@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,379 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Pages that flow"
|
|
3
|
+
description: "Pour headings, paragraphs, tables and images, or a Markdown file, into as many pages as they need, with footnotes, a contents page and \"Page X of Y\" footers."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/guides/flowing-pages/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Pages that flow
|
|
9
|
+
|
|
10
|
+
Pour headings, paragraphs, tables and images, or a Markdown file, into as many pages as they need, with footnotes, a contents page and "Page X of Y" footers.
|
|
11
|
+
|
|
12
|
+
This guide lays out a document whose length you don't know in advance, such as a report or a contract. You give Lamina PDF blocks and a page template, and it makes as many pages as they need, with a header and a footer on each.
|
|
13
|
+
|
|
14
|
+
It keeps headings with their text and never leaves a single line of a paragraph alone at a page's foot or head, and it tags the result so screen readers can follow it. Flowing layout is in the Rust crate today; the TypeScript package will follow.
|
|
15
|
+
|
|
16
|
+
## The example
|
|
17
|
+
|
|
18
|
+
The example builds a 13-page report: a cover with a contents page, then five sections of headings, paragraphs, a footnote, a bulleted list and a photo. One paragraph is in Arabic and one in Hindi, and each is split by a page break. Every page after the cover names its section in the header and says "Page 3 of 13" in the footer, and every section has a bookmark. The report is built under PDF/UA-1, the accessibility standard, so the build fails if the output breaks one of its rules.
|
|
19
|
+
|
|
20
|
+
`crates/laminapdf/examples/flowing_report.rs`:
|
|
21
|
+
|
|
22
|
+
```rust
|
|
23
|
+
//! A report that flows across pages: a cover with a contents page, then sections whose
|
|
24
|
+
//! paragraphs break between pages, footnotes at the foot of their page, the section's title in
|
|
25
|
+
//! a running header and "Page X of Y" in the footer.
|
|
26
|
+
//!
|
|
27
|
+
//! Lines split with at least two at the foot and the head of a page, headings stay with what
|
|
28
|
+
//! follows them, and the photo moves whole. The output is tagged, has bookmarks, and is checked
|
|
29
|
+
//! against PDF/UA-1. Run it with `cargo run -p laminapdf --example flowing_report -- regular.ttf
|
|
30
|
+
//! bold.ttc arabic.ttf devanagari.ttf photo.jpg`.
|
|
31
|
+
|
|
32
|
+
use std::error::Error;
|
|
33
|
+
|
|
34
|
+
use laminapdf::flow::{
|
|
35
|
+
Contents, Figure, Flow, FlowBlock, List, ListItem, ListKind, PageRegion, PageTemplate, Paragraph, ParagraphStyle,
|
|
36
|
+
TextStyle,
|
|
37
|
+
};
|
|
38
|
+
use laminapdf::{BuildOptions, Builder, Font, Image, Metadata, PageSize, Profile, SaveOptions};
|
|
39
|
+
|
|
40
|
+
const TITLE: &str = "Reading files offline: a year of the reader";
|
|
41
|
+
pub(crate) const ARABIC: &str =
|
|
42
|
+
"يعمل القارئ دون اتصال بالشبكة، فتُفتح الملفات وتُقرأ وتُبحث على الجهاز نفسه، ولا يغادر أي منها الحاسوب.";
|
|
43
|
+
pub(crate) const HINDI: &str = "नमस्ते दुनिया। हिन्दी पाठ का परीक्षण।";
|
|
44
|
+
const TOPICS: [&str; 5] = ["Opening files", "Search", "Printing", "Signing", "What comes next"];
|
|
45
|
+
|
|
46
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
47
|
+
let paths: Vec<String> = std::env::args().skip(1).collect();
|
|
48
|
+
let [regular, bold, arabic, hindi, photo] = paths.as_slice() else {
|
|
49
|
+
return Err("arguments: <regular.ttf> <bold.ttc> <arabic.ttf> <devanagari.ttf> <photo.jpg>".into());
|
|
50
|
+
};
|
|
51
|
+
let read = |path: &String| std::fs::read(path);
|
|
52
|
+
let fonts = [read(regular)?, read(bold)?, read(arabic)?, read(hindi)?];
|
|
53
|
+
let pdf = build(BuildOptions::new(), fonts, std::fs::read(photo)?)?;
|
|
54
|
+
std::fs::create_dir_all("target/examples-out")?;
|
|
55
|
+
Ok(std::fs::write("target/examples-out/flowing_report.pdf", pdf)?)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// The report's bytes. `options` can fix the file ID, the producer and the creation date.
|
|
59
|
+
pub(crate) fn build(options: BuildOptions, fonts: [Vec<u8>; 4], photo: Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
60
|
+
let [regular, bold, arabic, hindi] = fonts;
|
|
61
|
+
let mut b = Builder::new(options.with_profile(Profile::PdfUa1));
|
|
62
|
+
b.metadata(Metadata::default().with_title(TITLE));
|
|
63
|
+
b.lang("en");
|
|
64
|
+
let regular = b.font(Font::from_bytes(regular)?)?;
|
|
65
|
+
let body = TextStyle::new(®ular, 11.0)
|
|
66
|
+
.with_fallback(&b.font(Font::from_bytes(arabic)?)?)
|
|
67
|
+
.with_fallback(&b.font(Font::from_bytes(hindi)?)?);
|
|
68
|
+
let heading = TextStyle::new(&b.font(Font::from_collection(bold, 1)?)?, 16.0);
|
|
69
|
+
let small = TextStyle::new(®ular, 9.0);
|
|
70
|
+
let header = PageRegion::new().with_end("{section}");
|
|
71
|
+
let footer = PageRegion::new().with_center("Page {page} of {pages}");
|
|
72
|
+
let pages = PageTemplate::new(PageSize::A5).with_furniture(Some(header), Some(footer), &small);
|
|
73
|
+
let (cover, team) = (heading.clone().with_size(24.0), "A report from the reader's team.");
|
|
74
|
+
let mut flow = Flow::new(pages.with_first_page(PageTemplate::new(PageSize::A5)))
|
|
75
|
+
.with_title(TITLE)
|
|
76
|
+
.block(FlowBlock::Paragraph(Paragraph::plain(TITLE, &cover)))
|
|
77
|
+
.block(FlowBlock::Paragraph(Paragraph::plain(team, &body)))
|
|
78
|
+
.block(FlowBlock::Contents(Contents::new(&body)))
|
|
79
|
+
.block(FlowBlock::PageBreak);
|
|
80
|
+
let photo = Image::jpeg(photo)?;
|
|
81
|
+
for (i, topic) in TOPICS.iter().enumerate() {
|
|
82
|
+
flow = section(flow, i, topic, &body, &heading, &photo);
|
|
83
|
+
}
|
|
84
|
+
b.flow(&flow)?;
|
|
85
|
+
Ok(b.finish()?.save(&SaveOptions::new())?.bytes)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// A section: its heading, paragraphs (one in Arabic, one in Hindi), a list and a photo.
|
|
89
|
+
fn section(flow: Flow, i: usize, topic: &str, body: &TextStyle, heading: &TextStyle, photo: &Image) -> Flow {
|
|
90
|
+
let sentence = format!(
|
|
91
|
+
"The {} work of the year changed how readers use the app every day. ",
|
|
92
|
+
topic.to_lowercase()
|
|
93
|
+
);
|
|
94
|
+
let mut flow = flow.block(FlowBlock::Heading(1, Paragraph::plain(topic, heading)));
|
|
95
|
+
let note = Paragraph::plain(
|
|
96
|
+
format!("Measured on our test files for {topic}."),
|
|
97
|
+
&body.clone().with_size(9.0),
|
|
98
|
+
);
|
|
99
|
+
for n in 0..4 {
|
|
100
|
+
let p = Paragraph::new(ParagraphStyle::new().with_space(0.0, 8.0)).text(sentence.repeat(n + 4), body);
|
|
101
|
+
let p = if n == 3 { p.note(note.clone()) } else { p };
|
|
102
|
+
flow = flow.block(FlowBlock::Paragraph(p));
|
|
103
|
+
}
|
|
104
|
+
let foreign = match i {
|
|
105
|
+
1 => Some((format!("{ARABIC} ").repeat(8).trim_end().to_owned(), "ar")),
|
|
106
|
+
3 => Some((format!("{HINDI} ").repeat(12).trim_end().to_owned(), "hi")),
|
|
107
|
+
_ => None,
|
|
108
|
+
};
|
|
109
|
+
if let Some((text, lang)) = foreign {
|
|
110
|
+
let style = body.clone().with_language(lang);
|
|
111
|
+
flow = flow.block(FlowBlock::Paragraph(Paragraph::plain(text, &style)));
|
|
112
|
+
}
|
|
113
|
+
let list = ["faster", "smaller", "easier to share"]
|
|
114
|
+
.iter()
|
|
115
|
+
.fold(List::new(ListKind::Bullet, body), |list, word| {
|
|
116
|
+
list.item(ListItem::new(Paragraph::plain(format!("Files are {word}."), body)))
|
|
117
|
+
});
|
|
118
|
+
let alt = format!("A photo of the team working on {}", topic.to_lowercase());
|
|
119
|
+
flow.block(FlowBlock::List(list))
|
|
120
|
+
.block(FlowBlock::Figure(Figure::new(photo.clone(), alt).with_width(200.0)))
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The result is thirteen A5 pages: [open the reference PDF](https://docs.laminapdf.com/examples/pdf/flowing_report.pdf).
|
|
125
|
+
|
|
126
|
+
## Blocks, styles and the template
|
|
127
|
+
|
|
128
|
+
A `Flow` is a list of blocks and the `PageTemplate` they flow through.
|
|
129
|
+
|
|
130
|
+
- **Paragraphs** are made of pieces of text, each in its own `TextStyle`: its fonts, size, colour, language and a baseline rise for superscripts. A bold word is a piece in the bold font. A link is a piece with a target, and a line break or a footnote is a piece of its own.
|
|
131
|
+
- **Tables** have header rows, body rows and footer rows; a later section covers them.
|
|
132
|
+
- A **contents** block lists the headings with their pages, and footnotes go in paragraphs; both are covered below.
|
|
133
|
+
- **Headings** run from level 1 to 6. **Lists** are bulleted or numbered, and an item can hold a nested list. A **figure** is an image with its alternate text, as wide as the page's text column unless you give it a width.
|
|
134
|
+
- **Spacers, page breaks and keep groups** shape the layout: a spacer adds space, a page break starts a new page, and a `Keep` group keeps its blocks on one page.
|
|
135
|
+
|
|
136
|
+
The template gives the page size, the margins, a header and a footer, and optionally a different first page, such as a cover without a header. A header or footer has three slots, at the start, centre and end of the line.
|
|
137
|
+
|
|
138
|
+
## Where pages break
|
|
139
|
+
|
|
140
|
+
The flow lays out every page before it draws any, and it follows these rules:
|
|
141
|
+
|
|
142
|
+
- **Paragraphs split only between lines,** leaving at least two lines at the foot of a page and two at the head of the next. When a page has room for only one line of a paragraph, the paragraph moves to the next page. Change the counts with `with_widows_and_orphans`.
|
|
143
|
+
- **A heading stays with the block after it,** so a section never starts on the last line of a page. Any paragraph can do the same with `with_keep_with_next`.
|
|
144
|
+
- **Figures and keep groups move whole** to the next page when they don't fit.
|
|
145
|
+
- **A paragraph continued on a page of another width** is broken again at that width.
|
|
146
|
+
|
|
147
|
+
When blocks you asked to keep together are taller than a page, the flow splits them anyway and adds a `flow-keep-broken` warning to the report `Builder::flow` returns. A figure or a single line taller than a page fails with `flow-content-too-large`, since no page could hold it.
|
|
148
|
+
|
|
149
|
+
## Tables across pages
|
|
150
|
+
|
|
151
|
+
A table can run over as many pages as its rows need, with its header on each page and every row whole. The second example builds a three-page invoice of 60 rows with a total at the end:
|
|
152
|
+
|
|
153
|
+
`crates/laminapdf/examples/flowing_invoice.rs`:
|
|
154
|
+
|
|
155
|
+
```rust
|
|
156
|
+
//! An invoice whose table runs over three pages: its header row repeats at the top of each
|
|
157
|
+
//! page, no row is split between pages, and the total stays on the page of the last row.
|
|
158
|
+
//!
|
|
159
|
+
//! The columns fit their widest cell, and numbers line up on the right. The output is tagged
|
|
160
|
+
//! (one `Table` element across the pages) and checked against PDF/UA-1. Run it with
|
|
161
|
+
//! `cargo run -p laminapdf --example flowing_invoice -- regular.ttf bold.ttc`.
|
|
162
|
+
|
|
163
|
+
use std::error::Error;
|
|
164
|
+
|
|
165
|
+
use laminapdf::flow::{
|
|
166
|
+
ColumnWidth, Flow, FlowBlock, Margins, PageRegion, PageTemplate, Paragraph, Table, TableCell, TableRow, TableRule,
|
|
167
|
+
TextStyle,
|
|
168
|
+
};
|
|
169
|
+
use laminapdf::{Alignment, BuildOptions, Builder, Color, Font, Metadata, PageSize, Profile, SaveOptions};
|
|
170
|
+
|
|
171
|
+
const TITLE: &str = "Invoice 2026-114";
|
|
172
|
+
const PRODUCTS: [(&str, usize); 6] = [
|
|
173
|
+
("Paper, A4, 80 g, box of 2,500 sheets", 2_450),
|
|
174
|
+
("Toner cartridge, black", 6_900),
|
|
175
|
+
("Desk lamp with a weighted base and an adjustable arm", 4_275),
|
|
176
|
+
("Stapler", 1_290),
|
|
177
|
+
("Archive boxes, set of ten, with lids", 3_150),
|
|
178
|
+
("Ballpoint pens, box of fifty", 1_875),
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
182
|
+
let paths: Vec<String> = std::env::args().skip(1).collect();
|
|
183
|
+
let [regular, bold] = paths.as_slice() else {
|
|
184
|
+
return Err("arguments: <regular.ttf> <bold.ttc>".into());
|
|
185
|
+
};
|
|
186
|
+
let pdf = build(BuildOptions::new(), std::fs::read(regular)?, std::fs::read(bold)?)?;
|
|
187
|
+
std::fs::create_dir_all("target/examples-out")?;
|
|
188
|
+
Ok(std::fs::write("target/examples-out/flowing_invoice.pdf", pdf)?)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// The invoice's bytes. `options` can fix the file ID, the producer and the creation date.
|
|
192
|
+
pub(crate) fn build(options: BuildOptions, regular: Vec<u8>, bold: Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
193
|
+
let mut b = Builder::new(options.with_profile(Profile::PdfUa1));
|
|
194
|
+
b.metadata(Metadata::default().with_title(TITLE));
|
|
195
|
+
b.lang("en");
|
|
196
|
+
let body = TextStyle::new(&b.font(Font::from_bytes(regular)?)?, 10.0);
|
|
197
|
+
let strong = TextStyle::new(&b.font(Font::from_collection(bold, 1)?)?, 10.0);
|
|
198
|
+
let footer = PageRegion::new().with_center("{title}, page {page} of {pages}");
|
|
199
|
+
let pages = PageTemplate::new(PageSize::A4)
|
|
200
|
+
.with_margins(Margins::all(56.0))
|
|
201
|
+
.with_furniture(None, Some(footer), &body.clone().with_size(8.0));
|
|
202
|
+
let flow = Flow::new(pages)
|
|
203
|
+
.with_title(TITLE)
|
|
204
|
+
.block(FlowBlock::Heading(
|
|
205
|
+
1,
|
|
206
|
+
Paragraph::plain(TITLE, &strong.clone().with_size(18.0)),
|
|
207
|
+
))
|
|
208
|
+
.block(FlowBlock::Paragraph(Paragraph::plain(
|
|
209
|
+
"Office supplies delivered in October, to be paid within 30 days.",
|
|
210
|
+
&body,
|
|
211
|
+
)))
|
|
212
|
+
.block(FlowBlock::Table(table(&body, &strong)));
|
|
213
|
+
b.flow(&flow)?;
|
|
214
|
+
Ok(b.finish()?.save(&SaveOptions::new())?.bytes)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// Sixty lines of products, a header row and a footer row with the total.
|
|
218
|
+
fn table(body: &TextStyle, strong: &TextStyle) -> Table {
|
|
219
|
+
let number = |text: String, style: &TextStyle| TableCell::plain(text, style).with_align(Alignment::End);
|
|
220
|
+
let header = ["Item", "Description", "Qty", "Amount"]
|
|
221
|
+
.iter()
|
|
222
|
+
.enumerate()
|
|
223
|
+
.fold(TableRow::new(), |row, (i, text)| {
|
|
224
|
+
let cell = TableCell::plain(*text, strong);
|
|
225
|
+
row.cell(if i >= 2 { cell.with_align(Alignment::End) } else { cell })
|
|
226
|
+
});
|
|
227
|
+
let mut table = Table::new([ColumnWidth::Auto; 4])
|
|
228
|
+
.with_padding(6.0)
|
|
229
|
+
.with_frame(TableRule::new(0.75, Color::gray(0.4)))
|
|
230
|
+
.with_header_rule(TableRule::new(0.75, Color::gray(0.4)))
|
|
231
|
+
.with_row_rules(TableRule::new(0.25, Color::gray(0.7)))
|
|
232
|
+
.with_header_background(Color::gray(0.92))
|
|
233
|
+
.header(header);
|
|
234
|
+
let mut total = 0;
|
|
235
|
+
for (i, (name, cents)) in PRODUCTS.iter().cycle().take(60).enumerate() {
|
|
236
|
+
let qty = i % 5 + 1;
|
|
237
|
+
total += qty * cents;
|
|
238
|
+
table = table.row(
|
|
239
|
+
TableRow::new()
|
|
240
|
+
.cell(TableCell::plain(format!("{:03}", i + 1), body))
|
|
241
|
+
.cell(TableCell::plain(*name, body))
|
|
242
|
+
.cell(number(qty.to_string(), body))
|
|
243
|
+
.cell(number(money(qty * cents), body)),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
let total_row = TableRow::new()
|
|
247
|
+
.cell(TableCell::plain("Total", strong).with_span(3))
|
|
248
|
+
.cell(number(money(total), strong));
|
|
249
|
+
table.footer(total_row)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn money(cents: usize) -> String {
|
|
253
|
+
format!("{}.{:02}", cents / 100, cents % 100)
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The result is three A4 pages: [open the reference PDF](https://docs.laminapdf.com/examples/pdf/flowing_invoice.pdf).
|
|
258
|
+
|
|
259
|
+
A table follows three rules when it crosses a page:
|
|
260
|
+
|
|
261
|
+
- **The header rows repeat** at the top of every page the table continues on.
|
|
262
|
+
- **A body row never splits.** A row that doesn't fit what is left of a page starts the next one, and a row taller than a page fails with `flow-content-too-large`.
|
|
263
|
+
- **The footer rows stay with the last body row,** so a total never starts a page alone. When the footer doesn't fit under the last row, that row moves to the next page with the footer.
|
|
264
|
+
|
|
265
|
+
Columns get their widths once, for the whole table, so they line up from page to page. A column is `Fixed` (a width in points), `Auto` (as wide as its widest cell) or a `Fraction` (a share of the width the others leave). When automatic columns are too wide together, the widest ones narrow first, so a column of numbers keeps its width while the descriptions wrap.
|
|
266
|
+
|
|
267
|
+
A cell holds one or more paragraphs. `with_align` lines its text up at the start, centre or end (numbers at the end), and `with_vertical_align` places it in the row. `with_span` stretches a cell over several columns, as the invoice's "Total" does. Rules come from `with_frame`, `with_header_rule` and `with_row_rules`, and `with_header_background` shades the header rows.
|
|
268
|
+
|
|
269
|
+
## Footnotes, a contents page and bookmarks
|
|
270
|
+
|
|
271
|
+
A footnote is part of a paragraph: `Paragraph::note(body)` puts a raised number where you add it and the note's body at the foot of the same page. Notes are numbered 1, 2, 3 through the flow. A short rule separates them from the text above, and the page's footer stays below them. When a line's notes don't fit at the foot of its page, the line moves to the next page with them.
|
|
272
|
+
|
|
273
|
+
A contents block, `FlowBlock::Contents`, lists the flow's headings with the page each one starts on. Each entry is indented by its heading's level, with dots leading to its number, and the entry links to its heading. By default it lists levels 1 and 2; `Contents::with_levels` changes that. The report in the example puts its contents on the cover.
|
|
274
|
+
|
|
275
|
+
The flow can't know the page numbers before it has laid the pages out. So it lays everything out, writes the numbers it found into the contents page, and lays out again to check them. Each entry keeps room for a number of up to four digits, so the contents page keeps its length and the second layout confirms the numbers.
|
|
276
|
+
|
|
277
|
+
Every heading also gets a bookmark. PDF readers list the bookmarks in their sidebar, nested by heading level, and each one goes to its heading. Turn them off with `Flow::with_outline(false)`.
|
|
278
|
+
|
|
279
|
+
## Write it in Markdown
|
|
280
|
+
|
|
281
|
+
You can also write the document in Markdown and let `Builder::markdown` turn it into blocks. Each Markdown construct becomes the block you would build by hand: a `#` heading becomes a heading, a list becomes a list, and a table becomes a table that runs across pages, its header row repeated on each. A link to `#a-heading` goes to the heading of that name, written the way GitHub writes it: lower case, with hyphens for spaces. Front matter at the top, plain `key: value` lines between two `---` lines, sets the document's title, language, author and subject.
|
|
282
|
+
|
|
283
|
+
The example below builds a short report from a Markdown file. It gives the fonts, the page template and a contents page in `MarkdownBuildOptions`, and it passes the one image the Markdown names. The builder reads no files and fetches nothing, so it leaves out an image you don't pass and adds a `markdown-image-missing` warning. It leaves out raw HTML too, with `markdown-html-ignored`. Both warnings give the position in the Markdown, never its text.
|
|
284
|
+
|
|
285
|
+
`crates/laminapdf/examples/markdown_to_pdf.rs`:
|
|
286
|
+
|
|
287
|
+
```rust
|
|
288
|
+
//! A report written in Markdown, turned into tagged pages that flow: headings, paragraphs with
|
|
289
|
+
//! emphasis, code and links, a photo, lists and a task list, a block quote, a table that runs
|
|
290
|
+
//! across pages, a code block, footnotes, and paragraphs in Arabic and Hindi.
|
|
291
|
+
//!
|
|
292
|
+
//! The front matter gives the title and language, a contents page lists the headings, and the
|
|
293
|
+
//! output is checked against PDF/UA-1, so every font is embedded. Run it with `cargo run -p
|
|
294
|
+
//! laminapdf --example markdown_to_pdf -- report.md regular.ttf bold.ttc arabic.ttf
|
|
295
|
+
//! devanagari.ttf photo.jpg`.
|
|
296
|
+
|
|
297
|
+
use std::error::Error;
|
|
298
|
+
|
|
299
|
+
use laminapdf::flow::{PageRegion, PageTemplate, TextStyle};
|
|
300
|
+
use laminapdf::markdown::{MarkdownBuildOptions, MarkdownFonts};
|
|
301
|
+
use laminapdf::{BuildOptions, Builder, Font, Image, PageSize, Profile, SaveOptions};
|
|
302
|
+
|
|
303
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
304
|
+
let paths: Vec<String> = std::env::args().skip(1).collect();
|
|
305
|
+
let [markdown, regular, bold, arabic, hindi, photo] = paths.as_slice() else {
|
|
306
|
+
return Err("arguments: <report.md> <regular.ttf> <bold.ttc> <arabic.ttf> <devanagari.ttf> <photo.jpg>".into());
|
|
307
|
+
};
|
|
308
|
+
let read = |path: &String| std::fs::read(path);
|
|
309
|
+
let fonts = [read(regular)?, read(bold)?, read(arabic)?, read(hindi)?];
|
|
310
|
+
let markdown = std::fs::read_to_string(markdown)?;
|
|
311
|
+
let pdf = build(BuildOptions::new(), &markdown, fonts, read(photo)?)?;
|
|
312
|
+
std::fs::create_dir_all("target/examples-out")?;
|
|
313
|
+
Ok(std::fs::write("target/examples-out/markdown_to_pdf.pdf", pdf)?)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// The report's bytes. `options` can fix the file ID, the producer and the creation date.
|
|
317
|
+
pub(crate) fn build(
|
|
318
|
+
options: BuildOptions,
|
|
319
|
+
markdown: &str,
|
|
320
|
+
fonts: [Vec<u8>; 4],
|
|
321
|
+
photo: Vec<u8>,
|
|
322
|
+
) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
323
|
+
let [regular, bold, arabic, hindi] = fonts;
|
|
324
|
+
let mut b = Builder::new(options.with_profile(Profile::PdfUa1));
|
|
325
|
+
let regular = b.font(Font::from_bytes(regular)?)?;
|
|
326
|
+
let fonts = MarkdownFonts::new(®ular)
|
|
327
|
+
.with_bold(&b.font(Font::from_collection(bold, 1)?)?)
|
|
328
|
+
.with_fallback(&b.font(Font::from_bytes(arabic)?)?)
|
|
329
|
+
.with_fallback(&b.font(Font::from_bytes(hindi)?)?);
|
|
330
|
+
// A5 pages, so the table runs across two of them, with the page number in the footer.
|
|
331
|
+
let footer = PageRegion::new().with_center("Page {page} of {pages}");
|
|
332
|
+
let pages = PageTemplate::new(PageSize::A5).with_furniture(None, Some(footer), &TextStyle::new(®ular, 8.0));
|
|
333
|
+
let options = MarkdownBuildOptions::new()
|
|
334
|
+
.with_fonts(fonts)
|
|
335
|
+
.with_template(pages)
|
|
336
|
+
.with_contents(2)
|
|
337
|
+
.with_image("photo.jpg", Image::jpeg(photo)?);
|
|
338
|
+
let report = b.markdown(markdown, &options)?;
|
|
339
|
+
if let Some(warning) = report.warnings.first() {
|
|
340
|
+
return Err(format!("the Markdown has something the PDF leaves out: {}", warning.code()).into());
|
|
341
|
+
}
|
|
342
|
+
Ok(b.finish()?.save(&SaveOptions::new())?.bytes)
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The result is four A5 pages: [open the reference PDF](https://docs.laminapdf.com/examples/pdf/markdown_to_pdf.pdf).
|
|
347
|
+
|
|
348
|
+
If you pass no fonts, the builder sets the Markdown in Helvetica and Courier, two fonts every PDF reader has. They set only Latin text, and since they aren't embedded, a PDF/UA-1 build refuses them with `font-not-embedded`. Pass your own fonts with `MarkdownFonts` for other scripts or an accessible document.
|
|
349
|
+
|
|
350
|
+
## Fields in headers and footers
|
|
351
|
+
|
|
352
|
+
A slot's text can hold fields, which the flow fills in on each page:
|
|
353
|
+
|
|
354
|
+
- `{page}`: the page's number in the document, from 1.
|
|
355
|
+
- `{pages}`: the number of pages in the document once the flow's pages are added.
|
|
356
|
+
- `{section}`: the title of the last level-1 heading that started on or before the page. Choose another level with `with_section_level`.
|
|
357
|
+
- `{title}`: the title you gave the flow.
|
|
358
|
+
|
|
359
|
+
Write `{{` and `}}` for a literal brace. `{pages}` is right on every page because no page is drawn until all of them are laid out.
|
|
360
|
+
|
|
361
|
+
## What the flow tags
|
|
362
|
+
|
|
363
|
+
The output is tagged unless you turn tagging off with `with_tagged(false)`. Paragraphs are `P` elements, headings `H1` to `H6`, lists `L` with their items, figures `Figure` with their alternate text, and links `Link`. A paragraph's language, taken from its first piece, is set on its element; a piece in another language is a `Span` with that language. A paragraph split across pages stays one element, so a screen reader reads it as one.
|
|
364
|
+
|
|
365
|
+
A table is one `Table` element however many pages it crosses, with `THead`, `TBody` and `TFoot` around its rows. Header cells are `TH` elements that head their columns, and a cell across several columns records how many it spans.
|
|
366
|
+
|
|
367
|
+
The header rows drawn again on later pages are marked as page furniture, like headers and footers, so a screen reader and Lamina PDF's extraction read the table's header once. The table's rules and background are marked as decoration too.
|
|
368
|
+
|
|
369
|
+
A footnote's number is a `Reference` and its body a `Note` with its own identifier, inside the paragraph that cites it. A contents page is a `TOC` element with a `TOCI` for each entry, each holding its links; its dots are marked as decoration.
|
|
370
|
+
|
|
371
|
+
Headers and footers are marked as page furniture rather than content: screen readers skip them, and Lamina PDF's own extraction leaves them out of the text.
|
|
372
|
+
|
|
373
|
+
## What to change
|
|
374
|
+
|
|
375
|
+
- **A cover:** give the template a first page with `with_first_page`, and start the body with a page break.
|
|
376
|
+
- **Another typeface for a word:** add a piece of text in a `TextStyle` with that font. List fallback fonts with `with_fallback` for characters the first font lacks.
|
|
377
|
+
- **Page numbers only:** pass a footer with one centre slot, `"{page}"`, and no header.
|
|
378
|
+
|
|
379
|
+
Next: [Accessible PDFs](../../pages/guides/accessible-pdfs.md), for what PDF/UA-1 checks and how to meet it.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Text and fonts"
|
|
3
|
+
description: "Draw text in any script with your own fonts or the standard 14, one line at a time or as a paragraph that wraps in a box."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/guides/fonts-and-text/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Text and fonts
|
|
9
|
+
|
|
10
|
+
Draw text in any script with your own fonts or the standard 14, one line at a time or as a paragraph that wraps in a box.
|
|
11
|
+
|
|
12
|
+
You'll draw text on a page: single lines, and paragraphs that wrap to fit a box, in any script and direction. You need this whenever a PDF holds words, from a one-line label to a report in five languages.
|
|
13
|
+
|
|
14
|
+
Text needs the text module: `init({ text: true })` or `loadText()` in TypeScript, and the default `text` feature in Rust.
|
|
15
|
+
|
|
16
|
+
## Pick a font
|
|
17
|
+
|
|
18
|
+
Lamina PDF draws with two kinds of font:
|
|
19
|
+
|
|
20
|
+
- **The [standard 14 fonts](../../pages/reference/glossary.md#standard-14-fonts)** (`Font.standard("Helvetica")`, `Standard14::Helvetica` in Rust) need no font file and aren't embedded, because every PDF reader has them. They cover Latin text only, without shaping.
|
|
21
|
+
- **Your own fonts** (`Font.fromBytes(bytes)`: TrueType, OpenType or a font collection) handle any script. Lamina PDF shapes their text, so letters join and ligatures form, and embeds only the glyphs you draw.
|
|
22
|
+
|
|
23
|
+
A font is embedded once per document, however many `Font` objects name it.
|
|
24
|
+
|
|
25
|
+
## Draw a line or a paragraph
|
|
26
|
+
|
|
27
|
+
`surface.text(str, { font, size, at })` draws one line, with `at[1]` its baseline. `surface.textBox(str, { font, size, box, direction, align })` wraps a paragraph to fit its box and lays out each line's words in display order, so right-to-left text reads correctly. When the text doesn't fit, it reports `overflow`.
|
|
28
|
+
|
|
29
|
+
This example draws five scripts (Latin with ligatures, Arabic, Devanagari, Hebrew and Chinese), each in its own embedded font:
|
|
30
|
+
|
|
31
|
+
**TypeScript**
|
|
32
|
+
|
|
33
|
+
`packages/sdk/examples/multilingual.ts`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// Five scripts on one page, each as a single line and as a wrapped paragraph. The scripts are
|
|
37
|
+
// Latin, Arabic and Hebrew (right to left), Devanagari and simplified Chinese; each is shaped and
|
|
38
|
+
// bidi-resolved, and each paragraph wraps in a text box. Each script needs a font that covers it,
|
|
39
|
+
// such as the Noto fonts (OFL).
|
|
40
|
+
import { Builder, type BuilderOptions, type Direction, Font } from "@laminapdf/sdk";
|
|
41
|
+
|
|
42
|
+
/** Each script's line, its paragraph and the paragraph's direction, in the order of the fonts. */
|
|
43
|
+
export const SCRIPTS: readonly (readonly [line: string, paragraph: string, direction: Direction])[] = [
|
|
44
|
+
[
|
|
45
|
+
"Latin: the office staff fill the affine waffle flask",
|
|
46
|
+
"A paragraph breaks at spaces and fits the width of its box, line by line.",
|
|
47
|
+
"ltr",
|
|
48
|
+
],
|
|
49
|
+
["مرحبا بالعالم السلام عليكم", "مرحبا بالعالم السلام عليكم مرحبا بالعالم السلام عليكم", "rtl"],
|
|
50
|
+
["नमस्ते दुनिया। हिन्दी पाठ का परीक्षण", "नमस्ते दुनिया। हिन्दी पाठ का परीक्षण नमस्ते दुनिया।", "ltr"],
|
|
51
|
+
["שלום עולם וברוכים הבאים", "שלום עולם וברוכים הבאים שלום עולם וברוכים הבאים", "rtl"],
|
|
52
|
+
["你好世界,这是中文字体子集测试。", "你好世界,这是中文字体子集测试。你好世界,这是中文。", "ltr"],
|
|
53
|
+
];
|
|
54
|
+
const MARGIN = 56;
|
|
55
|
+
const FIRST_BASELINE = 80;
|
|
56
|
+
/** The distance from one script's line to the next one's. */
|
|
57
|
+
const SCRIPT_HEIGHT = 140;
|
|
58
|
+
/** The paragraph's box: this far below the line, this wide and this tall. */
|
|
59
|
+
const PARAGRAPH_DROP = 14;
|
|
60
|
+
const PARAGRAPH_WIDTH = 220;
|
|
61
|
+
const PARAGRAPH_HEIGHT = 100;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The document's bytes. Needs `init({ text: true })`.
|
|
65
|
+
*
|
|
66
|
+
* @param fonts - one font file per script, in the order of {@link SCRIPTS}.
|
|
67
|
+
* @param options - can fix the file ID, the producer and the creation date.
|
|
68
|
+
*/
|
|
69
|
+
export function multilingual(fonts: readonly Uint8Array[], options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
|
|
70
|
+
if (fonts.length !== SCRIPTS.length) throw new Error(`${SCRIPTS.length} fonts, one per script, are needed`);
|
|
71
|
+
using b = new Builder(options);
|
|
72
|
+
const loaded = fonts.map((bytes) => Font.fromBytes(bytes));
|
|
73
|
+
for (const font of loaded) b.font(font);
|
|
74
|
+
const page = b.page("A4");
|
|
75
|
+
const s = page.surface();
|
|
76
|
+
SCRIPTS.forEach(([line, paragraph, direction], row) => {
|
|
77
|
+
const font = loaded[row];
|
|
78
|
+
if (font === undefined) return;
|
|
79
|
+
const y = FIRST_BASELINE + SCRIPT_HEIGHT * row;
|
|
80
|
+
s.text(line, { font, size: 16, at: [MARGIN, y] });
|
|
81
|
+
// A narrow box breaks the paragraph into lines; right-to-left paragraphs start at the right.
|
|
82
|
+
const box = [MARGIN, y + PARAGRAPH_DROP, PARAGRAPH_WIDTH, PARAGRAPH_HEIGHT] as const;
|
|
83
|
+
s.textBox(paragraph, { font, size: 12, box, direction, align: "start" });
|
|
84
|
+
});
|
|
85
|
+
page.finish();
|
|
86
|
+
using doc = b.finish();
|
|
87
|
+
return doc.save();
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Rust**
|
|
92
|
+
|
|
93
|
+
`crates/laminapdf/examples/multilingual.rs`:
|
|
94
|
+
|
|
95
|
+
````rust
|
|
96
|
+
//! Five scripts on one page, each as a single line and as a wrapped paragraph.
|
|
97
|
+
//!
|
|
98
|
+
//! The scripts are Latin, Arabic and Hebrew (right to left), Devanagari and simplified Chinese;
|
|
99
|
+
//! each is shaped and bidi-resolved, and each paragraph wraps in a text box. Each script needs a
|
|
100
|
+
//! font that covers it, such as the Noto fonts (OFL).
|
|
101
|
+
//!
|
|
102
|
+
//! ```sh
|
|
103
|
+
//! cargo run -p laminapdf --example multilingual -- latin.ttf arabic.ttf devanagari.ttf hebrew.ttf chinese.otf
|
|
104
|
+
//! ```
|
|
105
|
+
|
|
106
|
+
use std::error::Error;
|
|
107
|
+
use std::path::PathBuf;
|
|
108
|
+
|
|
109
|
+
use laminapdf::{
|
|
110
|
+
Alignment, BuildOptions, Builder, Direction, PageSize, PointPt, Pt, RectPt, SaveOptions, TextBoxOptions,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/// Each script's line, its paragraph and the paragraph's direction, in the order of the fonts.
|
|
114
|
+
const SCRIPTS: [(&str, &str, Direction); 5] = [
|
|
115
|
+
(
|
|
116
|
+
"Latin: the office staff fill the affine waffle flask",
|
|
117
|
+
"A paragraph breaks at spaces and fits the width of its box, line by line.",
|
|
118
|
+
Direction::LeftToRight,
|
|
119
|
+
),
|
|
120
|
+
(
|
|
121
|
+
"مرحبا بالعالم السلام عليكم",
|
|
122
|
+
"مرحبا بالعالم السلام عليكم مرحبا بالعالم السلام عليكم",
|
|
123
|
+
Direction::RightToLeft,
|
|
124
|
+
),
|
|
125
|
+
(
|
|
126
|
+
"नमस्ते दुनिया। हिन्दी पाठ का परीक्षण",
|
|
127
|
+
"नमस्ते दुनिया। हिन्दी पाठ का परीक्षण नमस्ते दुनिया।",
|
|
128
|
+
Direction::LeftToRight,
|
|
129
|
+
),
|
|
130
|
+
(
|
|
131
|
+
"שלום עולם וברוכים הבאים",
|
|
132
|
+
"שלום עולם וברוכים הבאים שלום עולם וברוכים הבאים",
|
|
133
|
+
Direction::RightToLeft,
|
|
134
|
+
),
|
|
135
|
+
(
|
|
136
|
+
"你好世界,这是中文字体子集测试。",
|
|
137
|
+
"你好世界,这是中文字体子集测试。你好世界,这是中文。",
|
|
138
|
+
Direction::LeftToRight,
|
|
139
|
+
),
|
|
140
|
+
];
|
|
141
|
+
const MARGIN: f64 = 56.0;
|
|
142
|
+
const FIRST_BASELINE: f64 = 80.0;
|
|
143
|
+
/// The distance from one script's line to the next one's.
|
|
144
|
+
const SCRIPT_HEIGHT: f64 = 140.0;
|
|
145
|
+
/// The paragraph's box: this far below the line, this wide and this tall.
|
|
146
|
+
const PARAGRAPH_DROP: f64 = 14.0;
|
|
147
|
+
const PARAGRAPH_WIDTH: f64 = 220.0;
|
|
148
|
+
const PARAGRAPH_HEIGHT: f64 = 100.0;
|
|
149
|
+
|
|
150
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
151
|
+
let mut args: Vec<String> = std::env::args().skip(1).collect();
|
|
152
|
+
let out = PathBuf::from(if args.len() > SCRIPTS.len() {
|
|
153
|
+
args.remove(SCRIPTS.len())
|
|
154
|
+
} else {
|
|
155
|
+
"target/examples-out".into()
|
|
156
|
+
});
|
|
157
|
+
let fonts = args.iter().map(std::fs::read).collect::<Result<Vec<_>, _>>()?;
|
|
158
|
+
std::fs::create_dir_all(&out)?;
|
|
159
|
+
std::fs::write(out.join("multilingual.pdf"), build(BuildOptions::new(), fonts)?)?;
|
|
160
|
+
Ok(())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// The document's bytes, with one font per script in `fonts` (in the order of [`SCRIPTS`]).
|
|
164
|
+
/// `options` can fix the file ID, the producer and the creation date.
|
|
165
|
+
pub(crate) fn build(options: BuildOptions, fonts: Vec<Vec<u8>>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
166
|
+
if fonts.len() != SCRIPTS.len() {
|
|
167
|
+
return Err(format!("{} fonts, one per script, are needed", SCRIPTS.len()).into());
|
|
168
|
+
}
|
|
169
|
+
let mut b = Builder::new(options);
|
|
170
|
+
let fonts = fonts
|
|
171
|
+
.into_iter()
|
|
172
|
+
.map(|bytes| b.font_bytes(bytes))
|
|
173
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
174
|
+
let mut page = b.page(PageSize::A4);
|
|
175
|
+
let s = page.surface();
|
|
176
|
+
for (row, ((line, paragraph, direction), font)) in (0_u32..).zip(SCRIPTS.iter().zip(&fonts)) {
|
|
177
|
+
let y = FIRST_BASELINE + SCRIPT_HEIGHT * f64::from(row);
|
|
178
|
+
s.text(line, font, Pt::new(16.0), PointPt::from_points(MARGIN, y))?;
|
|
179
|
+
// A narrow box breaks the paragraph into lines; right-to-left paragraphs start at the right.
|
|
180
|
+
let options = TextBoxOptions::new()
|
|
181
|
+
.with_direction(*direction)
|
|
182
|
+
.with_align(Alignment::Start);
|
|
183
|
+
let area = RectPt::from_points(MARGIN, y + PARAGRAPH_DROP, PARAGRAPH_WIDTH, PARAGRAPH_HEIGHT);
|
|
184
|
+
s.text_box(paragraph, font, Pt::new(12.0), area, &options)?;
|
|
185
|
+
}
|
|
186
|
+
page.finish()?;
|
|
187
|
+
let mut doc = b.finish()?;
|
|
188
|
+
Ok(doc.save(&SaveOptions::new())?.bytes)
|
|
189
|
+
}
|
|
190
|
+
````
|
|
191
|
+
|
|
192
|
+
**Result:** [multilingual.pdf](https://docs.laminapdf.com/examples/pdf/multilingual.pdf)
|
|
193
|
+
|
|
194
|
+
[See the output](../../pages/recipes/multilingual.md). Text extracted from it, with pdf.js, pypdf or pdftotext, equals the input strings, ligatures included.
|
|
195
|
+
|
|
196
|
+
## When a font can't draw the text
|
|
197
|
+
|
|
198
|
+
- **A missing character:** text a font can't encode fails with `missing-glyph`, never with a blank box.
|
|
199
|
+
- **Too many letters for a standard 14 font:** a standard 14 font holds at most 37 distinct letters beyond its Western European set (WinAnsi); one more fails with `encoding-full`. Use your own font instead.
|
|
200
|
+
- **A font that may not be embedded:** a font whose licence bits forbid embedding (restricted, or bitmap only) fails with `font-not-embeddable`. In Rust, `EmbedPolicy::Override` embeds it anyway, if you hold the rights. A font marked "no subsetting" is embedded whole.
|
|
201
|
+
|
|
202
|
+
> **Details:** Lamina PDF shapes your own fonts' text with HarfRust, a Rust port of the HarfBuzz shaper, and subsets each font to the glyphs you draw. It embeds them as [Type 0](../../pages/reference/glossary.md#type-0) fonts with `/W` widths and a [ToUnicode](../../pages/reference/glossary.md#tounicode) [CMap](../../pages/reference/glossary.md#cmap), which is what lets the text be extracted again. Line breaking follows [UAX #14](https://www.unicode.org/reports/tr14/), and the order of mixed-direction text follows [UAX #9](https://www.unicode.org/reports/tr9/) (bidi). The licence bits are the font's `fsType`.
|
|
203
|
+
|
|
204
|
+
Next: [shapes, colour and images](../../pages/guides/images-and-vectors.md).
|