PluginProbe
HEIC Support / 2.2.1
HEIC Support v2.2.1
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.2.1, at heic-support.php

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