[Editor] Take into account the device pixel ratio when drawing an added image

Fixes #18626.
This commit is contained in:
Calixte Denizet 2024-09-15 20:40:01 +02:00
parent c72fb9b733
commit 46fac8b2c1
9 changed files with 164 additions and 100 deletions

View File

@ -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 { export {
deprecated, deprecated,
DOMCanvasFactory, DOMCanvasFactory,
@ -1143,6 +1173,7 @@ export {
isPdfFile, isPdfFile,
isValidFetchUrl, isValidFetchUrl,
noContextMenu, noContextMenu,
OutputScale,
PageViewport, PageViewport,
PDFDateString, PDFDateString,
PixelsPerInch, PixelsPerInch,

View File

@ -14,8 +14,8 @@
*/ */
import { AnnotationEditorType, shadow } from "../../shared/util.js"; import { AnnotationEditorType, shadow } from "../../shared/util.js";
import { OutputScale, PixelsPerInch } from "../display_utils.js";
import { AnnotationEditor } from "./editor.js"; import { AnnotationEditor } from "./editor.js";
import { PixelsPerInch } from "../display_utils.js";
import { StampAnnotationElement } from "../annotation_layer.js"; import { StampAnnotationElement } from "../annotation_layer.js";
/** /**
@ -185,7 +185,7 @@ class StampEditor extends AnnotationEditor {
} }
const { data, width, height } = const { data, width, height } =
imageData || imageData ||
this.copyCanvas(null, /* createImageData = */ true).imageData; this.copyCanvas(null, null, /* createImageData = */ true).imageData;
const response = await mlManager.guess({ const response = await mlManager.guess({
name: "altText", name: "altText",
request: { request: {
@ -453,35 +453,43 @@ class StampEditor extends AnnotationEditor {
} }
} }
copyCanvas(maxDimension, createImageData = false) { copyCanvas(maxDataDimension, maxPreviewDimension, createImageData = false) {
if (!maxDimension) { if (!maxDataDimension) {
// TODO: get this value from Firefox // TODO: get this value from Firefox
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1908184) // (https://bugzilla.mozilla.org/show_bug.cgi?id=1908184)
// It's the maximum dimension that the AI can handle. // It's the maximum dimension that the AI can handle.
maxDimension = 224; maxDataDimension = 224;
} }
const { width: bitmapWidth, height: bitmapHeight } = this.#bitmap; const { width: bitmapWidth, height: bitmapHeight } = this.#bitmap;
const canvas = document.createElement("canvas"); const outputScale = new OutputScale();
let bitmap = this.#bitmap; let bitmap = this.#bitmap;
let width = bitmapWidth, let width = bitmapWidth,
height = bitmapHeight; height = bitmapHeight;
if (bitmapWidth > maxDimension || bitmapHeight > maxDimension) { let canvas = null;
if (maxPreviewDimension) {
if (
bitmapWidth > maxPreviewDimension ||
bitmapHeight > maxPreviewDimension
) {
const ratio = Math.min( const ratio = Math.min(
maxDimension / bitmapWidth, maxPreviewDimension / bitmapWidth,
maxDimension / bitmapHeight maxPreviewDimension / bitmapHeight
); );
width = Math.floor(bitmapWidth * ratio); width = Math.floor(bitmapWidth * ratio);
height = Math.floor(bitmapHeight * 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) { if (!this.#isSvg) {
bitmap = this.#scaleBitmap(width, height); bitmap = this.#scaleBitmap(scaledWidth, scaledHeight);
}
} }
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
ctx.filter = this._uiManager.hcmFilter; ctx.filter = this._uiManager.hcmFilter;
@ -496,18 +504,57 @@ class StampEditor extends AnnotationEditor {
black = "#42414d"; black = "#42414d";
} }
const boxDim = 15; const boxDim = 15;
const pattern = new OffscreenCanvas(boxDim * 2, boxDim * 2); const boxDimWidth = boxDim * outputScale.sx;
const boxDimHeight = boxDim * outputScale.sy;
const pattern = new OffscreenCanvas(boxDimWidth * 2, boxDimHeight * 2);
const patternCtx = pattern.getContext("2d"); const patternCtx = pattern.getContext("2d");
patternCtx.fillStyle = white; patternCtx.fillStyle = white;
patternCtx.fillRect(0, 0, boxDim * 2, boxDim * 2); patternCtx.fillRect(0, 0, boxDimWidth * 2, boxDimHeight * 2);
patternCtx.fillStyle = black; patternCtx.fillStyle = black;
patternCtx.fillRect(0, 0, boxDim, boxDim); patternCtx.fillRect(0, 0, boxDimWidth, boxDimHeight);
patternCtx.fillRect(boxDim, boxDim, boxDim, boxDim); patternCtx.fillRect(boxDimWidth, boxDimHeight, boxDimWidth, boxDimHeight);
ctx.fillStyle = ctx.createPattern(pattern, "repeat"); ctx.fillStyle = ctx.createPattern(pattern, "repeat");
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, scaledWidth, scaledHeight);
ctx.drawImage(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
0,
0,
scaledWidth,
scaledHeight
);
}
let imageData = null;
if (createImageData) { 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", { const offscreenCtx = offscreen.getContext("2d", {
willReadFrequently: true, willReadFrequently: true,
}); });
@ -519,27 +566,17 @@ class StampEditor extends AnnotationEditor {
bitmap.height, bitmap.height,
0, 0,
0, 0,
width, dataWidth,
height dataHeight
); );
const data = offscreenCtx.getImageData(0, 0, width, height).data; imageData = {
ctx.drawImage(offscreen, 0, 0); width: dataWidth,
height: dataHeight,
return { canvas, imageData: { width, height, data } }; data: offscreenCtx.getImageData(0, 0, dataWidth, dataHeight).data,
};
} }
ctx.drawImage( return { canvas, width, height, imageData };
bitmap,
0,
0,
bitmap.width,
bitmap.height,
0,
0,
width,
height
);
return { canvas, imageData: null };
} }
/** /**
@ -619,17 +656,23 @@ class StampEditor extends AnnotationEditor {
} }
#drawBitmap(width, height) { #drawBitmap(width, height) {
width = Math.ceil(width); const outputScale = new OutputScale();
height = Math.ceil(height); const scaledWidth = Math.ceil(width * outputScale.sx);
const scaledHeight = Math.ceil(height * outputScale.sy);
const canvas = this.#canvas; const canvas = this.#canvas;
if (!canvas || (canvas.width === width && canvas.height === height)) { if (
!canvas ||
(canvas.width === scaledWidth && canvas.height === scaledHeight)
) {
return; return;
} }
canvas.width = width; canvas.width = scaledWidth;
canvas.height = height; canvas.height = scaledHeight;
const bitmap = this.#isSvg const bitmap = this.#isSvg
? this.#bitmap ? this.#bitmap
: this.#scaleBitmap(width, height); : this.#scaleBitmap(scaledWidth, scaledHeight);
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
ctx.filter = this._uiManager.hcmFilter; ctx.filter = this._uiManager.hcmFilter;
@ -641,8 +684,8 @@ class StampEditor extends AnnotationEditor {
bitmap.height, bitmap.height,
0, 0,
0, 0,
width, scaledWidth,
height scaledHeight
); );
} }

View File

@ -58,6 +58,7 @@ import {
isDataScheme, isDataScheme,
isPdfFile, isPdfFile,
noContextMenu, noContextMenu,
OutputScale,
PDFDateString, PDFDateString,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
@ -115,6 +116,7 @@ export {
noContextMenu, noContextMenu,
normalizeUnicode, normalizeUnicode,
OPS, OPS,
OutputScale,
PasswordResponses, PasswordResponses,
PDFDataRangeTransport, PDFDataRangeTransport,
PDFDateString, PDFDateString,

View File

@ -50,6 +50,7 @@ import {
isDataScheme, isDataScheme,
isPdfFile, isPdfFile,
noContextMenu, noContextMenu,
OutputScale,
PDFDateString, PDFDateString,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
@ -93,6 +94,7 @@ const expectedAPI = Object.freeze({
noContextMenu, noContextMenu,
normalizeUnicode, normalizeUnicode,
OPS, OPS,
OutputScale,
PasswordResponses, PasswordResponses,
PDFDataRangeTransport, PDFDataRangeTransport,
PDFDateString, PDFDateString,

View File

@ -371,14 +371,21 @@ class NewAltTextManager {
// TODO: get this value from Firefox // TODO: get this value from Firefox
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1908184) // (https://bugzilla.mozilla.org/show_bug.cgi?id=1908184)
const AI_MAX_IMAGE_DIMENSION = 224; 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 // The max dimension of the preview in the dialog is 180px, so we keep 224px
// and rescale it thanks to css. // and rescale it thanks to css.
let canvas; let canvas, width, height;
if (mlManager) { if (mlManager) {
({ canvas, imageData: this.#imageData } = editor.copyCanvas( ({
canvas,
width,
height,
imageData: this.#imageData,
} = editor.copyCanvas(
AI_MAX_IMAGE_DIMENSION, AI_MAX_IMAGE_DIMENSION,
MAX_PREVIEW_DIMENSION,
/* createImageData = */ true /* createImageData = */ true
)); ));
if (hasAI) { if (hasAI) {
@ -388,13 +395,17 @@ class NewAltTextManager {
); );
} }
} else { } else {
({ canvas } = editor.copyCanvas( ({ canvas, width, height } = editor.copyCanvas(
AI_MAX_IMAGE_DIMENSION, AI_MAX_IMAGE_DIMENSION,
MAX_PREVIEW_DIMENSION,
/* createImageData = */ false /* createImageData = */ false
)); ));
} }
canvas.setAttribute("role", "presentation"); canvas.setAttribute("role", "presentation");
const { style } = canvas;
style.width = `${width}px`;
style.height = `${height}px`;
this.#imagePreview.append(canvas); this.#imagePreview.append(canvas);
this.#toggleNotNow(); this.#toggleNotNow();

