PageRenderTime 50ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/private/preview.php

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