Ruby
Last updated: 2026-06-29
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.
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.
gem install rustpdf
# or in a Gemfile:
# gem "rustpdf"Requires Ruby 2.6+ (Fiddle is bundled with the standard library). Verify it loaded:
require "rustpdf"
puts RustPdf.version # native library versionRUSTPDF_LIB at a libpdf_ffi you built yourself (cargo build -p pdf-ffi --release) and the loader will use it.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:
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.closeMost methods return the document, so calls chain:
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.closeLicensing & 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:
export RUSTPDF_LICENSE="010f0000…" # the token we email you
# or point at a file:
export RUSTPDF_LICENSE_FILE=/etc/rustpdf/license.txtOr activate explicitly in code:
RustPdf.activate_license(token) # raises RustPdf::Error if forged / expired / malformedCoordinate system
- Units are points (1 pt = 1/72 inch). A4 is
595 × 842, US Letter612 × 792. - The origin
(0, 0)is the bottom-left corner;ygrows upward. - For text,
(x, y)is the baseline of the first glyph. - Drawing/text always targets the most recently added page.
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.
- Documents are independent. Each
Document/EditableDocshares no state, so they're safe to build across threads or processes, one handle per thread. - Don't share a live handle between threads; give each its own.
- Errors are per-thread. The native last-error is thread-local, so a failure in one thread never clobbers another's, and
RustPdf::Errorsurfaces on the calling side. - License is process-global.
activate_license(or the env var) applies to every thread; activate once at startup. - The GVL: a long generation holds Ruby's global VM lock for its duration. For heavy parallel jobs, fan out across processes (e.g. a job queue), one document per worker.
Authoring: create & save
RustPdf::Document.new freeCreates an empty document (A4 default page size). Call close to free the native handle.
| Method | Description |
|---|---|
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 = v | Set the PDF header version (0 → 1.4, 1 → 1.5, 2 → 1.7, 3 → 2.0). |
page_count | Number of pages so far. |
to_bytes | Render the document to a binary String. |
save(path) | Render and write to a file. |
close | Free the native handle. |
Pages & vector graphics
Graphics state and path operators mirror PDF's content-stream model. Colors are RGB in 0.0–1.0.
| Method | Description |
|---|---|
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. |
fill | Fill the current path with the fill color. |
stroke | Stroke the current path with the stroke color. |
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.closeFonts & 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) → Integershow_text(font, size, x, y, text, heading_level: 0)heading_level (1–6) tags the run as H1–H6 in an accessible document (see Accessibility); leave it out (or 0) for ordinary text.
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\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)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.closeSee 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.
| Method | Description |
|---|---|
add_image_file(path) → Integer | Load JPEG/PNG from a file; returns the image id. |
add_image_png(data) → Integer | Register a PNG from a byte String. |
add_image_jpeg(data) → Integer | Register 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). |
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.closePDF/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)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.closeinfo(title: …).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 H1–H6, and figure(..., alt) for described images.
taggeddoc = 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.closefigure() 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: "")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.closeattach_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)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.closeSee 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.
| Method | Description |
|---|---|
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. |
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.closeFill and flatten fields later with EditableDoc.
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.Hyperlinks free
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)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.closeRectangles 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)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.closeMetadata
info(title:, author:, subject:, keywords:, creator:)Sets the document information dictionary (and, for PDF/A, the matching XMP). Pass only the keywords you need.
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:)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.closeload 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
| Method | Description |
|---|---|
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) → EditableDoc | New document containing just those pages. |
page_count | Current page count. |
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.closereorder_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
| Method | Description |
|---|---|
set_info(key, value) | Set one info entry (e.g. "Title"). |
get_info(key) → String | Read 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) → Boolean | Fill an AcroForm text field; returns whether it was found. |
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.closeForm 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.
| Method | Description |
|---|---|
field_names → Array<String> | Fully-qualified names of every terminal field. |
fill_text_field(name, value) → Boolean | Set a text (or text-style choice) field; returns whether it matched. |
set_checkbox(name, checked = true) → Boolean | Check/uncheck a checkbox. |
set_radio(name, export_value) → Boolean | Select a radio button by its export value. |
set_choice(name, value) → Boolean | Set a dropdown / list-box value. |
flatten_forms | Bake all fields into static content and drop the /AcroForm. |
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.closeWatermarks 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)watermark_image_file(path, width, height, opacity: 0.30)ed = RustPdf::EditableDoc.load_file("report.pdf")
ed.watermark_text("CONFIDENTIAL", opacity: 0.25, rotation_deg: 45)
ed.save("stamped.pdf")
ed.closeRedaction 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) → Booleaned = 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.closeRustPdf.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)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.closeOptimize & compact
| Method | Description |
|---|---|
optimize | Drop unreferenced objects, Flate-compress uncompressed streams, dedupe identical objects. |
compact(on = true) | Pack objects into object streams + emit a cross-reference stream. |
ed = RustPdf::EditableDoc.load_file("big.pdf")
ed.optimize.compact(true)
ed.save("small.pdf")
ed.closeEncryption 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)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.closeSee 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
| Method | Description |
|---|---|
to_bytes → String | Serialize the manipulated document. |
save(path) | Serialize to a file. |
to_bytes_incremental(original) → String | Append only changes to the original bytes (signature-safe, non-destructive). |
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.closeDigital 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:) → Stringpdf = 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."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:) → StringRustPdf.timestamp(pdf, tsa_key_der, tsa_cert_der, date:) → Stringsigned = 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)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". 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 — it is not available on the free tier.
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']}"
endText & 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 freeRustPdf.extract_images_to_dir(data, dir) → Integer freedata = File.binread("report.pdf")
puts RustPdf.extract_text(data)
n = RustPdf.extract_images_to_dir(data, "out_images/") # returns how many were written
puts "wrote #{n} image(s)""à", 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.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 licensedRustPdf.page_count(pdf) → Integer freedata = 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
| Value | Level |
|---|---|
RustPdf::Pdfa::A1B | PDF/A-1b (basic, PDF 1.4) |
RustPdf::Pdfa::A2B | PDF/A-2b (basic): default of pdfa |
RustPdf::Pdfa::A2A | PDF/A-2a (accessible: pair with tagged) |
RustPdf::Pdfa::A3B | PDF/A-3b (basic, allows attachments) |
RustPdf::Pdfa::A3A | PDF/A-3a (accessible + attachments) |
RustPdf::Pdfa::A4 | PDF/A-4 (ISO 19005-4, based on PDF 2.0) |
RustPdf::Pdfa::A4E | PDF/A-4e (engineering) |
RustPdf::Pdfa::A4F | PDF/A-4f (requires at least one embedded file) |
Align
| Value | Meaning |
|---|---|
RustPdf::Align::LEFT | Left-aligned (default) |
RustPdf::Align::RIGHT | Right-aligned |
RustPdf::Align::CENTER | Centered |
RustPdf::Align::JUSTIFY | Justified (space distributed between words) |
Relationship
| Value | Meaning |
|---|---|
RustPdf::Relationship::SOURCE | Source data for the document (e.g. the invoice XML) |
RustPdf::Relationship::DATA | Data used to derive the visual content |
RustPdf::Relationship::ALTERNATIVE | Alternative representation |
RustPdf::Relationship::SUPPLEMENT | Supplementary material |
RustPdf::Relationship::UNSPECIFIED | Unspecified relationship |
Cipher
| Value | Cipher |
|---|---|
RustPdf::Cipher::RC4 | RC4 (legacy) |
RustPdf::Cipher::AES128 | AES-128 |
RustPdf::Cipher::AES256 | AES-256 (V5/R6): recommended |
FacturxProfile
| Value | Conformance level |
|---|---|
RustPdf::FacturxProfile::MINIMUM | Minimal header data only |
RustPdf::FacturxProfile::BASIC_WL | Basic, without line items |
RustPdf::FacturxProfile::BASIC | Basic, with line items |
RustPdf::FacturxProfile::EN16931 | EN 16931 (Comfort): the interoperable core, default |
RustPdf::FacturxProfile::EXTENDED | EN 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.
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".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
endUtilities
| Function | Description |
|---|---|
RustPdf.version → String | Native library version string. |
RustPdf.activate_license(token) | Activate a license token (raises on invalid/expired). |