@awacloud/pdf 0.0.0-stage → 1.0.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 (345) hide show
  1. package/CHANGELOG.md +609 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +77 -0
  4. package/README.md +363 -2
  5. package/dist/build/index.js +21 -0
  6. package/dist/build/pdf-full-rw.js +10972 -0
  7. package/dist/build/pdf-full-rw.meta.json +105 -0
  8. package/dist/build/pdf-full-rw.min.js +53 -0
  9. package/dist/build/pdf-full.js +6078 -0
  10. package/dist/build/pdf-full.meta.json +90 -0
  11. package/dist/build/pdf-full.min.js +32 -0
  12. package/dist/build/pdf-large-rw.js +10367 -0
  13. package/dist/build/pdf-large-rw.meta.json +99 -0
  14. package/dist/build/pdf-large-rw.min.js +53 -0
  15. package/dist/build/pdf-large.js +5473 -0
  16. package/dist/build/pdf-large.meta.json +84 -0
  17. package/dist/build/pdf-large.min.js +32 -0
  18. package/dist/build/pdf-legacy-rw.js +12402 -0
  19. package/dist/build/pdf-legacy-rw.meta.json +110 -0
  20. package/dist/build/pdf-legacy-rw.min.js +53 -0
  21. package/dist/build/pdf-legacy.js +7508 -0
  22. package/dist/build/pdf-legacy.meta.json +95 -0
  23. package/dist/build/pdf-legacy.min.js +32 -0
  24. package/dist/build/pdf-rw.js +7578 -0
  25. package/dist/build/pdf-rw.meta.json +77 -0
  26. package/dist/build/pdf-rw.min.js +53 -0
  27. package/dist/build/pdf.js +2684 -0
  28. package/dist/build/pdf.meta.json +62 -0
  29. package/dist/build/pdf.min.js +32 -0
  30. package/dist/standalone/pdf-full-rw.js +16798 -0
  31. package/dist/standalone/pdf-full-rw.meta.json +78 -0
  32. package/dist/standalone/pdf-full-rw.min.js +56 -0
  33. package/dist/standalone/pdf-full.js +11904 -0
  34. package/dist/standalone/pdf-full.meta.json +63 -0
  35. package/dist/standalone/pdf-full.min.js +35 -0
  36. package/dist/standalone/pdf-large-rw.js +16193 -0
  37. package/dist/standalone/pdf-large-rw.meta.json +72 -0
  38. package/dist/standalone/pdf-large-rw.min.js +56 -0
  39. package/dist/standalone/pdf-large.js +11299 -0
  40. package/dist/standalone/pdf-large.meta.json +57 -0
  41. package/dist/standalone/pdf-large.min.js +35 -0
  42. package/dist/standalone/pdf-legacy-rw.js +18228 -0
  43. package/dist/standalone/pdf-legacy-rw.meta.json +83 -0
  44. package/dist/standalone/pdf-legacy-rw.min.js +56 -0
  45. package/dist/standalone/pdf-legacy.js +13334 -0
  46. package/dist/standalone/pdf-legacy.meta.json +68 -0
  47. package/dist/standalone/pdf-legacy.min.js +35 -0
  48. package/dist/standalone/pdf-rw.js +13404 -0
  49. package/dist/standalone/pdf-rw.meta.json +50 -0
  50. package/dist/standalone/pdf-rw.min.js +56 -0
  51. package/dist/standalone/pdf.js +8510 -0
  52. package/dist/standalone/pdf.meta.json +35 -0
  53. package/dist/standalone/pdf.min.js +35 -0
  54. package/docs/README.md +53 -0
  55. package/docs/api/README.md +38 -0
  56. package/docs/api/_shared/README.md +91 -0
  57. package/docs/api/action/README.md +29 -0
  58. package/docs/api/action/action.md +81 -0
  59. package/docs/api/action/goTo.md +66 -0
  60. package/docs/api/action/launch.md +58 -0
  61. package/docs/api/action/named.md +55 -0
  62. package/docs/api/action/uri.md +54 -0
  63. package/docs/api/annot/README.md +53 -0
  64. package/docs/api/annot/annot.md +114 -0
  65. package/docs/api/annot/fileAttach.md +53 -0
  66. package/docs/api/annot/freeText.md +68 -0
  67. package/docs/api/annot/ink.md +69 -0
  68. package/docs/api/annot/link.md +74 -0
  69. package/docs/api/annot/markup.md +83 -0
  70. package/docs/api/annot/popup.md +52 -0
  71. package/docs/api/annot/projection.md +56 -0
  72. package/docs/api/annot/redact.md +67 -0
  73. package/docs/api/annot/square.md +87 -0
  74. package/docs/api/annot/stamp.md +54 -0
  75. package/docs/api/annot/text.md +69 -0
  76. package/docs/api/annot/widget.md +69 -0
  77. package/docs/api/associatedFiles/README.md +9 -0
  78. package/docs/api/associatedFiles/associatedFiles.md +78 -0
  79. package/docs/api/bundles/README.md +68 -0
  80. package/docs/api/bundles/dist-matrix.md +165 -0
  81. package/docs/api/bundles/pdf-full.md +148 -0
  82. package/docs/api/bundles/pdf-large.md +144 -0
  83. package/docs/api/bundles/pdf-legacy.md +169 -0
  84. package/docs/api/content/README.md +29 -0
  85. package/docs/api/content/color.md +99 -0
  86. package/docs/api/content/graphics.md +114 -0
  87. package/docs/api/content/images.md +124 -0
  88. package/docs/api/content/ops.md +100 -0
  89. package/docs/api/content/stream.md +107 -0
  90. package/docs/api/content/text.md +98 -0
  91. package/docs/api/crypto/README.md +29 -0
  92. package/docs/api/crypto/aesGcm.md +72 -0
  93. package/docs/api/crypto/permissions.md +79 -0
  94. package/docs/api/crypto/security.md +98 -0
  95. package/docs/api/crypto/standardV4.md +104 -0
  96. package/docs/api/crypto/standardV5.md +84 -0
  97. package/docs/api/crypto/standardV6.md +93 -0
  98. package/docs/api/destination/README.md +9 -0
  99. package/docs/api/destination/destination.md +79 -0
  100. package/docs/api/document/README.md +29 -0
  101. package/docs/api/document/builder.md +281 -0
  102. package/docs/api/document/catalog.md +98 -0
  103. package/docs/api/document/document.md +187 -0
  104. package/docs/api/document/encryptedWriter.md +149 -0
  105. package/docs/api/document/incrementalWriter.md +148 -0
  106. package/docs/api/document/page.md +99 -0
  107. package/docs/api/document/pages.md +82 -0
  108. package/docs/api/document/resources.md +102 -0
  109. package/docs/api/document/writer.md +157 -0
  110. package/docs/api/document/xrefStreamWriter.md +122 -0
  111. package/docs/api/embedded/README.md +13 -0
  112. package/docs/api/embedded/collection.md +80 -0
  113. package/docs/api/embedded/embeddedFile.md +86 -0
  114. package/docs/api/embedded/fileSpec.md +87 -0
  115. package/docs/api/errors.md +110 -0
  116. package/docs/api/extra/3d-richmedia.md +76 -0
  117. package/docs/api/extra/README.md +99 -0
  118. package/docs/api/extra/annot-extended.md +71 -0
  119. package/docs/api/extra/associated-files.md +70 -0
  120. package/docs/api/extra/ccitt-fax-decoder.md +74 -0
  121. package/docs/api/extra/color-spaces-extended.md +72 -0
  122. package/docs/api/extra/content-ops-extended.md +82 -0
  123. package/docs/api/extra/document-parts.md +69 -0
  124. package/docs/api/extra/embedded-files-portfolio.md +87 -0
  125. package/docs/api/extra/font-cid-typed.md +77 -0
  126. package/docs/api/extra/font-color-tagging.md +76 -0
  127. package/docs/api/extra/form-actions-extended.md +75 -0
  128. package/docs/api/extra/info-dict-deprecated.md +72 -0
  129. package/docs/api/extra/jbig2-read.md +80 -0
  130. package/docs/api/extra/legacy-deprecated-annots.md +89 -0
  131. package/docs/api/extra/legacy-deprecated-filters.md +78 -0
  132. package/docs/api/extra/legacy-rc4-read.md +74 -0
  133. package/docs/api/extra/legacy-xfa-read.md +65 -0
  134. package/docs/api/extra/linearization-write.md +71 -0
  135. package/docs/api/extra/misc.md +93 -0
  136. package/docs/api/extra/optional-content-extended.md +83 -0
  137. package/docs/api/extra/pdf-a-output-intent.md +65 -0
  138. package/docs/api/extra/pdf-sandbox.md +76 -0
  139. package/docs/api/extra/pdf-ua-tagged.md +63 -0
  140. package/docs/api/extra/pdf-x-prepress.md +65 -0
  141. package/docs/api/extra/redaction-iso32005.md +65 -0
  142. package/docs/api/extra/shading-typed.md +73 -0
  143. package/docs/api/extra/sig-aes-gcm.md +69 -0
  144. package/docs/api/extra/sig-pades.md +103 -0
  145. package/docs/api/extra/tagged-pdf-typed.md +78 -0
  146. package/docs/api/extra/transparency-typed.md +74 -0
  147. package/docs/api/extra/well-tagged-pdf.md +61 -0
  148. package/docs/api/extra/xmp-extended.md +65 -0
  149. package/docs/api/font/README.md +25 -0
  150. package/docs/api/font/embed.md +157 -0
  151. package/docs/api/font/encoding.md +95 -0
  152. package/docs/api/font/font.md +97 -0
  153. package/docs/api/font/type3.md +89 -0
  154. package/docs/api/form/README.md +35 -0
  155. package/docs/api/form/acroform.md +88 -0
  156. package/docs/api/form/appearance.md +87 -0
  157. package/docs/api/form/button.md +97 -0
  158. package/docs/api/form/choice.md +96 -0
  159. package/docs/api/form/fieldTree.md +93 -0
  160. package/docs/api/form/signature.md +90 -0
  161. package/docs/api/form/text.md +88 -0
  162. package/docs/api/linearization/README.md +11 -0
  163. package/docs/api/linearization/linearization.md +81 -0
  164. package/docs/api/main.md +116 -0
  165. package/docs/api/metadata/README.md +10 -0
  166. package/docs/api/metadata/info.md +70 -0
  167. package/docs/api/metadata/xmp.md +62 -0
  168. package/docs/api/ocg/README.md +23 -0
  169. package/docs/api/ocg/config.md +95 -0
  170. package/docs/api/ocg/ocg.md +77 -0
  171. package/docs/api/outline/README.md +11 -0
  172. package/docs/api/outline/outline.md +107 -0
  173. package/docs/api/pdf.md +152 -0
  174. package/docs/api/prepress/README.md +10 -0
  175. package/docs/api/prepress/outputIntent.md +79 -0
  176. package/docs/api/prepress/pageBoundary.md +75 -0
  177. package/docs/api/sig/README.md +32 -0
  178. package/docs/api/sig/byteRange.md +120 -0
  179. package/docs/api/sig/certChain.md +84 -0
  180. package/docs/api/sig/dss.md +111 -0
  181. package/docs/api/sig/oids.md +76 -0
  182. package/docs/api/sig/sha1.md +72 -0
  183. package/docs/api/sig/sign.md +317 -0
  184. package/docs/api/sig/signature.md +178 -0
  185. package/docs/api/sig/timestamp.md +84 -0
  186. package/docs/api/syntax/README.md +29 -0
  187. package/docs/api/syntax/crossRefStream.md +115 -0
  188. package/docs/api/syntax/filters/README.md +50 -0
  189. package/docs/api/syntax/filters/ascii85.md +76 -0
  190. package/docs/api/syntax/filters/asciiHex.md +73 -0
  191. package/docs/api/syntax/filters/dispatch.md +125 -0
  192. package/docs/api/syntax/filters/flate.md +134 -0
  193. package/docs/api/syntax/filters/runLength.md +78 -0
  194. package/docs/api/syntax/objStream.md +88 -0
  195. package/docs/api/syntax/parser-obj.md +97 -0
  196. package/docs/api/syntax/parser.md +151 -0
  197. package/docs/api/syntax/serializer.md +109 -0
  198. package/docs/api/syntax/tokenizer.md +104 -0
  199. package/docs/api/syntax/trailer.md +85 -0
  200. package/docs/api/syntax/xref.md +139 -0
  201. package/docs/api/tagged/README.md +25 -0
  202. package/docs/api/tagged/classMap.md +67 -0
  203. package/docs/api/tagged/markedContent.md +62 -0
  204. package/docs/api/tagged/parentTree.md +67 -0
  205. package/docs/api/tagged/roleMap.md +67 -0
  206. package/docs/api/tagged/structElement.md +76 -0
  207. package/docs/api/tagged/structTree.md +75 -0
  208. package/docs/guide/coverage.md +113 -0
  209. package/docs/guide/crypto.md +121 -0
  210. package/docs/guide/extending.md +76 -0
  211. package/docs/guide/getting-started.md +75 -0
  212. package/docs/guide/legacy-1.7.md +42 -0
  213. package/docs/guide/pades-integration.md +579 -0
  214. package/docs/guide/read-pdf.md +89 -0
  215. package/package.json +97 -4
  216. package/src/_shared/index.js +179 -0
  217. package/src/action/action.js +119 -0
  218. package/src/action/goTo.js +89 -0
  219. package/src/action/launch.js +61 -0
  220. package/src/action/named.js +54 -0
  221. package/src/action/uri.js +51 -0
  222. package/src/annot/annot.js +212 -0
  223. package/src/annot/fileAttach.js +55 -0
  224. package/src/annot/freeText.js +82 -0
  225. package/src/annot/ink.js +77 -0
  226. package/src/annot/link.js +77 -0
  227. package/src/annot/markup.js +91 -0
  228. package/src/annot/popup.js +53 -0
  229. package/src/annot/projection.js +52 -0
  230. package/src/annot/redact.js +87 -0
  231. package/src/annot/square.js +132 -0
  232. package/src/annot/stamp.js +48 -0
  233. package/src/annot/text.js +54 -0
  234. package/src/annot/widget.js +61 -0
  235. package/src/associatedFiles/associatedFiles.js +86 -0
  236. package/src/bundles/pdf-full.js +91 -0
  237. package/src/bundles/pdf-large.js +81 -0
  238. package/src/bundles/pdf-legacy.js +107 -0
  239. package/src/content/color.js +114 -0
  240. package/src/content/graphics.js +192 -0
  241. package/src/content/images.js +160 -0
  242. package/src/content/ops.js +137 -0
  243. package/src/content/stream.js +154 -0
  244. package/src/content/text.js +125 -0
  245. package/src/crypto/aesGcm.js +123 -0
  246. package/src/crypto/permissions.js +112 -0
  247. package/src/crypto/security.js +327 -0
  248. package/src/crypto/standardV4.js +443 -0
  249. package/src/crypto/standardV5.js +306 -0
  250. package/src/crypto/standardV6.js +334 -0
  251. package/src/destination/destination.js +183 -0
  252. package/src/document/builder.js +618 -0
  253. package/src/document/catalog.js +100 -0
  254. package/src/document/document.js +472 -0
  255. package/src/document/encryptedWriter.js +554 -0
  256. package/src/document/incrementalWriter.js +514 -0
  257. package/src/document/page.js +131 -0
  258. package/src/document/pages.js +103 -0
  259. package/src/document/resources.js +146 -0
  260. package/src/document/writer.js +211 -0
  261. package/src/document/xrefStreamWriter.js +353 -0
  262. package/src/embedded/collection.js +102 -0
  263. package/src/embedded/embeddedFile.js +99 -0
  264. package/src/embedded/fileSpec.js +137 -0
  265. package/src/errors.js +78 -0
  266. package/src/extra/3d-richmedia.js +171 -0
  267. package/src/extra/annot-extended.js +200 -0
  268. package/src/extra/associated-files.js +131 -0
  269. package/src/extra/ccitt-fax-decoder.js +776 -0
  270. package/src/extra/color-spaces-extended.js +196 -0
  271. package/src/extra/content-ops-extended.js +153 -0
  272. package/src/extra/document-parts.js +149 -0
  273. package/src/extra/embedded-files-portfolio.js +234 -0
  274. package/src/extra/font-cid-typed.js +185 -0
  275. package/src/extra/font-color-tagging.js +144 -0
  276. package/src/extra/form-actions-extended.js +196 -0
  277. package/src/extra/info-dict-deprecated.js +137 -0
  278. package/src/extra/jbig2-read.js +169 -0
  279. package/src/extra/legacy-deprecated-annots.js +198 -0
  280. package/src/extra/legacy-deprecated-filters.js +167 -0
  281. package/src/extra/legacy-rc4-read.js +235 -0
  282. package/src/extra/legacy-xfa-read.js +104 -0
  283. package/src/extra/linearization-write.js +97 -0
  284. package/src/extra/misc.js +217 -0
  285. package/src/extra/optional-content-extended.js +142 -0
  286. package/src/extra/pdf-a-output-intent.js +112 -0
  287. package/src/extra/pdf-sandbox.js +88 -0
  288. package/src/extra/pdf-ua-tagged.js +116 -0
  289. package/src/extra/pdf-x-prepress.js +114 -0
  290. package/src/extra/redaction-iso32005.js +136 -0
  291. package/src/extra/shading-typed.js +222 -0
  292. package/src/extra/sig-aes-gcm.js +135 -0
  293. package/src/extra/sig-pades.js +242 -0
  294. package/src/extra/tagged-pdf-typed.js +203 -0
  295. package/src/extra/transparency-typed.js +135 -0
  296. package/src/extra/well-tagged-pdf.js +138 -0
  297. package/src/extra/xmp-extended.js +190 -0
  298. package/src/font/embed.js +480 -0
  299. package/src/font/encoding.js +92 -0
  300. package/src/font/font.js +101 -0
  301. package/src/font/type3.js +75 -0
  302. package/src/form/acroform.js +94 -0
  303. package/src/form/appearance.js +90 -0
  304. package/src/form/button.js +105 -0
  305. package/src/form/choice.js +152 -0
  306. package/src/form/fieldTree.js +120 -0
  307. package/src/form/signature.js +100 -0
  308. package/src/form/text.js +101 -0
  309. package/src/linearization/linearization.js +107 -0
  310. package/src/main.js +411 -0
  311. package/src/metadata/info.js +87 -0
  312. package/src/metadata/xmp.js +62 -0
  313. package/src/ocg/config.js +156 -0
  314. package/src/ocg/ocg.js +124 -0
  315. package/src/outline/outline.js +157 -0
  316. package/src/pdf.js +133 -0
  317. package/src/prepress/outputIntent.js +118 -0
  318. package/src/prepress/pageBoundary.js +108 -0
  319. package/src/sig/byteRange.js +306 -0
  320. package/src/sig/certChain.js +247 -0
  321. package/src/sig/dss.js +317 -0
  322. package/src/sig/oids.js +157 -0
  323. package/src/sig/sha1.js +142 -0
  324. package/src/sig/sign.js +1899 -0
  325. package/src/sig/signature.js +1441 -0
  326. package/src/sig/timestamp.js +236 -0
  327. package/src/syntax/crossRefStream.js +133 -0
  328. package/src/syntax/filters/ascii85.js +122 -0
  329. package/src/syntax/filters/asciiHex.js +83 -0
  330. package/src/syntax/filters/dispatch.js +176 -0
  331. package/src/syntax/filters/flate.js +316 -0
  332. package/src/syntax/filters/runLength.js +96 -0
  333. package/src/syntax/objStream.js +99 -0
  334. package/src/syntax/parser-obj.js +52 -0
  335. package/src/syntax/parser.js +321 -0
  336. package/src/syntax/serializer.js +221 -0
  337. package/src/syntax/tokenizer.js +290 -0
  338. package/src/syntax/trailer.js +76 -0
  339. package/src/syntax/xref.js +341 -0
  340. package/src/tagged/classMap.js +81 -0
  341. package/src/tagged/markedContent.js +123 -0
  342. package/src/tagged/parentTree.js +126 -0
  343. package/src/tagged/roleMap.js +107 -0
  344. package/src/tagged/structElement.js +138 -0
  345. package/src/tagged/structTree.js +94 -0
