Unseal logo

Unseal

pub.dev package Dart CI MIT license Dart SDK 3.8 or newer

Unseal is a pure-Dart book parser and reading-data library. Give it bytes or a filesystem path and receive one format-neutral Book model with metadata, cover data, files, reading order, navigation, and statistics.

It supports EPUB, MOBI, AZW3, PalmDOC, eReader and Plucker PDB, RTF, Markdown, Sony LRF and LRX metadata, FB2, TXT, HTML, CHM, DOCX, ODT, AZW4, comic archives, PDF, DjVu, and TIFF/BigTIFF without a native executable, FFI bridge, or Flutter dependency in the library itself.

Unseal is a parser and data layer, not a reader UI or a general-purpose document renderer.

At a glance

  • One API for metadata-only scans and full book parsing.
  • Common models for metadata, HTML content, stylesheets, images, fonts, navigation, covers, and reading order.
  • Format detection, full-text search, reading progression, portable locators, EPUB CFI operations, annotation codecs, and OPDS feed parsing.
  • Pure Dart on the Dart VM, Flutter native targets, and browser JavaScript.
  • Optional web-worker parsing for browser applications.
  • Typed exceptions and format-specific recovery paths for damaged input.

Start with installation and the quick start. The documentation index links to usage, architecture, development, and the single roadmap.

Supported formats

