diff options
author | Lukas Reschke <lukas@statuscode.ch> | 2016-11-03 21:07:16 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-11-03 21:07:16 +0100 |
commit | c0bbae28f0bb1041a22645691fe653c642f0d1cb (patch) | |
tree | c75e0f48a2c0c44af5eca2062592855d044f1f10 /lib/private | |
parent | 8bf57462ea52cc2f4300b6fa097ae23100882c83 (diff) | |
parent | f44cd7aed37d84137902ce8f65aabd115ae1adef (diff) | |
download | nextcloud-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.php | 2 | ||||
-rw-r--r-- | lib/private/Preview/Generator.php | 338 | ||||
-rw-r--r-- | lib/private/Preview/GeneratorHelper.php | 91 | ||||
-rw-r--r-- | lib/private/Preview/Watcher.php | 98 | ||||
-rw-r--r-- | lib/private/Preview/WatcherConnector.php | 72 | ||||
-rw-r--r-- | lib/private/PreviewManager.php | 56 | ||||
-rw-r--r-- | lib/private/Server.php | 16 |
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) { |