Merge pull request #16522 from calixteman/restore_get_freetext_info_from_as
[Editor] Guess font size and color from the AS of FreeText annotations
This commit is contained in:
commit
9cd84aa0b2
@ -48,6 +48,7 @@ import {
|
|||||||
createDefaultAppearance,
|
createDefaultAppearance,
|
||||||
FakeUnicodeFont,
|
FakeUnicodeFont,
|
||||||
getPdfColor,
|
getPdfColor,
|
||||||
|
parseAppearanceStream,
|
||||||
parseDefaultAppearance,
|
parseDefaultAppearance,
|
||||||
} from "./default_appearance.js";
|
} from "./default_appearance.js";
|
||||||
import { Dict, isName, Name, Ref, RefSet } from "./primitives.js";
|
import { Dict, isName, Name, Ref, RefSet } from "./primitives.js";
|
||||||
@ -3545,20 +3546,25 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
|||||||
const { xref } = params;
|
const { xref } = params;
|
||||||
this.data.annotationType = AnnotationType.FREETEXT;
|
this.data.annotationType = AnnotationType.FREETEXT;
|
||||||
this.setDefaultAppearance(params);
|
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 strokeAlpha = params.dict.get("CA");
|
||||||
const fakeUnicodeFont = new FakeUnicodeFont(xref, "sans-serif");
|
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.appearance = fakeUnicodeFont.createAppearance(
|
||||||
this._contents.str,
|
this._contents.str,
|
||||||
this.rectangle,
|
this.rectangle,
|
||||||
this.rotation,
|
this.rotation,
|
||||||
fontData.fontSize || 10,
|
fontSize,
|
||||||
fontData.fontColor,
|
fontColor,
|
||||||
strokeAlpha
|
strokeAlpha
|
||||||
);
|
);
|
||||||
this._streams.push(this.appearance, FakeUnicodeFont.toUnicodeStream);
|
this._streams.push(this.appearance, FakeUnicodeFont.toUnicodeStream);
|
||||||
} else if (!this._isOffscreenCanvasSupported) {
|
} else {
|
||||||
warn(
|
warn(
|
||||||
"FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly."
|
"FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly."
|
||||||
);
|
);
|
||||||
|
@ -87,6 +87,92 @@ function parseDefaultAppearance(str) {
|
|||||||
return new DefaultAppearanceEvaluator(str).parse();
|
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) {
|
function getPdfColor(color, isFill) {
|
||||||
if (color[0] === color[1] && color[1] === color[2]) {
|
if (color[0] === color[1] && color[1] === color[2]) {
|
||||||
const gray = color[0] / 255;
|
const gray = color[0] / 255;
|
||||||
@ -368,5 +454,6 @@ export {
|
|||||||
createDefaultAppearance,
|
createDefaultAppearance,
|
||||||
FakeUnicodeFont,
|
FakeUnicodeFont,
|
||||||
getPdfColor,
|
getPdfColor,
|
||||||
|
parseAppearanceStream,
|
||||||
parseDefaultAppearance,
|
parseDefaultAppearance,
|
||||||
};
|
};
|
||||||
|
@ -15,8 +15,10 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
createDefaultAppearance,
|
createDefaultAppearance,
|
||||||
|
parseAppearanceStream,
|
||||||
parseDefaultAppearance,
|
parseDefaultAppearance,
|
||||||
} from "../../src/core/default_appearance.js";
|
} from "../../src/core/default_appearance.js";
|
||||||
|
import { StringStream } from "../../src/core/stream.js";
|
||||||
|
|
||||||
describe("Default appearance", function () {
|
describe("Default appearance", function () {
|
||||||
describe("parseDefaultAppearance and createDefaultAppearance", 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user