Format Main capabilities Important boundary
EPUB 2/3 OPF metadata, spine (including linear="no" items), XHTML, SVG content documents, CSS, images, fonts, media overlays, navigation, and CFI; EPUB 3 page list, landmarks, rendition, accessibility metadata, and fixed-layout viewports with effective per-page rendition helpers Font obfuscation is limited to the standard IDPF and Adobe algorithms.
KEPUB Kobo EPUB packages are read through the EPUB parser, preserving Kobo spans and EPUB content. Reported as BookFormat.epub; a Kobo-specific format type is not inferred from ZIP bytes.
MOBI 6 PalmDOC and HUFF/CDIC decompression, EXTH metadata, chapters, images, fonts, and navigation DRM-protected books are rejected.
PalmDOC PDB Uncompressed or PalmDOC-compressed TEXt/REAd records, title, and plain reading text Other PDB ebook families use separate parsers.
eReader PDB Unencrypted PNRd/PPrs records with PML emphasis, headings, local links, PNG images, footnotes, sidebars, and title/author metadata Reading-oriented HTML does not reproduce page layout or every PML control; protected books are rejected.
Plucker PDB Data/Plkr text pages, continued records, paragraph links and inline formatting; Palm 1/2/4-bit grayscale, 8-bit system and custom palettes, and 16-bit RGB565 direct-color images Palette and RGB565 paths are covered by generated Palm-bitmap fixtures; no real Plucker book using those variants has been verified. Internally compressed bitmaps, bookmarks, and owner-keyed documents are not converted.
Sony LRF BBeB metadata, ordered text pages, table of contents, and embedded JPEG/PNG/BMP/GIF images for versions 1–800 and 801+ Reflowed content rather than Sony's fixed layout. Exact typography, ruby styling, and complex canvas layouts are outside this contract.
Sony LRX Plaintext metadata from the LRX2 BBeB wrapper Protected book content is rejected with DrmProtectedException; metadata-only support does not decrypt or render it.
AZW3 / KF8 Skeleton and div reassembly, FDST flows, CSS/SVG, images, NCX navigation, and joint MOBI/KF8 files DRM-protected books are rejected.
FB2 / FBZ Metadata, covers, declared encodings, notes, links, styles, and section navigation The result is a reflowable HTML representation.
TXT / TXTZ UTF-8/UTF-16 decoding, paragraph-preserving XHTML, archive resources, and basic Markdown/Textile Not a full Markdown or Textile implementation. Plain TXT metadata is limited.
Standalone Markdown CommonMark blocks and inline formatting, headings, links, and simple front matter Byte-only calls use parseMarkdownBook; readFile recognizes .md, .markdown, and .mdown. Relative resources are references, not extracted files.
RTF Text, title/author, paragraphs, Unicode, codepages 1251/1252/1256 and UTF-8, and basic emphasis Tables, pictures, drawing objects, fields, and page layout are not converted.
HTML / HTMLZ Source-preserving HTML, head metadata, heading navigation, OPF manifests, and resources HTML is preserved, not sanitized.
CHM ITSF directory, LZX-compressed HTML topics and resources, #SYSTEM title/home, nested .hhc contents, Intel E8 restoration, and Windows-1251/1252/1256 text from the locale record Other legacy codepages, full-text index, merged help collections, and embedded scripts are outside this contract.
DOCX Open XML document structure, styles, headings, lists, tables, links, properties, and images Semantic conversion; not pixel-perfect Word layout.
ODT OpenDocument paragraphs, headings, styles, lists, tables, images, links, and metadata Semantic conversion; not pixel-perfect LibreOffice layout.
AZW4 PalmDB/MOBI wrapper validation, PDF extraction, metadata, pages, and original PDF bytes Requires a valid, unencrypted PDF payload.
CBZ ZIP comic pages, ComicInfo.xml reading direction and page roles, natural ordering, and declared front cover Page-oriented data; no comic UI is included.
CBR RAR 4 stored and ordinary non-solid method-29 entries, plus RAR 5 stored entries Split, solid, encrypted, PPMd, VM, and other unsupported variants are rejected.
CB7 7-Zip comic pages, ComicInfo.xml reading direction and page roles, natural ordering, and declared front cover Asynchronous only; encrypted or unsupported entries are rejected.
CBC Top-level comics.txt collections containing CBZ, CBR, or CB7 files The collection is flattened into one ComicBook page sequence.
PDF Metadata, bookmarks, page text with extraction diagnosis, conservative two-column reflow, facsimile data, and JPEG/CCITT/JBIG2 images Reading-oriented extraction, not OCR or a general-purpose PDF renderer.
DjVu (JPEG, JB2, and IW44 subset) Direct BGjp JPEG pages, Sjbz masks with shared Djbz dictionaries and INCL references, progressive BG44/FG44 layers, and INFO dimensions and rotation in single-page or bundled documents; compressed TXTz hidden text, legacy TXTa OCR, and NAVM bookmark outlines JB2 masks and composed IW44 pages become BMP images. Compressed text is byte-exact against djvutxt. Indirect document directories resolve through file-based opens only; FGbz recoloring, FGjp/Smmr layer replacements, ANTz annotations, and thumbnails are ignored or rejected as described below.
TIFF / BigTIFF Baseline uncompressed, PackBits, LZW, and Deflate/AdobeDeflate raster pages (8-bit gray and RGB, 16-bit gray, bilevel, with horizontal-differencing predictors), CCITT Group 4 and Group 3 (including modified Huffman and EOL-less producers), multi-page IFD chains in both byte orders, BigTIFF 64-bit offsets, ImageDescription/DateTime/Software/Artist metadata, and X/Y-resolution DPI Pages become BMP images (JPEG passthrough when a page holds one self-contained strip). Tiled layouts, planar=2, palette, float/SampleFormat≠1, LogLuv, JPEGTables-abbreviated strips, old-style JPEG, reversed FillOrder, and uncompressed-mode extensions are rejected typed. A page is capped at 256 megapixels; strips and tiles are bounds-checked before allocation; a document is capped at 128 MiB input, 1024 pages, 256 megapixels of decoded pixels, and 128 MiB of decoded raster. Pixel parity with libtiff (tiffcmp) is pinned by tests.

OPF is not a standalone reading format. When unseal finds metadata.opf inside TXTZ or HTMLZ, or a sibling <book-name>.opf beside a native file, it uses it as a metadata and manifest sidecar.

Installation

dart pub add unseal

Or add the dependency manually:

dependencies:
  unseal: ^1.2.0

The package requires Dart >=3.8.0 <4.0.0.

Quick start

The byte-based asynchronous API is the most portable entry point:

import 'dart:typed_data';

import 'package:unseal/unseal.dart';

Future<Book> openBook(Uint8List bytes) {
  return Unseal.read(bytes);
}

