Use a BMP decoder when resizing an image

The image decoding won't block the main thread any more.
For now, it isn't enabled for Chrome because issue6741.pdf leads to a crash.
This commit is contained in:
Calixte Denizet 2024-10-18 17:23:48 +02:00
parent 07a1d30fad
commit b649b6f8dd
3 changed files with 84 additions and 7 deletions

View File

@ -83,6 +83,7 @@ const DefaultPartialEvaluatorOptions = Object.freeze({
ignoreErrors: false, ignoreErrors: false,
isEvalSupported: true, isEvalSupported: true,
isOffscreenCanvasSupported: false, isOffscreenCanvasSupported: false,
isChrome: false,
canvasMaxAreaInBytes: -1, canvasMaxAreaInBytes: -1,
fontExtraProperties: false, fontExtraProperties: false,
useSystemFonts: true, useSystemFonts: true,
@ -232,7 +233,14 @@ class PartialEvaluator {
this._regionalImageCache = new RegionalImageCache(); this._regionalImageCache = new RegionalImageCache();
this._fetchBuiltInCMapBound = this.fetchBuiltInCMap.bind(this); this._fetchBuiltInCMapBound = this.fetchBuiltInCMap.bind(this);
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
ImageResizer.setMaxArea(this.options.canvasMaxAreaInBytes); ImageResizer.setMaxArea(this.options.canvasMaxAreaInBytes);
} else {
ImageResizer.setOptions({
isChrome: this.options.isChrome,
maxArea: this.options.canvasMaxAreaInBytes,
});
}
} }
/** /**

View File

@ -13,7 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { FeatureTest, ImageKind, shadow } from "../shared/util.js"; import { FeatureTest, ImageKind, shadow, warn } from "../shared/util.js";
const MIN_IMAGE_DIM = 2048; const MIN_IMAGE_DIM = 2048;
@ -34,11 +34,28 @@ const MAX_ERROR = 128;
class ImageResizer { class ImageResizer {
static #goodSquareLength = MIN_IMAGE_DIM; static #goodSquareLength = MIN_IMAGE_DIM;
static #isChrome = false;
constructor(imgData, isMask) { constructor(imgData, isMask) {
this._imgData = imgData; this._imgData = imgData;
this._isMask = isMask; this._isMask = isMask;
} }
static get canUseImageDecoder() {
// TODO: remove the isChrome, once Chrome doesn't crash anymore with
// issue6741.pdf.
// https://issues.chromium.org/issues/374807001.
return shadow(
this,
"canUseImageDecoder",
// eslint-disable-next-line no-undef
this.#isChrome || typeof ImageDecoder === "undefined"
? Promise.resolve(false)
: // eslint-disable-next-line no-undef
ImageDecoder.isTypeSupported("image/bmp")
);
}
static needsToBeResized(width, height) { static needsToBeResized(width, height) {
if (width <= this.#goodSquareLength && height <= this.#goodSquareLength) { if (width <= this.#goodSquareLength && height <= this.#goodSquareLength) {
return false; return false;
@ -113,6 +130,14 @@ class ImageResizer {
} }
} }
static setOptions(opts) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
throw new Error("Not implemented: setOptions");
}
this.setMaxArea(opts.maxArea ?? -1);
this.#isChrome = opts.isChrome ?? false;
}
static _areGoodDims(width, height) { static _areGoodDims(width, height) {
try { try {
// This code is working in either Firefox or Chrome. // This code is working in either Firefox or Chrome.
@ -157,10 +182,38 @@ class ImageResizer {
async _createImage() { async _createImage() {
const data = this._encodeBMP(); const data = this._encodeBMP();
const blob = new Blob([data.buffer], { let decoder, imagePromise;
if (await ImageResizer.canUseImageDecoder) {
// eslint-disable-next-line no-undef
decoder = new ImageDecoder({
data,
type: "image/bmp", type: "image/bmp",
preferAnimation: false,
transfer: [data.buffer],
}); });
const bitmapPromise = createImageBitmap(blob); imagePromise = decoder
.decode()
.catch(reason => {
warn(`BMP image decoding failed: ${reason}`);
// It's a bit unfortunate to create the BMP twice but we shouldn't be
// here in the first place.
return createImageBitmap(
new Blob([this._encodeBMP().buffer], {
type: "image/bmp",
})
);
})
.finally(() => {
decoder.close();
});
} else {
imagePromise = createImageBitmap(
new Blob([data.buffer], {
type: "image/bmp",
})
);
}
const { MAX_AREA, MAX_DIM } = ImageResizer; const { MAX_AREA, MAX_DIM } = ImageResizer;
const { _imgData: imgData } = this; const { _imgData: imgData } = this;
@ -185,7 +238,8 @@ class ImageResizer {
let newWidth = width; let newWidth = width;
let newHeight = height; let newHeight = height;
let bitmap = await bitmapPromise; const result = await imagePromise;
let bitmap = result.image || result;
for (const step of steps) { for (const step of steps) {
const prevWidth = newWidth; const prevWidth = newWidth;
@ -210,6 +264,9 @@ class ImageResizer {
newWidth, newWidth,
newHeight newHeight
); );
// Release the resources associated with the bitmap.
bitmap.close();
bitmap = canvas.transferToImageBitmap(); bitmap = canvas.transferToImageBitmap();
} }

View File

@ -21,6 +21,7 @@ import {
AbortException, AbortException,
AnnotationMode, AnnotationMode,
assert, assert,
FeatureTest,
getVerbosityLevel, getVerbosityLevel,
info, info,
InvalidPDFException, InvalidPDFException,
@ -177,6 +178,9 @@ const DefaultStandardFontDataFactory =
* `OffscreenCanvas` in the worker. Primarily used to improve performance of * `OffscreenCanvas` in the worker. Primarily used to improve performance of
* image conversion/rendering. * image conversion/rendering.
* The default value is `true` in web environments and `false` in Node.js. * The default value is `true` in web environments and `false` in Node.js.
* @property {boolean} [isChrome] - Determines if we can use bmp ImageDecoder.
* NOTE: Temporary option until [https://issues.chromium.org/issues/374807001]
* is fixed.
* @property {number} [canvasMaxAreaInBytes] - The integer value is used to * @property {number} [canvasMaxAreaInBytes] - The integer value is used to
* know when an image must be resized (uses `OffscreenCanvas` in the worker). * know when an image must be resized (uses `OffscreenCanvas` in the worker).
* If it's -1 then a possibly slow algorithm is used to guess the max value. * If it's -1 then a possibly slow algorithm is used to guess the max value.
@ -281,6 +285,13 @@ function getDocument(src = {}) {
typeof src.isOffscreenCanvasSupported === "boolean" typeof src.isOffscreenCanvasSupported === "boolean"
? src.isOffscreenCanvasSupported ? src.isOffscreenCanvasSupported
: !isNodeJS; : !isNodeJS;
const isChrome =
typeof src.isChrome === "boolean"
? src.isChrome
: (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
!FeatureTest.platform.isFirefox &&
typeof window !== "undefined" &&
!!window?.chrome;
const canvasMaxAreaInBytes = Number.isInteger(src.canvasMaxAreaInBytes) const canvasMaxAreaInBytes = Number.isInteger(src.canvasMaxAreaInBytes)
? src.canvasMaxAreaInBytes ? src.canvasMaxAreaInBytes
: -1; : -1;
@ -385,6 +396,7 @@ function getDocument(src = {}) {
ignoreErrors, ignoreErrors,
isEvalSupported, isEvalSupported,
isOffscreenCanvasSupported, isOffscreenCanvasSupported,
isChrome,
canvasMaxAreaInBytes, canvasMaxAreaInBytes,
fontExtraProperties, fontExtraProperties,
useSystemFonts, useSystemFonts,