aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/mikespub/php-epub-meta/src
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/mikespub/php-epub-meta/src')
-rw-r--r--vendor/mikespub/php-epub-meta/src/App/Handler.php236
-rw-r--r--vendor/mikespub/php-epub-meta/src/App/Util.php55
-rw-r--r--vendor/mikespub/php-epub-meta/src/Contents/Nav.php63
-rw-r--r--vendor/mikespub/php-epub-meta/src/Contents/NavPoint.php109
-rw-r--r--vendor/mikespub/php-epub-meta/src/Contents/NavPointList.php65
-rw-r--r--vendor/mikespub/php-epub-meta/src/Contents/Spine.php198
-rw-r--r--vendor/mikespub/php-epub-meta/src/Contents/Toc.php63
-rw-r--r--vendor/mikespub/php-epub-meta/src/Data/Item.php193
-rw-r--r--vendor/mikespub/php-epub-meta/src/Data/Manifest.php174
-rw-r--r--vendor/mikespub/php-epub-meta/src/Dom/Element.php196
-rw-r--r--vendor/mikespub/php-epub-meta/src/Dom/XPath.php32
-rw-r--r--vendor/mikespub/php-epub-meta/src/EPub.php2126
-rw-r--r--vendor/mikespub/php-epub-meta/src/Other.php121
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/HtmlTools.php97
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php450
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/ZipFile.php343
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/htmlBlockLevelElements.php42
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/htmlEntityMap.php260
18 files changed, 4823 insertions, 0 deletions
diff --git a/vendor/mikespub/php-epub-meta/src/App/Handler.php b/vendor/mikespub/php-epub-meta/src/App/Handler.php
new file mode 100644
index 000000000..f1f7547ec
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/App/Handler.php
@@ -0,0 +1,236 @@
+<?php
+/**
+ * PHP EPub Meta - App request handler
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Sébastien Lucas <sebastien@slucas.fr>
+ * @author Simon Schrape <simon@epubli.com> © 2015
+ * @author mikespub
+ */
+
+namespace SebLucas\EPubMeta\App;
+
+use SebLucas\EPubMeta\EPub;
+use SebLucas\EPubMeta\Tools\ZipEdit;
+use Exception;
+
+class Handler
+{
+ protected string $bookdir;
+ protected string $rootdir;
+ protected ?EPub $epub;
+ protected ?string $error;
+
+ public function __construct(string $bookdir)
+ {
+ $this->bookdir = $bookdir;
+ $this->rootdir = dirname(__DIR__, 2);
+ }
+
+ /**
+ * Handle request
+ * @param mixed $request @todo
+ * @return void
+ */
+ public function handle($request = null)
+ {
+ // proxy google requests
+ if (isset($_GET['api'])) {
+ header('application/json; charset=UTF-8');
+ echo $this->searchBookApi($_GET['api']);
+ return;
+ }
+ if (!empty($_REQUEST['book'])) {
+ try {
+ $book = preg_replace('/[^\w ._-]+/', '', $_REQUEST['book']);
+ $book = basename($book . '.epub'); // no upper dirs, lowers might be supported later
+ $this->epub = new EPub($this->bookdir . $book, ZipEdit::class);
+ } catch (Exception $e) {
+ $this->error = $e->getMessage();
+ }
+ }
+ // return image data
+ if (!empty($_REQUEST['img']) && isset($this->epub)) {
+ $img = $this->epub->getCoverInfo();
+ header('Content-Type: ' . $img['mime']);
+ echo $img['data'];
+ return;
+ }
+ // save epub data
+ if (isset($_REQUEST['save']) && isset($this->epub)) {
+ $this->epub = $this->saveEpubData($this->epub);
+ if (!$this->error) {
+ // rename
+ $new = $this->renameEpubFile($this->epub);
+ $go = basename($new, '.epub');
+ header('Location: ?book=' . rawurlencode($go));
+ return;
+ }
+ }
+ $data = [];
+ $data['bookdir'] = htmlspecialchars($this->bookdir);
+ $data['booklist'] = '';
+ $list = glob($this->bookdir . '/*.epub');
+ foreach ($list as $book) {
+ $base = basename($book, '.epub');
+ $name = Util::book_output($base);
+ $data['booklist'] .= '<li ' . ($base == $_REQUEST['book'] ? 'class="active"' : '') . '>';
+ $data['booklist'] .= '<a href="?book=' . htmlspecialchars($base) . '">' . $name . '</a>';
+ $data['booklist'] .= '</li>';
+ }
+ if (isset($this->error)) {
+ $data['alert'] = "alert('" . htmlspecialchars($this->error) . "');";
+ }
+ if (empty($this->epub)) {
+ $data['license'] = str_replace("\n\n", '</p><p>', htmlspecialchars(file_get_contents($this->rootdir . '/LICENSE')));
+ $template = $this->rootdir . '/templates/index.html';
+ } else {
+ $data = $this->getEpubData($this->epub, $data);
+ $template = $this->rootdir . '/templates/epub.html';
+ }
+ header('Content-Type: text/html; charset=utf-8');
+ echo $this->renderTemplate($template, $data);
+ }
+
+ /**
+ * Proxy google requests
+ * @param string $query
+ * @return string|false
+ */
+ protected function searchBookApi($query)
+ {
+ return file_get_contents('https://www.googleapis.com/books/v1/volumes?q=' . rawurlencode($query) . '&maxResults=25&printType=books&projection=full');
+ }
+
+ /**
+ * Get Epub data
+ * @param EPub $epub
+ * @param array<string, string> $data
+ * @return array<string, string>
+ */
+ protected function getEpubData($epub, $data = [])
+ {
+ $data['book'] = htmlspecialchars($_REQUEST['book']);
+ $data['title'] = htmlspecialchars($epub->getTitle());
+ $data['authors'] = '';
+ $count = 0;
+ foreach ($epub->getAuthors() as $as => $name) {
+ $data['authors'] .= '<p>';
+ $data['authors'] .= '<input type="text" name="authorname[' . $count . ']" value="' . htmlspecialchars($name) . '" />';
+ $data['authors'] .= ' (<input type="text" name="authoras[' . $count . ']" value="' . htmlspecialchars($as) . '" />)';
+ $data['authors'] .= '</p>';
+ $count++;
+ }
+ $data['cover'] = '?book=' . htmlspecialchars($_REQUEST['book']) . '&amp;img=1';
+ $c = $epub->getCoverInfo();
+ $data['imgclass'] = $c['found'] ? 'hasimg' : 'noimg';
+ $data['description'] = htmlspecialchars($epub->getDescription());
+ $data['subjects'] = htmlspecialchars(join(', ', $epub->getSubjects()));
+ $data['publisher'] = htmlspecialchars($epub->getPublisher());
+ $data['copyright'] = htmlspecialchars($epub->getCopyright());
+ $data['language'] = htmlspecialchars($epub->getLanguage());
+ $data['isbn'] = htmlspecialchars($epub->getISBN());
+ return $data;
+ }
+
+ /**
+ * Save Epub data
+ * @param EPub $epub
+ * @return EPub
+ */
+ protected function saveEpubData($epub)
+ {
+ $epub->setTitle($_POST['title']);
+ $epub->setDescription($_POST['description']);
+ $epub->setLanguage($_POST['language']);
+ $epub->setPublisher($_POST['publisher']);
+ $epub->setCopyright($_POST['copyright']);
+ $epub->setIsbn($_POST['isbn']);
+ $epub->setSubjects($_POST['subjects']);
+
+ $authors = [];
+ foreach ((array) $_POST['authorname'] as $num => $name) {
+ if ($name) {
+ $as = $_POST['authoras'][$num];
+ if (!$as) {
+ $as = $name;
+ }
+ $authors[$as] = $name;
+ }
+ }
+ $epub->setAuthors($authors);
+
+ // handle image
+ $cover = '';
+ if (preg_match('/^https?:\/\//i', $_POST['coverurl'])) {
+ $data = @file_get_contents($_POST['coverurl']);
+ if ($data) {
+ $cover = tempnam(sys_get_temp_dir(), 'epubcover');
+ file_put_contents($cover, $data);
+ unset($data);
+ }
+ } elseif(is_uploaded_file($_FILES['coverfile']['tmp_name'])) {
+ $cover = $_FILES['coverfile']['tmp_name'];
+ }
+ if ($cover) {
+ $info = @getimagesize($cover);
+ if (preg_match('/^image\/(gif|jpe?g|png)$/', $info['mime'])) {
+ $epub->setCoverInfo($cover, $info['mime']);
+ } else {
+ $this->error = 'Not a valid image file' . $cover;
+ }
+ }
+
+ // save the ebook
+ try {
+ $epub->save();
+ } catch (Exception $e) {
+ $this->error = $e->getMessage();
+ }
+
+ // clean up temporary cover file
+ if ($cover) {
+ @unlink($cover);
+ }
+
+ return $epub;
+ }
+
+ /**
+ * Rename Epub file
+ * @param EPub $epub
+ * @return string
+ */
+ protected function renameEpubFile($epub)
+ {
+ $author = array_keys($epub->getAuthors())[0];
+ $title = $epub->getTitle();
+ $new = Util::to_file($author . '-' . $title);
+ $new = $this->bookdir . $new . '.epub';
+ $old = $epub->file();
+ if (realpath($new) != realpath($old)) {
+ if (!@rename($old, $new)) {
+ $new = $old; //rename failed, stay here
+ }
+ }
+ return $new;
+ }
+
+ /**
+ * Render template with data
+ * @param string $template
+ * @param array<string, string> $data
+ * @return string
+ */
+ protected function renderTemplate($template, $data)
+ {
+ if (!file_exists($template)) {
+ throw new Exception('Invalid template ' . htmlspecialchars($template));
+ }
+ $content = file_get_contents($template);
+ foreach ($data as $name => $value) {
+ $content = preg_replace('/{{\s*' . $name . '\s*}}/', $value, $content);
+ }
+ return $content;
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/App/Util.php b/vendor/mikespub/php-epub-meta/src/App/Util.php
new file mode 100644
index 000000000..730e9ec95
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/App/Util.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * PHP EPub Meta utility functions for App interface
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Sébastien Lucas <sebastien@slucas.fr>
+ * @author Simon Schrape <simon@epubli.com> © 2015
+ * @author mikespub
+ */
+
+namespace SebLucas\EPubMeta\App;
+
+class Util
+{
+ /**
+ * Summary of to_file
+ * @param string $input
+ * @return string
+ */
+ public static function to_file($input)
+ {
+ $input = str_replace(' ', '_', $input);
+ $input = str_replace('__', '_', $input);
+ $input = str_replace(',_', ',', $input);
+ $input = str_replace('_,', ',', $input);
+ $input = str_replace('-_', '-', $input);
+ $input = str_replace('_-', '-', $input);
+ $input = str_replace(',', '__', $input);
+ return $input;
+ }
+
+ /**
+ * Summary of book_output
+ * @param string $input
+ * @return string
+ */
+ public static function book_output($input)
+ {
+ $input = str_replace('__', ',', $input);
+ $input = str_replace('_', ' ', $input);
+ $input = str_replace(',', ', ', $input);
+ $input = str_replace('-', ' - ', $input);
+ [$author, $title] = explode('-', $input, 2);
+ $author = trim($author);
+ $title = trim($title);
+
+ if (!$title) {
+ $title = $author;
+ $author = '';
+ }
+
+ return '<span class="title">' . htmlspecialchars($title) . '</span>' .
+ '<span class="author">' . htmlspecialchars($author) . '</author>';
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Contents/Nav.php b/vendor/mikespub/php-epub-meta/src/Contents/Nav.php
new file mode 100644
index 000000000..e8eb980a6
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Contents/Nav.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace SebLucas\EPubMeta\Contents;
+
+/**
+ * EPUB NAV structure for EPUB 3
+ *
+ * @author Simon Schrape <simon@epubli.com>
+ */
+class Nav
+{
+ /** @var string from main document */
+ protected $docTitle;
+ /** @var string from main document */
+ protected $docAuthor;
+ /** @var NavPointList */
+ protected $navMap;
+
+ /**
+ * Summary of __construct
+ * @param string $title
+ * @param string $author
+ */
+ public function __construct($title, $author)
+ {
+ $this->docTitle = $title;
+ $this->docAuthor = $author;
+ $this->navMap = new NavPointList();
+ }
+
+ /**
+ * @return string
+ */
+ public function getDocTitle()
+ {
+ return $this->docTitle;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDocAuthor()
+ {
+ return $this->docAuthor;
+ }
+
+ /**
+ * @return NavPointList
+ */
+ public function getNavMap()
+ {
+ return $this->navMap;
+ }
+
+ /**
+ * @param string $file
+ * @return array|NavPoint[]
+ */
+ public function findNavPointsForFile($file)
+ {
+ return $this->getNavMap()->findNavPointsForFile($file);
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Contents/NavPoint.php b/vendor/mikespub/php-epub-meta/src/Contents/NavPoint.php
new file mode 100644
index 000000000..7ffb29dca
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Contents/NavPoint.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace SebLucas\EPubMeta\Contents;
+
+/**
+ * An EPUB TOC navigation point.
+ *
+ * @author Simon Schrape <simon@epubli.com>
+ */
+class NavPoint
+{
+ /** @var string */
+ protected $id;
+ /** @var string */
+ protected $class;
+ /** @var int */
+ protected $playOrder;
+ /** @var string */
+ protected $navLabel;
+ /** @var string */
+ protected $contentSourceFile;
+ /** @var string */
+ protected $contentSourceFragment;
+ /** @var NavPointList */
+ protected $children;
+
+ /**
+ * @param string $id
+ * @param string $class
+ * @param int $playOrder
+ * @param string $label
+ * @param string $contentSource
+ */
+ public function __construct($id, $class, $playOrder, $label, $contentSource)
+ {
+ $this->id = $id;
+ $this->class = $class;
+ $this->playOrder = $playOrder;
+ $this->navLabel = $label;
+ $contentSourceParts = explode('#', $contentSource, 2);
+ $this->contentSourceFile = $contentSourceParts[0];
+ $this->contentSourceFragment = $contentSourceParts[1] ?? null;
+ $this->children = new NavPointList();
+ }
+
+ /**
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getClass()
+ {
+ return $this->class;
+ }
+
+ /**
+ * @return int
+ */
+ public function getPlayOrder()
+ {
+ return $this->playOrder;
+ }
+
+ /**
+ * @return string
+ */
+ public function getNavLabel()
+ {
+ return $this->navLabel;
+ }
+
+ /**
+ * @return string
+ */
+ public function getContentSource()
+ {
+ return $this->contentSourceFile . ($this->contentSourceFragment ? '#' . $this->contentSourceFragment : '');
+ }
+
+ /**
+ * @return string
+ */
+ public function getContentSourceFile()
+ {
+ return $this->contentSourceFile;
+ }
+
+ /**
+ * @return string
+ */
+ public function getContentSourceFragment()
+ {
+ return $this->contentSourceFragment;
+ }
+
+ /**
+ * @return NavPointList
+ */
+ public function getChildren()
+ {
+ return $this->children;
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Contents/NavPointList.php b/vendor/mikespub/php-epub-meta/src/Contents/NavPointList.php
new file mode 100644
index 000000000..540d2f876
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Contents/NavPointList.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace SebLucas\EPubMeta\Contents;
+
+use ArrayIterator;
+
+/**
+ * A list of EPUB TOC navigation points.
+ *
+ * @author Simon Schrape <simon@epubli.com>
+ * @author mikespub
+ * @extends ArrayIterator<int, NavPoint>
+ */
+class NavPointList extends ArrayIterator
+{
+ public function __construct() {}
+
+ /**
+ * @return NavPoint
+ */
+ public function first()
+ {
+ $this->rewind();
+ return $this->current();
+ }
+
+ /**
+ * @return NavPoint
+ */
+ public function last()
+ {
+ $this->seek($this->count() - 1);
+ return $this->current();
+ }
+
+ /**
+ * @param NavPoint $navPoint
+ * @return void
+ * @deprecated 2.1.0 use normal append() instead
+ */
+ public function addNavPoint(NavPoint $navPoint)
+ {
+ $this->append($navPoint);
+ }
+
+ /**
+ * @param string $file
+ *
+ * @return array|NavPoint[]
+ */
+ public function findNavPointsForFile($file)
+ {
+ $matches = [];
+ foreach ($this as $navPoint) {
+ if ($navPoint->getContentSourceFile() == $file) {
+ $matches[] = $navPoint;
+ }
+ $childMatches = $navPoint->getChildren()->findNavPointsForFile($file);
+ if (count($childMatches)) {
+ $matches = array_merge($matches, $childMatches);
+ }
+ }
+ return $matches;
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Contents/Spine.php b/vendor/mikespub/php-epub-meta/src/Contents/Spine.php
new file mode 100644
index 000000000..e6dfc2a84
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Contents/Spine.php
@@ -0,0 +1,198 @@
+<?php
+
+namespace SebLucas\EPubMeta\Contents;
+
+use SebLucas\EPubMeta\Data\Item;
+use ArrayAccess;
+use Countable;
+use Iterator;
+use BadMethodCallException;
+
+/**
+ * EPUB spine structure
+ *
+ * @author Simon Schrape <simon@epubli.com>
+ * @implements \Iterator<int, Item>
+ * @implements \ArrayAccess<int, Item>
+ */
+class Spine implements Iterator, Countable, ArrayAccess
+{
+ /** @var Item */
+ protected $tocItem;
+ protected string $tocFormat;
+ /** @var array|Item[] The ordered list of all Items in this Spine. */
+ protected $items = [];
+
+ /**
+ * Spine Constructor.
+ *
+ * @param Item $tocItem The TOC Item of this Spine.
+ * @param string $tocFormat The TOC Format of this Spine (Toc or Nav).
+ */
+ public function __construct(Item $tocItem, string $tocFormat)
+ {
+ $this->tocItem = $tocItem;
+ $this->tocFormat = $tocFormat;
+ }
+
+ /**
+ * Get the TOC Item of this Spine.
+ *
+ * @return Item
+ */
+ public function getTocItem()
+ {
+ return $this->tocItem;
+ }
+
+ /**
+ * Get the TOC Format of this Spine.
+ *
+ * @return string
+ */
+ public function getTocFormat()
+ {
+ return $this->tocFormat;
+ }
+
+ /**
+ * Append an Item to this Spine.
+ *
+ * @param Item $item The Item to append to this Spine.
+ * @return void
+ */
+ public function appendItem(Item $item)
+ {
+ $this->items[] = $item;
+ }
+
+ /**
+ * Return the current Item while iterating this Spine.
+ *
+ * @link http://php.net/manual/en/iterator.current.php
+ * @return Item
+ */
+ public function current(): Item
+ {
+ return current($this->items);
+ }
+
+ /**
+ * Move forward to next Item while iterating this Spine.
+ * @link http://php.net/manual/en/iterator.next.php
+ * @return void Any returned value is ignored.
+ */
+ public function next(): void
+ {
+ next($this->items);
+ }
+
+ /**
+ * Return the index of the current Item while iterating this Spine.
+ *
+ * @link http://php.net/manual/en/iterator.key.php
+ * @return int|null on success, or null on failure.
+ */
+ public function key(): ?int
+ {
+ return key($this->items);
+ }
+
+ /**
+ * Checks if current Iterator position is valid.
+ *
+ * @link http://php.net/manual/en/iterator.valid.php
+ * @return boolean true on success or false on failure.
+ */
+ public function valid(): bool
+ {
+ return (bool) current($this->items);
+ }
+
+ /**
+ * Rewind the Iterator to the first element.
+ *
+ * @link http://php.net/manual/en/iterator.rewind.php
+ * @return void Any returned value is ignored.
+ */
+ public function rewind(): void
+ {
+ reset($this->items);
+ }
+
+ /**
+ * Get the first Item of this Spine.
+ *
+ * @return Item
+ */
+ public function first()
+ {
+ return reset($this->items);
+ }
+
+ /**
+ * Get the last Item of this Spine.
+ *
+ * @return Item
+ */
+ public function last()
+ {
+ return end($this->items);
+ }
+
+ /**
+ * Count items of this Spine.
+ *
+ * @link https://php.net/manual/en/countable.count.php
+ * @return int The number of Items contained in this Spine.
+ */
+ public function count(): int
+ {
+ return count($this->items);
+ }
+
+ /**
+ * Whether a offset exists
+ * @link https://php.net/manual/en/arrayaccess.offsetexists.php
+ * @param int $offset An offset to check for.
+ * @return boolean true on success or false on failure.
+ */
+ public function offsetExists($offset): bool
+ {
+ return isset($this->items[$offset]);
+ }
+
+ /**
+ * Offset to retrieve
+ * @link https://php.net/manual/en/arrayaccess.offsetget.php
+ * @param int $offset The offset to retrieve.
+ * @return Item
+ */
+ public function offsetGet($offset): Item
+ {
+ return $this->items[$offset];
+ }
+
+ /**
+ * Offset to set
+ * @link https://php.net/manual/en/arrayaccess.offsetset.php
+ * @param mixed $offset The offset to assign the value to.
+ * @param mixed $value The value to set.
+ * @throws BadMethodCallException
+ */
+ public function offsetSet($offset, $value): void
+ {
+ throw new BadMethodCallException("Only reading array access is supported!");
+ }
+
+ /**
+ * Offset to unset
+ * @link https://php.net/manual/en/arrayaccess.offsetunset.php
+ * @param mixed $offset The offset to unset.
+ * @throws BadMethodCallException
+ */
+ public function offsetUnset($offset): void
+ {
+ throw new BadMethodCallException("Only reading array access is supported!");
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Contents/Toc.php b/vendor/mikespub/php-epub-meta/src/Contents/Toc.php
new file mode 100644
index 000000000..7992c4a8e
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Contents/Toc.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace SebLucas\EPubMeta\Contents;
+
+/**
+ * EPUB TOC structure for EPUB 2
+ *
+ * @author Simon Schrape <simon@epubli.com>
+ */
+class Toc
+{
+ /** @var string */
+ protected $docTitle;
+ /** @var string */
+ protected $docAuthor;
+ /** @var NavPointList */
+ protected $navMap;
+
+ /**
+ * Summary of __construct
+ * @param string $title
+ * @param string $author
+ */
+ public function __construct($title, $author)
+ {
+ $this->docTitle = $title;
+ $this->docAuthor = $author;
+ $this->navMap = new NavPointList();
+ }
+
+ /**
+ * @return string
+ */
+ public function getDocTitle()
+ {
+ return $this->docTitle;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDocAuthor()
+ {
+ return $this->docAuthor;
+ }
+
+ /**
+ * @return NavPointList
+ */
+ public function getNavMap()
+ {
+ return $this->navMap;
+ }
+
+ /**
+ * @param string $file
+ * @return array|NavPoint[]
+ */
+ public function findNavPointsForFile($file)
+ {
+ return $this->getNavMap()->findNavPointsForFile($file);
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Data/Item.php b/vendor/mikespub/php-epub-meta/src/Data/Item.php
new file mode 100644
index 000000000..271462920
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Data/Item.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace SebLucas\EPubMeta\Data;
+
+use SebLucas\EPubMeta\Tools\HtmlTools;
+use DOMDocument;
+use DOMElement;
+use DOMText;
+use DOMXPath;
+use Exception;
+
+/**
+ * An item of the EPUB manifest.
+ *
+ * @author Simon Schrape <simon@epubli.com>
+ */
+class Item
+{
+ public const XHTML = 'application/xhtml+xml';
+ /** @var string */
+ protected $id;
+ /** @var string The path to the corresponding file. */
+ protected $href;
+ /** @var string */
+ protected $mediaType;
+ /** @var callable|null A callable to get data from the referenced file. */
+ protected $dataCallable;
+ /** @var string The data read from the referenced file. */
+ protected $data;
+ /** @var int The size of the referenced file. */
+ protected $size;
+
+ /**
+ * @param string $id This Item’s identifier.
+ * @param string $href The path to the corresponding file.
+ * @param callable $dataCallable A callable to get data from the referenced file.
+ * @param int $size The size of the referenced file.
+ * @param string|null $mediaType The media type of the corresponding file. If omitted XHTML is assumed.
+ */
+ public function __construct($id, $href, $dataCallable, $size, $mediaType = null)
+ {
+ $this->id = $id;
+ $this->href = $href;
+ $this->dataCallable = $dataCallable;
+ $this->size = $size;
+ $this->mediaType = $mediaType ?: static::XHTML;
+ }
+
+ /**
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getHref()
+ {
+ return $this->href;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMediaType()
+ {
+ return $this->mediaType;
+ }
+
+ /**
+ * Extract (a part of) the contents from the referenced XML file.
+ *
+ * @param string|null $fragmentBegin ID of the element where to start reading the contents.
+ * @param string|null $fragmentEnd ID of the element where to stop reading the contents.
+ * @param bool $keepMarkup Whether to keep the XHTML markup rather than extracted plain text.
+ * @return string The contents of that fragment.
+ * @throws Exception
+ */
+ public function getContents($fragmentBegin = null, $fragmentEnd = null, $keepMarkup = false)
+ {
+ $dom = new DOMDocument();
+ $dom->loadXML(HtmlTools::convertEntitiesNamedToNumeric($this->getData()));
+
+ // get the starting point
+ if ($fragmentBegin) {
+ $xp = new DOMXPath($dom);
+ $node = $xp->query("//*[@id='$fragmentBegin']")->item(0);
+ if (!$node) {
+ throw new Exception("Begin of fragment not found: No element with ID $fragmentBegin!");
+ }
+ } else {
+ $node = $dom->getElementsByTagName('body')->item(0) ?: $dom->documentElement;
+ }
+
+ $allowableTags = [
+ 'br',
+ 'p',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'span',
+ 'div',
+ 'i',
+ 'strong',
+ 'b',
+ 'table',
+ 'td',
+ 'th',
+ 'tr',
+ ];
+ $contents = '';
+ $endTags = [];
+ /** @var DOMElement|DOMText $node */
+ // traverse DOM structure till end point is reached, accumulating the contents
+ while ($node && (!$fragmentEnd || !$node->hasAttributes() || $node->getAttribute('id') != $fragmentEnd)) {
+ if ($node instanceof DOMText) {
+ // when encountering a text node append its value to the contents
+ $contents .= $keepMarkup ? htmlspecialchars($node->nodeValue) : $node->nodeValue;
+ } elseif ($node instanceof DOMElement) {
+ $tag = $node->localName;
+ if ($keepMarkup && in_array($tag, $allowableTags)) {
+ $contents .= "<$tag>";
+ $endTags[] = "</$tag>";
+ } elseif (HtmlTools::isBlockLevelElement($tag)) {
+ // add whitespace between contents of adjacent blocks
+ $endTags[] = PHP_EOL;
+ } else {
+ $endTags[] = '';
+ }
+
+ if ($node->hasChildNodes()) {
+ // step into
+ $node = $node->firstChild;
+ continue;
+ }
+ }
+
+ // leave node
+ while ($node) {
+ if ($node instanceof DOMElement) {
+ $contents .= array_pop($endTags);
+ }
+
+ if ($node->nextSibling) {
+ // step right
+ $node = $node->nextSibling;
+ break;
+ } elseif ($node = $node->parentNode) {
+ // step out
+ continue;
+ } elseif ($fragmentEnd) {
+ // reached end of DOM without finding fragment end
+ throw new Exception("End of fragment not found: No element with ID $fragmentEnd!");
+ }
+ }
+ }
+ while ($endTags) {
+ $contents .= array_pop($endTags);
+ }
+
+ return $contents;
+ }
+
+ /**
+ * Get the file data.
+ *
+ * @return string The binary data of the corresponding file.
+ */
+ public function getData()
+ {
+ if ($this->dataCallable) {
+ $this->data = call_user_func($this->dataCallable);
+ $this->dataCallable = null;
+ }
+
+ return $this->data;
+ }
+
+ /**
+ * Get the size of the corresponding file.
+ *
+ * @return int
+ */
+ public function getSize()
+ {
+ return $this->size ?: strlen($this->getData());
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Data/Manifest.php b/vendor/mikespub/php-epub-meta/src/Data/Manifest.php
new file mode 100644
index 000000000..428b2cc02
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Data/Manifest.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace SebLucas\EPubMeta\Data;
+
+use ArrayAccess;
+use Countable;
+use Exception;
+use BadMethodCallException;
+use Iterator;
+
+/**
+ * EPUB manifest structure
+ *
+ * @author Simon Schrape <simon@epubli.com>
+ * @implements \Iterator<string, Item>
+ * @implements \ArrayAccess<string, Item>
+ */
+class Manifest implements Iterator, Countable, ArrayAccess
+{
+ /** @var array|Item[] The map of all Items in this Manifest indexed by their IDs. */
+ protected $items = [];
+
+ /**
+ * Create and add an Item with the given properties.
+ *
+ * @param string $id The identifier of the new item.
+ * @param string $href The relative path of the referenced file in the EPUB.
+ * @param callable $callable A callable to get data from the referenced file in the EPUB.
+ * @param int $size The size of the referenced file in the EPUB.
+ * @param string|null $mediaType
+ * @return Item The newly created Item.
+ * @throws Exception If $id is already taken.
+ */
+ public function createItem($id, $href, $callable, $size, $mediaType = null)
+ {
+ if (isset($this->items[$id])) {
+ throw new Exception("Item with ID $id already exists!");
+ }
+ $item = new Item($id, $href, $callable, $size, $mediaType);
+ $this->items[$id] = $item;
+
+ return $item;
+ }
+
+ /**
+ * Return the current Item while iterating this Manifest.
+ *
+ * @link http://php.net/manual/en/iterator.current.php
+ * @return Item
+ */
+ public function current(): Item
+ {
+ return current($this->items);
+ }
+
+ /**
+ * Move forward to next Item while iterating this Manifest.
+ * @link http://php.net/manual/en/iterator.next.php
+ * @return void Any returned value is ignored.
+ */
+ public function next(): void
+ {
+ next($this->items);
+ }
+
+ /**
+ * Return the ID of the current Item while iterating this Manifest.
+ *
+ * @link http://php.net/manual/en/iterator.key.php
+ * @return string|null on success, or null on failure.
+ */
+ public function key(): ?string
+ {
+ return key($this->items);
+ }
+
+ /**
+ * Checks if current Iterator position is valid.
+ *
+ * @link http://php.net/manual/en/iterator.valid.php
+ * @return boolean true on success or false on failure.
+ */
+ public function valid(): bool
+ {
+ return (bool) current($this->items);
+ }
+
+ /**
+ * Rewind the Iterator to the first element.
+ *
+ * @link http://php.net/manual/en/iterator.rewind.php
+ * @return void Any returned value is ignored.
+ */
+ public function rewind(): void
+ {
+ reset($this->items);
+ }
+
+ /**
+ * Get the first Item of this Manifest.
+ *
+ * @return Item
+ */
+ public function first()
+ {
+ return reset($this->items);
+ }
+
+ /**
+ * Get the last Item of this Manifest.
+ *
+ * @return Item
+ */
+ public function last()
+ {
+ return end($this->items);
+ }
+
+ /**
+ * Count items of this Manifest.
+ *
+ * @link https://php.net/manual/en/countable.count.php
+ * @return int The number of Items contained in this Manifest.
+ */
+ public function count(): int
+ {
+ return count($this->items);
+ }
+
+ /**
+ * Whether a offset exists
+ * @link https://php.net/manual/en/arrayaccess.offsetexists.php
+ * @param string $offset An offset to check for.
+ * @return boolean true on success or false on failure.
+ */
+ public function offsetExists($offset): bool
+ {
+ return isset($this->items[$offset]);
+ }
+
+ /**
+ * Offset to retrieve
+ * @link https://php.net/manual/en/arrayaccess.offsetget.php
+ * @param string $offset The offset to retrieve.
+ * @return Item
+ */
+ public function offsetGet($offset): Item
+ {
+ return $this->items[$offset];
+ }
+
+ /**
+ * Offset to set
+ * @link https://php.net/manual/en/arrayaccess.offsetset.php
+ * @param mixed $offset The offset to assign the value to.
+ * @param mixed $value The value to set.
+ * @throws BadMethodCallException
+ */
+ public function offsetSet($offset, $value): void
+ {
+ throw new BadMethodCallException("Only reading array access is supported!");
+ }
+
+ /**
+ * Offset to unset
+ * @link https://php.net/manual/en/arrayaccess.offsetunset.php
+ * @param mixed $offset The offset to unset.
+ * @throws BadMethodCallException
+ */
+ public function offsetUnset($offset): void
+ {
+ throw new BadMethodCallException("Only reading array access is supported!");
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Dom/Element.php b/vendor/mikespub/php-epub-meta/src/Dom/Element.php
new file mode 100644
index 000000000..2a7058eaa
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Dom/Element.php
@@ -0,0 +1,196 @@
+<?php
+/**
+ * PHP EPub Meta library
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Sébastien Lucas <sebastien@slucas.fr>
+ */
+
+namespace SebLucas\EPubMeta\Dom;
+
+use DOMElement;
+
+/**
+ * EPUB-specific subclass of DOMElement
+ *
+ * Source: https://github.com/splitbrain/php-epub-meta
+ * @author Andreas Gohr <andi@splitbrain.org> © 2012
+ * @author Simon Schrape <simon@epubli.com> © 2015–2018
+ * @author Sébastien Lucas <sebastien@slucas.fr>
+ *
+ * @property string $nodeValueUnescaped
+ */
+class Element extends DOMElement
+{
+ /** @var array<string, string> */
+ public static $namespaces = [
+ 'n' => 'urn:oasis:names:tc:opendocument:xmlns:container',
+ 'opf' => 'http://www.idpf.org/2007/opf',
+ 'dc' => 'http://purl.org/dc/elements/1.1/',
+ 'ncx' => 'http://www.daisy.org/z3986/2005/ncx/',
+ ];
+
+ /**
+ * Summary of __construct
+ * @param string $name
+ * @param string $value
+ * @param string $namespaceUri
+ */
+ public function __construct($name, $value = '', $namespaceUri = '')
+ {
+ [$prefix, $name] = $this->splitQualifiedName($name);
+ $value = htmlspecialchars($value);
+ if (!$namespaceUri && $prefix) {
+ //$namespaceUri = XmlNamespace::getUri($prefix);
+ $namespaceUri = static::$namespaces[$prefix];
+ }
+ parent::__construct($name, $value, $namespaceUri);
+ }
+
+ /**
+ * Summary of __get
+ * @param string $name
+ * @return string|null
+ */
+ public function __get($name)
+ {
+ switch ($name) {
+ case 'nodeValueUnescaped':
+ return htmlspecialchars_decode($this->nodeValue);
+ }
+
+ return null;
+ }
+
+ /**
+ * Summary of __set
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ */
+ public function __set($name, $value)
+ {
+ switch ($name) {
+ case 'nodeValueUnescaped':
+ $this->nodeValue = htmlspecialchars($value);
+ }
+ }
+
+ /**
+ * Create and append a new child
+ *
+ * Works with our epub namespaces and omits default namespaces
+ * @param string $name
+ * @param string $value
+ * @return Element|bool
+ */
+ public function newChild($name, $value = '')
+ {
+ [$localName, $namespaceUri] = $this->getNameContext($name);
+
+ // this doesn't call the constructor: $node = $this->ownerDocument->createElement($name,$value);
+ $node = new Element($namespaceUri ? $name : $localName, $value, $namespaceUri);
+
+ /** @var Element $node */
+ $node = $this->appendChild($node);
+ return $node;
+ }
+
+ /**
+ * Simple EPUB namespace aware attribute getter
+ * @param string $name
+ * @return string
+ */
+ public function getAttrib($name)
+ {
+ [$localName, $namespaceUri] = $this->getNameContext($name);
+
+ // return value if none was given
+ if ($namespaceUri) {
+ return $this->getAttributeNS($namespaceUri, $localName);
+ } else {
+ return $this->getAttribute($localName);
+ }
+ }
+
+ /**
+ * Simple EPUB namespace aware attribute setter
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ */
+ public function setAttrib($name, $value)
+ {
+ [$localName, $namespaceUri] = $this->getNameContext($name);
+
+ if ($namespaceUri) {
+ $this->setAttributeNS($namespaceUri, $localName, $value);
+ } else {
+ $this->setAttribute($localName, $value);
+ }
+ }
+
+ /**
+ * Simple EPUB namespace aware attribute remover
+ * @param string $name
+ * @return void
+ */
+ public function removeAttrib($name)
+ {
+ [$localName, $namespaceUri] = $this->getNameContext($name);
+
+ if ($namespaceUri) {
+ $this->removeAttributeNS($namespaceUri, $localName);
+ } else {
+ $this->removeAttribute($localName);
+ }
+ }
+
+ /**
+ * Remove this node from the DOM
+ * @return void
+ */
+ public function delete()
+ {
+ $this->parentNode->removeChild($this);
+ }
+
+ /**
+ * Split given name in namespace prefix and local part
+ *
+ * @param string $name
+ * @return array<string> (prefix, name)
+ */
+ protected function splitQualifiedName($name)
+ {
+ $list = explode(':', $name, 2);
+ if (count($list) < 2) {
+ array_unshift($list, '');
+ }
+
+ return $list;
+ }
+
+ /**
+ * @param string $name
+ * @return array<string>
+ */
+ protected function getNameContext($name)
+ {
+ [$prefix, $localName] = $this->splitQualifiedName($name);
+
+ $namespaceUri = '';
+ if ($prefix) {
+ //$namespaceUri = XmlNamespace::getUri($prefix);
+ $namespaceUri = static::$namespaces[$prefix];
+ if (
+ !$this->namespaceURI && $this->isDefaultNamespace($namespaceUri)
+ || $this->namespaceURI == $namespaceUri
+ ) {
+ $namespaceUri = '';
+ }
+ }
+
+ return [$localName, $namespaceUri];
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Dom/XPath.php b/vendor/mikespub/php-epub-meta/src/Dom/XPath.php
new file mode 100644
index 000000000..4d208c1cd
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Dom/XPath.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * PHP EPub Meta library
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Sébastien Lucas <sebastien@slucas.fr>
+ */
+
+namespace SebLucas\EPubMeta\Dom;
+
+use DOMDocument;
+use DOMXPath;
+
+/**
+ * EPUB-specific subclass of DOMXPath
+ *
+ * Source: https://github.com/splitbrain/php-epub-meta
+ * @author Andreas Gohr <andi@splitbrain.org> © 2012
+ * @author Simon Schrape <simon@epubli.com> © 2015–2018
+ * @author Sébastien Lucas <sebastien@slucas.fr>
+ */
+class XPath extends DOMXPath
+{
+ public function __construct(DOMDocument $doc)
+ {
+ parent::__construct($doc);
+
+ foreach (Element::$namespaces as $prefix => $namespaceUri) {
+ $this->registerNamespace($prefix, $namespaceUri);
+ }
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/EPub.php b/vendor/mikespub/php-epub-meta/src/EPub.php
new file mode 100644
index 000000000..171fd7641
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/EPub.php
@@ -0,0 +1,2126 @@
+<?php
+/**
+ * PHP EPub Meta library
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Sébastien Lucas <sebastien@slucas.fr>
+ * @author Simon Schrape <simon@epubli.com> © 2015
+ */
+
+namespace SebLucas\EPubMeta;
+
+use SebLucas\EPubMeta\Dom\Element as EpubDomElement;
+use SebLucas\EPubMeta\Dom\XPath as EpubDomXPath;
+use SebLucas\EPubMeta\Data\Manifest;
+use SebLucas\EPubMeta\Contents\Nav;
+use SebLucas\EPubMeta\Contents\Spine;
+use SebLucas\EPubMeta\Contents\NavPoint as TocNavPoint;
+use SebLucas\EPubMeta\Contents\NavPointList as TocNavPointList;
+use SebLucas\EPubMeta\Contents\Toc;
+use SebLucas\EPubMeta\Tools\ZipEdit;
+use SebLucas\EPubMeta\Tools\ZipFile;
+use DOMDocument;
+use DOMElement;
+use DOMNodeList;
+use Exception;
+use InvalidArgumentException;
+use JsonException;
+use ZipArchive;
+
+class EPub
+{
+ /** Identifier for cover image inserted by this lib. */
+ public const COVER_ID = 'php-epub-meta-cover';
+ /** Identifier for title page inserted by this lib. */
+ public const TITLE_PAGE_ID = 'php-epub-meta-titlepage';
+ public const METADATA_FILE = 'META-INF/container.xml';
+ public const MIME_TYPE = 'application/epub+zip';
+ public const BOOKMARK_FILE = 'META-INF/calibre_bookmarks.txt';
+ public const EPUB_FILE_TYPE_MAGIC = "encoding=json+base64:\n";
+ /** @var array<int, array<string>> */
+ public static $encodeNameReplace = [
+ ['/', '-'],
+ ['~SLASH~', '~DASH~'],
+ ];
+ /** @var array<int, array<string>> */
+ public static $decodeNameReplace = [
+ ['~SLASH~', '~DASH~'],
+ ['/', '-'],
+ ];
+
+ /** @var DOMDocument */
+ public $xml; //FIXME: change to protected, later
+ /** @var DOMDocument|null */
+ public $toc;
+ /** @var DOMDocument|null */
+ public $nav;
+ /** @var EpubDomXPath */
+ protected $xpath;
+ /** @var EpubDomXPath */
+ protected $toc_xpath;
+ /** @var EpubDomXPath */
+ protected $nav_xpath;
+ protected string $file;
+ protected string $meta;
+ /** @var ZipEdit|ZipFile */
+ protected $zip;
+ protected string $zipClass;
+ protected string $coverpath = '';
+ /** @var mixed */
+ protected $namespaces;
+ protected string $imagetoadd = '';
+ /** @var array<mixed> A map of ZIP items mapping filenames to file sizes */
+ protected $zipSizeMap;
+ /** @var Manifest|null The manifest (catalog of files) of this EPUB */
+ protected $manifest;
+ /** @var Spine|null The spine structure of this EPUB */
+ protected $spine;
+ /** @var Toc|Nav|null The TOC structure of this EPUB */
+ protected $tocnav;
+ protected int $epubVersion = 0;
+
+ /**
+ * Constructor
+ *
+ * @param string $file path to epub file to work on
+ * @param string $zipClass class to handle zip - ZipFile is read-only
+ * @throws Exception if metadata could not be loaded
+ */
+ public function __construct($file, $zipClass = ZipFile::class)
+ {
+ if (!is_file($file)) {
+ throw new Exception("Epub file does not exist!");
+ }
+ if (filesize($file) <= 0) {
+ throw new Exception("Epub file is empty!");
+ }
+ // open file
+ $this->file = $file;
+ $this->openZipFile($zipClass);
+
+ // read container data
+ $this->loadMetadata();
+ }
+
+ /**
+ * Summary of openZipFile
+ * @param string $zipClass
+ * @throws \Exception
+ * @return void
+ */
+ public function openZipFile($zipClass)
+ {
+ $this->zip = new $zipClass();
+ if (!$this->zip->Open($this->file)) {
+ throw new Exception('Failed to read epub file');
+ }
+ $this->zipClass = $zipClass;
+ }
+
+ /**
+ * Summary of loadMetadata
+ * @throws \Exception
+ * @return void
+ */
+ public function loadMetadata()
+ {
+ if (!$this->zip->FileExists(static::METADATA_FILE)) {
+ throw new Exception('Unable to find ' . static::METADATA_FILE);
+ }
+
+ $data = $this->zip->FileRead(static::METADATA_FILE);
+ if ($data == false) {
+ throw new Exception('Failed to access epub container data');
+ }
+ $xml = new DOMDocument();
+ $xml->registerNodeClass(DOMElement::class, EpubDomElement::class);
+ $xml->loadXML($data);
+ $xpath = new EpubDomXPath($xml);
+ $nodes = $xpath->query('//n:rootfiles/n:rootfile[@media-type="application/oebps-package+xml"]');
+ $this->meta = static::getAttr($nodes, 'full-path');
+
+ // load metadata
+ if (!$this->zip->FileExists($this->meta)) {
+ throw new Exception('Unable to find ' . $this->meta);
+ }
+
+ $data = $this->zip->FileRead($this->meta);
+ if (!$data) {
+ throw new Exception('Failed to access epub metadata');
+ }
+ $this->loadXmlData($data);
+
+ $this->zipSizeMap = $this->loadSizeMap($this->file);
+ }
+
+ /**
+ * Summary of loadXmlData
+ * @param string $data
+ * @return void
+ */
+ public function loadXmlData($data)
+ {
+ $this->xml = new DOMDocument();
+ $this->xml->registerNodeClass(DOMElement::class, EpubDomElement::class);
+ $this->xml->loadXML($data);
+ $this->xml->formatOutput = true;
+ $this->xpath = new EpubDomXPath($this->xml);
+ }
+
+ /**
+ * Summary of initSpineComponent
+ * @throws \Exception
+ * @return void
+ */
+ public function initSpineComponent()
+ {
+ $nodes = $this->xpath->query('//opf:spine');
+ $tocid = static::getAttr($nodes, 'toc');
+ if (empty($tocid)) {
+ $nodes = $this->xpath->query('//opf:manifest/opf:item[@properties="nav"]');
+ $navhref = static::getAttr($nodes, 'href');
+ $navpath = $this->getFullPath($navhref);
+ // read epub nav doc
+ if (!$this->zip->FileExists($navpath)) {
+ throw new Exception('Unable to find ' . $navpath);
+ }
+ $data = $this->zip->FileRead($navpath);
+ $this->loadNavData($data);
+ return;
+ }
+ $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="' . $tocid . '"]');
+ $tochref = static::getAttr($nodes, 'href');
+ $tocpath = $this->getFullPath($tochref);
+ // read epub toc
+ if (!$this->zip->FileExists($tocpath)) {
+ throw new Exception('Unable to find ' . $tocpath);
+ }
+
+ $data = $this->zip->FileRead($tocpath);
+ $this->loadTocData($data);
+ }
+
+ /**
+ * Summary of loadNavData
+ * @param string $data
+ * @return void
+ */
+ public function loadNavData($data)
+ {
+ $this->nav = new DOMDocument();
+ $this->nav->registerNodeClass(DOMElement::class, EpubDomElement::class);
+ $this->nav->loadXML($data);
+ $this->nav_xpath = new EpubDomXPath($this->nav);
+ $rootNamespace = $this->nav->lookupNamespaceUri($this->nav->namespaceURI);
+ $this->nav_xpath->registerNamespace('x', $rootNamespace);
+ }
+
+ /**
+ * Summary of loadTocData
+ * @param string $data
+ * @return void
+ */
+ public function loadTocData($data)
+ {
+ $this->toc = new DOMDocument();
+ $this->toc->registerNodeClass(DOMElement::class, EpubDomElement::class);
+ $this->toc->loadXML($data);
+ $this->toc_xpath = new EpubDomXPath($this->toc);
+ $rootNamespace = $this->toc->lookupNamespaceUri($this->toc->namespaceURI);
+ $this->toc_xpath->registerNamespace('x', $rootNamespace);
+ }
+
+ /**
+ * Get the ePub version
+ *
+ * @return int The number of the ePub version (2 or 3 for now) or 0 if not found
+ */
+ public function getEpubVersion()
+ {
+ if ($this->epubVersion) {
+ return $this->epubVersion;
+ }
+
+ $this->epubVersion = 0;
+ $nodes = $this->xpath->query('//opf:package[@unique-identifier="BookId"]');
+ if ($nodes->length) {
+ $this->epubVersion = (int) static::getAttr($nodes, 'version');
+ } else {
+ $nodes = $this->xpath->query('//opf:package');
+ if ($nodes->length) {
+ $this->epubVersion = (int) static::getAttr($nodes, 'version');
+ }
+ }
+
+ return $this->epubVersion;
+ }
+
+ /**
+ * file name getter
+ * @return string
+ */
+ public function file()
+ {
+ return $this->file;
+ }
+
+ /**
+ * meta file getter
+ * @return string
+ */
+ public function meta()
+ {
+ return $this->meta;
+ }
+
+ /**
+ * Close the epub file
+ * @return void
+ */
+ public function close()
+ {
+ $this->zip->FileCancelModif($this->meta);
+ // TODO: Add cancelation of cover image
+ $this->zip->Close();
+ }
+
+ /**
+ * Remove iTunes files
+ * @return void
+ */
+ public function cleanITunesCrap()
+ {
+ if ($this->zip->FileExists('iTunesMetadata.plist')) {
+ $this->zip->FileDelete('iTunesMetadata.plist');
+ }
+ if ($this->zip->FileExists('iTunesArtwork')) {
+ $this->zip->FileDelete('iTunesArtwork');
+ }
+ }
+
+ /**
+ * Writes back all meta data changes
+ * @return void
+ */
+ public function save()
+ {
+ $this->download();
+ $this->zip->close();
+ }
+
+ /**
+ * Get the updated epub
+ * @param mixed $file
+ * @param bool $sendHeaders
+ * @return void
+ */
+ public function download($file = false, $sendHeaders = true)
+ {
+ $this->zip->FileReplace($this->meta, $this->xml->saveXML());
+ // add the cover image
+ if ($this->imagetoadd) {
+ $this->zip->FileAddPath($this->coverpath, $this->imagetoadd);
+ $this->imagetoadd = '';
+ }
+ if ($file) {
+ $render = $this->zipClass::DOWNLOAD;
+ $this->zip->Flush($render, $file, static::MIME_TYPE, $sendHeaders);
+ } elseif ($this->zipClass == ZipEdit::class) {
+ $this->zip->SaveBeforeClose();
+ }
+ }
+
+ /**
+ * Get the components list as an array
+ * @return array<mixed>
+ */
+ public function components()
+ {
+ $spine = [];
+ $nodes = $this->xpath->query('//opf:spine/opf:itemref');
+ foreach ($nodes as $node) {
+ /** @var EpubDomElement $node */
+ $idref = $node->getAttribute('idref');
+ /** @var EpubDomElement $item */
+ $item = $this->xpath->query('//opf:manifest/opf:item[@id="' . $idref . '"]')->item(0);
+ $spine[] = $this->encodeComponentName($item->getAttribute('href'));
+ }
+ return $spine;
+ }
+
+ /**
+ * Get the component content
+ * @param mixed $comp
+ * @return mixed
+ */
+ public function component($comp)
+ {
+ $path = $this->decodeComponentName($comp);
+ $path = $this->getFullPath($path);
+ if (!$this->zip->FileExists($path)) {
+ $status = $this->zip->FileGetState($path);
+ throw new Exception('Unable to find ' . $path . ' <' . $comp . '> = ' . $status);
+ }
+
+ $data = $this->zip->FileRead($path);
+ return $data;
+ }
+
+ /**
+ * Summary of getComponentName
+ * @param mixed $comp
+ * @param mixed $elementPath
+ * @return bool|string
+ */
+ public function getComponentName($comp, $elementPath)
+ {
+ $path = $this->decodeComponentName($comp);
+ $path = $this->getFullPath($path, $elementPath);
+ if (!$this->zip->FileExists($path)) {
+ error_log('Unable to find ' . $path);
+ return false;
+ }
+ $ref = dirname('/' . $this->meta);
+ $ref = ltrim($ref, '\\');
+ $ref = ltrim($ref, '/');
+ if (strlen($ref) > 0) {
+ $path = str_replace($ref . '/', '', $path);
+ }
+ return $this->encodeComponentName($path);
+ }
+
+ /**
+ * Encode the component name (to replace / and -)
+ * @param mixed $src
+ * @return string
+ */
+ protected static function encodeComponentName($src)
+ {
+ return str_replace(
+ static::$encodeNameReplace[0],
+ static::$encodeNameReplace[1],
+ $src
+ );
+ }
+
+ /**
+ * Decode the component name (to replace / and -)
+ * @param mixed $src
+ * @return string
+ */
+ protected static function decodeComponentName($src)
+ {
+ return str_replace(
+ static::$decodeNameReplace[0],
+ static::$decodeNameReplace[1],
+ $src
+ );
+ }
+
+
+ /**
+ * Get the component content type
+ * @param mixed $comp
+ * @return string
+ */
+ public function componentContentType($comp)
+ {
+ $comp = $this->decodeComponentName($comp);
+ $nodes = $this->xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]');
+ if ($nodes->length) {
+ return static::getAttr($nodes, 'media-type');
+ }
+
+ // I had at least one book containing %20 instead of spaces in the opf file
+ $comp = str_replace(' ', '%20', $comp);
+ $nodes = $this->xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]');
+ if ($nodes->length) {
+ return static::getAttr($nodes, 'media-type');
+ }
+ return 'application/octet-stream';
+ }
+
+ /**
+ * Summary of getComponentSize
+ * @param mixed $comp
+ * @return bool|int
+ */
+ public function getComponentSize($comp)
+ {
+ $path = $this->decodeComponentName($comp);
+ $path = $this->getFullPath($path);
+ if (!$this->zip->FileExists($path)) {
+ error_log('Unable to find ' . $path);
+ return false;
+ }
+
+ $sizeMap = $this->loadSizeMap();
+ return $sizeMap[$path];
+ }
+
+ /**
+ * EPUB 2 navigation control file (NCX format)
+ * See https://idpf.org/epub/20/spec/OPF_2.0_latest.htm#Section2.4.1
+ * @param mixed $node
+ * @return array<string, string>
+ */
+ protected function getNavPointDetail($node)
+ {
+ $title = $this->toc_xpath->query('x:navLabel/x:text', $node)->item(0)->nodeValue;
+ $nodes = $this->toc_xpath->query('x:content', $node);
+ $src = static::getAttr($nodes, 'src');
+ $src = $this->encodeComponentName($src);
+ $item = ['title' => preg_replace('~[\r\n]+~', '', $title), 'src' => $src];
+ $insidenodes = $this->toc_xpath->query('x:navPoint', $node);
+ if (count($insidenodes) < 1) {
+ return $item;
+ }
+ $item['children'] = [];
+ foreach ($insidenodes as $insidenode) {
+ $item['children'][] = $this->getNavPointDetail($insidenode);
+ }
+ return $item;
+ }
+
+ /**
+ * EPUB 3 navigation document (toc nav element)
+ * See https://www.w3.org/TR/epub-33/#sec-nav-toc
+ * @param mixed $node
+ * @return array<string, string>
+ */
+ protected function getNavTocListItem($node)
+ {
+ $nodes = $this->nav_xpath->query('x:a', $node);
+ $title = $nodes->item(0)->nodeValue;
+ $src = static::getAttr($nodes, 'href');
+ $src = $this->encodeComponentName($src);
+ $item = ['title' => preg_replace('~[\r\n]+~', '', $title), 'src' => $src];
+ $insidenodes = $this->nav_xpath->query('x:ol/x:li', $node);
+ if (count($insidenodes) < 1) {
+ return $item;
+ }
+ $item['children'] = [];
+ foreach ($insidenodes as $insidenode) {
+ $item['children'][] = $this->getNavTocListItem($insidenode);
+ }
+ return $item;
+ }
+
+ /**
+ * Get the Epub content (TOC) as an array
+ *
+ * For each chapter there is a "title" and a "src", and optional "children"
+ * See https://github.com/joseph/Monocle/wiki/Book-data-object for details
+ * @return mixed
+ */
+ public function contents()
+ {
+ $contents = [];
+ if (!empty($this->nav)) {
+ $toc = $this->nav_xpath->query('//x:nav[@epub:type="toc"]')->item(0);
+ $nodes = $this->nav_xpath->query('x:ol/x:li', $toc);
+ foreach ($nodes as $node) {
+ $contents[] = $this->getNavTocListItem($node);
+ }
+ return $contents;
+ }
+ $nodes = $this->toc_xpath->query('//x:ncx/x:navMap/x:navPoint');
+ foreach ($nodes as $node) {
+ $contents[] = $this->getNavPointDetail($node);
+ }
+ return $contents;
+ }
+
+ /**
+ * Set the book author(s)
+ *
+ * Authors should be given with a "file-as" and a real name. The file as
+ * is used for sorting in e-readers.
+ *
+ * Example:
+ *
+ * array(
+ * 'Pratchett, Terry' => 'Terry Pratchett',
+ * 'Simpson, Jacqueline' => 'Jacqueline Simpson',
+ * )
+ *
+ * @param mixed $authors
+ * @return void
+ */
+ public function setAuthors($authors)
+ {
+ // Author where given as a comma separated list
+ if (is_string($authors)) {
+ if ($authors == '') {
+ $authors = [];
+ } else {
+ $authors = explode(',', $authors);
+ $authors = array_map('trim', $authors);
+ }
+ }
+
+ // delete existing nodes
+ $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]');
+ static::deleteNodes($nodes);
+
+ // add new nodes
+ /** @var EpubDomElement $parent */
+ $parent = $this->xpath->query('//opf:metadata')->item(0);
+ foreach ($authors as $as => $name) {
+ if (is_int($as)) {
+ $as = $name; //numeric array given
+ }
+ $node = $parent->newChild('dc:creator', $name);
+ $node->setAttrib('opf:role', 'aut');
+ $node->setAttrib('opf:file-as', $as);
+ }
+
+ $this->reparse();
+ }
+
+ /**
+ * Get the book author(s)
+ * @return array<string>
+ */
+ public function getAuthors()
+ {
+ $rolefix = false;
+ $authors = [];
+ $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]');
+ if ($nodes->length == 0) {
+ // no nodes where found, let's try again without role
+ $nodes = $this->xpath->query('//opf:metadata/dc:creator');
+ $rolefix = true;
+ }
+ foreach ($nodes as $node) {
+ /** @var EpubDomElement $node */
+ $name = $node->nodeValue;
+ $as = $node->getAttrib('opf:file-as');
+ if (!$as) {
+ $as = $name;
+ $node->setAttrib('opf:file-as', $as);
+ }
+ if ($rolefix) {
+ $node->setAttrib('opf:role', 'aut');
+ }
+ $authors[$as] = $name;
+ }
+ return $authors;
+ }
+
+ /**
+ * Set or get the Google Books ID
+ *
+ * @param string|bool $google
+ * @return mixed
+ */
+ public function Google($google = false)
+ {
+ return $this->getset('dc:identifier', $google, 'opf:scheme', 'GOOGLE');
+ }
+
+ /**
+ * Set or get the Amazon ID of the book
+ *
+ * @param string|bool $amazon
+ * @return mixed
+ */
+ public function Amazon($amazon = false)
+ {
+ return $this->getset('dc:identifier', $amazon, 'opf:scheme', 'AMAZON');
+ }
+
+ /**
+ * Set the Series of the book
+ *
+ * @param string $serie
+ * @return void
+ */
+ public function setSeries($serie)
+ {
+ $this->setMetaDestination('opf:meta', 'name', 'calibre:series', 'content', $serie);
+ }
+
+ /**
+ * Get the Series of the book
+ *
+ * @return mixed
+ */
+ public function getSeries()
+ {
+ return $this->getMetaDestination('opf:meta', 'name', 'calibre:series', 'content');
+ }
+
+ /**
+ * Set the Series Index of the book
+ *
+ * @param string $seriesIndex
+ * @return void
+ */
+ public function setSeriesIndex($seriesIndex)
+ {
+ $this->setMetaDestination('opf:meta', 'name', 'calibre:series_index', 'content', $seriesIndex);
+ }
+
+ /**
+ * Get the Series Index of the book
+ *
+ * @return mixed
+ */
+ public function getSeriesIndex()
+ {
+ return $this->getMetaDestination('opf:meta', 'name', 'calibre:series_index', 'content');
+ }
+
+ /**
+ * Set the book's subjects (aka. tags)
+ *
+ * Subject should be given as array, but a comma separated string will also
+ * be accepted.
+ *
+ * @param array<string>|string $subjects
+ * @return void
+ */
+ public function setSubjects($subjects)
+ {
+ if (is_string($subjects)) {
+ if ($subjects === '') {
+ $subjects = [];
+ } else {
+ $subjects = explode(',', $subjects);
+ $subjects = array_map('trim', $subjects);
+ }
+ }
+
+ // delete previous
+ $nodes = $this->xpath->query('//opf:metadata/dc:subject');
+ static::deleteNodes($nodes);
+ // add new ones
+ $parent = $this->xpath->query('//opf:metadata')->item(0);
+ foreach ($subjects as $subj) {
+ $node = $this->xml->createElement('dc:subject', htmlspecialchars($subj));
+ $node = $parent->appendChild($node);
+ }
+
+ $this->reparse();
+ }
+
+ /**
+ * Get the book's subjects (aka. tags)
+ * @return array<mixed>
+ */
+ public function getSubjects()
+ {
+ $subjects = [];
+ $nodes = $this->xpath->query('//opf:metadata/dc:subject');
+ foreach ($nodes as $node) {
+ $subjects[] = $node->nodeValue;
+ }
+ return $subjects;
+ }
+
+ /**
+ * Update the cover data
+ *
+ * When adding a new image this function return no or old data because the
+ * image contents are not in the epub file, yet. The image will be added when
+ * the save() method is called.
+ *
+ * @param string $path local filesystem path to a new cover image
+ * @param string $mime mime type of the given file
+ * @return void
+ */
+ public function setCoverInfo($path, $mime)
+ {
+ // remove current pointer
+ $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]');
+ static::deleteNodes($nodes);
+ // remove previous manifest entries if they where made by us
+ $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="' . static::COVER_ID . '"]');
+ static::deleteNodes($nodes);
+
+ // add pointer
+ /** @var EpubDomElement $parent */
+ $parent = $this->xpath->query('//opf:metadata')->item(0);
+ $node = $parent->newChild('opf:meta');
+ $node->setAttrib('opf:name', 'cover');
+ $node->setAttrib('opf:content', static::COVER_ID);
+
+ // add manifest
+ /** @var EpubDomElement $parent */
+ $parent = $this->xpath->query('//opf:manifest')->item(0);
+ $node = $parent->newChild('opf:item');
+ $node->setAttrib('id', static::COVER_ID);
+ $node->setAttrib('opf:href', static::COVER_ID . '.img');
+ $node->setAttrib('opf:media-type', $mime);
+
+ // remember path for save action
+ $this->imagetoadd = $path;
+ $this->coverpath = $this->getFullPath(static::COVER_ID . '.img');
+
+ $this->reparse();
+ }
+
+ /**
+ * Read the cover data
+ *
+ * Returns an associative array with the following keys:
+ *
+ * mime - filetype (usually image/jpeg)
+ * data - the binary image data
+ * found - the internal path, or false if no image is set in epub
+ *
+ * When no image is set in the epub file, the binary data for a transparent
+ * GIF pixel is returned.
+ *
+ * @return array<mixed>
+ */
+ public function getCoverInfo()
+ {
+ $item = $this->getCoverItem();
+ if (!$item) {
+ return $this->no_cover();
+ }
+
+ $mime = $item->getAttrib('opf:media-type');
+ $path = $item->getAttrib('opf:href');
+ $path = dirname('/' . $this->meta) . '/' . $path; // image path is relative to meta file
+ $path = ltrim($path, '/');
+
+ $zip = new ZipArchive();
+ if (!@$zip->open($this->file)) {
+ throw new Exception('Failed to read epub file');
+ }
+ $data = $zip->getFromName($path);
+
+ return [
+ 'mime' => $mime,
+ 'data' => $data,
+ 'found' => $path,
+ ];
+ }
+
+ /**
+ * Summary of getCoverId
+ * @return string|null
+ */
+ public function getCoverId()
+ {
+ $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]');
+ if (!$nodes->length) {
+ return null;
+ }
+
+ $coverid = (string) static::getAttr($nodes, 'opf:content');
+ if (!$coverid) {
+ return null;
+ }
+
+ return $coverid;
+ }
+
+ /**
+ * Summary of getCoverItem
+ * @return EpubDomElement|null
+ */
+ public function getCoverItem()
+ {
+ $coverid = $this->getCoverId();
+ if (!$coverid) {
+ return null;
+ }
+
+ $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="' . $coverid . '"]');
+ if (!$nodes->length) {
+ return null;
+ }
+
+ /** @var EpubDomElement $node */
+ $node = $nodes->item(0);
+ return $node;
+ }
+
+ /**
+ * Get the internal path of the cover image file.
+ *
+ * @return string|null
+ */
+ public function getCoverPath()
+ {
+ $item = $this->getCoverItem();
+ if (!$item) {
+ return null;
+ }
+
+ return $item->getAttrib('opf:href');
+ }
+
+ /**
+ * Summary of Combine
+ * @param mixed $a
+ * @param mixed $b
+ * @throws \InvalidArgumentException
+ * @return string
+ */
+ public static function Combine($a, $b)
+ {
+ $isAbsolute = false;
+ if ($a[0] == '/') {
+ $isAbsolute = true;
+ }
+
+ if ($b[0] == '/') {
+ throw new InvalidArgumentException('Second path part must not start with /');
+ }
+
+ $splittedA = preg_split('#/#', $a);
+ $splittedB = preg_split('#/#', $b);
+
+ $pathParts = [];
+ $mergedPath = array_merge($splittedA, $splittedB);
+
+ foreach ($mergedPath as $item) {
+ if ($item == null || $item == '' || $item == '.') {
+ continue;
+ }
+
+ if ($item == '..') {
+ array_pop($pathParts);
+ continue;
+ }
+
+ array_push($pathParts, $item);
+ }
+
+ $path = implode('/', $pathParts);
+ if ($isAbsolute) {
+ return('/' . $path);
+ } else {
+ return($path);
+ }
+ }
+
+ /**
+ * Summary of getFullPath
+ * @param mixed $file
+ * @param mixed $context
+ * @return string
+ */
+ protected function getFullPath($file, $context = null)
+ {
+ $path = dirname('/' . $this->meta) . '/' . $file;
+ $path = ltrim($path, '\\');
+ $path = ltrim($path, '/');
+ if (!empty($context)) {
+ $path = $this->combine(dirname($path), $context);
+ }
+ //error_log ("FullPath : $path ($file / $context)");
+ return $path;
+ }
+
+ /**
+ * Summary of updateForKepub
+ * @return void
+ */
+ public function updateForKepub()
+ {
+ $item = $this->getCoverItem();
+ if (!is_null($item)) {
+ $item->setAttrib('opf:properties', 'cover-image');
+ }
+ }
+
+ /**
+ * Summary of setCoverFile
+ * @param string $path
+ * @param string $mime
+ * @return array<mixed>|void
+ */
+ public function setCoverFile($path, $mime)
+ {
+ $hascover = true;
+ $item = $this->getCoverItem();
+ if (is_null($item)) {
+ $hascover = false;
+ return; // TODO For now only update
+ } else {
+ $mime = $item->getAttrib('opf:media-type');
+ $this->coverpath = $item->getAttrib('opf:href');
+ $this->coverpath = dirname('/' . $this->meta) . '/' . $this->coverpath; // image path is relative to meta file
+ $this->coverpath = ltrim($this->coverpath, '\\');
+ $this->coverpath = ltrim($this->coverpath, '/');
+ }
+
+ // set cover
+
+ $item->setAttrib('opf:media-type', $mime);
+
+ // remember path for save action
+ $this->imagetoadd = $path;
+
+ $this->reparse();
+
+ // not very useful here, but data gets added in download() if needed
+ return [
+ 'data' => null,
+ 'mime' => $mime,
+ 'found' => $this->coverpath,
+ ];
+ }
+
+ /**
+ * Summary of getAttr
+ * @param DOMNodeList<EpubDomElement> $nodes list of Element items
+ * @param string $att Attribute name
+ * @return string
+ */
+ protected static function getAttr($nodes, $att)
+ {
+ $node = $nodes->item(0);
+ return $node->getAttrib($att);
+ }
+
+ /**
+ * Summary of deleteNodes
+ * @param DOMNodeList<EpubDomElement> $nodes list of Element items
+ * @return void
+ */
+ protected static function deleteNodes($nodes)
+ {
+ foreach ($nodes as $node) {
+ $node->delete();
+ }
+ }
+
+ /**
+ * A simple getter/setter for simple meta attributes
+ *
+ * It should only be used for attributes that are expected to be unique
+ *
+ * @param string $item XML node to set/get
+ * @param string|bool $value New node value
+ * @param string|bool $att Attribute name
+ * @param string|bool|array<mixed> $aval Attribute value
+ * @param string|bool $datt Destination attribute
+ * @return string|void
+ */
+ protected function getset($item, $value = false, $att = false, $aval = false, $datt = false)
+ {
+ // construct xpath
+ $xpath = '//opf:metadata/' . $item;
+ if ($att) {
+ if (is_array($aval)) {
+ $xpath .= '[@' . $att . '="';
+ $xpath .= implode("\" or @$att=\"", $aval);
+ $xpath .= '"]';
+ } else {
+ $xpath .= '[@' . $att . '="' . $aval . '"]';
+ }
+ }
+
+ // set value
+ if ($value !== false) {
+ $value = htmlspecialchars($value);
+ $nodes = $this->xpath->query($xpath);
+ if ($nodes->length == 1) {
+ /** @var EpubDomElement $node */
+ $node = $nodes->item(0);
+ if ($value === '') {
+ // the user want's to empty this value -> delete the node
+ $node->delete();
+ } else {
+ // replace value
+ if ($datt) {
+ $node->setAttrib($datt, $value);
+ } else {
+ $node->nodeValue = $value;
+ }
+ }
+ } else {
+ // if there are multiple matching nodes for some reason delete
+ // them. we'll replace them all with our own single one
+ static::deleteNodes($nodes);
+ // readd them
+ if ($value) {
+ /** @var EpubDomElement $parent */
+ $parent = $this->xpath->query('//opf:metadata')->item(0);
+
+ $node = $parent->newChild($item);
+ if ($att) {
+ $node->setAttrib($att, $aval);
+ }
+ if ($datt) {
+ $node->setAttrib($datt, $value);
+ } else {
+ $node->nodeValue = $value;
+ }
+ }
+ }
+
+ $this->reparse();
+ }
+
+ // get value
+ $nodes = $this->xpath->query($xpath);
+ if ($nodes->length) {
+ /** @var EpubDomElement $node */
+ $node = $nodes->item(0);
+ if ($datt) {
+ return $node->getAttrib($datt);
+ } else {
+ return $node->nodeValue;
+ }
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Return a not found response for Cover()
+ * @return array<string, mixed>
+ */
+ protected function no_cover()
+ {
+ return [
+ 'data' => base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7'),
+ 'mime' => 'image/gif',
+ 'found' => false,
+ ];
+ }
+
+ /**
+ * Reparse the DOM tree
+ *
+ * I had to rely on this because otherwise xpath failed to find the newly
+ * added nodes
+ * @return void
+ */
+ protected function reparse()
+ {
+ $this->xml->loadXML($this->xml->saveXML());
+ $this->xpath = new EpubDomXPath($this->xml);
+ // reset structural members
+ $this->manifest = null;
+ $this->spine = null;
+ $this->tocnav = null;
+ }
+
+ /** based on slightly more updated version at https://github.com/epubli/epub */
+
+ /**
+ * A simple setter for simple meta attributes
+ *
+ * It should only be used for attributes that are expected to be unique
+ *
+ * @param string $item XML node to set
+ * @param string $value New node value
+ * @param bool|string $attribute Attribute name
+ * @param bool|string $attributeValue Attribute value
+ * @param bool $caseSensitive
+ * @return mixed
+ */
+ protected function setMeta($item, $value, $attribute = false, $attributeValue = false, $caseSensitive = true)
+ {
+ /**
+ if ($attributeValue !== false && !$caseSensitive) {
+ $attval = is_array($attributeValue) ? $attributeValue : [ $attributeValue ];
+ $vallist = [];
+ foreach ($attval as $val) {
+ $vallist[] = strtoupper($val);
+ $vallist[] = strtolower($val);
+ }
+ $attributeValue = $vallist;
+ }
+ */
+ return $this->getset($item, $value, $attribute, $attributeValue);
+ }
+
+ /**
+ * A simple getter for simple meta attributes
+ *
+ * It should only be used for attributes that are expected to be unique
+ *
+ * @param string $item XML node to get
+ * @param bool|string $att Attribute name
+ * @param bool|string $aval Attribute value
+ * @param bool $caseSensitive
+ * @return string
+ */
+ protected function getMeta($item, $att = false, $aval = false, $caseSensitive = true)
+ {
+ /**
+ if ($aval !== false && !$caseSensitive) {
+ $attval = is_array($aval) ? $aval : [ $aval ];
+ $vallist = [];
+ foreach ($attval as $val) {
+ $vallist[] = strtoupper($val);
+ $vallist[] = strtolower($val);
+ }
+ $aval = $vallist;
+ }
+ */
+ return $this->getset($item, false, $att, $aval);
+ }
+
+ /**
+ * A simple setter for simple meta attributes - with destination attribute (for Serie)
+ *
+ * It should only be used for attributes that are expected to be unique
+ *
+ * @param string $item XML node to set
+ * @param string $attribute Attribute name
+ * @param string $attributeValue Attribute value
+ * @param string $datt Destination attribute
+ * @param string $value New node value
+ * @return mixed
+ */
+ protected function setMetaDestination($item, $attribute, $attributeValue, $datt, $value)
+ {
+ return $this->getset($item, $value, $attribute, $attributeValue, $datt);
+ }
+
+ /**
+ * A simple getter for simple meta attributes - with destination attribute (for Serie)
+ *
+ * It should only be used for attributes that are expected to be unique
+ *
+ * @param string $item XML node to get
+ * @param string $att Attribute name
+ * @param string $aval Attribute value
+ * @param string $datt Destination attribute
+ * @return string
+ */
+ protected function getMetaDestination($item, $att, $aval, $datt)
+ {
+ return $this->getset($item, false, $att, $aval, $datt);
+ }
+
+ /**
+ * Set the book title
+ *
+ * @param string $title
+ * @return mixed
+ */
+ public function setTitle($title)
+ {
+ return $this->getset('dc:title', $title);
+ }
+
+ /**
+ * Get the book title
+ *
+ * @return mixed
+ */
+ public function getTitle()
+ {
+ return $this->getset('dc:title');
+ }
+
+ /**
+ * Set the book's language
+ *
+ * @param string $lang
+ * @return mixed
+ */
+ public function setLanguage($lang)
+ {
+ return $this->getset('dc:language', $lang);
+ }
+
+ /**
+ * Get the book's language
+ *
+ * @return mixed
+ */
+ public function getLanguage()
+ {
+ return $this->getset('dc:language');
+ }
+
+ /**
+ * Set the book's publisher info
+ *
+ * @param string $publisher
+ * @return void
+ */
+ public function setPublisher($publisher)
+ {
+ $this->setMeta('dc:publisher', $publisher);
+ }
+
+ /**
+ * Get the book's publisher info
+ *
+ * @return string
+ */
+ public function getPublisher()
+ {
+ return $this->getMeta('dc:publisher');
+ }
+
+ /**
+ * Set the book's copyright info
+ *
+ * @param string $rights
+ * @return void
+ */
+ public function setCopyright($rights)
+ {
+ $this->setMeta('dc:rights', $rights);
+ }
+
+ /**
+ * Get the book's copyright info
+ *
+ * @return string
+ */
+ public function getCopyright()
+ {
+ return $this->getMeta('dc:rights');
+ }
+
+ /**
+ * Set the book's description
+ *
+ * @param string $description
+ * @return void
+ */
+ public function setDescription($description)
+ {
+ $this->setMeta('dc:description', $description);
+ }
+
+ /**
+ * Get the book's description
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->getMeta('dc:description');
+ }
+
+ /**
+ * Set a date for an event in the package file’s meta section.
+ *
+ * @param string $event
+ * @param string $date Date eg: 2012-05-19T12:54:25Z
+ * @return void
+ */
+ public function setEventDate($event, $date)
+ {
+ $this->getset('dc:date', $date, 'opf:event', $event);
+ }
+
+ /**
+ * Get a date for an event in the package file’s meta section.
+ *
+ * @param string $event
+ * @return mixed
+ */
+ public function getEventDate($event)
+ {
+ $res = $this->getset('dc:date', false, 'opf:event', $event);
+
+ return $res;
+ }
+
+ /**
+ * Set the book's creation date
+ *
+ * @param string $date Date eg: 2012-05-19T12:54:25Z
+ * @return void
+ */
+ public function setCreationDate($date)
+ {
+ $this->setEventDate('creation', $date);
+ }
+
+ /**
+ * Get the book's creation date
+ *
+ * @return mixed
+ */
+ public function getCreationDate()
+ {
+ $res = $this->getEventDate('creation');
+
+ return $res;
+ }
+
+ /**
+ * Set the book's modification date
+ *
+ * @param string $date Date eg: 2012-05-19T12:54:25Z
+ * @return void
+ */
+ public function setModificationDate($date)
+ {
+ $this->setEventDate('modification', $date);
+ }
+
+ /**
+ * Get the book's modification date
+ *
+ * @return mixed
+ */
+ public function getModificationDate()
+ {
+ $res = $this->getEventDate('modification');
+
+ return $res;
+ }
+
+ /**
+ * Set an identifier in the package file’s meta section.
+ *
+ * @param string|array<string> $idScheme The identifier’s scheme. If an array is given
+ * all matching identifiers are replaced by one with the first value as scheme.
+ * @param string $value
+ * @param bool $caseSensitive
+ * @return void
+ */
+ public function setIdentifier($idScheme, $value, $caseSensitive = false)
+ {
+ $this->setMeta('dc:identifier', $value, 'opf:scheme', $idScheme, $caseSensitive);
+ }
+
+ /**
+ * Set an identifier from the package file’s meta section.
+ *
+ * @param string|array<string> $idScheme The identifier’s scheme. If an array is given
+ * the scheme can be any of its values.
+ * @param bool $caseSensitive - @todo changed to true here
+ * @return string The value of the first matching element.
+ */
+ public function getIdentifier($idScheme, $caseSensitive = true)
+ {
+ return $this->getMeta('dc:identifier', 'opf:scheme', $idScheme, $caseSensitive);
+ }
+
+ /**
+ * Set the book's unique identifier
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setUniqueIdentifier($value)
+ {
+ //$nodes = $this->xpath->query('/opf:package');
+ $idRef = $this->xpath->document->documentElement->getAttribute('unique-identifier');
+ $this->setMeta('dc:identifier', $value, 'id', $idRef);
+ }
+
+ /**
+ * Get the book's unique identifier
+ *
+ * @param bool $normalize
+ * @return string
+ */
+ public function getUniqueIdentifier($normalize = false)
+ {
+ //$nodes = $this->xpath->query('/opf:package');
+ $idRef = $this->xpath->document->documentElement->getAttribute('unique-identifier');
+ $idVal = $this->getMeta('dc:identifier', 'id', $idRef);
+ if ($normalize) {
+ $idVal = strtolower($idVal);
+ $idVal = str_replace('urn:uuid:', '', $idVal);
+ }
+
+ return $idVal;
+ }
+
+ /**
+ * Set the book's UUID - @todo pick one + case sensitive
+ *
+ * @param string $uuid
+ * @return void
+ */
+ public function setUuid($uuid)
+ {
+ //$this->setIdentifier(['UUID', 'uuid', 'URN', 'urn'], $uuid);
+ $this->setIdentifier('URN', $uuid);
+ }
+
+ /**
+ * Get the book's UUID - @todo pick one + case sensitive
+ *
+ * @return string
+ */
+ public function getUuid()
+ {
+ //return $this->getIdentifier(['uuid', 'urn']);
+ return $this->getIdentifier(['UUID', 'URN']);
+ }
+
+ /**
+ * Set the book's URI
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function setUri($uri)
+ {
+ $this->setIdentifier('URI', $uri);
+ }
+
+ /**
+ * Get the book's URI
+ *
+ * @return string
+ */
+ public function getUri()
+ {
+ return $this->getIdentifier('URI');
+ }
+
+ /**
+ * Set the book's ISBN
+ *
+ * @param string $isbn
+ * @return void
+ */
+ public function setIsbn($isbn)
+ {
+ $this->setIdentifier('ISBN', $isbn);
+ }
+
+ /**
+ * Get the book's ISBN
+ *
+ * @return string
+ */
+ public function getIsbn()
+ {
+ return $this->getIdentifier('ISBN');
+ }
+
+ /**
+ * Set the Calibre UUID of the book
+ *
+ * @param string $uuid
+ * @return void
+ */
+ public function setCalibre($uuid)
+ {
+ $this->setIdentifier('calibre', $uuid);
+ }
+
+ /**
+ * Get the Calibre UUID of the book
+ *
+ * @return string
+ */
+ public function getCalibre()
+ {
+ return $this->getIdentifier('calibre');
+ }
+
+ /**
+ * Remove the cover image
+ *
+ * If the actual image file was added by this library it will be removed. Otherwise only the
+ * reference to it is removed from the metadata, since the same image might be referenced
+ * by other parts of the EPUB file.
+ * @return void
+ */
+ public function clearCover()
+ {
+ if (!$this->hasCover()) {
+ return;
+ }
+
+ $manifest = $this->getManifest();
+
+ // remove any cover image file added by us
+ if (isset($manifest[static::COVER_ID])) {
+ $name = $this->getFullPath(static::COVER_ID . '.img');
+ if (!$this->zip->FileDelete($name)) {
+ throw new Exception('Unable to remove ' . $name);
+ }
+ }
+
+ // remove metadata cover pointer
+ $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]');
+ static::deleteNodes($nodes);
+
+ // remove previous manifest entries if they where made by us
+ $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="' . static::COVER_ID . '"]');
+ static::deleteNodes($nodes);
+
+ $this->reparse();
+ }
+
+ /**
+ * Set the cover image
+ *
+ * @param string $path local filesystem path to a new cover image
+ * @param string $mime mime type of the given file
+ * @return void
+ */
+ public function setCover($path, $mime)
+ {
+ if (!$path) {
+ throw new InvalidArgumentException('Parameter $path must not be empty!');
+ }
+
+ if (!is_readable($path)) {
+ throw new InvalidArgumentException("Cannot add $path as new cover image since that file is not readable!");
+ }
+
+ $this->clearCover();
+
+ // add metadata cover pointer
+ /** @var EpubDomElement $parent */
+ $parent = $this->xpath->query('//opf:metadata')->item(0);
+ $node = $parent->newChild('opf:meta');
+ $node->setAttrib('opf:name', 'cover');
+ $node->setAttrib('opf:content', static::COVER_ID);
+
+ // add manifest item
+ /** @var EpubDomElement $parent */
+ $parent = $this->xpath->query('//opf:manifest')->item(0);
+ $node = $parent->newChild('opf:item');
+ $node->setAttrib('id', static::COVER_ID);
+ $node->setAttrib('opf:href', static::COVER_ID . '.img');
+ $node->setAttrib('opf:media-type', $mime);
+
+ // add the cover image
+ $name = $this->getFullPath(static::COVER_ID . '.img');
+ if (!$this->zip->FileAddPath($name, $path)) {
+ throw new Exception('Unable to add ' . $name);
+ }
+
+ $this->reparse();
+ }
+
+ /**
+ * Get the cover image
+ *
+ * @return string|null The binary image data or null if no image exists.
+ */
+ public function getCover()
+ {
+ $comp = $this->getCoverPath();
+
+ return $comp ? $this->component($comp) : null;
+ }
+
+ /**
+ * Whether a cover image meta entry does exist.
+ *
+ * @return bool
+ */
+ public function hasCover()
+ {
+ return !empty($this->getCoverId());
+ }
+
+ /**
+ * Add a title page with the cover image to the EPUB.
+ *
+ * @param string $templatePath The path to the template file. Defaults to an XHTML file contained in this library.
+ * @return void
+ */
+ public function addCoverImageTitlePage($templatePath = __DIR__ . '/../templates/titlepage.xhtml')
+ {
+ $xhtmlFilename = static::TITLE_PAGE_ID . '.xhtml';
+
+ // add title page file to zip
+ $template = file_get_contents($templatePath);
+ $xhtml = strtr($template, ['{{ title }}' => $this->getTitle(), '{{ coverPath }}' => $this->getCoverPath()]);
+ $name = $this->getFullPath($xhtmlFilename);
+ if (!$this->zip->FileReplace($name, $xhtml)) {
+ throw new Exception('Unable to replace ' . $name);
+ }
+
+ // prepend title page file to manifest
+ $parent = $this->xpath->query('//opf:manifest')->item(0);
+ $node = new EpubDomElement('opf:item');
+ $parent->insertBefore($node, $parent->firstChild);
+ $node->setAttrib('id', static::TITLE_PAGE_ID);
+ $node->setAttrib('opf:href', $xhtmlFilename);
+ $node->setAttrib('opf:media-type', 'application/xhtml+xml');
+
+ // prepend title page spine item
+ $parent = $this->xpath->query('//opf:spine')->item(0);
+ $node = new EpubDomElement('opf:itemref');
+ $parent->insertBefore($node, $parent->firstChild);
+ $node->setAttrib('idref', static::TITLE_PAGE_ID);
+
+ // prepend title page guide reference
+ $parent = $this->xpath->query('//opf:guide')->item(0);
+ $node = new EpubDomElement('opf:reference');
+ $parent->insertBefore($node, $parent->firstChild);
+ $node->setAttrib('opf:href', $xhtmlFilename);
+ $node->setAttrib('opf:type', 'cover');
+ $node->setAttrib('opf:title', 'Title Page');
+ }
+
+ /**
+ * Remove the title page added by this library (determined by a certain manifest item ID).
+ * @return void
+ */
+ public function removeTitlePage()
+ {
+ $xhtmlFilename = static::TITLE_PAGE_ID . '.xhtml';
+
+ // remove title page file from zip
+ $name = $this->getFullPath($xhtmlFilename);
+ if (!$this->zip->FileDelete($name)) {
+ throw new Exception('Unable to remove ' . $name);
+ }
+
+ // remove title page file from manifest
+ $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="' . static::TITLE_PAGE_ID . '"]');
+ static::deleteNodes($nodes);
+
+ // remove title page spine item
+ $nodes = $this->xpath->query('//opf:spine/opf:itemref[@idref="' . static::TITLE_PAGE_ID . '"]');
+ static::deleteNodes($nodes);
+
+ // remove title page guide reference
+ $nodes = $this->xpath->query('//opf:guide/opf:reference[@href="' . $xhtmlFilename . '"]');
+ static::deleteNodes($nodes);
+ }
+
+ /**
+ * Get the Calibre book annotations from opf:metadata (if saved)
+ * @param ?string $data
+ * @return array<mixed>
+ */
+ public function getCalibreAnnotations($data = null)
+ {
+ if (!empty($data)) {
+ $this->loadXmlData($data);
+ }
+ $annotations = [];
+ $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="calibre:annotation"]');
+ if ($nodes->length == 0) {
+ return $annotations;
+ }
+ foreach ($nodes as $node) {
+ /** @var EpubDomElement $node */
+ $content = $node->getAttribute('content');
+ try {
+ $annotations[] = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
+ } catch (JsonException $e) {
+ $annotations[] = $content;
+ }
+ }
+ return $annotations;
+ }
+
+ /**
+ * Get the Calibre bookmarks from META-INF/calibre_bookmarks.txt (if saved)
+ * @param ?string $data
+ * @return array<mixed>
+ */
+ public function getCalibreBookmarks($data = null)
+ {
+ if (empty($data)) {
+ if (!$this->zip->FileExists(static::BOOKMARK_FILE)) {
+ throw new Exception('Unable to find ' . static::BOOKMARK_FILE);
+ }
+ $data = $this->zip->FileRead(static::BOOKMARK_FILE);
+ if ($data == false) {
+ throw new Exception('Failed to access epub bookmark file');
+ }
+ }
+ if (!str_starts_with($data, static::EPUB_FILE_TYPE_MAGIC)) {
+ throw new Exception('Invalid format for epub bookmark file');
+ }
+ $content = substr($data, strlen(static::EPUB_FILE_TYPE_MAGIC));
+ $content = base64_decode($content);
+ try {
+ $bookmarks = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
+ } catch (JsonException $e) {
+ $bookmarks = $content;
+ }
+ return $bookmarks;
+ }
+
+ /**
+ * Get the manifest of this EPUB.
+ *
+ * @return Manifest
+ * @throws Exception
+ */
+ public function getManifest()
+ {
+ if ($this->manifest) {
+ return $this->manifest;
+ }
+
+ /** @var EpubDomElement|null $manifestNode */
+ $manifestNode = $this->xpath->query('//opf:manifest')->item(0);
+ if (is_null($manifestNode)) {
+ throw new Exception('No manifest element found in EPUB!');
+ }
+
+ $this->manifest = new Manifest();
+ /** @var EpubDomElement $item */
+ foreach ($manifestNode->getElementsByTagName('item') as $item) {
+ $id = $item->getAttribute('id');
+ $href = urldecode($item->getAttribute('href'));
+ $fullPath = $this->getFullPath($href);
+ // this won't work with clsTbsZip - $this->zip->getStream($fullPath);
+ //$handle = $this->zip->FileStream($fullPath);
+ $callable = function () use ($fullPath) {
+ // Automatic binding of $this
+ return $this->zip->FileRead($fullPath);
+ };
+ $size = $this->zipSizeMap[$fullPath] ?? 0;
+ $mediaType = $item->getAttribute('media-type');
+ $this->manifest->createItem($id, $href, $callable, $size, $mediaType);
+ }
+
+ return $this->manifest;
+ }
+
+ /**
+ * Get the spine structure of this EPUB.
+ *
+ * @return Spine
+ * @throws Exception
+ */
+ public function getSpine()
+ {
+ if ($this->spine) {
+ return $this->spine;
+ }
+
+ $nodes = $this->xpath->query('//opf:spine');
+ if (!$nodes->length) {
+ throw new Exception('No spine element found in EPUB!');
+ }
+ $tocId = static::getAttr($nodes, 'toc');
+ $tocFormat = Toc::class;
+ if (empty($tocId)) {
+ $nodes = $this->xpath->query('//opf:manifest/opf:item[@properties="nav"]');
+ $tocId = static::getAttr($nodes, 'id');
+ $tocFormat = Nav::class;
+ }
+
+ $manifest = $this->getManifest();
+
+ if (!isset($manifest[$tocId])) {
+ throw new Exception('TOC or NAV item referenced in spine missing in manifest!');
+ }
+
+ $this->spine = new Spine($manifest[$tocId], $tocFormat);
+
+ /** @var EpubDomElement $spineNode */
+ $spineNode = $this->xpath->query('//opf:spine')->item(0);
+
+ $itemRefNodes = $spineNode->getElementsByTagName('itemref');
+ foreach ($itemRefNodes as $itemRef) {
+ /** @var EpubDomElement $itemRef */
+ $id = $itemRef->getAttribute('idref');
+ if (!isset($manifest[$id])) {
+ throw new Exception("Item $id referenced in spine missing in manifest!");
+ }
+ // Link the item from the manifest to the spine.
+ $this->spine->appendItem($manifest[$id]);
+ }
+
+ return $this->spine;
+ }
+
+ /**
+ * Get the table of contents structure of this EPUB.
+ *
+ * @return Toc|Nav
+ * @throws Exception
+ */
+ public function getToc()
+ {
+ if ($this->tocnav) {
+ return $this->tocnav;
+ }
+
+ // @todo support Nav structure as well, see initSpineComponent
+ if ($this->getSpine()->getTocFormat() === Nav::class) {
+ throw new Exception('TODO: support NAV structure as well');
+ }
+
+ $tocpath = $this->getFullPath($this->getSpine()->getTocItem()->getHref());
+ $data = $this->zip->FileRead($tocpath);
+ $toc = new DOMDocument();
+ $toc->registerNodeClass(DOMElement::class, EpubDomElement::class);
+ $toc->loadXML($data);
+ $xpath = new EpubDomXPath($toc);
+ //$rootNamespace = $toc->lookupNamespaceUri($toc->namespaceURI);
+ //$xpath->registerNamespace('x', $rootNamespace);
+
+ $titleNode = $xpath->query('//ncx:docTitle/ncx:text')->item(0);
+ $title = $titleNode ? $titleNode->nodeValue : '';
+ $authorNode = $xpath->query('//ncx:docAuthor/ncx:text')->item(0);
+ $author = $authorNode ? $authorNode->nodeValue : '';
+ $this->tocnav = new Toc($title, $author);
+
+ $navPointNodes = $xpath->query('//ncx:navMap/ncx:navPoint');
+
+ $this->loadNavPoints($navPointNodes, $this->tocnav->getNavMap(), $xpath);
+
+ return $this->tocnav;
+ }
+
+ /**
+ * Load navigation points from TOC XML DOM into TOC object structure.
+ *
+ * @param DOMNodeList<EPubDomElement> $navPointNodes List of nodes to load from.
+ * @param TocNavPointList $navPointList List structure to load into.
+ * @param EpubDomXPath $xp The XPath of the TOC document.
+ * @return void
+ */
+ protected static function loadNavPoints(DOMNodeList $navPointNodes, TocNavPointList $navPointList, EpubDomXPath $xp)
+ {
+ foreach ($navPointNodes as $navPointNode) {
+ /** @var EpubDomElement $navPointNode */
+ $id = $navPointNode->getAttribute('id');
+ $class = $navPointNode->getAttribute('class');
+ $playOrder = (int) $navPointNode->getAttribute('playOrder');
+ $labelTextNode = $xp->query('ncx:navLabel/ncx:text', $navPointNode)->item(0);
+ $label = $labelTextNode ? $labelTextNode->nodeValue : '';
+ /** @var EpubDomElement|null $contentNode */
+ $contentNode = $xp->query('ncx:content', $navPointNode)->item(0);
+ $contentSource = $contentNode ? $contentNode->getAttribute('src') : '';
+ $navPoint = new TocNavPoint($id, $class, $playOrder, $label, $contentSource);
+ $navPointList->append($navPoint);
+ $childNavPointNodes = $xp->query('ncx:navPoint', $navPointNode);
+ $childNavPoints = $navPoint->getChildren();
+
+ static::loadNavPoints($childNavPointNodes, $childNavPoints, $xp);
+ }
+ }
+
+ /**
+ * Summary of getNav
+ * @return Toc|Nav
+ */
+ public function getNav()
+ {
+ if ($this->tocnav) {
+ return $this->tocnav;
+ }
+
+ $navpath = $this->getFullPath($this->getSpine()->getTocItem()->getHref());
+ $data = $this->zip->FileRead($navpath);
+ $nav = new DOMDocument();
+ $nav->registerNodeClass(DOMElement::class, EpubDomElement::class);
+ $nav->loadXML($data);
+ $xpath = new EpubDomXPath($nav);
+ $rootNamespace = $nav->lookupNamespaceUri($nav->namespaceURI);
+ $xpath->registerNamespace('x', $rootNamespace);
+
+ // nav documents don't contain mandatory title or author - look in main doc
+ $title = $this->getTitle();
+ $author = implode(', ', $this->getAuthors());
+ $this->tocnav = new Nav($title, $author);
+
+ $toc = $xpath->query('//x:nav[@epub:type="toc"]')->item(0);
+ $navListNodes = $xpath->query('x:ol/x:li', $toc);
+ if ($navListNodes->length > 0) {
+ $this->loadNavList($navListNodes, $this->tocnav->getNavMap(), $xpath);
+ }
+
+ return $this->tocnav;
+ }
+
+ /**
+ * Load navigation points from NAV XML DOM into NAV object structure.
+ *
+ * @param DOMNodeList<EPubDomElement> $navListNodes List of nodes to load from.
+ * @param TocNavPointList $navPointList List structure to load into.
+ * @param EpubDomXPath $xp The XPath of the NAV document.
+ * @param int $depth Current depth of this list (recursive)
+ * @param int $order Current start order for this list
+ * @return void
+ */
+ protected static function loadNavList(DOMNodeList $navListNodes, TocNavPointList $navPointList, EpubDomXPath $xp, int $depth = 0, int $order = 0)
+ {
+ // h1 - h6 are supported as title for the list
+ $className = 'h' . strval($depth + 1);
+ if ($depth > 5) {
+ throw new Exception("We're at maximum depth for NAV DOC here!?");
+ }
+ foreach ($navListNodes as $navPointNode) {
+ $order += 1;
+ $nodes = $xp->query('x:a', $navPointNode);
+ $label = trim($nodes->item(0)->nodeValue);
+ if (empty($label)) {
+ // do we have an image with title or alt available?
+ $images = $xp->query('x:a/x:img', $navPointNode);
+ if ($images->length) {
+ $label = static::getAttr($images, 'alt');
+ if (empty($label)) {
+ $label = static::getAttr($images, 'title');
+ }
+ }
+ }
+ $contentSource = static::getAttr($nodes, 'href');
+ /** @var EpubDomElement $navPointNode */
+ $id = $navPointNode->getAttribute('id');
+ $class = $className;
+ $playOrder = $order;
+ $navPoint = new TocNavPoint($id, $class, $playOrder, $label, $contentSource);
+ $navPointList->append($navPoint);
+ $childNavPointNodes = $xp->query('x:ol/x:li', $navPointNode);
+ $childNavPoints = $navPoint->getChildren();
+
+ static::loadNavList($childNavPointNodes, $childNavPoints, $xp, $depth + 1, $order);
+ }
+ }
+
+ /**
+ * Extract the contents of this EPUB.
+ *
+ * This concatenates contents of items according to their order in the spine.
+ *
+ * @param bool $keepMarkup Whether to keep the XHTML markup rather than extracted plain text.
+ * @param float $fraction If less than 1, only the respective part from the beginning of the book is extracted.
+ * @return string The contents of this EPUB.
+ * @throws Exception
+ */
+ public function getContents($keepMarkup = false, $fraction = 1.0)
+ {
+ $contents = '';
+ if ($fraction < 1) {
+ $totalSize = 0;
+ foreach ($this->getSpine() as $item) {
+ $totalSize += $item->getSize();
+ }
+ $fractionSize = $totalSize * $fraction;
+ $contentsSize = 0;
+ foreach ($this->spine as $item) {
+ $itemSize = $item->getSize();
+ if ($contentsSize + $itemSize > $fractionSize) {
+ break;
+ }
+ $contentsSize += $itemSize;
+ $contents .= $item->getContents(null, null, $keepMarkup);
+ }
+ } else {
+ foreach ($this->getSpine() as $item) {
+ $contents .= $item->getContents(null, null, $keepMarkup);
+ }
+ }
+
+ return $contents;
+ }
+
+ /**
+ * Build an XPath expression to select certain nodes in the metadata section.
+ *
+ * @param string $element The node name of the elements to select.
+ * @param string $attribute If set, the attribute required in the element.
+ * @param string|array<string> $value If set, the value of the above named attribute. If an array is given
+ * all of its values will be allowed in the selector.
+ * @param bool $caseSensitive If false, attribute values are matched case insensitively.
+ * (This is not completely true, as only full upper or lower case strings are matched, not mixed case.
+ * A lower-case function is missing in XPath 1.0.)
+ * @return string
+ */
+ protected static function buildMetaXPath($element, $attribute, $value, $caseSensitive = true)
+ {
+ $xpath = '//opf:metadata/' . $element;
+ if ($attribute) {
+ $xpath .= "[@$attribute";
+ if ($value) {
+ $values = is_array($value) ? $value : [$value];
+ if (!$caseSensitive) {
+ $temp = [];
+ foreach ($values as $item) {
+ $temp[] = strtolower($item);
+ $temp[] = strtoupper($item);
+ }
+ $values = $temp;
+ }
+
+ $xpath .= '="';
+ $xpath .= implode("\" or @$attribute=\"", $values);
+ $xpath .= '"';
+ }
+ $xpath .= ']';
+ }
+
+ return $xpath;
+ }
+
+ /**
+ * Load an XML file from the EPUB/ZIP archive into a new XPath object.
+ *
+ * @param string $path The XML file to load from the ZIP archive.
+ * @return EpubDomXPath The XPath representation of the XML file.
+ * @throws Exception If the given path could not be read.
+ */
+ protected function loadXPathFromItem($path)
+ {
+ $data = $this->zip->FileRead($path);
+ if (!$data) {
+ throw new Exception("Failed to read from EPUB container: $path.");
+ }
+ $xml = new DOMDocument();
+ $xml->registerNodeClass(DOMElement::class, EpubDomElement::class);
+ $xml->loadXML($data);
+
+ return new EpubDomXPath($xml);
+ }
+
+ /**
+ * Get the stat entries for all files in a ZIP file
+ *
+ * @param string $file|null Path to a ZIP file or null for current file
+ * @return array<mixed> (filename => details of the entry)
+ */
+ public function getZipEntries($file = null)
+ {
+ $file ??= $this->file;
+ $entries = [];
+
+ $zip = new ZipArchive();
+ $result = $zip->open($file, ZipArchive::RDONLY);
+ if ($result !== true) {
+ throw new Exception("Unable to open file", $result);
+ }
+ for ($i = 0; $i < $zip->numFiles; $i++) {
+ $stat = $zip->statIndex($i);
+ $entries[$stat['name']] = $stat;
+ }
+ $zip->close();
+
+ return $entries;
+ }
+
+ /**
+ * Map the items of a ZIP file to their respective file sizes.
+ *
+ * @param string $file|null Path to a ZIP file or null for current ZIP file
+ * @return array<mixed> (filename => file size)
+ */
+ protected function loadSizeMap($file = null)
+ {
+ $entries = $this->getZipEntries($file);
+
+ $sizeMap = [];
+ foreach ($entries as $filename => $entry) {
+ $sizeMap[$filename] = $entry['size'];
+ }
+
+ return $sizeMap;
+ }
+
+ /**
+ * @return int
+ */
+ public function getImageCount()
+ {
+ $entries = $this->getZipEntries();
+ $images = array_filter($entries, static function ($k) {
+ return preg_match('/(.jpeg|.jpg|.png|.gif)/', $k);
+ }, ARRAY_FILTER_USE_KEY);
+
+ return count($images);
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Other.php b/vendor/mikespub/php-epub-meta/src/Other.php
new file mode 100644
index 000000000..6d60197ce
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Other.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * Representation of an EPUB document.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org> © 2012
+ * @author Simon Schrape <simon@epubli.com> © 2015
+ */
+
+//namespace Epubli\Epub;
+
+namespace SebLucas\EPubMeta;
+
+use SebLucas\EPubMeta\Dom\Element as EpubDomElement;
+use SebLucas\EPubMeta\Dom\XPath as EpubDomXPath;
+use SebLucas\EPubMeta\Data\Manifest;
+use SebLucas\EPubMeta\Contents\Spine;
+use SebLucas\EPubMeta\Contents\Toc;
+use DOMDocument;
+use DOMElement;
+use Exception;
+use InvalidArgumentException;
+use ZipArchive;
+
+/**
+ * @todo These are the methods that haven't been integrated with EPub here...
+ */
+class Other extends EPub
+{
+ /**
+ * A simple setter for simple meta attributes
+ *
+ * It should only be used for attributes that are expected to be unique
+ *
+ * @param string $item XML node to set
+ * @param string $value New node value
+ * @param bool|string $attribute Attribute name
+ * @param bool|string $attributeValue Attribute value
+ * @param bool $caseSensitive
+ */
+ protected function setMeta($item, $value, $attribute = false, $attributeValue = false, $caseSensitive = true)
+ {
+ $xpath = $this->buildMetaXPath($item, $attribute, $attributeValue, $caseSensitive);
+
+ // set value
+ $nodes = $this->xpath->query($xpath);
+ if ($nodes->length == 1) {
+ /** @var EpubDomElement $node */
+ $node = $nodes->item(0);
+ if ($value === '') {
+ // the user wants to empty this value -> delete the node
+ $node->delete();
+ } else {
+ // replace value
+ $node->nodeValueUnescaped = $value;
+ }
+ } else {
+ // if there are multiple matching nodes for some reason delete
+ // them. we'll replace them all with our own single one
+ foreach ($nodes as $node) {
+ /** @var EpubDomElement $node */
+ $node->delete();
+ }
+ // re-add them
+ if ($value) {
+ $parent = $this->xpath->query('//opf:metadata')->item(0);
+ $node = new EpubDomElement($item, $value);
+ $node = $parent->appendChild($node);
+ if ($attribute) {
+ if (is_array($attributeValue)) {
+ // use first given value for new attribute
+ $attributeValue = reset($attributeValue);
+ }
+ $node->setAttrib($attribute, $attributeValue);
+ }
+ }
+ }
+
+ $this->sync();
+ }
+
+ /**
+ * A simple getter for simple meta attributes
+ *
+ * It should only be used for attributes that are expected to be unique
+ *
+ * @param string $item XML node to get
+ * @param bool|string $att Attribute name
+ * @param bool|string $aval Attribute value
+ * @param bool $caseSensitive
+ * @return string
+ */
+ protected function getMeta($item, $att = false, $aval = false, $caseSensitive = true)
+ {
+ $xpath = $this->buildMetaXPath($item, $att, $aval, $caseSensitive);
+
+ // get value
+ $nodes = $this->xpath->query($xpath);
+ if ($nodes->length) {
+ /** @var EpubDomElement $node */
+ $node = $nodes->item(0);
+
+ return $node->nodeValueUnescaped;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Sync XPath object with updated DOM.
+ */
+ protected function sync()
+ {
+ $dom = $this->xpath->document;
+ $dom->loadXML($dom->saveXML());
+ $this->xpath = new EpubDomXPath($dom);
+ // reset structural members
+ $this->manifest = null;
+ $this->spine = null;
+ $this->tocnav = null;
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/HtmlTools.php b/vendor/mikespub/php-epub-meta/src/Tools/HtmlTools.php
new file mode 100644
index 000000000..f905d265a
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/HtmlTools.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace SebLucas\EPubMeta\Tools;
+
+/**
+ * From Epubli\Common\Tools - see https://github.com/epubli/common
+ * @author Epubli Developers <devs@epubli.com>
+ */
+class HtmlTools
+{
+ /**
+ * @param string $html
+ * @return string
+ */
+ public static function convertEntitiesNamedToNumeric($html)
+ {
+ return strtr($html, include(__DIR__ . '/htmlEntityMap.php'));
+ }
+
+ /**
+ * @param string $name
+ * @return bool
+ */
+ public static function isBlockLevelElement($name)
+ {
+ return in_array($name, include(__DIR__ . '/htmlBlockLevelElements.php'));
+ }
+
+ /**
+ * performs a tag-aware truncation of (html-) strings, preserving tag integrity
+ * @param array<string>|string $html
+ * @param int|string $length
+ * @return bool|string
+ */
+ public static function truncate($html, $length = "20%")
+ {
+ $htmls = is_array($html) ? $html : [$html];
+ foreach ($htmls as &$htmlString) {
+ if (is_string($length)) {
+ $length = trim($length);
+ /* interpret percentage value */
+ if (substr($length, -1) == '%') {
+ $length = (int) (strlen($htmlString) * intval(substr($length, 0, -1)) / 100);
+ }
+ }
+ $htmlString = substr($htmlString, 0, $length);
+ /* eliminate trailing truncated tag fragment if present */
+ $htmlString = preg_replace('/<[^>]*$/is', '', $htmlString);
+ }
+
+ return is_array($html) ? $htmls : array_pop($htmls);
+ }
+
+ /**
+ * strips all occurring html tags from $html (which can either be a string or an array of strings),
+ * preserving all content enclosed by all tags in $keep and
+ * dumping the content residing in all tags listed in $drop
+ * @param array<string>|string $html
+ * @param array<string> $keep
+ * @param array<string> $drop
+ * @return array<string>|string
+ */
+ public static function stripHtmlTags(
+ $html,
+ $keep =
+ ['title', 'br', 'p', 'h1','h2','h3','h4','h5','span','div','i','strong','b', 'table', 'td', 'th', 'tr'],
+ $drop =
+ ['head','style']
+ ) {
+ $htmls = is_array($html) ? $html : [$html];
+ foreach ($htmls as &$htmlString) {
+ foreach ($drop as $dumpTag) {
+ $htmlString = preg_replace("/<$dumpTag.*$dumpTag>/is", "\n", $htmlString);
+ }
+ $htmlString = preg_replace("/[\n\r ]{2,}/i", "\n", $htmlString);
+ $htmlString = preg_replace("/[\n|\r]/i", '<br />', $htmlString);
+
+ /* @TODO: remove style tags and only keep body content (drop head) */
+ $tempFunc = function ($matches) use ($keep) {
+ $htmlNode = "<" . $matches[1] . ">" . strip_tags($matches[2]) . "</" . $matches[1] . ">";
+ if (in_array($matches[1], $keep)) {
+ return " " . $htmlNode . " ";
+ } else {
+ return "";
+ }
+ };
+
+ $allowedTags = implode("|", array_values($keep));
+ $regExp = '@<(' . $allowedTags . ')[^>]*?>(.*?)<\/\1>@i';
+ $htmlString = preg_replace_callback($regExp, $tempFunc, $htmlString);
+
+ $htmlString = strip_tags($htmlString, "<" . implode("><", $keep) . ">");
+ }
+ /* preserve injected variable cast type (string|array) when returning processed entity */
+ return is_array($html) ? $htmls : array_pop($htmls);
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php b/vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php
new file mode 100644
index 000000000..ff9f607cf
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php
@@ -0,0 +1,450 @@
+<?php
+/**
+ * ZipEdit class
+ *
+ * @author mikespub
+ */
+
+namespace SebLucas\EPubMeta\Tools;
+
+use ZipStream\ZipStream;
+use DateTime;
+use Exception;
+use ZipArchive;
+
+/**
+ * ZipEdit class allows to edit zip files on the fly and stream them afterwards
+ */
+class ZipEdit
+{
+ public const DOWNLOAD = 1; // download (default)
+ public const NOHEADER = 4; // option to use with DOWNLOAD: no header is sent
+ public const FILE = 8; // output to file , or add from file
+ public const STRING = 32; // output to string, or add from string
+ public const MIME_TYPE = 'application/epub+zip';
+
+ /** @var ZipArchive|null */
+ private $mZip;
+ /** @var array<string, mixed>|null */
+ private $mEntries;
+ /** @var array<string, mixed> */
+ private $mChanges = [];
+ /** @var string|null */
+ private $mFileName;
+ private bool $mSaveMe = false;
+
+ public function __construct()
+ {
+ $this->mZip = null;
+ $this->mEntries = null;
+ $this->mFileName = null;
+ }
+
+ /**
+ * Destructor
+ */
+ public function __destruct()
+ {
+ $this->Close();
+ }
+
+ /**
+ * Open a zip file and read it's entries
+ *
+ * @param string $inFileName
+ * @param int|null $inFlags
+ * @return boolean True if zip file has been correctly opended, else false
+ */
+ public function Open($inFileName, $inFlags = 0) // ZipArchive::RDONLY)
+ {
+ $this->Close();
+
+ $this->mZip = new ZipArchive();
+ $result = $this->mZip->open($inFileName, ZipArchive::RDONLY);
+ if ($result !== true) {
+ return false;
+ }
+
+ $this->mFileName = $inFileName;
+
+ $this->mEntries = [];
+ $this->mChanges = [];
+
+ for ($i = 0; $i < $this->mZip->numFiles; $i++) {
+ $entry = $this->mZip->statIndex($i);
+ $fileName = $entry['name'];
+ $this->mEntries[$fileName] = $entry;
+ $this->mChanges[$fileName] = ['status' => 'unchanged'];
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if a file exist in the zip entries
+ *
+ * @param string $inFileName File to search
+ *
+ * @return boolean True if the file exist, else false
+ */
+ public function FileExists($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Read the content of a file in the zip entries
+ *
+ * @param string $inFileName File to search
+ *
+ * @return mixed File content the file exist, else false
+ */
+ public function FileRead($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ $data = false;
+
+ $changes = $this->mChanges[$inFileName] ?? ['status' => 'unchanged'];
+ switch ($changes['status']) {
+ case 'unchanged':
+ $data = $this->mZip->getFromName($inFileName);
+ break;
+ case 'added':
+ case 'modified':
+ if (isset($changes['data'])) {
+ $data = $changes['data'];
+ } elseif (isset($changes['path'])) {
+ $data = file_get_contents($changes['path']);
+ }
+ break;
+ case 'deleted':
+ default:
+ break;
+ }
+ return $data;
+ }
+
+ /**
+ * Get a file handler to a file in the zip entries (read-only)
+ *
+ * @param string $inFileName File to search
+ *
+ * @return resource|bool File handler if the file exist, else false
+ */
+ public function FileStream($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ // @todo streaming of added/modified data?
+ return $this->mZip->getStream($inFileName);
+ }
+
+ /**
+ * Summary of FileAdd
+ * @param string $inFileName
+ * @param mixed $inData
+ * @return bool
+ */
+ public function FileAdd($inFileName, $inData)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ $this->mEntries[$inFileName] = [
+ 'name' => $inFileName, // 'foobar/baz',
+ 'size' => strlen($inData),
+ 'mtime' => time(), // 1123164748,
+ ];
+ $this->mChanges[$inFileName] = ['status' => 'added', 'data' => $inData];
+ return true;
+ }
+
+ /**
+ * Summary of FileAddPath
+ * @param string $inFileName
+ * @param string $inFilePath
+ * @return bool
+ */
+ public function FileAddPath($inFileName, $inFilePath)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ $this->mEntries[$inFileName] = [
+ 'name' => $inFileName, // 'foobar/baz',
+ 'size' => filesize($inFilePath),
+ 'mtime' => filemtime($inFilePath), // 1123164748,
+ ];
+ $this->mChanges[$inFileName] = ['status' => 'added', 'path' => $inFilePath];
+ return true;
+ }
+
+ /**
+ * Summary of FileDelete
+ * @param string $inFileName
+ * @return bool
+ */
+ public function FileDelete($inFileName)
+ {
+ if (!$this->FileExists($inFileName)) {
+ return false;
+ }
+
+ $this->mEntries[$inFileName]['size'] = 0;
+ $this->mEntries[$inFileName]['mtime'] = time();
+ $this->mChanges[$inFileName] = ['status' => 'deleted'];
+ return true;
+ }
+
+ /**
+ * Replace the content of a file in the zip entries
+ *
+ * @param string $inFileName File with content to replace
+ * @param string|bool $inData Data content to replace, or false to delete
+ * @return bool
+ */
+ public function FileReplace($inFileName, $inData)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if ($inData === false) {
+ return $this->FileDelete($inFileName);
+ }
+
+ $this->mEntries[$inFileName] ??= [];
+ $this->mEntries[$inFileName]['name'] = $inFileName;
+ $this->mEntries[$inFileName]['size'] = strlen($inData);
+ $this->mEntries[$inFileName]['mtime'] = time();
+ $this->mChanges[$inFileName] = ['status' => 'modified', 'data' => $inData];
+ return true;
+ }
+
+ /**
+ * Return the state of the file.
+ * @param mixed $inFileName
+ * @return string|bool 'u'=unchanged, 'm'=modified, 'd'=deleted, 'a'=added, false=unknown
+ */
+ public function FileGetState($inFileName)
+ {
+ $changes = $this->mChanges[$inFileName] ?? ['status' => false];
+ return $changes['status'];
+ }
+
+ /**
+ * Summary of FileCancelModif
+ * @param string $inFileName
+ * @param bool $ReplacedAndDeleted
+ * @return int
+ */
+ public function FileCancelModif($inFileName, $ReplacedAndDeleted = true)
+ {
+ // cancel added, modified or deleted modifications on a file in the archive
+ // return the number of cancels
+
+ $nbr = 0;
+
+ $this->mChanges[$inFileName] = ['status' => 'unchanged'];
+ $nbr += 1;
+ return $nbr;
+ }
+
+ /**
+ * Close the zip file
+ *
+ * @return void
+ * @throws Exception
+ */
+ public function Close()
+ {
+ if (!isset($this->mZip)) {
+ return;
+ }
+
+ $outFileName = $this->mFileName . '.copy';
+ if ($this->mSaveMe) {
+ $outFileStream = fopen($outFileName, 'wb+');
+ if ($outFileStream === false) {
+ throw new Exception('Unable to open zip copy ' . $outFileName);
+ }
+ $this->Flush(self::DOWNLOAD, $this->mFileName, self::MIME_TYPE, false, $outFileStream);
+ $result = fclose($outFileStream);
+ if ($result === false) {
+ throw new Exception('Unable to close zip copy ' . $outFileName);
+ }
+ }
+
+ if (!$this->mZip->close()) {
+ $status = $this->mZip->getStatusString();
+ $this->mZip = null;
+ throw new Exception($status);
+ }
+ if ($this->mSaveMe) {
+ $result = rename($outFileName, $this->mFileName);
+ if ($result === false) {
+ throw new Exception('Unable to rename zip copy ' . $outFileName);
+ }
+ $this->mSaveMe = false;
+ }
+ $this->mZip = null;
+ }
+
+ /**
+ * Summary of SaveBeforeClose
+ * @return void
+ */
+ public function SaveBeforeClose()
+ {
+ // Coming from EPub()->download() without fileName, called in EPub()->save()
+ // This comes right before EPub()->zip->close(), at which point we're lost
+ $this->mSaveMe = true;
+ }
+
+ /**
+ * Summary of Flush
+ * @param mixed $render
+ * @param mixed $outFileName
+ * @param mixed $contentType
+ * @param bool $sendHeaders
+ * @param resource|null $outFileStream
+ * @return void
+ */
+ public function Flush($render = self::DOWNLOAD, $outFileName = '', $contentType = '', $sendHeaders = true, $outFileStream = null)
+ {
+ // we don't want to close the zip file to save all changes here - probably what you needed :-)
+ //$this->Close();
+
+ $outFileName = $outFileName ?: $this->mFileName;
+ $contentType = $contentType ?: self::MIME_TYPE;
+ if ($outFileStream) {
+ $sendHeaders = false;
+ }
+ if (!$sendHeaders) {
+ $render = $render | self::NOHEADER;
+ }
+ if (($render & self::NOHEADER) !== self::NOHEADER) {
+ $sendHeaders = true;
+ } else {
+ $sendHeaders = false;
+ }
+
+ $outZipStream = new ZipStream(
+ outputName: basename($outFileName),
+ outputStream: $outFileStream,
+ sendHttpHeaders: $sendHeaders,
+ contentType: $contentType,
+ );
+ foreach ($this->mEntries as $fileName => $entry) {
+ $changes = $this->mChanges[$fileName];
+ switch ($changes['status']) {
+ case 'unchanged':
+ // Automatic binding of $this
+ $callback = function () use ($fileName) {
+ // this expects a stream as result, not the actual data
+ return $this->mZip->getStream($fileName);
+ };
+ $date = new DateTime();
+ $date->setTimestamp($entry['mtime']);
+ $outZipStream->addFileFromCallback(
+ fileName: $fileName,
+ exactSize: $entry['size'],
+ lastModificationDateTime: $date,
+ callback: $callback,
+ );
+ break;
+ case 'added':
+ case 'modified':
+ if (isset($changes['data'])) {
+ $outZipStream->addFile(
+ fileName: $fileName,
+ data: $changes['data'],
+ );
+ } elseif (isset($changes['path'])) {
+ $outZipStream->addFileFromPath(
+ fileName: $fileName,
+ path: $changes['path'],
+ );
+ }
+ break;
+ case 'deleted':
+ default:
+ break;
+ }
+ }
+
+ $outZipStream->finish();
+ }
+
+ /**
+ * Summary of copyTest
+ * @param string $inFileName
+ * @param string $outFileName
+ * @return void
+ */
+ public static function copyTest($inFileName, $outFileName)
+ {
+ $inZipFile = new ZipArchive();
+ $result = $inZipFile->open($inFileName, ZipArchive::RDONLY);
+ if ($result !== true) {
+ throw new Exception('Unable to open zip file ' . $inFileName);
+ }
+
+ $entries = [];
+ for ($i = 0; $i < $inZipFile->numFiles; $i++) {
+ $entry = $inZipFile->statIndex($i);
+ $fileName = $entry['name'];
+ $entries[$fileName] = $entry;
+ }
+
+ // see ZipStreamTest.php
+ $outFileStream = fopen($outFileName, 'wb+');
+
+ $outZipStream = new ZipStream(
+ outputName: basename($outFileName),
+ outputStream: $outFileStream,
+ sendHttpHeaders: false,
+ );
+ foreach ($entries as $fileName => $entry) {
+ $date = new DateTime();
+ $date->setTimestamp($entry['mtime']);
+ // does not work in v2 - the zip stream is not seekable, but ZipStream checks for it in Stream.php
+ // does work in v3 - implemented using addFileFromCallback, so we might as well use that :-)
+ $outZipStream->addFileFromCallback(
+ fileName: $fileName,
+ exactSize: $entry['size'],
+ lastModificationDateTime: $date,
+ callback: function () use ($inZipFile, $fileName) {
+ // this expects a stream as result, not the actual data
+ return $inZipFile->getStream($fileName);
+ },
+ );
+ }
+
+ $outZipStream->finish();
+ fclose($outFileStream);
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/ZipFile.php b/vendor/mikespub/php-epub-meta/src/Tools/ZipFile.php
new file mode 100644
index 000000000..b083b0c4f
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/ZipFile.php
@@ -0,0 +1,343 @@
+<?php
+/**
+ * ZipFile class
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Didier Corbière <contact@atoll-digital-library.org>
+ * @author mikespub
+ */
+
+namespace SebLucas\EPubMeta\Tools;
+
+use Exception;
+use ZipArchive;
+
+/**
+ * ZipFile class allows to open files inside a zip file with the standard php zip functions
+ *
+ * This class also supports adding/replacing/deleting files inside the zip file, but changes
+ * will *not* be reflected correctly until you close the zip file, and open it again if needed
+ *
+ * Note: this is not meant to handle a massive amount of files inside generic archive files.
+ * It is specifically meant for EPUB files which typically contain hundreds/thousands of pages,
+ * not millions or more. And any changes you make are kept in memory, so don't re-write every
+ * page of War and Peace either - better to unzip that locally and then re-zip it afterwards.
+ */
+class ZipFile
+{
+ public const DOWNLOAD = 1; // download (default)
+ public const NOHEADER = 4; // option to use with DOWNLOAD: no header is sent
+ public const FILE = 8; // output to file , or add from file
+ public const STRING = 32; // output to string, or add from string
+ public const MIME_TYPE = 'application/epub+zip';
+
+ /** @var ZipArchive|null */
+ protected $mZip;
+ /** @var array<string, mixed>|null */
+ protected $mEntries;
+ /** @var array<string, mixed> */
+ protected $mChanges = [];
+ /** @var string|null */
+ protected $mFileName;
+
+ public function __construct()
+ {
+ $this->mZip = null;
+ $this->mEntries = null;
+ $this->mFileName = null;
+ }
+
+ /**
+ * Destructor
+ */
+ public function __destruct()
+ {
+ $this->Close();
+ }
+
+ /**
+ * Open a zip file and read it's entries
+ *
+ * @param string $inFileName
+ * @param int|null $inFlags
+ * @return boolean True if zip file has been correctly opended, else false
+ */
+ public function Open($inFileName, $inFlags = 0) // ZipArchive::RDONLY)
+ {
+ $this->Close();
+ $inFileName = realpath($inFileName);
+
+ $this->mZip = new ZipArchive();
+ $result = $this->mZip->open($inFileName, $inFlags);
+ if ($result !== true) {
+ return false;
+ }
+
+ $this->mFileName = $inFileName;
+
+ $this->mEntries = [];
+ $this->mChanges = [];
+
+ for ($i = 0; $i < $this->mZip->numFiles; $i++) {
+ $entry = $this->mZip->statIndex($i);
+ $fileName = $entry['name'];
+ $this->mEntries[$fileName] = $entry;
+ $this->mChanges[$fileName] = ['status' => 'unchanged'];
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if a file exist in the zip entries
+ *
+ * @param string $inFileName File to search
+ *
+ * @return boolean True if the file exist, else false
+ */
+ public function FileExists($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Read the content of a file in the zip entries
+ *
+ * @param string $inFileName File to search
+ *
+ * @return mixed File content the file exist, else false
+ */
+ public function FileRead($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ $data = false;
+
+ $changes = $this->mChanges[$inFileName] ?? ['status' => 'unchanged'];
+ switch ($changes['status']) {
+ case 'unchanged':
+ $data = $this->mZip->getFromName($inFileName);
+ break;
+ case 'added':
+ case 'modified':
+ if (isset($changes['data'])) {
+ $data = $changes['data'];
+ } elseif (isset($changes['path'])) {
+ $data = file_get_contents($changes['path']);
+ }
+ break;
+ case 'deleted':
+ default:
+ break;
+ }
+ return $data;
+ }
+
+ /**
+ * Get a file handler to a file in the zip entries (read-only)
+ *
+ * @param string $inFileName File to search
+ *
+ * @return resource|bool File handler if the file exist, else false
+ */
+ public function FileStream($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ // @todo streaming of added/modified data?
+ return $this->mZip->getStream($inFileName);
+ }
+
+ /**
+ * Summary of FileAdd
+ * @param string $inFileName
+ * @param mixed $inData
+ * @return bool
+ */
+ public function FileAdd($inFileName, $inData)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!$this->mZip->addFromString($inFileName, $inData)) {
+ return false;
+ }
+ $this->mEntries[$inFileName] = $this->mZip->statName($inFileName);
+ $this->mChanges[$inFileName] = ['status' => 'added', 'data' => $inData];
+ return true;
+ }
+
+ /**
+ * Summary of FileAddPath
+ * @param string $inFileName
+ * @param string $inFilePath
+ * @return mixed
+ */
+ public function FileAddPath($inFileName, $inFilePath)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!$this->mZip->addFile($inFilePath, $inFileName)) {
+ return false;
+ }
+ $this->mEntries[$inFileName] = $this->mZip->statName($inFileName);
+ $this->mChanges[$inFileName] = ['status' => 'added', 'path' => $inFilePath];
+ return true;
+ }
+
+ /**
+ * Summary of FileDelete
+ * @param string $inFileName
+ * @return bool
+ */
+ public function FileDelete($inFileName)
+ {
+ if (!$this->FileExists($inFileName)) {
+ return false;
+ }
+
+ if (!$this->mZip->deleteName($inFileName)) {
+ return false;
+ }
+ unset($this->mEntries[$inFileName]);
+ $this->mChanges[$inFileName] = ['status' => 'deleted'];
+ return true;
+ }
+
+ /**
+ * Replace the content of a file in the zip entries
+ *
+ * @param string $inFileName File with content to replace
+ * @param string|bool $inData Data content to replace, or false to delete
+ * @return bool
+ */
+ public function FileReplace($inFileName, $inData)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if ($inData === false) {
+ return $this->FileDelete($inFileName);
+ }
+
+ if (!$this->mZip->addFromString($inFileName, $inData)) {
+ return false;
+ }
+ $this->mEntries[$inFileName] = $this->mZip->statName($inFileName);
+ $this->mChanges[$inFileName] = ['status' => 'modified', 'data' => $inData];
+ return true;
+ }
+
+ /**
+ * Return the state of the file.
+ * @param mixed $inFileName
+ * @return string|bool 'u'=unchanged, 'm'=modified, 'd'=deleted, 'a'=added, false=unknown
+ */
+ public function FileGetState($inFileName)
+ {
+ $changes = $this->mChanges[$inFileName] ?? ['status' => false];
+ return $changes['status'];
+ }
+
+ /**
+ * Summary of FileCancelModif
+ * @param string $inFileName
+ * @param bool $ReplacedAndDeleted
+ * @return int
+ */
+ public function FileCancelModif($inFileName, $ReplacedAndDeleted = true)
+ {
+ // cancel added, modified or deleted modifications on a file in the archive
+ // return the number of cancels
+
+ $nbr = 0;
+
+ if (!$this->mZip->unchangeName($inFileName)) {
+ return $nbr;
+ }
+ $nbr += 1;
+ $this->mChanges[$inFileName] = ['status' => 'unchanged'];
+ return $nbr;
+ }
+
+ /**
+ * Close the zip file
+ *
+ * @return void
+ * @throws Exception
+ */
+ public function Close()
+ {
+ if (!isset($this->mZip)) {
+ return;
+ }
+
+ if (!$this->mZip->close()) {
+ $status = $this->mZip->getStatusString();
+ $this->mZip = null;
+ throw new Exception($status);
+ }
+ $this->mZip = null;
+ }
+
+ /**
+ * Summary of Flush
+ * @param mixed $render
+ * @param mixed $outFileName
+ * @param mixed $contentType
+ * @param bool $sendHeaders
+ * @return void
+ */
+ public function Flush($render = self::DOWNLOAD, $outFileName = '', $contentType = '', $sendHeaders = true)
+ {
+ // we need to close the zip file to save all changes here - probably not what you wanted :-()
+ $this->Close();
+
+ $outFileName = $outFileName ?: $this->mFileName;
+ $contentType = $contentType ?: static::MIME_TYPE;
+ if (!$sendHeaders) {
+ $render = $render | static::NOHEADER;
+ }
+ $inFilePath = realpath($this->mFileName);
+
+ if (($render & static::NOHEADER) !== static::NOHEADER) {
+ $expires = 60 * 60 * 24 * 14;
+ header('Pragma: public');
+ header('Cache-Control: max-age=' . $expires);
+ header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
+
+ header('Content-Type: ' . $contentType);
+ header('Content-Disposition: attachment; filename="' . basename($outFileName) . '"');
+
+ // see fetch.php for use of Config::get('x_accel_redirect')
+ header('Content-Length: ' . filesize($inFilePath));
+ //header(Config::get('x_accel_redirect') . ': ' . $inFilePath);
+ }
+
+ readfile($inFilePath);
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/htmlBlockLevelElements.php b/vendor/mikespub/php-epub-meta/src/Tools/htmlBlockLevelElements.php
new file mode 100644
index 000000000..6e96a6b0f
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/htmlBlockLevelElements.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * List of HTML block level elements.
+ * Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
+ */
+return [
+ 'address',
+ 'article',
+ 'aside',
+ 'audio',
+ 'video',
+ 'blockquote',
+ 'canvas',
+ 'dd',
+ 'div',
+ 'dl',
+ 'fieldset',
+ 'figcaption',
+ 'figure',
+ 'footer',
+ 'form',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'header',
+ 'hgroup',
+ 'hr',
+ 'li',
+ 'noscript',
+ 'ol',
+ 'output',
+ 'p',
+ 'pre',
+ 'section',
+ 'table',
+ 'tfoot',
+ 'ul',
+ 'video',
+];
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/htmlEntityMap.php b/vendor/mikespub/php-epub-meta/src/Tools/htmlEntityMap.php
new file mode 100644
index 000000000..6bc3f4653
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/htmlEntityMap.php
@@ -0,0 +1,260 @@
+<?php
+/**
+ * Maps every HTML named entity to its numeric equivalent.
+ * Source: http://stackoverflow.com/questions/11176752/converting-named-html-entities-to-numeric-html-entities#answer-11179875
+ */
+return [
+'&nbsp;' => '&#160;', # no-break space = non-breaking space, U+00A0 ISOnum
+'&iexcl;' => '&#161;', # inverted exclamation mark, U+00A1 ISOnum
+'&cent;' => '&#162;', # cent sign, U+00A2 ISOnum
+'&pound;' => '&#163;', # pound sign, U+00A3 ISOnum
+'&curren;' => '&#164;', # currency sign, U+00A4 ISOnum
+'&yen;' => '&#165;', # yen sign = yuan sign, U+00A5 ISOnum
+'&brvbar;' => '&#166;', # broken bar = broken vertical bar, U+00A6 ISOnum
+'&sect;' => '&#167;', # section sign, U+00A7 ISOnum
+'&uml;' => '&#168;', # diaeresis = spacing diaeresis, U+00A8 ISOdia
+'&copy;' => '&#169;', # copyright sign, U+00A9 ISOnum
+'&ordf;' => '&#170;', # feminine ordinal indicator, U+00AA ISOnum
+'&laquo;' => '&#171;', # left-pointing double angle quotation mark = left pointing guillemet, U+00AB ISOnum
+'&not;' => '&#172;', # not sign, U+00AC ISOnum
+'&shy;' => '&#173;', # soft hyphen = discretionary hyphen, U+00AD ISOnum
+'&reg;' => '&#174;', # registered sign = registered trade mark sign, U+00AE ISOnum
+'&macr;' => '&#175;', # macron = spacing macron = overline = APL overbar, U+00AF ISOdia
+'&deg;' => '&#176;', # degree sign, U+00B0 ISOnum
+'&plusmn;' => '&#177;', # plus-minus sign = plus-or-minus sign, U+00B1 ISOnum
+'&sup2;' => '&#178;', # superscript two = superscript digit two = squared, U+00B2 ISOnum
+'&sup3;' => '&#179;', # superscript three = superscript digit three = cubed, U+00B3 ISOnum
+'&acute;' => '&#180;', # acute accent = spacing acute, U+00B4 ISOdia
+'&micro;' => '&#181;', # micro sign, U+00B5 ISOnum
+'&para;' => '&#182;', # pilcrow sign = paragraph sign, U+00B6 ISOnum
+'&middot;' => '&#183;', # middle dot = Georgian comma = Greek middle dot, U+00B7 ISOnum
+'&cedil;' => '&#184;', # cedilla = spacing cedilla, U+00B8 ISOdia
+'&sup1;' => '&#185;', # superscript one = superscript digit one, U+00B9 ISOnum
+'&ordm;' => '&#186;', # masculine ordinal indicator, U+00BA ISOnum
+'&raquo;' => '&#187;', # right-pointing double angle quotation mark = right pointing guillemet, U+00BB ISOnum
+'&frac14;' => '&#188;', # vulgar fraction one quarter = fraction one quarter, U+00BC ISOnum
+'&frac12;' => '&#189;', # vulgar fraction one half = fraction one half, U+00BD ISOnum
+'&frac34;' => '&#190;', # vulgar fraction three quarters = fraction three quarters, U+00BE ISOnum
+'&iquest;' => '&#191;', # inverted question mark = turned question mark, U+00BF ISOnum
+'&Agrave;' => '&#192;', # latin capital letter A with grave = latin capital letter A grave, U+00C0 ISOlat1
+'&Aacute;' => '&#193;', # latin capital letter A with acute, U+00C1 ISOlat1
+'&Acirc;' => '&#194;', # latin capital letter A with circumflex, U+00C2 ISOlat1
+'&Atilde;' => '&#195;', # latin capital letter A with tilde, U+00C3 ISOlat1
+'&Auml;' => '&#196;', # latin capital letter A with diaeresis, U+00C4 ISOlat1
+'&Aring;' => '&#197;', # latin capital letter A with ring above = latin capital letter A ring, U+00C5 ISOlat1
+'&AElig;' => '&#198;', # latin capital letter AE = latin capital ligature AE, U+00C6 ISOlat1
+'&Ccedil;' => '&#199;', # latin capital letter C with cedilla, U+00C7 ISOlat1
+'&Egrave;' => '&#200;', # latin capital letter E with grave, U+00C8 ISOlat1
+'&Eacute;' => '&#201;', # latin capital letter E with acute, U+00C9 ISOlat1
+'&Ecirc;' => '&#202;', # latin capital letter E with circumflex, U+00CA ISOlat1
+'&Euml;' => '&#203;', # latin capital letter E with diaeresis, U+00CB ISOlat1
+'&Igrave;' => '&#204;', # latin capital letter I with grave, U+00CC ISOlat1
+'&Iacute;' => '&#205;', # latin capital letter I with acute, U+00CD ISOlat1
+'&Icirc;' => '&#206;', # latin capital letter I with circumflex, U+00CE ISOlat1
+'&Iuml;' => '&#207;', # latin capital letter I with diaeresis, U+00CF ISOlat1
+'&ETH;' => '&#208;', # latin capital letter ETH, U+00D0 ISOlat1
+'&Ntilde;' => '&#209;', # latin capital letter N with tilde, U+00D1 ISOlat1
+'&Ograve;' => '&#210;', # latin capital letter O with grave, U+00D2 ISOlat1
+'&Oacute;' => '&#211;', # latin capital letter O with acute, U+00D3 ISOlat1
+'&Ocirc;' => '&#212;', # latin capital letter O with circumflex, U+00D4 ISOlat1
+'&Otilde;' => '&#213;', # latin capital letter O with tilde, U+00D5 ISOlat1
+'&Ouml;' => '&#214;', # latin capital letter O with diaeresis, U+00D6 ISOlat1
+'&times;' => '&#215;', # multiplication sign, U+00D7 ISOnum
+'&Oslash;' => '&#216;', # latin capital letter O with stroke = latin capital letter O slash, U+00D8 ISOlat1
+'&Ugrave;' => '&#217;', # latin capital letter U with grave, U+00D9 ISOlat1
+'&Uacute;' => '&#218;', # latin capital letter U with acute, U+00DA ISOlat1
+'&Ucirc;' => '&#219;', # latin capital letter U with circumflex, U+00DB ISOlat1
+'&Uuml;' => '&#220;', # latin capital letter U with diaeresis, U+00DC ISOlat1
+'&Yacute;' => '&#221;', # latin capital letter Y with acute, U+00DD ISOlat1
+'&THORN;' => '&#222;', # latin capital letter THORN, U+00DE ISOlat1
+'&szlig;' => '&#223;', # latin small letter sharp s = ess-zed, U+00DF ISOlat1
+'&agrave;' => '&#224;', # latin small letter a with grave = latin small letter a grave, U+00E0 ISOlat1
+'&aacute;' => '&#225;', # latin small letter a with acute, U+00E1 ISOlat1
+'&acirc;' => '&#226;', # latin small letter a with circumflex, U+00E2 ISOlat1
+'&atilde;' => '&#227;', # latin small letter a with tilde, U+00E3 ISOlat1
+'&auml;' => '&#228;', # latin small letter a with diaeresis, U+00E4 ISOlat1
+'&aring;' => '&#229;', # latin small letter a with ring above = latin small letter a ring, U+00E5 ISOlat1
+'&aelig;' => '&#230;', # latin small letter ae = latin small ligature ae, U+00E6 ISOlat1
+'&ccedil;' => '&#231;', # latin small letter c with cedilla, U+00E7 ISOlat1
+'&egrave;' => '&#232;', # latin small letter e with grave, U+00E8 ISOlat1
+'&eacute;' => '&#233;', # latin small letter e with acute, U+00E9 ISOlat1
+'&ecirc;' => '&#234;', # latin small letter e with circumflex, U+00EA ISOlat1
+'&euml;' => '&#235;', # latin small letter e with diaeresis, U+00EB ISOlat1
+'&igrave;' => '&#236;', # latin small letter i with grave, U+00EC ISOlat1
+'&iacute;' => '&#237;', # latin small letter i with acute, U+00ED ISOlat1
+'&icirc;' => '&#238;', # latin small letter i with circumflex, U+00EE ISOlat1
+'&iuml;' => '&#239;', # latin small letter i with diaeresis, U+00EF ISOlat1
+'&eth;' => '&#240;', # latin small letter eth, U+00F0 ISOlat1
+'&ntilde;' => '&#241;', # latin small letter n with tilde, U+00F1 ISOlat1
+'&ograve;' => '&#242;', # latin small letter o with grave, U+00F2 ISOlat1
+'&oacute;' => '&#243;', # latin small letter o with acute, U+00F3 ISOlat1
+'&ocirc;' => '&#244;', # latin small letter o with circumflex, U+00F4 ISOlat1
+'&otilde;' => '&#245;', # latin small letter o with tilde, U+00F5 ISOlat1
+'&ouml;' => '&#246;', # latin small letter o with diaeresis, U+00F6 ISOlat1
+'&divide;' => '&#247;', # division sign, U+00F7 ISOnum
+'&oslash;' => '&#248;', # latin small letter o with stroke, = latin small letter o slash, U+00F8 ISOlat1
+'&ugrave;' => '&#249;', # latin small letter u with grave, U+00F9 ISOlat1
+'&uacute;' => '&#250;', # latin small letter u with acute, U+00FA ISOlat1
+'&ucirc;' => '&#251;', # latin small letter u with circumflex, U+00FB ISOlat1
+'&uuml;' => '&#252;', # latin small letter u with diaeresis, U+00FC ISOlat1
+'&yacute;' => '&#253;', # latin small letter y with acute, U+00FD ISOlat1
+'&thorn;' => '&#254;', # latin small letter thorn, U+00FE ISOlat1
+'&yuml;' => '&#255;', # latin small letter y with diaeresis, U+00FF ISOlat1
+'&fnof;' => '&#402;', # latin small f with hook = function = florin, U+0192 ISOtech
+'&Alpha;' => '&#913;', # greek capital letter alpha, U+0391
+'&Beta;' => '&#914;', # greek capital letter beta, U+0392
+'&Gamma;' => '&#915;', # greek capital letter gamma, U+0393 ISOgrk3
+'&Delta;' => '&#916;', # greek capital letter delta, U+0394 ISOgrk3
+'&Epsilon;' => '&#917;', # greek capital letter epsilon, U+0395
+'&Zeta;' => '&#918;', # greek capital letter zeta, U+0396
+'&Eta;' => '&#919;', # greek capital letter eta, U+0397
+'&Theta;' => '&#920;', # greek capital letter theta, U+0398 ISOgrk3
+'&Iota;' => '&#921;', # greek capital letter iota, U+0399
+'&Kappa;' => '&#922;', # greek capital letter kappa, U+039A
+'&Lambda;' => '&#923;', # greek capital letter lambda, U+039B ISOgrk3
+'&Mu;' => '&#924;', # greek capital letter mu, U+039C
+'&Nu;' => '&#925;', # greek capital letter nu, U+039D
+'&Xi;' => '&#926;', # greek capital letter xi, U+039E ISOgrk3
+'&Omicron;' => '&#927;', # greek capital letter omicron, U+039F
+'&Pi;' => '&#928;', # greek capital letter pi, U+03A0 ISOgrk3
+'&Rho;' => '&#929;', # greek capital letter rho, U+03A1
+'&Sigma;' => '&#931;', # greek capital letter sigma, U+03A3 ISOgrk3
+'&Tau;' => '&#932;', # greek capital letter tau, U+03A4
+'&Upsilon;' => '&#933;', # greek capital letter upsilon, U+03A5 ISOgrk3
+'&Phi;' => '&#934;', # greek capital letter phi, U+03A6 ISOgrk3
+'&Chi;' => '&#935;', # greek capital letter chi, U+03A7
+'&Psi;' => '&#936;', # greek capital letter psi, U+03A8 ISOgrk3
+'&Omega;' => '&#937;', # greek capital letter omega, U+03A9 ISOgrk3
+'&alpha;' => '&#945;', # greek small letter alpha, U+03B1 ISOgrk3
+'&beta;' => '&#946;', # greek small letter beta, U+03B2 ISOgrk3
+'&gamma;' => '&#947;', # greek small letter gamma, U+03B3 ISOgrk3
+'&delta;' => '&#948;', # greek small letter delta, U+03B4 ISOgrk3
+'&epsilon;' => '&#949;', # greek small letter epsilon, U+03B5 ISOgrk3
+'&zeta;' => '&#950;', # greek small letter zeta, U+03B6 ISOgrk3
+'&eta;' => '&#951;', # greek small letter eta, U+03B7 ISOgrk3
+'&theta;' => '&#952;', # greek small letter theta, U+03B8 ISOgrk3
+'&iota;' => '&#953;', # greek small letter iota, U+03B9 ISOgrk3
+'&kappa;' => '&#954;', # greek small letter kappa, U+03BA ISOgrk3
+'&lambda;' => '&#955;', # greek small letter lambda, U+03BB ISOgrk3
+'&mu;' => '&#956;', # greek small letter mu, U+03BC ISOgrk3
+'&nu;' => '&#957;', # greek small letter nu, U+03BD ISOgrk3
+'&xi;' => '&#958;', # greek small letter xi, U+03BE ISOgrk3
+'&omicron;' => '&#959;', # greek small letter omicron, U+03BF NEW
+'&pi;' => '&#960;', # greek small letter pi, U+03C0 ISOgrk3
+'&rho;' => '&#961;', # greek small letter rho, U+03C1 ISOgrk3
+'&sigmaf;' => '&#962;', # greek small letter final sigma, U+03C2 ISOgrk3
+'&sigma;' => '&#963;', # greek small letter sigma, U+03C3 ISOgrk3
+'&tau;' => '&#964;', # greek small letter tau, U+03C4 ISOgrk3
+'&upsilon;' => '&#965;', # greek small letter upsilon, U+03C5 ISOgrk3
+'&phi;' => '&#966;', # greek small letter phi, U+03C6 ISOgrk3
+'&chi;' => '&#967;', # greek small letter chi, U+03C7 ISOgrk3
+'&psi;' => '&#968;', # greek small letter psi, U+03C8 ISOgrk3
+'&omega;' => '&#969;', # greek small letter omega, U+03C9 ISOgrk3
+'&thetasym;' => '&#977;', # greek small letter theta symbol, U+03D1 NEW
+'&upsih;' => '&#978;', # greek upsilon with hook symbol, U+03D2 NEW
+'&piv;' => '&#982;', # greek pi symbol, U+03D6 ISOgrk3
+'&bull;' => '&#8226;', # bullet = black small circle, U+2022 ISOpub
+'&hellip;' => '&#8230;', # horizontal ellipsis = three dot leader, U+2026 ISOpub
+'&prime;' => '&#8242;', # prime = minutes = feet, U+2032 ISOtech
+'&Prime;' => '&#8243;', # double prime = seconds = inches, U+2033 ISOtech
+'&oline;' => '&#8254;', # overline = spacing overscore, U+203E NEW
+'&frasl;' => '&#8260;', # fraction slash, U+2044 NEW
+'&weierp;' => '&#8472;', # script capital P = power set = Weierstrass p, U+2118 ISOamso
+'&image;' => '&#8465;', # blackletter capital I = imaginary part, U+2111 ISOamso
+'&real;' => '&#8476;', # blackletter capital R = real part symbol, U+211C ISOamso
+'&trade;' => '&#8482;', # trade mark sign, U+2122 ISOnum
+'&alefsym;' => '&#8501;', # alef symbol = first transfinite cardinal, U+2135 NEW
+'&larr;' => '&#8592;', # leftwards arrow, U+2190 ISOnum
+'&uarr;' => '&#8593;', # upwards arrow, U+2191 ISOnum
+'&rarr;' => '&#8594;', # rightwards arrow, U+2192 ISOnum
+'&darr;' => '&#8595;', # downwards arrow, U+2193 ISOnum
+'&harr;' => '&#8596;', # left right arrow, U+2194 ISOamsa
+'&crarr;' => '&#8629;', # downwards arrow with corner leftwards = carriage return, U+21B5 NEW
+'&lArr;' => '&#8656;', # leftwards double arrow, U+21D0 ISOtech
+'&uArr;' => '&#8657;', # upwards double arrow, U+21D1 ISOamsa
+'&rArr;' => '&#8658;', # rightwards double arrow, U+21D2 ISOtech
+'&dArr;' => '&#8659;', # downwards double arrow, U+21D3 ISOamsa
+'&hArr;' => '&#8660;', # left right double arrow, U+21D4 ISOamsa
+'&forall;' => '&#8704;', # for all, U+2200 ISOtech
+'&part;' => '&#8706;', # partial differential, U+2202 ISOtech
+'&exist;' => '&#8707;', # there exists, U+2203 ISOtech
+'&empty;' => '&#8709;', # empty set = null set = diameter, U+2205 ISOamso
+'&nabla;' => '&#8711;', # nabla = backward difference, U+2207 ISOtech
+'&isin;' => '&#8712;', # element of, U+2208 ISOtech
+'&notin;' => '&#8713;', # not an element of, U+2209 ISOtech
+'&ni;' => '&#8715;', # contains as member, U+220B ISOtech
+'&prod;' => '&#8719;', # n-ary product = product sign, U+220F ISOamsb
+'&sum;' => '&#8721;', # n-ary sumation, U+2211 ISOamsb
+'&minus;' => '&#8722;', # minus sign, U+2212 ISOtech
+'&lowast;' => '&#8727;', # asterisk operator, U+2217 ISOtech
+'&radic;' => '&#8730;', # square root = radical sign, U+221A ISOtech
+'&prop;' => '&#8733;', # proportional to, U+221D ISOtech
+'&infin;' => '&#8734;', # infinity, U+221E ISOtech
+'&ang;' => '&#8736;', # angle, U+2220 ISOamso
+'&and;' => '&#8743;', # logical and = wedge, U+2227 ISOtech
+'&or;' => '&#8744;', # logical or = vee, U+2228 ISOtech
+'&cap;' => '&#8745;', # intersection = cap, U+2229 ISOtech
+'&cup;' => '&#8746;', # union = cup, U+222A ISOtech
+'&int;' => '&#8747;', # integral, U+222B ISOtech
+'&there4;' => '&#8756;', # therefore, U+2234 ISOtech
+'&sim;' => '&#8764;', # tilde operator = varies with = similar to, U+223C ISOtech
+'&cong;' => '&#8773;', # approximately equal to, U+2245 ISOtech
+'&asymp;' => '&#8776;', # almost equal to = asymptotic to, U+2248 ISOamsr
+'&ne;' => '&#8800;', # not equal to, U+2260 ISOtech
+'&equiv;' => '&#8801;', # identical to, U+2261 ISOtech
+'&le;' => '&#8804;', # less-than or equal to, U+2264 ISOtech
+'&ge;' => '&#8805;', # greater-than or equal to, U+2265 ISOtech
+'&sub;' => '&#8834;', # subset of, U+2282 ISOtech
+'&sup;' => '&#8835;', # superset of, U+2283 ISOtech
+'&nsub;' => '&#8836;', # not a subset of, U+2284 ISOamsn
+'&sube;' => '&#8838;', # subset of or equal to, U+2286 ISOtech
+'&supe;' => '&#8839;', # superset of or equal to, U+2287 ISOtech
+'&oplus;' => '&#8853;', # circled plus = direct sum, U+2295 ISOamsb
+'&otimes;' => '&#8855;', # circled times = vector product, U+2297 ISOamsb
+'&perp;' => '&#8869;', # up tack = orthogonal to = perpendicular, U+22A5 ISOtech
+'&sdot;' => '&#8901;', # dot operator, U+22C5 ISOamsb
+'&lceil;' => '&#8968;', # left ceiling = apl upstile, U+2308 ISOamsc
+'&rceil;' => '&#8969;', # right ceiling, U+2309 ISOamsc
+'&lfloor;' => '&#8970;', # left floor = apl downstile, U+230A ISOamsc
+'&rfloor;' => '&#8971;', # right floor, U+230B ISOamsc
+'&lang;' => '&#9001;', # left-pointing angle bracket = bra, U+2329 ISOtech
+'&rang;' => '&#9002;', # right-pointing angle bracket = ket, U+232A ISOtech
+'&loz;' => '&#9674;', # lozenge, U+25CA ISOpub
+'&spades;' => '&#9824;', # black spade suit, U+2660 ISOpub
+'&clubs;' => '&#9827;', # black club suit = shamrock, U+2663 ISOpub
+'&hearts;' => '&#9829;', # black heart suit = valentine, U+2665 ISOpub
+'&diams;' => '&#9830;', # black diamond suit, U+2666 ISOpub
+'&quot;' => '&#34;', # quotation mark = APL quote, U+0022 ISOnum
+'&amp;' => '&#38;', # ampersand, U+0026 ISOnum
+'&lt;' => '&#60;', # less-than sign, U+003C ISOnum
+'&gt;' => '&#62;', # greater-than sign, U+003E ISOnum
+'&OElig;' => '&#338;', # latin capital ligature OE, U+0152 ISOlat2
+'&oelig;' => '&#339;', # latin small ligature oe, U+0153 ISOlat2
+'&Scaron;' => '&#352;', # latin capital letter S with caron, U+0160 ISOlat2
+'&scaron;' => '&#353;', # latin small letter s with caron, U+0161 ISOlat2
+'&Yuml;' => '&#376;', # latin capital letter Y with diaeresis, U+0178 ISOlat2
+'&circ;' => '&#710;', # modifier letter circumflex accent, U+02C6 ISOpub
+'&tilde;' => '&#732;', # small tilde, U+02DC ISOdia
+'&ensp;' => '&#8194;', # en space, U+2002 ISOpub
+'&emsp;' => '&#8195;', # em space, U+2003 ISOpub
+'&thinsp;' => '&#8201;', # thin space, U+2009 ISOpub
+'&zwnj;' => '&#8204;', # zero width non-joiner, U+200C NEW RFC 2070
+'&zwj;' => '&#8205;', # zero width joiner, U+200D NEW RFC 2070
+'&lrm;' => '&#8206;', # left-to-right mark, U+200E NEW RFC 2070
+'&rlm;' => '&#8207;', # right-to-left mark, U+200F NEW RFC 2070
+'&ndash;' => '&#8211;', # en dash, U+2013 ISOpub
+'&mdash;' => '&#8212;', # em dash, U+2014 ISOpub
+'&lsquo;' => '&#8216;', # left single quotation mark, U+2018 ISOnum
+'&rsquo;' => '&#8217;', # right single quotation mark, U+2019 ISOnum
+'&sbquo;' => '&#8218;', # single low-9 quotation mark, U+201A NEW
+'&ldquo;' => '&#8220;', # left double quotation mark, U+201C ISOnum
+'&rdquo;' => '&#8221;', # right double quotation mark, U+201D ISOnum
+'&bdquo;' => '&#8222;', # double low-9 quotation mark, U+201E NEW
+'&dagger;' => '&#8224;', # dagger, U+2020 ISOpub
+'&Dagger;' => '&#8225;', # double dagger, U+2021 ISOpub
+'&permil;' => '&#8240;', # per mille sign, U+2030 ISOtech
+'&lsaquo;' => '&#8249;', # single left-pointing angle quotation mark, U+2039 ISO proposed
+'&rsaquo;' => '&#8250;', # single right-pointing angle quotation mark, U+203A ISO proposed
+'&euro;' => '&#8364;', # euro sign, U+20AC NEW
+'&apos;' => '&#39;', # apostrophe = APL quote, U+0027 ISOnum
+];