EffortLess Simple Image Optimiser

Description

Effortless Simple Image Optimiser hooks directly into the WordPress Media Library upload pipeline and compresses, resizes and (optionally) converts images the moment they are uploaded — all using the built-in PHP GD extension.

Features

  • JPEG compression — re-encodes JPEGs at a configurable quality level (default: 82). GD strips EXIF metadata automatically, further reducing file size.
  • PNG compression — applies lossless GD compression to PNG files.
  • PNG JPEG conversion — converts opaque (non-transparent) PNGs to JPEG for significant space savings. Transparent PNGs are always kept as PNG.
  • Image resizing — scales down images that exceed a configurable maximum width or height while maintaining the original aspect ratio.
  • WebP generation — optionally saves a .webp sidecar file alongside every optimised image (requires GD compiled with WebP support).
  • Zero external dependencies — no API keys, no third-party services, no composer packages.
  • Graceful degradation — if GD is not available the plugin skips processing and logs a notice; uploads are never blocked.

Settings

Navigate to Settings Image Optimiser to configure:

  • Enable / disable automatic optimisation on upload
  • JPEG quality (1–100, default 82)
  • Enable PNG JPEG conversion
  • Enable WebP sidecar generation + WebP quality
  • Enable / disable resizing
  • Maximum width in pixels (default 1920)
  • Maximum height in pixels (default 1920)

Requirements

  • PHP 7.4 or higher
  • PHP GD extension (enabled on virtually all shared hosting)
  • WordPress 5.9 or higher

Installation

From the WordPress admin (recommended)

  1. Go to Plugins Add New.
  2. Search for Effortless Simple Image Optimiser.
  3. Click Install Now, then Activate.
  4. Configure the plugin at Settings Image Optimiser.

Manual installation

  1. Download the plugin zip file.
  2. Unzip and upload the effortless-simple-image-optimiser folder to /wp-content/plugins/.
  3. Activate the plugin from the Plugins screen.
  4. Go to Settings Image Optimiser to configure your preferences.

FAQ

Does the plugin require an API key or external account?

No. Everything is processed locally using the PHP GD library that ships with PHP.

Will my existing images be optimised?

Not automatically. The plugin only processes images at the time of upload. To re-optimise existing images you will need to re-upload them or use a separate bulk-processing tool.

Is the original image kept as a backup?

By default the plugin overwrites the original file with the optimised version. Make sure you keep your own backups if you need to recover originals.

What happens if GD is not installed?

The plugin fails gracefully — uploads proceed normally, a notice is logged to the PHP error log, and a warning is displayed on the settings page.

Why does PNG JPEG conversion only apply to some PNGs?

PNGs that contain any transparent pixels are never converted to JPEG because JPEG does not support transparency. The plugin samples the image to detect transparency before converting.

Does the plugin support WebP uploads?

Yes. Uploaded WebP images are re-compressed using the configured WebP quality setting.

Will this slow down my site or uploads?

Processing is synchronous and happens during the upload request. On typical shared hosting the overhead is negligible for standard web images (< 5 MB). Very large files may add a second or two.

Reviews

There are no reviews for this plugin.

Contributors & Developers

“EffortLess Simple Image Optimiser” is open source software. The following people have contributed to this plugin.

Contributors

Changelog

1.0.56

  • Fix: Prefix all global variables in uninstall.php to comply with WordPress plugin coding standards.

1.0.55

  • Revert: Removed pixel-scan cap on PNG transparency detection — every pixel is checked to ensure even a single transparent pixel (icons, logos) keeps the image as PNG.

1.0.54

  • Security: Nonce is now verified before accessing $_GET parameters on the settings page.
  • Security: .htaccess write in originals directory uses exclusive file locking and logs errors on failure.
  • Security: render_env_row() now escapes output with wp_kses_post() instead of relying on caller discipline.
  • Performance: All unbounded posts_per_page => -1 queries replaced with paginated batching via elsio_get_all_ids() to prevent memory exhaustion on large sites.
  • Performance: Dedup scan primes the post meta cache in batches, eliminating N+1 queries.
  • Fix: Stale backup detection — if backup meta exists but the file is missing on disk, a fresh backup is created instead of silently skipping.
  • Improvement: WordPress 5.9+ version enforced on activation with a clear error message.
  • Improvement: Direct database queries in URL replacer and dedup now invalidate WordPress object cache (clean_post_cache() / wp_cache_delete()) after updates.

