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 <colopresso.h>
13 :
14 : #include <stdbool.h>
15 : #include <stddef.h>
16 : #include <stdint.h>
17 : #include <string.h>
18 :
19 : #include "internal/log.h"
20 : #include "internal/pngx_common.h"
21 :
22 : static int g_pngx_last_error = 0;
23 :
24 1 : int pngx_get_last_error(void) { return g_pngx_last_error; }
25 :
26 80 : void pngx_set_last_error(int error_code) { g_pngx_last_error = error_code; }
27 :
28 31 : bool pngx_run_quantization(const uint8_t *png_data, size_t png_size, const pngx_options_t *opts, uint8_t **out_data, size_t *out_size, int *quant_quality) {
29 : const char *label;
30 31 : uint32_t resolved_colors = 0, applied_colors = 0;
31 : bool success;
32 :
33 31 : if (!png_data || png_size == 0 || !opts || !out_data || !out_size) {
34 0 : return false;
35 : }
36 :
37 31 : *out_data = NULL;
38 31 : *out_size = 0;
39 31 : if (quant_quality) {
40 31 : *quant_quality = -1;
41 : }
42 :
43 31 : if (opts->lossy_type == PNGX_LOSSY_TYPE_REDUCED_RGBA32) {
44 5 : success = pngx_quantize_reduced_rgba32(png_data, png_size, opts, &resolved_colors, &applied_colors, out_data, out_size);
45 5 : if (success) {
46 5 : label = lossy_type_label(opts->lossy_type);
47 5 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: %s target %u colors -> %u unique", label, resolved_colors, applied_colors);
48 : }
49 5 : return success;
50 : }
51 :
52 26 : if (opts->lossy_type == PNGX_LOSSY_TYPE_LIMITED_RGBA4444) {
53 3 : return pngx_quantize_limited4444(png_data, png_size, opts, out_data, out_size);
54 : }
55 :
56 23 : return pngx_quantize_palette256(png_data, png_size, opts, out_data, out_size, quant_quality);
57 : }
58 :
59 37 : void pngx_fill_pngx_options(pngx_options_t *opts, const cpres_config_t *config) {
60 37 : uint16_t lossy_max_colors = COLOPRESSO_PNGX_DEFAULT_LOSSY_MAX_COLORS, palette256_alpha_bleed_max_distance = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_MAX_DISTANCE;
61 37 : uint8_t level = COLOPRESSO_PNGX_DEFAULT_LEVEL, lossy_quality_min = COLOPRESSO_PNGX_DEFAULT_LOSSY_QUALITY_MIN, lossy_quality_max = COLOPRESSO_PNGX_DEFAULT_LOSSY_QUALITY_MAX,
62 37 : lossy_speed = COLOPRESSO_PNGX_DEFAULT_LOSSY_SPEED, lossy_type = COLOPRESSO_PNGX_DEFAULT_LOSSY_TYPE, lossy_reduced_bits_rgb = COLOPRESSO_PNGX_DEFAULT_REDUCED_BITS_RGB,
63 37 : lossy_reduced_bits_alpha = COLOPRESSO_PNGX_DEFAULT_REDUCED_ALPHA_BITS, tmp, palette256_alpha_bleed_opaque_threshold = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_OPAQUE_THRESHOLD,
64 37 : palette256_alpha_bleed_soft_limit = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_SOFT_LIMIT;
65 37 : int32_t lossy_reduced_colors = COLOPRESSO_PNGX_DEFAULT_REDUCED_COLORS, clamped, thread_count = 0;
66 37 : int16_t palette256_tune_speed_max = PNGX_PALETTE256_TUNE_SPEED_MAX, palette256_tune_quality_min_floor = PNGX_PALETTE256_TUNE_QUALITY_MIN_FLOOR,
67 37 : palette256_tune_quality_max_target = PNGX_PALETTE256_TUNE_QUALITY_MAX_TARGET;
68 37 : bool strip_safe = COLOPRESSO_PNGX_DEFAULT_STRIP_SAFE, optimize_alpha = COLOPRESSO_PNGX_DEFAULT_OPTIMIZE_ALPHA, lossy_enable = COLOPRESSO_PNGX_DEFAULT_LOSSY_ENABLE, lossy_dither_auto = false,
69 37 : saliency_map_enable = COLOPRESSO_PNGX_DEFAULT_SALIENCY_MAP_ENABLE, chroma_anchor_enable = COLOPRESSO_PNGX_DEFAULT_CHROMA_ANCHOR_ENABLE,
70 37 : adaptive_dither_enable = COLOPRESSO_PNGX_DEFAULT_ADAPTIVE_DITHER_ENABLE, gradient_boost_enable = COLOPRESSO_PNGX_DEFAULT_GRADIENT_BOOST_ENABLE,
71 37 : chroma_weight_enable = COLOPRESSO_PNGX_DEFAULT_CHROMA_WEIGHT_ENABLE, postprocess_smooth_enable = COLOPRESSO_PNGX_DEFAULT_POSTPROCESS_SMOOTH_ENABLE,
72 37 : palette256_gradient_profile_enable = COLOPRESSO_PNGX_DEFAULT_PALETTE256_GRADIENT_PROFILE_ENABLE, palette256_alpha_bleed_enable = COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_ENABLE;
73 37 : float lossy_dither_level = COLOPRESSO_PNGX_DEFAULT_LOSSY_DITHER_LEVEL, postprocess_smooth_importance_cutoff = COLOPRESSO_PNGX_DEFAULT_POSTPROCESS_SMOOTH_IMPORTANCE_CUTOFF,
74 37 : palette256_gradient_profile_dither_floor = PNGX_PALETTE256_GRADIENT_PROFILE_DITHER_FLOOR, palette256_profile_opaque_ratio_threshold = PNGX_PALETTE256_GRADIENT_PROFILE_OPAQUE_RATIO_THRESHOLD,
75 37 : palette256_profile_gradient_mean_max = PNGX_PALETTE256_GRADIENT_PROFILE_GRADIENT_MEAN_MAX, palette256_profile_saturation_mean_max = PNGX_PALETTE256_GRADIENT_PROFILE_SATURATION_MEAN_MAX,
76 37 : palette256_tune_opaque_ratio_threshold = PNGX_PALETTE256_TUNE_OPAQUE_RATIO_THRESHOLD, palette256_tune_gradient_mean_max = PNGX_PALETTE256_TUNE_GRADIENT_MEAN_MAX,
77 37 : palette256_tune_saturation_mean_max = PNGX_PALETTE256_TUNE_SATURATION_MEAN_MAX;
78 :
79 37 : if (!opts) {
80 0 : return;
81 : }
82 :
83 37 : if (config) {
84 37 : if (config->pngx_level >= 0 && config->pngx_level <= 6) {
85 37 : level = (uint8_t)config->pngx_level;
86 : }
87 37 : strip_safe = config->pngx_strip_safe;
88 37 : optimize_alpha = config->pngx_optimize_alpha;
89 37 : lossy_enable = config->pngx_lossy_enable;
90 37 : if (config->pngx_lossy_type >= CPRES_PNGX_LOSSY_TYPE_PALETTE256 && config->pngx_lossy_type <= CPRES_PNGX_LOSSY_TYPE_REDUCED_RGBA32) {
91 37 : lossy_type = (uint8_t)config->pngx_lossy_type;
92 : }
93 :
94 37 : if (config->pngx_lossy_max_colors > 0 && config->pngx_lossy_max_colors <= 256) {
95 36 : lossy_max_colors = (uint16_t)config->pngx_lossy_max_colors;
96 : }
97 37 : if (config->pngx_lossy_reduced_colors == COLOPRESSO_PNGX_DEFAULT_REDUCED_COLORS) {
98 32 : lossy_reduced_colors = COLOPRESSO_PNGX_DEFAULT_REDUCED_COLORS;
99 5 : } else if (config->pngx_lossy_reduced_colors >= (int32_t)COLOPRESSO_PNGX_REDUCED_COLORS_MIN) {
100 2 : clamped = config->pngx_lossy_reduced_colors;
101 2 : if (clamped > (int32_t)COLOPRESSO_PNGX_REDUCED_COLORS_MAX) {
102 0 : clamped = (int32_t)COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
103 : }
104 :
105 2 : lossy_reduced_colors = clamped;
106 : }
107 37 : if (config->pngx_lossy_reduced_bits_rgb >= COLOPRESSO_PNGX_REDUCED_BITS_MIN && config->pngx_lossy_reduced_bits_rgb <= COLOPRESSO_PNGX_REDUCED_BITS_MAX) {
108 37 : lossy_reduced_bits_rgb = (uint8_t)config->pngx_lossy_reduced_bits_rgb;
109 : }
110 37 : if (config->pngx_lossy_reduced_alpha_bits >= COLOPRESSO_PNGX_REDUCED_BITS_MIN && config->pngx_lossy_reduced_alpha_bits <= COLOPRESSO_PNGX_REDUCED_BITS_MAX) {
111 37 : lossy_reduced_bits_alpha = (uint8_t)config->pngx_lossy_reduced_alpha_bits;
112 : }
113 37 : if (config->pngx_lossy_quality_min >= 0 && config->pngx_lossy_quality_min <= 100) {
114 36 : lossy_quality_min = (uint8_t)config->pngx_lossy_quality_min;
115 : }
116 37 : if (config->pngx_lossy_quality_max >= 0 && config->pngx_lossy_quality_max <= 100) {
117 36 : lossy_quality_max = (uint8_t)config->pngx_lossy_quality_max;
118 : }
119 37 : if (lossy_quality_max < lossy_quality_min) {
120 1 : tmp = lossy_quality_min;
121 1 : lossy_quality_min = lossy_quality_max;
122 1 : lossy_quality_max = tmp;
123 : }
124 37 : if (config->pngx_lossy_speed >= 1 && config->pngx_lossy_speed <= 10) {
125 36 : lossy_speed = (uint8_t)config->pngx_lossy_speed;
126 : }
127 37 : if (config->pngx_lossy_dither_level < 0.0f) {
128 1 : lossy_dither_auto = true;
129 36 : } else if (config->pngx_lossy_dither_level <= 1.0f) {
130 35 : lossy_dither_level = config->pngx_lossy_dither_level;
131 : }
132 37 : if (config->pngx_protected_colors && config->pngx_protected_colors_count > 0) {
133 2 : opts->protected_colors = config->pngx_protected_colors;
134 2 : opts->protected_colors_count = config->pngx_protected_colors_count;
135 : } else {
136 35 : opts->protected_colors = NULL;
137 35 : opts->protected_colors_count = 0;
138 : }
139 :
140 37 : saliency_map_enable = config->pngx_saliency_map_enable;
141 37 : chroma_anchor_enable = config->pngx_chroma_anchor_enable;
142 37 : adaptive_dither_enable = config->pngx_adaptive_dither_enable;
143 37 : gradient_boost_enable = config->pngx_gradient_boost_enable;
144 37 : chroma_weight_enable = config->pngx_chroma_weight_enable;
145 37 : postprocess_smooth_enable = config->pngx_postprocess_smooth_enable;
146 37 : postprocess_smooth_importance_cutoff = config->pngx_postprocess_smooth_importance_cutoff;
147 :
148 37 : palette256_gradient_profile_enable = config->pngx_palette256_gradient_profile_enable;
149 37 : if (config->pngx_palette256_gradient_dither_floor >= 0.0f && config->pngx_palette256_gradient_dither_floor <= 1.0f) {
150 37 : palette256_gradient_profile_dither_floor = config->pngx_palette256_gradient_dither_floor;
151 : }
152 37 : palette256_alpha_bleed_enable = config->pngx_palette256_alpha_bleed_enable;
153 :
154 37 : if (config->pngx_palette256_alpha_bleed_max_distance >= 0 && config->pngx_palette256_alpha_bleed_max_distance <= 65535) {
155 37 : palette256_alpha_bleed_max_distance = (uint16_t)config->pngx_palette256_alpha_bleed_max_distance;
156 : }
157 37 : if (config->pngx_palette256_alpha_bleed_opaque_threshold >= 0 && config->pngx_palette256_alpha_bleed_opaque_threshold <= 255) {
158 37 : palette256_alpha_bleed_opaque_threshold = (uint8_t)config->pngx_palette256_alpha_bleed_opaque_threshold;
159 : }
160 37 : if (config->pngx_palette256_alpha_bleed_soft_limit >= 0 && config->pngx_palette256_alpha_bleed_soft_limit <= 255) {
161 37 : palette256_alpha_bleed_soft_limit = (uint8_t)config->pngx_palette256_alpha_bleed_soft_limit;
162 : }
163 :
164 37 : if (config->pngx_palette256_profile_opaque_ratio_threshold >= 0.0f && config->pngx_palette256_profile_opaque_ratio_threshold <= 1.0f) {
165 36 : palette256_profile_opaque_ratio_threshold = config->pngx_palette256_profile_opaque_ratio_threshold;
166 : }
167 37 : if (config->pngx_palette256_profile_gradient_mean_max >= 0.0f && config->pngx_palette256_profile_gradient_mean_max <= 1.0f) {
168 36 : palette256_profile_gradient_mean_max = config->pngx_palette256_profile_gradient_mean_max;
169 : }
170 37 : if (config->pngx_palette256_profile_saturation_mean_max >= 0.0f && config->pngx_palette256_profile_saturation_mean_max <= 1.0f) {
171 36 : palette256_profile_saturation_mean_max = config->pngx_palette256_profile_saturation_mean_max;
172 : }
173 :
174 37 : if (config->pngx_palette256_tune_opaque_ratio_threshold >= 0.0f && config->pngx_palette256_tune_opaque_ratio_threshold <= 1.0f) {
175 37 : palette256_tune_opaque_ratio_threshold = config->pngx_palette256_tune_opaque_ratio_threshold;
176 : }
177 37 : if (config->pngx_palette256_tune_gradient_mean_max >= 0.0f && config->pngx_palette256_tune_gradient_mean_max <= 1.0f) {
178 37 : palette256_tune_gradient_mean_max = config->pngx_palette256_tune_gradient_mean_max;
179 : }
180 37 : if (config->pngx_palette256_tune_saturation_mean_max >= 0.0f && config->pngx_palette256_tune_saturation_mean_max <= 1.0f) {
181 37 : palette256_tune_saturation_mean_max = config->pngx_palette256_tune_saturation_mean_max;
182 : }
183 :
184 37 : if (config->pngx_palette256_tune_speed_max >= 1 && config->pngx_palette256_tune_speed_max <= 10) {
185 37 : palette256_tune_speed_max = (int16_t)config->pngx_palette256_tune_speed_max;
186 : }
187 37 : if (config->pngx_palette256_tune_quality_min_floor >= 0 && config->pngx_palette256_tune_quality_min_floor <= 100) {
188 37 : palette256_tune_quality_min_floor = (int16_t)config->pngx_palette256_tune_quality_min_floor;
189 : }
190 37 : if (config->pngx_palette256_tune_quality_max_target >= 0 && config->pngx_palette256_tune_quality_max_target <= 100) {
191 37 : palette256_tune_quality_max_target = (int16_t)config->pngx_palette256_tune_quality_max_target;
192 : }
193 37 : if (config->pngx_threads >= 0) {
194 37 : thread_count = (uint32_t)config->pngx_threads;
195 : }
196 : } else {
197 0 : opts->protected_colors = NULL;
198 0 : opts->protected_colors_count = 0;
199 : }
200 :
201 37 : lossy_quality_min = clamp_uint8_t(lossy_quality_min, 0, 100);
202 37 : lossy_quality_max = clamp_uint8_t(lossy_quality_max, lossy_quality_min, 100);
203 37 : lossy_speed = clamp_uint8_t(lossy_speed, 1, 10);
204 37 : lossy_max_colors = clamp_uint16_t(lossy_max_colors, 2, 256);
205 37 : lossy_dither_level = clamp_float(lossy_dither_level, 0.0f, 1.0f);
206 37 : lossy_reduced_bits_rgb = clamp_reduced_bits(lossy_reduced_bits_rgb);
207 37 : lossy_reduced_bits_alpha = clamp_reduced_bits(lossy_reduced_bits_alpha);
208 37 : if (postprocess_smooth_importance_cutoff < 0.0f) {
209 1 : postprocess_smooth_importance_cutoff = -1.0f;
210 : } else {
211 36 : postprocess_smooth_importance_cutoff = clamp_float(postprocess_smooth_importance_cutoff, 0.0f, 1.0f);
212 : }
213 :
214 37 : if (palette256_gradient_profile_dither_floor < 0.0f) {
215 0 : palette256_gradient_profile_dither_floor = -1.0f;
216 : } else {
217 37 : palette256_gradient_profile_dither_floor = clamp_float(palette256_gradient_profile_dither_floor, 0.0f, 1.0f);
218 : }
219 :
220 37 : if (palette256_profile_opaque_ratio_threshold < 0.0f) {
221 0 : palette256_profile_opaque_ratio_threshold = -1.0f;
222 : } else {
223 37 : palette256_profile_opaque_ratio_threshold = clamp_float(palette256_profile_opaque_ratio_threshold, 0.0f, 1.0f);
224 : }
225 37 : if (palette256_profile_gradient_mean_max < 0.0f) {
226 0 : palette256_profile_gradient_mean_max = -1.0f;
227 : } else {
228 37 : palette256_profile_gradient_mean_max = clamp_float(palette256_profile_gradient_mean_max, 0.0f, 1.0f);
229 : }
230 37 : if (palette256_profile_saturation_mean_max < 0.0f) {
231 0 : palette256_profile_saturation_mean_max = -1.0f;
232 : } else {
233 37 : palette256_profile_saturation_mean_max = clamp_float(palette256_profile_saturation_mean_max, 0.0f, 1.0f);
234 : }
235 :
236 37 : if (palette256_tune_opaque_ratio_threshold < 0.0f) {
237 0 : palette256_tune_opaque_ratio_threshold = -1.0f;
238 : } else {
239 37 : palette256_tune_opaque_ratio_threshold = clamp_float(palette256_tune_opaque_ratio_threshold, 0.0f, 1.0f);
240 : }
241 37 : if (palette256_tune_gradient_mean_max < 0.0f) {
242 0 : palette256_tune_gradient_mean_max = -1.0f;
243 : } else {
244 37 : palette256_tune_gradient_mean_max = clamp_float(palette256_tune_gradient_mean_max, 0.0f, 1.0f);
245 : }
246 37 : if (palette256_tune_saturation_mean_max < 0.0f) {
247 0 : palette256_tune_saturation_mean_max = -1.0f;
248 : } else {
249 37 : palette256_tune_saturation_mean_max = clamp_float(palette256_tune_saturation_mean_max, 0.0f, 1.0f);
250 : }
251 :
252 37 : if (palette256_tune_speed_max < -1) {
253 0 : palette256_tune_speed_max = -1;
254 : }
255 37 : if (palette256_tune_quality_min_floor < -1) {
256 0 : palette256_tune_quality_min_floor = -1;
257 : }
258 37 : if (palette256_tune_quality_max_target < -1) {
259 0 : palette256_tune_quality_max_target = -1;
260 : }
261 :
262 37 : opts->bridge.optimization_level = level;
263 37 : opts->bridge.strip_safe = strip_safe;
264 37 : opts->bridge.optimize_alpha = optimize_alpha;
265 37 : opts->lossy_enable = lossy_enable;
266 37 : opts->lossy_type = lossy_type;
267 37 : opts->lossy_max_colors = lossy_max_colors;
268 37 : opts->lossy_reduced_colors = lossy_reduced_colors;
269 37 : opts->lossy_reduced_bits_rgb = lossy_reduced_bits_rgb;
270 37 : opts->lossy_reduced_alpha_bits = lossy_reduced_bits_alpha;
271 37 : opts->lossy_quality_min = lossy_quality_min;
272 37 : opts->lossy_quality_max = lossy_quality_max;
273 37 : opts->lossy_speed = lossy_speed;
274 37 : opts->lossy_dither_level = lossy_dither_level;
275 37 : opts->lossy_dither_auto = lossy_dither_auto;
276 37 : opts->saliency_map_enable = saliency_map_enable;
277 37 : opts->chroma_anchor_enable = chroma_anchor_enable;
278 37 : opts->adaptive_dither_enable = adaptive_dither_enable;
279 37 : opts->gradient_boost_enable = gradient_boost_enable;
280 37 : opts->chroma_weight_enable = chroma_weight_enable;
281 37 : opts->postprocess_smooth_enable = postprocess_smooth_enable;
282 37 : opts->postprocess_smooth_importance_cutoff = postprocess_smooth_importance_cutoff;
283 37 : opts->palette256_gradient_profile_enable = palette256_gradient_profile_enable;
284 37 : opts->palette256_gradient_profile_dither_floor = palette256_gradient_profile_dither_floor;
285 37 : opts->palette256_alpha_bleed_enable = palette256_alpha_bleed_enable;
286 37 : opts->palette256_alpha_bleed_max_distance = palette256_alpha_bleed_max_distance;
287 37 : opts->palette256_alpha_bleed_opaque_threshold = palette256_alpha_bleed_opaque_threshold;
288 37 : opts->palette256_alpha_bleed_soft_limit = palette256_alpha_bleed_soft_limit;
289 37 : opts->palette256_profile_opaque_ratio_threshold = palette256_profile_opaque_ratio_threshold;
290 37 : opts->palette256_profile_gradient_mean_max = palette256_profile_gradient_mean_max;
291 37 : opts->palette256_profile_saturation_mean_max = palette256_profile_saturation_mean_max;
292 37 : opts->palette256_tune_opaque_ratio_threshold = palette256_tune_opaque_ratio_threshold;
293 37 : opts->palette256_tune_gradient_mean_max = palette256_tune_gradient_mean_max;
294 37 : opts->palette256_tune_saturation_mean_max = palette256_tune_saturation_mean_max;
295 37 : opts->palette256_tune_speed_max = palette256_tune_speed_max;
296 37 : opts->palette256_tune_quality_min_floor = palette256_tune_quality_min_floor;
297 37 : opts->palette256_tune_quality_max_target = palette256_tune_quality_max_target;
298 37 : opts->thread_count = thread_count;
299 : }
300 :
301 53 : bool pngx_run_lossless_optimization(const uint8_t *png_data, size_t png_size, const pngx_options_t *opts, uint8_t **out_data, size_t *out_size) {
302 : PngxBridgeLosslessOptions lossless;
303 : PngxBridgeResult result;
304 :
305 53 : if (!png_data || png_size == 0 || !opts || !out_data || !out_size) {
306 0 : return false;
307 : }
308 :
309 53 : *out_data = NULL;
310 53 : *out_size = 0;
311 :
312 53 : memset(&lossless, 0, sizeof(lossless));
313 53 : lossless.optimization_level = opts->bridge.optimization_level;
314 53 : lossless.strip_safe = opts->bridge.strip_safe;
315 53 : lossless.optimize_alpha = opts->bridge.optimize_alpha;
316 53 : result = pngx_bridge_optimize_lossless(png_data, png_size, out_data, out_size, &lossless);
317 53 : pngx_set_last_error((int)result);
318 :
319 53 : if (result != PNGX_BRIDGE_RESULT_SUCCESS) {
320 0 : return false;
321 : }
322 :
323 53 : return true;
324 : }
325 :
326 31 : bool pngx_should_attempt_quantization(const pngx_options_t *opts) {
327 31 : if (!opts) {
328 0 : return false;
329 : }
330 :
331 31 : return opts->lossy_enable;
332 : }
333 :
334 22 : bool pngx_quantization_better(size_t baseline_size, size_t candidate_size) { return candidate_size == 0 ? false : (baseline_size == 0 ? true : candidate_size < baseline_size); }
|