Completed
Push — master ( 7e9823...317cbb )
by smiley
08:54
created

Imagetiler   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 55
dl 0
loc 343
rs 6
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getExtension() 0 11 3
C createTilesForZoom() 0 58 9
C process() 0 53 14
C setOptions() 0 29 8
A getSize() 0 15 4
C prepareZoomBaseImages() 0 43 7
A __construct() 0 8 2
B saveImage() 0 20 6
A clearImage() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like Imagetiler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Imagetiler, and based on these observations, apply Extract Interface, too.

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\Traits\ContainerInterface;
16
use ImageOptimizer\Optimizer;
17
use ImageOptimizer\OptimizerFactory;
18
use Imagick;
19
use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger};
20
21
class Imagetiler implements LoggerAwareInterface{
22
	use LoggerAwareTrait;
23
24
	/**
25
	 * @var \chillerlan\Imagetiler\ImagetilerOptions
26
	 */
27
	protected $options;
28
29
	/**
30
	 * Imagetiler constructor.
31
	 *
32
	 * @param \chillerlan\Traits\ContainerInterface|null $options
33
	 * @param \Psr\Log\LoggerInterface|null              $logger
34
	 *
35
	 * @throws \chillerlan\Imagetiler\ImagetilerException
36
	 */
37
	public function __construct(ContainerInterface $options = null, LoggerInterface $logger = null){
38
39
		if(!extension_loaded('imagick')){
40
			throw new ImagetilerException('Imagick extension is not available');
41
		}
42
43
		$this->setOptions($options ?? new ImagetilerOptions);
44
		$this->setLogger($logger ?? new NullLogger);
45
	}
46
47
	/**
48
	 * @param \chillerlan\Traits\ContainerInterface $options
49
	 *
50
	 * @return \chillerlan\Imagetiler\Imagetiler
51
	 * @throws \chillerlan\Imagetiler\ImagetilerException
52
	 */
53
	public function setOptions(ContainerInterface $options):Imagetiler{
54
		$options->zoom_min = max(0, $options->zoom_min);
55
		$options->zoom_max = max(1, $options->zoom_max);
56
57
		if($options->zoom_normalize === null || $options->zoom_max < $options->zoom_normalize){
58
			$options->zoom_normalize = $options->zoom_max;
59
		}
60
61
		if($options->tile_ext === null){
62
			$options->tile_ext = $this->getExtension($options->tile_format);
63
		}
64
65
		$this->options = $options;
0 ignored issues
show
Documentation Bug introduced by
$options is of type chillerlan\Traits\ContainerInterface, 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...
66
67
		if(ini_set('memory_limit', $this->options->memory_limit) === false){
68
			throw new ImagetilerException('could not alter ini settings');
69
		}
70
71
		if(ini_get('memory_limit') !== (string)$this->options->memory_limit){
72
			throw new ImagetilerException('ini settings differ from options');
73
		}
74
75
		if($this->options->imagick_tmp !== null && is_dir($this->options->imagick_tmp)){
76
			apache_setenv('MAGICK_TEMPORARY_PATH', $this->options->imagick_tmp);
77
			putenv('MAGICK_TEMPORARY_PATH='.$this->options->imagick_tmp);
78
		}
79
80
81
		return $this;
82
	}
83
84
	/**
85
	 * @param string $image_path
86
	 * @param string $out_path
87
	 *
88
	 * @return \chillerlan\Imagetiler\Imagetiler
89
	 * @throws \chillerlan\Imagetiler\ImagetilerException
90
	 */
91
	public function process(string $image_path, string $out_path):Imagetiler{
92
93
		if(!is_file($image_path) || !is_readable($image_path)){
94
			throw new ImagetilerException('cannot read image '.$image_path);
95
		}
96
97
		if(!is_dir($out_path)|| !is_writable($out_path)){
98
99
			if(!mkdir($out_path, 0755, true)){
100
				throw new ImagetilerException('output path is not writable');
101
			}
102
103
		}
104
105
		// load the optional image optimizer
106
		$optimizer = null;
107
108
		if($this->options->optimize_output){
109
			$optimizer = (new OptimizerFactory($this->options->optimizer_settings, $this->logger))->get();
110
		}
111
112
		// prepare the zoom base images
113
		$this->prepareZoomBaseImages($image_path, $out_path);
114
115
		// create the tiles
116
		for($zoom = $this->options->zoom_min; $zoom <= $this->options->zoom_max; $zoom++){
117
118
			$base_image = $out_path.'/'.$zoom.'.'.$this->options->tile_ext;
119
120
			//load image
121
			if(!is_file($base_image) || !is_readable($base_image)){
122
				throw new ImagetilerException('cannot read base image '.$base_image.' for zoom '.$zoom);
123
			}
124
125
			$this->createTilesForZoom(new Imagick($base_image), $zoom, $out_path, $optimizer);
126
		}
127
128
		// clean up base images
129
		if($this->options->clean_up){
130
131
			for($zoom = $this->options->zoom_min; $zoom <= $this->options->zoom_max; $zoom++){
132
				$lvl_file = $out_path.'/'.$zoom.'.'.$this->options->tile_ext;
133
134
				if(is_file($lvl_file)){
135
					if(unlink($lvl_file)){
136
						$this->logger->info('deleted base image for zoom level '.$zoom.': '.$lvl_file);
137
					}
138
				}
139
			}
140
141
		}
142
143
		return $this;
144
	}
145
146
	/**
147
	 * prepare base images for each zoom level
148
	 *
149
	 * @param string $image_path
150
	 * @param string $out_path
151
	 */
152
	protected function prepareZoomBaseImages(string $image_path, string $out_path):void{
153
		$im = new Imagick($image_path);
154
		$im->setImageFormat($this->options->tile_format);
155
156
		$width  = $im->getimagewidth();
157
		$height = $im->getImageHeight();
158
159
		$this->logger->info('input image loaded: ['.$width.'x'.$height.'] '.$image_path);
160
161
		$start = true;
162
		$il    = null;
163
164
		for($zoom = $this->options->zoom_max; $zoom >= $this->options->zoom_min; $zoom--){
165
			$base_image = $out_path.'/'.$zoom.'.'.$this->options->tile_ext;
166
167
			// check if the base image already exists
168
			if(!$this->options->overwrite_base_image && is_file($base_image)){
169
				$this->logger->info('base image for zoom level '.$zoom.' already exists: '.$base_image);
170
				continue;
171
			}
172
173
			[$w, $h] = $this->getSize($width, $height, $zoom);
174
175
			// fit main image to current zoom level
176
			$il = $start ? clone $im : $il;
177
178
			$this->options->fast_resize === true
179
				// scaleImage - works fast, but without any quality configuration
180
				? $il->scaleImage($w, $h, true)
0 ignored issues
show
Bug introduced by
The method scaleImage() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

180
				? $il->/** @scrutinizer ignore-call */ scaleImage($w, $h, true)

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
181
				// resizeImage - works slower but offers better quality
182
				: $il->resizeImage($w, $h, $this->options->resize_filter, $this->options->resize_blur);
183
184
			$this->saveImage($il, $base_image);
0 ignored issues
show
Bug introduced by
It seems like $il can also be of type null; however, parameter $image of chillerlan\Imagetiler\Imagetiler::saveImage() does only seem to accept Imagick, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

184
			$this->saveImage(/** @scrutinizer ignore-type */ $il, $base_image);
Loading history...
185
186
			if($start){
187
				$this->clearImage($im);
188
			}
189
190
			$start = false;
191
			$this->logger->info('created image for zoom level '.$zoom.' ['.$w.'x'.$h.'] '.$base_image);
192
		}
193
194
		$this->clearImage($il);
195
	}
196
197
	/**
198
	 * create tiles for each zoom level
199
	 *
200
	 * @param \Imagick                       $im
201
	 * @param int                            $zoom
202
	 * @param string                         $out_path
203
	 * @param \ImageOptimizer\Optimizer|null $optimizer
204
	 *
205
	 * @return void
206
	 */
207
	protected function createTilesForZoom(Imagick $im, int $zoom, string $out_path, Optimizer $optimizer = null):void{
208
		$w = $im->getimagewidth();
209
		$h = $im->getImageHeight();
210
211
		$ts = $this->options->tile_size;
212
213
		$x = (int)ceil($w / $ts);
214
		$y = (int)ceil($h / $ts);
215
216
		// width
217
		for($ix = 0; $ix < $x; $ix++){
218
			$cx = $ix * $ts;
219
220
			// create a stripe tile_size * height
221
			$ci = clone $im;
222
			$ci->cropImage($ts, $h, $cx, 0);
223
224
			// height
225
			for($iy = 0; $iy < $y; $iy++){
226
				$tile = $out_path.'/'.sprintf($this->options->store_structure, $zoom, $ix, $iy).'.'.$this->options->tile_ext;
227
228
				// check if tile already exists
229
				if(!$this->options->overwrite_tile_image && is_file($tile)){
230
					$this->logger->info('tile '.$zoom.'/'.$x.'/'.$y.' already exists: '.$tile);
231
232
					continue;
233
				}
234
235
				// cut the stripe into pieces of height = tile_size
236
				$cy = $this->options->tms
237
					? $h - ($iy + 1) * $ts
238
					: $iy * $ts;
239
240
				$ti = clone $ci;
241
				$ti->setImagePage(0, 0, 0, 0);
242
				$ti->cropImage($ts, $ts, 0, $cy);
243
244
				// check if the current tile is smaller than the tile size (leftover edges on the input image)
245
				if($ti->getImageWidth() < $ts || $ti->getimageheight() < $ts){
246
247
					$th = $this->options->tms
248
						? $im->getImageHeight() - $ts
249
						: 0;
250
251
					$ti->setImageBackgroundColor($this->options->fill_color);
252
					$ti->extentImage($ts, $ts, 0, $th);
253
				}
254
255
				$this->saveImage($ti, $tile, $optimizer);
256
				$this->clearImage($ti);
257
			}
258
259
			$this->clearImage($ci);
260
			$this->logger->info('created column '.($ix+1).', x = '.$cx);
261
		}
262
263
		$this->clearImage($im);
264
		$this->logger->info('created tiles for zoom level: '.$zoom.', '.$y.' tile(s) per column');
265
	}
266
267
	/**
268
	 * save image in to destination
269
	 *
270
	 * @param Imagick                   $image
271
	 * @param string                    $dest full path with file name
272
	 * @param \ImageOptimizer\Optimizer $optimizer
273
	 *
274
	 * @return void
275
	 * @throws \chillerlan\Imagetiler\ImagetilerException
276
	 */
277
	protected function saveImage(Imagick $image, string $dest, Optimizer $optimizer = null):void{
278
		$dir = dirname($dest);
279
280
		if(!is_dir($dir)){
281
			if(!mkdir($dir, 0755, true)){
282
				throw new ImagetilerException('cannot crate folder '.$dir);
283
			}
284
		}
285
286
		if($this->options->tile_format === 'jpeg'){
287
			$image->setCompression(Imagick::COMPRESSION_JPEG);
288
			$image->setCompressionQuality($this->options->quality_jpeg);
289
		}
290
291
		if(!$image->writeImage($dest)){
292
			throw new ImagetilerException('cannot save image '.$dest);
293
		}
294
295
		if($optimizer instanceof Optimizer){
296
			$optimizer->optimize($dest);
297
		}
298
299
	}
300
301
	/**
302
	 * free resources, destroy imagick object
303
	 *
304
	 * @param \Imagick|null $image
305
	 *
306
	 * @return bool
307
	 */
308
	protected function clearImage(Imagick $image = null):bool{
309
310
		if($image instanceof Imagick){
311
			$image->clear();
312
313
			return $image->destroy();
314
		}
315
316
		return false;
317
	}
318
319
	/**
320
	 * calculate the image size for the given zoom level
321
	 *
322
	 * @param int $width
323
	 * @param int $height
324
	 * @param int $zoom
325
	 *
326
	 * @return int[]
327
	 */
328
	protected function getSize(int $width, int $height, int $zoom):array{
329
330
		if($this->options->zoom_max > $this->options->zoom_normalize && $zoom > $this->options->zoom_normalize){
331
			$zd = 2 ** ($zoom - $this->options->zoom_normalize);
332
333
			return [$zd * $width, $zd * $height];
334
		}
335
336
		if($zoom < $this->options->zoom_normalize){
337
			$zd = 2 ** ($this->options->zoom_normalize - $zoom);
338
339
			return [(int)round($width / $zd), (int)round($height / $zd)];
340
		}
341
342
		return [$width, $height];
343
	}
344
345
	/**
346
	 * return file extension depend of given format
347
	 *
348
	 * @param string $format
349
	 *
350
	 * @return string
351
	 * @throws \chillerlan\Imagetiler\ImagetilerException
352
	 */
353
	protected function getExtension(string $format):string{
354
355
		if(in_array($format, ['jpeg', 'jp2', 'jpc', 'jxr',], true)){
356
			return 'jpg';
357
		}
358
359
		if(in_array($format, ['png', 'png00', 'png8', 'png24', 'png32', 'png64',], true)){
360
			return 'png';
361
		}
362
363
		throw new ImagetilerException('invalid file format');
364
	}
365
366
}
367