@jasy/pdf 1.0.0-alpha.1 → 1.0.0-alpha.10

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 (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -21
  3. package/dist/api/args.d.ts +1 -1
  4. package/dist/api/args.js +2 -5
  5. package/dist/api/color.d.ts +4 -4
  6. package/dist/api/color.js +11 -17
  7. package/dist/api/content.d.ts +20 -13
  8. package/dist/api/content.js +44 -29
  9. package/dist/api/descriptor.d.ts +2 -2
  10. package/dist/api/descriptor.js +121 -31
  11. package/dist/api/dimension.d.ts +76 -0
  12. package/dist/api/dimension.js +43 -0
  13. package/dist/api/font-url.d.ts +28 -0
  14. package/dist/api/font-url.js +134 -0
  15. package/dist/api/forms.d.ts +347 -0
  16. package/dist/api/forms.js +189 -0
  17. package/dist/api/gradient.d.ts +53 -0
  18. package/dist/api/gradient.js +97 -0
  19. package/dist/api/index.d.ts +13 -8
  20. package/dist/api/index.js +13 -24
  21. package/dist/api/insets.d.ts +19 -12
  22. package/dist/api/insets.js +19 -12
  23. package/dist/api/layout.d.ts +149 -25
  24. package/dist/api/layout.js +182 -51
  25. package/dist/api/page-builder.d.ts +40 -0
  26. package/dist/api/page-builder.js +44 -0
  27. package/dist/api/structure.d.ts +100 -13
  28. package/dist/api/structure.js +179 -88
  29. package/dist/api/table.d.ts +5 -5
  30. package/dist/api/table.js +38 -26
  31. package/dist/api/text.d.ts +90 -4
  32. package/dist/api/text.js +116 -31
  33. package/dist/assets/font-data.d.ts +2 -0
  34. package/dist/assets/font-data.js +21 -0
  35. package/dist/assets/font-data.ts +37 -0
  36. package/dist/common/color.js +1 -5
  37. package/dist/constants/page-sizes.js +3 -6
  38. package/dist/constants/pdf-parts.js +1 -4
  39. package/dist/crypto/legacy-handler.d.ts +43 -0
  40. package/dist/crypto/legacy-handler.js +145 -0
  41. package/dist/crypto/security-handler.d.ts +62 -0
  42. package/dist/crypto/security-handler.js +155 -0
  43. package/dist/crypto/webcrypto.d.ts +11 -0
  44. package/dist/crypto/webcrypto.js +62 -0
  45. package/dist/edit/acroform-reader.d.ts +70 -0
  46. package/dist/edit/acroform-reader.js +126 -0
  47. package/dist/edit/appearance.d.ts +47 -0
  48. package/dist/edit/appearance.js +171 -0
  49. package/dist/edit/document.d.ts +145 -0
  50. package/dist/edit/document.js +642 -0
  51. package/dist/edit/fill.d.ts +65 -0
  52. package/dist/edit/fill.js +424 -0
  53. package/dist/edit/flatten.d.ts +54 -0
  54. package/dist/edit/flatten.js +462 -0
  55. package/dist/edit/index.d.ts +20 -0
  56. package/dist/edit/index.js +18 -0
  57. package/dist/edit/lexer.d.ts +30 -0
  58. package/dist/edit/lexer.js +285 -0
  59. package/dist/edit/objects.d.ts +68 -0
  60. package/dist/edit/objects.js +83 -0
  61. package/dist/edit/writer.d.ts +60 -0
  62. package/dist/edit/writer.js +283 -0
  63. package/dist/elements/container-element.d.ts +50 -5
  64. package/dist/elements/container-element.js +100 -39
  65. package/dist/elements/forms/checkbox-element.d.ts +51 -0
  66. package/dist/elements/forms/checkbox-element.js +48 -0
  67. package/dist/elements/forms/choice-element.d.ts +53 -0
  68. package/dist/elements/forms/choice-element.js +59 -0
  69. package/dist/elements/forms/push-button-element.d.ts +56 -0
  70. package/dist/elements/forms/push-button-element.js +49 -0
  71. package/dist/elements/forms/radio-element.d.ts +52 -0
  72. package/dist/elements/forms/radio-element.js +49 -0
  73. package/dist/elements/forms/signature-element.d.ts +54 -0
  74. package/dist/elements/forms/signature-element.js +47 -0
  75. package/dist/elements/forms/text-field-element.d.ts +61 -0
  76. package/dist/elements/forms/text-field-element.js +54 -0
  77. package/dist/elements/image-element.d.ts +58 -6
  78. package/dist/elements/image-element.js +145 -111
  79. package/dist/elements/index.d.ts +26 -10
  80. package/dist/elements/index.js +26 -28
  81. package/dist/elements/layout/anchor-element.d.ts +23 -0
  82. package/dist/elements/layout/anchor-element.js +27 -0
  83. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  84. package/dist/elements/layout/bookmark-element.js +30 -0
  85. package/dist/elements/layout/default-text-style-element.d.ts +30 -0
  86. package/dist/elements/layout/default-text-style-element.js +47 -0
  87. package/dist/elements/layout/deferred-element.d.ts +3 -3
  88. package/dist/elements/layout/deferred-element.js +4 -8
  89. package/dist/elements/layout/expanded-element.d.ts +6 -4
  90. package/dist/elements/layout/expanded-element.js +22 -17
  91. package/dist/elements/layout/keep-together-element.d.ts +42 -0
  92. package/dist/elements/layout/keep-together-element.js +85 -0
  93. package/dist/elements/layout/link-element.d.ts +33 -0
  94. package/dist/elements/layout/link-element.js +39 -0
  95. package/dist/elements/layout/padding-element.d.ts +10 -5
  96. package/dist/elements/layout/padding-element.js +25 -16
  97. package/dist/elements/layout/page-break-element.d.ts +17 -0
  98. package/dist/elements/layout/page-break-element.js +24 -0
  99. package/dist/elements/layout/page-builder-element.d.ts +32 -0
  100. package/dist/elements/layout/page-builder-element.js +45 -0
  101. package/dist/elements/layout/positioned-element.d.ts +47 -0
  102. package/dist/elements/layout/positioned-element.js +73 -0
  103. package/dist/elements/layout/repeating-header-element.d.ts +3 -3
  104. package/dist/elements/layout/repeating-header-element.js +8 -12
  105. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  106. package/dist/elements/layout/rotated-box-element.js +54 -0
  107. package/dist/elements/layout/rotated-element.d.ts +32 -0
  108. package/dist/elements/layout/rotated-element.js +41 -0
  109. package/dist/elements/layout/sized-container-element.d.ts +2 -2
  110. package/dist/elements/layout/sized-container-element.js +6 -11
  111. package/dist/elements/layout/struct-group.d.ts +28 -0
  112. package/dist/elements/layout/struct-group.js +42 -0
  113. package/dist/elements/line-element.d.ts +3 -3
  114. package/dist/elements/line-element.js +5 -10
  115. package/dist/elements/page-element.d.ts +26 -6
  116. package/dist/elements/page-element.js +72 -27
  117. package/dist/elements/pdf-document-element.d.ts +10 -4
  118. package/dist/elements/pdf-document-element.js +11 -10
  119. package/dist/elements/pdf-element.d.ts +150 -4
  120. package/dist/elements/pdf-element.js +138 -20
  121. package/dist/elements/rectangle-element.d.ts +52 -11
  122. package/dist/elements/rectangle-element.js +129 -36
  123. package/dist/elements/row-element.d.ts +45 -4
  124. package/dist/elements/row-element.js +66 -18
  125. package/dist/elements/text-element.d.ts +132 -11
  126. package/dist/elements/text-element.js +206 -56
  127. package/dist/forms/acroform.d.ts +63 -0
  128. package/dist/forms/acroform.js +427 -0
  129. package/dist/forms/appearance.d.ts +58 -0
  130. package/dist/forms/appearance.js +204 -0
  131. package/dist/forms/field.d.ts +164 -0
  132. package/dist/forms/field.js +1 -0
  133. package/dist/forms/pdf.d.ts +19 -0
  134. package/dist/forms/pdf.js +28 -0
  135. package/dist/index.d.ts +3 -3
  136. package/dist/index.js +3 -19
  137. package/dist/ir/display-list.d.ts +197 -8
  138. package/dist/ir/display-list.js +8 -2
  139. package/dist/layout/alignment.d.ts +10 -0
  140. package/dist/layout/alignment.js +6 -0
  141. package/dist/layout/box-constraints.d.ts +61 -0
  142. package/dist/layout/box-constraints.js +88 -6
  143. package/dist/layout/collect-images.d.ts +9 -0
  144. package/dist/layout/collect-images.js +34 -0
  145. package/dist/layout/fragmentation.d.ts +20 -1
  146. package/dist/layout/fragmentation.js +74 -18
  147. package/dist/layout/insets.d.ts +28 -0
  148. package/dist/layout/insets.js +20 -0
  149. package/dist/platform/browser-fs.d.ts +2 -0
  150. package/dist/platform/browser-fs.js +9 -0
  151. package/dist/platform/browser-image.d.ts +5 -0
  152. package/dist/platform/browser-image.js +13 -0
  153. package/dist/platform/node-fs.d.ts +2 -0
  154. package/dist/platform/node-fs.js +10 -0
  155. package/dist/platform/node-image.d.ts +5 -0
  156. package/dist/platform/node-image.js +9 -0
  157. package/dist/renderer/anchor-renderer.d.ts +6 -0
  158. package/dist/renderer/anchor-renderer.js +11 -0
  159. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  160. package/dist/renderer/bookmark-renderer.js +11 -0
  161. package/dist/renderer/container-renderer.d.ts +3 -3
  162. package/dist/renderer/container-renderer.js +11 -28
  163. package/dist/renderer/default-text-style-renderer.d.ts +6 -0
  164. package/dist/renderer/default-text-style-renderer.js +10 -0
  165. package/dist/renderer/deferred-renderer.d.ts +3 -3
  166. package/dist/renderer/deferred-renderer.js +8 -23
  167. package/dist/renderer/emoji-image.d.ts +9 -0
  168. package/dist/renderer/emoji-image.js +68 -0
  169. package/dist/renderer/expanded-renderer.d.ts +3 -3
  170. package/dist/renderer/expanded-renderer.js +6 -21
  171. package/dist/renderer/form-field-renderer.d.ts +20 -0
  172. package/dist/renderer/form-field-renderer.js +12 -0
  173. package/dist/renderer/image-renderer.d.ts +4 -4
  174. package/dist/renderer/image-renderer.js +86 -75
  175. package/dist/renderer/index.d.ts +10 -10
  176. package/dist/renderer/index.js +10 -26
  177. package/dist/renderer/keep-together-renderer.d.ts +10 -0
  178. package/dist/renderer/keep-together-renderer.js +12 -0
  179. package/dist/renderer/line-renderer.d.ts +3 -3
  180. package/dist/renderer/line-renderer.js +13 -28
  181. package/dist/renderer/link-renderer.d.ts +6 -0
  182. package/dist/renderer/link-renderer.js +11 -0
  183. package/dist/renderer/padding-renderer.d.ts +3 -3
  184. package/dist/renderer/padding-renderer.js +6 -21
  185. package/dist/renderer/page-break-renderer.d.ts +14 -0
  186. package/dist/renderer/page-break-renderer.js +15 -0
  187. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  188. package/dist/renderer/page-builder-renderer.js +11 -0
  189. package/dist/renderer/page-renderer.d.ts +2 -2
  190. package/dist/renderer/page-renderer.js +115 -77
  191. package/dist/renderer/pdf-backend.d.ts +31 -4
  192. package/dist/renderer/pdf-backend.js +287 -49
  193. package/dist/renderer/pdf-config.js +4 -7
  194. package/dist/renderer/pdf-document-class.d.ts +5 -5
  195. package/dist/renderer/pdf-document-class.js +27 -41
  196. package/dist/renderer/pdf-document-renderer.d.ts +19 -10
  197. package/dist/renderer/pdf-document-renderer.js +135 -91
  198. package/dist/renderer/pdf-renderer.d.ts +2 -2
  199. package/dist/renderer/pdf-renderer.js +165 -90
  200. package/dist/renderer/positioned-renderer.d.ts +6 -0
  201. package/dist/renderer/positioned-renderer.js +10 -0
  202. package/dist/renderer/rectangle-renderer.d.ts +3 -3
  203. package/dist/renderer/rectangle-renderer.js +49 -44
  204. package/dist/renderer/repeating-header-renderer.d.ts +3 -3
  205. package/dist/renderer/repeating-header-renderer.js +10 -26
  206. package/dist/renderer/rotated-renderer.d.ts +7 -0
  207. package/dist/renderer/rotated-renderer.js +29 -0
  208. package/dist/renderer/row-renderer.d.ts +3 -3
  209. package/dist/renderer/row-renderer.js +11 -28
  210. package/dist/renderer/struct-group-renderer.d.ts +10 -0
  211. package/dist/renderer/struct-group-renderer.js +24 -0
  212. package/dist/renderer/text-renderer.d.ts +9 -5
  213. package/dist/renderer/text-renderer.js +437 -89
  214. package/dist/text/advance.d.ts +34 -0
  215. package/dist/text/advance.js +45 -0
  216. package/dist/text/arabic.d.ts +20 -0
  217. package/dist/text/arabic.js +101 -0
  218. package/dist/text/bidi.d.ts +33 -0
  219. package/dist/text/bidi.js +94 -0
  220. package/dist/text/emoji-codepoints.d.ts +1 -0
  221. package/dist/text/emoji-codepoints.js +29 -0
  222. package/dist/text/font-fallback.d.ts +25 -0
  223. package/dist/text/font-fallback.js +38 -0
  224. package/dist/text/line-breaker.d.ts +50 -12
  225. package/dist/text/line-breaker.js +158 -37
  226. package/dist/text/line-metrics.d.ts +49 -0
  227. package/dist/text/line-metrics.js +57 -0
  228. package/dist/text/orphans-widows.d.ts +27 -0
  229. package/dist/text/orphans-widows.js +53 -0
  230. package/dist/text/shape.d.ts +29 -0
  231. package/dist/text/shape.js +67 -0
  232. package/dist/text/text-decoration.d.ts +50 -0
  233. package/dist/text/text-decoration.js +62 -0
  234. package/dist/text/text-style.d.ts +60 -0
  235. package/dist/text/text-style.js +79 -0
  236. package/dist/utils/afm-parser.d.ts +34 -0
  237. package/dist/utils/afm-parser.js +76 -17
  238. package/dist/utils/bytes.d.ts +24 -0
  239. package/dist/utils/bytes.js +76 -0
  240. package/dist/utils/dest-registry.d.ts +22 -0
  241. package/dist/utils/dest-registry.js +32 -0
  242. package/dist/utils/flex-layout.d.ts +36 -9
  243. package/dist/utils/flex-layout.js +207 -30
  244. package/dist/utils/font-metrics.d.ts +20 -1
  245. package/dist/utils/font-metrics.js +1 -2
  246. package/dist/utils/font-path.js +3 -6
  247. package/dist/utils/gsub.d.ts +36 -0
  248. package/dist/utils/gsub.js +178 -0
  249. package/dist/utils/image-helper.d.ts +6 -5
  250. package/dist/utils/image-helper.js +101 -111
  251. package/dist/utils/inflate.d.ts +16 -0
  252. package/dist/utils/inflate.js +52 -0
  253. package/dist/utils/md5.d.ts +4 -0
  254. package/dist/utils/md5.js +79 -0
  255. package/dist/utils/outline.d.ts +21 -0
  256. package/dist/utils/outline.js +79 -0
  257. package/dist/utils/pdf-object-manager.d.ts +110 -8
  258. package/dist/utils/pdf-object-manager.js +583 -141
  259. package/dist/utils/renderer-registry.d.ts +13 -1
  260. package/dist/utils/renderer-registry.js +23 -6
  261. package/dist/utils/struct-tree.d.ts +44 -0
  262. package/dist/utils/struct-tree.js +129 -0
  263. package/dist/utils/ttf-parser.d.ts +134 -2
  264. package/dist/utils/ttf-parser.js +873 -50
  265. package/dist/utils/ttf-subsetter.d.ts +1 -1
  266. package/dist/utils/ttf-subsetter.js +40 -42
  267. package/dist/utils/ua-xmp.d.ts +9 -0
  268. package/dist/utils/ua-xmp.js +32 -0
  269. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  270. package/dist/utils/utf8-to-windows1252-encoder.js +11 -4
  271. package/dist/utils/woff.d.ts +10 -0
  272. package/dist/utils/woff.js +116 -0
  273. package/dist/validators/element-validator.d.ts +2 -2
  274. package/dist/validators/element-validator.js +31 -26
  275. package/package.json +22 -2
@@ -1,125 +1,473 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.TextRenderer = void 0;
13
- const pdf_element_1 = require("../elements/pdf-element");
14
- const line_breaker_1 = require("../text/line-breaker");
15
- // Distance from the top of a line down to its baseline, as a fraction of the font
16
- // size. ~0.683 is the standard-14 ascent ratio used to seat the first baseline.
17
- const BASELINE_RATIO = 683 / 1000;
18
- class TextRenderer {
1
+ import { Color } from "../common/color.js";
2
+ import { HorizontalAlignment } from "../elements/pdf-element.js";
3
+ import { VERTICAL_TEXT_SHIFT } from "../elements/text-element.js";
4
+ import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
5
+ import { emojiImageNode } from "./emoji-image.js";
6
+ import { singleLineWidth, MAX_SPACE_SHRINK, wrapStringIntoLines, breakSegmentsIntoLines, } from "../text/line-breaker.js";
7
+ import { lineBoxForSegmentLine, lineBoxForString } from "../text/line-metrics.js";
8
+ import { runAdvance } from "../text/advance.js";
9
+ import { visualRuns, visualRunsOf } from "../text/bidi.js";
10
+ import { skipInkSegments, strikethroughStroke, underlineStroke, } from "../text/text-decoration.js";
11
+ export class TextRenderer {
19
12
  // Measuring only needs metrics, not the full object manager. (The render pass below
20
13
  // still receives the manager because it also registers fonts/images.)
21
- static calculateTextHeight(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth) {
22
- // Plain string: one line height per wrapped line.
14
+ static calculateTextHeight(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, maxLines, overflow, lineHeight, letterSpacing = 0, lineOptions = {}) {
15
+ // Plain string: every wrapped line gets the same box.
23
16
  if (typeof content === "string") {
24
- const lines = (0, line_breaker_1.wrapStringIntoLines)(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager);
25
- return lines.length * fontSize;
17
+ const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow, letterSpacing, lineOptions);
18
+ const box = lineBoxForString(objectManager, fontFamily, fontStyle, fontSize, lineHeight);
19
+ return lines.length * box.height;
26
20
  }
27
- // Segments: each line contributes its own (tallest-on-line) leading.
28
- const lines = (0, line_breaker_1.breakSegmentsIntoLines)(content, { fontFamily, fontSize, fontStyle }, maxWidth, objectManager);
29
- return lines.reduce((total, line) => total + line.maxFontSize, 0);
21
+ // Segments: each line's box comes from the fonts actually on it.
22
+ const defaults = { fontFamily, fontSize, fontStyle, letterSpacing };
23
+ const lines = breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow);
24
+ return lines.reduce((total, line) => total + lineBoxForSegmentLine(line, defaults, objectManager, lineHeight).height, 0);
30
25
  }
31
- static render(textElement, objectManager) {
32
- return __awaiter(this, void 0, void 0, function* () {
33
- const { x, y, width, fontSize, color, content, fontFamily, fontStyle, textAlignment } = textElement.getProps();
34
- // Component -> display list. Wrapping and positioning stay here; the backend
35
- // turns each run into BT/Tf/Td/Tj/ET. The wrapping algorithm is unchanged from
36
- // the original renderer - unifying it into the engine is Phase 3.
37
- return TextRenderer._buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, width !== null && width !== void 0 ? width : Number.NaN, textAlignment, color, x, y);
38
- });
26
+ static async render(textElement, objectManager) {
27
+ const { x, y, width, fontSize, color, content, fontFamily, fontStyle, textAlignment, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, wordSpacing, textIndent, role, } = textElement.getProps();
28
+ // Component -> display list. Wrapping and positioning stay here; the backend
29
+ // turns each run into BT/Tf/Td/Tj/ET. The wrapping algorithm is unchanged from
30
+ // the original renderer - unifying it into the engine is Phase 3.
31
+ const { runs, links, decorations } = TextRenderer._buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, width ?? Number.NaN, textAlignment, color, x, y, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, wordSpacing, textIndent);
32
+ // Accessible tagging: this whole text block is one structure element (a paragraph P, or a heading
33
+ // from `role`); each run's text lands under it. Emoji sub-nodes produced below stay untagged (drawn
34
+ // as artifacts). Keyed by structId so a block split across pages stays one element.
35
+ if (objectManager.struct.enabled) {
36
+ const pdfRole = role ? role.toUpperCase() : "P"; // "h1" -> "H1"; default paragraph
37
+ const key = objectManager.struct.openElement(textElement.structId, pdfRole);
38
+ for (const run of runs)
39
+ run.tag = { role: pdfRole, key };
40
+ }
41
+ // Color fonts (COLR/CPAL): expand each run's emoji into filled vector layers (or fetched images
42
+ // for an image emoji source); plain runs and monochrome fonts pass straight through unchanged.
43
+ const drawn = (await Promise.all(runs.map((run) => TextRenderer._expandColorGlyphs(run, objectManager)))).flat();
44
+ // Underline / strikethrough go on TOP of the glyphs (a strikethrough must), and stay untagged,
45
+ // so the structure tree treats them as artifacts rather than as text.
46
+ // Inline hyperlinks (href spans) ride along as /Link annotations - they draw nothing, so order
47
+ // vs the drawn runs does not matter; the page renderer peels them off into /Annots.
48
+ return [...drawn, ...decorations, ...links];
49
+ }
50
+ // Splits a text run into normal text sub-runs + filled `Path` layers for any COLR color glyphs,
51
+ // walking code point by code point and advancing the pen by each glyph's width. Returns `[run]`
52
+ // untouched when the font has no color glyphs (the common case).
53
+ static async _expandColorGlyphs(run, om) {
54
+ // The run's own font (if it's a color font) draws emoji directly; otherwise a document-level
55
+ // emoji source (`Document({ emoji })`) supplies them - a fallback FONT (color glyphs, native
56
+ // vector) or an IMAGE source (fetched emoji images) - for code points the run's font lacks, so
57
+ // `Text("Hallo 😅")` works in one string. No emoji source at all -> pass the run through.
58
+ const own = om.getColorFont(run.fontFamily, run.fontStyle);
59
+ const emojiName = om.getEmojiFont();
60
+ const fallback = emojiName && emojiName !== run.fontFamily
61
+ ? om.getColorFont(emojiName, run.fontStyle)
62
+ : undefined;
63
+ const imageSource = om.getEmojiImageSource();
64
+ if (!own && !fallback && !imageSource)
65
+ return [run];
66
+ // A SHAPED run cannot be split here: the sub-runs are cut by code point, while `glyphs` is a
67
+ // drawn-order list in which a ligature is one glyph for several code points - so no sub-run could
68
+ // be given the right slice, and inheriting the whole list would draw every glyph again per piece.
69
+ // Passed through whole instead: an emoji inside Arabic then comes from the text font rather than
70
+ // the emoji source (`todo.md`).
71
+ if (run.glyphs)
72
+ return [run];
73
+ const nodes = [];
74
+ let cursorX = run.x; // absolute x of the next glyph
75
+ let pending = ""; // run of consecutive non-color characters
76
+ let pendingX = run.x; // where that run started
77
+ const flushPending = () => {
78
+ if (pending) {
79
+ nodes.push({ ...run, x: pendingX, text: pending });
80
+ pending = "";
81
+ }
82
+ };
83
+ for (const ch of run.text) {
84
+ const cp = ch.codePointAt(0);
85
+ // Which color font renders this code point: the run's own font first, then the emoji fallback.
86
+ let source;
87
+ let layers = null;
88
+ if (own) {
89
+ const l = own.getColorGlyph(own.getGlyphIndex(cp));
90
+ if (l)
91
+ [source, layers] = [own, l];
92
+ }
93
+ if (!layers && fallback) {
94
+ const l = fallback.getColorGlyph(fallback.getGlyphIndex(cp));
95
+ if (l)
96
+ [source, layers] = [fallback, l];
97
+ }
98
+ // The advance matches measuring (getCharWidth applies the same emoji fallback), plus the run's
99
+ // letter-spacing so a spaced run's emoji sit where `Tc` advances them.
100
+ const advance = om.getCharWidth(ch, run.fontSize, undefined, run.fontFamily, run.fontStyle) +
101
+ (run.letterSpacing ?? 0);
102
+ if (source && layers) {
103
+ flushPending();
104
+ const scale = run.fontSize / source.unitsPerEm;
105
+ for (const layer of layers) {
106
+ const commands = TextRenderer._glyphToPath(source.getGlyphPath(layer.glyphId), cursorX, run.y, scale, layer.transform);
107
+ if (commands.length === 0)
108
+ continue; // an empty layer outline draws nothing
109
+ const paint = layer.paint;
110
+ let fill;
111
+ if (paint.type === "solid") {
112
+ const c = paint.color;
113
+ fill = c ? new Color(c.r, c.g, c.b, c.a / 255) : run.color; // null -> foreground
114
+ }
115
+ else {
116
+ fill = TextRenderer._toGradient(paint, cursorX, run.y, scale, run.color, layer.transform);
117
+ }
118
+ nodes.push({ type: "path", commands, fill });
119
+ }
120
+ }
121
+ else if (imageSource && isEmojiCodePoint(cp)) {
122
+ // Image emoji source: fetch + embed a 1em square. The emoji breaks the text run either way;
123
+ // a failed fetch (offline/404) leaves a blank em gap (advance already applied) rather than
124
+ // drawing a broken glyph.
125
+ flushPending();
126
+ const node = await emojiImageNode(cp, imageSource, cursorX, run.y, run.fontSize);
127
+ if (node)
128
+ nodes.push(node);
129
+ }
130
+ else {
131
+ if (!pending)
132
+ pendingX = cursorX;
133
+ pending += ch;
134
+ }
135
+ cursorX += advance;
136
+ }
137
+ flushPending();
138
+ return nodes;
139
+ }
140
+ // Maps a glyph outline (font units, y-up) to absolute IR path commands at the pen origin, scaling
141
+ // by `scale` and flipping the axis into the engine's top-left space (the seam flips it once more
142
+ // to PDF space). TrueType quadratics become cubics: C1 = P0 + 2/3(C-P0), C2 = P1 + 2/3(C-P1).
143
+ static _glyphToPath(cmds, originX, baselineY, scale, transform) {
144
+ // Apply the layer's affine (font units) first, then scale to points, position at the pen origin
145
+ // and flip into engine space. `mapX`/`mapY` take BOTH glyph coords because an affine mixes them.
146
+ const [a, b, c, d, e, f] = transform ?? [1, 0, 0, 1, 0, 0];
147
+ const mapX = (gx, gy) => originX + (a * gx + c * gy + e) * scale;
148
+ const mapY = (gx, gy) => baselineY - (b * gx + d * gy + f) * scale;
149
+ const out = [];
150
+ let curX = 0;
151
+ let curY = 0;
152
+ for (const cmd of cmds) {
153
+ if (cmd.type === "M") {
154
+ curX = mapX(cmd.x, cmd.y);
155
+ curY = mapY(cmd.x, cmd.y);
156
+ out.push({ op: "m", x: curX, y: curY });
157
+ }
158
+ else if (cmd.type === "L") {
159
+ curX = mapX(cmd.x, cmd.y);
160
+ curY = mapY(cmd.x, cmd.y);
161
+ out.push({ op: "l", x: curX, y: curY });
162
+ }
163
+ else if (cmd.type === "Q") {
164
+ const ctrlX = mapX(cmd.cx, cmd.cy);
165
+ const ctrlY = mapY(cmd.cx, cmd.cy);
166
+ const endX = mapX(cmd.x, cmd.y);
167
+ const endY = mapY(cmd.x, cmd.y);
168
+ out.push({
169
+ op: "c",
170
+ x1: curX + (2 / 3) * (ctrlX - curX),
171
+ y1: curY + (2 / 3) * (ctrlY - curY),
172
+ x2: endX + (2 / 3) * (ctrlX - endX),
173
+ y2: endY + (2 / 3) * (ctrlY - endY),
174
+ x: endX,
175
+ y: endY,
176
+ });
177
+ curX = endX;
178
+ curY = endY;
179
+ }
180
+ else {
181
+ out.push({ op: "z" });
182
+ }
183
+ }
184
+ return out;
185
+ }
186
+ // Maps a COLR v1 gradient paint (font-unit coordinates) into an IR `Gradient` positioned at the
187
+ // pen origin, using the SAME scale + baseline flip as the glyph outline so the gradient axis lines
188
+ // up with the shape. Radii scale uniformly (the em scale is uniform). A null stop color (the COLR
189
+ // "use foreground" sentinel) becomes the text color.
190
+ static _toGradient(paint, originX, baselineY, scale, foreground, transform) {
191
+ const [a, b, c, d, e, f] = transform ?? [1, 0, 0, 1, 0, 0];
192
+ const mapX = (gx, gy) => originX + (a * gx + c * gy + e) * scale;
193
+ const mapY = (gx, gy) => baselineY - (b * gx + d * gy + f) * scale;
194
+ // A radius has no single value under a general affine (a circle can shear into an ellipse); use
195
+ // the affine's geometric-mean scale sqrt(|det|) times the em scale - exact for uniform scaling.
196
+ const radiusScale = scale * Math.sqrt(Math.abs(a * d - b * c));
197
+ const stops = paint.stops.map((s) => ({
198
+ offset: s.offset,
199
+ color: s.color ? new Color(s.color.r, s.color.g, s.color.b, s.color.a / 255) : foreground,
200
+ }));
201
+ if (paint.type === "linearGradient") {
202
+ return {
203
+ type: "linear",
204
+ x0: mapX(paint.p0[0], paint.p0[1]),
205
+ y0: mapY(paint.p0[0], paint.p0[1]),
206
+ x1: mapX(paint.p1[0], paint.p1[1]),
207
+ y1: mapY(paint.p1[0], paint.p1[1]),
208
+ stops,
209
+ extend: paint.extend,
210
+ };
211
+ }
212
+ return {
213
+ type: "radial",
214
+ x0: mapX(paint.c0[0], paint.c0[1]),
215
+ y0: mapY(paint.c0[0], paint.c0[1]),
216
+ r0: paint.c0[2] * radiusScale,
217
+ x1: mapX(paint.c1[0], paint.c1[1]),
218
+ y1: mapY(paint.c1[0], paint.c1[1]),
219
+ r1: paint.c1[2] * radiusScale,
220
+ stops,
221
+ extend: paint.extend,
222
+ };
39
223
  }
40
224
  // Lay the content out into absolutely-positioned text runs. Glyph positions match
41
225
  // the previous hand-written operators exactly (verified by pixel-identical render);
42
226
  // only the output form changed from PDF strings to `TextRun`s.
43
- static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition) {
227
+ static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition, maxLines, overflow, lineHeight, underline = false, strikethrough = false, skipInk = false, letterSpacing = 0, direction = "ltr", wordSpacing = 0, textIndent = 0) {
44
228
  const runs = [];
229
+ // /Link annotation rects for any `href` spans, collected as we place segments. Empty for the
230
+ // common (no-href) case, so plain text keeps producing exactly the runs it did before.
231
+ const links = [];
232
+ // Underline / strikethrough strokes, one per decorated run (so a wrapped paragraph gets one
233
+ // per line, and a decorated span only spans its own glyphs).
234
+ const decorations = [];
45
235
  // Horizontal offset of a line of the given width under the current alignment.
46
- const alignmentOffset = (lineWidth) => {
47
- if (textAlignment === pdf_element_1.HorizontalAlignment.center)
48
- return (maxWidth - lineWidth) / 2;
49
- if (textAlignment === pdf_element_1.HorizontalAlignment.right)
50
- return maxWidth - lineWidth;
236
+ // `start` is the one alignment that depends on the direction, resolved at the single place
237
+ // alignment is consumed.
238
+ const align = textAlignment === HorizontalAlignment.start
239
+ ? direction === "rtl"
240
+ ? HorizontalAlignment.right
241
+ : HorizontalAlignment.left
242
+ : textAlignment;
243
+ const alignmentOffset = (lineWidth, indent = 0) => {
244
+ const room = maxWidth - indent;
245
+ if (align === HorizontalAlignment.center)
246
+ return (room - lineWidth) / 2;
247
+ if (align === HorizontalAlignment.right)
248
+ return room - lineWidth;
51
249
  return 0;
52
250
  };
53
- // Advance width WITHOUT kerning. This is how Tj moves the text cursor, so
54
- // segments flowing after each other land here. (Standard-14 fonts carry no
55
- // /Widths array; the viewer advances by the AFM widths - same source as below.)
56
- const advanceNoKerning = (text, family, size, style) => {
57
- let width = 0;
58
- for (const ch of text) {
59
- width += objectManager.getCharWidth(ch, size, undefined, family, style);
251
+ /**
252
+ * Justification: how much extra to put in EACH space of a line so it reaches the full width.
253
+ * The LAST line of a paragraph keeps its natural spacing, as it does in CSS and in print - a
254
+ * justified last line is the classic sign of a broken text engine.
255
+ *
256
+ * Spaces are stretched by moving the pen, not by the `Tw` operator: `Tw` applies only to the
257
+ * single byte 32, so an embedded (Identity-H) font would ignore it entirely.
258
+ */
259
+ const justifyExtra = (line, naturalWidth, isLast, indent = 0) => {
260
+ if (align !== HorizontalAlignment.justify)
261
+ return 0;
262
+ const gaps = [...line].filter((c) => c === " ").length;
263
+ if (gaps === 0)
264
+ return 0;
265
+ const slack = maxWidth - indent - naturalWidth;
266
+ // Negative slack is real now: the breaker keeps a word when the line can be SQUEEZED to hold
267
+ // it, so justification has to deliver that squeeze. The floor is the same limit the breaker
268
+ // used, which is what stops a line it never promised from being crushed.
269
+ const spaceWidth = runAdvance(objectManager, " ", { fontFamily, fontSize, fontStyle }, letterSpacing);
270
+ const extra = Math.max(slack / gaps, -spaceWidth * MAX_SPACE_SHRINK);
271
+ // The last line is never STRETCHED - the rule print and CSS follow. It may still be SQUEEZED:
272
+ // the breaker was allowed to pack it that way, and drawing it loose would push it out of its box.
273
+ return isLast ? Math.min(0, extra) : extra;
274
+ };
275
+ /**
276
+ * Push one drawn piece and return the advance it took. When the line is justified the pen jumps an
277
+ * extra `extra` at every space, which means one run per word - the only way that works for an
278
+ * embedded font, whose spaces `Tw` cannot reach.
279
+ *
280
+ * A SHAPED piece is never split: its glyph list is in drawn order with ligatures merged, so no
281
+ * slice of it is correct. Such a piece keeps its natural spacing.
282
+ */
283
+ const emitPiece = (piece, x, extra, style, runFont, spacing) => {
284
+ const push = (at, text, glyphs) => {
285
+ runs.push({ type: "text", x: at, text, ...(glyphs ? { glyphs } : {}), ...style });
286
+ };
287
+ if (extra === 0 || piece.glyphs || !piece.text.includes(" ")) {
288
+ push(x, piece.text, piece.glyphs);
289
+ return runAdvance(objectManager, piece.text, runFont, spacing);
290
+ }
291
+ const spaceWidth = runAdvance(objectManager, " ", runFont, spacing);
292
+ const words = piece.text.split(" ");
293
+ let pen = x;
294
+ words.forEach((word, i) => {
295
+ if (word !== "") {
296
+ push(pen, word);
297
+ pen += runAdvance(objectManager, word, runFont, spacing);
298
+ }
299
+ if (i < words.length - 1)
300
+ pen += spaceWidth + extra;
301
+ });
302
+ return pen - x;
303
+ };
304
+ // The decoration strokes for one drawn run, at the geometry ITS OWN font declares. A mixed-size
305
+ // line therefore gets a thicker line under the bigger span, which is what a browser does too.
306
+ const decorate = (text, runX, runWidth, baselineY, family, style, size, runColor, wantsUnderline, wantsStrikethrough, runLetterSpacing) => {
307
+ if ((!wantsUnderline && !wantsStrikethrough) || runWidth <= 0)
308
+ return;
309
+ const metrics = objectManager.getFontDecoration(family, style);
310
+ const push = (from, to, stroke) => {
311
+ decorations.push({
312
+ type: "line",
313
+ x1: runX + from,
314
+ y1: stroke.y,
315
+ x2: runX + to,
316
+ y2: stroke.y,
317
+ stroke: runColor,
318
+ strokeWidth: stroke.thickness,
319
+ });
320
+ };
321
+ if (wantsUnderline) {
322
+ const stroke = underlineStroke(metrics, size, baselineY);
323
+ for (const [from, to] of underlineSegments(text, runWidth, baselineY, size, family, style, stroke, runLetterSpacing)) {
324
+ push(from, to, stroke);
325
+ }
326
+ }
327
+ // A strikethrough crosses the letters by definition, so it is never interrupted.
328
+ if (wantsStrikethrough) {
329
+ const stroke = strikethroughStroke(metrics, size, baselineY);
330
+ push(0, runWidth, stroke);
331
+ }
332
+ };
333
+ // The x-ranges the underline actually draws. Without `skipInk` that is the whole run; with it,
334
+ // the descenders are cut out of it (which needs the real outlines, i.e. an embedded font).
335
+ const underlineSegments = (text, runWidth, baselineY, size, family, style, stroke, runLetterSpacing) => {
336
+ if (!skipInk)
337
+ return [[0, runWidth]];
338
+ if (!objectManager.isCustomFont(family, style)) {
339
+ throw new Error(`skipInk needs an embedded font, but "${family}" is a standard-14 font whose glyph outlines ` +
340
+ `live in the PDF viewer, not in the document - we cannot see where its ink is. Register a ` +
341
+ `font (the document's \`fonts\` option) or drop \`skipInk\`.`);
60
342
  }
61
- return width;
343
+ // The band the stroke covers, in points from the baseline, y UP (so both values are negative).
344
+ const below = stroke.y - baselineY;
345
+ const half = stroke.thickness / 2;
346
+ const inkSpans = objectManager.getInkSpansInBand(text, size, family, style, -below + half, -below - half, runLetterSpacing);
347
+ return skipInkSegments(runWidth, inkSpans, stroke.thickness);
348
+ };
349
+ /**
350
+ * One drawn piece of a line. SHAPING must see the run's LOGICAL text - a letter's form comes from
351
+ * its neighbours, and a reversed run has those swapped - so we shape that and then reverse the
352
+ * GLYPHS for an rtl run. A font that does not shape keeps the old path exactly: draw the visual
353
+ * text, no glyph list, byte-identical output.
354
+ */
355
+ const shapedPiece = (part, family, style) => {
356
+ const shaped = objectManager.shapeText(part.logical, family, style);
357
+ if (!shaped)
358
+ return { text: part.text };
359
+ const ordered = part.rtl ? [...shaped].reverse() : shaped;
360
+ return { text: part.logical, glyphs: ordered.map((g) => g.glyph) };
62
361
  };
362
+ // The advance the viewer will use: plain glyph widths (a `Tj` never kerns) plus one
363
+ // `letterSpacing` per code point (the `Tc` operator), from the one shared `advance.ts` primitive.
364
+ const font = { fontFamily, fontSize, fontStyle };
63
365
  // --- Plain string: one run per wrapped line. ---
64
366
  if (typeof content === "string") {
65
- const lines = (0, line_breaker_1.wrapStringIntoLines)(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager);
66
- // yPosition is the top of the text box (top-left); seat line 0's baseline below
67
- // it, then step DOWN per line. The seam flips the whole thing to PDF space.
68
- const baseline = yPosition + fontSize * BASELINE_RATIO;
367
+ const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow, letterSpacing,
368
+ // The SAME knobs the measure pass used (`TextElement.lineOptions`), or a line would be
369
+ // measured one width and drawn another.
370
+ {
371
+ wordSpacing,
372
+ indent: textIndent,
373
+ shrink: align === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
374
+ });
375
+ // yPosition is the top of the text box (top-left). The line box seats its own baseline; lines
376
+ // then stack by that box's height. Same numbers as calculateTextHeight, by construction.
377
+ const box = lineBoxForString(objectManager, fontFamily, fontStyle, fontSize, lineHeight);
69
378
  lines.forEach((line, index) => {
70
- const lineWidth = objectManager.getStringWidth(line, fontFamily, fontSize, fontStyle);
71
- runs.push({
72
- type: "text",
73
- x: initialX + alignmentOffset(lineWidth),
74
- y: baseline + fontSize * index,
75
- text: line,
76
- fontFamily,
77
- fontStyle,
78
- fontSize,
79
- color,
80
- });
379
+ const lineWidth = singleLineWidth(line, font, objectManager, letterSpacing, wordSpacing);
380
+ // CSS `text-indent`: the FIRST line starts in, and had that much less room to fill.
381
+ const indent = index === 0 ? textIndent : 0;
382
+ let x = initialX + indent + alignmentOffset(lineWidth, indent);
383
+ const baseline = yPosition + box.baseline + box.height * index;
384
+ // Latin-only text in an `ltr` paragraph comes back as one unchanged piece.
385
+ // Word-spacing is a per-gap advance just like the justification slack, so it rides the same
386
+ // mechanism: `Tw` would reach only the single byte 32 and miss every embedded font.
387
+ const extra = wordSpacing + justifyExtra(line, lineWidth, index === lines.length - 1, indent);
388
+ for (const part of visualRuns(line, direction)) {
389
+ const piece = shapedPiece(part, fontFamily, fontStyle);
390
+ const partWidth = emitPiece(piece, x, extra, {
391
+ y: baseline,
392
+ fontFamily,
393
+ fontStyle,
394
+ fontSize,
395
+ color,
396
+ ...(letterSpacing ? { letterSpacing } : {}),
397
+ }, font, letterSpacing);
398
+ decorate(piece.text, x, partWidth, baseline, fontFamily, fontStyle, fontSize, color, underline, strikethrough, letterSpacing);
399
+ x += partWidth;
400
+ }
81
401
  });
82
- return runs;
402
+ return { runs, links, decorations };
83
403
  }
84
404
  // --- Segments: break into lines (shared breaker), then emit one run per segment.
85
405
  // Segment 0 starts at the aligned line origin; each following segment is offset by
86
406
  // the previous segment's kerning-free advance. Each line drops by its OWN leading
87
407
  // (tallest font on that line), so mixed-font lines space correctly and the drawn
88
408
  // height matches the measured height.
89
- const pushLine = (line, lineY) => {
409
+ const pushLine = (line, lineY, isLast) => {
90
410
  let x = initialX + alignmentOffset(line.width);
91
- line.segments.forEach((segment) => {
411
+ const extra = justifyExtra(line.segments.map((seg) => seg.content).join(""), line.width, isLast);
412
+ // Across span boundaries, so a Hebrew span is ordered against its neighbours rather than inside
413
+ // itself. Each run keeps the segment it came from, and with it that span's style.
414
+ const ordered = visualRunsOf(line.segments.map((segment) => ({ text: segment.content, source: segment })), direction);
415
+ ordered.forEach((part) => {
416
+ const segment = part.source;
92
417
  const family = segment.fontFamily || fontFamily;
93
418
  const size = segment.fontSize || fontSize;
94
419
  const style = segment.fontStyle || fontStyle;
95
- runs.push({
96
- type: "text",
97
- x,
98
- y: lineY,
99
- text: segment.content,
420
+ const segLetterSpacing = segment.letterSpacing ?? letterSpacing;
421
+ const piece = shapedPiece(part, family, style);
422
+ const runText = piece.text;
423
+ const runColor = segment.fontColor || color;
424
+ // `vertical-align`: this run alone leaves the line's baseline. Its own decoration and link box
425
+ // ride along, since both are placed from the same y.
426
+ const shift = VERTICAL_TEXT_SHIFT[segment.verticalAlign ?? "baseline"] * size;
427
+ const runY = lineY - shift;
428
+ const drawn = emitPiece(piece, x, extra, {
429
+ y: runY,
100
430
  fontFamily: family,
101
431
  fontStyle: style,
102
432
  fontSize: size,
103
- color: segment.fontColor || color,
104
- });
105
- x += advanceNoKerning(segment.content, family, size, style);
433
+ color: runColor,
434
+ ...(segLetterSpacing ? { letterSpacing: segLetterSpacing } : {}),
435
+ }, { fontFamily: family, fontSize: size, fontStyle: style }, segLetterSpacing);
436
+ decorate(runText, x, drawn, runY, family, style, size, runColor, segment.underline ?? underline, segment.strikethrough ?? strikethrough, segLetterSpacing);
437
+ // An href/to span becomes a /Link annotation over exactly this run's glyph box: from its own
438
+ // ascent above the baseline down to its own descender. A span wrapped across lines yields
439
+ // one rect per line.
440
+ if (segment.href !== undefined || segment.dest !== undefined) {
441
+ const v = objectManager.getFontVerticals(family, style);
442
+ links.push({
443
+ type: "link",
444
+ x,
445
+ y: runY - v.ascent * size,
446
+ width: drawn,
447
+ height: (v.ascent + v.descent) * size,
448
+ href: segment.href,
449
+ dest: segment.dest,
450
+ });
451
+ }
452
+ x += drawn;
106
453
  });
107
454
  };
108
- // The overall tallest font seats the first baseline; each line then steps DOWN by
109
- // its own leading. yPosition is the top of the text box (top-left); the seam flips
110
- // the whole thing to PDF space.
111
- let overallMaxFont = fontSize;
112
- for (const segment of content) {
113
- const size = segment.fontSize || fontSize;
114
- if (size > overallMaxFont)
115
- overallMaxFont = size;
116
- }
117
- let lineY = yPosition + overallMaxFont * BASELINE_RATIO;
118
- for (const line of (0, line_breaker_1.breakSegmentsIntoLines)(content, { fontFamily, fontSize, fontStyle }, maxWidth, objectManager)) {
119
- pushLine(line, lineY);
120
- lineY += line.maxFontSize;
121
- }
122
- return runs;
455
+ // yPosition is the top of the text box (top-left). Each line seats its own baseline inside its
456
+ // own box (tallest ascent / deepest descent ON THAT LINE), then the next line starts below it.
457
+ // The seam flips the whole thing to PDF space. `letterSpacing` MUST be in the defaults so a span
458
+ // that does not override it wraps and aligns with the element's spacing - the same defaults the
459
+ // measure path (calculateTextHeight) uses, or segmented spaced text mis-wraps (measured != drawn).
460
+ const defaults = { fontFamily, fontSize, fontStyle, letterSpacing };
461
+ let top = yPosition;
462
+ // Materialised, because justification has to know which line is the LAST one of the paragraph.
463
+ const segmentLines = [
464
+ ...breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow),
465
+ ];
466
+ segmentLines.forEach((line, index) => {
467
+ const box = lineBoxForSegmentLine(line, defaults, objectManager, lineHeight);
468
+ pushLine(line, top + box.baseline, index === segmentLines.length - 1);
469
+ top += box.height;
470
+ });
471
+ return { runs, links, decorations };
123
472
  }
124
473
  }
125
- exports.TextRenderer = TextRenderer;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * How wide a run of text is: the horizontal counterpart to `line-metrics.ts` (how tall a line is)
3
+ * and `line-breaker.ts` (where a line breaks). The ONE canonical answer to "how far does the pen
4
+ * move", so measuring and drawing can never disagree about it.
5
+ *
6
+ * An advance has three parts, each with its own PDF mechanism:
7
+ *
8
+ * advance = sum(glyph widths) + sum(kerning) + n * letterSpacing
9
+ * /Widths TJ array Tc
10
+ *
11
+ * `letterSpacing` is added after EVERY code point, the last one included - exactly what the `Tc`
12
+ * operator does, and exactly what CSS `letter-spacing` does (which is why right-aligned spaced text
13
+ * looks a hair offset: a spacing hangs off the final glyph). Measuring `(n - 1)` spacings while `Tc`
14
+ * applies `n` would make every line draw one spacing wider than it was wrapped - text out of its box.
15
+ *
16
+ * Kerning is added ONLY when the document has it on (`metrics.kerningEnabled`), and then the backend
17
+ * emits a `TJ` with the same per-pair adjustments - so a measured advance always equals the drawn
18
+ * one. With kerning off, `getStringWidth` is the whole advance (its plain glyph sum), byte-identical
19
+ * to before. Folding kerning into the measurement while the output ignored it was the 2026-07-10 bug.
20
+ */
21
+ import type { FontStyle } from "../utils/pdf-object-manager.js";
22
+ import type { FontMetrics } from "../utils/font-metrics.js";
23
+ /** The font a run is set in. */
24
+ export interface RunFont {
25
+ fontFamily: string;
26
+ fontSize: number;
27
+ fontStyle: FontStyle;
28
+ }
29
+ /** Number of Unicode code points (not UTF-16 units): an astral char is ONE, so it takes one
30
+ * letter-spacing, not two. Matches how `getStringWidth` iterates. */
31
+ export declare function codePointCount(text: string): number;
32
+ /** The advance of `text` in points: the plain glyph widths, plus kerning (if the document has it on),
33
+ * plus one `letterSpacing` per code point. */
34
+ export declare function runAdvance(metrics: FontMetrics, text: string, font: RunFont, letterSpacing?: number): number;