From 0361493c370e61bc9dbac7dfde164e21ab88891b Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 18 Oct 2024 17:23:48 +0200 Subject: [PATCH] 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. --- src/core/evaluator.js | 10 +++++- src/core/image_resizer.js | 69 +++++++++++++++++++++++++++++++++++---- src/display/api.js | 12 +++++++ 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 27ba453fd441c9..d6d70bbdbc2d72 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -83,6 +83,7 @@ const DefaultPartialEvaluatorOptions = Object.freeze({ ignoreErrors: false, isEvalSupported: true, isOffscreenCanvasSupported: false, + isChrome: false, canvasMaxAreaInBytes: -1, fontExtraProperties: false, useSystemFonts: true, @@ -232,7 +233,14 @@ class PartialEvaluator { this._regionalImageCache = new RegionalImageCache(); this._fetchBuiltInCMapBound = this.fetchBuiltInCMap.bind(this); - ImageResizer.setMaxArea(this.options.canvasMaxAreaInBytes); + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + ImageResizer.setMaxArea(this.options.canvasMaxAreaInBytes); + } else { + ImageResizer.setOptions({ + isChrome: this.options.isChrome, + maxArea: this.options.canvasMaxAreaInBytes, + }); + } } /** diff --git a/src/core/image_resizer.js b/src/core/image_resizer.js index 946b03a1cc1c66..8652a828b60b2f 100644 --- a/src/core/image_resizer.js +++ b/src/core/image_resizer.js @@ -13,7 +13,7 @@ * 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; @@ -34,11 +34,28 @@ const MAX_ERROR = 128; class ImageResizer { static #goodSquareLength = MIN_IMAGE_DIM; + static #isChrome = false; + constructor(imgData, isMask) { this._imgData = imgData; 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) { if (width <= this.#goodSquareLength && height <= this.#goodSquareLength) { 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) { try { // This code is working in either Firefox or Chrome. @@ -157,10 +182,38 @@ class ImageResizer { async _createImage() { const data = this._encodeBMP(); - const blob = new Blob([data.buffer], { - type: "image/bmp", - }); - const bitmapPromise = createImageBitmap(blob); + let decoder, imagePromise; + + if (await ImageResizer.canUseImageDecoder) { + // eslint-disable-next-line no-undef + decoder = new ImageDecoder({ + data, + type: "image/bmp", + preferAnimation: false, + transfer: [data.buffer], + }); + 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 { _imgData: imgData } = this; @@ -185,7 +238,8 @@ class ImageResizer { let newWidth = width; let newHeight = height; - let bitmap = await bitmapPromise; + const result = await imagePromise; + let bitmap = result.image || result; for (const step of steps) { const prevWidth = newWidth; @@ -210,6 +264,9 @@ class ImageResizer { newWidth, newHeight ); + + // Release the resources associated with the bitmap. + bitmap.close(); bitmap = canvas.transferToImageBitmap(); } diff --git a/src/display/api.js b/src/display/api.js index 2488837622238f..6c0b9d2a8fd5cd 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -21,6 +21,7 @@ import { AbortException, AnnotationMode, assert, + FeatureTest, getVerbosityLevel, info, InvalidPDFException, @@ -177,6 +178,9 @@ const DefaultStandardFontDataFactory = * `OffscreenCanvas` in the worker. Primarily used to improve performance of * image conversion/rendering. * 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 * 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. @@ -281,6 +285,13 @@ function getDocument(src = {}) { typeof src.isOffscreenCanvasSupported === "boolean" ? src.isOffscreenCanvasSupported : !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) ? src.canvasMaxAreaInBytes : -1; @@ -385,6 +396,7 @@ function getDocument(src = {}) { ignoreErrors, isEvalSupported, isOffscreenCanvasSupported, + isChrome, canvasMaxAreaInBytes, fontExtraProperties, useSystemFonts,