Skip to content

Commit 4b36224

Browse files
Ability to convert images to next-gen formats (tinify#50)
* Add format options * Add settings * Add output examples * Add mock responses * Add file extension replacement * Add conversion actions to compression clients * Applied conversion * Replace content with picture * Import picture * Add launch json for debugging * Simplify conversion settings * If original is already converted * Show compression details * Add constants * Add conversion to fopen client * reformat * Use constant * delete converted images * Add settings test * Replace labels * Set correct mocks * Format * Add option to settings * Add conversion test case * get mimetype from input file * Change register to register_once * Add test case for convert and not replace * Remove convert attribute * Set callback after convert * Add stream support * Add unit cache to ignore * Format * Add tests for picture * Will return array * Change conditions to use non-constants * Start of integration tests * Support avif responses * Remove replace * Remove replace option * remove replace from integration tests * Change conversion settings * Remove replace from integration test * Remove convert options * Remove convert options * Format * Change documentation * Rename func to better indicate deleting size * Basic conversion test * Improve price explainer * Calculate price for conversion as well * Convert to static methods * Add helpers * Fix build * Convert to static * Add zip step * fopen request should be a get * Format * Zip cmd * Add xdebug for wordpress runner * Use ABS_PATH instead of get_home_dir * Remove get_home_path * Inject allowed domains and base_dir * Fix test * Remove trailing commas * Post unused * Different method for retrieving image url * Turn off conversion in bulk spec * Format style * format * format * Resolve int tests * Change check method * Skip welcome guide * Use get_convert_format_option in get_conversion_enabled * Add settings * Compress and convert in one action * Show conversions in summary * Show statistics and convert button * Show statistics for conversion * Admin styling * Change to public methods * Replace picture by srcset and server side checking * formatting * format: yoda cond * Ensure third param * Use srcset instead of picture element * format * Format settings * Format * Format * Add await to goto * Fix typo * Remove trailing komma * No ?? * Format * Get mimetypes from settings * Double return + test fix * Typos * Change to picture element implementation * Format * Format * Fix integration test * Use array instead of [] * Use statistics for compressed total size * Reduce MR size * Use raw string instead of DOMDocument * Remove debug script * Format * Format * Rename statistic to compressed * Add additiona table cells to match table size * Only show columns when conversion is enabled * Format * Add emailadress to limit reached response * Remove unneeded force click * Check if resize_options has valid values * It is possible to resize on only height or width * Add a more performant way of clearing the media library * Explicitly go to list view for attachments * Null check on row * Enhance conversion settings test utility - Add support for output format selection (smallest, webp, avif) - Remove force: true from checkbox interactions for more reliable tests - Improve setConversionSettings function to handle different conversion formats - Add switch statement to handle output format radio button selection * Update conversion test to use explicit output format - Add output: 'smallest' parameter to setConversionSettings call - Ensures test uses the smallest file format conversion option - Improves test clarity by being explicit about conversion settings * Fix bulk optimization to include images needing conversion - Fix bulk optimization logic to include already compressed images that need conversion - Update available-for-optimization array to consider both compression and conversion needs - Enhance bulk optimization JavaScript to properly display compression and conversion results - Add comprehensive test for conversion-only optimization scenario This ensures that when conversion is enabled, bulk optimization will process: 1. Uncompressed images (compression + conversion) 2. Already compressed images that need conversion (conversion only) Previously, already compressed images were excluded from bulk optimization even when they needed conversion, causing incomplete optimization runs. * Update bulk optimization UI text to reflect optimization instead of compression - Change 'activated for compression' to 'activated for optimization' - Change 'is compressed' to 'is optimized' in size count messages - Better reflects that bulk optimization now handles both compression and conversion - Improves user understanding of the feature scope * Fix bulk optimization logic to handle both compression and conversion - Fix available-for-optimization condition to check both uncompressed and unconverted sizes - Separate uncompressed and unconverted size filtering when conversion is enabled - Ensure compressed images can still be converted when conversion feature is toggled on - Update test to properly verify optimization statistics with conversion scenarios - Add test coverage for compressed images being eligible for conversion This resolves issues where already compressed images weren't being converted when users enabled the conversion feature after initial compression. * Format * Format * Refactor: rename parameter in estimate_cost for clarity Rename 'usage' parameter to 'compressions_used' in estimate_cost method to better reflect its purpose and improve code readability. * Refactor: extract bulk cost estimation logic and add tests - Move Tiny_Picture initialization to init() method for better lifecycle management - Extract get_estimated_bulk_cost() method from render_bulk_optimization_page() to improve code organization and testability - Add comprehensive unit tests for bulk cost estimation scenarios: - Basic cost calculation without conversion - Cost calculation with format conversion enabled - Improve code readability and maintainability * Format * test: enhance TinyPluginTest with comprehensive test coverage - Add tests for plugin initialization and configuration - Improve test structure and assertions - Expand test cases to cover more functionality * feat: add credit usage tracking for bulk optimization cost calculation - Add estimated_credit_use statistic to track compression credits needed - Update cost calculation to use accurate credit estimates instead of simple multiplication - Handle conversion scenarios properly: * Uncompressed images needing compression + conversion = 2 credits each * Compressed images needing only conversion = 1 credit each - Fix null handling for tiny_meta_value in bulk optimization statistics - Update tests to reflect accurate credit calculations and expectations This change provides more precise cost estimates for bulk optimization by tracking actual credit usage based on image compression and conversion states. * Format
1 parent 724cad2 commit 4b36224

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2201
-339
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ node_modules/
1313
/playwright/.cache/
1414
test/integration/.auth/user.json
1515
artifacts/
16+
.phpunit.result.cache

.vscode/launch.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
2-
// Use IntelliSense to learn about possible attributes.
3-
// Hover to view descriptions of existing attributes.
4-
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5-
"version": "0.2.0",
6-
"configurations": [
7-
{
8-
"name": "Listen for XDebug",
9-
"type": "php",
10-
"request": "launch",
11-
"port": 9003
12-
}
13-
]
14-
}
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "XDebug PHPUnit",
9+
"type": "php",
10+
"request": "launch",
11+
"port": 9003
12+
}
13+
]
14+
}

