Skip to content

Commit a5f908e

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 a5f908e

9 files changed

+318
-74
lines changed

src/core/colorspace_utils.js

Lines changed: 81 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,92 @@ 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+
164+
// Only cache the parsed ColorSpace globally, by reference.
165+
if (csRef) {
166+
globalColorSpaceCache.set(/* name = */ null, csRef, parsedCS);
167+
}
168+
return parsedCS;
169+
}
170+
171+
static #parse(cs, options, topLevel = false) {
172+
const { xref, pdfFunctionFactory, globalColorSpaceCache } = options;
130173

131174
cs = xref.fetchIfRef(cs);
132175
if (cs instanceof Name) {
133176
switch (cs.name) {
134177
case "G":
135-
case "DeviceGray":
178+
case "DeviceGray": {
179+
const defaultCS = topLevel && this.#getResCS("DefaultGray", options);
180+
if (defaultCS) {
181+
return this.#defaultParse(defaultCS, options, this.gray);
182+
}
136183
return this.gray;
184+
}
137185
case "RGB":
138-
case "DeviceRGB":
186+
case "DeviceRGB": {
187+
const defaultCS = topLevel && this.#getResCS("DefaultRGB", options);
188+
if (defaultCS) {
189+
return this.#defaultParse(defaultCS, options, this.rgb);
190+
}
139191
return this.rgb;
192+
}
140193
case "DeviceRGBA":
141194
return this.rgba;
142195
case "CMYK":
143-
case "DeviceCMYK":
196+
case "DeviceCMYK": {
197+
const defaultCS = topLevel && this.#getResCS("DefaultCMYK", options);
198+
if (defaultCS) {
199+
return this.#subParse(defaultCS, options, this.cmyk);
200+
}
144201
return this.cmyk;
202+
}
145203
case "Pattern":
146204
return new PatternCS(/* baseCS = */ null);
147205
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-
}
206+
const resourcesCS = xref.fetchIfRef(this.#getResCS(cs.name, options));
207+
if (resourcesCS) {
208+
if (resourcesCS instanceof Name) {
209+
return this.#parse(resourcesCS, options);
159210
}
211+
cs = resourcesCS;
212+
break;
160213
}
161214
// Fallback to the default gray color space.
162215
warn(`Unrecognized ColorSpace: ${cs.name}`);
@@ -276,6 +329,16 @@ class ColorSpaceUtils {
276329
return this.gray;
277330
}
278331

332+
static #getResCS(name, { resources }) {
333+
if (resources instanceof Dict) {
334+
const colorSpaces = resources.get("ColorSpace");
335+
if (colorSpaces instanceof Dict) {
336+
return colorSpaces.getRaw(name) ?? null;
337+
}
338+
}
339+
return null;
340+
}
341+
279342
static get gray() {
280343
return shadow(this, "gray", new DeviceGrayCS());
281344
}

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)