Passed
Push — master ( edf8ce...eba372 )
by Roeland
13:54 queued 15s
created

Generator::generatePreview()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 38
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 26
nc 13
nop 8
dl 0
loc 38
rs 8.5706
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

102
		$this->eventDispatcher->/** @scrutinizer ignore-call */ 
103
                          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...
103
		);
104
105
		// since we only ask for one preview, and the generate method return the last one it created, it returns the one we want
106
		return $this->generatePreviews($file, [$specification], $mimeType);
107
	}
108
109
	/**
110
	 * Generates previews of a file
111
	 *
112
	 * @param File $file
113
	 * @param array $specifications
114
	 * @param string $mimeType
115
	 * @return ISimpleFile the last preview that was generated
116
	 * @throws NotFoundException
117
	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
118
	 */
119
	public function generatePreviews(File $file, array $specifications, $mimeType = null) {
120
		//Make sure that we can read the file
121
		if (!$file->isReadable()) {
122
			throw new NotFoundException('Cannot read file');
123
		}
124
125
		if ($mimeType === null) {
126
			$mimeType = $file->getMimeType();
127
		}
128
		if (!$this->previewManager->isMimeSupported($mimeType)) {
129
			throw new NotFoundException();
130
		}
131
132
		$previewFolder = $this->getPreviewFolder($file);
133
134
		$previewVersion = '';
135
		if ($file instanceof IVersionedPreviewFile) {
136
			$previewVersion = $file->getPreviewVersion() . '-';
137
		}
138
139
		// Get the max preview and infer the max preview sizes from that
140
		$maxPreview = $this->getMaxPreview($previewFolder, $file, $mimeType, $previewVersion);
141
		$maxPreviewImage = null; // only load the image when we need it
142
		if ($maxPreview->getSize() === 0) {
143
			$maxPreview->delete();
144
			throw new NotFoundException('Max preview size 0, invalid!');
145
		}
146
147
		[$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion);
148
149
		$preview = null;
150
151
		foreach ($specifications as $specification) {
152
			$width = $specification['width'] ?? -1;
153
			$height = $specification['height'] ?? -1;
154
			$crop = $specification['crop'] ?? false;
155
			$mode = $specification['mode'] ?? IPreview::MODE_FILL;
156
157
			// If both width and heigth are -1 we just want the max preview
158
			if ($width === -1 && $height === -1) {
159
				$width = $maxWidth;
160
				$height = $maxHeight;
161
			}
162
163
			// Calculate the preview size
164
			[$width, $height] = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
165
166
			// No need to generate a preview that is just the max preview
167
			if ($width === $maxWidth && $height === $maxHeight) {
168
				// ensure correct return value if this was the last one
169
				$preview = $maxPreview;
170
				continue;
171
			}
172
173
			// Try to get a cached preview. Else generate (and store) one
174
			try {
175
				try {
176
					$preview = $this->getCachedPreview($previewFolder, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion);
177
				} catch (NotFoundException $e) {
178
					if ($maxPreviewImage === null) {
179
						$maxPreviewImage = $this->helper->getImage($maxPreview);
180
					}
181
182
					$preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion);
183
				}
184
			} catch (\InvalidArgumentException $e) {
185
				throw new NotFoundException("", 0, $e);
186
			}
187
188
			if ($preview->getSize() === 0) {
189
				$preview->delete();
190
				throw new NotFoundException('Cached preview size 0, invalid!');
191
			}
192
		}
193
194
		return $preview;
195
	}
196
197
	/**
198
	 * @param ISimpleFolder $previewFolder
199
	 * @param File $file
200
	 * @param string $mimeType
201
	 * @param string $prefix
202
	 * @return ISimpleFile
203
	 * @throws NotFoundException
204
	 */
