@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,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Element flow"
|
|
3
|
+
description: "A report whose title, paragraphs and photo each start below wherever the one before ended, with a body that continues on a new page."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/element-flow/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Element flow
|
|
9
|
+
|
|
10
|
+
A report whose title, paragraphs and photo each start below wherever the one before ended, with a body that continues on a new page.
|
|
11
|
+
|
|
12
|
+
You'll lay out a report where each part starts below wherever the part before it ended: a title, a summary, optional notes, a photo and a body that runs onto a second page. Use it when the length of your text changes from one document to the next.
|
|
13
|
+
|
|
14
|
+
The function takes a JPEG's bytes and the report's text, and returns the PDF's bytes. In TypeScript it draws text, so it needs `init({ text: true })`. Both languages write the same bytes:
|
|
15
|
+
|
|
16
|
+
**TypeScript**
|
|
17
|
+
|
|
18
|
+
`packages/sdk/examples/element-flow.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// A report whose title, paragraphs and photo each start below wherever the one before ended. Each
|
|
22
|
+
// text box reports its `bottom`, and the next element starts a gap below it, so a summary that
|
|
23
|
+
// wraps moves everything after it and empty notes take no space. The body continues on new pages
|
|
24
|
+
// from the top margin. Needs `init({ text: true })`.
|
|
25
|
+
import { Builder, type BuilderOptions, Font, Image, PAGE_SIZES, type Surface, type TextLayout } from "@laminapdf/sdk";
|
|
26
|
+
|
|
27
|
+
const [PAGE_WIDTH, PAGE_HEIGHT] = PAGE_SIZES.A5;
|
|
28
|
+
const MARGIN = 48;
|
|
29
|
+
const WIDTH = PAGE_WIDTH - 2 * MARGIN;
|
|
30
|
+
const BOTTOM = PAGE_HEIGHT - MARGIN;
|
|
31
|
+
const TITLE = 18;
|
|
32
|
+
const BODY = 11;
|
|
33
|
+
const GAP = 10;
|
|
34
|
+
/** The photo's height, for a 3:2 photo across the text column. */
|
|
35
|
+
const PHOTO_HEIGHT = WIDTH / 1.5;
|
|
36
|
+
|
|
37
|
+
/** What the report says. Empty `notes` take no space; blank lines separate the body's paragraphs. */
|
|
38
|
+
export interface Report {
|
|
39
|
+
readonly title: string;
|
|
40
|
+
readonly summary: string;
|
|
41
|
+
readonly notes: string;
|
|
42
|
+
readonly body: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The report's bytes, with `photo` (a JPEG) below its notes.
|
|
47
|
+
*
|
|
48
|
+
* @param options - can fix the file ID, the producer and the creation date.
|
|
49
|
+
*/
|
|
50
|
+
export function elementFlow(photo: Uint8Array, report: Report, options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
|
|
51
|
+
using b = new Builder(options);
|
|
52
|
+
const fonts = { bold: Font.standard("Helvetica-Bold"), regular: Font.standard("Helvetica") };
|
|
53
|
+
const jpeg = Image.jpeg(photo);
|
|
54
|
+
let page = b.page("A5");
|
|
55
|
+
let y = header(page.surface(), fonts, jpeg, report);
|
|
56
|
+
let [rest, newPage] = [report.body.trimEnd(), false];
|
|
57
|
+
for (;;) {
|
|
58
|
+
const layout = page.surface().textBox(rest, { font: fonts.regular, size: BODY, box: area(y) });
|
|
59
|
+
// A clipped box still drew its first lines: the rest goes on a new page, from its top.
|
|
60
|
+
const cut = layout.overflow;
|
|
61
|
+
if (cut === undefined) break;
|
|
62
|
+
if (cut === 0 && newPage) throw new Error("a line of the body is taller than the page");
|
|
63
|
+
[rest, y, newPage] = [rest.slice(cut).trimStart(), MARGIN, true];
|
|
64
|
+
page.finish();
|
|
65
|
+
page = b.page("A5");
|
|
66
|
+
}
|
|
67
|
+
page.finish();
|
|
68
|
+
using doc = b.finish();
|
|
69
|
+
return doc.save();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The title with a rule under it, the summary, the notes and the photo, each below the one before;
|
|
74
|
+
* returns where the body starts. Text that doesn't fit throws `text-overflow`.
|
|
75
|
+
*/
|
|
76
|
+
function header(s: Surface, fonts: { bold: Font; regular: Font }, photo: Image, report: Report): number {
|
|
77
|
+
const fit = (text: string, font: Font, size: number, top: number) =>
|
|
78
|
+
below(s.textBox(text, { font, size, box: area(top), overflow: "error" }), top);
|
|
79
|
+
let y = fit(report.title, fonts.bold, TITLE, MARGIN);
|
|
80
|
+
// A decoration half a gap above the summary. It's stroked: a fill colour would colour the text too.
|
|
81
|
+
s.strokeColor([0.6])
|
|
82
|
+
.path((p) => p.rect([MARGIN, y - GAP / 2, WIDTH, 0]))
|
|
83
|
+
.stroke();
|
|
84
|
+
for (const text of [report.summary, report.notes]) y = fit(text, fonts.regular, BODY, y);
|
|
85
|
+
if (y + PHOTO_HEIGHT > BOTTOM) throw new Error("the photo doesn't fit below the notes");
|
|
86
|
+
s.image(photo, [MARGIN, y, WIDTH, PHOTO_HEIGHT]);
|
|
87
|
+
return y + PHOTO_HEIGHT + GAP;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Where the next element starts: a gap below `layout`'s last drawn line, or `y` if it drew none. */
|
|
91
|
+
function below(layout: TextLayout, y: number): number {
|
|
92
|
+
return layout.bottom === undefined ? y : layout.bottom + GAP;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The rest of the page's text column, from `y` down to the bottom margin. */
|
|
96
|
+
function area(y: number): [number, number, number, number] {
|
|
97
|
+
return [MARGIN, y, WIDTH, Math.max(BOTTOM - y, 0)];
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Rust**
|
|
102
|
+
|
|
103
|
+
`crates/laminapdf/examples/element_flow.rs`:
|
|
104
|
+
|
|
105
|
+
```rust
|
|
106
|
+
//! A report whose title, paragraphs and photo each start below wherever the one before ended.
|
|
107
|
+
//!
|
|
108
|
+
//! Each text box reports its `bottom`, and the next element starts a gap below it, so a summary
|
|
109
|
+
//! that wraps moves everything after it and empty notes take no space. The body continues on new
|
|
110
|
+
//! pages from the top margin. Run it with `cargo run -p laminapdf --example element_flow -- photo.jpg`.
|
|
111
|
+
|
|
112
|
+
use std::error::Error;
|
|
113
|
+
|
|
114
|
+
use laminapdf::{
|
|
115
|
+
BuildOptions, Builder, Color, FontHandle, Image, Overflow, PageSize, Pt, RectPt, SaveOptions,
|
|
116
|
+
Standard14::{Helvetica, HelveticaBold},
|
|
117
|
+
StrokeStyle, Surface, TextBoxOptions, TextLayout,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const PAGE: PageSize = PageSize::A5;
|
|
121
|
+
const MARGIN: f64 = 48.0;
|
|
122
|
+
const WIDTH: f64 = PAGE.width.get() - 2.0 * MARGIN;
|
|
123
|
+
const BOTTOM: f64 = PAGE.height.get() - MARGIN;
|
|
124
|
+
const TITLE: Pt = Pt::new(18.0);
|
|
125
|
+
const BODY: Pt = Pt::new(11.0);
|
|
126
|
+
const GAP: f64 = 10.0;
|
|
127
|
+
const PHOTO_HEIGHT: f64 = WIDTH / 1.5; // a 3:2 photo across the text column
|
|
128
|
+
|
|
129
|
+
/// What the report says. Empty `notes` take no space; blank lines separate the body's paragraphs.
|
|
130
|
+
pub(crate) struct Report<'a> {
|
|
131
|
+
pub(crate) title: &'a str,
|
|
132
|
+
pub(crate) summary: &'a str,
|
|
133
|
+
pub(crate) notes: &'a str,
|
|
134
|
+
pub(crate) body: &'a str,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
138
|
+
let mut args = std::env::args().skip(1);
|
|
139
|
+
let photo = std::fs::read(args.next().ok_or("usage: element_flow <photo.jpg> [out-dir]")?)?;
|
|
140
|
+
let out = std::path::PathBuf::from(args.next().unwrap_or_else(|| "target/examples-out".into()));
|
|
141
|
+
let report = Report {
|
|
142
|
+
title: "Third-quarter report",
|
|
143
|
+
summary: "Revenue rose twelve per cent over the quarter, led by the new reader and its offline mode.",
|
|
144
|
+
notes: "Figures are unaudited.",
|
|
145
|
+
body: "Pages now appear as soon as they are read, and support requests about slow files fell by a third.",
|
|
146
|
+
};
|
|
147
|
+
let pdf = build(BuildOptions::new(), photo, &report)?;
|
|
148
|
+
std::fs::create_dir_all(&out)?;
|
|
149
|
+
Ok(std::fs::write(out.join("element_flow.pdf"), pdf)?)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// The report's bytes, with `photo` (a JPEG) below its notes. `options` can fix the file ID, the
|
|
153
|
+
/// producer and the creation date.
|
|
154
|
+
pub(crate) fn build(options: BuildOptions, photo: Vec<u8>, report: &Report) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
155
|
+
let mut b = Builder::new(options);
|
|
156
|
+
let fonts = [b.standard_font(HelveticaBold)?, b.standard_font(Helvetica)?];
|
|
157
|
+
let photo = Image::jpeg(photo)?;
|
|
158
|
+
let mut page = b.page(PAGE);
|
|
159
|
+
let mut y = header(page.surface(), &fonts, &photo, report)?;
|
|
160
|
+
let (mut rest, mut new_page) = (report.body.trim_end(), false);
|
|
161
|
+
loop {
|
|
162
|
+
let s = page.surface();
|
|
163
|
+
let layout = s.text_box(rest, &fonts[1], BODY, area(y), &TextBoxOptions::new())?;
|
|
164
|
+
// A clipped box still drew its first lines: the rest goes on a new page, from its top.
|
|
165
|
+
let Some(cut) = layout.overflow else { break };
|
|
166
|
+
if cut == 0 && new_page {
|
|
167
|
+
return Err("a line of the body is taller than the page".into());
|
|
168
|
+
}
|
|
169
|
+
(rest, y, new_page) = (rest[cut..].trim_start(), MARGIN, true);
|
|
170
|
+
page.finish()?;
|
|
171
|
+
page = b.page(PAGE);
|
|
172
|
+
}
|
|
173
|
+
page.finish()?;
|
|
174
|
+
Ok(b.finish()?.save(&SaveOptions::new())?.bytes)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// The title with a rule under it, the summary, the notes and the photo, each below the one
|
|
178
|
+
/// before; returns where the body starts. Text that doesn't fit fails `text-overflow`.
|
|
179
|
+
fn header(s: &mut Surface, fonts: &[FontHandle; 2], photo: &Image, report: &Report) -> Result<f64, Box<dyn Error>> {
|
|
180
|
+
let [bold, regular] = fonts;
|
|
181
|
+
let must_fit = TextBoxOptions::new().with_overflow(Overflow::Error);
|
|
182
|
+
let mut y = below(&s.text_box(report.title, bold, TITLE, area(MARGIN), &must_fit)?, MARGIN);
|
|
183
|
+
// A decoration half a gap above the summary. It's stroked: a fill colour would colour the text too.
|
|
184
|
+
let (rule, gray) = (RectPt::from_points(MARGIN, y - GAP / 2.0, WIDTH, 0.0), Color::gray(0.6));
|
|
185
|
+
let line = StrokeStyle::default();
|
|
186
|
+
s.stroke_color(gray).path(|p| _ = p.rect(rule)).stroke(&line);
|
|
187
|
+
for text in [report.summary, report.notes] {
|
|
188
|
+
y = below(&s.text_box(text, regular, BODY, area(y), &must_fit)?, y);
|
|
189
|
+
}
|
|
190
|
+
if y + PHOTO_HEIGHT > BOTTOM {
|
|
191
|
+
return Err("the photo doesn't fit below the notes".into());
|
|
192
|
+
}
|
|
193
|
+
s.image(photo, RectPt::from_points(MARGIN, y, WIDTH, PHOTO_HEIGHT));
|
|
194
|
+
Ok(y + PHOTO_HEIGHT + GAP)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Where the next element starts: a gap below `layout`'s last drawn line, or `y` if it drew none.
|
|
198
|
+
fn below(layout: &TextLayout, y: f64) -> f64 {
|
|
199
|
+
layout.bottom().map_or(y, |bottom| bottom.get() + GAP)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// The rest of the page's text column, from `y` down to the bottom margin.
|
|
203
|
+
fn area(y: f64) -> RectPt {
|
|
204
|
+
RectPt::from_points(MARGIN, y, WIDTH, (BOTTOM - y).max(0.0))
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Result:** [element_flow.pdf](https://docs.laminapdf.com/examples/pdf/element_flow.pdf)
|
|
209
|
+
|
|
210
|
+
The result is two A5 pages. The body fills the first page below the photo and finishes at the top of the second. The Result tab shows it, with a link to the PDF.
|
|
211
|
+
|
|
212
|
+
## How it works
|
|
213
|
+
|
|
214
|
+
Every text box tells you where its text ended, so you never type a position for anything after the first element.
|
|
215
|
+
|
|
216
|
+
- **`bottom` is where the next element can start.** `textBox` returns it with the lines it drew: the bottom of the last line that draws something. Rust has `TextLayout::bottom()`, and `TextRun::bottom()` for a single run of text.
|
|
217
|
+
- **Empty text has no `bottom`.** The notes here are optional. When they're empty, `bottom` is `undefined` (`None` in Rust), `below` keeps `y` where it was, and the photo moves up with no gap left behind.
|
|
218
|
+
- **A longer summary moves everything after it.** When the summary wraps onto one more line, the notes, the photo and the body each move down by one line height, and none of the code changes.
|
|
219
|
+
- **The photo is placed by your own rectangle.** An image reports nothing back, so the code adds the photo's height and the gap to `y` itself.
|
|
220
|
+
- **The rule under the title is a decoration.** It's drawn half a gap above the summary and leaves `y` alone. It's stroked rather than filled: a fill colour stays set and would colour the text drawn after it.
|
|
221
|
+
|
|
222
|
+
## Text that doesn't fit
|
|
223
|
+
|
|
224
|
+
The report treats overflow two ways, and you choose which for each element.
|
|
225
|
+
|
|
226
|
+
The title, summary and notes must fit on the first page. They're drawn with `overflow: "error"` (`Overflow::Error` in Rust), so text that doesn't fit throws `text-overflow` instead of disappearing. The photo must fit too: when it would cross the bottom margin, the code stops with an error of its own.
|
|
227
|
+
|
|
228
|
+
The body may run on. Its box reaches the bottom margin and clips what doesn't fit. Then `overflow` holds the offset of the first character it didn't draw: a UTF-16 offset in TypeScript, a byte offset in Rust. The loop starts a new page and draws the rest from the top margin. Test `overflow` with `!== undefined`, because `0` means not even one line fitted. If no line fits even on a fresh page, a line is taller than the page, so the loop stops with an error instead of adding pages for ever.
|
|
229
|
+
|
|
230
|
+
## What `bottom` measures
|
|
231
|
+
|
|
232
|
+
`bottom` is a typographic measurement in points: the descent of the last drawn line's font, below its baseline. A glyph that reaches lower, or a thick stroke around the text, can paint past it.
|
|
233
|
+
|
|
234
|
+
Like every position, `bottom` is in [page space](../../pages/reference/glossary.md#page-space), measured from the top of the page. It's measured before the surface's transform, so inside `transform` it stays in the coordinates you drew in.
|
|
235
|
+
|
|
236
|
+
In TypeScript you pass a box as a tuple, `[x, y, width, height]`. The lines a text box returns describe their `rect` as an object with `x`, `y`, `width` and `height`.
|
|
237
|
+
|
|
238
|
+
## What stays your job
|
|
239
|
+
|
|
240
|
+
Page breaks stay your decision: the code here makes each one from `overflow`. The builder's surface doesn't paginate, float an image beside text, or lay out tables. In Rust, [Pages that flow](../../pages/guides/flowing-pages.md) paginates for you: headings kept with their text, running headers and "Page X of Y" footers.
|
|
241
|
+
|
|
242
|
+
## What to change
|
|
243
|
+
|
|
244
|
+
- **Paragraphs a gap apart:** draw each paragraph in its own text box and start the next one at `bottom` plus the gap, as the summary and notes do.
|
|
245
|
+
- **A line height for the body:** pass `lineHeight` (`with_line_height` in Rust); `bottom` follows it.
|
|
246
|
+
- **Two columns:** keep a `y` for each column, and when the left column fills, carry on at the top margin of the right one.
|
|
247
|
+
|
|
248
|
+
Next: [the PDF/UA report](../../pages/recipes/pdf-ua-report.md), a report that screen readers can follow.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Extract text for an LLM"
|
|
3
|
+
description: "Turn a PDF into Markdown in reading order, JSON with a box for every block, and chunks sized for retrieval, in TypeScript and Rust."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/extract-markdown/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Extract text for an LLM
|
|
9
|
+
|
|
10
|
+
Turn a PDF into Markdown in reading order, JSON with a box for every block, and chunks sized for retrieval, in TypeScript and Rust.
|
|
11
|
+
|
|
12
|
+
You'll turn a PDF into three outputs an LLM or a search index can use: Markdown in reading order, JSON giving every paragraph and table its page and box, and 400-token chunks.
|
|
13
|
+
|
|
14
|
+
In TypeScript the function returns the three outputs and needs the extract module, `init({ extract: true })`. In Rust, the program instead extracts once and writes the three files. Both give the same JSON:
|
|
15
|
+
|
|
16
|
+
**TypeScript**
|
|
17
|
+
|
|
18
|
+
`packages/sdk/examples/extract-markdown.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// Extract a PDF's text for an LLM: Markdown in reading order, JSON with a box for every block,
|
|
22
|
+
// and chunks sized for retrieval.
|
|
23
|
+
//
|
|
24
|
+
// Tables drawn with lines come out as Markdown tables, and text a reader can't see (white,
|
|
25
|
+
// invisible or off the page) is left out of all three.
|
|
26
|
+
import { type Chunk, Document, type Extraction } from "@laminapdf/sdk";
|
|
27
|
+
|
|
28
|
+
/** What a PDF gives an LLM or a search index. */
|
|
29
|
+
export interface ForAnLlm {
|
|
30
|
+
/** The text in reading order, with a `<!-- page N -->` comment before each page. */
|
|
31
|
+
readonly markdown: string;
|
|
32
|
+
/** Every block with its page and box, the text left out, the file's warnings and the chunks. */
|
|
33
|
+
readonly json: Extraction;
|
|
34
|
+
/** About 400 tokens each, a new one at each heading. */
|
|
35
|
+
readonly chunks: readonly Chunk[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The Markdown, the JSON and the chunks of `bytes`. Needs `init({ extract: true })`. */
|
|
39
|
+
export function extractForAnLlm(bytes: Uint8Array): ForAnLlm {
|
|
40
|
+
using doc = Document.open(bytes);
|
|
41
|
+
// Page markers let a reader cite the page a passage came from.
|
|
42
|
+
const markdown = doc.extract({ format: "markdown", pageMarkers: true });
|
|
43
|
+
// The JSON carries the same chunks, and each block's place in the Markdown.
|
|
44
|
+
const json = doc.extract({ format: "json", pageMarkers: true, chunk: { maxTokens: 400 } });
|
|
45
|
+
return { markdown, json, chunks: json.chunks ?? [] };
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Rust**
|
|
50
|
+
|
|
51
|
+
`crates/laminapdf/examples/extract_markdown.rs`:
|
|
52
|
+
|
|
53
|
+
````rust
|
|
54
|
+
//! Extract a PDF's text for an LLM: Markdown in reading order, JSON with a box for every
|
|
55
|
+
//! block, and chunks sized for retrieval.
|
|
56
|
+
//!
|
|
57
|
+
//! Tables drawn with lines come out as Markdown tables, and text a reader can't see (white,
|
|
58
|
+
//! invisible or off the page) is left out of all three.
|
|
59
|
+
//!
|
|
60
|
+
//! ```sh
|
|
61
|
+
//! cargo run -p laminapdf --example extract_markdown -- <file.pdf> [out-dir]
|
|
62
|
+
//! # writes <out-dir>/<file>.md, <file>.json and <file>.chunks.jsonl
|
|
63
|
+
//! ```
|
|
64
|
+
|
|
65
|
+
use std::error::Error;
|
|
66
|
+
use std::path::{Path, PathBuf};
|
|
67
|
+
|
|
68
|
+
use laminapdf::extract::{ChunkOptions, ExtractOptions, MarkdownOptions, chunks_jsonl};
|
|
69
|
+
use laminapdf::{Document, OpenOptions};
|
|
70
|
+
|
|
71
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
72
|
+
let mut args = std::env::args().skip(1);
|
|
73
|
+
let input = PathBuf::from(args.next().ok_or("usage: extract_markdown <file.pdf> [out-dir]")?);
|
|
74
|
+
let out = PathBuf::from(args.next().unwrap_or_else(|| "target/examples-out".into()));
|
|
75
|
+
std::fs::create_dir_all(&out)?;
|
|
76
|
+
let stem = input.file_stem().and_then(|s| s.to_str()).unwrap_or("document");
|
|
77
|
+
write_outputs(&std::fs::read(&input)?, &out, stem)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Writes the Markdown, the JSON and the chunks of `bytes` into `out`.
|
|
81
|
+
pub(crate) fn write_outputs(bytes: &[u8], out: &Path, stem: &str) -> Result<(), Box<dyn Error>> {
|
|
82
|
+
let doc = Document::open_bytes(bytes.to_vec(), OpenOptions::new())?;
|
|
83
|
+
let extraction = doc.extract(&ExtractOptions::new())?;
|
|
84
|
+
|
|
85
|
+
// Page markers let a reader cite the page a passage came from.
|
|
86
|
+
let markdown = extraction.to_markdown(&MarkdownOptions::new().with_page_markers(true));
|
|
87
|
+
std::fs::write(out.join(format!("{stem}.md")), &markdown.text)?;
|
|
88
|
+
|
|
89
|
+
// About 400 tokens a chunk, starting a new one at each heading.
|
|
90
|
+
let chunks = extraction.chunks(&markdown, &ChunkOptions::new().with_max_tokens(400));
|
|
91
|
+
std::fs::write(out.join(format!("{stem}.chunks.jsonl")), chunks_jsonl(&chunks))?;
|
|
92
|
+
|
|
93
|
+
let json = doc.extract_json(&extraction, &markdown, Some(chunks));
|
|
94
|
+
std::fs::write(out.join(format!("{stem}.json")), json)?;
|
|
95
|
+
Ok(())
|
|
96
|
+
}
|
|
97
|
+
````
|
|
98
|
+
|
|
99
|
+
**Output**
|
|
100
|
+
|
|
101
|
+
`fixtures/extract/examples/invoice.md`:
|
|
102
|
+
|
|
103
|
+
```md
|
|
104
|
+
<!-- page 1 -->
|
|
105
|
+
|
|
106
|
+
# Invoice 42
|
|
107
|
+
|
|
108
|
+
Acme Widgets Ltd · 1 Example Street · London
|
|
109
|
+
|
|
110
|
+
| Widgets, blue (W-100) | 3 | € 36.00 |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| Gadget with fine-tuned dials | 1 | € 79.90 |
|
|
113
|
+
| Café-grade sprockets | 12 | € 15.00 |
|
|
114
|
+
| Delivery | 1 | € 4.50 |
|
|
115
|
+
|
|
116
|
+
Total € 135.40
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Run the Rust program on any PDF: `cargo run -p laminapdf --example extract_markdown -- report.pdf out`.
|
|
120
|
+
|
|
121
|
+
## How it works
|
|
122
|
+
|
|
123
|
+
*Extraction joins a page's text runs into lines and blocks, orders them, then writes Markdown, JSON and chunks.*
|
|
124
|
+
|
|
125
|
+
Extraction starts from each page's text runs. It sets aside runs a reader can't see, and runs the file marks as artifacts, into `dropped` with their reason: `invisible`, `off-page`, `tiny`, `clipped`, `low-contrast`, `covered` or `artifact`. The other runs are joined into lines, and the lines into blocks: paragraphs, headings and tables. Once every page is read, it also sets aside headers, footers and page numbers that repeat across pages, as `running-header`, `running-footer` or `page-number`. Reading order then puts the blocks in sequence: from the file's structure tree when it is tagged and the tree covers the page's text, else by cutting the page into columns. The ordered blocks become the three outputs: Markdown, the `laminapdf.extract/1` JSON and chunks.
|
|
126
|
+
|
|
127
|
+
*On a two-column page, a title across both columns comes first, then the left column, then the right.*
|
|
128
|
+
|
|
129
|
+
A page has a title across both columns and three blocks in each column. Extraction numbers the title 1, the left column's blocks 2 to 4 from top to bottom, and the right column's blocks 5 to 7.
|
|
130
|
+
|
|
131
|
+
- `extract` (`doc.extract` in TypeScript) reads every page's text, joins it into lines and paragraphs, and puts the paragraphs in reading order. A two-column page reads down the left column, then the right; a title across both columns comes first.
|
|
132
|
+
- Each block has a `confidence` from 0 to 1. When the page's columns could be read in more than one order, the page raises the `extract-column-order-uncertain` warning and the blocks in doubt get a confidence of 0.5. A pipeline can check those pages or send them to a layout model.
|
|
133
|
+
- Tables drawn with lines become table blocks with their cells, and Markdown writes them as tables. A cell that spans columns or rows makes the table HTML, since Markdown tables can't span.
|
|
134
|
+
- A table that goes on over a page break comes out as one table in the Markdown, the text and the chunks, with its header once. The JSON keeps one block per page, linked by `continuedBy` and `continues`, so each block's box stays on its own page.
|
|
135
|
+
- Text a reader can't see never reaches the outputs: invisible text (such as the hidden layer of a scanned page), text off the page or too small to read, and text cut away by a clipping path. Nor does near-white text with nothing behind it, or text painted over by a later shape, image or comment. Headers, footers and page numbers that repeat across pages, and text the file marks as an artifact, are left out too. The JSON lists each such run under `dropped`, with the reason and how many characters it held.
|
|
136
|
+
- `to_markdown` (`format: "markdown"` in TypeScript) writes headings, paragraphs and tables, with a `<!-- page N -->` comment before each page, so a passage can be traced to its page.
|
|
137
|
+
- `chunks` (`format: "chunks"`, or the `chunk` option with the JSON) groups whole paragraphs and tables up to the budget, starting a new chunk at each heading. Each chunk records the blocks, pages and boxes it came from, and where its text sits in the Markdown.
|
|
138
|
+
- `extract_json` (`format: "json"`) writes the `laminapdf.extract/1` document: the pages, every block with its box in points from the top-left corner, the dropped text, the file's warnings and the chunks.
|
|
139
|
+
|
|
140
|
+
## What to change
|
|
141
|
+
|
|
142
|
+
- **Only some pages:** `pages: [0, 1]` in TypeScript, `ExtractOptions::new().with_pages(vec![0, 1])` in Rust (page indexes start at 0).
|
|
143
|
+
- **Tables without lines:** `tables: "lattice-and-stream"` (`.with_tables(TableMode::LatticeAndStream)`) also looks for text lined up in columns. It finds more tables, and now and then a pair of columns that isn't one.
|
|
144
|
+
- **Bigger or smaller chunks:** change `maxTokens` (`with_max_tokens`). A token here is about four characters, a rough count that runs low for code, numbers and scripts other than Latin, so leave room.
|
|
145
|
+
- **Overlap between chunks:** `overlap: 80` (`.with_overlap(80)`) repeats up to about 80 tokens of whole paragraphs from the end of the previous chunk.
|
|
146
|
+
- **One output (TypeScript):** set `format` to `"markdown"`, `"text"`, `"json"` or `"chunks"`.
|
|
147
|
+
- **Pages that need OCR:** a scanned page has no text to extract, so its Markdown is empty. Each page in the JSON has a `textLayer` whose `status` says so: `pages.filter(p => p.textLayer.status === "needs-ocr")` in TypeScript, `extraction.pages.iter().filter(|p| p.text_layer.status == TextLayerStatus::NeedsOcr)` in Rust. Send those pages to OCR or a vision model. A page made searchable by OCR is `ocr-layer`, since extraction leaves its invisible text out; the file's warnings name both kinds.
|
|
148
|
+
- **A long file in steps (Rust):** `doc.extract_job(&options)` and `step` do a page per unit of work, so a long file can report progress or stop between steps.
|
|
149
|
+
|
|
150
|
+
[Benchmarks](../../pages/reference/benchmarks.md) has extraction's scores on a public benchmark and how many pages a second it reads.
|
|
151
|
+
|
|
152
|
+
Next: [all recipes](../../pages/recipes.md).
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Generate, then edit"
|
|
3
|
+
description: "Build a one-page draft, open its bytes, retitle it in one transaction and save the edit as a revision after the original bytes."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/generate-then-edit/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Generate, then edit
|
|
9
|
+
|
|
10
|
+
Build a one-page draft, open its bytes, retitle it in one transaction and save the edit as a revision after the original bytes.
|
|
11
|
+
|
|
12
|
+
You'll generate a one-page draft report, then open its bytes, retitle it and save the edit as a revision after them. You need this whenever your app changes a PDF it already has, such as a file a user uploads or one it wrote earlier.
|
|
13
|
+
|
|
14
|
+
One function builds and saves the draft. Another opens those bytes, changes the title and author, and saves again. In TypeScript the draft needs the text module, `init({ text: true })`. Both languages write the same bytes:
|
|
15
|
+
|
|
16
|
+
**TypeScript**
|
|
17
|
+
|
|
18
|
+
`packages/sdk/examples/generate-then-edit.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// Generate a document, then open its bytes, edit them and save incrementally. The edit changes
|
|
22
|
+
// the Info dictionary in one transaction, and the incremental save keeps the generated bytes as
|
|
23
|
+
// they are, with one revision appended.
|
|
24
|
+
import { Builder, type BuilderOptions, Document, Font } from "@laminapdf/sdk";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The generated file followed by the edit's revision. Needs `init({ text: true })`.
|
|
28
|
+
*
|
|
29
|
+
* @param options - can fix the file ID, the producer and the creation date.
|
|
30
|
+
* @param editedAt - the modification date to record, as a PDF date string.
|
|
31
|
+
*/
|
|
32
|
+
export function generateThenEdit(options: BuilderOptions = {}, editedAt?: string): Uint8Array<ArrayBuffer> {
|
|
33
|
+
return edit(generate(options), editedAt);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A one-page draft, saved (a new document is always saved as a full rewrite). */
|
|
37
|
+
export function generate(options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
|
|
38
|
+
using b = new Builder(options);
|
|
39
|
+
const page = b.page("A4");
|
|
40
|
+
page.surface().text("Draft report", { font: Font.standard("Helvetica"), size: 20, at: [72, 96] });
|
|
41
|
+
page.finish();
|
|
42
|
+
b.metadata({ title: "Draft report" });
|
|
43
|
+
using doc = b.finish();
|
|
44
|
+
return doc.save();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Opens `bytes`, edits the Info dictionary in one transaction (which `undo` could revert) and saves incrementally. */
|
|
48
|
+
function edit(bytes: Uint8Array, editedAt?: string): Uint8Array<ArrayBuffer> {
|
|
49
|
+
using doc = Document.open(bytes);
|
|
50
|
+
doc.transact((tx) => tx.setInfo({ title: "Final report", author: "Ada Lovelace" }, editedAt));
|
|
51
|
+
return doc.save({ mode: "incremental" });
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Rust**
|
|
56
|
+
|
|
57
|
+
`crates/laminapdf/examples/generate_then_edit.rs`:
|
|
58
|
+
|
|
59
|
+
````rust
|
|
60
|
+
//! Generate a document, then open its bytes, edit them and save incrementally.
|
|
61
|
+
//!
|
|
62
|
+
//! The edit changes the Info dictionary with an op (undoable and serialisable), and the
|
|
63
|
+
//! incremental save keeps the generated bytes as they are, with one revision appended.
|
|
64
|
+
//!
|
|
65
|
+
//! ```sh
|
|
66
|
+
//! cargo run -p laminapdf --example generate_then_edit -- [out-dir] # writes <out-dir>/generate_then_edit.pdf
|
|
67
|
+
//! ```
|
|
68
|
+
|
|
69
|
+
use std::error::Error;
|
|
70
|
+
use std::path::PathBuf;
|
|
71
|
+
|
|
72
|
+
use laminapdf::{
|
|
73
|
+
BuildOptions, Builder, Document, InfoPatch, Metadata, Op, OpenOptions, PageSize, PdfDate, PointPt, Pt, SaveMode,
|
|
74
|
+
SaveOptions, SetInfo, Standard14,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
78
|
+
let out = PathBuf::from(std::env::args().nth(1).unwrap_or_else(|| "target/examples-out".into()));
|
|
79
|
+
std::fs::create_dir_all(&out)?;
|
|
80
|
+
let bytes = build(BuildOptions::new(), None)?;
|
|
81
|
+
std::fs::write(out.join("generate_then_edit.pdf"), bytes)?;
|
|
82
|
+
Ok(())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// The generated file followed by the edit's revision. `edited_at` is recorded as the
|
|
86
|
+
/// modification date; `options` can fix the file ID, the producer and the creation date.
|
|
87
|
+
pub(crate) fn build(options: BuildOptions, edited_at: Option<PdfDate>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
88
|
+
let generated = generate(options)?;
|
|
89
|
+
edit(generated, edited_at)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// A one-page draft, saved (a new document is always saved as a full rewrite).
|
|
93
|
+
pub(crate) fn generate(options: BuildOptions) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
94
|
+
let mut b = Builder::new(options);
|
|
95
|
+
let helvetica = b.standard_font(Standard14::Helvetica)?;
|
|
96
|
+
let mut page = b.page(PageSize::A4);
|
|
97
|
+
page.surface().text(
|
|
98
|
+
"Draft report",
|
|
99
|
+
&helvetica,
|
|
100
|
+
Pt::new(20.0),
|
|
101
|
+
PointPt::from_points(72.0, 96.0),
|
|
102
|
+
)?;
|
|
103
|
+
page.finish()?;
|
|
104
|
+
b.metadata(Metadata::default().with_title("Draft report"));
|
|
105
|
+
Ok(b.finish()?.save(&SaveOptions::new())?.bytes)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Opens `bytes`, edits the Info dictionary in one transaction (which `undo` could revert) and
|
|
109
|
+
/// saves incrementally: `bytes` unchanged, then the appended revision.
|
|
110
|
+
fn edit(bytes: Vec<u8>, edited_at: Option<PdfDate>) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
111
|
+
let mut doc = Document::open_bytes(bytes, OpenOptions::new())?;
|
|
112
|
+
let patch = InfoPatch::new()
|
|
113
|
+
.title(Some("Final report"))
|
|
114
|
+
.author(Some("Ada Lovelace"));
|
|
115
|
+
doc.apply(vec![Op::from(SetInfo::new(patch, edited_at))])?;
|
|
116
|
+
let options = SaveOptions::new().with_mode(SaveMode::Incremental);
|
|
117
|
+
Ok(doc.save(&options)?.bytes)
|
|
118
|
+
}
|
|
119
|
+
````
|
|
120
|
+
|
|
121
|
+
**Result:** [generate_then_edit.pdf](https://docs.laminapdf.com/examples/pdf/generate_then_edit.pdf)
|
|
122
|
+
|
|
123
|
+
The result is the draft's bytes, unchanged, followed by one revision that retitles it "Final report" and names Ada Lovelace as its author. The Result tab shows it, with a link to the PDF.
|
|
124
|
+
|
|
125
|
+
## How it works
|
|
126
|
+
|
|
127
|
+
- A new document has no original bytes to append to, so the draft's save is a [full rewrite](../../pages/reference/glossary.md#full-rewrite).
|
|
128
|
+
- `Document.open` (`Document::open_bytes` in Rust) reads the draft. Your edits sit beside its bytes and never change them; see [How Lamina PDF thinks about a PDF](../../pages/concepts/how-lamina-thinks.md).
|
|
129
|
+
- `setInfo` changes the Info dictionary: the title, author and other facts a viewer shows in a file's properties. `transact` applies it as one transaction, which `undo` reverts as a whole. In Rust, `apply` takes the list of ops as one transaction.
|
|
130
|
+
- `mode: "incremental"` (`SaveMode::Incremental`) asks for an [incremental save](../../pages/reference/glossary.md#incremental-save): the draft's bytes first, then one revision with the edit.
|
|
131
|
+
- `editedAt` is the modification date to record, a PDF date such as `D:20261030120000Z`. Leave it out and no date is written, because Lamina PDF never reads the clock.
|
|
132
|
+
- The builder also wrote the draft's title into its XMP metadata, an XML copy of the same facts. `setInfo` leaves that copy alone, so it still says "Draft report".
|
|
133
|
+
|
|
134
|
+
## What to change
|
|
135
|
+
|
|
136
|
+
- **More edits in the same revision:** add them inside the callback, such as `tx.rotatePage(doc.page(0).id, 90)` (`RotatePage` in Rust). See [Edit a PDF](../../pages/guides/edit-a-pdf.md).
|
|
137
|
+
- **Removing an entry:** pass `null` for it, as in `{ author: null }` (`.author(None)` in Rust).
|
|
138
|
+
- **Your own file:** open its bytes in place of the draft's. A signed, repaired or encrypted file limits how it saves; [Save a PDF](../../pages/guides/save-a-pdf.md#when-a-file-refuses-a-save) lists the cases.
|
|
139
|
+
- **A whole new file:** pass `mode: "fullRewrite"` (`SaveMode::FullRewrite`), or leave `mode` out and let the save choose. See [Choose a save mode](../../pages/guides/save-a-pdf.md#choose-a-save-mode).
|
|
140
|
+
- **Edits stored as JSON:** replay a saved log with `doc.applyLog({ v: 1, ops })` (`apply_log` in Rust), as [Edit a PDF](../../pages/guides/edit-a-pdf.md#replay-edits-stored-as-json) shows.
|
|
141
|
+
|
|
142
|
+
Next: [a PDF/UA report](../../pages/recipes/pdf-ua-report.md), an accessible report checked as it's built.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Hello, world"
|
|
3
|
+
description: "One A4 page with a line of text in a standard 14 font, the smallest PDF Lamina PDF writes, in TypeScript and Rust."
|
|
4
|
+
version: "0.2.0-alpha.4"
|
|
5
|
+
url: "https://docs.laminapdf.com/recipes/hello/"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Hello, world
|
|
9
|
+
|
|
10
|
+
One A4 page with a line of text in a standard 14 font, the smallest PDF Lamina PDF writes, in TypeScript and Rust.
|
|
11
|
+
|
|
12
|
+
You'll make the smallest PDF Lamina PDF writes: one A4 page that says "Hello, world!" in Helvetica. Start from it to check that a new setup works, or as the skeleton of a document of your own.
|
|
13
|
+
|
|
14
|
+
The function builds the page 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/hello.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// The smallest document: one A4 page with a line of text in a standard 14 font.
|
|
22
|
+
import { Builder, type BuilderOptions, Font } from "@laminapdf/sdk";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The document's bytes. Needs `init({ text: true })`.
|
|
26
|
+
*
|
|
27
|
+
* @param options - can fix the file ID, the producer and the creation date, so that the same
|
|
28
|
+
* input gives the same bytes on every run.
|
|
29
|
+
*/
|
|
30
|
+
export function hello(options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
|
|
31
|
+
using b = new Builder(options);
|
|
32
|
+
const page = b.page("A4");
|
|
33
|
+
// Page space: points from the top-left corner, y down; `at[1]` is the text's baseline.
|
|
34
|
+
page.surface().text("Hello, world!", { font: Font.standard("Helvetica"), size: 24, at: [72, 96] });
|
|
35
|
+
page.finish();
|
|
36
|
+
using doc = b.finish();
|
|
37
|
+
return doc.save();
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Rust**
|
|
42
|
+
|
|
43
|
+
`crates/laminapdf/examples/hello.rs`:
|
|
44
|
+
|
|
45
|
+
````rust
|
|
46
|
+
//! The smallest document: one A4 page with a line of text in a standard 14 font.
|
|
47
|
+
//!
|
|
48
|
+
//! ```sh
|
|
49
|
+
//! cargo run -p laminapdf --example hello -- [out-dir] # writes <out-dir>/hello.pdf (default target/examples-out)
|
|
50
|
+
//! ```
|
|
51
|
+
|
|
52
|
+
use std::error::Error;
|
|
53
|
+
use std::path::PathBuf;
|
|
54
|
+
|
|
55
|
+
use laminapdf::{BuildOptions, Builder, PageSize, PointPt, Pt, SaveOptions, Standard14};
|
|
56
|
+
|
|
57
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
58
|
+
let out = PathBuf::from(std::env::args().nth(1).unwrap_or_else(|| "target/examples-out".into()));
|
|
59
|
+
std::fs::create_dir_all(&out)?;
|
|
60
|
+
std::fs::write(out.join("hello.pdf"), build(BuildOptions::new())?)?;
|
|
61
|
+
Ok(())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// The document's bytes. `options` can fix the file ID, the producer and the creation date, so
|
|
65
|
+
/// that the same input gives the same bytes on every run.
|
|
66
|
+
pub(crate) fn build(options: BuildOptions) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
67
|
+
let mut b = Builder::new(options);
|
|
68
|
+
let helvetica = b.standard_font(Standard14::Helvetica)?;
|
|
69
|
+
let mut page = b.page(PageSize::A4);
|
|
70
|
+
// Page space: points from the top-left corner, y down; `y` is the text's baseline.
|
|
71
|
+
page.surface().text(
|
|
72
|
+
"Hello, world!",
|
|
73
|
+
&helvetica,
|
|
74
|
+
Pt::new(24.0),
|
|
75
|
+
PointPt::from_points(72.0, 96.0),
|
|
76
|
+
)?;
|
|
77
|
+
page.finish()?;
|
|
78
|
+
let mut doc = b.finish()?;
|
|
79
|
+
Ok(doc.save(&SaveOptions::new())?.bytes)
|
|
80
|
+
}
|
|
81
|
+
````
|
|
82
|
+
|
|
83
|
+
**Result:** [hello.pdf](https://docs.laminapdf.com/examples/pdf/hello.pdf)
|
|
84
|
+
|
|
85
|
+
The result is one page with the line near its top-left corner. The Result tab shows it, with a link to the PDF.
|
|
86
|
+
|
|
87
|
+
## How it works
|
|
88
|
+
|
|
89
|
+
- The builder makes an A4 page, and the page's surface is what you draw on.
|
|
90
|
+
- `at` is in points from the top-left corner of the page, with y going down. Its second number is the text's baseline. See [Pages and coordinates](../../pages/concepts/pages-and-coordinates.md).
|
|
91
|
+
- Helvetica is one of the [standard 14 fonts](../../pages/reference/glossary.md#standard-14-fonts), which every PDF viewer has, so the PDF embeds no font.
|
|
92
|
+
- Finishing the builder gives a document, and saving the document gives its bytes. `using` in TypeScript frees both as soon as the function ends.
|
|
93
|
+
|
|
94
|
+
## What to change
|
|
95
|
+
|
|
96
|
+
- **The text and where it goes:** change the string, the size and `at`.
|
|
97
|
+
- **The page size:** name another size, such as `"Letter"`, or pass `[width, height]` in points.
|
|
98
|
+
- **The font:** for characters outside the standard 14 fonts' Latin set, embed a font. See [Text and fonts](../../pages/guides/fonts-and-text.md).
|
|
99
|
+
- **A file ID or a date of your own:** pass them in the builder's options. Without them, the file ID is a hash of the content and the PDF has no creation date, so the same code writes the same bytes on every run.
|
|
100
|
+
|
|
101
|
+
Next: [an invoice](../../pages/recipes/invoice.md), with an embedded font and a table of line items.
|