From 33ea817b20e01f5dc5fb15ca751c2c119f7a9dbe Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sat, 6 Nov 2021 18:36:49 +0100 Subject: [PATCH] [api-minor] Render pushbuttons on their own canvas (bug 1737260) - First step to fix https://bugzilla.mozilla.org/show_bug.cgi?id=1737260; - several interactive pdfs use the possibility to hide/show buttons to show different icons; - render pushbuttons on their own canvas and then insert it the annotation_layer; - update test/driver.js in order to convert canvases for pushbuttons into images. --- src/core/annotation.js | 51 ++++++++++----- src/core/document.js | 1 + src/display/annotation_layer.js | 106 +++++++++++++++++++++++++++---- src/display/api.js | 9 ++- src/display/canvas.js | 73 ++++++++++++++++++--- test/driver.js | 62 +++++++++++++----- test/unit/annotation_spec.js | 19 ++++++ web/annotation_layer_builder.css | 7 ++ web/annotation_layer_builder.js | 10 ++- web/base_viewer.js | 10 ++- web/interfaces.js | 5 +- web/pdf_page_view.js | 65 +++++++++++-------- web/pdf_viewer.css | 1 + 13 files changed, 333 insertions(+), 86 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 3e3ba1ad5..1e8605672 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -26,6 +26,7 @@ import { isAscii, isString, OPS, + RenderingIntentFlag, shadow, stringToPDFString, stringToUTF16BEString, @@ -386,6 +387,7 @@ class Annotation { modificationDate: this.modificationDate, rect: this.rectangle, subtype: params.subtype, + hasOwnCanvas: false, }; if (params.collectFields) { @@ -708,8 +710,8 @@ class Annotation { this.appearance = normalAppearanceState.get(as.name); } - loadResources(keys) { - return this.appearance.dict.getAsync("Resources").then(resources => { + loadResources(keys, appearance) { + return appearance.dict.getAsync("Resources").then(resources => { if (!resources) { return undefined; } @@ -721,22 +723,24 @@ class Annotation { }); } - getOperatorList(evaluator, task, renderForms, annotationStorage) { - if (!this.appearance) { - return Promise.resolve(new OperatorList()); + getOperatorList(evaluator, task, intent, renderForms, annotationStorage) { + const data = this.data; + let appearance = this.appearance; + const isUsingOwnCanvas = + data.hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY; + if (!appearance) { + if (!isUsingOwnCanvas) { + return Promise.resolve(new OperatorList()); + } + appearance = new StringStream(""); + appearance.dict = new Dict(); } - const appearance = this.appearance; - const data = this.data; const appearanceDict = appearance.dict; - const resourcesPromise = this.loadResources([ - "ExtGState", - "ColorSpace", - "Pattern", - "Shading", - "XObject", - "Font", - ]); + const resourcesPromise = this.loadResources( + ["ExtGState", "ColorSpace", "Pattern", "Shading", "XObject", "Font"], + appearance + ); const bbox = appearanceDict.getArray("BBox") || [0, 0, 1, 1]; const matrix = appearanceDict.getArray("Matrix") || [1, 0, 0, 1, 0, 0]; const transform = getTransformMatrix(data.rect, bbox, matrix); @@ -748,6 +752,7 @@ class Annotation { data.rect, transform, matrix, + isUsingOwnCanvas, ]); return evaluator @@ -1329,7 +1334,7 @@ class WidgetAnnotation extends Annotation { return !!(this.data.fieldFlags & flag); } - getOperatorList(evaluator, task, renderForms, annotationStorage) { + getOperatorList(evaluator, task, intent, renderForms, annotationStorage) { // Do not render form elements on the canvas when interactive forms are // enabled. The display layer is responsible for rendering them instead. if (renderForms && !(this instanceof SignatureWidgetAnnotation)) { @@ -1340,6 +1345,7 @@ class WidgetAnnotation extends Annotation { return super.getOperatorList( evaluator, task, + intent, renderForms, annotationStorage ); @@ -1351,6 +1357,7 @@ class WidgetAnnotation extends Annotation { return super.getOperatorList( evaluator, task, + intent, renderForms, annotationStorage ); @@ -1936,17 +1943,25 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { } else if (this.data.radioButton) { this._processRadioButton(params); } else if (this.data.pushButton) { + this.data.hasOwnCanvas = true; this._processPushButton(params); } else { warn("Invalid field flags for button widget annotation"); } } - async getOperatorList(evaluator, task, renderForms, annotationStorage) { + async getOperatorList( + evaluator, + task, + intent, + renderForms, + annotationStorage + ) { if (this.data.pushButton) { return super.getOperatorList( evaluator, task, + intent, false, // we use normalAppearance to render the button annotationStorage ); @@ -1965,6 +1980,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { return super.getOperatorList( evaluator, task, + intent, renderForms, annotationStorage ); @@ -1988,6 +2004,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { const operatorList = super.getOperatorList( evaluator, task, + intent, renderForms, annotationStorage ); diff --git a/src/core/document.js b/src/core/document.js index 4ac9c2788..c379a09ae 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -407,6 +407,7 @@ class Page { .getOperatorList( partialEvaluator, task, + intent, renderForms, annotationStorage ) diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index ecff7d96c..c52481a21 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -198,7 +198,25 @@ class AnnotationElement { page.view[3] - data.rect[3] + page.view[1], ]); - container.style.transform = `matrix(${viewport.transform.join(",")})`; + if (data.hasOwnCanvas) { + const transform = viewport.transform.slice(); + const [scaleX, scaleY] = Util.singularValueDecompose2dScale(transform); + width = Math.ceil(width * scaleX); + height = Math.ceil(height * scaleY); + rect[0] *= scaleX; + rect[1] *= scaleY; + // Reset the scale part of the transform matrix (which must be diagonal + // or anti-diagonal) in order to avoid to rescale the canvas. + // The canvas for the annotation is correctly scaled when it is drawn + // (see `beginAnnotation` in canvas.js). + for (let i = 0; i < 4; i++) { + transform[i] = Math.sign(transform[i]); + } + container.style.transform = `matrix(${transform.join(",")})`; + } else { + container.style.transform = `matrix(${viewport.transform.join(",")})`; + } + container.style.transformOrigin = `${-rect[0]}px ${-rect[1]}px`; if (!ignoreBorder && data.borderStyle.width > 0) { @@ -258,8 +276,13 @@ class AnnotationElement { container.style.left = `${rect[0]}px`; container.style.top = `${rect[1]}px`; - container.style.width = `${width}px`; - container.style.height = `${height}px`; + + if (data.hasOwnCanvas) { + container.style.width = container.style.height = "auto"; + } else { + container.style.width = `${width}px`; + container.style.height = `${height}px`; + } return container; } @@ -2318,10 +2341,12 @@ class AnnotationLayer { sortedAnnotations.push(...popupAnnotations); } + const div = parameters.div; + for (const data of sortedAnnotations) { const element = AnnotationElementFactory.create({ data, - layer: parameters.div, + layer: div, page: parameters.page, viewport: parameters.viewport, linkService: parameters.linkService, @@ -2343,19 +2368,21 @@ class AnnotationLayer { } if (Array.isArray(rendered)) { for (const renderedElement of rendered) { - parameters.div.appendChild(renderedElement); + div.appendChild(renderedElement); } } else { if (element instanceof PopupAnnotationElement) { // Popup annotation elements should not be on top of other // annotation elements to prevent interfering with mouse events. - parameters.div.prepend(rendered); + div.prepend(rendered); } else { - parameters.div.appendChild(rendered); + div.appendChild(rendered); } } } } + + this.#setAnnotationCanvasMap(div, parameters.annotationCanvasMap); } /** @@ -2366,18 +2393,73 @@ class AnnotationLayer { * @memberof AnnotationLayer */ static update(parameters) { - const transform = `matrix(${parameters.viewport.transform.join(",")})`; - for (const data of parameters.annotations) { - const elements = parameters.div.querySelectorAll( + const { page, viewport, annotations, annotationCanvasMap, div } = + parameters; + const transform = viewport.transform; + const matrix = `matrix(${transform.join(",")})`; + + let scale, ownMatrix; + for (const data of annotations) { + const elements = div.querySelectorAll( `[data-annotation-id="${data.id}"]` ); if (elements) { for (const element of elements) { - element.style.transform = transform; + if (data.hasOwnCanvas) { + const rect = Util.normalizeRect([ + data.rect[0], + page.view[3] - data.rect[1] + page.view[1], + data.rect[2], + page.view[3] - data.rect[3] + page.view[1], + ]); + + if (!ownMatrix) { + // When an annotation has its own canvas, then + // the scale has been already applied to the canvas, + // so we musn't scale it twice. + scale = Math.abs(transform[0] || transform[1]); + const ownTransform = transform.slice(); + for (let i = 0; i < 4; i++) { + ownTransform[i] = Math.sign(ownTransform[i]); + } + ownMatrix = `matrix(${ownTransform.join(",")})`; + } + + const left = rect[0] * scale; + const top = rect[1] * scale; + element.style.left = `${left}px`; + element.style.top = `${top}px`; + element.style.transformOrigin = `${-left}px ${-top}px`; + element.style.transform = ownMatrix; + } else { + element.style.transform = matrix; + } } } } - parameters.div.hidden = false; + + this.#setAnnotationCanvasMap(div, annotationCanvasMap); + div.hidden = false; + } + + static #setAnnotationCanvasMap(div, annotationCanvasMap) { + if (!annotationCanvasMap) { + return; + } + for (const [id, canvas] of annotationCanvasMap) { + const element = div.querySelector(`[data-annotation-id="${id}"]`); + if (!element) { + continue; + } + + const { firstChild } = element; + if (firstChild.nodeName === "CANVAS") { + element.replaceChild(canvas, firstChild); + } else { + element.insertBefore(canvas, firstChild); + } + } + annotationCanvasMap.clear(); } } diff --git a/src/display/api.js b/src/display/api.js index b975365a4..b3a7fdc95 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1161,6 +1161,8 @@ class PDFDocumentProxy { * created from `PDFDocumentProxy.getOptionalContentConfig`. If `null`, * the configuration will be fetched automatically with the default visibility * states set. + * @property {Map} [annotationCanvasMap] - Map some annotation + * ids with canvases used to render them. */ /** @@ -1374,6 +1376,7 @@ class PDFPageProxy { canvasFactory = null, background = null, optionalContentConfigPromise = null, + annotationCanvasMap = null, }) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) { if (arguments[0]?.renderInteractiveForms !== undefined) { @@ -1491,6 +1494,7 @@ class PDFPageProxy { }, objs: this.objs, commonObjs: this.commonObjs, + annotationCanvasMap, operatorList: intentState.operatorList, pageIndex: this._pageIndex, canvasFactory: canvasFactoryInstance, @@ -3216,6 +3220,7 @@ class InternalRenderTask { params, objs, commonObjs, + annotationCanvasMap, operatorList, pageIndex, canvasFactory, @@ -3226,6 +3231,7 @@ class InternalRenderTask { this.params = params; this.objs = objs; this.commonObjs = commonObjs; + this.annotationCanvasMap = annotationCanvasMap; this.operatorListIdx = null; this.operatorList = operatorList; this._pageIndex = pageIndex; @@ -3284,7 +3290,8 @@ class InternalRenderTask { this.objs, this.canvasFactory, imageLayer, - optionalContentConfig + optionalContentConfig, + this.annotationCanvasMap ); this.gfx.beginDrawing({ transform, diff --git a/src/display/canvas.js b/src/display/canvas.js index 713dc9dd9..92cf8da0c 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -1068,7 +1068,8 @@ class CanvasGraphics { objs, canvasFactory, imageLayer, - optionalContentConfig + optionalContentConfig, + annotationCanvasMap ) { this.ctx = canvasCtx; this.current = new CanvasExtraState( @@ -1100,6 +1101,10 @@ class CanvasGraphics { this.optionalContentConfig = optionalContentConfig; this.cachedCanvases = new CachedCanvases(this.canvasFactory); this.cachedPatterns = new Map(); + this.annotationCanvasMap = annotationCanvasMap; + this.viewportScale = 1; + this.outputScaleX = 1; + this.outputScaleY = 1; if (canvasCtx) { // NOTE: if mozCurrentTransform is polyfilled, then the current state of // the transformation must already be set in canvasCtx._transformMatrix. @@ -1147,8 +1152,11 @@ class CanvasGraphics { resetCtxToDefault(this.ctx); if (transform) { this.ctx.transform.apply(this.ctx, transform); + this.outputScaleX = transform[0]; + this.outputScaleY = transform[0]; } this.ctx.transform.apply(this.ctx, viewport.transform); + this.viewportScale = viewport.scale; this.baseTransform = this.ctx.mozCurrentTransform.slice(); this._combinedScaleFactor = Math.hypot( @@ -2691,27 +2699,72 @@ class CanvasGraphics { this.restore(); } - beginAnnotation(id, rect, transform, matrix) { + beginAnnotation(id, rect, transform, matrix, hasOwnCanvas) { this.save(); - resetCtxToDefault(this.ctx); - this.current = new CanvasExtraState( - this.ctx.canvas.width, - this.ctx.canvas.height - ); if (Array.isArray(rect) && rect.length === 4) { const width = rect[2] - rect[0]; const height = rect[3] - rect[1]; - this.ctx.rect(rect[0], rect[1], width, height); - this.clip(); - this.endPath(); + + if (hasOwnCanvas && this.annotationCanvasMap) { + transform = transform.slice(); + transform[4] -= rect[0]; + transform[5] -= rect[1]; + + rect = rect.slice(); + rect[0] = rect[1] = 0; + rect[2] = width; + rect[3] = height; + + const [scaleX, scaleY] = Util.singularValueDecompose2dScale( + this.ctx.mozCurrentTransform + ); + const { viewportScale } = this; + const canvasWidth = Math.ceil( + width * this.outputScaleX * viewportScale + ); + const canvasHeight = Math.ceil( + height * this.outputScaleY * viewportScale + ); + + this.annotationCanvas = this.canvasFactory.create( + canvasWidth, + canvasHeight + ); + const { canvas, context } = this.annotationCanvas; + canvas.style.width = `calc(${width}px * var(--viewport-scale-factor))`; + canvas.style.height = `calc(${height}px * var(--viewport-scale-factor))`; + this.annotationCanvasMap.set(id, canvas); + this.annotationCanvas.savedCtx = this.ctx; + this.ctx = context; + this.ctx.setTransform(scaleX, 0, 0, -scaleY, 0, height * scaleY); + addContextCurrentTransform(this.ctx); + + resetCtxToDefault(this.ctx); + } else { + resetCtxToDefault(this.ctx); + + this.ctx.rect(rect[0], rect[1], width, height); + this.clip(); + this.endPath(); + } } + this.current = new CanvasExtraState( + this.ctx.canvas.width, + this.ctx.canvas.height + ); + this.transform.apply(this, transform); this.transform.apply(this, matrix); } endAnnotation() { + if (this.annotationCanvas) { + this.ctx = this.annotationCanvas.savedCtx; + delete this.annotationCanvas.savedCtx; + delete this.annotationCanvas; + } this.restore(); } diff --git a/test/driver.js b/test/driver.js index 2eadf239e..62a3a317d 100644 --- a/test/driver.js +++ b/test/driver.js @@ -101,6 +101,25 @@ function inlineImages(images) { return Promise.all(imagePromises); } +async function convertCanvasesToImages(annotationCanvasMap) { + const results = new Map(); + const promises = []; + for (const [key, canvas] of annotationCanvasMap) { + promises.push( + new Promise(resolve => { + canvas.toBlob(blob => { + const image = document.createElement("img"); + image.onload = resolve; + results.set(key, image); + image.src = URL.createObjectURL(blob); + }); + }) + ); + } + await Promise.all(promises); + return results; +} + async function resolveImages(node, silentErrors = false) { const images = node.getElementsByTagName("img"); const data = await inlineImages(images); @@ -227,6 +246,7 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { ctx, viewport, annotations, + annotationCanvasMap, page, imageResourcesPath, renderForms = false @@ -255,6 +275,10 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { style.textContent = common + "\n" + overrides; var annotation_viewport = viewport.clone({ dontFlip: true }); + const annotationImageMap = await convertCanvasesToImages( + annotationCanvasMap + ); + var parameters = { viewport: annotation_viewport, div, @@ -263,6 +287,7 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { linkService: new SimpleLinkService(), imageResourcesPath, renderForms, + annotationCanvasMap: annotationImageMap, }; AnnotationLayer.render(parameters); @@ -671,7 +696,8 @@ var Driver = (function DriverClosure() { var renderAnnotations = false, renderForms = false, renderPrint = false, - renderXfa = false; + renderXfa = false, + annotationCanvasMap = null; if (task.annotationStorage) { const entries = Object.entries(task.annotationStorage), @@ -745,18 +771,8 @@ var Driver = (function DriverClosure() { if (!renderXfa) { // The annotation builder will draw its content // on the canvas. - initPromise = page - .getAnnotations({ intent: "display" }) - .then(function (annotations) { - return rasterizeAnnotationLayer( - annotationLayerContext, - viewport, - annotations, - page, - IMAGE_RESOURCES_PATH, - renderForms - ); - }); + initPromise = page.getAnnotations({ intent: "display" }); + annotationCanvasMap = new Map(); } else { initPromise = page.getXfa().then(function (xfa) { return rasterizeXfaLayer( @@ -774,11 +790,11 @@ var Driver = (function DriverClosure() { initPromise = Promise.resolve(); } } - var renderContext = { canvasContext: ctx, viewport, optionalContentConfigPromise: task.optionalContentConfigPromise, + annotationCanvasMap, }; if (renderForms) { renderContext.annotationMode = AnnotationMode.ENABLE_FORMS; @@ -811,7 +827,7 @@ var Driver = (function DriverClosure() { self._snapshot(task, error); }; initPromise - .then(function () { + .then(function (data) { const renderTask = page.render(renderContext); if (task.renderTaskOnContinue) { @@ -821,7 +837,21 @@ var Driver = (function DriverClosure() { }; } return renderTask.promise.then(function () { - completeRender(false); + if (annotationCanvasMap) { + rasterizeAnnotationLayer( + annotationLayerContext, + viewport, + data, + annotationCanvasMap, + page, + IMAGE_RESOURCES_PATH, + renderForms + ).then(() => { + completeRender(false); + }); + } else { + completeRender(false); + } }); }) .catch(function (error) { diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 26a470fe6..ca7842863 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -26,6 +26,7 @@ import { AnnotationFlag, AnnotationType, OPS, + RenderingIntentFlag, stringToBytes, stringToUTF8String, } from "../../src/shared/util.js"; @@ -1680,6 +1681,7 @@ describe("annotation", function () { const operatorList = await annotation.getOperatorList( partialEvaluator, task, + RenderingIntentFlag.PRINT, false, annotationStorage ); @@ -1694,6 +1696,7 @@ describe("annotation", function () { [0, 0, 32, 10], [32, 0, 0, 10, 0, 0], [1, 0, 0, 1, 0, 0], + false, ]); expect(operatorList.argsArray[1]).toEqual( new Uint8ClampedArray([26, 51, 76]) @@ -2319,6 +2322,7 @@ describe("annotation", function () { const operatorList = await annotation.getOperatorList( checkboxEvaluator, task, + RenderingIntentFlag.PRINT, false, annotationStorage ); @@ -2335,6 +2339,7 @@ describe("annotation", function () { [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], + false, ]); expect(operatorList.argsArray[3][0][0].unicode).toEqual("4"); }); @@ -2378,6 +2383,7 @@ describe("annotation", function () { let operatorList = await annotation.getOperatorList( partialEvaluator, task, + RenderingIntentFlag.PRINT, false, annotationStorage ); @@ -2392,6 +2398,7 @@ describe("annotation", function () { [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], + false, ]); expect(operatorList.argsArray[1]).toEqual( new Uint8ClampedArray([26, 51, 76]) @@ -2402,6 +2409,7 @@ describe("annotation", function () { operatorList = await annotation.getOperatorList( partialEvaluator, task, + RenderingIntentFlag.PRINT, false, annotationStorage ); @@ -2416,6 +2424,7 @@ describe("annotation", function () { [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], + false, ]); expect(operatorList.argsArray[1]).toEqual( new Uint8ClampedArray([76, 51, 26]) @@ -2464,6 +2473,7 @@ describe("annotation", function () { const operatorList = await annotation.getOperatorList( partialEvaluator, task, + RenderingIntentFlag.PRINT, false, annotationStorage ); @@ -2478,6 +2488,7 @@ describe("annotation", function () { [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], + false, ]); expect(operatorList.argsArray[1]).toEqual( new Uint8ClampedArray([26, 51, 76]) @@ -2524,6 +2535,7 @@ describe("annotation", function () { const operatorList = await annotation.getOperatorList( partialEvaluator, task, + RenderingIntentFlag.PRINT, false, annotationStorage ); @@ -2538,6 +2550,7 @@ describe("annotation", function () { [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], + false, ]); expect(operatorList.argsArray[1]).toEqual( new Uint8ClampedArray([26, 51, 76]) @@ -2727,6 +2740,7 @@ describe("annotation", function () { let operatorList = await annotation.getOperatorList( partialEvaluator, task, + RenderingIntentFlag.PRINT, false, annotationStorage ); @@ -2741,6 +2755,7 @@ describe("annotation", function () { [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], + false, ]); expect(operatorList.argsArray[1]).toEqual( new Uint8ClampedArray([26, 51, 76]) @@ -2751,6 +2766,7 @@ describe("annotation", function () { operatorList = await annotation.getOperatorList( partialEvaluator, task, + RenderingIntentFlag.PRINT, false, annotationStorage ); @@ -2765,6 +2781,7 @@ describe("annotation", function () { [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], + false, ]); expect(operatorList.argsArray[1]).toEqual( new Uint8ClampedArray([76, 51, 26]) @@ -2811,6 +2828,7 @@ describe("annotation", function () { const operatorList = await annotation.getOperatorList( partialEvaluator, task, + RenderingIntentFlag.PRINT, false, annotationStorage ); @@ -2825,6 +2843,7 @@ describe("annotation", function () { [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], + false, ]); expect(operatorList.argsArray[1]).toEqual( new Uint8ClampedArray([76, 51, 26]) diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 6368d3381..f0aaec32e 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -32,6 +32,13 @@ height: 100%; } +.annotationLayer .buttonWidgetAnnotation.pushButton > canvas { + position: relative; + top: 0; + left: 0; + z-index: -1; +} + .annotationLayer .linkAnnotation > a:hover, .annotationLayer .buttonWidgetAnnotation.pushButton > a:hover { opacity: 0.2; diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index 843b07b2c..3a55a87ef 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -36,6 +36,7 @@ import { SimpleLinkService } from "./pdf_link_service.js"; * @property {Promise> | null>} * [fieldObjectsPromise] * @property {Object} [mouseState] + * @property {Map} [annotationCanvasMap] */ class AnnotationLayerBuilder { @@ -55,6 +56,7 @@ class AnnotationLayerBuilder { hasJSActionsPromise = null, fieldObjectsPromise = null, mouseState = null, + annotationCanvasMap = null, }) { this.pageDiv = pageDiv; this.pdfPage = pdfPage; @@ -68,6 +70,7 @@ class AnnotationLayerBuilder { this._hasJSActionsPromise = hasJSActionsPromise; this._fieldObjectsPromise = fieldObjectsPromise; this._mouseState = mouseState; + this._annotationCanvasMap = annotationCanvasMap; this.div = null; this._cancelled = false; @@ -105,6 +108,7 @@ class AnnotationLayerBuilder { hasJSActions, fieldObjects, mouseState: this._mouseState, + annotationCanvasMap: this._annotationCanvasMap, }; if (this.div) { @@ -153,6 +157,8 @@ class DefaultAnnotationLayerFactory { * @param {Object} [mouseState] * @param {Promise> | null>} * [fieldObjectsPromise] + * @param {Map | null} [annotationCanvasMap] - Map some + * annotation ids with canvases used to render them. * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -165,7 +171,8 @@ class DefaultAnnotationLayerFactory { enableScripting = false, hasJSActionsPromise = null, mouseState = null, - fieldObjectsPromise = null + fieldObjectsPromise = null, + annotationCanvasMap = null ) { return new AnnotationLayerBuilder({ pageDiv, @@ -179,6 +186,7 @@ class DefaultAnnotationLayerFactory { hasJSActionsPromise, fieldObjectsPromise, mouseState, + annotationCanvasMap, }); } } diff --git a/web/base_viewer.js b/web/base_viewer.js index 5f9f67747..815c779b9 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -850,7 +850,12 @@ class BaseViewer { } return; } + this._doc.style.setProperty("--zoom-factor", newScale); + this._doc.style.setProperty( + "--viewport-scale-factor", + newScale * PixelsPerInch.PDF_TO_CSS_UNITS + ); const updateArgs = { scale: newScale }; for (const pageView of this._pages) { @@ -1480,6 +1485,7 @@ class BaseViewer { * @param {Object} [mouseState] * @param {Promise> | null>} * [fieldObjectsPromise] + * @param {Map} [annotationCanvasMap] * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -1492,7 +1498,8 @@ class BaseViewer { enableScripting = null, hasJSActionsPromise = null, mouseState = null, - fieldObjectsPromise = null + fieldObjectsPromise = null, + annotationCanvasMap = null ) { return new AnnotationLayerBuilder({ pageDiv, @@ -1510,6 +1517,7 @@ class BaseViewer { fieldObjectsPromise: fieldObjectsPromise || this.pdfDocument?.getFieldObjects(), mouseState: mouseState || this._scriptingManager?.mouseState, + annotationCanvasMap, }); } diff --git a/web/interfaces.js b/web/interfaces.js index ad9af67e1..dd1afd814 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -175,6 +175,8 @@ class IPDFAnnotationLayerFactory { * @param {Object} [mouseState] * @param {Promise> | null>} * [fieldObjectsPromise] + * @property {Map | null} [annotationCanvasMap] - Map some + * annotation ids with canvases used to render them. * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -187,7 +189,8 @@ class IPDFAnnotationLayerFactory { enableScripting = false, hasJSActionsPromise = null, mouseState = null, - fieldObjectsPromise = null + fieldObjectsPromise = null, + annotationCanvasMap = null ) {} } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index ca87ab4d8..bbcafb241 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -123,6 +123,8 @@ class PDFPageView { this._renderError = null; this._isStandalone = !this.renderingQueue?.hasViewer(); + this._annotationCanvasMap = null; + this.annotationLayer = null; this.textLayer = null; this.zoomLayer = null; @@ -322,17 +324,20 @@ class PDFPageView { if (optionalContentConfigPromise instanceof Promise) { this._optionalContentConfigPromise = optionalContentConfigPromise; } - if (this._isStandalone) { - const doc = document.documentElement; - doc.style.setProperty("--zoom-factor", this.scale); - } const totalRotation = (this.rotation + this.pdfPageRotate) % 360; + const viewportScale = this.scale * PixelsPerInch.PDF_TO_CSS_UNITS; this.viewport = this.viewport.clone({ - scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, + scale: viewportScale, rotation: totalRotation, }); + if (this._isStandalone) { + const { style } = document.documentElement; + style.setProperty("--zoom-factor", this.scale); + style.setProperty("--viewport-scale-factor", viewportScale); + } + if (this.svg) { this.cssTransform({ target: this.svg, @@ -418,6 +423,7 @@ class PDFPageView { ) { this.annotationLayer.cancel(); this.annotationLayer = null; + this._annotationCanvasMap = null; } if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) { this.xfaLayer.cancel(); @@ -580,6 +586,27 @@ class PDFPageView { } this.textLayer = textLayer; + if ( + this._annotationMode !== AnnotationMode.DISABLE && + this.annotationLayerFactory + ) { + this._annotationCanvasMap ||= new Map(); + this.annotationLayer ||= + this.annotationLayerFactory.createAnnotationLayerBuilder( + div, + pdfPage, + /* annotationStorage = */ null, + this.imageResourcesPath, + this._annotationMode === AnnotationMode.ENABLE_FORMS, + this.l10n, + /* enableScripting = */ null, + /* hasJSActionsPromise = */ null, + /* mouseState = */ null, + /* fieldObjectsPromise = */ null, + /* annotationCanvasMap */ this._annotationCanvasMap + ); + } + if (this.xfaLayer?.div) { // The xfa layer needs to stay on top. div.appendChild(this.xfaLayer.div); @@ -653,6 +680,10 @@ class PDFPageView { textLayer.setTextContentStream(readableStream); textLayer.render(); } + + if (this.annotationLayer) { + this._renderAnnotationLayer(); + } }); }, function (reason) { @@ -660,28 +691,6 @@ class PDFPageView { } ); - if ( - this._annotationMode !== AnnotationMode.DISABLE && - this.annotationLayerFactory - ) { - if (!this.annotationLayer) { - this.annotationLayer = - this.annotationLayerFactory.createAnnotationLayerBuilder( - div, - pdfPage, - /* annotationStorage = */ null, - this.imageResourcesPath, - this._annotationMode === AnnotationMode.ENABLE_FORMS, - this.l10n, - /* enableScripting = */ null, - /* hasJSActionsPromise = */ null, - /* mouseState = */ null, - /* fieldObjectsPromise = */ null - ); - } - this._renderAnnotationLayer(); - } - if (this.xfaLayerFactory) { if (!this.xfaLayer) { this.xfaLayer = this.xfaLayerFactory.createXfaLayerBuilder( @@ -804,6 +813,7 @@ class PDFPageView { canvas.height = roundToDivide(viewport.height * outputScale.sy, sfy[0]); canvas.style.width = roundToDivide(viewport.width, sfx[1]) + "px"; canvas.style.height = roundToDivide(viewport.height, sfy[1]) + "px"; + // Add the viewport so it's known what it was originally drawn with. this.paintedViewportMap.set(canvas, viewport); @@ -817,6 +827,7 @@ class PDFPageView { viewport: this.viewport, annotationMode: this._annotationMode, optionalContentConfigPromise: this._optionalContentConfigPromise, + annotationCanvasMap: this._annotationCanvasMap, }; const renderTask = this.pdfPage.render(renderContext); renderTask.onContinue = function (cont) { diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index b9e640832..73533ae7e 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -22,6 +22,7 @@ --page-border: 9px solid transparent; --spreadHorizontalWrapped-margin-LR: -3.5px; --zoom-factor: 1; + --viewport-scale-factor: 1; } @media screen and (forced-colors: active) {