nextcloud/lib/private/Preview/Generator.php

367 lines
9.1 KiB
PHP
Raw Normal View History

<?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\IAppData;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\IConfig;
use OCP\IImage;
use OCP\Image;
use OCP\IPreview;
use OCP\Preview\IProvider;
class Generator {
//the thumbnail folder
const THUMBNAILS_FOLDER = 'thumbnails';
const MODE_FILL = 'fill';
const MODE_COVER = 'cover';
/** @var IRootFolder*/
private $rootFolder;
/** @var File */
private $file;
/** @var IPreview */
private $previewManager;
/** @var IConfig */
private $config;
/** @var IAppData */
private $appData;
/**
* @param IRootFolder $rootFolder
* @param IConfig $config
* @param IPreview $previewManager
* @param File $file
* @param IAppData $appData
*/
public function __construct(
IRootFolder $rootFolder,
IConfig $config,
IPreview $previewManager,
File $file,
IAppData $appData
) {
$this->rootFolder = $rootFolder;
$this->config = $config;
$this->file = $file;
$this->previewManager = $previewManager;
$this->appData = $appData;
}
/**
* 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 int $width
* @param int $height
* @param bool $crop
* @param string $mode
* @return ISimpleFile
* @throws NotFoundException
*/
public function getPreview($width = -1, $height = -1, $crop = false, $mode = Generator::MODE_FILL) {
if (!$this->previewManager->isMimeSupported($this->file->getMimeType())) {
throw new NotFoundException();
}
/*
* Get the preview folder
* TODO: Separate preview creation from storing previews
*/
$previewFolder = $this->getPreviewFolder();
// Get the max preview and infer the max preview sizes from that
$maxPreview = $this->getMaxPreview($previewFolder);
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
* @return ISimpleFile
* @throws NotFoundException
*/
private function getMaxPreview(ISimpleFolder $previewFolder) {
$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, $this->file->getMimeType())) {
continue;
}
foreach ($providers as $provider) {
$provider = $provider();
if (!($provider instanceof IProvider)) {
continue;
}
list($view, $path) = $this->getViewAndPath($this->file);
$maxWidth = (int)$this->config->getSystemValue('preview_max_x', 2048);
$maxHeight = (int)$this->config->getSystemValue('preview_max_y', 2048);
$preview = $provider->getThumbnail($path, $maxWidth, $maxHeight, false, $view);
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 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 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 === self::MODE_FILL) {
if ($ratioH > $ratioW) {
$height = $width * $ratio;
} else {
$width = $height / $ratio;
}
} else if ($mode === self::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 = new Image($maxPreview->getContent());
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
*
* @return ISimpleFolder
*/
private function getPreviewFolder() {
try {
$folder = $this->appData->getFolder($this->file->getId());
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder($this->file->getId());
}
return $folder;
}
}