Skip to main content

@storyteller-platform/epub

A Node.js/browser library for inspecting, modifying, and creating EPUB 3 publications.

Installation​

npm:

npm install @storyteller-platform/epub

yarn:

yarn add @storyteller-platform/epub

deno:

deno install npm:@storyteller-platform/epub

About​

Throughout this library's documentation, there will be many references to the EPUB 3 specification. The lower level APIs exposed by this library require some knowledge of this specification. Here we will cover the very basics necessary to work with the library, but we recommend that users read through the linked specification to gain a deeper understanding of the format.

EPUB Basics​

An EPUB file is a ZIP archive with a partially specified directory and file structure. Most of the metadata and content is specified as XML documents, with additional resources referenced from those XML documents.

The most important of these documents is the package document.

The package document is an XML document that consists of a set of elements that each encapsulate information about a particular aspect of an EPUB publication. These elements serve to centralize metadata, detail the individual resources, and provide the reading order and other information necessary for its rendering.

This library is primarily concerned with providing access to the metadata, manifest, and spine of the EPUB publication. Metadata refers to information about the publication, such as its title or authors. The manifest refers to the complete set of resources that are used to render the publication, such as XHTML documents and image files. And the spine refers to the ordered list of manifest items that represent the default reading order — the order that readers will encounter the manifest items by simply turning pages one at a time.

What this library does​

@storyteller-platform/epub provides an API to interact with the metadata, manifest, and spine of the EPUB publication. There are higher level APIs that mostly abstract away the implementation details of the EPUB specification, like epub.setTitle(title: string) and epub.getCreators(), as well as lower level APIs like epub.writeItemContents(path: string, contents: Uint8Array) and epub.addMetadata(entry: MetadataEntry), which require some understanding of the EPUB structure to utilize effectively.

Because EPUB publications rely heavily on the XML document format, this library also provides utility methods for parsing, manipulating, and building XML documents. The underlying XML operations are based on fast-xml-parser.

Usage​

The entrypoint to the library is through the Epub class. An Epub can either be read from an existing EPUB publication file, or created from scratch.

Reading from a file​

import { Epub } from "@storyteller-platform/epub"

using epub = await Epub.from("path/to/book.epub")
// equivalent to
// using epub = await Epub.using(TmpFsAdapter).from("path/to/book.epub")
console.log(await epub.getTitle())

If you want to be a little more efficient and not unpack the entire EPUB file into disk (which can take some time when dealing with EPUBs with Media Overlays/audio contents), you can instead lazily read the contents of the EPUB file into memory.

import { Epub, MemoryAdapter } from "@storyteller-platform/epub"

using epub = await Epub.using(MemoryAdapter).from("path/to/book.epub")
console.log(await epub.getTitle())

Creating from scratch​

When creating an Epub from scratch, the title, language, and identifier must be provided, as these are required for all publications by the EPUB 3 specification.

Other Dublin Core and non-core metadata may also be provided at creation time, or may be added incrementally after creation.

import { randomUUID } from "node:crypto"

import { Epub } from "@storyteller-platform/epub"

// Implicitly uses the TmpFsAdapter
const epub = await Epub.create("/path/to/book.epub", {
title: "S'mores For Everyone",
// This should be the primary language of the publication.
// Individual content resources may specify their own languages.
language: new Intl.Locale("en-US"),
// This can be any unique identifier, including UUIDs, ISBNs, etc
identifier: randomUUID(),
})

Adding a chapter​

import { Epub, type ManifestItem, x } from "@storyteller-platform/epub"

const epub = await Epub.from("path/to/book.epub")

// Construct a manifest item describing the chapter
const manifestItem: ManifestItem = {
id: "chapter-one",
// This is the filepath for the chapter contents within the
// EPUB archive.
href: "XHTML/chapter-one.xhtml",
mediaType: "application/xhtml+xml",
}

// You can specify the contents as a string
const contents = `<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:epub="http://www.idpf.org/2007/ops"
xml:lang="en-US"
lang="en-US">
<head></head>
<body>
<h1>Chapter 1</h1>
<p>At first, there were s'mores.</p>
</body>
</html>`

// Or you can specify the contents as an XML structure
const xmlContents = await epub.createXhtmlDocument([
x("h1", null, "Chapter 1"),
x("p", null, "At first, there were s'mores."),
])

// Or, using JSX
/** @jsxImportSource @storyteller-platform/epub */
const xmlContents = await epub.createXhtmlDocument([
<h1>Chapter 1</h1>,
<p>At first, there were s'mores.</p>,
])

// First, add the new item to the manifest, and add
// its contents to the publication
await epub.addManifestItem(manifestItem, contents, "utf-8")

// OR, using the XMl:
await epub.addManifestItem(manifestItem, xmlContents, "xml")

// Then add the item to the spine
await epub.addSpineItem(manifestItem.id)

Writing to disk​

import { Epub } from "@storyteller-platform/epub"

const epub = await Epub.from("path/to/book.epub")
await epub.setTitle("S'mores for Everyone")

await epub.saveAndClose()

For more details about using the API, see the API documentation.

Upgrading an EPUB 2 to EPUB 3​

This library only supports operations on EPUB 3 files and will throw an error if you attempt to load an EPUB 2 file.

However, it does support upgrading an EPUB 2 file to EPUB 3, after which you can use the library like normal.

import { Epub } from "@storyteller-platform/epub"

using epub = await Epub.upgrade("path/to/epub2.epub", {
// optional. if omitted the Epub
outputPath: "path/to/epub3.epub",
})

// if .saveAndClose() is not called and `outputPath` isn't provided, the Epub will return to it's unupgraded state
await epub.saveAndClose()

// Do Epub stuff

Browser support​

This library is designed to be used in both Node.js and browser environments.

In the browser, currently you can only use the MemoryAdapter to read EPUB files.

import { useState, useEffect } from "react"

import {
Epub,
type EpubReader,
MemoryAdapter,
} from "@storyteller-platform/epub"

export function BookCover(file: File) {
const [cover, setCover] = useState<string | null>(null)

useEffect(() => {
let epub: EpubReader | null = null
let coverUrl: string | null = null

async function loadCover() {
// unfortunately Safari does not support `using` syntax yet
try {
epub = await Epub.using(MemoryAdapter).from(file)
const cover = await epub.getCoverImage()
if (cover) {
coverUrl = URL.createObjectURL(new Blob([new Uint8Array(cover)]))
setCover(coverUrl)
}
} finally {
epub?.discardAndClose()
}
}

loadCover()

return () => {
if (coverUrl) {
URL.revokeObjectURL(coverUrl)
}
}
}, [file])

return cover ? <img src={cover} alt="Cover" /> : null
}

Note that doing a bare Epub.from will result in an error.

Development​

This package lives in the Storyteller monorepo, and is developed alongside the Storyteller platform.

To get started with developing in the Storyteller monorepo, check out the development guides in the docs.