From 2d8dfb0b6280210a59d97c5926c628b322c7b453 Mon Sep 17 00:00:00 2001 From: maettuu Date: Wed, 6 Aug 2025 12:35:49 +0200 Subject: [PATCH 01/26] Add regression test for Unicode mapping Verifies that ToUnicodeMap correctly maps Extension B characters to their full Unicode code points using codePointAt See PR https://github.com/mozilla/pdf.js/pull/19184 --- test/unit/clitests.json | 1 + test/unit/to_unicode_map_spec.js | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 test/unit/to_unicode_map_spec.js diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 1328b6124..1440845f8 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -46,6 +46,7 @@ "struct_tree_spec.js", "svg_factory_spec.js", "text_layer_spec.js", + "to_unicode_map_spec.js", "type1_parser_spec.js", "ui_utils_spec.js", "unicode_spec.js", diff --git a/test/unit/to_unicode_map_spec.js b/test/unit/to_unicode_map_spec.js new file mode 100644 index 000000000..706718bdd --- /dev/null +++ b/test/unit/to_unicode_map_spec.js @@ -0,0 +1,33 @@ +/* Copyright 2025 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ToUnicodeMap } from "../../src/core/to_unicode_map.js"; + +describe("ToUnicodeMap", () => { + it("should correctly map Extension B characters using codePointAt", () => { + const cmap = { 0x20: "\uD840\uDC00" }; // Example Extension B character + const toUnicodeMap = new ToUnicodeMap(cmap); + + const expected = 0x20000; // Unicode code point for the character + let actual; + toUnicodeMap.forEach((charCode, unicode) => { + if (charCode === (0x20).toString()) { + actual = unicode; + } + }); + + expect(actual).toBe(expected); + }); +}); From a932804fb58f01a2cf794d02aad5a36a33b38cec Mon Sep 17 00:00:00 2001 From: legraina Date: Tue, 2 Sep 2025 21:11:04 -0400 Subject: [PATCH 02/26] A new CurrentPointers class to store current pointers used by the editor Move current pointer field of DrawingEditor to CurrentPointer class in tools.js: The pointer types fields have been moved to a CurrentPointer object in tools.js. This object is used by eraser.js and ink.js. Only reset pointer type when user select a new mode: Clear the pointer type when changing mode, instead of at the end of the session. It seems more stable, as the method is not called this way when the user changes pages. Also, clear the pointer type when the mode is changed by an event (the user changes the editor type), otherwise, the same pointer type is kept (the document is changed for example) --- src/display/editor/draw.js | 56 +++++++++---------------- src/display/editor/tools.js | 82 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 37 deletions(-) diff --git a/src/display/editor/draw.js b/src/display/editor/draw.js index bcff3f228..3ca7dfcfa 100644 --- a/src/display/editor/draw.js +++ b/src/display/editor/draw.js @@ -16,6 +16,7 @@ import { AnnotationEditorParamsType, unreachable } from "../../shared/util.js"; import { noContextMenu, stopEvent } from "../display_utils.js"; import { AnnotationEditor } from "./editor.js"; +import { CurrentPointers } from "./tools.js"; class DrawingOptions { #svgProperties = Object.create(null); @@ -81,14 +82,6 @@ class DrawingEditor extends AnnotationEditor { static #currentDrawingOptions = null; - static #currentPointerId = NaN; - - static #currentPointerType = null; - - static #currentPointerIds = null; - - static #currentMoveTimestamp = NaN; - static _INNER_MARGIN = 3; constructor(params) { @@ -678,20 +671,15 @@ class DrawingEditor extends AnnotationEditor { } static startDrawing(parent, uiManager, _isLTR, event) { - // The _currentPointerType is set when the user starts an empty drawing - // session. If, in the same drawing session, the user starts using a + // The pointerType of CurrentPointer is set when the user starts an empty + // drawing session. If, in the same drawing session, the user starts using a // different type of pointer (e.g. a pen and then a finger), we just return. // - // The _currentPointerId and _currentPointerIds are used to keep track of - // the pointers with a same type (e.g. two fingers). If the user starts to - // draw with a finger and then uses a second finger, we just stop the - // current drawing and let the user zoom the document. + // If the user starts to draw with a finger and then uses a second finger, + // we just stop the current drawing and let the user zoom the document. const { target, offsetX: x, offsetY: y, pointerId, pointerType } = event; - if ( - DrawingEditor.#currentPointerType && - DrawingEditor.#currentPointerType !== pointerType - ) { + if (CurrentPointers.isInitializedAndDifferentPointerType(pointerType)) { return; } @@ -704,16 +692,13 @@ class DrawingEditor extends AnnotationEditor { const ac = (DrawingEditor.#currentDrawingAC = new AbortController()); const signal = parent.combinedSignal(ac); - DrawingEditor.#currentPointerId ||= pointerId; - DrawingEditor.#currentPointerType ??= pointerType; + CurrentPointers.setPointer(pointerType, pointerId); window.addEventListener( "pointerup", e => { - if (DrawingEditor.#currentPointerId === e.pointerId) { + if (CurrentPointers.isSamePointerIdOrRemove(e.pointerId)) { this._endDraw(e); - } else { - DrawingEditor.#currentPointerIds?.delete(e.pointerId); } }, { signal } @@ -721,10 +706,8 @@ class DrawingEditor extends AnnotationEditor { window.addEventListener( "pointercancel", e => { - if (DrawingEditor.#currentPointerId === e.pointerId) { + if (CurrentPointers.isSamePointerIdOrRemove(e.pointerId)) { this._currentParent.endDrawingSession(); - } else { - DrawingEditor.#currentPointerIds?.delete(e.pointerId); } }, { signal } @@ -732,14 +715,14 @@ class DrawingEditor extends AnnotationEditor { window.addEventListener( "pointerdown", e => { - if (DrawingEditor.#currentPointerType !== e.pointerType) { + if (!CurrentPointers.isSamePointerType(e.pointerType)) { // For example, we started with a pen and the user // is now using a finger. return; } // For example, the user is using a second finger. - (DrawingEditor.#currentPointerIds ||= new Set()).add(e.pointerId); + CurrentPointers.initializeAndAddPointerId(e.pointerId); // The first finger created a first point and a second finger just // started, so we stop the drawing and remove this only point. @@ -765,7 +748,7 @@ class DrawingEditor extends AnnotationEditor { target.addEventListener( "touchmove", e => { - if (e.timeStamp === DrawingEditor.#currentMoveTimestamp) { + if (CurrentPointers.isSameTimeStamp(e.timeStamp)) { // This move event is used to draw so we don't want to scroll. stopEvent(e); } @@ -812,16 +795,16 @@ class DrawingEditor extends AnnotationEditor { } static _drawMove(event) { - DrawingEditor.#currentMoveTimestamp = -1; + CurrentPointers.isSameTimeStamp(event.timeStamp); if (!DrawingEditor.#currentDraw) { return; } const { offsetX, offsetY, pointerId } = event; - if (DrawingEditor.#currentPointerId !== pointerId) { + if (!CurrentPointers.isSamePointerId(pointerId)) { return; } - if (DrawingEditor.#currentPointerIds?.size >= 1) { + if (CurrentPointers.isUsingMultiplePointers()) { // The user is using multiple fingers and the first one is moving. this._endDraw(event); return; @@ -831,7 +814,7 @@ class DrawingEditor extends AnnotationEditor { DrawingEditor.#currentDraw.add(offsetX, offsetY) ); // We track the timestamp to know if the touchmove event is used to draw. - DrawingEditor.#currentMoveTimestamp = event.timeStamp; + CurrentPointers.setTimeStamp(event.timeStamp); stopEvent(event); } @@ -841,15 +824,14 @@ class DrawingEditor extends AnnotationEditor { this._currentParent = null; DrawingEditor.#currentDraw = null; DrawingEditor.#currentDrawingOptions = null; - DrawingEditor.#currentPointerType = null; - DrawingEditor.#currentMoveTimestamp = NaN; + CurrentPointers.clearPointerType(); + CurrentPointers.clearTimeStamp(); } if (DrawingEditor.#currentDrawingAC) { DrawingEditor.#currentDrawingAC.abort(); DrawingEditor.#currentDrawingAC = null; - DrawingEditor.#currentPointerId = NaN; - DrawingEditor.#currentPointerIds = null; + CurrentPointers.clearPointerIds(); } } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 1d1212a53..3fcb1f84b 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -42,6 +42,87 @@ function bindEvents(obj, element, names) { } } +/** + * Class to store current pointers used by the editor to be able to handle + * multiple pointers (e.g. two fingers, a pen, a mouse, ...). + */ +class CurrentPointers { + // To manage the pointer events. + + // The pointerId and pointerIds are used to keep track of + // the pointers with a same type (e.g. two fingers). + static #pointerId = NaN; + + static #pointerIds = null; + + // Track the timestamp to know if the touchmove event is used. + static #moveTimestamp = NaN; + + // The pointerType is used to know if we are using a mouse, a pen or a touch. + static #pointerType = null; + + static initializeAndAddPointerId(pointerId) { + // Store pointer ids. For example, the user is using a second finger. + (CurrentPointers.#pointerIds ||= new Set()).add(pointerId); + } + + static setPointer(pointerType, pointerId) { + CurrentPointers.#pointerId ||= pointerId; + CurrentPointers.#pointerType ??= pointerType; + } + + static setTimeStamp(timeStamp) { + CurrentPointers.#moveTimestamp = timeStamp; + } + + static isSamePointerId(pointerId) { + return CurrentPointers.#pointerId === pointerId; + } + + // Check if it's the same pointer id, otherwise remove it from the set. + static isSamePointerIdOrRemove(pointerId) { + if (CurrentPointers.#pointerId === pointerId) { + return true; + } + + CurrentPointers.#pointerIds?.delete(pointerId); + return false; + } + + static isSamePointerType(pointerType) { + return CurrentPointers.#pointerType === pointerType; + } + + static isInitializedAndDifferentPointerType(pointerType) { + return ( + CurrentPointers.#pointerType !== null && + !CurrentPointers.isSamePointerType(pointerType) + ); + } + + static isSameTimeStamp(timeStamp) { + return CurrentPointers.#moveTimestamp === timeStamp; + } + + static isUsingMultiplePointers() { + // Check if the user is using multiple fingers + return CurrentPointers.#pointerIds?.size >= 1; + } + + static clearPointerType() { + CurrentPointers.#pointerType = null; + } + + static clearPointerIds() { + CurrentPointers.#pointerId = NaN; + CurrentPointers.#pointerIds = null; + } + + static clearTimeStamp() { + CurrentPointers.#moveTimestamp = NaN; + } +} + /** * Class to create some unique ids for the different editors. */ @@ -2801,5 +2882,6 @@ export { bindEvents, ColorManager, CommandManager, + CurrentPointers, KeyboardManager, }; From aeceee1df3362de4c22e834da230ba408baaae5c Mon Sep 17 00:00:00 2001 From: calixteman Date: Wed, 29 Oct 2025 15:41:34 +0100 Subject: [PATCH 03/26] Revert "Add some telemetry in order to know what are the certificates used in pdfs (bug 1973573)" --- src/core/document.js | 62 -------------------------------------------- web/app.js | 7 ----- 2 files changed, 69 deletions(-) diff --git a/src/core/document.js b/src/core/document.js index a3892751f..7bc738bb5 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -1167,49 +1167,6 @@ class PDFDocument { }); } - #collectSignatureCertificates( - fields, - collectedSignatureCertificates, - visited = new RefSet() - ) { - if (!Array.isArray(fields)) { - return; - } - for (let field of fields) { - if (field instanceof Ref) { - if (visited.has(field)) { - continue; - } - visited.put(field); - } - field = this.xref.fetchIfRef(field); - if (!(field instanceof Dict)) { - continue; - } - if (field.has("Kids")) { - this.#collectSignatureCertificates( - field.get("Kids"), - collectedSignatureCertificates, - visited - ); - continue; - } - const isSignature = isName(field.get("FT"), "Sig"); - if (!isSignature) { - continue; - } - const value = field.get("V"); - if (!(value instanceof Dict)) { - continue; - } - const subFilter = value.get("SubFilter"); - if (!(subFilter instanceof Name)) { - continue; - } - collectedSignatureCertificates.add(subFilter.name); - } - } - get _xfaStreams() { const { acroForm } = this.catalog; if (!acroForm) { @@ -1525,20 +1482,6 @@ class PDFDocument { // specification). const sigFlags = acroForm.get("SigFlags"); const hasSignatures = !!(sigFlags & 0x1); - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - if (hasSignatures) { - const collectedSignatureCertificates = new Set(); - this.#collectSignatureCertificates( - fields, - collectedSignatureCertificates - ); - if (collectedSignatureCertificates.size > 0) { - formInfo.collectedSignatureCertificates = Array.from( - collectedSignatureCertificates - ); - } - } - } const hasOnlyDocumentSignatures = hasSignatures && this.#hasOnlyDocumentSignatures(fields); formInfo.hasAcroForm = hasFields && !hasOnlyDocumentSignatures; @@ -1566,11 +1509,6 @@ class PDFDocument { IsSignaturesPresent: formInfo.hasSignatures, }; - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - docInfo.collectedSignatureCertificates = - formInfo.collectedSignatureCertificates ?? null; - } - let infoDict; try { infoDict = xref.trailer.get("Info"); diff --git a/web/app.js b/web/app.js index 2dc66b6be..1f932d51a 100644 --- a/web/app.js +++ b/web/app.js @@ -1718,13 +1718,6 @@ const PDFViewerApplication = { if (pdfDocument !== this.pdfDocument) { return; // The document was closed while the metadata resolved. } - if (info.collectedSignatureCertificates) { - this.externalServices.reportTelemetry({ - type: "signatureCertificates", - data: info.collectedSignatureCertificates, - }); - } - this.documentInfo = info; this.metadata = metadata; this._contentDispositionFilename ??= contentDispositionFilename; From 4c22b99df32ceafa65e296c7777adcc036731583 Mon Sep 17 00:00:00 2001 From: Edoardo Cavazza Date: Thu, 2 Oct 2025 10:53:19 +0200 Subject: [PATCH 04/26] Collect all child nodes of lists and tables --- src/core/struct_tree.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/core/struct_tree.js b/src/core/struct_tree.js index 539ae008f..fa2d6e98c 100644 --- a/src/core/struct_tree.js +++ b/src/core/struct_tree.js @@ -824,6 +824,23 @@ class StructTreePage { const element = new StructElementNode(this, dict); map.set(dict, element); + switch (element.role) { + case "L": + case "LBody": + case "LI": + case "Table": + case "THead": + case "TBody": + case "TFoot": + case "TR": { + // Always collect all child nodes of lists and tables, even empty ones + for (const kid of element.kids) { + if (kid.type === StructElementType.ELEMENT) { + this.addNode(kid.dict, map, level - 1); + } + } + } + } const parent = dict.get("P"); From d04832a82f23c45bac1d58a7b240dceca573c9d8 Mon Sep 17 00:00:00 2001 From: Edoardo Cavazza Date: Mon, 6 Oct 2025 09:57:02 +0200 Subject: [PATCH 05/26] Add test case for empty cells --- test/pdfs/.gitignore | 1 + test/pdfs/issue20324.pdf | Bin 0 -> 11151 bytes test/test_manifest.json | 6 ++++++ test/unit/api_spec.js | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 test/pdfs/issue20324.pdf diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 75b11be7d..ed64906db 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -620,6 +620,7 @@ !autoprint.pdf !bug1811694.pdf !bug1811510.pdf +!issue20324.pdf !bug1815476.pdf !issue16021.pdf !bug1770750.pdf diff --git a/test/pdfs/issue20324.pdf b/test/pdfs/issue20324.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c5f2a1d4ffc8d94defd614bbfce93627b0287046 GIT binary patch literal 11151 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 literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index 03d1f1d71..afc29a290 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1676,6 +1676,12 @@ "lastPage": 1, "type": "eq" }, + { "id": "issue20324", + "file": "pdfs/issue20324.pdf", + "md5": "13250232aa91444f983279581d9c02d6", + "rounds": 1, + "type": "eq" + }, { "id": "issue13561_reduced", "file": "pdfs/issue13561_reduced.pdf", diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index a8e0fbc07..9e975cb00 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -5076,6 +5076,41 @@ Caron Broadcasting, Inc., an Ohio corporation (โ€œLesseeโ€).`) canvasFactory.destroy(canvasAndCtx); await loadingTask.destroy(); }); + + it("should collect all list and table items in StructTree", async function() { + const findNodes = (node, check) => { + const results = []; + if (check(node)) { + results.push(node); + } + if (node.children) { + for (const child of node.children) { + results.push(...findNodes(child, check)); + } + } + return results; + }; + const loadingTask = getDocument(buildGetDocumentParams("issue20324.pdf")); + + const pdfDoc = await loadingTask.promise; + const page = await pdfDoc.getPage(1); + const tree = await page.getStructTree({ + includeMarkedContent: true, + }); + const cells = findNodes( + tree, + node => node.role === "TD" + ); + expect(cells.length).toEqual(4); + + const listItems = findNodes( + tree, + node => node.role === "LI" + ); + expect(listItems.length).toEqual(4); + + await loadingTask.destroy(); + }); }); describe("Multiple `getDocument` instances", function () { From 17cdd9b1e781a261c68ae697bc82abfa6ff896cb Mon Sep 17 00:00:00 2001 From: Edoardo Cavazza Date: Mon, 20 Oct 2025 09:47:21 +0200 Subject: [PATCH 06/26] Move tables test to specific struct tree spec file --- test/test_manifest.json | 6 ------ test/unit/api_spec.js | 35 ----------------------------------- test/unit/struct_tree_spec.js | 30 +++++++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 42 deletions(-) diff --git a/test/test_manifest.json b/test/test_manifest.json index afc29a290..03d1f1d71 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1676,12 +1676,6 @@ "lastPage": 1, "type": "eq" }, - { "id": "issue20324", - "file": "pdfs/issue20324.pdf", - "md5": "13250232aa91444f983279581d9c02d6", - "rounds": 1, - "type": "eq" - }, { "id": "issue13561_reduced", "file": "pdfs/issue13561_reduced.pdf", diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 9e975cb00..a8e0fbc07 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -5076,41 +5076,6 @@ Caron Broadcasting, Inc., an Ohio corporation (โ€œLesseeโ€).`) canvasFactory.destroy(canvasAndCtx); await loadingTask.destroy(); }); - - it("should collect all list and table items in StructTree", async function() { - const findNodes = (node, check) => { - const results = []; - if (check(node)) { - results.push(node); - } - if (node.children) { - for (const child of node.children) { - results.push(...findNodes(child, check)); - } - } - return results; - }; - const loadingTask = getDocument(buildGetDocumentParams("issue20324.pdf")); - - const pdfDoc = await loadingTask.promise; - const page = await pdfDoc.getPage(1); - const tree = await page.getStructTree({ - includeMarkedContent: true, - }); - const cells = findNodes( - tree, - node => node.role === "TD" - ); - expect(cells.length).toEqual(4); - - const listItems = findNodes( - tree, - node => node.role === "LI" - ); - expect(listItems.length).toEqual(4); - - await loadingTask.destroy(); - }); }); describe("Multiple `getDocument` instances", function () { diff --git a/test/unit/struct_tree_spec.js b/test/unit/struct_tree_spec.js index ca722b854..9b138dc70 100644 --- a/test/unit/struct_tree_spec.js +++ b/test/unit/struct_tree_spec.js @@ -258,7 +258,7 @@ describe("struct tree", function () { await loadingTask.destroy(); }); - it("parses structure with some MathML in MS Office specific entry", async function () { + it("parses structure with some MathML in MS Office specific entry", async function() { const filename = "bug1937438_from_word.pdf"; const params = buildGetDocumentParams(filename); const loadingTask = getDocument(params); @@ -300,6 +300,34 @@ describe("struct tree", function () { }, struct ); + }); + + it("should collect all list and table items in StructTree", async function () { + const findNodes = (node, check) => { + const results = []; + if (check(node)) { + results.push(node); + } + if (node.children) { + for (const child of node.children) { + results.push(...findNodes(child, check)); + } + } + return results; + }; + const loadingTask = getDocument(buildGetDocumentParams("issue20324.pdf")); + + const pdfDoc = await loadingTask.promise; + const page = await pdfDoc.getPage(1); + const tree = await page.getStructTree({ + includeMarkedContent: true, + }); + const cells = findNodes(tree, node => node.role === "TD"); + expect(cells.length).toEqual(4); + + const listItems = findNodes(tree, node => node.role === "LI"); + expect(listItems.length).toEqual(4); + await loadingTask.destroy(); }); }); From a932a063ea48d05f12e9fae25f53c21df13dd602 Mon Sep 17 00:00:00 2001 From: Edoardo Cavazza Date: Wed, 29 Oct 2025 17:34:34 +0100 Subject: [PATCH 07/26] Revert unwanted change --- test/unit/struct_tree_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/struct_tree_spec.js b/test/unit/struct_tree_spec.js index 9b138dc70..e762c4502 100644 --- a/test/unit/struct_tree_spec.js +++ b/test/unit/struct_tree_spec.js @@ -258,7 +258,7 @@ describe("struct tree", function () { await loadingTask.destroy(); }); - it("parses structure with some MathML in MS Office specific entry", async function() { + it("parses structure with some MathML in MS Office specific entry", async function () { const filename = "bug1937438_from_word.pdf"; const params = buildGetDocumentParams(filename); const loadingTask = getDocument(params); From 26360c3e63b464d2b699e549e3f2341f8b276da0 Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Tue, 21 Oct 2025 11:41:04 -0500 Subject: [PATCH 08/26] Add text extractor for an external service --- web/app.js | 7 +++ web/external_services.js | 2 + web/firefoxcom.js | 4 ++ web/pdf_text_extractor.js | 90 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 web/pdf_text_extractor.js diff --git a/web/app.js b/web/app.js index 2dc66b6be..cbb20755a 100644 --- a/web/app.js +++ b/web/app.js @@ -89,6 +89,7 @@ import { PDFPrintServiceFactory } from "web-print_service"; import { PDFRenderingQueue } from "./pdf_rendering_queue.js"; import { PDFScriptingManager } from "./pdf_scripting_manager.js"; import { PDFSidebar } from "web-pdf_sidebar"; +import { PdfTextExtractor } from "./pdf_text_extractor.js"; import { PDFThumbnailViewer } from "web-pdf_thumbnail_viewer"; import { PDFViewer } from "./pdf_viewer.js"; import { Preferences } from "web-preferences"; @@ -129,6 +130,8 @@ const PDFViewerApplication = { pdfDocumentProperties: null, /** @type {PDFLinkService} */ pdfLinkService: null, + /** @type {PdfTextExtractor|null} */ + pdfTextExtractor: null, /** @type {PDFHistory} */ pdfHistory: null, /** @type {PDFSidebar} */ @@ -262,6 +265,8 @@ const PDFViewerApplication = { } await this._initializeViewerComponents(); + this.pdfTextExtractor = new PdfTextExtractor(this.externalServices); + // Bind the various event handlers *after* the viewer has been // initialized, to prevent errors if an event arrives too soon. this.bindEvents(); @@ -1144,6 +1149,7 @@ const PDFViewerApplication = { this.pdfViewer.setDocument(null); this.pdfLinkService.setDocument(null); this.pdfDocumentProperties?.setDocument(null); + this.pdfTextExtractor?.setDocument(null); } this.pdfLinkService.externalLinkEnabled = true; this.store = null; @@ -1450,6 +1456,7 @@ const PDFViewerApplication = { const pdfViewer = this.pdfViewer; pdfViewer.setDocument(pdfDocument); + this.pdfTextExtractor.setViewer(pdfViewer); const { firstPagePromise, onePageRendered, pagesPromise } = pdfViewer; this.pdfThumbnailViewer?.setDocument(pdfDocument); diff --git a/web/external_services.js b/web/external_services.js index b9b199bd4..437223a94 100644 --- a/web/external_services.js +++ b/web/external_services.js @@ -33,6 +33,8 @@ class BaseExternalServices { reportTelemetry(data) {} + reportText(data) {} + /** * @returns {Promise} */ diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 40ff9e0c2..253965f16 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -645,6 +645,10 @@ class ExternalServices extends BaseExternalServices { FirefoxCom.request("reportTelemetry", data); } + reportText(data) { + FirefoxCom.request("reportText", data); + } + updateEditorStates(data) { FirefoxCom.request("updateEditorStates", data); } diff --git a/web/pdf_text_extractor.js b/web/pdf_text_extractor.js new file mode 100644 index 000000000..f8bbbb609 --- /dev/null +++ b/web/pdf_text_extractor.js @@ -0,0 +1,90 @@ +/* Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This class manages the interaction of extracting the text content of the page + * and passing it back to the external service. + */ +class PdfTextExtractor { + /** @type {PDFViewer} */ + #pdfViewer; + + #externalServices; + + /** + * @type {?Promise} + */ + #textPromise; + + #pendingRequests = new Set(); + + constructor(externalServices) { + this.#externalServices = externalServices; + + window.addEventListener("requestTextContent", ({ detail }) => { + this.extractTextContent(detail.requestId); + }); + } + + /** + * The PDF viewer is required to get the page text. + * + * @param {PDFViewer | null} + */ + setViewer(pdfViewer) { + this.#pdfViewer = pdfViewer; + if (this.#pdfViewer && this.#pendingRequests.size) { + // Handle any pending requests that came in while things were loading. + for (const pendingRequest of this.#pendingRequests) { + this.extractTextContent(pendingRequest); + } + this.#pendingRequests = new Set(); + } + } + + /** + * Builds up all of the text from a PDF. + * + * @param {number} requestId + */ + async extractTextContent(requestId) { + if (!this.#pdfViewer) { + this.#pendingRequests.add(requestId); + return; + } + + if (!this.#textPromise) { + const textPromise = this.#pdfViewer.getAllText(); + this.#textPromise = textPromise; + + // After the text resolves, cache the text for a little bit in case + // multiple consumers call it. + textPromise.then(() => { + setTimeout(() => { + if (this.#textPromise === textPromise) { + this.#textPromise = null; + } + }, 5000); + }); + } + + this.#externalServices.reportText({ + text: await this.#textPromise, + requestId, + }); + } +} + +export { PdfTextExtractor }; From b7708da368ea947db286cf48a50c6ddcfd0291fa Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Thu, 30 Oct 2025 09:48:46 -0500 Subject: [PATCH 09/26] Address review feedback --- web/app.js | 2 +- web/pdf_text_extractor.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/web/app.js b/web/app.js index cbb20755a..adea7ca2f 100644 --- a/web/app.js +++ b/web/app.js @@ -1149,7 +1149,7 @@ const PDFViewerApplication = { this.pdfViewer.setDocument(null); this.pdfLinkService.setDocument(null); this.pdfDocumentProperties?.setDocument(null); - this.pdfTextExtractor?.setDocument(null); + this.pdfTextExtractor?.setViewer(null); } this.pdfLinkService.externalLinkEnabled = true; this.store = null; diff --git a/web/pdf_text_extractor.js b/web/pdf_text_extractor.js index f8bbbb609..630e8d5b0 100644 --- a/web/pdf_text_extractor.js +++ b/web/pdf_text_extractor.js @@ -50,7 +50,7 @@ class PdfTextExtractor { for (const pendingRequest of this.#pendingRequests) { this.extractTextContent(pendingRequest); } - this.#pendingRequests = new Set(); + this.#pendingRequests.clear(); } } @@ -66,8 +66,7 @@ class PdfTextExtractor { } if (!this.#textPromise) { - const textPromise = this.#pdfViewer.getAllText(); - this.#textPromise = textPromise; + const textPromise = (this.#textPromise = this.#pdfViewer.getAllText()); // After the text resolves, cache the text for a little bit in case // multiple consumers call it. From 6db23139beb7464ceeb5bc78db5e05c0e0282f67 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 30 Oct 2025 15:44:05 +0100 Subject: [PATCH 10/26] Don't set the MathML namespace for attributes in MathML tags (bug 1997343) And by default a XML file is UTF-8 encoded so correctly decode the embedded file. --- src/core/struct_tree.js | 10 ++++-- test/integration/accessibility_spec.mjs | 40 ++++++++++++++++++++++++ test/pdfs/.gitignore | 1 + test/pdfs/bug1997343.pdf | Bin 0 -> 189463 bytes web/struct_tree_layer_builder.js | 5 +-- 5 files changed, 50 insertions(+), 6 deletions(-) create mode 100755 test/pdfs/bug1997343.pdf diff --git a/src/core/struct_tree.js b/src/core/struct_tree.js index fa2d6e98c..083230c2b 100644 --- a/src/core/struct_tree.js +++ b/src/core/struct_tree.js @@ -13,7 +13,12 @@ * limitations under the License. */ -import { AnnotationPrefix, stringToPDFString, warn } from "../shared/util.js"; +import { + AnnotationPrefix, + stringToPDFString, + stringToUTF8String, + warn, +} from "../shared/util.js"; import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js"; import { lookupNormalRect, stringToAsciiOrUTF16BE } from "./core_utils.js"; import { BaseStream } from "./base_stream.js"; @@ -610,7 +615,8 @@ class StructElementNode { if (!isName(fileStream.dict.get("Subtype"), "application/mathml+xml")) { continue; } - return fileStream.getString(); + // The default encoding for xml files is UTF-8. + return stringToUTF8String(fileStream.getString()); } const A = this.dict.get("A"); if (A instanceof Dict) { diff --git a/test/integration/accessibility_spec.mjs b/test/integration/accessibility_spec.mjs index 5bb2c7915..94472ddc8 100644 --- a/test/integration/accessibility_spec.mjs +++ b/test/integration/accessibility_spec.mjs @@ -346,6 +346,46 @@ describe("accessibility", () => { }); }); + describe("MathML with some attributes in AF entry from LaTeX", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("bug1997343.pdf", ".textLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that the MathML is correctly inserted", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const isSanitizerSupported = await page.evaluate(() => { + try { + // eslint-disable-next-line no-undef + return typeof Sanitizer !== "undefined"; + } catch { + return false; + } + }); + if (isSanitizerSupported) { + const mathML = await page.$eval( + "span.structTree span[aria-owns='p21R_mc64']", + el => el?.innerHTML ?? "" + ); + expect(mathML) + .withContext(`In ${browserName}`) + .toEqual( + ' ๐‘› ๐‘ = ๐‘› mod ๐‘ ' + ); + } else { + pending(`Sanitizer API (in ${browserName}) is not supported`); + } + }) + ); + }); + }); + describe("MathML tags in the struct tree", () => { let pages; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index ed64906db..1eb2dce1b 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -753,3 +753,4 @@ !bug1937438_af_from_latex.pdf !bug1937438_from_word.pdf !bug1937438_mml_from_latex.pdf +!bug1997343.pdf diff --git a/test/pdfs/bug1997343.pdf b/test/pdfs/bug1997343.pdf new file mode 100755 index 0000000000000000000000000000000000000000..378d0fc41c516f122c536ed82e041fc413b30dd8 GIT binary patch literal 189463 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�$Zsj2(<< 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 ({ - name, - namespace: MathMLNamespace, - })), + ], comments: false, }) : null From 1febf9e30f01e0a85bf3a7fc7158e6339643001c Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 2 Nov 2025 13:11:00 +0100 Subject: [PATCH 11/26] Bump the stable version in `pdfjs.config` --- pdfjs.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdfjs.config b/pdfjs.config index f918867f9..0cd755d28 100644 --- a/pdfjs.config +++ b/pdfjs.config @@ -1,5 +1,5 @@ { - "stableVersion": "5.4.296", + "stableVersion": "5.4.394", "baseVersion": "1b427a3af5e0a40c296a3cafb08edbd36d973ff1", "versionPrefix": "5.4." } From 04db38558ad48620ddb8143dee5d11e146d330d4 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 5 Nov 2025 17:09:43 +0100 Subject: [PATCH 12/26] Create the number tree for the ParentTree only one time --- src/core/struct_tree.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/struct_tree.js b/src/core/struct_tree.js index 083230c2b..cdd12dfa8 100644 --- a/src/core/struct_tree.js +++ b/src/core/struct_tree.js @@ -42,6 +42,7 @@ class StructTreeRoot { this.roleMap = new Map(); this.structParentIds = null; this.kidRefToPosition = undefined; + this.parentTree = null; } getKidPosition(kidRef) { @@ -70,6 +71,11 @@ class StructTreeRoot { init() { this.readRoleMap(); + const parentTree = this.dict.get("ParentTree"); + if (!parentTree) { + return; + } + this.parentTree = new NumberTree(parentTree, this.xref); } #addIdToPage(pageRef, id, type) { @@ -771,7 +777,7 @@ class StructTreePage { return; } - const parentTree = this.rootDict.get("ParentTree"); + const { parentTree } = this.root; if (!parentTree) { return; } @@ -782,10 +788,9 @@ class StructTreePage { } const map = new Map(); - const numberTree = new NumberTree(parentTree, this.xref); if (Number.isInteger(id)) { - const parentArray = numberTree.get(id); + const parentArray = parentTree.get(id); if (Array.isArray(parentArray)) { for (const ref of parentArray) { if (ref instanceof Ref) { @@ -799,7 +804,7 @@ class StructTreePage { return; } for (const [elemId, type] of ids) { - const obj = numberTree.get(elemId); + const obj = parentTree.get(elemId); if (obj) { const elem = this.addNode(this.xref.fetchIfRef(obj), map); if ( From bc87f4e8d6402f45afd7b161cc6683f4e414acc7 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 30 Oct 2025 18:25:18 +0100 Subject: [PATCH 13/26] Add the possibility to create a pdf from different ones (bug 1997379) For now it's just possible to create a single pdf in selecting some pages in different pdf sources. The merge is for now pretty basic (it's why it's still a WIP) none of these data are merged for now: - the struct trees - the page labels - the outlines - named destinations For there are 2 new ref tests where some new pdfs are created: one with some extracted pages and an other one (encrypted) which is just rewritten. The ref images are generated from the original pdfs in selecting the page we want and the new images are taken from the generated pdfs. --- src/core/decode_stream.js | 13 + src/core/decrypt_stream.js | 4 + src/core/document.js | 6 +- src/core/editor/pdf_editor.js | 594 ++++++++++++++++++++++++++++++++++ src/core/primitives.js | 10 + src/core/stream.js | 9 + src/core/worker.js | 92 ++++++ src/core/writer.js | 83 ++--- src/display/api.js | 22 ++ test/driver.js | 44 ++- test/pdfs/.gitignore | 3 + test/pdfs/doc_1_3_pages.pdf | Bin 0 -> 12091 bytes test/pdfs/doc_2_3_pages.pdf | Bin 0 -> 39598 bytes test/pdfs/doc_3_3_pages.pdf | Bin 0 -> 40746 bytes test/test.mjs | 7 +- test/test_manifest.json | 18 ++ test/unit/api_spec.js | 208 ++++++++++++ test/unit/primitives_spec.js | 16 + test/unit/writer_spec.js | 4 +- 19 files changed, 1089 insertions(+), 44 deletions(-) create mode 100644 src/core/editor/pdf_editor.js create mode 100755 test/pdfs/doc_1_3_pages.pdf create mode 100755 test/pdfs/doc_2_3_pages.pdf create mode 100755 test/pdfs/doc_3_3_pages.pdf diff --git a/src/core/decode_stream.js b/src/core/decode_stream.js index 80bdcebd0..b541ed898 100644 --- a/src/core/decode_stream.js +++ b/src/core/decode_stream.js @@ -131,6 +131,19 @@ class DecodeStream extends BaseStream { getBaseStreams() { return this.stream ? this.stream.getBaseStreams() : null; } + + clone() { + // Make sure it has been fully read. + while (!this.eof) { + this.readBlock(); + } + return new Stream( + this.buffer, + this.start, + this.end - this.start, + this.dict.clone() + ); + } } class StreamsSequenceStream extends DecodeStream { diff --git a/src/core/decrypt_stream.js b/src/core/decrypt_stream.js index 8e93b9f86..78fbc5ae5 100644 --- a/src/core/decrypt_stream.js +++ b/src/core/decrypt_stream.js @@ -52,6 +52,10 @@ class DecryptStream extends DecodeStream { buffer.set(chunk, bufferLength); this.bufferLength = newLength; } + + getOriginalStream() { + return this; + } } export { DecryptStream }; diff --git a/src/core/document.js b/src/core/document.js index 7bc738bb5..f624458cb 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -178,7 +178,7 @@ class Page { ); } - #getBoundingBox(name) { + getBoundingBox(name) { if (this.xfaData) { return this.xfaData.bbox; } @@ -201,7 +201,7 @@ class Page { return shadow( this, "mediaBox", - this.#getBoundingBox("MediaBox") || LETTER_SIZE_MEDIABOX + this.getBoundingBox("MediaBox") || LETTER_SIZE_MEDIABOX ); } @@ -210,7 +210,7 @@ class Page { return shadow( this, "cropBox", - this.#getBoundingBox("CropBox") || this.mediaBox + this.getBoundingBox("CropBox") || this.mediaBox ); } diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js new file mode 100644 index 000000000..7df909156 --- /dev/null +++ b/src/core/editor/pdf_editor.js @@ -0,0 +1,594 @@ +/* Copyright 2025 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @typedef {import("../document.js").PDFDocument} PDFDocument */ +/** @typedef {import("../document.js").Page} Page */ +/** @typedef {import("../xref.js").XRef} XRef */ + +import { Dict, isName, Ref, RefSetCache } from "../primitives.js"; +import { getModificationDate, stringToPDFString } from "../../shared/util.js"; +import { incrementalUpdate, writeValue } from "../writer.js"; +import { BaseStream } from "../base_stream.js"; +import { StringStream } from "../stream.js"; +import { stringToAsciiOrUTF16BE } from "../core_utils.js"; + +const MAX_LEAVES_PER_PAGES_NODE = 16; + +class PageData { + constructor(page, documentData) { + this.page = page; + this.documentData = documentData; + this.annotations = null; + + documentData.pagesMap.put(page.ref, this); + } +} + +class DocumentData { + constructor(document) { + this.document = document; + this.pagesMap = new RefSetCache(); + this.oldRefMapping = new RefSetCache(); + } +} + +class PDFEditor { + constructor({ useObjectStreams = true, title = "", author = "" } = {}) { + this.hasSingleFile = false; + this.currentDocument = null; + this.oldPages = []; + this.newPages = []; + this.xref = [null]; + this.newRefCount = 1; + [this.rootRef, this.rootDict] = this.newDict; + [this.infoRef, this.infoDict] = this.newDict; + [this.pagesRef, this.pagesDict] = this.newDict; + this.namesDict = null; + this.useObjectStreams = useObjectStreams; + this.objStreamRefs = useObjectStreams ? new Set() : null; + this.version = "1.7"; + this.title = title; + this.author = author; + } + + /** + * Get a new reference for an object in the PDF. + * @returns {Ref} + */ + get newRef() { + const ref = Ref.get(this.newRefCount++, 0); + return ref; + } + + /** + * Create a new dictionary and its reference. + * @returns {[Ref, Dict]} + */ + get newDict() { + const ref = this.newRef; + const dict = (this.xref[ref.num] = new Dict()); + return [ref, dict]; + } + + /** + * Clone an object in the PDF. + * @param {*} obj + * @param {XRef} xref + * @returns {Promise} + */ + async #cloneObject(obj, xref) { + const ref = this.newRef; + this.xref[ref.num] = await this.#collectDependencies(obj, true, xref); + return ref; + } + + /** + * Collect the dependencies of an object and create new references for each + * dependency. + * @param {*} obj + * @param {boolean} mustClone + * @param {XRef} xref + * @returns {Promise<*>} + */ + async #collectDependencies(obj, mustClone, xref) { + if (obj instanceof Ref) { + const { + currentDocument: { oldRefMapping }, + } = this; + let newRef = oldRefMapping.get(obj); + if (newRef) { + return newRef; + } + newRef = this.newRef; + oldRefMapping.put(obj, newRef); + obj = await xref.fetchAsync(obj); + + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + if ( + obj instanceof Dict && + isName(obj.get("Type"), "Page") && + !this.currentDocument.pagesMap.has(obj) + ) { + throw new Error( + "Add a deleted page to the document is not supported." + ); + } + } + + this.xref[newRef.num] = await this.#collectDependencies(obj, true, xref); + return newRef; + } + const promises = []; + if (Array.isArray(obj)) { + if (mustClone) { + obj = obj.slice(); + } + for (let i = 0, ii = obj.length; i < ii; i++) { + promises.push( + this.#collectDependencies(obj[i], true, xref).then( + newObj => (obj[i] = newObj) + ) + ); + } + await Promise.all(promises); + return obj; + } + let dict; + if (obj instanceof BaseStream) { + ({ dict } = obj = obj.getOriginalStream().clone()); + } else if (obj instanceof Dict) { + if (mustClone) { + obj = obj.clone(); + } + dict = obj; + } + if (dict) { + for (const [key, rawObj] of dict.getRawEntries()) { + promises.push( + this.#collectDependencies(rawObj, true, xref).then(newObj => + dict.set(key, newObj) + ) + ); + } + await Promise.all(promises); + } + + return obj; + } + + /** + * @typedef {Object} PageInfo + * @property {PDFDocument} document + * @property {Array|number>} [includePages] + * included ranges (inclusive) or indices. + * @property {Array|number>} [excludePages] + * excluded ranges (inclusive) or indices. + */ + + /** + * Extract pages from the given documents. + * @param {Array} pageInfos + * @return {Promise} + */ + async extractPages(pageInfos) { + const promises = []; + let newIndex = 0; + this.hasSingleFile = pageInfos.length === 1; + for (const { document, includePages, excludePages } of pageInfos) { + if (!document) { + continue; + } + const documentData = new DocumentData(document); + promises.push(this.#collectDocumentData(documentData)); + let keptIndices, keptRanges, deletedIndices, deletedRanges; + for (const page of includePages || []) { + if (Array.isArray(page)) { + (keptRanges ||= []).push(page); + } else { + (keptIndices ||= new Set()).add(page); + } + } + for (const page of excludePages || []) { + if (Array.isArray(page)) { + (deletedRanges ||= []).push(page); + } else { + (deletedIndices ||= new Set()).add(page); + } + } + for (let i = 0, ii = document.numPages; i < ii; i++) { + if (deletedIndices?.has(i)) { + continue; + } + if (deletedRanges) { + let isDeleted = false; + for (const [start, end] of deletedRanges) { + if (i >= start && i <= end) { + isDeleted = true; + break; + } + } + if (isDeleted) { + continue; + } + } + + let takePage = false; + if (keptIndices) { + takePage = keptIndices.has(i); + } + if (!takePage && keptRanges) { + for (const [start, end] of keptRanges) { + if (i >= start && i <= end) { + takePage = true; + break; + } + } + } + if (!takePage && !keptIndices && !keptRanges) { + takePage = true; + } + if (!takePage) { + continue; + } + const newPageIndex = newIndex++; + promises.push( + document.getPage(i).then(page => { + this.oldPages[newPageIndex] = new PageData(page, documentData); + }) + ); + } + } + await Promise.all(promises); + promises.length = 0; + + for (const page of this.oldPages) { + promises.push(this.#postCollectPageData(page)); + } + await Promise.all(promises); + + for (let i = 0, ii = this.oldPages.length; i < ii; i++) { + this.newPages[i] = await this.#makePageCopy(i, null); + } + + return this.writePDF(); + } + + /** + * Collect the document data. + * @param {DocumentData} documentData + * @return {Promise} + */ + async #collectDocumentData(documentData) {} + + /** + * Post process the collected page data. + * @param {PageData} pageData + * @returns {Promise} + */ + async #postCollectPageData(pageData) { + const { + page: { xref, annotations }, + } = pageData; + + if (!annotations) { + return; + } + + const promises = []; + let newAnnotations = []; + let newIndex = 0; + + // TODO: remove only links to deleted pages. + for (const annotationRef of annotations) { + const newAnnotationIndex = newIndex++; + promises.push( + xref.fetchIfRefAsync(annotationRef).then(async annotationDict => { + if (!isName(annotationDict.get("Subtype"), "Link")) { + newAnnotations[newAnnotationIndex] = annotationRef; + } + }) + ); + } + await Promise.all(promises); + newAnnotations = newAnnotations.filter(annot => !!annot); + pageData.annotations = newAnnotations.length > 0 ? newAnnotations : null; + } + + /** + * Create a copy of a page. + * @param {number} pageIndex + * @returns {Promise} the page reference in the new PDF document. + */ + async #makePageCopy(pageIndex) { + const { page, documentData, annotations } = this.oldPages[pageIndex]; + this.currentDocument = documentData; + const { oldRefMapping } = documentData; + const { xref, rotate, mediaBox, resources, ref: oldPageRef } = page; + const pageRef = this.newRef; + const pageDict = (this.xref[pageRef.num] = page.pageDict.clone()); + oldRefMapping.put(oldPageRef, pageRef); + + // No need to keep these entries as we'll set them again later. + for (const key of [ + "Rotate", + "MediaBox", + "CropBox", + "BleedBox", + "TrimBox", + "ArtBox", + "Resources", + "Annots", + "Parent", + "UserUnit", + ]) { + pageDict.delete(key); + } + + const lastRef = this.newRefCount; + await this.#collectDependencies(pageDict, false, xref); + + pageDict.set("Rotate", rotate); + pageDict.set("MediaBox", mediaBox); + for (const boxName of ["CropBox", "BleedBox", "TrimBox", "ArtBox"]) { + const box = page.getBoundingBox(boxName); + if (box?.some((value, index) => value !== mediaBox[index])) { + // These boxes are optional and their default value is the MediaBox. + pageDict.set(boxName, box); + } + } + const userUnit = page.userUnit; + if (userUnit !== 1) { + pageDict.set("UserUnit", userUnit); + } + pageDict.setIfDict( + "Resources", + await this.#collectDependencies(resources, true, xref) + ); + pageDict.setIfArray( + "Annots", + await this.#collectDependencies(annotations, true, xref) + ); + + if (this.useObjectStreams) { + const newLastRef = this.newRefCount; + const pageObjectRefs = []; + for (let i = lastRef; i < newLastRef; i++) { + const obj = this.xref[i]; + if (obj instanceof BaseStream) { + continue; + } + pageObjectRefs.push(Ref.get(i, 0)); + } + for (let i = 0; i < pageObjectRefs.length; i += 0xffff) { + const objStreamRef = this.newRef; + this.objStreamRefs.add(objStreamRef.num); + this.xref[objStreamRef.num] = pageObjectRefs.slice(i, i + 0xffff); + } + } + + this.currentDocument = null; + + return pageRef; + } + + /** + * Create the page tree structure. + */ + #makePageTree() { + const { newPages: pages, rootDict, pagesRef, pagesDict } = this; + rootDict.set("Pages", pagesRef); + pagesDict.setIfName("Type", "Pages"); + pagesDict.set("Count", pages.length); + + const maxLeaves = + MAX_LEAVES_PER_PAGES_NODE <= 1 ? pages.length : MAX_LEAVES_PER_PAGES_NODE; + const stack = [{ dict: pagesDict, kids: pages, parentRef: pagesRef }]; + + while (stack.length > 0) { + const { dict, kids, parentRef } = stack.pop(); + if (kids.length <= maxLeaves) { + dict.set("Kids", kids); + for (const ref of kids) { + this.xref[ref.num].set("Parent", parentRef); + } + continue; + } + const chunkSize = Math.max(maxLeaves, Math.ceil(kids.length / maxLeaves)); + const kidsChunks = []; + for (let i = 0; i < kids.length; i += chunkSize) { + kidsChunks.push(kids.slice(i, i + chunkSize)); + } + const kidsRefs = []; + dict.set("Kids", kidsRefs); + for (const chunk of kidsChunks) { + const [kidRef, kidDict] = this.newDict; + kidsRefs.push(kidRef); + kidDict.setIfName("Type", "Pages"); + kidDict.set("Parent", parentRef); + kidDict.set("Count", chunk.length); + stack.push({ dict: kidDict, kids: chunk, parentRef: kidRef }); + } + } + } + + /** + * Create the root dictionary. + * @returns {Promise} + */ + async #makeRoot() { + const { rootDict } = this; + rootDict.setIfName("Type", "Catalog"); + rootDict.set("Version", this.version); + this.#makePageTree(); + } + + /** + * Create the info dictionary. + * @returns {Map} infoMap + */ + #makeInfo() { + const infoMap = new Map(); + if (this.hasSingleFile) { + const { + xref: { trailer }, + } = this.oldPages[0].documentData.document; + const oldInfoDict = trailer.get("Info"); + for (const [key, value] of oldInfoDict || []) { + if (typeof value === "string") { + infoMap.set(key, stringToPDFString(value)); + } + } + } + infoMap.delete("ModDate"); + infoMap.set("CreationDate", getModificationDate()); + infoMap.set("Creator", "PDF.js"); + infoMap.set("Producer", "Firefox"); + + if (this.author) { + infoMap.set("Author", this.author); + } + if (this.title) { + infoMap.set("Title", this.title); + } + for (const [key, value] of infoMap) { + this.infoDict.set(key, stringToAsciiOrUTF16BE(value)); + } + return infoMap; + } + + /** + * Create the encryption dictionary if required. + * @returns {Promise<[Dict|null, CipherTransformFactory|null, Array|null]>} + */ + async #makeEncrypt() { + if (!this.hasSingleFile) { + return [null, null, null]; + } + const { documentData } = this.oldPages[0]; + const { + document: { + xref: { trailer, encrypt }, + }, + } = documentData; + if (!trailer.has("Encrypt")) { + return [null, null, null]; + } + const encryptDict = trailer.get("Encrypt"); + if (!(encryptDict instanceof Dict)) { + return [null, null, null]; + } + this.currentDocument = documentData; + const result = [ + await this.#cloneObject(encryptDict, trailer.xref), + encrypt, + trailer.get("ID"), + ]; + this.currentDocument = null; + return result; + } + + /** + * Create the changes required to write the new PDF document. + * @returns {Promise<[RefSetCache, Ref]>} + */ + async #createChanges() { + const changes = new RefSetCache(); + changes.put(Ref.get(0, 0xffff), { data: null }); + for (let i = 1, ii = this.xref.length; i < ii; i++) { + if (this.objStreamRefs?.has(i)) { + await this.#createObjectStream(Ref.get(i, 0), this.xref[i], changes); + } else { + changes.put(Ref.get(i, 0), { data: this.xref[i] }); + } + } + + return [changes, this.newRef]; + } + + /** + * Create an object stream containing the given objects. + * @param {Ref} objStreamRef + * @param {Array} objRefs + * @param {RefSetCache} changes + */ + async #createObjectStream(objStreamRef, objRefs, changes) { + const streamBuffer = [""]; + const objOffsets = []; + let offset = 0; + const buffer = []; + for (let i = 0, ii = objRefs.length; i < ii; i++) { + const objRef = objRefs[i]; + changes.put(objRef, { data: null, objStreamRef, index: i }); + objOffsets.push(`${objRef.num} ${offset}`); + const data = this.xref[objRef.num]; + await writeValue(data, buffer, /* transform = */ null); + const obj = buffer.join(""); + buffer.length = 0; + streamBuffer.push(obj); + offset += obj.length + 1; + } + streamBuffer[0] = objOffsets.join("\n"); + const objStream = new StringStream(streamBuffer.join("\n")); + const objStreamDict = (objStream.dict = new Dict()); + objStreamDict.setIfName("Type", "ObjStm"); + objStreamDict.set("N", objRefs.length); + objStreamDict.set("First", streamBuffer[0].length + 1); + + changes.put(objStreamRef, { data: objStream }); + } + + /** + * Write the new PDF document to a Uint8Array. + * @returns {Promise} + */ + async writePDF() { + await this.#makeRoot(); + const infoMap = this.#makeInfo(); + const [encryptRef, encrypt, fileIds] = await this.#makeEncrypt(); + const [changes, xrefTableRef] = await this.#createChanges(); + + // Create the PDF header in order to help sniffers. + // PDF version must be in the range 1.0 to 1.7 inclusive. + // We add a binary comment line to ensure that the file is treated + // as a binary file by applications that open it. + const header = [ + ...`%PDF-${this.version}\n%`.split("").map(c => c.charCodeAt(0)), + 0xfa, + 0xde, + 0xfa, + 0xce, + ]; + return incrementalUpdate({ + originalData: new Uint8Array(header), + changes, + xrefInfo: { + startXRef: null, + rootRef: this.rootRef, + infoRef: this.infoRef, + encryptRef, + newRef: xrefTableRef, + fileIds: fileIds || [null, null], + infoMap, + }, + useXrefStream: this.useObjectStreams, + xref: { + encrypt, + encryptRef, + }, + }); + } +} + +export { PDFEditor }; diff --git a/src/core/primitives.js b/src/core/primitives.js index decd4338c..22cdd2527 100644 --- a/src/core/primitives.js +++ b/src/core/primitives.js @@ -188,6 +188,10 @@ class Dict { return [...this._map.values()]; } + getRawEntries() { + return this._map.entries(); + } + set(key, value) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { if (typeof key !== "string") { @@ -231,6 +235,12 @@ class Dict { } } + setIfDict(key, value) { + if (value instanceof Dict) { + this.set(key, value); + } + } + has(key) { return this._map.has(key); } diff --git a/src/core/stream.js b/src/core/stream.js index 7bc9791ed..710b92f8c 100644 --- a/src/core/stream.js +++ b/src/core/stream.js @@ -82,6 +82,15 @@ class Stream extends BaseStream { makeSubStream(start, length, dict = null) { return new Stream(this.bytes.buffer, start, length, dict); } + + clone() { + return new Stream( + this.bytes.buffer, + this.start, + this.end - this.start, + this.dict.clone() + ); + } } class StringStream extends Stream { diff --git a/src/core/worker.js b/src/core/worker.js index 578ea2bdb..8ef7b10f4 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -36,6 +36,7 @@ import { MessageHandler, wrapReason } from "../shared/message_handler.js"; import { AnnotationFactory } from "./annotation.js"; import { clearGlobalCaches } from "./cleanup_helper.js"; import { incrementalUpdate } from "./writer.js"; +import { PDFEditor } from "./editor/pdf_editor.js"; import { PDFWorkerStream } from "./worker_stream.js"; import { StructTreeRoot } from "./struct_tree.js"; @@ -557,6 +558,97 @@ class WorkerMessageHandler { return pdfManager.ensureDoc("calculationOrderIds"); }); + handler.on("ExtractPages", async function ({ pageInfos }) { + if (!pageInfos) { + warn("extractPages: nothing to extract."); + return null; + } + if (!Array.isArray(pageInfos)) { + pageInfos = [pageInfos]; + } + let newDocumentId = 0; + for (const pageInfo of pageInfos) { + if (pageInfo.document === null) { + pageInfo.document = pdfManager.pdfDocument; + } else if (ArrayBuffer.isView(pageInfo.document)) { + const manager = new LocalPdfManager({ + source: pageInfo.document, + docId: `${docId}_extractPages_${newDocumentId++}`, + handler, + password: pageInfo.password ?? null, + evaluatorOptions: Object.assign({}, pdfManager.evaluatorOptions), + }); + let recoveryMode = false; + let isValid = true; + while (true) { + try { + await manager.requestLoadedStream(); + await manager.ensureDoc("checkHeader"); + await manager.ensureDoc("parseStartXRef"); + await manager.ensureDoc("parse", [recoveryMode]); + break; + } catch (e) { + if (e instanceof XRefParseException) { + if (recoveryMode === false) { + recoveryMode = true; + continue; + } else { + isValid = false; + warn("extractPages: XRefParseException."); + } + } else if (e instanceof PasswordException) { + const task = new WorkerTask( + `PasswordException: response ${e.code}` + ); + + startWorkerTask(task); + + try { + const { password } = await handler.sendWithPromise( + "PasswordRequest", + e + ); + manager.updatePassword(password); + } catch { + isValid = false; + warn("extractPages: invalid password."); + } finally { + finishWorkerTask(task); + } + } else { + isValid = false; + warn("extractPages: invalid document."); + } + if (!isValid) { + break; + } + } + } + if (!isValid) { + pageInfo.document = null; + } + const isPureXfa = await manager.ensureDoc("isPureXfa"); + if (isPureXfa) { + pageInfo.document = null; + warn("extractPages does not support pure XFA documents."); + } else { + pageInfo.document = manager.pdfDocument; + } + } else { + warn("extractPages: invalid document."); + } + } + try { + const pdfEditor = new PDFEditor(); + const buffer = await pdfEditor.extractPages(pageInfos); + return buffer; + } catch (reason) { + // eslint-disable-next-line no-console + console.error(reason); + return null; + } + }); + handler.on( "SaveDocument", async function ({ isPureXfa, numPages, annotationStorage, filename }) { diff --git a/src/core/writer.js b/src/core/writer.js index bf66226a2..921936b3b 100644 --- a/src/core/writer.js +++ b/src/core/writer.js @@ -19,7 +19,6 @@ import { escapePDFName, escapeString, getSizeInBytes, - numberToString, parseXFAPath, } from "./core_utils.js"; import { SimpleDOMNode, SimpleXMLParser } from "./xml_parser.js"; @@ -27,29 +26,34 @@ import { Stream, StringStream } from "./stream.js"; import { BaseStream } from "./base_stream.js"; import { calculateMD5 } from "./calculate_md5.js"; -async function writeObject(ref, obj, buffer, { encrypt = null }) { - const transform = encrypt?.createCipherTransform(ref.num, ref.gen); +async function writeObject( + ref, + obj, + buffer, + { encrypt = null, encryptRef = null } +) { + // Avoid to encrypt the encrypt dictionary. + const transform = + encrypt && encryptRef !== ref + ? encrypt.createCipherTransform(ref.num, ref.gen) + : null; buffer.push(`${ref.num} ${ref.gen} obj\n`); - if (obj instanceof Dict) { - await writeDict(obj, buffer, transform); - } else if (obj instanceof BaseStream) { - await writeStream(obj, buffer, transform); - } else if (Array.isArray(obj) || ArrayBuffer.isView(obj)) { - await writeArray(obj, buffer, transform); - } + await writeValue(obj, buffer, transform); buffer.push("\nendobj\n"); } async function writeDict(dict, buffer, transform) { buffer.push("<<"); - for (const key of dict.getKeys()) { + for (const [key, rawObj] of dict.getRawEntries()) { buffer.push(` /${escapePDFName(key)} `); - await writeValue(dict.getRaw(key), buffer, transform); + await writeValue(rawObj, buffer, transform); } buffer.push(">>"); } async function writeStream(stream, buffer, transform) { + stream = stream.getOriginalStream(); + stream.reset(); let bytes = stream.getBytes(); const { dict } = stream; @@ -67,7 +71,7 @@ async function writeStream(stream, buffer, transform) { // The number 256 is arbitrary, but it should be reasonable. const MIN_LENGTH_FOR_COMPRESSING = 256; - if (bytes.length >= MIN_LENGTH_FOR_COMPRESSING || isFilterZeroFlateDecode) { + if (bytes.length >= MIN_LENGTH_FOR_COMPRESSING && !isFilterZeroFlateDecode) { try { const cs = new CompressionStream("deflate"); const writer = cs.writable.getWriter(); @@ -120,14 +124,11 @@ async function writeStream(stream, buffer, transform) { async function writeArray(array, buffer, transform) { buffer.push("["); - let first = true; - for (const val of array) { - if (!first) { + for (let i = 0, ii = array.length; i < ii; i++) { + await writeValue(array[i], buffer, transform); + if (i < ii - 1) { buffer.push(" "); - } else { - first = false; } - await writeValue(val, buffer, transform); } buffer.push("]"); } @@ -145,7 +146,11 @@ async function writeValue(value, buffer, transform) { } buffer.push(`(${escapeString(value)})`); } else if (typeof value === "number") { - buffer.push(numberToString(value)); + // Don't try to round numbers in general, it could lead to have degenerate + // matrices (e.g. [0.000008 0 0 0.000008 0 0]). + // The numbers must be "rounded" only when pdf.js is producing them and the + // current transformation matrix is well known. + buffer.push(value.toString()); } else if (typeof value === "boolean") { buffer.push(value.toString()); } else if (value instanceof Dict) { @@ -306,7 +311,7 @@ async function getXRefTable(xrefInfo, baseOffset, newRefs, newXref, buffer) { } computeIDs(baseOffset, xrefInfo, newXref); buffer.push("trailer\n"); - await writeDict(newXref, buffer); + await writeDict(newXref, buffer, null); buffer.push("\nstartxref\n", baseOffset.toString(), "\n%%EOF\n"); } @@ -332,10 +337,17 @@ async function getXRefStreamTable( const xrefTableData = []; let maxOffset = 0; let maxGen = 0; - for (const { ref, data } of newRefs) { + for (const { ref, data, objStreamRef, index } of newRefs) { let gen; maxOffset = Math.max(maxOffset, baseOffset); - if (data !== null) { + // The first number in each entry is the type (see 7.5.8.3): + // 0: free object + // 1: in-use object + // 2: compressed object + if (objStreamRef) { + gen = index; + xrefTableData.push([2, objStreamRef.num, gen]); + } else if (data !== null) { gen = Math.min(ref.gen, 0xffff); xrefTableData.push([1, baseOffset, gen]); baseOffset += data.length; @@ -371,13 +383,13 @@ async function getXRefStreamTable( function computeIDs(baseOffset, xrefInfo, newXref) { if (Array.isArray(xrefInfo.fileIds) && xrefInfo.fileIds.length > 0) { const md5 = computeMD5(baseOffset, xrefInfo); - newXref.set("ID", [xrefInfo.fileIds[0], md5]); + newXref.set("ID", [xrefInfo.fileIds[0] || md5, md5]); } } function getTrailerDict(xrefInfo, changes, useXrefStream) { const newXref = new Dict(null); - newXref.set("Prev", xrefInfo.startXRef); + newXref.setIfDefined("Prev", xrefInfo?.startXRef); const refForXrefTable = xrefInfo.newRef; if (useXrefStream) { changes.put(refForXrefTable, { data: "" }); @@ -386,21 +398,20 @@ function getTrailerDict(xrefInfo, changes, useXrefStream) { } else { newXref.set("Size", refForXrefTable.num); } - if (xrefInfo.rootRef !== null) { - newXref.set("Root", xrefInfo.rootRef); - } - if (xrefInfo.infoRef !== null) { - newXref.set("Info", xrefInfo.infoRef); - } - if (xrefInfo.encryptRef !== null) { - newXref.set("Encrypt", xrefInfo.encryptRef); - } + newXref.setIfDefined("Root", xrefInfo?.rootRef); + newXref.setIfDefined("Info", xrefInfo?.infoRef); + newXref.setIfDefined("Encrypt", xrefInfo?.encryptRef); + return newXref; } async function writeChanges(changes, xref, buffer = []) { const newRefs = []; - for (const [ref, { data }] of changes.items()) { + for (const [ref, { data, objStreamRef, index }] of changes.items()) { + if (objStreamRef) { + newRefs.push({ ref, data, objStreamRef, index }); + continue; + } if (data === null || typeof data === "string") { newRefs.push({ ref, data }); continue; @@ -483,4 +494,4 @@ async function incrementalUpdate({ return array; } -export { incrementalUpdate, writeChanges, writeDict, writeObject }; +export { incrementalUpdate, writeChanges, writeDict, writeObject, writeValue }; diff --git a/src/display/api.js b/src/display/api.js index 149ceb237..b279df994 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1025,6 +1025,24 @@ class PDFDocumentProxy { return this._transport.saveDocument(); } + /** + * @typedef {Object} PageInfo + * @property {null|Uint8Array} document + * @property {Array|number>} [includePages] + * included ranges or indices. + * @property {Array|number>} [excludePages] + * excluded ranges or indices. + */ + + /** + * @param {Array} pageInfos - The pages to extract. + * @returns {Promise} A promise that is resolved with a + * {Uint8Array} containing the full data of the saved document. + */ + extractPages(pageInfos) { + return this._transport.extractPages(pageInfos); + } + /** * @returns {Promise<{ length: number }>} A promise that is resolved when the * document's data is loaded. It is resolved with an {Object} that contains @@ -2900,6 +2918,10 @@ class WorkerTransport { }); } + extractPages(pageInfos) { + return this.messageHandler.sendWithPromise("ExtractPages", { pageInfos }); + } + getPage(pageNumber) { if ( !Number.isInteger(pageNumber) || diff --git a/test/driver.js b/test/driver.js index a21a4a610..212a0e85e 100644 --- a/test/driver.js +++ b/test/driver.js @@ -506,6 +506,7 @@ class Driver { this.inFlightRequests = 0; this.testFilter = JSON.parse(params.get("testfilter") || "[]"); this.xfaOnly = params.get("xfaonly") === "true"; + this.masterMode = params.get("mastermode") === "true"; // Create a working canvas this.canvas = document.createElement("canvas"); @@ -591,6 +592,25 @@ class Driver { task.stats = { times: [] }; task.enableXfa = task.enableXfa === true; + if (task.includePages && task.type === "extract") { + if (this.masterMode) { + const includePages = []; + for (const page of task.includePages) { + if (Array.isArray(page)) { + for (let i = page[0]; i <= page[1]; i++) { + includePages.push(i); + } + } else { + includePages.push(page); + } + } + task.numberOfTasks = includePages.length; + task.includePages = includePages; + } else { + delete task.pageMapping; + } + } + const prevFile = md5FileMap.get(task.md5); if (prevFile) { if (task.file !== prevFile) { @@ -658,6 +678,20 @@ class Driver { }); let promise = loadingTask.promise; + if (!this.masterMode && task.type === "extract") { + promise = promise.then(async doc => { + const data = await doc.extractPages([ + { + document: null, + includePages: task.includePages, + }, + ]); + await loadingTask.destroy(); + delete task.includePages; + return getDocument(data).promise; + }); + } + if (task.annotationStorage) { for (const annotation of Object.values(task.annotationStorage)) { const { bitmapName, quadPoints, paths, outlines } = annotation; @@ -862,7 +896,12 @@ class Driver { } } - if (task.skipPages?.includes(task.pageNum)) { + if ( + task.skipPages?.includes(task.pageNum) || + (this.masterMode && + task.includePages && + !task.includePages.includes(task.pageNum - 1)) + ) { this._log( ` Skipping page ${task.pageNum}/${task.pdfDoc.numPages}...\n` ); @@ -1274,10 +1313,11 @@ class Driver { id: task.id, numPages: task.pdfDoc ? task.lastPage || task.pdfDoc.numPages : 0, lastPageNum: this._getLastPageNumber(task), + numberOfTasks: task.numberOfTasks ?? -1, failure, file: task.file, round: task.round, - page: task.pageNum, + page: task.pageMapping?.[task.pageNum] ?? task.pageNum, snapshot, baselineSnapshot, stats: task.stats.times, diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 1eb2dce1b..91091a44e 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -754,3 +754,6 @@ !bug1937438_from_word.pdf !bug1937438_mml_from_latex.pdf !bug1997343.pdf +!doc_1_3_pages.pdf +!doc_2_3_pages.pdf +!doc_3_3_pages.pdf diff --git a/test/pdfs/doc_1_3_pages.pdf b/test/pdfs/doc_1_3_pages.pdf new file mode 100755 index 0000000000000000000000000000000000000000..f71ed36ab7403f8a074747e39ef6b6c486978bad GIT binary patch literal 12091 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

