Completed
Push — master ( 8bf574...c0bbae )
by Lukas
28:01 queued 15:22
created

Generator::getPreviewSize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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\SimpleFS\ISimpleFile;
30
use OCP\Files\SimpleFS\ISimpleFolder;
31
use OCP\IConfig;
32
use OCP\IImage;
33
use OCP\IPreview;
34
use OCP\Preview\IProvider;
35
36
class Generator {
37
38
	/** @var IPreview */
39
	private $previewManager;
40
	/** @var IConfig */
41
	private $config;
42
	/** @var IAppData */
43
	private $appData;
44
	/** @var GeneratorHelper */
45
	private $helper;
46
47
	/**
48
	 * @param IConfig $config
49
	 * @param IPreview $previewManager
50
	 * @param IAppData $appData
51
	 * @param GeneratorHelper $helper
52
	 */
53
	public function __construct(
54
		IConfig $config,
55
		IPreview $previewManager,
56
		IAppData $appData,
57
		GeneratorHelper $helper
58
	) {
59
		$this->config = $config;
60
		$this->previewManager = $previewManager;
61
		$this->appData = $appData;
62
		$this->helper = $helper;
63
	}
64
65
	/**
66
	 * Returns a preview of a file
67
	 *
68
	 * The cache is searched first and if nothing usable was found then a preview is
69
	 * generated by one of the providers
70
	 *
71
	 * @param File $file
72
	 * @param int $width
73
	 * @param int $height
74
	 * @param bool $crop
75
	 * @param string $mode
76
	 * @param string $mimeType
77
	 * @return ISimpleFile
78
	 * @throws NotFoundException
79
	 */
80
	public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) {
81
		if ($mimeType === null) {
82
			$mimeType = $file->getMimeType();
83
		}
84
		if (!$this->previewManager->isMimeSupported($mimeType)) {
85
			throw new NotFoundException();
86
		}
87
88
		$previewFolder = $this->getPreviewFolder($file);
89
90
		// Get the max preview and infer the max preview sizes from that
91
		$maxPreview = $this->getMaxPreview($previewFolder, $file, $mimeType);
92
		list($maxWidth, $maxHeight) = $this->getPreviewSize($maxPreview);
93
94
		// Calculate the preview size
95
		list($width, $height) = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
96
97
		// Try to get a cached preview. Else generate (and store) one
98
		try {
99
			$file = $this->getCachedPreview($previewFolder, $width, $height, $crop);
100
		} catch (NotFoundException $e) {
101
			$file = $this->generatePreview($previewFolder, $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight);
102
		}
103
104
		return $file;
105
	}
106
107
	/**
108
	 * @param ISimpleFolder $previewFolder
109
	 * @param File $file
110
	 * @param string $mimeType
111
	 * @return ISimpleFile
112
	 * @throws NotFoundException
113
	 */
114
	private function getMaxPreview(ISimpleFolder $previewFolder, File $file, $mimeType) {
115
		$nodes = $previewFolder->getDirectoryListing();
116
117
		foreach ($nodes as $node) {
118
			if (strpos($node->getName(), 'max')) {
119
				return $node;
120
			}
121
		}
122
123
		$previewProviders = $this->previewManager->getProviders();
124
		foreach ($previewProviders as $supportedMimeType => $providers) {
125
			if (!preg_match($supportedMimeType, $mimeType)) {
126
				continue;
127
			}
128
129
			foreach ($providers as $provider) {
130
				$provider = $this->helper->getProvider($provider);
131
				if (!($provider instanceof IProvider)) {
132
					continue;
133
				}
134
135
				$maxWidth = (int)$this->config->getSystemValue('preview_max_x', 2048);
136
				$maxHeight = (int)$this->config->getSystemValue('preview_max_y', 2048);
137
138
				$preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight);
139
140
				if (!($preview instanceof IImage)) {
141
					continue;
142
				}
143
144
				$path = strval($preview->width()) . '-' . strval($preview->height()) . '-max.png';
145
				$file = $previewFolder->newFile($path);
146
				$file->putContent($preview->data());
147
148
				return $file;
149
			}
150
		}
151
152
		throw new NotFoundException();
153
	}
154
155
	/**
156
	 * @param ISimpleFile $file
157
	 * @return int[]
158
	 */
159
	private function getPreviewSize(ISimpleFile $file) {
160
		$size = explode('-', $file->getName());
161
		return [(int)$size[0], (int)$size[1]];
162
	}
163
164
	/**
165
	 * @param int $width
166
	 * @param int $height
167
	 * @param bool $crop
168
	 * @return string
169
	 */
170
	private function generatePath($width, $height, $crop) {
171
		$path = strval($width) . '-' . strval($height);
172
		if ($crop) {
173
			$path .= '-crop';
174
		}
175
		$path .= '.png';
176
		return $path;
177
	}