Future<void> inspectBook(Uint8List bytes) async {
  final book = await openBook(bytes);

  print(book.format.name);
  print(book.metadata.title);
  print(book.metadata.authors);
  print(book.metadata.cover?.bytes);
  print(book.navigation.navPoints);
  print(book.files.html.length);
  print(book.statistics.wordCount);
}

For a library scanner that does not need the content files:

final metadata = await Unseal.readMetadata(bytes);
print(metadata.title);
print(metadata.authors);

To report whether the full book can be opened, use inspect. It performs the same parse as read and keeps the opened book in the result, so the application does not need to parse it again:

final inspection = await Unseal.inspect(bytes);
if (inspection.canOpen) {
  final book = inspection.book!;
  print(book.metadata.title);
} else {
  print(inspection.error); // Typed UnsealException.
}

detectedFormat is only a signature-level family hint; format identifies the concrete format after a successful open. Inspecting is not a lightweight metadata scan; use readMetadata for that.

On native platforms, filesystem paths are also supported:

final book = await Unseal.readFile('/books/wonderland.epub');
final metadata = await Unseal.readMetadataFile('/books/wonderland.epub');

A standalone Markdown file is selected by its path extension. With bytes alone, use the format-specific entry point because Markdown has no reliable signature:

import 'package:unseal/markdown.dart';

final book = parseMarkdownBook(markdownBytes, sourceName: 'notes.md');

Initial Markdown front matter accepts single-line key: value entries and dash lists for authors, languages, and tags. Supported metadata includes title, author(s), language(s), tag(s), and description. It is a deliberately small subset, not arbitrary YAML.

Path-based reads are filesystem-only and read the input file into memory before parsing. They also look for a sibling <book-name>.opf, followed by metadata.opf, and merge available sidecar metadata.

Progressive sessions for large books

Unseal.read materializes the whole book before returning. A BookSession returns its structure first and loads requested content through a resident isolate on the VM or a configured Web Worker in the browser. Inflated resources use a byte-budgeted LRU cache:

final session = await BookSession.openFile(path);
final book = session.book; // structure only: metadata, navigation, reading order

final first = book.readingOrder.first;
final chapter = await session.loadDocumentSource(first.name); // raw HTML
final text = await session.loadDocumentText(first.name); // canonical text space
final results = await session.search('island');
final progress = await session.computeProgression();
final cfi = await session.buildCfi(contentIndex: 0, offsetInText: 120);

session.cancelPending(); // fail in-flight requests with SessionCancelledException
session.dispose(); // end the session; later requests fail with SessionClosedException

session.book is a real EpubBook, ComicBook or PdfBook whose structure is complete (spine, rendition data, navigation, container inventory, the cover when it fits SessionPolicy.coverMaxBytes, page geometry for PDFs) while files carries no content — spine-driven consumers such as fixed-layout readers work unchanged, and loadResource(path) serves any container entry on demand. EPUB, CBZ, CBR and PDF open progressively today; other formats throw SessionUnsupportedFormatException and should use Unseal.read instead. PDF sessions materialize pages on demand (loadDocumentSource('page_<i>.html'), loadPdfPageLines(i) for the facsimile text-layer geometry) and serve byte ranges of the original document via loadBytesRange. Compressed CBR entries inflate during session opening, and session startup overhead can outweigh savings for small books. The classic entry points retain their full-book behavior.

SessionPolicy defaults to a 64 MiB resource cache, an 8 MiB header cover limit, and an 8 MiB cap per byte-range response. Whole-book operations such as search and statistics can still traverse all content. CBZ/CBR search returns no matches because their pages have no searchable text; sessions do not perform OCR. In browsers, use BookSession.open(bytes) and configure the worker as shown below. Without a healthy worker, sessions execute inline.

PDF sessions derive their reflow plan from up to 20 sampled pages. For longer documents, their canonical text and offsets can differ from a full parse. Within a session, loadDocumentText and loadPdfPageLines use the same plan. Do not assume that offsets saved from one extraction mode address the same passage in the other mode.

Format-specific APIs

The package exports dedicated entry points when an application needs details from one format:

