PaperForge

Document engineering for the BEAM.

Build, secure, validate, and transform documents entirely in Elixir.

Project Status

SignalStatus
Public APIStable 1.x compatibility contract
Production useReady within the documented scope
PackagePublished on Hex
DocumentationPublished on HexDocs
Continuous integrationFormatting, strict compilation, and the full test suite on GitHub Actions
PerformanceReproducible latency, memory, and concurrency benchmarks
RuntimePure Elixir; no installation-time native compilation

A five-page impact report generated entirely with PaperForge

The report above is native PDF output from the maintained paper_forge_1_4_showcase.exs example. The same engine also powers reusable .paperforge templates and scientific documents.

PaperForge is a document platform for Elixir applications. It turns structured content into native PDF object graphs, measured layouts, interactive forms, secure deliverables, imported documents, and production rendering workflows.

No browser, wkhtmltopdf, Chromium, ImageMagick, Ghostscript, or external rendering service is required.

PaperForge 1.4 provides a stable public API for pure-Elixir PDF authoring, versioned declarative templates, reusable design systems, AES-256 document security, PAdES digital signatures, tamper-evident protection, tagged PDF output, bounded concurrent rendering, production telemetry, and reproducible performance tooling. Documented public modules follow Semantic Versioning throughout the 1.x series.

Performance Snapshot

Reference large-document profile on Elixir 1.20.2, OTP 29, 10 schedulers, MIX_ENV=prod, 2 warmups, and 10 measured samples:

InputOutputMedian totalMedian layoutPeak process memory
5,000 table rows179 pages / 616,018 bytes766.95 ms717.66 msapproximately 81 MB

These are reference measurements, not cross-machine guarantees. See Performance Envelope for the runner, methodology, p95 values, concurrency profiles, and reproduction commands.

One Engine, Complete Document Lifecycle

flowchart LR
A["Elixir APIs"] --> IR["Measured Layout IR"]
B[".paperforge templates"] --> IR
C["HTML and Markdown"] --> IR
D["Existing PDF pages"] --> IR
IR --> P["Native PDF"]
P --> S["Secure and sign"]
P --> V["Validate and comply"]
P --> T["Compose and transform"]
P --> R["Render at production scale"]

Use the high-level PaperForge.Flow API for paginated documents, the declarative .paperforge format for reusable templates, or PaperForge.Page for precise drawing. All three paths converge on the same PDF-native engine.

Start Here

GoalStart with
Install and render a first PDFInstallation and Quick Start
Use PaperForge in a Phoenix applicationPhoenix Quick Start and PHOENIX.md
Build reports, invoices, or contractsDocument Authoring
Build PDFs from reusable data templatesDeclarative Documents
Import HTML, Markdown, or PDF pagesImport, Science, Forms, And Review and INTEROPERABILITY.md
Build formulas and scientific reportsSCIENTIFIC.md
Encrypt, sign, or protect documentsSecurity And Protection
Prepare accessible or archival PDFsCompliance And Accessibility
Draw at exact coordinatesLow-level Page API
Configure fonts and UnicodeFonts And Unicode Text
Add JPEG, PNG, or fitted imagesImages
Generate many PDFs safelyConcurrent Rendering
Instrument production rendersTelemetry
Reproduce performance measurementsPerformance Envelope
Understand the internal pipelineARCHITECTURE.md
Review stable public modulesAPI.md
Upgrade an existing applicationMIGRATING.md
Deploy and size production workloadsPRODUCTION.md

Why PaperForge?

PaperForge was built around a simple idea: document generation should be a first-class capability of the BEAM. Elixir applications should not need a browser, an external rendering service, or a platform-specific binary to produce professional documents.

Every layer, from measured layout and declarative templates to signatures, accessibility preparation, scientific notation, and bounded production rendering, is designed to remain deterministic, composable, and natural to operate inside an Elixir system.

PaperForge exists because generating professional PDF documents should not require browsers, external rendering engines, or native compilation.

Design Principles

Designed For

Financial reports, contracts, invoices, government and institutional forms, scientific papers, internal dashboards, certificates, statements, and other documents that need repeatable layout or PDF-native behavior.

Compatibility

SurfaceSupported or tested range
Elixir~> 1.20
Erlang/OTPOTP 29 in CI
PDF headersPDF 1.4 through PDF 1.7
Runtime installationBEAM dependencies only by default

Why Not Just HTML?

HTML-to-PDF tools are a good fit when a browser rendering model is the desired source of truth. PaperForge takes a different approach for applications that need deterministic paged layout and direct access to PDF capabilities.

ConcernTypical browser HTML-to-PDF pipelinePaperForge
Browser processUsually requiredNot required
Platform executableUsually requiredNot required by default
Layout modelBrowser and print CSSDeterministic document Layout IR
Runtime languageMixed process boundaryPure Elixir generation path
PDF-native structuresOften require post-processingBuilt into the document model
Forms, annotations, attachments, signaturesUsually separate toolingFirst-class APIs

Ecosystem

PaperForge
|-- Core document and PDF engine
|-- Flow and Page APIs
|-- .paperforge declarative templates
|-- HTML, Markdown, SVG, and PDF import
|-- Security, signatures, and compliance preparation
|-- Scientific documents and Math AST
`-- Production concurrency, telemetry, and benchmarks
Future directions
|-- Visual Template Studio
|-- Extended interoperability
`-- Distributed generation patterns

Capability Overview

AreaIncluded capabilities
LayoutUnified flow, automatic pagination, keep controls, widow/orphan control, grids, columns, reusable components, and diagnostics
TemplatesNamed and inherited templates, sections, margins, headers, footers, page variants, and total page counts
TablesWrapped cells, measured row heights, repeated headers, row policies, multipage splitting, colspan, rowspan, borders, and vertical alignment
TypographyStandard PDF fonts, embedded TrueType, physical subsetting, real metrics, font families, Unicode maps, rich text, alignment, justification, and deterministic hyphenation
NavigationLinked tables of contents, internal links, named destinations, page-aware references, outlines, and bookmarks
ImagesJPEG and PNG, alpha soft masks, EXIF orientation, deduplication, :contain/:cover, focal points, alignment, and numbered captions
GraphicsLines, rectangles, circles, scientific charts, QR codes, barcodes, and SVG paths, curves, transforms, groups, viewBox, clipping, styles, and text
InteroperabilityCommonMark Markdown and an HTML/CSS subset to Layout IR, classic PDF page import, ordered composition, and reusable resource inventories
Scientific documentsNative Math AST, fractions, roots, matrices, integrals, numbered equations, bibliography entries, citations, footnotes, and page-aware references
PDF featuresAcroForm fields and data workflows, review annotations, metadata, links, attachments, footnotes, endnotes, compression, and PDF 1.4 through 1.7 headers
SecurityAES-256 passwords and permissions, provider-backed PAdES signatures, watermarks, fingerprints, identifiers, and link/attachment policies
Accessibility and archive preparationTagged PDF structure, logical page reading order, language, alternate text metadata, XMP, PDF/UA-1 preparation, ICC output intents, and PDF/A-2b/3b preparation
ProductionStructured validation, deterministic output, incremental file writing, bounded concurrency, retries, resource limits, cancellation, and Telemetry
Declarative authoringVersioned .paperforge templates, typed variables, conditions, loops, components, Layout IR compilation, themes, tokens, and shared design libraries

