Swift

The RustPdf package wraps the rust-pdf core with idiomatic, handle-owning classes. It covers the whole product surface: vector graphics, embedded/subset fonts & Unicode text, paragraphs, images, PDF/A (1b–3a), tagged/accessible output, attachments, AcroForm fields, manipulation, text extraction, encryption and digital signatures. It ships as a SwiftPM package with a static xcframework, so it links into macOS and iOS apps with nothing to bundle alongside.

Two classes do almost everything. Document authors a new PDF; EditableDoc loads and manipulates an existing one. Both are reference types that own a native handle, released automatically in deinit. Stateless calls (version, licensing, extraction, signing) live on the Pdf enum. Every fallible call throws a PdfError.

Download

The binding ships as a SwiftPM package with a prebuilt, static RustPdfFFI.xcframework (macOS universal + iOS device + iOS simulator slices). Two artifacts are published per release: the self-contained package (sources + xcframework, consumed via a local path) and the standalone xcframework (consumed by URL). Basic PDF generation works immediately as a free trial; the corporate features unlock with a license token.

rustpdf-swift v0.1.0 macOS 11+ · iOS 13+ · static xcframework included · Swift 5.9+
Download package .zip SHA-256 checksum

Prefer the URL route? The standalone RustPdfFFI-0.1.0.xcframework.zip (.sha256) is referenced directly from a .binaryTarget(url:checksum:) (see Installation). Building from source? make swift-dist assembles the same artifacts.

Installation

The native library is linked statically inside the xcframework, so it works inside an iOS app bundle with no sidecar library and nothing to dlopen. Add the binding to your Package.swift one of two ways.

A. Local package. Unzip rustpdf-swift-0.1.0.zip and depend on it by path:

swift
// Package.swift
dependencies: [
    .package(path: "path/to/RustPdf")
],
targets: [
    .executableTarget(name: "MyApp", dependencies: [
        .product(name: "RustPdf", package: "RustPdf")
    ])
]

B. URL-hosted xcframework. Reference the published xcframework directly; SwiftPM downloads and verifies it:

swift
// In your Package.swift targets:
.binaryTarget(
    name: "CRustPdf",
    url: "https://rustpdf.dev/downloads/RustPdfFFI-0.1.0.xcframework.zip",
    checksum: "c801178fa37f15297c24e000f2de21631124099f5f599b443fb62024d7f93750"
),
.target(
    name: "RustPdf",
    dependencies: ["CRustPdf"],
    // the static Rust library links libiconv (its only non-system dependency)
    linkerSettings: [.linkedLibrary("iconv")]
)
In Xcode you can also add the package by its repository URL (File ▸ Add Package Dependencies…) and pin the swift-v0.1.0 tag.

Import it and verify the version:

swift
import RustPdf

print(Pdf.version)   // native library version, e.g. "0.1.0"

Quick start

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

swift
import RustPdf

let doc = try Document()            // A4 by default
try doc.addPage()
try doc.setFillRGB(0.86, 0.20, 0.18)
try doc.rect(x: 72, y: 640, width: 200, height: 120)   // points
try doc.fill()
try doc.save(to: "out.pdf")

Most mutators return self, so calls chain:

swift
let doc = try Document()
let font = try doc.addFont(path: "Roboto-Regular.ttf")
try doc.addPage()
    .setFillRGB(0.1, 0.1, 0.12).rect(x: 0, y: 800, width: 595, height: 42).fill()
    .showText(font: font, size: 24, x: 72, y: 740, "Olá, açúcar — café")
let data: [UInt8] = try doc.toBytes()   // in-memory bytes instead of a file
Binary payloads cross as Swift [UInt8]; the binding copies native out-buffers and frees them for you. Strings cross as UTF-8 automatically.

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 throw PdfError with status .license 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:

swift
try Pdf.activateLicense(token)   // throws PdfError(.license) 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

The core is Send but not Sync: you can build many documents in parallel, but a single handle must never be touched by two threads at once.

swift
DispatchQueue.concurrentPerform(iterations: 8) { i in
    do {
        let doc = try Document()            // one document per task
        try doc.addPage()
            .setFillRGB(0.1, 0.1, 0.12).rect(x: 72, y: 700, width: 200, height: 80).fill()
        try doc.save(to: "out-\(i).pdf")
    } catch { print("render \(i) failed:", error) }
}