final epub = parseEpubBook(bytes);
final mobi = parseMobiBook(bytes);
final fb2 = parseFb2Book(bytes);
final comic = parseComicBook(bytes); // CBZ or supported CBR
final pdf = parsePdfBook(bytes);
final txt = parseTxtBook(bytes);
final html = parseHtmlBook(bytes);
final docx = parseDocxBook(bytes);
final odt = parseOdtBook(bytes);
final azw4 = parseAzw4Book(bytes);

final cb7 = await parseComic7Book(bytes);
final cbc = await parseCbcBook(bytes);

CB7 and CBC use an asynchronous 7-Zip reader. Unseal.parse and Unseal.readMetadataSync reject those formats; use Unseal.read, Unseal.readMetadata, or the dedicated asynchronous functions instead.

For EPUB 3, EpubBook.navigation holds the table of contents from the EPUB 3 navigation document when available, with NCX as a fallback. pageList and landmarks expose other navigation sections separately. The package also exposes typed display preferences and accessibility metadata:

final book = await Unseal.read(bytes);
if (book is EpubBook) {
  print(book.pageList?.navPoints);
  print(book.landmarks.map((landmark) => landmark.types));

  if (book.package is Epub3Package) {
    final epub3 = book.package as Epub3Package;
    print(epub3.renditionSettings.layout);
    if (epub3.spine.items.isNotEmpty) {
      print(epub3.renditionSettings.layoutFor(epub3.spine.items.first));
    }
    if (epub3.metadata is Epub3Metadata) {
      print((epub3.metadata as Epub3Metadata).accessibility.sufficientAccessModes);
    }
  }
}

Rendition values come from the OPF and spine. Accessibility values reflect publisher metadata; Unseal does not certify accessibility.

For fixed-layout EPUBs, the book resolves the rendition, viewport, and linearity of any reading-order position directly, so a reader does not have to re-derive the spine path, manifest id, and rendition chain itself:

if (book is EpubBook) {
  const index = 0;
  print(book.layoutForSpineIndex(index)); // EpubLayout.prePaginated
  print(book.orientationForSpineIndex(index));
  print(book.spreadForSpineIndex(index));
  print(book.pageSpreadForSpineIndex(index)); // synthetic-spread placement
  print(book.viewportForSpineIndex(index)); // EpubViewport(width, height) or null
  print(book.spineItemIsLinear(index)); // false for linear="no" items
  print(book.nonLinearSpinePaths);
}

contentViewports maps content paths (the same key space as files.html) to the page dimensions declared inside fixed-layout XHTML (<meta name="viewport">) or standalone SVG content documents (root width/height, falling back to viewBox). Standalone SVG documents join the reading order as content documents. Malformed, fractional, non-positive, or oversized declarations yield no viewport instead of failing the parse; only documents the spine references are scanned.

BookProgression measures the full reading order, including non-linear spine items. Applications that omit auxiliary content must account for that difference when presenting progress.

EPUB metadata updates and Calibre catalogs

updateEpubMetadata returns new EPUB bytes with selected OPF fields changed. It does not write the file or change identifiers, the cover, or chapter content:

final updated = updateEpubMetadata(
  bytes,
  const EpubMetadataUpdate(title: 'A new title'),
);

To inspect an existing Calibre library database, read its metadata.db bytes and use the read-only parser:

final catalog = CalibreDatabase.parse(metadataDbBytes);
for (final entry in catalog.books) {
  print('${entry.title}: ${entry.formats}');
}

The Calibre records describe available formats and library-relative paths; opening a listed book remains the application's responsibility.

Page data for comics and PDFs

ComicBook.pages stays in natural archive order. pageInfo[i] describes pages[i]: its ComicInfo role, double-page hint, and optional declared dimensions. readingDirection becomes rightToLeft only when ComicInfo explicitly says Manga=YesAndRightToLeft. A page marked Delete remains in the list for the application to handle. A declared FrontCover is used as the book cover; otherwise the first page is used.

final comic = parseComicBook(comicBytes);
print(comic.readingDirection);
for (final page in comic.pageInfo) {
  print('${comic.pages[page.imageIndex].path}: ${page.type}, spread=${page.doublePage}');
}

