From 04bb59bf9c691f023744f232b8051e442840aade Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 5 Jun 2023 12:15:41 +0200 Subject: [PATCH] [Editor] Guess font size and color from the AS of FreeText annotations --- src/core/annotation.js | 16 ++- src/core/default_appearance.js | 87 +++++++++++++++++ test/unit/default_appearance_spec.js | 140 +++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 5 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 054487d57ddb72..7937479d452aa4 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -48,6 +48,7 @@ import { createDefaultAppearance, FakeUnicodeFont, getPdfColor, + parseAppearanceStream, parseDefaultAppearance, } from "./default_appearance.js"; import { Dict, isName, Name, Ref, RefSet } from "./primitives.js"; @@ -3545,20 +3546,25 @@ class FreeTextAnnotation extends MarkupAnnotation { const { xref } = params; this.data.annotationType = AnnotationType.FREETEXT; this.setDefaultAppearance(params); - if (!this.appearance && this._isOffscreenCanvasSupported) { + if (this.appearance) { + const { fontColor, fontSize } = parseAppearanceStream(this.appearance); + this.data.defaultAppearanceData.fontColor = fontColor; + this.data.defaultAppearanceData.fontSize = fontSize || 10; + } else if (this._isOffscreenCanvasSupported) { const strokeAlpha = params.dict.get("CA"); const fakeUnicodeFont = new FakeUnicodeFont(xref, "sans-serif"); - const fontData = this.data.defaultAppearanceData; + this.data.defaultAppearanceData.fontSize ||= 10; + const { fontColor, fontSize } = this.data.defaultAppearanceData; this.appearance = fakeUnicodeFont.createAppearance( this._contents.str, this.rectangle, this.rotation, - fontData.fontSize || 10, - fontData.fontColor, + fontSize, + fontColor, strokeAlpha ); this._streams.push(this.appearance, FakeUnicodeFont.toUnicodeStream); - } else if (!this._isOffscreenCanvasSupported) { + } else { warn( "FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly." ); diff --git a/src/core/default_appearance.js b/src/core/default_appearance.js index e886d3184f2f77..2816fc90e784b0 100644 --- a/src/core/default_appearance.js +++ b/src/core/default_appearance.js @@ -87,6 +87,92 @@ function parseDefaultAppearance(str) { return new DefaultAppearanceEvaluator(str).parse(); } +class AppearanceStreamEvaluator extends EvaluatorPreprocessor { + constructor(stream) { + super(stream); + this.stream = stream; + } + + parse() { + const operation = { + fn: 0, + args: [], + }; + let result = { + scaleFactor: 1, + fontSize: 0, + fontName: "", + fontColor: /* black = */ new Uint8ClampedArray(3), + }; + let breakLoop = false; + const stack = []; + + try { + while (true) { + operation.args.length = 0; // Ensure that `args` it's always reset. + + if (breakLoop || !this.read(operation)) { + break; + } + const { fn, args } = operation; + + switch (fn | 0) { + case OPS.save: + stack.push({ + scaleFactor: result.scaleFactor, + fontSize: result.fontSize, + fontName: result.fontName, + fontColor: result.fontColor.slice(), + }); + break; + case OPS.restore: + result = stack.pop() || result; + break; + case OPS.setTextMatrix: + result.scaleFactor *= Math.hypot(args[0], args[1]); + break; + case OPS.setFont: + const [fontName, fontSize] = args; + if (fontName instanceof Name) { + result.fontName = fontName.name; + } + if (typeof fontSize === "number" && fontSize > 0) { + result.fontSize = fontSize * result.scaleFactor; + } + break; + case OPS.setFillRGBColor: + ColorSpace.singletons.rgb.getRgbItem(args, 0, result.fontColor, 0); + break; + case OPS.setFillGray: + ColorSpace.singletons.gray.getRgbItem(args, 0, result.fontColor, 0); + break; + case OPS.setFillColorSpace: + ColorSpace.singletons.cmyk.getRgbItem(args, 0, result.fontColor, 0); + break; + case OPS.showText: + case OPS.showSpacedText: + case OPS.nextLineShowText: + case OPS.nextLineSetSpacingShowText: + breakLoop = true; + break; + } + } + } catch (reason) { + warn(`parseAppearanceStream - ignoring errors: "${reason}".`); + } + this.stream.reset(); + delete result.scaleFactor; + + return result; + } +} + +// 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 getPdfColor(color, isFill) { if (color[0] === color[1] && color[1] === color[2]) { const gray = color[0] / 255; @@ -368,5 +454,6 @@ export { createDefaultAppearance, FakeUnicodeFont, getPdfColor, + parseAppearanceStream, parseDefaultAppearance, }; diff --git a/test/unit/default_appearance_spec.js b/test/unit/default_appearance_spec.js index 7bd422736aeb5c..53b49ed5b51abf 100644 --- a/test/unit/default_appearance_spec.js +++ b/test/unit/default_appearance_spec.js @@ -15,8 +15,10 @@ import { createDefaultAppearance, + parseAppearanceStream, parseDefaultAppearance, } from "../../src/core/default_appearance.js"; +import { StringStream } from "../../src/core/stream.js"; describe("Default appearance", function () { describe("parseDefaultAppearance and createDefaultAppearance", function () { @@ -50,4 +52,142 @@ describe("Default appearance", function () { }); }); }); + + describe("parseAppearanceStream", () => { + it("should parse a FreeText (from Acrobat) appearance", () => { + const appearance = new StringStream(` + 0 w + 46.5 621.0552 156.389 18.969 re + n + q + 1 0 0 1 0 0 cm + 46.5 621.0552 156.389 18.969 re + W + n + 0 g + 1 w + BT + /Helv 14 Tf + 0.419998 0.850006 0.160004 rg + 46.5 626.77 Td + (Hello ) Tj + 35.793 0 Td + (World ) Tj + 40.448 0 Td + (from ) Tj + 31.89 0 Td + (Acrobat) Tj + ET + Q`); + const result = { + fontSize: 14, + fontName: "Helv", + fontColor: new Uint8ClampedArray([107, 217, 41]), + }; + expect(parseAppearanceStream(appearance)).toEqual(result); + expect(appearance.pos).toEqual(0); + }); + + it("should parse a FreeText (from Firefox) appearance", () => { + const appearance = new StringStream(` + q + 0 0 203.7 28.3 re W n + BT + 1 0 0 1 0 34.6 Tm 0 Tc 0.93 0.17 0.44 rg + /Helv 18 Tf + 0 -24.3 Td (Hello World From Firefox) Tj + ET + Q`); + const result = { + fontSize: 18, + fontName: "Helv", + fontColor: new Uint8ClampedArray([237, 43, 112]), + }; + expect(parseAppearanceStream(appearance)).toEqual(result); + expect(appearance.pos).toEqual(0); + }); + + it("should parse a FreeText (from Preview) appearance", () => { + 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`); + const result = { + fontSize: 22.00539, + fontName: "TT1", + fontColor: new Uint8ClampedArray([0, 0, 0]), + }; + expect(parseAppearanceStream(appearance)).toEqual(result); + expect(appearance.pos).toEqual(0); + }); + + it("should parse a FreeText (from Edge) appearance", () => { + const appearance = new StringStream(` + q + 0 0 292.5 18.75 re W n + BT + 0 Tc + 0.0627451 0.486275 0.0627451 rg + 0 3.8175 Td + /Helv 16.5 Tf + (Hello World from Edge without Acrobat) Tj + ET + Q`); + const result = { + fontSize: 16.5, + fontName: "Helv", + fontColor: new Uint8ClampedArray([16, 124, 16]), + }; + expect(parseAppearanceStream(appearance)).toEqual(result); + expect(appearance.pos).toEqual(0); + }); + + it("should parse a FreeText (from Foxit) appearance", () => { + const appearance = new StringStream(` + q + /Tx BMC + 0 -22.333 197.667 22.333 re + W + n + BT + 0.584314 0.247059 0.235294 rg + 0 -18.1 Td + /FXF0 20 Tf + (Hello World from Foxit) Tj + ET + EMC + Q`); + const result = { + fontSize: 20, + fontName: "FXF0", + fontColor: new Uint8ClampedArray([149, 63, 60]), + }; + expect(parseAppearanceStream(appearance)).toEqual(result); + expect(appearance.pos).toEqual(0); + }); + + it("should parse a FreeText (from Okular) appearance", () => { + const appearance = new StringStream(` + q + 0.00 0.00 172.65 41.46 re W n + 0.00000 0.33333 0.49804 rg + BT 1 0 0 1 0.00 41.46 Tm + /Invalid_font 18.00 Tf + 0.00 -18.00 Td + (Hello World from) Tj + /Invalid_font 18.00 Tf + 0.00 -18.00 Td + (Okular) Tj + ET Q`); + const result = { + fontSize: 18, + fontName: "Invalid_font", + fontColor: new Uint8ClampedArray([0, 85, 127]), + }; + expect(parseAppearanceStream(appearance)).toEqual(result); + expect(appearance.pos).toEqual(0); + }); + }); });