Authoring: create & save

try Document() free

Creates an empty document (A4 default page size). The native handle is released automatically when the Document is deallocated.

MethodDescription
addPage()Append a page using the default size.
addPage(width:height:)Append a page with an explicit size in points.
setDefaultSize(width:height:)Default size for subsequently added pages.
setVersion(_:)PDF header version (.v14 / .v15 / .v17).
pageCountNumber of pages so far.
toBytes()Render the document to [UInt8].
save(to:)Render and write to a file.

Pages & vector graphics

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

MethodDescription
setFillRGB(_:_:_:)Fill color.
setStrokeRGB(_:_:_:)Stroke color.
setLineWidth(_:)Stroke width in points.
rect(x:y:width:height:)Add a rectangle subpath.
fill()Fill the current path with the fill color.
stroke()Stroke the current path with the stroke color.
swift
try doc.addPage()
try doc.setStrokeRGB(0.10, 0.45, 0.90).setLineWidth(3)
try doc.rect(x: 72, y: 600, width: 300, height: 160).stroke()
try doc.setFillRGB(0.95, 0.77, 0.06)
try doc.rect(x: 120, y: 640, width: 120, height: 80).fill()
try doc.save(to: "shapes.pdf")

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). Register a font once, then reference it by its integer id.

addFont(path:) -> Int32   addFont(data:) -> Int32
showText(font:size:x:y:_:headingLevel:)

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

swift
let regular = try doc.addFont(path: "Roboto-Regular.ttf")
try doc.addPage()
try doc.showText(font: regular, size: 28, x: 72, y: 760, "Invoice #1024")
try doc.showText(font: regular, size: 12, x: 72, y: 720, "日本語 · Ελληνικά · العربية")
try doc.save(to: "text.pdf")

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:)
swift
let intro = "A long paragraph that wraps to the given width and is justified " +
            "automatically; extra space is distributed between words."
let f = try doc.addFont(path: "Roboto-Regular.ttf")
try doc.addPage()
try doc.paragraph(font: f, size: 12, x: 72, y: 700, width: 451, text: intro, align: .justify)
try doc.save(to: "paragraph.pdf")

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
addImage(path:) -> Int32Load JPEG/PNG from a file; returns the image id.
addImagePNG(data:) -> Int32Register a PNG from memory.
addImageJPEG(data:) -> Int32Register a JPEG from memory.
drawImage(_:x:y:width:height:)Draw at (x, y) scaled to w × h points.
figure(_:x:y:width:height:alt:)Draw as a tagged /Figure with alt text (accessibility).
swift
let logo = try doc.addImage(path: "logo.png")
try doc.addPage()
try doc.drawImage(logo, x: 72, y: 680, width: 160, height: 90)
try doc.save(to: "with_image.pdf")

PDF/A licensed

Produce archival-grade output. pdfa() defaults to A-2b; pass a PdfaLevel for a specific level. 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.

pdfa()   pdfa(_ level: PdfaLevel)
swift
try doc.pdfa(.a2b).setInfo(DocumentInfo(title: "Q3 Report", author: "Acme Inc."))
let f = try doc.addFont(path: "Roboto-Regular.ttf")
try doc.addPage()
try doc.showText(font: f, size: 20, x: 72, y: 760, "Archival report")
try doc.save(to: "report_pdfa.pdf")   // throws without a license granting PDF/A
PDF/A requires the title to be set for valid metadata: pass a non-empty title in DocumentInfo.

Accessibility (Tagged PDF / PDF/UA) licensed

tagged() builds a logical structure tree (PDF/UA-1). Combine with pdfa(.a2a) for archival and accessible output. Use headingLevel on showText for H1H6, and figure(…, alt:) for described images.

tagged()
swift
try doc.pdfa(.a2a).tagged().setInfo(DocumentInfo(title: "Accessible report"))
let f = try doc.addFont(path: "Roboto-Regular.ttf")
try doc.addPage()
try doc.showText(font: f, size: 26, x: 72, y: 760, "Annual report", headingLevel: 1)   // H1
try doc.showText(font: f, size: 14, x: 72, y: 720, "Overview", headingLevel: 2)         // H2
try doc.showText(font: f, size: 11, x: 72, y: 690, "Body paragraph of the section…")
let chart = try doc.addImage(path: "chart.png")
try doc.figure(chart, x: 72, y: 520, width: 300, height: 150, alt: "Revenue grew 18% year over year")
try doc.save(to: "accessible.pdf")

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

