Newer
Older
<?php
/**
* Copyright (c) 2013 Frank Karlitschek frank@owncloud.org
* Copyright (c) 2013 Georg Ehrke georg@ownCloud.com
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
* Thumbnails:
* structure of filename:
* /data/user/thumbnails/pathhash/x-y.png
require_once 'preview/image.php';
require_once 'preview/movies.php';
require_once 'preview/mp3.php';
require_once 'preview/svg.php';
require_once 'preview/txt.php';
require_once 'preview/office.php';
require_once 'preview/bitmap.php';
const THUMBNAILS_FOLDER = 'thumbnails';
private $maxScaleFactor;
private $configMaxX;
private $configMaxY;
private $fileView = null;
private $userView = null;
//vars
private $file;
private $maxX;
private $maxY;
private $scalingUp;
private $mimeType;

Thomas Müller
committed
private $keepAspect = false;
//filemapper used for deleting previews
// index is path, value is fileinfo
static public $deleteFileMapper = array();
static public $deleteChildrenMapper = array();
//preview providers
static private $providers = array();
static private $registeredProviders = array();
static private $enabledProviders = array();
/**
* @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
* @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) {
$user = \OC_User::getUser();
}
$this->fileView = new \OC\Files\View('/' . $user . '/' . $root);
$this->userView = new \OC\Files\View('/' . $user);
$this->configMaxX = \OC_Config::getValue('preview_max_x', null);
$this->configMaxY = \OC_Config::getValue('preview_max_y', null);

Georg Ehrke
committed
$this->maxScaleFactor = \OC_Config::getValue('preview_max_scale_factor', 2);
$this->setFile($file);
$this->setMaxX($maxX);
$this->setMaxY($maxY);
\OC_Log::write('core', 'No preview providers exist', \OC_Log::ERROR);
throw new \Exception('No preview providers');
* returns the path of the file you want a thumbnail from
public function getFile() {
* returns the max width of the preview
* returns the max height of the preview
* returns whether or not scalingup is enabled
* returns the name of the thumbnailfolder
* returns the max scale factor
* @return string
* returns the max width set in ownCloud's config
* @return string
* returns the max height set in ownCloud's config
* @return string
/**
* @return false|Files\FileInfo|\OCP\Files\FileInfo
*/
$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;
}
* set the path of the file you want a thumbnail from
public function setFile($file) {
$this->file = $file;
$this->getFileInfo();
if($this->info !== null && $this->info !== false) {
$this->mimeType = $this->info->getMimetype();
* set mime type explicitly
public function setMimetype($mimeType) {
$this->mimeType = $mimeType;
* set the the max width of the preview
* @return \OC\Preview $this
*/
public function setMaxX($maxX = 1) {
if ($maxX <= 0) {
if (!is_null($configMaxX)) {
if ($maxX > $configMaxX) {
\OC_Log::write('core', 'maxX reduced from ' . $maxX . ' to ' . $configMaxX, \OC_Log::DEBUG);
$maxX = $configMaxX;
}
}
$this->maxX = $maxX;
return $this;
}
/**
* set the the max height of the preview
* @return \OC\Preview $this
*/
public function setMaxY($maxY = 1) {
if ($maxY <= 0) {
if (!is_null($configMaxY)) {
if ($maxY > $configMaxY) {
\OC_Log::write('core', 'maxX reduced from ' . $maxY . ' to ' . $configMaxY, \OC_Log::DEBUG);
$maxY = $configMaxY;
}
}
$this->maxY = $maxY;
return $this;
}
/**
* set whether or not scalingup is enabled
* @return \OC\Preview $this
if ($this->getMaxScaleFactor() === 1) {
/**
* @param bool $keepAspect
* @return $this
*/

Thomas Müller
committed
public function setKeepAspect($keepAspect) {
$this->keepAspect = $keepAspect;
return $this;
}
* check if all parameters are valid
public function isFileValid() {
$file = $this->getFile();
\OC_Log::write('core', 'No filename passed', \OC_Log::DEBUG);
if (!$this->fileView->file_exists($file)) {
\OC_Log::write('core', 'File:"' . $file . '" not found', \OC_Log::DEBUG);
* deletes previews of a file with specific x and y
$fileInfo = $this->getFileInfo($file);
if($fileInfo !== null && $fileInfo !== false) {
$fileId = $fileInfo->getId();

Thomas Müller
committed
$previewPath = $this->buildCachePath($fileId);
return $this->userView->unlink($previewPath);
}
return false;
* deletes all previews of a file
$fileInfo = $this->getFileInfo($file);
$toDelete = $this->getChildren();
$toDelete[] = $fileInfo;
foreach ($toDelete as $delete) {
if ($delete !== null && $delete !== false) {
/** @var \OCP\Files\FileInfo $delete */
$fileId = $delete->getId();
$previewPath = $this->getPreviewPath($fileId);
$this->userView->deleteAll($previewPath);
$this->userView->rmdir($previewPath);
}
* check if thumbnail or bigger version of thumbnail of file is cached
* @param int $fileId fileId of the original image
* @return string|false path to thumbnail if it exists or false
public function isCached($fileId) {
return false;
}

Thomas Müller
committed
$preview = $this->buildCachePath($fileId);
//does a preview with the wanted height and width already exist?

Thomas Müller
committed
if ($this->userView->file_exists($preview)) {
return $preview;
* check if a bigger version of thumbnail of file is cached
* @param int $fileId fileId of the original image
* @return string|false path to bigger thumbnail if it exists or false
*/
return false;
}

Thomas Müller
committed
// in order to not loose quality we better generate aspect preserving previews from the original file
if ($this->keepAspect) {
return false;
}
//array for usable cached thumbnails
$possibleThumbnails = $this->getPossibleThumbnails($fileId);
foreach ($possibleThumbnails as $width => $path) {
if ($width < $maxX) {
continue;
} else {
return $path;
}
* get possible bigger thumbnails of the given image
* @param int $fileId fileId of the original image
* @return array an array of paths to bigger thumbnails
*/
private function getPossibleThumbnails($fileId) {
$previewPath = $this->getPreviewPath($fileId);
$wantedAspectRatio = (float) ($this->getMaxX() / $this->getMaxY());
//array for usable cached thumbnails
$allThumbnails = $this->userView->getDirectoryContent($previewPath);
foreach ($allThumbnails as $thumbnail) {
$name = rtrim($thumbnail['name'], '.png');
list($x, $y, $aspectRatio) = $this->getDimensionsFromFilename($name);
if (abs($aspectRatio - $wantedAspectRatio) >= 0.000001
/**
* @param string $name
* @return array
*/
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
*/
$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;
}

Frank Karlitschek
committed
* return a preview of a file
if (!is_null($this->preview) && $this->preview->valid()) {
return $this->preview;
}
$this->preview = null;
$file = $this->getFile();
$maxX = $this->getMaxX();
$maxY = $this->getMaxY();
$fileInfo = $this->getFileInfo($file);
if($fileInfo === null || $fileInfo === false) {
return new \OC_Image();
}
$stream = $this->userView->fopen($cached, 'r');
$this->preview = null;
if ($stream) {
$image = new \OC_Image();
$image->loadFromFileHandle($stream);
$this->preview = $image->valid() ? $image : null;

Thomas Müller
committed
$this->resizeAndCrop();
fclose($stream);
}
foreach (self::$providers as $supportedMimeType => $provider) {
if (!preg_match($supportedMimeType, $this->mimeType)) {
\OC_Log::write('core', 'Generating preview for "' . $file . '" with "' . get_class($provider) . '"', \OC_Log::DEBUG);
$preview = $provider->getThumbnail($file, $maxX, $maxY, $scalingUp, $this->fileView);
if (!($preview instanceof \OC_Image)) {
$this->preview = $preview;
$this->resizeAndCrop();
$previewPath = $this->getPreviewPath($fileId);

Thomas Müller
committed
$cachePath = $this->buildCachePath($fileId);
if ($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) {
$this->userView->mkdir($this->getThumbnailsFolder() . '/');
if ($this->userView->is_dir($previewPath) === false) {
$this->userView->file_put_contents($cachePath, $preview->data());
/**
* @param null|string $mimeType
* @throws NotFoundException

Thomas Müller
committed
public function showPreview($mimeType = null) {
// Check if file is valid
if($this->isFileValid() === false) {
throw new NotFoundException('File not found.');
}
if ($this->preview instanceof \OC_Image) {
$this->preview->show($mimeType);
}

Georg Ehrke
committed
}
* resize, crop and fix orientation
$maxScaleFactor = $this->getMaxScaleFactor();
\OC_Log::write('core', '$this->preview is not an instance of OC_Image', \OC_Log::DEBUG);
$realX = (int)$image->width();
$realY = (int)$image->height();

Robin Appelman
committed
// compute $maxY and $maxX using the aspect of the generated preview

Thomas Müller
committed
if ($this->keepAspect) {

Robin Appelman
committed
$ratio = $realX / $realY;
if($x / $ratio < $y) {
// width restricted
$y = $x / $ratio;
} else {
$x = $y * $ratio;
}

Thomas Müller
committed
}
if ($x === $realX && $y === $realY) {
$factorX = $x / $realX;
$factorY = $y / $realY;
if ($scalingUp === false) {
if ($factor > 1) {
if (!is_null($maxScaleFactor)) {
if ($factor > $maxScaleFactor) {
\OC_Log::write('core', 'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor, \OC_Log::DEBUG);
$factor = $maxScaleFactor;
}
$newXSize = (int)($realX * $factor);
$newYSize = (int)($realY * $factor);
$image->preciseResize($newXSize, $newYSize);
if ($newXSize === $x && $newYSize === $y) {
if ($newXSize >= $x && $newYSize >= $y) {
$cropX = floor(abs($x - $newXSize) * 0.5);
//don't crop previews on the Y axis, this sucks if it's a document.
//$cropY = floor(abs($y - $newYsize) * 0.5);
$cropY = 0;
if (($newXSize < $x || $newYSize < $y) && $scalingUp) {
if ($newXSize > $x) {
$cropX = floor(($newXSize - $x) * 0.5);
$image->crop($cropX, 0, $x, $newYSize);
if ($newYSize > $y) {
$cropY = floor(($newYSize - $y) * 0.5);
$image->crop(0, $cropY, $newXSize, $y);
$newXSize = (int)$image->width();
$newYSize = (int)$image->height();
$backgroundLayer = imagecreatetruecolor($x, $y);
$white = imagecolorallocate($backgroundLayer, 255, 255, 255);
imagefill($backgroundLayer, 0, 0, $white);
$mergeX = floor(abs($x - $newXSize) * 0.5);
$mergeY = floor(abs($y - $newYSize) * 0.5);
imagecopy($backgroundLayer, $image, $mergeX, $mergeY, 0, 0, $newXSize, $newYSize);
//$black = imagecolorallocate(0,0,0);
//imagecolortransparent($transparentlayer, $black);
$image = new \OC_Image($backgroundLayer);
* register a new preview provider to be used
* @param string $class
public static function registerProvider($class, $options = array()) {
/**
* Only register providers that have been explicitly enabled
*
* The following providers are enabled by default:
* - OC\Preview\Image
* - OC\Preview\MP3
* - OC\Preview\TXT
* - OC\Preview\MarkDown
*
* The following providers are disabled by default due to performance or privacy concerns:
* - OC\Preview\MSOfficeDoc
* - OC\Preview\MSOffice2003
* - OC\Preview\MSOffice2007
* - OC\Preview\OpenDocument
* - OC\Preview\StarOffice
* - OC\Preview\SVG
* - OC\Preview\Movies
* - OC\Preview\PDF
* - OC\Preview\TIFF
* - OC\Preview\Illustrator
* - OC\Preview\Postscript
* - OC\Preview\Photoshop
*/
if(empty(self::$enabledProviders)) {
self::$enabledProviders = \OC::$server->getConfig()->getSystemValue('enabledPreviewProviders', array(
'OC\Preview\Image',
'OC\Preview\MP3',
'OC\Preview\TXT',
'OC\Preview\MarkDown',
));
}
if(in_array($class, self::$enabledProviders)) {
self::$registeredProviders[] = array('class' => $class, 'options' => $options);
}
* create instances of all the registered preview providers
private static function initProviders() {
if (!\OC::$server->getConfig()->getSystemValue('enable_previews', true)) {
self::$providers = array();
foreach (self::$registeredProviders as $provider) {
$class = $provider['class'];
$options = $provider['options'];
$object = new $class($options);
self::$providers[$object->getMimeType()] = $object;
}
$keys = array_map('strlen', array_keys(self::$providers));
array_multisort($keys, SORT_DESC, self::$providers);
/**
* @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($args, $prefix='') {
$view = new \OC\Files\View('/' . \OC_User::getUser() . '/' . $prefix);
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
$absPath = Files\Filesystem::normalizePath($view->getAbsolutePath($path));
self::addPathToDeleteFileMapper($absPath, $view->getFileInfo($path));
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
* @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();
}
/**
* Check if a preview can be generated for a file
*
* @param \OC\Files\FileInfo $file
* @return bool
*/
public static function isAvailable(\OC\Files\FileInfo $file) {
if (!\OC_Config::getValue('enable_previews', true)) {
return false;
}
//check if there are preview backends
if (empty(self::$providers)) {
self::initProviders();
}
foreach (self::$providers as $supportedMimeType => $provider) {
/**
* @var \OC\Preview\Provider $provider
*/
if (preg_match($supportedMimeType, $file->getMimetype())) {
return $provider->isAvailable($file);
}
}
return false;
}
public static function isMimeSupported($mimeType) {
if (!\OC_Config::getValue('enable_previews', true)) {
//check if there are preview backends
self::initProviders();
}
foreach(self::$providers as $supportedMimetype => $provider) {
if(preg_match($supportedMimetype, $mimeType)) {
return true;
}
}
return false;
}

Thomas Müller
committed
/**

Thomas Müller
committed
* @return string
*/
private function buildCachePath($fileId) {
$maxX = $this->getMaxX();
$maxY = $this->getMaxY();
$previewPath = $this->getPreviewPath($fileId);
$preview = $previewPath . strval($maxX) . '-' . strval($maxY);

Thomas Müller
committed
if ($this->keepAspect) {

Thomas Müller
committed
}

Thomas Müller
committed
return $preview;
}
private function getPreviewPath($fileId) {
return $this->getThumbnailsFolder() . '/' . $fileId . '/';
}