Each PdfPageText reports quality, the number of content streams, and how many streams could not be decoded. imageWithoutText is a useful signal for an application that offers OCR; it does not prove that a page is a scan. columnOrderApplied indicates that Unseal reordered clearly separated, text-only columns for reflow and the canonical text stream.

DjvuBook.pages exposes directly stored JPEG pages, decoded JB2 masks as black-on-white 1-bit BMP images, and bounded IW44 photo or composed pages as 24-bit BMP images. It preserves dimensions, DPI, rotation, and hidden text: compressed TXTz chunks (preferred over the legacy TXTa encoding when a page stores both) carry the page text in reading order, rendered with the separator conventions of positional text extraction. A top-level NAVM chunk becomes real navigation; bookmark URLs stay verbatim on each point's content target. Per-page HTML keeps the text searchable. The complete source stays in DjvuBook.bytes for applications with a dedicated DjVu renderer. JB2 pages inherit the shared Djbz dictionary their INCL chunk names — including documents that switch dictionaries mid-document — and alternate image codecs that replace a supported layer still fail with typed errors.

final pdf = parsePdfBook(pdfBytes);
for (var i = 0; i < pdf.pageTexts.length; i++) {
  final page = pdf.pageTexts[i];
  print('Page ${i + 1}: ${page.quality}, skipped=${page.skippedContentStreams}');
}

The common book model

Every parser exposes the parts a reading application usually needs:

  • BookMetadata for title, authors, language, publisher, identifiers, subjects, dates, series, and cover data.
  • Files for HTML content, stylesheets, images, fonts, and binary resources.
  • ReadingOrderItem and Navigation for the spine and table of contents.
  • BookStatistics and TextFile.plainText for word counts and estimated reading time.
  • Format-specific models such as EpubBook, MobiBook, DocumentBook, ComicBook, and PdfBook when the generic model is not enough.

The library also provides reusable reading services:

  • Search with plain-text, case-sensitive, whole-word, regex, and proximity modes.
  • Portable text, page, and EPUB CFI locators.
  • Locator serialization and text-locator relocation after content changes.
  • Bookmark and highlight codecs for application-owned persistence.
  • EPUB CFI resolution and construction.
  • OPDS feed parsing. Networking and authentication remain the caller's job.
  • Bounded format detection before parsing.

Platform behavior

Environment Supported path Execution notes
Dart VM Bytes and filesystem paths Native asynchronous parsing uses an isolate when the runtime supports it.
Flutter Android, iOS, macOS, Linux, and Windows Bytes and filesystem paths The library has no Flutter dependency; the repository includes a minimal Flutter example.
Browser JavaScript Bytes only Without configuration, parsing runs on the browser's main thread.
Browser JavaScript with worker Bytes only UnsealWorker can move normal parsing off the main thread.
WebAssembly Not currently claimed as a verified target There is no WASM build or compatibility guarantee in the current validation matrix.

Browser usage

Browser applications should fetch or otherwise obtain a Uint8List and use a byte-based API. readFile and readMetadataFile throw UnsupportedError on the web.

To keep large parses off the browser's main thread, compile and serve the worker entry point:

dart compile js web/unseal_worker.dart -o <your-web-root>/unseal_worker.js

Configure it before opening books:

import 'package:unseal/unseal.dart';

void configureReader() {
  UnsealWorker.configure(Uri.parse('unseal_worker.js'));
}

The worker URL must be reachable under the browser's worker security rules, including same-origin or CORS requirements. The worker is lazy. If it cannot be loaded, normal parsing falls back to the main thread. The fallback keeps the API working but a large book can temporarily block the UI.

The worker receives and returns structured-cloned data. Large books can therefore temporarily exist in both the worker and the main-thread memory. CB7 and CBC stay on their asynchronous archive path instead of the normal worker operation.

Limitations and security boundaries

Rendering and fidelity

unseal produces reading data; it does not render pages or provide navigation controls. DOCX and ODT preserve semantic structure and common formatting, but complex layouts, footnotes, charts, equations, advanced numbering, fields, tracked changes, and complex drawing effects can be lost.

