Passed
Push — master ( 5cef89...027486 )
by Roeland
21:53 queued 11:52
created

Generator::generatePreview()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 39
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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