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 <stdint.h>
16 : #include <stdlib.h>
17 : #include <string.h>
18 :
19 : #include "internal/log.h"
20 : #include "internal/png.h"
21 : #include "internal/pngx_common.h"
22 : #include "internal/simd.h"
23 : #include "internal/threads.h"
24 :
25 : typedef struct {
26 : uint64_t r_sum;
27 : uint64_t g_sum;
28 : uint64_t b_sum;
29 : uint64_t a_sum;
30 : uint32_t count;
31 : float score;
32 : float importance_accum;
33 : } chroma_bucket_t;
34 :
35 : typedef struct {
36 : uint8_t *rgba;
37 : size_t pixel_count;
38 : uint8_t bits_rgb;
39 : uint8_t bits_alpha;
40 : } snap_rgba_parallel_ctx_t;
41 :
42 10 : static void snap_rgba_parallel_worker(void *context, uint32_t start, uint32_t end) {
43 10 : snap_rgba_parallel_ctx_t *ctx = (snap_rgba_parallel_ctx_t *)context;
44 : uint8_t r, g, b, a;
45 : size_t base, i;
46 :
47 10 : if (!ctx || !ctx->rgba) {
48 0 : return;
49 : }
50 :
51 1181966 : for (i = (size_t)start; i < (size_t)end && i < ctx->pixel_count; ++i) {
52 1181956 : base = i * 4;
53 1181956 : r = ctx->rgba[base + 0];
54 1181956 : g = ctx->rgba[base + 1];
55 1181956 : b = ctx->rgba[base + 2];
56 1181956 : a = ctx->rgba[base + 3];
57 :
58 1181956 : snap_rgba_to_bits(&r, &g, &b, &a, ctx->bits_rgb, ctx->bits_alpha);
59 :
60 1181956 : ctx->rgba[base + 0] = r;
61 1181956 : ctx->rgba[base + 1] = g;
62 1181956 : ctx->rgba[base + 2] = b;
63 1181956 : ctx->rgba[base + 3] = a;
64 : }
65 : }
66 :
67 17655426 : static inline float absf(float value) { return (value < 0.0f) ? -value : value; }
68 :
69 1610113 : static inline uint32_t chroma_bucket_index(uint8_t r, uint8_t g, uint8_t b) {
70 3220226 : return (((uint32_t)r >> PNGX_CHROMA_BUCKET_SHIFT) * PNGX_CHROMA_BUCKET_DIM * PNGX_CHROMA_BUCKET_DIM) + (((uint32_t)g >> PNGX_CHROMA_BUCKET_SHIFT) * PNGX_CHROMA_BUCKET_DIM) +
71 1610113 : ((uint32_t)b >> PNGX_CHROMA_BUCKET_SHIFT);
72 : }
73 :
74 8838346 : static inline float calc_saturation(uint8_t r, uint8_t g, uint8_t b) {
75 8838346 : uint8_t max_v = r, min_v = r;
76 :
77 8838346 : if (g > max_v) {
78 4499698 : max_v = g;
79 : }
80 8838346 : if (b > max_v) {
81 2207885 : max_v = b;
82 : }
83 8838346 : if (g < min_v) {
84 4220969 : min_v = g;
85 : }
86 8838346 : if (b < min_v) {
87 3088019 : min_v = b;
88 : }
89 :
90 8838346 : if (max_v == 0) {
91 36961 : return 0.0f;
92 : }
93 :
94 8801385 : return (float)(max_v - min_v) / (float)max_v;
95 : }
96 :
97 8838651 : static inline float calc_luma(uint8_t r, uint8_t g, uint8_t b) { return PNGX_COMMON_LUMA_R_COEFF * (float)r + PNGX_COMMON_LUMA_G_COEFF * (float)g + PNGX_COMMON_LUMA_B_COEFF * (float)b; }
98 :
99 36 : static inline bool decode_png_rgba(const uint8_t *png_data, size_t png_size, uint8_t **rgba, png_uint_32 *width, png_uint_32 *height) {
100 : cpres_error_t status;
101 :
102 36 : if (!png_data || png_size == 0 || !rgba || !width || !height) {
103 0 : return false;
104 : }
105 :
106 36 : status = png_decode_from_memory(png_data, png_size, rgba, width, height);
107 36 : if (status != CPRES_OK) {
108 0 : colopresso_log(CPRES_LOG_LEVEL_WARNING, "PNGX: Failed to decode PNG (%d)", (int)status);
109 0 : return false;
110 : }
111 :
112 36 : return true;
113 : }
114 :
115 28 : static inline void extract_chroma_anchors(pngx_quant_support_t *support, chroma_bucket_t *buckets, size_t bucket_count, size_t pixel_count) {
116 : cpres_rgba_color_t chosen[PNGX_MAX_DERIVED_COLORS];
117 : chroma_bucket_t *bucket;
118 : size_t selected, best_index, i, dst, auto_limit, scaled, check;
119 : float best_score, score, importance_boost;
120 : bool duplicate;
121 :
122 28 : if (!support || !buckets || bucket_count == 0) {
123 4 : return;
124 : }
125 :
126 28 : if (pixel_count > 0) {
127 28 : scaled = pixel_count / (size_t)PNGX_COMMON_ANCHOR_SCALE_DIVISOR;
128 28 : if (scaled < (size_t)PNGX_COMMON_ANCHOR_SCALE_MIN) {
129 18 : scaled = (size_t)PNGX_COMMON_ANCHOR_SCALE_MIN;
130 : }
131 28 : if (scaled > PNGX_MAX_DERIVED_COLORS) {
132 7 : scaled = PNGX_MAX_DERIVED_COLORS;
133 : }
134 28 : auto_limit = scaled;
135 : } else {
136 0 : auto_limit = (size_t)PNGX_COMMON_ANCHOR_AUTO_LIMIT_DEFAULT;
137 : }
138 :
139 28 : selected = 0;
140 551 : while (selected < auto_limit) {
141 540 : best_score = 0.0f;
142 540 : best_index = SIZE_MAX;
143 2212380 : for (i = 0; i < bucket_count; ++i) {
144 2211840 : bucket = &buckets[i];
145 2211840 : if (!bucket->count || bucket->score <= 0.0f) {
146 1878441 : continue;
147 : }
148 333399 : importance_boost = bucket->importance_accum * PNGX_COMMON_ANCHOR_IMPORTANCE_FACTOR;
149 333399 : if (importance_boost > PNGX_COMMON_ANCHOR_IMPORTANCE_THRESHOLD) {
150 298762 : importance_boost = PNGX_COMMON_ANCHOR_IMPORTANCE_BOOST_BASE + (importance_boost - PNGX_COMMON_ANCHOR_IMPORTANCE_THRESHOLD) * PNGX_COMMON_ANCHOR_IMPORTANCE_BOOST_SCALE;
151 : }
152 333399 : score = bucket->score + importance_boost;
153 333399 : if (bucket->count < PNGX_COMMON_ANCHOR_LOW_COUNT_THRESHOLD) {
154 44141 : score *= PNGX_COMMON_ANCHOR_LOW_COUNT_PENALTY;
155 : }
156 333399 : if (score > best_score) {
157 5573 : best_score = score;
158 5573 : best_index = i;
159 : }
160 : }
161 540 : if (best_index == SIZE_MAX || best_score < PNGX_COMMON_ANCHOR_SCORE_THRESHOLD) {
162 : break;
163 : }
164 :
165 523 : chosen[selected].r = (uint8_t)(buckets[best_index].r_sum / buckets[best_index].count);
166 523 : chosen[selected].g = (uint8_t)(buckets[best_index].g_sum / buckets[best_index].count);
167 523 : chosen[selected].b = (uint8_t)(buckets[best_index].b_sum / buckets[best_index].count);
168 523 : chosen[selected].a = (uint8_t)(buckets[best_index].a_sum / buckets[best_index].count);
169 523 : buckets[best_index].score = 0.0f;
170 523 : ++selected;
171 : }
172 :
173 28 : if (!selected) {
174 4 : return;
175 : }
176 :
177 24 : dst = 0;
178 547 : for (i = 0; i < selected; ++i) {
179 523 : duplicate = false;
180 3849 : for (check = 0; check < dst; ++check) {
181 3612 : if (color_distance_sq(&chosen[i], &chosen[check]) < PNGX_COMMON_ANCHOR_DISTANCE_SQ_THRESHOLD) {
182 286 : duplicate = true;
183 286 : break;
184 : }
185 : }
186 523 : if (duplicate) {
187 286 : continue;
188 : }
189 237 : chosen[dst] = chosen[i];
190 237 : ++dst;
191 : }
192 :
193 24 : if (!dst) {
194 0 : return;
195 : }
196 :
197 24 : support->derived_colors = (cpres_rgba_color_t *)malloc(sizeof(cpres_rgba_color_t) * dst);
198 24 : if (!support->derived_colors) {
199 0 : return;
200 : }
201 24 : memcpy(support->derived_colors, chosen, sizeof(cpres_rgba_color_t) * dst);
202 24 : support->derived_colors_len = dst;
203 : }
204 :
205 33 : void image_stats_reset(pngx_image_stats_t *stats) {
206 33 : if (!stats) {
207 0 : return;
208 : }
209 :
210 33 : stats->gradient_mean = 0.0f;
211 33 : stats->gradient_max = 0.0f;
212 33 : stats->saturation_mean = 0.0f;
213 33 : stats->opaque_ratio = 0.0f;
214 33 : stats->translucent_ratio = 0.0f;
215 33 : stats->vibrant_ratio = 0.0f;
216 : }
217 :
218 33 : void quant_support_reset(pngx_quant_support_t *support) {
219 33 : if (!support) {
220 0 : return;
221 : }
222 :
223 33 : free(support->importance_map);
224 33 : support->importance_map = NULL;
225 33 : support->importance_map_len = 0;
226 :
227 33 : free(support->derived_colors);
228 33 : support->derived_colors = NULL;
229 33 : support->derived_colors_len = 0;
230 :
231 33 : free(support->combined_fixed_colors);
232 33 : support->combined_fixed_colors = NULL;
233 33 : support->combined_fixed_len = 0;
234 :
235 33 : free(support->bit_hint_map);
236 33 : support->bit_hint_map = NULL;
237 33 : support->bit_hint_len = 0;
238 : }
239 :
240 8 : const char *lossy_type_label(uint8_t lossy_type) {
241 8 : switch (lossy_type) {
242 3 : case PNGX_LOSSY_TYPE_LIMITED_RGBA4444:
243 3 : return "Limited RGBA4444";
244 5 : case PNGX_LOSSY_TYPE_REDUCED_RGBA32:
245 5 : return "Reduced RGBA32";
246 0 : default:
247 0 : return "Palette256";
248 : }
249 : }
250 :
251 36 : void rgba_image_reset(pngx_rgba_image_t *image) {
252 36 : if (!image) {
253 0 : return;
254 : }
255 :
256 36 : free(image->rgba);
257 36 : image->rgba = NULL;
258 36 : image->width = 0;
259 36 : image->height = 0;
260 36 : image->pixel_count = 0;
261 : }
262 :
263 36 : bool load_rgba_image(const uint8_t *png_data, size_t png_size, pngx_rgba_image_t *image) {
264 36 : if (!png_data || png_size == 0 || !image) {
265 0 : return false;
266 : }
267 :
268 36 : image->rgba = NULL;
269 36 : image->width = 0;
270 36 : image->height = 0;
271 36 : image->pixel_count = 0;
272 :
273 36 : if (!decode_png_rgba(png_data, png_size, &image->rgba, &image->width, &image->height)) {
274 0 : rgba_image_reset(image);
275 0 : return false;
276 : }
277 :
278 36 : if (!image->rgba || image->width == 0 || image->height == 0) {
279 0 : rgba_image_reset(image);
280 0 : return false;
281 : }
282 :
283 36 : if ((size_t)image->width > SIZE_MAX / (size_t)image->height) {
284 0 : rgba_image_reset(image);
285 0 : return false;
286 : }
287 :
288 36 : image->pixel_count = (size_t)image->width * (size_t)image->height;
289 :
290 36 : return true;
291 : }
292 :
293 7411755 : uint8_t clamp_reduced_bits(uint8_t bits) {
294 7411755 : const uint8_t min_bits = (uint8_t)COLOPRESSO_PNGX_REDUCED_BITS_MIN, max_bits = (uint8_t)COLOPRESSO_PNGX_REDUCED_BITS_MAX;
295 :
296 7411755 : return bits < min_bits ? min_bits : (bits > max_bits ? max_bits : bits);
297 : }
298 :
299 30516067 : uint8_t quantize_channel_value(float value, uint8_t bits_per_channel) {
300 : uint32_t levels;
301 : float clamped, scaled, rounded, quantized;
302 :
303 30516067 : if (bits_per_channel >= 8) {
304 0 : return value < 0.0f ? 0 : (value > 255.0f ? 255 : (uint8_t)(value + 0.5f));
305 : }
306 :
307 30516067 : if (bits_per_channel < 1) {
308 0 : bits_per_channel = 1;
309 : }
310 :
311 30516067 : levels = 1 << bits_per_channel;
312 :
313 30516067 : clamped = value;
314 30516067 : if (clamped < 0.0f) {
315 539 : clamped = 0.0f;
316 30515528 : } else if (clamped > 255.0f) {
317 1243 : clamped = 255.0f;
318 : }
319 :
320 30516067 : if (levels <= 1) {
321 0 : return 0;
322 : }
323 :
324 30516067 : scaled = clamped * (float)(levels - 1) / 255.0f;
325 30516067 : if (scaled < 0.0f) {
326 0 : scaled = 0.0f;
327 : }
328 30516067 : if (scaled > (float)(levels - 1)) {
329 0 : scaled = (float)(levels - 1);
330 : }
331 :
332 30516067 : rounded = (float)((int32_t)(scaled + 0.5f));
333 :
334 30516067 : quantized = rounded * 255.0f / (float)(levels - 1);
335 30516067 : if (quantized < 0.0f) {
336 0 : quantized = 0.0f;
337 30516067 : } else if (quantized > 255.0f) {
338 0 : quantized = 255.0f;
339 : }
340 :
341 30516067 : return (uint8_t)(quantized + 0.5f);
342 : }
343 :
344 14622960 : uint8_t quantize_bits(uint8_t value, uint8_t bits) {
345 14622960 : if (bits >= 8) {
346 407949 : return value;
347 : }
348 :
349 14215011 : return quantize_channel_value((float)value, bits);
350 : }
351 :
352 1197400 : void snap_rgba_to_bits(uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a, uint8_t bits_rgb, uint8_t bits_alpha) {
353 1197400 : uint8_t rgb = clamp_reduced_bits(bits_rgb);
354 :
355 1197400 : if (r) {
356 1197400 : *r = quantize_bits(*r, rgb);
357 : }
358 1197400 : if (g) {
359 1197400 : *g = quantize_bits(*g, rgb);
360 : }
361 1197400 : if (b) {
362 1197400 : *b = quantize_bits(*b, rgb);
363 : }
364 1197400 : if (a) {
365 1197400 : *a = quantize_bits(*a, clamp_reduced_bits(bits_alpha));
366 : }
367 1197400 : }
368 :
369 10 : void snap_rgba_image_to_bits(uint32_t thread_count, uint8_t *rgba, size_t pixel_count, uint8_t bits_rgb, uint8_t bits_alpha) {
370 : snap_rgba_parallel_ctx_t ctx;
371 :
372 10 : if (!rgba || pixel_count == 0) {
373 0 : return;
374 : }
375 :
376 10 : if (clamp_reduced_bits(bits_rgb) >= 8 && clamp_reduced_bits(bits_alpha) >= 8) {
377 0 : return;
378 : }
379 :
380 10 : ctx.rgba = rgba;
381 10 : ctx.pixel_count = pixel_count;
382 10 : ctx.bits_rgb = bits_rgb;
383 10 : ctx.bits_alpha = bits_alpha;
384 :
385 : #if COLOPRESSO_ENABLE_THREADS
386 10 : colopresso_parallel_for(thread_count, (uint32_t)pixel_count, snap_rgba_parallel_worker, &ctx);
387 : #else
388 : snap_rgba_parallel_worker(&ctx, 0, (uint32_t)pixel_count);
389 : #endif
390 : }
391 :
392 4983 : uint32_t color_distance_sq(const cpres_rgba_color_t *lhs, const cpres_rgba_color_t *rhs) {
393 : uint32_t lhs_packed, rhs_packed;
394 :
395 4983 : if (!lhs || !rhs) {
396 0 : return 0;
397 : }
398 :
399 4983 : lhs_packed = (uint32_t)lhs->r | ((uint32_t)lhs->g << 8) | ((uint32_t)lhs->b << 16) | ((uint32_t)lhs->a << 24);
400 4983 : rhs_packed = (uint32_t)rhs->r | ((uint32_t)rhs->g << 8) | ((uint32_t)rhs->b << 16) | ((uint32_t)rhs->a << 24);
401 :
402 4983 : return simd_color_distance_sq_u32(lhs_packed, rhs_packed);
403 : }
404 :
405 7 : float estimate_bitdepth_dither_level(const uint8_t *rgba, png_uint_32 width, png_uint_32 height, uint8_t bits_per_channel) {
406 : png_uint_32 y, x;
407 : uint8_t r, g, b, a;
408 : size_t pixel_count, gradient_samples, opaque_pixels, translucent_pixels, base, right, below;
409 : float base_level, target, right_luma, below_luma, luma, normalized_gradient, opaque_ratio, translucent_ratio, min_luma, max_luma, coverage, gradient_span;
410 : double gradient_accum;
411 :
412 7 : if (!rgba || width == 0 || height == 0) {
413 2 : return clamp_float(COLOPRESSO_PNGX_DEFAULT_LOSSY_DITHER_LEVEL, 0.0f, 1.0f);
414 : }
415 :
416 5 : pixel_count = (size_t)width * (size_t)height;
417 5 : gradient_accum = 0.0;
418 5 : gradient_samples = 0;
419 5 : opaque_pixels = 0;
420 5 : translucent_pixels = 0;
421 5 : min_luma = 255.0f;
422 5 : max_luma = 0.0f;
423 :
424 26 : for (y = 0; y < height; ++y) {
425 134 : for (x = 0; x < width; ++x) {
426 113 : base = (((size_t)y * (size_t)width) + (size_t)x) * 4;
427 113 : r = rgba[base + 0];
428 113 : g = rgba[base + 1];
429 113 : b = rgba[base + 2];
430 113 : a = rgba[base + 3];
431 113 : luma = calc_luma(r, g, b);
432 :
433 113 : if (luma < min_luma) {
434 6 : min_luma = luma;
435 : }
436 113 : if (luma > max_luma) {
437 71 : max_luma = luma;
438 : }
439 :
440 113 : if (a > PNGX_COMMON_DITHER_ALPHA_OPAQUE_THRESHOLD) {
441 97 : ++opaque_pixels;
442 16 : } else if (a > PNGX_COMMON_DITHER_ALPHA_TRANSLUCENT_THRESHOLD) {
443 12 : ++translucent_pixels;
444 : }
445 :
446 113 : if (x + 1 < width) {
447 92 : right = base + 4;
448 92 : right_luma = calc_luma(rgba[right + 0], rgba[right + 1], rgba[right + 2]);
449 92 : gradient_accum += absf(right_luma - luma);
450 92 : ++gradient_samples;
451 : }
452 :
453 113 : if (y + 1 < height) {
454 92 : below = (((size_t)(y + 1) * (size_t)width) + (size_t)x) * 4;
455 92 : below_luma = calc_luma(rgba[below + 0], rgba[below + 1], rgba[below + 2]);
456 92 : gradient_accum += absf(below_luma - luma);
457 92 : ++gradient_samples;
458 : }
459 : }
460 : }
461 :
462 5 : if (gradient_samples == 0) {
463 1 : gradient_samples = 1;
464 : }
465 :
466 5 : normalized_gradient = (float)(gradient_accum / ((double)gradient_samples * 255.0));
467 5 : opaque_ratio = (pixel_count > 0) ? (float)((double)opaque_pixels / (double)pixel_count) : 0.0f;
468 5 : translucent_ratio = (pixel_count > 0) ? (float)((double)translucent_pixels / (double)pixel_count) : 0.0f;
469 :
470 5 : coverage = (max_luma - min_luma) / 255.0f;
471 5 : if (coverage < 0.0f) {
472 0 : coverage = 0.0f;
473 5 : } else if (coverage > 1.0f) {
474 0 : coverage = 1.0f;
475 : }
476 :
477 5 : gradient_span = coverage / ((normalized_gradient > PNGX_COMMON_DITHER_GRADIENT_MIN) ? normalized_gradient : PNGX_COMMON_DITHER_GRADIENT_MIN);
478 :
479 5 : base_level = PNGX_COMMON_DITHER_BASE_LEVEL;
480 5 : target = base_level;
481 :
482 5 : if (normalized_gradient > 0.35f) {
483 0 : target += PNGX_COMMON_DITHER_HIGH_GRADIENT_BOOST;
484 5 : } else if (normalized_gradient > 0.2f) {
485 1 : target += PNGX_COMMON_DITHER_MID_GRADIENT_BOOST;
486 4 : } else if (normalized_gradient < 0.08f) {
487 4 : target -= PNGX_COMMON_DITHER_LOW_GRADIENT_CUT;
488 0 : } else if (normalized_gradient < 0.15f) {
489 0 : target -= PNGX_COMMON_DITHER_MID_LOW_GRADIENT_CUT;
490 : }
491 :
492 5 : if (opaque_ratio < 0.35f) {
493 1 : target -= PNGX_COMMON_DITHER_OPAQUE_LOW_CUT;
494 4 : } else if (opaque_ratio > 0.9f) {
495 4 : target += PNGX_COMMON_DITHER_OPAQUE_HIGH_BOOST;
496 : }
497 :
498 5 : if (translucent_ratio > 0.3f) {
499 1 : target -= PNGX_COMMON_DITHER_TRANSLUCENT_CUT;
500 : }
501 :
502 5 : if (coverage > PNGX_COMMON_DITHER_COVERAGE_THRESHOLD && gradient_span > PNGX_COMMON_DITHER_SPAN_THRESHOLD) {
503 2 : if (target < PNGX_COMMON_DITHER_TARGET_CAP) {
504 2 : target = PNGX_COMMON_DITHER_TARGET_CAP;
505 : }
506 2 : if (bits_per_channel <= 4 && target < PNGX_COMMON_DITHER_TARGET_CAP_LOW_BIT) {
507 0 : target = PNGX_COMMON_DITHER_TARGET_CAP_LOW_BIT;
508 : }
509 : }
510 :
511 5 : if (bits_per_channel <= 2) {
512 1 : target += PNGX_COMMON_DITHER_LOW_BIT_BOOST;
513 1 : if (normalized_gradient > 0.25f) {
514 0 : target += PNGX_COMMON_DITHER_LOW_BIT_GRADIENT_BOOST;
515 : }
516 : }
517 :
518 5 : return clamp_float(target, PNGX_COMMON_DITHER_MIN, PNGX_COMMON_DITHER_MAX);
519 : }
520 :
521 1 : float estimate_bitdepth_dither_level_limited4444(const uint8_t *rgba, png_uint_32 width, png_uint_32 height) {
522 : uint8_t r, g, b, a;
523 : size_t pixel_count, gradient_samples, opaque_pixels, translucent_pixels, base, right, below;
524 : float right_luma, below_luma, luma, normalized_gradient, opaque_ratio, translucent_ratio, min_luma, max_luma, coverage, gradient_span, target;
525 : double gradient_accum;
526 : png_uint_32 y, x;
527 :
528 1 : if (!rgba || width == 0 || height == 0) {
529 0 : return 0.0f;
530 : }
531 :
532 1 : pixel_count = (size_t)width * (size_t)height;
533 1 : gradient_accum = 0.0;
534 1 : gradient_samples = 0;
535 1 : opaque_pixels = 0;
536 1 : translucent_pixels = 0;
537 1 : min_luma = 255.0f;
538 1 : max_luma = 0.0f;
539 :
540 3 : for (y = 0; y < height; ++y) {
541 6 : for (x = 0; x < width; ++x) {
542 4 : base = (((size_t)y * (size_t)width) + (size_t)x) * 4;
543 4 : r = rgba[base + 0];
544 4 : g = rgba[base + 1];
545 4 : b = rgba[base + 2];
546 4 : a = rgba[base + 3];
547 4 : luma = calc_luma(r, g, b);
548 :
549 4 : if (luma < min_luma) {
550 1 : min_luma = luma;
551 : }
552 4 : if (luma > max_luma) {
553 0 : max_luma = luma;
554 : }
555 :
556 4 : if (a > PNGX_COMMON_DITHER_ALPHA_OPAQUE_THRESHOLD) {
557 4 : ++opaque_pixels;
558 0 : } else if (a > PNGX_COMMON_DITHER_ALPHA_TRANSLUCENT_THRESHOLD) {
559 0 : ++translucent_pixels;
560 : }
561 :
562 4 : if (x + 1 < width) {
563 2 : right = base + 4;
564 2 : right_luma = calc_luma(rgba[right + 0], rgba[right + 1], rgba[right + 2]);
565 2 : gradient_accum += absf(right_luma - luma);
566 2 : ++gradient_samples;
567 : }
568 :
569 4 : if (y + 1 < height) {
570 2 : below = (((size_t)(y + 1) * (size_t)width) + (size_t)x) * 4;
571 2 : below_luma = calc_luma(rgba[below + 0], rgba[below + 1], rgba[below + 2]);
572 2 : gradient_accum += absf(below_luma - luma);
573 2 : ++gradient_samples;
574 : }
575 : }
576 : }
577 :
578 1 : if (gradient_samples == 0) {
579 0 : gradient_samples = 1;
580 : }
581 :
582 1 : normalized_gradient = (float)(gradient_accum / ((double)gradient_samples * 255.0));
583 1 : opaque_ratio = (pixel_count > 0) ? (float)((double)opaque_pixels / (double)pixel_count) : 0.0f;
584 1 : translucent_ratio = (pixel_count > 0) ? (float)((double)translucent_pixels / (double)pixel_count) : 0.0f;
585 :
586 1 : coverage = (max_luma - min_luma) / 255.0f;
587 1 : if (coverage < 0.0f) {
588 0 : coverage = 0.0f;
589 1 : } else if (coverage > 1.0f) {
590 0 : coverage = 1.0f;
591 : }
592 :
593 1 : gradient_span = coverage / ((normalized_gradient > PNGX_COMMON_DITHER_GRADIENT_MIN) ? normalized_gradient : PNGX_COMMON_DITHER_GRADIENT_MIN);
594 :
595 1 : target = 0.0f;
596 :
597 1 : if (coverage > PNGX_COMMON_DITHER_COVERAGE_THRESHOLD && gradient_span > PNGX_COMMON_DITHER_SPAN_THRESHOLD && normalized_gradient < 0.20f) {
598 0 : target = 0.55f;
599 : }
600 :
601 1 : if (translucent_ratio > 0.3f) {
602 0 : target *= 0.5f;
603 : }
604 :
605 1 : if (opaque_ratio > 0.9f) {
606 1 : target += 0.05f;
607 : }
608 :
609 1 : return clamp_float(target, 0.0f, 1.0f);
610 : }
611 :
612 22 : void build_fixed_palette(const pngx_options_t *source_opts, pngx_quant_support_t *support, pngx_options_t *patched_opts) {
613 : size_t user_count, derived_count, total_cap, i, j;
614 : bool duplicate;
615 :
616 22 : if (!source_opts || !support || !patched_opts) {
617 0 : return;
618 : }
619 :
620 22 : *patched_opts = *source_opts;
621 22 : user_count = (source_opts->protected_colors && source_opts->protected_colors_count > 0) ? (size_t)source_opts->protected_colors_count : 0;
622 22 : derived_count = support->derived_colors_len;
623 22 : if (derived_count == 0) {
624 3 : return;
625 : }
626 :
627 19 : total_cap = user_count + derived_count;
628 19 : support->combined_fixed_colors = (cpres_rgba_color_t *)malloc(sizeof(cpres_rgba_color_t) * total_cap);
629 19 : if (!support->combined_fixed_colors) {
630 0 : return;
631 : }
632 :
633 19 : if (user_count > 0 && source_opts->protected_colors) {
634 1 : memcpy(support->combined_fixed_colors, source_opts->protected_colors, sizeof(cpres_rgba_color_t) * user_count);
635 : }
636 19 : support->combined_fixed_len = user_count;
637 183 : for (i = 0; i < derived_count; ++i) {
638 164 : duplicate = false;
639 1534 : for (j = 0; j < support->combined_fixed_len; ++j) {
640 1370 : if (color_distance_sq(&support->derived_colors[i], &support->combined_fixed_colors[j]) < PNGX_COMMON_FIXED_PALETTE_DISTANCE_SQ) {
641 0 : duplicate = true;
642 0 : break;
643 : }
644 : }
645 164 : if (duplicate) {
646 0 : continue;
647 : }
648 164 : if (support->combined_fixed_len < total_cap) {
649 164 : support->combined_fixed_colors[support->combined_fixed_len] = support->derived_colors[i];
650 164 : ++support->combined_fixed_len;
651 : }
652 164 : if (support->combined_fixed_len >= PNGX_COMMON_FIXED_PALETTE_MAX) {
653 0 : break;
654 : }
655 : }
656 19 : if (support->combined_fixed_len > user_count) {
657 19 : patched_opts->protected_colors = support->combined_fixed_colors;
658 19 : patched_opts->protected_colors_count = (int32_t)support->combined_fixed_len;
659 : }
660 : }
661 :
662 25 : float resolve_quant_dither(const pngx_options_t *opts, const pngx_image_stats_t *stats) {
663 : float resolved, gradient_mean, saturation_mean, opaque_ratio, vibrant_ratio, gradient_max;
664 :
665 25 : if (!opts) {
666 0 : return 0.5f;
667 : }
668 :
669 25 : if (stats) {
670 25 : gradient_mean = stats->gradient_mean;
671 25 : saturation_mean = stats->saturation_mean;
672 25 : opaque_ratio = stats->opaque_ratio;
673 25 : vibrant_ratio = stats->vibrant_ratio;
674 25 : gradient_max = stats->gradient_max;
675 : } else {
676 0 : gradient_mean = PNGX_COMMON_RESOLVE_DEFAULT_GRADIENT;
677 0 : saturation_mean = PNGX_COMMON_RESOLVE_DEFAULT_SATURATION;
678 0 : opaque_ratio = PNGX_COMMON_RESOLVE_DEFAULT_OPAQUE;
679 0 : vibrant_ratio = PNGX_COMMON_RESOLVE_DEFAULT_VIBRANT;
680 0 : gradient_max = PNGX_COMMON_RESOLVE_DEFAULT_GRADIENT;
681 : }
682 :
683 25 : resolved = opts->lossy_dither_level;
684 :
685 25 : if (opts->lossy_dither_auto) {
686 1 : resolved = PNGX_COMMON_RESOLVE_AUTO_BASE + gradient_mean * PNGX_COMMON_RESOLVE_AUTO_GRADIENT_WEIGHT + saturation_mean * PNGX_COMMON_RESOLVE_AUTO_SATURATION_WEIGHT;
687 1 : if (opaque_ratio < 0.7f) {
688 1 : resolved -= PNGX_COMMON_RESOLVE_AUTO_OPAQUE_CUT;
689 : }
690 : }
691 :
692 25 : if (opts->adaptive_dither_enable) {
693 24 : if (gradient_mean < 0.10f) {
694 24 : resolved -= PNGX_COMMON_RESOLVE_ADAPTIVE_FLAT_CUT;
695 0 : } else if (gradient_mean > 0.30f) {
696 0 : resolved += PNGX_COMMON_RESOLVE_ADAPTIVE_GRADIENT_BOOST;
697 : }
698 24 : if (gradient_max > 0.5f && vibrant_ratio > 0.12f) {
699 0 : resolved -= PNGX_COMMON_RESOLVE_ADAPTIVE_VIBRANT_CUT;
700 : }
701 24 : if (saturation_mean > 0.38f) {
702 7 : resolved += PNGX_COMMON_RESOLVE_ADAPTIVE_SATURATION_BOOST;
703 17 : } else if (saturation_mean < 0.12f) {
704 3 : resolved -= PNGX_COMMON_RESOLVE_ADAPTIVE_SATURATION_CUT;
705 : }
706 : }
707 :
708 25 : if (resolved < PNGX_COMMON_RESOLVE_MIN) {
709 7 : resolved = PNGX_COMMON_RESOLVE_MIN;
710 18 : } else if (resolved > PNGX_COMMON_RESOLVE_MAX) {
711 0 : resolved = PNGX_COMMON_RESOLVE_MAX;
712 : }
713 :
714 25 : return resolved;
715 : }
716 :
717 33 : bool prepare_quant_support(const pngx_rgba_image_t *image, const pngx_options_t *opts, pngx_quant_support_t *support, pngx_image_stats_t *stats) {
718 33 : chroma_bucket_t *buckets = NULL, *bucket_entry;
719 : uint32_t x, y, range, sample;
720 33 : uint16_t *importance_work = NULL, raw_min, raw_max;
721 : uint8_t r, g, b, a, value;
722 : size_t pixel_index, base, next_row_base, opaque_pixels, translucent_pixels, vibrant_pixels;
723 33 : float gradient_sum, saturation_sum, luma, gradient, saturation, importance, alpha_factor, anchor_score, right_luma, below_luma, importance_mix, *luma_row_curr = NULL, *luma_row_next = NULL,
724 : *luma_row_tmp;
725 : bool need_map, need_buckets;
726 :
727 33 : if (!image || !opts || !support || !stats || image->pixel_count == 0) {
728 0 : return false;
729 : }
730 :
731 33 : raw_min = UINT16_MAX;
732 33 : raw_max = 0;
733 33 : need_map = opts->saliency_map_enable || opts->postprocess_smooth_enable;
734 33 : need_buckets = opts->chroma_anchor_enable;
735 33 : gradient_sum = 0.0f;
736 33 : saturation_sum = 0.0f;
737 33 : opaque_pixels = 0;
738 33 : translucent_pixels = 0;
739 33 : vibrant_pixels = 0;
740 33 : if (need_map) {
741 28 : importance_work = (uint16_t *)malloc(sizeof(uint16_t) * image->pixel_count);
742 28 : if (!importance_work) {
743 0 : return false;
744 : }
745 : }
746 :
747 33 : if (need_buckets) {
748 28 : buckets = (chroma_bucket_t *)calloc(PNGX_CHROMA_BUCKET_COUNT, sizeof(chroma_bucket_t));
749 28 : if (!buckets) {
750 0 : if (importance_work) {
751 0 : free(importance_work);
752 : }
753 :
754 0 : return false;
755 : }
756 : }
757 :
758 33 : luma_row_curr = (float *)malloc(sizeof(float) * image->width);
759 33 : luma_row_next = (float *)malloc(sizeof(float) * image->width);
760 33 : if (!luma_row_curr || !luma_row_next) {
761 0 : free(importance_work);
762 0 : free(buckets);
763 0 : free(luma_row_curr);
764 0 : free(luma_row_next);
765 :
766 0 : return false;
767 : }
768 :
769 11323 : for (x = 0; x < image->width; ++x) {
770 11290 : base = (size_t)x * 4;
771 11290 : luma_row_curr[x] = calc_luma(image->rgba[base + 0], image->rgba[base + 1], image->rgba[base + 2]) / 255.0f;
772 : }
773 :
774 10197 : for (y = 0; y < image->height; ++y) {
775 10164 : if (y + 1 < image->height) {
776 10131 : next_row_base = ((size_t)(y + 1) * (size_t)image->width) * 4;
777 8837187 : for (x = 0; x < image->width; ++x) {
778 8827056 : base = next_row_base + (size_t)x * 4;
779 8827056 : luma_row_next[x] = calc_luma(image->rgba[base + 0], image->rgba[base + 1], image->rgba[base + 2]) / 255.0f;
780 : }
781 : }
782 :
783 8848510 : for (x = 0; x < image->width; ++x) {
784 8838346 : base = (((size_t)y * (size_t)image->width) + (size_t)x) * 4;
785 8838346 : r = image->rgba[base + 0];
786 8838346 : g = image->rgba[base + 1];
787 8838346 : b = image->rgba[base + 2];
788 8838346 : a = image->rgba[base + 3];
789 8838346 : luma = luma_row_curr[x];
790 8838346 : gradient = 0.0f;
791 8838346 : saturation = calc_saturation(r, g, b);
792 8838346 : alpha_factor = (float)a / 255.0f;
793 :
794 8838346 : if (x + 1 < image->width) {
795 8828182 : right_luma = luma_row_curr[x + 1];
796 8828182 : gradient += absf(right_luma - luma);
797 : }
798 :
799 8838346 : if (y + 1 < image->height) {
800 8827056 : below_luma = luma_row_next[x];
801 8827056 : gradient += absf(below_luma - luma);
802 : }
803 :
804 8838346 : gradient *= PNGX_COMMON_PREPARE_GRADIENT_SCALE;
805 8838346 : if (gradient > 1.0f) {
806 0 : gradient = 1.0f;
807 : }
808 :
809 8838346 : gradient_sum += gradient;
810 8838346 : if (gradient > stats->gradient_max) {
811 735 : stats->gradient_max = gradient;
812 : }
813 :
814 8838346 : saturation_sum += saturation;
815 :
816 8838346 : if (a > PNGX_COMMON_DITHER_ALPHA_OPAQUE_THRESHOLD) {
817 250779 : ++opaque_pixels;
818 8587567 : } else if (a > PNGX_COMMON_DITHER_ALPHA_TRANSLUCENT_THRESHOLD) {
819 8478532 : ++translucent_pixels;
820 : }
821 :
822 8838346 : if (saturation > PNGX_COMMON_PREPARE_VIBRANT_SATURATION && gradient > PNGX_COMMON_PREPARE_VIBRANT_GRADIENT && a > PNGX_COMMON_PREPARE_VIBRANT_ALPHA) {
823 119460 : ++vibrant_pixels;
824 : }
825 :
826 8838346 : importance = gradient;
827 :
828 8838346 : if (opts->chroma_weight_enable) {
829 8767426 : importance += saturation * PNGX_COMMON_PREPARE_CHROMA_WEIGHT;
830 : }
831 :
832 8838346 : if (opts->gradient_boost_enable) {
833 8767426 : if (gradient > PNGX_COMMON_PREPARE_BOOST_THRESHOLD) {
834 7352 : importance += PNGX_COMMON_PREPARE_BOOST_BASE + (gradient * PNGX_COMMON_PREPARE_BOOST_FACTOR);
835 8760074 : } else if (gradient < PNGX_COMMON_PREPARE_CUT_THRESHOLD) {
836 8082901 : importance *= PNGX_COMMON_PREPARE_CUT_FACTOR;
837 : }
838 : }
839 :
840 8838346 : if (alpha_factor < PNGX_COMMON_PREPARE_ALPHA_THRESHOLD) {
841 7651423 : importance *= (PNGX_COMMON_PREPARE_ALPHA_BASE + alpha_factor * PNGX_COMMON_PREPARE_ALPHA_MULTIPLIER);
842 : }
843 :
844 8838346 : if (importance < 0.0f) {
845 0 : importance = 0.0f;
846 8838346 : } else if (importance > 1.0f) {
847 0 : importance = 1.0f;
848 : }
849 :
850 8838346 : if (need_map && importance_work) {
851 8771522 : pixel_index = ((size_t)y * (size_t)image->width) + (size_t)x;
852 8771522 : importance_work[pixel_index] = (uint16_t)(importance * PNGX_IMPORTANCE_SCALE + 0.5f);
853 :
854 8771522 : if (importance_work[pixel_index] < raw_min) {
855 865 : raw_min = importance_work[pixel_index];
856 : }
857 :
858 8771522 : if (importance_work[pixel_index] > raw_max) {
859 1213 : raw_max = importance_work[pixel_index];
860 : }
861 : }
862 :
863 8838346 : if (need_buckets && buckets) {
864 8767430 : if ((saturation > PNGX_COMMON_PREPARE_BUCKET_SATURATION || importance > PNGX_COMMON_PREPARE_BUCKET_IMPORTANCE) && a > PNGX_COMMON_PREPARE_BUCKET_ALPHA) {
865 2246399 : importance_mix = (importance * PNGX_COMMON_PREPARE_MIX_IMPORTANCE) + (gradient * PNGX_COMMON_PREPARE_MIX_GRADIENT);
866 2246399 : anchor_score = (saturation * PNGX_COMMON_PREPARE_ANCHOR_SATURATION) + (importance_mix * PNGX_COMMON_PREPARE_ANCHOR_MIX);
867 2246399 : if (importance > PNGX_COMMON_PREPARE_ANCHOR_IMPORTANCE_THRESHOLD) {
868 22 : anchor_score += PNGX_COMMON_PREPARE_ANCHOR_IMPORTANCE_BONUS;
869 : }
870 2246399 : if (anchor_score > PNGX_COMMON_PREPARE_ANCHOR_SCORE_THRESHOLD) {
871 1610113 : bucket_entry = &buckets[chroma_bucket_index(r, g, b)];
872 1610113 : bucket_entry->r_sum += (uint64_t)r;
873 1610113 : bucket_entry->g_sum += (uint64_t)g;
874 1610113 : bucket_entry->b_sum += (uint64_t)b;
875 1610113 : bucket_entry->a_sum += (uint64_t)a;
876 1610113 : bucket_entry->count += 1;
877 1610113 : bucket_entry->score += anchor_score;
878 1610113 : bucket_entry->importance_accum += importance;
879 : }
880 : }
881 : }
882 : }
883 :
884 10164 : luma_row_tmp = luma_row_curr;
885 10164 : luma_row_curr = luma_row_next;
886 10164 : luma_row_next = luma_row_tmp;
887 : }
888 :
889 33 : free(luma_row_curr);
890 33 : free(luma_row_next);
891 :
892 33 : if (image->pixel_count > 0) {
893 33 : stats->gradient_mean = gradient_sum / (float)image->pixel_count;
894 33 : stats->saturation_mean = saturation_sum / (float)image->pixel_count;
895 33 : stats->opaque_ratio = (float)opaque_pixels / (float)image->pixel_count;
896 33 : stats->translucent_ratio = (float)translucent_pixels / (float)image->pixel_count;
897 33 : stats->vibrant_ratio = (float)vibrant_pixels / (float)image->pixel_count;
898 : }
899 33 : if (need_map && importance_work && image->pixel_count > 0) {
900 28 : range = (uint32_t)(raw_max - raw_min);
901 28 : if (range == 0) {
902 3 : range = 1;
903 : }
904 :
905 28 : support->importance_map = (uint8_t *)malloc(image->pixel_count);
906 28 : if (support->importance_map) {
907 8771550 : for (pixel_index = 0; pixel_index < image->pixel_count; ++pixel_index) {
908 8771522 : sample = (uint32_t)(importance_work[pixel_index] - raw_min);
909 8771522 : value = (uint8_t)((sample * 255) / range);
910 8771522 : if (value < PNGX_COMMON_PREPARE_MAP_MIN_VALUE) {
911 97982 : value = (uint8_t)PNGX_COMMON_PREPARE_MAP_MIN_VALUE;
912 : }
913 8771522 : support->importance_map[pixel_index] = value;
914 : }
915 28 : support->importance_map_len = image->pixel_count;
916 : }
917 : }
918 :
919 33 : free(importance_work);
920 :
921 33 : if (need_buckets && buckets) {
922 28 : extract_chroma_anchors(support, buckets, PNGX_CHROMA_BUCKET_COUNT, image->pixel_count);
923 : }
924 :
925 33 : free(buckets);
926 :
927 33 : return true;
928 : }
|