diff --git a/src/admin/class-admin.php b/src/admin/class-admin.php index eb584e0..04f711f 100644 --- a/src/admin/class-admin.php +++ b/src/admin/class-admin.php @@ -137,6 +137,9 @@ public function register_settings() { ], // Image Optimization settings + 'smart_optimization' => [ + 'type' => 'integer', + ], 'webp_quality' => [ 'type' => 'integer', ], @@ -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'] ); diff --git a/src/admin/class-meta-box.php b/src/admin/class-meta-box.php index ef40c66..74caa50 100644 --- a/src/admin/class-meta-box.php +++ b/src/admin/class-meta-box.php @@ -185,11 +185,20 @@ function cimo_get_media_type_label( $mimetype ) { // Converted format echo '
  • '; - echo '🏞️ ' . sprintf( - /* translators: %s: converted format */ - esc_html__( 'Converted to %s', 'cimo-image-optimizer' ), - '' . esc_html( $converted_format ) . '' - ); + $converted_format_markup = '' . esc_html( $converted_format ) . ''; + 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 '
  • '; // Conversion time diff --git a/src/admin/class-metadata.php b/src/admin/class-metadata.php index da6932a..bb96be0 100644 --- a/src/admin/class-metadata.php +++ b/src/admin/class-metadata.php @@ -46,6 +46,7 @@ public function register_rest_route() { 'convertedFilesize', 'conversionTime', 'compressionSavings', + 'smartOptimized', ]; if ( ! is_array( $value ) ) { // translators: The %s is the parameter name. @@ -85,6 +86,7 @@ public function register_rest_route() { 'convertedFilesize', 'conversionTime', 'compressionSavings', + 'smartOptimized', ]; $sanitized = []; if ( is_array( $value ) ) { @@ -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 ] ); } @@ -137,6 +141,7 @@ public function save_metadata( $request ) { 'convertedFilesize', 'conversionTime', 'compressionSavings', + 'smartOptimized', ]; $sanitized_metadata = []; foreach ( $metadata_array as $item ) { @@ -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 ] ); } diff --git a/src/admin/class-script-loader.php b/src/admin/class-script-loader.php index 8b67131..499c425 100644 --- a/src/admin/class-script-loader.php +++ b/src/admin/class-script-loader.php @@ -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, diff --git a/src/admin/css/admin-page.css b/src/admin/css/admin-page.css index 85bfdf5..c6a663b 100644 --- a/src/admin/css/admin-page.css +++ b/src/admin/css/admin-page.css @@ -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; } diff --git a/src/admin/js/media-manager/drop-zone.js b/src/admin/js/media-manager/drop-zone.js index 57cee8b..679520f 100644 --- a/src/admin/js/media-manager/drop-zone.js +++ b/src/admin/js/media-manager/drop-zone.js @@ -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 ) diff --git a/src/admin/js/media-manager/select-files.js b/src/admin/js/media-manager/select-files.js index e941600..5860907 100644 --- a/src/admin/js/media-manager/select-files.js +++ b/src/admin/js/media-manager/select-files.js @@ -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 ) diff --git a/src/admin/js/media-manager/sidebar-info.js b/src/admin/js/media-manager/sidebar-info.js index 65ca224..0d7d6bf 100644 --- a/src/admin/js/media-manager/sidebar-info.js +++ b/src/admin/js/media-manager/sidebar-info.js @@ -194,9 +194,23 @@ function injectCimoMetadata( { } if ( ! isBulkOptimized ) { + const formatLabel = convertMimetypeToFormat( customMetadata.convertedFormat ) + const convertedFormatSpan = `${ escape( formatLabel ) }` + 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 += `
  • - 🏞️ Converted to ${ escape( convertMimetypeToFormat( customMetadata.convertedFormat ) ) } + 🏞️ ${ convertedLineText }
  • ` diff --git a/src/admin/js/page/admin-settings.js b/src/admin/js/page/admin-settings.js index 270f000..06aefd6 100644 --- a/src/admin/js/page/admin-settings.js +++ b/src/admin/js/page/admin-settings.js @@ -24,6 +24,7 @@ const AdminSettings = () => { thumbnailSizes: [], // Stores DISABLED thumbnail sizes // Image optimization settings + smartOptimization: 1, webpQuality: 80, maxImageDimension: '', @@ -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 || '', @@ -179,6 +181,7 @@ const AdminSettings = () => { setSettings( settings => { return { ...settings, + smartOptimization: 1, webpQuality: 80, maxImageDimension: 1920, } @@ -189,6 +192,7 @@ const AdminSettings = () => { setSettings( settings => { return { ...settings, + smartOptimization: 1, webpQuality: '', maxImageDimension: '', } @@ -295,6 +299,7 @@ const AdminSettings = () => { thumbnail_sizes: settings.thumbnailSizes, // Image Optimization settings + smart_optimization: settings.smartOptimization, webp_quality: parseInt( settings.webpQuality ) || 0, max_image_dimension: parseInt( settings.maxImageDimension ) || 0, @@ -570,21 +575,46 @@ const AdminSettings = () => { -
    - 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 */ } +
    + + { __( 'Smart Optimization', 'cimo-image-optimizer' ) } + { buildType === 'free' && ( + + { __( 'Premium', 'cimo-image-optimizer' ) } + + ) } + + } + 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' ) } />
    + + { /* WebP Image Quality */ } + { ( buildType === 'free' || settings.smartOptimization === 0 ) && ( +
    + 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' ) } + /> +
    + ) } + { /* Maximum Image Dimension */ }
    } 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. diff --git a/src/shared/converters/image-converter.js b/src/shared/converters/image-converter.js index 13a838f..04d5839 100644 --- a/src/shared/converters/image-converter.js +++ b/src/shared/converters/image-converter.js @@ -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, @@ -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' ) @@ -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 } } diff --git a/src/shared/converters/index.js b/src/shared/converters/index.js index 7a58c50..74fa862 100644 --- a/src/shared/converters/index.js +++ b/src/shared/converters/index.js @@ -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, } )