Skip to content
8 changes: 8 additions & 0 deletions src/admin/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ public function register_settings() {
],

// Image Optimization settings
'smart_optimization' => [
'type' => 'integer',
],
'webp_quality' => [
'type' => 'integer',
],
Expand Down Expand Up @@ -322,6 +325,11 @@ public function sanitize_options( $options ) {
$sanitized['thumbnail_sizes'] = array_map( 'sanitize_text_field', $options['thumbnail_sizes'] );
}

// Sanitize smart optimization
if ( isset( $options['smart_optimization'] ) ) {
$sanitized['smart_optimization'] = $options['smart_optimization'] ? 1 : 0;
}

// Sanitize webp quality
if ( isset( $options['webp_quality'] ) ) {
$quality = absint( $options['webp_quality'] );
Expand Down
19 changes: 14 additions & 5 deletions src/admin/class-meta-box.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,20 @@ function cimo_get_media_type_label( $mimetype ) {

// Converted format
echo '<li class="cimo-converted">';
echo '🏞️ ' . sprintf(
/* translators: %s: converted format */
esc_html__( 'Converted to %s', 'cimo-image-optimizer' ),
'<span class="cimo-value">' . esc_html( $converted_format ) . '</span>'
);
$converted_format_markup = '<span class="cimo-value">' . esc_html( $converted_format ) . '</span>';
if ( ! empty( $cimo['smartOptimized'] ) ) {
echo '🏞️ ' . sprintf(
/* translators: %s: converted format */
esc_html__( 'Smart optimized to %s', 'cimo-image-optimizer' ),
$converted_format_markup
);
} else {
echo '🏞️ ' . sprintf(
/* translators: %s: converted format */
esc_html__( 'Converted to %s', 'cimo-image-optimizer' ),
$converted_format_markup
);
}
echo '</li>';

// Conversion time
Expand Down
7 changes: 7 additions & 0 deletions src/admin/class-metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function register_rest_route() {
'convertedFilesize',
'conversionTime',
'compressionSavings',
'smartOptimized',
];
if ( ! is_array( $value ) ) {
// translators: The %s is the parameter name.
Expand Down Expand Up @@ -85,6 +86,7 @@ public function register_rest_route() {
'convertedFilesize',
'conversionTime',
'compressionSavings',
'smartOptimized',
];
$sanitized = [];
if ( is_array( $value ) ) {
Expand All @@ -98,6 +100,8 @@ public function register_rest_route() {
$entry[ $key ] = intval( $item[ $key ] );
} elseif ( in_array( $key, [ 'conversionTime', 'compressionSavings' ], true ) ) {
$entry[ $key ] = floatval( $item[ $key ] );
} elseif ( $key === 'smartOptimized' ) {
$entry[ $key ] = ! empty( $item[ $key ] ) ? 1 : 0;
} else {
$entry[ $key ] = sanitize_text_field( $item[ $key ] );
}
Expand Down Expand Up @@ -137,6 +141,7 @@ public function save_metadata( $request ) {
'convertedFilesize',
'conversionTime',
'compressionSavings',
'smartOptimized',
];
$sanitized_metadata = [];
foreach ( $metadata_array as $item ) {
Expand All @@ -149,6 +154,8 @@ public function save_metadata( $request ) {
$entry[ $key ] = intval( $item[ $key ] );
} elseif ( in_array( $key, [ 'conversionTime', 'compressionSavings' ], true ) ) {
$entry[ $key ] = floatval( $item[ $key ] );
} elseif ( $key === 'smartOptimized' ) {
$entry[ $key ] = ! empty( $item[ $key ] ) ? 1 : 0;
} else {
$entry[ $key ] = sanitize_text_field( $item[ $key ] );
}
Expand Down
3 changes: 3 additions & 0 deletions src/admin/class-script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ public static function enqueue_cimo_assets() {
'canManageOptions' => current_user_can( 'manage_options' ),
'optimizeAllMedia' => isset( $settings['optimize_all_media'] ) ? (int) $settings['optimize_all_media'] : 0,
'isPremium' => CIMO_BUILD === 'premium',
'smartOptimization' => CIMO_BUILD === 'premium'
? ( isset( $settings['smart_optimization'] ) ? (int) $settings['smart_optimization'] : 1 )
: 0,
'webpQuality' => ! empty( $settings['webp_quality'] ) ? (int) $settings['webp_quality'] : 80,
'maxImageDimension' => ! empty( $settings['max_image_dimension'] ) ? (int) $settings['max_image_dimension'] : 0,
'videoOptimizationEnabled' => isset( $settings['video_optimization_enabled'] ) ? (int) $settings['video_optimization_enabled'] : 1,
Expand Down
20 changes: 20 additions & 0 deletions src/admin/css/admin-page.css
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,26 @@
order: 1;
}

.cimo-smart-optimization-toggle {
order: 2;

~ .cimo-setting-field {
order: 3;
}

~ .cimo-webp-quality-range-control {
order: 1;
}

~ .cimo-reset-button {
order: 10;
}
}

.cimo-is-premium .cimo-smart-optimization-toggle {
order: 1;
}

@keyframes cimo-bulk-optimizer-blink {

0%, 100% { opacity: 1; }
Expand Down
2 changes: 1 addition & 1 deletion src/admin/js/media-manager/drop-zone.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function addDropZoneListenerToMediaManager( targetDocument ) {
const optimizedResults = await Promise.all(
fileConverters.map( async converter => {
try {
const result = await converter.convert()
const result = await converter.optimize()
if ( result.error ) {
// eslint-disable-next-line no-console
console.warn( result.error )
Expand Down
2 changes: 1 addition & 1 deletion src/admin/js/media-manager/select-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function addSelectFilesListenerToFileUploads( targetDocument ) {
const optimizedResults = await Promise.all(
fileConverters.map( async converter => {
try {
const result = await converter.convert()
const result = await converter.optimize()
if ( result.error ) {
// eslint-disable-next-line no-console
console.warn( result.error )
Expand Down
16 changes: 15 additions & 1 deletion src/admin/js/media-manager/sidebar-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,23 @@ function injectCimoMetadata( {
}

if ( ! isBulkOptimized ) {
const formatLabel = convertMimetypeToFormat( customMetadata.convertedFormat )
const convertedFormatSpan = `<span class="cimo-value">${ escape( formatLabel ) }</span>`
const convertedLineText = customMetadata.smartOptimized
? sprintf(
/* translators: %s: image format name (e.g. WebP) */
__( 'Smart optimized to %s', 'cimo-image-optimizer' ),
convertedFormatSpan
)
: sprintf(
/* translators: %s: image format name (e.g. WebP) */
__( 'Converted to %s', 'cimo-image-optimizer' ),
convertedFormatSpan
)

html += `
<li class="cimo-converted">
🏞️ Converted to <span class="cimo-value">${ escape( convertMimetypeToFormat( customMetadata.convertedFormat ) ) }</span>
🏞️ ${ convertedLineText }
</li>
`

Expand Down
56 changes: 43 additions & 13 deletions src/admin/js/page/admin-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const AdminSettings = () => {
thumbnailSizes: [], // Stores DISABLED thumbnail sizes

// Image optimization settings
smartOptimization: 1,
webpQuality: 80,
maxImageDimension: '',

Expand Down Expand Up @@ -90,6 +91,7 @@ const AdminSettings = () => {
thumbnailSizes: cimoOptions.thumbnail_sizes || [],

// Image Optimization settings
smartOptimization: cimoOptions.smart_optimization !== undefined ? cimoOptions.smart_optimization : 1,
webpQuality: cimoOptions.webp_quality !== undefined ? cimoOptions.webp_quality : 80,
maxImageDimension: cimoOptions.max_image_dimension || '',

Expand Down Expand Up @@ -179,6 +181,7 @@ const AdminSettings = () => {
setSettings( settings => {
return {
...settings,
smartOptimization: 1,
webpQuality: 80,
maxImageDimension: 1920,
}
Expand All @@ -189,6 +192,7 @@ const AdminSettings = () => {
setSettings( settings => {
return {
...settings,
smartOptimization: 1,
webpQuality: '',
maxImageDimension: '',
}
Expand Down Expand Up @@ -295,6 +299,7 @@ const AdminSettings = () => {
thumbnail_sizes: settings.thumbnailSizes,

// Image Optimization settings
smart_optimization: settings.smartOptimization,
webp_quality: parseInt( settings.webpQuality ) || 0,
Comment on lines +302 to 303
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Free mode still allows a non-static image quality.

Line 303 saves whatever webpQuality is in state, and Line 600 still renders an editable slider for free users. Issue #35 says free uploads should stay on a fixed 80% quality, so this either violates the spec or exposes a control the runtime is supposed to ignore. Lock free saves to 80 and make the free UI read-only or hide it.

🔧 Possible fix
 						// Image Optimization settings
 						smart_optimization: settings.smartOptimization,
-						webp_quality: parseInt( settings.webpQuality ) || 0,
+						webp_quality: buildType === 'free' ? 80 : ( parseInt( settings.webpQuality, 10 ) || 0 ),
 						max_image_dimension: parseInt( settings.maxImageDimension ) || 0,
@@
-						{ ( buildType === 'free' || settings.smartOptimization === 0 ) && (
+						{ buildType === 'free' && (
+							<div className="cimo-setting-field cimo-webp-quality-range-control">
+								<RangeControl
+									id="webpQuality"
+									label={ __( 'WebP Image Quality', 'cimo-image-optimizer' ) }
+									value={ 80 }
+									min="1"
+									max="100"
+									step="1"
+									disabled
+									__next40pxDefaultSize
+									help={ __( 'Free builds use a fixed 80% WebP quality.', 'cimo-image-optimizer' ) }
+								/>
+							</div>
+						) }
+						{ buildType !== 'free' && settings.smartOptimization === 0 && (
 							<div className="cimo-setting-field cimo-webp-quality-range-control">
 								<RangeControl
 									id="webpQuality"

Also applies to: 600-614

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

In `@src/admin/js/page/admin-settings.js` around lines 302 - 303, The code
currently writes webp_quality from settings.webpQuality and still renders an
editable slider for non-paid users; change the save logic so that when the
current account is on free tier the saved webp_quality is forced to 80 (e.g.,
replace writing webp_quality: parseInt(settings.webpQuality) || 0 with
conditional logic that sets webp_quality = 80 for free users, otherwise uses
parseInt(settings.webpQuality)), and update the settings UI rendering (the
component that renders the webp quality slider) to be read-only or hidden for
free users so the control is not editable—use the existing user-tier flag/check
in the code to gate both the save in the block that sets
smart_optimization/webp_quality and the slider rendering.

max_image_dimension: parseInt( settings.maxImageDimension ) || 0,

Expand Down Expand Up @@ -570,21 +575,46 @@ const AdminSettings = () => {
</Button>
</div>

<div className="cimo-setting-field">
<RangeControl
id="webpQuality"
label={ __( 'WebP Image Quality', 'cimo-image-optimizer' ) }
value={ settings.webpQuality || '' }
onChange={ value => handleInputChange( 'webpQuality', value || '' ) }
min="1"
max="100"
step="1"
__next40pxDefaultSize
allowReset
initialPosition={ 80 }
help={ __( 'Set the quality / compression level for generated .webp images. Default is 80%. Higher values mean better quality and larger file size; lower values reduce file size with more compression but lower quality.', 'cimo-image-optimizer' ) }
{ /* Smart Optimization */ }
<div className="cimo-setting-field cimo-smart-optimization-toggle">
<ToggleControl
__nextHasNoMarginBottom
label={
<span>
{ __( 'Smart Optimization', 'cimo-image-optimizer' ) }
{ buildType === 'free' && (
<span className="cimo-premium-tag">
{ __( 'Premium', 'cimo-image-optimizer' ) }
</span>
) }
</span>
}
checked={ buildType === 'free' ? false : ( settings.smartOptimization === 1 ) }
disabled={ buildType === 'free' }
onChange={ checked => handleInputChange( 'smartOptimization', checked ? 1 : 0 ) }
help={ __( 'Smart Optimization uses our advanced algorithms to choose the best compression and quality settings per image. This adds a small overhead to the upload process, but the results are even smaller file sizes and faster loading times.', 'cimo-image-optimizer' ) }
/>
</div>

{ /* WebP Image Quality */ }
{ ( buildType === 'free' || settings.smartOptimization === 0 ) && (
<div className="cimo-setting-field cimo-webp-quality-range-control">
<RangeControl
id="webpQuality"
label={ __( 'WebP Image Quality', 'cimo-image-optimizer' ) }
value={ settings.webpQuality || '' }
onChange={ value => handleInputChange( 'webpQuality', value || '' ) }
min="1"
max="100"
step="1"
__next40pxDefaultSize
allowReset
initialPosition={ 80 }
help={ __( 'Set the quality / compression level for generated .webp images. Default is 80%. Higher values mean better quality and larger file size; lower values reduce file size with more compression but lower quality.', 'cimo-image-optimizer' ) }
/>
</div>
) }

{ /* Maximum Image Dimension */ }
<div className="cimo-setting-field">
<TextControl
Expand Down
9 changes: 9 additions & 0 deletions src/shared/converters/converter-abstract.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ class Converter {
throw new Error( 'convert() must be implemented by subclass' )
}

/**
* Perform smart optimization.
* If a subclass has not implemented this method, perform regular conversion.
* @return {Promise<{file: File|Blob, metadata?: Object}>} Promise resolving with the converted file and optional metadata.
*/
async optimize() {
return await this.convert()
}

/**
* Cancel the current conversion.
* Subclasses should override this to implement actual cancellation logic.
Expand Down
39 changes: 27 additions & 12 deletions src/shared/converters/image-converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,10 @@ class ImageConverter extends Converter {

/**
* Convert an image file to the desired format and options.
* @param {boolean} [force=false] - Force conversion even if the file is already in the desired format.
* @param {Object} [options] - Options for the conversion.
* @param {Object} [options] - Options for the conversion.
* @return {Promise<{file: File|Blob, metadata?: Object}>} Promise resolving to the converted file and optional metadata.
*/
async convert( force = false, options = {} ) {
async convert( options = {} ) {
const file = this.file
const {
quality = this.options?.quality || 0.8,
Expand All @@ -239,15 +238,7 @@ class ImageConverter extends Converter {
}
}

// Skip if already the desired format
const formatInfo = supportedFormats.find( f => f.value === formatTo )
if ( ! force && formatInfo && file.type === formatInfo.mimeType ) {
return {
file,
metadata: null,
reason: 'same-format',
}
}

// Check if the browser supports the desired output format
const testCanvas = document.createElement( 'canvas' )
Expand Down Expand Up @@ -359,7 +350,31 @@ class ImageConverter extends Converter {
}

async optimize() {
return await applyFiltersAsync( 'cimo.imageConverter.optimize', this )
const smartOptimization =
Boolean( window.cimoSettings?.isPremium ) &&
String( window.cimoSettings?.smartOptimization ?? '1' ) !== '0'
let result = null

if ( smartOptimization ) {
result = await applyFiltersAsync( 'cimo.imageConverter.optimize', {
file: this.file,
metadata: null,
reason: 'no-optimizer',
}, this )
}

if ( ! result || result.reason === 'no-optimizer' ) {
result = await this.convert()
} else if ( result.metadata && typeof result.metadata === 'object' ) {
result = {
...result,
metadata: {
...result.metadata,
smartOptimized: 1,
},
}
}
return result
}
}

Expand Down
11 changes: 7 additions & 4 deletions src/shared/converters/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ export const getFileConverter = _file => {
}

if ( file.type.startsWith( 'image/' ) ) {
// If the browser doesn't support webp, then we can't convert it.
if ( ! isFormatSupported( 'webp' ) ) {
return new NullConverter( file )
let format = 'webp'

// If webp is not supported, use the same format of the file.
if ( ! isFormatSupported( format ) ) {
format = file.type
}

if ( ImageConverter.supportsMimeType( file.type ) ) {
return new ImageConverter( file, {
format: 'webp',
format,
quality: window.cimoSettings?.webpQuality || 0.8,
maxDimension: window.cimoSettings?.maxImageDimension || 0,
} )
Expand Down
Loading