Guide

# Pages, headers and footers

Page sizes and margins, header and footer regions that reserve space, page templates, print hints, monochrome mode, ZPL for label printers and debug outlines.

## Page size and margins

```ruby
page size: :a4, margin: [40, 44, 56, 44], layout: :landscape
```

| Size | Points (portrait) |
| --- | --- |
| `:a3` | 841.89 × 1190.55 |
| `:a4` | 595.28 × 841.89 |
| `:a5` | 419.53 × 595.28 |
| `:a6` | 297.64 × 419.53 |
| `:a7` | 209.76 × 297.64 |
| `:b5` | 498.9 × 708.66 |
| `:letter` (default) | 612 × 792 |
| `:legal` | 612 × 1008 |
| `:tabloid` | 792 × 1224 |
| `:dl` (envelope, 110 × 220 mm) | 311.81 × 623.62 |
| `:c5` (envelope, 162 × 229 mm) | 459.21 × 649.13 |
| `:c6` (envelope, 114 × 162 mm) | 323.15 × 459.21 |
| `:label_4x6` (4 × 6 in) | 288 × 432 |
| `:label_4x3` | 288 × 216 |
| `:label_4x2` | 288 × 144 |
| `:label_100x150` (100 × 150 mm) | 283.46 × 425.2 |
| `:label_100x50` | 283.46 × 141.73 |
| `[width, height]` | any size, in points |
| `["102mm", "74mm"]`, `"4in x 6in"` | any size, with its units |

`layout: :landscape` swaps width and height: envelopes are listed short edge first, so it is the side the address is on. `margin:` (default 36) takes one value, `[y, x]`, `[top, x, bottom]`, `[top, right, bottom, left]` or `{ top:, right:, bottom:, left:, x:, y: }`, each in points or as a length with its unit (`"3mm"`).

A size or a margin that cannot be read raises `ArgumentError` when the class is defined, with what `page` takes.

## One page only: max_pages

For a label, a receipt or a card.

Content that does not fit a page continues on the next, as a letter or a report should. A label, a receipt or a card must not, and `max_pages` says so:

```ruby
class ShippingLabel < Stationery::Document
  page size: :label_4x6, margin: mm(4)
  max_pages 1
end

label = ShippingLabel.new
label.to_pdf
label.warnings.map(&:message)
# => ["the document may have 1 page and needs 2: page 2 starts with a Code 128 barcode"]

label.to_pdf(max_pages: nil)   # one render without the limit
```

