diff --git a/pdf.js b/pdf.js index fd88184..bcadc11 100644 --- a/pdf.js +++ b/pdf.js @@ -114,6 +114,385 @@ const textLayerBuilderCSS = ` } ` +//https://github.com/mozilla/pdf.js/blob/d64f223d034ad74fb62571c3acff566d25eca413/web/annotation_layer_builder.css +const annotationLayerBuilderCSS = ` +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:root { + --annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,"); + --input-focus-border-color: Highlight; + --input-focus-outline: 1px solid Canvas; + --input-unfocused-border-color: transparent; + --input-disabled-border-color: transparent; + --input-hover-border-color: black; + --link-outline: none; +} + +@media screen and (forced-colors: active) { + :root { + --input-focus-border-color: CanvasText; + --input-unfocused-border-color: ActiveText; + --input-disabled-border-color: GrayText; + --input-hover-border-color: Highlight; + --link-outline: 1.5px solid LinkText; + --hcm-highligh-filter: invert(100%); + } + .annotationLayer .textWidgetAnnotation :is(input, textarea):required, + .annotationLayer .choiceWidgetAnnotation select:required, + .annotationLayer + .buttonWidgetAnnotation:is(.checkBox, .radioButton) + input:required { + outline: 1.5px solid selectedItem; + } + + .annotationLayer .linkAnnotation:hover { + backdrop-filter: var(--hcm-highligh-filter); + } + + .annotationLayer .linkAnnotation > a:hover { + opacity: 0 !important; + background: none !important; + box-shadow: none; + } + + .annotationLayer .popupAnnotation .popup { + outline: calc(1.5px * var(--scale-factor)) solid CanvasText !important; + background-color: ButtonFace !important; + color: ButtonText !important; + } + + .annotationLayer .highlightArea:hover::after { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + backdrop-filter: var(--hcm-highligh-filter); + content: ""; + pointer-events: none; + } + + .annotationLayer .popupAnnotation.focused .popup { + outline: calc(3px * var(--scale-factor)) solid Highlight !important; + } +} + +.annotationLayer { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + transform-origin: 0 0; + z-index: 3; +} + +.annotationLayer[data-main-rotation="90"] .norotate { + transform: rotate(270deg) translateX(-100%); +} +.annotationLayer[data-main-rotation="180"] .norotate { + transform: rotate(180deg) translate(-100%, -100%); +} +.annotationLayer[data-main-rotation="270"] .norotate { + transform: rotate(90deg) translateY(-100%); +} + +.annotationLayer canvas { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; +} + +.annotationLayer section { + position: absolute; + text-align: initial; + pointer-events: auto; + box-sizing: border-box; + transform-origin: 0 0; +} + +.annotationLayer .linkAnnotation { + outline: var(--link-outline); +} + +.annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a { + position: absolute; + font-size: 1em; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.annotationLayer + :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder) + > a:hover { + opacity: 0.2; + background-color: rgb(255 255 0); + box-shadow: 0 2px 10px rgb(255 255 0); +} + +.annotationLayer .linkAnnotation.hasBorder:hover { + background-color: rgb(255 255 0 / 0.2); +} + +.annotationLayer .hasBorder { + background-size: 100% 100%; +} + +.annotationLayer .textAnnotation img { + position: absolute; + cursor: pointer; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea), +.annotationLayer .choiceWidgetAnnotation select, +.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input { + background-image: var(--annotation-unfocused-field-background); + border: 2px solid var(--input-unfocused-border-color); + box-sizing: border-box; + font: calc(9px * var(--scale-factor)) sans-serif; + height: 100%; + margin: 0; + vertical-align: top; + width: 100%; +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea):required, +.annotationLayer .choiceWidgetAnnotation select:required, +.annotationLayer + .buttonWidgetAnnotation:is(.checkBox, .radioButton) + input:required { + outline: 1.5px solid red; +} + +.annotationLayer .choiceWidgetAnnotation select option { + padding: 0; +} + +.annotationLayer .buttonWidgetAnnotation.radioButton input { + border-radius: 50%; +} + +.annotationLayer .textWidgetAnnotation textarea { + resize: none; +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea)[disabled], +.annotationLayer .choiceWidgetAnnotation select[disabled], +.annotationLayer + .buttonWidgetAnnotation:is(.checkBox, .radioButton) + input[disabled] { + background: none; + border: 2px solid var(--input-disabled-border-color); + cursor: not-allowed; +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea):hover, +.annotationLayer .choiceWidgetAnnotation select:hover, +.annotationLayer + .buttonWidgetAnnotation:is(.checkBox, .radioButton) + input:hover { + border: 2px solid var(--input-hover-border-color); +} +.annotationLayer .textWidgetAnnotation :is(input, textarea):hover, +.annotationLayer .choiceWidgetAnnotation select:hover, +.annotationLayer .buttonWidgetAnnotation.checkBox input:hover { + border-radius: 2px; +} + +.annotationLayer .textWidgetAnnotation :is(input, textarea):focus, +.annotationLayer .choiceWidgetAnnotation select:focus { + background: none; + border: 2px solid var(--input-focus-border-color); + border-radius: 2px; + outline: var(--input-focus-outline); +} + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) :focus { + background-image: none; + background-color: transparent; +} + +.annotationLayer .buttonWidgetAnnotation.checkBox :focus { + border: 2px solid var(--input-focus-border-color); + border-radius: 2px; + outline: var(--input-focus-outline); +} + +.annotationLayer .buttonWidgetAnnotation.radioButton :focus { + border: 2px solid var(--input-focus-border-color); + outline: var(--input-focus-outline); +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before, +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after, +.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before { + background-color: CanvasText; + content: ""; + display: block; + position: absolute; +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before, +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after { + height: 80%; + left: 45%; + width: 1px; +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before { + transform: rotate(45deg); +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after { + transform: rotate(-45deg); +} + +.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before { + border-radius: 50%; + height: 50%; + left: 30%; + top: 20%; + width: 50%; +} + +.annotationLayer .textWidgetAnnotation input.comb { + font-family: monospace; + padding-left: 2px; + padding-right: 0; +} + +.annotationLayer .textWidgetAnnotation input.comb:focus { + /* + * Letter spacing is placed on the right side of each character. Hence, the + * letter spacing of the last character may be placed outside the visible + * area, causing horizontal scrolling. We avoid this by extending the width + * when the element has focus and revert this when it loses focus. + */ + width: 103%; +} + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input { + appearance: none; +} + +.annotationLayer .fileAttachmentAnnotation .popupTriggerArea { + height: 100%; + width: 100%; +} + +.annotationLayer .popupAnnotation { + position: absolute; + font-size: calc(9px * var(--scale-factor)); + pointer-events: none; + width: max-content; + max-width: 45%; + height: auto; +} + +.annotationLayer .popup { + background-color: rgb(255 255 153); + box-shadow: 0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor)) + rgb(136 136 136); + border-radius: calc(2px * var(--scale-factor)); + outline: 1.5px solid rgb(255 255 74); + padding: calc(6px * var(--scale-factor)); + cursor: pointer; + font: message-box; + white-space: normal; + word-wrap: break-word; + pointer-events: auto; +} + +.annotationLayer .popupAnnotation.focused .popup { + outline-width: 3px; +} + +.annotationLayer .popup * { + font-size: calc(9px * var(--scale-factor)); +} + +.annotationLayer .popup > .header { + display: inline-block; +} + +.annotationLayer .popup > .header h1 { + display: inline; +} + +.annotationLayer .popup > .header .popupDate { + display: inline-block; + margin-left: calc(5px * var(--scale-factor)); + width: fit-content; +} + +.annotationLayer .popupContent { + border-top: 1px solid rgb(51 51 51); + margin-top: calc(2px * var(--scale-factor)); + padding-top: calc(2px * var(--scale-factor)); +} + +.annotationLayer .richText > * { + white-space: pre-wrap; + font-size: calc(9px * var(--scale-factor)); +} + +.annotationLayer .popupTriggerArea { + cursor: pointer; +} + +.annotationLayer section svg { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +.annotationLayer .annotationTextContent { + position: absolute; + width: 100%; + height: 100%; + opacity: 0; + color: transparent; + user-select: none; + pointer-events: none; +} + +.annotationLayer .annotationTextContent span { + width: 100%; + display: inline-block; +} + +.annotationLayer svg.quadrilateralsContainer { + contain: strict; + width: 0; + height: 0; + position: absolute; + top: 0; + left: 0; + z-index: -1; +} +` + const renderPage = async (page, getImageBlob) => { const scale = devicePixelRatio const viewport = page.getViewport({ scale }) @@ -142,6 +521,16 @@ const renderPage = async (page, getImageBlob) => { container, viewport, }).promise + const div = document.createElement('div') + div.classList.add('annotationLayer') + await new pdfjsLib.AnnotationLayer({ page, viewport, div }).render({ + annotations: await page.getAnnotations(), + linkService: { + getDestinationHash: dest => JSON.stringify(dest), + addLinkAttributes: (link, url) => link.href = url, + }, + }) + const src = URL.createObjectURL(blob) const url = URL.createObjectURL(new Blob([` @@ -155,9 +544,11 @@ const renderPage = async (page, getImageBlob) => { padding: 0; } ${textLayerBuilderCSS} + ${annotationLayerBuilderCSS} ${container.outerHTML} + ${div.outerHTML} `], { type: 'text/html' })) return url } @@ -195,6 +586,7 @@ export const makePDF = async file => { }, size: 1000, })) + book.isExternal = uri => /^\w+:/i.test(uri) book.resolveHref = async href => { const parsed = JSON.parse(href) const dest = typeof parsed === 'string'