adrorocker/epub-builder 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

adrorocker/epub-builder

Composer 安装命令:

composer require adrorocker/epub-builder

包简介

Convert Tiptap JSON to valid EPUB 3.3 files with an injectable renderer interface

README 文档

README

tests

Convert Tiptap JSON documents into valid EPUB 3.3 files. Framework-agnostic, zero runtime dependencies, with an injectable renderer interface so you can plug in your own Tiptap-to-XHTML logic.

  • Spec-conformant: passes the W3C reference validator (EPUBCheck 5.x) with 0 errors / 0 warnings.
  • Cross-reader: tested against Apple Books, calibre, and the EPUBCheck reference. Ships an EPUB 2 NCX fallback for legacy readers (older Kindle apps, ADE pre-v4).
  • Tiptap-native: a default renderer covers the standard Tiptap node and mark set. Custom Tiptap nodes can be registered with a one-line handler.
  • No third-party runtime deps: only PHP 8.1+ and the zip and dom extensions.

Installation

composer require adrorocker/epub-builder

Requirements: PHP 8.1+, ext-zip, ext-dom.

Quick start

<?php

use AdroSoftware\EpubBuilder\EpubBuilder;

$poemJson = json_decode(file_get_contents('poem.json'), true);

$output = EpubBuilder::create()
    ->setMetadata(fn($m) => $m
        ->title('Selected Poems')
        ->author('Robert Frost')
        ->language('en')
        ->coverImage('/path/to/cover.jpg')
    )
    ->addChapter('The Road Not Taken', $poemJson)
    ->addChapter('Stopping by Woods', $poemJson)
    ->build('/output/path/book.epub');

echo $output->getPath();   // /output/path/book.epub
echo $output->getSize();   // 2,760,000 (bytes)

That's a complete, valid EPUB. Open in any reader.

Metadata

All metadata is set through a fluent builder. Title, language, and at least one chapter are required; everything else is optional.

$output = EpubBuilder::create()
    ->setMetadata(fn($m) => $m
        // Required
        ->title('White Nights and Other Stories')
        ->language('en')

        // Recommended
        ->author('Fyodor Dostoyevsky')
        ->author('Other Co-author')           // call again for multiple authors
        ->publisher('epubBooks Classics')
        ->description('A collection of short stories.')
        ->rights('Public domain')
        ->coverImage('/path/to/cover.jpg')

        // Genre / category tags (Apple Books, Google Play, calibre use these)
        ->subject('Classic Fiction')
        ->subject('Short Stories')

        // Contributors with MARC relator role codes (see table below)
        ->contributor('Constance Garnett', 'trl')   // translator
        ->contributor('Anne Editor', 'edt')         // editor

        // Optional extras
        ->isbn('978-3-16-148410-0')
        ->series('My Collection', 1)
        ->direction('ltr')                          // 'ltr' (default) or 'rtl'
        ->identifier('urn:uuid:...')                // defaults to a generated UUIDv4
        ->modified(new \DateTimeImmutable())        // defaults to now (UTC)
    )
    ->addChapter('Chapter One', $tiptapJson)
    ->build();

MARC relator codes

contributor() takes an optional second argument — a MARC relator code describing the contributor's role. Common ones:

Code Role
trl Translator
edt Editor
ill Illustrator
pht Photographer
aui Introduction
aft Afterword
fwd Foreword
cmp Compiled by
nrt Narrated by
oth Other (default)

These codes are emitted into the OPF and translated to readable labels ("Translated by", "Edited by", …) on the auto-generated cover page.

Chapters

Each call to addChapter(string $title, array $tiptapJson) adds one chapter:

  • The title becomes the chapter's <h1> and a top-level entry in the table of contents.
  • The Tiptap JSON is rendered to XHTML using the default renderer (or a custom one — see below).
  • Inner Tiptap headings nest under the chapter title in the TOC.
  • The chapter is auto-registered as a page in the EPUB 3 page-list nav (so readers can show "page N of M"). Disable with ->disableAutoPageList().
  • The chapter body is wrapped in <section epub:type="chapter"> for accessibility.
$builder
    ->addChapter('Introduction', $intro)
    ->addChapter('Chapter One', $ch1)
    ->addChapter('Chapter Two', $ch2);

Cover page

When you set a cover image, the library auto-generates a title page (OEBPS/chapters/cover.xhtml) showing:

  1. The cover image, full-bleed
  2. The book title (large, centered)
  3. Authors ("by Robert Frost & Jane Doe")
  4. Contributors with role labels ("Translated by Constance Garnett")
  5. Publisher
  6. Year
  7. Rights / copyright line
  8. Description (justified, smaller text)

