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

Generated by: LCOV version 2.0-1