Last active
March 11, 2026 19:25
-
-
Save ozgursar/268751d6fcb9c2d1e90337a5fc0fa007 to your computer and use it in GitHub Desktop.
Image Sizes Manager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Image Sizes Manager | |
| * Paste into functions.php or add via Code Snippets. | |
| * Goes to Tools > Image Sizes in your WordPress admin. | |
| */ | |
| class Image_Sizes_Manager { | |
| /** Option key for persisting disabled size slugs. */ | |
| const OPTION_KEY = 'ism_disabled_sizes'; | |
| /** WordPress built-in sizes (core + WP 5.3 large image sizes). */ | |
| const CORE_SIZES = [ 'thumbnail', 'medium', 'medium_large', 'large', '1536x1536', '2048x2048' ]; | |
| // ========================================================================= | |
| // Bootstrap | |
| // ========================================================================= | |
| public static function init() { | |
| $instance = new self(); | |
| // Identify origins via file scanning once all sizes are registered. | |
| add_action( 'init', [ $instance, 'resolve_origins' ], 9999 ); | |
| // Apply saved disabled sizes after origins are resolved. | |
| add_action( 'init', [ $instance, 'apply_disabled_sizes' ], 10000 ); | |
| // Clear the origins cache whenever themes or plugins change. | |
| add_action( 'switch_theme', [ $instance, 'clear_origins_cache' ] ); | |
| add_action( 'activated_plugin', [ $instance, 'clear_origins_cache' ] ); | |
| add_action( 'deactivated_plugin', [ $instance, 'clear_origins_cache' ] ); | |
| // Admin UI. | |
| add_action( 'admin_menu', [ $instance, 'register_tools_page' ] ); | |
| add_action( 'wp_ajax_ism_toggle_size', [ $instance, 'ajax_toggle_size' ] ); | |
| } | |
| // ========================================================================= | |
| // Origin detection — file scanning with transient cache | |
| // ========================================================================= | |
| /** | |
| * Populate $GLOBALS['_ism_origins'] mapping size slug → source label. | |
| * Uses a 1-hour transient so scanning only happens once per hour, not per request. | |
| */ | |
| public function resolve_origins() { | |
| $cached = get_transient( 'ism_origins_cache' ); | |
| if ( $cached !== false ) { | |
| $GLOBALS['_ism_origins'] = $cached; | |
| return; | |
| } | |
| $GLOBALS['_ism_origins'] = []; | |
| // Include disabled sizes (removed from registry) so their origin is preserved. | |
| $all_slugs = array_unique( array_merge( | |
| get_intermediate_image_sizes(), | |
| get_option( self::OPTION_KEY, [] ) | |
| ) ); | |
| foreach ( $all_slugs as $size ) { | |
| if ( in_array( $size, self::CORE_SIZES, true ) ) { | |
| continue; | |
| } | |
| $GLOBALS['_ism_origins'][ $size ] = $this->find_origin( $size ); | |
| } | |
| set_transient( 'ism_origins_cache', $GLOBALS['_ism_origins'], HOUR_IN_SECONDS ); | |
| } | |
| /** | |
| * Scan active theme(s) then active plugins to find who registered a given size. | |
| */ | |
| private function find_origin( $size ) { | |
| $active_dir = get_stylesheet_directory(); | |
| $parent_dir = get_template_directory(); | |
| $candidates = [ | |
| [ 'dir' => $active_dir, 'label' => wp_get_theme()->get( 'Name' ) . ' (Theme)' ], | |
| ]; | |
| if ( $active_dir !== $parent_dir ) { | |
| $candidates[] = [ | |
| 'dir' => $parent_dir, | |
| 'label' => wp_get_theme( get_template() )->get( 'Name' ) . ' (Parent Theme)', | |
| ]; | |
| } | |
| foreach ( $candidates as $c ) { | |
| if ( $this->dir_has_size( $c['dir'], $size ) ) { | |
| return $c['label']; | |
| } | |
| } | |
| foreach ( get_option( 'active_plugins', [] ) as $plugin_file ) { | |
| $plugin_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file ); | |
| if ( ! is_dir( $plugin_dir ) ) { | |
| continue; | |
| } | |
| if ( $this->dir_has_size( $plugin_dir, $size ) ) { | |
| $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file, false, false ); | |
| $name = ! empty( $data['Name'] ) ? $data['Name'] : dirname( $plugin_file ); | |
| return $name . ' (Plugin)'; | |
| } | |
| } | |
| return 'Unknown'; | |
| } | |
| /** | |
| * Recursively search PHP files in $dir for add_image_size( 'size-slug' ). | |
| */ | |
| private function dir_has_size( $dir, $size ) { | |
| if ( ! is_dir( $dir ) ) { | |
| return false; | |
| } | |
| $iterator = new RecursiveIteratorIterator( | |
| new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ) | |
| ); | |
| foreach ( $iterator as $file ) { | |
| if ( $file->getExtension() !== 'php' ) { | |
| continue; | |
| } | |
| $contents = @file_get_contents( $file->getPathname() ); | |
| if ( $contents === false ) { | |
| continue; | |
| } | |
| if ( | |
| strpos( $contents, "add_image_size( '{$size}'" ) !== false || | |
| strpos( $contents, "add_image_size('{$size}'" ) !== false | |
| ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| public function clear_origins_cache() { | |
| delete_transient( 'ism_origins_cache' ); | |
| } | |
| // ========================================================================= | |
| // Enforce disabled sizes | |
| // ========================================================================= | |
| public function apply_disabled_sizes() { | |
| foreach ( get_option( self::OPTION_KEY, [] ) as $size ) { | |
| if ( ! in_array( $size, self::CORE_SIZES, true ) ) { | |
| remove_image_size( $size ); | |
| } | |
| } | |
| } | |
| // ========================================================================= | |
| // AJAX — toggle a size on / off | |
| // ========================================================================= | |
| public function ajax_toggle_size() { | |
| check_ajax_referer( 'ism_nonce', 'nonce' ); | |
| if ( ! current_user_can( 'manage_options' ) ) { | |
| wp_send_json_error( 'Insufficient permissions.' ); | |
| } | |
| $size = sanitize_key( $_POST['size'] ?? '' ); | |
| $toggle = sanitize_key( $_POST['toggle'] ?? '' ); // 'disable' | 'enable' | |
| if ( empty( $size ) || ! in_array( $toggle, [ 'disable', 'enable' ], true ) ) { | |
| wp_send_json_error( 'Invalid request.' ); | |
| } | |
| if ( in_array( $size, self::CORE_SIZES, true ) ) { | |
| wp_send_json_error( 'Core sizes cannot be disabled.' ); | |
| } | |
| $disabled = get_option( self::OPTION_KEY, [] ); | |
| if ( $toggle === 'disable' ) { | |
| if ( ! in_array( $size, $disabled, true ) ) { | |
| $disabled[] = $size; | |
| } | |
| } else { | |
| $disabled = array_values( array_diff( $disabled, [ $size ] ) ); | |
| } | |
| update_option( self::OPTION_KEY, $disabled ); | |
| wp_send_json_success( [ | |
| 'message' => $toggle === 'disable' | |
| ? "'{$size}' disabled — won't be generated on future uploads." | |
| : "'{$size}' re-enabled.", | |
| ] ); | |
| } | |
| // ========================================================================= | |
| // Data helper | |
| // ========================================================================= | |
| private function get_sizes_data() { | |
| // We need the full list including disabled sizes, but apply_disabled_sizes() | |
| // already called remove_image_size() on them, so they're gone from the global. | |
| // Solution: snapshot ALL sizes before removal by reading the stored snapshot, | |
| // or simply re-add them temporarily just to read their dimensions. | |
| // The cleanest approach: store full size metadata in our own option at scan time. | |
| global $_wp_additional_image_sizes; | |
| $origins = $GLOBALS['_ism_origins'] ?? []; | |
| $disabled = get_option( self::OPTION_KEY, [] ); | |
| // Merge currently registered sizes with persisted metadata for disabled ones. | |
| $registered = get_intermediate_image_sizes(); | |
| $custom_sizes = $_wp_additional_image_sizes ?? []; | |
| // Load persisted size metadata (width/height/crop) for disabled sizes. | |
| $persisted_meta = get_option( 'ism_size_meta', [] ); | |
| // Build the full slug list: registered + any disabled ones not currently registered. | |
| $all_slugs = $registered; | |
| foreach ( $disabled as $slug ) { | |
| if ( ! in_array( $slug, $all_slugs, true ) ) { | |
| $all_slugs[] = $slug; | |
| } | |
| } | |
| $sizes = []; | |
| foreach ( $all_slugs as $slug ) { | |
| $is_disabled = in_array( $slug, $disabled, true ); | |
| if ( isset( $custom_sizes[ $slug ] ) ) { | |
| $width = $custom_sizes[ $slug ]['width']; | |
| $height = $custom_sizes[ $slug ]['height']; | |
| $crop = $custom_sizes[ $slug ]['crop']; | |
| // Persist this metadata so we still have it when the size is disabled. | |
| $persisted_meta[ $slug ] = compact( 'width', 'height', 'crop' ); | |
| } elseif ( in_array( $slug, $registered, true ) ) { | |
| $width = get_option( "{$slug}_size_w" ); | |
| $height = get_option( "{$slug}_size_h" ); | |
| $crop = get_option( "{$slug}_crop" ); | |
| } elseif ( isset( $persisted_meta[ $slug ] ) ) { | |
| // Size is disabled and removed from registry — use stored metadata. | |
| $width = $persisted_meta[ $slug ]['width']; | |
| $height = $persisted_meta[ $slug ]['height']; | |
| $crop = $persisted_meta[ $slug ]['crop']; | |
| } else { | |
| $width = '—'; | |
| $height = '—'; | |
| $crop = false; | |
| } | |
| $is_core = in_array( $slug, self::CORE_SIZES, true ); | |
| if ( $is_core ) { | |
| $source = 'WordPress Core'; | |
| $type = 'core'; | |
| } elseif ( isset( $origins[ $slug ] ) ) { | |
| $source = $origins[ $slug ]; | |
| $type = false !== strpos( $source, 'Theme' ) ? 'theme' : 'plugin'; | |
| } else { | |
| $source = 'Unknown'; | |
| $type = 'unknown'; | |
| } | |
| $sizes[] = [ | |
| 'slug' => $slug, | |
| 'width' => $width, | |
| 'height' => $height, | |
| 'crop' => $crop, | |
| 'source' => $source, | |
| 'type' => $type, | |
| 'is_core' => $is_core, | |
| 'disabled' => $is_disabled, | |
| ]; | |
| } | |
| // Persist metadata so disabled sizes still have dimensions next page load. | |
| update_option( 'ism_size_meta', $persisted_meta, false ); | |
| return $sizes; | |
| } | |
| // ========================================================================= | |
| // Tools page | |
| // ========================================================================= | |
| public function register_tools_page() { | |
| add_management_page( | |
| 'Image Sizes Manager', | |
| 'Image Sizes', | |
| 'manage_options', | |
| 'image-sizes-manager', | |
| [ $this, 'render_tools_page' ] | |
| ); | |
| } | |
| public function render_tools_page() { | |
| if ( ! current_user_can( 'manage_options' ) ) { | |
| return; | |
| } | |
| $sizes = $this->get_sizes_data(); | |
| $total = count( $sizes ); | |
| $extra = count( array_filter( $sizes, fn( $s ) => ! $s['is_core'] ) ); | |
| $disabled = get_option( self::OPTION_KEY, [] ); | |
| $nonce = wp_create_nonce( 'ism_nonce' ); | |
| ?> | |
| <div class="wrap"> | |
| <h1>📐 Image Sizes Manager</h1> | |
| <p> | |
| This site has <strong><?php echo $total; ?> registered image sizes</strong> | |
| — <?php echo $extra; ?> added by themes or plugins<?php echo count( $disabled ) ? ', <strong>' . count( $disabled ) . ' disabled</strong>' : ''; ?>. | |
| Use the toggles below to stop WordPress generating a size on future uploads. Existing files on disk are not affected. | |
| </p> | |
| <table class="wp-list-table widefat fixed striped" style="margin-top:16px;"> | |
| <thead> | |
| <tr> | |
| <th style="width:190px;">Size Slug</th> | |
| <th style="width:70px;">Width</th> | |
| <th style="width:70px;">Height</th> | |
| <th style="width:100px;">Crop</th> | |
| <th>Registered By</th> | |
| <th style="width:130px;text-align:center;">Status</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <?php foreach ( $sizes as $s ) : | |
| $row_id = 'ism-row-' . esc_attr( $s['slug'] ); | |
| ?> | |
| <tr id="<?php echo $row_id; ?>" class="<?php echo $s['disabled'] ? 'ism-row--disabled' : ''; ?>"> | |
| <td><code><?php echo esc_html( $s['slug'] ); ?></code></td> | |
| <td><?php echo esc_html( $s['width'] ); ?>px</td> | |
| <td><?php echo esc_html( $s['height'] ); ?>px</td> | |
| <td style="color:#666;"><?php echo $s['crop'] ? 'Hard crop' : 'Soft scale'; ?></td> | |
| <td> | |
| <span class="ism-badge ism-badge--<?php echo esc_attr( $s['type'] ); ?>"> | |
| <?php echo esc_html( $s['source'] ); ?> | |
| </span> | |
| </td> | |
| <td style="text-align:center;"> | |
| <?php if ( $s['is_core'] ) : ?> | |
| <span class="ism-core-label">Core — protected</span> | |
| <?php else : ?> | |
| <label class="ism-toggle"> | |
| <input | |
| type="checkbox" | |
| class="ism-toggle__cb" | |
| data-size="<?php echo esc_attr( $s['slug'] ); ?>" | |
| data-nonce="<?php echo esc_attr( $nonce ); ?>" | |
| <?php checked( ! $s['disabled'] ); ?> | |
| > | |
| <span class="ism-toggle__track"></span> | |
| <span class="ism-toggle__label"><?php echo $s['disabled'] ? 'Disabled' : 'Active'; ?></span> | |
| </label> | |
| <?php endif; ?> | |
| </td> | |
| </tr> | |
| <?php endforeach; ?> | |
| </tbody> | |
| </table> | |
| <p style="margin-top:14px;color:#666;font-size:13px;"> | |
| ⚠️ Disabling a size only prevents generation on <strong>new uploads</strong>. | |
| To clean up existing resized files, use | |
| <a href="https://wordpress.org/plugins/media-cleaner/" target="_blank">Media Cleaner</a>. | |
| </p> | |
| <div id="ism-notice" style="display:none;margin-top:10px;" class="notice inline"></div> | |
| </div><!-- .wrap --> | |
| <style> | |
| .ism-badge { | |
| display: inline-block; | |
| padding: 2px 10px; | |
| border-radius: 3px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: #fff; | |
| } | |
| .ism-badge--core { background: #3c8f3c; } | |
| .ism-badge--theme { background: #6a5acd; } | |
| .ism-badge--plugin { background: #c0392b; } | |
| .ism-badge--unknown { background: #999; } | |
| .ism-core-label { | |
| font-size: 12px; | |
| color: #999; | |
| } | |
| .ism-row--disabled td:not(:last-child) { | |
| opacity: 0.4; | |
| } | |
| .ism-toggle { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| .ism-toggle__cb { | |
| position: absolute; | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .ism-toggle__track { | |
| position: relative; | |
| display: inline-block; | |
| width: 36px; | |
| height: 20px; | |
| background: #ccc; | |
| border-radius: 20px; | |
| transition: background .2s; | |
| flex-shrink: 0; | |
| } | |
| .ism-toggle__track::after { | |
| content: ''; | |
| position: absolute; | |
| top: 3px; | |
| left: 3px; | |
| width: 14px; | |
| height: 14px; | |
| background: #fff; | |
| border-radius: 50%; | |
| transition: transform .2s; | |
| } | |
| .ism-toggle__cb:checked + .ism-toggle__track { background: #3c8f3c; } | |
| .ism-toggle__cb:checked + .ism-toggle__track::after { transform: translateX(16px); } | |
| .ism-toggle__label { | |
| font-size: 12px; | |
| color: #666; | |
| min-width: 48px; | |
| } | |
| </style> | |
| <script> | |
| (function () { | |
| var notice = document.getElementById( 'ism-notice' ); | |
| function flash( msg, type ) { | |
| notice.className = 'notice notice-' + type + ' inline'; | |
| notice.innerHTML = '<p>' + msg + '</p>'; | |
| notice.style.display = 'block'; | |
| setTimeout( function () { notice.style.display = 'none'; }, 4000 ); | |
| } | |
| document.querySelectorAll( '.ism-toggle__cb' ).forEach( function ( cb ) { | |
| cb.addEventListener( 'change', function () { | |
| var size = this.dataset.size; | |
| var nonce = this.dataset.nonce; | |
| var enabled = this.checked; | |
| var row = document.getElementById( 'ism-row-' + size ); | |
| var label = this.closest( '.ism-toggle' ).querySelector( '.ism-toggle__label' ); | |
| // Optimistic UI. | |
| row.classList.toggle( 'ism-row--disabled', ! enabled ); | |
| label.textContent = enabled ? 'Active' : 'Disabled'; | |
| var fd = new FormData(); | |
| fd.append( 'action', 'ism_toggle_size' ); | |
| fd.append( 'nonce', nonce ); | |
| fd.append( 'size', size ); | |
| fd.append( 'toggle', enabled ? 'enable' : 'disable' ); | |
| fetch( ajaxurl, { method: 'POST', body: fd } ) | |
| .then( function ( r ) { return r.json(); } ) | |
| .then( function ( res ) { | |
| if ( res.success ) { | |
| flash( '✓ ' + res.data.message, 'success' ); | |
| } else { | |
| cb.checked = ! enabled; | |
| row.classList.toggle( 'ism-row--disabled', enabled ); | |
| label.textContent = enabled ? 'Disabled' : 'Active'; | |
| flash( '✗ ' + ( res.data || 'Something went wrong.' ), 'error' ); | |
| } | |
| } ) | |
| .catch( function () { | |
| cb.checked = ! enabled; | |
| row.classList.toggle( 'ism-row--disabled', enabled ); | |
| label.textContent = enabled ? 'Disabled' : 'Active'; | |
| flash( '✗ Request failed — please try again.', 'error' ); | |
| } ); | |
| } ); | |
| } ); | |
| } )(); | |
| </script> | |
| <?php | |
| } | |
| } | |
| Image_Sizes_Manager::init(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment