Ruby

Last updated: 2026-07-02

The rustpdf gem wraps the rust-pdf C core with idiomatic, chainable classes over Ruby's built-in Fiddle standard library (pure FFI, no native gem to compile). It covers the whole product surface: vector graphics, embedded/subset fonts & Unicode text, paragraphs, images, PDF/A (1b–3a + A-4/4e/4f), tagged/accessible output, attachments, AcroForm fields, manipulation, text extraction, encryption and digital signatures.

Two classes do almost everything. RustPdf::Document authors a new PDF; RustPdf::EditableDoc loads and manipulates an existing one. Each holds a native handle, so call close when done to free it promptly (or let the GC do it).

Installation

Install from RubyGems. The native library (libpdf_ffi) ships inside platform-specific gems. macOS has arm64-darwin, x86_64-darwin and a fat universal-darwin (x86_64 + arm64); Linux has x86_64-linux and aarch64-linux; Windows has x64-mingw-ucrt. RubyGems downloads only the gem matching your OS and architecture, so there's nothing to compile.

shell
gem install rustpdf
# or in a Gemfile:
#   gem "rustpdf"

Requires Ruby 2.6+ (Fiddle is bundled with the standard library). Verify it loaded:

ruby
require "rustpdf"
puts RustPdf.version   # native library version
On a platform without a prebuilt gem, point RUSTPDF_LIB at a libpdf_ffi you built yourself (cargo build -p pdf-ffi --release) and the loader will use it.
macOS system Ruby. The platform gems are tagged arm64-darwin / x86_64-linux / aarch64-linux / x64-mingw-ucrt. The Ruby that ships with macOS reports its platform as universal-darwin, which RubyGems does not match against arm64-darwin, so gem install rustpdf fails with "Could not find a valid gem 'rustpdf'". Use a per-arch Ruby from rbenv, asdf or Homebrew (these report arm64-darwin-XX and match), or point RUSTPDF_LIB at a libpdf_ffi you built yourself.

Quick start

A one-page document with a filled rectangle, saved to disk:

ruby
require "rustpdf"

doc = RustPdf::Document.new          # A4 by default
doc.add_page
   .fill_rgb(0.86, 0.20, 0.18)
   .rect(72, 640, 200, 120)          # x, y, width, height (points)
   .fill
doc.save("out.pdf")
doc.close

Most methods return the document, so calls chain:

ruby
doc = RustPdf::Document.new
font = doc.add_font_file("Roboto-Regular.ttf")
doc.add_page
   .fill_rgb(0.1, 0.1, 0.12)
   .rect(0, 800, 595, 42).fill
   .show_text(font, 24, 72, 740, "Olá, açúcar — café")
data = doc.to_bytes                  # a String of bytes instead of a file
doc.close

Licensing & activation

Basic generation (everything above) is always free. The corporate features (PDF/A, digital signatures/PAdES, encryption, accessibility) require an active license token. Without one, those calls raise RustPdf::Error and produce no output.

Activation needs no rebuild. Easiest is an environment variable, auto-activated the first time a corporate feature is used:

shell
export RUSTPDF_LICENSE="010f0000…"           # the token we email you
# or point at a file:
export RUSTPDF_LICENSE_FILE=/etc/rustpdf/license.txt

Or activate explicitly in code:

ruby
RustPdf.activate_license(token)   # raises RustPdf::Error if forged / expired / malformed
Verification is fully offline: signature + expiry checked against a public key embedded in the library. No network callback, no telemetry.

Coordinate system

Threading & concurrency

Every native call is synchronous and the core is Send but not Sync: a single handle must never be touched by two threads at once.

Authoring: create & save

RustPdf::Document.new free

Creates an empty document (A4 default page size). Call close to free the native handle.

MethodDescription
add_page(width:, height:)Append a page. width/height are optional keyword points; omit for the default size.
default_size(w, h)Default size for subsequently added pages.
version = vSet the PDF header version (0 → 1.4, 1 → 1.5, 2 → 1.7, 3 → 2.0).
page_countNumber of pages so far.
to_bytesRender the document to a binary String.
save(path)Render and write to a file.
closeFree the native handle.
Validity. A document must have at least one page. Serializing an empty document raises an error. Color components (RGB/Gray/CMYK) are clamped to the valid 0–1 range.

Pages & vector graphics

Graphics state and path operators mirror PDF's content-stream model. Colors are RGB in 0.0–1.0.

MethodDescription
fill_rgb(r, g, b)Fill color.
stroke_rgb(r, g, b)Stroke color.
line_width(w)Stroke width in points.
rect(x, y, w, h)Add a rectangle subpath.
fillFill the current path with the fill color.
strokeStroke the current path with the stroke color.
ruby
doc = RustPdf::Document.new
doc.add_page
   .stroke_rgb(0.10, 0.45, 0.90).line_width(3)
   .rect(72, 600, 300, 160).stroke
   .fill_rgb(0.95, 0.77, 0.06)
   .rect(120, 640, 120, 80).fill
doc.save("shapes.pdf")
doc.close

Fonts & text

Fonts are embedded and subsetted, with HarfBuzz-quality shaping, kerning and full Unicode (Type0/CIDFontType2 with ToUnicode, so text extracts and copies correctly, provided the embedded font covers those characters). Register a font once, then reference it by its integer id.

add_font_file(path) → Integer   add_font(data) → Integer
show_text(font, size, x, y, text, heading_level: 0)

heading_level (1–6) tags the run as H1H6 in an accessible document (see Accessibility); leave it out (or 0) for ordinary text.

ruby
doc = RustPdf::Document.new
regular = doc.add_font_file("Roboto-Regular.ttf")
# …or from bytes you already have in memory:
# regular = doc.add_font(File.binread("Roboto-Regular.ttf"))

doc.add_page
   .show_text(regular, 28, 72, 760, "Invoice #1024")
   .show_text(regular, 12, 72, 720, "日本語 · Ελληνικά · العربية")
doc.save("text.pdf")
doc.close
Font coverage. Characters outside the embedded font's coverage are silently dropped. They render as the missing-glyph box and won't extract or copy. The bundled Roboto fallback covers Latin, Greek and Cyrillic but not CJK, Arabic, Hebrew or emoji. Embed a font that covers every script you write.
No NUL bytes in strings. Text passed across the API must not contain a NUL (\0) character. It truncates the string at the FFI boundary, silently dropping everything after the NUL. This applies to shown text, metadata and field names.

