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