Compare commits

..

No commits in common. "master" and "v6.2.108" have entirely different histories.

109 changed files with 1477 additions and 3259 deletions

View File

@ -24,13 +24,13 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 uses: github/codeql-action/init@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4.37.2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: security-and-quality queries: security-and-quality
- name: Autobuild CodeQL - name: Autobuild CodeQL
uses: github/codeql-action/autobuild@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 uses: github/codeql-action/autobuild@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4.37.2
- name: Perform CodeQL analysis - name: Perform CodeQL analysis
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 uses: github/codeql-action/analyze@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4.37.2

View File

@ -30,9 +30,6 @@ latest JavaScript features; please also see [this wiki page](https://github.com/
+ Older browsers: https://mozilla.github.io/pdf.js/legacy/web/viewer.html + Older browsers: https://mozilla.github.io/pdf.js/legacy/web/viewer.html
> [!NOTE]
> Open new files via the menu (the ">>" icon) or by dragging and dropping.
### Browser Extensions ### Browser Extensions
#### Firefox #### Firefox

View File

@ -62,13 +62,6 @@ export default [
rules: { rules: {
...regexpPlugin.configs["flat/recommended"].rules, ...regexpPlugin.configs["flat/recommended"].rules,
"regexp/no-legacy-features": "off", "regexp/no-legacy-features": "off",
"regexp/no-octal": "error",
"regexp/no-potentially-useless-backreference": "error",
"regexp/no-standalone-backslash": "error",
"regexp/no-super-linear-move": "error",
"regexp/optimal-lookaround-quantifier": "error",
"regexp/prefer-escape-replacement-dollar-char": "error",
"regexp/prefer-regexp-exec": "error",
}, },
}, },
{ {

View File

@ -53,7 +53,7 @@ function preprocess(inFilename, outFilename, defines) {
} }
return content.replaceAll( return content.replaceAll(
/^[ \t]*@import\s+url\(([^)]+)\);[ \t]*$/gm, /^\s*@import\s+url\(([^)]+)\);\s*$/gm,
function (all, url) { function (all, url) {
const file = path.join(path.dirname(baseUrl), url); const file = path.join(path.dirname(baseUrl), url);
const imported = fs.readFileSync(file, "utf8").toString(); const imported = fs.readFileSync(file, "utf8").toString();
@ -73,7 +73,10 @@ function preprocess(inFilename, outFilename, defines) {
const out = []; const out = [];
let i = 0; let i = 0;
function readLine() { function readLine() {
return i < totalLines ? lines[i++] : null; if (i < totalLines) {
return lines[i++];
}
return null;
} }
const writeLine = const writeLine =
typeof outFilename === "function" typeof outFilename === "function"
@ -124,7 +127,10 @@ function preprocess(inFilename, outFilename, defines) {
function expand(line) { function expand(line) {
line = line.replaceAll(/__\w+__/g, function (variable) { line = line.replaceAll(/__\w+__/g, function (variable) {
variable = variable.substring(2, variable.length - 2); variable = variable.substring(2, variable.length - 2);
return variable in defines ? defines[variable] : ""; if (variable in defines) {
return defines[variable];
}
return "";
}); });
writeLine(line); writeLine(line);
} }

View File

@ -37,7 +37,7 @@ function parseAdobeCMap(content) {
result.usecmap = m[1]; result.usecmap = m[1];
} }
const re = const re =
/\b(\d+)\s+(begincodespacerange|beginnotdefrange|begincidchar|begincidrange|beginbfchar|beginbfrange)\n([\s\S]*?)\n(?:endcodespacerange|endnotdefrange|endcidchar|endcidrange|endbfchar|endbfrange)/g; /(\d+)\s+(begincodespacerange|beginnotdefrange|begincidchar|begincidrange|beginbfchar|beginbfrange)\n([\s\S]*?)\n(?:endcodespacerange|endnotdefrange|endcidchar|endcidrange|endbfchar|endbfrange)/g;
while ((m = re.exec(body))) { while ((m = re.exec(body))) {
const lines = m[3].toLowerCase().split("\n"); const lines = m[3].toLowerCase().split("\n");

View File

@ -52,61 +52,21 @@ pdfjs-bookmark-button-label = بلگه هیم سکویی
pdfjs-tools-button = pdfjs-tools-button =
.title = ٱوزارا .title = ٱوزارا
pdfjs-tools-button-label = ٱوزارا pdfjs-tools-button-label = ٱوزارا
pdfjs-first-page-button =
.title = رئڌن و بلگه نیایی
pdfjs-first-page-button-label = رئڌن و بلگه نیایی
pdfjs-last-page-button =
.title = رئڌن و بلگه دیندایی
pdfjs-last-page-button-label = رئڌن و بلگه دیندایی
pdfjs-page-rotate-cw-button =
.title = لر خردن ساعتگرد
pdfjs-page-rotate-cw-button-label = لر خردن ساعتگرد
pdfjs-page-rotate-ccw-button =
.title = لر خردن پاد ساعتگرد
pdfjs-page-rotate-ccw-button-label = لر خردن پاد ساعتگرد
pdfjs-cursor-text-select-tool-button =
.title = فعال کردن ٱوزار پسند هؽل
pdfjs-cursor-text-select-tool-button-label = ٱوزار پسند هؽل pdfjs-cursor-text-select-tool-button-label = ٱوزار پسند هؽل
pdfjs-cursor-hand-tool-button = pdfjs-cursor-hand-tool-button =
.title = فعال کردن ٱوزار دست .title = فعال کردن ٱوزار دست
pdfjs-cursor-hand-tool-button-label = ٱوزار دست pdfjs-cursor-hand-tool-button-label = ٱوزار دست
pdfjs-scroll-page-button =
.title = و کار گرؽڌن اسکرۊل بلگه
pdfjs-scroll-page-button-label = اسکرۊل بلگه
pdfjs-scroll-vertical-button =
.title = و کار گرؽڌن اسکرۊل عمۊدی
pdfjs-scroll-vertical-button-label = اسکرۊل عمۊدی
pdfjs-scroll-horizontal-button =
.title = و کار گرؽڌن اسکرۊل اوفوقی
pdfjs-scroll-horizontal-button-label = اسکرۊل اوفوقی
pdfjs-scroll-wrapped-button =
.title = و کار گرؽڌن اسکرۊل پؽچسته
pdfjs-scroll-wrapped-button-label = اسکرۊل پؽچسته
## Document properties dialog ## Document properties dialog
pdfjs-document-properties-button =
.title = خۊسۊسیات سند…
pdfjs-document-properties-button-label = خۊسۊسیات سند…
pdfjs-document-properties-file-name = نوم فایل: pdfjs-document-properties-file-name = نوم فایل:
pdfjs-document-properties-file-size = هندا فایل: pdfjs-document-properties-file-size = هندا فایل:
pdfjs-document-properties-title = عونوان: pdfjs-document-properties-title = عونوان:
pdfjs-document-properties-author = هؽل کوݩ: pdfjs-document-properties-author = هؽل کوݩ:
pdfjs-document-properties-subject = سرتال: pdfjs-document-properties-subject = سرتال:
pdfjs-document-properties-creation-date = تاریخ وورکل وابیڌن:
pdfjs-document-properties-modification-date = تاریخ آلشتکاری:
# Variables:
# $dateObj (Date) - the creation/modification date and time of the PDF file
pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") }
pdfjs-document-properties-creator = وورکل کون:
pdfjs-document-properties-producer = وورکل کون PDF:
pdfjs-document-properties-version = نوسخه PDF:
pdfjs-document-properties-page-count = تئداد بلگه یل:
pdfjs-document-properties-page-size = هندا بلگه: pdfjs-document-properties-page-size = هندا بلگه:
pdfjs-document-properties-page-size-unit-inches = اینچ pdfjs-document-properties-page-size-unit-inches = اینچ
pdfjs-document-properties-page-size-unit-millimeters = میلی متر pdfjs-document-properties-page-size-unit-millimeters = میلی متر
pdfjs-document-properties-page-size-orientation-portrait = portrait
pdfjs-document-properties-page-size-orientation-landscape = landscape
pdfjs-document-properties-page-size-name-a-three = A3 pdfjs-document-properties-page-size-name-a-three = A3
pdfjs-document-properties-page-size-name-a-four = A4 pdfjs-document-properties-page-size-name-a-four = A4
pdfjs-document-properties-page-size-name-letter = نامه pdfjs-document-properties-page-size-name-letter = نامه
@ -124,16 +84,12 @@ pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $hei
## ##
# The linearization status of the document; usually called "Fast Web View" in
# English locales of Adobe software.
pdfjs-document-properties-linearized = نیشتن زل وب:
pdfjs-document-properties-linearized-yes = هری pdfjs-document-properties-linearized-yes = هری
pdfjs-document-properties-linearized-no = ن pdfjs-document-properties-linearized-no = ن
pdfjs-document-properties-close-button = بستن pdfjs-document-properties-close-button = بستن
## Print ## Print
pdfjs-print-progress-message = ٱماڌه کردن سند سی چاپ کردن…
# Variables: # Variables:
# $progress (Number) - percent value # $progress (Number) - percent value
pdfjs-print-progress-percent = { $progress }% pdfjs-print-progress-percent = { $progress }%
@ -143,55 +99,6 @@ pdfjs-print-progress-close-button = لقو
pdfjs-toggle-sidebar-button = pdfjs-toggle-sidebar-button =
.title = آلشت هالت نوار کلی .title = آلشت هالت نوار کلی
pdfjs-toggle-sidebar-button-label = آلشت هالت نوار کلی
pdfjs-document-outline-button-label = تئر سند
pdfjs-attachments-button =
.title = نشووݩ داڌن پیوستا
pdfjs-attachments-button-label = پیوستا
pdfjs-layers-button-label = لایه یل
pdfjs-thumbs-button =
.title = نشووݩ داڌن شؽواتا کۊچیر
pdfjs-thumbs-button-label = شؽواتا کۊچیر
pdfjs-findbar-button =
.title = جوستن من سند
pdfjs-findbar-button-label = جوستن
pdfjs-additional-layers = لایه یل ازافه
## Thumbnails panel item (tooltip and alt text for images)
# Variables:
# $page (Number) - the page number
pdfjs-thumb-page-title =
.title = بلگه { $page }
## Find panel button title and messages
pdfjs-find-previous-button-label = دیندایی
pdfjs-find-next-button-label = بئڌی
pdfjs-find-highlight-checkbox = هایلایت کردن پوی
## Predefined zoom values
pdfjs-page-scale-width = پئنا بلگه
pdfjs-page-scale-fit = هندا کردن بلگه
pdfjs-page-scale-auto = زۊم کردن خوتکار
pdfjs-page-scale-actual = هندا واقعی‌
# Variables:
# $scale (Number) - percent value for page scale
pdfjs-page-scale-percent = { $scale }%
## PDF page
# Variables:
# $page (Number) - the page number
pdfjs-page-landmark =
.aria-label = بلگه { $page }
## Annotations
# Variables:
# $dateObj (Date) - the modification date and time of the annotation
pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") }
## Password ## Password
@ -200,12 +107,6 @@ pdfjs-password-cancel-button = لقو
## Editing ## Editing
pdfjs-editor-free-text-button =
.title = هؽل
pdfjs-editor-free-text-button-label = هؽل
pdfjs-editor-ink-button =
.title = کشیڌن
pdfjs-editor-ink-button-label = کشیڌن
pdfjs-editor-stamp-button = pdfjs-editor-stamp-button =
.title = ٱووردن یا آلشت شؽواتا .title = ٱووردن یا آلشت شؽواتا
pdfjs-editor-stamp-button-label = ٱووردن یا آلشت شؽواتا pdfjs-editor-stamp-button-label = ٱووردن یا آلشت شؽواتا
@ -217,14 +118,6 @@ pdfjs-editor-stamp-editor =
## ##
# Editor Parameters
pdfjs-editor-free-text-color-input = رنگ
pdfjs-editor-free-text-size-input = هندا
pdfjs-editor-ink-color-input = رنگ
pdfjs-editor-ink-thickness-input = کۊلۊفتی
pdfjs-editor-ink-opacity-input = کر بیڌن
# This refers to the thickness of the line used for free highlighting (not bound to text)
pdfjs-editor-free-highlight-thickness-input = کۊلۊفتی
# .default-content is used as a placeholder in an empty text editor. # .default-content is used as a placeholder in an empty text editor.
pdfjs-free-text2 = pdfjs-free-text2 =
.aria-label = آلشتگر هؽل .aria-label = آلشتگر هؽل
@ -234,55 +127,15 @@ pdfjs-editor-comments-sidebar-no-comments-link = قلوه دووسته بۊین
## Alt-text dialog ## Alt-text dialog
pdfjs-editor-alt-text-cancel-button = لقو pdfjs-editor-alt-text-cancel-button = لقو
pdfjs-editor-alt-text-save-button = زفت
## Color picker
pdfjs-editor-colorpicker-yellow =
.title = هیل
pdfjs-editor-colorpicker-green =
.title = ساوز
pdfjs-editor-colorpicker-blue =
.title = کوۊ
pdfjs-editor-colorpicker-pink =
.title = آل
pdfjs-editor-colorpicker-red =
.title = سوئر
## Show all highlights
## This is a toggle button to show/hide all the highlights.
pdfjs-editor-highlight-show-all-button-label = نشووݩ داڌن پوی
pdfjs-editor-highlight-show-all-button =
.title = نشووݩ داڌن پوی
## New alt-text dialog ## New alt-text dialog
## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. ## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy.
pdfjs-editor-new-alt-text-disclaimer-learn-more-url = قلوه دووسته بۊین pdfjs-editor-new-alt-text-disclaimer-learn-more-url = قلوه دووسته بۊین
pdfjs-editor-new-alt-text-not-now-button = سکو ن
pdfjs-editor-new-alt-text-error-close-button = بستن
## "Annotations removed" bar
pdfjs-editor-undo-bar-undo-button-label = وورگندن
pdfjs-editor-undo-bar-close-button =
.title = بستن
pdfjs-editor-undo-bar-close-button-label = بستن
## Tab panels
pdfjs-editor-add-signature-draw-thickness-range-label = کۊلۊفتی
## Controls
pdfjs-editor-add-signature-error-close-button = بستن
## Dialog buttons ## Dialog buttons
pdfjs-editor-add-signature-cancel-button = لقو pdfjs-editor-add-signature-cancel-button = لقو
pdfjs-editor-add-signature-add-button = ٱووردن
pdfjs-editor-edit-signature-update-button = ورۊ رسۊوی
## Comment popup ## Comment popup
@ -294,32 +147,4 @@ pdfjs-editor-edit-comment-popup-button =
# An existing comment is edited # An existing comment is edited
pdfjs-editor-edit-comment-dialog-title-when-editing = آلشت منشڌ pdfjs-editor-edit-comment-dialog-title-when-editing = آلشت منشڌ
pdfjs-editor-edit-comment-dialog-save-button-when-editing = ورۊ رسۊوی
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-cancel-button = لقو
## The view manager is a sidebar displaying different views:
## - thumbnails;
## - outline;
## - attachments;
## - layers.
## The thumbnails view is used to edit the pdf: remove/insert pages, ...
pdfjs-views-manager-sidebar =
.aria-label = نوار کلی
pdfjs-views-manager-layers-option-label = لایه یل
pdfjs-views-manager-pages-status-action-button-label = دؽوۉداری
pdfjs-views-manager-pages-status-copy-button-label = لف گیری
pdfjs-views-manager-pages-status-cut-button-label = بۊریڌن
pdfjs-views-manager-pages-status-delete-button-label = پاک کردن
pdfjs-views-manager-status-undo-button-label = وورگندن
pdfjs-views-manager-status-done-button-label = ٱنجوم وابی
pdfjs-views-manager-status-close-button =
.title = بستن
pdfjs-views-manager-status-close-button-label = بستن
pdfjs-views-manager-paste-button-label = جا وندن
# Badge used to promote a new feature in the UI, keep it as short as possible.
# It's spelled uppercase for English, but it can be translated as usual.
pdfjs-new-badge-content = نۊ

View File

@ -67,8 +67,8 @@ pdfjs-page-rotate-cw-button =
.title = Im Uhrzeigersinn drehen .title = Im Uhrzeigersinn drehen
pdfjs-page-rotate-cw-button-label = Im Uhrzeigersinn drehen pdfjs-page-rotate-cw-button-label = Im Uhrzeigersinn drehen
pdfjs-page-rotate-ccw-button = pdfjs-page-rotate-ccw-button =
.title = Gegen den Uhrzeigersinn drehen .title = Gegen Uhrzeigersinn drehen
pdfjs-page-rotate-ccw-button-label = Gegen den Uhrzeigersinn drehen pdfjs-page-rotate-ccw-button-label = Gegen Uhrzeigersinn drehen
pdfjs-cursor-text-select-tool-button = pdfjs-cursor-text-select-tool-button =
.title = Textauswahl-Werkzeug aktivieren .title = Textauswahl-Werkzeug aktivieren
pdfjs-cursor-text-select-tool-button-label = Textauswahl-Werkzeug pdfjs-cursor-text-select-tool-button-label = Textauswahl-Werkzeug
@ -299,7 +299,7 @@ pdfjs-rendering-error = Beim Darstellen der Seite trat ein Fehler auf.
# (32000-1:2008 Table 169 Annotation types). # (32000-1:2008 Table 169 Annotation types).
# Some common types are e.g.: "Check", "Text", "Comment", "Note" # Some common types are e.g.: "Check", "Text", "Comment", "Note"
pdfjs-text-annotation-type = pdfjs-text-annotation-type =
.alt = [{ $type } Anmerkung] .alt = [Anlage: { $type }]
# Variables: # Variables:
# $dateObj (Date) - the modification date and time of the annotation # $dateObj (Date) - the modification date and time of the annotation
pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") }
@ -626,7 +626,7 @@ pdfjs-editor-add-signature-save-checkbox = Unterschrift speichern
pdfjs-editor-add-signature-save-warning-message = Sie haben die Grenze von 5 gespeicherten Unterschriften erreicht. Entfernen Sie eine, um weitere zu speichern. pdfjs-editor-add-signature-save-warning-message = Sie haben die Grenze von 5 gespeicherten Unterschriften erreicht. Entfernen Sie eine, um weitere zu speichern.
pdfjs-editor-add-signature-image-upload-error-title = Grafik konnte nicht hochgeladen werden pdfjs-editor-add-signature-image-upload-error-title = Grafik konnte nicht hochgeladen werden
pdfjs-editor-add-signature-image-upload-error-description = Überprüfen Sie Ihre Netzwerkverbindung, oder versuchen Sie es mit einer anderen Grafik. pdfjs-editor-add-signature-image-upload-error-description = Überprüfen Sie Ihre Netzwerkverbindung, oder versuchen Sie es mit einer anderen Grafik.
pdfjs-editor-add-signature-image-no-data-error-title = Kann Grafik nicht in eine Unterschrift umwandeln pdfjs-editor-add-signature-image-no-data-error-title = Kann Grafik nicht in eine Signatur umwandeln
pdfjs-editor-add-signature-image-no-data-error-description = Bitte versuchen Sie, eine andere Grafik hochzuladen. pdfjs-editor-add-signature-image-no-data-error-description = Bitte versuchen Sie, eine andere Grafik hochzuladen.
pdfjs-editor-add-signature-error-close-button = Schließen pdfjs-editor-add-signature-error-close-button = Schließen
@ -730,9 +730,9 @@ pdfjs-views-manager-pages-status-undo-delete-label =
} }
pdfjs-views-manager-pages-status-waiting-ready-label = Ihre Datei wird vorbereitet… pdfjs-views-manager-pages-status-waiting-ready-label = Ihre Datei wird vorbereitet…
pdfjs-views-manager-pages-status-waiting-uploading-label = Datei wird hochgeladen… pdfjs-views-manager-pages-status-waiting-uploading-label = Datei wird hochgeladen…
pdfjs-views-manager-status-warning-cut-label = Ausschneiden nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut. pdfjs-views-manager-status-warning-cut-label = Ausschneiden war nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-warning-copy-label = Kopieren nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut. pdfjs-views-manager-status-warning-copy-label = Kopieren nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-warning-delete-label = Löschen nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut. pdfjs-views-manager-status-warning-delete-label = Löschen war nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-warning-save-label = Speichern nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut. pdfjs-views-manager-status-warning-save-label = Speichern nicht möglich. Aktualisieren Sie die Seite und versuchen Sie es erneut.
pdfjs-views-manager-status-undo-button-label = Rückgängig pdfjs-views-manager-status-undo-button-label = Rückgängig
pdfjs-views-manager-status-done-button-label = Fertig pdfjs-views-manager-status-done-button-label = Fertig
@ -824,8 +824,8 @@ pdfjs-digital-signature-properties-certificate-revoked = Zertifikat: Widerrufen
## Main menu for adding/removing signatures ## Main menu for adding/removing signatures
pdfjs-editor-delete-signature-button1 = pdfjs-editor-delete-signature-button1 =
.title = Gespeicherte Unterschrift entfernen .title = Gespeicherte Signatur entfernen
pdfjs-editor-delete-signature-button-label1 = Gespeicherte Unterschrift entfernen pdfjs-editor-delete-signature-button-label1 = Gespeicherte Signatur entfernen
## Editor toolbar ## Editor toolbar

View File

@ -162,8 +162,8 @@ pdfjs-print-progress-message = Preparando documento para impresión…
# $progress (Number) - percent value # $progress (Number) - percent value
pdfjs-print-progress-percent = { $progress }% pdfjs-print-progress-percent = { $progress }%
pdfjs-print-progress-close-button = Cancelar pdfjs-print-progress-close-button = Cancelar
pdfjs-printing-not-supported = Advertencia: La impresión no está completamente soportada por este navegador. pdfjs-printing-not-supported = Advertencia: La impresión no esta completamente soportada por este navegador.
pdfjs-printing-not-ready = Advertencia: El PDF no cargó completamente para impresión. pdfjs-printing-not-ready = Advertencia: El PDF no cargo completamente para impresión.
## Tooltips and alt text for side panel toolbar buttons ## Tooltips and alt text for side panel toolbar buttons
@ -266,7 +266,7 @@ pdfjs-page-landmark =
## Loading indicator messages ## Loading indicator messages
pdfjs-loading-error = Un error ocurrió al cargar el PDF. pdfjs-loading-error = Un error ocurrió al cargar el PDF.
pdfjs-invalid-file-error = Archivo PDF inválido o dañado. pdfjs-invalid-file-error = Archivo PDF invalido o dañado.
pdfjs-missing-file-error = Archivo PDF no encontrado. pdfjs-missing-file-error = Archivo PDF no encontrado.
pdfjs-unexpected-response-error = Respuesta inesperada del servidor. pdfjs-unexpected-response-error = Respuesta inesperada del servidor.
pdfjs-rendering-error = Un error ocurrió al renderizar la página. pdfjs-rendering-error = Un error ocurrió al renderizar la página.
@ -361,7 +361,7 @@ pdfjs-editor-remove-signature-button =
pdfjs-editor-free-text-color-input = Color pdfjs-editor-free-text-color-input = Color
pdfjs-editor-free-text-size-input = Tamaño pdfjs-editor-free-text-size-input = Tamaño
pdfjs-editor-ink-color-input = Color pdfjs-editor-ink-color-input = Color
pdfjs-editor-ink-thickness-input = Grosor pdfjs-editor-ink-thickness-input = Grossor
pdfjs-editor-ink-opacity-input = Opacidad pdfjs-editor-ink-opacity-input = Opacidad
pdfjs-editor-stamp-add-image-button = pdfjs-editor-stamp-add-image-button =
.title = Agregar imagen .title = Agregar imagen
@ -581,7 +581,7 @@ pdfjs-editor-add-signature-type-input =
.aria-label = Escribe tu firma .aria-label = Escribe tu firma
.placeholder = Escribe tu firma .placeholder = Escribe tu firma
pdfjs-editor-add-signature-draw-placeholder = Dibuja tu firma pdfjs-editor-add-signature-draw-placeholder = Dibuja tu firma
pdfjs-editor-add-signature-draw-thickness-range-label = Grosor pdfjs-editor-add-signature-draw-thickness-range-label = Grossor
# Variables: # Variables:
# $thickness (Number) - the thickness (in pixels) of the line used to draw a signature. # $thickness (Number) - the thickness (in pixels) of the line used to draw a signature.
pdfjs-editor-add-signature-draw-thickness-range = pdfjs-editor-add-signature-draw-thickness-range =
@ -698,15 +698,15 @@ pdfjs-views-manager-status-undo-cut-label =
# $count (Number) - the number of selected pages to be copied. # $count (Number) - the number of selected pages to be copied.
pdfjs-views-manager-pages-status-undo-copy-label = pdfjs-views-manager-pages-status-undo-copy-label =
{ $count -> { $count ->
[one] 1 página copiada [one] 1 pagina copiada
*[other] { $count } páginas copiadas *[other] { $count } paginas copiadas
} }
# Variables: # Variables:
# $count (Number) - the number of selected pages to be deleted. # $count (Number) - the number of selected pages to be deleted.
pdfjs-views-manager-pages-status-undo-delete-label = pdfjs-views-manager-pages-status-undo-delete-label =
{ $count -> { $count ->
[one] 1 página eliminada [one] 1 pagina eliminada
*[other] { $count } páginas eliminadas *[other] { $count } paginas eliminadas
} }
pdfjs-views-manager-pages-status-waiting-ready-label = Preparando tu archivo… pdfjs-views-manager-pages-status-waiting-ready-label = Preparando tu archivo…
pdfjs-views-manager-pages-status-waiting-uploading-label = Subiendo archivo… pdfjs-views-manager-pages-status-waiting-uploading-label = Subiendo archivo…

View File

@ -422,11 +422,11 @@ pdfjs-editor-comments-sidebar-no-comments-link = Argibide gehiago
## Alt-text dialog ## Alt-text dialog
pdfjs-editor-alt-text-button-label = Ordezko testua pdfjs-editor-alt-text-button-label = Testu alternatiboa
pdfjs-editor-alt-text-edit-button = pdfjs-editor-alt-text-edit-button =
.aria-label = Editatu ordezko testua .aria-label = Editatu testu alternatiboa
pdfjs-editor-alt-text-dialog-label = Aukeratu aukera pdfjs-editor-alt-text-dialog-label = Aukeratu aukera
pdfjs-editor-alt-text-dialog-description = Ordezko testuak laguntzen du jendeak ezin duenean irudia ikusi edo ez denean kargatzen. pdfjs-editor-alt-text-dialog-description = Testu alternatiboak laguntzen du jendeak ezin duenean irudia ikusi edo ez denean kargatzen.
pdfjs-editor-alt-text-add-description-label = Gehitu azalpena pdfjs-editor-alt-text-add-description-label = Gehitu azalpena
pdfjs-editor-alt-text-add-description-description = Saiatu idazten gaia, ezarpena edo ekintzak deskribatzen dituen esaldi 1 edo 2. pdfjs-editor-alt-text-add-description-description = Saiatu idazten gaia, ezarpena edo ekintzak deskribatzen dituen esaldi 1 edo 2.
pdfjs-editor-alt-text-mark-decorative-label = Markatu apaingarri gisa pdfjs-editor-alt-text-mark-decorative-label = Markatu apaingarri gisa
@ -439,7 +439,7 @@ pdfjs-editor-alt-text-textarea =
.placeholder = Adibidez, "gizon gaztea mahaian eserita dago bazkaltzeko" .placeholder = Adibidez, "gizon gaztea mahaian eserita dago bazkaltzeko"
# Alternative text (alt text) helps when people can't see the image. # Alternative text (alt text) helps when people can't see the image.
pdfjs-editor-alt-text-button = pdfjs-editor-alt-text-button =
.aria-label = Ordezko testua .aria-label = Testu alternatiboa
## Editor resizers ## Editor resizers
## This is used in an aria label to help to understand the role of the resizer. ## This is used in an aria label to help to understand the role of the resizer.
@ -491,38 +491,38 @@ pdfjs-editor-highlight-show-all-button =
## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. ## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy.
# Modal header positioned above a text box where users can edit the alt text. # Modal header positioned above a text box where users can edit the alt text.
pdfjs-editor-new-alt-text-dialog-edit-label = Editatu ordezko testua (irudiaren azalpena) pdfjs-editor-new-alt-text-dialog-edit-label = Editatu testu alternatiboa (irudiaren azalpena)
# Modal header positioned above a text box where users can add the alt text. # Modal header positioned above a text box where users can add the alt text.
pdfjs-editor-new-alt-text-dialog-add-label = Gehitu ordezko testua (irudiaren azalpena) pdfjs-editor-new-alt-text-dialog-add-label = Gehitu testu alternatiboa (irudiaren azalpena)
pdfjs-editor-new-alt-text-textarea = pdfjs-editor-new-alt-text-textarea =
.placeholder = Idatzi zure azalpena hemen… .placeholder = Idatzi zure azalpena hemen…
# This text refers to the alt text box above this description. It offers a definition of alt text. # This text refers to the alt text box above this description. It offers a definition of alt text.
pdfjs-editor-new-alt-text-description = Azalpen laburra irudia ikusi ezin duen jendearentzat edo irudia kargatu ezin denerako. pdfjs-editor-new-alt-text-description = Azalpen laburra irudia ikusi ezin duen jendearentzat edo irudia kargatu ezin denerako.
# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. # This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human.
pdfjs-editor-new-alt-text-disclaimer1 = Ordezko testu hau automatikoki sortu da eta okerra izan liteke. pdfjs-editor-new-alt-text-disclaimer1 = Testu alternatibo hau automatikoki sortu da eta okerra izan liteke.
pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Argibide gehiago pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Argibide gehiago
pdfjs-editor-new-alt-text-create-automatically-button-label = Sortu ordezko testua automatikoki pdfjs-editor-new-alt-text-create-automatically-button-label = Sortu testu alternatiboa automatikoki
pdfjs-editor-new-alt-text-not-now-button = Une honetan ez pdfjs-editor-new-alt-text-not-now-button = Une honetan ez
pdfjs-editor-new-alt-text-error-title = Ezin da ordezko testua automatikoki sortu pdfjs-editor-new-alt-text-error-title = Ezin da testu alternatiboa automatikoki sortu
pdfjs-editor-new-alt-text-error-description = Idatzi zure ordezko testu propioa edo saiatu berriro geroago. pdfjs-editor-new-alt-text-error-description = Idatzi zure testu alternatibo propioa edo saiatu berriro geroago.
pdfjs-editor-new-alt-text-error-close-button = Itxi pdfjs-editor-new-alt-text-error-close-button = Itxi
# Variables: # Variables:
# $totalSize (Number) - the total size (in MB) of the AI model. # $totalSize (Number) - the total size (in MB) of the AI model.
# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. # $downloadedSize (Number) - the downloaded size (in MB) of the AI model.
pdfjs-editor-new-alt-text-ai-model-downloading-progress = Ordezko testuaren AA modeloa deskargatzen ({ $downloadedSize }/{ $totalSize } MB) pdfjs-editor-new-alt-text-ai-model-downloading-progress = Testu alternatiboaren AA modeloa deskargatzen ({ $downloadedSize }/{ $totalSize } MB)
.aria-valuetext = Ordezko testuaren AA modeloa deskargatzen ({ $downloadedSize }/{ $totalSize } MB) .aria-valuetext = Testu alternatiboaren AA modeloa deskargatzen ({ $downloadedSize }/{ $totalSize } MB)
# This is a button that users can click to edit the alt text they have already added. # This is a button that users can click to edit the alt text they have already added.
pdfjs-editor-new-alt-text-added-button = pdfjs-editor-new-alt-text-added-button =
.aria-label = Ordezko testua gehituta .aria-label = Testu alternatiboa gehituta
pdfjs-editor-new-alt-text-added-button-label = Ordezko testua gehituta pdfjs-editor-new-alt-text-added-button-label = Testu alternatiboa gehituta
# This is a button that users can click to open the alt text editor and add alt text when it is not present. # This is a button that users can click to open the alt text editor and add alt text when it is not present.
pdfjs-editor-new-alt-text-missing-button = pdfjs-editor-new-alt-text-missing-button =
.aria-label = Ordezko testua falta da .aria-label = Testu alternatiboa falta da
pdfjs-editor-new-alt-text-missing-button-label = Ordezko testua falta da pdfjs-editor-new-alt-text-missing-button-label = Testu alternatiboa falta da
# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. # This is a button that opens up the alt text modal where users should review the alt text that was automatically generated.
pdfjs-editor-new-alt-text-to-review-button = pdfjs-editor-new-alt-text-to-review-button =
.aria-label = Berrikusi ordezko testua .aria-label = Berrikusi testu alternatiboa
pdfjs-editor-new-alt-text-to-review-button-label = Berrikusi ordezko testua pdfjs-editor-new-alt-text-to-review-button-label = Berrikusi testu alternatiboa
# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. # "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear.
# Variables: # Variables:
# $generatedAltText (String) - the generated alt-text. # $generatedAltText (String) - the generated alt-text.
@ -531,22 +531,22 @@ pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Automatikoki sort
## Image alt-text settings ## Image alt-text settings
pdfjs-image-alt-text-settings-button = pdfjs-image-alt-text-settings-button =
.title = Irudien ordezko testuaren ezarpenak .title = Irudiaren testu alternatiboaren ezarpenak
pdfjs-image-alt-text-settings-button-label = Irudien ordezko testuaren ezarpenak pdfjs-image-alt-text-settings-button-label = Irudiaren testu alternatiboaren ezarpenak
pdfjs-editor-alt-text-settings-dialog-label = Irudien ordezko testuaren ezarpenak pdfjs-editor-alt-text-settings-dialog-label = Irudiaren testu alternatiboaren ezarpenak
pdfjs-editor-alt-text-settings-automatic-title = Ordezko testu automatikoa pdfjs-editor-alt-text-settings-automatic-title = Testu alternatibo automatikoa
pdfjs-editor-alt-text-settings-create-model-button-label = Sortu ordezko testua automatikoki pdfjs-editor-alt-text-settings-create-model-button-label = Sortu testu alternatiboa automatikoki
pdfjs-editor-alt-text-settings-create-model-description = Azalpenak iradokitzen ditu irudia ikusi ezin duen jendearentzat edo irudia kargatu ezin denerako. pdfjs-editor-alt-text-settings-create-model-description = Azalpenak iradokitzen ditu irudia ikusi ezin duen jendearentzat edo irudia kargatu ezin denerako.
# Variables: # Variables:
# $totalSize (Number) - the total size (in MB) of the AI model. # $totalSize (Number) - the total size (in MB) of the AI model.
pdfjs-editor-alt-text-settings-download-model-label = Ordezko testuaren AA modeloa ({ $totalSize } MB) pdfjs-editor-alt-text-settings-download-model-label = Testu alternatiboaren AA modeloa ({ $totalSize } MB)
pdfjs-editor-alt-text-settings-ai-model-description = Zure gailuan modu lokalean exekutatzen da eta zure datuak pribatu mantentzen dira. Ordezko testu automatikorako beharrezkoa. pdfjs-editor-alt-text-settings-ai-model-description = Zure gailuan modu lokalean exekutatzen da eta zure datuak pribatu mantentzen dira. Testu alternatibo automatikorako beharrezkoa.
pdfjs-editor-alt-text-settings-delete-model-button = Ezabatu pdfjs-editor-alt-text-settings-delete-model-button = Ezabatu
pdfjs-editor-alt-text-settings-download-model-button = Deskargatu pdfjs-editor-alt-text-settings-download-model-button = Deskargatu
pdfjs-editor-alt-text-settings-downloading-model-button = Deskargatzen… pdfjs-editor-alt-text-settings-downloading-model-button = Deskargatzen…
pdfjs-editor-alt-text-settings-editor-title = Ordezko testuaren editorea pdfjs-editor-alt-text-settings-editor-title = Testu alternatiboaren editorea
pdfjs-editor-alt-text-settings-show-dialog-button-label = Erakutsi ordezko testua irudi bat gehitzean berehala pdfjs-editor-alt-text-settings-show-dialog-button-label = Erakutsi testu alternatiboa irudi bat gehitzean berehala
pdfjs-editor-alt-text-settings-show-dialog-description = Zure irudi guztiek ordezko testua dutela ziurtatzen laguntzen dizu. pdfjs-editor-alt-text-settings-show-dialog-description = Zure irudiek testu alternatiboa duela ziurtatzen laguntzen dizu.
pdfjs-editor-alt-text-settings-close-button = Itxi pdfjs-editor-alt-text-settings-close-button = Itxi
## Accessibility labels (announced by screen readers) for objects added to the editor. ## Accessibility labels (announced by screen readers) for objects added to the editor.
@ -584,7 +584,7 @@ pdfjs-editor-undo-bar-close-button-label = Itxi
pdfjs-editor-add-signature-dialog-label = pdfjs-editor-add-signature-dialog-label =
Leiho modal honek PDF dokumentu batera gehitzeko sinadurak Leiho modal honek PDF dokumentu batera gehitzeko sinadurak
sortzea ahalbidetzen dio erabiltzaileari. Erabiltzaileak izena edita sortzea ahalbidetzen dio erabiltzaileari. Erabiltzaileak izena edita
dezake (ordezko testu modura ere erabiltzen dena) eta sinadura dezake (testu alternatibo modura ere erabiltzen dena) eta sinadura
gordetzeko aukera du gehiagotan erabili ahal izateko. gordetzeko aukera du gehiagotan erabili ahal izateko.
pdfjs-editor-add-signature-dialog-title = Gehitu sinadura pdfjs-editor-add-signature-dialog-title = Gehitu sinadura
@ -619,9 +619,9 @@ pdfjs-editor-add-signature-image-browse-link =
## Controls ## Controls
pdfjs-editor-add-signature-description-label = Azalpena (ordezko testua) pdfjs-editor-add-signature-description-label = Azalpena (testu alternatiboa)
pdfjs-editor-add-signature-description-input = pdfjs-editor-add-signature-description-input =
.title = Azalpena (ordezko testua) .title = Azalpena (testu alternatiboa)
pdfjs-editor-add-signature-description-default-when-drawing = Sinadura pdfjs-editor-add-signature-description-default-when-drawing = Sinadura
pdfjs-editor-add-signature-clear-button-label = Garbitu sinadura pdfjs-editor-add-signature-clear-button-label = Garbitu sinadura
pdfjs-editor-add-signature-clear-button = pdfjs-editor-add-signature-clear-button =

View File

@ -257,7 +257,7 @@ pdfjs-find-match-count = Occurrence { $current } sur { $total }
# $limit (Number) - the maximum number of matches # $limit (Number) - the maximum number of matches
pdfjs-find-match-count-limit = pdfjs-find-match-count-limit =
{ $limit -> { $limit ->
[1] Plus dune occurrence [one] Plus d{ $limit } occurrence
*[other] Plus de { $limit } occurrences *[other] Plus de { $limit } occurrences
} }
pdfjs-find-not-found = Expression non trouvée pdfjs-find-not-found = Expression non trouvée

View File

@ -261,7 +261,7 @@ pdfjs-find-match-count =
# $limit (Number) - the maximum number of matches # $limit (Number) - the maximum number of matches
pdfjs-find-match-count-limit = pdfjs-find-match-count-limit =
{ $limit -> { $limit ->
[one] Più di { $limit } corrispondenza [one] Più di una { $limit } corrispondenza
*[other] Più di { $limit } corrispondenze *[other] Più di { $limit } corrispondenze
} }
pdfjs-find-not-found = Testo non trovato pdfjs-find-not-found = Testo non trovato

View File

@ -132,8 +132,8 @@ pdfjs-document-properties-page-size-orientation-portrait = pokončno
pdfjs-document-properties-page-size-orientation-landscape = ležeče pdfjs-document-properties-page-size-orientation-landscape = ležeče
pdfjs-document-properties-page-size-name-a-three = A3 pdfjs-document-properties-page-size-name-a-three = A3
pdfjs-document-properties-page-size-name-a-four = A4 pdfjs-document-properties-page-size-name-a-four = A4
pdfjs-document-properties-page-size-name-letter = Letter pdfjs-document-properties-page-size-name-letter = Pismo
pdfjs-document-properties-page-size-name-legal = Legal pdfjs-document-properties-page-size-name-legal = Pravno
## Variables: ## Variables:
## $width (Number) - the width of the (current) page ## $width (Number) - the width of the (current) page

View File

@ -486,7 +486,7 @@ pdfjs-editor-new-alt-text-description = Mô tả ngắn gọn dành cho người
pdfjs-editor-new-alt-text-disclaimer1 = Văn bản thay thế này được tạo tự động và có thể không chính xác. pdfjs-editor-new-alt-text-disclaimer1 = Văn bản thay thế này được tạo tự động và có thể không chính xác.
pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Tìm hiểu thêm pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Tìm hiểu thêm
pdfjs-editor-new-alt-text-create-automatically-button-label = Tạo văn bản thay thế tự động pdfjs-editor-new-alt-text-create-automatically-button-label = Tạo văn bản thay thế tự động
pdfjs-editor-new-alt-text-not-now-button = Để sau pdfjs-editor-new-alt-text-not-now-button = Không phải bây giờ
pdfjs-editor-new-alt-text-error-title = Không thể tạo tự động văn bản thay thế pdfjs-editor-new-alt-text-error-title = Không thể tạo tự động văn bản thay thế
pdfjs-editor-new-alt-text-error-description = Vui lòng viết văn bản thay thế của riêng bạn hoặc thử lại sau. pdfjs-editor-new-alt-text-error-description = Vui lòng viết văn bản thay thế của riêng bạn hoặc thử lại sau.
pdfjs-editor-new-alt-text-error-close-button = Đóng pdfjs-editor-new-alt-text-error-close-button = Đóng

250
package-lock.json generated
View File

@ -15,8 +15,8 @@
"@fluent/dom": "^0.10.2", "@fluent/dom": "^0.10.2",
"@metalsmith/layouts": "^3.0.0", "@metalsmith/layouts": "^3.0.0",
"@metalsmith/markdown": "^1.10.0", "@metalsmith/markdown": "^1.10.0",
"@napi-rs/canvas": "^1.0.3", "@napi-rs/canvas": "^1.0.2",
"@types/node": "^26.1.2", "@types/node": "^26.1.1",
"autoprefixer": "^10.5.4", "autoprefixer": "^10.5.4",
"babel-loader": "^10.1.1", "babel-loader": "^10.1.1",
"babel-plugin-istanbul": "^8.0.2", "babel-plugin-istanbul": "^8.0.2",
@ -33,7 +33,7 @@
"eslint-plugin-prettier": "^5.5.6", "eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-regexp": "^3.1.1", "eslint-plugin-regexp": "^3.1.1",
"eslint-plugin-unicorn": "^72.0.0", "eslint-plugin-unicorn": "^72.0.0",
"globals": "^17.8.0", "globals": "^17.7.0",
"gulp": "^5.0.1", "gulp": "^5.0.1",
"gulp-cli": "^3.1.0", "gulp-cli": "^3.1.0",
"gulp-postcss": "^10.0.0", "gulp-postcss": "^10.0.0",
@ -49,14 +49,14 @@
"jstransformer-nunjucks": "^1.2.0", "jstransformer-nunjucks": "^1.2.0",
"kleur": "^4.1.5", "kleur": "^4.1.5",
"metalsmith": "^2.7.0", "metalsmith": "^2.7.0",
"metalsmith-html-relative": "^2.0.13", "metalsmith-html-relative": "^2.0.12",
"ordered-read-streams": "^2.0.0", "ordered-read-streams": "^2.0.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postcss": "^8.5.25", "postcss": "^8.5.23",
"postcss-discard-comments": "^8.0.1", "postcss-discard-comments": "^8.0.1",
"postcss-values-parser": "^8.0.0", "postcss-values-parser": "^8.0.0",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"puppeteer": "^25.4.0", "puppeteer": "^25.3.0",
"stylelint": "^17.14.1", "stylelint": "^17.14.1",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.3",
"svglint": "^4.2.1", "svglint": "^4.2.1",
@ -65,7 +65,7 @@
"ttest": "^4.0.0", "ttest": "^4.0.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vinyl": "^3.0.1", "vinyl": "^3.0.1",
"webpack": "^5.109.2", "webpack": "^5.109.0",
"webpack-stream": "^7.0.0" "webpack-stream": "^7.0.0"
}, },
"engines": { "engines": {
@ -3669,9 +3669,9 @@
} }
}, },
"node_modules/@napi-rs/canvas": { "node_modules/@napi-rs/canvas": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.2.tgz",
"integrity": "sha512-OlI657a5XXvKGFX7kNeIzJ8rO7IXt87Mqu2H8rXE46viAuOfum/JA7ysX7+eBhxNKznT+RCZh418mndlcFX3+w==", "integrity": "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
@ -3685,23 +3685,23 @@
"url": "https://github.com/sponsors/Brooooooklyn" "url": "https://github.com/sponsors/Brooooooklyn"
}, },
"optionalDependencies": { "optionalDependencies": {
"@napi-rs/canvas-android-arm64": "1.0.3", "@napi-rs/canvas-android-arm64": "1.0.2",
"@napi-rs/canvas-darwin-arm64": "1.0.3", "@napi-rs/canvas-darwin-arm64": "1.0.2",
"@napi-rs/canvas-darwin-x64": "1.0.3", "@napi-rs/canvas-darwin-x64": "1.0.2",
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.3", "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2",
"@napi-rs/canvas-linux-arm64-gnu": "1.0.3", "@napi-rs/canvas-linux-arm64-gnu": "1.0.2",
"@napi-rs/canvas-linux-arm64-musl": "1.0.3", "@napi-rs/canvas-linux-arm64-musl": "1.0.2",
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.3", "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2",
"@napi-rs/canvas-linux-x64-gnu": "1.0.3", "@napi-rs/canvas-linux-x64-gnu": "1.0.2",
"@napi-rs/canvas-linux-x64-musl": "1.0.3", "@napi-rs/canvas-linux-x64-musl": "1.0.2",
"@napi-rs/canvas-win32-arm64-msvc": "1.0.3", "@napi-rs/canvas-win32-arm64-msvc": "1.0.2",
"@napi-rs/canvas-win32-x64-msvc": "1.0.3" "@napi-rs/canvas-win32-x64-msvc": "1.0.2"
} }
}, },
"node_modules/@napi-rs/canvas-android-arm64": { "node_modules/@napi-rs/canvas-android-arm64": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.2.tgz",
"integrity": "sha512-7kSCdUhoXiO+AaIMXdBGdtp6EctZNkmF62Rea/BmVQlwKaM3bBhOzyGUzxyxz9dv5vdBfpyAaxhSRSJF4kqK4A==", "integrity": "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3720,9 +3720,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-darwin-arm64": { "node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.2.tgz",
"integrity": "sha512-ds14V1BPagLszQyaDTeggny5fNeTCqsUQ5QhFj9VDxSEfzrVxXtdbR0LoFyKa0Siaaw8KvqSk4t7k/WoZJwvbg==", "integrity": "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3741,9 +3741,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-darwin-x64": { "node_modules/@napi-rs/canvas-darwin-x64": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.2.tgz",
"integrity": "sha512-qof3LRAAycmkV2I1izZo9RoSHF8kCQr5O05sFwv0jK8rSdYV6KHVwimo6Qb7RxZj40WHKbLHm5JDaUF0o5XUAA==", "integrity": "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3762,9 +3762,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.2.tgz",
"integrity": "sha512-FU2kKZLmolHA9+KcUA+l1+xH3WTLUUTQDU/kLv9SEUr2TrRPu94aytOeizFJDHPs/QBcw4QL1mCQhetQXYBbag==", "integrity": "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -3783,9 +3783,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-arm64-gnu": { "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.2.tgz",
"integrity": "sha512-GVSjntxKeA+/y/ZKf1F+cmUw1WeIkE5aMRPqnZUlBTBvBcrvgWccJAWuYCKPX4QJQwZILIIwhgdAbl51yj6fpA==", "integrity": "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3807,9 +3807,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-arm64-musl": { "node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.2.tgz",
"integrity": "sha512-J51oK/axyZ13kxycumSMfLiDZMdWdOVvqDFI28BpuViZHE3A0bQfr8B5vg8YnPEnqLD3BSn1hkdlh2buspEcNQ==", "integrity": "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3831,9 +3831,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": { "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.2.tgz",
"integrity": "sha512-CtQgQjoVTX67jS9XuCTtJ40Sl7wRLMguoFnnGnfDmCWf7kzKFZVwj5ynqUOIGKFMSB61ZCuQlwPvVNxYTTseaw==", "integrity": "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -3855,9 +3855,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-x64-gnu": { "node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.2.tgz",
"integrity": "sha512-jtfzAHFp+FRaR7zGT4jyCe6wUgAG/dVb5A4Apd8FY9jKarntDfUAlJXscugiH7ZF5kKnu7/lHFk9LaDPcrGEVQ==", "integrity": "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3879,9 +3879,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-x64-musl": { "node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.2.tgz",
"integrity": "sha512-xTzaUCKUHTY4bCGadeeRZggbRVbGUT1petg7Z8r9AJR2+D9Bqu6nQAgqBGC6D47tA70LjaaaLTrJ7wNY1T74dg==", "integrity": "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3903,9 +3903,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-win32-arm64-msvc": { "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.2.tgz",
"integrity": "sha512-ktVLuBkI6QVOm5BwO/WbdGwxgeetAMJa7TTmR8qBarXF0OU2NKjvjUtPJAl2y8t+zBRczJl/1VOl9gua6WcK2g==", "integrity": "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3924,9 +3924,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-win32-x64-msvc": { "node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.2.tgz",
"integrity": "sha512-SGhlQ8bDjL1Cz2KnsKMasr/5sTcwG/SZkB6WCJxLsmSm/3aS2C+3p39bA7iZ2/94+NkVDySZfbiGoaSZSFHYxA==", "integrity": "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4160,9 +4160,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "26.1.2", "version": "26.1.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
"integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5647,9 +5647,9 @@
} }
}, },
"node_modules/chromium-bidi": { "node_modules/chromium-bidi": {
"version": "17.0.2", "version": "16.0.1",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
"integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -6132,9 +6132,9 @@
} }
}, },
"node_modules/devtools-protocol": { "node_modules/devtools-protocol": {
"version": "0.0.1653615", "version": "0.0.1638949",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1653615.tgz", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz",
"integrity": "sha512-pGVkY3T/qXxAp2nFPodwYqOevk6ncNMSmvL8QfRCx5ZWGd6Vor7AFNmyaA8Zs6uJyP1QAfjuLandCgvSix1BNA==", "integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==",
"dev": true, "dev": true,
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
@ -6320,9 +6320,9 @@
} }
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.24.5", "version": "5.24.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz",
"integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -6956,9 +6956,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.5", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -7448,9 +7448,9 @@
} }
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "17.8.0", "version": "17.7.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
"integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==", "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -9279,15 +9279,15 @@
} }
}, },
"node_modules/metalsmith-html-relative": { "node_modules/metalsmith-html-relative": {
"version": "2.0.13", "version": "2.0.12",
"resolved": "https://registry.npmjs.org/metalsmith-html-relative/-/metalsmith-html-relative-2.0.13.tgz", "resolved": "https://registry.npmjs.org/metalsmith-html-relative/-/metalsmith-html-relative-2.0.12.tgz",
"integrity": "sha512-jG6aVx5v1cL9/mY72qBknPPF+RZluOSim3zETDxJkQt6+Ejb/AyABmNUResNFwyfh4ZjBcAKxSXgid3PKANsfA==", "integrity": "sha512-VYZ0OsUbhTk/ktaDilMbgjAZowHaYXBbAd9O3zz66+iNtcWEDgrrrw2TLjxxHQPB4OCa7KqfRRFgInykTI9aUQ==",
"dev": true, "dev": true,
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"minimatch": "^10.2.6" "minimatch": "^10.2.5"
}, },
"engines": { "engines": {
"node": ">=20.18.1" "node": ">=20.18.1"
@ -9364,13 +9364,13 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "10.2.6", "version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"brace-expansion": "^5.0.8" "brace-expansion": "^5.0.5"
}, },
"engines": { "engines": {
"node": "18 || 20 || >=22" "node": "18 || 20 || >=22"
@ -9471,9 +9471,9 @@
} }
}, },
"node_modules/modern-tar": { "node_modules/modern-tar": {
"version": "0.7.7", "version": "0.7.6",
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.7.tgz", "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
"integrity": "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==", "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -10104,9 +10104,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.25", "version": "8.5.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
"integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -10339,18 +10339,18 @@
} }
}, },
"node_modules/puppeteer": { "node_modules/puppeteer": {
"version": "25.4.0", "version": "25.3.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.4.0.tgz", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.3.0.tgz",
"integrity": "sha512-xfQp8dFBcGaLc1hEMaVr7s+oW4ZkAurr8Y9H81ilKhu6QoLfSTkZjU7IavnyJ/VWpB9ni3KNJUQHUatslLWyGw==", "integrity": "sha512-O1tx8S315aw8eI99HZ5ZNcVEzJ9+jKF//eO5UvfZ3cXJ6okZ5sX3Y50u7DJaM+ewEK4LqXP068tBhfRaWikj+g==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@puppeteer/browsers": "3.0.6", "@puppeteer/browsers": "3.0.6",
"chromium-bidi": "17.0.2", "chromium-bidi": "16.0.1",
"devtools-protocol": "0.0.1653615", "devtools-protocol": "0.0.1638949",
"lilconfig": "^3.1.3", "lilconfig": "^3.1.3",
"puppeteer-core": "25.4.0", "puppeteer-core": "25.3.0",
"typed-query-selector": "^2.12.2" "typed-query-selector": "^2.12.2"
}, },
"bin": { "bin": {
@ -10361,18 +10361,18 @@
} }
}, },
"node_modules/puppeteer-core": { "node_modules/puppeteer-core": {
"version": "25.4.0", "version": "25.3.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.4.0.tgz", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.3.0.tgz",
"integrity": "sha512-K1plkLOdeoUnGeT1OvdqF3qxl33v+Ra/uH5VyPEhXdMcpvGiEskHzxxEU3fgpccJpJLIipB/rPUsvkZRWeKqOA==", "integrity": "sha512-fm+wpUr2oigH1PXZvwgATrM2tYWHMDG8ASzTEe9uukCye4X5Ldx1K5BTHPFKITrIWvQQAQ256d1NpbEveBcKjA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@puppeteer/browsers": "3.0.6", "@puppeteer/browsers": "3.0.6",
"chromium-bidi": "17.0.2", "chromium-bidi": "16.0.1",
"devtools-protocol": "0.0.1653615", "devtools-protocol": "0.0.1638949",
"typed-query-selector": "^2.12.2", "typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.2", "webdriver-bidi-protocol": "0.4.2",
"ws": "^8.21.1" "ws": "^8.21.0"
}, },
"engines": { "engines": {
"node": ">=22.12.0" "node": ">=22.12.0"
@ -11974,9 +11974,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/test-exclude/node_modules/brace-expansion": { "node_modules/test-exclude/node_modules/brace-expansion": {
"version": "2.1.4", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -12453,9 +12453,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "7.29.0", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -12761,9 +12761,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.109.2", "version": "5.109.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.2.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.0.tgz",
"integrity": "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==", "integrity": "sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -12775,7 +12775,7 @@
"acorn": "^8.16.0", "acorn": "^8.16.0",
"browserslist": "^4.28.1", "browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2", "chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.24.4", "enhanced-resolve": "^5.24.2",
"es-module-lexer": "^2.1.0", "es-module-lexer": "^2.1.0",
"eslint-scope": "5.1.1", "eslint-scope": "5.1.1",
"events": "^3.2.0", "events": "^3.2.0",
@ -13162,9 +13162,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.21.1", "version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -13240,16 +13240,16 @@
} }
}, },
"node_modules/yargs": { "node_modules/yargs": {
"version": "18.1.0", "version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cliui": "^9.0.1", "cliui": "^9.0.1",
"escalade": "^3.1.1", "escalade": "^3.1.1",
"get-caller-file": "^2.0.5", "get-caller-file": "^2.0.5",
"string-width": "^8.2.1", "string-width": "^7.2.0",
"y18n": "^5.0.5", "y18n": "^5.0.5",
"yargs-parser": "^22.0.0" "yargs-parser": "^22.0.0"
}, },
@ -13280,18 +13280,26 @@
"url": "https://github.com/chalk/ansi-regex?sponsor=1" "url": "https://github.com/chalk/ansi-regex?sponsor=1"
} }
}, },
"node_modules/yargs/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": { "node_modules/yargs/node_modules/string-width": {
"version": "8.2.2", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"get-east-asian-width": "^1.5.0", "emoji-regex": "^10.3.0",
"strip-ansi": "^7.1.2" "get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"

View File

@ -10,8 +10,8 @@
"@fluent/dom": "^0.10.2", "@fluent/dom": "^0.10.2",
"@metalsmith/layouts": "^3.0.0", "@metalsmith/layouts": "^3.0.0",
"@metalsmith/markdown": "^1.10.0", "@metalsmith/markdown": "^1.10.0",
"@napi-rs/canvas": "^1.0.3", "@napi-rs/canvas": "^1.0.2",
"@types/node": "^26.1.2", "@types/node": "^26.1.1",
"autoprefixer": "^10.5.4", "autoprefixer": "^10.5.4",
"babel-loader": "^10.1.1", "babel-loader": "^10.1.1",
"babel-plugin-istanbul": "^8.0.2", "babel-plugin-istanbul": "^8.0.2",
@ -28,7 +28,7 @@
"eslint-plugin-prettier": "^5.5.6", "eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-regexp": "^3.1.1", "eslint-plugin-regexp": "^3.1.1",
"eslint-plugin-unicorn": "^72.0.0", "eslint-plugin-unicorn": "^72.0.0",
"globals": "^17.8.0", "globals": "^17.7.0",
"gulp": "^5.0.1", "gulp": "^5.0.1",
"gulp-cli": "^3.1.0", "gulp-cli": "^3.1.0",
"gulp-postcss": "^10.0.0", "gulp-postcss": "^10.0.0",
@ -44,14 +44,14 @@
"jstransformer-nunjucks": "^1.2.0", "jstransformer-nunjucks": "^1.2.0",
"kleur": "^4.1.5", "kleur": "^4.1.5",
"metalsmith": "^2.7.0", "metalsmith": "^2.7.0",
"metalsmith-html-relative": "^2.0.13", "metalsmith-html-relative": "^2.0.12",
"ordered-read-streams": "^2.0.0", "ordered-read-streams": "^2.0.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postcss": "^8.5.25", "postcss": "^8.5.23",
"postcss-discard-comments": "^8.0.1", "postcss-discard-comments": "^8.0.1",
"postcss-values-parser": "^8.0.0", "postcss-values-parser": "^8.0.0",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"puppeteer": "^25.4.0", "puppeteer": "^25.3.0",
"stylelint": "^17.14.1", "stylelint": "^17.14.1",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "^5.0.3",
"svglint": "^4.2.1", "svglint": "^4.2.1",
@ -60,7 +60,7 @@
"ttest": "^4.0.0", "ttest": "^4.0.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vinyl": "^3.0.1", "vinyl": "^3.0.1",
"webpack": "^5.109.2", "webpack": "^5.109.0",
"webpack-stream": "^7.0.0" "webpack-stream": "^7.0.0"
}, },
"repository": { "repository": {

View File

@ -1,5 +1,5 @@
{ {
"stableVersion": "6.2.108", "stableVersion": "6.1.200",
"baseVersion": "ce4ff55faaa83b39b0137dc458af6eea6f96235f", "baseVersion": "eddd70a2ca1054ad2e0792972c3f2774b89f0cd2",
"versionPrefix": "6.3." "versionPrefix": "6.2."
} }

View File

@ -2887,7 +2887,10 @@ class TextWidgetAnnotation extends WidgetAnnotation {
this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL); this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL);
// Check if we have a date or time. // Check if we have a date or time.
const { actions } = this.data; const {
data: { actions },
} = this;
if (!actions) { if (!actions) {
return; return;
} }
@ -2895,35 +2898,32 @@ class TextWidgetAnnotation extends WidgetAnnotation {
const AFDateTime = const AFDateTime =
/^AF(Date|Time)_(?:Keystroke|Format)(?:Ex)?\(['"]?([^'"]+)['"]?\);$/; /^AF(Date|Time)_(?:Keystroke|Format)(?:Ex)?\(['"]?([^'"]+)['"]?\);$/;
let canUseHTMLDateTime = false; let canUseHTMLDateTime = false;
const aFormat = actions.get("Format"),
aKeystroke = actions.get("Keystroke");
if ( if (
(aFormat?.length === 1 && (actions.Format?.length === 1 &&
aKeystroke?.length === 1 && actions.Keystroke?.length === 1 &&
AFDateTime.test(aFormat[0]) && AFDateTime.test(actions.Format[0]) &&
AFDateTime.test(aKeystroke[0])) || AFDateTime.test(actions.Keystroke[0])) ||
(aFormat?.length === 0 && (actions.Format?.length === 0 &&
aKeystroke?.length === 1 && actions.Keystroke?.length === 1 &&
AFDateTime.test(aKeystroke[0])) || AFDateTime.test(actions.Keystroke[0])) ||
(aKeystroke?.length === 0 && (actions.Keystroke?.length === 0 &&
aFormat?.length === 1 && actions.Format?.length === 1 &&
AFDateTime.test(aFormat[0])) AFDateTime.test(actions.Format[0]))
) { ) {
// If the Format and Keystroke actions are the same, we can just use // If the Format and Keystroke actions are the same, we can just use
// the Format action. // the Format action.
canUseHTMLDateTime = true; canUseHTMLDateTime = true;
} }
const actionsToVisit = []; const actionsToVisit = [];
if (aFormat) { if (actions.Format) {
actionsToVisit.push(...aFormat); actionsToVisit.push(...actions.Format);
} }
if (aKeystroke) { if (actions.Keystroke) {
actionsToVisit.push(...aKeystroke); actionsToVisit.push(...actions.Keystroke);
} }
if (canUseHTMLDateTime) { if (canUseHTMLDateTime) {
actions.delete("Keystroke"); delete actions.Keystroke;
actions.set("Format", actionsToVisit); actions.Format = actionsToVisit;
} }
for (const formatAction of actionsToVisit) { for (const formatAction of actionsToVisit) {

View File

@ -19,7 +19,6 @@ import {
DocumentActionEventType, DocumentActionEventType,
FormatError, FormatError,
info, info,
makeArr,
PermissionFlag, PermissionFlag,
shadow, shadow,
stringToUTF8String, stringToUTF8String,
@ -1288,10 +1287,10 @@ class Catalog {
); );
if (javaScript) { if (javaScript) {
actions ??= new Map(); actions ??= Object.create(null);
for (const [key, val] of javaScript) { for (const [key, val] of javaScript) {
actions.getOrInsertComputed(key, makeArr).push(val); (actions[key] ??= []).push(val);
} }
} }
return shadow(this, "jsActions", actions); return shadow(this, "jsActions", actions);

View File

@ -1428,9 +1428,10 @@ class CFFFDSelect {
} }
getFDIndex(glyphIndex) { getFDIndex(glyphIndex) {
return glyphIndex < 0 || glyphIndex >= this.fdSelect.length if (glyphIndex < 0 || glyphIndex >= this.fdSelect.length) {
? -1 return -1;
: this.fdSelect[glyphIndex]; }
return this.fdSelect[glyphIndex];
} }
} }

View File

@ -239,10 +239,10 @@ class ChunkedStream extends Stream {
}; };
Object.defineProperty(ChunkedStreamSubstream.prototype, "isDataLoaded", { Object.defineProperty(ChunkedStreamSubstream.prototype, "isDataLoaded", {
get() { get() {
return ( if (this.numChunksLoaded === this.numChunks) {
this.numChunksLoaded === this.numChunks || return true;
this.getMissingChunks().length === 0 }
); return this.getMissingChunks().length === 0;
}, },
configurable: true, configurable: true,
}); });

