Reading a book
read takes the bytes of an EPUB 2 or 3 file and returns a Book:
import "@deckyard/epub/" as epub;
def b as epub.Book init epub.read(fs.readBytes("book.epub"));
io.printf("%s (%s), %d chapters\n", $b.title, $b.lang, len($b.chapters));
for (def c in $b.chapters) {
io.printf("## %s\n\n%s\n\n", $c.title, epub.text($c));
}examples/read_book.j prints a book's metadata, contents, files and the start of every chapter.
What comes back
- Metadata:
title,lang,identifier(the package's unique identifier),authors(everydc:creator, in order),description,publisher,date,modified(""in an EPUB 2 book), andversion, as the package declares it ("2.0","3.0"). direction:"ltr"or"rtl"from the spine'spage-progression-direction,""when it has none (ordefault).compress: true when an entry other thanmimetypeis deflated, so a rebuilt book is compressed as the original was.landmarks: the navigation document'slandmarks(Landmark{type, title, href}), else the EPUB 2<guide>, its types mapped to EPUB 3 names where they differ (textisbodymatter,title-pageistitlepage). Landmarks that are exactly the onesbuildgenerates come back as none.- Media overlays: each chapter's
mediaOverlay(the manifest id of its SMIL file, which is among the resources with its audio) and the package'smedia:metadata inmediaMeta. pages: the navigation document'spage-list, else (in a book read from its NCX) the NCX<pageList>, asPageTarget{label, href}.- Fixed layout:
layout,orientation,spread,flowfrom the package'srendition:metadata, each chapter'sspinePropertiesfrom its spineitemref, andviewportfrom the first chapter that declares one. cover: the href of the cover, from the EPUB 3cover-imageproperty, else the EPUB 2<meta name="cover">naming a picture or a chapter;""when there is none. It is a picture among the resources, or a chapter (an SVG or XHTML cover page).epub.resource($b, $b.cover)gets its bytes.chapters: the spine, in reading order. EachChapterhas itsid,href,mediaType,properties,linear(false for an item markedlinear="no"),content(the whole document) andtitle: the first table of contents entry pointing at it, else its<title>.toc: the table of contents, a flat list ofTocEntry{title, href, level}wherelevelgives the nesting. It comes from the EPUB 3 navigation document (<nav epub:type="toc">), else from the EPUB 2toc.ncx. A heading without a link has href"".resources: every other file in the manifest (stylesheets, pictures, fonts), with its bytes. The navigation document and the NCX are not among them (tocstands for them, andncxsays whether the book carried one), nor is a remote resource (anhttps://audio file), which has no bytes in the archive. A navigation document a chapter links into (an index pointing at its entries) is the exception: it is kept as a file, with itsnavproperty, so the links still lead somewhere when the book is written again.
Every href is relative to the package document, decoded (a%20b.xhtml is a b.xhtml). A link inside a chapter is relative to the chapter; resolve turns it into an href:
def c as epub.Chapter init $b.chapters[3]; # text/ch03.xhtml
def pic as epub.Resource init epub.resource($b, epub.resolve($c.href, "../img/fig1.png"));Plain text
text(chapter) gives the text of the chapter's <body> in reading order: one paragraph per block (<p>, a heading, a list item, a table row) with a blank line between them, whitespace collapsed as a browser would, <br/> as a line break, <pre> as written, and scripts, styles and the <head> left out. HTML entities are decoded: becomes U+00A0, — an em dash. A picture in the spine has no text.
Strict and lenient
The container and the package are what the book stands on, so they are read strictly: there must be a mimetype entry saying application/epub+zip; META-INF/container.xml must be well-formed XML in the OCF namespace and name a package document; the package document must be well-formed, in the OPF namespace, with a title, a language, the unique identifier it names, a manifest, and a non-empty spine of items from the manifest whose files are in the archive. Anything else is an EpubError naming what is wrong.
Tolerances, and notes
Some faults are common in books that readers open anyway. read tolerates these, and never silently: each one it applies is a line in the book's notes, which is empty for a book that keeps to the format (every book this deck builds):
mimetypeimperfect in form: deflated, not the first entry, or followed by whitespace or a newline, as long as it saysapplication/epub+zip. A missing one, or other content, is refused.- A legacy charset a document declares: a chapter or the navigation document that is not UTF-8 or UTF-16 and declares an ISO-8859 or Windows code page (in its XML declaration,
<meta charset>orhttp-equivContent-Type) is decoded with it. The package document and the NCX may declare one in their XML declaration only. A document that is valid UTF-8 is read as UTF-8 whatever it declares, since a Latin-1 label on UTF-8 text is the common mistake and the other reading would garble it; when it declares a legacy charset and is not plain ASCII, that mismatch is a note too. Undeclared, or in another charset, the document is refused (reason"content", or"package"/"toc"). - A manifest item whose file the archive lacks is left out of the book. A spine item that names one is still an error.
- A manifest href that matches an archive name only when case is ignored reads that file (the href stays as the manifest writes it). If several names match, the book is refused.
- A spine item that is no XHTML or SVG document (and no picture) with a manifest
fallbackis read as the first document down its fallback chain. A chain that loops or ends without one is refused (reason"package"). A picture in the spine stays a picture, fallback or not: the fallback of a comic page is usually one generic page for all of them.
def b as epub.Book init epub.read($data);
for (def n in $b.notes) {
io.eprintf("note: %s\n", $n);
}A manifest href starting with / names a file from the container root, as a URL would; it is not joined to the package document's directory.
Chapters are where real-world books are untidy: HTML entities such as , unclosed <p> and <br>, stray tags. A chapter the xml parser rejects is not an error. It keeps its content as it is, has wellFormed false, and is read with the bundled html parser for its title and its text. The navigation document is always read that way. A chapter is UTF-8, UTF-16 with a byte order mark (the encodings XML and EPUB allow), or a declared legacy charset (above).
A picture placed in the spine itself (a fixed-layout comic) is a chapter with no content; its bytes are among the resources.
DRM and obfuscated fonts
A DRM-protected book cannot be read, and read says so with reason "drm": when it carries META-INF/rights.xml (Adobe ADEPT), META-INF/license.lcpl (Readium LCP) or META-INF/sinf.xml (Apple FairPlay), or when META-INF/encryption.xml lists a file encrypted with anything but font obfuscation.
Font obfuscation is not DRM: it only stops a font being lifted out of the book as a file. Both kinds are undone, so a font resource holds the real font: the IDPF algorithm (the first 1040 bytes XORed with the SHA-1 of the unique identifier) and Adobe's (the first 1024 bytes XORed with the identifier's UUID).
Caps
Every limit is checked before the work it guards, and a reached one is a plain Error of kind "limit":
| Cap | Default | Guards |
|---|---|---|
maxBytes | 128 MiB | the EPUB file itself |
maxEntries | 10000 | the number of files in the archive |
maxEntryBytes | 64 MiB | one file, unpacked |
maxTotalBytes | 256 MiB | all files together, unpacked (a zip bomb) |
read uses the defaults; readWith takes a ReadOptions from readOptions(). A field at 0 takes the default and a negative one lifts the cap:
def o as epub.ReadOptions init epub.readOptions();
$o.maxBytes = 8 * 1024 * 1024;
def b as epub.Book init epub.readWith($data, $o);The archive is unpacked with the archive library, which also refuses a member named with an absolute path or a .. segment (a "zip slip") and symbolic links; the deck turns those into reason "archive". Parsing is bounded too: the xml and html parsers cap nesting depth and node count, and their "limit" passes through unchanged. A chapter past the xml cap is read as HTML instead, as long as the html parser (512 levels) takes it.
On the tiny build (jennifer-tiny, declared and tested) the deck works the same, within that build's smaller limits: the xml parser nests at most 64 levels (1000 on the default build), so a package document or NCX deeper than that is refused as "limit", a chapter deeper is read as HTML (wellFormed false), a chapter body given to chapter may nest about 60 levels, and a table of contents at most 28 (100 on the default build); and the interpreter allows less memory for copies, so the largest books (2,000 chapters with a navigation document to match, about 36 MB unpacked) stop with "limit" while the bundled html module parses that navigation document.