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

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