Completed
Push — master ( ac1aff...a37399 )
by Lukas
354:50 queued 342:03
created

Generator::getMaxPreview()   D

Complexity

Conditions 9
Paths 13

Size

Total Lines 44
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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