Passed
Push — master ( b385ad...86a7d2 )
by smiley
02:48
created

Imagetiler::createTilesForZoom()   C

Complexity

Conditions 11
Paths 10

Size

Total Lines 66
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 66
rs 5.9447
c 0
b 0
f 0
cc 11
eloc 32
nc 10
nop 2

How to fix   Long Method    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\Traits\ContainerInterface;
16
use Imagick;
17
use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger};
18
19
class Imagetiler implements LoggerAwareInterface{
20
	use LoggerAwareTrait;
21
22
	/**
23
	 * @var string
24
	 */
25
	protected $ext;
26
27
	/**
28
	 * @var \chillerlan\Imagetiler\ImagetilerOptions
29
	 */
30
	protected $options;
31
32
	/**
33
	 * Imagetiler constructor.
34
	 *
35
	 * @param \chillerlan\Traits\ContainerInterface|null $options
36
	 * @param \Psr\Log\LoggerInterface|null              $logger
37
	 *
38
	 * @throws \chillerlan\Imagetiler\ImagetilerException
39
	 */
40
	public function __construct(ContainerInterface $options = null, LoggerInterface $logger = null){
41
42
		if(!extension_loaded('imagick')){
43
			throw new ImagetilerException('Imagick extension is not available');
44
		}
45
46
		$this->setOptions($options ?? new ImagetilerOptions);
47
		$this->setLogger($logger ?? new NullLogger);
48
	}
49
50
	/**
51
	 * @param \chillerlan\Traits\ContainerInterface $options
52
	 *
53
	 * @return \chillerlan\Imagetiler\Imagetiler
54
	 * @throws \chillerlan\Imagetiler\ImagetilerException
55
	 */
56
	public function setOptions(ContainerInterface $options):Imagetiler{
57
58
		$options->zoom_min = max(0, $options->zoom_min);
59
		$options->zoom_max = max(1, $options->zoom_max);
60
61
		if($options->zoom_normalize === null || $options->zoom_max < $options->zoom_normalize){
62
			$options->zoom_normalize = $options->zoom_max;
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
		$this->ext = $this->getExtension();
81
82
		return $this;
83
	}
84
85
	/**
86
	 * @param string $image_path
87
	 * @param string $out_path
88
	 *
89
	 * @return \chillerlan\Imagetiler\Imagetiler
90
	 * @throws \chillerlan\Imagetiler\ImagetilerException
91
	 */
92
	public function process(string $image_path, string $out_path):Imagetiler{
93
94
		if(!is_file($image_path) || !is_readable($image_path)){
95
			throw new ImagetilerException('cannot read image '.$image_path);
96
		}
97
98
		if(!is_dir($out_path)|| !is_writable($out_path)){
99
100
			if(!mkdir($out_path, 0755, true)){
101
				throw new ImagetilerException('output path is not writable');
102
			}
103
104
		}
105
106
		// prepare base images for each zoom level
107
		$this->prepareZoomBaseImages($image_path, $out_path);
108
109
		// create tiles for each zoom level
110
		for($i = $this->options->zoom_min; $i <= $this->options->zoom_max; $i++){
111
			$this->createTilesForZoom($out_path, $i);
112
		}
113
114
		// clean up base images
115
		if($this->options->clean_up){
116
			$this->removeZoomBaseImages($out_path);
117
		}
118
119
		return $this;
120
	}
121
122
	/**
123
	 * prepare each zoom lvl base images
124
	 *
125
	 * @param string $image_path
126
	 * @param string $out_path
127
	 */
128
	protected function prepareZoomBaseImages(string $image_path, string $out_path):void{
129
130
		//load main image
131
		$im = new Imagick($image_path);
132
		$im->setImageFormat($this->options->tile_format);
133
134
		//get image size
135
		$width  = $im->getimagewidth();
136
		$height = $im->getImageHeight();
137
138
		$this->logger->info('input image loaded: ['.$width.'x'.$height.'] '.$image_path);
139
140
		//prepare each zoom lvl base images
141
		$start = true;
142
		$il    = null;
143
144
		for($zoom = $this->options->zoom_max; $zoom >= $this->options->zoom_min; $zoom--){
145
			$base_image = $out_path.'/'.$zoom.'.'.$this->ext;
146
147
			//check if already exist
148
			if(!$this->options->overwrite_base_image && is_file($base_image)){
149
				$this->logger->info('base image for zoom level '.$zoom.' already exists: '.$base_image);
150
				continue;
151
			}
152
153
			[$w, $h] = $this->getSize($width, $height, $zoom);
154
155
			//fit main image to current zoom lvl
156
			$il = $start ? clone $im : $il;
157
158
			$this->options->fast_resize === true
159
				// scaleImage - works fast, but without any quality configuration
160
				? $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

160
				? $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...
161
				// resizeImage - works slower but offers better quality
162
				: $il->resizeImage($w, $h, $this->options->resize_filter, $this->options->resize_blur);
163
164
			//store
165
			$this->imageSave($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::imageSave() 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

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