pretext-pdf 1.1.1 → 1.6.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 (342) hide show
  1. package/CHANGELOG.md +659 -0
  2. package/README.md +82 -7
  3. package/dist/allowed-props.d.ts +76 -0
  4. package/dist/allowed-props.d.ts.map +1 -1
  5. package/dist/allowed-props.js.map +1 -1
  6. package/dist/assets/generators/barcode.d.ts +9 -0
  7. package/dist/assets/generators/barcode.d.ts.map +1 -0
  8. package/dist/assets/generators/barcode.js +24 -0
  9. package/dist/assets/generators/barcode.js.map +1 -0
  10. package/dist/assets/generators/chart.d.ts +13 -0
  11. package/dist/assets/generators/chart.d.ts.map +1 -0
  12. package/dist/assets/generators/chart.js +32 -0
  13. package/dist/assets/generators/chart.js.map +1 -0
  14. package/dist/assets/generators/qr.d.ts +9 -0
  15. package/dist/assets/generators/qr.d.ts.map +1 -0
  16. package/dist/assets/generators/qr.js +25 -0
  17. package/dist/assets/generators/qr.js.map +1 -0
  18. package/dist/assets/index.d.ts +19 -0
  19. package/dist/assets/index.d.ts.map +1 -0
  20. package/dist/assets/index.js +19 -0
  21. package/dist/assets/index.js.map +1 -0
  22. package/dist/assets/loaders/images.d.ts +20 -0
  23. package/dist/assets/loaders/images.d.ts.map +1 -0
  24. package/dist/assets/loaders/images.js +69 -0
  25. package/dist/assets/loaders/images.js.map +1 -0
  26. package/dist/assets/loaders/orchestrator.d.ts +24 -0
  27. package/dist/assets/loaders/orchestrator.d.ts.map +1 -0
  28. package/dist/assets/loaders/orchestrator.js +109 -0
  29. package/dist/assets/loaders/orchestrator.js.map +1 -0
  30. package/dist/assets/loaders/vectors.d.ts +25 -0
  31. package/dist/assets/loaders/vectors.d.ts.map +1 -0
  32. package/dist/assets/loaders/vectors.js +118 -0
  33. package/dist/assets/loaders/vectors.js.map +1 -0
  34. package/dist/assets/loaders/watermark.d.ts +12 -0
  35. package/dist/assets/loaders/watermark.d.ts.map +1 -0
  36. package/dist/assets/loaders/watermark.js +40 -0
  37. package/dist/assets/loaders/watermark.js.map +1 -0
  38. package/dist/assets/security/fetch.d.ts +14 -0
  39. package/dist/assets/security/fetch.d.ts.map +1 -0
  40. package/dist/assets/security/fetch.js +112 -0
  41. package/dist/assets/security/fetch.js.map +1 -0
  42. package/dist/assets/security/ipv4-normalize.d.ts +28 -0
  43. package/dist/assets/security/ipv4-normalize.d.ts.map +1 -0
  44. package/dist/assets/security/ipv4-normalize.js +116 -0
  45. package/dist/assets/security/ipv4-normalize.js.map +1 -0
  46. package/dist/assets/security/path-allowlist.d.ts +12 -0
  47. package/dist/assets/security/path-allowlist.d.ts.map +1 -0
  48. package/dist/assets/security/path-allowlist.js +26 -0
  49. package/dist/assets/security/path-allowlist.js.map +1 -0
  50. package/dist/assets/security/url-validation.d.ts +22 -0
  51. package/dist/assets/security/url-validation.d.ts.map +1 -0
  52. package/dist/assets/security/url-validation.js +164 -0
  53. package/dist/assets/security/url-validation.js.map +1 -0
  54. package/dist/assets/svg/dimensions.d.ts +19 -0
  55. package/dist/assets/svg/dimensions.d.ts.map +1 -0
  56. package/dist/assets/svg/dimensions.js +43 -0
  57. package/dist/assets/svg/dimensions.js.map +1 -0
  58. package/dist/assets/svg/rasterize.d.ts +6 -0
  59. package/dist/assets/svg/rasterize.d.ts.map +1 -0
  60. package/dist/assets/svg/rasterize.js +38 -0
  61. package/dist/assets/svg/rasterize.js.map +1 -0
  62. package/dist/assets/svg/resolve-content.d.ts +16 -0
  63. package/dist/assets/svg/resolve-content.d.ts.map +1 -0
  64. package/dist/assets/svg/resolve-content.js +38 -0
  65. package/dist/assets/svg/resolve-content.js.map +1 -0
  66. package/dist/assets/svg/sanitize.d.ts +22 -0
  67. package/dist/assets/svg/sanitize.d.ts.map +1 -0
  68. package/dist/assets/svg/sanitize.js +46 -0
  69. package/dist/assets/svg/sanitize.js.map +1 -0
  70. package/dist/assets/util/redact-path.d.ts +14 -0
  71. package/dist/assets/util/redact-path.d.ts.map +1 -0
  72. package/dist/assets/util/redact-path.js +16 -0
  73. package/dist/assets/util/redact-path.js.map +1 -0
  74. package/dist/assets.d.ts +10 -27
  75. package/dist/assets.d.ts.map +1 -1
  76. package/dist/assets.js +10 -549
  77. package/dist/assets.js.map +1 -1
  78. package/dist/builder.d.ts.map +1 -1
  79. package/dist/builder.js +2 -1
  80. package/dist/builder.js.map +1 -1
  81. package/dist/cli.js +11 -1
  82. package/dist/cli.js.map +1 -1
  83. package/dist/compat.d.ts +63 -1
  84. package/dist/compat.d.ts.map +1 -1
  85. package/dist/compat.js +42 -5
  86. package/dist/compat.js.map +1 -1
  87. package/dist/errors.d.ts +2 -2
  88. package/dist/errors.d.ts.map +1 -1
  89. package/dist/errors.js +2 -2
  90. package/dist/errors.js.map +1 -1
  91. package/dist/fonts.d.ts.map +1 -1
  92. package/dist/fonts.js +8 -10
  93. package/dist/fonts.js.map +1 -1
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +7 -5
  97. package/dist/index.js.map +1 -1
  98. package/dist/layout-state.d.ts +1 -1
  99. package/dist/layout-state.d.ts.map +1 -1
  100. package/dist/layout-state.js +5 -0
  101. package/dist/layout-state.js.map +1 -1
  102. package/dist/measure-blocks/float-group.d.ts +9 -0
  103. package/dist/measure-blocks/float-group.d.ts.map +1 -0
  104. package/dist/measure-blocks/float-group.js +103 -0
  105. package/dist/measure-blocks/float-group.js.map +1 -0
  106. package/dist/measure-blocks/helpers.d.ts +44 -0
  107. package/dist/measure-blocks/helpers.d.ts.map +1 -0
  108. package/dist/measure-blocks/helpers.js +43 -0
  109. package/dist/measure-blocks/helpers.js.map +1 -0
  110. package/dist/measure-blocks/highlight.d.ts +26 -0
  111. package/dist/measure-blocks/highlight.d.ts.map +1 -0
  112. package/dist/measure-blocks/highlight.js +169 -0
  113. package/dist/measure-blocks/highlight.js.map +1 -0
  114. package/dist/measure-blocks/image.d.ts +9 -0
  115. package/dist/measure-blocks/image.d.ts.map +1 -0
  116. package/dist/measure-blocks/image.js +136 -0
  117. package/dist/measure-blocks/image.js.map +1 -0
  118. package/dist/measure-blocks/index.d.ts +24 -0
  119. package/dist/measure-blocks/index.d.ts.map +1 -0
  120. package/dist/measure-blocks/index.js +179 -0
  121. package/dist/measure-blocks/index.js.map +1 -0
  122. package/dist/measure-blocks/list.d.ts +8 -0
  123. package/dist/measure-blocks/list.d.ts.map +1 -0
  124. package/dist/measure-blocks/list.js +108 -0
  125. package/dist/measure-blocks/list.js.map +1 -0
  126. package/dist/measure-blocks/simple-blocks.d.ts +18 -0
  127. package/dist/measure-blocks/simple-blocks.d.ts.map +1 -0
  128. package/dist/measure-blocks/simple-blocks.js +121 -0
  129. package/dist/measure-blocks/simple-blocks.js.map +1 -0
  130. package/dist/measure-blocks/table/columns.d.ts +17 -0
  131. package/dist/measure-blocks/table/columns.d.ts.map +1 -0
  132. package/dist/measure-blocks/table/columns.js +83 -0
  133. package/dist/measure-blocks/table/columns.js.map +1 -0
  134. package/dist/measure-blocks/table/measure.d.ts +8 -0
  135. package/dist/measure-blocks/table/measure.d.ts.map +1 -0
  136. package/dist/measure-blocks/table/measure.js +231 -0
  137. package/dist/measure-blocks/table/measure.js.map +1 -0
  138. package/dist/measure-blocks/table/spans.d.ts +25 -0
  139. package/dist/measure-blocks/table/spans.d.ts.map +1 -0
  140. package/dist/measure-blocks/table/spans.js +55 -0
  141. package/dist/measure-blocks/table/spans.js.map +1 -0
  142. package/dist/measure-blocks/text-blocks.d.ts +17 -0
  143. package/dist/measure-blocks/text-blocks.d.ts.map +1 -0
  144. package/dist/measure-blocks/text-blocks.js +242 -0
  145. package/dist/measure-blocks/text-blocks.js.map +1 -0
  146. package/dist/measure-text.d.ts +21 -3
  147. package/dist/measure-text.d.ts.map +1 -1
  148. package/dist/measure-text.js +87 -36
  149. package/dist/measure-text.js.map +1 -1
  150. package/dist/measure.d.ts +1 -1
  151. package/dist/measure.d.ts.map +1 -1
  152. package/dist/measure.js +8 -6
  153. package/dist/measure.js.map +1 -1
  154. package/dist/node-polyfill.d.ts.map +1 -1
  155. package/dist/node-polyfill.js +9 -0
  156. package/dist/node-polyfill.js.map +1 -1
  157. package/dist/pipeline-footnotes.d.ts +1 -1
  158. package/dist/pipeline-footnotes.d.ts.map +1 -1
  159. package/dist/pipeline-toc.d.ts +1 -1
  160. package/dist/pipeline-toc.d.ts.map +1 -1
  161. package/dist/pipeline.d.ts +3 -3
  162. package/dist/pipeline.d.ts.map +1 -1
  163. package/dist/pipeline.js +4 -5
  164. package/dist/pipeline.js.map +1 -1
  165. package/dist/plugin-types.d.ts +1 -1
  166. package/dist/plugin-types.d.ts.map +1 -1
  167. package/dist/post-process.d.ts +2 -2
  168. package/dist/post-process.d.ts.map +1 -1
  169. package/dist/post-process.js +32 -9
  170. package/dist/post-process.js.map +1 -1
  171. package/dist/render-blocks/blockquote.d.ts +7 -0
  172. package/dist/render-blocks/blockquote.d.ts.map +1 -0
  173. package/dist/render-blocks/blockquote.js +87 -0
  174. package/dist/render-blocks/blockquote.js.map +1 -0
  175. package/dist/render-blocks/callout.d.ts +7 -0
  176. package/dist/render-blocks/callout.d.ts.map +1 -0
  177. package/dist/render-blocks/callout.js +84 -0
  178. package/dist/render-blocks/callout.js.map +1 -0
  179. package/dist/render-blocks/code.d.ts +7 -0
  180. package/dist/render-blocks/code.d.ts.map +1 -0
  181. package/dist/render-blocks/code.js +84 -0
  182. package/dist/render-blocks/code.js.map +1 -0
  183. package/dist/render-blocks/footnote.d.ts +11 -0
  184. package/dist/render-blocks/footnote.d.ts.map +1 -0
  185. package/dist/render-blocks/footnote.js +45 -0
  186. package/dist/render-blocks/footnote.js.map +1 -0
  187. package/dist/render-blocks/header-footer.d.ts +11 -0
  188. package/dist/render-blocks/header-footer.d.ts.map +1 -0
  189. package/dist/render-blocks/header-footer.js +56 -0
  190. package/dist/render-blocks/header-footer.js.map +1 -0
  191. package/dist/render-blocks/hr.d.ts +7 -0
  192. package/dist/render-blocks/hr.d.ts.map +1 -0
  193. package/dist/render-blocks/hr.js +24 -0
  194. package/dist/render-blocks/hr.js.map +1 -0
  195. package/dist/render-blocks/image.d.ts +9 -0
  196. package/dist/render-blocks/image.d.ts.map +1 -0
  197. package/dist/render-blocks/image.js +135 -0
  198. package/dist/render-blocks/image.js.map +1 -0
  199. package/dist/render-blocks/index.d.ts +17 -0
  200. package/dist/render-blocks/index.d.ts.map +1 -0
  201. package/dist/render-blocks/index.js +17 -0
  202. package/dist/render-blocks/index.js.map +1 -0
  203. package/dist/render-blocks/list-item.d.ts +7 -0
  204. package/dist/render-blocks/list-item.d.ts.map +1 -0
  205. package/dist/render-blocks/list-item.js +80 -0
  206. package/dist/render-blocks/list-item.js.map +1 -0
  207. package/dist/render-blocks/rich.d.ts +7 -0
  208. package/dist/render-blocks/rich.d.ts.map +1 -0
  209. package/dist/render-blocks/rich.js +160 -0
  210. package/dist/render-blocks/rich.js.map +1 -0
  211. package/dist/render-blocks/table.d.ts +7 -0
  212. package/dist/render-blocks/table.d.ts.map +1 -0
  213. package/dist/render-blocks/table.js +139 -0
  214. package/dist/render-blocks/table.js.map +1 -0
  215. package/dist/render-blocks/text.d.ts +7 -0
  216. package/dist/render-blocks/text.d.ts.map +1 -0
  217. package/dist/render-blocks/text.js +183 -0
  218. package/dist/render-blocks/text.js.map +1 -0
  219. package/dist/render-blocks/watermark.d.ts +8 -0
  220. package/dist/render-blocks/watermark.d.ts.map +1 -0
  221. package/dist/render-blocks/watermark.js +52 -0
  222. package/dist/render-blocks/watermark.js.map +1 -0
  223. package/dist/render-extras.d.ts.map +1 -1
  224. package/dist/render-extras.js +1 -2
  225. package/dist/render-extras.js.map +1 -1
  226. package/dist/render-utils.d.ts.map +1 -1
  227. package/dist/render-utils.js +10 -6
  228. package/dist/render-utils.js.map +1 -1
  229. package/dist/render.d.ts.map +1 -1
  230. package/dist/render.js +9 -3
  231. package/dist/render.js.map +1 -1
  232. package/dist/rich-text.d.ts +2 -1
  233. package/dist/rich-text.d.ts.map +1 -1
  234. package/dist/rich-text.js +0 -1
  235. package/dist/rich-text.js.map +1 -1
  236. package/dist/types-internal.d.ts +19 -3
  237. package/dist/types-internal.d.ts.map +1 -1
  238. package/dist/types-public/document.d.ts +261 -0
  239. package/dist/types-public/document.d.ts.map +1 -0
  240. package/dist/types-public/document.js +2 -0
  241. package/dist/types-public/document.js.map +1 -0
  242. package/dist/types-public/elements-block.d.ts +246 -0
  243. package/dist/types-public/elements-block.d.ts.map +1 -0
  244. package/dist/types-public/elements-block.js +8 -0
  245. package/dist/types-public/elements-block.js.map +1 -0
  246. package/dist/types-public/elements-media.d.ts +199 -0
  247. package/dist/types-public/elements-media.d.ts.map +1 -0
  248. package/dist/types-public/elements-media.js +2 -0
  249. package/dist/types-public/elements-media.js.map +1 -0
  250. package/dist/types-public/elements-text.d.ts +327 -0
  251. package/dist/types-public/elements-text.d.ts.map +1 -0
  252. package/dist/types-public/elements-text.js +2 -0
  253. package/dist/types-public/elements-text.js.map +1 -0
  254. package/dist/types-public/index.d.ts +14 -0
  255. package/dist/types-public/index.d.ts.map +1 -0
  256. package/dist/types-public/index.js +2 -0
  257. package/dist/types-public/index.js.map +1 -0
  258. package/dist/types-public/render-options.d.ts +38 -0
  259. package/dist/types-public/render-options.d.ts.map +1 -0
  260. package/dist/types-public/render-options.js +2 -0
  261. package/dist/types-public/render-options.js.map +1 -0
  262. package/dist/types-public/union.d.ts +13 -0
  263. package/dist/types-public/union.d.ts.map +1 -0
  264. package/dist/types-public/union.js +2 -0
  265. package/dist/types-public/union.js.map +1 -0
  266. package/dist/types-public/validation.d.ts +64 -0
  267. package/dist/types-public/validation.d.ts.map +1 -0
  268. package/dist/types-public/validation.js +2 -0
  269. package/dist/types-public/validation.js.map +1 -0
  270. package/dist/types-public.d.ts +5 -1081
  271. package/dist/types-public.d.ts.map +1 -1
  272. package/dist/types.d.ts +1 -1
  273. package/dist/types.d.ts.map +1 -1
  274. package/dist/validate/document.d.ts +28 -0
  275. package/dist/validate/document.d.ts.map +1 -0
  276. package/dist/validate/document.js +295 -0
  277. package/dist/validate/document.js.map +1 -0
  278. package/dist/validate/elements/forms-floats.d.ts +19 -0
  279. package/dist/validate/elements/forms-floats.d.ts.map +1 -0
  280. package/dist/validate/elements/forms-floats.js +96 -0
  281. package/dist/validate/elements/forms-floats.js.map +1 -0
  282. package/dist/validate/elements/list.d.ts +10 -0
  283. package/dist/validate/elements/list.d.ts.map +1 -0
  284. package/dist/validate/elements/list.js +66 -0
  285. package/dist/validate/elements/list.js.map +1 -0
  286. package/dist/validate/elements/media.d.ts +23 -0
  287. package/dist/validate/elements/media.d.ts.map +1 -0
  288. package/dist/validate/elements/media.js +179 -0
  289. package/dist/validate/elements/media.js.map +1 -0
  290. package/dist/validate/elements/structural-simple.d.ts +21 -0
  291. package/dist/validate/elements/structural-simple.d.ts.map +1 -0
  292. package/dist/validate/elements/structural-simple.js +63 -0
  293. package/dist/validate/elements/structural-simple.js.map +1 -0
  294. package/dist/validate/elements/structural.d.ts +12 -0
  295. package/dist/validate/elements/structural.d.ts.map +1 -0
  296. package/dist/validate/elements/structural.js +12 -0
  297. package/dist/validate/elements/structural.js.map +1 -0
  298. package/dist/validate/elements/table.d.ts +10 -0
  299. package/dist/validate/elements/table.d.ts.map +1 -0
  300. package/dist/validate/elements/table.js +165 -0
  301. package/dist/validate/elements/table.js.map +1 -0
  302. package/dist/validate/elements/text.d.ts +26 -0
  303. package/dist/validate/elements/text.d.ts.map +1 -0
  304. package/dist/validate/elements/text.js +331 -0
  305. package/dist/validate/elements/text.js.map +1 -0
  306. package/dist/validate/errors.d.ts +9 -0
  307. package/dist/validate/errors.d.ts.map +1 -0
  308. package/dist/validate/errors.js +43 -0
  309. package/dist/validate/errors.js.map +1 -0
  310. package/dist/validate/fonts.d.ts +11 -0
  311. package/dist/validate/fonts.d.ts.map +1 -0
  312. package/dist/validate/fonts.js +118 -0
  313. package/dist/validate/fonts.js.map +1 -0
  314. package/dist/validate/helpers.d.ts +76 -0
  315. package/dist/validate/helpers.d.ts.map +1 -0
  316. package/dist/validate/helpers.js +169 -0
  317. package/dist/validate/helpers.js.map +1 -0
  318. package/dist/validate/index.d.ts +37 -0
  319. package/dist/validate/index.d.ts.map +1 -0
  320. package/dist/validate/index.js +279 -0
  321. package/dist/validate/index.js.map +1 -0
  322. package/dist/validate.d.ts +6 -18
  323. package/dist/validate.d.ts.map +1 -1
  324. package/dist/validate.js +6 -1585
  325. package/dist/validate.js.map +1 -1
  326. package/dist/vendor/pretext/VERSION.d.ts +3 -0
  327. package/dist/vendor/pretext/VERSION.d.ts.map +1 -0
  328. package/dist/vendor/pretext/VERSION.js +12 -0
  329. package/dist/vendor/pretext/VERSION.js.map +1 -0
  330. package/dist/version-check.d.ts +47 -0
  331. package/dist/version-check.d.ts.map +1 -0
  332. package/dist/version-check.js +75 -0
  333. package/dist/version-check.js.map +1 -0
  334. package/package.json +26 -7
  335. package/dist/measure-blocks.d.ts +0 -26
  336. package/dist/measure-blocks.d.ts.map +0 -1
  337. package/dist/measure-blocks.js +0 -1317
  338. package/dist/measure-blocks.js.map +0 -1
  339. package/dist/render-blocks.d.ts +0 -28
  340. package/dist/render-blocks.d.ts.map +0 -1
  341. package/dist/render-blocks.js +0 -1059
  342. package/dist/render-blocks.js.map +0 -1
