Improve (local) caching of parsed ColorSpaces (PR 12001 follow-up)

This patch contains the following *notable* improvements:
 - Changes the `ColorSpace.parse` call-sites to, where possible, pass in a reference rather than actual ColorSpace data (necessary for the next point).
 - Adds (local) caching of `ColorSpace`s by `Ref`, when applicable, in addition the caching by name. This (generally) improves `ColorSpace` caching for e.g. the SMask code-paths.
 - Extends the (local) `ColorSpace` caching to also apply when handling Images and Patterns, thus further reducing unneeded re-parsing.
 - Adds a new `ColorSpace.parseAsync` method, almost identical to the existing `ColorSpace.parse` one, but returning a Promise instead (this simplifies some code in the `PartialEvaluator`).
This commit is contained in:
Jonas Jenwald 2020-06-17 18:45:11 +02:00
parent 51e87b9248
commit 19d7976483
5 changed files with 335 additions and 45 deletions

View File

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

View File

@ -411,12 +411,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,
@ -483,6 +486,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
operatorList,
cacheKey,
localImageCache,
localColorSpaceCache,
}) {
var dict = image.dict;
const imageRef = dict.objId;
@ -549,6 +553,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.
@ -585,6 +590,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
image,
isInline,
pdfFunctionFactory: this.pdfFunctionFactory,
localColorSpaceCache,
})
.then(imageObj => {
imgData = imageObj.createImageData(/* forceRGBA = */ false);
@ -1135,19 +1141,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;
@ -1165,7 +1164,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
@ -1194,7 +1202,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
this.xref,
resources,
this.handler,
this.pdfFunctionFactory
this.pdfFunctionFactory,
localColorSpaceCache
);
operatorList.addOp(fn, pattern.getIR());
return undefined;
@ -1352,6 +1361,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
operatorList,
cacheKey: name,
localImageCache,
localColorSpaceCache,
})
.then(resolveXObject, rejectXObject);
return;
@ -1425,6 +1435,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
operatorList,
cacheKey,
localImageCache,
localColorSpaceCache,
})
);
return;
@ -1483,11 +1494,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;
}
@ -1507,11 +1520,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;
}
@ -1579,7 +1594,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
cs,
patterns,
resources,
task
task,
localColorSpaceCache
)
);
return;
@ -1598,7 +1614,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
cs,
patterns,
resources,
task
task,
localColorSpaceCache
)
);
return;
@ -1624,7 +1641,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
xref,
resources,
self.handler,
self.pdfFunctionFactory
self.pdfFunctionFactory,
localColorSpaceCache
);
var patternIR = shadingFill.getIR();
args = [patternIR];

View File

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

View File

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

View File

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