Skip to content

Commit 140e9a4

Browse files
committed
Support /DefaultGray, /DefaultRGB, and /DefaultCMYK color spaces
Currently we don't support default color spaces at all, despite that having been included in the PDF specification for a long time; see https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#G7.1852999 Please also refer to https://github.com/pdf-association/pdf-differences/tree/main/DefaultColorSpaces
1 parent 220a289 commit 140e9a4

9 files changed

+320
-74
lines changed

src/core/colorspace_utils.js

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class ColorSpaceUtils {
7979
}
8080

8181
try {
82-
parsedCS = this.#parse(cs, options);
82+
parsedCS = this.#parse(cs, options, /* topLevel = */ true);
8383
} catch (ex) {
8484
if (asyncIfNotCached && !(ex instanceof MissingDataException)) {
8585
return Promise.reject(ex);
@@ -124,39 +124,94 @@ class ColorSpaceUtils {
124124
return parsedCS;
125125
}
126126

127-
static #parse(cs, options) {
128-
const { xref, resources, pdfFunctionFactory, globalColorSpaceCache } =
129-
options;
127+
/**
128+
* NOTE: This method should *only* be invoked from `this.#parse`,
129+
* when parsing "default" ColorSpaces (i.e. /DefaultGray, /DefaultRGB,
130+
* and /DefaultCMYK).
131+
*/
132+
static #defaultParse(cs, options, deviceCS) {
133+
const { globalColorSpaceCache } = options;
134+
let csRef, parsedCS;
135+
136+
// Check if the ColorSpace is cached first, to avoid re-parsing it.
137+
if (cs instanceof Ref) {
138+
csRef = cs;
139+
140+
const cachedCS = globalColorSpaceCache.getByRef(csRef);
141+
if (cachedCS) {
142+
return cachedCS;
143+
}
144+
}
145+
try {
146+
parsedCS = this.#parse(cs, options);
147+
} catch (ex) {
148+
if (ex instanceof MissingDataException) {
149+
throw ex;
150+
}
151+
warn(`Cannot parse default ColorSpace: "${ex}".`);
152+
return deviceCS;
153+
}
154+
155+
// The default ColorSpace must be compatible with the original one.
156+
if (parsedCS.numComps !== deviceCS.numComps) {
157+
warn(
158+
"Incorrect number of components in default ColorSpace, " +
159+
`expected "${deviceCS.numComps}" and got "${parsedCS.numComps}".`
160+
);
161+
return deviceCS;
162+
}
163+
// Ensure that any `name`-checks still work as before.
164+
parsedCS.name = deviceCS.name;
165+
166+
// Only cache the parsed ColorSpace globally, by reference.
167+
if (csRef) {
168+
globalColorSpaceCache.set(/* name = */ null, csRef, parsedCS);
169+
}
170+
return parsedCS;
171+
}
172+
173+
static #parse(cs, options, topLevel = false) {
174+
const { xref, pdfFunctionFactory, globalColorSpaceCache } = options;
130175

131176
cs = xref.fetchIfRef(cs);
132177
if (cs instanceof Name) {
133178
switch (cs.name) {
134179
case "G":
135-
case "DeviceGray":
180+
case "DeviceGray": {
181+
const defaultCS = topLevel && this.#getResCS("DefaultGray", options);
182+
if (defaultCS) {
183+
return this.#defaultParse(defaultCS, options, this.gray);
184+
}
136185
return this.gray;
186+
}
137187
case "RGB":
138-
case "DeviceRGB":
188+
case "DeviceRGB": {
189+
const defaultCS = topLevel && this.#getResCS("DefaultRGB", options);
190+
if (defaultCS) {
191+
return this.#defaultParse(defaultCS, options, this.rgb);
192+
}
139193
return this.rgb;
194+
}
140195
case "DeviceRGBA":
141196
return this.rgba;
142197
case "CMYK":
143-
case "DeviceCMYK":
198+
case "DeviceCMYK": {
199+
const defaultCS = topLevel && this.#getResCS("DefaultCMYK", options);
200+
if (defaultCS) {
201+
return this.#subParse(defaultCS, options, this.cmyk);
202+
}
144203
return this.cmyk;
204+
}
145205
case "Pattern":
146206
return new PatternCS(/* baseCS = */ null);
147207
default:
148-
if (resources instanceof Dict) {
149-
const colorSpaces = resources.get("ColorSpace");
150-
if (colorSpaces instanceof Dict) {
151-
const resourcesCS = colorSpaces.get(cs.name);
152-
if (resourcesCS) {
153-
if (resourcesCS instanceof Name) {
154-
return this.#parse(resourcesCS, options);
155-
}
156-
cs = resourcesCS;
157-
break;
158-
}
208+
const resourcesCS = xref.fetchIfRef(this.#getResCS(cs.name, options));
209+
if (resourcesCS) {
210+
if (resourcesCS instanceof Name) {
211+
return this.#parse(resourcesCS, options);
159212
}
213+
cs = resourcesCS;
214+
break;
160215
}
161216
// Fallback to the default gray color space.
162217
warn(`Unrecognized ColorSpace: ${cs.name}`);
@@ -276,6 +331,16 @@ class ColorSpaceUtils {
276331
return this.gray;
277332
}
278333

334+
static #getResCS(name, { resources }) {
335+
if (resources instanceof Dict) {
336+
const colorSpaces = resources.get("ColorSpace");
337+
if (colorSpaces instanceof Dict) {
338+
return colorSpaces.getRaw(name) ?? null;
339+
}
340+
}
341+
return null;
342+
}
343+
279344
static get gray() {
280345
return shadow(this, "gray", new DeviceGrayCS());
281346
}

src/core/default_appearance.js

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
119119
fontColor: /* black = */ new Uint8ClampedArray(3),
120120
fillColorSpace: ColorSpaceUtils.gray,
121121
};
122-
let breakLoop = false;
122+
let breakLoop = false,
123+
cs = null;
123124
const stack = [];
124125

125126
try {
@@ -157,27 +158,29 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
157158
}
158159
break;
159160
case OPS.setFillColorSpace:
160-
result.fillColorSpace = ColorSpaceUtils.parse({
161-
cs: args[0],
162-
xref: this.xref,
163-
resources: this.resources,
164-
pdfFunctionFactory: this._pdfFunctionFactory,
165-
globalColorSpaceCache: this.globalColorSpaceCache,
166-
localColorSpaceCache: this._localColorSpaceCache,
167-
});
161+
result.fillColorSpace = this.#parseColorSpace(args[0]);
168162
break;
169163
case OPS.setFillColor:
170-
const cs = result.fillColorSpace;
164+
cs = result.fillColorSpace;
171165
cs.getRgbItem(args, 0, result.fontColor, 0);
172166
break;
173167
case OPS.setFillRGBColor:
174-
ColorSpaceUtils.rgb.getRgbItem(args, 0, result.fontColor, 0);
168+
cs = result.fillColorSpace = this.#parseColorSpace(
169+
Name.get("DeviceRGB")
170+
);
171+
cs.getRgbItem(args, 0, result.fontColor, 0);
175172
break;
176173
case OPS.setFillGray:
177-
ColorSpaceUtils.gray.getRgbItem(args, 0, result.fontColor, 0);
174+
cs = result.fillColorSpace = this.#parseColorSpace(
175+
Name.get("DeviceGray")
176+
);
177+
cs.getRgbItem(args, 0, result.fontColor, 0);
178178
break;
179179
case OPS.setFillCMYKColor:
180-
ColorSpaceUtils.cmyk.getRgbItem(args, 0, result.fontColor, 0);
180+
cs = result.fillColorSpace = this.#parseColorSpace(
181+
Name.get("DeviceCMYK")
182+
);
183+
cs.getRgbItem(args, 0, result.fontColor, 0);
181184
break;
182185
case OPS.showText:
183186
case OPS.showSpacedText:
@@ -208,6 +211,17 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
208211
});
209212
return shadow(this, "_pdfFunctionFactory", pdfFunctionFactory);
210213
}
214+
215+
#parseColorSpace(cs) {
216+
return ColorSpaceUtils.parse({
217+
cs,
218+
xref: this.xref,
219+
resources: this.resources,
220+
pdfFunctionFactory: this._pdfFunctionFactory,
221+
globalColorSpaceCache: this.globalColorSpaceCache,
222+
localColorSpaceCache: this._localColorSpaceCache,
223+
});
224+
}
211225
}
212226

213227
// Parse appearance stream to extract font and color information.

0 commit comments

Comments
 (0)