1.0.52

  • Fix: preg_replace() null return now triggers a WP_Error instead of silently writing an empty restore path during format-changed restores (PNGJPEG or vice versa).
  • Fix: Restored file is validated after copy() — a 0-byte write now returns a WP_Error and the file is removed rather than silently replacing the live image.
  • Fix: Bulk optimisation aborts with an error response when the pre-optimisation backup cannot be created, preventing data loss.
  • Fix: Originals tab now auto-scans and shows backup stats as soon as the page loads, without requiring a manual Refresh click.

1.0.51

  • New: Original file backup and restore. Before each optimisation (upload and bulk), the plugin saves a copy of the source image to elsio-originals/ inside the uploads folder. A new Originals tab lets you restore all images to their pre-optimisation state or discard the backups to free disk space. Backups are protected from direct browser access on Apache and are deleted automatically when an attachment is removed.

1.0.50

  • Security: Path traversal via symlinks — upload directory guard now uses realpath() to resolve symlinks before comparing paths, preventing a symlink inside uploads from targeting files elsewhere on the filesystem. Applies to both bulk optimisation and thumbnail regeneration AJAX handlers.
  • Security: wp_upload_dir() error return is now checked before accessing basedir; if the uploads directory cannot be resolved the request is rejected cleanly.
  • Fixed: filesize() returns false on stat failure; all call sites now check for false before casting to int, preventing silent 0-byte size comparisons that could produce wrong PNGJPEG or WebP sidecar decisions.
  • Fixed: set_error_handler() in the bulk optimiser is now wrapped in try/finally, guaranteeing the handler is restored even if getimagesize() throws an exception.

1.0.49

  • Fixed: WebP sidecar was not generated for images that were already within size limits and did not need recompression. The early-return path in optimize() now generates the sidecar when missing and generate_webp is enabled.

1.0.48

  • Fixed: insert_rules() in ELSIO_Htaccess now aborts when the initial block-removal step fails, preventing duplicate marker blocks from being written to .htaccess.

1.0.47

  • Fixed: WebP rewrite rules were inserted after the WordPress block in .htaccess, causing Apache to short-circuit on its “file exists stop [L]” rule before ever evaluating the WebP conditions. Rules are now always placed before # BEGIN WordPress. Existing installations that have the block in the wrong position are automatically corrected on next settings save or plugin activation.

1.0.46

  • Fixed: .webp.webp double extension created when the source file was already a WebP (e.g. an uploaded .webp image or a WebP thumbnail). generate_webp_sidecar() now skips WebP sources entirely — the .htaccess rewrite rule only covers JPEG/PNG requests so a WebP sidecar for a WebP file would never be served anyway.

1.0.45

  • Fixed: Original image was left without a .webp sidecar after running Regenerate Thumbnails. compress_thumbnails_filter now also generates the main file’s WebP sidecar when missing. During normal uploads the existing sidecar is detected and skipped, so no double-processing occurs.

1.0.44

  • Fixed: Thumbnail .webp sidecars were orphaned on disk when an attachment was deleted. delete_webp_sidecar() now reads attachment metadata and removes the .webp sidecar for every thumbnail size alongside the main file’s sidecar.

1.0.43

  • Fixed: Dead result.style.display = 'none' assignment in ghost-scan handler was immediately overridden — removed the redundant line.
  • Fixed: Orphaned JSDoc block that no longer documented any function was removed from admin JS.
  • Fixed: File-level docblock in class-elsio-bulk-optimizer.php now correctly lists all six AJAX endpoints (was “three”).
  • Fixed: get_active_tab() docblock now includes 'regen' in its return value list.

