@laminapdf/sdk 0.2.0-alpha.3 → 0.2.0-alpha.5

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 (421) hide show
  1. package/README.md +21 -1
  2. package/dist/api.d.ts +5 -2
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +1 -0
  5. package/dist/api.js.map +1 -1
  6. package/dist/builder.d.ts +8 -3
  7. package/dist/builder.d.ts.map +1 -1
  8. package/dist/builder.js +8 -3
  9. package/dist/builder.js.map +1 -1
  10. package/dist/document.d.ts +78 -1
  11. package/dist/document.d.ts.map +1 -1
  12. package/dist/document.js +112 -0
  13. package/dist/document.js.map +1 -1
  14. package/dist/extract-job.d.ts +100 -0
  15. package/dist/extract-job.d.ts.map +1 -0
  16. package/dist/extract-job.js +177 -0
  17. package/dist/extract-job.js.map +1 -0
  18. package/dist/extract.d.ts +1 -1
  19. package/dist/extract.d.ts.map +1 -1
  20. package/dist/extract.js.map +1 -1
  21. package/dist/generated/builder-enums.d.ts +3 -3
  22. package/dist/generated/builder-enums.d.ts.map +1 -1
  23. package/dist/generated/builder-enums.js +10 -0
  24. package/dist/generated/builder-enums.js.map +1 -1
  25. package/dist/generated/codes.d.ts +2 -2
  26. package/dist/generated/codes.d.ts.map +1 -1
  27. package/dist/generated/codes.js +55 -1
  28. package/dist/generated/codes.js.map +1 -1
  29. package/dist/generated/explain.d.ts.map +1 -1
  30. package/dist/generated/explain.js +19 -2
  31. package/dist/generated/explain.js.map +1 -1
  32. package/dist/generated/extract.d.ts +50 -3
  33. package/dist/generated/extract.d.ts.map +1 -1
  34. package/dist/reflow.d.ts +88 -0
  35. package/dist/reflow.d.ts.map +1 -0
  36. package/dist/reflow.js +4 -0
  37. package/dist/reflow.js.map +1 -0
  38. package/dist/source/http.d.ts +1 -1
  39. package/dist/source/http.js +1 -1
  40. package/dist/types.d.ts +35 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/dist/worker/support.d.ts +10 -1
  45. package/dist/worker/support.d.ts.map +1 -1
  46. package/dist/worker/support.js +7 -0
  47. package/dist/worker/support.js.map +1 -1
  48. package/docs/AGENTS.md +1 -1
  49. package/docs/api/README.md +29 -1
  50. package/docs/api/classes/Builder.md +1 -1
  51. package/docs/api/classes/Document.md +186 -1
  52. package/docs/api/classes/ExtractJob.md +236 -0
  53. package/docs/api/classes/Font.md +1 -1
  54. package/docs/api/classes/Image.md +1 -1
  55. package/docs/api/classes/LaminaPdfError.md +1 -1
  56. package/docs/api/classes/Page.md +1 -1
  57. package/docs/api/classes/PageRef.md +1 -1
  58. package/docs/api/classes/PageText.md +1 -1
  59. package/docs/api/classes/PageTextRun.md +1 -1
  60. package/docs/api/classes/PendingPath.md +1 -1
  61. package/docs/api/classes/SaveJob.md +1 -1
  62. package/docs/api/classes/Surface.md +10 -5
  63. package/docs/api/classes/Transaction.md +1 -1
  64. package/docs/api/functions/callbackSource.md +1 -1
  65. package/docs/api/functions/extractWithModels.md +70 -0
  66. package/docs/api/functions/httpRangeSource.md +2 -2
  67. package/docs/api/functions/init.md +1 -1
  68. package/docs/api/functions/isExtractLoaded.md +1 -1
  69. package/docs/api/functions/isInitialized.md +1 -1
  70. package/docs/api/functions/isLaminaPdfError.md +1 -1
  71. package/docs/api/functions/isTextLoaded.md +1 -1
  72. package/docs/api/functions/loadExtract.md +1 -1
  73. package/docs/api/functions/loadText.md +1 -1
  74. package/docs/api/functions/onLeak.md +1 -1
  75. package/docs/api/functions/reset.md +1 -1
  76. package/docs/api/functions/wasmMemory.md +1 -1
  77. package/docs/api/interfaces/BuilderOptions.md +1 -1
  78. package/docs/api/interfaces/ByteRange.md +1 -1
  79. package/docs/api/interfaces/ChunkOptions.md +1 -1
  80. package/docs/api/interfaces/Diagnostic.md +1 -1
  81. package/docs/api/interfaces/ExtractOptions.md +1 -1
  82. package/docs/api/interfaces/ExtractResult.md +1 -1
  83. package/docs/api/interfaces/FontInfo.md +1 -1
  84. package/docs/api/interfaces/HostModels.md +57 -0
  85. package/docs/api/interfaces/HttpRangeOptions.md +1 -1
  86. package/docs/api/interfaces/InitOptions.md +1 -1
  87. package/docs/api/interfaces/LayoutRegion.md +38 -0
  88. package/docs/api/interfaces/Leak.md +1 -1
  89. package/docs/api/interfaces/Location.md +1 -1
  90. package/docs/api/interfaces/ModelInfo.md +45 -0
  91. package/docs/api/interfaces/ModelRequest.md +45 -0
  92. package/docs/api/interfaces/Models.md +45 -0
  93. package/docs/api/interfaces/OcrWord.md +37 -0
  94. package/docs/api/interfaces/OpLog.md +1 -1
  95. package/docs/api/interfaces/OpenOptions.md +1 -1
  96. package/docs/api/interfaces/PageTextOptions.md +1 -1
  97. package/docs/api/interfaces/PathBuilder.md +1 -1
  98. package/docs/api/interfaces/QuadPoint.md +1 -1
  99. package/docs/api/interfaces/RangeOptions.md +1 -1
  100. package/docs/api/interfaces/RangeSource.md +1 -1
  101. package/docs/api/interfaces/Reflow.md +61 -0
  102. package/docs/api/interfaces/ReflowChecks.md +69 -0
  103. package/docs/api/interfaces/StrokeStyle.md +1 -1
  104. package/docs/api/interfaces/TagSpec.md +3 -3
  105. package/docs/api/interfaces/TextBoxOptions.md +1 -1
  106. package/docs/api/interfaces/TextGlyph.md +1 -1
  107. package/docs/api/interfaces/TextOptions.md +1 -1
  108. package/docs/api/interfaces/WasmMemory.md +1 -1
  109. package/docs/api/type-aliases/AfRelationship.md +1 -1
  110. package/docs/api/type-aliases/Align.md +1 -1
  111. package/docs/api/type-aliases/ArtifactKind.md +2 -2
  112. package/docs/api/type-aliases/AttachmentOptions.md +1 -1
  113. package/docs/api/type-aliases/BlendMode.md +1 -1
  114. package/docs/api/type-aliases/BoxRect.md +1 -1
  115. package/docs/api/type-aliases/BuildOptions.md +1 -1
  116. package/docs/api/type-aliases/Cell.md +1 -1
  117. package/docs/api/type-aliases/Chunk.md +1 -1
  118. package/docs/api/type-aliases/Color.md +1 -1
  119. package/docs/api/type-aliases/Coordinates.md +1 -1
  120. package/docs/api/type-aliases/Direction.md +1 -1
  121. package/docs/api/type-aliases/DocumentInfo.md +1 -1
  122. package/docs/api/type-aliases/DocumentProperties.md +18 -0
  123. package/docs/api/type-aliases/DropReason.md +2 -2
  124. package/docs/api/type-aliases/Dropped.md +1 -1
  125. package/docs/api/type-aliases/Edited.md +1 -1
  126. package/docs/api/type-aliases/ErrorCode.md +2 -2
  127. package/docs/api/type-aliases/ErrorKind.md +1 -1
  128. package/docs/api/type-aliases/ExtractFormat.md +1 -1
  129. package/docs/api/type-aliases/ExtractJobOptions.md +31 -0
  130. package/docs/api/type-aliases/ExtractStep.md +61 -0
  131. package/docs/api/type-aliases/ExtractedBlock.md +13 -1
  132. package/docs/api/type-aliases/ExtractedPage.md +9 -1
  133. package/docs/api/type-aliases/ExtractedSource.md +1 -1
  134. package/docs/api/type-aliases/Extraction.md +1 -1
  135. package/docs/api/type-aliases/FillRule.md +1 -1
  136. package/docs/api/type-aliases/Fit.md +1 -1
  137. package/docs/api/type-aliases/Flags.md +1 -1
  138. package/docs/api/type-aliases/Generator.md +5 -3
  139. package/docs/api/type-aliases/GlyphQuad.md +1 -1
  140. package/docs/api/type-aliases/InfoPatch.md +1 -1
  141. package/docs/api/type-aliases/LaminaPdfWarning.md +1 -1
  142. package/docs/api/type-aliases/LimitName.md +1 -1
  143. package/docs/api/type-aliases/Limits.md +1 -1
  144. package/docs/api/type-aliases/LineBox.md +1 -1
  145. package/docs/api/type-aliases/LineCap.md +1 -1
  146. package/docs/api/type-aliases/LineJoin.md +1 -1
  147. package/docs/api/type-aliases/LinkTarget.md +1 -1
  148. package/docs/api/type-aliases/Metadata.md +1 -1
  149. package/docs/api/type-aliases/ModelResult.md +15 -0
  150. package/docs/api/type-aliases/Op.md +1 -1
  151. package/docs/api/type-aliases/OutlineEntry.md +1 -1
  152. package/docs/api/type-aliases/OutlineNode.md +15 -0
  153. package/docs/api/type-aliases/Overflow.md +1 -1
  154. package/docs/api/type-aliases/PageImageReason.md +15 -0
  155. package/docs/api/type-aliases/PageInfo.md +1 -1
  156. package/docs/api/type-aliases/PageLabel.md +1 -1
  157. package/docs/api/type-aliases/PageLink.md +20 -0
  158. package/docs/api/type-aliases/PageSizeName.md +1 -1
  159. package/docs/api/type-aliases/Point.md +1 -1
  160. package/docs/api/type-aliases/Profile.md +1 -1
  161. package/docs/api/type-aliases/Progress.md +1 -1
  162. package/docs/api/type-aliases/ReadingOrder.md +1 -1
  163. package/docs/api/type-aliases/Rect.md +1 -1
  164. package/docs/api/type-aliases/ReflowBlock.md +44 -0
  165. package/docs/api/type-aliases/ReflowKind.md +15 -0
  166. package/docs/api/type-aliases/ReflowRefusal.md +16 -0
  167. package/docs/api/type-aliases/ReflowSource.md +15 -0
  168. package/docs/api/type-aliases/Region.md +1 -1
  169. package/docs/api/type-aliases/RegionLabel.md +15 -0
  170. package/docs/api/type-aliases/RenderMode.md +1 -1
  171. package/docs/api/type-aliases/Rotation.md +1 -1
  172. package/docs/api/type-aliases/SaveOptions.md +1 -1
  173. package/docs/api/type-aliases/SaveReport.md +1 -1
  174. package/docs/api/type-aliases/Span.md +12 -1
  175. package/docs/api/type-aliases/SplitOn.md +1 -1
  176. package/docs/api/type-aliases/Standard14.md +1 -1
  177. package/docs/api/type-aliases/StructRef.md +1 -1
  178. package/docs/api/type-aliases/StructSource.md +1 -1
  179. package/docs/api/type-aliases/StructureKid.md +15 -0
  180. package/docs/api/type-aliases/StructureNode.md +16 -0
  181. package/docs/api/type-aliases/TableDetector.md +1 -1
  182. package/docs/api/type-aliases/TableMode.md +1 -1
  183. package/docs/api/type-aliases/TableScope.md +1 -1
  184. package/docs/api/type-aliases/Tag.md +1 -1
  185. package/docs/api/type-aliases/TagName.md +1 -1
  186. package/docs/api/type-aliases/Target.md +15 -0
  187. package/docs/api/type-aliases/TargetView.md +18 -0
  188. package/docs/api/type-aliases/TextLayer.md +72 -0
  189. package/docs/api/type-aliases/TextLayerStatus.md +15 -0
  190. package/docs/api/type-aliases/TextLayout.md +1 -1
  191. package/docs/api/type-aliases/TextRange.md +1 -1
  192. package/docs/api/type-aliases/TextRunInfo.md +1 -1
  193. package/docs/api/type-aliases/Transform.md +1 -1
  194. package/docs/api/type-aliases/UndoOutcome.md +1 -1
  195. package/docs/api/type-aliases/ViewerPreferences.md +1 -1
  196. package/docs/api/type-aliases/WarningCode.md +2 -2
  197. package/docs/api/type-aliases/WasmSource.md +1 -1
  198. package/docs/api/variables/Matrix.md +1 -1
  199. package/docs/api/variables/PAGE_SIZES.md +1 -1
  200. package/docs/api/variables/TAG_NAMES.md +2 -2
  201. package/docs/errors/README.md +18 -1
  202. package/docs/errors/aes-padding-invalid.md +1 -1
  203. package/docs/errors/already-initialized.md +1 -1
  204. package/docs/errors/binary-output-to-terminal.md +1 -1
  205. package/docs/errors/builder-finished.md +1 -1
  206. package/docs/errors/cancelled.md +1 -1
  207. package/docs/errors/cmap-invalid.md +1 -1
  208. package/docs/errors/collection-index.md +1 -1
  209. package/docs/errors/color-components.md +1 -1
  210. package/docs/errors/content-bad-operands.md +1 -1
  211. package/docs/errors/content-path-unpainted.md +1 -1
  212. package/docs/errors/content-resource-missing.md +1 -1
  213. package/docs/errors/content-syntax.md +1 -1
  214. package/docs/errors/content-unbalanced-bt.md +1 -1
  215. package/docs/errors/content-unbalanced-marked.md +1 -1
  216. package/docs/errors/content-unbalanced-q.md +1 -1
  217. package/docs/errors/content-unknown-operator.md +1 -1
  218. package/docs/errors/covered-text.md +33 -0
  219. package/docs/errors/data-pending.md +1 -1
  220. package/docs/errors/decompress.md +1 -1
  221. package/docs/errors/device-color-without-output-intent.md +1 -1
  222. package/docs/errors/direct-page.md +1 -1
  223. package/docs/errors/disposed.md +1 -1
  224. package/docs/errors/document-changed.md +1 -1
  225. package/docs/errors/embedded-file-forbidden.md +1 -1
  226. package/docs/errors/embedded-file-without-mime-type.md +1 -1
  227. package/docs/errors/encoding-full.md +1 -1
  228. package/docs/errors/encrypted-base.md +1 -1
  229. package/docs/errors/extract-column-order-uncertain.md +38 -0
  230. package/docs/errors/extract-limit-blocks.md +1 -1
  231. package/docs/errors/extract-module-not-loaded.md +1 -1
  232. package/docs/errors/extract-needs-ocr.md +37 -0
  233. package/docs/errors/extract-reading-order-ambiguous.md +1 -1
  234. package/docs/errors/extract-struct-tree-ignored.md +1 -1
  235. package/docs/errors/extract-table-ambiguous.md +1 -1
  236. package/docs/errors/extract-table-limit.md +1 -1
  237. package/docs/errors/extract-untagged-content.md +1 -1
  238. package/docs/errors/fetch-failed.md +3 -3
  239. package/docs/errors/flate-truncated.md +1 -1
  240. package/docs/errors/flow-content-too-large.md +34 -0
  241. package/docs/errors/flow-keep-broken.md +34 -0
  242. package/docs/errors/flow-layout-unstable.md +38 -0
  243. package/docs/errors/font-invalid.md +1 -1
  244. package/docs/errors/font-mismatch.md +1 -1
  245. package/docs/errors/font-not-embeddable.md +1 -1
  246. package/docs/errors/font-not-embedded.md +1 -1
  247. package/docs/errors/font-not-shapable.md +1 -1
  248. package/docs/errors/glyph-out-of-range.md +1 -1
  249. package/docs/errors/glyph-without-unicode.md +7 -5
  250. package/docs/errors/heading-level-skipped.md +1 -1
  251. package/docs/errors/image-data-length.md +1 -1
  252. package/docs/errors/image-too-large.md +1 -1
  253. package/docs/errors/inline-image-unterminated.md +1 -1
  254. package/docs/errors/input-unreadable.md +1 -1
  255. package/docs/errors/instance-reset.md +1 -1
  256. package/docs/errors/invalid-argument.md +1 -1
  257. package/docs/errors/invalid-axis-value.md +1 -1
  258. package/docs/errors/invalid-box.md +1 -1
  259. package/docs/errors/invalid-color.md +1 -1
  260. package/docs/errors/invalid-command-line.md +1 -1
  261. package/docs/errors/invalid-date.md +1 -1
  262. package/docs/errors/invalid-entry.md +1 -1
  263. package/docs/errors/invalid-feature.md +1 -1
  264. package/docs/errors/invalid-font.md +1 -1
  265. package/docs/errors/invalid-geometry.md +1 -1
  266. package/docs/errors/invalid-icc-profile.md +1 -1
  267. package/docs/errors/invalid-image.md +1 -1
  268. package/docs/errors/invalid-op-log.md +1 -1
  269. package/docs/errors/invalid-page-label.md +1 -1
  270. package/docs/errors/invalid-page-labels.md +1 -1
  271. package/docs/errors/invalid-page-range.md +1 -1
  272. package/docs/errors/invalid-rotate.md +1 -1
  273. package/docs/errors/invalid-spans.md +39 -0
  274. package/docs/errors/invalid-structure-nesting.md +1 -1
  275. package/docs/errors/invalid-structure.md +1 -1
  276. package/docs/errors/invalid-text-range.md +1 -1
  277. package/docs/errors/job-finished.md +1 -1
  278. package/docs/errors/limit-exceeded.md +1 -1
  279. package/docs/errors/link-without-contents.md +1 -1
  280. package/docs/errors/link-without-structure.md +1 -1
  281. package/docs/errors/markdown-html-ignored.md +33 -0
  282. package/docs/errors/markdown-image-missing.md +34 -0
  283. package/docs/errors/missing-alt-text.md +1 -1
  284. package/docs/errors/missing-glyph.md +1 -1
  285. package/docs/errors/missing-header.md +1 -1
  286. package/docs/errors/missing-lang.md +1 -1
  287. package/docs/errors/missing-root.md +1 -1
  288. package/docs/errors/missing-title.md +1 -1
  289. package/docs/errors/model-failed.md +33 -0
  290. package/docs/errors/model-needs-host.md +40 -0
  291. package/docs/errors/model-result-unexpected.md +36 -0
  292. package/docs/errors/name-tree-node-skipped.md +1 -1
  293. package/docs/errors/nested-transaction.md +1 -1
  294. package/docs/errors/no-pending-save.md +1 -1
  295. package/docs/errors/no-transaction.md +1 -1
  296. package/docs/errors/not-initialized.md +1 -1
  297. package/docs/errors/not-pdf.md +1 -1
  298. package/docs/errors/not-rebased.md +1 -1
  299. package/docs/errors/object-number-out-of-range.md +1 -1
  300. package/docs/errors/objstm-index-mismatch.md +1 -1
  301. package/docs/errors/out-of-memory.md +1 -1
  302. package/docs/errors/outline-cycle.md +1 -1
  303. package/docs/errors/outline-item-skipped.md +1 -1
  304. package/docs/errors/output-unwritable.md +1 -1
  305. package/docs/errors/overwrite-refused.md +1 -1
  306. package/docs/errors/page-count-mismatch.md +1 -1
  307. package/docs/errors/page-index-out-of-range.md +1 -1
  308. package/docs/errors/page-tree-node-skipped.md +1 -1
  309. package/docs/errors/password-required.md +1 -1
  310. package/docs/errors/path-outside-roots.md +1 -1
  311. package/docs/errors/perms-mismatch.md +1 -1
  312. package/docs/errors/profile-version-conflict.md +1 -1
  313. package/docs/errors/redaction-not-applied.md +32 -0
  314. package/docs/errors/reference-cycle.md +1 -1
  315. package/docs/errors/render-backend-unavailable.md +1 -1
  316. package/docs/errors/render-failed.md +1 -1
  317. package/docs/errors/render-too-large.md +1 -1
  318. package/docs/errors/repaired-base.md +1 -1
  319. package/docs/errors/requires-rewrite.md +1 -1
  320. package/docs/errors/revision-index-out-of-range.md +34 -0
  321. package/docs/errors/saved-not-rebased.md +1 -1
  322. package/docs/errors/sink-not-empty.md +1 -1
  323. package/docs/errors/sink.md +1 -1
  324. package/docs/errors/source-budget-exceeded.md +1 -1
  325. package/docs/errors/source-changed.md +1 -1
  326. package/docs/errors/source.md +1 -1
  327. package/docs/errors/stale-generation.md +1 -1
  328. package/docs/errors/stream-length-recovered.md +1 -1
  329. package/docs/errors/strict.md +1 -1
  330. package/docs/errors/subset-failed.md +1 -1
  331. package/docs/errors/syntax.md +1 -1
  332. package/docs/errors/tag-element-not-open.md +41 -0
  333. package/docs/errors/tag-element-unclosed.md +34 -0
  334. package/docs/errors/text-in-earlier-revision.md +36 -0
  335. package/docs/errors/text-module-not-loaded.md +1 -1
  336. package/docs/errors/text-overflow.md +1 -1
  337. package/docs/errors/tounicode-conflict.md +1 -1
  338. package/docs/errors/type.md +1 -1
  339. package/docs/errors/unexpected-eof.md +1 -1
  340. package/docs/errors/unknown-axis.md +1 -1
  341. package/docs/errors/unknown-code.md +1 -1
  342. package/docs/errors/unknown-destination.md +1 -1
  343. package/docs/errors/unknown-font.md +1 -1
  344. package/docs/errors/unknown-option.md +1 -1
  345. package/docs/errors/unknown-page.md +1 -1
  346. package/docs/errors/unmapped-glyph.md +1 -1
  347. package/docs/errors/unnumbered-heading-misused.md +1 -1
  348. package/docs/errors/unset-object.md +1 -1
  349. package/docs/errors/unsupported-filter.md +1 -1
  350. package/docs/errors/unsupported-image.md +1 -1
  351. package/docs/errors/unsupported-layout-option.md +1 -1
  352. package/docs/errors/unsupported-security-handler.md +1 -1
  353. package/docs/errors/unsupported-version.md +1 -1
  354. package/docs/errors/untagged-content.md +1 -1
  355. package/docs/errors/warnings-truncated.md +1 -1
  356. package/docs/errors/wasm-trap.md +1 -1
  357. package/docs/errors/whole-font-rebuilt.md +1 -1
  358. package/docs/errors/worker-crashed.md +1 -1
  359. package/docs/errors/worker-failed.md +1 -1
  360. package/docs/errors/worker-version-mismatch.md +1 -1
  361. package/docs/errors/would-invalidate-signatures.md +1 -1
  362. package/docs/errors/wrong-document.md +1 -1
  363. package/docs/errors/wrong-password.md +1 -1
  364. package/docs/errors/xobject-cycle.md +1 -1
  365. package/docs/errors/xref-repaired.md +1 -1
  366. package/docs/errors/xrefstm-ignored.md +1 -1
  367. package/docs/llms.txt +7 -1
  368. package/docs/pages/concepts/dependencies.md +2 -2
  369. package/docs/pages/concepts/drawing-pages.md +76 -0
  370. package/docs/pages/concepts/errors-and-warnings.md +6 -2
  371. package/docs/pages/concepts/how-lamina-thinks.md +13 -1
  372. package/docs/pages/concepts/pages-and-coordinates.md +9 -1
  373. package/docs/pages/guides/accessible-pdfs.md +3 -1
  374. package/docs/pages/guides/archival-pdfs.md +5 -1
  375. package/docs/pages/guides/edit-a-pdf.md +8 -2
  376. package/docs/pages/guides/flowing-pages.md +379 -0
  377. package/docs/pages/guides/fonts-and-text.md +3 -1
  378. package/docs/pages/guides/images-and-vectors.md +3 -1
  379. package/docs/pages/guides/keep-the-page-responsive.md +8 -4
  380. package/docs/pages/guides/links-outlines-and-metadata.md +3 -1
  381. package/docs/pages/guides/open-large-files.md +69 -0
  382. package/docs/pages/guides/password-protected-files.md +1 -1
  383. package/docs/pages/guides/save-a-pdf.md +12 -2
  384. package/docs/pages/guides/test-generated-pdfs.md +324 -0
  385. package/docs/pages/guides/untrusted-files.md +3 -1
  386. package/docs/pages/index.md +2 -2
  387. package/docs/pages/install.md +9 -5
  388. package/docs/pages/quick-start/browser.md +5 -1
  389. package/docs/pages/quick-start/bun.md +5 -1
  390. package/docs/pages/quick-start/deno.md +5 -1
  391. package/docs/pages/quick-start/node.md +5 -1
  392. package/docs/pages/quick-start/rust.md +6 -2
  393. package/docs/pages/quick-start/workers.md +2 -2
  394. package/docs/pages/recipes/cloudflare-worker.md +1 -1
  395. package/docs/pages/recipes/element-flow.md +248 -0
  396. package/docs/pages/recipes/extract-markdown.md +44 -6
  397. package/docs/pages/recipes/generate-then-edit.md +13 -6
  398. package/docs/pages/recipes/hello.md +13 -6
  399. package/docs/pages/recipes/images-and-vectors.md +13 -6
  400. package/docs/pages/recipes/invoice.md +13 -6
  401. package/docs/pages/recipes/links-and-outlines.md +13 -6
  402. package/docs/pages/recipes/multilingual.md +13 -6
  403. package/docs/pages/recipes/open-untrusted.md +1 -1
  404. package/docs/pages/recipes/pdf-a-archive.md +13 -6
  405. package/docs/pages/recipes/pdf-a3-invoice-with-xml.md +13 -6
  406. package/docs/pages/recipes/pdf-ua-report.md +13 -6
  407. package/docs/pages/recipes/report.md +2 -2
  408. package/docs/pages/recipes/web-worker-client.md +1 -1
  409. package/docs/pages/recipes.md +3 -2
  410. package/docs/pages/reference/benchmarks.md +93 -0
  411. package/docs/pages/reference/changelog.md +77 -1
  412. package/docs/pages/reference/codes.md +1 -1
  413. package/docs/pages/reference/glossary.md +1 -1
  414. package/docs/pages/reference/rust.md +11 -11
  415. package/docs/pages/reference/stability.md +3 -3
  416. package/docs/pages/start/coding-agents.md +1 -1
  417. package/docs/pages/start/first-pdf.md +6 -2
  418. package/docs/pages/troubleshooting.md +1 -1
  419. package/docs/schemas/error.schema.json +18 -1
  420. package/package.json +5 -3
  421. package/schemas/error.schema.json +18 -1
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Text and fonts"
3
3
  description: "Draw text in any script with your own fonts or the standard 14, one line at a time or as a paragraph that wraps in a box."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/guides/fonts-and-text/"
