settings = [ 'version' => get_plugin_data(__FILE__)['Version'], 'url' => plugin_dir_url(__FILE__), 'path' => plugin_dir_path(__FILE__), ]; $this->temp_path = null; // set text domain // https://codex.wordpress.org/Function_Reference/load_plugin_textdomain load_plugin_textdomain('acf-image-aspect-ratio-crop'); add_action('plugins_loaded', [$this, 'initialize_settings']); // include field add_action('acf/include_field_types', [ $this, 'include_field_types', ]); // v5 add_action('acf/save_post', function ($post_id) { $this->debug('post_id'); $this->debug($post_id); $this->debug('POST'); $this->debug($_POST); if ($post_id === 'options' && !empty($_GET['page'])) { // Options page needs an unique id $post_id = $_GET['page']; } $temp_post_id = $_POST['aiarc_temp_post_id']; // Bail early if we don't have data to process if (empty($temp_post_id)) { return; } // Let's find all posts with temp post id $temp_attachments = get_posts([ 'post_type' => 'attachment', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => 'acf_image_aspect_ratio_crop_temp_post_id', 'value' => $temp_post_id, 'compare' => '=', ], ], ]); foreach ($temp_attachments as $attachment) { // Attach parent post id to temporary attachments update_post_meta($attachment->ID, 'acf_image_aspect_ratio_crop_parent_post_id', $post_id); // Remove temporary data delete_post_meta($attachment->ID, 'acf_image_aspect_ratio_crop_temp_post_id'); delete_post_meta($attachment->ID, 'acf_image_aspect_ratio_crop_timestamp'); } // Bail early if unused attachment deletion is disabled if (!$this->user_settings['delete_unused']) { return; } $post_attachments = get_posts([ 'post_type' => 'attachment', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => 'acf_image_aspect_ratio_crop_parent_post_id', 'value' => $post_id, 'compare' => '=', ], ], ]); // Find crop field names // Compare crop field names to post input // Delete unused posts $this->debug('found following post attachments'); $this->debug($post_attachments); $this->debug('found following fields'); $fields = $_POST['acf']; $this->debug($fields); $preserve_ids = []; $this->check_fields($fields, $preserve_ids); $post_attachment_ids = array_map(function ($attachment){ return $attachment->ID; }, $post_attachments); $delete_ids = array_diff($post_attachment_ids, $preserve_ids); $this->debug('preserve ids'); $this->debug($preserve_ids); $this->debug('all ids'); $this->debug($post_attachment_ids); $this->debug('delete ids'); $this->debug($delete_ids); foreach ($delete_ids as $delete_id) { wp_delete_attachment($delete_id, true); } }, 15); add_action('wp_ajax_acf_image_aspect_ratio_crop_crop', function () { // WTF WordPress $post = array_map('stripslashes_deep', $_POST); $data = json_decode($post['data'], true); $image_data = wp_get_attachment_metadata($data['id']); // If the difference between the images is less than half a percentage, use the original image // prettier-ignore if ($image_data['height'] - $data['height'] < $image_data['height'] * 0.005 && $image_data['width'] - $data['width'] < $image_data['width'] * 0.005 && $data['cropType'] !== 'pixel_size' ) { wp_send_json(['id' => $data['id']]); wp_die(); } do_action('aiarc_pre_customize_upload_dir'); $media_dir = wp_upload_dir(); do_action('aiarc_after_customize_upload_dir'); // WP Smush compat: use original image if it exists $file = $media_dir['basedir'] . '/' . $image_data['file']; $parts = explode('.', $file); $extension = array_pop($parts); $backup_file = implode('.', $parts) . '.bak.' . $extension; $image = null; $scaled_data = null; if ( file_exists($file) && function_exists('wp_get_original_image_path') && wp_get_original_image_path($data['id']) && wp_get_original_image_path($data['id']) !== $file ) { // Handle the new asinine feature in WP 5.3 which resizes images without asking the user. We want the // original image so we do original_image -> crop instead or original_image -> resized_image -> crop $resized_image = wp_get_image_editor($file); $image = wp_get_image_editor(wp_get_original_image_path($data['id'])); $resized_width = $resized_image->get_size()['width']; $original_width = $image->get_size()['width']; // Get the scale $scale = $original_width / $resized_width; // Clone data array $scaled_data = $data; // Scale crop coordinates to fit larger image $scaled_data['x'] = floor($data['x'] * $scale); $scaled_data['y'] = floor($data['y'] * $scale); $scaled_data['width'] = floor($data['width'] * $scale); $scaled_data['height'] = floor($data['height'] * $scale); } else if (file_exists($backup_file)) { $image = wp_get_image_editor($backup_file); } else if (file_exists($file)) { $image = wp_get_image_editor($file); } else { // Let's attempt to get the file by URL $temp_name = wp_generate_uuid4(); $temp_directory = get_temp_dir(); $this->temp_path = $temp_directory . $temp_name; try { $guzzle = new \GuzzleHttp\Client(); $fetched_image = $guzzle->get(wp_get_attachment_url($data['id'])); $result = @file_put_contents($this->temp_path, $fetched_image->getBody()); if ($result === false) { throw new Exception('Failed to save image'); } $image = wp_get_image_editor($this->temp_path); } catch (Exception $exception) { $this->cleanup(); wp_send_json('Failed fetch remote image', 500); wp_die(); } } if (is_wp_error($image)) { $this->cleanup(); wp_send_json('Failed to open image', 500); wp_die(); } // Use scaled coordinates if we have those $this->crop($image, $scaled_data ? $scaled_data : $data); if ($data['cropType'] === 'pixel_size') { $image->resize($data['aspectRatioWidth'], $data['aspectRatioHeight'], true); } // Retrieve original filename and seperate it from its file extension $original_file_name = explode( '.', basename($image_data['file']) ); // Retrieve and remove file extension from array $original_file_extension = array_pop($original_file_name); $width = $data['aspectRatioWidth']; $height = $data['aspectRatioHeight']; $this->debug($data['cropType']); if ($data['cropType'] === 'free_crop') { $width = $data['width']; $height = $data['height']; } // Generate new base filename $target_file_name = implode('.', $original_file_name) . '-aspect-ratio-' . $width . '-' . $height . '.' . $original_file_extension; // Generate target path new file using existing media library $target_file_path = $media_dir['path'] . '/' . wp_unique_filename($media_dir['path'], $target_file_name); // Get the relative path to save as the actual image url $target_relative_path = str_replace( $media_dir['basedir'] . '/', '', $target_file_path ); //$save = $image->save('test.jpg'); $save = $image->save($target_file_path); if (is_wp_error($save)) { $this->cleanup(); wp_send_json('Failed to crop', 500); wp_die(); } $wp_filetype = wp_check_filetype($target_relative_path, null); $attachment = [ 'post_mime_type' => $wp_filetype['type'], 'post_title' => preg_replace( '/\.[^.]+$/', '', $target_file_name ), 'post_content' => '', 'post_status' => 'publish', ]; $attachment_id = wp_insert_attachment( $attachment, $target_relative_path ); if (is_wp_error($attachment_id)) { $this->cleanup(); wp_send_json('Failed to save attachment', 500); wp_die(); } require_once ABSPATH . "wp-admin" . '/includes/image.php'; $attachment_data = wp_generate_attachment_metadata( $attachment_id, $target_file_path ); wp_update_attachment_metadata($attachment_id, $attachment_data); add_post_meta( $attachment_id, 'acf_image_aspect_ratio_crop', true, true ); add_post_meta( $attachment_id, 'acf_image_aspect_ratio_crop_original_image_id', $data['id'], true ); add_post_meta( $attachment_id, 'acf_image_aspect_ratio_crop_coordinates', [ 'x' => $data['x'], 'y' => $data['y'], 'width' => $data['width'], 'height' => $data['height'] ], true ); /* Timestamp so we can purge unattached crop attachments periodically after specific time (like a week or so) */ add_post_meta( $attachment_id, 'acf_image_aspect_ratio_crop_timestamp', (new DateTime())->format('U'), true ); $this->debug('data'); $this->debug($data); $this->debug('temp post id'); $this->debug($data['temp_post_id']); add_post_meta( $attachment_id, 'acf_image_aspect_ratio_crop_temp_post_id', $data['temp_post_id'], true ); // WPML compat do_action('wpml_sync_all_custom_fields', $attachment_id); $this->cleanup(); wp_send_json(['id' => $attachment_id]); wp_die(); }); // WPML compat add_action('wpml_media_create_duplicate_attachment', function($attachment_id, $duplicate_attachment_id) { $keys = [ 'acf_image_aspect_ratio_crop', 'acf_image_aspect_ratio_crop_original_image_id', 'acf_image_aspect_ratio_crop_coordinates' ]; foreach ($keys as $key) { $value = get_post_meta($attachment_id, $key, true); if ($value) { update_post_meta($duplicate_attachment_id, $key, $value); } } }, 25, 2); // Enable Media Replace compat: if file is replaced using Enable Media Replace, wipe the coordinate data add_filter('wp_handle_upload', function ($data) { $id = attachment_url_to_postid($data['url']); if ($id !== 0) { $posts = get_posts([ 'post_type' => 'attachment', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => 'acf_image_aspect_ratio_crop_original_image_id', 'value' => $id, 'compare' => '=', ], [ 'key' => 'acf_image_aspect_ratio_crop_coordinates', 'compare' => 'EXISTS', ], ], ]); if (!empty($posts)) { foreach ($posts as $post) { delete_post_meta($post->ID, 'acf_image_aspect_ratio_crop_coordinates'); } } } return $data; }); // Hide cropped images in media library grid view add_filter('ajax_query_attachments_args', function ($args) { // post__in is only defined when clicking edit button in attachment if (empty($args['post__in'])) { $args['meta_query'] = [ [ 'key' => 'acf_image_aspect_ratio_crop', 'compare' => 'NOT EXISTS', ], ]; } return $args; }); // Add plugin to WordPress admin menu add_action('admin_menu', function () { add_submenu_page( null, __("ACF Image Aspect Ratio Crop", "acf-image-aspect-ratio-crop"), __("ACF Image Aspect Ratio Crop", "acf-image-aspect-ratio-crop"), 'manage_options', "acf-image-aspect-ratio-crop", [$this, 'settings_page'] ); }); // Add settings link on the plugin page add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) { $settings_link = '' . __('Settings', "acf-image-aspect-ratio-crop") . ''; array_unshift($links, $settings_link); return $links; }); if (!wp_next_scheduled('aiarc_delete_unused_attachments')) { wp_schedule_event(time(), 'daily', 'aiarc_delete_unused_attachments'); } add_action('aiarc_delete_unused_attachments', [$this, 'delete_unused_attachments']); } /* * include_field_types * * This function will include the field type class * * @type function * @date 17/02/2016 * @since 1.0.0 * * @param $version (int) major ACF version. Defaults to false * @return n/a */ function include_field_types() { // include include_once 'fields/class-npx-acf-field-image-aspect-ratio-crop-v5.php'; } /** * Render WordPress plugin settings page */ public function settings_page() { $updated = false; $settings = $this->user_settings; if (!empty($_POST)) { check_admin_referer("acf-image-aspect-ratio-crop"); if (!empty($_POST['modal_type'])) { $settings['modal_type'] = $_POST['modal_type']; } if (!empty($_POST['delete_unused'])) { $settings['delete_unused'] = filter_var($_POST['delete_unused'], FILTER_VALIDATE_BOOLEAN); } update_option("acf-image-aspect-ratio-crop-settings", $settings); $updated = true; } $modal_type = $settings['modal_type']; $delete_unused = $settings['delete_unused']; echo '
'; echo '

