1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-20 15:18:08 +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

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