diff --git a/src/core/annotation.js b/src/core/annotation.js index dfad8de36..a6f658e80 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -148,8 +148,7 @@ class AnnotationFactory { needAppearances: !collectFields && acroFormDict.get("NeedAppearances") === true, pageIndex, - isOffscreenCanvasSupported: - pdfManager.evaluatorOptions.isOffscreenCanvasSupported, + evaluatorOptions: pdfManager.evaluatorOptions, }; switch (subtype) { @@ -341,7 +340,7 @@ class AnnotationFactory { xref, annotation, dependencies, - image + { image } ) ); break; @@ -364,8 +363,7 @@ class AnnotationFactory { return null; } - const xref = evaluator.xref; - const { isOffscreenCanvasSupported } = evaluator.options; + const { options, xref } = evaluator; const promises = []; for (const annotation of annotations) { if (annotation.deleted) { @@ -377,19 +375,19 @@ class AnnotationFactory { FreeTextAnnotation.createNewPrintAnnotation(xref, annotation, { evaluator, task, - isOffscreenCanvasSupported, + evaluatorOptions: options, }) ); break; case AnnotationEditorType.INK: promises.push( InkAnnotation.createNewPrintAnnotation(xref, annotation, { - isOffscreenCanvasSupported, + evaluatorOptions: options, }) ); break; case AnnotationEditorType.STAMP: - if (!isOffscreenCanvasSupported) { + if (!options.isOffscreenCanvasSupported) { break; } const image = await imagePromises.get(annotation.bitmapId); @@ -402,7 +400,10 @@ class AnnotationFactory { image.imageStream = image.smaskStream = null; } promises.push( - StampAnnotation.createNewPrintAnnotation(xref, annotation, image) + StampAnnotation.createNewPrintAnnotation(xref, annotation, { + image, + evaluatorOptions: options, + }) ); break; } @@ -600,7 +601,8 @@ class Annotation { this.data.pageIndex = params.pageIndex; } - this._isOffscreenCanvasSupported = params.isOffscreenCanvasSupported; + this._isOffscreenCanvasSupported = + params.evaluatorOptions.isOffscreenCanvasSupported; this._fallbackFontDict = null; this._needAppearances = false; } @@ -1587,7 +1589,7 @@ class MarkupAnnotation extends Annotation { const newAnnotation = new this.prototype.constructor({ dict: annotationDict, xref, - isOffscreenCanvasSupported: params.isOffscreenCanvasSupported, + evaluatorOptions: params.evaluatorOptions, }); if (annotation.ref) { @@ -3648,11 +3650,15 @@ class FreeTextAnnotation extends MarkupAnnotation { this.data.hasOwnCanvas = true; - const { xref } = params; + const { evaluatorOptions, xref } = params; this.data.annotationType = AnnotationType.FREETEXT; this.setDefaultAppearance(params); 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.fontSize = fontSize || 10; } else if (this._isOffscreenCanvasSupported) { @@ -4570,7 +4576,7 @@ class StampAnnotation extends MarkupAnnotation { static async createNewAppearanceStream(annotation, xref, params) { const { rotation } = annotation; - const { imageRef, width, height } = params; + const { imageRef, width, height } = params.image; const resources = new Dict(xref); const xobject = new Dict(xref); resources.set("XObject", xobject); diff --git a/src/core/default_appearance.js b/src/core/default_appearance.js index 7dcf94857..fc1635104 100644 --- a/src/core/default_appearance.js +++ b/src/core/default_appearance.js @@ -20,9 +20,17 @@ import { numberToString, stringToUTF16HexString, } 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 { EvaluatorPreprocessor } from "./evaluator.js"; +import { LocalColorSpaceCache } from "./image_utils.js"; +import { PDFFunctionFactory } from "./function.js"; import { StringStream } from "./stream.js"; class DefaultAppearanceEvaluator extends EvaluatorPreprocessor { @@ -88,9 +96,13 @@ function parseDefaultAppearance(str) { } class AppearanceStreamEvaluator extends EvaluatorPreprocessor { - constructor(stream) { + constructor(stream, evaluatorOptions, xref) { super(stream); this.stream = stream; + this.evaluatorOptions = evaluatorOptions; + this.xref = xref; + + this.resources = stream.dict?.get("Resources"); } parse() { @@ -103,6 +115,7 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { fontSize: 0, fontName: "", fontColor: /* black = */ new Uint8ClampedArray(3), + fillColorSpace: ColorSpace.singletons.gray, }; let breakLoop = false; const stack = []; @@ -123,6 +136,7 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { fontSize: result.fontSize, fontName: result.fontName, fontColor: result.fontColor.slice(), + fillColorSpace: result.fillColorSpace, }); break; case OPS.restore: @@ -140,6 +154,19 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { result.fontSize = fontSize * result.scaleFactor; } 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: ColorSpace.singletons.rgb.getRgbItem(args, 0, result.fontColor, 0); break; @@ -162,15 +189,28 @@ class AppearanceStreamEvaluator extends EvaluatorPreprocessor { } this.stream.reset(); delete result.scaleFactor; + delete result.fillColorSpace; 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. // It returns the font properties used to render the first text object. -function parseAppearanceStream(stream) { - return new AppearanceStreamEvaluator(stream).parse(); +function parseAppearanceStream(stream, evaluatorOptions, xref) { + return new AppearanceStreamEvaluator(stream, evaluatorOptions, xref).parse(); } function getPdfColor(color, isFill) { diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 9de6b4a72..b00dae18e 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -57,6 +57,7 @@ describe("annotation", function () { }, }; this.evaluatorOptions = { + isEvalSupported: true, isOffscreenCanvasSupported: false, }; } @@ -314,35 +315,55 @@ describe("annotation", 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"); expect(annotation._contents).toEqual({ str: "Foo bar baz", dir: "ltr" }); }); 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); expect(annotation._contents).toEqual({ str: "", dir: "ltr" }); }); 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"); expect(annotation.modificationDate).toEqual("D:20190422"); }); 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); expect(annotation.modificationDate).toEqual(null); }); 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); expect(annotation.hasFlag(AnnotationFlag.INVISIBLE)).toEqual(true); @@ -353,63 +374,99 @@ describe("annotation", 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.printable).toEqual(false); }); 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]); expect(annotation.rectangle).toEqual([117, 694, 164.298, 720]); }); 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]); expect(annotation.rectangle).toEqual([0, 0, 0, 0]); }); 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"); expect(annotation.color).toEqual(new Uint8ClampedArray([0, 0, 0])); }); 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([]); expect(annotation.color).toEqual(null); }); 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]); expect(annotation.color).toEqual(new Uint8ClampedArray([102, 102, 102])); }); 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]); expect(annotation.color).toEqual(new Uint8ClampedArray([0, 0, 255])); }); 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]); expect(annotation.color).toEqual(new Uint8ClampedArray([234, 59, 48])); }); 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]); 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 () { - const markupAnnotation = new MarkupAnnotation({ dict, ref }); + const markupAnnotation = new MarkupAnnotation({ + dict, + ref, + evaluatorOptions: pdfManagerMock.evaluatorOptions, + }); markupAnnotation.setCreationDate("D:20190422"); expect(markupAnnotation.creationDate).toEqual("D:20190422"); }); 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); expect(markupAnnotation.creationDate).toEqual(null); diff --git a/test/unit/default_appearance_spec.js b/test/unit/default_appearance_spec.js index 53b49ed5b..94a844921 100644 --- a/test/unit/default_appearance_spec.js +++ b/test/unit/default_appearance_spec.js @@ -18,7 +18,9 @@ import { parseAppearanceStream, parseDefaultAppearance, } 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("parseDefaultAppearance and createDefaultAppearance", function () { @@ -54,6 +56,21 @@ describe("Default appearance", function () { }); 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", () => { const appearance = new StringStream(` 0 w @@ -84,7 +101,9 @@ describe("Default appearance", function () { fontName: "Helv", fontColor: new Uint8ClampedArray([107, 217, 41]), }; - expect(parseAppearanceStream(appearance)).toEqual(result); + expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( + result + ); expect(appearance.pos).toEqual(0); }); @@ -103,23 +122,46 @@ describe("Default appearance", function () { fontName: "Helv", fontColor: new Uint8ClampedArray([237, 43, 112]), }; - expect(parseAppearanceStream(appearance)).toEqual(result); + expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( + result + ); expect(appearance.pos).toEqual(0); }); 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(` 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 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 (vi) -0.2 (e) -0.2 (w) ] TJ ET Q Q`); + appearance.dict = appearanceDict; + const result = { fontSize: 22.00539, 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); }); @@ -140,7 +182,9 @@ describe("Default appearance", function () { fontName: "Helv", fontColor: new Uint8ClampedArray([16, 124, 16]), }; - expect(parseAppearanceStream(appearance)).toEqual(result); + expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( + result + ); expect(appearance.pos).toEqual(0); }); @@ -164,7 +208,9 @@ describe("Default appearance", function () { fontName: "FXF0", fontColor: new Uint8ClampedArray([149, 63, 60]), }; - expect(parseAppearanceStream(appearance)).toEqual(result); + expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( + result + ); expect(appearance.pos).toEqual(0); }); @@ -186,7 +232,9 @@ describe("Default appearance", function () { fontName: "Invalid_font", fontColor: new Uint8ClampedArray([0, 85, 127]), }; - expect(parseAppearanceStream(appearance)).toEqual(result); + expect(parseAppearanceStream(appearance, evaluatorOptions, xref)).toEqual( + result + ); expect(appearance.pos).toEqual(0); }); });