/ src / library / processors / EpubProcessor.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Vijeth Patil vijeth.patil@gmail.com
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\library\processors;

use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\UrlParser;
use seekquarry\yioop\library\PartialZipArchive;

/**
 * Used to create crawl summary information
 * for XML files (those served as application/epub+zip)
 *
 * @author Vijeth Patil
 */
class EpubProcessor extends TextProcessor
{
    /**
     * The constant represents the number of
     * child levels at which the data is present in
     * the content.opf file.
     */
    const MAX_DOM_LEVEL = 15;
    /**
     * Set-ups the any indexing plugins associated with this page
     * processor
     *
     * @param array $plugins an array of indexing plugins which might
     *     do further processing on the data handles by this page
     *     processor
     * @param int $max_description_len maximal length of a page summary
     * @param int $max_links_to_extract maximum number of links to extract
     *      from a single document
     * @param string $summarizer_option CRAWL_CONSTANT specifying what kind
     *      of summarizer to use self::BASIC_SUMMARIZER,
     *      self::GRAPH_BASED_SUMMARIZER and self::CENTROID_SUMMARIZER
     *      self::CENTROID_SUMMARIZER
     */
    public function __construct($plugins = [], $max_description_len = null,
        $max_links_to_extract = null,
        $summarizer_option = self::BASIC_SUMMARIZER)
    {
        parent::__construct($plugins, $max_description_len,
            $max_links_to_extract, $summarizer_option);
        /** Register File Types We Handle*/
        self::$indexed_file_types[] = "epub";
        self::$mime_processor["application/epub+zip"] = "EpubProcessor";
    }
    /**
     * The name of the tag element in an xml document
     *
     * @var string name
     */
    public $name;
    /**
     * The attribute of the tag element in an xml document
     *
     * @var string attributes
     */
    public $attributes;
    /**
     * The content of the tag element or attribute, used to extract
     * the fields like title, creator, language of the document
     *
     * @var string content
     */
    public $content;
    /**
     * The child tag element of a tag element.
     *
     * @var string children
     */
    public $children;
    /**
     * Used to extract the title, description and links from
     * a string consisting of ebook publication data.
     *
     * @param string $page epub contents
     * @param string $url the url where the page contents came from,
     *    used to canonicalize relative links
     *
     * @return array  a summary of the contents of the page
     *
     */
    public function process($page, $url)
    {
        $summary = null;
        $opf_pattern = "/.opf$/i";
        $html_pattern  = "/.html$/i";
        $xhtml_pattern = "/.xhtml$/i";
        $epub_url[0] = '';
        $epub_language = '';
        $epub_title = '';
        $epub_unique_identifier = '';
        $epub_author = '';
        $epub_publisher = '';
        $epub_date = '';
        $epub_subject = '';
        $desc = '';
        $htmlcontent = '';
        // Open a zip archive
        $zip = new PartialZipArchive($page);
        $num_files = $zip->numFiles();
        for ($i = 0; $i < $num_files; $i++) {
            // get the content file names of .epub document
            $filename[$i] = $zip->getNameIndex($i);
            if (preg_match($opf_pattern, $filename[$i])) {
                // Get the file data from zipped folder
                $opf_data = $zip->getFromName($filename[$i]);
                $opf_summary = $this->xmlToObject($opf_data);
                for ($m = 0; $m <= self::MAX_DOM_LEVEL; $m++) {
                    for ($n = 0; $n <= self::MAX_DOM_LEVEL; $n++) {
                        if (isset($opf_summary->children[$m]->children[$n])){
                            $child = $opf_summary->children[$m]->
                                children[$n];
                            if ( isset($child->name) &&
                                $child->name == "dc:language") {
                                $epub_language =
                                    $opf_summary->children[$m]->
                                        children[$n]->content ;
                            }
                            if ( ($opf_summary->children[$m]->children[$n]->
                                name) == "dc:title") {
                                $epub_title = $opf_summary->children[$m]->
                                    children[$n]->content;
                            }
                            if ( ($opf_summary->children[$m]->children[$n]->
                                name) == "dc:creator") {
                                $epub_author = $opf_summary->children[$m]->
                                    children[$n]->content ;
                            }
                            if ( ($opf_summary->children[$m]->children[$n]->
                                name) == "dc:identifier") {
                                $epub_unique_identifier = $opf_summary->
                                    children[$m]->children[$n]->content ;
                            }
                        }
                    }
                }
            } else if ((preg_match($html_pattern, $filename[$i])) ||
                (preg_match($xhtml_pattern, $filename[$i]))) {
                $html = new HtmlProcessor();
                $html_data = $zip->getFromName($filename[$i]);
                $description[$i] = $html->process($html_data, $url);
                $htmlcontent.= $description[$i]['t'];
            }
        }
        if ($epub_title != '') {
            $desc= " $epub_title .";
        }
        if ($epub_author != '') {
            $desc = $desc." $epub_author ";
        }
        if ($epub_language != '') {
            $desc = $desc." $epub_language ";
        }
        if ($epub_unique_identifier != '') {
            $desc = $desc." URN-".
            $epub_unique_identifier.".";
        }
        if ($epub_publisher != '') {
            $desc = $desc." $epub_publisher ";
        }
        if ($epub_date != '') {
            $desc = $desc." $epub_date ";
        }
        if ($epub_subject != '') {
            $desc = $desc." $epub_subject ";
        }
        $desc= $desc.$htmlcontent;
        //restrict the length of the description to maximum description length
        if (strlen($desc) > self::$max_description_len) {
            $desc = substr($desc, 0, self::$max_description_len);
        }
        $summary[self::TITLE] = $epub_title;
        $summary[self::DESCRIPTION] = $desc;
        $summary[self::LANG] = $epub_language;
        $summary[self::LINKS] = $epub_url;
        $summary[self::PAGE] = $page;
        return $summary;
    }
    /**
     * Used to extract the DOM tree containing the information
     * about the epub file such as title, author, language, unique
     * identifier of the book from a string consisting of ebook publication
     * content OPF file.
     *
     * @param string $xml page contents
     *
     * @return array an information about the contents of the page
     *
     */
    public function xmlToObject($xml)
    {
        $parser = xml_parser_create();
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
        xml_parse_into_struct($parser, $xml, $tags);
        $elements = [];  // the currently filling [child] XmlElement array
        $stack = [];
        foreach ($tags as $tag) {
            $index = count($elements);
            if ($tag['type'] == "complete" || $tag['type'] == "open") {
                $elements[$index] = new EpubProcessor;
                $elements[$index]->name = $tag['tag'];
                if (isset($tag['attributes'])) {
                    $elements[$index]->attributes = $tag['attributes'];
                }
                if (isset($tag['value'])) {
                    $elements[$index]->content = $tag['value'];
                }
                if ($tag['type'] == "open") {  // push
                    $elements[$index]->children = [];
                    $stack[count($stack)] = &$elements;
                    $elements = &$elements[$index]->children;
                }
            }
            if ($tag['type'] == "close") {  // pop
                $elements = &$stack[count($stack) - 1];
                unset($stack[count($stack) - 1]);
            }
        }
        return $elements[0];  // the single top-level element
    }
    /**
     * Used to create a thumbnail file in a thumb folder from a book. The
     * cover is a picture already inside the book, which names it in its
     * own list of contents, so it is taken out and scaled; a book naming
     * none falls back to having its opening words drawn.
     *
     * @param string $folder the folder the book is in
     * @param string $thumb_folder the folder to write the thumbnail to
     * @param string $file_name the book's file name
     * @param int $width the most the thumbnail may be across
     * @param int $height the most the thumbnail may be down
     */
    public static function createThumb($folder, $thumb_folder, $file_name,
        $width = C\THUMB_DIM, $height = C\THUMB_DIM)
    {
        if (!function_exists("imagecreatetruecolor")) {
            return;
        }
        $thumb_path = "$thumb_folder/$file_name.jpg";
        if (file_exists($thumb_path)) {
            @unlink($thumb_path);
        }
        $cover = self::coverInBook("$folder/$file_name");
        if ($cover !== false && self::scaleTo($cover, $thumb_path, $width,
            $height, "jpeg")) {
            clearstatcache(true, $thumb_path);
            return;
        }
        parent::createThumb($folder, $thumb_folder, $file_name, $width,
            $height);
    }
    /**
     * Finds the cover picture inside a book. The book is an archive, and
     * its list of contents names which file the cover is, so the picture
     * is already there to be taken out; turning the book into a portable
     * document first, as Yioop used to, only reached the same picture the
     * long way round.
     *
     * @param string $path the book's path on disk
     * @return string|bool the cover's bytes, or false if there is none
     */
    public static function coverInBook($path)
    {
        if (!class_exists("ZipArchive")) {
            return false;
        }
        $book = new \ZipArchive();
        if ($book->open($path) !== true) {
            return false;
        }
        $named = self::coverNameInBook($book);
        $picture = false;
        if ($named !== false) {
            $found = $book->getFromName($named);
            if ($found !== false && $found !== "") {
                $picture = $found;
            }
        }
        if ($picture === false) {
            /* No list of contents to go by, so the first picture in the
               archive is taken, which for a book is its cover. */
            for ($index = 0; $index < $book->numFiles; $index++) {
                $inside = $book->getNameIndex($index);
                if (preg_match('/\.(jpe?g|png|gif|webp)$/i', $inside)) {
                    $picture = $book->getFromIndex($index);
                    break;
                }
            }
        }
        $book->close();
        return $picture;
    }
    /**
     * Reads a book's list of contents for the name of its cover picture.
     *
     * @param object $book the opened archive
     * @return string|bool the cover's name inside the archive, or false
     */
    public static function coverNameInBook($book)
    {
        $where = false;
        for ($index = 0; $index < $book->numFiles; $index++) {
            $inside = $book->getNameIndex($index);
            if (substr($inside, -5) === ".opf") {
                $where = $inside;
                break;
            }
        }
        if ($where === false) {
            return false;
        }
        $contents = $book->getFromName($where);
        if ($contents === false) {
            return false;
        }
        $folder = (strpos($where, "/") === false) ? "" :
            substr($where, 0, strrpos($where, "/") + 1);
        $named = false;
        if (preg_match('/<meta[^>]*name="cover"[^>]*content="([^"]+)"/i',
            $contents, $found)) {
            $named = $found[1];
        }
        if ($named !== false && preg_match('/<item[^>]*id="' .
            preg_quote($named, "/") . '"[^>]*href="([^"]+)"/i', $contents,
            $found)) {
            return $folder . $found[1];
        }
        if (preg_match('/<item[^>]*properties="[^"]*cover-image[^"]*"' .
            '[^>]*href="([^"]+)"/i', $contents, $found)) {
            return $folder . $found[1];
        }
        if (preg_match('/<item[^>]*href="([^"]*cover[^"]*\.(?:jpe?g|png))"/i',
            $contents, $found)) {
            return $folder . $found[1];
        }
        return false;
    }
}
X