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:

  • Pure Elixir PDF generation
  • Multiple pages
  • A3, A4, A5, Letter, and Legal page sizes
  • Custom page dimensions
  • Portrait and landscape orientations
  • Built-in Helvetica text
  • Text size, position, and color
  • Lines
  • Rectangles
  • Circles
  • RGB and grayscale colors
  • Basic document metadata
  • PDF binary generation
  • Direct file output
  • Traditional PDF cross-reference tables

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

  • Text measurement
  • Horizontal text alignment
  • More built-in PDF fonts
  • Stream compression
  • JPEG image support
  • Top-left coordinate helpers
  • Improved metadata encoding

0.3.0

  • Paragraph layout
  • Automatic line wrapping
  • Margins
  • Vertical content flow
  • Automatic page breaks
  • Basic tables

Future

  • TrueType and OpenType fonts
  • Unicode text
  • PNG images
  • Reusable Form XObjects
  • Links and annotations
  • HTML parsing
  • CSS style resolution
  • HTML/CSS layout engine
  • Existing PDF parsing
  • Incremental updates
  • Digital signatures

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.