Формат ePub — это стандарт публикации, основанный на XHTML, CSS, XML и многом другом. А поскольку PHP хорошо подходит для работы с HTML и друзьями, почему бы не использовать его для создания электронных книг? В этой статье мы увидим, что входит в создание инструмента для создания пакетов ePub, начиная с набора файлов содержимого. Может быть, это ваш следующий самый продаваемый кибер-научный роман или документация для вашего последнего проекта кода … потому что мы все пишем хорошую документацию для наших проектов, не так ли?
Чтобы упростить работу авторов, наш инструмент сможет анализировать синтаксис Markdown, а полученная разметка будет помещена в шаблоны HTML с помощью библиотеки RainTPL .
Образец инструмента, который я написал для этой статьи, называется md2epub и стал настоящим проектом с открытым исходным кодом на Github . В данный момент он далек от совершенства, но не стесняйтесь раскошелиться и улучшить его!
Быстрый старт Теория ePub
Спецификации для формата ePub детализированы IDPF (Международным форумом по цифровым публикациям) , и мы будем ссылаться на версию 2.0.1 спецификаций, которая является наиболее широко используемой сегодня. Другими полезными ресурсами являются страница Wikipedia ePub и руководство по построению формата Epub Харрисона Эйнсворта .
По сути, книга ePub представляет собой архив с четко определенной структурой. Содержимое включает документы XHTML, таблицы стилей CSS, изображения (GIF, JPEG, PNG и SVG) и шрифты открытого типа. Есть и другие конкретные файлы, используемые для описания содержания книги; это файлы пакета и контейнера:
моя книга/ META-INF / container.xml MimeType content.opf toc.ncx stylesheet.css BookCover.jpg HomePage.xhtml Chapter1.xhtml ... ChapterN.xhtml index.xhtml
Первые четыре файла относятся к пакету ePub, остальные являются нашим контентом. Файл mimetype
должен содержать application / epub + zip в ASCII без символа конца строки. Это должен быть первый файл, включенный в архив, и он не должен быть сжат; мы не можем сделать это с помощью PHP, но я покажу вам обходной путь для этого позже.
Файл META-INF/container.xml
фиксирован по имени и расположению. Область действия этого файла — указать, где находится OPF-файл (обычно content.opf
) относительно корневого каталога книги.
content.opf
— это файл XML, который содержит метаданные книги, ссылки на все ресурсы контента и порядок, в котором содержимое должно загружаться приложением для чтения.
Файл toc.ncx
является необязательной, но рекомендуемой XML-картой навигации для электронной книги.
Есть и другие дополнительные файлы, которые пока не принимаются во внимание. И я не скажу вам, как добавить DRM, извините.
Наша первая электронная книга
Итак, как нам подготовить наш контент для ePub-lishing? Я создал каталог тестовой книги следующим образом:
моя книга/ 01-first-chapter.md 02-second-chapter.md book.json cover.jpg coverpage.md index.md style.css titlepage.md СМИ/ * .jpg
Большинство файлов являются файлами содержимого для публикации, текстом уценки и изображениями. Затем у нас есть файл style.css
(имя и позиция должны быть исправлены). Помните, что формат ePub понимает только подмножество спецификаций CSS 2.1.
Наконец у нас есть специальный файл с именем book.json
. Этот файл содержит все данные, которые необходимы нашему инструменту md2epub
для генерации стандартных файлов пакетов. Большинство ключей в этом файле имеют прямое соответствие с файлами OPF и NCX. Файл JSON имеет следующую структуру:
{ "id": "com.acme.books.MyUniqueBookID", "title": "Sample eBook Title", "language": "en", "authors": [ { "name": "John Smith", "role": "aut"}, { "name": "Jane Appleseed", "role": "dsr"} ], "description": "Brief description of the book", "subject": "List of keywords, pertinent to content, separated by commas", "publisher": "ACME Publishing Inc.", "rights": "Copyright (c) 2013, Someone", "date": "2013-02-27", "relation": "http://www.acme.com/books/MyUniqueBookIDWebEdition/", "files": { "coverpage": "coverpage.md", "title-page": "titlepage.md", "include": [ { "id":"ncx", "path":"toc.ncx" }, "cover.jpg", "style.css", "*.md", "media/*" ], "index": "index.md", "exclude": [] }, "spine": { "toc": "ncx", "items": [ "coverpage", "title-page", "copyright", "foreword", "|^c\d{1,2}-.*$|", "index" ] } }
Первые три группы ключей от id
к relation
сопоставляются с разделом <metadata>
файла OPF. Они содержат информацию о книге, авторе, издателе и т. Д. Ключи id
, title
и language
являются обязательными. Все остальные являются необязательными, и их значение не требует пояснений, за исключением, может быть, relation
которое используется для ссылки на другую публикацию с использованием формата ISBN или URL-адреса сайта. Значение id
должно быть уникальным, и я выбрал формат обратного домена com.publisher.series.BookID, хотя вы можете использовать любой формат, который вам нравится.
Ключ files
JSON отображается в раздел <manifest> OPF. Этот раздел содержит ссылки на каждый файл, используемый в книге, включая шрифты, таблицы стилей, изображения и специальные файлы. Каждому элементу в этом разделе требуется уникальный идентификатор, путь к ресурсу (href) и его mimetype. md2epub
позаботится о типе носителя и сгенерирует подходящий идентификатор, если он не указан.
Я также хотел, чтобы он мог указывать массовые включения файлов с использованием подстановочных знаков, таких как * .md и media / * , хотя список исключений в данный момент тоже не анализируется.
Ключ JSON spine отображается на <spine>
в OPF, он описывает порядок, в котором различные главы должны появляться в книге.
Начиная с этих данных, наш md2epub
будет:
- создать хорошо организованный каталог временных книг
- скопировать все указанные файлы
- конвертировать и копировать весь файл уценки в XHTML
- создать необходимые файлы ePub
- создать архив и очистку zip / epub
Окончательный файл может (и должен) быть проверен официальным EPUB Validator, который вы также можете загрузить и запустить локально как консольное приложение Java.
Md2Epub — Создание из
Чтобы остаться в теме этой статьи, я расскажу только о наиболее специфичном для epub коде и оставлю «служебный код» для изучения.
Полная программа состоит из консольного PHP-скрипта md2epub.php
который собирает md2epub.php
пользователем данные, создает рабочий каталог и передает все данные в класс EBook
. Файл md2epub
является оболочкой оболочки.
Класс EBook
выполняет все операции и проверки. Он использует библиотеку RainTPL для анализа шаблонов и библиотеку PHP Markdown Extra в качестве фильтра содержимого. Интересным в этом последнем компоненте является то, что функция Markdown()
предоставляется классу EBook
как внешний фильтр для применения к файлам *.md
. Таким образом, мы можем внедрить любые другие текстовые фильтры, такие как Textile или синтаксический анализатор вики.
Каталог share содержит общие файлы, в частности файлы шаблонов XHTML и XML; Вы можете редактировать разметку этих файлов, чтобы настроить тип книги. Файл mimetype.zip
является частью mimetype.zip
о котором я упоминал ранее — поскольку библиотека PHP Zip не позволяет нам указывать уровень сжатия при создании архива, я создал эту заглушку архива с помощью программы zip из командной строки. :
zip -0 -D -X mimetype.zip mimetype
PHP скопирует этот основной архив и последовательно добавит другие файлы.
Изучение объекта EBook
Конструктор класса EBook
принимает два аргумента: исходный каталог книги и необязательный массив параметров. Параметры указывают имя файла JSON для анализа (по умолчанию book.json
) и каталог, в котором будут содержаться файлы содержимого скомпилированной книги. Я выбрал OEBPS
, что означает «Структура публикации открытых электронных книг», поскольку это соглашение используется многими из исследованных мной электронных книг.
Роль метода parseFiles()
заключается в нормализации раздела файлов, загруженного из JSON. Пути подстановочных знаков расширяются с помощью glob()
и генерируются идентификаторы и типы мультимедиа. В конце он дает нам ассоциативный массив, где каждый файл представлен следующим образом:
'cover' => Array ( 'path' => 'cover.jpg', 'type' => 'image / jpeg' )
Каждый файл не должен быть указан более одного раза. Путь указывается относительно каталога содержимого, каталога, в котором будет находиться файл OPF. На этом этапе файлы разметки имеют тип text / plain и будут преобразованы позже.
Метод parseSpine()
выполняет аналогичную работу. Он устанавливает файл NCX / TOC и составляет список объявленных идентификаторов. Идентификаторы могут быть регулярными выражениями. Например, я указал шаблон ^cd{1,2}-.*$
, Который соответствует идентификаторам глав ( c01-first-chapter , c02-second-chapter и т. Д.). Начало «c» важно, поскольку атрибуты XML- id
не могут начинаться с цифры.
Мощный makeEpub()
затем запускает процесс преобразования для нашей книги. Необходимые данные:
- путь к целевому файлу .epub
- временный каталог, в котором компилируются и копируются все ресурсы
- исходный каталог для файлов шаблонов
- дополнительные фильтры содержимого, в данном случае Markdown связан с расширением md
Сначала он проверяет входные параметры, чтобы убедиться, что он имеет правильные разрешения файловой системы, а затем последовательно вызывает четыре вспомогательных метода, соответствующих требуемым шагам:
- создать каталог и данные META-INF
- экспортировать все файлы содержимого, компилировать шаблоны и применять фильтры
- создать файл манифеста OPF
- создать файл навигации NCX (если необходимо)
- создать сжатый архив epub
Метод createMetaInf()
является самым простым и помогает нам понять логику шаблона за другими:
<?php protected function createMetaInf($workDir) { // create destination directory if (!mkdir("$workDir/META-INF")) { throw new Exception('Unable to create content META-INF directory'); } // compile file $tpl = $this->initTemplateEngine( array( 'tpl_dir' => "{$this->params['templates_dir']}/", 'tpl_ext' => 'xml', 'cache_dir' => sys_get_temp_dir() . '/' ) ); $tpl->assign('ContentDirectory', $this->params['content_dir']); $container = $tpl->draw('book/META-INF/container', true); // write compiled file to destination if (file_put_contents("$workDir/META-INF/container.xml", $container) === false) { throw new Exception("Unable to create content META-INF/container.xml"); } }
В шаблоне container.xml
есть только одна переменная, которая задает путь к файлу content.opf
относительно базового каталога книги. Синтаксис: {$VariableName}
.
<?xml version="1.0"?> <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> <rootfiles> <rootfile full-path="{$ContentDirectory}/content.opf" media-type="application/oebps-package+xml"/> </rootfiles> </container>
Сначала библиотека RainTPL инициализируется с помощью каталога шаблонов, расширения файлов шаблонов и каталога временного кэша. Затем присваивается переменная шаблона ( $tpl->assign()
) и метод draw()
анализирует предоставленный файл шаблона. Его второй параметр сообщает draw()
, возвращать ли результат. Скомпилированный файл затем сохраняется в путь назначения.
Метод createOpf()
имеет ту же логику и еще несколько переменных для назначения. Я выбрал шаблон Book<Varname>
поэтому дополнительные метаданные назначаются в цикле:
<?php $optParams = array( 'authors', 'date', 'description', 'publisher', 'relation', 'rights', 'subject' ); foreach ($optParams as $p) { if (isset($this->$p)) { $tpl->assign('Book' . ucfirst($p), $this->$p); } }
И они вставляются в шаблон с условным синтаксисом:
{if="$BookDescription"}<dc:description>{$BookDescription}</dc:description>{/if}
Файлы и переменные позвоночника форматируются в методе и вставляются в шаблон с помощью цикла:
<manifest> {loop="$BookFiles"}<item id="{$key}" href="{$value.path}" media-type="{$value.type}"/> {/loop} </manifest>
processBookFiles()
вызывается перед createOpf()
и выполняет большой цикл со всеми ссылочными ресурсами ( $this->files
). Для каждого ресурса он может выбирать между двумя действиями:
- обработать и отфильтровать файл
- скопируйте путь к файлу «как есть» в целевом каталоге
Фильтр, применяемый к файлу, должен быть вызываемой функцией PHP или методом класса, который принимает один параметр: содержимое. Содержимое файла загружается из исходного источника с помощью file_get_contents()
а фильтр применяется с помощью встроенного call_user_func()
:
<?php $content = call_user_func($filters[$ext], $content);
Обработанный контент затем внедряется в шаблон XHTML, аналогично предыдущим методам. В этом случае также есть возможность использовать пользовательский шаблон. Если в .xhtml
templates
есть файл с именем .xhtml
, этот файл будет использоваться page.xhtml
по умолчанию. Кроме того, если style.css
файл style.css
то {$BookStyle}
переменная шаблона {$BookStyle}
и CSS связывается со страницей.
Полученный файл затем записывается в каталог назначения, и запись манифеста для этого файла обновляется с использованием нового пути и типа носителя, готовых для createOpf()
.
createNcx()
немного сложнее, потому что он должен изучить элементы секции позвоночника и извлечь содержимое тегов H1 и H2. Файл toc.ncx
имеет такую структуру, сначала стандартизированный заголовок:
<?xml version="1.0" encoding="UTF-8"?> <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1"> <head> <meta name="dtb:uid" content="{$BookID}"/> <!-- 1 = chapters only, 2 = subchapters, 3 = third level section --> <meta name="dtb:depth" content="2"/> <!-- Both required but unused, can be 0 --> <meta name="dtb:totalPageCount" content="0"/> <meta name="dtb:maxPageNumber" content="0"/> </head> <docTitle> <text>{$BookTitle}</text> </docTitle>
Настраиваемая dtb:uid
указывает идентификатор нашей книги, а dtb:depth
говорит о глубине навигации. Здесь я выбираю фиксированное значение 2, поэтому в коде синтаксического анализа я должен извлечь первые два уровня заголовков.
После заголовка есть <navMap>
который генерируется с помощью цикла шаблона.
{$playOrder=1} {loop="$BookChapters"} <navPoint id="{$key}" playOrder="{$playOrder++}"> <navLabel> <text>{$value.title}</text> </navLabel> <content src="{$value.path}"/> {if="isset($value.sections)"}{loop="$value.sections"} <navPoint id="{$key}" playOrder="{$playOrder++}"> <navLabel> <text>{$value.title}</text> </navLabel> <content src="{$value.path}"/> </navPoint> {/loop}{/if} </navPoint> {/loop}
,{$playOrder=1} {loop="$BookChapters"} <navPoint id="{$key}" playOrder="{$playOrder++}"> <navLabel> <text>{$value.title}</text> </navLabel> <content src="{$value.path}"/> {if="isset($value.sections)"}{loop="$value.sections"} <navPoint id="{$key}" playOrder="{$playOrder++}"> <navLabel> <text>{$value.title}</text> </navLabel> <content src="{$value.path}"/> </navPoint> {/loop}{/if} </navPoint> {/loop}
Каждый H1 становится <navPoint>
, id
— это соответствующий идентификатор ресурса (например, c01-first-chapter и т. Д.), Содержимое H1 становится меткой, а атрибут playOrder
указывает глобальный порядок чтения. В нашем примере каждая точка навигации может иметь вложенные точки навигации для субтитров H2.
В методе двойной цикл генерирует массив, структурированный так:
$chapters['c01-first-chapter'] = array( 'title' => 'Title for chapter 1', 'path' => '01-first-chapter.xhtml', 'sections' => array( 'section-1-1' => array( 'title' => 'Title for subsection 1.1', 'path' => '01-first-chapter.xhtml#section-1-1', ) ) )
Во внешнем цикле каждый элемент загружается как объект SimpleXML
:
<?php $doc = simplexml_load_file("$workDir/{$this->params['content_dir']}/{$this->files[$item]['path']}"); if ($doc && isset($doc->body->h1)) { // chapter title $chapters[$item] = array( 'title' => $doc->body->h1, 'path' => $this->files[$item]['path'] ); // subchapter title foreach ($doc->body->h2 as $section) { if (!empty($section['id'])) { $section_id = (string) $section['id']; $chapters[$item]['sections'][$section_id] = array( 'title' => $section, 'path' => $this->files[$item]['path'] . '#' . $section['id'] ); } } }
Если документ имеет заголовок H1, элемент первого уровня вставляется в массив $chapters
. Затем запрашиваются заголовки второго уровня, каждый заголовок с непустым атрибутом id
включается в разделы для каждой главы. Массив $chapters
становится {$BookChapters}
в шаблоне. Затем шаблон отображается и сохраняется в месте назначения.
Теперь у нас есть рабочий каталог назначения в /tmp/path/com.publisher.BookID
который можно упаковать на последнем шаге createArchive()
:
<?php protected function createArchive($workDir, $epubFile) { $excludes = array('.DS_Store', 'mimetype'); $mimeZip = "{$this->params['templates_dir']}/mimetype.zip"; $zipFile = sys_get_temp_dir() . '/book.zip'; if (!copy($mimeZip, $zipFile)) { throw new Exception("Unable to copy temporary archive file"); } $zip = new ZipArchive(); if ($zip->open($zipFile, ZipArchive::CREATE) != true) { throw new Exception("Unable open archive '$zipFile'"); } $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($workDir), RecursiveIteratorIterator::SELF_FIRST); foreach ($files as $file) { if (in_array(basename($file), $excludes)) { continue; } if (is_dir($file)) { $zip->addEmptyDir(str_replace("$workDir/", '', "$file/")); } elseif (is_file($file)) { $zip->addFromString( str_replace("$workDir/", '', $file), file_get_contents($file) ); } } $zip->close(); rename($zipFile, $epubFile); }
Массив $excludes
содержит специальные и системные файлы, которые нам не нужны в архиве. Создается временный путь для пустого $zipFile
, а затем путь $zipFile
заполняется копией нашего базового mimetype.zip
который загружается в объект ZipArchive
. На этом этапе содержимое рабочего каталога рекурсивно загружается в массив $files
с использованием объектов RecursiveIteratorIterator
и RecursiveDirectoryIterator
. В следующем цикле структура рабочего каталога копируется в zip-архив. Как последний штрих, архив перемещается в путь назначения, указанный вызывающей стороной.
Резюме
Когда все сказано и сделано, у нас есть новая блестящая электронная книга. У нас есть хороший шанс, что он будет проверен приложением EpubCheck, и, самое главное, у нас есть еще один полезный инструмент, который можно добавить в наш пояс супергероя PHP! И это только отправная точка, вы можете поиграть с кодом, чтобы добавить фильтры, функции, темы, стили и многое другое, если хотите. Сохраняйте спокойствие, получайте удовольствие и пишите эти электронные книги!
Изображение через Fotolia