]> source.dussan.org Git - nextcloud-server.git/commitdiff
Send images to imaginary docker to generate previews 24166/head
authorVincent Petry <vincent@nextcloud.com>
Mon, 16 Nov 2020 21:16:34 +0000 (22:16 +0100)
committerJulius Härtl <jus@bitgrid.net>
Thu, 17 Mar 2022 07:24:07 +0000 (08:24 +0100)
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Co-Authored-by: Vincent Petry <vincent@nextcloud.com>
config/config.sample.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/Http/Client/Client.php
lib/private/Preview/Generator.php
lib/private/Preview/GeneratorHelper.php
lib/private/Preview/Imaginary.php [new file with mode: 0644]
lib/private/PreviewManager.php
lib/private/StreamImage.php [new file with mode: 0644]
lib/public/IStreamImage.php [new file with mode: 0644]

index f861df2a7ceb2db1ffc4f0f2d572ea55e1294fc5..5bc1cc52cf0106a918b485d6583740238e660921 100644 (file)
@@ -1101,6 +1101,14 @@ $CONFIG = [
        ' --headless --nologo --nofirststartwizard --invisible --norestore '.
        '--convert-to png --outdir ',
 
+/**
+ * Set the URL of the Imaginary service to send image previews to.
+ * Also requires the OC\Preview\Imaginary provider to be enabled.
+ *
+ * See https://github.com/h2non/imaginary
+ */
+'preview_imaginary_url' => 'http://previews_hpb:8088/',
+
 /**
  * Only register providers that have been explicitly enabled
  *
index 81e02389e85fd6d11635f1faa9c1c62dee721ed8..5e136ddecd9dc2433cbf5ea1d1ad7d69f75c1d7c 100644 (file)
@@ -414,6 +414,7 @@ return array(
     'OCP\\ISearch' => $baseDir . '/lib/public/ISearch.php',
     'OCP\\IServerContainer' => $baseDir . '/lib/public/IServerContainer.php',
     'OCP\\ISession' => $baseDir . '/lib/public/ISession.php',
+    'OCP\\IStreamImage' => $baseDir . '/lib/public/IStreamImage.php',
     'OCP\\ITagManager' => $baseDir . '/lib/public/ITagManager.php',
     'OCP\\ITags' => $baseDir . '/lib/public/ITags.php',
     'OCP\\ITempManager' => $baseDir . '/lib/public/ITempManager.php',
@@ -1299,6 +1300,7 @@ return array(
     'OC\\Preview\\HEIC' => $baseDir . '/lib/private/Preview/HEIC.php',
     'OC\\Preview\\Illustrator' => $baseDir . '/lib/private/Preview/Illustrator.php',
     'OC\\Preview\\Image' => $baseDir . '/lib/private/Preview/Image.php',
+    'OC\\Preview\\Imaginary' => $baseDir . '/lib/private/Preview/Imaginary.php',
     'OC\\Preview\\JPEG' => $baseDir . '/lib/private/Preview/JPEG.php',
     'OC\\Preview\\Krita' => $baseDir . '/lib/private/Preview/Krita.php',
     'OC\\Preview\\MP3' => $baseDir . '/lib/private/Preview/MP3.php',
@@ -1455,6 +1457,7 @@ return array(
     'OC\\Share\\Helper' => $baseDir . '/lib/private/Share/Helper.php',
     'OC\\Share\\SearchResultSorter' => $baseDir . '/lib/private/Share/SearchResultSorter.php',
     'OC\\Share\\Share' => $baseDir . '/lib/private/Share/Share.php',
+    'OC\\StreamImage' => $baseDir . '/lib/private/StreamImage.php',
     'OC\\Streamer' => $baseDir . '/lib/private/Streamer.php',
     'OC\\SubAdmin' => $baseDir . '/lib/private/SubAdmin.php',
     'OC\\Support\\CrashReport\\Registry' => $baseDir . '/lib/private/Support/CrashReport/Registry.php',
index abaa64aa3c0f0c87074e8e928879057d520e83d1..c148b10ba455472ae4aa258cbd0f01559cc85f2d 100644 (file)
@@ -443,6 +443,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OCP\\ISearch' => __DIR__ . '/../../..' . '/lib/public/ISearch.php',
         'OCP\\IServerContainer' => __DIR__ . '/../../..' . '/lib/public/IServerContainer.php',
         'OCP\\ISession' => __DIR__ . '/../../..' . '/lib/public/ISession.php',
+        'OCP\\IStreamImage' => __DIR__ . '/../../..' . '/lib/public/IStreamImage.php',
         'OCP\\ITagManager' => __DIR__ . '/../../..' . '/lib/public/ITagManager.php',
         'OCP\\ITags' => __DIR__ . '/../../..' . '/lib/public/ITags.php',
         'OCP\\ITempManager' => __DIR__ . '/../../..' . '/lib/public/ITempManager.php',
@@ -1328,6 +1329,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OC\\Preview\\HEIC' => __DIR__ . '/../../..' . '/lib/private/Preview/HEIC.php',
         'OC\\Preview\\Illustrator' => __DIR__ . '/../../..' . '/lib/private/Preview/Illustrator.php',
         'OC\\Preview\\Image' => __DIR__ . '/../../..' . '/lib/private/Preview/Image.php',
+        'OC\\Preview\\Imaginary' => __DIR__ . '/../../..' . '/lib/private/Preview/Imaginary.php',
         'OC\\Preview\\JPEG' => __DIR__ . '/../../..' . '/lib/private/Preview/JPEG.php',
         'OC\\Preview\\Krita' => __DIR__ . '/../../..' . '/lib/private/Preview/Krita.php',
         'OC\\Preview\\MP3' => __DIR__ . '/../../..' . '/lib/private/Preview/MP3.php',
@@ -1484,6 +1486,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OC\\Share\\Helper' => __DIR__ . '/../../..' . '/lib/private/Share/Helper.php',
         'OC\\Share\\SearchResultSorter' => __DIR__ . '/../../..' . '/lib/private/Share/SearchResultSorter.php',
         'OC\\Share\\Share' => __DIR__ . '/../../..' . '/lib/private/Share/Share.php',
+        'OC\\StreamImage' => __DIR__ . '/../../..' . '/lib/private/StreamImage.php',
         'OC\\Streamer' => __DIR__ . '/../../..' . '/lib/private/Streamer.php',
         'OC\\SubAdmin' => __DIR__ . '/../../..' . '/lib/private/SubAdmin.php',
         'OC\\Support\\CrashReport\\Registry' => __DIR__ . '/../../..' . '/lib/private/Support/CrashReport/Registry.php',
index 55a44eb41ba3c7eea1dbf62e19f66d5f068f061d..673f566e354b31c5e2146982d485df7495afd9ac 100644 (file)
@@ -292,7 +292,8 @@ class Client implements IClient {
                        unset($options['body']);
                }
                $response = $this->client->request('post', $uri, $this->buildRequestOptions($options));
-               return new Response($response);
+               $isStream = isset($options['stream']) && $options['stream'];
+               return new Response($response, $isStream);
        }
 
        /**
index 6e1e0997a68f389a264776c31a3d2d92a0a5af82..a770fe3b459c37317b6307c2b30ea76abf973b9b 100644 (file)
@@ -38,6 +38,7 @@ use OCP\Files\SimpleFS\ISimpleFolder;
 use OCP\IConfig;
 use OCP\IImage;
 use OCP\IPreview;
+use OCP\IStreamImage;
 use OCP\Preview\IProviderV2;
 use OCP\Preview\IVersionedPreviewFile;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -136,6 +137,22 @@ class Generator {
                        $previewVersion = $file->getPreviewVersion() . '-';
                }
 
+               if (count($specifications) === 1
+                       && (($specifications[0]['width'] === 250 && $specifications[0]['height'] === 250)
+                               || ($specifications[0]['width'] === 150 && $specifications[0]['height'] === 150))
+                       && preg_match(Imaginary::supportedMimeTypes(), $mimeType)
+                       && $this->config->getSystemValueString('preview_imaginary_url', 'invalid') !== 'invalid') {
+                       $crop = $specifications[0]['crop'] ?? false;
+                       $preview = $this->getSmallImagePreview($previewFolder, $file, $mimeType, $previewVersion, $crop);
+
+                       if ($preview->getSize() === 0) {
+                               $preview->delete();
+                               throw new NotFoundException('Cached preview size 0, invalid!');
+                       }
+
+                       return $preview;
+               }
+
                // Get the max preview and infer the max preview sizes from that
                $maxPreview = $this->getMaxPreview($previewFolder, $file, $mimeType, $previewVersion);
                $maxPreviewImage = null; // only load the image when we need it
@@ -204,6 +221,65 @@ class Generator {
                return $preview;
        }
 
+       private function getSmallImagePreview(ISimpleFolder $previewFolder, File $file, string $mimeType, string $prefix, bool $crop) {
+               $nodes = $previewFolder->getDirectoryListing();
+
+               foreach ($nodes as $node) {
+                       $name = $node->getName();
+                       if (($prefix === '' || strpos($name, $prefix) === 0)
+                               && (str_starts_with($name, '256-256-crop') && $crop || str_starts_with($name, '256-256') && !$crop)) {
+                               return $node;
+                       }
+               }
+
+               $previewProviders = $this->previewManager->getProviders();
+               foreach ($previewProviders as $supportedMimeType => $providers) {
+                       // Filter out providers that does not support this mime
+                       if (!preg_match($supportedMimeType, $mimeType)) {
+                               continue;
+                       }
+
+                       foreach ($providers as $providerClosure) {
+                               $provider = $this->helper->getProvider($providerClosure);
+                               if (!($provider instanceof IProviderV2)) {
+                                       continue;
+                               }
+
+                               if (!$provider->isAvailable($file)) {
+                                       continue;
+                               }
+
+                               $preview = $this->helper->getThumbnail($provider, $file, 256, 256, true);
+
+                               if (!($preview instanceof IImage)) {
+                                       continue;
+                               }
+
+                               // Try to get the extension.
+                               try {
+                                       $ext = $this->getExtention($preview->dataMimeType());
+                               } catch (\InvalidArgumentException $e) {
+                                       // Just continue to the next iteration if this preview doesn't have a valid mimetype
+                                       continue;
+                               }
+
+                               $path = $this->generatePath(256, 256, $crop, $preview->dataMimeType(), $prefix);
+                               try {
+                                       $file = $previewFolder->newFile($path);
+                                       if ($preview instanceof IStreamImage) {
+                                               $file->putContent($preview->resource());
+                                       } else {
+                                               $file->putContent($preview->data());
+                                       }
+                               } catch (NotPermittedException $e) {
+                                       throw new NotFoundException();
+                               }
+
+                               return $file;
+                       }
+               }
+       }
+
        /**
         * @param ISimpleFolder $previewFolder
         * @param File $file
@@ -259,7 +335,11 @@ class Generator {
                                $path = $prefix . (string)$preview->width() . '-' . (string)$preview->height() . '-max.' . $ext;
                                try {
                                        $file = $previewFolder->newFile($path);
-                                       $file->putContent($preview->data());
+                                       if ($preview instanceof IStreamImage) {
+                                               $file->putContent($preview->resource());
+                                       } else {
+                                               $file->putContent($preview->data());
+                                       }
                                } catch (NotPermittedException $e) {
                                        throw new NotFoundException();
                                }
index a5a9dacf63bb2568db919c8cb93affb536872414..6a94b9482418974dcc442b8b367d099ac1adc875 100644 (file)
@@ -58,8 +58,11 @@ class GeneratorHelper {
         *
         * @return bool|IImage
         */
-       public function getThumbnail(IProviderV2 $provider, File $file, $maxWidth, $maxHeight) {
-               return $provider->getThumbnail($file, $maxWidth, $maxHeight);
+       public function getThumbnail(IProviderV2 $provider, File $file, $maxWidth, $maxHeight, bool $crop = false) {
+               if ($provider instanceof Imaginary) {
+                       return $provider->getCroppedThumbnail($file, $maxWidth, $maxHeight, $crop) ?? false;
+               }
+               return $provider->getThumbnail($file, $maxWidth, $maxHeight) ?? false;
        }
 
        /**
diff --git a/lib/private/Preview/Imaginary.php b/lib/private/Preview/Imaginary.php
new file mode 100644 (file)
index 0000000..7e6ce86
--- /dev/null
@@ -0,0 +1,136 @@
+<?php
+/**
+ * @copyright Copyright (c) 2020, Nextcloud, GmbH.
+ *
+ * @author Vincent Petry <vincent@nextcloud.com>
+ * @author Carl Schwan <carl@carlschwan.eu>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OC\Preview;
+
+use OCP\Files\File;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\IImage;
+
+use OC\StreamImage;
+use Psr\Log\LoggerInterface;
+
+class Imaginary extends ProviderV2 {
+       /** @var IConfig */
+       private $config;
+
+       /** @var IClientService */
+       private $service;
+
+       /** @var LoggerInterface */
+       private $logger;
+
+       public function __construct(array $config) {
+               parent::__construct($config);
+               $this->config = \OC::$server->get(IConfig::class);
+               $this->service = \OC::$server->get(IClientService::class);
+               $this->logger = \OC::$server->get(LoggerInterface::class);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public function getMimeType(): string {
+               return self::supportedMimeTypes();
+       }
+
+       public static function supportedMimeTypes(): string {
+               return '/image\/(bmp|x-bitmap|png|jpeg|gif|heic|svg|webp)/';
+       }
+
+       public function getCroppedThumbnail(File $file, int $maxX, int $maxY, bool $crop): ?IImage {
+               $maxSizeForImages = $this->config->getSystemValue('preview_max_filesize_image', 50);
+
+               $size = $file->getSize();
+
+               if ($maxSizeForImages !== -1 && $size > ($maxSizeForImages * 1024 * 1024)) {
+                       return null;
+               }
+
+               $imaginaryUrl = $this->config->getSystemValueString('preview_imaginary_url', 'invalid');
+               if ($imaginaryUrl === 'invalid') {
+                       $this->logger->error('Imaginary preview provider is enabled, but no url is configured. Please provide the url of your imaginary server to the \'preview_imaginary_url\' config variable.');
+                       return null;
+               }
+               $imaginaryUrl = rtrim($imaginaryUrl, '/');
+
+               // Object store
+               $stream = $file->fopen('r');
+
+               $httpClient = $this->service->newClient();
+
+               switch ($file->getMimeType()) {
+                       case 'image/gif':
+                       case 'image/png':
+                               $mimeType = 'png';
+                               break;
+                       default:
+                               $mimeType = 'jpeg';
+               }
+
+               $parameters = [
+                       'width' => $maxX,
+                       'height' => $maxY,
+                       'stripmeta' => 'true',
+                       'type' => $mimeType,
+               ];
+
+
+               try {
+                       $response = $httpClient->post(
+                               $imaginaryUrl . ($crop ? '/smartcrop' : '/fit'), [
+                                       'query' => $parameters,
+                                       'stream' => true,
+                                       'content-type' => $file->getMimeType(),
+                                       'body' => $stream,
+                                       'nextcloud' => ['allow_local_address' => true],
+                               ]);
+               } catch (\Exception $e) {
+                       $this->logger->error('Imaginary preview generation failed: ' . $e->getMessage(), [
+                               'exception' => $e,
+                       ]);
+                       return null;
+               }
+
+               if ($response->getStatusCode() !== 200) {
+                       $this->logger->error('Imaginary preview generation failed: ' . json_decode($response->getBody())['message']);
+                       return null;
+               }
+
+               if ($response->getHeader('X-Image-Width') && $response->getHeader('X-Image-Height')) {
+                       $maxX = (int)$response->getHeader('X-Image-Width');
+                       $maxY = (int)$response->getHeader('X-Image-Height');
+               }
+
+               $image = new StreamImage($response->getBody(), $response->getHeader('Content-Type'), $maxX, $maxY);
+               return $image->valid() ? $image : null;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
+               return $this->getCroppedThumbnail($file, $maxX, $maxY, false);
+       }
+}
index 18d4427d34670532f88a82ee1a84bf686b9c703e..6c17dd58b4b873aa3650b0b9e9b19db1f24f7fa9 100644 (file)
@@ -387,6 +387,7 @@ class PreviewManager implements IPreview {
                $this->registerCoreProvider(Preview\Krita::class, '/application\/x-krita/');
                $this->registerCoreProvider(Preview\MP3::class, '/audio\/mpeg/');
                $this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/');
+               $this->registerCoreProvider(Preview\Imaginary::class, Preview\Imaginary::supportedMimeTypes());
 
                // SVG, Office and Bitmap require imagick
                if (extension_loaded('imagick')) {
diff --git a/lib/private/StreamImage.php b/lib/private/StreamImage.php
new file mode 100644 (file)
index 0000000..38fd114
--- /dev/null
@@ -0,0 +1,152 @@
+<?php
+/**
+ * @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu>
+ *
+ * @author Carl Schwan <carl@carlschwan.eu>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC;
+
+use OCP\IStreamImage;
+use OCP\IImage;
+
+/**
+ * Only useful when dealing with transferring streamed previews from an external
+ * service to an object store.
+ *
+ * Only width/height/resource and mimeType are implemented and will give you a
+ * valid result.
+ */
+class StreamImage implements IStreamImage {
+       /** @var resource The internal stream */
+       private $stream;
+
+       /** @var string */
+       private $mimeType;
+
+       /** @var int */
+       private $width;
+
+       /** @var int */
+       private $height;
+
+       /** @param resource $stream */
+       public function __construct($stream, string $mimeType, int $width, int $height) {
+               $this->stream = $stream;
+               $this->mimeType = $mimeType;
+               $this->width = $width;
+               $this->height = $height;
+       }
+
+       /** @inheritDoc */
+       public function valid() {
+               return is_resource($this->stream);
+       }
+
+       /** @inheritDoc */
+       public function mimeType() {
+               return $this->mimeType;
+       }
+
+       /** @inheritDoc */
+       public function width() {
+               return $this->width;
+       }
+
+       /** @inheritDoc */
+       public function height() {
+               return $this->height;
+       }
+
+       public function widthTopLeft() {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function heightTopLeft() {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function show($mimeType = null) {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function save($filePath = null, $mimeType = null) {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function resource() {
+               return $this->stream;
+       }
+
+       public function dataMimeType() {
+               return $this->mimeType;
+       }
+
+       public function data() {
+               return '';
+       }
+
+       public function getOrientation() {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function fixOrientation() {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function resize($maxSize) {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function preciseResize(int $width, int $height): bool {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function centerCrop($size = 0) {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function crop(int $x, int $y, int $w, int $h): bool {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function fitIn($maxWidth, $maxHeight) {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function scaleDownToFit($maxWidth, $maxHeight) {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function copy(): IImage {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function cropCopy(int $x, int $y, int $w, int $h): IImage {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function preciseResizeCopy(int $width, int $height): IImage {
+               throw new \BadMethodCallException('Not implemented');
+       }
+
+       public function resizeCopy(int $maxSize): IImage {
+               throw new \BadMethodCallException('Not implemented');
+       }
+}
diff --git a/lib/public/IStreamImage.php b/lib/public/IStreamImage.php
new file mode 100644 (file)
index 0000000..a93381d
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu>
+ *
+ * @author Carl Schwan <carl@carlschwan.eu>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP;
+
+/**
+ * @since 24.0.0
+ */
+interface IStreamImage extends IImage {
+}