@@ -0,0 +1,618 @@
1
+ // Copyright (c) 2026 AwaCloud SAS
2
+ // Author: Matthieu Bouilloux
3
+ // SPDX-License-Identifier: AGPL-3.0-only
4
+ // Dual-licensed; see the NOTICE file for licensing and any additional terms.
5
+
6
+ /**
7
+ * @fileoverview Constructive document builder — ergonomic DSL on top of
8
+ * `pdfWriter.writeDocument`.
9
+ *
10
+ * Builds a PDF 2.0 document progressively from a chain of high-level
11
+ * calls (addPage, addContent, addFont, addImage, addMetadata, setVersion,
12
+ * setId) and emits `Uint8Array` bytes on `.build()`.
13
+ *
14
+ * The returned builder is closure-based (no `class`, no `this`) so the
15
+ * factory body remains worker-transportable when stringified.
16
+ *
17
+ * Example:
18
+ *
19
+ * const { builder } = pdfBuilder.factory(errors, parserObj, writer);
20
+ * const bytes = builder()
21
+ * .addPage({ mediaBox: [0, 0, 612, 792] })
22
+ * .addFont({ name: 'F1', baseFont: 'Helvetica', subtype: 'Type1' })
23
+ * .addContent('BT /F1 12 Tf 100 700 Td (Hello) Tj ET')
24
+ * .addMetadata({ Title: 'Demo', Author: 'awa' })
25
+ * .build();
26
+ *
27
+ * `addFont` also accepts an **embedded** font — a `pdfFontEmbed.embedSimple`
28
+ * or `embedCid` result. The builder owns the indirect wiring the embed adapter deliberately leaves undone
29
+ * (font-program stream with `/Length1`, `/FontDescriptor`, `/ToUnicode`, and
30
+ * for the composite route the descendant `CIDFont`), and never mutates the
31
+ * caller's dicts:
32
+ *
33
+ * const e = pdfFontEmbed.embedCid(font, codePoints);
34
+ * builder().addPage()
35
+ * .addFont({ name: 'F1', embedded: e })
36
+ * .addContent(concat('BT /F1 12 Tf 72 700 Td ', hex(e.encode('Hé')), ' Tj ET'))
37
+ * .build();
38
+ *
39
+ * `addImage` is the image counterpart of `addFont`'s embedded route: it
40
+ * allocates an image XObject as an indirect stream and registers it in the current page's
41
+ * `/Resources /XObject`. It decodes nothing — the caller supplies already
42
+ * encoded bytes plus their parameters — and it emits no content operator,
43
+ * so the caller places the ink itself:
44
+ *
45
+ * builder().addPage()
46
+ * .addImage({ name: 'Im0', width: 2, height: 2,
47
+ * colorSpace: 'DeviceRGB', bitsPerComponent: 8,
48
+ * data: rgbBytes })
49
+ * .addContent('q 200 0 0 200 72 500 cm /Im0 Do Q')
50
+ * .build();
51
+ *
52
+ * @module pdf/document/builder
53
+ */
54
+
55
+ /**
56
+ * Module factory — worker-safe, self-contained.
57
+ */
58
+ import { pdfErrors } from '../errors.js';
59
+ import { pdfParserObj } from '../syntax/parser-obj.js';
60
+ import { pdfWriter } from './writer.js';
61
+
62
+ export const pdfBuilder = {
63
+ name: 'pdfBuilder',
64
+ dependencies: ['pdfErrors', 'pdfParserObj', 'pdfWriter'],
65
+ deps: [pdfErrors, pdfParserObj, pdfWriter],
66
+ factory(errors, parserObjMod, writerMod) {
67
+ const { RenderError } = errors;
68
+ const obj = parserObjMod.obj;
69
+ const writeDocument = writerMod.writeDocument;
70
+
71
+ const te = new TextEncoder();
72
+
73
+ const STRINGY_INFO = new Set([
74
+ 'Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer'
75
+ ]);
76
+ const DATE_INFO = new Set(['CreationDate', 'ModDate']);
77
+
78
+ function toBytes(v) {
79
+ if (v instanceof Uint8Array) return v;
80
+ if (typeof v === 'string') return te.encode(v);
81
+ throw new RenderError('pdf/builder/bad-bytes',
82
+ 'expected string or Uint8Array',
83
+ { context: { typeof: typeof v } });
84
+ }
85
+
86
+ function strObj(s) {
87
+ return obj.string(te.encode(String(s)), 'lit');
88
+ }
89
+
90
+ function numObj(n) {
91
+ return Number.isInteger(n) ? obj.int(n) : obj.real(n);
92
+ }
93
+
94
+ function boxArray(box) {
95
+ if (!Array.isArray(box) || box.length !== 4) {
96
+ throw new RenderError('pdf/builder/bad-box',
97
+ 'box must be a 4-element array',
98
+ { context: { box } });
99
+ }
100
+ return obj.array(box.map(numObj));
101
+ }
102
+
103
+ /**
104
+ * Shallow-clone a typed dict, overriding/adding `patch` entries.
105
+ * The input dict is NEVER mutated — an embed result is the caller's
106
+ * object and may be registered on several pages.
107
+ */
108
+ function withEntries(dict, patch) {
109
+ const src = (dict && dict.type === 'dict' && dict.entries) ? dict.entries : {};
110
+ const entries = {};
111
+ for (const k of Object.keys(src)) entries[k] = src[k];
112
+ for (const k of Object.keys(patch)) entries[k] = patch[k];
113
+ return obj.dict(entries);
114
+ }
115
+
116
+ function isDict(v) { return !!(v && v.type === 'dict' && v.entries); }
117
+
118
+ /**
119
+ * A `pdfFontEmbed` result: `fontFile` + `descriptor` +
120
+ * `toUnicodeStream` and EITHER `fontDict` (simple) OR
121
+ * `type0Dict` + `cidFontDict` (composite) — never both.
122
+ */
123
+ function isEmbedResult(e) {
124
+ if (!e || typeof e !== 'object') return false;
125
+ if (!(e.fontFile instanceof Uint8Array)) return false;
126
+ if (!isDict(e.descriptor)) return false;
127
+ if (!e.toUnicodeStream || e.toUnicodeStream.type !== 'stream') return false;
128
+ const simple = isDict(e.fontDict);
129
+ const composite = isDict(e.type0Dict) && isDict(e.cidFontDict);
130
+ return simple !== composite;
131
+ }
132
+
133
+ function parseHexId(hex) {
134
+ if (hex instanceof Uint8Array) return hex;
135
+ if (typeof hex !== 'string') {
136
+ throw new RenderError('pdf/builder/bad-id',
137
+ 'id parts must be hex string or Uint8Array');
138
+ }
139
+ const clean = hex.replace(/[^0-9a-fA-F]/g, '');
140
+ if (clean.length % 2 !== 0) {
141
+ throw new RenderError('pdf/builder/bad-id-hex',
142
+ 'hex id must have even length');
143
+ }
144
+ const out = new Uint8Array(clean.length / 2);
145
+ for (let i = 0; i < out.length; i++) {
146
+ out[i] = parseInt(clean.substr(i * 2, 2), 16);
147
+ }
148
+ return out;
149
+ }
150
+
151
+ function createBuilder() {
152
+ // Local mutable state — held in closure.
153
+ let nextNum = 1;
154
+ const indirects = []; // [{ num, gen, value }]
155
+ const pageRecords = []; // [{ num, page-state }]
156
+ const embedNums = new WeakMap(); // embed result -> { num }
157
+ const stdFontNums = new Map(); // baseFont|subtype|encoding -> num
158
+ let currentPage = null;
159
+ let version = '2.0';
160
+ let docId = null; // [Uint8Array, Uint8Array]
161
+ const metadata = {}; // raw user metadata
162
+
163
+ function alloc() { return nextNum++; }
164
+
165
+ function pushIndirect(num, value) {
166
+ indirects.push({ num, gen: 0, value });
167
+ }
168
+
169
+ // ----------------------------------------------------------
170
+ // Page management
171
+ // ----------------------------------------------------------
172
+
173
+ function addPage(opts) {
174
+ const o = opts || {};
175
+ const num = alloc();
176
+ const rec = {
177
+ num,
178
+ mediaBox: o.mediaBox || [0, 0, 612, 792],
179
+ cropBox: o.cropBox || null,
180
+ rotate: Number.isFinite(o.rotate) ? (o.rotate | 0) : null,
181
+ fonts: {}, // name -> font ref { num, gen }
182
+ xobjects: {}, // name -> xobject ref { num, gen }
183
+ extraResources: o.resources || null,
184
+ contents: [] // [num, ...]
185
+ };
186
+ pageRecords.push(rec);
187
+ currentPage = rec;
188
+ return api;
189
+ }
190
+
191
+ // ----------------------------------------------------------
192
+ // Content streams
193
+ // ----------------------------------------------------------
194
+
195
+ function addContent(data) {
196
+ if (!currentPage) {
197
+ throw new RenderError('pdf/builder/no-page',
198
+ 'addContent requires a page (call addPage first)');
199
+ }
200
+ const raw = toBytes(data);
201
+ const num = alloc();
202
+ pushIndirect(num, obj.stream(obj.dict({}), raw));
203
+ currentPage.contents.push(num);
204
+ return api;
205
+ }
206
+
207
+ // ----------------------------------------------------------
208
+ // Fonts (simple Type1/TrueType registration in Resources)
209
+ // ----------------------------------------------------------
210
+
211
+ /** The three predefined simple-font encodings (ISO 32000-1 9.6.6). */
212
+ const STANDARD14_ENCODINGS = Object.freeze(
213
+ ['WinAnsiEncoding', 'MacRomanEncoding', 'StandardEncoding']);
214
+
215
+ function badFont(context) {
216
+ return new RenderError('pdf/builder/bad-font',
217
+ 'addFont requires { name, baseFont, subtype? } or { name, embedded }',
218
+ { context });
219
+ }
220
+
221
+ /**
222
+ * Allocate every indirect an embed result needs, ONCE per result
223
+ * object (identity cache) — the same `embedded` registered on
224
+ * several pages resolves to the same font object number.
225
+ *
226
+ * Simple route → 4 indirects (font program, descriptor,
227
+ * `/ToUnicode`, font dict); composite → 5 (the descendant
228
+ * `CIDFont` in between). Legacy `addFont` allocates 1, so the
229
+ * deltas over a legacy registration are +3 and +4.
230
+ *
231
+ * @returns {number} the font object number to put in Resources.
232
+ */
233
+ function allocEmbedded(embedded) {
234
+ const cached = embedNums.get(embedded);
235
+ if (cached) return cached.num;
236
+
237
+ // (1) font-program stream — /Length1 is the UNCOMPRESSED length
238
+ // (nothing is filtered here, so it equals the byte length).
239
+ const fileKey = typeof embedded.fontFileKey === 'string'
240
+ ? embedded.fontFileKey : 'FontFile2';
241
+ const fileDict = { Length1: obj.int(embedded.fontFile.length) };
242
+ if (fileKey === 'FontFile3') fileDict.Subtype = obj.name('OpenType');
243
+ const fileNum = alloc();
244
+ pushIndirect(fileNum, obj.stream(obj.dict(fileDict), embedded.fontFile));
245
+
246
+ // (2) descriptor — the adapter lifted the program out of it.
247
+ const descNum = alloc();
248
+ pushIndirect(descNum, withEntries(embedded.descriptor,
249
+ { [fileKey]: obj.ref(fileNum, 0) }));
250
+
251
+ // (3) /ToUnicode — the serializer refuses an inline stream.
252
+ const touNum = alloc();
253
+ pushIndirect(touNum, embedded.toUnicodeStream);
254
+
255
+ // (4) the font object itself.
256
+ let fontNum;
257
+ if (isDict(embedded.fontDict)) {
258
+ fontNum = alloc();
259
+ pushIndirect(fontNum, withEntries(embedded.fontDict, {
260
+ FontDescriptor: obj.ref(descNum, 0),
261
+ ToUnicode: obj.ref(touNum, 0)
262
+ }));
263
+ } else {
264
+ const cidNum = alloc();
265
+ pushIndirect(cidNum, withEntries(embedded.cidFontDict, {
266
+ FontDescriptor: obj.ref(descNum, 0)
267
+ }));
268
+ fontNum = alloc();
269
+ pushIndirect(fontNum, withEntries(embedded.type0Dict, {
270
+ DescendantFonts: obj.array([obj.ref(cidNum, 0)]),
271
+ ToUnicode: obj.ref(touNum, 0)
272
+ }));
273
+ }
274
+ embedNums.set(embedded, { num: fontNum });
275
+ return fontNum;
276
+ }
277
+
278
+ /**
279
+ * Register a font in the current page's `/Resources /Font`.
280
+ *
281
+ * A non-embedded font dictionary is allocated once per distinct
282
+ * `(baseFont, subtype, encoding)` and shared by every page that
283
+ * registers it; each page still holds its own resource name.
284
+ * An embedded font is allocated once per embed result object.
285
+ *
286
+ * @param {{ name: string, baseFont?: string, subtype?: string,
287
+ * encoding?: string, embedded?: object }} spec
288
+ * @returns {object} the builder api (chainable)
289
+ */
290
+ function addFont(spec) {
291
+ if (!currentPage) {
292
+ throw new RenderError('pdf/builder/no-page',
293
+ 'addFont requires a page (call addPage first)');
294
+ }
295
+ if (!spec || typeof spec !== 'object' || !spec.name) {
296
+ throw badFont({ spec });
297
+ }
298
+ // Truthiness, exactly as the legacy guard read `spec.baseFont`.
299
+ const hasEmbedded = !!spec.embedded;
300
+ const hasBaseFont = !!spec.baseFont;
301
+ if (hasEmbedded === hasBaseFont) {
302
+ throw badFont({ name: spec.name, hasBaseFont, hasEmbedded });
303
+ }
304
+
305
+ // Optional predefined simple-font encoding (ISO 32000-1
306
+ // 9.6.6 / Annex D), non-embedded route only. `undefined` =
307
+ // absent: no `/Encoding` key, bytes identical to before.
308
+ if (spec.encoding !== undefined
309
+ && (hasEmbedded
310
+ || !STANDARD14_ENCODINGS.includes(spec.encoding))) {
311
+ throw badFont({ name: spec.name, encoding: spec.encoding });
312
+ }
313
+
314
+ let num;
315
+ if (hasEmbedded) {
316
+ if (!isEmbedResult(spec.embedded)) {
317
+ throw badFont({
318
+ name: spec.name,
319
+ embeddedKeys: (spec.embedded && typeof spec.embedded === 'object')
320
+ ? Object.keys(spec.embedded) : typeof spec.embedded
321
+ });
322
+ }
323
+ num = allocEmbedded(spec.embedded);
324
+ } else {
325
+ const subtype = spec.subtype || 'Type1';
326
+ const fontEntries = {
327
+ Type: obj.name('Font'),
328
+ Subtype: obj.name(subtype),
329
+ BaseFont: obj.name(String(spec.baseFont))
330
+ };
331
+ if (spec.encoding !== undefined) {
332
+ fontEntries.Encoding = obj.name(spec.encoding);
333
+ }
334
+ const key = String(spec.baseFont) + '\0' + subtype
335
+ + '\0' + (spec.encoding ?? '');
336
+ num = stdFontNums.get(key);
337
+ if (num === undefined) {
338
+ num = alloc();
339
+ pushIndirect(num, obj.dict(fontEntries));
340
+ stdFontNums.set(key, num);
341
+ }
342
+ }
343
+ currentPage.fonts[String(spec.name)] = { num, gen: 0 };
344
+ return api;
345
+ }
346
+
347
+ // ----------------------------------------------------------
348
+ // Images (XObject registration in Resources)
349
+ // ----------------------------------------------------------
350
+
351
+ function badImage(context) {
352
+ return new RenderError('pdf/builder/bad-image',
353
+ 'addImage requires { name, width, height, colorSpace, '
354
+ + 'bitsPerComponent, data, filter?, decodeParms?, sMask? }',
355
+ { context });
356
+ }
357
+
358
+ function isPosInt(v) { return Number.isSafeInteger(v) && v > 0; }
359
+ function isText(v) { return typeof v === 'string' && v.length > 0; }
360
+
361
+ /**
362
+ * Validate an image spec and allocate its XObject stream.
363
+ *
364
+ * Nothing here decodes, transcodes or inspects `data`: the bytes
365
+ * go into the stream verbatim and every dict entry comes from the
366
+ * caller's parameters.
367
+ *
368
+ * @param {object} spec the `addImage` spec (or an `sMask` spec).
369
+ * @param {boolean} isMask true on the recursive `/SMask` leg — no
370
+ * `name` is required there, and a nested `sMask` is rejected.
371
+ * @returns {number} the allocated indirect object number.
372
+ */
373
+ function allocImage(spec, isMask) {
374
+ if (!spec || typeof spec !== 'object') {
375
+ throw badImage({ spec: spec === null ? 'null' : typeof spec,
376
+ sMask: isMask });
377
+ }
378
+ const keys = [];
379
+ if (!isMask && !isText(spec.name)) keys.push('name');
380
+ if (!isPosInt(spec.width)) keys.push('width');
381
+ if (!isPosInt(spec.height)) keys.push('height');
382
+ if (!isText(spec.colorSpace)) keys.push('colorSpace');
383
+ if (!isPosInt(spec.bitsPerComponent)) keys.push('bitsPerComponent');
384
+ if (!(spec.data instanceof Uint8Array)) keys.push('data');
385
+ if (spec.filter !== undefined && !isText(spec.filter)) {
386
+ keys.push('filter');
387
+ }
388
+ if (spec.decodeParms !== undefined && !isDict(spec.decodeParms)) {
389
+ keys.push('decodeParms');
390
+ }
391
+ // A soft mask has no soft mask of its own.
392
+ if (isMask && spec.sMask !== undefined) keys.push('sMask');
393
+ if (keys.length > 0) {
394
+ const context = { keys, sMask: isMask };
395
+ if (typeof spec.name === 'string') context.name = spec.name;
396
+ throw badImage(context);
397
+ }
398
+
399
+ // The mask is allocated FIRST so its number can be referenced.
400
+ let maskNum = null;
401
+ if (spec.sMask !== undefined) maskNum = allocImage(spec.sMask, true);
402
+
403
+ const dict = {
404
+ Type: obj.name('XObject'),
405
+ Subtype: obj.name('Image'),
406
+ Width: obj.int(spec.width),
407
+ Height: obj.int(spec.height),
408
+ ColorSpace: obj.name(String(spec.colorSpace)),
409
+ BitsPerComponent: obj.int(spec.bitsPerComponent)
410
+ };
411
+ if (spec.filter !== undefined) {
412
+ dict.Filter = obj.name(String(spec.filter));
413
+ }
414
+ if (spec.decodeParms !== undefined) {
415
+ dict.DecodeParms = spec.decodeParms; // passthrough, typed
416
+ }
417
+ if (maskNum !== null) dict.SMask = obj.ref(maskNum, 0);
418
+
419
+ const num = alloc();
420
+ pushIndirect(num, obj.stream(obj.dict(dict), spec.data));
421
+ return num;
422
+ }
423
+
424
+ /**
425
+ * Register an image XObject on the current page.
426
+ *
427
+ * Mirrors `addFont`'s embedded route: the data becomes an indirect
428
+ * stream object, and the page's `/Resources /XObject` dict maps
429
+ * `name` to it. The caller emits the content operators
430
+ * (`q … cm /name Do Q`) itself.
431
+ *
432
+ * @param {object} spec
433
+ * @param {string} spec.name Resource name, without the leading slash (e.g. 'Im0').
434
+ * @param {number} spec.width /Width — positive integer.
435
+ * @param {number} spec.height /Height — positive integer.
436
+ * @param {string} spec.colorSpace /ColorSpace name (e.g. 'DeviceRGB', 'DeviceGray').
437
+ * @param {number} spec.bitsPerComponent /BitsPerComponent.
438
+ * @param {string} [spec.filter] /Filter name (e.g. 'DCTDecode', 'FlateDecode'). Omit for unfiltered data.
439
+ * @param {object} [spec.decodeParms] /DecodeParms — a TYPED obj dict, passthrough.
440
+ * @param {Uint8Array} spec.data The encoded image bytes, verbatim.
441
+ * @param {object} [spec.sMask] Optional soft mask: the same spec shape minus `name`;
442
+ * emitted as its own XObject and referenced by `/SMask`.
443
+ * @returns {object} the builder api (chainable, exactly like addFont)
444
+ */
445
+ function addImage(spec) {
446
+ if (!currentPage) {
447
+ throw new RenderError('pdf/builder/no-page',
448
+ 'addImage requires a page (call addPage first)');
449
+ }
450
+ const num = allocImage(spec, false);
451
+ currentPage.xobjects[String(spec.name)] = { num, gen: 0 };
452
+ return api;
453
+ }
454
+
455
+ // ----------------------------------------------------------
456
+ // Metadata / Info dict
457
+ // ----------------------------------------------------------
458
+
459
+ function addMetadata(meta) {
460
+ if (!meta || typeof meta !== 'object') {
461
+ throw new RenderError('pdf/builder/bad-metadata',
462
+ 'addMetadata requires an object');
463
+ }
464
+ for (const k of Object.keys(meta)) {
465
+ metadata[k] = meta[k];
466
+ }
467
+ return api;
468
+ }
469
+
470
+ function setVersion(v) {
471
+ if (typeof v !== 'string' || !/^\d\.\d$/.test(v)) {
472
+ throw new RenderError('pdf/builder/bad-version',
473
+ 'version must look like "x.y"',
474
+ { context: { version: v } });
475
+ }
476
+ version = v;
477
+ return api;
478
+ }
479
+
480
+ function setId(a, b) {
481
+ const p1 = parseHexId(a);
482
+ const p2 = b !== undefined ? parseHexId(b) : p1;
483
+ docId = [p1, p2];
484
+ return api;
485
+ }
486
+
487
+ // ----------------------------------------------------------
488
+ // Build — assemble graph and call writeDocument
489
+ // ----------------------------------------------------------
490
+
491
+ function buildPageObject(rec, pagesNum) {
492
+ const entries = {
493
+ Type: obj.name('Page'),
494
+ Parent: obj.ref(pagesNum, 0),
495
+ MediaBox: boxArray(rec.mediaBox)
496
+ };
497
+ if (rec.cropBox) entries.CropBox = boxArray(rec.cropBox);
498
+ if (rec.rotate !== null) entries.Rotate = obj.int(rec.rotate);
499
+
500
+ // Resources
501
+ const resEntries = {};
502
+ const fontNames = Object.keys(rec.fonts);
503
+ if (fontNames.length > 0) {
504
+ const fontDict = {};
505
+ for (const fn of fontNames) {
506
+ const r = rec.fonts[fn];
507
+ fontDict[fn] = obj.ref(r.num, r.gen);
508
+ }
509
+ resEntries.Font = obj.dict(fontDict);
510
+ }
511
+ const xobjNames = Object.keys(rec.xobjects);
512
+ if (xobjNames.length > 0) {
513
+ const xobjDict = {};
514
+ for (const xn of xobjNames) {
515
+ const r = rec.xobjects[xn];
516
+ xobjDict[xn] = obj.ref(r.num, r.gen);
517
+ }
518
+ resEntries.XObject = obj.dict(xobjDict);
519
+ }
520
+ // Merge user-provided extra resources (passthrough — must be typed obj).
521
+ if (rec.extraResources && rec.extraResources.type === 'dict') {
522
+ for (const k of Object.keys(rec.extraResources.entries)) {
523
+ if (!resEntries[k]) {
524
+ resEntries[k] = rec.extraResources.entries[k];
525
+ }
526
+ }
527
+ }
528
+ entries.Resources = obj.dict(resEntries);
529
+
530
+ // Contents
531
+ if (rec.contents.length === 1) {
532
+ entries.Contents = obj.ref(rec.contents[0], 0);
533
+ } else if (rec.contents.length > 1) {
534
+ entries.Contents = obj.array(
535
+ rec.contents.map((n) => obj.ref(n, 0))
536
+ );
537
+ }
538
+ return obj.dict(entries);
539
+ }
540
+
541
+ function buildInfoDict() {
542
+ const keys = Object.keys(metadata);
543
+ if (keys.length === 0) return null;
544
+ const e = {};
545
+ for (const k of keys) {
546
+ const v = metadata[k];
547
+ if (v === null || v === undefined) continue;
548
+ if (STRINGY_INFO.has(k) || DATE_INFO.has(k)) {
549
+ e[k] = strObj(v);
550
+ } else {
551
+ // Unknown — accept strings as PDF strings.
552
+ if (typeof v === 'string') e[k] = strObj(v);
553
+ }
554
+ }
555
+ if (Object.keys(e).length === 0) return null;
556
+ return obj.dict(e);
557
+ }
558
+
559
+ function build() {
560
+ if (pageRecords.length === 0) {
561
+ throw new RenderError('pdf/builder/no-pages',
562
+ 'build requires at least one page');
563
+ }
564
+
565
+ // Reserve numbers for Catalog + Pages so refs work.
566
+ const catalogNum = alloc();
567
+ const pagesNum = alloc();
568
+
569
+ // Build page objects (uses reserved pagesNum as Parent).
570
+ const kidsRefs = [];
571
+ for (const rec of pageRecords) {
572
+ pushIndirect(rec.num, buildPageObject(rec, pagesNum));
573
+ kidsRefs.push(obj.ref(rec.num, 0));
574
+ }
575
+
576
+ // Catalog.
577
+ pushIndirect(catalogNum, obj.dict({
578
+ Type: obj.name('Catalog'),
579
+ Pages: obj.ref(pagesNum, 0)
580
+ }));
581
+
582
+ // Pages tree node.
583
+ pushIndirect(pagesNum, obj.dict({
584
+ Type: obj.name('Pages'),
585
+ Kids: obj.array(kidsRefs),
586
+ Count: obj.int(pageRecords.length)
587
+ }));
588
+
589
+ // Info dict (optional).
590
+ let infoRef = null;
591
+ const infoDict = buildInfoDict();
592
+ if (infoDict) {
593
+ const infoNum = alloc();
594
+ pushIndirect(infoNum, infoDict);
595
+ infoRef = { num: infoNum, gen: 0 };
596
+ }
597
+
598
+ const writeOpts = {
599
+ indirects,
600
+ root: { num: catalogNum, gen: 0 },
601
+ version
602
+ };
603
+ if (infoRef) writeOpts.info = infoRef;
604
+ if (docId) writeOpts.id = docId;
605
+
606
+ return writeDocument(writeOpts);
607
+ }
608
+
609
+ const api = {
610
+ addPage, addContent, addFont, addImage, addMetadata,
611
+ setVersion, setId, build
612
+ };
613
+ return api;
614
+ }
615
+
616
+ return { builder: createBuilder };
617
+ }
618
+ };