preview.php 15 KB

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