@jasy/pdf 1.0.0-alpha.8 → 1.0.0-beta.1

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 (167) hide show
  1. package/README.md +29 -10
  2. package/dist/api/color.js +69 -0
  3. package/dist/api/content.d.ts +45 -5
  4. package/dist/api/content.js +96 -7
  5. package/dist/api/descriptor.js +6 -1
  6. package/dist/api/dimension.d.ts +57 -0
  7. package/dist/api/dimension.js +27 -0
  8. package/dist/api/font-url.d.ts +28 -0
  9. package/dist/api/font-url.js +135 -0
  10. package/dist/api/gradient.d.ts +53 -0
  11. package/dist/api/gradient.js +97 -0
  12. package/dist/api/index.d.ts +2 -0
  13. package/dist/api/index.js +2 -0
  14. package/dist/api/insets.d.ts +19 -12
  15. package/dist/api/insets.js +18 -7
  16. package/dist/api/layout.d.ts +20 -6
  17. package/dist/api/layout.js +31 -6
  18. package/dist/api/page-builder.d.ts +8 -0
  19. package/dist/api/page-builder.js +12 -0
  20. package/dist/api/structure.d.ts +12 -0
  21. package/dist/api/structure.js +54 -7
  22. package/dist/api/text.d.ts +65 -8
  23. package/dist/api/text.js +38 -5
  24. package/dist/canvas/painter.d.ts +102 -0
  25. package/dist/canvas/painter.js +183 -0
  26. package/dist/edit/document.d.ts +14 -4
  27. package/dist/edit/document.js +15 -22
  28. package/dist/edit/fill.d.ts +19 -2
  29. package/dist/edit/fill.js +63 -34
  30. package/dist/edit/flatten.d.ts +13 -1
  31. package/dist/edit/flatten.js +38 -13
  32. package/dist/edit/index.d.ts +2 -0
  33. package/dist/edit/index.js +3 -1
  34. package/dist/edit/objects.d.ts +5 -0
  35. package/dist/edit/objects.js +27 -0
  36. package/dist/edit/writer.d.ts +7 -0
  37. package/dist/edit/writer.js +23 -1
  38. package/dist/elements/canvas-element.d.ts +38 -0
  39. package/dist/elements/canvas-element.js +47 -0
  40. package/dist/elements/container-element.d.ts +23 -1
  41. package/dist/elements/container-element.js +31 -10
  42. package/dist/elements/image-element.d.ts +18 -4
  43. package/dist/elements/image-element.js +27 -12
  44. package/dist/elements/index.d.ts +2 -0
  45. package/dist/elements/index.js +2 -0
  46. package/dist/elements/layout/expanded-element.d.ts +5 -1
  47. package/dist/elements/layout/expanded-element.js +16 -3
  48. package/dist/elements/layout/keep-together-element.d.ts +2 -0
  49. package/dist/elements/layout/keep-together-element.js +4 -0
  50. package/dist/elements/layout/padding-element.d.ts +9 -2
  51. package/dist/elements/layout/padding-element.js +12 -2
  52. package/dist/elements/layout/page-builder-element.js +2 -0
  53. package/dist/elements/layout/struct-group.d.ts +2 -0
  54. package/dist/elements/layout/struct-group.js +4 -0
  55. package/dist/elements/pdf-element.d.ts +75 -1
  56. package/dist/elements/pdf-element.js +80 -1
  57. package/dist/elements/rectangle-element.d.ts +22 -6
  58. package/dist/elements/rectangle-element.js +31 -12
  59. package/dist/elements/row-element.d.ts +26 -1
  60. package/dist/elements/row-element.js +31 -10
  61. package/dist/elements/svg-element.d.ts +41 -0
  62. package/dist/elements/svg-element.js +93 -0
  63. package/dist/elements/text-element.d.ts +92 -1
  64. package/dist/elements/text-element.js +208 -33
  65. package/dist/ir/display-list.d.ts +67 -11
  66. package/dist/ir/display-list.js +8 -1
  67. package/dist/layout/alignment.d.ts +10 -0
  68. package/dist/layout/alignment.js +6 -0
  69. package/dist/layout/box-constraints.d.ts +53 -0
  70. package/dist/layout/box-constraints.js +72 -0
  71. package/dist/layout/insets.d.ts +28 -0
  72. package/dist/layout/insets.js +20 -0
  73. package/dist/layout/min-intrinsic.d.ts +29 -0
  74. package/dist/layout/min-intrinsic.js +37 -0
  75. package/dist/renderer/anchor-renderer.js +1 -1
  76. package/dist/renderer/bookmark-renderer.js +1 -1
  77. package/dist/renderer/canvas-renderer.d.ts +14 -0
  78. package/dist/renderer/canvas-renderer.js +36 -0
  79. package/dist/renderer/container-renderer.js +1 -3
  80. package/dist/renderer/default-text-style-renderer.js +1 -1
  81. package/dist/renderer/deferred-renderer.js +1 -1
  82. package/dist/renderer/expanded-renderer.js +1 -1
  83. package/dist/renderer/image-renderer.js +2 -1
  84. package/dist/renderer/index.d.ts +2 -0
  85. package/dist/renderer/index.js +2 -0
  86. package/dist/renderer/keep-together-renderer.js +1 -1
  87. package/dist/renderer/link-renderer.js +1 -1
  88. package/dist/renderer/padding-renderer.js +1 -1
  89. package/dist/renderer/page-builder-renderer.js +1 -1
  90. package/dist/renderer/page-renderer.js +1 -3
  91. package/dist/renderer/pdf-backend.d.ts +21 -4
  92. package/dist/renderer/pdf-backend.js +198 -61
  93. package/dist/renderer/pdf-document-renderer.js +8 -1
  94. package/dist/renderer/pdf-renderer.js +5 -1
  95. package/dist/renderer/positioned-renderer.js +1 -1
  96. package/dist/renderer/rectangle-renderer.js +9 -5
  97. package/dist/renderer/repeating-header-renderer.js +1 -2
  98. package/dist/renderer/rotated-renderer.js +1 -1
  99. package/dist/renderer/row-renderer.js +1 -3
  100. package/dist/renderer/struct-group-renderer.js +1 -1
  101. package/dist/renderer/svg-renderer.d.ts +11 -0
  102. package/dist/renderer/svg-renderer.js +27 -0
  103. package/dist/renderer/text-renderer.d.ts +2 -2
  104. package/dist/renderer/text-renderer.js +176 -57
  105. package/dist/svg/css.d.ts +38 -0
  106. package/dist/svg/css.js +115 -0
  107. package/dist/svg/errors.d.ts +24 -0
  108. package/dist/svg/errors.js +30 -0
  109. package/dist/svg/gradients.d.ts +34 -0
  110. package/dist/svg/gradients.js +156 -0
  111. package/dist/svg/index.d.ts +22 -0
  112. package/dist/svg/index.js +331 -0
  113. package/dist/svg/shapes.d.ts +6 -0
  114. package/dist/svg/shapes.js +177 -0
  115. package/dist/svg/style.d.ts +61 -0
  116. package/dist/svg/style.js +144 -0
  117. package/dist/svg/transform.d.ts +13 -0
  118. package/dist/svg/transform.js +68 -0
  119. package/dist/svg/units.d.ts +11 -0
  120. package/dist/svg/units.js +73 -0
  121. package/dist/svg/xml.d.ts +24 -0
  122. package/dist/svg/xml.js +158 -0
  123. package/dist/text/advance.d.ts +3 -0
  124. package/dist/text/advance.js +8 -4
  125. package/dist/text/arabic.d.ts +20 -0
  126. package/dist/text/arabic.js +101 -0
  127. package/dist/text/bidi.d.ts +33 -0
  128. package/dist/text/bidi.js +94 -0
  129. package/dist/text/font-fallback.d.ts +25 -0
  130. package/dist/text/font-fallback.js +38 -0
  131. package/dist/text/glyph-coverage.d.ts +15 -0
  132. package/dist/text/glyph-coverage.js +77 -0
  133. package/dist/text/line-breaker.d.ts +43 -1
  134. package/dist/text/line-breaker.js +181 -45
  135. package/dist/text/orphans-widows.d.ts +27 -0
  136. package/dist/text/orphans-widows.js +53 -0
  137. package/dist/text/shape.d.ts +29 -0
  138. package/dist/text/shape.js +91 -0
  139. package/dist/text/text-style.d.ts +38 -0
  140. package/dist/text/text-style.js +50 -1
  141. package/dist/text/whitespace.d.ts +4 -0
  142. package/dist/text/whitespace.js +43 -0
  143. package/dist/text/word-splitting.d.ts +42 -0
  144. package/dist/text/word-splitting.js +72 -0
  145. package/dist/utils/flex-layout.d.ts +24 -4
  146. package/dist/utils/flex-layout.js +218 -7
  147. package/dist/utils/font-metrics.d.ts +13 -2
  148. package/dist/utils/gsub.d.ts +36 -0
  149. package/dist/utils/gsub.js +178 -0
  150. package/dist/utils/inflate.d.ts +16 -0
  151. package/dist/utils/inflate.js +52 -0
  152. package/dist/utils/pdf-object-manager.d.ts +44 -3
  153. package/dist/utils/pdf-object-manager.js +176 -14
  154. package/dist/utils/renderer-registry.d.ts +13 -1
  155. package/dist/utils/renderer-registry.js +22 -1
  156. package/dist/utils/ttf-parser.d.ts +6 -0
  157. package/dist/utils/ttf-parser.js +19 -0
  158. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  159. package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
  160. package/dist/utils/woff.d.ts +10 -0
  161. package/dist/utils/woff.js +116 -0
  162. package/dist/utils/woff2.d.ts +6 -0
  163. package/dist/utils/woff2.js +651 -0
  164. package/dist/validators/element-validator.js +9 -2
  165. package/dist/vector/path.d.ts +33 -0
  166. package/dist/vector/path.js +81 -0
  167. package/package.json +6 -2
