diff --git a/src/core/ccitt_stream.js b/src/core/ccitt_stream.js index 8bdb08b1e..0f50364dc 100644 --- a/src/core/ccitt_stream.js +++ b/src/core/ccitt_stream.js @@ -64,7 +64,7 @@ class CCITTFaxStream extends DecodeStream { : this.bytes; } - this.buffer = await JBig2CCITTFaxImage.decode( + this.buffer = await JBig2CCITTFaxImage.instance.decode( bytes, this.dict.get("W", "Width"), this.dict.get("H", "Height"), diff --git a/src/core/cleanup_helper.js b/src/core/cleanup_helper.js index 7c1bb5f89..8b629fb09 100644 --- a/src/core/cleanup_helper.js +++ b/src/core/cleanup_helper.js @@ -16,18 +16,16 @@ import { clearPatternCaches } from "./pattern.js"; import { clearPrimitiveCaches } from "./primitives.js"; import { clearUnicodeCaches } from "./unicode.js"; -import { JBig2CCITTFaxImage } from "./jbig2_ccittFax.js"; -import { JpxImage } from "./jpx.js"; +import { WasmImage } from "./wasm_image.js"; function clearGlobalCaches() { clearPatternCaches(); clearPrimitiveCaches(); clearUnicodeCaches(); - // Remove the global `JBig2CCITTFaxImage`/`JpxImage` instances, + // Remove the global `WasmImage` instances, // since they may hold references to the WebAssembly modules. - JBig2CCITTFaxImage.cleanup(); - JpxImage.cleanup(); + WasmImage.cleanup(); } export { clearGlobalCaches }; diff --git a/src/core/jbig2_ccittFax.js b/src/core/jbig2_ccittFax.js index 8fbde3d6a..8d3d38d88 100644 --- a/src/core/jbig2_ccittFax.js +++ b/src/core/jbig2_ccittFax.js @@ -13,9 +13,9 @@ * limitations under the License. */ -import { BaseException, warn } from "../shared/util.js"; -import { fetchBinaryData } from "./core_utils.js"; +import { BaseException, shadow } from "../shared/util.js"; import JBig2 from "../../external/jbig2/jbig2.js"; +import { WasmImage } from "./wasm_image.js"; class Jbig2Error extends BaseException { constructor(msg) { @@ -23,92 +23,17 @@ class Jbig2Error extends BaseException { } } -class JBig2CCITTFaxImage { - static #buffer = null; +class JBig2CCITTFaxImage extends WasmImage { + _filename = "jbig2.wasm"; - static #handler = null; + _noWasmFilename = "jbig2_nowasm_fallback.js"; - static #modulePromise = null; - - static #useWasm = true; - - static #useWorkerFetch = true; - - static #wasmUrl = null; - - static setOptions({ handler, useWasm, useWorkerFetch, wasmUrl }) { - this.#useWasm = useWasm; - this.#useWorkerFetch = useWorkerFetch; - this.#wasmUrl = wasmUrl; - - if (!useWorkerFetch) { - this.#handler = handler; - } + static get instance() { + return shadow(this, "instance", new JBig2CCITTFaxImage()); } - static async #getJsModule(fallbackCallback) { - const path = - typeof PDFJSDev === "undefined" - ? `../${this.#wasmUrl}jbig2_nowasm_fallback.js` - : `${this.#wasmUrl}jbig2_nowasm_fallback.js`; - - let instance = null; - try { - const mod = await (typeof PDFJSDev === "undefined" - ? import(path) // eslint-disable-line no-unsanitized/method - : __raw_import__(path)); - instance = mod.default(); - } catch (e) { - warn(`JBig2CCITTFaxImage#getJsModule: ${e}`); - } - fallbackCallback(instance); - } - - static async #instantiateWasm(fallbackCallback, imports, successCallback) { - const filename = "jbig2.wasm"; - try { - if (!this.#buffer) { - if (this.#useWorkerFetch) { - this.#buffer = await fetchBinaryData(`${this.#wasmUrl}${filename}`); - } else { - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - throw new Error("Only worker-thread fetching supported."); - } - this.#buffer = await this.#handler.sendWithPromise( - "FetchBinaryData", - { kind: "wasmUrl", filename } - ); - } - } - const results = await WebAssembly.instantiate(this.#buffer, imports); - return successCallback(results.instance); - } catch (reason) { - warn(`JBig2CCITTFaxImage#instantiateWasm: ${reason}`); - - this.#getJsModule(fallbackCallback); - return null; - } finally { - this.#handler = null; - } - } - - static async decode(bytes, width, height, globals, CCITTOptions) { - if (!this.#modulePromise) { - const { promise, resolve } = Promise.withResolvers(); - const promises = [promise]; - if (!this.#useWasm) { - this.#getJsModule(resolve); - } else { - promises.push( - JBig2({ - warn, - instantiateWasm: this.#instantiateWasm.bind(this, resolve), - }) - ); - } - this.#modulePromise = Promise.race(promises); - } - const module = await this.#modulePromise; + async decode(bytes, width, height, globals, CCITTOptions) { + const module = await this._getModule(JBig2); if (!module) { throw new Jbig2Error("JBig2 failed to initialize"); @@ -157,10 +82,6 @@ class JBig2CCITTFaxImage { } } } - - static cleanup() { - this.#modulePromise = null; - } } export { JBig2CCITTFaxImage, Jbig2Error }; diff --git a/src/core/jbig2_stream.js b/src/core/jbig2_stream.js index 75d94c9fc..e94c1edb1 100644 --- a/src/core/jbig2_stream.js +++ b/src/core/jbig2_stream.js @@ -64,7 +64,7 @@ class Jbig2Stream extends DecodeStream { globals = globalsStream.getBytes(); } } - this.buffer = await JBig2CCITTFaxImage.decode( + this.buffer = await JBig2CCITTFaxImage.instance.decode( bytes, this.dict.get("Width"), this.dict.get("Height"), diff --git a/src/core/jpx.js b/src/core/jpx.js index 6421643ca..2d4c092f9 100644 --- a/src/core/jpx.js +++ b/src/core/jpx.js @@ -13,10 +13,10 @@ * limitations under the License. */ -import { BaseException, warn } from "../shared/util.js"; -import { fetchBinaryData } from "./core_utils.js"; +import { BaseException, shadow } from "../shared/util.js"; import OpenJPEG from "../../external/openjpeg/openjpeg.js"; import { Stream } from "./stream.js"; +import { WasmImage } from "./wasm_image.js"; class JpxError extends BaseException { constructor(msg) { @@ -24,76 +24,16 @@ class JpxError extends BaseException { } } -class JpxImage { - static #buffer = null; +class JpxImage extends WasmImage { + _filename = "openjpeg.wasm"; - static #handler = null; + _noWasmFilename = "openjpeg_nowasm_fallback.js"; - static #modulePromise = null; - - static #useWasm = true; - - static #useWorkerFetch = true; - - static #wasmUrl = null; - - static setOptions({ handler, useWasm, useWorkerFetch, wasmUrl }) { - this.#useWasm = useWasm; - this.#useWorkerFetch = useWorkerFetch; - this.#wasmUrl = wasmUrl; - - if (!useWorkerFetch) { - this.#handler = handler; - } + static get instance() { + return shadow(this, "instance", new JpxImage()); } - static async #getJsModule(fallbackCallback) { - const path = - typeof PDFJSDev === "undefined" - ? `../${this.#wasmUrl}openjpeg_nowasm_fallback.js` - : `${this.#wasmUrl}openjpeg_nowasm_fallback.js`; - - let instance = null; - try { - const mod = await (typeof PDFJSDev === "undefined" - ? import(path) // eslint-disable-line no-unsanitized/method - : __raw_import__(path)); - instance = mod.default(); - } catch (e) { - warn(`JpxImage#getJsModule: ${e}`); - } - fallbackCallback(instance); - } - - static async #instantiateWasm(fallbackCallback, imports, successCallback) { - const filename = "openjpeg.wasm"; - try { - if (!this.#buffer) { - if (this.#useWorkerFetch) { - this.#buffer = await fetchBinaryData(`${this.#wasmUrl}${filename}`); - } else { - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - throw new Error("Only worker-thread fetching supported."); - } - this.#buffer = await this.#handler.sendWithPromise( - "FetchBinaryData", - { kind: "wasmUrl", filename } - ); - } - } - const results = await WebAssembly.instantiate(this.#buffer, imports); - return successCallback(results.instance); - } catch (reason) { - warn(`JpxImage#instantiateWasm: ${reason}`); - - this.#getJsModule(fallbackCallback); - return null; - } finally { - this.#handler = null; - } - } - - static async decode( + async decode( bytes, { numComponents = 4, @@ -102,22 +42,7 @@ class JpxImage { reducePower = 0, } = {} ) { - if (!this.#modulePromise) { - const { promise, resolve } = Promise.withResolvers(); - const promises = [promise]; - if (!this.#useWasm) { - this.#getJsModule(resolve); - } else { - promises.push( - OpenJPEG({ - warn, - instantiateWasm: this.#instantiateWasm.bind(this, resolve), - }) - ); - } - this.#modulePromise = Promise.race(promises); - } - const module = await this.#modulePromise; + const module = await this._getModule(OpenJPEG); if (!module) { throw new JpxError("OpenJPEG failed to initialize"); @@ -155,10 +80,6 @@ class JpxImage { } } - static cleanup() { - this.#modulePromise = null; - } - static parseImageProperties(stream) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("IMAGE_DECODERS")) { if (stream instanceof ArrayBuffer || ArrayBuffer.isView(stream)) { diff --git a/src/core/jpx_stream.js b/src/core/jpx_stream.js index f38c2bb58..f8e311a79 100644 --- a/src/core/jpx_stream.js +++ b/src/core/jpx_stream.js @@ -49,7 +49,7 @@ class JpxStream extends DecodeStream { return this.buffer; } bytes ||= this.bytes; - this.buffer = await JpxImage.decode(bytes, decoderOptions); + this.buffer = await JpxImage.instance.decode(bytes, decoderOptions); this.bufferLength = this.buffer.length; this.eof = true; diff --git a/src/core/pdf_manager.js b/src/core/pdf_manager.js index 6b1511493..9496e724d 100644 --- a/src/core/pdf_manager.js +++ b/src/core/pdf_manager.js @@ -22,15 +22,14 @@ import { } from "../shared/util.js"; import { ChunkedStreamManager } from "./chunked_stream.js"; import { ImageResizer } from "./image_resizer.js"; -import { JBig2CCITTFaxImage } from "./jbig2_ccittFax.js"; import { JpegStream } from "./jpeg_stream.js"; -import { JpxImage } from "./jpx.js"; import { MissingDataException } from "./core_utils.js"; import { OperatorList } from "./operator_list.js"; import { Pattern } from "./pattern.js"; import { PDFDocument } from "./document.js"; import { PDFFunctionFactory } from "./function.js"; import { Stream } from "./stream.js"; +import { WasmImage } from "./wasm_image.js"; function parseDocBaseUrl(url) { if (url) { @@ -82,12 +81,11 @@ class BasePdfManager { OperatorList.setOptions(evaluatorOptions); const options = { ...evaluatorOptions, handler }; - JpxImage.setOptions(options); IccColorSpace.setOptions(options); CmykICCBasedCS.setOptions(options); - JBig2CCITTFaxImage.setOptions(options); PDFFunctionFactory.setOptions(options); Pattern.setOptions(options); + WasmImage.setOptions(options); } get docId() { diff --git a/src/core/wasm_image.js b/src/core/wasm_image.js new file mode 100644 index 000000000..14ace07aa --- /dev/null +++ b/src/core/wasm_image.js @@ -0,0 +1,135 @@ +/* Copyright 2026 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { unreachable, warn } from "../shared/util.js"; +import { fetchBinaryData } from "./core_utils.js"; + +class WasmImage { + static #handler = null; + + static #instances = new Set(); + + static #useWasm = true; + + static #useWorkerFetch = true; + + static #wasmUrl = null; + + _buffer = null; + + _filename = ""; + + _noWasmFilename = ""; + + _modulePromise = null; + + static setOptions({ handler, useWasm, useWorkerFetch, wasmUrl }) { + WasmImage.#useWasm = useWasm; + WasmImage.#useWorkerFetch = useWorkerFetch; + WasmImage.#wasmUrl = wasmUrl; + + if (!useWorkerFetch) { + WasmImage.#handler = handler; + } + } + + // eslint-disable-next-line getter-return + static get instance() { + unreachable("Abstract getter `instance` accessed"); + } + + static cleanup() { + for (const instance of WasmImage.#instances) { + instance._modulePromise = null; + } + } + + constructor() { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === WasmImage + ) { + unreachable("Cannot initialize WasmImage."); + } + // Keep track of the instances for `cleanup` purposes. + WasmImage.#instances.add(this); + } + + async #getJsModule(fallbackCallback) { + let instance = null; + try { + const mod = await (typeof PDFJSDev === "undefined" + ? // eslint-disable-next-line no-unsanitized/method + import(`../${WasmImage.#wasmUrl}${this._noWasmFilename}`) + : __raw_import__(`${WasmImage.#wasmUrl}${this._noWasmFilename}`)); + instance = mod.default(); + } catch (ex) { + warn(`#getJsModule: ${ex}`); + } + fallbackCallback(instance); + } + + async #instantiateWasm(fallbackCallback, imports, successCallback) { + try { + if (!this._buffer) { + if (WasmImage.#useWorkerFetch) { + this._buffer = await fetchBinaryData( + `${WasmImage.#wasmUrl}${this._filename}` + ); + } else { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + throw new Error("Only worker-thread fetching supported."); + } + this._buffer = await WasmImage.#handler.sendWithPromise( + "FetchBinaryData", + { kind: "wasmUrl", filename: this._filename } + ); + } + } + const results = await WebAssembly.instantiate(this._buffer, imports); + return successCallback(results.instance); + } catch (ex) { + warn(`#instantiateWasm: ${ex}`); + + this.#getJsModule(fallbackCallback); + return null; + } + } + + _getModule(ImageDecoder) { + if (!this._modulePromise) { + const { promise, resolve } = Promise.withResolvers(); + const promises = [promise]; + if (!WasmImage.#useWasm) { + this.#getJsModule(resolve); + } else { + promises.push( + ImageDecoder({ + warn, + instantiateWasm: this.#instantiateWasm.bind(this, resolve), + }) + ); + } + this._modulePromise = Promise.race(promises); + } + return this._modulePromise; + } + + async decode(bytes, _params) { + unreachable("Abstract method `decode` called"); + } +} + +export { WasmImage };