reamkit 1.29.0 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +60 -24
  2. package/dist/esm/core/converter/project.js +3 -1
  3. package/dist/esm/core/crypto/offcrypto.js +1 -1
  4. package/dist/esm/core/document-model/index.d.ts +1 -1
  5. package/dist/esm/core/document-model/types.d.ts +65 -0
  6. package/dist/esm/core/drawingml/shape-render.js +13 -1
  7. package/dist/esm/core/font/index.d.ts +2 -0
  8. package/dist/esm/core/font/ttf-build.d.ts +113 -0
  9. package/dist/esm/core/font/ttf-build.js +1224 -0
  10. package/dist/esm/core/font/ttf-subset.d.ts +19 -0
  11. package/dist/esm/core/font/ttf-subset.js +14 -1
  12. package/dist/esm/core/fonts/index.d.ts +1 -1
  13. package/dist/esm/core/fonts/provider.d.ts +17 -0
  14. package/dist/esm/core/fonts/provider.js +27 -2
  15. package/dist/esm/core/fonts/remote-fonts.d.ts +8 -0
  16. package/dist/esm/core/fonts/remote-fonts.js +108 -18
  17. package/dist/esm/core/ir/flow.d.ts +88 -1
  18. package/dist/esm/core/numbering/index.d.ts +1 -1
  19. package/dist/esm/core/numbering/state.d.ts +11 -1
  20. package/dist/esm/core/numbering/state.js +10 -1
  21. package/dist/esm/core/style-cascade/resolver.js +35 -4
  22. package/dist/esm/core/style-cascade/types.d.ts +19 -1
  23. package/dist/esm/core/style-cascade/types.js +3 -0
  24. package/dist/esm/index.d.ts +2 -2
  25. package/dist/esm/layout/page-doc.d.ts +10 -3
  26. package/dist/esm/layout/styled-layout.d.ts +48 -7
  27. package/dist/esm/layout/styled-layout.js +881 -119
  28. package/dist/esm/layout/turned-section.d.ts +38 -0
  29. package/dist/esm/layout/turned-section.js +193 -0
  30. package/dist/esm/pdf/styled-page-emitter.js +78 -2
  31. package/dist/esm/pdf-reader/annot-draw.js +93 -1
  32. package/dist/esm/pdf-reader/annots.d.ts +18 -0
  33. package/dist/esm/pdf-reader/annots.js +86 -9
  34. package/dist/esm/pdf-reader/cff-outline.d.ts +27 -0
  35. package/dist/esm/pdf-reader/cff-outline.js +169 -21
  36. package/dist/esm/pdf-reader/cmap.js +5 -2
  37. package/dist/esm/pdf-reader/content.d.ts +70 -4
  38. package/dist/esm/pdf-reader/content.js +172 -12
  39. package/dist/esm/pdf-reader/display.d.ts +36 -0
  40. package/dist/esm/pdf-reader/display.js +82 -1
  41. package/dist/esm/pdf-reader/document.js +5 -1
  42. package/dist/esm/pdf-reader/embedded-fonts.d.ts +25 -0
  43. package/dist/esm/pdf-reader/embedded-fonts.js +78 -9
  44. package/dist/esm/pdf-reader/encodings.d.ts +8 -0
  45. package/dist/esm/pdf-reader/encodings.js +25 -3
  46. package/dist/esm/pdf-reader/face-outlines.d.ts +78 -0
  47. package/dist/esm/pdf-reader/face-outlines.js +362 -0
  48. package/dist/esm/pdf-reader/figures.d.ts +52 -0
  49. package/dist/esm/pdf-reader/figures.js +433 -0
  50. package/dist/esm/pdf-reader/flow-build.d.ts +224 -7
  51. package/dist/esm/pdf-reader/flow-build.js +545 -39
  52. package/dist/esm/pdf-reader/font.d.ts +27 -1
  53. package/dist/esm/pdf-reader/font.js +632 -63
  54. package/dist/esm/pdf-reader/glyf-outline.d.ts +33 -0
  55. package/dist/esm/pdf-reader/glyf-outline.js +148 -3
  56. package/dist/esm/pdf-reader/glyph-names.js +154 -1
  57. package/dist/esm/pdf-reader/glyph-shapes.d.ts +18 -0
  58. package/dist/esm/pdf-reader/glyph-shapes.js +57 -0
  59. package/dist/esm/pdf-reader/image-decode.js +70 -4
  60. package/dist/esm/pdf-reader/images.d.ts +5 -0
  61. package/dist/esm/pdf-reader/images.js +4 -2
  62. package/dist/esm/pdf-reader/jbig2.d.ts +40 -1
  63. package/dist/esm/pdf-reader/jbig2.js +78 -16
  64. package/dist/esm/pdf-reader/jpeg.d.ts +6 -3
  65. package/dist/esm/pdf-reader/jpeg.js +21 -1
  66. package/dist/esm/pdf-reader/layout.d.ts +119 -2
  67. package/dist/esm/pdf-reader/layout.js +2219 -184
  68. package/dist/esm/pdf-reader/lexer.d.ts +10 -0
  69. package/dist/esm/pdf-reader/lexer.js +17 -0
  70. package/dist/esm/pdf-reader/page-numbers.d.ts +53 -0
  71. package/dist/esm/pdf-reader/page-numbers.js +167 -0
  72. package/dist/esm/pdf-reader/pattern-tint.d.ts +11 -1
  73. package/dist/esm/pdf-reader/pattern-tint.js +21 -3
  74. package/dist/esm/pdf-reader/regions.d.ts +25 -0
  75. package/dist/esm/pdf-reader/regions.js +167 -0
  76. package/dist/esm/pdf-reader/shading.d.ts +58 -2
  77. package/dist/esm/pdf-reader/shading.js +181 -11
  78. package/dist/esm/pdf-reader/struct-tree.js +112 -8
  79. package/dist/esm/pdf-reader/tagged.js +207 -28
  80. package/dist/esm/pdf-reader/text-rules.js +1 -1
  81. package/dist/esm/pdf-reader/text.d.ts +6 -3
  82. package/dist/esm/pdf-reader/text.js +106 -22
  83. package/dist/esm/pdf-reader/type1-outline.d.ts +11 -0
  84. package/dist/esm/pdf-reader/type1-outline.js +63 -8
  85. package/dist/esm/pdf-reader/vector.d.ts +8 -2
  86. package/dist/esm/pdf-reader/vector.js +105 -6
  87. package/dist/esm/word/doc/doc-reader.js +6 -2
  88. package/dist/esm/word/doc/doc-text.d.ts +6 -0
  89. package/dist/esm/word/doc/doc-text.js +19 -1
  90. package/dist/esm/word/document-parser.d.ts +2 -2
  91. package/dist/esm/word/document-parser.js +11 -1
  92. package/dist/esm/word/docx-reader.js +5 -3
  93. package/dist/esm/word/docx-writer.js +341 -54
  94. package/dist/esm/word/drawing-parser.d.ts +5 -3
  95. package/dist/esm/word/drawing-parser.js +56 -10
  96. package/dist/esm/word/font-embed.d.ts +30 -0
  97. package/dist/esm/word/font-embed.js +173 -0
  98. package/dist/esm/word/font-table.d.ts +10 -0
  99. package/dist/esm/word/font-table.js +13 -1
  100. package/dist/esm/word/index.js +1 -1
  101. package/dist/esm/word/numbering-parser.d.ts +3 -1
  102. package/dist/esm/word/numbering-parser.js +2 -1
  103. package/dist/esm/word/paragraph-properties.d.ts +7 -6
  104. package/dist/esm/word/paragraph-properties.js +16 -2
  105. package/dist/esm/word/run-properties.js +26 -0
  106. package/package.json +12 -5
