Passed
Push — master ( b6c034...979f40 )
by John
40:53 queued 23:59
created

Generator   F

Complexity

Total Complexity 89

Size/Duplication

Total Lines 610
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 262
c 5
b 0
f 0
dl 0
loc 610
rs 2
wmc 89

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getPreview() 0 18 1
A __construct() 0 14 1
A getPreviewFolder() 0 11 2
A generatePath() 0 12 3
A getSmallImagePreview() 0 8 2
A getHardwareConcurrency() 0 10 3
A unguardWithSemaphore() 0 5 3
A getPreviewSize() 0 3 1
A getNumConcurrentPreviews() 0 22 6
F generatePreviews() 0 105 22
C calculateSize() 0 76 13
A getCachedPreview() 0 8 3
A getMaxPreview() 0 14 5
B generatePreview() 0 44 7
B generateProviderPreview() 0 47 9
A guardWithSemaphore() 0 12 4
A getExtension() 0 10 4

How to fix   Complexity   

Complex Class

Complex classes like Generator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Generator, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, Roeland Jago Douma <[email protected]>
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Elijah Martin-Merrill <[email protected]>
7
 * @author J0WI <[email protected]>
8
 * @author John Molakvoæ <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Scott Dutton <[email protected]>
13
 *
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
namespace OC\Preview;
31
32
use OCP\EventDispatcher\IEventDispatcher;
33
use OCP\Files\File;
34
use OCP\Files\IAppData;
35
use OCP\Files\InvalidPathException;
36
use OCP\Files\NotFoundException;
37
use OCP\Files\NotPermittedException;
38
use OCP\Files\SimpleFS\ISimpleFile;
39
use OCP\Files\SimpleFS\ISimpleFolder;
40
use OCP\IConfig;
41
use OCP\IImage;
42
use OCP\IPreview;
43
use OCP\IStreamImage;
44
use OCP\Preview\BeforePreviewFetchedEvent;
45
use OCP\Preview\IProviderV2;
46
use OCP\Preview\IVersionedPreviewFile;
47
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
48
use Symfony\Component\EventDispatcher\GenericEvent;
49
50
class Generator {
51
	public const SEMAPHORE_ID_ALL = 0x0a11;
52
	public const SEMAPHORE_ID_NEW = 0x07ea;
53
54
	/** @var IPreview */
55
	private $previewManager;
56
	/** @var IConfig */
57
	private $config;
58
	/** @var IAppData */
59
	private $appData;
60
	/** @var GeneratorHelper */
61
	private $helper;
62
	/** @var EventDispatcherInterface */
63
	private $legacyEventDispatcher;
64
	/** @var IEventDispatcher */
65
	private $eventDispatcher;
66
67
	public function __construct(
68
		IConfig $config,
69
		IPreview $previewManager,
70
		IAppData $appData,
71
		GeneratorHelper $helper,
72
		EventDispatcherInterface $legacyEventDispatcher,
73
		IEventDispatcher $eventDispatcher
74
	) {
75
		$this->config = $config;
76
		$this->previewManager = $previewManager;
77
		$this->appData = $appData;
78
		$this->helper = $helper;
79
		$this->legacyEventDispatcher = $legacyEventDispatcher;
80
		$this->eventDispatcher = $eventDispatcher;
81
	}
82
83
	/**
84
	 * Returns a preview of a file
85
	 *
86
	 * The cache is searched first and if nothing usable was found then a preview is
87
	 * generated by one of the providers
88
	 *
89
	 * @param File $file
90
	 * @param int $width
91
	 * @param int $height
92
	 * @param bool $crop
93
	 * @param string $mode
94
	 * @param string $mimeType
95
	 * @return ISimpleFile
96
	 * @throws NotFoundException
97
	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
98
	 */
99
	public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) {
100
		$specification = [
101
			'width' => $width,
102
			'height' => $height,
103
			'crop' => $crop,
104
			'mode' => $mode,
105
		];
106
107
		$this->legacyEventDispatcher->dispatch(
108
			IPreview::EVENT,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\IPreview::EVENT has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

108
			/** @scrutinizer ignore-deprecated */ IPreview::EVENT,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
109
			new GenericEvent($file, $specification)
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...($file, $specification). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

109
		$this->legacyEventDispatcher->/** @scrutinizer ignore-call */ 
110
                                dispatch(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
110
		);
111
		$this->eventDispatcher->dispatchTyped(new BeforePreviewFetchedEvent(
112
			$file
113
		));
114
115
		// since we only ask for one preview, and the generate method return the last one it created, it returns the one we want
116
		return $this->generatePreviews($file, [$specification], $mimeType);
117
	}
118
119
	/**
120
	 * Generates previews of a file
121
	 *
122
	 * @param File $file
123
	 * @param non-empty-array $specifications
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-array at position 0 could not be parsed: Unknown type name 'non-empty-array' at position 0 in non-empty-array.
Loading history...
124
	 * @param string $mimeType
125
	 * @return ISimpleFile the last preview that was generated
126
	 * @throws NotFoundException
127
	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
128
	 */
129
	public function generatePreviews(File $file, array $specifications, $mimeType = null) {
130
		//Make sure that we can read the file
131
		if (!$file->isReadable()) {
132
			throw new NotFoundException('Cannot read file');
133
		}
134
135
		if ($mimeType === null) {
136
			$mimeType = $file->getMimeType();
137
		}
138
139
		$previewFolder = $this->getPreviewFolder($file);
140
		// List every existing preview first instead of trying to find them one by one
141
		$previewFiles = $previewFolder->getDirectoryListing();
142
143
		$previewVersion = '';
144
		if ($file instanceof IVersionedPreviewFile) {
145
			$previewVersion = $file->getPreviewVersion() . '-';
146
		}
147
148
		// If imaginary is enabled, and we request a small thumbnail,
149
		// let's not generate the max preview for performance reasons
150
		if (count($specifications) === 1
151
			&& ($specifications[0]['width'] <= 256 || $specifications[0]['height'] <= 256)
152
			&& preg_match(Imaginary::supportedMimeTypes(), $mimeType)
153
			&& $this->config->getSystemValueString('preview_imaginary_url', 'invalid') !== 'invalid') {
154
			$crop = $specifications[0]['crop'] ?? false;
155
			$preview = $this->getSmallImagePreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion, $crop);
156
157
			if ($preview->getSize() === 0) {
158
				$preview->delete();
159
				throw new NotFoundException('Cached preview size 0, invalid!');
160
			}
161
162
			return $preview;
163
		}
164
165
		// Get the max preview and infer the max preview sizes from that
166
		$maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion);
