1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Resize and crop images on the fly, store generated images in a cache. |
4
|
|
|
* |
5
|
|
|
* @author Mikael Roos [email protected] |
6
|
|
|
* @example http://dbwebb.se/opensource/cimage |
7
|
|
|
* @link https://github.com/mosbth/cimage |
8
|
|
|
*/ |
9
|
|
|
class CImage |
10
|
|
|
{ |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Constants type of PNG image |
14
|
|
|
*/ |
15
|
|
|
const PNG_GREYSCALE = 0; |
16
|
|
|
const PNG_RGB = 2; |
17
|
|
|
const PNG_RGB_PALETTE = 3; |
18
|
|
|
const PNG_GREYSCALE_ALPHA = 4; |
19
|
|
|
const PNG_RGB_ALPHA = 6; |
20
|
|
|
|
21
|
|
|
|
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Constant for default image quality when not set |
25
|
|
|
*/ |
26
|
|
|
const JPEG_QUALITY_DEFAULT = 60; |
27
|
|
|
|
28
|
|
|
|
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Quality level for JPEG images. |
32
|
|
|
*/ |
33
|
|
|
private $quality; |
34
|
|
|
|
35
|
|
|
|
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Is the quality level set from external use (true) or is it default (false)? |
39
|
|
|
*/ |
40
|
|
|
private $useQuality = false; |
41
|
|
|
|
42
|
|
|
|
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Constant for default image quality when not set |
46
|
|
|
*/ |
47
|
|
|
const PNG_COMPRESSION_DEFAULT = -1; |
48
|
|
|
|
49
|
|
|
|
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Compression level for PNG images. |
53
|
|
|
*/ |
54
|
|
|
private $compress; |
55
|
|
|
|
56
|
|
|
|
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Is the compress level set from external use (true) or is it default (false)? |
60
|
|
|
*/ |
61
|
|
|
private $useCompress = false; |
62
|
|
|
|
63
|
|
|
|
64
|
|
|
|
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Add HTTP headers for outputing image. |
68
|
|
|
*/ |
69
|
|
|
private $HTTPHeader = array(); |
70
|
|
|
|
71
|
|
|
|
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Default background color, red, green, blue, alpha. |
75
|
|
|
* |
76
|
|
|
* @todo remake when upgrading to PHP 5.5 |
77
|
|
|
*/ |
78
|
|
|
/* |
79
|
|
|
const BACKGROUND_COLOR = array( |
80
|
|
|
'red' => 0, |
81
|
|
|
'green' => 0, |
82
|
|
|
'blue' => 0, |
83
|
|
|
'alpha' => null, |
84
|
|
|
);*/ |
85
|
|
|
|
86
|
|
|
|
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* Default background color to use. |
90
|
|
|
* |
91
|
|
|
* @todo remake when upgrading to PHP 5.5 |
92
|
|
|
*/ |
93
|
|
|
//private $bgColorDefault = self::BACKGROUND_COLOR; |
94
|
|
|
private $bgColorDefault = array( |
95
|
|
|
'red' => 0, |
96
|
|
|
'green' => 0, |
97
|
|
|
'blue' => 0, |
98
|
|
|
'alpha' => null, |
99
|
|
|
); |
100
|
|
|
|
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Background color to use, specified as part of options. |
104
|
|
|
*/ |
105
|
|
|
private $bgColor; |
106
|
|
|
|
107
|
|
|
|
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Where to save the target file. |
111
|
|
|
*/ |
112
|
|
|
private $saveFolder; |
113
|
|
|
|
114
|
|
|
|
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* The working image object. |
118
|
|
|
*/ |
119
|
|
|
private $image; |
120
|
|
|
|
121
|
|
|
|
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Image filename, may include subdirectory, relative from $imageFolder |
125
|
|
|
*/ |
126
|
|
|
private $imageSrc; |
127
|
|
|
|
128
|
|
|
|
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Actual path to the image, $imageFolder . '/' . $imageSrc |
132
|
|
|
*/ |
133
|
|
|
private $pathToImage; |
134
|
|
|
|
135
|
|
|
|
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* File type for source image, as provided by getimagesize() |
139
|
|
|
*/ |
140
|
|
|
private $fileType; |
141
|
|
|
|
142
|
|
|
|
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* File extension to use when saving image. |
146
|
|
|
*/ |
147
|
|
|
private $extension; |
148
|
|
|
|
149
|
|
|
|
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Output format, supports null (image) or json. |
153
|
|
|
*/ |
154
|
|
|
private $outputFormat = null; |
155
|
|
|
|
156
|
|
|
|
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Do lossy output using external postprocessing tools. |
160
|
|
|
*/ |
161
|
|
|
private $lossy = null; |
162
|
|
|
|
163
|
|
|
|
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Verbose mode to print out a trace and display the created image |
167
|
|
|
*/ |
168
|
|
|
private $verbose = false; |
169
|
|
|
|
170
|
|
|
|
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Keep a log/trace on what happens |
174
|
|
|
*/ |
175
|
|
|
private $log = array(); |
176
|
|
|
|
177
|
|
|
|
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Handle image as palette image |
181
|
|
|
*/ |
182
|
|
|
private $palette; |
183
|
|
|
|
184
|
|
|
|
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Target filename, with path, to save resulting image in. |
188
|
|
|
*/ |
189
|
|
|
private $cacheFileName; |
190
|
|
|
|
191
|
|
|
|
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Set a format to save image as, or null to use original format. |
195
|
|
|
*/ |
196
|
|
|
private $saveAs; |
197
|
|
|
|
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Path to command for lossy optimize, for example pngquant. |
201
|
|
|
*/ |
202
|
|
|
private $pngLossy; |
203
|
|
|
private $pngLossyCmd; |
204
|
|
|
|
205
|
|
|
|
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Path to command for filter optimize, for example optipng. |
209
|
|
|
*/ |
210
|
|
|
private $pngFilter; |
211
|
|
|
private $pngFilterCmd; |
212
|
|
|
|
213
|
|
|
|
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Path to command for deflate optimize, for example pngout. |
217
|
|
|
*/ |
218
|
|
|
private $pngDeflate; |
219
|
|
|
private $pngDeflateCmd; |
220
|
|
|
|
221
|
|
|
|
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Path to command to optimize jpeg images, for example jpegtran or null. |
225
|
|
|
*/ |
226
|
|
|
private $jpegOptimize; |
227
|
|
|
private $jpegOptimizeCmd; |
228
|
|
|
|
229
|
|
|
|
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Image dimensions, calculated from loaded image. |
233
|
|
|
*/ |
234
|
|
|
private $width; // Calculated from source image |
235
|
|
|
private $height; // Calculated from source image |
236
|
|
|
|
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* New image dimensions, incoming as argument or calculated. |
240
|
|
|
*/ |
241
|
|
|
private $newWidth; |
242
|
|
|
private $newWidthOrig; // Save original value |
243
|
|
|
private $newHeight; |
244
|
|
|
private $newHeightOrig; // Save original value |
245
|
|
|
|
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* Change target height & width when different dpr, dpr 2 means double image dimensions. |
249
|
|
|
*/ |
250
|
|
|
private $dpr = 1; |
251
|
|
|
|
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Always upscale images, even if they are smaller than target image. |
255
|
|
|
*/ |
256
|
|
|
const UPSCALE_DEFAULT = true; |
257
|
|
|
private $upscale = self::UPSCALE_DEFAULT; |
258
|
|
|
|
259
|
|
|
|
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Array with details on how to crop, incoming as argument and calculated. |
263
|
|
|
*/ |
264
|
|
|
public $crop; |
265
|
|
|
public $cropOrig; // Save original value |
266
|
|
|
|
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* String with details on how to do image convolution. String |
270
|
|
|
* should map a key in the $convolvs array or be a string of |
271
|
|
|
* 11 float values separated by comma. The first nine builds |
272
|
|
|
* up the matrix, then divisor and last offset. |
273
|
|
|
*/ |
274
|
|
|
private $convolve; |
275
|
|
|
|
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Custom convolution expressions, matrix 3x3, divisor and offset. |
279
|
|
|
*/ |
280
|
|
|
private $convolves = array( |
281
|
|
|
'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', |
282
|
|
|
'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', |
283
|
|
|
'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', |
284
|
|
|
'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', |
285
|
|
|
'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', |
286
|
|
|
'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', |
287
|
|
|
'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', |
288
|
|
|
'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', |
289
|
|
|
'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', |
290
|
|
|
'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', |
291
|
|
|
'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', |
292
|
|
|
'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', |
293
|
|
|
'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', |
294
|
|
|
); |
295
|
|
|
|
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Resize strategy to fill extra area with background color. |
299
|
|
|
* True or false. |
300
|
|
|
*/ |
301
|
|
|
private $fillToFit; |
302
|
|
|
|
303
|
|
|
|
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* To store value for option scale. |
307
|
|
|
*/ |
308
|
|
|
private $scale; |
309
|
|
|
|
310
|
|
|
|
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* To store value for option. |
314
|
|
|
*/ |
315
|
|
|
private $rotateBefore; |
316
|
|
|
|
317
|
|
|
|
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* To store value for option. |
321
|
|
|
*/ |
322
|
|
|
private $rotateAfter; |
323
|
|
|
|
324
|
|
|
|
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* To store value for option. |
328
|
|
|
*/ |
329
|
|
|
private $autoRotate; |
330
|
|
|
|
331
|
|
|
|
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* To store value for option. |
335
|
|
|
*/ |
336
|
|
|
private $sharpen; |
337
|
|
|
|
338
|
|
|
|
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* To store value for option. |
342
|
|
|
*/ |
343
|
|
|
private $emboss; |
344
|
|
|
|
345
|
|
|
|
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* To store value for option. |
349
|
|
|
*/ |
350
|
|
|
private $blur; |
351
|
|
|
|
352
|
|
|
|
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Used with option area to set which parts of the image to use. |
356
|
|
|
*/ |
357
|
|
|
private $offset; |
358
|
|
|
|
359
|
|
|
|
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* Calculate target dimension for image when using fill-to-fit resize strategy. |
363
|
|
|
*/ |
364
|
|
|
private $fillWidth; |
365
|
|
|
private $fillHeight; |
366
|
|
|
|
367
|
|
|
|
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Allow remote file download, default is to disallow remote file download. |
371
|
|
|
*/ |
372
|
|
|
private $allowRemote = false; |
373
|
|
|
|
374
|
|
|
|
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Path to cache for remote download. |
378
|
|
|
*/ |
379
|
|
|
private $remoteCache; |
380
|
|
|
|
381
|
|
|
|
382
|
|
|
|
383
|
|
|
/** |
384
|
|
|
* Pattern to recognize a remote file. |
385
|
|
|
*/ |
386
|
|
|
//private $remotePattern = '#^[http|https]://#'; |
387
|
|
|
private $remotePattern = '#^https?://#'; |
388
|
|
|
|
389
|
|
|
|
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* Use the cache if true, set to false to ignore the cached file. |
393
|
|
|
*/ |
394
|
|
|
private $useCache = true; |
395
|
|
|
|
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* Disable the fasttrackCacke to start with, inject an object to enable it. |
399
|
|
|
*/ |
400
|
|
|
private $fastTrackCache = null; |
401
|
|
|
|
402
|
|
|
|
403
|
|
|
|
404
|
|
|
/* |
405
|
|
|
* Set whitelist for valid hostnames from where remote source can be |
406
|
|
|
* downloaded. |
407
|
|
|
*/ |
408
|
|
|
private $remoteHostWhitelist = null; |
409
|
|
|
|
410
|
|
|
|
411
|
|
|
|
412
|
|
|
/* |
413
|
|
|
* Do verbose logging to file by setting this to a filename. |
414
|
|
|
*/ |
415
|
|
|
private $verboseFileName = null; |
416
|
|
|
|
417
|
|
|
|
418
|
|
|
|
419
|
|
|
/* |
420
|
|
|
* Output to ascii can take som options as an array. |
421
|
|
|
*/ |
422
|
|
|
private $asciiOptions = array(); |
423
|
|
|
|
424
|
|
|
|
425
|
|
|
|
426
|
|
|
/* |
427
|
|
|
* Use interlaced progressive mode for JPEG images. |
428
|
|
|
*/ |
429
|
|
|
private $interlace = false; |
430
|
|
|
|
431
|
|
|
|
432
|
|
|
|
433
|
|
|
/* |
434
|
7 |
|
* Image copy strategy, defaults to RESAMPLE. |
435
|
|
|
*/ |
436
|
7 |
|
const RESIZE = 1; |
437
|
7 |
|
const RESAMPLE = 2; |
438
|
7 |
|
private $copyStrategy = NULL; |
439
|
|
|
|
440
|
|
|
|
441
|
|
|
|
442
|
|
|
/** |
443
|
|
|
* Properties, the class is mutable and the method setOptions() |
444
|
|
|
* decides (partly) what properties are created. |
445
|
|
|
* |
446
|
|
|
* @todo Clean up these and check if and how they are used |
447
|
|
|
*/ |
448
|
|
|
|
449
|
|
|
public $keepRatio; |
450
|
|
|
public $cropToFit; |
451
|
|
|
private $cropWidth; |
452
|
|
|
private $cropHeight; |
453
|
|
|
public $crop_x; |
454
|
|
|
public $crop_y; |
455
|
|
|
public $filters; |
456
|
|
|
private $attr; // Calculated from source image |
457
|
|
|
|
458
|
|
|
|
459
|
|
|
|
460
|
|
|
|
461
|
|
|
/** |
462
|
|
|
* Constructor, can take arguments to init the object. |
463
|
|
|
* |
464
|
|
|
* @param string $imageSrc filename which may contain subdirectory. |
465
|
|
|
* @param string $imageFolder path to root folder for images. |
466
|
|
|
* @param string $saveFolder path to folder where to save the new file or null to skip saving. |
467
|
2 |
|
* @param string $saveName name of target file when saveing. |
468
|
|
|
*/ |
469
|
2 |
|
public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) |
470
|
2 |
|
{ |
471
|
|
|
$this->setSource($imageSrc, $imageFolder); |
472
|
|
|
$this->setTarget($saveFolder, $saveName); |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
|
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* Inject object and use it, must be available as member. |
479
|
|
|
* |
480
|
|
|
* @param string $property to set as object. |
481
|
|
|
* @param object $object to set to property. |
482
|
|
|
* |
483
|
|
|
* @return $this |
484
|
|
|
*/ |
485
|
|
|
public function injectDependency($property, $object) |
486
|
|
|
{ |
487
|
|
|
if (!property_exists($this, $property)) { |
488
|
|
|
$this->raiseError("Injecting unknown property."); |
489
|
|
|
} |
490
|
|
|
$this->$property = $object; |
491
|
|
|
return $this; |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
|
495
|
|
|
|
496
|
|
|
/** |
497
|
|
|
* Set verbose mode. |
498
|
|
|
* |
499
|
2 |
|
* @param boolean $mode true or false to enable and disable verbose mode, |
500
|
|
|
* default is true. |
501
|
2 |
|
* |
502
|
2 |
|
* @return $this |
503
|
|
|
*/ |
504
|
2 |
|
public function setVerbose($mode = true) |
505
|
|
|
{ |
506
|
2 |
|
$this->verbose = $mode; |
507
|
|
|
return $this; |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
|
511
|
|
|
|
512
|
|
|
/** |
513
|
|
|
* Set save folder, base folder for saving cache files. |
514
|
|
|
* |
515
|
|
|
* @todo clean up how $this->saveFolder is used in other methods. |
516
|
|
|
* |
517
|
|
|
* @param string $path where to store cached files. |
518
|
|
|
* |
519
|
2 |
|
* @return $this |
520
|
|
|
*/ |
521
|
2 |
|
public function setSaveFolder($path) |
522
|
2 |
|
{ |
523
|
|
|
$this->saveFolder = $path; |
524
|
2 |
|
return $this; |
525
|
|
|
} |
526
|
2 |
|
|
527
|
2 |
|
|
528
|
2 |
|
|
529
|
2 |
|
/** |
530
|
|
|
* Use cache or not. |
531
|
2 |
|
* |
532
|
|
|
* @param boolean $use true or false to use cache. |
533
|
|
|
* |
534
|
|
|
* @return $this |
535
|
|
|
*/ |
536
|
|
|
public function useCache($use = true) |
537
|
|
|
{ |
538
|
|
|
$this->useCache = $use; |
539
|
|
|
return $this; |
540
|
|
|
} |
541
|
|
|
|
542
|
|
|
|
543
|
2 |
|
|
544
|
|
|
/** |
545
|
2 |
|
* Create and save a dummy image. Use dimensions as stated in |
546
|
2 |
|
* $this->newWidth, or $width or default to 100 (same for height. |
547
|
2 |
|
* |
548
|
|
|
* @param integer $width use specified width for image dimension. |
549
|
|
|
* @param integer $height use specified width for image dimension. |
550
|
|
|
* |
551
|
|
|
* @return $this |
552
|
|
|
*/ |
553
|
|
|
public function createDummyImage($width = null, $height = null) |
554
|
|
|
{ |
555
|
|
|
$this->newWidth = $this->newWidth ?: $width ?: 100; |
556
|
|
|
$this->newHeight = $this->newHeight ?: $height ?: 100; |
557
|
|
|
|
558
|
|
|
$this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); |
559
|
|
|
|
560
|
2 |
|
return $this; |
561
|
|
|
} |
562
|
2 |
|
|
563
|
2 |
|
|
564
|
|
|
|
565
|
2 |
|
/** |
566
|
2 |
|
* Allow or disallow remote image download. |
567
|
2 |
|
* |
568
|
|
|
* @param boolean $allow true or false to enable and disable. |
569
|
|
|
* @param string $cache path to cache dir. |
570
|
|
|
* @param string $pattern to use to detect if its a remote file. |
571
|
|
|
* |
572
|
|
|
* @return $this |
573
|
|
|
*/ |
574
|
|
|
public function setRemoteDownload($allow, $cache, $pattern = null) |
575
|
|
|
{ |
576
|
|
|
$this->allowRemote = $allow; |
577
|
|
|
$this->remoteCache = $cache; |
578
|
|
|
$this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern; |
579
|
|
|
|
580
|
3 |
|
$this->log( |
581
|
|
|
"Set remote download to: " |
582
|
3 |
|
. ($this->allowRemote ? "true" : "false") |
583
|
1 |
|
. " using pattern " |
584
|
1 |
|
. $this->remotePattern |
585
|
|
|
); |
586
|
|
|
|
587
|
2 |
|
return $this; |
588
|
2 |
|
} |
589
|
2 |
|
|
590
|
|
|
|
591
|
2 |
|
|
592
|
|
|
/** |
593
|
2 |
|
* Check if the image resource is a remote file or not. |
594
|
2 |
|
* |
595
|
2 |
|
* @param string $src check if src is remote. |
596
|
|
|
* |
597
|
|
|
* @return boolean true if $src is a remote file, else false. |
598
|
|
|
*/ |
599
|
|
|
public function isRemoteSource($src) |
600
|
|
|
{ |
601
|
|
|
$remote = preg_match($this->remotePattern, $src); |
602
|
|
|
$this->log("Detected remote image: " . ($remote ? "true" : "false")); |
603
|
|
|
return !!$remote; |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
|
607
|
|
|
|
608
|
|
|
/** |
609
|
|
|
* Set whitelist for valid hostnames from where remote source can be |
610
|
|
|
* downloaded. |
611
|
|
|
* |
612
|
|
|
* @param array $whitelist with regexp hostnames to allow download from. |
613
|
|
|
* |
614
|
|
|
* @return $this |
615
|
|
|
*/ |
616
|
|
|
public function setRemoteHostWhitelist($whitelist = null) |
617
|
|
|
{ |
618
|
|
|
$this->remoteHostWhitelist = $whitelist; |
619
|
|
|
$this->log( |
620
|
|
|
"Setting remote host whitelist to: " |
621
|
|
|
. (is_null($whitelist) ? "null" : print_r($whitelist, 1)) |
622
|
|
|
); |
623
|
|
|
return $this; |
624
|
|
|
} |
625
|
|
|
|
626
|
2 |
|
|
627
|
|
|
|
628
|
2 |
|
/** |
629
|
|
|
* Check if the hostname for the remote image, is on a whitelist, |
630
|
2 |
|
* if the whitelist is defined. |
631
|
|
|
* |
632
|
|
|
* @param string $src the remote source. |
633
|
|
|
* |
634
|
2 |
|
* @return boolean true if hostname on $src is in the whitelist, else false. |
635
|
|
|
*/ |
636
|
|
|
public function isRemoteSourceOnWhitelist($src) |
637
|
|
|
{ |
638
|
|
|
if (is_null($this->remoteHostWhitelist)) { |
639
|
|
|
$this->log("Remote host on whitelist not configured - allowing."); |
640
|
|
|
return true; |
641
|
|
|
} |
642
|
|
|
|
643
|
|
|
$whitelist = new CWhitelist(); |
644
|
|
|
$hostname = parse_url($src, PHP_URL_HOST); |
645
|
|
|
$allow = $whitelist->check($hostname, $this->remoteHostWhitelist); |
|
|
|
|
646
|
|
|
|
647
|
|
|
$this->log( |
648
|
|
|
"Remote host is on whitelist: " |
649
|
|
|
. ($allow ? "true" : "false") |
650
|
|
|
); |
651
|
|
|
return $allow; |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
|
655
|
|
|
|
656
|
|
|
/** |
657
|
|
|
* Check if file extension is valid as a file extension. |
658
|
|
|
* |
659
|
|
|
* @param string $extension of image file. |
660
|
|
|
* |
661
|
|
|
* @return $this |
662
|
|
|
*/ |
663
|
|
|
private function checkFileExtension($extension) |
664
|
|
|
{ |
665
|
|
|
$valid = array('jpg', 'jpeg', 'png', 'gif', 'webp'); |
666
|
|
|
|
667
|
|
|
in_array(strtolower($extension), $valid) |
668
|
|
|
or $this->raiseError('Not a valid file extension.'); |
669
|
|
|
|
670
|
|
|
return $this; |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
|
674
|
|
|
|
675
|
|
|
/** |
676
|
|
|
* Normalize the file extension. |
677
|
|
|
* |
678
|
|
|
* @param string $extension of image file or skip to use internal. |
679
|
|
|
* |
680
|
|
|
* @return string $extension as a normalized file extension. |
681
|
|
|
*/ |
682
|
|
|
private function normalizeFileExtension($extension = null) |
683
|
|
|
{ |
684
|
|
|
$extension = strtolower($extension ? $extension : $this->extension); |
685
|
|
|
|
686
|
|
|
if ($extension == 'jpeg') { |
687
|
|
|
$extension = 'jpg'; |
688
|
7 |
|
} |
689
|
|
|
|
690
|
7 |
|
return $extension; |
691
|
7 |
|
} |
692
|
7 |
|
|
693
|
7 |
|
|
694
|
|
|
|
695
|
|
|
/** |
696
|
2 |
|
* Download a remote image and return path to its local copy. |
697
|
|
|
* |
698
|
|
|
* @param string $src remote path to image. |
699
|
|
|
* |
700
|
|
|
* @return string as path to downloaded remote source. |
701
|
2 |
|
*/ |
702
|
|
|
public function downloadRemoteSource($src) |
703
|
|
|
{ |
704
|
|
|
if (!$this->isRemoteSourceOnWhitelist($src)) { |
705
|
|
|
throw new Exception("Hostname is not on whitelist for remote sources."); |
706
|
2 |
|
} |
707
|
2 |
|
|
708
|
2 |
|
$remote = new CRemoteImage(); |
709
|
|
|
|
710
|
2 |
|
if (!is_writable($this->remoteCache)) { |
711
|
|
|
$this->log("The remote cache is not writable."); |
712
|
|
|
} |
713
|
|
|
|
714
|
|
|
$remote->setCache($this->remoteCache); |
715
|
|
|
$remote->useCache($this->useCache); |
716
|
|
|
$src = $remote->download($src); |
717
|
|
|
|
718
|
|
|
$this->log("Remote HTTP status: " . $remote->getStatus()); |
719
|
|
|
$this->log("Remote item is in local cache: $src"); |
720
|
|
|
$this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); |
721
|
|
|
|
722
|
|
|
return $src; |
723
|
|
|
} |
724
|
7 |
|
|
725
|
|
|
|
726
|
7 |
|
|
727
|
7 |
|
/** |
728
|
7 |
|
* Set source file to use as image source. |
729
|
|
|
* |
730
|
|
|
* @param string $src of image. |
731
|
2 |
|
* @param string $dir as optional base directory where images are. |
732
|
|
|
* |
733
|
|
|
* @return $this |
734
|
|
|
*/ |
735
|
2 |
|
public function setSource($src, $dir = null) |
736
|
|
|
{ |
737
|
|
|
if (!isset($src)) { |
738
|
2 |
|
$this->imageSrc = null; |
739
|
2 |
|
$this->pathToImage = null; |
740
|
|
|
return $this; |
741
|
2 |
|
} |
742
|
|
|
|
743
|
|
|
if ($this->allowRemote && $this->isRemoteSource($src)) { |
744
|
|
|
$src = $this->downloadRemoteSource($src); |
745
|
|
|
$dir = null; |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
if (!isset($dir)) { |
749
|
|
|
$dir = dirname($src); |
750
|
|
|
$src = basename($src); |
751
|
2 |
|
} |
752
|
|
|
|
753
|
2 |
|
$this->imageSrc = ltrim($src, '/'); |
754
|
|
|
$imageFolder = rtrim($dir, '/'); |
755
|
|
|
$this->pathToImage = $imageFolder . '/' . $this->imageSrc; |
756
|
|
|
|
757
|
|
|
return $this; |
758
|
|
|
} |
759
|
|
|
|
760
|
|
|
|
761
|
|
|
|
762
|
|
|
/** |
763
|
|
|
* Set target file. |
764
|
|
|
* |
765
|
|
|
* @param string $src of target image. |
766
|
|
|
* @param string $dir as optional base directory where images are stored. |
767
|
|
|
* Uses $this->saveFolder if null. |
768
|
|
|
* |
769
|
|
|
* @return $this |
770
|
|
|
*/ |
771
|
|
|
public function setTarget($src = null, $dir = null) |
772
|
|
|
{ |
773
|
|
|
if (!isset($src)) { |
774
|
|
|
$this->cacheFileName = null; |
775
|
|
|
return $this; |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
if (isset($dir)) { |
779
|
|
|
$this->saveFolder = rtrim($dir, '/'); |
780
|
|
|
} |
781
|
|
|
|
782
|
|
|
$this->cacheFileName = $this->saveFolder . '/' . $src; |
783
|
|
|
|
784
|
|
|
// Sanitize filename |
785
|
|
|
$this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); |
786
|
|
|
$this->log("The cache file name is: " . $this->cacheFileName); |
787
|
|
|
|
788
|
|
|
return $this; |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
|
792
|
|
|
|
793
|
|
|
/** |
794
|
|
|
* Get filename of target file. |
795
|
|
|
* |
796
|
|
|
* @return Boolean|String as filename of target or false if not set. |
797
|
|
|
*/ |
798
|
|
|
public function getTarget() |
799
|
|
|
{ |
800
|
|
|
return $this->cacheFileName; |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
|
804
|
|
|
|
805
|
|
|
/** |
806
|
|
|
* Set options to use when processing image. |
807
|
|
|
* |
808
|
|
|
* @param array $args used when processing image. |
809
|
|
|
* |
810
|
|
|
* @return $this |
811
|
|
|
*/ |
812
|
|
|
public function setOptions($args) |
813
|
|
|
{ |
814
|
|
|
$this->log("Set new options for processing image."); |
815
|
|
|
|
816
|
|
|
$defaults = array( |
817
|
|
|
// Options for calculate dimensions |
818
|
|
|
'newWidth' => null, |
819
|
|
|
'newHeight' => null, |
820
|
|
|
'aspectRatio' => null, |
821
|
|
|
'keepRatio' => true, |
822
|
|
|
'cropToFit' => false, |
823
|
|
|
'fillToFit' => null, |
824
|
|
|
'crop' => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), |
825
|
|
|
'area' => null, //'0,0,0,0', |
826
|
|
|
'upscale' => self::UPSCALE_DEFAULT, |
827
|
|
|
|
828
|
|
|
// Options for caching or using original |
829
|
|
|
'useCache' => true, |
830
|
|
|
'useOriginal' => true, |
831
|
|
|
|
832
|
|
|
// Pre-processing, before resizing is done |
833
|
|
|
'scale' => null, |
834
|
|
|
'rotateBefore' => null, |
835
|
|
|
'autoRotate' => false, |
836
|
|
|
|
837
|
|
|
// General options |
838
|
|
|
'bgColor' => null, |
839
|
|
|
|
840
|
|
|
// Post-processing, after resizing is done |
841
|
|
|
'palette' => null, |
842
|
|
|
'filters' => null, |
843
|
|
|
'sharpen' => null, |
844
|
|
|
'emboss' => null, |
845
|
|
|
'blur' => null, |
846
|
|
|
'convolve' => null, |
847
|
|
|
'rotateAfter' => null, |
848
|
|
|
'interlace' => null, |
849
|
|
|
|
850
|
|
|
// Output format |
851
|
|
|
'outputFormat' => null, |
852
|
|
|
'dpr' => 1, |
853
|
|
|
|
854
|
|
|
// Postprocessing using external tools |
855
|
|
|
'lossy' => null, |
856
|
|
|
); |
857
|
|
|
|
858
|
|
|
// Convert crop settings from string to array |
859
|
|
|
if (isset($args['crop']) && !is_array($args['crop'])) { |
860
|
|
|
$pices = explode(',', $args['crop']); |
861
|
|
|
$args['crop'] = array( |
862
|
|
|
'width' => $pices[0], |
863
|
|
|
'height' => $pices[1], |
864
|
|
|
'start_x' => $pices[2], |
865
|
|
|
'start_y' => $pices[3], |
866
|
|
|
); |
867
|
|
|
} |
868
|
|
|
|
869
|
|
|
// Convert area settings from string to array |
870
|
|
|
if (isset($args['area']) && !is_array($args['area'])) { |
871
|
|
|
$pices = explode(',', $args['area']); |
872
|
|
|
$args['area'] = array( |
873
|
|
|
'top' => $pices[0], |
874
|
|
|
'right' => $pices[1], |
875
|
|
|
'bottom' => $pices[2], |
876
|
|
|
'left' => $pices[3], |
877
|
|
|
); |
878
|
|
|
} |
879
|
|
|
|
880
|
|
|
// Convert filter settings from array of string to array of array |
881
|
|
|
if (isset($args['filters']) && is_array($args['filters'])) { |
882
|
|
|
foreach ($args['filters'] as $key => $filterStr) { |
883
|
|
|
$parts = explode(',', $filterStr); |
884
|
|
|
$filter = $this->mapFilter($parts[0]); |
885
|
|
|
$filter['str'] = $filterStr; |
886
|
|
|
for ($i=1; $i<=$filter['argc']; $i++) { |
887
|
|
|
if (isset($parts[$i])) { |
888
|
|
|
$filter["arg{$i}"] = $parts[$i]; |
889
|
|
|
} else { |
890
|
|
|
throw new Exception( |
891
|
|
|
'Missing arg to filter, review how many arguments are needed at |
892
|
|
|
http://php.net/manual/en/function.imagefilter.php' |
893
|
|
|
); |
894
|
|
|
} |
895
|
|
|
} |
896
|
|
|
$args['filters'][$key] = $filter; |
897
|
|
|
} |
898
|
|
|
} |
899
|
|
|
|
900
|
|
|
// Merge default arguments with incoming and set properties. |
901
|
|
|
//$args = array_merge_recursive($defaults, $args); |
902
|
|
|
$args = array_merge($defaults, $args); |
903
|
|
|
foreach ($defaults as $key => $val) { |
904
|
|
|
$this->{$key} = $args[$key]; |
905
|
|
|
} |
906
|
|
|
|
907
|
|
|
if ($this->bgColor) { |
908
|
|
|
$this->setDefaultBackgroundColor($this->bgColor); |
909
|
|
|
} |
910
|
|
|
|
911
|
|
|
// Save original values to enable re-calculating |
912
|
|
|
$this->newWidthOrig = $this->newWidth; |
913
|
|
|
$this->newHeightOrig = $this->newHeight; |
914
|
|
|
$this->cropOrig = $this->crop; |
915
|
|
|
|
916
|
|
|
return $this; |
917
|
|
|
} |
918
|
|
|
|
919
|
|
|
|
920
|
|
|
|
921
|
|
|
/** |
922
|
|
|
* Map filter name to PHP filter and id. |
923
|
|
|
* |
924
|
|
|
* @param string $name the name of the filter. |
925
|
|
|
* |
926
|
|
|
* @return array with filter settings |
927
|
|
|
* @throws Exception |
928
|
|
|
*/ |
929
|
|
|
private function mapFilter($name) |
930
|
|
|
{ |
931
|
|
|
$map = array( |
932
|
|
|
'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE), |
933
|
|
|
'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE), |
934
|
|
|
'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS), |
935
|
|
|
'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST), |
936
|
|
|
'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE), |
937
|
|
|
'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT), |
938
|
|
|
'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS), |
939
|
|
|
'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR), |
940
|
|
|
'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR), |
941
|
|
|
'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL), |
942
|
|
|
'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH), |
943
|
|
|
'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE), |
944
|
|
|
); |
945
|
|
|
|
946
|
|
|
if (isset($map[$name])) { |
947
|
|
|
return $map[$name]; |
948
|
|
|
} else { |
949
|
|
|
throw new Exception('No such filter.'); |
950
|
|
|
} |
951
|
|
|
} |
952
|
|
|
|
953
|
|
|
|
954
|
|
|
|
955
|
|
|
/** |
956
|
|
|
* Load image details from original image file. |
957
|
|
|
* |
958
|
|
|
* @param string $file the file to load or null to use $this->pathToImage. |
959
|
|
|
* |
960
|
|
|
* @return $this |
961
|
|
|
* @throws Exception |
962
|
|
|
*/ |
963
|
|
|
public function loadImageDetails($file = null) |
964
|
|
|
{ |
965
|
|
|
$file = $file ? $file : $this->pathToImage; |
966
|
|
|
|
967
|
|
|
is_readable($file) |
968
|
|
|
or $this->raiseError('Image file does not exist.'); |
969
|
|
|
|
970
|
|
|
$info = list($this->width, $this->height, $this->fileType) = getimagesize($file); |
971
|
|
|
if (empty($info)) { |
972
|
|
|
// To support webp |
973
|
|
|
$this->fileType = false; |
974
|
|
|
if (function_exists("exif_imagetype")) { |
975
|
|
|
$this->fileType = exif_imagetype($file); |
976
|
|
|
if ($this->fileType === false) { |
977
|
|
|
if (function_exists("imagecreatefromwebp")) { |
978
|
|
|
$webp = imagecreatefromwebp($file); |
979
|
|
|
if ($webp !== false) { |
980
|
|
|
$this->width = imagesx($webp); |
981
|
|
|
$this->height = imagesy($webp); |
982
|
|
|
$this->fileType = IMG_WEBP; |
983
|
|
|
} |
984
|
|
|
} |
985
|
|
|
} |
986
|
|
|
} |
987
|
|
|
} |
988
|
|
|
|
989
|
|
|
if (!$this->fileType) { |
990
|
|
|
throw new Exception("Loading image details, the file doesn't seem to be a valid image."); |
991
|
|
|
} |
992
|
|
|
|
993
|
|
|
if ($this->verbose) { |
994
|
|
|
$this->log("Loading image details for: {$file}"); |
995
|
|
|
$this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType})."); |
996
|
|
|
$this->log(" Image filesize: " . filesize($file) . " bytes."); |
997
|
|
|
$this->log(" Image mimetype: " . $this->getMimeType()); |
998
|
|
|
} |
999
|
|
|
|
1000
|
|
|
return $this; |
1001
|
|
|
} |
1002
|
|
|
|
1003
|
|
|
|
1004
|
|
|
|
1005
|
|
|
/** |
1006
|
|
|
* Get mime type for image type. |
1007
|
|
|
* |
1008
|
|
|
* @return $this |
1009
|
|
|
* @throws Exception |
1010
|
|
|
*/ |
1011
|
|
|
protected function getMimeType() |
1012
|
|
|
{ |
1013
|
|
|
if ($this->fileType === IMG_WEBP) { |
1014
|
|
|
return "image/webp"; |
1015
|
|
|
} |
1016
|
|
|
|
1017
|
|
|
return image_type_to_mime_type($this->fileType); |
1018
|
|
|
} |
1019
|
|
|
|
1020
|
|
|
|
1021
|
|
|
|
1022
|
|
|
/** |
1023
|
|
|
* Init new width and height and do some sanity checks on constraints, before any |
1024
|
|
|
* processing can be done. |
1025
|
|
|
* |
1026
|
|
|
* @return $this |
1027
|
|
|
* @throws Exception |
1028
|
|
|
*/ |
1029
|
|
|
public function initDimensions() |
1030
|
|
|
{ |
1031
|
|
|
$this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); |
1032
|
|
|
|
1033
|
|
|
// width as % |
1034
|
|
|
if ($this->newWidth |
1035
|
|
|
&& $this->newWidth[strlen($this->newWidth)-1] == '%') { |
1036
|
|
|
$this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100; |
1037
|
|
|
$this->log("Setting new width based on % to {$this->newWidth}"); |
1038
|
|
|
} |
1039
|
|
|
|
1040
|
|
|
// height as % |
1041
|
|
|
if ($this->newHeight |
1042
|
|
|
&& $this->newHeight[strlen($this->newHeight)-1] == '%') { |
1043
|
|
|
$this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100; |
1044
|
|
|
$this->log("Setting new height based on % to {$this->newHeight}"); |
1045
|
|
|
} |
1046
|
|
|
|
1047
|
|
|
is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); |
|
|
|
|
1048
|
|
|
|
1049
|
|
|
// width & height from aspect ratio |
1050
|
|
|
if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { |
1051
|
|
|
if ($this->aspectRatio >= 1) { |
1052
|
|
|
$this->newWidth = $this->width; |
1053
|
|
|
$this->newHeight = $this->width / $this->aspectRatio; |
1054
|
|
|
$this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}"); |
1055
|
|
|
|
1056
|
|
|
} else { |
1057
|
|
|
$this->newHeight = $this->height; |
1058
|
|
|
$this->newWidth = $this->height * $this->aspectRatio; |
1059
|
|
|
$this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}"); |
1060
|
|
|
} |
1061
|
|
|
|
1062
|
|
|
} elseif ($this->aspectRatio && is_null($this->newWidth)) { |
1063
|
|
|
$this->newWidth = $this->newHeight * $this->aspectRatio; |
1064
|
|
|
$this->log("Setting new width based on aspect ratio to {$this->newWidth}"); |
1065
|
|
|
|
1066
|
|
|
} elseif ($this->aspectRatio && is_null($this->newHeight)) { |
1067
|
|
|
$this->newHeight = $this->newWidth / $this->aspectRatio; |
1068
|
|
|
$this->log("Setting new height based on aspect ratio to {$this->newHeight}"); |
1069
|
|
|
} |
1070
|
|
|
|
1071
|
|
|
// Change width & height based on dpr |
1072
|
|
|
if ($this->dpr != 1) { |
1073
|
|
|
if (!is_null($this->newWidth)) { |
1074
|
|
|
$this->newWidth = round($this->newWidth * $this->dpr); |
1075
|
|
|
$this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}"); |
1076
|
|
|
} |
1077
|
|
|
if (!is_null($this->newHeight)) { |
1078
|
|
|
$this->newHeight = round($this->newHeight * $this->dpr); |
1079
|
|
|
$this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}"); |
1080
|
|
|
} |
1081
|
|
|
} |
1082
|
|
|
|
1083
|
|
|
// Check values to be within domain |
1084
|
|
|
is_null($this->newWidth) |
1085
|
|
|
or is_numeric($this->newWidth) |
1086
|
|
|
or $this->raiseError('Width not numeric'); |
1087
|
|
|
|
1088
|
|
|
is_null($this->newHeight) |
1089
|
|
|
or is_numeric($this->newHeight) |
1090
|
|
|
or $this->raiseError('Height not numeric'); |
1091
|
|
|
|
1092
|
|
|
$this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); |
1093
|
|
|
|
1094
|
|
|
return $this; |
1095
|
|
|
} |
1096
|
|
|
|
1097
|
|
|
|
1098
|
|
|
|
1099
|
|
|
/** |
1100
|
|
|
* Calculate new width and height of image, based on settings. |
1101
|
|
|
* |
1102
|
|
|
* @return $this |
1103
|
|
|
*/ |
1104
|
|
|
public function calculateNewWidthAndHeight() |
1105
|
|
|
{ |
1106
|
|
|
// Crop, use cropped width and height as base for calulations |
1107
|
|
|
$this->log("Calculate new width and height."); |
1108
|
|
|
$this->log("Original width x height is {$this->width} x {$this->height}."); |
1109
|
|
|
$this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); |
1110
|
|
|
|
1111
|
|
|
// Check if there is an area to crop off |
1112
|
|
|
if (isset($this->area)) { |
1113
|
|
|
$this->offset['top'] = round($this->area['top'] / 100 * $this->height); |
|
|
|
|
1114
|
|
|
$this->offset['right'] = round($this->area['right'] / 100 * $this->width); |
1115
|
|
|
$this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height); |
1116
|
|
|
$this->offset['left'] = round($this->area['left'] / 100 * $this->width); |
1117
|
|
|
$this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right']; |
1118
|
|
|
$this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom']; |
1119
|
|
|
$this->width = $this->offset['width']; |
1120
|
|
|
$this->height = $this->offset['height']; |
1121
|
|
|
$this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%."); |
1122
|
|
|
$this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px."); |
1123
|
|
|
} |
1124
|
|
|
|
1125
|
|
|
$width = $this->width; |
1126
|
|
|
$height = $this->height; |
1127
|
|
|
|
1128
|
|
|
// Check if crop is set |
1129
|
|
|
if ($this->crop) { |
1130
|
|
|
$width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width']; |
1131
|
|
|
$height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height']; |
1132
|
|
|
|
1133
|
|
|
if ($this->crop['start_x'] == 'left') { |
1134
|
|
|
$this->crop['start_x'] = 0; |
1135
|
|
|
} elseif ($this->crop['start_x'] == 'right') { |
1136
|
|
|
$this->crop['start_x'] = $this->width - $width; |
1137
|
|
|
} elseif ($this->crop['start_x'] == 'center') { |
1138
|
|
|
$this->crop['start_x'] = round($this->width / 2) - round($width / 2); |
1139
|
|
|
} |
1140
|
|
|
|
1141
|
|
|
if ($this->crop['start_y'] == 'top') { |
1142
|
|
|
$this->crop['start_y'] = 0; |
1143
|
|
|
} elseif ($this->crop['start_y'] == 'bottom') { |
1144
|
|
|
$this->crop['start_y'] = $this->height - $height; |
1145
|
|
|
} elseif ($this->crop['start_y'] == 'center') { |
1146
|
|
|
$this->crop['start_y'] = round($this->height / 2) - round($height / 2); |
1147
|
|
|
} |
1148
|
|
|
|
1149
|
|
|
$this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px."); |
1150
|
|
|
} |
1151
|
|
|
|
1152
|
|
|
// Calculate new width and height if keeping aspect-ratio. |
1153
|
|
|
if ($this->keepRatio) { |
1154
|
|
|
|
1155
|
|
|
$this->log("Keep aspect ratio."); |
1156
|
|
|
|
1157
|
|
|
// Crop-to-fit and both new width and height are set. |
1158
|
|
|
if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) { |
1159
|
|
|
|
1160
|
|
|
// Use newWidth and newHeigh as width/height, image should fit in box. |
1161
|
|
|
$this->log("Use newWidth and newHeigh as width/height, image should fit in box."); |
1162
|
|
|
|
1163
|
|
|
} elseif (isset($this->newWidth) && isset($this->newHeight)) { |
1164
|
|
|
|
1165
|
|
|
// Both new width and height are set. |
1166
|
|
|
// Use newWidth and newHeigh as max width/height, image should not be larger. |
1167
|
|
|
$ratioWidth = $width / $this->newWidth; |
1168
|
|
|
$ratioHeight = $height / $this->newHeight; |
1169
|
|
|
$ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; |
1170
|
|
|
$this->newWidth = round($width / $ratio); |
1171
|
|
|
$this->newHeight = round($height / $ratio); |
1172
|
|
|
$this->log("New width and height was set."); |
1173
|
|
|
|
1174
|
|
|
} elseif (isset($this->newWidth)) { |
1175
|
|
|
|
1176
|
|
|
// Use new width as max-width |
1177
|
|
|
$factor = (float)$this->newWidth / (float)$width; |
1178
|
|
|
$this->newHeight = round($factor * $height); |
1179
|
|
|
$this->log("New width was set."); |
1180
|
|
|
|
1181
|
|
|
} elseif (isset($this->newHeight)) { |
1182
|
|
|
|
1183
|
|
|
// Use new height as max-hight |
1184
|
|
|
$factor = (float)$this->newHeight / (float)$height; |
1185
|
|
|
$this->newWidth = round($factor * $width); |
1186
|
|
|
$this->log("New height was set."); |
1187
|
|
|
|
1188
|
|
|
} else { |
1189
|
|
|
|
1190
|
|
|
// Use existing width and height as new width and height. |
1191
|
|
|
$this->newWidth = $width; |
1192
|
|
|
$this->newHeight = $height; |
1193
|
|
|
} |
1194
|
|
|
|
1195
|
|
|
|
1196
|
|
|
// Get image dimensions for pre-resize image. |
1197
|
|
|
if ($this->cropToFit || $this->fillToFit) { |
1198
|
|
|
|
1199
|
|
|
// Get relations of original & target image |
1200
|
|
|
$ratioWidth = $width / $this->newWidth; |
1201
|
|
|
$ratioHeight = $height / $this->newHeight; |
1202
|
|
|
|
1203
|
|
|
if ($this->cropToFit) { |
1204
|
|
|
|
1205
|
|
|
// Use newWidth and newHeigh as defined width/height, |
1206
|
|
|
// image should fit the area. |
1207
|
|
|
$this->log("Crop to fit."); |
1208
|
|
|
$ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; |
1209
|
|
|
$this->cropWidth = round($width / $ratio); |
1210
|
|
|
$this->cropHeight = round($height / $ratio); |
1211
|
|
|
$this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio)."); |
1212
|
|
|
|
1213
|
|
|
} elseif ($this->fillToFit) { |
1214
|
|
|
|
1215
|
|
|
// Use newWidth and newHeigh as defined width/height, |
1216
|
|
|
// image should fit the area. |
1217
|
|
|
$this->log("Fill to fit."); |
1218
|
|
|
$ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth; |
1219
|
|
|
$this->fillWidth = round($width / $ratio); |
1220
|
|
|
$this->fillHeight = round($height / $ratio); |
1221
|
|
|
$this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio)."); |
1222
|
|
|
} |
1223
|
|
|
} |
1224
|
|
|
} |
1225
|
|
|
|
1226
|
|
|
// Crop, ensure to set new width and height |
1227
|
|
|
if ($this->crop) { |
1228
|
|
|
$this->log("Crop."); |
1229
|
|
|
$this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); |
1230
|
|
|
$this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); |
1231
|
|
|
} |
1232
|
|
|
|
1233
|
|
|
// Fill to fit, ensure to set new width and height |
1234
|
|
|
/*if ($this->fillToFit) { |
1235
|
|
|
$this->log("FillToFit."); |
1236
|
|
|
$this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); |
1237
|
|
|
$this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); |
1238
|
|
|
}*/ |
1239
|
|
|
|
1240
|
|
|
// No new height or width is set, use existing measures. |
1241
|
|
|
$this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width); |
1242
|
|
|
$this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height); |
1243
|
|
|
$this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}."); |
1244
|
|
|
|
1245
|
|
|
return $this; |
1246
|
|
|
} |
1247
|
|
|
|
1248
|
|
|
|
1249
|
|
|
|
1250
|
|
|
/** |
1251
|
|
|
* Re-calculate image dimensions when original image dimension has changed. |
1252
|
|
|
* |
1253
|
|
|
* @return $this |
1254
|
|
|
*/ |
1255
|
|
|
public function reCalculateDimensions() |
1256
|
|
|
{ |
1257
|
|
|
$this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight); |
1258
|
|
|
|
1259
|
|
|
$this->newWidth = $this->newWidthOrig; |
1260
|
|
|
$this->newHeight = $this->newHeightOrig; |
1261
|
|
|
$this->crop = $this->cropOrig; |
1262
|
|
|
|
1263
|
|
|
$this->initDimensions() |
1264
|
|
|
->calculateNewWidthAndHeight(); |
1265
|
|
|
|
1266
|
|
|
return $this; |
1267
|
|
|
} |
1268
|
|
|
|
1269
|
|
|
|
1270
|
|
|
|
1271
|
|
|
/** |
1272
|
|
|
* Set extension for filename to save as. |
1273
|
|
|
* |
1274
|
|
|
* @param string $saveas extension to save image as |
|
|
|
|
1275
|
|
|
* |
1276
|
|
|
* @return $this |
1277
|
|
|
*/ |
1278
|
|
|
public function setSaveAsExtension($saveAs = null) |
1279
|
|
|
{ |
1280
|
|
|
if (isset($saveAs)) { |
1281
|
|
|
$saveAs = strtolower($saveAs); |
1282
|
|
|
$this->checkFileExtension($saveAs); |
1283
|
|
|
$this->saveAs = $saveAs; |
1284
|
|
|
$this->extension = $saveAs; |
1285
|
|
|
} |
1286
|
|
|
|
1287
|
|
|
$this->log("Prepare to save image as: " . $this->extension); |
1288
|
|
|
|
1289
|
|
|
return $this; |
1290
|
|
|
} |
1291
|
|
|
|
1292
|
|
|
|
1293
|
|
|
|
1294
|
|
|
/** |
1295
|
|
|
* Set JPEG quality to use when saving image |
1296
|
|
|
* |
1297
|
|
|
* @param int $quality as the quality to set. |
1298
|
|
|
* |
1299
|
|
|
* @return $this |
1300
|
|
|
*/ |
1301
|
|
|
public function setJpegQuality($quality = null) |
1302
|
|
|
{ |
1303
|
|
|
if ($quality) { |
|
|
|
|
1304
|
2 |
|
$this->useQuality = true; |
1305
|
|
|
} |
1306
|
2 |
|
|
1307
|
2 |
|
$this->quality = isset($quality) |
1308
|
2 |
|
? $quality |
1309
|
2 |
|
: self::JPEG_QUALITY_DEFAULT; |
1310
|
2 |
|
|
1311
|
2 |
|
(is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100) |
1312
|
2 |
|
or $this->raiseError('Quality not in range.'); |
1313
|
2 |
|
|
1314
|
2 |
|
$this->log("Setting JPEG quality to {$this->quality}."); |
1315
|
2 |
|
|
1316
|
2 |
|
return $this; |
1317
|
|
|
} |
1318
|
2 |
|
|
1319
|
2 |
|
|
1320
|
|
|
|
1321
|
2 |
|
/** |
1322
|
2 |
|
* Set PNG compressen algorithm to use when saving image |
1323
|
|
|
* |
1324
|
|
|
* @param int $compress as the algorithm to use. |
1325
|
|
|
* |
1326
|
2 |
|
* @return $this |
1327
|
2 |
|
*/ |
1328
|
|
|
public function setPngCompression($compress = null) |
1329
|
2 |
|
{ |
1330
|
2 |
|
if ($compress) { |
|
|
|
|
1331
|
2 |
|
$this->useCompress = true; |
1332
|
|
|
} |
1333
|
2 |
|
|
1334
|
2 |
|
$this->compress = isset($compress) |
1335
|
2 |
|
? $compress |
1336
|
|
|
: self::PNG_COMPRESSION_DEFAULT; |
1337
|
2 |
|
|
1338
|
2 |
|
(is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9) |
1339
|
|
|
or $this->raiseError('Quality not in range.'); |
1340
|
|
|
|
1341
|
|
|
$this->log("Setting PNG compression level to {$this->compress}."); |
1342
|
|
|
|
1343
|
|
|
return $this; |
1344
|
|
|
} |
1345
|
|
|
|
1346
|
|
|
|
1347
|
|
|
|
1348
|
|
|
/** |
1349
|
2 |
|
* Use original image if possible, check options which affects image processing. |
1350
|
2 |
|
* |
1351
|
2 |
|
* @param boolean $useOrig default is to use original if possible, else set to false. |
1352
|
2 |
|
* |
1353
|
|
|
* @return $this |
1354
|
2 |
|
*/ |
1355
|
|
|
public function useOriginalIfPossible($useOrig = true) |
1356
|
2 |
|
{ |
1357
|
2 |
|
if ($useOrig |
1358
|
2 |
|
&& ($this->newWidth == $this->width) |
1359
|
|
|
&& ($this->newHeight == $this->height) |
1360
|
2 |
|
&& !$this->area |
1361
|
2 |
|
&& !$this->crop |
|
|
|
|
1362
|
|
|
&& !$this->cropToFit |
1363
|
|
|
&& !$this->fillToFit |
1364
|
|
|
&& !$this->filters |
1365
|
2 |
|
&& !$this->sharpen |
1366
|
2 |
|
&& !$this->emboss |
1367
|
|
|
&& !$this->blur |
1368
|
|
|
&& !$this->convolve |
1369
|
|
|
&& !$this->palette |
1370
|
2 |
|
&& !$this->useQuality |
1371
|
2 |
|
&& !$this->useCompress |
1372
|
|
|
&& !$this->saveAs |
1373
|
|
|
&& !$this->rotateBefore |
1374
|
|
|
&& !$this->rotateAfter |
1375
|
|
|
&& !$this->autoRotate |
1376
|
|
|
&& !$this->bgColor |
1377
|
2 |
|
&& ($this->upscale === self::UPSCALE_DEFAULT) |
1378
|
2 |
|
&& !$this->lossy |
1379
|
2 |
|
) { |
1380
|
2 |
|
$this->log("Using original image."); |
1381
|
2 |
|
$this->output($this->pathToImage); |
1382
|
2 |
|
} |
1383
|
2 |
|
|
1384
|
|
|
return $this; |
1385
|
2 |
|
} |
1386
|
|
|
|
1387
|
|
|
|
1388
|
|
|
|
1389
|
|
|
/** |
1390
|
|
|
* Generate filename to save file in cache. |
1391
|
|
|
* |
1392
|
|
|
* @param string $base as optional basepath for storing file. |
1393
|
|
|
* @param boolean $useSubdir use or skip the subdir part when creating the |
1394
|
|
|
* filename. |
1395
|
|
|
* @param string $prefix to add as part of filename |
1396
|
|
|
* |
1397
|
|
|
* @return $this |
1398
|
|
|
*/ |
1399
|
|
|
public function generateFilename($base = null, $useSubdir = true, $prefix = null) |
1400
|
|
|
{ |
1401
|
|
|
$filename = basename($this->pathToImage); |
1402
|
|
|
$cropToFit = $this->cropToFit ? '_cf' : null; |
1403
|
|
|
$fillToFit = $this->fillToFit ? '_ff' : null; |
1404
|
|
|
$crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; |
1405
|
|
|
$crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; |
1406
|
|
|
$scale = $this->scale ? "_s{$this->scale}" : null; |
1407
|
|
|
$bgColor = $this->bgColor ? "_bgc{$this->bgColor}" : null; |
1408
|
|
|
$quality = $this->quality ? "_q{$this->quality}" : null; |
1409
|
|
|
$compress = $this->compress ? "_co{$this->compress}" : null; |
1410
|
|
|
$rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null; |
1411
|
|
|
$rotateAfter = $this->rotateAfter ? "_ra{$this->rotateAfter}" : null; |
1412
|
|
|
$lossy = $this->lossy ? "_l" : null; |
1413
|
|
|
$interlace = $this->interlace ? "_i" : null; |
1414
|
|
|
|
1415
|
|
|
$saveAs = $this->normalizeFileExtension(); |
1416
|
|
|
$saveAs = $saveAs ? "_$saveAs" : null; |
1417
|
|
|
|
1418
|
|
|
$copyStrat = null; |
1419
|
|
|
if ($this->copyStrategy === self::RESIZE) { |
1420
|
|
|
$copyStrat = "_rs"; |
1421
|
|
|
} |
1422
|
|
|
|
1423
|
|
|
$width = $this->newWidth ? '_' . $this->newWidth : null; |
1424
|
|
|
$height = $this->newHeight ? '_' . $this->newHeight : null; |
1425
|
|
|
|
1426
|
|
|
$offset = isset($this->offset) |
1427
|
|
|
? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left'] |
1428
|
|
|
: null; |
1429
|
|
|
|
1430
|
|
|
$crop = $this->crop |
1431
|
|
|
? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] |
1432
|
|
|
: null; |
1433
|
|
|
|
1434
|
|
|
$filters = null; |
1435
|
|
|
if (isset($this->filters)) { |
1436
|
|
|
foreach ($this->filters as $filter) { |
1437
|
|
|
if (is_array($filter)) { |
1438
|
|
|
$filters .= "_f{$filter['id']}"; |
1439
|
|
|
for ($i=1; $i<=$filter['argc']; $i++) { |
1440
|
|
|
$filters .= "-".$filter["arg{$i}"]; |
1441
|
|
|
} |
1442
|
|
|
} |
1443
|
|
|
} |
1444
|
|
|
} |
1445
|
|
|
|
1446
|
|
|
$sharpen = $this->sharpen ? 's' : null; |
1447
|
|
|
$emboss = $this->emboss ? 'e' : null; |
1448
|
|
|
$blur = $this->blur ? 'b' : null; |
1449
|
|
|
$palette = $this->palette ? 'p' : null; |
1450
|
|
|
|
1451
|
|
|
$autoRotate = $this->autoRotate ? 'ar' : null; |
1452
|
|
|
|
1453
|
|
|
$optimize = $this->jpegOptimize ? 'o' : null; |
1454
|
|
|
$optimize .= $this->pngFilter ? 'f' : null; |
1455
|
|
|
$optimize .= $this->pngDeflate ? 'd' : null; |
1456
|
|
|
|
1457
|
|
|
$convolve = null; |
1458
|
|
|
if ($this->convolve) { |
1459
|
|
|
$convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve); |
1460
|
|
|
} |
1461
|
|
|
|
1462
|
|
|
$upscale = null; |
1463
|
|
|
if ($this->upscale !== self::UPSCALE_DEFAULT) { |
1464
|
|
|
$upscale = '_nu'; |
1465
|
|
|
} |
1466
|
|
|
|
1467
|
|
|
$subdir = null; |
1468
|
|
|
if ($useSubdir === true) { |
1469
|
|
|
$subdir = str_replace('/', '-', dirname($this->imageSrc)); |
1470
|
|
|
$subdir = ($subdir == '.') ? '_.' : $subdir; |
1471
|
|
|
$subdir .= '_'; |
1472
|
|
|
} |
1473
|
|
|
|
1474
|
|
|
$file = $prefix . $subdir . $filename . $width . $height |
1475
|
|
|
. $offset . $crop . $cropToFit . $fillToFit |
1476
|
|
|
. $crop_x . $crop_y . $upscale |
1477
|
|
|
. $quality . $filters . $sharpen . $emboss . $blur . $palette |
1478
|
|
|
. $optimize . $compress |
1479
|
|
|
. $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor |
1480
|
|
|
. $convolve . $copyStrat . $lossy . $interlace . $saveAs; |
1481
|
|
|
|
1482
|
|
|
return $this->setTarget($file, $base); |
1483
|
|
|
} |
1484
|
|
|
|
1485
|
|
|
|
1486
|
|
|
|
1487
|
|
|
/** |
1488
|
|
|
* Use cached version of image, if possible. |
1489
|
|
|
* |
1490
|
|
|
* @param boolean $useCache is default true, set to false to avoid using cached object. |
1491
|
|
|
* |
1492
|
|
|
* @return $this |
1493
|
|
|
*/ |
1494
|
|
|
public function useCacheIfPossible($useCache = true) |
1495
|
|
|
{ |
1496
|
|
|
if ($useCache && is_readable($this->cacheFileName)) { |
1497
|
|
|
$fileTime = filemtime($this->pathToImage); |
1498
|
|
|
$cacheTime = filemtime($this->cacheFileName); |
1499
|
|
|
|
1500
|
|
|
if ($fileTime <= $cacheTime) { |
1501
|
|
|
if ($this->useCache) { |
1502
|
|
|
if ($this->verbose) { |
1503
|
|
|
$this->log("Use cached file."); |
1504
|
|
|
$this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); |
1505
|
|
|
} |
1506
|
|
|
$this->output($this->cacheFileName, $this->outputFormat); |
1507
|
|
|
} else { |
1508
|
|
|
$this->log("Cache is valid but ignoring it by intention."); |
1509
|
|
|
} |
1510
|
|
|
} else { |
1511
|
|
|
$this->log("Original file is modified, ignoring cache."); |
1512
|
|
|
} |
1513
|
|
|
} else { |
1514
|
|
|
$this->log("Cachefile does not exists or ignoring it."); |
1515
|
|
|
} |
1516
|
|
|
|
1517
|
|
|
return $this; |
1518
|
|
|
} |
1519
|
|
|
|
1520
|
|
|
|
1521
|
|
|
|
1522
|
|
|
/** |
1523
|
|
|
* Load image from disk. Try to load image without verbose error message, |
1524
|
|
|
* if fail, load again and display error messages. |
1525
|
|
|
* |
1526
|
|
|
* @param string $src of image. |
1527
|
|
|
* @param string $dir as base directory where images are. |
1528
|
|
|
* |
1529
|
|
|
* @return $this |
1530
|
|
|
* |
1531
|
|
|
*/ |
1532
|
|
|
public function load($src = null, $dir = null) |
1533
|
|
|
{ |
1534
|
|
|
if (isset($src)) { |
1535
|
|
|
$this->setSource($src, $dir); |
1536
|
|
|
} |
1537
|
|
|
|
1538
|
|
|
$this->loadImageDetails(); |
1539
|
|
|
|
1540
|
|
|
if ($this->fileType === IMG_WEBP) { |
1541
|
|
|
$this->image = imagecreatefromwebp($this->pathToImage); |
1542
|
|
|
} else { |
1543
|
|
|
$imageAsString = file_get_contents($this->pathToImage); |
1544
|
|
|
$this->image = imagecreatefromstring($imageAsString); |
1545
|
|
|
} |
1546
|
|
|
if ($this->image === false) { |
1547
|
|
|
throw new Exception("Could not load image."); |
1548
|
|
|
} |
1549
|
|
|
|
1550
|
|
|
/* Removed v0.7.7 |
1551
|
|
|
if (image_type_to_mime_type($this->fileType) == 'image/png') { |
1552
|
|
|
$type = $this->getPngType(); |
1553
|
|
|
$hasFewColors = imagecolorstotal($this->image); |
1554
|
|
|
|
1555
|
|
|
if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) { |
1556
|
|
|
if ($this->verbose) { |
1557
|
|
|
$this->log("Handle this image as a palette image."); |
1558
|
|
|
} |
1559
|
|
|
$this->palette = true; |
1560
|
|
|
} |
1561
|
|
|
} |
1562
|
|
|
*/ |
1563
|
|
|
|
1564
|
|
|
if ($this->verbose) { |
1565
|
|
|
$this->log("### Image successfully loaded from file."); |
1566
|
|
|
$this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); |
1567
|
|
|
$this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); |
1568
|
|
|
$this->log(" Number of colors in image = " . $this->colorsTotal($this->image)); |
1569
|
|
|
$index = imagecolortransparent($this->image); |
1570
|
|
|
$this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); |
1571
|
|
|
} |
1572
|
|
|
|
1573
|
|
|
return $this; |
1574
|
|
|
} |
1575
|
|
|
|
1576
|
|
|
|
1577
|
|
|
|
1578
|
|
|
/** |
1579
|
|
|
* Get the type of PNG image. |
1580
|
|
|
* |
1581
|
|
|
* @param string $filename to use instead of default. |
1582
|
|
|
* |
1583
|
|
|
* @return int as the type of the png-image |
1584
|
|
|
* |
1585
|
|
|
*/ |
1586
|
|
|
public function getPngType($filename = null) |
1587
|
|
|
{ |
1588
|
|
|
$filename = $filename ? $filename : $this->pathToImage; |
1589
|
|
|
|
1590
|
|
|
$pngType = ord(file_get_contents($filename, false, null, 25, 1)); |
1591
|
|
|
|
1592
|
|
|
if ($this->verbose) { |
1593
|
|
|
$this->log("Checking png type of: " . $filename); |
1594
|
|
|
$this->log($this->getPngTypeAsString($pngType)); |
1595
|
|
|
} |
1596
|
|
|
|
1597
|
|
|
return $pngType; |
1598
|
|
|
} |
1599
|
|
|
|
1600
|
|
|
|
1601
|
|
|
|
1602
|
|
|
/** |
1603
|
|
|
* Get the type of PNG image as a verbose string. |
1604
|
|
|
* |
1605
|
|
|
* @param integer $type to use, default is to check the type. |
|
|
|
|
1606
|
|
|
* @param string $filename to use instead of default. |
1607
|
|
|
* |
1608
|
|
|
* @return int as the type of the png-image |
1609
|
|
|
* |
1610
|
|
|
*/ |
1611
|
|
|
private function getPngTypeAsString($pngType = null, $filename = null) |
1612
|
|
|
{ |
1613
|
|
|
if ($filename || !$pngType) { |
|
|
|
|
1614
|
|
|
$pngType = $this->getPngType($filename); |
1615
|
|
|
} |
1616
|
|
|
|
1617
|
|
|
$index = imagecolortransparent($this->image); |
1618
|
|
|
$transparent = null; |
1619
|
|
|
if ($index != -1) { |
1620
|
|
|
$transparent = " (transparent)"; |
1621
|
|
|
} |
1622
|
|
|
|
1623
|
|
|
switch ($pngType) { |
1624
|
|
|
|
1625
|
|
|
case self::PNG_GREYSCALE: |
1626
|
|
|
$text = "PNG is type 0, Greyscale$transparent"; |
1627
|
|
|
break; |
1628
|
|
|
|
1629
|
|
|
case self::PNG_RGB: |
1630
|
|
|
$text = "PNG is type 2, RGB$transparent"; |
1631
|
|
|
break; |
1632
|
|
|
|
1633
|
|
|
case self::PNG_RGB_PALETTE: |
1634
|
|
|
$text = "PNG is type 3, RGB with palette$transparent"; |
1635
|
|
|
break; |
1636
|
|
|
|
1637
|
|
|
case self::PNG_GREYSCALE_ALPHA: |
1638
|
|
|
$text = "PNG is type 4, Greyscale with alpha channel"; |
1639
|
|
|
break; |
1640
|
|
|
|
1641
|
|
|
case self::PNG_RGB_ALPHA: |
1642
|
|
|
$text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)"; |
1643
|
|
|
break; |
1644
|
|
|
|
1645
|
|
|
default: |
1646
|
|
|
$text = "PNG is UNKNOWN type, is it really a PNG image?"; |
1647
|
|
|
} |
1648
|
|
|
|
1649
|
|
|
return $text; |
1650
|
|
|
} |
1651
|
|
|
|
1652
|
|
|
|
1653
|
|
|
|
1654
|
|
|
|
1655
|
|
|
/** |
1656
|
|
|
* Calculate number of colors in an image. |
1657
|
|
|
* |
1658
|
|
|
* @param resource $im the image. |
1659
|
|
|
* |
1660
|
|
|
* @return int |
1661
|
|
|
*/ |
1662
|
|
|
private function colorsTotal($im) |
1663
|
|
|
{ |
1664
|
|
|
if (imageistruecolor($im)) { |
1665
|
|
|
$this->log("Colors as true color."); |
1666
|
|
|
$h = imagesy($im); |
1667
|
|
|
$w = imagesx($im); |
1668
|
|
|
$c = array(); |
1669
|
|
|
for ($x=0; $x < $w; $x++) { |
1670
|
|
|
for ($y=0; $y < $h; $y++) { |
1671
|
|
|
@$c['c'.imagecolorat($im, $x, $y)]++; |
|
|
|
|
1672
|
|
|
} |
1673
|
|
|
} |
1674
|
|
|
return count($c); |
1675
|
|
|
} else { |
1676
|
|
|
$this->log("Colors as palette."); |
1677
|
|
|
return imagecolorstotal($im); |
1678
|
|
|
} |
1679
|
|
|
} |
1680
|
|
|
|
1681
|
|
|
|
1682
|
|
|
|
1683
|
|
|
/** |
1684
|
|
|
* Preprocess image before rezising it. |
1685
|
|
|
* |
1686
|
|
|
* @return $this |
1687
|
|
|
*/ |
1688
|
|
|
public function preResize() |
1689
|
|
|
{ |
1690
|
|
|
$this->log("### Pre-process before resizing"); |
1691
|
|
|
|
1692
|
|
|
// Rotate image |
1693
|
|
|
if ($this->rotateBefore) { |
1694
|
|
|
$this->log("Rotating image."); |
1695
|
|
|
$this->rotate($this->rotateBefore, $this->bgColor) |
1696
|
|
|
->reCalculateDimensions(); |
1697
|
|
|
} |
1698
|
|
|
|
1699
|
|
|
// Auto-rotate image |
1700
|
|
|
if ($this->autoRotate) { |
1701
|
|
|
$this->log("Auto rotating image."); |
1702
|
|
|
$this->rotateExif() |
1703
|
|
|
->reCalculateDimensions(); |
1704
|
|
|
} |
1705
|
|
|
|
1706
|
|
|
// Scale the original image before starting |
1707
|
|
|
if (isset($this->scale)) { |
1708
|
|
|
$this->log("Scale by {$this->scale}%"); |
1709
|
|
|
$newWidth = $this->width * $this->scale / 100; |
1710
|
|
|
$newHeight = $this->height * $this->scale / 100; |
1711
|
|
|
$img = $this->CreateImageKeepTransparency($newWidth, $newHeight); |
1712
|
|
|
imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height); |
1713
|
|
|
$this->image = $img; |
1714
|
|
|
$this->width = $newWidth; |
1715
|
|
|
$this->height = $newHeight; |
1716
|
|
|
} |
1717
|
|
|
|
1718
|
|
|
return $this; |
1719
|
|
|
} |
1720
|
|
|
|
1721
|
|
|
|
1722
|
|
|
|
1723
|
|
|
/** |
1724
|
|
|
* Resize or resample the image while resizing. |
1725
|
|
|
* |
1726
|
|
|
* @param int $strategy as CImage::RESIZE or CImage::RESAMPLE |
1727
|
|
|
* |
1728
|
|
|
* @return $this |
1729
|
|
|
*/ |
1730
|
|
|
public function setCopyResizeStrategy($strategy) |
1731
|
|
|
{ |
1732
|
|
|
$this->copyStrategy = $strategy; |
1733
|
|
|
return $this; |
1734
|
|
|
} |
1735
|
|
|
|
1736
|
|
|
|
1737
|
|
|
|
1738
|
|
|
/** |
1739
|
|
|
* Resize and or crop the image. |
1740
|
|
|
* |
1741
|
|
|
* @return void |
1742
|
|
|
*/ |
1743
|
|
|
public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) |
1744
|
|
|
{ |
1745
|
|
|
if($this->copyStrategy == self::RESIZE) { |
1746
|
|
|
$this->log("Copy by resize"); |
1747
|
|
|
imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); |
1748
|
|
|
} else { |
1749
|
|
|
$this->log("Copy by resample"); |
1750
|
|
|
imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); |
1751
|
|
|
} |
1752
|
|
|
} |
1753
|
|
|
|
1754
|
|
|
|
1755
|
|
|
|
1756
|
|
|
/** |
1757
|
|
|
* Resize and or crop the image. |
1758
|
|
|
* |
1759
|
|
|
* @return $this |
1760
|
|
|
*/ |
1761
|
|
|
public function resize() |
1762
|
|
|
{ |
1763
|
|
|
|
1764
|
|
|
$this->log("### Starting to Resize()"); |
1765
|
|
|
$this->log("Upscale = '$this->upscale'"); |
1766
|
|
|
|
1767
|
|
|
// Only use a specified area of the image, $this->offset is defining the area to use |
1768
|
|
|
if (isset($this->offset)) { |
1769
|
|
|
|
1770
|
|
|
$this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}"); |
1771
|
|
|
$img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']); |
1772
|
|
|
imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']); |
1773
|
|
|
$this->image = $img; |
1774
|
|
|
$this->width = $this->offset['width']; |
1775
|
|
|
$this->height = $this->offset['height']; |
1776
|
|
|
} |
1777
|
|
|
|
1778
|
|
|
if ($this->crop) { |
|
|
|
|
1779
|
|
|
|
1780
|
|
|
// Do as crop, take only part of image |
1781
|
|
|
$this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}"); |
1782
|
|
|
$img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']); |
1783
|
|
|
imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']); |
1784
|
|
|
$this->image = $img; |
1785
|
|
|
$this->width = $this->crop['width']; |
1786
|
|
|
$this->height = $this->crop['height']; |
1787
|
|
|
} |
1788
|
|
|
|
1789
|
|
|
if (!$this->upscale) { |
1790
|
|
|
// Consider rewriting the no-upscale code to fit within this if-statement, |
1791
|
|
|
// likely to be more readable code. |
1792
|
|
|
// The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch |
1793
|
|
|
} |
1794
|
|
|
|
1795
|
|
|
if ($this->cropToFit) { |
1796
|
|
|
|
1797
|
|
|
// Resize by crop to fit |
1798
|
|
|
$this->log("Resizing using strategy - Crop to fit"); |
1799
|
|
|
|
1800
|
|
|
if (!$this->upscale |
1801
|
|
|
&& ($this->width < $this->newWidth || $this->height < $this->newHeight)) { |
1802
|
|
|
$this->log("Resizing - smaller image, do not upscale."); |
1803
|
|
|
|
1804
|
|
|
$posX = 0; |
1805
|
|
|
$posY = 0; |
1806
|
|
|
$cropX = 0; |
1807
|
|
|
$cropY = 0; |
1808
|
|
|
|
1809
|
|
|
if ($this->newWidth > $this->width) { |
1810
|
|
|
$posX = round(($this->newWidth - $this->width) / 2); |
1811
|
|
|
} |
1812
|
|
|
if ($this->newWidth < $this->width) { |
1813
|
|
|
$cropX = round(($this->width/2) - ($this->newWidth/2)); |
1814
|
|
|
} |
1815
|
|
|
|
1816
|
|
|
if ($this->newHeight > $this->height) { |
1817
|
|
|
$posY = round(($this->newHeight - $this->height) / 2); |
1818
|
|
|
} |
1819
|
|
|
if ($this->newHeight < $this->height) { |
1820
|
|
|
$cropY = round(($this->height/2) - ($this->newHeight/2)); |
1821
|
|
|
} |
1822
|
|
|
$this->log(" cwidth: $this->cropWidth"); |
1823
|
|
|
$this->log(" cheight: $this->cropHeight"); |
1824
|
|
|
$this->log(" nwidth: $this->newWidth"); |
1825
|
|
|
$this->log(" nheight: $this->newHeight"); |
1826
|
|
|
$this->log(" width: $this->width"); |
1827
|
|
|
$this->log(" height: $this->height"); |
1828
|
|
|
$this->log(" posX: $posX"); |
1829
|
|
|
$this->log(" posY: $posY"); |
1830
|
|
|
$this->log(" cropX: $cropX"); |
1831
|
|
|
$this->log(" cropY: $cropY"); |
1832
|
|
|
|
1833
|
|
|
$imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); |
1834
|
|
|
imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); |
1835
|
|
|
} else { |
1836
|
|
|
$cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); |
1837
|
|
|
$cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); |
1838
|
|
|
$imgPreCrop = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight); |
1839
|
|
|
$imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); |
1840
|
|
|
$this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height); |
1841
|
|
|
imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight); |
1842
|
|
|
} |
1843
|
|
|
|
1844
|
|
|
$this->image = $imageResized; |
1845
|
|
|
$this->width = $this->newWidth; |
1846
|
|
|
$this->height = $this->newHeight; |
1847
|
|
|
|
1848
|
|
|
} elseif ($this->fillToFit) { |
1849
|
|
|
|
1850
|
|
|
// Resize by fill to fit |
1851
|
|
|
$this->log("Resizing using strategy - Fill to fit"); |
1852
|
|
|
|
1853
|
|
|
$posX = 0; |
1854
|
|
|
$posY = 0; |
1855
|
|
|
|
1856
|
|
|
$ratioOrig = $this->width / $this->height; |
1857
|
|
|
$ratioNew = $this->newWidth / $this->newHeight; |
1858
|
|
|
|
1859
|
|
|
// Check ratio for landscape or portrait |
1860
|
|
|
if ($ratioOrig < $ratioNew) { |
1861
|
|
|
$posX = round(($this->newWidth - $this->fillWidth) / 2); |
1862
|
|
|
} else { |
1863
|
|
|
$posY = round(($this->newHeight - $this->fillHeight) / 2); |
1864
|
|
|
} |
1865
|
|
|
|
1866
|
|
|
if (!$this->upscale |
1867
|
|
|
&& ($this->width < $this->newWidth && $this->height < $this->newHeight) |
1868
|
|
|
) { |
1869
|
|
|
|
1870
|
|
|
$this->log("Resizing - smaller image, do not upscale."); |
1871
|
|
|
$posX = round(($this->newWidth - $this->width) / 2); |
1872
|
|
|
$posY = round(($this->newHeight - $this->height) / 2); |
1873
|
|
|
$imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); |
1874
|
|
|
imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height); |
1875
|
|
|
|
1876
|
|
|
} else { |
1877
|
|
|
$imgPreFill = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight); |
1878
|
|
|
$imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); |
1879
|
|
|
$this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height); |
1880
|
|
|
imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); |
1881
|
|
|
} |
1882
|
|
|
|
1883
|
|
|
$this->image = $imageResized; |
1884
|
|
|
$this->width = $this->newWidth; |
1885
|
|
|
$this->height = $this->newHeight; |
1886
|
|
|
|
1887
|
|
|
} elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) { |
1888
|
|
|
|
1889
|
|
|
// Resize it |
1890
|
|
|
$this->log("Resizing, new height and/or width"); |
1891
|
|
|
|
1892
|
|
|
if (!$this->upscale |
1893
|
|
|
&& ($this->width < $this->newWidth || $this->height < $this->newHeight) |
1894
|
|
|
) { |
1895
|
|
|
$this->log("Resizing - smaller image, do not upscale."); |
1896
|
|
|
|
1897
|
|
|
if (!$this->keepRatio) { |
1898
|
|
|
$this->log("Resizing - stretch to fit selected."); |
1899
|
|
|
|
1900
|
|
|
$posX = 0; |
1901
|
|
|
$posY = 0; |
1902
|
|
|
$cropX = 0; |
1903
|
|
|
$cropY = 0; |
1904
|
|
|
|
1905
|
|
|
if ($this->newWidth > $this->width && $this->newHeight > $this->height) { |
1906
|
|
|
$posX = round(($this->newWidth - $this->width) / 2); |
1907
|
|
|
$posY = round(($this->newHeight - $this->height) / 2); |
1908
|
|
|
} elseif ($this->newWidth > $this->width) { |
1909
|
|
|
$posX = round(($this->newWidth - $this->width) / 2); |
1910
|
|
|
$cropY = round(($this->height - $this->newHeight) / 2); |
1911
|
|
|
} elseif ($this->newHeight > $this->height) { |
1912
|
|
|
$posY = round(($this->newHeight - $this->height) / 2); |
1913
|
|
|
$cropX = round(($this->width - $this->newWidth) / 2); |
1914
|
|
|
} |
1915
|
|
|
|
1916
|
|
|
$imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); |
1917
|
|
|
imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); |
1918
|
|
|
$this->image = $imageResized; |
1919
|
|
|
$this->width = $this->newWidth; |
1920
|
|
|
$this->height = $this->newHeight; |
1921
|
|
|
} |
1922
|
|
|
} else { |
1923
|
|
|
$imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); |
1924
|
|
|
$this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height); |
1925
|
|
|
$this->image = $imageResized; |
1926
|
|
|
$this->width = $this->newWidth; |
1927
|
|
|
$this->height = $this->newHeight; |
1928
|
|
|
} |
1929
|
|
|
} |
1930
|
|
|
|
1931
|
|
|
return $this; |
1932
|
|
|
} |
1933
|
|
|
|
1934
|
|
|
|
1935
|
|
|
|
1936
|
|
|
/** |
1937
|
|
|
* Postprocess image after rezising image. |
1938
|
|
|
* |
1939
|
|
|
* @return $this |
1940
|
|
|
*/ |
1941
|
|
|
public function postResize() |
1942
|
|
|
{ |
1943
|
|
|
$this->log("### Post-process after resizing"); |
1944
|
|
|
|
1945
|
|
|
// Rotate image |
1946
|
|
|
if ($this->rotateAfter) { |
1947
|
|
|
$this->log("Rotating image."); |
1948
|
|
|
$this->rotate($this->rotateAfter, $this->bgColor); |
1949
|
|
|
} |
1950
|
|
|
|
1951
|
|
|
// Apply filters |
1952
|
|
|
if (isset($this->filters) && is_array($this->filters)) { |
1953
|
|
|
|
1954
|
|
|
foreach ($this->filters as $filter) { |
1955
|
|
|
$this->log("Applying filter {$filter['type']}."); |
1956
|
|
|
|
1957
|
|
|
switch ($filter['argc']) { |
1958
|
|
|
|
1959
|
|
|
case 0: |
1960
|
|
|
imagefilter($this->image, $filter['type']); |
1961
|
|
|
break; |
1962
|
|
|
|
1963
|
|
|
case 1: |
1964
|
|
|
imagefilter($this->image, $filter['type'], $filter['arg1']); |
1965
|
|
|
break; |
1966
|
|
|
|
1967
|
|
|
case 2: |
1968
|
|
|
imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']); |
1969
|
|
|
break; |
1970
|
|
|
|
1971
|
|
|
case 3: |
1972
|
|
|
imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']); |
1973
|
|
|
break; |
1974
|
|
|
|
1975
|
|
|
case 4: |
1976
|
|
|
imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']); |
1977
|
|
|
break; |
1978
|
|
|
} |
1979
|
|
|
} |
1980
|
|
|
} |
1981
|
|
|
|
1982
|
|
|
// Convert to palette image |
1983
|
|
|
if ($this->palette) { |
1984
|
|
|
$this->log("Converting to palette image."); |
1985
|
|
|
$this->trueColorToPalette(); |
1986
|
|
|
} |
1987
|
|
|
|
1988
|
|
|
// Blur the image |
1989
|
|
|
if ($this->blur) { |
1990
|
|
|
$this->log("Blur."); |
1991
|
|
|
$this->blurImage(); |
1992
|
|
|
} |
1993
|
|
|
|
1994
|
|
|
// Emboss the image |
1995
|
|
|
if ($this->emboss) { |
1996
|
|
|
$this->log("Emboss."); |
1997
|
|
|
$this->embossImage(); |
1998
|
|
|
} |
1999
|
|
|
|
2000
|
|
|
// Sharpen the image |
2001
|
|
|
if ($this->sharpen) { |
2002
|
|
|
$this->log("Sharpen."); |
2003
|
|
|
$this->sharpenImage(); |
2004
|
|
|
} |
2005
|
|
|
|
2006
|
|
|
// Custom convolution |
2007
|
|
|
if ($this->convolve) { |
2008
|
|
|
//$this->log("Convolve: " . $this->convolve); |
2009
|
|
|
$this->imageConvolution(); |
2010
|
|
|
} |
2011
|
|
|
|
2012
|
|
|
return $this; |
2013
|
|
|
} |
2014
|
|
|
|
2015
|
|
|
|
2016
|
|
|
|
2017
|
|
|
/** |
2018
|
|
|
* Rotate image using angle. |
2019
|
|
|
* |
2020
|
|
|
* @param float $angle to rotate image. |
2021
|
|
|
* @param int $anglebgColor to fill image with if needed. |
|
|
|
|
2022
|
|
|
* |
2023
|
|
|
* @return $this |
2024
|
|
|
*/ |
2025
|
|
|
public function rotate($angle, $bgColor) |
|
|
|
|
2026
|
|
|
{ |
2027
|
|
|
$this->log("Rotate image " . $angle . " degrees with filler color."); |
2028
|
|
|
|
2029
|
|
|
$color = $this->getBackgroundColor(); |
2030
|
|
|
$this->image = imagerotate($this->image, $angle, $color); |
2031
|
|
|
|
2032
|
|
|
$this->width = imagesx($this->image); |
2033
|
|
|
$this->height = imagesy($this->image); |
2034
|
|
|
|
2035
|
|
|
$this->log("New image dimension width x height: " . $this->width . " x " . $this->height); |
2036
|
|
|
|
2037
|
|
|
return $this; |
2038
|
|
|
} |
2039
|
|
|
|
2040
|
|
|
|
2041
|
|
|
|
2042
|
|
|
/** |
2043
|
|
|
* Rotate image using information in EXIF. |
2044
|
|
|
* |
2045
|
|
|
* @return $this |
2046
|
|
|
*/ |
2047
|
|
|
public function rotateExif() |
2048
|
|
|
{ |
2049
|
|
|
if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) { |
2050
|
|
|
$this->log("Autorotate ignored, EXIF not supported by this filetype."); |
2051
|
|
|
return $this; |
2052
|
|
|
} |
2053
|
|
|
|
2054
|
|
|
$exif = exif_read_data($this->pathToImage); |
2055
|
|
|
|
2056
|
|
|
if (!empty($exif['Orientation'])) { |
2057
|
|
|
switch ($exif['Orientation']) { |
2058
|
|
|
case 3: |
2059
|
|
|
$this->log("Autorotate 180."); |
2060
|
|
|
$this->rotate(180, $this->bgColor); |
2061
|
|
|
break; |
2062
|
|
|
|
2063
|
|
|
case 6: |
2064
|
|
|
$this->log("Autorotate -90."); |
2065
|
|
|
$this->rotate(-90, $this->bgColor); |
2066
|
|
|
break; |
2067
|
|
|
|
2068
|
|
|
case 8: |
2069
|
|
|
$this->log("Autorotate 90."); |
2070
|
|
|
$this->rotate(90, $this->bgColor); |
2071
|
|
|
break; |
2072
|
|
|
|
2073
|
|
|
default: |
2074
|
|
|
$this->log("Autorotate ignored, unknown value as orientation."); |
2075
|
|
|
} |
2076
|
|
|
} else { |
2077
|
|
|
$this->log("Autorotate ignored, no orientation in EXIF."); |
2078
|
|
|
} |
2079
|
|
|
|
2080
|
|
|
return $this; |
2081
|
|
|
} |
2082
|
|
|
|
2083
|
|
|
|
2084
|
|
|
|
2085
|
|
|
/** |
2086
|
|
|
* Convert true color image to palette image, keeping alpha. |
2087
|
|
|
* http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library |
2088
|
|
|
* |
2089
|
|
|
* @return void |
2090
|
|
|
*/ |
2091
|
|
|
public function trueColorToPalette() |
2092
|
|
|
{ |
2093
|
|
|
$img = imagecreatetruecolor($this->width, $this->height); |
2094
|
|
|
$bga = imagecolorallocatealpha($img, 0, 0, 0, 127); |
2095
|
|
|
imagecolortransparent($img, $bga); |
2096
|
|
|
imagefill($img, 0, 0, $bga); |
2097
|
|
|
imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height); |
2098
|
|
|
imagetruecolortopalette($img, false, 255); |
2099
|
|
|
imagesavealpha($img, true); |
2100
|
|
|
|
2101
|
|
|
if (imageistruecolor($this->image)) { |
2102
|
|
|
$this->log("Matching colors with true color image."); |
2103
|
|
|
imagecolormatch($this->image, $img); |
2104
|
|
|
} |
2105
|
|
|
|
2106
|
|
|
$this->image = $img; |
2107
|
|
|
} |
2108
|
|
|
|
2109
|
|
|
|
2110
|
|
|
|
2111
|
|
|
/** |
2112
|
|
|
* Sharpen image using image convolution. |
2113
|
|
|
* |
2114
|
|
|
* @return $this |
2115
|
|
|
*/ |
2116
|
|
|
public function sharpenImage() |
2117
|
|
|
{ |
2118
|
|
|
$this->imageConvolution('sharpen'); |
2119
|
|
|
return $this; |
2120
|
|
|
} |
2121
|
|
|
|
2122
|
|
|
|
2123
|
|
|
|
2124
|
|
|
/** |
2125
|
|
|
* Emboss image using image convolution. |
2126
|
|
|
* |
2127
|
|
|
* @return $this |
2128
|
|
|
*/ |
2129
|
|
|
public function embossImage() |
2130
|
|
|
{ |
2131
|
|
|
$this->imageConvolution('emboss'); |
2132
|
|
|
return $this; |
2133
|
|
|
} |
2134
|
|
|
|
2135
|
|
|
|
2136
|
|
|
|
2137
|
|
|
/** |
2138
|
|
|
* Blur image using image convolution. |
2139
|
|
|
* |
2140
|
|
|
* @return $this |
2141
|
|
|
*/ |
2142
|
|
|
public function blurImage() |
2143
|
|
|
{ |
2144
|
|
|
$this->imageConvolution('blur'); |
2145
|
|
|
return $this; |
2146
|
|
|
} |
2147
|
|
|
|
2148
|
|
|
|
2149
|
|
|
|
2150
|
|
|
/** |
2151
|
|
|
* Create convolve expression and return arguments for image convolution. |
2152
|
|
|
* |
2153
|
|
|
* @param string $expression constant string which evaluates to a list of |
2154
|
|
|
* 11 numbers separated by komma or such a list. |
2155
|
|
|
* |
2156
|
|
|
* @return array as $matrix (3x3), $divisor and $offset |
2157
|
|
|
*/ |
2158
|
|
|
public function createConvolveArguments($expression) |
2159
|
|
|
{ |
2160
|
|
|
// Check of matching constant |
2161
|
|
|
if (isset($this->convolves[$expression])) { |
2162
|
|
|
$expression = $this->convolves[$expression]; |
2163
|
|
|
} |
2164
|
|
|
|
2165
|
|
|
$part = explode(',', $expression); |
2166
|
|
|
$this->log("Creating convolution expressen: $expression"); |
2167
|
|
|
|
2168
|
|
|
// Expect list of 11 numbers, split by , and build up arguments |
2169
|
|
|
if (count($part) != 11) { |
2170
|
|
|
throw new Exception( |
2171
|
|
|
"Missmatch in argument convolve. Expected comma-separated string with |
2172
|
|
|
11 float values. Got $expression." |
2173
|
|
|
); |
2174
|
|
|
} |
2175
|
|
|
|
2176
|
|
|
array_walk($part, function ($item, $key) { |
|
|
|
|
2177
|
|
|
if (!is_numeric($item)) { |
2178
|
|
|
throw new Exception("Argument to convolve expression should be float but is not."); |
2179
|
|
|
} |
2180
|
|
|
}); |
2181
|
2 |
|
|
2182
|
|
|
return array( |
2183
|
2 |
|
array( |
2184
|
|
|
array($part[0], $part[1], $part[2]), |
2185
|
2 |
|
array($part[3], $part[4], $part[5]), |
2186
|
|
|
array($part[6], $part[7], $part[8]), |
2187
|
2 |
|
), |
2188
|
2 |
|
$part[9], |
2189
|
2 |
|
$part[10], |
2190
|
2 |
|
); |
2191
|
|
|
} |
2192
|
2 |
|
|
2193
|
|
|
|
2194
|
|
|
|
2195
|
2 |
|
/** |
2196
|
|
|
* Add custom expressions (or overwrite existing) for image convolution. |
2197
|
|
|
* |
2198
|
2 |
|
* @param array $options Key value array with strings to be converted |
2199
|
|
|
* to convolution expressions. |
2200
|
|
|
* |
2201
|
|
|
* @return $this |
2202
|
|
|
*/ |
2203
|
|
|
public function addConvolveExpressions($options) |
2204
|
|
|
{ |
2205
|
|
|
$this->convolves = array_merge($this->convolves, $options); |
2206
|
|
|
return $this; |
2207
|
|
|
} |
2208
|
|
|
|
2209
|
|
|
|
2210
|
|
|
|
2211
|
|
|
/** |
2212
|
|
|
* Image convolution. |
2213
|
|
|
* |
2214
|
|
|
* @param string $options A string with 11 float separated by comma. |
2215
|
2 |
|
* |
2216
|
|
|
* @return $this |
2217
|
2 |
|
*/ |
2218
|
2 |
|
public function imageConvolution($options = null) |
2219
|
2 |
|
{ |
2220
|
2 |
|
// Use incoming options or use $this. |
2221
|
|
|
$options = $options ? $options : $this->convolve; |
2222
|
2 |
|
|
2223
|
2 |
|
// Treat incoming as string, split by + |
2224
|
2 |
|
$this->log("Convolution with '$options'"); |
2225
|
|
|
$options = explode(":", $options); |
2226
|
2 |
|
|
2227
|
|
|
// Check each option if it matches constant value |
2228
|
|
|
foreach ($options as $option) { |
2229
|
|
|
list($matrix, $divisor, $offset) = $this->createConvolveArguments($option); |
2230
|
|
|
imageconvolution($this->image, $matrix, $divisor, $offset); |
2231
|
|
|
} |
2232
|
|
|
|
2233
|
|
|
return $this; |
2234
|
|
|
} |
2235
|
2 |
|
|
2236
|
|
|
|
2237
|
2 |
|
|
2238
|
2 |
|
/** |
2239
|
2 |
|
* Set default background color between 000000-FFFFFF or if using |
2240
|
2 |
|
* alpha 00000000-FFFFFF7F. |
2241
|
|
|
* |
2242
|
2 |
|
* @param string $color as hex value. |
2243
|
|
|
* |
2244
|
|
|
* @return $this |
2245
|
|
|
*/ |
2246
|
|
|
public function setDefaultBackgroundColor($color) |
2247
|
|
|
{ |
2248
|
|
|
$this->log("Setting default background color to '$color'."); |
2249
|
|
|
|
2250
|
|
|
if (!(strlen($color) == 6 || strlen($color) == 8)) { |
2251
|
|
|
throw new Exception( |
2252
|
|
|
"Background color needs a hex value of 6 or 8 |
2253
|
|
|
digits. 000000-FFFFFF or 00000000-FFFFFF7F. |
2254
|
|
|
Current value was: '$color'." |
2255
|
|
|
); |
2256
|
|
|
} |
2257
|
|
|
|
2258
|
|
|
$red = hexdec(substr($color, 0, 2)); |
2259
|
|
|
$green = hexdec(substr($color, 2, 2)); |
2260
|
|
|
$blue = hexdec(substr($color, 4, 2)); |
2261
|
|
|
|
2262
|
|
|
$alpha = (strlen($color) == 8) |
2263
|
|
|
? hexdec(substr($color, 6, 2)) |
2264
|
|
|
: null; |
2265
|
|
|
|
2266
|
|
|
if (($red < 0 || $red > 255) |
2267
|
|
|
|| ($green < 0 || $green > 255) |
2268
|
|
|
|| ($blue < 0 || $blue > 255) |
2269
|
|
|
|| ($alpha < 0 || $alpha > 127) |
2270
|
|
|
) { |
2271
|
|
|
throw new Exception( |
2272
|
|
|
"Background color out of range. Red, green blue |
2273
|
|
|
should be 00-FF and alpha should be 00-7F. |
2274
|
|
|
Current value was: '$color'." |
2275
|
|
|
); |
2276
|
|
|
} |
2277
|
|
|
|
2278
|
|
|
$this->bgColor = strtolower($color); |
2279
|
|
|
$this->bgColorDefault = array( |
2280
|
|
|
'red' => $red, |
2281
|
|
|
'green' => $green, |
2282
|
|
|
'blue' => $blue, |
2283
|
|
|
'alpha' => $alpha |
2284
|
2 |
|
); |
2285
|
|
|
|
2286
|
|
|
return $this; |
2287
|
2 |
|
} |
2288
|
|
|
|
2289
|
|
|
|
2290
|
2 |
|
|
2291
|
|
|
/** |
2292
|
|
|
* Get the background color. |
2293
|
|
|
* |
2294
|
|
|
* @param resource $img the image to work with or null if using $this->image. |
2295
|
|
|
* |
2296
|
|
|
* @return color value or null if no background color is set. |
2297
|
|
|
*/ |
2298
|
|
|
private function getBackgroundColor($img = null) |
2299
|
|
|
{ |
2300
|
|
|
$img = isset($img) ? $img : $this->image; |
2301
|
|
|
|
2302
|
|
|
if ($this->bgColorDefault) { |
|
|
|
|
2303
|
|
|
|
2304
|
|
|
$red = $this->bgColorDefault['red']; |
2305
|
2 |
|
$green = $this->bgColorDefault['green']; |
2306
|
|
|
$blue = $this->bgColorDefault['blue']; |
2307
|
2 |
|
$alpha = $this->bgColorDefault['alpha']; |
2308
|
|
|
|
2309
|
|
|
if ($alpha) { |
2310
|
|
|
$color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha); |
2311
|
2 |
|
} else { |
2312
|
|
|
$color = imagecolorallocate($img, $red, $green, $blue); |
2313
|
|
|
} |
2314
|
|
|
|
2315
|
|
|
return $color; |
2316
|
2 |
|
|
2317
|
|
|
} else { |
2318
|
|
|
return 0; |
2319
|
2 |
|
} |
2320
|
2 |
|
} |
2321
|
|
|
|
2322
|
|
|
|
2323
|
2 |
|
|
2324
|
2 |
|
/** |
2325
|
|
|
* Create a image and keep transparency for png and gifs. |
2326
|
|
|
* |
2327
|
|
|
* @param int $width of the new image. |
2328
|
|
|
* @param int $height of the new image. |
2329
|
|
|
* |
2330
|
|
|
* @return image resource. |
2331
|
|
|
*/ |
2332
|
|
|
private function createImageKeepTransparency($width, $height) |
2333
|
|
|
{ |
2334
|
|
|
$this->log("Creating a new working image width={$width}px, height={$height}px."); |
2335
|
|
|
$img = imagecreatetruecolor($width, $height); |
2336
|
|
|
imagealphablending($img, false); |
2337
|
|
|
imagesavealpha($img, true); |
2338
|
|
|
|
2339
|
|
|
$index = $this->image |
2340
|
|
|
? imagecolortransparent($this->image) |
2341
|
|
|
: -1; |
2342
|
2 |
|
|
2343
|
|
|
if ($index != -1) { |
2344
|
|
|
|
2345
|
|
|
imagealphablending($img, true); |
2346
|
|
|
$transparent = imagecolorsforindex($this->image, $index); |
2347
|
2 |
|
$color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']); |
2348
|
2 |
|
imagefill($img, 0, 0, $color); |
2349
|
2 |
|
$index = imagecolortransparent($img, $color); |
2350
|
|
|
$this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index"); |
2351
|
|
|
|
2352
|
2 |
|
} elseif ($this->bgColorDefault) { |
|
|
|
|
2353
|
2 |
|
|
2354
|
2 |
|
$color = $this->getBackgroundColor($img); |
2355
|
|
|
imagefill($img, 0, 0, $color); |
2356
|
|
|
$this->Log("Filling image with background color."); |
2357
|
2 |
|
} |
2358
|
|
|
|
2359
|
|
|
return $img; |
2360
|
|
|
} |
2361
|
|
|
|
2362
|
|
|
|
2363
|
|
|
|
2364
|
|
|
/** |
2365
|
|
|
* Set optimizing and post-processing options. |
2366
|
|
|
* |
2367
|
|
|
* @param array $options with config for postprocessing with external tools. |
2368
|
|
|
* |
2369
|
|
|
* @return $this |
2370
|
2 |
|
*/ |
2371
|
|
|
public function setPostProcessingOptions($options) |
2372
|
|
|
{ |
2373
|
|
|
if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) { |
2374
|
|
|
$this->jpegOptimizeCmd = $options['jpeg_optimize_cmd']; |
2375
|
|
|
} else { |
2376
|
|
|
$this->jpegOptimizeCmd = null; |
2377
|
|
|
} |
2378
|
|
|
|
2379
|
|
|
if (array_key_exists("png_lossy", $options) |
2380
|
|
|
&& $options['png_lossy'] !== false) { |
2381
|
2 |
|
$this->pngLossy = $options['png_lossy']; |
2382
|
2 |
|
$this->pngLossyCmd = $options['png_lossy_cmd']; |
2383
|
|
|
} else { |
2384
|
2 |
|
$this->pngLossyCmd = null; |
2385
|
|
|
} |
2386
|
|
|
|
2387
|
|
|
if (isset($options['png_filter']) && $options['png_filter']) { |
2388
|
|
|
$this->pngFilterCmd = $options['png_filter_cmd']; |
2389
|
|
|
} else { |
2390
|
|
|
$this->pngFilterCmd = null; |
2391
|
|
|
} |
2392
|
|
|
|
2393
|
|
|
if (isset($options['png_deflate']) && $options['png_deflate']) { |
2394
|
|
|
$this->pngDeflateCmd = $options['png_deflate_cmd']; |
2395
|
2 |
|
} else { |
2396
|
|
|
$this->pngDeflateCmd = null; |
2397
|
|
|
} |
2398
|
|
|
|
2399
|
|
|
return $this; |
2400
|
|
|
} |
2401
|
|
|
|
2402
|
|
|
|
2403
|
|
|
|
2404
|
|
|
/** |
2405
|
|
|
* Find out the type (file extension) for the image to be saved. |
2406
|
|
|
* |
2407
|
|
|
* @return string as image extension. |
2408
|
|
|
*/ |
2409
|
|
|
protected function getTargetImageExtension() |
2410
|
|
|
{ |
2411
|
|
|
// switch on mimetype |
2412
|
|
|
if (isset($this->extension)) { |
2413
|
|
|
return strtolower($this->extension); |
2414
|
|
|
} elseif ($this->fileType === IMG_WEBP) { |
2415
|
|
|
return "webp"; |
2416
|
|
|
} |
2417
|
|
|
|
2418
|
|
|
return substr(image_type_to_extension($this->fileType), 1); |
2419
|
|
|
} |
2420
|
|
|
|
2421
|
|
|
|
2422
|
|
|
|
2423
|
|
|
/** |
2424
|
|
|
* Save image. |
2425
|
|
|
* |
2426
|
|
|
* @param string $src as target filename. |
2427
|
|
|
* @param string $base as base directory where to store images. |
2428
|
|
|
* @param boolean $overwrite or not, default to always overwrite file. |
2429
|
|
|
* |
2430
|
|
|
* @return $this or false if no folder is set. |
2431
|
|
|
*/ |
2432
|
|
|
public function save($src = null, $base = null, $overwrite = true) |
2433
|
|
|
{ |
2434
|
|
|
if (isset($src)) { |
2435
|
|
|
$this->setTarget($src, $base); |
2436
|
|
|
} |
2437
|
|
|
|
2438
|
|
|
if ($overwrite === false && is_file($this->cacheFileName)) { |
2439
|
|
|
$this->Log("Not overwriting file since its already exists and \$overwrite if false."); |
2440
|
|
|
return; |
2441
|
|
|
} |
2442
|
|
|
|
2443
|
|
|
is_writable($this->saveFolder) |
2444
|
|
|
or $this->raiseError('Target directory is not writable.'); |
2445
|
|
|
|
2446
|
|
|
$type = $this->getTargetImageExtension(); |
2447
|
|
|
$this->Log("Saving image as " . $type); |
2448
|
|
|
switch($type) { |
2449
|
|
|
|
2450
|
|
|
case 'jpeg': |
2451
|
|
|
case 'jpg': |
2452
|
|
|
// Set as interlaced progressive JPEG |
2453
|
|
|
if ($this->interlace) { |
2454
|
|
|
$this->Log("Set JPEG image to be interlaced."); |
2455
|
|
|
$res = imageinterlace($this->image, true); |
|
|
|
|
2456
|
|
|
} |
2457
|
|
|
|
2458
|
|
|
$this->Log("Saving image as JPEG to cache using quality = {$this->quality}."); |
2459
|
|
|
imagejpeg($this->image, $this->cacheFileName, $this->quality); |
2460
|
|
|
|
2461
|
|
|
// Use JPEG optimize if defined |
2462
|
|
|
if ($this->jpegOptimizeCmd) { |
2463
|
|
|
if ($this->verbose) { |
2464
|
|
|
clearstatcache(); |
2465
|
|
|
$this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes."); |
2466
|
|
|
} |
2467
|
|
|
$res = array(); |
2468
|
|
|
$cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName"; |
2469
|
|
|
exec($cmd, $res); |
2470
|
|
|
$this->log($cmd); |
2471
|
|
|
$this->log($res); |
|
|
|
|
2472
|
|
|
} |
2473
|
|
|
break; |
2474
|
|
|
|
2475
|
|
|
case 'gif': |
2476
|
|
|
$this->Log("Saving image as GIF to cache."); |
2477
|
|
|
imagegif($this->image, $this->cacheFileName); |
2478
|
|
|
break; |
2479
|
|
|
|
2480
|
|
|
case 'webp': |
2481
|
|
|
$this->Log("Saving image as WEBP to cache using quality = {$this->quality}."); |
2482
|
|
|
imagewebp($this->image, $this->cacheFileName, $this->quality); |
2483
|
|
|
break; |
2484
|
|
|
|
2485
|
|
|
case 'png': |
2486
|
|
|
default: |
2487
|
|
|
$this->Log("Saving image as PNG to cache using compression = {$this->compress}."); |
2488
|
|
|
|
2489
|
|
|
// Turn off alpha blending and set alpha flag |
2490
|
|
|
imagealphablending($this->image, false); |
2491
|
|
|
imagesavealpha($this->image, true); |
2492
|
|
|
imagepng($this->image, $this->cacheFileName, $this->compress); |
2493
|
|
|
|
2494
|
|
|
// Use external program to process lossy PNG, if defined |
2495
|
|
|
$lossyEnabled = $this->pngLossy === true; |
2496
|
|
|
$lossySoftEnabled = $this->pngLossy === null; |
2497
|
|
|
$lossyActiveEnabled = $this->lossy === true; |
2498
|
|
|
if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) { |
2499
|
|
|
if ($this->verbose) { |
2500
|
|
|
clearstatcache(); |
2501
|
|
|
$this->log("Lossy enabled: $lossyEnabled"); |
2502
|
|
|
$this->log("Lossy soft enabled: $lossySoftEnabled"); |
2503
|
|
|
$this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes."); |
2504
|
|
|
} |
2505
|
|
|
$res = array(); |
2506
|
|
|
$cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName"; |
2507
|
|
|
exec($cmd, $res); |
2508
|
|
|
$this->Log($cmd); |
2509
|
|
|
$this->Log($res); |
|
|
|
|
2510
|
|
|
} |
2511
|
|
|
|
2512
|
|
|
// Use external program to filter PNG, if defined |
2513
|
|
|
if ($this->pngFilterCmd) { |
2514
|
|
|
if ($this->verbose) { |
2515
|
|
|
clearstatcache(); |
2516
|
|
|
$this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes."); |
2517
|
|
|
} |
2518
|
|
|
$res = array(); |
2519
|
|
|
$cmd = $this->pngFilterCmd . " $this->cacheFileName"; |
2520
|
|
|
exec($cmd, $res); |
2521
|
|
|
$this->Log($cmd); |
2522
|
|
|
$this->Log($res); |
|
|
|
|
2523
|
|
|
} |
2524
|
|
|
|
2525
|
|
|
// Use external program to deflate PNG, if defined |
2526
|
|
|
if ($this->pngDeflateCmd) { |
2527
|
|
|
if ($this->verbose) { |
2528
|
|
|
clearstatcache(); |
2529
|
|
|
$this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes."); |
2530
|
|
|
} |
2531
|
|
|
$res = array(); |
2532
|
|
|
$cmd = $this->pngDeflateCmd . " $this->cacheFileName"; |
2533
|
|
|
exec($cmd, $res); |
2534
|
|
|
$this->Log($cmd); |
2535
|
|
|
$this->Log($res); |
|
|
|
|
2536
|
|
|
} |
2537
|
|
|
break; |
2538
|
|
|
} |
2539
|
|
|
|
2540
|
|
|
if ($this->verbose) { |
2541
|
|
|
clearstatcache(); |
2542
|
|
|
$this->log("Saved image to cache."); |
2543
|
|
|
$this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); |
2544
|
|
|
$this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); |
2545
|
|
|
$this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); |
2546
|
|
|
$this->log(" Number of colors in image = " . $this->ColorsTotal($this->image)); |
2547
|
|
|
$index = imagecolortransparent($this->image); |
2548
|
|
|
$this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); |
2549
|
|
|
} |
2550
|
|
|
|
2551
|
|
|
return $this; |
2552
|
|
|
} |
2553
|
|
|
|
2554
|
|
|
|
2555
|
|
|
|
2556
|
|
|
/** |
2557
|
|
|
* Convert image from one colorpsace/color profile to sRGB without |
2558
|
|
|
* color profile. |
2559
|
|
|
* |
2560
|
|
|
* @param string $src of image. |
2561
|
|
|
* @param string $dir as base directory where images are. |
2562
|
|
|
* @param string $cache as base directory where to store images. |
2563
|
|
|
* @param string $iccFile filename of colorprofile. |
2564
|
|
|
* @param boolean $useCache or not, default to always use cache. |
2565
|
|
|
* |
2566
|
|
|
* @return string | boolean false if no conversion else the converted |
2567
|
|
|
* filename. |
2568
|
|
|
*/ |
2569
|
|
|
public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true) |
2570
|
|
|
{ |
2571
|
|
|
if ($this->verbose) { |
2572
|
|
|
$this->log("# Converting image to sRGB colorspace."); |
2573
|
|
|
} |
2574
|
|
|
|
2575
|
|
|
if (!class_exists("Imagick")) { |
2576
|
|
|
$this->log(" Ignoring since Imagemagick is not installed."); |
2577
|
|
|
return false; |
2578
|
|
|
} |
2579
|
|
|
|
2580
|
|
|
// Prepare |
2581
|
|
|
$this->setSaveFolder($cache) |
2582
|
|
|
->setSource($src, $dir) |
2583
|
|
|
->generateFilename(null, false, 'srgb_'); |
2584
|
|
|
|
2585
|
|
|
// Check if the cached version is accurate. |
2586
|
|
|
if ($useCache && is_readable($this->cacheFileName)) { |
2587
|
|
|
$fileTime = filemtime($this->pathToImage); |
2588
|
|
|
$cacheTime = filemtime($this->cacheFileName); |
2589
|
|
|
|
2590
|
|
|
if ($fileTime <= $cacheTime) { |
2591
|
|
|
$this->log(" Using cached version: " . $this->cacheFileName); |
2592
|
|
|
return $this->cacheFileName; |
2593
|
|
|
} |
2594
|
|
|
} |
2595
|
|
|
|
2596
|
|
|
// Only covert if cachedir is writable |
2597
|
|
|
if (is_writable($this->saveFolder)) { |
2598
|
|
|
// Load file and check if conversion is needed |
2599
|
|
|
$image = new Imagick($this->pathToImage); |
2600
|
|
|
$colorspace = $image->getImageColorspace(); |
2601
|
|
|
$this->log(" Current colorspace: " . $colorspace); |
2602
|
|
|
|
2603
|
|
|
$profiles = $image->getImageProfiles('*', false); |
2604
|
|
|
$hasICCProfile = (array_search('icc', $profiles) !== false); |
2605
|
|
|
$this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO")); |
2606
|
|
|
|
2607
|
|
|
if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) { |
2608
|
|
|
$this->log(" Converting to sRGB."); |
2609
|
|
|
|
2610
|
|
|
$sRGBicc = file_get_contents($iccFile); |
2611
|
|
|
$image->profileImage('icc', $sRGBicc); |
2612
|
|
|
|
2613
|
|
|
$image->transformImageColorspace(Imagick::COLORSPACE_SRGB); |
2614
|
|
|
$image->writeImage($this->cacheFileName); |
2615
|
|
|
return $this->cacheFileName; |
2616
|
|
|
} |
2617
|
|
|
} |
2618
|
|
|
|
2619
|
|
|
return false; |
2620
|
|
|
} |
2621
|
|
|
|
2622
|
|
|
|
2623
|
|
|
|
2624
|
|
|
/** |
2625
|
|
|
* Create a hard link, as an alias, to the cached file. |
2626
|
|
|
* |
2627
|
7 |
|
* @param string $alias where to store the link, |
2628
|
|
|
* filename without extension. |
2629
|
7 |
|
* |
2630
|
|
|
* @return $this |
2631
|
|
|
*/ |
2632
|
|
|
public function linkToCacheFile($alias) |
2633
|
7 |
|
{ |
2634
|
|
|
if ($alias === null) { |
2635
|
|
|
$this->log("Ignore creating alias."); |
2636
|
|
|
return $this; |
2637
|
|
|
} |
2638
|
|
|
|
2639
|
|
|
if (is_readable($alias)) { |
2640
|
|
|
unlink($alias); |
|
|
|
|
2641
|
|
|
} |
2642
|
|
|
|
2643
|
|
|
$res = link($this->cacheFileName, $alias); |
2644
|
|
|
|
2645
|
|
|
if ($res) { |
2646
|
|
|
$this->log("Created an alias as: $alias"); |
2647
|
|
|
} else { |
2648
|
|
|
$this->log("Failed to create the alias: $alias"); |
2649
|
|
|
} |
2650
|
|
|
|
2651
|
|
|
return $this; |
2652
|
|
|
} |
2653
|
|
|
|
2654
|
|
|
|
2655
|
|
|
|
2656
|
|
|
/** |
2657
|
|
|
* Add HTTP header for output together with image. |
2658
|
|
|
* |
2659
|
|
|
* @param string $type the header type such as "Cache-Control" |
2660
|
|
|
* @param string $value the value to use |
2661
|
|
|
* |
2662
|
|
|
* @return void |
2663
|
|
|
*/ |
2664
|
|
|
public function addHTTPHeader($type, $value) |
2665
|
|
|
{ |
2666
|
|
|
$this->HTTPHeader[$type] = $value; |
2667
|
|
|
} |
2668
|
|
|
|
2669
|
|
|
|
2670
|
|
|
|
2671
|
|
|
/** |
2672
|
|
|
* Output image to browser using caching. |
2673
|
|
|
* |
2674
|
|
|
* @param string $file to read and output, default is to |
2675
|
|
|
* use $this->cacheFileName |
2676
|
|
|
* @param string $format set to json to output file as json |
2677
|
|
|
* object with details |
2678
|
|
|
* |
2679
|
|
|
* @return void |
2680
|
|
|
*/ |
2681
|
|
|
public function output($file = null, $format = null) |
2682
|
|
|
{ |
2683
|
|
|
if (is_null($file)) { |
2684
|
|
|
$file = $this->cacheFileName; |
2685
|
|
|
} |
2686
|
|
|
|
2687
|
|
|
if (is_null($format)) { |
2688
|
|
|
$format = $this->outputFormat; |
2689
|
|
|
} |
2690
|
|
|
|
2691
|
|
|
$this->log("### Output"); |
2692
|
|
|
$this->log("Output format is: $format"); |
2693
|
|
|
|
2694
|
|
|
if (!$this->verbose && $format == 'json') { |
2695
|
|
|
header('Content-type: application/json'); |
2696
|
|
|
echo $this->json($file); |
2697
|
|
|
exit; |
2698
|
|
|
} elseif ($format == 'ascii') { |
2699
|
|
|
header('Content-type: text/plain'); |
2700
|
|
|
echo $this->ascii($file); |
|
|
|
|
2701
|
|
|
exit; |
2702
|
|
|
} |
2703
|
|
|
|
2704
|
|
|
$this->log("Outputting image: $file"); |
2705
|
|
|
|
2706
|
|
|
// Get image modification time |
2707
|
|
|
clearstatcache(); |
2708
|
|
|
$lastModified = filemtime($file); |
2709
|
|
|
$lastModifiedFormat = "D, d M Y H:i:s"; |
2710
|
|
|
$gmdate = gmdate($lastModifiedFormat, $lastModified); |
2711
|
|
|
|
2712
|
|
|
if (!$this->verbose) { |
2713
|
|
|
$header = "Last-Modified: $gmdate GMT"; |
2714
|
|
|
header($header); |
2715
|
|
|
$this->fastTrackCache->addHeader($header); |
2716
|
|
|
$this->fastTrackCache->setLastModified($lastModified); |
2717
|
|
|
} |
2718
|
|
|
|
2719
|
|
|
foreach ($this->HTTPHeader as $key => $val) { |
2720
|
|
|
$header = "$key: $val"; |
2721
|
|
|
header($header); |
2722
|
|
|
$this->fastTrackCache->addHeader($header); |
2723
|
|
|
} |
2724
|
|
|
|
2725
|
|
|
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) |
2726
|
|
|
&& strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) { |
2727
|
|
|
|
2728
|
|
|
if ($this->verbose) { |
2729
|
|
|
$this->log("304 not modified"); |
2730
|
|
|
$this->verboseOutput(); |
2731
|
|
|
exit; |
2732
|
|
|
} |
2733
|
|
|
|
2734
|
|
|
header("HTTP/1.0 304 Not Modified"); |
2735
|
|
|
if (CIMAGE_DEBUG) { |
2736
|
|
|
trace(__CLASS__ . " 304"); |
2737
|
|
|
} |
2738
|
|
|
|
2739
|
|
|
} else { |
2740
|
|
|
|
2741
|
|
|
$this->loadImageDetails($file); |
2742
|
|
|
$mime = $this->getMimeType(); |
2743
|
|
|
$size = filesize($file); |
2744
|
|
|
|
2745
|
|
|
if ($this->verbose) { |
2746
|
|
|
$this->log("Last-Modified: " . $gmdate . " GMT"); |
2747
|
|
|
$this->log("Content-type: " . $mime); |
2748
|
|
|
$this->log("Content-length: " . $size); |
2749
|
|
|
$this->verboseOutput(); |
2750
|
|
|
|
2751
|
|
|
if (is_null($this->verboseFileName)) { |
2752
|
|
|
exit; |
2753
|
|
|
} |
2754
|
|
|
} |
2755
|
|
|
|
2756
|
|
|
$header = "Content-type: $mime"; |
2757
|
|
|
header($header); |
2758
|
|
|
$this->fastTrackCache->addHeaderOnOutput($header); |
2759
|
|
|
|
2760
|
|
|
$header = "Content-length: $size"; |
2761
|
|
|
header($header); |
2762
|
|
|
$this->fastTrackCache->addHeaderOnOutput($header); |
2763
|
|
|
|
2764
|
|
|
$this->fastTrackCache->setSource($file); |
2765
|
|
|
$this->fastTrackCache->writeToCache(); |
2766
|
|
|
if (CIMAGE_DEBUG) { |
2767
|
|
|
trace(__CLASS__ . " 200"); |
2768
|
|
|
} |
2769
|
|
|
readfile($file); |
2770
|
|
|
} |
2771
|
|
|
|
2772
|
|
|
exit; |
2773
|
|
|
} |
2774
|
|
|
|
2775
|
|
|
|
2776
|
|
|
|
2777
|
|
|
/** |
2778
|
|
|
* Create a JSON object from the image details. |
2779
|
|
|
* |
2780
|
|
|
* @param string $file the file to output. |
2781
|
|
|
* |
2782
|
|
|
* @return string json-encoded representation of the image. |
2783
|
|
|
*/ |
2784
|
|
|
public function json($file = null) |
2785
|
|
|
{ |
2786
|
|
|
$file = $file ? $file : $this->cacheFileName; |
2787
|
|
|
|
2788
|
|
|
$details = array(); |
2789
|
|
|
|
2790
|
|
|
clearstatcache(); |
2791
|
|
|
|
2792
|
|
|
$details['src'] = $this->imageSrc; |
2793
|
|
|
$lastModified = filemtime($this->pathToImage); |
2794
|
|
|
$details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); |
2795
|
|
|
|
2796
|
|
|
$details['cache'] = basename($this->cacheFileName); |
2797
|
|
|
$lastModified = filemtime($this->cacheFileName); |
2798
|
|
|
$details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); |
2799
|
|
|
|
2800
|
|
|
$this->load($file); |
2801
|
|
|
|
2802
|
|
|
$details['filename'] = basename($file); |
2803
|
|
|
$details['mimeType'] = $this->getMimeType($this->fileType); |
|
|
|
|
2804
|
|
|
$details['width'] = $this->width; |
2805
|
|
|
$details['height'] = $this->height; |
2806
|
|
|
$details['aspectRatio'] = round($this->width / $this->height, 3); |
2807
|
|
|
$details['size'] = filesize($file); |
2808
|
|
|
$details['colors'] = $this->colorsTotal($this->image); |
2809
|
|
|
$details['includedFiles'] = count(get_included_files()); |
2810
|
|
|
$details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ; |
2811
|
|
|
$details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB"; |
2812
|
|
|
$details['memoryLimit'] = ini_get('memory_limit'); |
2813
|
|
|
|
2814
|
|
|
if (isset($_SERVER['REQUEST_TIME_FLOAT'])) { |
2815
|
|
|
$details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s"; |
2816
|
|
|
} |
2817
|
|
|
|
2818
|
|
|
if ($details['mimeType'] == 'image/png') { |
2819
|
|
|
$details['pngType'] = $this->getPngTypeAsString(null, $file); |
2820
|
|
|
} |
2821
|
|
|
|
2822
|
|
|
$options = null; |
2823
|
|
|
if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) { |
2824
|
|
|
$options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES; |
2825
|
|
|
} |
2826
|
|
|
|
2827
|
|
|
return json_encode($details, $options); |
2828
|
|
|
} |
2829
|
|
|
|
2830
|
|
|
|
2831
|
|
|
|
2832
|
|
|
/** |
2833
|
|
|
* Set options for creating ascii version of image. |
2834
|
|
|
* |
2835
|
|
|
* @param array $options empty to use default or set options to change. |
2836
|
|
|
* |
2837
|
|
|
* @return void. |
|
|
|
|
2838
|
|
|
*/ |
2839
|
|
|
public function setAsciiOptions($options = array()) |
2840
|
|
|
{ |
2841
|
|
|
$this->asciiOptions = $options; |
2842
|
|
|
} |
2843
|
|
|
|
2844
|
|
|
|
2845
|
|
|
|
2846
|
|
|
/** |
2847
|
|
|
* Create an ASCII version from the image details. |
2848
|
|
|
* |
2849
|
|
|
* @param string $file the file to output. |
2850
|
|
|
* |
2851
|
|
|
* @return string ASCII representation of the image. |
2852
|
|
|
*/ |
2853
|
|
|
public function ascii($file = null) |
2854
|
|
|
{ |
2855
|
|
|
$file = $file ? $file : $this->cacheFileName; |
2856
|
|
|
|
2857
|
|
|
$asciiArt = new CAsciiArt(); |
2858
|
|
|
$asciiArt->setOptions($this->asciiOptions); |
2859
|
|
|
return $asciiArt->createFromFile($file); |
2860
|
|
|
} |
2861
|
|
|
|
2862
|
|
|
|
2863
|
|
|
|
2864
|
|
|
/** |
2865
|
|
|
* Log an event if verbose mode. |
2866
|
|
|
* |
2867
|
|
|
* @param string $message to log. |
2868
|
|
|
* |
2869
|
|
|
* @return this |
2870
|
|
|
*/ |
2871
|
|
|
public function log($message) |
2872
|
|
|
{ |
2873
|
|
|
if ($this->verbose) { |
2874
|
|
|
$this->log[] = $message; |
2875
|
|
|
} |
2876
|
|
|
|
2877
|
|
|
return $this; |
2878
|
|
|
} |
2879
|
|
|
|
2880
|
|
|
|
2881
|
|
|
|
2882
|
|
|
/** |
2883
|
|
|
* Do verbose output to a file. |
2884
|
|
|
* |
2885
|
|
|
* @param string $fileName where to write the verbose output. |
2886
|
|
|
* |
2887
|
|
|
* @return void |
2888
|
|
|
*/ |
2889
|
|
|
public function setVerboseToFile($fileName) |
2890
|
|
|
{ |
2891
|
|
|
$this->log("Setting verbose output to file."); |
2892
|
|
|
$this->verboseFileName = $fileName; |
2893
|
|
|
} |
2894
|
|
|
|
2895
|
|
|
|
2896
|
|
|
|
2897
|
|
|
/** |
2898
|
|
|
* Do verbose output and print out the log and the actual images. |
2899
|
|
|
* |
2900
|
|
|
* @return void |
2901
|
|
|
*/ |
2902
|
|
|
private function verboseOutput() |
2903
|
|
|
{ |
2904
|
|
|
$log = null; |
2905
|
|
|
$this->log("### Summary of verbose log"); |
2906
|
|
|
$this->log("As JSON: \n" . $this->json()); |
2907
|
|
|
$this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M"); |
2908
|
|
|
$this->log("Memory limit: " . ini_get('memory_limit')); |
2909
|
|
|
|
2910
|
|
|
$included = get_included_files(); |
2911
|
|
|
$this->log("Included files: " . count($included)); |
2912
|
|
|
|
2913
|
|
|
foreach ($this->log as $val) { |
2914
|
|
|
if (is_array($val)) { |
2915
|
|
|
foreach ($val as $val1) { |
2916
|
|
|
$log .= htmlentities($val1) . '<br/>'; |
2917
|
|
|
} |
2918
|
|
|
} else { |
2919
|
|
|
$log .= htmlentities($val) . '<br/>'; |
2920
|
|
|
} |
2921
|
|
|
} |
2922
|
|
|
|
2923
|
|
|
if (!is_null($this->verboseFileName)) { |
2924
|
|
|
file_put_contents( |
2925
|
|
|
$this->verboseFileName, |
2926
|
|
|
str_replace("<br/>", "\n", $log) |
|
|
|
|
2927
|
|
|
); |
2928
|
|
|
} else { |
2929
|
|
|
echo <<<EOD |
2930
|
|
|
<h1>CImage Verbose Output</h1> |
2931
|
|
|
<pre>{$log}</pre> |
2932
|
|
|
EOD; |
2933
|
|
|
} |
2934
|
|
|
} |
2935
|
|
|
|
2936
|
|
|
|
2937
|
|
|
|
2938
|
|
|
/** |
2939
|
|
|
* Raise error, enables to implement a selection of error methods. |
2940
|
|
|
* |
2941
|
|
|
* @param string $message the error message to display. |
2942
|
|
|
* |
2943
|
|
|
* @return void |
2944
|
|
|
* @throws Exception |
2945
|
|
|
*/ |
2946
|
|
|
private function raiseError($message) |
2947
|
|
|
{ |
2948
|
|
|
throw new Exception($message); |
2949
|
|
|
} |
2950
|
|
|
} |
2951
|
|
|
|
This check looks for type mismatches where the missing type is
false
. This is usually indicative of an error condtion.Consider the follow example
This function either returns a new
DateTime
object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returnedfalse
before passing on the value to another function or method that may not be able to handle afalse
.