Skip to content

Instantly share code, notes, and snippets.

@ozgursar
Last active March 11, 2026 19:25
Show Gist options
  • Select an option

  • Save ozgursar/268751d6fcb9c2d1e90337a5fc0fa007 to your computer and use it in GitHub Desktop.

Select an option

Save ozgursar/268751d6fcb9c2d1e90337a5fc0fa007 to your computer and use it in GitHub Desktop.
Image Sizes Manager
/**
* 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