167
		$maxPreviewImage = null; // only load the image when we need it
168
		if ($maxPreview->getSize() === 0) {
169
			$maxPreview->delete();
170
			throw new NotFoundException('Max preview size 0, invalid!');
171
		}
172
173
		[$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion);
174
175
		$preview = null;
176
177
		foreach ($specifications as $specification) {
178
			$width = $specification['width'] ?? -1;
179
			$height = $specification['height'] ?? -1;
180
			$crop = $specification['crop'] ?? false;
181
			$mode = $specification['mode'] ?? IPreview::MODE_FILL;
182
183
			// If both width and height are -1 we just want the max preview
184
			if ($width === -1 && $height === -1) {
185
				$width = $maxWidth;
186
				$height = $maxHeight;
187
			}
188
189
			// Calculate the preview size
190
			[$width, $height] = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
191
192
			// No need to generate a preview that is just the max preview
193
			if ($width === $maxWidth && $height === $maxHeight) {
194
				// ensure correct return value if this was the last one
195
				$preview = $maxPreview;
196
				continue;
197
			}
198
199
			// Try to get a cached preview. Else generate (and store) one
200
			try {
201
				try {
202
					$preview = $this->getCachedPreview($previewFiles, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion);
203
				} catch (NotFoundException $e) {
204
					if (!$this->previewManager->isMimeSupported($mimeType)) {
205
						throw new NotFoundException();
206
					}
207
208
					if ($maxPreviewImage === null) {
209
						$maxPreviewImage = $this->helper->getImage($maxPreview);
210
					}
211
212
					$preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion);
213
					// New file, augment our array
214
					$previewFiles[] = $preview;
215
				}
216
			} catch (\InvalidArgumentException $e) {
217
				throw new NotFoundException("", 0, $e);
218
			}