Paragraphs

The paragraph layer wraps, aligns and justifies text inside a fixed width (greedy line breaking using shaped glyph widths).

paragraph(font, size, x, y, width, text, align: RustPdf::Align::LEFT)
ruby
intro =
  "A long paragraph that wraps to the given width and is justified " \
  "automatically; extra space is distributed between words."

doc = RustPdf::Document.new
f = doc.add_font_file("Roboto-Regular.ttf")
doc.add_page
   .paragraph(f, 12, 72, 700, 451, intro, align: RustPdf::Align::JUSTIFY)
doc.save("paragraph.pdf")
doc.close

See the Align enum for the alignment options.

Images

JPEGs are embedded verbatim (DCTDecode, no re-encode). PNGs are decoded and re-encoded (FlateDecode); alpha becomes an /SMask, palette becomes an Indexed color space. Register an image once, draw it many times.

MethodDescription
add_image_file(path) → IntegerLoad JPEG/PNG from a file; returns the image id.
add_image_png(data) → IntegerRegister a PNG from a byte String.
add_image_jpeg(data) → IntegerRegister a JPEG from a byte String.
draw_image(image, x, y, w, h)Draw at (x, y) scaled to w × h points.
figure(image, x, y, w, h, alt)Draw as a tagged /Figure with alt text (accessibility).
ruby
doc = RustPdf::Document.new
logo = doc.add_image_file("logo.png")
doc.add_page.draw_image(logo, 72, 680, 160, 90)
doc.save("with_image.pdf")
doc.close

PDF/A licensed

Produce archival-grade output. pdfa defaults to A-2b; pass a Pdfa level for a specific one. An embedded sRGB ICC profile, output intent, XMP metadata and document /ID are added automatically; A-1b also forces PDF 1.4 and emits a /CIDSet, and A-4 (ISO 19005-4) is based on PDF 2.0.

pdfa(level = nil)
ruby
doc = RustPdf::Document.new
doc.pdfa(RustPdf::Pdfa::A2B).info(title: "Q3 Report", author: "Acme Inc.")
f = doc.add_font_file("Roboto-Regular.ttf")
doc.add_page.show_text(f, 20, 72, 760, "Archival report")
doc.save("report_pdfa.pdf")     # raises RustPdf::Error without a license granting PDF/A
doc.close
A document title is recommended for valid PDF/A metadata, and required for the accessible “a” levels: call info(title: …).
PDF/A-4f needs an attachment. The A4f profile requires at least one embedded file (ISO 19005-4); the library now rejects A4f output that has no attachment, so call attach_file before serializing.

Accessibility (Tagged PDF / PDF/UA) licensed

tagged builds a logical structure tree (PDF/UA-1). Combine with pdfa(RustPdf::Pdfa::A2A) for archival and accessible output. Use heading_level: on show_text for H1H6, and figure(..., alt) for described images.

tagged
ruby
doc = RustPdf::Document.new
doc.pdfa(RustPdf::Pdfa::A2A).tagged.info(title: "Accessible report")
f = doc.add_font_file("Roboto-Regular.ttf")
doc.add_page
   .show_text(f, 26, 72, 760, "Annual report", heading_level: 1)   # H1
   .show_text(f, 14, 72, 720, "Overview", heading_level: 2)         # H2
   .show_text(f, 11, 72, 690, "Body paragraph of the section…")
chart = doc.add_image_file("chart.png")
doc.figure(chart, 72, 520, 300, 150, "Revenue grew 18% year over year")
doc.save("accessible.pdf")
doc.close
Figures need a tagged document. figure() only produces an accessible, alt-texted figure inside a tagged/accessible document; on a plain document the alt text has no effect.

Attachments (PDF/A-3) licensed

PDF/A-3 allows embedding arbitrary source files (e.g. the XML behind an e-invoice). Each attachment carries a MIME type and an Relationship.

attach_file(name, mime, data, relationship: RustPdf::Relationship::SOURCE, description: "")
ruby
xml = File.binread("invoice.xml")
doc = RustPdf::Document.new
doc.pdfa(RustPdf::Pdfa::A3B).info(title: "E-invoice 1024")
f = doc.add_font_file("Roboto-Regular.ttf")
doc.add_page.show_text(f, 18, 72, 760, "Invoice 1024")
doc.attach_file("invoice.xml", "text/xml", xml,
                relationship: RustPdf::Relationship::SOURCE,
                description: "Structured invoice data")
doc.save("einvoice.pdf")
doc.close
The licence gate is on PDF/A, not on attach_file itself. The licensed badge above reflects the PDF/A-3 workflow shown here. Calling attach_file on a plain (non-PDF/A) document does not require a licence: it succeeds and produces a valid PDF with an /EmbeddedFile. The licence is enforced only when you also request a PDF/A level, which is what makes the attachment archival.

ZUGFeRD / Factur-X e-invoices licensed

Turn the document into a ZUGFeRD / Factur-X electronic invoice: the embedded XML (the Cross-Industry Invoice) is attached as factur-x.xml, the file is marked PDF/A-3, and the Factur-X identification is written into the XMP metadata. The visible PDF is the human-readable invoice; the embedded XML is its machine-readable twin. Validates as PDF/A-3 + Factur-X under veraPDF.

facturx(xml, profile: RustPdf::FacturxProfile::EN16931)
ruby
xml = File.binread("factur-x.xml")         # your Cross-Industry Invoice XML
doc = RustPdf::Document.new
doc.info(title: "Invoice INV-2026-001")
f = doc.add_font_file("Roboto-Regular.ttf")
doc.add_page.show_text(f, 18, 72, 760, "Invoice INV-2026-001")
doc.facturx(xml, profile: RustPdf::FacturxProfile::EN16931)
doc.save("einvoice.pdf")        # PDF/A-3 + Factur-X; needs a PDF/A license
doc.close

See the FacturxProfile enum for the conformance levels (MINIMUM to EXTENDED).

AcroForm fields

Build interactive forms with generated appearance streams (no NeedAppearances). Rectangles are [x0, y0, x1, y1] arrays; page is a 0-based page index. Dotted names ("a.b.c") create hierarchical fields.

