Skip to content

Feat: bulk optimization#34

Open
bfintal wants to merge 6 commits intodevelopfrom
feat/bulk-optimization
Open

Feat: bulk optimization#34
bfintal wants to merge 6 commits intodevelopfrom
feat/bulk-optimization

Conversation

@bfintal
Copy link
Contributor

@bfintal bfintal commented Mar 17, 2026

fixes #26

Summary by CodeRabbit

  • New Features

    • Bulk media optimization UI with progress, sortable image list, per-item actions and aggregated stats
    • Bulk optimization stats updates (track bulk optimize/restore) and improved media metadata display
    • Enhanced image conversion workflow: configurable format/quality, new optimize action, clearer failure reasons and safer error handling
  • Bug Fixes

    • Improved upload/selection error detection and logging for converted files
  • Chores

    • Added image compression/conversion libraries; bumped tested compatibility to 6.9.1

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

Adds bulk image optimization support: new dependencies, admin localization flags, metadata and stats handling for bulk operations, bulk-optimizer admin UI and styles, enhanced client-side file conversion with options/error handling, and new ImageConverter optimize API.

Changes

Cohort / File(s) Summary
Package & Docs
package.json, readme.txt
Introduces runtime deps browser-image-compression, heic2any, classnames, plus mime and resemblejs; updates tested version in readme.txt to 6.9.1.
Admin bootstrap & localization
src/admin/class-admin.php
Adds isPremium and uploadsUrl to localized admin data.
Metadata handling
src/admin/class-metadata.php, src/admin/class-meta-box.php
Adds optimized_during_upload flag to sanitized metadata; preserve_cimo_metadata short-circuits when incoming metadata contains cimo; meta-box rendering updated to aggregate and display bulk optimization totals and translated strings.
Stats tracking
src/admin/class-stats.php
Parses cimo['bulk_optimization'] entries into totals and adds two public methods: update_stats_bulk_optimized and update_stats_bulk_restored.
Admin UI & Settings
src/admin/js/page/admin-settings.js, src/admin/css/admin-page.css, src/admin/css/index.css
Adds Bulk Optimization component hook, premium placeholders/learn-more actions, premium class toggle; large admin-page CSS for bulk optimizer UI, table/list styles, animations, responsive adjustments, and minor media-manager CSS tweaks.
Media manager UI logic
src/admin/js/media-manager/sidebar-info.js
Short-circuits when no meaningful metadata; computes aggregated original/converted sizes and percent saved; renders bulk-specific rows and arrows indicating size change.
File processing error handling
src/admin/js/media-manager/drop-zone.js, src/admin/js/media-manager/select-files.js
Capture converter results in a variable, detect/log per-result errors, set a general error flag, and return result objects rather than letting exceptions bubble.
Image conversion engine
src/shared/converters/image-converter.js
Signature change: convert(force = false, options = {}); adds optimize() method. Adds options (quality, forceSize, formatTo), early-return reasons (not-an-image, same-format, format-not-supported, animated-gif, resulting-image-bigger-than-input), PNG handling via dynamic import, richer metadata on success, and catch paths returning structured failure objects.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as Bulk Optimizer UI
    participant FileProc as File Processor (drop/select)
    participant Converter as ImageConverter
    participant Stats as Cimo_Stats
    participant Admin as Backend (Meta/Options)

    User->>UI: Select or drop files
    UI->>FileProc: Start processing files
    FileProc->>Converter: convert(force, options)
    alt conversion error or unsupported
        Converter-->>FileProc: { error, reason, message }
        FileProc-->>FileProc: log error, mark hasError
    else conversion success
        Converter-->>FileProc: { file, metadata }
        FileProc->>Admin: attach optimized_during_upload flag / save metadata
        FileProc->>Stats: update_stats_bulk_optimized( attachment, originalKB, optimizedKB )
        Stats-->>Admin: update_option(stats)
    end
    UI->>UI: update progress / animations
    UI-->>User: show results and totals
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I nibbled code and found a way,

to shrink your pictures, save the day.
Progress bars blink, the pixels cheer,
Bulk optimized — carrots near! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: bulk optimization' directly aligns with the PR's main objective to implement bulk optimization functionality, accurately summarizing the primary change.
Linked Issues check ✅ Passed The PR successfully implements bulk optimization support with new stats tracking methods, UI components, metadata handling, image conversion options, and comprehensive styling for the bulk optimizer interface.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing bulk optimization: dependency additions for image processing, metadata and stats tracking, UI components, file conversion enhancements, and styling updates all support the core bulk optimization feature.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bulk-optimization

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Stylelint (17.4.0)
src/admin/css/index.css

