PaperForge

PaperForge is a pure Elixir library for generating PDF documents directly, without browsers, operating-system binaries, or external rendering services.

It builds the PDF object graph, content streams, cross-reference table, and trailer entirely in Elixir.

PaperForge is currently in early development. The 0.1.x API may change while the document and graphics engines evolve.

Why PaperForge?

Many PDF-generation tools work by converting HTML through Chromium, wkhtmltopdf, or another external application.

PaperForge takes a different approach:

Elixir API
Page and graphics operations
PDF object graph
Serializer
Writer
PDF binary

This provides direct control over the generated PDF structure and creates a foundation for future document layout, HTML/CSS rendering, parsing, and editing capabilities.

Current features

PaperForge currently supports:

Installation

PaperForge is currently under active development and may not yet be published on Hex.

Add it directly from GitHub:

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

Then install dependencies:

mix deps.get

After the first stable Hex release, installation will use:

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

Quick start

alias PaperForge.Color
alias PaperForge.Page
document =
PaperForge.new()
|> PaperForge.metadata(
title: "PaperForge Example",
author: "Manuel Garcia",
subject: "Pure Elixir PDF generation",
keywords: ["Elixir", "PDF"]
)
|> PaperForge.add_page(fn page ->
page
|> Page.text(
"Hello from PaperForge",
x: 72,
y: 760,
size: 28,
color: Color.rgb255(35, 60, 120)
)
|> Page.line(
x1: 72,
y1: 740,
x2: 520,
y2: 740,
width: 2,
color: Color.rgb255(35, 60, 120)
)
end)
PaperForge.write!(document, "example.pdf")

Drawing 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 Bézier curves because PDF does not provide a native circle operator.

Multiple pages

alias PaperForge.Page
document =
PaperForge.new()
|> PaperForge.add_page(fn page ->
Page.text(
page,
"First page",
x: 72,
y: 750,
size: 28
)
end)
|> PaperForge.add_page(
[size: :letter, orientation: :landscape],
fn page ->
Page.text(
page,
"Second page",
x: 72,
y: 500,
size: 28
)
end
)
PaperForge.write!(document, "multiple_pages.pdf")

Page sizes

Supported page names:

:a3
:a4
:a5
:letter
:legal

Example:

PaperForge.add_page(
document,
[size: :letter, orientation: :landscape],
fn page ->
Page.text(page, "Landscape page", x: 72, y: 500)
end
)

Custom page dimensions are also supported:

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

All dimensions are expressed in PDF points.

1 point = 1/72 inch

Coordinate system

PaperForge currently uses the native PDF coordinate system.

Y
└────────────→ X
(0, 0)

The origin is located at the bottom-left corner of the page.

For an A4 page:

width: 595.28 points
height: 841.89 points

Example:

Page.text(
page,
"Near the top",
x: 72,
y: 760
)

A top-left coordinate abstraction is planned for a future release.

Colors

RGB using values from 0 to 1

Color.rgb(1, 0, 0)

RGB using values from 0 to 255

Color.rgb255(255, 0, 0)

Grayscale

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

Metadata

document =
PaperForge.new()
|> PaperForge.metadata(
title: "Monthly Report",
author: "Manuel Garcia",
subject: "Project summary",
keywords: ["report", "elixir", "pdf"],
creator: "PaperForge",
producer: "PaperForge"
)

Metadata is written into the PDF Info dictionary and referenced from the document trailer.

Binary output

PaperForge can return the complete PDF as a binary:

pdf_binary =
document
|> PaperForge.to_binary()

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))

Architecture

PaperForge separates the public drawing API from the low-level PDF representation.

PaperForge
├── Page
│ └── High-level drawing operations
├── Graphics
│ ├── Text
│ ├── Line
│ ├── Rectangle
│ └── Circle
├── Document
│ └── PDF object graph and object allocation
├── Object
│ └── Indirect PDF objects
├── Reference
│ └── References such as `3 0 R`
├── Stream
│ └── Stream dictionaries and binary data
├── Serializer
│ └── Elixir values to PDF syntax
└── Writer
├── PDF header
├── Indirect objects
├── Cross-reference table
├── Trailer
└── EOF marker

A page operation follows this pipeline:

Page.text/3
PDF text operators
Page content stream
Indirect PDF object
Serializer
Writer
PDF bytes

PDF structure

PaperForge generates PDFs using a graph of indirect objects.

Trailer
└── Catalog
└── Page tree
├── Page
│ ├── Resources
│ └── Content stream
└── Page
├── Resources
└── Content stream

Objects are connected through references such as:

3 0 R

This means:

object number: 3
generation: 0
type: indirect reference

Development

Clone the repository:

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

Install local development tools:

mix local.hex --force
mix local.rebar --force

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

Generate the graphics example:

mix run examples/graphics.exs
open tmp/paper_forge_0_1.pdf

Roadmap

0.2.0

0.3.0

Future

Project status

PaperForge is experimental and currently intended for learning, testing, and early integration.

The public API may change before version 1.0.0.

Contributing

Contributions, bug reports, architecture discussions, and PDF examples are welcome.

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.