diff options
Diffstat (limited to 'vendor/mikespub/php-epub-meta/src')
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']) . '&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 [ +' ' => ' ', # no-break space = non-breaking space, U+00A0 ISOnum +'¡' => '¡', # inverted exclamation mark, U+00A1 ISOnum +'¢' => '¢', # cent sign, U+00A2 ISOnum +'£' => '£', # pound sign, U+00A3 ISOnum +'¤' => '¤', # currency sign, U+00A4 ISOnum +'¥' => '¥', # yen sign = yuan sign, U+00A5 ISOnum +'¦' => '¦', # broken bar = broken vertical bar, U+00A6 ISOnum +'§' => '§', # section sign, U+00A7 ISOnum +'¨' => '¨', # diaeresis = spacing diaeresis, U+00A8 ISOdia +'©' => '©', # copyright sign, U+00A9 ISOnum +'ª' => 'ª', # feminine ordinal indicator, U+00AA ISOnum +'«' => '«', # left-pointing double angle quotation mark = left pointing guillemet, U+00AB ISOnum +'¬' => '¬', # not sign, U+00AC ISOnum +'­' => '­', # soft hyphen = discretionary hyphen, U+00AD ISOnum +'®' => '®', # registered sign = registered trade mark sign, U+00AE ISOnum +'¯' => '¯', # macron = spacing macron = overline = APL overbar, U+00AF ISOdia +'°' => '°', # degree sign, U+00B0 ISOnum +'±' => '±', # plus-minus sign = plus-or-minus sign, U+00B1 ISOnum +'²' => '²', # superscript two = superscript digit two = squared, U+00B2 ISOnum +'³' => '³', # superscript three = superscript digit three = cubed, U+00B3 ISOnum +'´' => '´', # acute accent = spacing acute, U+00B4 ISOdia +'µ' => 'µ', # micro sign, U+00B5 ISOnum +'¶' => '¶', # pilcrow sign = paragraph sign, U+00B6 ISOnum +'·' => '·', # middle dot = Georgian comma = Greek middle dot, U+00B7 ISOnum +'¸' => '¸', # cedilla = spacing cedilla, U+00B8 ISOdia +'¹' => '¹', # superscript one = superscript digit one, U+00B9 ISOnum +'º' => 'º', # masculine ordinal indicator, U+00BA ISOnum +'»' => '»', # right-pointing double angle quotation mark = right pointing guillemet, U+00BB ISOnum +'¼' => '¼', # vulgar fraction one quarter = fraction one quarter, U+00BC ISOnum +'½' => '½', # vulgar fraction one half = fraction one half, U+00BD ISOnum +'¾' => '¾', # vulgar fraction three quarters = fraction three quarters, U+00BE ISOnum +'¿' => '¿', # inverted question mark = turned question mark, U+00BF ISOnum +'À' => 'À', # latin capital letter A with grave = latin capital letter A grave, U+00C0 ISOlat1 +'Á' => 'Á', # latin capital letter A with acute, U+00C1 ISOlat1 +'Â' => 'Â', # latin capital letter A with circumflex, U+00C2 ISOlat1 +'Ã' => 'Ã', # latin capital letter A with tilde, U+00C3 ISOlat1 +'Ä' => 'Ä', # latin capital letter A with diaeresis, U+00C4 ISOlat1 +'Å' => 'Å', # latin capital letter A with ring above = latin capital letter A ring, U+00C5 ISOlat1 +'Æ' => 'Æ', # latin capital letter AE = latin capital ligature AE, U+00C6 ISOlat1 +'Ç' => 'Ç', # latin capital letter C with cedilla, U+00C7 ISOlat1 +'È' => 'È', # latin capital letter E with grave, U+00C8 ISOlat1 +'É' => 'É', # latin capital letter E with acute, U+00C9 ISOlat1 +'Ê' => 'Ê', # latin capital letter E with circumflex, U+00CA ISOlat1 +'Ë' => 'Ë', # latin capital letter E with diaeresis, U+00CB ISOlat1 +'Ì' => 'Ì', # latin capital letter I with grave, U+00CC ISOlat1 +'Í' => 'Í', # latin capital letter I with acute, U+00CD ISOlat1 +'Î' => 'Î', # latin capital letter I with circumflex, U+00CE ISOlat1 +'Ï' => 'Ï', # latin capital letter I with diaeresis, U+00CF ISOlat1 +'Ð' => 'Ð', # latin capital letter ETH, U+00D0 ISOlat1 +'Ñ' => 'Ñ', # latin capital letter N with tilde, U+00D1 ISOlat1 +'Ò' => 'Ò', # latin capital letter O with grave, U+00D2 ISOlat1 +'Ó' => 'Ó', # latin capital letter O with acute, U+00D3 ISOlat1 +'Ô' => 'Ô', # latin capital letter O with circumflex, U+00D4 ISOlat1 +'Õ' => 'Õ', # latin capital letter O with tilde, U+00D5 ISOlat1 +'Ö' => 'Ö', # latin capital letter O with diaeresis, U+00D6 ISOlat1 +'×' => '×', # multiplication sign, U+00D7 ISOnum +'Ø' => 'Ø', # latin capital letter O with stroke = latin capital letter O slash, U+00D8 ISOlat1 +'Ù' => 'Ù', # latin capital letter U with grave, U+00D9 ISOlat1 +'Ú' => 'Ú', # latin capital letter U with acute, U+00DA ISOlat1 +'Û' => 'Û', # latin capital letter U with circumflex, U+00DB ISOlat1 +'Ü' => 'Ü', # latin capital letter U with diaeresis, U+00DC ISOlat1 +'Ý' => 'Ý', # latin capital letter Y with acute, U+00DD ISOlat1 +'Þ' => 'Þ', # latin capital letter THORN, U+00DE ISOlat1 +'ß' => 'ß', # latin small letter sharp s = ess-zed, U+00DF ISOlat1 +'à' => 'à', # latin small letter a with grave = latin small letter a grave, U+00E0 ISOlat1 +'á' => 'á', # latin small letter a with acute, U+00E1 ISOlat1 +'â' => 'â', # latin small letter a with circumflex, U+00E2 ISOlat1 +'ã' => 'ã', # latin small letter a with tilde, U+00E3 ISOlat1 +'ä' => 'ä', # latin small letter a with diaeresis, U+00E4 ISOlat1 +'å' => 'å', # latin small letter a with ring above = latin small letter a ring, U+00E5 ISOlat1 +'æ' => 'æ', # latin small letter ae = latin small ligature ae, U+00E6 ISOlat1 +'ç' => 'ç', # latin small letter c with cedilla, U+00E7 ISOlat1 +'è' => 'è', # latin small letter e with grave, U+00E8 ISOlat1 +'é' => 'é', # latin small letter e with acute, U+00E9 ISOlat1 +'ê' => 'ê', # latin small letter e with circumflex, U+00EA ISOlat1 +'ë' => 'ë', # latin small letter e with diaeresis, U+00EB ISOlat1 +'ì' => 'ì', # latin small letter i with grave, U+00EC ISOlat1 +'í' => 'í', # latin small letter i with acute, U+00ED ISOlat1 +'î' => 'î', # latin small letter i with circumflex, U+00EE ISOlat1 +'ï' => 'ï', # latin small letter i with diaeresis, U+00EF ISOlat1 +'ð' => 'ð', # latin small letter eth, U+00F0 ISOlat1 +'ñ' => 'ñ', # latin small letter n with tilde, U+00F1 ISOlat1 +'ò' => 'ò', # latin small letter o with grave, U+00F2 ISOlat1 +'ó' => 'ó', # latin small letter o with acute, U+00F3 ISOlat1 +'ô' => 'ô', # latin small letter o with circumflex, U+00F4 ISOlat1 +'õ' => 'õ', # latin small letter o with tilde, U+00F5 ISOlat1 +'ö' => 'ö', # latin small letter o with diaeresis, U+00F6 ISOlat1 +'÷' => '÷', # division sign, U+00F7 ISOnum +'ø' => 'ø', # latin small letter o with stroke, = latin small letter o slash, U+00F8 ISOlat1 +'ù' => 'ù', # latin small letter u with grave, U+00F9 ISOlat1 +'ú' => 'ú', # latin small letter u with acute, U+00FA ISOlat1 +'û' => 'û', # latin small letter u with circumflex, U+00FB ISOlat1 +'ü' => 'ü', # latin small letter u with diaeresis, U+00FC ISOlat1 +'ý' => 'ý', # latin small letter y with acute, U+00FD ISOlat1 +'þ' => 'þ', # latin small letter thorn, U+00FE ISOlat1 +'ÿ' => 'ÿ', # latin small letter y with diaeresis, U+00FF ISOlat1 +'ƒ' => 'ƒ', # latin small f with hook = function = florin, U+0192 ISOtech +'Α' => 'Α', # greek capital letter alpha, U+0391 +'Β' => 'Β', # greek capital letter beta, U+0392 +'Γ' => 'Γ', # greek capital letter gamma, U+0393 ISOgrk3 +'Δ' => 'Δ', # greek capital letter delta, U+0394 ISOgrk3 +'Ε' => 'Ε', # greek capital letter epsilon, U+0395 +'Ζ' => 'Ζ', # greek capital letter zeta, U+0396 +'Η' => 'Η', # greek capital letter eta, U+0397 +'Θ' => 'Θ', # greek capital letter theta, U+0398 ISOgrk3 +'Ι' => 'Ι', # greek capital letter iota, U+0399 +'Κ' => 'Κ', # greek capital letter kappa, U+039A +'Λ' => 'Λ', # greek capital letter lambda, U+039B ISOgrk3 +'Μ' => 'Μ', # greek capital letter mu, U+039C +'Ν' => 'Ν', # greek capital letter nu, U+039D +'Ξ' => 'Ξ', # greek capital letter xi, U+039E ISOgrk3 +'Ο' => 'Ο', # greek capital letter omicron, U+039F +'Π' => 'Π', # greek capital letter pi, U+03A0 ISOgrk3 +'Ρ' => 'Ρ', # greek capital letter rho, U+03A1 +'Σ' => 'Σ', # greek capital letter sigma, U+03A3 ISOgrk3 +'Τ' => 'Τ', # greek capital letter tau, U+03A4 +'Υ' => 'Υ', # greek capital letter upsilon, U+03A5 ISOgrk3 +'Φ' => 'Φ', # greek capital letter phi, U+03A6 ISOgrk3 +'Χ' => 'Χ', # greek capital letter chi, U+03A7 +'Ψ' => 'Ψ', # greek capital letter psi, U+03A8 ISOgrk3 +'Ω' => 'Ω', # greek capital letter omega, U+03A9 ISOgrk3 +'α' => 'α', # greek small letter alpha, U+03B1 ISOgrk3 +'β' => 'β', # greek small letter beta, U+03B2 ISOgrk3 +'γ' => 'γ', # greek small letter gamma, U+03B3 ISOgrk3 +'δ' => 'δ', # greek small letter delta, U+03B4 ISOgrk3 +'ε' => 'ε', # greek small letter epsilon, U+03B5 ISOgrk3 +'ζ' => 'ζ', # greek small letter zeta, U+03B6 ISOgrk3 +'η' => 'η', # greek small letter eta, U+03B7 ISOgrk3 +'θ' => 'θ', # greek small letter theta, U+03B8 ISOgrk3 +'ι' => 'ι', # greek small letter iota, U+03B9 ISOgrk3 +'κ' => 'κ', # greek small letter kappa, U+03BA ISOgrk3 +'λ' => 'λ', # greek small letter lambda, U+03BB ISOgrk3 +'μ' => 'μ', # greek small letter mu, U+03BC ISOgrk3 +'ν' => 'ν', # greek small letter nu, U+03BD ISOgrk3 +'ξ' => 'ξ', # greek small letter xi, U+03BE ISOgrk3 +'ο' => 'ο', # greek small letter omicron, U+03BF NEW +'π' => 'π', # greek small letter pi, U+03C0 ISOgrk3 +'ρ' => 'ρ', # greek small letter rho, U+03C1 ISOgrk3 +'ς' => 'ς', # greek small letter final sigma, U+03C2 ISOgrk3 +'σ' => 'σ', # greek small letter sigma, U+03C3 ISOgrk3 +'τ' => 'τ', # greek small letter tau, U+03C4 ISOgrk3 +'υ' => 'υ', # greek small letter upsilon, U+03C5 ISOgrk3 +'φ' => 'φ', # greek small letter phi, U+03C6 ISOgrk3 +'χ' => 'χ', # greek small letter chi, U+03C7 ISOgrk3 +'ψ' => 'ψ', # greek small letter psi, U+03C8 ISOgrk3 +'ω' => 'ω', # greek small letter omega, U+03C9 ISOgrk3 +'ϑ' => 'ϑ', # greek small letter theta symbol, U+03D1 NEW +'ϒ' => 'ϒ', # greek upsilon with hook symbol, U+03D2 NEW +'ϖ' => 'ϖ', # greek pi symbol, U+03D6 ISOgrk3 +'•' => '•', # bullet = black small circle, U+2022 ISOpub +'…' => '…', # horizontal ellipsis = three dot leader, U+2026 ISOpub +'′' => '′', # prime = minutes = feet, U+2032 ISOtech +'″' => '″', # double prime = seconds = inches, U+2033 ISOtech +'‾' => '‾', # overline = spacing overscore, U+203E NEW +'⁄' => '⁄', # fraction slash, U+2044 NEW +'℘' => '℘', # script capital P = power set = Weierstrass p, U+2118 ISOamso +'ℑ' => 'ℑ', # blackletter capital I = imaginary part, U+2111 ISOamso +'ℜ' => 'ℜ', # blackletter capital R = real part symbol, U+211C ISOamso +'™' => '™', # trade mark sign, U+2122 ISOnum +'ℵ' => 'ℵ', # alef symbol = first transfinite cardinal, U+2135 NEW +'←' => '←', # leftwards arrow, U+2190 ISOnum +'↑' => '↑', # upwards arrow, U+2191 ISOnum +'→' => '→', # rightwards arrow, U+2192 ISOnum +'↓' => '↓', # downwards arrow, U+2193 ISOnum +'↔' => '↔', # left right arrow, U+2194 ISOamsa +'↵' => '↵', # downwards arrow with corner leftwards = carriage return, U+21B5 NEW +'⇐' => '⇐', # leftwards double arrow, U+21D0 ISOtech +'⇑' => '⇑', # upwards double arrow, U+21D1 ISOamsa +'⇒' => '⇒', # rightwards double arrow, U+21D2 ISOtech +'⇓' => '⇓', # downwards double arrow, U+21D3 ISOamsa +'⇔' => '⇔', # left right double arrow, U+21D4 ISOamsa +'∀' => '∀', # for all, U+2200 ISOtech +'∂' => '∂', # partial differential, U+2202 ISOtech +'∃' => '∃', # there exists, U+2203 ISOtech +'∅' => '∅', # empty set = null set = diameter, U+2205 ISOamso +'∇' => '∇', # nabla = backward difference, U+2207 ISOtech +'∈' => '∈', # element of, U+2208 ISOtech +'∉' => '∉', # not an element of, U+2209 ISOtech +'∋' => '∋', # contains as member, U+220B ISOtech +'∏' => '∏', # n-ary product = product sign, U+220F ISOamsb +'∑' => '∑', # n-ary sumation, U+2211 ISOamsb +'−' => '−', # minus sign, U+2212 ISOtech +'∗' => '∗', # asterisk operator, U+2217 ISOtech +'√' => '√', # square root = radical sign, U+221A ISOtech +'∝' => '∝', # proportional to, U+221D ISOtech +'∞' => '∞', # infinity, U+221E ISOtech +'∠' => '∠', # angle, U+2220 ISOamso +'∧' => '∧', # logical and = wedge, U+2227 ISOtech +'∨' => '∨', # logical or = vee, U+2228 ISOtech +'∩' => '∩', # intersection = cap, U+2229 ISOtech +'∪' => '∪', # union = cup, U+222A ISOtech +'∫' => '∫', # integral, U+222B ISOtech +'∴' => '∴', # therefore, U+2234 ISOtech +'∼' => '∼', # tilde operator = varies with = similar to, U+223C ISOtech +'≅' => '≅', # approximately equal to, U+2245 ISOtech +'≈' => '≈', # almost equal to = asymptotic to, U+2248 ISOamsr +'≠' => '≠', # not equal to, U+2260 ISOtech +'≡' => '≡', # identical to, U+2261 ISOtech +'≤' => '≤', # less-than or equal to, U+2264 ISOtech +'≥' => '≥', # greater-than or equal to, U+2265 ISOtech +'⊂' => '⊂', # subset of, U+2282 ISOtech +'⊃' => '⊃', # superset of, U+2283 ISOtech +'⊄' => '⊄', # not a subset of, U+2284 ISOamsn +'⊆' => '⊆', # subset of or equal to, U+2286 ISOtech +'⊇' => '⊇', # superset of or equal to, U+2287 ISOtech +'⊕' => '⊕', # circled plus = direct sum, U+2295 ISOamsb +'⊗' => '⊗', # circled times = vector product, U+2297 ISOamsb +'⊥' => '⊥', # up tack = orthogonal to = perpendicular, U+22A5 ISOtech +'⋅' => '⋅', # dot operator, U+22C5 ISOamsb +'⌈' => '⌈', # left ceiling = apl upstile, U+2308 ISOamsc +'⌉' => '⌉', # right ceiling, U+2309 ISOamsc +'⌊' => '⌊', # left floor = apl downstile, U+230A ISOamsc +'⌋' => '⌋', # right floor, U+230B ISOamsc +'⟨' => '〈', # left-pointing angle bracket = bra, U+2329 ISOtech +'⟩' => '〉', # right-pointing angle bracket = ket, U+232A ISOtech +'◊' => '◊', # lozenge, U+25CA ISOpub +'♠' => '♠', # black spade suit, U+2660 ISOpub +'♣' => '♣', # black club suit = shamrock, U+2663 ISOpub +'♥' => '♥', # black heart suit = valentine, U+2665 ISOpub +'♦' => '♦', # black diamond suit, U+2666 ISOpub +'"' => '"', # quotation mark = APL quote, U+0022 ISOnum +'&' => '&', # ampersand, U+0026 ISOnum +'<' => '<', # less-than sign, U+003C ISOnum +'>' => '>', # greater-than sign, U+003E ISOnum +'Œ' => 'Œ', # latin capital ligature OE, U+0152 ISOlat2 +'œ' => 'œ', # latin small ligature oe, U+0153 ISOlat2 +'Š' => 'Š', # latin capital letter S with caron, U+0160 ISOlat2 +'š' => 'š', # latin small letter s with caron, U+0161 ISOlat2 +'Ÿ' => 'Ÿ', # latin capital letter Y with diaeresis, U+0178 ISOlat2 +'ˆ' => 'ˆ', # modifier letter circumflex accent, U+02C6 ISOpub +'˜' => '˜', # small tilde, U+02DC ISOdia +' ' => ' ', # en space, U+2002 ISOpub +' ' => ' ', # em space, U+2003 ISOpub +' ' => ' ', # thin space, U+2009 ISOpub +'‌' => '‌', # zero width non-joiner, U+200C NEW RFC 2070 +'‍' => '‍', # zero width joiner, U+200D NEW RFC 2070 +'‎' => '‎', # left-to-right mark, U+200E NEW RFC 2070 +'‏' => '‏', # right-to-left mark, U+200F NEW RFC 2070 +'–' => '–', # en dash, U+2013 ISOpub +'—' => '—', # em dash, U+2014 ISOpub +'‘' => '‘', # left single quotation mark, U+2018 ISOnum +'’' => '’', # right single quotation mark, U+2019 ISOnum +'‚' => '‚', # single low-9 quotation mark, U+201A NEW +'“' => '“', # left double quotation mark, U+201C ISOnum +'”' => '”', # right double quotation mark, U+201D ISOnum +'„' => '„', # double low-9 quotation mark, U+201E NEW +'†' => '†', # dagger, U+2020 ISOpub +'‡' => '‡', # double dagger, U+2021 ISOpub +'‰' => '‰', # per mille sign, U+2030 ISOtech +'‹' => '‹', # single left-pointing angle quotation mark, U+2039 ISO proposed +'›' => '›', # single right-pointing angle quotation mark, U+203A ISO proposed +'€' => '€', # euro sign, U+20AC NEW +''' => ''', # apostrophe = APL quote, U+0027 ISOnum +]; |