Build PDF documents from plain Ruby, styling elements with a Tailwind-inspired class vocabulary. No runtime dependencies.
class Invoice < Bandoola::View
def view_template
div(class: "p-8 bg-slate-100 rounded-lg") do
text(class: "text-3xl font-bold") { "Bandoola" }
text(class: "text-base text-slate-600 mt-2") { "PDFs, the Tailwind way." }
end
end
end
invoice = Invoice.new
invoice.render # build the PDF (also returns the bytes)
invoice.write("out.pdf")bundle add bandoolaInside view_template you build a tree of elements. Each takes a class: (a
space-separated string or an array of tokens) and, where it makes sense, a block.
div(class:) { … }— a block-level box that lays out the elements inside it.text(class:) { "…" }— a run of text; the block returns the string. It wraps to its box width, and embedded newlines start new lines.img(src:, class:)— an image;srcis a path to an SVG or JPEG.
Elements flow like HTML blocks: each fills its container's width and stacks below the previous one, unless a width class makes it share a row (and reflow).
The class names follow Tailwind; see their docs for the full scales. What Bandoola understands today:
| Concern | Classes |
|---|---|
| Padding / margin | p-4, px-2, pt-1, m-4, mx-2, … |
| Width / height | w-32, w-1/2, w-full, h-16, h-1/2, h-full |
| Border width | border-1 … border-16 |
| Border radius | rounded, rounded-sm … rounded-3xl, rounded-full |
| Background | bg-cyan-100, bg-slate-800, bg-black, bg-white |
| Border color | border-blue-600 (needs a border width to show) |
| Text color | text-rose-500, text-white |
| Font family | font-sans, font-serif, font-mono, font-<registered> |
| Font weight | font-light … font-black |
| Text size | text-xs … text-9xl |
Sizes and spacing are points (1px ≈ 1pt). Notes:
divhas no fill or border by default — addbg-*,border-*, orrounded-*.- Colors work on the full Tailwind palette plus
black/white; a color with no shade (bg-cyan) uses 500. - A
div'sfont-*family and weight are inherited by the text inside it; text size is set pertextelement.
font-sans/serif/mono map to the standard PDF fonts (Helvetica, Times,
Courier), which cover Western European text. Register a TrueType font for
anything else:
class Doc < Bandoola::View
register_font :gowun, "fonts/GowunDodum-Regular.ttf"
# or per weight: register_font :gowun, regular: "…-Regular.ttf", bold: "…-Bold.ttf"
def view_template
text(class: "font-gowun") { "Räksmörgås · 안녕하세요" }
end
endEmbedded fonts render any Unicode they have glyphs for (Identity-H encoding) and
are subset to just the glyphs you use — a few KB rather than the whole file. Opt
out with compact_fonts false if you'd rather keep the original glyph ids. You
can register several fonts in one document and use whichever fits each script.
Define header and footer alongside view_template for content that repeats
at the top and bottom of every page. When the body is taller than one page it
splits automatically, and page_number / page_count are available inside the
header and footer.
class Report < Bandoola::View
def header = text(class: "font-bold mb-4") { "Quarterly Report" }
def footer = text(class: "text-sm mt-4") { "Page #{page_number} of #{page_count}" }
def view_template
50.times { |i| div(class: "p-2 mb-2 border-1") { text { "Row #{i + 1}" } } }
end
endGive the header an mb-* (and the footer an mt-*, or use my-*) for some air
between them and the body. Pages break between top-level elements — an element
isn't split across a page boundary.
The default page is A4. Override the geometry per view:
class Wide < Bandoola::View
def width = 1190
def height = 842
def margin = 48
endRender a file that defines a view straight to a PDF:
bundle exec bandoola doc.rb > doc.pdfbin/setup to install dependencies, rake test to run the tests, bin/console
for a prompt. Running rake test also regenerates the inspectable example PDFs
under tmp/pdf_samples/.
Bandoola is pre-release and the API may shift. It is not a browser — there's no cascade, no flexbox, line breaking is greedy (no hyphenation or justification), and a class either does what's listed above or is ignored. I add things as I need them, so coverage is deliberately partial; PRs and issues that fill a gap are welcome.
Available as open source under the MIT License.