summaryrefslogtreecommitdiffstats
path: root/lib/private
diff options
context:
space:
mode:
authorLukas Reschke <lukas@statuscode.ch>2016-11-03 21:07:16 +0100
committerGitHub <noreply@github.com>2016-11-03 21:07:16 +0100
commitc0bbae28f0bb1041a22645691fe653c642f0d1cb (patch)
treec75e0f48a2c0c44af5eca2062592855d044f1f10 /lib/private
parent8bf57462ea52cc2f4300b6fa097ae23100882c83 (diff)
parentf44cd7aed37d84137902ce8f65aabd115ae1adef (diff)
downloadnextcloud-server-c0bbae28f0bb1041a22645691fe653c642f0d1cb.tar.gz
nextcloud-server-c0bbae28f0bb1041a22645691fe653c642f0d1cb.zip
Merge pull request #1741 from nextcloud/new_preview
Improve previews
Diffstat (limited to 'lib/private')
-rw-r--r--lib/private/Preview.php2
-rw-r--r--lib/private/Preview/Generator.php338
-rw-r--r--lib/private/Preview/GeneratorHelper.php91
-rw-r--r--lib/private/Preview/Watcher.php98
-rw-r--r--lib/private/Preview/WatcherConnector.php72
-rw-r--r--lib/private/PreviewManager.php56
-rw-r--r--lib/private/Server.php16
7 files changed, 669 insertions, 4 deletions
diff --git a/lib/private/Preview.php b/lib/private/Preview.php
index ccaec738caf..caa1e89bacc 100644
--- a/lib/private/Preview.php
+++ b/lib/private/Preview.php
@@ -125,7 +125,7 @@ class Preview {
$sysConfig = \OC::$server->getConfig();
$this->configMaxWidth = $sysConfig->getSystemValue('preview_max_x', 2048);
$this->configMaxHeight = $sysConfig->getSystemValue('preview_max_y', 2048);
- $this->maxScaleFactor = $sysConfig->getSystemValue('preview_max_scale_factor', 2);
+ $this->maxScaleFactor = $sysConfig->getSystemValue('preview_max_scale_factor', 1);
//save parameters
$this->setFile($file);
diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php
new file mode 100644
index 00000000000..3d4e9bf3677
--- /dev/null
+++ b/lib/private/Preview/Generator.php
@@ -0,0 +1,338 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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\Preview;
+
+use OCP\Files\File;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\IConfig;
+use OCP\IImage;
+use OCP\IPreview;
+use OCP\Preview\IProvider;
+
+class Generator {
+
+ /** @var IPreview */
+ private $previewManager;
+ /** @var IConfig */
+ private $config;
+ /** @var IAppData */
+ private $appData;
+ /** @var GeneratorHelper */
+ private $helper;
+
+ /**
+ * @param IConfig $config
+ * @param IPreview $previewManager
+ * @param IAppData $appData
+ * @param GeneratorHelper $helper
+ */
+ public function __construct(
+ IConfig $config,
+ IPreview $previewManager,
+ IAppData $appData,
+ GeneratorHelper $helper
+ ) {
+ $this->config = $config;
+ $this->previewManager = $previewManager;
+ $this->appData = $appData;
+ $this->helper = $helper;
+ }
+
+ /**
+ * Returns a preview of a file
+ *
+ * The cache is searched first and if nothing usable was found then a preview is
+ * generated by one of the providers
+ *
+ * @param File $file
+ * @param int $width
+ * @param int $height
+ * @param bool $crop
+ * @param string $mode
+ * @param string $mimeType
+ * @return ISimpleFile
+ * @throws NotFoundException
+ */
+ public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) {
+ if ($mimeType === null) {
+ $mimeType = $file->getMimeType();
+ }
+ if (!$this->previewManager->isMimeSupported($mimeType)) {
+ throw new NotFoundException();
+ }
+
+ $previewFolder = $this->getPreviewFolder($file);
+
+ // Get the max preview and infer the max preview sizes from that
+ $maxPreview = $this->getMaxPreview($previewFolder, $file, $mimeType);
+ list($maxWidth, $maxHeight) = $this->getPreviewSize($maxPreview);
+
+ // Calculate the preview size
+ list($width, $height) = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
+
+ // Try to get a cached preview. Else generate (and store) one
+ try {
+ $file = $this->getCachedPreview($previewFolder, $width, $height, $crop);
+ } catch (NotFoundException $e) {
+ $file = $this->generatePreview($previewFolder, $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight);
+ }
+
+ return $file;
+ }
+
+ /**
+ * @param ISimpleFolder $previewFolder
+ * @param File $file
+ * @param string $mimeType
+ * @return ISimpleFile
+ * @throws NotFoundException
+ */
+ private function getMaxPreview(ISimpleFolder $previewFolder, File $file, $mimeType) {
+ $nodes = $previewFolder->getDirectoryListing();
+
+ foreach ($nodes as $node) {
+ if (strpos($node->getName(), 'max')) {
+ return $node;
+ }
+ }
+
+ $previewProviders = $this->previewManager->getProviders();
+ foreach ($previewProviders as $supportedMimeType => $providers) {
+ if (!preg_match($supportedMimeType, $mimeType)) {
+ continue;
+ }
+
+ foreach ($providers as $provider) {
+ $provider = $this->helper->getProvider($provider);
+ if (!($provider instanceof IProvider)) {
+ continue;
+ }
+
+ $maxWidth = (int)$this->config->getSystemValue('preview_max_x', 2048);
+ $maxHeight = (int)$this->config->getSystemValue('preview_max_y', 2048);
+
+ $preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight);
+
+ if (!($preview instanceof IImage)) {
+ continue;
+ }
+
+ $path = strval($preview->width()) . '-' . strval($preview->height()) . '-max.png';
+ $file = $previewFolder->newFile($path);
+ $file->putContent($preview->data());
+
+ return $file;
+ }
+ }
+
+ throw new NotFoundException();
+ }
+
+ /**
+ * @param ISimpleFile $file
+ * @return int[]
+ */
+ private function getPreviewSize(ISimpleFile $file) {
+ $size = explode('-', $file->getName());
+ return [(int)$size[0], (int)$size[1]];
+ }
+
+ /**
+ * @param int $width
+ * @param int $height
+ * @param bool $crop
+ * @return string
+ */
+ private function generatePath($width, $height, $crop) {
+ $path = strval($width) . '-' . strval($height);
+ if ($crop) {
+ $path .= '-crop';
+ }
+ $path .= '.png';
+ return $path;
+ }
+
+
+
+ /**
+ * @param int $width
+ * @param int $height
+ * @param bool $crop
+ * @param string $mode
+ * @param int $maxWidth
+ * @param int $maxHeight
+ * @return int[]
+ */
+ private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) {
+
+ /*
+ * If we are not cropping we have to make sure the requested image
+ * respects the aspect ratio of the original.
+ */
+ if (!$crop) {
+ $ratio = $maxHeight / $maxWidth;
+
+ if ($width === -1) {
+ $width = $height / $ratio;
+ }
+ if ($height === -1) {
+ $height = $width * $ratio;
+ }
+
+ $ratioH = $height / $maxHeight;
+ $ratioW = $width / $maxWidth;
+
+ /*
+ * Fill means that the $height and $width are the max
+ * Cover means min.
+ */
+ if ($mode === IPreview::MODE_FILL) {
+ if ($ratioH > $ratioW) {
+ $height = $width * $ratio;
+ } else {
+ $width = $height / $ratio;
+ }
+ } else if ($mode === IPreview::MODE_COVER) {
+ if ($ratioH > $ratioW) {
+ $width = $height / $ratio;
+ } else {
+ $height = $width * $ratio;
+ }
+ }
+ }
+
+ if ($height !== $maxHeight && $width !== $maxWidth) {
+ /*
+ * Scale to the nearest power of two
+ */
+ $pow2height = pow(2, ceil(log($height) / log(2)));
+ $pow2width = pow(2, ceil(log($width) / log(2)));
+
+ $ratioH = $height / $pow2height;
+ $ratioW = $width / $pow2width;
+
+ if ($ratioH < $ratioW) {
+ $width = $pow2width;
+ $height = $height / $ratioW;
+ } else {
+ $height = $pow2height;
+ $width = $width / $ratioH;
+ }
+ }
+
+ /*
+ * Make sure the requested height and width fall within the max
+ * of the preview.
+ */
+ if ($height > $maxHeight) {
+ $ratio = $height / $maxHeight;
+ $height = $maxHeight;
+ $width = $width / $ratio;
+ }
+ if ($width > $maxWidth) {
+ $ratio = $width / $maxWidth;
+ $width = $maxWidth;
+ $height = $height / $ratio;
+ }
+
+ return [(int)round($width), (int)round($height)];
+ }
+
+ /**
+ * @param ISimpleFolder $previewFolder
+ * @param ISimpleFile $maxPreview
+ * @param int $width
+ * @param int $height
+ * @param bool $crop
+ * @param int $maxWidth
+ * @param int $maxHeight
+ * @return ISimpleFile
+ * @throws NotFoundException
+ */
+ private function generatePreview(ISimpleFolder $previewFolder, ISimpleFile $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight) {
+ $preview = $this->helper->getImage($maxPreview);
+
+ if ($crop) {
+ if ($height !== $preview->height() && $width !== $preview->width()) {
+ //Resize
+ $widthR = $preview->width() / $width;
+ $heightR = $preview->height() / $height;
+
+ if ($widthR > $heightR) {
+ $scaleH = $height;
+ $scaleW = $maxWidth / $heightR;
+ } else {
+ $scaleH = $maxHeight / $widthR;
+ $scaleW = $width;
+ }
+ $preview->preciseResize(round($scaleW), round($scaleH));
+ }
+ $cropX = floor(abs($width - $preview->width()) * 0.5);
+ $cropY = 0;
+ $preview->crop($cropX, $cropY, $width, $height);
+ } else {
+ $preview->resize(max($width, $height));
+ }
+
+ $path = $this->generatePath($width, $height, $crop);
+ $file = $previewFolder->newFile($path);
+ $file->putContent($preview->data());
+
+ return $file;
+ }
+
+ /**
+ * @param ISimpleFolder $previewFolder
+ * @param int $width
+ * @param int $height
+ * @param bool $crop
+ * @return ISimpleFile
+ *
+ * @throws NotFoundException
+ */
+ private function getCachedPreview(ISimpleFolder $previewFolder, $width, $height, $crop) {
+ $path = $this->generatePath($width, $height, $crop);
+
+ return $previewFolder->getFile($path);
+ }
+
+ /**
+ * Get the specific preview folder for this file
+ *
+ * @param File $file
+ * @return ISimpleFolder
+ */
+ private function getPreviewFolder(File $file) {
+ try {
+ $folder = $this->appData->getFolder($file->getId());
+ } catch (NotFoundException $e) {
+ $folder = $this->appData->newFolder($file->getId());
+ }
+
+ return $folder;
+ }
+}
diff --git a/lib/private/Preview/GeneratorHelper.php b/lib/private/Preview/GeneratorHelper.php
new file mode 100644
index 00000000000..282c46a2a5d
--- /dev/null
+++ b/lib/private/Preview/GeneratorHelper.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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\Preview;
+
+use OC\Files\View;
+use OCP\Files\File;
+use OCP\Files\IRootFolder;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\IImage;
+use OCP\Image as img;
+use OCP\Preview\IProvider;
+
+/**
+ * Very small wrapper class to make the generator fully unit testable
+ */
+class GeneratorHelper {
+
+ /** @var IRootFolder */
+ private $rootFolder;
+
+ public function __construct(IRootFolder $rootFolder) {
+ $this->rootFolder = $rootFolder;
+ }
+
+ /**
+ * @param IProvider $provider
+ * @param File $file
+ * @param int $maxWidth
+ * @param int $maxHeight
+ * @return bool|IImage
+ */
+ public function getThumbnail(IProvider $provider, File $file, $maxWidth, $maxHeight) {
+ list($view, $path) = $this->getViewAndPath($file);
+ return $provider->getThumbnail($path, $maxWidth, $maxHeight, false, $view);
+ }
+
+ /**
+ * @param File $file
+ * @return array
+ * This is required to create the old view and path
+ */
+ private function getViewAndPath(File $file) {
+ $owner = $file->getOwner()->getUID();
+
+ $userFolder = $this->rootFolder->getUserFolder($owner)->getParent();
+
+ $nodes = $userFolder->getById($file->getId());
+ $file = $nodes[0];
+
+ $view = new View($userFolder->getPath());
+ $path = $userFolder->getRelativePath($file->getPath());
+
+ return [$view, $path];
+ }
+
+ /**
+ * @param ISimpleFile $maxPreview
+ * @return IImage
+ */
+ public function getImage(ISimpleFile $maxPreview) {
+ return new img($maxPreview->getContent());
+ }
+
+ /**
+ * @param $provider
+ * @return IProvider
+ */
+ public function getProvider($provider) {
+ return $provider();
+ }
+}
diff --git a/lib/private/Preview/Watcher.php b/lib/private/Preview/Watcher.php
new file mode 100644
index 00000000000..0b87bcda86e
--- /dev/null
+++ b/lib/private/Preview/Watcher.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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\Preview;
+
+use OCP\Files\File;
+use OCP\Files\Node;
+use OCP\Files\Folder;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+
+/**
+ * Class Watcher
+ *
+ * @package OC\Preview
+ *
+ * Class that will watch filesystem activity and remove previews as needed.
+ */
+class Watcher {
+ /** @var IAppData */
+ private $appData;
+
+ /** @var int[] */
+ private $toDelete = [];
+
+ /**
+ * Watcher constructor.
+ *
+ * @param IAppData $appData
+ */
+ public function __construct(IAppData $appData) {
+ $this->appData = $appData;
+ }
+
+ public function postWrite(Node $node) {
+ // We only handle files
+ if ($node instanceof Folder) {
+ return;
+ }
+
+ try {
+ $folder = $this->appData->getFolder($node->getId());
+ $folder->delete();
+ } catch (NotFoundException $e) {
+ //Nothing to do
+ }
+ }
+
+ public function preDelete(Node $node) {
+ // To avoid cycles
+ if ($this->toDelete !== []) {
+ return;
+ }
+
+ if ($node instanceof File) {
+ $this->toDelete[] = $node->getId();
+ return;
+ }
+
+ /** @var Folder $node */
+ $nodes = $node->search('');
+ foreach ($nodes as $node) {
+ if ($node instanceof File) {
+ $this->toDelete[] = $node->getId();
+ }
+ }
+ }
+
+ public function postDelete(Node $node) {
+ foreach ($this->toDelete as $fid) {
+ try {
+ $folder = $this->appData->getFolder($fid);
+ $folder->delete();
+ } catch (NotFoundException $e) {
+ // continue
+ }
+ }
+ }
+}
diff --git a/lib/private/Preview/WatcherConnector.php b/lib/private/Preview/WatcherConnector.php
new file mode 100644
index 00000000000..4e6e786cec7
--- /dev/null
+++ b/lib/private/Preview/WatcherConnector.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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\Preview;
+
+use OC\SystemConfig;
+use OCP\Files\Node;
+use OCP\Files\IRootFolder;
+
+class WatcherConnector {
+
+ /** @var IRootFolder */
+ private $root;
+
+ /** @var SystemConfig */
+ private $config;
+
+ /**
+ * WatcherConnector constructor.
+ *
+ * @param IRootFolder $root
+ * @param SystemConfig $config
+ */
+ public function __construct(IRootFolder $root,
+ SystemConfig $config) {
+ $this->root = $root;
+ $this->config = $config;
+ }
+
+ /**
+ * @return Watcher
+ */
+ private function getWatcher() {
+ return \OC::$server->query(Watcher::class);
+ }
+
+ public function connectWatcher() {
+ // Do not connect if we are not setup yet!
+ if ($this->config->getValue('instanceid', null) !== null) {
+ $this->root->listen('\OC\Files', 'postWrite', function (Node $node) {
+ $this->getWatcher()->postWrite($node);
+ });
+
+ $this->root->listen('\OC\Files', 'preDelete', function (Node $node) {
+ $this->getWatcher()->preDelete($node);
+ });
+
+ $this->root->listen('\OC\Files', 'postDelete', function (Node $node) {
+ $this->getWatcher()->postDelete($node);
+ });
+ }
+ }
+}
diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php
index 109feb45864..a2ef9659b3b 100644
--- a/lib/private/PreviewManager.php
+++ b/lib/private/PreviewManager.php
@@ -25,13 +25,30 @@
*/
namespace OC;
+use OC\Preview\Generator;
+use OC\Preview\GeneratorHelper;
+use OCP\Files\File;
+use OCP\Files\IAppData;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\IConfig;
use OCP\IPreview;
use OCP\Preview\IProvider;
class PreviewManager implements IPreview {
- /** @var \OCP\IConfig */
+ /** @var IConfig */
protected $config;
+ /** @var IRootFolder */
+ protected $rootFolder;
+
+ /** @var IAppData */
+ protected $appData;
+
+ /** @var Generator */
+ private $generator;
+
/** @var bool */
protected $providerListDirty = false;
@@ -52,8 +69,12 @@ class PreviewManager implements IPreview {
*
* @param \OCP\IConfig $config
*/
- public function __construct(\OCP\IConfig $config) {
+ public function __construct(IConfig $config,
+ IRootFolder $rootFolder,
+ IAppData $appData) {
$this->config = $config;
+ $this->rootFolder = $rootFolder;
+ $this->appData = $appData;
}
/**
@@ -121,6 +142,37 @@ class PreviewManager implements IPreview {
}
/**
+ * Returns a preview of a file
+ *
+ * The cache is searched first and if nothing usable was found then a preview is
+ * generated by one of the providers
+ *
+ * @param File $file
+ * @param int $width
+ * @param int $height
+ * @param bool $crop
+ * @param string $mode
+ * @param string $mimeType
+ * @return ISimpleFile
+ * @throws NotFoundException
+ * @since 9.2.0
+ */
+ public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) {
+ if ($this->generator === null) {
+ $this->generator = new Generator(
+ $this->config,
+ $this,
+ $this->appData,
+ new GeneratorHelper(
+ $this->rootFolder
+ )
+ );
+ }
+
+ return $this->generator->getPreview($file, $width, $height, $crop, $mode, $mimeType);
+ }
+
+ /**
* returns true if the passed mime type is supported
*
* @param string $mimeType
diff --git a/lib/private/Server.php b/lib/private/Server.php
index 9f993ade7fe..8f4e7d9ca2d 100644
--- a/lib/private/Server.php
+++ b/lib/private/Server.php
@@ -117,7 +117,17 @@ class Server extends ServerContainer implements IServerContainer {
});
$this->registerService('PreviewManager', function (Server $c) {
- return new PreviewManager($c->getConfig());
+ return new PreviewManager(
+ $c->getConfig(),
+ $c->getRootFolder(),
+ $c->getAppDataDir('preview')
+ );
+ });
+
+ $this->registerService(\OC\Preview\Watcher::class, function (Server $c) {
+ return new \OC\Preview\Watcher(
+ $c->getAppDataDir('preview')
+ );
});
$this->registerService('EncryptionManager', function (Server $c) {
@@ -192,6 +202,10 @@ class Server extends ServerContainer implements IServerContainer {
);
$connector = new HookConnector($root, $view);
$connector->viewToNode();
+
+ $previewConnector = new \OC\Preview\WatcherConnector($root, $c->getSystemConfig());
+ $previewConnector->connectWatcher();
+
return $root;
});
$this->registerService('LazyRootFolder', function(Server $c) {