6
6
  ---
7
7
 
@@ -189,6 +189,8 @@ pub(crate) fn build(options: BuildOptions, fonts: Vec<Vec<u8>>) -> Result<Vec<u8
189
189
  }
190
190
  ````
191
191
 
192
+ **Result:** [multilingual.pdf](https://docs.laminapdf.com/examples/pdf/multilingual.pdf)
193
+
192
194
  [See the output](../../pages/recipes/multilingual.md). Text extracted from it, with pdf.js, pypdf or pdftotext, equals the input strings, ligatures included.
193
195
 
194
196
  ## When a font can't draw the text
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Shapes, colour and images"
3
3
  description: "Draw lines, shapes and images on a page, in any colour, with transparency, clipping and transforms."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/guides/images-and-vectors/"
6
6
  ---
7
7
 
@@ -212,6 +212,8 @@ fn images(s: &mut Surface, photo: &Image, glow: &Image) {
212
212
  }
213
213
  ````
214
214
 
215
+ **Result:** [images_and_vectors.pdf](https://docs.laminapdf.com/examples/pdf/images_and_vectors.pdf)
216
+
215
217
  [See the output](../../pages/recipes/images-and-vectors.md).
216
218
 
217
219
  > **Details:** A PNG's alpha channel, and an RGBA image's, becomes a soft mask: a second image that holds the transparency.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Keep the page responsive"
3
3
  description: "Run Lamina PDF in a Web Worker with @laminapdf/sdk/worker, so a large file never freezes the page."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/guides/keep-the-page-responsive/"
6
6
  ---
7
7
 
@@ -58,11 +58,15 @@ The Web Worker leaves out a few members, each for a reason:
58
58
 
59
59
  ## Cancel a call
60
60
 
61
- Pass an `AbortSignal` as `signal` to any call. An aborted call rejects with `cancelled`, and `error.cause` holds the signal's reason. What happens to the work depends on where it was:
61
+ The Web Worker runs one call at a time, so a call waits while another runs. Pass an `AbortSignal` as `signal` to any call. An aborted call rejects with `cancelled`, and `error.cause` holds the signal's reason. What happens to the work depends on where it was:
62
62
 
63
63
  - A call that hasn't started never runs.
64
- - `save` stops at its next step, and the document stays usable.
65
- - Any other call runs to the end in the Web Worker, because it is one synchronous call. `client.terminate()` stops it at once.
64
+ - `save` and `saveWithReport` stop at their next step, and the document stays usable.
65
+ - Any other call runs to the end in the Web Worker, because it is one synchronous call, though the byte fetches of a file opened with `openUrl` stop. `client.terminate()` stops it at once.
66
+
67
+ *Each call crosses to the Web Worker as a message; an abort skips a call not started and stops a running save.*
68
+
69
+ Your page's client sends each call, such as `await doc.pageCount()`, to the Web Worker as a message. There `@laminapdf/sdk` runs one call at a time, and each answer comes back as a Promise; saved bytes and page text come back transferred rather than copied. When a call's `AbortSignal` aborts, the call rejects with `cancelled`. A call not started never runs, and a running `save` or `saveWithReport` stops at its next step. Any other call runs to the end, though the byte fetches of a file opened with `openUrl` stop.
66
70
 
67
71
  ## Move bytes instead of copying them
68
72
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Links, outlines and metadata"
3
3
  description: "Add links, named destinations, an outline, page labels, viewer preferences and metadata to a document."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/guides/links-outlines-and-metadata/"
6
6
  ---
7
7
 
@@ -215,6 +215,8 @@ fn navigation(b: &mut Builder) -> Result<(), Box<dyn Error>> {
215
215
  }
216
216
  ````
217
217
 
218
+ **Result:** [links_and_outlines.pdf](https://docs.laminapdf.com/examples/pdf/links_and_outlines.pdf)
219
+
218
220
  [See the output](../../pages/recipes/links-and-outlines.md).
219
221
 
220
222
  > **Details:** The metadata goes into two places: an XMP packet, the XML that describes the file, and the Info dictionary, the older list of the same facts. Lamina PDF writes both from the same values, so they always agree.
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: "Open large files by ranges"
3
+ description: "Open a PDF on a web server without downloading all of it, fetching only the byte ranges each read needs."
4
+ version: "0.2.0-alpha.5"
5
+ url: "https://docs.laminapdf.com/guides/open-large-files/"
6
+ ---
7
+
8
+ # Open large files by ranges
9
+
10
+ Open a PDF on a web server without downloading all of it, fetching only the byte ranges each read needs.
11
+
12
+ You'll open a PDF on a web server by fetching only the parts you read. A large report opens after a few small requests, and reading its first page fetches that page and little else. You need this when files are too large to download before the first page shows.
13
+
14
+ This example opens a remote file, then reads its page count and first page:
15
+
16
+ `packages/sdk/examples/open-large-file.ts`:
17
+
18
+ ```ts
19
+ // Opens a PDF on a web server by range requests: only the parts it reads are downloaded, so a
20
+ // large file opens without fetching all of it. The server must answer `Range` requests, as most
21
+ // file hosts and CDNs do; across origins it must also allow the `Range` and `If-Range` headers.
22
+ // Needs `init()`.
23
+ import { Document, httpRangeSource } from "@laminapdf/sdk";
24
+
25
+ /** What a first look at a remote file needs: its page count and the first page's size. */
26
+ export interface FirstLook {
27
+ readonly pages: number;
28
+ /** The first page's displayed size, in points (1/72 inch). */
29
+ readonly width: number;
30
+ readonly height: number;
31
+ }
32
+
33
+ /** Opens the PDF at `url` range by range and reads its page count and first page. */
34
+ export async function firstLook(url: string): Promise<FirstLook> {
35
+ // The first request reads the end of the file, where a PDF keeps the index of its objects.
36
+ using doc = await Document.openRange(httpRangeSource(url));
37
+ // A read can need bytes that haven't arrived. `ensure` catches the `data-pending` error, fetches
38
+ // the ranges it names, and runs the read again until it has what it needs.
39
+ const pages = await doc.ensure(() => doc.pageCount);
40
+ const first = await doc.ensure(() => doc.pageInfo(0));
41
+ return { pages, width: first.width, height: first.height };
42
+ }
43
+ ```
44
+
45
+ ## Each read fetches the bytes it needs
46
+
47
+ `Document.openRange` fetches only what opening needs: the end of the file, where a PDF keeps the index of its objects, then that index and the document's catalog. Calls on the document stay synchronous, so a read that needs bytes not fetched yet can't wait for them. It throws `data-pending` instead, with the byte ranges it needs. `doc.ensure` runs your read, fetches those ranges when it throws, and runs it again until it returns:
48
+
49
+ *A read that needs missing bytes fetches them and runs again.*
50
+
51
+ `Document.openRange` opens a remote file from the few byte ranges the open needs. A later read, such as `doc.pageInfo(0)`, may need bytes that haven't arrived: it then throws `data-pending`, naming the ranges it needs. `doc.ensure` catches the error, fetches those ranges through the source (`httpRangeSource`), supplies them to the document and runs the read again. Once the bytes are there, the read returns its result.
52
+
53
+ Wrap every read after the open in `doc.ensure`, saving included: `await doc.ensure(() => doc.save())` fetches whatever the save writes. A read fails with the last `data-pending` when its fetches bring nothing new, or when it still needs bytes after 1,024 rounds of fetches. Pass `maxRounds` to `Document.openRange` to change that number.
54
+
55
+ ## The server must answer `Range` requests
56
+
57
+ `httpRangeSource(url)` asks for byte ranges with the `Range` header, and most file hosts and CDNs answer them. Across origins, the server must also allow the `Range` and `If-Range` request headers (`Access-Control-Allow-Headers: Range, If-Range`) and expose `Content-Range`, `Content-Length` and `ETag` (`Access-Control-Expose-Headers`). It must not compress the response, or the ranges it sends wouldn't match the file's bytes. An error status from the server fails with `fetch-failed`, its `data.status` holding the status code.
58
+
59
+ A server that ignores `Range` and answers `200` sends the whole file. Lamina PDF then opens that one download, as `Document.open` would.
60
+
61
+ ## A file that changes on the server fails the read
62
+
63
+ Every request after the first sends `If-Range` with the `ETag` the first response gave, when it's a strong one, or else that response's `Last-Modified` date. When the file changes between two reads, the read fails with `source-changed` rather than mixing two versions. Open the file again to read the new one.
64
+
65
+ ## Limit how much a file can make you fetch
66
+
67
+ A remote file decides how much you fetch, so cap it when the file comes from someone else. Pass `budget` to `Document.openRange`: it's the most bytes the document may hold, and past it a read fails with `source-budget-exceeded`. Pass `signal` with the same options to stop the open's fetches. Later reads take their own: `doc.ensure(read, { signal: AbortSignal.timeout(10_000) })`.
68
+
69
+ The [limits for files you don't trust](../../pages/guides/untrusted-files.md) apply here too: pass `limits` with the other options.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Open password-protected files"
3
3
  description: "Open PDFs that need a password, ask again when it is missing or wrong, and save them afterwards."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/guides/password-protected-files/"
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Save a PDF"
3
3
  description: "Save edits as a revision appended to the original bytes or as a whole new file, and see when a file refuses a save."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/guides/save-a-pdf/"
6
6
  ---
7
7
 
@@ -118,6 +118,8 @@ fn edit(bytes: Vec<u8>, edited_at: Option<PdfDate>) -> Result<Vec<u8>, Box<dyn E
118
118
  }
119
119
  ````
120
120
 
121
+ **Result:** [generate_then_edit.pdf](https://docs.laminapdf.com/examples/pdf/generate_then_edit.pdf)
122
+
121
123
  ## Choose a save mode
122
124
 
123
125
  Every save either appends a revision to the bytes you opened (an incremental save) or writes the whole file again (a [full rewrite](../../pages/reference/glossary.md#full-rewrite)). You pick one with the `mode` option:
@@ -130,6 +132,10 @@ Every save either appends a revision to the bytes you opened (an incremental sav
130
132
 
131
133
  `doc.save(options)` returns the bytes, and `doc.saveWithReport(options)` also says what it wrote. `doc.saveJob()` does the same work in steps, so a page can save a large file without freezing. The same file with the same edits always saves to the same bytes.
132
134
 
135
+ *An incremental save appends a revision; a full rewrite writes every byte again.*
136
+
137
+ An incremental save (`incremental`) keeps the original bytes and appends one revision after them. A signature over those bytes stays valid for the edits it allows. A full rewrite (`fullRewrite`) writes every byte again as one new file. Signatures over the original bytes are no longer valid, so a signed file saves this way only when you pass `invalidateSignatures: true`, which removes them.
138
+
133
139
  ## When a file refuses a save
134
140
 
135
141
  Every save makes a plan before it writes. The plan reads the file's save constraint: what its signatures, permissions, repairs and encryption allow. Then it picks a mode or refuses:
@@ -137,7 +143,11 @@ Every save makes a plan before it writes. The plan reads the file's save constra
137
143
  - **A signed file** saves only incrementally, and only with edits its signatures allow. Anything else fails with `would-invalidate-signatures`. Pass `invalidateSignatures: true` to save anyway, which removes the broken signatures from the output.
138
144
  - **A repaired file**, one Lamina PDF had to fix as it opened, can't safely take an appended revision. It needs a full rewrite, so asking for `incremental` fails with `requires-rewrite`.
139
145
  - **An edited encrypted file** saves only as a full rewrite without its encryption, and only when you pass `removeEncryption: true`. Otherwise every mode fails with `encrypted-base`.
140
- - **An unchanged document** always saves incrementally, which returns its bytes unchanged.
146
+ - **An unchanged document**, even a signed, repaired or encrypted one, saves incrementally in `auto` and `incremental` mode and returns the bytes you opened. Passing `removeEncryption: true` rewrites it instead.
147
+
148
+ *What a file allows, and the options you pass, decide its save mode or the error the save fails with.*
149
+
150
+ Every save plans before it writes, and an unchanged document comes first: in `auto` and `incremental` mode it saves incrementally and returns the bytes you opened, whatever else is true of it. A signed file saves incrementally, with the edits its signatures allow. Any other save fails with `would-invalidate-signatures` unless you pass `invalidateSignatures: true`, which saves anyway and removes the signatures that no longer hold. A repaired file needs a full rewrite: `auto` picks `fullRewrite`, and asking for `incremental` fails with `requires-rewrite`. An encrypted file you edited saves as `fullRewrite` without its encryption when you pass `removeEncryption: true`; otherwise every mode fails with `encrypted-base`. An edited file in two of these states meets both rules: a signed file that was repaired fails until you pass `invalidateSignatures: true`.
141
151
 
142
152
  > **Details:** The save constraint covers signature fields, the [DocMDP](../../pages/reference/glossary.md#docmdp) and [UR3](../../pages/reference/glossary.md#ur3) permissions, a signature's `/Lock` entry (the form fields it locks), document timestamps, and whether the file needed repair.
143
153
 
@@ -0,0 +1,324 @@
1
+ ---
2
+ title: "Test the PDFs you generate"
3
+ description: "Compare the PDFs your code makes with reference PDFs you approved, byte for byte, and draw both files' pages to see what changed."
4
+ version: "0.2.0-alpha.5"
5
+ url: "https://docs.laminapdf.com/guides/test-generated-pdfs/"
6
+ ---
7
+
8
+ # Test the PDFs you generate
9
+
10
+ Compare the PDFs your code makes with reference PDFs you approved, byte for byte, and draw both files' pages to see what changed.
11
+
12
+ You'll write a test that fails when a PDF your code makes changes. The test compares the PDF's bytes with a reference PDF you opened and approved. When the bytes differ, a second example draws both files' pages to show where they look different.
13
+
14
+ ## Why the bytes repeat
15
+
16
+ Lamina PDF never reads a clock or picks a random value while it writes. With the same code, data, options and Lamina PDF version, Rust and every JavaScript runtime write the same bytes. Three values usually come from your app rather than from the content (the file ID, the producer and the creation date), so a test fixes them.
17
+
18
+ Your own code can vary the bytes too, and a failing test should point there first:
19
+
20
+ - a date taken from the clock, such as an invoice date or `new Date()` in a footer;
21
+ - records read in a different order, such as rows from a database without `ORDER BY`;
22
+ - a different font file: an update to the font changes the embedded subset.
23
+
24
+ ## Compare with a reference PDF
25
+
26
+ The example below holds the options a test passes and a function that compares a PDF with its reference. The function answers `same`, `created` or `changed`, and your test runner checks the answer, so it works in Vitest, Jest, `node:test` or `cargo test`:
27
+
28
+ **TypeScript**
29
+
30
+ `packages/sdk/examples/snapshot-test.ts`:
31
+
32
+ ```ts
33
+ // Checks that a PDF your code makes is byte for byte the reference PDF you approved.
34
+ //
35
+ // Build the document with `TEST_OPTIONS`, then let your test runner check the answer:
36
+ //
37
+ // expect(compareWithReference(invoice(order, TEST_OPTIONS), "references/invoice.pdf")).toBe("same");
38
+ //
39
+ // The first run fails with `created` and writes the reference: open it, and commit it once it
40
+ // looks right. When the bytes change later, the new PDF is written beside the reference for
41
+ // review; pass `{ update: true }` to accept it.
42
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
43
+ import { basename, dirname, extname, join } from "node:path";
44
+ import type { BuilderOptions } from "@laminapdf/sdk";
45
+
46
+ /**
47
+ * What a test passes in place of the clock and a random file ID. With these fixed, the same code
48
+ * and data give the same bytes on every run, machine and runtime.
49
+ */
50
+ export const TEST_OPTIONS: BuilderOptions = {
51
+ // The file ID: any 32 hex digits, as long as they stay the same.
52
+ id: "00112233445566778899aabbccddeeff",
53
+ // The Info dictionary's /Producer: your app's name, without a version that changes.
54
+ producer: "my-app tests",
55
+ // The Info dictionary's /CreationDate, in PDF date form.
56
+ creationDate: "D:20260101000000Z",
57
+ };
58
+
59
+ /** `same`: the bytes match. `created`: there was no reference, so one was written. `changed`: they differ. */
60
+ export type Outcome = "same" | "created" | "changed";
61
+
62
+ export interface CompareOptions {
63
+ /** Replace the reference with the new bytes instead of writing them beside it. */
64
+ readonly update?: boolean;
65
+ }
66
+
67
+ /**
68
+ * Compares `bytes` with the reference PDF at `reference`. A changed PDF is written to
69
+ * `<name>.new.pdf` next to the reference, or replaces it when `update` is set.
70
+ */
71
+ export function compareWithReference(bytes: Uint8Array, reference: string, options: CompareOptions = {}): Outcome {
72
+ const pending = join(dirname(reference), `${basename(reference, extname(reference))}.new.pdf`);
73
+ if (!existsSync(reference)) {
74
+ mkdirSync(dirname(reference), { recursive: true });
75
+ writeFileSync(reference, bytes);
76
+ return "created";
77
+ }
78
+ if (sameBytes(readFileSync(reference), bytes)) {
79
+ rmSync(pending, { force: true });
80
+ return "same";
81
+ }
82
+ if (options.update) {
83
+ writeFileSync(reference, bytes);
84
+ rmSync(pending, { force: true });
85
+ } else {
86
+ writeFileSync(pending, bytes);
87
+ }
88
+ return "changed";
89
+ }
90
+
91
+ function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
92
+ return a.length === b.length && a.every((byte, i) => byte === b[i]);
93
+ }
94
+ ```
95
+
96
+ **Rust**
97
+
98
+ `crates/laminapdf/examples/snapshot_test.rs`:
99
+
100
+ ````rust
101
+ //! Checks that a PDF your code makes is byte for byte the reference PDF you approved.
102
+ //!
103
+ //! Build the document with `test_options()`, then assert on the answer in a test:
104
+ //!
105
+ //! ```ignore
106
+ //! let bytes = invoice(&order, test_options()?)?;
107
+ //! assert_eq!(compare_with_reference(&bytes, Path::new("references/invoice.pdf"), false)?, Outcome::Same);
108
+ //! ```
109
+ //!
110
+ //! The first run fails with `Created` and writes the reference: open it, and commit it once it
111
+ //! looks right. When the bytes change later, the new PDF is written beside the reference for
112
+ //! review; pass `update: true` to accept it. Run on its own, the example compares two files:
113
+ //!
114
+ //! ```sh
115
+ //! cargo run -p laminapdf --example snapshot_test -- new.pdf reference.pdf
116
+ //! ```
117
+
118
+ use std::error::Error;
119
+ use std::path::{Path, PathBuf};
120
+
121
+ use laminapdf::BuildOptions;
122
+
123
+ fn main() -> Result<(), Box<dyn Error>> {
124
+ let mut args = std::env::args().skip(1);
125
+ let (Some(new), Some(reference)) = (args.next(), args.next()) else {
126
+ return Err("usage: snapshot_test <new.pdf> <reference.pdf>".into());
127
+ };
128
+ let outcome = compare_with_reference(&std::fs::read(new)?, Path::new(&reference), false)?;
129
+ println!("{outcome:?}");
130
+ Ok(())
131
+ }
132
+
133
+ /// What a test passes in place of the clock and a random file ID. With these fixed, the same code
134
+ /// and data give the same bytes on every run, machine and runtime.
135
+ #[cfg_attr(
136
+ not(test),
137
+ expect(dead_code, reason = "your tests call it; `main` only compares two files")
138
+ )]
139
+ pub(crate) fn test_options() -> Result<BuildOptions, Box<dyn Error>> {
140
+ /// The file ID: any 16 bytes, as long as they stay the same.
141
+ const FILE_ID: [u8; 16] = [
142
+ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
143
+ ];
144
+ /// The Info dictionary's `/Producer`: your app's name, without a version that changes.
145
+ const PRODUCER: &str = "my-app tests";
146
+ /// The Info dictionary's `/CreationDate`, in PDF date form.
147
+ const CREATED: &str = "D:20260101000000Z";
148
+ Ok(BuildOptions::new()
149
+ .with_id(FILE_ID)
150
+ .with_producer(PRODUCER)
151
+ .with_creation_date(CREATED.parse()?))
152
+ }
153
+
154
+ /// `Same`: the bytes match. `Created`: there was no reference, so one was written. `Changed`:
155
+ /// they differ.
156
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
157
+ pub(crate) enum Outcome {
158
+ Same,
159
+ Created,
160
+ Changed,
161
+ }
162
+
163
+ /// Compares `bytes` with the reference PDF at `reference`. A changed PDF is written to
164
+ /// `<name>.new.pdf` next to the reference, or replaces it when `update` is set.
165
+ pub(crate) fn compare_with_reference(bytes: &[u8], reference: &Path, update: bool) -> std::io::Result<Outcome> {
166
+ let pending = pending_path(reference);
167
+ if !reference.exists() {
168
+ if let Some(folder) = reference.parent() {
169
+ std::fs::create_dir_all(folder)?;
170
+ }
171
+ std::fs::write(reference, bytes)?;
172
+ return Ok(Outcome::Created);
173
+ }
174
+ if std::fs::read(reference)? == bytes {
175
+ remove_if_present(&pending)?;
176
+ return Ok(Outcome::Same);
177
+ }
178
+ if update {
179
+ std::fs::write(reference, bytes)?;
180
+ remove_if_present(&pending)?;
181
+ } else {
182
+ std::fs::write(&pending, bytes)?;
183
+ }
184
+ Ok(Outcome::Changed)
185
+ }
186
+
187
+ /// `invoice.pdf` → `invoice.new.pdf`, in the same folder.
188
+ fn pending_path(reference: &Path) -> PathBuf {
189
+ let stem = reference.file_stem().unwrap_or_default().to_string_lossy();
190
+ reference.with_file_name(format!("{stem}.new.pdf"))
191
+ }
192
+
193
+ fn remove_if_present(path: &Path) -> std::io::Result<()> {
194
+ match std::fs::remove_file(path) {
195
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
196
+ result => result,
197
+ }
198
+ }
199
+ ````
200
+
201
+ On the first run there is no reference yet, so the function writes one, answers `created`, and the test fails. Open that PDF before you commit it: the test only proves that later runs match it, so it has to be right. After that, every run answers `same` until the output changes. A reference you forget to commit fails the test on every other machine too, since a fresh checkout has none.
202
+
203
+ When the bytes change, the reference stays as it was and the new PDF goes beside it as `<name>.new.pdf`. Open both, and if the change is the one you meant, run the tests again with `update` set, for example from an environment variable such as `UPDATE_REFERENCES=1`. Commit the new reference with the change that caused it.
204
+
205
+ ## See where a changed PDF looks different
206
+
207
+ Different bytes don't always mean different pages: a reordered dictionary or a new producer changes the file but not how it looks. This example draws each page of both files and counts the pixels that differ:
208
+
209
+ `packages/sdk/examples/compare-pages.ts`:
210
+
211
+ ```ts
212
+ // Draws each page of two PDFs and counts the pixels that differ, to show where a changed PDF looks different.
213
+ //
214
+ // Both files are drawn by the same renderer, so its own quirks cancel out. In Node, load it with
215
+ // the module's bytes:
216
+ //
217
+ // const wasm = fileURLToPath(import.meta.resolve("@laminapdf/wasm/lamina_pdf_render_hayro.wasm"));
218
+ // const renderer = await loadRenderer(readFileSync(wasm));
219
+ import {
220
+ bytesSource,
221
+ DEFAULT_LIMITS,
222
+ type RenderBackend,
223
+ type RenderDocument,
224
+ type RenderedTile,
225
+ } from "@laminapdf/render";
226
+ import { initHayro } from "@laminapdf/render-hayro";
227
+
228
+ /** Pixels per point: 2 draws at 144 dpi, enough to see a glyph that moved. Pages too large for the renderer's limits at this scale are drawn smaller. */
229
+ const SCALE = 2;
230
+ /** How far a colour channel (0–255) may move before the pixel counts as changed: absorbs smoothing noise. */
231
+ const TOLERANCE = 8;
232
+
233
+ /** A rectangle in page points, from the page's top-left corner. */
234
+ export interface Box {
235
+ readonly x: number;
236
+ readonly y: number;
237
+ readonly width: number;
238
+ readonly height: number;
239
+ }
240
+
241
+ /** One page: how many pixels differ, and the box around them (`null` when none do). */
242
+ export interface PageDifference {
243
+ readonly page: number;
244
+ readonly pixels: number;
245
+ readonly box: Box | null;
246
+ }
247
+
248
+ export interface Comparison {
249
+ /** The page counts of the first and second file; only the pages both have are compared. */
250
+ readonly pageCounts: readonly [number, number];
251
+ readonly pages: readonly PageDifference[];
252
+ }
253
+
254
+ /** Loads the renderer from the module's bytes, a URL, or (in a browser) the file next to its code. */
255
+ export function loadRenderer(wasm?: Uint8Array | URL): Promise<RenderBackend> {
256
+ return initHayro(wasm);
257
+ }
258
+
259
+ /** Compares every page the two PDFs share. */
260
+ export function comparePages(renderer: RenderBackend, before: Uint8Array, after: Uint8Array): Comparison {
261
+ using a = renderer.open(bytesSource(before));
262
+ using b = renderer.open(bytesSource(after));
263
+ const shared = Math.min(a.pageCount, b.pageCount);
264
+ const pages = Array.from({ length: shared }, (_, page) => comparePage(a, b, page));
265
+ return { pageCounts: [a.pageCount, b.pageCount], pages };
266
+ }
267
+
268
+ function comparePage(a: RenderDocument, b: RenderDocument, page: number): PageDifference {
269
+ const scale = Math.min(fittingScale(a, page), fittingScale(b, page));
270
+ const first = a.render({ page, scale });
271
+ const second = b.render({ page, scale });
272
+ const width = Math.max(first.width, second.width);
273
+ const height = Math.max(first.height, second.height);
274
+ let pixels = 0;
275
+ let [left, top, right, bottom] = [width, height, -1, -1];
276
+ for (let y = 0; y < height; y++) {
277
+ for (let x = 0; x < width; x++) {
278
+ if (!differs(first, second, x, y)) continue;
279
+ pixels++;
280
+ [left, top] = [Math.min(left, x), Math.min(top, y)];
281
+ [right, bottom] = [Math.max(right, x), Math.max(bottom, y)];
282
+ }
283
+ }
284
+ const box =
285
+ pixels === 0
286
+ ? null
287
+ : { x: left / scale, y: top / scale, width: (right - left + 1) / scale, height: (bottom - top + 1) / scale };
288
+ return { page, pixels, box };
289
+ }
290
+
291
+ /** `SCALE`, or less when the page would pass the renderer's pixel or side limit at it. */
292
+ function fittingScale(doc: RenderDocument, page: number): number {
293
+ const { width, height } = doc.page(page);
294
+ const byArea = Math.sqrt(DEFAULT_LIMITS.maxRenderPixels / (width * height));
295
+ const bySide = DEFAULT_LIMITS.maxRenderSide / Math.max(width, height);
296
+ return Math.min(SCALE, byArea, bySide);
297
+ }
298
+
299
+ /** Whether the pixel at (x, y) differs; a pixel only one page has always does. */
300
+ function differs(first: RenderedTile, second: RenderedTile, x: number, y: number): boolean {
301
+ if (x >= first.width || y >= first.height || x >= second.width || y >= second.height) return true;
302
+ const i = (y * first.width + x) * 4;
303
+ const j = (y * second.width + x) * 4;
304
+ for (let channel = 0; channel < 4; channel++) {
305
+ const delta = Math.abs((first.pixels[i + channel] ?? 0) - (second.pixels[j + channel] ?? 0));
306
+ if (delta > TOLERANCE) return true;
307
+ }
308
+ return false;
309
+ }
310
+ ```
311
+
312
+ Each page gets one of two outcomes. With no differing pixels, the file's structure changed but its pages look the same. With some, the box shows where on the page, in points from the top-left corner. The same renderer draws both files, so its own small differences from other viewers cancel out.
313
+
314
+ To read the change itself, build with `compress: false` (`with_compress(false)` in Rust). The content streams then stay readable text, and a diff tool shows what moved. In Rust, the `laminapdf-render` crate's `hayro` feature draws pages the same way.
315
+
316
+ ## Upgrade Lamina PDF
317
+
318
+ Pin the exact Lamina PDF version, so an upgrade happens only when you choose it. A release that changes the bytes written for the same input lists that change first in its changelog. Upgrade in a change of its own, regenerate the references with `update` set, and look at the page comparison before you commit them.
319
+
320
+ ## Keep fonts and edits repeatable
321
+
322
+ Fonts need the same care. The embedded subset depends on the font file's exact bytes, so keep the font your test uses in your own repository rather than loading it from the system. Edited files repeat too: opening a PDF, applying the same edits and saving writes the same bytes each time.
323
+
324
+ The [stability policy](../../pages/reference/stability.md) states the promise these tests rely on: the same input gives the same bytes everywhere.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Open files you don't trust"
3
3
  description: "Cap how much work an uploaded PDF can make Lamina PDF do, and refuse damaged files with strict mode."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/guides/untrusted-files/"
6
6
  ---
7
7
 
@@ -77,4 +77,6 @@ These are the TypeScript names. In Rust they're builder methods, such as `Limits
77
77
 
78
78
  Lamina PDF repairs damaged files as it opens them and reports each repair as a warning. Pass `strict: true` to refuse any file that needs repair instead. It then fails with `strict`.
79
79
 
80
+ A file on someone else's server can also decide how much you download. [Open it by ranges](../../pages/guides/open-large-files.md) and cap the bytes it may fetch with `budget`.
81
+
80
82
  Next: [keep the page responsive](../../pages/guides/keep-the-page-responsive.md).
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Lamina PDF"
3
3
  description: "Create, read, edit and save PDFs from Rust and TypeScript, with one Rust core that runs natively and as WebAssembly."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/"
6
6
  ---
7
7
 
@@ -14,7 +14,7 @@ Lamina PDF creates, reads, edits and saves PDFs. One Rust core does the work, an
14
14
  - **[`laminapdf`](https://crates.io/crates/laminapdf)** on crates.io is the Rust crate. Its [API reference](../pages/reference/rust.md) is on docs.rs.
15
15
  - **[`@laminapdf/sdk`](https://www.npmjs.com/package/@laminapdf/sdk)** on npm is the TypeScript API over the WebAssembly build. After one `await init()`, every call is synchronous, and the API is the same in every runtime. Its [API reference](../api/README.md) is on this site.
16
16
 
17
- > **Alpha releases:** Both packages are alpha releases, so name the version when you install them: `npm install @laminapdf/sdk@0.2.0-alpha.3` and `cargo add laminapdf@=0.2.0-alpha.3`. [Install](../pages/install.md) lists every published package.
17
+ > **Alpha releases:** Both packages are alpha releases, so name the version when you install them: `npm install @laminapdf/sdk@0.2.0-alpha.5` and `cargo add laminapdf@=0.2.0-alpha.5`. [Install](../pages/install.md) lists every published package.
18
18
 
19
19
  ## What works today
20
20
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Install"
3
3
  description: "Install Lamina PDF from npm or crates.io, and find every published package."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/install/"
6
6
  ---
7
7
 
@@ -16,10 +16,10 @@ Lamina PDF is published to npm as [`@laminapdf/sdk`](https://www.npmjs.com/packa
16
16
  Install the version these docs describe:
17
17
 
18
18
  ```sh
19
- npm install @laminapdf/sdk@0.2.0-alpha.3
19
+ npm install @laminapdf/sdk@0.2.0-alpha.5
20
20
  ```
21
21
 
22
- `npm install @laminapdf/sdk@alpha` installs the newest alpha, whichever version that is. pnpm, Yarn and Bun take the same name (`pnpm add @laminapdf/sdk@0.2.0-alpha.3`), and Deno takes `npm:@laminapdf/sdk@0.2.0-alpha.3`. The package brings [`@laminapdf/wasm`](https://www.npmjs.com/package/@laminapdf/wasm), its WebAssembly modules, as a dependency.
22
+ `npm install @laminapdf/sdk@alpha` installs the newest alpha, whichever version that is. pnpm, Yarn and Bun take the same name (`pnpm add @laminapdf/sdk@0.2.0-alpha.5`), and Deno takes `npm:@laminapdf/sdk@0.2.0-alpha.5`. The package brings [`@laminapdf/wasm`](https://www.npmjs.com/package/@laminapdf/wasm), its WebAssembly modules, as a dependency.
23
23
 
24
24
  The package is ESM only. It has one entry per runtime (the `workerd`, `deno`, `browser`, `node` and `default` export conditions), and each entry loads the `.wasm` its own way. The [quick starts](../pages/quick-start/browser.md) show each one.
25
25
 
@@ -28,10 +28,10 @@ The package is ESM only. It has one entry per runtime (the `workerd`, `deno`, `b
28
28
  Add the crate at the version these docs describe:
29
29
 
30
30
  ```sh
31
- cargo add laminapdf@=0.2.0-alpha.3
31
+ cargo add laminapdf@=0.2.0-alpha.5
32
32
  ```
33
33
 
34
- The command writes `laminapdf = "=0.2.0-alpha.3"` to `Cargo.toml`, which keeps your project on this release until you change it: alphas promise nothing to each other (see the [stability policy](../pages/reference/stability.md)). The default features are `text` (fonts and text, which includes the builder and text extraction) and `saslprep` (passwords for AES-256 files). For reading and editing only, add `--no-default-features`.
34
+ The command writes `laminapdf = "=0.2.0-alpha.5"` to `Cargo.toml`, which keeps your project on this release until you change it: alphas promise nothing to each other (see the [stability policy](../pages/reference/stability.md)). The default features are `text` (fonts and text, which includes the builder and text extraction) and `saslprep` (passwords for AES-256 files). For reading and editing only, add `--no-default-features`.
35
35
 
36
36
  ## Every published package
37
37
 
@@ -58,6 +58,10 @@ Most projects need only the two above. The table links every published package t
58
58
  | `laminapdf-render` | [crates.io](https://crates.io/crates/laminapdf-render) | The Rust interface that page renderers implement |
59
59
  | `laminapdf-render-pdfium` | [crates.io](https://crates.io/crates/laminapdf-render-pdfium) | The native PDFium renderer |
60
60
 
61
+ *Each JavaScript package and what it depends on, beside the Rust API.*
62
+
63
+ In JavaScript, `@laminapdf/sdk` is the TypeScript API, and it depends on `@laminapdf/wasm`, whose WebAssembly the SDK loads as three modules: base, extract and text. `@laminapdf/viewer`, a web PDF viewer, depends on `@laminapdf/sdk`, on `@laminapdf/render`, the interface that renderers implement, and on both renderers. Those renderers are `@laminapdf/render-pdfium`, the default, and `@laminapdf/render-hayro`, which also uses `@laminapdf/wasm`. In Rust, the API is one crate, `laminapdf`, with features instead of modules.
64
+
61
65
  ## Try a change before it's released
62
66
 
63
67
  An unreleased change is only in the repository, so build the packages from a checkout. You need Rust (the toolchain in `rust-toolchain.toml` installs itself), Node 22 or newer and pnpm:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Browser and bundlers"
3
3
  description: "Load @laminapdf/sdk in a web page through Vite or another bundler."
4
- version: "0.2.0-alpha.3"
4
+ version: "0.2.0-alpha.5"
5
5
  url: "https://docs.laminapdf.com/quick-start/browser/"
6
6
  ---
7
7
 
@@ -13,6 +13,8 @@ In a browser, `@laminapdf/sdk` finds its `.wasm` files with `new URL("…", impo
13
13
 
14
14
  This app builds one A4 page that says "Hello, world!" and offers it to your user as `hello.pdf`:
15
15
 
16
+ **TypeScript**
17
+
16
18
  `packages/sdk/examples/quick-start/browser.ts`:
17
19
 
18
20
  ```ts
@@ -56,6 +58,8 @@ export async function hello(
56
58
  }
57
59
  ```
58
60
 
61
+ **Result:** [hello.pdf](https://docs.laminapdf.com/examples/pdf/hello.pdf)
62
+
59
63
  Import `hello` from your page's script and call it when your user asks for the file, for example from a button: `button.addEventListener("click", () => hello())`. Then start the page with your bundler, such as `npx vite`.
60
64
 
61
65
  Lamina PDF's tests bundle this app with Vite, run it in Chromium, Firefox and WebKit, and check that it builds the reference PDF byte for byte.