PDF support is designed for reading modes and extraction. It is not a promise of pixel-perfect rendering for every PDF producer. Vector graphics, forms, annotations, and image codecs outside the supported extraction contract may be omitted. The original PDF bytes are retained for an application that provides its own facsimile renderer.

DjVu visual reading covers direct JPEG pages, JB2 masks encoded with direct symbols, matched refinements, symbol copies, and shared-dictionary inheritance, plus progressive IW44 BG44 and FG44 layers. IW44 supports v1.0 through v1.2, including photo pages (reduced BG44 layers are expanded bilinearly), compound color pages selected by an Sjbz mask, and FGbz pages whose color recoding is ignored so the mask renders as black ink over the background. Layer decoding is bounded by chunk, slice, coefficient-work, cumulative-pixel, and raster-byte limits. readDjvuMetadata validates the container and page INFO without decoding its image layer. Text and pixel output are cross-checked against djvulibre 3.5.29: page text decodes byte-identically to djvutxt across the tracked public-domain corpus, and mask and photo pages render byte-identically to ddjvu; composed MRC pages use a deterministic hard mask selection whose bounded channel deviation from ddjvu's blended stencil is pinned by raster-hash goldens.

TIFF and BigTIFF reading covers multi-page IFD chains in both byte orders and 64-bit BigTIFF offsets, with pages expanded to BMP images. Raster codecs: uncompressed, PackBits, LZW, and Deflate/AdobeDeflate (with horizontal differencing predictors) for gray, RGB, and bilevel samples; CCITT Group 4 and Group 3 through a strict row-oriented T.4/T.6 decoder (including EOL-less producers and a Group 4 page decoded pixel-identically against libtiff); JPEG passthrough when a page holds a single self-contained strip. Old-style bit-reversed compat LZW decodes through libtiff's detection heuristic. Layouts outside the contract fail with TiffUnsupportedCodecException: tiled raster, planar=2, palette, float/SampleFormat≠1, LogLuv, JPEGTables-abbreviated strips, old-style JPEG, reversed FillOrder, and uncompressed-mode extensions; malformed containers fail with TiffException before any declared allocation. Pixels are verified against libtiff by tiffcmp parity over the tracked corpus (libtiff test images plus generated fixtures; see test/resources/tiff/PROVENANCE.md).

