epub_rtl
A pure-Flutter EPUB 2/3 parser and reader that handles right-to-left books. It is built for Arabic and other RTL texts, and it uses no WebView.
Features
Parser (pure Dart, depends only on archive and xml)
- Finds the package document through
META-INF/container.xml. - Reads the OPF:
- metadata: titles, creators with role and file-as (EPUB 2 attributes or
EPUB 3 refinements), languages, identifiers, publisher, date,
description,
dcterms:modifiedand the cover image; - the manifest;
- the spine, including
linearandpage-progression-direction.EpubBook.isRtluses the spine direction, and falls back to the book language when the spine declares none.
- metadata: titles, creators with role and file-as (EPUB 2 attributes or
EPUB 3 refinements), languages, identifiers, publisher, date,
description,
- Navigation: reads the EPUB 3 nav document (
toc,landmarks,page-list), and falls back to the EPUB 2 NCX (navMap,pageList) with the<guide>as landmarks. - Content access by href:
- resolves relative paths (
..,./, percent-encoding, fragments); - reads entries lazily: only the zip directory is read up front, and each chapter or resource is inflated when it is requested and not cached;
EpubBook.openFilereads entries from disk on demand.
- resolves relative paths (
- Errors:
- malformed books throw a typed
EpubExceptionwith anEpubErrorKind; lenient: truerecovers from common real-world errors and records them inwarnings. These include a missing or misplaced mimetype, a missing NCX or nav document, missing or unknown media types, dangling spine references and wrong letter case in paths.
- malformed books throw a typed
- Encryption: a book with DRM listed in
META-INF/encryption.xmlis refused withEpubErrorKind.encrypted. A book whose only encrypted resources are obfuscated fonts opens in lenient mode, and those fonts are unreadable.
Content model (pure Dart): EpubChapter turns XHTML into a flat list
of blocks. Each id maps to a block index, which is what locations point to.
- Blocks: headings, paragraphs,
pre, images (including SVG-wrapped covers), lists, blockquotes, basic tables and rules. - Inline content: emphasis, bold, underline, strike, sup/sub, code, links and basic ruby.
dirandlangare inherited.dir="auto"uses the first strong character.- Inline
diris wrapped in bidi isolates (LRI/RLI … PDI),bdiin FSI … PDI, andbdoin LRO/RLO … PDF. - A CSS subset from linked stylesheets,
<style>andstyle="":- properties:
text-align,font-style,font-weight,margin*,text-indent,directionanddisplay: none; - selectors: tag, class, id and descendant.
- properties:
Reader (Flutter)
EpubReaderViewhas two modes:scroll: one continuous vertical scroll through the whole book;paginated: one chapter per page, swiped in the book's reading direction. RTL books advance with a right swipe.
- Reading position:
EpubLocation(spine index + element index + offset fraction). You can serialise it withtoJsonorencode, restore it withinitialLocation, and get it throughonLocationChangedand the controller. EpubReaderController:jumpTo,jumpToHref(TOC entries and internal links),nextChapter,previousChapter.EpubTocDrawer: a TOC drawer in the book's direction.EpubReaderSettings:- font size and line height;
- light, sepia and dark themes;
arabicFontFamily, used for RTL-language runs and first in the fallback list;- justification.
- Jumps inside a chapter use
GlobalKeys andScrollable.ensureVisible. The package does not depend on the archivedscrollable_positioned_list. - Internal links jump inside the book. External links go to
onExternalLink.
Install
dependencies:
epub_rtl: ^0.1.0
Usage
import 'package:epub_rtl/epub_rtl.dart';
import 'package:flutter/material.dart';
class ReaderScreen extends StatefulWidget {
const ReaderScreen({super.key, required this.bytes, this.saved});
final List<int> bytes;
final String? saved; // a previously stored EpubLocation.encode()
@override
State<ReaderScreen> createState() => _ReaderScreenState();
}
class _ReaderScreenState extends State<ReaderScreen> {
late final EpubBook book = EpubBook.fromBytes(widget.bytes, lenient: true);
final controller = EpubReaderController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(book.metadata.title)),
drawer: EpubTocDrawer(book: book, controller: controller),
body: EpubReaderView(
book: book,
controller: controller,
mode: EpubReaderMode.paginated,
settings: const EpubReaderSettings(
fontSize: 20,
theme: EpubReaderTheme.sepia,
arabicFontFamily: 'Amiri', // bundle the font in your app
justify: true,
),
initialLocation:
widget.saved == null ? null : EpubLocation.tryDecode(widget.saved!),
onLocationChanged: (location) {
// Persist location.encode() or location.toJson().
},
onExternalLink: (url) {
// Open with url_launcher, for example.
},
),
);
}
Parser only:
final book = EpubBook.fromBytes(bytes);
print(book.metadata.title);
print(book.spine.pageProgressionDirection); // PageProgressionDirection.rtl
for (final entry in book.navigation.toc) {
print('${entry.title} -> ${entry.href}');
}
final chapter = EpubChapter.fromBook(book, 0);
print(chapter.blocks.length);
Migrating from epubx / epub_view
epubx has had no release since June 2023. epub_view depends on it and on
the archived scrollable_positioned_list.
| epubx / epub_view | epub_rtl |
|---|---|
EpubReader.readBook(bytes) (async, loads everything) |
EpubBook.fromBytes(bytes) (sync, lazy) |
book.Title, book.Author |
book.metadata.title, book.metadata.creators |
book.Chapters |
book.navigation.toc (titles + hrefs) and book.spine.items |
chapter.HtmlContent |
book.chapterHtml(i) or EpubChapter.fromBook(book, i) |
book.Content.Images[name] |
book.readBytes(fullPath) |
EpubView(controller: EpubController(document: ...)) |
EpubReaderView(book: book, controller: EpubReaderController()) |
EpubController.generateEpubCfi() / gotoEpubCfi() |
controller.location (EpubLocation) / controller.jumpTo(location) |
EpubViewTableOfContents |
EpubTocDrawer |
Locations are not EPUB CFIs. Saved epub_view CFIs cannot be converted.
Limitations
- DRM-encrypted books are refused. Obfuscated embedded fonts are not
de-obfuscated, and embedded fonts are not loaded. Use
fontFamilyandarabicFontFamilywith fonts bundled in your app. - Paginated mode shows one chapter per page, and the page scrolls vertically. The text is not split into screen-sized pages.
- The CSS support is a small subset: no floats, positioning, colours,
font sizes from CSS, pseudo-classes,
>/+/~selectors or@mediarules. - Tables are basic: no
colspanorrowspan. - Ruby is shown as annotation-over-base, without full ruby layout.
- SVG images, audio, video, MathML and scripts are not rendered. Fixed-layout EPUBs are rendered as reflowable text.
- Each chapter is built as one widget subtree so element keys exist for jumps. Very long single chapters cost more to lay out.
EpubBook.openFileneedsdart:ioand is not available on the web. UsefromBytesthere.
Data sources
No third-party book content is bundled. The test fixtures and the example's sample book are generated in code, with text written for this package.
License
MIT, see LICENSE.
Made by Abdeldjalil Chougui.
Libraries
- epub_rtl
- Pure-Flutter EPUB 2/3 parser and reader with right-to-left support.