Getting started

Documents and components

A Document is configured at class level and described in view_template; a Component is a reusable piece of one.

Document configuration#

Class-level macros, inherited by subclasses.

Everything about the PDF as a whole is declared on the class. Subclasses inherit a copy of the parent's configuration, so a base ApplicationPdf can hold the house style.

class ApplicationPdf < Stationery::Document
  page size: :a4, margin: [40, 44, 56, 44]
  font_family "Brand", regular: "fonts/Brand-Regular.ttf", bold: "fonts/Brand-Bold.ttf"
  font_fallbacks "Noto Sans Symbols"
  default_text font: "Brand", size: 9, color: "#1F2937"
  metadata author: "Acme Studio AB", creator: "stationery"
end

class InvoicePdf < ApplicationPdf
  metadata title: "Invoice"
  strict
end
MacroArgumentsDefaultDescription
pagesize:, margin:, layout:size: :letter, margin: 36, layout: :portraitPage size (a name such as :a4, :letter, :dl or :label_4x6, [width, height] in points, or with units: ["102mm", "74mm"], "4in x 6in"), margins (one value or CSS-style 2/4 values, in points or as "3mm") and :landscape. Checked when the class is defined. See Pages.
font_familyname, regular:, bold:, italic:, bold_italic:—Registers a family from .ttf/.otf files. With no paths it selects bundled Inter or an installed font pack.
font_fallbacks*names[]Families tried, in order, for characters the text's own family has no glyph for; bundled Inter is tried last.
shapershapernilAn object answering call(text, font, **options) that places the glyphs of every text, for complex scripts; also to_pdf(shaper:). See Fonts.
default_text**style{}Base text style for the whole document: font, size, color, align, leading, kerning, …
metadatatitle:, author:, subject:, keywords:, creator:, producer:{}The PDF Info dictionary, mirrored in an XMP packet (/Metadata: dc:title, dc:creator, dc:subject, the xmp: dates, pdf:Producer). An Array value is joined with ", "; other keys pass through; xmp: false leaves the packet out.
strictvalue = truefalseRaise Stationery::WarningsError instead of writing a PDF that produced warnings.
incrementalvalue = truefalseWrite each page as soon as it is painted, so a long document with headers or footers holds far less; also to_pdf(incremental:). See Rendering.
object_streamsvalue = trueon when taggedPack every object that is not a stream into deflated object streams with a cross-reference stream (PDF 1.5); a tagged render does unless object_streams false; also to_pdf(object_streams:). See Accessibility.
page_templatelayer: :foreground—A block run on every page after pagination; layer: :background paints under the content.
header / footerheight:, gap:, on:gap: 8, on: :allRegions that reserve space at the top/bottom of the pages on: matches.

Rendering#

to_pdf and what it leaves behind.

to_pdf(target = nil, strict: <class setting>, debug: false) builds the element tree, paginates it, runs page templates, resolves links and bookmarks and returns the PDF as a binary String. With a target it also writes to that path or IO.

document = InvoicePdf.new(invoice)
document.to_pdf("invoice.pdf")
document.warnings.each { |warning| Rails.logger.warn(warning.message) }
document.page_count # => 2, the pages the last render laid out

With a block, to_pdf { |chunk| … } streams the file in pieces as it is written and answers the number of bytes: the first bytes leave sooner and no output buffer is built. Every page is laid out and painted before the first byte. A signed document cannot go to a block.

incremental: true (or incremental at class level) writes each page's content as soon as the page is painted and lets go of it, to a block before the next page is painted. It is for long documents with headers, footers, page templates or a table of contents, which are painted once every page is known and become content streams of their own; the file lists its page contents first and is a little larger. A render with conformance: or sign:, and a strict one that goes to a block, take the usual path, since they must be checked before a byte is written. To a block, an error on a late page leaves the start of a file. The figures are on the Performance page.

strict: false opts one render out of a class-level strict; debug: outlines layout rectangles (see Pages).

Components#

Phlex's lifecycle, the same element DSL.

A Stationery::Component is a reusable piece of a document with Phlex's lifecycle — around_template, before_template, view_template, after_template — and every element. Content passed as a block is yielded from view_template:

class Callout < Stationery::Component
  def initialize(color:) = (super(); @color = color)
  def view_template(&) = box(background: @color, padding: 12, radius: 6, &)
end

class Report < Stationery::Document
  def view_template
    render Callout.new(color: "#F3F4F6") { text "Amount due" }
  end
end

A Document is itself a component, so documents can render other components, and components can render components.

render#

What it accepts.

ArgumentRenders
A component instancerender Callout.new(color: "#EEE") { … } — calls it with the block.
A component classrender Divider — instantiated with no arguments.
A StringA text paragraph.
A Proc or MethodYielded as content, like a block.
A list (any Enumerable)Each item rendered in turn.

Anything else raises ArgumentError. Blocks that take an argument receive the component, so { |c| c.text @title } keeps your own self inside the block.