Choose An API

Use caseAPI
Reports, invoices, statements, and contractsBuild blocks with PaperForge.Flow, then call PaperForge.layout/3
Exact coordinates, labels, or custom graphicsPaperForge.Page
Return a PDF binary for HTTP or object storagePaperForge.to_binary/1
Write a large PDF with lower serialization overheadPaperForge.write/2 or write!/2
Write an encrypted PDFPaperForge.write/3 with security: options
Sign a PDF with a certificatePaperForge.Signature.sign/2 or sign_file/3
Add watermarks and resource policyPaperForge.protect/2
Prepare and validate PDF/A or PDF/UA structurePaperForge.comply/2 and PaperForge.Compliance.validate/2
Render a finite batch and collect all resultsPaperForge.Concurrent.run/3
Consume a backpressured production queue lazilyPaperForge.Concurrent.stream/4
Start a cancellable asynchronous renderPaperForge.Concurrent.start_job/4
Inspect layout decisions and overflowPaperForge.debug/2
Maintain an application built on legacy helpersadd_flow/4 and add_table/4

Import, Science, Forms, And Review

PaperForge 1.4 can turn CommonMark or a practical HTML/CSS subset into the same Layout IR used by native Flow blocks. It can also import pages from classic, unencrypted PDFs, compose documents in order, and inventory reusable resources.

{:ok, intro} = PaperForge.Import.markdown("# Study results\n\nMeasured in Elixir.")
{:ok, appendix} = PaperForge.Import.html("<h2>Appendix</h2><p>Imported HTML.</p>")
{:ok, combined} = PaperForge.Interoperability.compose([first_pdf, second_pdf])

Imported tables have readable defaults but remain CSS-configurable. The safe subset supports element, class, and ID selectors plus practical table styling:

<style>
table {
width: 100%;
border-color: #ccd9d7;
border-width: 0.5pt;
border-collapse: collapse;
}
th {
color: #ffffff;
background-color: #173f52;
padding: 8pt;
text-align: left;
vertical-align: middle;
}
td {
color: #243b46;
padding: 7pt;
line-height: 12pt;
vertical-align: middle;
}
tr:nth-child(even) {
background-color: #f0f7f5;
}
</style>

Supported table declarations include foreground and background colors, font size and style, alignment, vertical alignment, padding, line height, width, border color, border width, border style, border collapse, margins, and even-row striping. Browser layout features such as flexbox, CSS Grid, positioned elements, scripts, and complex pseudo-elements are intentionally outside the deterministic paged-document subset.

The wider HTML importer also supports practical document CSS:

Block backgrounds, borders, and padding are rendered as native PDF geometry; they are not flattened screenshots. Unsupported declarations fail in strict mode or are ignored when strict_css: false is selected explicitly.

Scientific documents use a native Math AST rather than screenshots. Fractions, roots, matrices, scripts, and integrals remain selectable PDF vectors and text. PaperForge.Scientific adds equation numbering, page-aware references, citations, and bibliographies around the existing footnote and TOC engine.

Standard interactive forms are available through PaperForge.AcroForm, with text fields, checkboxes, push buttons, radio groups, list boxes, combo boxes, signature fields, defaults, appearances, restricted calculations, data import/export, and flattening. XFA is intentionally unsupported.

Review workflows can add text notes, highlights, underline and strikeout markup, stamps, free text, shapes, ink, file attachments, links, authorship, subjects, and reply relationships. See INTEROPERABILITY.md and SCIENTIFIC.md for complete examples and supported limits.

Installation

Add PaperForge to your dependencies:

def deps do
[
{:paper_forge, "~> 1.4"}
]
end

Then run:

mix deps.get

To use the GitHub release directly:

def deps do
[
{:paper_forge,
github: "Manuel1471/paper_forge",
tag: "v1.4.0"}
]
end

For local development against main:

def deps do
[
{:paper_forge,
github: "Manuel1471/paper_forge",
branch: "main"}
]
end

Quick Start

alias PaperForge.Flow
{document, report} =
PaperForge.new(compress: true, pdf_version: "1.7")
|> PaperForge.page_template(
:default,
size: :a4,
margins: [top: 72, right: 54, bottom: 72, left: 54],
header: "Quarterly Report",
footer: "Page {page} of {total}"
)
|> PaperForge.layout(
fn flow ->
flow
|> Flow.heading("Quarterly Report", level: 1)
|> Flow.paragraph("""
PaperForge measures, paginates, and renders document blocks automatically.
""")
|> Flow.list(
["Unified layout", "Automatic pagination", "Reusable templates"],
type: :unordered
)
|> Flow.table(
["Metric", "Value"],
[
["Revenue", "$120K"],
["Margin", "24%"]
],
repeat_header: true
)
end,
template: :default
)
IO.inspect(report.pages, label: "Pages")
PaperForge.write!(document, "report.pdf")

Phoenix Quick Start

PaperForge runs inside a Phoenix application like any other Elixir library. It does not require a browser process, command-line renderer, native executable, or separate document service.

Add {:paper_forge, "~> 1.4"} to the Phoenix application's dependencies, run mix deps.get, and return the generated binary from a controller:

def download(conn, %{"id" => id}) do
document = MyApp.Documents.build_report(id)
pdf = PaperForge.to_binary(document)
conn
|> put_resp_content_type("application/pdf")
|> put_resp_header("content-disposition", ~s(attachment; filename="report-#{id}.pdf"))
|> send_resp(:ok, pdf)
end

Use direct binary responses for small and medium documents. For large reports or burst traffic, render through a bounded Task.Supervisor or an optional Oban queue, write to temporary storage, upload the completed artifact, and let Phoenix serve or redirect to the stored file.