src/class-tiny-bulk-optimization.php

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ public static function get_optimization_statistics( $settings, $result = null )
2727
$stats = array();
2828
$stats['uploaded-images'] = 0;
2929
$stats['optimized-image-sizes'] = 0;
30-
$stats['available-unoptimised-sizes'] = 0;
30+
$stats['available-unoptimized-sizes'] = 0;
3131
$stats['optimized-library-size'] = 0;
3232
$stats['unoptimized-library-size'] = 0;
33+
$stats['estimated_credit_use'] = 0;
3334
$stats['available-for-optimization'] = array();
3435

3536
if ( is_null( $result ) ) {
@@ -99,9 +100,13 @@ private static function wpdb_retrieve_images_and_metadata( $start_id ) {
99100
private static function populate_optimization_statistics( $settings, $result, $stats ) {
100101
$active_sizes = $settings->get_sizes();
101102
$active_tinify_sizes = $settings->get_active_tinify_sizes();
103+
$conversion_enabled = $settings->get_conversion_enabled();
104+
102105
for ( $i = 0; $i < sizeof( $result ); $i++ ) {
103106
$wp_metadata = unserialize( (string) $result[ $i ]['meta_value'] );
104-
$tiny_metadata = unserialize( (string) $result[ $i ]['tiny_meta_value'] );
107+
$tiny_metadata = isset( $result[ $i ]['tiny_meta_value'] ) ?
108+
unserialize( (string) $result[ $i ]['tiny_meta_value'] ) :
109+
array();
105110
if ( ! is_array( $tiny_metadata ) ) {
106111
$tiny_metadata = array();
107112
}
@@ -114,18 +119,35 @@ private static function populate_optimization_statistics( $settings, $result, $s
114119
$active_tinify_sizes
115120
);
116121
$image_stats = $tiny_image->get_statistics( $active_sizes, $active_tinify_sizes );
122+
117123
$stats['uploaded-images']++;
118-
$stats['available-unoptimised-sizes'] += $image_stats['available_unoptimized_sizes'];
119-
$stats['optimized-image-sizes'] += $image_stats['image_sizes_optimized'];
120-
$stats['optimized-library-size'] += $image_stats['optimized_total_size'];
124+
$stats['estimated_credit_use'] += $image_stats['available_uncompressed_sizes'];
125+
if ( $conversion_enabled ) {
126+
$stats['available-unoptimized-sizes'] +=
127+
$image_stats['available_unconverted_sizes'];
128+
$stats['optimized-image-sizes'] +=
129+
$image_stats['image_sizes_converted'];
130+
$stats['estimated_credit_use'] += $image_stats['available_unconverted_sizes'];
131+
} else {
132+
$stats['available-unoptimized-sizes'] +=
133+
$image_stats['available_uncompressed_sizes'];
134+
$stats['optimized-image-sizes'] +=
135+
$image_stats['image_sizes_compressed'];
136+
}
137+
$stats['optimized-library-size'] += $image_stats['compressed_total_size'];
121138
$stats['unoptimized-library-size'] += $image_stats['initial_total_size'];
122-
if ( $image_stats['available_unoptimized_sizes'] > 0 ) {
139+
140+
$has_conversions = $image_stats['available_unconverted_sizes'] > 0;
141+
$has_compressions = $image_stats['available_uncompressed_sizes'] > 0;
142+
$has_optimizations = $has_compressions || ($conversion_enabled && $has_conversions);
143+
if ( $has_optimizations ) {
123144
$stats['available-for-optimization'][] = array(
124145
'ID' => $result[ $i ]['ID'],
125146
'post_title' => $result[ $i ]['post_title'],
126147
);
127148
}
128-
}
149+
}// End for().
150+
129151
return $stats;
130152
}
131153
}

src/class-tiny-compress-client.php

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
}
3030

