PluginProbe
HEIC Support / 2.1.3
HEIC Support v2.1.3
2.3.0 2.2.1 trunk 1.0.0 1.0.1 2.0.0 2.1.0 2.1.1 2.1.3 2.1.4 2.2.0
heic-support / heic-support.php

heic-support.php in HEIC Support 2.1.3, at heic-support.php

588 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * HEIC Support
4 *
5 * @author Corey Salzano <csalzano@duck.com>
6 * @package HEIC_Support
7 */
8
9 defined( 'ABSPATH' ) || exit;
10
11 /**
12 * Plugin Name: HEIC Support
13 * Description: Allows .heic uploads to the Media Library. Creates .webp or .jpg copies of .heic images when they are uploaded.
14 * Plugin URI: https://breakfastco.xyz/heic-support/
15 * Author: Breakfast
16 * Author URI: https://breakfastco.xyz/
17 * Version: 2.1.3
18 * Text-domain: heic-support
19 * License: GPLv2
20 */
21
22 if ( ! class_exists( 'Heic_Support_Plugin' ) ) {
23 /**
24 * Heic_Support_Plugin
25 */
26 class Heic_Support_Plugin {
27
28 const OPTION_TEST_IMAGE = 'heic_support_test_image_paths';
29
30 /**
31 * True or false, the test image conversion worked.
32 *
33 * @var bool $test_success
34 */
35 protected $test_success;
36
37 /**
38 * Escaped HTML displayed in the Test setting at Settings → Media.
39 *
40 * @var string $test_result_html
41 */
42 protected $test_result_html;
43
44 /**
45 * Adds filter and action hooks that power this plugin.
46 *
47 * @return void
48 */
49 public function add_hooks() {
50 // Allow .heic files to be uploaded into the Media Library.
51 add_filter( 'upload_mimes', array( $this, 'add_mimes' ) );
52
53 // Creates a copy of .heic images uploaded to the Media Library.
54 add_action( 'add_attachment', array( $this, 'create_copy' ), 12, 1 );
55
56 // Replace heic uploads without preserving the heic.
57 add_filter( 'wp_handle_upload_prefilter', array( $this, 'replace' ) );
58
59 // Populates width, height, and other attributes in meta key _wp_attachment_metadata.
60 add_filter( 'wp_generate_attachment_metadata', array( $this, 'populate_meta' ), 10, 2 );
61
62 // Adds settings to the dashboard at Settings → Media.
63 add_action( 'admin_init', array( $this, 'add_settings' ) );
64
65 // Adds a link to the plugins list that helps users find Settings → Media.
66 add_filter( 'plugin_action_links_heic-support/heic-support.php', array( $this, 'add_settings_link' ) );
67
68 // Deletes the test image when the plugin is uninstalled.
69 register_uninstall_hook( __FILE__, array( __CLASS__, 'uninstall' ) );
70
71 // Run our conversion test when users visit wp-admin/options-media.php.
72 add_action( 'admin_enqueue_scripts', array( $this, 'test_run' ) );
73 }
74
75 /**
76 * Allow .heic files to be uploaded into the Media Library.
77 *
78 * @param array $mimes Array of allowed mime types.
79 * @return array
80 */
81 public function add_mimes( $mimes ) {
82 if ( empty( $mimes['heic'] ) ) {
83 $mimes['heic'] = 'image/heic';
84 }
85 return $mimes;
86 }
87
88 /**
89 * Adds settings to the dashboard at Settings → Media.
90 *
91 * @return void
92 */
93 public function add_settings() {
94
95 $section = 'heic_support_section';
96 add_settings_section(
97 $section,
98 __( 'HEIC Support', 'heic-support' ),
99 array( $this, 'callback_section' ),
100 'media'
101 );
102
103 // Format setting registration.
104 register_setting(
105 'media',
106 'heic_support_format',
107 array(
108 'type' => 'string',
109 'description' => __( 'Convert .heic images to this format.', 'heic-support' ),
110 'sanitize_callback' => 'sanitize_text_field',
111 'show_in_rest' => true,
112 )
113 );
114
115 // Replace setting registration.
116 register_setting(
117 'media',
118 'heic_support_replace',
119 array(
120 'type' => 'boolean',
121 'description' => __( 'Replace .heic images uploaded to the Media Library instead of creating copies.', 'heic-support' ),
122 'sanitize_callback' => 'rest_sanitize_boolean',
123 'show_in_rest' => true,
124 )
125 );
126
127 // Is the plugin's primary feature going to work?
128 if ( ! class_exists( 'Imagick' ) ) {
129 // No. Do not output any of the options.
130 return;
131 }
132
133 if ( $this->test_success ) {
134 // Format setting output.
135 add_settings_field(
136 'format',
137 __( 'Convert To', 'heic-support' ),
138 array( $this, 'callback_format_setting' ),
139 'media',
140 $section
141 );
142
143 // Replace setting output.
144 add_settings_field(
145 'replace',
146 __( 'Replace', 'heic-support' ),
147 array( $this, 'callback_replace_setting' ),
148 'media',
149 $section
150 );
151
152 // ImageMagick setting.
153 add_settings_field(
154 'imagemagick',
155 __( 'ImageMagick', 'heic-support' ),
156 array( $this, 'callback_imagemagick_setting' ),
157 'media',
158 $section
159 );
160 }
161
162 // Test setting.
163 add_settings_field(
164 'test',
165 __( 'Test', 'heic-support' ),
166 array( $this, 'callback_test_setting' ),
167 'media',
168 $section
169 );
170 }
171
172 /**
173 * Adds a "Settings" link to this plugin's entry at wp-admin/plugins.php.
174 *
175 * @param array $links An array of plugin action links. By default this can include 'activate', 'deactivate', and 'delete'. With Multisite active this can also include 'network_active' and 'network_only' items.
176 * @return array
177 */
178 public function add_settings_link( $links ) {
179 $links[] = '<a href="' . admin_url( 'options-media.php' ) . '">' . __( 'Settings', 'heic-support' ) . '</a>';
180 return $links;
181 }
182
183 /**
184 * Returns the file extension to which .heic images are converted.
185 * Either "jpg" or "webp".
186 *
187 * @return string
188 */
189 protected static function get_extension() {
190 $format = self::get_format();
191 if ( 'jpeg' === $format ) {
192 return apply_filters( 'heic_support_extension', 'jpg' );
193 }
194 return $format;
195 }
196
197 /**
198 * Retrieves the file format to which .heic images are converted from
199 * the option where it is stored. Either "jpeg" or "webp".
200 *
201 * @return string
202 */
203 protected static function get_format() {
204 $value = get_option( 'heic_support_format' );
205 if ( empty( $value ) ) {
206 $value = 'webp';
207 }
208 return apply_filters( 'heic_support_format', $value );
209 }
210
211 /**
212 * Outputs HTML that renders the ImageMagick setting content at Settings
213 * → Media → HEIC Support. This is the version of ImageMagick running
214 * on the server.
215 *
216 * @return void
217 */
218 public function callback_imagemagick_setting() {
219 echo esc_html( $this->imagemagick_version() );
220 }
221
222 /**
223 * Outputs HTML that renders the Format setting radio buttons.
224 *
225 * @return void
226 */
227 public function callback_format_setting() {
228 $value = self::get_format();
229 printf(
230 '<fieldset><label for="heic_support_webp"><input type="radio" id="heic_support_webp" name="heic_support_format" value="webp" %1$s/> %2$s</label><br />'
231 . '<label for="heic_support_jpeg"><input type="radio" id="heic_support_jpeg" name="heic_support_format" value="jpeg" %3$s/> %4$s</label></fieldset>',
232 checked( $value, 'webp', false ),
233 esc_html__( '.webp', 'heic-support' ),
234 checked( $value, 'jpeg', false ),
235 esc_html__( '.jpg', 'heic-support' )
236 );
237 }
238
239 /**
240 * Outputs HTML that renders the Replace ID settings checkbox.
241 *
242 * @return void
243 */
244 public function callback_replace_setting() {
245 $value = get_option( 'heic_support_replace' );
246 printf(
247 '<input type="checkbox" id="heic_support_replace" name="heic_support_replace" %s/> <label for="heic_support_replace">%s</label><p class="description">%s</p>',
248 checked( $value, '1', false ),
249 esc_html__( 'Replace .heic images uploaded to the Media Library instead of creating copies.', 'heic-support' ),
250 esc_html__( 'Does not preserve the original .heic files.', 'heic-support' )
251 );
252 }
253
254 /**
255 * Outputs the Test setting content at Settings → Media → HEIC Support.
256 *
257 * @return void
258 */
259 public function callback_test_setting() {
260 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
261 echo $this->test_result_html ?? '';
262 }
263
264 /**
265 * Outputs the HEIC Support section content at Settings → Media.
266 *
267 * @return void
268 */
269 public function callback_section() {
270 // Is the plugin's primary feature going to work?
271 if ( ! class_exists( 'Imagick' ) ) {
272 // No.
273 printf(
274 /* translators: 1. Anchor element opening tag. 2. Anchor element closing tag. */
275 esc_html__( 'This server does not provide ImageMagick, and .heic images cannot be converted without it. Ask your web host to enable ImageMagick, or %1$sread our list of compatible hosts%2$s.', 'heic-support' ),
276 '<a href="https://breakfastco.xyz/heic-support/#hosting">',
277 '</a>'
278 );
279 return;
280 }
281 esc_html_e( 'Control how .heic images are handled during uploads.', 'heic-support' );
282 }
283
284 /**
285 * Filter callback on add_attachment. Creates a copy of .heic images
286 * uploaded to the Media Library.
287 *
288 * @param int $post_id The ID of a new attachment.
289 * @return void
290 */
291 public function create_copy( $post_id ) {
292 // Is the Replace feature enabled? If so, abort the copy.
293 $replace = filter_var( get_option( 'heic_support_replace' ), FILTER_VALIDATE_BOOLEAN );
294 if ( $replace ) {
295 // Yes. Replace is enabled. Abort.
296 return;
297 }
298
299 // Is ImageMagick running?
300 if ( ! class_exists( 'Imagick' ) ) {
301 // No.
302 return;
303 }
304 $file_path = get_attached_file( $post_id );
305 if ( false === $file_path ) {
306 return;
307 }
308 // Is the attachment an heic?
309 if ( 'heic' !== pathinfo( $file_path, PATHINFO_EXTENSION ) ) {
310 // No.
311 return;
312 }
313 $imagick = new Imagick();
314 try {
315 if ( $imagick->readImage( $file_path ) ) {
316 $imagick->setImageFormat( self::get_format() );
317 // Create a path to a copy of the image.
318 $name = basename( $file_path, '.heic' ) . '.' . self::get_extension();
319 $upload_dir = wp_upload_dir();
320 $imagick->writeImage( $upload_dir['path'] . DIRECTORY_SEPARATOR . $name );
321
322 /**
323 * The Media Library loads these files, but not the Editor.
324 *
325 * @link https://developer.wordpress.org/reference/functions/media_sideload_image/#more-information
326 */
327 if ( ! function_exists( 'media_sideload_image' ) ) {
328 require_once ABSPATH . 'wp-admin/includes/media.php';
329 require_once ABSPATH . 'wp-admin/includes/file.php';
330 require_once ABSPATH . 'wp-admin/includes/image.php';
331 }
332 $copy_post_id = media_sideload_image( $upload_dir['url'] . '/' . $name, 0 /* post_parent */, get_the_title( $post_id ), 'id' );
333 if ( ! is_wp_error( $copy_post_id ) ) {
334 update_post_meta( $copy_post_id, '_heic_support_copy_of', $post_id );
335 update_post_meta( $post_id, '_heic_support_copy_of', $copy_post_id );
336 }
337 }
338 } catch ( ImagickException $ie ) {
339 // "Fatal error: Uncaught ImagickException: no decode delegate for this image format `HEIC'".
340 // The version of Imagick does not support heic
341 return;
342 }
343 }
344
345 /**
346 * Returns the ImageMagick version string.
347 *
348 * @return string
349 */
350 protected function imagemagick_version() {
351 if ( ! class_exists( 'Imagick' ) ) {
352 return '';
353 }
354 return Imagick::getVersion()['versionString'];
355 }
356
357 /**
358 * When .heic files are added to the Media Library, populate their width,
359 * height, and other attributes that live in meta key _wp_attachment_metadata.
360 *
361 * @param array $metadata An array of attachment meta data.
362 * @param int $attachment_id Current attachment ID.
363 * @return array
364 */
365 public function populate_meta( $metadata, $attachment_id ) {
366 // Is ImageMagick running?
367 if ( ! class_exists( 'Imagick' ) ) {
368 // No.
369 return $metadata;
370 }
371 $file_path = get_attached_file( $attachment_id );
372 if ( false === $file_path ) {
373 return $metadata;
374 }
375 // Is the attachment an heic?
376 if ( 'heic' !== pathinfo( $file_path, PATHINFO_EXTENSION ) ) {
377 // No.
378 return $metadata;
379 }
380 // Are the width and height missing?
381 if ( false === $metadata ) {
382 $metadata = array();
383 }
384 if ( ! empty( $metadata['width'] ) && ! empty( $metadata['height'] ) ) {
385 // No.
386 return $metadata;
387 }
388 $imagick = new Imagick();
389 try {
390 if ( $imagick->readImage( $file_path ) ) {
391 $new_values = $imagick->getImageGeometry();
392 $new_values['sizes'] = array();
393 $new_values['file'] = get_post_meta( $attachment_id, '_wp_attached_file', true );
394 $metadata = wp_parse_args( $metadata, $new_values );
395 return $metadata;
396 }
397 } catch ( ImagickException $ie ) {
398 // "Fatal error: Uncaught ImagickException: no decode delegate for this image format `HEIC'".
399 // The version of Imagick does not support heic
400 return $metadata;
401 }
402 }
403
404 /**
405 * Replaces uploaded .heic files with equivalents during uploads.
406 *
407 * @param array $file An array of data for a single file.
408 * @return array
409 */
410 public function replace( $file ) {
411 // Does $file look like an uploaded file?
412 if ( empty( $file['tmp_name'] ) || empty( $file['name'] ) ) {
413 return $file;
414 }
415
416 // Is this image even an heic?
417 $wp_filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] );
418 if ( empty( $wp_filetype['type'] ) || 'image/heic' !== $wp_filetype['type'] ) {
419 // No.
420 return $file;
421 }
422
423 // Is ImageMagick available?
424 if ( ! class_exists( 'Imagick' ) ) {
425 // No.
426 return $file;
427 }
428
429 // Is this replace feature enabled?
430 $replace = filter_var( get_option( 'heic_support_replace' ), FILTER_VALIDATE_BOOLEAN );
431 if ( ! $replace ) {
432 // No. The feature is not enabled.
433 return $file;
434 }
435
436 $imagick = new Imagick();
437 try {
438 if ( $imagick->readImage( $file['tmp_name'] ) ) {
439 $format = self::get_format();
440 $imagick->setImageFormat( $format );
441 $file['type'] = apply_filters( 'heic_support_mime', 'image/' . $format );
442 $file['name'] = basename( $file['name'], '.heic' ) . '.' . self::get_extension();
443 $imagick->writeImage( $file['tmp_name'] );
444 $file['size'] = wp_filesize( $file['tmp_name'] );
445 }
446 } catch ( ImagickException $ie ) {
447 // "Fatal error: Uncaught ImagickException: no decode delegate for this image format `HEIC'".
448 // The version of Imagick does not support heic
449 return $file;
450 }
451
452 return $file;
453 }
454
455 /**
456 * Tries to convert an .heic image that ships with this plugin. Stashes
457 * a message describing what happened in $this->test_result_html so it
458 * can be retrieved.
459 *
460 * @param string $hook_suffix The current admin page.
461 * @return void
462 */
463 public function test_run( $hook_suffix ) {
464 // Is this page wp-admin/options-media.php?
465 if ( 'options-media.php' !== $hook_suffix ) {
466 // No.
467 return;
468 }
469
470 // Try our test image conversion & preserve data about the result.
471 if ( ! class_exists( 'Imagick' ) ) {
472 // Can't even try.
473 $this->test_success = false;
474 $this->test_result_html = esc_html__( 'ImageMagick is not installed on this server. This plugin only works on servers running ImageMagick. Some hosts require a switch be flipped before the program is available to a site.', 'heic-support' );
475 return;
476 }
477
478 $imagick = new Imagick();
479 try {
480 if ( $imagick->readImage( __DIR__ . DIRECTORY_SEPARATOR . 'image4.heic' ) ) {
481 $imagick->setImageFormat( self::get_format() );
482
483 // Create a copy of the image.
484 $path = self::test_file_path();
485 $this->test_save_image_path( $path );
486 $imagick->writeImage( $path );
487 $upload_dir = wp_upload_dir();
488 $name = basename( $path );
489 // It worked!
490 $this->test_success = true;
491 $this->test_result_html = sprintf(
492 '<figure><img src="%s" width="%d" /><figcaption>%s .%s.</figcaption></figure>',
493 esc_attr( $upload_dir['url'] . '/' . $name ),
494 esc_attr( get_option( 'medium_size_w' ) ),
495 esc_html__( 'This plugin can convert .heic images. If you do not see an image, your browser may not support', 'heic-support' ),
496 esc_html( self::get_extension() )
497 );
498 }
499 } catch ( ImagickException $ie ) {
500 // "Fatal error: Uncaught ImagickException: no decode delegate for this image format `HEIC'".
501 $msg = 'no decode delegate for this image format `HEIC\'';
502 if ( false !== strpos( $ie->getMessage(), $msg ) ) {
503 $this->test_success = false;
504 $this->test_result_html = sprintf(
505 /* translators: 1. An opening bold text tag <b>. 2. A closing bold text tag </b>. 3. An ImageMagick version string. 4. Anchor element opening tag. 5. Anchor element closing tag. */
506 esc_html__( '%1$sFailed%2$s. ImageMagick is installed, but does not support HEIC. You may upload .heic files, but they will not be converted. The version might be too old, or perhaps your server is missing libheif. Installed version is %3$s. %4$sRead our list of web hosts%5$s where this plugin has been tested.', 'heic-support' ),
507 '<b>',
508 '</b>',
509 esc_html( $this->imagemagick_version() ),
510 '<a href="https://breakfastco.xyz/heic-support/#hosting">',
511 '</a>'
512 );
513 }
514 }
515 }
516
517 /**
518 * Saves the path to a test image after deleting the file that belongs
519 * to the current value of the option where the path is stored.
520 *
521 * @param string $path The path to a test image we want to save.
522 * @return void
523 */
524 protected function test_save_image_path( $path ) {
525 $old_path = get_option( self::OPTION_TEST_IMAGE );
526 if ( ! empty( $old_path ) && file_exists( $old_path ) ) {
527 wp_delete_file( $old_path );
528 }
529 update_option( self::OPTION_TEST_IMAGE, $path );
530 }
531
532 /**
533 * Returns a unique file path where we can save a test image.
534 *
535 * @return string
536 */
537 protected static function test_file_path() {
538 $upload_dir = wp_upload_dir();
539 return $upload_dir['path'] . DIRECTORY_SEPARATOR
540 . wp_unique_filename(
541 $upload_dir['path'],
542 'heic-support-image4.' . self::get_extension()
543 );
544 }
545
546 /**
547 * Removes plugin data and test images from the current site.
548 *
549 * @return void
550 */
551 protected static function uninstall_guts() {
552 $path = get_option( self::OPTION_TEST_IMAGE );
553 if ( ! empty( $path ) && file_exists( $path ) ) {
554 wp_delete_file( $path );
555 }
556
557 delete_option( 'heic_support_format' );
558 delete_option( 'heic_support_replace' );
559 delete_option( self::OPTION_TEST_IMAGE );
560 }
561
562 /**
563 * Deletes plugin data and test images when the plugin is uninstalled.
564 *
565 * @return void
566 */
567 public static function uninstall() {
568 if ( ! is_multisite() ) {
569 self::uninstall_guts();
570 } else {
571 $sites = get_sites(
572 array(
573 'network' => 1,
574 'limit' => 1000,
575 )
576 );
577 foreach ( $sites as $site ) {
578 switch_to_blog( $site->blog_id );
579 self::uninstall_guts();
580 restore_current_blog();
581 }
582 }
583 }
584 }
585 }
586 $heic_support_plugin = new Heic_Support_Plugin();
587 $heic_support_plugin->add_hooks();
588