diff --git a/l10n/ca/viewer.ftl b/l10n/ca/viewer.ftl index 4e0d9cbb7fc1f..1c60c7b9d8569 100644 --- a/l10n/ca/viewer.ftl +++ b/l10n/ca/viewer.ftl @@ -42,6 +42,12 @@ pdfjs-print-button-label = Imprimeix pdfjs-save-button = .title = Desa pdfjs-save-button-label = Desa +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Baixa +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Baixa pdfjs-bookmark-button = .title = Pàgina actual (mostra l'URL de la pàgina actual) pdfjs-bookmark-button-label = Pàgina actual @@ -98,6 +104,14 @@ pdfjs-document-properties-button = pdfjs-document-properties-button-label = Propietats del document… pdfjs-document-properties-file-name = Nom del fitxer: pdfjs-document-properties-file-size = Mida del fitxer: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) pdfjs-document-properties-title = Títol: pdfjs-document-properties-author = Autor: pdfjs-document-properties-subject = Assumpte: @@ -136,6 +150,24 @@ pdfjs-document-properties-linearized = Vista web ràpida: pdfjs-document-properties-linearized-yes = Sí pdfjs-document-properties-linearized-no = No pdfjs-document-properties-close-button = Tanca +pdfjs-digital-signature-properties-view-certificate = Mostra el certificat +# Shown beneath an invalid signature card to explain why verification +# failed. The text comes from NSS (e.g. "Signature integrity has been +# compromised", "PKCS#7 signature could not be parsed") and is not +# itself localized — it is the underlying error message produced by +# the verification backend. +# Variables: +# $reason (String) - error message describing why the signature +# could not be verified. +pdfjs-digital-signature-properties-reason = Motiu: { $reason } +# Variables: +# $count (Number) - number of nested sub-signatures (one per earlier +# incremental revision of the document). +pdfjs-digital-signature-properties-sub-signatures = + { $count -> + [one] Una subsignatura + *[other] ({ $count }) subsignatures + } ## Print @@ -184,6 +216,11 @@ pdfjs-thumb-page-title = # $page (Number) - the page number pdfjs-thumb-page-canvas = .aria-label = Miniatura de la pàgina { $page } +# Variables: +# $page (Number) - the page number +# $total (Number) - the number of pages +pdfjs-thumb-page-title1 = + .title = Pàgina { $page } de { $total } ## Find panel button title and messages @@ -202,6 +239,21 @@ pdfjs-find-match-diacritics-checkbox-label = Respecta els diacrítics pdfjs-find-entire-word-checkbox-label = Paraules senceres pdfjs-find-reached-top = S'ha arribat al principi del document, es continua pel final pdfjs-find-reached-bottom = S'ha arribat al final del document, es continua pel principi +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } d'{ $total } coincidència + *[other] { $current } de { $total } coincidències + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Més d'{ $limit } coincidència + *[other] Més de { $limit } coincidències + } pdfjs-find-not-found = No s'ha trobat l'expressió ## Predefined zoom values @@ -251,10 +303,53 @@ pdfjs-web-fonts-disabled = Els tipus de lletra web estan desactivats: no es pode pdfjs-editor-free-text-button = .title = Text +pdfjs-editor-color-picker-free-text-input = + .title = Canvia el color del text pdfjs-editor-free-text-button-label = Text pdfjs-editor-ink-button = .title = Dibuixa +pdfjs-editor-color-picker-ink-input = + .title = Canvia el color de dibuix pdfjs-editor-ink-button-label = Dibuixa +pdfjs-editor-stamp-button = + .title = Afegeix o edita imatges +pdfjs-editor-stamp-button-label = Afegeix o edita imatges +pdfjs-editor-highlight-button = + .title = Ressalta +pdfjs-editor-highlight-button-label = Ressalta +pdfjs-highlight-floating-button1 = + .title = Ressalta + .aria-label = Ressalta +pdfjs-highlight-floating-button-label = Ressalta +pdfjs-editor-signature-button = + .title = Afegeix una signatura +pdfjs-editor-signature-button-label = Afegeix una signatura + +## Default editor aria labels + +# “Drawing” is a noun, the string is used on the editor for drawings. +pdfjs-editor-ink-editor = + .aria-label = Editor de dibuix +# Used when a signature editor is selected/hovered. +# Variables: +# $description (String) - a string describing/labeling the signature. +pdfjs-editor-signature-editor1 = + .aria-description = Editor de signatures: { $description } +pdfjs-editor-stamp-editor = + .aria-label = Editor d'imatges + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Suprimeix el dibuix +pdfjs-editor-remove-freetext-button = + .title = Suprimeix el text +pdfjs-editor-remove-stamp-button = + .title = Suprimeix la imatge +pdfjs-editor-remove-highlight-button = + .title = Suprimeix el ressaltat +pdfjs-editor-remove-signature-button = + .title = Elimina la signatura ## @@ -264,10 +359,135 @@ pdfjs-editor-free-text-size-input = Mida pdfjs-editor-ink-color-input = Color pdfjs-editor-ink-thickness-input = Gruix pdfjs-editor-ink-opacity-input = Opacitat +pdfjs-editor-stamp-add-image-button = + .title = Afegeix una imatge +pdfjs-editor-stamp-add-image-button-label = Afegeix una imatge +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Gruix +pdfjs-editor-add-signature-container = + .aria-label = Controls de signatura i signatures desades +pdfjs-editor-signature-add-signature-button = + .title = Afegeix una nova signatura +pdfjs-editor-signature-add-signature-button-label = Afegeix una nova signatura +# Used on the button to use an already saved signature. +# Variables: +# $description (String) - a string describing/labeling the signature. +pdfjs-editor-add-saved-signature-button = + .title = Signatura desada: { $description } +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor de text + .default-content = Comença a escriute... +# Used to show how many comments are present in the pdf file. +# Variables: +# $count (Number) - the number of comments. +pdfjs-editor-comments-sidebar-title = + { $count -> + [one] Comentari + *[other] Comentaris + } +pdfjs-editor-comments-sidebar-close-button = + .title = Tanca la barra lateral + .aria-label = Tanca la barra lateral +pdfjs-editor-comments-sidebar-close-button-label = Tanca la barra lateral +pdfjs-editor-comments-sidebar-no-comments-link = Més informació ## Alt-text dialog +pdfjs-editor-alt-text-button-label = Text alternatiu +pdfjs-editor-alt-text-edit-button = + .aria-label = Edita el text alternatiu +pdfjs-editor-alt-text-dialog-label = Trieu una opció +pdfjs-editor-alt-text-dialog-description = El text alternatiu és d'ajuda quan no es pot veure la imatge o quan no es carrega. +pdfjs-editor-alt-text-add-description-label = Afegeix una descripció +pdfjs-editor-alt-text-add-description-description = Una o dues frases amb la intenció de descriure el subjecte, l'entorn o les accions. +pdfjs-editor-alt-text-mark-decorative-label = Marca com a decoratiu +pdfjs-editor-alt-text-mark-decorative-description = Això s'utilitza per a imatges ornamentals, com ara vores o marques d'aigua. pdfjs-editor-alt-text-cancel-button = Cancel·la +pdfjs-editor-alt-text-save-button = Desa +pdfjs-editor-alt-text-decorative-tooltip = S'ha marcat com a decoratiu +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Per exemple, “Un jove seu a taula per menjar un àpat” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Text alternatiu + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Color de ressaltat +pdfjs-editor-colorpicker-button = + .title = Canvia el color +pdfjs-editor-colorpicker-dropdown = + .aria-label = Opcions de color +pdfjs-editor-colorpicker-yellow = + .title = Groc +pdfjs-editor-colorpicker-green = + .title = Verd +pdfjs-editor-colorpicker-blue = + .title = Blau +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Vermell + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mostra-ho tot +pdfjs-editor-highlight-show-all-button = + .title = Mostra-ho tot + +## 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. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Edita el text alternatiu (descripció de la imatge) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Afegeix text alternatiu (descripció de la imatge) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Escriviu la vostra descripció aquí... +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Descripció breu per a les persones que no poden veure la imatge o quan la imatge no es carrega. +# 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 = Aquest text alternatiu ha estat creat automàticament i pot ser inexacte. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Més informació +pdfjs-editor-new-alt-text-create-automatically-button-label = Crea el text alternatiu automàticament +pdfjs-editor-new-alt-text-not-now-button = Ara no +pdfjs-editor-new-alt-text-error-title = No s'ha pogut crear el text alternatiu automàticament +pdfjs-editor-new-alt-text-error-description = Escriviu el vostre propi text alternatiu o proveu més tard. +pdfjs-editor-new-alt-text-error-close-button = Tanca +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Text alternatiu afegit. +pdfjs-editor-new-alt-text-added-button-label = Text alternatiu afegit. + +## Image alt-text settings + +pdfjs-editor-alt-text-settings-delete-model-button = Suprimeix +pdfjs-editor-alt-text-settings-download-model-button = Baixa +pdfjs-editor-alt-text-settings-downloading-model-button = S'està descarregant… +pdfjs-editor-alt-text-settings-close-button = Tanca + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-ink = S'ha eliminat el dibuix +pdfjs-editor-undo-bar-message-stamp = S'ha eliminat la imatge +pdfjs-editor-undo-bar-message-signature = S’ha eliminat la signatura +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] S'ha eliminat una anotació + *[other] S'han eliminat { $count } anotacions + } +pdfjs-editor-undo-bar-undo-button = + .title = Desfés +pdfjs-editor-undo-bar-undo-button-label = Desfés +pdfjs-editor-undo-bar-close-button = + .title = Tanca +pdfjs-editor-undo-bar-close-button-label = Tanca ## Dialog buttons diff --git a/l10n/en-CA/viewer.ftl b/l10n/en-CA/viewer.ftl index f79758583a562..4c2daaa158811 100644 --- a/l10n/en-CA/viewer.ftl +++ b/l10n/en-CA/viewer.ftl @@ -153,6 +153,27 @@ pdfjs-document-properties-linearized = Fast Web View: pdfjs-document-properties-linearized-yes = Yes pdfjs-document-properties-linearized-no = No pdfjs-document-properties-close-button = Close +pdfjs-digital-signature-properties-view-certificate = View certificate +# Shown beneath an invalid signature card to explain why verification +# failed. The text comes from NSS (e.g. "Signature integrity has been +# compromised", "PKCS#7 signature could not be parsed") and is not +# itself localized — it is the underlying error message produced by +# the verification backend. +# Variables: +# $reason (String) - error message describing why the signature +# could not be verified. +pdfjs-digital-signature-properties-reason = Reason: { $reason } +# Variables: +# $dateObj (Date) - the signing time from the /Sig dict's /M entry. +pdfjs-digital-signature-properties-timestamp = Timestamp: { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $count (Number) - number of nested sub-signatures (one per earlier +# incremental revision of the document). +pdfjs-digital-signature-properties-sub-signatures = + { $count -> + [one] Sub-signature ({ $count }) + *[other] Sub-signatures ({ $count }) + } ## Print @@ -732,6 +753,74 @@ pdfjs-views-manager-waiting-for-file = Uploading file… pdfjs-toggle-views-manager-button1 = .title = Manage pages +## Digital signature properties (signature verification panel) + +pdfjs-digital-signature-properties-button = + .title = Digital signature properties + .aria-label = Digital signature properties +pdfjs-digital-signature-properties-button-label = Digital signature properties + +## Banner shown above the signature list summarising the overall +## verification state of the document. Each variant is selected by the +## viewer based on the worst per-signature status; one signature is +## enough to lower the banner. +## +## Variables: +## $count (Number) - number of signatures at the worst level. + +pdfjs-digital-signature-properties-banner-verified = Document was signed with a valid digital signature +pdfjs-digital-signature-properties-banner-unknown = + { $count -> + [one] Document signed but { $count } digital signature could not be verified + *[other] Document signed but { $count } digital signatures could not be verified + } +pdfjs-digital-signature-properties-banner-untrusted = + { $count -> + [one] Document signed with { $count } certificate that is not trusted + *[other] Document signed with { $count } certificates that are not trusted + } +pdfjs-digital-signature-properties-banner-expired = + { $count -> + [one] Document signed with { $count } expired certificate + *[other] Document signed with { $count } expired certificates + } +pdfjs-digital-signature-properties-banner-invalid = + { $count -> + [one] Document has { $count } invalid digital signature + *[other] Document has { $count } invalid digital signatures + } +pdfjs-digital-signature-properties-banner-revoked = + { $count -> + [one] Document signed with { $count } revoked certificate + *[other] Document signed with { $count } revoked certificates + } + +## Per-signature status row. Only three distinct strings are needed: +## the signature crypto either verified (the cert chain may still be +## untrusted/expired/revoked, but that's surfaced on the cert row +## below), or it failed, or its sub-format isn't supported. + +pdfjs-digital-signature-properties-status-verified = Status: Signature verified +pdfjs-digital-signature-properties-status-invalid = Status: Signature invalid +pdfjs-digital-signature-properties-status-unknown = Status: Unable to verify (unsupported) + +## Per-signature certificate row. The variants with an issuer / date in +## parentheses embed fully-localized context — no English fall-through. +## +## Variables: +## $issuer (String) - issuer or subject common name from the cert. +## $dateObj (Date) - notAfter date for the expired-with-date form. + +pdfjs-digital-signature-properties-certificate-trusted = Certificate: Trusted ({ $issuer }) +pdfjs-digital-signature-properties-certificate-unknown = Certificate: Unavailable +pdfjs-digital-signature-properties-certificate-untrusted = Certificate: Untrusted +pdfjs-digital-signature-properties-certificate-untrusted-unknown-issuer = Certificate: Unknown issuer ({ $issuer }) +pdfjs-digital-signature-properties-certificate-untrusted-self-signed = Certificate: Self-signed ({ $issuer }) +pdfjs-digital-signature-properties-certificate-untrusted-untrusted-issuer = Certificate: Untrusted issuer ({ $issuer }) +pdfjs-digital-signature-properties-certificate-expired = Certificate: Expired +pdfjs-digital-signature-properties-certificate-expired-with-date = Certificate: Expired ({ DATETIME($dateObj, dateStyle: "medium") }) +pdfjs-digital-signature-properties-certificate-revoked = Certificate: Revoked + ## Main menu for adding/removing signatures pdfjs-editor-delete-signature-button1 = diff --git a/l10n/fy-NL/viewer.ftl b/l10n/fy-NL/viewer.ftl index c88b1a777c3e8..828cd8545d209 100644 --- a/l10n/fy-NL/viewer.ftl +++ b/l10n/fy-NL/viewer.ftl @@ -182,8 +182,8 @@ pdfjs-print-progress-message = Dokumint tariede oar ôfdrukken… # $progress (Number) - percent value pdfjs-print-progress-percent = { $progress }% pdfjs-print-progress-close-button = Annulearje -pdfjs-printing-not-supported = Warning: Printen is net folslein stipe troch dizze browser. -pdfjs-printing-not-ready = Warning: PDF is net folslein laden om ôf te drukken. +pdfjs-printing-not-supported = Warskôging: ôfdrukken wurdt net folslein stipe troch dizze browser. +pdfjs-printing-not-ready = Warskôging: PDF is net folslein laden om ôf te drukken. ## Tooltips and alt text for side panel toolbar buttons @@ -476,7 +476,7 @@ pdfjs-editor-colorpicker-green = pdfjs-editor-colorpicker-blue = .title = Blau pdfjs-editor-colorpicker-pink = - .title = Roze + .title = Rôs pdfjs-editor-colorpicker-red = .title = Read @@ -581,7 +581,7 @@ pdfjs-editor-undo-bar-close-button-label = Slute ## Add a signature dialog -pdfjs-editor-add-signature-dialog-label = Mei dizze modal kin de brûker in hantekening meitsje om oan in PDF-dokumint ta te foegjen. De brûker kin de namme bewurkje (dy't ek tsjinnet as alternative tekst), en opsjoneel de ûndertekening bewarje foar werhelle gebrûk. +pdfjs-editor-add-signature-dialog-label = Mei dizze modal kin de brûker in hantekening meitsje om oan in PDF-dokumint ta te foegjen. De brûker kin de namme bewurkje (dy’t ek tsjinnet as alternative tekst), en opsjoneel de ûndertekening bewarje foar werhelle gebrûk. pdfjs-editor-add-signature-dialog-title = In hantekening tafoegje ## Tab names @@ -623,7 +623,7 @@ pdfjs-editor-add-signature-clear-button-label = Hantekening wiskje pdfjs-editor-add-signature-clear-button = .title = Hantekening wiskje pdfjs-editor-add-signature-save-checkbox = Hantekening bewarje -pdfjs-editor-add-signature-save-warning-message = Jo hawwe de limyt fan 5 bewarre hantekeningen berikt. Ferwiderje ien om in oar te bewarjen. +pdfjs-editor-add-signature-save-warning-message = Jo hawwe de limyt fan 5 bewarre hantekeningen berikt. Smyt ien fuort om in oar te bewarjen. pdfjs-editor-add-signature-image-upload-error-title = Kin de ôfbylding net oplade pdfjs-editor-add-signature-image-upload-error-description = Kontrolearje jo netwurkferbining of probearje in oare ôfbylding. pdfjs-editor-add-signature-image-no-data-error-title = Kin dizze ôfbylding net nei in hantekening konvertearje diff --git a/l10n/hy-AM/viewer.ftl b/l10n/hy-AM/viewer.ftl index 6ceb7181f5733..349374ce08451 100644 --- a/l10n/hy-AM/viewer.ftl +++ b/l10n/hy-AM/viewer.ftl @@ -223,8 +223,8 @@ pdfjs-find-next-button = .title = Գտիր արտահայտության հաջորդ հանդիպումը pdfjs-find-next-button-label = Հաջորդը pdfjs-find-highlight-checkbox = Գունանշել բոլորը -pdfjs-find-match-case-checkbox-label = Մեծ(փոքր)ատառ հաշվի առնել -pdfjs-find-match-diacritics-checkbox-label = Համապատասխանեցնել տարբերիչները +pdfjs-find-match-case-checkbox-label = Հաշվի առնել մեծ/փոքրատառը +pdfjs-find-match-diacritics-checkbox-label = Համընկել հնչյունատարբերիչ նշանները pdfjs-find-entire-word-checkbox-label = Ամբողջ բառերը pdfjs-find-reached-top = Հասել եք փաստաթղթի վերևին, կշարունակվի ներքևից pdfjs-find-reached-bottom = Հասել եք փաստաթղթի վերջին, կշարունակվի վերևից diff --git a/l10n/lv/viewer.ftl b/l10n/lv/viewer.ftl index e82433af296a9..e443b013343f7 100644 --- a/l10n/lv/viewer.ftl +++ b/l10n/lv/viewer.ftl @@ -198,8 +198,8 @@ pdfjs-page-scale-percent = { $scale }% ## Loading indicator messages pdfjs-loading-error = Ielādējot PDF notika kļūda. -pdfjs-invalid-file-error = Nederīgs vai bojāts PDF fails. -pdfjs-missing-file-error = PDF fails nav atrasts. +pdfjs-invalid-file-error = Nederīga vai bojāt PDF datne. +pdfjs-missing-file-error = PDF datne nav atrasta. pdfjs-unexpected-response-error = Negaidīa servera atbilde. pdfjs-rendering-error = Attēlojot lapu radās kļūda @@ -220,3 +220,55 @@ pdfjs-password-invalid = Nepareiza parole, mēģiniet vēlreiz. pdfjs-password-ok-button = Labi pdfjs-password-cancel-button = Atcelt pdfjs-web-fonts-disabled = Tīmekļa fonti nav aktivizēti: Nevar iegult PDF fontus. + +## Editing + +pdfjs-editor-stamp-button = + .title = Pievienot vai labot attēlus +pdfjs-editor-stamp-button-label = Pievienot vai labot attēlus +pdfjs-editor-signature-button = + .title = Pievienot parakstu +pdfjs-editor-signature-button-label = Pievienot parakstu + +## + +pdfjs-editor-stamp-add-image-button = + .title = Pievienot attēlu +pdfjs-editor-stamp-add-image-button-label = Pievienot attēlu +pdfjs-editor-signature-add-signature-button = + .title = Pievienot jaunu parakstu +pdfjs-editor-signature-add-signature-button-label = Pievienot jaunu parakstu + +## Alt-text dialog + +pdfjs-editor-alt-text-add-description-label = Pievienot aprakstu + +## Add a signature dialog + +pdfjs-editor-add-signature-dialog-title = Pievienot parakstu + +## Dialog buttons + +pdfjs-editor-add-signature-add-button = Pievienot + +## Edit a comment dialog + +# No existing comment +pdfjs-editor-edit-comment-dialog-title-when-adding = Pievienot piebildi +pdfjs-editor-edit-comment-dialog-save-button-when-adding = Pievienot + +## Edit a comment button in the editor toolbar + +pdfjs-editor-add-comment-button = + .title = Pievienot piebildi + +## 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-add-file-button = + .title = Pievienot datni +pdfjs-views-manager-add-file-button-label = Pievienot datni diff --git a/l10n/sk/viewer.ftl b/l10n/sk/viewer.ftl index f55766c6ea5bc..0feefc137e868 100644 --- a/l10n/sk/viewer.ftl +++ b/l10n/sk/viewer.ftl @@ -107,11 +107,11 @@ pdfjs-document-properties-file-size = Veľkosť súboru: # Variables: # $kb (Number) - the PDF file size in kilobytes # $b (Number) - the PDF file size in bytes -pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kB ({ $b } bajtov) +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kB ({ $b } bajtov) # Variables: # $mb (Number) - the PDF file size in megabytes # $b (Number) - the PDF file size in bytes -pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bajtov) +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bajtov) pdfjs-document-properties-title = Názov: pdfjs-document-properties-author = Autor: pdfjs-document-properties-subject = Predmet: @@ -517,8 +517,8 @@ pdfjs-editor-new-alt-text-error-close-button = Zavrieť # Variables: # $totalSize (Number) - the total 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 = Sťahuje sa model AI pre alternatívne texty ({ $downloadedSize } z { $totalSize } MB) - .aria-valuetext = Sťahuje sa model AI pre alternatívne texty ({ $downloadedSize } z { $totalSize } MB) +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Sťahuje sa model AI pre alternatívne texty ({ $downloadedSize } z { $totalSize } MB) + .aria-valuetext = Sťahuje sa model AI pre alternatívne texty ({ $downloadedSize } z { $totalSize } MB) # This is a button that users can click to edit the alt text they have already added. pdfjs-editor-new-alt-text-added-button = .aria-label = Alternatívny text bol pridaný diff --git a/l10n/zh-CN/viewer.ftl b/l10n/zh-CN/viewer.ftl index 2f48b10a49329..29f43645900cf 100644 --- a/l10n/zh-CN/viewer.ftl +++ b/l10n/zh-CN/viewer.ftl @@ -713,6 +713,39 @@ pdfjs-views-manager-waiting-for-file = 正在上传文件… pdfjs-toggle-views-manager-button1 = .title = 管理页面 +## Digital signature properties (signature verification panel) + +pdfjs-digital-signature-properties-button = + .title = 数字签名属性 + .aria-label = 数字签名属性 +pdfjs-digital-signature-properties-button-label = 数字签名属性 + +## Per-signature status row. Only three distinct strings are needed: +## the signature crypto either verified (the cert chain may still be +## untrusted/expired/revoked, but that's surfaced on the cert row +## below), or it failed, or its sub-format isn't supported. + +pdfjs-digital-signature-properties-status-verified = 状态:签名已验证 +pdfjs-digital-signature-properties-status-invalid = 状态:签名无效 +pdfjs-digital-signature-properties-status-unknown = 状态:无法验证(不支持) + +## Per-signature certificate row. The variants with an issuer / date in +## parentheses embed fully-localized context — no English fall-through. +## +## Variables: +## $issuer (String) - issuer or subject common name from the cert. +## $dateObj (Date) - notAfter date for the expired-with-date form. + +pdfjs-digital-signature-properties-certificate-trusted = 证书:受信任({ $issuer }) +pdfjs-digital-signature-properties-certificate-unknown = 证书:不可用 +pdfjs-digital-signature-properties-certificate-untrusted = 证书:不可信 +pdfjs-digital-signature-properties-certificate-untrusted-unknown-issuer = 证书:颁发者未知({ $issuer }) +pdfjs-digital-signature-properties-certificate-untrusted-self-signed = 证书:自签名({ $issuer }) +pdfjs-digital-signature-properties-certificate-untrusted-untrusted-issuer = 证书: 颁发者不可信({ $issuer }) +pdfjs-digital-signature-properties-certificate-expired = 证书: 已过期 +pdfjs-digital-signature-properties-certificate-expired-with-date = 证书:已过期({ DATETIME($dateObj, dateStyle: "medium") }) +pdfjs-digital-signature-properties-certificate-revoked = 证书:已吊销 + ## Main menu for adding/removing signatures pdfjs-editor-delete-signature-button1 = diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 9e55448541c45..15b0af58725cf 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -480,20 +480,11 @@ class AnnotationEditorLayer { true, /* updateButton = */ true ); - this.#textLayer.div.classList.add("free"); - this.toggleDrawing(); - HighlightEditor.startHighlighting( + HighlightEditor.startDrawing( this, + this.#uiManager, this.#uiManager.direction === "ltr", - { target: this.#textLayer.div, x: event.x, y: event.y } - ); - this.#textLayer.div.addEventListener( - "pointerup", - () => { - this.#textLayer.div.classList.remove("free"); - this.toggleDrawing(true); - }, - { once: true, signal: this.#uiManager._signal } + event ); event.preventDefault(); } diff --git a/src/display/editor/color_picker.js b/src/display/editor/color_picker.js index 03b5e2080e934..75c732cde0eb5 100644 --- a/src/display/editor/color_picker.js +++ b/src/display/editor/color_picker.js @@ -158,7 +158,7 @@ class ColorPicker { type: AnnotationEditorParamsType.HIGHLIGHT_COLOR, value: color, }); - this.updateColor(color); + this.update(color); } _colorSelectFromKeyboard(event) { @@ -279,7 +279,7 @@ class ColorPicker { }); } - updateColor(color) { + update(color) { if (this.#buttonSwatch) { this.#buttonSwatch.style.backgroundColor = color; } diff --git a/src/display/editor/draw.js b/src/display/editor/draw.js index c2b7d98a52726..71a49c934e9a0 100644 --- a/src/display/editor/draw.js +++ b/src/display/editor/draw.js @@ -13,10 +13,15 @@ * limitations under the License. */ +// eslint-disable-next-line max-len +/** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */ +// eslint-disable-next-line max-len +/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ + import { AnnotationEditorParamsType, unreachable } from "../../shared/util.js"; +import { bindEvents, CurrentPointers } from "./tools.js"; import { noContextMenu, stopEvent } from "../display_utils.js"; import { AnnotationEditor } from "./editor.js"; -import { CurrentPointers } from "./tools.js"; class DrawingOptions { #svgProperties = Object.create(null); @@ -64,14 +69,20 @@ class DrawingOptions { * Basic draw editor. */ class DrawingEditor extends AnnotationEditor { - #drawOutlines = null; + #internalDiv = null; #mustBeCommitted; + _clipPathId = null; + _colorPicker = null; _drawId = null; + _drawOutlines = null; + + _focusDrawId = null; + static _currentDrawId = -1; static _currentParent = null; @@ -82,6 +93,8 @@ class DrawingEditor extends AnnotationEditor { static #currentDrawingOptions = null; + static #currentClipPathId = null; + static _INNER_MARGIN = 3; constructor(params) { @@ -109,8 +122,8 @@ class DrawingEditor extends AnnotationEditor { } } - #createDrawOutlines({ drawOutlines, drawId, drawingOptions }) { - this.#drawOutlines = drawOutlines; + #createDrawOutlines({ drawOutlines, drawId, drawingOptions, clipPathId }) { + this._drawOutlines = drawOutlines; this._drawingOptions ||= drawingOptions; if (!this.annotationElementId) { this._uiManager.a11yAlert(AnnotationEditor._l10nAlert[this.editorType]); @@ -118,12 +131,14 @@ class DrawingEditor extends AnnotationEditor { if (drawId >= 0) { this._drawId = drawId; + this._clipPathId = clipPathId ?? null; // We need to redraw the drawing because we changed the coordinates to be // in the box coordinate system. this.parent.drawLayer.finalizeDraw( drawId, drawOutlines.defaultProperties ); + this.#createFocusOutline(this.parent); } else { // We create a new drawing. this._drawId = this.#createDrawing(drawOutlines, this.parent); @@ -133,17 +148,62 @@ class DrawingEditor extends AnnotationEditor { } #createDrawing(drawOutlines, parent) { - const { id } = parent.drawLayer.draw( + const { id, clipPathId } = parent.drawLayer.draw( DrawingEditor._mergeSVGProperties( this._drawingOptions.toSVGProperties(), drawOutlines.defaultSVGProperties ), /* isPathUpdatable = */ false, - /* hasClip = */ false + /* hasClip = */ this.constructor._hasClipPath ); + if (this.constructor._hasClipPath) { + this._clipPathId = clipPathId; + } + this.#createFocusOutline(parent); + return id; } + #createFocusOutline(parent) { + const properties = this._drawOutlines.getFocusSVGProperties( + this.#rotationAngle + ); + if (properties) { + this._focusDrawId = parent.drawLayer.drawOutline( + properties, + this._drawOutlines.focusMustRemoveSelfIntersections + ); + } + } + + #updateFocusOutline(angle = this.#rotationAngle) { + if (this._focusDrawId === null) { + return; + } + this.parent?.drawLayer.updateProperties( + this._focusDrawId, + this._drawOutlines.getFocusSVGProperties(angle) + ); + } + + #toggleFocusOutlineClass(rootClass) { + if (this._focusDrawId !== null) { + this.parent?.drawLayer.updateProperties(this._focusDrawId, { rootClass }); + } + } + + #updateVisibility() { + const { parent, _drawId, _focusDrawId, _isVisible } = this; + if (!parent || _drawId === null) { + return; + } + const rootClass = { hidden: !_isVisible }; + parent.drawLayer.updateProperties(_drawId, { rootClass }); + if (_focusDrawId !== null) { + parent.drawLayer.updateProperties(_focusDrawId, { rootClass }); + } + } + static _mergeSVGProperties(p1, p2) { const p1Keys = new Set(Object.keys(p1)); @@ -178,6 +238,14 @@ class DrawingEditor extends AnnotationEditor { return true; } + static get _hasClipPath() { + return false; + } + + static get _hasDrawClass() { + return true; + } + /** * @returns {boolean} `true` if several drawings can be added to the * annotation. @@ -186,6 +254,18 @@ class DrawingEditor extends AnnotationEditor { return false; } + get _drawRotation() { + return this.rotation; + } + + get _opacityName() { + return this.constructor.typesMap.get(this.opacityType); + } + + get #rotationAngle() { + return (this.parentRotation - this._drawRotation + 360) % 360; + } + /** @inheritdoc */ static updateDefaultParams(type, value) { const propertyName = this.typesMap.get(type); @@ -238,7 +318,7 @@ class DrawingEditor extends AnnotationEditor { const savedValue = options[name]; const setter = val => { options.updateProperty(name, val); - const bbox = this.#drawOutlines.updateProperty(name, val); + const bbox = this._drawOutlines.updateProperty(name, val); if (bbox) { this.#updateBbox(bbox); } @@ -266,17 +346,17 @@ class DrawingEditor extends AnnotationEditor { /** * Update color and opacity atomically as one undoable command. */ - _updateColorAndOpacity(color, opacity) { + _updateColorAndOpacity(color, opacity, type = this.colorAndOpacityType) { const colorName = this.constructor.typesMap.get(this.colorType); - const opacityName = this.constructor.typesMap.get(this.opacityType); + const opacityName = this._opacityName; const options = this._drawingOptions; const savedColor = options[colorName]; const savedOpacity = options[opacityName]; const setter = (c, op) => { options.updateProperty(colorName, c); options.updateProperty(opacityName, op); - this.#drawOutlines.updateProperty(colorName, c); - this.#drawOutlines.updateProperty(opacityName, op); + this._drawOutlines.updateProperty(colorName, c); + this._drawOutlines.updateProperty(opacityName, op); this.parent?.drawLayer.updateProperties( this._drawId, options.toSVGProperties() @@ -289,7 +369,7 @@ class DrawingEditor extends AnnotationEditor { undo: setter.bind(this, savedColor, savedOpacity), post: this._uiManager.updateUI.bind(this._uiManager, this), mustExec: true, - type: AnnotationEditorParamsType.INK_COLOR_AND_OPACITY, + type, overwriteIfSameType: true, keepUndo: true, }); @@ -300,7 +380,7 @@ class DrawingEditor extends AnnotationEditor { this.parent?.drawLayer.updateProperties( this._drawId, DrawingEditor._mergeSVGProperties( - this.#drawOutlines.getPathResizingSVGProperties( + this._drawOutlines.getPathResizingSVGProperties( this.#convertToDrawSpace() ), { @@ -315,7 +395,7 @@ class DrawingEditor extends AnnotationEditor { this.parent?.drawLayer.updateProperties( this._drawId, DrawingEditor._mergeSVGProperties( - this.#drawOutlines.getPathResizedSVGProperties( + this._drawOutlines.getPathResizedSVGProperties( this.#convertToDrawSpace() ), { @@ -323,6 +403,7 @@ class DrawingEditor extends AnnotationEditor { } ) ); + this.#updateFocusOutline(); } /** @inheritdoc */ @@ -337,7 +418,7 @@ class DrawingEditor extends AnnotationEditor { this.parent?.drawLayer.updateProperties( this._drawId, DrawingEditor._mergeSVGProperties( - this.#drawOutlines.getPathTranslatedSVGProperties( + this._drawOutlines.getPathTranslatedSVGProperties( this.#convertToDrawSpace(), this.parentDimensions ), @@ -364,12 +445,18 @@ class DrawingEditor extends AnnotationEditor { }); } + get _mustBeDisabledOnCommit() { + return true; + } + /** @inheritdoc */ commit() { super.commit(); - this.disableEditMode(); - this.disableEditing(); + if (this._mustBeDisabledOnCommit) { + this.disableEditMode(); + this.disableEditing(); + } } /** @inheritdoc */ @@ -414,6 +501,7 @@ class DrawingEditor extends AnnotationEditor { /** @inheritdoc */ remove() { + this._uiManager.removeShouldRescale(this); this.#cleanDrawLayer(); super.remove(); } @@ -429,7 +517,7 @@ class DrawingEditor extends AnnotationEditor { } this.#addToDrawLayer(); - this.#updateBbox(this.#drawOutlines.box); + this.#updateBbox(this._drawOutlines.box); if (!this.isAttachedToDOM) { // At some point this editor was removed and we're rebuilding it, @@ -452,6 +540,7 @@ class DrawingEditor extends AnnotationEditor { !this.parent && this.div?.classList.contains("selectedEditor"); } super.setParent(parent); + this.#updateVisibility(); if (mustBeSelected) { // We select it after the parent has been set. this.select(); @@ -462,8 +551,13 @@ class DrawingEditor extends AnnotationEditor { if (this._drawId === null || !this.parent) { return; } - this.parent.drawLayer.remove(this._drawId); + const { drawLayer } = this.parent; + drawLayer.remove(this._drawId); this._drawId = null; + if (this._focusDrawId !== null) { + drawLayer.remove(this._focusDrawId); + this._focusDrawId = null; + } // All the SVG properties must be reset in order to make it possible to // undo. @@ -476,17 +570,24 @@ class DrawingEditor extends AnnotationEditor { } if (this._drawId !== null) { // The parent has changed, we need to move the drawing to the new parent. - this.parent.drawLayer.updateParent(this._drawId, parent.drawLayer); + const { drawLayer } = this.parent; + drawLayer.updateParent(this._drawId, parent.drawLayer); + if (this._focusDrawId !== null) { + drawLayer.updateParent(this._focusDrawId, parent.drawLayer); + } return; } this._drawingOptions.updateAll(); - this._drawId = this.#createDrawing(this.#drawOutlines, parent); + this._drawId = this.#createDrawing(this._drawOutlines, parent); + if (this._clipPathId && this.#internalDiv) { + this.#internalDiv.style.clipPath = this._clipPathId; + } } #convertToParentSpace([x, y, width, height]) { const { parentDimensions: [pW, pH], - rotation, + _drawRotation: rotation, } = this; switch (rotation) { case 90: @@ -507,7 +608,7 @@ class DrawingEditor extends AnnotationEditor { width, height, parentDimensions: [pW, pH], - rotation, + _drawRotation: rotation, } = this; switch (rotation) { case 90: @@ -531,7 +632,7 @@ class DrawingEditor extends AnnotationEditor { this._onResized(); } - #rotateBox() { + #rotateBox(parentRotation = this.parentRotation) { // We've to deal with two rotations: the rotation of the annotation and the // rotation of the parent page. // When the page is rotated, all the layers are just rotated thanks to CSS @@ -549,8 +650,7 @@ class DrawingEditor extends AnnotationEditor { y, width, height, - rotation, - parentRotation, + _drawRotation: rotation, parentDimensions: [pW, pH], } = this; switch ((rotation * 4 + parentRotation) / 90) { @@ -635,34 +735,68 @@ class DrawingEditor extends AnnotationEditor { } } - /** @inheritdoc */ - rotate() { - if (!this.parent) { + /** + * @inheritdoc + * @param {number} [parentRotation] - The parent rotation to apply. + */ + rotate(parentRotation = this.parentRotation) { + if (!this.parent || this._drawId === null) { return; } + const angle = (parentRotation - this._drawRotation + 360) % 360; this.parent.drawLayer.updateProperties( this._drawId, DrawingEditor._mergeSVGProperties( { - bbox: this.#rotateBox(), + bbox: this.#rotateBox(parentRotation), }, - this.#drawOutlines.updateRotation( - (this.parentRotation - this.rotation + 360) % 360 - ) + this._drawOutlines.updateRotation(angle) ) ); + this.#updateFocusOutline(angle); + } + + /** @inheritdoc */ + show(visible = this._isVisible) { + super.show(visible); + this.#updateVisibility(); + } + + /** @inheritdoc */ + select() { + super.select(); + this.#toggleFocusOutlineClass({ hovered: false, selected: true }); + } + + /** @inheritdoc */ + unselect() { + super.unselect(); + this.#toggleFocusOutlineClass({ selected: false }); + } + + pointerover() { + if (!this.isSelected) { + this.#toggleFocusOutlineClass({ hovered: true }); + } + } + + pointerleave() { + if (!this.isSelected) { + this.#toggleFocusOutlineClass({ hovered: false }); + } } onScaleChanging() { if (!this.parent) { return; } - this.#updateBbox( - this.#drawOutlines.updateParentDimensions( - this.parentDimensions, - this.parent.scale - ) + const bbox = this._drawOutlines.updateParentDimensions( + this.parentDimensions, + this.parent.scale ); + if (bbox) { + this.#updateBbox(bbox); + } } static onScaleChangingWhenDrawing() {} @@ -680,12 +814,18 @@ class DrawingEditor extends AnnotationEditor { } const div = super.render(); - div.classList.add("draw"); + if (this.constructor._hasDrawClass) { + div.classList.add("draw"); + } - const drawDiv = document.createElement("div"); + const drawDiv = (this.#internalDiv = document.createElement("div")); div.append(drawDiv); drawDiv.setAttribute("aria-hidden", "true"); drawDiv.className = "internal"; + if (this._clipPathId) { + drawDiv.style.clipPath = this._clipPathId; + } + bindEvents(this, drawDiv, ["pointerover", "pointerleave"]); this.setDims(); this._uiManager.addShouldRescale(this); this.disableEditing(); @@ -698,18 +838,68 @@ class DrawingEditor extends AnnotationEditor { } /** - * Create a new drawer instance. - * @param {number} x - The x coordinate of the event. - * @param {number} y - The y coordinate of the event. - * @param {number} parentWidth - The parent width. - * @param {number} parentHeight - The parent height. - * @param {number} rotation - The parent rotation. + * @param {Object} params + * @param {number} params.x - The x coordinate of the event. + * @param {number} params.y - The y coordinate of the event. + * @param {Array} params.box - The target's client bounding box. + * @param {number} params.rotation - The viewport rotation. + * @param {AnnotationEditorLayer} params.parent - The parent layer. + * @param {boolean} params.isLTR - Whether the direction is left-to-right. */ - static createDrawerInstance(_x, _y, _parentWidth, _parentHeight, _rotation) { + static createDrawerInstance(_params) { unreachable("Not implemented"); } - static startDrawing(parent, uiManager, _isLTR, event) { + /** + * @param {AnnotationEditorLayer} _parent + * @param {PointerEvent} event + * @returns {HTMLElement} + */ + static _getDrawingTarget(_parent, { target }) { + return target; + } + + /** + * @param {PointerEvent} event + * @param {PointerEvent} [referenceEvent] + * @returns {Array} + */ + static _getPointerCoords( + { offsetX, offsetY, clientX, clientY }, + referenceEvent = null + ) { + if (!referenceEvent) { + return [offsetX, offsetY]; + } + + let deltaX = clientX - referenceEvent.clientX; + let deltaY = clientY - referenceEvent.clientY; + switch (this._currentParent.viewport.rotation) { + case 90: + [deltaX, deltaY] = [deltaY, -deltaX]; + break; + case 180: + [deltaX, deltaY] = [-deltaX, -deltaY]; + break; + case 270: + [deltaX, deltaY] = [-deltaY, deltaX]; + break; + } + return [referenceEvent.offsetX + deltaX, referenceEvent.offsetY + deltaY]; + } + + /** + * @param {HTMLElement} _target + * @param {AbortSignal} _signal + */ + static _addDrawingListeners(_target, _signal) {} + + /** @param {boolean} isAborted */ + static _endDrawingSession(isAborted = false) { + return this._currentParent.endDrawingSession(isAborted); + } + + static startDrawing(parent, uiManager, isLTR, event) { // The pointerType of CurrentPointer is set when the user starts an empty // drawing session. If, in the same drawing session, the user starts using a // different type of pointer (e.g. a pen and then a finger), we just return. @@ -717,16 +907,22 @@ class DrawingEditor extends AnnotationEditor { // If the user starts to draw with a finger and then uses a second finger, // we just stop the current drawing and let the user zoom the document. - const { target, offsetX: x, offsetY: y, pointerId, pointerType } = event; + const { pointerId, pointerType } = event; if (CurrentPointers.isInitializedAndDifferentPointerType(pointerType)) { return; } + const target = this._getDrawingTarget(parent, event); + const [x, y] = this._getPointerCoords(event); const { viewport: { rotation }, } = parent; - const { width: parentWidth, height: parentHeight } = - target.getBoundingClientRect(); + const { + x: boxX, + y: boxY, + width: parentWidth, + height: parentHeight, + } = target.getBoundingClientRect(); const ac = (DrawingEditor.#currentDrawingAC = new AbortController()); const signal = parent.combinedSignal(ac); @@ -746,7 +942,7 @@ class DrawingEditor extends AnnotationEditor { "pointercancel", e => { if (CurrentPointers.isSamePointerIdOrRemove(e.pointerId)) { - this._currentParent.endDrawingSession(); + this._endDrawingSession(); } }, { signal } @@ -768,7 +964,7 @@ class DrawingEditor extends AnnotationEditor { if (DrawingEditor.#currentDraw.isCancellable()) { DrawingEditor.#currentDraw.removeLastElement(); if (DrawingEditor.#currentDraw.isEmpty()) { - this._currentParent.endDrawingSession(/* isAborted = */ true); + this._endDrawingSession(/* isAborted = */ true); } else { this._endDraw(null); } @@ -794,6 +990,7 @@ class DrawingEditor extends AnnotationEditor { }, { signal } ); + this._addDrawingListeners(target, signal); parent.toggleDrawing(); uiManager._editorUndoBar?.hide(); @@ -813,24 +1010,27 @@ class DrawingEditor extends AnnotationEditor { uiManager.updateUIForDefaultProperties(this); - DrawingEditor.#currentDraw = this.createDrawerInstance( + DrawingEditor.#currentDraw = this.createDrawerInstance({ x, y, - parentWidth, - parentHeight, - rotation - ); + box: [boxX, boxY, parentWidth, parentHeight], + rotation, + parent, + isLTR, + }); DrawingEditor.#currentDrawingOptions = this.getDefaultDrawingOptions(); this._currentParent = parent; - ({ id: this._currentDrawId } = parent.drawLayer.draw( + const { id, clipPathId } = parent.drawLayer.draw( this._mergeSVGProperties( DrawingEditor.#currentDrawingOptions.toSVGProperties(), DrawingEditor.#currentDraw.defaultSVGProperties ), /* isPathUpdatable = */ true, - /* hasClip = */ false - )); + /* hasClip = */ this._hasClipPath + ); + this._currentDrawId = id; + DrawingEditor.#currentClipPathId = this._hasClipPath ? clipPathId : null; } static _drawMove(event) { @@ -838,9 +1038,7 @@ class DrawingEditor extends AnnotationEditor { if (!DrawingEditor.#currentDraw) { return; } - const { offsetX, offsetY, clientX, clientY, pointerId } = event; - - if (!CurrentPointers.isSamePointerId(pointerId)) { + if (!CurrentPointers.isSamePointerId(event.pointerId)) { return; } if (CurrentPointers.isUsingMultiplePointers()) { @@ -852,34 +1050,18 @@ class DrawingEditor extends AnnotationEditor { // A pointermove can represent multiple coalesced pointer updates. When // available, feed each sample to the outliner so it receives the // intermediate positions. - // offsetX/offsetY are computed in the target's untransformed coordinate - // system, whereas clientX/clientY are viewport coordinates. Since - // [data-main-rotation] transforms the layer, rotate each client-space delta - // back before adding it to the dispatched event's offsets. let properties; const coalesced = event.getCoalescedEvents?.(); if (coalesced?.length) { - const { rotation } = this._currentParent.viewport; const points = []; - for (const { clientX: x, clientY: y } of coalesced) { - let deltaX = x - clientX; - let deltaY = y - clientY; - switch (rotation) { - case 90: - [deltaX, deltaY] = [deltaY, -deltaX]; - break; - case 180: - [deltaX, deltaY] = [-deltaX, -deltaY]; - break; - case 270: - [deltaX, deltaY] = [-deltaY, deltaX]; - break; - } - points.push(offsetX + deltaX, offsetY + deltaY); + for (const sample of coalesced) { + points.push(...this._getPointerCoords(sample, event)); } properties = DrawingEditor.#currentDraw.addPoints(points); } else { - properties = DrawingEditor.#currentDraw.add(offsetX, offsetY); + properties = DrawingEditor.#currentDraw.add( + ...this._getPointerCoords(event) + ); } this._currentParent.drawLayer.updateProperties( this._currentDrawId, @@ -896,6 +1078,7 @@ class DrawingEditor extends AnnotationEditor { this._currentParent = null; DrawingEditor.#currentDraw = null; DrawingEditor.#currentDrawingOptions = null; + DrawingEditor.#currentClipPathId = null; CurrentPointers.clearTimeStamp(); } @@ -920,7 +1103,7 @@ class DrawingEditor extends AnnotationEditor { parent.drawLayer.updateProperties( this._currentDrawId, event?.target === parent.div - ? DrawingEditor.#currentDraw.end(event.offsetX, event.offsetY) + ? DrawingEditor.#currentDraw.end(...this._getPointerCoords(event)) : DrawingEditor.#currentDraw.end() ); if (this.supportMultipleDrawings) { @@ -966,6 +1149,7 @@ class DrawingEditor extends AnnotationEditor { false, { drawId: this._currentDrawId, + clipPathId: DrawingEditor.#currentClipPathId, drawOutlines: DrawingEditor.#currentDraw.getOutlines( pageWidth * scale, pageHeight * scale, @@ -997,8 +1181,9 @@ class DrawingEditor extends AnnotationEditor { * @param {number} pageY - The y coordinate of the page. * @param {number} pageWidth - The width of the page. * @param {number} pageHeight - The height of the page. - * @param {number} innerWidth - The inner width. + * @param {number} innerMargin - The outline's inner margin. * @param {Object} data - The data to deserialize. + * @param {AnnotationEditorUIManager} uiManager * @returns {Object} The deserialized outlines. */ static deserializeDraw( @@ -1006,8 +1191,9 @@ class DrawingEditor extends AnnotationEditor { _pageY, _pageWidth, _pageHeight, - _innerWidth, - _data + _innerMargin, + _data, + _uiManager ) { unreachable("Not implemented"); } @@ -1023,7 +1209,8 @@ class DrawingEditor extends AnnotationEditor { pageWidth, pageHeight, this._INNER_MARGIN, - data + data, + uiManager ); const editor = await super.deserialize(data, parent, uiManager); editor.createDrawingOptions(data); @@ -1038,7 +1225,7 @@ class DrawingEditor extends AnnotationEditor { serializeDraw(isForCopying) { const [pageX, pageY] = this.pageTranslation; const [pageWidth, pageHeight] = this.pageDimensions; - return this.#drawOutlines.serialize( + return this._drawOutlines.serialize( [pageX, pageY, pageWidth, pageHeight], isForCopying ); diff --git a/src/display/editor/drawers/freedraw.js b/src/display/editor/drawers/freedraw.js index 7ec0517f0f8ba..a842f8f35a96d 100644 --- a/src/display/editor/drawers/freedraw.js +++ b/src/display/editor/drawers/freedraw.js @@ -56,7 +56,7 @@ class FreeDrawOutliner { static #MIN = FreeDrawOutliner.#MIN_DIST + FreeDrawOutliner.#MIN_DIFF; - constructor({ x, y }, box, scaleFactor, thickness, isLTR, innerMargin = 0) { + constructor(x, y, box, scaleFactor, thickness, isLTR, innerMargin = 0) { this.#box = box; this.#thickness = thickness * scaleFactor; this.#isLTR = isLTR; @@ -75,6 +75,19 @@ class FreeDrawOutliner { return isNaN(this.#last[8]); } + isCancellable() { + // Treat strokes of at most five points as cancellable. + return this.#points.length <= 10; + } + + /** @returns {Object} The SVG properties to apply. */ + removeLastElement() { + this.#last.fill(NaN); + this.#top.length = this.#bottom.length = this.#points.length = 0; + + return { path: { d: "" } }; + } + #getLastCoords() { const lastTop = this.#last.subarray(4, 6); const lastBottom = this.#last.subarray(16, 18); @@ -88,7 +101,7 @@ class FreeDrawOutliner { ]; } - add({ x, y }) { + add(x, y) { this.#lastX = x; this.#lastY = y; const [layerX, layerY, layerWidth, layerHeight] = this.#box; @@ -627,9 +640,10 @@ class FreeDrawOutline extends Outline { return this.#bbox; } - newOutliner(point, box, scaleFactor, thickness, isLTR, innerMargin = 0) { + newOutliner(x, y, box, scaleFactor, thickness, isLTR, innerMargin = 0) { return new FreeDrawOutliner( - point, + x, + y, box, scaleFactor, thickness, @@ -638,6 +652,21 @@ class FreeDrawOutline extends Outline { ); } + /** + * @param {number} thickness + * @returns {Float32Array} The new bounding box. + */ + updateThickness(thickness) { + const outline = this.getNewOutline(thickness); + this.#outline = outline.#outline; + this.#points = outline.#points; + this.#bbox.set(outline.#bbox); + this.firstPoint = outline.firstPoint; + this.lastPoint = outline.lastPoint; + + return this.#bbox; + } + getNewOutline(thickness, innerMargin) { // Build the outline of the highlight to use as the focus outline. const [x, y, width, height] = this.#bbox; @@ -646,22 +675,18 @@ class FreeDrawOutline extends Outline { const sy = height * layerHeight; const tx = x * layerWidth + layerX; const ty = y * layerHeight + layerY; + const points = this.#points; const outliner = this.newOutliner( - { - x: this.#points[0] * sx + tx, - y: this.#points[1] * sy + ty, - }, + points[0] * sx + tx, + points[1] * sy + ty, this.#box, this.#scaleFactor, thickness, this.#isLTR, innerMargin ?? this.#innerMargin ); - for (let i = 2; i < this.#points.length; i += 2) { - outliner.add({ - x: this.#points[i] * sx + tx, - y: this.#points[i + 1] * sy + ty, - }); + for (let i = 2, ii = points.length; i < ii; i += 2) { + outliner.add(points[i] * sx + tx, points[i + 1] * sy + ty); } return outliner.getOutlines(); } diff --git a/src/display/editor/drawers/highlight.js b/src/display/editor/drawers/highlight.js index 9b97fdf7457f9..7e5a830fe4c5e 100644 --- a/src/display/editor/drawers/highlight.js +++ b/src/display/editor/drawers/highlight.js @@ -17,6 +17,48 @@ import { BBOX_INIT, Util } from "../../../shared/util.js"; import { FreeDrawOutline, FreeDrawOutliner } from "./freedraw.js"; import { Outline } from "./outline.js"; +/** + * @param {Outline} outline + * @returns {Object} + */ +function getHighlightSVGProperties(outline) { + return { + bbox: outline.box, + root: { + viewBox: "0 0 1 1", + }, + rootClass: { + highlight: true, + free: outline.isFree, + }, + path: { + d: outline.toSVGPath(), + }, + }; +} + +/** + * @param {Outline} outline + * @param {number} rotation + * @returns {Object} + */ +function getHighlightFocusSVGProperties(outline, rotation) { + const { focusOutline } = outline; + return { + bbox: Outline._rotateBox(focusOutline.box, rotation), + root: { + "data-main-rotation": rotation, + }, + rootClass: { + highlightOutline: true, + free: outline.isFree, + }, + path: { + d: focusOutline.toSVGPath(), + }, + }; +} + class HighlightOutliner { #box; @@ -288,6 +330,8 @@ class HighlightOutliner { class HighlightOutline extends Outline { #box; + #boxes = null; + #outlines; constructor(outlines, box, firstPoint, lastPoint) { @@ -298,6 +342,69 @@ class HighlightOutline extends Outline { this.lastPoint = lastPoint; } + /** + * Build a text selection and its hover/selection outline. + * @param {Array} boxes - the boxes of the selected text. + * @param {boolean} isLTR + * @returns {HighlightOutline} + */ + static build(boxes, isLTR) { + // The boxes come from the text layer, hence two contiguous ones can be + // separated by a tiny gap: expanding them makes them overlap and the + // sweep line merges them into a single gapless outline. + const outline = new HighlightOutliner( + boxes, + /* borderWidth = */ 0.001 + ).getOutlines(); + outline.#boxes = boxes; + // Expand the focus outline and leave room for its stroke. + outline.focusOutline = new HighlightOutliner( + boxes, + /* borderWidth = */ 0.0025, + /* innerMargin = */ 0.001, + isLTR + ).getOutlines(); + + return outline; + } + + get isFree() { + return false; + } + + /** @inheritdoc */ + get defaultSVGProperties() { + return getHighlightSVGProperties(this); + } + + /** @inheritdoc */ + getFocusSVGProperties(rotation) { + return getHighlightFocusSVGProperties(this, rotation); + } + + /** @inheritdoc */ + updateRotation(rotation) { + return { root: { "data-main-rotation": rotation } }; + } + + /** @inheritdoc */ + serializeQuadPoints([pageX, pageY], [pageWidth, pageHeight]) { + const boxes = this.#boxes; + const quadPoints = new Float32Array(boxes.length * 8); + let i = 0; + for (const { x, y, width, height } of boxes) { + const sx = x * pageWidth + pageX; + const sy = (1 - y) * pageHeight + pageY; + // QuadPoints order: top-left, top-right, bottom-left, bottom-right. + quadPoints[i] = quadPoints[i + 4] = sx; + quadPoints[i + 1] = quadPoints[i + 3] = sy; + quadPoints[i + 2] = quadPoints[i + 6] = sx + width * pageWidth; + quadPoints[i + 5] = quadPoints[i + 7] = sy - height * pageHeight; + i += 8; + } + return quadPoints; + } + toSVGPath() { const buffer = []; for (const polygon of this.#outlines) { @@ -358,10 +465,91 @@ class FreeHighlightOutliner extends FreeDrawOutliner { } } +class FreeHighlightDrawer { + #outliner; + + #thickness; + + constructor(x, y, box, scaleFactor, thickness, isLTR, innerMargin) { + this.#outliner = new FreeHighlightOutliner( + x, + y, + box, + scaleFactor, + thickness, + isLTR, + innerMargin + ); + this.#thickness = thickness; + } + + add(x, y) { + return this.#outliner.add(x, y) + ? { path: { d: this.#outliner.toSVGPath() } } + : null; + } + + addPoints(points) { + let hasChanged = false; + for (let i = 0, ii = points.length; i < ii; i += 2) { + hasChanged = this.#outliner.add(points[i], points[i + 1]) || hasChanged; + } + return hasChanged ? { path: { d: this.#outliner.toSVGPath() } } : null; + } + + end(x, y) { + return x === undefined ? null : this.add(x, y); + } + + isEmpty() { + return this.#outliner.isEmpty(); + } + + isCancellable() { + return this.#outliner.isCancellable(); + } + + removeLastElement() { + return this.#outliner.removeLastElement(); + } + + updateProperty(_name, _value) { + // Drawing options update the SVG, but this stroke's geometry stays fixed. + return null; + } + + getOutlines() { + const outlines = this.#outliner.getOutlines(); + outlines.buildFocusOutline(2 * this.#thickness); + + return outlines; + } + + get defaultSVGProperties() { + return { + bbox: [0, 0, 1, 1], + root: { + viewBox: "0 0 1 1", + }, + rootClass: { + highlight: true, + free: true, + }, + path: { + d: this.#outliner.toSVGPath(), + }, + }; + } +} + class FreeHighlightOutline extends FreeDrawOutline { - newOutliner(point, box, scaleFactor, thickness, isLTR, innerMargin = 0) { + // Extra radius around the highlight. + static #EXTRA_THICKNESS = 1.5; + + newOutliner(x, y, box, scaleFactor, thickness, isLTR, innerMargin = 0) { return new FreeHighlightOutliner( - point, + x, + y, box, scaleFactor, thickness, @@ -369,6 +557,61 @@ class FreeHighlightOutline extends FreeDrawOutline { innerMargin ); } + + get isFree() { + return true; + } + + /** @param {number} thickness */ + buildFocusOutline(thickness) { + this.focusOutline = this.getNewOutline( + thickness / 2 + FreeHighlightOutline.#EXTRA_THICKNESS, + /* innerMargin = */ 0.0025 + ); + } + + /** @inheritdoc */ + get defaultSVGProperties() { + return getHighlightSVGProperties(this); + } + + /** @inheritdoc */ + getFocusSVGProperties(rotation) { + return getHighlightFocusSVGProperties(this, rotation); + } + + /** @inheritdoc */ + get focusMustRemoveSelfIntersections() { + // Mask out the part of the stroke inside the shape. + return true; + } + + /** @inheritdoc */ + updateRotation(rotation) { + return { root: { "data-main-rotation": rotation } }; + } + + /** @inheritdoc */ + updateProperty(name, value) { + if (name !== "thickness") { + return null; + } + const bbox = this.updateThickness(value / 2); + this.buildFocusOutline(value); + + return bbox; + } + + /** @inheritdoc */ + getPathResizedSVGProperties() { + // Thickness changes rebuild the path. + return { path: { d: this.toSVGPath() } }; + } } -export { FreeHighlightOutliner, HighlightOutliner }; +export { + FreeHighlightDrawer, + FreeHighlightOutliner, + HighlightOutline, + HighlightOutliner, +}; diff --git a/src/display/editor/drawers/outline.js b/src/display/editor/drawers/outline.js index e4d41f7f831e0..5a2d3f80597c4 100644 --- a/src/display/editor/drawers/outline.js +++ b/src/display/editor/drawers/outline.js @@ -18,6 +18,9 @@ import { unreachable } from "../../../shared/util.js"; class Outline { static PRECISION = 1e-4; + /** @type {Outline|null} Optional hover/selection outline drawn separately. */ + focusOutline = null; + /** * @returns {string} The SVG path of the outline. */ @@ -37,6 +40,111 @@ class Outline { unreachable("Abstract method `serialize` must be implemented."); } + /** @type {Object} */ + // eslint-disable-next-line getter-return + get defaultSVGProperties() { + unreachable("Abstract getter `defaultSVGProperties` must be implemented."); + } + + /** @type {Object} SVG properties used to finalize a drawing session. */ + get defaultProperties() { + return this.defaultSVGProperties; + } + + /** + * @param {number} _rotation - the rotation to apply to the outline. + * @returns {Object|null} + */ + getFocusSVGProperties(_rotation) { + return null; + } + + /** @type {boolean} Whether `DrawLayer.drawOutline` applies its mask. */ + get focusMustRemoveSelfIntersections() { + return false; + } + + /** + * @param {string} _name + * @param {*} _value + * @returns {Array|Float32Array|null} The new bounding box, if any. + */ + updateProperty(_name, _value) { + return null; + } + + /** + * @param {Array} _dimensions + * @param {number} _scale + * @returns {Array|Float32Array|null} The new bounding box, if any. + */ + updateParentDimensions(_dimensions, _scale) { + return null; + } + + /** + * @param {Array} _pageTranslation + * @param {Array} _pageDimensions + * @returns {Float32Array|null} + */ + serializeQuadPoints(_pageTranslation, _pageDimensions) { + return null; + } + + /** + * @param {number} _rotation + * @returns {Object} the SVG properties to apply to the rotated shape. + */ + updateRotation(_rotation) { + return {}; + } + + /** + * Called on each resizing step, hence the outline itself is unchanged. + * @param {Array} _bbox - the bounding box being resized to. + * @returns {Object} the SVG properties to apply to the resizing shape. + */ + getPathResizingSVGProperties(_bbox) { + return {}; + } + + /** + * Called once the resizing is done, hence the outline can be updated. + * @param {Array} _bbox - the new bounding box. + * @returns {Object} the SVG properties to apply to the resized shape. + */ + getPathResizedSVGProperties(_bbox) { + return {}; + } + + /** + * Called once the translation is done, hence the outline can be updated. + * @param {Array} _bbox - the new bounding box. + * @param {Array} _parentDimensions + * @returns {Object} the SVG properties to apply to the translated shape. + */ + getPathTranslatedSVGProperties(_bbox, _parentDimensions) { + return {}; + } + + /** + * Rotate a bounding box which lives in the unit square. + * @param {Array} bbox + * @param {number} angle + * @returns {Array} + */ + static _rotateBox([x, y, width, height], angle) { + switch (angle) { + case 90: + return [1 - y - height, x, height, width]; + case 180: + return [1 - x - width, 1 - y - height, width, height]; + case 270: + return [y, 1 - x - width, height, width]; + } + return [x, y, width, height]; + } + static _rescale(src, tx, ty, sx, sy, dest) { dest ||= new Float32Array(src.length); for (let i = 0, ii = src.length; i < ii; i += 2) { diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 17fe64a41fee6..048c869c0f4b5 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1977,6 +1977,9 @@ class AnnotationEditor { } } + /** + * @returns {Array|null} + */ get toolbarPosition() { return null; } diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index fab06c83f7367..e8bf0b6aa7ebd 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -19,75 +19,70 @@ import { shadow, Util, } from "../../shared/util.js"; -import { bindEvents, KeyboardManager } from "./tools.js"; +import { DrawingEditor, DrawingOptions } from "./draw.js"; import { + FreeHighlightDrawer, FreeHighlightOutliner, - HighlightOutliner, + HighlightOutline, } from "./drawers/highlight.js"; import { HighlightAnnotationElement, InkAnnotationElement, } from "../annotation_layer.js"; -import { noContextMenu, stopEvent } from "../display_utils.js"; import { AnnotationEditor } from "./editor.js"; import { ColorPicker } from "./color_picker.js"; +import { KeyboardManager } from "./tools.js"; +import { stopEvent } from "../display_utils.js"; + +class HighlightDrawingOptions extends DrawingOptions { + constructor(properties = null) { + super(); + super.updateProperties(properties); + } + + /** @inheritdoc */ + updateSVGProperty(name, value) { + if (name !== "thickness") { + // Thickness changes free-highlight geometry, not SVG attributes. + super.updateSVGProperty(name, value); + } + } + + /** @inheritdoc */ + clone() { + const clone = new HighlightDrawingOptions(); + clone.updateAll(this); + return clone; + } +} /** - * Basic draw editor in order to generate an Highlight annotation. + * Editor for text-selection and freehand highlights. + * Their geometry comes from separate outline implementations. */ -class HighlightEditor extends AnnotationEditor { +class HighlightEditor extends DrawingEditor { #anchorNode = null; #anchorOffset = 0; - #boxes; - - #clipPathId = null; - - #colorPicker = null; - - #focusOutlines = null; - #focusNode = null; #focusOffset = 0; - #highlightDiv = null; - - #highlightOutlines = null; - - #id = null; - - #isFreeHighlight = false; - - #firstPoint = null; - - #lastPoint = null; - - #outlineId = null; + #methodOfCreation = ""; #text = ""; - #thickness; - - #methodOfCreation = ""; - - static _defaultColor = null; + static _DEFAULT_OPACITY = 1; - static _defaultOpacity = 1; + static _DEFAULT_THICKNESS = 12; - static _defaultThickness = 12; + static _defaultDrawingOptions = null; static _type = "highlight"; static _editorType = AnnotationEditorType.HIGHLIGHT; - static _freeHighlightId = -1; - - static _freeHighlight = null; - - static _freeHighlightClipId = ""; - static get _keyboardManager() { const proto = HighlightEditor.prototype; return shadow( @@ -104,41 +99,132 @@ class HighlightEditor extends AnnotationEditor { constructor(params) { super({ ...params, name: "highlightEditor" }); - this.color = params.color || HighlightEditor._defaultColor; - this.#thickness = params.thickness || HighlightEditor._defaultThickness; - this.opacity = params.opacity || HighlightEditor._defaultOpacity; - this.#boxes = params.boxes || null; - this.#methodOfCreation = params.methodOfCreation || ""; + this.#anchorNode = params.anchorNode || null; + this.#anchorOffset = params.anchorOffset || 0; + this.#focusNode = params.focusNode || null; + this.#focusOffset = params.focusOffset || 0; + this.#methodOfCreation = + params.methodOfCreation || + (this._drawOutlines?.isFree ? "main_toolbar" : ""); this.#text = params.text || ""; this._isDraggable = false; this.defaultL10nId = "pdfjs-editor-highlight-editor"; + this.rotate(); + } - if (params.highlightId > -1) { - this.#isFreeHighlight = true; - this.#createFreeOutlines(params); - this.#addToDrawLayer(); - } else if (this.#boxes) { - this.#anchorNode = params.anchorNode; - this.#anchorOffset = params.anchorOffset; - this.#focusNode = params.focusNode; - this.#focusOffset = params.focusOffset; - this.#createOutlines(); - this.#addToDrawLayer(); - this.rotate(this.rotation); - } + /** @inheritdoc */ + static initialize(l10n, uiManager) { + AnnotationEditor.initialize(l10n, uiManager); + // Preserve user-selected defaults across initialize calls. + this._defaultDrawingOptions ||= new HighlightDrawingOptions({ + fill: uiManager.highlightColors?.values().next().value || "#fff066", + "fill-opacity": HighlightEditor._DEFAULT_OPACITY, + thickness: HighlightEditor._DEFAULT_THICKNESS, + }); + } - if (!this.annotationElementId) { - this._uiManager.a11yAlert(AnnotationEditor._l10nAlert.highlight); + /** @inheritdoc */ + static getDefaultDrawingOptions(options) { + const clone = this._defaultDrawingOptions.clone(); + clone.updateProperties(options); + return clone; + } + + /** @inheritdoc */ + static get typesMap() { + return shadow( + this, + "typesMap", + new Map([ + [AnnotationEditorParamsType.HIGHLIGHT_COLOR, "fill"], + [AnnotationEditorParamsType.HIGHLIGHT_THICKNESS, "thickness"], + ]) + ); + } + + /** @inheritdoc */ + static get isDrawer() { + // Free highlights start on the text layer. + return false; + } + + /** @inheritdoc */ + static get _hasClipPath() { + // Clip the interactive div to the highlight shape. + return true; + } + + /** @inheritdoc */ + static get _hasDrawClass() { + return false; + } + + /** @inheritdoc */ + _addOutlines(params) { + const { boxes, drawOutlines } = params; + if (!boxes && !drawOutlines) { + return; } + this._drawingOptions ||= + params.drawingOptions || HighlightEditor.getDefaultDrawingOptions(); + if (boxes) { + params = { + ...params, + drawOutlines: HighlightOutline.build( + boxes, + this._uiManager.direction === "ltr" + ), + }; + } + super._addOutlines(params); + } + + get colorType() { + return AnnotationEditorParamsType.HIGHLIGHT_COLOR; + } + + get color() { + return this._drawingOptions.fill; + } + + get opacity() { + return this._drawingOptions["fill-opacity"]; + } + + /** @inheritdoc */ + get _opacityName() { + // Preserve imported opacity, which the UI doesn't expose. + return "fill-opacity"; + } + + /** @inheritdoc */ + get _drawRotation() { + // Text uses page coordinates; freehand uses editor rotation. + return this._drawOutlines?.isFree ? this.rotation : 0; + } + + /** @inheritdoc */ + get isResizable() { + return false; + } + + /** @inheritdoc */ + get _mustBeDisabledOnCommit() { + return false; + } + + /** @inheritdoc */ + get _mustFixPosition() { + return !this._drawOutlines?.isFree; } /** @inheritdoc */ get telemetryInitialData() { return { action: "added", - type: this.#isFreeHighlight ? "free_highlight" : "highlight", + type: this._drawOutlines.isFree ? "free_highlight" : "highlight", color: this._uiManager.getNonHCMColorName(this.color), - thickness: this.#thickness, + thickness: this._drawingOptions.thickness, methodOfCreation: this.#methodOfCreation, }; } @@ -156,310 +242,81 @@ class HighlightEditor extends AnnotationEditor { return { numberOfColors: data.get("color").size }; } - #createOutlines() { - const outliner = new HighlightOutliner( - this.#boxes, - /* borderWidth = */ 0.001 - ); - this.#highlightOutlines = outliner.getOutlines(); - [this.x, this.y, this.width, this.height] = this.#highlightOutlines.box; - - const outlinerForOutline = new HighlightOutliner( - this.#boxes, - /* borderWidth = */ 0.0025, - /* innerMargin = */ 0.001, - this._uiManager.direction === "ltr" - ); - this.#focusOutlines = outlinerForOutline.getOutlines(); - - const { firstPoint } = this.#highlightOutlines; - this.#firstPoint = [ - (firstPoint[0] - this.x) / this.width, - (firstPoint[1] - this.y) / this.height, - ]; - // The last point is in the pages coordinate system. - const { lastPoint } = this.#focusOutlines; - this.#lastPoint = [ - (lastPoint[0] - this.x) / this.width, - (lastPoint[1] - this.y) / this.height, - ]; - } - - #createFreeOutlines({ highlightOutlines, highlightId, clipPathId }) { - this.#highlightOutlines = highlightOutlines; - const extraThickness = 1.5; - this.#focusOutlines = highlightOutlines.getNewOutline( - /* Slightly bigger than the highlight in order to have a little - space between the highlight and the outline. */ - this.#thickness / 2 + extraThickness, - /* innerMargin = */ 0.0025 - ); - - if (highlightId >= 0) { - this.#id = highlightId; - this.#clipPathId = clipPathId; - // We need to redraw the highlight because we change the coordinates to be - // in the box coordinate system. - this.parent.drawLayer.finalizeDraw(highlightId, { - bbox: highlightOutlines.box, - path: { - d: highlightOutlines.toSVGPath(), - }, - }); - this.#outlineId = this.parent.drawLayer.drawOutline( - { - rootClass: { - highlightOutline: true, - free: true, - }, - bbox: this.#focusOutlines.box, - path: { - d: this.#focusOutlines.toSVGPath(), - }, - }, - /* mustRemoveSelfIntersections = */ true - ); - } else if (this.parent) { - const angle = this.parent.viewport.rotation; - this.parent.drawLayer.updateProperties(this.#id, { - bbox: HighlightEditor.#rotateBbox( - this.#highlightOutlines.box, - (angle - this.rotation + 360) % 360 - ), - path: { - d: highlightOutlines.toSVGPath(), - }, - }); - this.parent.drawLayer.updateProperties(this.#outlineId, { - bbox: HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle), - path: { - d: this.#focusOutlines.toSVGPath(), - }, - }); - } - const [x, y, width, height] = highlightOutlines.box; - switch (this.rotation) { - case 0: - this.x = x; - this.y = y; - this.width = width; - this.height = height; - break; - case 90: { - const [pageWidth, pageHeight] = this.parentDimensions; - this.x = y; - this.y = 1 - x; - this.width = (width * pageHeight) / pageWidth; - this.height = (height * pageWidth) / pageHeight; - break; - } - case 180: - this.x = 1 - x; - this.y = 1 - y; - this.width = width; - this.height = height; - break; - case 270: { - const [pageWidth, pageHeight] = this.parentDimensions; - this.x = 1 - y; - this.y = x; - this.width = (width * pageHeight) / pageWidth; - this.height = (height * pageWidth) / pageHeight; - break; - } - } - - const { firstPoint } = highlightOutlines; - this.#firstPoint = [ - (firstPoint[0] - x) / width, - (firstPoint[1] - y) / height, - ]; - const { lastPoint } = this.#focusOutlines; - this.#lastPoint = [(lastPoint[0] - x) / width, (lastPoint[1] - y) / height]; - } - - /** @inheritdoc */ - static initialize(l10n, uiManager) { - AnnotationEditor.initialize(l10n, uiManager); - HighlightEditor._defaultColor ||= - uiManager.highlightColors?.values().next().value || "#fff066"; - } - - /** @inheritdoc */ - static updateDefaultParams(type, value) { - switch (type) { - case AnnotationEditorParamsType.HIGHLIGHT_COLOR: - HighlightEditor._defaultColor = value; - break; - case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS: - HighlightEditor._defaultThickness = value; - break; - } - } - /** @inheritdoc */ translateInPage(x, y) {} /** @inheritdoc */ get toolbarPosition() { - return this.#lastPoint; + return this.#relativeToBox(this._drawOutlines.focusOutline.lastPoint); } /** @inheritdoc */ get commentButtonPosition() { - return this.#firstPoint; + return this.#relativeToBox(this._drawOutlines.firstPoint); + } + + #relativeToBox([pointX, pointY]) { + // The point and box use page coordinates. + const [x, y, width, height] = this._drawOutlines.box; + return [(pointX - x) / width, (pointY - y) / height]; } /** @inheritdoc */ updateParams(type, value) { switch (type) { case AnnotationEditorParamsType.HIGHLIGHT_COLOR: - this.#updateColor(value); + // User-selected colors use the default opacity. + this._updateColorAndOpacity( + value, + HighlightEditor._DEFAULT_OPACITY, + type + ); + this._reportTelemetry( + { + action: "color_changed", + color: this._uiManager.getNonHCMColorName(value), + }, + /* mustWait = */ true + ); break; case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS: - this.#updateThickness(value); + super.updateParams(type, value); + this._reportTelemetry( + { action: "thickness_changed", thickness: value }, + /* mustWait = */ true + ); break; } } - static get defaultPropertiesToUpdate() { - return [ - [ - AnnotationEditorParamsType.HIGHLIGHT_COLOR, - HighlightEditor._defaultColor, - ], - [ - AnnotationEditorParamsType.HIGHLIGHT_THICKNESS, - HighlightEditor._defaultThickness, - ], - ]; - } - /** @inheritdoc */ get propertiesToUpdate() { - return [ - [ - AnnotationEditorParamsType.HIGHLIGHT_COLOR, - this.color || HighlightEditor._defaultColor, - ], - [ - AnnotationEditorParamsType.HIGHLIGHT_THICKNESS, - this.#thickness || HighlightEditor._defaultThickness, - ], - [AnnotationEditorParamsType.HIGHLIGHT_FREE, this.#isFreeHighlight], - ]; - } - - /** @inheritdoc */ - onUpdatedColor() { - this.parent?.drawLayer.updateProperties(this.#id, { - root: { - fill: this.color, - "fill-opacity": this.opacity, - }, - }); - this.#colorPicker?.updateColor(this.color); - super.onUpdatedColor(); - } - - /** - * Update the color and make this action undoable. - * @param {string} color - */ - #updateColor(color) { - const setColorAndOpacity = (col, opa) => { - this.color = col; - this.opacity = opa; - this.onUpdatedColor(); - }; - const savedColor = this.color; - const savedOpacity = this.opacity; - this.addCommands({ - cmd: setColorAndOpacity.bind( - this, - color, - HighlightEditor._defaultOpacity - ), - undo: setColorAndOpacity.bind(this, savedColor, savedOpacity), - post: this._uiManager.updateUI.bind(this._uiManager, this), - mustExec: true, - type: AnnotationEditorParamsType.HIGHLIGHT_COLOR, - overwriteIfSameType: true, - keepUndo: true, - }); - - this._reportTelemetry( - { - action: "color_changed", - color: this._uiManager.getNonHCMColorName(color), - }, - /* mustWait = */ true - ); - } - - /** - * Update the thickness and make this action undoable. - * @param {number} thickness - */ - #updateThickness(thickness) { - const savedThickness = this.#thickness; - const setThickness = th => { - this.#thickness = th; - this.#changeThickness(th); - }; - this.addCommands({ - cmd: setThickness.bind(this, thickness), - undo: setThickness.bind(this, savedThickness), - post: this._uiManager.updateUI.bind(this._uiManager, this), - mustExec: true, - type: AnnotationEditorParamsType.INK_THICKNESS, - overwriteIfSameType: true, - keepUndo: true, - }); - this._reportTelemetry( - { action: "thickness_changed", thickness }, - /* mustWait = */ true - ); + const properties = super.propertiesToUpdate; + properties.push([ + AnnotationEditorParamsType.HIGHLIGHT_FREE, + this._drawOutlines.isFree, + ]); + return properties; } /** @inheritdoc */ get toolbarButtons() { if (this._uiManager.highlightColors) { - const colorPicker = (this.#colorPicker = new ColorPicker({ - editor: this, - })); - return [["colorPicker", colorPicker]]; + // The toolbar destroys its picker, so rebuild it with the toolbar. + this._colorPicker = new ColorPicker({ editor: this }); + return [["colorPicker", this._colorPicker]]; } return super.toolbarButtons; } - /** @inheritdoc */ - disableEditing() { - super.disableEditing(); - this.div.classList.toggle("disabled", true); - } - - /** @inheritdoc */ - enableEditing() { - super.enableEditing(); - this.div.classList.toggle("disabled", false); - } - /** @inheritdoc */ fixAndSetPosition() { - return super.fixAndSetPosition(this.#getRotation()); - } - - /** @inheritdoc */ - getBaseTranslation() { - // The editor itself doesn't have any CSS border (we're drawing one - // ourselves in using SVG). - return [0, 0]; + return super.fixAndSetPosition(this._drawRotation); } /** @inheritdoc */ getRect(tx, ty) { - return super.getRect(tx, ty, this.#getRotation()); + return super.getRect(tx, ty, this._drawRotation); } /** @inheritdoc */ @@ -474,155 +331,12 @@ class HighlightEditor extends AnnotationEditor { /** @inheritdoc */ remove() { - this.#cleanDrawLayer(); this._reportTelemetry({ action: "deleted", }); super.remove(); } - /** @inheritdoc */ - rebuild() { - if (!this.parent) { - return; - } - super.rebuild(); - if (this.div === null) { - return; - } - - this.#addToDrawLayer(); - - if (!this.isAttachedToDOM) { - // At some point this editor was removed and we're rebuilding it, - // hence we must add it to its parent. - this.parent.add(this); - } - } - - setParent(parent) { - let mustBeSelected = false; - if (this.parent && !parent) { - this.#cleanDrawLayer(); - } else if (parent) { - this.#addToDrawLayer(parent); - // If mustBeSelected is true it means that this editor was selected - // when its parent has been destroyed, hence we must select it again. - mustBeSelected = - !this.parent && this.div?.classList.contains("selectedEditor"); - } - super.setParent(parent); - this.show(this._isVisible); - if (mustBeSelected) { - // We select it after the parent has been set. - this.select(); - } - } - - #changeThickness(thickness) { - if (!this.#isFreeHighlight) { - return; - } - this.#createFreeOutlines({ - highlightOutlines: this.#highlightOutlines.getNewOutline(thickness / 2), - }); - this.fixAndSetPosition(); - this.setDims(); - } - - #cleanDrawLayer() { - if (this.#id === null || !this.parent) { - return; - } - this.parent.drawLayer.remove(this.#id); - this.#id = null; - this.parent.drawLayer.remove(this.#outlineId); - this.#outlineId = null; - } - - #addToDrawLayer(parent = this.parent) { - if (this.#id !== null) { - return; - } - ({ id: this.#id, clipPathId: this.#clipPathId } = parent.drawLayer.draw( - { - bbox: this.#highlightOutlines.box, - root: { - viewBox: "0 0 1 1", - fill: this.color, - "fill-opacity": this.opacity, - }, - rootClass: { - highlight: true, - free: this.#isFreeHighlight, - }, - path: { - d: this.#highlightOutlines.toSVGPath(), - }, - }, - /* isPathUpdatable = */ false, - /* hasClip = */ true - )); - this.#outlineId = parent.drawLayer.drawOutline( - { - rootClass: { - highlightOutline: true, - free: this.#isFreeHighlight, - }, - bbox: this.#focusOutlines.box, - path: { - d: this.#focusOutlines.toSVGPath(), - }, - }, - /* mustRemoveSelfIntersections = */ this.#isFreeHighlight - ); - - if (this.#highlightDiv) { - this.#highlightDiv.style.clipPath = this.#clipPathId; - } - } - - static #rotateBbox([x, y, width, height], angle) { - switch (angle) { - case 90: - return [1 - y - height, x, height, width]; - case 180: - return [1 - x - width, 1 - y - height, width, height]; - case 270: - return [y, 1 - x - width, height, width]; - } - return [x, y, width, height]; - } - - /** @inheritdoc */ - rotate(angle) { - // We need to rotate the svgs because of the coordinates system. - const { drawLayer } = this.parent; - let box; - if (this.#isFreeHighlight) { - angle = (angle - this.rotation + 360) % 360; - box = HighlightEditor.#rotateBbox(this.#highlightOutlines.box, angle); - } else { - // An highlight annotation is always drawn horizontally. - box = HighlightEditor.#rotateBbox( - [this.x, this.y, this.width, this.height], - angle - ); - } - drawLayer.updateProperties(this.#id, { - bbox: box, - root: { - "data-main-rotation": angle, - }, - }); - drawLayer.updateProperties(this.#outlineId, { - bbox: HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle), - root: { - "data-main-rotation": angle, - }, - }); - } - /** @inheritdoc */ render() { if (this.div) { @@ -634,46 +348,18 @@ class HighlightEditor extends AnnotationEditor { div.setAttribute("aria-label", this.#text); div.setAttribute("role", "mark"); } - if (this.#isFreeHighlight) { + if (this._drawOutlines.isFree) { div.classList.add("free"); } else { - this.div.addEventListener("keydown", this.#keydown.bind(this), { + div.addEventListener("keydown", this.#keydown.bind(this), { signal: this._uiManager._signal, }); } - const highlightDiv = (this.#highlightDiv = document.createElement("div")); - div.append(highlightDiv); - highlightDiv.setAttribute("aria-hidden", "true"); - highlightDiv.className = "internal"; - highlightDiv.style.clipPath = this.#clipPathId; - this.setDims(); - - bindEvents(this, this.#highlightDiv, ["pointerover", "pointerleave"]); this.enableEditing(); return div; } - pointerover() { - if (!this.isSelected) { - this.parent?.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - hovered: true, - }, - }); - } - } - - pointerleave() { - if (!this.isSelected) { - this.parent?.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - hovered: false, - }, - }); - } - } - #keydown(event) { HighlightEditor._keyboardManager.exec(this, event); } @@ -704,180 +390,121 @@ class HighlightEditor extends AnnotationEditor { } } - /** @inheritdoc */ - select() { - super.select(); - if (!this.#outlineId) { - return; - } - this.parent?.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - hovered: false, - selected: true, - }, - }); - } - /** @inheritdoc */ unselect() { super.unselect(); - if (!this.#outlineId) { - return; - } - this.parent?.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - selected: false, - }, - }); - if (!this.#isFreeHighlight) { + if (!this._drawOutlines.isFree) { this.#setCaret(/* start = */ false); } } /** @inheritdoc */ - get _mustFixPosition() { - return !this.#isFreeHighlight; + static createDrawerInstance({ x, y, box, parent, isLTR }) { + // The outliner spreads the stroke on both sides of the pointer path, hence + // it takes the half-thickness. The inner margin slightly inflates the + // bounding box, else the shape would be clipped by its own SVG viewport. + return new FreeHighlightDrawer( + x, + y, + box, + parent.scale, + this._defaultDrawingOptions.thickness / 2, + isLTR, + /* innerMargin = */ 0.001 + ); } /** @inheritdoc */ - show(visible = this._isVisible) { - super.show(visible); - if (this.parent) { - this.parent.drawLayer.updateProperties(this.#id, { - rootClass: { - hidden: !visible, - }, - }); - this.parent.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - hidden: !visible, - }, - }); - } + static _getDrawingTarget(parent, { target }) { + // The event target can be a child of the text layer. + return target.closest(".textLayer"); } - #getRotation() { - // Highlight annotations are always drawn horizontally but if - // a free highlight annotation can be rotated. - return this.#isFreeHighlight ? this.rotation : 0; - } - - #serializeBoxes() { - if (this.#isFreeHighlight) { - return null; - } - const [pageWidth, pageHeight] = this.pageDimensions; - const [pageX, pageY] = this.pageTranslation; - const boxes = this.#boxes; - const quadPoints = new Float32Array(boxes.length * 8); - let i = 0; - for (const { x, y, width, height } of boxes) { - const sx = x * pageWidth + pageX; - const sy = (1 - y) * pageHeight + pageY; - // Serializes the rectangle in the Adobe Acrobat format. - // The rectangle's coordinates (b = bottom, t = top, L = left, R = right) - // are ordered as follows: tL, tR, bL, bR (bL origin). - quadPoints[i] = quadPoints[i + 4] = sx; - quadPoints[i + 1] = quadPoints[i + 3] = sy; - quadPoints[i + 2] = quadPoints[i + 6] = sx + width * pageWidth; - quadPoints[i + 5] = quadPoints[i + 7] = sy - height * pageHeight; - i += 8; - } - return quadPoints; - } - - #serializeOutlines(rect) { - return this.#highlightOutlines.serialize(rect, this.#getRotation()); + /** @inheritdoc */ + static _getPointerCoords({ x, y }) { + // Child-relative offsets don't match the text layer's client box. + return [x, y]; } - static startHighlighting(parent, isLTR, { target: textLayer, x, y }) { - const { - x: layerX, - y: layerY, - width: parentWidth, - height: parentHeight, - } = textLayer.getBoundingClientRect(); - - const ac = new AbortController(); - const signal = parent.combinedSignal(ac); - - const pointerUpCallback = e => { - ac.abort(); - this.#endHighlight(parent, e); - }; - window.addEventListener("blur", pointerUpCallback, { signal }); - window.addEventListener("pointerup", pointerUpCallback, { signal }); + /** @inheritdoc */ + static _addDrawingListeners(target, signal) { + // Highlights bypass AnnotationEditorLayer.startDrawingSession. + target.classList.add("free"); + signal.addEventListener("abort", () => target.classList.remove("free"), { + once: true, + }); + window.addEventListener("blur", () => this._endDraw(null), { signal }); window.addEventListener( "pointerdown", - stopEvent /* Avoid to have undesired clicks during the drawing. */, + stopEvent /* Prevent pointerdown from reaching page content. */, { capture: true, passive: false, signal, } ); - window.addEventListener("contextmenu", noContextMenu, { signal }); + } - textLayer.addEventListener( - "pointermove", - this.#highlightMove.bind(this, parent), - { signal } - ); - this._freeHighlight = new FreeHighlightOutliner( - { x, y }, - [layerX, layerY, parentWidth, parentHeight], - parent.scale, - this._defaultThickness / 2, - isLTR, - /* innerMargin = */ 0.001 - ); - ({ id: this._freeHighlightId, clipPathId: this._freeHighlightClipId } = - parent.drawLayer.draw( - { - bbox: [0, 0, 1, 1], - root: { - viewBox: "0 0 1 1", - fill: this._defaultColor, - "fill-opacity": this._defaultOpacity, - }, - rootClass: { - highlight: true, - free: true, - }, - path: { - d: this._freeHighlight.toSVGPath(), - }, - }, - /* isPathUpdatable = */ true, - /* hasClip = */ true - )); + /** @inheritdoc */ + static _endDrawingSession(isAborted = false) { + return this.endDrawing(isAborted); } - static #highlightMove(parent, event) { - if (this._freeHighlight.add(event)) { - // Redraw only if the point has been added. - parent.drawLayer.updateProperties(this._freeHighlightId, { - path: { - d: this._freeHighlight.toSVGPath(), - }, - }); - } + /** @inheritdoc */ + createDrawingOptions({ color, opacity, thickness }) { + const { _defaultDrawingOptions: defaults, _DEFAULT_OPACITY } = + HighlightEditor; + this._drawingOptions = HighlightEditor.getDefaultDrawingOptions({ + fill: Util.makeHexColor(...color), + "fill-opacity": opacity || _DEFAULT_OPACITY, + thickness: thickness || defaults.thickness, + }); } - static #endHighlight(parent, event) { - if (!this._freeHighlight.isEmpty()) { - parent.createAndAddNewEditor(event, false, { - highlightId: this._freeHighlightId, - highlightOutlines: this._freeHighlight.getOutlines(), - clipPathId: this._freeHighlightClipId, - methodOfCreation: "main_toolbar", - }); - } else { - parent.drawLayer.remove(this._freeHighlightId); + /** @inheritdoc */ + static deserializeDraw( + pageX, + pageY, + pageWidth, + pageHeight, + _innerMargin, + data, + uiManager + ) { + const { quadPoints } = data; + if (quadPoints) { + const boxes = []; + for (let i = 0, ii = quadPoints.length; i < ii; i += 8) { + boxes.push({ + x: (quadPoints[i] - pageX) / pageWidth, + y: 1 - (quadPoints[i + 1] - pageY) / pageHeight, + width: (quadPoints[i + 2] - quadPoints[i]) / pageWidth, + height: (quadPoints[i + 1] - quadPoints[i + 5]) / pageHeight, + }); + } + return HighlightOutline.build(boxes, uiManager.direction === "ltr"); + } + + const thickness = data.thickness || this._defaultDrawingOptions.thickness; + const points = (data.inkLists || data.outlines.points)[0]; + // As in `createDrawerInstance`, the outliner takes the half-thickness and a + // non-null inner margin. + const outliner = new FreeHighlightOutliner( + points[0] - pageX, + pageHeight - (points[1] - pageY), + [0, 0, pageWidth, pageHeight], + 1, + thickness / 2, + true, + /* innerMargin = */ 0.001 + ); + for (let i = 0, ii = points.length; i < ii; i += 2) { + outliner.add(points[i] - pageX, pageHeight - (points[i + 1] - pageY)); } - this._freeHighlightId = -1; - this._freeHighlight = null; - this._freeHighlightClipId = ""; + const outlines = outliner.getOutlines(); + outlines.buildFocusOutline(thickness); + + return outlines; } /** @inheritdoc */ @@ -907,7 +534,6 @@ class HighlightEditor extends AnnotationEditor { color: Array.from(color), opacity, quadPoints, - boxes: null, pageIndex: pageNumber - 1, rect: rect.slice(0), rotation, @@ -944,7 +570,6 @@ class HighlightEditor extends AnnotationEditor { color: Array.from(color), thickness, inkLists, - boxes: null, pageIndex: pageNumber - 1, rect: rect.slice(0), rotation, @@ -959,83 +584,12 @@ class HighlightEditor extends AnnotationEditor { }; } - const { color, quadPoints, inkLists, outlines, opacity } = data; const editor = await super.deserialize(data, parent, uiManager); - - editor.color = Util.makeHexColor(...color); - editor.opacity = opacity || 1; - if (inkLists) { - editor.#thickness = data.thickness; - } editor._initialData = initialData; if (data.comment) { editor.setCommentData(data); } - const [pageWidth, pageHeight] = editor.pageDimensions; - const [pageX, pageY] = editor.pageTranslation; - - if (quadPoints) { - const boxes = (editor.#boxes = []); - for (let i = 0; i < quadPoints.length; i += 8) { - boxes.push({ - x: (quadPoints[i] - pageX) / pageWidth, - y: 1 - (quadPoints[i + 1] - pageY) / pageHeight, - width: (quadPoints[i + 2] - quadPoints[i]) / pageWidth, - height: (quadPoints[i + 1] - quadPoints[i + 5]) / pageHeight, - }); - } - editor.#createOutlines(); - editor.#addToDrawLayer(); - editor.rotate(editor.rotation); - } else if (inkLists || outlines) { - editor.#isFreeHighlight = true; - const points = (inkLists || outlines.points)[0]; - const point = { - x: points[0] - pageX, - y: pageHeight - (points[1] - pageY), - }; - const outliner = new FreeHighlightOutliner( - point, - [0, 0, pageWidth, pageHeight], - 1, - editor.#thickness / 2, - true, - 0.001 - ); - for (let i = 0, ii = points.length; i < ii; i += 2) { - point.x = points[i] - pageX; - point.y = pageHeight - (points[i + 1] - pageY); - outliner.add(point); - } - const { id, clipPathId } = parent.drawLayer.draw( - { - bbox: [0, 0, 1, 1], - root: { - viewBox: "0 0 1 1", - fill: editor.color, - "fill-opacity": editor._defaultOpacity, - }, - rootClass: { - highlight: true, - free: true, - }, - path: { - d: outliner.toSVGPath(), - }, - }, - /* isPathUpdatable = */ true, - /* hasClip = */ true - ); - editor.#createFreeOutlines({ - highlightOutlines: outliner.getOutlines(), - highlightId: id, - clipPathId, - }); - editor.#addToDrawLayer(); - editor.rotate(editor.parentRotation); - } - return editor; } @@ -1050,16 +604,21 @@ class HighlightEditor extends AnnotationEditor { return this.serializeDeleted(); } - const color = AnnotationEditor._colorManager.convert( - this._uiManager.getNonHCMColor(this.color) - ); const serialized = super.serialize(isForCopying); Object.assign(serialized, { - color, + color: AnnotationEditor._colorManager.convert( + this._uiManager.getNonHCMColor(this.color) + ), opacity: this.opacity, - thickness: this.#thickness, - quadPoints: this.#serializeBoxes(), - outlines: this.#serializeOutlines(serialized.rect), + thickness: this._drawingOptions.thickness, + quadPoints: this._drawOutlines.serializeQuadPoints( + this.pageTranslation, + this.pageDimensions + ), + outlines: this._drawOutlines.serialize( + serialized.rect, + this._drawRotation + ), }); this.addComment(serialized); @@ -1091,10 +650,6 @@ class HighlightEditor extends AnnotationEditor { return null; } - - static canCreateNewEmptyEditor() { - return false; - } } export { HighlightEditor }; diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index f42ca1af01589..26b015ccb9a69 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -106,12 +106,12 @@ class InkEditor extends DrawingEditor { } /** @inheritdoc */ - static createDrawerInstance(x, y, parentWidth, parentHeight, rotation) { + static createDrawerInstance({ x, y, box: [, , width, height], rotation }) { return new InkDrawOutliner( x, y, - parentWidth, - parentHeight, + width, + height, rotation, this._defaultDrawingOptions["stroke-width"] ); diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index 44efe0743a32d..a729cee12306f 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -613,6 +613,47 @@ describe("Highlight Editor", () => { }); }); + describe("Free highlight drawing state", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("empty.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must be cleared when the pointer is released outside the text layer", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getRect(page, ".textLayer"); + const x = rect.x + 40; + const y = rect.y + 40; + const clickHandle = await waitForPointerUp(page); + + await page.mouse.move(x, y); + await page.mouse.down(); + await page.waitForSelector(".textLayer.highlighting.free"); + await page.mouse.move(x + 40, y + 40); + await page.mouse.move(rect.x - 10, y); + await page.mouse.up(); + await awaitPromise(clickHandle); + + await page.waitForSelector(".textLayer.highlighting:not(.free)", { + visible: true, + }); + const isFree = await page.$eval(".textLayer", element => + element.classList.contains("free") + ); + expect(isFree).withContext(`In ${browserName}`).toEqual(false); + }) + ); + }); + }); + describe("Highlight with the keyboard", () => { let pages; diff --git a/test/pdfs/issue9186.pdf.link b/test/pdfs/issue9186.pdf.link index e8747599fb5c2..8b72be135f46f 100644 --- a/test/pdfs/issue9186.pdf.link +++ b/test/pdfs/issue9186.pdf.link @@ -1 +1 @@ -https://github.com/mozilla/pdf.js/files/1500985/Sample.lease.contract.26.pdf +https://github.com/mozilla/pdf.js/files/1500985/Sample.lease.contract.26.pdf diff --git a/web/annotation_editor_layer_builder.js b/web/annotation_editor_layer_builder.js index e02a570c32bbd..63fd6a1282710 100644 --- a/web/annotation_editor_layer_builder.js +++ b/web/annotation_editor_layer_builder.js @@ -137,6 +137,15 @@ class AnnotationEditorLayerBuilder { this.show(); } + /** @param {PageViewport} viewport */ + update(viewport) { + if (this.div) { + this.annotationEditorLayer.update({ + viewport: viewport.clone({ dontFlip: true }), + }); + } + } + cancel() { this._cancelled = true; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 6691ae349fe92..22f41dbbc62e3 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -860,6 +860,9 @@ class PDFPageView extends BasePDFPageView { } } this.cssTransform({}); + // Drawings stay visible in the unrotated canvas wrapper while the other + // kept layers are hidden, so reposition them before reset(). + this.annotationEditorLayer?.update(this.viewport); this.reset({ keepAnnotationLayer: true, keepAnnotationEditorLayer: true, diff --git a/web/toolbar.js b/web/toolbar.js index 66b453d2854c3..91e3d6d818c0d 100644 --- a/web/toolbar.js +++ b/web/toolbar.js @@ -309,7 +309,7 @@ class Toolbar { eventBus.on( "mainhighlightcolorpickerupdatecolor", ({ value }) => { - this.#colorPicker?.updateColor(value); + this.#colorPicker?.update(value); }, internalOpt );