Diff: STRATO-apps/wordpress_03/app/wp-includes/class-wp-image-editor-imagick.php
Keine Baseline-Datei – Diff nur gegen leer.
1
-
1
+
<?php
2
+
/**
3
+
* WordPress Imagick Image Editor
4
+
*
5
+
* @package WordPress
6
+
* @subpackage Image_Editor
7
+
*/
8
+
9
+
/**
10
+
* WordPress Image Editor Class for Image Manipulation through Imagick PHP Module
11
+
*
12
+
* @since 3.5.0
13
+
*
14
+
* @see WP_Image_Editor
15
+
*/
16
+
class WP_Image_Editor_Imagick extends WP_Image_Editor {
17
+
/**
18
+
* Imagick object.
19
+
*
20
+
* @var Imagick
21
+
*/
22
+
protected $image;
23
+
24
+
public function __destruct() {
25
+
if ( $this->image instanceof Imagick ) {
26
+
// We don't need the original in memory anymore.
27
+
$this->image->clear();
28
+
$this->image->destroy();
29
+
}
30
+
}
31
+
32
+
/**
33
+
* Checks to see if current environment supports Imagick.
34
+
*
35
+
* We require Imagick 2.2.0 or greater, based on whether the queryFormats()
36
+
* method can be called statically.
37
+
*
38
+
* @since 3.5.0
39
+
*
40
+
* @param array $args
41
+
* @return bool
42
+
*/
43
+
public static function test( $args = array() ) {
44
+
45
+
// First, test Imagick's extension and classes.
46
+
if ( ! extension_loaded( 'imagick' ) || ! class_exists( 'Imagick', false ) || ! class_exists( 'ImagickPixel', false ) ) {
47
+
return false;
48
+
}
49
+
50
+
if ( version_compare( phpversion( 'imagick' ), '2.2.0', '<' ) ) {
51
+
return false;
52
+
}
53
+
54
+
$required_methods = array(
55
+
'clear',
56
+
'destroy',
57
+
'valid',
58
+
'getimage',
59
+
'writeimage',
60
+
'getimageblob',
61
+
'getimagegeometry',
62
+
'getimageformat',
63
+
'setimageformat',
64
+
'setimagecompression',
65
+
'setimagecompressionquality',
66
+
'setimagepage',
67
+
'setoption',
68
+
'scaleimage',
69
+
'cropimage',
70
+
'rotateimage',
71
+
'flipimage',
72
+
'flopimage',
73
+
'readimage',
74
+
'readimageblob',
75
+
);
76
+
77
+
// Now, test for deep requirements within Imagick.
78
+
if ( ! defined( 'imagick::COMPRESSION_JPEG' ) ) {
79
+
return false;
80
+
}
81
+
82
+
$class_methods = array_map( 'strtolower', get_class_methods( 'Imagick' ) );
83
+
if ( array_diff( $required_methods, $class_methods ) ) {
84
+
return false;
85
+
}
86
+
87
+
return true;
88
+
}
89
+
90
+
/**
91
+
* Checks to see if editor supports the mime-type specified.
92
+
*
93
+
* @since 3.5.0
94
+
*
95
+
* @param string $mime_type
96
+
* @return bool
97
+
*/
98
+
public static function supports_mime_type( $mime_type ) {
99
+
$imagick_extension = strtoupper( self::get_extension( $mime_type ) );
100
+
101
+
if ( ! $imagick_extension ) {
102
+
return false;
103
+
}
104
+
105
+
/*
106
+
* setIteratorIndex is optional unless mime is an animated format.
107
+
* Here, we just say no if you are missing it and aren't loading a jpeg.
108
+
*/
109
+
if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) {
110
+
return false;
111
+
}
112
+
113
+
try {
114
+
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
115
+
return ( (bool) @Imagick::queryFormats( $imagick_extension ) );
116
+
} catch ( Exception $e ) {
117
+
return false;
118
+
}
119
+
}
120
+
121
+
/**
122
+
* Loads image from $this->file into new Imagick Object.
123
+
*
124
+
* @since 3.5.0
125
+
*
126
+
* @return true|WP_Error True if loaded; WP_Error on failure.
127
+
*/
128
+
public function load() {
129
+
if ( $this->image instanceof Imagick ) {
130
+
return true;
131
+
}
132
+
133
+
if ( ! is_file( $this->file ) && ! wp_is_stream( $this->file ) ) {
134
+
return new WP_Error( 'error_loading_image', __( 'File does not exist?' ), $this->file );
135
+
}
136
+
137
+
/*
138
+
* Even though Imagick uses less PHP memory than GD, set higher limit
139
+
* for users that have low PHP.ini limits.
140
+
*/
141
+
wp_raise_memory_limit( 'image' );
142
+
143
+
try {
144
+
$this->image = new Imagick();
145
+
$file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) );
146
+
147
+
if ( 'pdf' === $file_extension ) {
148
+
$pdf_loaded = $this->pdf_load_source();
149
+
150
+
if ( is_wp_error( $pdf_loaded ) ) {
151
+
return $pdf_loaded;
152
+
}
153
+
} else {
154
+
if ( wp_is_stream( $this->file ) ) {
155
+
// Due to reports of issues with streams with `Imagick::readImageFile()`, uses `Imagick::readImageBlob()` instead.
156
+
$this->image->readImageBlob( file_get_contents( $this->file ), $this->file );
157
+
} else {
158
+
$this->image->readImage( $this->file );
159
+
}
160
+
}
161
+
162
+
if ( ! $this->image->valid() ) {
163
+
return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
164
+
}
165
+
166
+
// Select the first frame to handle animated images properly.
167
+
if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) {
168
+
$this->image->setIteratorIndex( 0 );
169
+
}
170
+
171
+
if ( 'pdf' === $file_extension ) {
172
+
$this->remove_pdf_alpha_channel();
173
+
}
174
+
175
+
$this->mime_type = $this->get_mime_type( $this->image->getImageFormat() );
176
+
} catch ( Exception $e ) {
177
+
return new WP_Error( 'invalid_image', $e->getMessage(), $this->file );
178
+
}
179
+
180
+
$updated_size = $this->update_size();
181
+
182
+
if ( is_wp_error( $updated_size ) ) {
183
+
return $updated_size;
184
+
}
185
+
186
+
return $this->set_quality();
187
+
}
188
+
189
+
/**
190
+
* Sets Image Compression quality on a 1-100% scale.
191
+
*
192
+
* @since 3.5.0
193
+
* @since 6.8.0 The `$dims` parameter was added.
194
+
*
195
+
* @param int $quality Compression Quality. Range: [1,100]
196
+
* @param array $dims Optional. Image dimensions array with 'width' and 'height' keys.
197
+
* @return true|WP_Error True if set successfully; WP_Error on failure.
198
+
*/
199
+
public function set_quality( $quality = null, $dims = array() ) {
200
+
$quality_result = parent::set_quality( $quality, $dims );
201
+
if ( is_wp_error( $quality_result ) ) {
202
+
return $quality_result;
203
+
} else {
204
+
$quality = $this->get_quality();
205
+
}
206
+
207
+
try {
208
+
switch ( $this->mime_type ) {
209
+
case 'image/jpeg':
210
+
$this->image->setImageCompressionQuality( $quality );
211
+
$this->image->setCompressionQuality( $quality );
212
+
$this->image->setImageCompression( imagick::COMPRESSION_JPEG );
213
+
break;
214
+
case 'image/webp':
215
+
$webp_info = wp_get_webp_info( $this->file );
216
+
217
+
if ( 'lossless' === $webp_info['type'] ) {
218
+
// Use WebP lossless settings.
219
+
$this->image->setImageCompressionQuality( 100 );
220
+
$this->image->setCompressionQuality( 100 );
221
+
$this->image->setOption( 'webp:lossless', 'true' );
222
+
parent::set_quality( 100 );
223
+
} else {
224
+
$this->image->setImageCompressionQuality( $quality );
225
+
$this->image->setCompressionQuality( $quality );
226
+
}
227
+
break;
228
+
case 'image/avif':
229
+
// Set the AVIF encoder to work faster, with minimal impact on image size.
230
+
$this->image->setOption( 'heic:speed', 7 );
231
+
$this->image->setImageCompressionQuality( $quality );
232
+
$this->image->setCompressionQuality( $quality );
233
+
break;
234
+
default:
235
+
$this->image->setImageCompressionQuality( $quality );
236
+
$this->image->setCompressionQuality( $quality );
237
+
}
238
+
} catch ( Exception $e ) {
239
+
return new WP_Error( 'image_quality_error', $e->getMessage() );
240
+
}
241
+
return true;
242
+
}
243
+
244
+
245
+
/**
246
+
* Sets or updates current image size.
247
+
*
248
+
* @since 3.5.0
249
+
*
250
+
* @param int $width
251
+
* @param int $height
252
+
* @return true|WP_Error
253
+
*/
254
+
protected function update_size( $width = null, $height = null ) {
255
+
$size = null;
256
+
if ( ! $width || ! $height ) {
257
+
try {
258
+
$size = $this->image->getImageGeometry();
259
+
} catch ( Exception $e ) {
260
+
return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file );
261
+
}
262
+
}
263
+
264
+
if ( ! $width ) {
265
+
$width = $size['width'];
266
+
}
267
+
268
+
if ( ! $height ) {
269
+
$height = $size['height'];
270
+
}
271
+
272
+
/*
273
+
* If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF and HEIC images
274
+
* are properly sized without affecting previous `getImageGeometry` behavior.
275
+
*/
276
+
if ( ( ! $width || ! $height ) && ( 'image/avif' === $this->mime_type || wp_is_heic_image_mime_type( $this->mime_type ) ) ) {
277
+
$size = wp_getimagesize( $this->file );
278
+
$width = $size[0];
279
+
$height = $size[1];
280
+
}
281
+
282
+
return parent::update_size( $width, $height );
283
+
}
284
+
285
+
/**
286
+
* Sets Imagick time limit.
287
+
*
288
+
* Depending on configuration, Imagick processing may take time.
289
+
*
290
+
* Multiple problems exist if PHP times out before ImageMagick completed:
291
+
* 1. Temporary files aren't cleaned by ImageMagick garbage collection.
292
+
* 2. No clear error is provided.
293
+
* 3. The cause of such timeout can be hard to pinpoint.
294
+
*
295
+
* This function, which is expected to be run before heavy image routines, resolves
296
+
* point 1 above by aligning Imagick's timeout with PHP's timeout, assuming it is set.
297
+
*
298
+
* However seems it introduces more problems than it fixes,
299
+
* see https://core.trac.wordpress.org/ticket/58202.
300
+
*
301
+
* Note:
302
+
* - Imagick resource exhaustion does not issue catchable exceptions (yet).
303
+
* See https://github.com/Imagick/imagick/issues/333.
304
+
* - The resource limit is not saved/restored. It applies to subsequent
305
+
* image operations within the time of the HTTP request.
306
+
*
307
+
* @since 6.2.0
308
+
* @deprecated 6.3.0 No longer used in core.
309
+
*
310
+
* @return int|null The new limit on success, null on failure.
311
+
*/
312
+
public static function set_imagick_time_limit() {
313
+
_deprecated_function( __METHOD__, '6.3.0' );
314
+
315
+
if ( ! defined( 'Imagick::RESOURCETYPE_TIME' ) ) {
316
+
return null;
317
+
}
318
+
319
+
// Returns PHP_FLOAT_MAX if unset.
320
+
$imagick_timeout = Imagick::getResourceLimit( Imagick::RESOURCETYPE_TIME );
321
+
322
+
// Convert to an integer, keeping in mind that: 0 === (int) PHP_FLOAT_MAX.
323
+
$imagick_timeout = $imagick_timeout > PHP_INT_MAX ? PHP_INT_MAX : (int) $imagick_timeout;
324
+
325
+
$php_timeout = (int) ini_get( 'max_execution_time' );
326
+
327
+
if ( $php_timeout > 1 && $php_timeout < $imagick_timeout ) {
328
+
$limit = (float) 0.8 * $php_timeout;
329
+
Imagick::setResourceLimit( Imagick::RESOURCETYPE_TIME, $limit );
330
+
331
+
return $limit;
332
+
}
333
+
}
334
+
335
+
/**
336
+
* Resizes current image.
337
+
*
338
+
* At minimum, either a height or width must be provided.
339
+
* If one of the two is set to null, the resize will
340
+
* maintain aspect ratio according to the provided dimension.
341
+
*
342
+
* @since 3.5.0
343
+
*
344
+
* @param int|null $max_w Image width.
345
+
* @param int|null $max_h Image height.
346
+
* @param bool|array $crop {
347
+
* Optional. Image cropping behavior. If false, the image will be scaled (default).
348
+
* If true, image will be cropped to the specified dimensions using center positions.
349
+
* If an array, the image will be cropped using the array to specify the crop location:
350
+
*
351
+
* @type string $0 The x crop position. Accepts 'left', 'center', or 'right'.
352
+
* @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'.
353
+
* }
354
+
* @return true|WP_Error
355
+
*/
356
+
public function resize( $max_w, $max_h, $crop = false ) {
357
+
if ( ( $this->size['width'] === $max_w ) && ( $this->size['height'] === $max_h ) ) {
358
+
return true;
359
+
}
360
+
361
+
$dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
362
+
if ( ! $dims ) {
363
+
return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) );
364
+
}
365
+
366
+
list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
367
+
368
+
if ( $crop ) {
369
+
return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h );
370
+
}
371
+
372
+
$this->set_quality(
373
+
null,
374
+
array(
375
+
'width' => $dst_w,
376
+
'height' => $dst_h,
377
+
)
378
+
);
379
+
380
+
// Execute the resize.
381
+
$thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
382
+
if ( is_wp_error( $thumb_result ) ) {
383
+
return $thumb_result;
384
+
}
385
+
386
+
return $this->update_size( $dst_w, $dst_h );
387
+
}
388
+
389
+
/**
390
+
* Efficiently resize the current image
391
+
*
392
+
* This is a WordPress specific implementation of Imagick::thumbnailImage(),
393
+
* which resizes an image to given dimensions and removes any associated profiles.
394
+
*
395
+
* @since 4.5.0
396
+
*
397
+
* @param int $dst_w The destination width.
398
+
* @param int $dst_h The destination height.
399
+
* @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'.
400
+
* @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true.
401
+
* @return void|WP_Error
402
+
*/
403
+
protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) {
404
+
$allowed_filters = array(
405
+
'FILTER_POINT',
406
+
'FILTER_BOX',
407
+
'FILTER_TRIANGLE',
408
+
'FILTER_HERMITE',
409
+
'FILTER_HANNING',
410
+
'FILTER_HAMMING',
411
+
'FILTER_BLACKMAN',
412
+
'FILTER_GAUSSIAN',
413
+
'FILTER_QUADRATIC',
414
+
'FILTER_CUBIC',
415
+
'FILTER_CATROM',
416
+
'FILTER_MITCHELL',
417
+
'FILTER_LANCZOS',
418
+
'FILTER_BESSEL',
419
+
'FILTER_SINC',
420
+
);
421
+
422
+
/**
423
+
* Set the filter value if '$filter_name' name is in the allowed list and the related
424
+
* Imagick constant is defined or fall back to the default filter.
425
+
*/
426
+
if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) {
427
+
$filter = constant( 'Imagick::' . $filter_name );
428
+
} else {
429
+
$filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false;
430
+
}
431
+
432
+
/**
433
+
* Filters whether to strip metadata from images when they're resized.
434
+
*
435
+
* This filter only applies when resizing using the Imagick editor since GD
436
+
* always strips profiles by default.
437
+
*
438
+
* @since 4.5.0
439
+
*
440
+
* @param bool $strip_meta Whether to strip image metadata during resizing. Default true.
441
+
*/
442
+
if ( apply_filters( 'image_strip_meta', $strip_meta ) ) {
443
+
$this->strip_meta(); // Fail silently if not supported.
444
+
}
445
+
446
+
try {
447
+
/**
448
+
* Special handling for certain types of PNG images:
449
+
* 1. For PNG images, we need to specify compression settings and remove unneeded chunks.
450
+
* 2. For indexed PNG images, the number of colors must not exceed 256.
451
+
* 3. For indexed PNG images with an alpha channel, the tRNS chunk must be preserved.
452
+
* 4. For indexed PNG images with true alpha transparency (an alpha channel > 1 bit), we need to avoid saving
453
+
* the image using ImageMagick's 'png8' format, because that supports only binary (1 bit) transparency.
454
+
*
455
+
* For #4 we want to check whether the image has a 1-bit alpha channel before resizing, because resizing
456
+
* may cause the number of alpha values to multiply due to antialiasing. If the original image had only a
457
+
* 1-bit alpha channel, then a 1-bit alpha channel should be good enough for the resized images.
458
+
*
459
+
* Perform all the necessary checks before resizing the image and store the results in variables for later use.
460
+
*/
461
+
$is_png = false;
462
+
$is_indexed_png = false;
463
+
$is_indexed_png_with_alpha_channel = false;
464
+
$is_indexed_png_with_true_alpha_transparency = false;
465
+
466
+
if ( 'image/png' === $this->mime_type ) {
467
+
$is_png = true;
468
+
469
+
if (
470
+
is_callable( array( $this->image, 'getImageProperty' ) )
471
+
&& '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' )
472
+
) {
473
+
$is_indexed_png = true;
474
+
475
+
if (
476
+
is_callable( array( $this->image, 'getImageAlphaChannel' ) )
477
+
&& $this->image->getImageAlphaChannel()
478
+
) {
479
+
$is_indexed_png_with_alpha_channel = true;
480
+
481
+
if (
482
+
is_callable( array( $this->image, 'getImageChannelDepth' ) )
483
+
&& defined( 'Imagick::CHANNEL_ALPHA' )
484
+
&& 1 < $this->image->getImageChannelDepth( Imagick::CHANNEL_ALPHA )
485
+
) {
486
+
$is_indexed_png_with_true_alpha_transparency = true;
487
+
}
488
+
}
489
+
}
490
+
}
491
+
492
+
/*
493
+
* To be more efficient, resample large images to 5x the destination size before resizing
494
+
* whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111),
495
+
* unless we would be resampling to a scale smaller than 128x128.
496
+
*/
497
+
if ( is_callable( array( $this->image, 'sampleImage' ) ) ) {
498
+
$resize_ratio = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] );
499
+
$sample_factor = 5;
500
+
501
+
if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) {
502
+
$this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor );
503
+
}
504
+
}
505
+
506
+
/*
507
+
* Use resizeImage() when it's available and a valid filter value is set.
508
+
* Otherwise, fall back to the scaleImage() method for resizing, which
509
+
* results in better image quality over resizeImage() with default filter
510
+
* settings and retains backward compatibility with pre 4.5 functionality.
511
+
*/
512
+
if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) {
513
+
$this->image->setOption( 'filter:support', '2.0' );
514
+
$this->image->resizeImage( $dst_w, $dst_h, $filter, 1 );
515
+
} else {
516
+
$this->image->scaleImage( $dst_w, $dst_h );
517
+
}
518
+
519
+
// Set appropriate quality settings after resizing.
520
+
if ( 'image/jpeg' === $this->mime_type ) {
521
+
if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) {
522
+
$this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 );
523
+
}
524
+
525
+
$this->image->setOption( 'jpeg:fancy-upsampling', 'off' );
526
+
}
527
+
528
+
if ( $is_png ) {
529
+
$this->image->setOption( 'png:compression-filter', '5' );
530
+
$this->image->setOption( 'png:compression-level', '9' );
531
+
$this->image->setOption( 'png:compression-strategy', '1' );
532
+
533
+
// Indexed PNG files get some additional handling.
534
+
// See #63448 for details.
535
+
if ( $is_indexed_png ) {
536
+
537
+
// Check for an alpha channel.
538
+
if ( $is_indexed_png_with_alpha_channel ) {
539
+
$this->image->setOption( 'png:include-chunk', 'tRNS' );
540
+
} else {
541
+
$this->image->setOption( 'png:exclude-chunk', 'all' );
542
+
}
543
+
544
+
$this->image->quantizeImage( 256, $this->image->getColorspace(), 0, false, false );
545
+
546
+
/*
547
+
* If the colorspace is 'gray', use the png8 format to ensure it stays indexed.
548
+
* ImageMagick tends to save grayscale images as grayscale PNGs rather than indexed PNGs,
549
+
* even though grayscale PNGs usually have considerably larger file sizes.
550
+
* But we can force ImageMagick to save the image as an indexed PNG instead,
551
+
* by telling it to use png8 format.
552
+
*
553
+
* Note that we need to first call quantizeImage() before checking getImageColorspace(),
554
+
* because only after calling quantizeImage() will the colorspace be COLORSPACE_GRAY for grayscale images
555
+
* (and we have not found any other way to identify grayscale images).
556
+
*
557
+
* We need to avoid forcing indexed format for images with true alpha transparency,
558
+
* because ImageMagick does not support saving an image with true alpha transparency as an indexed PNG.
559
+
*/
560
+
if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() && ! $is_indexed_png_with_true_alpha_transparency ) {
561
+
// Set the image format to Indexed PNG.
562
+
$this->image->setOption( 'png:format', 'png8' );
563
+
}
564
+
} else {
565
+
$this->image->setOption( 'png:exclude-chunk', 'all' );
566
+
}
567
+
}
568
+
569
+
/*
570
+
* If alpha channel is not defined, set it opaque.
571
+
*
572
+
* Note that Imagick::getImageAlphaChannel() is only available if Imagick
573
+
* has been compiled against ImageMagick version 6.4.0 or newer.
574
+
*/
575
+
if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) )
576
+
&& is_callable( array( $this->image, 'setImageAlphaChannel' ) )
577
+
&& defined( 'Imagick::ALPHACHANNEL_UNDEFINED' )
578
+
&& defined( 'Imagick::ALPHACHANNEL_OPAQUE' )
579
+
) {
580
+
if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) {
581
+
$this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE );
582
+
}
583
+
}
584
+
585
+
// Limit the bit depth of resized images.
586
+
if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) {
587
+
/**
588
+
* Filters the maximum bit depth of resized images.
589
+
*
590
+
* This filter only applies when resizing using the Imagick editor since GD
591
+
* does not support getting or setting bit depth.
592
+
*
593
+
* Use this to adjust the maximum bit depth of resized images.
594
+
*
595
+
* @since 6.8.0
596
+
*
597
+
* @param int $max_depth The maximum bit depth. Default is the input depth.
598
+
* @param int $image_depth The bit depth of the original image.
599
+
*/
600
+
$max_depth = apply_filters( 'image_max_bit_depth', $this->image->getImageDepth(), $this->image->getImageDepth() );
601
+
$this->image->setImageDepth( $max_depth );
602
+
}
603
+
} catch ( Exception $e ) {
604
+
return new WP_Error( 'image_resize_error', $e->getMessage() );
605
+
}
606
+
}
607
+
608
+
/**
609
+
* Create multiple smaller images from a single source.
610
+
*
611
+
* Attempts to create all sub-sizes and returns the meta data at the end. This
612
+
* may result in the server running out of resources. When it fails there may be few
613
+
* "orphaned" images left over as the meta data is never returned and saved.
614
+
*
615
+
* As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates
616
+
* the new images one at a time and allows for the meta data to be saved after
617
+
* each new image is created.
618
+
*
619
+
* @since 3.5.0
620
+
*
621
+
* @param array $sizes {
622
+
* An array of image size data arrays.
623
+
*
624
+
* Either a height or width must be provided.
625
+
* If one of the two is set to null, the resize will
626
+
* maintain aspect ratio according to the provided dimension.
627
+
*
628
+
* @type array ...$0 {
629
+
* Array of height, width values, and whether to crop.
630
+
*
631
+
* @type int $width Image width. Optional if `$height` is specified.
632
+
* @type int $height Image height. Optional if `$width` is specified.
633
+
* @type bool|array $crop Optional. Whether to crop the image. Default false.
634
+
* }
635
+
* }
636
+
* @return array An array of resized images' metadata by size.
637
+
*/
638
+
public function multi_resize( $sizes ) {
639
+
$metadata = array();
640
+
641
+
foreach ( $sizes as $size => $size_data ) {
642
+
$meta = $this->make_subsize( $size_data );
643
+
644
+
if ( ! is_wp_error( $meta ) ) {
645
+
$metadata[ $size ] = $meta;
646
+
}
647
+
}
648
+
649
+
return $metadata;
650
+
}
651
+
652
+
/**
653
+
* Create an image sub-size and return the image meta data value for it.
654
+
*
655
+
* @since 5.3.0
656
+
*
657
+
* @param array $size_data {
658
+
* Array of size data.
659
+
*
660
+
* @type int $width The maximum width in pixels.
661
+
* @type int $height The maximum height in pixels.
662
+
* @type bool|array $crop Whether to crop the image to exact dimensions.
663
+
* }
664
+
* @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta,
665
+
* WP_Error object on error.
666
+
*/
667
+
public function make_subsize( $size_data ) {
668
+
if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
669
+
return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) );
670
+
}
671
+
672
+
$orig_size = $this->size;
673
+
$orig_image = $this->image->getImage();
674
+
675
+
if ( ! isset( $size_data['width'] ) ) {
676
+
$size_data['width'] = null;
677
+
}
678
+
679
+
if ( ! isset( $size_data['height'] ) ) {
680
+
$size_data['height'] = null;
681
+
}
682
+
683
+
if ( ! isset( $size_data['crop'] ) ) {
684
+
$size_data['crop'] = false;
685
+
}
686
+
687
+
if ( ( $this->size['width'] === $size_data['width'] ) && ( $this->size['height'] === $size_data['height'] ) ) {
688
+
return new WP_Error( 'image_subsize_create_error', __( 'The image already has the requested size.' ) );
689
+
}
690
+
691
+
$resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
692
+
693
+
if ( is_wp_error( $resized ) ) {
694
+
$saved = $resized;
695
+
} else {
696
+
$saved = $this->_save( $this->image );
697
+
698
+
$this->image->clear();
699
+
$this->image->destroy();
700
+
$this->image = null;
701
+
}
702
+
703
+
$this->size = $orig_size;
704
+
$this->image = $orig_image;
705
+
706
+
if ( ! is_wp_error( $saved ) ) {
707
+
unset( $saved['path'] );
708
+
}
709
+
710
+
return $saved;
711
+
}
712
+
713
+
/**
714
+
* Crops Image.
715
+
*
716
+
* @since 3.5.0
717
+
*
718
+
* @param int $src_x The start x position to crop from.
719
+
* @param int $src_y The start y position to crop from.
720
+
* @param int $src_w The width to crop.
721
+
* @param int $src_h The height to crop.
722
+
* @param int $dst_w Optional. The destination width.
723
+
* @param int $dst_h Optional. The destination height.
724
+
* @param bool $src_abs Optional. If the source crop points are absolute.
725
+
* @return true|WP_Error
726
+
*/
727
+
public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
728
+
if ( $src_abs ) {
729
+
$src_w -= $src_x;
730
+
$src_h -= $src_y;
731
+
}
732
+
733
+
try {
734
+
$this->image->cropImage( $src_w, $src_h, $src_x, $src_y );
735
+
$this->image->setImagePage( $src_w, $src_h, 0, 0 );
736
+
737
+
if ( $dst_w || $dst_h ) {
738
+
/*
739
+
* If destination width/height isn't specified,
740
+
* use same as width/height from source.
741
+
*/
742
+
if ( ! $dst_w ) {
743
+
$dst_w = $src_w;
744
+
}
745
+
if ( ! $dst_h ) {
746
+
$dst_h = $src_h;
747
+
}
748
+
749
+
$thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
750
+
if ( is_wp_error( $thumb_result ) ) {
751
+
return $thumb_result;
752
+
}
753
+
754
+
return $this->update_size();
755
+
}
756
+
} catch ( Exception $e ) {
757
+
return new WP_Error( 'image_crop_error', $e->getMessage() );
758
+
}
759
+
760
+
return $this->update_size();
761
+
}
762
+
763
+
/**
764
+
* Rotates current image counter-clockwise by $angle.
765
+
*
766
+
* @since 3.5.0
767
+
*
768
+
* @param float $angle
769
+
* @return true|WP_Error
770
+
*/
771
+
public function rotate( $angle ) {
772
+
/**
773
+
* $angle is 360-$angle because Imagick rotates clockwise
774
+
* (GD rotates counter-clockwise)
775
+
*/
776
+
try {
777
+
$this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle );
778
+
779
+
// Normalize EXIF orientation data so that display is consistent across devices.
780
+
if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
781
+
$this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
782
+
}
783
+
784
+
// Since this changes the dimensions of the image, update the size.
785
+
$result = $this->update_size();
786
+
if ( is_wp_error( $result ) ) {
787
+
return $result;
788
+
}
789
+
790
+
$this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 );
791
+
} catch ( Exception $e ) {
792
+
return new WP_Error( 'image_rotate_error', $e->getMessage() );
793
+
}
794
+
795
+
return true;
796
+
}
797
+
798
+
/**
799
+
* Flips current image.
800
+
*
801
+
* @since 3.5.0
802
+
*
803
+
* @param bool $horz Flip along Horizontal Axis
804
+
* @param bool $vert Flip along Vertical Axis
805
+
* @return true|WP_Error
806
+
*/
807
+
public function flip( $horz, $vert ) {
808
+
try {
809
+
if ( $horz ) {
810
+
$this->image->flipImage();
811
+
}
812
+
813
+
if ( $vert ) {
814
+
$this->image->flopImage();
815
+
}
816
+
817
+
// Normalize EXIF orientation data so that display is consistent across devices.
818
+
if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
819
+
$this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
820
+
}
821
+
} catch ( Exception $e ) {
822
+
return new WP_Error( 'image_flip_error', $e->getMessage() );
823
+
}
824
+
825
+
return true;
826
+
}
827
+
828
+
/**
829
+
* Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
830
+
*
831
+
* As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only
832
+
* if EXIF Orientation can be reset afterwards.
833
+
*
834
+
* @since 5.3.0
835
+
*
836
+
* @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation.
837
+
* WP_Error if error while rotating.
838
+
*/
839
+
public function maybe_exif_rotate() {
840
+
if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
841
+
return parent::maybe_exif_rotate();
842
+
} else {
843
+
return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) );
844
+
}
845
+
}
846
+
847
+
/**
848
+
* Saves current image to file.
849
+
*
850
+
* @since 3.5.0
851
+
* @since 6.0.0 The `$filesize` value was added to the returned array.
852
+
*
853
+
* @param string $destfilename Optional. Destination filename. Default null.
854
+
* @param string $mime_type Optional. The mime-type. Default null.
855
+
* @return array|WP_Error {
856
+
* Array on success or WP_Error if the file failed to save.
857
+
*
858
+
* @type string $path Path to the image file.
859
+
* @type string $file Name of the image file.
860
+
* @type int $width Image width.
861
+
* @type int $height Image height.
862
+
* @type string $mime-type The mime type of the image.
863
+
* @type int $filesize File size of the image.
864
+
* }
865
+
*/
866
+
public function save( $destfilename = null, $mime_type = null ) {
867
+
$saved = $this->_save( $this->image, $destfilename, $mime_type );
868
+
869
+
if ( ! is_wp_error( $saved ) ) {
870
+
$this->file = $saved['path'];
871
+
$this->mime_type = $saved['mime-type'];
872
+
873
+
try {
874
+
$this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) );
875
+
} catch ( Exception $e ) {
876
+
return new WP_Error( 'image_save_error', $e->getMessage(), $this->file );
877
+
}
878
+
}
879
+
880
+
return $saved;
881
+
}
882
+
883
+
/**
884
+
* Removes PDF alpha after it's been read.
885
+
*
886
+
* @since 6.4.0
887
+
*/
888
+
protected function remove_pdf_alpha_channel() {
889
+
$version = Imagick::getVersion();
890
+
// Remove alpha channel if possible to avoid black backgrounds for Ghostscript >= 9.14. RemoveAlphaChannel added in ImageMagick 6.7.5.
891
+
if ( $version['versionNumber'] >= 0x675 ) {
892
+
try {
893
+
// Imagick::ALPHACHANNEL_REMOVE mapped to RemoveAlphaChannel in PHP imagick 3.2.0b2.
894
+
$this->image->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 12 );
895
+
} catch ( Exception $e ) {
896
+
return new WP_Error( 'pdf_alpha_process_failed', $e->getMessage() );
897
+
}
898
+
}
899
+
}
900
+
901
+
/**
902
+
* @since 3.5.0
903
+
* @since 6.0.0 The `$filesize` value was added to the returned array.
904
+
*
905
+
* @param Imagick $image
906
+
* @param string $filename
907
+
* @param string $mime_type
908
+
* @return array|WP_Error {
909
+
* Array on success or WP_Error if the file failed to save.
910
+
*
911
+
* @type string $path Path to the image file.
912
+
* @type string $file Name of the image file.
913
+
* @type int $width Image width.
914
+
* @type int $height Image height.
915
+
* @type string $mime-type The mime type of the image.
916
+
* @type int $filesize File size of the image.
917
+
* }
918
+
*/
919
+
protected function _save( $image, $filename = null, $mime_type = null ) {
920
+
list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
921
+
922
+
if ( ! $filename ) {
923
+
$filename = $this->generate_filename( null, null, $extension );
924
+
}
925
+
926
+
try {
927
+
// Store initial format.
928
+
$orig_format = $this->image->getImageFormat();
929
+
930
+
$this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
931
+
} catch ( Exception $e ) {
932
+
return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
933
+
}
934
+
935
+
if ( method_exists( $this->image, 'setInterlaceScheme' )
936
+
&& method_exists( $this->image, 'getInterlaceScheme' )
937
+
&& defined( 'Imagick::INTERLACE_PLANE' )
938
+
) {
939
+
$orig_interlace = $this->image->getInterlaceScheme();
940
+
941
+
/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
942
+
if ( apply_filters( 'image_save_progressive', false, $mime_type ) ) {
943
+
$this->image->setInterlaceScheme( Imagick::INTERLACE_PLANE ); // True - line interlace output.
944
+
} else {
945
+
$this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); // False - no interlace output.
946
+
}
947
+
}
948
+
949
+
$write_image_result = $this->write_image( $this->image, $filename );
950
+
if ( is_wp_error( $write_image_result ) ) {
951
+
return $write_image_result;
952
+
}
953
+
954
+
try {
955
+
// Reset original format.
956
+
$this->image->setImageFormat( $orig_format );
957
+
958
+
if ( isset( $orig_interlace ) ) {
959
+
$this->image->setInterlaceScheme( $orig_interlace );
960
+
}
961
+
} catch ( Exception $e ) {
962
+
return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
963
+
}
964
+
965
+
// Set correct file permissions.
966
+
$stat = stat( dirname( $filename ) );
967
+
$perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits.
968
+
chmod( $filename, $perms );
969
+
970
+
return array(
971
+
'path' => $filename,
972
+
/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
973
+
'file' => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
974
+
'width' => $this->size['width'],
975
+
'height' => $this->size['height'],
976
+
'mime-type' => $mime_type,
977
+
'filesize' => wp_filesize( $filename ),
978
+
);
979
+
}
980
+
981
+
/**
982
+
* Writes an image to a file or stream.
983
+
*
984
+
* @since 5.6.0
985
+
*
986
+
* @param Imagick $image
987
+
* @param string $filename The destination filename or stream URL.
988
+
* @return true|WP_Error
989
+
*/
990
+
private function write_image( $image, $filename ) {
991
+
if ( wp_is_stream( $filename ) ) {
992
+
/*
993
+
* Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead.
994
+
* Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php
995
+
*/
996
+
if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) {
997
+
return new WP_Error(
998
+
'image_save_error',
999
+
sprintf(
1000
+
/* translators: %s: PHP function name. */
1001
+
__( '%s failed while writing image to stream.' ),
1002
+
'<code>file_put_contents()</code>'
1003
+
),
1004
+
$filename
1005
+
);
1006
+
} else {
1007
+
return true;
1008
+
}
1009
+
} else {
1010
+
$dirname = dirname( $filename );
1011
+
1012
+
if ( ! wp_mkdir_p( $dirname ) ) {
1013
+
return new WP_Error(
1014
+
'image_save_error',
1015
+
sprintf(
1016
+
/* translators: %s: Directory path. */
1017
+
__( 'Unable to create directory %s. Is its parent directory writable by the server?' ),
1018
+
esc_html( $dirname )
1019
+
)
1020
+
);
1021
+
}
1022
+
1023
+
try {
1024
+
return $image->writeImage( $filename );
1025
+
} catch ( Exception $e ) {
1026
+
return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
1027
+
}
1028
+
}
1029
+
}
1030
+
1031
+
/**
1032
+
* Streams current image to browser.
1033
+
*
1034
+
* @since 3.5.0
1035
+
*
1036
+
* @param string $mime_type The mime type of the image.
1037
+
* @return true|WP_Error True on success, WP_Error object on failure.
1038
+
*/
1039
+
public function stream( $mime_type = null ) {
1040
+
list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type );
1041
+
1042
+
try {
1043
+
// Temporarily change format for stream.
1044
+
$this->image->setImageFormat( strtoupper( $extension ) );
1045
+
1046
+
// Output stream of image content.
1047
+
header( "Content-Type: $mime_type" );
1048
+
print $this->image->getImageBlob();
1049
+
1050
+
// Reset image to original format.
1051
+
$this->image->setImageFormat( $this->get_extension( $this->mime_type ) );
1052
+
} catch ( Exception $e ) {
1053
+
return new WP_Error( 'image_stream_error', $e->getMessage() );
1054
+
}
1055
+
1056
+
return true;
1057
+
}
1058
+
1059
+
/**
1060
+
* Strips all image meta except color profiles from an image.
1061
+
*
1062
+
* @since 4.5.0
1063
+
*
1064
+
* @return true|WP_Error True if stripping metadata was successful. WP_Error object on error.
1065
+
*/
1066
+
protected function strip_meta() {
1067
+
1068
+
if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) {
1069
+
return new WP_Error(
1070
+
'image_strip_meta_error',
1071
+
sprintf(
1072
+
/* translators: %s: ImageMagick method name. */
1073
+
__( '%s is required to strip image meta.' ),
1074
+
'<code>Imagick::getImageProfiles()</code>'
1075
+
)
1076
+
);
1077
+
}
1078
+
1079
+
if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) {
1080
+
return new WP_Error(
1081
+
'image_strip_meta_error',
1082
+
sprintf(
1083
+
/* translators: %s: ImageMagick method name. */
1084
+
__( '%s is required to strip image meta.' ),
1085
+
'<code>Imagick::removeImageProfile()</code>'
1086
+
)
1087
+
);
1088
+
}
1089
+
1090
+
/*
1091
+
* Protect a few profiles from being stripped for the following reasons:
1092
+
*
1093
+
* - icc: Color profile information
1094
+
* - icm: Color profile information
1095
+
* - iptc: Copyright data
1096
+
* - exif: Orientation data
1097
+
* - xmp: Rights usage data
1098
+
*/
1099
+
$protected_profiles = array(
1100
+
'icc',
1101
+
'icm',
1102
+
'iptc',
1103
+
'exif',
1104
+
'xmp',
1105
+
);
1106
+
1107
+
try {
1108
+
// Strip profiles.
1109
+
foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) {
1110
+
if ( ! in_array( $key, $protected_profiles, true ) ) {
1111
+
$this->image->removeImageProfile( $key );
1112
+
}
1113
+
}
1114
+
} catch ( Exception $e ) {
1115
+
return new WP_Error( 'image_strip_meta_error', $e->getMessage() );
1116
+
}
1117
+
1118
+
return true;
1119
+
}
1120
+
1121
+
/**
1122
+
* Sets up Imagick for PDF processing.
1123
+
* Increases rendering DPI and only loads first page.
1124
+
*
1125
+
* @since 4.7.0
1126
+
*
1127
+
* @return string|WP_Error File to load or WP_Error on failure.
1128
+
*/
1129
+
protected function pdf_setup() {
1130
+
try {
1131
+
/*
1132
+
* By default, PDFs are rendered in a very low resolution.
1133
+
* We want the thumbnail to be readable, so increase the rendering DPI.
1134
+
*/
1135
+
$this->image->setResolution( 128, 128 );
1136
+
1137
+
// Only load the first page.
1138
+
return $this->file . '[0]';
1139
+
} catch ( Exception $e ) {
1140
+
return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file );
1141
+
}
1142
+
}
1143
+
1144
+
/**
1145
+
* Load the image produced by Ghostscript.
1146
+
*
1147
+
* Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files
1148
+
* when `use-cropbox` is set.
1149
+
*
1150
+
* @since 5.6.0
1151
+
*
1152
+
* @return true|WP_Error
1153
+
*/
1154
+
protected function pdf_load_source() {
1155
+
$filename = $this->pdf_setup();
1156
+
1157
+
if ( is_wp_error( $filename ) ) {
1158
+
return $filename;
1159
+
}
1160
+
1161
+
try {
1162
+
/*
1163
+
* When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped
1164
+
* area (resulting in unnecessary whitespace) unless the following option is set.
1165
+
*/
1166
+
$this->image->setOption( 'pdf:use-cropbox', true );
1167
+
1168
+
/*
1169
+
* Reading image after Imagick instantiation because `setResolution`
1170
+
* only applies correctly before the image is read.
1171
+
*/
1172
+
$this->image->readImage( $filename );
1173
+
} catch ( Exception $e ) {
1174
+
// Attempt to run `gs` without the `use-cropbox` option. See #48853.
1175
+
$this->image->setOption( 'pdf:use-cropbox', false );
1176
+
1177
+
$this->image->readImage( $filename );
1178
+
}
1179
+
1180
+
return true;
1181
+
}
1182
+
}
1183
+