Input and archive boundaries

  • Every ZIP-backed format goes through the same bounded decoder. Before inflation, unseal rejects containers declaring more than 1 GiB in total, entries larger than 512 MiB, or an expansion ratio above 200x once the declared output exceeds 32 MiB. The materialized output is bounded again, so forged central-directory sizes cannot bypass the limit. Encrypted entries, symbolic links, and compression methods other than stored or deflated are rejected with InvalidBookException.
  • CBR supports a deliberate subset of RAR. Unsupported compression modes, solid or split archives, and encrypted entries are rejected.
  • CB7 entries are bounded and parsed asynchronously. Unsupported codecs and encrypted entries are rejected.
  • AZW4 requires an unencrypted PDF payload. EPUB font obfuscation supports only the standard IDPF and Adobe algorithms.
  • HTMLZ selects one top-level HTML spine file. Other HTML files remain resources.
  • TXT/TXTZ conversion covers basic Markdown/Textile constructs, not every extension of either format.
  • Standalone Markdown input is limited to 16 MiB and front matter to 16 KiB. Relative image paths stay in the generated HTML; the parser does not load neighboring files. Markdown HTML is not sanitized.
  • RTF input is limited to 64 MiB, group depth to 128, and generated HTML to 64 MiB. Embedded pictures and advanced layout are skipped. High bytes in unsupported declared codepages are rejected; Unicode escapes still work.
  • PalmDOC input is limited to 64 MiB, with each text record limited to 4096 decoded bytes. Unsupported Palm database ebook subtypes are rejected.
  • eReader PDB input is limited to 64 MiB, 8192 records, 8192 compressed bytes and 65535 decoded bytes per text record. DRM-protected books are rejected.
  • Plucker PDB input is limited to 64 MiB, 8192 records, 8192 compressed bytes and 32768 decoded bytes per text record. Images are limited to 512 records, 480,000 source bits per bitmap, 60,000 source bytes per bitmap, and 32 MiB of generated PNG output.
  • CHM input is limited to 128 MiB, individual entries to 16 MiB, directory entries to 10,000, and total materialized content to 128 MiB. Unsupported LZX layouts and malformed directory/reset tables fail with typed errors. LZX work is capped at 8192 decoded frames, with frames reused within a reset interval. Intel E8 addresses are restored per frame. Windows-1251, Windows-1252, and Windows-1256 are selected from supported #SYSTEM locale IDs; other encodings use the shared text detector.
  • DjVu input is limited to 128 MiB, 1024 pages, 65536 IFF chunks, 100 million declared pixels per page, 32 MiB per directly stored JPEG, and 8 MiB per OCR chunk (uncompressed TXTa or BZZ-decompressed TXTz). JB2 decoding further caps a mask at 16 megapixels, one symbol at 4 megapixels, cumulative symbol decoding and blitting at 64 megapixels each, stored symbols at 16 megapixels total, records at 65536, arithmetic-number context nodes at 1,048,576, and comments at 64 KiB. Shared dictionaries add a 64-megapixel per-document cap charged before each symbol bitmap is allocated; a shared dictionary that itself inherits another dictionary is rejected typed. BZZ streams (TXTz, NAVM, DIRM names) validate every declared block size against the 4 MiB format limit and the chunk's output cap before allocating, and NAVM record streams are bounded by their 16-bit bookmark count with child claims verified against the record pool. Across one document, decoded visual layers are limited to 2048 million pixels and 1 GiB of materialized raster bytes — sized for real scanned books, which the speller corpus document approaches at 1.4 gigapixels; each page reserves this budget before its decoder allocates output. Indirect document directories are rejected over bytes and resolved only by the file-based open path, which loads DIRM-named sibling components (plain file names only, declared sizes matched) from the document's own directory.
  • DjVu chunks that neither carry displayable content nor replace a supported layer are ignored: ANTz/ANTa annotations, CIDa identifiers, TH44 thumbnails, BMKz compressed bookmarks, and FGbz blit recoloring. Chunks that replace a supported layer — Smmr MMR masks and FGjp JPEG foregrounds — fail with DjvuUnsupportedCodecException. Measured across the tracked corpus and the 1,196-page stress set, these cover every chunk observed in the wild except the supported ones; BMKz, TXTa, BGjp, FGjp, and Smmr never appear in the corpus files.
  • LRF input is limited to 128 MiB, 20,000 indexed objects, 10,000 pages, 32 MiB per expanded stream, 64 MiB of total expanded content, 4 Mi code units of rendered HTML per page, and 64 Mi code units of rendered HTML per book.
  • LRX2 metadata reads are limited to 128 MiB and 1,024 outer boxes. The parser rejects LRX book content as DRM-protected and does not process its protected payload. Unsupported tags and stream flags fail with typed exceptions.
  • TIFF/BigTIFF input is limited to 128 MiB, 1,024 pages, 65,536 IFD entries, 4 samples per pixel, a 256-megapixel page, a million strips or tiles per page, 256 megapixels of decoded pixels per document, and 128 MiB of decoded raster per document. Strip and tile offsets and byte counts are validated against the file bounds before any allocation, and IFD chains are cycle-checked. Unsupported codecs and layouts fail with typed exceptions.
  • EPUB navigation is optional. A damaged or missing table of contents can produce an empty navigation model while readable spine content remains available.
  • Generic archive formats do not all share the same repository-defined resource budget. Format-specific limits are intentional safeguards, not a universal guarantee for arbitrary input sizes.

Some examples of explicit adapter budgets are a 64 MiB plain-text limit, a 128 MiB TXTZ input limit, a 128 MiB CB7 entry limit, and a 512 MiB AZW4 container/PDF limit. These values are implementation safeguards and may change with future releases.

Content safety

HTML is preserved rather than sanitized. Applications must sanitize or isolate untrusted HTML before inserting it into a privileged web view or native HTML component.

