Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/display/display_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1143,6 +1173,7 @@ export {
isPdfFile,
isValidFetchUrl,
noContextMenu,
OutputScale,
PageViewport,
PDFDateString,
PixelsPerInch,
Expand Down
177 changes: 110 additions & 67 deletions src/display/editor/stamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
});
Expand All @@ -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 };
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -641,8 +684,8 @@ class StampEditor extends AnnotationEditor {
bitmap.height,
0,
0,
width,
height
scaledWidth,
scaledHeight
);
}

Expand Down
2 changes: 2 additions & 0 deletions src/pdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
isDataScheme,
isPdfFile,
noContextMenu,
OutputScale,
PDFDateString,
PixelsPerInch,
RenderingCancelledException,
Expand Down Expand Up @@ -115,6 +116,7 @@ export {
noContextMenu,
normalizeUnicode,
OPS,
OutputScale,
PasswordResponses,
PDFDataRangeTransport,
PDFDateString,
Expand Down
2 changes: 2 additions & 0 deletions test/unit/pdf_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
isDataScheme,
isPdfFile,
noContextMenu,
OutputScale,
PDFDateString,
PixelsPerInch,
RenderingCancelledException,
Expand Down Expand Up @@ -93,6 +94,7 @@ const expectedAPI = Object.freeze({
noContextMenu,
normalizeUnicode,
OPS,
OutputScale,
PasswordResponses,
PDFDataRangeTransport,
PDFDateString,
Expand Down
17 changes: 14 additions & 3 deletions web/new_alt_text_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion web/pdf_page_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import {
AbortException,
AnnotationMode,
OutputScale,
PixelsPerInch,
RenderingCancelledException,
setLayerDimensions,
Expand All @@ -36,7 +37,6 @@ import {
calcRound,
DEFAULT_SCALE,
floorToDivide,
OutputScale,
RenderingStates,
TextLayerMode,
} from "./ui_utils.js";
Expand Down
4 changes: 2 additions & 2 deletions web/pdf_thumbnail_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading