1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-11 19:58:15 +02:00

Support using ICC profiles in using qcms (bug 860023)

This commit is contained in:
Calixte Denizet 2025-02-26 23:12:55 +01:00
parent 4693b7ad2f
commit 971be48b60
22 changed files with 999 additions and 362 deletions

View file

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

View file

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

22
external/qcms/LICENSE_PDFJS_QCMS vendored Normal file
View file

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

21
external/qcms/LICENSE_QCMS vendored Normal file
View file

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

12
external/qcms/README.md vendored Normal file
View file

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

255
external/qcms/qcms.js vendored Normal file
View file

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

BIN
external/qcms/qcms_bg.wasm vendored Normal file

Binary file not shown.

43
external/qcms/qcms_utils.js vendored Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

155
src/core/icc_colorspace.js Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
https://github.com/user-attachments/files/19016079/version4pdf.pdf

View file

@ -11946,5 +11946,13 @@
}
}
}
},
{
"id": "issue2856",
"file": "pdfs/issue2856.pdf",
"md5": "a13033722b99d7b24a1383ac793d2b51",
"rounds": 1,
"link": true,
"type": "eq"
}
]

View file

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