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/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 = 更新 diff --git a/package-lock.json b/package-lock.json index e2b4137b9..4ce9f0353 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", @@ -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" } @@ -1895,22 +1892,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 +1955,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 +1978,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 +2544,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": { @@ -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" @@ -2765,9 +2760,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": { @@ -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", @@ -3847,9 +3840,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, @@ -4009,7 +4002,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -4174,9 +4166,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,12 +4886,11 @@ } }, "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 + "license": "BSD-3-Clause" }, "node_modules/dir-glob": { "version": "3.0.1", @@ -5375,21 +5366,20 @@ } }, "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, "dependencies": { "@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", @@ -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": { @@ -6563,9 +6552,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": { @@ -6680,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": { @@ -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", @@ -7941,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": { @@ -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", @@ -8529,15 +8515,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 +8533,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" }, @@ -9429,7 +9415,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9466,9 +9451,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": { @@ -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" }, @@ -9748,18 +9731,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 +9753,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" @@ -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", 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", 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." } diff --git a/src/core/catalog.js b/src/core/catalog.js index e5946c50f..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 { @@ -735,6 +739,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 +763,8 @@ class Catalog { } #readPageLabels() { - const obj = this.#catDict.getRaw("PageLabels"); - if (!obj) { + const nums = this.rawPageLabels; + if (!nums) { return null; } @@ -758,8 +772,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/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 a3892751f..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 ); } @@ -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/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js new file mode 100644 index 000000000..2f9d003e3 --- /dev/null +++ b/src/core/editor/pdf_editor.js @@ -0,0 +1,1600 @@ +/* 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, 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"; + +const MAX_LEAVES_PER_PAGES_NODE = 16; +const MAX_IN_NAME_TREE_NODE = 64; + +class PageData { + constructor(page, documentData) { + 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); + } +} + +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(); + 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 = []; + } +} + +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; + this.currentDocument = null; + 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; + [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; + 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 = []; + } + + /** + * 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; + } + + 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. + * @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; + } + 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(oldRef, newRef); + + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + if ( + obj instanceof Dict && + isName(obj.get("Type"), "Page") && + !this.currentDocument.pagesMap.has(oldRef) + ) { + 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 = []; + 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) + ) + ); + } + await Promise.all(promises); + return obj; + } + 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; + } + 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) + ) + ); + } + await Promise.all(promises); + } + + 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 + * @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; + 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 || []) { + 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; + + this.#collectValidDestinations(allDocumentData); + this.#collectPageLabels(); + + for (const page of this.oldPages) { + promises.push(this.#postCollectPageData(page)); + } + 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); + await this.#mergeStructTrees(allDocumentData); + + return this.writePDF(); + } + + /** + * Collect the document data. + * @param {DocumentData} documentData + * @return {Promise} + */ + async #collectDocumentData(documentData) { + const { + document: { pdfManager, xref }, + } = documentData; + await Promise.all([ + pdfManager + .ensureCatalog("destinations") + .then(destinations => (documentData.destinations = destinations)), + 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; + } + } + + /** + * Post process the collected page data. + * @param {PageData} pageData + * @returns {Promise} + */ + async #postCollectPageData(pageData) { + const { + page: { xref, annotations }, + documentData: { pagesMap, destinations, usedNamedDestinations }, + } = pageData; + + if (!annotations) { + return; + } + + const promises = []; + let newAnnotations = []; + let newIndex = 0; + + // 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(); + } + } + + #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). + * @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. + 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 = this.cloneDict(currentLabel); + 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 + * @returns {Promise} the page reference in the new PDF document. + */ + async #makePageCopy(pageIndex) { + const { page, documentData, annotations, pointingNamedDestinations } = + this.oldPages[pageIndex]; + this.currentDocument = documentData; + const { dedupNamedDestinations, oldRefMapping } = documentData; + const { xref, rotate, mediaBox, resources, ref: oldPageRef } = page; + const pageRef = this.newRef; + const pageDict = (this.xref[pageRef.num] = this.cloneDict(page.pageDict)); + 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", + "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) + ); + + 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; + 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 a name or number tree from the given map. + * @param {Array<[string|number, 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); + } + + #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 + ) + ); + } + + #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} + */ + async #makeRoot() { + const { rootDict } = this; + rootDict.setIfName("Type", "Catalog"); + rootDict.setIfName("Version", this.version); + this.#makePageTree(); + this.#makePageLabelsTree(); + this.#makeDestinationsTree(); + this.#makeStructTree(); + } + + /** + * 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/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/src/core/primitives.js b/src/core/primitives.js index decd4338c..854341dbd 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); } @@ -429,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/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/struct_tree.js b/src/core/struct_tree.js index 539ae008f..cdd12dfa8 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"; @@ -37,6 +42,7 @@ class StructTreeRoot { this.roleMap = new Map(); this.structParentIds = null; this.kidRefToPosition = undefined; + this.parentTree = null; } getKidPosition(kidRef) { @@ -65,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) { @@ -610,7 +621,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) { @@ -765,7 +777,7 @@ class StructTreePage { return; } - const parentTree = this.rootDict.get("ParentTree"); + const { parentTree } = this.root; if (!parentTree) { return; } @@ -776,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) { @@ -793,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 ( @@ -824,6 +835,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"); diff --git a/src/core/worker.js b/src/core/worker.js index 578ea2bdb..1b6accb85 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"; @@ -514,6 +515,7 @@ class WorkerMessageHandler { return Promise.all([ pdfManager.ensureDoc("documentInfo"), pdfManager.ensureCatalog("metadata"), + pdfManager.ensureCatalog("hasStructTree"), ]); }); @@ -557,6 +559,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 bb249baa5..53684f363 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 @@ -2902,6 +2920,10 @@ class WorkerTransport { }); } + extractPages(pageInfos) { + return this.messageHandler.sendWithPromise("ExtractPages", { pageInfos }); + } + getPage(pageNumber) { if ( !Number.isInteger(pageNumber) || @@ -3057,6 +3079,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/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 3eb723729..21023fd0f 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, }; 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/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/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/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/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, diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index ab63991a2..78e0b10cc 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 @@ -752,4 +753,12 @@ !bug1937438_af_from_latex.pdf !bug1937438_from_word.pdf !bug1937438_mml_from_latex.pdf -!marked_content_lang.pdf \ No newline at end of file +!marked_content_lang.pdf +!bug1997343.pdf +!doc_1_3_pages.pdf +!doc_2_3_pages.pdf +!doc_3_3_pages.pdf +!labelled_pages.pdf +!extract_link.pdf +!two_paragraphs.pdf +!paragraph_and_link.pdf diff --git a/test/pdfs/bug1997343.pdf b/test/pdfs/bug1997343.pdf new file mode 100755 index 000000000..378d0fc41 Binary files /dev/null and b/test/pdfs/bug1997343.pdf differ diff --git a/test/pdfs/doc_1_3_pages.pdf b/test/pdfs/doc_1_3_pages.pdf new file mode 100755 index 000000000..f71ed36ab Binary files /dev/null and b/test/pdfs/doc_1_3_pages.pdf differ diff --git a/test/pdfs/doc_2_3_pages.pdf b/test/pdfs/doc_2_3_pages.pdf new file mode 100755 index 000000000..3ccb2f422 Binary files /dev/null and b/test/pdfs/doc_2_3_pages.pdf differ diff --git a/test/pdfs/doc_3_3_pages.pdf b/test/pdfs/doc_3_3_pages.pdf new file mode 100755 index 000000000..63222e899 Binary files /dev/null and b/test/pdfs/doc_3_3_pages.pdf differ diff --git a/test/pdfs/extract_link.pdf b/test/pdfs/extract_link.pdf new file mode 100755 index 000000000..e9e4be432 Binary files /dev/null and b/test/pdfs/extract_link.pdf differ diff --git a/test/pdfs/issue20324.pdf b/test/pdfs/issue20324.pdf new file mode 100644 index 000000000..c5f2a1d4f Binary files /dev/null and b/test/pdfs/issue20324.pdf differ 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/pdfs/labelled_pages.pdf b/test/pdfs/labelled_pages.pdf new file mode 100755 index 000000000..68e389f40 Binary files /dev/null and b/test/pdfs/labelled_pages.pdf differ diff --git a/test/pdfs/paragraph_and_link.pdf b/test/pdfs/paragraph_and_link.pdf new file mode 100755 index 000000000..610abe5b8 Binary files /dev/null and b/test/pdfs/paragraph_and_link.pdf differ diff --git a/test/pdfs/two_paragraphs.pdf b/test/pdfs/two_paragraphs.pdf new file mode 100755 index 000000000..5bfc685a8 Binary files /dev/null and b/test/pdfs/two_paragraphs.pdf differ 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.mjs b/test/test.mjs index c6e432e1f..ee9cf7340 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -672,6 +672,7 @@ function checkRefTestResults(browser, id, results) { case "partial": case "text": case "highlight": + case "extract": checkEq(task, results, browser, session.masterMode); break; case "fbf": @@ -731,6 +732,7 @@ function refTestPostHandler(parsedUrl, req, res) { var snapshot = data.snapshot; var baselineSnapshot = data.baselineSnapshot; var lastPageNum = data.lastPageNum; + var numberOfTasks = data.numberOfTasks; session = getSession(browser); monitorBrowserTimeout(session, handleSessionTimeout); @@ -773,7 +775,10 @@ function refTestPostHandler(parsedUrl, req, res) { }); } - var isDone = taskResults.at(-1)?.[lastPageNum - 1]; + const lastTaskResults = taskResults.at(-1); + const isDone = + lastTaskResults?.[lastPageNum - 1] || + lastTaskResults?.filter(result => !!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..da5707fd0 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", @@ -13049,5 +13057,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/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
- - - - - + + + + + + - - + + + +