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