diff --git a/.prettierignore b/.prettierignore index 90371a116..dded2e6c7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,8 @@ node_modules/ external/bcmaps/ external/builder/fixtures/ external/builder/fixtures_babel/ +external/openjpeg/ +external/qcms/ external/quickjs/ test/stats/results/ test/tmp/ diff --git a/eslint.config.mjs b/eslint.config.mjs index 954b23a05..bb55f7581 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -33,8 +33,9 @@ export default [ "external/bcmaps/", "external/builder/fixtures/", "external/builder/fixtures_babel/", - "external/quickjs/", "external/openjpeg/", + "external/qcms/", + "external/quickjs/", "test/stats/results/", "test/tmp/", "test/pdfs/", diff --git a/external/qcms/LICENSE_PDFJS_QCMS b/external/qcms/LICENSE_PDFJS_QCMS new file mode 100644 index 000000000..7e1aeb34f --- /dev/null +++ b/external/qcms/LICENSE_PDFJS_QCMS @@ -0,0 +1,22 @@ +Copyright (c) 2025, Mozilla Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/external/qcms/LICENSE_QCMS b/external/qcms/LICENSE_QCMS new file mode 100644 index 000000000..eec8246df --- /dev/null +++ b/external/qcms/LICENSE_QCMS @@ -0,0 +1,21 @@ +qcms +Copyright (C) 2009-2024 Mozilla Corporation +Copyright (C) 1998-2007 Marti Maria + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/external/qcms/README.md b/external/qcms/README.md new file mode 100644 index 000000000..eb4c5ac44 --- /dev/null +++ b/external/qcms/README.md @@ -0,0 +1,12 @@ +## Build + +In order to generate the files `qcms.js` and `qcms_bg.wasm`: +* git clone https://github.com/mozilla/pdf.js.qcms/ +* the build requires to have a [Docker](https://www.docker.com/) setup and then: + * `node build.js -C` to build the Docker image + * `node build.js -co /pdf.js/external/qcms/` to compile the decoder + +## Licensing + +[qcms](https://github.com/FirefoxGraphics/qcms) is under [MIT](https://github.com/FirefoxGraphics/qcms/blob/main/COPYING) +and [pdf.js.qcms](https://github.com/mozilla/pdf.js.qcms/) is released under [MIT](https://github.com/mozilla/pdf.js.qcms/blob/main/LICENSE) license so `qcms.js` and `qcms_bg.wasm` are released under [MIT](https://github.com/mozilla/pdf.js.qcms/blob/main/LICENSE) license too. diff --git a/external/qcms/qcms.js b/external/qcms/qcms.js new file mode 100644 index 000000000..4f38c248f --- /dev/null +++ b/external/qcms/qcms.js @@ -0,0 +1,255 @@ +/* THIS FILE IS GENERATED - DO NOT EDIT */ +import { copy_result, copy_rgb } from './qcms_utils.js'; + +let wasm; + +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +let WASM_VECTOR_LEN = 0; + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} +/** + * # Safety + * + * This function is called directly from JavaScript. + * @param {number} transformer + * @param {Uint8Array} src + */ +export function qcms_convert_array(transformer, src) { + const ptr0 = passArray8ToWasm0(src, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + wasm.qcms_convert_array(transformer, ptr0, len0); +} + +/** + * # Safety + * + * This function is called directly from JavaScript. + * @param {number} transformer + * @param {number} src + */ +export function qcms_convert_one(transformer, src) { + wasm.qcms_convert_one(transformer, src); +} + +/** + * # Safety + * + * This function is called directly from JavaScript. + * @param {number} transformer + * @param {number} src1 + * @param {number} src2 + * @param {number} src3 + */ +export function qcms_convert_three(transformer, src1, src2, src3) { + wasm.qcms_convert_three(transformer, src1, src2, src3); +} + +/** + * # Safety + * + * This function is called directly from JavaScript. + * @param {number} transformer + * @param {number} src1 + * @param {number} src2 + * @param {number} src3 + * @param {number} src4 + */ +export function qcms_convert_four(transformer, src1, src2, src3, src4) { + wasm.qcms_convert_four(transformer, src1, src2, src3, src4); +} + +/** + * # Safety + * + * This function is called directly from JavaScript. + * @param {Uint8Array} mem + * @param {DataType} in_type + * @param {Intent} intent + * @returns {number} + */ +export function qcms_transformer_from_memory(mem, in_type, intent) { + const ptr0 = passArray8ToWasm0(mem, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.qcms_transformer_from_memory(ptr0, len0, in_type, intent); + return ret >>> 0; +} + +/** + * # Safety + * + * This function is called directly from JavaScript. + * @param {number} transformer + */ +export function qcms_drop_transformer(transformer) { + wasm.qcms_drop_transformer(transformer); +} + +/** + * @enum {0 | 1 | 2 | 3 | 4 | 5} + */ +export const DataType = Object.freeze({ + RGB8: 0, "0": "RGB8", + RGBA8: 1, "1": "RGBA8", + BGRA8: 2, "2": "BGRA8", + Gray8: 3, "3": "Gray8", + GrayA8: 4, "4": "GrayA8", + CMYK: 5, "5": "CMYK", +}); +/** + * @enum {0 | 1 | 2 | 3} + */ +export const Intent = Object.freeze({ + Perceptual: 0, "0": "Perceptual", + RelativeColorimetric: 1, "1": "RelativeColorimetric", + Saturation: 2, "2": "Saturation", + AbsoluteColorimetric: 3, "3": "AbsoluteColorimetric", +}); + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_copyresult_b08ee7d273f295dd = function(arg0, arg1) { + copy_result(arg0 >>> 0, arg1 >>> 0); + }; + imports.wbg.__wbg_copyrgb_d60ce17bb05d9b67 = function(arg0) { + copy_rgb(arg0 >>> 0); + }; + imports.wbg.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_export_0; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + ; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + + return imports; +} + +function __wbg_init_memory(imports, memory) { + +} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedUint8ArrayMemory0 = null; + + + wasm.__wbindgen_start(); + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('qcms_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + __wbg_init_memory(imports); + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync }; +export default __wbg_init; diff --git a/external/qcms/qcms_bg.wasm b/external/qcms/qcms_bg.wasm new file mode 100644 index 000000000..f38648f4a Binary files /dev/null and b/external/qcms/qcms_bg.wasm differ diff --git a/external/qcms/qcms_utils.js b/external/qcms/qcms_utils.js new file mode 100644 index 000000000..8a07d0461 --- /dev/null +++ b/external/qcms/qcms_utils.js @@ -0,0 +1,43 @@ +/* Copyright 2025 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. + */ + +class QCMS { + static _module = null; + + static _destBuffer = null; +} + +function copy_result(ptr, len) { + // This function is called from the wasm module (it's an external + // "C" function). Its goal is to copy the result from the wasm memory + // to the destination buffer without any intermediate copies. + const { _module, _destBuffer } = QCMS; + const result = new Uint8Array(_module.memory.buffer, ptr, len); + if (result.length === _destBuffer.length) { + _destBuffer.set(result); + return; + } + for (let i = 0, j = 0, ii = result.length; i < ii; i += 3, j += 4) { + _destBuffer[j] = result[i]; + _destBuffer[j + 1] = result[i + 1]; + _destBuffer[j + 2] = result[i + 2]; + } +} + +function copy_rgb(ptr) { + QCMS._destBuffer.set(new Uint8Array(QCMS._module.memory.buffer, ptr, 3)); +} + +export { copy_result, copy_rgb, QCMS }; diff --git a/gulpfile.mjs b/gulpfile.mjs index e1aae9244..f30919b17 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -663,6 +663,10 @@ function createWasmBundle() { encoding: false, } ), + gulp.src(["external/qcms/*.wasm", "external/qcms/LICENSE_*"], { + base: "external/qcms", + encoding: false, + }), ]); } @@ -1659,6 +1663,7 @@ function buildLib(defines, dir) { }), gulp.src("test/unit/*.js", { base: ".", encoding: false }), gulp.src("external/openjpeg/*.js", { base: "openjpeg/", encoding: false }), + gulp.src("external/qcms/*.js", { base: "qcms/", encoding: false }), ]); return buildLibHelper(bundleDefines, inputStream, dir); @@ -2140,7 +2145,7 @@ gulp.task( }, function watchWasm() { gulp.watch( - "external/openjpeg/*", + ["external/openjpeg/*", "external/qcms/*"], { ignoreInitial: false }, gulp.series("dev-wasm") ); diff --git a/src/core/annotation.js b/src/core/annotation.js index 9ef394a31..2da4b8198 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -64,7 +64,7 @@ import { Stream, StringStream } from "./stream.js"; import { BaseStream } from "./base_stream.js"; import { bidi } from "./bidi.js"; import { Catalog } from "./catalog.js"; -import { ColorSpace } from "./colorspace.js"; +import { ColorSpaceUtils } from "./colorspace_utils.js"; import { FileSpec } from "./file_spec.js"; import { JpegStream } from "./jpeg_stream.js"; import { ObjectLoader } from "./object_loader.js"; @@ -552,15 +552,15 @@ function getRgbColor(color, defaultColor = new Uint8ClampedArray(3)) { return null; case 1: // Convert grayscale to RGB - ColorSpace.singletons.gray.getRgbItem(color, 0, rgbColor, 0); + ColorSpaceUtils.singletons.gray.getRgbItem(color, 0, rgbColor, 0); return rgbColor; case 3: // Convert RGB percentages to RGB - ColorSpace.singletons.rgb.getRgbItem(color, 0, rgbColor, 0); + ColorSpaceUtils.singletons.rgb.getRgbItem(color, 0, rgbColor, 0); return rgbColor; case 4: // Convert CMYK to RGB - ColorSpace.singletons.cmyk.getRgbItem(color, 0, rgbColor, 0); + ColorSpaceUtils.singletons.cmyk.getRgbItem(color, 0, rgbColor, 0); return rgbColor; default: diff --git a/src/core/catalog.js b/src/core/catalog.js index 5e800cb89..32c65a8d6 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -49,7 +49,7 @@ import { GlobalColorSpaceCache, GlobalImageCache } from "./image_utils.js"; import { NameTree, NumberTree } from "./name_number_tree.js"; import { BaseStream } from "./base_stream.js"; import { clearGlobalCaches } from "./cleanup_helper.js"; -import { ColorSpace } from "./colorspace.js"; +import { ColorSpaceUtils } from "./colorspace_utils.js"; import { FileSpec } from "./file_spec.js"; import { MetadataParser } from "./metadata_parser.js"; import { StructTreeRoot } from "./struct_tree.js"; @@ -357,7 +357,7 @@ class Catalog { isNumberArray(color, 3) && (color[0] !== 0 || color[1] !== 0 || color[2] !== 0) ) { - rgbColor = ColorSpace.singletons.rgb.getRgb(color, 0); + rgbColor = ColorSpaceUtils.singletons.rgb.getRgb(color, 0); } const outlineItem = { diff --git a/src/core/colorspace.js b/src/core/colorspace.js index 496dcdd00..1f33f628c 100644 --- a/src/core/colorspace.js +++ b/src/core/colorspace.js @@ -22,9 +22,7 @@ import { unreachable, warn, } from "../shared/util.js"; -import { Dict, Name, Ref } from "./primitives.js"; import { BaseStream } from "./base_stream.js"; -import { MissingDataException } from "./core_utils.js"; /** * Resizes an RGB image with 3 components. @@ -306,283 +304,6 @@ class ColorSpace { return shadow(this, "usesZeroToOneRange", true); } - static #cache( - cacheKey, - parsedCS, - { xref, globalColorSpaceCache, localColorSpaceCache } - ) { - if (!globalColorSpaceCache || !localColorSpaceCache) { - throw new Error( - 'ColorSpace.#cache - expected "globalColorSpaceCache"/"localColorSpaceCache" argument.' - ); - } - if (!parsedCS) { - throw new Error('ColorSpace.#cache - expected "parsedCS" argument.'); - } - let csName, csRef; - if (cacheKey instanceof Ref) { - csRef = cacheKey; - - // If parsing succeeded, we know that this call cannot throw. - cacheKey = xref.fetch(cacheKey); - } - if (cacheKey instanceof Name) { - csName = cacheKey.name; - } - if (csName || csRef) { - localColorSpaceCache.set(csName, csRef, parsedCS); - - if (csRef) { - globalColorSpaceCache.set(/* name = */ null, csRef, parsedCS); - } - } - } - - static getCached( - cacheKey, - xref, - globalColorSpaceCache, - localColorSpaceCache - ) { - if (!globalColorSpaceCache || !localColorSpaceCache) { - throw new Error( - 'ColorSpace.getCached - expected "globalColorSpaceCache"/"localColorSpaceCache" argument.' - ); - } - if (cacheKey instanceof Ref) { - const cachedCS = - globalColorSpaceCache.getByRef(cacheKey) || - localColorSpaceCache.getByRef(cacheKey); - if (cachedCS) { - return cachedCS; - } - - try { - cacheKey = xref.fetch(cacheKey); - } catch (ex) { - if (ex instanceof MissingDataException) { - throw ex; - } - // Any errors should be handled during parsing, rather than here. - } - } - if (cacheKey instanceof Name) { - return localColorSpaceCache.getByName(cacheKey.name) || null; - } - return null; - } - - static async parseAsync({ - cs, - xref, - resources = null, - pdfFunctionFactory, - globalColorSpaceCache, - localColorSpaceCache, - }) { - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { - assert( - !this.getCached(cs, xref, globalColorSpaceCache, localColorSpaceCache), - "Expected `ColorSpace.getCached` to have been manually checked " + - "before calling `ColorSpace.parseAsync`." - ); - } - - const options = { - xref, - resources, - pdfFunctionFactory, - globalColorSpaceCache, - localColorSpaceCache, - }; - const parsedCS = this.#parse(cs, options); - - // Attempt to cache the parsed ColorSpace, by name and/or reference. - this.#cache(cs, parsedCS, options); - - return parsedCS; - } - - static parse({ - cs, - xref, - resources = null, - pdfFunctionFactory, - globalColorSpaceCache, - localColorSpaceCache, - }) { - const cachedCS = this.getCached( - cs, - xref, - globalColorSpaceCache, - localColorSpaceCache - ); - if (cachedCS) { - return cachedCS; - } - - const options = { - xref, - resources, - pdfFunctionFactory, - globalColorSpaceCache, - localColorSpaceCache, - }; - const parsedCS = this.#parse(cs, options); - - // Attempt to cache the parsed ColorSpace, by name and/or reference. - this.#cache(cs, parsedCS, options); - - return parsedCS; - } - - /** - * NOTE: This method should *only* be invoked from `this.#parse`, - * when parsing "sub" ColorSpaces. - */ - static #subParse(cs, options) { - const { globalColorSpaceCache } = options; - - let csRef; - if (cs instanceof Ref) { - const cachedCS = globalColorSpaceCache.getByRef(cs); - if (cachedCS) { - return cachedCS; - } - csRef = cs; - } - const parsedCS = this.#parse(cs, options); - - // Only cache the parsed ColorSpace globally, by reference. - if (csRef) { - globalColorSpaceCache.set(/* name = */ null, csRef, parsedCS); - } - return parsedCS; - } - - static #parse(cs, options) { - const { xref, resources, pdfFunctionFactory } = options; - - cs = xref.fetchIfRef(cs); - if (cs instanceof Name) { - switch (cs.name) { - case "G": - case "DeviceGray": - return this.singletons.gray; - case "RGB": - case "DeviceRGB": - return this.singletons.rgb; - case "DeviceRGBA": - return this.singletons.rgba; - case "CMYK": - case "DeviceCMYK": - return this.singletons.cmyk; - case "Pattern": - return new PatternCS(/* baseCS = */ null); - default: - if (resources instanceof Dict) { - const colorSpaces = resources.get("ColorSpace"); - if (colorSpaces instanceof Dict) { - const resourcesCS = colorSpaces.get(cs.name); - if (resourcesCS) { - if (resourcesCS instanceof Name) { - return this.#parse(resourcesCS, options); - } - cs = resourcesCS; - break; - } - } - } - // Fallback to the default gray color space. - warn(`Unrecognized ColorSpace: ${cs.name}`); - return this.singletons.gray; - } - } - if (Array.isArray(cs)) { - const mode = xref.fetchIfRef(cs[0]).name; - let params, numComps, baseCS, whitePoint, blackPoint, gamma; - - switch (mode) { - case "G": - case "DeviceGray": - return this.singletons.gray; - case "RGB": - case "DeviceRGB": - return this.singletons.rgb; - case "CMYK": - case "DeviceCMYK": - return this.singletons.cmyk; - case "CalGray": - params = xref.fetchIfRef(cs[1]); - whitePoint = params.getArray("WhitePoint"); - blackPoint = params.getArray("BlackPoint"); - gamma = params.get("Gamma"); - return new CalGrayCS(whitePoint, blackPoint, gamma); - case "CalRGB": - params = xref.fetchIfRef(cs[1]); - whitePoint = params.getArray("WhitePoint"); - blackPoint = params.getArray("BlackPoint"); - gamma = params.getArray("Gamma"); - const matrix = params.getArray("Matrix"); - return new CalRGBCS(whitePoint, blackPoint, gamma, matrix); - case "ICCBased": - const stream = xref.fetchIfRef(cs[1]); - const dict = stream.dict; - numComps = dict.get("N"); - const altRaw = dict.getRaw("Alternate"); - if (altRaw) { - const altCS = this.#subParse(altRaw, options); - // Ensure that the number of components are correct, - // and also (indirectly) that it is not a PatternCS. - if (altCS.numComps === numComps) { - return altCS; - } - warn("ICCBased color space: Ignoring incorrect /Alternate entry."); - } - if (numComps === 1) { - return this.singletons.gray; - } else if (numComps === 3) { - return this.singletons.rgb; - } else if (numComps === 4) { - return this.singletons.cmyk; - } - break; - case "Pattern": - baseCS = cs[1] || null; - if (baseCS) { - baseCS = this.#subParse(baseCS, options); - } - return new PatternCS(baseCS); - case "I": - case "Indexed": - baseCS = this.#subParse(cs[1], options); - const hiVal = Math.max(0, Math.min(xref.fetchIfRef(cs[2]), 255)); - const lookup = xref.fetchIfRef(cs[3]); - return new IndexedCS(baseCS, hiVal, lookup); - case "Separation": - case "DeviceN": - const name = xref.fetchIfRef(cs[1]); - numComps = Array.isArray(name) ? name.length : 1; - baseCS = this.#subParse(cs[2], options); - const tintFn = pdfFunctionFactory.create(cs[3]); - return new AlternateCS(numComps, baseCS, tintFn); - case "Lab": - params = xref.fetchIfRef(cs[1]); - whitePoint = params.getArray("WhitePoint"); - blackPoint = params.getArray("BlackPoint"); - const range = params.getArray("Range"); - return new LabCS(whitePoint, blackPoint, range); - default: - // Fallback to the default gray color space. - warn(`Unimplemented ColorSpace object: ${mode}`); - return this.singletons.gray; - } - } - // Fallback to the default gray color space. - warn(`Unrecognized ColorSpace object: ${cs}`); - return this.singletons.gray; - } - /** * Checks if a decode map matches the default decode map for a color space. * This handles the general decode maps where there are two values per @@ -607,23 +328,6 @@ class ColorSpace { } return true; } - - static get singletons() { - return shadow(this, "singletons", { - get gray() { - return shadow(this, "gray", new DeviceGrayCS()); - }, - get rgb() { - return shadow(this, "rgb", new DeviceRgbCS()); - }, - get rgba() { - return shadow(this, "rgba", new DeviceRgbaCS()); - }, - get cmyk() { - return shadow(this, "cmyk", new DeviceCmykCS()); - }, - }); - } } /** @@ -1583,4 +1287,16 @@ class LabCS extends ColorSpace { } } -export { ColorSpace }; +export { + AlternateCS, + CalGrayCS, + CalRGBCS, + ColorSpace, + DeviceCmykCS, + DeviceGrayCS, + DeviceRgbaCS, + DeviceRgbCS, + IndexedCS, + LabCS, + PatternCS, +}; diff --git a/src/core/colorspace_utils.js b/src/core/colorspace_utils.js new file mode 100644 index 000000000..b65df8f79 --- /dev/null +++ b/src/core/colorspace_utils.js @@ -0,0 +1,357 @@ +/* Copyright 2024 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 { + AlternateCS, + CalGrayCS, + CalRGBCS, + DeviceCmykCS, + DeviceGrayCS, + DeviceRgbaCS, + DeviceRgbCS, + IndexedCS, + LabCS, + PatternCS, +} from "./colorspace.js"; +import { assert, shadow, warn } from "../shared/util.js"; +import { Dict, Name, Ref } from "./primitives.js"; +import { IccColorSpace } from "./icc_colorspace.js"; +import { MissingDataException } from "./core_utils.js"; + +class ColorSpaceUtils { + /** + * @private + */ + static #cache( + cacheKey, + parsedCS, + { xref, globalColorSpaceCache, localColorSpaceCache } + ) { + if (!globalColorSpaceCache || !localColorSpaceCache) { + throw new Error( + 'ColorSpace.#cache - expected "globalColorSpaceCache"/"localColorSpaceCache" argument.' + ); + } + if (!parsedCS) { + throw new Error('ColorSpace.#cache - expected "parsedCS" argument.'); + } + let csName, csRef; + if (cacheKey instanceof Ref) { + csRef = cacheKey; + + // If parsing succeeded, we know that this call cannot throw. + cacheKey = xref.fetch(cacheKey); + } + if (cacheKey instanceof Name) { + csName = cacheKey.name; + } + if (csName || csRef) { + localColorSpaceCache.set(csName, csRef, parsedCS); + + if (csRef) { + globalColorSpaceCache.set(/* name = */ null, csRef, parsedCS); + } + } + } + + static getCached( + cacheKey, + xref, + globalColorSpaceCache, + localColorSpaceCache + ) { + if (!globalColorSpaceCache || !localColorSpaceCache) { + throw new Error( + 'ColorSpace.getCached - expected "globalColorSpaceCache"/"localColorSpaceCache" argument.' + ); + } + if (cacheKey instanceof Ref) { + const cachedCS = + globalColorSpaceCache.getByRef(cacheKey) || + localColorSpaceCache.getByRef(cacheKey); + if (cachedCS) { + return cachedCS; + } + + try { + cacheKey = xref.fetch(cacheKey); + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + // Any errors should be handled during parsing, rather than here. + } + } + if (cacheKey instanceof Name) { + return localColorSpaceCache.getByName(cacheKey.name) || null; + } + return null; + } + + static async parseAsync({ + cs, + xref, + resources = null, + pdfFunctionFactory, + globalColorSpaceCache, + localColorSpaceCache, + }) { + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + assert( + !this.getCached(cs, xref, globalColorSpaceCache, localColorSpaceCache), + "Expected `ColorSpace.getCached` to have been manually checked " + + "before calling `ColorSpace.parseAsync`." + ); + } + + const options = { + xref, + resources, + pdfFunctionFactory, + globalColorSpaceCache, + localColorSpaceCache, + }; + const parsedCS = this.#parse(cs, options); + + // Attempt to cache the parsed ColorSpace, by name and/or reference. + this.#cache(cs, parsedCS, options); + + return parsedCS; + } + + static parse({ + cs, + xref, + resources = null, + pdfFunctionFactory, + globalColorSpaceCache, + localColorSpaceCache, + }) { + const cachedCS = this.getCached( + cs, + xref, + globalColorSpaceCache, + localColorSpaceCache + ); + if (cachedCS) { + return cachedCS; + } + + const options = { + xref, + resources, + pdfFunctionFactory, + globalColorSpaceCache, + localColorSpaceCache, + }; + const parsedCS = this.#parse(cs, options); + + // Attempt to cache the parsed ColorSpace, by name and/or reference. + this.#cache(cs, parsedCS, options); + + return parsedCS; + } + + /** + * NOTE: This method should *only* be invoked from `this.#parse`, + * when parsing "sub" ColorSpaces. + */ + static #subParse(cs, options) { + const { globalColorSpaceCache } = options; + + let csRef; + if (cs instanceof Ref) { + const cachedCS = globalColorSpaceCache.getByRef(cs); + if (cachedCS) { + return cachedCS; + } + csRef = cs; + } + const parsedCS = this.#parse(cs, options); + + // Only cache the parsed ColorSpace globally, by reference. + if (csRef) { + globalColorSpaceCache.set(/* name = */ null, csRef, parsedCS); + } + return parsedCS; + } + + static #parse(cs, options) { + const { xref, resources, pdfFunctionFactory } = options; + + cs = xref.fetchIfRef(cs); + if (cs instanceof Name) { + switch (cs.name) { + case "G": + case "DeviceGray": + return this.singletons.gray; + case "RGB": + case "DeviceRGB": + return this.singletons.rgb; + case "DeviceRGBA": + return this.singletons.rgba; + case "CMYK": + case "DeviceCMYK": + return this.singletons.cmyk; + case "Pattern": + return new PatternCS(/* baseCS = */ null); + default: + if (resources instanceof Dict) { + const colorSpaces = resources.get("ColorSpace"); + if (colorSpaces instanceof Dict) { + const resourcesCS = colorSpaces.get(cs.name); + if (resourcesCS) { + if (resourcesCS instanceof Name) { + return this.#parse(resourcesCS, options); + } + cs = resourcesCS; + break; + } + } + } + // Fallback to the default gray color space. + warn(`Unrecognized ColorSpace: ${cs.name}`); + return this.singletons.gray; + } + } + if (Array.isArray(cs)) { + const mode = xref.fetchIfRef(cs[0]).name; + let params, numComps, baseCS, whitePoint, blackPoint, gamma; + + switch (mode) { + case "G": + case "DeviceGray": + return this.singletons.gray; + case "RGB": + case "DeviceRGB": + return this.singletons.rgb; + case "CMYK": + case "DeviceCMYK": + return this.singletons.cmyk; + case "CalGray": + params = xref.fetchIfRef(cs[1]); + whitePoint = params.getArray("WhitePoint"); + blackPoint = params.getArray("BlackPoint"); + gamma = params.get("Gamma"); + return new CalGrayCS(whitePoint, blackPoint, gamma); + case "CalRGB": + params = xref.fetchIfRef(cs[1]); + whitePoint = params.getArray("WhitePoint"); + blackPoint = params.getArray("BlackPoint"); + gamma = params.getArray("Gamma"); + const matrix = params.getArray("Matrix"); + return new CalRGBCS(whitePoint, blackPoint, gamma, matrix); + case "ICCBased": + const { globalColorSpaceCache } = options; + const isRef = cs[1] instanceof Ref; + if (isRef) { + const cachedCS = globalColorSpaceCache.getByRef(cs[1]); + if (cachedCS) { + return cachedCS; + } + } + + const stream = xref.fetchIfRef(cs[1]); + const dict = stream.dict; + numComps = dict.get("N"); + + if (IccColorSpace.isUsable) { + try { + const iccCS = new IccColorSpace(stream.getBytes(), numComps); + if (isRef) { + globalColorSpaceCache.set(/* name = */ null, cs[1], iccCS); + } + return iccCS; + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + warn(`ICCBased color space (${cs[1]}): "${ex}".`); + } + } + + const altRaw = dict.getRaw("Alternate"); + if (altRaw) { + const altCS = this.#subParse(altRaw, options); + // Ensure that the number of components are correct, + // and also (indirectly) that it is not a PatternCS. + if (altCS.numComps === numComps) { + return altCS; + } + warn("ICCBased color space: Ignoring incorrect /Alternate entry."); + } + if (numComps === 1) { + return this.singletons.gray; + } else if (numComps === 3) { + return this.singletons.rgb; + } else if (numComps === 4) { + return this.singletons.cmyk; + } + break; + case "Pattern": + baseCS = cs[1] || null; + if (baseCS) { + baseCS = this.#subParse(baseCS, options); + } + return new PatternCS(baseCS); + case "I": + case "Indexed": + baseCS = this.#subParse(cs[1], options); + const hiVal = Math.max(0, Math.min(xref.fetchIfRef(cs[2]), 255)); + const lookup = xref.fetchIfRef(cs[3]); + return new IndexedCS(baseCS, hiVal, lookup); + case "Separation": + case "DeviceN": + const name = xref.fetchIfRef(cs[1]); + numComps = Array.isArray(name) ? name.length : 1; + baseCS = this.#subParse(cs[2], options); + const tintFn = pdfFunctionFactory.create(cs[3]); + return new AlternateCS(numComps, baseCS, tintFn); + case "Lab": + params = xref.fetchIfRef(cs[1]); + whitePoint = params.getArray("WhitePoint"); + blackPoint = params.getArray("BlackPoint"); + const range = params.getArray("Range"); + return new LabCS(whitePoint, blackPoint, range); + default: + // Fallback to the default gray color space. + warn(`Unimplemented ColorSpace object: ${mode}`); + return this.singletons.gray; + } + } + // Fallback to the default gray color space. + warn(`Unrecognized ColorSpace object: ${cs}`); + return this.singletons.gray; + } + + static get singletons() { + return shadow(this, "singletons", { + get gray() { + return shadow(this, "gray", new DeviceGrayCS()); + }, + get rgb() { + return shadow(this, "rgb", new DeviceRgbCS()); + }, + get rgba() { + return shadow(this, "rgba", new DeviceRgbaCS()); + }, + get cmyk() { + return shadow(this, "cmyk", new DeviceCmykCS()); + }, + }); + } +} + +export { ColorSpaceUtils }; diff --git a/src/core/default_appearance.js b/src/core/default_appearance.js index 51d179daf..ff2c6a0ba 100644 --- a/src/core/default_appearance.js +++ b/src/core/default_appearance.js @@ -28,7 +28,7 @@ import { shadow, warn, } from "../shared/util.js"; -import { ColorSpace } from "./colorspace.js"; +import { ColorSpaceUtils } from "./colorspace_utils.js"; import { EvaluatorPreprocessor } from "./evaluator.js"; import { LocalColorSpaceCache } from "./image_utils.js"; import { PDFFunctionFactory } from "./function.js"; @@ -73,13 +73,28 @@ class DefaultAppearanceEvaluator extends EvaluatorPreprocessor { } break; case OPS.setFillRGBColor: - ColorSpace.singletons.rgb.getRgbItem(args, 0, result.fontColor, 0); + ColorSpaceUtils.singletons.rgb.getRgbItem( + args, + 0, + result.fontColor, + 0 + ); break; case OPS.setFillGray: - ColorSpace.singletons.gray.getRgbItem(args, 0, result.fontColor, 0); + ColorSpaceUtils.singletons.gray.getRgbItem( + args, + 0, + result.fontColor, + 0 + ); break; case OPS.setFillCMYKColor: - ColorSpace.singletons.cmyk.getRgbItem(args, 0, result.fontColor, 0); + ColorSpaceUtils.singletons.cmyk.getRgbItem( + args, + 0, + result.fontColor, + 0 + ); break; } } @@ -117,7 +132,7 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { fontSize: 0, fontName: "", fontColor: /* black = */ new Uint8ClampedArray(3), - fillColorSpace: ColorSpace.singletons.gray, + fillColorSpace: ColorSpaceUtils.singletons.gray, }; let breakLoop = false; const stack = []; @@ -157,7 +172,7 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { } break; case OPS.setFillColorSpace: - result.fillColorSpace = ColorSpace.parse({ + result.fillColorSpace = ColorSpaceUtils.parse({ cs: args[0], xref: this.xref, resources: this.resources, @@ -171,13 +186,28 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { cs.getRgbItem(args, 0, result.fontColor, 0); break; case OPS.setFillRGBColor: - ColorSpace.singletons.rgb.getRgbItem(args, 0, result.fontColor, 0); + ColorSpaceUtils.singletons.rgb.getRgbItem( + args, + 0, + result.fontColor, + 0 + ); break; case OPS.setFillGray: - ColorSpace.singletons.gray.getRgbItem(args, 0, result.fontColor, 0); + ColorSpaceUtils.singletons.gray.getRgbItem( + args, + 0, + result.fontColor, + 0 + ); break; case OPS.setFillCMYKColor: - ColorSpace.singletons.cmyk.getRgbItem(args, 0, result.fontColor, 0); + ColorSpaceUtils.singletons.cmyk.getRgbItem( + args, + 0, + result.fontColor, + 0 + ); break; case OPS.showText: case OPS.showSpacedText: diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 91ac7c28d..a41cd0345 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -68,7 +68,7 @@ import { } from "./image_utils.js"; import { BaseStream } from "./base_stream.js"; import { bidi } from "./bidi.js"; -import { ColorSpace } from "./colorspace.js"; +import { ColorSpaceUtils } from "./colorspace_utils.js"; import { DecodeStream } from "./decode_stream.js"; import { FontFlags } from "./fonts_utils.js"; import { getFontSubstitution } from "./font_substitutions.js"; @@ -491,7 +491,7 @@ class PartialEvaluator { if (group.has("CS")) { const cs = group.getRaw("CS"); - const cachedColorSpace = ColorSpace.getCached( + const cachedColorSpace = ColorSpaceUtils.getCached( cs, this.xref, this.globalColorSpaceCache, @@ -510,7 +510,7 @@ class PartialEvaluator { } if (smask?.backdrop) { - colorSpace ||= ColorSpace.singletons.rgb; + colorSpace ||= ColorSpaceUtils.singletons.rgb; smask.backdrop = colorSpace.getRgb(smask.backdrop, 0); } @@ -1463,7 +1463,7 @@ class PartialEvaluator { } parseColorSpace({ cs, resources, localColorSpaceCache }) { - return ColorSpace.parseAsync({ + return ColorSpaceUtils.parseAsync({ cs, xref: this.xref, resources, @@ -1981,7 +1981,7 @@ class PartialEvaluator { break; case OPS.setFillColorSpace: { - const cachedColorSpace = ColorSpace.getCached( + const cachedColorSpace = ColorSpaceUtils.getCached( args[0], xref, self.globalColorSpaceCache, @@ -2001,13 +2001,13 @@ class PartialEvaluator { }) .then(function (colorSpace) { stateManager.state.fillColorSpace = - colorSpace || ColorSpace.singletons.gray; + colorSpace || ColorSpaceUtils.singletons.gray; }) ); return; } case OPS.setStrokeColorSpace: { - const cachedColorSpace = ColorSpace.getCached( + const cachedColorSpace = ColorSpaceUtils.getCached( args[0], xref, self.globalColorSpaceCache, @@ -2027,7 +2027,7 @@ class PartialEvaluator { }) .then(function (colorSpace) { stateManager.state.strokeColorSpace = - colorSpace || ColorSpace.singletons.gray; + colorSpace || ColorSpaceUtils.singletons.gray; }) ); return; @@ -2043,38 +2043,41 @@ class PartialEvaluator { fn = OPS.setStrokeRGBColor; break; case OPS.setFillGray: - stateManager.state.fillColorSpace = ColorSpace.singletons.gray; - args = ColorSpace.singletons.gray.getRgb(args, 0); + stateManager.state.fillColorSpace = ColorSpaceUtils.singletons.gray; + args = ColorSpaceUtils.singletons.gray.getRgb(args, 0); fn = OPS.setFillRGBColor; break; case OPS.setStrokeGray: - stateManager.state.strokeColorSpace = ColorSpace.singletons.gray; - args = ColorSpace.singletons.gray.getRgb(args, 0); + stateManager.state.strokeColorSpace = + ColorSpaceUtils.singletons.gray; + args = ColorSpaceUtils.singletons.gray.getRgb(args, 0); fn = OPS.setStrokeRGBColor; break; case OPS.setFillCMYKColor: - stateManager.state.fillColorSpace = ColorSpace.singletons.cmyk; - args = ColorSpace.singletons.cmyk.getRgb(args, 0); + stateManager.state.fillColorSpace = ColorSpaceUtils.singletons.cmyk; + args = ColorSpaceUtils.singletons.cmyk.getRgb(args, 0); fn = OPS.setFillRGBColor; break; case OPS.setStrokeCMYKColor: - stateManager.state.strokeColorSpace = ColorSpace.singletons.cmyk; - args = ColorSpace.singletons.cmyk.getRgb(args, 0); + stateManager.state.strokeColorSpace = + ColorSpaceUtils.singletons.cmyk; + args = ColorSpaceUtils.singletons.cmyk.getRgb(args, 0); fn = OPS.setStrokeRGBColor; break; case OPS.setFillRGBColor: - stateManager.state.fillColorSpace = ColorSpace.singletons.rgb; - args = ColorSpace.singletons.rgb.getRgb(args, 0); + stateManager.state.fillColorSpace = ColorSpaceUtils.singletons.rgb; + args = ColorSpaceUtils.singletons.rgb.getRgb(args, 0); break; case OPS.setStrokeRGBColor: - stateManager.state.strokeColorSpace = ColorSpace.singletons.rgb; - args = ColorSpace.singletons.rgb.getRgb(args, 0); + stateManager.state.strokeColorSpace = + ColorSpaceUtils.singletons.rgb; + args = ColorSpaceUtils.singletons.rgb.getRgb(args, 0); break; case OPS.setFillColorN: cs = stateManager.state.patternFillColorSpace; if (!cs) { if (isNumberArray(args, null)) { - args = ColorSpace.singletons.gray.getRgb(args, 0); + args = ColorSpaceUtils.singletons.gray.getRgb(args, 0); fn = OPS.setFillRGBColor; break; } @@ -2106,7 +2109,7 @@ class PartialEvaluator { cs = stateManager.state.patternStrokeColorSpace; if (!cs) { if (isNumberArray(args, null)) { - args = ColorSpace.singletons.gray.getRgb(args, 0); + args = ColorSpaceUtils.singletons.gray.getRgb(args, 0); fn = OPS.setStrokeRGBColor; break; } @@ -4897,8 +4900,8 @@ class EvalState { this.ctm = new Float32Array(IDENTITY_MATRIX); this.font = null; this.textRenderingMode = TextRenderingMode.FILL; - this._fillColorSpace = ColorSpace.singletons.gray; - this._strokeColorSpace = ColorSpace.singletons.gray; + this._fillColorSpace = ColorSpaceUtils.singletons.gray; + this._strokeColorSpace = ColorSpaceUtils.singletons.gray; this.patternFillColorSpace = null; this.patternStrokeColorSpace = null; } diff --git a/src/core/icc_colorspace.js b/src/core/icc_colorspace.js new file mode 100644 index 000000000..39833df30 --- /dev/null +++ b/src/core/icc_colorspace.js @@ -0,0 +1,155 @@ +/* Copyright 2025 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 { + DataType, + initSync, + Intent, + qcms_convert_array, + qcms_convert_four, + qcms_convert_one, + qcms_convert_three, + qcms_drop_transformer, + qcms_transformer_from_memory, +} from "../../external/qcms/qcms.js"; +import { shadow, warn } from "../shared/util.js"; +import { ColorSpace } from "./colorspace.js"; +import { QCMS } from "../../external/qcms/qcms_utils.js"; + +class IccColorSpace extends ColorSpace { + #transformer; + + #convertPixel; + + static #useWasm = true; + + static #wasmUrl = null; + + static #finalizer = new FinalizationRegistry(transformer => { + qcms_drop_transformer(transformer); + }); + + constructor(iccProfile, numComps) { + if (!IccColorSpace.isUsable) { + throw new Error("No ICC color space support"); + } + + super("ICCBased", numComps); + + let inType; + switch (numComps) { + case 1: + inType = DataType.Gray8; + this.#convertPixel = (src, srcOffset) => + qcms_convert_one(this.#transformer, src[srcOffset] * 255); + break; + case 3: + inType = DataType.RGB8; + this.#convertPixel = (src, srcOffset) => + qcms_convert_three( + this.#transformer, + src[srcOffset] * 255, + src[srcOffset + 1] * 255, + src[srcOffset + 2] * 255 + ); + break; + case 4: + inType = DataType.CMYK; + this.#convertPixel = (src, srcOffset) => + qcms_convert_four( + this.#transformer, + src[srcOffset] * 255, + src[srcOffset + 1] * 255, + src[srcOffset + 2] * 255, + src[srcOffset + 3] * 255 + ); + break; + default: + throw new Error(`Unsupported number of components: ${numComps}`); + } + this.#transformer = qcms_transformer_from_memory( + iccProfile, + inType, + Intent.Perceptual + ); + if (!this.#transformer) { + throw new Error("Failed to create ICC color space"); + } + IccColorSpace.#finalizer.register(this, this.#transformer); + } + + getRgbItem(src, srcOffset, dest, destOffset) { + QCMS._destBuffer = dest.subarray(destOffset, destOffset + 3); + this.#convertPixel(src, srcOffset); + QCMS._destBuffer = null; + } + + getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) { + src = src.subarray(srcOffset, srcOffset + count * this.numComps); + if (bits !== 8) { + const scale = 255 / ((1 << bits) - 1); + for (let i = 0, ii = src.length; i < ii; i++) { + src[i] *= scale; + } + } + QCMS._destBuffer = dest.subarray( + destOffset, + destOffset + count * (3 + alpha01) + ); + qcms_convert_array(this.#transformer, src); + QCMS._destBuffer = null; + } + + getOutputLength(inputLength, alpha01) { + return ((inputLength / this.numComps) * (3 + alpha01)) | 0; + } + + static setOptions({ useWasm, useWorkerFetch, wasmUrl }) { + if (!useWorkerFetch) { + this.#useWasm = false; + return; + } + this.#useWasm = useWasm; + this.#wasmUrl = wasmUrl; + } + + static get isUsable() { + let isUsable = false; + if (this.#useWasm) { + try { + this._module = QCMS._module = this.#load(); + isUsable = !!this._module; + } catch (e) { + warn(`ICCBased color space: "${e}".`); + } + } + + return shadow(this, "isUsable", isUsable); + } + + static #load() { + // Parsing and using color spaces is still synchronous, + // so we must load the wasm module synchronously. + // TODO: Make the color space stuff asynchronous and use fetch. + const filename = "qcms_bg.wasm"; + const xhr = new XMLHttpRequest(); + xhr.open("GET", `${this.#wasmUrl}${filename}`, false); + xhr.responseType = "arraybuffer"; + xhr.send(null); + return initSync({ module: xhr.response }); + } +} + +export { IccColorSpace }; diff --git a/src/core/image.js b/src/core/image.js index cfdf43ade..e3e99907f 100644 --- a/src/core/image.js +++ b/src/core/image.js @@ -26,6 +26,7 @@ import { } from "../shared/image_utils.js"; import { BaseStream } from "./base_stream.js"; import { ColorSpace } from "./colorspace.js"; +import { ColorSpaceUtils } from "./colorspace_utils.js"; import { DecodeStream } from "./decode_stream.js"; import { ImageResizer } from "./image_resizer.js"; import { JpegStream } from "./jpeg_stream.js"; @@ -210,7 +211,7 @@ class PDFImage { colorSpace = Name.get("DeviceRGBA"); } - this.colorSpace = ColorSpace.parse({ + this.colorSpace = ColorSpaceUtils.parse({ cs: colorSpace, xref, resources: isInline ? res : null, diff --git a/src/core/pattern.js b/src/core/pattern.js index 7257a67f8..033e72383 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -30,7 +30,7 @@ import { MissingDataException, } from "./core_utils.js"; import { BaseStream } from "./base_stream.js"; -import { ColorSpace } from "./colorspace.js"; +import { ColorSpaceUtils } from "./colorspace_utils.js"; const ShadingType = { FUNCTION_BASED: 1, @@ -137,7 +137,7 @@ class RadialAxialShading extends BaseShading { if (!isNumberArray(this.coordsArr, coordsLen)) { throw new FormatError("RadialAxialShading: Invalid /Coords array."); } - const cs = ColorSpace.parse({ + const cs = ColorSpaceUtils.parse({ cs: dict.getRaw("CS") || dict.getRaw("ColorSpace"), xref, resources, @@ -473,7 +473,7 @@ class MeshShading extends BaseShading { const dict = stream.dict; this.shadingType = dict.get("ShadingType"); this.bbox = lookupNormalRect(dict.getArray("BBox"), null); - const cs = ColorSpace.parse({ + const cs = ColorSpaceUtils.parse({ cs: dict.getRaw("CS") || dict.getRaw("ColorSpace"), xref, resources, diff --git a/src/core/pdf_manager.js b/src/core/pdf_manager.js index 02c4e58b4..49e8a754b 100644 --- a/src/core/pdf_manager.js +++ b/src/core/pdf_manager.js @@ -20,6 +20,7 @@ import { warn, } from "../shared/util.js"; import { ChunkedStreamManager } from "./chunked_stream.js"; +import { IccColorSpace } from "./icc_colorspace.js"; import { ImageResizer } from "./image_resizer.js"; import { JpegStream } from "./jpeg_stream.js"; import { JpxImage } from "./jpx.js"; @@ -73,7 +74,10 @@ class BasePdfManager { // Initialize image-options once per document. ImageResizer.setOptions(evaluatorOptions); JpegStream.setOptions(evaluatorOptions); - JpxImage.setOptions({ ...evaluatorOptions, handler }); + + const options = { ...evaluatorOptions, handler }; + JpxImage.setOptions(options); + IccColorSpace.setOptions(options); } get docId() { diff --git a/test/pdfs/issue2856.pdf.link b/test/pdfs/issue2856.pdf.link new file mode 100644 index 000000000..dcd50973d --- /dev/null +++ b/test/pdfs/issue2856.pdf.link @@ -0,0 +1 @@ +https://github.com/user-attachments/files/19016079/version4pdf.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index c42928757..259f8fa04 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -11946,5 +11946,13 @@ } } } + }, + { + "id": "issue2856", + "file": "pdfs/issue2856.pdf", + "md5": "a13033722b99d7b24a1383ac793d2b51", + "rounds": 1, + "link": true, + "type": "eq" } ] diff --git a/test/unit/colorspace_spec.js b/test/unit/colorspace_spec.js index f9da88ccb..79a2ef222 100644 --- a/test/unit/colorspace_spec.js +++ b/test/unit/colorspace_spec.js @@ -20,6 +20,7 @@ import { } from "../../src/core/image_utils.js"; import { Stream, StringStream } from "../../src/core/stream.js"; import { ColorSpace } from "../../src/core/colorspace.js"; +import { ColorSpaceUtils } from "../../src/core/colorspace_utils.js"; import { PDFFunctionFactory } from "../../src/core/function.js"; import { XRefMock } from "./test_utils.js"; @@ -71,7 +72,7 @@ describe("colorspace", function () { xref, }); - const colorSpace1 = ColorSpace.parse({ + const colorSpace1 = ColorSpaceUtils.parse({ cs: Name.get("Pattern"), xref, resources: null, @@ -81,7 +82,7 @@ describe("colorspace", function () { }); expect(colorSpace1.name).toEqual("Pattern"); - const colorSpace2 = ColorSpace.parse({ + const colorSpace2 = ColorSpaceUtils.parse({ cs: Name.get("Pattern"), xref, resources: null, @@ -91,7 +92,7 @@ describe("colorspace", function () { }); expect(colorSpace2.name).toEqual("Pattern"); - const colorSpaceNonCached = ColorSpace.parse({ + const colorSpaceNonCached = ColorSpaceUtils.parse({ cs: Name.get("Pattern"), xref, resources: null, @@ -101,7 +102,7 @@ describe("colorspace", function () { }); expect(colorSpaceNonCached.name).toEqual("Pattern"); - const colorSpaceOther = ColorSpace.parse({ + const colorSpaceOther = ColorSpaceUtils.parse({ cs: Name.get("RGB"), xref, resources: null, @@ -144,7 +145,7 @@ describe("colorspace", function () { xref, }); - const colorSpace1 = ColorSpace.parse({ + const colorSpace1 = ColorSpaceUtils.parse({ cs: Ref.get(50, 0), xref, resources: null, @@ -154,7 +155,7 @@ describe("colorspace", function () { }); expect(colorSpace1.name).toEqual("CalGray"); - const colorSpace2 = ColorSpace.parse({ + const colorSpace2 = ColorSpaceUtils.parse({ cs: Ref.get(50, 0), xref, resources: null, @@ -164,7 +165,7 @@ describe("colorspace", function () { }); expect(colorSpace2.name).toEqual("CalGray"); - const colorSpaceNonCached = ColorSpace.parse({ + const colorSpaceNonCached = ColorSpaceUtils.parse({ cs: Ref.get(50, 0), xref, resources: null, @@ -174,7 +175,7 @@ describe("colorspace", function () { }); expect(colorSpaceNonCached.name).toEqual("CalGray"); - const colorSpaceOther = ColorSpace.parse({ + const colorSpaceOther = ColorSpaceUtils.parse({ cs: Ref.get(100, 0), xref, resources: null, @@ -216,7 +217,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -268,7 +269,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -326,7 +327,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -384,7 +385,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -448,7 +449,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -506,7 +507,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -575,7 +576,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -646,7 +647,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -715,7 +716,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -788,7 +789,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources, @@ -867,7 +868,7 @@ describe("colorspace", function () { const pdfFunctionFactory = new PDFFunctionFactory({ xref, }); - const colorSpace = ColorSpace.parse({ + const colorSpace = ColorSpaceUtils.parse({ cs, xref, resources,