' . __('ACF Image Aspect Ratio Crop', 'acf-image-aspect-ratio-crop') . '

'; echo '
'; if ($updated) { echo '
'; echo '

' . __('Options have been updated', "acf-image-aspect-ratio-crop") . '

'; echo '
'; } echo '
'; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo '
'; echo ' '; echo ' '; echo '

'; echo '

'; echo '
'; echo ' '; echo ' '; echo '

'; echo '

'; echo '
'; echo '

'. __('Please note that "Delete unused cropped images" feature is a beta feature because it requires more testing. Please do not enable the option without first backing up your database and uploads in order to prevent potential data loss.', 'acf-image-aspect-ratio-crop') . '

'; echo '

'; echo ' '; echo '

'; wp_nonce_field("acf-image-aspect-ratio-crop"); echo '
'; echo '
'; } function initialize_settings() { $database_version = get_option('acf-image-aspect-ratio-crop-version'); $plugin_version = $this->settings['version']; $settings = get_option('acf-image-aspect-ratio-crop-settings') ? get_option('acf-image-aspect-ratio-crop-settings') : []; // Initialize database settings if (empty($database_version)) { update_option('acf-image-aspect-ratio-crop-version', $plugin_version); } if (version_compare(get_option('acf-image-aspect-ratio-crop-version'), $plugin_version, 'lt')) { // Database migrations here update_option('acf-image-aspect-ratio-crop-version', $plugin_version); }; $default_user_settings = [ 'modal_type' => 'cropped', 'delete_unused' => false, ]; $this->user_settings = array_merge($default_user_settings, $settings); $this->settings['user_settings'] = $this->user_settings; } /** * Clean up any temporary files */ private function cleanup() { if ($this->temp_path) { @unlink($this->temp_path); } } public function delete_unused_attachments () { $this->debug('delete unused attachments cron'); // Bail early if unused attachment deletion is disabled if (!$this->user_settings['delete_unused']) { $this->debug('user has disabled unused attachment deletion'); return; } $timestamp = (new DateTime()) ->modify('-7 days') ->format('U'); $posts = get_posts([ 'post_type' => 'attachment', 'meta_query' => [ [ 'key' => 'acf_image_aspect_ratio_crop_timestamp', 'compare' => '<', 'value' => $timestamp, 'type' => 'numeric', ], ], ]); foreach ($posts as $post) { $this->debug('deleting unused attachment ' . $post->ID); wp_delete_attachment($post->ID, true); } } function debug($message) { if (defined('WP_DEBUG') && WP_DEBUG === true) { error_log(print_r($message, true)); } } private function crop(WP_Image_Editor $image, $data) { $image->crop( $data['x'], $data['y'], $data['width'], $data['height'] ); } public function check_fields($fields, &$preserve_ids) { $this->debug($preserve_ids); foreach ($fields as $key => $field) { if(is_array($field)) { $this->check_fields($field, $preserve_ids); } // This is kinda of a hack but nested fields are named like field_59416ac78945f_field_59217cf6eb710 in the // POST request and we are only interested in the last part so we just use a regex here to chop off the // last part preg_match_all('/field_[a-z0-9]+/', $key, $matches); if (!empty($matches[0])) { $last = array_values(array_slice($matches[0], -1))[0]; $definition = get_field_object($last); if (!empty($field) && $definition['type'] === 'image_aspect_ratio_crop') { array_push($preserve_ids, $field); } } } } } // initialize new npx_acf_plugin_image_aspect_ratio_crop();