fizk / epub
Requires
- php: ^7.2||^8.0
- ext-zip: *
- monolog/monolog: ^2.3
Requires (Dev)
- mikey179/vfsstream: ^1.6
- phpunit/phpunit: ^9
This package is auto-updated.
Last update: 2024-09-18 04:28:41 UTC
README
PHP库,用于将文本文件(HTML、Markdown等)转换为*.epu3文件。
理论。
将硬盘上的文件集(或它们所在的地方)转换为Epub的过程可以看作是三个步骤
- 遍历并收集组成Epub的文件。
- 格式化每个文件以符合Epub3标准。
- 合并文件并转换为*.epub3文件。
步骤 1 和 3 总是相同的。步骤 2 对每个用例都是特定的。这设置了流程和接口,所以你不需要担心获取和编译文件,你只需要关注提取和格式化。
示例
简单的Markdown示例
假设你有一个目录,其中包含一些你希望转换为Epub书籍的Markdown文档
projects
|-- index.php
|-- cover.jpg
`-- documents
|-- chapter-1
| | file1.md
| `-- file2.md
|-- chapter-2
| | file1.md
| `-- file2.md
`-- chapter-3
`-- file1.md
首先需要为这些Markdown文件实现一个Formatter。要创建一个格式化器,实现FormatterInterface。
interface FormatterInterface { public function setWorkspace(ContainerInterface $workspace); public function formatChapterTitle(ResourceInterface $resource): string; public function formatPageTitle(ResourceInterface $resource): string; public function chapterTemplate(ResourceInterface $resource, RecursiveIterator $children): ?DOMDocument; public function pageTemplate(ResourceInterface $resource): ?DOMDocument; }
我们可以采取捷径,扩展BlankFormatter并只覆盖pageTemplate方法
class MarkdownFormatter extends BlankFormatter { public function pageTemplate(ResourceInterface $resource): ?DOMDocument { $htmlString = $markdown->format($resource->getContent()); $dom = new DOMDocument(); $dom->loadXML($htmlString); return $dom; } }
在index.php文件中设置样板代码。
use Epub\Epub3; use Epub\Storage\StorageZip; use Epub\Resource\RecursiveDirectory; use Epub\Document\Package; // Setup Package. It is the thing that describes the meta-data of the book // like author, title and creation date. $package = new Package(uniqid() , 'Title', new DateTime()); // Setup a recursive iterator that will traverse and find all the markdown files $iterator = new RecursiveDirectory(realpath(__DIR__ .'/documents')); // Setup storage. It will reseive all document and store them in a *.epub file $storage = new StorageZip(__DIR__ .'/store.epub'); // Setup the Formatter, it will know hoe to change Markdown files in to XHTML files. $formatter = new MarkdownFormatter(); // Run the Epub3 generator. (new Epub3('Title')) ->setPackage($package) ->setCoverImage(file_get_contents(__DIR__ .'/cover.jpg'),'image/jpeg', 'jpg') ->setCoverPage('<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8"/> </head> <body> <h1>Title page</h1> </body> </html> ') ->setStorage($storage) ->setFormatter($formatter) ->save($iterator);
运行此代码,你将在项目目录中得到一个Epub文件。
带有章节页面的示例
上面的示例没有为每个章节创建一个专门的页面。为此,我们需要实现chapterTemplate(ResourceInterface $resource, RecursiveIterator $children): ?DOMDocument方法。
class MarkdownFormatter extends BlankFormatter { public function pageTemplate(ResourceInterface $resource): ?DOMDocument { // ... same as above } public function chapterTemplate(ResourceInterface $resource, RecursiveIterator $children): ?DOMDocument { $dom = new DOMDocument(); $htmlElement = $dom->createElement('html'); $bodyElement = $dom->createElement('body'); $headerElement = $dom->createElement('h1', $resource->getName()); $dom->appendChild($htmlElement); $htmlElement->appendChild($bodyElement); $bodyElement->appendChild($headerElement); return $dom; } }
也许我们想更进一步,将章节中的所有页面列在章节标题页上。我们可以这样做
class MarkdownFormatter extends BlankFormatter { public function pageTemplate(ResourceInterface $resource): ?DOMDocument { // ... same as above } public function chapterTemplate(ResourceInterface $resource, RecursiveIterator $children): ?DOMDocument { $dom = new DOMDocument(); $htmlElement = $dom->createElement('html'); $bodyElement = $dom->createElement('body'); $headerElement = $dom->createElement('h1', $resource->getName()); $listElement = $dom->createElement('ol'); foreach($children as $child) { $listItemElement = $dom->createElement('li', $child->getName()); $listElement->appendChild($listItemElement); } $dom->appendChild($htmlElement); $htmlElement->appendChild($bodyElement); $bodyElement->appendChild($headerElement); $bodyElement->appendChild($listElement); return $dom; } }
如果这个章节页面需要其目录项可点击,我们需要使用通过$this->workspace属性可访问的encodeContentUri方法。
class MarkdownFormatter extends BlankFormatter { public function pageTemplate(ResourceInterface $resource): ?DOMDocument { // ... same as above } public function chapterTemplate(ResourceInterface $resource, RecursiveIterator $children): ?DOMDocument { $dom = new DOMDocument(); $htmlElement = $dom->createElement('html'); $bodyElement = $dom->createElement('body'); $headerElement = $dom->createElement('h1', $resource->getName()); $listElement = $dom->createElement('ol'); foreach($children as $child) { $linkElement = $dom->createElement('a', $child->getName()); $linkElement->setAttribute('href', $this->workspace->encodeContentUri($child)); $listItemElement = $dom->createElement('li'); $listItemElement->appendChild($linkElement); $listElement->appendChild($listItemElement); } $dom->appendChild($htmlElement); $htmlElement->appendChild($bodyElement); $bodyElement->appendChild($headerElement); $bodyElement->appendChild($listElement); return $dom; } }
自定义章节和页面名称。
到目前为止,我们一直在使用相同的文件和文件夹名称作为章节和页面的名称。这可能不是理想的选择,因为这些名称用于目录。让我们来修正它。
对于章节名称,我们只将文件夹名称中包含的数字使用。对于页面名称,我们将查看Markdown文档并提取第一个顶级标题元素。
class MarkdownFormatter extends BlankFormatter { public function formatChapterTitle(ResourceInterface $resource): string { preg_match('/[0-9]+/', $resource->getName(), $match); return $match[0]; } public function formatPageTitle(ResourceInterface $resource): string { $dom = new DOMDocument(); $dom->loadHTML($markdown->parse($resource->getContent())); return $dom->getElementsByTagName('h1')->item->nodeValue; } }
带有摘要的自定义章节页面
在这个例子中,我们将为每个章节提供自定义名称。我们还在每个章节页面上包括一个简短的摘要。为此,我们将在每个目录中包括一个chapter.md文件,其中包含章节名称和摘要。此chapter.md文件看起来像这样
# Name of a chaper
This is a descriptino of the chapter.
...并包括这个新文档看起来像这样
projects
|-- index.php
|-- cover.jpg
`-- documents
|-- chapter-1
| | chapter.md <---- name and description of chapter-1
| | file1.md
| `-- file2.md
|-- chapter-2
| | chapter.md <---- name and description of chapter-2
| | file1.md
| `-- file2.md
`-- chapter-3
| chapter.md <---- name and description of chapter-3
`-- file1.md
接下来,我们需要扩展RecursiveDirectory类,使其忽略chapter.md文件。
class ExtendedRecursiveDirectory extends RecursiveDirectory { public function __construct($directory) { $this->currentFileObject = new SplFileInfo($directory); $this->children = array_values(array_map(function ($item) use ($directory) { return new class($directory . '/' . $item) extends SplFileInfo implements ResourceInterface { public function getContent() { return \file_get_contents($this->getRealPath()); } public function getName(): string { return $this->getFilename(); } public function getPath(): string { return $this->getRealPath(); } }; }, array_diff(scandir($this->currentFileObject->getRealPath()), array('..', '.', '.DS_Store', 'chapter.md')))); } public function getChildren(): RecursiveIterator { return new ExtendedRecursiveDirectory($this->children[$this->index]->getRealPath()); } }
然后,在我们的Formatter中,当我们在章节/目录中时,访问markdown.md。
class MarkdownFormatter extends BlankFormatter { public function formatChapterTitle(ResourceInterface $chapter): string { $file = \file_get_contents($chapter->getPath() . '/markdown.md'); $dom = new DOMDocument(); $dom->loadHTML($file); $documentTitle = $dom->getElementsByTagName('h1')->item(0)->nodeValue; return mb_convert_encoding(trim(htmlspecialchars($documentTitle)), 'UTF-8'); } public function chapterTemplate(ResourceInterface $chapter, RecursiveIterator $children): ?DOMDocument { $file = \file_get_contents($chapter->getPath() . '/markdown.md'); $dom = new DOMDocument(); $dom->loadHTML($file); $chapterDom = new DOMDocument(); $htmlElement = $chapterDom->createElement('html'); $bodyElement = $chapterDom->createElement('body'); $headerElement = $chapterDom->createElement('h1', $dom->getElementsByTagName('h1')->item(0)->nodeValue); $bodyElement->appendChild($headerElement); foreach($dom->getElementsByTagName('p') as $element) { $paragraphElement = $dom->importNode($element, true); $bodyElement->appendChild($paragraphElement); } return $chapterDom; } }
RecursiveDirectory
最后一个示例简要介绍了RecursiveDirectory类。它负责迭代遍历目录结构。它实际上实现了RecursiveIteratorIterator,因此可以实施不同的RecursiveIteratorIterator,例如遍历数据库、通过TCP/IP的外部服务,等等。
此存储库实际上还包含另一个迭代器:RecursiveMemory,它主要用于单元测试,但也实现了RecursiveIteratorIterator。
StorageZip
上述所有示例都使用了StorageZip作为存储类。这个类负责将所有资源打包成一个Epub文件。这个类实现了StorageInterface接口。
interface StorageInterface { public function createContainer(string $path): bool; public function createResource(string $path, string $content): bool; }
这个仓库还提供了一个StorageFilesystem类,它将所有资源写回磁盘。这对于调试很有用。这个仓库还提供了一个StorageMemory,它主要用于单元测试。
虽然这段代码是为运行CLI程序编写的,但可以想象它也可以在Web服务器环境中使用。在这种情况下,StorageZip类需要重写,以便不将ZIP文件存储在磁盘上,而是将其放入输出缓冲区。
添加资源
许多Epub包含图像和其他需要嵌入到最终产品中的媒体。为此,我们提供了addResource($content, string $mimetype, string $extension): string;方法。
在这个例子中,我从一个文档中获取所有<img />标签,将它们转换为低分辨率的黑白JPEG图像,将其添加为资源,并返回一个资源URL,用于更新现有的<img />标签。
class MarkdownFormatter extends BlankFormatter { public function pageTemplate(ResourceInterface $page): ?DOMDocument { $dom = new DOMDocument(); $dom->loadHTML($page->getContent()); $imagick = new Imagick(); foreach($dom->getElementsByTagName('img') as $imageElement) { $imagick->readImageBlob(\file_get_contents($imageElement->getAttribute('src'))); $imagick->setImageFormat('jpg'); $imagick->setImageCompression(Imagick::COMPRESSION_JPEG); $imagick->setImageType(Imagick::IMGTYPE_GRAYSCALEMATTE); $imagick->setImageCompressionQuality(35); $imagePath = $this->workspace->addResource($imagick->getImageBlob(), 'image/jpeg', 'jpg'); $imageElement->setAttribute('src', $imagePath); } return $dom; } }
XHTML命名空间。
因为Epub是由XHTML文档组成的集合,所以给<html>标签添加该命名空间是个好主意。
class MarkdownFormatter extends BlankFormatter { public function pageTemplate(ResourceInterface $page): ?DOMDocument { $dom = new DOMDocument('1.0', 'utf-8'); $html = $dom->createElement('html'); $html->setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); $html->setAttribute('xml:lang', 'en'); $html->setAttribute('lang', 'en'); // format and convert the document return $dom; } }
我在示例中省略了这一点,以使代码更简洁,但我总是对所有文档这样做。
Epub元数据和包。
如果您想添加更多详细信息到Epub的元数据中,您可以这样做
$package = new Package(uniqid(), 'Title of epub', new DateTime()); $package->addMetadata(new MetadataDescription("Short description")); $package->addMetadata(new MetadataAuthor('Autor name', 'Autor name, sorted by')); $package->addMetadata(new MetadataPublisher('Publisher')); $package->addMetadata(new MetadataPublishDate(new DateTime('2013-12-31'))); $package->addMetadata(new MetadataCover()); $epub = (new Epub3('Title of epub'))->setPackage($package);