178
179
180
181
	/**
182
	 * @param int $width
183
	 * @param int $height
184
	 * @param bool $crop
185
	 * @param string $mode
186
	 * @param int $maxWidth
187
	 * @param int $maxHeight
188
	 * @return int[]
189
	 */
190
	private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) {
191
192
		/*
193
		 * If we are not cropping we have to make sure the requested image
194
		 * respects the aspect ratio of the original.
195
		 */
196
		if (!$crop) {
197
			$ratio = $maxHeight / $maxWidth;
198
199
			if ($width === -1) {
200
				$width = $height / $ratio;
201
			}
202
			if ($height === -1) {
203
				$height = $width * $ratio;
204
			}
205
206
			$ratioH = $height / $maxHeight;
207
			$ratioW = $width / $maxWidth;
208
209
			/*
210
			 * Fill means that the $height and $width are the max
211
			 * Cover means min.
212
			 */
213
			if ($mode === IPreview::MODE_FILL) {
214
				if ($ratioH > $ratioW) {
215
					$height = $width * $ratio;
216
				} else {
217
					$width = $height / $ratio;
218
				}
219
			} else if ($mode === IPreview::MODE_COVER) {
220
				if ($ratioH > $ratioW) {
221
					$width = $height / $ratio;
222
				} else {
223
					$height = $width * $ratio;
224
				}
225
			}
226
		}
227
228
		if ($height !== $maxHeight && $width !== $maxWidth) {
229
			/*
230
			 * Scale to the nearest power of two
231
			 */
232
			$pow2height = pow(2, ceil(log($height) / log(2)));
233
			$pow2width = pow(2, ceil(log($width) / log(2)));
234
235
			$ratioH = $height / $pow2height;
236
			$ratioW = $width / $pow2width;
237
238
			if ($ratioH < $ratioW) {
239
				$width = $pow2width;
240
				$height = $height / $ratioW;
241
			} else {
242
				$height = $pow2height;
243
				$width = $width / $ratioH;
244
			}
245
		}
246
247
		/*
248
 		 * Make sure the requested height and width fall within the max
249
 		 * of the preview.
250
 		 */
251
		if ($height > $maxHeight) {
252
			$ratio = $height / $maxHeight;
253
			$height = $maxHeight;
254
			$width = $width / $ratio;
255
		}
256
		if ($width > $maxWidth) {
257
			$ratio = $width / $maxWidth;
258
			$width = $maxWidth;
259
			$height = $height / $ratio;
260
		}
261
262
		return [(int)round($width), (int)round($height)];
263
	}
264
265
	/**
266
	 * @param ISimpleFolder $previewFolder
267
	 * @param ISimpleFile $maxPreview
268
	 * @param int $width
269
	 * @param int $height
270
	 * @param bool $crop
271
	 * @param int $maxWidth
272
	 * @param int $maxHeight
273
	 * @return ISimpleFile
274
	 * @throws NotFoundException
275
	 */
276
	private function generatePreview(ISimpleFolder $previewFolder, ISimpleFile $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight) {
277
		$preview = $this->helper->getImage($maxPreview);
278
279
		if ($crop) {
280
			if ($height !== $preview->height() && $width !== $preview->width()) {
281
				//Resize
282
				$widthR = $preview->width() / $width;
283
				$heightR = $preview->height() / $height;
284
285
				if ($widthR > $heightR) {
286
					$scaleH = $height;
287
					$scaleW = $maxWidth / $heightR;
288
				} else {
289
					$scaleH = $maxHeight / $widthR;
290
					$scaleW = $width;
291
				}
292
				$preview->preciseResize(round($scaleW), round($scaleH));
293
			}
294
			$cropX = floor(abs($width - $preview->width()) * 0.5);
295
			$cropY = 0;
296
			$preview->crop($cropX, $cropY, $width, $height);
297
		} else {
298
			$preview->resize(max($width, $height));
299
		}
300
301
		$path = $this->generatePath($width, $height, $crop);
302
		$file = $previewFolder->newFile($path);
303
		$file->putContent($preview->data());
304
305
		return $file;
306
	}
307
308
	/**
309
	 * @param ISimpleFolder $previewFolder
310
	 * @param int $width
311
	 * @param int $height
312
	 * @param bool $crop
313
	 * @return ISimpleFile
314
	 *
315
	 * @throws NotFoundException
316
	 */
317
	private function getCachedPreview(ISimpleFolder $previewFolder, $width, $height, $crop) {
318
		$path = $this->generatePath($width, $height, $crop);
319
320
		return $previewFolder->getFile($path);
321
	}
322
323
	/**
324
	 * Get the specific preview folder for this file
325
	 *
326
	 * @param File $file
327
	 * @return ISimpleFolder
328
	 */
329
	private function getPreviewFolder(File $file) {
330
		try {
331
			$folder = $this->appData->getFolder($file->getId());
332
		} catch (NotFoundException $e) {
333
			$folder = $this->appData->newFolder($file->getId());
334
		}
335
336
		return $folder;
337
	}
338
}
339