1.0.42

  • Fixed: Thumbnail regeneration no longer fails with “metadata could not be saved” — wp_update_attachment_metadata() returns false both on DB error and when the stored value is unchanged, so false was treated as success rather than a hard error.

1.0.41

  • Improved: wp_enqueue_script now uses the WP 6.3+ array form with strategy: defer, allowing the browser to download the admin script in parallel and execute it after DOM parsing — no change to behaviour, better page load performance.
  • Improved: register_setting now declares type: object for schema completeness as recommended since WP 5.5.

1.0.40

  • Added: uninstall.php — removes all plugin options and post-meta (elsio_options, _elsio_optimised, _elsio_thumbs_regen, _elsio_file_hash) when the plugin is deleted.

1.0.39

  • Fixed: require_once for admin image helpers now loaded after permission check in both bulk-optimizer and thumb-regen AJAX handlers.
  • Fixed: wp_update_attachment_metadata() return value checked in thumbnail regeneration — returns an error instead of silently marking success if the DB write fails.
  • Fixed: All admin buttons now have explicit type="button" to prevent accidental form submission.
  • Fixed: Added documentation comment in url-replacer noting the allowed_classes => false limitation for PHP-serialized object properties.

1.0.38

  • Fixed: Critical JavaScript syntax error introduced in 1.0.37 — macOS sed wrote literal \n\t characters instead of real newlines when splitting the timer variable declarations, breaking the entire admin script (regen, dedup scan, bulk optimise, and ghost cleanup all non-functional).

1.0.37

  • Fixed: Regenerate Thumbnails tab was completely broken — regenLog() was called but only regenLogMsg() was defined, causing a JavaScript runtime error.
  • Fixed: unserialize() in ELSIO_Url_Replacer now passes ['allowed_classes' => false] to prevent PHP object injection from malicious serialised postmeta values.
  • Fixed: Replaced the single shared pendingTimer with four separate timers (bulkTimer, ghostTimer, dedupTimer, regenTimer) so concurrent or back-to-back operations can never cancel each other’s inter-image delay.

1.0.36

  • Added: Regenerate Thumbnails tab — bulk-regenerates all thumbnail sizes for existing Media Library images, one at a time with progress bar, stop button, and reset flags. Useful after changing image sizes in your theme.

1.0.35

  • Fixed: PNGJPEG URL replacement now covers post_excerpt and all postmeta values (including PHP-serialised data stored by Elementor, ACF, WPBakery, and other page builders), not just post_content. The new ELSIO_Url_Replacer helper is also used by the duplicate-finder cleanup.

1.0.34

  • Fixed: Bulk optimiser no longer calls wp_generate_attachment_metadata() unconditionally on every image. It now only regenerates metadata (and thumbnails) when the format changed (PNGJPEG) or the image was resized — i.e. when stored metadata is genuinely stale. For pure compression passes, existing thumbnails are compressed in-place and metadata is left untouched, preventing valid attachment records from being overwritten with potentially incomplete data.
  • Fixed: Thumbnails were previously double-processed during bulk runs — once by compress_thumbnails_filter (triggered inside wp_generate_attachment_metadata) and again by the manual loop that always followed. The two code paths are now mutually exclusive.

1.0.33

  • Fixed: The “Optimised” and “Pending” stat counters on the Bulk Optimise tab now update automatically after a run completes or flags are reset. A dedicated elsio_bulk_get_stats AJAX endpoint was added to return all three counters (total / optimised / pending) in one call.

1.0.32

  • Fixed: png_has_transparency() now returns true (assume transparency, skip conversion) when GD cannot load the PNG, instead of false — prevents producing a broken JPEG from an unreadable image.
  • Fixed: resize_image() now returns early when the calculated output dimensions round to zero, avoiding a imagecreatetruecolor(0, 0) failure on very small source images.
  • Fixed: PNGJPEG URL replacement in the bulk optimiser now fetches matching rows and replaces in PHP (str_replace) instead of using SQL REPLACE(), so only exact URL occurrences are updated and surrounding content cannot be corrupted.
  • Fixed: Duplicate file scan now caches the MD5 hash in post meta (keyed on file mtime) so repeated scans skip unchanged files — large libraries scan significantly faster on subsequent runs.
  • Fixed: ajax_process_one() now validates that the attachment file path falls within the uploads directory before processing, guarding against third-party filters that could redirect to an unexpected location.
  • Improved: PNG transparency Stage 3 is now split into a fast 20×20 grid sample (3a) followed by a full pixel scan (3b). Transparent images with scattered pixels exit after the grid; opaque images are still guaranteed correct via the full scan.

