@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,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Images and vectors"
|
|
3
|
+
description: "Shapes, a dashed curve, gray, RGB, CMYK and hex colour, transparency, a blend mode, a clip, a rotation, a JPEG and an RGBA image on one page."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/images-and-vectors/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Images and vectors
|
|
9
|
+
|
|
10
|
+
Shapes, a dashed curve, gray, RGB, CMYK and hex colour, transparency, a blend mode, a clip, a rotation, a JPEG and an RGBA image on one page.
|
|
11
|
+
|
|
12
|
+
You'll draw shapes and images on one A4 page: a rectangle, a circle, a dashed curve, two see-through discs, a photo cut to an ellipse and a rotated gradient that fades out. Start from it for a logo, a chart, the rules of a table or a page with photos.
|
|
13
|
+
|
|
14
|
+
The function takes a JPEG's bytes, draws the page and returns the PDF's bytes. It draws no text, so in TypeScript it needs only the base module, `init()`. Both languages write the same bytes:
|
|
15
|
+
|
|
16
|
+
**TypeScript**
|
|
17
|
+
|
|
18
|
+
`packages/sdk/examples/images-and-vectors.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// Images and vector graphics on one A4 page. It draws shapes and a dashed Bézier curve in gray,
|
|
22
|
+
// RGB, CMYK and hex colour, overlaps two translucent discs with a blend mode, clips a JPEG to an
|
|
23
|
+
// ellipse and rotates an RGBA image with a soft mask. Needs only the base module.
|
|
24
|
+
import { Builder, type BuilderOptions, Image, Matrix, type Surface } from "@laminapdf/sdk";
|
|
25
|
+
|
|
26
|
+
/** The RGBA gradient's side, in pixels. */
|
|
27
|
+
const GLOW_SIDE = 16;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The document's bytes, with `photo` (a JPEG) on the page.
|
|
31
|
+
*
|
|
32
|
+
* @param options - can fix the file ID, the producer and the creation date.
|
|
33
|
+
*/
|
|
34
|
+
export function imagesAndVectors(photo: Uint8Array, options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
|
|
35
|
+
using b = new Builder(options);
|
|
36
|
+
const jpeg = Image.jpeg(photo);
|
|
37
|
+
const glow = Image.rgba(GLOW_SIDE, GLOW_SIDE, gradient());
|
|
38
|
+
const page = b.page("A4");
|
|
39
|
+
const s = page.surface();
|
|
40
|
+
shapes(s);
|
|
41
|
+
overlappingDiscs(s);
|
|
42
|
+
images(s, jpeg, glow);
|
|
43
|
+
page.finish();
|
|
44
|
+
using doc = b.finish();
|
|
45
|
+
return doc.save();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A square gradient whose alpha falls off towards the right. */
|
|
49
|
+
function gradient(): Uint8Array {
|
|
50
|
+
const out = new Uint8Array(GLOW_SIDE * GLOW_SIDE * 4);
|
|
51
|
+
for (let y = 0; y < GLOW_SIDE; y++) {
|
|
52
|
+
for (let x = 0; x < GLOW_SIDE; x++) out.set([x * 16, y * 16, 200, 255 - x * 15], (y * GLOW_SIDE + x) * 4);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A rectangle, a circle and a dashed, round-capped Bézier curve. */
|
|
58
|
+
function shapes(s: Surface): void {
|
|
59
|
+
s.fillColor([0.9, 0.3, 0.2])
|
|
60
|
+
.path((p) => p.rect([56, 56, 160, 100]))
|
|
61
|
+
.fill();
|
|
62
|
+
s.fillColor([0.8, 0.1, 0, 0])
|
|
63
|
+
.path((p) => p.circle([330, 106], 50))
|
|
64
|
+
.fill();
|
|
65
|
+
s.strokeColor("#2a9d8f")
|
|
66
|
+
.path((p) => p.moveTo(56, 220).cubicTo(180, 140, 300, 300, 539, 200))
|
|
67
|
+
.stroke({ width: 3, cap: "round", dash: [12, 8] });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Two translucent discs, the second multiplied onto the first. */
|
|
71
|
+
function overlappingDiscs(s: Surface): void {
|
|
72
|
+
s.save().opacity(0.6, 1).fillColor([0.3]);
|
|
73
|
+
s.path((p) => p.circle([120, 330], 60)).fill();
|
|
74
|
+
s.blendMode("multiply").fillColor([1, 0.8, 0]);
|
|
75
|
+
s.path((p) => p.circle([190, 330], 60))
|
|
76
|
+
.fill()
|
|
77
|
+
.restore();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The photo clipped to an ellipse, and the gradient rotated by 30 degrees. */
|
|
81
|
+
function images(s: Surface, photo: Image, glow: Image): void {
|
|
82
|
+
s.save()
|
|
83
|
+
.path((p) => p.ellipse([300, 270, 239, 120]))
|
|
84
|
+
.clip();
|
|
85
|
+
s.image(photo, [300, 250, 239, 160]).restore();
|
|
86
|
+
s.save().transform(Matrix.after(Matrix.translate(150, 520), Matrix.rotate(30)));
|
|
87
|
+
s.image(glow, [-60, -60, 120, 120]).restore();
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Rust**
|
|
92
|
+
|
|
93
|
+
`crates/laminapdf/examples/images_and_vectors.rs`:
|
|
94
|
+
|
|
95
|
+
````rust
|
|
96
|
+
//! Images and vector graphics on one A4 page.
|
|
97
|
+
//!
|
|
98
|
+
//! It draws shapes and a dashed Bézier curve in gray, RGB, CMYK and hex colour, overlaps two
|
|
99
|
+
//! translucent discs with a blend mode, clips a JPEG to an ellipse and rotates an RGBA image
|
|
100
|
+
//! with a soft mask.
|
|
101
|
+
//!
|
|
102
|
+
//! ```sh
|
|
103
|
+
//! cargo run -p laminapdf --example images_and_vectors -- photo.jpg [out-dir] # writes images_and_vectors.pdf
|
|
104
|
+
//! ```
|
|
105
|
+
|
|
106
|
+
use std::error::Error;
|
|
107
|
+
use std::path::PathBuf;
|
|
108
|
+
|
|
109
|
+
use laminapdf::{
|
|
110
|
+
BlendMode, BuildOptions, Builder, Color, Dash, FillRule, Image, LineCap, Matrix, PageSize, PathBuilder, PointPt,
|
|
111
|
+
Pt, RectPt, SaveOptions, StrokeStyle, Surface,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/// The RGBA gradient's side, in pixels.
|
|
115
|
+
const GLOW_SIDE: u8 = 16;
|
|
116
|
+
|
|
117
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
118
|
+
let mut args = std::env::args().skip(1);
|
|
119
|
+
let photo = std::fs::read(args.next().ok_or("usage: images_and_vectors <photo.jpg> [out-dir]")?)?;
|
|
120
|
+
let out = PathBuf::from(args.next().unwrap_or_else(|| "target/examples-out".into()));
|
|
121
|
+
std::fs::create_dir_all(&out)?;
|
|
122
|
+
std::fs::write(out.join("images_and_vectors.pdf"), build(BuildOptions::new(), photo)?)?;
|
|
123
|
+
Ok(())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// The document's bytes, with `photo` (a JPEG) on the page. `options` can fix the file ID, the
|
|
127
|
+
/// producer and the creation date.
|
|
128
|
+
pub(crate) fn build(options: BuildOptions, photo: Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
129
|
+
let mut b = Builder::new(options);
|
|
130
|
+
let photo = Image::jpeg(photo)?;
|
|
131
|
+
let glow = Image::rgba(u32::from(GLOW_SIDE), u32::from(GLOW_SIDE), &gradient())?;
|
|
132
|
+
let mut page = b.page(PageSize::A4);
|
|
133
|
+
let s = page.surface();
|
|
134
|
+
shapes(s)?;
|
|
135
|
+
overlapping_discs(s);
|
|
136
|
+
images(s, &photo, &glow);
|
|
137
|
+
page.finish()?;
|
|
138
|
+
let mut doc = b.finish()?;
|
|
139
|
+
Ok(doc.save(&SaveOptions::new())?.bytes)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// A square gradient whose alpha falls off towards the right.
|
|
143
|
+
fn gradient() -> Vec<u8> {
|
|
144
|
+
(0..GLOW_SIDE)
|
|
145
|
+
.flat_map(|y| (0..GLOW_SIDE).flat_map(move |x| [x * 16, y * 16, 200, 255 - x * 15]))
|
|
146
|
+
.collect()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// A rectangle, a circle and a dashed, round-capped Bézier curve.
|
|
150
|
+
fn shapes(s: &mut Surface) -> Result<(), Box<dyn Error>> {
|
|
151
|
+
let at = PointPt::from_points;
|
|
152
|
+
s.fill_color(Color::rgb(0.9, 0.3, 0.2))
|
|
153
|
+
.path(|p| _ = p.rect(RectPt::from_points(56.0, 56.0, 160.0, 100.0)))
|
|
154
|
+
.fill(FillRule::NonZero);
|
|
155
|
+
s.fill_color(Color::cmyk(0.8, 0.1, 0.0, 0.0))
|
|
156
|
+
.path(|p| _ = p.circle(at(330.0, 106.0), Pt::new(50.0)))
|
|
157
|
+
.fill(FillRule::NonZero);
|
|
158
|
+
let dashed = StrokeStyle {
|
|
159
|
+
width: Pt::new(3.0),
|
|
160
|
+
cap: LineCap::Round,
|
|
161
|
+
dash: Some(Dash::new(vec![12.0, 8.0], 0.0)),
|
|
162
|
+
..StrokeStyle::default()
|
|
163
|
+
};
|
|
164
|
+
s.stroke_color(Color::hex("#2a9d8f")?)
|
|
165
|
+
.path(|p| {
|
|
166
|
+
p.move_to(at(56.0, 220.0))
|
|
167
|
+
.cubic_to(at(180.0, 140.0), at(300.0, 300.0), at(539.0, 200.0));
|
|
168
|
+
})
|
|
169
|
+
.stroke(&dashed);
|
|
170
|
+
Ok(())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Two translucent discs, the second multiplied onto the first.
|
|
174
|
+
fn overlapping_discs(s: &mut Surface) {
|
|
175
|
+
let disc = |x: f64| move |p: &mut PathBuilder| _ = p.circle(PointPt::from_points(x, 330.0), Pt::new(60.0));
|
|
176
|
+
s.save().opacity(0.6, 1.0).fill_color(Color::gray(0.3));
|
|
177
|
+
s.path(disc(120.0)).fill(FillRule::NonZero);
|
|
178
|
+
s.blend_mode(BlendMode::Multiply).fill_color(Color::rgb(1.0, 0.8, 0.0));
|
|
179
|
+
s.path(disc(190.0)).fill(FillRule::NonZero).restore();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// The photo clipped to an ellipse, and the gradient rotated by 30 degrees.
|
|
183
|
+
fn images(s: &mut Surface, photo: &Image, glow: &Image) {
|
|
184
|
+
s.save()
|
|
185
|
+
.path(|p| _ = p.ellipse(RectPt::from_points(300.0, 270.0, 239.0, 120.0)))
|
|
186
|
+
.clip(FillRule::NonZero);
|
|
187
|
+
s.image(photo, RectPt::from_points(300.0, 250.0, 239.0, 160.0))
|
|
188
|
+
.restore();
|
|
189
|
+
let placement = Matrix::translate(150.0, 520.0).after(&Matrix::rotate(30.0));
|
|
190
|
+
s.save().transform(placement);
|
|
191
|
+
s.image(glow, RectPt::from_points(-60.0, -60.0, 120.0, 120.0)).restore();
|
|
192
|
+
}
|
|
193
|
+
````
|
|
194
|
+
|
|
195
|
+
**Result:** [images_and_vectors.pdf](https://docs.laminapdf.com/examples/pdf/images_and_vectors.pdf)
|
|
196
|
+
|
|
197
|
+
The result is one A4 page with every shape and both images. The Result tab shows it, with a link to the PDF.
|
|
198
|
+
|
|
199
|
+
## How it works
|
|
200
|
+
|
|
201
|
+
- `path(p => …)` builds a shape and returns it unpainted. `fill` and `stroke` paint it, and `clip` limits what's drawn next to its inside. See [Shapes, colour and images](../../pages/guides/images-and-vectors.md).
|
|
202
|
+
- A colour of one, three or four numbers from 0 to 1 is gray, RGB or CMYK, and a string such as `"#2a9d8f"` is hex. Rust has `Color::gray`, `rgb`, `cmyk` and `hex`.
|
|
203
|
+
- `save()` and `restore()` bracket each group of drawings, so the opacity, blend mode, clip and rotation set inside stop at `restore()`.
|
|
204
|
+
- Both discs are 60% opaque, and `multiply` darkens the area where the second disc covers the first.
|
|
205
|
+
- `Image.jpeg` embeds the photo as it is, without decoding it. `Image.rgba` takes raw pixels, and their alpha becomes a soft mask: a second image that holds the transparency.
|
|
206
|
+
- The matrix turns the gradient 30 degrees clockwise, then moves it to (150, 520). The gradient is drawn centred on the origin, so it turns about its own centre. Like every position, the matrix works in [page space](../../pages/reference/glossary.md#page-space).
|
|
207
|
+
|
|
208
|
+
## What to change
|
|
209
|
+
|
|
210
|
+
- **A PNG instead of the JPEG:** `Image.png(bytes)` (`Image::png` in Rust) keeps the PNG's transparency.
|
|
211
|
+
- **A shape with a hole:** draw both outlines in one path and fill it with `"evenOdd"` (`FillRule::EvenOdd`), so the inner shape stays empty.
|
|
212
|
+
- **A filled shape with an outline:** `fillStroke(rule, style)` (`fill_stroke` in Rust) paints both at once.
|
|
213
|
+
- **Another blend mode:** `"screen"` lightens where `"multiply"` darkens. `BlendMode` lists all sixteen.
|
|
214
|
+
- **Alternate text:** pass a description as the third argument of `image` (`figure` in Rust), and a screen reader reads it out. See [Accessible PDFs](../../pages/guides/accessible-pdfs.md).
|
|
215
|
+
|
|
216
|
+
Next: [links and outlines](../../pages/recipes/links-and-outlines.md), with four pages joined by links, an outline and page labels.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "An invoice"
|
|
3
|
+
description: "A tagged one-page invoice with an embedded font, a table of line items with right-aligned amounts and a total, in TypeScript and Rust."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/invoice/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An invoice
|
|
9
|
+
|
|
10
|
+
A tagged one-page invoice with an embedded font, a table of line items with right-aligned amounts and a total, in TypeScript and Rust.
|
|
11
|
+
|
|
12
|
+
You'll make a one-page invoice: a heading, an address, a table of line items with right-aligned amounts, and a total. It's tagged, so a screen reader reads it in order. Start from it for any document built around a table, such as a quote or a receipt.
|
|
13
|
+
|
|
14
|
+
The function takes a font file, draws the invoice and returns the PDF's bytes. In TypeScript it needs the text module, `init({ text: true })`. Both languages write the same bytes:
|
|
15
|
+
|
|
16
|
+
**TypeScript**
|
|
17
|
+
|
|
18
|
+
`packages/sdk/examples/invoice.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// An invoice that screen readers can follow, with an embedded font and a table of line items.
|
|
22
|
+
// The heading, the address, the table of items and the total are tagged, so a screen reader
|
|
23
|
+
// reads them in order.
|
|
24
|
+
import { Builder, type BuilderOptions, Font, PAGE_SIZES, type Rect, type Surface } from "@laminapdf/sdk";
|
|
25
|
+
|
|
26
|
+
const ITEMS: readonly (readonly [item: string, qty: number, price: number])[] = [
|
|
27
|
+
["Widgets, blue (W-100)", 3, 12.0],
|
|
28
|
+
["Gadget with fine-tuned dials", 1, 79.9],
|
|
29
|
+
["Café-grade sprockets", 12, 1.25],
|
|
30
|
+
["Delivery", 1, 4.5],
|
|
31
|
+
];
|
|
32
|
+
const ADDRESS = "Acme Widgets Ltd · 1 Example Street · London";
|
|
33
|
+
const PAGE = "A4";
|
|
34
|
+
const [PAGE_WIDTH] = PAGE_SIZES[PAGE];
|
|
35
|
+
const MARGIN = 56;
|
|
36
|
+
const RIGHT_EDGE = PAGE_WIDTH - MARGIN;
|
|
37
|
+
const TITLE_AT = [MARGIN, 80] as const;
|
|
38
|
+
const ADDRESS_AT = [MARGIN, 100] as const;
|
|
39
|
+
const TITLE = 24;
|
|
40
|
+
const BODY = 11;
|
|
41
|
+
const FIRST_ROW = 160;
|
|
42
|
+
const ROW_HEIGHT = 24;
|
|
43
|
+
/** The quantity's and the amount's columns, each a width ending this far left of the right edge. */
|
|
44
|
+
const COLUMNS = [
|
|
45
|
+
[40, 169],
|
|
46
|
+
[100, 0],
|
|
47
|
+
] as const;
|
|
48
|
+
/** A cell's box starts this far above its row's baseline and is this tall. */
|
|
49
|
+
const CELL_RISE = 12;
|
|
50
|
+
const CELL_HEIGHT = 16;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The document's bytes, in `fontBytes` (a TrueType or OpenType file). Needs `init({ text: true })`.
|
|
54
|
+
*
|
|
55
|
+
* @param options - can fix the file ID, the producer and the creation date.
|
|
56
|
+
*/
|
|
57
|
+
export function invoice(fontBytes: Uint8Array, options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
|
|
58
|
+
using b = new Builder({ ...options, tagged: true });
|
|
59
|
+
const font = Font.fromBytes(fontBytes);
|
|
60
|
+
const bold = Font.standard("Helvetica-Bold");
|
|
61
|
+
// Registered up front, in this order, so the embedded font is the document's first.
|
|
62
|
+
b.font(font);
|
|
63
|
+
b.font(bold);
|
|
64
|
+
const page = b.page(PAGE);
|
|
65
|
+
const s = page.surface();
|
|
66
|
+
s.text("Invoice 42", { font: bold, size: TITLE, at: TITLE_AT, tag: "H1" });
|
|
67
|
+
s.text(ADDRESS, { font, size: BODY, at: ADDRESS_AT, tag: "P" });
|
|
68
|
+
items(s, font);
|
|
69
|
+
const total = ITEMS.reduce((sum, [, qty, price]) => sum + qty * price, 0);
|
|
70
|
+
const box = cell(COLUMNS[1], rowBaseline(ITEMS.length));
|
|
71
|
+
s.textBox(`Total € ${total.toFixed(2)}`, { font, size: BODY, box, align: "end", tag: "P" });
|
|
72
|
+
page.finish();
|
|
73
|
+
b.metadata({ title: "Invoice 42", author: "Acme Widgets Ltd" }).lang("en-GB");
|
|
74
|
+
using doc = b.finish();
|
|
75
|
+
return doc.save();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The table of line items: what, how many, and the amount, right-aligned. */
|
|
79
|
+
function items(s: Surface, font: Font): void {
|
|
80
|
+
s.tag("Table", (s) => {
|
|
81
|
+
ITEMS.forEach(([item, qty, price], row) => {
|
|
82
|
+
const y = rowBaseline(row);
|
|
83
|
+
const cells = [
|
|
84
|
+
[String(qty), COLUMNS[0]],
|
|
85
|
+
[`€ ${(qty * price).toFixed(2)}`, COLUMNS[1]],
|
|
86
|
+
] as const;
|
|
87
|
+
s.tag("TR", (s) => {
|
|
88
|
+
s.text(item, { font, size: BODY, at: [MARGIN, y], tag: "TD" });
|
|
89
|
+
for (const [text, column] of cells) {
|
|
90
|
+
s.textBox(text, { font, size: BODY, box: cell(column, y), align: "end", tag: "TD" });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The baseline of row `row` of the table. */
|
|
98
|
+
function rowBaseline(row: number): number {
|
|
99
|
+
return FIRST_ROW + ROW_HEIGHT * row;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The box of a cell in `column` (width, distance from the right edge) on the row at `baseline`. */
|
|
103
|
+
function cell([width, inset]: readonly [number, number], baseline: number): Rect {
|
|
104
|
+
return [RIGHT_EDGE - inset - width, baseline - CELL_RISE, width, CELL_HEIGHT];
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Rust**
|
|
109
|
+
|
|
110
|
+
`crates/laminapdf/examples/invoice.rs`:
|
|
111
|
+
|
|
112
|
+
```rust
|
|
113
|
+
//! An invoice that screen readers can follow, with an embedded font and a table of line items.
|
|
114
|
+
//!
|
|
115
|
+
//! The heading, the address, the table of items and the total are tagged, so a screen reader
|
|
116
|
+
//! reads them in order. Run it with `cargo run -p laminapdf --example invoice -- <font.ttf> [out-dir]`.
|
|
117
|
+
|
|
118
|
+
use laminapdf::{
|
|
119
|
+
Alignment, BuildError, BuildOptions, Builder, FontHandle, Metadata, PageSize, PointPt, Pt, RectPt, SaveOptions,
|
|
120
|
+
Standard14, Surface, TagKind, TextBoxOptions,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ITEMS: [(&str, u32, f64); 4] = [
|
|
124
|
+
("Widgets, blue (W-100)", 3, 12.00),
|
|
125
|
+
("Gadget with fine-tuned dials", 1, 79.90),
|
|
126
|
+
("Café-grade sprockets", 12, 1.25),
|
|
127
|
+
("Delivery", 1, 4.50),
|
|
128
|
+
];
|
|
129
|
+
const ADDRESS: &str = "Acme Widgets Ltd · 1 Example Street · London";
|
|
130
|
+
const PAGE: PageSize = PageSize::A4;
|
|
131
|
+
const MARGIN: f64 = 56.0;
|
|
132
|
+
const RIGHT_EDGE: f64 = PAGE.width.get() - MARGIN;
|
|
133
|
+
const TITLE_AT: PointPt = PointPt::from_points(MARGIN, 80.0);
|
|
134
|
+
const ADDRESS_AT: PointPt = PointPt::from_points(MARGIN, 100.0);
|
|
135
|
+
const TITLE: Pt = Pt::new(24.0);
|
|
136
|
+
const BODY: Pt = Pt::new(11.0);
|
|
137
|
+
const FIRST_ROW: f64 = 160.0;
|
|
138
|
+
const ROW_HEIGHT: f64 = 24.0;
|
|
139
|
+
/// The quantity's and the amount's columns, each a width ending this far left of the right edge.
|
|
140
|
+
const COLUMNS: [(f64, f64); 2] = [(40.0, 169.0), (100.0, 0.0)];
|
|
141
|
+
/// A cell's box starts this far above its row's baseline and is this tall.
|
|
142
|
+
const CELL_RISE: f64 = 12.0;
|
|
143
|
+
const CELL_HEIGHT: f64 = 16.0;
|
|
144
|
+
|
|
145
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
146
|
+
let mut args = std::env::args().skip(1);
|
|
147
|
+
let font = std::fs::read(args.next().ok_or("usage: invoice <font.ttf> [out-dir]")?)?;
|
|
148
|
+
let out = std::path::PathBuf::from(args.next().unwrap_or_else(|| "target/examples-out".into()));
|
|
149
|
+
std::fs::create_dir_all(&out)?;
|
|
150
|
+
let pdf = build(BuildOptions::new(), font)?;
|
|
151
|
+
Ok(std::fs::write(out.join("invoice.pdf"), pdf)?)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// The bytes, in `font` (TrueType or OpenType); `options` can fix the file ID, producer and date.
|
|
155
|
+
pub(crate) fn build(options: BuildOptions, font: Vec<u8>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
|
156
|
+
let mut b = Builder::new(options.with_tagged(true));
|
|
157
|
+
draw(&mut b, font)?;
|
|
158
|
+
Ok(b.finish()?.save(&SaveOptions::new())?.bytes)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Draws the invoice's one page into `b` and sets its title, author and language.
|
|
162
|
+
pub(crate) fn draw(b: &mut Builder, font: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
|
|
163
|
+
let (font, bold) = (b.font_bytes(font)?, b.standard_font(Standard14::HelveticaBold)?);
|
|
164
|
+
let mut page = b.page(PAGE);
|
|
165
|
+
let s = page.surface();
|
|
166
|
+
s.text("Invoice 42", &bold, TITLE, TITLE_AT)?.tag(TagKind::H1);
|
|
167
|
+
s.text(ADDRESS, &font, BODY, ADDRESS_AT)?.tag(TagKind::P);
|
|
168
|
+
items(s, &font)?;
|
|
169
|
+
let total: f64 = ITEMS.iter().map(|(_, qty, price)| f64::from(*qty) * price).sum();
|
|
170
|
+
let (total, below_items) = (format!("Total € {total:.2}"), u32::try_from(ITEMS.len())?);
|
|
171
|
+
let area = cell(COLUMNS[1], row_baseline(below_items));
|
|
172
|
+
s.tag(TagKind::P, |s| right_aligned(s, &font, &total, area))?;
|
|
173
|
+
page.finish()?;
|
|
174
|
+
let metadata = Metadata::default().with_title("Invoice 42");
|
|
175
|
+
b.metadata(metadata.with_author("Acme Widgets Ltd")).lang("en-GB");
|
|
176
|
+
Ok(())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// The table of line items: what, how many, and the amount, right-aligned.
|
|
180
|
+
fn items(s: &mut Surface, font: &FontHandle) -> Result<(), BuildError> {
|
|
181
|
+
s.tag(TagKind::Table, |s| {
|
|
182
|
+
for (row, (item, qty, price)) in (0_u32..).zip(ITEMS) {
|
|
183
|
+
let y = row_baseline(row);
|
|
184
|
+
s.tag(TagKind::TR, |s| {
|
|
185
|
+
s.text(item, font, BODY, PointPt::from_points(MARGIN, y))?
|
|
186
|
+
.tag(TagKind::TD);
|
|
187
|
+
let amount = format!("€ {:.2}", f64::from(qty) * price);
|
|
188
|
+
for (text, column) in [qty.to_string(), amount].iter().zip(COLUMNS) {
|
|
189
|
+
s.tag(TagKind::TD, |s| right_aligned(s, font, text, cell(column, y)))?;
|
|
190
|
+
}
|
|
191
|
+
Ok(())
|
|
192
|
+
})?;
|
|
193
|
+
}
|
|
194
|
+
Ok(())
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// The baseline of row `row` of the table.
|
|
199
|
+
fn row_baseline(row: u32) -> f64 {
|
|
200
|
+
FIRST_ROW + ROW_HEIGHT * f64::from(row)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// The box of a cell in `column` (width, distance from the right edge) on the row at `baseline`.
|
|
204
|
+
fn cell((width, inset): (f64, f64), baseline: f64) -> RectPt {
|
|
205
|
+
RectPt::from_points(RIGHT_EDGE - inset - width, baseline - CELL_RISE, width, CELL_HEIGHT)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// `text` in `area`, in the body size, aligned to its right edge.
|
|
209
|
+
fn right_aligned(s: &mut Surface, font: &FontHandle, text: &str, area: RectPt) -> Result<(), BuildError> {
|
|
210
|
+
let right = TextBoxOptions::new().with_align(Alignment::End);
|
|
211
|
+
s.text_box(text, font, BODY, area, &right).map(|_| ())
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Result:** [invoice.pdf](https://docs.laminapdf.com/examples/pdf/invoice.pdf)
|
|
216
|
+
|
|
217
|
+
The result is one A4 page with its body set in Noto Sans. The Result tab shows it, with a link to the PDF.
|
|
218
|
+
|
|
219
|
+
## How it works
|
|
220
|
+
|
|
221
|
+
- The body uses your font file, and Lamina PDF embeds only the glyphs the invoice draws. The heading uses Helvetica Bold, one of the [standard 14 fonts](../../pages/reference/glossary.md#standard-14-fonts), so it embeds nothing.
|
|
222
|
+
- Each quantity and amount is a text box aligned to its `end`, so the numbers line up on the right. `cell` works out each box from its column's width and its distance from the right margin.
|
|
223
|
+
- `tagged: true` (`with_tagged(true)` in Rust) gives the document a structure for screen readers. The `tag` option and `s.tag(…)` make the heading an `H1`, the address and the total each a paragraph (`P`), and the items a `Table` of rows (`TR`) and cells (`TD`).
|
|
224
|
+
- `metadata` sets the title and author, and `lang("en-GB")` tells a screen reader which language to speak.
|
|
225
|
+
|
|
226
|
+
## What to change
|
|
227
|
+
|
|
228
|
+
- **The line items:** edit `ITEMS`. Each row sits `ROW_HEIGHT` points below the one before, and the total follows the last row.
|
|
229
|
+
- **The columns:** each pair in `COLUMNS` is a column's width and its distance from the right margin, in points.
|
|
230
|
+
- **The currency:** change the `€` in the amount and total strings. Your font needs a glyph for the new symbol, or drawing fails with `missing-glyph`. See [Text and fonts](../../pages/guides/fonts-and-text.md).
|
|
231
|
+
- **A PDF/UA-1 invoice:** add `profiles: ["PDF/UA-1"]` (`with_profile(Profile::PdfUa1)` in Rust) to check the rules for accessible PDFs. PDF/UA-1 needs every font embedded, so load a bold font file for the heading. See [Accessible PDFs](../../pages/guides/accessible-pdfs.md).
|
|
232
|
+
|
|
233
|
+
Next: [multilingual text](../../pages/recipes/multilingual.md), with five scripts, each in its own embedded font.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Links and outlines"
|
|
3
|
+
description: "Four A5 pages joined by web, page and named links, with a nested outline, page labels that read i, ii, 1, 2 and the outline shown on open."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/links-and-outlines/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Links and outlines
|
|
9
|
+
|
|
10
|
+
Four A5 pages joined by web, page and named links, with a nested outline, page labels that read i, ii, 1, 2 and the outline shown on open.
|
|
11
|
+
|
|
12
|
+
You'll make four A5 pages joined by links to a website and between pages, an outline (the bookmarks panel) and page numbers that read i, ii, 1, 2. Start from it for a manual, a report or any document longer than a page or two.
|
|
13
|
+
|
|
14
|
+
The function draws the four pages, links them and returns the PDF's bytes. It draws text, so in TypeScript it needs the text module, `init({ text: true })`. Both languages write the same bytes:
|
|
15
|
+
|
|
16
|
+
**TypeScript**
|
|
17
|
+
|
|
18
|
+
`packages/sdk/examples/links-and-outlines.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// Links and outlines: four A5 pages joined by links, bookmarks and page labels. The title page
|
|
22
|
+
// links to a website, to a page and through a named destination; every other page has a linked
|
|
23
|
+
// text run back to it. The outline is nested and shown when the file opens.
|
|
24
|
+
import { Builder, type BuilderOptions, Font, type LinkTarget, type Surface } from "@laminapdf/sdk";
|
|
25
|
+
|
|
26
|
+
const TITLES = ["Title page", "Contents", "Chapter 1", "Chapter 2"] as const;
|
|
27
|
+
const MARGIN = 36;
|
|
28
|
+
const TITLE_BASELINE = 60;
|
|
29
|
+
const TITLE_SIZE = 18;
|
|
30
|
+
const LINK_SIZE = 12;
|
|
31
|
+
/** The baselines of the title page's three links. */
|
|
32
|
+
const LINK_BASELINES = [100, 125, 150] as const;
|
|
33
|
+
/** A link's clickable box: this wide, this tall, starting this far above the baseline. */
|
|
34
|
+
const LINK_WIDTH = 220;
|
|
35
|
+
const LINK_HEIGHT = 20;
|
|
36
|
+
const LINK_RISE = 15;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The document's bytes. Needs `init({ text: true })`.
|
|
40
|
+
*
|
|
41
|
+
* @param options - can fix the file ID, the producer and the creation date.
|
|
42
|
+
*/
|
|
43
|
+
export function linksAndOutlines(options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
|
|
44
|
+
using b = new Builder(options);
|
|
45
|
+
const font = Font.standard("Helvetica");
|
|
46
|
+
b.font(font);
|
|
47
|
+
b.destination("chapter-2", 3);
|
|
48
|
+
TITLES.forEach((title, index) => {
|
|
49
|
+
const page = b.page("A5");
|
|
50
|
+
const s = page.surface();
|
|
51
|
+
s.text(title, { font, size: TITLE_SIZE, at: [MARGIN, TITLE_BASELINE] });
|
|
52
|
+
if (index === 0) titlePageLinks(s, font);
|
|
53
|
+
else {
|
|
54
|
+
const link = { target: { type: "page", index: 0 }, contents: "Title page" } as const;
|
|
55
|
+
s.text("Back to the title page", { font, size: LINK_SIZE, at: [MARGIN, 100], fill: [0, 0.2, 0.8], link });
|
|
56
|
+
}
|
|
57
|
+
page.finish();
|
|
58
|
+
});
|
|
59
|
+
navigation(b);
|
|
60
|
+
using doc = b.finish();
|
|
61
|
+
return doc.save();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The title page's links: to a website, to a page, and to a named destination. */
|
|
65
|
+
function titlePageLinks(s: Surface, font: Font): void {
|
|
66
|
+
const links: readonly (readonly [label: string, target: LinkTarget, contents?: string])[] = [
|
|
67
|
+
["Visit example.com", { type: "uri", uri: "https://example.com/" }, "Example"],
|
|
68
|
+
["Go to chapter 1", { type: "page", index: 2 }],
|
|
69
|
+
["Go to chapter 2 (named)", { type: "named", name: "chapter-2" }],
|
|
70
|
+
];
|
|
71
|
+
links.forEach(([label, target, contents], i) => {
|
|
72
|
+
const baseline = LINK_BASELINES[i] ?? 0;
|
|
73
|
+
s.text(label, { font, size: LINK_SIZE, at: [MARGIN, baseline] });
|
|
74
|
+
s.link([MARGIN, baseline - LINK_RISE, LINK_WIDTH, LINK_HEIGHT], target, contents);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The outline (bookmarks), the page labels, and the viewer opening with the outline shown. */
|
|
79
|
+
function navigation(b: Builder): void {
|
|
80
|
+
b.outline([
|
|
81
|
+
{ title: "Title page", target: { type: "page", index: 0 } },
|
|
82
|
+
{ title: "Contents", target: { type: "page", index: 1 }, color: [0, 0, 0.6] },
|
|
83
|
+
{
|
|
84
|
+
title: "Chapters",
|
|
85
|
+
open: true,
|
|
86
|
+
bold: true,
|
|
87
|
+
children: [
|
|
88
|
+
{ title: "Chapter 1", target: { type: "page", index: 2 } },
|
|
89
|
+
{ title: "Chapter 2", target: { type: "named", name: "chapter-2" }, italic: true },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
b.pageLabels([
|
|
94
|
+
{ start: 0, style: "lowerRoman" },
|
|
95
|
+
{ start: 2, style: "decimal" },
|
|
96
|
+
]).viewerPreferences({ pageMode: "outlines" });
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Rust**
|
|
101
|
+
|
|
102
|
+
`crates/laminapdf/examples/links_and_outlines.rs`:
|
|
103
|
+
|
|
104
|
+
````rust
|
|
105
|
+
//! Links and outlines: four A5 pages joined by links, bookmarks and page labels.
|
|
106
|
+
//!
|
|
107
|
+
//! The title page has a URI link, a link to a page and one through a named destination; every
|
|
108
|
+
//! other page has a linked text run back to it. The outline (bookmarks) is nested and open in
|
|
109
|
+
//! the viewer, and the pages are labelled i, ii, 1, 2.
|
|
110
|
+
//!
|
|
111
|
+
//! ```sh
|
|
112
|
+
//! cargo run -p laminapdf --example links_and_outlines -- [out-dir] # writes <out-dir>/links_and_outlines.pdf
|
|
113
|
+
//! ```
|
|
114
|
+
|
|
115
|
+
use std::error::Error;
|
|
116
|
+
use std::path::PathBuf;
|
|
117
|
+
|
|
118
|
+
use laminapdf::{
|
|
119
|
+
BuildOptions, Builder, Color, Fit, FontHandle, LabelStyle, LinkTarget, OutlineEntry, PageLabel, PageMode, PageSize,
|
|
120
|
+
PointPt, Pt, RectPt, SaveOptions, Standard14, Surface, ViewerPreferences,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const TITLES: [&str; 4] = ["Title page", "Contents", "Chapter 1", "Chapter 2"];
|
|
124
|
+
const MARGIN: f64 = 36.0;
|
|
125
|
+
const TITLE_AT: PointPt = PointPt::from_points(MARGIN, 60.0);
|
|
126
|
+
const BACK_AT: PointPt = PointPt::from_points(MARGIN, 100.0);
|
|
127
|
+
const TITLE_SIZE: Pt = Pt::new(18.0);
|
|
128
|
+
const LINK_SIZE: Pt = Pt::new(12.0);
|
|
129
|
+
/// The baselines of the title page's three links.
|
|
130
|
+
const LINK_BASELINES: [f64; 3] = [100.0, 125.0, 150.0];
|
|
131
|
+
/// A link's clickable box: this wide, this tall, starting this far above the baseline.
|
|
132
|
+
const LINK_WIDTH: f64 = 220.0;
|
|
133
|
+
const LINK_HEIGHT: f64 = 20.0;
|
|
134
|
+
const LINK_RISE: f64 = 15.0;
|
|
135
|
+
|
|
136
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
137
|
+
let out = PathBuf::from(std::env::args().nth(1).unwrap_or_else(|| "target/examples-out".into()));
|
|
138
|
+
std::fs::create_dir_all(&out)?;
|
|
139
|
+
std::fs::write(out.join("links_and_outlines.pdf"), build(BuildOptions::new())?)?;
|
|
140
|
+
Ok(())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// The document's bytes. `options` can fix the file ID, the producer and the creation date.
|
|
144
|
+
pub(crate) fn build(options: BuildOptions) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
145
|
+
let mut b = Builder::new(options);
|
|
146
|
+
let helvetica = b.standard_font(Standard14::Helvetica)?;
|
|
147
|
+
b.destination("chapter-2", 3, Fit::Page);
|
|
148
|
+
for (index, title) in (0_u32..).zip(TITLES) {
|
|
149
|
+
let mut page = b.page(PageSize::A5);
|
|
150
|
+
let s = page.surface();
|
|
151
|
+
s.text(title, &helvetica, TITLE_SIZE, TITLE_AT)?;
|
|
152
|
+
if index == 0 {
|
|
153
|
+
title_page_links(s, &helvetica)?;
|
|
154
|
+
} else {
|
|
155
|
+
s.text("Back to the title page", &helvetica, LINK_SIZE, BACK_AT)?
|
|
156
|
+
.fill(Color::rgb(0.0, 0.2, 0.8))
|
|
157
|
+
.link(LinkTarget::page(0), Some("Title page".into()));
|
|
158
|
+
}
|
|
159
|
+
page.finish()?;
|
|
160
|
+
}
|
|
161
|
+
navigation(&mut b)?;
|
|
162
|
+
let mut doc = b.finish()?;
|
|
163
|
+
Ok(doc.save(&SaveOptions::new())?.bytes)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// The title page's links: to a website, to a page, and to a named destination.
|
|
167
|
+
fn title_page_links(s: &mut Surface, font: &FontHandle) -> Result<(), Box<dyn Error>> {
|
|
168
|
+
let site = LinkTarget::Uri("https://example.com/".into());
|
|
169
|
+
let links = [
|
|
170
|
+
("Visit example.com", site, Some("Example")),
|
|
171
|
+
("Go to chapter 1", LinkTarget::page(2), None),
|
|
172
|
+
("Go to chapter 2 (named)", LinkTarget::Named("chapter-2".into()), None),
|
|
173
|
+
];
|
|
174
|
+
for ((label, target, contents), baseline) in links.into_iter().zip(LINK_BASELINES) {
|
|
175
|
+
s.text(label, font, LINK_SIZE, PointPt::from_points(MARGIN, baseline))?;
|
|
176
|
+
let area = RectPt::from_points(MARGIN, baseline - LINK_RISE, LINK_WIDTH, LINK_HEIGHT);
|
|
177
|
+
s.link(area, target, contents.map(String::from));
|
|
178
|
+
}
|
|
179
|
+
Ok(())
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// The outline (bookmarks), the page labels, and the viewer opening with the outline shown.
|
|
183
|
+
fn navigation(b: &mut Builder) -> Result<(), Box<dyn Error>> {
|
|
184
|
+
b.outline(vec![
|
|
185
|
+
OutlineEntry::new("Title page", LinkTarget::page(0)),
|
|
186
|
+
OutlineEntry::new("Contents", LinkTarget::page(1)).with_color(0.0, 0.0, 0.6),
|
|
187
|
+
OutlineEntry::heading("Chapters")
|
|
188
|
+
.open(true)
|
|
189
|
+
.bold()
|
|
190
|
+
.with_child(OutlineEntry::new("Chapter 1", LinkTarget::page(2)))
|
|
191
|
+
.with_child(OutlineEntry::new("Chapter 2", LinkTarget::Named("chapter-2".into())).italic()),
|
|
192
|
+
])?;
|
|
193
|
+
b.page_labels(vec![
|
|
194
|
+
PageLabel::new(0, Some(LabelStyle::LowerRoman)),
|
|
195
|
+
PageLabel::new(2, Some(LabelStyle::Decimal)),
|
|
196
|
+
])
|
|
197
|
+
.viewer_preferences(ViewerPreferences::default().with_page_mode(PageMode::Outlines));
|
|
198
|
+
Ok(())
|
|
199
|
+
}
|
|
200
|
+
````
|
|
201
|
+
|
|
202
|
+
**Result:** [links_and_outlines.pdf](https://docs.laminapdf.com/examples/pdf/links_and_outlines.pdf)
|
|
203
|
+
|
|
204
|
+
The result is four A5 pages with an outline and page labels. The Result tab shows it, with a link to the PDF.
|
|
205
|
+
|
|
206
|
+
## How it works
|
|
207
|
+
|
|
208
|
+
- `s.link(rect, target, contents)` puts a clickable box over each label on the title page. A target is a web address (`uri`), a page by its index from 0, or a named destination. See [Pages and coordinates](../../pages/concepts/pages-and-coordinates.md#pages-count-from-0).
|
|
209
|
+
- `destination("chapter-2", 3)` names the fourth page, so a link or an outline entry can go there by name. When the builder finishes, a missing page fails with `page-index-out-of-range` and a missing name with `unknown-destination`.
|
|
210
|
+
- On the other pages, the `link` option of `text` (`.link(…)` on the run in Rust) makes the blue run itself the link, with no box to work out.
|
|
211
|
+
- `contents` describes a link in words, for viewers and screen readers. Accessible PDFs need one on every link.
|
|
212
|
+
- `outline` builds the bookmarks. "Chapters" has no target, so it's a heading, shown open and in bold with both chapters inside it.
|
|
213
|
+
- `pageLabels` numbers the pages i and ii, then 1 and 2 from the third page. `pageMode: "outlines"` asks the viewer to open the file with the outline showing.
|
|
214
|
+
|
|
215
|
+
## What to change
|
|
216
|
+
|
|
217
|
+
- **Where a page link lands:** add a `fit` to the target, such as `fit: { type: "width" }` (`Fit::Width` in Rust) to fill the window with the page's width. `destination` takes one too.
|
|
218
|
+
- **The page numbers:** a label also takes a `prefix` and a `first` number, so `{ start: 2, style: "decimal", prefix: "A-" }` numbers the chapters A-1 and A-2.
|
|
219
|
+
- **The panel on open:** `pageMode` also takes `"thumbnails"`, `"attachments"` or `"none"`.
|
|
220
|
+
- **A title and an author:** add `metadata({ title, author })`, and `displayDocTitle: true` in the viewer preferences so viewers show the title instead of the file name. See [Links, outlines and metadata](../../pages/guides/links-outlines-and-metadata.md).
|
|
221
|
+
|
|
222
|
+
Next: [generate, then edit](../../pages/recipes/generate-then-edit.md), which opens a PDF you made and changes its metadata.
|