The complete guide covers routes, controllers, .paperforge templates, LiveView downloads, supervision, Oban, security, Telemetry, testing, and production limits in PHOENIX.md.

Declarative Documents

PaperForge 1.4 separates document design from application code. A versioned .paperforge JSON file defines variables, conditions, loops, reusable components, styles, themes, and shared layouts. The safe compiler validates input data and produces PaperForge.Flow Layout IR without evaluating Elixir source.

Why Use .paperforge?

Use it when the document design should be editable independently from the application: invoices maintained by operations, reports reused across teams, or customer documents generated from changing data. A template describes the document, JSON supplies the changing information, and reusable component files keep repeated sections consistent. Most authors only need to understand the document they want to build, not Elixir or PDF internals.

Start with one template, declare its variables, arrange blocks, and move any repeated section into kind: "component" files. Validate everything with mix paper_forge.validate before generating PDFs.

For a complete plain-language walkthrough, read PAPERFORGE_TEMPLATES.md. The exhaustive format and security reference remains in DECLARATIVE.md.

{
"version": "1",
"variables": {
"customer": {"type": "string", "required": true},
"items": {"type": "list", "required": true}
},
"blocks": [
{"type": "heading", "text": "Invoice for {{customer}}"},
{
"for": "item in items",
"blocks": [
{"type": "paragraph", "text": "{{item.name}} - {{item.total}}"}
]
}
]
}
with {:ok, template} <- PaperForge.Declarative.load("invoice.paperforge"),
{:ok, document, report} <- PaperForge.Declarative.render(template, invoice_data),
:ok <- PaperForge.write(document, "invoice.pdf") do
IO.inspect(report.pages, label: "Pages")
end

Declarative templates may also define security policy, protection, and compliance. Signature metadata may be declared too. User and owner passwords can live directly in a controlled .paperforge template:

"security": {
"algorithm": "aes_256",
"user_password": "reader-secret",
"owner_password": "owner-secret",
"permissions": {"copy": false, "modify": false}
}

Passwords stored this way are plain text. Shared and production templates should normally inject secrets when writing; runtime values override template values:

PaperForge.Declarative.write(template, data, "contract.pdf",
security: [
user_password: "reader-secret",
owner_password: System.fetch_env!("PDF_OWNER_PASSWORD")
]
)

PaperForge.DesignSystem provides immutable tokens, named styles, visual components, shared layouts, mergeable libraries, and themes with inheritance. Inline design definitions can extend an application-owned design library and override selected values per template.

Production templates may also import design libraries, include document fragments, invoke application-registered trusted components, validate nested data constraints, and use comparison and boolean expressions. Imports and local resources are confined to an explicit root; expansion, data, table, and file-size limits are configurable.

Semantic validation errors identify the source file, exact line and column, JSON path, stable error code, and a human-readable explanation. When a separate data JSON file is used, errors point to that file instead of the template.

Visual components do not require Elixir. Put each component in a readable .paperforge file and list it from the document:

{
"version": "1",
"components": ["components/metrics_section.paperforge"],
"blocks": [
{
"component": "metrics_section",
"props": {"title": "Performance", "metrics": "{{metrics}}"}
}
]
}

Component files declare kind: "component", a name, typed props, optional slots and variants, and ordinary blocks. They may import other component files, which makes complete design libraries possible without application code.

Validate templates in CI without producing a PDF:

mix paper_forge.validate templates/report.paperforge data/report.json \
--root templates --reject-unknown-data

The packaged JSON Schema is available through PaperForge.Declarative.schema_path/0. Compiled templates expose a stable template_id and SHA-256 template_hash; compile_cached/3 provides a bounded process-local cache for templates that do not use registered Elixir components.

See DECLARATIVE.md for the complete format, validation contract, supported blocks, trust boundary, and design-system API.

PaperForge 1.4 templates can also import supported HTML and Markdown, declare native Math AST equations, references, notes, bibliographies, common review annotations, and root-level AcroForms. PDF page import and whole-document composition remain explicit application operations through PaperForge.Interoperability; templates cannot open arbitrary PDF files.

Template Safety And Import Validation

Template-defined style and page-template names remain readable strings unless they already match a public atom. Declarative embedded fonts are mapped to a fixed internal key pool, so compiling untrusted .paperforge input cannot create an unbounded number of BEAM atoms. The public Elixir API remains atom-compatible.

PaperForge.Interoperability.import_pages/3 uses one-based page indexes. 0, negative indexes, indexes beyond the source page count, and unsupported selection values return explicit errors instead of selecting an unintended page:

{:error, :page_index_out_of_range} =
PaperForge.Interoperability.import_pages(source_document, [0])
{:error, :invalid_page_selection} =
PaperForge.Interoperability.import_pages(source_document, :first)

Security And Protection

PaperForge emits a PDF Standard Security Handler using AES-256, revision 6. Passwords, salts, initialization vectors, and derived keys exist only during serialization; they are never stored in PaperForge.Document or a compiled .paperforge template.

PaperForge.write!(document, "contract.pdf",
security: [
user_password: "reader-secret",
owner_password: System.fetch_env!("PDF_OWNER_PASSWORD"),
permissions: [
print: :high_resolution,
copy: false,
modify: false,
extract: false
]
]
)

PaperForge.protect/2 is independent from encryption. It can add a watermark, a deterministic document identifier and SHA-256 content fingerprint, then enforce allowed URI schemes/hosts and attachment count, size, and MIME rules before output:

document =
PaperForge.protect(document,
watermark: [text: "CONFIDENTIAL", opacity: 0.12, angle: 35],
policy: [
allowed_uri_schemes: ["https", "mailto"],
allow_attachments: true,
max_attachment_bytes: 5_000_000,
allowed_attachment_mimes: ["application/pdf", "text/csv"]
]
)
:ok = PaperForge.Protection.verify_fingerprint(document)

Passwords should come from a secret manager or environment at the final write boundary. Do not place passwords in source-controlled .paperforge or JSON data files.

Digital Signatures

PaperForge.Signature adds an incremental PAdES signature without rebuilding the original PDF revision. The default provider uses PKCS#8 PEM keys and X.509 certificate chains through Elixir/OTP only: it requires no executable, NIF, native compilation, or extra system installation.

{:ok, signed_pdf} =
PaperForge.Signature.sign(pdf,
certificate:
{:pkcs8,
key_path: "secrets/signing-key.pem",
cert_path: "secrets/signing-chain.pem",
password: System.get_env("PDF_KEY_PASSWORD")},
reason: "Contract approval",
location: "Monterrey, Mexico",
contact_info: "legal@example.com",
tsa_url: "https://tsa.example.com"
)

