Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a GlobalColorSpaceCache to reduce unnecessary re-parsing #19583

Merged
merged 1 commit into from
Mar 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
109 changes: 62 additions & 47 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,10 +369,7 @@ class ColorSpace {
}
}
if (cacheKey instanceof Name) {
const localColorSpace = localColorSpaceCache.getByName(cacheKey.name);
if (localColorSpace) {
return localColorSpace;
}
return localColorSpaceCache.getByName(cacheKey.name) || null;
}
return null;
}
Expand All @@ -370,56 +379,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 +458,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 +508,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 +527,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