MethodDescription
text_field(name, page, rect, value:, size:)Text input (size: 0 → auto font size).
checkbox(name, page, rect, checked)Checkbox (checked is a boolean).
dropdown(name, page, rect, options, selected:, size:)Combo box from an array of strings.
radio_group(name, page, buttons, selected:)buttons = array of [rect, "export"] pairs.
ruby
doc = RustPdf::Document.new
doc.add_page
   .text_field("applicant.name", 0, [72, 700, 320, 720], value: "")
   .checkbox("agree", 0, [72, 660, 88, 676], false)
   .dropdown("plan", 0, [72, 620, 240, 640],
             ["Starter", "Pro", "Enterprise"], selected: 1)
   .radio_group("billing", 0, [
     [[72, 580, 88, 596], "monthly"],
     [[140, 580, 156, 596], "annual"],
   ], selected: 1)
doc.save("form.pdf")
doc.close

Fill and flatten fields later with EditableDoc.

Page index and rectangle are validated. A field (or internal link) whose page does not exist, or whose rectangle is degenerate (x1 < x0 or zero area), is rejected when the document is serialized: to_bytes/save raise RustPdf::Error ("targets page index N…" / "degenerate rectangle…") instead of producing an invisible, never-appearing widget. Pass a 0-based page index that exists and a rectangle with x1 > x0 and y1 > y0.

Add clickable link rectangles to the current page: a web link opens a URL; an internal link jumps to another page (optionally scrolling so a given top y-coordinate sits at the top of the view).

link_uri(rect, uri)   link_to_page(rect, page_index, top: nil)
ruby
doc = RustPdf::Document.new
f = doc.add_font_file("Roboto-Regular.ttf")
doc.add_page.show_text(f, 14, 72, 760, "Visit rustpdf.dev (see page 2)")
doc.link_uri([72, 756, 320, 776], "https://rustpdf.dev/")   # web link
doc.link_to_page([330, 756, 430, 776], 1, top: 800)         # jump to page 2
doc.add_page
doc.save("links.pdf")
doc.close

Rectangles are [x0, y0, x1, y1] in points; page_index is 0-based.

Bookmarks / outline free

Build a navigable document outline. A Bookmark has a title, a target page and an optional top; nest children with .child(...). A document with bookmarks opens with the outline pane shown.

RustPdf::Bookmark.new(title, page, top: nil)   .child(bookmark)   add_bookmark(bookmark)
ruby
doc = RustPdf::Document.new
f = doc.add_font_file("Roboto-Regular.ttf")
3.times { doc.add_page }
doc.add_bookmark(
  RustPdf::Bookmark.new("Chapter 1", 0, top: 820)
    .child(RustPdf::Bookmark.new("Section 1.1", 1))
    .child(RustPdf::Bookmark.new("Section 1.2", 2)))
doc.add_bookmark(RustPdf::Bookmark.new("Chapter 2", 2))
doc.save("outline.pdf")
doc.close

Metadata

info(title:, author:, subject:, keywords:, creator:)

Sets the document information dictionary (and, for PDF/A, the matching XMP). Pass only the keywords you need.

ruby
doc.info(title: "Q3 Report", author: "Acme Inc.",
         subject: "Quarterly results", keywords: "finance, q3")

Manipulation: load an existing PDF

EditableDoc parses an existing document (classic & xref streams, object streams, all standard filters, RC4/AES decryption) into an editable model. Pages are a flat list; the page tree is rebuilt on output.

RustPdf::EditableDoc.load(data, password:)
RustPdf::EditableDoc.load_file(path, password:)
ruby
ed = RustPdf::EditableDoc.load_file("in.pdf")
puts ed.page_count
ed.close

# encrypted input:
sec = RustPdf::EditableDoc.load_file("secured.pdf", password: "user-or-owner-pw")
sec.save("plain.pdf")
sec.close
Damaged input is recovered, not rejected. Like qpdf, load runs a recovery scan over a malformed or truncated PDF instead of raising, so a badly damaged file can load with fewer pages than expected (even page_count == 0) and no error. When loading untrusted or possibly-corrupt input, check page_count before saving or processing.

Pages: merge, split, reorder, rotate

MethodDescription
merge(other)Append all pages of another EditableDoc (objects renumbered & remapped).
rotate_page(index, degrees)Rotate one page (90 / 180 / 270).
delete_page(index)Remove a page.
reorder_pages(order)Reorder with a full permutation array of indices.
extract_pages(indices) → EditableDocNew document containing just those pages.
page_countCurrent page count.
ruby
a = RustPdf::EditableDoc.load_file("a.pdf")
b = RustPdf::EditableDoc.load_file("b.pdf")
a.merge(b)                          # a now has a's pages followed by b's
a.rotate_page(0, 90)
a.reorder_pages((0...a.page_count).to_a.reverse)
a.save("merged.pdf")
b.close

subset = a.extract_pages([0, 2])    # pages 1 and 3
subset.save("subset.pdf")
subset.close
a.close
Page indices are 0-based. An out-of-range index to rotate/delete is silently ignored, and extract skips out-of-range indices. reorder_pages requires a true permutation of every page index (each used exactly once); an invalid argument (wrong length, a repeated index, or out-of-range) is rejected and leaves the page order unchanged. The call is a silent no-op that raises no error, so an invalid reorder cannot be detected from a return value.

Metadata, overlay & form fill

MethodDescription
set_info(key, value)Set one info entry (e.g. "Title").
get_info(key) → StringRead an info entry.
set_xmp(xml)Replace the XMP metadata stream.
overlay_page(index, content)Overlay a content-stream fragment onto a page (stamps/watermarks).
fill_text_field(name, value) → BooleanFill an AcroForm text field; returns whether it was found.
ruby
ed = RustPdf::EditableDoc.load_file("form.pdf")
ed.set_info("Title", "Filled form")
found = ed.fill_text_field("applicant.name", "Jane Doe")
puts "filled: #{found} | title: #{ed.get_info('Title')}"
ed.save("filled.pdf")
ed.close

Form fill & flatten free

Fill the fields of an existing AcroForm and (optionally) flatten them: filling generates a fresh appearance stream (no NeedAppearances), and flattening bakes every widget's appearance into the page content and removes the interactive form entirely.

MethodDescription
field_names → Array<String>Fully-qualified names of every terminal field.
fill_text_field(name, value) → BooleanSet a text (or text-style choice) field; returns whether it matched.
set_checkbox(name, checked = true) → BooleanCheck/uncheck a checkbox.
set_radio(name, export_value) → BooleanSelect a radio button by its export value.
set_choice(name, value) → BooleanSet a dropdown / list-box value.
flatten_formsBake all fields into static content and drop the /AcroForm.
ruby
ed = RustPdf::EditableDoc.load_file("form.pdf")
p ed.field_names                    # ["applicant.name", "agree", "plan", ...]
ed.fill_text_field("applicant.name", "Jane Doe")
ed.set_checkbox("agree", true)
ed.set_radio("billing", "annual")
ed.set_choice("plan", "Pro")
ed.flatten_forms                    # optional: make it non-editable
ed.save("filled.pdf")
ed.close

