Completed
Push — master ( abb00f...2065b2 )
by smiley
03:02
created

Imagetiler::prepareZoomBaseImages()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 43
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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

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