@laminapdf/sdk 0.2.0-alpha.2 → 0.2.0-alpha.4
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.
- package/README.md +63 -8
- package/dist/api.d.ts +8 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -2
- package/dist/api.js.map +1 -1
- package/dist/builder.d.ts +43 -22
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +122 -83
- package/dist/builder.js.map +1 -1
- package/dist/document.d.ts +89 -22
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +177 -67
- package/dist/document.js.map +1 -1
- package/dist/errors.d.ts +5 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -16
- package/dist/errors.js.map +1 -1
- package/dist/extract-job.d.ts +100 -0
- package/dist/extract-job.d.ts.map +1 -0
- package/dist/extract-job.js +177 -0
- package/dist/extract-job.js.map +1 -0
- package/dist/extract.d.ts +57 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +5 -0
- package/dist/extract.js.map +1 -0
- package/dist/font.d.ts +13 -6
- package/dist/font.d.ts.map +1 -1
- package/dist/font.js +50 -20
- package/dist/font.js.map +1 -1
- package/dist/generated/builder-enums.d.ts +3 -3
- package/dist/generated/builder-enums.d.ts.map +1 -1
- package/dist/generated/builder-enums.js +10 -0
- package/dist/generated/builder-enums.js.map +1 -1
- package/dist/generated/codes.d.ts +18 -6
- package/dist/generated/codes.d.ts.map +1 -1
- package/dist/generated/codes.js +114 -5
- package/dist/generated/codes.js.map +1 -1
- package/dist/generated/explain.d.ts.map +1 -1
- package/dist/generated/explain.js +45 -11
- package/dist/generated/explain.js.map +1 -1
- package/dist/generated/extract.d.ts +282 -0
- package/dist/generated/extract.d.ts.map +1 -0
- package/dist/generated/extract.js +4 -0
- package/dist/generated/extract.js.map +1 -0
- package/dist/init.d.ts +13 -5
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +23 -11
- package/dist/init.js.map +1 -1
- package/dist/internal/handle.d.ts +58 -0
- package/dist/internal/handle.d.ts.map +1 -0
- package/dist/internal/handle.js +113 -0
- package/dist/internal/handle.js.map +1 -0
- package/dist/internal/instance.d.ts +56 -0
- package/dist/internal/instance.d.ts.map +1 -0
- package/dist/internal/instance.js +125 -0
- package/dist/internal/instance.js.map +1 -0
- package/dist/source/http.d.ts +1 -1
- package/dist/source/http.js +2 -2
- package/dist/source/http.js.map +1 -1
- package/dist/source/range.d.ts +6 -1
- package/dist/source/range.d.ts.map +1 -1
- package/dist/source/range.js +10 -6
- package/dist/source/range.js.map +1 -1
- package/dist/text.d.ts +3 -3
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +3 -3
- package/dist/text.js.map +1 -1
- package/dist/types.d.ts +19 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/dist/worker/client.d.ts +3 -2
- package/dist/worker/client.d.ts.map +1 -1
- package/dist/worker/client.js +6 -4
- package/dist/worker/client.js.map +1 -1
- package/dist/worker/entry.js +1 -1
- package/dist/worker/entry.js.map +1 -1
- package/dist/worker/server.d.ts.map +1 -1
- package/dist/worker/server.js +42 -9
- package/dist/worker/server.js.map +1 -1
- package/dist/worker/support.d.ts +13 -3
- package/dist/worker/support.d.ts.map +1 -1
- package/dist/worker/support.js +5 -0
- package/dist/worker/support.js.map +1 -1
- package/dist/worker/types.d.ts +8 -1
- package/dist/worker/types.d.ts.map +1 -1
- package/docs/AGENTS.md +14 -0
- package/docs/api/README.md +166 -0
- package/docs/api/classes/Builder.md +467 -0
- package/docs/api/classes/Document.md +854 -0
- package/docs/api/classes/ExtractJob.md +236 -0
- package/docs/api/classes/Font.md +74 -0
- package/docs/api/classes/Image.md +140 -0
- package/docs/api/classes/LaminaPdfError.md +189 -0
- package/docs/api/classes/Page.md +178 -0
- package/docs/api/classes/PageRef.md +253 -0
- package/docs/api/classes/PageText.md +129 -0
- package/docs/api/classes/PageTextRun.md +167 -0
- package/docs/api/classes/PendingPath.md +97 -0
- package/docs/api/classes/SaveJob.md +205 -0
- package/docs/api/classes/Surface.md +331 -0
- package/docs/api/classes/Transaction.md +111 -0
- package/docs/api/functions/callbackSource.md +33 -0
- package/docs/api/functions/extractWithModels.md +70 -0
- package/docs/api/functions/httpRangeSource.md +48 -0
- package/docs/api/functions/init.md +36 -0
- package/docs/api/functions/isExtractLoaded.md +21 -0
- package/docs/api/functions/isInitialized.md +19 -0
- package/docs/api/functions/isLaminaPdfError.md +41 -0
- package/docs/api/functions/isTextLoaded.md +19 -0
- package/docs/api/functions/loadExtract.md +28 -0
- package/docs/api/functions/loadText.md +28 -0
- package/docs/api/functions/onLeak.md +29 -0
- package/docs/api/functions/reset.md +22 -0
- package/docs/api/functions/wasmMemory.md +22 -0
- package/docs/api/interfaces/BuilderOptions.md +108 -0
- package/docs/api/interfaces/ByteRange.md +29 -0
- package/docs/api/interfaces/ChunkOptions.md +37 -0
- package/docs/api/interfaces/Diagnostic.md +67 -0
- package/docs/api/interfaces/ExtractOptions.md +92 -0
- package/docs/api/interfaces/ExtractResult.md +37 -0
- package/docs/api/interfaces/FontInfo.md +37 -0
- package/docs/api/interfaces/HostModels.md +57 -0
- package/docs/api/interfaces/HttpRangeOptions.md +78 -0
- package/docs/api/interfaces/InitOptions.md +40 -0
- package/docs/api/interfaces/LayoutRegion.md +38 -0
- package/docs/api/interfaces/Leak.md +21 -0
- package/docs/api/interfaces/Location.md +53 -0
- package/docs/api/interfaces/ModelInfo.md +45 -0
- package/docs/api/interfaces/ModelRequest.md +45 -0
- package/docs/api/interfaces/Models.md +45 -0
- package/docs/api/interfaces/OcrWord.md +37 -0
- package/docs/api/interfaces/OpLog.md +25 -0
- package/docs/api/interfaces/OpenOptions.md +37 -0
- package/docs/api/interfaces/PageTextOptions.md +21 -0
- package/docs/api/interfaces/PathBuilder.md +195 -0
- package/docs/api/interfaces/QuadPoint.md +25 -0
- package/docs/api/interfaces/RangeOptions.md +54 -0
- package/docs/api/interfaces/RangeSource.md +59 -0
- package/docs/api/interfaces/StrokeStyle.md +61 -0
- package/docs/api/interfaces/TagSpec.md +69 -0
- package/docs/api/interfaces/TextBoxOptions.md +93 -0
- package/docs/api/interfaces/TextGlyph.md +29 -0
- package/docs/api/interfaces/TextOptions.md +101 -0
- package/docs/api/interfaces/WasmMemory.md +37 -0
- package/docs/api/type-aliases/AfRelationship.md +15 -0
- package/docs/api/type-aliases/Align.md +15 -0
- package/docs/api/type-aliases/ArtifactKind.md +15 -0
- package/docs/api/type-aliases/AttachmentOptions.md +15 -0
- package/docs/api/type-aliases/BlendMode.md +15 -0
- package/docs/api/type-aliases/BoxRect.md +15 -0
- package/docs/api/type-aliases/BuildOptions.md +15 -0
- package/docs/api/type-aliases/Cell.md +71 -0
- package/docs/api/type-aliases/Chunk.md +79 -0
- package/docs/api/type-aliases/Color.md +16 -0
- package/docs/api/type-aliases/Coordinates.md +55 -0
- package/docs/api/type-aliases/Direction.md +15 -0
- package/docs/api/type-aliases/DocumentInfo.md +15 -0
- package/docs/api/type-aliases/DropReason.md +15 -0
- package/docs/api/type-aliases/Dropped.md +59 -0
- package/docs/api/type-aliases/Edited.md +15 -0
- package/docs/api/type-aliases/ErrorCode.md +15 -0
- package/docs/api/type-aliases/ErrorKind.md +15 -0
- package/docs/api/type-aliases/ExtractFormat.md +15 -0
- package/docs/api/type-aliases/ExtractJobOptions.md +31 -0
- package/docs/api/type-aliases/ExtractStep.md +61 -0
- package/docs/api/type-aliases/ExtractedBlock.md +94 -0
- package/docs/api/type-aliases/ExtractedPage.md +73 -0
- package/docs/api/type-aliases/ExtractedSource.md +48 -0
- package/docs/api/type-aliases/Extraction.md +22 -0
- package/docs/api/type-aliases/FillRule.md +15 -0
- package/docs/api/type-aliases/Fit.md +15 -0
- package/docs/api/type-aliases/Flags.md +15 -0
- package/docs/api/type-aliases/Generator.md +43 -0
- package/docs/api/type-aliases/GlyphQuad.md +15 -0
- package/docs/api/type-aliases/InfoPatch.md +15 -0
- package/docs/api/type-aliases/LaminaPdfWarning.md +16 -0
- package/docs/api/type-aliases/LimitName.md +15 -0
- package/docs/api/type-aliases/Limits.md +15 -0
- package/docs/api/type-aliases/LineBox.md +15 -0
- package/docs/api/type-aliases/LineCap.md +15 -0
- package/docs/api/type-aliases/LineJoin.md +15 -0
- package/docs/api/type-aliases/LinkTarget.md +15 -0
- package/docs/api/type-aliases/Metadata.md +15 -0
- package/docs/api/type-aliases/ModelResult.md +15 -0
- package/docs/api/type-aliases/Op.md +15 -0
- package/docs/api/type-aliases/OutlineEntry.md +15 -0
- package/docs/api/type-aliases/OutlineNode.md +15 -0
- package/docs/api/type-aliases/Overflow.md +15 -0
- package/docs/api/type-aliases/PageInfo.md +15 -0
- package/docs/api/type-aliases/PageLabel.md +15 -0
- package/docs/api/type-aliases/PageSizeName.md +15 -0
- package/docs/api/type-aliases/Point.md +15 -0
- package/docs/api/type-aliases/Profile.md +15 -0
- package/docs/api/type-aliases/Progress.md +15 -0
- package/docs/api/type-aliases/ReadingOrder.md +15 -0
- package/docs/api/type-aliases/Rect.md +15 -0
- package/docs/api/type-aliases/Region.md +31 -0
- package/docs/api/type-aliases/RegionLabel.md +15 -0
- package/docs/api/type-aliases/RenderMode.md +15 -0
- package/docs/api/type-aliases/Rotation.md +15 -0
- package/docs/api/type-aliases/SaveOptions.md +15 -0
- package/docs/api/type-aliases/SaveReport.md +15 -0
- package/docs/api/type-aliases/Span.md +74 -0
- package/docs/api/type-aliases/SplitOn.md +15 -0
- package/docs/api/type-aliases/Standard14.md +15 -0
- package/docs/api/type-aliases/StructRef.md +41 -0
- package/docs/api/type-aliases/StructSource.md +17 -0
- package/docs/api/type-aliases/StructureKid.md +15 -0
- package/docs/api/type-aliases/StructureNode.md +16 -0
- package/docs/api/type-aliases/TableDetector.md +15 -0
- package/docs/api/type-aliases/TableMode.md +15 -0
- package/docs/api/type-aliases/TableScope.md +15 -0
- package/docs/api/type-aliases/Tag.md +15 -0
- package/docs/api/type-aliases/TagName.md +15 -0
- package/docs/api/type-aliases/Target.md +15 -0
- package/docs/api/type-aliases/TargetView.md +18 -0
- package/docs/api/type-aliases/TextLayer.md +72 -0
- package/docs/api/type-aliases/TextLayerStatus.md +15 -0
- package/docs/api/type-aliases/TextLayout.md +15 -0
- package/docs/api/type-aliases/TextRange.md +33 -0
- package/docs/api/type-aliases/TextRunInfo.md +15 -0
- package/docs/api/type-aliases/Transform.md +15 -0
- package/docs/api/type-aliases/UndoOutcome.md +15 -0
- package/docs/api/type-aliases/ViewerPreferences.md +15 -0
- package/docs/api/type-aliases/WarningCode.md +15 -0
- package/docs/api/type-aliases/WasmSource.md +16 -0
- package/docs/api/variables/Matrix.md +99 -0
- package/docs/api/variables/PAGE_SIZES.md +15 -0
- package/docs/api/variables/TAG_NAMES.md +15 -0
- package/docs/errors/README.md +173 -0
- package/docs/errors/aes-padding-invalid.md +34 -0
- package/docs/errors/already-initialized.md +34 -0
- package/docs/errors/binary-output-to-terminal.md +35 -0
- package/docs/errors/builder-finished.md +35 -0
- package/docs/errors/cancelled.md +35 -0
- package/docs/errors/cmap-invalid.md +42 -0
- package/docs/errors/collection-index.md +33 -0
- package/docs/errors/color-components.md +33 -0
- package/docs/errors/content-bad-operands.md +36 -0
- package/docs/errors/content-path-unpainted.md +36 -0
- package/docs/errors/content-resource-missing.md +37 -0
- package/docs/errors/content-syntax.md +36 -0
- package/docs/errors/content-unbalanced-bt.md +36 -0
- package/docs/errors/content-unbalanced-marked.md +36 -0
- package/docs/errors/content-unbalanced-q.md +36 -0
- package/docs/errors/content-unknown-operator.md +37 -0
- package/docs/errors/covered-text.md +33 -0
- package/docs/errors/data-pending.md +34 -0
- package/docs/errors/decompress.md +34 -0
- package/docs/errors/device-color-without-output-intent.md +33 -0
- package/docs/errors/direct-page.md +34 -0
- package/docs/errors/disposed.md +34 -0
- package/docs/errors/document-changed.md +34 -0
- package/docs/errors/embedded-file-forbidden.md +35 -0
- package/docs/errors/embedded-file-without-mime-type.md +36 -0
- package/docs/errors/encoding-full.md +34 -0
- package/docs/errors/encrypted-base.md +33 -0
- package/docs/errors/extract-column-order-uncertain.md +38 -0
- package/docs/errors/extract-limit-blocks.md +35 -0
- package/docs/errors/extract-module-not-loaded.md +37 -0
- package/docs/errors/extract-needs-ocr.md +37 -0
- package/docs/errors/extract-reading-order-ambiguous.md +32 -0
- package/docs/errors/extract-struct-tree-ignored.md +32 -0
- package/docs/errors/extract-table-ambiguous.md +32 -0
- package/docs/errors/extract-table-limit.md +35 -0
- package/docs/errors/extract-untagged-content.md +32 -0
- package/docs/errors/fetch-failed.md +34 -0
- package/docs/errors/flate-truncated.md +34 -0
- package/docs/errors/flow-content-too-large.md +34 -0
- package/docs/errors/flow-keep-broken.md +34 -0
- package/docs/errors/flow-layout-unstable.md +38 -0
- package/docs/errors/font-invalid.md +36 -0
- package/docs/errors/font-mismatch.md +34 -0
- package/docs/errors/font-not-embeddable.md +34 -0
- package/docs/errors/font-not-embedded.md +35 -0
- package/docs/errors/font-not-shapable.md +33 -0
- package/docs/errors/glyph-out-of-range.md +34 -0
- package/docs/errors/glyph-without-unicode.md +37 -0
- package/docs/errors/heading-level-skipped.md +36 -0
- package/docs/errors/image-data-length.md +33 -0
- package/docs/errors/image-too-large.md +32 -0
- package/docs/errors/inline-image-unterminated.md +36 -0
- package/docs/errors/input-unreadable.md +37 -0
- package/docs/errors/instance-reset.md +34 -0
- package/docs/errors/invalid-argument.md +33 -0
- package/docs/errors/invalid-axis-value.md +32 -0
- package/docs/errors/invalid-box.md +34 -0
- package/docs/errors/invalid-color.md +32 -0
- package/docs/errors/invalid-command-line.md +44 -0
- package/docs/errors/invalid-date.md +34 -0
- package/docs/errors/invalid-entry.md +34 -0
- package/docs/errors/invalid-feature.md +32 -0
- package/docs/errors/invalid-font.md +32 -0
- package/docs/errors/invalid-geometry.md +32 -0
- package/docs/errors/invalid-icc-profile.md +32 -0
- package/docs/errors/invalid-image.md +33 -0
- package/docs/errors/invalid-op-log.md +33 -0
- package/docs/errors/invalid-page-label.md +34 -0
- package/docs/errors/invalid-page-labels.md +32 -0
- package/docs/errors/invalid-page-range.md +37 -0
- package/docs/errors/invalid-rotate.md +34 -0
- package/docs/errors/invalid-spans.md +39 -0
- package/docs/errors/invalid-structure-nesting.md +38 -0
- package/docs/errors/invalid-structure.md +34 -0
- package/docs/errors/invalid-text-range.md +33 -0
- package/docs/errors/job-finished.md +34 -0
- package/docs/errors/limit-exceeded.md +38 -0
- package/docs/errors/link-without-contents.md +33 -0
- package/docs/errors/link-without-structure.md +37 -0
- package/docs/errors/markdown-html-ignored.md +33 -0
- package/docs/errors/markdown-image-missing.md +34 -0
- package/docs/errors/missing-alt-text.md +33 -0
- package/docs/errors/missing-glyph.md +33 -0
- package/docs/errors/missing-header.md +37 -0
- package/docs/errors/missing-lang.md +32 -0
- package/docs/errors/missing-root.md +34 -0
- package/docs/errors/missing-title.md +32 -0
- package/docs/errors/model-failed.md +33 -0
- package/docs/errors/model-needs-host.md +40 -0
- package/docs/errors/model-result-unexpected.md +36 -0
- package/docs/errors/name-tree-node-skipped.md +34 -0
- package/docs/errors/nested-transaction.md +34 -0
- package/docs/errors/no-pending-save.md +32 -0
- package/docs/errors/no-transaction.md +33 -0
- package/docs/errors/not-initialized.md +35 -0
- package/docs/errors/not-pdf.md +33 -0
- package/docs/errors/not-rebased.md +35 -0
- package/docs/errors/object-number-out-of-range.md +38 -0
- package/docs/errors/objstm-index-mismatch.md +36 -0
- package/docs/errors/out-of-memory.md +38 -0
- package/docs/errors/outline-cycle.md +34 -0
- package/docs/errors/outline-item-skipped.md +34 -0
- package/docs/errors/output-unwritable.md +37 -0
- package/docs/errors/overwrite-refused.md +36 -0
- package/docs/errors/page-count-mismatch.md +34 -0
- package/docs/errors/page-index-out-of-range.md +34 -0
- package/docs/errors/page-tree-node-skipped.md +35 -0
- package/docs/errors/password-required.md +36 -0
- package/docs/errors/path-outside-roots.md +32 -0
- package/docs/errors/perms-mismatch.md +34 -0
- package/docs/errors/profile-version-conflict.md +35 -0
- package/docs/errors/redaction-not-applied.md +32 -0
- package/docs/errors/reference-cycle.md +35 -0
- package/docs/errors/render-backend-unavailable.md +32 -0
- package/docs/errors/render-failed.md +33 -0
- package/docs/errors/render-too-large.md +36 -0
- package/docs/errors/repaired-base.md +34 -0
- package/docs/errors/requires-rewrite.md +38 -0
- package/docs/errors/revision-index-out-of-range.md +34 -0
- package/docs/errors/saved-not-rebased.md +34 -0
- package/docs/errors/sink-not-empty.md +33 -0
- package/docs/errors/sink.md +31 -0
- package/docs/errors/source-budget-exceeded.md +34 -0
- package/docs/errors/source-changed.md +33 -0
- package/docs/errors/source.md +30 -0
- package/docs/errors/stale-generation.md +34 -0
- package/docs/errors/stream-length-recovered.md +37 -0
- package/docs/errors/strict.md +34 -0
- package/docs/errors/subset-failed.md +31 -0
- package/docs/errors/syntax.md +34 -0
- package/docs/errors/tag-element-not-open.md +41 -0
- package/docs/errors/tag-element-unclosed.md +34 -0
- package/docs/errors/text-in-earlier-revision.md +36 -0
- package/docs/errors/text-module-not-loaded.md +36 -0
- package/docs/errors/text-overflow.md +34 -0
- package/docs/errors/tounicode-conflict.md +32 -0
- package/docs/errors/type.md +34 -0
- package/docs/errors/unexpected-eof.md +34 -0
- package/docs/errors/unknown-axis.md +33 -0
- package/docs/errors/unknown-code.md +36 -0
- package/docs/errors/unknown-destination.md +33 -0
- package/docs/errors/unknown-font.md +33 -0
- package/docs/errors/unknown-option.md +33 -0
- package/docs/errors/unknown-page.md +34 -0
- package/docs/errors/unmapped-glyph.md +36 -0
- package/docs/errors/unnumbered-heading-misused.md +34 -0
- package/docs/errors/unset-object.md +30 -0
- package/docs/errors/unsupported-filter.md +34 -0
- package/docs/errors/unsupported-image.md +33 -0
- package/docs/errors/unsupported-layout-option.md +33 -0
- package/docs/errors/unsupported-security-handler.md +34 -0
- package/docs/errors/unsupported-version.md +32 -0
- package/docs/errors/untagged-content.md +35 -0
- package/docs/errors/warnings-truncated.md +35 -0
- package/docs/errors/wasm-trap.md +37 -0
- package/docs/errors/whole-font-rebuilt.md +30 -0
- package/docs/errors/worker-crashed.md +33 -0
- package/docs/errors/worker-failed.md +32 -0
- package/docs/errors/worker-version-mismatch.md +34 -0
- package/docs/errors/would-invalidate-signatures.md +38 -0
- package/docs/errors/wrong-document.md +33 -0
- package/docs/errors/wrong-password.md +34 -0
- package/docs/errors/xobject-cycle.md +36 -0
- package/docs/errors/xref-repaired.md +36 -0
- package/docs/errors/xrefstm-ignored.md +34 -0
- package/docs/llms.txt +94 -0
- package/docs/pages/concepts/dependencies.md +44 -0
- package/docs/pages/concepts/drawing-pages.md +76 -0
- package/docs/pages/concepts/errors-and-warnings.md +68 -0
- package/docs/pages/concepts/how-lamina-thinks.md +98 -0
- package/docs/pages/concepts/pages-and-coordinates.md +75 -0
- package/docs/pages/guides/accessible-pdfs.md +262 -0
- package/docs/pages/guides/archival-pdfs.md +433 -0
- package/docs/pages/guides/edit-a-pdf.md +143 -0
- package/docs/pages/guides/flowing-pages.md +379 -0
- package/docs/pages/guides/fonts-and-text.md +204 -0
- package/docs/pages/guides/images-and-vectors.md +221 -0
- package/docs/pages/guides/keep-the-page-responsive.md +85 -0
- package/docs/pages/guides/links-outlines-and-metadata.md +224 -0
- package/docs/pages/guides/open-large-files.md +69 -0
- package/docs/pages/guides/password-protected-files.md +70 -0
- package/docs/pages/guides/save-a-pdf.md +154 -0
- package/docs/pages/guides/test-generated-pdfs.md +324 -0
- package/docs/pages/guides/untrusted-files.md +82 -0
- package/docs/pages/index.md +33 -0
- package/docs/pages/install.md +79 -0
- package/docs/pages/quick-start/browser.md +71 -0
- package/docs/pages/quick-start/bun.md +64 -0
- package/docs/pages/quick-start/deno.md +62 -0
- package/docs/pages/quick-start/node.md +65 -0
- package/docs/pages/quick-start/rust.md +62 -0
- package/docs/pages/quick-start/workers.md +139 -0
- package/docs/pages/recipes/cloudflare-worker.md +115 -0
- package/docs/pages/recipes/element-flow.md +248 -0
- package/docs/pages/recipes/extract-markdown.md +152 -0
- package/docs/pages/recipes/generate-then-edit.md +142 -0
- package/docs/pages/recipes/hello.md +101 -0
- package/docs/pages/recipes/images-and-vectors.md +216 -0
- package/docs/pages/recipes/invoice.md +233 -0
- package/docs/pages/recipes/links-and-outlines.md +222 -0
- package/docs/pages/recipes/multilingual.md +196 -0
- package/docs/pages/recipes/open-untrusted.md +65 -0
- package/docs/pages/recipes/pdf-a-archive.md +212 -0
- package/docs/pages/recipes/pdf-a3-invoice-with-xml.md +229 -0
- package/docs/pages/recipes/pdf-ua-report.md +244 -0
- package/docs/pages/recipes/report.md +125 -0
- package/docs/pages/recipes/web-worker-client.md +64 -0
- package/docs/pages/recipes.md +48 -0
- package/docs/pages/reference/benchmarks.md +93 -0
- package/docs/pages/reference/changelog.md +134 -0
- package/docs/pages/reference/codes.md +14 -0
- package/docs/pages/reference/glossary.md +108 -0
- package/docs/pages/reference/rust.md +36 -0
- package/docs/pages/reference/stability.md +59 -0
- package/docs/pages/start/coding-agents.md +45 -0
- package/docs/pages/start/first-pdf.md +81 -0
- package/docs/pages/troubleshooting.md +74 -0
- package/docs/renames.json +86 -0
- package/docs/schemas/error.schema.json +322 -0
- package/docs/schemas/renames.schema.json +29 -0
- package/package.json +7 -3
- package/schemas/error.schema.json +37 -3
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "A PDF/A-3 invoice with XML"
|
|
3
|
+
description: "A PDF/A-3b invoice that carries the same invoice as an attached XML file, for people to read and for software to process."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/pdf-a3-invoice-with-xml/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A PDF/A-3 invoice with XML
|
|
9
|
+
|
|
10
|
+
A PDF/A-3b invoice that carries the same invoice as an attached XML file, for people to read and for software to process.
|
|
11
|
+
|
|
12
|
+
You'll write a one-page invoice as PDF/A-3b, with the same invoice attached as XML. You need this for e-invoices such as Factur-X and ZUGFeRD, where a person reads the PDF and the buyer's software reads the XML.
|
|
13
|
+
|
|
14
|
+
The function draws the invoice, writes the same items as XML and attaches that file before it finishes. In TypeScript it needs the text module, `init({ text: true })`. Both languages write the same bytes:
|
|
15
|
+
|
|
16
|
+
**TypeScript**
|
|
17
|
+
|
|
18
|
+
`packages/sdk/examples/pdf-a3-invoice-with-xml.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// A PDF/A-3b invoice that carries the same invoice as XML, attached for software to read.
|
|
22
|
+
import { Builder, Font, PAGE_SIZES, type Surface } from "@laminapdf/sdk";
|
|
23
|
+
|
|
24
|
+
const MARGIN = 56;
|
|
25
|
+
/** The page's right edge, less the margin: where the rule under the items ends. */
|
|
26
|
+
const RIGHT = PAGE_SIZES.A4[0] - MARGIN;
|
|
27
|
+
const AMOUNTS = 460;
|
|
28
|
+
const ROW = 22;
|
|
29
|
+
const TITLE_BASELINE = 80;
|
|
30
|
+
const FIRST_ROW = 130;
|
|
31
|
+
/**
|
|
32
|
+
* How far above the next row's baseline the rule sits, and how far below it the total and
|
|
33
|
+
* the note are.
|
|
34
|
+
*/
|
|
35
|
+
const RULE_RISE = 7;
|
|
36
|
+
const TOTAL_DROP = 18;
|
|
37
|
+
const NOTE_DROP = 68;
|
|
38
|
+
/** How far left of the amounts column the total starts, to fit its "Total" label. */
|
|
39
|
+
const TOTAL_INDENT = 40;
|
|
40
|
+
const TITLE = 24;
|
|
41
|
+
const TOTAL = 14;
|
|
42
|
+
const BODY = 11;
|
|
43
|
+
const ITEMS = [
|
|
44
|
+
["Sprockets", 12, 1.25],
|
|
45
|
+
["Gadget", 1, 79.9],
|
|
46
|
+
["Delivery", 1, 4.5],
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
/** The invoice's total. */
|
|
50
|
+
function total(): number {
|
|
51
|
+
return ITEMS.reduce((sum, [, qty, price]) => sum + qty * price, 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The invoice as XML, in a made-up schema (a real e-invoice follows EN 16931, as Factur-X does). */
|
|
55
|
+
function invoiceXml(): string {
|
|
56
|
+
const lines = ITEMS.map(
|
|
57
|
+
([item, qty, price]) => ` <line item="${item}" quantity="${qty}" price="${price.toFixed(2)}"/>\n`,
|
|
58
|
+
);
|
|
59
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<invoice number="42" currency="EUR">\n${lines.join("")} <total>${total().toFixed(2)}</total>\n</invoice>\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The invoice's bytes. Needs `init({ text: true })`.
|
|
64
|
+
*
|
|
65
|
+
* @param notoSans - the bytes of the Noto Sans font file.
|
|
66
|
+
*/
|
|
67
|
+
export function pdfA3InvoiceWithXml(notoSans: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
68
|
+
using b = new Builder({ profiles: ["PDF/A-3b"] });
|
|
69
|
+
b.metadata({ title: "Invoice 42", author: "Acme Widgets Ltd" });
|
|
70
|
+
const font = Font.fromBytes(notoSans);
|
|
71
|
+
b.font(font);
|
|
72
|
+
const page = b.page("A4");
|
|
73
|
+
draw(page.surface(), font);
|
|
74
|
+
page.finish();
|
|
75
|
+
b.attach("invoice-42.xml", new TextEncoder().encode(invoiceXml()), "text/xml", {
|
|
76
|
+
relationship: "alternative",
|
|
77
|
+
description: "Invoice 42 as XML",
|
|
78
|
+
});
|
|
79
|
+
using doc = b.finish();
|
|
80
|
+
return doc.save();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function draw(s: Surface, font: Font): void {
|
|
84
|
+
s.text("Invoice 42", { font, size: TITLE, at: [MARGIN, TITLE_BASELINE] });
|
|
85
|
+
let y = FIRST_ROW;
|
|
86
|
+
for (const [item, qty, price] of ITEMS) {
|
|
87
|
+
s.text(`${qty} × ${item}`, { font, size: BODY, at: [MARGIN, y] });
|
|
88
|
+
s.text(`€ ${(qty * price).toFixed(2)}`, { font, size: BODY, at: [AMOUNTS, y] });
|
|
89
|
+
y += ROW;
|
|
90
|
+
}
|
|
91
|
+
const rule = y - RULE_RISE;
|
|
92
|
+
s.strokeColor([0.5])
|
|
93
|
+
.path((p) => p.moveTo(MARGIN, rule).lineTo(RIGHT, rule))
|
|
94
|
+
.stroke();
|
|
95
|
+
s.text(`Total € ${total().toFixed(2)}`, { font, size: TOTAL, at: [AMOUNTS - TOTAL_INDENT, y + TOTAL_DROP] });
|
|
96
|
+
s.text("The invoice is attached as invoice-42.xml.", {
|
|
97
|
+
font,
|
|
98
|
+
size: BODY,
|
|
99
|
+
at: [MARGIN, y + NOTE_DROP],
|
|
100
|
+
link: { target: { type: "uri", uri: "https://example.com/invoices/42" }, contents: "Invoice 42 online" },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Rust**
|
|
106
|
+
|
|
107
|
+
`crates/laminapdf/examples/pdf_a3_invoice_with_xml.rs`:
|
|
108
|
+
|
|
109
|
+
```rust
|
|
110
|
+
//! A PDF/A-3b invoice that carries the same invoice as XML, attached for software to read.
|
|
111
|
+
//!
|
|
112
|
+
//! Run it with `cargo run -p laminapdf --example pdf_a3_invoice_with_xml -- <font.ttf> [out-dir]`.
|
|
113
|
+
|
|
114
|
+
use std::error::Error;
|
|
115
|
+
use std::fmt::Write as _;
|
|
116
|
+
use std::path::PathBuf;
|
|
117
|
+
|
|
118
|
+
use laminapdf::{
|
|
119
|
+
AfRelationship, Attachment, BuildError, BuildOptions, Builder, Color, FontHandle, LinkTarget, Metadata, PageSize,
|
|
120
|
+
PointPt, Profile, Pt, SaveOptions, StrokeStyle, Surface,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const MARGIN: f64 = 56.0;
|
|
124
|
+
/// The page's right edge, less the margin: where the rule under the items ends.
|
|
125
|
+
const RIGHT: f64 = PageSize::A4.width.get() - MARGIN;
|
|
126
|
+
const AMOUNTS: f64 = 460.0;
|
|
127
|
+
const ROW: f64 = 22.0;
|
|
128
|
+
const TITLE_BASELINE: f64 = 80.0;
|
|
129
|
+
const FIRST_ROW: f64 = 130.0;
|
|
130
|
+
/// How far above the next row's baseline the rule sits, and how far below it the total and
|
|
131
|
+
/// the note are.
|
|
132
|
+
const RULE_RISE: f64 = 7.0;
|
|
133
|
+
const TOTAL_DROP: f64 = 18.0;
|
|
134
|
+
const NOTE_DROP: f64 = 68.0;
|
|
135
|
+
/// How far left of the amounts column the total starts, to fit its "Total" label.
|
|
136
|
+
const TOTAL_INDENT: f64 = 40.0;
|
|
137
|
+
const TITLE: Pt = Pt::new(24.0);
|
|
138
|
+
const TOTAL: Pt = Pt::new(14.0);
|
|
139
|
+
const BODY: Pt = Pt::new(11.0);
|
|
140
|
+
const ITEMS: [(&str, u32, f64); 3] = [("Sprockets", 12, 1.25), ("Gadget", 1, 79.90), ("Delivery", 1, 4.50)];
|
|
141
|
+
|
|
142
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
143
|
+
let mut args = std::env::args().skip(1);
|
|
144
|
+
let font = std::fs::read(args.next().ok_or("arguments: <font.ttf> [out-dir]")?)?;
|
|
145
|
+
let out = PathBuf::from(args.next().unwrap_or_else(|| "target/examples-out".into()));
|
|
146
|
+
std::fs::create_dir_all(&out)?;
|
|
147
|
+
Ok(std::fs::write(out.join("pdf_a3_invoice_with_xml.pdf"), build(font)?)?)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// The invoice's total.
|
|
151
|
+
fn total() -> f64 {
|
|
152
|
+
ITEMS.iter().map(|(_, qty, price)| f64::from(*qty) * price).sum()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The invoice as XML, in a made-up schema (a real e-invoice follows EN 16931, as Factur-X does).
|
|
156
|
+
fn invoice_xml() -> Result<String, std::fmt::Error> {
|
|
157
|
+
let mut xml =
|
|
158
|
+
String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<invoice number=\"42\" currency=\"EUR\">\n");
|
|
159
|
+
for (item, qty, price) in ITEMS {
|
|
160
|
+
writeln!(xml, " <line item=\"{item}\" quantity=\"{qty}\" price=\"{price:.2}\"/>")?;
|
|
161
|
+
}
|
|
162
|
+
writeln!(xml, " <total>{:.2}</total>\n</invoice>", total())?;
|
|
163
|
+
Ok(xml)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// The invoice's bytes, in `font` (a TrueType or OpenType file).
|
|
167
|
+
pub(crate) fn build(font: Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
168
|
+
let mut b = Builder::new(BuildOptions::new().with_profile(Profile::PdfA3b));
|
|
169
|
+
b.metadata(
|
|
170
|
+
Metadata::default()
|
|
171
|
+
.with_title("Invoice 42")
|
|
172
|
+
.with_author("Acme Widgets Ltd"),
|
|
173
|
+
);
|
|
174
|
+
let font = b.font_bytes(font)?;
|
|
175
|
+
let mut page = b.page(PageSize::A4);
|
|
176
|
+
draw(page.surface(), &font)?;
|
|
177
|
+
page.finish()?;
|
|
178
|
+
let xml = Attachment::new("invoice-42.xml", invoice_xml()?.into_bytes(), "text/xml")
|
|
179
|
+
.with_relationship(AfRelationship::Alternative)
|
|
180
|
+
.with_description("Invoice 42 as XML");
|
|
181
|
+
b.attach(xml);
|
|
182
|
+
let mut doc = b.finish()?;
|
|
183
|
+
Ok(doc.save(&SaveOptions::new())?.bytes)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn draw(s: &mut Surface, font: &FontHandle) -> Result<(), BuildError> {
|
|
187
|
+
let at = PointPt::from_points;
|
|
188
|
+
s.text("Invoice 42", font, TITLE, at(MARGIN, TITLE_BASELINE))?;
|
|
189
|
+
let mut y = FIRST_ROW;
|
|
190
|
+
for (item, qty, price) in ITEMS {
|
|
191
|
+
s.text(&format!("{qty} × {item}"), font, BODY, at(MARGIN, y))?;
|
|
192
|
+
let amount = f64::from(qty) * price;
|
|
193
|
+
s.text(&format!("€ {amount:.2}"), font, BODY, at(AMOUNTS, y))?;
|
|
194
|
+
y += ROW;
|
|
195
|
+
}
|
|
196
|
+
let rule = y - RULE_RISE;
|
|
197
|
+
s.stroke_color(Color::gray(0.5))
|
|
198
|
+
.path(|p| _ = p.move_to(at(MARGIN, rule)).line_to(at(RIGHT, rule)))
|
|
199
|
+
.stroke(&StrokeStyle::default());
|
|
200
|
+
let total_at = at(AMOUNTS - TOTAL_INDENT, y + TOTAL_DROP);
|
|
201
|
+
s.text(&format!("Total € {:.2}", total()), font, TOTAL, total_at)?;
|
|
202
|
+
let note = "The invoice is attached as invoice-42.xml.";
|
|
203
|
+
let note = s.text(note, font, BODY, at(MARGIN, y + NOTE_DROP))?;
|
|
204
|
+
let online = LinkTarget::Uri("https://example.com/invoices/42".into());
|
|
205
|
+
note.link(online, Some("Invoice 42 online".into()));
|
|
206
|
+
Ok(())
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Result:** [pdf_a3_invoice_with_xml.pdf](https://docs.laminapdf.com/examples/pdf/pdf_a3_invoice_with_xml.pdf)
|
|
211
|
+
|
|
212
|
+
The result is one A4 page with three line items and a total of € 99.40, carrying `invoice-42.xml`. The Result tab shows it, with a link to the PDF. Viewers that show attachments list it there.
|
|
213
|
+
|
|
214
|
+
## How it works
|
|
215
|
+
|
|
216
|
+
- `profiles: ["PDF/A-3b"]` (`Profile::PdfA3b` in Rust) asks for PDF/A-2b's rules and allows attached files of any kind. `finish()` fails with a coded error for the first rule the invoice breaks; see [Archival PDFs](../../pages/guides/archival-pdfs.md).
|
|
217
|
+
- PDF/A needs every font embedded, so the invoice uses Noto Sans rather than one of the [standard 14 fonts](../../pages/reference/glossary.md#standard-14-fonts).
|
|
218
|
+
- `invoiceXml` writes the line items and the total from the same `ITEMS` the page draws, so the PDF and the XML always agree. Its schema is made up for the recipe.
|
|
219
|
+
- `attach` adds the file with its name, its bytes and its MIME type, `text/xml`. PDF/A needs a MIME type on every attachment, or `finish()` fails with `embedded-file-without-mime-type`.
|
|
220
|
+
- `relationship: "alternative"` (`AfRelationship::Alternative`) says the XML is another form of the same content, and the builder associates it with the document. `description` is the text a viewer shows beside the file.
|
|
221
|
+
|
|
222
|
+
## What to change
|
|
223
|
+
|
|
224
|
+
- **The items:** change `ITEMS`. The page, the total and the XML all follow.
|
|
225
|
+
- **A real e-invoice:** replace `invoiceXml` with XML that follows EN 16931, the European e-invoicing standard, as Factur-X and ZUGFeRD do. Those formats also need XMP entries of their own, which the builder can't add yet.
|
|
226
|
+
- **Other attachments:** attach the file the invoice was made from with `relationship: "source"`, or the data behind it with `"data"` (`AfRelationship::Source`, `AfRelationship::Data`). Each needs its own name and MIME type.
|
|
227
|
+
- **Accessible too:** add `"PDF/UA-1"` to `profiles`, set `lang` and tag the content, as the [PDF/UA report](../../pages/recipes/pdf-ua-report.md) does.
|
|
228
|
+
|
|
229
|
+
Next: [a 100-page report](../../pages/recipes/report.md), the case Lamina PDF times in every runtime.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "A PDF/UA report"
|
|
3
|
+
description: "A one-page sales report a screen reader can follow, with headings, a table, a described chart and a link, checked against PDF/UA-1 as it's built."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/pdf-ua-report/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A PDF/UA report
|
|
9
|
+
|
|
10
|
+
A one-page sales report a screen reader can follow, with headings, a table, a described chart and a link, checked against PDF/UA-1 as it's built.
|
|
11
|
+
|
|
12
|
+
You'll build a one-page sales report that a screen reader can follow: headings, a table, a chart described in words and a link. You need this when a document has to meet accessibility law or a client's accessibility policy.
|
|
13
|
+
|
|
14
|
+
The function turns on the PDF/UA-1 profile, then draws each part of the report inside a tag that says what it is: a heading, a table cell, a figure. In TypeScript it needs the text module, `init({ text: true })`. Both languages write the same bytes:
|
|
15
|
+
|
|
16
|
+
**TypeScript**
|
|
17
|
+
|
|
18
|
+
`packages/sdk/examples/pdf-ua-report.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// An accessible report that screen readers can follow, checked against PDF/UA-1 when it is built.
|
|
22
|
+
import { Builder, Font, type Surface, type Tag, type TagName } from "@laminapdf/sdk";
|
|
23
|
+
|
|
24
|
+
const MARGIN = 56;
|
|
25
|
+
const ROW = 18;
|
|
26
|
+
const COLUMN = 144;
|
|
27
|
+
const HEADER_BASELINE = 40;
|
|
28
|
+
const TITLE_BASELINE = 90;
|
|
29
|
+
const TABLE_TOP = 150;
|
|
30
|
+
const LINK_BASELINE = 400;
|
|
31
|
+
const CHART_LEFT = 320;
|
|
32
|
+
const CHART_BASE = 360;
|
|
33
|
+
const BAR_WIDTH = 36;
|
|
34
|
+
const BAR_PITCH = 50;
|
|
35
|
+
const BODY = 11;
|
|
36
|
+
/** The font sizes of the title and the headings; other text is `BODY`. */
|
|
37
|
+
const SIZES: Partial<Record<TagName, number>> = { H1: 22, H2: 15 };
|
|
38
|
+
const BLUE = [0.12, 0.47, 0.78] as const;
|
|
39
|
+
const QUARTERS = [
|
|
40
|
+
["Q1", 120],
|
|
41
|
+
["Q2", 135],
|
|
42
|
+
["Q3", 160],
|
|
43
|
+
["Q4", 150],
|
|
44
|
+
] as const;
|
|
45
|
+
const CHART_ALT = "Bar chart of sales by quarter: 120, 135, 160 and 150 thousand euros.";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The report's bytes. Needs `init({ text: true })`.
|
|
49
|
+
*
|
|
50
|
+
* @param notoSans - the bytes of the Noto Sans font file.
|
|
51
|
+
*/
|
|
52
|
+
export function pdfUaReport(notoSans: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
53
|
+
using b = new Builder({ profiles: ["PDF/UA-1"] });
|
|
54
|
+
b.metadata({ title: "Annual sales report 2026" });
|
|
55
|
+
b.lang("en-GB");
|
|
56
|
+
const font = Font.fromBytes(notoSans);
|
|
57
|
+
b.font(font);
|
|
58
|
+
const page = b.page("A4");
|
|
59
|
+
draw(page.surface(), font);
|
|
60
|
+
page.finish();
|
|
61
|
+
using doc = b.finish();
|
|
62
|
+
return doc.save();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One line of text with its baseline at `y`, as the structure element `tag`, sized by it. */
|
|
66
|
+
function line(s: Surface, font: Font, tag: Tag, text: string, x: number, y: number): void {
|
|
67
|
+
const size = (typeof tag === "string" ? SIZES[tag] : undefined) ?? BODY;
|
|
68
|
+
s.tag(tag, (s) => s.text(text, { font, size, at: [x, y] }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function draw(s: Surface, font: Font): void {
|
|
72
|
+
// The running header is decoration: an artifact, which screen readers skip.
|
|
73
|
+
s.artifact("pagination", (s) => s.text("Acme Widgets", { font, size: BODY, at: [MARGIN, HEADER_BASELINE] }));
|
|
74
|
+
line(s, font, "H1", "Annual sales report 2026", MARGIN, TITLE_BASELINE);
|
|
75
|
+
line(s, font, "P", "Sales grew in three quarters of four.", MARGIN, TITLE_BASELINE + 2 * ROW);
|
|
76
|
+
table(s, font);
|
|
77
|
+
chart(s);
|
|
78
|
+
const link = {
|
|
79
|
+
target: { type: "uri", uri: "https://example.com/accounts" },
|
|
80
|
+
contents: "The full 2026 accounts",
|
|
81
|
+
} as const;
|
|
82
|
+
s.tag("P", (s) =>
|
|
83
|
+
s.text("The full accounts", { font, size: BODY, at: [MARGIN, LINK_BASELINE], fill: [...BLUE], link }),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A heading and a table of the quarters, whose first row holds the columns' header cells. */
|
|
88
|
+
function table(s: Surface, font: Font): void {
|
|
89
|
+
line(s, font, "H2", "Sales by quarter", MARGIN, TABLE_TOP);
|
|
90
|
+
const header: Tag = { type: "TH", scope: "column" };
|
|
91
|
+
s.tag("Table", (s) => {
|
|
92
|
+
s.tag("TR", (s) => {
|
|
93
|
+
line(s, font, header, "Quarter", MARGIN, TABLE_TOP + ROW);
|
|
94
|
+
line(s, font, header, "Sales (k€)", MARGIN + COLUMN, TABLE_TOP + ROW);
|
|
95
|
+
});
|
|
96
|
+
QUARTERS.forEach(([quarter, sales], i) => {
|
|
97
|
+
const y = TABLE_TOP + ROW * (i + 2);
|
|
98
|
+
s.tag("TR", (s) => {
|
|
99
|
+
line(s, font, "TD", quarter, MARGIN, y);
|
|
100
|
+
line(s, font, "TD", sales.toFixed(0), MARGIN + COLUMN, y);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The quarters as bars standing on the chart's base: one figure with alternate text. */
|
|
107
|
+
function chart(s: Surface): void {
|
|
108
|
+
s.tag({ type: "Figure", alt: CHART_ALT }, (s) => {
|
|
109
|
+
s.fillColor([...BLUE]);
|
|
110
|
+
QUARTERS.forEach(([, sales], i) => {
|
|
111
|
+
s.path((p) => p.rect([CHART_LEFT + BAR_PITCH * i, CHART_BASE - sales, BAR_WIDTH, sales])).fill();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Rust**
|
|
118
|
+
|
|
119
|
+
`crates/laminapdf/examples/pdf_ua_report.rs`:
|
|
120
|
+
|
|
121
|
+
```rust
|
|
122
|
+
//! An accessible report that screen readers can follow, checked against PDF/UA-1 when it is built.
|
|
123
|
+
|
|
124
|
+
use laminapdf::{
|
|
125
|
+
ArtifactKind, BuildError, BuildOptions, Builder, Color, FillRule, FontHandle, LinkTarget, Metadata, PageSize,
|
|
126
|
+
PointPt, Profile, Pt, RectPt, SaveOptions, Surface, TableScope, Tag, TagKind,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const MARGIN: f64 = 56.0;
|
|
130
|
+
const ROW: f64 = 18.0;
|
|
131
|
+
const COLUMN: f64 = 144.0;
|
|
132
|
+
const HEADER_AT: PointPt = PointPt::from_points(MARGIN, 40.0);
|
|
133
|
+
const TITLE_BASELINE: f64 = 90.0;
|
|
134
|
+
const TABLE_TOP: f64 = 150.0;
|
|
135
|
+
const LINK_AT: PointPt = PointPt::from_points(MARGIN, 400.0);
|
|
136
|
+
const CHART_LEFT: f64 = 320.0;
|
|
137
|
+
const CHART_BASE: f64 = 360.0;
|
|
138
|
+
const BAR_WIDTH: Pt = Pt::new(36.0);
|
|
139
|
+
const BAR_PITCH: f64 = 50.0;
|
|
140
|
+
const BODY: Pt = Pt::new(11.0);
|
|
141
|
+
/// The font sizes of the title and the headings; other text is `BODY`.
|
|
142
|
+
const SIZES: [(TagKind, Pt); 2] = [(TagKind::H1, Pt::new(22.0)), (TagKind::H2, Pt::new(15.0))];
|
|
143
|
+
const BLUE: Color = Color::Rgb(0.12, 0.47, 0.78);
|
|
144
|
+
const QUARTERS: [(&str, f64); 4] = [("Q1", 120.0), ("Q2", 135.0), ("Q3", 160.0), ("Q4", 150.0)];
|
|
145
|
+
const CHART_ALT: &str = "Bar chart of sales by quarter: 120, 135, 160 and 150 thousand euros.";
|
|
146
|
+
|
|
147
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
148
|
+
let mut args = std::env::args().skip(1);
|
|
149
|
+
let font = std::fs::read(args.next().ok_or("arguments: <font.ttf> [out-dir]")?)?;
|
|
150
|
+
let out = std::path::PathBuf::from(args.next().unwrap_or_else(|| "target/examples-out".into()));
|
|
151
|
+
std::fs::create_dir_all(&out)?;
|
|
152
|
+
Ok(std::fs::write(out.join("pdf_ua_report.pdf"), build(font)?)?)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pub(crate) fn build(font: Vec<u8>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
|
156
|
+
let mut b = Builder::new(BuildOptions::new().with_profile(Profile::PdfUa1));
|
|
157
|
+
b.metadata(Metadata::default().with_title("Annual sales report 2026"));
|
|
158
|
+
b.lang("en-GB");
|
|
159
|
+
let font = b.font_bytes(font)?;
|
|
160
|
+
let mut page = b.page(PageSize::A4);
|
|
161
|
+
draw(page.surface(), &font)?;
|
|
162
|
+
page.finish()?;
|
|
163
|
+
Ok(b.finish()?.save(&SaveOptions::new())?.bytes)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// One line of text with its baseline at `y`, as the structure element `tag`, sized by it.
|
|
167
|
+
fn line(s: &mut Surface, font: &FontHandle, tag: impl Into<Tag>, text: &str, x: f64, y: f64) -> Result<(), BuildError> {
|
|
168
|
+
let tag = tag.into();
|
|
169
|
+
let size = SIZES.iter().find(|e| e.0 == tag.kind()).map_or(BODY, |e| e.1);
|
|
170
|
+
s.tag(tag, |s| s.text(text, font, size, PointPt::from_points(x, y)).map(drop))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn draw(s: &mut Surface, font: &FontHandle) -> Result<(), BuildError> {
|
|
174
|
+
// The running header is decoration: an artifact, which screen readers skip.
|
|
175
|
+
let header = |s: &mut Surface| s.text("Acme Widgets", font, BODY, HEADER_AT).map(drop);
|
|
176
|
+
s.artifact(ArtifactKind::Pagination, header)?;
|
|
177
|
+
line(s, font, TagKind::H1, "Annual sales report 2026", MARGIN, TITLE_BASELINE)?;
|
|
178
|
+
let summary = "Sales grew in three quarters of four.";
|
|
179
|
+
line(s, font, TagKind::P, summary, MARGIN, TITLE_BASELINE + 2.0 * ROW)?;
|
|
180
|
+
table(s, font)?;
|
|
181
|
+
chart(s)?;
|
|
182
|
+
let link = LinkTarget::Uri("https://example.com/accounts".into());
|
|
183
|
+
s.tag(TagKind::P, |s| {
|
|
184
|
+
let text = s.text("The full accounts", font, BODY, LINK_AT)?;
|
|
185
|
+
text.fill(BLUE).link(link, Some("The full 2026 accounts".into()));
|
|
186
|
+
Ok(())
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// A heading and a table of the quarters, whose first row holds the columns' header cells.
|
|
191
|
+
fn table(s: &mut Surface, font: &FontHandle) -> Result<(), BuildError> {
|
|
192
|
+
line(s, font, TagKind::H2, "Sales by quarter", MARGIN, TABLE_TOP)?;
|
|
193
|
+
let header = Tag::new(TagKind::TH).with_scope(TableScope::Column);
|
|
194
|
+
s.tag(TagKind::Table, |s| {
|
|
195
|
+
s.tag(TagKind::TR, |s| {
|
|
196
|
+
line(s, font, header.clone(), "Quarter", MARGIN, TABLE_TOP + ROW)?;
|
|
197
|
+
line(s, font, header.clone(), "Sales (k€)", MARGIN + COLUMN, TABLE_TOP + ROW)
|
|
198
|
+
})?;
|
|
199
|
+
for (row, (quarter, sales)) in (2_u8..).zip(QUARTERS) {
|
|
200
|
+
let y = TABLE_TOP + ROW * f64::from(row);
|
|
201
|
+
s.tag(TagKind::TR, |s| {
|
|
202
|
+
line(s, font, TagKind::TD, quarter, MARGIN, y)?;
|
|
203
|
+
line(s, font, TagKind::TD, &format!("{sales:.0}"), MARGIN + COLUMN, y)
|
|
204
|
+
})?;
|
|
205
|
+
}
|
|
206
|
+
Ok(())
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// The quarters as bars standing on the chart's base: one figure with alternate text.
|
|
211
|
+
fn chart(s: &mut Surface) -> Result<(), BuildError> {
|
|
212
|
+
s.tag(Tag::new(TagKind::Figure).with_alt(CHART_ALT), |s| {
|
|
213
|
+
s.fill_color(BLUE);
|
|
214
|
+
for (i, (_, sales)) in (0_u8..).zip(QUARTERS) {
|
|
215
|
+
let x = CHART_LEFT + BAR_PITCH * f64::from(i);
|
|
216
|
+
let bar = RectPt::from_xywh(Pt::new(x), Pt::new(CHART_BASE - sales), BAR_WIDTH, Pt::new(sales));
|
|
217
|
+
s.path(|p| _ = p.rect(bar)).fill(FillRule::NonZero);
|
|
218
|
+
}
|
|
219
|
+
Ok(())
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Result:** [pdf_ua_report.pdf](https://docs.laminapdf.com/examples/pdf/pdf_ua_report.pdf)
|
|
225
|
+
|
|
226
|
+
The result is one A4 page that reads, in order, as a title, a summary, a table of four quarters, a bar chart and a link. The Result tab shows it, with a link to the PDF.
|
|
227
|
+
|
|
228
|
+
## How it works
|
|
229
|
+
|
|
230
|
+
- `profiles: ["PDF/UA-1"]` (`Profile::PdfUa1` in Rust) tags the document and makes `finish()` check the rules a program can check. It fails with a coded error for the first rule the report breaks; [Accessible PDFs](../../pages/guides/accessible-pdfs.md#what-pdfua-asks-for) lists them.
|
|
231
|
+
- `metadata` gives the report its title and `lang` its language, which PDF/UA-1 asks for. The font is an embedded Noto Sans, because PDF/UA-1 refuses the [standard 14 fonts](../../pages/reference/glossary.md#standard-14-fonts).
|
|
232
|
+
- `tag` puts what its callback draws inside a structure element, such as `H1`, `P` or `TD`, in reading order. The `line` helper tags one line of text and sizes it by its tag: 22 points for `H1`, 15 for `H2`, 11 for the rest.
|
|
233
|
+
- The table's first row holds `TH` header cells with a column scope, so a screen reader can name each cell's column.
|
|
234
|
+
- The four bars are one `Figure` with alternate text, the sentence a screen reader reads in place of the drawing. The running header is an artifact, which screen readers skip.
|
|
235
|
+
- The link's `contents`, "The full 2026 accounts", is its description. The builder makes the link a `Link` element inside its paragraph.
|
|
236
|
+
|
|
237
|
+
## What to change
|
|
238
|
+
|
|
239
|
+
- **The numbers:** change `QUARTERS`, then change `CHART_ALT` to match. The builder checks that the chart has alternate text, not that the text is right; see [What a program can't check](../../pages/guides/accessible-pdfs.md#what-a-program-cant-check).
|
|
240
|
+
- **A list:** nest an `"L"`, an `"LI"` for each item, and inside each a `"Lbl"` for the bullet and an `"LBody"` for the text. An `L` holds `LI` items or a nested `L`, and an `LI` holds a `Lbl` and an `LBody`: anything else inside them fails `finish()` with `invalid-structure-nesting`.
|
|
241
|
+
- **A passage in another language:** tag it with its own, as in `{ type: "P", lang: "fr-FR" }` (`Tag::new(TagKind::P).with_lang("fr-FR")` in Rust).
|
|
242
|
+
- **Accessible and archival at once:** add a PDF/A profile beside it, as in `profiles: ["PDF/A-2u", "PDF/UA-1"]`. The [PDF/A archive](../../pages/recipes/pdf-a-archive.md) does this.
|
|
243
|
+
|
|
244
|
+
Next: [a PDF/A archive](../../pages/recipes/pdf-a-archive.md), a letter written for long-term keeping.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "A 100-page report"
|
|
3
|
+
description: "A 100-page A4 report with two embedded fonts and a table on every page, the document Lamina PDF times in each JavaScript runtime."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/report/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A 100-page report
|
|
9
|
+
|
|
10
|
+
A 100-page A4 report with two embedded fonts and a table on every page, the document Lamina PDF times in each JavaScript runtime.
|
|
11
|
+
|
|
12
|
+
You'll make a 100-page report with two embedded fonts: on every page, a heading, a paragraph, a line of Chinese and a table of 20 products. Start from it for any long document you generate from data, such as monthly statements or a catalogue.
|
|
13
|
+
|
|
14
|
+
The function draws one region's page at a time in a loop, then returns the PDF's bytes:
|
|
15
|
+
|
|
16
|
+
`packages/sdk/examples/report.ts`:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// A 100-page report with two embedded fonts. Each page has a heading, a paragraph broken into a
|
|
20
|
+
// box, a line of Chinese, a table of products with rules between the rows, and a page number.
|
|
21
|
+
import { Builder, type BuilderOptions, Font, PAGE_SIZES, type Rect, type Surface } from "@laminapdf/sdk";
|
|
22
|
+
|
|
23
|
+
/** The number of pages. */
|
|
24
|
+
const REPORT_PAGES = 100;
|
|
25
|
+
const PRODUCTS = 20;
|
|
26
|
+
const PRICE = 12.5;
|
|
27
|
+
const [PAGE_WIDTH] = PAGE_SIZES.A4;
|
|
28
|
+
const MARGIN = 56;
|
|
29
|
+
const RIGHT_EDGE = PAGE_WIDTH - MARGIN;
|
|
30
|
+
const HEADING_AT = [MARGIN, 72] as const;
|
|
31
|
+
const PARAGRAPH_BOX = [MARGIN, 88, RIGHT_EDGE - MARGIN, 60] as const;
|
|
32
|
+
const CHINESE_AT = [MARGIN, 166] as const;
|
|
33
|
+
const FOOTER_AT = [MARGIN, 800] as const;
|
|
34
|
+
const FIRST_ROW = 200;
|
|
35
|
+
const ROW_HEIGHT = 24;
|
|
36
|
+
/** The units' and the revenue's columns (left edge, width), both right-aligned. */
|
|
37
|
+
const UNITS_COLUMN = [300, 60] as const;
|
|
38
|
+
const REVENUE_COLUMN = [400, RIGHT_EDGE - 400] as const;
|
|
39
|
+
/** A cell's box starts this far above its row's baseline and is this tall; the rule is this far below. */
|
|
40
|
+
const CELL_RISE = 11;
|
|
41
|
+
const CELL_HEIGHT = 14;
|
|
42
|
+
const RULE_DROP = 7;
|
|
43
|
+
/** Font sizes: the heading, the paragraph and the Chinese line, the table, the page number. */
|
|
44
|
+
const SIZES = { heading: 20, body: 11, table: 10, footer: 9 } as const;
|
|
45
|
+
const PARAGRAPH =
|
|
46
|
+
"Quarterly figures for every region follow. Each table lists the units shipped, the revenue " +
|
|
47
|
+
"and the change against the previous quarter; totals are at the end of the report.";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The document's bytes. Needs `init({ text: true })`.
|
|
51
|
+
*
|
|
52
|
+
* @param latinFont - a font for the text, such as Noto Sans.
|
|
53
|
+
* @param chineseFont - a font with Chinese glyphs, such as Noto Sans SC.
|
|
54
|
+
* @param options - can fix the file ID, the producer and the creation date.
|
|
55
|
+
*/
|
|
56
|
+
export function report(
|
|
57
|
+
latinFont: Uint8Array,
|
|
58
|
+
chineseFont: Uint8Array,
|
|
59
|
+
options: BuilderOptions = {},
|
|
60
|
+
): Uint8Array<ArrayBuffer> {
|
|
61
|
+
using b = new Builder(options);
|
|
62
|
+
const font = Font.fromBytes(latinFont);
|
|
63
|
+
const chinese = Font.fromBytes(chineseFont);
|
|
64
|
+
b.font(font);
|
|
65
|
+
b.font(chinese);
|
|
66
|
+
for (let region = 1; region <= REPORT_PAGES; region++) {
|
|
67
|
+
const page = b.page("A4");
|
|
68
|
+
const s = page.surface();
|
|
69
|
+
s.text(`Region ${region}: quarterly report`, { font, size: SIZES.heading, at: HEADING_AT });
|
|
70
|
+
s.textBox(PARAGRAPH, { font, size: SIZES.body, box: PARAGRAPH_BOX });
|
|
71
|
+
s.text("你好世界,这是中文字体子集测试。", { font: chinese, size: SIZES.body, at: CHINESE_AT });
|
|
72
|
+
products(s, font, region);
|
|
73
|
+
s.text(`Page ${region} of ${REPORT_PAGES}`, { font, size: SIZES.footer, at: FOOTER_AT });
|
|
74
|
+
page.finish();
|
|
75
|
+
}
|
|
76
|
+
b.metadata({ title: "Quarterly report", author: "lamina examples" }).lang("en-GB");
|
|
77
|
+
using doc = b.finish();
|
|
78
|
+
return doc.save();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Made-up sales figures: the units of `product` that `region` shipped, from 0 to 499. */
|
|
82
|
+
function unitsShipped(region: number, product: number): number {
|
|
83
|
+
return (region * 37 + product * 11) % 500;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A region's table: one row per product, with its units and revenue right-aligned. */
|
|
87
|
+
function products(s: Surface, font: Font, region: number): void {
|
|
88
|
+
for (let product = 0; product < PRODUCTS; product++) {
|
|
89
|
+
const y = FIRST_ROW + ROW_HEIGHT * product;
|
|
90
|
+
const units = unitsShipped(region, product);
|
|
91
|
+
const size = SIZES.table;
|
|
92
|
+
s.text(`Product ${product + 1}`, { font, size, at: [MARGIN, y] });
|
|
93
|
+
s.textBox(String(units), { font, size, box: cell(UNITS_COLUMN, y), align: "end" });
|
|
94
|
+
s.textBox(`€ ${(units * PRICE).toFixed(2)}`, { font, size, box: cell(REVENUE_COLUMN, y), align: "end" });
|
|
95
|
+
s.strokeColor([0.8])
|
|
96
|
+
.path((p) => p.moveTo(MARGIN, y + RULE_DROP).lineTo(RIGHT_EDGE, y + RULE_DROP))
|
|
97
|
+
.stroke({ width: 0.5 });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The box of a cell in `column` (left edge, width) on the row at `baseline`. */
|
|
102
|
+
function cell([left, width]: readonly [number, number], baseline: number): Rect {
|
|
103
|
+
return [left, baseline - CELL_RISE, width, CELL_HEIGHT];
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The result is 100 A4 pages, one per region. It has no reference PDF to open. Instead, Lamina PDF's tests build and time the report in every JavaScript runtime and check its SHA-256 there, as does the Rust benchmark that builds the same bytes. [Benchmarks](../../pages/reference/benchmarks.md) has the timings.
|
|
108
|
+
|
|
109
|
+
## How it works
|
|
110
|
+
|
|
111
|
+
- It needs the text module, `init({ text: true })`, because it draws with embedded fonts.
|
|
112
|
+
- `Font.fromBytes` reads each font file, and `b.font` registers both before the first page. Each font is embedded once for the whole report, with only the glyphs its 100 pages draw.
|
|
113
|
+
- `s.text` draws one line, with `at[1]` as its baseline, and `s.textBox` breaks the paragraph into lines that fit `PARAGRAPH_BOX`.
|
|
114
|
+
- In the table, the units and the revenue are text boxes as wide as their columns, with `align: "end"` to right-align them. `cell` works out each box from its column and the row's baseline.
|
|
115
|
+
- `strokeColor([0.8])` sets a light gray, and each rule is a path from the left margin to the right edge, stroked 0.5 points wide.
|
|
116
|
+
- `metadata` and `lang("en-GB")` set the title, author and language once for the document, and `using` frees the builder and the document when the function returns.
|
|
117
|
+
|
|
118
|
+
## What to change
|
|
119
|
+
|
|
120
|
+
- **Your data:** replace `unitsShipped` with your own figures, and set `REPORT_PAGES` and `PRODUCTS` to your counts.
|
|
121
|
+
- **A longer table:** 20 rows end at 656 points, above the page number at 800. For more rows, lower `ROW_HEIGHT` or carry the rest over to the next page.
|
|
122
|
+
- **Another script:** load a font that covers it and draw its text in that font, as the Chinese line does. See [Text and fonts](../../pages/guides/fonts-and-text.md).
|
|
123
|
+
- **The file's details:** pass `producer` and `creationDate` in `options`, such as `creationDate: "D:20261104120000Z"`. The builder never reads the clock, so a report without a date has none.
|
|
124
|
+
|
|
125
|
+
Next: [open someone else's file](../../pages/recipes/open-untrusted.md), with a password and tighter limits.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Save in a Web Worker"
|
|
3
|
+
description: "Open and save a PDF in a Web Worker, so a large file never freezes the page, with progress as it saves and a signal that stops the work."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/web-worker-client/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Save in a Web Worker
|
|
9
|
+
|
|
10
|
+
Open and save a PDF in a Web Worker, so a large file never freezes the page, with progress as it saves and a signal that stops the work.
|
|
11
|
+
|
|
12
|
+
You'll save a PDF in a Web Worker, reporting progress as it goes and stopping when the user presses Cancel. You need this in a browser app once files get large, because a save on the main thread freezes the page until it ends.
|
|
13
|
+
|
|
14
|
+
`saveInWorker` starts a Web Worker, opens the bytes there and saves them as a [full rewrite](../../pages/reference/glossary.md#full-rewrite), handing your `onProgress` and `signal` to each call:
|
|
15
|
+
|
|
16
|
+
`packages/sdk/examples/worker-client.ts`:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// Saving a PDF in a Web Worker, so a large file never blocks the page: `@laminapdf/sdk/worker`
|
|
20
|
+
// runs the same API there, and each method returns a Promise. The save reports its progress step
|
|
21
|
+
// by step, and aborting the signal (a Cancel button, say) stops the work.
|
|
22
|
+
import { type CallOptions, connect } from "@laminapdf/sdk/worker";
|
|
23
|
+
|
|
24
|
+
/** What the save reports to and listens to. */
|
|
25
|
+
export type SaveInWorkerOptions = Pick<CallOptions, "onProgress" | "signal">;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Opens `bytes` in a Web Worker and saves them there as a full rewrite.
|
|
29
|
+
*
|
|
30
|
+
* @param bytes - any PDF; it is copied to the worker (`transfer: true` would move it).
|
|
31
|
+
* @param options - `onProgress` is called after each step of the save. Aborting `signal` stops
|
|
32
|
+
* the work wherever it is (starting the Web Worker, opening, or at the save's next step), and
|
|
33
|
+
* the promise rejects with a `cancelled` error.
|
|
34
|
+
* @returns the saved file.
|
|
35
|
+
*/
|
|
36
|
+
export async function saveInWorker(bytes: Uint8Array, options: SaveInWorkerOptions = {}): Promise<Uint8Array> {
|
|
37
|
+
const client = await connect(options);
|
|
38
|
+
try {
|
|
39
|
+
await using doc = await client.open(bytes, options);
|
|
40
|
+
return await doc.save({ mode: "fullRewrite" }, options);
|
|
41
|
+
} finally {
|
|
42
|
+
client.terminate();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The promise resolves to the saved file, or rejects with `cancelled` once the signal aborts. Lamina PDF's tests run it in Chromium, Firefox and WebKit.
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
- `connect` starts the Web Worker file that ships with `@laminapdf/sdk` and checks that it comes from the same build as your page.
|
|
52
|
+
- `client.open` copies the bytes into the Web Worker and returns a remote document, whose methods return promises. `await using` closes it there when the block ends.
|
|
53
|
+
- The Web Worker saves in steps of 20,000 units of work, about one per object in a full rewrite. Between steps it calls `onProgress` with `done`, `total` and `phase`, at most every 50 ms, so a small file saves in one step and reports none.
|
|
54
|
+
- Aborting the signal rejects the call at once with `cancelled`, and `error.cause` holds the signal's reason. A save stops at its next step.
|
|
55
|
+
- `client.terminate()` in `finally` stops the Web Worker however the save ended, which also stops an open that was still running.
|
|
56
|
+
|
|
57
|
+
## What to change
|
|
58
|
+
|
|
59
|
+
- **A progress bar:** in `onProgress`, set a `<progress>` element's `value` to `done` and, when the save knows it, its `max` to `total`.
|
|
60
|
+
- **Large files:** pass `transfer: true` to `client.open` to move the buffer instead of copying it, which leaves your copy empty. See [Keep the page responsive](../../pages/guides/keep-the-page-responsive.md#move-bytes-instead-of-copying-them).
|
|
61
|
+
- **Several files:** call `connect` once, reuse the client for each file, and terminate it when the page no longer needs it.
|
|
62
|
+
- **Edits before the save:** apply them with `await doc.apply(ops)`, then save in the default `auto` mode. It appends them as an [incremental save](../../pages/reference/glossary.md#incremental-save) when the file allows one. See [Save a PDF](../../pages/guides/save-a-pdf.md#choose-a-save-mode).
|
|
63
|
+
|
|
64
|
+
Next: [a Cloudflare Worker](../../pages/recipes/cloudflare-worker.md) that generates a PDF per request.
|