ConfigurationError: Could not find "@wordpress/stylelint-config". Do you need to install the package or use the "configBasedir" option?
at getModulePath (file:///usr/local/lib/node_modules/stylelint/lib/utils/getModulePath.mjs:29:9)
at loadExtendedConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:241:21)
at extendConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:208:25)
at augmentConfigBasic (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:73:26)
at augmentConfigFull (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:126:30)
at getConfigForFile (file:///usr/local/lib/node_modules/stylelint/lib/getConfigForFile.mjs:102:32)
at async resolveOptionValue (file:///usr/local/lib/node_modules/stylelint/lib/utils/resolveOptionValue.mjs:27:24)
at async standalone (file:///usr/local/lib/node_modules/stylelint/lib/standalone.mjs:202:19)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use your project's `biome` configuration to improve the quality of JS/TS/CSS/JSON code reviews.

Add a configuration file to your project to customize how CodeRabbit runs biome.

@github-actions
Copy link

github-actions bot commented Mar 17, 2026

🤖 Pull request artifacts

file commit
pr34-cimo-34-merge.zip 69a2fbd

github-actions bot added a commit that referenced this pull request Mar 17, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/admin/class-stats.php (2)

183-189: Minor: Unused $attachment_id parameter.

Static analysis notes this parameter is unused. If reserved for future tracking, prefix with underscore to silence warnings.

-	public static function update_stats_bulk_optimized( $attachment_id, $original_size, $optimized_size ) {
+	public static function update_stats_bulk_optimized( $_attachment_id, $original_size, $optimized_size ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/class-stats.php` around lines 183 - 189, The parameter
$attachment_id in the static method update_stats_bulk_optimized is unused and
triggers static-analysis warnings; to silence them, rename the parameter to
$_attachment_id (or otherwise prefix it with an underscore) in the method
signature and any callers, leaving the body unchanged; reference:
update_stats_bulk_optimized and its parameter $attachment_id.

93-93: Minor: Unused loop variable $size.

Static analysis flagged that $size is unused in the loop body. If it's not needed, use $_ or just iterate values.

-				foreach ( $cimo_data['bulk_optimization'] as $size => $data ) {
+				foreach ( $cimo_data['bulk_optimization'] as $data ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/class-stats.php` at line 93, The foreach over
$cimo_data['bulk_optimization'] declares an unused loop key $size; update the
loop in src/admin/class-stats.php to avoid the unused variable by iterating only
values (e.g., change the foreach signature that currently uses $size to use just
$data) or explicitly use an underscore key (e.g., $_ => $data) so static
analysis no longer flags $size as unused while preserving the existing loop body
that references $data.
src/shared/converters/image-converter.js (1)

360-363: Consider documenting the expected filter contract.

The optimize() method delegates to a WordPress filter, but the expected return type and filter behavior are undocumented. Consider adding a JSDoc comment explaining what filters should return.

📝 Suggested documentation
+	/**
+	 * Optimize the image using registered filters.
+	 * Filters receive this ImageConverter instance and should return
+	 * an object with { file, metadata } or the original instance if no optimization is performed.
+	 * `@return` {Promise<Object>} Promise resolving to optimization result.
+	 */
 	async optimize() {
 		return await applyFiltersAsync( 'cimo.imageConverter.optimize', this )
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/converters/image-converter.js` around lines 360 - 363, The
optimize() method delegates to the WordPress filter
'cimo.imageConverter.optimize' via applyFiltersAsync(this), but there is no
JSDoc describing the expected filter contract; add a JSDoc block above the
optimize() method that documents the expected input (this ImageConverter
instance), the expected return type (e.g. a Promise resolving to void, an
updated ImageConverter, or a specific result object), whether filters should
mutate the instance or return a new one, and any error/exception behavior;
reference the optimize() method name and the filter hook
'cimo.imageConverter.optimize' in the comment so implementers know what to
implement when hooking into applyFiltersAsync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/admin/class-metadata.php`:
- Around line 162-164: You added an associative key 'optimized_during_upload' =>
true into $sanitized_metadata (a numerically-indexed array), which mixes array
types and breaks consumers like add_attachment_metadata() that expect each item
to be an array with a 'filename' key; instead, either (A) push a properly
structured metadata entry into $sanitized_metadata (e.g., an array with
'filename' and a flag field) so iteration still yields arrays, or (B) keep the
boolean flag separate (e.g., set it on the attachment post meta or a separate
variable/property) and do not inject an associative key into
$sanitized_metadata; update any usages in add_attachment_metadata(),
class-meta-box.php, and class-stats.php to read the flag from the new location.

In `@src/admin/class-stats.php`:
- Around line 198-204: In update_stats_bulk_restored, the two size fields are
using the wrong parameters: swap the size arithmetic so total_optimized_size
subtracts $optimized_size/1024 and total_original_size is adjusted only
according to how originals were tracked during optimization (e.g. subtract
$restored_size/1024 if originals were previously added, otherwise leave
unchanged) — update the lines in update_stats_bulk_restored to use
$optimized_size and $restored_size in the correct places (reference function
update_stats_bulk_restored and variables $optimized_size, $restored_size,
$stats['total_original_size'], $stats['total_optimized_size']); also address the
unused $attachment_id by either prefixing it with an underscore or adding a //
phpcs:ignore comment.
- Around line 87-106: The code double-counts media_optimized_num because it
increments inside the foreach over $cimo_data['bulk_optimization'] and then
unconditionally again after that block; update the logic around $cimo_data and
$updated_stats so the final $updated_stats['media_optimized_num']++ only runs
for individually optimized images (i.e., when bulk_optimization is not present
or empty). Concretely, use an if/else or guard that checks
isset($cimo_data['bulk_optimization']) (and non-empty if appropriate) and only
perform the standalone increment when no bulk_optimization entries were
processed, leaving the increments inside the foreach unchanged.

In `@src/shared/converters/image-converter.js`:
- Around line 229-231: The format normalization can leave formatTo as an
unsupported string (e.g., "jpeg") which causes formatInfo to be undefined later;
update the logic around formatTo (derived from options.format and
this.options?.format) to canonicalize common aliases (e.g., "jpeg" -> "jpg")
and/or map "image/..." MIME types to supportedFormats values, then look up
supportedFormats to get formatInfo and if lookup returns undefined fall back to
a safe default like 'webp' (and log or throw a clear error if appropriate) so
that any subsequent use of formatInfo.mimeType in the image conversion flow
cannot dereference undefined.

---

Nitpick comments:
In `@src/admin/class-stats.php`:
- Around line 183-189: The parameter $attachment_id in the static method
update_stats_bulk_optimized is unused and triggers static-analysis warnings; to
silence them, rename the parameter to $_attachment_id (or otherwise prefix it
with an underscore) in the method signature and any callers, leaving the body
unchanged; reference: update_stats_bulk_optimized and its parameter
$attachment_id.
- Line 93: The foreach over $cimo_data['bulk_optimization'] declares an unused
loop key $size; update the loop in src/admin/class-stats.php to avoid the unused
variable by iterating only values (e.g., change the foreach signature that
currently uses $size to use just $data) or explicitly use an underscore key
(e.g., $_ => $data) so static analysis no longer flags $size as unused while
preserving the existing loop body that references $data.

In `@src/shared/converters/image-converter.js`:
- Around line 360-363: The optimize() method delegates to the WordPress filter
'cimo.imageConverter.optimize' via applyFiltersAsync(this), but there is no
JSDoc describing the expected filter contract; add a JSDoc block above the
optimize() method that documents the expected input (this ImageConverter
instance), the expected return type (e.g. a Promise resolving to void, an
updated ImageConverter, or a specific result object), whether filters should
mutate the instance or return a new one, and any error/exception behavior;
reference the optimize() method name and the filter hook
'cimo.imageConverter.optimize' in the comment so implementers know what to
implement when hooking into applyFiltersAsync.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3daa2999-3f41-463c-a3d5-042f6c705705

📥 Commits

Reviewing files that changed from the base of the PR and between 7f63e64 and a7762b0.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • package.json
  • readme.txt
  • src/admin/class-admin.php
  • src/admin/class-metadata.php
  • src/admin/class-stats.php
  • src/admin/css/admin-page.css
  • src/admin/js/media-manager/drop-zone.js
  • src/admin/js/media-manager/select-files.js
  • src/admin/js/page/admin-settings.js
  • src/shared/converters/image-converter.js

Comment on lines +162 to +164
// Add our metadata key to note that this was optimized during upload.
$sanitized_metadata['optimized_during_upload'] = true;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Data structure inconsistency: mixing non-numeric key into a numerically-indexed array.

$sanitized_metadata is a numerically-indexed array of metadata entries. Adding 'optimized_during_upload' => true as an associative key creates a mixed array that will cause issues downstream:

  1. In add_attachment_metadata() (line 215), the foreach iterates expecting each $item to be an array with a 'filename' key. When it encounters the boolean true value, isset($item['filename']) will fail or produce unexpected behavior.

  2. This could also affect consumers in class-meta-box.php and class-stats.php that expect consistent data structures.

Consider storing this flag separately or restructuring to avoid mixing numeric and associative keys:

Proposed fix
-		// Add our metadata key to note that this was optimized during upload.
-		$sanitized_metadata['optimized_during_upload'] = true;
-
 		// Save to a transient queue for metadata waiting for attachment creation
 		$transient_key = 'cimo_metadata_queue';
 		$queue = get_transient( $transient_key );
 		if ( ! is_array( $queue ) ) {
 			$queue = [];
 		}

 		// Append new metadata to the queue
-		$queue = array_merge( $queue, $sanitized_metadata );
+		// Mark each entry as optimized during upload
+		foreach ( $sanitized_metadata as &$entry ) {
+			$entry['optimized_during_upload'] = true;
+		}
+		$queue = array_merge( $queue, $sanitized_metadata );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/class-metadata.php` around lines 162 - 164, You added an
associative key 'optimized_during_upload' => true into $sanitized_metadata (a
numerically-indexed array), which mixes array types and breaks consumers like
add_attachment_metadata() that expect each item to be an array with a 'filename'
key; instead, either (A) push a properly structured metadata entry into
$sanitized_metadata (e.g., an array with 'filename' and a flag field) so
iteration still yields arrays, or (B) keep the boolean flag separate (e.g., set
it on the attachment post meta or a separate variable/property) and do not
inject an associative key into $sanitized_metadata; update any usages in
add_attachment_metadata(), class-meta-box.php, and class-stats.php to read the
flag from the new location.

Comment on lines +87 to 106

/**
* This data comes from bulk optimizing media in the Media Library:
*/

if ( isset( $cimo_data['bulk_optimization'] ) ) {
foreach ( $cimo_data['bulk_optimization'] as $size => $data ) {
$updated_stats['media_optimized_num']++;
$original_size_b = isset( $data['originalFilesize'] ) ? (int) $data['originalFilesize'] : 0;
$converted_size_b = isset( $data['convertedFilesize'] ) ? (int) $data['convertedFilesize'] : 0;
$updated_stats['total_original_size'] += $original_size_b / 1024;
$updated_stats['total_optimized_size'] += $converted_size_b / 1024;
}
}

/**
* This data comes from individually optimized images from the user uploading media:
*/

$updated_stats['media_optimized_num']++;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Double-counting issue: media_optimized_num is incremented both inside and outside the bulk optimization loop.

When bulk_optimization data exists, the code increments media_optimized_num for each entry inside the loop (line 94), but then also unconditionally increments it again at line 106. This results in double-counting.

Proposed fix: only count individually optimized images when bulk_optimization is absent
 				if ( isset( $cimo_data['bulk_optimization'] ) ) {
 					foreach ( $cimo_data['bulk_optimization'] as $size => $data ) {
 						$updated_stats['media_optimized_num']++;
 						$original_size_b = isset( $data['originalFilesize'] ) ? (int) $data['originalFilesize'] : 0;
 						$converted_size_b = isset( $data['convertedFilesize'] ) ? (int) $data['convertedFilesize'] : 0;
 						$updated_stats['total_original_size'] += $original_size_b / 1024;
 						$updated_stats['total_optimized_size'] += $converted_size_b / 1024;
 					}
-				}
-
-				/**
-				 * This data comes from individually optimized images from the user uploading media:
-				 */
-
-				$updated_stats['media_optimized_num']++;
-
-				// Extract file sizes (bytes) and add to KB totals
-				$original_size_b = isset( $cimo_data['originalFilesize'] ) ? (int) $cimo_data['originalFilesize'] : 0;
-				$converted_size_b = isset( $cimo_data['convertedFilesize'] ) ? (int) $cimo_data['convertedFilesize'] : 0;
-
-				$updated_stats['total_original_size'] += $original_size_b / 1024;
-				$updated_stats['total_optimized_size'] += $converted_size_b / 1024;
+				} else {
+					/**
+					 * This data comes from individually optimized images from the user uploading media:
+					 */
+					$updated_stats['media_optimized_num']++;
+
+					// Extract file sizes (bytes) and add to KB totals
+					$original_size_b = isset( $cimo_data['originalFilesize'] ) ? (int) $cimo_data['originalFilesize'] : 0;
+					$converted_size_b = isset( $cimo_data['convertedFilesize'] ) ? (int) $cimo_data['convertedFilesize'] : 0;
+
+					$updated_stats['total_original_size'] += $original_size_b / 1024;
+					$updated_stats['total_optimized_size'] += $converted_size_b / 1024;
+				}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 93-93: Avoid unused local variables such as '$size'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/class-stats.php` around lines 87 - 106, The code double-counts
media_optimized_num because it increments inside the foreach over
$cimo_data['bulk_optimization'] and then unconditionally again after that block;
update the logic around $cimo_data and $updated_stats so the final
$updated_stats['media_optimized_num']++ only runs for individually optimized
images (i.e., when bulk_optimization is not present or empty). Concretely, use
an if/else or guard that checks isset($cimo_data['bulk_optimization']) (and
non-empty if appropriate) and only perform the standalone increment when no
bulk_optimization entries were processed, leaving the increments inside the
foreach unchanged.

Comment on lines +198 to +204
public static function update_stats_bulk_restored( $attachment_id, $optimized_size, $restored_size ) {
$stats = self::get_stats();
$stats['media_optimized_num']--;
$stats['total_original_size'] -= $optimized_size / 1024;
$stats['total_optimized_size'] -= $restored_size / 1024;
update_option( self::OPTION_KEY, $stats, false );
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Verify the restore stats logic: parameter usage appears inverted.

The subtraction logic seems backwards:

  • total_original_size -= $optimized_size — but when restoring, the original size should remain unchanged or increase
  • total_optimized_size -= $restored_size — this should subtract the optimized size being removed

Also, $attachment_id is unused per static analysis. If it's reserved for future use, consider adding a // phpcs:ignore comment or prefixing with underscore.

Suggested fix (verify intended behavior)
-	public static function update_stats_bulk_restored( $attachment_id, $optimized_size, $restored_size ) {
+	public static function update_stats_bulk_restored( $_attachment_id, $optimized_size, $restored_size ) {
 		$stats = self::get_stats();
 		$stats['media_optimized_num']--;
-		$stats['total_original_size'] -= $optimized_size / 1024;
-		$stats['total_optimized_size'] -= $restored_size / 1024;
+		$stats['total_original_size'] -= $restored_size / 1024;
+		$stats['total_optimized_size'] -= $optimized_size / 1024;
 		update_option( self::OPTION_KEY, $stats, false );
 	}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 198-198: Avoid unused parameters such as '$attachment_id'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/class-stats.php` around lines 198 - 204, In
update_stats_bulk_restored, the two size fields are using the wrong parameters:
swap the size arithmetic so total_optimized_size subtracts $optimized_size/1024
and total_original_size is adjusted only according to how originals were tracked
during optimization (e.g. subtract $restored_size/1024 if originals were
previously added, otherwise leave unchanged) — update the lines in
update_stats_bulk_restored to use $optimized_size and $restored_size in the
correct places (reference function update_stats_bulk_restored and variables
$optimized_size, $restored_size, $stats['total_original_size'],
$stats['total_optimized_size']); also address the unused $attachment_id by
either prefixing it with an underscore or adding a // phpcs:ignore comment.

Comment on lines +229 to +231
let formatTo = options.format || this.options?.format || ''
formatTo = ( formatTo.startsWith( 'image/' ) ? supportedFormats.find( f => f.mimeType === formatTo )?.value : formatTo ) ||
'webp'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential null dereference when formatTo is an unsupported format string.

If a caller passes formatTo = 'jpeg' (rather than 'jpg' or 'image/jpeg'), the normalization logic preserves it unchanged. Then at line 243, formatInfo becomes undefined since supportedFormats uses 'jpg' as the value. This causes a crash at line 336 when accessing formatInfo.mimeType.

Consider adding early validation:

🛡️ Proposed fix to validate formatTo
 let formatTo = options.format || this.options?.format || ''
 formatTo = ( formatTo.startsWith( 'image/' ) ? supportedFormats.find( f => f.mimeType === formatTo )?.value : formatTo ) ||
     'webp'
+
+ // Normalize 'jpeg' to 'jpg' for consistency
+ if ( formatTo === 'jpeg' ) {
+     formatTo = 'jpg'
+ }
+
+ // Validate formatTo is a supported format
+ const formatInfo = supportedFormats.find( f => f.value === formatTo )
+ if ( ! formatInfo ) {
+     return {
+         file,
+         metadata: null,
+         reason: 'unsupported-format',
+         error: `Unsupported output format: ${ formatTo }`,
+     }
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let formatTo = options.format || this.options?.format || ''
formatTo = ( formatTo.startsWith( 'image/' ) ? supportedFormats.find( f => f.mimeType === formatTo )?.value : formatTo ) ||
'webp'
let formatTo = options.format || this.options?.format || ''
formatTo = ( formatTo.startsWith( 'image/' ) ? supportedFormats.find( f => f.mimeType === formatTo )?.value : formatTo ) ||
'webp'
// Normalize 'jpeg' to 'jpg' for consistency
if ( formatTo === 'jpeg' ) {
formatTo = 'jpg'
}
// Validate formatTo is a supported format
const formatInfo = supportedFormats.find( f => f.value === formatTo )
if ( ! formatInfo ) {
return {
file,
metadata: null,
reason: 'unsupported-format',
error: `Unsupported output format: ${ formatTo }`,
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/converters/image-converter.js` around lines 229 - 231, The format
normalization can leave formatTo as an unsupported string (e.g., "jpeg") which
causes formatInfo to be undefined later; update the logic around formatTo
(derived from options.format and this.options?.format) to canonicalize common
aliases (e.g., "jpeg" -> "jpg") and/or map "image/..." MIME types to
supportedFormats values, then look up supportedFormats to get formatInfo and if
lookup returns undefined fall back to a safe default like 'webp' (and log or
throw a clear error if appropriate) so that any subsequent use of
formatInfo.mimeType in the image conversion flow cannot dereference undefined.

github-actions bot added a commit that referenced this pull request Mar 21, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/admin/class-meta-box.php (1)

101-109: Drop the unused bulk locals.

$bulk_optimization_sizes and $size_key are never read, so they just add PHPMD noise here.

Small cleanup
-							$bulk_optimization_sizes = array_keys( $cimo['bulk_optimization'] );
@@
-								foreach ( $cimo['bulk_optimization'] as $size_key => $bulk_data ) {
+								foreach ( $cimo['bulk_optimization'] as $bulk_data ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/class-meta-box.php` around lines 101 - 109, Remove the unused
locals $bulk_optimization_sizes and the unused foreach key variable $size_key to
silence PHPMD; delete the line that sets $bulk_optimization_sizes = array_keys(
$cimo['bulk_optimization'] ) and change the foreach over
$cimo['bulk_optimization'] to use a value-only form (foreach (
$cimo['bulk_optimization'] as $bulk_data ) ) inside the block guarded by
$is_bulk_optimized, leaving the existing $orig, $bulk_original_filesize and
$bulk_converted_filesize logic intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/admin/class-meta-box.php`:
- Around line 94-120: Short-circuit rendering and avoid dividing by zero: detect
when the per-image stat compressionSavings is missing and $is_bulk_optimized is
false/empty and return/skip rendering the meta box (or the stats block) early;
when handling $cimo['bulk_optimization'] in the block around $is_bulk_optimized
and the loop that computes $bulk_original_filesize/$bulk_converted_filesize, add
a guard before computing $optimization_savings to ensure $original_filesize > 0
(otherwise set $optimization_savings to 0 or skip showing it) and remove the
unconditional division; update logic that sets
$original_filesize/$converted_filesize and $optimization_savings so it only runs
when there are usable stats (use $is_bulk_optimized and nonzero
$bulk_original_filesize as conditions).

In `@src/admin/js/media-manager/sidebar-info.js`:
- Around line 98-112: The code currently initializes originalFilesize and
convertedFilesize from top-level customMetadata and then adds every entry in
customMetadata.bulk_optimization, but the meta-box renderer replaces the totals
with the bulk sum; change the logic in sidebar-info.js so that when
customMetadata.bulk_optimization is present and non-empty you compute the totals
only from the bulk_optimization entries (do not start from the top-level
fields), i.e. set originalFilesize/convertedFilesize to 0 and sum each
bulk_optimization[ size ].originalFilesize and .convertedFilesize into those
variables so the sidebar totals match class-meta-box.php behavior.
- Around line 132-134: The computed optimizationSavings can produce NaN/Infinity
when byte totals are zero; update the logic in the optimizationSavings
calculation (referencing optimizationSavings, customMetadata.compressionSavings,
originalFilesize, convertedFilesize and any bulk_optimization totals) to first
check for a non-null compressionSavings using nullish coalescing and to ensure
the denominator (originalFilesize or bulk total) is > 0 before doing the
division; if totals are zero or missing, return a safe fallback (e.g., null or
'0.00') instead of performing the calculation so you never produce NaN/Infinity
in the sidebar.

---

Nitpick comments:
In `@src/admin/class-meta-box.php`:
- Around line 101-109: Remove the unused locals $bulk_optimization_sizes and the
unused foreach key variable $size_key to silence PHPMD; delete the line that
sets $bulk_optimization_sizes = array_keys( $cimo['bulk_optimization'] ) and
change the foreach over $cimo['bulk_optimization'] to use a value-only form
(foreach ( $cimo['bulk_optimization'] as $bulk_data ) ) inside the block guarded
by $is_bulk_optimized, leaving the existing $orig, $bulk_original_filesize and
$bulk_converted_filesize logic intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69588b7e-a5dc-4a94-9ccb-392830b42904

📥 Commits

Reviewing files that changed from the base of the PR and between a7762b0 and 69a2fbd.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • src/admin/class-meta-box.php
  • src/admin/css/index.css
  • src/admin/js/media-manager/sidebar-info.js
✅ Files skipped from review due to trivial changes (1)
  • src/admin/css/index.css

Comment on lines +94 to +120
$is_bulk_optimized = isset( $cimo['bulk_optimization'] ) &&
is_array( $cimo['bulk_optimization'] ) &&
! empty( $cimo['bulk_optimization'] );

// For bulk optimization, we need to recalculate the optimization savings percentage
// based on the original and converted file sizes for all the thumbnails
if ( $is_bulk_optimized ) {
$bulk_optimization_count = count( $cimo['bulk_optimization'] );
$bulk_optimization_sizes = array_keys( $cimo['bulk_optimization'] );

// Gather all original and converted file sizes for bulk_optimization
$bulk_original_filesize = 0;
$bulk_converted_filesize = 0;
if ( $is_bulk_optimized ) {
foreach ( $cimo['bulk_optimization'] as $size_key => $bulk_data ) {
$orig = isset( $bulk_data['originalFilesize'] ) ? floatval( $bulk_data['originalFilesize'] ) : 0;
$conv = isset( $bulk_data['convertedFilesize'] ) ? floatval( $bulk_data['convertedFilesize'] ) : 0;
$bulk_original_filesize += $orig;
$bulk_converted_filesize += $conv;
}
}

$original_filesize = $bulk_original_filesize;
$converted_filesize = $bulk_converted_filesize;

$optimization_savings = number_format( 100 * ( $original_filesize - $converted_filesize ) / $original_filesize, 2 );
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Short-circuit the meta box when there are no usable optimization stats.

sidebar-info.js now returns early when compressionSavings is missing and bulk_optimization is absent/empty. This path still renders the full box for that state, which leaves blank/zeroed stats, and the bulk percentage calculation still divides by $original_filesize unconditionally.

Suggested guard
 						$is_bulk_optimized = isset( $cimo['bulk_optimization'] ) &&
 							is_array( $cimo['bulk_optimization'] ) &&
 							! empty( $cimo['bulk_optimization'] );
+
+						if ( $compression_savings === null && ! $is_bulk_optimized ) {
+							echo '<p>' . esc_html__( 'Cimo did not optimize this attachment.', 'cimo-image-optimizer' ) . '</p>';
+							return;
+						}
@@
-							$optimization_savings = number_format( 100 * ( $original_filesize - $converted_filesize ) / $original_filesize, 2 );
+							$optimization_savings = $original_filesize > 0
+								? number_format( 100 * ( $original_filesize - $converted_filesize ) / $original_filesize, 2 )
+								: '0.00';
 						}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$is_bulk_optimized = isset( $cimo['bulk_optimization'] ) &&
is_array( $cimo['bulk_optimization'] ) &&
! empty( $cimo['bulk_optimization'] );
// For bulk optimization, we need to recalculate the optimization savings percentage
// based on the original and converted file sizes for all the thumbnails
if ( $is_bulk_optimized ) {
$bulk_optimization_count = count( $cimo['bulk_optimization'] );
$bulk_optimization_sizes = array_keys( $cimo['bulk_optimization'] );
// Gather all original and converted file sizes for bulk_optimization
$bulk_original_filesize = 0;
$bulk_converted_filesize = 0;
if ( $is_bulk_optimized ) {
foreach ( $cimo['bulk_optimization'] as $size_key => $bulk_data ) {
$orig = isset( $bulk_data['originalFilesize'] ) ? floatval( $bulk_data['originalFilesize'] ) : 0;
$conv = isset( $bulk_data['convertedFilesize'] ) ? floatval( $bulk_data['convertedFilesize'] ) : 0;
$bulk_original_filesize += $orig;
$bulk_converted_filesize += $conv;
}
}
$original_filesize = $bulk_original_filesize;
$converted_filesize = $bulk_converted_filesize;
$optimization_savings = number_format( 100 * ( $original_filesize - $converted_filesize ) / $original_filesize, 2 );
}
$is_bulk_optimized = isset( $cimo['bulk_optimization'] ) &&
is_array( $cimo['bulk_optimization'] ) &&
! empty( $cimo['bulk_optimization'] );
if ( $compression_savings === null && ! $is_bulk_optimized ) {
echo '<p>' . esc_html__( 'Cimo did not optimize this attachment.', 'cimo-image-optimizer' ) . '</p>';
return;
}
// For bulk optimization, we need to recalculate the optimization savings percentage
// based on the original and converted file sizes for all the thumbnails
if ( $is_bulk_optimized ) {
$bulk_optimization_count = count( $cimo['bulk_optimization'] );
$bulk_optimization_sizes = array_keys( $cimo['bulk_optimization'] );
// Gather all original and converted file sizes for bulk_optimization
$bulk_original_filesize = 0;
$bulk_converted_filesize = 0;
if ( $is_bulk_optimized ) {
foreach ( $cimo['bulk_optimization'] as $size_key => $bulk_data ) {
$orig = isset( $bulk_data['originalFilesize'] ) ? floatval( $bulk_data['originalFilesize'] ) : 0;
$conv = isset( $bulk_data['convertedFilesize'] ) ? floatval( $bulk_data['convertedFilesize'] ) : 0;
$bulk_original_filesize += $orig;
$bulk_converted_filesize += $conv;
}
}
$original_filesize = $bulk_original_filesize;
$converted_filesize = $bulk_converted_filesize;
$optimization_savings = $original_filesize > 0
? number_format( 100 * ( $original_filesize - $converted_filesize ) / $original_filesize, 2 )
: '0.00';
}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 102-102: Avoid unused local variables such as '$bulk_optimization_sizes'. (undefined)

(UnusedLocalVariable)


[warning] 108-108: Avoid unused local variables such as '$size_key'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/class-meta-box.php` around lines 94 - 120, Short-circuit rendering
and avoid dividing by zero: detect when the per-image stat compressionSavings is
missing and $is_bulk_optimized is false/empty and return/skip rendering the meta
box (or the stats block) early; when handling $cimo['bulk_optimization'] in the
block around $is_bulk_optimized and the loop that computes
$bulk_original_filesize/$bulk_converted_filesize, add a guard before computing
$optimization_savings to ensure $original_filesize > 0 (otherwise set
$optimization_savings to 0 or skip showing it) and remove the unconditional
division; update logic that sets $original_filesize/$converted_filesize and
$optimization_savings so it only runs when there are usable stats (use
$is_bulk_optimized and nonzero $bulk_original_filesize as conditions).

Comment on lines +98 to +112
let originalFilesize = parseInt( customMetadata.originalFilesize || 0 )
let convertedFilesize = parseInt( customMetadata.convertedFilesize || 0 )

const isBulkOptimized = customMetadata.bulk_optimization &&
typeof customMetadata.bulk_optimization === 'object' &&
! Array.isArray( customMetadata.bulk_optimization ) &&
Object.keys( customMetadata.bulk_optimization ).length > 0

// Bulk optimization keys are the sizes, so we need to sum the original filesizes of the bulk optimization
if ( isBulkOptimized ) {
for ( const size in customMetadata.bulk_optimization ) {
originalFilesize += parseInt( customMetadata.bulk_optimization[ size ].originalFilesize || 0 )
convertedFilesize += parseInt( customMetadata.bulk_optimization[ size ].convertedFilesize || 0 )
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align the bulk filesize aggregation with the meta-box renderer.

This starts from the top-level filesize fields and then adds every bulk_optimization entry. src/admin/class-meta-box.php Line 116 and Line 117 replace the totals with the bulk sum instead, so the same attachment can show different original/optimized sizes depending on which admin screen is open.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/js/media-manager/sidebar-info.js` around lines 98 - 112, The code
currently initializes originalFilesize and convertedFilesize from top-level
customMetadata and then adds every entry in customMetadata.bulk_optimization,
but the meta-box renderer replaces the totals with the bulk sum; change the
logic in sidebar-info.js so that when customMetadata.bulk_optimization is
present and non-empty you compute the totals only from the bulk_optimization
entries (do not start from the top-level fields), i.e. set
originalFilesize/convertedFilesize to 0 and sum each bulk_optimization[ size
].originalFilesize and .convertedFilesize into those variables so the sidebar
totals match class-meta-box.php behavior.

Comment on lines 132 to +134
const optimizationSavings = customMetadata.compressionSavings
? ( 100 - ( customMetadata.compressionSavings * 100 ) ).toFixed( 2 )
: null
: ( 100 * ( originalFilesize - convertedFilesize ) / originalFilesize ).toFixed( 2 )
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard the derived savings percentage when bulk totals are zero.

If bulk_optimization exists but its byte totals are empty/zero, this renders NaN/Infinity in the sidebar. Using a nullish check here also avoids treating a numeric 0 as “missing”.

Suggested guard
-	const optimizationSavings = customMetadata.compressionSavings
-		? ( 100 - ( customMetadata.compressionSavings * 100 ) ).toFixed( 2 )
-		: ( 100 * ( originalFilesize - convertedFilesize ) / originalFilesize ).toFixed( 2 )
+	const optimizationSavings =
+		customMetadata.compressionSavings != null
+			? ( 100 - ( customMetadata.compressionSavings * 100 ) ).toFixed( 2 )
+			: originalFilesize > 0
+				? ( 100 * ( originalFilesize - convertedFilesize ) / originalFilesize ).toFixed( 2 )
+				: '0.00'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/js/media-manager/sidebar-info.js` around lines 132 - 134, The
computed optimizationSavings can produce NaN/Infinity when byte totals are zero;
update the logic in the optimizationSavings calculation (referencing
optimizationSavings, customMetadata.compressionSavings, originalFilesize,
convertedFilesize and any bulk_optimization totals) to first check for a
non-null compressionSavings using nullish coalescing and to ensure the
denominator (originalFilesize or bulk total) is > 0 before doing the division;
if totals are zero or missing, return a safe fallback (e.g., null or '0.00')
instead of performing the calculation so you never produce NaN/Infinity in the
sidebar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Support for Bulk Optimization of Images

1 participant