PKCS#12/PFX is an explicit compatibility source and uses the locally installed openssl command only when that source is selected:

certificate: {:pkcs12, "certificate.p12", password: System.fetch_env!("CERT_PASSWORD")}

Applications can select another implementation per call with provider: or globally with config :paper_forge, :signature_provider, MyProvider. This is the extension point for HSM, cloud KMS, visible-signature, and multi-signature providers. The built-in provider currently advertises invisible incremental signing and timestamps; requests for visible or multiple signatures return an explicit {:unsupported_capability, capability} error instead of silently degrading the document.

Compliance And Accessibility

PaperForge.comply/2 prepares document structure for PDF/UA-1 and PDF/A-2b or PDF/A-3b. PDF/UA preparation adds tagged structure, page-level logical reading order, language, title display behavior, and XMP identification. Image XObjects can receive alternate text with PaperForge.Compliance.alternate_text/3.

PDF/A preparation requires an application-supplied RGB, Gray, or CMYK ICC profile. PaperForge validates the ICC header, embeds it as an output intent, adds XMP profile identification, and rejects encryption in a PDF/A policy.

document =
PaperForge.comply(document,
profiles: [:pdf_a_3b, :pdf_ua_1],
language: "en-MX",
icc_profile: "priv/color/sRGB.icc"
)
{:ok, report} =
PaperForge.Compliance.validate(document,
profiles: [:pdf_a_3b, :pdf_ua_1]
)

The built-in report validates structures emitted by PaperForge. Archive and accessibility certification should additionally run VeraPDF and manual assistive technology checks against the final document. PDF/A and encryption are mutually exclusive.

Document Authoring

The authoring API builds structured documents on top of PaperForge.Flow. Register shared styles, reusable components, and inherited page templates once, then compose documents from measured blocks instead of calculating page coordinates manually.

alias PaperForge.Flow
document =
PaperForge.new()
|> PaperForge.style(:body, size: 10, line_height: 14)
|> PaperForge.component(:customer, fn assigns ->
Flow.new()
|> Flow.rich_text([
{assigns.name, [weight: :bold]},
{"\n#{assigns.address}", [size: 9]}
])
end)
|> PaperForge.page_template(:base, size: :a4, margins: 54, footer: "Page {page} of {total}")
|> PaperForge.page_template(:invoice, extends: :base, header: "Invoice")
{document, _report} =
PaperForge.layout(document, fn flow ->
flow
|> Flow.table_of_contents()
|> Flow.heading("Invoice")
|> Flow.component(:customer, %{name: "Acme", address: "Monterrey, MX"})
|> Flow.grid(2, ["Subtotal\n$1,200", "Due\n30 days"], cell_height: 60)
|> Flow.columns(2, ["Terms and conditions...", "Payment instructions..."])
end, template: :invoice)

Available authoring blocks include rich_text/3, table_of_contents/2, reference/3, component/4, grid/4, and columns/4. Tables accept explicit :column_widths, :header_fill_color, :header_color, and :stripe_fill_color options. See paper_forge_1_4_showcase.exs for a complete production-style document.

Images support fit: :fill | :contain | :cover, horizontal and vertical alignment, and focal_point: {x, y}. Numbered images and tables create stable destinations for page-aware references.

The complete release example is paper_forge_1_4_showcase.exs. It combines navigation, advanced tables, footnotes, endnotes, charts, SVG, QR, barcode, attachments, components, and custom report panels in one PDF.

Page-aware navigation is resolved with bounded multi-pass pagination:

flow
|> Flow.table_of_contents(title: "Contents")
|> Flow.heading("Financial results", destination: :financial_results)
|> Flow.reference(:financial_results, prefix: "Financial results begin on page ")

Custom blocks receive their measured block_x, block_y, block_width, and block_height in PageContext, so bespoke report panels can participate in normal flow without hard-coding page coordinates.

Typography And Report Visuals

Paragraph blocks can request hyphenation and minimum line counts around page breaks. Layout reports expose :measurements for each placed block.

flow
|> Flow.paragraph(long_copy, hyphenate: true, min_lines_at_top: 2, min_lines_at_bottom: 2)
|> Flow.chart([{"Q1", 418}, {"Q2", 432}, {"Q3", 451}], height: 140)
|> Flow.chart([{"Jan", 32}, {"Feb", 46}, {"Mar", 41}],
chart_type: :line,
color: PaperForge.Color.rgb255(15, 143, 131),
background_color: PaperForge.Color.rgb255(244, 247, 248),
label_color: PaperForge.Color.rgb255(16, 47, 66),
chart_padding: 14,
point_radius: 4
)
|> Flow.chart([{"Research", 62}, {"Operations", 23}, {"Reserve", 15}],
chart_type: :donut,
colors: [
PaperForge.Color.rgb255(15, 143, 131),
PaperForge.Color.rgb255(231, 101, 84),
PaperForge.Color.rgb255(232, 173, 53)
],
background_color: PaperForge.Color.rgb255(237, 247, 247),
label_color: PaperForge.Color.rgb255(16, 47, 66),
chart_padding: 14
)
|> Flow.svg("<svg><rect x='0' y='0' width='80' height='30' fill='#0077b5'/></svg>", height: 40)
|> Flow.qr_code("https://example.com/pay/INV-2048", width: 96, height: 96)
|> Flow.barcode("20481234", width: 180, height: 64)

Advanced Tables And Notes

Table rows are measured from their wrapped cell content. :keep moves an oversized row to a fresh page, :split continues cell content across pages, and :error raises PaperForge.TableError when a row cannot fit.

Use Flow.cell/2 for composable cells with :colspan, :rowspan, :valign, per-cell colors, per-side borders, and nested flow blocks.

flow
|> Flow.table(
["Item", "Description"],
rows,
column_widths: [110, 340],
repeat_header: true,
row_split: :split,
cell_line_height: 12
)
|> Flow.footnote("Values are unaudited and shown in USD.")
|> Flow.endnotes([])

Footnotes reserve space at the bottom of the current flow page, number themselves when the number is omitted, append a visible call marker to the preceding paragraph, rich-text block, heading, or final table cell, and continue on another page when necessary. Pass marker: false to author the call marker manually. Flow.endnotes/3 emits the collected notes as a document section.

Document Options

PaperForge.new/1 accepts:

PaperForge.new()
PaperForge.new(compress: false)
PaperForge.new(pdf_version: "1.4")
PaperForge.new(default_font: :helvetica)

Low-level Page API

