{"id":290457,"date":"2026-03-28T06:12:29","date_gmt":"2026-03-28T06:12:29","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/effortless-simple-image-optimiser\/"},"modified":"2026-03-28T06:13:30","modified_gmt":"2026-03-28T06:13:30","slug":"effortless-simple-image-optimiser","status":"publish","type":"plugin","link":"https:\/\/ko.wordpress.org\/plugins\/effortless-simple-image-optimiser\/","author":23148025,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_crdt_document":"","version":"1.0.56","stable_tag":"1.0.56","tested":"6.9.4","requires":"5.9","requires_php":"7.4","requires_plugins":null,"header_name":"EffortLess Simple Image Optimiser","header_author":"domclic","header_description":"Automatically optimises uploaded images using the PHP GD library. Compress JPEG\/PNG, resize oversized images, strip EXIF metadata and optionally generate WebP copies \u2014 all without external APIs.","assets_banners_color":"","last_updated":"2026-03-28 06:13:30","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"","rating":0,"author_block_rating":0,"active_installs":0,"downloads":53,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.56":{"tag":"1.0.56","author":"domclic","date":"2026-03-28 06:13:30"}},"upgrade_notice":{"1.0.1":"<p>Documentation update \u2014 no functional changes.<\/p>"},"ratings":[],"assets_icons":[],"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.0.56"],"block_files":[],"assets_screenshots":[],"screenshots":{"1":"The settings page under Settings \u2192 Image Optimiser showing all available options.","2":"The environment sidebar confirming GD availability and supported formats."},"jetpack_post_was_ever_published":false},"plugin_section":[],"plugin_tags":[56174,258832,125378,2700,15376],"plugin_category":[],"plugin_contributors":[241557],"plugin_business_model":[],"class_list":["post-290457","plugin","type-plugin","status-publish","hentry","plugin_tags-compress-images","plugin_tags-gd-library","plugin_tags-image-optimisation","plugin_tags-resize-images","plugin_tags-webp","plugin_contributors-domclic","plugin_committers-domclic"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/s.w.org\/plugins\/geopattern-icon\/effortless-simple-image-optimiser.svg","icon_2x":false,"generated":true},"screenshots":[],"raw_content":"<!--section=description-->\n<p><strong>Effortless Simple Image Optimiser<\/strong> hooks directly into the WordPress Media Library upload pipeline and compresses, resizes and (optionally) converts images the moment they are uploaded \u2014 all using the built-in PHP GD extension.<\/p>\n\n<h4>Features<\/h4>\n\n<ul>\n<li><strong>JPEG compression<\/strong> \u2014 re-encodes JPEGs at a configurable quality level (default: 82). GD strips EXIF metadata automatically, further reducing file size.<\/li>\n<li><strong>PNG compression<\/strong> \u2014 applies lossless GD compression to PNG files.<\/li>\n<li><strong>PNG \u2192 JPEG conversion<\/strong> \u2014 converts opaque (non-transparent) PNGs to JPEG for significant space savings. Transparent PNGs are always kept as PNG.<\/li>\n<li><strong>Image resizing<\/strong> \u2014 scales down images that exceed a configurable maximum width or height while maintaining the original aspect ratio.<\/li>\n<li><strong>WebP generation<\/strong> \u2014 optionally saves a <code>.webp<\/code> sidecar file alongside every optimised image (requires GD compiled with WebP support).<\/li>\n<li><strong>Zero external dependencies<\/strong> \u2014 no API keys, no third-party services, no composer packages.<\/li>\n<li><strong>Graceful degradation<\/strong> \u2014 if GD is not available the plugin skips processing and logs a notice; uploads are never blocked.<\/li>\n<\/ul>\n\n<h4>Settings<\/h4>\n\n<p>Navigate to <strong>Settings \u2192 Image Optimiser<\/strong> to configure:<\/p>\n\n<ul>\n<li>Enable \/ disable automatic optimisation on upload<\/li>\n<li>JPEG quality (1\u2013100, default 82)<\/li>\n<li>Enable PNG \u2192 JPEG conversion<\/li>\n<li>Enable WebP sidecar generation + WebP quality<\/li>\n<li>Enable \/ disable resizing<\/li>\n<li>Maximum width in pixels (default 1920)<\/li>\n<li>Maximum height in pixels (default 1920)<\/li>\n<\/ul>\n\n<h4>Requirements<\/h4>\n\n<ul>\n<li>PHP 7.4 or higher<\/li>\n<li>PHP GD extension (enabled on virtually all shared hosting)<\/li>\n<li>WordPress 5.9 or higher<\/li>\n<\/ul>\n\n<!--section=installation-->\n<p><strong>From the WordPress admin (recommended)<\/strong><\/p>\n\n<ol>\n<li>Go to <strong>Plugins \u2192 Add New<\/strong>.<\/li>\n<li>Search for <em>Effortless Simple Image Optimiser<\/em>.<\/li>\n<li>Click <strong>Install Now<\/strong>, then <strong>Activate<\/strong>.<\/li>\n<li>Configure the plugin at <strong>Settings \u2192 Image Optimiser<\/strong>.<\/li>\n<\/ol>\n\n<p><strong>Manual installation<\/strong><\/p>\n\n<ol>\n<li>Download the plugin zip file.<\/li>\n<li>Unzip and upload the <code>effortless-simple-image-optimiser<\/code> folder to <code>\/wp-content\/plugins\/<\/code>.<\/li>\n<li>Activate the plugin from the <strong>Plugins<\/strong> screen.<\/li>\n<li>Go to <strong>Settings \u2192 Image Optimiser<\/strong> to configure your preferences.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20the%20plugin%20require%20an%20api%20key%20or%20external%20account%3F\"><h3>Does the plugin require an API key or external account?<\/h3><\/dt>\n<dd><p>No. Everything is processed locally using the PHP GD library that ships with PHP.<\/p><\/dd>\n<dt id=\"will%20my%20existing%20images%20be%20optimised%3F\"><h3>Will my existing images be optimised?<\/h3><\/dt>\n<dd><p>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.<\/p><\/dd>\n<dt id=\"is%20the%20original%20image%20kept%20as%20a%20backup%3F\"><h3>Is the original image kept as a backup?<\/h3><\/dt>\n<dd><p>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.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20gd%20is%20not%20installed%3F\"><h3>What happens if GD is not installed?<\/h3><\/dt>\n<dd><p>The plugin fails gracefully \u2014 uploads proceed normally, a notice is logged to the PHP error log, and a warning is displayed on the settings page.<\/p><\/dd>\n<dt id=\"why%20does%20png%20%E2%86%92%20jpeg%20conversion%20only%20apply%20to%20some%20pngs%3F\"><h3>Why does PNG \u2192 JPEG conversion only apply to some PNGs?<\/h3><\/dt>\n<dd><p>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.<\/p><\/dd>\n<dt id=\"does%20the%20plugin%20support%20webp%20uploads%3F\"><h3>Does the plugin support WebP uploads?<\/h3><\/dt>\n<dd><p>Yes. Uploaded WebP images are re-compressed using the configured WebP quality setting.<\/p><\/dd>\n<dt id=\"will%20this%20slow%20down%20my%20site%20or%20uploads%3F\"><h3>Will this slow down my site or uploads?<\/h3><\/dt>\n<dd><p>Processing is synchronous and happens during the upload request. On typical shared hosting the overhead is negligible for standard web images (&lt; 5 MB). Very large files may add a second or two.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.0.56<\/h4>\n\n<ul>\n<li>Fix: Prefix all global variables in uninstall.php to comply with WordPress plugin coding standards.<\/li>\n<\/ul>\n\n<h4>1.0.55<\/h4>\n\n<ul>\n<li>Revert: Removed pixel-scan cap on PNG transparency detection \u2014 every pixel is checked to ensure even a single transparent pixel (icons, logos) keeps the image as PNG.<\/li>\n<\/ul>\n\n<h4>1.0.54<\/h4>\n\n<ul>\n<li>Security: Nonce is now verified before accessing <code>$_GET<\/code> parameters on the settings page.<\/li>\n<li>Security: <code>.htaccess<\/code> write in originals directory uses exclusive file locking and logs errors on failure.<\/li>\n<li>Security: <code>render_env_row()<\/code> now escapes output with <code>wp_kses_post()<\/code> instead of relying on caller discipline.<\/li>\n<li>Performance: All unbounded <code>posts_per_page =&gt; -1<\/code> queries replaced with paginated batching via <code>elsio_get_all_ids()<\/code> to prevent memory exhaustion on large sites.<\/li>\n<li>Performance: Dedup scan primes the post meta cache in batches, eliminating N+1 queries.<\/li>\n<li>Fix: Stale backup detection \u2014 if backup meta exists but the file is missing on disk, a fresh backup is created instead of silently skipping.<\/li>\n<li>Improvement: WordPress 5.9+ version enforced on activation with a clear error message.<\/li>\n<li>Improvement: Direct database queries in URL replacer and dedup now invalidate WordPress object cache (<code>clean_post_cache()<\/code> \/ <code>wp_cache_delete()<\/code>) after updates.<\/li>\n<\/ul>\n\n<h4>1.0.52<\/h4>\n\n<ul>\n<li>Fix: <code>preg_replace()<\/code> null return now triggers a WP_Error instead of silently writing an empty restore path during format-changed restores (PNG\u2192JPEG or vice versa).<\/li>\n<li>Fix: Restored file is validated after <code>copy()<\/code> \u2014 a 0-byte write now returns a WP_Error and the file is removed rather than silently replacing the live image.<\/li>\n<li>Fix: Bulk optimisation aborts with an error response when the pre-optimisation backup cannot be created, preventing data loss.<\/li>\n<li>Fix: Originals tab now auto-scans and shows backup stats as soon as the page loads, without requiring a manual Refresh click.<\/li>\n<\/ul>\n\n<h4>1.0.51<\/h4>\n\n<ul>\n<li>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.<\/li>\n<\/ul>\n\n<h4>1.0.50<\/h4>\n\n<ul>\n<li>Security: Path traversal via symlinks \u2014 upload directory guard now uses <code>realpath()<\/code> 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.<\/li>\n<li>Security: <code>wp_upload_dir()<\/code> error return is now checked before accessing <code>basedir<\/code>; if the uploads directory cannot be resolved the request is rejected cleanly.<\/li>\n<li>Fixed: <code>filesize()<\/code> returns <code>false<\/code> on stat failure; all call sites now check for <code>false<\/code> before casting to <code>int<\/code>, preventing silent 0-byte size comparisons that could produce wrong PNG\u2192JPEG or WebP sidecar decisions.<\/li>\n<li>Fixed: <code>set_error_handler()<\/code> in the bulk optimiser is now wrapped in try\/finally, guaranteeing the handler is restored even if <code>getimagesize()<\/code> throws an exception.<\/li>\n<\/ul>\n\n<h4>1.0.49<\/h4>\n\n<ul>\n<li>Fixed: WebP sidecar was not generated for images that were already within size limits and did not need recompression. The early-return path in <code>optimize()<\/code> now generates the sidecar when missing and <code>generate_webp<\/code> is enabled.<\/li>\n<\/ul>\n\n<h4>1.0.48<\/h4>\n\n<ul>\n<li>Fixed: <code>insert_rules()<\/code> in <code>ELSIO_Htaccess<\/code> now aborts when the initial block-removal step fails, preventing duplicate marker blocks from being written to <code>.htaccess<\/code>.<\/li>\n<\/ul>\n\n<h4>1.0.47<\/h4>\n\n<ul>\n<li>Fixed: WebP rewrite rules were inserted after the WordPress block in .htaccess, causing Apache to short-circuit on its \"file exists \u2192 stop [L]\" rule before ever evaluating the WebP conditions. Rules are now always placed before <code># BEGIN WordPress<\/code>. Existing installations that have the block in the wrong position are automatically corrected on next settings save or plugin activation.<\/li>\n<\/ul>\n\n<h4>1.0.46<\/h4>\n\n<ul>\n<li>Fixed: <code>.webp.webp<\/code> double extension created when the source file was already a WebP (e.g. an uploaded <code>.webp<\/code> image or a WebP thumbnail). <code>generate_webp_sidecar()<\/code> now skips WebP sources entirely \u2014 the <code>.htaccess<\/code> rewrite rule only covers JPEG\/PNG requests so a WebP sidecar for a WebP file would never be served anyway.<\/li>\n<\/ul>\n\n<h4>1.0.45<\/h4>\n\n<ul>\n<li>Fixed: Original image was left without a .webp sidecar after running Regenerate Thumbnails. <code>compress_thumbnails_filter<\/code> 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.<\/li>\n<\/ul>\n\n<h4>1.0.44<\/h4>\n\n<ul>\n<li>Fixed: Thumbnail .webp sidecars were orphaned on disk when an attachment was deleted. <code>delete_webp_sidecar()<\/code> now reads attachment metadata and removes the .webp sidecar for every thumbnail size alongside the main file's sidecar.<\/li>\n<\/ul>\n\n<h4>1.0.43<\/h4>\n\n<ul>\n<li>Fixed: Dead <code>result.style.display = 'none'<\/code> assignment in ghost-scan handler was immediately overridden \u2014 removed the redundant line.<\/li>\n<li>Fixed: Orphaned JSDoc block that no longer documented any function was removed from admin JS.<\/li>\n<li>Fixed: File-level docblock in <code>class-elsio-bulk-optimizer.php<\/code> now correctly lists all six AJAX endpoints (was \"three\").<\/li>\n<li>Fixed: <code>get_active_tab()<\/code> docblock now includes <code>'regen'<\/code> in its return value list.<\/li>\n<\/ul>\n\n<h4>1.0.42<\/h4>\n\n<ul>\n<li>Fixed: Thumbnail regeneration no longer fails with \"metadata could not be saved\" \u2014 <code>wp_update_attachment_metadata()<\/code> returns false both on DB error and when the stored value is unchanged, so false was treated as success rather than a hard error.<\/li>\n<\/ul>\n\n<h4>1.0.41<\/h4>\n\n<ul>\n<li>Improved: <code>wp_enqueue_script<\/code> now uses the WP 6.3+ array form with <code>strategy: defer<\/code>, allowing the browser to download the admin script in parallel and execute it after DOM parsing \u2014 no change to behaviour, better page load performance.<\/li>\n<li>Improved: <code>register_setting<\/code> now declares <code>type: object<\/code> for schema completeness as recommended since WP 5.5.<\/li>\n<\/ul>\n\n<h4>1.0.40<\/h4>\n\n<ul>\n<li>Added: <code>uninstall.php<\/code> \u2014 removes all plugin options and post-meta (<code>elsio_options<\/code>, <code>_elsio_optimised<\/code>, <code>_elsio_thumbs_regen<\/code>, <code>_elsio_file_hash<\/code>) when the plugin is deleted.<\/li>\n<\/ul>\n\n<h4>1.0.39<\/h4>\n\n<ul>\n<li>Fixed: <code>require_once<\/code> for admin image helpers now loaded after permission check in both bulk-optimizer and thumb-regen AJAX handlers.<\/li>\n<li>Fixed: <code>wp_update_attachment_metadata()<\/code> return value checked in thumbnail regeneration \u2014 returns an error instead of silently marking success if the DB write fails.<\/li>\n<li>Fixed: All admin buttons now have explicit <code>type=\"button\"<\/code> to prevent accidental form submission.<\/li>\n<li>Fixed: Added documentation comment in url-replacer noting the <code>allowed_classes =&gt; false<\/code> limitation for PHP-serialized object properties.<\/li>\n<\/ul>\n\n<h4>1.0.38<\/h4>\n\n<ul>\n<li>Fixed: Critical JavaScript syntax error introduced in 1.0.37 \u2014 macOS <code>sed<\/code> wrote literal <code>\\n\\t<\/code> 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).<\/li>\n<\/ul>\n\n<h4>1.0.37<\/h4>\n\n<ul>\n<li>Fixed: Regenerate Thumbnails tab was completely broken \u2014 <code>regenLog()<\/code> was called but only <code>regenLogMsg()<\/code> was defined, causing a JavaScript runtime error.<\/li>\n<li>Fixed: <code>unserialize()<\/code> in <code>ELSIO_Url_Replacer<\/code> now passes <code>['allowed_classes' =&gt; false]<\/code> to prevent PHP object injection from malicious serialised postmeta values.<\/li>\n<li>Fixed: Replaced the single shared <code>pendingTimer<\/code> with four separate timers (<code>bulkTimer<\/code>, <code>ghostTimer<\/code>, <code>dedupTimer<\/code>, <code>regenTimer<\/code>) so concurrent or back-to-back operations can never cancel each other's inter-image delay.<\/li>\n<\/ul>\n\n<h4>1.0.36<\/h4>\n\n<ul>\n<li>Added: Regenerate Thumbnails tab \u2014 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.<\/li>\n<\/ul>\n\n<h4>1.0.35<\/h4>\n\n<ul>\n<li>Fixed: PNG\u2192JPEG URL replacement now covers <code>post_excerpt<\/code> and all postmeta values (including PHP-serialised data stored by Elementor, ACF, WPBakery, and other page builders), not just <code>post_content<\/code>. The new <code>ELSIO_Url_Replacer<\/code> helper is also used by the duplicate-finder cleanup.<\/li>\n<\/ul>\n\n<h4>1.0.34<\/h4>\n\n<ul>\n<li>Fixed: Bulk optimiser no longer calls <code>wp_generate_attachment_metadata()<\/code> unconditionally on every image. It now only regenerates metadata (and thumbnails) when the format changed (PNG\u2192JPEG) or the image was resized \u2014 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.<\/li>\n<li>Fixed: Thumbnails were previously double-processed during bulk runs \u2014 once by <code>compress_thumbnails_filter<\/code> (triggered inside <code>wp_generate_attachment_metadata<\/code>) and again by the manual loop that always followed. The two code paths are now mutually exclusive.<\/li>\n<\/ul>\n\n<h4>1.0.33<\/h4>\n\n<ul>\n<li>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 <code>elsio_bulk_get_stats<\/code> AJAX endpoint was added to return all three counters (total \/ optimised \/ pending) in one call.<\/li>\n<\/ul>\n\n<h4>1.0.32<\/h4>\n\n<ul>\n<li>Fixed: <code>png_has_transparency()<\/code> now returns <code>true<\/code> (assume transparency, skip conversion) when GD cannot load the PNG, instead of <code>false<\/code> \u2014 prevents producing a broken JPEG from an unreadable image.<\/li>\n<li>Fixed: <code>resize_image()<\/code> now returns early when the calculated output dimensions round to zero, avoiding a <code>imagecreatetruecolor(0, 0)<\/code> failure on very small source images.<\/li>\n<li>Fixed: PNG\u2192JPEG URL replacement in the bulk optimiser now fetches matching rows and replaces in PHP (<code>str_replace<\/code>) instead of using SQL <code>REPLACE()<\/code>, so only exact URL occurrences are updated and surrounding content cannot be corrupted.<\/li>\n<li>Fixed: Duplicate file scan now caches the MD5 hash in post meta (keyed on file mtime) so repeated scans skip unchanged files \u2014 large libraries scan significantly faster on subsequent runs.<\/li>\n<li>Fixed: <code>ajax_process_one()<\/code> 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.<\/li>\n<li>Improved: PNG transparency Stage 3 is now split into a fast 20\u00d720 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.<\/li>\n<\/ul>\n\n<h4>1.0.31<\/h4>\n\n<ul>\n<li>Fixed: Race condition in WebP sidecar size comparison \u2014 <code>filesize()<\/code> calls are now guarded with <code>file_exists()<\/code> to prevent PHP warnings if a concurrent process removes the file between stat-cache clear and size read.<\/li>\n<li>Fixed: <code>generate_webp_sidecar()<\/code> now guards <code>imagewebp()<\/code> with an internal <code>function_exists()<\/code> check, making the method safe to call directly without relying on the caller's guard.<\/li>\n<li>Fixed: <code>resize_image()<\/code> now returns false immediately when either source dimension is zero, preventing a division-by-zero on corrupted image files.<\/li>\n<li>Fixed: <code>png_has_transparency()<\/code> 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.<\/li>\n<li>Fixed: Duplicate-finder regex patterns now use <code>preg_quote()<\/code> on the ID string for future-proofing, even though <code>absint()<\/code> already guarantees digits only.<\/li>\n<li>Improved: Replaced magic literals (<code>4<\/code>, <code>6<\/code>, <code>0x7F<\/code>, <code>9<\/code>, <code>26<\/code>) in <code>ELSIO_Image_Optimizer<\/code> with named class constants (<code>PNG_COLOR_TYPE_GREYSCALE_ALPHA<\/code>, <code>PNG_COLOR_TYPE_RGBA<\/code>, <code>GD_ALPHA_BITMASK<\/code>, <code>GD_PNG_MAX_COMPRESSION<\/code>, <code>PNG_HEADER_READ_BYTES<\/code>) for clarity.<\/li>\n<li>Improved: Informational <code>error_log<\/code> messages (PNG\u2192JPEG discarded, WebP sidecar discarded) now carry an <code>[INFO]<\/code> tag to distinguish them from actual errors in the log.<\/li>\n<\/ul>\n\n<h4>1.0.30<\/h4>\n\n<ul>\n<li>Fixed: <code>png_has_transparency()<\/code> Stage 3 previously sampled a 10\u00d710 pixel grid, which could miss a small transparent area. It now scans every pixel so that even a single transparent pixel prevents PNG\u2192JPEG conversion.<\/li>\n<\/ul>\n\n<h4>1.0.29<\/h4>\n\n<ul>\n<li>Fixed: PNG\u2192JPEG conversion broke image links in posts. Two root causes:\n\n<ol>\n<li><code>handle_upload_filter()<\/code> returned the original <code>$upload<\/code> array unchanged after conversion \u2014 WordPress created the attachment record with <code>image\/png<\/code> MIME type and the old <code>.png<\/code> URL even though the file on disk was already a <code>.jpg<\/code>. The filter now updates <code>$upload['file']<\/code>, <code>$upload['url']<\/code> and <code>$upload['type']<\/code> when conversion happened, so the attachment is registered correctly from the start.<\/li>\n<li>Bulk optimizer never updated existing <code>&lt;img&gt;<\/code> references in post content after conversion. It now captures the old PNG URL before changing the stored file path, calls <code>wp_update_post()<\/code> to set <code>post_mime_type<\/code> to <code>image\/jpeg<\/code>, then runs a <code>REPLACE()<\/code> SQL query to rewrite all occurrences of the old URL to the new JPEG URL across all post content.<\/li>\n<\/ol><\/li>\n<\/ul>\n\n<h4>1.0.28<\/h4>\n\n<ul>\n<li>Fixed: Palette PNGs (color type 3, e.g. icons) had their transparency silently missed by <code>png_has_transparency()<\/code> \u2014 <code>imagecolorat()<\/code> returns a palette index on non-truecolor images whose high bits are not alpha. The function now calls <code>imagepalettetotruecolor()<\/code> before sampling so alpha bits are meaningful.<\/li>\n<li>Fixed: Transparent corners (typical for circular\/icon PNGs) could theoretically be missed by the 10\u00d710 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.<\/li>\n<\/ul>\n\n<h4>1.0.27<\/h4>\n\n<ul>\n<li>Fixed: <code>PHP Warning: imagewebp(): Palette image not supported by webp<\/code> \u2014 palette-based PNGs (color type 3, indexed color) loaded by GD are not truecolor and cannot be passed to <code>imagewebp()<\/code> or certain <code>imagepng()<\/code> operations. Added an <code>imageistruecolor()<\/code> check in <code>load_image()<\/code> immediately after <code>imagecreatefrompng()<\/code>: if the result is a palette image, <code>imagepalettetotruecolor()<\/code> is called before returning the resource. This also prevents three duplicate warnings (original + thumbnails).<\/li>\n<\/ul>\n\n<h4>1.0.26<\/h4>\n\n<ul>\n<li>Fixed: Pending count in the stats row was not refreshed after ghost attachment cleanup completed. Added <code>refreshPendingCount()<\/code> call at the end of the ghost <code>cleanNext()<\/code> loop.<\/li>\n<\/ul>\n\n<h4>1.0.25<\/h4>\n\n<ul>\n<li>Fixed: \"Fix all duplicates\" had no effect \u2014 duplicate attachments were never actually deleted. <code>new URLSearchParams({ 'dup_ids[]': [1,2,3] })<\/code> serialised the array as a single comma-joined string (<code>dup_ids[]=1%2C2%2C3<\/code>), so PHP received a string rather than an array, <code>is_array()<\/code> returned false, and the ID list was discarded. Replaced the <code>Object.assign<\/code> + <code>URLSearchParams(object)<\/code> pattern in <code>post()<\/code> and <code>postRaw()<\/code> with a <code>buildParams()<\/code> helper that iterates array values and appends them individually, producing correct <code>dup_ids[]=1&amp;dup_ids[]=2<\/code> serialisation.<\/li>\n<\/ul>\n\n<h4>1.0.24<\/h4>\n\n<ul>\n<li>Fixed: PHPCS warning \u2014 replaced <code>fopen<\/code>\/<code>fread<\/code>\/<code>fclose<\/code> in <code>png_has_transparency()<\/code> with <code>file_get_contents(..., 0, 26)<\/code> + <code>phpcs:ignore<\/code> annotation; reads only the 26-byte PNG header without loading the whole file via WP_Filesystem.<\/li>\n<\/ul>\n\n<h4>1.0.23<\/h4>\n\n<ul>\n<li>Fixed: XSS risk in dedup summary \u2014 <code>buildDedupSummaryHTML()<\/code> injected backend values directly into <code>.innerHTML<\/code>. Replaced with DOM-based <code>buildDedupSummary()<\/code> using <code>textContent<\/code> throughout; no user-controlled data can reach the parser.<\/li>\n<li>Fixed: <code>dedupProcessNext()<\/code> and <code>dedupFinish()<\/code> accessed <code>dedupBar<\/code>, <code>dedupProgressWrap<\/code>, <code>dedupLabel<\/code>, <code>btnDedupFix<\/code>, <code>btnDedupScan<\/code> without null checks \u2014 any missing DOM element crashed the entire fix flow. Added guards throughout.<\/li>\n<li>Fixed: <code>onGhostsCleanClick()<\/code> called <code>JSON.parse(btnClean.dataset.ids)<\/code> without try\/catch \u2014 malformed data would throw and break the cleanup handler. Wrapped in try\/catch with an error log message.<\/li>\n<li>Fixed: PNG\u2192JPEG path detection in bulk optimizer checked <code>file_exists($jpg_path)<\/code> alone \u2014 a pre-existing .jpg with the same stem would be mistakenly used. Now also verifies the original PNG is gone (<code>!file_exists($file_path)<\/code>) before treating the .jpg as the converted output.<\/li>\n<li>Fixed: <code>formatBytes()<\/code> did not handle negative or non-numeric input \u2014 normalised with <code>Math.max(0, parseInt(...) || 0)<\/code>.<\/li>\n<li>Fixed: <code>sprintf()<\/code> returned <code>undefined<\/code> for out-of-bounds positional args \u2014 added bounds check, returns empty string instead.<\/li>\n<li>Fixed: Pending <code>setTimeout<\/code> timers were not cancelled on page unload \u2014 AJAX requests could fire after navigation. Added <code>beforeunload<\/code> listener that clears <code>pendingTimer<\/code>.<\/li>\n<li>Fixed: <code>is_apache()<\/code> returned false for Apache servers where <code>SERVER_SOFTWARE<\/code> is masked by a proxy. Added <code>function_exists('apache_get_modules')<\/code> as a fallback.<\/li>\n<li>Clarified: PNG compression intentionally reuses the JPEG quality setting for GD compression level \u2014 added inline comment to make this explicit.<\/li>\n<\/ul>\n\n<h4>1.0.22<\/h4>\n\n<ul>\n<li>Fixed: PNG\u2192JPEG conversion block destroyed the GD image resource before falling through to step 3 \u2014 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; <code>$image<\/code> is now destroyed exactly once.<\/li>\n<li>Fixed: <code>png_has_transparency()<\/code> only sampled a 10\u00d710 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) \u2014 types 4 and 6 now return <code>true<\/code> immediately without loading the image.<\/li>\n<\/ul>\n\n<h4>1.0.21<\/h4>\n\n<ul>\n<li>Fixed: Gutenberg block <code>\"id\":5<\/code> LIKE pattern also matched <code>\"id\":15<\/code> \u2014 replaced SQL REPLACE with PHP-side <code>preg_replace<\/code> using a negative-lookahead <code>(?!\\d)<\/code> boundary, preventing IDs that start with the same digits from being corrupted.<\/li>\n<li>Fixed: Dedup progress bar had class <code>elsio-progress-bar<\/code> instead of <code>elsio-progress-fill<\/code> \u2014 the animated fill bar was invisible.<\/li>\n<li>Fixed: <code>ajax_process_group()<\/code> had no upper bound on <code>dup_ids[]<\/code> \u2014 added a hard limit of 1000 IDs per request to prevent DoS via oversized payloads.<\/li>\n<li>Fixed: PNG\u2192JPEG conversion always deleted the source PNG even when the resulting JPEG was larger \u2014 now compares file sizes and discards the JPEG if it is not smaller, keeping the original PNG.<\/li>\n<li>Fixed: WebP sidecar deletion (when sidecar was not smaller than source) was silent \u2014 now logs a notice under <code>WP_DEBUG_LOG<\/code>.<\/li>\n<\/ul>\n\n<h4>1.0.20<\/h4>\n\n<ul>\n<li>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 \u2014 only the database records are removed.<\/li>\n<\/ul>\n\n<h4>1.0.19<\/h4>\n\n<ul>\n<li>Fixed: Ghost attachments (DB record exists but file missing on disk) were counted as errors during bulk optimisation. Now returned as a success with <code>skipped:true<\/code>, logged in orange as a warning, and excluded from the error count.<\/li>\n<\/ul>\n\n<h4>1.0.18<\/h4>\n\n<ul>\n<li>Fixed: \"Fix all duplicates\" button had no click handler \u2014 <code>initDedup()<\/code> ran on page load when the Duplicates tab was not active, so all <code>getElementById()<\/code> calls returned null. Replaced with document-level event delegation so clicks are captured regardless of which tab rendered the page.<\/li>\n<\/ul>\n\n<h4>1.0.17<\/h4>\n\n<ul>\n<li>Fixed: Duplicate finder stat boxes showed <code>undefined<\/code> labels \u2014 i18n keys <code>groups<\/code>, <code>redundantFiles<\/code>, <code>recoverable<\/code> were missing from the PHP localization array.<\/li>\n<li>Fixed: \"Scanning Media Library&#8230;\" displayed the HTML entity literally \u2014 replaced with plain ASCII ellipsis since <code>textContent<\/code> does not parse HTML entities.<\/li>\n<\/ul>\n\n<h4>1.0.16<\/h4>\n\n<ul>\n<li>New feature: \"Find Duplicates\" tab under Settings \u2192 Image Optimiser.<\/li>\n<li>Scans the entire Media Library for exact duplicate images using MD5 file hashing.<\/li>\n<li>Keeps the oldest copy (lowest attachment ID) and updates all references: image URLs in post content, Gutenberg block <code>id<\/code> attributes, <code>wp-image-X<\/code> CSS classes, and <code>_thumbnail_id<\/code> featured image meta.<\/li>\n<li>Duplicate attachments are then permanently deleted via <code>wp_delete_attachment()<\/code>.<\/li>\n<li>Scan uses a 120-second timeout to handle large libraries gracefully.<\/li>\n<\/ul>\n\n<h4>1.0.15<\/h4>\n\n<ul>\n<li>Fixed: WordPress-generated thumbnail sizes (150x150, 300x225, 768x576, 1024x768, etc.) were never compressed \u2014 only the original file was processed. Added <code>wp_generate_attachment_metadata<\/code> filter to compress all thumbnails on upload, and added thumbnail compression loop to the bulk optimiser. Resize and PNG\u2192JPEG conversion are intentionally disabled for thumbnails to preserve WordPress filename expectations.<\/li>\n<\/ul>\n\n<h4>1.0.14<\/h4>\n\n<ul>\n<li>Fixed: PHP 8.1 deprecation notice \"Implicit conversion from float to int loses precision\" on <code>imagecolorat()<\/code> in <code>png_has_transparency()<\/code>. Operator precedence caused <code>$step<\/code> to remain a float (<code>(int)<\/code> was only applied to <code>min()<\/code>, not to the division result). Fixed by wrapping the full expression: <code>(int) round( min( $w, $h ) \/ 10 )<\/code>.<\/li>\n<\/ul>\n\n<h4>1.0.13<\/h4>\n\n<ul>\n<li>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 <code>wp_generate_attachment_metadata()<\/code> and <code>wp_update_attachment_metadata()<\/code> after each optimisation.<\/li>\n<li>Fixed: PNG\u2192JPEG conversion during bulk optimise now correctly updates the stored file path via <code>update_attached_file()<\/code>.<\/li>\n<\/ul>\n\n<h4>1.0.12<\/h4>\n\n<ul>\n<li>Fixed: PNG images within the configured max dimensions were silently skipped and never compressed. <code>needs_recompression()<\/code> now includes <code>image\/png<\/code> so PNG files are always re-encoded regardless of their dimensions.<\/li>\n<\/ul>\n\n<h4>1.0.11<\/h4>\n\n<ul>\n<li>Fixed: HTML entity <code>&amp;#8212;<\/code> displayed as literal text in the bulk log (e.g. <code>saved &amp;#8212; 74%<\/code>). The <code>log()<\/code> function uses <code>textContent<\/code> which does not parse HTML entities \u2014 replaced the entity with the actual UTF-8 em dash character <code>\u2014<\/code> in the PHP string.<\/li>\n<\/ul>\n\n<h4>1.0.10<\/h4>\n\n<ul>\n<li>Fixed: Double percent sign (<code>%%<\/code>) displayed in bulk optimisation log and progress counter. The PHP <code>%%<\/code> escape (for PHP sprintf) was incorrectly used in strings passed to the JavaScript sprintf \u2014 replaced with a single literal <code>%<\/code>.<\/li>\n<\/ul>\n\n<h4>1.0.9<\/h4>\n\n<ul>\n<li>Fixed: Added <code>@package<\/code> tag to main plugin file doc block to satisfy PHPCS file comment requirements.<\/li>\n<li>Fixed: Added <code>phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated<\/code> on all <code>imagedestroy()<\/code> calls \u2014 function remains callable in PHP 8 and is required for PHP 7.4 compatibility.<\/li>\n<li>Fixed: Added <code>phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler<\/code> on <code>set_error_handler()<\/code> calls used to suppress GD warnings without the <code>@<\/code> operator.<\/li>\n<li>Fixed: Corrected doc comment long descriptions in <code>class-elsio-htaccess.php<\/code> that started with lowercase (PHPCS capitalization rule).<\/li>\n<li>Fixed: Added loop incrementer <code>phpcs:ignore<\/code> annotation on the pixel-sampling loop in <code>png_has_transparency()<\/code>.<\/li>\n<\/ul>\n\n<h4>1.0.8<\/h4>\n\n<ul>\n<li>Fixed: WordPress.Security.EscapeOutput.OutputNotEscaped on $notice_text \u2014 store raw translated string with __() and escape at the point of output with esc_html().<\/li>\n<\/ul>\n\n<h4>1.0.7<\/h4>\n\n<ul>\n<li>Fixed: WordPress.WP.I18n.MissingTranslatorsComment \u2014 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.<\/li>\n<\/ul>\n\n<h4>1.0.6<\/h4>\n\n<ul>\n<li>i18n: Fixed <code>\\uXXXX<\/code> JS escape sequences used inside PHP strings \u2014 replaced with HTML entities (<code>&amp;#8230;<\/code>, <code>&amp;#8212;<\/code>, <code>&amp;#8220;<\/code>, <code>&amp;#8221;<\/code>).<\/li>\n<li>i18n: Switched multi-placeholder strings to positional format (<code>%1$s<\/code>, <code>%2$s<\/code>\u2026) so translators can reorder arguments.<\/li>\n<li>i18n: Added <code>\/* translators: *\/<\/code> comments to all strings containing placeholders.<\/li>\n<li>i18n: Updated JS <code>sprintf()<\/code> to support both sequential (<code>%s<\/code>) and positional (<code>%1$s<\/code>) token formats.<\/li>\n<li>i18n: Created <code>languages\/effortless-simple-image-optimiser.pot<\/code> with all 60+ translatable strings.<\/li>\n<\/ul>\n\n<h4>1.0.5<\/h4>\n\n<ul>\n<li>Fixed: PNG compression level formula was inverted \u2014 quality 82 now correctly maps to GD level 7 instead of level 2.<\/li>\n<li>Fixed: <code>imagewebp()<\/code> return value now checked; failures are logged under WP_DEBUG_LOG.<\/li>\n<li>Fixed: WebP sidecar is now deleted if it is not smaller than the source file, preventing .htaccess from serving a larger WebP.<\/li>\n<li>Fixed: Bulk optimizer now resolves the correct output path when PNG is converted to JPEG, preventing false 100% saving reports.<\/li>\n<li>Fixed: Added <code>delete_attachment<\/code> hook \u2014 <code>.webp<\/code> sidecar files are now removed when the parent image is deleted from the Media Library.<\/li>\n<li>Fixed: AJAX <code>fetch()<\/code> requests in JS now carry a 60-second <code>AbortController<\/code> timeout to prevent the bulk queue hanging indefinitely.<\/li>\n<\/ul>\n\n<h4>1.0.4<\/h4>\n\n<ul>\n<li>PHP\/WordPress\/PHPCS\/WPCS compliance pass across all files.<\/li>\n<li>Replaced <code>@<\/code> error suppression with <code>set_error_handler<\/code>\/<code>restore_error_handler<\/code>.<\/li>\n<li><code>error_log()<\/code> calls are now guarded by <code>WP_DEBUG_LOG<\/code>.<\/li>\n<li><code>unlink()<\/code> replaced with <code>wp_delete_file()<\/code>.<\/li>\n<li><code>file_get_contents()<\/code> replaced with WP_Filesystem in htaccess class.<\/li>\n<li><code>is_writable()<\/code> replaced with <code>wp_is_writable()<\/code>.<\/li>\n<li><code>insert_with_markers()<\/code> and <code>get_home_path()<\/code> dependencies loaded explicitly.<\/li>\n<li><code>$_SERVER['SERVER_SOFTWARE']<\/code> sanitised with <code>sanitize_text_field()<\/code> + <code>wp_unslash()<\/code>.<\/li>\n<li>All <code>$_POST<\/code> inputs sanitised with <code>absint()<\/code> + <code>wp_unslash()<\/code>.<\/li>\n<li><code>$_GET<\/code> tab\/action values sanitised with <code>sanitize_key()<\/code> + <code>wp_unslash()<\/code>.<\/li>\n<li>Inline <code>oninput<\/code> JS removed from range fields; replaced with <code>data-linked<\/code> attributes handled in JS.<\/li>\n<li><code>printf<\/code> with raw HTML replaced with proper <code>wp_kses()<\/code> escaping via <code>render_env_row()<\/code> helper.<\/li>\n<li><code>echo $notice<\/code> replaced with pre-escaped output split into typed variables.<\/li>\n<li><code>$wpdb-&gt;delete()<\/code> direct query replaced with <code>delete_metadata( 'post', 0, $key, '', true )<\/code>.<\/li>\n<li><code>new WP_Query()-&gt;found_posts<\/code> replaced with <code>get_posts()<\/code> + <code>count()<\/code> and <code>no_found_rows =&gt; true<\/code>.<\/li>\n<li>Added <code>return<\/code> statements after <code>wp_send_json_error()<\/code> calls to make flow explicit.<\/li>\n<li>Added <code>label_for<\/code> to all settings fields for accessible form labels.<\/li>\n<li>IIFE closure in JS uses <code>}()<\/code> form; <code>finally<\/code> chain preserved for modern browsers.<\/li>\n<\/ul>\n\n<h4>1.0.3<\/h4>\n\n<ul>\n<li>Added Apache <code>.htaccess<\/code> WebP delivery via mod_rewrite (Option 1).<\/li>\n<li>Rules inserted automatically on activation, removed on deactivation, re-synced on settings save.<\/li>\n<li><code>Vary: Accept<\/code> header appended so CDNs cache JPEG and WebP variants separately.<\/li>\n<li>Environment sidebar now shows Apache detection, <code>.htaccess<\/code> writability, and rewrite rule status.<\/li>\n<li>\"Insert now\" button to manually inject rules if they are missing post-activation.<\/li>\n<\/ul>\n\n<h4>1.0.2<\/h4>\n\n<ul>\n<li>Added Bulk Optimise tab under Settings \u2192 Image Optimiser.<\/li>\n<li>New AJAX endpoints: <code>elsio_bulk_get_ids<\/code>, <code>elsio_bulk_process_one<\/code>, <code>elsio_bulk_reset_flags<\/code>.<\/li>\n<li>Progress bar, live log and stats panel for bulk runs.<\/li>\n<li>Post-meta flag <code>_elsio_optimised<\/code> prevents reprocessing already-optimised images.<\/li>\n<li>\"Re-optimise all\" checkbox and \"Reset flags\" button for manual re-runs.<\/li>\n<\/ul>\n\n<h4>1.0.1<\/h4>\n\n<ul>\n<li>Added readme.txt with full documentation.<\/li>\n<li>Minor version bump following project conventions.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release.<\/li>\n<li>JPEG compression with configurable quality.<\/li>\n<li>PNG compression and optional PNG \u2192 JPEG conversion (transparency-aware).<\/li>\n<li>Image resizing with aspect-ratio preservation.<\/li>\n<li>Optional WebP sidecar generation.<\/li>\n<li>Admin settings page under Settings \u2192 Image Optimiser.<\/li>\n<li>GD availability check with graceful fallback.<\/li>\n<\/ul>","raw_excerpt":"Automatically optimise uploaded images using the PHP GD library \u2014 no external APIs, no account required.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/290457","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=290457"}],"author":[{"embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/domclic"}],"wp:attachment":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=290457"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=290457"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=290457"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=290457"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=290457"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=290457"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}