PageRenderTime 278ms CodeModel.GetById 256ms app.highlight 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/private/preview.php

https://github.com/JasonEades/core
PHP | 768 lines | 455 code | 129 blank | 184 comment | 74 complexity | 07505f9a0fc65efc0094689a41fae4d3 MD5 | raw file
  1<?php
  2/**
  3 * Copyright (c) 2013 Frank Karlitschek frank@owncloud.org
  4 * Copyright (c) 2013 Georg Ehrke georg@ownCloud.com
  5 * This file is licensed under the Affero General Public License version 3 or
  6 * later.
  7 * See the COPYING-README file.
  8 *
  9 * Thumbnails:
 10 * structure of filename:
 11 * /data/user/thumbnails/pathhash/x-y.png
 12 *
 13 */
 14namespace OC;
 15
 16use OC\Preview\Provider;
 17
 18require_once 'preview/image.php';
 19require_once 'preview/movies.php';
 20require_once 'preview/mp3.php';
 21require_once 'preview/pdf.php';
 22require_once 'preview/svg.php';
 23require_once 'preview/txt.php';
 24require_once 'preview/unknown.php';
 25require_once 'preview/office.php';
 26
 27class Preview {
 28	//the thumbnail folder
 29	const THUMBNAILS_FOLDER = 'thumbnails';
 30
 31	//config
 32	private $maxScaleFactor;
 33	private $configMaxX;
 34	private $configMaxY;
 35
 36	//fileview object
 37	private $fileView = null;
 38	private $userView = null;
 39
 40	//vars
 41	private $file;
 42	private $maxX;
 43	private $maxY;
 44	private $scalingUp;
 45	private $mimeType;
 46	private $keepAspect = false;
 47
 48	//filemapper used for deleting previews
 49	// index is path, value is fileinfo
 50	static public $deleteFileMapper = array();
 51
 52	/**
 53	 * preview images object
 54	 *
 55	 * @var \OC_Image
 56	 */
 57	private $preview;
 58
 59	//preview providers
 60	static private $providers = array();
 61	static private $registeredProviders = array();
 62
 63	/**
 64	 * @var \OCP\Files\FileInfo
 65	 */
 66	protected $info;
 67
 68	/**
 69	 * check if thumbnail or bigger version of thumbnail of file is cached
 70	 * @param string $user userid - if no user is given, OC_User::getUser will be used
 71	 * @param string $root path of root
 72	 * @param string $file The path to the file where you want a thumbnail from
 73	 * @param int $maxX The maximum X size of the thumbnail. It can be smaller depending on the shape of the image
 74	 * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the shape of the image
 75	 * @param bool $scalingUp Disable/Enable upscaling of previews
 76	 * @throws \Exception
 77	 * @return mixed (bool / string)
 78	 *                    false if thumbnail does not exist
 79	 *                    path to thumbnail if thumbnail exists
 80	 */
 81	public function __construct($user = '', $root = '/', $file = '', $maxX = 1, $maxY = 1, $scalingUp = true) {
 82		//init fileviews
 83		if ($user === '') {
 84			$user = \OC_User::getUser();
 85		}
 86		$this->fileView = new \OC\Files\View('/' . $user . '/' . $root);
 87		$this->userView = new \OC\Files\View('/' . $user);
 88
 89		//set config
 90		$this->configMaxX = \OC_Config::getValue('preview_max_x', null);
 91		$this->configMaxY = \OC_Config::getValue('preview_max_y', null);
 92		$this->maxScaleFactor = \OC_Config::getValue('preview_max_scale_factor', 2);
 93
 94		//save parameters
 95		$this->setFile($file);
 96		$this->setMaxX($maxX);
 97		$this->setMaxY($maxY);
 98		$this->setScalingUp($scalingUp);
 99
100		$this->preview = null;
101
102		//check if there are preview backends
103		if (empty(self::$providers)) {
104			self::initProviders();
105		}
106
107		if (empty(self::$providers)) {
108			\OC_Log::write('core', 'No preview providers exist', \OC_Log::ERROR);
109			throw new \Exception('No preview providers');
110		}
111	}
112
113	/**
114	 * returns the path of the file you want a thumbnail from
115	 * @return string
116	 */
117	public function getFile() {
118		return $this->file;
119	}
120
121	/**
122	 * returns the max width of the preview
123	 * @return integer
124	 */
125	public function getMaxX() {
126		return $this->maxX;
127	}
128
129	/**
130	 * returns the max height of the preview
131	 * @return integer
132	 */
133	public function getMaxY() {
134		return $this->maxY;
135	}
136
137	/**
138	 * returns whether or not scalingup is enabled
139	 * @return bool
140	 */
141	public function getScalingUp() {
142		return $this->scalingUp;
143	}
144
145	/**
146	 * returns the name of the thumbnailfolder
147	 * @return string
148	 */
149	public function getThumbnailsFolder() {
150		return self::THUMBNAILS_FOLDER;
151	}
152
153	/**
154	 * returns the max scale factor
155	 * @return string
156	 */
157	public function getMaxScaleFactor() {
158		return $this->maxScaleFactor;
159	}
160
161	/**
162	 * returns the max width set in ownCloud's config
163	 * @return string
164	 */
165	public function getConfigMaxX() {
166		return $this->configMaxX;
167	}
168
169	/**
170	 * returns the max height set in ownCloud's config
171	 * @return string
172	 */
173	public function getConfigMaxY() {
174		return $this->configMaxY;
175	}
176
177	/**
178	 * @return false|Files\FileInfo|\OCP\Files\FileInfo
179	 */
180	protected function getFileInfo() {
181		$absPath = $this->fileView->getAbsolutePath($this->file);
182		$absPath = Files\Filesystem::normalizePath($absPath);
183		if(array_key_exists($absPath, self::$deleteFileMapper)) {
184			$this->info = self::$deleteFileMapper[$absPath];
185		} else if (!$this->info) {
186			$this->info = $this->fileView->getFileInfo($this->file);
187		}
188		return $this->info;
189	}
190
191	/**
192	 * set the path of the file you want a thumbnail from
193	 * @param string $file
194	 * @return \OC\Preview $this
195	 */
196	public function setFile($file) {
197		$this->file = $file;
198		$this->info = null;
199		if ($file !== '') {
200			$this->getFileInfo();
201			if($this->info !== null && $this->info !== false) {
202				$this->mimeType = $this->info->getMimetype();
203			}
204		}
205		return $this;
206	}
207
208	/**
209	 * set mime type explicitly
210	 * @param string $mimeType
211	 */
212	public function setMimetype($mimeType) {
213		$this->mimeType = $mimeType;
214	}
215
216	/**
217	 * set the the max width of the preview
218	 * @param int $maxX
219	 * @throws \Exception
220	 * @return \OC\Preview $this
221	 */
222	public function setMaxX($maxX = 1) {
223		if ($maxX <= 0) {
224			throw new \Exception('Cannot set width of 0 or smaller!');
225		}
226		$configMaxX = $this->getConfigMaxX();
227		if (!is_null($configMaxX)) {
228			if ($maxX > $configMaxX) {
229				\OC_Log::write('core', 'maxX reduced from ' . $maxX . ' to ' . $configMaxX, \OC_Log::DEBUG);
230				$maxX = $configMaxX;
231			}
232		}
233		$this->maxX = $maxX;
234		return $this;
235	}
236
237	/**
238	 * set the the max height of the preview
239	 * @param int $maxY
240	 * @throws \Exception
241	 * @return \OC\Preview $this
242	 */
243	public function setMaxY($maxY = 1) {
244		if ($maxY <= 0) {
245			throw new \Exception('Cannot set height of 0 or smaller!');
246		}
247		$configMaxY = $this->getConfigMaxY();
248		if (!is_null($configMaxY)) {
249			if ($maxY > $configMaxY) {
250				\OC_Log::write('core', 'maxX reduced from ' . $maxY . ' to ' . $configMaxY, \OC_Log::DEBUG);
251				$maxY = $configMaxY;
252			}
253		}
254		$this->maxY = $maxY;
255		return $this;
256	}
257
258	/**
259	 * set whether or not scalingup is enabled
260	 * @param bool $scalingUp
261	 * @return \OC\Preview $this
262	 */
263	public function setScalingup($scalingUp) {
264		if ($this->getMaxScaleFactor() === 1) {
265			$scalingUp = false;
266		}
267		$this->scalingUp = $scalingUp;
268		return $this;
269	}
270
271	public function setKeepAspect($keepAspect) {
272		$this->keepAspect = $keepAspect;
273		return $this;
274	}
275
276	/**
277	 * check if all parameters are valid
278	 * @return bool
279	 */
280	public function isFileValid() {
281		$file = $this->getFile();
282		if ($file === '') {
283			\OC_Log::write('core', 'No filename passed', \OC_Log::DEBUG);
284			return false;
285		}
286
287		if (!$this->fileView->file_exists($file)) {
288			\OC_Log::write('core', 'File:"' . $file . '" not found', \OC_Log::DEBUG);
289			return false;
290		}
291
292		return true;
293	}
294
295	/**
296	 * deletes previews of a file with specific x and y
297	 * @return bool
298	 */
299	public function deletePreview() {
300		$file = $this->getFile();
301
302		$fileInfo = $this->getFileInfo($file);
303		if($fileInfo !== null && $fileInfo !== false) {
304			$fileId = $fileInfo->getId();
305
306			$previewPath = $this->buildCachePath($fileId);
307			return $this->userView->unlink($previewPath);
308		}
309		return false;
310	}
311
312	/**
313	 * deletes all previews of a file
314	 * @return bool
315	 */
316	public function deleteAllPreviews() {
317		$file = $this->getFile();
318
319		$fileInfo = $this->getFileInfo($file);
320		if($fileInfo !== null && $fileInfo !== false) {
321			$fileId = $fileInfo->getId();
322
323			$previewPath = $this->getThumbnailsFolder() . '/' . $fileId . '/';
324			$this->userView->deleteAll($previewPath);
325			return $this->userView->rmdir($previewPath);
326		}
327		return false;
328	}
329
330	/**
331	 * check if thumbnail or bigger version of thumbnail of file is cached
332	 * @param int $fileId fileId of the original image
333	 * @return string|false path to thumbnail if it exists or false
334	 */
335	public function isCached($fileId) {
336		if (is_null($fileId)) {
337			return false;
338		}
339
340		$preview = $this->buildCachePath($fileId);
341
342		//does a preview with the wanted height and width already exist?
343		if ($this->userView->file_exists($preview)) {
344			return $preview;
345		}
346
347		return $this->isCachedBigger($fileId);
348	}
349
350	/**
351	 * check if a bigger version of thumbnail of file is cached
352	 * @param int $fileId fileId of the original image
353	 * @return string|false path to bigger thumbnail if it exists or false
354	*/
355	private function isCachedBigger($fileId) {
356
357		if (is_null($fileId)) {
358			return false;
359		}
360
361		// in order to not loose quality we better generate aspect preserving previews from the original file
362		if ($this->keepAspect) {
363			return false;
364		}
365
366		$maxX = $this->getMaxX();
367
368		//array for usable cached thumbnails
369		$possibleThumbnails = $this->getPossibleThumbnails($fileId);
370
371		foreach ($possibleThumbnails as $width => $path) {
372			if ($width < $maxX) {
373				continue;
374			} else {
375				return $path;
376			}
377		}
378
379		return false;
380	}
381
382	/**
383	 * get possible bigger thumbnails of the given image
384	 * @param int $fileId fileId of the original image
385	 * @return array an array of paths to bigger thumbnails
386	*/
387	private function getPossibleThumbnails($fileId) {
388
389		if (is_null($fileId)) {
390			return array();
391		}
392
393		$previewPath = $this->getThumbnailsFolder() . '/' . $fileId . '/';
394
395		$wantedAspectRatio = (float) ($this->getMaxX() / $this->getMaxY());
396
397		//array for usable cached thumbnails
398		$possibleThumbnails = array();
399
400		$allThumbnails = $this->userView->getDirectoryContent($previewPath);
401		foreach ($allThumbnails as $thumbnail) {
402			$name = rtrim($thumbnail['name'], '.png');
403			list($x, $y, $aspectRatio) = $this->getDimensionsFromFilename($name);
404
405			if (abs($aspectRatio - $wantedAspectRatio) >= 0.000001
406				|| $this->unscalable($x, $y)
407			) {
408				continue;
409			}
410			$possibleThumbnails[$x] = $thumbnail['path'];
411		}
412
413		ksort($possibleThumbnails);
414
415		return $possibleThumbnails;
416	}
417
418	/**
419	 * @param string $name
420	 * @return array
421	 */
422	private function getDimensionsFromFilename($name) {
423			$size = explode('-', $name);
424			$x = (int) $size[0];
425			$y = (int) $size[1];
426			$aspectRatio = (float) ($x / $y);
427			return array($x, $y, $aspectRatio);
428	}
429
430	/**
431	 * @param int $x
432	 * @param int $y
433	 * @return bool
434	 */
435	private function unscalable($x, $y) {
436
437		$maxX = $this->getMaxX();
438		$maxY = $this->getMaxY();
439		$scalingUp = $this->getScalingUp();
440		$maxScaleFactor = $this->getMaxScaleFactor();
441
442		if ($x < $maxX || $y < $maxY) {
443			if ($scalingUp) {
444				$scalefactor = $maxX / $x;
445				if ($scalefactor > $maxScaleFactor) {
446					return true;
447				}
448			} else {
449				return true;
450			}
451		}
452		return false;
453	}
454
455	/**
456	 * return a preview of a file
457	 * @return \OC_Image
458	 */
459	public function getPreview() {
460		if (!is_null($this->preview) && $this->preview->valid()) {
461			return $this->preview;
462		}
463
464		$this->preview = null;
465		$file = $this->getFile();
466		$maxX = $this->getMaxX();
467		$maxY = $this->getMaxY();
468		$scalingUp = $this->getScalingUp();
469
470		$fileInfo = $this->getFileInfo($file);
471		if($fileInfo === null || $fileInfo === false) {
472			return new \OC_Image();
473		}
474		$fileId = $fileInfo->getId();
475
476		$cached = $this->isCached($fileId);
477		if ($cached) {
478			$stream = $this->userView->fopen($cached, 'r');
479			$image = new \OC_Image();
480			$image->loadFromFileHandle($stream);
481			$this->preview = $image->valid() ? $image : null;
482
483			$this->resizeAndCrop();
484			fclose($stream);
485		}
486
487		if (is_null($this->preview)) {
488			$preview = null;
489
490			foreach (self::$providers as $supportedMimeType => $provider) {
491				if (!preg_match($supportedMimeType, $this->mimeType)) {
492					continue;
493				}
494
495				\OC_Log::write('core', 'Generating preview for "' . $file . '" with "' . get_class($provider) . '"', \OC_Log::DEBUG);
496
497				/** @var $provider Provider */
498				$preview = $provider->getThumbnail($file, $maxX, $maxY, $scalingUp, $this->fileView);
499
500				if (!($preview instanceof \OC_Image)) {
501					continue;
502				}
503
504				$this->preview = $preview;
505				$this->resizeAndCrop();
506
507				$previewPath = $this->getThumbnailsFolder() . '/' . $fileId . '/';
508				$cachePath = $this->buildCachePath($fileId);
509
510				if ($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) {
511					$this->userView->mkdir($this->getThumbnailsFolder() . '/');
512				}
513
514				if ($this->userView->is_dir($previewPath) === false) {
515					$this->userView->mkdir($previewPath);
516				}
517
518				$this->userView->file_put_contents($cachePath, $preview->data());
519
520				break;
521			}
522		}
523
524		if (is_null($this->preview)) {
525			$this->preview = new \OC_Image();
526		}
527
528		return $this->preview;
529	}
530
531	/**
532	 * show preview
533	 * @return void
534	 */
535	public function showPreview($mimeType = null) {
536		\OCP\Response::enableCaching(3600 * 24); // 24 hours
537		if (is_null($this->preview)) {
538			$this->getPreview();
539		}
540		$this->preview->show($mimeType);
541	}
542
543	/**
544	 * resize, crop and fix orientation
545	 * @return void
546	 */
547	private function resizeAndCrop() {
548		$image = $this->preview;
549		$x = $this->getMaxX();
550		$y = $this->getMaxY();
551		$scalingUp = $this->getScalingUp();
552		$maxScaleFactor = $this->getMaxScaleFactor();
553
554		if (!($image instanceof \OC_Image)) {
555			\OC_Log::write('core', '$this->preview is not an instance of OC_Image', \OC_Log::DEBUG);
556			return;
557		}
558
559		$image->fixOrientation();
560
561		$realX = (int)$image->width();
562		$realY = (int)$image->height();
563
564		// compute $maxY using the aspect of the generated preview
565		if ($this->keepAspect) {
566			$y = $x / ($realX / $realY);
567		}
568
569		if ($x === $realX && $y === $realY) {
570			$this->preview = $image;
571			return;
572		}
573
574		$factorX = $x / $realX;
575		$factorY = $y / $realY;
576
577		if ($factorX >= $factorY) {
578			$factor = $factorX;
579		} else {
580			$factor = $factorY;
581		}
582
583		if ($scalingUp === false) {
584			if ($factor > 1) {
585				$factor = 1;
586			}
587		}
588
589		if (!is_null($maxScaleFactor)) {
590			if ($factor > $maxScaleFactor) {
591				\OC_Log::write('core', 'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor, \OC_Log::DEBUG);
592				$factor = $maxScaleFactor;
593			}
594		}
595
596		$newXSize = (int)($realX * $factor);
597		$newYSize = (int)($realY * $factor);
598
599		$image->preciseResize($newXSize, $newYSize);
600
601		if ($newXSize === $x && $newYSize === $y) {
602			$this->preview = $image;
603			return;
604		}
605
606		if ($newXSize >= $x && $newYSize >= $y) {
607			$cropX = floor(abs($x - $newXSize) * 0.5);
608			//don't crop previews on the Y axis, this sucks if it's a document.
609			//$cropY = floor(abs($y - $newYsize) * 0.5);
610			$cropY = 0;
611
612			$image->crop($cropX, $cropY, $x, $y);
613
614			$this->preview = $image;
615			return;
616		}
617
618		if (($newXSize < $x || $newYSize < $y) && $scalingUp) {
619			if ($newXSize > $x) {
620				$cropX = floor(($newXSize - $x) * 0.5);
621				$image->crop($cropX, 0, $x, $newYSize);
622			}
623
624			if ($newYSize > $y) {
625				$cropY = floor(($newYSize - $y) * 0.5);
626				$image->crop(0, $cropY, $newXSize, $y);
627			}
628
629			$newXSize = (int)$image->width();
630			$newYSize = (int)$image->height();
631
632			//create transparent background layer
633			$backgroundLayer = imagecreatetruecolor($x, $y);
634			$white = imagecolorallocate($backgroundLayer, 255, 255, 255);
635			imagefill($backgroundLayer, 0, 0, $white);
636
637			$image = $image->resource();
638
639			$mergeX = floor(abs($x - $newXSize) * 0.5);
640			$mergeY = floor(abs($y - $newYSize) * 0.5);
641
642			imagecopy($backgroundLayer, $image, $mergeX, $mergeY, 0, 0, $newXSize, $newYSize);
643
644			//$black = imagecolorallocate(0,0,0);
645			//imagecolortransparent($transparentlayer, $black);
646
647			$image = new \OC_Image($backgroundLayer);
648
649			$this->preview = $image;
650			return;
651		}
652	}
653
654	/**
655	 * register a new preview provider to be used
656	 * @param array $options
657	 * @return void
658	 */
659	public static function registerProvider($class, $options = array()) {
660		self::$registeredProviders[] = array('class' => $class, 'options' => $options);
661	}
662
663	/**
664	 * create instances of all the registered preview providers
665	 * @return void
666	 */
667	private static function initProviders() {
668		if (!\OC_Config::getValue('enable_previews', true)) {
669			$provider = new Preview\Unknown(array());
670			self::$providers = array($provider->getMimeType() => $provider);
671			return;
672		}
673
674		if (count(self::$providers) > 0) {
675			return;
676		}
677
678		foreach (self::$registeredProviders as $provider) {
679			$class = $provider['class'];
680			$options = $provider['options'];
681
682			/** @var $object Provider */
683			$object = new $class($options);
684
685			self::$providers[$object->getMimeType()] = $object;
686		}
687
688		$keys = array_map('strlen', array_keys(self::$providers));
689		array_multisort($keys, SORT_DESC, self::$providers);
690	}
691
692	public static function post_write($args) {
693		self::post_delete($args, 'files/');
694	}
695
696	public static function prepare_delete_files($args) {
697		self::prepare_delete($args, 'files/');
698	}
699
700	public static function prepare_delete($args, $prefix='') {
701		$path = $args['path'];
702		if (substr($path, 0, 1) === '/') {
703			$path = substr($path, 1);
704		}
705
706		$view = new \OC\Files\View('/' . \OC_User::getUser() . '/' . $prefix);
707		$info = $view->getFileInfo($path);
708
709		\OC\Preview::$deleteFileMapper = array_merge(
710			\OC\Preview::$deleteFileMapper,
711			array(
712				Files\Filesystem::normalizePath($view->getAbsolutePath($path)) => $info,
713			)
714		);
715	}
716
717	public static function post_delete_files($args) {
718		self::post_delete($args, 'files/');
719	}
720
721	public static function post_delete($args, $prefix='') {
722		$path = Files\Filesystem::normalizePath($args['path']);
723
724		$preview = new Preview(\OC_User::getUser(), $prefix, $path);
725		$preview->deleteAllPreviews();
726	}
727
728	/**
729	 * @param string $mimeType
730	 * @return bool
731	 */
732	public static function isMimeSupported($mimeType) {
733		if (!\OC_Config::getValue('enable_previews', true)) {
734			return false;
735		}
736
737		//check if there are preview backends
738		if (empty(self::$providers)) {
739			self::initProviders();
740		}
741
742		//remove last element because it has the mimetype *
743		$providers = array_slice(self::$providers, 0, -1);
744		foreach ($providers as $supportedMimeType => $provider) {
745			if (preg_match($supportedMimeType, $mimeType)) {
746				return true;
747			}
748		}
749		return false;
750	}
751
752	/**
753	 * @param int $fileId
754	 * @return string
755	 */
756	private function buildCachePath($fileId) {
757		$maxX = $this->getMaxX();
758		$maxY = $this->getMaxY();
759
760		$previewPath = $this->getThumbnailsFolder() . '/' . $fileId . '/';
761		$preview = $previewPath . $maxX . '-' . $maxY . '.png';
762		if ($this->keepAspect) {
763			$preview = $previewPath . $maxX . '-with-aspect.png';
764			return $preview;
765		}
766		return $preview;
767	}
768}