Use PaperForge.Page when you need manual graphics, exact coordinates, or a lower-level drawing surface. New structured documents should usually start with PaperForge.Flow and PaperForge.layout/3.

Add a page with default options:

document =
PaperForge.new()
|> PaperForge.add_page(fn page ->
Page.text(page, "Default A4 page", x: 72, y: 750)
end)

Add a page with options:

document =
PaperForge.new()
|> PaperForge.add_page(
[
size: :letter,
orientation: :landscape,
origin: :top_left,
margins: [top: 48, right: 54, bottom: 48, left: 54]
],
fn page ->
Page.text(page, "Landscape page", y: 48)
end
)

Supported page sizes:

:a3
:a4
:a5
:letter
:legal

Custom page sizes use {width, height} in PDF points:

Page.new(size: {500, 700})

All dimensions are expressed in PDF points.

1 point = 1/72 inch

Coordinates And Margins

PaperForge supports both PDF-native bottom-left coordinates and top-left coordinates.

Page.new(origin: :bottom_left)
Page.new(origin: :top_left)

You can also set the origin per operation:

Page.rectangle(page, x: 72, y: 72, width: 100, height: 40, origin: :top_left)

Margins can be uniform:

Page.new(margins: 72)

Or side-specific:

Page.new(margins: [top: 40, right: 50, bottom: 40, left: 50])

Content helpers:

Page.content_width(page)
Page.content_height(page)
Page.content_left(page)
Page.content_top(page)
Page.content_bottom(page)

Text

Draw a single line of text:

Page.text(
page,
"Centered title",
x: Page.content_left(page),
y: 72,
width: Page.content_width(page),
align: :center,
font: :helvetica_bold,
size: 24,
color: Color.black()
)

Draw wrapped multiline text:

Page.text_box(
page,
"""
PaperForge wraps text into multiple lines using built-in font metrics.
Explicit line breaks are preserved.
""",
x: Page.content_left(page),
y: 120,
width: Page.content_width(page),
height: 160,
font: :times_roman,
size: 12,
line_height: 17,
align: :left
)

Supported alignment values:

:left
:center
:right

Fonts And Unicode Text

PaperForge supports two font paths: the 14 standard PDF Type 1 fonts and embedded TrueType fonts.

Standard Type 1 fonts are registered automatically when used:

:helvetica
:helvetica_bold
:helvetica_oblique
:helvetica_bold_oblique
:times_roman
:times_bold
:times_italic
:times_bold_italic
:courier
:courier_bold
:courier_oblique
:courier_bold_oblique
:symbol
:zapf_dingbats

Standard Type 1 fonts are convenient for simple Latin text, but they are not full Unicode fonts. For visible Unicode text, register a TrueType .ttf font before adding pages:

document =
PaperForge.new()
|> PaperForge.register_font(
:inter,
path: "assets/fonts/Inter-Regular.ttf"
)

You can also register a font from an in-memory binary:

document =
PaperForge.register_font(
document,
:inter,
data: File.read!("assets/fonts/Inter-Regular.ttf")
)

Then use the registered key in text operations:

Page.text(
page,
"Unicode sample: café, piñata, Привет, Ω",
x: 72,
y: 720,
font: :inter,
size: 18
)

Declarative templates can register root-confined files or trusted host sources:

{
"fonts": {
"body": {"path": "assets/NotoSans-Regular.ttf", "subset": true},
"body_bold": {"source": "app:noto-sans-bold", "subset": true}
},
"font_fallbacks": {"body": ["symbols"]},
"document": {"default_font": "body"}
}

Applications resolve logical sources with font_sources: %{"app:noto-sans-bold" => font_binary}. This keeps templates portable while preserving the loader's root and filesystem security policy.

Embedded TrueType fonts are written as PDF Type 0 fonts with a CIDFontType2 descendant, Identity-H encoding, a /FontFile2 stream, widths from the TTF hmtx table, and a /ToUnicode CMap so text extraction and search can recover Unicode characters.

Supported embedded font input:

Current limitations:

Font Families

Register related TrueType files as a family:

document =
PaperForge.new()
|> PaperForge.register_font_family(
:inter,
regular: [path: "assets/fonts/Inter-Regular.ttf"],
bold: [path: "assets/fonts/Inter-Bold.ttf"],
italic: [path: "assets/fonts/Inter-Italic.ttf"],
bold_italic: [path: "assets/fonts/Inter-BoldItalic.ttf"]
)

Then select a variant with :weight and :style:

Page.text(
page,
"Important",
x: 72,
y: 720,
font: :inter,
weight: :bold,
style: :italic
)

Set a document default font when most text should use the same font:

document =
PaperForge.new()
|> PaperForge.register_font(:inter, path: "assets/fonts/Inter-Regular.ttf")
|> PaperForge.default_font(:inter)

Shapes

Lines

Page.line(
page,
x1: 72,
y1: 700,
x2: 300,
y2: 700,
width: 2,
color: Color.rgb255(40, 70, 140)
)

Rectangles

Page.rectangle(
page,
x: 72,
y: 560,
width: 220,
height: 100,
fill: true,
stroke: true,
fill_color: Color.rgb255(235, 240, 250),
stroke_color: Color.rgb255(40, 70, 140),
line_width: 2
)

Circles

Page.circle(
page,
x: 400,
y: 610,
radius: 50,
fill: true,
stroke: true,
fill_color: Color.rgb255(245, 180, 70),
stroke_color: Color.rgb255(120, 70, 20),
line_width: 2
)

PaperForge approximates circles using four cubic Bezier curves because PDF does not provide a native circle operator.

Colors

RGB values can be expressed from 0 to 1:

Color.rgb(1.0, 0.0, 0.0)

Or from 0 to 255:

Color.rgb255(255, 0, 0)

Grayscale helpers:

Color.gray(0.5)
Color.black()
Color.white()

Images

Page.image/3 accepts a supported image binary or a file path.

png = File.read!("logo.png")
page
|> Page.image(png, x: 72, y: 120, width: 200)
|> Page.image("photo.jpg", x: 72, y: 360, width: 200, height: 120)

When only one dimension is supplied, PaperForge preserves the source aspect ratio:

Page.image(page, "logo.png", x: 72, y: 120, width: 200)
Page.image(page, "logo.png", x: 72, y: 120, height: 80)

Supported JPEGs:

Supported PNGs:

PNG alpha is written as a PDF soft mask (/SMask). PNG grayscale/RGB images without alpha use the original compressed IDAT data directly with /FlateDecode and PNG predictor decode parameters. JPEG image data is embedded directly with /DCTDecode.

