diff options
Diffstat (limited to 'vendor/mikespub/php-epub-meta/src/EPub.php')
-rw-r--r-- | vendor/mikespub/php-epub-meta/src/EPub.php | 2126 |
1 files changed, 0 insertions, 2126 deletions
diff --git a/vendor/mikespub/php-epub-meta/src/EPub.php b/vendor/mikespub/php-epub-meta/src/EPub.php deleted file mode 100644 index 171fd7641..000000000 --- a/vendor/mikespub/php-epub-meta/src/EPub.php +++ /dev/null @@ -1,2126 +0,0 @@ -<?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); - } -} |