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 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/sanitizer.h"
25 :
26 : #include "internal/log.h"
27 : #include "internal/png.h"
28 :
29 : #include "internal/avif.h"
30 : #include "internal/pngx.h"
31 : #include "internal/webp.h"
32 :
33 134 : void cpres_config_init_defaults(cpres_config_t *config) {
34 134 : if (!config) {
35 1 : return;
36 : }
37 :
38 133 : memset(config, 0, sizeof(*config));
39 :
40 133 : config->webp_quality = COLOPRESSO_WEBP_DEFAULT_QUALITY;
41 133 : config->webp_lossless = COLOPRESSO_WEBP_DEFAULT_LOSSLESS;
42 133 : config->webp_method = COLOPRESSO_WEBP_DEFAULT_METHOD;
43 133 : config->webp_target_size = COLOPRESSO_WEBP_DEFAULT_TARGET_SIZE;
44 133 : config->webp_target_psnr = COLOPRESSO_WEBP_DEFAULT_TARGET_PSNR;
45 133 : config->webp_segments = COLOPRESSO_WEBP_DEFAULT_SEGMENTS;
46 133 : config->webp_sns_strength = COLOPRESSO_WEBP_DEFAULT_SNS_STRENGTH;
47 133 : config->webp_filter_strength = COLOPRESSO_WEBP_DEFAULT_FILTER_STRENGTH;
48 133 : config->webp_filter_sharpness = COLOPRESSO_WEBP_DEFAULT_FILTER_SHARPNESS;
49 133 : config->webp_filter_type = COLOPRESSO_WEBP_DEFAULT_FILTER_TYPE;
50 133 : config->webp_autofilter = COLOPRESSO_WEBP_DEFAULT_AUTOFILTER;
51 133 : config->webp_alpha_compression = COLOPRESSO_WEBP_DEFAULT_ALPHA_COMPRESSION;
52 133 : config->webp_alpha_filtering = COLOPRESSO_WEBP_DEFAULT_ALPHA_FILTERING;
53 133 : config->webp_alpha_quality = COLOPRESSO_WEBP_DEFAULT_ALPHA_QUALITY;
54 133 : config->webp_pass = COLOPRESSO_WEBP_DEFAULT_PASS;
55 133 : config->webp_preprocessing = COLOPRESSO_WEBP_DEFAULT_PREPROCESSING;
56 133 : config->webp_partitions = COLOPRESSO_WEBP_DEFAULT_PARTITIONS;
57 133 : config->webp_partition_limit = COLOPRESSO_WEBP_DEFAULT_PARTITION_LIMIT;
58 133 : config->webp_emulate_jpeg_size = COLOPRESSO_WEBP_DEFAULT_EMULATE_JPEG_SIZE;
59 133 : config->webp_thread_level = COLOPRESSO_WEBP_DEFAULT_THREAD_LEVEL;
60 133 : config->webp_low_memory = COLOPRESSO_WEBP_DEFAULT_LOW_MEMORY;
61 133 : config->webp_near_lossless = COLOPRESSO_WEBP_DEFAULT_NEAR_LOSSLESS;
62 133 : config->webp_exact = COLOPRESSO_WEBP_DEFAULT_EXACT;
63 133 : config->webp_use_delta_palette = COLOPRESSO_WEBP_DEFAULT_USE_DELTA_PALETTE;
64 133 : config->webp_use_sharp_yuv = COLOPRESSO_WEBP_DEFAULT_USE_SHARP_YUV;
65 :
66 133 : config->avif_quality = COLOPRESSO_AVIF_DEFAULT_QUALITY;
67 133 : config->avif_alpha_quality = COLOPRESSO_AVIF_DEFAULT_ALPHA_QUALITY;
68 133 : config->avif_lossless = COLOPRESSO_AVIF_DEFAULT_LOSSLESS;
69 133 : config->avif_speed = COLOPRESSO_AVIF_DEFAULT_SPEED;
70 133 : config->avif_threads = COLOPRESSO_AVIF_DEFAULT_THREADS;
71 :
72 133 : config->pngx_level = COLOPRESSO_PNGX_DEFAULT_LEVEL;
73 133 : config->pngx_strip_safe = COLOPRESSO_PNGX_DEFAULT_STRIP_SAFE;
74 133 : config->pngx_optimize_alpha = COLOPRESSO_PNGX_DEFAULT_OPTIMIZE_ALPHA;
75 133 : config->pngx_lossy_enable = COLOPRESSO_PNGX_DEFAULT_LOSSY_ENABLE;
76 133 : config->pngx_lossy_type = COLOPRESSO_PNGX_DEFAULT_LOSSY_TYPE;
77 133 : config->pngx_lossy_max_colors = COLOPRESSO_PNGX_DEFAULT_LOSSY_MAX_COLORS;
78 133 : config->pngx_lossy_reduced_colors = COLOPRESSO_PNGX_DEFAULT_REDUCED_COLORS;
79 133 : config->pngx_lossy_reduced_bits_rgb = COLOPRESSO_PNGX_DEFAULT_REDUCED_BITS_RGB;
80 133 : config->pngx_lossy_reduced_alpha_bits = COLOPRESSO_PNGX_DEFAULT_REDUCED_ALPHA_BITS;
81 133 : config->pngx_lossy_quality_min = COLOPRESSO_PNGX_DEFAULT_LOSSY_QUALITY_MIN;
82 133 : config->pngx_lossy_quality_max = COLOPRESSO_PNGX_DEFAULT_LOSSY_QUALITY_MAX;
83 133 : config->pngx_lossy_speed = COLOPRESSO_PNGX_DEFAULT_LOSSY_SPEED;
84 133 : config->pngx_lossy_dither_level = COLOPRESSO_PNGX_DEFAULT_LOSSY_DITHER_LEVEL;
85 133 : config->pngx_saliency_map_enable = COLOPRESSO_PNGX_DEFAULT_SALIENCY_MAP_ENABLE;
86 133 : config->pngx_chroma_anchor_enable = COLOPRESSO_PNGX_DEFAULT_CHROMA_ANCHOR_ENABLE;
87 133 : config->pngx_adaptive_dither_enable = COLOPRESSO_PNGX_DEFAULT_ADAPTIVE_DITHER_ENABLE;
88 133 : config->pngx_gradient_boost_enable = COLOPRESSO_PNGX_DEFAULT_GRADIENT_BOOST_ENABLE;
89 133 : config->pngx_chroma_weight_enable = COLOPRESSO_PNGX_DEFAULT_CHROMA_WEIGHT_ENABLE;
90 133 : config->pngx_postprocess_smooth_enable = COLOPRESSO_PNGX_DEFAULT_POSTPROCESS_SMOOTH_ENABLE;
91 133 : config->pngx_postprocess_smooth_importance_cutoff = COLOPRESSO_PNGX_DEFAULT_POSTPROCESS_SMOOTH_IMPORTANCE_CUTOFF;
92 133 : config->pngx_palette256_gradient_profile_enable = COLOPRESSO_PNGX_DEFAULT_PALETTE256_GRADIENT_PROFILE_ENABLE;
93 133 : config->pngx_palette256_gradient_dither_floor = COLOPRESSO_PNGX_DEFAULT_PALETTE256_GRADIENT_DITHER_FLOOR;
94 133 : config->pngx_palette256_alpha_bleed_enable = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_ENABLE;
95 133 : config->pngx_palette256_alpha_bleed_max_distance = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_MAX_DISTANCE;
96 133 : config->pngx_palette256_alpha_bleed_opaque_threshold = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_OPAQUE_THRESHOLD;
97 133 : config->pngx_palette256_alpha_bleed_soft_limit = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_SOFT_LIMIT;
98 133 : config->pngx_palette256_profile_opaque_ratio_threshold = COLOPRESSO_PNGX_DEFAULT_PALETTE256_PROFILE_OPAQUE_RATIO_THRESHOLD;
99 133 : config->pngx_palette256_profile_gradient_mean_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_PROFILE_GRADIENT_MEAN_MAX;
100 133 : config->pngx_palette256_profile_saturation_mean_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_PROFILE_SATURATION_MEAN_MAX;
101 133 : config->pngx_palette256_tune_opaque_ratio_threshold = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_OPAQUE_RATIO_THRESHOLD;
102 133 : config->pngx_palette256_tune_gradient_mean_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_GRADIENT_MEAN_MAX;
103 133 : config->pngx_palette256_tune_saturation_mean_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_SATURATION_MEAN_MAX;
104 133 : config->pngx_palette256_tune_speed_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_SPEED_MAX;
105 133 : config->pngx_palette256_tune_quality_min_floor = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_QUALITY_MIN_FLOOR;
106 133 : config->pngx_palette256_tune_quality_max_target = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_QUALITY_MAX_TARGET;
107 133 : config->pngx_threads = COLOPRESSO_PNGX_DEFAULT_THREADS;
108 : }
109 :
110 37 : 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) {
111 : uint32_t width, height;
112 : cpres_error_t error;
113 : uint8_t *rgba_data;
114 : size_t encoded_size;
115 :
116 37 : if (!png_data || !webp_data || !webp_size || !config) {
117 8 : return CPRES_ERROR_INVALID_PARAMETER;
118 : }
119 :
120 29 : if (png_size == 0) {
121 2 : return CPRES_ERROR_INVALID_PARAMETER;
122 : }
123 :
124 27 : if (png_size > COLOPRESSO_PNG_MAX_MEMORY_INPUT_SIZE) {
125 2 : return CPRES_ERROR_INVALID_PARAMETER;
126 : }
127 :
128 25 : *webp_data = NULL;
129 25 : *webp_size = 0;
130 25 : encoded_size = 0;
131 :
132 25 : rgba_data = NULL;
133 25 : error = cpres_png_decode_from_memory(png_data, png_size, &rgba_data, &width, &height);
134 25 : if (error != CPRES_OK) {
135 3 : cpres_log(CPRES_LOG_LEVEL_ERROR, "PNG decode from memory failed: %s", cpres_error_string(error));
136 3 : return error;
137 : }
138 :
139 22 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNG decoded from memory - %dx%d pixels", width, height);
140 :
141 22 : error = cpres_webp_encode_rgba_to_memory(rgba_data, width, height, webp_data, &encoded_size, config);
142 22 : if (error == CPRES_OK) {
143 22 : if (*webp_data && encoded_size >= png_size) {
144 1 : cpres_log(CPRES_LOG_LEVEL_WARNING, "WebP: Encoded output larger than input (%zu > %zu)", encoded_size, png_size);
145 1 : cpres_free(*webp_data);
146 1 : *webp_data = NULL;
147 1 : error = CPRES_ERROR_OUTPUT_NOT_SMALLER;
148 : }
149 22 : *webp_size = encoded_size;
150 : }
151 :
152 22 : free(rgba_data);
153 :
154 22 : return error;
155 : }
156 :
157 18 : 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) {
158 : uint32_t width, height;
159 : uint8_t *rgba_data;
160 : size_t encoded_size;
161 : cpres_error_t error;
162 :
163 18 : if (!png_data || !avif_data || !avif_size || !config) {
164 4 : return CPRES_ERROR_INVALID_PARAMETER;
165 : }
166 14 : if (png_size == 0) {
167 1 : return CPRES_ERROR_INVALID_PARAMETER;
168 : }
169 :
170 13 : if (png_size > COLOPRESSO_PNG_MAX_MEMORY_INPUT_SIZE) {
171 1 : return CPRES_ERROR_INVALID_PARAMETER;
172 : }
173 :
174 12 : *avif_data = NULL;
175 12 : *avif_size = 0;
176 12 : encoded_size = 0;
177 :
178 12 : rgba_data = NULL;
179 12 : error = cpres_png_decode_from_memory(png_data, png_size, &rgba_data, &width, &height);
180 12 : if (error != CPRES_OK) {
181 0 : cpres_log(CPRES_LOG_LEVEL_ERROR, "PNG decode (AVIF) from memory failed: %s", cpres_error_string(error));
182 0 : return error;
183 : }
184 :
185 12 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNG decoded (AVIF) from memory - %dx%d pixels", width, height);
186 :
187 12 : error = cpres_avif_encode_rgba_to_memory(rgba_data, width, height, avif_data, &encoded_size, config);
188 12 : if (error == CPRES_OK) {
189 12 : if (*avif_data && encoded_size >= png_size) {
190 1 : cpres_log(CPRES_LOG_LEVEL_WARNING, "AVIF: Encoded output larger than input (%zu > %zu)", encoded_size, png_size);
191 1 : cpres_free(*avif_data);
192 1 : *avif_data = NULL;
193 1 : error = CPRES_ERROR_OUTPUT_NOT_SMALLER;
194 : }
195 12 : *avif_size = encoded_size;
196 : }
197 12 : free(rgba_data);
198 :
199 12 : return error;
200 : }
201 :
202 36 : 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) {
203 : pngx_options_t opts;
204 : uint8_t *lossless_data, *quant_data, *quant_optimized, *final_data;
205 : size_t lossless_size, quant_size, quant_optimized_size, final_size, candidate_size;
206 : bool lossless_ok, quant_ok, quant_lossless_ok, quant_is_rgba_lossy, final_is_quantized;
207 : int quant_quality, threads;
208 :
209 36 : if (!png_data || png_size == 0 || !optimized_data || !optimized_size || !config) {
210 5 : return CPRES_ERROR_INVALID_PARAMETER;
211 : }
212 :
213 31 : if (png_size > COLOPRESSO_PNG_MAX_MEMORY_INPUT_SIZE) {
214 0 : return CPRES_ERROR_INVALID_PARAMETER;
215 : }
216 :
217 31 : *optimized_data = NULL;
218 31 : *optimized_size = 0;
219 :
220 31 : lossless_data = NULL;
221 31 : lossless_size = 0;
222 31 : quant_data = NULL;
223 31 : quant_size = 0;
224 31 : quant_optimized = NULL;
225 31 : quant_optimized_size = 0;
226 31 : final_data = NULL;
227 31 : final_size = 0;
228 31 : candidate_size = 0;
229 31 : lossless_ok = false;
230 31 : quant_ok = false;
231 31 : quant_lossless_ok = false;
232 31 : quant_is_rgba_lossy = false;
233 31 : final_is_quantized = false;
234 31 : quant_quality = -1;
235 31 : threads = 0;
236 :
237 31 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Starting optimization - input size: %zu bytes", png_size);
238 :
239 31 : pngx_fill_pngx_options(&opts, config);
240 :
241 31 : threads = config ? config->pngx_threads : 0;
242 31 : if (threads > 0) {
243 31 : pngx_bridge_init_threads(threads);
244 : }
245 :
246 31 : quant_is_rgba_lossy = (opts.lossy_type == PNGX_LOSSY_TYPE_LIMITED_RGBA4444 || opts.lossy_type == PNGX_LOSSY_TYPE_REDUCED_RGBA32);
247 :
248 31 : quant_ok = pngx_should_attempt_quantization(&opts) && pngx_run_quantization(png_data, png_size, &opts, &quant_data, &quant_size, &quant_quality);
249 31 : if (quant_ok) {
250 30 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Quantization produced %zu bytes (quality=%d)", quant_size, quant_quality);
251 : }
252 :
253 31 : lossless_ok = pngx_run_lossless_optimization(png_data, png_size, &opts, &lossless_data, &lossless_size);
254 31 : if (!lossless_ok) {
255 0 : lossless_data = (uint8_t *)malloc(png_size);
256 0 : if (!lossless_data) {
257 0 : free(quant_data);
258 0 : return CPRES_ERROR_OUT_OF_MEMORY;
259 : }
260 0 : memcpy(lossless_data, png_data, png_size);
261 0 : lossless_size = png_size;
262 : }
263 31 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Lossless optimization produced %zu bytes", lossless_size);
264 :
265 31 : final_data = lossless_data;
266 31 : final_size = lossless_size;
267 31 : final_is_quantized = false;
268 :
269 31 : if (quant_ok) {
270 30 : if (!quant_is_rgba_lossy) {
271 23 : quant_lossless_ok = pngx_run_lossless_optimization(quant_data, quant_size, &opts, &quant_optimized, &quant_optimized_size);
272 23 : if (quant_lossless_ok) {
273 23 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Lossless optimization on quantized data produced %zu bytes", quant_optimized_size);
274 23 : free(quant_data);
275 23 : quant_data = NULL;
276 : } else {
277 0 : quant_optimized = quant_data;
278 0 : quant_optimized_size = quant_size;
279 0 : quant_data = NULL;
280 : }
281 : } else {
282 7 : quant_optimized = quant_data;
283 7 : quant_optimized_size = quant_size;
284 7 : quant_data = NULL;
285 : }
286 : }
287 :
288 31 : if (quant_ok) {
289 30 : candidate_size = quant_optimized_size;
290 30 : if (quant_is_rgba_lossy || pngx_quantization_better(lossless_size, candidate_size)) {
291 25 : final_data = quant_optimized;
292 25 : final_size = candidate_size;
293 25 : final_is_quantized = true;
294 25 : free(lossless_data);
295 25 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Selected quantized result (%zu bytes)", final_size);
296 : } else {
297 5 : free(quant_optimized);
298 5 : quant_optimized = NULL;
299 5 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Selected lossless result (%zu bytes)", final_size);
300 : }
301 : }
302 :
303 31 : if (!final_data) {
304 0 : return CPRES_ERROR_ENCODE_FAILED;
305 : }
306 :
307 31 : if (final_size >= png_size) {
308 5 : if (!(quant_is_rgba_lossy && final_is_quantized)) {
309 2 : cpres_log(CPRES_LOG_LEVEL_WARNING, "PNGX: Optimized output larger than input (%zu > %zu)", final_size, png_size);
310 2 : free(final_data);
311 2 : *optimized_data = NULL;
312 2 : *optimized_size = final_size;
313 2 : return CPRES_ERROR_OUTPUT_NOT_SMALLER;
314 : }
315 :
316 3 : cpres_log(CPRES_LOG_LEVEL_WARNING, "PNGX: RGBA lossy output larger than input (%zu > %zu) but forcing write per RGBA mode", final_size, png_size);
317 : }
318 :
319 : MSAN_UNPOISON(final_data, final_size);
320 :
321 29 : *optimized_data = final_data;
322 29 : *optimized_size = final_size;
323 :
324 29 : return CPRES_OK;
325 : }
326 :
327 87 : void cpres_free(uint8_t *data) {
328 87 : if (data) {
329 83 : free(data);
330 : }
331 87 : }
332 :
333 18 : 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 : uint32_t cpres_get_version(void) { return (uint32_t)COLOPRESSO_VERSION; }
361 :
362 1 : uint32_t cpres_get_libwebp_version(void) { return (uint32_t)WebPGetEncoderVersion(); }
363 :
364 1 : uint32_t cpres_get_libpng_version(void) { return (uint32_t)png_access_version_number(); }
365 :
366 1 : uint32_t cpres_get_libavif_version(void) { return (uint32_t)AVIF_VERSION; }
367 :
368 1 : uint32_t cpres_get_pngx_oxipng_version(void) { return pngx_bridge_oxipng_version(); }
369 :
370 1 : uint32_t cpres_get_pngx_libimagequant_version(void) { return pngx_bridge_libimagequant_version(); }
371 :
372 1 : uint32_t cpres_get_buildtime(void) {
373 : #ifdef COLOPRESSO_BUILDTIME
374 1 : return (uint32_t)COLOPRESSO_BUILDTIME;
375 : #else
376 : return 0;
377 : #endif
378 : }
|