preview.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  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. require_once 'preview/image.php';
  16. require_once 'preview/movies.php';
  17. require_once 'preview/mp3.php';
  18. require_once 'preview/pdf.php';
  19. require_once 'preview/svg.php';
  20. require_once 'preview/txt.php';
  21. require_once 'preview/unknown.php';
  22. require_once 'preview/office.php';
  23. class Preview {
  24. //the thumbnail folder
  25. const THUMBNAILS_FOLDER = 'thumbnails';
  26. //config
  27. private $maxScaleFactor;
  28. private $configMaxX;
  29. private $configMaxY;
  30. //fileview object
  31. private $fileView = null;
  32. private $userView = null;
  33. //vars
  34. private $file;
  35. private $maxX;
  36. private $maxY;
  37. private $scalingup;
  38. private $mimetype;
  39. //preview images object
  40. /**
  41. * @var \OC_Image
  42. */
  43. private $preview;
  44. //preview providers
  45. static private $providers = array();
  46. static private $registeredProviders = array();
  47. /**
  48. * @brief check if thumbnail or bigger version of thumbnail of file is cached
  49. * @param string $user userid - if no user is given, OC_User::getUser will be used
  50. * @param string $root path of root
  51. * @param string $file The path to the file where you want a thumbnail from
  52. * @param int $maxX The maximum X size of the thumbnail. It can be smaller depending on the shape of the image
  53. * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the shape of the image
  54. * @param bool $scalingUp Disable/Enable upscaling of previews
  55. * @return mixed (bool / string)
  56. * false if thumbnail does not exist
  57. * path to thumbnail if thumbnail exists
  58. */
  59. public function __construct($user='', $root='/', $file='', $maxX=1, $maxY=1, $scalingUp=true) {
  60. //init fileviews
  61. if($user === ''){
  62. $user = \OC_User::getUser();
  63. }
  64. $this->fileView = new \OC\Files\View('/' . $user . '/' . $root);
  65. $this->userView = new \OC\Files\View('/' . $user);
  66. //set config
  67. $this->configMaxX = \OC_Config::getValue('preview_max_x', null);
  68. $this->configMaxY = \OC_Config::getValue('preview_max_y', null);
  69. $this->maxScaleFactor = \OC_Config::getValue('preview_max_scale_factor', 2);
  70. //save parameters
  71. $this->setFile($file);
  72. $this->setMaxX($maxX);
  73. $this->setMaxY($maxY);
  74. $this->setScalingUp($scalingUp);
  75. $this->preview = null;
  76. //check if there are preview backends
  77. if(empty(self::$providers)) {
  78. self::initProviders();
  79. }
  80. if(empty(self::$providers)) {
  81. \OC_Log::write('core', 'No preview providers exist', \OC_Log::ERROR);
  82. throw new \Exception('No preview providers');
  83. }
  84. }
  85. /**
  86. * @brief returns the path of the file you want a thumbnail from
  87. * @return string
  88. */
  89. public function getFile() {
  90. return $this->file;
  91. }
  92. /**
  93. * @brief returns the max width of the preview
  94. * @return integer
  95. */
  96. public function getMaxX() {
  97. return $this->maxX;
  98. }
  99. /**
  100. * @brief returns the max height of the preview
  101. * @return integer
  102. */
  103. public function getMaxY() {
  104. return $this->maxY;
  105. }
  106. /**
  107. * @brief returns whether or not scalingup is enabled
  108. * @return bool
  109. */
  110. public function getScalingUp() {
  111. return $this->scalingup;
  112. }
  113. /**
  114. * @brief returns the name of the thumbnailfolder
  115. * @return string
  116. */
  117. public function getThumbnailsFolder() {
  118. return self::THUMBNAILS_FOLDER;
  119. }
  120. /**
  121. * @brief returns the max scale factor
  122. * @return integer
  123. */
  124. public function getMaxScaleFactor() {
  125. return $this->maxScaleFactor;
  126. }
  127. /**
  128. * @brief returns the max width set in ownCloud's config
  129. * @return integer
  130. */
  131. public function getConfigMaxX() {
  132. return $this->configMaxX;
  133. }
  134. /**
  135. * @brief returns the max height set in ownCloud's config
  136. * @return integer
  137. */
  138. public function getConfigMaxY() {
  139. return $this->configMaxY;
  140. }
  141. /**
  142. * @brief set the path of the file you want a thumbnail from
  143. * @param string $file
  144. * @return $this
  145. */
  146. public function setFile($file) {
  147. $this->file = $file;
  148. if ($file !== '') {
  149. $this->mimetype = $this->fileView->getMimeType($this->file);
  150. }
  151. return $this;
  152. }
  153. /**
  154. * @brief set mimetype explicitely
  155. * @param string $mimetype
  156. */
  157. public function setMimetype($mimetype) {
  158. $this->mimetype = $mimetype;
  159. }
  160. /**
  161. * @brief set the the max width of the preview
  162. * @param int $maxX
  163. * @return $this
  164. */
  165. public function setMaxX($maxX=1) {
  166. if($maxX <= 0) {
  167. throw new \Exception('Cannot set width of 0 or smaller!');
  168. }
  169. $configMaxX = $this->getConfigMaxX();
  170. if(!is_null($configMaxX)) {
  171. if($maxX > $configMaxX) {
  172. \OC_Log::write('core', 'maxX reduced from ' . $maxX . ' to ' . $configMaxX, \OC_Log::DEBUG);
  173. $maxX = $configMaxX;
  174. }
  175. }
  176. $this->maxX = $maxX;
  177. return $this;
  178. }
  179. /**
  180. * @brief set the the max height of the preview
  181. * @param int $maxY
  182. * @return $this
  183. */
  184. public function setMaxY($maxY=1) {
  185. if($maxY <= 0) {
  186. throw new \Exception('Cannot set height of 0 or smaller!');
  187. }
  188. $configMaxY = $this->getConfigMaxY();
  189. if(!is_null($configMaxY)) {
  190. if($maxY > $configMaxY) {
  191. \OC_Log::write('core', 'maxX reduced from ' . $maxY . ' to ' . $configMaxY, \OC_Log::DEBUG);
  192. $maxY = $configMaxY;
  193. }
  194. }
  195. $this->maxY = $maxY;
  196. return $this;
  197. }
  198. /**
  199. * @brief set whether or not scalingup is enabled
  200. * @param bool $scalingUp
  201. * @return $this
  202. */
  203. public function setScalingup($scalingUp) {
  204. if($this->getMaxScaleFactor() === 1) {
  205. $scalingUp = false;
  206. }
  207. $this->scalingup = $scalingUp;
  208. return $this;
  209. }
  210. /**
  211. * @brief check if all parameters are valid
  212. * @return bool
  213. */
  214. public function isFileValid() {
  215. $file = $this->getFile();
  216. if($file === '') {
  217. \OC_Log::write('core', 'No filename passed', \OC_Log::DEBUG);
  218. return false;
  219. }
  220. if(!$this->fileView->file_exists($file)) {
  221. \OC_Log::write('core', 'File:"' . $file . '" not found', \OC_Log::DEBUG);
  222. return false;
  223. }
  224. return true;
  225. }
  226. /**
  227. * @brief deletes previews of a file with specific x and y
  228. * @return bool
  229. */
  230. public function deletePreview() {
  231. $file = $this->getFile();
  232. $fileInfo = $this->fileView->getFileInfo($file);
  233. $fileId = $fileInfo['fileid'];
  234. $previewPath = $this->getThumbnailsFolder() . '/' . $fileId . '/' . $this->getMaxX() . '-' . $this->getMaxY() . '.png';
  235. $this->userView->unlink($previewPath);
  236. return !$this->userView->file_exists($previewPath);
  237. }
  238. /**
  239. * @brief deletes all previews of a file
  240. * @return bool
  241. */
  242. public function deleteAllPreviews() {
  243. $file = $this->getFile();
  244. $fileInfo = $this->fileView->getFileInfo($file);
  245. $fileId = $fileInfo['fileid'];
  246. $previewPath = $this->getThumbnailsFolder() . '/' . $fileId . '/';
  247. $this->userView->deleteAll($previewPath);
  248. $this->userView->rmdir($previewPath);
  249. return !$this->userView->is_dir($previewPath);
  250. }
  251. /**
  252. * @brief check if thumbnail or bigger version of thumbnail of file is cached
  253. * @return mixed (bool / string)
  254. * false if thumbnail does not exist
  255. * path to thumbnail if thumbnail exists
  256. */
  257. private function isCached() {
  258. $file = $this->getFile();
  259. $maxX = $this->getMaxX();
  260. $maxY = $this->getMaxY();
  261. $scalingUp = $this->getScalingUp();
  262. $maxScaleFactor = $this->getMaxScaleFactor();
  263. $fileInfo = $this->fileView->getFileInfo($file);
  264. $fileId = $fileInfo['fileid'];
  265. if(is_null($fileId)) {
  266. return false;
  267. }
  268. $previewPath = $this->getThumbnailsFolder() . '/' . $fileId . '/';
  269. if(!$this->userView->is_dir($previewPath)) {
  270. return false;
  271. }
  272. //does a preview with the wanted height and width already exist?
  273. if($this->userView->file_exists($previewPath . $maxX . '-' . $maxY . '.png')) {
  274. return $previewPath . $maxX . '-' . $maxY . '.png';
  275. }
  276. $wantedAspectRatio = (float) ($maxX / $maxY);
  277. //array for usable cached thumbnails
  278. $possibleThumbnails = array();
  279. $allThumbnails = $this->userView->getDirectoryContent($previewPath);
  280. foreach($allThumbnails as $thumbnail) {
  281. $name = rtrim($thumbnail['name'], '.png');
  282. $size = explode('-', $name);
  283. $x = (int) $size[0];
  284. $y = (int) $size[1];
  285. $aspectRatio = (float) ($x / $y);
  286. if($aspectRatio !== $wantedAspectRatio) {
  287. continue;
  288. }
  289. if($x < $maxX || $y < $maxY) {
  290. if($scalingUp) {
  291. $scalefactor = $maxX / $x;
  292. if($scalefactor > $maxScaleFactor) {
  293. continue;
  294. }
  295. }else{
  296. continue;
  297. }
  298. }
  299. $possibleThumbnails[$x] = $thumbnail['path'];
  300. }
  301. if(count($possibleThumbnails) === 0) {
  302. return false;
  303. }
  304. if(count($possibleThumbnails) === 1) {
  305. return current($possibleThumbnails);
  306. }
  307. ksort($possibleThumbnails);
  308. if(key(reset($possibleThumbnails)) > $maxX) {
  309. return current(reset($possibleThumbnails));
  310. }
  311. if(key(end($possibleThumbnails)) < $maxX) {
  312. return current(end($possibleThumbnails));
  313. }
  314. foreach($possibleThumbnails as $width => $path) {
  315. if($width < $maxX) {
  316. continue;
  317. }else{
  318. return $path;
  319. }
  320. }
  321. }
  322. /**
  323. * @brief return a preview of a file
  324. * @return \OC_Image
  325. */
  326. public function getPreview() {
  327. if(!is_null($this->preview) && $this->preview->valid()){
  328. return $this->preview;
  329. }
  330. $this->preview = null;
  331. $file = $this->getFile();
  332. $maxX = $this->getMaxX();
  333. $maxY = $this->getMaxY();
  334. $scalingUp = $this->getScalingUp();
  335. $fileInfo = $this->fileView->getFileInfo($file);
  336. $fileId = $fileInfo['fileid'];
  337. $cached = $this->isCached();
  338. if($cached) {
  339. $image = new \OC_Image($this->userView->file_get_contents($cached, 'r'));
  340. $this->preview = $image->valid() ? $image : null;
  341. $this->resizeAndCrop();
  342. }
  343. if(is_null($this->preview)) {
  344. $preview = null;
  345. foreach(self::$providers as $supportedMimetype => $provider) {
  346. if(!preg_match($supportedMimetype, $this->mimetype)) {
  347. continue;
  348. }
  349. \OC_Log::write('core', 'Generating preview for "' . $file . '" with "' . get_class($provider) . '"', \OC_Log::DEBUG);
  350. $preview = $provider->getThumbnail($file, $maxX, $maxY, $scalingUp, $this->fileView);
  351. if(!($preview instanceof \OC_Image)) {
  352. continue;
  353. }
  354. $this->preview = $preview;
  355. $this->resizeAndCrop();
  356. $previewPath = $this->getThumbnailsFolder() . '/' . $fileId . '/';
  357. $cachePath = $previewPath . $maxX . '-' . $maxY . '.png';
  358. if($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) {
  359. $this->userView->mkdir($this->getThumbnailsFolder() . '/');
  360. }
  361. if($this->userView->is_dir($previewPath) === false) {
  362. $this->userView->mkdir($previewPath);
  363. }
  364. $this->userView->file_put_contents($cachePath, $preview->data());
  365. break;
  366. }
  367. }
  368. if(is_null($this->preview)) {
  369. $this->preview = new \OC_Image();
  370. }
  371. return $this->preview;
  372. }
  373. /**
  374. * @brief show preview
  375. * @return void
  376. */
  377. public function showPreview() {
  378. \OCP\Response::enableCaching(3600 * 24); // 24 hours
  379. if(is_null($this->preview)) {
  380. $this->getPreview();
  381. }
  382. $this->preview->show();
  383. return;
  384. }
  385. /**
  386. * @brief show preview
  387. * @return void
  388. */
  389. public function show() {
  390. $this->showPreview();
  391. return;
  392. }
  393. /**
  394. * @brief resize, crop and fix orientation
  395. * @return void
  396. */
  397. private function resizeAndCrop() {
  398. $image = $this->preview;
  399. $x = $this->getMaxX();
  400. $y = $this->getMaxY();
  401. $scalingUp = $this->getScalingUp();
  402. $maxscalefactor = $this->getMaxScaleFactor();
  403. if(!($image instanceof \OC_Image)) {
  404. \OC_Log::write('core', '$this->preview is not an instance of OC_Image', \OC_Log::DEBUG);
  405. return;
  406. }
  407. $image->fixOrientation();
  408. $realx = (int) $image->width();
  409. $realy = (int) $image->height();
  410. if($x === $realx && $y === $realy) {
  411. $this->preview = $image;
  412. return;
  413. }
  414. $factorX = $x / $realx;
  415. $factorY = $y / $realy;
  416. if($factorX >= $factorY) {
  417. $factor = $factorX;
  418. }else{
  419. $factor = $factorY;
  420. }
  421. if($scalingUp === false) {
  422. if($factor > 1) {
  423. $factor = 1;
  424. }
  425. }
  426. if(!is_null($maxscalefactor)) {
  427. if($factor > $maxscalefactor) {
  428. \OC_Log::write('core', 'scalefactor reduced from ' . $factor . ' to ' . $maxscalefactor, \OC_Log::DEBUG);
  429. $factor = $maxscalefactor;
  430. }
  431. }
  432. $newXsize = (int) ($realx * $factor);
  433. $newYsize = (int) ($realy * $factor);
  434. $image->preciseResize($newXsize, $newYsize);
  435. if($newXsize === $x && $newYsize === $y) {
  436. $this->preview = $image;
  437. return;
  438. }
  439. if($newXsize >= $x && $newYsize >= $y) {
  440. $cropX = floor(abs($x - $newXsize) * 0.5);
  441. //don't crop previews on the Y axis, this sucks if it's a document.
  442. //$cropY = floor(abs($y - $newYsize) * 0.5);
  443. $cropY = 0;
  444. $image->crop($cropX, $cropY, $x, $y);
  445. $this->preview = $image;
  446. return;
  447. }
  448. if($newXsize < $x || $newYsize < $y) {
  449. if($newXsize > $x) {
  450. $cropX = floor(($newXsize - $x) * 0.5);
  451. $image->crop($cropX, 0, $x, $newYsize);
  452. }
  453. if($newYsize > $y) {
  454. $cropY = floor(($newYsize - $y) * 0.5);
  455. $image->crop(0, $cropY, $newXsize, $y);
  456. }
  457. $newXsize = (int) $image->width();
  458. $newYsize = (int) $image->height();
  459. //create transparent background layer
  460. $backgroundlayer = imagecreatetruecolor($x, $y);
  461. $white = imagecolorallocate($backgroundlayer, 255, 255, 255);
  462. imagefill($backgroundlayer, 0, 0, $white);
  463. $image = $image->resource();
  464. $mergeX = floor(abs($x - $newXsize) * 0.5);
  465. $mergeY = floor(abs($y - $newYsize) * 0.5);
  466. imagecopy($backgroundlayer, $image, $mergeX, $mergeY, 0, 0, $newXsize, $newYsize);
  467. //$black = imagecolorallocate(0,0,0);
  468. //imagecolortransparent($transparentlayer, $black);
  469. $image = new \OC_Image($backgroundlayer);
  470. $this->preview = $image;
  471. return;
  472. }
  473. }
  474. /**
  475. * @brief register a new preview provider to be used
  476. * @param string $provider class name of a Preview_Provider
  477. * @param array $options
  478. * @return void
  479. */
  480. public static function registerProvider($class, $options=array()) {
  481. self::$registeredProviders[]=array('class'=>$class, 'options'=>$options);
  482. }
  483. /**
  484. * @brief create instances of all the registered preview providers
  485. * @return void
  486. */
  487. private static function initProviders() {
  488. if(!\OC_Config::getValue('enable_previews', true)) {
  489. $provider = new Preview\Unknown(array());
  490. self::$providers = array($provider->getMimeType() => $provider);
  491. return;
  492. }
  493. if(count(self::$providers)>0) {
  494. return;
  495. }
  496. foreach(self::$registeredProviders as $provider) {
  497. $class=$provider['class'];
  498. $options=$provider['options'];
  499. $object = new $class($options);
  500. self::$providers[$object->getMimeType()] = $object;
  501. }
  502. $keys = array_map('strlen', array_keys(self::$providers));
  503. array_multisort($keys, SORT_DESC, self::$providers);
  504. }
  505. public static function post_write($args) {
  506. self::post_delete($args);
  507. }
  508. public static function post_delete($args) {
  509. $path = $args['path'];
  510. if(substr($path, 0, 1) === '/') {
  511. $path = substr($path, 1);
  512. }
  513. $preview = new Preview(\OC_User::getUser(), 'files/', $path);
  514. $preview->deleteAllPreviews();
  515. }
  516. public static function isMimeSupported($mimetype) {
  517. if(!\OC_Config::getValue('enable_previews', true)) {
  518. return false;
  519. }
  520. //check if there are preview backends
  521. if(empty(self::$providers)) {
  522. self::initProviders();
  523. }
  524. //remove last element because it has the mimetype *
  525. $providers = array_slice(self::$providers, 0, -1);
  526. foreach($providers as $supportedMimetype => $provider) {
  527. if(preg_match($supportedMimetype, $mimetype)) {
  528. return true;
  529. }
  530. }
  531. return false;
  532. }
  533. }