From 971be48b6057c72622e75eb75479c09ab8c1794f Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 26 Feb 2025 23:12:55 +0100 Subject: [PATCH] Support using ICC profiles in using qcms (bug 860023) --- .prettierignore | 2 + eslint.config.mjs | 3 +- external/qcms/LICENSE_PDFJS_QCMS | 22 ++ external/qcms/LICENSE_QCMS | 21 ++ external/qcms/README.md | 12 ++ external/qcms/qcms.js | 255 ++++++++++++++++++++++ external/qcms/qcms_bg.wasm | Bin 0 -> 108938 bytes external/qcms/qcms_utils.js | 43 ++++ gulpfile.mjs | 7 +- src/core/annotation.js | 8 +- src/core/catalog.js | 4 +- src/core/colorspace.js | 310 ++------------------------- src/core/colorspace_utils.js | 357 +++++++++++++++++++++++++++++++ src/core/default_appearance.js | 48 ++++- src/core/evaluator.js | 51 ++--- src/core/icc_colorspace.js | 155 ++++++++++++++ src/core/image.js | 3 +- src/core/pattern.js | 6 +- src/core/pdf_manager.js | 6 +- test/pdfs/issue2856.pdf.link | 1 + test/test_manifest.json | 8 + test/unit/colorspace_spec.js | 39 ++-- 22 files changed, 999 insertions(+), 362 deletions(-) create mode 100644 external/qcms/LICENSE_PDFJS_QCMS create mode 100644 external/qcms/LICENSE_QCMS create mode 100644 external/qcms/README.md create mode 100644 external/qcms/qcms.js create mode 100644 external/qcms/qcms_bg.wasm create mode 100644 external/qcms/qcms_utils.js create mode 100644 src/core/colorspace_utils.js create mode 100644 src/core/icc_colorspace.js create mode 100644 test/pdfs/issue2856.pdf.link 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 0000000000000000000000000000000000000000..f38648f4a14f8af992d66bd13ebe0c96d7afabe1 GIT binary patch literal 108938 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 literal 0 HcmV?d00001 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,