Passed
Push — master ( 06c932...7812b7 )
by smiley
03:13
created

Imagetiler::process()   C

Complexity

Conditions 14
Paths 14

Size

Total Lines 50
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 20
c 3
b 0
f 0
dl 0
loc 50
rs 6.2666
cc 14
nc 14
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Class Imagetiler
4
 *
5
 * @filesource   Imagetiler.php
6
 * @created      20.06.2018
7
 * @package      chillerlan\Imagetiler
8
 * @author       smiley <[email protected]>
9
 * @copyright    2018 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\Imagetiler;
14
15
use chillerlan\Settings\SettingsContainerInterface;
16
use ImageOptimizer\Optimizer;
17
use Imagick;
18
use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger};
19
20
use function ceil, dirname, extension_loaded, function_exists, ini_get, ini_set, is_dir,
21
	is_file, is_readable, is_writable, mkdir, putenv, round, sprintf, unlink;
22
23
class Imagetiler implements LoggerAwareInterface{
24
	use LoggerAwareTrait;
25
26
	/**
27
	 * @var \chillerlan\Imagetiler\ImagetilerOptions
28
	 */
29
	protected $options;
30
31
	/**
32
	 * @var \ImageOptimizer\Optimizer
33
	 */
34
	protected $optimizer;
35
36
	/**
37
	 * Imagetiler constructor.
38
	 *
39
	 * @param \chillerlan\Settings\SettingsContainerInterface|null $options
40
	 * @param \ImageOptimizer\Optimizer                            $optimizer
41
	 * @param \Psr\Log\LoggerInterface|null                        $logger
42
	 *
43
	 * @throws \chillerlan\Imagetiler\ImagetilerException
44
	 */
45
	public function __construct(SettingsContainerInterface $options = null, Optimizer $optimizer = null, LoggerInterface $logger = null){
46
47
		if(!extension_loaded('imagick')){
48
			throw new ImagetilerException('Imagick extension is not available');
49
		}
50
51
		$this->setOptions($options ?? new ImagetilerOptions);
52
		$this->setLogger($logger ?? new NullLogger);
53
54
		if($optimizer instanceof Optimizer){
55
			$this->setOptimizer($optimizer);
56
		}
57
	}
58
59
	/**
60
	 * @param \chillerlan\Settings\SettingsContainerInterface $options
61
	 *
62
	 * @return \chillerlan\Imagetiler\Imagetiler
63
	 * @throws \chillerlan\Imagetiler\ImagetilerException
64
	 */
65
	public function setOptions(SettingsContainerInterface $options):Imagetiler{
66
		$this->options = $options;
0 ignored issues
show
Documentation Bug introduced by
$options is of type chillerlan\Settings\SettingsContainerInterface, but the property $options was declared to be of type chillerlan\Imagetiler\ImagetilerOptions. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
67
68
		if(ini_set('memory_limit', $this->options->memory_limit) === false){
69
			throw new ImagetilerException('could not alter ini settings');
70
		}
71
72
		if(ini_get('memory_limit') !== (string)$this->options->memory_limit){
73
			throw new ImagetilerException('ini settings differ from options');
74
		}
75
76
		return $this;
77
	}
78
79
	/**
80
	 * @param \ImageOptimizer\Optimizer $optimizer
81
	 *
82
	 * @return \chillerlan\Imagetiler\Imagetiler
83
	 */
84
	public function setOptimizer(Optimizer $optimizer):Imagetiler{
85
		$this->optimizer = $optimizer;
86
87
		return $this;
88
	}
89
90
	/**
91
	 * @param string $image_path
92
	 * @param string $out_path
93
	 *
94
	 * @return \chillerlan\Imagetiler\Imagetiler
95
	 * @throws \chillerlan\Imagetiler\ImagetilerException
96
	 */
97
	public function process(string $image_path, string $out_path):Imagetiler{
98
99
		if(!is_file($image_path) || !is_readable($image_path)){
100
			throw new ImagetilerException('cannot read image '.$image_path);
101
		}
102
103
		if(!is_dir($out_path) || !is_writable($out_path)){
104
105
			if(!mkdir($out_path, 0755, true)){
106
				throw new ImagetilerException('output path is not writable');
107
			}
108
109
		}
110
111
		$this->logger->info('processing image: '.$image_path.', out path: '.$out_path);
112
113
		// prepare the zoom base images
114
		$base_images = $this->prepareZoomBaseImages($image_path, $out_path);
115
116
		if($this->options->no_temp_baseimages === true){
117
			return $this;
118
		}
119
120
		// create the tiles
121
		foreach($base_images as $zoom => $base_image){
122
123
			//load image
124
			if(!is_file($base_image) || !is_readable($base_image)){
125
				throw new ImagetilerException('cannot read base image '.$base_image.' for zoom '.$zoom);
126
			}
127
128
			$this->createTilesForZoom(new Imagick($base_image), $zoom, $out_path);
129
		}
130
131
		// clean up base images
132
		if($this->options->clean_up){
133
134
			for($zoom = $this->options->zoom_min; $zoom <= $this->options->zoom_max; $zoom++){
135
				$lvl_file = $out_path.'/'.$zoom.'.'.$this->options->tile_ext;
136
137
				if(is_file($lvl_file)){
138
					if(unlink($lvl_file)){
139
						$this->logger->info('deleted base image for zoom level '.$zoom.': '.$lvl_file);
140
					}
141
				}
142
			}
143
144
		}
145
146
		return $this;
147
	}
148
149
	/**
150
	 * prepare base images for each zoom level
151
	 *
152
	 * @param string $image_path
153
	 * @param string $out_path
154
	 *
155
	 * @return array
156
	 */
157
	protected function prepareZoomBaseImages(string $image_path, string $out_path):array{
158
		$base_images = [];
159
160
		// create base image file names
161
		for($zoom = $this->options->zoom_max; $zoom >= $this->options->zoom_min; $zoom--){
162
			$base_image = $out_path.'/'.$zoom.'.'.$this->options->tile_ext;
163
			// check if the base image already exists
164
			if(!$this->options->overwrite_base_image && is_file($base_image)){
165
				$this->logger->info('base image for zoom level '.$zoom.' already exists: '.$base_image);
166
167
				continue;
168
			}
169
170
			$base_images[$zoom] = $base_image;
171
		}
172
173
		if(empty($base_images)){
174
			return [];
175
		}
176
177
		$im = new Imagick($image_path);
178
		$im->setColorspace(Imagick::COLORSPACE_SRGB);
179
		$im->setImageFormat($this->options->tile_format);
180
181
		$width  = $im->getimagewidth();
182
		$height = $im->getImageHeight();
183
184
		$this->logger->info('input image loaded: ['.$width.'x'.$height.'] '.$image_path);
185
186
		foreach($base_images as $zoom => $base_image){
187
			[$w, $h] = $this->getSize($width, $height, $zoom);
188
189
			// clone the original image and fit it to the current zoom level
190
			$il = clone $im;
191
192
			if($zoom > $this->options->zoom_normalize){
193
				$this->scale($il, $w, $h, $this->options->fast_resize_upsample, $this->options->resize_filter_upsample, $this->options->resize_blur_upsample);
194
			}
195
			elseif($zoom < $this->options->zoom_normalize){
196
				$this->scale($il, $w, $h, $this->options->fast_resize_downsample, $this->options->resize_filter_downsample, $this->options->resize_blur_downsample);
197
			}
198
199
			$this->options->no_temp_baseimages === false
200
				? $this->saveImage($il, $base_image, false)
201
				: $this->createTilesForZoom($il, $zoom, $out_path);
202
203
			$this->clearImage($il);
204
205
			$this->logger->info('created image for zoom level '.$zoom.' ['.$w.'x'.$h.'] '.$base_image);
206
		}
207
208
		$this->clearImage($im);
209
210
		return $base_images;
211
	}
212
213
	/**
214
	 * @param \Imagick $im
215
	 * @param int      $w
216
	 * @param int      $h
217
	 * @param bool     $fast
218
	 * @param int      $filter
219
	 * @param float    $blur
220
	 *
221
	 * @return void
222
	 */
223
	protected function scale(Imagick $im, int $w, int $h, bool $fast, int $filter, float $blur):void{
224
		$fast === true
225
			// scaleImage - works fast, but without any quality configuration
226
			? $im->scaleImage($w, $h, true)
227
			// resizeImage - works slower but offers better quality
228
			: $im->resizeImage($w, $h, $filter, $blur);
229
	}
230
231
	/**
232
	 * create tiles for each zoom level
233
	 *
234
	 * @param \Imagick $im
235
	 * @param int      $zoom
236
	 * @param string   $out_path
237
	 *
238
	 * @return void
239
	 */
240
	protected function createTilesForZoom(Imagick $im, int $zoom, string $out_path):void{
241
		$im->setColorspace(Imagick::COLORSPACE_SRGB);
242
		$ts = $this->options->tile_size;
243
		$h  = $im->getImageHeight();
244
		$x  = (int)ceil($im->getimagewidth() / $ts);
245
		$y  = (int)ceil($h / $ts);
246
247
		// width
248
		for($ix = 0; $ix < $x; $ix++){
249
			$cx = $ix * $ts;
250
251
			// create a stripe tile_size * height
252
			$ci = clone $im;
253
			$ci->cropImage($ts, $h, $cx, 0);
254
255
			// height
256
			for($iy = 0; $iy < $y; $iy++){
257
				$tile = $out_path.'/'.sprintf($this->options->store_structure, $zoom, $ix, $iy).'.'.$this->options->tile_ext;
258
259
				// check if tile already exists
260
				if(!$this->options->overwrite_tile_image && is_file($tile)){
261
					$this->logger->info('tile '.$zoom.'/'.$ix.'/'.$iy.' already exists: '.$tile);
262
263
					continue;
264
				}
265
266
				// cut the stripe into pieces of height = tile_size
267
				$cy = $this->options->tms
268
					? $h - ($iy + 1) * $ts
269
					: $iy * $ts;
270
271
				$ti = clone $ci;
272
				$ti->setImagePage(0, 0, 0, 0);
273
				$ti->cropImage($ts, $ts, 0, $cy);
274
275
				// check if the current tile is smaller than the tile size (leftover edges on the input image)
276
				if($ti->getImageWidth() < $ts || $ti->getimageheight() < $ts){
277
278
					$th = $this->options->tms ? $h - $ts : 0;
279
280
					$ti->setImageBackgroundColor($this->options->fill_color);
281
					$ti->extentImage($ts, $ts, 0, $th);
282
				}
283
284
				$this->saveImage($ti, $tile, true);
285
				$this->clearImage($ti);
286
			}
287
288
			$this->clearImage($ci);
289
			$this->logger->info('created column '.$ix.', zoom = '.$zoom.', x = '.$cx);
290
		}
291
292
		$this->clearImage($im);
293
		$this->logger->info('created tiles for zoom level: '.$zoom.', '.$x.' columns, '.$y.' tile(s) per column');
294
	}
295
296
	/**
297
	 * save image in to destination
298
	 *
299
	 * @param Imagick $image
300
	 * @param string  $dest full path with file name
301
	 * @param bool    $optimize
302
	 *
303
	 * @return void
304
	 * @throws \chillerlan\Imagetiler\ImagetilerException
305
	 */
306
	protected function saveImage(Imagick $image, string $dest, bool $optimize):void{
307
		$dir = dirname($dest);
308
309
		if(!is_dir($dir)){
310
			if(!mkdir($dir, 0755, true)){
311
				throw new ImagetilerException('cannot create folder '.$dir);
312
			}
313
		}
314
315
		if($this->options->tile_format === 'jpeg'){
316
			$image->setCompression(Imagick::COMPRESSION_JPEG2000);
317
			$image->setCompressionQuality($this->options->quality_jpeg);
318
		}
319
320
		if(!$image->writeImage($dest)){
321
			throw new ImagetilerException('cannot save image '.$dest);
322
		}
323
324
		if($this->options->optimize_output && $optimize && $this->optimizer instanceof Optimizer){
325
			$this->optimizer->optimize($dest);
326
		}
327
328
	}
329
330
	/**
331
	 * free resources, destroy imagick object
332
	 *
333
	 * @param \Imagick|null $image
334
	 *
335
	 * @return bool
336
	 */
337
	protected function clearImage(Imagick $image = null):bool{
338
339
		if($image instanceof Imagick){
340
			$image->clear();
341
342
			return $image->destroy();
343
		}
344
345
		return false;
346
	}
347
348
	/**
349
	 * calculate the image size for the given zoom level
350
	 *
351
	 * @param int $width
352
	 * @param int $height
353
	 * @param int $zoom
354
	 *
355
	 * @return int[]
356
	 */
357
	protected function getSize(int $width, int $height, int $zoom):array{
358
		$zoom_normalize = $this->options->zoom_normalize ?? $this->options->zoom_max;
359
360
		if($this->options->zoom_max > $zoom_normalize && $zoom > $zoom_normalize){
361
			$zd = 2 ** ($zoom - $zoom_normalize);
362
363
			return [$zd * $width, $zd * $height];
364
		}
365
366
		if($zoom < $zoom_normalize){
367
			$zd = 2 ** ($zoom_normalize - $zoom);
368
369
			return [(int)round($width / $zd), (int)round($height / $zd)];
370
		}
371
372
		return [$width, $height];
373
	}
374
375
}
376