This is rendered XHTML, so it shows up the same in every reader — even readers that don't surface OPF metadata in their library/info UIs.

->setMetadata(fn($m) => $m
    ->title('Book')
    ->coverImage('/path/to/cover.jpg')   // jpg, png, gif, webp, svg
);

The cover is automatically:

  • Added to the OPF manifest with properties="cover-image" (EPUB 3) and <meta name="cover"> (EPUB 2 legacy).
  • Placed first in the spine with linear="no" so it appears as cover artwork only, not as the first reading-flow page.
  • Added to the EPUB 3 landmarks nav and the legacy <guide> block.

Output

build() returns an EpubOutput value object:

$output = $builder->build('/optional/explicit/path.epub');

$output->getPath();         // string — path on disk
$output->getSize();         // int    — byte size
$output->getContents();     // string — raw .epub bytes
$output->delete();          // remove the file

// HTTP response — Apache/nginx/PHP-FPM, framework-agnostic
$output->download('book.epub');   // sets headers, streams, exit()

// Chunked streaming — write into any resource
$output->streamTo(STDOUT);                    // returns int bytes written
foreach ($output->chunks(8192) as $chunk) {   // generator, no buffer
    // ...
}

If you don't pass a path to build(), it writes to sys_get_temp_dir() with a random name.

Streaming without keeping a file

When you don't want a file at all — e.g. you're piping the EPUB straight into an HTTP response or an S3 upload — use buildToStream() or buildToString(). They use a temp file internally and delete it once the bytes are out.

// Write straight to the HTTP response body. Temp file is removed.
header('Content-Type: application/epub+zip');
header('Content-Disposition: attachment; filename="book.epub"');
$out = fopen('php://output', 'wb');
EpubBuilder::create()
    ->setMetadata(fn($m) => $m->title('Book')->author('Author'))
    ->addChapter('Chapter 1', $tiptapJson)
    ->buildToStream($out);

// Or load the bytes (small books, in-memory pipelines).
$bytes = $builder->buildToString();

Laravel

use AdroSoftware\EpubBuilder\EpubBuilder;
use Illuminate\Http\Response;

public function downloadEpub(EpubBuilder $builder): Response
{
    $output = $builder
        ->setMetadata(fn($m) => $m->title('Book')->author('Author'))
        ->addChapter('Chapter 1', $tiptapJson)
        ->build(storage_path('app/temp/book.epub'));

    return response()->download($output->getPath(), 'book.epub', [
        'Content-Type' => 'application/epub+zip',
    ]);
}

Or stream without staging the file in your storage directory:

return response()->stream(
    function () use ($builder) {
        $builder->buildToStream(fopen('php://output', 'wb'));
    },
    200,
    [
        'Content-Type'        => 'application/epub+zip',
        'Content-Disposition' => 'attachment; filename="book.epub"',
    ],
);

Symfony

use Symfony\Component\HttpFoundation\BinaryFileResponse;

$output = EpubBuilder::create()
    ->setMetadata(fn($m) => $m->title('Book')->author('Author'))
    ->addChapter('Chapter 1', $tiptapJson)
    ->build();

return new BinaryFileResponse(
    $output->getPath(),
    headers: ['Content-Type' => 'application/epub+zip'],
);

Streaming variant via StreamedResponse:

use Symfony\Component\HttpFoundation\StreamedResponse;

return new StreamedResponse(
    function () use ($builder) {
        $builder->buildToStream(fopen('php://output', 'wb'));
    },
    200,
    [
        'Content-Type'        => 'application/epub+zip',
        'Content-Disposition' => 'attachment; filename="book.epub"',
    ],
);

Custom Tiptap nodes

The default renderer ships handlers for the standard Tiptap node and mark set:

Nodes: doc, paragraph, heading, blockquote, codeBlock, bulletList, orderedList, listItem, horizontalRule, hardBreak, image, table, tableRow, tableCell, tableHeader, text, pageBreak

Marks: bold, italic, underline, strike, code, link, highlight, subscript, superscript