1.0.31

  • Fixed: Race condition in WebP sidecar size comparison — filesize() calls are now guarded with file_exists() to prevent PHP warnings if a concurrent process removes the file between stat-cache clear and size read.
  • Fixed: generate_webp_sidecar() now guards imagewebp() with an internal function_exists() check, making the method safe to call directly without relying on the caller’s guard.
  • Fixed: resize_image() now returns false immediately when either source dimension is zero, preventing a division-by-zero on corrupted image files.
  • Fixed: png_has_transparency() now validates the 8-byte PNG signature before reading the color-type byte, so a truncated or mis-identified file cannot produce a false result.
  • Fixed: Duplicate-finder regex patterns now use preg_quote() on the ID string for future-proofing, even though absint() already guarantees digits only.
  • Improved: Replaced magic literals (4, 6, 0x7F, 9, 26) in ELSIO_Image_Optimizer with named class constants (PNG_COLOR_TYPE_GREYSCALE_ALPHA, PNG_COLOR_TYPE_RGBA, GD_ALPHA_BITMASK, GD_PNG_MAX_COMPRESSION, PNG_HEADER_READ_BYTES) for clarity.
  • Improved: Informational error_log messages (PNGJPEG discarded, WebP sidecar discarded) now carry an [INFO] tag to distinguish them from actual errors in the log.

1.0.30

  • Fixed: png_has_transparency() Stage 3 previously sampled a 10×10 pixel grid, which could miss a small transparent area. It now scans every pixel so that even a single transparent pixel prevents PNGJPEG conversion.

1.0.29

  • Fixed: PNGJPEG conversion broke image links in posts. Two root causes:
    1. handle_upload_filter() returned the original $upload array unchanged after conversion — WordPress created the attachment record with image/png MIME type and the old .png URL even though the file on disk was already a .jpg. The filter now updates $upload['file'], $upload['url'] and $upload['type'] when conversion happened, so the attachment is registered correctly from the start.
    2. Bulk optimizer never updated existing <img> references in post content after conversion. It now captures the old PNG URL before changing the stored file path, calls wp_update_post() to set post_mime_type to image/jpeg, then runs a REPLACE() SQL query to rewrite all occurrences of the old URL to the new JPEG URL across all post content.

1.0.28

  • Fixed: Palette PNGs (color type 3, e.g. icons) had their transparency silently missed by png_has_transparency()imagecolorat() returns a palette index on non-truecolor images whose high bits are not alpha. The function now calls imagepalettetotruecolor() before sampling so alpha bits are meaningful.
  • Fixed: Transparent corners (typical for circular/icon PNGs) could theoretically be missed by the 10×10 grid if all sampled points fell inside an opaque area. The function now explicitly checks all four corners as a dedicated stage before the grid scan, guaranteeing corner transparency is always detected.

1.0.27

  • Fixed: PHP Warning: imagewebp(): Palette image not supported by webp — palette-based PNGs (color type 3, indexed color) loaded by GD are not truecolor and cannot be passed to imagewebp() or certain imagepng() operations. Added an imageistruecolor() check in load_image() immediately after imagecreatefrompng(): if the result is a palette image, imagepalettetotruecolor() is called before returning the resource. This also prevents three duplicate warnings (original + thumbnails).

1.0.26

  • Fixed: Pending count in the stats row was not refreshed after ghost attachment cleanup completed. Added refreshPendingCount() call at the end of the ghost cleanNext() loop.

