Unseal
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. |
| 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:
BookMetadatafor title, authors, language, publisher, identifiers, subjects, dates, series, and cover data.Filesfor HTML content, stylesheets, images, fonts, and binary resources.ReadingOrderItemandNavigationfor the spine and table of contents.BookStatisticsandTextFile.plainTextfor word counts and estimated reading time.- Format-specific models such as
EpubBook,MobiBook,DocumentBook,ComicBook, andPdfBookwhen 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
#SYSTEMlocale 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/ANTaannotations,CIDaidentifiers,TH44thumbnails,BMKzcompressed bookmarks, andFGbzblit recoloring. Chunks that replace a supported layer —SmmrMMR masks andFGjpJPEG foregrounds — fail withDjvuUnsupportedCodecException. 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, andSmmrnever 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:
FormatNotSupportedExceptionfor unknown formats or known formats outside the supported contract.InvalidBookExceptionfor corrupted input of a detected format.DrmProtectedExceptionfor rejected DRM-protected books.ComicException,Comic7Exception,DocxException,HtmlException, andOdtExceptionfor format-specific failures.PdfExceptionandPdfEncryptedExceptionfor PDF structure and security failures.DjvuExceptionfor malformed or excessive DjVu input andDjvuUnsupportedCodecExceptionwhen a page cannot be displayed.EpubException,MobiException, andFb2Exceptionfor 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:
- Preserve usable reading content when optional metadata or navigation is damaged.
- Give applications one predictable model instead of one renderer contract per file format.
- Prefer portable pure-Dart implementations over platform executables.
- Put explicit bounds around decompression, image decoding, and recovery where an adapter needs protection from malformed or unusually large input.
- 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_archivefor 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 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.