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#

page size: :a4, margin: [40, 44, 56, 44], layout: :landscape
SizePoints (portrait)
:a3841.89 × 1190.55
:a4595.28 × 841.89
:a5419.53 × 595.28
:a6297.64 × 419.53
:a7209.76 × 297.64
:b5498.9 × 708.66
:letter (default)612 × 792
:legal612 × 1008
:tabloid792 × 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_4x3288 × 216
:label_4x2288 × 144
:label_100x150 (100 × 150 mm)283.46 × 425.2
:label_100x50283.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:

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 it raises.

RenderMore pages than max_pages
to_pdfevery page written, the warning reported (strict raises)
to_pnga picture of every page, the warning reported (strict raises)
to_zplraises 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:

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:

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
PartAccepted
Numberdigits, with a fraction after a point: 102, 101.6. No sign, exponent or comma
Unitmm, cm, in, pt, in either case
Between the two lengths of a sizex or ×
Spacesaround 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.

page_template#

Runs on every page after pagination.

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.

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 %.

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

Viewerscaling:copies:duplex:pages:pick_tray_by_size:dialog:
Acrobat, Acrobat Readersets 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)
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)read, not used (source)read, not used (source)read, not used (source)read, not used (source)starts printing (source)
Preview (macOS)not testednot testednot testednot testednot testednot tested
Evince, Okular (poppler)not testednot testednot testednot testednot testednot 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:

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
OptionDefaultWhat it does
dpi:203the printer's dots per inch: one dot is 72/dpi pt (0.355 pt at 203 dpi, 0.24 pt at 300)
snap:falsechange colours and thin lines instead of reporting them
threshold:0.5under snap:, a fill whose tone is darker is painted black, a lighter one is not painted
dither::floyd_steinberghow 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.

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:

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

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.

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

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

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

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 add ?debug=1; from the CLI pass --debug.