View File

@ -23,7 +23,6 @@ import {
} from "../shared/util.js"; } from "../shared/util.js";
import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js"; import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js";
import { BaseStream } from "./base_stream.js"; import { BaseStream } from "./base_stream.js";
import { CONTROL_CHAR_REGEXP } from "../shared/css_utils.js";
import { stringToPDFString } from "./string_utils.js"; import { stringToPDFString } from "./string_utils.js";
const PDF_VERSION_REGEXP = /^[1-9]\.\d$/; const PDF_VERSION_REGEXP = /^[1-9]\.\d$/;
@ -342,8 +341,7 @@ function lookupNormalRect(arr, fallback) {
* each part of the path. * each part of the path.
*/ */
function parseXFAPath(path) { function parseXFAPath(path) {
// Anchoring prevents retrying the match at every character. const positionPattern = /(.+)\[(\d+)\]$/;
const positionPattern = /^(.+)\[(\d+)\]$/;
return path.split(".").map(component => { return path.split(".").map(component => {
const m = component.match(positionPattern); const m = component.match(positionPattern);
if (m) { if (m) {
@ -451,7 +449,7 @@ function _collectJS(entry, xref, list, parents) {
} }
function collectActions(xref, dict, eventType) { function collectActions(xref, dict, eventType) {
const actions = new Map(); const actions = Object.create(null);
const additionalActionsDicts = getInheritableProperty({ const additionalActionsDicts = getInheritableProperty({
dict, dict,
key: "AA", key: "AA",
@ -477,7 +475,7 @@ function collectActions(xref, dict, eventType) {
const list = []; const list = [];
_collectJS(rawActionDict, xref, list, parents); _collectJS(rawActionDict, xref, list, parents);
if (list.length > 0) { if (list.length > 0) {
actions.set(action, list); actions[action] = list;
} }
} }
} }
@ -489,10 +487,10 @@ function collectActions(xref, dict, eventType) {
const list = []; const list = [];
_collectJS(actionDict, xref, list, parents); _collectJS(actionDict, xref, list, parents);
if (list.length > 0) { if (list.length > 0) {
actions.set("Action", list); actions.Action = list;
} }
} }
return actions.size ? actions : null; return Object.keys(actions).length ? actions : null;
} }
const XMLEntities = { const XMLEntities = {
@ -562,17 +560,6 @@ function validateFontName(fontFamily, mustWarn = false) {
} }
return false; return false;
} }
// A <string> is terminated by a newline, which for CSS also includes the
// form feed character; see https://drafts.csswg.org/css-syntax/#newline.
// The font family is escaped before being used, see `serializeFontFamily`,
// hence this only prevents values that cannot sensibly name a font from
// being used at all (the unquoted case below is already this strict).
if (CONTROL_CHAR_REGEXP.test(fontFamily)) {
if (mustWarn) {
warn(`FontFamily contains control characters: ${fontFamily}.`);
}
return false;
}
} else { } else {
// See https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident. // See https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident.
for (const ident of fontFamily.split(/[ \t]+/)) { for (const ident of fontFamily.split(/[ \t]+/)) {
@ -587,18 +574,6 @@ function validateFontName(fontFamily, mustWarn = false) {
return true; return true;
} }
// Strip the spaces preceding a digit, since e.g. "Wingdings 3" is not a valid
// font name in the css specs.
// The optional trailing digit is matched as part of the space run, so that a
// failing match cannot backtrack over the spaces; otherwise the replacement
// would be quadratic in the number of consecutive spaces.
function normalizeCSSFontFamily(fontFamily) {
return fontFamily.replaceAll(
/( +)(\d)?/g,
(_, spaces, digit) => digit ?? " "
);
}
function validateCSSFont(cssFontInfo) { function validateCSSFont(cssFontInfo) {
// See https://developer.mozilla.org/en-US/docs/Web/CSS/font-style. // See https://developer.mozilla.org/en-US/docs/Web/CSS/font-style.
const DEFAULT_CSS_FONT_OBLIQUE = "14"; const DEFAULT_CSS_FONT_OBLIQUE = "14";
@ -775,7 +750,6 @@ export {
lookupRect, lookupRect,
MAX_INT_32, MAX_INT_32,
MissingDataException, MissingDataException,
normalizeCSSFontFamily,
numberToString, numberToString,
ParserEOFException, ParserEOFException,
parseXFAPath, parseXFAPath,

View File

@ -111,9 +111,10 @@ class DecodeStream extends BaseStream {
async getImageData(length, decoderOptions) { async getImageData(length, decoderOptions) {
if (!this.canAsyncDecodeImageFromBuffer) { if (!this.canAsyncDecodeImageFromBuffer) {
return this.isAsyncDecoder if (this.isAsyncDecoder) {
? this.decodeImage(null, length, decoderOptions) return this.decodeImage(null, length, decoderOptions);
: this.getBytes(length, decoderOptions); }
return this.getBytes(length, decoderOptions);
} }
const data = await this.stream.asyncGetBytes(); const data = await this.stream.asyncGetBytes();
return this.decodeImage(data, length, decoderOptions); return this.decodeImage(data, length, decoderOptions);

View File

@ -42,7 +42,6 @@ import {
isWhiteSpace, isWhiteSpace,
lookupNormalRect, lookupNormalRect,
MissingDataException, MissingDataException,
normalizeCSSFontFamily,
PDF_VERSION_REGEXP, PDF_VERSION_REGEXP,
RESOURCES_KEYS_OPERATOR_LIST, RESOURCES_KEYS_OPERATOR_LIST,
RESOURCES_KEYS_TEXT_CONTENT, RESOURCES_KEYS_TEXT_CONTENT,
@ -1397,7 +1396,9 @@ class PDFDocument {
if (!(descriptor instanceof Dict)) { if (!(descriptor instanceof Dict)) {
continue; continue;
} }
const fontFamily = normalizeCSSFontFamily(descriptor.get("FontFamily")); let fontFamily = descriptor.get("FontFamily");
// For example, "Wingdings 3" is not a valid font name in the css specs.
fontFamily = fontFamily.replaceAll(/ +(\d)/g, "$1");
const fontWeight = descriptor.get("FontWeight"); const fontWeight = descriptor.get("FontWeight");
// Angle is expressed in degrees counterclockwise in PDF // Angle is expressed in degrees counterclockwise in PDF
@ -1961,7 +1962,7 @@ class PDFDocument {
const { acroForm } = annotationGlobals; const { acroForm } = annotationGlobals;
const visitedRefs = new RefSet(); const visitedRefs = new RefSet();
const allFields = new Map(); const allFields = Object.create(null);
const fieldPromises = new Map(); const fieldPromises = new Map();
const orphanFields = new RefSetCache(); const orphanFields = new RefSetCache();
for (const fieldRef of acroForm.get("Fields")) { for (const fieldRef of acroForm.get("Fields")) {
@ -1982,7 +1983,7 @@ class PDFDocument {
Promise.all(promises).then(fields => { Promise.all(promises).then(fields => {
fields = fields.filter(field => !!field); fields = fields.filter(field => !!field);
if (fields.length > 0) { if (fields.length > 0) {
allFields.set(name, fields); allFields[name] = fields;
} }
}) })
); );
@ -1990,7 +1991,7 @@ class PDFDocument {
await Promise.all(allPromises); await Promise.all(allPromises);
return { return {
allFields: allFields.size ? allFields : null, allFields: Object.keys(allFields).length ? allFields : null,
orphanFields, orphanFields,
}; };
}); });
@ -2268,9 +2269,9 @@ class PDFDocument {
return true; return true;
} }
if (fieldObjects?.allFields) { if (fieldObjects?.allFields) {
return fieldObjects.allFields return Object.values(fieldObjects.allFields).some(fieldObject =>
.values() fieldObject.some(object => object.actions !== null)
.some(fieldObj => fieldObj.some(obj => obj.actions !== null)); );
} }
return false; return false;
} }

View File

@ -57,8 +57,6 @@ class PageData {
this.annotations = null; this.annotations = null;
// Named destinations which points to this page. // Named destinations which points to this page.
this.pointingNamedDestinations = null; this.pointingNamedDestinations = null;
// Rank of this page among the output pages sharing the same source page.
this.copyLevel = 0;
documentData.pagesMap.put(page.ref, this); documentData.pagesMap.put(page.ref, this);
} }
@ -265,7 +263,7 @@ class PDFEditor {
) { ) {
if (obj instanceof Ref) { if (obj instanceof Ref) {
const { const {
currentDocument: { fieldToParent, oldRefMapping }, currentDocument: { oldRefMapping },
} = this; } = this;
const existingRef = oldRefMapping.get(obj); const existingRef = oldRefMapping.get(obj);
if (existingRef) { if (existingRef) {
@ -273,12 +271,6 @@ class PDFEditor {
} }
const oldRef = obj; const oldRef = obj;
obj = await xref.fetchAsync(oldRef); obj = await xref.fetchAsync(oldRef);
const mappedRef = oldRefMapping.get(oldRef);
if (mappedRef) {
// Another concurrent traversal may have allocated the clone while the
// source object was being fetched.
return mappedRef;
}
if (typeof obj === "number") { if (typeof obj === "number") {
// Simple value; no need to create a new reference. // Simple value; no need to create a new reference.
return obj; return obj;
@ -307,17 +299,9 @@ class PDFEditor {
} }
} }
let cloneSource = true;
if (fieldToParent.has(oldRef) && obj instanceof Dict) {
// Avoid following a widget's field hierarchy while cloning the page,
// without mutating the source dictionary cached by its XRef.
obj = this.cloneDict(obj);
obj.delete("Parent");
cloneSource = false;
}
this.xref[newRef.num] = await this.#collectDependencies( this.xref[newRef.num] = await this.#collectDependencies(
obj, obj,
cloneSource, true,
xref, xref,
resourceStreamPath resourceStreamPath
); );
@ -1099,18 +1083,10 @@ class PDFEditor {
} }
} }
await Promise.all(promises); await Promise.all(promises);
const copyCounts = new Map();
for (let i = 0, ii = this.oldPages.length; i < ii; i++) { for (let i = 0, ii = this.oldPages.length; i < ii; i++) {
const pageData = this.oldPages[i]; if (this.oldPages[i] === undefined) {
if (pageData === undefined) {
throw new Error("extractPages: sparse pageIndices."); throw new Error("extractPages: sparse pageIndices.");
} }
if (pageData) {
const { page } = pageData;
const copyLevel = copyCounts.get(page) ?? 0;
copyCounts.set(page, copyLevel + 1);
pageData.copyLevel = copyLevel;
}
} }
promises.length = 0; promises.length = 0;
@ -1247,8 +1223,11 @@ class PDFEditor {
"Sig" "Sig"
); );
const parentRef = annotationDict.getRaw("Parent") || null; const parentRef = annotationDict.getRaw("Parent") || null;
// The parent will be omitted from the annotation clone to avoid // We remove the parent to avoid visiting it when cloning the
// visiting it, then restored by #mergeAcroForms. // annotation.
// It'll be fixed later in #mergeAcroForms when merging the
// AcroForms.
annotationDict.delete("Parent");
fieldToParent.put(annotationRef, parentRef); fieldToParent.put(annotationRef, parentRef);
} }
@ -2157,20 +2136,8 @@ class PDFEditor {
} }
let parent = parentRef; let parent = parentRef;
let lastNonNullParent = parentRef; let lastNonNullParent = parentRef;
const visited = new RefSet();
while (true) { while (true) {
if (parent instanceof Ref) { parent = xref.fetchIfRef(parent)?.getRaw("Parent") || null;
if (visited.has(parent)) {
// Cyclic Parent chain: stop on the field closing the cycle.
break;
}
visited.put(parent);
}
const parentDict = xref.fetchIfRef(parent);
if (!(parentDict instanceof Dict)) {
break;
}
parent = parentDict.getRaw("Parent") || null;
if (!parent) { if (!parent) {
break; break;
} }
@ -2243,9 +2210,6 @@ class PDFEditor {
} }
processed.put(oldKidRef); processed.put(oldKidRef);
const kid = xref.fetchIfRef(oldKidRef); const kid = xref.fetchIfRef(oldKidRef);
if (!(kid instanceof Dict)) {
continue;
}
if (kid.has("Kids")) { if (kid.has("Kids")) {
const kidsArray = kid.get("Kids"); const kidsArray = kid.get("Kids");
if (!Array.isArray(kidsArray)) { if (!Array.isArray(kidsArray)) {
@ -2423,18 +2387,12 @@ class PDFEditor {
/** /**
* Create a copy of a page. * Create a copy of a page.
* @param {number} pageIndex - Index of the page slot in the new document * @param {number} pageIndex
* (the index of the source page is `page.pageIndex`).
* @returns {Promise<Ref>} the page reference in the new PDF document. * @returns {Promise<Ref>} the page reference in the new PDF document.
*/ */
async #makePageCopy(pageIndex) { async #makePageCopy(pageIndex) {
const { const { page, documentData, annotations, pointingNamedDestinations } =
page, this.oldPages[pageIndex];
documentData,
annotations,
pointingNamedDestinations,
copyLevel,
} = this.oldPages[pageIndex];
this.currentDocument = documentData; this.currentDocument = documentData;
const { dedupNamedDestinations, oldRefMapping } = documentData; const { dedupNamedDestinations, oldRefMapping } = documentData;
const { xref, rotate, mediaBox, resources, ref: oldPageRef } = page; const { xref, rotate, mediaBox, resources, ref: oldPageRef } = page;
@ -2503,11 +2461,9 @@ class PDFEditor {
const newAnnotations = const newAnnotations =
documentData.document === this.#primaryDocument documentData.document === this.#primaryDocument
? this.#newAnnotationsParams?.newAnnotationsByPage ? this.#newAnnotationsParams?.newAnnotationsByPage?.get(page.pageIndex)
?.get(page.pageIndex)
?.filter(({ copyLevel: level }) => (level ?? 0) === copyLevel)
: null; : null;
if (newAnnotations?.length) { if (newAnnotations) {
const { handler, task, imagesPromises } = this.#newAnnotationsParams; const { handler, task, imagesPromises } = this.#newAnnotationsParams;
const changes = new RefSetCache(); const changes = new RefSetCache();
const newData = await AnnotationFactory.saveNewAnnotations( const newData = await AnnotationFactory.saveNewAnnotations(

View File

@ -186,7 +186,10 @@ class Parser {
} }
if (typeof buf1 === "string") { if (typeof buf1 === "string") {
return cipherTransform ? cipherTransform.decryptString(buf1) : buf1; if (cipherTransform) {
return cipherTransform.decryptString(buf1);
}
return buf1;
} }
// simple object // simple object

View File

@ -132,7 +132,7 @@ class Lexer {
this.pos = 0; this.pos = 0;
this.len = data.length; this.len = data.length;
// Sticky regexes: set lastIndex before exec() to match at an exact offset. // Sticky regexes: set lastIndex before exec() to match at an exact offset.
this._numberPattern = /[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/iy; this._numberPattern = /[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/iy;
this._identifierPattern = /[a-z]+/y; this._identifierPattern = /[a-z]+/y;
} }

View File

@ -39,7 +39,10 @@ class Stream extends BaseStream {
} }
getByte() { getByte() {
return this.pos >= this.end ? -1 : this.bytes[this.pos++]; if (this.pos >= this.end) {
return -1;
}
return this.bytes[this.pos++];
} }
getBytes(length) { getBytes(length) {

View File

@ -16,15 +16,19 @@
import { stringToBytes, Util, warn } from "../shared/util.js"; import { stringToBytes, Util, warn } from "../shared/util.js";
function isAscii(str) { function isAscii(str) {
// eslint-disable-next-line no-control-regex return (
return typeof str === "string" && (!str || /^[\x00-\x7F]*$/.test(str)); typeof str === "string" &&
// eslint-disable-next-line no-control-regex
(!str || /^[\x00-\x7F]*$/.test(str))
);
} }
// If the string is null or undefined then it is returned as is. // If the string is null or undefined then it is returned as is.
function stringToAsciiOrUTF16BE(str) { function stringToAsciiOrUTF16BE(str) {
return str === null || str === undefined || isAscii(str) if (str === null || str === undefined) {
? str return str;
: stringToUTF16String(str, /* bigEndian = */ true); }
return isAscii(str) ? str : stringToUTF16String(str, /* bigEndian = */ true);
} }
function stringToUTF16HexString(str) { function stringToUTF16HexString(str) {

View File

@ -83,9 +83,10 @@ class IdentityToUnicodeMap {
} }
get(i) { get(i) {
return this.firstChar <= i && i <= this.lastChar if (this.firstChar <= i && i <= this.lastChar) {
? String.fromCharCode(i) return String.fromCharCode(i);
: undefined; }
return undefined;
} }
charCodeOf(v) { charCodeOf(v) {

View File

@ -148,31 +148,6 @@ async function writeArray(array, buffer, transform) {
buffer.push("]"); buffer.push("]");
} }
// The exponential notation isn't valid in a PDF, hence a number is always
// written with all its digits.
function numberToPDFString(value) {
// `toFixed` uses the exponential notation from 1e21 on, so such a number is
// written thanks to BigInt: it's necessarily an integer, and `isInteger` also
// rules out NaN and ±Infinity for which BigInt would throw.
if (Number.isInteger(value) && Math.abs(value) >= 1e21) {
return BigInt(value).toString();
}
// Below that limit `toFixed(10)` never uses the exponential notation (unlike
// `toString` which uses it under 1e-6) and it rounds the value: it always
// adds 10 decimals, hence scan them backwards to remove the trailing zeros,
// and then the dot itself when none of the decimals is left.
const str = value.toFixed(10);
let end = str.length;
while (str[end - 1] === "0") {
end--;
}
if (str[end - 1] === ".") {
end--;
}
return str.slice(0, end);
}
async function writeValue(value, buffer, transform) { async function writeValue(value, buffer, transform) {
if (value instanceof Name) { if (value instanceof Name) {
buffer.push(`/${escapePDFName(value.name)}`); buffer.push(`/${escapePDFName(value.name)}`);
@ -190,7 +165,9 @@ async function writeValue(value, buffer, transform) {
// matrices (e.g. [0.000008 0 0 0.000008 0 0]). // 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 // The numbers must be "rounded" only when pdf.js is producing them and the
// current transformation matrix is well known. // current transformation matrix is well known.
buffer.push(numberToPDFString(value)); // toFixed(10) avoids scientific notation and rounds; the replace removes
// trailing zeros (and a trailing dot for integers).
buffer.push(value.toFixed(10).replace(/\.?0+$/, ""));
} else if (typeof value === "boolean") { } else if (typeof value === "boolean") {
buffer.push(value.toString()); buffer.push(value.toString());
} else if (value instanceof Dict) { } else if (value instanceof Dict) {

View File

@ -190,9 +190,11 @@ class Builder {
if (hasNamespace) { if (hasNamespace) {
this._currentNamespace = this._namespaceStack.pop(); this._currentNamespace = this._namespaceStack.pop();
} }
prefixes?.forEach(({ prefix }) => { if (prefixes) {
this._namespacePrefixes.get(prefix).pop(); prefixes.forEach(({ prefix }) => {
}); this._namespacePrefixes.get(prefix).pop();
});
}
if (nsAgnostic) { if (nsAgnostic) {
this._nsAgnosticLevel--; this._nsAgnosticLevel--;
} }

View File

@ -154,7 +154,10 @@ class FontFinder {
function selectFont(xfaFont, typeface) { function selectFont(xfaFont, typeface) {
if (xfaFont.posture === "italic") { if (xfaFont.posture === "italic") {
return xfaFont.weight === "bold" ? typeface.bolditalic : typeface.italic; if (xfaFont.weight === "bold") {
return typeface.bolditalic;
}
return typeface.italic;
} else if (xfaFont.weight === "bold") { } else if (xfaFont.weight === "bold") {
return typeface.bold; return typeface.bold;
} }

View File

@ -28,7 +28,6 @@ import {
import { createValidAbsoluteUrl, warn } from "../../shared/util.js"; import { createValidAbsoluteUrl, warn } from "../../shared/util.js";
import { getMeasurement, stripQuotes } from "./utils.js"; import { getMeasurement, stripQuotes } from "./utils.js";
import { selectFont } from "./fonts.js"; import { selectFont } from "./fonts.js";
import { serializeFontFamily } from "../../shared/css_utils.js";
import { TextMeasure } from "./text.js"; import { TextMeasure } from "./text.js";
import { XFAObject } from "./xfa_object.js"; import { XFAObject } from "./xfa_object.js";
@ -598,16 +597,13 @@ function setFontFamily(xfaFont, node, fontFinder, style) {
} }
const name = stripQuotes(xfaFont.typeface); const name = stripQuotes(xfaFont.typeface);
// Use the same serialization as the `@font-face` rule, resp. the `FontFace` style.fontFamily = `"${name}"`;
// instance, that the font is registered with; see `createFontFaceRule` and
// `createNativeFontFace` in `src/display/font_loader.js`.
style.fontFamily = serializeFontFamily(name);
const typeface = fontFinder.find(name); const typeface = fontFinder.find(name);
if (typeface) { if (typeface) {
const { fontFamily } = typeface.regular.cssFontInfo; const { fontFamily } = typeface.regular.cssFontInfo;
if (fontFamily !== name) { if (fontFamily !== name) {
style.fontFamily = serializeFontFamily(fontFamily); style.fontFamily = `"${fontFamily}"`;
} }
const para = getCurrentPara(node); const para = getCurrentPara(node);

View File

@ -6073,9 +6073,10 @@ class Value extends XFAObject {
[$text]() { [$text]() {
if (this.exData) { if (this.exData) {
return typeof this.exData[$content] === "string" if (typeof this.exData[$content] === "string") {
? this.exData[$content].trim() return this.exData[$content].trim();
: this.exData[$content][$text]().trim(); }
return this.exData[$content][$text]().trim();
} }
for (const name of Object.getOwnPropertyNames(this)) { for (const name of Object.getOwnPropertyNames(this)) {
if (name === "image") { if (name === "image") {

View File

@ -284,9 +284,10 @@ class XFAObject {
} }
[$text]() { [$text]() {
return this[_children].length === 0 if (this[_children].length === 0) {
? this[$content] return this[$content];
: this[_children].map(c => c[$text]()).join(""); }
return this[_children].map(c => c[$text]()).join("");
} }
get [_attributeNames]() { get [_attributeNames]() {
@ -328,7 +329,11 @@ class XFAObject {
} }
[$getChildren](name = null) { [$getChildren](name = null) {
return !name ? this[_children] : this[name]; if (!name) {
return this[_children];
}
return this[name];
} }
[$dump]() { [$dump]() {
@ -675,9 +680,11 @@ class XFAObject {
} }
[$getChildren](name = null) { [$getChildren](name = null) {
return !name if (!name) {
? this[_children] return this[_children];
: this[_children].filter(c => c[$nodeName] === name); }
return this[_children].filter(c => c[$nodeName] === name);
} }
[$getChildrenByClass](name) { [$getChildrenByClass](name) {
@ -902,9 +909,11 @@ class XmlObject extends XFAObject {
} }
[$getChildren](name = null) { [$getChildren](name = null) {
return !name if (!name) {
? this[_children] return this[_children];
: this[_children].filter(c => c[$nodeName] === name); }
return this[_children].filter(c => c[$nodeName] === name);
} }
[$getAttributes]() { [$getAttributes]() {

View File

@ -49,17 +49,16 @@ function isWhitespaceString(s) {
class XMLParserBase { class XMLParserBase {
static get _entityRegex() { static get _entityRegex() {
// Entity references cannot contain "&", keeping the scan linear. return shadow(this, "_entityRegex", /&(?:#x([^;]+)|#([^;]+)|([^;]+));/g);
return shadow(this, "_entityRegex", /&(?:#x([^;&]+)|#([^;&]+)|([^;&]+));/g);
} }
_resolveEntities(s) { _resolveEntities(s) {
return s.replaceAll(XMLParserBase._entityRegex, (all, hex, dec, entity) => { return s.replaceAll(XMLParserBase._entityRegex, (_, hex, dec, entity) => {
if (hex || dec) { if (hex) {
const code = hex ? parseInt(hex, 16) : parseInt(dec, 10); return String.fromCodePoint(parseInt(hex, 16));
// An out-of-range or unparsable code point is kept as-is, since }
// `String.fromCodePoint` would throw on it. if (dec) {
return code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : all; return String.fromCodePoint(parseInt(dec, 10));
} }
switch (entity) { switch (entity) {
case "lt": case "lt":
@ -325,9 +324,10 @@ class SimpleDOMNode {
} }
get textContent() { get textContent() {
return !this.childNodes if (!this.childNodes) {
? this.nodeValue || "" return this.nodeValue || "";
: this.childNodes.map(child => child.textContent).join(""); }
return this.childNodes.map(child => child.textContent).join("");
} }
get children() { get children() {

View File

@ -75,7 +75,7 @@ const TIMEZONE_OFFSET = new Date().getTimezoneOffset() * 60 * 1000;
* @property {Object} svgFactory * @property {Object} svgFactory
* @property {boolean} [enableScripting] * @property {boolean} [enableScripting]
* @property {boolean} [hasJSActions] * @property {boolean} [hasJSActions]
* @property {Map} [fieldObjects] * @property {Object} [fieldObjects]
*/ */
class AnnotationElementFactory { class AnnotationElementFactory {
@ -803,7 +803,7 @@ class AnnotationElement {
const fields = []; const fields = [];
if (this._fieldObjects) { if (this._fieldObjects) {
const fieldObj = this._fieldObjects.get(name) || []; const fieldObj = this._fieldObjects[name] || [];
for (const { page, id, exportValues } of fieldObj) { for (const { page, id, exportValues } of fieldObj) {
if (page === -1) { if (page === -1) {
@ -1012,9 +1012,9 @@ class LinkAnnotationElement extends AnnotationElement {
} else { } else {
if ( if (
data.actions && data.actions &&
(data.actions.has("Action") || (data.actions.Action ||
data.actions.has("Mouse Up") || data.actions["Mouse Up"] ||
data.actions.has("Mouse Down")) && data.actions["Mouse Down"]) &&
this.enableScripting && this.enableScripting &&
this.hasJSActions this.hasJSActions
) { ) {
@ -1158,14 +1158,14 @@ class LinkAnnotationElement extends AnnotationElement {
* @param {Object} data * @param {Object} data
* @memberof LinkAnnotationElement * @memberof LinkAnnotationElement
*/ */
_bindJSAction(link, { actions, id, overlaidText }) { _bindJSAction(link, data) {
link.href = this.linkService.getAnchorUrl(""); link.href = this.linkService.getAnchorUrl("");
const map = new Map([ const map = new Map([
["Action", "onclick"], ["Action", "onclick"],
["Mouse Up", "onmouseup"], ["Mouse Up", "onmouseup"],
["Mouse Down", "onmousedown"], ["Mouse Down", "onmousedown"],
]); ]);
for (const name of actions.keys()) { for (const name of Object.keys(data.actions)) {
const jsName = map.get(name); const jsName = map.get(name);
if (!jsName) { if (!jsName) {
continue; continue;
@ -1173,13 +1173,16 @@ class LinkAnnotationElement extends AnnotationElement {
link[jsName] = () => { link[jsName] = () => {
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this, source: this,
detail: { id, name }, detail: {
id: data.id,
name,
},
}); });
return false; return false;
}; };
} }
if (overlaidText) { if (data.overlaidText) {
link.title = overlaidText; link.title = data.overlaidText;
} }
link.onclick ||= () => false; link.onclick ||= () => false;
@ -1217,12 +1220,12 @@ class LinkAnnotationElement extends AnnotationElement {
if (resetFormFields.length !== 0 || resetFormRefs.length !== 0) { if (resetFormFields.length !== 0 || resetFormRefs.length !== 0) {
const fieldIds = new Set(resetFormRefs); const fieldIds = new Set(resetFormRefs);
for (const fieldName of resetFormFields) { for (const fieldName of resetFormFields) {
const fields = this._fieldObjects.get(fieldName) || []; const fields = this._fieldObjects[fieldName] || [];
for (const { id } of fields) { for (const { id } of fields) {
fieldIds.add(id); fieldIds.add(id);
} }
} }
for (const fields of this._fieldObjects.values()) { for (const fields of Object.values(this._fieldObjects)) {
for (const field of fields) { for (const field of fields) {
if (fieldIds.has(field.id) === include) { if (fieldIds.has(field.id) === include) {
allFields.push(field); allFields.push(field);
@ -1230,7 +1233,7 @@ class LinkAnnotationElement extends AnnotationElement {
} }
} }
} else { } else {
for (const fields of this._fieldObjects.values()) { for (const fields of Object.values(this._fieldObjects)) {
allFields.push(...fields); allFields.push(...fields);
} }
} }
@ -1376,10 +1379,8 @@ class WidgetAnnotationElement extends AnnotationElement {
} }
_setEventListeners(element, elementData, names, getter) { _setEventListeners(element, elementData, names, getter) {
const { actions } = this.data;
for (const [baseName, eventName] of names) { for (const [baseName, eventName] of names) {
if (eventName === "Action" || actions?.has(eventName)) { if (eventName === "Action" || this.data.actions?.[eventName]) {
if (eventName === "Focus" || eventName === "Blur") { if (eventName === "Focus" || eventName === "Blur") {
elementData ||= { focused: false }; elementData ||= { focused: false };
} }
@ -1390,10 +1391,10 @@ class WidgetAnnotationElement extends AnnotationElement {
eventName, eventName,
getter getter
); );
if (eventName === "Focus" && !actions?.has("Blur")) { if (eventName === "Focus" && !this.data.actions?.Blur) {
// Ensure that elementData will have the correct value. // Ensure that elementData will have the correct value.
this._setEventListener(element, elementData, "blur", "Blur", null); this._setEventListener(element, elementData, "blur", "Blur", null);
} else if (eventName === "Blur" && !actions?.has("Focus")) { } else if (eventName === "Blur" && !this.data.actions?.Focus) {
this._setEventListener(element, elementData, "focus", "Focus", null); this._setEventListener(element, elementData, "focus", "Focus", null);
} }
} }
@ -1629,7 +1630,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
} }
elementData.lastCommittedValue = target.value; elementData.lastCommittedValue = target.value;
elementData.commitKey = 1; elementData.commitKey = 1;
if (!this.data.actions?.has("Focus")) { if (!this.data.actions?.Focus) {
elementData.focused = true; elementData.focused = true;
} }
}); });
@ -1751,7 +1752,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
if (!elementData.focused || !event.relatedTarget) { if (!elementData.focused || !event.relatedTarget) {
return; return;
} }
if (!this.data.actions?.has("Blur")) { if (!this.data.actions?.Blur) {
elementData.focused = false; elementData.focused = false;
} }
const { target } = event; const { target } = event;
@ -1799,7 +1800,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
_blurListener(event); _blurListener(event);
}); });
if (this.data.actions?.has("Keystroke")) { if (this.data.actions?.Keystroke) {
element.addEventListener("beforeinput", event => { element.addEventListener("beforeinput", event => {
elementData.lastCommittedValue = null; elementData.lastCommittedValue = null;
const { data, target } = event; const { data, target } = event;
@ -1811,21 +1812,11 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
switch (event.inputType) { switch (event.inputType) {
// https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes // https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes
case "deleteWordBackward": { case "deleteWordBackward": {
// The previous unanchored regex could take quadratic time, so const match = value
// scan backwards over the trailing non-word characters and .substring(0, selectionStart)
// then the word. .match(/\w*\W*$/);
const wordCharPattern = /\w/; if (match) {
while ( selStart -= match[0].length;
selStart > 0 &&
!wordCharPattern.test(value[selStart - 1])
) {
selStart--;
}
while (
selStart > 0 &&
wordCharPattern.test(value[selStart - 1])
) {
selStart--;
} }
break; break;
} }
@ -3132,7 +3123,10 @@ class PopupElement {
} }
get isVisible() { get isVisible() {
return !this.#commentManager && this.#container.hidden === false; if (this.#commentManager) {
return false;
}
return this.#container.hidden === false;
} }
} }
@ -3953,7 +3947,7 @@ class MediaAnnotationElement extends AnnotationElement {
* @property {boolean} [enableScripting] - Enable embedded script execution. * @property {boolean} [enableScripting] - Enable embedded script execution.
* @property {boolean} [hasJSActions] - Some fields have JS actions. * @property {boolean} [hasJSActions] - Some fields have JS actions.
* The default value is `false`. * The default value is `false`.
* @property {Map<string, Array<Object>> | null} [fieldObjects] * @property {Object<string, Array<Object>> | null} [fieldObjects]
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] * @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
* @property {TextAccessibilityManager} [accessibilityManager] * @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationEditorUIManager} [annotationEditorUIManager] * @property {AnnotationEditorUIManager} [annotationEditorUIManager]

View File

@ -867,8 +867,8 @@ class PDFDocumentProxy {
} }
/** /**
* @returns {Promise<Map | null>} A promise that is resolved with a {Map} with * @returns {Promise<Object | null>} A promise that is resolved with
* the JavaScript actions: * an {Object} with the JavaScript actions:
* - from the name tree. * - from the name tree.
* - from A or AA entries in the catalog dictionary. * - from A or AA entries in the catalog dictionary.
* , or `null` if no JavaScript exists. * , or `null` if no JavaScript exists.
@ -1009,15 +1009,11 @@ class PDFDocumentProxy {
/** /**
* @param {Array<PageInfo>} pageInfos - The pages to extract. * @param {Array<PageInfo>} pageInfos - The pages to extract.
* @param {Int32Array} [copyLevels] - For each viewer page, its rank among the
* extracted pages sharing the same source page, or -1 if it isn't extracted.
* This routes editor annotations when the viewer contains multiple copies
* of a source page.
* @returns {Promise<Uint8Array>} A promise that is resolved with a * @returns {Promise<Uint8Array>} A promise that is resolved with a
* {Uint8Array} containing the full data of the saved document. * {Uint8Array} containing the full data of the saved document.
*/ */
extractPages(pageInfos, copyLevels = null) { extractPages(pageInfos) {
return this._transport.extractPages(pageInfos, copyLevels); return this._transport.extractPages(pageInfos);
} }
/** /**
@ -1073,9 +1069,9 @@ class PDFDocumentProxy {
} }
/** /**
* @returns {Promise<Map<string, Array<Object>> | null>} A promise that is * @returns {Promise<Object<string, Array<Object>> | null>} A promise that is
* resolved with a {Map} containing /AcroForm field data for the JS sandbox, * resolved with an {Object} containing /AcroForm field data for the JS
* or `null` when no field data is present in the PDF file. * sandbox, or `null` when no field data is present in the PDF file.
*/ */
getFieldObjects() { getFieldObjects() {
return this._transport.getFieldObjects(); return this._transport.getFieldObjects();
@ -1433,8 +1429,8 @@ class PDFPageProxy {
} }
/** /**
* @returns {Promise<Map | null>} A promise that is resolved with a {Map} with * @returns {Promise<Object>} A promise that is resolved with an
* the JavaScript actions, or `null` if no JavaScript exists. * {Object} with JS actions.
*/ */
getJSActions() { getJSActions() {
return this._transport.getPageJSActions(this._pageIndex); return this._transport.getPageJSActions(this._pageIndex);
@ -2940,7 +2936,7 @@ class WorkerTransport {
}); });
} }
extractPages(pageInfos, copyLevels = null) { extractPages(pageInfos) {
const params = { const params = {
pageInfos, pageInfos,
}; };
@ -2967,8 +2963,6 @@ class WorkerTransport {
// Annotation pageIndex tracks the editor's current viewer position; the // Annotation pageIndex tracks the editor's current viewer position; the
// worker keys lookups by source index. Remap UI -> source via pagesMapper // worker keys lookups by source index. Remap UI -> source via pagesMapper
// so reorganized pages still receive their annotations after extraction. // so reorganized pages still receive their annotations after extraction.
// Multiple viewer pages can share a source page. The copy level routes
// each editor annotation to the corresponding extracted copy.
const mapping = this.pagesMapper.getMapping(); const mapping = this.pagesMapper.getMapping();
if (mapping) { if (mapping) {
const remapped = new Map(); const remapped = new Map();
@ -2978,13 +2972,9 @@ class WorkerTransport {
v.pageIndex >= 0 && v.pageIndex >= 0 &&
v.pageIndex < mapping.length v.pageIndex < mapping.length
) { ) {
// copyLevels uses -1 for non-extracted pages. Keep their entries
// because an extracted stamp may share their bitmapId; the worker
// uses the negative level to skip the annotation itself.
const copyLevel = copyLevels?.[v.pageIndex] ?? 0;
const sourceIdx = mapping[v.pageIndex] - 1; const sourceIdx = mapping[v.pageIndex] - 1;
if (sourceIdx !== v.pageIndex || copyLevel !== 0) { if (sourceIdx !== v.pageIndex) {
remapped.set(k, { ...v, pageIndex: sourceIdx, copyLevel }); remapped.set(k, { ...v, pageIndex: sourceIdx });
continue; continue;
} }
} }

View File

@ -188,7 +188,10 @@ class CanvasBBoxTracker {
} }
getOpenMarker() { getOpenMarker() {
return this._savesStack.length === 0 ? null : this._savesStack.at(-1); if (this._savesStack.length === 0) {
return null;
}
return this._savesStack.at(-1);
} }
recordCloseMarker(opIdx, onSavePopped) { recordCloseMarker(opIdx, onSavePopped) {

View File

@ -188,22 +188,10 @@ function getPdfFilenameFromUrl(url, defaultFilename = "document.pdf") {
} }
if (newURL.hash) { if (newURL.hash) {
// Locate the last ".pdf" and then extend it to the left, up to the closest const reFilename = /[^/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
// separator. Both steps are linear, whereas a single pattern starting with const hashFilename = reFilename.exec(newURL.hash);
// `[^/?#=]+` is quadratic on a hash which contains no ".pdf" at all. if (hashFilename) {
const { hash } = newURL; return decode(hashFilename[0]);
let extensionStart = -1;
for (const { index } of hash.matchAll(/\.pdf\b/gi)) {
extensionStart = index;
}
if (extensionStart > 0) {
let filenameStart = extensionStart;
while (filenameStart > 0 && !"/?#=".includes(hash[filenameStart - 1])) {
filenameStart--;
}
if (filenameStart < extensionStart) {
return decode(hash.slice(filenameStart, extensionStart + 4));
}
} }
} }

View File

@ -641,8 +641,7 @@ class DrawLayer {
textLayerData.selectionDiv = div; textLayerData.selectionDiv = div;
} }
if (drawLayer.#parent && div.parentNode !== drawLayer.#parent) { if (!div.parentNode && drawLayer.#parent) {
// The div can still be in a canvas wrapper which has been removed.
drawLayer.#parent.append(div); drawLayer.#parent.append(div);
this.#selections.add(div); this.#selections.add(div);
} }

View File

@ -131,15 +131,17 @@ class AltText {
} }
isEmpty() { isEmpty() {
return this.#useNewAltTextFlow if (this.#useNewAltTextFlow) {
? this.#altText === null return this.#altText === null;
: !this.#altText && !this.#altTextDecorative; }
return !this.#altText && !this.#altTextDecorative;
} }
hasData() { hasData() {
return this.#useNewAltTextFlow if (this.#useNewAltTextFlow) {
? this.#altText !== null || !!this.#guessedText return this.#altText !== null || !!this.#guessedText;
: this.isEmpty(); }
return this.isEmpty();
} }
get guessedText() { get guessedText() {

View File

@ -622,7 +622,10 @@ class InkDrawOutline extends Outline {
} }
updateProperty(name, value) { updateProperty(name, value) {
return name === "stroke-width" ? this.#updateThickness(value) : null; if (name === "stroke-width") {
return this.#updateThickness(value);
}
return null;
} }
#updateThickness(thickness) { #updateThickness(thickness) {

View File

@ -1359,7 +1359,16 @@ class AnnotationEditor {
bindEvents(this, div, ["keydown", "pointerdown", "dblclick"]); bindEvents(this, div, ["keydown", "pointerdown", "dblclick"]);
this.#addTouchManager(); if (this.isResizable && this._uiManager._supportsPinchToZoom) {
this.#touchManager ||= new TouchManager({
container: div,
isPinchingDisabled: () => !this.isSelected,
onPinchStart: this.#touchPinchStartCallback.bind(this),
onPinching: this.#touchPinchCallback.bind(this),
onPinchEnd: this.#touchPinchEndCallback.bind(this),
signal: this._uiManager._signal,
});
}
this.addStandaloneCommentButton(); this.addStandaloneCommentButton();
this._uiManager._editorUndoBar?.hide(); this._uiManager._editorUndoBar?.hide();
@ -1794,25 +1803,6 @@ class AnnotationEditor {
this.div.addEventListener("focusout", this.focusout.bind(this), { signal }); this.div.addEventListener("focusout", this.focusout.bind(this), { signal });
} }
#addTouchManager() {
if (
this.#touchManager ||
!this.div ||
!this.isResizable ||
!this._uiManager._supportsPinchToZoom
) {
return;
}
this.#touchManager = new TouchManager({
container: this.div,
isPinchingDisabled: () => !this.isSelected,
onPinchStart: this.#touchPinchStartCallback.bind(this),
onPinching: this.#touchPinchCallback.bind(this),
onPinchEnd: this.#touchPinchEndCallback.bind(this),
signal: this._uiManager._signal,
});
}
/** /**
* Rebuild the editor in case it has been removed on undo. * Rebuild the editor in case it has been removed on undo.
* *
@ -1820,7 +1810,6 @@ class AnnotationEditor {
*/ */
rebuild() { rebuild() {
this.#addFocusListeners(); this.#addFocusListeners();
this.#addTouchManager();
} }
/** /**

View File

@ -254,9 +254,10 @@ class SignatureEditor extends DrawingEditor {
/** @inheritdoc */ /** @inheritdoc */
get toolbarButtons() { get toolbarButtons() {
return this._uiManager.signatureManager if (this._uiManager.signatureManager) {
? [["editSignature", this._uiManager.signatureManager]] return [["editSignature", this._uiManager.signatureManager]];
: super.toolbarButtons; }
return super.toolbarButtons;
} }
addSignature(data, heightInPage, description, uuid) { addSignature(data, heightInPage, description, uuid) {

View File

@ -748,8 +748,6 @@ class AnnotationEditorUIManager {
#savedAllLayers = null; #savedAllLayers = null;
#savedEditorsByPage = null;
#altTextManager = null; #altTextManager = null;
#annotationStorage = null; #annotationStorage = null;
@ -1915,7 +1913,7 @@ class AnnotationEditorUIManager {
} }
updatePageIndex(oldPageIndex, newPageIndex) { updatePageIndex(oldPageIndex, newPageIndex) {
for (const editor of this.#savedEditorsByPage.get(oldPageIndex) || []) { for (const editor of this.getEditors(oldPageIndex)) {
editor.pageIndex = newPageIndex; editor.pageIndex = newPageIndex;
} }
const layer = this.#savedAllLayers.get(oldPageIndex); const layer = this.#savedAllLayers.get(oldPageIndex);
@ -1933,32 +1931,10 @@ class AnnotationEditorUIManager {
startUpdatePages() { startUpdatePages() {
this.#savedAllLayers = new Map(this.#allLayers); this.#savedAllLayers = new Map(this.#allLayers);
this.#allLayers.clear(); this.#allLayers.clear();
const savedEditorsByPage = (this.#savedEditorsByPage = new Map());
const saveEditor = editor => {
savedEditorsByPage
.getOrInsertComputed(editor.pageIndex, makeArr)
.push(editor);
};
for (const editor of this.#allEditors.values()) {
saveEditor(editor);
}
// Clones are initially kept serialized until their editor layer is
// rendered, hence they're not present in #allEditors yet.
for (const [id, editor] of this.#annotationStorage) {
if (
id.startsWith(AnnotationEditorPrefix) &&
!this.#allEditors.has(id) &&
Number.isInteger(editor?.pageIndex)
) {
saveEditor(editor);
}
}
} }
endUpdatePages() { endUpdatePages() {
this.#savedAllLayers = null; this.#savedAllLayers = null;
this.#savedEditorsByPage = null;
} }
clonePage(pageIndex, newPageIndex) { clonePage(pageIndex, newPageIndex) {

View File

@ -22,7 +22,6 @@ import {
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { makePathFromDrawOPS } from "./display_utils.js"; import { makePathFromDrawOPS } from "./display_utils.js";
import { serializeFontFamily } from "../shared/css_utils.js";
class FontLoader { class FontLoader {
#systemFonts = new Set(); #systemFonts = new Set();
@ -440,7 +439,7 @@ class FontFaceObject {
css.style = `oblique ${this.cssFontInfo.italicAngle}deg`; css.style = `oblique ${this.cssFontInfo.italicAngle}deg`;
} }
nativeFontFace = new FontFace( nativeFontFace = new FontFace(
serializeFontFamily(this.cssFontInfo.fontFamily), this.cssFontInfo.fontFamily,
this.data, this.data,
css css
); );
@ -464,10 +463,7 @@ class FontFaceObject {
if (this.cssFontInfo.italicAngle) { if (this.cssFontInfo.italicAngle) {
css += `font-style: oblique ${this.cssFontInfo.italicAngle}deg;`; css += `font-style: oblique ${this.cssFontInfo.italicAngle}deg;`;
} }
// The font family originates from the PDF document, hence it must be rule = `@font-face {font-family:"${this.cssFontInfo.fontFamily}";${css}src:${url}}`;
// serialized as a <string> to prevent arbitrary rule injection.
const fontFamily = serializeFontFamily(this.cssFontInfo.fontFamily);
rule = `@font-face {font-family:${fontFamily};${css}src:${url}}`;
} }
this._inspectFont?.(this, url); this._inspectFont?.(this, url);

View File

@ -25,7 +25,6 @@ import {
ensureResponseOrigin, ensureResponseOrigin,
extractFilenameFromHeader, extractFilenameFromHeader,
getResponseOrigin, getResponseOrigin,
trimHeadersEnd,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
} from "./network_utils.js"; } from "./network_utils.js";
import { endRequests } from "./transport_stream.js"; import { endRequests } from "./transport_stream.js";
@ -209,7 +208,9 @@ class PDFNetworkStreamReader extends BasePDFStreamReader {
const rawResponseHeaders = fullRequestXhr.getAllResponseHeaders(); const rawResponseHeaders = fullRequestXhr.getAllResponseHeaders();
const responseHeaders = new Headers( const responseHeaders = new Headers(
rawResponseHeaders rawResponseHeaders
? trimHeadersEnd(rawResponseHeaders.trimStart()) ? rawResponseHeaders
.trimStart()
.replace(/[^\S ]+$/, "") // Not `trimEnd`, to keep regular spaces.
.split(/[\r\n]+/) .split(/[\r\n]+/)
.map(x => { .map(x => {
const [key, ...val] = x.split(": "); const [key, ...val] = x.split(": ");

View File

@ -32,17 +32,6 @@ function createHeaders(isHttp, httpHeaders) {
return headers; return headers;
} }
// Trim the trailing whitespace of the raw response headers, but keep the
// regular spaces (hence no `trimEnd`). Scanning backwards keeps this linear,
// whereas a `$`-anchored regex is quadratic in the length of the run.
function trimHeadersEnd(str) {
let end = str.length;
while (end > 0 && str[end - 1] !== " " && /\s/.test(str[end - 1])) {
end--;
}
return str.slice(0, end);
}
function getResponseOrigin(url) { function getResponseOrigin(url) {
// Notably, null is distinct from "null" string (e.g. from file:-URLs). // Notably, null is distinct from "null" string (e.g. from file:-URLs).
return URL.parse(url)?.origin ?? null; return URL.parse(url)?.origin ?? null;
@ -128,6 +117,5 @@ export {
ensureResponseOrigin, ensureResponseOrigin,
extractFilenameFromHeader, extractFilenameFromHeader,
getResponseOrigin, getResponseOrigin,
trimHeadersEnd,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
}; };

View File

@ -111,6 +111,7 @@ class PagesMapper {
movePages(selectedPages, pagesToMove, index) { movePages(selectedPages, pagesToMove, index) {
this.#ensureInit(); this.#ensureInit();
const pageNumberToId = this.#pageNumberToId; const pageNumberToId = this.#pageNumberToId;
const prevIdToPageNumber = this.#buildIdToPageNumber();
const movedCount = pagesToMove.length; const movedCount = pagesToMove.length;
const mappedPagesToMove = new Uint32Array(movedCount); const mappedPagesToMove = new Uint32Array(movedCount);
let removedBeforeTarget = 0; let removedBeforeTarget = 0;
@ -125,7 +126,6 @@ class PagesMapper {
const pagesNumber = this.#pagesNumber; const pagesNumber = this.#pagesNumber;
const remainingLen = pagesNumber - movedCount; const remainingLen = pagesNumber - movedCount;
const prevPageNumbers = new Int32Array(pagesNumber);
const adjustedTarget = MathClamp( const adjustedTarget = MathClamp(
index - removedBeforeTarget, index - removedBeforeTarget,
0, 0,
@ -135,8 +135,7 @@ class PagesMapper {
// Compact: keep only non-moved pages. // Compact: keep only non-moved pages.
for (let i = 0, r = 0; i < pagesNumber; i++) { for (let i = 0, r = 0; i < pagesNumber; i++) {
if (!selectedPages.has(i + 1)) { if (!selectedPages.has(i + 1)) {
pageNumberToId[r] = pageNumberToId[i]; pageNumberToId[r++] = pageNumberToId[i];
prevPageNumbers[r++] = i + 1;
} }
} }
@ -147,13 +146,8 @@ class PagesMapper {
remainingLen remainingLen
); );
pageNumberToId.set(mappedPagesToMove, adjustedTarget); pageNumberToId.set(mappedPagesToMove, adjustedTarget);
prevPageNumbers.copyWithin(
adjustedTarget + movedCount, this.#updatePrevPageNumbers(prevIdToPageNumber);
adjustedTarget,
remainingLen
);
prevPageNumbers.set(pagesToMove, adjustedTarget);
this.#prevPageNumbers = prevPageNumbers;
if (pageNumberToId.every((id, i) => id === i + 1)) { if (pageNumberToId.every((id, i) => id === i + 1)) {
this.#pageNumberToId = null; this.#pageNumberToId = null;
@ -316,48 +310,12 @@ class PagesMapper {
return this.#pageNumberToId !== null; return this.#pageNumberToId !== null;
} }
/**
* Builds the copy level of every page. Extracted pages are ranked, in page
* order, among the extracted pages sharing their page ID. Pages which aren't
* extracted get -1.
* @param {Array<number>} [extractedPageNumbers] - Sorted 1-based page
* numbers to extract, or null to extract everything.
* @returns {Int32Array|null} null when the page mapping is the identity.
*/
#buildCopyLevels(extractedPageNumbers = null) {
if (!this.#pageNumberToId) {
return null;
}
const copyLevels = new Int32Array(this.#pagesNumber).fill(-1);
const counts = new Map();
if (extractedPageNumbers) {
for (const pageNumber of extractedPageNumbers) {
const id = this.getPageId(pageNumber);
const level = counts.get(id) ?? 0;
counts.set(id, level + 1);
copyLevels[pageNumber - 1] = level;
}
} else {
for (let i = 0, ii = this.#pagesNumber; i < ii; i++) {
const id = this.#pageNumberToId[i];
const level = counts.get(id) ?? 0;
counts.set(id, level + 1);
copyLevels[i] = level;
}
}
return copyLevels;
}
/** /**
* Gets the current page mapping suitable for saving. * Gets the current page mapping suitable for saving.
* @param {Map<number, Array<number>>} [idToPageNumber] * @param {Map<number, Array<number>>} [idToPageNumber]
* @param {Int32Array} [copyLevels] * @returns {Array<Object>}
* @returns {{pageInfos: Array<Object>, copyLevels: Int32Array|null}}
*/ */
getPageMappingForSaving( getPageMappingForSaving(idToPageNumber = null) {
idToPageNumber = null,
copyLevels = this.#buildCopyLevels()
) {
idToPageNumber ??= this.#buildIdToPageNumber(); idToPageNumber ??= this.#buildIdToPageNumber();
// idToPageNumber maps used 1-based IDs to 1-based page numbers. // idToPageNumber maps used 1-based IDs to 1-based page numbers.
// For example if the final pdf contains page 3 twice and they are moved at // For example if the final pdf contains page 3 twice and they are moved at
@ -406,7 +364,7 @@ class PagesMapper {
} }
} }
return { pageInfos: extractParams, copyLevels }; return extractParams;
} }
extractPages(extractedPageNumbers) { extractPages(extractedPageNumbers) {
@ -419,10 +377,7 @@ class PagesMapper {
const usedPageNumbers = usedIds.getOrInsertComputed(id, makeArr); const usedPageNumbers = usedIds.getOrInsertComputed(id, makeArr);
usedPageNumbers.push(i + 1); usedPageNumbers.push(i + 1);
} }
return this.getPageMappingForSaving( return this.getPageMappingForSaving(usedIds);
usedIds,
this.#buildCopyLevels(extractedPageNumbers)
);
} }
/** /**

View File

@ -15,10 +15,6 @@
import { OutputScale, stopEvent } from "./display_utils.js"; import { OutputScale, stopEvent } from "./display_utils.js";
function preventDefault(evt) {
evt.preventDefault();
}
class TouchManager { class TouchManager {
#container; #container;
@ -139,13 +135,8 @@ class TouchManager {
opt.capture = true; opt.capture = true;
container.addEventListener("pointerdown", stopEvent, opt); container.addEventListener("pointerdown", stopEvent, opt);
container.addEventListener("pointermove", stopEvent, opt); container.addEventListener("pointermove", stopEvent, opt);
// `pointerup` and `pointercancel` are only default-prevented: a container.addEventListener("pointercancel", stopEvent, opt);
// `stopPropagation` in the capture phase also skips the bubble-phase container.addEventListener("pointerup", stopEvent, opt);
// listeners of the very node it's called on, hence swallowing them here
// would prevent any session in flight, e.g. an editor being resized, from
// ever being ended.
container.addEventListener("pointercancel", preventDefault, opt);
container.addEventListener("pointerup", preventDefault, opt);
this.#onPinchStart?.(); this.#onPinchStart?.();
} }
@ -198,7 +189,7 @@ class TouchManager {
const pDistance = Math.hypot(prevGapX, prevGapY) || 1; const pDistance = Math.hypot(prevGapX, prevGapY) || 1;
if ( if (
!this.#isPinching && !this.#isPinching &&
Math.abs(pDistance - distance) <= this.MIN_TOUCH_DISTANCE_TO_PINCH Math.abs(pDistance - distance) <= TouchManager.MIN_TOUCH_DISTANCE_TO_PINCH
) { ) {
return; return;
} }
@ -216,12 +207,7 @@ class TouchManager {
return; return;
} }
// The distances are in screen CSS pixels, but the origin must be in client const origin = [(screen0X + screen1X) / 2, (screen0Y + screen1Y) / 2];
// coordinates, like the one coming from a wheel event.
const origin = [
(touch0.clientX + touch1.clientX) / 2,
(touch0.clientY + touch1.clientY) / 2,
];
this.#onPinching?.(origin, pDistance, distance); this.#onPinching?.(origin, pDistance, distance);
} }

View File

@ -130,12 +130,18 @@ export class SandboxSupportBase {
} }
this.win.alert(cMsg); this.win.alert(cMsg);
}, },
confirm: cMsg => confirm: cMsg => {
typeof cMsg !== "string" ? false : this.win.confirm(cMsg), if (typeof cMsg !== "string") {
prompt: (cQuestion, cDefault) => return false;
typeof cQuestion !== "string" || typeof cDefault !== "string" }
? null return this.win.confirm(cMsg);
: this.win.prompt(cQuestion, cDefault), },
prompt: (cQuestion, cDefault) => {
if (typeof cQuestion !== "string" || typeof cDefault !== "string") {
return null;
}
return this.win.prompt(cQuestion, cDefault);
},
parseURL: cUrl => { parseURL: cUrl => {
const url = new this.win.URL(cUrl); const url = new this.win.URL(cUrl);
const props = [ const props = [

View File

@ -19,9 +19,7 @@ class SandboxSupport extends SandboxSupportBase {
exportValueToSandbox(val) { exportValueToSandbox(val) {
// The communication with the Quickjs sandbox is based on strings // The communication with the Quickjs sandbox is based on strings
// So we use JSON.stringfy to serialize // So we use JSON.stringfy to serialize
return JSON.stringify(val, (k, v) => return JSON.stringify(val);
v instanceof Map ? Object.fromEntries(v) : v
);
} }
importValueFromSandbox(val) { importValueFromSandbox(val) {
@ -67,7 +65,7 @@ class Sandbox {
let success = false; let success = false;
let buf = 0; let buf = 0;
try { try {
const sandboxData = this.support.exportValueToSandbox(data); const sandboxData = JSON.stringify(data);
// "pdfjsScripting.initSandbox..." MUST be the last line to be evaluated // "pdfjsScripting.initSandbox..." MUST be the last line to be evaluated
// since the returned value is used for the communication. // since the returned value is used for the communication.
code.push(`pdfjsScripting.initSandbox({ data: ${sandboxData} })`); code.push(`pdfjsScripting.initSandbox({ data: ${sandboxData} })`);

View File

@ -52,9 +52,11 @@ class AForm {
} }
AFMergeChange(event = globalThis.event) { AFMergeChange(event = globalThis.event) {
return event.willCommit if (event.willCommit) {
? event.value.toString() return event.value.toString();
: this._app._eventDispatcher.mergeChange(event); }
return this._app._eventDispatcher.mergeChange(event);
} }
AFParseDateEx(cString, cOrder) { AFParseDateEx(cString, cOrder) {
@ -100,7 +102,10 @@ class AForm {
} }
AFMakeArrayFromList(string) { AFMakeArrayFromList(string) {
return typeof string === "string" ? string.split(/, ?/g) : string; if (typeof string === "string") {
return string.split(/, ?/g);
}
return string;
} }
AFNumber_Format( AFNumber_Format(
@ -611,9 +616,11 @@ class AForm {
} }
AFExactMatch(rePatterns, str) { AFExactMatch(rePatterns, str) {
return rePatterns instanceof RegExp if (rePatterns instanceof RegExp) {
? str.match(rePatterns)?.[0] === str || 0 return str.match(rePatterns)?.[0] === str || 0;
: rePatterns.findIndex(re => str.match(re)?.[0] === str) + 1; }
return rePatterns.findIndex(re => str.match(re)?.[0] === str) + 1;
} }
} }

View File

@ -21,8 +21,8 @@ const FieldType = {
time: 4, time: 4,
}; };
function createMap(val) { function createActionsMap(actions) {
return val instanceof Map ? val : new Map(val ? Object.entries(val) : null); return new Map(actions ? Object.entries(actions) : null);
} }
function getFieldType(actions) { function getFieldType(actions) {
@ -30,8 +30,10 @@ function getFieldType(actions) {
if (!format) { if (!format) {
return FieldType.none; return FieldType.none;
} }
format = format[0].trim();
format = format[0];
format = format.trim();
if (format.startsWith("AFNumber_")) { if (format.startsWith("AFNumber_")) {
return FieldType.number; return FieldType.number;
} }
@ -47,4 +49,4 @@ function getFieldType(actions) {
return FieldType.none; return FieldType.none;
} }
export { createMap, FieldType, getFieldType }; export { createActionsMap, FieldType, getFieldType };

View File

@ -14,7 +14,7 @@
*/ */
import { makeArr, makeMap, serializeError } from "./app_utils.js"; import { makeArr, makeMap, serializeError } from "./app_utils.js";
import { createMap } from "./common.js"; import { createActionsMap } from "./common.js";
import { PDFObject } from "./pdf_object.js"; import { PDFObject } from "./pdf_object.js";
import { PrintParams } from "./print_params.js"; import { PrintParams } from "./print_params.js";
import { ZoomType } from "./constants.js"; import { ZoomType } from "./constants.js";
@ -98,7 +98,7 @@ class Doc extends PDFObject {
this._zoomType = ZoomType.none; this._zoomType = ZoomType.none;
this._zoom = data.zoom || 100; this._zoom = data.zoom || 100;
this._actions = createMap(data.actions); this._actions = createActionsMap(data.actions);
this._globalEval = data.globalEval; this._globalEval = data.globalEval;
this._userActivation = false; this._userActivation = false;
this._disablePrinting = false; this._disablePrinting = false;
@ -173,9 +173,9 @@ class Doc extends PDFObject {
_dispatchPageEvent(name, actions, pageNumber) { _dispatchPageEvent(name, actions, pageNumber) {
if (name === "PageOpen") { if (name === "PageOpen") {
this.#pageActions ??= new Map(); this.#pageActions ??= new Map();
this.#pageActions.getOrInsertComputed(pageNumber, () => if (!this.#pageActions.has(pageNumber)) {
createMap(actions) this.#pageActions.set(pageNumber, createActionsMap(actions));
); }
this._pageNum = pageNumber - 1; this._pageNum = pageNumber - 1;
} }

View File

@ -13,10 +13,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { createMap, FieldType, getFieldType } from "./common.js"; import { createActionsMap, FieldType, getFieldType } from "./common.js";
import { makeArr, serializeError } from "./app_utils.js";
import { Color } from "./color.js"; import { Color } from "./color.js";
import { PDFObject } from "./pdf_object.js"; import { PDFObject } from "./pdf_object.js";
import { serializeError } from "./app_utils.js";
class Field extends PDFObject { class Field extends PDFObject {
constructor(data) { constructor(data) {
@ -65,7 +65,7 @@ class Field extends PDFObject {
this.userName = data.userName; this.userName = data.userName;
// Private // Private
this._actions = createMap(data.actions); this._actions = createActionsMap(data.actions);
this._browseForFileToSubmit = data.browseForFileToSubmit || null; this._browseForFileToSubmit = data.browseForFileToSubmit || null;
this._buttonCaption = null; this._buttonCaption = null;
this._buttonIcon = null; this._buttonIcon = null;
@ -98,7 +98,10 @@ class Field extends PDFObject {
} }
get currentValueIndices() { get currentValueIndices() {
return !this._isChoice ? 0 : this._currentValueIndices; if (!this._isChoice) {
return 0;
}
return this._currentValueIndices;
} }
set currentValueIndices(indices) { set currentValueIndices(indices) {
@ -496,7 +499,10 @@ class Field extends PDFObject {
if (typeof cTrigger !== "string" || typeof cScript !== "string") { if (typeof cTrigger !== "string" || typeof cScript !== "string") {
return; return;
} }
this._actions.getOrInsertComputed(cTrigger, makeArr).push(cScript); if (!(cTrigger in this._actions)) {
this._actions[cTrigger] = [];
}
this._actions[cTrigger].push(cScript);
} }
setFocus() { setFocus() {
@ -578,7 +584,7 @@ class RadioButtonField extends Field {
for (const radioData of otherButtons) { for (const radioData of otherButtons) {
this.exportValues.push(radioData.exportValues); this.exportValues.push(radioData.exportValues);
this._radioIds.push(radioData.id); this._radioIds.push(radioData.id);
this._radioActions.push(createMap(radioData.actions)); this._radioActions.push(createActionsMap(radioData.actions));
if (this._value === radioData.exportValues) { if (this._value === radioData.exportValues) {
this._id = radioData.id; this._id = radioData.id;
} }
@ -677,13 +683,17 @@ class CheckboxField extends RadioButtonField {
} }
isBoxChecked(nWidget) { isBoxChecked(nWidget) {
return this._value === "Off" ? false : super.isBoxChecked(nWidget); if (this._value === "Off") {
return false;
}
return super.isBoxChecked(nWidget);
} }
isDefaultChecked(nWidget) { isDefaultChecked(nWidget) {
return this.defaultValue === "Off" if (this.defaultValue === "Off") {
? this._value === "Off" return this._value === "Off";
: super.isDefaultChecked(nWidget); }
return super.isDefaultChecked(nWidget);
} }
checkThisBox(nWidget, bCheckIt = true) { checkThisBox(nWidget, bCheckIt = true) {

View File

@ -32,7 +32,6 @@ import { AForm } from "./aform.js";
import { App } from "./app.js"; import { App } from "./app.js";
import { Color } from "./color.js"; import { Color } from "./color.js";
import { Console } from "./console.js"; import { Console } from "./console.js";
import { createMap } from "./common.js";
import { Doc } from "./doc.js"; import { Doc } from "./doc.js";
import { ProxyHandler } from "./proxy.js"; import { ProxyHandler } from "./proxy.js";
import { serializeError } from "./app_utils.js"; import { serializeError } from "./app_utils.js";
@ -71,57 +70,61 @@ function initSandbox(params) {
const util = new Util({ externalCall }); const util = new Util({ externalCall });
const appObjects = app._objects; const appObjects = app._objects;
for (const [name, objs] of createMap(data.objects)) { if (data.objects) {
const annotations = []; const annotations = [];
let container = null;
for (const obj of objs) { for (const [name, objs] of Object.entries(data.objects)) {
if (obj.type !== "") { annotations.length = 0;
annotations.push(obj); let container = null;
} else {
container = obj;
}
}
let obj = container; for (const obj of objs) {
if (annotations.length > 0) { if (obj.type !== "") {
obj = annotations[0]; annotations.push(obj);
obj.send = send; } else {
} container = obj;
obj.globalEval = globalEval;
obj.doc = _document;
obj.fieldPath = name;
obj.appObjects = appObjects;
obj.util = util;
const otherFields = annotations.slice(1);
let field;
switch (obj.type) {
case "radiobutton": {
field = new RadioButtonField(otherFields, obj);
break;
}
case "checkbox": {
field = new CheckboxField(otherFields, obj);
break;
}
default:
if (otherFields.length > 0) {
obj.siblings = otherFields.map(x => x.id);
} }
field = new Field(obj); }
}
const wrapped = new Proxy(field, proxyHandler); let obj = container;
const _object = { obj: field, wrapped }; if (annotations.length > 0) {
doc._addField(name, _object); obj = annotations[0];
for (const object of objs) { obj.send = send;
appObjects[object.id] = _object; }
}
if (container) { obj.globalEval = globalEval;
appObjects[container.id] = _object; obj.doc = _document;
obj.fieldPath = name;
obj.appObjects = appObjects;
obj.util = util;
const otherFields = annotations.slice(1);
let field;
switch (obj.type) {
case "radiobutton": {
field = new RadioButtonField(otherFields, obj);
break;
}
case "checkbox": {
field = new CheckboxField(otherFields, obj);
break;
}
default:
if (otherFields.length > 0) {
obj.siblings = otherFields.map(x => x.id);
}
field = new Field(obj);
}
const wrapped = new Proxy(field, proxyHandler);
const _object = { obj: field, wrapped };
doc._addField(name, _object);
for (const object of objs) {
appObjects[object.id] = _object;
}
if (container) {
appObjects[container.id] = _object;
}
} }
} }

View File

@ -43,10 +43,13 @@ class ProxyHandler {
} }
set(obj, prop, value) { set(obj, prop, value) {
// If the field is a container for other fields then dispatch the kids. if (obj._kidIds) {
obj._kidIds?.forEach(id => { // If the field is a container for other fields then
obj._appObjects[id].wrapped[prop] = value; // dispatch the kids.
}); obj._kidIds.forEach(id => {
obj._appObjects[id].wrapped[prop] = value;
});
}
if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) { if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) {
const old = obj[prop]; const old = obj[prop];

View File

@ -252,9 +252,10 @@ class Util extends PDFObject {
const patterns = const patterns =
/(mmmm|mmm|mm|m|dddd|ddd|dd|d|yyyy|yy|HH|H|hh|h|MM|M|ss|s|tt|t|\\.)/g; /(mmmm|mmm|mm|m|dddd|ddd|dd|d|yyyy|yy|HH|H|hh|h|MM|M|ss|s|tt|t|\\.)/g;
return cFormat.replaceAll(patterns, function (match, pattern) { return cFormat.replaceAll(patterns, function (match, pattern) {
return pattern in handlers if (pattern in handlers) {
? handlers[pattern](data) return handlers[pattern](data);
: pattern.charCodeAt(1); }
return pattern.charCodeAt(1);
}); });
} }

View File

@ -1,76 +0,0 @@
/* Copyright 2026 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.
*/
const CONTROL_CHAR_REGEXP = /\p{Cc}/u;
/**
* Checks if the given value already is a well-formed CSS <string>, i.e. a
* value which can be used verbatim since it cannot introduce delimiters.
* See https://drafts.csswg.org/css-syntax/#string-token-diagram.
* @param {string} str
* @returns {boolean}
*/
function isCSSString(str) {
const quote = str[0];
if (
str.length < 2 ||
(quote !== `"` && quote !== `'`) ||
str.at(-1) !== quote
) {
return false;
}
const end = str.length - 1;
for (let i = 1; i < end; i++) {
const char = str[i];
if (char === quote || CONTROL_CHAR_REGEXP.test(char)) {
return false;
}
if (char === "\\") {
// Skip the escaped character. A trailing backslash would instead escape
// the closing quote, and control characters must not occur in a CSS
// <string> even when escaped this way.
if (++i >= end || CONTROL_CHAR_REGEXP.test(str[i])) {
return false;
}
}
}
return true;
}
/**
* Serializes a font family, originating from the PDF document, such that it
* can safely be interpolated into CSS.
* @param {string} fontFamily
* @returns {string}
*/
function serializeFontFamily(fontFamily) {
if (isCSSString(fontFamily)) {
return fontFamily;
}
// Always emit a <string>, rather than a <custom-ident> sequence, since both
// denote the same family name but only the former cannot be mistaken for a
// generic family (e.g. `serif`) or a CSS-wide keyword (e.g. `inherit`);
// those are not valid font family names and would be ignored.
// Control characters use hexadecimal escapes, since CSS line terminators
// cannot be escaped by simply prefixing them with a backslash.
const escaped = fontFamily.replaceAll(/["\\\p{Cc}]/gu, char =>
char === `"` || char === "\\"
? `\\${char}`
: `\\${char.codePointAt(0).toString(16)} `
);
return `"${escaped}"`;
}
export { CONTROL_CHAR_REGEXP, serializeFontFamily };

View File

@ -1007,9 +1007,10 @@ class Driver {
} }
_getLastPageNumber(task) { _getLastPageNumber(task) {
return !task.pdfDoc if (!task.pdfDoc) {
? task.firstPage || 1 return task.firstPage || 1;
: task.lastPage || task.pdfDoc.numPages; }
return task.lastPage || task.pdfDoc.numPages;
} }
_nextPage(task, loadError) { _nextPage(task, loadError) {

View File

@ -31,12 +31,10 @@ import {
kbUndo, kbUndo,
loadAndWait, loadAndWait,
moveEditor, moveEditor,
pinch,
scrollIntoView, scrollIntoView,
selectEditor, selectEditor,
selectEditors, selectEditors,
switchToEditor, switchToEditor,
unselectEditor,
waitForAnnotationModeChanged, waitForAnnotationModeChanged,
waitForNoElement, waitForNoElement,
waitForPointerUp, waitForPointerUp,
@ -67,26 +65,6 @@ const drawLine = async (page, x0, y0, x1, y1) => {
await awaitPromise(clickHandle); await awaitPromise(clickHandle);
}; };
// Draw an editor large enough to have room for two fingers on it, and leave it
// selected since that's what makes it resizable with a touchscreen.
const drawAndSelectEditor = async page => {
await switchToInk(page);
const { x, y, width, height } = await getRect(page, ".annotationEditorLayer");
await drawLine(
page,
x + 0.15 * width,
y + 0.1 * height,
x + 0.75 * width,
y + 0.35 * height
);
await commit(page);
return {
layer: { x, y, width, height },
editor: await getRect(page, getEditorSelector(0)),
};
};
describe("Ink Editor", () => { describe("Ink Editor", () => {
describe("Basic operations", () => { describe("Basic operations", () => {
let pages; let pages;
@ -1429,148 +1407,3 @@ describe("Ink must be committed when the document is saved", () => {
); );
}); });
}); });
describe("Pinch to resize a drawing", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must keep resizing a drawing which came back with an undo", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToInk(page);
const rect = await getRect(page, ".annotationEditorLayer");
await drawLine(
page,
rect.x + 200,
rect.y + 200,
rect.x + 300,
rect.y + 260
);
await commit(page);
const editorSelector = getEditorSelector(0);
await clearAll(page);
await waitForNoElement(page, editorSelector);
await kbUndo(page);
await page.waitForSelector(editorSelector);
await selectEditor(page, editorSelector);
const before = await getRect(page, editorSelector);
const startGap = Math.min(before.width, before.height) * 0.2;
const endGap = Math.max(before.width, before.height) * 1.8;
await pinch(page, {
centerX: before.x + before.width / 2,
centerY: before.y + before.height / 2,
startGap,
endGap,
});
const { width: after } = await getRect(page, editorSelector);
expect(after)
.withContext(`In ${browserName}`)
.toBeGreaterThan(before.width);
})
);
});
});
describe("Resize with a touchscreen", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the resize session is ended when the finger is lifted while another one is down", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const { layer } = await drawAndSelectEditor(page);
// Grabbing a resizer starts a resize session, which disables the
// pointer events of the editor layer until the finger is lifted.
const resizer = await getRect(
page,
`${getEditorSelector(0)} .resizer.bottomRight`
);
await pinch(page, {
steps: 0,
startPoints: [
{
x: resizer.x + resizer.width / 2,
y: resizer.y + resizer.height / 2,
},
{
x: layer.x + 0.3 * layer.width,
y: layer.y + 0.9 * layer.height,
},
],
afterFirstStart: () =>
page.waitForSelector(".annotationEditorLayer.disabled"),
afterFirstEnd: () =>
// The `pointerup` of the resizing finger must still be dispatched,
// else the resize session would never end and the editor would keep
// being resized by the plain mouse moves coming afterwards.
page.waitForSelector(".annotationEditorLayer:not(.disabled)"),
});
})
);
});
});
describe("Tap after a two-finger gesture", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the tap following a two-finger gesture selects the editor", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const { layer, editor } = await drawAndSelectEditor(page);
const editorSelector = getEditorSelector(0);
// One finger on the editor, which starts a drag session, and a second
// one elsewhere on the page, which turns it into a two-finger gesture.
await pinch(page, {
steps: 0,
startPoints: [
{
x: editor.x + 0.5 * editor.width,
y: editor.y + 0.8 * editor.height,
},
{
x: layer.x + 0.3 * layer.width,
y: layer.y + 0.9 * layer.height,
},
],
});
// Once every finger is up, no listener may be left behind to swallow
// the `pointerdown` of the next tap on the editor.
await unselectEditor(page, editorSelector);
await page.touchscreen.tap(
editor.x + 0.5 * editor.width,
editor.y + 0.8 * editor.height
);
await waitForSelectedEditor(page, editorSelector);
})
);
});
});

View File

@ -815,13 +815,19 @@ describe("Reorganize Pages View", () => {
await page.waitForSelector("#viewsManagerStatusActionButton", { await page.waitForSelector("#viewsManagerStatusActionButton", {
visible: true, visible: true,
}); });
// Drag-and-drop behavior is covered separately. Use an exact move const rect1 = await getRect(page, getThumbnailSelector(1));
// here so this test only checks the save payload. const rect2 = await getRect(page, getThumbnailSelector(2));
await movePages(page, [1], 2);
await dragAndDrop(
page,
getThumbnailSelector(1),
[[0, rect2.y - rect1.y + rect2.height / 2]],
10
);
const handleSave = await createPromise(page, resolve => { const handleSave = await createPromise(page, resolve => {
window.PDFViewerApplication.onSavePages = async ({ data }) => { window.PDFViewerApplication.onSavePages = async ({ data }) => {
resolve(Array.from(data.pageInfos[0].pageIndices)); resolve(Array.from(data[0].pageIndices));
}; };
}); });
@ -2026,12 +2032,9 @@ describe("Reorganize Pages View", () => {
const pagesData = await awaitPromise(handleExport); const pagesData = await awaitPromise(handleExport);
expect(pagesData) expect(pagesData)
.withContext(`In ${browserName}`) .withContext(`In ${browserName}`)
.toEqual({ .toEqual([
pageInfos: [ { document: null, pageIndices: [0, 1], includePages: [0, 2] },
{ document: null, pageIndices: [0, 1], includePages: [0, 2] }, ]);
],
copyLevels: null,
});
await waitForTextToBe(page, labelSelector, "Select pages"); await waitForTextToBe(page, labelSelector, "Select pages");
// All checkboxes should be unchecked. // All checkboxes should be unchecked.
@ -2862,7 +2865,7 @@ describe("Reorganize Pages View", () => {
await closePages(pages); await closePages(pages);
}); });
it("should keep an ink annotation on a pasted and moved page", async () => { it("should check that the pasted page has an ink annotation in the DOM", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
// Enable ink editor mode and draw a line on page 1. // Enable ink editor mode and draw a line on page 1.
@ -2896,43 +2899,20 @@ describe("Reorganize Pages View", () => {
// Both the original and the cloned annotation must now be in storage. // Both the original and the cloned annotation must now be in storage.
await waitForStorageEntries(page, 2); await waitForStorageEntries(page, 2);
const editorIds = await page.evaluate(() => { // Close the reorganize view and navigate to page 3 (the pasted copy)
const entries = Array.from( // to trigger rendering of its annotation editor layer.
window.PDFViewerApplication.pdfDocument.annotationStorage
);
return {
original: entries.find(([, editor]) => editor.pageIndex === 0)[0],
clone: entries.find(([, editor]) => editor.pageIndex === 2)[0],
};
});
// Move the pasted copy before the original and verify that both
// stored page indices follow the new page order.
await movePages(page, [3], 0);
const editorPageIndices = await page.evaluate(ids => {
const storage =
window.PDFViewerApplication.pdfDocument.annotationStorage;
return {
original: storage.getRawValue(ids.original).pageIndex,
clone: storage.getRawValue(ids.clone).pageIndex,
};
}, editorIds);
expect(editorPageIndices)
.withContext(`In ${browserName}`)
.toEqual({ original: 1, clone: 0 });
// Show the moved copy and verify that its cloned editor survived.
await page.click("#viewsManagerToggleButton"); await page.click("#viewsManagerToggleButton");
await page.waitForSelector("#viewsManager", { hidden: true }); await page.waitForSelector("#viewsManager", { hidden: true });
await page.evaluate(() => { await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.currentPageNumber = 1; window.PDFViewerApplication.pdfViewer.currentPageNumber = 3;
}); });
await page.waitForSelector(`.page[data-page-number="1"] .inkEditor`, { // The cloned ink annotation must appear in the DOM of page 3.
await page.waitForSelector(`.page[data-page-number="3"] .inkEditor`, {
visible: true, visible: true,
}); });
const inkEditors = await page.$$( const inkEditors = await page.$$(
`.page[data-page-number="1"] .inkEditor` `.page[data-page-number="3"] .inkEditor`
); );
expect(inkEditors.length).withContext(`In ${browserName}`).toBe(1); expect(inkEditors.length).withContext(`In ${browserName}`).toBe(1);
}) })

View File

@ -1204,69 +1204,6 @@ describe("Interaction", () => {
); );
}); });
it("must efficiently delete a word from a large field", async () => {
const nonWordLength = 200000;
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForScripting(page);
const result = await page.$eval(
getSelector("27R"),
(element, length) => {
element.value = `${"!".repeat(length)}a`;
element.setSelectionRange(
element.value.length,
element.value.length
);
const eventBus = window.PDFViewerApplication.eventBus;
const eventBusPrototype = Object.getPrototypeOf(eventBus);
const originalDispatch = eventBusPrototype.dispatch;
let selection;
eventBusPrototype.dispatch = function (eventName, data) {
if (
this === eventBus &&
eventName === "dispatcheventinsandbox"
) {
const { selEnd, selStart } = data.detail;
selection = { selEnd, selStart };
return;
}
originalDispatch.call(this, eventName, data);
};
const event = new InputEvent("beforeinput", {
bubbles: true,
cancelable: true,
inputType: "deleteWordBackward",
});
try {
const startTime = performance.now();
element.dispatchEvent(event);
return {
duration: performance.now() - startTime,
selection,
};
} finally {
eventBusPrototype.dispatch = originalDispatch;
}
},
nonWordLength
);
expect(result.selection)
.withContext(`In ${browserName}`)
.toEqual({
selEnd: nonWordLength + 1,
selStart: nonWordLength,
});
expect(result.duration)
.withContext(`In ${browserName}`)
.toBeLessThan(1000);
})
);
});
it("must check that an infinite loop is not triggered", async () => { it("must check that an infinite loop is not triggered", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {

View File

@ -600,77 +600,6 @@ async function dragAndDrop(page, selector, translations, steps = 1) {
await page.waitForSelector("#viewer:not(.noUserSelect)"); await page.waitForSelector("#viewer:not(.noUserSelect)");
} }
// Move two fingers, horizontally centered on (centerX, centerY), from startGap
// to endGap: it's a pinch out when endGap is larger than startGap.
// Keep in mind that `TouchManager` starts to pinch only once the distance
// between the two fingers changed by more than `MIN_TOUCH_DISTANCE_TO_PINCH`
// (35 CSS pixels), hence the first moves are swallowed and the resulting zoom
// factor is smaller than endGap / startGap.
// Explicit start/end points can be used for tests which need an asymmetric
// gesture, or hooks around each touch lifetime.
async function pinch(
page,
{
afterEnd = null,
afterFirstEnd = null,
afterFirstStart = null,
afterStart = null,
beforeEnd = null,
centerX = 0,
centerY = 0,
centerDeltaX = 0,
centerDeltaY = 0,
startGap = 0,
endGap = startGap,
endPoints = null,
startPoints = null,
steps = 12,
}
) {
const normalizePoint = point =>
Array.isArray(point) ? { x: point[0], y: point[1] } : point;
const start = (
startPoints || [
{ x: centerX - startGap, y: centerY },
{ x: centerX + startGap, y: centerY },
]
).map(normalizePoint);
let end;
if (endPoints) {
end = endPoints.map(normalizePoint);
} else if (startPoints) {
end = start;
} else {
end = [
{ x: centerX + centerDeltaX - endGap, y: centerY + centerDeltaY },
{ x: centerX + centerDeltaX + endGap, y: centerY + centerDeltaY },
];
}
const finger0 = await page.touchscreen.touchStart(start[0].x, start[0].y);
await afterFirstStart?.(finger0);
const finger1 = await page.touchscreen.touchStart(start[1].x, start[1].y);
await afterStart?.([finger0, finger1]);
for (let i = 1; i <= steps; i++) {
const t = i / steps;
await finger0.move(
start[0].x + (end[0].x - start[0].x) * t,
start[0].y + (end[0].y - start[0].y) * t
);
await finger1.move(
start[1].x + (end[1].x - start[1].x) * t,
start[1].y + (end[1].y - start[1].y) * t
);
}
await beforeEnd?.([finger0, finger1]);
await finger0.end();
await afterFirstEnd?.([finger0, finger1]);
await finger1.end();
await afterEnd?.([finger0, finger1]);
}
function waitForPageChanging(page) { function waitForPageChanging(page) {
return createPromise(page, resolve => { return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on("pagechanging", resolve, { window.PDFViewerApplication.eventBus.on("pagechanging", resolve, {
@ -1234,7 +1163,6 @@ export {
paste, paste,
pasteFromClipboard, pasteFromClipboard,
PDI, PDI,
pinch,
scrollIntoView, scrollIntoView,
selectEditor, selectEditor,
selectEditors, selectEditors,

View File

@ -20,11 +20,9 @@
import { import {
closePages, closePages,
closeSinglePage, closeSinglePage,
firstPageOnTop,
getSpanRectFromText, getSpanRectFromText,
kbSelectAll, kbSelectAll,
loadAndWait, loadAndWait,
scrollIntoView,
waitForEvent, waitForEvent,
} from "./test_utils.mjs"; } from "./test_utils.mjs";
import { MathClamp } from "../../src/shared/math_clamp.js"; import { MathClamp } from "../../src/shared/math_clamp.js";
@ -1150,94 +1148,6 @@ describe("Text layer", () => {
); );
}); });
}); });
describe("when the page has been destroyed and rendered again", () => {
const selectionSelector = ".canvasWrapper .selection svg path[d]";
const timeout = 5000;
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
`.page[data-page-number = "1"] .endOfContent`,
undefined,
undefined,
{ annotationEditorMode: -1 }
);
});
afterEach(async () => {
await closePages(pages);
});
async function selectSomeText(page) {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"(frequently executed) bytecode sequences, records"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"them, and compiles them to fast native code. We call such a se-"
).then(belowEndPosition),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
}
it("must draw the selection", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await selectSomeText(page);
await page.waitForSelector(selectionSelector, { timeout });
await page.evaluate(() => {
document.getSelection().removeAllRanges();
});
await page.waitForSelector(selectionSelector, {
hidden: true,
timeout,
});
// Scroll down, page by page, until the first page view is
// evicted from the buffer: its canvas wrapper is then removed.
const pagesCount = await page.evaluate(
() => window.PDFViewerApplication.pagesCount
);
let isDestroyed = false;
for (let i = 2; i <= pagesCount && !isDestroyed; i++) {
const selector = `.page[data-page-number = "${i}"]`;
await scrollIntoView(page, selector);
await page.waitForSelector(
`${selector} .canvasWrapper canvas`,
{ timeout: 0 }
);
isDestroyed = !(await page.$(
`.page[data-page-number = "1"] .canvasWrapper`
));
}
expect(isDestroyed)
.withContext(`In ${browserName}, first page destroyed`)
.toBeTrue();
await firstPageOnTop(page);
await page.waitForSelector(
`.page[data-page-number = "1"] .canvasWrapper canvas`,
{ timeout: 0 }
);
await selectSomeText(page);
await page.waitForSelector(selectionSelector, { timeout });
})
);
});
});
}); });
describe("using selection carets", () => { describe("using selection carets", () => {

View File

@ -327,80 +327,6 @@ describe("PDF Thumbnail View", () => {
}) })
); );
}); });
it("must move to the previous item after pressing the End key", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
await enableMenuItems(page);
await kbFocusNext(page, "#viewsManagerStatusActionButton");
await page.keyboard.press("Enter");
await waitForMenu(page, "#viewsManagerStatusActionButton");
await page.waitForSelector("#viewsManagerStatusActionCopy:focus", {
visible: true,
});
// Move to the second menu item.
await page.keyboard.press("ArrowDown");
await page.waitForSelector("#viewsManagerStatusActionCut:focus", {
visible: true,
});
// Jump to the last menu item.
await page.keyboard.press("End");
await page.waitForSelector("#viewsManagerStatusActionExport:focus", {
visible: true,
});
// The focus must move relative to the last menu item, and not
// relative to the item that was focused before pressing End.
await page.keyboard.press("ArrowUp");
await page.waitForSelector("#viewsManagerStatusActionDelete:focus", {
visible: true,
});
})
);
});
it("must move to the next item after pressing the Home key", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
await enableMenuItems(page);
await kbFocusNext(page, "#viewsManagerStatusActionButton");
await page.keyboard.press("Enter");
await waitForMenu(page, "#viewsManagerStatusActionButton");
await page.waitForSelector("#viewsManagerStatusActionCopy:focus", {
visible: true,
});
// Wrap around to the last menu item.
await page.keyboard.press("ArrowUp");
await page.waitForSelector("#viewsManagerStatusActionExport:focus", {
visible: true,
});
// Jump to the first menu item.
await page.keyboard.press("Home");
await page.waitForSelector("#viewsManagerStatusActionCopy:focus", {
visible: true,
});
// The focus must move relative to the first menu item, and not
// relative to the item that was focused before pressing Home.
await page.keyboard.press("ArrowDown");
await page.waitForSelector("#viewsManagerStatusActionCut:focus", {
visible: true,
});
})
);
});
}); });
describe("Checkbox accessibility", () => { describe("Checkbox accessibility", () => {

View File

@ -20,7 +20,6 @@ import {
getRect, getRect,
getSpanRectFromText, getSpanRectFromText,
loadAndWait, loadAndWait,
pinch,
scrollIntoView, scrollIntoView,
showViewsManager, showViewsManager,
waitAndClick, waitAndClick,
@ -1528,11 +1527,7 @@ describe("PDF viewer", () => {
beforeEach(async () => { beforeEach(async () => {
pages = await loadAndWait( pages = await loadAndWait(
"tracemonkey.pdf", "tracemonkey.pdf",
`.page[data-page-number = "1"] .endOfContent`, `.page[data-page-number = "1"] .endOfContent`
// Pin the zoom: the drift checked below is proportional to the zoom
// level reached at the end of the pinch, and the default `page-fit`
// depends on the size of the window.
50
); );
}); });
@ -1540,9 +1535,20 @@ describe("PDF viewer", () => {
await closePages(pages); await closePages(pages);
}); });
it("keeps the content under the pinch center fixed on the screen", async () => { it("keeps the content under the pinch centre fixed on the screen", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
if (browserName === "firefox") {
pending(
"Touch events are not supported on devices without touch screen in Firefox."
);
}
if (browserName === "chrome") {
pending(
"Pinch zoom emulation is not supported for WebDriver BiDi in Chrome."
);
}
const rect = await getSpanRectFromText(page, 1, "type-stable"); const rect = await getSpanRectFromText(page, 1, "type-stable");
const originX = rect.x + rect.width / 2; const originX = rect.x + rect.width / 2;
const originY = rect.y + rect.height / 2; const originY = rect.y + rect.height / 2;
@ -1558,17 +1564,14 @@ describe("PDF viewer", () => {
}; };
window.PDFViewerApplication.eventBus.on("textlayerrendered", cb); window.PDFViewerApplication.eventBus.on("textlayerrendered", cb);
}); });
// Spread the two fingers from 50 to 200 pixels apart: the first const client = await page.target().createCDPSession();
// moves are swallowed until the distance between them changed by await client.send("Input.synthesizePinchGesture", {
// more than 35 pixels, hence a zoom factor of about 200/85 = 2.4. x: originX,
await pinch(page, { y: originY,
centerX: originX, scaleFactor: 3,
centerY: originY, gestureSourceType: "touch",
startGap: 25,
endGap: 100,
}); });
await awaitPromise(rendered); await awaitPromise(rendered);
const spanHandle = await page.evaluateHandle(() => const spanHandle = await page.evaluateHandle(() =>
Array.from( Array.from(
document.querySelectorAll( document.querySelectorAll(
@ -1576,21 +1579,7 @@ describe("PDF viewer", () => {
) )
).find(span => span.textContent.includes("type-stable")) ).find(span => span.textContent.includes("type-stable"))
); );
expect(await spanHandle.isIntersectingViewport()) expect(await spanHandle.isIntersectingViewport()).toBeTrue();
.withContext(`In ${browserName}`)
.toBeTrue();
// The text which was under the fingers must still be at the same
// height: only vertically because a page which is larger than its
// container isn't centered in it anymore.
// A few pixels are tolerated because the origin is preserved by
// scrolling: Chrome snaps the scroll offsets to the device pixels and
// the discarded fractions show up as a small drift. It's exact in
// Firefox, which keeps them.
const newRect = await getSpanRectFromText(page, 1, "type-stable");
expect(Math.abs(newRect.y + newRect.height / 2 - originY))
.withContext(`In ${browserName}`)
.toBeLessThan(5);
}) })
); );
}); });

View File

@ -2460,14 +2460,17 @@ describe("annotation", function () {
annotationGlobalsMock, annotationGlobalsMock,
idFactoryMock idFactoryMock
); );
const { actions } = await annotation.getFieldObject(); const fieldObject = await annotation.getFieldObject();
expect(actions).toEqual( const actions = fieldObject.actions;
new Map([ expect(actions["Mouse Enter"]).toEqual(["hello()"]);
["Mouse Enter", ["hello()"]], expect(actions["Mouse Exit"]).toEqual([
["Mouse Exit", ["world()", "olleh()", "foo()", "dlrow()", "oof()"]], "world()",
["Mouse Down", ["bar()"]], "olleh()",
]) "foo()",
); "dlrow()",
"oof()",
]);
expect(actions["Mouse Down"]).toEqual(["bar()"]);
}); });
it("should save Japanese text", async function () { it("should save Japanese text", async function () {
@ -3725,7 +3728,7 @@ describe("annotation", function () {
); );
expect(data.annotationType).toEqual(AnnotationType.WIDGET); expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.pushButton).toBeTrue(); expect(data.pushButton).toBeTrue();
expect(data.actions.get("Action")).toEqual(["do_something();"]); expect(data.actions.Action).toEqual(["do_something();"]);
}); });
it("should handle push buttons that act as a tooltip only", async function () { it("should handle push buttons that act as a tooltip only", async function () {

View File

@ -986,9 +986,9 @@ describe("api", function () {
expect(pdfDocument.numPages).toEqual(1); expect(pdfDocument.numPages).toEqual(1);
const jsActions = await pdfDocument.getJSActions(); const jsActions = await pdfDocument.getJSActions();
expect(jsActions).toEqual( expect(jsActions).toEqual({
new Map([["OpenAction", ["func=function(){app.alert(1)};func();"]]]) OpenAction: ["func=function(){app.alert(1)};func();"],
); });
const page = await pdfDocument.getPage(1); const page = await pdfDocument.getPage(1);
expect(page).toBeInstanceOf(PDFPageProxy); expect(page).toBeInstanceOf(PDFPageProxy);
@ -1943,17 +1943,12 @@ describe("api", function () {
// PDF document with "JavaScript" action in the OpenAction dictionary. // PDF document with "JavaScript" action in the OpenAction dictionary.
const loadingTask = getDocument(buildGetDocumentParams("issue6106.pdf")); const loadingTask = getDocument(buildGetDocumentParams("issue6106.pdf"));
const pdfDoc = await loadingTask.promise; const pdfDoc = await loadingTask.promise;
const jsActions = await pdfDoc.getJSActions(); const { OpenAction } = await pdfDoc.getJSActions();
expect(jsActions).toEqual( expect(OpenAction).toEqual([
new Map([ "this.print({bUI:true,bSilent:false,bShrinkToFit:true});",
[ ]);
"OpenAction", expect(OpenAction[0]).toMatch(AutoPrintRegExp);
["this.print({bUI:true,bSilent:false,bShrinkToFit:true});"],
],
])
);
expect(jsActions.get("OpenAction")[0]).toMatch(AutoPrintRegExp);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -1993,27 +1988,21 @@ describe("api", function () {
const page3 = await pdfDoc.getPage(3); const page3 = await pdfDoc.getPage(3);
const page3Actions = await page3.getJSActions(); const page3Actions = await page3.getJSActions();
expect(docActions).toEqual( expect(docActions).toEqual({
new Map([ DidPrint: [`this.getField("Text2").value = "DidPrint";`],
["DidPrint", [`this.getField("Text2").value = "DidPrint";`]], DidSave: [`this.getField("Text2").value = "DidSave";`],
["DidSave", [`this.getField("Text2").value = "DidSave";`]], WillClose: [`this.getField("Text1").value = "WillClose";`],
["WillClose", [`this.getField("Text1").value = "WillClose";`]], WillPrint: [`this.getField("Text1").value = "WillPrint";`],
["WillPrint", [`this.getField("Text1").value = "WillPrint";`]], WillSave: [`this.getField("Text1").value = "WillSave";`],
["WillSave", [`this.getField("Text1").value = "WillSave";`]], });
]) expect(page1Actions).toEqual({
); PageOpen: [`this.getField("Text1").value = "PageOpen 1";`],
expect(page1Actions).toEqual( PageClose: [`this.getField("Text2").value = "PageClose 1";`],
new Map([ });
["PageOpen", [`this.getField("Text1").value = "PageOpen 1";`]], expect(page3Actions).toEqual({
["PageClose", [`this.getField("Text2").value = "PageClose 1";`]], PageOpen: [`this.getField("Text5").value = "PageOpen 3";`],
]) PageClose: [`this.getField("Text6").value = "PageClose 3";`],
); });
expect(page3Actions).toEqual(
new Map([
["PageOpen", [`this.getField("Text5").value = "PageOpen 3";`]],
["PageClose", [`this.getField("Text6").value = "PageClose 3";`]],
])
);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -2028,64 +2017,53 @@ describe("api", function () {
const pdfDoc = await loadingTask.promise; const pdfDoc = await loadingTask.promise;
const fieldObjects = await pdfDoc.getFieldObjects(); const fieldObjects = await pdfDoc.getFieldObjects();
expect(fieldObjects).toEqual( expect(fieldObjects).toEqual({
new Map([ Text1: [
[ {
"Text1", id: "25R",
[ value: "",
{ defaultValue: "",
id: "25R", multiline: false,
value: "", password: false,
defaultValue: "", charLimit: 0,
multiline: false, comb: false,
password: false, editable: true,
charLimit: 0, hidden: false,
comb: false, name: "Text1",
editable: true, rect: [24.1789, 719.66, 432.22, 741.66],
hidden: false, actions: null,
name: "Text1", page: 0,
rect: [24.1789, 719.66, 432.22, 741.66], strokeColor: null,
actions: null, fillColor: null,
page: 0, rotation: 0,
strokeColor: null, datetimeFormat: undefined,
fillColor: null, hasDatetimeHTML: false,
rotation: 0, type: "text",
datetimeFormat: undefined, },
hasDatetimeHTML: false, ],
type: "text", Button1: [
}, {
], id: "26R",
], value: "Off",
[ defaultValue: null,
"Button1", exportValues: undefined,
[ editable: true,
{ name: "Button1",
id: "26R", rect: [455.436, 719.678, 527.436, 739.678],
value: "Off", hidden: false,
defaultValue: null, actions: {
exportValues: undefined, Action: [
editable: true, `this.getField("Text1").value = this.info.authors.join("::");`,
name: "Button1", ],
rect: [455.436, 719.678, 527.436, 739.678], },
hidden: false, page: 0,
actions: new Map([ strokeColor: null,
[ fillColor: new Uint8ClampedArray([192, 192, 192]),
"Action", rotation: 0,
[ type: "button",
`this.getField("Text1").value = this.info.authors.join("::");`, },
], ],
], });
]),
page: 0,
strokeColor: null,
fillColor: new Uint8ClampedArray([192, 192, 192]),
rotation: 0,
type: "button",
},
],
],
])
);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -2095,8 +2073,8 @@ describe("api", function () {
const pdfDoc = await loadingTask.promise; const pdfDoc = await loadingTask.promise;
const fieldObjects = await pdfDoc.getFieldObjects(); const fieldObjects = await pdfDoc.getFieldObjects();
for (const [name, objs] of fieldObjects) { for (const name in fieldObjects) {
const pageIndexes = objs.map(o => o.page); const pageIndexes = fieldObjects[name].map(o => o.page);
let expected; let expected;
switch (name) { switch (name) {
@ -6308,29 +6286,6 @@ small scripts as well as for`);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
it("clones shared indirect objects reached concurrently only once", async function () {
const pdfData = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R " +
"/MediaBox [0 0 100 100] /Resources << /ExtGState " +
"<< /GS1 4 0 R /GS2 4 0 R >> >> >>\nendobj\n",
"4 0 obj\n<< /Type /ExtGState /CA 0.5 >>\nendobj\n",
]);
const loadingTask = getDocument({ data: pdfData });
const pdfDoc = await loadingTask.promise;
const data = await pdfDoc.extractPages([{ document: null }]);
expect(countMarker(data, "/Type /ExtGState")).toEqual(1);
await loadingTask.destroy();
const newLoadingTask = getDocument({ data });
const newPdfDoc = await newLoadingTask.promise;
expect(newPdfDoc.numPages).toEqual(1);
await newPdfDoc.getPage(1);
await newLoadingTask.destroy();
});
it("should merge two PDFs with page included ranges", async function () { it("should merge two PDFs with page included ranges", async function () {
const loadingTask = getDocument( const loadingTask = getDocument(
buildGetDocumentParams("tracemonkey.pdf") buildGetDocumentParams("tracemonkey.pdf")
@ -7302,14 +7257,9 @@ small scripts as well as for`);
let loadingTask = getDocument(buildGetDocumentParams("empty.pdf")); let loadingTask = getDocument(buildGetDocumentParams("empty.pdf"));
let pdfDoc = await loadingTask.promise; let pdfDoc = await loadingTask.promise;
// Mirror the viewer: copy page 1 and paste the copy right after it. // Simulate what clonePage() puts in annotationStorage when a page is
pdfDoc.pagesMapper.copyPages(new Uint32Array([1])); // copied: the original annotation stays on pageIndex 0 and the clone
pdfDoc.pagesMapper.pastePages(1); // is placed on pageIndex 1 (the new position of the pasted copy).
expect(pdfDoc.pagesMapper.pagesNumber).toEqual(2);
// Model the annotationStorage entries after clonePage(): the original
// stays at viewer page index 0 and the copy gets an entry at index 1.
// Both viewer pages map to source page 0.
const inkAnnotation = { const inkAnnotation = {
annotationType: AnnotationEditorType.INK, annotationType: AnnotationEditorType.INK,
rect: [50, 50, 200, 200], rect: [50, 50, 200, 200],
@ -7347,15 +7297,15 @@ small scripts as well as for`);
}); });
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_1", { pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_1", {
...inkAnnotation, ...inkAnnotation,
color: [255, 0, 0],
pageIndex: 1, pageIndex: 1,
isClone: true,
}); });
const { pageInfos, copyLevels } = pdfDoc.pagesMapper.extractPages( // Extract page 0 twice: once at output position 0 (original) and once
new Set([1, 2]) // at output position 1 (clone), mirroring copy+paste in the UI.
); const data = await pdfDoc.extractPages([
const data = await pdfDoc.extractPages(pageInfos, copyLevels); { document: null, includePages: [0], pageIndices: [0] },
{ document: null, includePages: [0], pageIndices: [1] },
]);
await loadingTask.destroy(); await loadingTask.destroy();
loadingTask = getDocument({ data }); loadingTask = getDocument({ data });
@ -7363,8 +7313,7 @@ small scripts as well as for`);
expect(pdfDoc.numPages).toEqual(2); expect(pdfDoc.numPages).toEqual(2);
// Each page must have the expected color and a distinct annotation ID. // Both pages should carry the ink annotation.
const annotationIds = [];
for (let i = 1; i <= 2; i++) { for (let i = 1; i <= 2; i++) {
const pdfPage = await pdfDoc.getPage(i); const pdfPage = await pdfDoc.getPage(i);
const annotations = await pdfPage.getAnnotations(); const annotations = await pdfPage.getAnnotations();
@ -7372,174 +7321,7 @@ small scripts as well as for`);
expect(annotations[0].annotationType) expect(annotations[0].annotationType)
.withContext(`Page ${i}`) .withContext(`Page ${i}`)
.toEqual(AnnotationType.INK); .toEqual(AnnotationType.INK);
expect(Array.from(annotations[0].color))
.withContext(`Page ${i}`)
.toEqual(i === 1 ? [0, 0, 255] : [255, 0, 0]);
annotationIds.push(annotations[0].id);
} }
expect(new Set(annotationIds).size).toEqual(2);
await loadingTask.destroy();
});
it("keeps distinct annotations when a clone moves before its original", async function () {
let loadingTask = getDocument(buildGetDocumentParams("empty.pdf"));
let pdfDoc = await loadingTask.promise;
pdfDoc.pagesMapper.copyPages(new Uint32Array([1]));
pdfDoc.pagesMapper.pastePages(1);
const freeText = {
annotationType: AnnotationEditorType.FREETEXT,
rect: [12, 34, 56, 78],
rotation: 0,
fontSize: 10,
color: [0, 0, 0],
};
const original = {
...freeText,
value: "on the original",
pageIndex: 0,
};
const clone = {
...freeText,
value: "on the clone",
pageIndex: 1,
isClone: true,
};
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_0", original);
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_1", clone);
// Move the clone before the original and update both editor page
// indices to match.
pdfDoc.pagesMapper.movePages(new Set([2]), [2], 0);
expect(pdfDoc.pagesMapper.getPrevPageNumber(1)).toEqual(2);
expect(pdfDoc.pagesMapper.getPrevPageNumber(2)).toEqual(1);
clone.pageIndex = 0;
original.pageIndex = 1;
const { pageInfos, copyLevels } =
pdfDoc.pagesMapper.getPageMappingForSaving();
const data = await pdfDoc.extractPages(pageInfos, copyLevels);
await loadingTask.destroy();
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(2);
for (let i = 1; i <= 2; i++) {
const pdfPage = await pdfDoc.getPage(i);
const annotations = await pdfPage.getAnnotations();
expect(annotations.map(a => a.contentsObj?.str)).toEqual([
i === 1 ? "on the clone" : "on the original",
]);
}
await loadingTask.destroy();
});
it("only keeps the annotations of the extracted copy of a page", async function () {
let loadingTask = getDocument(
buildGetDocumentParams("three_pages_with_number.pdf")
);
let pdfDoc = await loadingTask.promise;
// Copy page 1 and paste it at the end: source page 0 is now at the
// viewer positions 1 and 4.
pdfDoc.pagesMapper.copyPages(new Uint32Array([1]));
pdfDoc.pagesMapper.pastePages(3);
expect(pdfDoc.pagesMapper.pagesNumber).toEqual(4);
const freeText = {
annotationType: AnnotationEditorType.FREETEXT,
rect: [12, 34, 56, 78],
rotation: 0,
fontSize: 10,
color: [0, 0, 0],
};
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_0", {
...freeText,
value: "on the original",
pageIndex: 0,
});
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_1", {
...freeText,
value: "on the clone",
pageIndex: 3,
isClone: true,
});
// Extract the clone only: the annotation of the original must not be
// written on it.
const { pageInfos, copyLevels } = pdfDoc.pagesMapper.extractPages(
new Set([4])
);
const data = await pdfDoc.extractPages(pageInfos, copyLevels);
await loadingTask.destroy();
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(1);
const pdfPage = await pdfDoc.getPage(1);
const annotations = await pdfPage.getAnnotations();
expect(annotations.map(a => a.contentsObj?.str)).toEqual([
"on the clone",
]);
await loadingTask.destroy();
});
it("keeps a shared stamp image when extracting only a copied page", async function () {
if (isNodeJS) {
pending("Cannot create a bitmap from Node.js.");
}
const bitmap = await getImageBitmap("firefox_logo.png");
let loadingTask = getDocument(buildGetDocumentParams("empty.pdf"));
let pdfDoc = await loadingTask.promise;
pdfDoc.pagesMapper.copyPages(new Uint32Array([1]));
pdfDoc.pagesMapper.pastePages(1);
const stamp = {
annotationType: AnnotationEditorType.STAMP,
rect: [12, 34, 56, 78],
rotation: 0,
bitmapId: "im1",
};
// Model stamp serialization: both entries share a bitmapId, but only
// one carries the bitmap data.
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_0", {
...stamp,
bitmap,
pageIndex: 0,
});
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_1", {
...stamp,
pageIndex: 1,
isClone: true,
isCopy: true,
});
const { pageInfos, copyLevels } = pdfDoc.pagesMapper.extractPages(
new Set([2])
);
const data = await pdfDoc.extractPages(pageInfos, copyLevels);
expect(data).not.toBeNull();
await loadingTask.destroy();
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(1);
const pdfPage = await pdfDoc.getPage(1);
const annotations = await pdfPage.getAnnotations();
expect(annotations.length).toEqual(1);
expect(annotations[0].annotationType).toEqual(AnnotationType.STAMP);
const opList = await pdfPage.getOperatorList();
expect(opList.fnArray).toContain(OPS.paintImageXObject);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -7889,36 +7671,6 @@ small scripts as well as for`);
return fontIndex < 0 ? null : operatorList.argsArray[fontIndex][0]; return fontIndex < 0 ? null : operatorList.argsArray[fontIndex][0];
}; };
it("does not mutate source widget parents", async function () {
const pdfData = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R " +
"/AcroForm 6 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R " +
"/MediaBox [0 0 100 100] /Annots [4 0 R] >>\nendobj\n",
"4 0 obj\n<< /Type /Annot /Subtype /Widget /Rect [0 0 20 10] " +
"/Parent 5 0 R >>\nendobj\n",
"5 0 obj\n<< /FT /Tx /T (group) /Kids [4 0 R] >>\nendobj\n",
"6 0 obj\n<< /Fields [] /DA (/Helv 10 Tf) >>\nendobj\n",
]);
let loadingTask = getDocument({ data: pdfData });
let pdfDoc = await loadingTask.promise;
const data = await pdfDoc.extractPages([{ document: null }]);
const sourceAnnotations = await (
await pdfDoc.getPage(1)
).getAnnotations();
expect(sourceAnnotations[0].fieldName).toEqual("group");
expect(sourceAnnotations[0].fieldType).toEqual("Tx");
await loadingTask.destroy();
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(1);
await loadingTask.destroy();
});
it("rebuilds a missing AcroForm Fields array", async function () { it("rebuilds a missing AcroForm Fields array", async function () {
const data = assemblePdf([ const data = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>\nendobj\n", "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>\nendobj\n",
@ -7939,73 +7691,9 @@ small scripts as well as for`);
loadingTask = getDocument({ data: extracted }); loadingTask = getDocument({ data: extracted });
pdfDoc = await loadingTask.promise; pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getFieldObjects())).toEqual(["group"]);
const fieldObjects = await pdfDoc.getFieldObjects();
expect([...fieldObjects.keys()]).toEqual(["group"]);
const annotations = await (await pdfDoc.getPage(1)).getAnnotations(); const annotations = await (await pdfDoc.getPage(1)).getAnnotations();
expect(annotations[0].fieldName).toEqual("group"); expect(annotations[0].fieldName).toEqual("group");
await loadingTask.destroy();
});
it("rebuilds a missing AcroForm Fields array with a Parent cycle", async function () {
const data = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] " +
"/Annots [4 0 R] >>\nendobj\n",
"4 0 obj\n<< /Type /Annot /Subtype /Widget /Rect [0 0 20 10] " +
"/Parent 5 0 R >>\nendobj\n",
// The Parent chain 5 -> 7 -> 5 is cyclic.
"5 0 obj\n<< /FT /Tx /T (group) /Kids [4 0 R] /Parent 7 0 R >>\n" +
"endobj\n",
"6 0 obj\n<< /DA (/Helv 10 Tf) >>\nendobj\n",
"7 0 obj\n<< /T (cycle) /Kids [5 0 R] /Parent 5 0 R >>\nendobj\n",
]);
let loadingTask = getDocument({ data });
let pdfDoc = await loadingTask.promise;
const extracted = await pdfDoc.extractPages([{ document: null }]);
expect(extracted).not.toBeNull();
await loadingTask.destroy();
loadingTask = getDocument({ data: extracted });
pdfDoc = await loadingTask.promise;
const fieldObjects = await pdfDoc.getFieldObjects();
expect([...fieldObjects.keys()]).toEqual(["group"]);
const annotations = await (await pdfDoc.getPage(1)).getAnnotations();
expect(annotations[0].fieldName).toEqual("group");
await loadingTask.destroy();
});
it("rebuilds a missing AcroForm Fields array with a bogus Parent", async function () {
const data = assemblePdf([
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] " +
"/Annots [4 0 R] >>\nendobj\n",
"4 0 obj\n<< /Type /Annot /Subtype /Widget /Rect [0 0 20 10] " +
"/FT /Tx /T (widget) /Parent 5 0 R >>\nendobj\n",
// The Parent isn't a dictionary.
"5 0 obj\n[1 2 3]\nendobj\n",
"6 0 obj\n<< /DA (/Helv 10 Tf) >>\nendobj\n",
]);
let loadingTask = getDocument({ data });
let pdfDoc = await loadingTask.promise;
const extracted = await pdfDoc.extractPages([{ document: null }]);
expect(extracted).not.toBeNull();
await loadingTask.destroy();
loadingTask = getDocument({ data: extracted });
pdfDoc = await loadingTask.promise;
const annotations = await (await pdfDoc.getPage(1)).getAnnotations();
expect(annotations.length).toEqual(1);
expect(annotations[0].fieldName).toEqual("widget");
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -8033,10 +7721,7 @@ small scripts as well as for`);
loadingTask = getDocument({ data }); loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise; pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getFieldObjects())).toEqual(["field"]);
const fieldObjects = await pdfDoc.getFieldObjects();
expect([...fieldObjects.keys()]).toEqual(["field"]);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -8094,10 +7779,9 @@ small scripts as well as for`);
loadingTask = getDocument({ data }); loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise; pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getFieldObjects())).toEqual([
const fieldObjects = await pdfDoc.getFieldObjects(); "signature",
expect([...fieldObjects.keys()]).toEqual(["signature"]); ]);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -8143,7 +7827,7 @@ small scripts as well as for`);
// reflects the T entries of the fields in the AcroForm dictionary. // reflects the T entries of the fields in the AcroForm dictionary.
const fieldObjects = await pdfDoc.getFieldObjects(); const fieldObjects = await pdfDoc.getFieldObjects();
expect(fieldObjects).not.toBeNull(); expect(fieldObjects).not.toBeNull();
expect([...fieldObjects.keys()].sort()).toEqual(origFieldNames); expect(Object.keys(fieldObjects).sort()).toEqual(origFieldNames);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -8201,7 +7885,7 @@ small scripts as well as for`);
const allOrigFieldNames = [ const allOrigFieldNames = [
...new Set([...origPage1FieldNames, ...origPage2FieldNames]), ...new Set([...origPage1FieldNames, ...origPage2FieldNames]),
].sort(); ].sort();
expect([...fieldObjects.keys()].sort()).toEqual(allOrigFieldNames); expect(Object.keys(fieldObjects).sort()).toEqual(allOrigFieldNames);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -8213,8 +7897,9 @@ small scripts as well as for`);
let pdfDoc = await loadingTask.promise; let pdfDoc = await loadingTask.promise;
expect(await pdfDoc.getCalculationOrderIds()).toEqual(["6R"]); expect(await pdfDoc.getCalculationOrderIds()).toEqual(["6R"]);
const fieldObjects1 = await pdfDoc.getFieldObjects(); expect(Object.keys((await pdfDoc.getFieldObjects()) || {})).toEqual([
expect([...fieldObjects1.keys()]).toEqual(["group"]); "group",
]);
const data = await pdfDoc.extractPages([{ document: null }]); const data = await pdfDoc.extractPages([{ document: null }]);
await loadingTask.destroy(); await loadingTask.destroy();
@ -8226,8 +7911,9 @@ small scripts as well as for`);
expect(Array.isArray(calculationOrder)).toBeTrue(); expect(Array.isArray(calculationOrder)).toBeTrue();
expect(calculationOrder.length).toEqual(1); expect(calculationOrder.length).toEqual(1);
expect(calculationOrder[0]).not.toEqual("6R"); expect(calculationOrder[0]).not.toEqual("6R");
const fieldObjects2 = await pdfDoc.getFieldObjects(); expect(Object.keys((await pdfDoc.getFieldObjects()) || {})).toEqual([
expect([...fieldObjects2.keys()]).toEqual(["group"]); "group",
]);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -8270,9 +7956,9 @@ small scripts as well as for`);
loadingTask = getDocument({ data }); loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise; pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(2); expect(pdfDoc.numPages).toEqual(2);
const fieldObjects = await pdfDoc.getFieldObjects(); expect(
expect([...fieldObjects.keys()].sort()).toEqual(["first", "second"]); Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort()
).toEqual(["first", "second"]);
for (const pageNumber of [1, 2]) { for (const pageNumber of [1, 2]) {
const fontName = await getAppearanceFontName(pdfDoc, pageNumber); const fontName = await getAppearanceFontName(pdfDoc, pageNumber);
expect(fontName).not.toBeNull(); expect(fontName).not.toBeNull();
@ -8311,9 +7997,9 @@ small scripts as well as for`);
loadingTask = getDocument({ data }); loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise; pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(2); expect(pdfDoc.numPages).toEqual(2);
const fieldObjects = await pdfDoc.getFieldObjects(); expect(
expect([...fieldObjects.keys()].sort()).toEqual(["broken", "main"]); Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort()
).toEqual(["broken", "main"]);
const fontName = await getAppearanceFontName(pdfDoc, 2); const fontName = await getAppearanceFontName(pdfDoc, 2);
expect(fontName).not.toBeNull(); expect(fontName).not.toBeNull();
expect(fontName).not.toEqual("g_font_error"); expect(fontName).not.toEqual("g_font_error");
@ -8356,9 +8042,9 @@ small scripts as well as for`);
loadingTask = getDocument({ data }); loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise; pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(2); expect(pdfDoc.numPages).toEqual(2);
const fieldObjects = await pdfDoc.getFieldObjects(); expect(
expect([...fieldObjects.keys()].sort()).toEqual(["check", "main"]); Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort()
).toEqual(["check", "main"]);
const fontName = await getAppearanceFontName(pdfDoc, 2); const fontName = await getAppearanceFontName(pdfDoc, 2);
expect(fontName).not.toBeNull(); expect(fontName).not.toBeNull();
expect(fontName).not.toEqual("g_font_error"); expect(fontName).not.toEqual("g_font_error");

View File

@ -220,24 +220,4 @@ describe("autolinker", function () {
["john.doe@uni-cityname.tld", "mailto:john.doe@uni-cityname.tld"], ["john.doe@uni-cityname.tld", "mailto:john.doe@uni-cityname.tld"],
]); ]);
}); });
it("should find emails with the longest parts allowed by the RFCs", function () {
const local = "a".repeat(64);
const label = "b".repeat(63);
testLinks([[`${local}@${label}.com`, `mailto:${local}@${label}.com`]]);
});
it("shouldn't find emails with parts longer than allowed by the RFCs", function () {
expect(
Autolinker.findLinks(`${"a".repeat(107)}@${"a".repeat(80)}.com`)
).toEqual([]);
});
it("should handle a long run of characters before an @ efficiently", function () {
const text = `${"a".repeat(50000)}@`;
const startTime = performance.now();
expect(Autolinker.findLinks(text)).toEqual([]);
expect(performance.now() - startTime).toBeLessThan(1000);
});
}); });

View File

@ -26,7 +26,6 @@
"evaluator_spec.js", "evaluator_spec.js",
"event_utils_spec.js", "event_utils_spec.js",
"fetch_stream_spec.js", "fetch_stream_spec.js",
"font_loader_spec.js",
"font_substitutions_spec.js", "font_substitutions_spec.js",
"fonts_spec.js", "fonts_spec.js",
"image_utils_spec.js", "image_utils_spec.js",

View File

@ -24,7 +24,6 @@ import {
getRotationMatrix, getRotationMatrix,
getSizeInBytes, getSizeInBytes,
isWhiteSpace, isWhiteSpace,
normalizeCSSFontFamily,
numberToString, numberToString,
parseXFAPath, parseXFAPath,
recoverJsURL, recoverJsURL,
@ -244,32 +243,6 @@ describe("core_utils", function () {
{ name: "BAR", pos: 456 }, { name: "BAR", pos: 456 },
]); ]);
}); });
it("should ignore a malformed position", function () {
expect(parseXFAPath("foo[].bar[1x].oof[].[3]")).toEqual([
{ name: "foo[]", pos: 0 },
{ name: "bar[1x]", pos: 0 },
{ name: "oof[]", pos: 0 },
{ name: "[3]", pos: 0 },
]);
});
it("should keep the longest name when a component has several brackets", function () {
expect(parseXFAPath("foo[1][2]")).toEqual([{ name: "foo[1]", pos: 2 }]);
});
it("should handle a long component efficiently", function () {
// Looking for the position with a leading `.+` is quadratic in the
// length of a component which doesn't end with one.
const name = "a".repeat(200000);
const startTime = performance.now();
const parsedPath = parseXFAPath(name);
const duration = performance.now() - startTime;
expect(parsedPath).toEqual([{ name, pos: 0 }]);
expect(duration).toBeLessThan(1000);
});
}); });
describe("recoverJsURL", function () { describe("recoverJsURL", function () {
@ -352,41 +325,6 @@ describe("core_utils", function () {
}); });
}); });
describe("normalizeCSSFontFamily", function () {
it("should strip the spaces preceding a digit", function () {
expect(normalizeCSSFontFamily("Wingdings 3")).toEqual("Wingdings3");
expect(normalizeCSSFontFamily("Wingdings 3")).toEqual("Wingdings3");
expect(normalizeCSSFontFamily(" 1 2 3")).toEqual("123");
expect(normalizeCSSFontFamily("MS Gothic 2 Bold 7")).toEqual(
"MS Gothic2 Bold7"
);
});
it("should keep the spaces which don't precede a digit", function () {
expect(normalizeCSSFontFamily("")).toEqual("");
expect(normalizeCSSFontFamily("Times New Roman")).toEqual(
"Times New Roman"
);
// The runs of spaces must be preserved as-is.
expect(normalizeCSSFontFamily(" Times New Roman ")).toEqual(
" Times New Roman "
);
// A digit which isn't preceded by a space is left alone.
expect(normalizeCSSFontFamily("Wingdings3")).toEqual("Wingdings3");
});
it("should handle long runs of spaces efficiently", function () {
// Guard against a regular expression that backtracks over the spaces,
// which makes the replacement quadratic: that needs several seconds
// here, whereas a linear one needs well under a millisecond.
const fontFamily = `Wingdings${" ".repeat(100000)}`;
const startTime = performance.now();
expect(normalizeCSSFontFamily(fontFamily)).toEqual("Wingdings ");
expect(performance.now() - startTime).toBeLessThan(1000);
});
});
describe("validateCSSFont", function () { describe("validateCSSFont", function () {
it("Check font family", function () { it("Check font family", function () {
const cssFontInfo = { const cssFontInfo = {
@ -440,30 +378,6 @@ describe("core_utils", function () {
expect(validateCSSFont(cssFontInfo)).toBeFalse(); expect(validateCSSFont(cssFontInfo)).toBeFalse();
}); });
it("Check font family containing control characters", function () {
const cssFontInfo = {
fontFamily: "",
fontWeight: 0,
italicAngle: 0,
};
// A form feed is a newline in CSS, hence it terminates the <string>.
cssFontInfo.fontFamily = `"blah\fblah"`;
expect(validateCSSFont(cssFontInfo)).toBeFalse();
cssFontInfo.fontFamily = `"blah\x00blah"`;
expect(validateCSSFont(cssFontInfo)).toBeFalse();
cssFontInfo.fontFamily = `"blah\tblah"`;
expect(validateCSSFont(cssFontInfo)).toBeFalse();
cssFontInfo.fontFamily = `"blah\nblah"`;
expect(validateCSSFont(cssFontInfo)).toBeFalse();
cssFontInfo.fontFamily = `"blah blah"`;
expect(validateCSSFont(cssFontInfo)).toBeTrue();
});
it("Check font weight", function () { it("Check font weight", function () {
const cssFontInfo = { const cssFontInfo = {
fontFamily: "blah", fontFamily: "blah",

View File

@ -122,30 +122,6 @@ describe("display_utils", function () {
expect( expect(
getPdfFilenameFromUrl("http://www.example.com/pdfs/pdf.html#file2.pdf") getPdfFilenameFromUrl("http://www.example.com/pdfs/pdf.html#file2.pdf")
).toEqual("file2.pdf"); ).toEqual("file2.pdf");
// Only the last ".pdf" of the hash is used.
expect(getPdfFilenameFromUrl("/pdfs/pdfs.html#a.pdf/b.pdf")).toEqual(
"b.pdf"
);
// A ".pdf" which isn't preceded by a name is ignored.
expect(getPdfFilenameFromUrl("/pdfs/pdfs.html#=.pdf")).toEqual(
"document.pdf"
);
// An invalid last ".pdf" prevents an earlier valid one from being used.
expect(getPdfFilenameFromUrl("/pdfs/pdfs.html#a.pdf/=.pdf")).toEqual(
"document.pdf"
);
});
it("gets PDF filename from a long hash string efficiently", function () {
// Scanning the hash for a name is quadratic when it contains no ".pdf".
const url = `/pdfs/pdfs.html#${"a".repeat(200000)}`;
const startTime = performance.now();
const filename = getPdfFilenameFromUrl(url);
const duration = performance.now() - startTime;
expect(filename).toEqual("document.pdf");
expect(duration).toBeLessThan(1000);
}); });
it("gets correct PDF filename when multiple ones are present", function () { it("gets correct PDF filename when multiple ones are present", function () {

View File

@ -628,43 +628,39 @@ describe("document", function () {
const kid2BisRef = Ref.get(266, 0); const kid2BisRef = Ref.get(266, 0);
const parentRef = Ref.get(358, 0); const parentRef = Ref.get(358, 0);
const allFieldsObj = Object.create(null); const allFields = Object.create(null);
for (const name of ["parent", "kid1", "kid2", "kid11"]) { for (const name of ["parent", "kid1", "kid2", "kid11"]) {
const buttonWidgetDict = new Dict(); const buttonWidgetDict = new Dict();
buttonWidgetDict.set("Type", Name.get("Annot")); buttonWidgetDict.set("Type", Name.get("Annot"));
buttonWidgetDict.set("Subtype", Name.get("Widget")); buttonWidgetDict.set("Subtype", Name.get("Widget"));
buttonWidgetDict.set("FT", Name.get("Btn")); buttonWidgetDict.set("FT", Name.get("Btn"));
buttonWidgetDict.set("T", name); buttonWidgetDict.set("T", name);
allFieldsObj[name] = buttonWidgetDict; allFields[name] = buttonWidgetDict;
} }
allFieldsObj.kid1.set("Kids", [kid11Ref]); allFields.kid1.set("Kids", [kid11Ref]);
allFieldsObj.parent.set("Kids", [kid1Ref, kid2Ref, kid2BisRef]); allFields.parent.set("Kids", [kid1Ref, kid2Ref, kid2BisRef]);
const xref = new XRefMock([ const xref = new XRefMock([
{ ref: parentRef, data: allFieldsObj.parent }, { ref: parentRef, data: allFields.parent },
{ ref: kid1Ref, data: allFieldsObj.kid1 }, { ref: kid1Ref, data: allFields.kid1 },
{ ref: kid11Ref, data: allFieldsObj.kid11 }, { ref: kid11Ref, data: allFields.kid11 },
{ ref: kid2Ref, data: allFieldsObj.kid2 }, { ref: kid2Ref, data: allFields.kid2 },
{ ref: kid2BisRef, data: allFieldsObj.kid2 }, { ref: kid2BisRef, data: allFields.kid2 },
]); ]);
acroForm.set("Fields", [parentRef]); acroForm.set("Fields", [parentRef]);
pdfDocument = getDocument(acroForm, xref); pdfDocument = getDocument(acroForm, xref);
fields = (await pdfDocument.fieldObjects).allFields;
const { allFields, orphanFields } = await pdfDocument.fieldObjects; for (const [name, objs] of Object.entries(fields)) {
fields[name] = objs.map(obj => obj.id);
}
const objIds = Array.from(allFields.entries(), ([name, objs]) => [ expect(fields["parent.kid1"]).toEqual(["314R"]);
name, expect(fields["parent.kid1.kid11"]).toEqual(["159R"]);
objs.map(obj => obj.id), expect(fields["parent.kid2"]).toEqual(["265R", "266R"]);
]); expect(fields.parent).toEqual(["358R"]);
expect(objIds).toEqual([
["parent", ["358R"]],
["parent.kid1", ["314R"]],
["parent.kid1.kid11", ["159R"]],
["parent.kid2", ["265R", "266R"]],
]);
expect(orphanFields.size).toEqual(3);
}); });
it("should get field objects with a circular `Parent` chain", async function () { it("should get field objects with a circular `Parent` chain", async function () {
@ -697,14 +693,9 @@ describe("document", function () {
acroForm.set("Fields", [widgetRef]); acroForm.set("Fields", [widgetRef]);
const pdfDocument = getDocument(acroForm, xref); const pdfDocument = getDocument(acroForm, xref);
const { allFields, orphanFields } = await pdfDocument.fieldObjects; const { allFields } = await pdfDocument.fieldObjects;
expect(Object.keys(allFields)).toEqual([""]);
const objIds = Array.from(allFields.entries(), ([name, objs]) => [ expect(allFields[""].map(obj => obj.id)).toEqual(["1R"]);
name,
objs.map(obj => obj.id),
]);
expect(objIds).toEqual([["", ["1R"]]]);
expect(orphanFields.size).toEqual(0);
}); });
it("should check if fields have any actions", async function () { it("should check if fields have any actions", async function () {

View File

@ -1,151 +0,0 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FontFaceObject } from "../../src/display/font_loader.js";
import { isNodeJS } from "../../src/shared/util.js";
describe("font_loader", function () {
describe("FontFaceObject", function () {
function createFontFaceObject(fontFamily) {
return new FontFaceObject({
cssFontInfo: { fontFamily, fontWeight: "400", italicAngle: "0" },
data: new Uint8Array([0x00]),
disableFontFace: false,
fontExtraProperties: false,
loadedName: "g_d0_f1",
mimetype: "font/opentype",
});
}
function getFontFamily(rule) {
const start = rule.indexOf("font-family:") + "font-family:".length;
return rule.slice(start, rule.indexOf(";font-weight:", start));
}
it("creates a font-face rule", function () {
expect(
getFontFamily(createFontFaceObject("Foo-Bar").createFontFaceRule())
).toEqual(`"Foo-Bar"`);
});
it("keeps an injected rule inside the font family <string> (issue GHSA-wxrh-xgw3-3wqf)", function () {
const fontFamily = `"};body{background-image:url(https://example.com/)};a{x:"`;
const rule = createFontFaceObject(fontFamily).createFontFaceRule();
// The value already is a well-formed <string>, hence it's kept as-is.
expect(getFontFamily(rule)).toEqual(fontFamily);
});
it("serializes a font family which isn't a well-formed <string>", function () {
expect(
getFontFamily(
createFontFaceObject(String.raw`Foo"Bar\Baz`).createFontFaceRule()
)
).toEqual(String.raw`"Foo\"Bar\\Baz"`);
// A trailing backslash would otherwise escape the closing quote.
expect(
getFontFamily(
createFontFaceObject(
String.raw`"};body{background-image:url(https://example.com/)};a{x:\"`
).createFontFaceRule()
)
).toEqual(
String.raw`"\"};body{background-image:url(https://example.com/)};a{x:\\\""`
);
// A trailing backslash would otherwise escape the following semi-colon,
// thus swallowing the `font-weight` declaration.
expect(
getFontFamily(createFontFaceObject("Foo\\").createFontFaceRule())
).toEqual(String.raw`"Foo\\"`);
});
it("escapes CSS line terminators", function () {
const rule = createFontFaceObject(
"safe\f}body{background-image:url(https://example.com/)}/*"
).createFontFaceRule();
expect(rule).not.toContain("\f");
expect(getFontFamily(rule)).toEqual(
String.raw`"safe\c }body{background-image:url(https://example.com/)}/*"`
);
});
it("quotes generic families and CSS-wide keywords", function () {
// Those are not valid font family names, hence the `font-family`
// descriptor would be ignored if they were emitted unquoted.
for (const fontFamily of ["serif", "monospace", "inherit", "initial"]) {
expect(
getFontFamily(createFontFaceObject(fontFamily).createFontFaceRule())
).toEqual(`"${fontFamily}"`);
}
});
it("uses the same font family in both font loading paths", function () {
const NativeFontFace = globalThis.FontFace;
globalThis.FontFace = function MockFontFace(family) {
this.family = family;
};
try {
for (const fontFamily of [`"Foo Bar"`, "Foo-Bar", "serif"]) {
const font = createFontFaceObject(fontFamily);
expect(font.createNativeFontFace().family).toEqual(
getFontFamily(font.createFontFaceRule())
);
}
} finally {
globalThis.FontFace = NativeFontFace;
}
});
it("cannot escape the @font-face rule", function () {
if (isNodeJS) {
pending("Document is not supported in Node.js.");
}
const style = document.createElement("style");
document.head.append(style);
try {
for (const fontFamily of [
`"};body{background-image:url(https://example.com/)};a{x:"`,
String.raw`"};body{background-image:url(https://example.com/)};a{x:\"`,
"safe\f}body{background-image:url(https://example.com/)}/*",
String.raw`Foo"Bar\Baz`,
"Foo\\",
"serif",
]) {
const rule = createFontFaceObject(fontFamily).createFontFaceRule();
style.sheet.insertRule(rule, style.sheet.cssRules.length);
const cssRule = [...style.sheet.cssRules].at(-1);
expect(cssRule.constructor.name)
.withContext(fontFamily)
.toEqual("CSSFontFaceRule");
// The `font-family` descriptor must be both present and complete,
// i.e. the value must not have been truncated nor dropped.
expect(cssRule.style.getPropertyValue("font-family"))
.withContext(fontFamily)
.not.toEqual("");
}
// No additional rules were injected.
expect(style.sheet.cssRules.length).toEqual(6);
} finally {
style.remove();
}
});
});
});

View File

@ -72,7 +72,6 @@ async function initializePDFJS(callback) {
"pdfjs-test/unit/evaluator_spec.js", "pdfjs-test/unit/evaluator_spec.js",
"pdfjs-test/unit/event_utils_spec.js", "pdfjs-test/unit/event_utils_spec.js",
"pdfjs-test/unit/fetch_stream_spec.js", "pdfjs-test/unit/fetch_stream_spec.js",
"pdfjs-test/unit/font_loader_spec.js",
"pdfjs-test/unit/font_substitutions_spec.js", "pdfjs-test/unit/font_substitutions_spec.js",
"pdfjs-test/unit/fonts_spec.js", "pdfjs-test/unit/fonts_spec.js",
"pdfjs-test/unit/image_utils_spec.js", "pdfjs-test/unit/image_utils_spec.js",

View File

@ -17,7 +17,6 @@ import {
createHeaders, createHeaders,
createResponseError, createResponseError,
extractFilenameFromHeader, extractFilenameFromHeader,
trimHeadersEnd,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
} from "../../src/display/network_utils.js"; } from "../../src/display/network_utils.js";
import { ResponseException } from "../../src/shared/util.js"; import { ResponseException } from "../../src/shared/util.js";
@ -387,38 +386,4 @@ describe("network_utils", function () {
testCreateResponseError(new URL("https://foo.com/bar.pdf"), 0, false); testCreateResponseError(new URL("https://foo.com/bar.pdf"), 0, false);
}); });
}); });
describe("trimHeadersEnd", function () {
it("removes the trailing whitespace", function () {
expect(trimHeadersEnd("a: 1\r\nb: 2\r\n")).toEqual("a: 1\r\nb: 2");
expect(trimHeadersEnd("a: 1\n\n")).toEqual("a: 1");
expect(trimHeadersEnd("a: 1\t\r\n")).toEqual("a: 1");
});
it("keeps the regular spaces", function () {
expect(trimHeadersEnd("a: 1 ")).toEqual("a: 1 ");
expect(trimHeadersEnd("a: 1\r\n ")).toEqual("a: 1\r\n ");
expect(trimHeadersEnd(" ")).toEqual(" ");
});
it("handles strings without trailing whitespace", function () {
expect(trimHeadersEnd("")).toEqual("");
expect(trimHeadersEnd("a: 1")).toEqual("a: 1");
expect(trimHeadersEnd("\r\na: 1")).toEqual("\r\na: 1");
});
it("handles a long run of whitespace efficiently", function () {
// Removing the run with a `$`-anchored regex is quadratic in its length,
// and a server controls how long the headers are.
const run = "\t".repeat(100000);
const startTime = performance.now();
// The run is trailing, hence removed.
expect(trimHeadersEnd(`a: 1${run}`)).toEqual("a: 1");
// The run is followed by a non-whitespace, hence kept: this is the case
// which a regex has to backtrack over.
expect(trimHeadersEnd(`a: 1${run}b`)).toEqual(`a: 1${run}b`);
expect(performance.now() - startTime).toBeLessThan(1000);
});
});
}); });

View File

@ -363,80 +363,6 @@ describe("Scripting", function () {
}); });
}); });
it("should trigger an event added with setAction", async () => {
const refId = getId();
const data = {
objects: {
field: [
{
id: refId,
value: "",
actions: {},
type: "text",
},
],
},
appInfo: { language: "en-US", platform: "Linux x86_64" },
calculationOrder: [],
};
sandbox.createSandbox(data);
await myeval(
`(this.getField("field").setAction("test", 'event.source.value = "abc";'), 0)`
);
await sandbox.dispatchEventInSandbox({
id: refId,
value: "",
name: "test",
willCommit: true,
});
expect(send_queue.has(refId)).toBeTrue();
expect(send_queue.get(refId)).toEqual({
id: refId,
value: "abc",
});
});
it("should trigger an event appended with setAction", async () => {
const refId = getId();
const data = {
objects: {
field: [
{
id: refId,
value: "",
actions: {
test: [`event.source.value = "a";`],
},
type: "text",
},
],
},
appInfo: { language: "en-US", platform: "Linux x86_64" },
calculationOrder: [],
};
sandbox.createSandbox(data);
await myeval(
`(this.getField("field").setAction("test", 'event.source.value += "b";'), 0)`
);
await sandbox.dispatchEventInSandbox({
id: refId,
value: "",
name: "test",
willCommit: true,
});
expect(send_queue.has(refId)).toBeTrue();
expect(send_queue.get(refId)).toEqual({
id: refId,
value: "ab",
});
});
it("should trigger a Keystroke event and invalidate it", async () => { it("should trigger a Keystroke event and invalidate it", async () => {
const refId = getId(); const refId = getId();
const data = { const data = {
@ -753,10 +679,10 @@ describe("Scripting", function () {
it("should parse a date with a format", async () => { it("should parse a date with a format", async () => {
const check = async (date, format, expected) => { const check = async (date, format, expected) => {
const value = await myeval( const value = await myeval(
`AFParseDateEx("${date}", "${format}").toISOString().replace(/T.*/, "")` `AFParseDateEx("${date}", "${format}").toISOString().replace(/T.*$/, "")`
); );
expect(value).toEqual( expect(value).toEqual(
new Date(expected).toISOString().replace(/T.*/, "") new Date(expected).toISOString().replace(/T.*$/, "")
); );
}; };

View File

@ -38,9 +38,10 @@ const WASM_URL = isNodeJS
class DefaultFileReaderFactory { class DefaultFileReaderFactory {
static async fetch(params) { static async fetch(params) {
return isNodeJS if (isNodeJS) {
? fetchDataNode(params.path) return fetchDataNode(params.path);
: fetchDataDOM(params.path, /* type = */ "bytes"); }
return fetchDataDOM(params.path, /* type = */ "bytes");
} }
} }

View File

@ -295,20 +295,10 @@ describe("Writer", function () {
}); });
it("should not use scientific notation for very large numbers", async function () { it("should not use scientific notation for very large numbers", async function () {
// JavaScript's toString() and toFixed() produce scientific notation from // JavaScript produces scientific notation above ~1e21 but such values
// 1e21 on, which is invalid PDF: such a number must be written with all // are unlikely in PDFs; values below that threshold must be plain.
// its digits, which are the exact ones of the underlying double.
expect(await serialize(1e10)).toEqual("10000000000"); expect(await serialize(1e10)).toEqual("10000000000");
expect(await serialize(1.5e6)).toEqual("1500000"); expect(await serialize(1.5e6)).toEqual("1500000");
expect(await serialize(1e20)).toEqual("100000000000000000000");
expect(await serialize(1e21)).toEqual("1000000000000000000000");
// Removing the trailing zeros of the exponent used to change the value:
// "1e+30" was written "1e+3" and "1e+100" was written "1e+1".
expect(await serialize(1e30)).toEqual("1000000000000000019884624838656");
expect(await serialize(-1e30)).toEqual(
"-1000000000000000019884624838656"
);
expect((await serialize(1e100)).length).toEqual(101);
}); });
it("should round to at most 10 decimal places", async function () { it("should round to at most 10 decimal places", async function () {

View File

@ -109,27 +109,6 @@ describe("XML", function () {
}); });
}); });
describe("character references", function () {
const parseText = xml =>
new SimpleXMLParser({}).parseFromString(xml).documentElement.textContent;
it("should resolve the valid ones", function () {
expect(parseText("<a>&#65;&#x42;&#x1F602;&#0;&#x10FFFF;</a>")).toEqual(
"AB\u{1F602}\0\u{10FFFF}"
);
});
it("should keep the invalid ones as-is", function () {
// These must not throw: `String.fromCodePoint` rejects anything which
// isn't a code point.
expect(parseText("<a>&#xZZ;</a>")).toEqual("&#xZZ;");
expect(parseText("<a>&#zz;</a>")).toEqual("&#zz;");
expect(parseText("<a>&#x110000;</a>")).toEqual("&#x110000;");
expect(parseText("<a>&#1114112;</a>")).toEqual("&#1114112;");
expect(parseText("<a>&#-1;</a>")).toEqual("&#-1;");
});
});
it("should parse processing instructions", function () { it("should parse processing instructions", function () {
const xml = ` const xml = `
<a> <a>
@ -153,33 +132,4 @@ describe("XML", function () {
["foo", ""], ["foo", ""],
]); ]);
}); });
describe("entities", function () {
const parseText = xml =>
new SimpleXMLParser({}).parseFromString(xml).documentElement.textContent;
it("should resolve the entities", function () {
expect(
parseText("<a>&lt;b&gt; &amp; &quot;c&quot; &apos;d&apos;</a>")
).toEqual(`<b> & "c" 'd'`);
expect(parseText("<a>&#65;&#x42;</a>")).toEqual("AB");
});
it("should keep the unresolved entities as-is", function () {
expect(parseText("<a>&unknown; a&b;c</a>")).toEqual("&unknown; a&b;c");
});
it("should resolve an entity preceded by a bare ampersand", function () {
expect(parseText("<a>AT&T &amp; Co</a>")).toEqual("AT&T & Co");
expect(parseText("<a>&&amp;</a>")).toEqual("&&");
});
it("should handle a long run of ampersands efficiently", function () {
const text = "&".repeat(100000);
const startTime = performance.now();
expect(parseText(`<a>${text}</a>`)).toEqual(text);
expect(performance.now() - startTime).toBeLessThan(1000);
});
});
}); });

View File

@ -21,18 +21,15 @@
:root { :root {
--editor-toolbar-vert-offset: 6px; --editor-toolbar-vert-offset: 6px;
--outline-width: 2px; --outline-width: 2px;
/* Palette primitives, not the semantic light-dark tokens: these are drawn on --outline-color: #0060df;
the always-light PDF page, where --color-accent-primary and friends would
resolve to their dark value. */
--outline-color: var(--color-blue-60, #0060df);
--outline-around-width: 1px; --outline-around-width: 1px;
--outline-around-color: var(--color-gray-20, #f0f0f4); --outline-around-color: #f0f0f4;
--hover-outline-around-color: var(--outline-around-color); --hover-outline-around-color: var(--outline-around-color);
--editor-selection-outline: solid var(--outline-width) var(--outline-color); --focus-outline: solid var(--outline-width) var(--outline-color);
--unfocus-outline: solid var(--outline-width) transparent; --unfocus-outline: solid var(--outline-width) transparent;
--focus-outline-around: solid var(--outline-around-width) --focus-outline-around: solid var(--outline-around-width)
var(--outline-around-color); var(--outline-around-color);
--hover-outline-color: var(--color-gray-60, #8f8f9d); --hover-outline-color: #8f8f9d;
--hover-outline: solid var(--outline-width) var(--hover-outline-color); --hover-outline: solid var(--outline-width) var(--hover-outline-color);
--hover-outline-around: solid var(--outline-around-width) --hover-outline-around: solid var(--outline-around-width)
var(--hover-outline-around-color); var(--hover-outline-around-color);
@ -98,7 +95,7 @@
--outline-color: CanvasText; --outline-color: CanvasText;
--outline-around-color: ButtonFace; --outline-around-color: ButtonFace;
--resizer-bg-color: ButtonText; --resizer-bg-color: ButtonText;
--hover-outline-color: var(--color-accent-primary-hover, Highlight); --hover-outline-color: Highlight;
--hover-outline-around-color: SelectedItemText; --hover-outline-around-color: SelectedItemText;
} }
} }
@ -191,7 +188,7 @@
} }
&.selectedEditor { &.selectedEditor {
border: var(--editor-selection-outline); border: var(--focus-outline);
outline: var(--focus-outline-around); outline: var(--focus-outline-around);
&::before { &::before {
@ -247,51 +244,30 @@
--editor-toolbar-bg-color: light-dark(#f0f0f4, #2b2a33); --editor-toolbar-bg-color: light-dark(#f0f0f4, #2b2a33);
--editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg); --editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg);
--editor-toolbar-fg-color: light-dark(#2e2e56, #fbfbfe); --editor-toolbar-fg-color: light-dark(#2e2e56, #fbfbfe);
--editor-toolbar-border-color: var(--border-color-interactive, #8f8f9d); --editor-toolbar-border-color: #8f8f9d;
--editor-toolbar-hover-border-color: var(--editor-toolbar-border-color); --editor-toolbar-hover-border-color: var(--editor-toolbar-border-color);
--editor-toolbar-hover-bg-color: light-dark(#e0e0e6, #52525e); --editor-toolbar-hover-bg-color: light-dark(#e0e0e6, #52525e);
--editor-toolbar-hover-fg-color: var(--editor-toolbar-fg-color); --editor-toolbar-hover-fg-color: var(--editor-toolbar-fg-color);
--editor-toolbar-hover-outline: none; --editor-toolbar-hover-outline: none;
--editor-toolbar-focus-outline-color: var( --editor-toolbar-focus-outline-color: light-dark(#0060df, #0df);
--focus-outline-color,
light-dark(#0060df, #0df)
);
--editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2); --editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2);
--editor-toolbar-height: 28px; --editor-toolbar-height: 28px;
--editor-toolbar-padding: 2px; --editor-toolbar-padding: 2px;
--alt-text-done-color: var( --alt-text-done-color: light-dark(#2ac3a2, #54ffbd);
--color-accent-attention,
light-dark(#2ac3a2, #54ffbd)
);
--alt-text-warning-color: light-dark(#0090ed, #80ebff); --alt-text-warning-color: light-dark(#0090ed, #80ebff);
--alt-text-hover-done-color: var(--alt-text-done-color); --alt-text-hover-done-color: var(--alt-text-done-color);
--alt-text-hover-warning-color: var(--alt-text-warning-color); --alt-text-hover-warning-color: var(--alt-text-warning-color);
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
--editor-toolbar-bg-color: var(--button-background-color, ButtonFace); --editor-toolbar-bg-color: ButtonFace;
--editor-toolbar-fg-color: var(--button-text-color, ButtonText); --editor-toolbar-fg-color: ButtonText;
--editor-toolbar-border-color: var(--button-border-color, ButtonText); --editor-toolbar-border-color: ButtonText;
--editor-toolbar-hover-border-color: var( --editor-toolbar-hover-border-color: AccentColor;
--button-border-color-hover, --editor-toolbar-hover-bg-color: ButtonFace;
AccentColor --editor-toolbar-hover-fg-color: AccentColor;
);
--editor-toolbar-hover-bg-color: var(
--button-background-color-hover,
ButtonFace
);
--editor-toolbar-hover-fg-color: var(
--button-text-color-hover,
AccentColor
);
--editor-toolbar-hover-outline: 2px solid --editor-toolbar-hover-outline: 2px solid
var(--editor-toolbar-hover-border-color); var(--editor-toolbar-hover-border-color);
/* --button-border-color, not --focus-outline-color: the ring is painted --editor-toolbar-focus-outline-color: ButtonBorder;
on the toolbar's own ButtonFace surface, and --focus-outline-color is
CanvasText in Firefox HCM. */
--editor-toolbar-focus-outline-color: var(
--button-border-color,
ButtonBorder
);
--editor-toolbar-shadow: none; --editor-toolbar-shadow: none;
--alt-text-done-color: var(--editor-toolbar-fg-color); --alt-text-done-color: var(--editor-toolbar-fg-color);
--alt-text-warning-color: var(--editor-toolbar-fg-color); --alt-text-warning-color: var(--editor-toolbar-fg-color);
@ -485,18 +461,15 @@
&.show { &.show {
--alt-text-tooltip-bg: light-dark(#f0f0f4, #1c1b22); --alt-text-tooltip-bg: light-dark(#f0f0f4, #1c1b22);
--alt-text-tooltip-fg: var( --alt-text-tooltip-fg: light-dark(#15141a, #fbfbfe);
--text-color, --alt-text-tooltip-border: #8f8f9d;
light-dark(#15141a, #fbfbfe)
);
--alt-text-tooltip-border: var(--border-color-interactive, #8f8f9d);
--alt-text-tooltip-shadow: 0 2px 6px 0 --alt-text-tooltip-shadow: 0 2px 6px 0
light-dark(rgb(58 57 68 / 0.2), #15141a); light-dark(rgb(58 57 68 / 0.2), #15141a);
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
--alt-text-tooltip-bg: Canvas; --alt-text-tooltip-bg: Canvas;
--alt-text-tooltip-fg: var(--text-color, CanvasText); --alt-text-tooltip-fg: CanvasText;
--alt-text-tooltip-border: var(--border-color, CanvasText); --alt-text-tooltip-border: CanvasText;
--alt-text-tooltip-shadow: none; --alt-text-tooltip-shadow: none;
} }
@ -1082,10 +1055,7 @@
} }
.colorPicker { .colorPicker {
--hover-outline-color: var( --hover-outline-color: light-dark(#0250bb, #80ebff);
--color-accent-primary-hover,
light-dark(#0250bb, #80ebff)
);
--selected-outline-color: light-dark(#0060df, #aaf2ff); --selected-outline-color: light-dark(#0060df, #aaf2ff);
--swatch-border-color: light-dark(#cfcfd8, #52525e); --swatch-border-color: light-dark(#cfcfd8, #52525e);

View File

@ -370,7 +370,6 @@ const PDFViewerApplication = {
// Set some specific preferences for tests. // Set some specific preferences for tests.
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
Object.assign(opts, { Object.assign(opts, {
annotationEditorMode: x => parseInt(x, 10),
capCanvasAreaFactor: x => parseInt(x, 10), capCanvasAreaFactor: x => parseInt(x, 10),
docBaseUrl: x => x, docBaseUrl: x => x,
enableAltText: x => x === "true", enableAltText: x => x === "true",
@ -2563,7 +2562,7 @@ const PDFViewerApplication = {
this.pdfViewer.onPagesEdited(data); this.pdfViewer.onPagesEdited(data);
}, },
async onSavePages({ data: { pageInfos, copyLevels } }) { async onSavePages({ data: extractParams }) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
return; return;
} }
@ -2573,10 +2572,7 @@ const PDFViewerApplication = {
if (!this.pdfDocument) { if (!this.pdfDocument) {
return; return;
} }
const modifiedPdfBytes = await this.pdfDocument.extractPages( const modifiedPdfBytes = await this.pdfDocument.extractPages(extractParams);
pageInfos,
copyLevels
);
if (!modifiedPdfBytes) { if (!modifiedPdfBytes) {
console.error( console.error(
"Something wrong happened when saving the edited PDF.\nPlease file a bug." "Something wrong happened when saving the edited PDF.\nPlease file a bug."
@ -2590,14 +2586,11 @@ const PDFViewerApplication = {
); );
}, },
async onSaveAndLoad({ data: { pageInfos, copyLevels } }) { async onSaveAndLoad({ data: extractParams }) {
if (!this.pdfDocument) { if (!this.pdfDocument) {
return; return;
} }
const modifiedPdfBytes = await this.pdfDocument.extractPages( const modifiedPdfBytes = await this.pdfDocument.extractPages(extractParams);
pageInfos,
copyLevels
);
if (!modifiedPdfBytes) { if (!modifiedPdfBytes) {
console.error( console.error(
"Something wrong happened when saving the edited PDF.\nPlease file a bug." "Something wrong happened when saving the edited PDF.\nPlease file a bug."

View File

@ -416,18 +416,6 @@ const defaultOptions = new Map([
kind: OptionKind.VIEWER + OptionKind.PREFERENCE, kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
}, },
], ],
[
// Whether the viewer follows the Firefox design system (see the pref-gated
// @import of tokens-brand.css in viewer.css). Read from CSS via
// -moz-pref(), not from JS, so it is Firefox-only and has no effect
// elsewhere.
"enableNova",
{
/** @type {boolean} */
value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
],
[ [
"enableOptimizedPartialRendering", "enableOptimizedPartialRendering",
{ {

View File

@ -136,13 +136,10 @@ class Autolinker {
static #numericTLDRegex; static #numericTLDRegex;
static findLinks(text) { static findLinks(text) {
// Regex can be tested and verified at https://regex101.com/r/riHjvK/1. // Regex can be tested and verified at https://regex101.com/r/rXoLiT/2.
// The email parts are bounded to keep the scan linear: a local part can't
// exceed 64 characters (RFC 5321, section 4.5.3.1.1) and a domain label
// can't exceed 63 (RFC 1035, section 2.3.4).
this.#regex ??= this.#regex ??=
// eslint-disable-next-line regexp/no-super-linear-backtracking // eslint-disable-next-line regexp/no-super-linear-backtracking
/\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|(?=\p{L})[\S--[@\p{Ps}\p{Pe}<>]]{1,64}@([\S--[[\p{P}--\-]<>]]{1,63}(?:\.[\S--[[\p{P}--\-]<>]]{1,63})+)/gv; /\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|(?=\p{L})[\S--[@\p{Ps}\p{Pe}<>]]+@([\S--[[\p{P}--\-]<>]]+(?:\.[\S--[[\p{P}--\-]<>]]+)+)/gv;
const [normalizedText, diffs] = normalize(text, { ignoreDashEOL: true }); const [normalizedText, diffs] = normalize(text, { ignoreDashEOL: true });
const matches = normalizedText.matchAll(this.#regex); const matches = normalizedText.matchAll(this.#regex);

View File

@ -1,247 +0,0 @@
/* Copyright 2026 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.
*/
/*
* Shared button primitive: .primaryButton / .secondaryButton, for any button in
* the viewer and not only those inside a .dialog. The colors consume Firefox
* design tokens with the shipped literal as a fallback (see pdf_viewer.css).
*/
:root {
--button-primary-bg-color: var(
--button-background-color-primary,
light-dark(#0060df, #0df)
);
--button-primary-fg-color: var(
--button-text-color-primary,
light-dark(#fbfbfe, #15141a)
);
--button-primary-border-color: var(--button-primary-bg-color);
--button-primary-active-bg-color: var(
--button-background-color-primary-active,
light-dark(#054096, #aaf2ff)
);
--button-primary-active-fg-color: var(--button-primary-fg-color);
--button-primary-active-border-color: var(--button-primary-active-bg-color);
--button-primary-hover-bg-color: var(
--button-background-color-primary-hover,
light-dark(#0250bb, #80ebff)
);
--button-primary-hover-fg-color: var(--button-primary-fg-color);
--button-primary-hover-border-color: var(--button-primary-hover-bg-color);
--button-primary-disabled-bg-color: var(--button-primary-bg-color);
--button-primary-disabled-border-color: var(--button-primary-border-color);
--button-primary-disabled-fg-color: var(--button-primary-fg-color);
--button-secondary-bg-color: var(
--button-background-color,
light-dark(rgb(21 20 26 / 0.07), rgb(251 251 254 / 0.07))
);
--button-secondary-fg-color: var(--text-color, light-dark(#15141a, #fbfbfe));
/* Firefox's outline-style secondary button has a transparent fill and carries
the edge on --button-border-color; the fallback matches the fill instead,
since the shipped one is a translucent overlay. */
--button-secondary-border-color: var(
--button-border-color,
var(--button-secondary-bg-color)
);
--button-secondary-active-bg-color: var(
--button-background-color-active,
light-dark(rgb(21 20 26 / 0.21), rgb(251 251 254 / 0.21))
);
--button-secondary-active-fg-color: var(--button-secondary-fg-color);
--button-secondary-active-border-color: var(--button-secondary-bg-color);
--button-secondary-hover-bg-color: var(
--button-background-color-hover,
light-dark(rgb(21 20 26 / 0.14), rgb(251 251 254 / 0.14))
);
--button-secondary-hover-fg-color: var(--button-secondary-fg-color);
--button-secondary-hover-border-color: var(--button-secondary-hover-bg-color);
--button-secondary-disabled-bg-color: var(--button-secondary-bg-color);
--button-secondary-disabled-border-color: var(
--button-secondary-border-color
);
--button-secondary-disabled-fg-color: var(--button-secondary-fg-color);
--button-disabled-opacity: 0.4;
--hover-filter: brightness(0.9);
@media (prefers-color-scheme: dark) {
--button-disabled-opacity: 0.6;
--hover-filter: brightness(1.4);
}
@media screen and (forced-colors: active) {
--button-primary-bg-color: var(
--button-background-color-primary,
ButtonText
);
--button-primary-fg-color: var(--button-text-color-primary, ButtonFace);
--button-primary-border-color: var(
--button-border-color-primary,
ButtonText
);
--button-primary-active-bg-color: var(
--button-background-color-primary-active,
SelectedItem
);
--button-primary-active-fg-color: var(
--button-text-color-primary-active,
HighlightText
);
--button-primary-active-border-color: var(
--button-border-color-primary-active,
ButtonText
);
--button-primary-hover-bg-color: var(
--button-background-color-primary-hover,
SelectedItem
);
--button-primary-hover-fg-color: var(
--button-text-color-primary-hover,
HighlightText
);
--button-primary-hover-border-color: var(
--button-border-color-primary-hover,
SelectedItem
);
--button-primary-disabled-bg-color: var(
--button-background-color-primary-disabled,
GrayText
);
--button-primary-disabled-fg-color: var(
--button-text-color-primary-disabled,
ButtonFace
);
--button-primary-disabled-border-color: var(
--button-border-color-primary-disabled,
GrayText
);
--button-secondary-bg-color: var(--button-background-color, ButtonFace);
--button-secondary-fg-color: var(--button-text-color, ButtonText);
--button-secondary-border-color: var(--button-border-color, ButtonText);
--button-secondary-active-bg-color: var(
--button-background-color-active,
HighlightText
);
--button-secondary-active-fg-color: var(
--button-text-color-active,
SelectedItem
);
--button-secondary-active-border-color: var(
--button-border-color-active,
ButtonText
);
--button-secondary-hover-bg-color: var(
--button-background-color-hover,
HighlightText
);
--button-secondary-hover-fg-color: var(
--button-text-color-hover,
SelectedItem
);
--button-secondary-hover-border-color: var(
--button-border-color-hover,
SelectedItem
);
--button-secondary-disabled-fg-color: var(
--button-text-color-disabled,
GrayText
);
--button-secondary-disabled-border-color: var(
--button-border-color-disabled,
GrayText
);
--button-disabled-opacity: 1;
--hover-filter: none;
}
}
.primaryButton,
.secondaryButton {
border-radius: var(--button-border-radius, 4px);
border: 1px solid;
font: menu;
font-weight: var(--button-font-weight, 590);
font-size: var(--button-font-size, 13px);
padding: var(--button-padding, 4px 16px);
width: auto;
min-height: var(--button-min-height, 32px);
&:hover {
cursor: pointer;
filter: var(--hover-filter);
}
> span {
color: inherit;
font: inherit;
}
&:disabled {
pointer-events: none;
}
}
.primaryButton {
color: var(--button-primary-fg-color);
background-color: var(--button-primary-bg-color);
border-color: var(--button-primary-border-color);
opacity: 1;
&:hover {
color: var(--button-primary-hover-fg-color);
background-color: var(--button-primary-hover-bg-color);
border-color: var(--button-primary-hover-border-color);
}
&:active {
color: var(--button-primary-active-fg-color);
background-color: var(--button-primary-active-bg-color);
border-color: var(--button-primary-active-border-color);
}
&:disabled {
background-color: var(--button-primary-disabled-bg-color);
border-color: var(--button-primary-disabled-border-color);
color: var(--button-primary-disabled-fg-color);
opacity: var(--button-disabled-opacity);
}
}
.secondaryButton {
color: var(--button-secondary-fg-color);
background-color: var(--button-secondary-bg-color);
border-color: var(--button-secondary-border-color);
&:hover {
color: var(--button-secondary-hover-fg-color);
background-color: var(--button-secondary-hover-bg-color);
border-color: var(--button-secondary-hover-border-color);
}
&:active {
color: var(--button-secondary-active-fg-color);
background-color: var(--button-secondary-active-bg-color);
border-color: var(--button-secondary-active-border-color);
}
&:disabled {
background-color: var(--button-secondary-disabled-bg-color);
border-color: var(--button-secondary-disabled-border-color);
color: var(--button-secondary-disabled-fg-color);
opacity: var(--button-disabled-opacity);
}
}

View File

@ -65,86 +65,39 @@
:is(.annotationLayer, .annotationEditorLayer) { :is(.annotationLayer, .annotationEditorLayer) {
.annotationCommentButton { .annotationCommentButton {
color-scheme: light dark; color-scheme: light dark;
--comment-button-bg: var( --comment-button-bg: light-dark(white, #1c1b22);
--background-color-canvas,
light-dark(white, #1c1b22)
);
--comment-button-fg: light-dark(#5b5b66, #fbfbfe); --comment-button-fg: light-dark(#5b5b66, #fbfbfe);
--comment-button-active-bg: light-dark(#0041a4, #a6ecf4); --comment-button-active-bg: light-dark(#0041a4, #a6ecf4);
--comment-button-active-fg: var( --comment-button-active-fg: light-dark(white, #15141a);
--button-text-color-primary, --comment-button-hover-bg: light-dark(#0053cb, #61dce9);
light-dark(white, #15141a) --comment-button-hover-fg: light-dark(white, #15141a);
); --comment-button-selected-bg: light-dark(#0062fa, #00cadb);
--comment-button-hover-bg: var( --comment-button-border-color: light-dark(#8f8f9d, #bfbfc9);
--color-accent-primary-hover,
light-dark(#0053cb, #61dce9)
);
--comment-button-hover-fg: var(
--button-text-color-primary,
light-dark(white, #15141a)
);
--comment-button-selected-bg: var(
--color-accent-primary,
light-dark(#0062fa, #00cadb)
);
--comment-button-border-color: var(
--border-color-interactive,
light-dark(#8f8f9d, #bfbfc9)
);
--comment-button-active-border-color: var(--comment-button-active-bg); --comment-button-active-border-color: var(--comment-button-active-bg);
--comment-button-focus-border-color: light-dark(#cfcfd8, #3a3944); --comment-button-focus-border-color: light-dark(#cfcfd8, #3a3944);
--comment-button-hover-border-color: var(--comment-button-hover-bg); --comment-button-hover-border-color: var(--comment-button-hover-bg);
--comment-button-selected-border-color: var(--comment-button-selected-bg); --comment-button-selected-border-color: var(--comment-button-selected-bg);
--comment-button-selected-fg: var( --comment-button-selected-fg: light-dark(white, #15141a);
--button-text-color-primary,
light-dark(white, #15141a)
);
--comment-button-dim: 24px; --comment-button-dim: 24px;
--comment-button-box-shadow: --comment-button-box-shadow:
0 0.25px 0.75px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)), 0 0.25px 0.75px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
0 2px 6px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4)); 0 2px 6px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
--comment-button-focus-outline-color: var( --comment-button-focus-outline-color: light-dark(#0062fa, #00cadb);
--focus-outline-color,
light-dark(#0062fa, #00cadb)
);
/* Each state reads the *matching* pair of Firefox button tokens, so the
background and the icon can never resolve to the same system colour. */
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
--comment-button-bg: var(--button-background-color, ButtonFace); --comment-button-bg: ButtonFace;
--comment-button-fg: var(--button-text-color, ButtonText); --comment-button-fg: ButtonText;
--comment-button-hover-bg: var( --comment-button-hover-bg: SelectedItemText;
--button-background-color-hover, --comment-button-hover-fg: SelectedItem;
SelectedItemText --comment-button-active-bg: SelectedItemText;
); --comment-button-active-fg: SelectedItem;
--comment-button-hover-fg: var(--button-text-color-hover, SelectedItem); --comment-button-border-color: ButtonBorder;
--comment-button-active-bg: var( --comment-button-active-border-color: ButtonBorder;
--button-background-color-active, --comment-button-hover-border-color: SelectedItem;
SelectedItemText
);
--comment-button-active-fg: var(--button-text-color-active, SelectedItem);
--comment-button-border-color: var(--button-border-color, ButtonBorder);
--comment-button-active-border-color: var(
--button-border-color-active,
ButtonBorder
);
--comment-button-hover-border-color: var(
--button-border-color-hover,
SelectedItem
);
--comment-button-box-shadow: none; --comment-button-box-shadow: none;
--comment-button-focus-outline-color: var( --comment-button-focus-outline-color: CanvasText;
--focus-outline-color, --comment-button-selected-bg: ButtonBorder;
CanvasText --comment-button-selected-fg: ButtonFace;
);
--comment-button-selected-bg: var(
--button-background-color-primary,
ButtonBorder
);
--comment-button-selected-fg: var(
--button-text-color-primary,
ButtonFace
);
} }
position: absolute; position: absolute;
@ -232,28 +185,19 @@
--comment-active-brightness: 0.825; --comment-active-brightness: 0.825;
--comment-active-filter: brightness(var(--comment-active-brightness)); --comment-active-filter: brightness(var(--comment-active-brightness));
--comment-border-color: light-dark(#f0f0f4, #52525e); --comment-border-color: light-dark(#f0f0f4, #52525e);
--comment-focus-outline-color: var( --comment-focus-outline-color: light-dark(#0062fa, #00cadb);
--focus-outline-color, --comment-fg-color: light-dark(#15141a, #fbfbfe);
light-dark(#0062fa, #00cadb)
);
--comment-fg-color: var(--text-color, light-dark(#15141a, #fbfbfe));
--comment-count-bg-color: light-dark(#e2f7ff, #00317e); --comment-count-bg-color: light-dark(#e2f7ff, #00317e);
--comment-indicator-active-fg-color: light-dark(#0041a4, #a6ecf4); --comment-indicator-active-fg-color: light-dark(#0041a4, #a6ecf4);
--comment-indicator-active-filter: brightness( --comment-indicator-active-filter: brightness(
calc(1 / var(--comment-active-brightness)) calc(1 / var(--comment-active-brightness))
); );
--comment-indicator-focus-fg-color: light-dark(#5b5b66, #fbfbfe); --comment-indicator-focus-fg-color: light-dark(#5b5b66, #fbfbfe);
--comment-indicator-hover-fg-color: var( --comment-indicator-hover-fg-color: light-dark(#0053cb, #61dce9);
--color-accent-primary-hover,
light-dark(#0053cb, #61dce9)
);
--comment-indicator-hover-filter: brightness( --comment-indicator-hover-filter: brightness(
calc(1 / var(--comment-hover-brightness)) calc(1 / var(--comment-hover-brightness))
); );
--comment-indicator-selected-fg-color: var( --comment-indicator-selected-fg-color: light-dark(#0062fa, #00cadb);
--color-accent-primary,
light-dark(#0062fa, #00cadb)
);
--button-comment-bg: transparent; --button-comment-bg: transparent;
--button-comment-color: var(--main-color); --button-comment-color: var(--main-color);
@ -264,6 +208,9 @@
--button-comment-hover-bg: light-dark(#e0e0e6, #52525e); --button-comment-hover-bg: light-dark(#e0e0e6, #52525e);
--button-comment-hover-color: var(--button-comment-color); --button-comment-hover-color: var(--button-comment-color);
--link-fg-color: light-dark(#0060df, #0df);
--link-hover-fg-color: light-dark(#0250bb, #80ebff);
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
--comment-date-fg-color: CanvasText; --comment-date-fg-color: CanvasText;
--comment-bg-color: Canvas; --comment-bg-color: Canvas;
@ -272,13 +219,11 @@
--comment-active-bg-color: Canvas; --comment-active-bg-color: Canvas;
--comment-active-filter: none; --comment-active-filter: none;
--comment-border-color: CanvasText; --comment-border-color: CanvasText;
--comment-fg-color: var(--text-color, CanvasText); --comment-fg-color: CanvasText;
--comment-count-bg-color: Canvas; --comment-count-bg-color: Canvas;
--comment-indicator-active-fg-color: SelectedItem; --comment-indicator-active-fg-color: SelectedItem;
--comment-indicator-focus-fg-color: var(--text-color, CanvasText); --comment-indicator-focus-fg-color: CanvasText;
/* On the Canvas comment surface, so paired with --text-color rather than a --comment-indicator-hover-fg-color: CanvasText;
button/accent token. */
--comment-indicator-hover-fg-color: var(--text-color, CanvasText);
--comment-indicator-selected-fg-color: SelectedItem; --comment-indicator-selected-fg-color: SelectedItem;
--button-comment-bg: ButtonFace; --button-comment-bg: ButtonFace;
--button-comment-color: ButtonText; --button-comment-color: ButtonText;
@ -287,6 +232,8 @@
--button-comment-border: 1px solid ButtonText; --button-comment-border: 1px solid ButtonText;
--button-comment-hover-bg: Highlight; --button-comment-hover-bg: Highlight;
--button-comment-hover-color: HighlightText; --button-comment-hover-color: HighlightText;
--link-fg-color: LinkText;
--link-hover-fg-color: LinkText;
} }
} }

View File

@ -14,42 +14,117 @@
*/ */
.dialog { .dialog {
--dialog-bg-color: var(--background-color-canvas, light-dark(white, #1c1b22)); --dialog-bg-color: light-dark(white, #1c1b22);
--dialog-border-color: var( --dialog-border-color: light-dark(white, #1c1b22);
--background-color-canvas,
light-dark(white, #1c1b22)
);
--dialog-shadow: 0 2px 14px 0 light-dark(rgb(58 57 68 / 0.2), #15141a); --dialog-shadow: 0 2px 14px 0 light-dark(rgb(58 57 68 / 0.2), #15141a);
--text-primary-color: light-dark(#15141a, #fbfbfe);
--text-secondary-color: light-dark(#5b5b66, #cfcfd8); --text-secondary-color: light-dark(#5b5b66, #cfcfd8);
--hover-filter: brightness(0.9);
--link-fg-color: light-dark(#0060df, #0df);
--link-hover-fg-color: light-dark(#0250bb, #80ebff);
--separator-color: light-dark(#f0f0f4, #52525e); --separator-color: light-dark(#f0f0f4, #52525e);
/* A textarea is an interactive control, so it takes the interactive border --textarea-border-color: #8f8f9d;
token (whose light value is the shipped literal) and not --border-color,
which is the low-contrast decorative one. */
--textarea-border-color: var(--border-color-interactive, #8f8f9d);
--textarea-bg-color: light-dark(white, #42414d); --textarea-bg-color: light-dark(white, #42414d);
--textarea-fg-color: var(--text-secondary-color); --textarea-fg-color: var(--text-secondary-color);
/* .primaryButton / .secondaryButton button tokens are defined once, at :root --radio-bg-color: light-dark(#f0f0f4, #2b2a33);
in buttons.css (the shared primitive), and inherit into dialogs. */ --radio-checked-bg-color: light-dark(#fbfbfe, #15141a);
--radio-border-color: #8f8f9d;
--radio-checked-border-color: light-dark(#0060df, #0df);
--button-secondary-bg-color: light-dark(
rgb(21 20 26 / 0.07),
rgb(251 251 254 / 0.07)
);
--button-secondary-fg-color: var(--text-primary-color);
--button-secondary-border-color: var(--button-secondary-bg-color);
--button-secondary-active-bg-color: light-dark(
rgb(21 20 26 / 0.21),
rgb(251 251 254 / 0.21)
);
--button-secondary-active-fg-color: var(--button-secondary-fg-color);
--button-secondary-active-border-color: var(--button-secondary-bg-color);
--button-secondary-hover-bg-color: light-dark(
rgb(21 20 26 / 0.14),
rgb(251 251 254 / 0.14)
);
--button-secondary-hover-fg-color: var(--button-secondary-fg-color);
--button-secondary-hover-border-color: var(--button-secondary-hover-bg-color);
--button-secondary-disabled-bg-color: var(--button-secondary-bg-color);
--button-secondary-disabled-border-color: var(
--button-secondary-border-color
);
--button-secondary-disabled-fg-color: var(--button-secondary-fg-color);
--button-primary-bg-color: light-dark(#0060df, #0df);
--button-primary-fg-color: light-dark(#fbfbfe, #15141a);
--button-primary-border-color: var(--button-primary-bg-color);
--button-primary-active-bg-color: light-dark(#054096, #aaf2ff);
--button-primary-active-fg-color: var(--button-primary-fg-color);
--button-primary-active-border-color: var(--button-primary-active-bg-color);
--button-primary-hover-bg-color: light-dark(#0250bb, #80ebff);
--button-primary-hover-fg-color: var(--button-primary-fg-color);
--button-primary-hover-border-color: var(--button-primary-hover-bg-color);
--button-primary-disabled-bg-color: var(--button-primary-bg-color);
--button-primary-disabled-border-color: var(--button-primary-border-color);
--button-primary-disabled-fg-color: var(--button-primary-fg-color);
--button-disabled-opacity: 0.4;
--input-text-bg-color: light-dark(white, #42414d); --input-text-bg-color: light-dark(white, #42414d);
--input-text-fg-color: var(--text-primary-color); --input-text-fg-color: var(--text-primary-color);
@media (prefers-color-scheme: dark) {
--hover-filter: brightness(1.4);
--button-disabled-opacity: 0.6;
}
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
--dialog-bg-color: var(--background-color-canvas, Canvas); --dialog-bg-color: Canvas;
/* border-only: --background-color-canvas would resolve to Canvas in FF HCM --dialog-border-color: CanvasText;
(= the dialog bg) and the shadow is none here, leaving no boundary; route
through --border-color (CanvasText in HCM) to keep a visible border. */
--dialog-border-color: var(--border-color, CanvasText);
--dialog-shadow: none; --dialog-shadow: none;
--text-primary-color: CanvasText;
--text-secondary-color: CanvasText; --text-secondary-color: CanvasText;
--hover-filter: none;
--link-fg-color: LinkText;
--link-hover-fg-color: LinkText;
--separator-color: CanvasText; --separator-color: CanvasText;
--textarea-border-color: ButtonBorder; --textarea-border-color: ButtonBorder;
--textarea-bg-color: Field; --textarea-bg-color: Field;
--textarea-fg-color: ButtonText; --textarea-fg-color: ButtonText;
--radio-bg-color: ButtonFace;
--radio-checked-bg-color: ButtonFace;
--radio-border-color: ButtonText;
--radio-checked-border-color: ButtonText;
--button-secondary-bg-color: ButtonFace;
--button-secondary-fg-color: ButtonText;
--button-secondary-border-color: ButtonText;
--button-secondary-active-bg-color: HighlightText;
--button-secondary-active-fg-color: SelectedItem;
--button-secondary-active-border-color: ButtonText;
--button-secondary-hover-bg-color: HighlightText;
--button-secondary-hover-fg-color: SelectedItem;
--button-secondary-hover-border-color: SelectedItem;
--button-secondary-disabled-fg-color: GrayText;
--button-secondary-disabled-border-color: GrayText;
--button-primary-bg-color: ButtonText;
--button-primary-fg-color: ButtonFace;
--button-primary-border-color: ButtonText;
--button-primary-active-bg-color: SelectedItem;
--button-primary-active-fg-color: HighlightText;
--button-primary-active-border-color: ButtonText;
--button-primary-hover-bg-color: SelectedItem;
--button-primary-hover-fg-color: HighlightText;
--button-primary-hover-border-color: SelectedItem;
--button-primary-disabled-bg-color: GrayText;
--button-primary-disabled-fg-color: ButtonFace;
--button-primary-disabled-border-color: GrayText;
--button-disabled-opacity: 1;
--input-text-bg-color: Field; --input-text-bg-color: Field;
--input-text-fg-color: FieldText; --input-text-fg-color: FieldText;
} }
@ -58,7 +133,7 @@
font-size: 13px; font-size: 13px;
font-weight: 400; font-weight: 400;
line-height: 150%; line-height: 150%;
border-radius: var(--border-radius-medium, 4px); border-radius: 4px;
padding: 12px 16px; padding: 12px 16px;
border: 1px solid var(--dialog-border-color); border: 1px solid var(--dialog-border-color);
background: var(--dialog-bg-color); background: var(--dialog-bg-color);
@ -101,15 +176,6 @@
align-self: flex-end; align-self: flex-end;
} }
/* Native checkbox/radio tinted with the accent at Firefox's control size,
matching Firefox's default form controls (the custom appearance is
Nova-only in Firefox, so the default look is native + accent-color). */
input:is([type="checkbox"], [type="radio"]) {
accent-color: var(--color-accent-primary, light-dark(#0060df, #0df));
width: var(--checkbox-size, 16px);
height: var(--checkbox-size, 16px);
}
.radio { .radio {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -121,6 +187,25 @@
gap: 8px; gap: 8px;
align-self: stretch; align-self: stretch;
align-items: center; align-items: center;
input {
appearance: none;
box-sizing: border-box;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: var(--radio-bg-color);
border: 1px solid var(--radio-border-color);
&:hover {
filter: var(--hover-filter);
}
&:checked {
background-color: var(--radio-checked-bg-color);
border: 4px solid var(--radio-checked-border-color);
}
}
} }
> .radioLabel { > .radioLabel {
@ -139,15 +224,14 @@
} }
button:not(:is(.toggle-button, .closeButton, .clearInputButton)) { button:not(:is(.toggle-button, .closeButton, .clearInputButton)) {
border-radius: var(--button-border-radius, 4px); border-radius: 4px;
border-width: 1px; border: 1px solid;
border-style: solid;
font: menu; font: menu;
font-weight: var(--button-font-weight, 590); font-weight: 590;
font-size: var(--button-font-size, 13px); font-size: 13px;
padding: var(--button-padding, 4px 16px); padding: 4px 16px;
width: auto; width: auto;
min-height: var(--button-min-height, 32px); height: 32px;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
@ -159,9 +243,57 @@
font: inherit; font: inherit;
} }
/* .primaryButton / .secondaryButton colours and states come from the &.secondaryButton {
shared buttons.css primitive; only the dialog-specific base geometry color: var(--button-secondary-fg-color);
(above) lives here, so it also covers unclassed dialog buttons. */ background-color: var(--button-secondary-bg-color);
border-color: var(--button-secondary-border-color);
&:hover {
color: var(--button-secondary-hover-fg-color);
background-color: var(--button-secondary-hover-bg-color);
border-color: var(--button-secondary-hover-border-color);
}
&:active {
color: var(--button-secondary-active-fg-color);
background-color: var(--button-secondary-active-bg-color);
border-color: var(--button-secondary-active-border-color);
}
&:disabled {
background-color: var(--button-secondary-disabled-bg-color);
border-color: var(--button-secondary-disabled-border-color);
color: var(--button-secondary-disabled-fg-color);
opacity: var(--button-disabled-opacity);
}
}
&.primaryButton {
color: var(--button-primary-fg-color);
background-color: var(--button-primary-bg-color);
border-color: var(--button-primary-border-color);
opacity: 1;
&:hover {
color: var(--button-primary-hover-fg-color);
background-color: var(--button-primary-hover-bg-color);
border-color: var(--button-primary-hover-border-color);
}
&:active {
color: var(--button-primary-active-fg-color);
background-color: var(--button-primary-active-bg-color);
border-color: var(--button-primary-active-border-color);
}
&:disabled {
background-color: var(--button-primary-disabled-bg-color);
border-color: var(--button-primary-disabled-border-color);
color: var(--button-primary-disabled-fg-color);
opacity: var(--button-disabled-opacity);
}
}
&:disabled { &:disabled {
pointer-events: none; pointer-events: none;
} }
@ -181,7 +313,7 @@
resize: none; resize: none;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
border-radius: var(--border-radius-small, 4px); border-radius: 4px;
border: 1px solid var(--textarea-border-color); border: 1px solid var(--textarea-border-color);
background: var(--textarea-bg-color); background: var(--textarea-bg-color);
color: var(--textarea-fg-color); color: var(--textarea-fg-color);

View File

@ -28,28 +28,16 @@
--sig-detail-color: light-dark(rgb(96 96 96), rgb(180 180 184)); --sig-detail-color: light-dark(rgb(96 96 96), rgb(180 180 184));
--sig-divider-color: light-dark(rgb(228 228 232), rgb(82 82 86)); --sig-divider-color: light-dark(rgb(228 228 232), rgb(82 82 86));
--sig-summary-hover-color: light-dark(rgb(28 67 138), rgb(126 169 255)); --sig-summary-hover-color: light-dark(rgb(28 67 138), rgb(126 169 255));
--sig-link-color: var( --sig-link-color: light-dark(rgb(28 113 216), rgb(126 169 255));
--link-color,
light-dark(rgb(28 113 216), rgb(126 169 255))
);
--sig-link-hover-bg: light-dark( --sig-link-hover-bg: light-dark(
rgb(28 113 216 / 0.1), rgb(28 113 216 / 0.1),
rgb(126 169 255 / 0.15) rgb(126 169 255 / 0.15)
); );
--sig-banner-verified-bg: var( --sig-banner-verified-bg: light-dark(rgb(228 247 235), rgb(28 84 49));
--background-color-success,
light-dark(rgb(228 247 235), rgb(28 84 49))
);
--sig-banner-verified-color: light-dark(rgb(16 92 47), rgb(176 232 196)); --sig-banner-verified-color: light-dark(rgb(16 92 47), rgb(176 232 196));
--sig-banner-warn-bg: var( --sig-banner-warn-bg: light-dark(rgb(255 247 217), rgb(95 67 9));
--background-color-warning,
light-dark(rgb(255 247 217), rgb(95 67 9))
);
--sig-banner-warn-color: light-dark(rgb(124 84 9), rgb(255 222 153)); --sig-banner-warn-color: light-dark(rgb(124 84 9), rgb(255 222 153));
--sig-banner-error-bg: var( --sig-banner-error-bg: light-dark(rgb(254 226 235), rgb(122 21 51));
--background-color-critical,
light-dark(rgb(254 226 235), rgb(122 21 51))
);
--sig-banner-error-color: light-dark(rgb(167 26 70), rgb(255 188 207)); --sig-banner-error-color: light-dark(rgb(167 26 70), rgb(255 188 207));
/* Tint colours for the row / toolbar icons. These are paired with /* Tint colours for the row / toolbar icons. These are paired with
@ -60,18 +48,9 @@
* verified = green (only used for the top-level "everything fine" * verified = green (only used for the top-level "everything fine"
* row and the toolbar's verified badge). */ * row and the toolbar's verified badge). */
--sig-icon-default: light-dark(rgb(150 150 150), rgb(180 180 184)); --sig-icon-default: light-dark(rgb(150 150 150), rgb(180 180 184));
--sig-icon-warn: var( --sig-icon-warn: light-dark(rgb(217 142 27), rgb(255 178 77));
--icon-color-warning, --sig-icon-error: light-dark(rgb(196 31 71), rgb(255 117 145));
light-dark(rgb(217 142 27), rgb(255 178 77)) --sig-icon-verified: light-dark(rgb(29 142 61), rgb(106 210 126));
);
--sig-icon-error: var(
--icon-color-critical,
light-dark(rgb(196 31 71), rgb(255 117 145))
);
--sig-icon-verified: var(
--icon-color-success,
light-dark(rgb(29 142 61), rgb(106 210 126))
);
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
/* HCM keywords are picked by *semantic role*, not by hue the /* HCM keywords are picked by *semantic role*, not by hue the
@ -98,26 +77,23 @@
--sig-detail-color: GrayText; --sig-detail-color: GrayText;
--sig-divider-color: GrayText; --sig-divider-color: GrayText;
--sig-summary-hover-color: AccentColor; --sig-summary-hover-color: AccentColor;
--sig-link-color: var(--link-color, LinkText); --sig-link-color: LinkText;
--sig-link-hover-bg: transparent; --sig-link-hover-bg: transparent;
/* Firefox collapses the status backgrounds onto --background-color-canvas --sig-banner-verified-bg: ButtonFace;
in HCM, so the foregrounds have to follow --text-color to stay a pair --sig-banner-verified-color: ButtonText;
(ButtonText over that Canvas surface is not guaranteed to contrast). */ --sig-banner-warn-bg: ButtonFace;
--sig-banner-verified-bg: var(--background-color-success, ButtonFace); --sig-banner-warn-color: ButtonText;
--sig-banner-verified-color: var(--text-color, ButtonText); --sig-banner-error-bg: ButtonFace;
--sig-banner-warn-bg: var(--background-color-warning, ButtonFace); --sig-banner-error-color: ButtonText;
--sig-banner-warn-color: var(--text-color, ButtonText);
--sig-banner-error-bg: var(--background-color-critical, ButtonFace);
--sig-banner-error-color: var(--text-color, ButtonText);
/* Severities collapse to the same emphasis keyword (AccentColor) /* Severities collapse to the same emphasis keyword (AccentColor)
* in HCM same trick as alt-text where `done` and `warning` * in HCM same trick as alt-text where `done` and `warning`
* share their hover colour. The glyph shape (check vs `!` vs ``) * share their hover colour. The glyph shape (check vs `!` vs ``)
* carries the remaining distinction. The neutral "row crypto * carries the remaining distinction. The neutral "row crypto
* verified" check stays muted (GrayText). */ * verified" check stays muted (GrayText). */
--sig-icon-default: GrayText; --sig-icon-default: GrayText;
--sig-icon-warn: var(--icon-color-warning, AccentColor); --sig-icon-warn: AccentColor;
--sig-icon-error: var(--icon-color-critical, AccentColor); --sig-icon-error: AccentColor;
--sig-icon-verified: var(--icon-color-success, AccentColor); --sig-icon-verified: AccentColor;
} }
} }

View File

@ -14,7 +14,6 @@
*/ */
import { INTERNAL_EVT, internalOpt } from "./internal_evt.js"; import { INTERNAL_EVT, internalOpt } from "./internal_evt.js";
import { makeSet } from "pdfjs-lib";
const WaitOnType = { const WaitOnType = {
EVENT: "event", EVENT: "event",
@ -73,7 +72,7 @@ async function waitOnEventOrTimeout({ target, name, delay = 0 }) {
* and `off` methods. To raise an event, the `dispatch` method shall be used. * and `off` methods. To raise an event, the `dispatch` method shall be used.
*/ */
class EventBus { class EventBus {
#listeners = new Map(); #listeners = Object.create(null);
constructor() { constructor() {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
@ -102,7 +101,8 @@ class EventBus {
signal.addEventListener("abort", onAbort); signal.addEventListener("abort", onAbort);
} }
this.#listeners.getOrInsertComputed(eventName, makeSet).add({ const eventListeners = (this.#listeners[eventName] ??= []);
eventListeners.push({
listener, listener,
internal: options?.internal === INTERNAL_EVT, internal: options?.internal === INTERNAL_EVT,
once: options?.once === true, once: options?.once === true,
@ -116,11 +116,17 @@ class EventBus {
* @param {Object} [options] * @param {Object} [options]
*/ */
off(eventName, listener, options = null) { off(eventName, listener, options = null) {
const eventListeners = this.#listeners.get(eventName); const eventListeners = this.#listeners[eventName];
const evt = eventListeners?.keys().find(e => e.listener === listener); if (!eventListeners) {
if (evt) { return;
evt.rmAbort?.(); // Ensure that the `AbortSignal` listener is removed. }
eventListeners.delete(evt); for (let i = 0, ii = eventListeners.length; i < ii; i++) {
const evt = eventListeners[i];
if (evt.listener === listener) {
evt.rmAbort?.(); // Ensure that the `AbortSignal` listener is removed.
eventListeners.splice(i, 1);
return;
}
} }
} }
@ -129,14 +135,14 @@ class EventBus {
* @param {Object} data * @param {Object} data
*/ */
dispatch(eventName, data) { dispatch(eventName, data) {
const eventListeners = this.#listeners.get(eventName); const eventListeners = this.#listeners[eventName];
if (!eventListeners?.size) { if (!eventListeners?.length) {
return; return;
} }
let extListeners; let extListeners;
// Always create a copy of the listeners in case they are modified // Making copy of the listeners array in case if it will be modified
// during dispatch. // during dispatch.
for (const { listener, internal, once } of new Set(eventListeners)) { for (const { listener, internal, once } of eventListeners.slice(0)) {
if (once) { if (once) {
this.off(eventName, listener); this.off(eventName, listener);
} }

View File

@ -41,13 +41,10 @@ button.hasPopupMenu {
var(--menu-text-color), var(--menu-text-color),
transparent 93% transparent 93%
); );
--menuitem-focus-outline-color: var( --menuitem-focus-outline-color: light-dark(#0062fa, #00cadb);
--focus-outline-color,
light-dark(#0062fa, #00cadb)
);
--menuitem-focus-border-color: light-dark(white, black); --menuitem-focus-border-color: light-dark(white, black);
--menu-bg: var(--background-color-box, light-dark(white, #23222b)); --menu-bg: light-dark(white, #23222b);
--menu-background-blend-mode: normal; --menu-background-blend-mode: normal;
--menu-box-shadow: --menu-box-shadow:
0 0.375px 1.5px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)), 0 0.375px 1.5px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
@ -55,7 +52,7 @@ button.hasPopupMenu {
--menu-border-color: light-dark(rgb(21 20 26 / 0.1), rgb(251 251 254 / 0.1)); --menu-border-color: light-dark(rgb(21 20 26 / 0.1), rgb(251 251 254 / 0.1));
--menuitem-border-radius: 8px; --menuitem-border-radius: 8px;
--menu-backdrop-filter: none; --menu-backdrop-filter: none;
--menu-text-color: var(--text-color, light-dark(#15141a, #fbfbfe)); --menu-text-color: light-dark(#15141a, #fbfbfe);
--menu-text-disabled-color: var(--menu-text-color); --menu-text-disabled-color: var(--menu-text-color);
--menuitem-text-hover-fg: var(--menu-text-color); --menuitem-text-hover-fg: var(--menu-text-color);
--menuitem-hover-bg: color-mix( --menuitem-hover-bg: color-mix(
@ -67,11 +64,11 @@ button.hasPopupMenu {
--disabled-opacity: 0.62; --disabled-opacity: 0.62;
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
--menu-bg: var(--background-color-box, Canvas); --menu-bg: Canvas;
--menu-background-blend-mode: normal; --menu-background-blend-mode: normal;
--menu-box-shadow: none; --menu-box-shadow: none;
--menu-backdrop-filter: none; --menu-backdrop-filter: none;
--menu-text-color: var(--text-color, ButtonText); --menu-text-color: ButtonText;
--menu-text-disabled-color: GrayText; --menu-text-disabled-color: GrayText;
--menu-border-color: CanvasText; --menu-border-color: CanvasText;
--menuitem-border-color: none; --menuitem-border-color: none;
@ -81,7 +78,7 @@ button.hasPopupMenu {
--menuitem-active-border-color: ButtonText; --menuitem-active-border-color: ButtonText;
--menuitem-text-active-fg: SelectedItem; --menuitem-text-active-fg: SelectedItem;
--menuitem-focus-bg: ButtonFace; --menuitem-focus-bg: ButtonFace;
--menuitem-focus-outline-color: var(--focus-outline-color, CanvasText); --menuitem-focus-outline-color: CanvasText;
--menuitem-focus-border-color: none; --menuitem-focus-border-color: none;
--disabled-opacity: 1; --disabled-opacity: 1;
} }

View File

@ -39,9 +39,14 @@ class Menu {
constructor(menuContainer, triggeringButton, menuItems) { constructor(menuContainer, triggeringButton, menuItems) {
this.#menu = menuContainer; this.#menu = menuContainer;
this.#triggeringButton = triggeringButton; this.#triggeringButton = triggeringButton;
this.#menuItems = Array.isArray(menuItems) if (Array.isArray(menuItems)) {
? menuItems this.#menuItems = menuItems;
: [...this.#menu.querySelectorAll("button")]; } else {
this.#menuItems = [];
for (const button of this.#menu.querySelectorAll("button")) {
this.#menuItems.push(button);
}
}
this.#setUpMenu(); this.#setUpMenu();
} }
@ -141,11 +146,19 @@ class Menu {
stopEvent(e); stopEvent(e);
break; break;
case "Home": case "Home":
this.#goToFirstLast(false); this.#menuItems
.find(
item => !item.disabled && !item.classList.contains("hidden")
)
?.focus();
stopEvent(e); stopEvent(e);
break; break;
case "End": case "End":
this.#goToFirstLast(true); this.#menuItems
.findLast(
item => !item.disabled && !item.classList.contains("hidden")
)
?.focus();
stopEvent(e); stopEvent(e);
break; break;
default: default:
@ -181,7 +194,11 @@ class Menu {
if (!this.#openMenuAC) { if (!this.#openMenuAC) {
this.#openMenu(); this.#openMenu();
} }
this.#goToFirstLast(false); this.#menuItems
.find(
item => !item.disabled && !item.classList.contains("hidden")
)
?.focus();
break; break;
case "ArrowUp": case "ArrowUp":
case "End": case "End":
@ -189,7 +206,11 @@ class Menu {
if (!this.#openMenuAC) { if (!this.#openMenuAC) {
this.#openMenu(); this.#openMenu();
} }
this.#goToFirstLast(true); this.#menuItems
.findLast(
item => !item.disabled && !item.classList.contains("hidden")
)
?.focus();
break; break;
case "Escape": case "Escape":
this.#closeMenu(); this.#closeMenu();
@ -231,20 +252,6 @@ class Menu {
} }
} }
/**
* Go to the first/last menu item.
* @param {boolean} [last]
*/
#goToFirstLast(last = false) {
const i = this.#menuItems[last ? "findLastIndex" : "findIndex"](
item => !item.disabled && !item.classList.contains("hidden")
);
if (i >= 0) {
this.#menuItems[i].focus();
this.#lastIndex = i;
}
}
destroy() { destroy() {
this.#closeMenu(); this.#closeMenu();
this.#menuAC?.abort(); this.#menuAC?.abort();

View File

@ -17,46 +17,28 @@
--closing-button-icon: url(images/messageBar_closingButton.svg); --closing-button-icon: url(images/messageBar_closingButton.svg);
--message-bar-close-button-color: var(--text-primary-color); --message-bar-close-button-color: var(--text-primary-color);
--message-bar-close-button-color-hover: var(--text-primary-color); --message-bar-close-button-color-hover: var(--text-primary-color);
--message-bar-close-button-border-radius: var(--border-radius-small, 4px); --message-bar-close-button-border-radius: 4px;
--message-bar-close-button-border: none; --message-bar-close-button-border: none;
--message-bar-close-button-hover-bg-color: var( --message-bar-close-button-hover-bg-color: light-dark(
--button-background-color-hover, rgb(21 20 26 / 0.14),
light-dark(rgb(21 20 26 / 0.14), rgb(251 251 254 / 0.14)) rgb(251 251 254 / 0.14)
); );
--message-bar-close-button-active-bg-color: var( --message-bar-close-button-active-bg-color: light-dark(
--button-background-color-active, rgb(21 20 26 / 0.21),
light-dark(rgb(21 20 26 / 0.21), rgb(251 251 254 / 0.21)) rgb(251 251 254 / 0.21)
); );
--message-bar-close-button-focus-bg-color: var( --message-bar-close-button-focus-bg-color: light-dark(
--button-background-color, rgb(21 20 26 / 0.07),
light-dark(rgb(21 20 26 / 0.07), rgb(251 251 254 / 0.07)) rgb(251 251 254 / 0.07)
); );
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
--message-bar-close-button-color: var(--button-text-color, ButtonText); --message-bar-close-button-color: ButtonText;
--message-bar-close-button-border: 1px solid --message-bar-close-button-border: 1px solid ButtonText;
var(--button-border-color, ButtonText); --message-bar-close-button-hover-bg-color: ButtonText;
/* One icon colour is shared by hover/active/focus, so all three backgrounds --message-bar-close-button-active-bg-color: ButtonText;
read the -hover token and pair with --button-text-color-hover. The --message-bar-close-button-focus-bg-color: ButtonText;
fallbacks are SelectedItemText/SelectedItem for the same reason: the --message-bar-close-button-color-hover: HighlightText;
previous ButtonText-behind-HighlightText hid the icon outright in any
theme whose button ink and highlight text are both light. */
--message-bar-close-button-hover-bg-color: var(
--button-background-color-hover,
SelectedItemText
);
--message-bar-close-button-active-bg-color: var(
--button-background-color-hover,
SelectedItemText
);
--message-bar-close-button-focus-bg-color: var(
--button-background-color-hover,
SelectedItemText
);
--message-bar-close-button-color-hover: var(
--button-text-color-hover,
SelectedItem
);
} }
display: flex; display: flex;
@ -68,9 +50,7 @@
gap: 8px; gap: 8px;
user-select: none; user-select: none;
/* Nests inside .dialog, whose radius is --border-radius-medium, so it has to border-radius: 4px;
scale with it to keep the inner corner tighter than the outer one. */
border-radius: var(--border-radius-small, 4px);
border: 1px solid var(--message-bar-border-color); border: 1px solid var(--message-bar-border-color);
background: var(--message-bar-bg-color); background: var(--message-bar-bg-color);
@ -150,6 +130,8 @@
} }
#editorUndoBar { #editorUndoBar {
--text-primary-color: light-dark(#15141a, #fbfbfe);
--message-bar-icon: url(images/messageBar_info.svg); --message-bar-icon: url(images/messageBar_info.svg);
--message-bar-icon-color: light-dark(#0060df, #73a7f3); --message-bar-icon-color: light-dark(#0060df, #73a7f3);
--message-bar-bg-color: light-dark(#deeafc, #003070); --message-bar-bg-color: light-dark(#deeafc, #003070);
@ -159,10 +141,41 @@
rgb(255 255 255 / 0.08) rgb(255 255 255 / 0.08)
); );
--undo-button-bg-color: light-dark(
rgb(21 20 26 / 0.07),
rgb(255 255 255 / 0.08)
);
--undo-button-bg-color-hover: light-dark(
rgb(21 20 26 / 0.14),
rgb(255 255 255 / 0.14)
);
--undo-button-bg-color-active: light-dark(
rgb(21 20 26 / 0.21),
rgb(255 255 255 / 0.21)
);
--undo-button-border: 1px solid light-dark(#0060df, #0df);
--undo-button-fg-color: var(--message-bar-fg-color);
--undo-button-fg-color-hover: var(--undo-button-fg-color);
--undo-button-fg-color-active: var(--undo-button-fg-color);
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
--text-primary-color: CanvasText;
--message-bar-icon-color: CanvasText; --message-bar-icon-color: CanvasText;
--message-bar-bg-color: Canvas; --message-bar-bg-color: Canvas;
--message-bar-border-color: CanvasText; --message-bar-border-color: CanvasText;
--undo-button-bg-color: ButtonText;
--undo-button-bg-color-hover: SelectedItem;
--undo-button-bg-color-active: SelectedItem;
--undo-button-fg-color: ButtonFace;
--undo-button-fg-color-hover: SelectedItemText;
--undo-button-fg-color-active: SelectedItemText;
--undo-button-border: none;
} }
position: fixed; position: fixed;
@ -184,10 +197,26 @@
} }
#editorUndoBarUndoButton { #editorUndoBarUndoButton {
border-radius: 4px;
font-weight: 590;
line-height: 19.5px;
color: var(--undo-button-fg-color);
border: var(--undo-button-border);
padding: 4px 16px;
margin-inline-start: 8px; margin-inline-start: 8px;
/* The generic secondary border is the same translucent wash as its fill, height: 32px;
which disappears on the tinted bar, so keep an explicit accent edge. */
border-color: var(--color-accent-primary, light-dark(#0060df, #0df)); background-color: var(--undo-button-bg-color);
&:hover {
background-color: var(--undo-button-bg-color-hover);
color: var(--undo-button-fg-color-hover);
}
&:active {
background-color: var(--undo-button-bg-color-active);
color: var(--undo-button-fg-color-active);
}
} }
> div { > div {

View File

@ -936,14 +936,9 @@ class PDFPageView extends BasePDFPageView {
if (this.structTreeLayer && !this.textLayer) { if (this.structTreeLayer && !this.textLayer) {
this.structTreeLayer = null; this.structTreeLayer = null;
} }
// The annotation editor layer and the draw layer keep references on the
// text layer (the latter uses its div in order to render the selection),
// hence they must be recreated too.
if ( if (
this.annotationEditorLayer && this.annotationEditorLayer &&
(!keepAnnotationEditorLayer || (!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
!this.annotationEditorLayer.div ||
!this.textLayer)
) { ) {
if (this.drawLayer) { if (this.drawLayer) {
this.drawLayer.cancel(); this.drawLayer.cancel();
@ -952,10 +947,6 @@ class PDFPageView extends BasePDFPageView {
this.annotationEditorLayer.cancel(); this.annotationEditorLayer.cancel();
this.annotationEditorLayer = null; this.annotationEditorLayer = null;
} }
if (this.drawLayer && !this.textLayer) {
this.drawLayer.cancel();
this.drawLayer = null;
}
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) { if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
this.xfaLayer.cancel(); this.xfaLayer.cancel();
this.xfaLayer = null; this.xfaLayer = null;

View File

@ -111,7 +111,7 @@ class PDFScriptingManager {
// targeting an unknown id can be ignored. // targeting an unknown id can be ignored.
if (objects) { if (objects) {
this.#objectIds = new Set(); this.#objectIds = new Set();
for (const fields of objects.values()) { for (const fields of Object.values(objects)) {
for (const { id } of fields) { for (const { id } of fields) {
this.#objectIds.add(id); this.#objectIds.add(id);
} }

View File

@ -386,8 +386,8 @@ class PDFThumbnailViewer {
const pagesCount = this.#pagesMapper.pagesNumber; const pagesCount = this.#pagesMapper.pagesNumber;
const data = this.hasStructuralChanges() const data = this.hasStructuralChanges()
? this.getStructuralChanges() ? this.getStructuralChanges()
: { pageInfos: [{ document: null }], copyLevels: null }; : [{ document: null }];
data.pageInfos.push(...entries); data.push(...entries);
this.eventBus.on( this.eventBus.on(
"pagesloaded", "pagesloaded",
() => { () => {

View File

@ -12,22 +12,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
/*
* Firefox design-system bridge.
*
* The viewer chrome is authored against Firefox design-token names with the
* shipped literal as the fallback: `var(--fx-token, <literal>)`. GENERIC and
* the components build resolve the fallback and stay self-contained; the
* MOZCENTRAL build imports tokens-brand.css when pdfjs.enableNova is set (see
* viewer.css), so those names resolve to live Firefox values and the theme
* never has to be re-synced by hand.
*
* Firefox declares its tokens inside `@layer tokens-foundation-brand`, so any
* unlayered declaration wins over them. To *consume* a Firefox value,
* reference it through var() (with a fallback) and do NOT redeclare the same
* name: that would shadow Firefox's for the whole subtree.
*/
@import url(buttons.css);
@import url(message_bar.css); @import url(message_bar.css);
@import url(dialog.css); @import url(dialog.css);
@import url(text_layer_builder.css); @import url(text_layer_builder.css);
@ -50,12 +34,8 @@
--page-border: 9px solid transparent; --page-border: 9px solid transparent;
--spreadHorizontalWrapped-margin-LR: -3.5px; --spreadHorizontalWrapped-margin-LR: -3.5px;
--loading-icon-delay: 400ms; --loading-icon-delay: 400ms;
--focus-ring-color: var(--focus-outline-color, light-dark(#0060df, #0df)); --focus-ring-color: light-dark(#0060df, #0df);
--focus-ring-outline: var(--focus-outline-width, 2px) solid --focus-ring-outline: 2px solid var(--focus-ring-color);
var(--focus-ring-color);
--text-primary-color: var(--text-color, light-dark(#15141a, #fbfbfe));
--link-fg-color: var(--link-color, light-dark(#0060df, #0df));
--link-hover-fg-color: var(--link-color-hover, light-dark(#0250bb, #80ebff));
--new-badge-bg: light-dark(#070, #37b847); --new-badge-bg: light-dark(#070, #37b847);
--new-badge-color: light-dark(#fff, #15141a); --new-badge-color: light-dark(#fff, #15141a);
--new-badge-border-color: light-dark(#fbfbfe / 40%, #15141a / 40%); --new-badge-border-color: light-dark(#fbfbfe / 40%, #15141a / 40%);
@ -65,10 +45,7 @@
--page-margin: 8px auto -1px; --page-margin: 8px auto -1px;
--page-border: 1px solid CanvasText; --page-border: 1px solid CanvasText;
--spreadHorizontalWrapped-margin-LR: 3.5px; --spreadHorizontalWrapped-margin-LR: 3.5px;
--focus-ring-color: var(--focus-outline-color, CanvasText); --focus-ring-color: CanvasText;
--text-primary-color: var(--text-color, CanvasText);
--link-fg-color: var(--link-color, LinkText);
--link-hover-fg-color: var(--link-color-hover, LinkText);
--new-badge-bg: AccentColor; --new-badge-bg: AccentColor;
--new-badge-color: ButtonFace; --new-badge-color: ButtonFace;
--new-badge-border-color: CanvasText; --new-badge-border-color: CanvasText;

View File

@ -1600,10 +1600,13 @@ class PDFViewer {
} }
get #pageWidthScaleFactor() { get #pageWidthScaleFactor() {
return this._spreadMode !== SpreadMode.NONE && if (
this._spreadMode !== SpreadMode.NONE &&
this._scrollMode !== ScrollMode.HORIZONTAL this._scrollMode !== ScrollMode.HORIZONTAL
? 2 ) {
: 1; return 2;
}
return 1;
} }
#setScale(value, options) { #setScale(value, options) {
@ -1887,20 +1890,19 @@ class PDFViewer {
container.scrollLeft - firstPage.x, container.scrollLeft - firstPage.x,
container.scrollTop - firstPage.y container.scrollTop - firstPage.y
); );
const [left, top] = topLeft; const intLeft = Math.round(topLeft[0]);
const intTop = Math.round(topLeft[1]);
let pdfOpenParams = `#page=${pageNumber}`; let pdfOpenParams = `#page=${pageNumber}`;
if (!this.isInPresentationMode) { if (!this.isInPresentationMode) {
pdfOpenParams += pdfOpenParams += `&zoom=${normalizedScaleValue},${intLeft},${intTop}`;
`&zoom=${normalizedScaleValue},` +
`${Math.round(left)},${Math.round(top)}`;
} }
this._location = { this._location = {
pageNumber, pageNumber,
scale: normalizedScaleValue, scale: normalizedScaleValue,
top, top: intTop,
left, left: intLeft,
rotation: this._pagesRotation, rotation: this._pagesRotation,
pdfOpenParams, pdfOpenParams,
}; };

Some files were not shown because too many files have changed in this diff Show More