1.0.25

  • Fixed: “Fix all duplicates” had no effect — duplicate attachments were never actually deleted. new URLSearchParams({ 'dup_ids[]': [1,2,3] }) serialised the array as a single comma-joined string (dup_ids[]=1%2C2%2C3), so PHP received a string rather than an array, is_array() returned false, and the ID list was discarded. Replaced the Object.assign + URLSearchParams(object) pattern in post() and postRaw() with a buildParams() helper that iterates array values and appends them individually, producing correct dup_ids[]=1&dup_ids[]=2 serialisation.

1.0.24

  • Fixed: PHPCS warning — replaced fopen/fread/fclose in png_has_transparency() with file_get_contents(..., 0, 26) + phpcs:ignore annotation; reads only the 26-byte PNG header without loading the whole file via WP_Filesystem.

1.0.23

  • Fixed: XSS risk in dedup summary — buildDedupSummaryHTML() injected backend values directly into .innerHTML. Replaced with DOM-based buildDedupSummary() using textContent throughout; no user-controlled data can reach the parser.
  • Fixed: dedupProcessNext() and dedupFinish() accessed dedupBar, dedupProgressWrap, dedupLabel, btnDedupFix, btnDedupScan without null checks — any missing DOM element crashed the entire fix flow. Added guards throughout.
  • Fixed: onGhostsCleanClick() called JSON.parse(btnClean.dataset.ids) without try/catch — malformed data would throw and break the cleanup handler. Wrapped in try/catch with an error log message.
  • Fixed: PNGJPEG path detection in bulk optimizer checked file_exists($jpg_path) alone — a pre-existing .jpg with the same stem would be mistakenly used. Now also verifies the original PNG is gone (!file_exists($file_path)) before treating the .jpg as the converted output.
  • Fixed: formatBytes() did not handle negative or non-numeric input — normalised with Math.max(0, parseInt(...) || 0).
  • Fixed: sprintf() returned undefined for out-of-bounds positional args — added bounds check, returns empty string instead.
  • Fixed: Pending setTimeout timers were not cancelled on page unload — AJAX requests could fire after navigation. Added beforeunload listener that clears pendingTimer.
  • Fixed: is_apache() returned false for Apache servers where SERVER_SOFTWARE is masked by a proxy. Added function_exists('apache_get_modules') as a fallback.
  • Clarified: PNG compression intentionally reuses the JPEG quality setting for GD compression level — added inline comment to make this explicit.

1.0.22

  • Fixed: PNGJPEG conversion block destroyed the GD image resource before falling through to step 3 — any PNG that triggered the path (JPEG not smaller, or save failed) would attempt to use a freed resource. Restructured to early-return only when JPEG is actually kept; $image is now destroyed exactly once.
  • Fixed: png_has_transparency() only sampled a 10×10 pixel grid, so PNGs with an alpha channel (color type 4 or 6) that happened to have all sampled pixels fully opaque were incorrectly converted to JPEG. Added a pre-check of the PNG color-type header byte (offset 25) — types 4 and 6 now return true immediately without loading the image.

1.0.21

  • Fixed: Gutenberg block "id":5 LIKE pattern also matched "id":15 — replaced SQL REPLACE with PHP-side preg_replace using a negative-lookahead (?!\d) boundary, preventing IDs that start with the same digits from being corrupted.
  • Fixed: Dedup progress bar had class elsio-progress-bar instead of elsio-progress-fill — the animated fill bar was invisible.
  • Fixed: ajax_process_group() had no upper bound on dup_ids[] — added a hard limit of 1000 IDs per request to prevent DoS via oversized payloads.
  • Fixed: PNGJPEG conversion always deleted the source PNG even when the resulting JPEG was larger — now compares file sizes and discards the JPEG if it is not smaller, keeping the original PNG.
  • Fixed: WebP sidecar deletion (when sidecar was not smaller than source) was silent — now logs a notice under WP_DEBUG_LOG.

1.0.20

  • New feature: “Clean Up Ghost Attachments” section in the Bulk Optimise tab. Scans for Media Library records whose files no longer exist on disk, then deletes the orphaned DB records one by one with a confirmation step. Files are already missing — only the database records are removed.

