{"id":50517464,"url":"https://github.com/whuppi/pdf_manipulator","last_synced_at":"2026-06-03T01:01:35.115Z","repository":{"id":62459006,"uuid":"540853026","full_name":"whuppi/pdf_manipulator","owner":"whuppi","description":"A flutter plugin for doing various manipulations such as merge, split, compress and many more on PDF easily.","archived":false,"fork":false,"pushed_at":"2026-06-02T21:09:07.000Z","size":5962,"stargazers_count":15,"open_issues_count":9,"forks_count":16,"subscribers_count":1,"default_branch":"dev","last_synced_at":"2026-06-02T21:10:47.414Z","etag":null,"topics":["compress","dart","decrypt","encrypt","flutter","images-to-pdf","merge","pdf","reorder","rotate","split"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/pdf_manipulator","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/whuppi.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2022-09-24T14:17:59.000Z","updated_at":"2026-05-21T06:20:38.000Z","dependencies_parsed_at":"2023-02-05T14:16:14.569Z","dependency_job_id":null,"html_url":"https://github.com/whuppi/pdf_manipulator","commit_stats":null,"previous_names":["whuppi/pdf_manipulator"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/whuppi/pdf_manipulator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whuppi%2Fpdf_manipulator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whuppi%2Fpdf_manipulator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whuppi%2Fpdf_manipulator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whuppi%2Fpdf_manipulator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/whuppi","download_url":"https://codeload.github.com/whuppi/pdf_manipulator/tar.gz/refs/heads/dev","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whuppi%2Fpdf_manipulator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33843611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["compress","dart","decrypt","encrypt","flutter","images-to-pdf","merge","pdf","reorder","rotate","split"],"created_at":"2026-06-03T01:01:34.049Z","updated_at":"2026-06-03T01:01:35.097Z","avatar_url":"https://github.com/whuppi.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pdf_manipulator\n\nCross-platform PDF manipulation for Dart \u0026 Flutter. Merge, split, render, extract, search, sign, encrypt, validate, convert, build from scratch. Native and web. Off the main thread.\n\n\u003e **Coming from the old Android-only package?** See the [migration guide](docs/MIGRATION.md).\n\n---\n\n## Contents\n\n- [Install](#install)\n- [Quick start](#quick-start)\n- [What you can do](#what-you-can-do)\n  - [Combine \u0026 split](#combine--split)\n  - [Read \u0026 query](#read--query)\n  - [Edit \u0026 transform](#edit--transform)\n  - [Security \u0026 signing](#security--signing)\n  - [Convert](#convert)\n  - [Create from scratch](#create-from-scratch)\n  - [Batch editing](#batch-editing)\n- [Error handling](#error-handling)\n- [Platforms](#platforms)\n- [When NOT to use pdf_manipulator](#when-not-to-use-pdf_manipulator)\n- [Docs](#docs)\n\n---\n\n## Install\n\n```yaml\ndependencies:\n  pdf_manipulator: ^1.0.0\n```\n\n**Web only** — run once after install (and after each package update):\n\n```sh\ndart run pdf_manipulator:setup\n```\n\nNative platforms need nothing extra — the build hook handles everything.\n\n---\n\n## Quick start\n\n```dart\nimport 'package:pdf_manipulator/pdf_manipulator.dart';\n\nfinal pdf = Pdf();\n\n// Open and inspect\nfinal doc = await pdf.open(source);\nprint('${doc.pageCount} pages, v${doc.version}');\nfinal text = await doc.extract(pages: PdfPages.all());\nawait doc.dispose();\n\n// One-shot operations\nawait pdf.merge([sourceA, sourceB], outputSink);\nawait pdf.watermark(source, sink, text: 'DRAFT');\n\npdf.dispose();\n```\n\n`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:\n\n```dart\nabstract interface class DataSource {\n  int get length;\n  FutureOr\u003cUint8List\u003e readAt(int offset, int count);\n}\n\nabstract interface class DataSink {\n  FutureOr\u003cvoid\u003e write(Uint8List chunk);\n}\n```\n\nWrap 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.\n\n\u003cdetails\u003e\n\u003csummary\u003eExample implementations (memory, file, HTTP, web blob)\u003c/summary\u003e\n\n```dart\n// Memory — tests, small files\nclass MemorySource implements DataSource {\n  MemorySource(this._data);\n\n  final Uint8List _data;\n\n  @override\n  int get length =\u003e _data.length;\n\n  @override\n  Uint8List readAt(int offset, int count) =\u003e\n      Uint8List.sublistView(\n        _data, offset, (offset + count).clamp(0, _data.length));\n}\n\nclass MemorySink implements DataSink {\n  final _buf = BytesBuilder(copy: false);\n\n  @override\n  void write(Uint8List chunk) =\u003e _buf.add(chunk);\n\n  Uint8List takeBytes() =\u003e _buf.takeBytes();\n}\n```\n\n```dart\n// File — mobile/desktop, constant memory for any size\nclass FileSource implements DataSource {\n  FileSource(this._file) : length = _file.lengthSync();\n\n  final File _file;\n\n  @override\n  final int length;\n\n  @override\n  Future\u003cUint8List\u003e readAt(int offset, int count) async {\n    final raf = await _file.open();\n    await raf.setPosition(offset);\n    final bytes = await raf.read(count);\n    await raf.close();\n    return bytes;\n  }\n}\n```\n\n```dart\n// HTTP — stream from server without downloading the whole file\nclass HttpSource implements DataSource {\n  HttpSource(this._url, this.length);\n\n  final Uri _url;\n\n  @override\n  final int length;\n\n  @override\n  Future\u003cUint8List\u003e readAt(int offset, int count) async {\n    final req = await HttpClient().getUrl(_url);\n    req.headers.set('Range', 'bytes=$offset-${offset + count - 1}');\n    final res = await req.close();\n    final builder = BytesBuilder();\n    await for (final chunk in res) {\n      builder.add(chunk);\n    }\n    return builder.takeBytes();\n  }\n}\n```\n\n```dart\n// Web Blob — browser file picker or drag-and-drop (package:web)\nclass BlobSource implements DataSource {\n  BlobSource(this._blob) : length = _blob.size;\n\n  final web.Blob _blob;\n\n  @override\n  final int length;\n\n  @override\n  Future\u003cUint8List\u003e readAt(int offset, int count) async {\n    final slice = _blob.slice(offset, offset + count);\n    final bytes = await slice.arrayBuffer().toDart;\n    return bytes.asUint8List();\n  }\n}\n```\n\n\u003c/details\u003e\n\n---\n\n## What you can do\n\n### Combine \u0026 split\n\n```dart\n// Merge\nawait pdf.merge([sourceA, sourceB, sourceC], outputSink);\n\n// Split every N pages\nawait pdf.split(source, (i) =\u003e MemorySink(), every: 5);\n\n// Split by file size\nawait pdf.splitBySize(source, (i) =\u003e MemorySink(), maxBytes: 500000);\n\n// Split at bookmark boundaries\nawait pdf.splitByBookmarks(source, (i) =\u003e MemorySink());\n\n// Pick specific pages\nawait pdf.extractPages(source, sink, pages: [0, 2, 5]);\n\n// Remove pages\nawait pdf.deletePages(source, sink, pages: [3]);\n\n// Reorder\nawait pdf.reorderPages(source, sink, order: [4, 3, 2, 1, 0]);\n\n// Move one page\nawait pdf.movePage(source, sink, from: 0, to: 4);\n```\n\n### Read \u0026 query\n\nOpen a PDF once, run any number of queries, dispose when done. All queries go through the `PdfDoc` handle:\n\n```dart\nfinal doc = await pdf.open(source);\nprint('${doc.pageCount} pages, v${doc.version}');\nprint('encrypted: ${doc.isEncrypted}, tagged: ${doc.isTagged}');\nprint('title: ${doc.title}, author: ${doc.author}');\n```\n\n**Extract text** — plain, markdown, or html:\n\n```dart\nfinal text = await doc.extract(pages: PdfPages.all());\nfinal md = await doc.extract(\n    pages: PdfPages.single(0), format: PdfExtractionFormat.markdown);\nfinal html = await doc.extract(\n    pages: PdfPages.single(0), format: PdfExtractionFormat.html);\n```\n\n**Search** with bounding rectangles:\n\n```dart\nfinal hits = await doc.search(query: 'revenue', pages: PdfPages.all());\nfor (final hit in hits) {\n  print('p${hit.page}: \"${hit.text}\" at (${hit.rect.x}, ${hit.rect.y})');\n}\n```\n\n**Render** to images — streams one page at a time, constant memory:\n\n```dart\nawait for (final page in doc.render(\n    pages: PdfPages.all(), size: PdfRenderSize.thumbnail(200))) {\n  // page.width, page.height, page.data (RGBA Uint8List)\n}\n```\n\n**Extract embedded images:**\n\n```dart\nawait for (final img in doc.extractImages(pages: PdfPages.single(0))) {\n  print('${img.width}×${img.height} ${img.format}');\n}\n```\n\n**Validate, classify, inspect:**\n\n```dart\n// PDF/A and PDF/UA compliance\nfinal pdfA = await doc.validatePdfA();\nprint('PDF/A: ${pdfA.compliant} (${pdfA.errors} errors, ${pdfA.warnings} warnings)');\nfinal accessible = await doc.validatePdfUa();\n\n// Auto-detect page/document type\nfinal pageType = await doc.classifyPage(0);\nfinal docType = await doc.classifyDocument();\n\n// Digital signatures\nfinal sigs = await doc.getSignatures();\nfinal valid = await doc.verifySignatures();\n\n// Bookmark structure\nfinal segments = await doc.planSplitByBookmarks();\n\nawait doc.dispose();\n```\n\n### Edit \u0026 transform\n\n```dart\n// Rotate\nawait pdf.rotatePages(source, sink, pages: {0: 90, 2: 180});\nawait pdf.rotateAllPages(source, sink, degrees: 90);\n\n// Watermark — centered (default), tiled, corner, or exact position\nawait pdf.watermark(source, sink,\n    text: 'CONFIDENTIAL',\n    style: PdfWatermarkStyle(opacity: 0.2, fontSize: 60, rotation: 45));\n\n// Tiled watermark behind content\nawait pdf.watermark(source, sink,\n    text: 'DRAFT',\n    position: PdfWatermarkPosition.tiled(columns: 3, rows: 4),\n    layer: PdfWatermarkLayer.background);\n\n// Corner watermark\nawait pdf.watermark(source, sink,\n    text: 'SAMPLE',\n    position: PdfWatermarkPosition.corner(PdfCorner.topRight));\n\n// Stamps\nawait pdf.addStamp(source, sink,\n    page: 0, type: PdfStampType.approved,\n    rect: PdfRect(x: 100, y: 100, width: 200, height: 50));\nawait pdf.addImageStamp(source, sink,\n    page: 0, imageData: imageSource,\n    rect: PdfRect(x: 100, y: 100, width: 150, height: 150));\n\n// Compress\nawait pdf.compress(source, sink, imageQuality: 75);\n\n// Flatten forms / redactions\nawait pdf.flattenForms(source, sink);\nawait pdf.applyRedactions(source, sink);\n\n// Embed file / erase regions\nawait pdf.embedFile(source, sink, name: 'data.csv', fileData: csvSource);\nawait pdf.eraseRegions(source, sink,\n    page: 0, regions: [PdfRect(x: 50, y: 700, width: 200, height: 30)]);\n\n// Convert to PDF/A\nawait pdf.convertToPdfA(source, sink);\n\n// Images to PDF\nawait pdf.imagesToPdf([img1, img2, img3], sink);\n```\n\nFor multiple edits on the same PDF, use the [batch editor](#batch-editing) — parse once, mutate many, save once.\n\n### Security \u0026 signing\n\n```dart\n// Encrypt\nawait pdf.encrypt(source, sink,\n    encryption: PdfEncryptionConfig(\n      ownerPassword: 'secret',\n      algorithm: PdfEncryptionAlgorithm.aes256,\n      permissions: PdfPermissions(copy: false, modify: false),\n    ));\n\n// Decrypt\nawait pdf.decrypt(source, sink, password: 'secret');\n\n// Sign (PKCS#12)\nawait pdf.sign(source, sink,\n    credentials: PdfSigningCredentials.pkcs12(certBytes, 'cert-pw'),\n    reason: 'Approved', location: 'HQ');\n\n// Sign (PEM)\nawait pdf.sign(source, sink,\n    credentials: PdfSigningCredentials.pem(certPem, keyPem));\n```\n\n### Convert\n\n```dart\n// PDF → Office\nawait pdf.convertTo(source, sink, format: PdfDocumentFormat.docx);\nawait pdf.convertTo(source, sink, format: PdfDocumentFormat.pptx);\nawait pdf.convertTo(source, sink, format: PdfDocumentFormat.xlsx);\n\n// Office → PDF\nawait pdf.convertToPdf(docxSource, sink, format: PdfDocumentFormat.docx);\n```\n\n### Create from scratch\n\n```dart\nfinal builder = await pdf.build();\nawait builder.setTitle('Invoice #1042');\nawait builder.setAuthor('Acme Corp');\n\nfinal page = await builder.addA4Page();\nawait page.heading(1, 'Invoice');\nawait page.paragraph('Thank you for your purchase.');\nawait page.space(20);\nawait page.textField('notes', PdfRect(x: 50, y: 400, width: 300, height: 100));\nawait page.checkbox('agree', PdfRect(x: 50, y: 370, width: 14, height: 14));\nawait page.linkUrl('https://example.com');\nawait page.footnote('1', 'Terms apply.');\nawait page.done();\n\nawait builder.save(sink);\nawait builder.dispose();\n```\n\nText, 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.\n\n### Batch editing\n\nWhen 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.\n\n```dart\nfinal editor = await pdf.edit(source);\n\nawait editor.setTitle('Q4 Report');\nawait editor.mergeFrom(appendixSource);\nawait editor.deletePage(4);\nawait editor.selectPages([0, 1, 2, 5, 6]);\nawait editor.addWatermark(0, 'FINAL', style: PdfWatermarkStyle(opacity: 0.15));\nawait editor.optimizeImages(quality: 70);\nawait editor.convertToPdfA();\n\nawait editor.save(sink, options: PdfSaveOptions.incremental());\nawait editor.dispose();\n```\n\nSave options:\n- `PdfSaveOptions.fullRewrite()` — default. Recompresses, garbage-collects unused objects.\n- `PdfSaveOptions.fullRewrite(encryption: PdfEncryption.config(...))` — encrypt on save.\n- `PdfSaveOptions.fullRewrite(encryption: PdfEncryption.remove())` — strip encryption.\n- `PdfSaveOptions.incremental()` — appends changes without rewriting. Faster, larger file.\n\nEvery 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.\n\n---\n\n## Error handling\n\n```dart\ntry {\n  await pdf.open(source);\n} on PdfPasswordRequired {\n  // needs a password — retry with pdf.open(source, password: '...')\n} on PdfCorrupted catch (e) {\n  print('Bad PDF: ${e.message}');\n} on PdfIoError catch (e) {\n  print('I/O problem: ${e.message}');\n}\n```\n\nEvery error is a typed subclass of `PdfError`. No string matching. No `PlatformException`.\n\n---\n\n## Platforms\n\n### Native\n\n| Platform | Architectures |\n|---|---|\n| macOS | arm64, x64 |\n| iOS | arm64, simulator (arm64, x64) |\n| Android | arm64, arm, x86_64, x86 |\n| Linux | x64, arm64 |\n| Windows | x64, arm64 |\n\nThe 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.\n\n### Web\n\nWorks out of the box on all modern browsers:\n\n| Browser | Version | Released |\n|---|---|---|\n| Chrome / Edge | 102+ | May 2022 |\n| Firefox | 111+ | Mar 2023 |\n| Safari / Safari iOS | 15.2+ | Dec 2021 |\n| Chrome Android | 102+ | May 2022 |\n| Samsung Internet | 21+ | 2023 |\n\nRun once after install (and after each package update):\n\n```sh\ndart run pdf_manipulator:setup\n```\n\nThe engine compiles to WASM and runs in a Web Worker pool. Your UI thread never does PDF work.\n\nThree I/O modes, auto-detected (best first):\n\n| Mode | What it does | Requires |\n|---|---|---|\n| **JSPI** | True streaming via WebAssembly promise suspension | Chrome 137+ / Firefox 139+ |\n| **Atomics** | True streaming via SharedArrayBuffer | COOP/COEP headers (see below) |\n| **OPFS** | Pre-copies entire source to disk, then processes (O(N) latency + disk) | All modern browsers |\n\nThe package detects which mode is available and picks the best one automatically. No code changes needed. To force a specific mode:\n\n```dart\nfinal pdf = Pdf(config: PdfConfig(webIoMode: PdfIoMode.atomics));\n```\n\nTo check which mode was selected (useful for detecting OPFS fallback):\n\n```dart\nfinal mode = await pdf.ensureInitialized();\nif (mode == PdfIoMode.opfs) {\n  // OPFS mode: pre-copies each source to disk before processing.\n  // Slower first byte + uses disk quota vs streaming modes.\n  // To get streaming: deploy with COOP/COEP headers (Atomics)\n  // or target Chrome 137+ / Firefox 139+ (JSPI auto-detected).\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eAdvanced: faster web with COOP/COEP headers (has trade-offs)\u003c/summary\u003e\n\nBy default on browsers without JSPI support, the package copies your PDF to temporary disk storage (OPFS) before processing — works everywhere, no server config needed.\n\nOn 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.\n\nFor **older browsers** that have `SharedArrayBuffer` but not JSPI, adding two server headers enables Atomics mode — direct memory reads, no disk copy, lower latency:\n\n```\nCross-Origin-Opener-Policy: same-origin\nCross-Origin-Embedder-Policy: require-corp\n```\n\n**⚠️ 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.\n\nWith these headers, browser support for streaming goes further back:\n\n| Browser | Version | Released |\n|---|---|---|\n| Chrome / Edge | 68+ | Jul 2018 |\n| Firefox | 79+ | Jul 2020 |\n| Safari / Safari iOS | 15.2+ | Dec 2021 |\n\nFor development:\n\n```sh\nflutter run -d chrome --cross-origin-isolation\n```\n\nThis adds the COOP/COEP headers to Flutter's dev server automatically.\n\n\u003c/details\u003e\n\n---\n\n## When NOT to use pdf_manipulator\n\n- **You only need to display PDFs.** Use [`pdfx`](https://pub.dev/packages/pdfx) or [`flutter_pdfview`](https://pub.dev/packages/flutter_pdfview).\n- **Server-side batch processing.** This package is for client-side use. For thousands of PDFs per second, use qpdf or poppler.\n- **OCR.** This package extracts text already in the PDF. For scanned images, you need Tesseract or similar.\n\n---\n\n## Docs\n\n| | |\n|---|---|\n| [Architecture](docs/ARCHITECTURE.md) | How it's built — layers, streaming I/O, three web modes |\n| [Capabilities](docs/CAPABILITY_ROADMAP.md) | What's shipped, what's planned |\n| [Updating](docs/UPDATING.md) | Maintaining the vendored Rust engine |\n| [Migration](docs/MIGRATION.md) | Upgrading from the old Android-only version |\n| [Contributing](CONTRIBUTING.md) | Setup, PR workflow, adding operations |\n\n---\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhuppi%2Fpdf_manipulator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwhuppi%2Fpdf_manipulator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhuppi%2Fpdf_manipulator/lists"}