219
220
			if ($preview->getSize() === 0) {
221
				$preview->delete();
222
				throw new NotFoundException('Cached preview size 0, invalid!');
223
			}
224
		}
225
		assert($preview !== null);
226
227
		// Free memory being used by the embedded image resource.  Without this the image is kept in memory indefinitely.
228
		// Garbage Collection does NOT free this memory.  We have to do it ourselves.
229
		if ($maxPreviewImage instanceof \OCP\Image) {
230
			$maxPreviewImage->destroy();
231
		}
232
233
		return $preview;
234
	}
235
236
	/**
237
	 * Generate a small image straight away without generating a max preview first
238
	 * Preview generated is 256x256
239
	 *
240
	 * @param ISimpleFile[] $previewFiles
241
	 *
242
	 * @throws NotFoundException
243
	 */
244
	private function getSmallImagePreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, string $mimeType, string $prefix, bool $crop): ISimpleFile {
245
		$width = 256;
246
		$height = 256;
247
248
		try {
249
			return $this->getCachedPreview($previewFiles, $width, $height, $crop, $mimeType, $prefix);
250
		} catch (NotFoundException $e) {
251
			return $this->generateProviderPreview($previewFolder, $file, $width, $height, $crop, false, $mimeType, $prefix);
252
		}
253
	}
254
255
	/**
256
	 * Acquire a semaphore of the specified id and concurrency, blocking if necessary.
257
	 * Return an identifier of the semaphore on success, which can be used to release it via
258
	 * {@see Generator::unguardWithSemaphore()}.
259
	 *
260
	 * @param int $semId
261
	 * @param int $concurrency
262
	 * @return false|resource the semaphore on success or false on failure
263
	 */
264
	public static function guardWithSemaphore(int $semId, int $concurrency) {
265
		if (!extension_loaded('sysvsem')) {
266
			return false;
267
		}
268
		$sem = sem_get($semId, $concurrency);
269
		if ($sem === false) {
270
			return false;
271
		}
272
		if (!sem_acquire($sem)) {
273
			return false;
274
		}
275
		return $sem;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sem also could return the type SysvSemaphore which is incompatible with the documented return type false|resource.
Loading history...
276
	}
277
278
	/**
279
	 * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}.
280
	 *
281
	 * @param resource|bool $semId the semaphore identifier returned by guardWithSemaphore
282
	 * @return bool
283
	 */
284
	public static function unguardWithSemaphore($semId): bool {
285
		if (!is_resource($semId) || !extension_loaded('sysvsem')) {
286
			return false;
287
		}
288
		return sem_release($semId);
289
	}
290
291
	/**
292
	 * Get the number of concurrent threads supported by the host.
293
	 *
294
	 * @return int number of concurrent threads, or 0 if it cannot be determined
295
	 */
296
	public static function getHardwareConcurrency(): int {
297
		static $width;
298
		if (!isset($width)) {
299
			if (is_file("/proc/cpuinfo")) {
300
				$width = substr_count(file_get_contents("/proc/cpuinfo"), "processor");
301
			} else {
302
				$width = 0;
303
			}
304
		}
305
		return $width;
306
	}
