Skip to content

Commit

Permalink
Use a BMP decoder when resizing an image
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
calixteman committed Oct 28, 2024
1 parent 5418060 commit 0361493
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 7 deletions.
10 changes: 9 additions & 1 deletion src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const DefaultPartialEvaluatorOptions = Object.freeze({
ignoreErrors: false,
isEvalSupported: true,
isOffscreenCanvasSupported: false,
isChrome: false,
canvasMaxAreaInBytes: -1,
fontExtraProperties: false,
useSystemFonts: true,
Expand Down Expand Up @@ -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,
});
}
}

/**
Expand Down
69 changes: 63 additions & 6 deletions src/core/image_resizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -210,6 +264,9 @@ class ImageResizer {
newWidth,
newHeight
);

// Release the resources associated with the bitmap.
bitmap.close();
bitmap = canvas.transferToImageBitmap();
}

Expand Down
12 changes: 12 additions & 0 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
AbortException,
AnnotationMode,
assert,
FeatureTest,
getVerbosityLevel,
info,
InvalidPDFException,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -385,6 +396,7 @@ function getDocument(src = {}) {
ignoreErrors,
isEvalSupported,
isOffscreenCanvasSupported,
isChrome,
canvasMaxAreaInBytes,
fontExtraProperties,
useSystemFonts,
Expand Down

0 comments on commit 0361493

Please sign in to comment.