A render that needs more pages than the limit lays them all out and reports `Warnings::TooManyPages` (`limit`, `pages`, `moved`), naming what the first page past the limit starts with: text by its first line, quoted and cut at 40 characters, anything else by its kind (`a table`, `an image`, `a QR code`, `a box`). Under [`strict`](https://stationery.zoolutions.llc/docs/warnings#strict-mode) it raises.

| Render | More pages than `max_pages` |
| --- | --- |
| `to_pdf` | every page written, the warning reported (`strict` raises) |
| `to_png` | a picture of every page, the warning reported (`strict` raises) |
| `to_zpl` | raises `Stationery::WarningsError`, strict or not: no label is written, since each page is one more label printed |

`to_pdf(max_pages:)`, `to_png(max_pages:)` and `to_zpl(max_pages:)` replace the limit for one render, `nil` takes it away. It is inherited; `max_pages nil` in a subclass takes it away, and a subclass's `page` keeps it. The declaration writes nothing: a document that fits is byte for byte what it is without it. Anything but a positive Integer or `nil` raises `ArgumentError` where it is written.

## Millimetres and inches

Units for labels, envelopes and pre-printed forms.

Every size and position is in points (1/72 inch). `mm`, `cm`, `inch` and `pt` convert to them, in the class body and in `view_template` of every component and document:

```ruby
class Label < Stationery::Document
  page size: [mm(102), mm(74)], margin: mm(3)

  def view_template
    box(at: [mm(20), mm(45)], width: mm(60)) { text "Fragile" }
  end
end
```

They are private methods, `Numeric` is not patched, and a method of your own with one of the names wins. Any other class gets them with `include Stationery::Units`, or calls `Stationery::Units.mm(102)`.

`page` also reads lengths written with their unit:

```ruby
page size: ["102mm", "74mm"], margin: "3mm"
page size: "4in x 6in", margin: ["0.25in", "0.5in"]
page size: "102 x 74 mm"          # the first length takes the unit of the second
```

| Part | Accepted |
| --- | --- |
| Number | digits, with a fraction after a point: `102`, `101.6`. No sign, exponent or comma |
| Unit | `mm`, `cm`, `in`, `pt`, in either case |
| Between the two lengths of a size | `x` or `×` |
| Spaces | around numbers, units and the `x`, or none: `"4inx6in"` |

`Stationery::Units.points("3mm")` reads one length. Unit strings are read by `page` (and `Stationery::Page.new`); elements take points, so write `padding: mm(3)` there.

## header and footer

Regions that reserve space; the body flows between them.

```ruby
header(gap: 8) do |page|
  row do
    text "ACME"
    text "Page #{page.number} of #{page.count}", align: :right
  end
end
footer(on: :rest) { text "Confidential", size: 7, align: :center }
header(on: :first) {} # no header on page 1
```

`header(height: nil, gap: 8, on: :all) { |page| … }` and `footer(…)` take the same options:

- **`on:`** `:all` (default), `:first`, `:rest`, `:odd`, `:even`, `:last`, a page number, a range or `->(number) { … }`. Later declarations win for the pages they match, and an empty block removes the region there.
- **`gap:`** extra space between the region and the body.
- **`height:`** without it a region is measured once, on the first page that uses it; pass it when the content varies per page.

The footer is bottom-aligned. A region taller than its space is still drawn and listed in `document.warnings`; regions that leave no room for the body raise `ArgumentError`.

> **on: :last:** A taller last-page footer (say, with totals) is supported: the paginator checks whether the rest fits above it; content that fits a normal page but not the last one continues onto an extra page.

## page_template

Runs on every page after pagination.

```ruby
page_template do |page|
  box(at: [page.margin[3], page.height - 34], width: page.content_box.width) do
    text "Page #{page.number} of #{page.count}", size: 7, align: :right
  end
end

page_template(layer: :background) do |page|
  canvas(height: page.height, at: [0, 0], width: page.width) do |c, rect|
    c.fill_rect(rect.x, rect.y, rect.width, rect.height, color: "#FFFBEB")
  end
end
```

The block receives `page.number`, `page.count`, `page.width`, `page.height`, `page.margin` (`[top, right, bottom, left]`) and `page.content_box` (`x`, `y`, `width`, `height`). With a header or footer, `content_box` excludes their space — it is the body's area.

## Layers

Foreground and background.

`page_template` paints on the **foreground** by default, over the body. `layer: :background` paints under the content — full-bleed backgrounds, watermarks, letterheads. Use `box(outset:)` to bleed a single box's background into the margins instead.

Anchors drawn by page templates resolve to the first page; bookmarks inside page templates are ignored. Warnings raised while templates draw are included in `document.warnings`.

## Page labels

How viewers name the pages.

`page_labels` maps a 1-based first page to the range that starts there. Each range takes `style:` (`:decimal`, `:roman`, `:roman_lower`, `:alpha`, `:alpha_lower`, or `nil` for a prefix only), `start:` (the number the range counts from, default 1) and `prefix:`. Pages before the first range keep their physical number in the viewer.

```ruby
class Report < Stationery::Document
  page_labels 1 => { style: :roman_lower },                       # i, ii, iii
              4 => { style: :decimal },                           # 1, 2, 3, …
              12 => { style: :alpha, prefix: "Appendix " }        # Appendix A, Appendix B
end

Report.new.to_pdf(page_labels: nil) # one render without labels
```

The labels are written as the catalog's `/PageLabels` number tree; `Inspector#page_labels` and `have_page_labels` read them back per page.

## Printing

Scaling, copies, duplex and the print dialog: hints to the viewer.

A PDF can say how it wants to be printed. For a page that is the medium itself (a label, a card, a pre-printed form) the defaults of a print dialog are the wrong ones: "fit to page" prints a 100 × 60 mm label at 94 %.

```ruby
class ShelfLabel < Stationery::Document
  page size: :label_100x50, margin: "3mm"
  print scaling: :none, copies: 2, pick_tray_by_size: true, duplex: :simplex
end

ShelfLabel.new.to_pdf(print: { copies: 1 })           # laid over the hints of the class
ShelfLabel.new.to_pdf(print: { duplex: nil })         # without one of them
ShelfLabel.new.to_pdf(print: nil)                     # without any (false does the same)
```

| Option | Values | Written as |
| --- | --- | --- |
| `scaling:` | `:none`, `:default` | `/PrintScaling` `/None`, `/AppDefault` |
| `copies:` | an Integer of 1 or more | `/NumCopies` |
| `pick_tray_by_size:` | `true`, `false` | `/PickTrayByPDFSize` |
| `duplex:` | `:simplex`, `:long_edge`, `:short_edge` | `/Duplex` `/Simplex`, `/DuplexFlipLongEdge`, `/DuplexFlipShortEdge` |
| `pages:` | a Range of page numbers from 1, or a list of them: `1..3`, `[1..1, 3..4]`, `2..` | `/PrintPageRange` |
| `dialog:` | `:on_open` | `/OpenAction << /S /Named /N /Print >>` |

- The first five are entries of the catalog's `/ViewerPreferences` (ISO 32000-1, 12.2, table 150), beside the `/DisplayDocTitle` of a tagged document. `dialog: :on_open` asks the viewer to open its print dialog when the file is opened, as `window.print()` does for a page, without JavaScript.
- `print` at class level adds to what the class inherits, and `to_pdf(print:)` to what the class declares. A `nil` takes a hint away (`print copies: nil` in a subclass).
- Every option is checked where it is written, the class body or the call of `to_pdf`, and raises `ArgumentError` naming what it takes: `print scaling: is :none or :default, not :fit`. Ranges that overlap are refused.
- `pages:` counts from 1, as the file does, and is written in page order. How many pages there are is known when the document is rendered: a range is cut at the last page (`2..` and `2..99` both end there) and one that starts after it is left out, because a viewer drops a range that names a page the document does not have.
- `/ViewerPreferences` is allowed under PDF/A and PDF/UA. The print dialog is not an action PDF/A has: `dialog: :on_open` with `conformance :pdf_a2b` or `:pdf_a3b` raises `Stationery::ConformanceError` (ISO 19005-2/3, 6.5.1; veraPDF rule 6.5.1-2). PDF/UA-1 alone takes it.
- The hints are written by every render: `incremental:`, signed and encrypted ones too.
- `print` in a class body is this declaration. Inside `view_template` and every other method of a document it is still `Kernel#print`.
- `Inspector#print_preferences` reads them back as `print` takes them, `pages` as a list of ranges, and `have_print_preference(scaling: :none)` and `assert_print_preference` assert them.

They are hints: a viewer follows the ones it knows, and whoever prints can overrule them in the dialog. The table says what each viewer does and how that is known. Nothing in it was tried in a running viewer: "documented" is what the vendor's PDF reference says, "source" what the viewer's source code did when it was read on 2026-09-28, "reported" what a user wrote in the issue named.

| Viewer | `scaling:` | `copies:` | `duplex:` | `pages:` | `pick_tray_by_size:` | `dialog:` |
| --- | --- | --- | --- | --- | --- | --- |
| Acrobat, Acrobat Reader | sets the dialog (documented) | sets the dialog, 2 to 5 (documented) | sets the dialog (documented) | sets the dialog (documented) | sets the dialog (documented) | opens the dialog (reported, [pdf.js 11442](https://github.com/mozilla/pdf.js/issues/11442)) |
| Chrome, Edge (PDFium) | preset of the print preview (source) | preset of the print preview (source) | preset of the print preview (source) | not read (source) | not read (source) | starts printing (source) |
| Firefox (pdf.js) | read, not used by the viewer (source; [bug 1243580](https://bugzilla.mozilla.org/show_bug.cgi?id=1243580)) | read, not used (source) | read, not used (source) | read, not used (source) | read, not used (source) | starts printing (source) |
| Preview (macOS) | not tested | not tested | not tested | not tested | not tested | not tested |
| Evince, Okular (poppler) | not tested | not tested | not tested | not tested | not tested | not tested |

## Monochrome

Black or nothing: thermal label printers and other one-bit devices.

A thermal label printer (203 or 300 dpi), a receipt printer or an e-paper display prints black or nothing. Whatever else the PDF holds, the driver or the printer turns into dots of its own choosing: grey text comes out speckled, a light rule or background vanishes or turns into a dot pattern, and a line thinner than one dot prints or not depending on where it lands. `monochrome` makes those choices where they can be seen, before anything is printed:

```ruby
class ShelfLabel < Stationery::Document
  page size: [mm(100), mm(60)], margin: mm(4)
  monochrome dpi: 203                               # report what a 203 dpi printer cannot print as it is
end

ShelfLabel.new.to_pdf(monochrome: { snap: true })  # change it instead, for this render
ShelfLabel.new.to_pdf(monochrome: false)           # as it was, byte for byte
```

| Option | Default | What it does |
| --- | --- | --- |
| `dpi:` | `203` | the printer's dots per inch: one dot is 72/dpi pt (0.355 pt at 203 dpi, 0.24 pt at 300) |
| `snap:` | `false` | change colours and thin lines instead of reporting them |
| `threshold:` | `0.5` | under `snap:`, a fill whose tone is darker is painted black, a lighter one is not painted |
| `dither:` | `:floyd_steinberg` | how a bitmap becomes dots: `:floyd_steinberg`, `:ordered` (an 8 × 8 Bayer pattern) or `:threshold` (cut at half) |

- **Colours.** Every colour that is not black or white, and anything painted at an `opacity:` below 1, is a `Warnings::NotMonochrome` naming the colour and what painted it: `text`, `a rule` (a `rule`, an underline, a strikethrough: a filled rectangle 3 pt thick or less), `a background` (any other fill), `a border or line` (any stroke), `a gradient` or `an image`, once per colour, kind and page: `text in #888888 on page 1 is not black or white`. `strict` raises on them. It sees whatever is painted, by an element, an SVG or a `canvas { }` block alike.
- **`snap: true`.** Text, rules, borders and lines are painted black, except white ones, which stay white (white text on a black box). A fill is painted black when its tone is darker than `threshold:` and left out when it is lighter; the tone is the colour's luma (ITU-R BT.601), an opacity laid over white paper (black at 0.3 is a tone of 0.7). A gradient is black or left out by the average tone of its stops. Opacity is taken away. Nothing is reported.
- **Line widths.** A stroke or a rule thinner than a dot (`width × the scale of a transform`) is a `Warnings::ThinLine` and is left as it is; with `snap: true` it is widened to a dot.
- **The dot grid.** Outside any transform, a filled rectangle (a rule, a background, an underline) and a stroke made of horizontal and vertical lines (a border, a table's cell borders, a line) are put on the printer's grid, counted from the top left corner of the page: their edges on whole dots and their widths whole numbers of dots, so a hairline is as wide on every label. A curve, a rounded corner, a slanted line and anything under a transform keeps its geometry.
- **Images.** A PNG, a lossless WebP or a JPEG is greyed (a transparent pixel is white paper), resampled to the dots it covers at `dpi:` and dithered, and embedded as a one-bit DeviceGray image (`/BitsPerComponent 1`): what prints is the pattern chosen here, not the driver's. A JPEG is decoded for it in Ruby, no larger than the dots need; one of a kind that is not decoded (lossless, arithmetic-coded, hierarchical, 12-bit) is embedded as it is and reported (`an image in JPEG 640x480`).
- `monochrome` at class level is inherited and adds to what the class inherits; `monochrome false` takes it away. `to_pdf(monochrome:)` takes `true`, `false` or options laid over the class's.
- Monochrome renders keep PDF/A and PDF/UA: the one-bit image is DeviceGray, which the sRGB output intent covers (checked with veraPDF, PDF/A-3b and PDF/UA-1).
- `Inspector#colors` lists what a PDF paints with, so a spec can hold it: `expect(pdf).to have_pdf_colors("#000000")`.

What it does not do: it does not dither a lossless, arithmetic-coded, hierarchical or 12-bit JPEG, does not look at form fields (their widget draws them, not the page), does not put dashes, line caps or curves on the grid, and does not make small text bolder. It changes nothing without `monochrome`. The rules live in `Stationery::Monochrome::Rules` and `Monochrome::Grid`, apart from the PDF canvas, and `to_png` and `to_zpl` apply the same ones (see below).

## Label printers: to_zpl

ZPL II for Zebra and compatible thermal printers.

A thermal label printer (Zebra, and the many makes that emulate ZPL II) is driven in its own language, not PDF. `to_zpl` writes the document as ZPL: each page is drawn one bit to a dot, as `to_png(monochrome: …)` draws it, and sent as one graphic field, so the label is the layout `to_pdf` produces, in any font the document uses. One document class serves the preview on screen and the printer.

```ruby
class ShippingLabel < Stationery::Document
  page size: "4in x 6in", margin: mm(4)
  max_pages 1                               # a second page raises instead of printing a second label
  monochrome dpi: 203
end

label = ShippingLabel.new(parcel)
label.to_zpl                                # => "^XA^PW812^LL1218^LH0,0^FO0,0^GFA,…^XZ\n", one label per page
label.to_zpl("label.zpl", dpi: 300, copies: 2)
label.to_zpl(compression: :hex, pages: 1)   # plain hex, for printers that do not take Z64
```

How the ZPL reaches the printer is the application's business: a socket to port 9100, a print server, a browser print agent. For example, over the network:

```ruby
require "socket"
TCPSocket.open("printer.local", 9100) { |socket| label.to_zpl(socket) }
```

- **What is written.** Per page `^XA`, `^PW` and `^LL` (the page's width and length in dots, `Raster.pixels(points, dpi)`), `^LH0,0`, `^FO0,0`, one `^GFA` graphic field, `^FS`, `^PQ` (copies) and `^XZ`, then a newline. The field's dots are 1 for black, each row padded to a byte with white. `compression: :z64` (the default) deflates the rows with zlib and writes them in Base64, followed by the CRC Zebra's software writes (CRC-16/XMODEM of the Base64 text, checked against a ZebraDesigner print file); `:hex` writes them as plain hex, about 20 times larger.
- **Always monochrome.** A label printer prints one bit, so `to_zpl` renders with the class's `monochrome` settings when it declares them and with the defaults when it does not (`snap: false`, `dither: :floyd_steinberg`): a colour is reported as `Warnings::NotMonochrome` and dithered, as `to_png(monochrome: true)` shows it. `monochrome:` takes options laid over them (`{ snap: true }`); `monochrome: false` raises.
- **`dpi:`** is the printer's: 152, 203, 300 or 600 (6, 8, 12 or 24 dots a millimetre); anything else raises `ArgumentError` naming them. It defaults to the class's `monochrome dpi:`, else 203, and replaces it when given, so `to_zpl(dpi: 300)` and `to_png(monochrome: { dpi: 300 })` are the same dots.
- **`copies:`** is `^PQ`, by default the class's `print copies:`, else 1. **`pages:`** a page number, a Range or an Array, in the order given; `target` a path or anything answering `write` (a socket).
- **One label.** Content that does not fit the label goes to a next page, and every page is a label. Declare `max_pages 1` (see Pages) and a document that needs a second page writes no ZPL: `to_zpl` raises `Stationery::WarningsError` with `Warnings::TooManyPages`, strict or not, because the harm of a second label is done when it prints, where no one reads a warning. `to_zpl(max_pages: nil)` writes them all; `to_pdf` and `to_png` of the same document keep every page and warn, so the page that spilled can be looked at.
- `strict:`, `debug:`, `shaper:` and `max_pages:` are those of `to_pdf`; what only a PDF has raises, as for `to_png`.
- **Speed.** A 4 × 6 in label takes about 25 ms at 203 dpi and 35 ms at 300 dpi (Ruby 3.4, Apple M-series), and is 10 to 20 KB of ZPL.
- The labels were read back and compared with `to_png` dot for dot, and rendered by [Labelary](http://labelary.com/viewer.html), a ZPL viewer, identically. They were not printed on a printer by the gem's specs.

**Barcodes the printer draws.** A `barcode` is part of the picture unless it asks otherwise: `barcode "SX0042771903", native: true`, or `to_zpl(native: true)` for every barcode that does not say `native: false`. Such a barcode is left out of the graphic field and written after it as the printer's own command, where the picture had it, so the printer puts the bars on its own dot grid:

```
^FO146,874^BY4^BCN,124,N,N,N,N^FD>:SX>50042771903^FS             Code 128, its code sets named
^FO65,197^BY3^BEN,113,N,N^FD400638133393^FS                       EAN-13, the printer adds the check digit
^FO650,625^BQN,2,4^FDMM,B0034https://track.example/SX0042771903^FS  QR code, byte mode, level M
^FO42,42^BXN,8,200,14,14,6,_,1^FDSX0042771903^FS                  Data Matrix (ECC 200), 14 × 14
```

- `^BY` is the module in whole dots, the height in dots, and data ZPL would read as a command is escaped with `^FH`. A QR code's `^FO` is 10 dots above it, since `^BQ` draws that far below its origin. A Data Matrix is asked for at its own size, so it takes the same square.
- It stays in the picture when the printer could not draw it as it is: rotated or otherwise transformed, clipped, lighter than half grey, a module over 10 dots, a QR code of UTF-8 text (its ECI has no field in `^BQ`), or a Data Matrix holding `_` (the escape character `^BX` is given).
- Rendered by Labelary, the Code 128 is the same dots as `to_png` draws; the EAN-13's guard bars come out 13 dots longer, as the printer draws them; a QR code covers the same modules' square, its modules chosen by the printer's own encoder (another mask); a Data Matrix of digits and capitals is the same dots, one of other text another encodation in the same square. All decode (ZBar, dmtxread) to the data.
- Rules and boxes are not written as `^GB`: a box drawn natively would print over white text on it, which the picture keeps.

`stationery render label.rb --zpl --dpi 300` writes `label.zpl` (see [CLI](#cli)).

## Embedded files

Attachments, for Factur-X and friends.

`attach_file name, data, mime:, description:, relationship:, modified_at:` embeds a file in every render; `to_pdf(attachments: [{ name:, data:, … }])` adds more for one render. Each file is a `/Filespec` with an `/EmbeddedFile` stream, listed in the catalog's `/EmbeddedFiles` name tree and `/AF` array. `relationship:` writes `/AFRelationship`: `:alternative` (the machine-readable twin Factur-X and ZUGFeRD require), `:source`, `:data`, `:supplement` or `:unspecified` (default). The same name twice raises `ArgumentError`; an encrypted document encrypts the embedded streams too.

```ruby
class InvoicePdf < Stationery::Document
  attach_file "factur-x.xml", invoice_xml, mime: "text/xml", description: "Factur-X",
              relationship: :alternative
end
```

`Inspector#attachments` reads them back as `{ name:, mime:, bytes:, description:, relationship: }`; `have_attachment` and `assert_pdf_attachment` assert one by name, type and relationship.

For an e-invoice, [`factur_x`](https://stationery.zoolutions.llc/docs/conformance#factur-x-zugferd-e-invoices) does all of it in one line: it embeds the XML, claims PDF/A-3b and writes the Factur-X identification in XMP.

## Accessibility (tagged PDF)

A structure tree for screen readers.

```ruby
class ReportPdf < Stationery::Document
  tagged                                   # or to_pdf(tagged: true) for one render
  metadata title: "Q3 report", lang: "en-US"

  def view_template
    text "Quarterly report", size: 20, heading: 1
    text "Revenue grew in every region."
    image "chart.png", width: 300, alt: "Revenue by region, Q1 to Q3"
    box(role: :note, padding: 8) { text "Figures are unaudited." }
  end
end
```

A tagged PDF carries a structure tree screen readers and reflowing viewers follow, in paint order and across page breaks: a paragraph continued on the next page stays one `P`.

- `text` is a `P`; `heading: 1..6` makes it `H1`–`H6`.
- `image`/`svg` are a `Figure` with `/Alt` from `alt:` and a bounding box; `alt: false` marks one decorative (an artifact, not announced).
- `box(role:)` groups its content: `:section` (`Sect`), `:div`, `:blockquote`, `:note`, `:caption`, `:article` (`Art`), `:part`. Boxes without a role, rows, columns, groups and wraps add no element.
- Lists are `L` (with `/ListNumbering` from the bullet shape or number format) > `LI` > `Lbl` (a text marker; drawn bullets are artifacts) and `LBody`.
- Tables are `Table` > `TR` > `TH` (header rows, `/Scope /Column`) or `TD`, with `/ColSpan` and `/RowSpan`. A table split across pages stays one `Table`; its repeated header rows are artifacts.
- Linked text is a `Link` inside its paragraph holding the link annotation (`/OBJR`, `/StructParent`); `box(link:)` groups its content in a `Link`.
- `table_of_contents` is `TOC` > `TOCI` > `Link` (the title and its annotation) and `Reference` (the page number).
- `html`/`markdown` tag headings `H1`–`H6` (with or without `bookmarks: true`) and block quotes `BlockQuote`, and give images their `alt`. In `html`, `<img alt="">` is decoration as `alt: false` is (an `<img>` without the attribute is a description missing). Markdown has no way to say so: `![](photo.png)` is a description missing, and decoration goes through `html` or `image`.
- Headers, footers and page templates are pagination artifacts, but for their links: a `link:` one paints (`text(link:)`, markup, `html`, `markdown`, `box(link:)`) is a `Link` of the `Document` holding its text and its annotation, read after the content of its page, in the order header, footer, page templates. The rest of the region stays an artifact. Backgrounds, borders and rules drawn outside any element are layout artifacts.
- `metadata lang:` writes the catalog's `/Lang`; the title is shown instead of the file name.
- Every document carries an XMP packet (`/Metadata`, uncompressed) mirroring the Info dictionary: `dc:title`, `dc:creator`, `dc:description`, `dc:subject`, `dc:language`, the `xmp:` dates and `pdf:Producer`. PDF/A and PDF/UA identification lives there (see [PDF/A and PDF/UA](#pdfa-and-pdfua)); `metadata xmp: false` or `to_pdf(xmp: false)` leaves it out.
- An image or drawing without `alt:`, or with a blank one (`alt: ""`, whitespace alone, which veraPDF would accept), is a `Warnings::MissingAlt`; `alt: false` is how decoration is marked.
- A heading level that is skipped is a `Warnings::SkippedHeading` (`level`, `allowed`, `page`): the first heading is `heading: 1` and a heading is at most one level below the heading before it (`1`, then `3` skips `2`). Going back up is free (`3`, then `1`). Headings are read in the order of the structure tree, inside sections, lists, table cells, columns and floats; those of headers, footers and page templates are artifacts and do not count.
- A link annotation that belongs to no `Link` element is a `Warnings::UntaggedLink` (`target`, `place`, `page`): a link in the header row a table repeats on its next pages, which is an artifact (`place` is `:artifact`), and `canvas.link` without a `tag:` (`:canvas` in the body, `:header`, `:footer` or `:page_template` on the canvas of one). Every `link:` is tagged, in the body and in headers, footers and page templates; on a canvas of the body, pass the `Link` element that holds what the link draws: `canvas.link(x, y, w, h, url, tag: element)` after `canvas.tag(element) { … }`.
- A missing `lang` is a `Warnings::MissingLanguage`. All four are warnings, so `strict` catches them, and `conformance :pdf_ua1` raises on the first three.
- A tagged render packs its structure elements, and every other object that is not a stream, into deflated object streams (`/Type /ObjStm`, 200 objects each) with a cross-reference stream in place of the table and trailer (ISO 32000-1, 7.5.7 and 7.5.8). A table of 300 rows is 115 KB instead of 501 KB. Streams, the `/Encrypt` dictionary and a signature dictionary stay out; PDF/A-2, PDF/A-3 and PDF/UA-1 allow them. `object_streams false` at class level, or `to_pdf(object_streams: false)`, writes the classic table for a reader that cannot read them; `to_pdf(object_streams: true)` packs an untagged render too.
- Untagged documents (the default) are written exactly as before.

Check the tree in tests with `have_structure` and `have_tagged_content` (see [Testing](#testing)):

```ruby
expect(ReportPdf.new).to have_tagged_content # tagged, and no text outside marked content
expect(ReportPdf.new).to have_structure(
  [[:Document, [[:H1, "Quarterly report"], [:P, "Revenue grew in every region."],
                [:Figure, "Revenue by region, Q1 to Q3"], [:Note, [[:P, "Figures are unaudited."]]]]]]
)
```

`tagged` alone does not label the file; `conformance :pdf_ua1` does (see [PDF/A and PDF/UA](#pdfa-and-pdfua)). Nothing checks colour contrast or the reading order you build out of positioned boxes (`box(at:)` joins the reading order where it paints).

## Debug outlines

See every layout rectangle.

`to_pdf(debug: true)` outlines every layout rectangle on top of the content: boxes (red, padding dashed), row columns (blue), flow slots (grey, dotted), table cells (green, padding dashed), positioned boxes (pink), images (teal) and the page content box (cyan). Pass an Array to outline only some kinds:

```ruby
Invoice.new.to_pdf("invoice.pdf", debug: %i[cell cell_padding])
```

The kinds are `:box`, `:padding`, `:column`, `:flow`, `:cell`, `:cell_padding`, `:positioned`, `:image`, `:page` and `:region`. In a Rails [preview](https://stationery.zoolutions.llc/docs/rails#previews) add `?debug=1`; from the [CLI](https://stationery.zoolutions.llc/docs/cli) pass `--debug`.