307
308
	/**
309
	 * Get number of concurrent preview generations from system config
310
	 *
311
	 * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`,
312
	 * are available. If not set, the default values are determined with the hardware concurrency
313
	 * of the host. In case the hardware concurrency cannot be determined, or the user sets an
314
	 * invalid value, fallback values are:
315
	 * For new images whose previews do not exist and need to be generated, 4;
316
	 * For all preview generation requests, 8.
317
	 * Value of `preview_concurrency_all` should be greater than or equal to that of
318
	 * `preview_concurrency_new`, otherwise, the latter is returned.
319
	 *
320
	 * @param string $type either `preview_concurrency_new` or `preview_concurrency_all`
321
	 * @return int number of concurrent preview generations, or -1 if $type is invalid
322
	 */
323
	public function getNumConcurrentPreviews(string $type): int {
324
		static $cached = array();
325
		if (array_key_exists($type, $cached)) {
326
			return $cached[$type];
327
		}
328
329
		$hardwareConcurrency = self::getHardwareConcurrency();
330
		switch ($type) {
331
			case "preview_concurrency_all":
332
				$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8;
333
				$concurrency_all = $this->config->getSystemValueInt($type, $fallback);
334
				$concurrency_new = $this->getNumConcurrentPreviews("preview_concurrency_new");
335
				$cached[$type] = max($concurrency_all, $concurrency_new);
336
				break;
337
			case "preview_concurrency_new":
338
				$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4;
339
				$cached[$type] = $this->config->getSystemValueInt($type, $fallback);
340
				break;
341
			default:
342
				return -1;
343
		}
344
		return $cached[$type];
345
	}
346
347
	/**
348
	 * @param ISimpleFolder $previewFolder
349
	 * @param ISimpleFile[] $previewFiles
350
	 * @param File $file
351
	 * @param string $mimeType
352
	 * @param string $prefix
353
	 * @return ISimpleFile
354
	 * @throws NotFoundException
355
	 */
356
	private function getMaxPreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, $mimeType, $prefix) {
357
		// We don't know the max preview size, so we can't use getCachedPreview.
358
		// It might have been generated with a higher resolution than the current value.
359
		foreach ($previewFiles as $node) {
360
			$name = $node->getName();
361
			if (($prefix === '' || strpos($name, $prefix) === 0) && strpos($name, 'max')) {
362
				return $node;
363
			}
364
		}
365
366
		$maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096);
367
		$maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096);
368
369
		return $this->generateProviderPreview($previewFolder, $file, $maxWidth, $maxHeight, false, true, $mimeType, $prefix);
370
	}
371
372
	private function generateProviderPreview(ISimpleFolder $previewFolder, File $file, int $width, int $height, bool $crop, bool $max, string $mimeType, string $prefix) {
373
		$previewProviders = $this->previewManager->getProviders();
374
		foreach ($previewProviders as $supportedMimeType => $providers) {
375
			// Filter out providers that does not support this mime
376
			if (!preg_match($supportedMimeType, $mimeType)) {
377
				continue;
378
			}
379
380
			foreach ($providers as $providerClosure) {
381
				$provider = $this->helper->getProvider($providerClosure);
382
				if (!($provider instanceof IProviderV2)) {
383
					continue;
384
				}
385
386
				if (!$provider->isAvailable($file)) {
387
					continue;
388
				}
389
390
				$previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
391
				$sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
392
				try {
393
					$preview = $this->helper->getThumbnail($provider, $file, $width, $height);
394
				} finally {
395
					self::unguardWithSemaphore($sem);
396
				}
397
398
				if (!($preview instanceof IImage)) {
399
					continue;
400
				}
401
402
				$path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix);
403
				try {
404
					$file = $previewFolder->newFile($path);
405
					if ($preview instanceof IStreamImage) {
406
						$file->putContent($preview->resource());
407
					} else {
408
						$file->putContent($preview->data());
409
					}
410
				} catch (NotPermittedException $e) {
411
					throw new NotFoundException();
412
				}
413
414
				return $file;
415
			}
416
		}