attachFile(name:mime:data:relationship:description:)
swift
let xml: [UInt8] = Array(invoiceXmlData)
try doc.pdfa(.a3b).setInfo(DocumentInfo(title: "E-invoice 1024"))
let f = try doc.addFont(path: "Roboto-Regular.ttf")
try doc.addPage()
try doc.showText(font: f, size: 18, x: 72, y: 760, "Invoice 1024")
try doc.attachFile(name: "invoice.xml", mime: "text/xml", data: xml,
                   relationship: .source, description: "Structured invoice data")
try doc.save(to: "einvoice.pdf")

AcroForm fields

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

MethodDescription
textField(name:page:rect:value:size:)Text input (size: 0 → auto font size).
checkbox(name:page:rect:checked:)Checkbox.
dropdown(name:page:rect:options:selected:size:)Combo box from an array of strings.
radioGroup(name:page:buttons:selected:)buttons: [RadioButton], each with a rect and an export value.
swift
try doc.addPage()
try doc.textField(name: "applicant.name", page: 0, rect: (72, 700, 320, 720))
try doc.checkbox(name: "agree", page: 0, rect: (72, 660, 88, 676), checked: false)
try doc.dropdown(name: "plan", page: 0, rect: (72, 620, 240, 640),
                 options: ["Starter", "Pro", "Enterprise"], selected: 1)
try doc.radioGroup(name: "billing", page: 0, buttons: [
    RadioButton(rect: (72, 580, 88, 596),  export: "monthly"),
    RadioButton(rect: (140, 580, 156, 596), export: "annual"),
], selected: 1)
try doc.save(to: "form.pdf")

Fill fields later with EditableDoc.fillTextField.

Metadata

setInfo(_ info: DocumentInfo)

Sets the document information dictionary (and, for PDF/A, the matching XMP). Every field of DocumentInfo is optional.