@@ -1,1059 +0,0 @@
1
- /**
2
- * render-blocks.ts — Element-level rendering functions
3
- * All the specific renderer functions for different content types.
4
- */
5
- import { rgb, degrees } from '@cantoo/pdf-lib';
6
- import { PretextPdfError } from './errors.js';
7
- import { drawJustifiedLine, addLinkAnnotation, addStickyNoteAnnotation, drawTextDecoration, toPdfY, resolveX, resolveTokens, hexToRgb, drawTabularText, LINE_HEIGHT_BODY, LINE_HEIGHT_COMPACT, } from './render-utils.js';
8
- import { buildFontKey } from './measure.js';
9
- // ─── Text block rendering (paragraph + heading) ───────────────────────────────
10
- export function renderTextBlock(pdfPage, pagedBlock, geo, fontMap, pdfDoc) {
11
- const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
12
- const { element } = measuredBlock;
13
- const lines = measuredBlock.lines.slice(startLine, endLine);
14
- if (lines.length === 0)
15
- return;
16
- const pdfFont = fontMap.get(measuredBlock.fontKey);
17
- if (!pdfFont) {
18
- throw new PretextPdfError('FONT_NOT_LOADED', `Font "${measuredBlock.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
19
- }
20
- const colorHex = (element.type === 'paragraph' || element.type === 'heading')
21
- ? (element.color ?? '#000000')
22
- : '#000000';
23
- const [r, g, b] = hexToRgb(colorHex);
24
- const alignRaw = (element.type === 'paragraph' || element.type === 'heading')
25
- ? (element.align ?? (measuredBlock.isRTL ? 'right' : 'left'))
26
- : 'left';
27
- // For resolveX, treat 'justify' as 'left' (justify is handled by drawJustifiedLine)
28
- const align = alignRaw === 'justify' ? 'left' : alignRaw;
29
- const fontHeight = pdfFont.heightAtSize(measuredBlock.fontSize);
30
- // Narrowed reference for paragraph/heading-only fields (smallCaps, tabularNumbers, letterSpacing, annotation)
31
- const textElement = (element.type === 'paragraph' || element.type === 'heading') ? element : null;
32
- // Draw background color for paragraph and heading (if set)
33
- if ((element.type === 'paragraph' || element.type === 'heading') && element.bgColor) {
34
- const columnData = measuredBlock.columnData;
35
- const chunkHeight = columnData
36
- ? columnData.linesPerColumn * measuredBlock.lineHeight
37
- : lines.length * measuredBlock.lineHeight;
38
- const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
39
- const boxPdfY = toPdfY(boxAbsY, chunkHeight, geo.pageHeight);
40
- const [bgR, bgG, bgB] = hexToRgb(element.bgColor);
41
- pdfPage.drawRectangle({
42
- x: geo.margins.left,
43
- y: boxPdfY,
44
- width: geo.contentWidth,
45
- height: chunkHeight,
46
- color: rgb(bgR, bgG, bgB),
47
- borderWidth: 0,
48
- });
49
- }
50
- // Multi-column layout — mirrors single-column features (smallCaps, letterSpacing, justify, decoration)
51
- const columnData = measuredBlock.columnData;
52
- if (columnData) {
53
- const { columnGap, columnWidth, linesPerColumn } = columnData;
54
- const hasSmallCaps = textElement?.smallCaps === true;
55
- const mcFontSize = hasSmallCaps ? measuredBlock.fontSize * 0.8 : measuredBlock.fontSize;
56
- const hasTabular = textElement?.tabularNumbers === true;
57
- const letterSpacing = (textElement?.letterSpacing ?? 0) > 0 ? textElement.letterSpacing : 0;
58
- for (let i = 0; i < lines.length; i++) {
59
- const line = lines[i];
60
- if (line.text === '')
61
- continue;
62
- const colIdx = Math.floor(i / linesPerColumn);
63
- const lineInCol = i % linesPerColumn;
64
- const lineYFromTop = yFromTop + (lineInCol * measuredBlock.lineHeight);
65
- const absoluteYFromTop = lineYFromTop + geo.margins.top + geo.headerHeight;
66
- const pdfY = toPdfY(absoluteYFromTop, fontHeight, geo.pageHeight);
67
- const colX = geo.margins.left + colIdx * (columnWidth + columnGap);
68
- let trimmedText = line.text.trimEnd();
69
- if (hasSmallCaps)
70
- trimmedText = trimmedText.toUpperCase();
71
- // Last line of each column should not be force-justified (left-align instead)
72
- const isLastLineInCol = lineInCol === linesPerColumn - 1 || i === lines.length - 1;
73
- let drawX;
74
- if (alignRaw === 'justify' && letterSpacing === 0 && !hasTabular) {
75
- drawJustifiedLine(pdfPage, trimmedText, isLastLineInCol, colX, pdfY, columnWidth, mcFontSize, pdfFont, rgb(r, g, b));
76
- drawX = colX;
77
- }
78
- else if (letterSpacing > 0) {
79
- const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize) + letterSpacing * (trimmedText.length - 1);
80
- drawX = resolveX(align, colX, columnWidth, alignWidth);
81
- let cx = drawX;
82
- for (const ch of trimmedText) {
83
- pdfPage.drawText(ch, { x: cx, y: pdfY, size: mcFontSize, font: pdfFont, color: rgb(r, g, b) });
84
- cx += pdfFont.widthOfTextAtSize(ch, mcFontSize) + letterSpacing;
85
- }
86
- }
87
- else if (hasTabular) {
88
- const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize);
89
- drawX = resolveX(align, colX, columnWidth, alignWidth);
90
- drawTabularText(pdfPage, trimmedText, drawX, pdfY, mcFontSize, pdfFont, rgb(r, g, b));
91
- }
92
- else {
93
- const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize);
94
- drawX = resolveX(align, colX, columnWidth, alignWidth);
95
- pdfPage.drawText(trimmedText, { x: drawX, y: pdfY, size: mcFontSize, font: pdfFont, color: rgb(r, g, b) });
96
- }
97
- // Text decoration (underline, strikethrough)
98
- if ((element.type === 'paragraph' || element.type === 'heading') && (element.underline || element.strikethrough)) {
99
- const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
100
- drawTextDecoration(pdfPage, drawX, lineWidth, pdfY, mcFontSize, pdfFont, [r, g, b], { underline: element.underline ?? false, strikethrough: element.strikethrough ?? false });
101
- }
102
- // Hyperlink annotation
103
- if ((element.type === 'paragraph' || element.type === 'heading') && element.url) {
104
- const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
105
- addLinkAnnotation(pdfDoc, pdfPage, drawX, pdfY, lineWidth, mcFontSize, element.url);
106
- }
107
- }
108
- // Sticky note annotation (once per block, not per line)
109
- if (textElement?.annotation) {
110
- const ann = textElement.annotation;
111
- const absY = yFromTop + geo.margins.top + geo.headerHeight;
112
- const annotPdfY = geo.pageHeight - absY;
113
- addStickyNoteAnnotation(pdfDoc, pdfPage, geo.margins.left, annotPdfY, ann.contents, ann.author, ann.color, ann.open);
114
- }
115
- return; // skip standard single-column path
116
- }
117
- // Single-column layout (standard path)
118
- for (let i = 0; i < lines.length; i++) {
119
- const line = lines[i];
120
- if (line.text === '')
121
- continue; // empty lines from \n\n — occupy space, draw nothing
122
- const lineYFromTop = yFromTop + (i * measuredBlock.lineHeight);
123
- const absoluteYFromTop = lineYFromTop + geo.margins.top + geo.headerHeight;
124
- const pdfY = toPdfY(absoluteYFromTop, fontHeight, geo.pageHeight);
125
- let trimmedText = line.text.trimEnd();
126
- const isLastLine = i === lines.length - 1;
127
- // smallCaps — uppercase text at 80% font size
128
- const hasSmallCaps = textElement?.smallCaps === true;
129
- const effectiveFontSize = hasSmallCaps ? measuredBlock.fontSize * 0.8 : measuredBlock.fontSize;
130
- if (hasSmallCaps)
131
- trimmedText = trimmedText.toUpperCase();
132
- const hasTabular = textElement?.tabularNumbers === true;
133
- // letterSpacing — draw char by char
134
- const letterSpacing = (textElement?.letterSpacing ?? 0) > 0
135
- ? textElement.letterSpacing
136
- : 0;
137
- let drawX;
138
- if (alignRaw === 'justify' && letterSpacing === 0 && !hasTabular) {
139
- drawJustifiedLine(pdfPage, trimmedText, isLastLine, geo.margins.left, pdfY, geo.contentWidth, effectiveFontSize, pdfFont, rgb(r, g, b));
140
- drawX = geo.margins.left; // used for decoration below
141
- }
142
- else if (letterSpacing > 0) {
143
- const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize) + letterSpacing * (trimmedText.length - 1);
144
- drawX = resolveX(align, geo.margins.left, geo.contentWidth, alignWidth);
145
- let cx = drawX;
146
- for (const ch of trimmedText) {
147
- pdfPage.drawText(ch, { x: cx, y: pdfY, size: effectiveFontSize, font: pdfFont, color: rgb(r, g, b) });
148
- cx += pdfFont.widthOfTextAtSize(ch, effectiveFontSize) + letterSpacing;
149
- }
150
- }
151
- else if (hasTabular) {
152
- const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize);
153
- drawX = resolveX(align, geo.margins.left, geo.contentWidth, alignWidth);
154
- drawTabularText(pdfPage, trimmedText, drawX, pdfY, effectiveFontSize, pdfFont, rgb(r, g, b));
155
- }
156
- else {
157
- const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize);
158
- drawX = resolveX(align, geo.margins.left, geo.contentWidth, alignWidth);
159
- pdfPage.drawText(trimmedText, {
160
- x: drawX,
161
- y: pdfY,
162
- size: effectiveFontSize,
163
- font: pdfFont,
164
- color: rgb(r, g, b),
165
- });
166
- }
167
- if ((element.type === 'paragraph' || element.type === 'heading') && (element.underline || element.strikethrough)) {
168
- const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
169
- drawTextDecoration(pdfPage, drawX, lineWidth, pdfY, effectiveFontSize, pdfFont, [r, g, b], { underline: element.underline ?? false, strikethrough: element.strikethrough ?? false });
170
- }
171
- // Clickable link annotation on paragraph/heading
172
- if ((element.type === 'paragraph' || element.type === 'heading') && element.url) {
173
- const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
174
- addLinkAnnotation(pdfDoc, pdfPage, drawX, pdfY, lineWidth, effectiveFontSize, element.url);
175
- }
176
- }
177
- // Annotation on paragraph/heading — attach sticky note at top of block
178
- if (textElement?.annotation) {
179
- const ann = textElement.annotation;
180
- const absY = yFromTop + geo.margins.top + geo.headerHeight;
181
- const annotPdfY = geo.pageHeight - absY;
182
- addStickyNoteAnnotation(pdfDoc, pdfPage, geo.margins.left, annotPdfY, ann.contents, ann.author, ann.color, ann.open);
183
- }
184
- }
185
- // ─── List item rendering ──────────────────────────────────────────────────────
186
- export function renderListItem(pdfPage, pagedBlock, geo, fontMap) {
187
- const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
188
- const listItemData = measuredBlock.listItemData;
189
- const lines = measuredBlock.lines.slice(startLine, endLine);
190
- if (lines.length === 0)
191
- return;
192
- const pdfFont = fontMap.get(measuredBlock.fontKey);
193
- if (!pdfFont) {
194
- throw new PretextPdfError('FONT_NOT_LOADED', `Font "${measuredBlock.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
195
- }
196
- const fontHeight = pdfFont.heightAtSize(measuredBlock.fontSize);
197
- const [cr, cg, cb] = hexToRgb(listItemData.color);
198
- // RTL support: mirror list layout if detected
199
- const isRTL = measuredBlock.isRTL ?? false;
200
- let textStartX;
201
- let textAreaWidth;
202
- if (isRTL) {
203
- // RTL: marker on the right, text area on the left
204
- textAreaWidth = geo.contentWidth - listItemData.indent - listItemData.markerWidth;
205
- textStartX = geo.margins.left + listItemData.indent;
206
- }
207
- else {
208
- // LTR: marker on the left, text area on the right
209
- textStartX = geo.margins.left + listItemData.indent + listItemData.markerWidth;
210
- textAreaWidth = geo.contentWidth - listItemData.indent - listItemData.markerWidth;
211
- }
212
- // Draw marker on the first line of this item (only if startLine === 0)
213
- // If startLine > 0, the item continued from a previous page — no marker
214
- if (startLine === 0) {
215
- const markerText = listItemData.marker;
216
- const markerMeasuredWidth = pdfFont.widthOfTextAtSize(markerText, measuredBlock.fontSize);
217
- let markerX;
218
- if (isRTL) {
219
- // RTL: marker on the right, right-aligned within marker column
220
- markerX = geo.margins.left + geo.contentWidth - listItemData.indent - markerMeasuredWidth;
221
- }
222
- else {
223
- // LTR: marker on the left, right-aligned within marker column
224
- markerX = geo.margins.left + listItemData.indent + listItemData.markerWidth - markerMeasuredWidth;
225
- }
226
- const firstLineAbsY = yFromTop + geo.margins.top + geo.headerHeight;
227
- const markerPdfY = toPdfY(firstLineAbsY, fontHeight, geo.pageHeight);
228
- pdfPage.drawText(markerText, {
229
- x: markerX,
230
- y: markerPdfY,
231
- size: measuredBlock.fontSize,
232
- font: pdfFont,
233
- color: rgb(cr, cg, cb),
234
- });
235
- }
236
- // Draw all text lines, indented to align with body text column
237
- // RTL lists are right-aligned, LTR lists are left-aligned
238
- const textAlign = isRTL ? 'right' : 'left';
239
- for (let i = 0; i < lines.length; i++) {
240
- const line = lines[i];
241
- if (line.text === '')
242
- continue;
243
- const lineYFromTop = yFromTop + (i * measuredBlock.lineHeight);
244
- const absoluteYFromTop = lineYFromTop + geo.margins.top + geo.headerHeight;
245
- const pdfY = toPdfY(absoluteYFromTop, fontHeight, geo.pageHeight);
246
- const trimmedText = line.text.trimEnd();
247
- const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, measuredBlock.fontSize);
248
- const x = resolveX(textAlign, textStartX, textAreaWidth, lineWidth);
249
- pdfPage.drawText(trimmedText, {
250
- x,
251
- y: pdfY,
252
- size: measuredBlock.fontSize,
253
- font: pdfFont,
254
- color: rgb(cr, cg, cb),
255
- });
256
- }
257
- }
258
- // ─── Table rendering ──────────────────────────────────────────────────────────
259
- export function renderTable(pdfPage, pagedBlock, geo, fontMap) {
260
- const { measuredBlock, yFromTop } = pagedBlock;
261
- const tableData = measuredBlock.tableData;
262
- const startRow = pagedBlock.startRow ?? 0;
263
- const endRow = pagedBlock.endRow ?? tableData.rows.length - tableData.headerRowCount;
264
- const { columnWidths, cellPaddingH, cellPaddingV, borderWidth, borderColor, headerBgColor } = tableData;
265
- // Collect the rows to render for this chunk: headers (always) + body slice
266
- const headerRows = tableData.rows.slice(0, tableData.headerRowCount);
267
- const bodyRows = tableData.rows.slice(tableData.headerRowCount);
268
- const chunkBodyRows = bodyRows.slice(startRow, endRow);
269
- const chunkRows = [...headerRows, ...chunkBodyRows];
270
- const chunkStartAbsY = yFromTop + geo.margins.top + geo.headerHeight;
271
- const totalTableWidth = columnWidths.reduce((s, w) => s + w, 0);
272
- const totalChunkHeight = chunkRows.reduce((s, r) => s + r.height, 0);
273
- // ── Pass 1: Cell backgrounds ──────────────────────────────────────────────
274
- let rowAbsY = chunkStartAbsY;
275
- for (const row of chunkRows) {
276
- let cellX = geo.margins.left;
277
- for (const cell of row.cells) {
278
- if (!cell.isSpanPlaceholder) {
279
- const cellRenderHeight = cell.spanHeight ?? row.height;
280
- const bgColorHex = cell.bgColor ?? (row.isHeader ? headerBgColor : undefined);
281
- if (bgColorHex) {
282
- const [r, g, b] = hexToRgb(bgColorHex);
283
- pdfPage.drawRectangle({ x: cellX, y: toPdfY(rowAbsY, cellRenderHeight, geo.pageHeight), width: cell.mergedWidth, height: cellRenderHeight, color: rgb(r, g, b), borderWidth: 0 });
284
- }
285
- }
286
- cellX += cell.mergedWidth;
287
- }
288
- rowAbsY += row.height;
289
- }
290
- // ── Pass 2: Grid (border-collapse model) ─────────────────────────────────
291
- // Draw outer border + internal lines — single-thickness at every edge.
292
- if (borderWidth > 0) {
293
- const [br, bg, bb] = hexToRgb(borderColor);
294
- const borderRgb = rgb(br, bg, bb);
295
- const tableTopPdfY = toPdfY(chunkStartAbsY, totalChunkHeight, geo.pageHeight);
296
- // Outer border rectangle (no fill)
297
- pdfPage.drawRectangle({
298
- x: geo.margins.left,
299
- y: tableTopPdfY,
300
- width: totalTableWidth,
301
- height: totalChunkHeight,
302
- borderColor: borderRgb,
303
- borderWidth,
304
- });
305
- // Internal horizontal lines (row separators, between rows, not at edges)
306
- // Suppressed after rows that have a spanning cell crossing into the next row
307
- let lineAbsY = chunkStartAbsY;
308
- for (let ri = 0; ri < chunkRows.length - 1; ri++) {
309
- lineAbsY += chunkRows[ri].height;
310
- if (!chunkRows[ri].hasRowspan) {
311
- const linePdfY = geo.pageHeight - lineAbsY;
312
- pdfPage.drawLine({
313
- start: { x: geo.margins.left, y: linePdfY },
314
- end: { x: geo.margins.left + totalTableWidth, y: linePdfY },
315
- thickness: borderWidth,
316
- color: borderRgb,
317
- });
318
- }
319
- }
320
- // Internal vertical lines (column separators, between columns, not at edges)
321
- // Draw per-row segments: a boundary absent from a row's activeBoundaries means a merged cell
322
- // spans it — a full-chunk line would cut through it. Per-row drawing preserves colspan correctness.
323
- // Pre-compute X positions once; convert each row's activeBoundaries to a Set for O(1) lookup.
324
- const colBoundaryXPositions = [];
325
- let bx = geo.margins.left;
326
- for (let ci = 0; ci < columnWidths.length - 1; ci++) {
327
- bx += columnWidths[ci];
328
- colBoundaryXPositions.push(bx);
329
- }
330
- const rowBoundarySets = chunkRows.map(row => new Set(row.activeBoundaries));
331
- let vertRowAbsY = chunkStartAbsY;
332
- for (let ri = 0; ri < chunkRows.length; ri++) {
333
- const row = chunkRows[ri];
334
- const rowBoundarySet = rowBoundarySets[ri];
335
- const rowTopPdfY = geo.pageHeight - vertRowAbsY;
336
- const rowBottomPdfY = geo.pageHeight - (vertRowAbsY + row.height);
337
- for (let ci = 0; ci < colBoundaryXPositions.length; ci++) {
338
- if (rowBoundarySet.has(ci)) {
339
- pdfPage.drawLine({
340
- start: { x: colBoundaryXPositions[ci], y: rowTopPdfY },
341
- end: { x: colBoundaryXPositions[ci], y: rowBottomPdfY },
342
- thickness: borderWidth,
343
- color: borderRgb,
344
- });
345
- }
346
- }
347
- vertRowAbsY += row.height;
348
- }
349
- }
350
- // ── Pass 3: Cell text ─────────────────────────────────────────────────────
351
- rowAbsY = chunkStartAbsY;
352
- for (const row of chunkRows) {
353
- let cellX = geo.margins.left;
354
- for (const cell of row.cells) {
355
- if (!cell.isSpanPlaceholder && cell.lines.length > 0) {
356
- const pdfFont = fontMap.get(cell.fontKey);
357
- if (!pdfFont) {
358
- throw new PretextPdfError('FONT_NOT_LOADED', `Table cell font "${cell.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
359
- }
360
- const fontHeight = pdfFont.heightAtSize(cell.fontSize);
361
- const [r, g, b] = hexToRgb(cell.color);
362
- const textAreaX = cellX + cellPaddingH;
363
- const textAreaWidth = cell.mergedWidth - 2 * cellPaddingH;
364
- // For rowspan cells, vertically center within the full spanHeight
365
- const cellRenderHeight = cell.spanHeight ?? row.height;
366
- const totalTextHeight = cell.lines.length * cell.lineHeight;
367
- const verticalOffset = Math.max(0, (cellRenderHeight - totalTextHeight - 2 * cellPaddingV) / 2);
368
- for (let li = 0; li < cell.lines.length; li++) {
369
- const line = cell.lines[li];
370
- if (line.text === '')
371
- continue;
372
- const lineYFromPageTop = rowAbsY + cellPaddingV + verticalOffset + li * cell.lineHeight;
373
- const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
374
- const trimmedText = line.text.trimEnd();
375
- const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, cell.fontSize);
376
- const x = resolveX(cell.align, textAreaX, textAreaWidth, lineWidth);
377
- if (cell.tabularNumbers) {
378
- drawTabularText(pdfPage, trimmedText, x, pdfY, cell.fontSize, pdfFont, rgb(r, g, b));
379
- }
380
- else {
381
- pdfPage.drawText(trimmedText, { x, y: pdfY, size: cell.fontSize, font: pdfFont, color: rgb(r, g, b) });
382
- }
383
- }
384
- }
385
- cellX += cell.mergedWidth;
386
- }
387
- rowAbsY += row.height;
388
- }
389
- }
390
- // ─── Image rendering ──────────────────────────────────────────────────────────
391
- export function renderImage(pdfPage, pagedBlock, geo, imageMap) {
392
- const { measuredBlock, yFromTop } = pagedBlock;
393
- const imageData = measuredBlock.imageData;
394
- const pdfImage = imageMap.get(imageData.imageKey);
395
- if (!pdfImage) {
396
- throw new PretextPdfError('IMAGE_LOAD_FAILED', `Image "${imageData.imageKey}" not found in imageMap. This is a bug — image loading should have caught this.`);
397
- }
398
- const absoluteYFromTop = yFromTop + geo.margins.top + geo.headerHeight;
399
- // drawImage places the BOTTOM-LEFT corner at (x, y) — use toPdfY with renderHeight
400
- const pdfY = toPdfY(absoluteYFromTop, imageData.renderHeight, geo.pageHeight);
401
- const x = resolveX(imageData.align, geo.margins.left, geo.contentWidth, imageData.renderWidth);
402
- pdfPage.drawImage(pdfImage, {
403
- x,
404
- y: pdfY,
405
- width: imageData.renderWidth,
406
- height: imageData.renderHeight,
407
- });
408
- }
409
- // ─── Float image block rendering ─────────────────────────────────────────────
410
- export function renderFloatBlock(pdfPage, pagedBlock, geo, fontMap, imageMap, pdfDoc) {
411
- const { measuredBlock, yFromTop } = pagedBlock;
412
- const fd = measuredBlock.floatData;
413
- const baseAbsY = yFromTop + geo.margins.top + geo.headerHeight;
414
- // Draw image
415
- const pdfImage = imageMap.get(fd.imageKey);
416
- if (!pdfImage)
417
- throw new PretextPdfError('IMAGE_LOAD_FAILED', `Float image key "${fd.imageKey}" not found in imageMap. This is a bug — image loading should have caught this.`);
418
- const imgX = geo.margins.left + fd.imageColX;
419
- const imgPdfY = toPdfY(baseAbsY, fd.imageRenderHeight, geo.pageHeight);
420
- pdfPage.drawImage(pdfImage, {
421
- x: imgX,
422
- y: imgPdfY,
423
- width: fd.imageRenderWidth,
424
- height: fd.imageRenderHeight,
425
- });
426
- // Draw text lines (rich or plain)
427
- const textBaseX = geo.margins.left + fd.textColX;
428
- if (fd.richFloatLines && fd.richFloatLines.length > 0) {
429
- let cumY = 0;
430
- for (const richLine of fd.richFloatLines) {
431
- const lineAbsY = baseAbsY + cumY;
432
- for (const fragment of richLine.fragments) {
433
- if (!fragment.text || fragment.text.trim() === '')
434
- continue;
435
- const pdfFont = fontMap.get(fragment.fontKey);
436
- if (!pdfFont)
437
- throw new PretextPdfError('FONT_NOT_LOADED', `Float rich text font "${fragment.fontKey}" not found in fontMap.`);
438
- const fontHeight = pdfFont.heightAtSize(fragment.fontSize);
439
- const [r, g, b] = hexToRgb(fragment.color);
440
- const drawX = textBaseX + fragment.x;
441
- const pdfY = toPdfY(lineAbsY, fontHeight, geo.pageHeight) + (fragment.yOffset ?? 0);
442
- const drawText = fragment.text.trimEnd();
443
- if (fragment.letterSpacing && fragment.letterSpacing > 0) {
444
- let cx = drawX;
445
- for (const ch of drawText) {
446
- pdfPage.drawText(ch, { x: cx, y: pdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
447
- cx += pdfFont.widthOfTextAtSize(ch, fragment.fontSize) + fragment.letterSpacing;
448
- }
449
- }
450
- else {
451
- pdfPage.drawText(drawText, { x: drawX, y: pdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
452
- }
453
- const fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
454
- drawTextDecoration(pdfPage, drawX, fragWidth, pdfY, fragment.fontSize, pdfFont, [r, g, b], { underline: fragment.underline ?? false, strikethrough: fragment.strikethrough ?? false });
455
- if (fragment.url)
456
- addLinkAnnotation(pdfDoc, pdfPage, drawX, pdfY, fragWidth, fragment.fontSize, fragment.url);
457
- }
458
- cumY += richLine.lineHeight;
459
- }
460
- }
461
- else {
462
- const pdfFont = fontMap.get(fd.textFontKey);
463
- if (!pdfFont)
464
- throw new PretextPdfError('FONT_NOT_LOADED', `Float text font key "${fd.textFontKey}" not found in fontMap. This is a bug — font loading should have caught this.`);
465
- const fontHeight = pdfFont.heightAtSize(fd.textFontSize);
466
- const [r, g, b] = hexToRgb(fd.textColor);
467
- for (let i = 0; i < fd.textLines.length; i++) {
468
- const line = fd.textLines[i];
469
- if (line.text === '')
470
- continue;
471
- const lineAbsY = baseAbsY + (i * fd.textLineHeight);
472
- const pdfY = toPdfY(lineAbsY, fontHeight, geo.pageHeight);
473
- pdfPage.drawText(line.text.trimEnd(), { x: textBaseX, y: pdfY, size: fd.textFontSize, font: pdfFont, color: rgb(r, g, b) });
474
- }
475
- }
476
- }
477
- // ─── Float group block rendering ──────────────────────────────────────────────
478
- export function renderFloatGroup(pdfPage, pagedBlock, geo, fontMap, imageMap, pdfDoc) {
479
- const { measuredBlock, yFromTop } = pagedBlock;
480
- const fd = measuredBlock.floatGroupData;
481
- const baseAbsY = yFromTop + geo.margins.top + geo.headerHeight;
482
- // Draw image
483
- const pdfImage = imageMap.get(fd.imageKey);
484
- if (!pdfImage)
485
- throw new PretextPdfError('IMAGE_LOAD_FAILED', `Float group image key "${fd.imageKey}" not found in imageMap. This is a bug — image loading should have caught this.`);
486
- const imgX = geo.margins.left + fd.imageColX;
487
- const imgPdfY = toPdfY(baseAbsY, fd.imageRenderHeight, geo.pageHeight);
488
- pdfPage.drawImage(pdfImage, {
489
- x: imgX,
490
- y: imgPdfY,
491
- width: fd.imageRenderWidth,
492
- height: fd.imageRenderHeight,
493
- });
494
- // Draw text items
495
- const textBaseX = geo.margins.left + fd.textColX;
496
- for (const textItem of fd.textItems) {
497
- const pdfFont = fontMap.get(textItem.fontKey);
498
- if (!pdfFont)
499
- throw new PretextPdfError('FONT_NOT_LOADED', `Float group font key "${textItem.fontKey}" not found in fontMap. This is a bug — font loading should have caught this.`);
500
- const fontHeight = pdfFont.heightAtSize(textItem.fontSize);
501
- // Draw plain lines (plain-text fallback for rich-paragraphs)
502
- for (let i = 0; i < textItem.lines.length; i++) {
503
- const line = textItem.lines[i];
504
- if (line.text === '')
505
- continue;
506
- const lineAbsY = baseAbsY + textItem.yOffsetFromTop + (i * textItem.lineHeight);
507
- const pdfY = toPdfY(lineAbsY, fontHeight, geo.pageHeight);
508
- pdfPage.drawText(line.text.trimEnd(), {
509
- x: textBaseX,
510
- y: pdfY,
511
- size: textItem.fontSize,
512
- font: pdfFont,
513
- color: rgb(0, 0, 0),
514
- });
515
- }
516
- }
517
- }
518
- // ─── Horizontal rule rendering ────────────────────────────────────────────────
519
- export function renderHR(pdfPage, pagedBlock, geo) {
520
- const { measuredBlock, yFromTop } = pagedBlock;
521
- const element = measuredBlock.element;
522
- const spaceAbove = element.spaceAbove ?? 12;
523
- const thickness = element.thickness ?? 0.5;
524
- const colorHex = element.color ?? '#cccccc';
525
- // Line sits at the middle of the HR element (after spaceAbove, before spaceBelow)
526
- const lineYFromTop = yFromTop + spaceAbove + geo.margins.top + geo.headerHeight;
527
- const pdfY = toPdfY(lineYFromTop, thickness / 2, geo.pageHeight);
528
- const [r, g, b] = hexToRgb(colorHex);
529
- pdfPage.drawLine({
530
- start: { x: geo.margins.left, y: pdfY },
531
- end: { x: geo.margins.left + geo.contentWidth, y: pdfY },
532
- thickness,
533
- color: rgb(r, g, b),
534
- });
535
- }
536
- // ─── Code block rendering ─────────────────────────────────────────────────────
537
- export function renderCodeBlock(pdfPage, pagedBlock, geo, fontMap) {
538
- const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
539
- const element = measuredBlock.element;
540
- const padding = measuredBlock.codePadding ?? 8;
541
- const bgColorHex = element.bgColor ?? '#f6f8fa';
542
- const textColorHex = element.color ?? '#24292f';
543
- // Slice the lines being rendered on this page chunk
544
- const lines = measuredBlock.lines.slice(startLine, endLine);
545
- const lineHeight = measuredBlock.lineHeight;
546
- const fontSize = measuredBlock.fontSize;
547
- // Compute per-chunk padding (only apply padding at the edge of the code block)
548
- const isFirstChunk = startLine === 0;
549
- const isLastChunk = endLine === measuredBlock.lines.length;
550
- const paddingTop = isFirstChunk ? padding : 0;
551
- const paddingBottom = isLastChunk ? padding : 0;
552
- const visibleHeight = lines.length * lineHeight + paddingTop + paddingBottom;
553
- // ── Background box ──────────────────────────────────────────────────────────
554
- const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
555
- const boxPdfY = toPdfY(boxAbsY, visibleHeight, geo.pageHeight);
556
- const [bgR, bgG, bgB] = hexToRgb(bgColorHex);
557
- pdfPage.drawRectangle({
558
- x: geo.margins.left,
559
- y: boxPdfY,
560
- width: geo.contentWidth,
561
- height: visibleHeight,
562
- color: rgb(bgR, bgG, bgB),
563
- borderWidth: 0,
564
- });
565
- // ── Text lines ──────────────────────────────────────────────────────────────
566
- const pdfFont = fontMap.get(measuredBlock.fontKey);
567
- if (!pdfFont || lines.length === 0)
568
- return;
569
- const fontHeight = pdfFont.heightAtSize(fontSize);
570
- const [r, g, b] = hexToRgb(textColorHex);
571
- const textX = geo.margins.left + padding;
572
- // Syntax highlighting: tokenize if language is set and highlight.js is available
573
- const highlightTokens = measuredBlock.codeHighlightTokens;
574
- if (highlightTokens && highlightTokens.length > 0) {
575
- // Render with per-token colors
576
- for (let i = 0; i < lines.length; i++) {
577
- const lineYFromPageTop = boxAbsY + paddingTop + i * lineHeight;
578
- const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
579
- const lineTokens = highlightTokens[startLine + i];
580
- if (!lineTokens)
581
- continue;
582
- let curX = textX;
583
- for (const token of lineTokens) {
584
- if (!token.text)
585
- continue;
586
- const [tr, tg, tb] = hexToRgb(token.color);
587
- pdfPage.drawText(token.text, {
588
- x: curX,
589
- y: pdfY,
590
- size: fontSize,
591
- font: pdfFont,
592
- color: rgb(tr, tg, tb),
593
- });
594
- curX += pdfFont.widthOfTextAtSize(token.text, fontSize);
595
- }
596
- }
597
- }
598
- else {
599
- // Plain text rendering (no highlighting)
600
- for (let i = 0; i < lines.length; i++) {
601
- const line = lines[i];
602
- const lineYFromPageTop = boxAbsY + paddingTop + i * lineHeight;
603
- const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
604
- pdfPage.drawText(line.text.trimEnd(), {
605
- x: textX,
606
- y: pdfY,
607
- size: fontSize,
608
- font: pdfFont,
609
- color: rgb(r, g, b),
610
- });
611
- }
612
- }
613
- }
614
- // ─── Blockquote rendering ─────────────────────────────────────────────────────
615
- export function renderBlockquote(pdfPage, pagedBlock, geo, fontMap) {
616
- const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
617
- const element = measuredBlock.element;
618
- const paddingV = measuredBlock.blockquotePaddingV ?? 10;
619
- const paddingH = measuredBlock.blockquotePaddingH ?? 16;
620
- const borderWidth = measuredBlock.blockquoteBorderWidth ?? 3;
621
- const bgColorHex = element.bgColor ?? '#f8f9fa';
622
- const borderColorHex = element.borderColor ?? '#0070f3';
623
- const textColorHex = element.color ?? '#333333';
624
- const alignRaw = element.align ?? (measuredBlock.isRTL ? 'right' : 'left');
625
- const align = alignRaw === 'justify' ? 'left' : alignRaw;
626
- const lines = measuredBlock.lines.slice(startLine, endLine);
627
- const lineHeight = measuredBlock.lineHeight;
628
- const fontSize = measuredBlock.fontSize;
629
- // Compute per-chunk padding (only at the edge of the block, like code)
630
- const isFirstChunk = startLine === 0;
631
- const isLastChunk = endLine === measuredBlock.lines.length;
632
- const paddingTop = isFirstChunk ? paddingV : 0;
633
- const paddingBottom = isLastChunk ? paddingV : 0;
634
- const visibleHeight = lines.length * lineHeight + paddingTop + paddingBottom;
635
- const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
636
- const boxPdfY = toPdfY(boxAbsY, visibleHeight, geo.pageHeight);
637
- // ── Background box ──────────────────────────────────────────────────────────
638
- const [bgR, bgG, bgB] = hexToRgb(bgColorHex);
639
- pdfPage.drawRectangle({
640
- x: geo.margins.left,
641
- y: boxPdfY,
642
- width: geo.contentWidth,
643
- height: visibleHeight,
644
- color: rgb(bgR, bgG, bgB),
645
- borderWidth: 0,
646
- });
647
- // ── Left border stripe ──────────────────────────────────────────────────────
648
- const [bdR, bdG, bdB] = hexToRgb(borderColorHex);
649
- pdfPage.drawRectangle({
650
- x: geo.margins.left,
651
- y: boxPdfY,
652
- width: borderWidth,
653
- height: visibleHeight,
654
- color: rgb(bdR, bdG, bdB),
655
- borderWidth: 0,
656
- });
657
- // ── Text lines ──────────────────────────────────────────────────────────────
658
- const pdfFont = fontMap.get(measuredBlock.fontKey);
659
- if (!pdfFont || lines.length === 0)
660
- return;
661
- const fontHeight = pdfFont.heightAtSize(fontSize);
662
- const [r, g, b] = hexToRgb(textColorHex);
663
- const textStartX = geo.margins.left + borderWidth + paddingH;
664
- const textAreaWidth = geo.contentWidth - borderWidth - 2 * paddingH;
665
- for (let i = 0; i < lines.length; i++) {
666
- const line = lines[i];
667
- if (line.text === '')
668
- continue;
669
- const lineYFromPageTop = boxAbsY + paddingTop + i * lineHeight;
670
- const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
671
- const trimmedText = line.text.trimEnd();
672
- const isLastLine = i === lines.length - 1;
673
- let drawX;
674
- if (alignRaw === 'justify') {
675
- drawJustifiedLine(pdfPage, trimmedText, isLastLine, textStartX, pdfY, textAreaWidth, fontSize, pdfFont, rgb(r, g, b));
676
- drawX = textStartX;
677
- }
678
- else {
679
- const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, fontSize);
680
- drawX = resolveX(align, textStartX, textAreaWidth, lineWidth);
681
- pdfPage.drawText(trimmedText, {
682
- x: drawX,
683
- y: pdfY,
684
- size: fontSize,
685
- font: pdfFont,
686
- color: rgb(r, g, b),
687
- });
688
- }
689
- if (element.underline || element.strikethrough) {
690
- const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, fontSize);
691
- drawTextDecoration(pdfPage, drawX, lineWidth, pdfY, fontSize, pdfFont, [r, g, b], { underline: element.underline ?? false, strikethrough: element.strikethrough ?? false });
692
- }
693
- }
694
- }
695
- // ─── Callout rendering ────────────────────────────────────────────
696
- export function renderCallout(pdfPage, pagedBlock, geo, fontMap) {
697
- const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
698
- const el = measuredBlock.element;
699
- const cd = measuredBlock.calloutData;
700
- if (!cd)
701
- return;
702
- const { paddingH, paddingV, borderColor, backgroundColor, titleColor, color, titleText } = cd;
703
- const isFirstChunk = startLine === 0;
704
- const isLastChunk = endLine === measuredBlock.lines.length;
705
- const lines = measuredBlock.lines.slice(startLine, endLine);
706
- const fs = measuredBlock.fontSize;
707
- const lh = measuredBlock.lineHeight;
708
- const font = fontMap.get(measuredBlock.fontKey) ?? [...fontMap.values()][0];
709
- if (!font)
710
- return;
711
- const titleH = isFirstChunk && titleText ? cd.titleHeight : 0;
712
- const topPad = isFirstChunk ? paddingV : 0;
713
- const bottomPad = isLastChunk ? paddingV : 0;
714
- const chunkHeight = topPad + titleH + lines.length * lh + bottomPad;
715
- const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
716
- const boxPdfY = toPdfY(boxAbsY, chunkHeight, geo.pageHeight);
717
- // Background
718
- const [bgR, bgG, bgB] = hexToRgb(backgroundColor);
719
- pdfPage.drawRectangle({
720
- x: geo.margins.left,
721
- y: boxPdfY,
722
- width: geo.contentWidth,
723
- height: chunkHeight,
724
- color: rgb(bgR, bgG, bgB),
725
- borderWidth: 0,
726
- });
727
- // Left border stripe (3pt wide)
728
- const [bdR, bdG, bdB] = hexToRgb(borderColor);
729
- pdfPage.drawRectangle({
730
- x: geo.margins.left,
731
- y: boxPdfY,
732
- width: 3,
733
- height: chunkHeight,
734
- color: rgb(bdR, bdG, bdB),
735
- borderWidth: 0,
736
- });
737
- const fontHeight = font.heightAtSize(fs);
738
- let currentAbsY = boxAbsY + topPad;
739
- // Draw title if first chunk
740
- if (isFirstChunk && titleText) {
741
- // Try to get bold font variant by modifying the fontKey
742
- const boldFontKey = measuredBlock.fontKey.replace(/-400-/, '-700-');
743
- const titleFont = fontMap.get(boldFontKey) ?? font;
744
- const [tR, tG, tB] = hexToRgb(titleColor);
745
- const titlePdfY = toPdfY(currentAbsY + (fs * LINE_HEIGHT_COMPACT - fs) / 2, fontHeight, geo.pageHeight);
746
- pdfPage.drawText(titleText, {
747
- x: geo.margins.left + paddingH,
748
- y: titlePdfY,
749
- size: fs,
750
- font: titleFont,
751
- color: rgb(tR, tG, tB),
752
- });
753
- currentAbsY += titleH;
754
- }
755
- // Draw content lines
756
- const [tR, tG, tB] = hexToRgb(color);
757
- for (let i = 0; i < lines.length; i++) {
758
- const line = lines[i];
759
- if (line.text === '') {
760
- currentAbsY += lh;
761
- continue;
762
- }
763
- const linePdfY = toPdfY(currentAbsY + (lh - fs) / 2, fontHeight, geo.pageHeight);
764
- pdfPage.drawText(line.text.trimEnd(), {
765
- x: geo.margins.left + paddingH,
766
- y: linePdfY,
767
- size: fs,
768
- font,
769
- color: rgb(tR, tG, tB),
770
- });
771
- currentAbsY += lh;
772
- }
773
- }
774
- // ─── Rich paragraph rendering ─────────────────────────────────────────────────
775
- export function renderRichParagraph(pdfPage, pagedBlock, geo, fontMap, pdfDoc, footnoteNumbering) {
776
- const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
777
- const { element, richLines, lineHeight, fontSize } = measuredBlock;
778
- const tabularNumbers = element.type === 'rich-paragraph' && element.tabularNumbers === true;
779
- if (!richLines || richLines.length === 0)
780
- return;
781
- // Only render the lines on this page chunk
782
- const visibleLines = richLines.slice(startLine, endLine);
783
- // Draw background color if set
784
- const columnData = measuredBlock.columnData;
785
- if (element.type === 'rich-paragraph' && element.bgColor) {
786
- // Use sum of per-line heights (may vary with per-span fontSize)
787
- const chunkHeight = columnData
788
- ? visibleLines.slice(0, columnData.linesPerColumn).reduce((sum, rl) => sum + rl.lineHeight, 0)
789
- : visibleLines.reduce((sum, rl) => sum + rl.lineHeight, 0);
790
- const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
791
- const boxPdfY = toPdfY(boxAbsY, chunkHeight, geo.pageHeight);
792
- const [bgR, bgG, bgB] = hexToRgb(element.bgColor);
793
- pdfPage.drawRectangle({
794
- x: geo.margins.left,
795
- y: boxPdfY,
796
- width: geo.contentWidth,
797
- height: chunkHeight,
798
- color: rgb(bgR, bgG, bgB),
799
- borderWidth: 0,
800
- });
801
- }
802
- // Multi-column layout
803
- if (columnData) {
804
- const { columnCount, columnGap, columnWidth, linesPerColumn } = columnData;
805
- // Track cumulative Y per column (per-line heights may vary)
806
- const colCumY = new Array(columnCount).fill(0);
807
- for (let i = 0; i < visibleLines.length; i++) {
808
- const richLine = visibleLines[i];
809
- const colIdx = Math.floor(i / linesPerColumn);
810
- const colOffsetX = colIdx * (columnWidth + columnGap);
811
- const lineYFromTop = yFromTop + colCumY[colIdx] + geo.margins.top + geo.headerHeight;
812
- for (const fragment of richLine.fragments) {
813
- if (!fragment.text || fragment.text.trim() === '')
814
- continue;
815
- const pdfFont = fontMap.get(fragment.fontKey);
816
- if (!pdfFont) {
817
- throw new PretextPdfError('FONT_NOT_LOADED', `Rich text fragment font "${fragment.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
818
- }
819
- const fontHeight = pdfFont.heightAtSize(fragment.fontSize);
820
- const basePdfY = toPdfY(lineYFromTop, fontHeight, geo.pageHeight);
821
- const [r, g, b] = hexToRgb(fragment.color);
822
- const drawX = geo.margins.left + colOffsetX + fragment.x;
823
- // Footnote ref spans render as superscript number, replacing the original text
824
- if (fragment.footnoteRef) {
825
- const num = footnoteNumbering?.get(fragment.footnoteRef) ?? '?';
826
- const superText = String(num);
827
- const superSize = fragment.fontSize * 0.65;
828
- const superYOffset = fragment.fontSize * 0.4;
829
- const superPdfY = basePdfY + superYOffset;
830
- pdfPage.drawText(superText, {
831
- x: drawX,
832
- y: superPdfY,
833
- size: superSize,
834
- font: pdfFont,
835
- color: rgb(r, g, b),
836
- });
837
- continue;
838
- }
839
- const fragmentPdfY = basePdfY + (fragment.yOffset ?? 0);
840
- const drawText = fragment.text.trimEnd();
841
- let fragWidth;
842
- if (fragment.letterSpacing && fragment.letterSpacing > 0) {
843
- let cx = drawX;
844
- for (const ch of drawText) {
845
- pdfPage.drawText(ch, { x: cx, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
846
- cx += pdfFont.widthOfTextAtSize(ch, fragment.fontSize) + fragment.letterSpacing;
847
- }
848
- fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize) + fragment.letterSpacing * (drawText.length - 1);
849
- }
850
- else if (tabularNumbers) {
851
- drawTabularText(pdfPage, drawText, drawX, fragmentPdfY, fragment.fontSize, pdfFont, rgb(r, g, b));
852
- fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
853
- }
854
- else {
855
- pdfPage.drawText(drawText, { x: drawX, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
856
- fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
857
- }
858
- drawTextDecoration(pdfPage, drawX, fragWidth, fragmentPdfY, fragment.fontSize, pdfFont, [r, g, b], { underline: fragment.underline ?? false, strikethrough: fragment.strikethrough ?? false });
859
- if (fragment.url) {
860
- addLinkAnnotation(pdfDoc, pdfPage, drawX, fragmentPdfY, fragWidth, fragment.fontSize, fragment.url);
861
- }
862
- }
863
- colCumY[colIdx] += richLine.lineHeight;
864
- }
865
- return; // skip standard single-column path
866
- }
867
- // Single-column layout (standard path)
868
- // Track cumulative Y (per-line heights may vary due to per-span fontSize)
869
- let cumY = 0;
870
- for (let i = 0; i < visibleLines.length; i++) {
871
- const richLine = visibleLines[i];
872
- const lineYFromTop = yFromTop + cumY + geo.margins.top + geo.headerHeight;
873
- for (const fragment of richLine.fragments) {
874
- if (!fragment.text || fragment.text.trim() === '')
875
- continue;
876
- const pdfFont = fontMap.get(fragment.fontKey);
877
- if (!pdfFont) {
878
- throw new PretextPdfError('FONT_NOT_LOADED', `Rich text fragment font "${fragment.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
879
- }
880
- const fontHeight = pdfFont.heightAtSize(fragment.fontSize);
881
- const basePdfY = toPdfY(lineYFromTop, fontHeight, geo.pageHeight);
882
- const [r, g, b] = hexToRgb(fragment.color);
883
- const drawX = geo.margins.left + fragment.x;
884
- // Footnote ref spans render as superscript number, replacing the original text
885
- if (fragment.footnoteRef) {
886
- const num = footnoteNumbering?.get(fragment.footnoteRef) ?? '?';
887
- const superText = String(num);
888
- const superSize = fragment.fontSize * 0.65;
889
- const superYOffset = fragment.fontSize * 0.4;
890
- const superPdfY = basePdfY + superYOffset;
891
- pdfPage.drawText(superText, {
892
- x: drawX,
893
- y: superPdfY,
894
- size: superSize,
895
- font: pdfFont,
896
- color: rgb(r, g, b),
897
- });
898
- continue;
899
- }
900
- const fragmentPdfY = basePdfY + (fragment.yOffset ?? 0);
901
- const drawText = fragment.text.trimEnd();
902
- let fragWidth;
903
- if (fragment.letterSpacing && fragment.letterSpacing > 0) {
904
- let cx = drawX;
905
- for (const ch of drawText) {
906
- pdfPage.drawText(ch, { x: cx, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
907
- cx += pdfFont.widthOfTextAtSize(ch, fragment.fontSize) + fragment.letterSpacing;
908
- }
909
- fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize) + fragment.letterSpacing * (drawText.length - 1);
910
- }
911
- else if (tabularNumbers) {
912
- drawTabularText(pdfPage, drawText, drawX, fragmentPdfY, fragment.fontSize, pdfFont, rgb(r, g, b));
913
- fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
914
- }
915
- else {
916
- pdfPage.drawText(drawText, { x: drawX, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
917
- fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
918
- }
919
- drawTextDecoration(pdfPage, drawX, fragWidth, fragmentPdfY, fragment.fontSize, pdfFont, [r, g, b], { underline: fragment.underline ?? false, strikethrough: fragment.strikethrough ?? false });
920
- if (fragment.url) {
921
- addLinkAnnotation(pdfDoc, pdfPage, drawX, fragmentPdfY, fragWidth, fragment.fontSize, fragment.url);
922
- }
923
- }
924
- cumY += richLine.lineHeight;
925
- }
926
- }
927
- // ─── Footnote zone rendering ──────────────────────────────────────────────────
928
- export function renderFootnoteZone(pdfPage, footnoteItems, zoneHeight, fontMap, doc, geo) {
929
- const { pageHeight, margins, footerHeight, contentWidth } = geo;
930
- const SEPARATOR_PADDING = 6; // pt above and below the separator line
931
- // Zone top in PDF coords (Y=0 at bottom of page)
932
- const zoneTopPdfY = margins.bottom + footerHeight + zoneHeight;
933
- const separatorY = zoneTopPdfY - SEPARATOR_PADDING;
934
- // Draw separator line: 1/3 content width, max 120pt
935
- const lineLength = Math.min(contentWidth * 0.33, 120);
936
- pdfPage.drawLine({
937
- start: { x: margins.left, y: separatorY },
938
- end: { x: margins.left + lineLength, y: separatorY },
939
- thickness: 0.5,
940
- color: rgb(0.5, 0.5, 0.5),
941
- });
942
- const defaultFontSize = doc.defaultFontSize ?? 12;
943
- let currentPdfY = separatorY - SEPARATOR_PADDING;
944
- for (const { def, number } of footnoteItems) {
945
- const fontSize = def.fontSize ?? Math.max(8, defaultFontSize - 2);
946
- const lineHeight = fontSize * LINE_HEIGHT_BODY;
947
- const fontFamily = def.fontFamily ?? doc.defaultFont ?? 'Inter';
948
- const fontKey = buildFontKey(fontFamily, 400, 'normal');
949
- const pdfFont = fontMap.get(fontKey);
950
- if (!pdfFont)
951
- continue;
952
- currentPdfY -= lineHeight;
953
- const prefix = `${number}. `;
954
- const fullText = prefix + def.text;
955
- pdfPage.drawText(fullText, {
956
- x: margins.left,
957
- y: currentPdfY,
958
- size: fontSize,
959
- font: pdfFont,
960
- color: rgb(0.2, 0.2, 0.2),
961
- });
962
- currentPdfY -= (def.spaceAfter ?? 4);
963
- }
964
- }
965
- // ─── Header / Footer rendering ────────────────────────────────────────────────
966
- export function renderHeaderFooter(pdfPage, spec, pageNumber, totalPages, geo, fontMap, position, extra) {
967
- const text = resolveTokens(spec.text, pageNumber, totalPages, extra);
968
- const fontSize = spec.fontSize ?? 10;
969
- const align = spec.align ?? 'center';
970
- const fontKey = `${spec.fontFamily ?? 'Inter'}-${spec.fontWeight ?? 400}-normal`;
971
- const pdfFont = fontMap.get(fontKey);
972
- if (!pdfFont) {
973
- throw new PretextPdfError('FONT_NOT_LOADED', `${position} font "${fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
974
- }
975
- const fontHeight = pdfFont.heightAtSize(fontSize);
976
- let yFromTop;
977
- if (position === 'header') {
978
- yFromTop = (geo.margins.top - fontHeight) / 2;
979
- }
980
- else {
981
- yFromTop = geo.pageHeight - geo.margins.bottom + (geo.margins.bottom - fontHeight) / 2;
982
- }
983
- const pdfY = toPdfY(yFromTop, fontHeight, geo.pageHeight);
984
- const textWidth = pdfFont.widthOfTextAtSize(text, fontSize);
985
- const x = resolveX(align, geo.margins.left, geo.contentWidth, textWidth);
986
- const [textR, textG, textB] = hexToRgb(spec.color ?? '#666666');
987
- pdfPage.drawText(text, {
988
- x,
989
- y: pdfY,
990
- size: fontSize,
991
- font: pdfFont,
992
- color: rgb(textR, textG, textB),
993
- });
994
- // Separator line
995
- if (position === 'header') {
996
- const lineY = toPdfY(geo.margins.top - 4, 1, geo.pageHeight);
997
- pdfPage.drawLine({
998
- start: { x: geo.margins.left, y: lineY },
999
- end: { x: geo.margins.left + geo.contentWidth, y: lineY },
1000
- thickness: 0.5,
1001
- color: rgb(0.8, 0.8, 0.8),
1002
- });
1003
- }
1004
- else {
1005
- const lineY = toPdfY(geo.pageHeight - geo.margins.bottom + 4, 1, geo.pageHeight);
1006
- pdfPage.drawLine({
1007
- start: { x: geo.margins.left, y: lineY },
1008
- end: { x: geo.margins.left + geo.contentWidth, y: lineY },
1009
- thickness: 0.5,
1010
- color: rgb(0.8, 0.8, 0.8),
1011
- });
1012
- }
1013
- }
1014
- // ─── Watermark rendering ──────────────────────────────────────────────────
1015
- export function renderWatermark(pdfPage, doc, fontMap, imageMap, geo) {
1016
- const wm = doc.watermark;
1017
- if (!wm)
1018
- return;
1019
- const opacity = wm.opacity ?? 0.3;
1020
- const rotation = wm.rotation ?? -45;
1021
- const { pageWidth, pageHeight } = geo;
1022
- if (wm.text) {
1023
- const fontKey = `${wm.fontFamily ?? doc.defaultFont ?? 'Inter'}-${wm.fontWeight ?? 400}-normal`;
1024
- const pdfFont = fontMap.get(fontKey);
1025
- if (!pdfFont) {
1026
- throw new PretextPdfError('FONT_NOT_LOADED', `Watermark font "${fontKey}" not found in fontMap. This is a bug.`);
1027
- }
1028
- // Auto-compute font size to span ~60% of page diagonal
1029
- const fontSize = wm.fontSize ?? (() => {
1030
- const diagonal = Math.sqrt(pageWidth ** 2 + pageHeight ** 2);
1031
- const widthAt100 = pdfFont.widthOfTextAtSize(wm.text, 100);
1032
- return Math.min(120, (diagonal * 0.6 / widthAt100) * 100);
1033
- })();
1034
- const [r, g, b] = hexToRgb(wm.color ?? '#CCCCCC');
1035
- pdfPage.drawText(wm.text, {
1036
- x: pageWidth / 2,
1037
- y: pageHeight / 2,
1038
- size: fontSize,
1039
- font: pdfFont,
1040
- color: rgb(r, g, b),
1041
- rotate: degrees(rotation),
1042
- opacity,
1043
- });
1044
- }
1045
- if (wm.image) {
1046
- const pdfImage = imageMap.get('watermark');
1047
- if (!pdfImage)
1048
- return;
1049
- const margin = 40;
1050
- pdfPage.drawImage(pdfImage, {
1051
- x: margin,
1052
- y: margin,
1053
- width: pageWidth - margin * 2,
1054
- height: pageHeight - margin * 2,
1055
- opacity,
1056
- });
1057
- }
1058
- }
1059
- //# sourceMappingURL=render-blocks.js.map