Skip to content

Commit

Permalink
Add a GlobalColorSpaceCache to reduce unnecessary re-parsing
Browse files Browse the repository at this point in the history
This complements the existing `LocalColorSpaceCache`, which is unique to each `getOperatorList`-invocation since it also caches by `Name`, which should help reduce unnecessary re-parsing especially for e.g. `ICCBased` ColorSpaces once we properly support those.
  • Loading branch information
Snuffleupagus committed Mar 1, 2025
1 parent 6e1cfa2 commit 4c08c50
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 75 deletions.
16 changes: 13 additions & 3 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,23 @@ class AnnotationFactory {
// Only necessary to prevent the `Catalog.attachments`-getter, used
// with "GoToE" actions, from throwing and thus breaking parsing:
pdfManager.ensureCatalog("attachments"),
pdfManager.ensureCatalog("globalColorSpaceCache"),
]).then(
([acroForm, xfaDatasets, structTreeRoot, baseUrl, attachments]) => ({
([
acroForm,
xfaDatasets,
structTreeRoot,
baseUrl,
attachments,
globalColorSpaceCache,
]) => ({
pdfManager,
acroForm: acroForm instanceof Dict ? acroForm : Dict.empty,
xfaDatasets,
structTreeRoot,
baseUrl,
attachments,
globalColorSpaceCache,
}),
reason => {
warn(`createGlobals: "${reason}".`);
Expand Down Expand Up @@ -3880,7 +3889,7 @@ class FreeTextAnnotation extends MarkupAnnotation {
// We want to be able to add mouse listeners to the annotation.
this.data.noHTML = false;

const { evaluatorOptions, xref } = params;
const { annotationGlobals, evaluatorOptions, xref } = params;
this.data.annotationType = AnnotationType.FREETEXT;
this.setDefaultAppearance(params);
this._hasAppearance = !!this.appearance;
Expand All @@ -3889,7 +3898,8 @@ class FreeTextAnnotation extends MarkupAnnotation {
const { fontColor, fontSize } = parseAppearanceStream(
this.appearance,
evaluatorOptions,
xref
xref,
annotationGlobals.globalColorSpaceCache
);
this.data.defaultAppearanceData.fontColor = fontColor;
this.data.defaultAppearanceData.fontSize = fontSize || 10;
Expand Down
4 changes: 3 additions & 1 deletion src/core/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ import {
RefSet,
RefSetCache,
} from "./primitives.js";
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 { FileSpec } from "./file_spec.js";
import { GlobalImageCache } from "./image_utils.js";
import { MetadataParser } from "./metadata_parser.js";
import { StructTreeRoot } from "./struct_tree.js";

Expand Down Expand Up @@ -140,6 +140,7 @@ class Catalog {
this.fontCache = new RefSetCache();
this.builtInCMapCache = new Map();
this.standardFontDataCache = new Map();
this.globalColorSpaceCache = new GlobalColorSpaceCache();
this.globalImageCache = new GlobalImageCache();
this.pageKidsCountCache = new RefSetCache();
this.pageIndexCache = new RefSetCache();
Expand Down Expand Up @@ -1171,6 +1172,7 @@ class Catalog {

async cleanup(manuallyTriggered = false) {
clearGlobalCaches();
this.globalColorSpaceCache.clear();
this.globalImageCache.clear(/* onlyData = */ manuallyTriggered);
this.pageKidsCountCache.clear();
this.pageIndexCache.clear();
Expand Down
110 changes: 64 additions & 46 deletions src/core/colorspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,19 +306,20 @@ class ColorSpace {
return shadow(this, "usesZeroToOneRange", true);
}

/**
* @private
*/
static _cache(cacheKey, xref, localColorSpaceCache, parsedColorSpace) {
if (!localColorSpaceCache) {
static #cache(
cacheKey,
xref,
globalColorSpaceCache,
localColorSpaceCache,
parsedCS
) {
if (!globalColorSpaceCache || !localColorSpaceCache) {
throw new Error(
'ColorSpace._cache - expected "localColorSpaceCache" argument.'
'ColorSpace.#cache - expected "globalColorSpaceCache"/"localColorSpaceCache" argument.'
);
}
if (!parsedColorSpace) {
throw new Error(
'ColorSpace._cache - expected "parsedColorSpace" argument.'
);
if (!parsedCS) {
throw new Error('ColorSpace.#cache - expected "parsedCS" argument.');
}
let csName, csRef;
if (cacheKey instanceof Ref) {
Expand All @@ -331,20 +332,31 @@ class ColorSpace {
csName = cacheKey.name;
}
if (csName || csRef) {
localColorSpaceCache.set(csName, csRef, parsedColorSpace);
localColorSpaceCache.set(csName, csRef, parsedCS);

if (csRef) {
globalColorSpaceCache.set(/* name = */ null, csRef, parsedCS);
}
}
}

static getCached(cacheKey, xref, localColorSpaceCache) {
if (!localColorSpaceCache) {
static getCached(
cacheKey,
xref,
globalColorSpaceCache,
localColorSpaceCache
) {
if (!globalColorSpaceCache || !localColorSpaceCache) {
throw new Error(
'ColorSpace.getCached - expected "localColorSpaceCache" argument.'
'ColorSpace.getCached - expected "globalColorSpaceCache"/"localColorSpaceCache" argument.'
);
}
if (cacheKey instanceof Ref) {
const localColorSpace = localColorSpaceCache.getByRef(cacheKey);
if (localColorSpace) {
return localColorSpace;
const cachedCS =
globalColorSpaceCache.getByRef(cacheKey) ||
localColorSpaceCache.getByRef(cacheKey);
if (cachedCS) {
return cachedCS;
}

try {
Expand All @@ -357,9 +369,9 @@ class ColorSpace {
}
}
if (cacheKey instanceof Name) {
const localColorSpace = localColorSpaceCache.getByName(cacheKey.name);
if (localColorSpace) {
return localColorSpace;
const cachedCS = localColorSpaceCache.getByName(cacheKey.name);
if (cachedCS) {
return cachedCS;
}
}
return null;
Expand All @@ -370,56 +382,62 @@ class ColorSpace {
xref,
resources = null,
pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache,
}) {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert(
!this.getCached(cs, xref, localColorSpaceCache),
!this.getCached(cs, xref, globalColorSpaceCache, localColorSpaceCache),
"Expected `ColorSpace.getCached` to have been manually checked " +
"before calling `ColorSpace.parseAsync`."
);
}
const parsedColorSpace = this._parse(
const parsedCS = this.#parse(cs, xref, resources, pdfFunctionFactory);

// Attempt to cache the parsed ColorSpace, by name and/or reference.
this.#cache(
cs,
xref,
resources,
pdfFunctionFactory
globalColorSpaceCache,
localColorSpaceCache,
parsedCS
);

// Attempt to cache the parsed ColorSpace, by name and/or reference.
this._cache(cs, xref, localColorSpaceCache, parsedColorSpace);

return parsedColorSpace;
return parsedCS;
}

static parse({
cs,
xref,
resources = null,
pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache,
}) {
const cachedColorSpace = this.getCached(cs, xref, localColorSpaceCache);
if (cachedColorSpace) {
return cachedColorSpace;
}
const parsedColorSpace = this._parse(
const cachedCS = this.getCached(
cs,
xref,
resources,
pdfFunctionFactory
globalColorSpaceCache,
localColorSpaceCache
);
if (cachedCS) {
return cachedCS;
}
const parsedCS = this.#parse(cs, xref, resources, pdfFunctionFactory);

// Attempt to cache the parsed ColorSpace, by name and/or reference.
this._cache(cs, xref, localColorSpaceCache, parsedColorSpace);
this.#cache(
cs,
xref,
globalColorSpaceCache,
localColorSpaceCache,
parsedCS
);

return parsedColorSpace;
return parsedCS;
}

/**
* @private
*/
static _parse(cs, xref, resources = null, pdfFunctionFactory) {
static #parse(cs, xref, resources = null, pdfFunctionFactory) {
cs = xref.fetchIfRef(cs);
if (cs instanceof Name) {
switch (cs.name) {
Expand All @@ -443,7 +461,7 @@ class ColorSpace {
const resourcesCS = colorSpaces.get(cs.name);
if (resourcesCS) {
if (resourcesCS instanceof Name) {
return this._parse(
return this.#parse(
resourcesCS,
xref,
resources,
Expand Down Expand Up @@ -493,7 +511,7 @@ class ColorSpace {
numComps = dict.get("N");
const alt = dict.get("Alternate");
if (alt) {
const altCS = this._parse(alt, xref, resources, pdfFunctionFactory);
const altCS = this.#parse(alt, xref, resources, pdfFunctionFactory);
// Ensure that the number of components are correct,
// and also (indirectly) that it is not a PatternCS.
if (altCS.numComps === numComps) {
Expand All @@ -512,20 +530,20 @@ class ColorSpace {
case "Pattern":
baseCS = cs[1] || null;
if (baseCS) {
baseCS = this._parse(baseCS, xref, resources, pdfFunctionFactory);
baseCS = this.#parse(baseCS, xref, resources, pdfFunctionFactory);
}
return new PatternCS(baseCS);
case "I":
case "Indexed":
baseCS = this._parse(cs[1], xref, resources, pdfFunctionFactory);
baseCS = this.#parse(cs[1], xref, resources, pdfFunctionFactory);
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._parse(cs[2], xref, resources, pdfFunctionFactory);
baseCS = this.#parse(cs[2], xref, resources, pdfFunctionFactory);
const tintFn = pdfFunctionFactory.create(cs[3]);
return new AlternateCS(numComps, baseCS, tintFn);
case "Lab":
Expand Down
18 changes: 15 additions & 3 deletions src/core/default_appearance.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,12 @@ function parseDefaultAppearance(str) {
}

class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
constructor(stream, evaluatorOptions, xref) {
constructor(stream, evaluatorOptions, xref, globalColorSpaceCache) {
super(stream);
this.stream = stream;
this.evaluatorOptions = evaluatorOptions;
this.xref = xref;
this.globalColorSpaceCache = globalColorSpaceCache;

this.resources = stream.dict?.get("Resources");
}
Expand Down Expand Up @@ -161,6 +162,7 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
xref: this.xref,
resources: this.resources,
pdfFunctionFactory: this._pdfFunctionFactory,
globalColorSpaceCache: this.globalColorSpaceCache,
localColorSpaceCache: this._localColorSpaceCache,
});
break;
Expand Down Expand Up @@ -210,8 +212,18 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {

// Parse appearance stream to extract font and color information.
// It returns the font properties used to render the first text object.
function parseAppearanceStream(stream, evaluatorOptions, xref) {
return new AppearanceStreamEvaluator(stream, evaluatorOptions, xref).parse();
function parseAppearanceStream(
stream,
evaluatorOptions,
xref,
globalColorSpaceCache
) {
return new AppearanceStreamEvaluator(
stream,
evaluatorOptions,
xref,
globalColorSpaceCache
).parse();
}

function getPdfColor(color, isFill) {
Expand Down
9 changes: 9 additions & 0 deletions src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class Page {
fontCache,
builtInCMapCache,
standardFontDataCache,
globalColorSpaceCache,
globalImageCache,
systemFontCache,
nonBlendModesSet,
Expand All @@ -100,6 +101,7 @@ class Page {
this.fontCache = fontCache;
this.builtInCMapCache = builtInCMapCache;
this.standardFontDataCache = standardFontDataCache;
this.globalColorSpaceCache = globalColorSpaceCache;
this.globalImageCache = globalImageCache;
this.systemFontCache = systemFontCache;
this.nonBlendModesSet = nonBlendModesSet;
Expand Down Expand Up @@ -327,6 +329,7 @@ class Page {
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
Expand Down Expand Up @@ -381,6 +384,7 @@ class Page {
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
Expand Down Expand Up @@ -446,6 +450,7 @@ class Page {
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
Expand Down Expand Up @@ -670,6 +675,7 @@ class Page {
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
Expand Down Expand Up @@ -742,6 +748,7 @@ class Page {
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
Expand Down Expand Up @@ -1632,6 +1639,7 @@ class PDFDocument {
fontCache: catalog.fontCache,
builtInCMapCache: catalog.builtInCMapCache,
standardFontDataCache: catalog.standardFontDataCache,
globalColorSpaceCache: catalog.globalColorSpaceCache,
globalImageCache: catalog.globalImageCache,
systemFontCache: catalog.systemFontCache,
nonBlendModesSet: catalog.nonBlendModesSet,
Expand Down Expand Up @@ -1731,6 +1739,7 @@ class PDFDocument {
fontCache: catalog.fontCache,
builtInCMapCache: catalog.builtInCMapCache,
standardFontDataCache: catalog.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: catalog.globalImageCache,
systemFontCache: catalog.systemFontCache,
nonBlendModesSet: catalog.nonBlendModesSet,
Expand Down
Loading

0 comments on commit 4c08c50

Please sign in to comment.