dox_pdf_lib 0.0.3
dox_pdf_lib: ^0.0.3 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.
PDF/X and PDF/A #
PDF/X is for print. PDF/A is for long-term archives. One call,
setConformance, sets up a document for one of these standards:
| Standard | For | Transparency | Colors | Attachments |
|---|---|---|---|---|
| PDF/X-1a | no | CMYK, gray and spot only | do not use | |
| PDF/X-3 | no | must match the output profile | do not use | |
| PDF/X-4 | yes | must match the output profile | do not use | |
| PDF/A-1b | archive | no | must match the output profile | no |
| PDF/A-2b, PDF/A-2u | archive | yes | must match the output profile | PDF/A files only |
| PDF/A-3b, PDF/A-3u | archive | yes | must match the output profile | any file, with afRelationship |
The "u" levels also need Unicode text. Fonts from embedFont with a font
file have it.
"Must match the output profile" means: RGB colors (rgb(), JPEG and PNG
images in RGB) need an RGB profile. CMYK colors (cmyk(), CMYK JPEGs)
need a CMYK profile. Gray works with both.
PDF/A
import 'dart:io';
import 'dart:typed_data';
import 'package:dox_pdf_lib/dox_pdf_lib.dart';
Future<void> main() async {
// You bring the ICC profile. dox_pdf_lib ships none.
Uint8List srgb = File('sRGB2014.icc').readAsBytesSync();
PDFDocument doc = await PDFDocument.create();
doc.setTitle('Annual report');
doc.setAuthor('Jane Doe');
doc.setConformance(
PDFStandard.pdfA2b,
iccProfile: srgb,
outputConditionIdentifier: 'sRGB IEC61966-2.1',
);
// The 14 standard fonts (Helvetica, ...) are never embedded. Use a font file.
PDFFont font = await doc.embedFont(
File('NotoSans-Regular.ttf').readAsBytesSync(),
subset: true,
);
PDFPage page = doc.addPage();
page.drawText(
'Kept for a long time',
x: 50,
y: 750,
font: font,
color: rgb(0, 0, 0),
);
for (ConformanceIssue issue in doc.checkConformance(PDFStandard.pdfA2b)) {
print('${issue.kind.name}: $issue');
}
File('report.pdf').writeAsBytesSync(await doc.save());
}
For PDF/A-3, attach files with a relationship and a MIME type:
await doc.attach(
File('invoice.xml').readAsBytesSync(),
'invoice.xml',
mimeType: 'text/xml',
afRelationship: AFRelationship.data,
);
PDF/X
import 'dart:io';
import 'dart:typed_data';
import 'package:dox_pdf_lib/dox_pdf_lib.dart';
Future<void> main() async {
// The CMYK profile of your printer, eg. from the ECI (FOGRA39).
Uint8List coated = File('ISOcoated_v2_eci.icc').readAsBytesSync();
PDFDocument doc = await PDFDocument.create();
doc.setTitle('Flyer');
doc.setTrapped(Trapped.no);
doc.setConformance(
PDFStandard.pdfX1a,
iccProfile: coated,
outputConditionIdentifier: 'FOGRA39',
outputCondition: 'Offset printing, coated paper',
registryName: 'http://www.color.org',
);
PDFFont font = await doc.embedFont(
File('NotoSans-Regular.ttf').readAsBytesSync(),
subset: true,
);
PDFPage page = doc.addPage(<double>[612, 792]);
// PDF/X needs a TrimBox: the size of the page after cutting.
page.setTrimBox(9, 9, 594, 774);
// PDF/X-1a: CMYK only. The default text color is RGB, so set it.
page.drawText(
'Printed in CMYK',
x: 50,
y: 700,
font: font,
color: cmyk(0, 0, 0, 1),
);
for (ConformanceIssue issue in doc.checkConformance(PDFStandard.pdfX1a)) {
print('${issue.kind.name}: $issue');
}
File('flyer.pdf').writeAsBytesSync(await doc.save());
}
What setConformance does for you
Call setConformance again after PDFDocument.load, before you save,
also for a file that is already PDF/A or PDF/X. A plain save does not know
the standard: it changes the version, the object streams, the
modification date and the producer, so the file is not PDF/A or PDF/X
any more.
- Adds the output intent with your ICC profile. It checks the profile
header and throws a
PDFLibErrorwith a clear message when the profile is broken or does not fit (eg. an RGB profile for PDF/X-1a). - At save: the PDF version of the standard, a file
/ID, and XMP metadata made from the title, author, dates and other info (PDF/A needs them to say the same). PDF/X-1a, PDF/X-3 and PDF/A-1 are PDF 1.4, so save writes no object streams for them. - The XMP is made again at each save, from the document info. So when
you load a file, its old XMP is replaced, and extra fields in it (eg.
dc:rightsor your own namespaces) are lost. To keep them: readgetXmpMetadata(), add your fields tocreateXmpMetadata(standard: ...), and pass the result tosetXmpMetadata. Save then keeps your XMP as it is, so it must still match the document info. - PDF/X:
/Trappedis False when you did not set it, and the/GTS_PDFXVersionkey is set. - PDF/A: form field appearances keep only their normal look, and the
form has no
NeedAppearances.
What you must do
- Embed every font with a font file. The 14 standard fonts are never
embedded, so they never work. Form fields draw with Helvetica by
default: call
form.updateFieldAppearances(font)with your font. - PDF/X-1a: CMYK, gray and spot colors only. Pass
color: cmyk(...)to every draw call (the default color is RGB) and use CMYK JPEGs. - PDF/X-1a, PDF/X-3 and PDF/A-1: no transparency. Do not use
opacitybelow 1, blend modes, or PNG images with alpha. - PDF/X: give each page a TrimBox (
page.setTrimBox), and a title (setTitle). Keep form fields and other annotations outside the TrimBox. - Attachments: only in PDF/A-3, with
afRelationshipandmimeType. PDF/A-2 takes only PDF/A files. PDF/A-1 takes none. Do not attach files to print files (PDF/X). - Call
checkConformance(standard)before you save. It lists what is still wrong, with the page and a plain message. It is not a full validator.
Other calls
addOutputIntent(subtype: ..., iccProfile: ..., outputConditionIdentifier: ...): an output intent alone, without the other steps.setXmpMetadata(xml),getXmpMetadata(),createXmpMetadata(standard: ...): your own XMP. Save keeps it, also aftersetConformance.setCustomMetadata(key, value),getCustomMetadata(key),getCustomMetadataKeys(),removeCustomMetadata(key): your own document info keys. The standard keys (Title, ...) have their own setters and are not allowed here.setTrapped(Trapped.yes),getTrapped().
ICC profiles
dox_pdf_lib ships no ICC profile. You always pass your own.
- RGB (for PDF/A): sRGB from the ICC website, https://www.color.org/srgbprofiles.xalter.
- CMYK (for PDF/X): the profile of your print shop. Common ones, eg.
FOGRA39 (
ISOcoated_v2_eci.icc) or FOGRA51, come from the ECI, https://www.eci.org/en/downloads. - PDF/X-1a, PDF/X-3 and PDF/A-1 need a version 2 profile.
How we test it
- PDF/A: CI checks the files of our tests with veraPDF, the open source PDF/A validator, for PDF/A-1b, -2b, -2u, -3b and -3u. They pass.
- PDF/X: there is no free full validator. Our tests read the saved files
back and check every required item: output intent, version,
/ID, XMP, info keys, TrimBox, embedded fonts and no transparency. Check your final print files with a preflight tool, eg. Acrobat Preflight.
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.
Do you need iOS or Android? Give a 👍 or a comment on the issue for your platform:
When an issue has requests from 50 users, we add support for that platform.
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.