@@ -19,94 +19,94 @@ |
||
19 | 19 | * @since 6.0.0 |
20 | 20 | */ |
21 | 21 | interface IPreview { |
22 | - /** |
|
23 | - * @since 11.0.0 |
|
24 | - */ |
|
25 | - public const MODE_FILL = 'fill'; |
|
22 | + /** |
|
23 | + * @since 11.0.0 |
|
24 | + */ |
|
25 | + public const MODE_FILL = 'fill'; |
|
26 | 26 | |
27 | - /** |
|
28 | - * @since 11.0.0 |
|
29 | - */ |
|
30 | - public const MODE_COVER = 'cover'; |
|
27 | + /** |
|
28 | + * @since 11.0.0 |
|
29 | + */ |
|
30 | + public const MODE_COVER = 'cover'; |
|
31 | 31 | |
32 | - /** |
|
33 | - * In order to improve lazy loading a closure can be registered which will be |
|
34 | - * called in case preview providers are actually requested |
|
35 | - * |
|
36 | - * $callable has to return an instance of \OCP\Preview\IProvider |
|
37 | - * |
|
38 | - * @param string $mimeTypeRegex Regex with the mime types that are supported by this provider |
|
39 | - * @param \Closure $callable |
|
40 | - * @return void |
|
41 | - * @since 8.1.0 |
|
42 | - * @see \OCP\AppFramework\Bootstrap\IRegistrationContext::registerPreviewProvider |
|
43 | - * |
|
44 | - * @deprecated 23.0.0 Register your provider via the IRegistrationContext when booting the app |
|
45 | - */ |
|
46 | - public function registerProvider($mimeTypeRegex, \Closure $callable); |
|
32 | + /** |
|
33 | + * In order to improve lazy loading a closure can be registered which will be |
|
34 | + * called in case preview providers are actually requested |
|
35 | + * |
|
36 | + * $callable has to return an instance of \OCP\Preview\IProvider |
|
37 | + * |
|
38 | + * @param string $mimeTypeRegex Regex with the mime types that are supported by this provider |
|
39 | + * @param \Closure $callable |
|
40 | + * @return void |
|
41 | + * @since 8.1.0 |
|
42 | + * @see \OCP\AppFramework\Bootstrap\IRegistrationContext::registerPreviewProvider |
|
43 | + * |
|
44 | + * @deprecated 23.0.0 Register your provider via the IRegistrationContext when booting the app |
|
45 | + */ |
|
46 | + public function registerProvider($mimeTypeRegex, \Closure $callable); |
|
47 | 47 | |
48 | - /** |
|
49 | - * Get all providers |
|
50 | - * @return array |
|
51 | - * @since 8.1.0 |
|
52 | - */ |
|
53 | - public function getProviders(); |
|
48 | + /** |
|
49 | + * Get all providers |
|
50 | + * @return array |
|
51 | + * @since 8.1.0 |
|
52 | + */ |
|
53 | + public function getProviders(); |
|
54 | 54 | |
55 | - /** |
|
56 | - * Does the manager have any providers |
|
57 | - * @return bool |
|
58 | - * @since 8.1.0 |
|
59 | - */ |
|
60 | - public function hasProviders(); |
|
55 | + /** |
|
56 | + * Does the manager have any providers |
|
57 | + * @return bool |
|
58 | + * @since 8.1.0 |
|
59 | + */ |
|
60 | + public function hasProviders(); |
|
61 | 61 | |
62 | - /** |
|
63 | - * Returns a preview of a file |
|
64 | - * |
|
65 | - * The cache is searched first and if nothing usable was found then a preview is |
|
66 | - * generated by one of the providers |
|
67 | - * |
|
68 | - * @param File $file |
|
69 | - * @param int $width |
|
70 | - * @param int $height |
|
71 | - * @param bool $crop |
|
72 | - * @param string $mode |
|
73 | - * @param string $mimeType To force a given mimetype for the file (files_versions needs this) |
|
74 | - * @param bool $cacheResult Whether or not to cache the preview on the filesystem. Default to true. Can be useful to set to false to limit the amount of stored previews. |
|
75 | - * @return ISimpleFile |
|
76 | - * @throws NotFoundException |
|
77 | - * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
78 | - * @since 11.0.0 - \InvalidArgumentException was added in 12.0.0 |
|
79 | - * @since 32.0.0 - getPreview($cacheResult) added the $cacheResult argument to the signature |
|
80 | - */ |
|
81 | - public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null, bool $cacheResult = true); |
|
62 | + /** |
|
63 | + * Returns a preview of a file |
|
64 | + * |
|
65 | + * The cache is searched first and if nothing usable was found then a preview is |
|
66 | + * generated by one of the providers |
|
67 | + * |
|
68 | + * @param File $file |
|
69 | + * @param int $width |
|
70 | + * @param int $height |
|
71 | + * @param bool $crop |
|
72 | + * @param string $mode |
|
73 | + * @param string $mimeType To force a given mimetype for the file (files_versions needs this) |
|
74 | + * @param bool $cacheResult Whether or not to cache the preview on the filesystem. Default to true. Can be useful to set to false to limit the amount of stored previews. |
|
75 | + * @return ISimpleFile |
|
76 | + * @throws NotFoundException |
|
77 | + * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
78 | + * @since 11.0.0 - \InvalidArgumentException was added in 12.0.0 |
|
79 | + * @since 32.0.0 - getPreview($cacheResult) added the $cacheResult argument to the signature |
|
80 | + */ |
|
81 | + public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null, bool $cacheResult = true); |
|
82 | 82 | |
83 | - /** |
|
84 | - * Returns true if the passed mime type is supported |
|
85 | - * @param string $mimeType |
|
86 | - * @return boolean |
|
87 | - * @since 6.0.0 |
|
88 | - */ |
|
89 | - public function isMimeSupported($mimeType = '*'); |
|
83 | + /** |
|
84 | + * Returns true if the passed mime type is supported |
|
85 | + * @param string $mimeType |
|
86 | + * @return boolean |
|
87 | + * @since 6.0.0 |
|
88 | + */ |
|
89 | + public function isMimeSupported($mimeType = '*'); |
|
90 | 90 | |
91 | - /** |
|
92 | - * Check if a preview can be generated for a file |
|
93 | - * |
|
94 | - * @param \OCP\Files\FileInfo $file |
|
95 | - * @return bool |
|
96 | - * @since 8.0.0 |
|
97 | - */ |
|
98 | - public function isAvailable(\OCP\Files\FileInfo $file); |
|
91 | + /** |
|
92 | + * Check if a preview can be generated for a file |
|
93 | + * |
|
94 | + * @param \OCP\Files\FileInfo $file |
|
95 | + * @return bool |
|
96 | + * @since 8.0.0 |
|
97 | + */ |
|
98 | + public function isAvailable(\OCP\Files\FileInfo $file); |
|
99 | 99 | |
100 | - /** |
|
101 | - * Generates previews of a file |
|
102 | - * |
|
103 | - * @param File $file |
|
104 | - * @param array $specifications |
|
105 | - * @param string $mimeType |
|
106 | - * @return ISimpleFile the last preview that was generated |
|
107 | - * @throws NotFoundException |
|
108 | - * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
109 | - * @since 19.0.0 |
|
110 | - */ |
|
111 | - public function generatePreviews(File $file, array $specifications, $mimeType = null); |
|
100 | + /** |
|
101 | + * Generates previews of a file |
|
102 | + * |
|
103 | + * @param File $file |
|
104 | + * @param array $specifications |
|
105 | + * @param string $mimeType |
|
106 | + * @return ISimpleFile the last preview that was generated |
|
107 | + * @throws NotFoundException |
|
108 | + * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
109 | + * @since 19.0.0 |
|
110 | + */ |
|
111 | + public function generatePreviews(File $file, array $specifications, $mimeType = null); |
|
112 | 112 | } |
@@ -28,436 +28,436 @@ |
||
28 | 28 | use function array_key_exists; |
29 | 29 | |
30 | 30 | class PreviewManager implements IPreview { |
31 | - protected IConfig $config; |
|
32 | - protected IRootFolder $rootFolder; |
|
33 | - protected IAppData $appData; |
|
34 | - protected IEventDispatcher $eventDispatcher; |
|
35 | - private ?Generator $generator = null; |
|
36 | - private GeneratorHelper $helper; |
|
37 | - protected bool $providerListDirty = false; |
|
38 | - protected bool $registeredCoreProviders = false; |
|
39 | - protected array $providers = []; |
|
40 | - |
|
41 | - /** @var array mime type => support status */ |
|
42 | - protected array $mimeTypeSupportMap = []; |
|
43 | - protected ?array $defaultProviders = null; |
|
44 | - protected ?string $userId; |
|
45 | - private Coordinator $bootstrapCoordinator; |
|
46 | - |
|
47 | - /** |
|
48 | - * Hash map (without value) of loaded bootstrap providers |
|
49 | - * @psalm-var array<string, null> |
|
50 | - */ |
|
51 | - private array $loadedBootstrapProviders = []; |
|
52 | - private ContainerInterface $container; |
|
53 | - private IBinaryFinder $binaryFinder; |
|
54 | - private IMagickSupport $imagickSupport; |
|
55 | - private bool $enablePreviews; |
|
56 | - |
|
57 | - public function __construct( |
|
58 | - IConfig $config, |
|
59 | - IRootFolder $rootFolder, |
|
60 | - IAppData $appData, |
|
61 | - IEventDispatcher $eventDispatcher, |
|
62 | - GeneratorHelper $helper, |
|
63 | - ?string $userId, |
|
64 | - Coordinator $bootstrapCoordinator, |
|
65 | - ContainerInterface $container, |
|
66 | - IBinaryFinder $binaryFinder, |
|
67 | - IMagickSupport $imagickSupport, |
|
68 | - ) { |
|
69 | - $this->config = $config; |
|
70 | - $this->rootFolder = $rootFolder; |
|
71 | - $this->appData = $appData; |
|
72 | - $this->eventDispatcher = $eventDispatcher; |
|
73 | - $this->helper = $helper; |
|
74 | - $this->userId = $userId; |
|
75 | - $this->bootstrapCoordinator = $bootstrapCoordinator; |
|
76 | - $this->container = $container; |
|
77 | - $this->binaryFinder = $binaryFinder; |
|
78 | - $this->imagickSupport = $imagickSupport; |
|
79 | - $this->enablePreviews = $config->getSystemValueBool('enable_previews', true); |
|
80 | - } |
|
81 | - |
|
82 | - /** |
|
83 | - * In order to improve lazy loading a closure can be registered which will be |
|
84 | - * called in case preview providers are actually requested |
|
85 | - * |
|
86 | - * $callable has to return an instance of \OCP\Preview\IProvider or \OCP\Preview\IProviderV2 |
|
87 | - * |
|
88 | - * @param string $mimeTypeRegex Regex with the mime types that are supported by this provider |
|
89 | - * @param \Closure $callable |
|
90 | - * @return void |
|
91 | - */ |
|
92 | - public function registerProvider($mimeTypeRegex, \Closure $callable): void { |
|
93 | - if (!$this->enablePreviews) { |
|
94 | - return; |
|
95 | - } |
|
96 | - |
|
97 | - if (!isset($this->providers[$mimeTypeRegex])) { |
|
98 | - $this->providers[$mimeTypeRegex] = []; |
|
99 | - } |
|
100 | - $this->providers[$mimeTypeRegex][] = $callable; |
|
101 | - $this->providerListDirty = true; |
|
102 | - } |
|
103 | - |
|
104 | - /** |
|
105 | - * Get all providers |
|
106 | - */ |
|
107 | - public function getProviders(): array { |
|
108 | - if (!$this->enablePreviews) { |
|
109 | - return []; |
|
110 | - } |
|
111 | - |
|
112 | - $this->registerCoreProviders(); |
|
113 | - $this->registerBootstrapProviders(); |
|
114 | - if ($this->providerListDirty) { |
|
115 | - $keys = array_map('strlen', array_keys($this->providers)); |
|
116 | - array_multisort($keys, SORT_DESC, $this->providers); |
|
117 | - $this->providerListDirty = false; |
|
118 | - } |
|
119 | - |
|
120 | - return $this->providers; |
|
121 | - } |
|
122 | - |
|
123 | - /** |
|
124 | - * Does the manager have any providers |
|
125 | - */ |
|
126 | - public function hasProviders(): bool { |
|
127 | - $this->registerCoreProviders(); |
|
128 | - return !empty($this->providers); |
|
129 | - } |
|
130 | - |
|
131 | - private function getGenerator(): Generator { |
|
132 | - if ($this->generator === null) { |
|
133 | - $this->generator = new Generator( |
|
134 | - $this->config, |
|
135 | - $this, |
|
136 | - $this->appData, |
|
137 | - new GeneratorHelper( |
|
138 | - $this->rootFolder, |
|
139 | - $this->config |
|
140 | - ), |
|
141 | - $this->eventDispatcher, |
|
142 | - $this->container->get(LoggerInterface::class), |
|
143 | - ); |
|
144 | - } |
|
145 | - return $this->generator; |
|
146 | - } |
|
147 | - |
|
148 | - public function getPreview( |
|
149 | - File $file, |
|
150 | - $width = -1, |
|
151 | - $height = -1, |
|
152 | - $crop = false, |
|
153 | - $mode = IPreview::MODE_FILL, |
|
154 | - $mimeType = null, |
|
155 | - bool $cacheResult = true, |
|
156 | - ): ISimpleFile { |
|
157 | - $this->throwIfPreviewsDisabled(); |
|
158 | - $previewConcurrency = $this->getGenerator()->getNumConcurrentPreviews('preview_concurrency_all'); |
|
159 | - $sem = Generator::guardWithSemaphore(Generator::SEMAPHORE_ID_ALL, $previewConcurrency); |
|
160 | - try { |
|
161 | - $preview = $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType, $cacheResult); |
|
162 | - } finally { |
|
163 | - Generator::unguardWithSemaphore($sem); |
|
164 | - } |
|
165 | - |
|
166 | - return $preview; |
|
167 | - } |
|
168 | - |
|
169 | - /** |
|
170 | - * Generates previews of a file |
|
171 | - * |
|
172 | - * @param File $file |
|
173 | - * @param array $specifications |
|
174 | - * @param string $mimeType |
|
175 | - * @return ISimpleFile the last preview that was generated |
|
176 | - * @throws NotFoundException |
|
177 | - * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
178 | - * @since 19.0.0 |
|
179 | - */ |
|
180 | - public function generatePreviews(File $file, array $specifications, $mimeType = null) { |
|
181 | - $this->throwIfPreviewsDisabled(); |
|
182 | - return $this->getGenerator()->generatePreviews($file, $specifications, $mimeType); |
|
183 | - } |
|
184 | - |
|
185 | - /** |
|
186 | - * returns true if the passed mime type is supported |
|
187 | - * |
|
188 | - * @param string $mimeType |
|
189 | - * @return boolean |
|
190 | - */ |
|
191 | - public function isMimeSupported($mimeType = '*') { |
|
192 | - if (!$this->enablePreviews) { |
|
193 | - return false; |
|
194 | - } |
|
195 | - |
|
196 | - if (isset($this->mimeTypeSupportMap[$mimeType])) { |
|
197 | - return $this->mimeTypeSupportMap[$mimeType]; |
|
198 | - } |
|
199 | - |
|
200 | - $this->registerCoreProviders(); |
|
201 | - $this->registerBootstrapProviders(); |
|
202 | - $providerMimeTypes = array_keys($this->providers); |
|
203 | - foreach ($providerMimeTypes as $supportedMimeType) { |
|
204 | - if (preg_match($supportedMimeType, $mimeType)) { |
|
205 | - $this->mimeTypeSupportMap[$mimeType] = true; |
|
206 | - return true; |
|
207 | - } |
|
208 | - } |
|
209 | - $this->mimeTypeSupportMap[$mimeType] = false; |
|
210 | - return false; |
|
211 | - } |
|
212 | - |
|
213 | - /** |
|
214 | - * Check if a preview can be generated for a file |
|
215 | - */ |
|
216 | - public function isAvailable(\OCP\Files\FileInfo $file): bool { |
|
217 | - if (!$this->enablePreviews) { |
|
218 | - return false; |
|
219 | - } |
|
220 | - |
|
221 | - $this->registerCoreProviders(); |
|
222 | - if (!$this->isMimeSupported($file->getMimetype())) { |
|
223 | - return false; |
|
224 | - } |
|
225 | - |
|
226 | - $mount = $file->getMountPoint(); |
|
227 | - if ($mount and !$mount->getOption('previews', true)) { |
|
228 | - return false; |
|
229 | - } |
|
230 | - |
|
231 | - foreach ($this->providers as $supportedMimeType => $providers) { |
|
232 | - if (preg_match($supportedMimeType, $file->getMimetype())) { |
|
233 | - foreach ($providers as $providerClosure) { |
|
234 | - $provider = $this->helper->getProvider($providerClosure); |
|
235 | - if (!($provider instanceof IProviderV2)) { |
|
236 | - continue; |
|
237 | - } |
|
238 | - |
|
239 | - if ($provider->isAvailable($file)) { |
|
240 | - return true; |
|
241 | - } |
|
242 | - } |
|
243 | - } |
|
244 | - } |
|
245 | - return false; |
|
246 | - } |
|
247 | - |
|
248 | - /** |
|
249 | - * List of enabled default providers |
|
250 | - * |
|
251 | - * The following providers are enabled by default: |
|
252 | - * - OC\Preview\PNG |
|
253 | - * - OC\Preview\JPEG |
|
254 | - * - OC\Preview\GIF |
|
255 | - * - OC\Preview\BMP |
|
256 | - * - OC\Preview\XBitmap |
|
257 | - * - OC\Preview\MarkDown |
|
258 | - * - OC\Preview\MP3 |
|
259 | - * - OC\Preview\TXT |
|
260 | - * |
|
261 | - * The following providers are disabled by default due to performance or privacy concerns: |
|
262 | - * - OC\Preview\Font |
|
263 | - * - OC\Preview\HEIC |
|
264 | - * - OC\Preview\Illustrator |
|
265 | - * - OC\Preview\Movie |
|
266 | - * - OC\Preview\MSOfficeDoc |
|
267 | - * - OC\Preview\MSOffice2003 |
|
268 | - * - OC\Preview\MSOffice2007 |
|
269 | - * - OC\Preview\OpenDocument |
|
270 | - * - OC\Preview\PDF |
|
271 | - * - OC\Preview\Photoshop |
|
272 | - * - OC\Preview\Postscript |
|
273 | - * - OC\Preview\StarOffice |
|
274 | - * - OC\Preview\SVG |
|
275 | - * - OC\Preview\TIFF |
|
276 | - * |
|
277 | - * @return array |
|
278 | - */ |
|
279 | - protected function getEnabledDefaultProvider() { |
|
280 | - if ($this->defaultProviders !== null) { |
|
281 | - return $this->defaultProviders; |
|
282 | - } |
|
283 | - |
|
284 | - $imageProviders = [ |
|
285 | - Preview\PNG::class, |
|
286 | - Preview\JPEG::class, |
|
287 | - Preview\GIF::class, |
|
288 | - Preview\BMP::class, |
|
289 | - Preview\XBitmap::class, |
|
290 | - Preview\Krita::class, |
|
291 | - Preview\WebP::class, |
|
292 | - ]; |
|
293 | - |
|
294 | - $this->defaultProviders = $this->config->getSystemValue('enabledPreviewProviders', array_merge([ |
|
295 | - Preview\MarkDown::class, |
|
296 | - Preview\MP3::class, |
|
297 | - Preview\TXT::class, |
|
298 | - Preview\OpenDocument::class, |
|
299 | - ], $imageProviders)); |
|
300 | - |
|
301 | - if (in_array(Preview\Image::class, $this->defaultProviders)) { |
|
302 | - $this->defaultProviders = array_merge($this->defaultProviders, $imageProviders); |
|
303 | - } |
|
304 | - $this->defaultProviders = array_unique($this->defaultProviders); |
|
305 | - return $this->defaultProviders; |
|
306 | - } |
|
307 | - |
|
308 | - /** |
|
309 | - * Register the default providers (if enabled) |
|
310 | - * |
|
311 | - * @param string $class |
|
312 | - * @param string $mimeType |
|
313 | - */ |
|
314 | - protected function registerCoreProvider($class, $mimeType, $options = []) { |
|
315 | - if (in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) { |
|
316 | - $this->registerProvider($mimeType, function () use ($class, $options) { |
|
317 | - return new $class($options); |
|
318 | - }); |
|
319 | - } |
|
320 | - } |
|
321 | - |
|
322 | - /** |
|
323 | - * Register the default providers (if enabled) |
|
324 | - */ |
|
325 | - protected function registerCoreProviders() { |
|
326 | - if ($this->registeredCoreProviders) { |
|
327 | - return; |
|
328 | - } |
|
329 | - $this->registeredCoreProviders = true; |
|
330 | - |
|
331 | - $this->registerCoreProvider(Preview\TXT::class, '/text\/plain/'); |
|
332 | - $this->registerCoreProvider(Preview\MarkDown::class, '/text\/(x-)?markdown/'); |
|
333 | - $this->registerCoreProvider(Preview\PNG::class, '/image\/png/'); |
|
334 | - $this->registerCoreProvider(Preview\JPEG::class, '/image\/jpeg/'); |
|
335 | - $this->registerCoreProvider(Preview\GIF::class, '/image\/gif/'); |
|
336 | - $this->registerCoreProvider(Preview\BMP::class, '/image\/bmp/'); |
|
337 | - $this->registerCoreProvider(Preview\XBitmap::class, '/image\/x-xbitmap/'); |
|
338 | - $this->registerCoreProvider(Preview\WebP::class, '/image\/webp/'); |
|
339 | - $this->registerCoreProvider(Preview\Krita::class, '/application\/x-krita/'); |
|
340 | - $this->registerCoreProvider(Preview\MP3::class, '/audio\/mpeg$/'); |
|
341 | - $this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/'); |
|
342 | - $this->registerCoreProvider(Preview\Imaginary::class, Preview\Imaginary::supportedMimeTypes()); |
|
343 | - $this->registerCoreProvider(Preview\ImaginaryPDF::class, Preview\ImaginaryPDF::supportedMimeTypes()); |
|
344 | - |
|
345 | - // SVG and Bitmap require imagick |
|
346 | - if ($this->imagickSupport->hasExtension()) { |
|
347 | - $imagickProviders = [ |
|
348 | - 'SVG' => ['mimetype' => '/image\/svg\+xml/', 'class' => Preview\SVG::class], |
|
349 | - 'TIFF' => ['mimetype' => '/image\/tiff/', 'class' => Preview\TIFF::class], |
|
350 | - 'PDF' => ['mimetype' => '/application\/pdf/', 'class' => Preview\PDF::class], |
|
351 | - 'AI' => ['mimetype' => '/application\/illustrator/', 'class' => Preview\Illustrator::class], |
|
352 | - 'PSD' => ['mimetype' => '/application\/x-photoshop/', 'class' => Preview\Photoshop::class], |
|
353 | - 'EPS' => ['mimetype' => '/application\/postscript/', 'class' => Preview\Postscript::class], |
|
354 | - 'TTF' => ['mimetype' => '/application\/(?:font-sfnt|x-font$)/', 'class' => Preview\Font::class], |
|
355 | - 'HEIC' => ['mimetype' => '/image\/(x-)?hei(f|c)/', 'class' => Preview\HEIC::class], |
|
356 | - 'TGA' => ['mimetype' => '/image\/(x-)?t(ar)?ga/', 'class' => Preview\TGA::class], |
|
357 | - 'SGI' => ['mimetype' => '/image\/(x-)?sgi/', 'class' => Preview\SGI::class], |
|
358 | - ]; |
|
359 | - |
|
360 | - foreach ($imagickProviders as $queryFormat => $provider) { |
|
361 | - $class = $provider['class']; |
|
362 | - if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) { |
|
363 | - continue; |
|
364 | - } |
|
365 | - |
|
366 | - if ($this->imagickSupport->supportsFormat($queryFormat)) { |
|
367 | - $this->registerCoreProvider($class, $provider['mimetype']); |
|
368 | - } |
|
369 | - } |
|
370 | - } |
|
371 | - |
|
372 | - $this->registerCoreProvidersOffice(); |
|
373 | - |
|
374 | - // Video requires avconv or ffmpeg |
|
375 | - if (in_array(Preview\Movie::class, $this->getEnabledDefaultProvider())) { |
|
376 | - $movieBinary = $this->config->getSystemValue('preview_ffmpeg_path', null); |
|
377 | - if (!is_string($movieBinary)) { |
|
378 | - $movieBinary = $this->binaryFinder->findBinaryPath('avconv'); |
|
379 | - if (!is_string($movieBinary)) { |
|
380 | - $movieBinary = $this->binaryFinder->findBinaryPath('ffmpeg'); |
|
381 | - } |
|
382 | - } |
|
383 | - |
|
384 | - |
|
385 | - if (is_string($movieBinary)) { |
|
386 | - $this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ['movieBinary' => $movieBinary]); |
|
387 | - } |
|
388 | - } |
|
389 | - } |
|
390 | - |
|
391 | - private function registerCoreProvidersOffice(): void { |
|
392 | - $officeProviders = [ |
|
393 | - ['mimetype' => '/application\/msword/', 'class' => Preview\MSOfficeDoc::class], |
|
394 | - ['mimetype' => '/application\/vnd.ms-.*/', 'class' => Preview\MSOffice2003::class], |
|
395 | - ['mimetype' => '/application\/vnd.openxmlformats-officedocument.*/', 'class' => Preview\MSOffice2007::class], |
|
396 | - ['mimetype' => '/application\/vnd.oasis.opendocument.*/', 'class' => Preview\OpenDocument::class], |
|
397 | - ['mimetype' => '/application\/vnd.sun.xml.*/', 'class' => Preview\StarOffice::class], |
|
398 | - ['mimetype' => '/image\/emf/', 'class' => Preview\EMF::class], |
|
399 | - ]; |
|
400 | - |
|
401 | - $findBinary = true; |
|
402 | - $officeBinary = false; |
|
403 | - |
|
404 | - foreach ($officeProviders as $provider) { |
|
405 | - $class = $provider['class']; |
|
406 | - if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) { |
|
407 | - continue; |
|
408 | - } |
|
409 | - |
|
410 | - if ($findBinary) { |
|
411 | - // Office requires openoffice or libreoffice |
|
412 | - $officeBinary = $this->config->getSystemValue('preview_libreoffice_path', false); |
|
413 | - if ($officeBinary === false) { |
|
414 | - $officeBinary = $this->binaryFinder->findBinaryPath('libreoffice'); |
|
415 | - } |
|
416 | - if ($officeBinary === false) { |
|
417 | - $officeBinary = $this->binaryFinder->findBinaryPath('openoffice'); |
|
418 | - } |
|
419 | - $findBinary = false; |
|
420 | - } |
|
421 | - |
|
422 | - if ($officeBinary) { |
|
423 | - $this->registerCoreProvider($class, $provider['mimetype'], ['officeBinary' => $officeBinary]); |
|
424 | - } |
|
425 | - } |
|
426 | - } |
|
427 | - |
|
428 | - private function registerBootstrapProviders(): void { |
|
429 | - $context = $this->bootstrapCoordinator->getRegistrationContext(); |
|
430 | - |
|
431 | - if ($context === null) { |
|
432 | - // Just ignore for now |
|
433 | - return; |
|
434 | - } |
|
435 | - |
|
436 | - $providers = $context->getPreviewProviders(); |
|
437 | - foreach ($providers as $provider) { |
|
438 | - $key = $provider->getMimeTypeRegex() . '-' . $provider->getService(); |
|
439 | - if (array_key_exists($key, $this->loadedBootstrapProviders)) { |
|
440 | - // Do not load the provider more than once |
|
441 | - continue; |
|
442 | - } |
|
443 | - $this->loadedBootstrapProviders[$key] = null; |
|
444 | - |
|
445 | - $this->registerProvider($provider->getMimeTypeRegex(), function () use ($provider) { |
|
446 | - try { |
|
447 | - return $this->container->get($provider->getService()); |
|
448 | - } catch (QueryException $e) { |
|
449 | - return null; |
|
450 | - } |
|
451 | - }); |
|
452 | - } |
|
453 | - } |
|
454 | - |
|
455 | - /** |
|
456 | - * @throws NotFoundException if preview generation is disabled |
|
457 | - */ |
|
458 | - private function throwIfPreviewsDisabled(): void { |
|
459 | - if (!$this->enablePreviews) { |
|
460 | - throw new NotFoundException('Previews disabled'); |
|
461 | - } |
|
462 | - } |
|
31 | + protected IConfig $config; |
|
32 | + protected IRootFolder $rootFolder; |
|
33 | + protected IAppData $appData; |
|
34 | + protected IEventDispatcher $eventDispatcher; |
|
35 | + private ?Generator $generator = null; |
|
36 | + private GeneratorHelper $helper; |
|
37 | + protected bool $providerListDirty = false; |
|
38 | + protected bool $registeredCoreProviders = false; |
|
39 | + protected array $providers = []; |
|
40 | + |
|
41 | + /** @var array mime type => support status */ |
|
42 | + protected array $mimeTypeSupportMap = []; |
|
43 | + protected ?array $defaultProviders = null; |
|
44 | + protected ?string $userId; |
|
45 | + private Coordinator $bootstrapCoordinator; |
|
46 | + |
|
47 | + /** |
|
48 | + * Hash map (without value) of loaded bootstrap providers |
|
49 | + * @psalm-var array<string, null> |
|
50 | + */ |
|
51 | + private array $loadedBootstrapProviders = []; |
|
52 | + private ContainerInterface $container; |
|
53 | + private IBinaryFinder $binaryFinder; |
|
54 | + private IMagickSupport $imagickSupport; |
|
55 | + private bool $enablePreviews; |
|
56 | + |
|
57 | + public function __construct( |
|
58 | + IConfig $config, |
|
59 | + IRootFolder $rootFolder, |
|
60 | + IAppData $appData, |
|
61 | + IEventDispatcher $eventDispatcher, |
|
62 | + GeneratorHelper $helper, |
|
63 | + ?string $userId, |
|
64 | + Coordinator $bootstrapCoordinator, |
|
65 | + ContainerInterface $container, |
|
66 | + IBinaryFinder $binaryFinder, |
|
67 | + IMagickSupport $imagickSupport, |
|
68 | + ) { |
|
69 | + $this->config = $config; |
|
70 | + $this->rootFolder = $rootFolder; |
|
71 | + $this->appData = $appData; |
|
72 | + $this->eventDispatcher = $eventDispatcher; |
|
73 | + $this->helper = $helper; |
|
74 | + $this->userId = $userId; |
|
75 | + $this->bootstrapCoordinator = $bootstrapCoordinator; |
|
76 | + $this->container = $container; |
|
77 | + $this->binaryFinder = $binaryFinder; |
|
78 | + $this->imagickSupport = $imagickSupport; |
|
79 | + $this->enablePreviews = $config->getSystemValueBool('enable_previews', true); |
|
80 | + } |
|
81 | + |
|
82 | + /** |
|
83 | + * In order to improve lazy loading a closure can be registered which will be |
|
84 | + * called in case preview providers are actually requested |
|
85 | + * |
|
86 | + * $callable has to return an instance of \OCP\Preview\IProvider or \OCP\Preview\IProviderV2 |
|
87 | + * |
|
88 | + * @param string $mimeTypeRegex Regex with the mime types that are supported by this provider |
|
89 | + * @param \Closure $callable |
|
90 | + * @return void |
|
91 | + */ |
|
92 | + public function registerProvider($mimeTypeRegex, \Closure $callable): void { |
|
93 | + if (!$this->enablePreviews) { |
|
94 | + return; |
|
95 | + } |
|
96 | + |
|
97 | + if (!isset($this->providers[$mimeTypeRegex])) { |
|
98 | + $this->providers[$mimeTypeRegex] = []; |
|
99 | + } |
|
100 | + $this->providers[$mimeTypeRegex][] = $callable; |
|
101 | + $this->providerListDirty = true; |
|
102 | + } |
|
103 | + |
|
104 | + /** |
|
105 | + * Get all providers |
|
106 | + */ |
|
107 | + public function getProviders(): array { |
|
108 | + if (!$this->enablePreviews) { |
|
109 | + return []; |
|
110 | + } |
|
111 | + |
|
112 | + $this->registerCoreProviders(); |
|
113 | + $this->registerBootstrapProviders(); |
|
114 | + if ($this->providerListDirty) { |
|
115 | + $keys = array_map('strlen', array_keys($this->providers)); |
|
116 | + array_multisort($keys, SORT_DESC, $this->providers); |
|
117 | + $this->providerListDirty = false; |
|
118 | + } |
|
119 | + |
|
120 | + return $this->providers; |
|
121 | + } |
|
122 | + |
|
123 | + /** |
|
124 | + * Does the manager have any providers |
|
125 | + */ |
|
126 | + public function hasProviders(): bool { |
|
127 | + $this->registerCoreProviders(); |
|
128 | + return !empty($this->providers); |
|
129 | + } |
|
130 | + |
|
131 | + private function getGenerator(): Generator { |
|
132 | + if ($this->generator === null) { |
|
133 | + $this->generator = new Generator( |
|
134 | + $this->config, |
|
135 | + $this, |
|
136 | + $this->appData, |
|
137 | + new GeneratorHelper( |
|
138 | + $this->rootFolder, |
|
139 | + $this->config |
|
140 | + ), |
|
141 | + $this->eventDispatcher, |
|
142 | + $this->container->get(LoggerInterface::class), |
|
143 | + ); |
|
144 | + } |
|
145 | + return $this->generator; |
|
146 | + } |
|
147 | + |
|
148 | + public function getPreview( |
|
149 | + File $file, |
|
150 | + $width = -1, |
|
151 | + $height = -1, |
|
152 | + $crop = false, |
|
153 | + $mode = IPreview::MODE_FILL, |
|
154 | + $mimeType = null, |
|
155 | + bool $cacheResult = true, |
|
156 | + ): ISimpleFile { |
|
157 | + $this->throwIfPreviewsDisabled(); |
|
158 | + $previewConcurrency = $this->getGenerator()->getNumConcurrentPreviews('preview_concurrency_all'); |
|
159 | + $sem = Generator::guardWithSemaphore(Generator::SEMAPHORE_ID_ALL, $previewConcurrency); |
|
160 | + try { |
|
161 | + $preview = $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType, $cacheResult); |
|
162 | + } finally { |
|
163 | + Generator::unguardWithSemaphore($sem); |
|
164 | + } |
|
165 | + |
|
166 | + return $preview; |
|
167 | + } |
|
168 | + |
|
169 | + /** |
|
170 | + * Generates previews of a file |
|
171 | + * |
|
172 | + * @param File $file |
|
173 | + * @param array $specifications |
|
174 | + * @param string $mimeType |
|
175 | + * @return ISimpleFile the last preview that was generated |
|
176 | + * @throws NotFoundException |
|
177 | + * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
178 | + * @since 19.0.0 |
|
179 | + */ |
|
180 | + public function generatePreviews(File $file, array $specifications, $mimeType = null) { |
|
181 | + $this->throwIfPreviewsDisabled(); |
|
182 | + return $this->getGenerator()->generatePreviews($file, $specifications, $mimeType); |
|
183 | + } |
|
184 | + |
|
185 | + /** |
|
186 | + * returns true if the passed mime type is supported |
|
187 | + * |
|
188 | + * @param string $mimeType |
|
189 | + * @return boolean |
|
190 | + */ |
|
191 | + public function isMimeSupported($mimeType = '*') { |
|
192 | + if (!$this->enablePreviews) { |
|
193 | + return false; |
|
194 | + } |
|
195 | + |
|
196 | + if (isset($this->mimeTypeSupportMap[$mimeType])) { |
|
197 | + return $this->mimeTypeSupportMap[$mimeType]; |
|
198 | + } |
|
199 | + |
|
200 | + $this->registerCoreProviders(); |
|
201 | + $this->registerBootstrapProviders(); |
|
202 | + $providerMimeTypes = array_keys($this->providers); |
|
203 | + foreach ($providerMimeTypes as $supportedMimeType) { |
|
204 | + if (preg_match($supportedMimeType, $mimeType)) { |
|
205 | + $this->mimeTypeSupportMap[$mimeType] = true; |
|
206 | + return true; |
|
207 | + } |
|
208 | + } |
|
209 | + $this->mimeTypeSupportMap[$mimeType] = false; |
|
210 | + return false; |
|
211 | + } |
|
212 | + |
|
213 | + /** |
|
214 | + * Check if a preview can be generated for a file |
|
215 | + */ |
|
216 | + public function isAvailable(\OCP\Files\FileInfo $file): bool { |
|
217 | + if (!$this->enablePreviews) { |
|
218 | + return false; |
|
219 | + } |
|
220 | + |
|
221 | + $this->registerCoreProviders(); |
|
222 | + if (!$this->isMimeSupported($file->getMimetype())) { |
|
223 | + return false; |
|
224 | + } |
|
225 | + |
|
226 | + $mount = $file->getMountPoint(); |
|
227 | + if ($mount and !$mount->getOption('previews', true)) { |
|
228 | + return false; |
|
229 | + } |
|
230 | + |
|
231 | + foreach ($this->providers as $supportedMimeType => $providers) { |
|
232 | + if (preg_match($supportedMimeType, $file->getMimetype())) { |
|
233 | + foreach ($providers as $providerClosure) { |
|
234 | + $provider = $this->helper->getProvider($providerClosure); |
|
235 | + if (!($provider instanceof IProviderV2)) { |
|
236 | + continue; |
|
237 | + } |
|
238 | + |
|
239 | + if ($provider->isAvailable($file)) { |
|
240 | + return true; |
|
241 | + } |
|
242 | + } |
|
243 | + } |
|
244 | + } |
|
245 | + return false; |
|
246 | + } |
|
247 | + |
|
248 | + /** |
|
249 | + * List of enabled default providers |
|
250 | + * |
|
251 | + * The following providers are enabled by default: |
|
252 | + * - OC\Preview\PNG |
|
253 | + * - OC\Preview\JPEG |
|
254 | + * - OC\Preview\GIF |
|
255 | + * - OC\Preview\BMP |
|
256 | + * - OC\Preview\XBitmap |
|
257 | + * - OC\Preview\MarkDown |
|
258 | + * - OC\Preview\MP3 |
|
259 | + * - OC\Preview\TXT |
|
260 | + * |
|
261 | + * The following providers are disabled by default due to performance or privacy concerns: |
|
262 | + * - OC\Preview\Font |
|
263 | + * - OC\Preview\HEIC |
|
264 | + * - OC\Preview\Illustrator |
|
265 | + * - OC\Preview\Movie |
|
266 | + * - OC\Preview\MSOfficeDoc |
|
267 | + * - OC\Preview\MSOffice2003 |
|
268 | + * - OC\Preview\MSOffice2007 |
|
269 | + * - OC\Preview\OpenDocument |
|
270 | + * - OC\Preview\PDF |
|
271 | + * - OC\Preview\Photoshop |
|
272 | + * - OC\Preview\Postscript |
|
273 | + * - OC\Preview\StarOffice |
|
274 | + * - OC\Preview\SVG |
|
275 | + * - OC\Preview\TIFF |
|
276 | + * |
|
277 | + * @return array |
|
278 | + */ |
|
279 | + protected function getEnabledDefaultProvider() { |
|
280 | + if ($this->defaultProviders !== null) { |
|
281 | + return $this->defaultProviders; |
|
282 | + } |
|
283 | + |
|
284 | + $imageProviders = [ |
|
285 | + Preview\PNG::class, |
|
286 | + Preview\JPEG::class, |
|
287 | + Preview\GIF::class, |
|
288 | + Preview\BMP::class, |
|
289 | + Preview\XBitmap::class, |
|
290 | + Preview\Krita::class, |
|
291 | + Preview\WebP::class, |
|
292 | + ]; |
|
293 | + |
|
294 | + $this->defaultProviders = $this->config->getSystemValue('enabledPreviewProviders', array_merge([ |
|
295 | + Preview\MarkDown::class, |
|
296 | + Preview\MP3::class, |
|
297 | + Preview\TXT::class, |
|
298 | + Preview\OpenDocument::class, |
|
299 | + ], $imageProviders)); |
|
300 | + |
|
301 | + if (in_array(Preview\Image::class, $this->defaultProviders)) { |
|
302 | + $this->defaultProviders = array_merge($this->defaultProviders, $imageProviders); |
|
303 | + } |
|
304 | + $this->defaultProviders = array_unique($this->defaultProviders); |
|
305 | + return $this->defaultProviders; |
|
306 | + } |
|
307 | + |
|
308 | + /** |
|
309 | + * Register the default providers (if enabled) |
|
310 | + * |
|
311 | + * @param string $class |
|
312 | + * @param string $mimeType |
|
313 | + */ |
|
314 | + protected function registerCoreProvider($class, $mimeType, $options = []) { |
|
315 | + if (in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) { |
|
316 | + $this->registerProvider($mimeType, function () use ($class, $options) { |
|
317 | + return new $class($options); |
|
318 | + }); |
|
319 | + } |
|
320 | + } |
|
321 | + |
|
322 | + /** |
|
323 | + * Register the default providers (if enabled) |
|
324 | + */ |
|
325 | + protected function registerCoreProviders() { |
|
326 | + if ($this->registeredCoreProviders) { |
|
327 | + return; |
|
328 | + } |
|
329 | + $this->registeredCoreProviders = true; |
|
330 | + |
|
331 | + $this->registerCoreProvider(Preview\TXT::class, '/text\/plain/'); |
|
332 | + $this->registerCoreProvider(Preview\MarkDown::class, '/text\/(x-)?markdown/'); |
|
333 | + $this->registerCoreProvider(Preview\PNG::class, '/image\/png/'); |
|
334 | + $this->registerCoreProvider(Preview\JPEG::class, '/image\/jpeg/'); |
|
335 | + $this->registerCoreProvider(Preview\GIF::class, '/image\/gif/'); |
|
336 | + $this->registerCoreProvider(Preview\BMP::class, '/image\/bmp/'); |
|
337 | + $this->registerCoreProvider(Preview\XBitmap::class, '/image\/x-xbitmap/'); |
|
338 | + $this->registerCoreProvider(Preview\WebP::class, '/image\/webp/'); |
|
339 | + $this->registerCoreProvider(Preview\Krita::class, '/application\/x-krita/'); |
|
340 | + $this->registerCoreProvider(Preview\MP3::class, '/audio\/mpeg$/'); |
|
341 | + $this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/'); |
|
342 | + $this->registerCoreProvider(Preview\Imaginary::class, Preview\Imaginary::supportedMimeTypes()); |
|
343 | + $this->registerCoreProvider(Preview\ImaginaryPDF::class, Preview\ImaginaryPDF::supportedMimeTypes()); |
|
344 | + |
|
345 | + // SVG and Bitmap require imagick |
|
346 | + if ($this->imagickSupport->hasExtension()) { |
|
347 | + $imagickProviders = [ |
|
348 | + 'SVG' => ['mimetype' => '/image\/svg\+xml/', 'class' => Preview\SVG::class], |
|
349 | + 'TIFF' => ['mimetype' => '/image\/tiff/', 'class' => Preview\TIFF::class], |
|
350 | + 'PDF' => ['mimetype' => '/application\/pdf/', 'class' => Preview\PDF::class], |
|
351 | + 'AI' => ['mimetype' => '/application\/illustrator/', 'class' => Preview\Illustrator::class], |
|
352 | + 'PSD' => ['mimetype' => '/application\/x-photoshop/', 'class' => Preview\Photoshop::class], |
|
353 | + 'EPS' => ['mimetype' => '/application\/postscript/', 'class' => Preview\Postscript::class], |
|
354 | + 'TTF' => ['mimetype' => '/application\/(?:font-sfnt|x-font$)/', 'class' => Preview\Font::class], |
|
355 | + 'HEIC' => ['mimetype' => '/image\/(x-)?hei(f|c)/', 'class' => Preview\HEIC::class], |
|
356 | + 'TGA' => ['mimetype' => '/image\/(x-)?t(ar)?ga/', 'class' => Preview\TGA::class], |
|
357 | + 'SGI' => ['mimetype' => '/image\/(x-)?sgi/', 'class' => Preview\SGI::class], |
|
358 | + ]; |
|
359 | + |
|
360 | + foreach ($imagickProviders as $queryFormat => $provider) { |
|
361 | + $class = $provider['class']; |
|
362 | + if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) { |
|
363 | + continue; |
|
364 | + } |
|
365 | + |
|
366 | + if ($this->imagickSupport->supportsFormat($queryFormat)) { |
|
367 | + $this->registerCoreProvider($class, $provider['mimetype']); |
|
368 | + } |
|
369 | + } |
|
370 | + } |
|
371 | + |
|
372 | + $this->registerCoreProvidersOffice(); |
|
373 | + |
|
374 | + // Video requires avconv or ffmpeg |
|
375 | + if (in_array(Preview\Movie::class, $this->getEnabledDefaultProvider())) { |
|
376 | + $movieBinary = $this->config->getSystemValue('preview_ffmpeg_path', null); |
|
377 | + if (!is_string($movieBinary)) { |
|
378 | + $movieBinary = $this->binaryFinder->findBinaryPath('avconv'); |
|
379 | + if (!is_string($movieBinary)) { |
|
380 | + $movieBinary = $this->binaryFinder->findBinaryPath('ffmpeg'); |
|
381 | + } |
|
382 | + } |
|
383 | + |
|
384 | + |
|
385 | + if (is_string($movieBinary)) { |
|
386 | + $this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ['movieBinary' => $movieBinary]); |
|
387 | + } |
|
388 | + } |
|
389 | + } |
|
390 | + |
|
391 | + private function registerCoreProvidersOffice(): void { |
|
392 | + $officeProviders = [ |
|
393 | + ['mimetype' => '/application\/msword/', 'class' => Preview\MSOfficeDoc::class], |
|
394 | + ['mimetype' => '/application\/vnd.ms-.*/', 'class' => Preview\MSOffice2003::class], |
|
395 | + ['mimetype' => '/application\/vnd.openxmlformats-officedocument.*/', 'class' => Preview\MSOffice2007::class], |
|
396 | + ['mimetype' => '/application\/vnd.oasis.opendocument.*/', 'class' => Preview\OpenDocument::class], |
|
397 | + ['mimetype' => '/application\/vnd.sun.xml.*/', 'class' => Preview\StarOffice::class], |
|
398 | + ['mimetype' => '/image\/emf/', 'class' => Preview\EMF::class], |
|
399 | + ]; |
|
400 | + |
|
401 | + $findBinary = true; |
|
402 | + $officeBinary = false; |
|
403 | + |
|
404 | + foreach ($officeProviders as $provider) { |
|
405 | + $class = $provider['class']; |
|
406 | + if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) { |
|
407 | + continue; |
|
408 | + } |
|
409 | + |
|
410 | + if ($findBinary) { |
|
411 | + // Office requires openoffice or libreoffice |
|
412 | + $officeBinary = $this->config->getSystemValue('preview_libreoffice_path', false); |
|
413 | + if ($officeBinary === false) { |
|
414 | + $officeBinary = $this->binaryFinder->findBinaryPath('libreoffice'); |
|
415 | + } |
|
416 | + if ($officeBinary === false) { |
|
417 | + $officeBinary = $this->binaryFinder->findBinaryPath('openoffice'); |
|
418 | + } |
|
419 | + $findBinary = false; |
|
420 | + } |
|
421 | + |
|
422 | + if ($officeBinary) { |
|
423 | + $this->registerCoreProvider($class, $provider['mimetype'], ['officeBinary' => $officeBinary]); |
|
424 | + } |
|
425 | + } |
|
426 | + } |
|
427 | + |
|
428 | + private function registerBootstrapProviders(): void { |
|
429 | + $context = $this->bootstrapCoordinator->getRegistrationContext(); |
|
430 | + |
|
431 | + if ($context === null) { |
|
432 | + // Just ignore for now |
|
433 | + return; |
|
434 | + } |
|
435 | + |
|
436 | + $providers = $context->getPreviewProviders(); |
|
437 | + foreach ($providers as $provider) { |
|
438 | + $key = $provider->getMimeTypeRegex() . '-' . $provider->getService(); |
|
439 | + if (array_key_exists($key, $this->loadedBootstrapProviders)) { |
|
440 | + // Do not load the provider more than once |
|
441 | + continue; |
|
442 | + } |
|
443 | + $this->loadedBootstrapProviders[$key] = null; |
|
444 | + |
|
445 | + $this->registerProvider($provider->getMimeTypeRegex(), function () use ($provider) { |
|
446 | + try { |
|
447 | + return $this->container->get($provider->getService()); |
|
448 | + } catch (QueryException $e) { |
|
449 | + return null; |
|
450 | + } |
|
451 | + }); |
|
452 | + } |
|
453 | + } |
|
454 | + |
|
455 | + /** |
|
456 | + * @throws NotFoundException if preview generation is disabled |
|
457 | + */ |
|
458 | + private function throwIfPreviewsDisabled(): void { |
|
459 | + if (!$this->enablePreviews) { |
|
460 | + throw new NotFoundException('Previews disabled'); |
|
461 | + } |
|
462 | + } |
|
463 | 463 | } |
@@ -28,88 +28,88 @@ |
||
28 | 28 | * @template-implements IEventListener<AMetadataEvent> |
29 | 29 | */ |
30 | 30 | class GenerateBlurhashMetadata implements IEventListener { |
31 | - private const COMPONENTS_X = 4; |
|
32 | - private const COMPONENTS_Y = 3; |
|
33 | - |
|
34 | - public function __construct( |
|
35 | - private IPreview $preview, |
|
36 | - ) { |
|
37 | - } |
|
38 | - |
|
39 | - /** |
|
40 | - * @throws NotPermittedException |
|
41 | - * @throws GenericFileException |
|
42 | - * @throws LockedException |
|
43 | - */ |
|
44 | - public function handle(Event $event): void { |
|
45 | - if (!($event instanceof MetadataLiveEvent) |
|
46 | - && !($event instanceof MetadataBackgroundEvent)) { |
|
47 | - return; |
|
48 | - } |
|
49 | - |
|
50 | - $file = $event->getNode(); |
|
51 | - if (!($file instanceof File)) { |
|
52 | - return; |
|
53 | - } |
|
54 | - |
|
55 | - $currentEtag = $file->getEtag(); |
|
56 | - $metadata = $event->getMetadata(); |
|
57 | - if ($metadata->getEtag('blurhash') === $currentEtag) { |
|
58 | - return; |
|
59 | - } |
|
60 | - |
|
61 | - // too heavy to run on the live thread, request a rerun as a background job |
|
62 | - if ($event instanceof MetadataLiveEvent) { |
|
63 | - $event->requestBackgroundJob(); |
|
64 | - return; |
|
65 | - } |
|
66 | - |
|
67 | - if (!str_starts_with($file->getMimetype(), 'image/')) { |
|
68 | - return; |
|
69 | - } |
|
70 | - |
|
71 | - $preview = $this->preview->getPreview($file, 64, 64, cacheResult: false); |
|
72 | - $image = @imagecreatefromstring($preview->getContent()); |
|
73 | - |
|
74 | - if (!$image) { |
|
75 | - return; |
|
76 | - } |
|
77 | - |
|
78 | - $metadata->setString('blurhash', $this->generateBlurHash($image)) |
|
79 | - ->setEtag('blurhash', $currentEtag); |
|
80 | - } |
|
81 | - |
|
82 | - /** |
|
83 | - * @param GdImage $image |
|
84 | - * |
|
85 | - * @return string |
|
86 | - */ |
|
87 | - public function generateBlurHash(GdImage $image): string { |
|
88 | - $width = imagesx($image); |
|
89 | - $height = imagesy($image); |
|
90 | - |
|
91 | - $pixels = []; |
|
92 | - for ($y = 0; $y < $height; ++$y) { |
|
93 | - $row = []; |
|
94 | - for ($x = 0; $x < $width; ++$x) { |
|
95 | - $index = imagecolorat($image, $x, $y); |
|
96 | - $colors = imagecolorsforindex($image, $index); |
|
97 | - $row[] = [$colors['red'], $colors['green'], $colors['blue']]; |
|
98 | - } |
|
99 | - |
|
100 | - $pixels[] = $row; |
|
101 | - } |
|
102 | - |
|
103 | - return Blurhash::encode($pixels, self::COMPONENTS_X, self::COMPONENTS_Y); |
|
104 | - } |
|
105 | - |
|
106 | - /** |
|
107 | - * @param IEventDispatcher $eventDispatcher |
|
108 | - * |
|
109 | - * @return void |
|
110 | - */ |
|
111 | - public static function loadListeners(IEventDispatcher $eventDispatcher): void { |
|
112 | - $eventDispatcher->addServiceListener(MetadataLiveEvent::class, self::class); |
|
113 | - $eventDispatcher->addServiceListener(MetadataBackgroundEvent::class, self::class); |
|
114 | - } |
|
31 | + private const COMPONENTS_X = 4; |
|
32 | + private const COMPONENTS_Y = 3; |
|
33 | + |
|
34 | + public function __construct( |
|
35 | + private IPreview $preview, |
|
36 | + ) { |
|
37 | + } |
|
38 | + |
|
39 | + /** |
|
40 | + * @throws NotPermittedException |
|
41 | + * @throws GenericFileException |
|
42 | + * @throws LockedException |
|
43 | + */ |
|
44 | + public function handle(Event $event): void { |
|
45 | + if (!($event instanceof MetadataLiveEvent) |
|
46 | + && !($event instanceof MetadataBackgroundEvent)) { |
|
47 | + return; |
|
48 | + } |
|
49 | + |
|
50 | + $file = $event->getNode(); |
|
51 | + if (!($file instanceof File)) { |
|
52 | + return; |
|
53 | + } |
|
54 | + |
|
55 | + $currentEtag = $file->getEtag(); |
|
56 | + $metadata = $event->getMetadata(); |
|
57 | + if ($metadata->getEtag('blurhash') === $currentEtag) { |
|
58 | + return; |
|
59 | + } |
|
60 | + |
|
61 | + // too heavy to run on the live thread, request a rerun as a background job |
|
62 | + if ($event instanceof MetadataLiveEvent) { |
|
63 | + $event->requestBackgroundJob(); |
|
64 | + return; |
|
65 | + } |
|
66 | + |
|
67 | + if (!str_starts_with($file->getMimetype(), 'image/')) { |
|
68 | + return; |
|
69 | + } |
|
70 | + |
|
71 | + $preview = $this->preview->getPreview($file, 64, 64, cacheResult: false); |
|
72 | + $image = @imagecreatefromstring($preview->getContent()); |
|
73 | + |
|
74 | + if (!$image) { |
|
75 | + return; |
|
76 | + } |
|
77 | + |
|
78 | + $metadata->setString('blurhash', $this->generateBlurHash($image)) |
|
79 | + ->setEtag('blurhash', $currentEtag); |
|
80 | + } |
|
81 | + |
|
82 | + /** |
|
83 | + * @param GdImage $image |
|
84 | + * |
|
85 | + * @return string |
|
86 | + */ |
|
87 | + public function generateBlurHash(GdImage $image): string { |
|
88 | + $width = imagesx($image); |
|
89 | + $height = imagesy($image); |
|
90 | + |
|
91 | + $pixels = []; |
|
92 | + for ($y = 0; $y < $height; ++$y) { |
|
93 | + $row = []; |
|
94 | + for ($x = 0; $x < $width; ++$x) { |
|
95 | + $index = imagecolorat($image, $x, $y); |
|
96 | + $colors = imagecolorsforindex($image, $index); |
|
97 | + $row[] = [$colors['red'], $colors['green'], $colors['blue']]; |
|
98 | + } |
|
99 | + |
|
100 | + $pixels[] = $row; |
|
101 | + } |
|
102 | + |
|
103 | + return Blurhash::encode($pixels, self::COMPONENTS_X, self::COMPONENTS_Y); |
|
104 | + } |
|
105 | + |
|
106 | + /** |
|
107 | + * @param IEventDispatcher $eventDispatcher |
|
108 | + * |
|
109 | + * @return void |
|
110 | + */ |
|
111 | + public static function loadListeners(IEventDispatcher $eventDispatcher): void { |
|
112 | + $eventDispatcher->addServiceListener(MetadataLiveEvent::class, self::class); |
|
113 | + $eventDispatcher->addServiceListener(MetadataBackgroundEvent::class, self::class); |
|
114 | + } |
|
115 | 115 | } |
@@ -25,592 +25,592 @@ |
||
25 | 25 | use Psr\Log\LoggerInterface; |
26 | 26 | |
27 | 27 | class Generator { |
28 | - public const SEMAPHORE_ID_ALL = 0x0a11; |
|
29 | - public const SEMAPHORE_ID_NEW = 0x07ea; |
|
30 | - |
|
31 | - public function __construct( |
|
32 | - private IConfig $config, |
|
33 | - private IPreview $previewManager, |
|
34 | - private IAppData $appData, |
|
35 | - private GeneratorHelper $helper, |
|
36 | - private IEventDispatcher $eventDispatcher, |
|
37 | - private LoggerInterface $logger, |
|
38 | - ) { |
|
39 | - } |
|
40 | - |
|
41 | - /** |
|
42 | - * Returns a preview of a file |
|
43 | - * |
|
44 | - * The cache is searched first and if nothing usable was found then a preview is |
|
45 | - * generated by one of the providers |
|
46 | - * |
|
47 | - * @return ISimpleFile |
|
48 | - * @throws NotFoundException |
|
49 | - * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
50 | - */ |
|
51 | - public function getPreview( |
|
52 | - File $file, |
|
53 | - int $width = -1, |
|
54 | - int $height = -1, |
|
55 | - bool $crop = false, |
|
56 | - string $mode = IPreview::MODE_FILL, |
|
57 | - ?string $mimeType = null, |
|
58 | - bool $cacheResult = true, |
|
59 | - ): ISimpleFile { |
|
60 | - $specification = [ |
|
61 | - 'width' => $width, |
|
62 | - 'height' => $height, |
|
63 | - 'crop' => $crop, |
|
64 | - 'mode' => $mode, |
|
65 | - ]; |
|
66 | - |
|
67 | - $this->eventDispatcher->dispatchTyped(new BeforePreviewFetchedEvent( |
|
68 | - $file, |
|
69 | - $width, |
|
70 | - $height, |
|
71 | - $crop, |
|
72 | - $mode, |
|
73 | - $mimeType, |
|
74 | - )); |
|
75 | - |
|
76 | - $this->logger->debug('Requesting preview for {path} with width={width}, height={height}, crop={crop}, mode={mode}, mimeType={mimeType}', [ |
|
77 | - 'path' => $file->getPath(), |
|
78 | - 'width' => $width, |
|
79 | - 'height' => $height, |
|
80 | - 'crop' => $crop, |
|
81 | - 'mode' => $mode, |
|
82 | - 'mimeType' => $mimeType, |
|
83 | - ]); |
|
84 | - |
|
85 | - |
|
86 | - // since we only ask for one preview, and the generate method return the last one it created, it returns the one we want |
|
87 | - return $this->generatePreviews($file, [$specification], $mimeType, $cacheResult); |
|
88 | - } |
|
89 | - |
|
90 | - /** |
|
91 | - * Generates previews of a file |
|
92 | - * |
|
93 | - * @throws NotFoundException |
|
94 | - * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
95 | - */ |
|
96 | - public function generatePreviews(File $file, array $specifications, ?string $mimeType = null, bool $cacheResult = true): ISimpleFile { |
|
97 | - //Make sure that we can read the file |
|
98 | - if (!$file->isReadable()) { |
|
99 | - $this->logger->warning('Cannot read file: {path}, skipping preview generation.', ['path' => $file->getPath()]); |
|
100 | - throw new NotFoundException('Cannot read file'); |
|
101 | - } |
|
102 | - |
|
103 | - if ($mimeType === null) { |
|
104 | - $mimeType = $file->getMimeType(); |
|
105 | - } |
|
106 | - |
|
107 | - $previewFolder = $this->getPreviewFolder($file); |
|
108 | - // List every existing preview first instead of trying to find them one by one |
|
109 | - $previewFiles = $previewFolder->getDirectoryListing(); |
|
110 | - |
|
111 | - $previewVersion = ''; |
|
112 | - if ($file instanceof IVersionedPreviewFile) { |
|
113 | - $previewVersion = $file->getPreviewVersion() . '-'; |
|
114 | - } |
|
115 | - |
|
116 | - // Get the max preview and infer the max preview sizes from that |
|
117 | - $maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion); |
|
118 | - $maxPreviewImage = null; // only load the image when we need it |
|
119 | - if ($maxPreview->getSize() === 0) { |
|
120 | - $maxPreview->delete(); |
|
121 | - $this->logger->error('Max preview generated for file {path} has size 0, deleting and throwing exception.', ['path' => $file->getPath()]); |
|
122 | - throw new NotFoundException('Max preview size 0, invalid!'); |
|
123 | - } |
|
124 | - |
|
125 | - [$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion); |
|
126 | - |
|
127 | - if ($maxWidth <= 0 || $maxHeight <= 0) { |
|
128 | - throw new NotFoundException('The maximum preview sizes are zero or less pixels'); |
|
129 | - } |
|
130 | - |
|
131 | - $preview = null; |
|
132 | - |
|
133 | - foreach ($specifications as $specification) { |
|
134 | - $width = $specification['width'] ?? -1; |
|
135 | - $height = $specification['height'] ?? -1; |
|
136 | - $crop = $specification['crop'] ?? false; |
|
137 | - $mode = $specification['mode'] ?? IPreview::MODE_FILL; |
|
138 | - |
|
139 | - // If both width and height are -1 we just want the max preview |
|
140 | - if ($width === -1 && $height === -1) { |
|
141 | - $width = $maxWidth; |
|
142 | - $height = $maxHeight; |
|
143 | - } |
|
144 | - |
|
145 | - // Calculate the preview size |
|
146 | - [$width, $height] = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight); |
|
147 | - |
|
148 | - // No need to generate a preview that is just the max preview |
|
149 | - if ($width === $maxWidth && $height === $maxHeight) { |
|
150 | - // ensure correct return value if this was the last one |
|
151 | - $preview = $maxPreview; |
|
152 | - continue; |
|
153 | - } |
|
154 | - |
|
155 | - // Try to get a cached preview. Else generate (and store) one |
|
156 | - try { |
|
157 | - try { |
|
158 | - $preview = $this->getCachedPreview($previewFiles, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion); |
|
159 | - } catch (NotFoundException $e) { |
|
160 | - if (!$this->previewManager->isMimeSupported($mimeType)) { |
|
161 | - throw new NotFoundException(); |
|
162 | - } |
|
163 | - |
|
164 | - if ($maxPreviewImage === null) { |
|
165 | - $maxPreviewImage = $this->helper->getImage($maxPreview); |
|
166 | - } |
|
167 | - |
|
168 | - $this->logger->warning('Cached preview not found for file {path}, generating a new preview.', ['path' => $file->getPath()]); |
|
169 | - $preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion, $cacheResult); |
|
170 | - // New file, augment our array |
|
171 | - $previewFiles[] = $preview; |
|
172 | - } |
|
173 | - } catch (\InvalidArgumentException $e) { |
|
174 | - throw new NotFoundException('', 0, $e); |
|
175 | - } |
|
176 | - |
|
177 | - if ($preview->getSize() === 0) { |
|
178 | - $preview->delete(); |
|
179 | - throw new NotFoundException('Cached preview size 0, invalid!'); |
|
180 | - } |
|
181 | - } |
|
182 | - assert($preview !== null); |
|
183 | - |
|
184 | - // Free memory being used by the embedded image resource. Without this the image is kept in memory indefinitely. |
|
185 | - // Garbage Collection does NOT free this memory. We have to do it ourselves. |
|
186 | - if ($maxPreviewImage instanceof \OCP\Image) { |
|
187 | - $maxPreviewImage->destroy(); |
|
188 | - } |
|
189 | - |
|
190 | - return $preview; |
|
191 | - } |
|
192 | - |
|
193 | - /** |
|
194 | - * Acquire a semaphore of the specified id and concurrency, blocking if necessary. |
|
195 | - * Return an identifier of the semaphore on success, which can be used to release it via |
|
196 | - * {@see Generator::unguardWithSemaphore()}. |
|
197 | - * |
|
198 | - * @param int $semId |
|
199 | - * @param int $concurrency |
|
200 | - * @return false|\SysvSemaphore the semaphore on success or false on failure |
|
201 | - */ |
|
202 | - public static function guardWithSemaphore(int $semId, int $concurrency) { |
|
203 | - if (!extension_loaded('sysvsem')) { |
|
204 | - return false; |
|
205 | - } |
|
206 | - $sem = sem_get($semId, $concurrency); |
|
207 | - if ($sem === false) { |
|
208 | - return false; |
|
209 | - } |
|
210 | - if (!sem_acquire($sem)) { |
|
211 | - return false; |
|
212 | - } |
|
213 | - return $sem; |
|
214 | - } |
|
215 | - |
|
216 | - /** |
|
217 | - * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}. |
|
218 | - * |
|
219 | - * @param false|\SysvSemaphore $semId the semaphore identifier returned by guardWithSemaphore |
|
220 | - * @return bool |
|
221 | - */ |
|
222 | - public static function unguardWithSemaphore(false|\SysvSemaphore $semId): bool { |
|
223 | - if ($semId === false || !($semId instanceof \SysvSemaphore)) { |
|
224 | - return false; |
|
225 | - } |
|
226 | - return sem_release($semId); |
|
227 | - } |
|
228 | - |
|
229 | - /** |
|
230 | - * Get the number of concurrent threads supported by the host. |
|
231 | - * |
|
232 | - * @return int number of concurrent threads, or 0 if it cannot be determined |
|
233 | - */ |
|
234 | - public static function getHardwareConcurrency(): int { |
|
235 | - static $width; |
|
236 | - |
|
237 | - if (!isset($width)) { |
|
238 | - if (function_exists('ini_get')) { |
|
239 | - $openBasedir = ini_get('open_basedir'); |
|
240 | - if (empty($openBasedir) || strpos($openBasedir, '/proc/cpuinfo') !== false) { |
|
241 | - $width = is_readable('/proc/cpuinfo') ? substr_count(file_get_contents('/proc/cpuinfo'), 'processor') : 0; |
|
242 | - } else { |
|
243 | - $width = 0; |
|
244 | - } |
|
245 | - } else { |
|
246 | - $width = 0; |
|
247 | - } |
|
248 | - } |
|
249 | - return $width; |
|
250 | - } |
|
251 | - |
|
252 | - /** |
|
253 | - * Get number of concurrent preview generations from system config |
|
254 | - * |
|
255 | - * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`, |
|
256 | - * are available. If not set, the default values are determined with the hardware concurrency |
|
257 | - * of the host. In case the hardware concurrency cannot be determined, or the user sets an |
|
258 | - * invalid value, fallback values are: |
|
259 | - * For new images whose previews do not exist and need to be generated, 4; |
|
260 | - * For all preview generation requests, 8. |
|
261 | - * Value of `preview_concurrency_all` should be greater than or equal to that of |
|
262 | - * `preview_concurrency_new`, otherwise, the latter is returned. |
|
263 | - * |
|
264 | - * @param string $type either `preview_concurrency_new` or `preview_concurrency_all` |
|
265 | - * @return int number of concurrent preview generations, or -1 if $type is invalid |
|
266 | - */ |
|
267 | - public function getNumConcurrentPreviews(string $type): int { |
|
268 | - static $cached = []; |
|
269 | - if (array_key_exists($type, $cached)) { |
|
270 | - return $cached[$type]; |
|
271 | - } |
|
272 | - |
|
273 | - $hardwareConcurrency = self::getHardwareConcurrency(); |
|
274 | - switch ($type) { |
|
275 | - case 'preview_concurrency_all': |
|
276 | - $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8; |
|
277 | - $concurrency_all = $this->config->getSystemValueInt($type, $fallback); |
|
278 | - $concurrency_new = $this->getNumConcurrentPreviews('preview_concurrency_new'); |
|
279 | - $cached[$type] = max($concurrency_all, $concurrency_new); |
|
280 | - break; |
|
281 | - case 'preview_concurrency_new': |
|
282 | - $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4; |
|
283 | - $cached[$type] = $this->config->getSystemValueInt($type, $fallback); |
|
284 | - break; |
|
285 | - default: |
|
286 | - return -1; |
|
287 | - } |
|
288 | - return $cached[$type]; |
|
289 | - } |
|
290 | - |
|
291 | - /** |
|
292 | - * @param ISimpleFolder $previewFolder |
|
293 | - * @param ISimpleFile[] $previewFiles |
|
294 | - * @param File $file |
|
295 | - * @param string $mimeType |
|
296 | - * @param string $prefix |
|
297 | - * @return ISimpleFile |
|
298 | - * @throws NotFoundException |
|
299 | - */ |
|
300 | - private function getMaxPreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, $mimeType, $prefix) { |
|
301 | - // We don't know the max preview size, so we can't use getCachedPreview. |
|
302 | - // It might have been generated with a higher resolution than the current value. |
|
303 | - foreach ($previewFiles as $node) { |
|
304 | - $name = $node->getName(); |
|
305 | - if (($prefix === '' || str_starts_with($name, $prefix)) && strpos($name, 'max')) { |
|
306 | - return $node; |
|
307 | - } |
|
308 | - } |
|
309 | - |
|
310 | - $maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096); |
|
311 | - $maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096); |
|
312 | - |
|
313 | - return $this->generateProviderPreview($previewFolder, $file, $maxWidth, $maxHeight, false, true, $mimeType, $prefix); |
|
314 | - } |
|
315 | - |
|
316 | - private function generateProviderPreview(ISimpleFolder $previewFolder, File $file, int $width, int $height, bool $crop, bool $max, string $mimeType, string $prefix) { |
|
317 | - $previewProviders = $this->previewManager->getProviders(); |
|
318 | - foreach ($previewProviders as $supportedMimeType => $providers) { |
|
319 | - // Filter out providers that does not support this mime |
|
320 | - if (!preg_match($supportedMimeType, $mimeType)) { |
|
321 | - continue; |
|
322 | - } |
|
323 | - |
|
324 | - foreach ($providers as $providerClosure) { |
|
325 | - $provider = $this->helper->getProvider($providerClosure); |
|
326 | - if (!($provider instanceof IProviderV2)) { |
|
327 | - continue; |
|
328 | - } |
|
329 | - |
|
330 | - if (!$provider->isAvailable($file)) { |
|
331 | - continue; |
|
332 | - } |
|
333 | - |
|
334 | - $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); |
|
335 | - $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); |
|
336 | - try { |
|
337 | - $this->logger->debug('Calling preview provider for {mimeType} with width={width}, height={height}', [ |
|
338 | - 'mimeType' => $mimeType, |
|
339 | - 'width' => $width, |
|
340 | - 'height' => $height, |
|
341 | - ]); |
|
342 | - $preview = $this->helper->getThumbnail($provider, $file, $width, $height); |
|
343 | - } finally { |
|
344 | - self::unguardWithSemaphore($sem); |
|
345 | - } |
|
346 | - |
|
347 | - if (!($preview instanceof IImage)) { |
|
348 | - continue; |
|
349 | - } |
|
350 | - |
|
351 | - $path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix); |
|
352 | - try { |
|
353 | - if ($preview instanceof IStreamImage) { |
|
354 | - return $previewFolder->newFile($path, $preview->resource()); |
|
355 | - } else { |
|
356 | - return $previewFolder->newFile($path, $preview->data()); |
|
357 | - } |
|
358 | - } catch (NotPermittedException $e) { |
|
359 | - throw new NotFoundException(); |
|
360 | - } |
|
361 | - |
|
362 | - return $file; |
|
363 | - } |
|
364 | - } |
|
365 | - |
|
366 | - throw new NotFoundException('No provider successfully handled the preview generation'); |
|
367 | - } |
|
368 | - |
|
369 | - /** |
|
370 | - * @param ISimpleFile $file |
|
371 | - * @param string $prefix |
|
372 | - * @return int[] |
|
373 | - */ |
|
374 | - private function getPreviewSize(ISimpleFile $file, string $prefix = '') { |
|
375 | - $size = explode('-', substr($file->getName(), strlen($prefix))); |
|
376 | - return [(int)$size[0], (int)$size[1]]; |
|
377 | - } |
|
378 | - |
|
379 | - /** |
|
380 | - * @param int $width |
|
381 | - * @param int $height |
|
382 | - * @param bool $crop |
|
383 | - * @param bool $max |
|
384 | - * @param string $mimeType |
|
385 | - * @param string $prefix |
|
386 | - * @return string |
|
387 | - */ |
|
388 | - private function generatePath($width, $height, $crop, $max, $mimeType, $prefix) { |
|
389 | - $path = $prefix . (string)$width . '-' . (string)$height; |
|
390 | - if ($crop) { |
|
391 | - $path .= '-crop'; |
|
392 | - } |
|
393 | - if ($max) { |
|
394 | - $path .= '-max'; |
|
395 | - } |
|
396 | - |
|
397 | - $ext = $this->getExtension($mimeType); |
|
398 | - $path .= '.' . $ext; |
|
399 | - return $path; |
|
400 | - } |
|
401 | - |
|
402 | - |
|
403 | - /** |
|
404 | - * @param int $width |
|
405 | - * @param int $height |
|
406 | - * @param bool $crop |
|
407 | - * @param string $mode |
|
408 | - * @param int $maxWidth |
|
409 | - * @param int $maxHeight |
|
410 | - * @return int[] |
|
411 | - */ |
|
412 | - private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) { |
|
413 | - /* |
|
28 | + public const SEMAPHORE_ID_ALL = 0x0a11; |
|
29 | + public const SEMAPHORE_ID_NEW = 0x07ea; |
|
30 | + |
|
31 | + public function __construct( |
|
32 | + private IConfig $config, |
|
33 | + private IPreview $previewManager, |
|
34 | + private IAppData $appData, |
|
35 | + private GeneratorHelper $helper, |
|
36 | + private IEventDispatcher $eventDispatcher, |
|
37 | + private LoggerInterface $logger, |
|
38 | + ) { |
|
39 | + } |
|
40 | + |
|
41 | + /** |
|
42 | + * Returns a preview of a file |
|
43 | + * |
|
44 | + * The cache is searched first and if nothing usable was found then a preview is |
|
45 | + * generated by one of the providers |
|
46 | + * |
|
47 | + * @return ISimpleFile |
|
48 | + * @throws NotFoundException |
|
49 | + * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
50 | + */ |
|
51 | + public function getPreview( |
|
52 | + File $file, |
|
53 | + int $width = -1, |
|
54 | + int $height = -1, |
|
55 | + bool $crop = false, |
|
56 | + string $mode = IPreview::MODE_FILL, |
|
57 | + ?string $mimeType = null, |
|
58 | + bool $cacheResult = true, |
|
59 | + ): ISimpleFile { |
|
60 | + $specification = [ |
|
61 | + 'width' => $width, |
|
62 | + 'height' => $height, |
|
63 | + 'crop' => $crop, |
|
64 | + 'mode' => $mode, |
|
65 | + ]; |
|
66 | + |
|
67 | + $this->eventDispatcher->dispatchTyped(new BeforePreviewFetchedEvent( |
|
68 | + $file, |
|
69 | + $width, |
|
70 | + $height, |
|
71 | + $crop, |
|
72 | + $mode, |
|
73 | + $mimeType, |
|
74 | + )); |
|
75 | + |
|
76 | + $this->logger->debug('Requesting preview for {path} with width={width}, height={height}, crop={crop}, mode={mode}, mimeType={mimeType}', [ |
|
77 | + 'path' => $file->getPath(), |
|
78 | + 'width' => $width, |
|
79 | + 'height' => $height, |
|
80 | + 'crop' => $crop, |
|
81 | + 'mode' => $mode, |
|
82 | + 'mimeType' => $mimeType, |
|
83 | + ]); |
|
84 | + |
|
85 | + |
|
86 | + // since we only ask for one preview, and the generate method return the last one it created, it returns the one we want |
|
87 | + return $this->generatePreviews($file, [$specification], $mimeType, $cacheResult); |
|
88 | + } |
|
89 | + |
|
90 | + /** |
|
91 | + * Generates previews of a file |
|
92 | + * |
|
93 | + * @throws NotFoundException |
|
94 | + * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
95 | + */ |
|
96 | + public function generatePreviews(File $file, array $specifications, ?string $mimeType = null, bool $cacheResult = true): ISimpleFile { |
|
97 | + //Make sure that we can read the file |
|
98 | + if (!$file->isReadable()) { |
|
99 | + $this->logger->warning('Cannot read file: {path}, skipping preview generation.', ['path' => $file->getPath()]); |
|
100 | + throw new NotFoundException('Cannot read file'); |
|
101 | + } |
|
102 | + |
|
103 | + if ($mimeType === null) { |
|
104 | + $mimeType = $file->getMimeType(); |
|
105 | + } |
|
106 | + |
|
107 | + $previewFolder = $this->getPreviewFolder($file); |
|
108 | + // List every existing preview first instead of trying to find them one by one |
|
109 | + $previewFiles = $previewFolder->getDirectoryListing(); |
|
110 | + |
|
111 | + $previewVersion = ''; |
|
112 | + if ($file instanceof IVersionedPreviewFile) { |
|
113 | + $previewVersion = $file->getPreviewVersion() . '-'; |
|
114 | + } |
|
115 | + |
|
116 | + // Get the max preview and infer the max preview sizes from that |
|
117 | + $maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion); |
|
118 | + $maxPreviewImage = null; // only load the image when we need it |
|
119 | + if ($maxPreview->getSize() === 0) { |
|
120 | + $maxPreview->delete(); |
|
121 | + $this->logger->error('Max preview generated for file {path} has size 0, deleting and throwing exception.', ['path' => $file->getPath()]); |
|
122 | + throw new NotFoundException('Max preview size 0, invalid!'); |
|
123 | + } |
|
124 | + |
|
125 | + [$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion); |
|
126 | + |
|
127 | + if ($maxWidth <= 0 || $maxHeight <= 0) { |
|
128 | + throw new NotFoundException('The maximum preview sizes are zero or less pixels'); |
|
129 | + } |
|
130 | + |
|
131 | + $preview = null; |
|
132 | + |
|
133 | + foreach ($specifications as $specification) { |
|
134 | + $width = $specification['width'] ?? -1; |
|
135 | + $height = $specification['height'] ?? -1; |
|
136 | + $crop = $specification['crop'] ?? false; |
|
137 | + $mode = $specification['mode'] ?? IPreview::MODE_FILL; |
|
138 | + |
|
139 | + // If both width and height are -1 we just want the max preview |
|
140 | + if ($width === -1 && $height === -1) { |
|
141 | + $width = $maxWidth; |
|
142 | + $height = $maxHeight; |
|
143 | + } |
|
144 | + |
|
145 | + // Calculate the preview size |
|
146 | + [$width, $height] = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight); |
|
147 | + |
|
148 | + // No need to generate a preview that is just the max preview |
|
149 | + if ($width === $maxWidth && $height === $maxHeight) { |
|
150 | + // ensure correct return value if this was the last one |
|
151 | + $preview = $maxPreview; |
|
152 | + continue; |
|
153 | + } |
|
154 | + |
|
155 | + // Try to get a cached preview. Else generate (and store) one |
|
156 | + try { |
|
157 | + try { |
|
158 | + $preview = $this->getCachedPreview($previewFiles, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion); |
|
159 | + } catch (NotFoundException $e) { |
|
160 | + if (!$this->previewManager->isMimeSupported($mimeType)) { |
|
161 | + throw new NotFoundException(); |
|
162 | + } |
|
163 | + |
|
164 | + if ($maxPreviewImage === null) { |
|
165 | + $maxPreviewImage = $this->helper->getImage($maxPreview); |
|
166 | + } |
|
167 | + |
|
168 | + $this->logger->warning('Cached preview not found for file {path}, generating a new preview.', ['path' => $file->getPath()]); |
|
169 | + $preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion, $cacheResult); |
|
170 | + // New file, augment our array |
|
171 | + $previewFiles[] = $preview; |
|
172 | + } |
|
173 | + } catch (\InvalidArgumentException $e) { |
|
174 | + throw new NotFoundException('', 0, $e); |
|
175 | + } |
|
176 | + |
|
177 | + if ($preview->getSize() === 0) { |
|
178 | + $preview->delete(); |
|
179 | + throw new NotFoundException('Cached preview size 0, invalid!'); |
|
180 | + } |
|
181 | + } |
|
182 | + assert($preview !== null); |
|
183 | + |
|
184 | + // Free memory being used by the embedded image resource. Without this the image is kept in memory indefinitely. |
|
185 | + // Garbage Collection does NOT free this memory. We have to do it ourselves. |
|
186 | + if ($maxPreviewImage instanceof \OCP\Image) { |
|
187 | + $maxPreviewImage->destroy(); |
|
188 | + } |
|
189 | + |
|
190 | + return $preview; |
|
191 | + } |
|
192 | + |
|
193 | + /** |
|
194 | + * Acquire a semaphore of the specified id and concurrency, blocking if necessary. |
|
195 | + * Return an identifier of the semaphore on success, which can be used to release it via |
|
196 | + * {@see Generator::unguardWithSemaphore()}. |
|
197 | + * |
|
198 | + * @param int $semId |
|
199 | + * @param int $concurrency |
|
200 | + * @return false|\SysvSemaphore the semaphore on success or false on failure |
|
201 | + */ |
|
202 | + public static function guardWithSemaphore(int $semId, int $concurrency) { |
|
203 | + if (!extension_loaded('sysvsem')) { |
|
204 | + return false; |
|
205 | + } |
|
206 | + $sem = sem_get($semId, $concurrency); |
|
207 | + if ($sem === false) { |
|
208 | + return false; |
|
209 | + } |
|
210 | + if (!sem_acquire($sem)) { |
|
211 | + return false; |
|
212 | + } |
|
213 | + return $sem; |
|
214 | + } |
|
215 | + |
|
216 | + /** |
|
217 | + * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}. |
|
218 | + * |
|
219 | + * @param false|\SysvSemaphore $semId the semaphore identifier returned by guardWithSemaphore |
|
220 | + * @return bool |
|
221 | + */ |
|
222 | + public static function unguardWithSemaphore(false|\SysvSemaphore $semId): bool { |
|
223 | + if ($semId === false || !($semId instanceof \SysvSemaphore)) { |
|
224 | + return false; |
|
225 | + } |
|
226 | + return sem_release($semId); |
|
227 | + } |
|
228 | + |
|
229 | + /** |
|
230 | + * Get the number of concurrent threads supported by the host. |
|
231 | + * |
|
232 | + * @return int number of concurrent threads, or 0 if it cannot be determined |
|
233 | + */ |
|
234 | + public static function getHardwareConcurrency(): int { |
|
235 | + static $width; |
|
236 | + |
|
237 | + if (!isset($width)) { |
|
238 | + if (function_exists('ini_get')) { |
|
239 | + $openBasedir = ini_get('open_basedir'); |
|
240 | + if (empty($openBasedir) || strpos($openBasedir, '/proc/cpuinfo') !== false) { |
|
241 | + $width = is_readable('/proc/cpuinfo') ? substr_count(file_get_contents('/proc/cpuinfo'), 'processor') : 0; |
|
242 | + } else { |
|
243 | + $width = 0; |
|
244 | + } |
|
245 | + } else { |
|
246 | + $width = 0; |
|
247 | + } |
|
248 | + } |
|
249 | + return $width; |
|
250 | + } |
|
251 | + |
|
252 | + /** |
|
253 | + * Get number of concurrent preview generations from system config |
|
254 | + * |
|
255 | + * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`, |
|
256 | + * are available. If not set, the default values are determined with the hardware concurrency |
|
257 | + * of the host. In case the hardware concurrency cannot be determined, or the user sets an |
|
258 | + * invalid value, fallback values are: |
|
259 | + * For new images whose previews do not exist and need to be generated, 4; |
|
260 | + * For all preview generation requests, 8. |
|
261 | + * Value of `preview_concurrency_all` should be greater than or equal to that of |
|
262 | + * `preview_concurrency_new`, otherwise, the latter is returned. |
|
263 | + * |
|
264 | + * @param string $type either `preview_concurrency_new` or `preview_concurrency_all` |
|
265 | + * @return int number of concurrent preview generations, or -1 if $type is invalid |
|
266 | + */ |
|
267 | + public function getNumConcurrentPreviews(string $type): int { |
|
268 | + static $cached = []; |
|
269 | + if (array_key_exists($type, $cached)) { |
|
270 | + return $cached[$type]; |
|
271 | + } |
|
272 | + |
|
273 | + $hardwareConcurrency = self::getHardwareConcurrency(); |
|
274 | + switch ($type) { |
|
275 | + case 'preview_concurrency_all': |
|
276 | + $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8; |
|
277 | + $concurrency_all = $this->config->getSystemValueInt($type, $fallback); |
|
278 | + $concurrency_new = $this->getNumConcurrentPreviews('preview_concurrency_new'); |
|
279 | + $cached[$type] = max($concurrency_all, $concurrency_new); |
|
280 | + break; |
|
281 | + case 'preview_concurrency_new': |
|
282 | + $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4; |
|
283 | + $cached[$type] = $this->config->getSystemValueInt($type, $fallback); |
|
284 | + break; |
|
285 | + default: |
|
286 | + return -1; |
|
287 | + } |
|
288 | + return $cached[$type]; |
|
289 | + } |
|
290 | + |
|
291 | + /** |
|
292 | + * @param ISimpleFolder $previewFolder |
|
293 | + * @param ISimpleFile[] $previewFiles |
|
294 | + * @param File $file |
|
295 | + * @param string $mimeType |
|
296 | + * @param string $prefix |
|
297 | + * @return ISimpleFile |
|
298 | + * @throws NotFoundException |
|
299 | + */ |
|
300 | + private function getMaxPreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, $mimeType, $prefix) { |
|
301 | + // We don't know the max preview size, so we can't use getCachedPreview. |
|
302 | + // It might have been generated with a higher resolution than the current value. |
|
303 | + foreach ($previewFiles as $node) { |
|
304 | + $name = $node->getName(); |
|
305 | + if (($prefix === '' || str_starts_with($name, $prefix)) && strpos($name, 'max')) { |
|
306 | + return $node; |
|
307 | + } |
|
308 | + } |
|
309 | + |
|
310 | + $maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096); |
|
311 | + $maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096); |
|
312 | + |
|
313 | + return $this->generateProviderPreview($previewFolder, $file, $maxWidth, $maxHeight, false, true, $mimeType, $prefix); |
|
314 | + } |
|
315 | + |
|
316 | + private function generateProviderPreview(ISimpleFolder $previewFolder, File $file, int $width, int $height, bool $crop, bool $max, string $mimeType, string $prefix) { |
|
317 | + $previewProviders = $this->previewManager->getProviders(); |
|
318 | + foreach ($previewProviders as $supportedMimeType => $providers) { |
|
319 | + // Filter out providers that does not support this mime |
|
320 | + if (!preg_match($supportedMimeType, $mimeType)) { |
|
321 | + continue; |
|
322 | + } |
|
323 | + |
|
324 | + foreach ($providers as $providerClosure) { |
|
325 | + $provider = $this->helper->getProvider($providerClosure); |
|
326 | + if (!($provider instanceof IProviderV2)) { |
|
327 | + continue; |
|
328 | + } |
|
329 | + |
|
330 | + if (!$provider->isAvailable($file)) { |
|
331 | + continue; |
|
332 | + } |
|
333 | + |
|
334 | + $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); |
|
335 | + $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); |
|
336 | + try { |
|
337 | + $this->logger->debug('Calling preview provider for {mimeType} with width={width}, height={height}', [ |
|
338 | + 'mimeType' => $mimeType, |
|
339 | + 'width' => $width, |
|
340 | + 'height' => $height, |
|
341 | + ]); |
|
342 | + $preview = $this->helper->getThumbnail($provider, $file, $width, $height); |
|
343 | + } finally { |
|
344 | + self::unguardWithSemaphore($sem); |
|
345 | + } |
|
346 | + |
|
347 | + if (!($preview instanceof IImage)) { |
|
348 | + continue; |
|
349 | + } |
|
350 | + |
|
351 | + $path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix); |
|
352 | + try { |
|
353 | + if ($preview instanceof IStreamImage) { |
|
354 | + return $previewFolder->newFile($path, $preview->resource()); |
|
355 | + } else { |
|
356 | + return $previewFolder->newFile($path, $preview->data()); |
|
357 | + } |
|
358 | + } catch (NotPermittedException $e) { |
|
359 | + throw new NotFoundException(); |
|
360 | + } |
|
361 | + |
|
362 | + return $file; |
|
363 | + } |
|
364 | + } |
|
365 | + |
|
366 | + throw new NotFoundException('No provider successfully handled the preview generation'); |
|
367 | + } |
|
368 | + |
|
369 | + /** |
|
370 | + * @param ISimpleFile $file |
|
371 | + * @param string $prefix |
|
372 | + * @return int[] |
|
373 | + */ |
|
374 | + private function getPreviewSize(ISimpleFile $file, string $prefix = '') { |
|
375 | + $size = explode('-', substr($file->getName(), strlen($prefix))); |
|
376 | + return [(int)$size[0], (int)$size[1]]; |
|
377 | + } |
|
378 | + |
|
379 | + /** |
|
380 | + * @param int $width |
|
381 | + * @param int $height |
|
382 | + * @param bool $crop |
|
383 | + * @param bool $max |
|
384 | + * @param string $mimeType |
|
385 | + * @param string $prefix |
|
386 | + * @return string |
|
387 | + */ |
|
388 | + private function generatePath($width, $height, $crop, $max, $mimeType, $prefix) { |
|
389 | + $path = $prefix . (string)$width . '-' . (string)$height; |
|
390 | + if ($crop) { |
|
391 | + $path .= '-crop'; |
|
392 | + } |
|
393 | + if ($max) { |
|
394 | + $path .= '-max'; |
|
395 | + } |
|
396 | + |
|
397 | + $ext = $this->getExtension($mimeType); |
|
398 | + $path .= '.' . $ext; |
|
399 | + return $path; |
|
400 | + } |
|
401 | + |
|
402 | + |
|
403 | + /** |
|
404 | + * @param int $width |
|
405 | + * @param int $height |
|
406 | + * @param bool $crop |
|
407 | + * @param string $mode |
|
408 | + * @param int $maxWidth |
|
409 | + * @param int $maxHeight |
|
410 | + * @return int[] |
|
411 | + */ |
|
412 | + private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) { |
|
413 | + /* |
|
414 | 414 | * If we are not cropping we have to make sure the requested image |
415 | 415 | * respects the aspect ratio of the original. |
416 | 416 | */ |
417 | - if (!$crop) { |
|
418 | - $ratio = $maxHeight / $maxWidth; |
|
417 | + if (!$crop) { |
|
418 | + $ratio = $maxHeight / $maxWidth; |
|
419 | 419 | |
420 | - if ($width === -1) { |
|
421 | - $width = $height / $ratio; |
|
422 | - } |
|
423 | - if ($height === -1) { |
|
424 | - $height = $width * $ratio; |
|
425 | - } |
|
420 | + if ($width === -1) { |
|
421 | + $width = $height / $ratio; |
|
422 | + } |
|
423 | + if ($height === -1) { |
|
424 | + $height = $width * $ratio; |
|
425 | + } |
|
426 | 426 | |
427 | - $ratioH = $height / $maxHeight; |
|
428 | - $ratioW = $width / $maxWidth; |
|
427 | + $ratioH = $height / $maxHeight; |
|
428 | + $ratioW = $width / $maxWidth; |
|
429 | 429 | |
430 | - /* |
|
430 | + /* |
|
431 | 431 | * Fill means that the $height and $width are the max |
432 | 432 | * Cover means min. |
433 | 433 | */ |
434 | - if ($mode === IPreview::MODE_FILL) { |
|
435 | - if ($ratioH > $ratioW) { |
|
436 | - $height = $width * $ratio; |
|
437 | - } else { |
|
438 | - $width = $height / $ratio; |
|
439 | - } |
|
440 | - } elseif ($mode === IPreview::MODE_COVER) { |
|
441 | - if ($ratioH > $ratioW) { |
|
442 | - $width = $height / $ratio; |
|
443 | - } else { |
|
444 | - $height = $width * $ratio; |
|
445 | - } |
|
446 | - } |
|
447 | - } |
|
448 | - |
|
449 | - if ($height !== $maxHeight && $width !== $maxWidth) { |
|
450 | - /* |
|
434 | + if ($mode === IPreview::MODE_FILL) { |
|
435 | + if ($ratioH > $ratioW) { |
|
436 | + $height = $width * $ratio; |
|
437 | + } else { |
|
438 | + $width = $height / $ratio; |
|
439 | + } |
|
440 | + } elseif ($mode === IPreview::MODE_COVER) { |
|
441 | + if ($ratioH > $ratioW) { |
|
442 | + $width = $height / $ratio; |
|
443 | + } else { |
|
444 | + $height = $width * $ratio; |
|
445 | + } |
|
446 | + } |
|
447 | + } |
|
448 | + |
|
449 | + if ($height !== $maxHeight && $width !== $maxWidth) { |
|
450 | + /* |
|
451 | 451 | * Scale to the nearest power of four |
452 | 452 | */ |
453 | - $pow4height = 4 ** ceil(log($height) / log(4)); |
|
454 | - $pow4width = 4 ** ceil(log($width) / log(4)); |
|
455 | - |
|
456 | - // Minimum size is 64 |
|
457 | - $pow4height = max($pow4height, 64); |
|
458 | - $pow4width = max($pow4width, 64); |
|
459 | - |
|
460 | - $ratioH = $height / $pow4height; |
|
461 | - $ratioW = $width / $pow4width; |
|
462 | - |
|
463 | - if ($ratioH < $ratioW) { |
|
464 | - $width = $pow4width; |
|
465 | - $height /= $ratioW; |
|
466 | - } else { |
|
467 | - $height = $pow4height; |
|
468 | - $width /= $ratioH; |
|
469 | - } |
|
470 | - } |
|
471 | - |
|
472 | - /* |
|
453 | + $pow4height = 4 ** ceil(log($height) / log(4)); |
|
454 | + $pow4width = 4 ** ceil(log($width) / log(4)); |
|
455 | + |
|
456 | + // Minimum size is 64 |
|
457 | + $pow4height = max($pow4height, 64); |
|
458 | + $pow4width = max($pow4width, 64); |
|
459 | + |
|
460 | + $ratioH = $height / $pow4height; |
|
461 | + $ratioW = $width / $pow4width; |
|
462 | + |
|
463 | + if ($ratioH < $ratioW) { |
|
464 | + $width = $pow4width; |
|
465 | + $height /= $ratioW; |
|
466 | + } else { |
|
467 | + $height = $pow4height; |
|
468 | + $width /= $ratioH; |
|
469 | + } |
|
470 | + } |
|
471 | + |
|
472 | + /* |
|
473 | 473 | * Make sure the requested height and width fall within the max |
474 | 474 | * of the preview. |
475 | 475 | */ |
476 | - if ($height > $maxHeight) { |
|
477 | - $ratio = $height / $maxHeight; |
|
478 | - $height = $maxHeight; |
|
479 | - $width /= $ratio; |
|
480 | - } |
|
481 | - if ($width > $maxWidth) { |
|
482 | - $ratio = $width / $maxWidth; |
|
483 | - $width = $maxWidth; |
|
484 | - $height /= $ratio; |
|
485 | - } |
|
486 | - |
|
487 | - return [(int)round($width), (int)round($height)]; |
|
488 | - } |
|
489 | - |
|
490 | - /** |
|
491 | - * @throws NotFoundException |
|
492 | - * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
493 | - */ |
|
494 | - private function generatePreview( |
|
495 | - ISimpleFolder $previewFolder, |
|
496 | - IImage $maxPreview, |
|
497 | - int $width, |
|
498 | - int $height, |
|
499 | - bool $crop, |
|
500 | - int $maxWidth, |
|
501 | - int $maxHeight, |
|
502 | - string $prefix, |
|
503 | - bool $cacheResult, |
|
504 | - ): ISimpleFile { |
|
505 | - $preview = $maxPreview; |
|
506 | - if (!$preview->valid()) { |
|
507 | - throw new \InvalidArgumentException('Failed to generate preview, failed to load image'); |
|
508 | - } |
|
509 | - |
|
510 | - $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); |
|
511 | - $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); |
|
512 | - try { |
|
513 | - if ($crop) { |
|
514 | - if ($height !== $preview->height() && $width !== $preview->width()) { |
|
515 | - //Resize |
|
516 | - $widthR = $preview->width() / $width; |
|
517 | - $heightR = $preview->height() / $height; |
|
518 | - |
|
519 | - if ($widthR > $heightR) { |
|
520 | - $scaleH = $height; |
|
521 | - $scaleW = $maxWidth / $heightR; |
|
522 | - } else { |
|
523 | - $scaleH = $maxHeight / $widthR; |
|
524 | - $scaleW = $width; |
|
525 | - } |
|
526 | - $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH)); |
|
527 | - } |
|
528 | - $cropX = (int)floor(abs($width - $preview->width()) * 0.5); |
|
529 | - $cropY = (int)floor(abs($height - $preview->height()) * 0.5); |
|
530 | - $preview = $preview->cropCopy($cropX, $cropY, $width, $height); |
|
531 | - } else { |
|
532 | - $preview = $maxPreview->resizeCopy(max($width, $height)); |
|
533 | - } |
|
534 | - } finally { |
|
535 | - self::unguardWithSemaphore($sem); |
|
536 | - } |
|
537 | - |
|
538 | - |
|
539 | - $path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix); |
|
540 | - try { |
|
541 | - if ($cacheResult) { |
|
542 | - return $previewFolder->newFile($path, $preview->data()); |
|
543 | - } else { |
|
544 | - return new InMemoryFile($path, $preview->data()); |
|
545 | - } |
|
546 | - } catch (NotPermittedException $e) { |
|
547 | - throw new NotFoundException(); |
|
548 | - } |
|
549 | - return $file; |
|
550 | - } |
|
551 | - |
|
552 | - /** |
|
553 | - * @param ISimpleFile[] $files Array of FileInfo, as the result of getDirectoryListing() |
|
554 | - * @param int $width |
|
555 | - * @param int $height |
|
556 | - * @param bool $crop |
|
557 | - * @param string $mimeType |
|
558 | - * @param string $prefix |
|
559 | - * @return ISimpleFile |
|
560 | - * |
|
561 | - * @throws NotFoundException |
|
562 | - */ |
|
563 | - private function getCachedPreview($files, $width, $height, $crop, $mimeType, $prefix) { |
|
564 | - $path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix); |
|
565 | - foreach ($files as $file) { |
|
566 | - if ($file->getName() === $path) { |
|
567 | - $this->logger->debug('Found cached preview: {path}', ['path' => $path]); |
|
568 | - return $file; |
|
569 | - } |
|
570 | - } |
|
571 | - throw new NotFoundException(); |
|
572 | - } |
|
573 | - |
|
574 | - /** |
|
575 | - * Get the specific preview folder for this file |
|
576 | - * |
|
577 | - * @param File $file |
|
578 | - * @return ISimpleFolder |
|
579 | - * |
|
580 | - * @throws InvalidPathException |
|
581 | - * @throws NotFoundException |
|
582 | - * @throws NotPermittedException |
|
583 | - */ |
|
584 | - private function getPreviewFolder(File $file) { |
|
585 | - // Obtain file id outside of try catch block to prevent the creation of an existing folder |
|
586 | - $fileId = (string)$file->getId(); |
|
587 | - |
|
588 | - try { |
|
589 | - $folder = $this->appData->getFolder($fileId); |
|
590 | - } catch (NotFoundException $e) { |
|
591 | - $folder = $this->appData->newFolder($fileId); |
|
592 | - } |
|
593 | - |
|
594 | - return $folder; |
|
595 | - } |
|
596 | - |
|
597 | - /** |
|
598 | - * @param string $mimeType |
|
599 | - * @return null|string |
|
600 | - * @throws \InvalidArgumentException |
|
601 | - */ |
|
602 | - private function getExtension($mimeType) { |
|
603 | - switch ($mimeType) { |
|
604 | - case 'image/png': |
|
605 | - return 'png'; |
|
606 | - case 'image/jpeg': |
|
607 | - return 'jpg'; |
|
608 | - case 'image/webp': |
|
609 | - return 'webp'; |
|
610 | - case 'image/gif': |
|
611 | - return 'gif'; |
|
612 | - default: |
|
613 | - throw new \InvalidArgumentException('Not a valid mimetype: "' . $mimeType . '"'); |
|
614 | - } |
|
615 | - } |
|
476 | + if ($height > $maxHeight) { |
|
477 | + $ratio = $height / $maxHeight; |
|
478 | + $height = $maxHeight; |
|
479 | + $width /= $ratio; |
|
480 | + } |
|
481 | + if ($width > $maxWidth) { |
|
482 | + $ratio = $width / $maxWidth; |
|
483 | + $width = $maxWidth; |
|
484 | + $height /= $ratio; |
|
485 | + } |
|
486 | + |
|
487 | + return [(int)round($width), (int)round($height)]; |
|
488 | + } |
|
489 | + |
|
490 | + /** |
|
491 | + * @throws NotFoundException |
|
492 | + * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) |
|
493 | + */ |
|
494 | + private function generatePreview( |
|
495 | + ISimpleFolder $previewFolder, |
|
496 | + IImage $maxPreview, |
|
497 | + int $width, |
|
498 | + int $height, |
|
499 | + bool $crop, |
|
500 | + int $maxWidth, |
|
501 | + int $maxHeight, |
|
502 | + string $prefix, |
|
503 | + bool $cacheResult, |
|
504 | + ): ISimpleFile { |
|
505 | + $preview = $maxPreview; |
|
506 | + if (!$preview->valid()) { |
|
507 | + throw new \InvalidArgumentException('Failed to generate preview, failed to load image'); |
|
508 | + } |
|
509 | + |
|
510 | + $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); |
|
511 | + $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); |
|
512 | + try { |
|
513 | + if ($crop) { |
|
514 | + if ($height !== $preview->height() && $width !== $preview->width()) { |
|
515 | + //Resize |
|
516 | + $widthR = $preview->width() / $width; |
|
517 | + $heightR = $preview->height() / $height; |
|
518 | + |
|
519 | + if ($widthR > $heightR) { |
|
520 | + $scaleH = $height; |
|
521 | + $scaleW = $maxWidth / $heightR; |
|
522 | + } else { |
|
523 | + $scaleH = $maxHeight / $widthR; |
|
524 | + $scaleW = $width; |
|
525 | + } |
|
526 | + $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH)); |
|
527 | + } |
|
528 | + $cropX = (int)floor(abs($width - $preview->width()) * 0.5); |
|
529 | + $cropY = (int)floor(abs($height - $preview->height()) * 0.5); |
|
530 | + $preview = $preview->cropCopy($cropX, $cropY, $width, $height); |
|
531 | + } else { |
|
532 | + $preview = $maxPreview->resizeCopy(max($width, $height)); |
|
533 | + } |
|
534 | + } finally { |
|
535 | + self::unguardWithSemaphore($sem); |
|
536 | + } |
|
537 | + |
|
538 | + |
|
539 | + $path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix); |
|
540 | + try { |
|
541 | + if ($cacheResult) { |
|
542 | + return $previewFolder->newFile($path, $preview->data()); |
|
543 | + } else { |
|
544 | + return new InMemoryFile($path, $preview->data()); |
|
545 | + } |
|
546 | + } catch (NotPermittedException $e) { |
|
547 | + throw new NotFoundException(); |
|
548 | + } |
|
549 | + return $file; |
|
550 | + } |
|
551 | + |
|
552 | + /** |
|
553 | + * @param ISimpleFile[] $files Array of FileInfo, as the result of getDirectoryListing() |
|
554 | + * @param int $width |
|
555 | + * @param int $height |
|
556 | + * @param bool $crop |
|
557 | + * @param string $mimeType |
|
558 | + * @param string $prefix |
|
559 | + * @return ISimpleFile |
|
560 | + * |
|
561 | + * @throws NotFoundException |
|
562 | + */ |
|
563 | + private function getCachedPreview($files, $width, $height, $crop, $mimeType, $prefix) { |
|
564 | + $path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix); |
|
565 | + foreach ($files as $file) { |
|
566 | + if ($file->getName() === $path) { |
|
567 | + $this->logger->debug('Found cached preview: {path}', ['path' => $path]); |
|
568 | + return $file; |
|
569 | + } |
|
570 | + } |
|
571 | + throw new NotFoundException(); |
|
572 | + } |
|
573 | + |
|
574 | + /** |
|
575 | + * Get the specific preview folder for this file |
|
576 | + * |
|
577 | + * @param File $file |
|
578 | + * @return ISimpleFolder |
|
579 | + * |
|
580 | + * @throws InvalidPathException |
|
581 | + * @throws NotFoundException |
|
582 | + * @throws NotPermittedException |
|
583 | + */ |
|
584 | + private function getPreviewFolder(File $file) { |
|
585 | + // Obtain file id outside of try catch block to prevent the creation of an existing folder |
|
586 | + $fileId = (string)$file->getId(); |
|
587 | + |
|
588 | + try { |
|
589 | + $folder = $this->appData->getFolder($fileId); |
|
590 | + } catch (NotFoundException $e) { |
|
591 | + $folder = $this->appData->newFolder($fileId); |
|
592 | + } |
|
593 | + |
|
594 | + return $folder; |
|
595 | + } |
|
596 | + |
|
597 | + /** |
|
598 | + * @param string $mimeType |
|
599 | + * @return null|string |
|
600 | + * @throws \InvalidArgumentException |
|
601 | + */ |
|
602 | + private function getExtension($mimeType) { |
|
603 | + switch ($mimeType) { |
|
604 | + case 'image/png': |
|
605 | + return 'png'; |
|
606 | + case 'image/jpeg': |
|
607 | + return 'jpg'; |
|
608 | + case 'image/webp': |
|
609 | + return 'webp'; |
|
610 | + case 'image/gif': |
|
611 | + return 'gif'; |
|
612 | + default: |
|
613 | + throw new \InvalidArgumentException('Not a valid mimetype: "' . $mimeType . '"'); |
|
614 | + } |
|
615 | + } |
|
616 | 616 | } |
@@ -22,442 +22,442 @@ |
||
22 | 22 | use Psr\Log\LoggerInterface; |
23 | 23 | |
24 | 24 | class GeneratorTest extends \Test\TestCase { |
25 | - /** @var IConfig&\PHPUnit\Framework\MockObject\MockObject */ |
|
26 | - private $config; |
|
27 | - |
|
28 | - /** @var IPreview&\PHPUnit\Framework\MockObject\MockObject */ |
|
29 | - private $previewManager; |
|
30 | - |
|
31 | - /** @var IAppData&\PHPUnit\Framework\MockObject\MockObject */ |
|
32 | - private $appData; |
|
33 | - |
|
34 | - /** @var GeneratorHelper&\PHPUnit\Framework\MockObject\MockObject */ |
|
35 | - private $helper; |
|
36 | - |
|
37 | - /** @var IEventDispatcher&\PHPUnit\Framework\MockObject\MockObject */ |
|
38 | - private $eventDispatcher; |
|
39 | - |
|
40 | - /** @var Generator */ |
|
41 | - private $generator; |
|
42 | - |
|
43 | - private LoggerInterface&\PHPUnit\Framework\MockObject\MockObject $logger; |
|
44 | - |
|
45 | - protected function setUp(): void { |
|
46 | - parent::setUp(); |
|
47 | - |
|
48 | - $this->config = $this->createMock(IConfig::class); |
|
49 | - $this->previewManager = $this->createMock(IPreview::class); |
|
50 | - $this->appData = $this->createMock(IAppData::class); |
|
51 | - $this->helper = $this->createMock(GeneratorHelper::class); |
|
52 | - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); |
|
53 | - $this->logger = $this->createMock(LoggerInterface::class); |
|
54 | - |
|
55 | - $this->generator = new Generator( |
|
56 | - $this->config, |
|
57 | - $this->previewManager, |
|
58 | - $this->appData, |
|
59 | - $this->helper, |
|
60 | - $this->eventDispatcher, |
|
61 | - $this->logger, |
|
62 | - ); |
|
63 | - } |
|
64 | - |
|
65 | - public function testGetCachedPreview(): void { |
|
66 | - $file = $this->createMock(File::class); |
|
67 | - $file->method('isReadable') |
|
68 | - ->willReturn(true); |
|
69 | - $file->method('getMimeType') |
|
70 | - ->willReturn('myMimeType'); |
|
71 | - $file->method('getId') |
|
72 | - ->willReturn(42); |
|
73 | - |
|
74 | - $this->previewManager->method('isMimeSupported') |
|
75 | - ->with($this->equalTo('myMimeType')) |
|
76 | - ->willReturn(true); |
|
77 | - |
|
78 | - $previewFolder = $this->createMock(ISimpleFolder::class); |
|
79 | - $this->appData->method('getFolder') |
|
80 | - ->with($this->equalTo(42)) |
|
81 | - ->willReturn($previewFolder); |
|
82 | - |
|
83 | - $maxPreview = $this->createMock(ISimpleFile::class); |
|
84 | - $maxPreview->method('getName') |
|
85 | - ->willReturn('1000-1000-max.png'); |
|
86 | - $maxPreview->method('getSize')->willReturn(1000); |
|
87 | - $maxPreview->method('getMimeType') |
|
88 | - ->willReturn('image/png'); |
|
89 | - |
|
90 | - $previewFile = $this->createMock(ISimpleFile::class); |
|
91 | - $previewFile->method('getSize')->willReturn(1000); |
|
92 | - $previewFile->method('getName')->willReturn('256-256.png'); |
|
93 | - |
|
94 | - $previewFolder->method('getDirectoryListing') |
|
95 | - ->willReturn([$maxPreview, $previewFile]); |
|
96 | - |
|
97 | - $this->eventDispatcher->expects($this->once()) |
|
98 | - ->method('dispatchTyped') |
|
99 | - ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null)); |
|
100 | - |
|
101 | - $result = $this->generator->getPreview($file, 100, 100); |
|
102 | - $this->assertSame($previewFile, $result); |
|
103 | - } |
|
104 | - |
|
105 | - public function testGetNewPreview(): void { |
|
106 | - $file = $this->createMock(File::class); |
|
107 | - $file->method('isReadable') |
|
108 | - ->willReturn(true); |
|
109 | - $file->method('getMimeType') |
|
110 | - ->willReturn('myMimeType'); |
|
111 | - $file->method('getId') |
|
112 | - ->willReturn(42); |
|
113 | - |
|
114 | - $this->previewManager->method('isMimeSupported') |
|
115 | - ->with($this->equalTo('myMimeType')) |
|
116 | - ->willReturn(true); |
|
117 | - |
|
118 | - $previewFolder = $this->createMock(ISimpleFolder::class); |
|
119 | - $this->appData->method('getFolder') |
|
120 | - ->with($this->equalTo(42)) |
|
121 | - ->willThrowException(new NotFoundException()); |
|
122 | - |
|
123 | - $this->appData->method('newFolder') |
|
124 | - ->with($this->equalTo(42)) |
|
125 | - ->willReturn($previewFolder); |
|
126 | - |
|
127 | - $this->config->method('getSystemValue') |
|
128 | - ->willReturnCallback(function ($key, $default) { |
|
129 | - return $default; |
|
130 | - }); |
|
131 | - |
|
132 | - $this->config->method('getSystemValueInt') |
|
133 | - ->willReturnCallback(function ($key, $default) { |
|
134 | - return $default; |
|
135 | - }); |
|
136 | - |
|
137 | - $invalidProvider = $this->createMock(IProviderV2::class); |
|
138 | - $invalidProvider->method('isAvailable') |
|
139 | - ->willReturn(true); |
|
140 | - $unavailableProvider = $this->createMock(IProviderV2::class); |
|
141 | - $unavailableProvider->method('isAvailable') |
|
142 | - ->willReturn(false); |
|
143 | - $validProvider = $this->createMock(IProviderV2::class); |
|
144 | - $validProvider->method('isAvailable') |
|
145 | - ->with($file) |
|
146 | - ->willReturn(true); |
|
147 | - |
|
148 | - $this->previewManager->method('getProviders') |
|
149 | - ->willReturn([ |
|
150 | - '/image\/png/' => ['wrongProvider'], |
|
151 | - '/myMimeType/' => ['brokenProvider', 'invalidProvider', 'unavailableProvider', 'validProvider'], |
|
152 | - ]); |
|
153 | - |
|
154 | - $this->helper->method('getProvider') |
|
155 | - ->willReturnCallback(function ($provider) use ($invalidProvider, $validProvider, $unavailableProvider) { |
|
156 | - if ($provider === 'wrongProvider') { |
|
157 | - $this->fail('Wrongprovider should not be constructed!'); |
|
158 | - } elseif ($provider === 'brokenProvider') { |
|
159 | - return false; |
|
160 | - } elseif ($provider === 'invalidProvider') { |
|
161 | - return $invalidProvider; |
|
162 | - } elseif ($provider === 'validProvider') { |
|
163 | - return $validProvider; |
|
164 | - } elseif ($provider === 'unavailableProvider') { |
|
165 | - return $unavailableProvider; |
|
166 | - } |
|
167 | - $this->fail('Unexpected provider requested'); |
|
168 | - }); |
|
169 | - |
|
170 | - $image = $this->createMock(IImage::class); |
|
171 | - $image->method('width')->willReturn(2048); |
|
172 | - $image->method('height')->willReturn(2048); |
|
173 | - $image->method('valid')->willReturn(true); |
|
174 | - $image->method('dataMimeType')->willReturn('image/png'); |
|
175 | - |
|
176 | - $this->helper->method('getThumbnail') |
|
177 | - ->willReturnCallback(function ($provider, $file, $x, $y) use ($invalidProvider, $validProvider, $image) { |
|
178 | - if ($provider === $validProvider) { |
|
179 | - return $image; |
|
180 | - } else { |
|
181 | - return false; |
|
182 | - } |
|
183 | - }); |
|
184 | - |
|
185 | - $image->method('data') |
|
186 | - ->willReturn('my data'); |
|
187 | - |
|
188 | - $maxPreview = $this->createMock(ISimpleFile::class); |
|
189 | - $maxPreview->method('getName')->willReturn('2048-2048-max.png'); |
|
190 | - $maxPreview->method('getMimeType')->willReturn('image/png'); |
|
191 | - $maxPreview->method('getSize')->willReturn(1000); |
|
192 | - |
|
193 | - $previewFile = $this->createMock(ISimpleFile::class); |
|
194 | - $previewFile->method('getSize')->willReturn(1000); |
|
195 | - |
|
196 | - $previewFolder->method('getDirectoryListing') |
|
197 | - ->willReturn([]); |
|
198 | - $previewFolder->method('newFile') |
|
199 | - ->willReturnMap([ |
|
200 | - ['2048-2048-max.png', 'my data', $maxPreview], |
|
201 | - ['256-256.png', 'my resized data', $previewFile], |
|
202 | - ]); |
|
203 | - |
|
204 | - $previewFolder->method('getFile') |
|
205 | - ->with($this->equalTo('256-256.png')) |
|
206 | - ->willThrowException(new NotFoundException()); |
|
207 | - |
|
208 | - $image = $this->getMockImage(2048, 2048, 'my resized data'); |
|
209 | - $this->helper->method('getImage') |
|
210 | - ->with($this->equalTo($maxPreview)) |
|
211 | - ->willReturn($image); |
|
212 | - |
|
213 | - $this->eventDispatcher->expects($this->once()) |
|
214 | - ->method('dispatchTyped') |
|
215 | - ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null)); |
|
216 | - |
|
217 | - $result = $this->generator->getPreview($file, 100, 100); |
|
218 | - $this->assertSame($previewFile, $result); |
|
219 | - } |
|
220 | - |
|
221 | - public function testInvalidMimeType(): void { |
|
222 | - $this->expectException(NotFoundException::class); |
|
223 | - |
|
224 | - $file = $this->createMock(File::class); |
|
225 | - $file->method('isReadable') |
|
226 | - ->willReturn(true); |
|
227 | - $file->method('getId') |
|
228 | - ->willReturn(42); |
|
229 | - |
|
230 | - $this->previewManager->method('isMimeSupported') |
|
231 | - ->with('invalidType') |
|
232 | - ->willReturn(false); |
|
233 | - |
|
234 | - $previewFolder = $this->createMock(ISimpleFolder::class); |
|
235 | - $this->appData->method('getFolder') |
|
236 | - ->with($this->equalTo(42)) |
|
237 | - ->willReturn($previewFolder); |
|
238 | - |
|
239 | - $maxPreview = $this->createMock(ISimpleFile::class); |
|
240 | - $maxPreview->method('getName') |
|
241 | - ->willReturn('2048-2048-max.png'); |
|
242 | - $maxPreview->method('getMimeType') |
|
243 | - ->willReturn('image/png'); |
|
244 | - |
|
245 | - $previewFolder->method('getDirectoryListing') |
|
246 | - ->willReturn([$maxPreview]); |
|
247 | - |
|
248 | - $previewFolder->method('getFile') |
|
249 | - ->with($this->equalTo('1024-512-crop.png')) |
|
250 | - ->willThrowException(new NotFoundException()); |
|
251 | - |
|
252 | - $this->eventDispatcher->expects($this->once()) |
|
253 | - ->method('dispatchTyped') |
|
254 | - ->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType')); |
|
255 | - |
|
256 | - $this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'); |
|
257 | - } |
|
258 | - |
|
259 | - public function testReturnCachedPreviewsWithoutCheckingSupportedMimetype(): void { |
|
260 | - $file = $this->createMock(File::class); |
|
261 | - $file->method('isReadable') |
|
262 | - ->willReturn(true); |
|
263 | - $file->method('getId') |
|
264 | - ->willReturn(42); |
|
265 | - |
|
266 | - |
|
267 | - $previewFolder = $this->createMock(ISimpleFolder::class); |
|
268 | - $this->appData->method('getFolder') |
|
269 | - ->with($this->equalTo(42)) |
|
270 | - ->willReturn($previewFolder); |
|
271 | - |
|
272 | - $maxPreview = $this->createMock(ISimpleFile::class); |
|
273 | - $maxPreview->method('getName') |
|
274 | - ->willReturn('2048-2048-max.png'); |
|
275 | - $maxPreview->method('getSize')->willReturn(1000); |
|
276 | - $maxPreview->method('getMimeType') |
|
277 | - ->willReturn('image/png'); |
|
278 | - |
|
279 | - $preview = $this->createMock(ISimpleFile::class); |
|
280 | - $preview->method('getSize')->willReturn(1000); |
|
281 | - $preview->method('getName')->willReturn('1024-512-crop.png'); |
|
282 | - |
|
283 | - $previewFolder->method('getDirectoryListing') |
|
284 | - ->willReturn([$maxPreview, $preview]); |
|
285 | - |
|
286 | - $this->previewManager->expects($this->never()) |
|
287 | - ->method('isMimeSupported'); |
|
288 | - |
|
289 | - $this->eventDispatcher->expects($this->once()) |
|
290 | - ->method('dispatchTyped') |
|
291 | - ->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType')); |
|
292 | - |
|
293 | - $result = $this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'); |
|
294 | - $this->assertSame($preview, $result); |
|
295 | - } |
|
296 | - |
|
297 | - public function testNoProvider(): void { |
|
298 | - $file = $this->createMock(File::class); |
|
299 | - $file->method('isReadable') |
|
300 | - ->willReturn(true); |
|
301 | - $file->method('getMimeType') |
|
302 | - ->willReturn('myMimeType'); |
|
303 | - $file->method('getId') |
|
304 | - ->willReturn(42); |
|
305 | - |
|
306 | - $previewFolder = $this->createMock(ISimpleFolder::class); |
|
307 | - $this->appData->method('getFolder') |
|
308 | - ->with($this->equalTo(42)) |
|
309 | - ->willReturn($previewFolder); |
|
310 | - |
|
311 | - $previewFolder->method('getDirectoryListing') |
|
312 | - ->willReturn([]); |
|
313 | - |
|
314 | - $this->previewManager->method('getProviders') |
|
315 | - ->willReturn([]); |
|
316 | - |
|
317 | - $this->eventDispatcher->expects($this->once()) |
|
318 | - ->method('dispatchTyped') |
|
319 | - ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null)); |
|
320 | - |
|
321 | - $this->expectException(NotFoundException::class); |
|
322 | - $this->generator->getPreview($file, 100, 100); |
|
323 | - } |
|
324 | - |
|
325 | - private function getMockImage($width, $height, $data = null) { |
|
326 | - $image = $this->createMock(IImage::class); |
|
327 | - $image->method('height')->willReturn($width); |
|
328 | - $image->method('width')->willReturn($height); |
|
329 | - $image->method('valid')->willReturn(true); |
|
330 | - $image->method('dataMimeType')->willReturn('image/png'); |
|
331 | - $image->method('data')->willReturn($data); |
|
332 | - |
|
333 | - $image->method('resizeCopy')->willReturnCallback(function ($size) use ($data) { |
|
334 | - return $this->getMockImage($size, $size, $data); |
|
335 | - }); |
|
336 | - $image->method('preciseResizeCopy')->willReturnCallback(function ($width, $height) use ($data) { |
|
337 | - return $this->getMockImage($width, $height, $data); |
|
338 | - }); |
|
339 | - $image->method('cropCopy')->willReturnCallback(function ($x, $y, $width, $height) use ($data) { |
|
340 | - return $this->getMockImage($width, $height, $data); |
|
341 | - }); |
|
342 | - |
|
343 | - return $image; |
|
344 | - } |
|
345 | - |
|
346 | - public function dataSize() { |
|
347 | - return [ |
|
348 | - [1024, 2048, 512, 512, false, IPreview::MODE_FILL, 256, 512], |
|
349 | - [1024, 2048, 512, 512, false, IPreview::MODE_COVER, 512, 1024], |
|
350 | - [1024, 2048, 512, 512, true, IPreview::MODE_FILL, 1024, 1024], |
|
351 | - [1024, 2048, 512, 512, true, IPreview::MODE_COVER, 1024, 1024], |
|
352 | - |
|
353 | - [1024, 2048, -1, 512, false, IPreview::MODE_COVER, 256, 512], |
|
354 | - [1024, 2048, 512, -1, false, IPreview::MODE_FILL, 512, 1024], |
|
355 | - |
|
356 | - [1024, 2048, 250, 1100, true, IPreview::MODE_COVER, 256, 1126], |
|
357 | - [1024, 1100, 250, 1100, true, IPreview::MODE_COVER, 250, 1100], |
|
358 | - |
|
359 | - [1024, 2048, 4096, 2048, false, IPreview::MODE_FILL, 1024, 2048], |
|
360 | - [1024, 2048, 4096, 2048, false, IPreview::MODE_COVER, 1024, 2048], |
|
361 | - |
|
362 | - |
|
363 | - [2048, 1024, 512, 512, false, IPreview::MODE_FILL, 512, 256], |
|
364 | - [2048, 1024, 512, 512, false, IPreview::MODE_COVER, 1024, 512], |
|
365 | - [2048, 1024, 512, 512, true, IPreview::MODE_FILL, 1024, 1024], |
|
366 | - [2048, 1024, 512, 512, true, IPreview::MODE_COVER, 1024, 1024], |
|
367 | - |
|
368 | - [2048, 1024, -1, 512, false, IPreview::MODE_FILL, 1024, 512], |
|
369 | - [2048, 1024, 512, -1, false, IPreview::MODE_COVER, 512, 256], |
|
370 | - |
|
371 | - [2048, 1024, 4096, 1024, true, IPreview::MODE_FILL, 2048, 512], |
|
372 | - [2048, 1024, 4096, 1024, true, IPreview::MODE_COVER, 2048, 512], |
|
373 | - |
|
374 | - //Test minimum size |
|
375 | - [2048, 1024, 32, 32, false, IPreview::MODE_FILL, 64, 32], |
|
376 | - [2048, 1024, 32, 32, false, IPreview::MODE_COVER, 64, 32], |
|
377 | - [2048, 1024, 32, 32, true, IPreview::MODE_FILL, 64, 64], |
|
378 | - [2048, 1024, 32, 32, true, IPreview::MODE_COVER, 64, 64], |
|
379 | - ]; |
|
380 | - } |
|
381 | - |
|
382 | - /** |
|
383 | - * @dataProvider dataSize |
|
384 | - * |
|
385 | - * @param int $maxX |
|
386 | - * @param int $maxY |
|
387 | - * @param int $reqX |
|
388 | - * @param int $reqY |
|
389 | - * @param bool $crop |
|
390 | - * @param string $mode |
|
391 | - * @param int $expectedX |
|
392 | - * @param int $expectedY |
|
393 | - */ |
|
394 | - public function testCorrectSize($maxX, $maxY, $reqX, $reqY, $crop, $mode, $expectedX, $expectedY): void { |
|
395 | - $file = $this->createMock(File::class); |
|
396 | - $file->method('isReadable') |
|
397 | - ->willReturn(true); |
|
398 | - $file->method('getMimeType') |
|
399 | - ->willReturn('myMimeType'); |
|
400 | - $file->method('getId') |
|
401 | - ->willReturn(42); |
|
402 | - |
|
403 | - $this->previewManager->method('isMimeSupported') |
|
404 | - ->with($this->equalTo('myMimeType')) |
|
405 | - ->willReturn(true); |
|
406 | - |
|
407 | - $previewFolder = $this->createMock(ISimpleFolder::class); |
|
408 | - $this->appData->method('getFolder') |
|
409 | - ->with($this->equalTo(42)) |
|
410 | - ->willReturn($previewFolder); |
|
411 | - |
|
412 | - $maxPreview = $this->createMock(ISimpleFile::class); |
|
413 | - $maxPreview->method('getName') |
|
414 | - ->willReturn($maxX . '-' . $maxY . '-max.png'); |
|
415 | - $maxPreview->method('getMimeType') |
|
416 | - ->willReturn('image/png'); |
|
417 | - $maxPreview->method('getSize')->willReturn(1000); |
|
418 | - |
|
419 | - $previewFolder->method('getDirectoryListing') |
|
420 | - ->willReturn([$maxPreview]); |
|
421 | - |
|
422 | - $filename = $expectedX . '-' . $expectedY; |
|
423 | - if ($crop) { |
|
424 | - $filename .= '-crop'; |
|
425 | - } |
|
426 | - $filename .= '.png'; |
|
427 | - $previewFolder->method('getFile') |
|
428 | - ->with($this->equalTo($filename)) |
|
429 | - ->willThrowException(new NotFoundException()); |
|
430 | - |
|
431 | - $image = $this->getMockImage($maxX, $maxY); |
|
432 | - $this->helper->method('getImage') |
|
433 | - ->with($this->equalTo($maxPreview)) |
|
434 | - ->willReturn($image); |
|
435 | - |
|
436 | - $preview = $this->createMock(ISimpleFile::class); |
|
437 | - $preview->method('getSize')->willReturn(1000); |
|
438 | - $previewFolder->method('newFile') |
|
439 | - ->with($this->equalTo($filename)) |
|
440 | - ->willReturn($preview); |
|
441 | - |
|
442 | - $this->eventDispatcher->expects($this->once()) |
|
443 | - ->method('dispatchTyped') |
|
444 | - ->with(new BeforePreviewFetchedEvent($file, $reqX, $reqY, $crop, $mode, null)); |
|
445 | - |
|
446 | - $result = $this->generator->getPreview($file, $reqX, $reqY, $crop, $mode); |
|
447 | - if ($expectedX === $maxX && $expectedY === $maxY) { |
|
448 | - $this->assertSame($maxPreview, $result); |
|
449 | - } else { |
|
450 | - $this->assertSame($preview, $result); |
|
451 | - } |
|
452 | - } |
|
453 | - |
|
454 | - public function testUnreadbleFile(): void { |
|
455 | - $file = $this->createMock(File::class); |
|
456 | - $file->method('isReadable') |
|
457 | - ->willReturn(false); |
|
458 | - |
|
459 | - $this->expectException(NotFoundException::class); |
|
460 | - |
|
461 | - $this->generator->getPreview($file, 100, 100, false); |
|
462 | - } |
|
25 | + /** @var IConfig&\PHPUnit\Framework\MockObject\MockObject */ |
|
26 | + private $config; |
|
27 | + |
|
28 | + /** @var IPreview&\PHPUnit\Framework\MockObject\MockObject */ |
|
29 | + private $previewManager; |
|
30 | + |
|
31 | + /** @var IAppData&\PHPUnit\Framework\MockObject\MockObject */ |
|
32 | + private $appData; |
|
33 | + |
|
34 | + /** @var GeneratorHelper&\PHPUnit\Framework\MockObject\MockObject */ |
|
35 | + private $helper; |
|
36 | + |
|
37 | + /** @var IEventDispatcher&\PHPUnit\Framework\MockObject\MockObject */ |
|
38 | + private $eventDispatcher; |
|
39 | + |
|
40 | + /** @var Generator */ |
|
41 | + private $generator; |
|
42 | + |
|
43 | + private LoggerInterface&\PHPUnit\Framework\MockObject\MockObject $logger; |
|
44 | + |
|
45 | + protected function setUp(): void { |
|
46 | + parent::setUp(); |
|
47 | + |
|
48 | + $this->config = $this->createMock(IConfig::class); |
|
49 | + $this->previewManager = $this->createMock(IPreview::class); |
|
50 | + $this->appData = $this->createMock(IAppData::class); |
|
51 | + $this->helper = $this->createMock(GeneratorHelper::class); |
|
52 | + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); |
|
53 | + $this->logger = $this->createMock(LoggerInterface::class); |
|
54 | + |
|
55 | + $this->generator = new Generator( |
|
56 | + $this->config, |
|
57 | + $this->previewManager, |
|
58 | + $this->appData, |
|
59 | + $this->helper, |
|
60 | + $this->eventDispatcher, |
|
61 | + $this->logger, |
|
62 | + ); |
|
63 | + } |
|
64 | + |
|
65 | + public function testGetCachedPreview(): void { |
|
66 | + $file = $this->createMock(File::class); |
|
67 | + $file->method('isReadable') |
|
68 | + ->willReturn(true); |
|
69 | + $file->method('getMimeType') |
|
70 | + ->willReturn('myMimeType'); |
|
71 | + $file->method('getId') |
|
72 | + ->willReturn(42); |
|
73 | + |
|
74 | + $this->previewManager->method('isMimeSupported') |
|
75 | + ->with($this->equalTo('myMimeType')) |
|
76 | + ->willReturn(true); |
|
77 | + |
|
78 | + $previewFolder = $this->createMock(ISimpleFolder::class); |
|
79 | + $this->appData->method('getFolder') |
|
80 | + ->with($this->equalTo(42)) |
|
81 | + ->willReturn($previewFolder); |
|
82 | + |
|
83 | + $maxPreview = $this->createMock(ISimpleFile::class); |
|
84 | + $maxPreview->method('getName') |
|
85 | + ->willReturn('1000-1000-max.png'); |
|
86 | + $maxPreview->method('getSize')->willReturn(1000); |
|
87 | + $maxPreview->method('getMimeType') |
|
88 | + ->willReturn('image/png'); |
|
89 | + |
|
90 | + $previewFile = $this->createMock(ISimpleFile::class); |
|
91 | + $previewFile->method('getSize')->willReturn(1000); |
|
92 | + $previewFile->method('getName')->willReturn('256-256.png'); |
|
93 | + |
|
94 | + $previewFolder->method('getDirectoryListing') |
|
95 | + ->willReturn([$maxPreview, $previewFile]); |
|
96 | + |
|
97 | + $this->eventDispatcher->expects($this->once()) |
|
98 | + ->method('dispatchTyped') |
|
99 | + ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null)); |
|
100 | + |
|
101 | + $result = $this->generator->getPreview($file, 100, 100); |
|
102 | + $this->assertSame($previewFile, $result); |
|
103 | + } |
|
104 | + |
|
105 | + public function testGetNewPreview(): void { |
|
106 | + $file = $this->createMock(File::class); |
|
107 | + $file->method('isReadable') |
|
108 | + ->willReturn(true); |
|
109 | + $file->method('getMimeType') |
|
110 | + ->willReturn('myMimeType'); |
|
111 | + $file->method('getId') |
|
112 | + ->willReturn(42); |
|
113 | + |
|
114 | + $this->previewManager->method('isMimeSupported') |
|
115 | + ->with($this->equalTo('myMimeType')) |
|
116 | + ->willReturn(true); |
|
117 | + |
|
118 | + $previewFolder = $this->createMock(ISimpleFolder::class); |
|
119 | + $this->appData->method('getFolder') |
|
120 | + ->with($this->equalTo(42)) |
|
121 | + ->willThrowException(new NotFoundException()); |
|
122 | + |
|
123 | + $this->appData->method('newFolder') |
|
124 | + ->with($this->equalTo(42)) |
|
125 | + ->willReturn($previewFolder); |
|
126 | + |
|
127 | + $this->config->method('getSystemValue') |
|
128 | + ->willReturnCallback(function ($key, $default) { |
|
129 | + return $default; |
|
130 | + }); |
|
131 | + |
|
132 | + $this->config->method('getSystemValueInt') |
|
133 | + ->willReturnCallback(function ($key, $default) { |
|
134 | + return $default; |
|
135 | + }); |
|
136 | + |
|
137 | + $invalidProvider = $this->createMock(IProviderV2::class); |
|
138 | + $invalidProvider->method('isAvailable') |
|
139 | + ->willReturn(true); |
|
140 | + $unavailableProvider = $this->createMock(IProviderV2::class); |
|
141 | + $unavailableProvider->method('isAvailable') |
|
142 | + ->willReturn(false); |
|
143 | + $validProvider = $this->createMock(IProviderV2::class); |
|
144 | + $validProvider->method('isAvailable') |
|
145 | + ->with($file) |
|
146 | + ->willReturn(true); |
|
147 | + |
|
148 | + $this->previewManager->method('getProviders') |
|
149 | + ->willReturn([ |
|
150 | + '/image\/png/' => ['wrongProvider'], |
|
151 | + '/myMimeType/' => ['brokenProvider', 'invalidProvider', 'unavailableProvider', 'validProvider'], |
|
152 | + ]); |
|
153 | + |
|
154 | + $this->helper->method('getProvider') |
|
155 | + ->willReturnCallback(function ($provider) use ($invalidProvider, $validProvider, $unavailableProvider) { |
|
156 | + if ($provider === 'wrongProvider') { |
|
157 | + $this->fail('Wrongprovider should not be constructed!'); |
|
158 | + } elseif ($provider === 'brokenProvider') { |
|
159 | + return false; |
|
160 | + } elseif ($provider === 'invalidProvider') { |
|
161 | + return $invalidProvider; |
|
162 | + } elseif ($provider === 'validProvider') { |
|
163 | + return $validProvider; |
|
164 | + } elseif ($provider === 'unavailableProvider') { |
|
165 | + return $unavailableProvider; |
|
166 | + } |
|
167 | + $this->fail('Unexpected provider requested'); |
|
168 | + }); |
|
169 | + |
|
170 | + $image = $this->createMock(IImage::class); |
|
171 | + $image->method('width')->willReturn(2048); |
|
172 | + $image->method('height')->willReturn(2048); |
|
173 | + $image->method('valid')->willReturn(true); |
|
174 | + $image->method('dataMimeType')->willReturn('image/png'); |
|
175 | + |
|
176 | + $this->helper->method('getThumbnail') |
|
177 | + ->willReturnCallback(function ($provider, $file, $x, $y) use ($invalidProvider, $validProvider, $image) { |
|
178 | + if ($provider === $validProvider) { |
|
179 | + return $image; |
|
180 | + } else { |
|
181 | + return false; |
|
182 | + } |
|
183 | + }); |
|
184 | + |
|
185 | + $image->method('data') |
|
186 | + ->willReturn('my data'); |
|
187 | + |
|
188 | + $maxPreview = $this->createMock(ISimpleFile::class); |
|
189 | + $maxPreview->method('getName')->willReturn('2048-2048-max.png'); |
|
190 | + $maxPreview->method('getMimeType')->willReturn('image/png'); |
|
191 | + $maxPreview->method('getSize')->willReturn(1000); |
|
192 | + |
|
193 | + $previewFile = $this->createMock(ISimpleFile::class); |
|
194 | + $previewFile->method('getSize')->willReturn(1000); |
|
195 | + |
|
196 | + $previewFolder->method('getDirectoryListing') |
|
197 | + ->willReturn([]); |
|
198 | + $previewFolder->method('newFile') |
|
199 | + ->willReturnMap([ |
|
200 | + ['2048-2048-max.png', 'my data', $maxPreview], |
|
201 | + ['256-256.png', 'my resized data', $previewFile], |
|
202 | + ]); |
|
203 | + |
|
204 | + $previewFolder->method('getFile') |
|
205 | + ->with($this->equalTo('256-256.png')) |
|
206 | + ->willThrowException(new NotFoundException()); |
|
207 | + |
|
208 | + $image = $this->getMockImage(2048, 2048, 'my resized data'); |
|
209 | + $this->helper->method('getImage') |
|
210 | + ->with($this->equalTo($maxPreview)) |
|
211 | + ->willReturn($image); |
|
212 | + |
|
213 | + $this->eventDispatcher->expects($this->once()) |
|
214 | + ->method('dispatchTyped') |
|
215 | + ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null)); |
|
216 | + |
|
217 | + $result = $this->generator->getPreview($file, 100, 100); |
|
218 | + $this->assertSame($previewFile, $result); |
|
219 | + } |
|
220 | + |
|
221 | + public function testInvalidMimeType(): void { |
|
222 | + $this->expectException(NotFoundException::class); |
|
223 | + |
|
224 | + $file = $this->createMock(File::class); |
|
225 | + $file->method('isReadable') |
|
226 | + ->willReturn(true); |
|
227 | + $file->method('getId') |
|
228 | + ->willReturn(42); |
|
229 | + |
|
230 | + $this->previewManager->method('isMimeSupported') |
|
231 | + ->with('invalidType') |
|
232 | + ->willReturn(false); |
|
233 | + |
|
234 | + $previewFolder = $this->createMock(ISimpleFolder::class); |
|
235 | + $this->appData->method('getFolder') |
|
236 | + ->with($this->equalTo(42)) |
|
237 | + ->willReturn($previewFolder); |
|
238 | + |
|
239 | + $maxPreview = $this->createMock(ISimpleFile::class); |
|
240 | + $maxPreview->method('getName') |
|
241 | + ->willReturn('2048-2048-max.png'); |
|
242 | + $maxPreview->method('getMimeType') |
|
243 | + ->willReturn('image/png'); |
|
244 | + |
|
245 | + $previewFolder->method('getDirectoryListing') |
|
246 | + ->willReturn([$maxPreview]); |
|
247 | + |
|
248 | + $previewFolder->method('getFile') |
|
249 | + ->with($this->equalTo('1024-512-crop.png')) |
|
250 | + ->willThrowException(new NotFoundException()); |
|
251 | + |
|
252 | + $this->eventDispatcher->expects($this->once()) |
|
253 | + ->method('dispatchTyped') |
|
254 | + ->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType')); |
|
255 | + |
|
256 | + $this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'); |
|
257 | + } |
|
258 | + |
|
259 | + public function testReturnCachedPreviewsWithoutCheckingSupportedMimetype(): void { |
|
260 | + $file = $this->createMock(File::class); |
|
261 | + $file->method('isReadable') |
|
262 | + ->willReturn(true); |
|
263 | + $file->method('getId') |
|
264 | + ->willReturn(42); |
|
265 | + |
|
266 | + |
|
267 | + $previewFolder = $this->createMock(ISimpleFolder::class); |
|
268 | + $this->appData->method('getFolder') |
|
269 | + ->with($this->equalTo(42)) |
|
270 | + ->willReturn($previewFolder); |
|
271 | + |
|
272 | + $maxPreview = $this->createMock(ISimpleFile::class); |
|
273 | + $maxPreview->method('getName') |
|
274 | + ->willReturn('2048-2048-max.png'); |
|
275 | + $maxPreview->method('getSize')->willReturn(1000); |
|
276 | + $maxPreview->method('getMimeType') |
|
277 | + ->willReturn('image/png'); |
|
278 | + |
|
279 | + $preview = $this->createMock(ISimpleFile::class); |
|
280 | + $preview->method('getSize')->willReturn(1000); |
|
281 | + $preview->method('getName')->willReturn('1024-512-crop.png'); |
|
282 | + |
|
283 | + $previewFolder->method('getDirectoryListing') |
|
284 | + ->willReturn([$maxPreview, $preview]); |
|
285 | + |
|
286 | + $this->previewManager->expects($this->never()) |
|
287 | + ->method('isMimeSupported'); |
|
288 | + |
|
289 | + $this->eventDispatcher->expects($this->once()) |
|
290 | + ->method('dispatchTyped') |
|
291 | + ->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType')); |
|
292 | + |
|
293 | + $result = $this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'); |
|
294 | + $this->assertSame($preview, $result); |
|
295 | + } |
|
296 | + |
|
297 | + public function testNoProvider(): void { |
|
298 | + $file = $this->createMock(File::class); |
|
299 | + $file->method('isReadable') |
|
300 | + ->willReturn(true); |
|
301 | + $file->method('getMimeType') |
|
302 | + ->willReturn('myMimeType'); |
|
303 | + $file->method('getId') |
|
304 | + ->willReturn(42); |
|
305 | + |
|
306 | + $previewFolder = $this->createMock(ISimpleFolder::class); |
|
307 | + $this->appData->method('getFolder') |
|
308 | + ->with($this->equalTo(42)) |
|
309 | + ->willReturn($previewFolder); |
|
310 | + |
|
311 | + $previewFolder->method('getDirectoryListing') |
|
312 | + ->willReturn([]); |
|
313 | + |
|
314 | + $this->previewManager->method('getProviders') |
|
315 | + ->willReturn([]); |
|
316 | + |
|
317 | + $this->eventDispatcher->expects($this->once()) |
|
318 | + ->method('dispatchTyped') |
|
319 | + ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null)); |
|
320 | + |
|
321 | + $this->expectException(NotFoundException::class); |
|
322 | + $this->generator->getPreview($file, 100, 100); |
|
323 | + } |
|
324 | + |
|
325 | + private function getMockImage($width, $height, $data = null) { |
|
326 | + $image = $this->createMock(IImage::class); |
|
327 | + $image->method('height')->willReturn($width); |
|
328 | + $image->method('width')->willReturn($height); |
|
329 | + $image->method('valid')->willReturn(true); |
|
330 | + $image->method('dataMimeType')->willReturn('image/png'); |
|
331 | + $image->method('data')->willReturn($data); |
|
332 | + |
|
333 | + $image->method('resizeCopy')->willReturnCallback(function ($size) use ($data) { |
|
334 | + return $this->getMockImage($size, $size, $data); |
|
335 | + }); |
|
336 | + $image->method('preciseResizeCopy')->willReturnCallback(function ($width, $height) use ($data) { |
|
337 | + return $this->getMockImage($width, $height, $data); |
|
338 | + }); |
|
339 | + $image->method('cropCopy')->willReturnCallback(function ($x, $y, $width, $height) use ($data) { |
|
340 | + return $this->getMockImage($width, $height, $data); |
|
341 | + }); |
|
342 | + |
|
343 | + return $image; |
|
344 | + } |
|
345 | + |
|
346 | + public function dataSize() { |
|
347 | + return [ |
|
348 | + [1024, 2048, 512, 512, false, IPreview::MODE_FILL, 256, 512], |
|
349 | + [1024, 2048, 512, 512, false, IPreview::MODE_COVER, 512, 1024], |
|
350 | + [1024, 2048, 512, 512, true, IPreview::MODE_FILL, 1024, 1024], |
|
351 | + [1024, 2048, 512, 512, true, IPreview::MODE_COVER, 1024, 1024], |
|
352 | + |
|
353 | + [1024, 2048, -1, 512, false, IPreview::MODE_COVER, 256, 512], |
|
354 | + [1024, 2048, 512, -1, false, IPreview::MODE_FILL, 512, 1024], |
|
355 | + |
|
356 | + [1024, 2048, 250, 1100, true, IPreview::MODE_COVER, 256, 1126], |
|
357 | + [1024, 1100, 250, 1100, true, IPreview::MODE_COVER, 250, 1100], |
|
358 | + |
|
359 | + [1024, 2048, 4096, 2048, false, IPreview::MODE_FILL, 1024, 2048], |
|
360 | + [1024, 2048, 4096, 2048, false, IPreview::MODE_COVER, 1024, 2048], |
|
361 | + |
|
362 | + |
|
363 | + [2048, 1024, 512, 512, false, IPreview::MODE_FILL, 512, 256], |
|
364 | + [2048, 1024, 512, 512, false, IPreview::MODE_COVER, 1024, 512], |
|
365 | + [2048, 1024, 512, 512, true, IPreview::MODE_FILL, 1024, 1024], |
|
366 | + [2048, 1024, 512, 512, true, IPreview::MODE_COVER, 1024, 1024], |
|
367 | + |
|
368 | + [2048, 1024, -1, 512, false, IPreview::MODE_FILL, 1024, 512], |
|
369 | + [2048, 1024, 512, -1, false, IPreview::MODE_COVER, 512, 256], |
|
370 | + |
|
371 | + [2048, 1024, 4096, 1024, true, IPreview::MODE_FILL, 2048, 512], |
|
372 | + [2048, 1024, 4096, 1024, true, IPreview::MODE_COVER, 2048, 512], |
|
373 | + |
|
374 | + //Test minimum size |
|
375 | + [2048, 1024, 32, 32, false, IPreview::MODE_FILL, 64, 32], |
|
376 | + [2048, 1024, 32, 32, false, IPreview::MODE_COVER, 64, 32], |
|
377 | + [2048, 1024, 32, 32, true, IPreview::MODE_FILL, 64, 64], |
|
378 | + [2048, 1024, 32, 32, true, IPreview::MODE_COVER, 64, 64], |
|
379 | + ]; |
|
380 | + } |
|
381 | + |
|
382 | + /** |
|
383 | + * @dataProvider dataSize |
|
384 | + * |
|
385 | + * @param int $maxX |
|
386 | + * @param int $maxY |
|
387 | + * @param int $reqX |
|
388 | + * @param int $reqY |
|
389 | + * @param bool $crop |
|
390 | + * @param string $mode |
|
391 | + * @param int $expectedX |
|
392 | + * @param int $expectedY |
|
393 | + */ |
|
394 | + public function testCorrectSize($maxX, $maxY, $reqX, $reqY, $crop, $mode, $expectedX, $expectedY): void { |
|
395 | + $file = $this->createMock(File::class); |
|
396 | + $file->method('isReadable') |
|
397 | + ->willReturn(true); |
|
398 | + $file->method('getMimeType') |
|
399 | + ->willReturn('myMimeType'); |
|
400 | + $file->method('getId') |
|
401 | + ->willReturn(42); |
|
402 | + |
|
403 | + $this->previewManager->method('isMimeSupported') |
|
404 | + ->with($this->equalTo('myMimeType')) |
|
405 | + ->willReturn(true); |
|
406 | + |
|
407 | + $previewFolder = $this->createMock(ISimpleFolder::class); |
|
408 | + $this->appData->method('getFolder') |
|
409 | + ->with($this->equalTo(42)) |
|
410 | + ->willReturn($previewFolder); |
|
411 | + |
|
412 | + $maxPreview = $this->createMock(ISimpleFile::class); |
|
413 | + $maxPreview->method('getName') |
|
414 | + ->willReturn($maxX . '-' . $maxY . '-max.png'); |
|
415 | + $maxPreview->method('getMimeType') |
|
416 | + ->willReturn('image/png'); |
|
417 | + $maxPreview->method('getSize')->willReturn(1000); |
|
418 | + |
|
419 | + $previewFolder->method('getDirectoryListing') |
|
420 | + ->willReturn([$maxPreview]); |
|
421 | + |
|
422 | + $filename = $expectedX . '-' . $expectedY; |
|
423 | + if ($crop) { |
|
424 | + $filename .= '-crop'; |
|
425 | + } |
|
426 | + $filename .= '.png'; |
|
427 | + $previewFolder->method('getFile') |
|
428 | + ->with($this->equalTo($filename)) |
|
429 | + ->willThrowException(new NotFoundException()); |
|
430 | + |
|
431 | + $image = $this->getMockImage($maxX, $maxY); |
|
432 | + $this->helper->method('getImage') |
|
433 | + ->with($this->equalTo($maxPreview)) |
|
434 | + ->willReturn($image); |
|
435 | + |
|
436 | + $preview = $this->createMock(ISimpleFile::class); |
|
437 | + $preview->method('getSize')->willReturn(1000); |
|
438 | + $previewFolder->method('newFile') |
|
439 | + ->with($this->equalTo($filename)) |
|
440 | + ->willReturn($preview); |
|
441 | + |
|
442 | + $this->eventDispatcher->expects($this->once()) |
|
443 | + ->method('dispatchTyped') |
|
444 | + ->with(new BeforePreviewFetchedEvent($file, $reqX, $reqY, $crop, $mode, null)); |
|
445 | + |
|
446 | + $result = $this->generator->getPreview($file, $reqX, $reqY, $crop, $mode); |
|
447 | + if ($expectedX === $maxX && $expectedY === $maxY) { |
|
448 | + $this->assertSame($maxPreview, $result); |
|
449 | + } else { |
|
450 | + $this->assertSame($preview, $result); |
|
451 | + } |
|
452 | + } |
|
453 | + |
|
454 | + public function testUnreadbleFile(): void { |
|
455 | + $file = $this->createMock(File::class); |
|
456 | + $file->method('isReadable') |
|
457 | + ->willReturn(false); |
|
458 | + |
|
459 | + $this->expectException(NotFoundException::class); |
|
460 | + |
|
461 | + $this->generator->getPreview($file, 100, 100, false); |
|
462 | + } |
|
463 | 463 | } |
@@ -125,12 +125,12 @@ discard block |
||
125 | 125 | ->willReturn($previewFolder); |
126 | 126 | |
127 | 127 | $this->config->method('getSystemValue') |
128 | - ->willReturnCallback(function ($key, $default) { |
|
128 | + ->willReturnCallback(function($key, $default) { |
|
129 | 129 | return $default; |
130 | 130 | }); |
131 | 131 | |
132 | 132 | $this->config->method('getSystemValueInt') |
133 | - ->willReturnCallback(function ($key, $default) { |
|
133 | + ->willReturnCallback(function($key, $default) { |
|
134 | 134 | return $default; |
135 | 135 | }); |
136 | 136 | |
@@ -152,7 +152,7 @@ discard block |
||
152 | 152 | ]); |
153 | 153 | |
154 | 154 | $this->helper->method('getProvider') |
155 | - ->willReturnCallback(function ($provider) use ($invalidProvider, $validProvider, $unavailableProvider) { |
|
155 | + ->willReturnCallback(function($provider) use ($invalidProvider, $validProvider, $unavailableProvider) { |
|
156 | 156 | if ($provider === 'wrongProvider') { |
157 | 157 | $this->fail('Wrongprovider should not be constructed!'); |
158 | 158 | } elseif ($provider === 'brokenProvider') { |
@@ -174,7 +174,7 @@ discard block |
||
174 | 174 | $image->method('dataMimeType')->willReturn('image/png'); |
175 | 175 | |
176 | 176 | $this->helper->method('getThumbnail') |
177 | - ->willReturnCallback(function ($provider, $file, $x, $y) use ($invalidProvider, $validProvider, $image) { |
|
177 | + ->willReturnCallback(function($provider, $file, $x, $y) use ($invalidProvider, $validProvider, $image) { |
|
178 | 178 | if ($provider === $validProvider) { |
179 | 179 | return $image; |
180 | 180 | } else { |
@@ -330,13 +330,13 @@ discard block |
||
330 | 330 | $image->method('dataMimeType')->willReturn('image/png'); |
331 | 331 | $image->method('data')->willReturn($data); |
332 | 332 | |
333 | - $image->method('resizeCopy')->willReturnCallback(function ($size) use ($data) { |
|
333 | + $image->method('resizeCopy')->willReturnCallback(function($size) use ($data) { |
|
334 | 334 | return $this->getMockImage($size, $size, $data); |
335 | 335 | }); |
336 | - $image->method('preciseResizeCopy')->willReturnCallback(function ($width, $height) use ($data) { |
|
336 | + $image->method('preciseResizeCopy')->willReturnCallback(function($width, $height) use ($data) { |
|
337 | 337 | return $this->getMockImage($width, $height, $data); |
338 | 338 | }); |
339 | - $image->method('cropCopy')->willReturnCallback(function ($x, $y, $width, $height) use ($data) { |
|
339 | + $image->method('cropCopy')->willReturnCallback(function($x, $y, $width, $height) use ($data) { |
|
340 | 340 | return $this->getMockImage($width, $height, $data); |
341 | 341 | }); |
342 | 342 | |
@@ -411,7 +411,7 @@ discard block |
||
411 | 411 | |
412 | 412 | $maxPreview = $this->createMock(ISimpleFile::class); |
413 | 413 | $maxPreview->method('getName') |
414 | - ->willReturn($maxX . '-' . $maxY . '-max.png'); |
|
414 | + ->willReturn($maxX.'-'.$maxY.'-max.png'); |
|
415 | 415 | $maxPreview->method('getMimeType') |
416 | 416 | ->willReturn('image/png'); |
417 | 417 | $maxPreview->method('getSize')->willReturn(1000); |
@@ -419,7 +419,7 @@ discard block |
||
419 | 419 | $previewFolder->method('getDirectoryListing') |
420 | 420 | ->willReturn([$maxPreview]); |
421 | 421 | |
422 | - $filename = $expectedX . '-' . $expectedY; |
|
422 | + $filename = $expectedX.'-'.$expectedY; |
|
423 | 423 | if ($crop) { |
424 | 424 | $filename .= '-crop'; |
425 | 425 | } |