Images are deduplicated by SHA-256 hash, so drawing the same image several times does not embed duplicate image streams.

Unified Flow

PaperForge.flow/2 builds a document from layout blocks instead of manual page operations. The engine measures blocks, paginates them, calculates total pages, and then renders the final pages.

alias PaperForge.Flow
{document, report} =
PaperForge.new()
|> PaperForge.page_template(
:report,
size: :a4,
margins: [top: 72, right: 54, bottom: 72, left: 54],
header: "Quarterly report",
footer: "Page {page} of {total}"
)
|> PaperForge.layout(
fn flow ->
flow
|> Flow.heading("Quarterly report", level: 1)
|> Flow.paragraph("Summary text that wraps and splits across pages.")
|> Flow.list(["Revenue", "Expenses", "Cash"], type: :ordered)
|> Flow.table(
["Metric", "Value"],
[
["Revenue", "$120K"],
["Margin", "24%"]
],
repeat_header: true
)
|> Flow.separator()
|> Flow.page_break()
|> Flow.section(:appendix, [title: "Appendix"], fn section ->
section
|> Flow.paragraph("Section content receives section metadata.")
end)
end,
template: :report
)

The report returned by PaperForge.layout/3 contains page count, block count, placements, warnings, and rendered page values. Placements include block ID, block type, page number, coordinates, dimensions, and section metadata:

{document, report} =
PaperForge.layout(document, flow_function, template: :report)
report.pages
report.blocks
report.placements

Pagination options can be set on flow blocks:

flow
|> Flow.heading("Appendix", page_break_before: true, keep_with_next: true)
|> Flow.paragraph("This paragraph should stay visually connected.")
|> Flow.separator(page_break_after: true)

Sections group related content under a stable section ID. A section can add a title heading, start or end with page breaks, switch to a named page template, and pass section metadata into PageContext:

flow
|> Flow.section(:appendix, [title: "Appendix", template: :appendix], fn section ->
section
|> Flow.paragraph("Appendix content")
end)

Page templates can configure page geometry and reusable header/footer content:

document =
PaperForge.new()
|> PaperForge.page_template(
:appendix,
size: :letter,
orientation: :landscape,
margins: [top: 60, right: 48, bottom: 60, left: 48],
header: fn page, context ->
Page.text(page, "Appendix", x: context.content_left, y: 24)
end,
footer: "Page {page} of {total}"
)

Custom blocks receive the current Page and PageContext:

Flow.custom(flow, fn page, context ->
Page.text(
page,
"Page #{context.page_number} of #{context.total_pages}",
x: context.content_left,
y: context.content_top
)
end, height: 24)

Debug reports summarize the generated document:

PaperForge.debug(document,
show_margins: true,
show_blocks: true,
show_page_breaks: true
)

Existing Page, add_flow/4, and add_table/4 APIs remain supported for compatibility. New applications should prefer PaperForge.Flow and PaperForge.layout/3.

Legacy Flow And Page-level APIs

The APIs in this section remain supported for compatibility. New applications should prefer PaperForge.Flow and PaperForge.layout/3.

Flow text blocks across pages:

document =
PaperForge.new()
|> PaperForge.add_flow(
[
"First paragraph with enough text to wrap.",
"Second paragraph. PaperForge creates new pages as needed."
],
[size: :letter, margins: 72],
font: :helvetica,
size: 11,
line_height: 15,
gap: 8
)

Get flow overflow information:

{document, report} =
PaperForge.layout_flow(
PaperForge.new(),
["A long paragraph", "Another long paragraph"],
[size: :letter, margins: 72],
header: "Quarterly report",
footer: "Generated by PaperForge",
keep_together: true
)
report.pages_added
report.overflow?

Draw a basic table:

page =
page
|> Page.table(
[
["Name", "Score"],
["Ana", 10],
["Luis", 9]
],
x: Page.content_left(page),
y: 96,
width: Page.content_width(page),
header: true
)

Add a URI link annotation:

page =
page
|> Page.text("Project", x: 72, y: 720)
|> Page.link(
"https://github.com/Manuel1471/paper_forge",
x: 72,
y: 700,
width: 180,
height: 24
)

Create internal navigation:

document =
PaperForge.new()
|> PaperForge.add_page(fn page ->
page
|> Page.destination(:intro, y: 720)
|> Page.bookmark("Introduction", y: 720)
|> Page.text("Introduction", x: 72, y: 720)
end)
|> PaperForge.add_page(fn page ->
page
|> Page.text("Back to intro", x: 72, y: 720)
|> Page.link_to(:intro, x: 72, y: 700, width: 120, height: 24)
end)

Add a paginated table with repeated headers:

document =
PaperForge.add_table(
document,
rows,
[size: :a4, margins: 72],
repeat_header: true,
row_split: :keep
)

Metadata

document =
PaperForge.new()
|> PaperForge.metadata(
title: "Mexico Report",
author: "Manuel Garcia",
subject: "Informacion \u65E5\u672C\u8A9E",
keywords: ["report", "elixir", "pdf"],
creator: "PaperForge",
producer: "PaperForge",
creation_date: DateTime.utc_now(),
modification_date: DateTime.utc_now()
)

Metadata is written into the PDF Info dictionary and referenced from the document trailer. Latin-1-compatible strings are stored as PDF literal strings. Other Unicode strings are stored as UTF-16BE hexadecimal strings.

Binary Output

PaperForge can return the complete PDF as a binary:

pdf_binary =
PaperForge.to_binary(document)

This can be used in Phoenix or Plug responses:

conn
|> put_resp_content_type("application/pdf")
|> put_resp_header(
"content-disposition",
~s(attachment; filename="document.pdf")
)
|> send_resp(200, PaperForge.to_binary(document))

Write to disk:

PaperForge.write(document, "document.pdf")
PaperForge.write!(document, "document.pdf")

Architecture

PaperForge separates authoring intent, measured layout, PDF objects, and byte serialization. High-level and low-level APIs share the same compiler and writer.

flowchart TD
F["Flow API / .paperforge / imports"] --> IR["Layout IR"]
IR --> L["Layout Engine"]
L --> P["Page operations"]
X["Page API"] --> P
P --> C["Page Compiler"]
C --> O["PDF object graph and resources"]
O --> S["Serializer"]
S --> W["Atomic or incremental Writer"]
W --> PDF["PDF 1.4-1.7 document"]
LayerPrimary responsibility
Flow, declarative compiler, and importersExpress document intent as reusable blocks
Layout EngineMeasure, paginate, split, and resolve page-aware content
Page and GraphicsRecord exact drawing and annotation operations
Page CompilerTransform coordinates and register fonts, images, and resources
DocumentOwn indirect objects, registries, metadata, navigation, and security state
Serializer and WriterProduce PDF syntax, xref data, trailers, and final output