3131
class Tiny_Compress_Client extends Tiny_Compress {
32+
3233
private $last_error_code = 0;
3334
private $last_message = '';
3435
private $proxy;
@@ -72,7 +73,6 @@ protected function validate() {
7273
$this->set_request_options( \Tinify\Tinify::getClient( \Tinify\Tinify::ANONYMOUS ) );
7374
\Tinify\Tinify::getClient()->request( 'get', '/keys/' . $this->get_key() );
7475
return true;
75-
7676
} catch ( \Tinify\Exception $err ) {
7777
$this->last_error_code = $err->status;
7878

@@ -88,7 +88,7 @@ protected function validate() {
8888
}
8989
}
9090

91-
protected function compress( $input, $resize_opts, $preserve_opts ) {
91+
protected function compress( $input, $resize_opts, $preserve_opts, $convert_opts ) {
9292
try {
9393
$this->last_error_code = 0;
9494
$this->set_request_options( \Tinify\Tinify::getClient() );
@@ -103,25 +103,39 @@ protected function compress( $input, $resize_opts, $preserve_opts ) {
103103
$source = $source->preserve( $preserve_opts );
104104
}
105105

106-
$result = $source->result();
107-
106+
$compress_result = $source->result();
108107
$meta = array(
109108
'input' => array(
110109
'size' => strlen( $input ),
111-
'type' => $result->mediaType(),
110+
'type' => Tiny_Helpers::get_mimetype( $input ),
112111
),
113112
'output' => array(
114-
'size' => $result->size(),
115-
'type' => $result->mediaType(),
116-
'width' => $result->width(),
117-
'height' => $result->height(),
118-
'ratio' => round( $result->size() / strlen( $input ), 4 ),
113+
'size' => $compress_result->size(),
114+
'type' => $compress_result->mediaType(),
115+
'width' => $compress_result->width(),
116+
'height' => $compress_result->height(),
117+
'ratio' => round( $compress_result->size() / strlen( $input ), 4 ),
119118
),
120119
);
121120

122-
$buffer = $result->toBuffer();
123-
return array( $buffer, $meta );
121+
$buffer = $compress_result->toBuffer();
122+
$result = array( $buffer, $meta, null );
123+
124+
if ( isset( $convert_opts['convert'] ) && true == $convert_opts['convert'] ) {
125+
$convert_to = $convert_opts['convert_to'];
126+
$convert_source = $source->convert( array(
127+
'type' => $convert_to,
128+
) );
129+
$convert_result = $convert_source->result();
130+
$meta['convert'] = array(
131+
'type' => $convert_result->mediaType(),
132+
'size' => $convert_result->size(),
133+
);
134+
$convert_buffer = $convert_result->toBuffer();
135+
$result = array( $buffer, $meta, $convert_buffer );
136+
}
124137

138+
return $result;
125139
} catch ( \Tinify\Exception $err ) {
126140
$this->last_error_code = $err->status;
127141

@@ -130,7 +144,7 @@ protected function compress( $input, $resize_opts, $preserve_opts ) {
130144
get_class( $err ),
131145
$err->status
132146
);
133-
}// End try().
147+
} // End try().
134148
}
135149

136150
public function create_key( $email, $options ) {

src/class-tiny-compress-fopen.php

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ protected function validate() {
8282
}
8383
}
8484

85-
protected function compress( $input, $resize_opts, $preserve_opts ) {
85+
protected function compress( $input, $resize_opts, $preserve_opts, $convert_opts ) {
8686
$params = $this->request_options( 'POST', $input );
8787
list($details, $headers, $status_code) = $this->request( $params );
8888

@@ -133,7 +133,7 @@ protected function compress( $input, $resize_opts, $preserve_opts ) {
133133
$meta = array(
134134
'input' => array(
135135
'size' => strlen( $input ),
136-
'type' => $headers['content-type'],
136+
'type' => Tiny_Helpers::get_mimetype( $input ),
137137
),
138138
'output' => array(
139139
'size' => strlen( $output ),
@@ -144,9 +144,36 @@ protected function compress( $input, $resize_opts, $preserve_opts ) {
144144
),
145145
);
146146

147-
return array( $output, $meta );
148-
}
147+
$convert = null;
148+
149+
if ( isset( $convert_opts['convert'] ) && true === $convert_opts['convert'] ) {
150+
$convert_to = $convert_opts['convert_to'];
151+
$convert_params = $this->request_options(
152+
'POST',
153+
array(
154+
'convert' => array(
155+
'type' => $convert_to,
156+
),
157+
),
158+
array( 'Content-Type: application/json' )
159+
);
160+
161+
list($convert_output, $convert_headers) = $this->request(
162+
$convert_params,
163+
$output_url
164+
);
165+
$meta['convert'] = array(
166+
'type' => $convert_headers['content-type'],
167+
'size' => strlen( $convert_output ),
168+
);
169+
$convert = $convert_output;
149170

171+
}
172+
173+
$result = array( $output, $meta, $convert );
174+
175+
return $result;
176+
}
150177
private function request( $params, $url = Tiny_Config::SHRINK_URL ) {
151178
$context = stream_context_create( $params );
152179
$request = fopen( $url, 'rb', false, $context );
@@ -188,8 +215,10 @@ private function request( $params, $url = Tiny_Config::SHRINK_URL ) {
188215
$response = stream_get_contents( $request );
189216
fclose( $request );
190217

191-
if ( isset( $headers['content-type'] ) &&
192-
substr( 'application/json' == $headers['content-type'], 0, 16 ) ) {
218+
if (
219+
isset( $headers['content-type'] ) &&
220+
substr( 'application/json' == $headers['content-type'], 0, 16 )
221+
) {
193222
$response = $this->decode( $response );
194223
}
195224

@@ -221,11 +250,11 @@ private function request_options( $method, $body = null, $headers = array() ) {
221250
return array(
222251
'http' => array(
223252
'method' => $method,
224-
'header' => array_merge( $headers, array(
253+
'header' => array_merge($headers, array(
225254
'Authorization: Basic ' . base64_encode( 'api:' . $this->api_key ),
226255
'User-Agent: ' . self::identifier(),
227256
'Content-Type: multipart/form-data',
228-
) ),
257+
)),
229258
'content' => $body,
230259
'follow_location' => 0,
231260
'max_redirects' => 1, // Necessary for PHP 5.2

src/class-tiny-compress.php

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*/
2020

2121
abstract class Tiny_Compress {
22+
2223
const KEY_MISSING = 'Register an account or provide an API key first';
2324
const FILE_MISSING = 'File does not exist';
2425
const WRITE_ERROR = 'No permission to write to file';
@@ -40,10 +41,10 @@ public static function create( $api_key, $after_compress_callback = null ) {
4041
}
4142

4243
/* Based on pricing April 2016. */
43-
public static function estimate_cost( $compressions, $usage ) {
44+
public static function estimate_cost( $compressions, $compressions_used ) {
4445
return round(
45-
self::compression_cost( $compressions + $usage ) -
46-
self::compression_cost( $usage ),
46+
self::compression_cost( $compressions + $compressions_used ) -
47+
self::compression_cost( $compressions_used ),
4748
2
4849
);
4950
}
@@ -80,7 +81,7 @@ public function get_status() {
8081
if ( $err->get_status() == 404 ) {
8182
$message = 'The key that you have entered is not valid';
8283
} else {
83-
list( $message ) = explode( ' (HTTP', $err->getMessage(), 2 );
84+
list($message) = explode( ' (HTTP', $err->getMessage(), 2 );
8485
}
8586
}
8687

@@ -92,7 +93,21 @@ public function get_status() {
9293
);
9394
}
9495

95-
public function compress_file( $file, $resize_opts = array(), $preserve_opts = array() ) {
96+
/**
97+
* Compresses a single file
98+
*
99+
* @param [type] $file
100+
* @param array $resize_opts
101+
* @param array $preserve_opts
102+
* @param array{ convert: bool, convert_to: string } conversion options
103+
* @return void
104+
*/
105+
public function compress_file(
106+
$file,
107+
$resize_opts = array(),
108+
$preserve_opts = array(),
109+
$convert_opts = array()
110+
) {
96111
if ( $this->get_key() == null ) {
97112
throw new Tiny_Exception( self::KEY_MISSING, 'KeyError' );
98113
}
@@ -110,28 +125,55 @@ public function compress_file( $file, $resize_opts = array(), $preserve_opts = a
110125
}
111126

112127
try {
113-
list( $output, $details ) = $this->compress(
114-
file_get_contents( $file ),
128+
$file_data = file_get_contents( $file );
129+
130+
list($output, $details, $convert_output ) = $this->compress(
131+
$file_data,
115132
$resize_opts,
116-
$preserve_opts
133+
$preserve_opts,
134+
$convert_opts
117135
);
118136
} catch ( Tiny_Exception $err ) {
119137
$this->call_after_compress_callback();
120138
throw $err;
121139
}
122140

123-
$this->call_after_compress_callback();
124-
file_put_contents( $file, $output );
141+
try {
142+
file_put_contents( $file, $output );
143+
} catch ( Exception $e ) {
144+
throw new Tiny_Exception( $e->getMessage(), 'FileError' );
145+
}
146+
147+
if ( $convert_output ) {
148+
$converted_filepath = Tiny_Helpers::replace_file_extension(
149+
$details['convert']['type'],
150+
$file
151+
);
152+
153+
try {
154+
file_put_contents( $converted_filepath, $convert_output );
155+
} catch ( Exception $e ) {
156+
throw new Tiny_Exception( $e->getMessage(), 'FileError' );
157+
}
158+
$details['convert']['path'] = $converted_filepath;
159+
}
125160

126161
if ( $resize_opts ) {
127162
$details['output']['resized'] = true;
128163
}
129164

165+
$this->call_after_compress_callback();
166+
130167
return $details;
131168
}
132169

133170
protected abstract function validate();
134-
protected abstract function compress( $input, $resize_options, $preserve_options );
171+
protected abstract function compress(
172+
$input,
173+
$resize_options,
174+
$preserve_options,
175+
$convert_opts
176+
);
135177

136178
protected static function identifier() {
137179
return 'WordPress/' . Tiny_Plugin::wp_version() . ' Plugin/' . Tiny_Plugin::version();
@@ -150,7 +192,12 @@ private static function needs_resize( $file, $resize_options ) {
150192

151193
list($width, $height) = getimagesize( $file );
152194

153-
return ( $width > $resize_options['width'] || $height > $resize_options['height'] );
195+
$should_resize_width = isset( $resize_options['width'] ) &&
196+
$width > $resize_options['width'];
197+
$should_resize_height = isset( $resize_options['height'] ) &&
198+
$height > $resize_options['height'];
199+
200+
return $should_resize_width || $should_resize_height;
154201
}
155202

156203
private static function compression_cost( $total ) {

0 commit comments

Comments
 (0)