@@ -0,0 +1,38 @@
1
+ import { BodyElement } from '../core/document-model/index.js';
2
+ import { PageItem } from './page-doc.js';
3
+ /** The sheet a turned section prints on, and the margins it states for it. */
4
+ export interface Sheet {
5
+ readonly width: number;
6
+ readonly height: number;
7
+ readonly marginLeft: number;
8
+ readonly marginRight: number;
9
+ readonly marginTop: number;
10
+ readonly marginBottom: number;
11
+ }
12
+ /**
13
+ * One item of a turned section's body, carried from the frame the text reads
14
+ * in onto the sheet: a point `(x, y)` of the frame (top-left origin, y down)
15
+ * lands at `(sheetWidth − y, x)` — the frame's top edge is the sheet's right
16
+ * edge, and its left edge the sheet's top.
17
+ *
18
+ * @param item The item as the frame's layout placed it.
19
+ * @param sheetWidth The sheet's width, which is the frame's height.
20
+ * @returns The same item on the sheet, turned a quarter clockwise.
21
+ */
22
+ export declare function turnOntoSheet(item: PageItem, sheetWidth: number): PageItem;
23
+ /**
24
+ * A drawing anchored in a turned section, given the place in the text's frame
25
+ * from which turning the body carries it to where Word stands it: on the sheet,
26
+ * at the offsets its anchor states, upright.
27
+ *
28
+ * The anchor is resolved against the SHEET — its page, its margins and the
29
+ * bands beside them. What is measured from the text itself (a paragraph, a
30
+ * line, a column) has no place on the sheet before the text is laid out, and is
31
+ * measured from the margin instead. Only pictures and shapes are carried; a
32
+ * chart or a floating table turns with the text.
33
+ *
34
+ * @param el The body element, anchored or not.
35
+ * @param sheet The sheet the section prints on.
36
+ * @returns The element, its anchor restated in the frame and its turn backed off.
37
+ */
38
+ export declare function floatIntoFrame(el: BodyElement, sheet: Sheet): BodyElement;
@@ -0,0 +1,193 @@
1
+ import { pt } from "../core/ir/units.js";
2
+ //#region src/layout/turned-section.ts
3
+ /** A quarter turn in the 1/60000° a DrawingML rotation is stated in (§20.1.7.6). */
4
+ var QUARTER_60K = 90 * 6e4;
5
+ /**
6
+ * One item of a turned section's body, carried from the frame the text reads
7
+ * in onto the sheet: a point `(x, y)` of the frame (top-left origin, y down)
8
+ * lands at `(sheetWidth − y, x)` — the frame's top edge is the sheet's right
9
+ * edge, and its left edge the sheet's top.
10
+ *
11
+ * @param item The item as the frame's layout placed it.
12
+ * @param sheetWidth The sheet's width, which is the frame's height.
13
+ * @returns The same item on the sheet, turned a quarter clockwise.
14
+ */
15
+ function turnOntoSheet(item, sheetWidth) {
16
+ const box = (x, y, w, h) => ({
17
+ x: pt(sheetWidth - y - h),
18
+ y: pt(x),
19
+ width: pt(h),
20
+ height: pt(w)
21
+ });
22
+ switch (item.type) {
23
+ case "line": {
24
+ const quarter = withinTurn((item.rotationDeg ?? 0) - 90);
25
+ const turned = quarter > 180 ? quarter - 360 : quarter;
26
+ const { rotationDeg: _flat, clip, ...rest } = item;
27
+ return {
28
+ ...rest,
29
+ originX: pt(sheetWidth - item.baselineY),
30
+ baselineY: pt(item.originX),
31
+ ...turned !== 0 ? { rotationDeg: turned } : {},
32
+ ...clip ? { clip: box(clip.x, clip.y, clip.width, clip.height) } : {}
33
+ };
34
+ }
35
+ case "image": {
36
+ const cx = item.x + item.width / 2;
37
+ const cy = item.y + item.height / 2;
38
+ const turned = withinTurn((item.rotationDeg ?? 0) + 90);
39
+ const { rotationDeg: _flat, clip, ...rest } = item;
40
+ return {
41
+ ...rest,
42
+ x: pt(sheetWidth - cy - item.width / 2),
43
+ y: pt(cx - item.height / 2),
44
+ ...turned !== 0 ? { rotationDeg: turned } : {},
45
+ ...clip ? { clip: {
46
+ paths: clip.paths,
47
+ transform: turnMatrix(clip.transform, sheetWidth)
48
+ } } : {}
49
+ };
50
+ }
51
+ case "shape": {
52
+ const shadow = item.shape.shadow;
53
+ return {
54
+ ...item,
55
+ shape: {
56
+ ...item.shape,
57
+ transform: turnMatrix(item.shape.transform, sheetWidth),
58
+ ...shadow ? { shadow: {
59
+ ...shadow,
60
+ dxPt: -shadow.dyPt,
61
+ dyPt: shadow.dxPt
62
+ } } : {}
63
+ }
64
+ };
65
+ }
66
+ case "border": return {
67
+ ...item,
68
+ ...box(item.x, item.y, item.width, item.height),
69
+ side: TURNED_SIDE[item.side]
70
+ };
71
+ case "fill": return {
72
+ ...item,
73
+ ...box(item.x, item.y, item.width, item.height)
74
+ };
75
+ }
76
+ }
77
+ /** An angle in degrees brought into one full turn, `[0, 360)`, with the float noise off. */
78
+ function withinTurn(deg) {
79
+ const t = (deg % 360 + 360) % 360;
80
+ return Math.abs(t) < 1e-9 || Math.abs(t - 360) < 1e-9 ? 0 : t;
81
+ }
82
+ /** Which edge of a box each edge becomes when the box turns a quarter clockwise. */
83
+ var TURNED_SIDE = {
84
+ top: "right",
85
+ right: "bottom",
86
+ bottom: "left",
87
+ left: "top"
88
+ };
89
+ /**
90
+ * A local→page matrix `[a b c d e f]` (x' = a·u + c·v + e, y' = b·u + d·v + f,
91
+ * page top-left, y down) followed by the turn onto the sheet.
92
+ */
93
+ function turnMatrix(m, sheetWidth) {
94
+ return [
95
+ -m[1],
96
+ m[0],
97
+ -m[3],
98
+ m[2],
99
+ sheetWidth - m[5],
100
+ m[4]
101
+ ];
102
+ }
103
+ /**
104
+ * A drawing anchored in a turned section, given the place in the text's frame
105
+ * from which turning the body carries it to where Word stands it: on the sheet,
106
+ * at the offsets its anchor states, upright.
107
+ *
108
+ * The anchor is resolved against the SHEET — its page, its margins and the
109
+ * bands beside them. What is measured from the text itself (a paragraph, a
110
+ * line, a column) has no place on the sheet before the text is laid out, and is
111
+ * measured from the margin instead. Only pictures and shapes are carried; a
112
+ * chart or a floating table turns with the text.
113
+ *
114
+ * @param el The body element, anchored or not.
115
+ * @param sheet The sheet the section prints on.
116
+ * @returns The element, its anchor restated in the frame and its turn backed off.
117
+ */
118
+ function floatIntoFrame(el, sheet) {
119
+ if (el.kind === "image" && el.image.float !== void 0) {
120
+ const { image } = el;
121
+ const float = image.float;
122
+ const placed = frameAnchor(float, image.width, image.height, sheet);
123
+ return {
124
+ ...el,
125
+ image: {
126
+ ...image,
127
+ float: placed,
128
+ rotation60k: backedOff(image.rotation60k)
129
+ }
130
+ };
131
+ }
132
+ if (el.kind === "shape" && el.shape.float !== void 0) {
133
+ const { shape } = el;
134
+ const float = shape.float;
135
+ const placed = frameAnchor(float, shape.width, shape.height, sheet);
136
+ return {
137
+ ...el,
138
+ shape: {
139
+ ...shape,
140
+ float: placed,
141
+ transform: {
142
+ ...shape.transform,
143
+ rotation60k: backedOff(shape.transform?.rotation60k)
144
+ }
145
+ }
146
+ };
147
+ }
148
+ return el;
149
+ }
150
+ /** A clockwise turn with a quarter taken off, kept in the one full turn. */
151
+ function backedOff(rotation60k) {
152
+ const full = 360 * 6e4;
153
+ return (((rotation60k ?? 0) - QUARTER_60K) % full + full) % full;
154
+ }
155
+ /**
156
+ * The anchor a drawing of `w × h` needs in the frame for the body's turn to put
157
+ * it where `float` stands it on the sheet.
158
+ */
159
+ function frameAnchor(float, w, h, sheet) {
160
+ const x = sheetX(float.posH, w, sheet);
161
+ const cx = sheetY(float.posV, h, sheet) + h / 2;
162
+ const cy = sheet.width - (x + w / 2);
163
+ const { posH: _h, posV: _v, ...rest } = float;
164
+ return {
165
+ ...rest,
166
+ posH: {
167
+ relativeFrom: "page",
168
+ offsetPt: pt(cx - w / 2)
169
+ },
170
+ posV: {
171
+ relativeFrom: "page",
172
+ offsetPt: pt(cy - h / 2)
173
+ }
174
+ };
175
+ }
176
+ /** §20.4.3.3 — the box's left edge on the sheet. */
177
+ function sheetX(h, w, sheet) {
178
+ const textRight = sheet.width - sheet.marginRight;
179
+ const [base, span] = h?.relativeFrom === "page" ? [0, sheet.width] : h?.relativeFrom === "leftMargin" ? [0, sheet.marginLeft] : h?.relativeFrom === "rightMargin" ? [textRight, sheet.marginRight] : [sheet.marginLeft, textRight - sheet.marginLeft];
180
+ if (h?.align === "center") return base + (span - w) / 2;
181
+ if (h?.align === "right") return base + span - w;
182
+ return base + (h?.offsetPt ?? 0);
183
+ }
184
+ /** §20.4.3.4 — the box's top edge on the sheet. */
185
+ function sheetY(v, h, sheet) {
186
+ const textBottom = sheet.height - sheet.marginBottom;
187
+ const [base, span] = v?.relativeFrom === "page" ? [0, sheet.height] : v?.relativeFrom === "topMargin" ? [0, sheet.marginTop] : v?.relativeFrom === "bottomMargin" ? [textBottom, sheet.marginBottom] : [sheet.marginTop, textBottom - sheet.marginTop];
188
+ if (v?.align === "center") return base + (span - h) / 2;
189
+ if (v?.align === "bottom") return base + span - h;
190
+ return base + (v?.offsetPt ?? 0);
191
+ }
192
+ //#endregion
193
+ export { floatIntoFrame, turnOntoSheet };
@@ -676,12 +676,13 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
676
676
  }