lrr 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 M;7XAue~;_`0oyRn_y7O^ literal 0 HcmV?d00001 diff --git a/test/pdfs/doc_3_3_pages.pdf b/test/pdfs/doc_3_3_pages.pdf new file mode 100755 index 0000000000000000000000000000000000000000..63222e8991d42d4305e0ed35b4478733cf8ba230 GIT binary patch literal 40746 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 !!result).length === numberOfTasks; if (isDone) { checkRefTestResults(browser, id, taskResults); session.remaining--; diff --git a/test/test_manifest.json b/test/test_manifest.json index 03d1f1d71..f864c2be9 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -13049,5 +13049,23 @@ "rotation": 0 } } + }, + { + "id": "tracemonkey-extract_0_2_12", + "file": "pdfs/tracemonkey.pdf", + "md5": "9a192d8b1a7dc652a19835f6f08098bd", + "rounds": 1, + "type": "extract", + "includePages": [0, 2, 12], + "pageMapping": { "1": 1, "3": 2, "13": 3 } + }, + { + "id": "bug900822-encrypted-extract_0", + "file": "pdfs/bug900822.pdf", + "md5": "70e2a3c5922574eeda169c955cf9d084", + "rounds": 1, + "type": "extract", + "includePages": [0], + "pageMapping": { "1": 1 } } ] diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index a8e0fbc07..0729875b6 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -5335,4 +5335,212 @@ deployment as easy as distributing a source ๏ฌle. They are used for small scripts as well as for`); }); }); + + describe("PDF page editing", function () { + describe("Merge pdfs", function () { + it("should merge three PDFs", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("doc_1_3_pages.pdf") + ); + const pdfDoc = await loadingTask.promise; + const pdfData2 = await DefaultFileReaderFactory.fetch({ + path: TEST_PDFS_PATH + "doc_2_3_pages.pdf", + }); + const pdfData3 = await DefaultFileReaderFactory.fetch({ + path: TEST_PDFS_PATH + "doc_3_3_pages.pdf", + }); + + let data = await pdfDoc.extractPages([ + { document: null }, + { document: pdfData2 }, + { document: pdfData3 }, + ]); + let newLoadingTask = getDocument(data); + let newPdfDoc = await newLoadingTask.promise; + expect(newPdfDoc.numPages).toEqual(9); + + for (let i = 1; i <= 9; i++) { + const pdfPage = await newPdfDoc.getPage(i); + const { items: textItems } = await pdfPage.getTextContent(); + expect(mergeText(textItems)).toEqual( + `Document ${Math.ceil(i / 3)}:Page ${((i - 1) % 3) + 1}` + ); + } + await newLoadingTask.destroy(); + + data = await pdfDoc.extractPages([ + { document: pdfData3 }, + { document: pdfData2 }, + { document: null }, + ]); + newLoadingTask = getDocument(data); + newPdfDoc = await newLoadingTask.promise; + expect(newPdfDoc.numPages).toEqual(9); + for (let i = 1; i <= 9; i++) { + const pdfPage = await newPdfDoc.getPage(i); + const { items: textItems } = await pdfPage.getTextContent(); + expect(mergeText(textItems)).toEqual( + `Document ${Math.ceil((10 - i) / 3)}:Page ${((i - 1) % 3) + 1}` + ); + } + await newLoadingTask.destroy(); + + data = await pdfDoc.extractPages([ + { document: null, includePages: [0] }, + { document: pdfData2, includePages: [0] }, + { document: pdfData3, includePages: [0] }, + ]); + newLoadingTask = getDocument(data); + newPdfDoc = await newLoadingTask.promise; + expect(newPdfDoc.numPages).toEqual(3); + for (let i = 1; i <= 3; i++) { + const pdfPage = await newPdfDoc.getPage(i); + const { items: textItems } = await pdfPage.getTextContent(); + expect(mergeText(textItems)).toEqual(`Document ${i}:Page 1`); + } + await newLoadingTask.destroy(); + + data = await pdfDoc.extractPages([ + { document: null, excludePages: [0] }, + { document: pdfData2, excludePages: [0] }, + { document: pdfData3, excludePages: [0] }, + ]); + newLoadingTask = getDocument(data); + newPdfDoc = await newLoadingTask.promise; + expect(newPdfDoc.numPages).toEqual(6); + for (let i = 1; i <= 6; i++) { + const pdfPage = await newPdfDoc.getPage(i); + const { items: textItems } = await pdfPage.getTextContent(); + expect(mergeText(textItems)).toEqual( + `Document ${Math.ceil(i / 2)}:Page ${((i - 1) % 2) + 2}` + ); + } + await newLoadingTask.destroy(); + + await loadingTask.destroy(); + }); + + it("should merge two PDFs with page included ranges", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("tracemonkey.pdf") + ); + const pdfDoc = await loadingTask.promise; + const pdfData1 = await DefaultFileReaderFactory.fetch({ + path: TEST_PDFS_PATH + "doc_1_3_pages.pdf", + }); + + const data = await pdfDoc.extractPages([ + { document: pdfData1, includePages: [[0, 0], 2] }, + { document: null, includePages: [[2, 4], 7] }, + ]); + const newLoadingTask = getDocument(data); + const newPdfDoc = await newLoadingTask.promise; + expect(newPdfDoc.numPages).toEqual(6); + + for (let i = 1; i <= 2; i++) { + const pdfPage = await newPdfDoc.getPage(i); + const { items: textItems } = await pdfPage.getTextContent(); + expect(mergeText(textItems)).toEqual(`Document 1:Page ${2 * i - 1}`); + } + + const expectedPagesText = [ + "v0 := ld s", + "i=4. On th", + "resentatio", + "5.1 Optimi", + ]; + for (let i = 3; i <= 6; i++) { + const pdfPage = await newPdfDoc.getPage(i); + const { items: textItems } = await pdfPage.getTextContent(); + const text = mergeText(textItems); + expect(text.substring(0, 10)).toEqual(expectedPagesText[i - 3]); + } + + await newLoadingTask.destroy(); + await loadingTask.destroy(); + }); + + it("should merge two PDFs with page excluded ranges", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("tracemonkey.pdf") + ); + const pdfDoc = await loadingTask.promise; + const pdfData1 = await DefaultFileReaderFactory.fetch({ + path: TEST_PDFS_PATH + "doc_1_3_pages.pdf", + }); + + const data = await pdfDoc.extractPages([ + { document: pdfData1, excludePages: [[1, 1]] }, + { + document: null, + excludePages: [ + [0, 1], + [5, 6], + [8, 13], + ], + }, + ]); + const newLoadingTask = getDocument(data); + const newPdfDoc = await newLoadingTask.promise; + expect(newPdfDoc.numPages).toEqual(6); + + for (let i = 1; i <= 2; i++) { + const pdfPage = await newPdfDoc.getPage(i); + const { items: textItems } = await pdfPage.getTextContent(); + expect(mergeText(textItems)).toEqual(`Document 1:Page ${2 * i - 1}`); + } + + const expectedPagesText = [ + "v0 := ld s", + "i=4. On th", + "resentatio", + "5.1 Optimi", + ]; + for (let i = 3; i <= 6; i++) { + const pdfPage = await newPdfDoc.getPage(i); + const { items: textItems } = await pdfPage.getTextContent(); + const text = mergeText(textItems); + expect(text.substring(0, 10)).toEqual(expectedPagesText[i - 3]); + } + + await newLoadingTask.destroy(); + await loadingTask.destroy(); + }); + + it("should merge two PDFs with one with a password", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("doc_1_3_pages.pdf") + ); + const pdfDoc = await loadingTask.promise; + const pdfData1 = await DefaultFileReaderFactory.fetch({ + path: TEST_PDFS_PATH + "pr6531_2.pdf", + }); + + const data = await pdfDoc.extractPages([ + { document: null, includePages: [0] }, + { document: pdfData1, password: "asdfasdf" }, + ]); + const newLoadingTask = getDocument(data); + const newPdfDoc = await newLoadingTask.promise; + expect(newPdfDoc.numPages).toEqual(2); + + const expectedPagesText = ["Document 1:Page 1", ""]; + for (let i = 1; i <= 2; i++) { + const pdfPage = await newPdfDoc.getPage(i); + const { items: textItems } = await pdfPage.getTextContent(); + expect(mergeText(textItems)).toEqual(expectedPagesText[i - 1]); + } + + const page2 = await newPdfDoc.getPage(2); + const annots = await page2.getAnnotations(); + expect(annots.length).toEqual(1); + expect(annots[0].contentsObj.str).toEqual( + "Bluebeam should be encrypting this." + ); + + await newLoadingTask.destroy(); + await loadingTask.destroy(); + }); + }); + }); }); diff --git a/test/unit/primitives_spec.js b/test/unit/primitives_spec.js index 04c92009b..b71df0b83 100644 --- a/test/unit/primitives_spec.js +++ b/test/unit/primitives_spec.js @@ -310,6 +310,16 @@ describe("primitives", function () { expect(rawValues2.sort()).toEqual(expectedRawValues2); }); + it("should get all raw entries", function () { + const expectedRawEntries = [ + ["FontFile", testFontFile], + ["FontFile2", testFontFile2], + ["FontFile3", testFontFile3], + ]; + const rawEntries = Array.from(dictWithManyKeys.getRawEntries()); + expect(rawEntries.sort()).toEqual(expectedRawEntries); + }); + it("should create only one object for Dict.empty", function () { const firstDictEmpty = Dict.empty; const secondDictEmpty = Dict.empty; @@ -423,6 +433,12 @@ describe("primitives", function () { dict.setIfName("k", 1234); expect(dict.has("k")).toBeFalse(); + + dict.setIfDict("l", new Dict()); + expect(dict.get("l")).toEqual(new Dict()); + + dict.setIfDict("m", "not a dict"); + expect(dict.has("m")).toBeFalse(); }); }); diff --git a/test/unit/writer_spec.js b/test/unit/writer_spec.js index 15866ee14..394c74429 100644 --- a/test/unit/writer_spec.js +++ b/test/unit/writer_spec.js @@ -170,8 +170,8 @@ describe("Writer", function () { const expected = "<< /A /B /B 123 456 R /C 789 /D (hello world) " + - "/E (\\(hello\\\\world\\)) /F [1.23 4.5 6] " + - "/G << /H 123 /I << /Length 8>> stream\n" + + "/E (\\(hello\\\\world\\)) /F [1.23001 4.50001 6] " + + "/G << /H 123.00001 /I << /Length 8>> stream\n" + "a stream\n" + "endstream>> /J true /K false " + "/NullArr [null 10] /NullVal null>>"; From ad97c5b816b01055711ebfe6018c6adc7d17cdc0 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 7 Nov 2025 15:34:08 +0100 Subject: [PATCH 14/26] Update the page labels tree when a pdf is extracted (bug 1997379) --- src/core/catalog.js | 16 +++-- src/core/editor/pdf_editor.js | 123 +++++++++++++++++++++++++++++++++- test/pdfs/.gitignore | 1 + test/pdfs/labelled_pages.pdf | Bin 0 -> 6717 bytes test/unit/api_spec.js | 34 ++++++++++ 5 files changed, 169 insertions(+), 5 deletions(-) create mode 100755 test/pdfs/labelled_pages.pdf diff --git a/src/core/catalog.js b/src/core/catalog.js index e5946c50f..5b5b319c7 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -735,6 +735,16 @@ class Catalog { return rawDests; } + get rawPageLabels() { + const obj = this.#catDict.getRaw("PageLabels"); + if (!obj) { + return null; + } + + const numberTree = new NumberTree(obj, this.xref); + return numberTree.getAll(); + } + get pageLabels() { let obj = null; try { @@ -749,8 +759,8 @@ class Catalog { } #readPageLabels() { - const obj = this.#catDict.getRaw("PageLabels"); - if (!obj) { + const nums = this.rawPageLabels; + if (!nums) { return null; } @@ -758,8 +768,6 @@ class Catalog { let style = null, prefix = ""; - const numberTree = new NumberTree(obj, this.xref); - const nums = numberTree.getAll(); let currentLabel = "", currentIndex = 1; diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 7df909156..5241863a7 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -25,6 +25,7 @@ import { StringStream } from "../stream.js"; import { stringToAsciiOrUTF16BE } from "../core_utils.js"; const MAX_LEAVES_PER_PAGES_NODE = 16; +const MAX_IN_NAME_TREE_NODE = 64; class PageData { constructor(page, documentData) { @@ -39,6 +40,7 @@ class PageData { class DocumentData { constructor(document) { this.document = document; + this.pageLabels = null; this.pagesMap = new RefSetCache(); this.oldRefMapping = new RefSetCache(); } @@ -61,6 +63,7 @@ class PDFEditor { this.version = "1.7"; this.title = title; this.author = author; + this.pageLabels = null; } /** @@ -253,6 +256,8 @@ class PDFEditor { await Promise.all(promises); promises.length = 0; + this.#collectPageLabels(); + for (const page of this.oldPages) { promises.push(this.#postCollectPageData(page)); } @@ -270,7 +275,12 @@ class PDFEditor { * @param {DocumentData} documentData * @return {Promise} */ - async #collectDocumentData(documentData) {} + async #collectDocumentData(documentData) { + const { document } = documentData; + await document.pdfManager + .ensureCatalog("rawPageLabels") + .then(pageLabels => (documentData.pageLabels = pageLabels)); + } /** * Post process the collected page data. @@ -306,6 +316,56 @@ class PDFEditor { pageData.annotations = newAnnotations.length > 0 ? newAnnotations : null; } + async #collectPageLabels() { + // We can only preserve page labels when editing a single PDF file. + // This is consistent with behavior in Adobe Acrobat. + if (!this.hasSingleFile) { + return; + } + const { + documentData: { document, pageLabels }, + } = this.oldPages[0]; + if (!pageLabels) { + return; + } + const numPages = document.numPages; + const oldPageLabels = []; + const oldPageIndices = new Set( + this.oldPages.map(({ page: { pageIndex } }) => pageIndex) + ); + let currentLabel = null; + let stFirstIndex = -1; + for (let i = 0; i < numPages; i++) { + const newLabel = pageLabels.get(i); + if (newLabel) { + currentLabel = newLabel; + stFirstIndex = currentLabel.has("St") ? i : -1; + } + if (!oldPageIndices.has(i)) { + continue; + } + if (stFirstIndex !== -1) { + const st = currentLabel.get("St"); + currentLabel = currentLabel.clone(); + currentLabel.set("St", st + (i - stFirstIndex)); + stFirstIndex = -1; + } + oldPageLabels.push(currentLabel); + } + currentLabel = oldPageLabels[0]; + let currentIndex = 0; + const newPageLabels = (this.pageLabels = [[0, currentLabel]]); + for (let i = 0, ii = oldPageLabels.length; i < ii; i++) { + const label = oldPageLabels[i]; + if (label === currentLabel) { + continue; + } + currentIndex = i; + currentLabel = label; + newPageLabels.push([currentIndex, currentLabel]); + } + } + /** * Create a copy of a page. * @param {number} pageIndex @@ -423,6 +483,66 @@ class PDFEditor { } } + /** + * Create a name or number tree from the given map. + * @param {Array<[string, any]>} map + * @returns {Ref} + */ + #makeNameNumTree(map, areNames) { + const allEntries = map.sort( + areNames + ? ([keyA], [keyB]) => keyA.localeCompare(keyB) + : ([keyA], [keyB]) => keyA - keyB + ); + const maxLeaves = + MAX_IN_NAME_TREE_NODE <= 1 ? allEntries.length : MAX_IN_NAME_TREE_NODE; + const [treeRef, treeDict] = this.newDict; + const stack = [{ dict: treeDict, entries: allEntries }]; + const valueType = areNames ? "Names" : "Nums"; + + while (stack.length > 0) { + const { dict, entries } = stack.pop(); + if (entries.length <= maxLeaves) { + dict.set("Limits", [entries[0][0], entries.at(-1)[0]]); + dict.set(valueType, entries.flat()); + continue; + } + const entriesChunks = []; + const chunkSize = Math.max( + maxLeaves, + Math.ceil(entries.length / maxLeaves) + ); + for (let i = 0; i < entries.length; i += chunkSize) { + entriesChunks.push(entries.slice(i, i + chunkSize)); + } + const entriesRefs = []; + dict.set("Kids", entriesRefs); + for (const chunk of entriesChunks) { + const [entriesRef, entriesDict] = this.newDict; + entriesRefs.push(entriesRef); + entriesDict.set("Limits", [chunk[0][0], chunk.at(-1)[0]]); + stack.push({ dict: entriesDict, entries: chunk }); + } + } + return treeRef; + } + + /** + * Create the page labels tree if it exists. + */ + #makePageLabelsTree() { + const { pageLabels } = this; + if (!pageLabels || pageLabels.length === 0) { + return; + } + const { rootDict } = this; + const pageLabelsRef = this.#makeNameNumTree( + this.pageLabels, + /* areNames = */ false + ); + rootDict.set("PageLabels", pageLabelsRef); + } + /** * Create the root dictionary. * @returns {Promise} @@ -432,6 +552,7 @@ class PDFEditor { rootDict.setIfName("Type", "Catalog"); rootDict.set("Version", this.version); this.#makePageTree(); + this.#makePageLabelsTree(); } /** diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 91091a44e..9fc4acaf5 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -757,3 +757,4 @@ !doc_1_3_pages.pdf !doc_2_3_pages.pdf !doc_3_3_pages.pdf +!labelled_pages.pdf diff --git a/test/pdfs/labelled_pages.pdf b/test/pdfs/labelled_pages.pdf new file mode 100755 index 0000000000000000000000000000000000000000..68e389f40c5d467f478b2899378d375682f64ce3 GIT binary patch literal 6717 zcmeHMdvFuS8JDrKV~xc)E+vmTun~q5W9fG9C7nhRmL(gTSXh2g8;px|y0eij>EzQX zHcY02ha@}#hO}uW88a;zNFmUMn2=0TXfbI^2-JWRm=sz_NRxpQNT#KQv{2}t{1PC~ z#51J+$C{Bwce~%~_x*PF+ua!+SBU``S>23J|9b30otEYR1QOq@vs#JrKv?w2f#sqA z5K#_T6EYDMfI&ntFe8eX4-g`1084Quq7>8vnnD0E<_5c6Cx!)82T1nOX&G|ZhD-EI zg6Q$dp(wWKl!CEPI0`7jBa1@7uShaLq}@)G1cHhv6D2{PBKC_AWv+UwtB8Y6k~1?b z!%-}4c93S;;dGH4Wnz)TN!zWEb3il46&Dw|93>o07m+k%iyUSrD&m}`Vu!tfC<_Z> zdp!=EQU5nefHq_Qs)%wi+@vf86jYsgLXFc*R7sKo7#MG_#>fDMCTf;NM53-rY$R&y z5x}uw1fq&8`a(MG(z84-WP5q(+GiJq&s@Ig2Ok(`hkK16>Coiz+@@ zX_v)Dofhh}GiDT5l<2gBFk-7p=Z;n^f zVafj9BoLgE-kJV%`b4^Q^7%$1N&8!Ke5DQeCbhDq>0y^C=3DDgNy7HHqauUJORDYeR4F0~3FU zgX+ZcL8)n&Pz=J20|uP36e*J0>!A^g<1dRy{A=RyZv)}R?D37*is>eX6+p9bQ&nPA ziphQv6A&}4N>VV#0PIhz>gBNcv9+EsYQ`Y;1mcZ>b$U$mu2S;ev96#0-<)+SOUKT- z>h0KBS7+j}v!1XP-UH5$Ht$HC7RN4llrY;RS!zlGa#R6`9a?HA2I7){r1~e$3ck5o z2_-H$>z;r7@!u6re)`tog%|Uw==sc7K1n^ZddlT@GmN|M%gK6m&#M>qU+Y}2^cho9 zY2BWiMs@t;vqCXi_<8Py%uRV2&-bSMG+Ou_bJ?U>U8(u!3*DdOahrpa4lZ$Bb=nS| ztF1eGWqOZ>D_OiZ$y$sOmWvV7M{n`KBR+qN zr~qDU3WRO?ulDZOgMeVu*D`MCjyT1ofzsu&SiO9HjemKIpA+{@HPr9Bi0;bH}} zhl1g#rQN3Y;o?=a;Q#7tLJ#6ylop%bp*Dazw+A>SSp=LB8T=3;Tyz*&6C=PzQvo0$ z9s-ah5HcXnLLv)d2g^6TU9IG-ve0O$a+M6&!%sH-Qbmba2%@d6&Dcg6CAo<}9LEum zBuLVLdl;h2!iulm5ROjoS2C!_B}V;nAfg1MFi`dScqyjX^!lNaQMEk~SST`NC>%A$ zgEac35Yg_75XcCLWI}>}NG1}KgK^gcKOqLi5U!x2SPmtX!ZQy) z6iJRYm}r4D&;l|Wc#_A;SyD7ZhC(9h4-e~2kWJ8wBe8g7Nr?GH`I|s>TmJz)Rze*& z3FD}LkjCHQQHo~>VrYtpH>o4)Tm1|N_$`_lH!>=4$Ke`>Yg7n~s`EIz#^D+j0;B3Y z&aQg~mv(rQfzNAg`Zm0UNVbbfIIbFT?}f~RdoP5~)#u|g@A#SL-5X~TEr1?~sv)X` zs20+DJg;~6wH+mTXQ#;j{0&vTc1y?Zbgcsfz;|~}t6I2m$Fz+v{%ldODWkSUTiM)v z-w(8puJ753T$D>H|~^yRO@$qJ`YKnWXaT@41SkqSn$&=IxO09)IJaMhJ;sD)y;zUk( zQDNne<^|;Z)`O=`rKCNm3H{pe+hjo{oUJ9liJ Date: Fri, 7 Nov 2025 18:21:51 +0100 Subject: [PATCH 15/26] Update the named page destinations when some pdf are combined (bug 1997379) and remove link annotations pointing on a deleted page. --- src/core/editor/pdf_editor.js | 271 ++++++++++++++++++++++++++++++++-- src/core/primitives.js | 6 + test/pdfs/.gitignore | 1 + test/pdfs/extract_link.pdf | Bin 0 -> 10213 bytes test/unit/api_spec.js | 146 ++++++++++++++++++ test/unit/primitives_spec.js | 6 + 6 files changed, 415 insertions(+), 15 deletions(-) create mode 100755 test/pdfs/extract_link.pdf diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 5241863a7..42769f636 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -32,6 +32,8 @@ class PageData { this.page = page; this.documentData = documentData; this.annotations = null; + // Named destinations which points to this page. + this.pointingNamedDestinations = null; documentData.pagesMap.put(page.ref, this); } @@ -40,9 +42,13 @@ class PageData { class DocumentData { constructor(document) { this.document = document; + this.destinations = null; this.pageLabels = null; this.pagesMap = new RefSetCache(); this.oldRefMapping = new RefSetCache(); + this.dedupNamedDestinations = new Map(); + this.usedNamedDestinations = new Set(); + this.postponedRefCopies = new RefSetCache(); } } @@ -64,6 +70,7 @@ class PDFEditor { this.title = title; this.author = author; this.pageLabels = null; + this.namedDestinations = new Map(); } /** @@ -114,15 +121,21 @@ class PDFEditor { if (newRef) { return newRef; } + const oldRef = obj; + obj = await xref.fetchAsync(oldRef); + if (typeof obj === "number") { + // Simple value; no need to create a new reference. + return obj; + } + newRef = this.newRef; - oldRefMapping.put(obj, newRef); - obj = await xref.fetchAsync(obj); + oldRefMapping.put(oldRef, newRef); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { if ( obj instanceof Dict && isName(obj.get("Type"), "Page") && - !this.currentDocument.pagesMap.has(obj) + !this.currentDocument.pagesMap.has(oldRef) ) { throw new Error( "Add a deleted page to the document is not supported." @@ -134,11 +147,20 @@ class PDFEditor { return newRef; } const promises = []; + const { + currentDocument: { postponedRefCopies }, + } = this; if (Array.isArray(obj)) { if (mustClone) { obj = obj.slice(); } for (let i = 0, ii = obj.length; i < ii; i++) { + const postponedActions = postponedRefCopies.get(obj[i]); + if (postponedActions) { + // The object is a reference that needs to be copied later. + postponedActions.push(ref => (obj[i] = ref)); + continue; + } promises.push( this.#collectDependencies(obj[i], true, xref).then( newObj => (obj[i] = newObj) @@ -159,6 +181,12 @@ class PDFEditor { } if (dict) { for (const [key, rawObj] of dict.getRawEntries()) { + const postponedActions = postponedRefCopies.get(rawObj); + if (postponedActions) { + // The object is a reference that needs to be copied later. + postponedActions.push(ref => dict.set(key, ref)); + continue; + } promises.push( this.#collectDependencies(rawObj, true, xref).then(newObj => dict.set(key, newObj) @@ -189,11 +217,13 @@ class PDFEditor { const promises = []; let newIndex = 0; this.hasSingleFile = pageInfos.length === 1; + const allDocumentData = []; for (const { document, includePages, excludePages } of pageInfos) { if (!document) { continue; } const documentData = new DocumentData(document); + allDocumentData.push(documentData); promises.push(this.#collectDocumentData(documentData)); let keptIndices, keptRanges, deletedIndices, deletedRanges; for (const page of includePages || []) { @@ -256,6 +286,7 @@ class PDFEditor { await Promise.all(promises); promises.length = 0; + this.#collectValidDestinations(allDocumentData); this.#collectPageLabels(); for (const page of this.oldPages) { @@ -263,10 +294,15 @@ class PDFEditor { } await Promise.all(promises); + this.#findDuplicateNamedDestinations(); + this.#setPostponedRefCopies(allDocumentData); + for (let i = 0, ii = this.oldPages.length; i < ii; i++) { this.newPages[i] = await this.#makePageCopy(i, null); } + this.#fixPostponedRefCopies(allDocumentData); + return this.writePDF(); } @@ -276,10 +312,17 @@ class PDFEditor { * @return {Promise} */ async #collectDocumentData(documentData) { - const { document } = documentData; - await document.pdfManager - .ensureCatalog("rawPageLabels") - .then(pageLabels => (documentData.pageLabels = pageLabels)); + const { + document: { pdfManager }, + } = documentData; + await Promise.all([ + pdfManager + .ensureCatalog("destinations") + .then(destinations => (documentData.destinations = destinations)), + pdfManager + .ensureCatalog("rawPageLabels") + .then(pageLabels => (documentData.pageLabels = pageLabels)), + ]); } /** @@ -290,6 +333,7 @@ class PDFEditor { async #postCollectPageData(pageData) { const { page: { xref, annotations }, + documentData: { pagesMap, destinations, usedNamedDestinations }, } = pageData; if (!annotations) { @@ -300,22 +344,185 @@ class PDFEditor { let newAnnotations = []; let newIndex = 0; - // TODO: remove only links to deleted pages. + // Filter out annotations that are linking to deleted pages. for (const annotationRef of annotations) { const newAnnotationIndex = newIndex++; promises.push( xref.fetchIfRefAsync(annotationRef).then(async annotationDict => { if (!isName(annotationDict.get("Subtype"), "Link")) { newAnnotations[newAnnotationIndex] = annotationRef; + return; + } + const action = annotationDict.get("A"); + const dest = + action instanceof Dict + ? action.get("D") + : annotationDict.get("Dest"); + + if ( + !dest /* not a destination */ || + (Array.isArray(dest) && + (!(dest[0] instanceof Ref) || pagesMap.has(dest[0]))) + ) { + // Keep the annotation as is: it isn't linking to a deleted page. + newAnnotations[newAnnotationIndex] = annotationRef; + } else if (typeof dest === "string") { + const destString = stringToPDFString( + dest, + /* keepEscapeSequence = */ true + ); + if (destinations.has(destString)) { + // Keep the annotation as is: the named destination is valid. + // Valid named destinations have been collected previously (see + // #collectValidDestinations). + newAnnotations[newAnnotationIndex] = annotationRef; + usedNamedDestinations.add(destString); + } } }) ); } + await Promise.all(promises); newAnnotations = newAnnotations.filter(annot => !!annot); pageData.annotations = newAnnotations.length > 0 ? newAnnotations : null; } + /** + * Some references cannot be copied right away since they correspond to some + * pages that haven't been processed yet. Postpone the copy of those + * references. + * @param {Array} allDocumentData + */ + #setPostponedRefCopies(allDocumentData) { + for (const { postponedRefCopies, pagesMap } of allDocumentData) { + for (const oldPageRef of pagesMap.keys()) { + postponedRefCopies.put(oldPageRef, []); + } + } + } + + /** + * Fix all postponed reference copies. + * @param {Array} allDocumentData + */ + #fixPostponedRefCopies(allDocumentData) { + for (const { postponedRefCopies, oldRefMapping } of allDocumentData) { + for (const [oldRef, actions] of postponedRefCopies.items()) { + const newRef = oldRefMapping.get(oldRef); + for (const action of actions) { + action(newRef); + } + } + postponedRefCopies.clear(); + } + } + + /** + * Collect named destinations that are still valid (i.e. pointing to kept + * pages). + * @param {Array} allDocumentData + */ + #collectValidDestinations(allDocumentData) { + // TODO: Handle OpenAction as well. + for (const documentData of allDocumentData) { + if (!documentData.destinations) { + continue; + } + const { destinations, pagesMap } = documentData; + const newDestinations = (documentData.destinations = new Map()); + for (const [key, dest] of Object.entries(destinations)) { + const pageRef = dest[0]; + const pageData = pagesMap.get(pageRef); + if (!pageData) { + continue; + } + (pageData.pointingNamedDestinations ||= new Set()).add(key); + newDestinations.set(key, dest); + } + } + } + + /** + * Find and rename duplicate named destinations. + */ + #findDuplicateNamedDestinations() { + const { namedDestinations } = this; + for (let i = 0, ii = this.oldPages.length; i < ii; i++) { + const page = this.oldPages[i]; + const { + documentData: { + destinations, + dedupNamedDestinations, + usedNamedDestinations, + }, + } = page; + let { pointingNamedDestinations } = page; + + if (!pointingNamedDestinations) { + // No named destinations pointing to this page. + continue; + } + // Keep only the named destinations that are still used. + page.pointingNamedDestinations = pointingNamedDestinations = + pointingNamedDestinations.intersection(usedNamedDestinations); + + for (const pointingDest of pointingNamedDestinations) { + if (!usedNamedDestinations.has(pointingDest)) { + // If the named destination isn't used, we can keep it as is. + continue; + } + const dest = destinations.get(pointingDest).slice(); + if (!namedDestinations.has(pointingDest)) { + // If the named destination hasn't been used yet, we can keep it + // as is. + namedDestinations.set(pointingDest, dest); + continue; + } + // Create a new unique named destination. + const newName = `${pointingDest}_p${i + 1}`; + dedupNamedDestinations.set(pointingDest, newName); + namedDestinations.set(newName, dest); + } + } + } + + /** + * Fix named destinations in the annotations. + * @param {Array} annotations + * @param {Map} dedupNamedDestinations + */ + #fixNamedDestinations(annotations, dedupNamedDestinations) { + if (dedupNamedDestinations.size === 0) { + return; + } + const fixDestination = (dict, key, dest) => { + if (typeof dest === "string") { + dict.set( + key, + dedupNamedDestinations.get( + stringToPDFString(dest, /* keepEscapeSequence = */ true) + ) || dest + ); + } + }; + + for (const annotRef of annotations) { + const annotDict = this.xref[annotRef.num]; + if (!isName(annotDict.get("Subtype"), "Link")) { + continue; + } + const action = annotDict.get("A"); + if (action instanceof Dict && action.has("D")) { + const dest = action.get("D"); + fixDestination(action, "D", dest); + continue; + } + const dest = annotDict.get("Dest"); + fixDestination(annotDict, "Dest", dest); + } + } + async #collectPageLabels() { // We can only preserve page labels when editing a single PDF file. // This is consistent with behavior in Adobe Acrobat. @@ -372,14 +579,23 @@ class PDFEditor { * @returns {Promise} the page reference in the new PDF document. */ async #makePageCopy(pageIndex) { - const { page, documentData, annotations } = this.oldPages[pageIndex]; + const { page, documentData, annotations, pointingNamedDestinations } = + this.oldPages[pageIndex]; this.currentDocument = documentData; - const { oldRefMapping } = documentData; + const { dedupNamedDestinations, oldRefMapping } = documentData; const { xref, rotate, mediaBox, resources, ref: oldPageRef } = page; const pageRef = this.newRef; const pageDict = (this.xref[pageRef.num] = page.pageDict.clone()); oldRefMapping.put(oldPageRef, pageRef); + if (pointingNamedDestinations) { + for (const pointingDest of pointingNamedDestinations) { + const name = dedupNamedDestinations.get(pointingDest) || pointingDest; + const dest = this.namedDestinations.get(name); + dest[0] = pageRef; + } + } + // No need to keep these entries as we'll set them again later. for (const key of [ "Rotate", @@ -416,10 +632,16 @@ class PDFEditor { "Resources", await this.#collectDependencies(resources, true, xref) ); - pageDict.setIfArray( - "Annots", - await this.#collectDependencies(annotations, true, xref) - ); + + if (annotations) { + const newAnnotations = await this.#collectDependencies( + annotations, + true, + xref + ); + this.#fixNamedDestinations(newAnnotations, dedupNamedDestinations); + pageDict.setIfArray("Annots", newAnnotations); + } if (this.useObjectStreams) { const newLastRef = this.newRefCount; @@ -485,7 +707,7 @@ class PDFEditor { /** * Create a name or number tree from the given map. - * @param {Array<[string, any]>} map + * @param {Array<[string|number, any]>} map * @returns {Ref} */ #makeNameNumTree(map, areNames) { @@ -543,6 +765,24 @@ class PDFEditor { rootDict.set("PageLabels", pageLabelsRef); } + #makeDestinationsTree() { + const { namedDestinations } = this; + if (namedDestinations.size === 0) { + return; + } + if (!this.namesDict) { + [this.namesRef, this.namesDict] = this.newDict; + this.rootDict.set("Names", this.namesRef); + } + this.namesDict.set( + "Dests", + this.#makeNameNumTree( + Array.from(namedDestinations.entries()), + /* areNames = */ true + ) + ); + } + /** * Create the root dictionary. * @returns {Promise} @@ -553,6 +793,7 @@ class PDFEditor { rootDict.set("Version", this.version); this.#makePageTree(); this.#makePageLabelsTree(); + this.#makeDestinationsTree(); } /** diff --git a/src/core/primitives.js b/src/core/primitives.js index 22cdd2527..854341dbd 100644 --- a/src/core/primitives.js +++ b/src/core/primitives.js @@ -439,6 +439,12 @@ class RefSetCache { yield [Ref.fromString(ref), value]; } } + + *keys() { + for (const ref of this._map.keys()) { + yield Ref.fromString(ref); + } + } } function isName(v, name) { diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 9fc4acaf5..e2bb0f1e8 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -758,3 +758,4 @@ !doc_2_3_pages.pdf !doc_3_3_pages.pdf !labelled_pages.pdf +!extract_link.pdf diff --git a/test/pdfs/extract_link.pdf b/test/pdfs/extract_link.pdf new file mode 100755 index 0000000000000000000000000000000000000000..e9e4be43296109fcc1c629dc61fa09c809c6c261 GIT binary patch literal 10213 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 P#svjpNF?@0iP--DBXrd} literal 0 HcmV?d00001 diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 892166732..34559c4b4 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -5337,6 +5337,15 @@ small scripts as well as for`); }); describe("PDF page editing", function () { + const getPageRefs = async pdfDoc => { + const refs = []; + for (let i = 1; i <= pdfDoc.numPages; i++) { + const page = await pdfDoc.getPage(i); + refs.push(page.ref); + } + return refs; + }; + describe("Merge pdfs", function () { it("should merge three PDFs", async function () { const loadingTask = getDocument( @@ -5576,5 +5585,142 @@ small scripts as well as for`); await loadingTask.destroy(); }); }); + + describe("Named destinations", function () { + it("extract page and check destinations", async function () { + let loadingTask = getDocument(buildGetDocumentParams("issue6204.pdf")); + let pdfDoc = await loadingTask.promise; + let pagesRef = await getPageRefs(pdfDoc); + let destinations = await pdfDoc.getDestinations(); + expect(destinations).toEqual({ + "Page.1": [pagesRef[0], { name: "XYZ" }, 0, 375, null], + "Page.2": [pagesRef[1], { name: "XYZ" }, 0, 375, null], + }); + + let data = await pdfDoc.extractPages([ + { document: null }, + { document: null }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument(data); + pdfDoc = await loadingTask.promise; + + expect(pdfDoc.numPages).toEqual(4); + + pagesRef = await getPageRefs(pdfDoc); + destinations = await pdfDoc.getDestinations(); + expect(destinations).toEqual({ + "Page.1": [pagesRef[0], { name: "XYZ" }, 0, 375, null], + "Page.2": [pagesRef[1], { name: "XYZ" }, 0, 375, null], + "Page.1_p3": [pagesRef[2], { name: "XYZ" }, 0, 375, null], + "Page.2_p4": [pagesRef[3], { name: "XYZ" }, 0, 375, null], + }); + const expectedDests = ["Page.2", "Page.1", "Page.2_p4", "Page.1_p3"]; + for (let i = 1; i <= 4; i++) { + const pdfPage = await pdfDoc.getPage(i); + const annots = await pdfPage.getAnnotations(); + expect(annots.length).toEqual(1); + expect(annots[0].dest).toEqual(expectedDests[i - 1]); + } + + data = await pdfDoc.extractPages([ + { document: null }, + { document: null }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument(data); + pdfDoc = await loadingTask.promise; + + expect(pdfDoc.numPages).toEqual(8); + + pagesRef = await getPageRefs(pdfDoc); + destinations = await pdfDoc.getDestinations(); + expect(destinations).toEqual({ + "Page.1": [pagesRef[0], { name: "XYZ" }, 0, 375, null], + "Page.2": [pagesRef[1], { name: "XYZ" }, 0, 375, null], + "Page.1_p3": [pagesRef[2], { name: "XYZ" }, 0, 375, null], + "Page.2_p4": [pagesRef[3], { name: "XYZ" }, 0, 375, null], + "Page.1_p5": [pagesRef[4], { name: "XYZ" }, 0, 375, null], + "Page.2_p6": [pagesRef[5], { name: "XYZ" }, 0, 375, null], + "Page.1_p3_p7": [pagesRef[6], { name: "XYZ" }, 0, 375, null], + "Page.2_p4_p8": [pagesRef[7], { name: "XYZ" }, 0, 375, null], + }); + expectedDests.push( + "Page.2_p6", + "Page.1_p5", + "Page.2_p4_p8", + "Page.1_p3_p7" + ); + for (let i = 1; i <= 8; i++) { + const pdfPage = await pdfDoc.getPage(i); + const annots = await pdfPage.getAnnotations(); + expect(annots.length).toEqual(1); + expect(annots[0].dest).toEqual(expectedDests[i - 1]); + } + await loadingTask.destroy(); + }); + + it("extract pages and check deleted destinations", async function () { + let loadingTask = getDocument(buildGetDocumentParams("issue6204.pdf")); + let pdfDoc = await loadingTask.promise; + const data = await pdfDoc.extractPages([ + { document: null }, + { document: null, excludePages: [0] }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument(data); + pdfDoc = await loadingTask.promise; + + expect(pdfDoc.numPages).toEqual(3); + + const pagesRef = await getPageRefs(pdfDoc); + const destinations = await pdfDoc.getDestinations(); + expect(destinations).toEqual({ + "Page.1": [pagesRef[0], { name: "XYZ" }, 0, 375, null], + "Page.2": [pagesRef[1], { name: "XYZ" }, 0, 375, null], + }); + const pdfPage = await pdfDoc.getPage(3); + const annots = await pdfPage.getAnnotations(); + expect(annots.length).toEqual(0); + }); + }); + + describe("Destinations with a page reference", function () { + it("extract page and check destinations", async function () { + let loadingTask = getDocument( + buildGetDocumentParams("extract_link.pdf") + ); + let pdfDoc = await loadingTask.promise; + let pagesRef = await getPageRefs(pdfDoc); + let pdfPage = await pdfDoc.getPage(1); + let annotations = await pdfPage.getAnnotations(); + expect(annotations.length).toEqual(1); + expect(annotations[0].dest[0]).toEqual(pagesRef[1]); + + const data = await pdfDoc.extractPages([ + { document: null }, + { document: null }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument(data); + pdfDoc = await loadingTask.promise; + + expect(pdfDoc.numPages).toEqual(4); + + pagesRef = await getPageRefs(pdfDoc); + for (let i = 1; i <= 4; i += 2) { + pdfPage = await pdfDoc.getPage(i); + annotations = await pdfPage.getAnnotations(); + expect(annotations.length).toEqual(1); + expect(annotations[0].dest[0]).toEqual(pagesRef[i]); + } + + await loadingTask.destroy(); + }); + }); }); }); diff --git a/test/unit/primitives_spec.js b/test/unit/primitives_spec.js index b71df0b83..ed9993121 100644 --- a/test/unit/primitives_spec.js +++ b/test/unit/primitives_spec.js @@ -562,6 +562,12 @@ describe("primitives", function () { [ref2, obj2], ]); }); + + it("should support iteration over keys", function () { + cache.put(ref1, obj1); + cache.put(ref2, obj2); + expect([...cache.keys()]).toEqual([ref1, ref2]); + }); }); describe("isName", function () { From 7fc23de26f4710e7ef356d693c74591cf71c3659 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 8 Nov 2025 19:25:10 +0100 Subject: [PATCH 16/26] Update dependencies to the most recent versions --- package-lock.json | 136 +++++++++++++++++++++++----------------------- package.json | 14 ++--- 2 files changed, 75 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2b4137b9..2f01d97e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,12 @@ "@metalsmith/layouts": "^3.0.0", "@metalsmith/markdown": "^1.10.0", "@napi-rs/canvas": "^0.1.81", - "@types/node": "^24.9.1", + "@types/node": "^24.10.0", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", - "caniuse-lite": "^1.0.30001751", + "caniuse-lite": "^1.0.30001754", "core-js": "^3.46.0", - "eslint": "^9.38.0", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jasmine": "^4.2.2", @@ -30,7 +30,7 @@ "eslint-plugin-perfectionist": "^4.15.1", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-unicorn": "^62.0.0", - "globals": "^16.4.0", + "globals": "^16.5.0", "gulp": "^5.0.1", "gulp-cli": "^3.1.0", "gulp-postcss": "^10.0.0", @@ -42,15 +42,15 @@ "jsdoc": "^4.0.5", "jstransformer-nunjucks": "^1.2.0", "metalsmith": "^2.6.3", - "metalsmith-html-relative": "^2.0.8", + "metalsmith-html-relative": "^2.0.9", "ordered-read-streams": "^2.0.0", "pngjs": "^7.0.0", "postcss": "^8.5.6", "postcss-dir-pseudo-class": "^9.0.1", - "postcss-discard-comments": "^7.0.4", + "postcss-discard-comments": "^7.0.5", "postcss-nesting": "^13.0.2", "prettier": "^3.6.2", - "puppeteer": "^24.26.1", + "puppeteer": "^24.29.1", "stylelint": "^16.25.0", "stylelint-prettier": "^5.0.3", "svglint": "^4.1.2", @@ -1895,22 +1895,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", - "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1958,9 +1958,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { @@ -1981,13 +1981,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -2547,9 +2547,9 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.10.12", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.12.tgz", - "integrity": "sha512-mP9iLFZwH+FapKJLeA7/fLqOlSUwYpMwjR1P5J23qd4e7qGJwecJccJqHYrjw33jmIZYV4dtiTHPD/J+1e7cEw==", + "version": "2.10.13", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.13.tgz", + "integrity": "sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2765,9 +2765,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, "license": "MIT", "dependencies": { @@ -3847,9 +3847,9 @@ } }, "node_modules/bare-url": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.1.tgz", - "integrity": "sha512-v2yl0TnaZTdEnelkKtXZGnotiV6qATBlnNuUMrHl6v9Lmmrh9mw9RYyImPU7/4RahumSwQS1k2oKXcRfXcbjJw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -4174,9 +4174,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "dev": true, "funding": [ { @@ -4894,9 +4894,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1508733", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz", - "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==", + "version": "0.0.1521046", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", + "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", "dev": true, "license": "BSD-3-Clause", "peer": true @@ -5375,9 +5375,9 @@ } }, "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", "peer": true, @@ -5385,11 +5385,11 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.1", - "@eslint/core": "^0.16.0", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -6563,9 +6563,9 @@ } }, "node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { @@ -8529,15 +8529,15 @@ } }, "node_modules/metalsmith-html-relative": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/metalsmith-html-relative/-/metalsmith-html-relative-2.0.8.tgz", - "integrity": "sha512-mxaKo5KRon23iEJqHF2UhCQedZI1dTPwWSe2gnYbHuoMbkczd5eJK9GYmO40ji7EYnnda6p6CGe6twteGO60Wg==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/metalsmith-html-relative/-/metalsmith-html-relative-2.0.9.tgz", + "integrity": "sha512-r5QnNNtoNoLuxCcfGeqxGgxrmzeIdnt+ikUbRXtVD6EGqYG78f9fpjqAjvh1y8ao4NUgUJAJiQWdTFIl03D78w==", "dev": true, "license": "GPL-3.0-or-later", "dependencies": { "cheerio": "^1.1.2", "deepmerge": "^4.3.1", - "minimatch": "^10.0.3" + "minimatch": "^10.1.1" }, "engines": { "node": ">=20.18.1" @@ -8547,11 +8547,11 @@ } }, "node_modules/metalsmith-html-relative/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -9466,9 +9466,9 @@ } }, "node_modules/postcss-discard-comments": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.4.tgz", - "integrity": "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.5.tgz", + "integrity": "sha512-IR2Eja8WfYgN5n32vEGSctVQ1+JARfu4UH8M7bgGh1bC+xI/obsPJXaBpQF7MAByvgwZinhpHpdrmXtvVVlKcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9748,18 +9748,18 @@ } }, "node_modules/puppeteer": { - "version": "24.26.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.26.1.tgz", - "integrity": "sha512-3RG2UqclzMFolM2fS4bN8t5/EjZ0VwEoAGVxG8PMGeprjLzj+x0U4auH7MQ4B6ftW+u1JUnTTN8ab4ABPdl4mA==", + "version": "24.29.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.29.1.tgz", + "integrity": "sha512-pX05JV1mMP+1N0vP3I4DOVwjMdpihv2LxQTtSfw6CUm5F0ZFLUFE/LSZ4yUWHYaM3C11Hdu+sgn7uY7teq5MYw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.12", + "@puppeteer/browsers": "2.10.13", "chromium-bidi": "10.5.1", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1508733", - "puppeteer-core": "24.26.1", + "devtools-protocol": "0.0.1521046", + "puppeteer-core": "24.29.1", "typed-query-selector": "^2.12.0" }, "bin": { @@ -9770,16 +9770,16 @@ } }, "node_modules/puppeteer-core": { - "version": "24.26.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.26.1.tgz", - "integrity": "sha512-YHZdo3chJ5b9pTYVnuDuoI3UX/tWJFJyRZvkLbThGy6XeHWC+0KI8iN0UMCkvde5l/YOk3huiVZ/PvwgSbwdrA==", + "version": "24.29.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.29.1.tgz", + "integrity": "sha512-ErJ9qKCK+bdLvBa7QVSQTBSPm8KZbl1yC/WvhrZ0ut27hDf2QBzjDsn1IukzE1i1KtZ7NYGETOV4W1beoo9izA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.12", + "@puppeteer/browsers": "2.10.13", "chromium-bidi": "10.5.1", "debug": "^4.4.3", - "devtools-protocol": "0.0.1508733", + "devtools-protocol": "0.0.1521046", "typed-query-selector": "^2.12.0", "webdriver-bidi-protocol": "0.3.8", "ws": "^8.18.3" diff --git a/package.json b/package.json index b2b4eba8a..8a48fa3a0 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,12 @@ "@metalsmith/layouts": "^3.0.0", "@metalsmith/markdown": "^1.10.0", "@napi-rs/canvas": "^0.1.81", - "@types/node": "^24.9.1", + "@types/node": "^24.10.0", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", - "caniuse-lite": "^1.0.30001751", + "caniuse-lite": "^1.0.30001754", "core-js": "^3.46.0", - "eslint": "^9.38.0", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jasmine": "^4.2.2", @@ -25,7 +25,7 @@ "eslint-plugin-perfectionist": "^4.15.1", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-unicorn": "^62.0.0", - "globals": "^16.4.0", + "globals": "^16.5.0", "gulp": "^5.0.1", "gulp-cli": "^3.1.0", "gulp-postcss": "^10.0.0", @@ -37,15 +37,15 @@ "jsdoc": "^4.0.5", "jstransformer-nunjucks": "^1.2.0", "metalsmith": "^2.6.3", - "metalsmith-html-relative": "^2.0.8", + "metalsmith-html-relative": "^2.0.9", "ordered-read-streams": "^2.0.0", "pngjs": "^7.0.0", "postcss": "^8.5.6", "postcss-dir-pseudo-class": "^9.0.1", - "postcss-discard-comments": "^7.0.4", + "postcss-discard-comments": "^7.0.5", "postcss-nesting": "^13.0.2", "prettier": "^3.6.2", - "puppeteer": "^24.26.1", + "puppeteer": "^24.29.1", "stylelint": "^16.25.0", "stylelint-prettier": "^5.0.3", "svglint": "^4.1.2", From 398ba0331ccbc20e396b21f8d65cbf9c36358d13 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 8 Nov 2025 19:27:15 +0100 Subject: [PATCH 17/26] Update translations to the most recent versions --- l10n/be/viewer.ftl | 14 ++------------ l10n/bg/viewer.ftl | 4 ++++ l10n/bs/viewer.ftl | 15 --------------- l10n/ca/viewer.ftl | 4 ---- l10n/cs/viewer.ftl | 18 ++++-------------- l10n/cy/viewer.ftl | 14 ++------------ l10n/da/viewer.ftl | 14 ++------------ l10n/de/viewer.ftl | 12 ------------ l10n/dsb/viewer.ftl | 12 ------------ l10n/el/viewer.ftl | 14 ++------------ l10n/en-CA/viewer.ftl | 12 ------------ l10n/en-GB/viewer.ftl | 12 ------------ l10n/eo/viewer.ftl | 14 ++------------ l10n/es-AR/viewer.ftl | 12 ------------ l10n/es-CL/viewer.ftl | 14 ++------------ l10n/es-ES/viewer.ftl | 14 ++------------ l10n/es-MX/viewer.ftl | 12 ------------ l10n/eu/viewer.ftl | 12 ------------ l10n/fi/viewer.ftl | 12 ------------ l10n/fr/viewer.ftl | 12 ------------ l10n/fur/viewer.ftl | 12 ------------ l10n/fy-NL/viewer.ftl | 12 ------------ l10n/gn/viewer.ftl | 14 ++------------ l10n/he/viewer.ftl | 12 ------------ l10n/hsb/viewer.ftl | 12 ------------ l10n/hu/viewer.ftl | 12 ------------ l10n/hy-AM/viewer.ftl | 15 --------------- l10n/ia/viewer.ftl | 12 ------------ l10n/id/viewer.ftl | 15 --------------- l10n/is/viewer.ftl | 15 --------------- l10n/it/viewer.ftl | 16 ++-------------- l10n/ja/viewer.ftl | 9 +++++++-- l10n/ka/viewer.ftl | 12 ------------ l10n/kab/viewer.ftl | 12 ------------ l10n/kk/viewer.ftl | 15 +++------------ l10n/ko/viewer.ftl | 12 ------------ l10n/nb-NO/viewer.ftl | 14 ++------------ l10n/nl/viewer.ftl | 12 ------------ l10n/nn-NO/viewer.ftl | 12 ------------ l10n/pa-IN/viewer.ftl | 16 ++++------------ l10n/pl/viewer.ftl | 14 ++------------ l10n/pt-BR/viewer.ftl | 14 ++------------ l10n/rm/viewer.ftl | 12 ------------ l10n/ro/viewer.ftl | 16 ++-------------- l10n/ru/viewer.ftl | 12 ------------ l10n/sc/viewer.ftl | 1 - l10n/sk/viewer.ftl | 12 ------------ l10n/sl/viewer.ftl | 14 ++------------ l10n/sq/viewer.ftl | 14 ++------------ l10n/sv-SE/viewer.ftl | 12 ------------ l10n/tg/viewer.ftl | 14 ++------------ l10n/th/viewer.ftl | 12 ------------ l10n/tr/viewer.ftl | 14 ++------------ l10n/vi/viewer.ftl | 12 ------------ l10n/zh-CN/viewer.ftl | 14 ++------------ l10n/zh-TW/viewer.ftl | 12 ------------ 56 files changed, 58 insertions(+), 649 deletions(-) diff --git a/l10n/be/viewer.ftl b/l10n/be/viewer.ftl index 73c2b4664..31d44c288 100644 --- a/l10n/be/viewer.ftl +++ b/l10n/be/viewer.ftl @@ -622,18 +622,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ะ”ะทะตัะฝะฝั– -pdfjs-editor-edit-comment-actions-button = - .title = ะ”ะทะตัะฝะฝั– -pdfjs-editor-edit-comment-close-button-label = ะ—ะฐะบั€ั‹ั†ัŒ -pdfjs-editor-edit-comment-close-button = - .title = ะ—ะฐะบั€ั‹ั†ัŒ -pdfjs-editor-edit-comment-actions-edit-button-label = ะŸั€ะฐัžะบะฐ -pdfjs-editor-edit-comment-actions-delete-button-label = ะ’ั‹ะดะฐะปั–ั†ัŒ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ะฃะฒัะดะทั–ั†ะต ัะฒะพะน ะบะฐะผะตะฝั‚ะฐั€ั‹ะน -pdfjs-editor-edit-comment-manager-cancel-button = ะกะบะฐัะฐะฒะฐั†ัŒ -pdfjs-editor-edit-comment-manager-save-button = ะ—ะฐั…ะฐะฒะฐั†ัŒ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = ะ—ะผัะฝั–ั†ัŒ ะบะฐะผะตะฝั‚ะฐั€ั‹ะน pdfjs-editor-edit-comment-dialog-save-button-when-editing = ะะฑะฝะฐะฒั–ั†ัŒ @@ -648,6 +636,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = ะกะบะฐัะฐะฒะฐั†ัŒ pdfjs-editor-edit-comment-button = .title = ะ—ะผัะฝั–ั†ัŒ ะบะฐะผะตะฝั‚ะฐั€ั‹ะน +pdfjs-editor-add-comment-button = + .title = ะ”ะฐะดะฐั†ัŒ ะบะฐะผะตะฝั‚ะฐั€ั‹ะน ## Main menu for adding/removing signatures diff --git a/l10n/bg/viewer.ftl b/l10n/bg/viewer.ftl index b372ff45f..90cdbeaaf 100644 --- a/l10n/bg/viewer.ftl +++ b/l10n/bg/viewer.ftl @@ -382,3 +382,7 @@ pdfjs-editor-colorpicker-red = pdfjs-editor-new-alt-text-disclaimer-learn-more-url = ะะฐัƒั‡ะตั‚ะต ะฟะพะฒะตั‡ะต pdfjs-editor-new-alt-text-not-now-button = ะะต ัะตะณะฐ + +## Image alt-text settings + +pdfjs-editor-alt-text-settings-delete-model-button = ะ˜ะทั‚ั€ะธะฒะฐะฝะต diff --git a/l10n/bs/viewer.ftl b/l10n/bs/viewer.ftl index 9bb77fee8..251a62ea4 100644 --- a/l10n/bs/viewer.ftl +++ b/l10n/bs/viewer.ftl @@ -573,21 +573,6 @@ pdfjs-editor-add-signature-cancel-button = Otkaลพi pdfjs-editor-add-signature-add-button = Dodaj pdfjs-editor-edit-signature-update-button = Aลพuriraj -## Edit a comment dialog - -pdfjs-editor-edit-comment-actions-button-label = Radnje -pdfjs-editor-edit-comment-actions-button = - .title = Radnje -pdfjs-editor-edit-comment-close-button-label = Zatvori -pdfjs-editor-edit-comment-close-button = - .title = Zatvori -pdfjs-editor-edit-comment-actions-edit-button-label = Uredi -pdfjs-editor-edit-comment-actions-delete-button-label = Izbriลกi -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Unesite svoj komentar -pdfjs-editor-edit-comment-manager-cancel-button = Otkaลพi -pdfjs-editor-edit-comment-manager-save-button = Saฤuvaj - ## Edit a comment button in the editor toolbar pdfjs-editor-edit-comment-button = diff --git a/l10n/ca/viewer.ftl b/l10n/ca/viewer.ftl index 9d38ef743..4e0d9cbb7 100644 --- a/l10n/ca/viewer.ftl +++ b/l10n/ca/viewer.ftl @@ -272,7 +272,3 @@ pdfjs-editor-alt-text-cancel-button = Cancelยทla ## Dialog buttons pdfjs-editor-add-signature-cancel-button = Cancelยทla - -## Edit a comment dialog - -pdfjs-editor-edit-comment-manager-cancel-button = Cancelยทla diff --git a/l10n/cs/viewer.ftl b/l10n/cs/viewer.ftl index 29373fce2..6f9a6f687 100644 --- a/l10n/cs/viewer.ftl +++ b/l10n/cs/viewer.ftl @@ -564,8 +564,8 @@ pdfjs-editor-add-signature-dialog-title = Pล™idat podpis ## Tab names # Type is a verb (you can type your name as signature) -pdfjs-editor-add-signature-type-button = Typ - .title = Typ +pdfjs-editor-add-signature-type-button = Psรกt + .title = Psรกt # Draw is a verb (you can draw your signature) pdfjs-editor-add-signature-draw-button = Kreslit .title = Kreslit @@ -626,18 +626,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Akce -pdfjs-editor-edit-comment-actions-button = - .title = Akce -pdfjs-editor-edit-comment-close-button-label = Zavล™รญt -pdfjs-editor-edit-comment-close-button = - .title = Zavล™รญt -pdfjs-editor-edit-comment-actions-edit-button-label = Upravit -pdfjs-editor-edit-comment-actions-delete-button-label = Smazat -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Zadejte komentรกล™ -pdfjs-editor-edit-comment-manager-cancel-button = Zruลกit -pdfjs-editor-edit-comment-manager-save-button = Uloลพit # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Upravit komentรกล™ pdfjs-editor-edit-comment-dialog-save-button-when-editing = Aktualizovat @@ -652,6 +640,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Zruลกit pdfjs-editor-edit-comment-button = .title = Upravit komentรกล™ +pdfjs-editor-add-comment-button = + .title = Pล™idรกnรญ komentรกล™e ## Main menu for adding/removing signatures diff --git a/l10n/cy/viewer.ftl b/l10n/cy/viewer.ftl index fc8a2669e..62e228413 100644 --- a/l10n/cy/viewer.ftl +++ b/l10n/cy/viewer.ftl @@ -634,18 +634,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Gweithredoedd -pdfjs-editor-edit-comment-actions-button = - .title = Gweithredoedd -pdfjs-editor-edit-comment-close-button-label = Cau -pdfjs-editor-edit-comment-close-button = - .title = Cau -pdfjs-editor-edit-comment-actions-edit-button-label = Golygu -pdfjs-editor-edit-comment-actions-delete-button-label = Dileu -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Rhowch eich sylw -pdfjs-editor-edit-comment-manager-cancel-button = Diddymu -pdfjs-editor-edit-comment-manager-save-button = Cadw # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Golygu sylw pdfjs-editor-edit-comment-dialog-save-button-when-editing = Diweddaru @@ -660,6 +648,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Diddymu pdfjs-editor-edit-comment-button = .title = Golygu sylw +pdfjs-editor-add-comment-button = + .title = Ychwanegu sylw ## Main menu for adding/removing signatures diff --git a/l10n/da/viewer.ftl b/l10n/da/viewer.ftl index 4f90b6aac..08552b58c 100644 --- a/l10n/da/viewer.ftl +++ b/l10n/da/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Handlinger -pdfjs-editor-edit-comment-actions-button = - .title = Handlinger -pdfjs-editor-edit-comment-close-button-label = Luk -pdfjs-editor-edit-comment-close-button = - .title = Luk -pdfjs-editor-edit-comment-actions-edit-button-label = Rediger -pdfjs-editor-edit-comment-actions-delete-button-label = Slet -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Indtast din kommentar -pdfjs-editor-edit-comment-manager-cancel-button = Annuller -pdfjs-editor-edit-comment-manager-save-button = Gem # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Rediger kommentar pdfjs-editor-edit-comment-dialog-save-button-when-editing = Opdater @@ -644,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Annuller pdfjs-editor-edit-comment-button = .title = Rediger kommentar +pdfjs-editor-add-comment-button = + .title = Tilfรธj kommentar ## Main menu for adding/removing signatures diff --git a/l10n/de/viewer.ftl b/l10n/de/viewer.ftl index a5c6b0d1c..10c9ef122 100644 --- a/l10n/de/viewer.ftl +++ b/l10n/de/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Aktionen -pdfjs-editor-edit-comment-actions-button = - .title = Aktionen -pdfjs-editor-edit-comment-close-button-label = SchlieรŸen -pdfjs-editor-edit-comment-close-button = - .title = SchlieรŸen -pdfjs-editor-edit-comment-actions-edit-button-label = Bearbeiten -pdfjs-editor-edit-comment-actions-delete-button-label = Lรถschen -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Kommentar eingeben -pdfjs-editor-edit-comment-manager-cancel-button = Abbrechen -pdfjs-editor-edit-comment-manager-save-button = Speichern # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Kommentar bearbeiten pdfjs-editor-edit-comment-dialog-save-button-when-editing = Aktualisieren diff --git a/l10n/dsb/viewer.ftl b/l10n/dsb/viewer.ftl index bb6e68ffc..cdb0a5349 100644 --- a/l10n/dsb/viewer.ftl +++ b/l10n/dsb/viewer.ftl @@ -626,18 +626,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Akcije -pdfjs-editor-edit-comment-actions-button = - .title = Akcije -pdfjs-editor-edit-comment-close-button-label = Zacyniล› -pdfjs-editor-edit-comment-close-button = - .title = Zacyniล› -pdfjs-editor-edit-comment-actions-edit-button-label = Wobลบฤ›ล‚aล› -pdfjs-editor-edit-comment-actions-delete-button-label = Laลกowaล› -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Zapรณdajล›o swรณj komentar -pdfjs-editor-edit-comment-manager-cancel-button = Pล›etergnuล› -pdfjs-editor-edit-comment-manager-save-button = Skล‚adowaล› # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Komentar wobลบฤ›ล‚aล› pdfjs-editor-edit-comment-dialog-save-button-when-editing = Aktualizฤ›rowaล› diff --git a/l10n/el/viewer.ftl b/l10n/el/viewer.ftl index fc6ccf297..3debc70ca 100644 --- a/l10n/el/viewer.ftl +++ b/l10n/el/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ฮ•ฮฝฮญฯฮณฮตฮนฮตฯ‚ -pdfjs-editor-edit-comment-actions-button = - .title = ฮ•ฮฝฮญฯฮณฮตฮนฮตฯ‚ -pdfjs-editor-edit-comment-close-button-label = ฮšฮปฮตฮฏฯƒฮนฮผฮฟ -pdfjs-editor-edit-comment-close-button = - .title = ฮšฮปฮตฮฏฯƒฮนฮผฮฟ -pdfjs-editor-edit-comment-actions-edit-button-label = ฮ•ฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ -pdfjs-editor-edit-comment-actions-delete-button-label = ฮ”ฮนฮฑฮณฯฮฑฯ†ฮฎ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฯ„ฮฟ ฯƒฯ‡ฯŒฮปฮนฯŒ ฯƒฮฑฯ‚ -pdfjs-editor-edit-comment-manager-cancel-button = ฮ‘ฮบฯฯฯ‰ฯƒฮท -pdfjs-editor-edit-comment-manager-save-button = ฮ‘ฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮท # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = ฮ•ฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ ฯƒฯ‡ฮฟฮปฮฏฮฟฯ… pdfjs-editor-edit-comment-dialog-save-button-when-editing = ฮ•ฮฝฮทฮผฮญฯฯ‰ฯƒฮท @@ -644,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = ฮ‘ฮบฯฯฯ‰ฯƒฮท pdfjs-editor-edit-comment-button = .title = ฮ•ฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ ฯƒฯ‡ฮฟฮปฮฏฮฟฯ… +pdfjs-editor-add-comment-button = + .title = ฮ ฯฮฟฯƒฮธฮฎฮบฮท ฯƒฯ‡ฮฟฮปฮฏฮฟฯ… ## Main menu for adding/removing signatures diff --git a/l10n/en-CA/viewer.ftl b/l10n/en-CA/viewer.ftl index 8b2a45769..9542c128a 100644 --- a/l10n/en-CA/viewer.ftl +++ b/l10n/en-CA/viewer.ftl @@ -616,18 +616,6 @@ pdfjs-editor-delete-comment-popup-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Actions -pdfjs-editor-edit-comment-actions-button = - .title = Actions -pdfjs-editor-edit-comment-close-button-label = Close -pdfjs-editor-edit-comment-close-button = - .title = Close -pdfjs-editor-edit-comment-actions-edit-button-label = Edit -pdfjs-editor-edit-comment-actions-delete-button-label = Delete -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Enter your comment -pdfjs-editor-edit-comment-manager-cancel-button = Cancel -pdfjs-editor-edit-comment-manager-save-button = Save # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Edit comment # No existing comment diff --git a/l10n/en-GB/viewer.ftl b/l10n/en-GB/viewer.ftl index 6c24265ca..aedf60d4f 100644 --- a/l10n/en-GB/viewer.ftl +++ b/l10n/en-GB/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Actions -pdfjs-editor-edit-comment-actions-button = - .title = Actions -pdfjs-editor-edit-comment-close-button-label = Close -pdfjs-editor-edit-comment-close-button = - .title = Close -pdfjs-editor-edit-comment-actions-edit-button-label = Edit -pdfjs-editor-edit-comment-actions-delete-button-label = Delete -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Enter your comment -pdfjs-editor-edit-comment-manager-cancel-button = Cancel -pdfjs-editor-edit-comment-manager-save-button = Save # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Edit comment pdfjs-editor-edit-comment-dialog-save-button-when-editing = Update diff --git a/l10n/eo/viewer.ftl b/l10n/eo/viewer.ftl index ceae4b63b..f332a269c 100644 --- a/l10n/eo/viewer.ftl +++ b/l10n/eo/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Agoj -pdfjs-editor-edit-comment-actions-button = - .title = Agoj -pdfjs-editor-edit-comment-close-button-label = Fermi -pdfjs-editor-edit-comment-close-button = - .title = Fermi -pdfjs-editor-edit-comment-actions-edit-button-label = Modifi -pdfjs-editor-edit-comment-actions-delete-button-label = Forigi -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Tajpu vian komenton -pdfjs-editor-edit-comment-manager-cancel-button = Nuligi -pdfjs-editor-edit-comment-manager-save-button = Konservi # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Modifi komenton pdfjs-editor-edit-comment-dialog-save-button-when-editing = ฤœisdatigi @@ -644,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Nuligi pdfjs-editor-edit-comment-button = .title = Modifi komenton +pdfjs-editor-add-comment-button = + .title = Aldoni komenton ## Main menu for adding/removing signatures diff --git a/l10n/es-AR/viewer.ftl b/l10n/es-AR/viewer.ftl index db24641b7..3dccc24bd 100644 --- a/l10n/es-AR/viewer.ftl +++ b/l10n/es-AR/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Acciones -pdfjs-editor-edit-comment-actions-button = - .title = Acciones -pdfjs-editor-edit-comment-close-button-label = Cerrar -pdfjs-editor-edit-comment-close-button = - .title = Cerrar -pdfjs-editor-edit-comment-actions-edit-button-label = Editar -pdfjs-editor-edit-comment-actions-delete-button-label = Borrar -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Ingresar un comentario -pdfjs-editor-edit-comment-manager-cancel-button = Cancelar -pdfjs-editor-edit-comment-manager-save-button = Guardar # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Editar comentario pdfjs-editor-edit-comment-dialog-save-button-when-editing = Actualizar diff --git a/l10n/es-CL/viewer.ftl b/l10n/es-CL/viewer.ftl index b70bbfdb1..fa4e5fc7e 100644 --- a/l10n/es-CL/viewer.ftl +++ b/l10n/es-CL/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Acciones -pdfjs-editor-edit-comment-actions-button = - .title = Acciones -pdfjs-editor-edit-comment-close-button-label = Cerrar -pdfjs-editor-edit-comment-close-button = - .title = Cerrar -pdfjs-editor-edit-comment-actions-edit-button-label = Editar -pdfjs-editor-edit-comment-actions-delete-button-label = Eliminar -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Ingresa tu comentario -pdfjs-editor-edit-comment-manager-cancel-button = Cancelar -pdfjs-editor-edit-comment-manager-save-button = Guardar # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Editar comentario pdfjs-editor-edit-comment-dialog-save-button-when-editing = Actualizar @@ -644,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Cancelar pdfjs-editor-edit-comment-button = .title = Editar comentario +pdfjs-editor-add-comment-button = + .title = Aรฑadir comentario ## Main menu for adding/removing signatures diff --git a/l10n/es-ES/viewer.ftl b/l10n/es-ES/viewer.ftl index b95e43210..ecfc5f59b 100644 --- a/l10n/es-ES/viewer.ftl +++ b/l10n/es-ES/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Acciones -pdfjs-editor-edit-comment-actions-button = - .title = Acciones -pdfjs-editor-edit-comment-close-button-label = Cerrar -pdfjs-editor-edit-comment-close-button = - .title = Cerrar -pdfjs-editor-edit-comment-actions-edit-button-label = Editar -pdfjs-editor-edit-comment-actions-delete-button-label = Eliminar -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Introduzca su comentario -pdfjs-editor-edit-comment-manager-cancel-button = Cancelar -pdfjs-editor-edit-comment-manager-save-button = Guardar # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Editar comentario pdfjs-editor-edit-comment-dialog-save-button-when-editing = Actualizar @@ -644,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Cancelar pdfjs-editor-edit-comment-button = .title = Editar comentario +pdfjs-editor-add-comment-button = + .title = Aรฑadir comentario ## Main menu for adding/removing signatures diff --git a/l10n/es-MX/viewer.ftl b/l10n/es-MX/viewer.ftl index 77e5e77d0..dc98cedcd 100644 --- a/l10n/es-MX/viewer.ftl +++ b/l10n/es-MX/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Acciones -pdfjs-editor-edit-comment-actions-button = - .title = Acciones -pdfjs-editor-edit-comment-close-button-label = Cerrar -pdfjs-editor-edit-comment-close-button = - .title = Cerrar -pdfjs-editor-edit-comment-actions-edit-button-label = Editar -pdfjs-editor-edit-comment-actions-delete-button-label = Eliminar -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Ingresa tu comentario -pdfjs-editor-edit-comment-manager-cancel-button = Cancelar -pdfjs-editor-edit-comment-manager-save-button = Guardar # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Editar comentario pdfjs-editor-edit-comment-dialog-save-button-when-editing = Actualizar diff --git a/l10n/eu/viewer.ftl b/l10n/eu/viewer.ftl index afb3eeab8..cda22d50a 100644 --- a/l10n/eu/viewer.ftl +++ b/l10n/eu/viewer.ftl @@ -622,18 +622,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Ekintzak -pdfjs-editor-edit-comment-actions-button = - .title = Ekintzak -pdfjs-editor-edit-comment-close-button-label = Itxi -pdfjs-editor-edit-comment-close-button = - .title = Itxi -pdfjs-editor-edit-comment-actions-edit-button-label = Editatu -pdfjs-editor-edit-comment-actions-delete-button-label = Ezabatu -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Idatzi zure iruzkina -pdfjs-editor-edit-comment-manager-cancel-button = Utzi -pdfjs-editor-edit-comment-manager-save-button = Gorde # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Editatu iruzkina pdfjs-editor-edit-comment-dialog-save-button-when-editing = Eguneratu diff --git a/l10n/fi/viewer.ftl b/l10n/fi/viewer.ftl index a58408c45..8a4073da7 100644 --- a/l10n/fi/viewer.ftl +++ b/l10n/fi/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Toiminnot -pdfjs-editor-edit-comment-actions-button = - .title = Toiminnot -pdfjs-editor-edit-comment-close-button-label = Sulje -pdfjs-editor-edit-comment-close-button = - .title = Sulje -pdfjs-editor-edit-comment-actions-edit-button-label = Muokkaa -pdfjs-editor-edit-comment-actions-delete-button-label = Poista -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Kirjoita kommenttisi -pdfjs-editor-edit-comment-manager-cancel-button = Peruuta -pdfjs-editor-edit-comment-manager-save-button = Tallenna # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Muokkaa kommenttia pdfjs-editor-edit-comment-dialog-save-button-when-editing = Pรคivitรค diff --git a/l10n/fr/viewer.ftl b/l10n/fr/viewer.ftl index 931ff7a7a..3a21aa7ec 100644 --- a/l10n/fr/viewer.ftl +++ b/l10n/fr/viewer.ftl @@ -614,18 +614,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Actions -pdfjs-editor-edit-comment-actions-button = - .title = Actions -pdfjs-editor-edit-comment-close-button-label = Fermer -pdfjs-editor-edit-comment-close-button = - .title = Fermer -pdfjs-editor-edit-comment-actions-edit-button-label = Modifier -pdfjs-editor-edit-comment-actions-delete-button-label = Supprimer -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Saisissez votre commentaire -pdfjs-editor-edit-comment-manager-cancel-button = Annuler -pdfjs-editor-edit-comment-manager-save-button = Enregistrer # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Modifier le commentaire pdfjs-editor-edit-comment-dialog-save-button-when-editing = Mettre ร  jour diff --git a/l10n/fur/viewer.ftl b/l10n/fur/viewer.ftl index 8f027a299..8dbe742ab 100644 --- a/l10n/fur/viewer.ftl +++ b/l10n/fur/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Azions -pdfjs-editor-edit-comment-actions-button = - .title = Azions -pdfjs-editor-edit-comment-close-button-label = Siere -pdfjs-editor-edit-comment-close-button = - .title = Siere -pdfjs-editor-edit-comment-actions-edit-button-label = Modifiche -pdfjs-editor-edit-comment-actions-delete-button-label = Elimine -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Inserรฌs il to coment -pdfjs-editor-edit-comment-manager-cancel-button = Anule -pdfjs-editor-edit-comment-manager-save-button = Salve # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Modifiche coment pdfjs-editor-edit-comment-dialog-save-button-when-editing = Inzorne diff --git a/l10n/fy-NL/viewer.ftl b/l10n/fy-NL/viewer.ftl index f7cd373d1..6fc77c02e 100644 --- a/l10n/fy-NL/viewer.ftl +++ b/l10n/fy-NL/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Aksjes -pdfjs-editor-edit-comment-actions-button = - .title = Aksjes -pdfjs-editor-edit-comment-close-button-label = Slute -pdfjs-editor-edit-comment-close-button = - .title = Slute -pdfjs-editor-edit-comment-actions-edit-button-label = Bewurkje -pdfjs-editor-edit-comment-actions-delete-button-label = Fuortsmite -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Fier jo opmerking yn -pdfjs-editor-edit-comment-manager-cancel-button = Annulearje -pdfjs-editor-edit-comment-manager-save-button = Bewarje # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Opmerking bewurkje pdfjs-editor-edit-comment-dialog-save-button-when-editing = Bywurkje diff --git a/l10n/gn/viewer.ftl b/l10n/gn/viewer.ftl index e997becde..5f24b555d 100644 --- a/l10n/gn/viewer.ftl +++ b/l10n/gn/viewer.ftl @@ -617,18 +617,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ร‘emonguโ€™e -pdfjs-editor-edit-comment-actions-button = - .title = ร‘emonguโ€™e -pdfjs-editor-edit-comment-close-button-label = Mboty -pdfjs-editor-edit-comment-close-button = - .title = Mboty -pdfjs-editor-edit-comment-actions-edit-button-label = Mbosakoโ€™i -pdfjs-editor-edit-comment-actions-delete-button-label = Mboguete -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Ehai peteฤฉ jeโ€™erei -pdfjs-editor-edit-comment-manager-cancel-button = Heja -pdfjs-editor-edit-comment-manager-save-button = ร‘ongatu # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Jeโ€™erei mbosakoโ€™i pdfjs-editor-edit-comment-dialog-save-button-when-editing = Mbohekopyahu @@ -643,6 +631,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Eheja pdfjs-editor-edit-comment-button = .title = Jeโ€™erei mbosakoโ€™i +pdfjs-editor-add-comment-button = + .title = Jeโ€™erei mbojuaju ## Main menu for adding/removing signatures diff --git a/l10n/he/viewer.ftl b/l10n/he/viewer.ftl index 3718101ea..a5a606091 100644 --- a/l10n/he/viewer.ftl +++ b/l10n/he/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ืคืขื•ืœื•ืช -pdfjs-editor-edit-comment-actions-button = - .title = ืคืขื•ืœื•ืช -pdfjs-editor-edit-comment-close-button-label = ืกื’ื™ืจื” -pdfjs-editor-edit-comment-close-button = - .title = ืกื’ื™ืจื” -pdfjs-editor-edit-comment-actions-edit-button-label = ืขืจื™ื›ื” -pdfjs-editor-edit-comment-actions-delete-button-label = ืžื—ื™ืงื” -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ื ื ืœื”ื›ื ื™ืก ืืช ื”ื”ืขืจื” ืฉืœืš -pdfjs-editor-edit-comment-manager-cancel-button = ื‘ื™ื˜ื•ืœ -pdfjs-editor-edit-comment-manager-save-button = ืฉืžื™ืจื” # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = ืขืจื™ื›ืช ื”ืขืจื” pdfjs-editor-edit-comment-dialog-save-button-when-editing = ืขื“ื›ื•ืŸ diff --git a/l10n/hsb/viewer.ftl b/l10n/hsb/viewer.ftl index 69c8e932b..3c372e0a9 100644 --- a/l10n/hsb/viewer.ftl +++ b/l10n/hsb/viewer.ftl @@ -626,18 +626,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Akcije -pdfjs-editor-edit-comment-actions-button = - .title = Akcije -pdfjs-editor-edit-comment-close-button-label = Zaฤiniฤ‡ -pdfjs-editor-edit-comment-close-button = - .title = Zaฤiniฤ‡ -pdfjs-editor-edit-comment-actions-edit-button-label = Wobdลบฤ›ล‚aฤ‡ -pdfjs-editor-edit-comment-actions-delete-button-label = Zhaลกeฤ‡ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Zapodajฤ‡e swรณj komentar -pdfjs-editor-edit-comment-manager-cancel-button = Pล™etorhnyฤ‡ -pdfjs-editor-edit-comment-manager-save-button = Skล‚adowaฤ‡ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Komentar wobdลบฤ›ล‚aฤ‡ pdfjs-editor-edit-comment-dialog-save-button-when-editing = Aktualizowaฤ‡ diff --git a/l10n/hu/viewer.ftl b/l10n/hu/viewer.ftl index 9a54bc8af..b8339b3f7 100644 --- a/l10n/hu/viewer.ftl +++ b/l10n/hu/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Mลฑveletek -pdfjs-editor-edit-comment-actions-button = - .title = Mลฑveletek -pdfjs-editor-edit-comment-close-button-label = Bezรกrรกs -pdfjs-editor-edit-comment-close-button = - .title = Bezรกrรกs -pdfjs-editor-edit-comment-actions-edit-button-label = Szerkesztรฉs -pdfjs-editor-edit-comment-actions-delete-button-label = Tรถrlรฉs -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = รrja be a megjegyzรฉsรฉt -pdfjs-editor-edit-comment-manager-cancel-button = Mรฉgse -pdfjs-editor-edit-comment-manager-save-button = Mentรฉs # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Megjegyzรฉs szerkesztรฉse pdfjs-editor-edit-comment-dialog-save-button-when-editing = Frissรญtรฉs diff --git a/l10n/hy-AM/viewer.ftl b/l10n/hy-AM/viewer.ftl index 6495942b0..b2eb7fa8e 100644 --- a/l10n/hy-AM/viewer.ftl +++ b/l10n/hy-AM/viewer.ftl @@ -586,21 +586,6 @@ pdfjs-editor-add-signature-cancel-button = ี‰ีฅีฒีกึ€ีฏีฅีฌ pdfjs-editor-add-signature-add-button = ิฑีพีฅีฌีกึีถีฅีฌ pdfjs-editor-edit-signature-update-button = ินีกึ€ีดีกึีถีฅีฌ -## Edit a comment dialog - -pdfjs-editor-edit-comment-actions-button-label = ิณีธึ€ีฎีธีฒีธึ‚ีฉีตีธึ‚ีถีถีฅึ€ -pdfjs-editor-edit-comment-actions-button = - .title = ิณีธึ€ีฎีธีฒีธึ‚ีฉีตีธึ‚ีถีถีฅึ€ -pdfjs-editor-edit-comment-close-button-label = ี“ีกีฏีฅีฌ -pdfjs-editor-edit-comment-close-button = - .title = ี“ีกีฏีฅีฌ -pdfjs-editor-edit-comment-actions-edit-button-label = ิฝีดีขีกีฃึ€ีฅีฌ -pdfjs-editor-edit-comment-actions-delete-button-label = ี‹ีถีปีฅีฌ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ี„ีธึ‚ีฟึ„ีกีฃึ€ีฅึ„ ีฑีฅึ€ ีดีฅีฏีถีกีขีกีถีธึ‚ีฉีตีธึ‚ีถีจ -pdfjs-editor-edit-comment-manager-cancel-button = ี‰ีฅีฒีกึ€ีฏีฅีฌ -pdfjs-editor-edit-comment-manager-save-button = ีŠีกีฐีบีกีถีฅีฌ - ## Edit a comment button in the editor toolbar pdfjs-editor-edit-comment-button = diff --git a/l10n/ia/viewer.ftl b/l10n/ia/viewer.ftl index e50254184..ff347ef09 100644 --- a/l10n/ia/viewer.ftl +++ b/l10n/ia/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Actiones -pdfjs-editor-edit-comment-actions-button = - .title = Actiones -pdfjs-editor-edit-comment-close-button-label = Clauder -pdfjs-editor-edit-comment-close-button = - .title = Clauder -pdfjs-editor-edit-comment-actions-edit-button-label = Rediger -pdfjs-editor-edit-comment-actions-delete-button-label = Deler -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Insere tu commento -pdfjs-editor-edit-comment-manager-cancel-button = Cancellar -pdfjs-editor-edit-comment-manager-save-button = Salvar # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Rediger commento pdfjs-editor-edit-comment-dialog-save-button-when-editing = Actualisar diff --git a/l10n/id/viewer.ftl b/l10n/id/viewer.ftl index ba34b82ab..60cea4cb8 100644 --- a/l10n/id/viewer.ftl +++ b/l10n/id/viewer.ftl @@ -574,21 +574,6 @@ pdfjs-editor-add-signature-cancel-button = Batal pdfjs-editor-add-signature-add-button = Tambah pdfjs-editor-edit-signature-update-button = Perbarui -## Edit a comment dialog - -pdfjs-editor-edit-comment-actions-button-label = Aksi -pdfjs-editor-edit-comment-actions-button = - .title = Aksi -pdfjs-editor-edit-comment-close-button-label = Tutup -pdfjs-editor-edit-comment-close-button = - .title = Tutup -pdfjs-editor-edit-comment-actions-edit-button-label = Sunting -pdfjs-editor-edit-comment-actions-delete-button-label = Hapus -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Masukkan komentar Anda -pdfjs-editor-edit-comment-manager-cancel-button = Batal -pdfjs-editor-edit-comment-manager-save-button = Simpan - ## Edit a comment button in the editor toolbar pdfjs-editor-edit-comment-button = diff --git a/l10n/is/viewer.ftl b/l10n/is/viewer.ftl index 807d0ef2a..8082890cd 100644 --- a/l10n/is/viewer.ftl +++ b/l10n/is/viewer.ftl @@ -557,21 +557,6 @@ pdfjs-editor-add-signature-cancel-button = Hรฆtta viรฐ pdfjs-editor-add-signature-add-button = Bรฆta viรฐ pdfjs-editor-edit-signature-update-button = Uppfรฆra -## Edit a comment dialog - -pdfjs-editor-edit-comment-actions-button-label = Aรฐgerรฐir -pdfjs-editor-edit-comment-actions-button = - .title = Aรฐgerรฐir -pdfjs-editor-edit-comment-close-button-label = Loka -pdfjs-editor-edit-comment-close-button = - .title = Loka -pdfjs-editor-edit-comment-actions-edit-button-label = Breyta -pdfjs-editor-edit-comment-actions-delete-button-label = Eyรฐa -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Settu inn athugasemdina รพรญna -pdfjs-editor-edit-comment-manager-cancel-button = Hรฆtta viรฐ -pdfjs-editor-edit-comment-manager-save-button = Vista - ## Edit a comment button in the editor toolbar pdfjs-editor-edit-comment-button = diff --git a/l10n/it/viewer.ftl b/l10n/it/viewer.ftl index cc96d1a62..cc72bcb7e 100644 --- a/l10n/it/viewer.ftl +++ b/l10n/it/viewer.ftl @@ -290,10 +290,10 @@ pdfjs-editor-color-picker-free-text-input = .title = Cambia colore del testo pdfjs-editor-free-text-button-label = Testo pdfjs-editor-ink-button = - .title = Disegno + .title = Disegna pdfjs-editor-color-picker-ink-input = .title = Cambia colore del disegno -pdfjs-editor-ink-button-label = Disegno +pdfjs-editor-ink-button-label = Disegna pdfjs-editor-stamp-button = .title = Aggiungi o rimuovi immagine pdfjs-editor-stamp-button-label = Aggiungi o rimuovi immagine @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Azioni -pdfjs-editor-edit-comment-actions-button = - .title = Azioni -pdfjs-editor-edit-comment-close-button-label = Chiudi -pdfjs-editor-edit-comment-close-button = - .title = Chiudi -pdfjs-editor-edit-comment-actions-edit-button-label = Modifica -pdfjs-editor-edit-comment-actions-delete-button-label = Elimina -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Inserisci il tuo commento -pdfjs-editor-edit-comment-manager-cancel-button = Annulla -pdfjs-editor-edit-comment-manager-save-button = Salva # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Modifica commento pdfjs-editor-edit-comment-dialog-save-button-when-editing = Aggiorna diff --git a/l10n/ja/viewer.ftl b/l10n/ja/viewer.ftl index 6f9864d1a..c7c124c92 100644 --- a/l10n/ja/viewer.ftl +++ b/l10n/ja/viewer.ftl @@ -597,6 +597,8 @@ pdfjs-editor-edit-comment-popup-button = pdfjs-editor-delete-comment-popup-button-label = ใ‚ณใƒกใƒณใƒˆใ‚’ๅ‰Š้™ค pdfjs-editor-delete-comment-popup-button = .title = ใ‚ณใƒกใƒณใƒˆใ‚’ๅ‰Š้™คใ—ใพใ™ +pdfjs-show-comment-button = + .title = ใ‚ณใƒกใƒณใƒˆใ‚’่กจ็คบใ—ใพใ™ ## Edit a comment dialog @@ -614,17 +616,20 @@ pdfjs-editor-edit-comment-manager-cancel-button = ใ‚ญใƒฃใƒณใ‚ปใƒซ pdfjs-editor-edit-comment-manager-save-button = ไฟๅญ˜ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = ใ‚ณใƒกใƒณใƒˆใ‚’็ทจ้›† +pdfjs-editor-edit-comment-dialog-save-button-when-editing = ๆ›ดๆ–ฐ # No existing comment pdfjs-editor-edit-comment-dialog-title-when-adding = ใ‚ณใƒกใƒณใƒˆใ‚’่ฟฝๅŠ  +pdfjs-editor-edit-comment-dialog-save-button-when-adding = ่ฟฝๅŠ  pdfjs-editor-edit-comment-dialog-text-input = .placeholder = ใ‚ณใƒกใƒณใƒˆใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„... pdfjs-editor-edit-comment-dialog-cancel-button = ใ‚ญใƒฃใƒณใ‚ปใƒซ -pdfjs-editor-edit-comment-dialog-save-button = ไฟๅญ˜ ## Edit a comment button in the editor toolbar pdfjs-editor-edit-comment-button = - .title = Edit comment + .title = ใ‚ณใƒกใƒณใƒˆใ‚’็ทจ้›†ใ—ใพใ™ +pdfjs-editor-add-comment-button = + .title = ใ‚ณใƒกใƒณใƒˆใ‚’่ฟฝๅŠ ใ—ใพใ™ ## Main menu for adding/removing signatures diff --git a/l10n/ka/viewer.ftl b/l10n/ka/viewer.ftl index 1bc60d552..525c9b85d 100644 --- a/l10n/ka/viewer.ftl +++ b/l10n/ka/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = แƒ›แƒแƒฅแƒ›แƒ”แƒ“แƒ”แƒ‘แƒ”แƒ‘แƒ˜ -pdfjs-editor-edit-comment-actions-button = - .title = แƒ›แƒแƒฅแƒ›แƒ”แƒ“แƒ”แƒ‘แƒ”แƒ‘แƒ˜ -pdfjs-editor-edit-comment-close-button-label = แƒ“แƒแƒฎแƒฃแƒ แƒ•แƒ -pdfjs-editor-edit-comment-close-button = - .title = แƒ“แƒแƒฎแƒฃแƒ แƒ•แƒ -pdfjs-editor-edit-comment-actions-edit-button-label = แƒฉแƒแƒกแƒฌแƒแƒ แƒ”แƒ‘แƒ -pdfjs-editor-edit-comment-actions-delete-button-label = แƒฌแƒแƒจแƒšแƒ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = แƒจแƒ”แƒ˜แƒงแƒ•แƒแƒœแƒ”แƒ— แƒ“แƒแƒกแƒแƒ แƒ—แƒแƒ•แƒ˜ แƒจแƒ”แƒœแƒ˜แƒจแƒ•แƒœแƒ -pdfjs-editor-edit-comment-manager-cancel-button = แƒ’แƒแƒฃแƒฅแƒ›แƒ”แƒ‘แƒ -pdfjs-editor-edit-comment-manager-save-button = แƒจแƒ”แƒœแƒแƒฎแƒ•แƒ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = แƒจแƒ”แƒœแƒ˜แƒจแƒ•แƒœแƒ˜แƒก แƒฉแƒแƒกแƒฌแƒแƒ แƒ”แƒ‘แƒ pdfjs-editor-edit-comment-dialog-save-button-when-editing = แƒ’แƒแƒœแƒแƒฎแƒšแƒ”แƒ‘แƒ diff --git a/l10n/kab/viewer.ftl b/l10n/kab/viewer.ftl index b1b1dcd20..33a5abcf5 100644 --- a/l10n/kab/viewer.ftl +++ b/l10n/kab/viewer.ftl @@ -601,18 +601,6 @@ pdfjs-editor-delete-comment-popup-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Tigawin -pdfjs-editor-edit-comment-actions-button = - .title = Tigawin -pdfjs-editor-edit-comment-close-button-label = Mdel -pdfjs-editor-edit-comment-close-button = - .title = Mdel -pdfjs-editor-edit-comment-actions-edit-button-label = แบ’reg -pdfjs-editor-edit-comment-actions-delete-button-label = Kkes -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Aru awennit-ikโ‹…im -pdfjs-editor-edit-comment-manager-cancel-button = Sefsex -pdfjs-editor-edit-comment-manager-save-button = Sekles # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = แบ’reg awennit # No existing comment diff --git a/l10n/kk/viewer.ftl b/l10n/kk/viewer.ftl index dfe709c6b..edf781dbd 100644 --- a/l10n/kk/viewer.ftl +++ b/l10n/kk/viewer.ftl @@ -596,25 +596,16 @@ pdfjs-editor-edit-signature-update-button = ะ–ะฐาฃะฐั€ั‚ัƒ ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ำ˜ั€ะตะบะตั‚ั‚ะตั€ -pdfjs-editor-edit-comment-actions-button = - .title = ำ˜ั€ะตะบะตั‚ั‚ะตั€ -pdfjs-editor-edit-comment-close-button-label = ะ–ะฐะฑัƒ -pdfjs-editor-edit-comment-close-button = - .title = ะ–ะฐะฑัƒ -pdfjs-editor-edit-comment-actions-edit-button-label = ะขาฏะทะตั‚ัƒ -pdfjs-editor-edit-comment-actions-delete-button-label = ำจัˆั–ั€ัƒ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ะŸั–ะบั–ั€ั–าฃั–ะทะดั– ะตะฝะณั–ะทั–าฃั–ะท -pdfjs-editor-edit-comment-manager-cancel-button = ะ‘ะฐั ั‚ะฐั€ั‚ัƒ -pdfjs-editor-edit-comment-manager-save-button = ะกะฐา›ั‚ะฐัƒ pdfjs-editor-edit-comment-dialog-save-button-when-editing = ะ–ะฐาฃะฐั€ั‚ัƒ +pdfjs-editor-edit-comment-dialog-save-button-when-adding = าšะพััƒ pdfjs-editor-edit-comment-dialog-cancel-button = ะ‘ะฐั ั‚ะฐั€ั‚ัƒ ## Edit a comment button in the editor toolbar pdfjs-editor-edit-comment-button = .title = ะŸั–ะบั–ั€ะดั– ั‚าฏะทะตั‚ัƒ +pdfjs-editor-add-comment-button = + .title = ะŸั–ะบั–ั€ า›ะพััƒ ## Main menu for adding/removing signatures diff --git a/l10n/ko/viewer.ftl b/l10n/ko/viewer.ftl index 2ffab2736..4e8116857 100644 --- a/l10n/ko/viewer.ftl +++ b/l10n/ko/viewer.ftl @@ -602,18 +602,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ๋™์ž‘ -pdfjs-editor-edit-comment-actions-button = - .title = ๋™์ž‘ -pdfjs-editor-edit-comment-close-button-label = ๋‹ซ๊ธฐ -pdfjs-editor-edit-comment-close-button = - .title = ๋‹ซ๊ธฐ -pdfjs-editor-edit-comment-actions-edit-button-label = ํŽธ์ง‘ -pdfjs-editor-edit-comment-actions-delete-button-label = ์‚ญ์ œ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ์ฃผ์„์„ ์ž…๋ ฅํ•˜์„ธ์š” -pdfjs-editor-edit-comment-manager-cancel-button = ์ทจ์†Œ -pdfjs-editor-edit-comment-manager-save-button = ์ €์žฅ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = ์ฃผ์„ ํŽธ์ง‘ pdfjs-editor-edit-comment-dialog-save-button-when-editing = ์—…๋ฐ์ดํŠธ diff --git a/l10n/nb-NO/viewer.ftl b/l10n/nb-NO/viewer.ftl index c27a0d4b9..a25a6f77f 100644 --- a/l10n/nb-NO/viewer.ftl +++ b/l10n/nb-NO/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Handlinger -pdfjs-editor-edit-comment-actions-button = - .title = Handlinger -pdfjs-editor-edit-comment-close-button-label = Lukk -pdfjs-editor-edit-comment-close-button = - .title = Lukk -pdfjs-editor-edit-comment-actions-edit-button-label = Rediger -pdfjs-editor-edit-comment-actions-delete-button-label = Slett -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Skriv inn kommentaren din -pdfjs-editor-edit-comment-manager-cancel-button = Avbryt -pdfjs-editor-edit-comment-manager-save-button = Lagre # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Rediger kommentar pdfjs-editor-edit-comment-dialog-save-button-when-editing = Oppdater @@ -644,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Avbryt pdfjs-editor-edit-comment-button = .title = Rediger kommentar +pdfjs-editor-add-comment-button = + .title = Legg til kommentar ## Main menu for adding/removing signatures diff --git a/l10n/nl/viewer.ftl b/l10n/nl/viewer.ftl index 250d8a1da..81c2485cb 100644 --- a/l10n/nl/viewer.ftl +++ b/l10n/nl/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Acties -pdfjs-editor-edit-comment-actions-button = - .title = Acties -pdfjs-editor-edit-comment-close-button-label = Sluiten -pdfjs-editor-edit-comment-close-button = - .title = Sluiten -pdfjs-editor-edit-comment-actions-edit-button-label = Bewerken -pdfjs-editor-edit-comment-actions-delete-button-label = Verwijderen -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Voer uw opmerking in -pdfjs-editor-edit-comment-manager-cancel-button = Annuleren -pdfjs-editor-edit-comment-manager-save-button = Opslaan # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Opmerking bewerken pdfjs-editor-edit-comment-dialog-save-button-when-editing = Bijwerken diff --git a/l10n/nn-NO/viewer.ftl b/l10n/nn-NO/viewer.ftl index ff0eb2c34..d6cbbdccc 100644 --- a/l10n/nn-NO/viewer.ftl +++ b/l10n/nn-NO/viewer.ftl @@ -616,18 +616,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Handlingar -pdfjs-editor-edit-comment-actions-button = - .title = Handlingar -pdfjs-editor-edit-comment-close-button-label = Lat att -pdfjs-editor-edit-comment-close-button = - .title = Lat att -pdfjs-editor-edit-comment-actions-edit-button-label = Rediger -pdfjs-editor-edit-comment-actions-delete-button-label = Slett -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Skriv inn kommentaren din -pdfjs-editor-edit-comment-manager-cancel-button = Avbryt -pdfjs-editor-edit-comment-manager-save-button = Lagre # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Rediger kommentar pdfjs-editor-edit-comment-dialog-save-button-when-editing = Oppdater diff --git a/l10n/pa-IN/viewer.ftl b/l10n/pa-IN/viewer.ftl index b70f545db..e83bdc367 100644 --- a/l10n/pa-IN/viewer.ftl +++ b/l10n/pa-IN/viewer.ftl @@ -386,6 +386,8 @@ pdfjs-editor-comments-sidebar-close-button = .title = เจฌเจพเจนเฉ€ เจจเฉ‚เฉฐ เจฌเฉฐเจฆ เจ•เจฐเฉ‹ .aria-label = เจฌเจพเจนเฉ€ เจจเฉ‚เฉฐ เจฌเฉฐเจฆ เจ•เจฐเฉ‹ pdfjs-editor-comments-sidebar-close-button-label = เจฌเจพเจนเฉ€ เจจเฉ‚เฉฐ เจฌเฉฐเจฆ เจ•เจฐเฉ‹ +# Instructional copy to add a comment by selecting text or an annotations. +pdfjs-editor-comments-sidebar-no-comments1 = เจ•เฉ€ เจ•เฉเจ เจงเจฟเจ†เจจ เจฆเฉ‡เจฃ เจฏเฉ‹เจ— เจตเฉ‡เจ–เจฟเจ† เจนเฉˆ? เจ‡เจธ เจจเฉ‚เฉฐ เจ‰เจ˜เจพเฉœเฉ‹ เจ…เจคเฉ‡ เจŸเจฟเฉฑเจชเจฃเฉ€ เจฆเจฟเจ“เฅค pdfjs-editor-comments-sidebar-no-comments-link = เจนเฉ‹เจฐ เจœเจพเจฃเฉ‹ ## Alt-text dialog @@ -616,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = เจ•เจพเจฐเจตเจพเจˆเจ†เจ‚ -pdfjs-editor-edit-comment-actions-button = - .title = เจ•เจพเจฐเจตเจพเจˆเจ†เจ‚ -pdfjs-editor-edit-comment-close-button-label = เจฌเฉฐเจฆ เจ•เจฐเฉ‹ -pdfjs-editor-edit-comment-close-button = - .title = เจฌเฉฐเจฆ เจ•เจฐเฉ‹ -pdfjs-editor-edit-comment-actions-edit-button-label = เจธเฉ‹เจงเฉ‹ -pdfjs-editor-edit-comment-actions-delete-button-label = เจนเจŸเจพเจ“ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = เจ†เจชเจฃเฉ€ เจŸเจฟเฉฑเจชเจฃเฉ€ เจฆเจฟเจ“ -pdfjs-editor-edit-comment-manager-cancel-button = เจฐเฉฑเจฆ เจ•เจฐเฉ‹ -pdfjs-editor-edit-comment-manager-save-button = เจธเฉฐเจญเจพเจฒเฉ‹ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = เจŸเจฟเฉฑเจชเจฃเฉ€ เจจเฉ‚เฉฐ เจธเฉ‹เจงเฉ‹ pdfjs-editor-edit-comment-dialog-save-button-when-editing = เจ…เฉฑเจชเจกเฉ‡เจŸ เจ•เจฐเฉ‹ @@ -642,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = เจฐเฉฑเจฆ เจ•เจฐเฉ‹ pdfjs-editor-edit-comment-button = .title = เจŸเจฟเฉฑเจชเจฃเฉ€ เจจเฉ‚เฉฐ เจธเฉ‹เจงเฉ‹ +pdfjs-editor-add-comment-button = + .title = เจŸเจฟเฉฑเจชเจฃเฉ€ เจœเฉ‹เฉœเฉ‹ ## Main menu for adding/removing signatures diff --git a/l10n/pl/viewer.ftl b/l10n/pl/viewer.ftl index 780997c08..2d111ccb5 100644 --- a/l10n/pl/viewer.ftl +++ b/l10n/pl/viewer.ftl @@ -621,18 +621,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Dziaล‚ania -pdfjs-editor-edit-comment-actions-button = - .title = Dziaล‚ania -pdfjs-editor-edit-comment-close-button-label = Zamknij -pdfjs-editor-edit-comment-close-button = - .title = Zamknij -pdfjs-editor-edit-comment-actions-edit-button-label = Edytuj -pdfjs-editor-edit-comment-actions-delete-button-label = Usuล„ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Napisz komentarz -pdfjs-editor-edit-comment-manager-cancel-button = Anuluj -pdfjs-editor-edit-comment-manager-save-button = Zapisz # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Edytuj komentarz pdfjs-editor-edit-comment-dialog-save-button-when-editing = Aktualizuj @@ -647,6 +635,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Anuluj pdfjs-editor-edit-comment-button = .title = Edytuj komentarz +pdfjs-editor-add-comment-button = + .title = Dodaj komentarz ## Main menu for adding/removing signatures diff --git a/l10n/pt-BR/viewer.ftl b/l10n/pt-BR/viewer.ftl index f100094ad..d340e1010 100644 --- a/l10n/pt-BR/viewer.ftl +++ b/l10n/pt-BR/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Aรงรตes -pdfjs-editor-edit-comment-actions-button = - .title = Aรงรตes -pdfjs-editor-edit-comment-close-button-label = Fechar -pdfjs-editor-edit-comment-close-button = - .title = Fechar -pdfjs-editor-edit-comment-actions-edit-button-label = Editar -pdfjs-editor-edit-comment-actions-delete-button-label = Excluir -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Digite seu comentรกrio -pdfjs-editor-edit-comment-manager-cancel-button = Cancelar -pdfjs-editor-edit-comment-manager-save-button = Salvar # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Editar comentรกrio pdfjs-editor-edit-comment-dialog-save-button-when-editing = Atualizar @@ -644,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Cancelar pdfjs-editor-edit-comment-button = .title = Editar comentรกrio +pdfjs-editor-add-comment-button = + .title = Adicionar comentรกrio ## Main menu for adding/removing signatures diff --git a/l10n/rm/viewer.ftl b/l10n/rm/viewer.ftl index d330ef859..c5262cc01 100644 --- a/l10n/rm/viewer.ftl +++ b/l10n/rm/viewer.ftl @@ -616,18 +616,6 @@ pdfjs-editor-delete-comment-popup-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Acziuns -pdfjs-editor-edit-comment-actions-button = - .title = Acziuns -pdfjs-editor-edit-comment-close-button-label = Serrar -pdfjs-editor-edit-comment-close-button = - .title = Serrar -pdfjs-editor-edit-comment-actions-edit-button-label = Modifitgar -pdfjs-editor-edit-comment-actions-delete-button-label = Stizzar -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Endatar in commentari -pdfjs-editor-edit-comment-manager-cancel-button = Interrumper -pdfjs-editor-edit-comment-manager-save-button = Memorisar # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Modifitgar il commentari # No existing comment diff --git a/l10n/ro/viewer.ftl b/l10n/ro/viewer.ftl index 4debd54a2..a6655281c 100644 --- a/l10n/ro/viewer.ftl +++ b/l10n/ro/viewer.ftl @@ -37,8 +37,8 @@ pdfjs-open-file-button = .title = Deschide un fiศ™ier pdfjs-open-file-button-label = Deschide pdfjs-print-button = - .title = Listeazฤƒ -pdfjs-print-button-label = Listeazฤƒ + .title = Printeazฤƒ +pdfjs-print-button-label = Printeazฤƒ pdfjs-save-button = .title = Salveazฤƒ pdfjs-save-button-label = Salveazฤƒ @@ -621,18 +621,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Acศ›iuni -pdfjs-editor-edit-comment-actions-button = - .title = Acศ›iuni -pdfjs-editor-edit-comment-close-button-label = รŽnchide -pdfjs-editor-edit-comment-close-button = - .title = รŽnchide -pdfjs-editor-edit-comment-actions-edit-button-label = Editeazฤƒ -pdfjs-editor-edit-comment-actions-delete-button-label = ศ˜terge -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Introdu comentariul -pdfjs-editor-edit-comment-manager-cancel-button = Anuleazฤƒ -pdfjs-editor-edit-comment-manager-save-button = Salveazฤƒ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Editeazฤƒ comentariul pdfjs-editor-edit-comment-dialog-save-button-when-editing = Actualizeazฤƒ diff --git a/l10n/ru/viewer.ftl b/l10n/ru/viewer.ftl index d59e7f983..9242e154c 100644 --- a/l10n/ru/viewer.ftl +++ b/l10n/ru/viewer.ftl @@ -622,18 +622,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ะ”ะตะนัั‚ะฒะธั -pdfjs-editor-edit-comment-actions-button = - .title = ะ”ะตะนัั‚ะฒะธั -pdfjs-editor-edit-comment-close-button-label = ะ—ะฐะบั€ั‹ั‚ัŒ -pdfjs-editor-edit-comment-close-button = - .title = ะ—ะฐะบั€ั‹ั‚ัŒ -pdfjs-editor-edit-comment-actions-edit-button-label = ะ˜ะทะผะตะฝะธั‚ัŒ -pdfjs-editor-edit-comment-actions-delete-button-label = ะฃะดะฐะปะธั‚ัŒ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ะ’ะฒะตะดะธั‚ะต ะฒะฐัˆ ะบะพะผะผะตะฝั‚ะฐั€ะธะน -pdfjs-editor-edit-comment-manager-cancel-button = ะžั‚ะผะตะฝะฐ -pdfjs-editor-edit-comment-manager-save-button = ะกะพั…ั€ะฐะฝะธั‚ัŒ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะบะพะผะผะตะฝั‚ะฐั€ะธะน pdfjs-editor-edit-comment-dialog-save-button-when-editing = ะžะฑะฝะพะฒะธั‚ัŒ diff --git a/l10n/sc/viewer.ftl b/l10n/sc/viewer.ftl index 691588ba3..c8a732735 100644 --- a/l10n/sc/viewer.ftl +++ b/l10n/sc/viewer.ftl @@ -338,6 +338,5 @@ pdfjs-editor-add-signature-cancel-button = Annulla ## Edit a comment dialog -pdfjs-editor-edit-comment-manager-cancel-button = Annulla pdfjs-editor-edit-comment-dialog-text-input = .placeholder = Cumintza a iscrรฌereโ€ฆ diff --git a/l10n/sk/viewer.ftl b/l10n/sk/viewer.ftl index a066721a8..d9b603962 100644 --- a/l10n/sk/viewer.ftl +++ b/l10n/sk/viewer.ftl @@ -626,18 +626,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Akcie -pdfjs-editor-edit-comment-actions-button = - .title = Akcie -pdfjs-editor-edit-comment-close-button-label = Zavrieลฅ -pdfjs-editor-edit-comment-close-button = - .title = Zavrieลฅ -pdfjs-editor-edit-comment-actions-edit-button-label = Upraviลฅ -pdfjs-editor-edit-comment-actions-delete-button-label = Odstrรกniลฅ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Zadajte svoj komentรกr -pdfjs-editor-edit-comment-manager-cancel-button = Zruลกiลฅ -pdfjs-editor-edit-comment-manager-save-button = Uloลพiลฅ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Upraviลฅ komentรกr pdfjs-editor-edit-comment-dialog-save-button-when-editing = Aktualizovaลฅ diff --git a/l10n/sl/viewer.ftl b/l10n/sl/viewer.ftl index 83dc835b8..6ead758da 100644 --- a/l10n/sl/viewer.ftl +++ b/l10n/sl/viewer.ftl @@ -626,18 +626,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Dejanja -pdfjs-editor-edit-comment-actions-button = - .title = Dejanja -pdfjs-editor-edit-comment-close-button-label = Zapri -pdfjs-editor-edit-comment-close-button = - .title = Zapri -pdfjs-editor-edit-comment-actions-edit-button-label = Uredi -pdfjs-editor-edit-comment-actions-delete-button-label = Izbriลกi -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Vnesite komentar -pdfjs-editor-edit-comment-manager-cancel-button = Prekliฤi -pdfjs-editor-edit-comment-manager-save-button = Shrani # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Uredi komentar pdfjs-editor-edit-comment-dialog-save-button-when-editing = Spremeni @@ -652,6 +640,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Prekliฤi pdfjs-editor-edit-comment-button = .title = Uredi komentar +pdfjs-editor-add-comment-button = + .title = Dodaj komentar ## Main menu for adding/removing signatures diff --git a/l10n/sq/viewer.ftl b/l10n/sq/viewer.ftl index 895055321..9501e7fb4 100644 --- a/l10n/sq/viewer.ftl +++ b/l10n/sq/viewer.ftl @@ -609,18 +609,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Veprime -pdfjs-editor-edit-comment-actions-button = - .title = Veprime -pdfjs-editor-edit-comment-close-button-label = Mbylle -pdfjs-editor-edit-comment-close-button = - .title = Mbylle -pdfjs-editor-edit-comment-actions-edit-button-label = Pรซrpunoni -pdfjs-editor-edit-comment-actions-delete-button-label = Fshije -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Jepni komentin tuaj -pdfjs-editor-edit-comment-manager-cancel-button = Anuloje -pdfjs-editor-edit-comment-manager-save-button = Ruaje # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Pรซrpunoni koment pdfjs-editor-edit-comment-dialog-save-button-when-editing = Pรซrditรซsojeni @@ -635,6 +623,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Anuloje pdfjs-editor-edit-comment-button = .title = Pรซrpunoni koment +pdfjs-editor-add-comment-button = + .title = Shtoni koment ## Main menu for adding/removing signatures diff --git a/l10n/sv-SE/viewer.ftl b/l10n/sv-SE/viewer.ftl index 75fd6a703..187f9a5f2 100644 --- a/l10n/sv-SE/viewer.ftl +++ b/l10n/sv-SE/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ร…tgรคrder -pdfjs-editor-edit-comment-actions-button = - .title = ร…tgรคrder -pdfjs-editor-edit-comment-close-button-label = Stรคng -pdfjs-editor-edit-comment-close-button = - .title = Stรคng -pdfjs-editor-edit-comment-actions-edit-button-label = Redigera -pdfjs-editor-edit-comment-actions-delete-button-label = Ta bort -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Ange din kommentar -pdfjs-editor-edit-comment-manager-cancel-button = Avbryt -pdfjs-editor-edit-comment-manager-save-button = Spara # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Redigera kommentar pdfjs-editor-edit-comment-dialog-save-button-when-editing = Uppdatera diff --git a/l10n/tg/viewer.ftl b/l10n/tg/viewer.ftl index 88932ddce..af1935b6a 100644 --- a/l10n/tg/viewer.ftl +++ b/l10n/tg/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ะะผะฐะปาณะพ -pdfjs-editor-edit-comment-actions-button = - .title = ะะผะฐะปาณะพ -pdfjs-editor-edit-comment-close-button-label = ะŸำฏัˆะธะดะฐะฝ -pdfjs-editor-edit-comment-close-button = - .title = ะŸำฏัˆะธะดะฐะฝ -pdfjs-editor-edit-comment-actions-edit-button-label = ะขะฐาณั€ะธั€ ะบะฐั€ะดะฐะฝ -pdfjs-editor-edit-comment-actions-delete-button-label = ะะตัั‚ ะบะฐั€ะดะฐะฝ -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ะจะฐั€าณะธ ั…ัƒะดั€ะพ ะฒะพั€ะธะด ะบัƒะฝะตะด -pdfjs-editor-edit-comment-manager-cancel-button = ะ‘ะตะบะพั€ ะบะฐั€ะดะฐะฝ -pdfjs-editor-edit-comment-manager-save-button = ะะธะณะพาณ ะดะพัˆั‚ะฐะฝ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = ะขะฐาณั€ะธั€ ะบะฐั€ะดะฐะฝะธ ัˆะฐั€าณ pdfjs-editor-edit-comment-dialog-save-button-when-editing = ะะฐะฒัะพะทำฃ ะบะฐั€ะดะฐะฝ @@ -644,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = ะ‘ะตะบะพั€ ะบะฐั€ะดะฐะฝ pdfjs-editor-edit-comment-button = .title = ะขะฐาณั€ะธั€ ะบะฐั€ะดะฐะฝะธ ัˆะฐั€าณ +pdfjs-editor-add-comment-button = + .title = ะ˜ะปะพะฒะฐ ะบะฐั€ะดะฐะฝะธ ัˆะฐั€าณ ## Main menu for adding/removing signatures diff --git a/l10n/th/viewer.ftl b/l10n/th/viewer.ftl index 75a59ef3d..eaf8fb7df 100644 --- a/l10n/th/viewer.ftl +++ b/l10n/th/viewer.ftl @@ -602,18 +602,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = เธเธฒเธฃเธเธฃเธฐเธ—เธณ -pdfjs-editor-edit-comment-actions-button = - .title = เธเธฒเธฃเธเธฃเธฐเธ—เธณ -pdfjs-editor-edit-comment-close-button-label = เธ›เธดเธ” -pdfjs-editor-edit-comment-close-button = - .title = เธ›เธดเธ” -pdfjs-editor-edit-comment-actions-edit-button-label = เนเธเน‰เน„เธ‚ -pdfjs-editor-edit-comment-actions-delete-button-label = เธฅเธš -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = เธ›เน‰เธญเธ™เธ„เธงเธฒเธกเธ„เธดเธ”เน€เธซเน‡เธ™เธ‚เธญเธ‡เธ„เธธเธ“ -pdfjs-editor-edit-comment-manager-cancel-button = เธขเธเน€เธฅเธดเธ -pdfjs-editor-edit-comment-manager-save-button = เธšเธฑเธ™เธ—เธถเธ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = เนเธเน‰เน„เธ‚เธ„เธงเธฒเธกเธ„เธดเธ”เน€เธซเน‡เธ™ pdfjs-editor-edit-comment-dialog-save-button-when-editing = เธญเธฑเธ›เน€เธ”เธ• diff --git a/l10n/tr/viewer.ftl b/l10n/tr/viewer.ftl index 81faa593d..41f558932 100644 --- a/l10n/tr/viewer.ftl +++ b/l10n/tr/viewer.ftl @@ -618,18 +618,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Eylemler -pdfjs-editor-edit-comment-actions-button = - .title = Eylemler -pdfjs-editor-edit-comment-close-button-label = Kapat -pdfjs-editor-edit-comment-close-button = - .title = Kapat -pdfjs-editor-edit-comment-actions-edit-button-label = Dรผzenle -pdfjs-editor-edit-comment-actions-delete-button-label = Sil -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Yorumunuzu yazฤฑn -pdfjs-editor-edit-comment-manager-cancel-button = Vazgeรง -pdfjs-editor-edit-comment-manager-save-button = Kaydet # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Yorumu dรผzenle pdfjs-editor-edit-comment-dialog-save-button-when-editing = Gรผncelle @@ -644,6 +632,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Vazgeรง pdfjs-editor-edit-comment-button = .title = Yorumu dรผzenle +pdfjs-editor-add-comment-button = + .title = Yorum ekle ## Main menu for adding/removing signatures diff --git a/l10n/vi/viewer.ftl b/l10n/vi/viewer.ftl index 92769b5e4..d9f42af85 100644 --- a/l10n/vi/viewer.ftl +++ b/l10n/vi/viewer.ftl @@ -602,18 +602,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Hร nh ฤ‘แป™ng -pdfjs-editor-edit-comment-actions-button = - .title = Hร nh ฤ‘แป™ng -pdfjs-editor-edit-comment-close-button-label = ฤรณng -pdfjs-editor-edit-comment-close-button = - .title = ฤรณng -pdfjs-editor-edit-comment-actions-edit-button-label = Chแป‰nh sแปญa -pdfjs-editor-edit-comment-actions-delete-button-label = Xรณa -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Nhแบญp chรบ thรญch cแปงa bแบกn -pdfjs-editor-edit-comment-manager-cancel-button = Hแปงy bแป -pdfjs-editor-edit-comment-manager-save-button = Lฦฐu # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = Chแป‰nh sแปญa chรบ thรญch pdfjs-editor-edit-comment-dialog-save-button-when-editing = Cแบญp nhแบญt diff --git a/l10n/zh-CN/viewer.ftl b/l10n/zh-CN/viewer.ftl index 934595107..7127316a7 100644 --- a/l10n/zh-CN/viewer.ftl +++ b/l10n/zh-CN/viewer.ftl @@ -602,18 +602,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ๆ“ไฝœ -pdfjs-editor-edit-comment-actions-button = - .title = ๆ“ไฝœ -pdfjs-editor-edit-comment-close-button-label = ๅ…ณ้—ญ -pdfjs-editor-edit-comment-close-button = - .title = ๅ…ณ้—ญ -pdfjs-editor-edit-comment-actions-edit-button-label = ็ผ–่พ‘ -pdfjs-editor-edit-comment-actions-delete-button-label = ๅˆ ้™ค -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ่พ“ๅ…ฅๆ‰นๆณจ -pdfjs-editor-edit-comment-manager-cancel-button = ๅ–ๆถˆ -pdfjs-editor-edit-comment-manager-save-button = ไฟๅญ˜ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = ็ผ–่พ‘ๆ‰นๆณจ pdfjs-editor-edit-comment-dialog-save-button-when-editing = ๆ›ดๆ–ฐ @@ -628,6 +616,8 @@ pdfjs-editor-edit-comment-dialog-cancel-button = ๅ–ๆถˆ pdfjs-editor-edit-comment-button = .title = ็ผ–่พ‘ๆ‰นๆณจ +pdfjs-editor-add-comment-button = + .title = ๆทปๅŠ ๆ‰นๆณจ ## Main menu for adding/removing signatures diff --git a/l10n/zh-TW/viewer.ftl b/l10n/zh-TW/viewer.ftl index cb3d3a04e..239d16446 100644 --- a/l10n/zh-TW/viewer.ftl +++ b/l10n/zh-TW/viewer.ftl @@ -602,18 +602,6 @@ pdfjs-show-comment-button = ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = ๅ‹•ไฝœ -pdfjs-editor-edit-comment-actions-button = - .title = ๅ‹•ไฝœ -pdfjs-editor-edit-comment-close-button-label = ้—œ้–‰ -pdfjs-editor-edit-comment-close-button = - .title = ้—œ้–‰ -pdfjs-editor-edit-comment-actions-edit-button-label = ็ทจ่ผฏ -pdfjs-editor-edit-comment-actions-delete-button-label = ๅˆช้™ค -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = ่ผธๅ…ฅๆ‚จ็š„่จป่งฃ -pdfjs-editor-edit-comment-manager-cancel-button = ๅ–ๆถˆ -pdfjs-editor-edit-comment-manager-save-button = ๅ„ฒๅญ˜ # An existing comment is edited pdfjs-editor-edit-comment-dialog-title-when-editing = ็ทจ่ผฏ่จป่งฃ pdfjs-editor-edit-comment-dialog-save-button-when-editing = ๆ›ดๆ–ฐ From b5b821365ed67aa2f468128b3a3b4f3967b07dd9 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 18 Oct 2025 17:19:21 +0200 Subject: [PATCH 18/26] Introduce a helper function to create a freetext editor in the integration tests Doing so has a number of advantages: - it removes code duplication, thereby improving readability; - it removes hardcoded editor IDs, by using the `getNextEditorId` helper function that was previously introduced for the highlight editor integration tests, thereby improving readability and reusability; - it removes potential for intermittent failures by not proceeding until the freetext editor is fully created and all assertions pass, which didn't happen consistently before because the code wasn't centralized. --- test/integration/freetext_editor_spec.mjs | 616 +++++++++++----------- test/integration/test_utils.mjs | 17 + 2 files changed, 319 insertions(+), 314 deletions(-) diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index ca062b023..b8a5cb4e6 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -19,6 +19,8 @@ import { closePages, copy, copyToClipboard, + countSerialized, + countStorageEntries, createPromise, dragAndDrop, firstPageOnTop, @@ -26,6 +28,7 @@ import { getEditors, getEditorSelector, getFirstSerialized, + getNextEditorId, getRect, getSerialized, isCanvasMonochrome, @@ -85,6 +88,34 @@ const cancelFocusIn = async (page, selector) => { }, selector); }; +const createFreeTextEditor = async ({ + page, + x, + y, + data = null, + noFocusIn = false, +}) => { + const editorSelector = getEditorSelector(await getNextEditorId(page)); + const serializedCount = await countSerialized(page); + const storageEntriesCount = await countStorageEntries(page); + + await page.mouse.click(x, y); + await page.waitForSelector(editorSelector, { visible: true }); + if (data) { + await page.type(`${editorSelector} .internal`, data); + } + if (noFocusIn) { + await cancelFocusIn(page, editorSelector); + } + await commit(page); + + await waitForSelectedEditor(page, editorSelector); + await waitForStorageEntries(page, storageEntriesCount + 1); + await waitForSerialized(page, serializedCount + 1); + + return editorSelector; +}; + describe("FreeText Editor", () => { describe("FreeText", () => { let pages; @@ -103,15 +134,13 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - - await waitForSelectedEditor(page, editorSelector); - await waitForStorageEntries(page, 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data, + }); await page.waitForFunction( `document.getElementById("viewer-alert").textContent === "Text added"` @@ -143,13 +172,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const firstEditorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(firstEditorSelector, { visible: true }); - await page.type(`${firstEditorSelector} .internal`, data); - await commit(page); - await waitForStorageEntries(page, 1); + const firstEditorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await selectEditor(page, firstEditorSelector); await copy(page); @@ -187,13 +215,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const firstEditorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(firstEditorSelector, { visible: true }); - await page.type(`${firstEditorSelector} .internal`, data); - await commit(page); - await waitForStorageEntries(page, 1); + const firstEditorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await page.evaluate(() => { window.PDFViewerApplication.eventBus.dispatch( @@ -240,12 +267,12 @@ describe("FreeText Editor", () => { const rect = await getRect(page, ".annotationEditorLayer"); for (const n of [0, 1, 2]) { - const editorSelector = getEditorSelector(n); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100 * n, rect.y + 100 * n); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100 * n, + y: rect.y + 100 * n, + data: "Hello PDF.js World !!", + }); const hasEditor = await page.evaluate( sel => !!document.querySelector(sel), @@ -275,12 +302,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - let editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + let editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await selectEditor(page, editorSelector); await copy(page); @@ -317,12 +344,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); expect(await getEditors(page, "selected")) .withContext(`In ${browserName}`) @@ -460,12 +487,12 @@ describe("FreeText Editor", () => { const editorCenters = []; let lastX = rect.x + rect.width / 10; for (let i = 0; i < 4; i++) { - const editorSelector = getEditorSelector(i); - const data = `FreeText ${i}`; - await page.mouse.click(lastX, rect.y + rect.height / 10); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: lastX, + y: rect.y + rect.height / 10, + data: `FreeText ${i}`, + }); const editorRect = await getRect(page, editorSelector); lastX = editorRect.x + editorRect.width + 10; @@ -627,16 +654,13 @@ describe("FreeText Editor", () => { ); expect(oldAriaOwns).withContext(`In ${browserName}`).toEqual(null); - const editorSelector = getEditorSelector(0); const rect = await getRect(page, `span[pdfjs="true"]`); - const data = "Hello PDF.js World !!"; - await page.mouse.click( - rect.x + rect.width / 2, - rect.y + rect.height / 2 - ); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + await createFreeTextEditor({ + page, + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + data: "Hello PDF.js World !!", + }); const newAriaOwns = await page.$eval(`span[pdfjs="true"]`, el => el.getAttribute("aria-owns") @@ -664,7 +688,6 @@ describe("FreeText Editor", () => { await Promise.all( pages.map(async ([browserName, page]) => { await switchToFreeText(page); - let currentId = 0; const expected = []; const oneToFourteen = Array.from(new Array(14).keys(), x => x + 1); @@ -682,23 +705,19 @@ describe("FreeText Editor", () => { } const rect = await getRect(page, annotationLayerSelector); - const editorSelector = getEditorSelector(currentId); const data = `Hello PDF.js World !! on page ${pageNumber}`; expected.push(data); - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - - await waitForSelectedEditor(page, editorSelector); - await waitForStorageEntries(page, currentId + 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data, + }); const content = await page.$eval(editorSelector, el => el.innerText.trimEnd() ); expect(content).withContext(`In ${browserName}`).toEqual(data); - - currentId += 1; } const serialize = proprName => @@ -808,19 +827,16 @@ describe("FreeText Editor", () => { await Promise.all( pages.map(async ([browserName, page]) => { await switchToFreeText(page); - let currentId = 0; for (let step = 0; step < 3; step++) { await firstPageOnTop(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(currentId); - const data = `Hello ${step}`; - const x = Math.max(rect.x + 0.1 * rect.width, 10); - const y = Math.max(rect.y + 0.1 * rect.height, 10); - await page.mouse.click(x, y); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + await createFreeTextEditor({ + page, + x: Math.max(rect.x + 0.1 * rect.width, 10), + y: Math.max(rect.y + 0.1 * rect.height, 10), + data: `Hello ${step}`, + }); const promise = await waitForAnnotationEditorLayer(page); await page.evaluate(() => { @@ -828,7 +844,6 @@ describe("FreeText Editor", () => { }); await awaitPromise(promise); - currentId += 1; await page.waitForSelector( ".page[data-page-number='1'] .canvasWrapper", { @@ -1353,12 +1368,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); // Make Chrome happy. await page.waitForFunction(() => { @@ -1445,9 +1460,8 @@ describe("FreeText Editor", () => { await Promise.all( pages.map(async ([browserName, page]) => { await switchToFreeText(page); - let currentId = 0; - const oneToFourteen = Array.from(new Array(14).keys(), x => x + 1); + const oneToFourteen = Array.from(new Array(14).keys(), x => x + 1); for (const pageNumber of oneToFourteen) { const pageSelector = `.page[data-page-number = "${pageNumber}"]`; @@ -1462,14 +1476,12 @@ describe("FreeText Editor", () => { } const rect = await getRect(page, annotationLayerSelector); - const editorSelector = getEditorSelector(currentId); - const data = `Hello PDF.js World !! on page ${pageNumber}`; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - - currentId += 1; + await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: `Hello PDF.js World !! on page ${pageNumber}`, + }); } await selectAll(page); @@ -1804,12 +1816,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await page.focus("#editorFreeTextColor"); await kbUndo(page); @@ -1848,13 +1860,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - - const data = "Hello PDF.js World !!"; - const editorSelector = getEditorSelector(0); - await page.mouse.click(rect.x + 200, rect.y + 200); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 200, + y: rect.y + 200, + data: "Hello PDF.js World !!", + }); const [pageX, pageY] = await getFirstSerialized(page, x => x.rect); @@ -1911,12 +1922,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const data = "Hello PDF.js World !!"; - const editorSelector = getEditorSelector(0); - await page.mouse.click(rect.x + 200, rect.y + 200); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 200, + y: rect.y + 200, + data: "Hello PDF.js World !!", + }); await selectAll(page); await page.focus("#editorFreeTextFontSize"); @@ -1950,16 +1961,17 @@ describe("FreeText Editor", () => { const rect = await getRect(page, ".annotationEditorLayer"); const data = "Hello PDF.js World !!"; - let editorSelector = getEditorSelector(0); - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data, + }); const [pageX, pageY] = await getFirstSerialized(page, x => x.rect); await clearAll(page); - editorSelector = getEditorSelector(1); + const editorSelector = getEditorSelector(1); await page.mouse.click(rect.x + 100, rect.y + 100); await page.waitForSelector(editorSelector, { visible: true }); @@ -2023,13 +2035,13 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await cancelFocusIn(page, editorSelector); - await commit(page); + await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + noFocusIn: true, + }); const oneToFourteen = Array.from(new Array(14).keys(), x => x + 1); @@ -2076,23 +2088,21 @@ describe("FreeText Editor", () => { await switchToFreeText(page); let rect = await getRect(page, ".annotationEditorLayer"); - - const firstEditorSelector = getEditorSelector(0); - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(firstEditorSelector, { visible: true }); - await page.type(`${firstEditorSelector} .internal`, "A"); - await commit(page); + const firstEditorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "A", + }); // Create a new editor. rect = await getRect(page, firstEditorSelector); - const secondEditorSelector = getEditorSelector(1); - await page.mouse.click( - rect.x + 5 * rect.width, - rect.y + 5 * rect.height - ); - await page.waitForSelector(secondEditorSelector, { visible: true }); - await page.type(`${secondEditorSelector} .internal`, "B"); - await commit(page); + const secondEditorSelector = await createFreeTextEditor({ + page, + x: rect.x + 5 * rect.width, + y: rect.y + 5 * rect.height, + data: "B", + }); // Select the second editor. await selectEditor(page, secondEditorSelector); @@ -2155,15 +2165,12 @@ describe("FreeText Editor", () => { const allPositions = []; for (let i = 0; i < 10; i++) { - const editorSelector = getEditorSelector(i); - await page.mouse.click(rect.x + 10 + 30 * i, rect.y + 100 + 5 * i); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type( - `${editorSelector} .internal`, - String.fromCharCode(65 + i) - ); - await commit(page); - + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 10 + 30 * i, + y: rect.y + 100 + 5 * i, + data: String.fromCharCode(65 + i), + }); allPositions.push(await getRect(page, editorSelector)); } @@ -2214,13 +2221,13 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await cancelFocusIn(page, editorSelector); - await commit(page); + await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + noFocusIn: true, + }); await page.evaluate(() => { window.editingEvents = []; @@ -2267,12 +2274,13 @@ describe("FreeText Editor", () => { const page1Selector = `.page[data-page-number = "1"] > .annotationEditorLayer.freetextEditing`; let rect = await getRect(page, page1Selector); - const firstEditorSelector = getEditorSelector(0); - await page.mouse.click(rect.x + 10, rect.y + 10); - await page.waitForSelector(firstEditorSelector, { visible: true }); - await page.type(`${firstEditorSelector} .internal`, "Hello"); - await cancelFocusIn(page, firstEditorSelector); - await commit(page); + const firstEditorSelector = await createFreeTextEditor({ + page, + x: rect.x + 10, + y: rect.y + 10, + data: "Hello", + noFocusIn: true, + }); // Unselect. await unselectEditor(page, firstEditorSelector); @@ -2290,11 +2298,12 @@ describe("FreeText Editor", () => { }); rect = await getRect(page, page14Selector); - const secondEditorSelector = getEditorSelector(1); - await page.mouse.click(rect.x + 10, rect.y + 10); - await page.waitForSelector(secondEditorSelector, { visible: true }); - await page.type(`${secondEditorSelector} .internal`, "World"); - await commit(page); + await createFreeTextEditor({ + page, + x: rect.x + 10, + y: rect.y + 10, + data: "World", + }); for (let i = 0; i < 13; i++) { await page.keyboard.press("P"); @@ -2336,12 +2345,13 @@ describe("FreeText Editor", () => { const page1Selector = `.page[data-page-number = "1"] > .annotationEditorLayer.freetextEditing`; const rect = await getRect(page, page1Selector); - const editorSelector = getEditorSelector(0); - await page.mouse.click(rect.x + 10, rect.y + 10); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, "Hello"); - await cancelFocusIn(page, editorSelector); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 10, + y: rect.y + 10, + data: "Hello", + noFocusIn: true, + }); // Unselect. await unselectEditor(page, editorSelector); @@ -2394,7 +2404,6 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const parentId = "p3R_mc8"; - const editorSelector = getEditorSelector(0); const rect = await page.evaluate(id => { const parent = document.getElementById(id); let span = null; @@ -2407,15 +2416,13 @@ describe("FreeText Editor", () => { const { x, y, width, height } = span.getBoundingClientRect(); return { x, y, width, height }; }, parentId); - await page.mouse.click( - rect.x + rect.width + 5, - rect.y + rect.height / 2 - ); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, "Hello Wolrd"); - await commit(page); - await waitForStorageEntries(page, 1); + await createFreeTextEditor({ + page, + x: rect.x + rect.width + 5, + y: rect.y + rect.height / 2, + data: "Hello World", + }); const id = await getFirstSerialized(page, x => x.structTreeParentId); expect(id).withContext(`In ${browserName}`).toEqual(parentId); @@ -2441,21 +2448,19 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - const internalEditorSelector = `${editorSelector} .internal`; - await page.type(internalEditorSelector, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data, + }); await page.click(editorSelector, { count: 2 }); await page.waitForSelector( `${editorSelector} .overlay:not(.enabled)` ); - await page.click(internalEditorSelector, { - count: 3, - }); + await page.click(`${editorSelector} .internal`, { count: 3 }); const selection = await page.evaluate(() => document.getSelection().toString() ); @@ -2588,12 +2593,13 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data, + }); let handle = await createPromise(page, resolve => { document.addEventListener("selectionchange", resolve, { @@ -2642,12 +2648,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); // Delete it in using the button. await page.click(`${editorSelector} button.deleteButton`); @@ -2685,38 +2691,39 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); + const firstEditorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); + const secondEditorSelector = await createFreeTextEditor({ + page, + x: rect.x + 200, + y: rect.y + 200, + data: "Hello PDF.js World !!", + }); - const data = "Hello PDF.js World !!"; - - for (let i = 1; i <= 2; i++) { - const editorSelector = getEditorSelector(i - 1); - await page.mouse.click(rect.x + i * 100, rect.y + i * 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - } - - // Select the editor created previously. - const editorSelector = getEditorSelector(0); - await selectEditor(page, editorSelector); + // Select the first editor. + await selectEditor(page, firstEditorSelector); await selectAll(page); // Delete it in using the button. - await page.focus(`${editorSelector} button.deleteButton`); + await page.focus(`${firstEditorSelector} button.deleteButton`); await page.keyboard.press("Enter"); await page.waitForFunction( sel => !document.querySelector(sel), {}, - editorSelector + firstEditorSelector ); await waitForStorageEntries(page, 0); // Undo. await kbUndo(page); await waitForSerialized(page, 2); - await page.waitForSelector(editorSelector, { visible: true }); - await page.waitForSelector(getEditorSelector(1), { visible: true }); + await page.waitForSelector(firstEditorSelector, { visible: true }); + await page.waitForSelector(secondEditorSelector, { visible: true }); }) ); }); @@ -2801,14 +2808,14 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); const data = "Hello\nPDF.js\nWorld\n!!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data, + }); - await waitForSerialized(page, 1); const serialized = (await getSerialized(page))[0]; expect(serialized.value) .withContext(`In ${browserName}`) @@ -2835,12 +2842,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await page.evaluate(() => { window.PDFViewerApplication.eventBus.dispatch( @@ -2936,13 +2943,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - await waitForSerialized(page, 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await page.waitForSelector(`${editorSelector} button.deleteButton`); await page.click(`${editorSelector} button.deleteButton`); @@ -2986,13 +2992,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - await waitForSerialized(page, 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await page.waitForSelector(`${editorSelector} button.deleteButton`); await page.click(`${editorSelector} button.deleteButton`); @@ -3031,13 +3036,13 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - - let editorSelector = getEditorSelector(0); const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + let editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data, + }); const waitForTextChange = (previous, edSelector) => page.waitForFunction( @@ -3300,7 +3305,6 @@ describe("FreeText Editor", () => { describe("Undo deletion popup has the expected behaviour", () => { let pages; - const editorSelector = getEditorSelector(0); beforeEach(async () => { pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); @@ -3316,12 +3320,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - await waitForSerialized(page, 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await page.waitForSelector(`${editorSelector} button.deleteButton`); await page.click(`${editorSelector} button.deleteButton`); @@ -3344,12 +3348,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - await waitForSerialized(page, 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await page.waitForSelector(`${editorSelector} button.deleteButton`); await page.click(`${editorSelector} button.deleteButton`); @@ -3377,12 +3381,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); let rect = await getRect(page, ".annotationEditorLayer"); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - await waitForSerialized(page, 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); await page.waitForSelector(`${editorSelector} button.deleteButton`); await page.click(`${editorSelector} button.deleteButton`); @@ -3390,13 +3394,12 @@ describe("FreeText Editor", () => { await page.waitForSelector("#editorUndoBar", { visible: true }); rect = await getRect(page, ".annotationEditorLayer"); - const secondEditorSelector = getEditorSelector(1); - const newData = "This is a new text box!"; - await page.mouse.click(rect.x + 150, rect.y + 150); - await page.waitForSelector(secondEditorSelector, { visible: true }); - await page.type(`${secondEditorSelector} .internal`, newData); - await commit(page); - await waitForSerialized(page, 1); + await createFreeTextEditor({ + page, + x: rect.x + 150, + y: rect.y + 150, + data: "This is a new text box!", + }); await page.waitForSelector("#editorUndoBar", { hidden: true }); }) ); @@ -3420,14 +3423,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - await waitForSerialized(page, 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + 100, + y: rect.y + 100, + data: "Hello PDF.js World !!", + }); let alignment = await page.$eval( `${editorSelector} .internal`, @@ -3468,17 +3469,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - - const data = "Hello PDF.js World !!"; - await page.mouse.click( - rect.x + rect.width / 2, - rect.y + rect.height / 2 - ); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - await waitForSerialized(page, 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + data: "Hello PDF.js World !!", + }); await switchToFreeText(page, /* disable */ true); @@ -3508,17 +3504,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - - const data = "Hello PDF.js World !!"; - await page.mouse.click( - rect.x + rect.width / 2, - rect.y + rect.height / 2 - ); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); - await waitForSerialized(page, 1); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + data: "Hello PDF.js World !!", + }); await switchToFreeText(page, /* disable */ true); await switchToEditor("Ink", page); @@ -3573,15 +3564,12 @@ describe("FreeText Editor", () => { await switchToFreeText(page); const rect = await getRect(page, ".annotationEditorLayer"); - const editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click( - rect.x + rect.width / 2, - rect.y + rect.height / 2 - ); - await page.waitForSelector(editorSelector, { visible: true }); - await page.type(`${editorSelector} .internal`, data); - await commit(page); + const editorSelector = await createFreeTextEditor({ + page, + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + data: "Hello PDF.js World !!", + }); const colorPickerSelector = `${editorSelector} input.basicColorPicker`; await page.waitForSelector(colorPickerSelector, { visible: true }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 820690efd..630eb6275 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -310,6 +310,12 @@ async function waitForEvent({ } } +async function countStorageEntries(page) { + return page.evaluate( + () => window.PDFViewerApplication.pdfDocument.annotationStorage.size + ); +} + async function waitForStorageEntries(page, nEntries) { return page.waitForFunction( n => window.PDFViewerApplication.pdfDocument.annotationStorage.size === n, @@ -318,6 +324,14 @@ async function waitForStorageEntries(page, nEntries) { ); } +async function countSerialized(page) { + return page.evaluate( + () => + window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.map + ?.size ?? 0 + ); +} + async function waitForSerialized(page, nEntries) { return page.waitForFunction( n => { @@ -924,6 +938,8 @@ export { closeSinglePage, copy, copyToClipboard, + countSerialized, + countStorageEntries, createPromise, dragAndDrop, firstPageOnTop, @@ -935,6 +951,7 @@ export { getEditors, getEditorSelector, getFirstSerialized, + getNextEditorId, getQuerySelector, getRect, getSelector, From 65881f0e21c45bec3b76927eb81037e1a9ebe555 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sun, 9 Nov 2025 15:28:43 +0100 Subject: [PATCH 19/26] Add a wrapper for the new xref in order to be able to get some values from cloned dictionaries --- src/core/editor/pdf_editor.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 42769f636..1a2845716 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -52,6 +52,16 @@ class DocumentData { } } +class XRefWrapper { + constructor(entries) { + this.entries = entries; + } + + fetch(ref) { + return ref instanceof Ref ? this.entries[ref.num] : ref; + } +} + class PDFEditor { constructor({ useObjectStreams = true, title = "", author = "" } = {}) { this.hasSingleFile = false; @@ -59,6 +69,7 @@ class PDFEditor { this.oldPages = []; this.newPages = []; this.xref = [null]; + this.xrefWrapper = new XRefWrapper(this.xref); this.newRefCount = 1; [this.rootRef, this.rootDict] = this.newDict; [this.infoRef, this.infoDict] = this.newDict; @@ -173,9 +184,11 @@ class PDFEditor { let dict; if (obj instanceof BaseStream) { ({ dict } = obj = obj.getOriginalStream().clone()); + dict.xref = this.xrefWrapper; } else if (obj instanceof Dict) { if (mustClone) { obj = obj.clone(); + obj.xref = this.xrefWrapper; } dict = obj; } From a98b0b1fb5e0802436d01b6d674605c6a65c3e3b Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sun, 9 Nov 2025 15:34:57 +0100 Subject: [PATCH 20/26] Version entry in the catalog has to be a name and not a string --- src/core/editor/pdf_editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 5241863a7..e15a2d6c5 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -550,7 +550,7 @@ class PDFEditor { async #makeRoot() { const { rootDict } = this; rootDict.setIfName("Type", "Catalog"); - rootDict.set("Version", this.version); + rootDict.setIfName("Version", this.version); this.#makePageTree(); this.#makePageLabelsTree(); } From 50c48cf11b36025f2f3c297f2abc9a062013de10 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 29 Oct 2025 16:06:01 +0100 Subject: [PATCH 21/26] Add telemetry for tagged pdfs (bug 1997134) --- src/core/catalog.js | 4 ++++ src/core/worker.js | 1 + src/display/api.js | 1 + web/app.js | 13 +++++++++++-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/core/catalog.js b/src/core/catalog.js index 5b5b319c7..3f73e6d91 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -267,6 +267,10 @@ class Catalog { return markInfo; } + get hasStructTree() { + return this.#catDict.has("StructTreeRoot"); + } + get structTreeRoot() { let structTree = null; try { diff --git a/src/core/worker.js b/src/core/worker.js index 8ef7b10f4..1b6accb85 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -515,6 +515,7 @@ class WorkerMessageHandler { return Promise.all([ pdfManager.ensureDoc("documentInfo"), pdfManager.ensureCatalog("metadata"), + pdfManager.ensureCatalog("hasStructTree"), ]); }); diff --git a/src/display/api.js b/src/display/api.js index b279df994..75ed964d3 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -3077,6 +3077,7 @@ class WorkerTransport { metadata: results[1] ? new Metadata(results[1]) : null, contentDispositionFilename: this._fullReader?.filename ?? null, contentLength: this._fullReader?.contentLength ?? null, + hasStructTree: results[2], })); this.#methodPromises.set(name, promise); return promise; diff --git a/web/app.js b/web/app.js index a9a6e8d88..3d6b122f0 100644 --- a/web/app.js +++ b/web/app.js @@ -1719,12 +1719,21 @@ const PDFViewerApplication = { * @private */ async _initializeMetadata(pdfDocument) { - const { info, metadata, contentDispositionFilename, contentLength } = - await pdfDocument.getMetadata(); + const { + info, + metadata, + contentDispositionFilename, + contentLength, + hasStructTree, + } = await pdfDocument.getMetadata(); if (pdfDocument !== this.pdfDocument) { return; // The document was closed while the metadata resolved. } + this.externalServices.reportTelemetry({ + type: "taggedPDF", + data: hasStructTree, + }); this.documentInfo = info; this.metadata = metadata; this._contentDispositionFilename ??= contentDispositionFilename; From e13a618df358eca47a123b3b5f89f26261a34e74 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 10 Nov 2025 21:09:35 +0100 Subject: [PATCH 22/26] Merge the structure trees coming from different pdfs (bug 1997379) --- src/core/editor/pdf_editor.js | 641 ++++++++++++++++++++++++++++++- src/core/name_number_tree.js | 7 +- test/pdfs/.gitignore | 2 + test/pdfs/paragraph_and_link.pdf | Bin 0 -> 45232 bytes test/pdfs/two_paragraphs.pdf | Bin 0 -> 25142 bytes test/unit/api_spec.js | 246 ++++++++++++ 6 files changed, 889 insertions(+), 7 deletions(-) create mode 100755 test/pdfs/paragraph_and_link.pdf create mode 100755 test/pdfs/two_paragraphs.pdf diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 9e4f81c62..2f9d003e3 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -17,9 +17,10 @@ /** @typedef {import("../document.js").Page} Page */ /** @typedef {import("../xref.js").XRef} XRef */ -import { Dict, isName, Ref, RefSetCache } from "../primitives.js"; +import { Dict, isName, Name, Ref, RefSet, RefSetCache } from "../primitives.js"; import { getModificationDate, stringToPDFString } from "../../shared/util.js"; import { incrementalUpdate, writeValue } from "../writer.js"; +import { NameTree, NumberTree } from "../name_number_tree.js"; import { BaseStream } from "../base_stream.js"; import { StringStream } from "../stream.js"; import { stringToAsciiOrUTF16BE } from "../core_utils.js"; @@ -49,6 +50,16 @@ class DocumentData { this.dedupNamedDestinations = new Map(); this.usedNamedDestinations = new Set(); this.postponedRefCopies = new RefSetCache(); + this.usedStructParents = new Set(); + this.oldStructParentMapping = new Map(); + this.structTreeRoot = null; + this.parentTree = null; + this.idTree = null; + this.roleMap = null; + this.classMap = null; + this.namespaces = null; + this.structTreeAF = null; + this.structTreePronunciationLexicon = []; } } @@ -82,6 +93,14 @@ class PDFEditor { this.author = author; this.pageLabels = null; this.namedDestinations = new Map(); + this.parentTree = new Map(); + this.structTreeKids = []; + this.idTree = new Map(); + this.classMap = new Dict(); + this.roleMap = new Dict(); + this.namespaces = new Map(); + this.structTreeAF = []; + this.structTreePronunciationLexicon = []; } /** @@ -115,6 +134,12 @@ class PDFEditor { return ref; } + cloneDict(dict) { + const newDict = dict.clone(); + newDict.xref = this.xrefWrapper; + return newDict; + } + /** * Collect the dependencies of an object and create new references for each * dependency. @@ -212,6 +237,232 @@ class PDFEditor { return obj; } + async #cloneStructTreeNode( + parentStructRef, + node, + xref, + removedStructElements, + dedupIDs, + dedupClasses, + dedupRoles, + visited = new RefSet() + ) { + const { + currentDocument: { pagesMap, oldRefMapping }, + } = this; + const pg = node.getRaw("Pg"); + if (pg instanceof Ref && !pagesMap.has(pg)) { + return null; + } + let kids; + const k = (kids = node.getRaw("K")); + if (k instanceof Ref) { + // We're only interested by ref referencing nodes and not an array. + if (visited.has(k)) { + return null; + } + kids = await xref.fetchAsync(k); + if (!Array.isArray(kids)) { + kids = [k]; + } + } + kids = Array.isArray(kids) ? kids : [kids]; + const newKids = []; + const structElemIndices = []; + for (let kid of kids) { + const kidRef = kid instanceof Ref ? kid : null; + if (kidRef) { + if (visited.has(kidRef)) { + continue; + } + visited.put(kidRef); + kid = await xref.fetchAsync(kidRef); + } + if (typeof kid === "number") { + newKids.push(kid); + continue; + } + if (!(kid instanceof Dict)) { + continue; + } + const pgRef = kid.getRaw("Pg"); + if (pgRef instanceof Ref && !pagesMap.has(pgRef)) { + continue; + } + const type = kid.get("Type"); + if (!type || isName(type, "StructElem")) { + let setAsSpan = false; + if (kidRef && removedStructElements.has(kidRef)) { + if (!isName(kid.get("S"), "Link")) { + continue; + } + // A link annotation has been removed but we still need to keep the + // node in order to preserve the structure tree. Mark it as a Span + // so that it doesn't affect the semantics. + setAsSpan = true; + } + const newKidRef = await this.#cloneStructTreeNode( + kidRef, + kid, + xref, + removedStructElements, + dedupIDs, + dedupClasses, + dedupRoles, + visited + ); + if (newKidRef) { + structElemIndices.push(newKids.length); + newKids.push(newKidRef); + if (kidRef) { + oldRefMapping.put(kidRef, newKidRef); + } + if (setAsSpan) { + this.xref[newKidRef.num].setIfName("S", "Span"); + } + } + continue; + } + if (isName(type, "OBJR")) { + if (!kidRef) { + continue; + } + const newKidRef = oldRefMapping.get(kidRef); + if (!newKidRef) { + continue; + } + const newKid = this.xref[newKidRef.num]; + // Fix the missing StructParent entry in the referenced object. + const objRef = newKid.getRaw("Obj"); + if (objRef instanceof Ref) { + const obj = this.xref[objRef.num]; + if ( + obj instanceof Dict && + !obj.has("StructParent") && + parentStructRef + ) { + const structParent = this.parentTree.size; + this.parentTree.set(structParent, [oldRefMapping, parentStructRef]); + obj.set("StructParent", structParent); + } + } + newKids.push(newKidRef); + continue; + } + if (isName(type, "MCR")) { + const newKid = await this.#collectDependencies( + kidRef || kid, + true, + xref + ); + newKids.push(newKid); + continue; + } + if (kidRef) { + const newKidRef = await this.#collectDependencies(kidRef, true, xref); + newKids.push(newKidRef); + } + } + if (kids.length !== 0 && newKids.length === 0) { + return null; + } + + const newNodeRef = this.newRef; + const newNode = (this.xref[newNodeRef.num] = this.cloneDict(node)); + // Don't collect for ID or C since they will be fixed later. + newNode.delete("ID"); + newNode.delete("C"); + newNode.delete("K"); + newNode.delete("P"); + newNode.delete("S"); + await this.#collectDependencies(newNode, false, xref); + + // Fix the class names. + const classNames = node.get("C"); + if (classNames instanceof Name) { + const newClassName = dedupClasses.get(classNames.name); + if (newClassName) { + newNode.set("C", Name.get(newClassName)); + } else { + newNode.set("C", classNames); + } + } else if (Array.isArray(classNames)) { + const newClassNames = []; + for (const className of classNames) { + if (className instanceof Name) { + const newClassName = dedupClasses.get(className.name); + if (newClassName) { + newClassNames.push(Name.get(newClassName)); + } else { + newClassNames.push(className); + } + } + } + newNode.set("C", newClassNames); + } + + // Fix the role name. + const roleName = node.get("S"); + if (roleName instanceof Name) { + const newRoleName = dedupRoles.get(roleName.name); + if (newRoleName) { + newNode.set("S", Name.get(newRoleName)); + } else { + newNode.set("S", roleName); + } + } + + // Fix the ID. + const id = node.get("ID"); + if (typeof id === "string") { + const stringId = stringToPDFString(id, /* keepEscapeSequence = */ false); + const newId = dedupIDs.get(stringId); + if (newId) { + newNode.set("ID", stringToAsciiOrUTF16BE(newId)); + } else { + newNode.set("ID", id); + } + } + + // Table headers may contain IDs that need to be deduplicated. + let attributes = newNode.get("A"); + if (attributes) { + if (!Array.isArray(attributes)) { + attributes = [attributes]; + } + for (let attr of attributes) { + attr = this.xrefWrapper.fetch(attr); + if (isName(attr.get("O"), "Table") && attr.has("Headers")) { + const headers = this.xrefWrapper.fetch(attr.getRaw("Headers")); + if (Array.isArray(headers)) { + for (let i = 0, ii = headers.length; i < ii; i++) { + const newId = dedupIDs.get( + stringToPDFString(headers[i], /* keepEscapeSequence = */ false) + ); + if (newId) { + headers[i] = newId; + } + } + } + } + } + } + + for (const index of structElemIndices) { + const structElemRef = newKids[index]; + const structElem = this.xref[structElemRef.num]; + structElem.set("P", newNodeRef); + } + + if (newKids.length === 1) { + newNode.set("K", newKids[0]); + } else if (newKids.length > 1) { + newNode.set("K", newKids); + } + + return newNodeRef; + } + /** * @typedef {Object} PageInfo * @property {PDFDocument} document @@ -315,6 +566,7 @@ class PDFEditor { } this.#fixPostponedRefCopies(allDocumentData); + await this.#mergeStructTrees(allDocumentData); return this.writePDF(); } @@ -326,7 +578,7 @@ class PDFEditor { */ async #collectDocumentData(documentData) { const { - document: { pdfManager }, + document: { pdfManager, xref }, } = documentData; await Promise.all([ pdfManager @@ -335,7 +587,34 @@ class PDFEditor { pdfManager .ensureCatalog("rawPageLabels") .then(pageLabels => (documentData.pageLabels = pageLabels)), + pdfManager + .ensureCatalog("structTreeRoot") + .then(structTreeRoot => (documentData.structTreeRoot = structTreeRoot)), ]); + const structTreeRoot = documentData.structTreeRoot; + if (structTreeRoot) { + const rootDict = structTreeRoot.dict; + const parentTree = rootDict.get("ParentTree"); + if (parentTree) { + const numberTree = new NumberTree(parentTree, xref); + documentData.parentTree = numberTree.getAll(/* isRaw = */ true); + } + const idTree = rootDict.get("IDTree"); + if (idTree) { + const nameTree = new NameTree(idTree, xref); + documentData.idTree = nameTree.getAll(/* isRaw = */ true); + } + documentData.roleMap = rootDict.get("RoleMap") || null; + documentData.classMap = rootDict.get("ClassMap") || null; + let namespaces = rootDict.get("Namespaces") || null; + if (namespaces && !Array.isArray(namespaces)) { + namespaces = [namespaces]; + } + documentData.namespaces = namespaces; + documentData.structTreeAF = rootDict.get("AF") || null; + documentData.structTreePronunciationLexicon = + rootDict.get("PronunciationLexicon") || null; + } } /** @@ -371,7 +650,6 @@ class PDFEditor { action instanceof Dict ? action.get("D") : annotationDict.get("Dest"); - if ( !dest /* not a destination */ || (Array.isArray(dest) && @@ -431,6 +709,293 @@ class PDFEditor { } } + #visitObject(obj, callback, visited = new RefSet()) { + if (obj instanceof Ref) { + if (!visited.has(obj)) { + visited.put(obj); + this.#visitObject(this.xref[obj.num], callback, visited); + } + return; + } + if (Array.isArray(obj)) { + for (const item of obj) { + this.#visitObject(item, callback, visited); + } + return; + } + let dict; + if (obj instanceof BaseStream) { + ({ dict } = obj); + } else if (obj instanceof Dict) { + dict = obj; + } + if (dict) { + callback(dict); + for (const value of dict.getRawValues()) { + this.#visitObject(value, callback, visited); + } + } + } + + async #mergeStructTrees(allDocumentData) { + let newStructParentId = 0; + const { parentTree: newParentTree } = this; + for (let i = 0, ii = this.newPages.length; i < ii; i++) { + const { + documentData: { + parentTree, + oldRefMapping, + oldStructParentMapping, + usedStructParents, + document: { xref }, + }, + } = this.oldPages[i]; + if (!parentTree) { + continue; + } + const pageRef = this.newPages[i]; + const pageDict = this.xref[pageRef.num]; + + // Visit the new page in order to collect used StructParent entries. + this.#visitObject(pageDict, dict => { + const structParent = + dict.get("StructParent") ?? dict.get("StructParents"); + if (typeof structParent !== "number") { + return; + } + usedStructParents.add(structParent); + let parent = parentTree.get(structParent); + const parentRef = parent instanceof Ref ? parent : null; + if (parentRef) { + const array = xref.fetch(parentRef); + if (Array.isArray(array)) { + parent = array; + } + } + if (Array.isArray(parent) && parent.every(ref => ref === null)) { + parent = null; + } + if (!parent) { + if (dict.has("StructParent")) { + dict.delete("StructParent"); + } else { + dict.delete("StructParents"); + } + return; + } + let newStructParent = oldStructParentMapping.get(structParent); + if (newStructParent === undefined) { + newStructParent = newStructParentId++; + oldStructParentMapping.set(structParent, newStructParent); + newParentTree.set(newStructParent, [oldRefMapping, parent]); + } + if (dict.has("StructParent")) { + dict.set("StructParent", newStructParent); + } else { + dict.set("StructParents", newStructParent); + } + }); + } + + const { + structTreeKids, + idTree: newIdTree, + classMap: newClassMap, + roleMap: newRoleMap, + namespaces: newNamespaces, + structTreeAF: newStructTreeAF, + structTreePronunciationLexicon: newStructTreePronunciationLexicon, + } = this; + // Clone the struct tree nodes for each document. + for (const documentData of allDocumentData) { + const { + document: { xref }, + oldRefMapping, + parentTree, + usedStructParents, + structTreeRoot, + idTree, + classMap, + roleMap, + namespaces, + structTreeAF, + structTreePronunciationLexicon, + } = documentData; + + if (!structTreeRoot) { + continue; + } + + this.currentDocument = documentData; + // Get all the removed StructElem + const removedStructElements = new RefSet(); + for (const [key, value] of parentTree || []) { + if (!usedStructParents.has(key) && value instanceof Ref) { + removedStructElements.put(value); + } + } + + // Deduplicate IDs in the ID tree. + // We keep the old node references since they will be cloned later when + // cloning the struct tree. + const dedupIDs = new Map(); + for (const [id, nodeRef] of idTree || []) { + let _id = id; + if (newIdTree.has(id)) { + for (let i = 1; ; i++) { + const newId = `${id}_${i}`; + if (!newIdTree.has(newId)) { + dedupIDs.set(id, newId); + _id = newId; + break; + } + } + } + newIdTree.set(_id, nodeRef); + } + + const dedupClasses = new Map(); + if (classMap?.size > 0) { + // Deduplicate ClassMap entries. + for (let [className, classDict] of classMap) { + classDict = await this.#collectDependencies(classDict, true, xref); + if (newClassMap.has(className)) { + for (let i = 1; ; i++) { + const newClassName = `${className}_${i}`; + if (!newClassMap.has(newClassName)) { + dedupClasses.set(className, newClassName); + className = newClassName; + break; + } + } + } + newClassMap.set(className, classDict); + } + } + + const dedupRoles = new Map(); + if (roleMap?.size > 0) { + // Deduplicate RoleMap entries. + for (const [roleName, mappedName] of roleMap) { + const newMappedName = newRoleMap.get(roleName); + if (!newMappedName) { + newRoleMap.set(roleName, mappedName); + continue; + } + if (newMappedName === mappedName) { + continue; + } + for (let i = 1; ; i++) { + const newRoleName = `${roleName}_${i}`; + if (!newRoleMap.has(newRoleName)) { + dedupRoles.set(roleName, newRoleName); + newRoleMap.set(newRoleName, mappedName); + break; + } + } + } + } + + if (namespaces?.length > 0) { + for (const namespaceRef of namespaces) { + const namespace = await xref.fetchIfRefAsync(namespaceRef); + let ns = namespace.get("NS"); + if (!ns || newNamespaces.has(ns)) { + continue; + } + ns = stringToPDFString(ns, /* keepEscapeSequence = */ false); + const newNamespace = await this.#collectDependencies( + namespace, + true, + xref + ); + newNamespaces.set(ns, newNamespace); + } + } + + if (structTreeAF) { + for (const afRef of structTreeAF) { + newStructTreeAF.push( + await this.#collectDependencies(afRef, true, xref) + ); + } + } + + if (structTreePronunciationLexicon) { + for (const lexiconRef of structTreePronunciationLexicon) { + newStructTreePronunciationLexicon.push( + await this.#collectDependencies(lexiconRef, true, xref) + ); + } + } + + // Get the kids. + let kids = structTreeRoot.dict.get("K"); + if (!kids) { + continue; + } + kids = Array.isArray(kids) ? kids : [kids]; + for (let kid of kids) { + const kidRef = kid instanceof Ref ? kid : null; + if (kidRef && removedStructElements.has(kidRef)) { + continue; + } + kid = await xref.fetchIfRefAsync(kid); + const newKidRef = await this.#cloneStructTreeNode( + kidRef, + kid, + xref, + removedStructElements, + dedupIDs, + dedupClasses, + dedupRoles + ); + if (newKidRef) { + structTreeKids.push(newKidRef); + } + } + + // Fix the ID tree. + for (const [id, nodeRef] of idTree || []) { + const newNodeRef = oldRefMapping.get(nodeRef); + const newId = dedupIDs.get(id) || id; + if (newNodeRef) { + newIdTree.set(newId, newNodeRef); + } else { + newIdTree.delete(newId); + } + } + } + + for (const [key, [oldRefMapping, parent]] of newParentTree) { + if (!parent) { + newParentTree.delete(key); + continue; + } + // Some nodes haven't been visited while cloning the struct trees so their + // ref don't belong to the oldRefMapping. Remove those nodes. + if (!Array.isArray(parent)) { + const newParent = oldRefMapping.get(parent); + if (newParent === undefined) { + newParentTree.delete(key); + } else { + newParentTree.set(key, newParent); + } + continue; + } + const newParents = parent.map( + ref => (ref instanceof Ref && oldRefMapping.get(ref)) || null + ); + if (newParents.length === 0 || newParents.every(ref => ref === null)) { + newParentTree.delete(key); + continue; + } + newParentTree.set(key, newParents); + } + + this.currentDocument = null; + } + /** * Collect named destinations that are still valid (i.e. pointing to kept * pages). @@ -566,7 +1131,7 @@ class PDFEditor { } if (stFirstIndex !== -1) { const st = currentLabel.get("St"); - currentLabel = currentLabel.clone(); + currentLabel = this.cloneDict(currentLabel); currentLabel.set("St", st + (i - stFirstIndex)); stFirstIndex = -1; } @@ -598,7 +1163,7 @@ class PDFEditor { const { dedupNamedDestinations, oldRefMapping } = documentData; const { xref, rotate, mediaBox, resources, ref: oldPageRef } = page; const pageRef = this.newRef; - const pageDict = (this.xref[pageRef.num] = page.pageDict.clone()); + const pageDict = (this.xref[pageRef.num] = this.cloneDict(page.pageDict)); oldRefMapping.put(oldPageRef, pageRef); if (pointingNamedDestinations) { @@ -796,6 +1361,71 @@ class PDFEditor { ); } + #makeStructTree() { + const { structTreeKids } = this; + if (!structTreeKids || structTreeKids.length === 0) { + return; + } + const { rootDict } = this; + const structTreeRef = this.newRef; + const structTree = (this.xref[structTreeRef.num] = new Dict()); + structTree.setIfName("Type", "StructTreeRoot"); + structTree.setIfArray("K", structTreeKids); + for (const kidRef of structTreeKids) { + const kid = this.xref[kidRef.num]; + const type = kid.get("Type"); + if (!type || isName(type, "StructElem")) { + kid.set("P", structTreeRef); + } + } + if (this.parentTree.size > 0) { + const parentTreeRef = this.#makeNameNumTree( + Array.from(this.parentTree.entries()), + /* areNames = */ false + ); + const parentTree = this.xref[parentTreeRef.num]; + parentTree.setIfName("Type", "ParentTree"); + structTree.set("ParentTree", parentTreeRef); + structTree.set("ParentTreeNextKey", this.parentTree.size); + } + if (this.idTree.size > 0) { + const idTreeRef = this.#makeNameNumTree( + Array.from(this.idTree.entries()), + /* areNames = */ true + ); + const idTree = this.xref[idTreeRef.num]; + idTree.setIfName("Type", "IDTree"); + structTree.set("IDTree", idTreeRef); + } + if (this.classMap.size > 0) { + const classMapRef = this.newRef; + this.xref[classMapRef.num] = this.classMap; + structTree.set("ClassMap", classMapRef); + } + if (this.roleMap.size > 0) { + const roleMapRef = this.newRef; + this.xref[roleMapRef.num] = this.roleMap; + structTree.set("RoleMap", roleMapRef); + } + if (this.namespaces.size > 0) { + const namespacesRef = this.newRef; + this.xref[namespacesRef.num] = Array.from(this.namespaces.values()); + structTree.set("Namespaces", namespacesRef); + } + if (this.structTreeAF.length > 0) { + const structTreeAFRef = this.newRef; + this.xref[structTreeAFRef.num] = this.structTreeAF; + structTree.set("AF", structTreeAFRef); + } + if (this.structTreePronunciationLexicon.length > 0) { + const structTreePronunciationLexiconRef = this.newRef; + this.xref[structTreePronunciationLexiconRef.num] = + this.structTreePronunciationLexicon; + structTree.set("PronunciationLexicon", structTreePronunciationLexiconRef); + } + rootDict.set("StructTreeRoot", structTreeRef); + } + /** * Create the root dictionary. * @returns {Promise} @@ -807,6 +1437,7 @@ class PDFEditor { this.#makePageTree(); this.#makePageLabelsTree(); this.#makeDestinationsTree(); + this.#makeStructTree(); } /** diff --git a/src/core/name_number_tree.js b/src/core/name_number_tree.js index 461711d1f..c5b63dc7b 100644 --- a/src/core/name_number_tree.js +++ b/src/core/name_number_tree.js @@ -34,7 +34,7 @@ class NameOrNumberTree { this._type = type; } - getAll() { + getAll(isRaw = false) { const map = new Map(); if (!this.root) { return map; @@ -68,7 +68,10 @@ class NameOrNumberTree { continue; } for (let i = 0, ii = entries.length; i < ii; i += 2) { - map.set(xref.fetchIfRef(entries[i]), xref.fetchIfRef(entries[i + 1])); + map.set( + xref.fetchIfRef(entries[i]), + isRaw ? entries[i + 1] : xref.fetchIfRef(entries[i + 1]) + ); } } return map; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index e2bb0f1e8..cb41f1d0a 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -759,3 +759,5 @@ !doc_3_3_pages.pdf !labelled_pages.pdf !extract_link.pdf +!two_paragraphs.pdf +!paragraph_and_link.pdf diff --git a/test/pdfs/paragraph_and_link.pdf b/test/pdfs/paragraph_and_link.pdf new file mode 100755 index 0000000000000000000000000000000000000000..610abe5b8a1d19c61be3036190ddef082fc64740 GIT binary patch literal 45232 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

    Uka;-HRcCU`x__p9!?`q&He=AA_e$ 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 w=A-aA`t73If*@&Et; literal 0 HcmV?d00001 diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 34559c4b4..50673ad4b 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -5722,5 +5722,251 @@ small scripts as well as for`); await loadingTask.destroy(); }); }); + + describe("Struct trees", function () { + it("extract pages and merge struct trees", async function () { + let loadingTask = getDocument( + buildGetDocumentParams("two_paragraphs.pdf") + ); + let pdfDoc = await loadingTask.promise; + let pdfPage = await pdfDoc.getPage(1); + const structTree = await pdfPage.getStructTree(); + expect(structTree).toEqual({ + children: [ + { + role: "Document", + children: [ + { + role: "Sect", + children: [ + { + role: "P", + children: [{ type: "content", id: "p19R_mc0" }], + lang: "EN-US", + }, + { + role: "P", + children: [{ type: "content", id: "p19R_mc1" }], + lang: "EN-US", + }, + ], + }, + ], + }, + ], + role: "Root", + }); + const filterItems = item => { + if (item.type === "beginMarkedContentProps") { + return item.id; + } + if (item.str !== undefined) { + return item.str; + } + return null; + }; + let { items } = await pdfPage.getTextContent({ + includeMarkedContent: true, + disableNormalization: true, + }); + expect(items.map(filterItems)).toEqual([ + "p19R_mc0", + "The ๏ฌrst paragraph.", + null, + "p19R_mc1", + "", + "The second paragraph.", + null, + ]); + + const data = await pdfDoc.extractPages([ + { document: null }, + { document: null }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument(data); + pdfDoc = await loadingTask.promise; + + expect(pdfDoc.numPages).toEqual(2); + pdfPage = await pdfDoc.getPage(1); + const structTree1 = await pdfPage.getStructTree(); + expect(structTree1).toEqual({ + children: [ + { + role: "Document", + children: [ + { + role: "Sect", + children: [ + { + role: "P", + children: [{ type: "content", id: "p4R_mc0" }], + lang: "EN-US", + }, + { + role: "P", + children: [{ type: "content", id: "p4R_mc1" }], + lang: "EN-US", + }, + ], + }, + ], + }, + ], + role: "Root", + }); + + ({ items } = await pdfPage.getTextContent({ + includeMarkedContent: true, + disableNormalization: true, + })); + expect(items.map(filterItems)).toEqual([ + "p4R_mc0", + "The ๏ฌrst paragraph.", + null, + "p4R_mc1", + "", + "The second paragraph.", + null, + ]); + + pdfPage = await pdfDoc.getPage(2); + const structTree2 = await pdfPage.getStructTree(); + expect(structTree2).toEqual({ + children: [ + { + role: "Document", + children: [ + { + role: "Sect", + children: [ + { + role: "P", + children: [{ type: "content", id: "p19R_mc0" }], + lang: "EN-US", + }, + { + role: "P", + children: [{ type: "content", id: "p19R_mc1" }], + lang: "EN-US", + }, + ], + }, + ], + }, + ], + role: "Root", + }); + + ({ items } = await pdfPage.getTextContent({ + includeMarkedContent: true, + disableNormalization: true, + })); + expect(items.map(filterItems)).toEqual([ + "p19R_mc0", + "The ๏ฌrst paragraph.", + null, + "p19R_mc1", + "", + "The second paragraph.", + null, + ]); + + await loadingTask.destroy(); + }); + + it("extract pages with a removed link", async function () { + let loadingTask = getDocument( + buildGetDocumentParams("paragraph_and_link.pdf") + ); + let pdfDoc = await loadingTask.promise; + + const data = await pdfDoc.extractPages([ + { document: null, excludePages: [1] }, + { document: null }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument(data); + pdfDoc = await loadingTask.promise; + + expect(pdfDoc.numPages).toEqual(3); + let pdfPage = await pdfDoc.getPage(1); + let structTree = await pdfPage.getStructTree(); + expect(structTree).toEqual({ + children: [ + { + role: "Document", + children: [ + { + role: "Sect", + children: [ + { + role: "P", + children: [{ type: "content", id: "p4R_mc0" }], + lang: "EN-US", + }, + { + role: "P", + children: [{ type: "content", id: "p4R_mc3" }], + lang: "EN-US", + }, + { + role: "P", + children: [{ type: "content", id: "p4R_mc6" }], + lang: "EN-US", + }, + ], + }, + ], + }, + ], + role: "Root", + }); + + pdfPage = await pdfDoc.getPage(2); + structTree = await pdfPage.getStructTree(); + + expect(structTree).toEqual({ + children: [ + { + role: "Document", + children: [ + { + role: "Sect", + children: [ + { + role: "P", + children: [{ type: "content", id: "p23R_mc0" }], + lang: "EN-US", + }, + { + role: "P", + children: [ + { + role: "Reference", + children: [{ type: "content", id: "p23R_mc2" }], + lang: "EN-US", + }, + { type: "content", id: "p23R_mc3" }, + ], + lang: "EN-US", + }, + { + role: "P", + children: [{ type: "content", id: "p23R_mc6" }], + lang: "EN-US", + }, + ], + }, + ], + }, + ], + role: "Root", + }); + await loadingTask.destroy(); + }); + }); }); }); From b9368b576d3b380b87e07d5dc5433d3485282dab Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 18 Nov 2025 17:16:49 +0100 Subject: [PATCH 23/26] Lint and format the HTML in using Prettier The linter found some issues in viewer.html with which isn't required and a missing closing div in test/resources/reftest-analyzer.html. The HTML can now be nicely formatted. In order to not break the build for mozilla-central, the preprocessor has been fixed in order to take into account the white spaces at the beginning of a comment line. And finally, make .prettierrc (which is supposed to be either json or yaml) itself lintable. --- .prettierrc | 15 +- examples/components/pageviewer.html | 42 +- examples/components/simpleviewer.html | 58 +- examples/components/singlepageviewer.html | 58 +- examples/image_decoders/jpeg_viewer.html | 38 +- examples/learning/helloworld.html | 123 ++-- examples/learning/helloworld64.html | 134 ++--- examples/learning/prevnext.html | 235 ++++---- examples/mobile-viewer/viewer.html | 14 +- examples/text-only/index.html | 17 +- examples/webpack/index.html | 18 +- extensions/chromium/options/options.html | 312 +++++----- external/builder/builder.mjs | 4 +- gulpfile.mjs | 5 +- test/font/font_test.html | 37 +- test/resources/reftest-analyzer.html | 263 ++++----- test/test_slave.html | 18 +- test/unit/unit_test.html | 93 ++- web/viewer-geckoview.html | 61 +- web/viewer-snippet-chrome-extension.html | 4 +- web/viewer-snippet-chrome-overlays.html | 16 +- web/viewer-snippet.html | 2 +- web/viewer.html | 687 +++++++++++++++++----- 23 files changed, 1306 insertions(+), 948 deletions(-) diff --git a/.prettierrc b/.prettierrc index 3455ffe60..49e6b8e7e 100644 --- a/.prettierrc +++ b/.prettierrc @@ -9,10 +9,17 @@ "overrides": [ { - files: ["tsconfig.json"], - options: { - parser: "json", - }, + "files": ["tsconfig.json", ".prettierrc"], + "options": { + "parser": "json" + } }, + { + "files": ["**/*.html"], + "options": { + "parser": "html", + "printWidth": 160 + } + } ] } diff --git a/examples/components/pageviewer.html b/examples/components/pageviewer.html index 267f8cc29..76f7e3684 100644 --- a/examples/components/pageviewer.html +++ b/examples/components/pageviewer.html @@ -1,4 +1,4 @@ - + - - - - - PDF.js page viewer using built components + + + + + PDF.js page viewer using built components - + - + - - - + + + - -
    + +
    - - + + diff --git a/examples/components/simpleviewer.html b/examples/components/simpleviewer.html index e6493263d..8504062d5 100644 --- a/examples/components/simpleviewer.html +++ b/examples/components/simpleviewer.html @@ -1,4 +1,4 @@ - + - - - - - PDF.js viewer using built components + + + + + PDF.js viewer using built components - + - + - - - + + + - -
    -
    -
    + +
    +
    +
    - - + + diff --git a/examples/components/singlepageviewer.html b/examples/components/singlepageviewer.html index 3636dbe77..3e71aad80 100644 --- a/examples/components/singlepageviewer.html +++ b/examples/components/singlepageviewer.html @@ -1,4 +1,4 @@ - + - - - - - PDF.js Single Page Viewer using built components + + + + + PDF.js Single Page Viewer using built components - + - + - - - + + + - -
    -
    -
    + +
    +
    +
    - - + + diff --git a/examples/image_decoders/jpeg_viewer.html b/examples/image_decoders/jpeg_viewer.html index 87f757bd6..a120efedd 100644 --- a/examples/image_decoders/jpeg_viewer.html +++ b/examples/image_decoders/jpeg_viewer.html @@ -1,4 +1,4 @@ - + - - - - - PDF.js standalone JpegImage parser + + + + + PDF.js standalone JpegImage parser - + - - + + - - + + - - + + diff --git a/examples/learning/helloworld.html b/examples/learning/helloworld.html index 6a74298f8..028fb015f 100644 --- a/examples/learning/helloworld.html +++ b/examples/learning/helloworld.html @@ -1,76 +1,71 @@ - + - - - 'Hello, world!' example - - + + + 'Hello, world!' example + + +

    'Hello, world!' example

    -

    'Hello, world!' example

    + - + - + - // - // Render PDF page into canvas context - // - const renderContext = { - canvasContext: context, - transform, - viewport, - }; - page.render(renderContext); - - -
    -

    JavaScript code:

    -
    
    -
    -
    +    
    +

    JavaScript code:

    +
    
    +    
    +  
     
    diff --git a/examples/learning/helloworld64.html b/examples/learning/helloworld64.html
    index ed98e189f..5b833a2db 100644
    --- a/examples/learning/helloworld64.html
    +++ b/examples/learning/helloworld64.html
    @@ -1,81 +1,77 @@
    -
    +
     
    -
    -  
    -  'Hello, world!' base64 example
    -
    -
    +  
    +    
    +    'Hello, world!' base64 example
    +  
    +  
    +    

    'Hello, world!' example

    -

    'Hello, world!' example

    + - + - + - // Render PDF page into canvas context. - var renderContext = { - canvasContext: context, - transform, - viewport, - }; - page.render(renderContext); - - -
    -

    JavaScript code:

    -
    
    -
    -
    +    
    +

    JavaScript code:

    +
    
    +    
    +  
     
    diff --git a/examples/learning/prevnext.html b/examples/learning/prevnext.html
    index e1043bf1e..5249d32aa 100644
    --- a/examples/learning/prevnext.html
    +++ b/examples/learning/prevnext.html
    @@ -1,139 +1,134 @@
    -
    +
     
    -
    -  
    -  Previous/Next example
    -
    -
    +  
    +    
    +    Previous/Next example
    +  
    +  
    +    

    'Previous/Next' example

    -

    'Previous/Next' example

    +
    + + +     + Page: / +
    -
    - - -     - Page: / -
    +
    + +
    -
    - -
    + - + - - + // Initial/first page rendering + renderPage(pageNum); + + diff --git a/examples/mobile-viewer/viewer.html b/examples/mobile-viewer/viewer.html index 6bd8b5406..1812db948 100644 --- a/examples/mobile-viewer/viewer.html +++ b/examples/mobile-viewer/viewer.html @@ -1,4 +1,4 @@ - + - - + + PDF.js viewer - - + + @@ -46,12 +46,12 @@ limitations under the License. - + - + diff --git a/examples/text-only/index.html b/examples/text-only/index.html index 2acbd553e..410307cc8 100644 --- a/examples/text-only/index.html +++ b/examples/text-only/index.html @@ -1,14 +1,13 @@ - + - - + + Text-only PDF.js example - - -

    Text-only PDF.js example

    -
    -
    - + + +

    Text-only PDF.js example

    +
    + diff --git a/examples/webpack/index.html b/examples/webpack/index.html index ed25387f2..a9ba7bd12 100644 --- a/examples/webpack/index.html +++ b/examples/webpack/index.html @@ -1,11 +1,11 @@ - + - - - webpack example - - - - - + + + webpack example + + + + + diff --git a/extensions/chromium/options/options.html b/extensions/chromium/options/options.html index bd18c2456..e83db6c62 100644 --- a/extensions/chromium/options/options.html +++ b/extensions/chromium/options/options.html @@ -15,171 +15,171 @@ See the License for the specific language governing permissions and limitations under the License. --> - - -PDF.js viewer options - - - -
    - + + + PDF.js viewer options + + + +
    + - + - + - + - + - + - + - + - + - + - + - - + + diff --git a/external/builder/builder.mjs b/external/builder/builder.mjs index 929a9cf87..b81ee107d 100644 --- a/external/builder/builder.mjs +++ b/external/builder/builder.mjs @@ -151,7 +151,7 @@ function preprocess(inFilename, outFilename, defines) { let state = STATE_NONE; const stack = []; const control = - /^(?:\/\/|\s*\/\*|)?$)?/; + /^(?:\/\/|\s*\/\*|\s*)?$)?/; while ((line = readLine()) !== null) { ++lineNumber; @@ -213,7 +213,7 @@ function preprocess(inFilename, outFilename, defines) { ) { writeLine( line - .replaceAll(/^\/\/|^$/g, "") ); diff --git a/gulpfile.mjs b/gulpfile.mjs index 196f5f214..9717a334a 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -2023,7 +2023,7 @@ gulp.task( gulp.task("lint", function (done) { console.log(); - console.log("### Linting JS/CSS/JSON/SVG files"); + console.log("### Linting JS/CSS/JSON/SVG/HTML files"); // Ensure that we lint the Firefox specific *.jsm files too. const esLintOptions = [ @@ -2047,9 +2047,10 @@ gulp.task("lint", function (done) { const prettierOptions = [ "node_modules/prettier/bin/prettier.cjs", "**/*.json", + "**/*.html", ]; if (process.argv.includes("--fix")) { - prettierOptions.push("--log-level", "silent", "--write"); + prettierOptions.push("--log-level", "error", "--write"); } else { prettierOptions.push("--log-level", "warn", "--check"); } diff --git a/test/font/font_test.html b/test/font/font_test.html index c8b09b700..4b8d5abcd 100644 --- a/test/font/font_test.html +++ b/test/font/font_test.html @@ -1,25 +1,24 @@ - + - - PDF.js font tests + + PDF.js font tests - + - - + + - - - - - + + + + diff --git a/test/resources/reftest-analyzer.html b/test/resources/reftest-analyzer.html index 00f9eaa0d..2dcc77421 100644 --- a/test/resources/reftest-analyzer.html +++ b/test/resources/reftest-analyzer.html @@ -1,4 +1,4 @@ - + - - Reftest analyzer - - - - - -
    -

    Reftest analyzer

    -

    - Paste your log into this textarea:
    -
    - -

    -

    -
    ...or load it from a file:
    - -

    -
    -
    Loading log...
    -
    -
    -
    - - - - - - - - - - - - - - - - - -
    Pixel at:
    Test:
    Reference:
    -
    -
    ? -
    -

    Move the mouse over the reftest image on the right to show - magnified pixels on the left. The color information above is for - the pixel centered in the magnified view.

    -

    The test is shown in the upper triangle of each pixel and - the reference is shown in the lower triangle.

    + + Reftest analyzer + + + + + +
    +

    Reftest analyzer

    +

    + Paste your log into this textarea:
    +
    + +

    +

    +
    ...or load it from a file:
    + +

    +
    +
    Loading log...
    +
    +
    +
    + + + + + + + + + + + + + + + + + +
    Pixel at:
    Test:
    Reference:
    +
    +
    + ? +
    +

    + Move the mouse over the reftest image on the right to show magnified pixels on the left. The color information above is for the pixel centered + in the magnified view. +

    +

    The test is shown in the upper triangle of each pixel and the reference is shown in the lower triangle.

    +
    +
    + + + +
    -
    - - - +
    +
    -
    -
    -
    -
    -
    -
    - - - - Shortcuts: n=next p=previous t=toggle d=differences -
    -
    - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - + + + + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
    +
    -
    - + diff --git a/test/test_slave.html b/test/test_slave.html index 8d202ce37..b8eb0beea 100644 --- a/test/test_slave.html +++ b/test/test_slave.html @@ -1,4 +1,4 @@ - + - - + + PDF.js viewer - - - + + + - - - - - - + + + + + + - - - + + + - + - - - - + + +
    -
    -
    +
    +
    @@ -119,16 +117,21 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    - - + +
    -
    - -
    +
    + +
    +
    diff --git a/web/viewer-snippet-chrome-extension.html b/web/viewer-snippet-chrome-extension.html index 5c8b404c8..3187cd495 100644 --- a/web/viewer-snippet-chrome-extension.html +++ b/web/viewer-snippet-chrome-extension.html @@ -1,4 +1,4 @@ - - + + diff --git a/web/viewer-snippet-chrome-overlays.html b/web/viewer-snippet-chrome-overlays.html index 1089963d1..fff0eb2f3 100644 --- a/web/viewer-snippet-chrome-overlays.html +++ b/web/viewer-snippet-chrome-overlays.html @@ -4,7 +4,9 @@ users with recognizing which checkbox they have to click when they visit chrome://extensions. --> -

    - Click on - "Allow access to file URLs" - at + word-break: break-all; + " + > + Click on "Allow access to file URLs" at chrome://extensions -
    +
    to view this PDF file.

    or select the file again: - +

    diff --git a/web/viewer-snippet.html b/web/viewer-snippet.html index 3baf94773..15d0d71a4 100644 --- a/web/viewer-snippet.html +++ b/web/viewer-snippet.html @@ -1,3 +1,3 @@ - + diff --git a/web/viewer.html b/web/viewer.html index 1ee1a4c4e..e56f02441 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -1,4 +1,4 @@ -๏ปฟ +๏ปฟ - - - - - + + + + + PDF.js viewer - - - - - - - + + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - + @@ -105,17 +105,53 @@ See https://github.com/adobe-type-tools/cmap-resources
    - - - -
    @@ -124,42 +160,63 @@ See https://github.com/adobe-type-tools/cmap-resources
    -
    -
    -
    - - - +
    + + +
    - + +
    -
    - +
    - +
    @@ -231,7 +298,14 @@ See https://github.com/adobe-type-tools/cmap-resources - + @@ -246,7 +320,17 @@ See https://github.com/adobe-type-tools/cmap-resources
    -
    - +
    - +
    - +
    -
    -
    - +
    -
    -
    +
    @@ -495,7 +757,8 @@ See https://github.com/adobe-type-tools/cmap-resources
    -
    +
    +
    @@ -503,7 +766,7 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    @@ -583,7 +846,7 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    @@ -597,7 +860,7 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    @@ -606,7 +869,9 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    @@ -621,17 +886,43 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    -
    +
    +
    + + +
    +
    - +
    @@ -640,15 +931,23 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    - +
    - - - + + +
    @@ -667,7 +966,15 @@ See https://github.com/adobe-type-tools/cmap-resources
    - + +
    @@ -677,8 +984,12 @@ See https://github.com/adobe-type-tools/cmap-resources
    - - + + @@ -696,7 +1007,9 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    @@ -708,13 +1021,37 @@ See https://github.com/adobe-type-tools/cmap-resources
    - - - + + +
    - +
    @@ -722,7 +1059,17 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    @@ -733,7 +1080,7 @@ See https://github.com/adobe-type-tools/cmap-resources - +
    @@ -741,14 +1088,16 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    - +
    - + @@ -760,58 +1109,68 @@ See https://github.com/adobe-type-tools/cmap-resources
    - +
    - - + +
    - + - -
    -
    - -
    -
    -
    - - - - - + +
    +
    + +
    +
    +
    + + + + + +
    + +
    +
    + +
    -
    -
    - - -
    -
    -
    + - -
    -
    - + +
    +
    + +
    + +
    + + +
    - -
    - - -
    -
    -
    + - - + +
    @@ -823,11 +1182,12 @@ See https://github.com/adobe-type-tools/cmap-resources
    - - - - - + + + + + + - - + + + +
    From 447aab7fe68d7a81aad5e1ec8258699324dc6fc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:43:29 +0000 Subject: [PATCH 24/26] Bump glob Bumps and [glob](https://github.com/isaacs/node-glob). These dependencies needed to be updated together. Updates `glob` from 10.4.5 to 10.5.0 - [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md) - [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0) Updates `glob` from 11.0.3 to 11.1.0 - [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md) - [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0) --- updated-dependencies: - dependency-name: glob dependency-version: 10.5.0 dependency-type: indirect - dependency-name: glob dependency-version: 11.1.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 48 ++++++++++++++--------------------------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f01d97e4..3e11d780e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,7 +98,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1649,7 +1648,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1673,7 +1671,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2693,7 +2690,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2751,7 +2747,6 @@ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -3165,7 +3160,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3772,8 +3766,7 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/bare-fs": { "version": "4.5.0", @@ -4009,7 +4002,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -4898,8 +4890,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dir-glob": { "version": "3.0.1", @@ -5380,7 +5371,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5441,7 +5431,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6417,9 +6406,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -6699,7 +6688,6 @@ "integrity": "sha512-PErok3DZSA5WGMd6XXV3IRNO0mlB+wW3OzhFJLEec1jSERg2j1bxJ6e5Fh6N6fn3FH2T9AP4UYNb/pYlADB9sA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob-watcher": "^6.0.0", "gulp-cli": "^3.1.0", @@ -8386,7 +8374,6 @@ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -8509,7 +8496,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^3.6.0", "commander": "^10.0.1", @@ -9429,7 +9415,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9589,7 +9574,6 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -9621,7 +9605,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10291,7 +10274,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11317,15 +11299,15 @@ "license": "BSD-2-Clause" }, "node_modules/svglint/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -11382,11 +11364,11 @@ } }, "node_modules/svglint/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -11898,7 +11880,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12288,7 +12269,6 @@ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", From b392cbf3c428567dcf8ee3e4dac66bb23008a2a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 19:46:50 +0000 Subject: [PATCH 25/26] Bump js-yaml from 3.14.1 to 3.14.2 Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 3.14.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e11d780e..4ce9f0353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6669,9 +6669,9 @@ } }, "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -7929,9 +7929,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { From 7743d1159498a76ca003ca24ee3343bd2d5d5184 Mon Sep 17 00:00:00 2001 From: Aditi Date: Fri, 7 Nov 2025 01:02:44 +0530 Subject: [PATCH 26/26] Add setter for some FontFaceObject properties --- src/display/font_loader.js | 8 ++++++++ test/pdfs/issue20426.pdf.link | 1 + test/test_manifest.json | 8 ++++++++ 3 files changed, 17 insertions(+) create mode 100644 test/pdfs/issue20426.pdf.link diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 9e6fa1e34..7d8f01a9f 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -457,6 +457,10 @@ class FontFaceObject { return this.#fontData.disableFontFace ?? false; } + set disableFontFace(value) { + shadow(this, "disableFontFace", !!value); + } + get fontExtraProperties() { return this.#fontData.fontExtraProperties ?? false; } @@ -501,6 +505,10 @@ class FontFaceObject { return this.#fontData.bbox; } + set bbox(bbox) { + shadow(this, "bbox", bbox); + } + get fontMatrix() { return this.#fontData.fontMatrix; } diff --git a/test/pdfs/issue20426.pdf.link b/test/pdfs/issue20426.pdf.link new file mode 100644 index 000000000..6db965138 --- /dev/null +++ b/test/pdfs/issue20426.pdf.link @@ -0,0 +1 @@ +https://github.com/user-attachments/files/23383534/test.1.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index 03d1f1d71..a57924e9a 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1948,6 +1948,14 @@ "type": "eq", "forms": true }, + { + "id": "issue20426", + "file": "pdfs/issue20426.pdf", + "md5": "b9a753df595f1dd30505a67c96373dd8", + "link": true, + "rounds": 1, + "type": "eq" + }, { "id": "issue13845", "file": "pdfs/issue13845.pdf",