LCOV - code coverage report
Current view: top level - src - pngx_common.c (source / functions) Coverage Total Hit
Test: colopresso Coverage Report Lines: 82.9 % 492 408
Test Date: 2025-12-13 15:41:21 Functions: 100.0 % 21 21
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 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 <errno.h>
      15              : #include <limits.h>
      16              : #include <math.h>
      17              : #include <stdbool.h>
      18              : #include <stdint.h>
      19              : #include <stdlib.h>
      20              : #include <string.h>
      21              : 
      22              : #include <png.h>
      23              : #include <zlib.h>
      24              : 
      25              : #include "internal/log.h"
      26              : #include "internal/png.h"
      27              : #include "internal/pngx_common.h"
      28              : 
      29              : #define PNGX_LUMA_R 0.2126f
      30              : #define PNGX_LUMA_G 0.7152f
      31              : #define PNGX_LUMA_B 0.0722f
      32              : 
      33              : #define PNGX_ANCHOR_SCALE_DIVISOR 8192
      34              : #define PNGX_ANCHOR_SCALE_MIN 12
      35              : #define PNGX_ANCHOR_AUTO_LIMIT_DEFAULT 16
      36              : #define PNGX_ANCHOR_IMPORTANCE_FACTOR 0.45f
      37              : #define PNGX_ANCHOR_IMPORTANCE_THRESHOLD 0.4f
      38              : #define PNGX_ANCHOR_IMPORTANCE_BOOST_BASE 0.4f
      39              : #define PNGX_ANCHOR_IMPORTANCE_BOOST_SCALE 0.5f
      40              : #define PNGX_ANCHOR_SCORE_THRESHOLD 0.35f
      41              : #define PNGX_ANCHOR_LOW_COUNT_PENALTY 0.5f
      42              : #define PNGX_ANCHOR_LOW_COUNT_THRESHOLD 4
      43              : #define PNGX_ANCHOR_DISTANCE_SQ_THRESHOLD 625
      44              : 
      45              : #define PNGX_DITHER_OPAQUE_THRESHOLD 248
      46              : #define PNGX_DITHER_TRANSLUCENT_THRESHOLD 32
      47              : #define PNGX_DITHER_GRADIENT_MIN 0.02f
      48              : #define PNGX_DITHER_BASE_LEVEL 0.62f
      49              : #define PNGX_DITHER_HIGH_GRADIENT_BOOST 0.12f
      50              : #define PNGX_DITHER_MID_GRADIENT_BOOST 0.05f
      51              : #define PNGX_DITHER_LOW_GRADIENT_CUT 0.12f
      52              : #define PNGX_DITHER_MID_LOW_GRADIENT_CUT 0.05f
      53              : #define PNGX_DITHER_OPAQUE_LOW_CUT 0.08f
      54              : #define PNGX_DITHER_OPAQUE_HIGH_BOOST 0.05f
      55              : #define PNGX_DITHER_TRANSLUCENT_CUT 0.05f
      56              : #define PNGX_DITHER_COVERAGE_THRESHOLD 0.35f
      57              : #define PNGX_DITHER_SPAN_THRESHOLD 2.0f
      58              : #define PNGX_DITHER_TARGET_CAP 0.9f
      59              : #define PNGX_DITHER_TARGET_CAP_LOW_BIT 0.96f
      60              : #define PNGX_DITHER_LOW_BIT_BOOST 0.05f
      61              : #define PNGX_DITHER_LOW_BIT_GRADIENT_BOOST 0.05f
      62              : #define PNGX_DITHER_MIN 0.2f
      63              : #define PNGX_DITHER_MAX 0.95f
      64              : 
      65              : #define PNGX_FIXED_PALETTE_DISTANCE_SQ 400
      66              : #define PNGX_FIXED_PALETTE_MAX 256
      67              : 
      68              : #define PNGX_RESOLVE_DEFAULT_GRADIENT 0.2f
      69              : #define PNGX_RESOLVE_DEFAULT_SATURATION 0.2f
      70              : #define PNGX_RESOLVE_DEFAULT_OPAQUE 1.0f
      71              : #define PNGX_RESOLVE_DEFAULT_VIBRANT 0.05f
      72              : #define PNGX_RESOLVE_AUTO_BASE 0.35f
      73              : #define PNGX_RESOLVE_AUTO_GRADIENT_WEIGHT 0.35f
      74              : #define PNGX_RESOLVE_AUTO_SATURATION_WEIGHT 0.15f
      75              : #define PNGX_RESOLVE_AUTO_OPAQUE_CUT 0.06f
      76              : #define PNGX_RESOLVE_ADAPTIVE_FLAT_CUT 0.12f
      77              : #define PNGX_RESOLVE_ADAPTIVE_GRADIENT_BOOST 0.06f
      78              : #define PNGX_RESOLVE_ADAPTIVE_VIBRANT_CUT 0.05f
      79              : #define PNGX_RESOLVE_ADAPTIVE_SATURATION_BOOST 0.03f
      80              : #define PNGX_RESOLVE_ADAPTIVE_SATURATION_CUT 0.02f
      81              : #define PNGX_RESOLVE_MIN 0.02f
      82              : #define PNGX_RESOLVE_MAX 0.90f
      83              : 
      84              : #define PNGX_PREPARE_GRADIENT_SCALE 0.5f
      85              : #define PNGX_PREPARE_VIBRANT_SATURATION 0.55f
      86              : #define PNGX_PREPARE_VIBRANT_GRADIENT 0.05f
      87              : #define PNGX_PREPARE_VIBRANT_ALPHA 127
      88              : #define PNGX_PREPARE_CHROMA_WEIGHT 0.35f
      89              : #define PNGX_PREPARE_BOOST_THRESHOLD 0.25f
      90              : #define PNGX_PREPARE_BOOST_BASE 0.08f
      91              : #define PNGX_PREPARE_BOOST_FACTOR 0.3f
      92              : #define PNGX_PREPARE_CUT_THRESHOLD 0.08f
      93              : #define PNGX_PREPARE_CUT_FACTOR 0.65f
      94              : #define PNGX_PREPARE_ALPHA_THRESHOLD 0.85f
      95              : #define PNGX_PREPARE_ALPHA_BASE 0.4f
      96              : #define PNGX_PREPARE_ALPHA_MULTIPLIER 0.6f
      97              : #define PNGX_PREPARE_BUCKET_SATURATION 0.35f
      98              : #define PNGX_PREPARE_BUCKET_IMPORTANCE 0.55f
      99              : #define PNGX_PREPARE_BUCKET_ALPHA 170
     100              : #define PNGX_PREPARE_MIX_IMPORTANCE 0.6f
     101              : #define PNGX_PREPARE_MIX_GRADIENT 0.3f
     102              : #define PNGX_PREPARE_ANCHOR_SATURATION 0.45f
     103              : #define PNGX_PREPARE_ANCHOR_MIX 0.55f
     104              : #define PNGX_PREPARE_ANCHOR_IMP_THRESHOLD 0.75f
     105              : #define PNGX_PREPARE_ANCHOR_IMP_BONUS 0.05f
     106              : #define PNGX_PREPARE_ANCHOR_SCORE_THRESHOLD 0.35f
     107              : #define PNGX_PREPARE_MAP_MIN_VALUE 4
     108              : 
     109              : #define PNGX_FS_COEFF_7 (7.0f / 16.0f)
     110              : #define PNGX_FS_COEFF_3 (3.0f / 16.0f)
     111              : #define PNGX_FS_COEFF_5 (5.0f / 16.0f)
     112              : #define PNGX_FS_COEFF_1 (1.0f / 16.0f)
     113              : 
     114              : typedef struct {
     115              :   uint64_t r_sum;
     116              :   uint64_t g_sum;
     117              :   uint64_t b_sum;
     118              :   uint64_t a_sum;
     119              :   uint32_t count;
     120              :   float score;
     121              :   float importance_accum;
     122              : } pngx_chroma_bucket_t;
     123              : 
     124     26301176 : static inline float absf(float value) { return (value < 0.0f) ? -value : value; }
     125              : 
     126      1965600 : static inline uint32_t chroma_bucket_index(uint8_t r, uint8_t g, uint8_t b) {
     127      3931200 :   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) +
     128      1965600 :          ((uint32_t)b >> PNGX_CHROMA_BUCKET_SHIFT);
     129              : }
     130              : 
     131     13167242 : static inline float calc_saturation(uint8_t r, uint8_t g, uint8_t b) {
     132     13167242 :   uint8_t max_v = r, min_v = r;
     133              : 
     134     13167242 :   if (g > max_v) {
     135      6736834 :     max_v = g;
     136              :   }
     137     13167242 :   if (b > max_v) {
     138      3283905 :     max_v = b;
     139              :   }
     140     13167242 :   if (g < min_v) {
     141      6217084 :     min_v = g;
     142              :   }
     143     13167242 :   if (b < min_v) {
     144      4943059 :     min_v = b;
     145              :   }
     146              : 
     147     13167242 :   if (max_v == 0) {
     148        36961 :     return 0.0f;
     149              :   }
     150              : 
     151     13130281 :   return (float)(max_v - min_v) / (float)max_v;
     152              : }
     153              : 
     154     13167250 : static inline float calc_luma(uint8_t r, uint8_t g, uint8_t b) { return PNGX_LUMA_R * (float)r + PNGX_LUMA_G * (float)g + PNGX_LUMA_B * (float)b; }
     155              : 
     156           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) {
     157              :   cpres_error_t status;
     158              : 
     159           36 :   if (!png_data || png_size == 0 || !rgba || !width || !height) {
     160            0 :     return false;
     161              :   }
     162              : 
     163           36 :   status = cpres_png_decode_from_memory(png_data, png_size, rgba, width, height);
     164           36 :   if (status != CPRES_OK) {
     165            0 :     cpres_log(CPRES_LOG_LEVEL_WARNING, "PNGX: Failed to decode PNG (%d)", (int)status);
     166            0 :     return false;
     167              :   }
     168              : 
     169           36 :   return true;
     170              : }
     171              : 
     172           28 : static inline void extract_chroma_anchors(pngx_quant_support_t *support, pngx_chroma_bucket_t *buckets, size_t bucket_count, size_t pixel_count) {
     173              :   cpres_rgba_color_t chosen[PNGX_MAX_DERIVED_COLORS];
     174              :   pngx_chroma_bucket_t *bucket;
     175              :   size_t selected, best_index, i, dst, auto_limit, scaled, check;
     176              :   float best_score, score, importance_boost;
     177              :   bool duplicate;
     178              : 
     179           28 :   if (!support || !buckets || bucket_count == 0) {
     180            4 :     return;
     181              :   }
     182              : 
     183           28 :   if (pixel_count > 0) {
     184           28 :     scaled = pixel_count / PNGX_ANCHOR_SCALE_DIVISOR;
     185           28 :     if (scaled < PNGX_ANCHOR_SCALE_MIN) {
     186            7 :       scaled = PNGX_ANCHOR_SCALE_MIN;
     187              :     }
     188           28 :     if (scaled > PNGX_MAX_DERIVED_COLORS) {
     189           18 :       scaled = PNGX_MAX_DERIVED_COLORS;
     190              :     }
     191           28 :     auto_limit = scaled;
     192              :   } else {
     193            0 :     auto_limit = PNGX_ANCHOR_AUTO_LIMIT_DEFAULT;
     194              :   }
     195              : 
     196           28 :   selected = 0;
     197         1002 :   while (selected < auto_limit) {
     198          980 :     best_score = 0.0f;
     199          980 :     best_index = SIZE_MAX;
     200      4015060 :     for (i = 0; i < bucket_count; ++i) {
     201      4014080 :       bucket = &buckets[i];
     202      4014080 :       if (!bucket->count || bucket->score <= 0.0f) {
     203      3215205 :         continue;
     204              :       }
     205       798875 :       importance_boost = bucket->importance_accum * PNGX_ANCHOR_IMPORTANCE_FACTOR;
     206       798875 :       if (importance_boost > PNGX_ANCHOR_IMPORTANCE_THRESHOLD) {
     207       677899 :         importance_boost = PNGX_ANCHOR_IMPORTANCE_BOOST_BASE + (importance_boost - PNGX_ANCHOR_IMPORTANCE_THRESHOLD) * PNGX_ANCHOR_IMPORTANCE_BOOST_SCALE;
     208              :       }
     209       798875 :       score = bucket->score + importance_boost;
     210       798875 :       if (bucket->count < PNGX_ANCHOR_LOW_COUNT_THRESHOLD) {
     211       171664 :         score *= PNGX_ANCHOR_LOW_COUNT_PENALTY;
     212              :       }
     213       798875 :       if (score > best_score) {
     214        15517 :         best_score = score;
     215        15517 :         best_index = i;
     216              :       }
     217              :     }
     218          980 :     if (best_index == SIZE_MAX || best_score < PNGX_ANCHOR_SCORE_THRESHOLD) {
     219              :       break;
     220              :     }
     221              : 
     222          974 :     chosen[selected].r = (uint8_t)(buckets[best_index].r_sum / buckets[best_index].count);
     223          974 :     chosen[selected].g = (uint8_t)(buckets[best_index].g_sum / buckets[best_index].count);
     224          974 :     chosen[selected].b = (uint8_t)(buckets[best_index].b_sum / buckets[best_index].count);
     225          974 :     chosen[selected].a = (uint8_t)(buckets[best_index].a_sum / buckets[best_index].count);
     226          974 :     buckets[best_index].score = 0.0f;
     227          974 :     ++selected;
     228              :   }
     229              : 
     230           28 :   if (!selected) {
     231            4 :     return;
     232              :   }
     233              : 
     234           24 :   dst = 0;
     235          998 :   for (i = 0; i < selected; ++i) {
     236          974 :     duplicate = false;
     237         7237 :     for (check = 0; check < dst; ++check) {
     238         6835 :       if (color_distance_sq(&chosen[i], &chosen[check]) < PNGX_ANCHOR_DISTANCE_SQ_THRESHOLD) {
     239          572 :         duplicate = true;
     240          572 :         break;
     241              :       }
     242              :     }
     243          974 :     if (duplicate) {
     244          572 :       continue;
     245              :     }
     246          402 :     chosen[dst] = chosen[i];
     247          402 :     ++dst;
     248              :   }
     249              : 
     250           24 :   if (!dst) {
     251            0 :     return;
     252              :   }
     253              : 
     254           24 :   support->derived_colors = (cpres_rgba_color_t *)malloc(sizeof(cpres_rgba_color_t) * dst);
     255           24 :   if (!support->derived_colors) {
     256            0 :     return;
     257              :   }
     258           24 :   memcpy(support->derived_colors, chosen, sizeof(cpres_rgba_color_t) * dst);
     259           24 :   support->derived_colors_len = dst;
     260              : }
     261              : 
     262              : /* Call from libpng on setjmp */
     263              : /* LCOV_EXCL_START */
     264              : void memory_buffer_reset(png_memory_buffer_t *buffer) {
     265              :   if (!buffer) {
     266              :     return;
     267              :   }
     268              : 
     269              :   free(buffer->data);
     270              :   buffer->data = NULL;
     271              :   buffer->size = 0;
     272              :   buffer->capacity = 0;
     273              : }
     274              : /* LCOV_EXCL_STOP */
     275              : 
     276           33 : void image_stats_reset(pngx_image_stats_t *stats) {
     277           33 :   if (!stats) {
     278            0 :     return;
     279              :   }
     280              : 
     281           33 :   stats->gradient_mean = 0.0f;
     282           33 :   stats->gradient_max = 0.0f;
     283           33 :   stats->saturation_mean = 0.0f;
     284           33 :   stats->opaque_ratio = 0.0f;
     285           33 :   stats->translucent_ratio = 0.0f;
     286           33 :   stats->vibrant_ratio = 0.0f;
     287              : }
     288              : 
     289           33 : void quant_support_reset(pngx_quant_support_t *support) {
     290           33 :   if (!support) {
     291            0 :     return;
     292              :   }
     293              : 
     294           33 :   free(support->importance_map);
     295           33 :   support->importance_map = NULL;
     296           33 :   support->importance_map_len = 0;
     297              : 
     298           33 :   free(support->derived_colors);
     299           33 :   support->derived_colors = NULL;
     300           33 :   support->derived_colors_len = 0;
     301              : 
     302           33 :   free(support->combined_fixed_colors);
     303           33 :   support->combined_fixed_colors = NULL;
     304           33 :   support->combined_fixed_len = 0;
     305              : 
     306           33 :   free(support->bit_hint_map);
     307           33 :   support->bit_hint_map = NULL;
     308           33 :   support->bit_hint_len = 0;
     309              : }
     310              : 
     311            8 : const char *lossy_type_label(uint8_t lossy_type) {
     312            8 :   switch (lossy_type) {
     313            3 :   case PNGX_LOSSY_TYPE_LIMITED_RGBA4444:
     314            3 :     return "Limited RGBA4444";
     315            5 :   case PNGX_LOSSY_TYPE_REDUCED_RGBA32:
     316            5 :     return "Reduced RGBA32";
     317            0 :   default:
     318            0 :     return "Palette256";
     319              :   }
     320              : }
     321              : 
     322           36 : void rgba_image_reset(pngx_rgba_image_t *image) {
     323           36 :   if (!image) {
     324            0 :     return;
     325              :   }
     326              : 
     327           36 :   free(image->rgba);
     328           36 :   image->rgba = NULL;
     329           36 :   image->width = 0;
     330           36 :   image->height = 0;
     331           36 :   image->pixel_count = 0;
     332              : }
     333              : 
     334           36 : bool load_rgba_image(const uint8_t *png_data, size_t png_size, pngx_rgba_image_t *image) {
     335           36 :   if (!png_data || png_size == 0 || !image) {
     336            0 :     return false;
     337              :   }
     338              : 
     339           36 :   image->rgba = NULL;
     340           36 :   image->width = 0;
     341           36 :   image->height = 0;
     342           36 :   image->pixel_count = 0;
     343              : 
     344           36 :   if (!decode_png_rgba(png_data, png_size, &image->rgba, &image->width, &image->height)) {
     345            0 :     rgba_image_reset(image);
     346            0 :     return false;
     347              :   }
     348              : 
     349           36 :   if (!image->rgba || image->width == 0 || image->height == 0) {
     350            0 :     rgba_image_reset(image);
     351            0 :     return false;
     352              :   }
     353              : 
     354           36 :   if ((size_t)image->width > SIZE_MAX / (size_t)image->height) {
     355            0 :     rgba_image_reset(image);
     356            0 :     return false;
     357              :   }
     358              : 
     359           36 :   image->pixel_count = (size_t)image->width * (size_t)image->height;
     360              : 
     361           36 :   return true;
     362              : }
     363              : 
     364      7411761 : uint8_t clamp_reduced_bits(uint8_t bits) {
     365      7411761 :   const uint8_t min_bits = (uint8_t)COLOPRESSO_PNGX_REDUCED_BITS_MIN, max_bits = (uint8_t)COLOPRESSO_PNGX_REDUCED_BITS_MAX;
     366              : 
     367      7411761 :   if (bits < min_bits) {
     368            0 :     return min_bits;
     369              :   }
     370      7411761 :   if (bits > max_bits) {
     371            0 :     return max_bits;
     372              :   }
     373      7411761 :   return bits;
     374              : }
     375              : 
     376     30516067 : uint8_t quantize_channel_value(float value, uint8_t bits_per_channel) {
     377              :   uint32_t levels;
     378              :   float clamped, scaled, rounded, quantized;
     379              : 
     380     30516067 :   if (bits_per_channel >= 8) {
     381            0 :     if (value < 0.0f) {
     382            0 :       return 0;
     383              :     }
     384            0 :     if (value > 255.0f) {
     385            0 :       return 255;
     386              :     }
     387            0 :     return (uint8_t)(value + 0.5f);
     388              :   }
     389              : 
     390     30516067 :   if (bits_per_channel < 1) {
     391            0 :     bits_per_channel = 1;
     392              :   }
     393              : 
     394     30516067 :   levels = 1 << bits_per_channel;
     395              : 
     396     30516067 :   clamped = value;
     397     30516067 :   if (clamped < 0.0f) {
     398          539 :     clamped = 0.0f;
     399     30515528 :   } else if (clamped > 255.0f) {
     400         1243 :     clamped = 255.0f;
     401              :   }
     402              : 
     403     30516067 :   if (levels <= 1) {
     404            0 :     return 0;
     405              :   }
     406              : 
     407     30516067 :   scaled = clamped * (float)(levels - 1) / 255.0f;
     408     30516067 :   if (scaled < 0.0f) {
     409            0 :     scaled = 0.0f;
     410              :   }
     411     30516067 :   if (scaled > (float)(levels - 1)) {
     412            0 :     scaled = (float)(levels - 1);
     413              :   }
     414              : 
     415     30516067 :   rounded = (float)((int32_t)(scaled + 0.5f));
     416              : 
     417     30516067 :   quantized = rounded * 255.0f / (float)(levels - 1);
     418     30516067 :   if (quantized < 0.0f) {
     419            0 :     quantized = 0.0f;
     420     30516067 :   } else if (quantized > 255.0f) {
     421            0 :     quantized = 255.0f;
     422              :   }
     423              : 
     424     30516067 :   return (uint8_t)(quantized + 0.5f);
     425              : }
     426              : 
     427     14622960 : uint8_t quantize_bits(uint8_t value, uint8_t bits) {
     428     14622960 :   if (bits >= 8) {
     429       407949 :     return value;
     430              :   }
     431              : 
     432     14215011 :   return quantize_channel_value((float)value, bits);
     433              : }
     434              : 
     435      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) {
     436      1197400 :   bits_rgb = clamp_reduced_bits(bits_rgb);
     437      1197400 :   bits_alpha = clamp_reduced_bits(bits_alpha);
     438              : 
     439      1197400 :   if (r) {
     440      1197400 :     *r = quantize_bits(*r, bits_rgb);
     441              :   }
     442      1197400 :   if (g) {
     443      1197400 :     *g = quantize_bits(*g, bits_rgb);
     444              :   }
     445      1197400 :   if (b) {
     446      1197400 :     *b = quantize_bits(*b, bits_rgb);
     447              :   }
     448      1197400 :   if (a) {
     449      1197400 :     *a = quantize_bits(*a, bits_alpha);
     450              :   }
     451      1197400 : }
     452              : 
     453           10 : void snap_rgba_image_to_bits(uint8_t *rgba, size_t pixel_count, uint8_t bits_rgb, uint8_t bits_alpha) {
     454              :   uint8_t r, g, b, a;
     455              :   size_t base, i;
     456              : 
     457           10 :   if (!rgba || pixel_count == 0) {
     458            0 :     return;
     459              :   }
     460              : 
     461           10 :   bits_rgb = clamp_reduced_bits(bits_rgb);
     462           10 :   bits_alpha = clamp_reduced_bits(bits_alpha);
     463           10 :   if (bits_rgb >= 8 && bits_alpha >= 8) {
     464            0 :     return;
     465              :   }
     466              : 
     467      1181966 :   for (i = 0; i < pixel_count; ++i) {
     468      1181956 :     base = i * 4;
     469              : 
     470      1181956 :     r = rgba[base + 0];
     471      1181956 :     g = rgba[base + 1];
     472      1181956 :     b = rgba[base + 2];
     473      1181956 :     a = rgba[base + 3];
     474              : 
     475      1181956 :     snap_rgba_to_bits(&r, &g, &b, &a, bits_rgb, bits_alpha);
     476              : 
     477      1181956 :     rgba[base + 0] = r;
     478      1181956 :     rgba[base + 1] = g;
     479      1181956 :     rgba[base + 2] = b;
     480      1181956 :     rgba[base + 3] = a;
     481              :   }
     482              : }
     483              : 
     484        10021 : uint32_t color_distance_sq(const cpres_rgba_color_t *lhs, const cpres_rgba_color_t *rhs) {
     485              :   int32_t dr, dg, db, da;
     486              : 
     487        10021 :   if (!lhs || !rhs) {
     488            0 :     return 0;
     489              :   }
     490              : 
     491        10021 :   dr = (int32_t)lhs->r - (int32_t)rhs->r;
     492        10021 :   dg = (int32_t)lhs->g - (int32_t)rhs->g;
     493        10021 :   db = (int32_t)lhs->b - (int32_t)rhs->b;
     494        10021 :   da = (int32_t)lhs->a - (int32_t)rhs->a;
     495              : 
     496        10021 :   return (uint32_t)(dr * dr + dg * dg + db * db + da * da);
     497              : }
     498              : 
     499            1 : float estimate_bitdepth_dither_level(const uint8_t *rgba, png_uint_32 width, png_uint_32 height, uint8_t bits_per_channel) {
     500              :   png_uint_32 y, x;
     501              :   uint8_t r, g, b, a;
     502              :   size_t pixel_count, gradient_samples, opaque_pixels, translucent_pixels, base, right, below;
     503              :   double gradient_accum;
     504              :   float base_level, target, right_luma, below_luma, luma, normalized_gradient, opaque_ratio, translucent_ratio, min_luma, max_luma, coverage, gradient_span;
     505              : 
     506            1 :   if (!rgba || width == 0 || height == 0) {
     507            0 :     return clamp_float(COLOPRESSO_PNGX_DEFAULT_LOSSY_DITHER_LEVEL, 0.0f, 1.0f);
     508              :   }
     509              : 
     510            1 :   pixel_count = (size_t)width * (size_t)height;
     511            1 :   gradient_accum = 0.0;
     512            1 :   gradient_samples = 0;
     513            1 :   opaque_pixels = 0;
     514            1 :   translucent_pixels = 0;
     515            1 :   min_luma = 255.0f;
     516            1 :   max_luma = 0.0f;
     517              : 
     518            3 :   for (y = 0; y < height; ++y) {
     519            6 :     for (x = 0; x < width; ++x) {
     520            4 :       base = (((size_t)y * (size_t)width) + (size_t)x) * 4;
     521            4 :       r = rgba[base + 0];
     522            4 :       g = rgba[base + 1];
     523            4 :       b = rgba[base + 2];
     524            4 :       a = rgba[base + 3];
     525            4 :       luma = calc_luma(r, g, b);
     526              : 
     527            4 :       if (luma < min_luma) {
     528            1 :         min_luma = luma;
     529              :       }
     530            4 :       if (luma > max_luma) {
     531            0 :         max_luma = luma;
     532              :       }
     533              : 
     534            4 :       if (a > PNGX_DITHER_OPAQUE_THRESHOLD) {
     535            4 :         ++opaque_pixels;
     536            0 :       } else if (a > PNGX_DITHER_TRANSLUCENT_THRESHOLD) {
     537            0 :         ++translucent_pixels;
     538              :       }
     539              : 
     540            4 :       if (x + 1 < width) {
     541            2 :         right = base + 4;
     542            2 :         right_luma = calc_luma(rgba[right + 0], rgba[right + 1], rgba[right + 2]);
     543            2 :         gradient_accum += absf(right_luma - luma);
     544            2 :         ++gradient_samples;
     545              :       }
     546              : 
     547            4 :       if (y + 1 < height) {
     548            2 :         below = (((size_t)(y + 1) * (size_t)width) + (size_t)x) * 4;
     549            2 :         below_luma = calc_luma(rgba[below + 0], rgba[below + 1], rgba[below + 2]);
     550            2 :         gradient_accum += absf(below_luma - luma);
     551            2 :         ++gradient_samples;
     552              :       }
     553              :     }
     554              :   }
     555              : 
     556            1 :   if (gradient_samples == 0) {
     557            0 :     gradient_samples = 1;
     558              :   }
     559              : 
     560            1 :   normalized_gradient = (float)(gradient_accum / ((double)gradient_samples * 255.0));
     561            1 :   opaque_ratio = (pixel_count > 0) ? (float)((double)opaque_pixels / (double)pixel_count) : 0.0f;
     562            1 :   translucent_ratio = (pixel_count > 0) ? (float)((double)translucent_pixels / (double)pixel_count) : 0.0f;
     563              : 
     564            1 :   coverage = (max_luma - min_luma) / 255.0f;
     565            1 :   if (coverage < 0.0f) {
     566            0 :     coverage = 0.0f;
     567            1 :   } else if (coverage > 1.0f) {
     568            0 :     coverage = 1.0f;
     569              :   }
     570              : 
     571            1 :   gradient_span = coverage / ((normalized_gradient > PNGX_DITHER_GRADIENT_MIN) ? normalized_gradient : PNGX_DITHER_GRADIENT_MIN);
     572              : 
     573            1 :   base_level = PNGX_DITHER_BASE_LEVEL;
     574            1 :   target = base_level;
     575              : 
     576            1 :   if (normalized_gradient > 0.35f) {
     577            0 :     target += PNGX_DITHER_HIGH_GRADIENT_BOOST;
     578            1 :   } else if (normalized_gradient > 0.2f) {
     579            0 :     target += PNGX_DITHER_MID_GRADIENT_BOOST;
     580            1 :   } else if (normalized_gradient < 0.08f) {
     581            1 :     target -= PNGX_DITHER_LOW_GRADIENT_CUT;
     582            0 :   } else if (normalized_gradient < 0.15f) {
     583            0 :     target -= PNGX_DITHER_MID_LOW_GRADIENT_CUT;
     584              :   }
     585              : 
     586            1 :   if (opaque_ratio < 0.35f) {
     587            0 :     target -= PNGX_DITHER_OPAQUE_LOW_CUT;
     588            1 :   } else if (opaque_ratio > 0.9f) {
     589            1 :     target += PNGX_DITHER_OPAQUE_HIGH_BOOST;
     590              :   }
     591              : 
     592            1 :   if (translucent_ratio > 0.3f) {
     593            0 :     target -= PNGX_DITHER_TRANSLUCENT_CUT;
     594              :   }
     595              : 
     596            1 :   if (coverage > PNGX_DITHER_COVERAGE_THRESHOLD && gradient_span > PNGX_DITHER_SPAN_THRESHOLD) {
     597            0 :     if (target < PNGX_DITHER_TARGET_CAP) {
     598            0 :       target = PNGX_DITHER_TARGET_CAP;
     599              :     }
     600            0 :     if (bits_per_channel <= 4 && target < PNGX_DITHER_TARGET_CAP_LOW_BIT) {
     601            0 :       target = PNGX_DITHER_TARGET_CAP_LOW_BIT;
     602              :     }
     603              :   }
     604              : 
     605            1 :   if (bits_per_channel <= 2) {
     606            0 :     target += PNGX_DITHER_LOW_BIT_BOOST;
     607            0 :     if (normalized_gradient > 0.25f) {
     608            0 :       target += PNGX_DITHER_LOW_BIT_GRADIENT_BOOST;
     609              :     }
     610              :   }
     611              : 
     612            1 :   return clamp_float(target, PNGX_DITHER_MIN, PNGX_DITHER_MAX);
     613              : }
     614              : 
     615           22 : void build_fixed_palette(const pngx_options_t *source_opts, pngx_quant_support_t *support, pngx_options_t *patched_opts) {
     616              :   size_t user_count, derived_count, total_cap, i, j;
     617              :   bool duplicate;
     618              : 
     619           22 :   if (!source_opts || !support || !patched_opts) {
     620            0 :     return;
     621              :   }
     622              : 
     623           22 :   *patched_opts = *source_opts;
     624           22 :   user_count = (source_opts->protected_colors && source_opts->protected_colors_count > 0) ? (size_t)source_opts->protected_colors_count : 0;
     625           22 :   derived_count = support->derived_colors_len;
     626           22 :   if (derived_count == 0) {
     627            3 :     return;
     628              :   }
     629              : 
     630           19 :   total_cap = user_count + derived_count;
     631           19 :   support->combined_fixed_colors = (cpres_rgba_color_t *)malloc(sizeof(cpres_rgba_color_t) * total_cap);
     632           19 :   if (!support->combined_fixed_colors) {
     633            0 :     return;
     634              :   }
     635              : 
     636           19 :   if (user_count > 0 && source_opts->protected_colors) {
     637            1 :     memcpy(support->combined_fixed_colors, source_opts->protected_colors, sizeof(cpres_rgba_color_t) * user_count);
     638              :   }
     639           19 :   support->combined_fixed_len = user_count;
     640          348 :   for (i = 0; i < derived_count; ++i) {
     641          329 :     duplicate = false;
     642         3514 :     for (j = 0; j < support->combined_fixed_len; ++j) {
     643         3185 :       if (color_distance_sq(&support->derived_colors[i], &support->combined_fixed_colors[j]) < PNGX_FIXED_PALETTE_DISTANCE_SQ) {
     644            0 :         duplicate = true;
     645            0 :         break;
     646              :       }
     647              :     }
     648          329 :     if (duplicate) {
     649            0 :       continue;
     650              :     }
     651          329 :     if (support->combined_fixed_len < total_cap) {
     652          329 :       support->combined_fixed_colors[support->combined_fixed_len] = support->derived_colors[i];
     653          329 :       ++support->combined_fixed_len;
     654              :     }
     655          329 :     if (support->combined_fixed_len >= PNGX_FIXED_PALETTE_MAX) {
     656            0 :       break;
     657              :     }
     658              :   }
     659           19 :   if (support->combined_fixed_len > user_count) {
     660           19 :     patched_opts->protected_colors = support->combined_fixed_colors;
     661           19 :     patched_opts->protected_colors_count = (int32_t)support->combined_fixed_len;
     662              :   }
     663              : }
     664              : 
     665           25 : float resolve_quant_dither(const pngx_options_t *opts, const pngx_image_stats_t *stats) {
     666              :   float resolved, gradient_mean, saturation_mean, opaque_ratio, vibrant_ratio, gradient_max;
     667              : 
     668           25 :   if (!opts) {
     669            0 :     return 0.5f;
     670              :   }
     671              : 
     672           25 :   if (stats) {
     673           25 :     gradient_mean = stats->gradient_mean;
     674           25 :     saturation_mean = stats->saturation_mean;
     675           25 :     opaque_ratio = stats->opaque_ratio;
     676           25 :     vibrant_ratio = stats->vibrant_ratio;
     677           25 :     gradient_max = stats->gradient_max;
     678              :   } else {
     679            0 :     gradient_mean = PNGX_RESOLVE_DEFAULT_GRADIENT;
     680            0 :     saturation_mean = PNGX_RESOLVE_DEFAULT_SATURATION;
     681            0 :     opaque_ratio = PNGX_RESOLVE_DEFAULT_OPAQUE;
     682            0 :     vibrant_ratio = PNGX_RESOLVE_DEFAULT_VIBRANT;
     683            0 :     gradient_max = PNGX_RESOLVE_DEFAULT_GRADIENT;
     684              :   }
     685              : 
     686           25 :   resolved = opts->lossy_dither_level;
     687              : 
     688           25 :   if (opts->lossy_dither_auto) {
     689            1 :     resolved = PNGX_RESOLVE_AUTO_BASE + gradient_mean * PNGX_RESOLVE_AUTO_GRADIENT_WEIGHT + saturation_mean * PNGX_RESOLVE_AUTO_SATURATION_WEIGHT;
     690            1 :     if (opaque_ratio < 0.7f) {
     691            1 :       resolved -= PNGX_RESOLVE_AUTO_OPAQUE_CUT;
     692              :     }
     693              :   }
     694              : 
     695           25 :   if (opts->adaptive_dither_enable) {
     696           24 :     if (gradient_mean < 0.10f) {
     697           24 :       resolved -= PNGX_RESOLVE_ADAPTIVE_FLAT_CUT;
     698            0 :     } else if (gradient_mean > 0.30f) {
     699            0 :       resolved += PNGX_RESOLVE_ADAPTIVE_GRADIENT_BOOST;
     700              :     }
     701           24 :     if (gradient_max > 0.5f && vibrant_ratio > 0.12f) {
     702            0 :       resolved -= PNGX_RESOLVE_ADAPTIVE_VIBRANT_CUT;
     703              :     }
     704           24 :     if (saturation_mean > 0.38f) {
     705            7 :       resolved += PNGX_RESOLVE_ADAPTIVE_SATURATION_BOOST;
     706           17 :     } else if (saturation_mean < 0.12f) {
     707            3 :       resolved -= PNGX_RESOLVE_ADAPTIVE_SATURATION_CUT;
     708              :     }
     709              :   }
     710              : 
     711           25 :   if (resolved < PNGX_RESOLVE_MIN) {
     712            7 :     resolved = PNGX_RESOLVE_MIN;
     713           18 :   } else if (resolved > PNGX_RESOLVE_MAX) {
     714            0 :     resolved = PNGX_RESOLVE_MAX;
     715              :   }
     716              : 
     717           25 :   return resolved;
     718              : }
     719              : 
     720           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) {
     721           33 :   pngx_chroma_bucket_t *buckets = NULL, *bucket_entry;
     722              :   uint32_t x, y, range, sample;
     723           33 :   uint16_t *importance_work = NULL, raw_min, raw_max;
     724              :   uint8_t r, g, b, a, value;
     725              :   size_t pixel_index, base, next_row_base, opaque_pixels, translucent_pixels, vibrant_pixels;
     726           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,
     727              :                                                                                                                                                   *luma_row_tmp;
     728              :   bool need_map, need_buckets;
     729              : 
     730           33 :   if (!image || !opts || !support || !stats || image->pixel_count == 0) {
     731            0 :     return false;
     732              :   }
     733              : 
     734           33 :   raw_min = UINT16_MAX;
     735           33 :   raw_max = 0;
     736           33 :   need_map = opts->saliency_map_enable || opts->postprocess_smooth_enable;
     737           33 :   need_buckets = opts->chroma_anchor_enable;
     738           33 :   gradient_sum = 0.0f;
     739           33 :   saturation_sum = 0.0f;
     740           33 :   opaque_pixels = 0;
     741           33 :   translucent_pixels = 0;
     742           33 :   vibrant_pixels = 0;
     743           33 :   if (need_map) {
     744           28 :     importance_work = (uint16_t *)malloc(sizeof(uint16_t) * image->pixel_count);
     745           28 :     if (!importance_work) {
     746            0 :       return false;
     747              :     }
     748              :   }
     749              : 
     750           33 :   if (need_buckets) {
     751           28 :     buckets = (pngx_chroma_bucket_t *)calloc(PNGX_CHROMA_BUCKET_COUNT, sizeof(pngx_chroma_bucket_t));
     752           28 :     if (!buckets) {
     753            0 :       if (importance_work) {
     754            0 :         free(importance_work);
     755              :       }
     756              : 
     757            0 :       return false;
     758              :     }
     759              :   }
     760              : 
     761           33 :   luma_row_curr = (float *)malloc(sizeof(float) * image->width);
     762           33 :   luma_row_next = (float *)malloc(sizeof(float) * image->width);
     763           33 :   if (!luma_row_curr || !luma_row_next) {
     764            0 :     free(importance_work);
     765            0 :     free(buckets);
     766            0 :     free(luma_row_curr);
     767            0 :     free(luma_row_next);
     768              : 
     769            0 :     return false;
     770              :   }
     771              : 
     772        19309 :   for (x = 0; x < image->width; ++x) {
     773        19276 :     base = (size_t)x * 4;
     774        19276 :     luma_row_curr[x] = calc_luma(image->rgba[base + 0], image->rgba[base + 1], image->rgba[base + 2]) / 255.0f;
     775              :   }
     776              : 
     777        14069 :   for (y = 0; y < image->height; ++y) {
     778        14036 :     if (y + 1 < image->height) {
     779        14003 :       next_row_base = ((size_t)(y + 1) * (size_t)image->width) * 4;
     780     13161969 :       for (x = 0; x < image->width; ++x) {
     781     13147966 :         base = next_row_base + (size_t)x * 4;
     782     13147966 :         luma_row_next[x] = calc_luma(image->rgba[base + 0], image->rgba[base + 1], image->rgba[base + 2]) / 255.0f;
     783              :       }
     784              :     }
     785              : 
     786     13181278 :     for (x = 0; x < image->width; ++x) {
     787     13167242 :       base = (((size_t)y * (size_t)image->width) + (size_t)x) * 4;
     788     13167242 :       r = image->rgba[base + 0];
     789     13167242 :       g = image->rgba[base + 1];
     790     13167242 :       b = image->rgba[base + 2];
     791     13167242 :       a = image->rgba[base + 3];
     792     13167242 :       luma = luma_row_curr[x];
     793     13167242 :       gradient = 0.0f;
     794     13167242 :       saturation = calc_saturation(r, g, b);
     795     13167242 :       alpha_factor = (float)a / 255.0f;
     796              : 
     797     13167242 :       if (x + 1 < image->width) {
     798     13153206 :         right_luma = luma_row_curr[x + 1];
     799     13153206 :         gradient += absf(right_luma - luma);
     800              :       }
     801              : 
     802     13167242 :       if (y + 1 < image->height) {
     803     13147966 :         below_luma = luma_row_next[x];
     804     13147966 :         gradient += absf(below_luma - luma);
     805              :       }
     806              : 
     807     13167242 :       gradient *= PNGX_PREPARE_GRADIENT_SCALE;
     808     13167242 :       if (gradient > 1.0f) {
     809            0 :         gradient = 1.0f;
     810              :       }
     811              : 
     812     13167242 :       gradient_sum += gradient;
     813     13167242 :       if (gradient > stats->gradient_max) {
     814          702 :         stats->gradient_max = gradient;
     815              :       }
     816              : 
     817     13167242 :       saturation_sum += saturation;
     818              : 
     819     13167242 :       if (a > PNGX_DITHER_OPAQUE_THRESHOLD) {
     820       176859 :         ++opaque_pixels;
     821     12990383 :       } else if (a > PNGX_DITHER_TRANSLUCENT_THRESHOLD) {
     822     12884846 :         ++translucent_pixels;
     823              :       }
     824              : 
     825     13167242 :       if (saturation > PNGX_PREPARE_VIBRANT_SATURATION && gradient > PNGX_PREPARE_VIBRANT_GRADIENT && a > PNGX_PREPARE_VIBRANT_ALPHA) {
     826       545479 :         ++vibrant_pixels;
     827              :       }
     828              : 
     829     13167242 :       importance = gradient;
     830              : 
     831     13167242 :       if (opts->chroma_weight_enable) {
     832     13096322 :         importance += saturation * PNGX_PREPARE_CHROMA_WEIGHT;
     833              :       }
     834              : 
     835     13167242 :       if (opts->gradient_boost_enable) {
     836     13096322 :         if (gradient > PNGX_PREPARE_BOOST_THRESHOLD) {
     837        29792 :           importance += PNGX_PREPARE_BOOST_BASE + (gradient * PNGX_PREPARE_BOOST_FACTOR);
     838     13066530 :         } else if (gradient < PNGX_PREPARE_CUT_THRESHOLD) {
     839      9940372 :           importance *= PNGX_PREPARE_CUT_FACTOR;
     840              :         }
     841              :       }
     842              : 
     843     13167242 :       if (alpha_factor < PNGX_PREPARE_ALPHA_THRESHOLD) {
     844     11234189 :         importance *= (PNGX_PREPARE_ALPHA_BASE + alpha_factor * PNGX_PREPARE_ALPHA_MULTIPLIER);
     845              :       }
     846              : 
     847     13167242 :       if (importance < 0.0f) {
     848            0 :         importance = 0.0f;
     849     13167242 :       } else if (importance > 1.0f) {
     850            0 :         importance = 1.0f;
     851              :       }
     852              : 
     853     13167242 :       if (need_map && importance_work) {
     854     13100418 :         pixel_index = ((size_t)y * (size_t)image->width) + (size_t)x;
     855     13100418 :         importance_work[pixel_index] = (uint16_t)(importance * PNGX_IMPORTANCE_SCALE + 0.5f);
     856              : 
     857     13100418 :         if (importance_work[pixel_index] < raw_min) {
     858          942 :           raw_min = importance_work[pixel_index];
     859              :         }
     860              : 
     861     13100418 :         if (importance_work[pixel_index] > raw_max) {
     862         1114 :           raw_max = importance_work[pixel_index];
     863              :         }
     864              :       }
     865              : 
     866     13167242 :       if (need_buckets && buckets) {
     867     13096326 :         if ((saturation > PNGX_PREPARE_BUCKET_SATURATION || importance > PNGX_PREPARE_BUCKET_IMPORTANCE) && a > PNGX_PREPARE_BUCKET_ALPHA) {
     868      3794649 :           importance_mix = (importance * PNGX_PREPARE_MIX_IMPORTANCE) + (gradient * PNGX_PREPARE_MIX_GRADIENT);
     869      3794649 :           anchor_score = (saturation * PNGX_PREPARE_ANCHOR_SATURATION) + (importance_mix * PNGX_PREPARE_ANCHOR_MIX);
     870      3794649 :           if (importance > PNGX_PREPARE_ANCHOR_IMP_THRESHOLD) {
     871            0 :             anchor_score += PNGX_PREPARE_ANCHOR_IMP_BONUS;
     872              :           }
     873      3794649 :           if (anchor_score > PNGX_PREPARE_ANCHOR_SCORE_THRESHOLD) {
     874      1965600 :             bucket_entry = &buckets[chroma_bucket_index(r, g, b)];
     875      1965600 :             bucket_entry->r_sum += (uint64_t)r;
     876      1965600 :             bucket_entry->g_sum += (uint64_t)g;
     877      1965600 :             bucket_entry->b_sum += (uint64_t)b;
     878      1965600 :             bucket_entry->a_sum += (uint64_t)a;
     879      1965600 :             bucket_entry->count += 1;
     880      1965600 :             bucket_entry->score += anchor_score;
     881      1965600 :             bucket_entry->importance_accum += importance;
     882              :           }
     883              :         }
     884              :       }
     885              :     }
     886              : 
     887        14036 :     luma_row_tmp = luma_row_curr;
     888        14036 :     luma_row_curr = luma_row_next;
     889        14036 :     luma_row_next = luma_row_tmp;
     890              :   }
     891              : 
     892           33 :   free(luma_row_curr);
     893           33 :   free(luma_row_next);
     894              : 
     895           33 :   if (image->pixel_count > 0) {
     896           33 :     stats->gradient_mean = gradient_sum / (float)image->pixel_count;
     897           33 :     stats->saturation_mean = saturation_sum / (float)image->pixel_count;
     898           33 :     stats->opaque_ratio = (float)opaque_pixels / (float)image->pixel_count;
     899           33 :     stats->translucent_ratio = (float)translucent_pixels / (float)image->pixel_count;
     900           33 :     stats->vibrant_ratio = (float)vibrant_pixels / (float)image->pixel_count;
     901              :   }
     902           33 :   if (need_map && importance_work && image->pixel_count > 0) {
     903           28 :     range = (uint32_t)(raw_max - raw_min);
     904           28 :     if (range == 0) {
     905            3 :       range = 1;
     906              :     }
     907              : 
     908           28 :     support->importance_map = (uint8_t *)malloc(image->pixel_count);
     909           28 :     if (support->importance_map) {
     910     13100446 :       for (pixel_index = 0; pixel_index < image->pixel_count; ++pixel_index) {
     911     13100418 :         sample = (uint32_t)(importance_work[pixel_index] - raw_min);
     912     13100418 :         value = (uint8_t)((sample * 255) / range);
     913     13100418 :         if (value < 4) {
     914        84309 :           value = 4;
     915              :         }
     916     13100418 :         support->importance_map[pixel_index] = value;
     917              :       }
     918           28 :       support->importance_map_len = image->pixel_count;
     919              :     }
     920              :   }
     921              : 
     922           33 :   free(importance_work);
     923              : 
     924           33 :   if (need_buckets && buckets) {
     925           28 :     extract_chroma_anchors(support, buckets, PNGX_CHROMA_BUCKET_COUNT, image->pixel_count);
     926              :   }
     927              : 
     928           33 :   free(buckets);
     929              : 
     930           33 :   return true;
     931              : }
        

Generated by: LCOV version 2.0-1