Unknown nodes are silently dropped (so editors emitting custom node types like unsplashImage, callout, embed don't crash builds). To opt in to rendering a custom node, write a NodeHandlerInterface and register it:

use AdroSoftware\EpubBuilder\Contracts\NodeHandlerInterface;
use AdroSoftware\EpubBuilder\Renderers\DefaultTiptapRenderer;

final class CalloutHandler implements NodeHandlerInterface
{
    public function render(array $node, callable $renderChildren): string
    {
        $type = htmlspecialchars($node['attrs']['type'] ?? 'info', ENT_XML1 | ENT_QUOTES, 'UTF-8');
        $content = $renderChildren($node['content'] ?? []);
        return "<aside class=\"callout callout--{$type}\">{$content}</aside>\n";
    }
}

$builder = EpubBuilder::create();
$renderer = $builder->getRenderer();
if ($renderer instanceof DefaultTiptapRenderer) {
    $renderer->register('callout', new CalloutHandler());
}

Now {"type": "callout", "attrs": {"type": "warning"}, "content": [...]} renders to <aside class="callout callout--warning">…</aside>.

Custom node that needs to embed images

If your custom node has remote or local image references, register them with the AssetManager so they're embedded in the EPUB:

use AdroSoftware\EpubBuilder\Contracts\NodeHandlerInterface;
use AdroSoftware\EpubBuilder\Epub\AssetManager;

final class UnsplashImageHandler implements NodeHandlerInterface
{
    public function __construct(private readonly AssetManager $assetManager) {}

    public function render(array $node, callable $renderChildren): string
    {
        $src = (string) ($node['attrs']['src'] ?? '');
        if ($src === '') return '';

        // AssetManager downloads remote URLs into OEBPS/images/ at zip-write time.
        // The 'jpg' hint is for URLs without a file extension in the path.
        $epubSrc = $this->assetManager->addImage($src, 'jpg');

        $alt = htmlspecialchars($node['attrs']['alt'] ?? '', ENT_XML1 | ENT_QUOTES, 'UTF-8');
        return "<figure><img src=\"{$epubSrc}\" alt=\"{$alt}\"/></figure>\n";
    }
}

$builder->getRenderer()->register('unsplashImage', new UnsplashImageHandler(
    $builder->assetManager(),
));

A complete working example is in examples/UnsplashImageHandler.php.

Overriding a built-in node

override() replaces a default handler:

$renderer->override('image', new MyCdnImageHandler($builder->assetManager()));

Custom marks

Same pattern via MarkHandlerInterface:

use AdroSoftware\EpubBuilder\Contracts\MarkHandlerInterface;

final class SmallCapsMark implements MarkHandlerInterface
{
    public function render(string $content, array $mark): string
    {
        return "<span class=\"smallcaps\">{$content}</span>";
    }
}

$renderer->registerMark('smallCaps', new SmallCapsMark());

Custom renderer (full replacement)

If you'd rather not use the default renderer at all — for example, you have an existing Tiptap-to-HTML pipeline you want to wrap — implement ContentRendererInterface:

use AdroSoftware\EpubBuilder\Contracts\ContentRendererInterface;

final class MyRenderer implements ContentRendererInterface
{
    public function render(array $tiptapJson): string
    {
        // Your own rendering logic. Return a well-formed XHTML body fragment
        // (no <html>, <body> wrapper — the library wraps it).
        return $myExistingTiptapEngine->toHtml($tiptapJson);
    }
}

EpubBuilder::create()
    ->setRenderer(new MyRenderer())
    ->setMetadata(fn($m) => $m->title('Book')->author('A'))
    ->addChapter('One', $json)
    ->build();

You'd typically use this to wrap a third-party Tiptap PHP renderer like ueberdosis/tiptap-php. The default renderer remains an option you can fall back to.

Styling

A clean default stylesheet is embedded automatically. To customize:

// Replace the default with your own CSS file
$builder->setStylesheet('/path/to/my-styles.css');

// Or append additional rules on top of the default
$builder->addCss('
  body { font-family: "Iowan Old Style", Georgia, serif; }
  blockquote { font-style: italic; }
');

Navigation features

The library auto-generates three EPUB nav structures (you don't have to think about any of them):

  • TOC (<nav epub:type="toc">) — every chapter at the top level, inner Tiptap headings nested below.
  • Landmarks (<nav epub:type="landmarks">) — cover, table of contents, beginning of body.
  • Page-list (<nav epub:type="page-list">) — one entry per chapter so readers can show "page N of M". Disable with ->disableAutoPageList().

Plus the legacy formats for older readers:

  • NCX (OEBPS/toc.ncx) — EPUB 2 navigation. Older Kindle apps, Adobe Digital Editions pre-v4, and calibre's older TOC UI fall back to this. Disable with ->disableNcx().
  • Guide (<guide> in OPF) — EPUB 2 reference list (cover, toc, beginning).

Verification

The library passes EPUBCheck 5.x (the W3C reference validator) with 0 errors / 0 warnings on every build. To verify your output:

brew install epubcheck     # macOS
epubcheck path/to/your.epub

Expected output:

Validating using EPUB version 3.3 rules.
No errors or warnings detected.
Messages: 0 fatals / 0 errors / 0 warnings / 0 infos
EPUBCheck completed

Spec references

The library follows the W3C EPUB 3.3 Recommendation:

Plus selected EPUB 2 features for legacy-reader compatibility (NCX, <guide>, <meta name="cover">).

API reference

EpubBuilder

Method Description
EpubBuilder::create(): static Entry point — start a new builder.
setMetadata(callable $cb): static $cb(BookMetadata $m) — configure metadata.
addChapter(string $title, array $tiptapJson): static Add a chapter.
setRenderer(ContentRendererInterface): static Replace the default Tiptap renderer entirely.
getRenderer(): ContentRendererInterface Get the current renderer (used to register custom nodes/marks before adding chapters).
setStylesheet(string $path): static Use a custom CSS file in place of the default.
addCss(string $css): static Append CSS rules to the default (or custom) stylesheet.
disableAutoPageList(): static Opt out of one-page-per-chapter page-list entries.
disableNcx(): static Opt out of the legacy EPUB 2 NCX fallback.
assetManager(): AssetManager Internal asset manager (for custom node handlers that embed images/fonts).
tocBuilder(): TocBuilder Internal TOC builder (for custom heading-like nodes).
pageListBuilder(): PageListBuilder Internal page-list builder (for custom page-break nodes).
metadata(): BookMetadata Direct access to the metadata object.
build(?string $path = null): EpubOutput Validate, render, and write the EPUB.
buildToStream($resource, int $chunkSize = 8192): int Build and copy bytes into a writable stream resource; deletes the temp file afterwards. Returns bytes written.
buildToString(): string Build and return the EPUB as a binary string; deletes the temp file afterwards.

BookMetadata

All setters return static for chaining.

Method EPUB field
title(string) <dc:title> (required)
language(string) <dc:language> (required, BCP 47)
author(string) <dc:creator> — call multiple times for multiple authors
contributor(string $name, string $role = 'oth') <dc:contributor> with MARC relator role
subject(string) <dc:subject> — call multiple times for multiple tags
publisher(string) <dc:publisher>
description(string) <dc:description>
rights(string) <dc:rights>
isbn(string) <dc:identifier> with urn:isbn: prefix
coverImage(string $path) Triggers cover-page generation; image is embedded
direction(string) 'ltr' (default) or 'rtl'
series(string $name, ?int $position) EPUB 3 collection metadata
identifier(string) Override the auto-generated UUIDv4
modified(\DateTimeImmutable) Override the auto-generated modification time

EpubOutput

Method Description
getPath(): string Path on disk where the EPUB was written.
getSize(): int File size in bytes.
getContents(): string Raw EPUB bytes.
download(string $filename = 'book.epub'): never Stream as HTTP download (calls exit).
streamTo($resource, int $chunkSize = 8192): int Copy the EPUB into any writable stream resource in chunks. Returns bytes written.
chunks(int $chunkSize = 8192): \Generator Yield the EPUB body as a generator of byte chunks.
delete(): void Remove the file.

Architecture overview

EpubBuilder              ← public facade
  ├── BookMetadata
  ├── ContentRenderer    ← DefaultTiptapRenderer (or your own)
  │     ├── NodeRegistry
  │     │     └── 17 default node handlers + your custom ones
  │     └── MarkRegistry
  │           └── 9 default mark handlers + your custom ones
  ├── TocBuilder         ← collects TOC entries during render
  ├── PageListBuilder    ← collects page entries during render
  ├── AssetManager       ← collects image/font references during render
  ├── StyleManager       ← default + custom CSS
  ├── PackageBuilder     ← OPF (manifest, spine, metadata, guide)
  ├── NcxBuilder         ← legacy EPUB 2 NCX
  ├── CoverPageBuilder   ← title-page-style cover XHTML
  ├── LandmarksNav, PageListNav, XhtmlWrapper
  ├── ConformanceValidator
  └── OcfWriter          ← assembles the OCF ZIP (mimetype-first)
        └── EpubOutput

The package is intentionally small and composable: replace any layer by passing a different implementation, or extend behavior by registering handlers in the registries.

License

MIT.

adrorocker/epub-builder 适用场景与选型建议

adrorocker/epub-builder 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 230 次下载、GitHub Stars 达 1, 最近一次更新时间为 2026 年 05 月 06 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 adrorocker/epub-builder 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 adrorocker/epub-builder 我们能提供哪些服务?
定制开发 / 二次开发

基于 adrorocker/epub-builder 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 230
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 9
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-05-06