f56ae37e8f
* put a file without a generated preview in the trashbin (e.g. a *.docx file) * open the trashbin * following errors will show up in the nextcloud.log: - filesize(): stat failed for ... - fopen(...): failed to open stream: No such file or directory at ... - fread() expects parameter 1 to be resource, boolean given at ... - fclose() expects parameter 1 to be resource, boolean given at ... - imagecreatefromstring(): Empty string or invalid image at ... This is because the preview code tries to load an SVG image, which is obviously only text. The fix simply handles this before the loading happens and the web UI keeps showing the default mimetype icon.
1349 lines
36 KiB
PHP
1349 lines
36 KiB
PHP
<?php
|
|
/**
|
|
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
*
|
|
* @author Björn Schießle <bjoern@schiessle.org>
|
|
* @author Frank Karlitschek <frank@karlitschek.de>
|
|
* @author Georg Ehrke <georg@owncloud.com>
|
|
* @author Joas Schilling <coding@schilljs.com>
|
|
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
|
|
* @author Lukas Reschke <lukas@statuscode.ch>
|
|
* @author Morris Jobke <hey@morrisjobke.de>
|
|
* @author Olivier Paroz <github@oparoz.com>
|
|
* @author Robin Appelman <robin@icewind.nl>
|
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
|
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
|
* @author Tobias Kaminsky <tobias@kaminsky.me>
|
|
*
|
|
* @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() {
|
|
$thumbnailMount = $this->userView->getMount($this->getThumbnailsFolder());
|
|
$propagator = $thumbnailMount->getStorage()->getPropagator();
|
|
$propagator->beginBatch();
|
|
|
|
$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->rmdir($previewPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
$propagator->commitBatch();
|
|
}
|
|
|
|
/**
|
|
* 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 || !$fileInfo->isReadable()) {
|
|
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
|
|
* @throws PreviewNotAvailableException
|
|
*/
|
|
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
|
|
* @throws PreviewNotAvailableException
|
|
*/
|
|
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);
|
|
}
|
|
// we can't load SVGs into an image
|
|
if (substr($mimeIconWebPath, -4) === '.svg') {
|
|
throw new PreviewNotAvailableException('SVG mimetype cannot be rendered');
|
|
}
|
|
$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();
|
|
}
|
|
|
|
}
|