Add a GlobalColorSpaceCache to reduce unnecessary re-parsing

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.
This commit is contained in:
Jonas Jenwald 2025-02-28 19:11:42 +01:00
parent 6e1cfa20d1
commit 4be79748c9
11 changed files with 311 additions and 77 deletions

View File

@ -83,14 +83,23 @@ class AnnotationFactory {
// Only necessary to prevent the `Catalog.attachments`-getter, used // Only necessary to prevent the `Catalog.attachments`-getter, used
// with "GoToE" actions, from throwing and thus breaking parsing: // with "GoToE" actions, from throwing and thus breaking parsing:
pdfManager.ensureCatalog("attachments"), pdfManager.ensureCatalog("attachments"),
pdfManager.ensureCatalog("globalColorSpaceCache"),
]).then( ]).then(
([acroForm, xfaDatasets, structTreeRoot, baseUrl, attachments]) => ({ ([
acroForm,
xfaDatasets,
structTreeRoot,
baseUrl,
attachments,
globalColorSpaceCache,
]) => ({
pdfManager, pdfManager,
acroForm: acroForm instanceof Dict ? acroForm : Dict.empty, acroForm: acroForm instanceof Dict ? acroForm : Dict.empty,
xfaDatasets, xfaDatasets,
structTreeRoot, structTreeRoot,
baseUrl, baseUrl,
attachments, attachments,
globalColorSpaceCache,
}), }),
reason => { reason => {
warn(`createGlobals: "${reason}".`); warn(`createGlobals: "${reason}".`);
@ -3880,7 +3889,7 @@ class FreeTextAnnotation extends MarkupAnnotation {
// We want to be able to add mouse listeners to the annotation. // We want to be able to add mouse listeners to the annotation.
this.data.noHTML = false; this.data.noHTML = false;
const { evaluatorOptions, xref } = params; const { annotationGlobals, evaluatorOptions, xref } = params;
this.data.annotationType = AnnotationType.FREETEXT; this.data.annotationType = AnnotationType.FREETEXT;
this.setDefaultAppearance(params); this.setDefaultAppearance(params);
this._hasAppearance = !!this.appearance; this._hasAppearance = !!this.appearance;
@ -3889,7 +3898,8 @@ class FreeTextAnnotation extends MarkupAnnotation {
const { fontColor, fontSize } = parseAppearanceStream( const { fontColor, fontSize } = parseAppearanceStream(
this.appearance, this.appearance,
evaluatorOptions, evaluatorOptions,
xref xref,
annotationGlobals.globalColorSpaceCache
); );
this.data.defaultAppearanceData.fontColor = fontColor; this.data.defaultAppearanceData.fontColor = fontColor;
this.data.defaultAppearanceData.fontSize = fontSize || 10; this.data.defaultAppearanceData.fontSize = fontSize || 10;

View File

@ -44,12 +44,12 @@ import {
RefSet, RefSet,
RefSetCache, RefSetCache,
} from "./primitives.js"; } from "./primitives.js";
import { GlobalColorSpaceCache, GlobalImageCache } from "./image_utils.js";
import { NameTree, NumberTree } from "./name_number_tree.js"; import { NameTree, NumberTree } from "./name_number_tree.js";
import { BaseStream } from "./base_stream.js"; import { BaseStream } from "./base_stream.js";
import { clearGlobalCaches } from "./cleanup_helper.js"; import { clearGlobalCaches } from "./cleanup_helper.js";
import { ColorSpace } from "./colorspace.js"; import { ColorSpace } from "./colorspace.js";
import { FileSpec } from "./file_spec.js"; import { FileSpec } from "./file_spec.js";
import { GlobalImageCache } from "./image_utils.js";
import { MetadataParser } from "./metadata_parser.js"; import { MetadataParser } from "./metadata_parser.js";
import { StructTreeRoot } from "./struct_tree.js"; import { StructTreeRoot } from "./struct_tree.js";
@ -140,6 +140,7 @@ class Catalog {
this.fontCache = new RefSetCache(); this.fontCache = new RefSetCache();
this.builtInCMapCache = new Map(); this.builtInCMapCache = new Map();
this.standardFontDataCache = new Map(); this.standardFontDataCache = new Map();
this.globalColorSpaceCache = new GlobalColorSpaceCache();
this.globalImageCache = new GlobalImageCache(); this.globalImageCache = new GlobalImageCache();
this.pageKidsCountCache = new RefSetCache(); this.pageKidsCountCache = new RefSetCache();
this.pageIndexCache = new RefSetCache(); this.pageIndexCache = new RefSetCache();
@ -1171,6 +1172,7 @@ class Catalog {
async cleanup(manuallyTriggered = false) { async cleanup(manuallyTriggered = false) {
clearGlobalCaches(); clearGlobalCaches();
this.globalColorSpaceCache.clear();
this.globalImageCache.clear(/* onlyData = */ manuallyTriggered); this.globalImageCache.clear(/* onlyData = */ manuallyTriggered);
this.pageKidsCountCache.clear(); this.pageKidsCountCache.clear();
this.pageIndexCache.clear(); this.pageIndexCache.clear();

View File

@ -306,19 +306,20 @@ class ColorSpace {
return shadow(this, "usesZeroToOneRange", true); return shadow(this, "usesZeroToOneRange", true);
} }
/** static #cache(
* @private cacheKey,
*/ xref,
static _cache(cacheKey, xref, localColorSpaceCache, parsedColorSpace) { globalColorSpaceCache,
if (!localColorSpaceCache) { localColorSpaceCache,
parsedCS
) {
if (!globalColorSpaceCache || !localColorSpaceCache) {
throw new Error( throw new Error(
'ColorSpace._cache - expected "localColorSpaceCache" argument.' 'ColorSpace.#cache - expected "globalColorSpaceCache"/"localColorSpaceCache" argument.'
); );
} }
if (!parsedColorSpace) { if (!parsedCS) {
throw new Error( throw new Error('ColorSpace.#cache - expected "parsedCS" argument.');
'ColorSpace._cache - expected "parsedColorSpace" argument.'
);
} }
let csName, csRef; let csName, csRef;
if (cacheKey instanceof Ref) { if (cacheKey instanceof Ref) {
@ -331,20 +332,31 @@ class ColorSpace {
csName = cacheKey.name; csName = cacheKey.name;
} }
if (csName || csRef) { 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) { static getCached(
if (!localColorSpaceCache) { cacheKey,
xref,
globalColorSpaceCache,
localColorSpaceCache
) {
if (!globalColorSpaceCache || !localColorSpaceCache) {
throw new Error( throw new Error(
'ColorSpace.getCached - expected "localColorSpaceCache" argument.' 'ColorSpace.getCached - expected "globalColorSpaceCache"/"localColorSpaceCache" argument.'
); );
} }
if (cacheKey instanceof Ref) { if (cacheKey instanceof Ref) {
const localColorSpace = localColorSpaceCache.getByRef(cacheKey); const cachedCS =
if (localColorSpace) { globalColorSpaceCache.getByRef(cacheKey) ||
return localColorSpace; localColorSpaceCache.getByRef(cacheKey);
if (cachedCS) {
return cachedCS;
} }
try { try {
@ -357,10 +369,7 @@ class ColorSpace {
} }
} }
if (cacheKey instanceof Name) { if (cacheKey instanceof Name) {
const localColorSpace = localColorSpaceCache.getByName(cacheKey.name); return localColorSpaceCache.getByName(cacheKey.name) || null;
if (localColorSpace) {
return localColorSpace;
}
} }
return null; return null;
} }
@ -370,26 +379,28 @@ class ColorSpace {
xref, xref,
resources = null, resources = null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}) { }) {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert( assert(
!this.getCached(cs, xref, localColorSpaceCache), !this.getCached(cs, xref, globalColorSpaceCache, localColorSpaceCache),
"Expected `ColorSpace.getCached` to have been manually checked " + "Expected `ColorSpace.getCached` to have been manually checked " +
"before calling `ColorSpace.parseAsync`." "before calling `ColorSpace.parseAsync`."
); );
} }
const parsedColorSpace = this._parse( const parsedCS = this.#parse(cs, xref, resources, pdfFunctionFactory);
cs,
xref,
resources,
pdfFunctionFactory
);
// Attempt to cache the parsed ColorSpace, by name and/or reference. // 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;
} }
static parse({ static parse({
@ -397,29 +408,33 @@ class ColorSpace {
xref, xref,
resources = null, resources = null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}) { }) {
const cachedColorSpace = this.getCached(cs, xref, localColorSpaceCache); const cachedCS = this.getCached(
if (cachedColorSpace) {
return cachedColorSpace;
}
const parsedColorSpace = this._parse(
cs, cs,
xref, xref,
resources, globalColorSpaceCache,
pdfFunctionFactory localColorSpaceCache
); );
if (cachedCS) {
return cachedCS;
}
const parsedCS = this.#parse(cs, xref, resources, pdfFunctionFactory);
// Attempt to cache the parsed ColorSpace, by name and/or reference. // 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;
} }
/** static #parse(cs, xref, resources = null, pdfFunctionFactory) {
* @private
*/
static _parse(cs, xref, resources = null, pdfFunctionFactory) {
cs = xref.fetchIfRef(cs); cs = xref.fetchIfRef(cs);
if (cs instanceof Name) { if (cs instanceof Name) {
switch (cs.name) { switch (cs.name) {
@ -443,7 +458,7 @@ class ColorSpace {
const resourcesCS = colorSpaces.get(cs.name); const resourcesCS = colorSpaces.get(cs.name);
if (resourcesCS) { if (resourcesCS) {
if (resourcesCS instanceof Name) { if (resourcesCS instanceof Name) {
return this._parse( return this.#parse(
resourcesCS, resourcesCS,
xref, xref,
resources, resources,
@ -493,7 +508,7 @@ class ColorSpace {
numComps = dict.get("N"); numComps = dict.get("N");
const alt = dict.get("Alternate"); const alt = dict.get("Alternate");
if (alt) { 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, // Ensure that the number of components are correct,
// and also (indirectly) that it is not a PatternCS. // and also (indirectly) that it is not a PatternCS.
if (altCS.numComps === numComps) { if (altCS.numComps === numComps) {
@ -512,12 +527,12 @@ class ColorSpace {
case "Pattern": case "Pattern":
baseCS = cs[1] || null; baseCS = cs[1] || null;
if (baseCS) { if (baseCS) {
baseCS = this._parse(baseCS, xref, resources, pdfFunctionFactory); baseCS = this.#parse(baseCS, xref, resources, pdfFunctionFactory);
} }
return new PatternCS(baseCS); return new PatternCS(baseCS);
case "I": case "I":
case "Indexed": 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 hiVal = Math.max(0, Math.min(xref.fetchIfRef(cs[2]), 255));
const lookup = xref.fetchIfRef(cs[3]); const lookup = xref.fetchIfRef(cs[3]);
return new IndexedCS(baseCS, hiVal, lookup); return new IndexedCS(baseCS, hiVal, lookup);
@ -525,7 +540,7 @@ class ColorSpace {
case "DeviceN": case "DeviceN":
const name = xref.fetchIfRef(cs[1]); const name = xref.fetchIfRef(cs[1]);
numComps = Array.isArray(name) ? name.length : 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]); const tintFn = pdfFunctionFactory.create(cs[3]);
return new AlternateCS(numComps, baseCS, tintFn); return new AlternateCS(numComps, baseCS, tintFn);
case "Lab": case "Lab":

View File

@ -97,11 +97,12 @@ function parseDefaultAppearance(str) {
} }
class AppearanceStreamEvaluator extends EvaluatorPreprocessor { class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
constructor(stream, evaluatorOptions, xref) { constructor(stream, evaluatorOptions, xref, globalColorSpaceCache) {
super(stream); super(stream);
this.stream = stream; this.stream = stream;
this.evaluatorOptions = evaluatorOptions; this.evaluatorOptions = evaluatorOptions;
this.xref = xref; this.xref = xref;
this.globalColorSpaceCache = globalColorSpaceCache;
this.resources = stream.dict?.get("Resources"); this.resources = stream.dict?.get("Resources");
} }
@ -161,6 +162,7 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
xref: this.xref, xref: this.xref,
resources: this.resources, resources: this.resources,
pdfFunctionFactory: this._pdfFunctionFactory, pdfFunctionFactory: this._pdfFunctionFactory,
globalColorSpaceCache: this.globalColorSpaceCache,
localColorSpaceCache: this._localColorSpaceCache, localColorSpaceCache: this._localColorSpaceCache,
}); });
break; break;
@ -210,8 +212,18 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
// Parse appearance stream to extract font and color information. // Parse appearance stream to extract font and color information.
// It returns the font properties used to render the first text object. // It returns the font properties used to render the first text object.
function parseAppearanceStream(stream, evaluatorOptions, xref) { function parseAppearanceStream(
return new AppearanceStreamEvaluator(stream, evaluatorOptions, xref).parse(); stream,
evaluatorOptions,
xref,
globalColorSpaceCache
) {
return new AppearanceStreamEvaluator(
stream,
evaluatorOptions,
xref,
globalColorSpaceCache
).parse();
} }
function getPdfColor(color, isFill) { function getPdfColor(color, isFill) {

View File

@ -87,6 +87,7 @@ class Page {
fontCache, fontCache,
builtInCMapCache, builtInCMapCache,
standardFontDataCache, standardFontDataCache,
globalColorSpaceCache,
globalImageCache, globalImageCache,
systemFontCache, systemFontCache,
nonBlendModesSet, nonBlendModesSet,
@ -100,6 +101,7 @@ class Page {
this.fontCache = fontCache; this.fontCache = fontCache;
this.builtInCMapCache = builtInCMapCache; this.builtInCMapCache = builtInCMapCache;
this.standardFontDataCache = standardFontDataCache; this.standardFontDataCache = standardFontDataCache;
this.globalColorSpaceCache = globalColorSpaceCache;
this.globalImageCache = globalImageCache; this.globalImageCache = globalImageCache;
this.systemFontCache = systemFontCache; this.systemFontCache = systemFontCache;
this.nonBlendModesSet = nonBlendModesSet; this.nonBlendModesSet = nonBlendModesSet;
@ -327,6 +329,7 @@ class Page {
fontCache: this.fontCache, fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache, builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache, standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache, globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache, systemFontCache: this.systemFontCache,
options: this.evaluatorOptions, options: this.evaluatorOptions,
@ -381,6 +384,7 @@ class Page {
fontCache: this.fontCache, fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache, builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache, standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache, globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache, systemFontCache: this.systemFontCache,
options: this.evaluatorOptions, options: this.evaluatorOptions,
@ -446,6 +450,7 @@ class Page {
fontCache: this.fontCache, fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache, builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache, standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache, globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache, systemFontCache: this.systemFontCache,
options: this.evaluatorOptions, options: this.evaluatorOptions,
@ -670,6 +675,7 @@ class Page {
fontCache: this.fontCache, fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache, builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache, standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache, globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache, systemFontCache: this.systemFontCache,
options: this.evaluatorOptions, options: this.evaluatorOptions,
@ -742,6 +748,7 @@ class Page {
fontCache: this.fontCache, fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache, builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache, standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache, globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache, systemFontCache: this.systemFontCache,
options: this.evaluatorOptions, options: this.evaluatorOptions,
@ -1632,6 +1639,7 @@ class PDFDocument {
fontCache: catalog.fontCache, fontCache: catalog.fontCache,
builtInCMapCache: catalog.builtInCMapCache, builtInCMapCache: catalog.builtInCMapCache,
standardFontDataCache: catalog.standardFontDataCache, standardFontDataCache: catalog.standardFontDataCache,
globalColorSpaceCache: catalog.globalColorSpaceCache,
globalImageCache: catalog.globalImageCache, globalImageCache: catalog.globalImageCache,
systemFontCache: catalog.systemFontCache, systemFontCache: catalog.systemFontCache,
nonBlendModesSet: catalog.nonBlendModesSet, nonBlendModesSet: catalog.nonBlendModesSet,
@ -1731,6 +1739,7 @@ class PDFDocument {
fontCache: catalog.fontCache, fontCache: catalog.fontCache,
builtInCMapCache: catalog.builtInCMapCache, builtInCMapCache: catalog.builtInCMapCache,
standardFontDataCache: catalog.standardFontDataCache, standardFontDataCache: catalog.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: catalog.globalImageCache, globalImageCache: catalog.globalImageCache,
systemFontCache: catalog.systemFontCache, systemFontCache: catalog.systemFontCache,
nonBlendModesSet: catalog.nonBlendModesSet, nonBlendModesSet: catalog.nonBlendModesSet,

View File

@ -221,6 +221,7 @@ class PartialEvaluator {
fontCache, fontCache,
builtInCMapCache, builtInCMapCache,
standardFontDataCache, standardFontDataCache,
globalColorSpaceCache,
globalImageCache, globalImageCache,
systemFontCache, systemFontCache,
options = null, options = null,
@ -232,6 +233,7 @@ class PartialEvaluator {
this.fontCache = fontCache; this.fontCache = fontCache;
this.builtInCMapCache = builtInCMapCache; this.builtInCMapCache = builtInCMapCache;
this.standardFontDataCache = standardFontDataCache; this.standardFontDataCache = standardFontDataCache;
this.globalColorSpaceCache = globalColorSpaceCache;
this.globalImageCache = globalImageCache; this.globalImageCache = globalImageCache;
this.systemFontCache = systemFontCache; this.systemFontCache = systemFontCache;
this.options = options || DefaultPartialEvaluatorOptions; this.options = options || DefaultPartialEvaluatorOptions;
@ -492,6 +494,7 @@ class PartialEvaluator {
const cachedColorSpace = ColorSpace.getCached( const cachedColorSpace = ColorSpace.getCached(
cs, cs,
this.xref, this.xref,
this.globalColorSpaceCache,
localColorSpaceCache localColorSpaceCache
); );
if (cachedColorSpace) { if (cachedColorSpace) {
@ -737,6 +740,7 @@ class PartialEvaluator {
image, image,
isInline, isInline,
pdfFunctionFactory: this._pdfFunctionFactory, pdfFunctionFactory: this._pdfFunctionFactory,
globalColorSpaceCache: this.globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
// We force the use of RGBA_32BPP images here, because we can't handle // We force the use of RGBA_32BPP images here, because we can't handle
@ -839,6 +843,7 @@ class PartialEvaluator {
image, image,
isInline, isInline,
pdfFunctionFactory: this._pdfFunctionFactory, pdfFunctionFactory: this._pdfFunctionFactory,
globalColorSpaceCache: this.globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}) })
.then(async imageObj => { .then(async imageObj => {
@ -1463,6 +1468,7 @@ class PartialEvaluator {
xref: this.xref, xref: this.xref,
resources, resources,
pdfFunctionFactory: this._pdfFunctionFactory, pdfFunctionFactory: this._pdfFunctionFactory,
globalColorSpaceCache: this.globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}).catch(reason => { }).catch(reason => {
if (reason instanceof AbortException) { if (reason instanceof AbortException) {
@ -1496,6 +1502,7 @@ class PartialEvaluator {
this.xref, this.xref,
resources, resources,
this._pdfFunctionFactory, this._pdfFunctionFactory,
this.globalColorSpaceCache,
localColorSpaceCache localColorSpaceCache
); );
patternIR = shadingFill.getIR(); patternIR = shadingFill.getIR();
@ -1977,6 +1984,7 @@ class PartialEvaluator {
const cachedColorSpace = ColorSpace.getCached( const cachedColorSpace = ColorSpace.getCached(
args[0], args[0],
xref, xref,
self.globalColorSpaceCache,
localColorSpaceCache localColorSpaceCache
); );
if (cachedColorSpace) { if (cachedColorSpace) {
@ -2002,6 +2010,7 @@ class PartialEvaluator {
const cachedColorSpace = ColorSpace.getCached( const cachedColorSpace = ColorSpace.getCached(
args[0], args[0],
xref, xref,
self.globalColorSpaceCache,
localColorSpaceCache localColorSpaceCache
); );
if (cachedColorSpace) { if (cachedColorSpace) {

View File

@ -100,6 +100,7 @@ class PDFImage {
mask = null, mask = null,
isMask = false, isMask = false,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}) { }) {
this.image = image; this.image = image;
@ -214,6 +215,7 @@ class PDFImage {
xref, xref,
resources: isInline ? res : null, resources: isInline ? res : null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
this.numComps = this.colorSpace.numComps; this.numComps = this.colorSpace.numComps;
@ -261,6 +263,7 @@ class PDFImage {
image: smask, image: smask,
isInline, isInline,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
} else if (mask) { } else if (mask) {
@ -277,6 +280,7 @@ class PDFImage {
isInline, isInline,
isMask: true, isMask: true,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
} }
@ -297,6 +301,7 @@ class PDFImage {
image, image,
isInline = false, isInline = false,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}) { }) {
const imageData = image; const imageData = image;
@ -328,6 +333,7 @@ class PDFImage {
smask: smaskData, smask: smaskData,
mask: maskData, mask: maskData,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
} }

View File

@ -169,6 +169,26 @@ class RegionalImageCache extends BaseLocalCache {
} }
} }
class GlobalColorSpaceCache extends BaseLocalCache {
constructor(options) {
super({ onlyRefs: true });
}
set(name = null, ref, data) {
if (!ref) {
throw new Error('GlobalColorSpaceCache.set - expected "ref" argument.');
}
if (this._imageCache.has(ref)) {
return;
}
this._imageCache.put(ref, data);
}
clear() {
this._imageCache.clear();
}
}
class GlobalImageCache { class GlobalImageCache {
static NUM_PAGES_THRESHOLD = 2; static NUM_PAGES_THRESHOLD = 2;
@ -290,6 +310,7 @@ class GlobalImageCache {
} }
export { export {
GlobalColorSpaceCache,
GlobalImageCache, GlobalImageCache,
LocalColorSpaceCache, LocalColorSpaceCache,
LocalFunctionCache, LocalFunctionCache,

View File

@ -52,6 +52,7 @@ class Pattern {
xref, xref,
res, res,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache localColorSpaceCache
) { ) {
const dict = shading instanceof BaseStream ? shading.dict : shading; const dict = shading instanceof BaseStream ? shading.dict : shading;
@ -66,6 +67,7 @@ class Pattern {
xref, xref,
res, res,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache localColorSpaceCache
); );
case ShadingType.FREE_FORM_MESH: case ShadingType.FREE_FORM_MESH:
@ -77,6 +79,7 @@ class Pattern {
xref, xref,
res, res,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache localColorSpaceCache
); );
default: default:
@ -114,7 +117,14 @@ class BaseShading {
// Radial and axial shading have very similar implementations // Radial and axial shading have very similar implementations
// If needed, the implementations can be broken into two classes. // If needed, the implementations can be broken into two classes.
class RadialAxialShading extends BaseShading { class RadialAxialShading extends BaseShading {
constructor(dict, xref, resources, pdfFunctionFactory, localColorSpaceCache) { constructor(
dict,
xref,
resources,
pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache
) {
super(); super();
this.shadingType = dict.get("ShadingType"); this.shadingType = dict.get("ShadingType");
let coordsLen = 0; let coordsLen = 0;
@ -132,6 +142,7 @@ class RadialAxialShading extends BaseShading {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
this.bbox = lookupNormalRect(dict.getArray("BBox"), null); this.bbox = lookupNormalRect(dict.getArray("BBox"), null);
@ -452,6 +463,7 @@ class MeshShading extends BaseShading {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache localColorSpaceCache
) { ) {
super(); super();
@ -466,6 +478,7 @@ class MeshShading extends BaseShading {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
this.background = dict.has("Background") this.background = dict.has("Background")

View File

@ -14,9 +14,12 @@
*/ */
import { Dict, Name, Ref } from "../../src/core/primitives.js"; import { Dict, Name, Ref } from "../../src/core/primitives.js";
import {
GlobalColorSpaceCache,
LocalColorSpaceCache,
} from "../../src/core/image_utils.js";
import { Stream, StringStream } from "../../src/core/stream.js"; import { Stream, StringStream } from "../../src/core/stream.js";
import { ColorSpace } from "../../src/core/colorspace.js"; import { ColorSpace } from "../../src/core/colorspace.js";
import { LocalColorSpaceCache } from "../../src/core/image_utils.js";
import { PDFFunctionFactory } from "../../src/core/function.js"; import { PDFFunctionFactory } from "../../src/core/function.js";
import { XRefMock } from "./test_utils.js"; import { XRefMock } from "./test_utils.js";
@ -50,13 +53,15 @@ describe("colorspace", function () {
}); });
describe("ColorSpace caching", function () { describe("ColorSpace caching", function () {
let localColorSpaceCache = null; let globalColorSpaceCache, localColorSpaceCache;
beforeAll(function () { beforeAll(function () {
globalColorSpaceCache = new GlobalColorSpaceCache();
localColorSpaceCache = new LocalColorSpaceCache(); localColorSpaceCache = new LocalColorSpaceCache();
}); });
afterAll(function () { afterAll(function () {
globalColorSpaceCache = null;
localColorSpaceCache = null; localColorSpaceCache = null;
}); });
@ -71,6 +76,7 @@ describe("colorspace", function () {
xref, xref,
resources: null, resources: null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
expect(colorSpace1.name).toEqual("Pattern"); expect(colorSpace1.name).toEqual("Pattern");
@ -80,6 +86,7 @@ describe("colorspace", function () {
xref, xref,
resources: null, resources: null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
expect(colorSpace2.name).toEqual("Pattern"); expect(colorSpace2.name).toEqual("Pattern");
@ -89,6 +96,7 @@ describe("colorspace", function () {
xref, xref,
resources: null, resources: null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache: new GlobalColorSpaceCache(),
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
expect(colorSpaceNonCached.name).toEqual("Pattern"); expect(colorSpaceNonCached.name).toEqual("Pattern");
@ -98,6 +106,7 @@ describe("colorspace", function () {
xref, xref,
resources: null, resources: null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
expect(colorSpaceOther.name).toEqual("DeviceRGB"); expect(colorSpaceOther.name).toEqual("DeviceRGB");
@ -140,6 +149,7 @@ describe("colorspace", function () {
xref, xref,
resources: null, resources: null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
expect(colorSpace1.name).toEqual("CalGray"); expect(colorSpace1.name).toEqual("CalGray");
@ -149,6 +159,7 @@ describe("colorspace", function () {
xref, xref,
resources: null, resources: null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
expect(colorSpace2.name).toEqual("CalGray"); expect(colorSpace2.name).toEqual("CalGray");
@ -158,6 +169,7 @@ describe("colorspace", function () {
xref, xref,
resources: null, resources: null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache: new GlobalColorSpaceCache(),
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
expect(colorSpaceNonCached.name).toEqual("CalGray"); expect(colorSpaceNonCached.name).toEqual("CalGray");
@ -167,6 +179,7 @@ describe("colorspace", function () {
xref, xref,
resources: null, resources: null,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache, localColorSpaceCache,
}); });
expect(colorSpaceOther.name).toEqual("CalRGB"); expect(colorSpaceOther.name).toEqual("CalRGB");
@ -180,6 +193,16 @@ describe("colorspace", function () {
}); });
describe("DeviceGrayCS", function () { describe("DeviceGrayCS", function () {
let globalColorSpaceCache;
beforeAll(function () {
globalColorSpaceCache = new GlobalColorSpaceCache();
});
afterAll(function () {
globalColorSpaceCache = null;
});
it("should handle the case when cs is a Name object", function () { it("should handle the case when cs is a Name object", function () {
const cs = Name.get("DeviceGray"); const cs = Name.get("DeviceGray");
const xref = new XRefMock([ const xref = new XRefMock([
@ -198,6 +221,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -249,6 +273,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -278,6 +303,16 @@ describe("colorspace", function () {
}); });
describe("DeviceRgbCS", function () { describe("DeviceRgbCS", function () {
let globalColorSpaceCache;
beforeAll(function () {
globalColorSpaceCache = new GlobalColorSpaceCache();
});
afterAll(function () {
globalColorSpaceCache = null;
});
it("should handle the case when cs is a Name object", function () { it("should handle the case when cs is a Name object", function () {
const cs = Name.get("DeviceRGB"); const cs = Name.get("DeviceRGB");
const xref = new XRefMock([ const xref = new XRefMock([
@ -296,6 +331,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -353,6 +389,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -388,6 +425,16 @@ describe("colorspace", function () {
}); });
describe("DeviceCmykCS", function () { describe("DeviceCmykCS", function () {
let globalColorSpaceCache;
beforeAll(function () {
globalColorSpaceCache = new GlobalColorSpaceCache();
});
afterAll(function () {
globalColorSpaceCache = null;
});
it("should handle the case when cs is a Name object", function () { it("should handle the case when cs is a Name object", function () {
const cs = Name.get("DeviceCMYK"); const cs = Name.get("DeviceCMYK");
const xref = new XRefMock([ const xref = new XRefMock([
@ -406,6 +453,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -463,6 +511,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -498,6 +547,16 @@ describe("colorspace", function () {
}); });
describe("CalGrayCS", function () { describe("CalGrayCS", function () {
let globalColorSpaceCache;
beforeAll(function () {
globalColorSpaceCache = new GlobalColorSpaceCache();
});
afterAll(function () {
globalColorSpaceCache = null;
});
it("should handle the case when cs is an array", function () { it("should handle the case when cs is an array", function () {
const params = new Dict(); const params = new Dict();
params.set("WhitePoint", [1, 1, 1]); params.set("WhitePoint", [1, 1, 1]);
@ -521,6 +580,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -557,6 +617,16 @@ describe("colorspace", function () {
}); });
describe("CalRGBCS", function () { describe("CalRGBCS", function () {
let globalColorSpaceCache;
beforeAll(function () {
globalColorSpaceCache = new GlobalColorSpaceCache();
});
afterAll(function () {
globalColorSpaceCache = null;
});
it("should handle the case when cs is an array", function () { it("should handle the case when cs is an array", function () {
const params = new Dict(); const params = new Dict();
params.set("WhitePoint", [1, 1, 1]); params.set("WhitePoint", [1, 1, 1]);
@ -581,6 +651,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -616,6 +687,16 @@ describe("colorspace", function () {
}); });
describe("LabCS", function () { describe("LabCS", function () {
let globalColorSpaceCache;
beforeAll(function () {
globalColorSpaceCache = new GlobalColorSpaceCache();
});
afterAll(function () {
globalColorSpaceCache = null;
});
it("should handle the case when cs is an array", function () { it("should handle the case when cs is an array", function () {
const params = new Dict(); const params = new Dict();
params.set("WhitePoint", [1, 1, 1]); params.set("WhitePoint", [1, 1, 1]);
@ -639,6 +720,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -675,6 +757,16 @@ describe("colorspace", function () {
}); });
describe("IndexedCS", function () { describe("IndexedCS", function () {
let globalColorSpaceCache;
beforeAll(function () {
globalColorSpaceCache = new GlobalColorSpaceCache();
});
afterAll(function () {
globalColorSpaceCache = null;
});
it("should handle the case when cs is an array", function () { it("should handle the case when cs is an array", function () {
// prettier-ignore // prettier-ignore
const lookup = new Stream( const lookup = new Stream(
@ -701,6 +793,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });
@ -730,6 +823,16 @@ describe("colorspace", function () {
}); });
describe("AlternateCS", function () { describe("AlternateCS", function () {
let globalColorSpaceCache;
beforeAll(function () {
globalColorSpaceCache = new GlobalColorSpaceCache();
});
afterAll(function () {
globalColorSpaceCache = null;
});
it("should handle the case when cs is an array", function () { it("should handle the case when cs is an array", function () {
const fnDict = new Dict(); const fnDict = new Dict();
fnDict.set("FunctionType", 4); fnDict.set("FunctionType", 4);
@ -769,6 +872,7 @@ describe("colorspace", function () {
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
globalColorSpaceCache,
localColorSpaceCache: new LocalColorSpaceCache(), localColorSpaceCache: new LocalColorSpaceCache(),
}); });

View File

@ -20,6 +20,7 @@ import {
} from "../../src/core/default_appearance.js"; } from "../../src/core/default_appearance.js";
import { Dict, Name } from "../../src/core/primitives.js"; import { Dict, Name } from "../../src/core/primitives.js";
import { NullStream, StringStream } from "../../src/core/stream.js"; import { NullStream, StringStream } from "../../src/core/stream.js";
import { GlobalColorSpaceCache } from "../../src/core/image_utils.js";
import { XRefMock } from "./test_utils.js"; import { XRefMock } from "./test_utils.js";
describe("Default appearance", function () { describe("Default appearance", function () {
@ -56,7 +57,7 @@ describe("Default appearance", function () {
}); });
describe("parseAppearanceStream", () => { describe("parseAppearanceStream", () => {
let evaluatorOptions, xref; let evaluatorOptions, xref, globalColorSpaceCache;
beforeAll(function () { beforeAll(function () {
evaluatorOptions = { evaluatorOptions = {
@ -64,11 +65,13 @@ describe("Default appearance", function () {
isOffscreenCanvasSupported: false, isOffscreenCanvasSupported: false,
}; };
xref = new XRefMock(); xref = new XRefMock();
globalColorSpaceCache = new GlobalColorSpaceCache();
}); });
afterAll(function () { afterAll(function () {
evaluatorOptions = null; evaluatorOptions = null;
xref = null; xref = null;
globalColorSpaceCache = null;
}); });
it("should parse a FreeText (from Acrobat) appearance", () => { it("should parse a FreeText (from Acrobat) appearance", () => {
@ -101,9 +104,14 @@ describe("Default appearance", function () {
fontName: "Helv", fontName: "Helv",
fontColor: new Uint8ClampedArray([107, 217, 41]), fontColor: new Uint8ClampedArray([107, 217, 41]),
}; };
expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( expect(
result parseAppearanceStream(
); appearance,
evaluatorOptions,
xref,
globalColorSpaceCache
)
).toEqual(result);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
@ -122,9 +130,14 @@ describe("Default appearance", function () {
fontName: "Helv", fontName: "Helv",
fontColor: new Uint8ClampedArray([237, 43, 112]), fontColor: new Uint8ClampedArray([237, 43, 112]),
}; };
expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( expect(
result parseAppearanceStream(
); appearance,
evaluatorOptions,
xref,
globalColorSpaceCache
)
).toEqual(result);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
@ -159,9 +172,14 @@ describe("Default appearance", function () {
fontName: "TT1", fontName: "TT1",
fontColor: new Uint8ClampedArray([135, 78, 254]), fontColor: new Uint8ClampedArray([135, 78, 254]),
}; };
expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( expect(
result parseAppearanceStream(
); appearance,
evaluatorOptions,
xref,
globalColorSpaceCache
)
).toEqual(result);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
@ -182,9 +200,14 @@ describe("Default appearance", function () {
fontName: "Helv", fontName: "Helv",
fontColor: new Uint8ClampedArray([16, 124, 16]), fontColor: new Uint8ClampedArray([16, 124, 16]),
}; };
expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( expect(
result parseAppearanceStream(
); appearance,
evaluatorOptions,
xref,
globalColorSpaceCache
)
).toEqual(result);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
@ -208,9 +231,14 @@ describe("Default appearance", function () {
fontName: "FXF0", fontName: "FXF0",
fontColor: new Uint8ClampedArray([149, 63, 60]), fontColor: new Uint8ClampedArray([149, 63, 60]),
}; };
expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( expect(
result parseAppearanceStream(
); appearance,
evaluatorOptions,
xref,
globalColorSpaceCache
)
).toEqual(result);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
@ -232,9 +260,14 @@ describe("Default appearance", function () {
fontName: "Invalid_font", fontName: "Invalid_font",
fontColor: new Uint8ClampedArray([0, 85, 127]), fontColor: new Uint8ClampedArray([0, 85, 127]),
}; };
expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( expect(
result parseAppearanceStream(
); appearance,
evaluatorOptions,
xref,
globalColorSpaceCache
)
).toEqual(result);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
}); });