preview.php 33 KB

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