Add an abstract WasmImage class, that JBig2CCITTFaxImage and JpxImage inherit from

Given that these classes are, with the exception of their `decode` methods, virtually identical this helps reduce code duplication and simplifies maintenance.

These changes reduce the size of the `gulp mozcentral` build-target by `1292` bytes, which obviously isn't a lot but still cannot hurt.
This commit is contained in:
Jonas Jenwald 2026-05-05 16:45:45 +02:00
parent e8d3d19f67
commit 6ff0f8690f
8 changed files with 161 additions and 188 deletions

View File

@ -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"),

View File

@ -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 };

View File

@ -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 };

View File

@ -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"),

View File

@ -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)) {

View File

@ -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;

View File

@ -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() {

135
src/core/wasm_image.js Normal file
View File

@ -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 };