diff --git a/src/core/colorspace_utils.js b/src/core/colorspace_utils.js index 04fa44dd837dc..39bb5b8cb5bde 100644 --- a/src/core/colorspace_utils.js +++ b/src/core/colorspace_utils.js @@ -79,7 +79,7 @@ class ColorSpaceUtils { } try { - parsedCS = this.#parse(cs, options); + parsedCS = this.#parse(cs, options, /* topLevel = */ true); } catch (ex) { if (asyncIfNotCached && !(ex instanceof MissingDataException)) { return Promise.reject(ex); @@ -124,39 +124,92 @@ class ColorSpaceUtils { return parsedCS; } - static #parse(cs, options) { - const { xref, resources, pdfFunctionFactory, globalColorSpaceCache } = - options; + /** + * NOTE: This method should *only* be invoked from `this.#parse`, + * when parsing "default" ColorSpaces (i.e. /DefaultGray, /DefaultRGB, + * and /DefaultCMYK). + */ + static #defaultParse(cs, options, deviceCS) { + const { globalColorSpaceCache } = options; + let csRef, parsedCS; + + // Check if the ColorSpace is cached first, to avoid re-parsing it. + if (cs instanceof Ref) { + csRef = cs; + + const cachedCS = globalColorSpaceCache.getByRef(csRef); + if (cachedCS) { + return cachedCS; + } + } + try { + parsedCS = this.#parse(cs, options); + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + warn(`Cannot parse default ColorSpace: "${ex}".`); + return deviceCS; + } + + // The default ColorSpace must be compatible with the original one. + if (parsedCS.numComps !== deviceCS.numComps) { + warn( + "Incorrect number of components in default ColorSpace, " + + `expected "${deviceCS.numComps}" and got "${parsedCS.numComps}".` + ); + return deviceCS; + } + + // Only cache the parsed ColorSpace globally, by reference. + if (csRef) { + globalColorSpaceCache.set(/* name = */ null, csRef, parsedCS); + } + return parsedCS; + } + + static #parse(cs, options, topLevel = false) { + const { xref, pdfFunctionFactory, globalColorSpaceCache } = options; cs = xref.fetchIfRef(cs); if (cs instanceof Name) { switch (cs.name) { case "G": - case "DeviceGray": + case "DeviceGray": { + const defaultCS = topLevel && this.#getResCS("DefaultGray", options); + if (defaultCS) { + return this.#defaultParse(defaultCS, options, this.gray); + } return this.gray; + } case "RGB": - case "DeviceRGB": + case "DeviceRGB": { + const defaultCS = topLevel && this.#getResCS("DefaultRGB", options); + if (defaultCS) { + return this.#defaultParse(defaultCS, options, this.rgb); + } return this.rgb; + } case "DeviceRGBA": return this.rgba; case "CMYK": - case "DeviceCMYK": + case "DeviceCMYK": { + const defaultCS = topLevel && this.#getResCS("DefaultCMYK", options); + if (defaultCS) { + return this.#subParse(defaultCS, options, this.cmyk); + } return this.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; - } + const resourcesCS = xref.fetchIfRef(this.#getResCS(cs.name, options)); + 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}`); @@ -276,6 +329,16 @@ class ColorSpaceUtils { return this.gray; } + static #getResCS(name, { resources }) { + if (resources instanceof Dict) { + const colorSpaces = resources.get("ColorSpace"); + if (colorSpaces instanceof Dict) { + return colorSpaces.getRaw(name) ?? null; + } + } + return null; + } + static get gray() { return shadow(this, "gray", new DeviceGrayCS()); } diff --git a/src/core/default_appearance.js b/src/core/default_appearance.js index b05ac5065647d..30a74231e2530 100644 --- a/src/core/default_appearance.js +++ b/src/core/default_appearance.js @@ -119,7 +119,8 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { fontColor: /* black = */ new Uint8ClampedArray(3), fillColorSpace: ColorSpaceUtils.gray, }; - let breakLoop = false; + let breakLoop = false, + cs = null; const stack = []; try { @@ -157,27 +158,29 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { } break; case OPS.setFillColorSpace: - result.fillColorSpace = ColorSpaceUtils.parse({ - cs: args[0], - xref: this.xref, - resources: this.resources, - pdfFunctionFactory: this._pdfFunctionFactory, - globalColorSpaceCache: this.globalColorSpaceCache, - localColorSpaceCache: this._localColorSpaceCache, - }); + result.fillColorSpace = this.#parseColorSpace(args[0]); break; case OPS.setFillColor: - const cs = result.fillColorSpace; + cs = result.fillColorSpace; cs.getRgbItem(args, 0, result.fontColor, 0); break; case OPS.setFillRGBColor: - ColorSpaceUtils.rgb.getRgbItem(args, 0, result.fontColor, 0); + cs = result.fillColorSpace = this.#parseColorSpace( + Name.get("DeviceRGB") + ); + cs.getRgbItem(args, 0, result.fontColor, 0); break; case OPS.setFillGray: - ColorSpaceUtils.gray.getRgbItem(args, 0, result.fontColor, 0); + cs = result.fillColorSpace = this.#parseColorSpace( + Name.get("DeviceGray") + ); + cs.getRgbItem(args, 0, result.fontColor, 0); break; case OPS.setFillCMYKColor: - ColorSpaceUtils.cmyk.getRgbItem(args, 0, result.fontColor, 0); + cs = result.fillColorSpace = this.#parseColorSpace( + Name.get("DeviceCMYK") + ); + cs.getRgbItem(args, 0, result.fontColor, 0); break; case OPS.showText: case OPS.showSpacedText: @@ -208,6 +211,17 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { }); return shadow(this, "_pdfFunctionFactory", pdfFunctionFactory); } + + #parseColorSpace(cs) { + return ColorSpaceUtils.parse({ + cs, + xref: this.xref, + resources: this.resources, + pdfFunctionFactory: this._pdfFunctionFactory, + globalColorSpaceCache: this.globalColorSpaceCache, + localColorSpaceCache: this._localColorSpaceCache, + }); + } } // Parse appearance stream to extract font and color information. diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 3b1cb549f9a4d..50b14c3220a09 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -1449,16 +1449,16 @@ class PartialEvaluator { }); } - async _handleColorSpace(csPromise) { + async _handleColorSpace(csPromise, name = null) { try { return await csPromise; } catch (ex) { if (ex instanceof AbortException) { - return null; + return name ? ColorSpaceUtils[name] : null; } if (this.options.ignoreErrors) { warn(`_handleColorSpace - ignoring ColorSpace: "${ex}".`); - return null; + return name ? ColorSpaceUtils[name] : null; } throw ex; } @@ -1951,9 +1951,8 @@ class PartialEvaluator { } next( - self._handleColorSpace(fillCS).then(colorSpace => { - stateManager.state.fillColorSpace = - colorSpace || ColorSpaceUtils.gray; + self._handleColorSpace(fillCS, "gray").then(CS => { + stateManager.state.fillColorSpace = CS; }) ); return; @@ -1970,9 +1969,8 @@ class PartialEvaluator { } next( - self._handleColorSpace(strokeCS).then(colorSpace => { - stateManager.state.strokeColorSpace = - colorSpace || ColorSpaceUtils.gray; + self._handleColorSpace(strokeCS, "gray").then(CS => { + stateManager.state.strokeColorSpace = CS; }) ); return; @@ -1987,41 +1985,164 @@ class PartialEvaluator { args = cs.getRgb(args, 0); fn = OPS.setStrokeRGBColor; break; - case OPS.setFillGray: - stateManager.state.fillColorSpace = ColorSpaceUtils.gray; - args = ColorSpaceUtils.gray.getRgb(args, 0); - fn = OPS.setFillRGBColor; - break; - case OPS.setStrokeGray: - stateManager.state.strokeColorSpace = ColorSpaceUtils.gray; - args = ColorSpaceUtils.gray.getRgb(args, 0); - fn = OPS.setStrokeRGBColor; - break; - case OPS.setFillCMYKColor: - stateManager.state.fillColorSpace = ColorSpaceUtils.cmyk; - args = ColorSpaceUtils.cmyk.getRgb(args, 0); - fn = OPS.setFillRGBColor; - break; - case OPS.setStrokeCMYKColor: - stateManager.state.strokeColorSpace = ColorSpaceUtils.cmyk; - args = ColorSpaceUtils.cmyk.getRgb(args, 0); - fn = OPS.setStrokeRGBColor; - break; - case OPS.setFillRGBColor: - stateManager.state.fillColorSpace = ColorSpaceUtils.rgb; - args = ColorSpaceUtils.rgb.getRgb(args, 0); - break; - case OPS.setStrokeRGBColor: - stateManager.state.strokeColorSpace = ColorSpaceUtils.rgb; - args = ColorSpaceUtils.rgb.getRgb(args, 0); - break; + case OPS.setFillGray: { + const fillCS = self._getColorSpace( + Name.get("DeviceGray"), + resources, + localColorSpaceCache + ); + if (fillCS instanceof ColorSpace) { + stateManager.state.fillColorSpace = fillCS; + + args = fillCS.getRgb(args, 0); + fn = OPS.setFillRGBColor; + break; + } + + next( + self._handleColorSpace(fillCS, "gray").then(CS => { + stateManager.state.fillColorSpace = CS; + + operatorList.addOp(OPS.setFillRGBColor, CS.getRgb(args, 0)); + }) + ); + return; + } + case OPS.setStrokeGray: { + const strokeCS = self._getColorSpace( + Name.get("DeviceGray"), + resources, + localColorSpaceCache + ); + if (strokeCS instanceof ColorSpace) { + stateManager.state.strokeColorSpace = strokeCS; + + args = strokeCS.getRgb(args, 0); + fn = OPS.setStrokeRGBColor; + break; + } + + next( + self._handleColorSpace(strokeCS, "gray").then(CS => { + stateManager.state.strokeColorSpace = CS; + + operatorList.addOp(OPS.setStrokeRGBColor, CS.getRgb(args, 0)); + }) + ); + return; + } + case OPS.setFillCMYKColor: { + const fillCS = self._getColorSpace( + Name.get("DeviceCMYK"), + resources, + localColorSpaceCache + ); + if (fillCS instanceof ColorSpace) { + stateManager.state.fillColorSpace = fillCS; + + args = fillCS.getRgb(args, 0); + fn = OPS.setFillRGBColor; + break; + } + + next( + self._handleColorSpace(fillCS, "cmyk").then(CS => { + stateManager.state.fillColorSpace = CS; + + operatorList.addOp(OPS.setFillRGBColor, CS.getRgb(args, 0)); + }) + ); + return; + } + case OPS.setStrokeCMYKColor: { + const strokeCS = self._getColorSpace( + Name.get("DeviceCMYK"), + resources, + localColorSpaceCache + ); + if (strokeCS instanceof ColorSpace) { + stateManager.state.strokeColorSpace = strokeCS; + + args = strokeCS.getRgb(args, 0); + fn = OPS.setStrokeRGBColor; + break; + } + + next( + self._handleColorSpace(strokeCS, "cmyk").then(CS => { + stateManager.state.strokeColorSpace = CS; + + operatorList.addOp(OPS.setStrokeRGBColor, CS.getRgb(args, 0)); + }) + ); + return; + } + case OPS.setFillRGBColor: { + const fillCS = self._getColorSpace( + Name.get("DeviceRGB"), + resources, + localColorSpaceCache + ); + if (fillCS instanceof ColorSpace) { + stateManager.state.fillColorSpace = fillCS; + + args = fillCS.getRgb(args, 0); + break; + } + + next( + self._handleColorSpace(fillCS, "rgb").then(CS => { + stateManager.state.fillColorSpace = CS; + + operatorList.addOp(OPS.setFillRGBColor, CS.getRgb(args, 0)); + }) + ); + return; + } + case OPS.setStrokeRGBColor: { + const strokeCS = self._getColorSpace( + Name.get("DeviceRGB"), + resources, + localColorSpaceCache + ); + if (strokeCS instanceof ColorSpace) { + stateManager.state.strokeColorSpace = strokeCS; + + args = strokeCS.getRgb(args, 0); + fn = OPS.setStrokeRGBColor; + break; + } + + next( + self._handleColorSpace(strokeCS, "rgb").then(CS => { + stateManager.state.strokeColorSpace = CS; + + operatorList.addOp(OPS.setStrokeRGBColor, CS.getRgb(args, 0)); + }) + ); + return; + } case OPS.setFillColorN: cs = stateManager.state.patternFillColorSpace; if (!cs) { if (isNumberArray(args, null)) { - args = ColorSpaceUtils.gray.getRgb(args, 0); - fn = OPS.setFillRGBColor; - break; + const fillCS = self._getColorSpace( + Name.get("DeviceGray"), + resources, + localColorSpaceCache + ); + if (fillCS instanceof ColorSpace) { + args = fillCS.getRgb(args, 0); + fn = OPS.setFillRGBColor; + break; + } + + next( + self._handleColorSpace(fillCS, "gray").then(CS => { + operatorList.addOp(OPS.setFillRGBColor, CS.getRgb(args, 0)); + }) + ); + return; } args = []; fn = OPS.setFillTransparent; @@ -2051,9 +2172,26 @@ class PartialEvaluator { cs = stateManager.state.patternStrokeColorSpace; if (!cs) { if (isNumberArray(args, null)) { - args = ColorSpaceUtils.gray.getRgb(args, 0); - fn = OPS.setStrokeRGBColor; - break; + const strokeCS = self._getColorSpace( + Name.get("DeviceGray"), + resources, + localColorSpaceCache + ); + if (strokeCS instanceof ColorSpace) { + args = strokeCS.getRgb(args, 0); + fn = OPS.setStrokeRGBColor; + break; + } + + next( + self._handleColorSpace(strokeCS, "gray").then(CS => { + operatorList.addOp( + OPS.setStrokeRGBColor, + CS.getRgb(args, 0) + ); + }) + ); + return; } args = []; fn = OPS.setStrokeTransparent; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index a06a28688a2a7..937e6c0db06d5 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -192,6 +192,9 @@ !simpletype3font.pdf !Type3WordSpacing.pdf !IndexedCS_negative_and_high.pdf +!DefaultRGBColourSpaces.pdf +!DefaultRGBColourSpaces-inherit.pdf +!DefaultColourSpaces-230802.pdf !sizes.pdf !javauninstall-7r.pdf !file_url_link.pdf diff --git a/test/pdfs/DefaultColourSpaces-230802.pdf b/test/pdfs/DefaultColourSpaces-230802.pdf new file mode 100644 index 0000000000000..2e11ebd5c450e Binary files /dev/null and b/test/pdfs/DefaultColourSpaces-230802.pdf differ diff --git a/test/pdfs/DefaultRGBColourSpaces-inherit.pdf b/test/pdfs/DefaultRGBColourSpaces-inherit.pdf new file mode 100644 index 0000000000000..f0f467d502cd3 Binary files /dev/null and b/test/pdfs/DefaultRGBColourSpaces-inherit.pdf differ diff --git a/test/pdfs/DefaultRGBColourSpaces.pdf b/test/pdfs/DefaultRGBColourSpaces.pdf new file mode 100644 index 0000000000000..eadbb548252d3 Binary files /dev/null and b/test/pdfs/DefaultRGBColourSpaces.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index b1950cdffbe32..e94299b07f02b 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -2859,6 +2859,30 @@ "rounds": 1, "type": "eq" }, + { + "id": "DefaultRGBColourSpaces", + "file": "pdfs/DefaultRGBColourSpaces.pdf", + "md5": "43549c004e6be3f64d9323c0b9d99638", + "link": false, + "rounds": 1, + "type": "eq" + }, + { + "id": "DefaultRGBColourSpaces-inherit", + "file": "pdfs/DefaultRGBColourSpaces-inherit.pdf", + "md5": "87a7d71c31e29e8b4bc60de80042847d", + "link": false, + "rounds": 1, + "type": "eq" + }, + { + "id": "DefaultColourSpaces-230802", + "file": "pdfs/DefaultColourSpaces-230802.pdf", + "md5": "8e0f4a0552d69d9f6bb55d7fd8f83f0a", + "link": false, + "rounds": 1, + "type": "eq" + }, { "id": "issue13372", "file": "pdfs/issue13372.pdf", diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index a75747b208222..b56ab7efb82af 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -43,6 +43,7 @@ import { import { Dict, Name, Ref, RefSetCache } from "../../src/core/primitives.js"; import { Lexer, Parser } from "../../src/core/parser.js"; import { FlateStream } from "../../src/core/flate_stream.js"; +import { GlobalColorSpaceCache } from "../../src/core/image_utils.js"; import { PartialEvaluator } from "../../src/core/evaluator.js"; import { StringStream } from "../../src/core/stream.js"; import { WorkerTask } from "../../src/core/worker.js"; @@ -136,6 +137,7 @@ describe("annotation", function () { fontCache: new RefSetCache(), builtInCMapCache, standardFontDataCache: new Map(), + globalColorSpaceCache: new GlobalColorSpaceCache(), systemFontCache: new Map(), }); });