Watermarks free

Stamp a diagonal text watermark or a centered image watermark across every page, drawn semi-transparently over the existing content. Text uses the standard Helvetica font, so keep it to WinAnsi (Latin-1) for stamps like "CONFIDENTIAL".

watermark_text(text, size: 64.0, color: [0.5, 0.5, 0.5], opacity: 0.30, rotation_deg: 45.0, opaque_background: false)
watermark_image_file(path, width, height, opacity: 0.30, rotation_deg: 0.0)

When opaque_background is true, the text is drawn over an opaque white box (a white-out stamp) instead of being blended into the page content. watermark_image_file rotates the image rotation_deg degrees counter-clockwise.

ruby
ed = RustPdf::EditableDoc.load_file("report.pdf")
ed.watermark_text("CONFIDENTIAL", opacity: 0.25, rotation_deg: 45,
                  opaque_background: true)
ed.watermark_image_file("stamp.png", 120, 120, opacity: 0.20, rotation_deg: 30)
ed.save("stamped.pdf")
ed.close

Positioned drawing free

Place a filled rectangle, a line of text, a wrapped paragraph or an image at exact coordinates on a page. Coordinates are in the page's visible space by default (origin lower-left, y up), so content lands where the viewer sees it regardless of the page's /Rotate (see the Interactive positioning guide). All return whether the page (and font) existed.

fill_rect(page_index, x, y, width, height, color = [1.0, 1.0, 1.0], opacity = 1.0) → Boolean
place_text(page_index, x, y, text, size = 12.0, color = [0.0, 0.0, 0.0], rotation_deg = 0.0, align: RustPdf::Align::LEFT, font_id: -1, anchor: RustPdf::VerticalAnchor::BASELINE) → Boolean
masked_text(page_index, x, y, width, height, text, size = 12.0, text_color = [0.0, 0.0, 0.0], bg_color = [1.0, 1.0, 1.0], align: RustPdf::Align::LEFT, font_id: -1, valign: RustPdf::VerticalAlign::MIDDLE, padding: nil) → Boolean
draw_image(page_index, image, x, y, width, height, rotation_deg = 0.0, anchor: RustPdf::ImageAnchor::CORNER) → Boolean

color is RGB (each component 0..1). place_text draws the baseline at (x, y); rotation_deg rotates the text counter-clockwise about its anchor (match the page rotation to follow a rotated page), and align: (a RustPdf::Align value) shifts the start point along the baseline by the text width so RIGHT/CENTER anchor at (x, y). masked_text is the masking idiom in one call: it fills the box [x, y, x+width, y+height] in bg_color (default white), then writes text in text_color (default black) horizontally aligned per align:. draw_image takes raw JPEG/PNG image bytes (a binary String) and places the image with its lower-left corner at (x, y) scaled to width × height points.

ruby
ed = RustPdf::EditableDoc.load_file("form.pdf")
geom = RustPdf.measure_page(File.binread("form.pdf"), 0)
puts "page is #{geom.width}×#{geom.height} pt"

# white-out a placeholder, then write the real value on top
ed.fill_rect(0, 100, 100, 200, 40)                       # opaque white mask
ed.place_text(0, 110, 112, "APPROVED", 14, [0, 0, 1])    # blue 14 pt text
ed.draw_image(0, File.binread("logo.png"), 110, 160, 120, 48)  # stamp a logo

# or do the mask + centered value in one call
ed.masked_text(0, 100, 100, 200, 40, "APPROVED", 14,
               [0, 0, 1], [1, 1, 1], align: RustPdf::Align::CENTER)
ed.save("filled.pdf")
ed.close

Stamping fonts

By default the stamping primitives draw with the built-in Helvetica (WinAnsi / Latin-1 only). Register a TrueType/OpenType font on the EditableDoc and pass its id as font_id: to stamp with real embedded glyphs and metrics: full Unicode, subset-embedded exactly like Document#add_font_file + show_text.

add_font_file(path) → Integer   add_font(data) → Integer
ruby
ed = RustPdf::EditableDoc.load_file("form.pdf")
roboto = ed.add_font_file("Roboto-Regular.ttf")
ed.place_text(0, 72, 700, "Şărbătoare — Unicode ✓", 14, font_id: roboto)

Vertical anchors

place_text's anchor: keyword (a RustPdf::VerticalAnchor) says what y means: BASELINE (default, the historical behavior), TOP hangs the text from y (the baseline lands ascent × size below it, matching legacy fixed-position layout), BOTTOM rests the descender line on y. LINE_TOP / LINE_BOTTOM anchor via the layout line box; its height is also the leading basis, so a single anchored line and a wrapped paragraph agree vertically. Ascent/descent come from the selected font's metrics.

ruby
# y = 700 is the TOP of the text, not the baseline (legacy layout semantics)
ed.place_text(0, 72, 700, "Hangs from y", 12,
              anchor: RustPdf::VerticalAnchor::TOP)
# layout line-box bottom at y = 640
ed.place_text(0, 72, 640, "Line box bottom", 12,
              anchor: RustPdf::VerticalAnchor::LINE_BOTTOM)

masked_text vertical alignment & padding

valign: (a RustPdf::VerticalAlign) controls where the line sits inside the box: MIDDLE (default, the historical cap-height centering), TOP hangs the line from the top edge (top line-alignment in rectangle-based text APIs semantics), BOTTOM rests the descender line on the bottom edge. padding: is the horizontal edge inset in points for LEFT/RIGHT alignment: text starts at x + padding (or ends at x + width − padding). nil keeps the historical min(0.15 × size, width / 4); 0 starts flush with the box edge like rectangle-based DrawString APIs.

ruby
ed.masked_text(0, 100, 100, 200, 40, "TOP-LEFT, flush", 12,
               valign: RustPdf::VerticalAlign::TOP, padding: 0)

Paragraphs with word wrapping

place_paragraph(page_index, x, y, width, text, size: 12.0, color: [0.0, 0.0, 0.0], align: RustPdf::Align::LEFT, font_id: -1, max_height: nil, line_height: 1.0, anchor: RustPdf::VerticalAnchor::TOP, rotation_deg: 0.0) → Boolean
place_paragraph_measured(...) → { lines: Integer, height: Float }

