Статьи

Сборка ePub с PHP и Markdown

Формат 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