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 <stdlib.h>
13 : #include <string.h>
14 :
15 : #include <png.h>
16 :
17 : #include "internal/log.h"
18 : #include "internal/pngx_common.h"
19 :
20 3 : static inline uint8_t lossy_type_bits(uint8_t lossy_type)
21 : {
22 3 : switch (lossy_type) {
23 3 : case PNGX_LOSSY_TYPE_LIMITED_RGBA4444:
24 3 : return PNGX_LIMITED_RGBA4444_BITS;
25 0 : default:
26 0 : return PNGX_FULL_CHANNEL_BITS;
27 : }
28 : }
29 :
30 1671168 : static inline void process_bitdepth_pixel(uint8_t *rgba, png_uint_32 width, png_uint_32 height,
31 : uint32_t x, uint32_t y, uint8_t bits_per_channel,
32 : float dither_level, float *err_curr, float *err_next,
33 : bool left_to_right)
34 : {
35 : uint8_t channel, quantized;
36 1671168 : size_t pixel_index = ((size_t)y * (size_t)width + (size_t)x) * PNGX_RGBA_CHANNELS,
37 1671168 : err_index = (size_t)x * PNGX_RGBA_CHANNELS;
38 : float value, error;
39 :
40 8355840 : for (channel = 0; channel < PNGX_RGBA_CHANNELS; ++channel) {
41 6684672 : value = (float)rgba[pixel_index + channel] + err_curr[err_index + channel];
42 6684672 : quantized = quantize_channel_value(value, bits_per_channel);
43 6684672 : error = (value - (float)quantized) * dither_level;
44 :
45 6684672 : rgba[pixel_index + channel] = quantized;
46 :
47 6684672 : if (dither_level <= 0.0f || error == 0.0f) {
48 690 : continue;
49 : }
50 :
51 6683982 : if (left_to_right) {
52 3341912 : if (x + 1 < width) {
53 3338845 : err_curr[err_index + PNGX_RGBA_CHANNELS + channel] += error * (7.0f / 16.0f);
54 : }
55 :
56 3341912 : if (y + 1 < height) {
57 3341912 : if (x > 0) {
58 3338842 : err_next[err_index - PNGX_RGBA_CHANNELS + channel] += error * (3.0f / 16.0f);
59 : }
60 :
61 3341912 : err_next[err_index + channel] += error * (5.0f / 16.0f);
62 :
63 3341912 : if (x + 1 < width) {
64 3338845 : err_next[err_index + PNGX_RGBA_CHANNELS + channel] += error * (1.0f / 16.0f);
65 : }
66 : }
67 : } else {
68 3342070 : if (x > 0) {
69 3338998 : err_curr[err_index - PNGX_RGBA_CHANNELS + channel] += error * (7.0f / 16.0f);
70 : }
71 :
72 3342070 : if (y + 1 < height) {
73 3335926 : if (x + 1 < width) {
74 3332874 : err_next[err_index + PNGX_RGBA_CHANNELS + channel] += error * (3.0f / 16.0f);
75 : }
76 :
77 3335926 : err_next[err_index + channel] += error * (5.0f / 16.0f);
78 :
79 3335926 : if (x > 0) {
80 3332866 : err_next[err_index - PNGX_RGBA_CHANNELS + channel] += error * (1.0f / 16.0f);
81 : }
82 : }
83 : }
84 : }
85 1671168 : }
86 :
87 3 : static inline void reduce_rgba_bitdepth_dither(uint32_t thread_count, uint8_t *rgba,
88 : png_uint_32 width, png_uint_32 height,
89 : uint8_t bits_per_channel, float dither_level)
90 : {
91 : uint32_t x, y;
92 : size_t row_stride;
93 : bool left_to_right;
94 : float *err_curr, *err_next, *tmp;
95 :
96 3 : if (!rgba || width == 0 || height == 0 || bits_per_channel >= PNGX_FULL_CHANNEL_BITS) {
97 0 : return;
98 : }
99 :
100 3 : if (dither_level <= 0.0f) {
101 0 : snap_rgba_image_to_bits(thread_count, rgba, (size_t)width * (size_t)height,
102 : bits_per_channel, bits_per_channel);
103 :
104 0 : return;
105 : }
106 :
107 3 : row_stride = (size_t)width * PNGX_RGBA_CHANNELS;
108 3 : err_curr = (float *)calloc(row_stride, sizeof(float));
109 3 : err_next = (float *)calloc(row_stride, sizeof(float));
110 3 : if (!err_curr || !err_next) {
111 0 : free(err_curr);
112 0 : free(err_next);
113 0 : snap_rgba_image_to_bits(thread_count, rgba, (size_t)width * (size_t)height,
114 : bits_per_channel, bits_per_channel);
115 :
116 0 : return;
117 : }
118 :
119 1539 : for (y = 0; y < height; ++y) {
120 1536 : left_to_right = ((y & 1) == 0);
121 1536 : memset(err_next, 0, row_stride * sizeof(float));
122 :
123 1536 : if (left_to_right) {
124 836352 : for (x = 0; x < width; ++x) {
125 835584 : process_bitdepth_pixel(rgba, width, height, x, y, bits_per_channel, dither_level,
126 : err_curr, err_next, true);
127 : }
128 : } else {
129 768 : x = width;
130 836352 : while (x-- > 0) {
131 835584 : process_bitdepth_pixel(rgba, width, height, x, y, bits_per_channel, dither_level,
132 : err_curr, err_next, false);
133 : }
134 : }
135 :
136 1536 : tmp = err_curr;
137 1536 : err_curr = err_next;
138 1536 : err_next = tmp;
139 : }
140 :
141 3 : free(err_curr);
142 3 : free(err_next);
143 : }
144 :
145 3 : static inline void reduce_rgba_bitdepth(uint32_t thread_count, uint8_t *rgba, png_uint_32 width,
146 : png_uint_32 height, uint8_t bits_per_channel,
147 : float dither_level)
148 : {
149 3 : if (!rgba || width == 0 || height == 0) {
150 0 : return;
151 : }
152 :
153 3 : if (bits_per_channel >= PNGX_FULL_CHANNEL_BITS) {
154 0 : return;
155 : }
156 :
157 3 : if (dither_level > 0.0f) {
158 3 : reduce_rgba_bitdepth_dither(thread_count, rgba, width, height, bits_per_channel,
159 : dither_level);
160 : } else {
161 0 : snap_rgba_image_to_bits(thread_count, rgba, (size_t)width * (size_t)height,
162 : bits_per_channel, bits_per_channel);
163 : }
164 : }
165 :
166 3 : extern bool pngx_quantize_limited4444(const uint8_t *png_data, size_t png_size,
167 : const pngx_options_t *opts, uint8_t **out_data,
168 : size_t *out_size)
169 : {
170 : const char *label;
171 : pngx_rgba_image_t image;
172 : bool success;
173 : float resolved_dither;
174 :
175 3 : if (!png_data || png_size == 0 || !opts || !out_data || !out_size) {
176 0 : return false;
177 : }
178 :
179 3 : if (!load_rgba_image(png_data, png_size, &image)) {
180 0 : return false;
181 : }
182 :
183 3 : if (opts->lossy_dither_auto) {
184 : resolved_dither =
185 0 : estimate_bitdepth_dither_level_limited4444(image.rgba, image.width, image.height);
186 : } else {
187 3 : resolved_dither = clamp_float(opts->lossy_dither_level, 0.0f, 1.0f);
188 : }
189 :
190 3 : reduce_rgba_bitdepth(opts->thread_count, image.rgba, image.width, image.height,
191 3 : lossy_type_bits(opts->lossy_type), resolved_dither);
192 :
193 3 : success = create_rgba_png(image.rgba, image.pixel_count, image.width, image.height, out_data,
194 : out_size);
195 3 : rgba_image_reset(&image);
196 :
197 3 : if (success) {
198 3 : label = lossy_type_label(opts->lossy_type);
199 :
200 3 : if (opts->lossy_dither_auto) {
201 0 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Auto dither %.2f selected for %s",
202 : resolved_dither, label);
203 : } else {
204 3 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Manual dither %.2f used for %s",
205 : resolved_dither, label);
206 : }
207 : }
208 :
209 3 : return success;
210 : }
|