
Quarto
a book size of about 9½ × 12 inches (24 × 30 centimetres), determined by folding printed sheets twice to form four leaves or eight pages.
Generate EPUB3 and Kobo kepub files from HTML — entirely in memory, with no native dependencies and no external binaries.
import { writeFile } from "node:fs/promises";
import { generateEpub, toKepub } from "@voidberg/quarto";
const epub = await generateEpub({
title: "On the Shortness of Life",
author: "Seneca",
chapters: [{ title: "I", html: "<p>It is not that we have a short time to live…</p>" }],
});
await writeFile("seneca.epub", epub); // or Deno.writeFile, Bun.write…
await writeFile("seneca.kepub.epub", toKepub(epub));Why another EPUB library?
Quarto came out of specific needs in my own projects (like instakobo):
- skip the table of contents, which for articles and newsletters is just noise
- generate kepubs without relying on kepubify
- work in the browser
Features
- Valid EPUB3 (verified against EPUBCheck in CI)
- Optional table of contents (
includeToc) - Native kepub conversion (
toKepub) — Kobo reading-location spans, no binary needed - In-memory: returns a
Uint8Array, never touches the filesystem - Runtime-agnostic: Node, Deno, Bun, and the browser (Web APIs +
fflate) - Re-serializes messy HTML into well-formed XHTML for you
- Downloads and embeds remote images so the book is self-contained
- Modern ESM-only (Node ≥ 18;
require()-able from CommonJS on Node ≥ 20.19 / 22)
Used by
- instakobo — Read and annotate (and sync back) your Instapaper articles on your Kobo device
- reSafari — Send webpages to your reMarkable tablet from Safari
Install
npm install @voidberg/quarto # npm / pnpm / yarn
deno add jsr:@voidberg/quarto # Deno (JSR)API
generateEpub(input): Promise<Uint8Array>
| Option | Type | Default | Notes |
|---|---|---|---|
title |
string |
— | Required. |
chapters |
Chapter[] |
— | Required, at least one. |
author |
string | string[] |
— | One or many creators. |
includeToc |
boolean |
true |
false ⇒ no visible TOC page. |
tocTitle |
string |
"Table of Contents" |
Heading on the TOC page. |
cover |
string | Uint8Array |
— | URL or raw bytes; generates a cover page. |
coverFromLeadImage |
boolean |
false |
Promote a chapter's leading image to the cover (see below). |
coverBackground |
string |
reader default | CSS colour filling the cover's letterbox bands. |
language |
string |
"en" |
BCP-47 tag. |
css |
string | false |
bundled stylesheet | false ships no CSS. |
downloadImages |
boolean |
true |
Embed remote <img> sources. |
transformImage |
ImageTransform |
— | Rewrite each image before embedding (see below). |
transformCover |
CoverTransform |
— | Compose/replace the cover before embedding (see below). |
publisher |
string |
— | |
description |
string |
— | |
date |
string (ISO-8601) |
— | Pass for reproducible builds. |
id |
string |
derived (stable UUID) | Unique book identifier. |
fetch |
typeof fetch |
global fetch |
Override for proxies/testing. |
A Chapter is { title, html, excludeFromToc?, author?, insertTitle? }. html is an HTML fragment — it does not need to be well-formed; Quarto parses and re-serializes it as valid XHTML. Set insertTitle: false to suppress the auto-generated <h1> heading and render only your markup.
toKepub(epub: Uint8Array): Uint8Array
Converts an EPUB (such as the output of generateEpub) into a Kobo kepub: every content document is rewritten with koboSpan reading-location markers and Kobo's book-columns / book-inner wrappers. Write the result with a .kepub.epub extension. No kepubify binary required.
DEFAULT_CSS: string
The bundled stylesheet, exported so you can extend rather than replace it.
imageSize(bytes, mime): { width, height } | undefined
Reads an image's pixel dimensions straight from its header — no decoding, no native deps. Supports PNG, GIF and JPEG; returns undefined for anything else or malformed data. Handy inside a transformCover to make layout decisions.
Images & covers
By default every <img> source (and the cover) is downloaded and embedded so the book is self-contained. Two hooks let you customize what gets stored:
transformImage(image)runs on each fetched image before the core-media-type check, so it can transcode an unsupported format (e.g. WebP/AVIF → PNG for older e-readers), resize, or returnnullto drop the image.transformCover(cover, meta)runs after the cover source is downloaded and passed throughtransformImage. It's called even when there's no cover source, so you can compose a designed cover frommeta.title/meta.authoralone. Returnnullfor no cover.
Both receive/return RawImage ({ data: Uint8Array; mime: string }) and may be async.
import { generateEpub, imageSize, type CoverTransform } from "@voidberg/quarto";
const brandCover: CoverTransform = (cover, meta) => {
if (cover && imageSize(cover.data, cover.mime)) return cover; // usable as-is
return renderCover(meta.title, meta.author); // your designer → RawImage
};
await generateEpub({
title: "Field Notes",
coverFromLeadImage: true, // no cover? promote the article's leading image
coverBackground: "#f4f1ea", // blend the letterbox bands into the artwork
transformCover: brandCover,
chapters: [{ title: "Field Notes", html }],
});coverFromLeadImage only promotes an image that appears before any text in the first chapter (and removes it from the body so it isn't shown twice); images that follow text are left in place.
Example: no table of contents
const epub = await generateEpub({
title: "A Single Essay",
includeToc: false,
chapters: [{ title: "Essay", html: essayHtml, excludeFromToc: true }],
});Development
npm install
npm test # vitest
npm run build # tsc → dist (ESM + d.ts)
npm run typecheck
npm run validate # EPUBCheck (needs Java + EPUBCHECK_JAR)EPUBCheck runs in CI against generated fixtures to guarantee spec compliance.
Thanks to
- epub-gen and epub-gen-memory for inspiration.
- kepubify for documenting the kepub transform.
- epub-css-starter-kit for the styles.
- Freepik – Flaticon for the logo.
License
MIT Alexandru Badiu