1.0.19

  • Fixed: Ghost attachments (DB record exists but file missing on disk) were counted as errors during bulk optimisation. Now returned as a success with skipped:true, logged in orange as a warning, and excluded from the error count.

1.0.18

  • Fixed: “Fix all duplicates” button had no click handler — initDedup() ran on page load when the Duplicates tab was not active, so all getElementById() calls returned null. Replaced with document-level event delegation so clicks are captured regardless of which tab rendered the page.

1.0.17

  • Fixed: Duplicate finder stat boxes showed undefined labels — i18n keys groups, redundantFiles, recoverable were missing from the PHP localization array.
  • Fixed: “Scanning Media Library…” displayed the HTML entity literally — replaced with plain ASCII ellipsis since textContent does not parse HTML entities.

1.0.16

  • New feature: “Find Duplicates” tab under Settings Image Optimiser.
  • Scans the entire Media Library for exact duplicate images using MD5 file hashing.
  • Keeps the oldest copy (lowest attachment ID) and updates all references: image URLs in post content, Gutenberg block id attributes, wp-image-X CSS classes, and _thumbnail_id featured image meta.
  • Duplicate attachments are then permanently deleted via wp_delete_attachment().
  • Scan uses a 120-second timeout to handle large libraries gracefully.

1.0.15

  • Fixed: WordPress-generated thumbnail sizes (150×150, 300×225, 768×576, 1024×768, etc.) were never compressed — only the original file was processed. Added wp_generate_attachment_metadata filter to compress all thumbnails on upload, and added thumbnail compression loop to the bulk optimiser. Resize and PNGJPEG conversion are intentionally disabled for thumbnails to preserve WordPress filename expectations.

1.0.14

  • Fixed: PHP 8.1 deprecation notice “Implicit conversion from float to int loses precision” on imagecolorat() in png_has_transparency(). Operator precedence caused $step to remain a float ((int) was only applied to min(), not to the division result). Fixed by wrapping the full expression: (int) round( min( $w, $h ) / 10 ).

1.0.13

  • Fixed: Bulk optimiser did not update WordPress attachment metadata after resizing. The Media Library was still showing original dimensions from the database even though the file on disk had been resized. Now calls wp_generate_attachment_metadata() and wp_update_attachment_metadata() after each optimisation.
  • Fixed: PNGJPEG conversion during bulk optimise now correctly updates the stored file path via update_attached_file().

1.0.12

  • Fixed: PNG images within the configured max dimensions were silently skipped and never compressed. needs_recompression() now includes image/png so PNG files are always re-encoded regardless of their dimensions.

1.0.11

  • Fixed: HTML entity &#8212; displayed as literal text in the bulk log (e.g. saved &#8212; 74%). The log() function uses textContent which does not parse HTML entities — replaced the entity with the actual UTF-8 em dash character in the PHP string.

1.0.10

  • Fixed: Double percent sign (%%) displayed in bulk optimisation log and progress counter. The PHP %% escape (for PHP sprintf) was incorrectly used in strings passed to the JavaScript sprintf — replaced with a single literal %.

1.0.9

  • Fixed: Added @package tag to main plugin file doc block to satisfy PHPCS file comment requirements.
  • Fixed: Added phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated on all imagedestroy() calls — function remains callable in PHP 8 and is required for PHP 7.4 compatibility.
  • Fixed: Added phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler on set_error_handler() calls used to suppress GD warnings without the @ operator.
  • Fixed: Corrected doc comment long descriptions in class-elsio-htaccess.php that started with lowercase (PHPCS capitalization rule).
  • Fixed: Added loop incrementer phpcs:ignore annotation on the pixel-sampling loop in png_has_transparency().

1.0.8

  • Fixed: WordPress.Security.EscapeOutput.OutputNotEscaped on $notice_text — store raw translated string with __() and escape at the point of output with esc_html().

1.0.7

  • Fixed: WordPress.WP.I18n.MissingTranslatorsComment — extracted all __() and _n() calls that contain placeholders into standalone variables so the /* translators: */ comment sits on the line immediately above the i18n function call, satisfying WPCS token-level validation.

