From ea551d7a66b57d23e512cecf7d0680df5606e5c1 Mon Sep 17 00:00:00 2001 From: Cestercian Date: Thu, 29 Oct 2026 07:37:04 +0000 Subject: [PATCH] Don't shift FreeText clones by their font size (issue 22271) A cloned FreeText editor is positioned from its serialized rect, which already includes the baseline shift. getInitialTranslation() applied that shift again, and _moveAfterPaste() returns early for clones, so the editor moved up by its font size. Keep the padding compensation that getPDFRect() adds back, and skip only the font-size part. --- src/display/editor/freetext.js | 13 +++++-- test/integration/freetext_editor_spec.mjs | 45 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index cddc7d9b8dec6..0d983cffe1fbb 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -276,10 +276,15 @@ class FreeTextEditor extends AnnotationEditor { getInitialTranslation() { // The start of the base line is where the user clicked. const scale = this.parentScale; - return [ - -FreeTextEditor._internalPadding * scale, - -(FreeTextEditor._internalPadding + this.#fontSize) * scale, - ]; + const padding = FreeTextEditor._internalPadding * scale; + let fontSize = this.#fontSize; + if (this.isClone) { + // The serialized rect already includes this baseline shift. Applying it + // again moves the clone up by its font size. The padding must stay so + // that `getPDFRect()` round-trips (issue 22271). + fontSize = 0; + } + return [-padding, -(padding + fontSize * scale)]; } /** @inheritdoc */ diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 0eb382d9c0ed3..7dc88d56ca9f0 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -486,6 +486,51 @@ describe("FreeText Editor", () => { }); }); + describe("FreeText (clone)", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 100); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must keep the same rect when a FreeText editor is cloned", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToFreeText(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Clone me", + }); + + // Same path as AnnotationEditorUIManager.findClonesForPage(), used + // when a page is copied with enableSplitMerge. + const rects = await page.evaluate(async () => { + const ui = + window.PDFViewerApplication.pdfViewer._layerProperties + .annotationEditorUIManager; + const [editor] = ui.getEditors(0); + const data = editor.serialize(true); + const layer = ui.getLayer(0); + const clone = await layer.deserialize(data); + clone.isClone = true; + layer.addOrRebuild(clone); + return [data.rect, clone.serialize(true).rect]; + }); + + expect(rects[1]).withContext(`In ${browserName}`).toEqual(rects[0]); + }) + ); + }); + }); + describe("FreeText (multiselection)", () => { let pages;