epub_rtl 0.1.0
epub_rtl: ^0.1.0 copied to clipboard
Pure-Flutter EPUB 2/3 parser and reader with right-to-left support for Arabic books: page-progression-direction, dir, bidi, paginated RTL paging.
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.