place_paragraph breaks text into lines that fit width points (greedy, by word; "\n" forces a break) and draws them downward from (x, y). With the default anchor: TOP, (x, y) is the block's top-left corner: the first baseline lands ascent × size below y, like legacy fixed-position layout. BOTTOM / LINE_BOTTOM bottom-pin the block: its bottom rests on y and it grows upward by its real content height. max_height: is a ceiling: overflowing lines are cut (from the bottom when top-anchored, from the top when bottom-pinned, so the last lines stay pinned). line_height: scales the default 1.2 × size leading. align: RustPdf::Align::JUSTIFY stretches the word gaps of every line but the last of each paragraph. place_paragraph_measured takes the same arguments and returns how many lines were drawn and the consumed block height in points, so you can stack blocks without re-measuring.

ruby
terms = "The parties agree that the deliverables described herein " \
        "shall be provided in accordance with the schedule…"

# wrap into a 300 pt column whose TOP is at y = 700
res = ed.place_paragraph_measured(0, 72, 700, 300, terms,
                                  size: 10, font_id: roboto,
                                  max_height: 120)         # truncate past 120 pt
puts "drew #{res[:lines]} lines, #{res[:height]} pt tall"

# next block starts right below the previous one
ed.place_paragraph(0, 72, 700 - res[:height] - 12, 300, "Next clause…",
                   size: 10, font_id: roboto)

# bottom-pinned: the block's bottom rests on y = 60 and grows upward
ed.place_paragraph(0, 72, 60, 300, "Footer note that may wrap",
                   size: 8, anchor: RustPdf::VerticalAnchor::BOTTOM)

Coordinate space: visible vs media

stamp_space = RustPdf::StampSpace::VISIBLE | MEDIA   set_stamp_space(space) (chainable)

Chooses the coordinate space of all the positioned stamping primitives for subsequent calls. VISIBLE (default) keeps the historical behavior: coordinates in the page's displayed space, compensating /Rotate so a rotation_deg = 0 stamp reads upright on screen. MEDIA interprets coordinates and rotation_deg in the raw PDF user space (the raw-coordinate semantics of legacy layout engines), never composing with the page's /Rotate or crop offset; use it to reproduce legacy-engine placement on rotated/scanned pages. Watermarks and redaction are unaffected.

ruby
ed.stamp_space = RustPdf::StampSpace::MEDIA   # legacy raw-space coordinates from here on
ed.place_text(0, 72, 60, "Same numbers as the legacy layout engines code")
ed.set_stamp_space(RustPdf::StampSpace::VISIBLE)  # back to the default

Image rotation anchor

draw_image's anchor: keyword (a RustPdf::ImageAnchor) controls how a rotated image is anchored: CORNER (default) rotates the image about its own lower-left corner at (x, y), so the image sweeps around the point. BOUNDING_BOX lands the rotated image's bounding box with its lower-left at (x, y), bounding-box layout semantics: a 90° image occupies [x, x+height] × [y, y+width], and the drawn pixels always sit at/above/right of the anchor.

ruby
ed.draw_image(0, File.binread("logo.png"), 300, 100, 120, 48, 90.0,
              anchor: RustPdf::ImageAnchor::BOUNDING_BOX)

Redaction licensed

True redaction: the text and graphics whose origin falls inside a rectangle are removed from the content stream (not just covered), so the data is gone from the file and is no longer extractable. Opaque black boxes are then painted over the regions.

redact(page_index, rects) → Boolean
ruby
ed = RustPdf::EditableDoc.load_file("statement.pdf")
# rects = array of [x0, y0, x1, y1] on that page
ed.redact(0, [[60, 590, 400, 620], [60, 540, 400, 570]])
ed.save("redacted.pdf")     # raises RustPdf::Error without a license granting redaction
ed.close
Content under a rect is deleted before the file is written, so RustPdf.extract_text on the output no longer returns it.

Convert to PDF/A licensed

Convert an existing PDF to archival PDF/A (a basic profile: A-1b, A-2b or A-3b). An sRGB output intent, PDF/A XMP metadata (synced with /Info) and a document /ID are added. Fails if any font is not embedded (PDF/A requires every font embedded) or a level-A profile is requested.

convert_to_pdfa(level = RustPdf::Pdfa::A2B)
ruby
ed = RustPdf::EditableDoc.load_file("in.pdf")
ed.convert_to_pdfa(RustPdf::Pdfa::A2B)   # raises RustPdf::Error if fonts aren't embedded
ed.save("archival.pdf")                  # veraPDF: PDF/A-2b compliant
ed.close

Optimize & compact

MethodDescription
optimizeDrop unreferenced objects, Flate-compress uncompressed streams, dedupe identical objects.
compact(on = true)Pack objects into object streams + emit a cross-reference stream.
ruby
ed = RustPdf::EditableDoc.load_file("big.pdf")
ed.optimize.compact(true)
ed.save("small.pdf")
ed.close

Encryption licensed

Apply standard-handler encryption at output. AES-256 (V5/R6) uses OS-CSPRNG keys/IVs.

encrypt(method: RustPdf::Cipher::AES256, user: "", owner: "", read_only: false)
ruby
ed = RustPdf::EditableDoc.load_file("in.pdf")
ed.encrypt(method: RustPdf::Cipher::AES256, owner: "owner-secret", read_only: true)
ed.save("secured.pdf")          # raises RustPdf::Error without an Encryption license
ed.close

See the Cipher enum for RC4 / AES-128 / AES-256.

read_only is not confidentiality. It only sets the permission bits (/P), which cooperating readers honour but the format does not enforce. Real confidentiality comes from a non-empty user password plus a strong cipher (prefer AES-256): the content is then unreadable without the password. A document encrypted with only an owner password (no user password) still opens for anyone; read_only alone restricts nothing cryptographically.

Output & incremental update

MethodDescription
to_bytes → StringSerialize the manipulated document.
save(path)Serialize to a file.
to_bytes_incremental(original) → StringAppend only changes to the original bytes (signature-safe, non-destructive).
ruby
original = File.binread("in.pdf")
ed = RustPdf::EditableDoc.load(original)
ed.set_info("Subject", "reviewed")
incremental = ed.to_bytes_incremental(original)   # original bytes preserved verbatim
File.binwrite("reviewed.pdf", incremental)
ed.close

