A pure Lua library for generating PDF files without external dependencies. It is distributed as a single pdf.lua module and supports text, vector drawing, multi-page documents, layout helpers, and native PNG embedding.
- Single-file library:
require("pdf") - Pure Lua PDF generation
- Multi-page documents with custom page sizes
- Standard PDF fonts: Helvetica, Times, Courier
- Text placement with left, center, and right alignment plus basic wrapping
- Basic vector drawing: rectangles, circles, lines
- Helper methods for headers, footers, title pages, checklists, and progress bars
- Native PNG embedding from file paths or raw PNG data
- Link and note annotations
- Basic AcroForm support for text fields, checkboxes, radio buttons, dropdowns, list boxes, and signature widgets
- Built-in utilities for units, colors, paper sizes, and formatting
pdf/
├── pdf.lua
├── README.md
├── GETTING_STARTED.md
├── INDEX.md
├── LICENSE
└── examples/
├── basic_text_shapes.lua
├── multipage_document.lua
├── report_layout.lua
├── advanced_features.lua
├── forms.lua
├── png_embedding.lua
├── test.lua
└── blh_cat_transparent.png
Place pdf.lua somewhere on your Lua package path, then:
local PDF = require("pdf")Requires Lua 5.1+.
local PDF = require("pdf")
local doc = PDF.new()
doc.title = "Hello"
doc.author = "Lua PDF Library"
doc.keywords = "hello, example, lua"
doc:add_page(PDF.PaperSizes.A4.width, PDF.PaperSizes.A4.height)
doc:set_font("Helvetica", "B", 16)
doc:text(10, 12, "Hello, World!")
doc:set_color_fill(52, 152, 219)
doc:rect(10, 22, 60, 18, "F")
doc:set_font("Helvetica", "", 10)
doc:set_color_fill(255, 255, 255)
doc:text(40, 33, "Generated in pure Lua", nil, "C")
doc:save("hello.pdf")From the repository root:
lua examples/basic_text_shapes.lua
lua examples/multipage_document.lua
lua examples/report_layout.lua
lua examples/advanced_features.lua
lua examples/forms.lua
lua examples/png_embedding.luaRun the bundled smoke test with:
lua examples/test.lualocal doc = PDF.new()
doc:add_page(210, 297)
doc:save("output.pdf")PDF.new()
- Creates a new document object.
- Stream compression is enabled by default; set
doc.compression = falsebeforesave()to write raw streams instead
Common document metadata fields are available as direct properties:
doc.titledoc.authordoc.subjectdoc.keywordsdoc.creatordoc.producerdoc.createddoc.modified
You can also set them in one call:
doc:set_metadata({
title = "Quarterly Report",
author = "Finance Team",
keywords = "q1, sales, finance",
creator = "Internal Reporting Tool",
producer = "Lua PDF Library",
Company = "Example Corp",
})doc:set_metadata(metadata_table)
- Updates standard Info dictionary fields and stores any additional keys as custom PDF Info entries
- Date strings should use PDF timestamp format such as
D:20260518143000
doc:add_page(width, height, [orientation])
- Adds a page in millimeters.
orientationmay be"P"or"L".
Call set_font before adding text.
doc:set_font("Helvetica", "B", 12)
doc:text(10, 10, "Left aligned")
doc:text(105, 20, "Centered", nil, "C")
doc:text(200, 30, "Right aligned", nil, "R")
doc:text(10, 40, "This paragraph wraps when width is provided.", 50, "L")doc:set_font(family, [style], [size])
- Families:
Helvetica,Times,Courier - Styles:
"","B","I","BI" - Size is in points
doc:text(x, y, text, [width], [align])
- Positions text in millimeters
alignmay be"L","C", or"R"- If
widthis omitted, alignment usesxas the anchor point - If
widthis provided, text wraps within that column width and the method returns the rendered height in millimeters
The library can emit basic PDF annotations alongside forms.
doc:text(10, 20, "Project website")
doc:link(10, 20, 35, 6, "https://example.com")
doc:note(50, 20, 8, 8, "Follow up on this section", {
title = "Reviewer",
icon = "Comment",
})doc:link(x, y, width, height, url, [options])
- Adds an external URL link annotation over the given rectangle
options.border_widthcontrols the visible border and defaults to0
doc:note(x, y, width, height, contents, [options])
- Adds a text note annotation
options.titlesets the popup author/titleoptions.iconsets the PDF note icon name such asNote,Comment, orHelpoptions.opencontrols whether the note starts expandedoptions.coloraccepts either0..255or0..1RGB values
doc:set_color_fill(255, 0, 0)
doc:rect(10, 40, 40, 20, "F")
doc:set_color_stroke(0, 0, 0)
doc:set_line_width(0.5)
doc:circle(80, 50, 10, "S")
doc:line(10, 70, 100, 70)doc:rect(x, y, width, height, [style])
- Styles:
"S","F","DF"
doc:circle(x, y, radius, [style])
- Styles:
"S","F","DF"
doc:line(x1, y1, x2, y2)
doc:set_color_fill(r, g, b, [a])
doc:set_color_stroke(r, g, b, [a])
doc:set_line_width(width)
Color values can be given as either 0..255 or normalized 0..1.
The library can embed PNGs directly and preserves PNG alpha via a soft mask when present.
doc:image_png("examples/blh_cat_transparent.png", 20, 40, 80, 80)local raw_png = assert(io.open("icon.png", "rb")):read("*all")
doc:image_png_data(raw_png, 20, 130, 25, 25, "icon-cache-key")doc:image_png(path, x, y, [width], [height])
- Loads a PNG from disk and draws it on the current page
doc:image_png_data(data, x, y, [width], [height], [cache_key])
- Draws from raw PNG bytes already in memory
If width and height are omitted, the image uses a default 72 DPI conversion.
These are exposed as document methods and also via PDF.Helper.
local y = doc:add_header("Monthly Report", "April 2026")
y = doc:section_header("Summary", y + 10)
doc:highlight_box(10, y, 60, 18, "Healthy", {46, 204, 113}, {255, 255, 255})
doc:add_footer(true)Available helpers:
doc:add_header(title, [subtitle])doc:add_footer([show_date])doc:section_header(text, y)doc:highlight_box(x, y, width, height, text, [bgcolor], [textcolor])doc:box(x, y, width, height, [border_color], [border_width])doc:title_page(title, [subtitle], [content_lines])doc:page_break(page_width, page_height, [with_header])doc:two_column_layout(left_title, left_content, right_title, right_content, y)doc:watermark(text, [opacity])doc:checklist_item(x, y, text, checked)doc:progress_bar(x, y, width, height, percentage, [color])
The library can emit interactive AcroForm widgets for simple PDFs.
doc:add_page(210, 297)
doc:set_font("Helvetica", "", 11)
doc:text(10, 20, "Name")
doc:form_text(10, 24, 90, 10, "customer_name", {
value = "Ada Lovelace",
})
doc:text(10, 42, "Subscribe")
doc:form_checkbox(10, 46, 6, "newsletter_opt_in", true)
doc:text(10, 60, "Department")
doc:form_combo(10, 64, 80, 10, "department", {
"Research",
"Engineering",
"Operations",
}, {
value = "Engineering",
})
doc:text(10, 80, "Interests")
doc:form_list(10, 84, 80, 22, "interests", {
"Math",
"Computing",
"Astronomy",
}, {
value = {"Math", "Computing"},
multi_select = true,
})
doc:text(10, 116, "Signature")
doc:form_signature(10, 120, 90, 18, "customer_signature")
doc:text(110, 60, "Plan")
doc:form_radio(110, 64, 6, "plan_tier", "Basic", false)
doc:text(118, 66, "Basic")
doc:form_radio(110, 74, 6, "plan_tier", "Pro", true)
doc:text(118, 76, "Pro")doc:form_text(x, y, width, height, name, [options])
- Adds an interactive text field
options.valuesets the current valueoptions.default_valuesets the reset valueoptions.multiline,options.read_only,options.required, andoptions.passwordmap to PDF field flagsoptions.alignmay be"L","C", or"R"options.font_size,options.border_width,options.border_color,options.background_color, andoptions.text_colorcontrol appearance hints
doc:form_checkbox(x, y, size, name, checked, [options])
- Adds an interactive checkbox widget
checkedsets the initial stateoptions.read_onlyandoptions.requiredare supported
doc:form_radio(x, y, size, group_name, option_name, checked, [options])
- Adds one radio button widget to a shared group
- Use the same
group_namefor all buttons in the set option_nameis the export value for that choice- Only one button in a group should be created with
checked = true options.no_toggle_to_offkeeps the group from clearing once one item is selected
doc:form_combo(x, y, width, height, name, choices, [options])
- Adds a dropdown choice field
choicesis an array of visible option stringsoptions.valueandoptions.default_valueset the selected valueoptions.editablecreates an editable combo boxoptions.align,options.font_size, and color/border options work likeform_text
doc:form_list(x, y, width, height, name, choices, [options])
- Adds a list box field
options.valuemay be a single string or an array whenoptions.multi_select = trueoptions.top_indexcontrols the first visible rowoptions.align,options.font_size, and color/border options work likeform_text
doc:form_signature(x, y, width, height, name, [options])
- Adds an empty signature widget area
- This creates the field container only; actual cryptographic signing is not implemented by the library
require("pdf") also exposes helpers directly:
local PDF = require("pdf")
local a4 = PDF.PaperSizes.A4
local blue = PDF.Colors.blue
local pt = PDF.Utils.mm_to_pt(10)
local hex = PDF.Utils.rgb_to_hex(255, 0, 0)Useful utility groups:
PDF.PaperSizesPDF.ColorsPDF.UtilsPDF.HelperPDF.QuickRef
Selected utility functions:
PDF.Utils.mm_to_pt(mm)PDF.Utils.pt_to_mm(pt)PDF.Utils.in_to_mm(inches)PDF.Utils.mm_to_in(mm)PDF.Utils.rgb_to_hex(r, g, b)PDF.Utils.hex_to_rgb(hex)PDF.Utils.hsv_to_rgb(h, s, v)PDF.Utils.rgb_to_hsv(r, g, b)PDF.Utils.color_gradient(color1, color2, steps)PDF.Utils.format_number(num, decimals)PDF.Utils.get_pdf_timestamp()
- Units are millimeters
- Origin is top-left
- X increases to the right
- Y increases downward
- Built-in fonts are limited to the standard PDF font set
- General drawing transparency is not emitted as PDF transparency, even though color setters accept an alpha argument
- PNG is the only image format supported today
- Form support is currently limited to AcroForm text fields, checkboxes, radio buttons, dropdowns, list boxes, and unsigned signature widgets
- Push buttons and digital signing workflows are not implemented yet
- No non-form annotations or table abstraction layer
- Annotation support is currently limited to external links and text notes
- Metadata is currently written to the PDF Info dictionary only; XMP metadata packets are not emitted yet
MIT