theseus/bundled-addons/pdf-editor/lib/shape.js

233 lines
9.7 KiB
JavaScript
Raw Normal View History

feat(pdf-editor): a mark you placed is something you can still work on Everything the editor put on a page was final. A text stamp could not be corrected without deleting it and typing it again, nothing could be resized, and the only way to remove a mark was a Delete key nobody had been told about — the selection drew a dashed box and offered no action at all. Placing a stamp also left its tool armed, so the next click stamped a second copy. Marks are now editable objects. Selecting one gives it grab handles and a small bar pinned above it: delete and duplicate for anything, and for text an edit button, a size stepper and bold and italic. Double-clicking text reopens it for rewriting in place rather than adding a second one. Placing a text stamp or a signature drops straight back to the select tool with the new mark live, which is both what people expect and what puts it immediately within reach of a nudge. Resizing is one function over every mark type rather than a special case per kind: a handle drag produces a new bounding box, and the mark is mapped from its old box into that one. Text scales by font size instead of stretching its glyphs, signatures keep their aspect on a corner, and lines offer their two endpoints instead of a box that would let you stretch them in ways you never aimed at. A whole gesture lands on the undo stack as one step. Selecting a thin mark used to mean clicking its outline exactly — about one screen pixel. Each stroked mark now carries an invisible fat copy of itself purely to catch the pointer. New marks to go with it: underline and strike-through, which share the highlight's text-selection geometry and differ only in where the rule sits; a plain line; and a fill toggle for rectangles and ellipses. Bold and italic mean three more Helvetica variants embedded at save time, since a PDF treats them as separate fonts rather than as a style. Double-click is detected from the pointer stream rather than from a dblclick listener, because selecting a mark calls preventDefault() on the pointerdown and that suppresses the compatibility mouse events the browser would have synthesised the dblclick from.
2026-09-21 03:13:06 +02:00
// Geometry shared by the on-screen overlay and the pdf-lib writer.
//
// Both need to agree exactly on where an arrowhead's barbs land, where each
// line of a text stamp sits, and how a normalised signature stroke maps into
// its placed box. Keeping that arithmetic in one place is what makes "what I
// drew" and "what got saved" the same picture; two copies of it drift the
// first time either side is touched.
//
// Everything here works in PDF user space: points, origin bottom-left, y up.
/** Approximate ascent as a fraction of font size, for Helvetica. */
export const ASCENT = 0.76;
/** Line advance as a fraction of font size. */
export const LINE_GAP = 1.2;
feat(pdf-editor): replace the document's own text, on its own baseline Until now "editing" a PDF here meant laying things over it. You could put a word on top of a word, but the document underneath never changed, and the result read like a sticker because it was one. This adds the thing the word Edit actually promises: click a line of the document's text, type different words, and they land where the old ones were, in the old size and the old colour. The position and size come from pdf.js's text layer, which has already placed a span over every run and carries that run's size in unscaled PDF points — so the size is right whatever the zoom, which reading it off the rendered box would not be. The colours come from the rendered page, because nothing in the text API reports them: the background is the average of the most common colour bucket in the run's box, since type is a minority of the pixels even when it is dense, and the ink is whatever sits furthest from that background. On the test fixture it recovers the marker's red exactly. Two things that look like details and are not. The bucket only chooses WHICH pixels are background; the colour itself is their average, because rebuilding it from the bucket index rounds white down to #f8f8f8 and a not-quite-white patch on a white page is a visible seam. And the cover reaches below the baseline by a quarter of the font size, because pdf.js sizes its spans to the em box: cut the cover to the span and every descender in the original line survives as a little hook under the replacement. A replacement is a cover plus text, so it is a mark like any other — movable, resizable, undoable, and rendered on screen from the same numbers the writer uses, which is what makes the preview trustworthy. Said plainly in the dialog and again in the save summary: this hides the original, it does not remove it. The old glyphs are still in the content stream underneath. Redact is the tool that takes text away, and it says so too.
2026-09-22 21:49:37 +02:00
/**
* Where each line of a replaced text run is drawn.
*
* A "textedit" is a cover rectangle over the original run plus replacement
* text sitting on that run's own baseline so it is a text stamp whose origin
* came from the document rather than from a click. Sharing the line layout
* with textLayout keeps the two kinds rendering identically.
*/
export function editLayout(a) {
const lines = String(a.text ?? "").split("\n");
const size = a.size || 12;
return lines.map((text, i) => ({ text, x: a.x, y: a.baseline - i * size * LINE_GAP }));
}
feat(pdf-editor): a mark you placed is something you can still work on Everything the editor put on a page was final. A text stamp could not be corrected without deleting it and typing it again, nothing could be resized, and the only way to remove a mark was a Delete key nobody had been told about — the selection drew a dashed box and offered no action at all. Placing a stamp also left its tool armed, so the next click stamped a second copy. Marks are now editable objects. Selecting one gives it grab handles and a small bar pinned above it: delete and duplicate for anything, and for text an edit button, a size stepper and bold and italic. Double-clicking text reopens it for rewriting in place rather than adding a second one. Placing a text stamp or a signature drops straight back to the select tool with the new mark live, which is both what people expect and what puts it immediately within reach of a nudge. Resizing is one function over every mark type rather than a special case per kind: a handle drag produces a new bounding box, and the mark is mapped from its old box into that one. Text scales by font size instead of stretching its glyphs, signatures keep their aspect on a corner, and lines offer their two endpoints instead of a box that would let you stretch them in ways you never aimed at. A whole gesture lands on the undo stack as one step. Selecting a thin mark used to mean clicking its outline exactly — about one screen pixel. Each stroked mark now carries an invisible fat copy of itself purely to catch the pointer. New marks to go with it: underline and strike-through, which share the highlight's text-selection geometry and differ only in where the rule sits; a plain line; and a fill toggle for rectangles and ellipses. Bold and italic mean three more Helvetica variants embedded at save time, since a PDF treats them as separate fonts rather than as a style. Double-click is detected from the pointer stream rather than from a dblclick listener, because selecting a mark calls preventDefault() on the pointerdown and that suppresses the compatibility mouse events the browser would have synthesised the dblclick from.
2026-09-21 03:13:06 +02:00
/**
* Where each line of a text stamp is drawn. `a.y` is the TOP of the block
* that is what the user clicked so every baseline hangs below it.
* @returns {{text:string, x:number, y:number}[]} y is the baseline.
*/
export function textLayout(a) {
const lines = String(a.text ?? "").split("\n");
const size = a.size || 12;
return lines.map((text, i) => ({
text,
x: a.x,
y: a.y - size * ASCENT - i * size * LINE_GAP,
}));
}
/** Height of a text stamp's block, for hit-testing and the selection box. */
export function textBlockHeight(a) {
const lines = String(a.text ?? "").split("\n").length;
const size = a.size || 12;
return size * ASCENT + (lines - 1) * size * LINE_GAP + size * 0.24;
}
/**
* The two barb points of an arrowhead at (x2,y2) coming from (x1,y1).
* Head length scales with stroke width but is floored so a thin arrow still
* reads as an arrow.
*/
export function arrowBarbs(x1, y1, x2, y2, width) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.hypot(dx, dy) || 1;
const head = Math.max(6, Math.min(len * 0.34, (width || 2) * 4.5));
const ux = dx / len, uy = dy / len;
// 24° either side of the shaft.
const cos = Math.cos(0.42), sin = Math.sin(0.42);
return [
{ x: x2 - head * (ux * cos - uy * sin), y: y2 - head * (uy * cos + ux * sin) },
{ x: x2 - head * (ux * cos + uy * sin), y: y2 - head * (uy * cos - ux * sin) },
];
}
/**
* A placed signature's strokes in user space. Strokes are stored normalised
* (0..1, y measured DOWN from the top of the pad) so the same drawing can be
* dropped at any size, on any page, any number of times.
*/
export function signatureStrokes(a) {
const { x, y, w, h } = a;
return (a.strokes || []).map((s) => s.map(([nx, ny]) => [x + nx * w, y + (1 - ny) * h]));
}
/** Normalise a box that may have been dragged right-to-left or bottom-to-top. */
export function normBox(x0, y0, x1, y1) {
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
}
/**
* Where a rule sits inside a line's rectangle, as a fraction of its height
* from the baseline side. Shared by the renderer and the writer so that what
* is on screen and what lands in the file are the same picture two copies of
* this number is two pictures the first time either is touched.
*/
export const RULE_OFFSET = { underline: 0.06, strikeout: 0.42 };
/** Marks whose geometry is a list of text-line rectangles. */
export const RECT_LIST_KINDS = new Set(["highlight", "underline", "strikeout"]);
/** Marks whose geometry is a single box. */
feat(pdf-editor): replace the document's own text, on its own baseline Until now "editing" a PDF here meant laying things over it. You could put a word on top of a word, but the document underneath never changed, and the result read like a sticker because it was one. This adds the thing the word Edit actually promises: click a line of the document's text, type different words, and they land where the old ones were, in the old size and the old colour. The position and size come from pdf.js's text layer, which has already placed a span over every run and carries that run's size in unscaled PDF points — so the size is right whatever the zoom, which reading it off the rendered box would not be. The colours come from the rendered page, because nothing in the text API reports them: the background is the average of the most common colour bucket in the run's box, since type is a minority of the pixels even when it is dense, and the ink is whatever sits furthest from that background. On the test fixture it recovers the marker's red exactly. Two things that look like details and are not. The bucket only chooses WHICH pixels are background; the colour itself is their average, because rebuilding it from the bucket index rounds white down to #f8f8f8 and a not-quite-white patch on a white page is a visible seam. And the cover reaches below the baseline by a quarter of the font size, because pdf.js sizes its spans to the em box: cut the cover to the span and every descender in the original line survives as a little hook under the replacement. A replacement is a cover plus text, so it is a mark like any other — movable, resizable, undoable, and rendered on screen from the same numbers the writer uses, which is what makes the preview trustworthy. Said plainly in the dialog and again in the save summary: this hides the original, it does not remove it. The old glyphs are still in the content stream underneath. Redact is the tool that takes text away, and it says so too.
2026-09-22 21:49:37 +02:00
export const BOX_KINDS = new Set(["rect", "ellipse", "redact", "signature", "textedit"]);
feat(pdf-editor): a mark you placed is something you can still work on Everything the editor put on a page was final. A text stamp could not be corrected without deleting it and typing it again, nothing could be resized, and the only way to remove a mark was a Delete key nobody had been told about — the selection drew a dashed box and offered no action at all. Placing a stamp also left its tool armed, so the next click stamped a second copy. Marks are now editable objects. Selecting one gives it grab handles and a small bar pinned above it: delete and duplicate for anything, and for text an edit button, a size stepper and bold and italic. Double-clicking text reopens it for rewriting in place rather than adding a second one. Placing a text stamp or a signature drops straight back to the select tool with the new mark live, which is both what people expect and what puts it immediately within reach of a nudge. Resizing is one function over every mark type rather than a special case per kind: a handle drag produces a new bounding box, and the mark is mapped from its old box into that one. Text scales by font size instead of stretching its glyphs, signatures keep their aspect on a corner, and lines offer their two endpoints instead of a box that would let you stretch them in ways you never aimed at. A whole gesture lands on the undo stack as one step. Selecting a thin mark used to mean clicking its outline exactly — about one screen pixel. Each stroked mark now carries an invisible fat copy of itself purely to catch the pointer. New marks to go with it: underline and strike-through, which share the highlight's text-selection geometry and differ only in where the rule sits; a plain line; and a fill toggle for rectangles and ellipses. Bold and italic mean three more Helvetica variants embedded at save time, since a PDF treats them as separate fonts rather than as a style. Double-click is detected from the pointer stream rather than from a dblclick listener, because selecting a mark calls preventDefault() on the pointerdown and that suppresses the compatibility mouse events the browser would have synthesised the dblclick from.
2026-09-21 03:13:06 +02:00
/** Marks defined by two endpoints. */
export const SEGMENT_KINDS = new Set(["arrow", "line"]);
export function boundsOf(a) {
if (RECT_LIST_KINDS.has(a.kind)) {
const rs = a.rects || [];
if (!rs.length) return { x: 0, y: 0, w: 0, h: 0 };
const x0 = Math.min(...rs.map((r) => r.x)), y0 = Math.min(...rs.map((r) => r.y));
const x1 = Math.max(...rs.map((r) => r.x + r.w)), y1 = Math.max(...rs.map((r) => r.y + r.h));
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
}
if (SEGMENT_KINDS.has(a.kind)) return normBox(a.x1, a.y1, a.x2, a.y2);
switch (a.kind) {
case "pen": {
const pts = a.pts || [];
if (!pts.length) return { x: 0, y: 0, w: 0, h: 0 };
const xs = pts.map((p) => p[0]), ys = pts.map((p) => p[1]);
const pad = (a.width || 2) / 2;
return { x: Math.min(...xs) - pad, y: Math.min(...ys) - pad,
w: Math.max(...xs) - Math.min(...xs) + pad * 2, h: Math.max(...ys) - Math.min(...ys) + pad * 2 };
}
case "text": {
// Width is unknown without measuring the font; the renderer measures it
// and caches it on the mark as `_w` so selection boxes are tight.
return { x: a.x, y: a.y - textBlockHeight(a), w: a._w || (a.size || 12) * 6, h: textBlockHeight(a) };
}
default: return { x: a.x, y: a.y, w: a.w, h: a.h };
}
}
/** Shift a mark by (dx,dy) in user space. Returns the patch, does not mutate. */
export function translatePatch(a, dx, dy) {
if (RECT_LIST_KINDS.has(a.kind)) {
return { rects: (a.rects || []).map((r) => ({ ...r, x: r.x + dx, y: r.y + dy })) };
}
if (SEGMENT_KINDS.has(a.kind)) {
return { x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
}
if (a.kind === "pen") return { pts: (a.pts || []).map(([x, y]) => [x + dx, y + dy]) };
feat(pdf-editor): replace the document's own text, on its own baseline Until now "editing" a PDF here meant laying things over it. You could put a word on top of a word, but the document underneath never changed, and the result read like a sticker because it was one. This adds the thing the word Edit actually promises: click a line of the document's text, type different words, and they land where the old ones were, in the old size and the old colour. The position and size come from pdf.js's text layer, which has already placed a span over every run and carries that run's size in unscaled PDF points — so the size is right whatever the zoom, which reading it off the rendered box would not be. The colours come from the rendered page, because nothing in the text API reports them: the background is the average of the most common colour bucket in the run's box, since type is a minority of the pixels even when it is dense, and the ink is whatever sits furthest from that background. On the test fixture it recovers the marker's red exactly. Two things that look like details and are not. The bucket only chooses WHICH pixels are background; the colour itself is their average, because rebuilding it from the bucket index rounds white down to #f8f8f8 and a not-quite-white patch on a white page is a visible seam. And the cover reaches below the baseline by a quarter of the font size, because pdf.js sizes its spans to the em box: cut the cover to the span and every descender in the original line survives as a little hook under the replacement. A replacement is a cover plus text, so it is a mark like any other — movable, resizable, undoable, and rendered on screen from the same numbers the writer uses, which is what makes the preview trustworthy. Said plainly in the dialog and again in the save summary: this hides the original, it does not remove it. The old glyphs are still in the content stream underneath. Redact is the tool that takes text away, and it says so too.
2026-09-22 21:49:37 +02:00
if (a.kind === "textedit") return { x: a.x + dx, y: a.y + dy, baseline: a.baseline + dy };
feat(pdf-editor): a mark you placed is something you can still work on Everything the editor put on a page was final. A text stamp could not be corrected without deleting it and typing it again, nothing could be resized, and the only way to remove a mark was a Delete key nobody had been told about — the selection drew a dashed box and offered no action at all. Placing a stamp also left its tool armed, so the next click stamped a second copy. Marks are now editable objects. Selecting one gives it grab handles and a small bar pinned above it: delete and duplicate for anything, and for text an edit button, a size stepper and bold and italic. Double-clicking text reopens it for rewriting in place rather than adding a second one. Placing a text stamp or a signature drops straight back to the select tool with the new mark live, which is both what people expect and what puts it immediately within reach of a nudge. Resizing is one function over every mark type rather than a special case per kind: a handle drag produces a new bounding box, and the mark is mapped from its old box into that one. Text scales by font size instead of stretching its glyphs, signatures keep their aspect on a corner, and lines offer their two endpoints instead of a box that would let you stretch them in ways you never aimed at. A whole gesture lands on the undo stack as one step. Selecting a thin mark used to mean clicking its outline exactly — about one screen pixel. Each stroked mark now carries an invisible fat copy of itself purely to catch the pointer. New marks to go with it: underline and strike-through, which share the highlight's text-selection geometry and differ only in where the rule sits; a plain line; and a fill toggle for rectangles and ellipses. Bold and italic mean three more Helvetica variants embedded at save time, since a PDF treats them as separate fonts rather than as a style. Double-click is detected from the pointer stream rather than from a dblclick listener, because selecting a mark calls preventDefault() on the pointerdown and that suppresses the compatibility mouse events the browser would have synthesised the dblclick from.
2026-09-21 03:13:06 +02:00
return { x: a.x + dx, y: a.y + dy };
}
// ---- resizing ---------------------------------------------------------
//
// Handles are named by the corner or edge they sit on, in screen terms: "nw"
// is top-left as the reader sees it. User space has y running the other way,
// so "n" moves the box's y + h and "s" moves its y. Keeping the names in
// reader terms means the cursor CSS and the drag maths agree with what the
// user is doing with the mouse.
export const BOX_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
export const CORNER_HANDLES = ["nw", "ne", "se", "sw"];
/** Smallest box a resize may produce, in points. */
export const MIN_EXTENT = 4;
export const MIN_FONT = 4;
export const MAX_FONT = 288;
/** Which handles a mark offers. Segments get their two endpoints instead. */
export function handlesFor(a) {
if (SEGMENT_KINDS.has(a.kind)) return ["p1", "p2"];
if (a.kind === "text") return CORNER_HANDLES; // uniform scale only
return BOX_HANDLES;
}
/**
* The box a drag of `handle` produces, given the mark's current bounds and a
* user-space delta. Anchors the opposite corner or edge, and never inverts:
* dragging the top edge past the bottom stops at the minimum rather than
* flipping the mark over, which is disorienting mid-gesture.
*/
export function resizeBox(b, handle, dx, dy, { uniform = false } = {}) {
let { x, y, w, h } = b;
const east = handle.includes("e"), west = handle.includes("w");
const north = handle.includes("n"), south = handle.includes("s");
if (uniform && (east || west) && (north || south)) {
// Corner drag on a uniform mark: one scale factor, taken from whichever
// axis moved further, so the mark never distorts.
const sx = (w + (east ? dx : -dx)) / (w || 1);
const sy = (h + (north ? dy : -dy)) / (h || 1);
const k = Math.max(MIN_EXTENT / Math.max(w, h, 1), Math.abs(sx) > Math.abs(sy) ? sx : sy);
const nw = Math.max(MIN_EXTENT, w * k), nh = Math.max(MIN_EXTENT, h * k);
return {
x: west ? x + w - nw : x,
y: south ? y + h - nh : y,
w: nw, h: nh,
};
}
if (east) w = Math.max(MIN_EXTENT, w + dx);
if (west) { const nw2 = Math.max(MIN_EXTENT, w - dx); x = x + w - nw2; w = nw2; }
if (north) h = Math.max(MIN_EXTENT, h + dy);
if (south) { const nh2 = Math.max(MIN_EXTENT, h - dy); y = y + h - nh2; h = nh2; }
return { x, y, w, h };
}
/**
* Map a mark from one bounding box to another. One function for every kind,
* so a new mark type gets resizing for free as long as boundsOf knows it.
*/
export function scaleToBox(a, from, to) {
const sx = from.w > 0.001 ? to.w / from.w : 1;
const sy = from.h > 0.001 ? to.h / from.h : 1;
const fx = (x) => to.x + (x - from.x) * sx;
const fy = (y) => to.y + (y - from.y) * sy;
if (RECT_LIST_KINDS.has(a.kind)) {
return { rects: (a.rects || []).map((r) => ({ x: fx(r.x), y: fy(r.y), w: r.w * sx, h: r.h * sy })) };
}
if (SEGMENT_KINDS.has(a.kind)) {
return { x1: fx(a.x1), y1: fy(a.y1), x2: fx(a.x2), y2: fy(a.y2) };
}
if (a.kind === "pen") {
return { pts: (a.pts || []).map(([x, y]) => [fx(x), fy(y)]) };
}
if (a.kind === "text") {
// Text scales by font size, not by stretching glyphs. `y` is the top of
// the block, which is where the new box's top is.
const size = clampFont((a.size || 12) * (sy || 1));
return { size, x: to.x, y: to.y + to.h };
}
feat(pdf-editor): replace the document's own text, on its own baseline Until now "editing" a PDF here meant laying things over it. You could put a word on top of a word, but the document underneath never changed, and the result read like a sticker because it was one. This adds the thing the word Edit actually promises: click a line of the document's text, type different words, and they land where the old ones were, in the old size and the old colour. The position and size come from pdf.js's text layer, which has already placed a span over every run and carries that run's size in unscaled PDF points — so the size is right whatever the zoom, which reading it off the rendered box would not be. The colours come from the rendered page, because nothing in the text API reports them: the background is the average of the most common colour bucket in the run's box, since type is a minority of the pixels even when it is dense, and the ink is whatever sits furthest from that background. On the test fixture it recovers the marker's red exactly. Two things that look like details and are not. The bucket only chooses WHICH pixels are background; the colour itself is their average, because rebuilding it from the bucket index rounds white down to #f8f8f8 and a not-quite-white patch on a white page is a visible seam. And the cover reaches below the baseline by a quarter of the font size, because pdf.js sizes its spans to the em box: cut the cover to the span and every descender in the original line survives as a little hook under the replacement. A replacement is a cover plus text, so it is a mark like any other — movable, resizable, undoable, and rendered on screen from the same numbers the writer uses, which is what makes the preview trustworthy. Said plainly in the dialog and again in the save summary: this hides the original, it does not remove it. The old glyphs are still in the content stream underneath. Redact is the tool that takes text away, and it says so too.
2026-09-22 21:49:37 +02:00
if (a.kind === "textedit") {
// The cover follows the box; the baseline rides with it so the
// replacement keeps sitting where the original line sat.
return {
x: to.x, y: to.y, w: Math.max(MIN_EXTENT, to.w), h: Math.max(MIN_EXTENT, to.h),
size: clampFont((a.size || 12) * (sy || 1)),
baseline: fy(a.baseline),
};
}
feat(pdf-editor): a mark you placed is something you can still work on Everything the editor put on a page was final. A text stamp could not be corrected without deleting it and typing it again, nothing could be resized, and the only way to remove a mark was a Delete key nobody had been told about — the selection drew a dashed box and offered no action at all. Placing a stamp also left its tool armed, so the next click stamped a second copy. Marks are now editable objects. Selecting one gives it grab handles and a small bar pinned above it: delete and duplicate for anything, and for text an edit button, a size stepper and bold and italic. Double-clicking text reopens it for rewriting in place rather than adding a second one. Placing a text stamp or a signature drops straight back to the select tool with the new mark live, which is both what people expect and what puts it immediately within reach of a nudge. Resizing is one function over every mark type rather than a special case per kind: a handle drag produces a new bounding box, and the mark is mapped from its old box into that one. Text scales by font size instead of stretching its glyphs, signatures keep their aspect on a corner, and lines offer their two endpoints instead of a box that would let you stretch them in ways you never aimed at. A whole gesture lands on the undo stack as one step. Selecting a thin mark used to mean clicking its outline exactly — about one screen pixel. Each stroked mark now carries an invisible fat copy of itself purely to catch the pointer. New marks to go with it: underline and strike-through, which share the highlight's text-selection geometry and differ only in where the rule sits; a plain line; and a fill toggle for rectangles and ellipses. Bold and italic mean three more Helvetica variants embedded at save time, since a PDF treats them as separate fonts rather than as a style. Double-click is detected from the pointer stream rather than from a dblclick listener, because selecting a mark calls preventDefault() on the pointerdown and that suppresses the compatibility mouse events the browser would have synthesised the dblclick from.
2026-09-21 03:13:06 +02:00
return { x: to.x, y: to.y, w: Math.max(MIN_EXTENT, to.w), h: Math.max(MIN_EXTENT, to.h) };
}
export function clampFont(n) {
return Math.min(MAX_FONT, Math.max(MIN_FONT, Math.round(n * 10) / 10));
}