Generator.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
  4. *
  5. * @author Roeland Jago Douma <roeland@famdouma.nl>
  6. *
  7. * @license GNU AGPL version 3 or any later version
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. */
  23. namespace OC\Preview;
  24. use OCP\Files\File;
  25. use OCP\Files\IAppData;
  26. use OCP\Files\NotFoundException;
  27. use OCP\Files\NotPermittedException;
  28. use OCP\Files\SimpleFS\ISimpleFile;
  29. use OCP\Files\SimpleFS\ISimpleFolder;
  30. use OCP\IConfig;
  31. use OCP\IImage;
  32. use OCP\IPreview;
  33. use OCP\Preview\IProvider;
  34. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  35. use Symfony\Component\EventDispatcher\GenericEvent;
  36. class Generator {
  37. /** @var IPreview */
  38. private $previewManager;
  39. /** @var IConfig */
  40. private $config;
  41. /** @var IAppData */
  42. private $appData;
  43. /** @var GeneratorHelper */
  44. private $helper;
  45. /** @var EventDispatcherInterface */
  46. private $eventDispatcher;
  47. /**
  48. * @param IConfig $config
  49. * @param IPreview $previewManager
  50. * @param IAppData $appData
  51. * @param GeneratorHelper $helper
  52. * @param EventDispatcherInterface $eventDispatcher
  53. */
  54. public function __construct(
  55. IConfig $config,
  56. IPreview $previewManager,
  57. IAppData $appData,
  58. GeneratorHelper $helper,
  59. EventDispatcherInterface $eventDispatcher
  60. ) {
  61. $this->config = $config;
  62. $this->previewManager = $previewManager;
  63. $this->appData = $appData;
  64. $this->helper = $helper;
  65. $this->eventDispatcher = $eventDispatcher;
  66. }
  67. /**
  68. * Returns a preview of a file
  69. *
  70. * The cache is searched first and if nothing usable was found then a preview is
  71. * generated by one of the providers
  72. *
  73. * @param File $file
  74. * @param int $width
  75. * @param int $height
  76. * @param bool $crop
  77. * @param string $mode
  78. * @param string $mimeType
  79. * @return ISimpleFile
  80. * @throws NotFoundException
  81. * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
  82. */
  83. public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) {
  84. //Make sure that we can read the file
  85. if (!$file->isReadable()) {
  86. throw new NotFoundException('Cannot read file');
  87. }
  88. $this->eventDispatcher->dispatch(
  89. IPreview::EVENT,
  90. new GenericEvent($file,[
  91. 'width' => $width,
  92. 'height' => $height,
  93. 'crop' => $crop,
  94. 'mode' => $mode
  95. ])
  96. );
  97. if ($mimeType === null) {
  98. $mimeType = $file->getMimeType();
  99. }
  100. if (!$this->previewManager->isMimeSupported($mimeType)) {
  101. throw new NotFoundException();
  102. }
  103. $previewFolder = $this->getPreviewFolder($file);
  104. // Get the max preview and infer the max preview sizes from that
  105. $maxPreview = $this->getMaxPreview($previewFolder, $file, $mimeType);
  106. list($maxWidth, $maxHeight) = $this->getPreviewSize($maxPreview);
  107. // Calculate the preview size
  108. list($width, $height) = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
  109. // No need to generate a preview that is just the max preview
  110. if ($width === $maxWidth && $height === $maxHeight) {
  111. return $maxPreview;
  112. }
  113. // Try to get a cached preview. Else generate (and store) one
  114. try {
  115. try {
  116. $file = $this->getCachedPreview($previewFolder, $width, $height, $crop, $maxPreview->getMimeType());
  117. } catch (NotFoundException $e) {
  118. $file = $this->generatePreview($previewFolder, $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight);
  119. }
  120. } catch (\InvalidArgumentException $e) {
  121. throw new NotFoundException();
  122. }
  123. return $file;
  124. }
  125. /**
  126. * @param ISimpleFolder $previewFolder
  127. * @param File $file
  128. * @param string $mimeType
  129. * @return ISimpleFile
  130. * @throws NotFoundException
  131. */
  132. private function getMaxPreview(ISimpleFolder $previewFolder, File $file, $mimeType) {
  133. $nodes = $previewFolder->getDirectoryListing();
  134. foreach ($nodes as $node) {
  135. if (strpos($node->getName(), 'max')) {
  136. return $node;
  137. }
  138. }
  139. $previewProviders = $this->previewManager->getProviders();
  140. foreach ($previewProviders as $supportedMimeType => $providers) {
  141. if (!preg_match($supportedMimeType, $mimeType)) {
  142. continue;
  143. }
  144. foreach ($providers as $provider) {
  145. $provider = $this->helper->getProvider($provider);
  146. if (!($provider instanceof IProvider)) {
  147. continue;
  148. }
  149. $maxWidth = (int)$this->config->getSystemValue('preview_max_x', 2048);
  150. $maxHeight = (int)$this->config->getSystemValue('preview_max_y', 2048);
  151. $preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight);
  152. if (!($preview instanceof IImage)) {
  153. continue;
  154. }
  155. // Try to get the extention.
  156. try {
  157. $ext = $this->getExtention($preview->dataMimeType());
  158. } catch (\InvalidArgumentException $e) {
  159. // Just continue to the next iteration if this preview doesn't have a valid mimetype
  160. continue;
  161. }
  162. $path = (string)$preview->width() . '-' . (string)$preview->height() . '-max.' . $ext;
  163. try {
  164. $file = $previewFolder->newFile($path);
  165. $file->putContent($preview->data());
  166. } catch (NotPermittedException $e) {
  167. throw new NotFoundException();
  168. }
  169. return $file;
  170. }
  171. }
  172. throw new NotFoundException();
  173. }
  174. /**
  175. * @param ISimpleFile $file
  176. * @return int[]
  177. */
  178. private function getPreviewSize(ISimpleFile $file) {
  179. $size = explode('-', $file->getName());
  180. return [(int)$size[0], (int)$size[1]];
  181. }
  182. /**
  183. * @param int $width
  184. * @param int $height
  185. * @param bool $crop
  186. * @param string $mimeType
  187. * @return string
  188. */
  189. private function generatePath($width, $height, $crop, $mimeType) {
  190. $path = (string)$width . '-' . (string)$height;
  191. if ($crop) {
  192. $path .= '-crop';
  193. }
  194. $ext = $this->getExtention($mimeType);
  195. $path .= '.' . $ext;
  196. return $path;
  197. }
  198. /**
  199. * @param int $width
  200. * @param int $height
  201. * @param bool $crop
  202. * @param string $mode
  203. * @param int $maxWidth
  204. * @param int $maxHeight
  205. * @return int[]
  206. */
  207. private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) {
  208. /*
  209. * If we are not cropping we have to make sure the requested image
  210. * respects the aspect ratio of the original.
  211. */
  212. if (!$crop) {
  213. $ratio = $maxHeight / $maxWidth;
  214. if ($width === -1) {
  215. $width = $height / $ratio;
  216. }
  217. if ($height === -1) {
  218. $height = $width * $ratio;
  219. }
  220. $ratioH = $height / $maxHeight;
  221. $ratioW = $width / $maxWidth;
  222. /*
  223. * Fill means that the $height and $width are the max
  224. * Cover means min.
  225. */
  226. if ($mode === IPreview::MODE_FILL) {
  227. if ($ratioH > $ratioW) {
  228. $height = $width * $ratio;
  229. } else {
  230. $width = $height / $ratio;
  231. }
  232. } else if ($mode === IPreview::MODE_COVER) {
  233. if ($ratioH > $ratioW) {
  234. $width = $height / $ratio;
  235. } else {
  236. $height = $width * $ratio;
  237. }
  238. }
  239. }
  240. if ($height !== $maxHeight && $width !== $maxWidth) {
  241. /*
  242. * Scale to the nearest power of two
  243. */
  244. $pow2height = 2 ** ceil(log($height) / log(2));
  245. $pow2width = 2 ** ceil(log($width) / log(2));
  246. $ratioH = $height / $pow2height;
  247. $ratioW = $width / $pow2width;
  248. if ($ratioH < $ratioW) {
  249. $width = $pow2width;
  250. $height /= $ratioW;
  251. } else {
  252. $height = $pow2height;
  253. $width /= $ratioH;
  254. }
  255. }
  256. /*
  257. * Make sure the requested height and width fall within the max
  258. * of the preview.
  259. */
  260. if ($height > $maxHeight) {
  261. $ratio = $height / $maxHeight;
  262. $height = $maxHeight;
  263. $width /= $ratio;
  264. }
  265. if ($width > $maxWidth) {
  266. $ratio = $width / $maxWidth;
  267. $width = $maxWidth;
  268. $height /= $ratio;
  269. }
  270. return [(int)round($width), (int)round($height)];
  271. }
  272. /**
  273. * @param ISimpleFolder $previewFolder
  274. * @param ISimpleFile $maxPreview
  275. * @param int $width
  276. * @param int $height
  277. * @param bool $crop
  278. * @param int $maxWidth
  279. * @param int $maxHeight
  280. * @return ISimpleFile
  281. * @throws NotFoundException
  282. * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
  283. */
  284. private function generatePreview(ISimpleFolder $previewFolder, ISimpleFile $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight) {
  285. $preview = $this->helper->getImage($maxPreview);
  286. if (!$preview->valid()) {
  287. throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
  288. }
  289. if ($crop) {
  290. if ($height !== $preview->height() && $width !== $preview->width()) {
  291. //Resize
  292. $widthR = $preview->width() / $width;
  293. $heightR = $preview->height() / $height;
  294. if ($widthR > $heightR) {
  295. $scaleH = $height;
  296. $scaleW = $maxWidth / $heightR;
  297. } else {
  298. $scaleH = $maxHeight / $widthR;
  299. $scaleW = $width;
  300. }
  301. $preview->preciseResize(round($scaleW), round($scaleH));
  302. }
  303. $cropX = floor(abs($width - $preview->width()) * 0.5);
  304. $cropY = 0;
  305. $preview->crop($cropX, $cropY, $width, $height);
  306. } else {
  307. $preview->resize(max($width, $height));
  308. }
  309. $path = $this->generatePath($width, $height, $crop, $preview->dataMimeType());
  310. try {
  311. $file = $previewFolder->newFile($path);
  312. $file->putContent($preview->data());
  313. } catch (NotPermittedException $e) {
  314. throw new NotFoundException();
  315. }
  316. return $file;
  317. }
  318. /**
  319. * @param ISimpleFolder $previewFolder
  320. * @param int $width
  321. * @param int $height
  322. * @param bool $crop
  323. * @param string $mimeType
  324. * @return ISimpleFile
  325. *
  326. * @throws NotFoundException
  327. */
  328. private function getCachedPreview(ISimpleFolder $previewFolder, $width, $height, $crop, $mimeType) {
  329. $path = $this->generatePath($width, $height, $crop, $mimeType);
  330. return $previewFolder->getFile($path);
  331. }
  332. /**
  333. * Get the specific preview folder for this file
  334. *
  335. * @param File $file
  336. * @return ISimpleFolder
  337. */
  338. private function getPreviewFolder(File $file) {
  339. try {
  340. $folder = $this->appData->getFolder($file->getId());
  341. } catch (NotFoundException $e) {
  342. $folder = $this->appData->newFolder($file->getId());
  343. }
  344. return $folder;
  345. }
  346. /**
  347. * @param string $mimeType
  348. * @return null|string
  349. * @throws \InvalidArgumentException
  350. */
  351. private function getExtention($mimeType) {
  352. switch ($mimeType) {
  353. case 'image/png':
  354. return 'png';
  355. case 'image/jpeg':
  356. return 'jpg';
  357. case 'image/gif':
  358. return 'gif';
  359. default:
  360. throw new \InvalidArgumentException('Not a valid mimetype');
  361. }
  362. }
  363. }