The generated PDF uses traditional cross-reference tables. Tests verify that xref offsets point to the start of their corresponding indirect objects.

Examples

Examples write generated files under tmp/. Two maintained showcases cover the code-first and no-code authoring paths:

ExampleDemonstrates
examples/paper_forge_1_4_showcase.exsEditorial report using the full-control Elixir API, tagged PDF, protection policy, attachments, navigation, tables, charts, QR, and optional AES-256 output
examples/paper_forge_1_4_showcase.paperforgeSelf-contained no-code report with variables, components, themes, imported HTML/Markdown, numbered Math AST equations, bibliography, AcroForms, annotations, charts, tables, navigation, QR, signing metadata, security policy, and PDF/UA preparation
examples/paper_forge_1_4_html_css.exsFocused HTML and CSS import example showing strict CSS validation, semantic content, page breaks, native Math AST, and aligned AcroForm fields
examples/investigacion.paperforgeSix-page fictional physics study authored declaratively with Math AST equations, a coupling matrix, scientific tables, line/area/scatter/donut charts, references, embedded AES-256 credentials, review fields, and digital-signature metadata
examples/investigacion.exsMinimal delivery runner that emits a password-protected review edition and a digitally signed companion from the same declarative research template

Run an example from the project root:

mix run examples/paper_forge_1_4_showcase.exs
mix run examples/paper_forge_1_4_html_css.exs

Development

Clone the repository:

git clone git@github.com:Manuel1471/paper_forge.git
cd paper_forge

Run the test suite:

mix test

Compile with warnings treated as errors:

mix compile --warnings-as-errors

Format the source code:

mix format

Run all checks:

mix do format, compile --warnings-as-errors, test

Run the TrueType and Unicode benchmark script:

mix run benchmarks/truetype.exs

Scope Boundaries

Performance Envelope

PaperForge 1.1 adds bounded process-local caches for repeated text metrics and Flate-compressed streams. Font metrics use stable font identities, and the PDF writer accumulates serialized objects linearly before producing the final binary. Documents without page-aware contents or references paginate once; navigation-aware documents retain bounded multi-pass convergence. Caches require no server process, remain isolated between render processes, and cannot grow without a fixed limit.

Latency benchmarks

Latency profiles answer: How long does one document take to generate?

They render increasingly large table-based reports with 25, 500, and 5,000 rows. These are the appropriate figures for estimating the latency and memory cost of an individual business document.

Run reproducible small, medium, and large profiles in the production environment:

MIX_ENV=prod SAMPLES=10 WARMUPS=2 mix run benchmarks/render_profiles.exs

Use PROFILE=small, PROFILE=medium, or PROFILE=large to isolate one scale. For release measurements, increase SAMPLES to 30. Each sample runs in a fresh process after explicit garbage collection.

The benchmark reports median, p95, minimum, and maximum values for layout, cold serialization, warm serialization, total time, peak process memory, reductions, garbage collections, and reclaimed words. It also records the Elixir version, OTP release, scheduler count, Mix environment, PDF size, page count, and cache hits. benchmarks/document_scale.exs remains available for comparison with the original 5,000-row baseline. Results are reference measurements rather than fixed guarantees across machines.

The following measurements compare the unchanged 1.0.0 commit with 1.1.0 using the same runner, 2 warmups, 10 samples, Elixir 1.20.2, OTP 29, 10 schedulers, and MIX_ENV=prod:

Profile1.0 median1.1 medianChange1.0 p951.1 p95
25 rows2.28 ms1.83 ms-19.7%2.44 ms1.95 ms
500 rows43.71 ms39.95 ms-8.6%44.93 ms40.54 ms
5,000 rows830.91 ms766.95 ms-7.7%836.93 ms801.19 ms

For the 5,000-row profile, median layout fell from 756.72 ms to 717.66 ms, reductions from 67.1 million to 56.8 million, and garbage collections from 196 to 177. Peak process memory remained effectively flat near 81.0 MB. The generated document remained byte-for-byte identical at 179 pages and 616,018 bytes.

Concurrent Rendering

PaperForge can render independent documents concurrently with bounded demand, per-document isolation, timeouts, cancellation, and runtime metrics. Phoenix applications should own the task supervisor:

children = [
{Task.Supervisor, name: MyApp.PDFSupervisor}
]

Consume a lazy, backpressured stream without loading every result in advance:

MyApp.PDFSupervisor
|> PaperForge.Concurrent.stream(invoices, &InvoicePDF.render/1,
max_concurrency: System.schedulers_online(),
timeout: 30_000,
ordered: false,
job_id: fn invoice, _index -> invoice.id end
)
|> Stream.each(fn
%{status: :ok, value: pdf, id: id} ->
Storage.put_pdf(id, pdf)
%{status: status, error: error, id: id} ->
Logger.error("PDF #{id} failed with #{status}: #{inspect(error)}")
end)
|> Stream.run()

PaperForge.Concurrent.run/3 collects a finite batch and creates a temporary supervisor when the caller does not provide one. start_job/4 returns a task handle that can be passed to cancel/2. Every result reports duration, reductions, and process-local garbage collections.

The concurrency limit bounds active jobs and provides backpressure through the lazy input stream. Errors and timeouts affect only their originating document. Font, image, text-metric, and compression state remains immutable or process-local, avoiding shared mutable cache locks between renders.

Common production options:

OptionPurpose
:max_concurrencyMaximum active render processes; size this against scheduler and memory limits
:timeoutMaximum time allowed for one render attempt
:orderedPreserve input order when true; emit completed work sooner when false
:job_idFunction used to attach an application identifier to each result and event
:max_memory_bytesFail a job that exceeds its configured process-memory budget
:max_reductionsFail a job that exceeds its configured BEAM reduction budget
:max_attemptsTotal attempts allowed for retryable failures
:retry_delayDelay between attempts
:retry_onFailure categories eligible for retry

Successful and failed jobs return a PaperForge.Concurrent.Result with :status, :id, :index, :attempts, :duration_us, :reductions, :garbage_collections, :peak_memory_bytes, and either :value or :error. Possible statuses are :ok, :error, :timeout, and :resource_limit.

Scalability benchmarks

Scalability profiles answer: How does the runtime behave as concurrent demand increases?

