https://github.com/whuppi/pdf_manipulator
A flutter plugin for doing various manipulations such as merge, split, compress and many more on PDF easily.
https://github.com/whuppi/pdf_manipulator
compress dart decrypt encrypt flutter images-to-pdf merge pdf reorder rotate split
Last synced: 9 days ago
JSON representation
A flutter plugin for doing various manipulations such as merge, split, compress and many more on PDF easily.
- Host: GitHub
- URL: https://github.com/whuppi/pdf_manipulator
- Owner: whuppi
- License: mit
- Created: 2022-09-24T14:17:59.000Z (over 3 years ago)
- Default Branch: dev
- Last Pushed: 2026-06-02T21:09:07.000Z (10 days ago)
- Last Synced: 2026-06-02T21:10:47.414Z (10 days ago)
- Topics: compress, dart, decrypt, encrypt, flutter, images-to-pdf, merge, pdf, reorder, rotate, split
- Language: Dart
- Homepage: https://pub.dev/packages/pdf_manipulator
- Size: 5.69 MB
- Stars: 15
- Watchers: 1
- Forks: 16
- Open Issues: 9
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# pdf_manipulator
Cross-platform PDF manipulation for Dart & Flutter. Merge, split, render, extract, search, sign, encrypt, validate, convert, build from scratch. Native and web. Off the main thread.
> **Coming from the old Android-only package?** See the [migration guide](docs/MIGRATION.md).
---
## Contents
- [Install](#install)
- [Quick start](#quick-start)
- [What you can do](#what-you-can-do)
- [Combine & split](#combine--split)
- [Read & query](#read--query)
- [Edit & transform](#edit--transform)
- [Security & signing](#security--signing)
- [Convert](#convert)
- [Create from scratch](#create-from-scratch)
- [Batch editing](#batch-editing)
- [Error handling](#error-handling)
- [Platforms](#platforms)
- [When NOT to use pdf_manipulator](#when-not-to-use-pdf_manipulator)
- [Docs](#docs)
---
## Install
```yaml
dependencies:
pdf_manipulator: ^1.0.0
```
**Web only** — run once after install (and after each package update):
```sh
dart run pdf_manipulator:setup
```
Native platforms need nothing extra — the build hook handles everything.
---
## Quick start
```dart
import 'package:pdf_manipulator/pdf_manipulator.dart';
final pdf = Pdf();
// Open and inspect
final doc = await pdf.open(source);
print('${doc.pageCount} pages, v${doc.version}');
final text = await doc.extract(pages: PdfPages.all());
await doc.dispose();
// One-shot operations
await pdf.merge([sourceA, sourceB], outputSink);
await pdf.watermark(source, sink, text: 'DRAFT');
pdf.dispose();
```
`source` is a `DataSource` — random-access byte source. The engine reads arbitrary offsets (xref at end of file, objects scattered throughout), so forward-only pipes (one-shot sockets, stdin) can't be used directly — buffer them first. `outputSink` is a `DataSink` — receives sequential chunks. Two interfaces, two methods each:
```dart
abstract interface class DataSource {
int get length;
FutureOr readAt(int offset, int count);
}
abstract interface class DataSink {
FutureOr write(Uint8List chunk);
}
```
Wrap whatever you have — `Uint8List` for memory, `RandomAccessFile` for disk, `Blob.slice` for web file pickers, HTTP Range requests for servers. The engine reads at most 64KB per `readAt` call, never the whole file. Constant memory regardless of file size.
Example implementations (memory, file, HTTP, web blob)
```dart
// Memory — tests, small files
class MemorySource implements DataSource {
MemorySource(this._data);
final Uint8List _data;
@override
int get length => _data.length;
@override
Uint8List readAt(int offset, int count) =>
Uint8List.sublistView(
_data, offset, (offset + count).clamp(0, _data.length));
}
class MemorySink implements DataSink {
final _buf = BytesBuilder(copy: false);
@override
void write(Uint8List chunk) => _buf.add(chunk);
Uint8List takeBytes() => _buf.takeBytes();
}
```
```dart
// File — mobile/desktop, constant memory for any size
class FileSource implements DataSource {
FileSource(this._file) : length = _file.lengthSync();
final File _file;
@override
final int length;
@override
Future readAt(int offset, int count) async {
final raf = await _file.open();
await raf.setPosition(offset);
final bytes = await raf.read(count);
await raf.close();
return bytes;
}
}
```
```dart
// HTTP — stream from server without downloading the whole file
class HttpSource implements DataSource {
HttpSource(this._url, this.length);
final Uri _url;
@override
final int length;
@override
Future readAt(int offset, int count) async {
final req = await HttpClient().getUrl(_url);
req.headers.set('Range', 'bytes=$offset-${offset + count - 1}');
final res = await req.close();
final builder = BytesBuilder();
await for (final chunk in res) {
builder.add(chunk);
}
return builder.takeBytes();
}
}
```
```dart
// Web Blob — browser file picker or drag-and-drop (package:web)
class BlobSource implements DataSource {
BlobSource(this._blob) : length = _blob.size;
final web.Blob _blob;
@override
final int length;
@override
Future readAt(int offset, int count) async {
final slice = _blob.slice(offset, offset + count);
final bytes = await slice.arrayBuffer().toDart;
return bytes.asUint8List();
}
}
```
---
## What you can do
### Combine & split
```dart
// Merge
await pdf.merge([sourceA, sourceB, sourceC], outputSink);
// Split every N pages
await pdf.split(source, (i) => MemorySink(), every: 5);
// Split by file size
await pdf.splitBySize(source, (i) => MemorySink(), maxBytes: 500000);
// Split at bookmark boundaries
await pdf.splitByBookmarks(source, (i) => MemorySink());
// Pick specific pages
await pdf.extractPages(source, sink, pages: [0, 2, 5]);
// Remove pages
await pdf.deletePages(source, sink, pages: [3]);
// Reorder
await pdf.reorderPages(source, sink, order: [4, 3, 2, 1, 0]);
// Move one page
await pdf.movePage(source, sink, from: 0, to: 4);
```
### Read & query
Open a PDF once, run any number of queries, dispose when done. All queries go through the `PdfDoc` handle:
```dart
final doc = await pdf.open(source);
print('${doc.pageCount} pages, v${doc.version}');
print('encrypted: ${doc.isEncrypted}, tagged: ${doc.isTagged}');
print('title: ${doc.title}, author: ${doc.author}');
```
**Extract text** — plain, markdown, or html:
```dart
final text = await doc.extract(pages: PdfPages.all());
final md = await doc.extract(
pages: PdfPages.single(0), format: PdfExtractionFormat.markdown);
final html = await doc.extract(
pages: PdfPages.single(0), format: PdfExtractionFormat.html);
```
**Search** with bounding rectangles:
```dart
final hits = await doc.search(query: 'revenue', pages: PdfPages.all());
for (final hit in hits) {
print('p${hit.page}: "${hit.text}" at (${hit.rect.x}, ${hit.rect.y})');
}
```
**Render** to images — streams one page at a time, constant memory:
```dart
await for (final page in doc.render(
pages: PdfPages.all(), size: PdfRenderSize.thumbnail(200))) {
// page.width, page.height, page.data (RGBA Uint8List)
}
```
**Extract embedded images:**
```dart
await for (final img in doc.extractImages(pages: PdfPages.single(0))) {
print('${img.width}×${img.height} ${img.format}');
}
```
**Validate, classify, inspect:**
```dart
// PDF/A and PDF/UA compliance
final pdfA = await doc.validatePdfA();
print('PDF/A: ${pdfA.compliant} (${pdfA.errors} errors, ${pdfA.warnings} warnings)');
final accessible = await doc.validatePdfUa();
// Auto-detect page/document type
final pageType = await doc.classifyPage(0);
final docType = await doc.classifyDocument();
// Digital signatures
final sigs = await doc.getSignatures();
final valid = await doc.verifySignatures();
// Bookmark structure
final segments = await doc.planSplitByBookmarks();
await doc.dispose();
```
### Edit & transform
```dart
// Rotate
await pdf.rotatePages(source, sink, pages: {0: 90, 2: 180});
await pdf.rotateAllPages(source, sink, degrees: 90);
// Watermark — centered (default), tiled, corner, or exact position
await pdf.watermark(source, sink,
text: 'CONFIDENTIAL',
style: PdfWatermarkStyle(opacity: 0.2, fontSize: 60, rotation: 45));
// Tiled watermark behind content
await pdf.watermark(source, sink,
text: 'DRAFT',
position: PdfWatermarkPosition.tiled(columns: 3, rows: 4),
layer: PdfWatermarkLayer.background);
// Corner watermark
await pdf.watermark(source, sink,
text: 'SAMPLE',
position: PdfWatermarkPosition.corner(PdfCorner.topRight));
// Stamps
await pdf.addStamp(source, sink,
page: 0, type: PdfStampType.approved,
rect: PdfRect(x: 100, y: 100, width: 200, height: 50));
await pdf.addImageStamp(source, sink,
page: 0, imageData: imageSource,
rect: PdfRect(x: 100, y: 100, width: 150, height: 150));
// Compress
await pdf.compress(source, sink, imageQuality: 75);
// Flatten forms / redactions
await pdf.flattenForms(source, sink);
await pdf.applyRedactions(source, sink);
// Embed file / erase regions
await pdf.embedFile(source, sink, name: 'data.csv', fileData: csvSource);
await pdf.eraseRegions(source, sink,
page: 0, regions: [PdfRect(x: 50, y: 700, width: 200, height: 30)]);
// Convert to PDF/A
await pdf.convertToPdfA(source, sink);
// Images to PDF
await pdf.imagesToPdf([img1, img2, img3], sink);
```
For multiple edits on the same PDF, use the [batch editor](#batch-editing) — parse once, mutate many, save once.
### Security & signing
```dart
// Encrypt
await pdf.encrypt(source, sink,
encryption: PdfEncryptionConfig(
ownerPassword: 'secret',
algorithm: PdfEncryptionAlgorithm.aes256,
permissions: PdfPermissions(copy: false, modify: false),
));
// Decrypt
await pdf.decrypt(source, sink, password: 'secret');
// Sign (PKCS#12)
await pdf.sign(source, sink,
credentials: PdfSigningCredentials.pkcs12(certBytes, 'cert-pw'),
reason: 'Approved', location: 'HQ');
// Sign (PEM)
await pdf.sign(source, sink,
credentials: PdfSigningCredentials.pem(certPem, keyPem));
```
### Convert
```dart
// PDF → Office
await pdf.convertTo(source, sink, format: PdfDocumentFormat.docx);
await pdf.convertTo(source, sink, format: PdfDocumentFormat.pptx);
await pdf.convertTo(source, sink, format: PdfDocumentFormat.xlsx);
// Office → PDF
await pdf.convertToPdf(docxSource, sink, format: PdfDocumentFormat.docx);
```
### Create from scratch
```dart
final builder = await pdf.build();
await builder.setTitle('Invoice #1042');
await builder.setAuthor('Acme Corp');
final page = await builder.addA4Page();
await page.heading(1, 'Invoice');
await page.paragraph('Thank you for your purchase.');
await page.space(20);
await page.textField('notes', PdfRect(x: 50, y: 400, width: 300, height: 100));
await page.checkbox('agree', PdfRect(x: 50, y: 370, width: 14, height: 14));
await page.linkUrl('https://example.com');
await page.footnote('1', 'Terms apply.');
await page.done();
await builder.save(sink);
await builder.dispose();
```
Text, headings, paragraphs, images, form fields (text, checkbox, combo box, push button, signature), links, footnotes, columns, watermarks — all from Dart. Page sizes: A4, Letter, or custom dimensions.
### Batch editing
When you need to do multiple things to the same PDF, open an editor. It parses the PDF once, applies all your mutations in memory, and writes once on save.
```dart
final editor = await pdf.edit(source);
await editor.setTitle('Q4 Report');
await editor.mergeFrom(appendixSource);
await editor.deletePage(4);
await editor.selectPages([0, 1, 2, 5, 6]);
await editor.addWatermark(0, 'FINAL', style: PdfWatermarkStyle(opacity: 0.15));
await editor.optimizeImages(quality: 70);
await editor.convertToPdfA();
await editor.save(sink, options: PdfSaveOptions.incremental());
await editor.dispose();
```
Save options:
- `PdfSaveOptions.fullRewrite()` — default. Recompresses, garbage-collects unused objects.
- `PdfSaveOptions.fullRewrite(encryption: PdfEncryption.config(...))` — encrypt on save.
- `PdfSaveOptions.fullRewrite(encryption: PdfEncryption.remove())` — strip encryption.
- `PdfSaveOptions.incremental()` — appends changes without rewriting. Faster, larger file.
Every operation from the sections above is also available on the editor: rotate, stamp, flatten, redact, crop, resize images, embed files, set form field values, scrub metadata, and more.
---
## Error handling
```dart
try {
await pdf.open(source);
} on PdfPasswordRequired {
// needs a password — retry with pdf.open(source, password: '...')
} on PdfCorrupted catch (e) {
print('Bad PDF: ${e.message}');
} on PdfIoError catch (e) {
print('I/O problem: ${e.message}');
}
```
Every error is a typed subclass of `PdfError`. No string matching. No `PlatformException`.
---
## Platforms
### Native
| Platform | Architectures |
|---|---|
| macOS | arm64, x64 |
| iOS | arm64, simulator (arm64, x64) |
| Android | arm64, arm, x86_64, x86 |
| Linux | x64, arm64 |
| Windows | x64, arm64 |
The PDF engine is compiled Rust. For consumers (installed via pub.dev), the build hook downloads a pre-built binary from GitHub Releases automatically — no Rust toolchain needed. For contributors (cloned with `--recursive`), it compiles from the vendored source.
### Web
Works out of the box on all modern browsers:
| Browser | Version | Released |
|---|---|---|
| Chrome / Edge | 102+ | May 2022 |
| Firefox | 111+ | Mar 2023 |
| Safari / Safari iOS | 15.2+ | Dec 2021 |
| Chrome Android | 102+ | May 2022 |
| Samsung Internet | 21+ | 2023 |
Run once after install (and after each package update):
```sh
dart run pdf_manipulator:setup
```
The engine compiles to WASM and runs in a Web Worker pool. Your UI thread never does PDF work.
Three I/O modes, auto-detected (best first):
| Mode | What it does | Requires |
|---|---|---|
| **JSPI** | True streaming via WebAssembly promise suspension | Chrome 137+ / Firefox 139+ |
| **Atomics** | True streaming via SharedArrayBuffer | COOP/COEP headers (see below) |
| **OPFS** | Pre-copies entire source to disk, then processes (O(N) latency + disk) | All modern browsers |
The package detects which mode is available and picks the best one automatically. No code changes needed. To force a specific mode:
```dart
final pdf = Pdf(config: PdfConfig(webIoMode: PdfIoMode.atomics));
```
To check which mode was selected (useful for detecting OPFS fallback):
```dart
final mode = await pdf.ensureInitialized();
if (mode == PdfIoMode.opfs) {
// OPFS mode: pre-copies each source to disk before processing.
// Slower first byte + uses disk quota vs streaming modes.
// To get streaming: deploy with COOP/COEP headers (Atomics)
// or target Chrome 137+ / Firefox 139+ (JSPI auto-detected).
}
```
Advanced: faster web with COOP/COEP headers (has trade-offs)
By default on browsers without JSPI support, the package copies your PDF to temporary disk storage (OPFS) before processing — works everywhere, no server config needed.
On Chrome 137+ and Firefox 139+, JSPI mode is auto-detected and gives true streaming without any server config. This is the best mode and requires no action from you.
For **older browsers** that have `SharedArrayBuffer` but not JSPI, adding two server headers enables Atomics mode — direct memory reads, no disk copy, lower latency:
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
**⚠️ These headers have side effects.** `require-corp` blocks loading ANY cross-origin resource (images, fonts, scripts, iframes) that doesn't explicitly opt in via `Cross-Origin-Resource-Policy` or CORS headers. Google Fonts, CDN images, analytics scripts, OAuth popups, embedded videos — all break unless their servers also send the right headers. Only add these if your app controls all its resource origins or you've tested thoroughly.
With these headers, browser support for streaming goes further back:
| Browser | Version | Released |
|---|---|---|
| Chrome / Edge | 68+ | Jul 2018 |
| Firefox | 79+ | Jul 2020 |
| Safari / Safari iOS | 15.2+ | Dec 2021 |
For development:
```sh
flutter run -d chrome --cross-origin-isolation
```
This adds the COOP/COEP headers to Flutter's dev server automatically.
---
## When NOT to use pdf_manipulator
- **You only need to display PDFs.** Use [`pdfx`](https://pub.dev/packages/pdfx) or [`flutter_pdfview`](https://pub.dev/packages/flutter_pdfview).
- **Server-side batch processing.** This package is for client-side use. For thousands of PDFs per second, use qpdf or poppler.
- **OCR.** This package extracts text already in the PDF. For scanned images, you need Tesseract or similar.
---
## Docs
| | |
|---|---|
| [Architecture](docs/ARCHITECTURE.md) | How it's built — layers, streaming I/O, three web modes |
| [Capabilities](docs/CAPABILITY_ROADMAP.md) | What's shipped, what's planned |
| [Updating](docs/UPDATING.md) | Maintaining the vendored Rust engine |
| [Migration](docs/MIGRATION.md) | Upgrading from the old Android-only version |
| [Contributing](CONTRIBUTING.md) | Setup, PR workflow, adding operations |
---
## License
MIT. See [LICENSE](LICENSE).