You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

preview.php 15KB

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