The bundled concurrency benchmark deliberately generates minimal, single-page PDFs. It measures scheduling overhead, backpressure, task isolation, and the effect of worker limits. Its throughput must not be interpreted as the expected throughput for invoices, image-heavy reports, embedded fonts, or large tables.

Run the 1,000-document minimal-workload benchmark:

MIX_ENV=prod JOBS=1000 CONCURRENCY=1,10,20 \
mix run benchmarks/concurrent_renders.exs

Minimal single-page workload only:

WorkersBatch timeThroughputFailures
140.82 ms24,500 renders/s0
1015.11 ms66,190 renders/s0
2015.16 ms65,972 renders/s0

These numbers describe concurrency infrastructure overhead. They are not document-generation capacity claims for real invoices or reports.

Representative table-report workloads on the same Elixir 1.20.2, OTP 29, 10-scheduler development machine:

Medium workload: 100 documents, 500 rows and 18 pages each

WorkersBatchThroughputJob medianJob p95Job memory p95BEAM peakFailures
14,390 ms22.78/s40.16 ms49.28 ms10.32 MB95 MB0
51,074 ms93.12/s52.63 ms61.04 ms10.35 MB176 MB0
10836 ms119.69/s80.16 ms98.62 ms10.35 MB252 MB0

Large workload: 20 documents, 5,000 rows and 179 pages each

WorkersBatchThroughputJob medianJob p95Job memory p95BEAM peakFailures
117,320 ms1.15/s824.13 ms962.40 ms102.22 MB308 MB0
55,151 ms3.88/s1,246.59 ms1,409.22 ms102.47 MB1,095 MB0
104,167 ms4.80/s2,060.99 ms2,152.38 ms102.72 MB1,860 MB0

Reproduce these workloads:

MIX_ENV=prod WORKLOAD=medium JOBS=100 CONCURRENCY=1,5,10 \
mix run benchmarks/concurrent_renders.exs
MIX_ENV=prod WORKLOAD=large JOBS=20 CONCURRENCY=1,5,10 \
mix run benchmarks/concurrent_renders.exs

Higher concurrency improves batch throughput but increases per-document latency and total VM memory. For the measured large workload, moving from one to ten workers improves throughput by about 4.2x while increasing peak BEAM memory from roughly 308 MB to 1.86 GB. Choose the worker limit according to the application's latency target, memory budget, queue depth, and schedulers.

Production workloads can additionally set max_memory_bytes, max_reductions, max_attempts, retry_delay, and retry_on. File-oriented jobs should prefer PaperForge.write/2, which now serializes objects incrementally to a temporary file and atomically renames the finished PDF.

See PRODUCTION.md for staged rendering, optional Oban integration, distributed-node strategies, failure recovery, very large documents, and deployment sizing.

Telemetry

PaperForge emits stable Telemetry events for standalone rendering and concurrent production workloads:

EventEmitted
[:paperforge, :render, :start]Before binary or incremental file output
[:paperforge, :render, :stop]After a successful or returned-error render
[:paperforge, :render, :exception]Before an exception, throw, or exit is reraised
[:paperforge, :batch, :job]After every concurrent attempt
[:paperforge, :batch, :complete]After Concurrent.run/3 collects a full batch

Measurements include duration, bytes, memory, reductions, and gc. Batch completion also includes jobs. Metadata includes pages, output, status, id, index, attempt, status counts, ordering, and the configured concurrency limit where applicable.

duration follows Telemetry convention and uses native time units. Convert it for display with:

System.convert_time_unit(duration, :native, :millisecond)

Example metrics definitions:

import Telemetry.Metrics
[
distribution("paperforge.render.stop.duration",
event_name: [:paperforge, :render, :stop],
measurement: :duration,
unit: {:native, :millisecond},
tags: [:status, :output]
),
counter("paperforge.render.exception.count",
event_name: [:paperforge, :render, :exception]
),
distribution("paperforge.batch.job.duration",
event_name: [:paperforge, :batch, :job],
measurement: :duration,
unit: {:native, :millisecond},
tags: [:status, :attempt]
),
sum("paperforge.batch.complete.jobs",
event_name: [:paperforge, :batch, :complete],
measurement: :jobs
)
]

PaperForge emits standard Telemetry data and does not require a particular reporter. Applications can connect these definitions to Prometheus, OpenTelemetry, StatsD, or their existing observability stack.

For production capacity planning, replace the renderer in benchmarks/concurrent_renders.exs with a representative application document and keep the same JOBS and CONCURRENCY matrix. Report both categories:

See API.md for the public compatibility policy and MIGRATING.md for the 0.6-to-1.0 upgrade guide.

Production Hardening

ConcernPaperForge behavior
ValidationPaperForge.validate/1 returns structured reports and issue codes; validate!/1 raises PaperForge.ValidationError
Object integritySerialization checks required objects, indirect references, page-tree counts, object identity, and PDF versions
ReproducibilityIdentical immutable documents produce byte-for-byte identical output
Corrupt inputImage fuzz tests exercise deterministic failures instead of silent output corruption
PDF structureConformance tests verify headers, xref offsets, trailers, references, and EOF markers
Reader compatibilityThe compatibility test invokes pdfinfo when it is installed
File safetywrite/2 serializes to a temporary file and atomically renames successful output
Runtime isolationConcurrent failures, timeouts, retries, and resource-limit violations remain scoped to their originating job

See MIGRATING.md for the stable 1.0 compatibility contract.

Roadmap

1.x Stable document-authoring platform
|
Next Extended interoperability and international typography
|
Future Optional visual Studio and distributed generation patterns

The stable 1.x contract remains the priority. Future work extends inputs, typography, and operational tooling without making the visual product or an external service a requirement for the open-source library.

Future Enhancements

These ideas are not required by, or promised as part of, the stable 1.x API. They describe possible directions for later minor and major releases.

International Typography

Visual Template Designer

A separate visual authoring product is planned for teams that prefer arranging documents visually or want to shorten template-development time. It will generate the same portable .paperforge format, so using it will remain optional and its output will continue to work with the open library. The current direction is a modest annual license tied to supported product versions, rather than an expensive per-document service. Pricing and release details are not part of the PaperForge library contract and will be documented only when the product is ready.

Distributed Generation

Interoperability Expansion

Contributing

Contributions, bug reports, architecture discussions, and PDF examples are welcome. Please read CONTRIBUTING.md before opening a change and follow the community expectations in CODE_OF_CONDUCT.md.

Before opening a pull request:

mix format
mix compile --warnings-as-errors
mix test

License

PaperForge is available under the terms specified in the LICENSE.