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 34KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308
  1. <?php
  2. /**
  3. * @author Björn Schießle <schiessle@owncloud.com>
  4. * @author Frank Karlitschek <frank@owncloud.org>
  5. * @author Georg Ehrke <georg@owncloud.com>
  6. * @author Joas Schilling <nickvergessen@owncloud.com>
  7. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  8. * @author Lukas Reschke <lukas@owncloud.com>
  9. * @author Morris Jobke <hey@morrisjobke.de>
  10. * @author Olivier Paroz <github@oparoz.com>
  11. * @author Robin Appelman <icewind@owncloud.com>
  12. * @author Thomas Müller <thomas.mueller@tmit.eu>
  13. * @author Tobias Kaminsky <tobias@kaminsky.me>
  14. *
  15. * @copyright Copyright (c) 2015, ownCloud, Inc.
  16. * @license AGPL-3.0
  17. *
  18. * This code is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License, version 3,
  20. * as published by the Free Software Foundation.
  21. *
  22. * This program is distributed in the hope that it will be useful,
  23. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  25. * GNU Affero General Public License for more details.
  26. *
  27. * You should have received a copy of the GNU Affero General Public License, version 3,
  28. * along with this program. If not, see <http://www.gnu.org/licenses/>
  29. *
  30. */
  31. namespace OC;
  32. use OC\Preview\Provider;
  33. use OCP\Files\FileInfo;
  34. use OCP\Files\NotFoundException;
  35. class Preview {
  36. //the thumbnail folder
  37. const THUMBNAILS_FOLDER = 'thumbnails';
  38. const MODE_FILL = 'fill';
  39. const MODE_COVER = 'cover';
  40. //config
  41. private $maxScaleFactor;
  42. /** @var int maximum width allowed for a preview */
  43. private $configMaxWidth;
  44. /** @var int maximum height allowed for a preview */
  45. private $configMaxHeight;
  46. //fileview object
  47. private $fileView = null;
  48. private $userView = null;
  49. //vars
  50. private $file;
  51. private $maxX;
  52. private $maxY;
  53. private $scalingUp;
  54. private $mimeType;
  55. private $keepAspect = false;
  56. private $mode = self::MODE_FILL;
  57. //used to calculate the size of the preview to generate
  58. /** @var int $maxPreviewWidth max width a preview can have */
  59. private $maxPreviewWidth;
  60. /** @var int $maxPreviewHeight max height a preview can have */
  61. private $maxPreviewHeight;
  62. /** @var int $previewWidth calculated width of the preview we're looking for */
  63. private $previewWidth;
  64. /** @var int $previewHeight calculated height of the preview we're looking for */
  65. private $previewHeight;
  66. //filemapper used for deleting previews
  67. // index is path, value is fileinfo
  68. static public $deleteFileMapper = array();
  69. static public $deleteChildrenMapper = array();
  70. /**
  71. * preview images object
  72. *
  73. * @var \OCP\IImage
  74. */
  75. private $preview;
  76. /**
  77. * @var \OCP\Files\FileInfo
  78. */
  79. protected $info;
  80. /**
  81. * check if thumbnail or bigger version of thumbnail of file is cached
  82. *
  83. * @param string $user userid - if no user is given, OC_User::getUser will be used
  84. * @param string $root path of root
  85. * @param string $file The path to the file where you want a thumbnail from
  86. * @param int $maxX The maximum X size of the thumbnail. It can be smaller depending on the
  87. * shape of the image
  88. * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the
  89. * shape of the image
  90. * @param bool $scalingUp Disable/Enable upscaling of previews
  91. *
  92. * @throws \Exception
  93. * @return mixed (bool / string)
  94. * false if thumbnail does not exist
  95. * path to thumbnail if thumbnail exists
  96. */
  97. public function __construct(
  98. $user = '',
  99. $root = '/',
  100. $file = '', $maxX = 1,
  101. $maxY = 1,
  102. $scalingUp = true
  103. ) {
  104. //init fileviews
  105. if ($user === '') {
  106. $user = \OC_User::getUser();
  107. }
  108. $this->fileView = new \OC\Files\View('/' . $user . '/' . $root);
  109. $this->userView = new \OC\Files\View('/' . $user);
  110. //set config
  111. $sysConfig = \OC::$server->getConfig();
  112. $this->configMaxWidth = $sysConfig->getSystemValue('preview_max_x', 2048);
  113. $this->configMaxHeight = $sysConfig->getSystemValue('preview_max_y', 2048);
  114. $this->maxScaleFactor = $sysConfig->getSystemValue('preview_max_scale_factor', 2);
  115. //save parameters
  116. $this->setFile($file);
  117. $this->setMaxX((int)$maxX);
  118. $this->setMaxY((int)$maxY);
  119. $this->setScalingUp($scalingUp);
  120. $this->preview = null;
  121. //check if there are preview backends
  122. if (!\OC::$server->getPreviewManager()
  123. ->hasProviders()
  124. && \OC::$server->getConfig()
  125. ->getSystemValue('enable_previews', true)
  126. ) {
  127. \OCP\Util::writeLog('core', 'No preview providers exist', \OCP\Util::ERROR);
  128. throw new \Exception('No preview providers');
  129. }
  130. }
  131. /**
  132. * returns the path of the file you want a thumbnail from
  133. *
  134. * @return string
  135. */
  136. public function getFile() {
  137. return $this->file;
  138. }
  139. /**
  140. * returns the max width of the preview
  141. *
  142. * @return integer
  143. */
  144. public function getMaxX() {
  145. return $this->maxX;
  146. }
  147. /**
  148. * returns the max height of the preview
  149. *
  150. * @return integer
  151. */
  152. public function getMaxY() {
  153. return $this->maxY;
  154. }
  155. /**
  156. * returns whether or not scalingup is enabled
  157. *
  158. * @return bool
  159. */
  160. public function getScalingUp() {
  161. return $this->scalingUp;
  162. }
  163. /**
  164. * returns the name of the thumbnailfolder
  165. *
  166. * @return string
  167. */
  168. public function getThumbnailsFolder() {
  169. return self::THUMBNAILS_FOLDER;
  170. }
  171. /**
  172. * returns the max scale factor
  173. *
  174. * @return string
  175. */
  176. public function getMaxScaleFactor() {
  177. return $this->maxScaleFactor;
  178. }
  179. /**
  180. * returns the max width set in ownCloud's config
  181. *
  182. * @return string
  183. */
  184. public function getConfigMaxX() {
  185. return $this->configMaxWidth;
  186. }
  187. /**
  188. * returns the max height set in ownCloud's config
  189. *
  190. * @return string
  191. */
  192. public function getConfigMaxY() {
  193. return $this->configMaxHeight;
  194. }
  195. /**
  196. * Returns the FileInfo object associated with the file to preview
  197. *
  198. * @return false|Files\FileInfo|\OCP\Files\FileInfo
  199. */
  200. protected function getFileInfo() {
  201. $absPath = $this->fileView->getAbsolutePath($this->file);
  202. $absPath = Files\Filesystem::normalizePath($absPath);
  203. if (array_key_exists($absPath, self::$deleteFileMapper)) {
  204. $this->info = self::$deleteFileMapper[$absPath];
  205. } else if (!$this->info) {
  206. $this->info = $this->fileView->getFileInfo($this->file);
  207. }
  208. return $this->info;
  209. }
  210. /**
  211. * @return array|null
  212. */
  213. private function getChildren() {
  214. $absPath = $this->fileView->getAbsolutePath($this->file);
  215. $absPath = Files\Filesystem::normalizePath($absPath);
  216. if (array_key_exists($absPath, self::$deleteChildrenMapper)) {
  217. return self::$deleteChildrenMapper[$absPath];
  218. }
  219. return null;
  220. }
  221. /**
  222. * Sets the path of the file you want a preview of
  223. *
  224. * @param string $file
  225. *
  226. * @return \OC\Preview
  227. */
  228. public function setFile($file) {
  229. $this->file = $file;
  230. $this->info = null;
  231. if ($file !== '') {
  232. $this->getFileInfo();
  233. if ($this->info instanceof \OCP\Files\FileInfo) {
  234. $this->mimeType = $this->info->getMimetype();
  235. }
  236. }
  237. return $this;
  238. }
  239. /**
  240. * Forces the use of a specific media type
  241. *
  242. * @param string $mimeType
  243. */
  244. public function setMimetype($mimeType) {
  245. $this->mimeType = $mimeType;
  246. }
  247. /**
  248. * Sets the max width of the preview. It's capped by the maximum allowed size set in the
  249. * configuration
  250. *
  251. * @param int $maxX
  252. *
  253. * @throws \Exception
  254. * @return \OC\Preview
  255. */
  256. public function setMaxX($maxX = 1) {
  257. if ($maxX <= 0) {
  258. throw new \Exception('Cannot set width of 0 or smaller!');
  259. }
  260. $configMaxX = $this->getConfigMaxX();
  261. $maxX = $this->limitMaxDim($maxX, $configMaxX, 'maxX');
  262. $this->maxX = $maxX;
  263. return $this;
  264. }
  265. /**
  266. * Sets the max height of the preview. It's capped by the maximum allowed size set in the
  267. * configuration
  268. *
  269. * @param int $maxY
  270. *
  271. * @throws \Exception
  272. * @return \OC\Preview
  273. */
  274. public function setMaxY($maxY = 1) {
  275. if ($maxY <= 0) {
  276. throw new \Exception('Cannot set height of 0 or smaller!');
  277. }
  278. $configMaxY = $this->getConfigMaxY();
  279. $maxY = $this->limitMaxDim($maxY, $configMaxY, 'maxY');
  280. $this->maxY = $maxY;
  281. return $this;
  282. }
  283. /**
  284. * Sets whether we're allowed to scale up when generating a preview. It's capped by the maximum
  285. * allowed scale factor set in the configuration
  286. *
  287. * @param bool $scalingUp
  288. *
  289. * @return \OC\Preview
  290. */
  291. public function setScalingup($scalingUp) {
  292. if ($this->getMaxScaleFactor() === 1) {
  293. $scalingUp = false;
  294. }
  295. $this->scalingUp = $scalingUp;
  296. return $this;
  297. }
  298. /**
  299. * Set whether to cover or fill the specified dimensions
  300. *
  301. * @param string $mode
  302. *
  303. * @return \OC\Preview
  304. */
  305. public function setMode($mode) {
  306. $this->mode = $mode;
  307. return $this;
  308. }
  309. /**
  310. * Sets whether we need to generate a preview which keeps the aspect ratio of the original file
  311. *
  312. * @param bool $keepAspect
  313. *
  314. * @return \OC\Preview
  315. */
  316. public function setKeepAspect($keepAspect) {
  317. $this->keepAspect = $keepAspect;
  318. return $this;
  319. }
  320. /**
  321. * Makes sure we were given a file to preview and that it exists in the filesystem
  322. *
  323. * @return bool
  324. */
  325. public function isFileValid() {
  326. $file = $this->getFile();
  327. if ($file === '') {
  328. \OCP\Util::writeLog('core', 'No filename passed', \OCP\Util::DEBUG);
  329. return false;
  330. }
  331. if (!$this->fileView->file_exists($file)) {
  332. \OCP\Util::writeLog('core', 'File:"' . $file . '" not found', \OCP\Util::DEBUG);
  333. return false;
  334. }
  335. return true;
  336. }
  337. /**
  338. * Deletes the preview of a file with specific width and height
  339. *
  340. * This should never delete the max preview, use deleteAllPreviews() instead
  341. *
  342. * @return bool
  343. */
  344. public function deletePreview() {
  345. $fileInfo = $this->getFileInfo();
  346. if ($fileInfo !== null && $fileInfo !== false) {
  347. $fileId = $fileInfo->getId();
  348. $previewPath = $this->buildCachePath($fileId);
  349. if (!strpos($previewPath, 'max')) {
  350. return $this->userView->unlink($previewPath);
  351. }
  352. }
  353. return false;
  354. }
  355. /**
  356. * Deletes all previews of a file
  357. */
  358. public function deleteAllPreviews() {
  359. $toDelete = $this->getChildren();
  360. $toDelete[] = $this->getFileInfo();
  361. foreach ($toDelete as $delete) {
  362. if ($delete instanceof FileInfo) {
  363. /** @var \OCP\Files\FileInfo $delete */
  364. $fileId = $delete->getId();
  365. // getId() might return null, e.g. when the file is a
  366. // .ocTransferId*.part file from chunked file upload.
  367. if (!empty($fileId)) {
  368. $previewPath = $this->getPreviewPath($fileId);
  369. $this->userView->deleteAll($previewPath);
  370. $this->userView->rmdir($previewPath);
  371. }
  372. }
  373. }
  374. }
  375. /**
  376. * Checks if a preview matching the asked dimensions or a bigger version is already cached
  377. *
  378. * * We first retrieve the size of the max preview since this is what we be used to create
  379. * all our preview. If it doesn't exist we return false, so that it can be generated
  380. * * Using the dimensions of the max preview, we calculate what the size of the new
  381. * thumbnail should be
  382. * * And finally, we look for a suitable candidate in the cache
  383. *
  384. * @param int $fileId fileId of the original file we need a preview of
  385. *
  386. * @return string|false path to the cached preview if it exists or false
  387. */
  388. public function isCached($fileId) {
  389. if (is_null($fileId)) {
  390. return false;
  391. }
  392. /**
  393. * Phase 1: Looking for the max preview
  394. */
  395. $previewPath = $this->getPreviewPath($fileId);
  396. // We currently can't look for a single file due to bugs related to #16478
  397. $allThumbnails = $this->userView->getDirectoryContent($previewPath);
  398. list($maxPreviewWidth, $maxPreviewHeight) = $this->getMaxPreviewSize($allThumbnails);
  399. // Only use the cache if we have a max preview
  400. if (!is_null($maxPreviewWidth) && !is_null($maxPreviewHeight)) {
  401. /**
  402. * Phase 2: Calculating the size of the preview we need to send back
  403. */
  404. $this->maxPreviewWidth = $maxPreviewWidth;
  405. $this->maxPreviewHeight = $maxPreviewHeight;
  406. list($previewWidth, $previewHeight) = $this->simulatePreviewDimensions();
  407. if (empty($previewWidth) || empty($previewHeight)) {
  408. return false;
  409. }
  410. $this->previewWidth = $previewWidth;
  411. $this->previewHeight = $previewHeight;
  412. /**
  413. * Phase 3: We look for a preview of the exact size
  414. */
  415. // This gives us a calculated path to a preview of asked dimensions
  416. // thumbnailFolder/fileId/<maxX>-<maxY>(-max|-with-aspect).png
  417. $preview = $this->buildCachePath($fileId, $previewWidth, $previewHeight);
  418. // This checks if we have a preview of those exact dimensions in the cache
  419. if ($this->userView->file_exists($preview)) {
  420. return $preview;
  421. }
  422. /**
  423. * Phase 4: We look for a larger preview, matching the aspect ratio
  424. */
  425. if (($this->getMaxX() >= $maxPreviewWidth)
  426. && ($this->getMaxY() >= $maxPreviewHeight)
  427. ) {
  428. // The preview we-re looking for is the exact size or larger than the max preview,
  429. // so return that
  430. return $this->buildCachePath($fileId, $maxPreviewWidth, $maxPreviewHeight);
  431. } else {
  432. // The last resort is to look for something bigger than what we've calculated,
  433. // but still smaller than the max preview
  434. return $this->isCachedBigger($fileId, $allThumbnails);
  435. }
  436. }
  437. return false;
  438. }
  439. /**
  440. * Returns the dimensions of the max preview
  441. *
  442. * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
  443. *
  444. * @return int[]
  445. */
  446. private function getMaxPreviewSize($allThumbnails) {
  447. $maxPreviewX = null;
  448. $maxPreviewY = null;
  449. foreach ($allThumbnails as $thumbnail) {
  450. $name = $thumbnail['name'];
  451. if (strpos($name, 'max')) {
  452. list($maxPreviewX, $maxPreviewY) = $this->getDimensionsFromFilename($name);
  453. break;
  454. }
  455. }
  456. return [$maxPreviewX, $maxPreviewY];
  457. }
  458. /**
  459. * Determines the size of the preview we should be looking for in the cache
  460. *
  461. * @return int[]
  462. */
  463. private function simulatePreviewDimensions() {
  464. $askedWidth = $this->getMaxX();
  465. $askedHeight = $this->getMaxY();
  466. if ($this->keepAspect) {
  467. list($newPreviewWidth, $newPreviewHeight) =
  468. $this->applyAspectRatio($askedWidth, $askedHeight);
  469. } else {
  470. list($newPreviewWidth, $newPreviewHeight) = $this->fixSize($askedWidth, $askedHeight);
  471. }
  472. return [(int)$newPreviewWidth, (int)$newPreviewHeight];
  473. }
  474. /**
  475. * Resizes the boundaries to match the aspect ratio
  476. *
  477. * @param int $askedWidth
  478. * @param int $askedHeight
  479. *
  480. * @param int $originalWidth
  481. * @param int $originalHeight
  482. * @return \int[]
  483. */
  484. private function applyAspectRatio($askedWidth, $askedHeight, $originalWidth = 0, $originalHeight = 0) {
  485. if(!$originalWidth){
  486. $originalWidth= $this->maxPreviewWidth;
  487. }
  488. if (!$originalHeight) {
  489. $originalHeight = $this->maxPreviewHeight;
  490. }
  491. $originalRatio = $originalWidth / $originalHeight;
  492. // Defines the box in which the preview has to fit
  493. $scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
  494. $askedWidth = min($askedWidth, $originalWidth * $scaleFactor);
  495. $askedHeight = min($askedHeight, $originalHeight * $scaleFactor);
  496. if ($askedWidth / $originalRatio < $askedHeight) {
  497. // width restricted
  498. $askedHeight = round($askedWidth / $originalRatio);
  499. } else {
  500. $askedWidth = round($askedHeight * $originalRatio);
  501. }
  502. return [(int)$askedWidth, (int)$askedHeight];
  503. }
  504. /**
  505. * Resizes the boundaries to cover the area
  506. *
  507. * @param int $askedWidth
  508. * @param int $askedHeight
  509. * @param int $previewWidth
  510. * @param int $previewHeight
  511. * @return \int[]
  512. */
  513. private function applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight) {
  514. $originalRatio = $previewWidth / $previewHeight;
  515. // Defines the box in which the preview has to fit
  516. $scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1;
  517. $askedWidth = min($askedWidth, $previewWidth * $scaleFactor);
  518. $askedHeight = min($askedHeight, $previewHeight * $scaleFactor);
  519. if ($askedWidth / $originalRatio > $askedHeight) {
  520. // height restricted
  521. $askedHeight = round($askedWidth / $originalRatio);
  522. } else {
  523. $askedWidth = round($askedHeight * $originalRatio);
  524. }
  525. return [(int)$askedWidth, (int)$askedHeight];
  526. }
  527. /**
  528. * Makes sure an upscaled preview doesn't end up larger than the max dimensions defined in the
  529. * config
  530. *
  531. * @param int $askedWidth
  532. * @param int $askedHeight
  533. *
  534. * @return \int[]
  535. */
  536. private function fixSize($askedWidth, $askedHeight) {
  537. if ($this->scalingUp) {
  538. $askedWidth = min($this->configMaxWidth, $askedWidth);
  539. $askedHeight = min($this->configMaxHeight, $askedHeight);
  540. }
  541. return [(int)$askedWidth, (int)$askedHeight];
  542. }
  543. /**
  544. * Checks if a bigger version of a file preview is cached and if not
  545. * return the preview of max allowed dimensions
  546. *
  547. * @param int $fileId fileId of the original image
  548. * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
  549. *
  550. * @return string path to bigger thumbnail
  551. */
  552. private function isCachedBigger($fileId, $allThumbnails) {
  553. // This is used to eliminate any thumbnail narrower than what we need
  554. $maxX = $this->getMaxX();
  555. //array for usable cached thumbnails
  556. $possibleThumbnails = $this->getPossibleThumbnails($allThumbnails);
  557. foreach ($possibleThumbnails as $width => $path) {
  558. if ($width < $maxX) {
  559. continue;
  560. } else {
  561. return $path;
  562. }
  563. }
  564. // At this stage, we didn't find a preview, so we return the max preview
  565. return $this->buildCachePath($fileId, $this->maxPreviewWidth, $this->maxPreviewHeight);
  566. }
  567. /**
  568. * Get possible bigger thumbnails of the given image with the proper aspect ratio
  569. *
  570. * @param FileInfo[] $allThumbnails the list of all our cached thumbnails
  571. *
  572. * @return string[] an array of paths to bigger thumbnails
  573. */
  574. private function getPossibleThumbnails($allThumbnails) {
  575. if ($this->keepAspect) {
  576. $wantedAspectRatio = (float)($this->maxPreviewWidth / $this->maxPreviewHeight);
  577. } else {
  578. $wantedAspectRatio = (float)($this->getMaxX() / $this->getMaxY());
  579. }
  580. //array for usable cached thumbnails
  581. $possibleThumbnails = array();
  582. foreach ($allThumbnails as $thumbnail) {
  583. $name = rtrim($thumbnail['name'], '.png');
  584. list($x, $y, $aspectRatio) = $this->getDimensionsFromFilename($name);
  585. if (abs($aspectRatio - $wantedAspectRatio) >= 0.000001
  586. || $this->unscalable($x, $y)
  587. ) {
  588. continue;
  589. }
  590. $possibleThumbnails[$x] = $thumbnail['path'];
  591. }
  592. ksort($possibleThumbnails);
  593. return $possibleThumbnails;
  594. }
  595. /**
  596. * Looks at the preview filename from the cache and extracts the size of the preview
  597. *
  598. * @param string $name
  599. *
  600. * @return array<int,int,float>
  601. */
  602. private function getDimensionsFromFilename($name) {
  603. $size = explode('-', $name);
  604. $x = (int)$size[0];
  605. $y = (int)$size[1];
  606. $aspectRatio = (float)($x / $y);
  607. return array($x, $y, $aspectRatio);
  608. }
  609. /**
  610. * @param int $x
  611. * @param int $y
  612. *
  613. * @return bool
  614. */
  615. private function unscalable($x, $y) {
  616. $maxX = $this->getMaxX();
  617. $maxY = $this->getMaxY();
  618. $scalingUp = $this->getScalingUp();
  619. $maxScaleFactor = $this->getMaxScaleFactor();
  620. if ($x < $maxX || $y < $maxY) {
  621. if ($scalingUp) {
  622. $scaleFactor = $maxX / $x;
  623. if ($scaleFactor > $maxScaleFactor) {
  624. return true;
  625. }
  626. } else {
  627. return true;
  628. }
  629. }
  630. return false;
  631. }
  632. /**
  633. * Returns a preview of a file
  634. *
  635. * The cache is searched first and if nothing usable was found then a preview is
  636. * generated by one of the providers
  637. *
  638. * @return \OCP\IImage
  639. */
  640. public function getPreview() {
  641. if (!is_null($this->preview) && $this->preview->valid()) {
  642. return $this->preview;
  643. }
  644. $this->preview = null;
  645. $fileInfo = $this->getFileInfo();
  646. if ($fileInfo === null || $fileInfo === false) {
  647. return new \OC_Image();
  648. }
  649. $fileId = $fileInfo->getId();
  650. $cached = $this->isCached($fileId);
  651. if ($cached) {
  652. $this->getCachedPreview($fileId, $cached);
  653. }
  654. if (is_null($this->preview)) {
  655. $this->generatePreview($fileId);
  656. }
  657. // We still don't have a preview, so we send back an empty object
  658. if (is_null($this->preview)) {
  659. $this->preview = new \OC_Image();
  660. }
  661. return $this->preview;
  662. }
  663. /**
  664. * Sends the preview, including the headers to client which requested it
  665. *
  666. * @param null|string $mimeTypeForHeaders the media type to use when sending back the reply
  667. *
  668. * @throws NotFoundException
  669. */
  670. public function showPreview($mimeTypeForHeaders = null) {
  671. // Check if file is valid
  672. if ($this->isFileValid() === false) {
  673. throw new NotFoundException('File not found.');
  674. }
  675. if (is_null($this->preview)) {
  676. $this->getPreview();
  677. }
  678. if ($this->preview instanceof \OCP\IImage) {
  679. if ($this->preview->valid()) {
  680. \OCP\Response::enableCaching(3600 * 24); // 24 hours
  681. } else {
  682. $this->getMimeIcon();
  683. }
  684. $this->preview->show($mimeTypeForHeaders);
  685. }
  686. }
  687. /**
  688. * Retrieves the preview from the cache and resizes it if necessary
  689. *
  690. * @param int $fileId fileId of the original image
  691. * @param string $cached the path to the cached preview
  692. */
  693. private function getCachedPreview($fileId, $cached) {
  694. $stream = $this->userView->fopen($cached, 'r');
  695. $this->preview = null;
  696. if ($stream) {
  697. $image = new \OC_Image();
  698. $image->loadFromFileHandle($stream);
  699. $this->preview = $image->valid() ? $image : null;
  700. if (!is_null($this->preview)) {
  701. // Size of the preview we calculated
  702. $maxX = $this->previewWidth;
  703. $maxY = $this->previewHeight;
  704. // Size of the preview we retrieved from the cache
  705. $previewX = (int)$this->preview->width();
  706. $previewY = (int)$this->preview->height();
  707. // We don't have an exact match
  708. if ($previewX !== $maxX || $previewY !== $maxY) {
  709. $this->resizeAndStore($fileId);
  710. }
  711. }
  712. fclose($stream);
  713. }
  714. }
  715. /**
  716. * Resizes, crops, fixes orientation and stores in the cache
  717. *
  718. * @param int $fileId fileId of the original image
  719. */
  720. private function resizeAndStore($fileId) {
  721. $image = $this->preview;
  722. if (!($image instanceof \OCP\IImage)) {
  723. \OCP\Util::writeLog(
  724. 'core', '$this->preview is not an instance of \OCP\IImage', \OCP\Util::DEBUG
  725. );
  726. return;
  727. }
  728. $previewWidth = (int)$image->width();
  729. $previewHeight = (int)$image->height();
  730. $askedWidth = $this->getMaxX();
  731. $askedHeight = $this->getMaxY();
  732. /**
  733. * Phase 1: If required, adjust boundaries to keep aspect ratio
  734. */
  735. if ($this->keepAspect) {
  736. list($askedWidth, $askedHeight) =
  737. $this->applyAspectRatio($askedWidth, $askedHeight, $previewWidth, $previewHeight);
  738. }
  739. if ($this->mode === self::MODE_COVER) {
  740. list($scaleWidth, $scaleHeight) =
  741. $this->applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight);
  742. } else {
  743. $scaleWidth = $askedWidth;
  744. $scaleHeight = $askedHeight;
  745. }
  746. /**
  747. * Phase 2: Resizes preview to try and match requirements.
  748. * Takes the scaling ratio into consideration
  749. */
  750. list($newPreviewWidth, $newPreviewHeight) = $this->scale(
  751. $image, $scaleWidth, $scaleHeight, $previewWidth, $previewHeight
  752. );
  753. // The preview has been resized and should now have the asked dimensions
  754. if ($newPreviewWidth === $askedWidth && $newPreviewHeight === $askedHeight) {
  755. $this->storePreview($fileId, $newPreviewWidth, $newPreviewHeight);
  756. return;
  757. }
  758. /**
  759. * Phase 3: We're still not there yet, so we're clipping and filling
  760. * to match the asked dimensions
  761. */
  762. // It turns out the scaled preview is now too big, so we crop the image
  763. if ($newPreviewWidth >= $askedWidth && $newPreviewHeight >= $askedHeight) {
  764. $this->crop($image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight);
  765. $this->storePreview($fileId, $askedWidth, $askedHeight);
  766. return;
  767. }
  768. // At least one dimension of the scaled preview is too small,
  769. // so we fill the space with a transparent background
  770. if (($newPreviewWidth < $askedWidth || $newPreviewHeight < $askedHeight)) {
  771. $this->cropAndFill(
  772. $image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight
  773. );
  774. $this->storePreview($fileId, $askedWidth, $askedHeight);
  775. return;
  776. }
  777. // The preview is smaller, but we can't touch it
  778. $this->storePreview($fileId, $newPreviewWidth, $newPreviewHeight);
  779. }
  780. /**
  781. * Calculates the new dimensions of the preview
  782. *
  783. * The new dimensions can be larger or smaller than the ones of the preview we have to resize
  784. *
  785. * @param \OCP\IImage $image
  786. * @param int $askedWidth
  787. * @param int $askedHeight
  788. * @param int $previewWidth
  789. * @param null $previewHeight
  790. *
  791. * @return int[]
  792. */
  793. private function scale($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
  794. $scalingUp = $this->getScalingUp();
  795. $maxScaleFactor = $this->getMaxScaleFactor();
  796. $factorX = $askedWidth / $previewWidth;
  797. $factorY = $askedHeight / $previewHeight;
  798. if ($factorX >= $factorY) {
  799. $factor = $factorX;
  800. } else {
  801. $factor = $factorY;
  802. }
  803. if ($scalingUp === false) {
  804. if ($factor > 1) {
  805. $factor = 1;
  806. }
  807. }
  808. // We cap when upscaling
  809. if (!is_null($maxScaleFactor)) {
  810. if ($factor > $maxScaleFactor) {
  811. \OCP\Util::writeLog(
  812. 'core', 'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor,
  813. \OCP\Util::DEBUG
  814. );
  815. $factor = $maxScaleFactor;
  816. }
  817. }
  818. $newPreviewWidth = round($previewWidth * $factor);
  819. $newPreviewHeight = round($previewHeight * $factor);
  820. $image->preciseResize($newPreviewWidth, $newPreviewHeight);
  821. $this->preview = $image;
  822. return [$newPreviewWidth, $newPreviewHeight];
  823. }
  824. /**
  825. * Crops a preview which is larger than the dimensions we've received
  826. *
  827. * @param \OCP\IImage $image
  828. * @param int $askedWidth
  829. * @param int $askedHeight
  830. * @param int $previewWidth
  831. * @param null $previewHeight
  832. */
  833. private function crop($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight = null) {
  834. $cropX = floor(abs($askedWidth - $previewWidth) * 0.5);
  835. //don't crop previews on the Y axis, this sucks if it's a document.
  836. //$cropY = floor(abs($y - $newPreviewHeight) * 0.5);
  837. $cropY = 0;
  838. $image->crop($cropX, $cropY, $askedWidth, $askedHeight);
  839. $this->preview = $image;
  840. }
  841. /**
  842. * Crops an image if it's larger than the dimensions we've received and fills the empty space
  843. * with a transparent background
  844. *
  845. * @param \OCP\IImage $image
  846. * @param int $askedWidth
  847. * @param int $askedHeight
  848. * @param int $previewWidth
  849. * @param null $previewHeight
  850. */
  851. private function cropAndFill($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) {
  852. if ($previewWidth > $askedWidth) {
  853. $cropX = floor(($previewWidth - $askedWidth) * 0.5);
  854. $image->crop($cropX, 0, $askedWidth, $previewHeight);
  855. $previewWidth = $askedWidth;
  856. }
  857. if ($previewHeight > $askedHeight) {
  858. $cropY = floor(($previewHeight - $askedHeight) * 0.5);
  859. $image->crop(0, $cropY, $previewWidth, $askedHeight);
  860. $previewHeight = $askedHeight;
  861. }
  862. // Creates a transparent background
  863. $backgroundLayer = imagecreatetruecolor($askedWidth, $askedHeight);
  864. imagealphablending($backgroundLayer, false);
  865. $transparency = imagecolorallocatealpha($backgroundLayer, 0, 0, 0, 127);
  866. imagefill($backgroundLayer, 0, 0, $transparency);
  867. imagesavealpha($backgroundLayer, true);
  868. $image = $image->resource();
  869. $mergeX = floor(abs($askedWidth - $previewWidth) * 0.5);
  870. $mergeY = floor(abs($askedHeight - $previewHeight) * 0.5);
  871. // Pastes the preview on top of the background
  872. imagecopy(
  873. $backgroundLayer, $image, $mergeX, $mergeY, 0, 0, $previewWidth,
  874. $previewHeight
  875. );
  876. $image = new \OC_Image($backgroundLayer);
  877. $this->preview = $image;
  878. }
  879. /**
  880. * Saves a preview in the cache to speed up future calls
  881. *
  882. * Do not nullify the preview as it might send the whole process in a loop
  883. *
  884. * @param int $fileId fileId of the original image
  885. * @param int $previewWidth
  886. * @param int $previewHeight
  887. */
  888. private function storePreview($fileId, $previewWidth, $previewHeight) {
  889. if (empty($previewWidth) || empty($previewHeight)) {
  890. \OCP\Util::writeLog(
  891. 'core', 'Cannot save preview of dimension ' . $previewWidth . 'x' . $previewHeight,
  892. \OCP\Util::DEBUG
  893. );
  894. } else {
  895. $cachePath = $this->buildCachePath($fileId, $previewWidth, $previewHeight);
  896. $this->userView->file_put_contents($cachePath, $this->preview->data());
  897. }
  898. }
  899. /**
  900. * Returns the path to a preview based on its dimensions and aspect
  901. *
  902. * @param int $fileId
  903. * @param int|null $maxX
  904. * @param int|null $maxY
  905. *
  906. * @return string
  907. */
  908. private function buildCachePath($fileId, $maxX = null, $maxY = null) {
  909. if (is_null($maxX)) {
  910. $maxX = $this->getMaxX();
  911. }
  912. if (is_null($maxY)) {
  913. $maxY = $this->getMaxY();
  914. }
  915. $previewPath = $this->getPreviewPath($fileId);
  916. $previewPath = $previewPath . strval($maxX) . '-' . strval($maxY);
  917. $isMaxPreview =
  918. ($maxX === $this->maxPreviewWidth && $maxY === $this->maxPreviewHeight) ? true : false;
  919. if ($isMaxPreview) {
  920. $previewPath .= '-max';
  921. }
  922. if ($this->keepAspect && !$isMaxPreview) {
  923. $previewPath .= '-with-aspect';
  924. }
  925. if ($this->mode === self::MODE_COVER) {
  926. $previewPath .= '-cover';
  927. }
  928. $previewPath .= '.png';
  929. return $previewPath;
  930. }
  931. /**
  932. * Returns the path to the folder where the previews are stored, identified by the fileId
  933. *
  934. * @param int $fileId
  935. *
  936. * @return string
  937. */
  938. private function getPreviewPath($fileId) {
  939. return $this->getThumbnailsFolder() . '/' . $fileId . '/';
  940. }
  941. /**
  942. * Asks the provider to send a preview of the file which respects the maximum dimensions
  943. * defined in the configuration and after saving it in the cache, it is then resized to the
  944. * asked dimensions
  945. *
  946. * This is only called once in order to generate a large PNG of dimensions defined in the
  947. * configuration file. We'll be able to quickly resize it later on.
  948. * We never upscale the original conversion as this will be done later by the resizing
  949. * operation
  950. *
  951. * @param int $fileId fileId of the original image
  952. */
  953. private function generatePreview($fileId) {
  954. $file = $this->getFile();
  955. $preview = null;
  956. $previewProviders = \OC::$server->getPreviewManager()
  957. ->getProviders();
  958. foreach ($previewProviders as $supportedMimeType => $providers) {
  959. if (!preg_match($supportedMimeType, $this->mimeType)) {
  960. continue;
  961. }
  962. foreach ($providers as $closure) {
  963. $provider = $closure();
  964. if (!($provider instanceof \OCP\Preview\IProvider)) {
  965. continue;
  966. }
  967. \OCP\Util::writeLog(
  968. 'core', 'Generating preview for "' . $file . '" with "' . get_class($provider)
  969. . '"', \OCP\Util::DEBUG
  970. );
  971. /** @var $provider Provider */
  972. $preview = $provider->getThumbnail(
  973. $file, $this->configMaxWidth, $this->configMaxHeight, $scalingUp = false,
  974. $this->fileView
  975. );
  976. if (!($preview instanceof \OCP\IImage)) {
  977. continue;
  978. }
  979. $this->preview = $preview;
  980. $previewPath = $this->getPreviewPath($fileId);
  981. if ($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) {
  982. $this->userView->mkdir($this->getThumbnailsFolder() . '/');
  983. }
  984. if ($this->userView->is_dir($previewPath) === false) {
  985. $this->userView->mkdir($previewPath);
  986. }
  987. // This stores our large preview so that it can be used in subsequent resizing requests
  988. $this->storeMaxPreview($previewPath);
  989. break 2;
  990. }
  991. }
  992. // The providers have been kind enough to give us a preview
  993. if ($preview) {
  994. $this->resizeAndStore($fileId);
  995. }
  996. }
  997. /**
  998. * Defines the media icon, for the media type of the original file, as the preview
  999. */
  1000. private function getMimeIcon() {
  1001. $image = new \OC_Image();
  1002. $mimeIconWebPath = \OC_Helper::mimetypeIcon($this->mimeType);
  1003. if (empty(\OC::$WEBROOT)) {
  1004. $mimeIconServerPath = \OC::$SERVERROOT . $mimeIconWebPath;
  1005. } else {
  1006. $mimeIconServerPath = str_replace(\OC::$WEBROOT, \OC::$SERVERROOT, $mimeIconWebPath);
  1007. }
  1008. $image->loadFromFile($mimeIconServerPath);
  1009. $this->preview = $image;
  1010. }
  1011. /**
  1012. * Stores the max preview in the cache
  1013. *
  1014. * @param string $previewPath path to the preview
  1015. */
  1016. private function storeMaxPreview($previewPath) {
  1017. $maxPreviewExists = false;
  1018. $preview = $this->preview;
  1019. $allThumbnails = $this->userView->getDirectoryContent($previewPath);
  1020. // This is so that the cache doesn't need emptying when upgrading
  1021. // Can be replaced by an upgrade script...
  1022. foreach ($allThumbnails as $thumbnail) {
  1023. $name = rtrim($thumbnail['name'], '.png');
  1024. if (strpos($name, 'max')) {
  1025. $maxPreviewExists = true;
  1026. break;
  1027. }
  1028. }
  1029. // We haven't found the max preview, so we create it
  1030. if (!$maxPreviewExists) {
  1031. $previewWidth = $preview->width();
  1032. $previewHeight = $preview->height();
  1033. $previewPath = $previewPath . strval($previewWidth) . '-' . strval($previewHeight);
  1034. $previewPath .= '-max.png';
  1035. $this->userView->file_put_contents($previewPath, $preview->data());
  1036. $this->maxPreviewWidth = $previewWidth;
  1037. $this->maxPreviewHeight = $previewHeight;
  1038. }
  1039. }
  1040. /**
  1041. * Limits a dimension to the maximum dimension provided as argument
  1042. *
  1043. * @param int $dim
  1044. * @param int $maxDim
  1045. * @param string $dimName
  1046. *
  1047. * @return mixed
  1048. */
  1049. private function limitMaxDim($dim, $maxDim, $dimName) {
  1050. if (!is_null($maxDim)) {
  1051. if ($dim > $maxDim) {
  1052. \OCP\Util::writeLog(
  1053. 'core', $dimName . ' reduced from ' . $dim . ' to ' . $maxDim, \OCP\Util::DEBUG
  1054. );
  1055. $dim = $maxDim;
  1056. }
  1057. }
  1058. return $dim;
  1059. }
  1060. /**
  1061. * @param array $args
  1062. */
  1063. public static function post_write($args) {
  1064. self::post_delete($args, 'files/');
  1065. }
  1066. /**
  1067. * @param array $args
  1068. */
  1069. public static function prepare_delete_files($args) {
  1070. self::prepare_delete($args, 'files/');
  1071. }
  1072. /**
  1073. * @param array $args
  1074. * @param string $prefix
  1075. */
  1076. public static function prepare_delete($args, $prefix = '') {
  1077. $path = $args['path'];
  1078. if (substr($path, 0, 1) === '/') {
  1079. $path = substr($path, 1);
  1080. }
  1081. $view = new \OC\Files\View('/' . \OC_User::getUser() . '/' . $prefix);
  1082. $absPath = Files\Filesystem::normalizePath($view->getAbsolutePath($path));
  1083. self::addPathToDeleteFileMapper($absPath, $view->getFileInfo($path));
  1084. if ($view->is_dir($path)) {
  1085. $children = self::getAllChildren($view, $path);
  1086. self::$deleteChildrenMapper[$absPath] = $children;
  1087. }
  1088. }
  1089. /**
  1090. * @param string $absolutePath
  1091. * @param \OCP\Files\FileInfo $info
  1092. */
  1093. private static function addPathToDeleteFileMapper($absolutePath, $info) {
  1094. self::$deleteFileMapper[$absolutePath] = $info;
  1095. }
  1096. /**
  1097. * @param \OC\Files\View $view
  1098. * @param string $path
  1099. *
  1100. * @return array
  1101. */
  1102. private static function getAllChildren($view, $path) {
  1103. $children = $view->getDirectoryContent($path);
  1104. $childrensFiles = array();
  1105. $fakeRootLength = strlen($view->getRoot());
  1106. for ($i = 0; $i < count($children); $i++) {
  1107. $child = $children[$i];
  1108. $childsPath = substr($child->getPath(), $fakeRootLength);
  1109. if ($view->is_dir($childsPath)) {
  1110. $children = array_merge(
  1111. $children,
  1112. $view->getDirectoryContent($childsPath)
  1113. );
  1114. } else {
  1115. $childrensFiles[] = $child;
  1116. }
  1117. }
  1118. return $childrensFiles;
  1119. }
  1120. /**
  1121. * @param array $args
  1122. */
  1123. public static function post_delete_files($args) {
  1124. self::post_delete($args, 'files/');
  1125. }
  1126. /**
  1127. * @param array $args
  1128. * @param string $prefix
  1129. */
  1130. public static function post_delete($args, $prefix = '') {
  1131. $path = Files\Filesystem::normalizePath($args['path']);
  1132. $preview = new Preview(\OC_User::getUser(), $prefix, $path);
  1133. $preview->deleteAllPreviews();
  1134. }
  1135. }