diff --git a/src/display/display_utils.js b/src/display/display_utils.js index d8694e1542a29..f88dd3041baee 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -1124,6 +1124,36 @@ function setLayerDimensions( } } +/** + * Scale factors for the canvas, necessary with HiDPI displays. + */ +class OutputScale { + constructor() { + const pixelRatio = window.devicePixelRatio || 1; + + /** + * @type {number} Horizontal scale. + */ + this.sx = pixelRatio; + + /** + * @type {number} Vertical scale. + */ + this.sy = pixelRatio; + } + + /** + * @type {boolean} Returns `true` when scaling is required, `false` otherwise. + */ + get scaled() { + return this.sx !== 1 || this.sy !== 1; + } + + get symmetric() { + return this.sx === this.sy; + } +} + export { deprecated, DOMCanvasFactory, @@ -1143,6 +1173,7 @@ export { isPdfFile, isValidFetchUrl, noContextMenu, + OutputScale, PageViewport, PDFDateString, PixelsPerInch, diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index 4b912c0a95447..6dd348d320545 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -14,8 +14,8 @@ */ import { AnnotationEditorType, shadow } from "../../shared/util.js"; +import { OutputScale, PixelsPerInch } from "../display_utils.js"; import { AnnotationEditor } from "./editor.js"; -import { PixelsPerInch } from "../display_utils.js"; import { StampAnnotationElement } from "../annotation_layer.js"; /** @@ -185,7 +185,7 @@ class StampEditor extends AnnotationEditor { } const { data, width, height } = imageData || - this.copyCanvas(null, /* createImageData = */ true).imageData; + this.copyCanvas(null, null, /* createImageData = */ true).imageData; const response = await mlManager.guess({ name: "altText", request: { @@ -453,61 +453,108 @@ class StampEditor extends AnnotationEditor { } } - copyCanvas(maxDimension, createImageData = false) { - if (!maxDimension) { + copyCanvas(maxDataDimension, maxPreviewDimension, createImageData = false) { + if (!maxDataDimension) { // TODO: get this value from Firefox // (https://bugzilla.mozilla.org/show_bug.cgi?id=1908184) // It's the maximum dimension that the AI can handle. - maxDimension = 224; + maxDataDimension = 224; } const { width: bitmapWidth, height: bitmapHeight } = this.#bitmap; - const canvas = document.createElement("canvas"); + const outputScale = new OutputScale(); let bitmap = this.#bitmap; let width = bitmapWidth, height = bitmapHeight; - if (bitmapWidth > maxDimension || bitmapHeight > maxDimension) { - const ratio = Math.min( - maxDimension / bitmapWidth, - maxDimension / bitmapHeight - ); - width = Math.floor(bitmapWidth * ratio); - height = Math.floor(bitmapHeight * ratio); + let canvas = null; + + if (maxPreviewDimension) { + if ( + bitmapWidth > maxPreviewDimension || + bitmapHeight > maxPreviewDimension + ) { + const ratio = Math.min( + maxPreviewDimension / bitmapWidth, + maxPreviewDimension / bitmapHeight + ); + width = Math.floor(bitmapWidth * ratio); + height = Math.floor(bitmapHeight * ratio); + } + + canvas = document.createElement("canvas"); + const scaledWidth = (canvas.width = Math.ceil(width * outputScale.sx)); + const scaledHeight = (canvas.height = Math.ceil(height * outputScale.sy)); if (!this.#isSvg) { - bitmap = this.#scaleBitmap(width, height); + bitmap = this.#scaleBitmap(scaledWidth, scaledHeight); } - } - - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext("2d"); - ctx.filter = this._uiManager.hcmFilter; - // Add a checkerboard pattern as a background in case the image has some - // transparency. - let white = "white", - black = "#cfcfd8"; - if (this._uiManager.hcmFilter !== "none") { - black = "black"; - } else if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) { - white = "#8f8f9d"; - black = "#42414d"; - } - const boxDim = 15; - const pattern = new OffscreenCanvas(boxDim * 2, boxDim * 2); - const patternCtx = pattern.getContext("2d"); - patternCtx.fillStyle = white; - patternCtx.fillRect(0, 0, boxDim * 2, boxDim * 2); - patternCtx.fillStyle = black; - patternCtx.fillRect(0, 0, boxDim, boxDim); - patternCtx.fillRect(boxDim, boxDim, boxDim, boxDim); - ctx.fillStyle = ctx.createPattern(pattern, "repeat"); - ctx.fillRect(0, 0, width, height); + const ctx = canvas.getContext("2d"); + ctx.filter = this._uiManager.hcmFilter; + + // Add a checkerboard pattern as a background in case the image has some + // transparency. + let white = "white", + black = "#cfcfd8"; + if (this._uiManager.hcmFilter !== "none") { + black = "black"; + } else if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) { + white = "#8f8f9d"; + black = "#42414d"; + } + const boxDim = 15; + const boxDimWidth = boxDim * outputScale.sx; + const boxDimHeight = boxDim * outputScale.sy; + const pattern = new OffscreenCanvas(boxDimWidth * 2, boxDimHeight * 2); + const patternCtx = pattern.getContext("2d"); + patternCtx.fillStyle = white; + patternCtx.fillRect(0, 0, boxDimWidth * 2, boxDimHeight * 2); + patternCtx.fillStyle = black; + patternCtx.fillRect(0, 0, boxDimWidth, boxDimHeight); + patternCtx.fillRect(boxDimWidth, boxDimHeight, boxDimWidth, boxDimHeight); + ctx.fillStyle = ctx.createPattern(pattern, "repeat"); + ctx.fillRect(0, 0, scaledWidth, scaledHeight); + ctx.drawImage( + bitmap, + 0, + 0, + bitmap.width, + bitmap.height, + 0, + 0, + scaledWidth, + scaledHeight + ); + } + let imageData = null; if (createImageData) { - const offscreen = new OffscreenCanvas(width, height); + let dataWidth, dataHeight; + if ( + outputScale.symmetric && + bitmap.width < maxDataDimension && + bitmap.height < maxDataDimension + ) { + dataWidth = bitmap.width; + dataHeight = bitmap.height; + } else { + bitmap = this.#bitmap; + if (bitmapWidth > maxDataDimension || bitmapHeight > maxDataDimension) { + const ratio = Math.min( + maxDataDimension / bitmapWidth, + maxDataDimension / bitmapHeight + ); + dataWidth = Math.floor(bitmapWidth * ratio); + dataHeight = Math.floor(bitmapHeight * ratio); + + if (!this.#isSvg) { + bitmap = this.#scaleBitmap(dataWidth, dataHeight); + } + } + } + + const offscreen = new OffscreenCanvas(dataWidth, dataHeight); const offscreenCtx = offscreen.getContext("2d", { willReadFrequently: true, }); @@ -519,27 +566,17 @@ class StampEditor extends AnnotationEditor { bitmap.height, 0, 0, - width, - height + dataWidth, + dataHeight ); - const data = offscreenCtx.getImageData(0, 0, width, height).data; - ctx.drawImage(offscreen, 0, 0); - - return { canvas, imageData: { width, height, data } }; + imageData = { + width: dataWidth, + height: dataHeight, + data: offscreenCtx.getImageData(0, 0, dataWidth, dataHeight).data, + }; } - ctx.drawImage( - bitmap, - 0, - 0, - bitmap.width, - bitmap.height, - 0, - 0, - width, - height - ); - return { canvas, imageData: null }; + return { canvas, width, height, imageData }; } /** @@ -619,17 +656,23 @@ class StampEditor extends AnnotationEditor { } #drawBitmap(width, height) { - width = Math.ceil(width); - height = Math.ceil(height); + const outputScale = new OutputScale(); + const scaledWidth = Math.ceil(width * outputScale.sx); + const scaledHeight = Math.ceil(height * outputScale.sy); + const canvas = this.#canvas; - if (!canvas || (canvas.width === width && canvas.height === height)) { + if ( + !canvas || + (canvas.width === scaledWidth && canvas.height === scaledHeight) + ) { return; } - canvas.width = width; - canvas.height = height; + canvas.width = scaledWidth; + canvas.height = scaledHeight; + const bitmap = this.#isSvg ? this.#bitmap - : this.#scaleBitmap(width, height); + : this.#scaleBitmap(scaledWidth, scaledHeight); const ctx = canvas.getContext("2d"); ctx.filter = this._uiManager.hcmFilter; @@ -641,8 +684,8 @@ class StampEditor extends AnnotationEditor { bitmap.height, 0, 0, - width, - height + scaledWidth, + scaledHeight ); } diff --git a/src/pdf.js b/src/pdf.js index 064ae6c61f802..c90244e8abcfe 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -58,6 +58,7 @@ import { isDataScheme, isPdfFile, noContextMenu, + OutputScale, PDFDateString, PixelsPerInch, RenderingCancelledException, @@ -115,6 +116,7 @@ export { noContextMenu, normalizeUnicode, OPS, + OutputScale, PasswordResponses, PDFDataRangeTransport, PDFDateString, diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 7e2224c8de772..0a93ced31859f 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -50,6 +50,7 @@ import { isDataScheme, isPdfFile, noContextMenu, + OutputScale, PDFDateString, PixelsPerInch, RenderingCancelledException, @@ -93,6 +94,7 @@ const expectedAPI = Object.freeze({ noContextMenu, normalizeUnicode, OPS, + OutputScale, PasswordResponses, PDFDataRangeTransport, PDFDateString, diff --git a/web/new_alt_text_manager.js b/web/new_alt_text_manager.js index 3ac259a0551f4..b4481b2a8372a 100644 --- a/web/new_alt_text_manager.js +++ b/web/new_alt_text_manager.js @@ -371,14 +371,21 @@ class NewAltTextManager { // TODO: get this value from Firefox // (https://bugzilla.mozilla.org/show_bug.cgi?id=1908184) const AI_MAX_IMAGE_DIMENSION = 224; + const MAX_PREVIEW_DIMENSION = 180; // The max dimension of the preview in the dialog is 180px, so we keep 224px // and rescale it thanks to css. - let canvas; + let canvas, width, height; if (mlManager) { - ({ canvas, imageData: this.#imageData } = editor.copyCanvas( + ({ + canvas, + width, + height, + imageData: this.#imageData, + } = editor.copyCanvas( AI_MAX_IMAGE_DIMENSION, + MAX_PREVIEW_DIMENSION, /* createImageData = */ true )); if (hasAI) { @@ -388,13 +395,17 @@ class NewAltTextManager { ); } } else { - ({ canvas } = editor.copyCanvas( + ({ canvas, width, height } = editor.copyCanvas( AI_MAX_IMAGE_DIMENSION, + MAX_PREVIEW_DIMENSION, /* createImageData = */ false )); } canvas.setAttribute("role", "presentation"); + const { style } = canvas; + style.width = `${width}px`; + style.height = `${height}px`; this.#imagePreview.append(canvas); this.#toggleNotNow(); diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 2714109cc5e1d..4737775eeea32 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -26,6 +26,7 @@ import { AbortException, AnnotationMode, + OutputScale, PixelsPerInch, RenderingCancelledException, setLayerDimensions, @@ -36,7 +37,6 @@ import { calcRound, DEFAULT_SCALE, floorToDivide, - OutputScale, RenderingStates, TextLayerMode, } from "./ui_utils.js"; diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js index 8be3b62ad250f..df28a7632d22e 100644 --- a/web/pdf_thumbnail_view.js +++ b/web/pdf_thumbnail_view.js @@ -23,8 +23,8 @@ // eslint-disable-next-line max-len /** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */ -import { OutputScale, RenderingStates } from "./ui_utils.js"; -import { RenderingCancelledException } from "pdfjs-lib"; +import { OutputScale, RenderingCancelledException } from "pdfjs-lib"; +import { RenderingStates } from "./ui_utils.js"; const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below. const MAX_NUM_SCALING_STEPS = 3; diff --git a/web/pdfjs.js b/web/pdfjs.js index ad709d93676a1..ccd6b9b6d15e3 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -42,6 +42,7 @@ const { noContextMenu, normalizeUnicode, OPS, + OutputScale, PasswordResponses, PDFDataRangeTransport, PDFDateString, @@ -88,6 +89,7 @@ export { noContextMenu, normalizeUnicode, OPS, + OutputScale, PasswordResponses, PDFDataRangeTransport, PDFDateString, diff --git a/web/ui_utils.js b/web/ui_utils.js index 12c74f1fec4b3..07dc55c7a7221 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -76,32 +76,6 @@ const CursorTool = { // Used by `PDFViewerApplication`, and by the API unit-tests. const AutoPrintRegExp = /\bprint\s*\(/; -/** - * Scale factors for the canvas, necessary with HiDPI displays. - */ -class OutputScale { - constructor() { - const pixelRatio = window.devicePixelRatio || 1; - - /** - * @type {number} Horizontal scale. - */ - this.sx = pixelRatio; - - /** - * @type {number} Vertical scale. - */ - this.sy = pixelRatio; - } - - /** - * @type {boolean} Returns `true` when scaling is required, `false` otherwise. - */ - get scaled() { - return this.sx !== 1 || this.sy !== 1; - } -} - /** * Scrolls specified element into view of its parent. * @param {HTMLElement} element - The element to be visible. @@ -908,7 +882,6 @@ export { MIN_SCALE, normalizeWheelEventDelta, normalizeWheelEventDirection, - OutputScale, parseQueryString, PresentationModeState, ProgressBar,