@@ -0,0 +1,158 @@
1
+ import { SvgParseError } from "./errors.js";
2
+ const ENTITIES = {
3
+ amp: "&",
4
+ lt: "<",
5
+ gt: ">",
6
+ quot: '"',
7
+ apos: "'",
8
+ };
9
+ /** The five predefined entities plus numeric references; anything else is left as written. */
10
+ function decodeEntities(text) {
11
+ if (!text.includes("&"))
12
+ return text;
13
+ return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (whole, body) => {
14
+ if (body.startsWith("#")) {
15
+ const code = body.startsWith("#x")
16
+ ? parseInt(body.slice(2), 16)
17
+ : parseInt(body.slice(1), 10);
18
+ return Number.isFinite(code) && code >= 0 && code <= 0x10ffff
19
+ ? String.fromCodePoint(code)
20
+ : whole;
21
+ }
22
+ return ENTITIES[body] ?? whole;
23
+ });
24
+ }
25
+ const ATTRIBUTE = /([^\s=/>]+)\s*(?:=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
26
+ function parseAttributes(text) {
27
+ const out = {};
28
+ ATTRIBUTE.lastIndex = 0;
29
+ let match;
30
+ while ((match = ATTRIBUTE.exec(text)) !== null) {
31
+ // A valueless attribute (`hidden`) is its own name, as in HTML; SVG has none, but it must not
32
+ // swallow the following one.
33
+ out[match[1]] = decodeEntities(match[3] ?? match[4] ?? match[5] ?? "");
34
+ }
35
+ return out;
36
+ }
37
+ /**
38
+ * Parses a document into its root elements. Text outside an element is dropped, which is what the
39
+ * whitespace between tags is.
40
+ *
41
+ * Mismatched closing tags are tolerated rather than fatal: real exports contain them, browsers
42
+ * recover, and a logo that draws is worth more than a purist error. An unclosed element simply ends
43
+ * at the end of the document.
44
+ */
45
+ /**
46
+ * The index of the `>` that ends a tag, skipping any inside a quoted attribute value - `d="M0 0>10"`
47
+ * is legal XML, and a plain `indexOf(">")` would cut the tag in half there.
48
+ */
49
+ function tagEnd(source, from) {
50
+ let quote = "";
51
+ for (let i = from + 1; i < source.length; i++) {
52
+ const ch = source[i];
53
+ if (quote) {
54
+ if (ch === quote)
55
+ quote = "";
56
+ }
57
+ else if (ch === '"' || ch === "'") {
58
+ quote = ch;
59
+ }
60
+ else if (ch === ">") {
61
+ return i;
62
+ }
63
+ }
64
+ return -1;
65
+ }
66
+ export function parseXml(source) {
67
+ const roots = [];
68
+ const stack = [];
69
+ let i = 0;
70
+ const push = (node) => {
71
+ if (stack.length === 0) {
72
+ if (node.type === "element")
73
+ roots.push(node);
74
+ }
75
+ else {
76
+ stack[stack.length - 1].children.push(node);
77
+ }
78
+ };
79
+ while (i < source.length) {
80
+ const open = source.indexOf("<", i);
81
+ if (open === -1)
82
+ break;
83
+ if (open > i) {
84
+ const raw = source.slice(i, open);
85
+ if (/\S/.test(raw))
86
+ push({ type: "text", value: decodeEntities(raw) });
87
+ }
88
+ // `<!-- -->`, `<![CDATA[ ]]>`, `<?xml ?>` and `<!DOCTYPE >` are skipped or unwrapped whole.
89
+ if (source.startsWith("<!--", open)) {
90
+ const end = source.indexOf("-->", open + 4);
91
+ i = end === -1 ? source.length : end + 3;
92
+ continue;
93
+ }
94
+ if (source.startsWith("<![CDATA[", open)) {
95
+ const end = source.indexOf("]]>", open + 9);
96
+ const value = source.slice(open + 9, end === -1 ? source.length : end);
97
+ if (value)
98
+ push({ type: "text", value });
99
+ i = end === -1 ? source.length : end + 3;
100
+ continue;
101
+ }
102
+ if (source.startsWith("<?", open)) {
103
+ const end = source.indexOf("?>", open + 2);
104
+ i = end === -1 ? source.length : end + 2;
105
+ continue;
106
+ }
107
+ if (source.startsWith("<!", open)) {
108
+ // A doctype may carry an internal subset in brackets, which can itself contain '>'.
109
+ let depth = 0;
110
+ let j = open + 2;
111
+ for (; j < source.length; j++) {
112
+ const ch = source[j];
113
+ if (ch === "[")
114
+ depth++;
115
+ else if (ch === "]")
116
+ depth--;
117
+ else if (ch === ">" && depth <= 0)
118
+ break;
119
+ }
120
+ i = j + 1;
121
+ continue;
122
+ }
123
+ const close = tagEnd(source, open);
124
+ if (close === -1)
125
+ break;
126
+ const inner = source.slice(open + 1, close);
127
+ if (inner.startsWith("/")) {
128
+ const name = inner.slice(1).trim();
129
+ // Close the nearest matching ancestor. A stray closing tag with no match is ignored.
130
+ const at = stack.map((e) => e.tagName).lastIndexOf(name);
131
+ if (at !== -1)
132
+ stack.length = at;
133
+ i = close + 1;
134
+ continue;
135
+ }
136
+ const selfClosing = inner.trimEnd().endsWith("/");
137
+ const body = selfClosing ? inner.slice(0, inner.lastIndexOf("/")) : inner;
138
+ const nameEnd = body.search(/[\s/]/);
139
+ const tagName = (nameEnd === -1 ? body : body.slice(0, nameEnd)).trim();
140
+ if (tagName === "") {
141
+ i = close + 1;
142
+ continue;
143
+ }
144
+ const element = {
145
+ type: "element",
146
+ tagName,
147
+ attributes: nameEnd === -1 ? {} : parseAttributes(body.slice(nameEnd)),
148
+ children: [],
149
+ };
150
+ push(element);
151
+ if (!selfClosing)
152
+ stack.push(element);
153
+ i = close + 1;
154
+ }
155
+ if (roots.length === 0)
156
+ throw new SvgParseError("the file contains no XML elements at all.");
157
+ return roots;
158
+ }
@@ -25,6 +25,9 @@ export interface RunFont {
25
25
  fontFamily: string;
26
26
  fontSize: number;
27
27
  fontStyle: FontStyle;
28
+ /** Whether this run gets the font's ligatures. Part of the FONT, because it changes which glyphs are
29
+ * drawn and therefore how wide the run is - measuring without it would not match the drawing. */
30
+ ligatures?: boolean;
28
31
  }
29
32
  /** Number of Unicode code points (not UTF-16 units): an astral char is ONE, so it takes one
30
33
  * letter-spacing, not two. Matches how `getStringWidth` iterates. */
@@ -29,12 +29,16 @@ export function codePointCount(text) {
29
29
  /** The advance of `text` in points: the plain glyph widths, plus kerning (if the document has it on),
30
30
  * plus one `letterSpacing` per code point. */
31
31
  export function runAdvance(metrics, text, font, letterSpacing = 0) {
32
- let advance = metrics.getStringWidth(text, font.fontFamily, font.fontSize, font.fontStyle);
33
- if (letterSpacing !== 0)
34
- advance += codePointCount(text) * letterSpacing;
32
+ let advance = metrics.getStringWidth(text, font.fontFamily, font.fontSize, font.fontStyle, font.ligatures);
33
+ if (letterSpacing !== 0) {
34
+ // Per DRAWN glyph, which is what `Tc` applies to; only a ligature makes that differ.
35
+ const glyphs = metrics.shapedGlyphCount?.(text, font.fontFamily, font.fontStyle, font.ligatures) ??
36
+ codePointCount(text);
37
+ advance += glyphs * letterSpacing;
38
+ }
35
39
  if (metrics.kerningEnabled) {
36
40
  let units = 0;
37
- for (const k of metrics.getKernPairs(text, font.fontFamily, font.fontStyle))
41
+ for (const k of metrics.getKernPairs(text, font.fontFamily, font.fontStyle, font.ligatures))
38
42
  units += k;
39
43
  advance += (units / 1000) * font.fontSize; // kern units are em/1000
40
44
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Arabic (and Syriac / N'Ko / Adlam) JOINING: which of the four positional forms each letter takes -
3
+ * the half of shaping Unicode owns. Runs on the LOGICAL string.
4
+ *
5
+ * Name the SIDES, not the neighbours: a letter is joined on its right when the preceding one can join
6
+ * forwards (type D or L), on its left when the following one can join backwards (D or R). Thinking in
7
+ * "previous/next" puts a final form where an initial belongs.
8
+ */
9
+ export type JoiningType = "D" | "R" | "L" | "C" | "T" | "U";
10
+ /** The four positional forms, named as the OpenType features that realise them. */
11
+ export type JoiningForm = "isol" | "init" | "medi" | "fina";
12
+ /** The joining type of one code point. `U` for everything the table does not mention. */
13
+ export declare function joiningType(codePoint: number): JoiningType;
14
+ /** Whether a run contains anything the joining pass would touch. Cheap, and false for Latin. */
15
+ export declare function needsJoining(codePoints: readonly number[]): boolean;
16
+ /**
17
+ * The positional form of every code point, or `null` where none applies (space, digit, mark).
18
+ * `C` (tatweel, ZWJ) counts when deciding a NEIGHBOUR's form but takes none itself.
19
+ */
20
+ export declare function joiningForms(codePoints: readonly number[]): (JoiningForm | null)[];
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Arabic (and Syriac / N'Ko / Adlam) JOINING: which of the four positional forms each letter takes -
3
+ * the half of shaping Unicode owns. Runs on the LOGICAL string.
4
+ *
5
+ * Name the SIDES, not the neighbours: a letter is joined on its right when the preceding one can join
6
+ * forwards (type D or L), on its left when the following one can join backwards (D or R). Thinking in
7
+ * "previous/next" puts a final form where an initial belongs.
8
+ */
9
+ /**
10
+ * Joining types, from Unicode 17.0.0 `ArabicShaping.txt`, packed as `start,length,type` in hex.
11
+ * Anything not listed is `U` (non-joining), which is the standard's own default.
12
+ *
13
+ * `T` (transparent - combining marks) is mostly NOT in that file: the standard says an unlisted mark
14
+ * (category Mn/Me/Cf) is transparent, so those ranges are folded in from the character database.
15
+ */
16
+ const PACKED = "300,6f,5 483,6,5 591,2c,5 5bf,0,5 5c1,1,5 5c4,1,5 5c7,0,5 610,a,5 61c,0,5 620,0,1 622,3,2" +
17
+ " 626,0,1 627,0,2 628,0,1 629,0,2 62a,4,1 62f,3,2 633,c,1 640,0,4 641,6,1 648,0,2 649,1,1 64b,14,5" +
18
+ " 66e,1,1 670,0,5 671,2,2 675,2,2 678,f,1 688,11,2 69a,25,1 6c0,0,2 6c1,1,1 6c3,8,2 6cc,0,1" +
19
+ " 6cd,0,2 6ce,0,1 6cf,0,2 6d0,1,1 6d2,1,2 6d5,0,2 6d6,6,5 6df,5,5 6e7,1,5 6ea,3,5 6ee,1,2 6fa,2,1" +
20
+ " 6ff,0,1 70f,0,5 710,0,2 711,0,5 712,2,1 715,4,2 71a,3,1 71e,0,2 71f,8,1 728,0,2 729,0,1 72a,0,2" +
21
+ " 72b,0,1 72c,0,2 72d,1,1 72f,0,2 730,1a,5 74d,0,2 74e,a,1 759,2,2 75c,e,1 76b,1,2 76d,3,1 771,0,2" +
22
+ " 772,0,1 773,1,2 775,2,1 778,1,2 77a,5,1 7a6,a,5 7ca,20,1 7eb,8,5 7fa,0,4 7fd,0,5 816,3,5 81b,8,5" +
23
+ " 825,2,5 829,4,5 840,0,2 841,4,1 846,1,2 848,0,1 849,0,2 84a,9,1 854,0,2 855,0,1 856,2,2 859,2,5" +
24
+ " 860,0,1 862,3,1 867,0,2 868,0,1 869,1,2 870,12,2 883,2,4 886,0,1 889,4,1 88e,0,2 88f,0,1 897,8,5" +
25
+ " 8a0,9,1 8aa,2,2 8ae,0,2 8af,1,1 8b1,1,2 8b3,5,1 8b9,0,2 8ba,e,1 8ca,17,5 8e3,1c,5 1807,0,1" +
26
+ " 180a,0,4 1820,58,1 1885,1,5 1887,21,1 18aa,0,1 200d,0,4 a840,31,1 a872,0,3 fe00,f,5 fe20,f,5" +
27
+ " 10ac0,4,1 10ac5,0,2 10ac7,0,2 10ac9,1,2 10acd,0,3 10ace,4,2 10ad3,3,1 10ad7,0,3 10ad8,4,1" +
28
+ " 10add,0,2 10ade,2,1 10ae1,0,2 10ae4,0,2 10aeb,3,1 10aef,0,2 10b80,0,1 10b81,0,2 10b82,0,1" +
29
+ " 10b83,2,2 10b86,2,1 10b89,0,2 10b8a,1,1 10b8c,0,2 10b8d,0,1 10b8e,1,2 10b90,0,1 10b91,0,2" +
30
+ " 10ba9,3,2 10bad,1,1 10d00,0,3 10d01,20,1 10d22,0,2 10d23,0,1 10ec2,0,2 10ec3,1,1 10ec6,1,1" +
31
+ " 10f30,2,1 10f33,0,2 10f34,10,1 10f51,2,1 10f54,0,2 10f70,3,1 10f74,1,2 10f76,b,1 10fb0,0,1" +
32
+ " 10fb2,1,1 10fb4,2,2 10fb8,0,1 10fb9,1,2 10fbb,1,1 10fbd,0,2 10fbe,1,1 10fc1,0,1 10fc2,1,2" +
33
+ " 10fc4,0,1 10fc9,0,2 10fca,0,1 10fcb,0,3 1e900,43,1 1e94b,0,5";
34
+ const TYPES = ["U", "D", "R", "L", "C", "T"];
35
+ // [start, end, typeIndex] triples, sorted by start - binary searched, so the table costs one lookup.
36
+ const RANGES = PACKED.trim()
37
+ .split(/\s+/)
38
+ .map((entry) => {
39
+ const [start, length, type] = entry.split(",");
40
+ const from = parseInt(start, 16);
41
+ return [from, from + parseInt(length, 16), Number(type)];
42
+ });
43
+ /** The joining type of one code point. `U` for everything the table does not mention. */
44
+ export function joiningType(codePoint) {
45
+ let lo = 0;
46
+ let hi = RANGES.length - 1;
47
+ while (lo <= hi) {
48
+ const mid = (lo + hi) >> 1;
49
+ const [from, to, type] = RANGES[mid];
50
+ if (codePoint < from)
51
+ hi = mid - 1;
52
+ else if (codePoint > to)
53
+ lo = mid + 1;
54
+ else
55
+ return TYPES[type];
56
+ }
57
+ return "U";
58
+ }
59
+ /** Whether a run contains anything the joining pass would touch. Cheap, and false for Latin. */
60
+ export function needsJoining(codePoints) {
61
+ return codePoints.some((cp) => {
62
+ const t = joiningType(cp);
63
+ return t === "D" || t === "R" || t === "L" || t === "C";
64
+ });
65
+ }
66
+ /**
67
+ * The positional form of every code point, or `null` where none applies (space, digit, mark).
68
+ * `C` (tatweel, ZWJ) counts when deciding a NEIGHBOUR's form but takes none itself.
69
+ */
70
+ export function joiningForms(codePoints) {
71
+ const types = codePoints.map(joiningType);
72
+ /** The nearest neighbour that is not transparent - a mark never breaks a join. */
73
+ const neighbour = (from, step) => {
74
+ for (let i = from + step; i >= 0 && i < types.length; i += step) {
75
+ if (types[i] !== "T")
76
+ return types[i];
77
+ }
78
+ return "U";
79
+ };
80
+ return types.map((type, i) => {
81
+ if (type !== "D" && type !== "R" && type !== "L")
82
+ return null;
83
+ const before = neighbour(i, -1);
84
+ const after = neighbour(i, +1);
85
+ const joinedRight = before === "D" || before === "L" || before === "C";
86
+ const joinedLeft = after === "D" || after === "R" || after === "C";
87
+ if (type === "D") {
88
+ if (joinedRight && joinedLeft)
89
+ return "medi";
90
+ if (joinedRight)
91
+ return "fina";
92
+ if (joinedLeft)
93
+ return "init";
94
+ return "isol";
95
+ }
96
+ // R joins only on its right; L (a handful of Syriac letters) only on its left.
97
+ if (type === "R")
98
+ return joinedRight ? "fina" : "isol";
99
+ return joinedLeft ? "init" : "isol";
100
+ });
101
+ }
@@ -0,0 +1,33 @@
1
+ export type Direction = "ltr" | "rtl";
2
+ /** A piece of a line, already in the order it is drawn. */
3
+ export interface VisualRun {
4
+ /** VISUAL order: hand it to the drawing code as it stands, left to right. */
5
+ text: string;
6
+ /**
7
+ * The same characters in LOGICAL order. SHAPING must use this: a letter's form comes from its
8
+ * neighbours, and reversing swaps them - shaping the visual text gives every letter the wrong form
9
+ * (measured: a word 8pt too wide). Shape this, then reverse the GLYPHS.
10
+ */
11
+ logical: string;
12
+ /** True when this run came out of right-to-left text - the caller may need it for alignment. */
13
+ rtl: boolean;
14
+ }
15
+ /** A run that remembers which input piece it came from, so a span keeps its font and colour. */
16
+ export interface AttributedRun<T> extends VisualRun {
17
+ source: T;
18
+ }
19
+ /** Whether this text could be reordered at all. Cheap, and false for every Latin-only document. */
20
+ export declare const needsBidi: (text: string) => boolean;
21
+ /**
22
+ * Reorder one LINE of pieces into the runs to draw. The algorithm runs over the pieces JOINED, not
23
+ * each on its own, or a Hebrew span beside a Latin one would stay in source order. A run ends where
24
+ * the direction or the source piece changes.
25
+ *
26
+ * Line breaking happens before this, in logical order - what UAX #9 prescribes.
27
+ */
28
+ export declare function visualRunsOf<T>(pieces: {
29
+ text: string;
30
+ source: T;
31
+ }[], base?: Direction): AttributedRun<T>[];
32
+ /** The same for one plain string. An empty string still yields one run, or a blank line would vanish. */
33
+ export declare const visualRuns: (text: string, base?: Direction) => VisualRun[];
@@ -0,0 +1,94 @@
1
+ import bidiFactory from "bidi-js";
2
+ /**
3
+ * Bidirectional text (UAX #9): one logical line in, the runs to draw left to right out. ORDERING
4
+ * only - joining letters into shapes is `text/shape.ts`.
5
+ *
6
+ * The whole seam to `bidi-js`: nothing outside this file knows it exists, so replacing it with our
7
+ * own is a change here alone (`todo.md`).
8
+ */
9
+ const bidi = bidiFactory();
10
+ // Every block whose script runs right to left, plus the explicit bidi control characters. Used only
11
+ // as a fast path: no match and a left-to-right base means the algorithm cannot reorder anything.
12
+ const RTL_OR_CONTROL = /[\u0590-\u08FF\u200F\u202B\u202E\u2067\uFB1D-\uFDFF\uFE70-\uFEFF]|[\uD802\uD803\uD83A\uD83B][\uDC00-\uDFFF]/;
13
+ /** Whether this text could be reordered at all. Cheap, and false for every Latin-only document. */
14
+ export const needsBidi = (text) => RTL_OR_CONTROL.test(text);
15
+ const isHighSurrogate = (c) => c >= "\uD800" && c <= "\uDBFF";
16
+ const isLowSurrogate = (c) => c >= "\uDC00" && c <= "\uDFFF";
17
+ /**
18
+ * Reorder one LINE of pieces into the runs to draw. The algorithm runs over the pieces JOINED, not
19
+ * each on its own, or a Hebrew span beside a Latin one would stay in source order. A run ends where
20
+ * the direction or the source piece changes.
21
+ *
22
+ * Line breaking happens before this, in logical order - what UAX #9 prescribes.
23
+ */
24
+ export function visualRunsOf(pieces, base = "ltr") {
25
+ const text = pieces.map((p) => p.text).join("");
26
+ // Verbatim - same count, same order, empty pieces included - so untouched documents stay
27
+ // byte-identical.
28
+ if (text === "" || (base === "ltr" && !needsBidi(text))) {
29
+ return pieces.map((p) => ({ ...p, logical: p.text, rtl: false }));
30
+ }
31
+ // Which piece each code unit came from, so a visual run can be traced back to its span.
32
+ const owner = new Int32Array(text.length);
33
+ let at = 0;
34
+ pieces.forEach((piece, index) => {
35
+ owner.fill(index, at, at + piece.text.length);
36
+ at += piece.text.length;
37
+ });
38
+ const levels = bidi.getEmbeddingLevels(text, base);
39
+ const order = bidi.getReorderedIndices(text, levels);
40
+ // `.levels`, NOT the result object the library's README passes: it indexes its argument directly,
41
+ // and given the object it silently returns an empty map - no bracket ever mirrored.
42
+ const mirrored = bidi.getMirroredCharactersMap(text, levels.levels);
43
+ const runs = [];
44
+ let current = []; // visual order, mirrored
45
+ let written = []; // the same run as the author wrote it
46
+ let currentRtl = false;
47
+ let currentOwner = -1;
48
+ const flush = () => {
49
+ if (current.length > 0) {
50
+ runs.push({
51
+ text: current.join(""),
52
+ // Tracked separately rather than un-reversing the visual text: that text has been MIRRORED,
53
+ // so a bracket would come back as its twin and shaping - and `ToUnicode` after it - would see
54
+ // a character the author never wrote.
55
+ logical: (currentRtl ? [...written].reverse() : written).join(""),
56
+ rtl: currentRtl,
57
+ source: pieces[currentOwner].source,
58
+ });
59
+ current = [];
60
+ written = [];
61
+ }
62
+ };
63
+ for (let k = 0; k < order.length; k++) {
64
+ const i = order[k];
65
+ const rtl = (levels.levels[i] & 1) === 1;
66
+ if (rtl !== currentRtl || owner[i] !== currentOwner)
67
+ flush();
68
+ currentRtl = rtl;
69
+ currentOwner = owner[i];
70
+ // A surrogate pair is two code units at one level, so reversing splits an astral character (an
71
+ // emoji in Hebrew text) into two broken halves. They arrive adjacent and swapped.
72
+ const next = k + 1 < order.length ? order[k + 1] : -1;
73
+ if (isLowSurrogate(text[i]) && next === i - 1 && isHighSurrogate(text[next])) {
74
+ current.push(text[next], text[i]);
75
+ written.push(text[next], text[i]);
76
+ k++;
77
+ continue;
78
+ }
79
+ current.push(mirrored.get(i) ?? text[i]);
80
+ written.push(text[i]);
81
+ }
82
+ flush();
83
+ return runs;
84
+ }
85
+ /** The same for one plain string. An empty string still yields one run, or a blank line would vanish. */
86
+ export const visualRuns = (text, base = "ltr") => {
87
+ if (text === "")
88
+ return [{ text, logical: text, rtl: false }];
89
+ return visualRunsOf([{ text, source: null }], base).map(({ text: t, logical, rtl }) => ({
90
+ text: t,
91
+ logical,
92
+ rtl,
93
+ }));
94
+ };
@@ -0,0 +1,25 @@
1
+ import type { FontStyle } from "../utils/pdf-object-manager.js";
2
+ import type { FontMetrics } from "../utils/font-metrics.js";
3
+ /**
4
+ * Font FALLBACK: picking, per code point, the first family in a stack that can draw it.
5
+ *
6
+ * `Text({ font: ["Inter", "NotoSansCJK"] })` should just work for mixed-script text. Without this a
7
+ * code point the first family lacks comes out as `.notdef` - the empty box.
8
+ *
9
+ * The output is a list of pieces, each in ONE family, which is exactly the shape a styled `span`
10
+ * already has. So the fallback reuses the whole segment machinery - breaking, bidi, shaping and
11
+ * drawing all handle per-piece fonts already - instead of adding a second one beside it.
12
+ */
13
+ /** A stretch of text that one family can draw. */
14
+ export interface FontRun {
15
+ text: string;
16
+ fontFamily: string;
17
+ }
18
+ /**
19
+ * Split `text` into runs by family. Returns `undefined` when there is nothing to split - no stack, or
20
+ * the first family draws everything - so a document that never asks for a fallback keeps its old path.
21
+ *
22
+ * A code point that NO family in the stack can draw stays with the first one, which then draws its
23
+ * `.notdef`. That is what a browser does too: the fallback chain ends, and the missing glyph shows.
24
+ */
25
+ export declare function splitByFont(text: string, fontFamily: string, fallback: readonly string[], fontStyle: FontStyle, metrics: FontMetrics): FontRun[] | undefined;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Split `text` into runs by family. Returns `undefined` when there is nothing to split - no stack, or
3
+ * the first family draws everything - so a document that never asks for a fallback keeps its old path.
4
+ *
5
+ * A code point that NO family in the stack can draw stays with the first one, which then draws its
6
+ * `.notdef`. That is what a browser does too: the fallback chain ends, and the missing glyph shows.
7
+ */
8
+ export function splitByFont(text, fontFamily, fallback, fontStyle, metrics) {
9
+ if (fallback.length === 0)
10
+ return undefined;
11
+ const familyFor = (codePoint) => {
12
+ if (metrics.hasGlyph(codePoint, fontFamily, fontStyle))
13
+ return fontFamily;
14
+ for (const family of fallback) {
15
+ if (metrics.hasGlyph(codePoint, family, fontStyle))
16
+ return family;
17
+ }
18
+ return fontFamily; // nobody has it - the first family shows the missing glyph
19
+ };
20
+ const runs = [];
21
+ let current = "";
22
+ let currentFamily = fontFamily;
23
+ for (const ch of text) {
24
+ const family = familyFor(ch.codePointAt(0));
25
+ if (current !== "" && family !== currentFamily) {
26
+ runs.push({ text: current, fontFamily: currentFamily });
27
+ current = "";
28
+ }
29
+ currentFamily = family;
30
+ current += ch;
31
+ }
32
+ if (current !== "")
33
+ runs.push({ text: current, fontFamily: currentFamily });
34
+ // Nothing substituted - no runs at all (empty text), or one in the family we started with. Say so,
35
+ // and the caller keeps its cheaper path instead of being handed an empty list to draw.
36
+ const unchanged = runs.length === 0 || (runs.length === 1 && runs[0].fontFamily === fontFamily);
37
+ return unchanged ? undefined : runs;
38
+ }
@@ -0,0 +1,15 @@
1
+ import type { FontMetrics } from "../utils/font-metrics.js";
2
+ import type { FontStyle } from "../utils/pdf-object-manager.js";
3
+ export interface CoverageResult {
4
+ /** The text as it can actually be drawn. */
5
+ text: string;
6
+ /** Code points that had to go - empty for every document that never hits this. */
7
+ dropped: number[];
8
+ }
9
+ /**
10
+ * Replaces or removes what `fontFamily` cannot draw. Returns the input unchanged (and no allocation
11
+ * beyond the scan) when everything is drawable, which is the overwhelmingly common case.
12
+ */
13
+ export declare function coverText(text: string, fontFamily: string, fontStyle: FontStyle, metrics: FontMetrics): CoverageResult;
14
+ /** `U+2713` - how a dropped code point is named to the caller. */
15
+ export declare function formatCodePoint(cp: number): string;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * A code point the chosen font cannot draw becomes glyph 0 - which IS `.notdef`, and referencing it is
3
+ * forbidden by PDF/A (ISO 19005-3, 6.2.11.8). So an invoice carrying a "✓" in its description is a
4
+ * conforming file right up until someone validates it.
5
+ *
6
+ * The check has to be DYNAMIC, against the font actually resolved: which characters are missing is a
7
+ * property of that font, not of Unicode. `@jasy/e-invoice` embeds Liberation, and its gaps look
8
+ * arbitrary from the outside - "→" draws, "⇒" does not; "₴" draws, "₽" does not.
9
+ *
10
+ * What we do about it, in this order:
11
+ * 1. The font has it -> keep, untouched. This is every ordinary document, unchanged.
12
+ * 2. A plain equivalent MEANS the same and the font has THAT -> substitute.
13
+ * 3. Otherwise -> drop it, and report it, so the application can say which character
14
+ * went missing instead of shipping a box or a broken file.
15
+ */
16
+ /**
17
+ * Substitutes that keep the meaning. Deliberately tiny: only where dropping would CHANGE the text
18
+ * rather than thin it. `E‑Rechnung` with a non-breaking hyphen must not become `ERechnung`.
19
+ *
20
+ * Not a list of "what Word pastes" - most of that (quotes, dashes, the ellipsis) is in WinAnsi and in
21
+ * every normal font, so it never reaches rule 3. Entries earn their place by being measured, not
22
+ * guessed: each one is a code point seen missing from a real embedded font.
23
+ */
24
+ const EQUIVALENTS = {
25
+ 0x2010: "-", // hyphen
26
+ 0x2011: "-", // non-breaking hyphen - the one that turns E‑Rechnung into ERechnung
27
+ 0x2012: "-", // figure dash
28
+ 0x2015: "-", // horizontal bar
29
+ 0x2007: " ", // figure space
30
+ 0x2008: " ", // punctuation space
31
+ 0x2009: " ", // thin space
32
+ 0x200a: " ", // hair space
33
+ 0x202f: " ", // narrow no-break space
34
+ 0x2032: "'", // prime
35
+ 0x2033: '"', // double prime
36
+ 0x02bc: "'", // modifier letter apostrophe
37
+ };
38
+ /**
39
+ * Replaces or removes what `fontFamily` cannot draw. Returns the input unchanged (and no allocation
40
+ * beyond the scan) when everything is drawable, which is the overwhelmingly common case.
41
+ */
42
+ export function coverText(text, fontFamily, fontStyle, metrics) {
43
+ let out = null; // built only once something has to change
44
+ const dropped = [];
45
+ let index = 0;
46
+ for (const char of text) {
47
+ const cp = char.codePointAt(0);
48
+ // A space and a line break are handled by the breaker, never drawn as a glyph.
49
+ const drawable = cp === 0x20 ||
50
+ cp === 0x0a ||
51
+ metrics.hasGlyph(cp, fontFamily, fontStyle) ||
52
+ // Colour emoji is drawn from a fallback font or an image, NOT from the text font - it is missing
53
+ // there by design, and removing it would delete the very thing `Document({ emoji })` exists for.
54
+ metrics.rendersAsEmoji?.(cp, fontFamily, fontStyle) === true;
55
+ if (!drawable) {
56
+ if (out === null)
57
+ out = text.slice(0, index);
58
+ const equivalent = EQUIVALENTS[cp];
59
+ if (equivalent !== undefined &&
60
+ metrics.hasGlyph(equivalent.codePointAt(0), fontFamily, fontStyle)) {
61
+ out += equivalent;
62
+ }
63
+ else {
64
+ dropped.push(cp);
65
+ }
66
+ }
67
+ else if (out !== null) {
68
+ out += char;
69
+ }
70
+ index += char.length;
71
+ }
72
+ return out === null ? { text, dropped } : { text: out, dropped };
73
+ }
74
+ /** `U+2713` - how a dropped code point is named to the caller. */
75
+ export function formatCodePoint(cp) {
76
+ return "U+" + cp.toString(16).toUpperCase().padStart(4, "0");
77
+ }
@@ -1,6 +1,33 @@
1
1
  import type { FontStyle } from "../utils/pdf-object-manager.js";
2
2
  import type { FontMetrics } from "../utils/font-metrics.js";
3
3
  import type { TextSegment } from "../elements/text-element.js";
4
+ import { type WordSplitting } from "./word-splitting.js";
5
+ /**
6
+ * How far a JUSTIFIED line's spaces may be squeezed to keep one more word on it.
7
+ *
8
+ * A quarter, because our breaker is GREEDY: it will use the whole allowance every time that saves a
9
+ * line, so the limit has to be the tightest word space still worth reading - about three quarters of
10
+ * natural, the usual typographic floor. TeX allows a third, but its badness model rarely spends it.
11
+ * For reference, react-pdf reaches -19% in practice (measured: a 3.18pt space squeezed to 2.58pt),
12
+ * which this covers. Its packing can still differ by a word, because it spreads the squeeze over the
13
+ * CHARACTERS as well as the spaces.
14
+ *
15
+ * Only justified text uses it: under any other alignment a squeezed line would simply overflow.
16
+ */
17
+ export declare const MAX_SPACE_SHRINK: number;
18
+ /** The knobs that change where a line ENDS, beyond the font and the box. */
19
+ export interface LineOptions {
20
+ /** Extra advance at every space (CSS `word-spacing`), in points. */
21
+ wordSpacing?: number;
22
+ /** How far the FIRST line starts in (CSS `text-indent`), in points - it has that much less room. */
23
+ indent?: number;
24
+ /** How far a justified line's spaces may be squeezed to keep one more word (0 = not justified). */
25
+ shrink?: number;
26
+ /** How a word wider than its box is split - hyphenation, break-word, or neither (the default). */
27
+ splitting?: WordSplitting;
28
+ /** Whether the run gets the font's ligatures. Changes which glyphs are drawn, hence the width. */
29
+ ligatures?: boolean;
30
+ }
4
31
  /** Default font for segments that don't override it. */
5
32
  export interface SegmentDefaults {
6
33
  fontFamily: string;
@@ -8,6 +35,9 @@ export interface SegmentDefaults {
8
35
  fontStyle: FontStyle;
9
36
  /** Extra space after every glyph, in points; a segment may override it. Default 0. */
10
37
  letterSpacing?: number;
38
+ /** Same knobs as the string path - see `LineOptions.splitting`. */
39
+ splitting?: WordSplitting;
40
+ ligatures?: boolean;
11
41
  }
12
42
  /** What happens to text beyond `maxLines`: `"clip"` drops it, `"ellipsis"` ends the last kept line
13
43
  * with an ellipsis. Mirrors Flutter's `TextOverflow`. */
@@ -26,7 +56,19 @@ export interface SegmentLine {
26
56
  * rendering call this, so they can never disagree. Depends only on `FontMetrics`,
27
57
  * not the PDF byte writer - the future fragmentation pass can reuse it.
28
58
  */
29
- export declare function wrapStringIntoLines(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, maxWidth: number, metrics: FontMetrics, maxLines?: number, overflow?: TextOverflow, letterSpacing?: number): string[];
59
+ /**
60
+ * The width of `text` set as ONE line, accumulated in EXACTLY the order `wrapStringIntoLines` builds a
61
+ * line: the first word, then `+ (space + word)` each time. The order matters because floating-point
62
+ * addition is not associative - a box sized by a differently-grouped sum can be one bit too small for
63
+ * the very line the breaker then measures, and the text wraps inside a box made to fit it. That is not
64
+ * hypothetical: it is what split a footer sized to its own content.
65
+ */
66
+ export declare function singleLineWidth(text: string, font: {
67
+ fontFamily: string;
68
+ fontSize: number;
69
+ fontStyle: FontStyle;
70
+ }, metrics: FontMetrics, letterSpacing?: number, wordSpacing?: number): number;
71
+ export declare function wrapStringIntoLines(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, maxWidth: number, metrics: FontMetrics, maxLines?: number, overflow?: TextOverflow, letterSpacing?: number, options?: LineOptions): string[];
30
72
  /**
31
73
  * Break styled segments into lines that fit within `maxWidth`. Same greedy word-splitting as the
32
74
  * string breaker; each line keeps the segments that landed on it, which is what `line-metrics.ts`