betto_pdfium 0.1.0-dev.2 copy "betto_pdfium: ^0.1.0-dev.2" to clipboard
betto_pdfium: ^0.1.0-dev.2 copied to clipboard

A pure Dart package wrapping the PDFium library for PDF rendering, text extraction, and annotation support.

betto_pdfium #

A pure Dart package that wraps PDFium via Dart FFI. No dependency on dart:ui or Flutter — works in CLI tools, server-side Dart, and Flutter apps alike.

Pre-built PDFium binaries are sourced from bblanchon/pdfium-binaries and downloaded automatically by the native-assets hook on first use.

Platform support #

Platform Status
macOS arm64 Supported
Linux x86_64 Supported
Linux arm64 Supported
iOS arm64 Supported (xcframework)
Android arm64 Supported
Android x86_64 Supported
Windows x86_64 Not yet supported
Web (WASM) Not yet supported

Installation #

Add to your pubspec.yaml:

dependencies:
  betto_pdfium: ^0.1.0-dev.2

Flutter iOS apps also need the companion plugin, which delivers the PDFium xcframework via Swift Package Manager:

dependencies:
  betto_pdfium: ^0.1.0-dev.2
  betto_pdfium_ios: ^0.1.0-dev.2

No additional setup is needed on desktop — the PDFium binary is fetched automatically when you first run dart test or dart run. For iOS and Android see Mobile below.

Quick start #

import 'dart:io';
import 'package:betto_pdfium/betto_pdfium.dart';

void main() async {
  final bytes = await File('document.pdf').readAsBytes();
  final doc = await PdfDocument.fromBytes(bytes);
  try {
    final meta = await doc.getMetadata();
    print(meta.title);
  } finally {
    await doc.close();
  }
}

API #

All capabilities are on PdfDocument. Import a single file:

import 'package:betto_pdfium/betto_pdfium.dart';

Loading a document #

final bytes = await File('document.pdf').readAsBytes();
final doc = await PdfDocument.fromBytes(bytes);

Throws PdfExtractionException on failure. Inspect exception.error to distinguish recoverable conditions:

try {
  final doc = await PdfDocument.fromBytes(bytes);
} on PdfExtractionException catch (e) {
  switch (e.error) {
    case PdfError.passwordRequired:
      print('Password required.');
    case PdfError.invalidDocument:
      print('Not a valid PDF.');
  }
}

Closing and resource management #

Always call close() when finished. A Finalizer is registered as a safety net, but explicit disposal is strongly preferred:

final doc = await PdfDocument.fromBytes(bytes);
try {
  // work with doc
} finally {
  await doc.close();
}

After close() returns, all other methods throw StateError. Calling close() more than once is safe.

Metadata #

final meta = await doc.getMetadata();
print(meta.title);
print(meta.author);
print(meta.creationDate?.value?.toIso8601String());

All fields on PdfMetadata are nullable — a null means the entry was absent from the PDF's Info dictionary.

Document info #

Returns the PDF file version and the permanent/changing file identifiers:

final info = await doc.getDocumentInfo();
print('PDF version: ${info.fileVersion}');

final hex = info.permanentId
    ?.map((b) => b.toRadixString(16).padLeft(2, '0'))
    .join();
print('Permanent ID: $hex');

Page count #

final count = await doc.pageCount;
print('Pages: $count');

Text extraction #

Streams pages one at a time, in index order:

await for (final page in doc.extractPlainText()) {
  if (page.hasTextLayer) {
    print('Page ${page.pageIndex}: ${page.text}');
  } else {
    print('Page ${page.pageIndex}: scanned (no text layer)');
  }
}

Extract a single page by index:

final page = await doc.extractPlainText(pageIndex: 0).first;

Check whether the document is worth extracting before streaming all pages:

if (!await doc.isPlainTextExtractable()) {
  print('Document appears to be scanned.');
}

Page size #

Returns the page's intrinsic dimensions in PDF user units (points, 1/72 inch):

final size = await doc.getPageSize(0);
print('${size.widthPt} × ${size.heightPt} pt');
print('Aspect ratio: ${size.aspectRatio}');

Convert to pixels for a specific DPI:

final px = size.sizeForDpi(150);
// px.width, px.height are doubles

Rendering #

Renders a page to a raw BGRA byte buffer:

final size = await doc.getPageSize(0);
final px = size.sizeForDpi(150);
final result = await doc.renderPageToBytes(
  0,
  px.width.round(),
  px.height.round(),
);

// result.pixels  — Uint8List of BGRA bytes
// result.pixelWidth, result.pixelHeight
assert(result.pixels.length == result.pixelWidth * result.pixelHeight * 4);

In a Flutter app, decode the BGRA buffer into a dart:ui Image via decodeImageFromPixels. The rendering surface is intentionally kept at the pure-Dart layer so it can be used outside Flutter.

Optional flags:

final result = await doc.renderPageToBytes(
  pageIndex,
  width,
  height,
  renderAnnotations: true,   // default: true  (FPDF_ANNOT)
  lcdText: false,            // default: false (FPDF_LCD_TEXT)
  backgroundColor: 0xFFFFFFFF, // default: opaque white (ARGB)
);

