Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-or-later
3 : *
4 : * This file is part of colopresso
5 : *
6 : * Copyright (C) 2025-2026 COLOPL, Inc.
7 : *
8 : * Author: Go Kudo <g-kudo@colopl.co.jp>
9 : * Developed with AI (LLM) code assistance. See `NOTICE` for details.
10 : */
11 :
12 : #include <stdbool.h>
13 : #include <stdio.h>
14 : #include <stdlib.h>
15 : #include <string.h>
16 :
17 : #include <avif/avif.h>
18 : #include <png.h>
19 : #include <webp/encode.h>
20 :
21 : #include <colopresso.h>
22 : #include <colopresso/portable.h>
23 :
24 : #include "internal/log.h"
25 : #include "internal/png.h"
26 :
27 : #include "internal/avif.h"
28 : #include "internal/pngx.h"
29 : #include "internal/webp.h"
30 :
31 141 : extern void cpres_config_init_defaults(cpres_config_t *config) {
32 141 : if (!config) {
33 1 : return;
34 : }
35 :
36 140 : memset(config, 0, sizeof(*config));
37 :
38 140 : config->webp_quality = COLOPRESSO_WEBP_DEFAULT_QUALITY;
39 140 : config->webp_lossless = COLOPRESSO_WEBP_DEFAULT_LOSSLESS;
40 140 : config->webp_method = COLOPRESSO_WEBP_DEFAULT_METHOD;
41 140 : config->webp_target_size = COLOPRESSO_WEBP_DEFAULT_TARGET_SIZE;
42 140 : config->webp_target_psnr = COLOPRESSO_WEBP_DEFAULT_TARGET_PSNR;
43 140 : config->webp_segments = COLOPRESSO_WEBP_DEFAULT_SEGMENTS;
44 140 : config->webp_sns_strength = COLOPRESSO_WEBP_DEFAULT_SNS_STRENGTH;
45 140 : config->webp_filter_strength = COLOPRESSO_WEBP_DEFAULT_FILTER_STRENGTH;
46 140 : config->webp_filter_sharpness = COLOPRESSO_WEBP_DEFAULT_FILTER_SHARPNESS;
47 140 : config->webp_filter_type = COLOPRESSO_WEBP_DEFAULT_FILTER_TYPE;
48 140 : config->webp_autofilter = COLOPRESSO_WEBP_DEFAULT_AUTOFILTER;
49 140 : config->webp_alpha_compression = COLOPRESSO_WEBP_DEFAULT_ALPHA_COMPRESSION;
50 140 : config->webp_alpha_filtering = COLOPRESSO_WEBP_DEFAULT_ALPHA_FILTERING;
51 140 : config->webp_alpha_quality = COLOPRESSO_WEBP_DEFAULT_ALPHA_QUALITY;
52 140 : config->webp_pass = COLOPRESSO_WEBP_DEFAULT_PASS;
53 140 : config->webp_preprocessing = COLOPRESSO_WEBP_DEFAULT_PREPROCESSING;
54 140 : config->webp_partitions = COLOPRESSO_WEBP_DEFAULT_PARTITIONS;
55 140 : config->webp_partition_limit = COLOPRESSO_WEBP_DEFAULT_PARTITION_LIMIT;
56 140 : config->webp_emulate_jpeg_size = COLOPRESSO_WEBP_DEFAULT_EMULATE_JPEG_SIZE;
57 140 : config->webp_thread_level = COLOPRESSO_WEBP_DEFAULT_THREAD_LEVEL;
58 140 : config->webp_low_memory = COLOPRESSO_WEBP_DEFAULT_LOW_MEMORY;
59 140 : config->webp_near_lossless = COLOPRESSO_WEBP_DEFAULT_NEAR_LOSSLESS;
60 140 : config->webp_exact = COLOPRESSO_WEBP_DEFAULT_EXACT;
61 140 : config->webp_use_delta_palette = COLOPRESSO_WEBP_DEFAULT_USE_DELTA_PALETTE;
62 140 : config->webp_use_sharp_yuv = COLOPRESSO_WEBP_DEFAULT_USE_SHARP_YUV;
63 :
64 140 : config->avif_quality = COLOPRESSO_AVIF_DEFAULT_QUALITY;
65 140 : config->avif_alpha_quality = COLOPRESSO_AVIF_DEFAULT_ALPHA_QUALITY;
66 140 : config->avif_lossless = COLOPRESSO_AVIF_DEFAULT_LOSSLESS;
67 140 : config->avif_speed = COLOPRESSO_AVIF_DEFAULT_SPEED;
68 140 : config->avif_threads = COLOPRESSO_AVIF_DEFAULT_THREADS;
69 :
70 140 : config->pngx_level = COLOPRESSO_PNGX_DEFAULT_LEVEL;
71 140 : config->pngx_strip_safe = COLOPRESSO_PNGX_DEFAULT_STRIP_SAFE;
72 140 : config->pngx_optimize_alpha = COLOPRESSO_PNGX_DEFAULT_OPTIMIZE_ALPHA;
73 140 : config->pngx_lossy_enable = COLOPRESSO_PNGX_DEFAULT_LOSSY_ENABLE;
74 140 : config->pngx_lossy_type = COLOPRESSO_PNGX_DEFAULT_LOSSY_TYPE;
75 140 : config->pngx_lossy_max_colors = COLOPRESSO_PNGX_DEFAULT_LOSSY_MAX_COLORS;
76 140 : config->pngx_lossy_reduced_colors = COLOPRESSO_PNGX_DEFAULT_REDUCED_COLORS;
77 140 : config->pngx_lossy_reduced_bits_rgb = COLOPRESSO_PNGX_DEFAULT_REDUCED_BITS_RGB;
78 140 : config->pngx_lossy_reduced_alpha_bits = COLOPRESSO_PNGX_DEFAULT_REDUCED_ALPHA_BITS;
79 140 : config->pngx_lossy_quality_min = COLOPRESSO_PNGX_DEFAULT_LOSSY_QUALITY_MIN;
80 140 : config->pngx_lossy_quality_max = COLOPRESSO_PNGX_DEFAULT_LOSSY_QUALITY_MAX;
81 140 : config->pngx_lossy_speed = COLOPRESSO_PNGX_DEFAULT_LOSSY_SPEED;
82 140 : config->pngx_lossy_dither_level = COLOPRESSO_PNGX_DEFAULT_LOSSY_DITHER_LEVEL;
83 140 : config->pngx_saliency_map_enable = COLOPRESSO_PNGX_DEFAULT_SALIENCY_MAP_ENABLE;
84 140 : config->pngx_chroma_anchor_enable = COLOPRESSO_PNGX_DEFAULT_CHROMA_ANCHOR_ENABLE;
85 140 : config->pngx_adaptive_dither_enable = COLOPRESSO_PNGX_DEFAULT_ADAPTIVE_DITHER_ENABLE;
86 140 : config->pngx_gradient_boost_enable = COLOPRESSO_PNGX_DEFAULT_GRADIENT_BOOST_ENABLE;
87 140 : config->pngx_chroma_weight_enable = COLOPRESSO_PNGX_DEFAULT_CHROMA_WEIGHT_ENABLE;
88 140 : config->pngx_postprocess_smooth_enable = COLOPRESSO_PNGX_DEFAULT_POSTPROCESS_SMOOTH_ENABLE;
89 140 : config->pngx_postprocess_smooth_importance_cutoff = COLOPRESSO_PNGX_DEFAULT_POSTPROCESS_SMOOTH_IMPORTANCE_CUTOFF;
90 140 : config->pngx_palette256_gradient_profile_enable = COLOPRESSO_PNGX_DEFAULT_PALETTE256_GRADIENT_PROFILE_ENABLE;
91 140 : config->pngx_palette256_gradient_dither_floor = COLOPRESSO_PNGX_DEFAULT_PALETTE256_GRADIENT_DITHER_FLOOR;
92 140 : config->pngx_palette256_alpha_bleed_enable = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_ENABLE;
93 140 : config->pngx_palette256_alpha_bleed_max_distance = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_MAX_DISTANCE;
94 140 : config->pngx_palette256_alpha_bleed_opaque_threshold = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_OPAQUE_THRESHOLD;
95 140 : config->pngx_palette256_alpha_bleed_soft_limit = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_SOFT_LIMIT;
96 140 : config->pngx_palette256_profile_opaque_ratio_threshold = COLOPRESSO_PNGX_DEFAULT_PALETTE256_PROFILE_OPAQUE_RATIO_THRESHOLD;
97 140 : config->pngx_palette256_profile_gradient_mean_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_PROFILE_GRADIENT_MEAN_MAX;
98 140 : config->pngx_palette256_profile_saturation_mean_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_PROFILE_SATURATION_MEAN_MAX;
99 140 : config->pngx_palette256_tune_opaque_ratio_threshold = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_OPAQUE_RATIO_THRESHOLD;
100 140 : config->pngx_palette256_tune_gradient_mean_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_GRADIENT_MEAN_MAX;
101 140 : config->pngx_palette256_tune_saturation_mean_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_SATURATION_MEAN_MAX;
102 140 : config->pngx_palette256_tune_speed_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_SPEED_MAX;
103 140 : config->pngx_palette256_tune_quality_min_floor = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_QUALITY_MIN_FLOOR;
104 140 : config->pngx_palette256_tune_quality_max_target = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_QUALITY_MAX_TARGET;
105 140 : config->pngx_threads = COLOPRESSO_PNGX_DEFAULT_THREADS;
106 : }
107 :
108 37 : extern cpres_error_t cpres_encode_webp_memory(const uint8_t *png_data, size_t png_size, uint8_t **webp_data, size_t *webp_size, const cpres_config_t *config) {
109 : uint32_t width, height;
110 : cpres_error_t error;
111 : uint8_t *rgba_data;
112 : size_t encoded_size;
113 :
114 37 : if (!png_data || !webp_data || !webp_size || !config) {
115 8 : return CPRES_ERROR_INVALID_PARAMETER;
116 : }
117 :
118 29 : if (png_size == 0) {
119 2 : return CPRES_ERROR_INVALID_PARAMETER;
120 : }
121 :
122 27 : if (png_size > COLOPRESSO_PNG_MAX_MEMORY_INPUT_SIZE) {
123 2 : return CPRES_ERROR_INVALID_PARAMETER;
124 : }
125 :
126 25 : *webp_data = NULL;
127 25 : *webp_size = 0;
128 25 : encoded_size = 0;
129 :
130 25 : rgba_data = NULL;
131 25 : error = png_decode_from_memory(png_data, png_size, &rgba_data, &width, &height);
132 25 : if (error != CPRES_OK) {
133 3 : colopresso_log(CPRES_LOG_LEVEL_ERROR, "PNG decode from memory failed: %s", cpres_error_string(error));
134 3 : return error;
135 : }
136 :
137 22 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNG decoded from memory - %dx%d pixels", width, height);
138 :
139 22 : error = webp_encode_rgba_to_memory(rgba_data, width, height, webp_data, &encoded_size, config);
140 22 : if (error == CPRES_OK) {
141 22 : if (*webp_data && encoded_size >= png_size) {
142 1 : colopresso_log(CPRES_LOG_LEVEL_WARNING, "WebP: Encoded output larger than input (%zu > %zu)", encoded_size, png_size);
143 1 : cpres_free(*webp_data);
144 1 : *webp_data = NULL;
145 1 : error = CPRES_ERROR_OUTPUT_NOT_SMALLER;
146 : }
147 22 : *webp_size = encoded_size;
148 : }
149 :
150 22 : free(rgba_data);
151 :
152 22 : return error;
153 : }
154 :
155 18 : extern cpres_error_t cpres_encode_avif_memory(const uint8_t *png_data, size_t png_size, uint8_t **avif_data, size_t *avif_size, const cpres_config_t *config) {
156 : uint32_t width, height;
157 : uint8_t *rgba_data;
158 : size_t encoded_size;
159 : cpres_error_t error;
160 :
161 18 : if (!png_data || !avif_data || !avif_size || !config) {
162 4 : return CPRES_ERROR_INVALID_PARAMETER;
163 : }
164 14 : if (png_size == 0) {
165 1 : return CPRES_ERROR_INVALID_PARAMETER;
166 : }
167 :
168 13 : if (png_size > COLOPRESSO_PNG_MAX_MEMORY_INPUT_SIZE) {
169 1 : return CPRES_ERROR_INVALID_PARAMETER;
170 : }
171 :
172 12 : *avif_data = NULL;
173 12 : *avif_size = 0;
174 12 : encoded_size = 0;
175 :
176 12 : rgba_data = NULL;
177 12 : error = png_decode_from_memory(png_data, png_size, &rgba_data, &width, &height);
178 12 : if (error != CPRES_OK) {
179 0 : colopresso_log(CPRES_LOG_LEVEL_ERROR, "PNG decode (AVIF) from memory failed: %s", cpres_error_string(error));
180 0 : return error;
181 : }
182 :
183 12 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNG decoded (AVIF) from memory - %dx%d pixels", width, height);
184 :
185 12 : error = avif_encode_rgba_to_memory(rgba_data, width, height, avif_data, &encoded_size, config);
186 12 : if (error == CPRES_OK) {
187 12 : if (*avif_data && encoded_size >= png_size) {
188 1 : colopresso_log(CPRES_LOG_LEVEL_WARNING, "AVIF: Encoded output larger than input (%zu > %zu)", encoded_size, png_size);
189 1 : cpres_free(*avif_data);
190 1 : *avif_data = NULL;
191 1 : error = CPRES_ERROR_OUTPUT_NOT_SMALLER;
192 : }
193 12 : *avif_size = encoded_size;
194 : }
195 12 : free(rgba_data);
196 :
197 12 : return error;
198 : }
199 :
200 36 : extern cpres_error_t cpres_encode_pngx_memory(const uint8_t *png_data, size_t png_size, uint8_t **optimized_data, size_t *optimized_size, const cpres_config_t *config) {
201 : pngx_options_t opts;
202 : uint8_t *lossless_data, *quant_data, *quant_optimized, *final_data;
203 : size_t lossless_size, quant_size, quant_optimized_size, final_size, candidate_size;
204 : bool lossless_ok, quant_ok, quant_lossless_ok, quant_is_rgba_lossy, final_is_quantized;
205 : int quant_quality, threads;
206 :
207 36 : if (!png_data || png_size == 0 || !optimized_data || !optimized_size || !config) {
208 5 : return CPRES_ERROR_INVALID_PARAMETER;
209 : }
210 :
211 31 : if (png_size > COLOPRESSO_PNG_MAX_MEMORY_INPUT_SIZE) {
212 0 : return CPRES_ERROR_INVALID_PARAMETER;
213 : }
214 :
215 31 : *optimized_data = NULL;
216 31 : *optimized_size = 0;
217 :
218 31 : lossless_data = NULL;
219 31 : lossless_size = 0;
220 31 : quant_data = NULL;
221 31 : quant_size = 0;
222 31 : quant_optimized = NULL;
223 31 : quant_optimized_size = 0;
224 31 : final_data = NULL;
225 31 : final_size = 0;
226 31 : candidate_size = 0;
227 31 : lossless_ok = false;
228 31 : quant_ok = false;
229 31 : quant_lossless_ok = false;
230 31 : quant_is_rgba_lossy = false;
231 31 : final_is_quantized = false;
232 31 : quant_quality = -1;
233 31 : threads = 0;
234 :
235 31 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Starting optimization - input size: %zu bytes", png_size);
236 :
237 31 : pngx_fill_pngx_options(&opts, config);
238 :
239 31 : threads = config ? config->pngx_threads : 0;
240 : #if !defined(PNGX_BRIDGE_WASM_SEPARATION)
241 31 : if (threads >= 0) {
242 31 : pngx_bridge_init_threads(threads);
243 : }
244 : #else
245 : (void)threads;
246 : #endif
247 :
248 31 : quant_is_rgba_lossy = (opts.lossy_type == PNGX_LOSSY_TYPE_LIMITED_RGBA4444 || opts.lossy_type == PNGX_LOSSY_TYPE_REDUCED_RGBA32);
249 :
250 31 : quant_ok = pngx_should_attempt_quantization(&opts) && pngx_run_quantization(png_data, png_size, &opts, &quant_data, &quant_size, &quant_quality);
251 31 : if (quant_ok) {
252 29 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Quantization produced %zu bytes (quality=%d)", quant_size, quant_quality);
253 : }
254 :
255 31 : lossless_ok = pngx_run_lossless_optimization(png_data, png_size, &opts, &lossless_data, &lossless_size);
256 31 : if (!lossless_ok) {
257 0 : lossless_data = (uint8_t *)malloc(png_size);
258 0 : if (!lossless_data) {
259 0 : free(quant_data);
260 0 : return CPRES_ERROR_OUT_OF_MEMORY;
261 : }
262 0 : memcpy(lossless_data, png_data, png_size);
263 0 : lossless_size = png_size;
264 : }
265 31 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Lossless optimization produced %zu bytes", lossless_size);
266 :
267 31 : final_data = lossless_data;
268 31 : final_size = lossless_size;
269 31 : final_is_quantized = false;
270 :
271 31 : if (quant_ok) {
272 29 : if (!quant_is_rgba_lossy) {
273 22 : quant_lossless_ok = pngx_run_lossless_optimization(quant_data, quant_size, &opts, &quant_optimized, &quant_optimized_size);
274 22 : if (quant_lossless_ok) {
275 22 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Lossless optimization on quantized data produced %zu bytes", quant_optimized_size);
276 22 : free(quant_data);
277 22 : quant_data = NULL;
278 : } else {
279 0 : quant_optimized = quant_data;
280 0 : quant_optimized_size = quant_size;
281 0 : quant_data = NULL;
282 : }
283 : } else {
284 7 : quant_optimized = quant_data;
285 7 : quant_optimized_size = quant_size;
286 7 : quant_data = NULL;
287 : }
288 : }
289 :
290 31 : if (quant_ok) {
291 29 : candidate_size = quant_optimized_size;
292 29 : if (quant_is_rgba_lossy || pngx_quantization_better(lossless_size, candidate_size)) {
293 25 : final_data = quant_optimized;
294 25 : final_size = candidate_size;
295 25 : final_is_quantized = true;
296 25 : free(lossless_data);
297 25 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Selected quantized result (%zu bytes)", final_size);
298 : } else {
299 4 : free(quant_optimized);
300 4 : quant_optimized = NULL;
301 4 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Selected lossless result (%zu bytes)", final_size);
302 : }
303 : }
304 :
305 31 : if (!final_data) {
306 0 : return CPRES_ERROR_ENCODE_FAILED;
307 : }
308 :
309 31 : if (final_size >= png_size) {
310 5 : if (!(quant_is_rgba_lossy && final_is_quantized)) {
311 2 : colopresso_log(CPRES_LOG_LEVEL_WARNING, "PNGX: Optimized output larger than input (%zu > %zu)", final_size, png_size);
312 2 : free(final_data);
313 2 : *optimized_data = NULL;
314 2 : *optimized_size = final_size;
315 2 : return CPRES_ERROR_OUTPUT_NOT_SMALLER;
316 : }
317 :
318 3 : colopresso_log(CPRES_LOG_LEVEL_WARNING, "PNGX: RGBA lossy output larger than input (%zu > %zu) but forcing write per RGBA mode", final_size, png_size);
319 : }
320 :
321 29 : *optimized_data = final_data;
322 29 : *optimized_size = final_size;
323 :
324 29 : return CPRES_OK;
325 : }
326 :
327 87 : extern void cpres_free(uint8_t *data) {
328 87 : if (data) {
329 83 : free(data);
330 : }
331 87 : }
332 :
333 18 : extern const char *cpres_error_string(cpres_error_t error) {
334 18 : switch (error) {
335 1 : case CPRES_OK:
336 1 : return "Success";
337 4 : case CPRES_ERROR_FILE_NOT_FOUND:
338 4 : return "File not found";
339 4 : case CPRES_ERROR_INVALID_PNG:
340 4 : return "Invalid PNG file";
341 1 : case CPRES_ERROR_INVALID_FORMAT:
342 1 : return "Invalid WebP file";
343 1 : case CPRES_ERROR_OUT_OF_MEMORY:
344 1 : return "Out of memory";
345 1 : case CPRES_ERROR_ENCODE_FAILED:
346 1 : return "Encoding failed";
347 1 : case CPRES_ERROR_DECODE_FAILED:
348 1 : return "Decoding failed";
349 1 : case CPRES_ERROR_IO:
350 1 : return "I/O error";
351 1 : case CPRES_ERROR_INVALID_PARAMETER:
352 1 : return "Invalid parameter";
353 2 : case CPRES_ERROR_OUTPUT_NOT_SMALLER:
354 2 : return "Output image would be larger than input";
355 1 : default:
356 1 : return "Unknown error";
357 : }
358 : }
359 :
360 1 : extern uint32_t cpres_get_version(void) { return (uint32_t)COLOPRESSO_VERSION; }
361 :
362 1 : extern uint32_t cpres_get_libwebp_version(void) { return (uint32_t)WebPGetEncoderVersion(); }
363 :
364 1 : extern uint32_t cpres_get_libpng_version(void) { return (uint32_t)png_access_version_number(); }
365 :
366 1 : extern uint32_t cpres_get_libavif_version(void) { return (uint32_t)AVIF_VERSION; }
367 :
368 1 : extern uint32_t cpres_get_pngx_oxipng_version(void) {
369 : #if !defined(PNGX_BRIDGE_WASM_SEPARATION)
370 1 : return pngx_bridge_oxipng_version();
371 : #else
372 : return 0;
373 : #endif
374 : }
375 :
376 1 : extern uint32_t cpres_get_pngx_libimagequant_version(void) {
377 : #if !defined(PNGX_BRIDGE_WASM_SEPARATION)
378 1 : return pngx_bridge_libimagequant_version();
379 : #else
380 : return 0;
381 : #endif
382 : }
383 :
384 1 : extern uint32_t cpres_get_buildtime(void) {
385 : #ifdef COLOPRESSO_BUILDTIME
386 1 : return (uint32_t)COLOPRESSO_BUILDTIME;
387 : #else
388 : return 0;
389 : #endif
390 : }
391 :
392 1 : extern const char *cpres_get_compiler_version_string(void) {
393 : #ifdef COLOPRESSO_COMPILER_VERSION_STRING
394 1 : return COLOPRESSO_COMPILER_VERSION_STRING;
395 : #else
396 : return "unknown";
397 : #endif
398 : }
399 :
400 1 : extern const char *cpres_get_rust_version_string(void) {
401 : #if !defined(PNGX_BRIDGE_WASM_SEPARATION)
402 1 : return pngx_bridge_rust_version_string();
403 : #else
404 : return "unknown";
405 : #endif
406 : }
|