@laminapdf/sdk 0.2.0-alpha.2 → 0.2.0-alpha.3
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 +42 -7
- package/dist/api.d.ts +5 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -2
- package/dist/api.js.map +1 -1
- package/dist/builder.d.ts +35 -19
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +114 -80
- package/dist/builder.js.map +1 -1
- package/dist/document.d.ts +47 -21
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +112 -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.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/codes.d.ts +18 -6
- package/dist/generated/codes.d.ts.map +1 -1
- package/dist/generated/codes.js +59 -4
- package/dist/generated/codes.js.map +1 -1
- package/dist/generated/explain.d.ts.map +1 -1
- package/dist/generated/explain.js +27 -10
- package/dist/generated/explain.js.map +1 -1
- package/dist/generated/extract.d.ts +235 -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.js +1 -1
- 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/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 +6 -2
- package/dist/worker/support.d.ts.map +1 -1
- package/dist/worker/support.js +1 -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 +147 -0
- package/docs/api/classes/Builder.md +467 -0
- package/docs/api/classes/Document.md +742 -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 +326 -0
- package/docs/api/classes/Transaction.md +111 -0
- package/docs/api/functions/callbackSource.md +33 -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/HttpRangeOptions.md +78 -0
- package/docs/api/interfaces/InitOptions.md +40 -0
- package/docs/api/interfaces/Leak.md +21 -0
- package/docs/api/interfaces/Location.md +53 -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/ExtractedBlock.md +82 -0
- package/docs/api/type-aliases/ExtractedPage.md +65 -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 +41 -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/Op.md +15 -0
- package/docs/api/type-aliases/OutlineEntry.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/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 +63 -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/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/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 +156 -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/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-limit-blocks.md +35 -0
- package/docs/errors/extract-module-not-loaded.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/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 +35 -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-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/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/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/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/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/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 +88 -0
- package/docs/pages/concepts/dependencies.md +44 -0
- package/docs/pages/concepts/errors-and-warnings.md +64 -0
- package/docs/pages/concepts/how-lamina-thinks.md +86 -0
- package/docs/pages/concepts/pages-and-coordinates.md +67 -0
- package/docs/pages/guides/accessible-pdfs.md +260 -0
- package/docs/pages/guides/archival-pdfs.md +429 -0
- package/docs/pages/guides/edit-a-pdf.md +137 -0
- package/docs/pages/guides/fonts-and-text.md +202 -0
- package/docs/pages/guides/images-and-vectors.md +219 -0
- package/docs/pages/guides/keep-the-page-responsive.md +81 -0
- package/docs/pages/guides/links-outlines-and-metadata.md +222 -0
- package/docs/pages/guides/password-protected-files.md +70 -0
- package/docs/pages/guides/save-a-pdf.md +144 -0
- package/docs/pages/guides/untrusted-files.md +80 -0
- package/docs/pages/index.md +33 -0
- package/docs/pages/install.md +75 -0
- package/docs/pages/quick-start/browser.md +67 -0
- package/docs/pages/quick-start/bun.md +60 -0
- package/docs/pages/quick-start/deno.md +58 -0
- package/docs/pages/quick-start/node.md +61 -0
- package/docs/pages/quick-start/rust.md +58 -0
- package/docs/pages/quick-start/workers.md +139 -0
- package/docs/pages/recipes/cloudflare-worker.md +115 -0
- package/docs/pages/recipes/extract-markdown.md +114 -0
- package/docs/pages/recipes/generate-then-edit.md +135 -0
- package/docs/pages/recipes/hello.md +94 -0
- package/docs/pages/recipes/images-and-vectors.md +209 -0
- package/docs/pages/recipes/invoice.md +226 -0
- package/docs/pages/recipes/links-and-outlines.md +215 -0
- package/docs/pages/recipes/multilingual.md +189 -0
- package/docs/pages/recipes/open-untrusted.md +65 -0
- package/docs/pages/recipes/pdf-a-archive.md +205 -0
- package/docs/pages/recipes/pdf-a3-invoice-with-xml.md +222 -0
- package/docs/pages/recipes/pdf-ua-report.md +237 -0
- package/docs/pages/recipes/report.md +125 -0
- package/docs/pages/recipes/web-worker-client.md +64 -0
- package/docs/pages/recipes.md +47 -0
- package/docs/pages/reference/changelog.md +86 -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 +77 -0
- package/docs/pages/troubleshooting.md +74 -0
- package/docs/renames.json +86 -0
- package/docs/schemas/error.schema.json +305 -0
- package/docs/schemas/renames.schema.json +29 -0
- package/package.json +5 -3
- package/schemas/error.schema.json +19 -2
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Archival PDFs"
|
|
3
|
+
description: "Build PDF/A-2b, PDF/A-2u and PDF/A-3b documents that look the same in years to come, and attach files such as an e-invoice's XML."
|
|
4
|
+
version: "0.2.0-alpha.3"
|
|
5
|
+
url: "https://docs.laminapdf.com/guides/archival-pdfs/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Archival PDFs
|
|
9
|
+
|
|
10
|
+
Build PDF/A-2b, PDF/A-2u and PDF/A-3b documents that look the same in years to come, and attach files such as an e-invoice's XML.
|
|
11
|
+
|
|
12
|
+
This guide builds PDF/A documents, which you need when a file must stay readable and look the same for decades: contracts, records kept by law, e-invoices. Lamina PDF writes PDF/A as it generates a document; it doesn't convert existing files to PDF/A.
|
|
13
|
+
|
|
14
|
+
## What PDF/A asks for
|
|
15
|
+
|
|
16
|
+
PDF/A is the archival subset of PDF. A PDF/A file carries everything it needs to be displayed: its fonts are embedded, its colours are defined, and nothing depends on the viewer, the network or a password. Lamina PDF writes three PDF/A profiles:
|
|
17
|
+
|
|
18
|
+
- **PDF/A-2b** ("basic"): the document looks the same everywhere.
|
|
19
|
+
- **PDF/A-2u** ("Unicode"): PDF/A-2b, and every glyph maps to Unicode, so the text can be searched and copied.
|
|
20
|
+
- **PDF/A-3b:** PDF/A-2b that may carry attached files of any kind.
|
|
21
|
+
|
|
22
|
+
Choose one with the `profiles` option (`BuildOptions::with_profile` in Rust). The builder then writes two things the standard asks for. It writes the PDF/A identification in the document's XMP metadata, the XML packet in the file that describes it. That packet always agrees with the Info dictionary, the older list of the same facts (title, author, dates) that PDF readers also show.
|
|
23
|
+
|
|
24
|
+
It also writes an output intent: a declaration of what device colours mean. The intent embeds an ICC profile, the standard file format that describes a colour space. Lamina PDF's output intent is sRGB, so RGB and gray colour are defined. `DeviceCMYK` is CMYK colour given as plain numbers, with no profile to say which inks they mean (`Color::cmyk` in Rust, four numbers in TypeScript), so the sRGB intent leaves it undefined.
|
|
25
|
+
|
|
26
|
+
`finish()` fails with a coded error for each rule the document breaks:
|
|
27
|
+
|
|
28
|
+
| Rule | Error |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| Every font is embedded (no standard 14 font) | [`font-not-embedded`](../../errors/font-not-embedded.md) |
|
|
31
|
+
| No `DeviceCMYK` colour or CMYK image (the output intent is sRGB) | [`device-color-without-output-intent`](../../errors/device-color-without-output-intent.md) |
|
|
32
|
+
| PDF/A-2u: every glyph maps to Unicode | [`glyph-without-unicode`](../../errors/glyph-without-unicode.md) |
|
|
33
|
+
| PDF/A-2b, PDF/A-2u: no attached files | [`embedded-file-forbidden`](../../errors/embedded-file-forbidden.md) |
|
|
34
|
+
| Every attached file has a MIME type | [`embedded-file-without-mime-type`](../../errors/embedded-file-without-mime-type.md) |
|
|
35
|
+
| The PDF version is 1.7 or lower | [`profile-version-conflict`](../../errors/profile-version-conflict.md) |
|
|
36
|
+
|
|
37
|
+
For CMYK colour under PDF/A, give the colour an ICC profile instead (`Color::icc` in Rust): an ICC-based colour defines itself. Transparency is allowed in PDF/A-2 and PDF/A-3.
|
|
38
|
+
|
|
39
|
+
## An archived letter
|
|
40
|
+
|
|
41
|
+
The letter below is written twice, as PDF/A-2b and as PDF/A-2u. It embeds its font, places a photo and draws a translucent band across the top. The PDF/A-2u copy is also PDF/UA-1, the accessibility standard (see [Accessible PDFs](../../pages/guides/accessible-pdfs.md)): a document can be both archival and accessible.
|
|
42
|
+
|
|
43
|
+
**TypeScript**
|
|
44
|
+
|
|
45
|
+
`packages/sdk/examples/pdf-a-archive.ts`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// An archival letter, written as PDF/A-2b and as PDF/A-2u that is also PDF/UA-1.
|
|
49
|
+
import { Builder, Font, Image, PAGE_SIZES, type Profile, type Surface } from "@laminapdf/sdk";
|
|
50
|
+
|
|
51
|
+
const MARGIN = 56;
|
|
52
|
+
const BAND_HEIGHT = 70;
|
|
53
|
+
const TITLE_BASELINE = 110;
|
|
54
|
+
/** The top of the letter's text and of the photo beside it, and the height of both. */
|
|
55
|
+
const BLOCK_TOP = 130;
|
|
56
|
+
const BLOCK_HEIGHT = 120;
|
|
57
|
+
const LETTER_WIDTH = 300;
|
|
58
|
+
const PHOTO_LEFT = 380;
|
|
59
|
+
const PHOTO_WIDTH = 160;
|
|
60
|
+
const FOOTER_BASELINE = 300;
|
|
61
|
+
const TITLE = 22;
|
|
62
|
+
const BODY = 11;
|
|
63
|
+
const BAND_OPACITY = 0.35;
|
|
64
|
+
const PHOTO_ALT = "A photograph of the sprockets, packed in a box";
|
|
65
|
+
const LETTER =
|
|
66
|
+
"Thank you for your order of 12 sprockets. This letter confirms the delivery date and the price we agreed. " +
|
|
67
|
+
"Keep it: PDF/A files look the same in any viewer, now and in the future.";
|
|
68
|
+
|
|
69
|
+
/** PDF/A-2b, or PDF/A-2u (whose text maps to Unicode) with PDF/UA-1. */
|
|
70
|
+
const PROFILES: Record<"2b" | "2u", Profile[]> = {
|
|
71
|
+
"2b": ["PDF/A-2b"],
|
|
72
|
+
"2u": ["PDF/A-2u", "PDF/UA-1"],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The letter's bytes. Needs `init({ text: true })`.
|
|
77
|
+
*
|
|
78
|
+
* @param level - `2b` or `2u`.
|
|
79
|
+
* @param notoSans - the bytes of the Noto Sans font file.
|
|
80
|
+
* @param photo - the bytes of a JPEG photo.
|
|
81
|
+
*/
|
|
82
|
+
export function pdfAArchive(level: "2b" | "2u", notoSans: Uint8Array, photo: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
83
|
+
using b = new Builder({ profiles: PROFILES[level], tagged: true });
|
|
84
|
+
b.metadata({ title: "Order confirmation 42", author: "Acme Widgets Ltd" });
|
|
85
|
+
b.lang("en-GB");
|
|
86
|
+
const font = Font.fromBytes(notoSans);
|
|
87
|
+
b.font(font);
|
|
88
|
+
const page = b.page("A4");
|
|
89
|
+
draw(page.surface(), font, Image.jpeg(photo));
|
|
90
|
+
page.finish();
|
|
91
|
+
using doc = b.finish();
|
|
92
|
+
return doc.save();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function draw(s: Surface, font: Font, photo: Image): void {
|
|
96
|
+
// A translucent band across the top: decoration, and transparency, which PDF/A-2 allows.
|
|
97
|
+
const [width] = PAGE_SIZES.A4;
|
|
98
|
+
s.artifact("layout", (s) => {
|
|
99
|
+
s.save().opacity(BAND_OPACITY, 1).fillColor([0.12, 0.47, 0.78]);
|
|
100
|
+
s.path((p) => p.rect([0, 0, width, BAND_HEIGHT])).fill();
|
|
101
|
+
s.restore();
|
|
102
|
+
});
|
|
103
|
+
s.tag("H1", (s) => s.text("Order confirmation 42", { font, size: TITLE, at: [MARGIN, TITLE_BASELINE] }));
|
|
104
|
+
s.tag("P", (s) => s.textBox(LETTER, { font, size: BODY, box: [MARGIN, BLOCK_TOP, LETTER_WIDTH, BLOCK_HEIGHT] }));
|
|
105
|
+
s.image(photo, [PHOTO_LEFT, BLOCK_TOP, PHOTO_WIDTH, BLOCK_HEIGHT], PHOTO_ALT);
|
|
106
|
+
s.tag("P", (s) =>
|
|
107
|
+
s.text("Acme Widgets Ltd, 1 Example Street, London", { font, size: BODY, at: [MARGIN, FOOTER_BASELINE] }),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Rust**
|
|
113
|
+
|
|
114
|
+
`crates/laminapdf/examples/pdf_a_archive.rs`:
|
|
115
|
+
|
|
116
|
+
```rust
|
|
117
|
+
//! An archival letter, written as PDF/A-2b and as PDF/A-2u that is also PDF/UA-1.
|
|
118
|
+
//!
|
|
119
|
+
//! Run it with `cargo run -p laminapdf --example pdf_a_archive -- <font.ttf> <photo.jpg> [out-dir]`: it writes
|
|
120
|
+
//! `pdf_a_archive-2b.pdf` and `pdf_a_archive-2u.pdf`.
|
|
121
|
+
|
|
122
|
+
use std::error::Error;
|
|
123
|
+
use std::path::PathBuf;
|
|
124
|
+
|
|
125
|
+
use laminapdf::{
|
|
126
|
+
ArtifactKind, BuildError, BuildOptions, Builder, Color, FillRule, FontHandle, Image, Metadata, PageSize, PointPt,
|
|
127
|
+
Profile, Pt, RectPt, SaveOptions, Surface, TagKind, TextBoxOptions,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const MARGIN: f64 = 56.0;
|
|
131
|
+
const BAND_HEIGHT: f64 = 70.0;
|
|
132
|
+
const TITLE_AT: PointPt = PointPt::from_points(MARGIN, 110.0);
|
|
133
|
+
/// The top of the letter's text and of the photo beside it, and the height of both.
|
|
134
|
+
const BLOCK_TOP: f64 = 130.0;
|
|
135
|
+
const BLOCK_HEIGHT: f64 = 120.0;
|
|
136
|
+
const LETTER_WIDTH: f64 = 300.0;
|
|
137
|
+
const PHOTO_LEFT: f64 = 380.0;
|
|
138
|
+
const PHOTO_WIDTH: f64 = 160.0;
|
|
139
|
+
const FOOTER_AT: PointPt = PointPt::from_points(MARGIN, 300.0);
|
|
140
|
+
const TITLE: Pt = Pt::new(22.0);
|
|
141
|
+
const BODY: Pt = Pt::new(11.0);
|
|
142
|
+
const BAND_OPACITY: f64 = 0.35;
|
|
143
|
+
const PHOTO_ALT: &str = "A photograph of the sprockets, packed in a box";
|
|
144
|
+
const LETTER: &str = "Thank you for your order of 12 sprockets. This letter confirms the delivery date and the \
|
|
145
|
+
price we agreed. Keep it: PDF/A files look the same in any viewer, now and in the future.";
|
|
146
|
+
|
|
147
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
148
|
+
let mut args = std::env::args().skip(1);
|
|
149
|
+
let usage = "arguments: <font.ttf> <photo.jpg> [out-dir]";
|
|
150
|
+
let font = std::fs::read(args.next().ok_or(usage)?)?;
|
|
151
|
+
let photo = std::fs::read(args.next().ok_or(usage)?)?;
|
|
152
|
+
let out = PathBuf::from(args.next().unwrap_or_else(|| "target/examples-out".into()));
|
|
153
|
+
std::fs::create_dir_all(&out)?;
|
|
154
|
+
let a2b = build(Level::A2b, font.clone(), photo.clone())?;
|
|
155
|
+
std::fs::write(out.join("pdf_a_archive-2b.pdf"), a2b)?;
|
|
156
|
+
let a2u = build(Level::A2u, font, photo)?;
|
|
157
|
+
Ok(std::fs::write(out.join("pdf_a_archive-2u.pdf"), a2u)?)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Which archive to write.
|
|
161
|
+
#[derive(Debug, Clone, Copy)]
|
|
162
|
+
pub(crate) enum Level {
|
|
163
|
+
/// PDF/A-2b: the letter looks the same everywhere.
|
|
164
|
+
A2b,
|
|
165
|
+
/// PDF/A-2u, whose text maps to Unicode, and PDF/UA-1.
|
|
166
|
+
A2u,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// The letter's bytes, in `font` (a TrueType or OpenType file) with `photo` (a JPEG).
|
|
170
|
+
pub(crate) fn build(level: Level, font: Vec<u8>, photo: Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
171
|
+
let options = match level {
|
|
172
|
+
Level::A2b => BuildOptions::new().with_profile(Profile::PdfA2b),
|
|
173
|
+
Level::A2u => BuildOptions::new()
|
|
174
|
+
.with_profile(Profile::PdfA2u)
|
|
175
|
+
.with_profile(Profile::PdfUa1),
|
|
176
|
+
};
|
|
177
|
+
let mut b = Builder::new(options.with_tagged(true));
|
|
178
|
+
b.metadata(
|
|
179
|
+
Metadata::default()
|
|
180
|
+
.with_title("Order confirmation 42")
|
|
181
|
+
.with_author("Acme Widgets Ltd"),
|
|
182
|
+
);
|
|
183
|
+
b.lang("en-GB");
|
|
184
|
+
let font = b.font_bytes(font)?;
|
|
185
|
+
let photo = Image::jpeg(photo)?;
|
|
186
|
+
let mut page = b.page(PageSize::A4);
|
|
187
|
+
draw(page.surface(), &font, &photo)?;
|
|
188
|
+
page.finish()?;
|
|
189
|
+
let mut doc = b.finish()?;
|
|
190
|
+
Ok(doc.save(&SaveOptions::new())?.bytes)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fn draw(s: &mut Surface, font: &FontHandle, photo: &Image) -> Result<(), BuildError> {
|
|
194
|
+
// A translucent band across the top: decoration, and transparency, which PDF/A-2 allows.
|
|
195
|
+
let width = PageSize::A4.width.get();
|
|
196
|
+
s.artifact(ArtifactKind::Layout, |s| {
|
|
197
|
+
s.save()
|
|
198
|
+
.opacity(BAND_OPACITY, 1.0)
|
|
199
|
+
.fill_color(Color::rgb(0.12, 0.47, 0.78));
|
|
200
|
+
s.path(|p| _ = p.rect(RectPt::from_points(0.0, 0.0, width, BAND_HEIGHT)))
|
|
201
|
+
.fill(FillRule::NonZero);
|
|
202
|
+
s.restore();
|
|
203
|
+
Ok(())
|
|
204
|
+
})?;
|
|
205
|
+
s.tag(TagKind::H1, |s| {
|
|
206
|
+
s.text("Order confirmation 42", font, TITLE, TITLE_AT).map(drop)
|
|
207
|
+
})?;
|
|
208
|
+
let body = RectPt::from_points(MARGIN, BLOCK_TOP, LETTER_WIDTH, BLOCK_HEIGHT);
|
|
209
|
+
s.tag(TagKind::P, |s| {
|
|
210
|
+
s.text_box(LETTER, font, BODY, body, &TextBoxOptions::new()).map(drop)
|
|
211
|
+
})?;
|
|
212
|
+
let beside = RectPt::from_points(PHOTO_LEFT, BLOCK_TOP, PHOTO_WIDTH, BLOCK_HEIGHT);
|
|
213
|
+
s.figure(photo, beside, Some(PHOTO_ALT));
|
|
214
|
+
let footer = "Acme Widgets Ltd, 1 Example Street, London";
|
|
215
|
+
s.tag(TagKind::P, |s| s.text(footer, font, BODY, FOOTER_AT).map(drop))
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
[See the output](../../pages/recipes/pdf-a-archive.md).
|
|
220
|
+
|
|
221
|
+
## Attaching files: a hybrid invoice
|
|
222
|
+
|
|
223
|
+
PDF/A-3 lets a document carry other files, provided each one is associated with the document and says how it relates to it: its source, the data it was made from, or another representation of the same content. E-invoicing formats such as Factur-X and ZUGFeRD use this to put the machine-readable invoice (XML) inside the human-readable one (PDF).
|
|
224
|
+
|
|
225
|
+
`attach(name, bytes, mime, { relationship })` adds a file, its MIME type and its relationship, and lists it in the document's attachments. The example uses a made-up XML schema; a real e-invoice follows the EN 16931 standard and adds its own XMP metadata.
|
|
226
|
+
|
|
227
|
+
**TypeScript**
|
|
228
|
+
|
|
229
|
+
`packages/sdk/examples/pdf-a3-invoice-with-xml.ts`:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// A PDF/A-3b invoice that carries the same invoice as XML, attached for software to read.
|
|
233
|
+
import { Builder, Font, PAGE_SIZES, type Surface } from "@laminapdf/sdk";
|
|
234
|
+
|
|
235
|
+
const MARGIN = 56;
|
|
236
|
+
/** The page's right edge, less the margin: where the rule under the items ends. */
|
|
237
|
+
const RIGHT = PAGE_SIZES.A4[0] - MARGIN;
|
|
238
|
+
const AMOUNTS = 460;
|
|
239
|
+
const ROW = 22;
|
|
240
|
+
const TITLE_BASELINE = 80;
|
|
241
|
+
const FIRST_ROW = 130;
|
|
242
|
+
/**
|
|
243
|
+
* How far above the next row's baseline the rule sits, and how far below it the total and
|
|
244
|
+
* the note are.
|
|
245
|
+
*/
|
|
246
|
+
const RULE_RISE = 7;
|
|
247
|
+
const TOTAL_DROP = 18;
|
|
248
|
+
const NOTE_DROP = 68;
|
|
249
|
+
/** How far left of the amounts column the total starts, to fit its "Total" label. */
|
|
250
|
+
const TOTAL_INDENT = 40;
|
|
251
|
+
const TITLE = 24;
|
|
252
|
+
const TOTAL = 14;
|
|
253
|
+
const BODY = 11;
|
|
254
|
+
const ITEMS = [
|
|
255
|
+
["Sprockets", 12, 1.25],
|
|
256
|
+
["Gadget", 1, 79.9],
|
|
257
|
+
["Delivery", 1, 4.5],
|
|
258
|
+
] as const;
|
|
259
|
+
|
|
260
|
+
/** The invoice's total. */
|
|
261
|
+
function total(): number {
|
|
262
|
+
return ITEMS.reduce((sum, [, qty, price]) => sum + qty * price, 0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The invoice as XML, in a made-up schema (a real e-invoice follows EN 16931, as Factur-X does). */
|
|
266
|
+
function invoiceXml(): string {
|
|
267
|
+
const lines = ITEMS.map(
|
|
268
|
+
([item, qty, price]) => ` <line item="${item}" quantity="${qty}" price="${price.toFixed(2)}"/>\n`,
|
|
269
|
+
);
|
|
270
|
+
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`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The invoice's bytes. Needs `init({ text: true })`.
|
|
275
|
+
*
|
|
276
|
+
* @param notoSans - the bytes of the Noto Sans font file.
|
|
277
|
+
*/
|
|
278
|
+
export function pdfA3InvoiceWithXml(notoSans: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
279
|
+
using b = new Builder({ profiles: ["PDF/A-3b"] });
|
|
280
|
+
b.metadata({ title: "Invoice 42", author: "Acme Widgets Ltd" });
|
|
281
|
+
const font = Font.fromBytes(notoSans);
|
|
282
|
+
b.font(font);
|
|
283
|
+
const page = b.page("A4");
|
|
284
|
+
draw(page.surface(), font);
|
|
285
|
+
page.finish();
|
|
286
|
+
b.attach("invoice-42.xml", new TextEncoder().encode(invoiceXml()), "text/xml", {
|
|
287
|
+
relationship: "alternative",
|
|
288
|
+
description: "Invoice 42 as XML",
|
|
289
|
+
});
|
|
290
|
+
using doc = b.finish();
|
|
291
|
+
return doc.save();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function draw(s: Surface, font: Font): void {
|
|
295
|
+
s.text("Invoice 42", { font, size: TITLE, at: [MARGIN, TITLE_BASELINE] });
|
|
296
|
+
let y = FIRST_ROW;
|
|
297
|
+
for (const [item, qty, price] of ITEMS) {
|
|
298
|
+
s.text(`${qty} × ${item}`, { font, size: BODY, at: [MARGIN, y] });
|
|
299
|
+
s.text(`€ ${(qty * price).toFixed(2)}`, { font, size: BODY, at: [AMOUNTS, y] });
|
|
300
|
+
y += ROW;
|
|
301
|
+
}
|
|
302
|
+
const rule = y - RULE_RISE;
|
|
303
|
+
s.strokeColor([0.5])
|
|
304
|
+
.path((p) => p.moveTo(MARGIN, rule).lineTo(RIGHT, rule))
|
|
305
|
+
.stroke();
|
|
306
|
+
s.text(`Total € ${total().toFixed(2)}`, { font, size: TOTAL, at: [AMOUNTS - TOTAL_INDENT, y + TOTAL_DROP] });
|
|
307
|
+
s.text("The invoice is attached as invoice-42.xml.", {
|
|
308
|
+
font,
|
|
309
|
+
size: BODY,
|
|
310
|
+
at: [MARGIN, y + NOTE_DROP],
|
|
311
|
+
link: { target: { type: "uri", uri: "https://example.com/invoices/42" }, contents: "Invoice 42 online" },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Rust**
|
|
317
|
+
|
|
318
|
+
`crates/laminapdf/examples/pdf_a3_invoice_with_xml.rs`:
|
|
319
|
+
|
|
320
|
+
```rust
|
|
321
|
+
//! A PDF/A-3b invoice that carries the same invoice as XML, attached for software to read.
|
|
322
|
+
//!
|
|
323
|
+
//! Run it with `cargo run -p laminapdf --example pdf_a3_invoice_with_xml -- <font.ttf> [out-dir]`.
|
|
324
|
+
|
|
325
|
+
use std::error::Error;
|
|
326
|
+
use std::fmt::Write as _;
|
|
327
|
+
use std::path::PathBuf;
|
|
328
|
+
|
|
329
|
+
use laminapdf::{
|
|
330
|
+
AfRelationship, Attachment, BuildError, BuildOptions, Builder, Color, FontHandle, LinkTarget, Metadata, PageSize,
|
|
331
|
+
PointPt, Profile, Pt, SaveOptions, StrokeStyle, Surface,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const MARGIN: f64 = 56.0;
|
|
335
|
+
/// The page's right edge, less the margin: where the rule under the items ends.
|
|
336
|
+
const RIGHT: f64 = PageSize::A4.width.get() - MARGIN;
|
|
337
|
+
const AMOUNTS: f64 = 460.0;
|
|
338
|
+
const ROW: f64 = 22.0;
|
|
339
|
+
const TITLE_BASELINE: f64 = 80.0;
|
|
340
|
+
const FIRST_ROW: f64 = 130.0;
|
|
341
|
+
/// How far above the next row's baseline the rule sits, and how far below it the total and
|
|
342
|
+
/// the note are.
|
|
343
|
+
const RULE_RISE: f64 = 7.0;
|
|
344
|
+
const TOTAL_DROP: f64 = 18.0;
|
|
345
|
+
const NOTE_DROP: f64 = 68.0;
|
|
346
|
+
/// How far left of the amounts column the total starts, to fit its "Total" label.
|
|
347
|
+
const TOTAL_INDENT: f64 = 40.0;
|
|
348
|
+
const TITLE: Pt = Pt::new(24.0);
|
|
349
|
+
const TOTAL: Pt = Pt::new(14.0);
|
|
350
|
+
const BODY: Pt = Pt::new(11.0);
|
|
351
|
+
const ITEMS: [(&str, u32, f64); 3] = [("Sprockets", 12, 1.25), ("Gadget", 1, 79.90), ("Delivery", 1, 4.50)];
|
|
352
|
+
|
|
353
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
354
|
+
let mut args = std::env::args().skip(1);
|
|
355
|
+
let font = std::fs::read(args.next().ok_or("arguments: <font.ttf> [out-dir]")?)?;
|
|
356
|
+
let out = PathBuf::from(args.next().unwrap_or_else(|| "target/examples-out".into()));
|
|
357
|
+
std::fs::create_dir_all(&out)?;
|
|
358
|
+
Ok(std::fs::write(out.join("pdf_a3_invoice_with_xml.pdf"), build(font)?)?)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// The invoice's total.
|
|
362
|
+
fn total() -> f64 {
|
|
363
|
+
ITEMS.iter().map(|(_, qty, price)| f64::from(*qty) * price).sum()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// The invoice as XML, in a made-up schema (a real e-invoice follows EN 16931, as Factur-X does).
|
|
367
|
+
fn invoice_xml() -> Result<String, std::fmt::Error> {
|
|
368
|
+
let mut xml =
|
|
369
|
+
String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<invoice number=\"42\" currency=\"EUR\">\n");
|
|
370
|
+
for (item, qty, price) in ITEMS {
|
|
371
|
+
writeln!(xml, " <line item=\"{item}\" quantity=\"{qty}\" price=\"{price:.2}\"/>")?;
|
|
372
|
+
}
|
|
373
|
+
writeln!(xml, " <total>{:.2}</total>\n</invoice>", total())?;
|
|
374
|
+
Ok(xml)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// The invoice's bytes, in `font` (a TrueType or OpenType file).
|
|
378
|
+
pub(crate) fn build(font: Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
379
|
+
let mut b = Builder::new(BuildOptions::new().with_profile(Profile::PdfA3b));
|
|
380
|
+
b.metadata(
|
|
381
|
+
Metadata::default()
|
|
382
|
+
.with_title("Invoice 42")
|
|
383
|
+
.with_author("Acme Widgets Ltd"),
|
|
384
|
+
);
|
|
385
|
+
let font = b.font_bytes(font)?;
|
|
386
|
+
let mut page = b.page(PageSize::A4);
|
|
387
|
+
draw(page.surface(), &font)?;
|
|
388
|
+
page.finish()?;
|
|
389
|
+
let xml = Attachment::new("invoice-42.xml", invoice_xml()?.into_bytes(), "text/xml")
|
|
390
|
+
.with_relationship(AfRelationship::Alternative)
|
|
391
|
+
.with_description("Invoice 42 as XML");
|
|
392
|
+
b.attach(xml);
|
|
393
|
+
let mut doc = b.finish()?;
|
|
394
|
+
Ok(doc.save(&SaveOptions::new())?.bytes)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
fn draw(s: &mut Surface, font: &FontHandle) -> Result<(), BuildError> {
|
|
398
|
+
let at = PointPt::from_points;
|
|
399
|
+
s.text("Invoice 42", font, TITLE, at(MARGIN, TITLE_BASELINE))?;
|
|
400
|
+
let mut y = FIRST_ROW;
|
|
401
|
+
for (item, qty, price) in ITEMS {
|
|
402
|
+
s.text(&format!("{qty} × {item}"), font, BODY, at(MARGIN, y))?;
|
|
403
|
+
let amount = f64::from(qty) * price;
|
|
404
|
+
s.text(&format!("€ {amount:.2}"), font, BODY, at(AMOUNTS, y))?;
|
|
405
|
+
y += ROW;
|
|
406
|
+
}
|
|
407
|
+
let rule = y - RULE_RISE;
|
|
408
|
+
s.stroke_color(Color::gray(0.5))
|
|
409
|
+
.path(|p| _ = p.move_to(at(MARGIN, rule)).line_to(at(RIGHT, rule)))
|
|
410
|
+
.stroke(&StrokeStyle::default());
|
|
411
|
+
let total_at = at(AMOUNTS - TOTAL_INDENT, y + TOTAL_DROP);
|
|
412
|
+
s.text(&format!("Total € {:.2}", total()), font, TOTAL, total_at)?;
|
|
413
|
+
let note = "The invoice is attached as invoice-42.xml.";
|
|
414
|
+
let note = s.text(note, font, BODY, at(MARGIN, y + NOTE_DROP))?;
|
|
415
|
+
let online = LinkTarget::Uri("https://example.com/invoices/42".into());
|
|
416
|
+
note.link(online, Some("Invoice 42 online".into()));
|
|
417
|
+
Ok(())
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
[See the output](../../pages/recipes/pdf-a3-invoice-with-xml.md).
|
|
422
|
+
|
|
423
|
+
## How Lamina PDF checks its own output
|
|
424
|
+
|
|
425
|
+
Every example on this site that claims a PDF/A profile is validated each night with veraPDF, the open-source PDF/A and PDF/UA validator the PDF Association backs.
|
|
426
|
+
|
|
427
|
+
> **Details:** PDF/A is ISO 19005: PDF/A-2 is part 2 and PDF/A-3 part 3. PDF/A-2, PDF/A-3 and PDF/UA-1 all build on PDF 1.7, which is why a PDF 2.0 document can't claim any of them.
|
|
428
|
+
|
|
429
|
+
Next: open a PDF and change it, in [Edit a PDF](../../pages/guides/edit-a-pdf.md).
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Edit a PDF"
|
|
3
|
+
description: "Change an existing PDF in transactions you can undo, and replay edits stored as JSON."
|
|
4
|
+
version: "0.2.0-alpha.3"
|
|
5
|
+
url: "https://docs.laminapdf.com/guides/edit-a-pdf/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Edit a PDF
|
|
9
|
+
|
|
10
|
+
Change an existing PDF in transactions you can undo, and replay edits stored as JSON.
|
|
11
|
+
|
|
12
|
+
You'll open a PDF, change its title and author, and save the edit after the original bytes. You need this whenever your app changes a file someone gave it: a corrected title, a rotated scan, a report marked final.
|
|
13
|
+
|
|
14
|
+
This example generates a draft report, opens its bytes and retitles it in one transaction. Then it saves incrementally, which keeps the draft's bytes at the start of the result, unchanged:
|
|
15
|
+
|
|
16
|
+
**TypeScript**
|
|
17
|
+
|
|
18
|
+
`packages/sdk/examples/generate-then-edit.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// Generate a document, then open its bytes, edit them and save incrementally. The edit changes
|
|
22
|
+
// the Info dictionary in one transaction, and the incremental save keeps the generated bytes as
|
|
23
|
+
// they are, with one revision appended.
|
|
24
|
+
import { Builder, type BuilderOptions, Document, Font } from "@laminapdf/sdk";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The generated file followed by the edit's revision. Needs `init({ text: true })`.
|
|
28
|
+
*
|
|
29
|
+
* @param options - can fix the file ID, the producer and the creation date.
|
|
30
|
+
* @param editedAt - the modification date to record, as a PDF date string.
|
|
31
|
+
*/
|
|
32
|
+
export function generateThenEdit(options: BuilderOptions = {}, editedAt?: string): Uint8Array<ArrayBuffer> {
|
|
33
|
+
return edit(generate(options), editedAt);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A one-page draft, saved (a new document is always saved as a full rewrite). */
|
|
37
|
+
export function generate(options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
|
|
38
|
+
using b = new Builder(options);
|
|
39
|
+
const page = b.page("A4");
|
|
40
|
+
page.surface().text("Draft report", { font: Font.standard("Helvetica"), size: 20, at: [72, 96] });
|
|
41
|
+
page.finish();
|
|
42
|
+
b.metadata({ title: "Draft report" });
|
|
43
|
+
using doc = b.finish();
|
|
44
|
+
return doc.save();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Opens `bytes`, edits the Info dictionary in one transaction (which `undo` could revert) and saves incrementally. */
|
|
48
|
+
function edit(bytes: Uint8Array, editedAt?: string): Uint8Array<ArrayBuffer> {
|
|
49
|
+
using doc = Document.open(bytes);
|
|
50
|
+
doc.transact((tx) => tx.setInfo({ title: "Final report", author: "Ada Lovelace" }, editedAt));
|
|
51
|
+
return doc.save({ mode: "incremental" });
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Rust**
|
|
56
|
+
|
|
57
|
+
`crates/laminapdf/examples/generate_then_edit.rs`:
|
|
58
|
+
|
|
59
|
+
````rust
|
|
60
|
+
//! Generate a document, then open its bytes, edit them and save incrementally.
|
|
61
|
+
//!
|
|
62
|
+
//! The edit changes the Info dictionary with an op (undoable and serialisable), and the
|
|
63
|
+
//! incremental save keeps the generated bytes as they are, with one revision appended.
|
|
64
|
+
//!
|
|
65
|
+
//! ```sh
|
|
66
|
+
//! cargo run -p laminapdf --example generate_then_edit -- [out-dir] # writes <out-dir>/generate_then_edit.pdf
|
|
67
|
+
//! ```
|
|
68
|
+
|
|
69
|
+
use std::error::Error;
|
|
70
|
+
use std::path::PathBuf;
|
|
71
|
+
|
|
72
|
+
use laminapdf::{
|
|
73
|
+
BuildOptions, Builder, Document, InfoPatch, Metadata, Op, OpenOptions, PageSize, PdfDate, PointPt, Pt, SaveMode,
|
|
74
|
+
SaveOptions, SetInfo, Standard14,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
78
|
+
let out = PathBuf::from(std::env::args().nth(1).unwrap_or_else(|| "target/examples-out".into()));
|
|
79
|
+
std::fs::create_dir_all(&out)?;
|
|
80
|
+
let bytes = build(BuildOptions::new(), None)?;
|
|
81
|
+
std::fs::write(out.join("generate_then_edit.pdf"), bytes)?;
|
|
82
|
+
Ok(())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// The generated file followed by the edit's revision. `edited_at` is recorded as the
|
|
86
|
+
/// modification date; `options` can fix the file ID, the producer and the creation date.
|
|
87
|
+
pub(crate) fn build(options: BuildOptions, edited_at: Option<PdfDate>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
88
|
+
let generated = generate(options)?;
|
|
89
|
+
edit(generated, edited_at)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// A one-page draft, saved (a new document is always saved as a full rewrite).
|
|
93
|
+
pub(crate) fn generate(options: BuildOptions) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
94
|
+
let mut b = Builder::new(options);
|
|
95
|
+
let helvetica = b.standard_font(Standard14::Helvetica)?;
|
|
96
|
+
let mut page = b.page(PageSize::A4);
|
|
97
|
+
page.surface().text(
|
|
98
|
+
"Draft report",
|
|
99
|
+
&helvetica,
|
|
100
|
+
Pt::new(20.0),
|
|
101
|
+
PointPt::from_points(72.0, 96.0),
|
|
102
|
+
)?;
|
|
103
|
+
page.finish()?;
|
|
104
|
+
b.metadata(Metadata::default().with_title("Draft report"));
|
|
105
|
+
Ok(b.finish()?.save(&SaveOptions::new())?.bytes)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Opens `bytes`, edits the Info dictionary in one transaction (which `undo` could revert) and
|
|
109
|
+
/// saves incrementally: `bytes` unchanged, then the appended revision.
|
|
110
|
+
fn edit(bytes: Vec<u8>, edited_at: Option<PdfDate>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
111
|
+
let mut doc = Document::open_bytes(bytes, OpenOptions::new())?;
|
|
112
|
+
let patch = InfoPatch::new()
|
|
113
|
+
.title(Some("Final report"))
|
|
114
|
+
.author(Some("Ada Lovelace"));
|
|
115
|
+
doc.apply(vec![Op::from(SetInfo::new(patch, edited_at))])?;
|
|
116
|
+
let options = SaveOptions::new().with_mode(SaveMode::Incremental);
|
|
117
|
+
Ok(doc.save(&options)?.bytes)
|
|
118
|
+
}
|
|
119
|
+
````
|
|
120
|
+
|
|
121
|
+
[See the output](../../pages/recipes/generate-then-edit.md).
|
|
122
|
+
|
|
123
|
+
Your edits live beside the bytes you opened and never change them. Nothing reaches a file until you save. [How Lamina PDF thinks about a PDF](../../pages/concepts/how-lamina-thinks.md) explains why it works this way.
|
|
124
|
+
|
|
125
|
+
## Group edits into a transaction
|
|
126
|
+
|
|
127
|
+
`doc.transact(tx => …)` collects edits, such as `setInfo` and `rotatePage`, and applies them together when the callback returns. If the callback throws, none of them apply. The callback is synchronous, and editing or saving the same document inside it fails with `nested-transaction`.
|
|
128
|
+
|
|
129
|
+
## Undo and redo
|
|
130
|
+
|
|
131
|
+
`doc.undo()` reverts the last transaction as a whole, and `doc.redo()` applies it again. A transaction is the unit of undo, so group the edits a user would expect to undo together.
|
|
132
|
+
|
|
133
|
+
## Replay edits stored as JSON
|
|
134
|
+
|
|
135
|
+
Every edit is also a plain JSON object, such as `{ op: "rotatePage", args: { … } }`. So you can store edits, send them to a server and replay them later. `doc.apply(ops)` applies a list of them as one transaction, and `doc.applyLog({ v: 1, ops })` replays a saved log.
|
|
136
|
+
|
|
137
|
+
Next: [save a PDF](../../pages/guides/save-a-pdf.md).
|