⏲️ Estimated reading time: 21 min
Table of Contents
Learn how to build a lightweight WordPress “Image Defect Scanner” that finds missing, corrupted, zero-byte, unreadable, and MIME-mismatched images inside your Media Library and uploads folder. This guide includes full plugin code, an admin UI, batch AJAX scanning, and CSV export safe, fast, and beginner-friendly.
WordPress Image Defect Scanner Plugin (PHP + AJAX)
What This WordPress Image Defect Scanner Actually Solves
Broken images are sneaky. Sometimes you notice them right away. Other times they sit quietly for weeks, hurting your site in ways that don’t look obvious:
Slow pages because the browser keeps retrying failed assets.
Ugly placeholders in posts that make content look abandoned.
Damaged trust when visitors see “missing image” icons.
Extra support tickets because users think the site is “buggy.”
SEO issues when important visuals fail to load.
Most WordPress sites eventually end up with image problems for simple reasons:
You migrated the site and not all uploads copied.
A backup restored the database but not the uploads folder.
A CDN or caching plugin rewrote image paths incorrectly.
A cleanup script removed files but left attachments behind.
FTP uploads created “orphan” images not registered in Media Library.
That’s why a scanner is useful: it tells you what’s wrong, where it is, and what needs attention without guessing.
What The Scanner Checks
This plugin is designed to detect real file defects, not “opinions.” It focuses on practical failures that break images in the real world.
Missing File
The attachment exists in WordPress, but the file is gone from disk.
Permission Denied
The file exists, but PHP can’t read it (wrong permissions/ownership).
Empty File (0 bytes)
A failed upload or interrupted transfer often results in a zero-byte file.
Oversized File
Large files can overload scans. We set a safety limit to avoid server stress.
Invalid MIME Type
The file extension may say “jpg,” but the actual file is not an image.
MIME Mismatch
WordPress expects one MIME, but the server detects another. This commonly happens after conversions (e.g., JPG → WebP/AVIF).
Corrupted / Truncated Images
getimagesize() fails and signature checks detect incomplete files (like JPEG missing end marker).
Why This Version Is Better Than “Scan Everything In One Request”
Many examples online scan all attachments in one run (posts_per_page => -1). That works on small sites. On real sites, it often fails because of:
Timeouts (max_execution_time)
Memory limits
Large Media Libraries
Heavy disk I/O
This plugin solves it by scanning in AJAX batches:
The admin page triggers an AJAX request.
Server scans only 80–200 items per request.
The cursor moves forward and continues until done.
The UI updates progress and logs issues.
You can export results to CSV at the end.
It’s the safest approach for shared hosting and still fast on VPS.
Plugin File Structure
Create a folder:
wp-content/plugins/hz-image-defect-scanner/
Inside it, create these files:
hz-image-defect-scanner.phpassets/admin.jsassets/admin.css
Full Plugin Code (PHP)
Create:
wp-content/plugins/hz-image-defect-scanner/hz-image-defect-scanner.php
<?php
/**
* Plugin Name: HZ Image Defect Scanner
* Description: Scans Media Library + uploads for missing, corrupted, zero-byte, unreadable, invalid MIME, MIME mismatch, truncated images. Admin UI + AJAX batch scan + CSV export.
* Version: 1.0.0
* Author: Tokyo Blade
* Requires at least: 6.0
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) exit;
final class HZ_Image_Defect_Scanner {
private static ?self $instance = null;
private const NONCE_ACTION = 'hz_ids_nonce';
private const AJAX_ACTION_SCAN = 'hz_ids_scan_step';
private const AJAX_ACTION_EXPORT = 'hz_ids_export_csv';
// Tuning knobs
private int $batch_size = 120; // items per AJAX step
private int $max_file_size = 52428800; // 50MB safety limit
private array $allowed_mimes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/bmp',
'image/x-ms-bmp',
'image/avif',
];
private array $allowed_ext = ['jpg','jpeg','png','gif','webp','bmp','avif'];
public static function instance(): self {
return self::$instance ??= new self();
}
private function __construct() {
add_action('admin_menu', [$this, 'admin_menu']);
add_action('admin_enqueue_scripts', [$this, 'assets']);
add_action('wp_ajax_' . self::AJAX_ACTION_SCAN, [$this, 'ajax_scan_step']);
add_action('wp_ajax_' . self::AJAX_ACTION_EXPORT, [$this, 'ajax_export_csv']);
}
public function admin_menu(): void {
add_management_page(
'Image Defect Scanner',
'Image Defect Scanner',
'manage_options',
'hz-image-defect-scanner',
[$this, 'render_page']
);
}
public function assets(string $hook): void {
if ($hook !== 'tools_page_hz-image-defect-scanner') return;
wp_enqueue_style(
'hz-ids-admin',
plugin_dir_url(__FILE__) . 'assets/admin.css',
[],
'1.0.0'
);
wp_enqueue_script(
'hz-ids-admin',
plugin_dir_url(__FILE__) . 'assets/admin.js',
['jquery'],
'1.0.0',
true
);
wp_localize_script('hz-ids-admin', 'HZ_IDS', [
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce(self::NONCE_ACTION),
]);
}
public function render_page(): void {
if (!current_user_can('manage_options')) {
wp_die('Permission denied');
}
?>
<div class="wrap hz-ids-wrap">
<h1>Image Defect Scanner</h1>
<div class="hz-ids-intro">
<p>Scan your WordPress images to detect real file issues: missing files, unreadable permissions, 0-byte files, invalid or mismatched MIME types, corrupted/truncated images, and uploads orphan files.</p>
</div>
<div class="notice notice-info">
<p><strong>What this scanner checks:</strong> missing file, unreadable, empty file, oversized, invalid MIME/mismatch, getimagesize failures, JPEG/PNG/GIF signature checks.</p>
</div>
<div class="hz-ids-controls">
<button class="button button-primary" id="hz-ids-start" data-mode="library">Scan Media Library</button>
<button class="button" id="hz-ids-start-uploads" data-mode="uploads">Scan Uploads Folder</button>
<button class="button button-secondary" id="hz-ids-export" disabled>Export CSV</button>
<span class="hz-ids-status" id="hz-ids-status"></span>
</div>
<div class="hz-ids-progress" id="hz-ids-progress" style="display:none;">
<div class="hz-ids-bar">
<div class="hz-ids-bar-fill" id="hz-ids-bar-fill" style="width:0%;"></div>
</div>
<div class="hz-ids-progress-text" id="hz-ids-progress-text">Initializing...</div>
</div>
<div class="hz-ids-results" id="hz-ids-results">
<h2>Results</h2>
<p>Click one of the scan buttons to begin.</p>
</div>
</div>
<?php
}
public function ajax_scan_step(): void {
$this->verify_ajax();
$mode = isset($_POST['mode']) ? sanitize_text_field($_POST['mode']) : 'library';
$cursor = isset($_POST['cursor']) ? max(0, intval($_POST['cursor'])) : 0;
$payload = ($mode === 'uploads')
? $this->scan_uploads_step($cursor)
: $this->scan_library_step($cursor);
wp_send_json_success($payload);
}
public function ajax_export_csv(): void {
$this->verify_ajax();
$issues = isset($_POST['issues']) ? wp_unslash($_POST['issues']) : '';
if (empty($issues)) {
wp_send_json_error('No data');
}
$decoded = json_decode($issues, true);
if (!is_array($decoded)) {
wp_send_json_error('Invalid JSON');
}
nocache_headers();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="image-defect-report-' . gmdate('Y-m-d-H-i-s') . '.csv"');
$out = fopen('php://output', 'w');
fputcsv($out, ['Type', 'Message', 'URL', 'Path', 'Attachment ID', 'In Library']);
foreach ($decoded as $row) {
$type = isset($row['type']) ? (string)$row['type'] : '';
$msg = isset($row['message']) ? (string)$row['message'] : '';
$url = isset($row['url']) ? (string)$row['url'] : '';
$path = isset($row['path']) ? (string)$row['path'] : '';
$id = isset($row['id']) ? (string)$row['id'] : '0';
$in = !empty($row['in_library']) ? 'Yes' : 'No';
fputcsv($out, [$type, $msg, $url, $path, $id, $in]);
}
fclose($out);
exit;
}
private function verify_ajax(): void {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
}
private function scan_library_step(int $cursor): array {
$args = [
'post_type' => 'attachment',
'post_mime_type' => 'image',
'post_status' => 'inherit',
'posts_per_page' => $this->batch_size,
'offset' => $cursor,
'fields' => 'ids',
'no_found_rows' => false,
];
$q = new WP_Query($args);
$ids = $q->posts;
$total = (int)$q->found_posts;
$issues = [];
foreach ($ids as $attachment_id) {
$check = $this->check_attachment((int)$attachment_id);
if (!empty($check['has_issue'])) {
$issues[] = [
'id' => (int)$attachment_id,
'type' => $check['issue_type'],
'message' => $check['message'],
'url' => wp_get_attachment_url((int)$attachment_id),
'path' => get_attached_file((int)$attachment_id),
'edit_link' => get_edit_post_link((int)$attachment_id, 'raw'),
'in_library' => true,
];
}
}
$next_cursor = $cursor + count($ids);
$done = ($next_cursor >= $total);
return [
'mode' => 'library',
'cursor' => $next_cursor,
'total' => $total,
'checked' => min($next_cursor, $total),
'done' => $done,
'issues' => $issues,
'batch_size' => $this->batch_size,
];
}
private function scan_uploads_step(int $cursor): array {
$upload = wp_upload_dir();
$base_dir = $upload['basedir'];
$base_url = $upload['baseurl'];
if (!is_dir($base_dir)) {
return [
'mode' => 'uploads',
'cursor' => 0,
'total' => null,
'checked' => 0,
'done' => true,
'issues' => [],
];
}
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($base_dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
$issues = [];
$collected = 0;
$position = 0;
foreach ($it as $file) {
if (!$file->isFile()) continue;
$ext = strtolower($file->getExtension());
if (!in_array($ext, $this->allowed_ext, true)) continue;
if ($position < $cursor) {
$position++;
continue;
}
if ($collected >= $this->batch_size) break;
$position++;
$collected++;
$path = $file->getPathname();
$check = $this->validate_image_file($path, null);
if (!empty($check['has_issue'])) {
$rel = ltrim(str_replace($base_dir, '', $path), '/\\');
$url = trailingslashit($base_url) . str_replace('\\', '/', $rel);
$attachment_id = $this->attachment_id_by_relative_path($rel);
$issues[] = [
'id' => $attachment_id ? (int)$attachment_id : 0,
'type' => $check['issue_type'],
'message' => $check['message'],
'url' => $url,
'path' => $path,
'edit_link' => $attachment_id ? get_edit_post_link((int)$attachment_id, 'raw') : '',
'in_library' => (bool)$attachment_id,
];
}
}
$done = ($collected < $this->batch_size);
return [
'mode' => 'uploads',
'cursor' => $cursor + $collected,
'total' => null,
'checked' => $cursor + $collected,
'done' => $done,
'issues' => $issues,
'batch_size' => $this->batch_size,
];
}
private function check_attachment(int $attachment_id): array {
$path = get_attached_file($attachment_id);
$expected = get_post_mime_type($attachment_id);
if (empty($path)) {
return $this->issue('Missing File Path', 'WordPress cannot determine the filesystem path for this attachment.');
}
return $this->validate_image_file($path, $expected ?: null);
}
private function validate_image_file(string $path, ?string $expected_mime): array {
if (!file_exists($path)) {
return $this->issue('File Not Found', 'The file does not exist at the expected location.');
}
if (!is_readable($path)) {
return $this->issue('Permission Denied', 'File exists but is not readable. Check permissions/ownership.');
}
$size = @filesize($path);
if ($size === 0) {
return $this->issue('Empty File', 'File size is 0 bytes (empty).');
}
if ($size !== false && $size > $this->max_file_size) {
return $this->issue('Oversized File', 'File exceeds scan limit: ' . size_format($this->max_file_size) . '.');
}
$actual = $this->detect_mime($path);
if (empty($actual)) {
return $this->issue('Unknown MIME', 'Could not detect MIME type.');
}
if (!in_array($actual, $this->allowed_mimes, true)) {
return $this->issue('Invalid MIME Type', 'Detected MIME: ' . $actual . ' (not a supported image format).');
}
if ($expected_mime && $actual !== $expected_mime) {
return $this->issue('MIME Mismatch', 'WordPress expects: ' . $expected_mime . ' | Detected: ' . $actual . '.');
}
$dim = @getimagesize($path);
if ($dim === false) {
if (extension_loaded('gd')) {
$gd = $this->check_with_gd($path);
if (!empty($gd['has_issue'])) return $gd;
}
if (extension_loaded('imagick')) {
$im = $this->check_with_imagick($path);
if (!empty($im['has_issue'])) return $im;
}
return $this->issue('Corrupted Image', 'getimagesize() failed. File may be corrupted or truncated.');
}
$sig = $this->signature_checks($path, $actual);
if (!empty($sig['has_issue'])) return $sig;
return [
'has_issue' => false,
'issue_type' => 'Valid',
'message' => 'OK',
'mime' => $actual,
'width' => (int)$dim[0],
'height' => (int)$dim[1],
];
}
private function detect_mime(string $path): ?string {
if (function_exists('finfo_open')) {
$fi = @finfo_open(FILEINFO_MIME_TYPE);
if ($fi) {
$mime = @finfo_file($fi, $path);
@finfo_close($fi);
return is_string($mime) ? $mime : null;
}
}
if (function_exists('mime_content_type')) {
$mime = @mime_content_type($path);
return is_string($mime) ? $mime : null;
}
return null;
}
private function signature_checks(string $path, string $mime): array {
$head = $this->read_bytes($path, 0, 16);
$tail = $this->read_tail($path, 16);
if ($mime === 'image/jpeg') {
if (substr($head, 0, 2) !== "\xFF\xD8") {
return $this->issue('Invalid JPEG Header', 'JPEG missing start marker (SOI).');
}
if (substr($tail, -2) !== "\xFF\xD9") {
return $this->issue('Truncated JPEG', 'JPEG seems truncated (missing end marker EOI).');
}
}
if ($mime === 'image/png') {
$png_sig = "\x89PNG\r\n\x1a\n";
if (substr($head, 0, 8) !== $png_sig) {
return $this->issue('Invalid PNG Signature', 'PNG missing standard signature.');
}
if (strpos($tail, 'IEND') === false) {
return $this->issue('Truncated PNG', 'PNG seems truncated (no IEND chunk found near the end).');
}
}
if ($mime === 'image/gif') {
$gif = strtoupper(substr($head, 0, 6));
if ($gif !== 'GIF87A' && $gif !== 'GIF89A') {
return $this->issue('Invalid GIF Header', 'GIF header is invalid (GIF87a / GIF89a expected).');
}
}
return ['has_issue' => false];
}
private function read_bytes(string $path, int $offset, int $length): string {
$h = @fopen($path, 'rb');
if (!$h) return '';
@fseek($h, $offset);
$data = (string)@fread($h, $length);
@fclose($h);
return $data;
}
private function read_tail(string $path, int $length): string {
$h = @fopen($path, 'rb');
if (!$h) return '';
$size = @filesize($path);
if (!$size || $size < 1) {
@fclose($h);
return '';
}
$offset = max(0, (int)$size - $length);
@fseek($h, $offset);
$data = (string)@fread($h, $length);
@fclose($h);
return $data;
}
private function check_with_gd(string $path): array {
$data = @file_get_contents($path);
if ($data === false) {
return $this->issue('Read Error', 'Could not read file contents (file_get_contents failed).');
}
$img = @imagecreatefromstring($data);
if ($img === false) {
return $this->issue('GD Processing Failed', 'GD could not decode this image. File is likely corrupted.');
}
imagedestroy($img);
return ['has_issue' => false];
}
private function check_with_imagick(string $path): array {
try {
$im = new Imagick();
$im->readImage($path);
if ($im->getNumberImages() < 1) {
return $this->issue('Imagick Empty', 'Imagick found no valid frames/images in the file.');
}
$w = (int)$im->getImageWidth();
$h = (int)$im->getImageHeight();
$im->clear();
$im->destroy();
if ($w < 1 || $h < 1) {
return $this->issue('Zero Dimensions', 'Imagick detected invalid dimensions.');
}
return ['has_issue' => false];
} catch (Throwable $e) {
return $this->issue('Imagick Error', 'Imagick threw an exception: ' . $e->getMessage());
}
}
private function attachment_id_by_relative_path(string $relative): int {
global $wpdb;
$relative = ltrim($relative, '/\\');
$id = $wpdb->get_var(
$wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_wp_attached_file' AND meta_value = %s LIMIT 1",
$relative
)
);
return $id ? (int)$id : 0;
}
private function issue(string $type, string $message): array {
return [
'has_issue' => true,
'issue_type' => $type,
'message' => $message,
];
}
}
HZ_Image_Defect_Scanner::instance();
Admin CSS
Create:
wp-content/plugins/hz-image-defect-scanner/assets/admin.css
.hz-ids-wrap .hz-ids-controls {
margin: 16px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.hz-ids-status {
margin-left: 8px;
font-weight: 600;
}
.hz-ids-progress {
margin: 18px 0;
}
.hz-ids-bar {
width: 100%;
height: 18px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.hz-ids-bar-fill {
height: 100%;
width: 0%;
background: #2271b1;
transition: width 0.25s ease;
}
.hz-ids-results table td code {
font-size: 12px;
}
Admin JavaScript (AJAX Batch Scan + CSV Export)
Create:
wp-content/plugins/hz-image-defect-scanner/assets/admin.js
jQuery(function ($) {
let running = false;
let mode = "library";
let cursor = 0;
let issues = [];
function setStatus(txt) {
$("#hz-ids-status").text(txt);
}
function renderHeader(data) {
let html = "";
html += "<h2>Results</h2>";
html += "<p><strong>Mode:</strong> " + (data.mode === "uploads" ? "Uploads Folder" : "Media Library") + "</p>";
html += "<p><strong>Checked:</strong> <span id='hz-ids-checked'>" + (data.checked ?? 0) + "</span>";
if (data.total !== null && data.total !== undefined) {
html += " / <span id='hz-ids-total'>" + data.total + "</span>";
}
html += "</p>";
html += "<p><strong>Issues found:</strong> <span id='hz-ids-issues-count'>" + issues.length + "</span></p>";
html += "<table class='wp-list-table widefat fixed striped'>";
html += "<thead><tr>";
html += "<th style='width:120px;'>Preview</th>";
html += "<th>Issue</th>";
html += "<th>Path</th>";
html += "<th style='width:140px;'>Actions</th>";
html += "</tr></thead><tbody id='hz-ids-tbody'></tbody></table>";
$("#hz-ids-results").html(html);
}
function addRows(newIssues) {
if (!newIssues || !newIssues.length) return;
let rows = "";
newIssues.forEach(function (it) {
const safeUrl = it.url ? String(it.url) : "";
const safePath = it.path ? String(it.path) : "";
const safeType = it.type ? String(it.type) : "";
const safeMsg = it.message ? String(it.message) : "";
const edit = it.edit_link ? String(it.edit_link) : "";
rows += "<tr>";
rows += "<td>";
if (safeUrl) {
rows += "<img src='" + safeUrl + "' style='max-width:90px; max-height:60px; object-fit:cover;' onerror=\"this.style.display='none'\" />";
} else {
rows += "-";
}
rows += "</td>";
rows += "<td><strong style='color:#d63638;'>" + safeType + "</strong><br>" + safeMsg + "</td>";
rows += "<td><code style='word-break:break-all;'>" + safePath + "</code></td>";
rows += "<td>";
if (edit) rows += "<a class='button button-small' href='" + edit + "'>Edit</a> ";
if (safeUrl) rows += "<a class='button button-small' href='" + safeUrl + "' target='_blank' rel='noopener'>Open</a>";
rows += "</td>";
rows += "</tr>";
});
$("#hz-ids-tbody").append(rows);
$("#hz-ids-issues-count").text(issues.length);
}
function updateProgress(data) {
let pct = 0;
if (data.total !== null && data.total !== undefined && data.total > 0) {
pct = Math.round((data.checked / data.total) * 100);
} else {
pct = data.done ? 100 : Math.min(95, Math.round((data.checked % 1000) / 10));
}
$("#hz-ids-bar-fill").css("width", pct + "%");
let text = "Checked: " + (data.checked ?? 0);
if (data.total !== null && data.total !== undefined) text += " / " + data.total;
text += " | Issues: " + issues.length;
$("#hz-ids-progress-text").text(text);
$("#hz-ids-checked").text(data.checked ?? 0);
if (data.total !== null && data.total !== undefined) $("#hz-ids-total").text(data.total);
}
function scanStep() {
if (!running) return;
$.post(HZ_IDS.ajaxurl, {
action: "hz_ids_scan_step",
nonce: HZ_IDS.nonce,
mode: mode,
cursor: cursor
})
.done(function (resp) {
if (!resp || !resp.success) {
running = false;
setStatus("Error");
$("#hz-ids-progress").hide();
return;
}
const data = resp.data;
if (cursor === 0) {
issues = [];
renderHeader(data);
}
cursor = data.cursor ?? 0;
if (data.issues && data.issues.length) {
issues = issues.concat(data.issues);
addRows(data.issues);
}
updateProgress(data);
if (data.done) {
running = false;
setStatus("Done");
$("#hz-ids-export").prop("disabled", issues.length === 0);
return;
}
setTimeout(scanStep, 120);
})
.fail(function () {
running = false;
setStatus("AJAX Error");
$("#hz-ids-progress").hide();
});
}
function start(newMode) {
if (running) return;
running = true;
mode = newMode;
cursor = 0;
issues = [];
setStatus("Scanning...");
$("#hz-ids-progress").show();
$("#hz-ids-bar-fill").css("width", "0%");
$("#hz-ids-progress-text").text("Initializing...");
$("#hz-ids-export").prop("disabled", true);
scanStep();
}
$("#hz-ids-start").on("click", function () {
start("library");
});
$("#hz-ids-start-uploads").on("click", function () {
start("uploads");
});
$("#hz-ids-export").on("click", function () {
if (!issues.length) return;
const form = $("<form method='POST' action='" + HZ_IDS.ajaxurl + "'></form>");
form.append("<input type='hidden' name='action' value='hz_ids_export_csv'>");
form.append("<input type='hidden' name='nonce' value='" + HZ_IDS.nonce + "'>");
form.append("<input type='hidden' name='issues' value='" + $("<div/>").text(JSON.stringify(issues)).html().replace(/"/g, """) + "'>");
$("body").append(form);
form.submit();
form.remove();
});
});
How To Install The Plugin
Install as a normal plugin
Upload the folder hz-image-defect-scanner to:
/wp-content/plugins/
Then go to:
WordPress Admin → Plugins → Installed Plugins → Activate “HZ Image Defect Scanner”
Open it here:
Tools → Image Defect Scanner
Install as an MU-plugin (optional)
This scanner has JS/CSS assets, so a normal plugin is the easiest path.
If you want the “engine” as MU-plugin, you can move only the PHP file into:
/wp-content/mu-plugins/
But you would lose the admin UI unless you also handle asset loading carefully.

How To Read The Report Like A Pro
“File Not Found” is usually a migration problem
Most of the time: database moved, uploads didn’t.
“Permission Denied” is a server ownership issue
Common on VPS after moving files with root user.
“Empty File” means a failed upload
Often caused by broken upload limits or interrupted connection.
“MIME Mismatch” is not always fatal
If you converted images to WebP/AVIF and WordPress metadata stayed old, you’ll see this. It’s still useful because it signals inconsistency.
Safe Performance Tips
Reduce batch size on cheap hosting
Change:
private int $batch_size = 120;
Try 80 if the server is slow.
Keep the max file size limit
Big files can slow scans heavily. 50MB is a solid protection.
Run scans when traffic is low
Scanning reads many files from disk. Low-traffic hours feel smoother.
Frequently Asked Questions
Does this plugin delete or modify images?
No. It only reads files and reports problems. It doesn’t delete or edit anything.
Can I scan a site with thousands of images?
Yes. That’s why it uses AJAX batch scanning instead of one huge request.
Why do I see MIME Mismatch?
Because WordPress stores an expected MIME in the database, while the server detects the actual file format differently (often after conversions or migrations).
Why doesn’t uploads scan show a total count?
Counting every file first would double the work and slow everything down. Uploads mode is optimized for speed.
Do I need Imagick?
No. Imagick is an optional fallback if getimagesize() fails. The scanner works without it.
What about thumbnails?
This version checks the main file. You can extend it to verify generated sizes from attachment metadata if you want deeper audits.
Can I export the results?
Yes. After the scan completes, click “Export CSV.”
Is this AdSense-friendly?
Yes. It’s a technical maintenance guide, no restricted content.
Will it work on multisite?
Yes, inside each site dashboard. Uploads scan will target the current site’s uploads directory as WordPress reports it.
Can I auto-check images during upload?
You can, but it’s not recommended by default. Automatic checks can slow uploads. Manual audits are safer.
Key Takeaways You’ll Actually Use
A scanner helps you find image problems quickly and confidently.
Batch AJAX scanning avoids timeouts and memory crashes.
Signature checks catch truncated JPEG/PNG/GIF files.
Uploads scan reveals orphan files not registered in Media Library.
CSV export makes cleanup work easier and trackable.
⚠️ Disclaimer and Source Hygiene
This tutorial is for educational purposes. Always take a full backup before changing files on your server. The methods shown rely on standard WordPress APIs and common PHP file validation techniques (MIME detection, getimagesize(), GD/Imagick fallbacks, and basic file signature checks). For complex migrations, consult a qualified WordPress or server professional.
🔔 For more tutorials like this, consider subscribing to our blog.
📩 Do you have questions or suggestions? Leave a comment or contact us!
🏷️ Tags: WordPress image scanner, broken images WordPress, Media Library cleanup, uploads folder audit, WordPress maintenance plugin, PHP WordPress plugin, fix missing images, corrupted images detection, WordPress admin tools, CSV export WordPress
📢 Hashtags: #WordPress #WordPressPlugin #PHP #WebDev #SiteMaintenance #MediaLibrary #SEO #Performance #VPS #HelpZone
📚 Sources and References
WordPress Developer Resources: WP_Query, admin-ajax.php, nonces, capabilities, admin_menu/admin_enqueue_scripts hooks.
PHP Documentation: finfo, getimagesize, filesystem functions, GD, Imagick.
🕊️ Secondary Sources and Testimonials
Common WordPress maintenance patterns from real-world site management: failed migrations, incomplete restores, permission mistakes after FTP/SCP, and image conversions that leave mismatched metadata.