false, 'mode' => null, 'cli_path' => null, 'gm_path' => null, 'handle_sizes' => [ 'thumbnail' => 'size', 'medium' => 'quality', 'medium_large' => 'quality', 'large' => 'quality', ], 'quality' => [ 'quality' => -1, 'size' => 70, ], 'interlace' => false, 'keep_exif' => false, // Turn off the browser-based image processing WordPress 7.1 added, so all // sub-sizes are generated by ImageMagick instead of a mix of both. 'disable_client_side_processing' => true, 'version' => constant( 'IME_OPTION_VERSION' ), ]; // Available quality modes $ime_available_quality_modes = [ 'quality', 'size', 'skip' ]; // Current options $ime_options = null; // Keep track of attachment file & sizes between different filters $ime_image_sizes = null; $ime_image_file = null; $ime_failed_sizes = []; /* * Functions */ add_action( 'plugins_loaded', 'ime_init_early' ); add_action( 'init', 'ime_init' ); register_uninstall_hook( __FILE__, 'ime_uninstall' ); /* Plugin setup (early) */ function ime_init_early() { load_plugin_textdomain( 'imagemagick-engine', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); if ( ime_active() ) { add_filter( 'intermediate_image_sizes_advanced', 'ime_filter_image_sizes', 99, 1 ); add_filter( 'wp_read_image_metadata', 'ime_filter_read_image_metadata', 10, 3 ); /* * Priority 5, deliberately ahead of the default 10. * * On this filter we are a *producer* of $metadata['sizes'], not a * consumer: we are finishing work core would already have done before * any filter ran, had ime_filter_image_sizes() not stripped those sizes * out of intermediate_image_sizes_advanced. So we have to run before * everything that reads $metadata['sizes'], and the default priority of * 10 is where those consumers live. * * The concrete case that motivated this is the Modern Image Formats * plugin (webp-uploads), which also filters at 10 and bails early when * $metadata['sizes'] is still empty -- leaving our sub-sizes without * WebP/AVIF variants. See issue #41. */ add_filter( 'wp_generate_attachment_metadata', 'ime_filter_attachment_metadata', 5, 2 ); if ( ime_disable_client_side_processing() ) { add_filter( 'wp_client_side_media_processing_enabled', '__return_false' ); } } } /* Plugin setup */ function ime_init() { if ( is_admin() ) { add_action( 'admin_menu', 'ime_admin_menu' ); add_filter( 'plugin_action_links', 'ime_filter_plugin_actions', 10, 2 ); add_filter( 'media_meta', 'ime_filter_media_meta', 10, 2 ); add_action( 'wp_ajax_ime_test_im_path', 'ime_ajax_test_im_path' ); add_action( 'wp_ajax_ime_process_image', 'ime_ajax_process_image' ); add_action( 'wp_ajax_ime_regen_start', 'ime_ajax_regen_start' ); add_action( 'wp_ajax_ime_regen_batch', 'ime_ajax_regen_batch' ); add_action( 'wp_ajax_ime_regen_cancel', 'ime_ajax_regen_cancel' ); add_action( 'wp_ajax_ime_regen_state', 'ime_ajax_regen_state' ); wp_register_script( 'ime-alpinejs', plugins_url( '/js/alpine.csp.min.js', __FILE__ ), [ 'ime-admin' ], '3.15.9', true ); wp_register_script( 'ime-admin', plugins_url( '/js/ime-admin.js', __FILE__ ), [], constant( 'IME_VERSION' ), true ); } } /* Remove all plugin data on uninstall */ function ime_uninstall() { delete_option( 'ime_options' ); delete_option( IME_REGEN_OPTION ); delete_transient( 'ime_cli_valid' ); delete_transient( 'ime_gm_valid' ); } /* Are we enabled with valid mode? */ function ime_active() { return ime_get_option( 'enabled' ) && ime_mode_valid(); } /* Check if mode is valid */ function ime_mode_valid( $mode = null ) { if ( empty( $mode ) ) { $mode = ime_get_option( 'mode' ); } $fn = 'ime_im_' . $mode . '_valid'; return ( ! empty( $mode ) && function_exists( $fn ) && call_user_func( $fn ) ); } // Get array of available image sizes function ime_available_image_sizes() { $sizes = [ 'thumbnail' => __( 'Thumbnail' ), 'medium' => __( 'Medium' ), 'medium_large' => __( 'Medium Large' ), 'large' => __( 'Large' ), ]; // Standard sizes foreach ( wp_get_additional_image_sizes() as $name => $spec ) { $sizes[ $name ] = $name; } return $sizes; } /* * Plugin option handling */ // Setup plugin options function ime_setup_options() { global $ime_options; // Already setup? if ( is_array( $ime_options ) ) { return; } $ime_options = get_option( 'ime_options' ); // No stored options yet? if ( ! is_array( $ime_options ) ) { global $ime_options_default; $ime_options = $ime_options_default ?? array(); } // Do we need to upgrade options? if ( ! array_key_exists( 'version', $ime_options ) || $ime_options['version'] < constant( 'IME_OPTION_VERSION' ) ) { /* * Future compatability code goes here! */ // Option version 2: added client-side media processing switch (WP 7.1). if ( ! array_key_exists( 'disable_client_side_processing', $ime_options ) ) { $ime_options['disable_client_side_processing'] = true; } $ime_options['version'] = constant( 'IME_OPTION_VERSION' ); ime_store_options(); } } // Store plugin options function ime_store_options() { global $ime_options; ime_setup_options(); $stored_options = get_option( 'ime_options' ); if ( $stored_options === false ) { add_option( 'ime_options', $ime_options, null, false ); } else { update_option( 'ime_options', $ime_options ); } } // Get plugin option function ime_get_option( $option_name, $default = null ) { ime_setup_options(); global $ime_options, $ime_options_default; if ( is_array( $ime_options ) && array_key_exists( $option_name, $ime_options ) ) { return $ime_options[ $option_name ]; } if ( ! is_null( $default ) ) { return $default; } if ( is_array( $ime_options_default ) && array_key_exists( $option_name, $ime_options_default ) ) { return $ime_options_default[ $option_name ]; } return null; } // Set plugin option function ime_set_option( $option_name, $option_value, $store = false ) { ime_setup_options(); global $ime_options; $ime_options[ $option_name ] = $option_value; if ( $store ) { ime_store_options(); } } // Should images be converted with interlace or not function ime_interlace() { return ime_get_option( 'interlace' ); } // Should Exif data (including GPS) be preserved when stripping metadata function ime_keep_exif() { return ime_get_option( 'keep_exif' ); } /* * Should WordPress' client-side media processing be turned off? * * WordPress 7.1 generates sub-sizes in the browser (WebAssembly) when the admin * runs in a secure context. Those sizes never reach our filters, so an upload * would end up with some sizes made by ImageMagick and some by the browser. */ function ime_disable_client_side_processing() { return ime_client_side_processing_available() && ime_get_option( 'disable_client_side_processing' ); } // Does this WordPress version have client-side media processing? (7.1+) function ime_client_side_processing_available() { return function_exists( 'wp_is_client_side_media_processing_enabled' ); } // Get image quality setting for type function ime_get_quality( $resize_mode = 'quality' ) { $quality = ime_get_option( 'quality', '-1' ); if ( ! $quality ) { return -1; } if ( ! is_array( $quality ) ) { return $quality; } if ( isset( $quality[ $resize_mode ] ) ) { return $quality[ $resize_mode ]; } return -1; } // Get resize mode for size function ime_get_resize_mode( $size ) { $handle_sizes = ime_get_option( 'handle_sizes' ); if ( isset( $handle_sizes[ $size ] ) && is_string( $handle_sizes[ $size ] ) ) { return $handle_sizes[ $size ]; } else { return 'quality'; // default to quality } } /* * WP integration & image handling functions */ /* * Filter image sizes (in wp_generate_attachment_metadata()). * * We store the sizes we are interested in, and remove those sizes from the * list so that WP doesn't handle them -- we will take care of them later. * * The reason we do things this way is so we do not resize image twize (once * by WordPress using GD, and then again by us). */ function ime_filter_image_sizes( $sizes ) { global $ime_image_sizes; $handle_sizes = ime_get_option( 'handle_sizes' ); foreach ( $handle_sizes as $s => $handle ) { if ( ! $handle || $handle == 'skip' || ! array_key_exists( $s, $sizes ) ) { continue; } $ime_image_sizes[ $s ] = $sizes[ $s ]; unset( $sizes[ $s ] ); } return $sizes; } /* * Filter to get target file name. * * Function wp_generate_attachment_metadata calls wp_read_image_metadata which * gives us a hook to get the target filename. */ function ime_filter_read_image_metadata( $metadata, $file, $ignore ) { global $ime_image_file; $ime_image_file = $file; return $metadata; } /* * Filter new attachment metadata * * Resize image for the sizes we are interested in. * * Parts of function copied from wp-includes/media.php:image_resize() */ function ime_filter_attachment_metadata( $metadata, $attachment_id ) { global $ime_image_sizes, $ime_image_file, $ime_failed_sizes; // Reset before the loop below so a previous attachment's failures cannot // leak into this one. $ime_failed_sizes = []; // Any sizes we are interested in? if ( empty( $ime_image_sizes ) ) { return $metadata; } $attachment = get_post( $attachment_id ); // We can only process attachments. if ( 'attachment' !== get_post_type( $attachment ) ) { return $metadata; } // Make sure file exists on server if ( ! $ime_image_file || ! file_exists( $ime_image_file ) ) { return $metadata; } $editor = wp_get_image_editor( $ime_image_file ); if ( is_wp_error( $editor ) ) { // Display a more helpful error message. if ( 'image_no_editor' === $editor->get_error_code() ) { $editor = new WP_Error( 'image_no_editor', __( 'The current image editor cannot process this file type.', 'imagemagick-engine' ) ); } $editor->add_data( array( 'attachment' => $attachment, 'status' => 415, ) ); return $editor; } // Get size & image type of original image $old_stats = wp_getimagesize( $ime_image_file ); if ( ! $old_stats || is_wp_error( $old_stats ) ) { return $metadata; } list($orig_w, $orig_h, $orig_type) = $old_stats; /* * Exif orientation 5-8 means the image is displayed rotated a quarter turn. * All engines apply that rotation to the pixels before resizing, so the * dimension math below has to use the rotated width/height. Without the * swap the target size is computed for the wrong aspect ratio, and both the * resized file and the stored metadata come out wrong. */ if ( in_array( ime_image_exif_orientation( $ime_image_file, $orig_type ), [ 5, 6, 7, 8 ], true ) ) { list($orig_w, $orig_h) = [ $orig_h, $orig_w ]; } /* * Sort out the filename, extension (and image type) of resized images */ $info = pathinfo( $ime_image_file ); $dir = $info['dirname']; $ext = $info['extension']; /* * Do the actual resize */ foreach ( $ime_image_sizes as $size => $size_data ) { $width = $size_data['width']; $height = $size_data['height']; // ignore sizes equal to or larger than original size if ( $orig_w <= $width && $orig_h <= $height ) { continue; } $crop = $size_data['crop']; $dims = image_resize_dimensions( $orig_w, $orig_h, $width, $height, $crop ); if ( ! $dims ) { continue; } list($dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) = $dims; $suffix = "{$dst_w}x{$dst_h}"; $new_filename = $editor->generate_filename( $suffix, null, $ext ); $resized = ime_im_resize( $ime_image_file, $new_filename, $dst_w, $dst_h, $crop, ime_get_resize_mode( $size ) ); if ( ! $resized ) { $ime_failed_sizes[] = $size; continue; } /* * Keep the same keys, in the same order, that core's image editor * writes for a sub-size: file, width, height, mime-type, filesize. * Consumers rely on them -- webp-uploads reads filesize to decide * whether a generated WebP/AVIF is actually smaller than what we made. */ $size_meta = [ 'file' => wp_basename( $new_filename ), 'width' => $dst_w, 'height' => $dst_h, ]; // The engines take their output format from the extension, which is the // source extension, so the written file is what we ask wp_check_filetype // about. Omit the key entirely rather than store a falsy value. $filetype = wp_check_filetype( $new_filename ); if ( ! empty( $filetype['type'] ) ) { $size_meta['mime-type'] = $filetype['type']; } $size_meta['filesize'] = wp_filesize( $new_filename ); $metadata['sizes'][ $size ] = $size_meta; if ( ! isset( $metadata['image-converter'] ) || ! is_array( $metadata['image-converter'] ) ) { $metadata['image-converter'] = []; } $metadata['image-converter'][ $size ] = 'IME'; // Set correct file permissions $stat = stat( dirname( $new_filename ) ); $perms = $stat['mode'] & 0000666; //same permissions as parent folder, strip off the executable bits @ chmod( $new_filename, $perms ); } $ime_image_sizes = null; return $metadata; } /* * Read the Exif orientation (1-8) of an image file, 1 when unknown. * * $image_type is an IMAGETYPE_* constant as returned by wp_getimagesize(). */ function ime_image_exif_orientation( $file, $image_type = null ) { if ( ! is_callable( 'exif_read_data' ) ) { return 1; } // exif_read_data() only handles JPEG and TIFF $exif_types = [ IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM ]; if ( ! is_null( $image_type ) && ! in_array( $image_type, $exif_types, true ) ) { return 1; } $exif = @exif_read_data( $file ); if ( empty( $exif['Orientation'] ) ) { return 1; } $orientation = (int) $exif['Orientation']; return ( $orientation >= 1 && $orientation <= 8 ) ? $orientation : 1; } // Resize file by calling mode specific resize function function ime_im_resize( $old_file, $new_file, $width, $height, $crop, $resize_mode = 'quality' ) { $mode = ime_get_option( 'mode' ); $fn = 'ime_im_' . $mode . '_valid'; if ( empty( $mode ) || ! function_exists( $fn ) || ! call_user_func( $fn ) ) { return false; } $fn = 'ime_im_' . $mode . '_resize'; $success = ( function_exists( $fn ) && call_user_func( $fn, $old_file, $new_file, $width, $height, $crop, $resize_mode ) ); do_action( 'ime_after_resize', $success, $old_file, $new_file, $width, $height, $crop, $resize_mode ); return $success; } // Is this the filename of a jpeg? function ime_im_filename_is_jpg( $filename ) { $info = pathinfo( $filename ); $ext = $info['extension']; return ( strcasecmp( $ext, 'jpg' ) == 0 ) || ( strcasecmp( $ext, 'jpeg' ) == 0 ); } // Get file extenstion function ime_im_get_filetype( $filename ) { $info = pathinfo( $filename ); return strtolower( $info['extension'] ); } /* * PHP ImageMagick ("Imagick") class handling */ // Does class exist? function ime_im_php_valid() { return class_exists( 'Imagick' ); } // Resize file using PHP Imagick class function ime_im_php_resize( $old_file, $new_file, $width, $height, $crop, $resize_mode = 'quality' ) { try { $im = new Imagick( $old_file ); if ( ! $im->valid() ) { return false; } $im->setImageFormat( ime_im_get_filetype( $old_file ) ); // Apply Exif orientation to actual pixels before any dimension calculations. // Without this, getImageGeometry() returns pre-rotation dimensions and // resized images end up with the wrong orientation. $im->autoOrient(); $quality = ime_get_quality( $resize_mode ); if ( is_numeric( $quality ) && $quality >= 0 && $quality <= 100 && ime_im_filename_is_jpg( $new_file ) ) { $im->setImageCompression( Imagick::COMPRESSION_JPEG ); $im->setImageCompressionQuality( $quality ); } if ( ime_interlace() ) { $im->setInterlaceScheme( Imagick::INTERLACE_PLANE ); } if ( $resize_mode == 'size' ) { if ( ime_keep_exif() ) { // Strip everything except Exif (preserves GPS and other Exif data) foreach ( [ 'iptc', '8bim', 'xmp', 'APP13' ] as $profile ) { @$im->removeImageProfile( $profile ); } } else { $im->stripImage(); } } if ( $crop ) { /* * Unfortunately we cannot use the PHP module * cropThumbnailImage() function as it strips profile data. * * Crop an area proportional to target $width and $height and * fall through to scaleImage() below. */ $geo = $im->getImageGeometry(); $orig_width = $geo['width']; $orig_height = $geo['height']; if ( ( $orig_width / $width ) < ( $orig_height / $height ) ) { $crop_width = $orig_width; $crop_height = ceil( ( $height * $orig_width ) / $width ); $off_x = 0; $off_y = ceil( ( $orig_height - $crop_height ) / 2 ); } else { $crop_width = ceil( ( $width * $orig_height ) / $height ); $crop_height = $orig_height; $off_x = ceil( ( $orig_width - $crop_width ) / 2 ); $off_y = 0; } $im->cropImage( $crop_width, $crop_height, $off_x, $off_y ); } $im->scaleImage( $width, $height, true ); $im->setImagePage( $width, $height, 0, 0 ); // to make sure canvas is correct $im->writeImage( $new_file ); return file_exists( $new_file ); } catch ( ImagickException $ie ) { return false; } } // Does Gmagick class exist? function ime_im_gmagick_valid() { return class_exists( 'Gmagick' ); } // Resize file using PHP Gmagick class function ime_im_gmagick_resize( $old_file, $new_file, $width, $height, $crop, $resize_mode = 'quality' ) { try { $im = new Gmagick( $old_file ); $im->setimageformat( ime_im_get_filetype( $old_file ) ); // Apply Exif orientation correction manually (Gmagick has no autoOrient()) $orientation = $im->getimageorientation(); switch ( $orientation ) { case Gmagick::ORIENTATION_BOTTOMRIGHT: // 3 — rotated 180 $im->rotateimage( '#000000', 180 ); break; case Gmagick::ORIENTATION_RIGHTTOP: // 6 — rotated 90 CW $im->rotateimage( '#000000', 90 ); break; case Gmagick::ORIENTATION_LEFTBOTTOM: // 8 — rotated 270 CW $im->rotateimage( '#000000', 270 ); break; case Gmagick::ORIENTATION_TOPRIGHT: // 2 — flipped horizontal $im->flopimage(); break; case Gmagick::ORIENTATION_BOTTOMLEFT: // 4 — flipped vertical $im->flipimage(); break; case Gmagick::ORIENTATION_LEFTTOP: // 5 — transpose $im->flopimage(); $im->rotateimage( '#000000', 90 ); break; case Gmagick::ORIENTATION_RIGHTBOTTOM: // 7 — transverse $im->flopimage(); $im->rotateimage( '#000000', 270 ); break; } if ( $orientation > 1 ) { $im->setimageorientation( Gmagick::ORIENTATION_TOPLEFT ); } $quality = ime_get_quality( $resize_mode ); if ( is_numeric( $quality ) && $quality >= 0 && $quality <= 100 && ime_im_filename_is_jpg( $new_file ) ) { $im->setimagecompression( Gmagick::COMPRESSION_JPEG ); $im->setimagecompressionquality( intval( $quality ) ); } if ( ime_interlace() && defined( 'Gmagick::INTERLACE_PLANE' ) ) { $im->setinterlacescheme( Gmagick::INTERLACE_PLANE ); } if ( $resize_mode == 'size' ) { if ( ime_keep_exif() ) { foreach ( [ 'iptc', '8bim', 'xmp', 'APP13' ] as $profile ) { try { $im->removeimageprofile( $profile ); } catch ( GmagickException $e ) { // Profile may not exist — not an error } } } else { $im->stripimage(); } } $orig_width = $im->getimagewidth(); $orig_height = $im->getimageheight(); if ( $crop ) { if ( ( $orig_width / $width ) < ( $orig_height / $height ) ) { $crop_width = $orig_width; $crop_height = ceil( ( $height * $orig_width ) / $width ); $off_x = 0; $off_y = ceil( ( $orig_height - $crop_height ) / 2 ); } else { $crop_width = ceil( ( $width * $orig_height ) / $height ); $crop_height = $orig_height; $off_x = ceil( ( $orig_width - $crop_width ) / 2 ); $off_y = 0; } $im->cropimage( intval( $crop_width ), intval( $crop_height ), intval( $off_x ), intval( $off_y ) ); } $im->scaleimage( $width, $height, true ); $im->writeimage( $new_file ); return file_exists( $new_file ); } catch ( GmagickException $ge ) { return false; } } /* * ImageMagick executable handling */ // Check if path is executable depending on OS function ime_is_executable($fullpath) { if ( ! function_exists('proc_open') ) { return @is_executable($fullpath); } $whereIsCommand = (PHP_OS == 'WINNT') ? 'where' : 'which'; $process = proc_open( [ $whereIsCommand, $fullpath ], [ 1 => [ 'pipe', 'w' ], 2 => [ 'pipe', 'w' ] ], $pipes ); if ( ! is_resource($process) ) { return false; } $output = trim( stream_get_contents( $pipes[1] ) ); fclose( $pipes[1] ); fclose( $pipes[2] ); proc_close( $process ); return ! empty( $output ); } // Do we have a valid CLI executable set? Pass $is_gm = true for GraphicsMagick. function ime_im_cli_valid( $is_gm = false ) { $transient = $is_gm ? 'ime_gm_valid' : 'ime_cli_valid'; if ( WP_DEBUG || false === ( $valid = get_transient( $transient ) ) ) { $cmd = ime_im_cli_command( $is_gm ); $valid = ( ! empty( $cmd ) && ime_is_executable( $cmd ) ) ? 'yes' : 'no'; set_transient( $transient, $valid, DAY_IN_SECONDS ); } return $valid === 'yes'; } // Test if executable is a working IM or GM binary. function ime_im_cli_check_executable( $fullpath, $is_gm = false ) { if ( ! @is_executable( $fullpath ) || ! function_exists( 'proc_open' ) ) { return false; } $args = $is_gm ? [ $fullpath, 'version' ] : [ $fullpath, '--version' ]; $process = proc_open( $args, [ 1 => [ 'pipe', 'w' ], 2 => [ 'pipe', 'w' ] ], $pipes ); if ( ! is_resource( $process ) ) { return false; } $output = stream_get_contents( $pipes[1] ); fclose( $pipes[1] ); fclose( $pipes[2] ); proc_close( $process ); if ( $is_gm ) { preg_match( '/GraphicsMagick ([0-9]+\.[0-9]+(?:\.[0-9]+)?)/', $output, $version ); if ( isset( $version[1] ) ) { ime_set_option( 'graphicsmagick_version', $version[1], true ); return true; } } else { preg_match( '/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $output, $version ); if ( isset( $version[1] ) ) { ime_set_option( 'imagemagick_version', $version[1], true ); return true; } } return false; } /* * Try to get realpath of path * * This won't work if there is open_basename restrictions. */ function ime_try_realpath( $path ) { $realpath = @realpath( $path ); if ( $realpath ) { return $realpath; } else { return $path; } } // Check if a directory contains a working IM or GM executable. function ime_im_cli_check_command( $path, $is_gm = false ) { $path = ime_try_realpath( $path ); $executables = $is_gm ? [ 'gm' ] : [ 'magick', 'convert' ]; foreach ( $executables as $executable ) { $full_path = $path . DIRECTORY_SEPARATOR . $executable; if ( ime_im_cli_check_executable( $full_path, $is_gm ) ) { return $full_path; } $full_path_exe = $full_path . '.exe'; if ( ime_im_cli_check_executable( $full_path_exe, $is_gm ) ) { return $full_path_exe; } } return null; } // Try to auto-discover an IM or GM executable in common paths. function ime_im_cli_find_command( $is_gm = false ) { $possible_paths = [ '/usr/bin', '/usr/local/bin', '/opt/homebrew/bin' ]; foreach ( $possible_paths as $path ) { if ( ime_im_cli_check_command( $path, $is_gm ) ) { return $path; } } return null; } // Get the full path to the IM or GM executable. function ime_im_cli_command( $is_gm = false ) { $path_option = $is_gm ? 'gm_path' : 'cli_path'; $path = ime_get_option( $path_option ); if ( ! empty( $path ) ) { return ime_im_cli_check_command( $path, $is_gm ); } $path = ime_im_cli_find_command( $is_gm ); if ( empty( $path ) ) { return null; } ime_set_option( $path_option, $path, true ); return ime_im_cli_check_command( $path, $is_gm ); } // Thin wrappers so the mode dispatch system finds ime_im_graphicsmagick_valid(). function ime_im_graphicsmagick_valid() { return ime_im_cli_valid( true ); } // Shared resize implementation for both ImageMagick and GraphicsMagick CLI. // GraphicsMagick requires 'convert' as a subcommand; ImageMagick does not. function ime_im_cli_do_resize( $cmd_path, $is_gm, $old_file, $new_file, $width, $height, $crop, $resize_mode ) { $geometry = intval( $width ) . 'x' . intval( $height ); $prefix = $is_gm ? [ $cmd_path, 'convert' ] : [ $cmd_path ]; // Build command args array — passed directly to proc_open, no shell interpretation $cmd_args = array_merge( $prefix, [ $old_file, '-auto-orient', // apply Exif rotation to pixels before resizing '-limit', 'memory', '157286400', '-limit', 'map', '134217728', '-resize', $geometry . ( $crop ? '^' : '!' ), ] ); if ( $crop ) { $cmd_args = array_merge( $cmd_args, [ '-gravity', 'center', '-extent', $geometry ] ); } $quality = ime_get_quality( $resize_mode ); if ( is_numeric( $quality ) && $quality >= 0 && $quality <= 100 && ime_im_filename_is_jpg( $new_file ) ) { $cmd_args = array_merge( $cmd_args, [ '-quality', (string) intval( $quality ) ] ); } if ( ime_interlace() ) { $cmd_args = array_merge( $cmd_args, [ '-interlace', 'Plane' ] ); } if ( $resize_mode == 'size' ) { if ( ime_keep_exif() ) { // Remove bulky non-Exif profiles; preserve Exif (contains GPS) $cmd_args = array_merge( $cmd_args, [ '+profile', '8bim', '+profile', 'iptc', '+profile', 'xmp' ] ); } else { $cmd_args[] = '-strip'; } } $cmd_args[] = $new_file; $process = proc_open( $cmd_args, [ 1 => [ 'pipe', 'w' ], 2 => [ 'pipe', 'w' ] ], $pipes ); if ( is_resource( $process ) ) { fclose( $pipes[1] ); fclose( $pipes[2] ); proc_close( $process ); } return file_exists( $new_file ); } function ime_im_cli_resize( $old_file, $new_file, $width, $height, $crop, $resize_mode = 'quality' ) { $cmd_path = ime_im_cli_command(); if ( empty( $cmd_path ) ) { return false; } return ime_im_cli_do_resize( $cmd_path, false, $old_file, $new_file, $width, $height, $crop, $resize_mode ); } function ime_im_graphicsmagick_resize( $old_file, $new_file, $width, $height, $crop, $resize_mode = 'quality' ) { $cmd_path = ime_im_cli_command( true ); if ( empty( $cmd_path ) ) { return false; } return ime_im_cli_do_resize( $cmd_path, true, $old_file, $new_file, $width, $height, $crop, $resize_mode ); }