diff --git a/src/core/colorspace.js b/src/core/colorspace.js index 3b0f7e751efbd0..b45f74b38b2987 100644 --- a/src/core/colorspace.js +++ b/src/core/colorspace.js @@ -22,7 +22,8 @@ import { unreachable, warn, } from "../shared/util.js"; -import { isDict, isName, isStream } from "./primitives.js"; +import { isDict, isName, isStream, Name, Ref } from "./primitives.js"; +import { MissingDataException } from "./core_utils.js"; /** * Resizes an RGB image with 3 components. @@ -259,9 +260,109 @@ class ColorSpace { return shadow(this, "usesZeroToOneRange", true); } - static parse({ cs, xref, resources = null, pdfFunctionFactory }) { + /** + * @private + */ + static _cache(cacheKey, xref, localColorSpaceCache, parsedColorSpace) { + if (!localColorSpaceCache) { + throw new Error( + 'ColorSpace._cache - expected "localColorSpaceCache" argument.' + ); + } + if (!parsedColorSpace) { + throw new Error( + 'ColorSpace._cache - expected "parsedColorSpace" 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, parsedColorSpace); + } + } + + static getCached(cacheKey, xref, localColorSpaceCache) { + if (!localColorSpaceCache) { + throw new Error( + 'ColorSpace.getCached - expected "localColorSpaceCache" argument.' + ); + } + if (cacheKey instanceof Ref) { + const localColorSpace = localColorSpaceCache.getByRef(cacheKey); + if (localColorSpace) { + return localColorSpace; + } + + 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) { + const localColorSpace = localColorSpaceCache.getByName(cacheKey.name); + if (localColorSpace) { + return localColorSpace; + } + } + return null; + } + + static async parseAsync({ + cs, + xref, + resources = null, + pdfFunctionFactory, + localColorSpaceCache, + }) { + if ( + typeof PDFJSDev === "undefined" || + PDFJSDev.test("!PRODUCTION || TESTING") + ) { + assert( + !this.getCached(cs, xref, localColorSpaceCache), + "Expected `ColorSpace.getCached` to have been manually checked " + + "before calling `ColorSpace.parseAsync`." + ); + } const IR = this.parseToIR(cs, xref, resources, pdfFunctionFactory); - return this.fromIR(IR); + const parsedColorSpace = this.fromIR(IR); + + // Attempt to cache the parsed ColorSpace, by name and/or reference. + this._cache(cs, xref, localColorSpaceCache, parsedColorSpace); + + return parsedColorSpace; + } + + static parse({ + cs, + xref, + resources = null, + pdfFunctionFactory, + localColorSpaceCache, + }) { + const cachedColorSpace = this.getCached(cs, xref, localColorSpaceCache); + if (cachedColorSpace) { + return cachedColorSpace; + } + const IR = this.parseToIR(cs, xref, resources, pdfFunctionFactory); + const parsedColorSpace = this.fromIR(IR); + + // Attempt to cache the parsed ColorSpace, by name and/or reference. + this._cache(cs, xref, localColorSpaceCache, parsedColorSpace); + + return parsedColorSpace; } static fromIR(IR) { diff --git a/src/core/evaluator.js b/src/core/evaluator.js index b7be7a01e7964d..109f401f38fc5e 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -408,12 +408,15 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { groupOptions.isolated = group.get("I") || false; groupOptions.knockout = group.get("K") || false; if (group.has("CS")) { - const cs = group.get("CS"); + const cs = group.getRaw("CS"); - const localColorSpace = - cs instanceof Name && localColorSpaceCache.getByName(cs.name); - if (localColorSpace) { - colorSpace = localColorSpace; + const cachedColorSpace = ColorSpace.getCached( + cs, + this.xref, + localColorSpaceCache + ); + if (cachedColorSpace) { + colorSpace = cachedColorSpace; } else { colorSpace = await this.parseColorSpace({ cs, @@ -480,6 +483,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { operatorList, cacheKey, localImageCache, + localColorSpaceCache, }) { var dict = image.dict; const imageRef = dict.objId; @@ -546,6 +550,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { image, isInline, pdfFunctionFactory: this.pdfFunctionFactory, + localColorSpaceCache, }); // We force the use of RGBA_32BPP images here, because we can't handle // any other kind. @@ -582,6 +587,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { image, isInline, pdfFunctionFactory: this.pdfFunctionFactory, + localColorSpaceCache, }) .then(imageObj => { imgData = imageObj.createImageData(/* forceRGBA = */ false); @@ -1132,19 +1138,12 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, parseColorSpace({ cs, resources, localColorSpaceCache }) { - return new Promise(resolve => { - const parsedColorSpace = ColorSpace.parse({ - cs, - xref: this.xref, - resources, - pdfFunctionFactory: this.pdfFunctionFactory, - }); - - const csName = cs instanceof Name ? cs.name : null; - if (csName) { - localColorSpaceCache.set(csName, /* ref = */ null, parsedColorSpace); - } - resolve(parsedColorSpace); + return ColorSpace.parseAsync({ + cs, + xref: this.xref, + resources, + pdfFunctionFactory: this.pdfFunctionFactory, + localColorSpaceCache, }).catch(reason => { if (reason instanceof AbortException) { return null; @@ -1162,7 +1161,16 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }); }, - async handleColorN(operatorList, fn, args, cs, patterns, resources, task) { + async handleColorN( + operatorList, + fn, + args, + cs, + patterns, + resources, + task, + localColorSpaceCache + ) { // compile tiling patterns var patternName = args[args.length - 1]; // SCN/scn applies patterns along with normal colors @@ -1191,7 +1199,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { this.xref, resources, this.handler, - this.pdfFunctionFactory + this.pdfFunctionFactory, + localColorSpaceCache ); operatorList.addOp(fn, pattern.getIR()); return undefined; @@ -1349,6 +1358,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { operatorList, cacheKey: name, localImageCache, + localColorSpaceCache, }) .then(resolveXObject, rejectXObject); return; @@ -1422,6 +1432,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { operatorList, cacheKey, localImageCache, + localColorSpaceCache, }) ); return; @@ -1480,11 +1491,13 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { break; case OPS.setFillColorSpace: { - const localColorSpace = - args[0] instanceof Name && - localColorSpaceCache.getByName(args[0].name); - if (localColorSpace) { - stateManager.state.fillColorSpace = localColorSpace; + const cachedColorSpace = ColorSpace.getCached( + args[0], + xref, + localColorSpaceCache + ); + if (cachedColorSpace) { + stateManager.state.fillColorSpace = cachedColorSpace; continue; } @@ -1504,11 +1517,13 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { return; } case OPS.setStrokeColorSpace: { - const localColorSpace = - args[0] instanceof Name && - localColorSpaceCache.getByName(args[0].name); - if (localColorSpace) { - stateManager.state.strokeColorSpace = localColorSpace; + const cachedColorSpace = ColorSpace.getCached( + args[0], + xref, + localColorSpaceCache + ); + if (cachedColorSpace) { + stateManager.state.strokeColorSpace = cachedColorSpace; continue; } @@ -1576,7 +1591,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { cs, patterns, resources, - task + task, + localColorSpaceCache ) ); return; @@ -1595,7 +1611,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { cs, patterns, resources, - task + task, + localColorSpaceCache ) ); return; @@ -1621,7 +1638,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { xref, resources, self.handler, - self.pdfFunctionFactory + self.pdfFunctionFactory, + localColorSpaceCache ); var patternIR = shadingFill.getIR(); args = [patternIR]; diff --git a/src/core/image.js b/src/core/image.js index 3da9f12c519172..f41a1991aab5dd 100644 --- a/src/core/image.js +++ b/src/core/image.js @@ -89,6 +89,7 @@ var PDFImage = (function PDFImageClosure() { mask = null, isMask = false, pdfFunctionFactory, + localColorSpaceCache, }) { this.image = image; var dict = image.dict; @@ -159,7 +160,7 @@ var PDFImage = (function PDFImageClosure() { this.bpc = bitsPerComponent; if (!this.imageMask) { - var colorSpace = dict.get("ColorSpace", "CS"); + let colorSpace = dict.getRaw("ColorSpace") || dict.getRaw("CS"); if (!colorSpace) { info("JPX images (which do not require color spaces)"); switch (image.numComps) { @@ -184,6 +185,7 @@ var PDFImage = (function PDFImageClosure() { xref, resources: isInline ? res : null, pdfFunctionFactory, + localColorSpaceCache, }); this.numComps = this.colorSpace.numComps; } @@ -220,6 +222,7 @@ var PDFImage = (function PDFImageClosure() { image: smask, isInline, pdfFunctionFactory, + localColorSpaceCache, }); } else if (mask) { if (isStream(mask)) { @@ -235,6 +238,7 @@ var PDFImage = (function PDFImageClosure() { isInline, isMask: true, pdfFunctionFactory, + localColorSpaceCache, }); } } else { @@ -253,6 +257,7 @@ var PDFImage = (function PDFImageClosure() { image, isInline = false, pdfFunctionFactory, + localColorSpaceCache, }) { const imageData = image; let smaskData = null; @@ -279,6 +284,7 @@ var PDFImage = (function PDFImageClosure() { smask: smaskData, mask: maskData, pdfFunctionFactory, + localColorSpaceCache, }); }; diff --git a/src/core/pattern.js b/src/core/pattern.js index 384a9bb3ac47c4..e1f63b513ed161 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -57,7 +57,8 @@ var Pattern = (function PatternClosure() { xref, res, handler, - pdfFunctionFactory + pdfFunctionFactory, + localColorSpaceCache ) { var dict = isStream(shading) ? shading.dict : shading; var type = dict.get("ShadingType"); @@ -72,7 +73,8 @@ var Pattern = (function PatternClosure() { matrix, xref, res, - pdfFunctionFactory + pdfFunctionFactory, + localColorSpaceCache ); case ShadingType.FREE_FORM_MESH: case ShadingType.LATTICE_FORM_MESH: @@ -83,7 +85,8 @@ var Pattern = (function PatternClosure() { matrix, xref, res, - pdfFunctionFactory + pdfFunctionFactory, + localColorSpaceCache ); default: throw new FormatError("Unsupported ShadingType: " + type); @@ -111,16 +114,24 @@ Shadings.SMALL_NUMBER = 1e-6; // Radial and axial shading have very similar implementations // If needed, the implementations can be broken into two classes Shadings.RadialAxial = (function RadialAxialClosure() { - function RadialAxial(dict, matrix, xref, resources, pdfFunctionFactory) { + function RadialAxial( + dict, + matrix, + xref, + resources, + pdfFunctionFactory, + localColorSpaceCache + ) { this.matrix = matrix; this.coordsArr = dict.getArray("Coords"); this.shadingType = dict.get("ShadingType"); this.type = "Pattern"; const cs = ColorSpace.parse({ - cs: dict.get("ColorSpace", "CS"), + cs: dict.getRaw("ColorSpace") || dict.getRaw("CS"), xref, resources, pdfFunctionFactory, + localColorSpaceCache, }); this.cs = cs; const bbox = dict.getArray("BBox"); @@ -834,7 +845,14 @@ Shadings.Mesh = (function MeshClosure() { } } - function Mesh(stream, matrix, xref, resources, pdfFunctionFactory) { + function Mesh( + stream, + matrix, + xref, + resources, + pdfFunctionFactory, + localColorSpaceCache + ) { if (!isStream(stream)) { throw new FormatError("Mesh data is not a stream"); } @@ -849,10 +867,11 @@ Shadings.Mesh = (function MeshClosure() { this.bbox = null; } const cs = ColorSpace.parse({ - cs: dict.get("ColorSpace", "CS"), + cs: dict.getRaw("ColorSpace") || dict.getRaw("CS"), xref, resources, pdfFunctionFactory, + localColorSpaceCache, }); this.cs = cs; this.background = dict.has("Background") diff --git a/test/unit/colorspace_spec.js b/test/unit/colorspace_spec.js index 05946d9da37e6f..eeb381e33c7527 100644 --- a/test/unit/colorspace_spec.js +++ b/test/unit/colorspace_spec.js @@ -16,18 +16,21 @@ import { Dict, Name, Ref } from "../../src/core/primitives.js"; import { Stream, StringStream } from "../../src/core/stream.js"; import { ColorSpace } from "../../src/core/colorspace.js"; +import { LocalColorSpaceCache } from "../../src/core/image_utils.js"; import { PDFFunctionFactory } from "../../src/core/function.js"; import { XRefMock } from "./test_utils.js"; describe("colorspace", function () { - describe("ColorSpace", function () { + describe("ColorSpace.isDefaultDecode", function () { it("should be true if decode is not an array", function () { expect(ColorSpace.isDefaultDecode("string", 0)).toBeTruthy(); }); + it("should be true if length of decode array is not correct", function () { expect(ColorSpace.isDefaultDecode([0], 1)).toBeTruthy(); expect(ColorSpace.isDefaultDecode([0, 1, 0], 1)).toBeTruthy(); }); + it("should be true if decode map matches the default decode map", function () { expect(ColorSpace.isDefaultDecode([], 0)).toBeTruthy(); @@ -46,6 +49,138 @@ describe("colorspace", function () { }); }); + describe("ColorSpace caching", function () { + let localColorSpaceCache = null; + + beforeAll(function (done) { + localColorSpaceCache = new LocalColorSpaceCache(); + done(); + }); + + afterAll(function (done) { + localColorSpaceCache = null; + done(); + }); + + it("caching by Name", function () { + const xref = new XRefMock(); + const pdfFunctionFactory = new PDFFunctionFactory({ + xref, + }); + + const colorSpace1 = ColorSpace.parse({ + cs: Name.get("Pattern"), + xref, + resources: null, + pdfFunctionFactory, + localColorSpaceCache, + }); + expect(colorSpace1.name).toEqual("Pattern"); + + const colorSpace2 = ColorSpace.parse({ + cs: Name.get("Pattern"), + xref, + resources: null, + pdfFunctionFactory, + localColorSpaceCache, + }); + expect(colorSpace2.name).toEqual("Pattern"); + + const colorSpaceNonCached = ColorSpace.parse({ + cs: Name.get("Pattern"), + xref, + resources: null, + pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), + }); + expect(colorSpaceNonCached.name).toEqual("Pattern"); + + const colorSpaceOther = ColorSpace.parse({ + cs: Name.get("RGB"), + xref, + resources: null, + pdfFunctionFactory, + localColorSpaceCache, + }); + expect(colorSpaceOther.name).toEqual("DeviceRGB"); + + // These two must be *identical* if caching worked as intended. + expect(colorSpace1).toBe(colorSpace2); + + expect(colorSpace1).not.toBe(colorSpaceNonCached); + expect(colorSpace1).not.toBe(colorSpaceOther); + }); + + it("caching by Ref", function () { + const paramsCalGray = new Dict(); + paramsCalGray.set("WhitePoint", [1, 1, 1]); + paramsCalGray.set("BlackPoint", [0, 0, 0]); + paramsCalGray.set("Gamma", 2.0); + + const paramsCalRGB = new Dict(); + paramsCalRGB.set("WhitePoint", [1, 1, 1]); + paramsCalRGB.set("BlackPoint", [0, 0, 0]); + paramsCalRGB.set("Gamma", [1, 1, 1]); + paramsCalRGB.set("Matrix", [1, 0, 0, 0, 1, 0, 0, 0, 1]); + + const xref = new XRefMock([ + { + ref: Ref.get(50, 0), + data: [Name.get("CalGray"), paramsCalGray], + }, + { + ref: Ref.get(100, 0), + data: [Name.get("CalRGB"), paramsCalRGB], + }, + ]); + const pdfFunctionFactory = new PDFFunctionFactory({ + xref, + }); + + const colorSpace1 = ColorSpace.parse({ + cs: Ref.get(50, 0), + xref, + resources: null, + pdfFunctionFactory, + localColorSpaceCache, + }); + expect(colorSpace1.name).toEqual("CalGray"); + + const colorSpace2 = ColorSpace.parse({ + cs: Ref.get(50, 0), + xref, + resources: null, + pdfFunctionFactory, + localColorSpaceCache, + }); + expect(colorSpace2.name).toEqual("CalGray"); + + const colorSpaceNonCached = ColorSpace.parse({ + cs: Ref.get(50, 0), + xref, + resources: null, + pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), + }); + expect(colorSpaceNonCached.name).toEqual("CalGray"); + + const colorSpaceOther = ColorSpace.parse({ + cs: Ref.get(100, 0), + xref, + resources: null, + pdfFunctionFactory, + localColorSpaceCache, + }); + expect(colorSpaceOther.name).toEqual("CalRGB"); + + // These two must be *identical* if caching worked as intended. + expect(colorSpace1).toBe(colorSpace2); + + expect(colorSpace1).not.toBe(colorSpaceNonCached); + expect(colorSpace1).not.toBe(colorSpaceOther); + }); + }); + describe("DeviceGrayCS", function () { it("should handle the case when cs is a Name object", function () { const cs = Name.get("DeviceGray"); @@ -65,6 +200,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); const testSrc = new Uint8Array([27, 125, 250, 131]); @@ -115,6 +251,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); const testSrc = new Uint8Array([27, 125, 250, 131]); @@ -161,6 +298,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); // prettier-ignore @@ -217,6 +355,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); // prettier-ignore @@ -269,6 +408,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); // prettier-ignore @@ -325,6 +465,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); // prettier-ignore @@ -382,6 +523,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); const testSrc = new Uint8Array([27, 125, 250, 131]); @@ -441,6 +583,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); // prettier-ignore @@ -498,6 +641,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); // prettier-ignore @@ -557,6 +701,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); const testSrc = new Uint8Array([2, 2, 0, 1]); @@ -624,6 +769,7 @@ describe("colorspace", function () { xref, resources, pdfFunctionFactory, + localColorSpaceCache: new LocalColorSpaceCache(), }); const testSrc = new Uint8Array([27, 25, 50, 31]);