Improve parseAppearanceStream to handle more "complex" ColorSpaces

The existing code is unable to *correctly* extract the color from the appearance-stream when the ColorSpace-data is "complex". To reproduce this:
 - Open `freetexts.pdf` in the viewer.
 - Note the purple color of the "Hello World from Preview" annotation.
 - Enable any of the Editors.
 - Note how the relevant annotation is now black.
This commit is contained in:
Jonas Jenwald 2023-07-06 13:03:11 +02:00
parent 8281bb8858
commit 6442a6cc4e
4 changed files with 201 additions and 42 deletions

View File

@ -148,8 +148,7 @@ class AnnotationFactory {
needAppearances: needAppearances:
!collectFields && acroFormDict.get("NeedAppearances") === true, !collectFields && acroFormDict.get("NeedAppearances") === true,
pageIndex, pageIndex,
isOffscreenCanvasSupported: evaluatorOptions: pdfManager.evaluatorOptions,
pdfManager.evaluatorOptions.isOffscreenCanvasSupported,
}; };
switch (subtype) { switch (subtype) {
@ -341,7 +340,7 @@ class AnnotationFactory {
xref, xref,
annotation, annotation,
dependencies, dependencies,
image { image }
) )
); );
break; break;
@ -364,8 +363,7 @@ class AnnotationFactory {
return null; return null;
} }
const xref = evaluator.xref; const { options, xref } = evaluator;
const { isOffscreenCanvasSupported } = evaluator.options;
const promises = []; const promises = [];
for (const annotation of annotations) { for (const annotation of annotations) {
if (annotation.deleted) { if (annotation.deleted) {
@ -377,19 +375,19 @@ class AnnotationFactory {
FreeTextAnnotation.createNewPrintAnnotation(xref, annotation, { FreeTextAnnotation.createNewPrintAnnotation(xref, annotation, {
evaluator, evaluator,
task, task,
isOffscreenCanvasSupported, evaluatorOptions: options,
}) })
); );
break; break;
case AnnotationEditorType.INK: case AnnotationEditorType.INK:
promises.push( promises.push(
InkAnnotation.createNewPrintAnnotation(xref, annotation, { InkAnnotation.createNewPrintAnnotation(xref, annotation, {
isOffscreenCanvasSupported, evaluatorOptions: options,
}) })
); );
break; break;
case AnnotationEditorType.STAMP: case AnnotationEditorType.STAMP:
if (!isOffscreenCanvasSupported) { if (!options.isOffscreenCanvasSupported) {
break; break;
} }
const image = await imagePromises.get(annotation.bitmapId); const image = await imagePromises.get(annotation.bitmapId);
@ -402,7 +400,10 @@ class AnnotationFactory {
image.imageStream = image.smaskStream = null; image.imageStream = image.smaskStream = null;
} }
promises.push( promises.push(
StampAnnotation.createNewPrintAnnotation(xref, annotation, image) StampAnnotation.createNewPrintAnnotation(xref, annotation, {
image,
evaluatorOptions: options,
})
); );
break; break;
} }
@ -600,7 +601,8 @@ class Annotation {
this.data.pageIndex = params.pageIndex; this.data.pageIndex = params.pageIndex;
} }
this._isOffscreenCanvasSupported = params.isOffscreenCanvasSupported; this._isOffscreenCanvasSupported =
params.evaluatorOptions.isOffscreenCanvasSupported;
this._fallbackFontDict = null; this._fallbackFontDict = null;
this._needAppearances = false; this._needAppearances = false;
} }
@ -1587,7 +1589,7 @@ class MarkupAnnotation extends Annotation {
const newAnnotation = new this.prototype.constructor({ const newAnnotation = new this.prototype.constructor({
dict: annotationDict, dict: annotationDict,
xref, xref,
isOffscreenCanvasSupported: params.isOffscreenCanvasSupported, evaluatorOptions: params.evaluatorOptions,
}); });
if (annotation.ref) { if (annotation.ref) {
@ -3648,11 +3650,15 @@ class FreeTextAnnotation extends MarkupAnnotation {
this.data.hasOwnCanvas = true; this.data.hasOwnCanvas = true;
const { xref } = params; const { evaluatorOptions, xref } = params;
this.data.annotationType = AnnotationType.FREETEXT; this.data.annotationType = AnnotationType.FREETEXT;
this.setDefaultAppearance(params); this.setDefaultAppearance(params);
if (this.appearance) { if (this.appearance) {
const { fontColor, fontSize } = parseAppearanceStream(this.appearance); const { fontColor, fontSize } = parseAppearanceStream(
this.appearance,
evaluatorOptions,
xref
);
this.data.defaultAppearanceData.fontColor = fontColor; this.data.defaultAppearanceData.fontColor = fontColor;
this.data.defaultAppearanceData.fontSize = fontSize || 10; this.data.defaultAppearanceData.fontSize = fontSize || 10;
} else if (this._isOffscreenCanvasSupported) { } else if (this._isOffscreenCanvasSupported) {
@ -4570,7 +4576,7 @@ class StampAnnotation extends MarkupAnnotation {
static async createNewAppearanceStream(annotation, xref, params) { static async createNewAppearanceStream(annotation, xref, params) {
const { rotation } = annotation; const { rotation } = annotation;
const { imageRef, width, height } = params; const { imageRef, width, height } = params.image;
const resources = new Dict(xref); const resources = new Dict(xref);
const xobject = new Dict(xref); const xobject = new Dict(xref);
resources.set("XObject", xobject); resources.set("XObject", xobject);

View File

@ -20,9 +20,17 @@ import {
numberToString, numberToString,
stringToUTF16HexString, stringToUTF16HexString,
} from "./core_utils.js"; } from "./core_utils.js";
import { LINE_DESCENT_FACTOR, LINE_FACTOR, OPS, warn } from "../shared/util.js"; import {
LINE_DESCENT_FACTOR,
LINE_FACTOR,
OPS,
shadow,
warn,
} from "../shared/util.js";
import { ColorSpace } from "./colorspace.js"; import { ColorSpace } from "./colorspace.js";
import { EvaluatorPreprocessor } from "./evaluator.js"; import { EvaluatorPreprocessor } from "./evaluator.js";
import { LocalColorSpaceCache } from "./image_utils.js";
import { PDFFunctionFactory } from "./function.js";
import { StringStream } from "./stream.js"; import { StringStream } from "./stream.js";
class DefaultAppearanceEvaluator extends EvaluatorPreprocessor { class DefaultAppearanceEvaluator extends EvaluatorPreprocessor {
@ -88,9 +96,13 @@ function parseDefaultAppearance(str) {
} }
class AppearanceStreamEvaluator extends EvaluatorPreprocessor { class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
constructor(stream) { constructor(stream, evaluatorOptions, xref) {
super(stream); super(stream);
this.stream = stream; this.stream = stream;
this.evaluatorOptions = evaluatorOptions;
this.xref = xref;
this.resources = stream.dict?.get("Resources");
} }
parse() { parse() {
@ -103,6 +115,7 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
fontSize: 0, fontSize: 0,
fontName: "", fontName: "",
fontColor: /* black = */ new Uint8ClampedArray(3), fontColor: /* black = */ new Uint8ClampedArray(3),
fillColorSpace: ColorSpace.singletons.gray,
}; };
let breakLoop = false; let breakLoop = false;
const stack = []; const stack = [];
@ -123,6 +136,7 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
fontSize: result.fontSize, fontSize: result.fontSize,
fontName: result.fontName, fontName: result.fontName,
fontColor: result.fontColor.slice(), fontColor: result.fontColor.slice(),
fillColorSpace: result.fillColorSpace,
}); });
break; break;
case OPS.restore: case OPS.restore:
@ -140,6 +154,19 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
result.fontSize = fontSize * result.scaleFactor; result.fontSize = fontSize * result.scaleFactor;
} }
break; break;
case OPS.setFillColorSpace:
result.fillColorSpace = ColorSpace.parse({
cs: args[0],
xref: this.xref,
resources: this.resources,
pdfFunctionFactory: this._pdfFunctionFactory,
localColorSpaceCache: this._localColorSpaceCache,
});
break;
case OPS.setFillColor:
const cs = result.fillColorSpace;
cs.getRgbItem(args, 0, result.fontColor, 0);
break;
case OPS.setFillRGBColor: case OPS.setFillRGBColor:
ColorSpace.singletons.rgb.getRgbItem(args, 0, result.fontColor, 0); ColorSpace.singletons.rgb.getRgbItem(args, 0, result.fontColor, 0);
break; break;
@ -162,15 +189,28 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor {
} }
this.stream.reset(); this.stream.reset();
delete result.scaleFactor; delete result.scaleFactor;
delete result.fillColorSpace;
return result; return result;
} }
get _localColorSpaceCache() {
return shadow(this, "_localColorSpaceCache", new LocalColorSpaceCache());
}
get _pdfFunctionFactory() {
const pdfFunctionFactory = new PDFFunctionFactory({
xref: this.xref,
isEvalSupported: this.evaluatorOptions.isEvalSupported,
});
return shadow(this, "_pdfFunctionFactory", pdfFunctionFactory);
}
} }
// 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) { function parseAppearanceStream(stream, evaluatorOptions, xref) {
return new AppearanceStreamEvaluator(stream).parse(); return new AppearanceStreamEvaluator(stream, evaluatorOptions, xref).parse();
} }
function getPdfColor(color, isFill) { function getPdfColor(color, isFill) {

View File

@ -57,6 +57,7 @@ describe("annotation", function () {
}, },
}; };
this.evaluatorOptions = { this.evaluatorOptions = {
isEvalSupported: true,
isOffscreenCanvasSupported: false, isOffscreenCanvasSupported: false,
}; };
} }
@ -314,35 +315,55 @@ describe("annotation", function () {
}); });
it("should set and get valid contents", function () { it("should set and get valid contents", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setContents("Foo bar baz"); annotation.setContents("Foo bar baz");
expect(annotation._contents).toEqual({ str: "Foo bar baz", dir: "ltr" }); expect(annotation._contents).toEqual({ str: "Foo bar baz", dir: "ltr" });
}); });
it("should not set and get invalid contents", function () { it("should not set and get invalid contents", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setContents(undefined); annotation.setContents(undefined);
expect(annotation._contents).toEqual({ str: "", dir: "ltr" }); expect(annotation._contents).toEqual({ str: "", dir: "ltr" });
}); });
it("should set and get a valid modification date", function () { it("should set and get a valid modification date", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setModificationDate("D:20190422"); annotation.setModificationDate("D:20190422");
expect(annotation.modificationDate).toEqual("D:20190422"); expect(annotation.modificationDate).toEqual("D:20190422");
}); });
it("should not set and get an invalid modification date", function () { it("should not set and get an invalid modification date", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setModificationDate(undefined); annotation.setModificationDate(undefined);
expect(annotation.modificationDate).toEqual(null); expect(annotation.modificationDate).toEqual(null);
}); });
it("should set and get flags", function () { it("should set and get flags", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setFlags(13); annotation.setFlags(13);
expect(annotation.hasFlag(AnnotationFlag.INVISIBLE)).toEqual(true); expect(annotation.hasFlag(AnnotationFlag.INVISIBLE)).toEqual(true);
@ -353,63 +374,99 @@ describe("annotation", function () {
}); });
it("should be viewable and not printable by default", function () { it("should be viewable and not printable by default", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
expect(annotation.viewable).toEqual(true); expect(annotation.viewable).toEqual(true);
expect(annotation.printable).toEqual(false); expect(annotation.printable).toEqual(false);
}); });
it("should set and get a valid rectangle", function () { it("should set and get a valid rectangle", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setRectangle([117, 694, 164.298, 720]); annotation.setRectangle([117, 694, 164.298, 720]);
expect(annotation.rectangle).toEqual([117, 694, 164.298, 720]); expect(annotation.rectangle).toEqual([117, 694, 164.298, 720]);
}); });
it("should not set and get an invalid rectangle", function () { it("should not set and get an invalid rectangle", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setRectangle([117, 694, 164.298]); annotation.setRectangle([117, 694, 164.298]);
expect(annotation.rectangle).toEqual([0, 0, 0, 0]); expect(annotation.rectangle).toEqual([0, 0, 0, 0]);
}); });
it("should reject a color if it is not an array", function () { it("should reject a color if it is not an array", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setColor("red"); annotation.setColor("red");
expect(annotation.color).toEqual(new Uint8ClampedArray([0, 0, 0])); expect(annotation.color).toEqual(new Uint8ClampedArray([0, 0, 0]));
}); });
it("should set and get a transparent color", function () { it("should set and get a transparent color", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setColor([]); annotation.setColor([]);
expect(annotation.color).toEqual(null); expect(annotation.color).toEqual(null);
}); });
it("should set and get a grayscale color", function () { it("should set and get a grayscale color", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setColor([0.4]); annotation.setColor([0.4]);
expect(annotation.color).toEqual(new Uint8ClampedArray([102, 102, 102])); expect(annotation.color).toEqual(new Uint8ClampedArray([102, 102, 102]));
}); });
it("should set and get an RGB color", function () { it("should set and get an RGB color", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setColor([0, 0, 1]); annotation.setColor([0, 0, 1]);
expect(annotation.color).toEqual(new Uint8ClampedArray([0, 0, 255])); expect(annotation.color).toEqual(new Uint8ClampedArray([0, 0, 255]));
}); });
it("should set and get a CMYK color", function () { it("should set and get a CMYK color", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setColor([0.1, 0.92, 0.84, 0.02]); annotation.setColor([0.1, 0.92, 0.84, 0.02]);
expect(annotation.color).toEqual(new Uint8ClampedArray([234, 59, 48])); expect(annotation.color).toEqual(new Uint8ClampedArray([234, 59, 48]));
}); });
it("should not set and get an invalid color", function () { it("should not set and get an invalid color", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
annotation.setColor([0.4, 0.6]); annotation.setColor([0.4, 0.6]);
expect(annotation.color).toEqual(new Uint8ClampedArray([0, 0, 0])); expect(annotation.color).toEqual(new Uint8ClampedArray([0, 0, 0]));
@ -514,14 +571,22 @@ describe("annotation", function () {
}); });
it("should set and get a valid creation date", function () { it("should set and get a valid creation date", function () {
const markupAnnotation = new MarkupAnnotation({ dict, ref }); const markupAnnotation = new MarkupAnnotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
markupAnnotation.setCreationDate("D:20190422"); markupAnnotation.setCreationDate("D:20190422");
expect(markupAnnotation.creationDate).toEqual("D:20190422"); expect(markupAnnotation.creationDate).toEqual("D:20190422");
}); });
it("should not set and get an invalid creation date", function () { it("should not set and get an invalid creation date", function () {
const markupAnnotation = new MarkupAnnotation({ dict, ref }); const markupAnnotation = new MarkupAnnotation({
dict,
ref,
evaluatorOptions: pdfManagerMock.evaluatorOptions,
});
markupAnnotation.setCreationDate(undefined); markupAnnotation.setCreationDate(undefined);
expect(markupAnnotation.creationDate).toEqual(null); expect(markupAnnotation.creationDate).toEqual(null);

View File

@ -18,7 +18,9 @@ import {
parseAppearanceStream, parseAppearanceStream,
parseDefaultAppearance, parseDefaultAppearance,
} from "../../src/core/default_appearance.js"; } from "../../src/core/default_appearance.js";
import { StringStream } from "../../src/core/stream.js"; import { Dict, Name } from "../../src/core/primitives.js";
import { NullStream, StringStream } from "../../src/core/stream.js";
import { XRefMock } from "./test_utils.js";
describe("Default appearance", function () { describe("Default appearance", function () {
describe("parseDefaultAppearance and createDefaultAppearance", function () { describe("parseDefaultAppearance and createDefaultAppearance", function () {
@ -54,6 +56,21 @@ describe("Default appearance", function () {
}); });
describe("parseAppearanceStream", () => { describe("parseAppearanceStream", () => {
let evaluatorOptions, xref;
beforeAll(function () {
evaluatorOptions = {
isEvalSupported: true,
isOffscreenCanvasSupported: false,
};
xref = new XRefMock();
});
afterAll(function () {
evaluatorOptions = null;
xref = null;
});
it("should parse a FreeText (from Acrobat) appearance", () => { it("should parse a FreeText (from Acrobat) appearance", () => {
const appearance = new StringStream(` const appearance = new StringStream(`
0 w 0 w
@ -84,7 +101,9 @@ describe("Default appearance", function () {
fontName: "Helv", fontName: "Helv",
fontColor: new Uint8ClampedArray([107, 217, 41]), fontColor: new Uint8ClampedArray([107, 217, 41]),
}; };
expect(parseAppearanceStream(appearance)).toEqual(result); expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual(
result
);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
@ -103,23 +122,46 @@ describe("Default appearance", function () {
fontName: "Helv", fontName: "Helv",
fontColor: new Uint8ClampedArray([237, 43, 112]), fontColor: new Uint8ClampedArray([237, 43, 112]),
}; };
expect(parseAppearanceStream(appearance)).toEqual(result); expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual(
result
);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
it("should parse a FreeText (from Preview) appearance", () => { it("should parse a FreeText (from Preview) appearance", () => {
const indexedDict = new Dict(xref);
indexedDict.set("Alternate", Name.get("DeviceRGB"));
indexedDict.set("N", 3);
indexedDict.set("Length", 0);
const indexedStream = new NullStream();
indexedStream.dict = indexedDict;
const colorSpaceDict = new Dict(xref);
colorSpaceDict.set("Cs1", [Name.get("ICCBased"), indexedStream]);
const resourcesDict = new Dict(xref);
resourcesDict.set("ColorSpace", colorSpaceDict);
const appearanceDict = new Dict(xref);
appearanceDict.set("Resources", resourcesDict);
const appearance = new StringStream(` const appearance = new StringStream(`
q Q q 2.128482 2.128482 247.84 26 re W n /Cs1 cs 0.52799 0.3071 0.99498 sc q Q q 2.128482 2.128482 247.84 26 re W n /Cs1 cs 0.52799 0.3071 0.99498 sc
q 1 0 0 -1 -108.3364 459.8485 cm BT 22.00539 0 0 -22.00539 110.5449 452.72 q 1 0 0 -1 -108.3364 459.8485 cm BT 22.00539 0 0 -22.00539 110.5449 452.72
Tm /TT1 1 Tf [ (H) -0.2 (e) -0.2 (l) -0.2 (l) -0.2 (o) -0.2 ( ) 0.2 (W) 17.7 Tm /TT1 1 Tf [ (H) -0.2 (e) -0.2 (l) -0.2 (l) -0.2 (o) -0.2 ( ) 0.2 (W) 17.7
(o) -0.2 (rl) -0.2 (d) -0.2 ( ) 0.2 (f) 0.2 (ro) -0.2 (m ) 0.2 (Pre) -0.2 (o) -0.2 (rl) -0.2 (d) -0.2 ( ) 0.2 (f) 0.2 (ro) -0.2 (m ) 0.2 (Pre) -0.2
(vi) -0.2 (e) -0.2 (w) ] TJ ET Q Q`); (vi) -0.2 (e) -0.2 (w) ] TJ ET Q Q`);
appearance.dict = appearanceDict;
const result = { const result = {
fontSize: 22.00539, fontSize: 22.00539,
fontName: "TT1", fontName: "TT1",
fontColor: new Uint8ClampedArray([0, 0, 0]), fontColor: new Uint8ClampedArray([135, 78, 254]),
}; };
expect(parseAppearanceStream(appearance)).toEqual(result); expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual(
result
);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
@ -140,7 +182,9 @@ describe("Default appearance", function () {
fontName: "Helv", fontName: "Helv",
fontColor: new Uint8ClampedArray([16, 124, 16]), fontColor: new Uint8ClampedArray([16, 124, 16]),
}; };
expect(parseAppearanceStream(appearance)).toEqual(result); expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual(
result
);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
@ -164,7 +208,9 @@ describe("Default appearance", function () {
fontName: "FXF0", fontName: "FXF0",
fontColor: new Uint8ClampedArray([149, 63, 60]), fontColor: new Uint8ClampedArray([149, 63, 60]),
}; };
expect(parseAppearanceStream(appearance)).toEqual(result); expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual(
result
);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
@ -186,7 +232,9 @@ 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)).toEqual(result); expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual(
result
);
expect(appearance.pos).toEqual(0); expect(appearance.pos).toEqual(0);
}); });
}); });