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
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:
FidelityPdfView(source: PdfSource.asset(class=class="tok-str">"tok-str">'assets/report.pdf')) PdfSource also takes a URL or bytes in hand:
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.
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:
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.
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.
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:
| Type | Means |
|---|---|
PdfSourceException | The bytes could not be fetched. |
UnsupportedDocumentError | Not a PDF or supported image. |
PdfEngineUnsupportedError | The 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.
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); FidelityPdfView(
source: source,
config: const FidelityConfig(workerUrl: class=class="tok-str">"tok-str">'/pdfium_worker.js'),
);