Normalize & downgrade free

Set the output PDF version or strip PDF/A conformance from a loaded document. set_version downgrades (or upgrades) the header version; strip_pdfa removes the PDF/A markers (OutputIntents, the XMP pdfaid identifier and catalog /Version) so the file is a plain PDF; normalize does both at once.

MethodDescription
set_version(version)Set the output version using a Version code (V1_4, V1_5, V1_7, V2_0).
strip_pdfaRemove PDF/A conformance, leaving a plain PDF.
normalize(version = RustPdf::Version::V1_7)Strip PDF/A and set the version in one call.
ruby
ed = RustPdf::EditableDoc.load_file("archival.pdf")
ed.normalize(RustPdf::Version::V1_7)   # strip PDF/A + downgrade to 1.7
# or step by step:
# ed.strip_pdfa.set_version(RustPdf::Version::V1_7)
ed.save("plain.pdf")
ed.close

Digital signatures licensed

Sign a PDF with a PKCS#7 detached signature via an incremental update (the original bytes are preserved). Keys and certificates are passed as DER byte Strings. pades: true switches to PAdES-B-B.

RustPdf.sign(pdf, key_der, cert_der, reason:, location:, name:, pades:) → String
ruby
pdf      = File.binread("contract.pdf")
key_der  = File.binread("signing-key.pkcs8.der")   # PKCS#8 private key (DER)
cert_der = File.binread("signing-cert.der")        # X.509 certificate (DER)

signed = RustPdf.sign(pdf, key_der, cert_der,
                      reason: "Approved", location: "New York",
                      name: "Jane Doe", pades: true)
File.binwrite("contract.signed.pdf", signed)
# Verify in a shell: pdfsig contract.signed.pdf  →  "Signature is Valid."

Deferred / HSM signing

Sign without the private key ever entering this library: the key stays in an HSM, cloud KMS, smartcard or PKI token. The library builds the CMS signed attributes and asks your code only for the raw RSA PKCS#1 v1.5 signature over SHA-256 of the bytes it hands you. This works with any PKI (eIDAS, AATL, or your own CA). Use RustPdf.list_signatures first to inventory the signature fields already present.

RustPdf.sign_with(pdf, cert_der, chain:, options:) { |to_sign| ... } → String

Model A (remote signer). The block receives the bytes to sign and returns the raw RSA signature; the private key never reaches the library. cert_der is the signer certificate (DER) and chain: the intermediate certificates (DER), both supplied independently of the key. Pass authoring details and policy through options: (a SigningOptions).

ruby
pdf      = File.binread("contract.pdf")
cert_der = File.binread("signing-cert.der")   # X.509 signer certificate (DER)

opts = RustPdf::SigningOptions.new(reason: "Approved", location: "HQ",
                                   name: "Jane Doe", pades: true)

signed = RustPdf.sign_with(pdf, cert_der, chain: [issuer_der], options: opts) do |to_sign|
  hsm.sign_rsa_sha256(to_sign)   # bytes in, raw signature out (HSM / cloud KMS / token)
end
File.binwrite("contract.signed.pdf", signed)
RustPdf.begin_signing(pdf, options:) → SigningSession
RustPdf.complete_signature(document, container) → String

Model B (two-phase). Use this when the signer is asynchronous or lives in a separate process. begin_signing returns a SigningSession whose #document is the prepared PDF (with a zero-filled /Contents placeholder), #bytes the exact bytes the signature covers, and #hash their SHA-256 (the 32-byte value an HSM signs). Build the DER CMS / PKCS#7 container out of band, then call session.complete(container) (or RustPdf.complete_signature(document, container)). The key never reaches the library.

ruby
pdf = File.binread("contract.pdf")

session   = RustPdf.begin_signing(pdf, options: RustPdf::SigningOptions.new(reason: "Approved"))
container = remote.build_cms(session.hash)   # send the 32-byte hash to a remote signer
final     = session.complete(container)      # embed the finished DER CMS / PKCS#7
File.binwrite("contract.signed.pdf", final)
RustPdf.list_signatures(pdf) → Array<SignatureField>

Inventory the signature fields in a PDF before signing. Each SignatureField is a Struct with name and signed (true when the field already carries a signature). An empty array means there are no signature fields.

ruby
RustPdf.list_signatures(File.binread("contract.pdf")).each do |f|
  puts "#{f.name}: #{f.signed ? 'signed' : 'empty'}"
end

Both models accept a SigningOptions:

FieldMeaning
reason: / location: / name:Signature metadata recorded in the signature dictionary.
pades:true produces a PAdES-B-B signature (ETSI.CAdES.detached).
certify:Certify the document (DocMDP) with a Certify value; use only on the first signature.
container_size:Reserved /Contents bytes; 0 = default. Raise it for large cloud-KMS CMS containers.
policy:A SignaturePolicy identifier (PAdES-EPES); nil = none.
visible: / visible_page: / visible_rect: / visible_text:Draw a visible signature appearance. visible_rect is [x0, y0, x1, y1] in points on visible_page; visible_text is newline-separated appearance lines.
visible_image:PNG/JPEG bytes of a handwritten-signature image, drawn aspect-fit behind any visible_text.

A visible signature with an embedded image:

ruby
opts = RustPdf::SigningOptions.new(
  reason: "Approved", name: "Jane Doe",
  visible: true, visible_page: 0, visible_rect: [360, 60, 540, 130],
  visible_text: "Jane Doe\n2026-06-30",
  visible_image: File.binread("signature.png"))   # PNG/JPEG, aspect-fit behind the text

RustPdf::Certify selects the DocMDP level: NONE (not certifying), LOCKED (no changes permitted), FORMS (form-filling and signing permitted) and FORMS_AND_ANNOTATIONS (also annotations). RustPdf::SignaturePolicy.new(oid:, hash:, hash_algorithm_oid:, uri:) carries a policy OID, its document hash bytes (under hash_algorithm_oid, nil = SHA-256) and an optional SPURI.

Timestamp & DSS (PAdES LTV) licensed

Build long-term-validation signatures offline. add_dss appends a Document Security Store (/DSS with certs/CRLs, PAdES-B-LT); timestamp appends an RFC 3161 document timestamp (/DocTimeStamp, PAdES-B-LTA).

RustPdf.add_dss(pdf, certs:, crls:) → String
RustPdf.timestamp(pdf, tsa_key_der, tsa_cert_der, date:) → String
ruby
signed = File.binread("contract.signed.pdf")