417
418
		throw new NotFoundException('No provider successfully handled the preview generation');
419
	}
420
421
	/**
422
	 * @param ISimpleFile $file
423
	 * @param string $prefix
424
	 * @return int[]
425
	 */
426
	private function getPreviewSize(ISimpleFile $file, string $prefix = '') {
427
		$size = explode('-', substr($file->getName(), strlen($prefix)));
428
		return [(int)$size[0], (int)$size[1]];
429
	}
430
431
	/**
432
	 * @param int $width
433
	 * @param int $height
434
	 * @param bool $crop
435
	 * @param bool $max
436
	 * @param string $mimeType
437
	 * @param string $prefix
438
	 * @return string
439
	 */
440
	private function generatePath($width, $height, $crop, $max, $mimeType, $prefix) {
441
		$path = $prefix . (string)$width . '-' . (string)$height;
442
		if ($crop) {
443
			$path .= '-crop';
444
		}
445
		if ($max) {
446
			$path .= '-max';
447
		}
448
449
		$ext = $this->getExtension($mimeType);
450
		$path .= '.' . $ext;
451
		return $path;
452
	}
453
454
455
	/**
456
	 * @param int $width
457
	 * @param int $height
458
	 * @param bool $crop
459
	 * @param string $mode
460
	 * @param int $maxWidth
461
	 * @param int $maxHeight
462
	 * @return int[]
463
	 */
464
	private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) {
465
		/*
466
		 * If we are not cropping we have to make sure the requested image
467
		 * respects the aspect ratio of the original.
468
		 */
469
		if (!$crop) {
470
			$ratio = $maxHeight / $maxWidth;
471
472
			if ($width === -1) {
473
				$width = $height / $ratio;
474
			}
475
			if ($height === -1) {
476
				$height = $width * $ratio;
477
			}
478
479
			$ratioH = $height / $maxHeight;
480
			$ratioW = $width / $maxWidth;
481
482
			/*
483
			 * Fill means that the $height and $width are the max
484
			 * Cover means min.
485
			 */
486
			if ($mode === IPreview::MODE_FILL) {
487
				if ($ratioH > $ratioW) {
488
					$height = $width * $ratio;
489
				} else {
490
					$width = $height / $ratio;
491
				}
492
			} elseif ($mode === IPreview::MODE_COVER) {
493
				if ($ratioH > $ratioW) {
494
					$width = $height / $ratio;
495
				} else {
496
					$height = $width * $ratio;
497
				}
498
			}
499
		}
500
501
		if ($height !== $maxHeight && $width !== $maxWidth) {
502
			/*
503
			 * Scale to the nearest power of four
504
			 */
505
			$pow4height = 4 ** ceil(log($height) / log(4));
506
			$pow4width = 4 ** ceil(log($width) / log(4));
507
508
			// Minimum size is 64
509
			$pow4height = max($pow4height, 64);
510
			$pow4width = max($pow4width, 64);
511
512
			$ratioH = $height / $pow4height;
513
			$ratioW = $width / $pow4width;
514
515
			if ($ratioH < $ratioW) {
516
				$width = $pow4width;
517
				$height /= $ratioW;
518
			} else {
519
				$height = $pow4height;
520
				$width /= $ratioH;
521
			}
522
		}
523
524
		/*
525
		 * Make sure the requested height and width fall within the max
526
		 * of the preview.
527
		 */
528
		if ($height > $maxHeight) {
529
			$ratio = $height / $maxHeight;
530
			$height = $maxHeight;
531
			$width /= $ratio;
532
		}
533
		if ($width > $maxWidth) {
534
			$ratio = $width / $maxWidth;
535
			$width = $maxWidth;
536
			$height /= $ratio;
537
		}
538
539
		return [(int)round($width), (int)round($height)];
540
	}
