LCOV - code coverage report
Current view: top level - src - pngx_reduced.c (source / functions) Coverage Total Hit
Test: colopresso Coverage Report Lines: 85.2 % 1376 1173
Test Date: 2026-11-09 10:50:48 Functions: 100.0 % 57 57
Legend: Lines:     hit not hit

            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 <limits.h>
      13              : #include <math.h>
      14              : #include <stdio.h>
      15              : #include <stdlib.h>
      16              : #include <string.h>
      17              : 
      18              : #include <png.h>
      19              : 
      20              : #include "internal/log.h"
      21              : #include "internal/pngx_common.h"
      22              : #include "internal/simd.h"
      23              : #include "internal/threads.h"
      24              : 
      25              : typedef struct {
      26              :     uint32_t color;
      27              :     uint32_t count;
      28              :     uint32_t mapped_color;
      29              :     uint8_t detail_bits_rgb;
      30              :     uint8_t detail_bits_alpha;
      31              :     bool locked;
      32              : } color_entry_t;
      33              : 
      34              : typedef struct {
      35              :     color_entry_t *entries;
      36              :     size_t count;
      37              :     size_t unlocked_count;
      38              : } color_histogram_t;
      39              : 
      40              : typedef struct {
      41              :     uint32_t color;
      42              :     uint16_t weight;
      43              :     uint8_t rgb_bits;
      44              :     uint8_t alpha_bits;
      45              : } histogram_sample_t;
      46              : 
      47              : typedef struct {
      48              :     size_t start;
      49              :     size_t end;
      50              :     uint8_t min_r;
      51              :     uint8_t min_g;
      52              :     uint8_t min_b;
      53              :     uint8_t min_a;
      54              :     uint8_t max_r;
      55              :     uint8_t max_g;
      56              :     uint8_t max_b;
      57              :     uint8_t max_a;
      58              :     uint64_t total_weight;
      59              : } color_box_t;
      60              : 
      61              : typedef struct {
      62              :     uint32_t color;
      63              :     uint32_t mapped_color;
      64              : } color_map_entry_t;
      65              : 
      66              : typedef struct {
      67              :     uint32_t color;
      68              :     uint32_t count;
      69              : } color_freq_t;
      70              : 
      71              : typedef struct {
      72              :     size_t index;
      73              :     uint32_t count;
      74              :     uint32_t color;
      75              : } freq_rank_t;
      76              : 
      77              : typedef struct {
      78              :     uint8_t *rgba;
      79              :     size_t pixel_count;
      80              :     const uint8_t *importance_map;
      81              :     size_t importance_map_len;
      82              :     uint8_t bits_rgb;
      83              :     uint8_t bits_alpha;
      84              :     uint8_t boost_bits_rgb;
      85              :     uint8_t boost_bits_alpha;
      86              :     uint8_t *bit_hint_map;
      87              :     size_t bit_hint_len;
      88              : } reduce_bitdepth_parallel_ctx_t;
      89              : 
      90              : typedef struct {
      91              :     uint8_t *rgba;
      92              :     size_t pixel_count;
      93              :     const color_map_entry_t *map;
      94              :     size_t map_count;
      95              : } color_remap_parallel_ctx_t;
      96              : 
      97              : typedef struct {
      98              :     const uint8_t *rgba;
      99              :     uint32_t *packed;
     100              :     size_t pixel_count;
     101              : } pack_rgba_parallel_ctx_t;
     102              : 
     103           18 : static inline uint32_t compute_grid_capacity(uint8_t bits_rgb, uint8_t bits_alpha)
     104              : {
     105              :     uint64_t capacity;
     106           18 :     uint32_t rgb_levels = 1, alpha_levels = 1;
     107              : 
     108           18 :     bits_rgb = clamp_reduced_bits(bits_rgb);
     109           18 :     bits_alpha = clamp_reduced_bits(bits_alpha);
     110              : 
     111           18 :     if (bits_rgb < 8) {
     112           14 :         rgb_levels = (uint32_t)1 << bits_rgb;
     113           14 :         rgb_levels = rgb_levels * rgb_levels * rgb_levels;
     114              :     } else {
     115            4 :         rgb_levels = UINT32_MAX;
     116              :     }
     117              : 
     118           18 :     if (bits_alpha < 8) {
     119           18 :         alpha_levels = (uint32_t)1 << bits_alpha;
     120              :     } else {
     121            0 :         alpha_levels = UINT32_MAX;
     122              :     }
     123              : 
     124           18 :     if (rgb_levels == UINT32_MAX || alpha_levels == UINT32_MAX) {
     125            4 :         return (uint32_t)COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
     126              :     }
     127              : 
     128           14 :     capacity = (uint64_t)rgb_levels * (uint64_t)alpha_levels;
     129           14 :     if (capacity > (uint64_t)COLOPRESSO_PNGX_REDUCED_COLORS_MAX) {
     130            8 :         capacity = (uint64_t)COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
     131              :     }
     132              : 
     133           14 :     return (uint32_t)capacity;
     134              : }
     135              : 
     136      9238467 : static inline uint32_t pack_rgba_u32(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
     137              : {
     138      9238467 :     return ((uint32_t)a << 24) | ((uint32_t)b << 16) | ((uint32_t)g << 8) | (uint32_t)r;
     139              : }
     140              : 
     141              : static inline uint8_t color_component(uint32_t color, uint8_t channel)
     142              : {
     143              :     return (uint8_t)((color >> (channel * 8)) & 0xff);
     144              : }
     145              : 
     146      1037047 : static inline void unpack_rgba_u32(uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a)
     147              : {
     148      1037047 :     if (r) {
     149      1037047 :         *r = (uint8_t)(color & 0xff);
     150              :     }
     151              : 
     152      1037047 :     if (g) {
     153      1037047 :         *g = (uint8_t)((color >> 8) & 0xff);
     154              :     }
     155              : 
     156      1037047 :     if (b) {
     157      1037047 :         *b = (uint8_t)((color >> 16) & 0xff);
     158              :     }
     159              : 
     160      1037047 :     if (a) {
     161      1037047 :         *a = (uint8_t)((color >> 24) & 0xff);
     162              :     }
     163      1037047 : }
     164              : 
     165      2424832 : static inline float importance_dither_scale(uint8_t importance_value)
     166              : {
     167              :     float normalized, scale;
     168              : 
     169      2424832 :     normalized = (float)importance_value / PNGX_REDUCED_IMPORTANCE_SCALE_DENOM;
     170      2424832 :     scale = PNGX_REDUCED_IMPORTANCE_SCALE_MIN +
     171      2424832 :             (1.0f - normalized) * PNGX_REDUCED_IMPORTANCE_SCALE_RANGE;
     172              : 
     173      2424832 :     return scale < 0.0f ? 0.0f : (scale > 1.0f ? 1.0f : scale);
     174              : }
     175              : 
     176            8 : static inline void color_histogram_reset(color_histogram_t *hist)
     177              : {
     178            8 :     if (!hist) {
     179            0 :         return;
     180              :     }
     181              : 
     182            8 :     free(hist->entries);
     183            8 :     hist->entries = NULL;
     184            8 :     hist->count = 0;
     185            8 :     hist->unlocked_count = 0;
     186              : }
     187              : 
     188        16937 : static inline bool is_protected_color(uint32_t color, const uint32_t *protected_colors,
     189              :                                       size_t protected_count)
     190              : {
     191              :     size_t i;
     192              : 
     193        16937 :     if (!protected_colors || protected_count == 0) {
     194        16933 :         return false;
     195              :     }
     196              : 
     197           10 :     for (i = 0; i < protected_count; ++i) {
     198           10 :         if (protected_colors[i] == color) {
     199            4 :             return true;
     200              :         }
     201              :     }
     202              : 
     203            0 :     return false;
     204              : }
     205              : 
     206      2491400 : static inline uint16_t histogram_importance_weight(const pngx_quant_support_t *support,
     207              :                                                    size_t pixel_index)
     208              : {
     209              :     const uint8_t *map;
     210              :     uint16_t weight;
     211              :     uint8_t importance;
     212              : 
     213      2491400 :     if (!support || !support->importance_map || pixel_index >= support->importance_map_len) {
     214        66568 :         return 1;
     215              :     }
     216              : 
     217      2424832 :     map = support->importance_map;
     218      2424832 :     importance = map[pixel_index];
     219              : 
     220      2424832 :     weight = 1 + (uint16_t)(importance >> PNGX_REDUCED_IMPORTANCE_WEIGHT_SHIFT);
     221              : 
     222      2424832 :     if (importance > PNGX_REDUCED_IMPORTANCE_WEIGHT_THRESHOLD_STRONG) {
     223        23900 :         weight += PNGX_REDUCED_IMPORTANCE_WEIGHT_BONUS_STRONG;
     224      2400932 :     } else if (importance > PNGX_REDUCED_IMPORTANCE_WEIGHT_THRESHOLD_HIGH) {
     225       545339 :         weight += PNGX_REDUCED_IMPORTANCE_WEIGHT_BONUS_HIGH;
     226      1855593 :     } else if (importance > PNGX_REDUCED_IMPORTANCE_WEIGHT_THRESHOLD_MEDIUM) {
     227      1190152 :         weight += PNGX_REDUCED_IMPORTANCE_WEIGHT_BONUS_MEDIUM;
     228              :     }
     229              : 
     230      2424832 :     if (weight > PNGX_REDUCED_IMPORTANCE_WEIGHT_CAP) {
     231            0 :         weight = PNGX_REDUCED_IMPORTANCE_WEIGHT_CAP;
     232              :     }
     233              : 
     234      2424832 :     return weight;
     235              : }
     236              : 
     237     39340728 : static inline int compare_histogram_sample(const void *lhs, const void *rhs)
     238              : {
     239     39340728 :     const histogram_sample_t *a = (const histogram_sample_t *)lhs,
     240     39340728 :                              *b = (const histogram_sample_t *)rhs;
     241              : 
     242     39340728 :     return a->color < b->color ? -1 : (a->color > b->color ? 1 : 0);
     243              : }
     244              : 
     245         9123 : static inline void color_box_refresh(const color_entry_t *entries, color_box_t *box)
     246              : {
     247              :     uint32_t c;
     248              :     uint8_t r, g, b, a;
     249              :     size_t i;
     250              : 
     251         9123 :     if (!entries || !box || box->end <= box->start) {
     252            0 :         return;
     253              :     }
     254              : 
     255         9123 :     box->min_r = 255;
     256         9123 :     box->min_g = 255;
     257         9123 :     box->min_b = 255;
     258         9123 :     box->min_a = 255;
     259         9123 :     box->max_r = 0;
     260         9123 :     box->max_g = 0;
     261         9123 :     box->max_b = 0;
     262         9123 :     box->max_a = 0;
     263         9123 :     box->total_weight = 0;
     264       167036 :     for (i = box->start; i < box->end; ++i) {
     265       157913 :         c = entries[i].color;
     266       157913 :         r = (uint8_t)(c & 0xff);
     267       157913 :         g = (uint8_t)((c >> 8) & 0xff);
     268       157913 :         b = (uint8_t)((c >> 16) & 0xff);
     269       157913 :         a = (uint8_t)((c >> 24) & 0xff);
     270              : 
     271       157913 :         if (r < box->min_r) {
     272        15732 :             box->min_r = r;
     273              :         }
     274              : 
     275       157913 :         if (r > box->max_r) {
     276        16254 :             box->max_r = r;
     277              :         }
     278              : 
     279       157913 :         if (g < box->min_g) {
     280        10480 :             box->min_g = g;
     281              :         }
     282              : 
     283       157913 :         if (g > box->max_g) {
     284        14455 :             box->max_g = g;
     285              :         }
     286              : 
     287       157913 :         if (b < box->min_b) {
     288        10909 :             box->min_b = b;
     289              :         }
     290              : 
     291       157913 :         if (b > box->max_b) {
     292        18039 :             box->max_b = b;
     293              :         }
     294              : 
     295       157913 :         if (a < box->min_a) {
     296         8445 :             box->min_a = a;
     297              :         }
     298              : 
     299       157913 :         if (a > box->max_a) {
     300        12590 :             box->max_a = a;
     301              :         }
     302              : 
     303       157913 :         box->total_weight += (uint64_t)entries[i].count;
     304              :     }
     305              : }
     306              : 
     307     55863124 : static inline int compare_u32(const void *lhs, const void *rhs)
     308              : {
     309     55863124 :     uint32_t a = *(const uint32_t *)lhs, b = *(const uint32_t *)rhs;
     310              : 
     311     55863124 :     if (a < b) {
     312     14669971 :         return -1;
     313              :     }
     314              : 
     315     41193153 :     if (a > b) {
     316     24983192 :         return 1;
     317              :     }
     318              : 
     319     16209961 :     return 0;
     320              : }
     321              : 
     322      3202653 : static inline bool color_box_splittable(const color_box_t *box)
     323              : {
     324      3202653 :     return box && box->end > box->start + 1;
     325              : }
     326              : 
     327       952896 : static inline uint8_t color_box_max_span(const color_box_t *box)
     328              : {
     329       952896 :     uint8_t span_r = box->max_r - box->min_r, span_g = box->max_g - box->min_g,
     330       952896 :             span_b = box->max_b - box->min_b, span_a = box->max_a - box->min_a, span = span_r;
     331              : 
     332       952896 :     return span_g > span ? span_g : (span_b > span ? span_b : (span_a > span ? span_a : span));
     333              : }
     334              : 
     335       952896 : static inline float color_box_detail_bias(const color_entry_t *entries, const color_box_t *box,
     336              :                                           uint8_t base_bits_rgb, uint8_t base_bits_alpha)
     337              : {
     338              :     const color_entry_t *entry;
     339       952896 :     uint64_t weight = 0, w, accum_x2 = 0;
     340              :     uint32_t delta_rgb, delta_alpha, score_x2;
     341              :     size_t idx;
     342              : 
     343       952896 :     if (!entries || !box || box->end <= box->start) {
     344            0 :         return 0.0f;
     345              :     }
     346              : 
     347     11931878 :     for (idx = box->start; idx < box->end; ++idx) {
     348     10978982 :         entry = &entries[idx];
     349     10978982 :         w = entry->count ? entry->count : 1;
     350      5900517 :         delta_rgb = (entry->detail_bits_rgb > base_bits_rgb)
     351      5078465 :                         ? (uint32_t)(entry->detail_bits_rgb - base_bits_rgb)
     352     10978982 :                         : 0;
     353      5900517 :         delta_alpha = (entry->detail_bits_alpha > base_bits_alpha)
     354      5078465 :                           ? (uint32_t)(entry->detail_bits_alpha - base_bits_alpha)
     355     10978982 :                           : 0;
     356     10978982 :         score_x2 = (delta_rgb << 1) + delta_alpha;
     357              : 
     358     10978982 :         if (score_x2 > 0) {
     359      5078465 :             accum_x2 += (uint64_t)score_x2 * w;
     360              :         }
     361              : 
     362     10978982 :         weight += w;
     363              :     }
     364              : 
     365       952896 :     if (weight == 0) {
     366            0 :         return 0.0f;
     367              :     }
     368              : 
     369       952896 :     return (float)accum_x2 / (float)(weight * 2);
     370              : }
     371              : 
     372         4558 : static inline int select_box_to_split(const color_box_t *boxes, size_t box_count,
     373              :                                       const color_entry_t *entries, uint8_t base_bits_rgb,
     374              :                                       uint8_t base_bits_alpha)
     375              : {
     376         4558 :     int best_index = -1;
     377              :     size_t i;
     378         4558 :     float best_priority = -1.0f, detail_bias, span_score, weight_score, priority;
     379              : 
     380         4558 :     if (!boxes || box_count == 0) {
     381            0 :         return -1;
     382              :     }
     383              : 
     384      3202653 :     for (i = 0; i < box_count; ++i) {
     385      3198095 :         if (!color_box_splittable(&boxes[i])) {
     386      2245199 :             continue;
     387              :         }
     388              : 
     389       952896 :         detail_bias = color_box_detail_bias(entries, &boxes[i], base_bits_rgb, base_bits_alpha);
     390       952896 :         span_score = (float)color_box_max_span(&boxes[i]) / 255.0f;
     391            0 :         weight_score =
     392       952896 :             (boxes[i].total_weight > 0) ? logf((float)boxes[i].total_weight + 1.0f) : 0.0f;
     393       952896 :         priority = (span_score * PNGX_REDUCED_PRIORITY_SPAN_WEIGHT) +
     394       952896 :                    (detail_bias * PNGX_REDUCED_PRIORITY_DETAIL_WEIGHT) +
     395       952896 :                    (weight_score * PNGX_REDUCED_PRIORITY_MASS_WEIGHT);
     396              : 
     397       952896 :         if (priority > best_priority) {
     398        25538 :             best_priority = priority;
     399        25538 :             best_index = (int)i;
     400              :         }
     401              :     }
     402              : 
     403         4558 :     return best_index;
     404              : }
     405              : 
     406         4558 : static inline size_t box_find_split_index(const color_entry_t *entries, const color_box_t *box)
     407              : {
     408         4558 :     uint64_t half, accum = 0;
     409              :     size_t idx;
     410              : 
     411         4558 :     if (!entries || !box || box->end <= box->start) {
     412            0 :         return box ? box->start : 0;
     413              :     }
     414              : 
     415         4558 :     half = box->total_weight / 2;
     416         4558 :     if (half == 0) {
     417            0 :         half = 1;
     418              :     }
     419              : 
     420        67505 :     for (idx = box->start; idx < box->end; ++idx) {
     421        67505 :         accum += (uint64_t)entries[idx].count;
     422        67505 :         if (accum >= half) {
     423         4558 :             return idx;
     424              :         }
     425              :     }
     426              : 
     427            0 :     return box->start + ((box->end - box->start) / 2);
     428              : }
     429              : 
     430       290060 : static inline int compare_entries_r(const void *lhs, const void *rhs)
     431              : {
     432       290060 :     const color_entry_t *a = (const color_entry_t *)lhs, *b = (const color_entry_t *)rhs;
     433       290060 :     uint8_t av = (uint8_t)(a->color & 0xff), bv = (uint8_t)(b->color & 0xff);
     434              : 
     435       290060 :     return (av < bv) ? -1 : ((av > bv) ? 1 : 0);
     436              : }
     437              : 
     438       213350 : static inline int compare_entries_g(const void *lhs, const void *rhs)
     439              : {
     440       213350 :     const color_entry_t *a = (const color_entry_t *)lhs, *b = (const color_entry_t *)rhs;
     441       213350 :     uint8_t av = (uint8_t)((a->color >> 8) & 0xff), bv = (uint8_t)((b->color >> 8) & 0xff);
     442              : 
     443       213350 :     return (av < bv) ? -1 : ((av > bv) ? 1 : 0);
     444              : }
     445              : 
     446       103592 : static inline int compare_entries_b(const void *lhs, const void *rhs)
     447              : {
     448       103592 :     const color_entry_t *a = (const color_entry_t *)lhs, *b = (const color_entry_t *)rhs;
     449       103592 :     uint8_t av = (uint8_t)((a->color >> 16) & 0xff), bv = (uint8_t)((b->color >> 16) & 0xff);
     450              : 
     451       103592 :     return (av < bv) ? -1 : ((av > bv) ? 1 : 0);
     452              : }
     453              : 
     454       144554 : static inline int compare_entries_a(const void *lhs, const void *rhs)
     455              : {
     456       144554 :     const color_entry_t *a = (const color_entry_t *)lhs, *b = (const color_entry_t *)rhs;
     457       144554 :     uint8_t av = (uint8_t)((a->color >> 24) & 0xff), bv = (uint8_t)((b->color >> 24) & 0xff);
     458              : 
     459       144554 :     return (av < bv) ? -1 : ((av > bv) ? 1 : 0);
     460              : }
     461              : 
     462         4558 : static inline void sort_entries_by_axis(color_entry_t *entries, size_t count, int axis)
     463              : {
     464         4558 :     if (!entries || count == 0) {
     465            0 :         return;
     466              :     }
     467              : 
     468         4558 :     switch (axis) {
     469         1294 :     case 0:
     470         1294 :         qsort(entries, count, sizeof(color_entry_t), compare_entries_r);
     471              : 
     472         1294 :         break;
     473         1213 :     case 1:
     474         1213 :         qsort(entries, count, sizeof(color_entry_t), compare_entries_g);
     475              : 
     476         1213 :         break;
     477         1220 :     case 2:
     478         1220 :         qsort(entries, count, sizeof(color_entry_t), compare_entries_b);
     479              : 
     480         1220 :         break;
     481          831 :     case 3:
     482          831 :         qsort(entries, count, sizeof(color_entry_t), compare_entries_a);
     483              : 
     484          831 :         break;
     485            0 :     default:
     486            0 :         qsort(entries, count, sizeof(color_entry_t), compare_entries_r);
     487              : 
     488            0 :         break;
     489              :     }
     490              : }
     491              : 
     492         4558 : static inline bool split_color_box(color_entry_t *entries, color_box_t *box, color_box_t *new_box)
     493              : {
     494              :     int axis;
     495              :     size_t split_index;
     496              : 
     497         4558 :     if (!entries || !box || !new_box || !color_box_splittable(box)) {
     498            0 :         return false;
     499              :     }
     500              : 
     501         4558 :     axis = 0;
     502              : 
     503         4558 :     if ((box->max_g - box->min_g) > (box->max_r - box->min_r)) {
     504         1500 :         axis = 1;
     505              :     }
     506              : 
     507         4558 :     if ((box->max_b - box->min_b) > (box->max_g - box->min_g) &&
     508         1424 :         (box->max_b - box->min_b) >= (box->max_r - box->min_r)) {
     509         1278 :         axis = 2;
     510              :     }
     511              : 
     512         4558 :     if ((box->max_a - box->min_a) > (box->max_b - box->min_b) &&
     513         1022 :         (box->max_a - box->min_a) >= (box->max_g - box->min_g) &&
     514          899 :         (box->max_a - box->min_a) >= (box->max_r - box->min_r)) {
     515          831 :         axis = 3;
     516              :     }
     517              : 
     518         4558 :     sort_entries_by_axis(entries + box->start, box->end - box->start, axis);
     519              : 
     520         4558 :     split_index = box_find_split_index(entries, box);
     521         4558 :     if (split_index <= box->start) {
     522          977 :         split_index = box->start + 1;
     523              :     }
     524              : 
     525         4558 :     if (split_index >= box->end) {
     526            0 :         split_index = box->end - 1;
     527              :     }
     528              : 
     529         4558 :     new_box->start = split_index;
     530         4558 :     new_box->end = box->end;
     531         4558 :     box->end = split_index;
     532              : 
     533         4558 :     color_box_refresh(entries, box);
     534         4558 :     color_box_refresh(entries, new_box);
     535              : 
     536         4558 :     return true;
     537              : }
     538              : 
     539       152503 : static inline int compare_color_map_by_color(const void *lhs, const void *rhs)
     540              : {
     541       152503 :     const color_map_entry_t *a = (const color_map_entry_t *)lhs,
     542       152503 :                             *b = (const color_map_entry_t *)rhs;
     543              : 
     544       152503 :     return a->color < b->color ? -1 : (a->color > b->color ? 1 : 0);
     545              : }
     546              : 
     547       145518 : static inline int compare_color_map_by_mapped(const void *lhs, const void *rhs)
     548              : {
     549       145518 :     const color_map_entry_t *a = (const color_map_entry_t *)lhs,
     550       145518 :                             *b = (const color_map_entry_t *)rhs;
     551              : 
     552       145518 :     return a->mapped_color < b->mapped_color ? -1 : (a->mapped_color > b->mapped_color ? 1 : 0);
     553              : }
     554              : 
     555      2462854 : static inline const color_map_entry_t *find_color_mapping(const color_map_entry_t *map,
     556              :                                                           size_t count, uint32_t color)
     557              : {
     558      2462854 :     size_t left = 0, right = (count > 0) ? count - 1 : 0, mid;
     559              : 
     560      2462854 :     if (!map || count == 0) {
     561            0 :         return NULL;
     562              :     }
     563              : 
     564     30603647 :     while (left <= right) {
     565     29094766 :         mid = left + ((right - left) / 2);
     566              : 
     567     29094766 :         if (map[mid].color == color) {
     568       878632 :             return &map[mid];
     569              :         }
     570              : 
     571     28216134 :         if (map[mid].color < color) {
     572     12347562 :             left = mid + 1;
     573              :         } else {
     574     15868572 :             if (mid == 0) {
     575        75341 :                 break;
     576              :             }
     577              : 
     578     15793231 :             right = mid - 1;
     579              :         }
     580              :     }
     581              : 
     582      1584222 :     return NULL;
     583              : }
     584              : 
     585            7 : static inline void refine_reduced_palette(color_entry_t *entries, size_t unlocked_count,
     586              :                                           const uint32_t *seed_palette,
     587              :                                           const uint8_t *palette_bits_rgb,
     588              :                                           const uint8_t *palette_bits_alpha, size_t palette_count)
     589              : {
     590            7 :     const size_t max_iter = 3;
     591              :     uint64_t *sum_r, *sum_g, *sum_b, *sum_a, *sum_w, w;
     592              :     uint32_t *palette, color, best_dist, weight, dist, new_color;
     593              :     uint8_t r, g, b, a, snap_bits_rgb, snap_bits_alpha, cr, cg, cb, ca;
     594              :     size_t iter, best_index, candidate, i, p;
     595              :     bool palette_changed;
     596              : 
     597            7 :     if (!entries || unlocked_count == 0 || !seed_palette || palette_count == 0 ||
     598              :         palette_count > 4096) {
     599            0 :         return;
     600              :     }
     601              : 
     602            7 :     palette = (uint32_t *)malloc(sizeof(uint32_t) * palette_count);
     603            7 :     if (!palette) {
     604            0 :         return;
     605              :     }
     606              : 
     607            7 :     memcpy(palette, seed_palette, sizeof(uint32_t) * palette_count);
     608              : 
     609            7 :     sum_r = (uint64_t *)malloc(sizeof(uint64_t) * palette_count);
     610            7 :     sum_g = (uint64_t *)malloc(sizeof(uint64_t) * palette_count);
     611            7 :     sum_b = (uint64_t *)malloc(sizeof(uint64_t) * palette_count);
     612            7 :     sum_a = (uint64_t *)malloc(sizeof(uint64_t) * palette_count);
     613            7 :     sum_w = (uint64_t *)malloc(sizeof(uint64_t) * palette_count);
     614              : 
     615            7 :     if (!sum_r || !sum_g || !sum_b || !sum_a || !sum_w) {
     616            0 :         free(sum_r);
     617            0 :         free(sum_g);
     618            0 :         free(sum_b);
     619            0 :         free(sum_a);
     620            0 :         free(sum_w);
     621            0 :         free(palette);
     622              : 
     623            0 :         return;
     624              :     }
     625              : 
     626           21 :     for (iter = 0; iter < max_iter; ++iter) {
     627           17 :         palette_changed = false;
     628              : 
     629           17 :         memset(sum_r, 0, sizeof(uint64_t) * palette_count);
     630           17 :         memset(sum_g, 0, sizeof(uint64_t) * palette_count);
     631           17 :         memset(sum_b, 0, sizeof(uint64_t) * palette_count);
     632           17 :         memset(sum_a, 0, sizeof(uint64_t) * palette_count);
     633           17 :         memset(sum_w, 0, sizeof(uint64_t) * palette_count);
     634              : 
     635        47374 :         for (i = 0; i < unlocked_count; ++i) {
     636        47357 :             color = entries[i].color;
     637        47357 :             best_dist = UINT32_MAX;
     638        47357 :             best_index = 0;
     639        47357 :             candidate = 0;
     640        47357 :             weight = entries[i].count ? entries[i].count : 1;
     641              : 
     642     33876129 :             for (candidate = 0; candidate < palette_count; ++candidate) {
     643     33828772 :                 dist = simd_color_distance_sq_u32(color, palette[candidate]);
     644     33828772 :                 if (dist < best_dist) {
     645       337174 :                     best_dist = dist;
     646       337174 :                     best_index = candidate;
     647              :                 }
     648              :             }
     649              : 
     650        47357 :             unpack_rgba_u32(color, &cr, &cg, &cb, &ca);
     651              : 
     652        47357 :             sum_r[best_index] += (uint64_t)cr * (uint64_t)weight;
     653        47357 :             sum_g[best_index] += (uint64_t)cg * (uint64_t)weight;
     654        47357 :             sum_b[best_index] += (uint64_t)cb * (uint64_t)weight;
     655        47357 :             sum_a[best_index] += (uint64_t)ca * (uint64_t)weight;
     656        47357 :             sum_w[best_index] += (uint64_t)weight;
     657              :         }
     658              : 
     659        10974 :         for (p = 0; p < palette_count; ++p) {
     660        10957 :             w = sum_w[p];
     661        10957 :             if (w == 0) {
     662           78 :                 continue;
     663              :             }
     664              : 
     665        10879 :             r = (uint8_t)((sum_r[p] + (w / 2)) / w);
     666        10879 :             g = (uint8_t)((sum_g[p] + (w / 2)) / w);
     667        10879 :             b = (uint8_t)((sum_b[p] + (w / 2)) / w);
     668        10879 :             a = (uint8_t)((sum_a[p] + (w / 2)) / w);
     669        10879 :             snap_bits_rgb =
     670        10879 :                 palette_bits_rgb ? palette_bits_rgb[p] : COLOPRESSO_PNGX_REDUCED_BITS_MAX;
     671        10879 :             snap_bits_alpha =
     672        10879 :                 palette_bits_alpha ? palette_bits_alpha[p] : COLOPRESSO_PNGX_REDUCED_BITS_MAX;
     673        10879 :             snap_rgba_to_bits(&r, &g, &b, &a, snap_bits_rgb, snap_bits_alpha);
     674        10879 :             new_color = pack_rgba_u32(r, g, b, a);
     675        10879 :             if (new_color != palette[p]) {
     676          831 :                 palette[p] = new_color;
     677          831 :                 palette_changed = true;
     678              :             }
     679              :         }
     680              : 
     681           17 :         if (!palette_changed) {
     682            3 :             break;
     683              :         }
     684              :     }
     685              : 
     686        16940 :     for (i = 0; i < unlocked_count; ++i) {
     687        16933 :         color = entries[i].color;
     688        16933 :         best_dist = UINT32_MAX;
     689        16933 :         best_index = 0;
     690              : 
     691     13258047 :         for (candidate = 0; candidate < palette_count; ++candidate) {
     692     13241114 :             dist = simd_color_distance_sq_u32(color, palette[candidate]);
     693     13241114 :             if (dist < best_dist) {
     694       124418 :                 best_dist = dist;
     695       124418 :                 best_index = candidate;
     696              :             }
     697              :         }
     698              : 
     699        16933 :         entries[i].mapped_color = palette[best_index];
     700              :     }
     701              : 
     702            7 :     free(sum_r);
     703            7 :     free(sum_g);
     704            7 :     free(sum_b);
     705            7 :     free(sum_a);
     706            7 :     free(sum_w);
     707              : 
     708            7 :     free(palette);
     709              : }
     710              : 
     711            8 : static inline bool build_color_histogram(const pngx_rgba_image_t *image, const pngx_options_t *opts,
     712              :                                          const pngx_quant_support_t *support,
     713              :                                          color_histogram_t *hist)
     714              : {
     715            8 :     const cpres_rgba_color_t *protected_colors =
     716            8 :         (opts && opts->protected_colors_count > 0) ? opts->protected_colors : NULL;
     717            8 :     histogram_sample_t *samples = NULL;
     718              :     color_entry_t tmp;
     719              :     uint64_t weight_sum;
     720              :     uint32_t protected_table[256], color;
     721            8 :     uint8_t bits_rgb = 8, bits_alpha = 8, r, g, b, a, sample_bits_rgb, sample_bits_alpha, hint,
     722              :             hint_rgb, hint_alpha, max_bits_rgb, max_bits_alpha;
     723              :     size_t pixel_count, i, unique_count, base, run, write,
     724            8 :         protected_count = (protected_colors && opts->protected_colors_count > 0)
     725            1 :                               ? (size_t)opts->protected_colors_count
     726            8 :                               : 0;
     727            8 :     bool quantize_rgb = false, quantize_alpha = false;
     728              : 
     729            8 :     if (!image || !image->rgba || !hist || image->pixel_count == 0) {
     730            0 :         return false;
     731              :     }
     732              : 
     733            8 :     if (opts) {
     734            8 :         bits_rgb = clamp_reduced_bits(opts->lossy_reduced_bits_rgb);
     735            8 :         bits_alpha = clamp_reduced_bits(opts->lossy_reduced_alpha_bits);
     736              :     }
     737              : 
     738            8 :     quantize_rgb = bits_rgb < 8;
     739            8 :     quantize_alpha = bits_alpha < 8;
     740              : 
     741            8 :     if (protected_count > 256) {
     742            0 :         protected_count = 256;
     743              :     }
     744              : 
     745           12 :     for (i = 0; i < protected_count; ++i) {
     746            4 :         r = protected_colors[i].r;
     747            4 :         g = protected_colors[i].g;
     748            4 :         b = protected_colors[i].b;
     749            4 :         a = protected_colors[i].a;
     750              : 
     751            4 :         if (quantize_rgb) {
     752            4 :             r = quantize_bits(r, bits_rgb);
     753            4 :             g = quantize_bits(g, bits_rgb);
     754            4 :             b = quantize_bits(b, bits_rgb);
     755              :         }
     756              : 
     757            4 :         if (quantize_alpha) {
     758            4 :             a = quantize_bits(a, bits_alpha);
     759              :         }
     760              : 
     761            4 :         protected_table[i] = pack_rgba_u32(r, g, b, a);
     762              :     }
     763              : 
     764            8 :     pixel_count = image->pixel_count;
     765              : 
     766            8 :     samples = (histogram_sample_t *)malloc(pixel_count * sizeof(histogram_sample_t));
     767            8 :     if (!samples) {
     768            0 :         return false;
     769              :     }
     770              : 
     771      2491408 :     for (i = 0; i < pixel_count; ++i) {
     772      2491400 :         base = i * 4;
     773      2491400 :         r = image->rgba[base + 0];
     774      2491400 :         g = image->rgba[base + 1];
     775      2491400 :         b = image->rgba[base + 2];
     776      2491400 :         a = image->rgba[base + 3];
     777      2491400 :         sample_bits_rgb = bits_rgb;
     778      2491400 :         sample_bits_alpha = bits_alpha;
     779              : 
     780      2491400 :         if (support && support->bit_hint_map && i < support->bit_hint_len) {
     781      2491400 :             hint = support->bit_hint_map[i];
     782      2491400 :             hint_rgb = hint >> 4;
     783      2491400 :             hint_alpha = hint & 0x0fu;
     784              : 
     785      2491400 :             if (hint_rgb >= COLOPRESSO_PNGX_REDUCED_BITS_MIN) {
     786      2491400 :                 sample_bits_rgb = clamp_reduced_bits(hint_rgb);
     787              :             }
     788              : 
     789      2491400 :             if (hint_alpha >= COLOPRESSO_PNGX_REDUCED_BITS_MIN) {
     790      2491400 :                 sample_bits_alpha = clamp_reduced_bits(hint_alpha);
     791              :             }
     792              :         }
     793              : 
     794      2491400 :         if (quantize_rgb) {
     795      2424840 :             r = quantize_bits(r, bits_rgb);
     796      2424840 :             g = quantize_bits(g, bits_rgb);
     797      2424840 :             b = quantize_bits(b, bits_rgb);
     798              :         }
     799              : 
     800      2491400 :         if (quantize_alpha) {
     801      2491400 :             a = quantize_bits(a, bits_alpha);
     802              :         }
     803              : 
     804      2491400 :         samples[i].color = pack_rgba_u32(r, g, b, a);
     805      2491400 :         samples[i].weight = histogram_importance_weight(support, i);
     806      2491400 :         samples[i].rgb_bits = sample_bits_rgb;
     807      2491400 :         samples[i].alpha_bits = sample_bits_alpha;
     808              :     }
     809              : 
     810            8 :     qsort(samples, pixel_count, sizeof(histogram_sample_t), compare_histogram_sample);
     811              : 
     812            8 :     unique_count = 0;
     813              : 
     814        16945 :     for (i = 0; i < pixel_count;) {
     815        16937 :         color = samples[i].color;
     816        16937 :         run = 1;
     817              : 
     818      2491400 :         while (i + run < pixel_count && samples[i + run].color == color) {
     819      2474463 :             ++run;
     820              :         }
     821              : 
     822        16937 :         ++unique_count;
     823        16937 :         i += run;
     824              :     }
     825              : 
     826            8 :     hist->entries = (color_entry_t *)malloc(unique_count * sizeof(color_entry_t));
     827            8 :     if (!hist->entries) {
     828            0 :         free(samples);
     829              : 
     830            0 :         return false;
     831              :     }
     832              : 
     833            8 :     hist->count = unique_count;
     834            8 :     hist->unlocked_count = 0;
     835              : 
     836        16945 :     for (i = 0, unique_count = 0; i < pixel_count;) {
     837        16937 :         color = samples[i].color;
     838        16937 :         run = 1;
     839        16937 :         weight_sum = samples[i].weight;
     840        16937 :         max_bits_rgb = samples[i].rgb_bits;
     841        16937 :         max_bits_alpha = samples[i].alpha_bits;
     842              : 
     843      2491400 :         while (i + run < pixel_count && samples[i + run].color == color) {
     844      2474463 :             weight_sum += samples[i + run].weight;
     845              : 
     846      2474463 :             if (samples[i + run].rgb_bits > max_bits_rgb) {
     847          770 :                 max_bits_rgb = samples[i + run].rgb_bits;
     848              :             }
     849              : 
     850      2474463 :             if (samples[i + run].alpha_bits > max_bits_alpha) {
     851          540 :                 max_bits_alpha = samples[i + run].alpha_bits;
     852              :             }
     853              : 
     854      2474463 :             ++run;
     855              :         }
     856              : 
     857        16937 :         hist->entries[unique_count].color = color;
     858        16937 :         hist->entries[unique_count].count =
     859        16937 :             (uint32_t)((weight_sum > UINT32_MAX) ? UINT32_MAX : weight_sum);
     860        16937 :         hist->entries[unique_count].mapped_color = color;
     861        33874 :         hist->entries[unique_count].locked =
     862        16937 :             is_protected_color(color, protected_table, protected_count);
     863        16937 :         hist->entries[unique_count].detail_bits_rgb = clamp_reduced_bits(max_bits_rgb);
     864        16937 :         hist->entries[unique_count].detail_bits_alpha = clamp_reduced_bits(max_bits_alpha);
     865        16937 :         ++unique_count;
     866        16937 :         i += run;
     867              :     }
     868              : 
     869            8 :     free(samples);
     870              : 
     871            8 :     if (unique_count == 0) {
     872            0 :         return true;
     873              :     }
     874              : 
     875            8 :     write = 0;
     876              : 
     877        16945 :     for (i = 0; i < unique_count; ++i) {
     878        16937 :         if (!hist->entries[i].locked) {
     879        16933 :             if (write != i) {
     880            0 :                 tmp = hist->entries[write];
     881            0 :                 hist->entries[write] = hist->entries[i];
     882            0 :                 hist->entries[i] = tmp;
     883              :             }
     884              : 
     885        16933 :             ++write;
     886              :         }
     887              :     }
     888              : 
     889            8 :     hist->unlocked_count = write;
     890              : 
     891            8 :     return true;
     892              : }
     893              : 
     894         4565 : static inline uint32_t box_representative_color(const color_entry_t *entries,
     895              :                                                 const color_box_t *box)
     896              : {
     897         4565 :     uint64_t sum_r = 0, sum_g = 0, sum_b = 0, sum_a = 0, total = 0, weight;
     898              :     uint32_t c;
     899              :     size_t i;
     900              : 
     901         4565 :     if (!entries || !box) {
     902            0 :         return 0;
     903              :     }
     904              : 
     905        21498 :     for (i = box->start; i < box->end; ++i) {
     906        16933 :         c = entries[i].color;
     907        16933 :         weight = (uint64_t)entries[i].count;
     908        16933 :         sum_r += (uint64_t)(c & 0xff) * weight;
     909        16933 :         sum_g += (uint64_t)((c >> 8) & 0xff) * weight;
     910        16933 :         sum_b += (uint64_t)((c >> 16) & 0xff) * weight;
     911        16933 :         sum_a += (uint64_t)((c >> 24) & 0xff) * weight;
     912        16933 :         total += weight;
     913              :     }
     914              : 
     915         4565 :     if (total == 0) {
     916            0 :         return entries[box->start].color;
     917              :     }
     918              : 
     919         4565 :     sum_r = (sum_r + (total / 2)) / total;
     920         4565 :     sum_g = (sum_g + (total / 2)) / total;
     921         4565 :     sum_b = (sum_b + (total / 2)) / total;
     922         4565 :     sum_a = (sum_a + (total / 2)) / total;
     923              : 
     924         4565 :     return pack_rgba_u32((uint8_t)sum_r, (uint8_t)sum_g, (uint8_t)sum_b, (uint8_t)sum_a);
     925              : }
     926              : 
     927            7 : static inline void color_remap_parallel_worker(void *context, uint32_t start, uint32_t end)
     928              : {
     929              :     const color_map_entry_t *mapping;
     930            7 :     color_remap_parallel_ctx_t *ctx = (color_remap_parallel_ctx_t *)context;
     931              :     uint32_t original;
     932              :     uint8_t *px;
     933              :     size_t i, base;
     934              : 
     935            7 :     if (!ctx || !ctx->rgba || !ctx->map || ctx->map_count == 0) {
     936            0 :         return;
     937              :     }
     938              : 
     939      2491403 :     for (i = (size_t)start; i < (size_t)end && i < ctx->pixel_count; ++i) {
     940      2491396 :         base = i * PNGX_RGBA_CHANNELS;
     941      2491396 :         px = &ctx->rgba[base];
     942      2491396 :         if (px[3] <= PNGX_REDUCED_ALPHA_NEAR_TRANSPARENT) {
     943        28542 :             continue;
     944              :         }
     945              : 
     946      2462854 :         original = pack_rgba_u32(px[0], px[1], px[2], px[3]);
     947      2462854 :         mapping = find_color_mapping(ctx->map, ctx->map_count, original);
     948              : 
     949      2462854 :         if (!mapping) {
     950      1584222 :             continue;
     951              :         }
     952              : 
     953       878632 :         unpack_rgba_u32(mapping->mapped_color, &px[0], &px[1], &px[2], &px[3]);
     954              :     }
     955              : }
     956              : 
     957            7 : static inline bool apply_reduced_rgba32_quantization(uint32_t thread_count, color_histogram_t *hist,
     958              :                                                      pngx_rgba_image_t *image,
     959              :                                                      uint32_t target_colors, uint8_t bits_rgb,
     960              :                                                      uint8_t bits_alpha, uint32_t *applied_colors)
     961              : {
     962            7 :     color_box_t *boxes = NULL, new_box;
     963            7 :     color_map_entry_t *map = NULL;
     964              :     color_remap_parallel_ctx_t remap_ctx;
     965            7 :     uint32_t *palette_seed = NULL, actual_colors = 0, mapped;
     966            7 :     uint8_t *palette_bits_rgb = NULL, *palette_bits_alpha = NULL, r, g, b, a;
     967              :     int32_t split_index;
     968            7 :     size_t box_count = 0, map_count, i, idx;
     969              : 
     970            7 :     if (!hist || !image || target_colors == 0) {
     971            0 :         if (applied_colors) {
     972            0 :             *applied_colors = (uint32_t)(hist ? hist->count : 0);
     973              :         }
     974              : 
     975            0 :         return true;
     976              :     }
     977              : 
     978            7 :     if (hist->unlocked_count == 0) {
     979            0 :         if (applied_colors) {
     980            0 :             *applied_colors = (uint32_t)hist->count;
     981              :         }
     982              : 
     983            0 :         return true;
     984              :     }
     985              : 
     986            7 :     if (target_colors > (uint32_t)hist->unlocked_count) {
     987            0 :         target_colors = (uint32_t)hist->unlocked_count;
     988              :     }
     989              : 
     990            7 :     if (target_colors == 0) {
     991            0 :         target_colors = 1;
     992              :     }
     993              : 
     994            7 :     boxes = (color_box_t *)calloc(target_colors, sizeof(color_box_t));
     995            7 :     if (!boxes) {
     996            0 :         return false;
     997              :     }
     998              : 
     999            7 :     boxes[0].start = 0;
    1000            7 :     boxes[0].end = hist->unlocked_count;
    1001            7 :     color_box_refresh(hist->entries, &boxes[0]);
    1002            7 :     box_count = (boxes[0].end > boxes[0].start) ? 1 : 0;
    1003              : 
    1004         4565 :     while (box_count < target_colors) {
    1005         4558 :         split_index = select_box_to_split(boxes, box_count, hist->entries, bits_rgb, bits_alpha);
    1006         4558 :         if (split_index < 0) {
    1007            0 :             break;
    1008              :         }
    1009              : 
    1010         4558 :         if (!split_color_box(hist->entries, &boxes[split_index], &new_box)) {
    1011            0 :             break;
    1012              :         }
    1013              : 
    1014         4558 :         boxes[box_count++] = new_box;
    1015              :     }
    1016              : 
    1017            7 :     if (box_count == 0) {
    1018            0 :         free(boxes);
    1019              : 
    1020            0 :         if (applied_colors) {
    1021            0 :             *applied_colors = (uint32_t)hist->count;
    1022              :         }
    1023              : 
    1024            0 :         return true;
    1025              :     }
    1026              : 
    1027            7 :     palette_seed = (uint32_t *)malloc(sizeof(uint32_t) * box_count);
    1028            7 :     palette_bits_rgb = (uint8_t *)calloc(box_count, sizeof(uint8_t));
    1029            7 :     palette_bits_alpha = (uint8_t *)calloc(box_count, sizeof(uint8_t));
    1030            7 :     if (!palette_seed || !palette_bits_rgb || !palette_bits_alpha) {
    1031            0 :         free(palette_seed);
    1032            0 :         free(palette_bits_rgb);
    1033            0 :         free(palette_bits_alpha);
    1034            0 :         free(boxes);
    1035              : 
    1036            0 :         return false;
    1037              :     }
    1038              : 
    1039         4572 :     for (i = 0; i < box_count; ++i) {
    1040         4565 :         mapped = box_representative_color(hist->entries, &boxes[i]);
    1041         4565 :         unpack_rgba_u32(mapped, &r, &g, &b, &a);
    1042         4565 :         snap_rgba_to_bits(&r, &g, &b, &a, bits_rgb, bits_alpha);
    1043         4565 :         mapped = pack_rgba_u32(r, g, b, a);
    1044         4565 :         palette_seed[i] = mapped;
    1045         4565 :         palette_bits_rgb[i] = bits_rgb;
    1046         4565 :         palette_bits_alpha[i] = bits_alpha;
    1047              : 
    1048        21498 :         for (idx = boxes[i].start; idx < boxes[i].end; ++idx) {
    1049        16933 :             hist->entries[idx].mapped_color = mapped;
    1050              :         }
    1051              :     }
    1052              : 
    1053            7 :     if (box_count > 0 && box_count <= 4096) {
    1054            7 :         refine_reduced_palette(hist->entries, hist->unlocked_count, palette_seed, palette_bits_rgb,
    1055              :                                palette_bits_alpha, box_count);
    1056              :     }
    1057              : 
    1058            7 :     for (i = hist->unlocked_count; i < hist->count; ++i) {
    1059            0 :         hist->entries[i].mapped_color = hist->entries[i].color;
    1060              :     }
    1061              : 
    1062            7 :     map_count = hist->count;
    1063            7 :     if (map_count == 0) {
    1064            0 :         free(palette_seed);
    1065            0 :         free(palette_bits_rgb);
    1066            0 :         free(palette_bits_alpha);
    1067            0 :         free(boxes);
    1068              : 
    1069            0 :         if (applied_colors) {
    1070            0 :             *applied_colors = 0;
    1071              :         }
    1072              : 
    1073            0 :         return true;
    1074              :     }
    1075              : 
    1076            7 :     map = (color_map_entry_t *)malloc(map_count * sizeof(color_map_entry_t));
    1077            7 :     if (!map) {
    1078            0 :         free(palette_seed);
    1079            0 :         free(palette_bits_rgb);
    1080            0 :         free(palette_bits_alpha);
    1081            0 :         free(boxes);
    1082              : 
    1083            0 :         return false;
    1084              :     }
    1085              : 
    1086        16940 :     for (i = 0; i < map_count; ++i) {
    1087        16933 :         map[i].color = hist->entries[i].color;
    1088        16933 :         map[i].mapped_color = hist->entries[i].mapped_color;
    1089              :     }
    1090              : 
    1091            7 :     qsort(map, map_count, sizeof(*map), compare_color_map_by_color);
    1092              : 
    1093            7 :     remap_ctx.rgba = image->rgba;
    1094            7 :     remap_ctx.pixel_count = image->pixel_count;
    1095            7 :     remap_ctx.map = map;
    1096            7 :     remap_ctx.map_count = map_count;
    1097              : 
    1098              : #if COLOPRESSO_ENABLE_THREADS
    1099            7 :     colopresso_parallel_for(thread_count, (uint32_t)image->pixel_count, color_remap_parallel_worker,
    1100              :                             &remap_ctx);
    1101              : #else
    1102              :     color_remap_parallel_worker(&remap_ctx, 0, (uint32_t)image->pixel_count);
    1103              : #endif
    1104              : 
    1105            7 :     qsort(map, map_count, sizeof(*map), compare_color_map_by_mapped);
    1106              : 
    1107        16940 :     for (i = 0; i < map_count; ++i) {
    1108        16933 :         if (i == 0 || map[i].mapped_color != map[i - 1].mapped_color) {
    1109         4537 :             ++actual_colors;
    1110              :         }
    1111              :     }
    1112              : 
    1113            7 :     if (applied_colors) {
    1114            7 :         *applied_colors = actual_colors;
    1115              :     }
    1116              : 
    1117            7 :     free(map);
    1118            7 :     free(palette_seed);
    1119            7 :     free(palette_bits_rgb);
    1120            7 :     free(palette_bits_alpha);
    1121            7 :     free(boxes);
    1122              : 
    1123            7 :     return true;
    1124              : }
    1125              : 
    1126           34 : static inline void pack_rgba_parallel_worker(void *context, uint32_t start, uint32_t end)
    1127              : {
    1128           34 :     pack_rgba_parallel_ctx_t *ctx = (pack_rgba_parallel_ctx_t *)context;
    1129              :     size_t i, base;
    1130              : 
    1131           34 :     if (!ctx || !ctx->rgba || !ctx->packed) {
    1132            0 :         return;
    1133              :     }
    1134              : 
    1135      3673386 :     for (i = (size_t)start; i < (size_t)end && i < ctx->pixel_count; ++i) {
    1136      3673352 :         base = i * 4;
    1137      3673352 :         ctx->packed[i] = pack_rgba_u32(ctx->rgba[base + 0], ctx->rgba[base + 1],
    1138      3673352 :                                        ctx->rgba[base + 2], ctx->rgba[base + 3]);
    1139              :     }
    1140              : }
    1141              : 
    1142           17 : static inline bool pack_sorted_rgba(const uint8_t *rgba, size_t pixel_count, uint32_t **out_sorted)
    1143              : {
    1144              :     pack_rgba_parallel_ctx_t ctx;
    1145              :     uint32_t *packed;
    1146              : 
    1147           17 :     if (!rgba || pixel_count == 0 || !out_sorted) {
    1148            0 :         return false;
    1149              :     }
    1150              : 
    1151           17 :     if (pixel_count > SIZE_MAX / sizeof(uint32_t)) {
    1152            0 :         return false;
    1153              :     }
    1154              : 
    1155           17 :     packed = (uint32_t *)malloc(sizeof(uint32_t) * pixel_count);
    1156           17 :     if (!packed) {
    1157            0 :         return false;
    1158              :     }
    1159              : 
    1160           17 :     ctx.rgba = rgba;
    1161           17 :     ctx.packed = packed;
    1162           17 :     ctx.pixel_count = pixel_count;
    1163              : 
    1164              : #if COLOPRESSO_ENABLE_THREADS
    1165           17 :     colopresso_parallel_for(0, (uint32_t)pixel_count, pack_rgba_parallel_worker, &ctx);
    1166              : #else
    1167              :     pack_rgba_parallel_worker(&ctx, 0, (uint32_t)pixel_count);
    1168              : #endif
    1169              : 
    1170           17 :     qsort(packed, pixel_count, sizeof(uint32_t), compare_u32);
    1171           17 :     *out_sorted = packed;
    1172              : 
    1173           17 :     return true;
    1174              : }
    1175              : 
    1176           13 : static inline size_t count_unique_rgba(const uint8_t *rgba, size_t pixel_count)
    1177              : {
    1178              :     uint32_t *packed;
    1179           13 :     size_t i, unique = 0;
    1180              : 
    1181           13 :     if (!rgba || pixel_count == 0) {
    1182            0 :         return 0;
    1183              :     }
    1184              : 
    1185           13 :     if (!pack_sorted_rgba(rgba, pixel_count, &packed)) {
    1186            0 :         return 0;
    1187              :     }
    1188              : 
    1189      3082517 :     for (i = 0; i < pixel_count; ++i) {
    1190      3082504 :         if (i == 0 || packed[i] != packed[i - 1]) {
    1191        74495 :             ++unique;
    1192              :         }
    1193              :     }
    1194              : 
    1195           13 :     free(packed);
    1196              : 
    1197           13 :     return unique;
    1198              : }
    1199              : 
    1200            4 : static inline bool build_color_frequency(const uint8_t *rgba, size_t pixel_count,
    1201              :                                          color_freq_t **out_freq, size_t *out_count)
    1202              : {
    1203              :     color_freq_t *freq, *shrunk;
    1204              :     uint32_t *packed;
    1205              :     size_t i, unique;
    1206              : 
    1207            4 :     if (!out_freq || !out_count) {
    1208            0 :         return false;
    1209              :     }
    1210              : 
    1211            4 :     if (!pack_sorted_rgba(rgba, pixel_count, &packed)) {
    1212            0 :         return false;
    1213              :     }
    1214              : 
    1215            4 :     freq = (color_freq_t *)malloc(sizeof(color_freq_t) * pixel_count);
    1216            4 :     if (!freq) {
    1217            0 :         free(packed);
    1218              : 
    1219            0 :         return false;
    1220              :     }
    1221              : 
    1222            4 :     unique = 0;
    1223       590852 :     for (i = 0; i < pixel_count; ++i) {
    1224       590848 :         if (i == 0 || packed[i] != packed[i - 1]) {
    1225         4373 :             freq[unique].color = packed[i];
    1226         4373 :             freq[unique].count = 1;
    1227         4373 :             ++unique;
    1228              :         } else {
    1229       586475 :             ++freq[unique - 1].count;
    1230              :         }
    1231              :     }
    1232              : 
    1233            4 :     free(packed);
    1234              : 
    1235            4 :     if (unique == 0) {
    1236            0 :         free(freq);
    1237            0 :         *out_freq = NULL;
    1238            0 :         *out_count = 0;
    1239              : 
    1240            0 :         return true;
    1241              :     }
    1242              : 
    1243            4 :     shrunk = (color_freq_t *)realloc(freq, sizeof(color_freq_t) * unique);
    1244            4 :     if (!shrunk) {
    1245            0 :         free(freq);
    1246              : 
    1247            0 :         return false;
    1248              :     }
    1249              : 
    1250            4 :     freq = shrunk;
    1251              : 
    1252            4 :     *out_freq = freq;
    1253            4 :     *out_count = unique;
    1254              : 
    1255            4 :     return true;
    1256              : }
    1257              : 
    1258       590848 : static inline bool find_freq_index(const color_freq_t *freq, size_t freq_count, uint32_t color,
    1259              :                                    size_t *index_out)
    1260              : {
    1261       590848 :     size_t left = 0, right = freq_count, mid;
    1262              : 
    1263       590848 :     if (!freq || freq_count == 0) {
    1264            0 :         return false;
    1265              :     }
    1266              : 
    1267      5599644 :     while (left < right) {
    1268      5599644 :         mid = left + (right - left) / 2;
    1269      5599644 :         if (freq[mid].color == color) {
    1270       590848 :             if (index_out) {
    1271       590848 :                 *index_out = mid;
    1272              :             }
    1273              : 
    1274       590848 :             return true;
    1275              :         }
    1276              : 
    1277      5008796 :         if (freq[mid].color < color) {
    1278      2165167 :             left = mid + 1;
    1279              :         } else {
    1280      2843629 :             right = mid;
    1281              :         }
    1282              :     }
    1283              : 
    1284            0 :     return false;
    1285              : }
    1286              : 
    1287        39216 : static inline int compare_freq_rank_desc(const void *lhs, const void *rhs)
    1288              : {
    1289        39216 :     const freq_rank_t *a = (const freq_rank_t *)lhs, *b = (const freq_rank_t *)rhs;
    1290              : 
    1291        39216 :     if (a->count != b->count) {
    1292        30328 :         return a->count > b->count ? -1 : 1;
    1293              :     }
    1294              : 
    1295         8888 :     return a->color < b->color ? -1 : (a->color > b->color ? 1 : 0);
    1296              : }
    1297              : 
    1298            4 : static inline bool enforce_manual_reduced_limit(uint32_t thread_count, pngx_rgba_image_t *image,
    1299              :                                                 uint32_t manual_limit, uint8_t bits_rgb,
    1300              :                                                 uint8_t bits_alpha, uint32_t *applied_colors)
    1301              : {
    1302            4 :     color_freq_t *freq = NULL;
    1303            4 :     freq_rank_t *rank = NULL;
    1304            4 :     uint32_t *mapped = NULL, source, best_color, best_dist, candidate_color, distance, original;
    1305            4 :     size_t *keep_indices = NULL, freq_count = 0, keep_count, current_unique, unique_after,
    1306              :            pixel_index, idx, best_keep, keep_idx, candidate_index, base, freq_index, i;
    1307            4 :     bool success = false;
    1308              : 
    1309            4 :     if (!image || !image->rgba || image->pixel_count == 0 || manual_limit == 0) {
    1310            0 :         if (applied_colors && image && image->rgba) {
    1311            0 :             current_unique = count_unique_rgba(image->rgba, image->pixel_count);
    1312              : 
    1313            0 :             if (current_unique > UINT32_MAX) {
    1314            0 :                 current_unique = UINT32_MAX;
    1315              :             }
    1316              : 
    1317            0 :             *applied_colors = (uint32_t)current_unique;
    1318              :         }
    1319              : 
    1320            0 :         return true;
    1321              :     }
    1322              : 
    1323            4 :     snap_rgba_image_to_bits(thread_count, image->rgba, image->pixel_count, bits_rgb, bits_alpha);
    1324              : 
    1325            4 :     if (!build_color_frequency(image->rgba, image->pixel_count, &freq, &freq_count)) {
    1326            0 :         goto bailout;
    1327              :     }
    1328              : 
    1329            4 :     if (freq_count == 0) {
    1330            0 :         if (applied_colors) {
    1331            0 :             *applied_colors = 0;
    1332              :         }
    1333              : 
    1334            0 :         success = true;
    1335              : 
    1336            0 :         goto bailout;
    1337              :     }
    1338              : 
    1339            4 :     if (freq_count <= (size_t)manual_limit) {
    1340            0 :         if (applied_colors) {
    1341            0 :             *applied_colors = (uint32_t)freq_count;
    1342              :         }
    1343              : 
    1344            0 :         success = true;
    1345              : 
    1346            0 :         goto bailout;
    1347              :     }
    1348              : 
    1349            4 :     keep_count = (size_t)manual_limit;
    1350            4 :     if (keep_count > freq_count) {
    1351            0 :         keep_count = freq_count;
    1352              :     }
    1353              : 
    1354            4 :     rank = (freq_rank_t *)malloc(sizeof(freq_rank_t) * freq_count);
    1355            4 :     mapped = (uint32_t *)malloc(sizeof(uint32_t) * freq_count);
    1356            4 :     keep_indices = (size_t *)malloc(sizeof(size_t) * keep_count);
    1357            4 :     if (!rank || !mapped || !keep_indices) {
    1358            0 :         goto bailout;
    1359              :     }
    1360              : 
    1361         4377 :     for (i = 0; i < freq_count; ++i) {
    1362         4373 :         rank[i].index = i;
    1363         4373 :         rank[i].count = freq[i].count;
    1364         4373 :         rank[i].color = freq[i].color;
    1365         4373 :         mapped[i] = freq[i].color;
    1366              :     }
    1367              : 
    1368            4 :     qsort(rank, freq_count, sizeof(freq_rank_t), compare_freq_rank_desc);
    1369              : 
    1370         2700 :     for (i = 0; i < keep_count; ++i) {
    1371         2696 :         keep_indices[i] = rank[i].index;
    1372              :     }
    1373              : 
    1374         1681 :     for (i = keep_count; i < freq_count; ++i) {
    1375         1677 :         idx = rank[i].index;
    1376         1677 :         source = freq[idx].color;
    1377         1677 :         best_keep = keep_indices[0];
    1378         1677 :         best_color = freq[best_keep].color;
    1379         1677 :         best_dist = simd_color_distance_sq_u32(source, best_color);
    1380              : 
    1381       601676 :         for (keep_idx = 1; keep_idx < keep_count; ++keep_idx) {
    1382       599999 :             candidate_index = keep_indices[keep_idx];
    1383       599999 :             candidate_color = freq[candidate_index].color;
    1384       599999 :             distance = simd_color_distance_sq_u32(source, candidate_color);
    1385              : 
    1386       599999 :             if (distance < best_dist) {
    1387        10265 :                 best_dist = distance;
    1388        10265 :                 best_color = candidate_color;
    1389              :             }
    1390              :         }
    1391              : 
    1392         1677 :         mapped[idx] = best_color;
    1393              :     }
    1394              : 
    1395       590852 :     for (pixel_index = 0; pixel_index < image->pixel_count; ++pixel_index) {
    1396       590848 :         base = pixel_index * 4;
    1397       590848 :         original = pack_rgba_u32(image->rgba[base + 0], image->rgba[base + 1],
    1398       590848 :                                  image->rgba[base + 2], image->rgba[base + 3]);
    1399              : 
    1400       590848 :         if (!find_freq_index(freq, freq_count, original, &freq_index)) {
    1401            0 :             continue;
    1402              :         }
    1403              : 
    1404       590848 :         if (mapped[freq_index] == original) {
    1405       484355 :             continue;
    1406              :         }
    1407              : 
    1408       106493 :         unpack_rgba_u32(mapped[freq_index], &image->rgba[base + 0], &image->rgba[base + 1],
    1409       106493 :                         &image->rgba[base + 2], &image->rgba[base + 3]);
    1410              :     }
    1411              : 
    1412            4 :     snap_rgba_image_to_bits(thread_count, image->rgba, image->pixel_count, bits_rgb, bits_alpha);
    1413              : 
    1414            4 :     unique_after = count_unique_rgba(image->rgba, image->pixel_count);
    1415              : 
    1416            4 :     if (applied_colors) {
    1417            4 :         if (unique_after > UINT32_MAX) {
    1418            0 :             unique_after = UINT32_MAX;
    1419              :         }
    1420              : 
    1421            4 :         *applied_colors = (uint32_t)unique_after;
    1422              :     }
    1423              : 
    1424            4 :     colopresso_log(CPRES_LOG_LEVEL_DEBUG,
    1425              :                    "PNGX: Reduced RGBA32 manual target enforcement trimmed %zu -> %zu colors",
    1426              :                    freq_count, unique_after);
    1427              : 
    1428            4 :     success = true;
    1429              : 
    1430            4 : bailout:
    1431            4 :     free(freq);
    1432            4 :     free(rank);
    1433            4 :     free(mapped);
    1434            4 :     free(keep_indices);
    1435              : 
    1436            4 :     return success;
    1437              : }
    1438              : 
    1439      4983312 : static inline uint8_t resolve_pixel_bits(uint8_t importance, uint8_t base_bits, uint8_t boost_bits)
    1440              : {
    1441      4983312 :     uint8_t resolved = base_bits, mid_bits;
    1442              : 
    1443      4983312 :     if (boost_bits <= base_bits) {
    1444       133648 :         return base_bits;
    1445              :     }
    1446              : 
    1447      4849664 :     if (importance >= PNGX_REDUCED_IMPORTANCE_LEVEL_FULL) {
    1448        30540 :         resolved = boost_bits;
    1449      4819124 :     } else if (importance >= PNGX_REDUCED_IMPORTANCE_LEVEL_HIGH) {
    1450       311036 :         mid_bits = (uint8_t)((base_bits + boost_bits + 1) / 2);
    1451       311036 :         resolved = mid_bits;
    1452      4508088 :     } else if (importance >= PNGX_REDUCED_IMPORTANCE_LEVEL_MEDIUM) {
    1453      1374458 :         mid_bits = (uint8_t)((base_bits * 2 + boost_bits + 2) / 3);
    1454      1374458 :         resolved = mid_bits;
    1455      3133630 :     } else if (importance >= PNGX_REDUCED_IMPORTANCE_LEVEL_LOW) {
    1456      1456982 :         resolved = (uint8_t)((base_bits * 3 + boost_bits + 3) / 4);
    1457              :     }
    1458              : 
    1459      4849664 :     if (resolved > boost_bits) {
    1460            0 :         resolved = boost_bits;
    1461              :     }
    1462              : 
    1463      4849664 :     if (resolved < base_bits) {
    1464            0 :         resolved = base_bits;
    1465              :     }
    1466              : 
    1467      4849664 :     return resolved;
    1468              : }
    1469              : 
    1470            5 : static inline void reduce_bitdepth_parallel_worker(void *context, uint32_t start, uint32_t end)
    1471              : {
    1472            5 :     reduce_bitdepth_parallel_ctx_t *ctx = (reduce_bitdepth_parallel_ctx_t *)context;
    1473              :     uint8_t importance, pixel_bits_rgb, pixel_bits_alpha;
    1474              :     size_t i, base;
    1475              : 
    1476            5 :     if (!ctx || !ctx->rgba) {
    1477            0 :         return;
    1478              :     }
    1479              : 
    1480        66829 :     for (i = (size_t)start; i < (size_t)end && i < ctx->pixel_count; ++i) {
    1481        66824 :         base = i * PNGX_RGBA_CHANNELS;
    1482        66824 :         importance =
    1483        66824 :             (ctx->importance_map && i < ctx->importance_map_len) ? ctx->importance_map[i] : 0;
    1484        66824 :         pixel_bits_rgb = resolve_pixel_bits(importance, ctx->bits_rgb, ctx->boost_bits_rgb);
    1485        66824 :         pixel_bits_alpha = resolve_pixel_bits(importance, ctx->bits_alpha, ctx->boost_bits_alpha);
    1486              : 
    1487        66824 :         if (ctx->bit_hint_map && i < ctx->bit_hint_len) {
    1488        66824 :             ctx->bit_hint_map[i] = (uint8_t)((pixel_bits_rgb << 4) | (pixel_bits_alpha & 0x0fu));
    1489              :         }
    1490              : 
    1491        66824 :         if (ctx->rgba[base + 3] > PNGX_REDUCED_ALPHA_NEAR_TRANSPARENT) {
    1492        66760 :             if (pixel_bits_rgb < PNGX_FULL_CHANNEL_BITS) {
    1493          200 :                 ctx->rgba[base + 0] = quantize_bits(ctx->rgba[base + 0], pixel_bits_rgb);
    1494          200 :                 ctx->rgba[base + 1] = quantize_bits(ctx->rgba[base + 1], pixel_bits_rgb);
    1495          200 :                 ctx->rgba[base + 2] = quantize_bits(ctx->rgba[base + 2], pixel_bits_rgb);
    1496              :             }
    1497              :         }
    1498              : 
    1499        66824 :         if (pixel_bits_alpha < PNGX_FULL_CHANNEL_BITS) {
    1500        66824 :             ctx->rgba[base + 3] = quantize_bits(ctx->rgba[base + 3], pixel_bits_alpha);
    1501              :         }
    1502              :     }
    1503              : }
    1504              : 
    1505            5 : static inline void reduce_rgba_custom_bitdepth_simple(
    1506              :     uint32_t thread_count, uint8_t *rgba, size_t pixel_count, uint8_t bits_rgb, uint8_t bits_alpha,
    1507              :     const uint8_t *importance_map, size_t importance_map_len, uint8_t boost_bits_rgb,
    1508              :     uint8_t boost_bits_alpha, uint8_t *bit_hint_map, size_t bit_hint_len)
    1509              : {
    1510              :     reduce_bitdepth_parallel_ctx_t ctx;
    1511              : 
    1512            5 :     if (!rgba || pixel_count == 0) {
    1513            0 :         return;
    1514              :     }
    1515              : 
    1516            5 :     ctx.rgba = rgba;
    1517            5 :     ctx.pixel_count = pixel_count;
    1518            5 :     ctx.importance_map = importance_map;
    1519            5 :     ctx.importance_map_len = importance_map_len;
    1520            5 :     ctx.bits_rgb = clamp_reduced_bits(bits_rgb);
    1521            5 :     ctx.bits_alpha = clamp_reduced_bits(bits_alpha);
    1522            5 :     ctx.boost_bits_rgb = clamp_reduced_bits(boost_bits_rgb);
    1523            5 :     ctx.boost_bits_alpha = clamp_reduced_bits(boost_bits_alpha);
    1524            5 :     ctx.bit_hint_map = bit_hint_map;
    1525            5 :     ctx.bit_hint_len = bit_hint_len;
    1526              : 
    1527              : #if COLOPRESSO_ENABLE_THREADS
    1528            5 :     colopresso_parallel_for(thread_count, (uint32_t)pixel_count, reduce_bitdepth_parallel_worker,
    1529              :                             &ctx);
    1530              : #else
    1531              :     reduce_bitdepth_parallel_worker(&ctx, 0, (uint32_t)pixel_count);
    1532              : #endif
    1533              : }
    1534              : 
    1535      2424832 : static inline void process_custom_bitdepth_pixel(uint8_t *rgba, png_uint_32 width,
    1536              :                                                  png_uint_32 height, uint32_t x, uint32_t y,
    1537              :                                                  uint8_t base_bits_rgb, uint8_t base_bits_alpha,
    1538              :                                                  uint8_t boost_bits_rgb, uint8_t boost_bits_alpha,
    1539              :                                                  float base_dither, const uint8_t *importance_map,
    1540              :                                                  size_t pixel_count, float *err_curr,
    1541              :                                                  float *err_next, bool left_to_right,
    1542              :                                                  uint8_t *bit_hint_map, size_t bit_hint_len)
    1543              : {
    1544      2424832 :     uint8_t importance = 0, pixel_bits_rgb, pixel_bits_alpha, channel, bits, quantized;
    1545      2424832 :     size_t pixel_index = (size_t)y * (size_t)width + (size_t)x,
    1546      2424832 :            rgba_index = pixel_index * PNGX_RGBA_CHANNELS,
    1547      2424832 :            err_index = (size_t)x * PNGX_RGBA_CHANNELS;
    1548      2424832 :     float dither = base_dither, value, error, alpha_factor, dither_ch;
    1549              : 
    1550      2424832 :     if (importance_map && pixel_index < pixel_count) {
    1551      2424832 :         importance = importance_map[pixel_index];
    1552      2424832 :         dither *= importance_dither_scale(importance);
    1553              :     }
    1554              : 
    1555      2424832 :     alpha_factor = (float)rgba[rgba_index + 3] / 255.0f;
    1556              : 
    1557      2424832 :     pixel_bits_rgb = resolve_pixel_bits(importance, base_bits_rgb, boost_bits_rgb);
    1558      2424832 :     pixel_bits_alpha = resolve_pixel_bits(importance, base_bits_alpha, boost_bits_alpha);
    1559              : 
    1560      2424832 :     if (bit_hint_map && pixel_index < bit_hint_len) {
    1561      2424832 :         bit_hint_map[pixel_index] = (uint8_t)((pixel_bits_rgb << 4) | (pixel_bits_alpha & 0x0fu));
    1562              :     }
    1563              : 
    1564     12124160 :     for (channel = 0; channel < PNGX_RGBA_CHANNELS; ++channel) {
    1565      9699328 :         if (channel != 3 && rgba[rgba_index + 3] <= PNGX_REDUCED_ALPHA_NEAR_TRANSPARENT) {
    1566        82944 :             bits = PNGX_FULL_CHANNEL_BITS;
    1567              :         } else {
    1568      9616384 :             bits = (channel == 3) ? pixel_bits_alpha : pixel_bits_rgb;
    1569              :         }
    1570              : 
    1571      9699328 :         if (bits >= PNGX_FULL_CHANNEL_BITS) {
    1572        82944 :             err_curr[err_index + channel] = 0.0f;
    1573              : 
    1574        82944 :             continue;
    1575              :         }
    1576              : 
    1577      9616384 :         value = (float)rgba[rgba_index + channel] + err_curr[err_index + channel];
    1578      9616384 :         quantized = quantize_channel_value(value, bits);
    1579      9616384 :         error = (value - (float)quantized);
    1580              : 
    1581      9616384 :         if (channel == 3) {
    1582      2424832 :             dither_ch = 0.0f;
    1583              :         } else {
    1584      7191552 :             dither_ch = dither * alpha_factor;
    1585              : 
    1586      7191552 :             if (alpha_factor < PNGX_REDUCED_ALPHA_MIN_DITHER_FACTOR) {
    1587        18432 :                 dither_ch = 0.0f;
    1588              :             }
    1589              :         }
    1590              : 
    1591      9616384 :         error *= dither_ch;
    1592              : 
    1593      9616384 :         rgba[rgba_index + channel] = quantized;
    1594      9616384 :         err_curr[err_index + channel] = 0.0f;
    1595              : 
    1596      9616384 :         if (dither_ch <= 0.0f || error == 0.0f) {
    1597      2573894 :             continue;
    1598              :         }
    1599              : 
    1600      7042490 :         if (left_to_right) {
    1601      3520033 :             if (x + 1 < width) {
    1602      3515946 :                 err_curr[err_index + PNGX_RGBA_CHANNELS + channel] += error * (7.0f / 16.0f);
    1603              :             }
    1604              : 
    1605      3520033 :             if (y + 1 < height) {
    1606      3520033 :                 if (x > 0) {
    1607      3515928 :                     err_next[err_index - PNGX_RGBA_CHANNELS + channel] += error * (3.0f / 16.0f);
    1608              :                 }
    1609              : 
    1610      3520033 :                 err_next[err_index + channel] += error * (5.0f / 16.0f);
    1611              : 
    1612      3520033 :                 if (x + 1 < width) {
    1613      3515946 :                     err_next[err_index + PNGX_RGBA_CHANNELS + channel] += error * (1.0f / 16.0f);
    1614              :                 }
    1615              :             }
    1616              :         } else {
    1617      3522457 :             if (x > 0) {
    1618      3518333 :                 err_curr[err_index - PNGX_RGBA_CHANNELS + channel] += error * (7.0f / 16.0f);
    1619              :             }
    1620              : 
    1621      3522457 :             if (y + 1 < height) {
    1622      3514481 :                 if (x + 1 < width) {
    1623      3510412 :                     err_next[err_index + PNGX_RGBA_CHANNELS + channel] += error * (3.0f / 16.0f);
    1624              :                 }
    1625              : 
    1626      3514481 :                 err_next[err_index + channel] += error * (5.0f / 16.0f);
    1627              : 
    1628      3514481 :                 if (x > 0) {
    1629      3510369 :                     err_next[err_index - PNGX_RGBA_CHANNELS + channel] += error * (1.0f / 16.0f);
    1630              :                 }
    1631              :             }
    1632              :         }
    1633              :     }
    1634      2424832 : }
    1635              : 
    1636            4 : static inline bool reduce_rgba_custom_bitdepth_dither(
    1637              :     uint8_t *rgba, png_uint_32 width, png_uint_32 height, uint8_t bits_rgb, uint8_t bits_alpha,
    1638              :     uint8_t boost_bits_rgb, uint8_t boost_bits_alpha, float dither_level,
    1639              :     const uint8_t *importance_map, size_t pixel_count, uint8_t *bit_hint_map, size_t bit_hint_len)
    1640              : {
    1641              :     uint32_t x, y;
    1642              :     size_t row_stride;
    1643              :     bool left_to_right;
    1644              :     float *err_curr, *err_next, *tmp;
    1645              : 
    1646            4 :     bits_rgb = clamp_reduced_bits(bits_rgb);
    1647            4 :     bits_alpha = clamp_reduced_bits(bits_alpha);
    1648            4 :     boost_bits_rgb = clamp_reduced_bits(boost_bits_rgb);
    1649            4 :     boost_bits_alpha = clamp_reduced_bits(boost_bits_alpha);
    1650              : 
    1651            4 :     if ((!rgba) || width == 0 || height == 0 ||
    1652            0 :         (bits_rgb >= PNGX_FULL_CHANNEL_BITS && bits_alpha >= PNGX_FULL_CHANNEL_BITS)) {
    1653            0 :         return true;
    1654              :     }
    1655              : 
    1656            4 :     row_stride = (size_t)width * PNGX_RGBA_CHANNELS;
    1657            4 :     err_curr = (float *)calloc(row_stride, sizeof(float));
    1658            4 :     err_next = (float *)calloc(row_stride, sizeof(float));
    1659            4 :     if (!err_curr || !err_next) {
    1660            0 :         free(err_curr);
    1661            0 :         free(err_next);
    1662            0 :         colopresso_log(CPRES_LOG_LEVEL_ERROR, "PNGX: Reduced RGBA32 dither allocation failed");
    1663              : 
    1664            0 :         return false;
    1665              :     }
    1666              : 
    1667         2820 :     for (y = 0; y < height; ++y) {
    1668         2816 :         left_to_right = ((y & 1) == 0);
    1669         2816 :         memset(err_next, 0, row_stride * sizeof(float));
    1670              : 
    1671         2816 :         if (left_to_right) {
    1672      1213824 :             for (x = 0; x < width; ++x) {
    1673      1212416 :                 process_custom_bitdepth_pixel(rgba, width, height, x, y, bits_rgb, bits_alpha,
    1674              :                                               boost_bits_rgb, boost_bits_alpha, dither_level,
    1675              :                                               importance_map, pixel_count, err_curr, err_next, true,
    1676              :                                               bit_hint_map, bit_hint_len);
    1677              :             }
    1678              :         } else {
    1679         1408 :             x = width;
    1680      1213824 :             while (x-- > 0) {
    1681      1212416 :                 process_custom_bitdepth_pixel(rgba, width, height, x, y, bits_rgb, bits_alpha,
    1682              :                                               boost_bits_rgb, boost_bits_alpha, dither_level,
    1683              :                                               importance_map, pixel_count, err_curr, err_next,
    1684              :                                               false, bit_hint_map, bit_hint_len);
    1685              :             }
    1686              :         }
    1687              : 
    1688         2816 :         tmp = err_curr;
    1689         2816 :         err_curr = err_next;
    1690         2816 :         err_next = tmp;
    1691              :     }
    1692              : 
    1693            4 :     free(err_curr);
    1694            4 :     free(err_next);
    1695              : 
    1696            4 :     return true;
    1697              : }
    1698              : 
    1699            9 : static inline bool reduce_rgba_custom_bitdepth(uint32_t thread_count, uint8_t *rgba,
    1700              :                                                png_uint_32 width, png_uint_32 height,
    1701              :                                                uint8_t bits_rgb, uint8_t bits_alpha,
    1702              :                                                float dither_level, const uint8_t *importance_map,
    1703              :                                                size_t importance_map_len,
    1704              :                                                pngx_quant_support_t *support)
    1705              : {
    1706            9 :     uint8_t boost_bits_rgb, boost_bits_alpha, *bit_hint_map = NULL;
    1707            9 :     size_t bit_hint_len = 0, pixel_count, hint_len;
    1708              :     bool need_rgb, need_alpha;
    1709              : 
    1710            9 :     if (!rgba || width == 0 || height == 0) {
    1711            0 :         return true;
    1712              :     }
    1713              : 
    1714            9 :     bits_rgb = clamp_reduced_bits(bits_rgb);
    1715            9 :     bits_alpha = clamp_reduced_bits(bits_alpha);
    1716            9 :     need_rgb = bits_rgb < PNGX_FULL_CHANNEL_BITS;
    1717            9 :     need_alpha = bits_alpha < PNGX_FULL_CHANNEL_BITS;
    1718            9 :     if (!need_rgb && !need_alpha) {
    1719            0 :         return true;
    1720              :     }
    1721              : 
    1722            9 :     pixel_count = (size_t)width * (size_t)height;
    1723            9 :     if (!importance_map || importance_map_len < pixel_count) {
    1724            5 :         importance_map = NULL;
    1725              :     }
    1726              : 
    1727            9 :     boost_bits_rgb = (importance_map && bits_rgb < PNGX_FULL_CHANNEL_BITS)
    1728            4 :                          ? clamp_reduced_bits((uint8_t)(bits_rgb + 3))
    1729              :                          : bits_rgb;
    1730            9 :     boost_bits_alpha = (importance_map && bits_alpha < PNGX_FULL_CHANNEL_BITS)
    1731            4 :                            ? clamp_reduced_bits((uint8_t)(bits_alpha + 2))
    1732              :                            : bits_alpha;
    1733              : 
    1734            9 :     if (support) {
    1735            9 :         free(support->bit_hint_map);
    1736            9 :         support->bit_hint_map = NULL;
    1737            9 :         support->bit_hint_len = 0;
    1738              : 
    1739            9 :         if (pixel_count > 0) {
    1740            9 :             support->bit_hint_map = (uint8_t *)malloc(pixel_count);
    1741            9 :             if (support->bit_hint_map) {
    1742            9 :                 support->bit_hint_len = pixel_count;
    1743            9 :                 bit_hint_map = support->bit_hint_map;
    1744            9 :                 bit_hint_len = support->bit_hint_len;
    1745              :             }
    1746              :         }
    1747              :     }
    1748              : 
    1749            9 :     if (dither_level > 0.0f) {
    1750            4 :         if (!reduce_rgba_custom_bitdepth_dither(
    1751              :                 rgba, width, height, bits_rgb, bits_alpha, boost_bits_rgb, boost_bits_alpha,
    1752              :                 dither_level, importance_map, pixel_count, bit_hint_map, bit_hint_len)) {
    1753            0 :             return false;
    1754              :         }
    1755              :     } else {
    1756            5 :         hint_len = importance_map ? pixel_count : 0;
    1757            5 :         reduce_rgba_custom_bitdepth_simple(thread_count, rgba, pixel_count, bits_rgb, bits_alpha,
    1758              :                                            importance_map, hint_len, boost_bits_rgb,
    1759              :                                            boost_bits_alpha, bit_hint_map, bit_hint_len);
    1760              :     }
    1761              : 
    1762            9 :     return true;
    1763              : }
    1764              : 
    1765            9 : static inline uint32_t collect_alpha_levels(const pngx_rgba_image_t *image, float *non_opaque_ratio)
    1766              : {
    1767            9 :     uint32_t unique = 0, non_opaque = 0;
    1768              :     uint8_t alpha;
    1769              :     size_t i;
    1770            9 :     bool level_used[256] = {false};
    1771              : 
    1772            9 :     if (!image || !image->rgba || image->pixel_count == 0) {
    1773            0 :         if (non_opaque_ratio) {
    1774            0 :             *non_opaque_ratio = 0.0f;
    1775              :         }
    1776              : 
    1777            0 :         return 0;
    1778              :     }
    1779              : 
    1780      2491665 :     for (i = 0; i < image->pixel_count; ++i) {
    1781      2491656 :         alpha = image->rgba[i * 4 + 3];
    1782      2491656 :         if (!level_used[alpha]) {
    1783          932 :             level_used[alpha] = true;
    1784          932 :             ++unique;
    1785              :         }
    1786              : 
    1787      2491656 :         if (alpha < 255) {
    1788      2420117 :             ++non_opaque;
    1789              :         }
    1790              :     }
    1791              : 
    1792            9 :     if (non_opaque_ratio) {
    1793            9 :         *non_opaque_ratio =
    1794            9 :             (image->pixel_count > 0) ? ((float)non_opaque / (float)image->pixel_count) : 0.0f;
    1795              :     }
    1796              : 
    1797            9 :     return unique;
    1798              : }
    1799              : 
    1800            9 : static inline uint8_t bits_for_level_count(uint32_t levels)
    1801              : {
    1802            9 :     uint32_t capacity = 1;
    1803            9 :     uint8_t bits = 1;
    1804              : 
    1805            9 :     if (levels == 0) {
    1806            0 :         return 1;
    1807              :     }
    1808              : 
    1809           41 :     while (capacity < levels && bits < COLOPRESSO_PNGX_REDUCED_BITS_MAX) {
    1810           32 :         ++bits;
    1811           32 :         capacity <<= 1;
    1812              :     }
    1813              : 
    1814            9 :     return clamp_reduced_bits(bits);
    1815              : }
    1816              : 
    1817            9 : static inline void tune_reduced_bitdepth(const pngx_rgba_image_t *image,
    1818              :                                          const pngx_image_stats_t *stats, uint8_t *bits_rgb,
    1819              :                                          uint8_t *bits_alpha)
    1820              : {
    1821              :     uint32_t alpha_levels;
    1822              :     uint8_t tuned_rgb, tuned_alpha, level_bits, next_alpha;
    1823              :     float gradient, saturation, vibrant, opaque, translucent, non_opaque_ratio;
    1824              : 
    1825            9 :     if (!bits_rgb || !bits_alpha) {
    1826            0 :         return;
    1827              :     }
    1828              : 
    1829            9 :     tuned_rgb = clamp_reduced_bits(*bits_rgb);
    1830            9 :     tuned_alpha = clamp_reduced_bits(*bits_alpha);
    1831            9 :     gradient = stats ? stats->gradient_mean : PNGX_REDUCED_TUNE_DEFAULT_GRADIENT;
    1832            9 :     saturation = stats ? stats->saturation_mean : PNGX_REDUCED_TUNE_DEFAULT_SATURATION;
    1833            9 :     vibrant = stats ? stats->vibrant_ratio : PNGX_REDUCED_TUNE_DEFAULT_VIBRANT;
    1834            9 :     opaque = stats ? stats->opaque_ratio : PNGX_REDUCED_TUNE_DEFAULT_OPAQUE;
    1835            9 :     translucent = stats ? stats->translucent_ratio : PNGX_REDUCED_TUNE_DEFAULT_TRANSLUCENT;
    1836              : 
    1837            9 :     if (gradient < PNGX_REDUCED_TUNE_FLAT_GRADIENT_THRESHOLD &&
    1838            0 :         saturation < PNGX_REDUCED_TUNE_FLAT_SATURATION_THRESHOLD &&
    1839            0 :         vibrant < PNGX_REDUCED_TUNE_FLAT_VIBRANT_THRESHOLD && tuned_rgb > 3) {
    1840            0 :         --tuned_rgb;
    1841            0 :         if (gradient < PNGX_REDUCED_TUNE_VERY_FLAT_GRADIENT &&
    1842            0 :             saturation < PNGX_REDUCED_TUNE_VERY_FLAT_SATURATION && tuned_rgb > 3) {
    1843            0 :             --tuned_rgb;
    1844              :         }
    1845              : 
    1846            0 :         if (tuned_rgb < 3) {
    1847            0 :             tuned_rgb = 3;
    1848              :         }
    1849              :     }
    1850              : 
    1851            9 :     non_opaque_ratio = 0.0f;
    1852            9 :     alpha_levels = collect_alpha_levels(image, &non_opaque_ratio);
    1853            9 :     if (alpha_levels > 0) {
    1854            9 :         level_bits = bits_for_level_count(alpha_levels);
    1855            9 :         if (alpha_levels <= PNGX_REDUCED_ALPHA_LEVEL_LIMIT_FEW &&
    1856            5 :             non_opaque_ratio < PNGX_REDUCED_ALPHA_RATIO_FEW && tuned_alpha > level_bits) {
    1857            3 :             tuned_alpha = (level_bits < 2) ? 2 : level_bits;
    1858            6 :         } else if (alpha_levels <= PNGX_REDUCED_ALPHA_NEAR_TRANSPARENT &&
    1859            2 :                    non_opaque_ratio < PNGX_REDUCED_ALPHA_RATIO_LOW &&
    1860            0 :                    tuned_alpha > (uint8_t)(level_bits + 1)) {
    1861            0 :             tuned_alpha = (uint8_t)(level_bits + 1);
    1862            6 :         } else if (alpha_levels <= 16 && non_opaque_ratio < PNGX_REDUCED_ALPHA_RATIO_MINIMAL &&
    1863            0 :                    tuned_alpha > (uint8_t)(level_bits + 2)) {
    1864            0 :             tuned_alpha = (uint8_t)(level_bits + 2);
    1865              :         }
    1866              : 
    1867            9 :         if (opaque > PNGX_REDUCED_ALPHA_OPAQUE_LIMIT && level_bits <= 2 && tuned_alpha > 2) {
    1868            0 :             tuned_alpha = 2;
    1869            9 :         } else if (translucent < PNGX_REDUCED_ALPHA_TRANSLUCENT_LIMIT && tuned_alpha > level_bits) {
    1870            3 :             next_alpha = (uint8_t)(tuned_alpha - 1);
    1871            3 :             if (next_alpha < level_bits) {
    1872            0 :                 next_alpha = level_bits;
    1873              :             }
    1874              : 
    1875            3 :             tuned_alpha = next_alpha;
    1876              :         }
    1877              :     }
    1878              : 
    1879            9 :     if (tuned_alpha < COLOPRESSO_PNGX_REDUCED_BITS_MIN) {
    1880            0 :         tuned_alpha = COLOPRESSO_PNGX_REDUCED_BITS_MIN;
    1881              :     }
    1882              : 
    1883            9 :     *bits_rgb = tuned_rgb;
    1884            9 :     *bits_alpha = tuned_alpha;
    1885              : }
    1886              : 
    1887            9 : static inline bool apply_reduced_rgba32_prepass(pngx_rgba_image_t *image,
    1888              :                                                 const pngx_options_t *opts,
    1889              :                                                 pngx_quant_support_t *support,
    1890              :                                                 pngx_image_stats_t *stats)
    1891              : {
    1892            9 :     const uint8_t *importance = NULL;
    1893            9 :     uint8_t bits_rgb = COLOPRESSO_PNGX_DEFAULT_REDUCED_BITS_RGB,
    1894            9 :             bits_alpha = COLOPRESSO_PNGX_DEFAULT_REDUCED_ALPHA_BITS;
    1895            9 :     size_t importance_len = 0;
    1896            9 :     float dither = 0.0f;
    1897              : 
    1898            9 :     if (!image || !image->rgba || image->pixel_count == 0) {
    1899            0 :         return true;
    1900              :     }
    1901              : 
    1902            9 :     if (opts) {
    1903            9 :         bits_rgb = clamp_reduced_bits(opts->lossy_reduced_bits_rgb);
    1904            9 :         bits_alpha = clamp_reduced_bits(opts->lossy_reduced_alpha_bits);
    1905              : 
    1906            9 :         if (opts->lossy_dither_auto) {
    1907            1 :             dither = resolve_quant_dither(opts, stats);
    1908              :         } else {
    1909            8 :             dither = clamp_float(opts->lossy_dither_level, 0.0f, 1.0f);
    1910              :         }
    1911              : 
    1912            9 :         if (support && support->importance_map &&
    1913            4 :             support->importance_map_len >= image->pixel_count) {
    1914            4 :             importance = support->importance_map;
    1915            4 :             importance_len = support->importance_map_len;
    1916              :         }
    1917              :     }
    1918              : 
    1919            9 :     return reduce_rgba_custom_bitdepth(opts->thread_count, image->rgba, image->width, image->height,
    1920              :                                        bits_rgb, bits_alpha, dither, importance, importance_len,
    1921              :                                        support);
    1922              : }
    1923              : 
    1924          552 : static inline void head_heap_sift_up(uint64_t *heap, size_t index)
    1925              : {
    1926              :     uint64_t parent_value;
    1927              :     size_t parent;
    1928              : 
    1929          868 :     while (index > 0) {
    1930          843 :         parent = (index - 1) / 2;
    1931          843 :         parent_value = heap[parent];
    1932              : 
    1933          843 :         if (parent_value <= heap[index]) {
    1934          527 :             break;
    1935              :         }
    1936              : 
    1937          316 :         heap[parent] = heap[index];
    1938          316 :         heap[index] = parent_value;
    1939          316 :         index = parent;
    1940              :     }
    1941          552 : }
    1942              : 
    1943         1528 : static inline void head_heap_sift_down(uint64_t *heap, size_t size, size_t index)
    1944              : {
    1945              :     uint64_t tmp;
    1946              :     size_t left, right, smallest;
    1947              : 
    1948              :     while (true) {
    1949         7556 :         left = index * 2 + 1;
    1950         7556 :         right = left + 1;
    1951         7556 :         smallest = index;
    1952              : 
    1953         7556 :         if (left < size && heap[left] < heap[smallest]) {
    1954         5567 :             smallest = left;
    1955              :         }
    1956              : 
    1957         7556 :         if (right < size && heap[right] < heap[smallest]) {
    1958         2877 :             smallest = right;
    1959              :         }
    1960              : 
    1961         7556 :         if (smallest == index) {
    1962         1528 :             break;
    1963              :         }
    1964              : 
    1965         6028 :         tmp = heap[index];
    1966         6028 :         heap[index] = heap[smallest];
    1967         6028 :         heap[smallest] = tmp;
    1968         6028 :         index = smallest;
    1969              :     }
    1970         1528 : }
    1971              : 
    1972           12 : static inline float histogram_head_dominance(const color_histogram_t *hist, size_t head_limit)
    1973              : {
    1974           12 :     uint64_t heap[PNGX_REDUCED_HEAD_DOMINANCE_LIMIT], weight, total_weight = 0, head_sum = 0;
    1975           12 :     size_t heap_size = 0, capacity, i;
    1976              : 
    1977           12 :     if (!hist || hist->count == 0 || head_limit == 0) {
    1978            0 :         return 0.0f;
    1979              :     }
    1980              : 
    1981           12 :     capacity = head_limit;
    1982           12 :     if (capacity > PNGX_REDUCED_HEAD_DOMINANCE_LIMIT) {
    1983            0 :         capacity = PNGX_REDUCED_HEAD_DOMINANCE_LIMIT;
    1984              :     }
    1985              : 
    1986        28800 :     for (i = 0; i < hist->count; ++i) {
    1987        28788 :         weight = hist->entries[i].count ? hist->entries[i].count : 1;
    1988        28788 :         total_weight += weight;
    1989              : 
    1990        28788 :         if (capacity == 0) {
    1991            0 :             continue;
    1992              :         }
    1993              : 
    1994        28788 :         if (heap_size < capacity) {
    1995          552 :             heap[heap_size] = weight;
    1996          552 :             ++heap_size;
    1997          552 :             head_heap_sift_up(heap, heap_size - 1);
    1998        28236 :         } else if (weight > heap[0]) {
    1999         1528 :             heap[0] = weight;
    2000         1528 :             head_heap_sift_down(heap, heap_size, 0);
    2001              :         }
    2002              :     }
    2003              : 
    2004           12 :     if (total_weight == 0 || heap_size == 0) {
    2005            0 :         return 0.0f;
    2006              :     }
    2007              : 
    2008          564 :     for (i = 0; i < heap_size; ++i) {
    2009          552 :         head_sum += heap[i];
    2010              :     }
    2011              : 
    2012           12 :     return (float)head_sum / (float)total_weight;
    2013              : }
    2014              : 
    2015           12 : static inline float histogram_low_weight_ratio(const color_histogram_t *hist,
    2016              :                                                uint32_t weight_threshold)
    2017              : {
    2018              :     const color_entry_t *entry;
    2019           12 :     uint64_t total_weight = 0, low_weight = 0, weight;
    2020              :     size_t i;
    2021              : 
    2022           12 :     if (!hist || hist->count == 0 || weight_threshold == 0) {
    2023            0 :         return 0.0f;
    2024              :     }
    2025              : 
    2026        28800 :     for (i = 0; i < hist->count; ++i) {
    2027        28788 :         entry = &hist->entries[i];
    2028        28788 :         weight = entry->count ? entry->count : 1;
    2029        28788 :         total_weight += weight;
    2030              : 
    2031        28788 :         if (entry->count <= weight_threshold) {
    2032        17648 :             low_weight += weight;
    2033              :         }
    2034              :     }
    2035              : 
    2036           12 :     if (total_weight == 0) {
    2037            0 :         return 0.0f;
    2038              :     }
    2039              : 
    2040           12 :     return (float)low_weight / (float)total_weight;
    2041              : }
    2042              : 
    2043           12 : static inline float histogram_detail_pressure(const color_histogram_t *hist, uint8_t base_bits_rgb,
    2044              :                                               uint8_t base_bits_alpha)
    2045              : {
    2046              :     const color_entry_t *entry;
    2047           12 :     uint64_t total_weight = 0, detail_weight = 0, weight;
    2048              :     size_t i;
    2049              : 
    2050           12 :     if (!hist || hist->count == 0) {
    2051            0 :         return 0.0f;
    2052              :     }
    2053              : 
    2054        28800 :     for (i = 0; i < hist->count; ++i) {
    2055        28788 :         entry = &hist->entries[i];
    2056        28788 :         weight = entry->count ? entry->count : 1;
    2057        28788 :         total_weight += weight;
    2058              : 
    2059        28788 :         if (entry->detail_bits_rgb > base_bits_rgb || entry->detail_bits_alpha > base_bits_alpha) {
    2060        13338 :             detail_weight += weight;
    2061              :         }
    2062              :     }
    2063              : 
    2064           12 :     if (total_weight == 0) {
    2065            0 :         return 0.0f;
    2066              :     }
    2067              : 
    2068           12 :     return (float)detail_weight / (float)total_weight;
    2069              : }
    2070              : 
    2071           12 : static inline float stats_flatness_factor(const pngx_image_stats_t *stats)
    2072              : {
    2073           12 :     float gradient = stats ? stats->gradient_mean : PNGX_REDUCED_STATS_FLAT_DEFAULT_GRADIENT,
    2074           12 :           saturation = stats ? stats->saturation_mean : PNGX_REDUCED_STATS_FLAT_DEFAULT_SATURATION,
    2075           12 :           vibrant = stats ? stats->vibrant_ratio : PNGX_REDUCED_STATS_FLAT_DEFAULT_VIBRANT,
    2076           12 :           gradient_term = clamp_float((PNGX_REDUCED_STATS_FLAT_GRADIENT_REF - gradient) /
    2077              :                                           PNGX_REDUCED_STATS_FLAT_GRADIENT_REF,
    2078              :                                       0.0f, 1.0f),
    2079           12 :           saturation_term = clamp_float((PNGX_REDUCED_STATS_FLAT_SATURATION_REF - saturation) /
    2080              :                                             PNGX_REDUCED_STATS_FLAT_SATURATION_REF,
    2081              :                                         0.0f, 1.0f),
    2082           12 :           vibrant_term = clamp_float((PNGX_REDUCED_VIBRANT_RATIO_LOW - vibrant) /
    2083              :                                          PNGX_REDUCED_VIBRANT_RATIO_LOW,
    2084              :                                      0.0f, 1.0f);
    2085              : 
    2086           12 :     return (gradient_term * PNGX_REDUCED_STATS_FLAT_GRADIENT_WEIGHT) +
    2087           24 :            (saturation_term * PNGX_REDUCED_STATS_FLAT_SATURATION_WEIGHT) +
    2088           12 :            (vibrant_term * PNGX_REDUCED_STATS_FLAT_VIBRANT_WEIGHT);
    2089              : }
    2090              : 
    2091           12 : static inline float stats_alpha_simplicity(const pngx_image_stats_t *stats)
    2092              : {
    2093           12 :     float opaque = stats ? stats->opaque_ratio : PNGX_REDUCED_ALPHA_SIMPLE_DEFAULT_OPAQUE,
    2094           12 :           translucent =
    2095           12 :               stats ? stats->translucent_ratio : PNGX_REDUCED_ALPHA_SIMPLE_DEFAULT_TRANSLUCENT,
    2096           12 :           opaque_term = clamp_float((opaque - PNGX_REDUCED_ALPHA_SIMPLE_OPAQUE_REF) /
    2097              :                                         PNGX_REDUCED_ALPHA_SIMPLE_OPAQUE_RANGE,
    2098              :                                     0.0f, 1.0f),
    2099           12 :           translucent_term = clamp_float((PNGX_REDUCED_ALPHA_SIMPLE_TRANSLUCENT_REF - translucent) /
    2100              :                                              PNGX_REDUCED_ALPHA_SIMPLE_TRANSLUCENT_RANGE,
    2101              :                                          0.0f, 1.0f);
    2102              : 
    2103           24 :     return (opaque_term * PNGX_REDUCED_ALPHA_SIMPLE_OPAQUE_WEIGHT) +
    2104           12 :            (translucent_term * PNGX_REDUCED_ALPHA_SIMPLE_TRANSLUCENT_WEIGHT);
    2105              : }
    2106              : 
    2107            9 : static inline uint32_t resolve_reduced_passthrough_threshold(uint32_t grid_cap,
    2108              :                                                              const pngx_image_stats_t *stats)
    2109              : {
    2110              :     uint32_t threshold;
    2111            9 :     float gradient = stats ? stats->gradient_mean : PNGX_REDUCED_PASSTHROUGH_DEFAULT_GRADIENT,
    2112            9 :           saturation = stats ? stats->saturation_mean : PNGX_REDUCED_PASSTHROUGH_DEFAULT_SATURATION,
    2113            9 :           vibrant = stats ? stats->vibrant_ratio : PNGX_REDUCED_PASSTHROUGH_DEFAULT_VIBRANT,
    2114            9 :           ratio = PNGX_REDUCED_PASSTHROUGH_RATIO_BASE +
    2115            9 :                   clamp_float((gradient * PNGX_REDUCED_PASSTHROUGH_GRADIENT_WEIGHT) +
    2116            9 :                                   (saturation * PNGX_REDUCED_PASSTHROUGH_SATURATION_WEIGHT) +
    2117            9 :                                   (vibrant * PNGX_REDUCED_PASSTHROUGH_VIBRANT_WEIGHT),
    2118            9 :                               0.0f, 1.0f) *
    2119              :                       PNGX_REDUCED_PASSTHROUGH_RATIO_GAIN;
    2120              : 
    2121            9 :     if (ratio > PNGX_REDUCED_PASSTHROUGH_RATIO_CAP) {
    2122            0 :         ratio = PNGX_REDUCED_PASSTHROUGH_RATIO_CAP;
    2123              :     }
    2124              : 
    2125            9 :     threshold = (uint32_t)((float)grid_cap * ratio + PNGX_REDUCED_ROUNDING_OFFSET);
    2126            9 :     if (threshold < PNGX_REDUCED_RGBA32_PASSTHROUGH_MIN_COLORS) {
    2127            1 :         threshold = PNGX_REDUCED_RGBA32_PASSTHROUGH_MIN_COLORS;
    2128              :     }
    2129              : 
    2130            9 :     if (threshold > grid_cap) {
    2131            1 :         threshold = grid_cap;
    2132              :     }
    2133              : 
    2134            9 :     return threshold;
    2135              : }
    2136              : 
    2137            6 : static inline uint32_t compute_auto_trim_limit(const color_histogram_t *hist, size_t pixel_count,
    2138              :                                                uint32_t actual_colors, uint8_t bits_rgb,
    2139              :                                                uint8_t bits_alpha, const pngx_image_stats_t *stats)
    2140              : {
    2141              :     uint32_t low_weight_threshold, limit;
    2142              :     double density;
    2143              :     float low_weight_ratio, detail_pressure, head_dominance, flatness, alpha_simple, density_f,
    2144            6 :         trim = 0.0f, head_term, tail_term;
    2145              : 
    2146            6 :     if (!hist || hist->count == 0 || pixel_count == 0 ||
    2147              :         actual_colors <=
    2148              :             (uint32_t)(COLOPRESSO_PNGX_REDUCED_COLORS_MIN + PNGX_REDUCED_TRIM_MIN_COLOR_MARGIN)) {
    2149            2 :         return 0;
    2150              :     }
    2151              : 
    2152            4 :     density = (double)hist->count / (double)pixel_count;
    2153            4 :     density_f = (float)density;
    2154            4 :     low_weight_threshold = (uint32_t)(pixel_count / PNGX_REDUCED_LOW_WEIGHT_DIVISOR);
    2155            4 :     if (low_weight_threshold < PNGX_REDUCED_LOW_WEIGHT_MIN) {
    2156            1 :         low_weight_threshold = PNGX_REDUCED_LOW_WEIGHT_MIN;
    2157            3 :     } else if (low_weight_threshold > PNGX_REDUCED_LOW_WEIGHT_MAX) {
    2158            0 :         low_weight_threshold = PNGX_REDUCED_LOW_WEIGHT_MAX;
    2159              :     }
    2160              : 
    2161            4 :     low_weight_ratio = histogram_low_weight_ratio(hist, low_weight_threshold);
    2162            4 :     detail_pressure = histogram_detail_pressure(hist, bits_rgb, bits_alpha);
    2163            4 :     head_dominance = histogram_head_dominance(hist, PNGX_REDUCED_HEAD_DOMINANCE_LIMIT);
    2164            4 :     flatness = stats_flatness_factor(stats);
    2165            4 :     alpha_simple = stats_alpha_simplicity(stats);
    2166              : 
    2167            4 :     if (head_dominance > PNGX_REDUCED_TRIM_HEAD_DOMINANCE_THRESHOLD &&
    2168              :         detail_pressure < PNGX_REDUCED_TRIM_DETAIL_PRESSURE_HEAD_LIMIT) {
    2169            0 :         head_term = clamp_float(
    2170            0 :             (head_dominance - PNGX_REDUCED_TRIM_HEAD_DOMINANCE_THRESHOLD) *
    2171            0 :                 (PNGX_REDUCED_TRIM_HEAD_WEIGHT + flatness * PNGX_REDUCED_TRIM_FLATNESS_WEIGHT),
    2172              :             0.0f, PNGX_REDUCED_TRIM_HEAD_CLAMP);
    2173            0 :         trim += head_term;
    2174              :     }
    2175              : 
    2176            4 :     if (low_weight_ratio > PNGX_REDUCED_TRIM_TAIL_RATIO_THRESHOLD &&
    2177              :         detail_pressure < PNGX_REDUCED_TRIM_DETAIL_PRESSURE_TAIL_LIMIT) {
    2178            2 :         tail_term = (low_weight_ratio - PNGX_REDUCED_TRIM_TAIL_RATIO_THRESHOLD) *
    2179            2 :                     (PNGX_REDUCED_TRIM_TAIL_BASE_WEIGHT +
    2180            2 :                      PNGX_REDUCED_TRIM_TAIL_DETAIL_WEIGHT * (1.0f - detail_pressure));
    2181            2 :         trim += clamp_float(tail_term, 0.0f, PNGX_REDUCED_TRIM_TAIL_CLAMP);
    2182              :     }
    2183              : 
    2184            4 :     if (density_f < PNGX_REDUCED_TRIM_DENSITY_THRESHOLD) {
    2185            3 :         trim += clamp_float((PNGX_REDUCED_TRIM_DENSITY_THRESHOLD - density_f) *
    2186              :                                 PNGX_REDUCED_TRIM_DENSITY_SCALE,
    2187              :                             0.0f, PNGX_REDUCED_TRIM_DENSITY_CLAMP);
    2188              :     }
    2189              : 
    2190            4 :     if (alpha_simple > PNGX_REDUCED_TRIM_ALPHA_SIMPLE_THRESHOLD &&
    2191              :         detail_pressure < PNGX_REDUCED_TRIM_DETAIL_PRESSURE_HEAD_LIMIT) {
    2192            2 :         trim += clamp_float(alpha_simple * PNGX_REDUCED_TRIM_ALPHA_SIMPLE_SCALE, 0.0f,
    2193              :                             PNGX_REDUCED_TRIM_ALPHA_SIMPLE_CLAMP);
    2194              :     }
    2195              : 
    2196            4 :     if (flatness > PNGX_REDUCED_TRIM_FLATNESS_THRESHOLD &&
    2197              :         detail_pressure < PNGX_REDUCED_TRIM_DETAIL_PRESSURE_FLAT_LIMIT) {
    2198            2 :         trim += clamp_float((flatness - PNGX_REDUCED_TRIM_FLATNESS_THRESHOLD) *
    2199              :                                 PNGX_REDUCED_TRIM_FLATNESS_SCALE,
    2200              :                             0.0f, PNGX_REDUCED_TRIM_FLATNESS_CLAMP);
    2201              :     }
    2202              : 
    2203            4 :     trim = clamp_float(trim, 0.0f, PNGX_REDUCED_TRIM_TOTAL_CLAMP);
    2204            4 :     if (trim < PNGX_REDUCED_TRIM_MIN_TRIGGER) {
    2205            0 :         return 0;
    2206              :     }
    2207              : 
    2208            4 :     limit = (uint32_t)((float)actual_colors * (1.0f - trim) + PNGX_REDUCED_ROUNDING_OFFSET);
    2209            4 :     if (limit < (uint32_t)COLOPRESSO_PNGX_REDUCED_COLORS_MIN) {
    2210            0 :         limit = (uint32_t)COLOPRESSO_PNGX_REDUCED_COLORS_MIN;
    2211              :     }
    2212              : 
    2213            4 :     if (actual_colors <= limit || (actual_colors - limit) < PNGX_REDUCED_TRIM_MIN_COLOR_DIFF) {
    2214            2 :         return 0;
    2215              :     }
    2216              : 
    2217            2 :     return limit;
    2218              : }
    2219              : 
    2220            8 : static inline uint32_t resolve_reduced_rgba32_target(const color_histogram_t *hist,
    2221              :                                                      size_t pixel_count, int32_t hint,
    2222              :                                                      uint8_t bits_rgb, uint8_t bits_alpha,
    2223              :                                                      const pngx_image_stats_t *stats)
    2224              : {
    2225            8 :     uint64_t rgb_cap = 0, alpha_cap = 0;
    2226            8 :     uint32_t target, grid_cap = COLOPRESSO_PNGX_REDUCED_COLORS_MAX, low_weight_threshold;
    2227            8 :     size_t unique_colors = hist ? hist->count : 0, unlocked = hist ? hist->unlocked_count : 0;
    2228              :     double base, density;
    2229            8 :     float low_weight_ratio = 0.0f, detail_pressure = 0.0f, reduction_scale, boost_scale,
    2230            8 :           flatness_score = 0.0f, alpha_simple = 0.0f, head_dominance = 0.0f, tail_cut,
    2231              :           dominance_cut, gentle_cut, applied_cut, tail_gain, dominance_gain, relief,
    2232              :           gradient_relief, softness_relief, detail_relief, combined_cut, density_gap,
    2233              :           flatness_reduction, alpha_reduction, span_ratio;
    2234              : 
    2235            8 :     if (unique_colors == 0) {
    2236            0 :         return 0;
    2237              :     }
    2238              : 
    2239            8 :     density = (pixel_count > 0) ? ((double)unique_colors / (double)pixel_count) : 0.0;
    2240              : 
    2241            8 :     bits_rgb = clamp_reduced_bits(bits_rgb);
    2242            8 :     bits_alpha = clamp_reduced_bits(bits_alpha);
    2243              : 
    2244            8 :     if (bits_rgb < 8) {
    2245            6 :         rgb_cap = (uint64_t)1 << (bits_rgb * 3);
    2246              :     }
    2247              : 
    2248            8 :     if (bits_alpha < 8) {
    2249            8 :         alpha_cap = (uint64_t)1 << bits_alpha;
    2250              :     }
    2251              : 
    2252            8 :     if (rgb_cap == 0) {
    2253            2 :         rgb_cap = COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
    2254              :     }
    2255              : 
    2256            8 :     if (alpha_cap == 0) {
    2257            0 :         alpha_cap = 1;
    2258              :     }
    2259              : 
    2260            8 :     if (rgb_cap > (uint64_t)COLOPRESSO_PNGX_REDUCED_COLORS_MAX) {
    2261            0 :         rgb_cap = COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
    2262              :     }
    2263              : 
    2264            8 :     if (alpha_cap > (uint64_t)COLOPRESSO_PNGX_REDUCED_COLORS_MAX) {
    2265            0 :         alpha_cap = COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
    2266              :     }
    2267              : 
    2268            8 :     if (rgb_cap * alpha_cap < (uint64_t)COLOPRESSO_PNGX_REDUCED_COLORS_MAX) {
    2269            2 :         grid_cap = (uint32_t)(rgb_cap * alpha_cap);
    2270              :     }
    2271              : 
    2272            8 :     if (hint > 0) {
    2273            2 :         target = (uint32_t)hint;
    2274            2 :         if (target < COLOPRESSO_PNGX_REDUCED_COLORS_MIN) {
    2275            0 :             target = COLOPRESSO_PNGX_REDUCED_COLORS_MIN;
    2276              :         }
    2277              : 
    2278            2 :         if (target > COLOPRESSO_PNGX_REDUCED_COLORS_MAX) {
    2279            0 :             target = COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
    2280              :         }
    2281              :     } else {
    2282            6 :         target = (uint32_t)unique_colors;
    2283              : 
    2284            6 :         if (unique_colors > PNGX_REDUCED_TARGET_UNIQUE_COLOR_THRESHOLD) {
    2285            3 :             base = sqrt((double)unique_colors) * PNGX_REDUCED_TARGET_UNIQUE_BASE_SCALE;
    2286              : 
    2287            3 :             if (density < PNGX_REDUCED_TARGET_DENSITY_LOW_THRESHOLD) {
    2288            3 :                 base *= PNGX_REDUCED_TARGET_DENSITY_LOW_SCALE;
    2289            0 :             } else if (density > PNGX_REDUCED_TARGET_DENSITY_HIGH_THRESHOLD) {
    2290            0 :                 base *= PNGX_REDUCED_TARGET_DENSITY_HIGH_SCALE;
    2291              :             }
    2292              : 
    2293            3 :             if (base < PNGX_REDUCED_TARGET_BASE_MIN) {
    2294            1 :                 base = PNGX_REDUCED_TARGET_BASE_MIN;
    2295              :             }
    2296              : 
    2297            3 :             if (base > (double)COLOPRESSO_PNGX_REDUCED_COLORS_MAX) {
    2298            0 :                 base = (double)COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
    2299              :             }
    2300              : 
    2301            3 :             target = (uint32_t)(base + PNGX_REDUCED_ROUNDING_OFFSET);
    2302              :         }
    2303              :     }
    2304              : 
    2305            8 :     if (pixel_count > 0 && hist) {
    2306            8 :         low_weight_threshold = (uint32_t)(pixel_count / PNGX_REDUCED_LOW_WEIGHT_DIVISOR);
    2307            8 :         if (low_weight_threshold < PNGX_REDUCED_LOW_WEIGHT_MIN) {
    2308            3 :             low_weight_threshold = PNGX_REDUCED_LOW_WEIGHT_MIN;
    2309            5 :         } else if (low_weight_threshold > PNGX_REDUCED_LOW_WEIGHT_MAX) {
    2310            0 :             low_weight_threshold = PNGX_REDUCED_LOW_WEIGHT_MAX;
    2311              :         }
    2312              : 
    2313            8 :         low_weight_ratio = histogram_low_weight_ratio(hist, low_weight_threshold);
    2314            8 :         detail_pressure = histogram_detail_pressure(hist, bits_rgb, bits_alpha);
    2315            8 :         head_dominance = histogram_head_dominance(hist, PNGX_REDUCED_TARGET_HEAD_DOMINANCE_BUCKETS);
    2316              : 
    2317            8 :         if (low_weight_ratio > PNGX_REDUCED_TARGET_LOW_WEIGHT_REDUCTION_START) {
    2318            4 :             reduction_scale =
    2319              :                 1.0f -
    2320            4 :                 clamp_float((low_weight_ratio - PNGX_REDUCED_TARGET_LOW_WEIGHT_REDUCTION_START) *
    2321            4 :                                 (PNGX_REDUCED_TARGET_LOW_WEIGHT_REDUCTION_BASE +
    2322            4 :                                  (PNGX_REDUCED_TARGET_LOW_WEIGHT_REDUCTION_DETAIL *
    2323            4 :                                   (1.0f - detail_pressure))),
    2324              :                             0.0f, PNGX_REDUCED_TARGET_LOW_WEIGHT_REDUCTION_CLAMP);
    2325            4 :             target = (uint32_t)((float)target * reduction_scale + PNGX_REDUCED_ROUNDING_OFFSET);
    2326              :         }
    2327              : 
    2328            8 :         if (low_weight_ratio > PNGX_REDUCED_TARGET_TAIL_RATIO_THRESHOLD &&
    2329              :             detail_pressure < PNGX_REDUCED_TARGET_DETAIL_PRESSURE_TAIL_LIMIT) {
    2330            3 :             tail_cut = clamp_float((low_weight_ratio - PNGX_REDUCED_TARGET_TAIL_RATIO_THRESHOLD) *
    2331            3 :                                        (PNGX_REDUCED_TARGET_TAIL_WIDTH_BASE - detail_pressure) *
    2332              :                                        PNGX_REDUCED_TARGET_TAIL_WIDTH_SCALE,
    2333              :                                    0.0f, PNGX_REDUCED_TARGET_TAIL_CUT_CLAMP);
    2334            3 :             target = (uint32_t)((float)target * (1.0f - tail_cut) + PNGX_REDUCED_ROUNDING_OFFSET);
    2335              :         }
    2336              : 
    2337            8 :         if (detail_pressure > PNGX_REDUCED_TARGET_DETAIL_PRESSURE_BOOST) {
    2338            4 :             boost_scale =
    2339            4 :                 1.0f + clamp_float((detail_pressure - PNGX_REDUCED_TARGET_DETAIL_PRESSURE_BOOST) *
    2340              :                                        PNGX_REDUCED_TARGET_DETAIL_BOOST_SCALE,
    2341              :                                    0.0f, PNGX_REDUCED_TARGET_DETAIL_BOOST_CLAMP);
    2342            4 :             target = (uint32_t)((float)target * boost_scale + PNGX_REDUCED_ROUNDING_OFFSET);
    2343              :         }
    2344              : 
    2345            8 :         if (head_dominance > PNGX_REDUCED_TARGET_HEAD_DOMINANCE_THRESHOLD &&
    2346              :             detail_pressure < PNGX_REDUCED_TARGET_DETAIL_PRESSURE_HEAD_LIMIT) {
    2347            0 :             gradient_relief =
    2348              :                 stats
    2349            2 :                     ? clamp_float((PNGX_REDUCED_TARGET_GRADIENT_RELIEF_REF - stats->gradient_mean) /
    2350              :                                       PNGX_REDUCED_TARGET_GRADIENT_RELIEF_REF,
    2351              :                                   0.0f, 1.0f)
    2352            2 :                     : PNGX_REDUCED_TARGET_GRADIENT_RELIEF_DEFAULT;
    2353              :             dominance_cut =
    2354            2 :                 clamp_float((head_dominance - PNGX_REDUCED_TARGET_HEAD_DOMINANCE_THRESHOLD) *
    2355            2 :                                 (PNGX_REDUCED_TARGET_HEAD_CUT_BASE +
    2356            2 :                                  PNGX_REDUCED_TARGET_HEAD_CUT_RELIEF * gradient_relief),
    2357              :                             0.0f, PNGX_REDUCED_TARGET_HEAD_CUT_CLAMP);
    2358            2 :             target =
    2359            2 :                 (uint32_t)((float)target * (1.0f - dominance_cut) + PNGX_REDUCED_ROUNDING_OFFSET);
    2360              :         }
    2361              : 
    2362            8 :         if (head_dominance > PNGX_REDUCED_TARGET_HEAD_DOMINANCE_STRONG &&
    2363            2 :             low_weight_ratio > PNGX_REDUCED_TARGET_LOW_WEIGHT_RATIO_STRONG &&
    2364              :             detail_pressure < PNGX_REDUCED_TARGET_DETAIL_PRESSURE_STRONG_LIMIT) {
    2365            0 :             gradient_relief =
    2366            2 :                 stats ? clamp_float((PNGX_REDUCED_TARGET_GRADIENT_RELIEF_SECONDARY_REF -
    2367            2 :                                      stats->gradient_mean) /
    2368              :                                         PNGX_REDUCED_TARGET_GRADIENT_RELIEF_SECONDARY_REF,
    2369              :                                     0.0f, 1.0f)
    2370            2 :                       : PNGX_REDUCED_TARGET_GRADIENT_RELIEF_SECONDARY_DEFAULT;
    2371            0 :             softness_relief = stats ? clamp_float((PNGX_REDUCED_TARGET_SATURATION_RELIEF_REF -
    2372            2 :                                                    stats->saturation_mean) /
    2373              :                                                       PNGX_REDUCED_TARGET_SATURATION_RELIEF_REF,
    2374              :                                                   0.0f, 1.0f)
    2375            2 :                                     : PNGX_REDUCED_TARGET_SATURATION_RELIEF_DEFAULT;
    2376            2 :             relief = clamp_float((gradient_relief * PNGX_REDUCED_TARGET_RELIEF_GRADIENT_WEIGHT +
    2377            2 :                                   softness_relief * PNGX_REDUCED_TARGET_RELIEF_SATURATION_WEIGHT) *
    2378              :                                      PNGX_REDUCED_TARGET_RELIEF_SCALE,
    2379              :                                  0.0f, PNGX_REDUCED_TARGET_RELIEF_CLAMP);
    2380              :             dominance_gain =
    2381            2 :                 clamp_float((head_dominance - PNGX_REDUCED_TARGET_HEAD_DOMINANCE_STRONG) *
    2382              :                                 PNGX_REDUCED_TARGET_DOMINANCE_GAIN_SCALE,
    2383              :                             0.0f, PNGX_REDUCED_TARGET_DOMINANCE_GAIN_CLAMP);
    2384            2 :             tail_gain = clamp_float(
    2385            2 :                 (low_weight_ratio - PNGX_REDUCED_TARGET_LOW_WEIGHT_RATIO_STRONG) *
    2386            2 :                     (PNGX_REDUCED_TARGET_TAIL_GAIN_BASE +
    2387            2 :                      PNGX_REDUCED_TARGET_TAIL_GAIN_RELIEF * (gradient_relief + softness_relief)),
    2388              :                 0.0f, PNGX_REDUCED_TARGET_TAIL_GAIN_CLAMP);
    2389            2 :             detail_relief = clamp_float((PNGX_REDUCED_TARGET_DETAIL_RELIEF_BASE - detail_pressure) *
    2390              :                                             PNGX_REDUCED_TARGET_DETAIL_RELIEF_SCALE,
    2391              :                                         0.0f, PNGX_REDUCED_TARGET_DETAIL_RELIEF_CLAMP);
    2392              :             combined_cut =
    2393            2 :                 clamp_float((dominance_gain + tail_gain) *
    2394            2 :                                 (PNGX_REDUCED_TARGET_COMBINED_CUT_BASE + relief + detail_relief),
    2395              :                             0.0f, PNGX_REDUCED_TARGET_COMBINED_CUT_CLAMP);
    2396            2 :             target =
    2397            2 :                 (uint32_t)((float)target * (1.0f - combined_cut) + PNGX_REDUCED_ROUNDING_OFFSET);
    2398              :         }
    2399              : 
    2400            8 :         if (density < PNGX_REDUCED_TARGET_DENSITY_THRESHOLD &&
    2401              :             detail_pressure < PNGX_REDUCED_TARGET_DETAIL_PRESSURE_DENSITY_LIMIT) {
    2402            1 :             density_gap = clamp_float((PNGX_REDUCED_TARGET_DENSITY_THRESHOLD - (float)density) *
    2403              :                                           PNGX_REDUCED_TARGET_DENSITY_GAP_SCALE,
    2404              :                                       0.0f, PNGX_REDUCED_TARGET_DENSITY_GAP_CLAMP);
    2405            1 :             target =
    2406            1 :                 (uint32_t)((float)target * (1.0f - density_gap) + PNGX_REDUCED_ROUNDING_OFFSET);
    2407              :         }
    2408              :     }
    2409              : 
    2410            8 :     if (stats) {
    2411            8 :         flatness_score = stats_flatness_factor(stats);
    2412            8 :         if (flatness_score > PNGX_REDUCED_TARGET_FLATNESS_THRESHOLD &&
    2413              :             detail_pressure < PNGX_REDUCED_TARGET_DETAIL_PRESSURE_FLAT_LIMIT) {
    2414            4 :             flatness_reduction = clamp_float(flatness_score * PNGX_REDUCED_TARGET_FLATNESS_SCALE,
    2415              :                                              0.0f, PNGX_REDUCED_TARGET_FLATNESS_CLAMP);
    2416            4 :             target = (uint32_t)((float)target * (1.0f - flatness_reduction) +
    2417              :                                 PNGX_REDUCED_ROUNDING_OFFSET);
    2418              :         }
    2419              : 
    2420            8 :         alpha_simple = stats_alpha_simplicity(stats);
    2421            8 :         if (alpha_simple > PNGX_REDUCED_TARGET_ALPHA_SIMPLE_THRESHOLD &&
    2422              :             detail_pressure < PNGX_REDUCED_TARGET_DETAIL_PRESSURE_ALPHA_LIMIT) {
    2423            3 :             alpha_reduction = clamp_float(alpha_simple * PNGX_REDUCED_TARGET_ALPHA_SIMPLE_SCALE,
    2424              :                                           0.0f, PNGX_REDUCED_TARGET_ALPHA_SIMPLE_CLAMP);
    2425            3 :             target =
    2426            3 :                 (uint32_t)((float)target * (1.0f - alpha_reduction) + PNGX_REDUCED_ROUNDING_OFFSET);
    2427              :         }
    2428              :     }
    2429              : 
    2430            8 :     if (hint <= 0 && unique_colors > PNGX_REDUCED_TARGET_GENTLE_MIN_COLORS &&
    2431            1 :         unique_colors <= PNGX_REDUCED_TARGET_GENTLE_MAX_COLORS &&
    2432              :         detail_pressure < PNGX_REDUCED_TARGET_DETAIL_PRESSURE_GENTLE_LIMIT) {
    2433            1 :         span_ratio = ((float)unique_colors - (float)PNGX_REDUCED_TARGET_GENTLE_MIN_COLORS) /
    2434              :                      PNGX_REDUCED_TARGET_GENTLE_COLOR_RANGE;
    2435            1 :         if (span_ratio < 0.0f) {
    2436            0 :             span_ratio = 0.0f;
    2437            1 :         } else if (span_ratio > 1.0f) {
    2438            0 :             span_ratio = 1.0f;
    2439              :         }
    2440              : 
    2441            1 :         gentle_cut = clamp_float((1.0f - detail_pressure) * PNGX_REDUCED_TARGET_GENTLE_SCALE, 0.0f,
    2442              :                                  PNGX_REDUCED_TARGET_GENTLE_CLAMP);
    2443            1 :         applied_cut = span_ratio * gentle_cut;
    2444            1 :         target = (uint32_t)((float)target * (1.0f - applied_cut) + PNGX_REDUCED_ROUNDING_OFFSET);
    2445              :     }
    2446              : 
    2447            8 :     if (target > unique_colors) {
    2448            0 :         target = (uint32_t)unique_colors;
    2449              :     }
    2450              : 
    2451            8 :     if (target < (uint32_t)COLOPRESSO_PNGX_REDUCED_COLORS_MIN) {
    2452            1 :         target = COLOPRESSO_PNGX_REDUCED_COLORS_MIN;
    2453              :     }
    2454              : 
    2455            8 :     if (target > grid_cap) {
    2456            0 :         target = grid_cap;
    2457              :     }
    2458              : 
    2459            8 :     if (unlocked > 0 && target > (uint32_t)unlocked) {
    2460            0 :         target = (uint32_t)unlocked;
    2461              :     }
    2462              : 
    2463            8 :     if (unlocked == 0) {
    2464            1 :         target = (uint32_t)unique_colors;
    2465              :     }
    2466              : 
    2467            8 :     return target;
    2468              : }
    2469              : 
    2470            9 : extern bool pngx_quantize_reduced_rgba32(const uint8_t *png_data, size_t png_size,
    2471              :                                          const pngx_options_t *opts, uint32_t *resolved_target,
    2472              :                                          uint32_t *applied_colors, uint8_t **out_data,
    2473              :                                          size_t *out_size)
    2474              : {
    2475              :     pngx_rgba_image_t image;
    2476            9 :     pngx_quant_support_t support = {0};
    2477              :     pngx_image_stats_t stats;
    2478              :     pngx_options_t tuned_opts;
    2479              :     color_histogram_t histogram;
    2480            9 :     uint32_t target = 0, actual = 0, manual_limit = 0, auto_trim_limit = 0, grid_cap;
    2481              :     uint8_t bits_rgb, bits_alpha;
    2482              :     size_t grid_unique, passthrough_threshold;
    2483            9 :     bool wrote, manual_target = false, success, auto_target, grid_passthrough,
    2484            9 :                 auto_trim_applied = false;
    2485              : 
    2486            9 :     if (!png_data || png_size == 0 || !opts || !out_data || !out_size) {
    2487            0 :         return false;
    2488              :     }
    2489              : 
    2490            9 :     if (!load_rgba_image(png_data, png_size, &image)) {
    2491            0 :         return false;
    2492              :     }
    2493              : 
    2494            9 :     histogram.entries = NULL;
    2495            9 :     histogram.count = 0;
    2496            9 :     histogram.unlocked_count = 0;
    2497            9 :     tuned_opts = *opts;
    2498              : 
    2499            9 :     image_stats_reset(&stats);
    2500              : 
    2501            9 :     if (!prepare_quant_support(&image, &tuned_opts, &support, &stats)) {
    2502            0 :         rgba_image_reset(&image);
    2503              : 
    2504            0 :         return false;
    2505              :     }
    2506              : 
    2507            9 :     tune_reduced_bitdepth(&image, &stats, &tuned_opts.lossy_reduced_bits_rgb,
    2508              :                           &tuned_opts.lossy_reduced_alpha_bits);
    2509            9 :     if (!apply_reduced_rgba32_prepass(&image, &tuned_opts, &support, &stats)) {
    2510            0 :         color_histogram_reset(&histogram);
    2511            0 :         quant_support_reset(&support);
    2512            0 :         rgba_image_reset(&image);
    2513              : 
    2514            0 :         return false;
    2515              :     }
    2516              : 
    2517            9 :     bits_rgb = clamp_reduced_bits(tuned_opts.lossy_reduced_bits_rgb);
    2518            9 :     bits_alpha = clamp_reduced_bits(tuned_opts.lossy_reduced_alpha_bits);
    2519            9 :     grid_cap = compute_grid_capacity(bits_rgb, bits_alpha);
    2520            9 :     if (grid_cap == 0) {
    2521            0 :         grid_cap = COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
    2522              :     }
    2523              : 
    2524            9 :     if (opts->lossy_reduced_colors >= (int32_t)COLOPRESSO_PNGX_REDUCED_COLORS_MIN) {
    2525            2 :         manual_target = true;
    2526            2 :         manual_limit = (uint32_t)opts->lossy_reduced_colors;
    2527            2 :         if (manual_limit > grid_cap) {
    2528            0 :             manual_limit = grid_cap;
    2529              :         }
    2530              :     }
    2531              : 
    2532            9 :     grid_unique = count_unique_rgba(image.rgba, image.pixel_count);
    2533            9 :     grid_cap = compute_grid_capacity(bits_rgb, bits_alpha);
    2534            9 :     auto_target = (opts->lossy_reduced_colors <= 0);
    2535            9 :     grid_passthrough = false;
    2536              : 
    2537            9 :     if (grid_cap == 0) {
    2538            0 :         grid_cap = COLOPRESSO_PNGX_REDUCED_COLORS_MAX;
    2539              :     }
    2540              : 
    2541            9 :     passthrough_threshold = resolve_reduced_passthrough_threshold(grid_cap, &stats);
    2542              : 
    2543            9 :     if (grid_unique > (size_t)grid_cap) {
    2544            0 :         grid_unique = (size_t)grid_cap;
    2545              :     }
    2546              : 
    2547            9 :     if (auto_target && grid_unique >= passthrough_threshold) {
    2548            1 :         grid_passthrough = true;
    2549              :     }
    2550              : 
    2551            9 :     if (grid_passthrough) {
    2552            1 :         wrote = false;
    2553            1 :         snap_rgba_image_to_bits(opts->thread_count, image.rgba, image.pixel_count, bits_rgb,
    2554              :                                 bits_alpha);
    2555              : 
    2556            1 :         if (resolved_target) {
    2557            1 :             *resolved_target = (uint32_t)grid_unique;
    2558              :         }
    2559              : 
    2560            1 :         if (applied_colors) {
    2561            1 :             *applied_colors = (uint32_t)grid_unique;
    2562              :         }
    2563              : 
    2564            1 :         wrote = create_rgba_png(image.rgba, image.pixel_count, image.width, image.height, out_data,
    2565              :                                 out_size);
    2566            1 :         if (wrote) {
    2567            1 :             colopresso_log(CPRES_LOG_LEVEL_DEBUG,
    2568              :                            "PNGX: Reduced RGBA32 grid passthrough kept %zu colors (capacity=%u)",
    2569              :                            grid_unique, grid_cap);
    2570              :         }
    2571              : 
    2572            1 :         quant_support_reset(&support);
    2573            1 :         rgba_image_reset(&image);
    2574              : 
    2575            1 :         return wrote;
    2576              :     }
    2577              : 
    2578            8 :     if (!build_color_histogram(&image, &tuned_opts, &support, &histogram)) {
    2579            0 :         quant_support_reset(&support);
    2580            0 :         rgba_image_reset(&image);
    2581              : 
    2582            0 :         return false;
    2583              :     }
    2584              : 
    2585            8 :     target = resolve_reduced_rgba32_target(
    2586            8 :         &histogram, image.pixel_count, opts->lossy_reduced_colors, bits_rgb, bits_alpha, &stats);
    2587            8 :     if (histogram.unlocked_count == 0 || target == 0) {
    2588            1 :         actual = (uint32_t)histogram.count;
    2589            1 :         snap_rgba_image_to_bits(opts->thread_count, image.rgba, image.pixel_count, bits_rgb,
    2590              :                                 bits_alpha);
    2591              :     } else {
    2592            7 :         if (!apply_reduced_rgba32_quantization(opts->thread_count, &histogram, &image, target,
    2593              :                                                bits_rgb, bits_alpha, &actual)) {
    2594            0 :             color_histogram_reset(&histogram);
    2595            0 :             quant_support_reset(&support);
    2596            0 :             rgba_image_reset(&image);
    2597              : 
    2598            0 :             return false;
    2599              :         }
    2600              :     }
    2601              : 
    2602            8 :     if (!manual_target) {
    2603            6 :         auto_trim_limit = compute_auto_trim_limit(&histogram, image.pixel_count, actual, bits_rgb,
    2604              :                                                   bits_alpha, &stats);
    2605            6 :         if (auto_trim_limit > 0 && auto_trim_limit < actual) {
    2606            4 :             if (enforce_manual_reduced_limit(opts->thread_count, &image, auto_trim_limit, bits_rgb,
    2607              :                                              bits_alpha, &actual)) {
    2608            2 :                 auto_trim_applied = true;
    2609            2 :                 colopresso_log(CPRES_LOG_LEVEL_DEBUG,
    2610              :                                "PNGX: Reduced RGBA32 auto trim applied %u -> %u colors", target,
    2611              :                                auto_trim_limit);
    2612              :             } else {
    2613            0 :                 colopresso_log(CPRES_LOG_LEVEL_WARNING,
    2614              :                                "PNGX: Reduced RGBA32 auto trim request failed (limit=%u)",
    2615              :                                auto_trim_limit);
    2616            0 :                 auto_trim_limit = 0;
    2617              :             }
    2618              :         } else {
    2619            4 :             auto_trim_limit = 0;
    2620              :         }
    2621              :     }
    2622              : 
    2623            8 :     if (manual_target) {
    2624            2 :         if (!enforce_manual_reduced_limit(opts->thread_count, &image, manual_limit, bits_rgb,
    2625              :                                           bits_alpha, &actual)) {
    2626            0 :             color_histogram_reset(&histogram);
    2627            0 :             quant_support_reset(&support);
    2628            0 :             rgba_image_reset(&image);
    2629              : 
    2630            0 :             return false;
    2631              :         }
    2632              :     }
    2633              : 
    2634            8 :     success = create_rgba_png(image.rgba, image.pixel_count, image.width, image.height, out_data,
    2635              :                               out_size);
    2636              : 
    2637            8 :     if (resolved_target) {
    2638            8 :         if (manual_target) {
    2639            2 :             *resolved_target = manual_limit;
    2640            6 :         } else if (auto_trim_applied && auto_trim_limit > 0) {
    2641            2 :             *resolved_target = auto_trim_limit;
    2642            4 :         } else if (histogram.unlocked_count == 0 || target == 0) {
    2643            1 :             *resolved_target = (uint32_t)histogram.count;
    2644              :         } else {
    2645            3 :             *resolved_target = target;
    2646              :         }
    2647              :     }
    2648              : 
    2649            8 :     if (applied_colors) {
    2650            8 :         *applied_colors = actual;
    2651              :     }
    2652              : 
    2653            8 :     color_histogram_reset(&histogram);
    2654            8 :     quant_support_reset(&support);
    2655            8 :     rgba_image_reset(&image);
    2656              : 
    2657            8 :     return success;
    2658              : }
        

Generated by: LCOV version 2.0-1