Annotations #

Streams one PdfPageAnnotations per page. Pages with no annotations yield an entry with an empty annotations list:

await for (final page in doc.extractAnnotations()) {
  for (final annot in page.annotations) {
    switch (annot) {
      case PdfTextAnnotation(:final contents, :final rect):
        print('Note on page ${page.pageIndex}: $contents at $rect');
      case PdfMarkupAnnotation(:final subtype, :final quadPoints)
          when subtype == PdfAnnotationType.highlight:
        print('Highlight on page ${page.pageIndex}');
      case PdfInkAnnotation(:final strokes):
        print('Ink with ${strokes.length} stroke(s)');
      default:
        print('Other annotation: ${annot.runtimeType}');
    }
  }
}

Extract a single page:

final page = await doc.extractAnnotations(pageIndex: 2).first;

Images #

Enumerate image objects page by page:

await for (final page in doc.extractImages()) {
  for (final img in page.images) {
    print('Image ${img.objectIndex}: '
        '${img.metadata.width}×${img.metadata.height} '
        '${img.filters.join(",")}');
  }
}

Fetch the rendered BGRA bitmap for a specific image object:

final bitmap = await doc.renderImage(pageIndex, objectIndex);
if (bitmap != null) {
  // bitmap.bgra, bitmap.width, bitmap.height
}

For bulk extraction, pass includeBitmap: true to extractImages() to retrieve bitmaps in a single stream pass.

await for (final match in doc.search('example')) {
  print('Match on page ${match.pageIndex + 1}: '
      'char ${match.charIndex}, ${match.rects.length} rect(s)');
}

Control matching behaviour with PdfSearchFlag values:

final matches = doc.search(
  'Dart',
  flags: {PdfSearchFlag.matchCase, PdfSearchFlag.matchWholeWord},
  pageIndex: 0, // restrict to one page
);

Match rects are in PDF user-space (origin bottom-left). Apply FPDF_PageToDevice if you need screen coordinates.

Table of contents #

final toc = await doc.tableOfContents;
for (final entry in toc) {
  final target = entry.pageIndex != null
      ? 'page ${entry.pageIndex! + 1}'
      : '(no target)';
  print('${entry.title} → $target');
  for (final child in entry.children) {
    print('  ${child.title}');
  }
}

Returns an empty list when the document has no bookmarks — not an error.

Thumbnails #

final thumb = await doc.getThumbnail(0);
if (thumb != null) {
  // thumb.bgra, thumb.width, thumb.height
  // thumb.source == PdfThumbnailSource.embedded
  //             or PdfThumbnailSource.rendered
}

In Flutter, scale maxDimension by MediaQuery.of(context).devicePixelRatio for crisp thumbnails on high-DPI displays:

final dpr = MediaQuery.of(context).devicePixelRatio;
final thumb = await doc.getThumbnail(0, maxDimension: (256 * dpr).round());

Pass generateIfAbsent: false to return null rather than rendering a fallback when no embedded thumbnail exists.

Error types #

Exception When thrown
PdfExtractionException fromBytes() fails (wrong password, corrupt file)
PdfiumException Unexpected PDFium native failure (allocation, render)
StateError Any method called after close()
RangeError Page index out of range, non-positive render dimensions

Architecture #

PDFium is not thread-safe. On native platforms all PDFium calls run on a dedicated background Isolate (PdfiumIsolate) so the calling isolate (e.g. the Flutter UI isolate) is never blocked. The isolate is spawned lazily on the first PdfDocument.fromBytes() call and held for the process lifetime.

The platform backend is selected automatically via Dart's conditional import mechanism — callers import only betto_pdfium.dart and receive the correct implementation for their target.

Running the examples #

From the repo root:

dart run example/main.dart   # metadata extraction
dart run example/extract.dart  # full text extraction
dart run bin/pdfinfo.dart    # pdfinfo CLI tool

See example/README.md for more details.

Mobile (iOS / Android) #

iOS support is provided by the companion package betto_pdfium_ios, which wraps the PDFium xcframework as a Flutter plugin and delivers it via Swift Package Manager. Add it to your Flutter app's pubspec.yaml alongside betto_pdfium (see Installation).

iOS and Android require additional one-time setup. See integration_test_app/ and the repo Makefile for targets that fetch mobile binaries and run on-device tests:

make fetch_mobile_binaries   # Android only — iOS xcframework fetched by SPM
make ios_test
make android_test

iOS prerequisite (one-time global setup):

flutter config --enable-swift-package-manager

License #

Apache 2.0 — see LICENSE.

1
likes
0
points
320
downloads

Publisher

verified publisherbettongia.com

Weekly Downloads

A pure Dart package wrapping the PDFium library for PDF rendering, text extraction, and annotation support.

Homepage
Repository (GitHub)
View/report issues

Topics

#pdfium #pdf #ffi

License

unknown (license)

Dependencies

args, code_assets, collection, ffi, hooks, meta

More

Packages that depend on betto_pdfium