541
542
	/**
543
	 * @param ISimpleFolder $previewFolder
544
	 * @param ISimpleFile $maxPreview
545
	 * @param int $width
546
	 * @param int $height
547
	 * @param bool $crop
548
	 * @param int $maxWidth
549
	 * @param int $maxHeight
550
	 * @param string $prefix
551
	 * @return ISimpleFile
552
	 * @throws NotFoundException
553
	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
554
	 */
555
	private function generatePreview(ISimpleFolder $previewFolder, IImage $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight, $prefix) {
556
		$preview = $maxPreview;
557
		if (!$preview->valid()) {
558
			throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
559
		}
560
561
		$previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
562
		$sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
563
		try {
564
			if ($crop) {
565
				if ($height !== $preview->height() && $width !== $preview->width()) {
566
					//Resize
567
					$widthR = $preview->width() / $width;
568
					$heightR = $preview->height() / $height;
569
570
					if ($widthR > $heightR) {
571
						$scaleH = $height;
572
						$scaleW = $maxWidth / $heightR;
573
					} else {
574
						$scaleH = $maxHeight / $widthR;
575
						$scaleW = $width;
576
					}
577
					$preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH));
578
				}
579
				$cropX = (int)floor(abs($width - $preview->width()) * 0.5);
580
				$cropY = (int)floor(abs($height - $preview->height()) * 0.5);
581
				$preview = $preview->cropCopy($cropX, $cropY, $width, $height);
582
			} else {
583
				$preview = $maxPreview->resizeCopy(max($width, $height));
584
			}
585
		} finally {
586
			self::unguardWithSemaphore($sem);
587
		}
588
589
590
		$path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix);
591
		try {
592
			$file = $previewFolder->newFile($path);
593
			$file->putContent($preview->data());
594
		} catch (NotPermittedException $e) {
595
			throw new NotFoundException();
596
		}
597
598
		return $file;
599
	}
600
601
	/**
602
	 * @param ISimpleFile[] $files Array of FileInfo, as the result of getDirectoryListing()
603
	 * @param int $width
604
	 * @param int $height
605
	 * @param bool $crop
606
	 * @param string $mimeType
607
	 * @param string $prefix
608
	 * @return ISimpleFile
609
	 *
610
	 * @throws NotFoundException
611
	 */
612
	private function getCachedPreview($files, $width, $height, $crop, $mimeType, $prefix) {
613
		$path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix);
614
		foreach ($files as $file) {
615
			if ($file->getName() === $path) {
616
				return $file;
617
			}
618
		}
619
		throw new NotFoundException();
620
	}
621
622
	/**
623
	 * Get the specific preview folder for this file
624
	 *
625
	 * @param File $file
626
	 * @return ISimpleFolder
627
	 *
628
	 * @throws InvalidPathException
629
	 * @throws NotFoundException
630
	 * @throws NotPermittedException
631
	 */
632
	private function getPreviewFolder(File $file) {
633
		// Obtain file id outside of try catch block to prevent the creation of an existing folder
634
		$fileId = (string)$file->getId();
635
636
		try {
637
			$folder = $this->appData->getFolder($fileId);
638
		} catch (NotFoundException $e) {
639
			$folder = $this->appData->newFolder($fileId);
640
		}
641
642
		return $folder;
643
	}
644
645
	/**
646
	 * @param string $mimeType
647
	 * @return null|string
648
	 * @throws \InvalidArgumentException
649
	 */
650
	private function getExtension($mimeType) {
651
		switch ($mimeType) {
652
			case 'image/png':
653
				return 'png';
654
			case 'image/jpeg':
655
				return 'jpg';
656
			case 'image/gif':
657
				return 'gif';
658
			default:
659
				throw new \InvalidArgumentException('Not a valid mimetype: "' . $mimeType . '"');
660
		}
661
	}
662
}
663