205
	private function getMaxPreview(ISimpleFolder $previewFolder, File $file, $mimeType, $prefix) {
206
		$nodes = $previewFolder->getDirectoryListing();
207
208
		foreach ($nodes as $node) {
209
			$name = $node->getName();
210
			if (($prefix === '' || strpos($name, $prefix) === 0) && strpos($name, 'max')) {
211
				return $node;
212
			}
213
		}
214
215
		$previewProviders = $this->previewManager->getProviders();
216
		foreach ($previewProviders as $supportedMimeType => $providers) {
217
			if (!preg_match($supportedMimeType, $mimeType)) {
218
				continue;
219
			}
220
221
			foreach ($providers as $providerClosure) {
222
				$provider = $this->helper->getProvider($providerClosure);
223
				if (!($provider instanceof IProviderV2)) {
224
					continue;
225
				}
226
227
				if (!$provider->isAvailable($file)) {
228
					continue;
229
				}
230
231
				$maxWidth = (int)$this->config->getSystemValue('preview_max_x', 4096);
232
				$maxHeight = (int)$this->config->getSystemValue('preview_max_y', 4096);
233
234
				$preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight);
235
236
				if (!($preview instanceof IImage)) {
237
					continue;
238
				}
239
240
				// Try to get the extention.
241
				try {
242
					$ext = $this->getExtention($preview->dataMimeType());
243
				} catch (\InvalidArgumentException $e) {
244
					// Just continue to the next iteration if this preview doesn't have a valid mimetype
245
					continue;
246
				}
247
248
				$path = $prefix . (string)$preview->width() . '-' . (string)$preview->height() . '-max.' . $ext;
249
				try {
250
					$file = $previewFolder->newFile($path);
251
					$file->putContent($preview->data());
252
				} catch (NotPermittedException $e) {
253
					throw new NotFoundException();
254
				}
255
256
				return $file;
257
			}
258
		}
259
260
		throw new NotFoundException();
261
	}
262
263
	/**
264
	 * @param ISimpleFile $file
265
	 * @param string $prefix
266
	 * @return int[]
267
	 */
268
	private function getPreviewSize(ISimpleFile $file, string $prefix = '') {
269
		$size = explode('-', substr($file->getName(), strlen($prefix)));
270
		return [(int)$size[0], (int)$size[1]];
271
	}
272
273
	/**
274
	 * @param int $width
275
	 * @param int $height
276
	 * @param bool $crop
277
	 * @param string $mimeType
278
	 * @param string $prefix
279
	 * @return string
280
	 */
281
	private function generatePath($width, $height, $crop, $mimeType, $prefix) {
282
		$path = $prefix . (string)$width . '-' . (string)$height;
283
		if ($crop) {
284
			$path .= '-crop';
285
		}
286
287
		$ext = $this->getExtention($mimeType);
288
		$path .= '.' . $ext;
289
		return $path;
290
	}
291
292
293
	/**
294
	 * @param int $width
295
	 * @param int $height
296
	 * @param bool $crop
297
	 * @param string $mode
298
	 * @param int $maxWidth
299
	 * @param int $maxHeight
300
	 * @return int[]
301
	 */
302
	private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) {
303
304
		/*
305
		 * If we are not cropping we have to make sure the requested image
306
		 * respects the aspect ratio of the original.
307
		 */
308
		if (!$crop) {
309
			$ratio = $maxHeight / $maxWidth;
310
311
			if ($width === -1) {
312
				$width = $height / $ratio;
313
			}
314
			if ($height === -1) {
315
				$height = $width * $ratio;
316
			}
317
318
			$ratioH = $height / $maxHeight;
319
			$ratioW = $width / $maxWidth;
320
321
			/*
322
			 * Fill means that the $height and $width are the max
323
			 * Cover means min.
324
			 */
325
			if ($mode === IPreview::MODE_FILL) {
326
				if ($ratioH > $ratioW) {
327
					$height = $width * $ratio;
328
				} else {
329
					$width = $height / $ratio;
330
				}
331
			} elseif ($mode === IPreview::MODE_COVER) {
332
				if ($ratioH > $ratioW) {
333
					$width = $height / $ratio;
334
				} else {
335
					$height = $width * $ratio;
336
				}
337
			}
338
		}
339
340
		if ($height !== $maxHeight && $width !== $maxWidth) {
341
			/*
342
			 * Scale to the nearest power of four
343
			 */
344
			$pow4height = 4 ** ceil(log($height) / log(4));
345
			$pow4width = 4 ** ceil(log($width) / log(4));
346
347
			// Minimum size is 64
348
			$pow4height = max($pow4height, 64);
349
			$pow4width = max($pow4width, 64);
350
351
			$ratioH = $height / $pow4height;
352
			$ratioW = $width / $pow4width;
353
354
			if ($ratioH < $ratioW) {
355
				$width = $pow4width;
356
				$height /= $ratioW;
357
			} else {
358
				$height = $pow4height;
359
				$width /= $ratioH;
360
			}
361
		}
362
363
		/*
364
		 * Make sure the requested height and width fall within the max
365
		 * of the preview.
366
		 */
367
		if ($height > $maxHeight) {
368
			$ratio = $height / $maxHeight;
369
			$height = $maxHeight;
370
			$width /= $ratio;
371
		}
372
		if ($width > $maxWidth) {
373
			$ratio = $width / $maxWidth;
374
			$width = $maxWidth;
375
			$height /= $ratio;
376
		}
377
378
		return [(int)round($width), (int)round($height)];
379
	}