677
677
  lastStroke = strokeHex;
678
678
  }
679
- const tz = Math.round((tok.synthetic?.widthScale ?? 1) * 100);
679
+ const scale = (tok.synthetic?.widthScale ?? 1) * (tok.resolvedRun.widthScale ?? 1);
680
+ const tz = Math.round(scale * 100);
680
681
  if (tz !== lastTz) {
681
682
  out.push(`${formatNumber(tz)} Tz`);
682
683
  lastTz = tz;
683
684
  }
684
- const tc = tok.resolvedRun.letterSpacingPt ?? 0;
685
+ const tc = (tok.resolvedRun.letterSpacingPt ?? 0) / (tz > 0 ? tz / 100 : 1);
685
686
  if (tc !== lastTc) {
686
687
  out.push(`${formatNumber(tc)} Tc`);
687
688
  lastTc = tc;
@@ -888,7 +889,69 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
888
889
  lastColor = "";
889
890
  lastTc = 0;
890
891
  };
892
+ const emitTurned = (cmd) => {
893
+ if (inBT) {
894
+ out.push("ET");
895
+ inBT = false;
896
+ }
897
+ const rad = (cmd.rotationDeg ?? 0) * Math.PI / 180;
898
+ const cos = Math.cos(rad);
899
+ const sin = Math.sin(rad);
900
+ const ox = cmd.originX;
901
+ const oy = H - cmd.baselineY;
902
+ out.push("q");
903
+ out.push(`${formatNumber(cos)} ${formatNumber(sin)} ${formatNumber(-sin)} ${formatNumber(cos)} ${formatNumber(ox - ox * cos + oy * sin)} ${formatNumber(oy - ox * sin - oy * cos)} cm`);
904
+ lastFont = "";
905
+ lastFauxWidth = 0;
906
+ lastStroke = void 0;
907
+ lastTz = 100;
908
+ lastSize = -1;
909
+ lastColor = "";
910
+ lastTc = 0;
911
+ const from = links.length;
912
+ const { rotationDeg: _turned, ...flat } = cmd;
913
+ out.push("BT");
914
+ inBT = true;
915
+ emitOneLine(flat);
916
+ if (inBT) {
917
+ out.push("ET");
918
+ inBT = false;
919
+ }
920
+ out.push("Q");
921
+ lastFont = "";
922
+ lastFauxWidth = 0;
923
+ lastStroke = void 0;
924
+ lastTz = 100;
925
+ lastSize = -1;
926
+ lastColor = "";
927
+ lastTc = 0;
928
+ for (let k = from; k < links.length; k++) {
929
+ const link = links[k];
930
+ const [x0, y0, x1, y1] = link.rect;
931
+ const corners = [
932
+ [x0, y0],
933
+ [x1, y0],
934
+ [x1, y1],
935
+ [x0, y1]
936
+ ].map(([x, y]) => [ox + (x - ox) * cos - (y - oy) * sin, oy + (x - ox) * sin + (y - oy) * cos]);
937
+ const xs = corners.map((c) => c[0]);
938
+ const ys = corners.map((c) => c[1]);
939
+ links[k] = {
940
+ ...link,
941
+ rect: [
942
+ Math.min(...xs),
943
+ Math.min(...ys),
944
+ Math.max(...xs),
945
+ Math.max(...ys)
946
+ ]
947
+ };
948
+ }
949
+ };
891
950
  const emitOneLine = (cmd) => {
951
+ if (cmd.rotationDeg && cmd.warp === void 0 && !settableInOneMatrix(cmd.line)) {
952
+ emitTurned(cmd);
953
+ return;
954
+ }
892
955
  const line = cmd.line;
893
956
  const originX = cmd.originX;
894
957
  const baselineY = H - cmd.baselineY;
@@ -1397,6 +1460,19 @@ function rotateAboutCm(cx, cy, deg) {
1397
1460
  const s = Math.sin(rad);
1398
1461
  return `${formatNumber(c)} ${formatNumber(s)} ${formatNumber(-s)} ${formatNumber(c)} ${formatNumber(cx - cx * c + cy * s)} ${formatNumber(cy - cx * s - cy * c)} cm`;
1399
1462
  }
1463
+ /**
1464
+ * Whether a line is ONE text matrix and a run of glyphs: nothing in it placed
1465
+ * on its own — a tab, a stretched space, a picture, a formula, a right-to-left
1466
+ * run, a rise, a faked italic — and nothing drawn beside its glyphs, a run's
1467
+ * shading, a highlight or a rule. Only such a line can be turned by its matrix
1468
+ * alone; any other is placed token by token in page space.
1469
+ *
1470
+ * @param line The laid-out line.
1471
+ * @returns True when a single `Tm` sets the whole of it.
1472
+ */
1473
+ function settableInOneMatrix(line) {
1474
+ return computeJustifyExtra(line) === 0 && line.tokens.every((t) => t.kind === "text" && t.tab !== true && t.bidiLevel % 2 === 0 && t.risePt === void 0 && t.synthetic?.italic !== true && t.highlight !== true && t.resolvedRun.shadingColorHex === void 0 && t.resolvedRun.underline === "none" && !t.resolvedRun.strike);
1475
+ }
1400
1476
  function computeJustifyExtra(line) {
1401
1477
  const alignment = line.resolved.alignment;
1402
1478
  if (alignment !== "both" && alignment !== "distribute") return 0;
@@ -1,4 +1,6 @@
1
- import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
1
+ import { PDF_NULL, PdfHexString, PdfName, PdfStream } from "../pdf/objects.js";
2
+ import { reorderVisual } from "../core/bidi/algorithm.js";
3
+ import { analyzeString, hasBidiCharacters } from "../core/bidi/index.js";
2
4
  //#region src/pdf-reader/annot-draw.ts
3
5
  /** The circle constant: how far a Bézier handle reaches to round a quarter. */
4
6
  var KAPPA = .5523;
@@ -20,6 +22,7 @@ function drawnAppearance(file, annot) {
20
22
  const subtype = file.get(annot, "Subtype");
21
23
  if (!(subtype instanceof PdfName)) return void 0;
22
24
  if (subtype.value === "Widget") return typedValue(file, annot) ?? checkedBox(file, annot);
25
+ if (subtype.value === "FreeText") return freeText(file, annot);
23
26
  const markup = markupShape(file, annot, subtype.value);
24
27
  if (markup) return markup;
25
28
  const pen = borderWidth(file, annot);
@@ -162,6 +165,10 @@ var SQUIGGLE_AMP_EM = .07;
162
165
  */
163
166
  function drawnResources(file, annot) {
164
167
  const subtype = file.get(annot, "Subtype");
168
+ if (subtype instanceof PdfName && subtype.value === "FreeText") {
169
+ const text = freeTextContents(file, annot);
170
+ return text === void 0 ? void 0 : new Map([["Font", new Map([[FREE_TEXT_FONT, spelledFace(text)]])]]);
171
+ }
165
172
  if (!(subtype instanceof PdfName) || subtype.value !== "Widget") return void 0;
166
173
  const acro = file.get(file.catalog, "AcroForm");
167
174
  if (!(acro instanceof Map)) return void 0;
@@ -211,6 +218,91 @@ function typedValue(file, annot) {
211
218
  ops.push("ET", "Q");
212
219
  return new PdfStream(/* @__PURE__ */ new Map(), new TextEncoder().encode(ops.join("\n")));
213
220
  }
221
+ /**
222
+ * §12.5.6.6 — a free-text annotation with no appearance: its contents, a line
223
+ * to each line break, from the top of its `/Rect` in the size and colour its
224
+ * `/DA` asks. Left undrawn the words were simply gone: bug1865341.pdf's one
225
+ * note, "Załącznik", and nothing else on the page.
226
+ */
227
+ function freeText(file, annot) {
228
+ const text = freeTextContents(file, annot);
229
+ const rect = rectangle(file.get(annot, "Rect"));
230
+ if (text === void 0 || !rect) return void 0;
231
+ const da = defaultAppearance(file, annot);
232
+ const box = inset(rect, 2);
233
+ const size = da.size > 0 ? da.size : 12;
234
+ const codes = codesOf(text);
235
+ const ops = [
236
+ "q",
237
+ "BT",
238
+ `/${FREE_TEXT_FONT} ${num(size)} Tf`,
239
+ `${da.color} rg`
240
+ ];
241
+ text.split(/\r\n|[\r\n]/u).forEach((line, i) => {
242
+ const y = box[3] - size * (.8 + i * 1.16);
243
+ const hex = visualOrder(line).map((ch) => (codes.get(ch) ?? 0).toString(16).padStart(2, "0")).join("");
244
+ ops.push(`1 0 0 1 ${num(box[0])} ${num(y)} Tm`, `<${hex}> Tj`);
245
+ });
246
+ ops.push("ET", "Q");
247
+ return new PdfStream(/* @__PURE__ */ new Map(), new TextEncoder().encode(ops.join("\n")));
248
+ }
249
+ /** UAX #9 — a line's characters in the order they stand on the page, left to right. */
250
+ function visualOrder(line) {
251
+ const chars = [...line];
252
+ if (!hasBidiCharacters(line)) return chars;
253
+ const { levels } = analyzeString(line);
254
+ return reorderVisual(levels).map((i) => chars[i] ?? "");
255
+ }
256
+ /** The resource name the face of a drawn free-text annotation goes by. */
257
+ var FREE_TEXT_FONT = "FreeText";
258
+ /** §7.9.2.2 — an annotation's `/Contents`, decoded as the text string it is. */
259
+ function freeTextContents(file, annot) {
260
+ const found = file.get(annot, "Contents");
261
+ const raw = found instanceof PdfHexString ? String.fromCharCode(...found.bytes) : typeof found === "string" ? found : void 0;
262
+ if (raw === void 0) return void 0;
263
+ const text = textString(raw).replace(/\s+$/u, "");
264
+ return text.length > 0 ? text : void 0;
265
+ }
266
+ /**
267
+ * §7.9.2.2 — a text string's characters: UTF-16BE behind its byte-order mark,
268
+ * UTF-8 behind its own, and otherwise one byte to a character.
269
+ */
270
+ function textString(raw) {
271
+ if (raw.startsWith("þÿ")) {
272
+ let out = "";
273
+ for (let i = 2; i + 1 < raw.length; i += 2) out += String.fromCharCode(raw.charCodeAt(i) << 8 | raw.charCodeAt(i + 1));
274
+ return out;
275
+ }
276
+ if (raw.startsWith("")) return new TextDecoder("utf-8").decode(Uint8Array.from([...raw.slice(3)].map((c) => c.charCodeAt(0))));
277
+ return raw;
278
+ }
279
+ /**
280
+ * A code for each character a text shows, in the order it first shows them —
281
+ * so a simple face can set a text no standard encoding covers.
282
+ */
283
+ function codesOf(text) {
284
+ const codes = /* @__PURE__ */ new Map();
285
+ for (const ch of text.replace(/[\r\n]/gu, "")) if (!codes.has(ch) && codes.size < 255) codes.set(ch, codes.size + 1);
286
+ return codes;
287
+ }
288
+ /**
289
+ * The Helvetica a drawn free-text annotation is set in, its encoding spelling
290
+ * each code's character out as a glyph name (`uni0142` for ł) — which is how
291
+ * the text reads back as what it is.
292
+ */
293
+ function spelledFace(text) {
294
+ const differences = [1];
295
+ for (const ch of codesOf(text).keys()) {
296
+ const cp = ch.codePointAt(0) ?? 32;
297
+ differences.push(new PdfName(`uni${cp.toString(16).toUpperCase().padStart(4, "0")}`));
298
+ }
299
+ return new Map([
300
+ ["Type", new PdfName("Font")],
301
+ ["Subtype", new PdfName("Type1")],
302
+ ["BaseFont", new PdfName("Helvetica")],
303
+ ["Encoding", new Map([["Differences", differences]])]
304
+ ]);
305
+ }
214
306
  /** §12.7.4.3 — the field is a multi-line text box (bit 13 of `/Ff`). */
215
307
  var MULTILINE = 4096;
216
308
  /**
@@ -8,5 +8,23 @@ export interface Appearance {
8
8
  readonly ctm: Matrix;
9
9
  /** The appearance's `/Resources`, when it states its own. */
10
10
  readonly resources: PdfDict | undefined;
11
+ /** §8.10.1 — the form's `/BBox`, in its own space, which clips what it paints. */
12
+ readonly bbox?: readonly [number, number, number, number];
11
13
  }
14
+ /**
15
+ * §8.10.1 — an appearance's content as it paints: clipped to its `/BBox`.
16
+ *
17
+ * The box is the form's clip, not a hint. A form-filling tool that wrote its
18
+ * field backgrounds in PAGE coordinates inside a box forty points wide paints
19
+ * them nowhere any viewer shows — and painted anyway, with the fill colour it
20
+ * never set, bug1669099.pdf's form came back with three black slabs over its
21
+ * letterhead and its terms of payment, each twice as far across the sheet as
22
+ * the field it belonged to. The clip is installed the way the content would
23
+ * install one, so every pass that reads the appearance sees the same page.
24
+ *
25
+ * @param file The owning file.
26
+ * @param appearance The appearance.
27
+ * @returns Its content, preceded by the clip to its box where it has one.
28
+ */
29
+ export declare function appearanceContent(file: PdfFile, appearance: Appearance): Uint8Array;
12
30
  export declare function collectPageAppearances(file: PdfFile, page: PdfPage): Array<Appearance>;
@@ -2,6 +2,32 @@ import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
2
2
  import { IDENTITY, multiply } from "./content.js";
3
3
  import { drawnAppearance, drawnResources, textMarkupOf } from "./annot-draw.js";
4
4
  //#region src/pdf-reader/annots.ts
5
+ /**
6
+ * §8.10.1 — an appearance's content as it paints: clipped to its `/BBox`.
7
+ *
8
+ * The box is the form's clip, not a hint. A form-filling tool that wrote its
9
+ * field backgrounds in PAGE coordinates inside a box forty points wide paints
10
+ * them nowhere any viewer shows — and painted anyway, with the fill colour it
11
+ * never set, bug1669099.pdf's form came back with three black slabs over its
12
+ * letterhead and its terms of payment, each twice as far across the sheet as
13
+ * the field it belonged to. The clip is installed the way the content would
14
+ * install one, so every pass that reads the appearance sees the same page.
15
+ *
16
+ * @param file The owning file.
17
+ * @param appearance The appearance.
18
+ * @returns Its content, preceded by the clip to its box where it has one.
19
+ */
20
+ function appearanceContent(file, appearance) {
21
+ const data = file.streamData(appearance.stream);
22
+ const box = appearance.bbox;
23
+ if (!box) return data;
24
+ const [x0, y0, x1, y1] = box;
25
+ const clip = new TextEncoder().encode(`${String(x0)} ${String(y0)} ${String(x1 - x0)} ${String(y1 - y0)} re W n\n`);
26
+ const out = new Uint8Array(clip.length + data.length);
27
+ out.set(clip, 0);
28
+ out.set(data, clip.length);
29
+ return out;
30
+ }
5
31
  /** §12.5.3 `/F` — the annotation is not painted at all. */
6
32
  var FLAG_HIDDEN = 2;
7
33
  var FLAG_NOVIEW = 32;
@@ -35,6 +61,7 @@ function collectPageAppearances(file, page) {
35
61
  const annots = file.get(page.dict, "Annots");
36
62
  if (!Array.isArray(annots)) return [];
37
63
  const out = [];
64
+ const regenerate = needsAppearances(file);
38
65
  for (const entry of annots) {
39
66
  const annot = file.resolve(entry);
40
67
  if (!(annot instanceof Map)) continue;
@@ -44,6 +71,18 @@ function collectPageAppearances(file, page) {
44
71
  const flags = file.get(annot, "F");
45
72
  if (typeof flags === "number" && (flags & FLAG_HIDDEN || flags & FLAG_NOVIEW)) continue;
46
73
  const stream = normalAppearance(file, annot);
74
+ const typed = regenerate && isWidget(file, annot) ? drawnAppearance(file, annot) : void 0;
75
+ if (typed && typedField(file, annot)) {
76
+ const kept = stream ? withoutVariableText(file, stream) : void 0;
77
+ const rect = rectangle(file.get(annot, "Rect"));
78
+ if (kept && rect) out.push(fitted(file, kept, rect));
79
+ out.push({
80
+ stream: typed,
81
+ ctm: IDENTITY,
82
+ resources: drawnResources(file, annot)
83
+ });
84
+ continue;
85
+ }
47
86
  if (!stream) {
48
87
  const drawn = drawnAppearance(file, annot);
49
88
  if (drawn) out.push({
@@ -55,17 +94,55 @@ function collectPageAppearances(file, page) {
55
94
  }
56
95
  const rect = rectangle(file.get(annot, "Rect"));
57
96
  if (!rect) continue;
58
- const matrix = matrixOf(file, stream.dict);
59
- const bbox = rectangle(file.get(stream.dict, "BBox"));
60
- const resources = file.get(stream.dict, "Resources");
61
- out.push({
62
- stream,
63
- ctm: multiply(matrix, fitToRect(bbox, matrix, rect)),
64
- resources: resources instanceof Map ? resources : void 0
65
- });
97
+ out.push(fitted(file, stream, rect));
66
98
  }
67
99
  return out;
68
100
  }
101
+ /** An appearance stream placed on its annotation's `/Rect` (§12.5.5). */
102
+ function fitted(file, stream, rect) {
103
+ const matrix = matrixOf(file, stream.dict);
104
+ const bbox = rectangle(file.get(stream.dict, "BBox"));
105
+ const resources = file.get(stream.dict, "Resources");
106
+ return {
107
+ stream,
108
+ ctm: multiply(matrix, fitToRect(bbox, matrix, rect)),
109
+ resources: resources instanceof Map ? resources : void 0,
110
+ ...bbox ? { bbox } : {}
111
+ };
112
+ }
113
+ /** §12.7.2 — whether the form asks the viewer to build its fields' appearances. */
114
+ function needsAppearances(file) {
115
+ const acro = file.get(file.catalog, "AcroForm");
116
+ return acro instanceof Map && file.get(acro, "NeedAppearances") === true;
117
+ }
118
+ function isWidget(file, annot) {
119
+ const subtype = file.get(annot, "Subtype");
120
+ return subtype instanceof PdfName && subtype.value === "Widget";
121
+ }
122
+ /** Whether a widget belongs to a text field — `/FT /Tx`, on it or up its field tree. */
123
+ function typedField(file, annot) {
124
+ let node = annot;
125
+ for (let depth = 0; node && depth < 32; depth++) {
126
+ const type = file.get(node, "FT");
127
+ if (type instanceof PdfName) return type.value === "Tx";
128
+ const parent = file.get(node, "Parent");
129
+ node = parent instanceof Map ? parent : void 0;
130
+ }
131
+ return false;
132
+ }
133
+ /**
134
+ * §12.7.3.3 — an appearance with its variable text taken out: the part it
135
+ * marks `/Tx BMC … EMC`, which is what a viewer rebuilds from the value.
136
+ * bug1844583.pdf's stale appearance still reads "Dlrow Olleh" there, over a
137
+ * field whose value is "Hello World".
138
+ */
139
+ function withoutVariableText(file, stream) {
140
+ const stripped = new TextDecoder("latin1").decode(file.streamData(stream)).replace(/\/Tx\s+BMC[\s\S]*?EMC/gu, "");
141
+ const dict = new Map(stream.dict);
142
+ dict.delete("Filter");
143
+ dict.delete("DecodeParms");
144
+ return new PdfStream(dict, Uint8Array.from([...stripped].map((c) => c.charCodeAt(0))));
145
+ }
69
146
  /** §12.5.5 `/AP` `/N` — the normal appearance, through `/AS` when it is a set. */
70
147
  function normalAppearance(file, annot) {
71
148
  const ap = file.get(annot, "AP");
@@ -149,4 +226,4 @@ function rectangle(v) {
149
226
  ];
150
227
  }
151
228
  //#endregion
152
- export { collectPageAppearances };
229
+ export { appearanceContent, collectPageAppearances };
@@ -32,5 +32,32 @@ export declare function cffNameToGid(program: Uint8Array): Map<string, number> |
32
32
  * CID-keyed or cannot be read.
33
33
  */
34
34
  export declare function cffCidToGid(program: Uint8Array): Map<number, number> | undefined;
35
+ /**
36
+ * How far the pen moves after a name-keyed CFF program's `space`, in
37
+ * thousandths of an em — the width a writer gives the space a page never
38
+ * showed, which is the one it was typeset with.
39
+ *
40
+ * @param program The raw CFF bytes.
41
+ * @returns The advance, or `undefined` where the program has no `space` or its
42
+ * width cannot be read off the charstring.
43
+ */
44
+ export declare function cffSpaceAdvance(program: Uint8Array): number | undefined;
45
+ /**
46
+ * The embedding a CFF program's licence allows: CFF has no OS/2 table, and
47
+ * Adobe writes the OS/2 `fsType` into the Top DICT's `PostScript` string
48
+ * instead (TN 5176 §9, key 12 21) as `/FSType 8 def`.
49
+ *
50
+ * @param program The raw CFF bytes.
51
+ * @returns The value, or `undefined` where the program states none.
52
+ */
53
+ export declare function cffFsType(program: Uint8Array): number | undefined;
54
+ /**
55
+ * The `/FSType n def` a PostScript font program writes where an OpenType one
56
+ * has its OS/2 field (Adobe TN 5176 §9; a Type 1 font's `FontInfo` alike).
57
+ *
58
+ * @param text The PostScript to search.
59
+ * @returns The value, or `undefined` where the text states none.
60
+ */
61
+ export declare function postScriptFsType(text: string): number | undefined;
35
62
  /** The `CFF ` table of an OpenType wrapper, where the program is one. */
36
63
  export declare function openTypeCff(program: Uint8Array): Uint8Array | undefined;