swift
try doc.setInfo(DocumentInfo(
    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.

EditableDoc(loading: [UInt8])   EditableDoc(loading: [UInt8], password: String)
swift
let bytes = [UInt8](try Data(contentsOf: URL(fileURLWithPath: "in.pdf")))
let ed = try EditableDoc(loading: bytes)
print(ed.pageCount)

// encrypted input:
let secured = try EditableDoc(loading: encryptedBytes, password: "user-or-owner-pw")
try secured.save(to: "plain.pdf")

Pages: merge, split, reorder, rotate

MethodDescription
merge(_:)Append all pages of another EditableDoc (objects renumbered & remapped).
rotatePage(_:degrees:)Rotate one page (90 / 180 / 270).
deletePage(_:)Remove a page.
reorderPages(_:)Reorder with a full permutation array of indices.
extractPages(_:) -> EditableDocNew document containing just those pages.
pageCountCurrent page count.
swift
let a = try EditableDoc(loading: try read("a.pdf"))
let b = try EditableDoc(loading: try read("b.pdf"))
try a.merge(b)                  // a now has a's pages followed by b's
try a.rotatePage(0, degrees: 90)
try a.reorderPages([2, 1, 0])
try a.save(to: "merged.pdf")

Metadata, overlay & form fill

MethodDescription
setInfo(key:value:)Set one info entry (e.g. "Title").
getInfo(key:) -> StringRead an info entry.
setXMP(_:)Replace the XMP metadata stream.
overlayPage(_:content:)Overlay a content-stream fragment onto a page (stamps/watermarks).
fillTextField(name:value:) -> BoolFill an AcroForm text field; returns whether it was found.
swift
let ed = try EditableDoc(loading: try read("form.pdf"))
try ed.setInfo(key: "Title", value: "Filled form")
let found = try ed.fillTextField(name: "applicant.name", value: "Jane Doe")
print("filled:", found, "| title:", try ed.getInfo(key: "Title"))
try ed.save(to: "filled.pdf")

Optimize & compact

MethodDescription
optimize()Drop unreferenced objects, Flate-compress uncompressed streams, dedupe identical objects.
compact(_:)Pack objects into object streams + emit a cross-reference stream.
swift
let ed = try EditableDoc(loading: try read("big.pdf"))
try ed.optimize().compact(true)
try ed.save(to: "small.pdf")

Encryption licensed

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

encrypt(method:user:owner:readOnly:)
swift
let ed = try EditableDoc(loading: try read("in.pdf"))
try ed.encrypt(method: .aes256, user: "", owner: "owner-secret", readOnly: true)
try ed.save(to: "secured.pdf")     // throws without an Encryption license

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

Output & incremental update

MethodDescription
toBytes() -> [UInt8]Serialize the manipulated document.
save(to:)Serialize to a file.
toBytesIncremental(over:) -> [UInt8]Append only changes to the original bytes (signature-safe, non-destructive).
swift
let original = try read("in.pdf")
let ed = try EditableDoc(loading: original)
try ed.setInfo(key: "Subject", value: "reviewed")
let incremental = try ed.toBytesIncremental(over: original)   // original bytes preserved verbatim

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 [UInt8]. pades: true switches to PAdES-B-B.

Pdf.sign(pdf:keyDER:certDER:options:) -> [UInt8]
swift
let pdf  = try read("contract.pdf")
let key  = try read("signer.pk8")    // PKCS#8 private key (DER)
let cert = try read("signer.der")    // X.509 certificate (DER)

let signed = try Pdf.sign(pdf: pdf, keyDER: key, certDER: cert,
    options: SignOptions(reason: "Approved", location: "New York",
                         name: "Jane Doe", pades: true))
// Verify in a shell: pdfsig contract.signed.pdf  →  "Signature is Valid."

Timestamp & DSS (PAdES LTV) licensed

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

Pdf.addDss(pdf:certs:crls:) -> [UInt8]
Pdf.timestamp(pdf:tsaKeyDER:tsaCertDER:date:) -> [UInt8]
swift
// B-LT: embed validation material (caller supplies DER certs/CRLs)
let lt = try Pdf.addDss(pdf: signed, certs: [cert], crls: [crl])

// B-LTA: add a document timestamp signed by a TSA key/cert
let lta = try Pdf.timestamp(pdf: lt, tsaKeyDER: tsaKey, tsaCertDER: tsaCert)

Text extraction

Extract a document's text, mapping shown glyph codes back to Unicode through each font's ToUnicode map, with space/line inference.

Pdf.extractText(_ pdf: [UInt8]) -> String free
swift
let data = try read("report.pdf")
print(try Pdf.extractText(data))

Enums

PdfaLevel

ValueLevel
.a1bPDF/A-1b (basic, PDF 1.4)
.a2bPDF/A-2b (basic): default of pdfa()
.a2aPDF/A-2a (accessible: pair with tagged())
.a3bPDF/A-3b (basic, allows attachments)
.a3aPDF/A-3a (accessible + attachments)

Align

ValueMeaning
.leftLeft-aligned (default)
.rightRight-aligned
.centerCentered
.justifyJustified (space distributed between words)

AFRelationship

ValueMeaning
.sourceSource data for the document (e.g. the invoice XML)
.dataData used to derive the visual content
.alternativeAlternative representation
.supplementSupplementary material
.unspecifiedUnspecified relationship

Encryption

ValueCipher
.rc4RC4-128 (legacy)
.aes128AES-128
.aes256AES-256 (V5/R6): recommended

Error handling

Every failing native call throws PdfError, carrying the status (a PdfStatus) and the library's last-error message. License failures (missing/expired/forged token, or a feature the token doesn't grant) surface here too.

swift
do {
    try doc.pdfa().setInfo(DocumentInfo(title: "x"))
    try doc.addPage()
    try doc.save(to: "out.pdf")
} catch let e as PdfError {
    print("failed:", e.message, "(status \(e.status))")   // e.g. .license
}

Utilities

MemberDescription
Pdf.versionNative library version string.
Pdf.activateLicense(_:)Activate a license token (throws on invalid/expired).
Looking for another language? The same API exists in Python, C#, Go, PHP, Ruby, Node, Java and Delphi: browse all docs. They share one core, so behavior is identical.