# B-LT: embed validation material (caller supplies DER certs/CRLs)
lt = RustPdf.add_dss(signed, certs: [cert_der], crls: [crl_der])

# B-LTA: add a document timestamp signed by a TSA key/cert
lta = RustPdf.timestamp(lt, tsa_key_der, tsa_cert_der)
File.binwrite("contract.lta.pdf", lta)

Network TSA timestamp (AD-RT)

To timestamp against a real network TSA instead of a local key, use the transport-agnostic helpers: you do the HTTP POST, the library does the cryptography. begin_timestamp prepares the document and returns [document, tbs]; build an RFC 3161 request with timestamp_request, POST it to the TSA, extract the token with timestamp_token_from_response, then embed it with complete_signature. Free public TSAs work (FreeTSA https://freetsa.org/tsr, DigiCert http://timestamp.digicert.com).

RustPdf.begin_timestamp(pdf) → [document, tbs]
RustPdf.timestamp_request(imprint, nonce:, cert_req:) → String
RustPdf.timestamp_token_from_response(response) → String
ruby
require "net/http"
require "digest"

signed = File.binread("contract.signed.pdf")
document, tbs = RustPdf.begin_timestamp(signed)

imprint = Digest::SHA256.digest(tbs)               # RFC 3161 message imprint
req     = RustPdf.timestamp_request(imprint, cert_req: true)

resp = Net::HTTP.post(URI("https://freetsa.org/tsr"), req,
                      "Content-Type" => "application/timestamp-query")
token = RustPdf.timestamp_token_from_response(resp.body)

lta = RustPdf.complete_signature(document, token)  # embed the /DocTimeStamp
File.binwrite("contract.lta.pdf", lta)

Validate signatures licensed

Validate every signature in a PDF: each report recomputes the /ByteRange digest, parses the CMS, and checks that the cryptographic signature is valid, that the messageDigest matches the covered bytes, and whether the signature covers the whole document.

RustPdf.verify_signatures(data) → Array<Hash>

Each entry is a Hash with keys "field_name" and "signer" (both nil when absent), plus "sub_filter", "covers_whole_document", "digest_valid", "signature_valid", "is_valid" and "byte_range". Each report also carries the richer certificate fields (any may be nil): "issuer", "serial_number", "valid_from", "valid_to", "algorithm" (e.g. SHA256withRSA), "signing_time", "cert_count" and "has_timestamp". An empty array means the document is unsigned. Signature validation is a licensed feature: this call requires an active license (signatures) even when the document is unsigned, so it is not available on the free tier.

ruby
data = File.binread("contract.signed.pdf")
RustPdf.verify_signatures(data).each do |sig|
  puts "#{sig['signer']} valid: #{sig['is_valid']} " \
       "covers whole doc: #{sig['covers_whole_document']}"
  puts "  issuer: #{sig['issuer']} serial: #{sig['serial_number']}"
  puts "  algorithm: #{sig['algorithm']} signed: #{sig['signing_time']}"
  puts "  valid #{sig['valid_from']} → #{sig['valid_to']}, " \
       "certs: #{sig['cert_count']}, timestamped: #{sig['has_timestamp']}"
end

Text & image extraction

Extract a document's text, mapping shown glyph codes back to Unicode through each font's ToUnicode map, with space/line inference. Raster images can be pulled out too: JPEGs are written verbatim as .jpg, everything else as .png. The output directory is created automatically if it does not already exist.

RustPdf.extract_text(data) → String free
RustPdf.extract_page_text(data, page_index) → String free
RustPdf.extract_images_to_dir(data, dir) → Integer free

extract_page_text returns the text of a single 0-based page without building an intermediate one-page document.

ruby
data = File.binread("report.pdf")
puts RustPdf.extract_text(data)
puts RustPdf.extract_page_text(data, 0)   # just the first page

n = RustPdf.extract_images_to_dir(data, "out_images/")   # returns how many were written
puts "wrote #{n} image(s)"
Extracted text is best-effort, not byte-faithful. Two things to expect when comparing the output against your source string. (1) Unicode normalization can differ. A character you wrote pre-composed (e.g. "à", U+00E0) may come back de-composed ("a" + U+0300), so the result can mix NFC and NFD forms. Normalize both sides (str.unicode_normalize) before any byte-for-byte comparison. (2) Layout is inferred, not exact. Spaces and line breaks are reconstructed from glyph positions, so independently positioned runs may be joined or split differently than you laid them out. Treat extract_text as a search/indexing aid (good for RAG), not as a lossless inverse of authoring.

Find every occurrence of a query string and get back its position on the page. Each hit is a box in PDF points (origin lower-left), so you can highlight, redact or link the result. See the concept page searching text in a PDF for background.

RustPdf.find_text(pdf, query, case_sensitive: false) → Array<TextHit>

Returns an Array of TextHit (a Struct with page, text, x, y, width, height); x/y is the lower-left corner of the box. case_sensitive defaults to false. An empty Array means no match.

ruby
data = File.binread("report.pdf")
RustPdf.find_text(data, "Total").each do |hit|
  puts "page #{hit.page}: '#{hit.text}' at " \
       "(#{hit.x}, #{hit.y}) #{hit.width}×#{hit.height} pt"
end

Page geometry free

Read the size, rotation, MediaBox and CropBox of every page without mutating the document. Sizes are in PDF points (72 per inch). width/height ignore the page's /Rotate, while rotated_width/rotated_height account for it (swapped for 90°/270° pages).

RustPdf.measure_pages(pdf) → Array<PageGeometry>
RustPdf.measure_page(pdf, index) → PageGeometry

Each PageGeometry is a Struct with page, width, height, rotation, rotated_width, rotated_height, media_box and crop_box. The boxes are PdfRect values (x0, y0, x1, y1, plus width and height). measure_page raises IndexError when index is out of range.

ruby
data = File.binread("report.pdf")
RustPdf.measure_pages(data).each do |g|
  puts "page #{g.page}: #{g.width}×#{g.height} pt, /Rotate #{g.rotation}, " \
       "as displayed #{g.rotated_width}×#{g.rotated_height} pt"
end

first = RustPdf.measure_page(data, 0)
puts "MediaBox #{first.media_box.width}×#{first.media_box.height} pt"

Document inspection free

Get a non-mutating overview of a PDF (version, PDF/A level, encryption posture, page count) without opening or decrypting it. The encryption fields are reported even for password-protected files, so you can branch before supplying a password.

RustPdf.inspect_pdf(pdf) → PdfOverview

Returns a PdfOverview Struct with version (e.g. "1.7"), pdfa_level (nil when not PDF/A), encrypted (Boolean), encryption ("None", "RC4", "AES-128" or "AES-256"), requires_password (Boolean) and page_count. It is named inspect_pdf to avoid shadowing Object#inspect.

ruby
info = RustPdf.inspect_pdf(File.binread("report.pdf"))
puts "PDF #{info.version}, #{info.page_count} page(s)"
puts "PDF/A: #{info.pdfa_level || 'no'}"
puts "encryption: #{info.encryption} (needs password: #{info.requires_password})"

Render a page to an image licensed

Rasterize a page to a PNG image. A native Rust renderer (built on tiny-skia, with no headless browser) interprets the page content stream, painting real glyph outlines, vector graphics, images, color and transparency. Page rendering is a Pro feature; page_count is free.

RustPdf.render_page_to_png(pdf, page = 0, dpi = 150.0) → String licensed
RustPdf.page_count(pdf) → Integer free
ruby
data = File.binread("report.pdf")
puts "#{RustPdf.page_count(data)} page(s)"
File.binwrite("page1.png", RustPdf.render_page_to_png(data, 0, 150.0))

Enums

Pdfa

ValueLevel
RustPdf::Pdfa::A1BPDF/A-1b (basic, PDF 1.4)
RustPdf::Pdfa::A2BPDF/A-2b (basic): default of pdfa
RustPdf::Pdfa::A2APDF/A-2a (accessible: pair with tagged)
RustPdf::Pdfa::A3BPDF/A-3b (basic, allows attachments)
RustPdf::Pdfa::A3APDF/A-3a (accessible + attachments)
RustPdf::Pdfa::A4PDF/A-4 (ISO 19005-4, based on PDF 2.0)
RustPdf::Pdfa::A4EPDF/A-4e (engineering)
RustPdf::Pdfa::A4FPDF/A-4f (requires at least one embedded file)

Align

ValueMeaning
RustPdf::Align::LEFTLeft-aligned (default)
RustPdf::Align::RIGHTRight-aligned
RustPdf::Align::CENTERCentered
RustPdf::Align::JUSTIFYJustified (space distributed between words)

VerticalAnchor

ValueMeaning of y (place_text / place_paragraph)
RustPdf::VerticalAnchor::BASELINEText baseline (place_text default)
RustPdf::VerticalAnchor::TOPTop of the text/block, legacy fixed-position layout (place_paragraph default)
RustPdf::VerticalAnchor::BOTTOMDescender line / block bottom rests on y
RustPdf::VerticalAnchor::LINE_TOPTop of the layout line box
RustPdf::VerticalAnchor::LINE_BOTTOMBottom of the layout line box

VerticalAlign

ValueLine position inside a masked_text box
RustPdf::VerticalAlign::TOPHangs from the top edge (top line-alignment in rectangle-based text APIs)
RustPdf::VerticalAlign::MIDDLECap-height centered (default)
RustPdf::VerticalAlign::BOTTOMDescender line on the bottom edge

StampSpace

ValueCoordinate space of the stamping primitives
RustPdf::StampSpace::VISIBLEDisplayed space, compensating /Rotate (default)
RustPdf::StampSpace::MEDIARaw PDF user space (legacy layout semantics)

ImageAnchor

ValueRotated draw_image anchoring
RustPdf::ImageAnchor::CORNERRotate about the image's lower-left corner (default)
RustPdf::ImageAnchor::BOUNDING_BOXRotated bounding box lands at (x, y) (legacy layout engines)

Relationship

ValueMeaning
RustPdf::Relationship::SOURCESource data for the document (e.g. the invoice XML)
RustPdf::Relationship::DATAData used to derive the visual content
RustPdf::Relationship::ALTERNATIVEAlternative representation
RustPdf::Relationship::SUPPLEMENTSupplementary material
RustPdf::Relationship::UNSPECIFIEDUnspecified relationship

Cipher

ValueCipher
RustPdf::Cipher::RC4RC4 (legacy)
RustPdf::Cipher::AES128AES-128
RustPdf::Cipher::AES256AES-256 (V5/R6): recommended

Version

ValuePDF version
RustPdf::Version::V1_4PDF 1.4
RustPdf::Version::V1_5PDF 1.5
RustPdf::Version::V1_7PDF 1.7 (default of normalize)
RustPdf::Version::V2_0PDF 2.0

FacturxProfile

ValueConformance level
RustPdf::FacturxProfile::MINIMUMMinimal header data only
RustPdf::FacturxProfile::BASIC_WLBasic, without line items
RustPdf::FacturxProfile::BASICBasic, with line items
RustPdf::FacturxProfile::EN16931EN 16931 (Comfort): the interoperable core, default
RustPdf::FacturxProfile::EXTENDEDEN 16931 plus extensions

Error handling

Every failing native call raises RustPdf::Error (a StandardError) carrying the status (PdfStatus) code and the library's last-error message. License failures (missing/expired/forged token, or a feature the token doesn't grant) surface here too. Token activation failures carry the dedicated license status code (12); a gated build, sign or encrypt call instead reports that operation’s own status (for example Serialize = 4 or Sign = 10) with the same “requires a valid license” message.

Always branch on the message, not on status == 0. A few raising paths (notably a failed EditableDoc.load of an unparseable or wrong-password file, and using a handle after close) report status 0, the same numeric code as success. The exception was still raised and is still a genuine failure; 0 here only means "no specific PdfStatus was attached". Decide success or failure by whether RustPdf::Error was raised (rescue it), and use e.message for the human-readable reason. Do not treat status 0 as "ok".
ruby
begin
  doc = RustPdf::Document.new
  doc.pdfa.info(title: "x")
  doc.add_page
  doc.save("out.pdf")
  doc.close
rescue RustPdf::Error => e
  warn "failed: #{e.status} #{e.message}"
  # e.g. PdfStatus=4 (Serialize): feature 'pdfa' requires a valid license
end

Utilities

FunctionDescription
RustPdf.version → StringNative library version string.
RustPdf.activate_license(token)Activate a license token (raises on invalid/expired).
Looking for another language? The same API exists in Python, Node / TypeScript, Delphi / Free Pascal, Swift, C#, Go and PHP: browse all docs. They share one core, so behavior is identical.