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.

pub package License: MIT

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:modified and the cover image;
    • the manifest;
    • the spine, including linear and page-progression-direction. EpubBook.isRtl uses the spine direction, and falls back to the book language when the spine declares none.
  • 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.openFile reads entries from disk on demand.
  • Errors:
    • malformed books throw a typed EpubException with an EpubErrorKind;
    • lenient: true recovers from common real-world errors and records them in warnings. 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.
  • Encryption: a book with DRM listed in META-INF/encryption.xml is refused with EpubErrorKind.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.
  • dir and lang are inherited. dir="auto" uses the first strong character.
  • Inline dir is wrapped in bidi isolates (LRI/RLI … PDI), bdi in FSI … PDI, and bdo in LRO/RLO … PDF.
  • A CSS subset from linked stylesheets, <style> and style="":
    • properties: text-align, font-style, font-weight, margin*, text-indent, direction and display: none;
    • selectors: tag, class, id and descendant.

Reader (Flutter)

  • EpubReaderView has 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 with toJson or encode, restore it with initialLocation, and get it through onLocationChanged and 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 and Scrollable.ensureVisible. The package does not depend on the archived scrollable_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 fontFamily and arabicFontFamily with 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 @media rules.
  • Tables are basic: no colspan or rowspan.
  • 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.openFile needs dart:io and is not available on the web. Use fromBytes there.

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.