Passed
Push — master ( 5e97e8...d85193 )
by smiley
02:34
created

src/Imagetiler.php (1 issue)

Severity
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 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\Traits\ContainerInterface|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(ContainerInterface $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\Traits\ContainerInterface $options
58
	 *
59
	 * @return \chillerlan\Imagetiler\Imagetiler
60
	 * @throws \chillerlan\Imagetiler\ImagetilerException
61
	 */
62
	public function setOptions(ContainerInterface $options):Imagetiler{
63
		$options->zoom_min = max(0, $options->zoom_min);
64
		$options->zoom_max = max(1, $options->zoom_max);
65
66
		if($options->zoom_normalize === null || $options->zoom_max < $options->zoom_normalize){
67
			$options->zoom_normalize = $options->zoom_max;
68
		}
69
70
		if($options->tile_ext === null){
71
			$options->tile_ext = $this->getExtension($options->tile_format);
72
		}
73
74
		$this->options = $options;
75
76
		if(ini_set('memory_limit', $this->options->memory_limit) === false){
77
			throw new ImagetilerException('could not alter ini settings');
78
		}
79
80
		if(ini_get('memory_limit') !== (string)$this->options->memory_limit){
81
			throw new ImagetilerException('ini settings differ from options');
82
		}
83
84
		if($this->options->imagick_tmp !== null && is_dir($this->options->imagick_tmp)){
85
			apache_setenv('MAGICK_TEMPORARY_PATH', $this->options->imagick_tmp);
86
			putenv('MAGICK_TEMPORARY_PATH='.$this->options->imagick_tmp);
87
		}
88
89
90
		return $this;
91
	}
92
93
	/**
94
	 * @param \ImageOptimizer\Optimizer $optimizer
95
	 *
96
	 * @return \chillerlan\Imagetiler\Imagetiler
97
	 */
98
	public function setOptimizer(Optimizer $optimizer):Imagetiler{
99
		$this->optimizer = $optimizer;
100
101
		return $this;
102
	}
103
104
	/**
105
	 * @param string $image_path
106
	 * @param string $out_path
107
	 *
108
	 * @return \chillerlan\Imagetiler\Imagetiler
109
	 * @throws \chillerlan\Imagetiler\ImagetilerException
110
	 */
111
	public function process(string $image_path, string $out_path):Imagetiler{
112
113
		if(!is_file($image_path) || !is_readable($image_path)){
114
			throw new ImagetilerException('cannot read image '.$image_path);
115
		}
116
117
		if(!is_dir($out_path)|| !is_writable($out_path)){
118
119
			if(!mkdir($out_path, 0755, true)){
120
				throw new ImagetilerException('output path is not writable');
121
			}
122
123
		}
124
125
		// prepare the zoom base images
126
		$this->prepareZoomBaseImages($image_path, $out_path);
127
128
		// create the tiles
129
		for($zoom = $this->options->zoom_min; $zoom <= $this->options->zoom_max; $zoom++){
130
131
			$base_image = $out_path.'/'.$zoom.'.'.$this->options->tile_ext;
132
133
			//load image
134
			if(!is_file($base_image) || !is_readable($base_image)){
135
				throw new ImagetilerException('cannot read base image '.$base_image.' for zoom '.$zoom);
136
			}
137
138
			$this->createTilesForZoom(new Imagick($base_image), $zoom, $out_path);
139
		}
140
141
		// clean up base images
142
		if($this->options->clean_up){
143
144
			for($zoom = $this->options->zoom_min; $zoom <= $this->options->zoom_max; $zoom++){
145
				$lvl_file = $out_path.'/'.$zoom.'.'.$this->options->tile_ext;
146
147
				if(is_file($lvl_file)){
148
					if(unlink($lvl_file)){
149
						$this->logger->info('deleted base image for zoom level '.$zoom.': '.$lvl_file);
150
					}
151
				}
152
			}
153
154
		}
155
156
		return $this;
157
	}
158
159
	/**
160
	 * prepare base images for each zoom level
161
	 *
162
	 * @param string $image_path
163
	 * @param string $out_path
164
	 */
165
	protected function prepareZoomBaseImages(string $image_path, string $out_path):void{
166
		$im = new Imagick($image_path);
167
		$im->setImageFormat($this->options->tile_format);
168
169
		$width  = $im->getimagewidth();
170
		$height = $im->getImageHeight();
171
172
		$this->logger->info('input image loaded: ['.$width.'x'.$height.'] '.$image_path);
173
174
		$start = true;
175
		$il    = null;
176
177
		for($zoom = $this->options->zoom_max; $zoom >= $this->options->zoom_min; $zoom--){
178
			$base_image = $out_path.'/'.$zoom.'.'.$this->options->tile_ext;
179
180
			// check if the base image already exists
181
			if(!$this->options->overwrite_base_image && is_file($base_image)){
182
				$this->logger->info('base image for zoom level '.$zoom.' already exists: '.$base_image);
183
				continue;
184
			}
185
186
			[$w, $h] = $this->getSize($width, $height, $zoom);
187
188
			// fit main image to current zoom level
189
			$il = $start ? clone $im : $il;
190
191
			$this->options->fast_resize === true
192
				// scaleImage - works fast, but without any quality configuration
193
				? $il->scaleImage($w, $h, true)
194
				// resizeImage - works slower but offers better quality
195
				: $il->resizeImage($w, $h, $this->options->resize_filter, $this->options->resize_blur);
196
197
			// save without optimizing
198
			$this->saveImage($il, $base_image, false);
199
200
			if($start){
201
				$this->clearImage($im);
202
			}
203
204
			$start = false;
205
			$this->logger->info('created image for zoom level '.$zoom.' ['.$w.'x'.$h.'] '.$base_image);
206
		}
207
208
		$this->clearImage($il);
209
	}
210
211
	/**
212
	 * create tiles for each zoom level
213
	 *
214
	 * @param \Imagick                       $im
215
	 * @param int                            $zoom
216
	 * @param string                         $out_path
217
	 * @param \ImageOptimizer\Optimizer|null $optimizer
218
	 *
219
	 * @return void
220
	 */
221
	protected function createTilesForZoom(Imagick $im, int $zoom, string $out_path, Optimizer $optimizer = null):void{
0 ignored issues
show
The parameter $optimizer is not used and could be removed. ( Ignorable by Annotation )

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

221
	protected function createTilesForZoom(Imagick $im, int $zoom, string $out_path, /** @scrutinizer ignore-unused */ Optimizer $optimizer = null):void{

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
222
		$w = $im->getimagewidth();
223
		$h = $im->getImageHeight();
224
225
		$ts = $this->options->tile_size;
226
227
		$x = (int)ceil($w / $ts);
228
		$y = (int)ceil($h / $ts);
229
230
		// width
231
		for($ix = 0; $ix < $x; $ix++){
232
			$cx = $ix * $ts;
233
234
			// create a stripe tile_size * height
235
			$ci = clone $im;
236
			$ci->cropImage($ts, $h, $cx, 0);
237
238
			// height
239
			for($iy = 0; $iy < $y; $iy++){
240
				$tile = $out_path.'/'.sprintf($this->options->store_structure, $zoom, $ix, $iy).'.'.$this->options->tile_ext;
241
242
				// check if tile already exists
243
				if(!$this->options->overwrite_tile_image && is_file($tile)){
244
					$this->logger->info('tile '.$zoom.'/'.$x.'/'.$y.' already exists: '.$tile);
245
246
					continue;
247
				}
248
249
				// cut the stripe into pieces of height = tile_size
250
				$cy = $this->options->tms
251
					? $h - ($iy + 1) * $ts
252
					: $iy * $ts;
253
254
				$ti = clone $ci;
255
				$ti->setImagePage(0, 0, 0, 0);
256
				$ti->cropImage($ts, $ts, 0, $cy);
257
258
				// check if the current tile is smaller than the tile size (leftover edges on the input image)
259
				if($ti->getImageWidth() < $ts || $ti->getimageheight() < $ts){
260
261
					$th = $this->options->tms
262
						? $im->getImageHeight() - $ts
263
						: 0;
264
265
					$ti->setImageBackgroundColor($this->options->fill_color);
266
					$ti->extentImage($ts, $ts, 0, $th);
267
				}
268
269
				$this->saveImage($ti, $tile, true);
270
				$this->clearImage($ti);
271
			}
272
273
			$this->clearImage($ci);
274
			$this->logger->info('created column '.($ix+1).', x = '.$cx);
275
		}
276
277
		$this->clearImage($im);
278
		$this->logger->info('created tiles for zoom level: '.$zoom.', '.$y.' tile(s) per column');
279
	}
280
281
	/**
282
	 * save image in to destination
283
	 *
284
	 * @param Imagick $image
285
	 * @param string  $dest full path with file name
286
	 * @param bool    $optimize
287
	 *
288
	 * @return void
289
	 * @throws \chillerlan\Imagetiler\ImagetilerException
290
	 */
291
	protected function saveImage(Imagick $image, string $dest, bool $optimize):void{
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
		if($this->options->tile_format === 'jpeg'){
301
			$image->setCompression(Imagick::COMPRESSION_JPEG);
302
			$image->setCompressionQuality($this->options->quality_jpeg);
303
		}
304
305
		if(!$image->writeImage($dest)){
306
			throw new ImagetilerException('cannot save image '.$dest);
307
		}
308
309
		if($this->options->optimize_output && $optimize && $this->optimizer instanceof Optimizer){
310
			$this->optimizer->optimize($dest);
311
		}
312
313
	}
314
315
	/**
316
	 * free resources, destroy imagick object
317
	 *
318
	 * @param \Imagick|null $image
319
	 *
320
	 * @return bool
321
	 */
322
	protected function clearImage(Imagick $image = null):bool{
323
324
		if($image instanceof Imagick){
325
			$image->clear();
326
327
			return $image->destroy();
328
		}
329
330
		return false;
331
	}
332
333
	/**
334
	 * calculate the image size for the given zoom level
335
	 *
336
	 * @param int $width
337
	 * @param int $height
338
	 * @param int $zoom
339
	 *
340
	 * @return int[]
341
	 */
342
	protected function getSize(int $width, int $height, int $zoom):array{
343
344
		if($this->options->zoom_max > $this->options->zoom_normalize && $zoom > $this->options->zoom_normalize){
345
			$zd = 2 ** ($zoom - $this->options->zoom_normalize);
346
347
			return [$zd * $width, $zd * $height];
348
		}
349
350
		if($zoom < $this->options->zoom_normalize){
351
			$zd = 2 ** ($this->options->zoom_normalize - $zoom);
352
353
			return [(int)round($width / $zd), (int)round($height / $zd)];
354
		}
355
356
		return [$width, $height];
357
	}
358
359
	/**
360
	 * return file extension depend of given format
361
	 *
362
	 * @param string $format
363
	 *
364
	 * @return string
365
	 * @throws \chillerlan\Imagetiler\ImagetilerException
366
	 */
367
	protected function getExtension(string $format):string{
368
369
		if(in_array($format, ['jpeg', 'jp2', 'jpc', 'jxr',], true)){
370
			return 'jpg';
371
		}
372
373
		if(in_array($format, ['png', 'png00', 'png8', 'png24', 'png32', 'png64',], true)){
374
			return 'png';
375
		}
376
377
		throw new ImagetilerException('invalid file format');
378
	}
379
380
}
381