380
381
	/**
382
	 * @param ISimpleFolder $previewFolder
383
	 * @param ISimpleFile $maxPreview
384
	 * @param int $width
385
	 * @param int $height
386
	 * @param bool $crop
387
	 * @param int $maxWidth
388
	 * @param int $maxHeight
389
	 * @param string $prefix
390
	 * @return ISimpleFile
391
	 * @throws NotFoundException
392
	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
393
	 */
394
	private function generatePreview(ISimpleFolder $previewFolder, IImage $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight, $prefix) {
395
		$preview = $maxPreview;
396
		if (!$preview->valid()) {
397
			throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
398
		}
399
400
		if ($crop) {
401
			if ($height !== $preview->height() && $width !== $preview->width()) {
402
				//Resize
403
				$widthR = $preview->width() / $width;
404
				$heightR = $preview->height() / $height;
405
406
				if ($widthR > $heightR) {
407
					$scaleH = $height;
408
					$scaleW = $maxWidth / $heightR;
409
				} else {
410
					$scaleH = $maxHeight / $widthR;
411
					$scaleW = $width;
412
				}
413
				$preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH));
414
			}
415
			$cropX = (int)floor(abs($width - $preview->width()) * 0.5);
416
			$cropY = (int)floor(abs($height - $preview->height()) * 0.5);
417
			$preview = $preview->cropCopy($cropX, $cropY, $width, $height);
418
		} else {
419
			$preview = $maxPreview->resizeCopy(max($width, $height));
420
		}
421
422
423
		$path = $this->generatePath($width, $height, $crop, $preview->dataMimeType(), $prefix);
424
		try {
425
			$file = $previewFolder->newFile($path);
426
			$file->putContent($preview->data());
427
		} catch (NotPermittedException $e) {
428
			throw new NotFoundException();
429
		}
430
431
		return $file;
432
	}
433
434
	/**
435
	 * @param ISimpleFolder $previewFolder
436
	 * @param int $width
437
	 * @param int $height
438
	 * @param bool $crop
439
	 * @param string $mimeType
440
	 * @param string $prefix
441
	 * @return ISimpleFile
442
	 *
443
	 * @throws NotFoundException
444
	 */
445
	private function getCachedPreview(ISimpleFolder $previewFolder, $width, $height, $crop, $mimeType, $prefix) {
446
		$path = $this->generatePath($width, $height, $crop, $mimeType, $prefix);
447
448
		return $previewFolder->getFile($path);
449
	}
450
451
	/**
452
	 * Get the specific preview folder for this file
453
	 *
454
	 * @param File $file
455
	 * @return ISimpleFolder
456
	 */
457
	private function getPreviewFolder(File $file) {
458
		try {
459
			$folder = $this->appData->getFolder($file->getId());
460
		} catch (NotFoundException $e) {
461
			$folder = $this->appData->newFolder($file->getId());
462
		}
463
464
		return $folder;
465
	}
466
467
	/**
468
	 * @param string $mimeType
469
	 * @return null|string
470
	 * @throws \InvalidArgumentException
471
	 */
472
	private function getExtention($mimeType) {
473
		switch ($mimeType) {
474
			case 'image/png':
475
				return 'png';
476
			case 'image/jpeg':
477
				return 'jpg';
478
			case 'image/gif':
479
				return 'gif';
480
			default:
481
				throw new \InvalidArgumentException('Not a valid mimetype: "' . $mimeType . '"');
482
		}
483
	}
484
}
485