View File

@ -26,6 +26,7 @@
import { import {
AbortException, AbortException,
AnnotationMode, AnnotationMode,
OutputScale,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
setLayerDimensions, setLayerDimensions,
@ -36,7 +37,6 @@ import {
calcRound, calcRound,
DEFAULT_SCALE, DEFAULT_SCALE,
floorToDivide, floorToDivide,
OutputScale,
RenderingStates, RenderingStates,
TextLayerMode, TextLayerMode,
} from "./ui_utils.js"; } from "./ui_utils.js";

View File

@ -23,8 +23,8 @@
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */ /** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
import { OutputScale, RenderingStates } from "./ui_utils.js"; import { OutputScale, RenderingCancelledException } from "pdfjs-lib";
import { RenderingCancelledException } from "pdfjs-lib"; import { RenderingStates } from "./ui_utils.js";
const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below. const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below.
const MAX_NUM_SCALING_STEPS = 3; const MAX_NUM_SCALING_STEPS = 3;

View File

@ -42,6 +42,7 @@ const {
noContextMenu, noContextMenu,
normalizeUnicode, normalizeUnicode,
OPS, OPS,
OutputScale,
PasswordResponses, PasswordResponses,
PDFDataRangeTransport, PDFDataRangeTransport,
PDFDateString, PDFDateString,
@ -88,6 +89,7 @@ export {
noContextMenu, noContextMenu,
normalizeUnicode, normalizeUnicode,
OPS, OPS,
OutputScale,
PasswordResponses, PasswordResponses,
PDFDataRangeTransport, PDFDataRangeTransport,
PDFDateString, PDFDateString,

View File

@ -76,32 +76,6 @@ const CursorTool = {
// Used by `PDFViewerApplication`, and by the API unit-tests. // Used by `PDFViewerApplication`, and by the API unit-tests.
const AutoPrintRegExp = /\bprint\s*\(/; 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. * Scrolls specified element into view of its parent.
* @param {HTMLElement} element - The element to be visible. * @param {HTMLElement} element - The element to be visible.
@ -908,7 +882,6 @@ export {
MIN_SCALE, MIN_SCALE,
normalizeWheelEventDelta, normalizeWheelEventDelta,
normalizeWheelEventDirection, normalizeWheelEventDirection,
OutputScale,
parseQueryString, parseQueryString,
PresentationModeState, PresentationModeState,
ProgressBar, ProgressBar,