DRM is not bypassed. DRM-protected MOBI/AZW3 books, unsupported EPUB encryption, and encrypted archive entries are rejected. Supported PDF standard-security documents can be opened with the correct password. After a successful open, Unseal does not enforce or expose copy, print, or extraction permission flags.

Errors

The public API uses typed exceptions so callers can distinguish unsupported input from corrupted or protected content. Common types include:

  • FormatNotSupportedException for unknown formats or known formats outside the supported contract.
  • InvalidBookException for corrupted input of a detected format.
  • DrmProtectedException for rejected DRM-protected books.
  • ComicException, Comic7Exception, DocxException, HtmlException, and OdtException for format-specific failures.
  • PdfException and PdfEncryptedException for PDF structure and security failures.
  • DjvuException for malformed or excessive DjVu input and DjvuUnsupportedCodecException when a page cannot be displayed.
  • EpubException, MobiException, and Fb2Exception for parser-specific errors.

Empty inputs, corrupt ZIP containers, and excessively deep PDF or FB2 structures are normalized to these typed failures instead of leaking parser implementation errors such as RangeError, FormatException, or stack overflows.

Design principles and inspirations

The project is guided by a few practical goals:

  1. Preserve usable reading content when optional metadata or navigation is damaged.
  2. Give applications one predictable model instead of one renderer contract per file format.
  3. Prefer portable pure-Dart implementations over platform executables.
  4. Put explicit bounds around decompression, image decoding, and recovery where an adapter needs protection from malformed or unusually large input.
  5. Keep format specifications, behavioral compatibility references, and implementation dependencies visibly separate.

Compatibility work draws on:

  • Observable metadata and conversion conventions from Calibre's TXT, HTMLZ, AZW4, and comic importers. Calibre is GPLv3 and is a behavioral reference, not a runtime or source-code dependency. Unseal's implementations are independent pure Dart.
  • Microsoft Open XML package guidance for DOCX parts and relationships.
  • koni_archive for the pure-Dart 7-Zip implementation used by CB7 and CBC.
  • PDF.js and Poppler as independent references for PDF extraction and image behavior.
  • The DjVu 3 specification for IFF structure, JB2 records, ZP coding, and the BZZ byte stream (BWT, modified MTF-4, and the 262-context decision tree). JB2 ZP state-table constants and the BZZ decode order were cross-checked against the independent MIT-licensed djvu-rs implementation; its notice is in licenses/djvu-rs-MIT.txt. No DjVuLibre GPL source is copied into this parser; behavioral parity comes from the spec plus the public-domain corpus verified against local djvulibre renders.

Development

Use a source checkout and the SDK pinned in .dart-version:

dart pub get --no-example
dart analyze --fatal-infos
dart test --exclude-tags fuzz

The development guide covers browser workers, fuzz, fixture provenance, performance measurements, and the release commands. Read the architecture before changing module or safety boundaries. Use the codebase guide to find a format's implementation and the roadmap for pending work.

The Flutter example demonstrates the public API by loading a bundled EPUB and showing its parsed summary.

License

MIT

Libraries

azw4
AZW4 PDF-wrapper parsing entry points.
chm
Microsoft Compiled HTML Help (CHM) parsing entry points.
comic
Comic archive parsing entry points.
comic7
CB7 and CBC comic parsing entry points.
djvu
DjVu parsing entry points and page model.
docx
DOCX parsing entry points.
epub
EPUB parsing entry points and EPUB-specific document contracts.
ereader
eReader PDB parsing entry points.
fb2
FictionBook 2 parsing entry points.
html
HTML and HTMLZ parsing entry points.
lrf
Sony BBeB/LRF parsing entry points.
lrx
Sony BBeB/LRX DRM detection and plaintext metadata entry points.
markdown
Standalone Markdown parsing entry points.
mobi
MOBI and AZW3 parsing entry points.
odt
OpenDocument Text parsing entry points.
palmdoc
PalmDOC parsing entry points.
pdf
PDF parsing entry points.
plucker
Plucker PDB text parsing entry points.
rtf
Rich Text Format parsing entry points.
tiff
TIFF and BigTIFF parsing entry points and page model.
txt
TXT and TXTZ parsing entry points.
unseal
Public, format-agnostic Unseal API.