Guide

Getting started

fidelity_pdf renders PDFs and raster images entirely in the browser. pdfium runs in a pool of Web Workers; the UI thread only hands out rectangles and decodes the pixels that come back.

Install

pubspec.yaml
dependencies:
  fidelity_pdf: ^1.1.0

pdfium.wasm ships with the dependency and is bundled into your web build automatically. There is no asset to copy and no script tag to add.

Web only, today. pdfium runs in Web Workers, so this targets Flutter web. On other platforms every entry point throws PdfEngineUnsupportedError rather than silently rasterising on the UI thread. PdfEngine is the seam a native (FFI + isolate) backend would slot into.

Render something

The whole viewer, including pan and zoom:

viewer.dart
FidelityPdfView(source: PdfSource.asset(class=class="tok-str">"tok-str">'assets/report.pdf'))

PdfSource also takes a URL or bytes in hand:

sources.dart
PdfSource.url(class=class="tok-str">"tok-str">'https://example.com/drawing.pdf')
PdfSource.bytes(bytes, name: class=class="tok-str">"tok-str">'upload.pdf')

Drive it yourself

Pass a controller when you need pages, thumbnails or the camera. The widget will not dispose a controller it does not own.

controlled.dart
final camera = CameraSystem();
final doc = await FidelityController.open(
  PdfSource.bytes(bytes),
  camera: camera,
);
doc.showPage(2);

FidelityPdfView(controller: doc, camera: camera);

Network documents

A fetched URL is cached to disk on first open and served from there afterwards, so a large document downloads once:

network.dart
FidelityPdfView(source: PdfSource.url(class=class="tok-str">"tok-str">'https://example.com/plan.pdf'))

// Opt out for one-shot or sensitive files:
PdfSource.url(url, cache: false)

On web this is the Cache API — not IndexedDB. It is purpose-built for URL→Response, the browser manages quota and eviction, and entries survive a reload. It needs a secure context (https or localhost); everywhere it is unavailable the calls degrade to no-ops and fetching simply proceeds. Nothing here ever throws: a cache that cannot be read is a reason to fetch, never a reason to fail an open.

This saves the download, not the memory. pdfium needs the whole document inside each worker's wasm heap, so a cached 100 MB file still costs workers × 100 MB once open. Caching to storage and streaming from storage are different problems, and only the first is solved here.

The key is the URL, so a document that changes behind a stable address would serve stale — call evictCachedDocument(url) or clearDocumentCache() when that can happen.

Images

PNG, JPEG, BMP, GIF and WebP work through the same call. The format is sniffed from the bytes — never from the file extension or a Content-Type, both of which lie routinely.

This is not a wrapper around Image.network. A 9000×6000 PNG is 205 MB decoded and larger than the GPU's maximum texture, so it cannot be drawn as one image at all. Tiling fixes that, and deep zoom comes free.

Images have a resolution limit; PDFs don't. A PDF is vector, so pdfium keeps producing real detail at any zoom. An image is a fixed grid of pixels: past one source pixel per point there is nothing left to render. Rendering stops at native automatically, and the camera stops at maxMagnification past it (default 4×) so a photo can be inspected but never zoomed into mush.

Thumbnails

doc.thumbnails is created on first use and shares the same worker pool at background priority — a page rail is dozens of renders, and without the demotion it would starve the tiles the user is looking at.

rail.dart
ListView.builder(
  itemCount: doc.pageCount,
  itemBuilder: (context, i) => PdfThumbnail(
    thumbnailer: doc.thumbnails,
    page: i,
    placeholder: const ColoredBox(color: Color(0xFFEDEFF2)),
  ),
)

Thumbnails are LRU'd under a byte budget and disposed on eviction. Re-read cached(i) on every build rather than holding a ui.Image across frames.

Configuration

Every tunable is on FidelityConfig, and estimate() reports what a configuration costs before you pay for it. This matters more than it sounds: a wasm heap never shrinks, so over-provisioning does not merely waste memory — it is what kills workers.

config.dart
const config = FidelityConfig(
  workers: 3,          // parallelism AND the document-memory multiplier
  pageCacheSize: 2,    // parsed pages held open, per worker
  previewSize: 1600,   // whole-page preview: why it doesn't pop in
  prefetchRing: 1,     // rings of off-screen tiles rendered ahead
  maxCachedTiles: 256, // decoded tiles for the current page
);

print(config.estimate(bytes.length));
// documents 81.0 MB + tiles 256.0 MB + preview 9.8 MB + thumbs 32.0 MB
// = 378.8 MB (+ parsed pages, unbounded)

// For large files, or wherever memory is tighter than latency:
FidelityPdfView(source: source, config: FidelityConfig.lowMemory);

previewSize — why the page doesn't pop in

The moment a page opens, the engine renders one image of the whole page at previewSize px on its longest edge. Tiles then sharpen it in place.

That it is a single render is the entire point. A grid of a dozen medium-resolution tiles finishes at a dozen different moments, so the page assembles itself square by square — the effect you know from map apps. One render arrives once, and the reader gets a complete, legible page in a single step. Going from medium to crisp reads as refinement; going from blank to content reads as popping.

The default 1600 is roughly screen resolution for a fit-to-window page, so the first thing you see already looks right. It is pinned and never evicted — it is the fallback of last resort for every region, and the painter only consults fallbacks when a tile is missing, so anything subject to an LRU that never sees it used would be collected exactly when it is needed. Set 0 to disable, and expect a blank page while the first tiles arrive.

prefetchRing — rendering ahead of a pan

Panning is predictable: the tile you want next is almost always adjacent to one already on screen. prefetchRing: 1 renders a one-tile halo around the viewport at backgroundPriority, so a pan usually lands on tiles that are already decoded. It can never delay a visible tile — speculation always sorts behind what the camera asked for, and the moment paint asks for a halo tile it is promoted to full priority.

Cost grows quickly: ring 1 is roughly 2.5× the visible tiles, ring 2 about 5×. Set 0 to disable.

Errors & CSP

Everything is typed, so an app can react rather than spin:

TypeMeans
PdfSourceExceptionThe bytes could not be fetched.
UnsupportedDocumentErrorNot a PDF or supported image.
PdfEngineUnsupportedErrorThe browser cannot host the engine. Carries a reason.

The failure that actually happens is CSP. Flutter never spawns a blob: worker, so a policy with worker-src 'self' leaves your app running perfectly while only PDF rendering dies (reason: blobWorkerBlocked). Fix it by self-hosting the worker.

tool/write_worker.dart
import class=class="tok-str">"tok-str">'dart:io';
import class=class="tok-str">"tok-str">'package:fidelity_pdf/fidelity_pdf.dart';

void main() => File(class=class="tok-str">"tok-str">'web/pdfium_worker.js').writeAsStringSync(kPdfiumWorkerJs);
csp_safe.dart
FidelityPdfView(
  source: source,
  config: const FidelityConfig(workerUrl: class=class="tok-str">"tok-str">'/pdfium_worker.js'),
);

Full API reference →