dox_pdf_lib 0.0.2
dox_pdf_lib: ^0.0.2 copied to clipboard
Create and edit PDF documents in Dart, with the API of the pdf-lib JavaScript library. A native Rust core does the work through dart:ffi.
dox_pdf_lib #
Create and edit PDF files in Dart.
dox_pdf_lib follows the API of the JavaScript library pdf-lib. Classes and methods have the same names. If you know pdf-lib, you already know this package. A small native core, written in Rust, does the work.
- Create new PDFs, or load and edit existing ones.
- Add, insert, remove, copy and merge pages.
- Draw text, lines, shapes, SVG paths, JPEG and PNG images.
- Use the 14 standard fonts, or embed your own TrueType and OpenType fonts. You can embed only the letters you use (a subset).
- Write text in many languages, including Thai, Myanmar, Khmer, Arabic, Hebrew and Hindi.
- Read, fill, create and flatten forms.
- Attach files, and read them back.
- Place pages of another PDF on a page (for templates).
- Set the title, author and other metadata.
It works in Dart programs on macOS, Linux and Windows. It does not work on the web. Android and iOS are not supported yet.
Install #
dart pub add dox_pdf_lib
You need Dart 3.13 or newer. A ready-built native library is downloaded for macOS, Linux and Windows, so you do not need Rust. See Platforms.
Quick start #
import 'dart:io';
import 'package:dox_pdf_lib/dox_pdf_lib.dart';
Future<void> main() async {
PDFDocument doc = await PDFDocument.create();
PDFFont font = await doc.embedFont(StandardFonts.helvetica);
PDFPage page = doc.addPage(PageSizes.A4);
page.drawText('Hello, PDF!', x: 50, y: 750, font: font, size: 30);
File('hello.pdf').writeAsBytesSync(await doc.save());
}
Guide #
Load, edit and save #
PDFDocument doc = await PDFDocument.load(File('in.pdf').readAsBytesSync());
print(doc.getPageCount());
PDFPage first = doc.getPage(0);
first.setRotation(degrees(90));
File('out.pdf').writeAsBytesSync(await doc.save());
save()returns the bytes of the file.saveAsBase64()returns a base64 string, or a data URI withdataUri: true.loadthrowsEncryptedPDFErrorfor an encrypted file. UseignoreEncryption: trueto load it anyway (see Differences).- Big files load and save on a native thread, so your app is not blocked.
- A document is freed when Dart's garbage collector removes it. Call
doc.dispose()to free it right away.
Pages #
PDFDocument doc = await PDFDocument.create();
doc.addPage(PageSizes.Letter);
doc.addPage(<double>[400, 600]); // width and height in points
doc.insertPage(0, PageSizes.A5);
doc.removePage(1);
PDFPage page = doc.getPage(0);
print(page.getSize()); // (height: 595.28, width: 419.53)
page.setSize(420, 600);
page.setCropBox(10, 10, 400, 580);
Merge two PDFs with copyPages:
PDFDocument a = await PDFDocument.load(File('a.pdf').readAsBytesSync());
PDFDocument b = await PDFDocument.load(File('b.pdf').readAsBytesSync());
PDFDocument merged = await PDFDocument.create();
for (PDFDocument source in <PDFDocument>[a, b]) {
List<PDFPage> pages = await merged.copyPages(source, source.getPageIndices());
for (PDFPage page in pages) {
merged.addPage(page);
}
}
File('merged.pdf').writeAsBytesSync(await merged.save());
Metadata #
doc.setTitle('Invoice 42');
doc.setAuthor('Jane Doe');
doc.setKeywords(<String>['invoice', 'paid']);
doc.setLanguage('en-US');
doc.setCreationDate(DateTime.now());
print(doc.getTitle());
Draw text and shapes #
PDFDocument doc = await PDFDocument.create();
PDFFont bold = await doc.embedFont(StandardFonts.helveticaBold);
PDFPage page = doc.addPage();
page.drawText(
'A long line of text that wraps inside 300 points.',
x: 50,
y: 700,
font: bold,
size: 14,
color: rgb(0.1, 0.3, 0.8),
maxWidth: 300,
lineHeight: 18,
);
page.drawLine(
start: (x: 50, y: 650),
end: (x: 350, y: 650),
thickness: 2,
color: grayscale(0.5),
);
page.drawRectangle(
x: 50,
y: 500,
width: 200,
height: 100,
color: rgb(1, 0.9, 0.9),
borderColor: rgb(1, 0, 0),
borderWidth: 2,
opacity: 0.8,
);
page.drawCircle(x: 350, y: 550, size: 40, color: cmyk(0.5, 0, 0, 0));
page.drawSvgPath('M 0,20 L 100,160 Q 130,200 150,120 Z', x: 50, y: 450);
Options are named parameters, with the same names and defaults as pdf-lib.
Measure text with font.widthOfTextAtSize(text, size) and font.heightAtSize(size).
Images #
PDFImage png = await doc.embedPng(File('logo.png').readAsBytesSync());
PDFImage jpg = await doc.embedJpg(File('photo.jpg').readAsBytesSync());
page.drawImage(png, x: 50, y: 700, width: 100, height: 50);
({double width, double height}) size = jpg.scaleToFit(200, 200);
page.drawImage(jpg, x: 50, y: 400, width: size.width, height: size.height);
PNG files with transparency, palettes, gray and 16-bit colors work. JPEG files go into the PDF as they are, without new compression.
Custom fonts and languages #
import 'dart:typed_data';
Uint8List bytes = File('NotoSansThai-Regular.ttf').readAsBytesSync();
PDFFont thai = await doc.embedFont(bytes, subset: true);
page.drawText('สวัสดีครับ', x: 50, y: 700, font: thai, size: 20);
embedFonttakes TrueType (.ttf) and OpenType (.otf) files. There is noregisterFontkit: the font code is built in.subset: trueputs only the letters you use into the file. Use it for big fonts, like Chinese, Japanese and Korean fonts. Without a subset, a 16 MB font makes a PDF of about 14 MB.- Use
subset: falsefor fonts in form fields that people will edit in a PDF viewer. A subset has only the letters known at save time, so the viewer cannot show new letters people type. featuresturns OpenType features on or off, for examplefeatures: {'liga': false}.- A font file is read once and shared by all documents that use the same bytes.
For Latin, Chinese, Japanese, Korean and other simple scripts, text widths and heights are the same as in pdf-lib with fontkit. So text is measured and wrapped the same way. Complex scripts, like Thai, Arabic and Hindi, are laid out like HarfBuzz, the standard text engine. Copy and paste from the PDF gives back the text. Right-to-left text with numbers or English words follows the Unicode rules, like a web browser.
These languages are tested, with full and subset fonts: English, German, Vietnamese, Japanese, Chinese, Korean, Thai, Myanmar, Khmer, Arabic, Hebrew and Hindi.
Word breaks in Thai, Lao, Khmer and Myanmar #
These scripts have no spaces between words. To wrap such text yourself, you need the places where a word ends. dictionaryWordBreaks gives them. It is an extra: pdf-lib does not have it.
List<int> breaks = dictionaryWordBreaks('ทุกสองสัปดาห์'); // [3, 6]
- The result is a sorted list of indexes into the string (UTF-16, like
String.substring). A word starts at each index. - The words come from the ICU word lists, with the same rules as ICU. So the breaks are the same as
Intl.Segmenterwithgranularity: 'word'in Node.js 24 and in browsers. - Each run of Thai, Lao, Khmer or Myanmar letters is cut into words. The start and the end of a run are not in the list. Other text (spaces, numbers, Latin, Chinese, ...) gives no breaks.
- The word lists make the native library about 1 MB bigger. They load the first time you call the function.
Forms #
Fill a form:
PDFDocument doc = await PDFDocument.load(File('form.pdf').readAsBytesSync());
PDFForm form = doc.getForm();
form.getTextField('person.name').setText('Jane Doe');
form.getCheckBox('agree').check();
form.getRadioGroup('color').select('green');
form.getDropdown('country').select('Myanmar');
form.flatten(); // optional: draw the fields into the pages
File('filled.pdf').writeAsBytesSync(await doc.save());
Create a form:
PDFDocument doc = await PDFDocument.create();
PDFPage page = doc.addPage();
PDFForm form = doc.getForm();
PDFTextField name = form.createTextField('name');
name.setText('Jane');
name.addToPage(page, x: 50, y: 700, width: 200, height: 24);
PDFCheckBox agree = form.createCheckBox('agree');
agree.addToPage(page, x: 50, y: 650, width: 16, height: 16);
Text fields, check boxes, radio groups, dropdowns, option lists, buttons and signatures all work, with the pdf-lib methods.
save() updates the look of changed fields, like pdf-lib.
To use a custom font in fields, call form.updateFieldAppearances(font), or pass font: to addToPage.
Attachments #
await doc.attach(
File('data.csv').readAsBytesSync(),
'data.csv',
mimeType: 'text/csv',
description: 'The raw data',
);
for (PDFAttachment file in doc.getAttachments()) {
print('${file.name}: ${file.data.length} bytes');
}
attach also takes a base64 string or a data URI.
Place pages of another PDF #
Use another PDF as a background or template:
PDFDocument out = await PDFDocument.create();
List<PDFEmbeddedPage> pages = await out.embedPdf(
File('template.pdf').readAsBytesSync(),
);
PDFEmbeddedPage template = pages.first;
PDFPage page = out.addPage(<double>[template.width, template.height]);
page.drawPage(template);
page.drawText('Filled in on top', x: 50, y: 50);
embedPdftakes bytes, a base64 string or a loadedPDFDocument.embedPageandembedPagestake pages.- By default
embedPdfembeds only the first page, like pdf-lib. Pass page indices for more, for exampleembedPdf(bytes, [0, 1]). drawPagetakesx,y,xScale,yScale,width,height,rotate,xSkew,ySkew,opacityandblendMode.- To make many files from one template, load the template once and embed its pages into each new document. Shared parts, like fonts, are copied only once per document.
Coming from pdf-lib #
The names are the same. These things change because this is Dart:
| pdf-lib (JavaScript) | dox_pdf_lib (Dart) |
|---|---|
page.drawText('Hi', { x: 50, y: 50 }) |
page.drawText('Hi', x: 50, y: 50) |
StandardFonts.TimesRoman |
StandardFonts.timesRoman |
BlendMode.Multiply, LineCapStyle.Round |
BlendMode.multiply, LineCapStyle.round |
PageSizes['4A0'] |
PageSizes.$4A0 |
page.drawLine({ start: { x: 0, y: 0 }, ... }) |
page.drawLine(start: (x: 0, y: 0), ...) |
page.getSize() gives { width, height } |
page.getSize() gives a record: .width, .height |
page.pushOperators(a, b, c) |
page.pushOperators([a, b, c]) |
doc.registerFontkit(fontkit) |
not needed |
field.ref is a PDFRef |
field.ref is an int |
Errors have the pdf-lib names, for example EncryptedPDFError, NoSuchFieldError and ExceededMaxLengthError.
They all extend PDFLibError.
Wrong arguments throw Dart's ArgumentError or RangeError.
Differences from pdf-lib #
This list compares with pdf-lib 1.17.1. Most of these are fixes or small extras. Most pdf-lib code works the same after you change it to Dart.
Documents #
- The default producer is
dox_pdf_lib (...), notpdf-lib (...).getLanguage()is an extra. - Sizes and boxes keep about 7 digits:
595.28stays595.28,123456.789becomes123456.79. - A page with no size at all (no MediaBox) reads as US Letter. pdf-lib throws an error.
- NaN and infinity in page sizes and boxes throw an
ArgumentError. - There is no low-level object API: no
doc.context,doc.catalog,PDFDictorPDFName, and nodoc.copy(). - A bad page index in
copyPages, and a file that has only a header, throw clear errors. pdf-lib fails with aTypeError. load(..., ignoreEncryption: true):- A file with only an owner password (it opens with no password) is decrypted.
isEncryptedstays true.save()writes the file without encryption. - A file with a user password throws
PDFParsingError. pdf-lib still lists its pages, but they cannot be read.
- A file with only an owner password (it opens with no password) is decrypted.
Drawing #
- A font, image or opacity setting is added once per page, not once per draw call. Their names are
Helvetica-1,Image-1,GS-1, without a random number. - Pages with no font set share one Helvetica font per document. pdf-lib adds one per page.
- An SVG path with too few numbers (for example
M 1 2 L) throws an error. pdf-lib draws nothing or writes broken numbers for some of them. - A font or image of another document throws an
ArgumentErrorindrawText,drawImageandsetFont.
Fonts #
- Complex scripts (Thai, Lao, Myanmar, Khmer, Indic, Arabic, Hebrew and others) are laid out like HarfBuzz, not like fontkit. fontkit draws some of them wrong or cannot lay them out at all. Their widths are HarfBuzz widths.
- Right-to-left text with numbers or English words follows the Unicode bidi rules. pdf-lib draws numbers in reversed order.
- With
wordBreaks: [''], a line never breaks between a letter and its marks. - pdf-lib gives some glyphs no width, so viewers draw them too wide. Here every glyph gets its real width. Copy and paste works for them too.
- A full OpenType (CFF) font is embedded as OpenType. pdf-lib embeds it as TrueType.
- Font collections (
.ttc), WOFF files and variable OpenType (CFF2) fonts are not supported. A variable TrueType font uses its default style, like fontkit.
Forms #
- A field with no
/DAtakes it from its parent field or the form, as the PDF standard says. removeFieldandflattenalso remove the widgets from their pages. pdf-lib leaves broken references behind.flattenskips a broken widget (no page or no look). pdf-lib throws.- Some options have a short export value and a longer display text, for example
CAandCanada.selecttakes either one and saves the export value.getSelectedreturns the display text. pdf-lib saves the display text. - A combed text field with more than 1000 cells is drawn as one normal line.
- There are no custom appearance providers.
acroFieldis a small read-only view. getForm()removes XFA data like pdf-lib, but prints no warning.
Attachments and placed pages #
getAttachments()is an extra: pdf-lib cannot read attachments.- New attachment names are kept in sorted order, as the PDF standard asks. pdf-lib adds them at the end.
- Pages of another document are embedded right away. Embedding the same page twice gives the same result, so it is stored once. Pages of the same document are embedded at save, like pdf-lib.
- If you change a source document after you embed its pages, the next embed sees the change.
Platforms #
dox_pdf_lib works in Dart programs on macOS, Linux and Windows.
It uses dart:ffi, so it does not work on the web.
Android and iOS are not supported yet.
Ready-built native libraries exist for:
| System | CPU |
|---|---|
| macOS | arm64, x64 |
| Linux | x64, arm64 |
| Windows | x64 |
The Linux libraries are built on Ubuntu 22.04. They need glibc 2.35 or newer, for example Ubuntu 22.04 or Debian 12. On older Linux, or on Alpine (musl), build the library with cargo and set lib_path (see below).
On another desktop system, install Rust. The library is then built from source.
Cross builds (building on one system for another) work only with a ready-built library. cargo and native/target/release are used only when you build for the system you are on.
How the native library is found #
A Dart build hook finds the library. It tries these steps in order:
- A path you set in your app's
pubspec.yaml:
Use the file name of your system:hooks: user_defines: dox_pdf_lib: lib_path: /path/to/libdox_pdf_lib.so.dylibon macOS,.soon Linux,.dllon Windows. If the file does not exist, the build fails. - From pub.dev: the ready-built library from the GitHub release, checked with SHA-256.
- A build from source with
cargo, when Rust is installed. - A library that is already in
native/target/release, when Rust is not installed. - As a path or git dependency: the ready-built library from the GitHub release.
As a path or git dependency, cargo comes first, because the source may be newer than the release.
If no step works, the build still passes.
Every PDF call then throws an ArgumentError that starts with Couldn't resolve native function.
Thanks #
The API comes from pdf-lib by Andrew Dillon. The native core uses lopdf, harfrust, read-fonts, subsetter, png, flate2, unicode-bidi and icu_segmenter (its word lists of Thai, Lao, Khmer and Myanmar). The word break rules are a port of the ICU4C dictionary break engines (Unicode License V3).
The native library contains code and data of these and other Rust crates. Their licenses and copyright notices are in THIRD_PARTY_NOTICES. It is also attached to each GitHub release, next to the libraries.
License #
MIT. See LICENSE.