1.0.6

  • i18n: Fixed \uXXXX JS escape sequences used inside PHP strings — replaced with HTML entities (&#8230;, &#8212;, &#8220;, &#8221;).
  • i18n: Switched multi-placeholder strings to positional format (%1$s, %2$s…) so translators can reorder arguments.
  • i18n: Added /* translators: */ comments to all strings containing placeholders.
  • i18n: Updated JS sprintf() to support both sequential (%s) and positional (%1$s) token formats.
  • i18n: Created languages/effortless-simple-image-optimiser.pot with all 60+ translatable strings.

1.0.5

  • Fixed: PNG compression level formula was inverted — quality 82 now correctly maps to GD level 7 instead of level 2.
  • Fixed: imagewebp() return value now checked; failures are logged under WP_DEBUG_LOG.
  • Fixed: WebP sidecar is now deleted if it is not smaller than the source file, preventing .htaccess from serving a larger WebP.
  • Fixed: Bulk optimizer now resolves the correct output path when PNG is converted to JPEG, preventing false 100% saving reports.
  • Fixed: Added delete_attachment hook — .webp sidecar files are now removed when the parent image is deleted from the Media Library.
  • Fixed: AJAX fetch() requests in JS now carry a 60-second AbortController timeout to prevent the bulk queue hanging indefinitely.

1.0.4

  • PHP/WordPress/PHPCS/WPCS compliance pass across all files.
  • Replaced @ error suppression with set_error_handler/restore_error_handler.
  • error_log() calls are now guarded by WP_DEBUG_LOG.
  • unlink() replaced with wp_delete_file().
  • file_get_contents() replaced with WP_Filesystem in htaccess class.
  • is_writable() replaced with wp_is_writable().
  • insert_with_markers() and get_home_path() dependencies loaded explicitly.
  • $_SERVER['SERVER_SOFTWARE'] sanitised with sanitize_text_field() + wp_unslash().
  • All $_POST inputs sanitised with absint() + wp_unslash().
  • $_GET tab/action values sanitised with sanitize_key() + wp_unslash().
  • Inline oninput JS removed from range fields; replaced with data-linked attributes handled in JS.
  • printf with raw HTML replaced with proper wp_kses() escaping via render_env_row() helper.
  • echo $notice replaced with pre-escaped output split into typed variables.
  • $wpdb->delete() direct query replaced with delete_metadata( 'post', 0, $key, '', true ).
  • new WP_Query()->found_posts replaced with get_posts() + count() and no_found_rows => true.
  • Added return statements after wp_send_json_error() calls to make flow explicit.
  • Added label_for to all settings fields for accessible form labels.
  • IIFE closure in JS uses }() form; finally chain preserved for modern browsers.

1.0.3

  • Added Apache .htaccess WebP delivery via mod_rewrite (Option 1).
  • Rules inserted automatically on activation, removed on deactivation, re-synced on settings save.
  • Vary: Accept header appended so CDNs cache JPEG and WebP variants separately.
  • Environment sidebar now shows Apache detection, .htaccess writability, and rewrite rule status.
  • “Insert now” button to manually inject rules if they are missing post-activation.

1.0.2

  • Added Bulk Optimise tab under Settings Image Optimiser.
  • New AJAX endpoints: elsio_bulk_get_ids, elsio_bulk_process_one, elsio_bulk_reset_flags.
  • Progress bar, live log and stats panel for bulk runs.
  • Post-meta flag _elsio_optimised prevents reprocessing already-optimised images.
  • “Re-optimise all” checkbox and “Reset flags” button for manual re-runs.

1.0.1

  • Added readme.txt with full documentation.
  • Minor version bump following project conventions.

1.0.0

  • Initial release.
  • JPEG compression with configurable quality.
  • PNG compression and optional PNG JPEG conversion (transparency-aware).
  • Image resizing with aspect-ratio preservation.
  • Optional WebP sidecar generation.
  • Admin settings page under Settings Image Optimiser.
  • GD availability check with graceful fallback.