Passed
Push — master ( c546ba...2b43ba )
by smiley
04:05
created

Imagetiler::getExtension()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

181
				? $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...
182
				// resizeImage - works slower but offers better quality
183
				: $il->resizeImage($w, $h, $this->options->resize_filter, $this->options->resize_blur);
184
185
			// save without optimizing
186
			$this->saveImage($il, $base_image, false);
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

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