aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Preview.php
diff options
context:
space:
mode:
authorRoeland Jago Douma <rullzer@owncloud.com>2016-04-29 15:08:01 +0200
committerRoeland Jago Douma <rullzer@owncloud.com>2016-04-29 15:08:01 +0200
commitf52c5b31b6b92bc83e224413f4a8ae4ba7d11993 (patch)
tree09654a6d35089eab4b7b0deeda72cee522ae304f /lib/private/Preview.php
parent66ebc2ee6fe09e1e8419f4b399cd2c7c155d533e (diff)
downloadnextcloud-server-f52c5b31b6b92bc83e224413f4a8ae4ba7d11993.tar.gz
nextcloud-server-f52c5b31b6b92bc83e224413f4a8ae4ba7d11993.zip
Move more from \OC to PSR-4
* \OC\OCSClient * \OC\Preview * \OC\PreviewManager * \OC\Repair * \OC\RepairException * \OC\Search * \OC\ServerContainer * \OC\ServerNotAvailableException * \OC\ServiceUnavailableException * \OC\Setup * \OC\Streamer * \OC\SubAdmin * \OC\SystemConfig * \OC\TagManager * \OC\Tags * \OC\TempManager * \OC\TemplateLayout * \OC\URLGenerator * \OC\Updater
Diffstat (limited to 'lib/private/Preview.php')
-rw-r--r--lib/private/Preview.php1337
1 files changed, 1337 insertions, 0 deletions
diff --git a/lib/private/Preview.php b/lib/private/Preview.php
new file mode 100644
index 00000000000..4fca56dd984
--- /dev/null
+++ b/lib/private/Preview.php
@@ -0,0 +1,1337 @@
+<?php
+/**
+ * @author Björn Schießle <schiessle@owncloud.com>
+ * @author Frank Karlitschek <frank@owncloud.org>
+ * @author Georg Ehrke <georg@owncloud.com>
+ * @author Joas Schilling <nickvergessen@owncloud.com>
+ * @author Jörn Friedrich Dreyer <jfd@butonic.de>
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Olivier Paroz <github@oparoz.com>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Roeland Jago Douma <rullzer@owncloud.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Tobias Kaminsky <tobias@kaminsky.me>
+ *
+ * @copyright Copyright (c) 2016, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+namespace OC;
+
+use OC\Preview\Provider;
+use OCP\Files\FileInfo;
+use OCP\Files\NotFoundException;
+
+class Preview {
+ //the thumbnail folder
+ const THUMBNAILS_FOLDER = 'thumbnails';
+
+ const MODE_FILL = 'fill';
+ const MODE_COVER = 'cover';
+
+ //config
+ private $maxScaleFactor;
+ /** @var int maximum width allowed for a preview */
+ private $configMaxWidth;
+ /** @var int maximum height allowed for a preview */
+ private $configMaxHeight;
+
+ //fileview object
+ private $fileView = null;
+ private $userView = null;
+
+ //vars
+ private $file;
+ private $maxX;
+ private $maxY;
+ private $scalingUp;
+ private $mimeType;
+ private $keepAspect = false;
+ private $mode = self::MODE_FILL;
+
+ //used to calculate the size of the preview to generate
+ /** @var int $maxPreviewWidth max width a preview can have */
+ private $maxPreviewWidth;
+ /** @var int $maxPreviewHeight max height a preview can have */
+ private $maxPreviewHeight;
+ /** @var int $previewWidth calculated width of the preview we're looking for */
+ private $previewWidth;
+ /** @var int $previewHeight calculated height of the preview we're looking for */
+ private $previewHeight;
+
+ //filemapper used for deleting previews
+ // index is path, value is fileinfo
+ static public $deleteFileMapper = array();
+ static public $deleteChildrenMapper = array();
+
+ /**
+ * preview images object
+ *
+ * @var \OCP\IImage
+ */
+ private $preview;
+
+ /**
+ * @var \OCP\Files\FileInfo
+ */
+ protected $info;
+
+ /**
+ * check if thumbnail or bigger version of thumbnail of file is cached
+ *
+ * @param string $user userid - if no user is given, OC_User::getUser will be used
+ * @param string $root path of root
+ * @param string $file The path to the file where you want a thumbnail from
+ * @param int $maxX The maximum X size of the thumbnail. It can be smaller depending on the
+ * shape of the image
+ * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the
+ * shape of the image
+ * @param bool $scalingUp Disable/Enable upscaling of previews
+ *
+ * @throws \Exception
+ * @return mixed (bool / string)
+ * false if thumbnail does not exist
+ * path to thumbnail if thumbnail exists
+ */
+ public function __construct(
+ $user = '',
+ $root = '/',
+ $file = '', $maxX = 1,
+ $maxY = 1,
+ $scalingUp = true
+ ) {
+ //init fileviews
+ if ($user === '') {
+ $user = \OC_User::getUser();
+ }
+ $this->fileView = new \OC\Files\View('/' . $user . '/' . $root);
+ $this->userView = new \OC\Files\View('/' . $user);
+
+ //set config
+ $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);
+
+ //save parameters
+ $this->setFile($file);
+ $this->setMaxX((int)$maxX);
+ $this->setMaxY((int)$maxY);
+ $this->setScalingUp($scalingUp);
+
+ $this->preview = null;
+
+ //check if there are preview backends
+ if (!\OC::$server->getPreviewManager()
+ ->hasProviders()
+ && \OC::$server->getConfig()
+ ->getSystemValue('enable_previews', true)
+ ) {
+ \OCP\Util::writeLog('core', 'No preview providers exist', \OCP\Util::ERROR);
+ throw new \Exception('No preview providers');
+ }
+ }
+
+ /**
+ * returns the path of the file you want a thumbnail from
+ *
+ * @return string
+ */
+ public function getFile() {
+ return $this->file;
+ }
+
+ /**
+ * returns the max width of the preview
+ *
+ * @return integer
+ */
+ public function getMaxX() {
+ return $this->maxX;
+ }
+
+ /**
+ * returns the max height of the preview
+ *
+ * @return integer
+ */
+ public function getMaxY() {
+ return $this->maxY;
+ }
+
+ /**
+ * returns whether or not scalingup is enabled
+ *
+ * @return bool
+ */
+ public function getScalingUp() {
+ return $this->scalingUp;
+ }
+
+ /**
+ * returns the name of the thumbnailfolder
+ *
+ * @return string
+ */
+ public function getThumbnailsFolder() {
+ return self::THUMBNAILS_FOLDER;
+ }
+
+ /**
+ * returns the max scale factor
+ *
+ * @return string
+ */
+ public function getMaxScaleFactor() {
+ return $this->maxScaleFactor;
+ }
+
+ /**
+ * returns the max width set in ownCloud's config
+ *
+ * @return integer
+ */
+ public function getConfigMaxX() {
+ return $this->configMaxWidth;
+ }
+
+ /**
+ * returns the max height set in ownCloud's config
+ *
+ * @return integer
+ */
+ public function getConfigMaxY() {
+ return $this->configMaxHeight;
+ }
+
+ /**
+ * Returns the FileInfo object associated with the file to preview
+ *
+ * @return false|Files\FileInfo|\OCP\Files\FileInfo
+ */
+ protected function getFileInfo() {
+ $absPath = $this->fileView->getAbsolutePath($this->file);
+ $absPath = Files\Filesystem::normalizePath($absPath);
+ if (array_key_exists($absPath, self::$deleteFileMapper)) {
+ $this->info = self::$deleteFileMapper[$absPath];
+ } else if (!$this->info) {
+ $this->info = $this->fileView->getFileInfo($this->file);
+ }
+
+ return $this->info;
+ }
+
+
+ /**
+ * @return array|null
+ */
+ private function getChildren() {
+ $absPath = $this->fileView->getAbsolutePath($this->file);
+ $absPath = Files\Filesystem::normalizePath($absPath);
+
+ if (array_key_exists($absPath, self::$deleteChildrenMapper)) {
+ return self::$deleteChildrenMapper[$absPath];
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the path of the file you want a preview of
+ *
+ * @param string $file
+ * @param \OCP\Files\FileInfo|null $info
+ *
+ * @return \OC\Preview
+ */
+ public function setFile($file, $info = null) {
+ $this->file = $file;
+ $this->info = $info;
+
+ if ($file !== '') {
+ $this->getFileInfo();
+ if ($this->info instanceof \OCP\Files\FileInfo) {
+ $this->mimeType = $this->info->getMimetype();
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Forces the use of a specific media type
+ *
+ * @param string $mimeType
+ */
+ public function setMimetype($mimeType) {
+ $this->mimeType = $mimeType;
+ }
+
+ /**
+ * Sets the max width of the preview. It's capped by the maximum allowed size set in the
+ * configuration
+ *
+ * @param int $maxX
+ *
+ * @throws \Exception
+ * @return \OC\Preview
+ */
+ public function setMaxX($maxX = 1) {
+ if ($maxX <= 0) {
+ throw new \Exception('Cannot set width of 0 or smaller!');
+ }
+ $configMaxX = $this->getConfigMaxX();
+ $maxX = $this->limitMaxDim($maxX, $configMaxX, 'maxX');
+ $this->maxX = $maxX;
+
+ return $this;
+ }
+
+ /**
+ * Sets the max height of the preview. It's capped by the maximum allowed size set in the
+ * configuration
+ *
+ * @param int $maxY
+ *
+ * @throws \Exception
+ * @return \OC\Preview
+ */
+ public function setMaxY($maxY = 1) {
+ if ($maxY <= 0) {
+ throw new \Exception('Cannot set height of 0 or smaller!');
+ }
+ $configMaxY = $this->getConfigMaxY();
+ $maxY = $this->limitMaxDim($maxY, $configMaxY, 'maxY');
+ $this->maxY = $maxY;
+
+ return $this;
+ }
+
+ /**
+ * Sets whether we're allowed to scale up when generating a preview. It's capped by the maximum
+ * allowed scale factor set in the configuration
+ *
+ * @param bool $scalingUp
+ *
+ * @return \OC\Preview
+ */
+ public function setScalingup($scalingUp) {
+ if ($this->getMaxScaleFactor() === 1) {
+ $scalingUp = false;
+ }
+ $this->scalingUp = $scalingUp;
+
+ return $this;
+ }
+
+ /**
+ * Set whether to cover or fill the specified dimensions
+ *
+ * @param string $mode
+ *
+ * @return \OC\Preview
+ */
+ public function setMode($mode) {
+ $this->mode = $mode;
+
+ return $this;
+ }
+
+ /**
+ * Sets whether we need to generate a preview which keeps the aspect ratio of the original file
+ *
+ * @param bool $keepAspect
+ *
+ * @return \OC\Preview
+ */
+ public function setKeepAspect($keepAspect) {
+ $this->keepAspect = $keepAspect;
+
+ return $this;
+ }
+
+ /**
+ * Makes sure we were given a file to preview and that it exists in the filesystem
+ *
+ * @return bool
+ */
+ public function isFileValid() {
+ $file = $this->getFile();
+ if ($file === '') {
+ \OCP\Util::writeLog('core', 'No filename passed', \OCP\Util::DEBUG);
+
+ return false;
+ }
+
+ if (!$this->getFileInfo() instanceof FileInfo) {
+ \OCP\Util::writeLog('core', 'File:"' . $file . '" not found', \OCP\Util::DEBUG);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Deletes the preview of a file with specific width and height
+ *
+ * This should never delete the max preview, use deleteAllPreviews() instead
+ *
+ * @return bool
+ */
+ public function deletePreview() {
+ $fileInfo = $this->getFileInfo();
+ if ($fileInfo !== null && $fileInfo !== false) {
+ $fileId = $fileInfo->getId();
+
+ $previewPath = $this->buildCachePath($fileId);
+ if (!strpos($previewPath, 'max')) {
+ return $this->userView->unlink($previewPath);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Deletes all previews of a file
+ */
+ public function deleteAllPreviews() {
+ $toDelete = $this->getChildren();
+ $toDelete[] = $this->getFileInfo();
+
+ foreach ($toDelete as $delete) {
+ if ($delete instanceof FileInfo) {
+ /** @var \OCP\Files\FileInfo $delete */
+ $fileId = $delete->getId();
+
+ // getId() might return null, e.g. when the file is a
+ // .ocTransferId*.part file from chunked file upload.
+ if (!empty($fileId)) {
+ $previewPath = $this->getPreviewPath($fileId);
+ $this->userView->deleteAll($previewPath);
+ $this->userView->rmdir($previewPath);
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks if a preview matching the asked dimensions or a bigger version is already cached
+ *
+ * * We first retrieve the size of the max preview since this is what we be used to create
+ * all our preview. If it doesn't exist we return false, so that it can be generated
+ * * Using the dimensions of the max preview, we calculate what the size of the new
+ * thumbnail should be
+ * * And finally, we look for a suitable candidate in the cache
+ *
+ * @param int $fileId fileId of the original file we need a preview of
+ *
+ * @return string|false path to the cached preview if it exists or false
+ */
+ public function isCached($fileId) {
+ if (is_null($fileId)) {
+ return false;
+ }
+
+ /**
+ * Phase 1: Looking for the max preview
+ */
+ $previewPath = $this->getPreviewPath($fileId);
+ // We currently can't look for a single file due to bugs related to #16478
+ $allThumbnails = $this->userView->getDirectoryContent($previewPath);
+ list($maxPreviewWidth, $maxPreviewHeight) = $this->getMaxPreviewSize($allThumbnails);
+
+ // Only use the cache if we have a max preview
+ if (!is_null($maxPreviewWidth) && !is_null($maxPreviewHeight)) {
+
+ /**
+ * Phase 2: Calculating the size of the preview we need to send back
+ */
+ $this->maxPreviewWidth = $maxPreviewWidth;
+ $this->maxPreviewHeight = $maxPreviewHeight;
+
+ list($previewWidth, $previewHeight) = $this->simulatePreviewDimensions();
+ if (empty($previewWidth) || empty($previewHeight)) {
+ return false;
+ }
+
+ $this->previewWidth = $previewWidth;
+ $this->previewHeight = $previewHeight;
+
+ /**
+ * Phase 3: We look for a preview of the exact size
+ */
+ // This gives us a calculated path to a preview of asked dimensions
+ // thumbnailFolder/fileId/<maxX>-<maxY>(-max|-with-aspect).png
+ $preview = $this->buildCachePath($fileId, $previewWidth, $previewHeight);
+
+ // This checks if we have a preview of those exact dimensions in the cache
+ if ($this->thumbnailSizeExists($allThumbnails, basename($preview))) {
+ return $preview;
+ }
+
+ /**
+ * Phase 4: We look for a larger preview, matching the aspect ratio
+ */
+ if (($this->getMaxX() >= $maxPreviewWidth)
+ && ($this->getMaxY() >= $maxPreviewHeight)
+ ) {
+ // The preview we-re looking for is the exact size or larger than the max preview,
+ // so return that
+ return $this->buildCachePath($fileId, $maxPreviewWidth, $maxPreviewHeight);
+ } else {
+ // The last resort is to look for something bigger than what we've calculated,
+ // but still smaller than the max preview
+ return $this->isCachedBigger($fileId, $allThumbnails);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the dimensions of the max preview
+ *
+ * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
+ *
+ * @return int[]
+ */
+ private function getMaxPreviewSize($allThumbnails) {
+ $maxPreviewX = null;
+ $maxPreviewY = null;
+
+ foreach ($allThumbnails as $thumbnail) {
+ $name = $thumbnail['name'];
+ if (strpos($name, 'max')) {
+ list($maxPreviewX, $maxPreviewY) = $this->getDimensionsFromFilename($name);
+ break;
+ }
+ }
+
+ return [$maxPreviewX, $maxPreviewY];
+ }
+
+ /**
+ * Check if a specific thumbnail size is cached
+ *
+ * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
+ * @param string $name
+ * @return bool
+ */
+ private function thumbnailSizeExists(array $allThumbnails, $name) {
+
+ foreach ($allThumbnails as $thumbnail) {
+ if ($name === $thumbnail->getName()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Determines the size of the preview we should be looking for in the cache
+ *
+ * @return integer[]
+ */
+ private function simulatePreviewDimensions() {
+ $askedWidth = $this->getMaxX();
+ $askedHeight = $this->getMaxY();
+
+ if ($this->keepAspect) {
+ list($newPreviewWidth, $newPreviewHeight) =
+ $this->applyAspectRatio($askedWidth, $askedHeight);
+ } else {
+ list($newPreviewWidth, $newPreviewHeight) = $this->fixSize($askedWidth, $askedHeight);
+ }
+
+ return [(int)$newPreviewWidth, (int)$newPreviewHeight];
+ }
+
+ /**
+ * Resizes the boundaries to match the aspect ratio
+ *
+ * @param int $askedWidth
+ * @param int $askedHeight
+ *
+ * @param int $originalWidth
+ * @param int $originalHeight
+ * @return integer[]
+ */
+ private function applyAspectRatio($askedWidth, $askedHeight, $originalWidth = 0, $originalHeight = 0) {
+ if(!$originalWidth){
+ $originalWidth= $this->maxPreviewWidth;
+ }
+ if (!$originalHeight) {
+ $originalHeight = $this->maxPreviewHeight;
+ }
+ $originalRatio = $originalWidth / $originalHeight;
+ // Defines the box in which the preview has to fit
+ $scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
+ $askedWidth = min($askedWidth, $originalWidth * $scaleFactor);
+ $askedHeight = min($askedHeight, $originalHeight * $scaleFactor);
+
+ if ($askedWidth / $originalRatio < $askedHeight) {
+ // width restricted
+ $askedHeight = round($askedWidth / $originalRatio);
+ } else {
+ $askedWidth = round($askedHeight * $originalRatio);
+ }
+
+ return [(int)$askedWidth, (int)$askedHeight];
+ }
+
+ /**
+ * Resizes the boundaries to cover the area
+ *
+ * @param int $askedWidth
+ * @param int $askedHeight
+ * @param int $previewWidth
+ * @param int $previewHeight
+ * @return integer[]
+ */
+ private function applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight) {
+ $originalRatio = $previewWidth / $previewHeight;
+ // Defines the box in which the preview has to fit
+ $scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
+ $askedWidth = min($askedWidth, $previewWidth * $scaleFactor);
+ $askedHeight = min($askedHeight, $previewHeight * $scaleFactor);
+
+ if ($askedWidth / $originalRatio > $askedHeight) {
+ // height restricted
+ $askedHeight = round($askedWidth / $originalRatio);
+ } else {
+ $askedWidth = round($askedHeight * $originalRatio);
+ }
+
+ return [(int)$askedWidth, (int)$askedHeight];
+ }
+
+ /**
+ * Makes sure an upscaled preview doesn't end up larger than the max dimensions defined in the
+ * config
+ *
+ * @param int $askedWidth
+ * @param int $askedHeight
+ *
+ * @return integer[]
+ */
+ private function fixSize($askedWidth, $askedHeight) {
+ if ($this->scalingUp) {
+ $askedWidth = min($this->configMaxWidth, $askedWidth);
+ $askedHeight = min($this->configMaxHeight, $askedHeight);
+ }
+
+ return [(int)$askedWidth, (int)$askedHeight];
+ }
+
+ /**
+ * Checks if a bigger version of a file preview is cached and if not
+ * return the preview of max allowed dimensions
+ *
+ * @param int $fileId fileId of the original image
+ * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
+ *
+ * @return string path to bigger thumbnail
+ */
+ private function isCachedBigger($fileId, $allThumbnails) {
+ // This is used to eliminate any thumbnail narrower than what we need
+ $maxX = $this->getMaxX();
+
+ //array for usable cached thumbnails
+ $possibleThumbnails = $this->getPossibleThumbnails($allThumbnails);
+
+ foreach ($possibleThumbnails as $width => $path) {
+ if ($width < $maxX) {
+ continue;
+ } else {
+ return $path;
+ }
+ }
+
+ // At this stage, we didn't find a preview, so we return the max preview
+ return $this->buildCachePath($fileId, $this->maxPreviewWidth, $this->maxPreviewHeight);
+ }
+
+ /**
+ * Get possible bigger thumbnails of the given image with the proper aspect ratio
+ *
+ * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
+ *
+ * @return string[] an array of paths to bigger thumbnails
+ */
+ private function getPossibleThumbnails($allThumbnails) {
+ if ($this->keepAspect) {
+ $wantedAspectRatio = (float)($this->maxPreviewWidth / $this->maxPreviewHeight);
+ } else {
+ $wantedAspectRatio = (float)($this->getMaxX() / $this->getMaxY());
+ }
+
+ //array for usable cached thumbnails
+ $possibleThumbnails = array();
+ foreach ($allThumbnails as $thumbnail) {
+ $name = rtrim($thumbnail['name'], '.png');
+ list($x, $y, $aspectRatio) = $this->getDimensionsFromFilename($name);
+ if (abs($aspectRatio - $wantedAspectRatio) >= 0.000001
+ || $this->unscalable($x, $y)
+ ) {
+ continue;
+ }
+ $possibleThumbnails[$x] = $thumbnail['path'];
+ }
+
+ ksort($possibleThumbnails);
+
+ return $possibleThumbnails;
+ }
+
+ /**
+ * Looks at the preview filename from the cache and extracts the size of the preview
+ *
+ * @param string $name
+ *
+ * @return array<int,int,float>
+ */
+ private function getDimensionsFromFilename($name) {
+ $size = explode('-', $name);
+ $x = (int)$size[0];
+ $y = (int)$size[1];
+ $aspectRatio = (float)($x / $y);
+
+ return array($x, $y, $aspectRatio);
+ }
+
+ /**
+ * @param int $x
+ * @param int $y
+ *
+ * @return bool
+ */
+ private function unscalable($x, $y) {
+
+ $maxX = $this->getMaxX();
+ $maxY = $this->getMaxY();
+ $scalingUp = $this->getScalingUp();
+ $maxScaleFactor = $this->getMaxScaleFactor();
+
+ if ($x < $maxX || $y < $maxY) {
+ if ($scalingUp) {
+ $scaleFactor = $maxX / $x;
+ if ($scaleFactor > $maxScaleFactor) {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 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
+ *
+ * @return \OCP\IImage
+ */
+ public function getPreview() {
+ if (!is_null($this->preview) && $this->preview->valid()) {
+ return $this->preview;
+ }
+
+ $this->preview = null;
+ $fileInfo = $this->getFileInfo();
+ if ($fileInfo === null || $fileInfo === false) {
+ return new \OC_Image();
+ }
+
+ $fileId = $fileInfo->getId();
+ $cached = $this->isCached($fileId);
+ if ($cached) {
+ $this->getCachedPreview($fileId, $cached);
+ }
+
+ if (is_null($this->preview)) {
+ $this->generatePreview($fileId);
+ }
+
+ // We still don't have a preview, so we send back an empty object
+ if (is_null($this->preview)) {
+ $this->preview = new \OC_Image();
+ }
+
+ return $this->preview;
+ }
+
+ /**
+ * Sends the preview, including the headers to client which requested it
+ *
+ * @param null|string $mimeTypeForHeaders the media type to use when sending back the reply
+ *
+ * @throws NotFoundException
+ */
+ public function showPreview($mimeTypeForHeaders = null) {
+ // Check if file is valid
+ if ($this->isFileValid() === false) {
+ throw new NotFoundException('File not found.');
+ }
+
+ if (is_null($this->preview)) {
+ $this->getPreview();
+ }
+ if ($this->preview instanceof \OCP\IImage) {
+ if ($this->preview->valid()) {
+ \OCP\Response::enableCaching(3600 * 24); // 24 hours
+ } else {
+ $this->getMimeIcon();
+ }
+ $this->preview->show($mimeTypeForHeaders);
+ }
+ }
+
+ /**
+ * Retrieves the preview from the cache and resizes it if necessary
+ *
+ * @param int $fileId fileId of the original image
+ * @param string $cached the path to the cached preview
+ */
+ private function getCachedPreview($fileId, $cached) {
+ $stream = $this->userView->fopen($cached, 'r');
+ $this->preview = null;
+ if ($stream) {
+ $image = new \OC_Image();
+ $image->loadFromFileHandle($stream);
+
+ $this->preview = $image->valid() ? $image : null;
+
+ if (!is_null($this->preview)) {
+ // Size of the preview we calculated
+ $maxX = $this->previewWidth;
+ $maxY = $this->previewHeight;
+ // Size of the preview we retrieved from the cache
+ $previewX = (int)$this->preview->width();
+ $previewY = (int)$this->preview->height();
+
+ // We don't have an exact match
+ if ($previewX !== $maxX || $previewY !== $maxY) {
+ $this->resizeAndStore($fileId);
+ }
+ }
+
+ fclose($stream);
+ }
+ }
+
+ /**
+ * Resizes, crops, fixes orientation and stores in the cache
+ *
+ * @param int $fileId fileId of the original image
+ */
+ private function resizeAndStore($fileId) {
+ $image = $this->preview;
+ if (!($image instanceof \OCP\IImage)) {
+ \OCP\Util::writeLog(
+ 'core', '$this->preview is not an instance of \OCP\IImage', \OCP\Util::DEBUG
+ );
+
+ return;
+ }
+ $previewWidth = (int)$image->width();
+ $previewHeight = (int)$image->height();
+ $askedWidth = $this->getMaxX();
+ $askedHeight = $this->getMaxY();
+
+ if ($this->mode === self::MODE_COVER) {
+ list($askedWidth, $askedHeight) =
+ $this->applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight);
+ }
+
+ /**
+ * Phase 1: If required, adjust boundaries to keep aspect ratio
+ */
+ if ($this->keepAspect) {
+ list($askedWidth, $askedHeight) =
+ $this->applyAspectRatio($askedWidth, $askedHeight, $previewWidth, $previewHeight);
+ }
+
+ /**
+ * Phase 2: Resizes preview to try and match requirements.
+ * Takes the scaling ratio into consideration
+ */
+ list($newPreviewWidth, $newPreviewHeight) = $this->scale(
+ $image, $askedWidth, $askedHeight, $previewWidth, $previewHeight
+ );
+
+ // The preview has been resized and should now have the asked dimensions
+ if ($newPreviewWidth === $askedWidth && $newPreviewHeight === $askedHeight) {
+ $this->storePreview($fileId, $newPreviewWidth, $newPreviewHeight);
+
+ return;
+ }
+
+ /**
+ * Phase 3: We're still not there yet, so we're clipping and filling
+ * to match the asked dimensions
+ */
+ // It turns out the scaled preview is now too big, so we crop the image
+ if ($newPreviewWidth >= $askedWidth && $newPreviewHeight >= $askedHeight) {
+ $this->crop($image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight);
+ $this->storePreview($fileId, $askedWidth, $askedHeight);
+
+ return;
+ }
+
+ // At least one dimension of the scaled preview is too small,
+ // so we fill the space with a transparent background
+ if (($newPreviewWidth < $askedWidth || $newPreviewHeight < $askedHeight)) {
+ $this->cropAndFill(
+ $image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight
+ );
+ $this->storePreview($fileId, $askedWidth, $askedHeight);
+
+ return;
+ }
+
+ // The preview is smaller, but we can't touch it
+ $this->storePreview($fileId, $newPreviewWidth, $newPreviewHeight);
+ }
+
+ /**
+ * Calculates the new dimensions of the preview
+ *
+ * The new dimensions can be larger or smaller than the ones of the preview we have to resize
+ *
+ * @param \OCP\IImage $image
+ * @param int $askedWidth
+ * @param int $askedHeight
+ * @param int $previewWidth
+ * @param int $previewHeight
+ *
+ * @return int[]
+ */
+ private function scale($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
+ $scalingUp = $this->getScalingUp();
+ $maxScaleFactor = $this->getMaxScaleFactor();
+
+ $factorX = $askedWidth / $previewWidth;
+ $factorY = $askedHeight / $previewHeight;
+
+ if ($factorX >= $factorY) {
+ $factor = $factorX;
+ } else {
+ $factor = $factorY;
+ }
+
+ if ($scalingUp === false) {
+ if ($factor > 1) {
+ $factor = 1;
+ }
+ }
+
+ // We cap when upscaling
+ if (!is_null($maxScaleFactor)) {
+ if ($factor > $maxScaleFactor) {
+ \OCP\Util::writeLog(
+ 'core', 'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor,
+ \OCP\Util::DEBUG
+ );
+ $factor = $maxScaleFactor;
+ }
+ }
+
+ $newPreviewWidth = round($previewWidth * $factor);
+ $newPreviewHeight = round($previewHeight * $factor);
+
+ $image->preciseResize($newPreviewWidth, $newPreviewHeight);
+ $this->preview = $image;
+
+ return [$newPreviewWidth, $newPreviewHeight];
+ }
+
+ /**
+ * Crops a preview which is larger than the dimensions we've received
+ *
+ * @param \OCP\IImage $image
+ * @param int $askedWidth
+ * @param int $askedHeight
+ * @param int $previewWidth
+ * @param int $previewHeight
+ */
+ private function crop($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight = null) {
+ $cropX = floor(abs($askedWidth - $previewWidth) * 0.5);
+ //don't crop previews on the Y axis, this sucks if it's a document.
+ //$cropY = floor(abs($y - $newPreviewHeight) * 0.5);
+ $cropY = 0;
+ $image->crop($cropX, $cropY, $askedWidth, $askedHeight);
+ $this->preview = $image;
+ }
+
+ /**
+ * Crops an image if it's larger than the dimensions we've received and fills the empty space
+ * with a transparent background
+ *
+ * @param \OCP\IImage $image
+ * @param int $askedWidth
+ * @param int $askedHeight
+ * @param int $previewWidth
+ * @param int $previewHeight
+ */
+ private function cropAndFill($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
+ if ($previewWidth > $askedWidth) {
+ $cropX = floor(($previewWidth - $askedWidth) * 0.5);
+ $image->crop($cropX, 0, $askedWidth, $previewHeight);
+ $previewWidth = $askedWidth;
+ }
+
+ if ($previewHeight > $askedHeight) {
+ $cropY = floor(($previewHeight - $askedHeight) * 0.5);
+ $image->crop(0, $cropY, $previewWidth, $askedHeight);
+ $previewHeight = $askedHeight;
+ }
+
+ // Creates a transparent background
+ $backgroundLayer = imagecreatetruecolor($askedWidth, $askedHeight);
+ imagealphablending($backgroundLayer, false);
+ $transparency = imagecolorallocatealpha($backgroundLayer, 0, 0, 0, 127);
+ imagefill($backgroundLayer, 0, 0, $transparency);
+ imagesavealpha($backgroundLayer, true);
+
+ $image = $image->resource();
+
+ $mergeX = floor(abs($askedWidth - $previewWidth) * 0.5);
+ $mergeY = floor(abs($askedHeight - $previewHeight) * 0.5);
+
+ // Pastes the preview on top of the background
+ imagecopy(
+ $backgroundLayer, $image, $mergeX, $mergeY, 0, 0, $previewWidth,
+ $previewHeight
+ );
+
+ $image = new \OC_Image($backgroundLayer);
+
+ $this->preview = $image;
+ }
+
+ /**
+ * Saves a preview in the cache to speed up future calls
+ *
+ * Do not nullify the preview as it might send the whole process in a loop
+ *
+ * @param int $fileId fileId of the original image
+ * @param int $previewWidth
+ * @param int $previewHeight
+ */
+ private function storePreview($fileId, $previewWidth, $previewHeight) {
+ if (empty($previewWidth) || empty($previewHeight)) {
+ \OCP\Util::writeLog(
+ 'core', 'Cannot save preview of dimension ' . $previewWidth . 'x' . $previewHeight,
+ \OCP\Util::DEBUG
+ );
+
+ } else {
+ $cachePath = $this->buildCachePath($fileId, $previewWidth, $previewHeight);
+ $this->userView->file_put_contents($cachePath, $this->preview->data());
+ }
+ }
+
+ /**
+ * Returns the path to a preview based on its dimensions and aspect
+ *
+ * @param int $fileId
+ * @param int|null $maxX
+ * @param int|null $maxY
+ *
+ * @return string
+ */
+ private function buildCachePath($fileId, $maxX = null, $maxY = null) {
+ if (is_null($maxX)) {
+ $maxX = $this->getMaxX();
+ }
+ if (is_null($maxY)) {
+ $maxY = $this->getMaxY();
+ }
+
+ $previewPath = $this->getPreviewPath($fileId);
+ $previewPath = $previewPath . strval($maxX) . '-' . strval($maxY);
+ $isMaxPreview =
+ ($maxX === $this->maxPreviewWidth && $maxY === $this->maxPreviewHeight) ? true : false;
+ if ($isMaxPreview) {
+ $previewPath .= '-max';
+ }
+ if ($this->keepAspect && !$isMaxPreview) {
+ $previewPath .= '-with-aspect';
+ }
+ if ($this->mode === self::MODE_COVER) {
+ $previewPath .= '-cover';
+ }
+ $previewPath .= '.png';
+
+ return $previewPath;
+ }
+
+ /**
+ * Returns the path to the folder where the previews are stored, identified by the fileId
+ *
+ * @param int $fileId
+ *
+ * @return string
+ */
+ private function getPreviewPath($fileId) {
+ return $this->getThumbnailsFolder() . '/' . $fileId . '/';
+ }
+
+ /**
+ * Asks the provider to send a preview of the file which respects the maximum dimensions
+ * defined in the configuration and after saving it in the cache, it is then resized to the
+ * asked dimensions
+ *
+ * This is only called once in order to generate a large PNG of dimensions defined in the
+ * configuration file. We'll be able to quickly resize it later on.
+ * We never upscale the original conversion as this will be done later by the resizing
+ * operation
+ *
+ * @param int $fileId fileId of the original image
+ */
+ private function generatePreview($fileId) {
+ $file = $this->getFile();
+ $preview = null;
+
+ $previewProviders = \OC::$server->getPreviewManager()
+ ->getProviders();
+ foreach ($previewProviders as $supportedMimeType => $providers) {
+ if (!preg_match($supportedMimeType, $this->mimeType)) {
+ continue;
+ }
+
+ foreach ($providers as $closure) {
+ $provider = $closure();
+ if (!($provider instanceof \OCP\Preview\IProvider)) {
+ continue;
+ }
+
+ \OCP\Util::writeLog(
+ 'core', 'Generating preview for "' . $file . '" with "' . get_class($provider)
+ . '"', \OCP\Util::DEBUG
+ );
+
+ /** @var $provider Provider */
+ $preview = $provider->getThumbnail(
+ $file, $this->configMaxWidth, $this->configMaxHeight, $scalingUp = false,
+ $this->fileView
+ );
+
+ if (!($preview instanceof \OCP\IImage)) {
+ continue;
+ }
+
+ $this->preview = $preview;
+ $previewPath = $this->getPreviewPath($fileId);
+
+ if ($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) {
+ $this->userView->mkdir($this->getThumbnailsFolder() . '/');
+ }
+
+ if ($this->userView->is_dir($previewPath) === false) {
+ $this->userView->mkdir($previewPath);
+ }
+
+ // This stores our large preview so that it can be used in subsequent resizing requests
+ $this->storeMaxPreview($previewPath);
+
+ break 2;
+ }
+ }
+
+ // The providers have been kind enough to give us a preview
+ if ($preview) {
+ $this->resizeAndStore($fileId);
+ }
+ }
+
+ /**
+ * Defines the media icon, for the media type of the original file, as the preview
+ */
+ private function getMimeIcon() {
+ $image = new \OC_Image();
+ $mimeIconWebPath = \OC::$server->getMimeTypeDetector()->mimeTypeIcon($this->mimeType);
+ if (empty(\OC::$WEBROOT)) {
+ $mimeIconServerPath = \OC::$SERVERROOT . $mimeIconWebPath;
+ } else {
+ $mimeIconServerPath = str_replace(\OC::$WEBROOT, \OC::$SERVERROOT, $mimeIconWebPath);
+ }
+ $image->loadFromFile($mimeIconServerPath);
+
+ $this->preview = $image;
+ }
+
+ /**
+ * Stores the max preview in the cache
+ *
+ * @param string $previewPath path to the preview
+ */
+ private function storeMaxPreview($previewPath) {
+ $maxPreviewExists = false;
+ $preview = $this->preview;
+
+ $allThumbnails = $this->userView->getDirectoryContent($previewPath);
+ // This is so that the cache doesn't need emptying when upgrading
+ // Can be replaced by an upgrade script...
+ foreach ($allThumbnails as $thumbnail) {
+ $name = rtrim($thumbnail['name'], '.png');
+ if (strpos($name, 'max')) {
+ $maxPreviewExists = true;
+ break;
+ }
+ }
+ // We haven't found the max preview, so we create it
+ if (!$maxPreviewExists) {
+ $previewWidth = $preview->width();
+ $previewHeight = $preview->height();
+ $previewPath = $previewPath . strval($previewWidth) . '-' . strval($previewHeight);
+ $previewPath .= '-max.png';
+ $this->userView->file_put_contents($previewPath, $preview->data());
+ $this->maxPreviewWidth = $previewWidth;
+ $this->maxPreviewHeight = $previewHeight;
+ }
+ }
+
+ /**
+ * Limits a dimension to the maximum dimension provided as argument
+ *
+ * @param int $dim
+ * @param int $maxDim
+ * @param string $dimName
+ *
+ * @return integer
+ */
+ private function limitMaxDim($dim, $maxDim, $dimName) {
+ if (!is_null($maxDim)) {
+ if ($dim > $maxDim) {
+ \OCP\Util::writeLog(
+ 'core', $dimName . ' reduced from ' . $dim . ' to ' . $maxDim, \OCP\Util::DEBUG
+ );
+ $dim = $maxDim;
+ }
+ }
+
+ return $dim;
+ }
+
+ /**
+ * @param array $args
+ */
+ public static function post_write($args) {
+ self::post_delete($args, 'files/');
+ }
+
+ /**
+ * @param array $args
+ */
+ public static function prepare_delete_files($args) {
+ self::prepare_delete($args, 'files/');
+ }
+
+ /**
+ * @param array $args
+ * @param string $prefix
+ */
+ public static function prepare_delete(array $args, $prefix = '') {
+ $path = $args['path'];
+ if (substr($path, 0, 1) === '/') {
+ $path = substr($path, 1);
+ }
+
+ $view = new \OC\Files\View('/' . \OC_User::getUser() . '/' . $prefix);
+
+ $absPath = Files\Filesystem::normalizePath($view->getAbsolutePath($path));
+ $fileInfo = $view->getFileInfo($path);
+ if($fileInfo === false) {
+ return;
+ }
+ self::addPathToDeleteFileMapper($absPath, $fileInfo);
+ if ($view->is_dir($path)) {
+ $children = self::getAllChildren($view, $path);
+ self::$deleteChildrenMapper[$absPath] = $children;
+ }
+ }
+
+ /**
+ * @param string $absolutePath
+ * @param \OCP\Files\FileInfo $info
+ */
+ private static function addPathToDeleteFileMapper($absolutePath, $info) {
+ self::$deleteFileMapper[$absolutePath] = $info;
+ }
+
+ /**
+ * @param \OC\Files\View $view
+ * @param string $path
+ *
+ * @return array
+ */
+ private static function getAllChildren($view, $path) {
+ $children = $view->getDirectoryContent($path);
+ $childrensFiles = array();
+
+ $fakeRootLength = strlen($view->getRoot());
+
+ for ($i = 0; $i < count($children); $i++) {
+ $child = $children[$i];
+
+ $childsPath = substr($child->getPath(), $fakeRootLength);
+
+ if ($view->is_dir($childsPath)) {
+ $children = array_merge(
+ $children,
+ $view->getDirectoryContent($childsPath)
+ );
+ } else {
+ $childrensFiles[] = $child;
+ }
+ }
+
+ return $childrensFiles;
+ }
+
+ /**
+ * @param array $args
+ */
+ public static function post_delete_files($args) {
+ self::post_delete($args, 'files/');
+ }
+
+ /**
+ * @param array $args
+ */
+ public static function post_delete_versions($args) {
+ self::post_delete($args, 'files/');
+ }
+
+ /**
+ * @param array $args
+ * @param string $prefix
+ */
+ public static function post_delete($args, $prefix = '') {
+ $path = Files\Filesystem::normalizePath($args['path']);
+
+ $preview = new Preview(\OC_User::getUser(), $prefix, $path);
+ $preview->deleteAllPreviews();
+ }
+
+}