diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index eff7421..285cc08 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -8,6 +8,7 @@
use OCA\Epubviewer\Listener\BeforeTemplateRenderedListener;
use OCA\Epubviewer\Listener\FilesLoadAdditionalScriptsListener;
use OCA\Epubviewer\Listener\PublicShareBeforeTemplateRenderedListener;
+use OCA\Epubviewer\Preview\EPubPreview;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCP\AppFramework\App;
@@ -27,6 +28,8 @@ public function __construct() {
public function register(IRegistrationContext $context): void {
include_once __DIR__ . '/../../vendor/autoload.php';
+ $this->registerProvider($context);
+
// “Emitted before the rendering step of each TemplateResponse. The event holds a flag that specifies if a user is logged in.”
// See: https://docs.nextcloud.com/server/latest/developer_manual/basics/events.html#oca-settings-events-beforetemplaterenderedevent
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
@@ -44,6 +47,10 @@ public function register(IRegistrationContext $context): void {
// Hooks::register();
}
+ private function registerProvider(IRegistrationContext $context): void {
+ $context->registerPreviewProvider(EPubPreview::class, '/^application\/epub\+zip$/');
+ }
+
public function boot(IBootContext $context): void {
}
}
diff --git a/lib/Preview/EPubPreview.php b/lib/Preview/EPubPreview.php
new file mode 100644
index 0000000..73ab740
--- /dev/null
+++ b/lib/Preview/EPubPreview.php
@@ -0,0 +1,248 @@
+
+ *
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\Epubviewer\Preview;
+
+//.epub
+use OC\Archive\ZIP;
+use OCP\Files\File;
+use OCP\Files\FileInfo;
+use OCP\IImage;
+use OCP\Preview\IProviderV2;
+use OCP\ITempManager;
+
+class EPubPreview implements IProviderV2 {
+ private ?ZIP $zip = null;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getMimeType(): string {
+ return '/application\/epub\+zip/';
+ }
+
+ /**
+ * Check if a preview can be generated for $path
+ *
+ * {@inheritDoc}
+ */
+ public function isAvailable(FileInfo $file): bool {
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
+ $image = $this->extractThumbnail($file, '');
+ if ($image && $image->valid()) {
+ return $image;
+ }
+ return null;
+ }
+
+ /**
+ * extractThumbnail from complicated epub format
+ */
+ private function extractThumbnail(File $file, string $path): ?IImage {
+ $tmpManager = \OC::$server->get(ITempManager::class);
+ $sourceTmp = $tmpManager->getTemporaryFile();
+
+ try {
+ $content = $file->fopen('r');
+ file_put_contents($sourceTmp, $content);
+
+ $this->zip = new ZIP($sourceTmp);
+
+ $img_data = null;
+ $contentPath = $this->getContentPath();
+ if ($contentPath) {
+ $package = $this->extractXML($contentPath);
+ if ($package) {
+ $path = $contentPath;
+ $img_src = $cover = null;
+ // Try first through
+ $items = $package->manifest->children();
+ foreach($items as $item) {
+ if (($item['id'] == 'cover' || $item['id'] == 'cover-image') && preg_match('/image\//', (string) $item['media-type'])) {
+ $img_src = (string) $item['href'];
+ break;
+ }
+ }
+
+ // in references
+ if (!$img_src) {
+ $references = $package->guide->children();
+ foreach($references as $reference) {
+ if ($reference['type'] == 'cover' || $reference['type'] == 'title-page') {
+ $cover = (string) $reference['href'];
+ break;
+ }
+ }
+ }
+
+ // no cover ? no image ? take the first page
+ if (!$img_src && !$cover) {
+ $first_page_id = (string) $package->spine->itemref['idref'];
+ if ($first_page_id) {
+ foreach($items as $item) {
+ if ($item['id'] == $first_page_id) {
+ $cover = (string) $item['href'];
+ break;
+ }
+ }
+
+ }
+ }
+
+ // have we a "cover" file ?
+ if ($cover) {
+ // relative to container
+ $img_src = null;
+ $path = $this->resolvePath($path, $cover);
+ $dom = $this->extractHTML($path);
+ if ($dom) {
+ // search img
+ $images = $dom->getElementsByTagName('img');
+ if ($images->length) {
+ $img_src = $images[0]->getAttribute('src');
+ } else {
+ $images = $dom->getElementsByTagName('image');
+ if ($images->length) {
+ $img_src = $images[0]->getAttribute('xlink:href');
+ }
+ }
+ }
+ }// cover
+
+ // img ?
+ if ($img_src) {
+ $img_src = $this->resolvePath($path, $img_src);
+ $img_data = $this->extractFileData($img_src);
+ }
+ }
+ }
+
+ // Pfff. Make a pause
+ if ($img_data) {
+ $image = new \OC_Image();
+ $image->loadFromData($img_data);
+ return $image;
+ }
+ return null;
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+
+ /**
+ * find the main content XML (usually "content.opf")
+ */
+ private function getContentPath() : ?string {
+ $xml_container = $this->extractXML('META-INF/container.xml');
+ if (is_object($xml_container)) {
+ $full_path = $xml_container->rootfiles->rootfile['full-path'][0];
+ if ($full_path) {
+ return $full_path->__toString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * extract HTML from Zip path
+ * @param string $path
+ * @return \DOMDocument|null
+ */
+ protected function extractHTML(string $path): \DOMDocument|null {
+ $html = $this->extractFileData($path);
+ if (is_string($html)) {
+ $dom = new \DOMDocument('1.0', 'utf-8');
+ $dom->strictErrorChecking = false;
+ if (@$dom->loadHTML($html)) {
+ return $dom;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * extract XML from Zip path
+ *
+ * @psalm-param 'META-INF/container.xml' $path
+ */
+ private function extractXML(string $path): \SimpleXMLElement|false|null {
+ $xml = $this->extractFileData($path);
+ if (is_string($xml)) {
+ return simplexml_load_string($xml);
+ }
+ return null;
+ }
+
+ /**
+ * get unzipped data
+ *
+ * @param string $path file path in zip
+ *
+ * @psalm-param 'META-INF/container.xml' $path
+ *
+ * @return false|null|string
+ */
+ private function extractFileData(string $path): string|false|null {
+ if ($this->zip === null) {
+ return null;
+ }
+ $fp = $this->zip->getStream($path, 'r');
+ if ($fp) {
+ $content = stream_get_contents($fp);
+ fclose($fp);
+ return $content;
+ }
+ return null;
+ }
+
+ /**
+ * Resolve relative $relPath from $path (removes ./, ../)
+ *
+ * @param string $path reference path
+ * @param string $relPath relative path
+ * @return string
+ */
+ private function resolvePath(string $path, string $relPath): string {
+ $path = dirname($path).'/'.$relPath;
+ $pieces = explode('/', $path);
+ $parents = [];
+ foreach($pieces as $dir) {
+ switch($dir) {
+ case '.':
+ // Don't need to do anything here
+ break;
+ case '..':
+ array_pop($parents);
+ break;
+ default:
+ $parents[] = $dir;
+ break;
+ }
+ }
+ return implode('/', $parents);
+ }
+}