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 : #include <zlib.h>
17 :
18 : #include "internal/log.h"
19 : #include "internal/pngx_common.h"
20 : #include "internal/threads.h"
21 :
22 : typedef struct {
23 : pngx_rgba_image_t image;
24 : pngx_quant_support_t support;
25 : pngx_image_stats_t stats;
26 : pngx_options_t tuned_opts;
27 : float resolved_dither;
28 : bool prefer_uniform;
29 : bool initialized;
30 : } palette256_context_t;
31 :
32 : typedef struct {
33 : uint8_t *indices;
34 : const uint8_t *reference;
35 : uint32_t width;
36 : uint32_t height;
37 : const cpres_rgba_color_t *palette;
38 : size_t palette_len;
39 : const uint8_t *importance_map;
40 : float cutoff;
41 : } postprocess_indices_parallel_ctx_t;
42 :
43 : typedef struct {
44 : const uint8_t *pixels;
45 : size_t row_stride;
46 : uint32_t width;
47 : uint32_t height;
48 : int color_type;
49 : const png_color *palette;
50 : const png_byte *alpha;
51 : int palette_len;
52 : int num_trans;
53 : } png_write_job_t;
54 :
55 : /* Context of the multi-step API (pngx_palette256_prepare / _finalize) used by the WASM separation
56 : * mode, whose steps are called one after another from JS. */
57 : static palette256_context_t g_palette256_ctx = {0};
58 :
59 30 : static inline void alpha_bleed_rgb_from_opaque(uint8_t *rgba, uint32_t width, uint32_t height,
60 : const pngx_options_t *opts)
61 : {
62 : uint32_t *seed_rgb, x, y, best_rgb;
63 : uint16_t *dist, best, d, max_distance;
64 : uint8_t opaque_threshold, soft_limit;
65 : size_t pixel_count, i, base, idx;
66 : bool has_seed;
67 :
68 30 : if (!rgba || width == 0 || height == 0) {
69 0 : return;
70 : }
71 :
72 30 : if (!opts || !opts->palette256_alpha_bleed_enable) {
73 28 : return;
74 : }
75 :
76 2 : max_distance = opts->palette256_alpha_bleed_max_distance;
77 2 : opaque_threshold = opts->palette256_alpha_bleed_opaque_threshold;
78 2 : soft_limit = opts->palette256_alpha_bleed_soft_limit;
79 :
80 2 : if ((size_t)width > SIZE_MAX / (size_t)height) {
81 0 : return;
82 : }
83 :
84 2 : pixel_count = (size_t)width * (size_t)height;
85 :
86 2 : dist = (uint16_t *)malloc(sizeof(uint16_t) * pixel_count);
87 2 : seed_rgb = (uint32_t *)malloc(sizeof(uint32_t) * pixel_count);
88 2 : if (!dist || !seed_rgb) {
89 0 : free(dist);
90 0 : free(seed_rgb);
91 :
92 0 : return;
93 : }
94 :
95 2 : has_seed = false;
96 4100 : for (i = 0; i < pixel_count; ++i) {
97 4098 : base = i * 4;
98 4098 : if (rgba[base + 3] == 0) {
99 4096 : rgba[base + 0] = 0;
100 4096 : rgba[base + 1] = 0;
101 4096 : rgba[base + 2] = 0;
102 : }
103 :
104 4098 : if (rgba[base + 3] >= opaque_threshold) {
105 1 : dist[i] = 0;
106 1 : seed_rgb[i] = ((uint32_t)rgba[base + 0] << 16) | ((uint32_t)rgba[base + 1] << 8) |
107 1 : (uint32_t)rgba[base + 2];
108 1 : has_seed = true;
109 : } else {
110 4097 : dist[i] = UINT16_MAX;
111 4097 : seed_rgb[i] = 0;
112 : }
113 : }
114 :
115 2 : if (!has_seed) {
116 1 : free(dist);
117 1 : free(seed_rgb);
118 :
119 1 : return;
120 : }
121 :
122 4 : for (i = 0; i < 3; ++i) {
123 6 : for (y = 0; y < height; ++y) {
124 9 : for (x = 0; x < width; ++x) {
125 6 : idx = (size_t)y * (size_t)width + (size_t)x;
126 6 : best = dist[idx];
127 6 : best_rgb = seed_rgb[idx];
128 :
129 6 : if (x > 0 && dist[idx - 1] != UINT16_MAX && (uint16_t)(dist[idx - 1] + 1) < best) {
130 1 : best = (uint16_t)(dist[idx - 1] + 1);
131 1 : best_rgb = seed_rgb[idx - 1];
132 : }
133 :
134 6 : if (y > 0 && dist[idx - (size_t)width] != UINT16_MAX &&
135 0 : (uint16_t)(dist[idx - (size_t)width] + 1) < best) {
136 0 : best = (uint16_t)(dist[idx - (size_t)width] + 1);
137 0 : best_rgb = seed_rgb[idx - (size_t)width];
138 : }
139 :
140 6 : if (x > 0 && y > 0 && dist[idx - (size_t)width - 1] != UINT16_MAX &&
141 0 : (uint16_t)(dist[idx - (size_t)width - 1] + 1) < best) {
142 0 : best = (uint16_t)(dist[idx - (size_t)width - 1] + 1);
143 0 : best_rgb = seed_rgb[idx - (size_t)width - 1];
144 : }
145 :
146 6 : if (x + 1 < width && y > 0 && dist[idx - (size_t)width + 1] != UINT16_MAX &&
147 0 : (uint16_t)(dist[idx - (size_t)width + 1] + 1) < best) {
148 0 : best = (uint16_t)(dist[idx - (size_t)width + 1] + 1);
149 0 : best_rgb = seed_rgb[idx - (size_t)width + 1];
150 : }
151 :
152 6 : dist[idx] = best;
153 6 : seed_rgb[idx] = best_rgb;
154 : }
155 : }
156 :
157 6 : for (y = height; y-- > 0;) {
158 9 : for (x = width; x-- > 0;) {
159 6 : idx = (size_t)y * (size_t)width + (size_t)x;
160 6 : best = dist[idx];
161 6 : best_rgb = seed_rgb[idx];
162 :
163 6 : if (x + 1 < width && dist[idx + 1] != UINT16_MAX &&
164 3 : (uint16_t)(dist[idx + 1] + 1) < best) {
165 0 : best = (uint16_t)(dist[idx + 1] + 1);
166 0 : best_rgb = seed_rgb[idx + 1];
167 : }
168 :
169 6 : if (y + 1 < height && dist[idx + (size_t)width] != UINT16_MAX &&
170 0 : (uint16_t)(dist[idx + (size_t)width] + 1) < best) {
171 0 : best = (uint16_t)(dist[idx + (size_t)width] + 1);
172 0 : best_rgb = seed_rgb[idx + (size_t)width];
173 : }
174 :
175 6 : if (x + 1 < width && y + 1 < height &&
176 0 : dist[idx + (size_t)width + 1] != UINT16_MAX &&
177 0 : (uint16_t)(dist[idx + (size_t)width + 1] + 1) < best) {
178 0 : best = (uint16_t)(dist[idx + (size_t)width + 1] + 1);
179 0 : best_rgb = seed_rgb[idx + (size_t)width + 1];
180 : }
181 :
182 6 : if (x > 0 && y + 1 < height && dist[idx + (size_t)width - 1] != UINT16_MAX &&
183 0 : (uint16_t)(dist[idx + (size_t)width - 1] + 1) < best) {
184 0 : best = (uint16_t)(dist[idx + (size_t)width - 1] + 1);
185 0 : best_rgb = seed_rgb[idx + (size_t)width - 1];
186 : }
187 :
188 6 : dist[idx] = best;
189 6 : seed_rgb[idx] = best_rgb;
190 : }
191 : }
192 : }
193 :
194 2 : for (y = 0; y < height; ++y) {
195 3 : for (x = 0; x < width; ++x) {
196 2 : idx = (size_t)y * (size_t)width + (size_t)x;
197 2 : d = dist[idx];
198 2 : base = idx * 4;
199 :
200 2 : if (rgba[base + 3] <= soft_limit && d != UINT16_MAX && d <= max_distance) {
201 1 : rgba[base + 0] = (uint8_t)((seed_rgb[idx] >> 16) & 0xFFu);
202 1 : rgba[base + 1] = (uint8_t)((seed_rgb[idx] >> 8) & 0xFFu);
203 1 : rgba[base + 2] = (uint8_t)(seed_rgb[idx] & 0xFFu);
204 : }
205 : }
206 : }
207 :
208 1 : free(dist);
209 1 : free(seed_rgb);
210 : }
211 :
212 27 : static inline void sanitize_transparent_palette(cpres_rgba_color_t *palette, size_t palette_len)
213 : {
214 : size_t i;
215 :
216 27 : if (!palette || palette_len == 0) {
217 0 : return;
218 : }
219 :
220 1932 : for (i = 0; i < palette_len; ++i) {
221 1905 : if (palette[i].a == 0) {
222 15 : palette[i].r = 0;
223 15 : palette[i].g = 0;
224 15 : palette[i].b = 0;
225 : }
226 : }
227 : }
228 :
229 25 : static inline bool is_smooth_gradient_profile(const pngx_image_stats_t *stats,
230 : const pngx_options_t *opts)
231 : {
232 : float opaque_ratio_threshold, gradient_mean_max, saturation_mean_max;
233 :
234 25 : if (!stats) {
235 0 : return false;
236 : }
237 :
238 25 : opaque_ratio_threshold = (opts ? opts->palette256_profile_opaque_ratio_threshold : -1.0f);
239 25 : if (opaque_ratio_threshold < 0.0f) {
240 0 : opaque_ratio_threshold = PNGX_PALETTE256_GRADIENT_PROFILE_OPAQUE_RATIO_THRESHOLD;
241 : }
242 :
243 25 : gradient_mean_max = (opts ? opts->palette256_profile_gradient_mean_max : -1.0f);
244 25 : if (gradient_mean_max < 0.0f) {
245 0 : gradient_mean_max = PNGX_PALETTE256_GRADIENT_PROFILE_GRADIENT_MEAN_MAX;
246 : }
247 :
248 25 : saturation_mean_max = (opts ? opts->palette256_profile_saturation_mean_max : -1.0f);
249 25 : if (saturation_mean_max < 0.0f) {
250 0 : saturation_mean_max = PNGX_PALETTE256_GRADIENT_PROFILE_SATURATION_MEAN_MAX;
251 : }
252 :
253 25 : if (stats->opaque_ratio > opaque_ratio_threshold && stats->gradient_mean < gradient_mean_max &&
254 2 : stats->saturation_mean < saturation_mean_max) {
255 2 : return true;
256 : }
257 :
258 23 : return false;
259 : }
260 :
261 30 : static inline void tune_quant_params_for_image(pngx_bridge_quant_params_t *params,
262 : const pngx_options_t *opts,
263 : const pngx_image_stats_t *stats)
264 : {
265 : uint8_t quality_min, quality_max;
266 : int32_t speed_max, quality_min_floor, quality_max_target;
267 : float opaque_ratio_threshold, gradient_mean_max, saturation_mean_max;
268 :
269 30 : if (!params || !opts || !stats) {
270 0 : return;
271 : }
272 :
273 30 : opaque_ratio_threshold = opts->palette256_tune_opaque_ratio_threshold;
274 30 : if (opaque_ratio_threshold < 0.0f) {
275 0 : opaque_ratio_threshold = PNGX_PALETTE256_TUNE_OPAQUE_RATIO_THRESHOLD;
276 : }
277 :
278 30 : gradient_mean_max = opts->palette256_tune_gradient_mean_max;
279 30 : if (gradient_mean_max < 0.0f) {
280 0 : gradient_mean_max = PNGX_PALETTE256_TUNE_GRADIENT_MEAN_MAX;
281 : }
282 :
283 30 : saturation_mean_max = opts->palette256_tune_saturation_mean_max;
284 30 : if (saturation_mean_max < 0.0f) {
285 0 : saturation_mean_max = PNGX_PALETTE256_TUNE_SATURATION_MEAN_MAX;
286 : }
287 :
288 30 : speed_max = opts->palette256_tune_speed_max;
289 30 : if (speed_max < 0) {
290 0 : speed_max = PNGX_PALETTE256_TUNE_SPEED_MAX;
291 : }
292 :
293 30 : quality_min_floor = opts->palette256_tune_quality_min_floor;
294 30 : if (quality_min_floor < 0) {
295 0 : quality_min_floor = PNGX_PALETTE256_TUNE_QUALITY_MIN_FLOOR;
296 : }
297 :
298 30 : quality_max_target = opts->palette256_tune_quality_max_target;
299 30 : if (quality_max_target < 0) {
300 0 : quality_max_target = PNGX_PALETTE256_TUNE_QUALITY_MAX_TARGET;
301 : }
302 :
303 30 : if (stats->opaque_ratio > opaque_ratio_threshold && stats->gradient_mean < gradient_mean_max &&
304 5 : stats->saturation_mean < saturation_mean_max) {
305 4 : if (params->speed > speed_max) {
306 3 : params->speed = speed_max;
307 : }
308 :
309 4 : quality_min = params->quality_min;
310 4 : quality_max = params->quality_max;
311 :
312 4 : if (quality_max < (uint8_t)quality_max_target) {
313 3 : quality_max = (uint8_t)quality_max_target;
314 : }
315 :
316 4 : if (quality_min < (uint8_t)quality_min_floor) {
317 3 : quality_min = (uint8_t)quality_min_floor;
318 : }
319 :
320 4 : if (quality_min > quality_max) {
321 1 : quality_min = quality_max;
322 : }
323 :
324 4 : params->quality_min = quality_min;
325 4 : params->quality_max = quality_max;
326 : }
327 : }
328 :
329 4 : static inline void postprocess_indices_parallel_worker(void *context, uint32_t start, uint32_t end)
330 : {
331 4 : postprocess_indices_parallel_ctx_t *ctx = (postprocess_indices_parallel_ctx_t *)context;
332 : uint32_t x, y, dist_sq, idx_start_y, idx_end_y;
333 : uint8_t base_color, importance, neighbor_colors[4], neighbor_used, candidate;
334 : size_t idx;
335 : bool neighbor_has_base;
336 :
337 4 : if (!ctx || !ctx->indices || !ctx->reference || !ctx->importance_map) {
338 0 : return;
339 : }
340 :
341 4 : idx_start_y = start;
342 4 : idx_end_y = end;
343 4 : if (idx_end_y > ctx->height) {
344 0 : idx_end_y = ctx->height;
345 : }
346 :
347 165 : for (y = idx_start_y; y < idx_end_y; ++y) {
348 9379 : for (x = 0; x < ctx->width; ++x) {
349 9218 : idx = (size_t)y * (size_t)ctx->width + (size_t)x;
350 9218 : base_color = ctx->reference[idx];
351 9218 : importance = ctx->importance_map[idx];
352 9218 : neighbor_used = 0;
353 9218 : neighbor_has_base = false;
354 :
355 9218 : if (ctx->cutoff >= 0.0f && ((float)importance / 255.0f) >= ctx->cutoff) {
356 2 : continue;
357 : }
358 :
359 9216 : if (x > 0) {
360 9056 : neighbor_colors[neighbor_used] = ctx->reference[idx - 1];
361 9056 : if (neighbor_colors[neighbor_used] == base_color) {
362 8918 : neighbor_has_base = true;
363 : }
364 :
365 9056 : ++neighbor_used;
366 : }
367 :
368 9216 : if (x + 1 < ctx->width) {
369 9055 : neighbor_colors[neighbor_used] = ctx->reference[idx + 1];
370 9055 : if (neighbor_colors[neighbor_used] == base_color) {
371 8918 : neighbor_has_base = true;
372 : }
373 :
374 9055 : ++neighbor_used;
375 : }
376 :
377 9216 : if (y > 0) {
378 9055 : neighbor_colors[neighbor_used] = ctx->reference[idx - (size_t)ctx->width];
379 9055 : if (neighbor_colors[neighbor_used] == base_color) {
380 8977 : neighbor_has_base = true;
381 : }
382 :
383 9055 : ++neighbor_used;
384 : }
385 :
386 9216 : if (y + 1 < ctx->height) {
387 9055 : neighbor_colors[neighbor_used] = ctx->reference[idx + (size_t)ctx->width];
388 9055 : if (neighbor_colors[neighbor_used] == base_color) {
389 8977 : neighbor_has_base = true;
390 : }
391 :
392 9055 : ++neighbor_used;
393 : }
394 :
395 9216 : if (neighbor_used < 3) {
396 13 : continue;
397 : }
398 :
399 9203 : candidate = neighbor_colors[0];
400 :
401 9203 : if (neighbor_used == 3) {
402 616 : if (!(neighbor_colors[1] == candidate && neighbor_colors[2] == candidate)) {
403 20 : continue;
404 : }
405 : } else {
406 8587 : if (!(neighbor_colors[1] == candidate && neighbor_colors[2] == candidate &&
407 8280 : neighbor_colors[3] == candidate)) {
408 354 : continue;
409 : }
410 : }
411 :
412 8829 : if (candidate == base_color) {
413 8828 : continue;
414 : }
415 :
416 1 : if (neighbor_has_base) {
417 0 : continue;
418 : }
419 :
420 1 : if (ctx->palette && ctx->palette_len > 0 && (size_t)base_color < ctx->palette_len &&
421 1 : (size_t)candidate < ctx->palette_len) {
422 1 : dist_sq = color_distance_sq(&ctx->palette[base_color], &ctx->palette[candidate]);
423 1 : if (dist_sq > PNGX_POSTPROCESS_MAX_COLOR_DISTANCE_SQ) {
424 0 : continue;
425 : }
426 : }
427 :
428 1 : ctx->indices[idx] = candidate;
429 : }
430 : }
431 : }
432 :
433 27 : static inline void postprocess_indices(uint32_t thread_count, uint8_t *indices, uint32_t width,
434 : uint32_t height, const cpres_rgba_color_t *palette,
435 : size_t palette_len, const pngx_quant_support_t *support,
436 : const pngx_options_t *opts)
437 : {
438 : postprocess_indices_parallel_ctx_t ctx;
439 : uint8_t *reference;
440 : size_t pixel_count;
441 : float cutoff;
442 :
443 27 : if (!indices || !support || !opts || width == 0 || height == 0) {
444 23 : return;
445 : }
446 :
447 27 : if (!opts->postprocess_smooth_enable) {
448 2 : return;
449 : }
450 :
451 25 : if (opts->lossy_dither_level >= PNGX_POSTPROCESS_DISABLE_DITHER_THRESHOLD) {
452 21 : return;
453 : }
454 :
455 4 : if (!support->importance_map || support->importance_map_len < (size_t)width * (size_t)height) {
456 0 : return;
457 : }
458 :
459 4 : cutoff = opts->postprocess_smooth_importance_cutoff;
460 4 : if (cutoff >= 0.0f) {
461 3 : if (cutoff > 1.0f) {
462 0 : cutoff = 1.0f;
463 : }
464 : } else {
465 1 : cutoff = -1.0f;
466 : }
467 :
468 4 : pixel_count = (size_t)width * (size_t)height;
469 :
470 4 : reference = (uint8_t *)malloc(pixel_count);
471 4 : if (!reference) {
472 0 : return;
473 : }
474 :
475 4 : memcpy(reference, indices, pixel_count);
476 :
477 4 : ctx.indices = indices;
478 4 : ctx.reference = reference;
479 4 : ctx.width = width;
480 4 : ctx.height = height;
481 4 : ctx.palette = palette;
482 4 : ctx.palette_len = palette_len;
483 4 : ctx.importance_map = support->importance_map;
484 4 : ctx.cutoff = cutoff;
485 :
486 : #if COLOPRESSO_ENABLE_THREADS
487 4 : colopresso_parallel_for(thread_count, height, postprocess_indices_parallel_worker, &ctx);
488 : #else
489 : postprocess_indices_parallel_worker(&ctx, 0, height);
490 : #endif
491 :
492 4 : free(reference);
493 : }
494 :
495 30 : static inline void fill_quant_params(pngx_bridge_quant_params_t *params, const pngx_options_t *opts,
496 : const uint8_t *importance_map, size_t importance_map_len)
497 : {
498 : uint8_t quality_min, quality_max;
499 :
500 30 : if (!params || !opts) {
501 0 : return;
502 : }
503 :
504 30 : quality_min = clamp_uint8_t(opts->lossy_quality_min, 0, 100);
505 30 : quality_max = clamp_uint8_t(opts->lossy_quality_max, quality_min, 100);
506 30 : params->speed = clamp_int32_t((int32_t)opts->lossy_speed, 1, 10);
507 30 : params->quality_min = quality_min;
508 30 : params->quality_max = quality_max;
509 30 : params->max_colors = clamp_uint32_t((uint32_t)opts->lossy_max_colors, 2, 256);
510 30 : params->min_posterization = -1;
511 30 : params->dithering_level = clamp_float(opts->lossy_dither_level, 0.0f, 1.0f);
512 30 : params->importance_map = importance_map;
513 30 : params->importance_map_len = importance_map_len;
514 30 : params->fixed_colors = opts->protected_colors;
515 30 : params->fixed_colors_len =
516 30 : (size_t)((opts->protected_colors_count > 0) ? opts->protected_colors_count : 0);
517 30 : params->remap = true;
518 : }
519 :
520 28 : static inline void free_quant_output(pngx_bridge_quant_output_t *output)
521 : {
522 28 : if (!output) {
523 0 : return;
524 : }
525 :
526 28 : if (output->palette) {
527 27 : pngx_bridge_free((uint8_t *)output->palette);
528 27 : output->palette = NULL;
529 : }
530 :
531 28 : output->palette_len = 0;
532 :
533 28 : if (output->indices) {
534 27 : pngx_bridge_free(output->indices);
535 27 : output->indices = NULL;
536 : }
537 :
538 28 : output->indices_len = 0;
539 : }
540 :
541 44 : static inline bool init_write_struct(png_structp *png_ptr, png_infop *info_ptr)
542 : {
543 : png_structp tmp_png;
544 : png_infop tmp_info;
545 :
546 44 : if (!png_ptr || !info_ptr) {
547 0 : return false;
548 : }
549 :
550 44 : tmp_png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
551 44 : if (!tmp_png) {
552 0 : return false;
553 : }
554 :
555 44 : tmp_info = png_create_info_struct(tmp_png);
556 44 : if (!tmp_info) {
557 0 : png_destroy_write_struct(&tmp_png, NULL);
558 :
559 0 : return false;
560 : }
561 :
562 44 : *png_ptr = tmp_png;
563 44 : *info_ptr = tmp_info;
564 :
565 44 : return true;
566 : }
567 :
568 4023 : static inline bool memory_buffer_reserve(png_memory_buffer_t *buffer, size_t additional)
569 : {
570 : uint8_t *new_data;
571 : size_t required, capacity;
572 :
573 4023 : if (!buffer || additional > SIZE_MAX - buffer->size) {
574 0 : return false;
575 : }
576 :
577 4023 : required = buffer->size + additional;
578 4023 : capacity = buffer->capacity ? buffer->capacity : 4096;
579 :
580 4114 : while (capacity < required) {
581 91 : if (capacity > SIZE_MAX / 2) {
582 0 : capacity = required;
583 :
584 0 : break;
585 : }
586 :
587 91 : capacity *= 2;
588 : }
589 :
590 4023 : new_data = (uint8_t *)realloc(buffer->data, capacity);
591 4023 : if (!new_data) {
592 0 : return false;
593 : }
594 :
595 4023 : buffer->data = new_data;
596 4023 : buffer->capacity = capacity;
597 :
598 4023 : return true;
599 : }
600 :
601 : /* Excluded from coverage as this is only called in rare cases such as malloc failure. */
602 : /* LCOV_EXCL_START */
603 : static inline void memory_buffer_reset(png_memory_buffer_t *buffer)
604 : {
605 : if (!buffer) {
606 : return;
607 : }
608 :
609 : free(buffer->data);
610 : buffer->data = NULL;
611 : buffer->size = 0;
612 : buffer->capacity = 0;
613 : }
614 : /* LCOV_EXCL_STOP */
615 :
616 4023 : static inline void memory_write(png_structp png_ptr, png_bytep data, png_size_t length)
617 : {
618 4023 : png_memory_buffer_t *buffer = (png_memory_buffer_t *)png_get_io_ptr(png_ptr);
619 :
620 4023 : if (!buffer || !memory_buffer_reserve(buffer, length)) {
621 0 : png_error(png_ptr, "png write failure");
622 :
623 : return;
624 : }
625 :
626 4023 : memcpy(buffer->data + buffer->size, data, length);
627 :
628 4023 : buffer->size += length;
629 : }
630 :
631 44 : static inline bool finalize_memory_png(png_memory_buffer_t *buffer, uint8_t **out_data,
632 : size_t *out_size)
633 : {
634 : uint8_t *shrunk;
635 :
636 44 : if (!buffer || !out_data || !out_size) {
637 0 : return false;
638 : }
639 :
640 44 : if (buffer->data) {
641 44 : shrunk = (uint8_t *)realloc(buffer->data, buffer->size);
642 44 : if (shrunk) {
643 44 : buffer->data = shrunk;
644 44 : buffer->capacity = buffer->size;
645 : }
646 : }
647 :
648 44 : if (!buffer->data || buffer->size == 0) {
649 0 : memory_buffer_reset(buffer);
650 0 : *out_data = NULL;
651 0 : *out_size = 0;
652 :
653 0 : return false;
654 : }
655 :
656 44 : *out_data = buffer->data;
657 44 : *out_size = buffer->size;
658 :
659 44 : return true;
660 : }
661 :
662 44 : static inline png_bytep *build_row_pointers(const uint8_t *data, size_t row_stride, uint32_t height)
663 : {
664 : png_bytep *row_pointers;
665 : uint32_t y;
666 :
667 44 : if (!data || row_stride == 0 || height == 0 || (size_t)height > SIZE_MAX / sizeof(png_bytep)) {
668 0 : return NULL;
669 : }
670 :
671 44 : row_pointers = (png_bytep *)malloc(sizeof(png_bytep) * (size_t)height);
672 44 : if (!row_pointers) {
673 0 : return NULL;
674 : }
675 :
676 12499 : for (y = 0; y < height; ++y) {
677 12455 : row_pointers[y] = (png_bytep)(data + (size_t)y * row_stride);
678 : }
679 :
680 44 : return row_pointers;
681 : }
682 :
683 44 : static bool write_png_guarded(png_structp png_ptr, png_infop info_ptr, const png_write_job_t *job,
684 : png_bytep *row_pointers, png_memory_buffer_t *buffer)
685 : {
686 44 : if (setjmp(png_jmpbuf(png_ptr)) != 0) {
687 0 : return false;
688 : }
689 :
690 44 : png_set_write_fn(png_ptr, buffer, memory_write, NULL);
691 44 : png_set_compression_level(png_ptr, Z_BEST_COMPRESSION);
692 44 : png_set_compression_strategy(png_ptr, Z_FILTERED);
693 44 : png_set_filter(png_ptr, PNG_FILTER_TYPE_BASE, PNG_ALL_FILTERS);
694 44 : png_set_IHDR(png_ptr, info_ptr, job->width, job->height, 8, job->color_type, PNG_INTERLACE_NONE,
695 : PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
696 :
697 44 : if (job->palette) {
698 27 : png_set_PLTE(png_ptr, info_ptr, job->palette, job->palette_len);
699 :
700 27 : if (job->num_trans > 0) {
701 22 : png_set_tRNS(png_ptr, info_ptr, job->alpha, job->num_trans, NULL);
702 : }
703 : }
704 :
705 44 : png_write_info(png_ptr, info_ptr);
706 44 : png_write_image(png_ptr, row_pointers);
707 44 : png_write_end(png_ptr, NULL);
708 :
709 44 : return true;
710 : }
711 :
712 44 : static bool write_png_to_memory(const png_write_job_t *job, uint8_t **out_data, size_t *out_size)
713 : {
714 44 : png_memory_buffer_t buffer = {0};
715 : png_structp png_ptr;
716 : png_infop info_ptr;
717 : png_bytep *row_pointers;
718 : bool written;
719 :
720 44 : row_pointers = build_row_pointers(job->pixels, job->row_stride, job->height);
721 44 : if (!row_pointers) {
722 0 : return false;
723 : }
724 :
725 44 : if (!init_write_struct(&png_ptr, &info_ptr)) {
726 0 : free(row_pointers);
727 :
728 0 : return false;
729 : }
730 :
731 44 : written = write_png_guarded(png_ptr, info_ptr, job, row_pointers, &buffer);
732 44 : png_destroy_write_struct(&png_ptr, &info_ptr);
733 44 : free(row_pointers);
734 :
735 44 : if (!written) {
736 0 : memory_buffer_reset(&buffer);
737 :
738 0 : return false;
739 : }
740 :
741 44 : return finalize_memory_png(&buffer, out_data, out_size);
742 : }
743 :
744 30 : static void palette256_context_reset(palette256_context_t *ctx)
745 : {
746 30 : rgba_image_reset(&ctx->image);
747 30 : quant_support_reset(&ctx->support);
748 30 : memset(ctx, 0, sizeof(*ctx));
749 30 : }
750 :
751 31 : static bool palette256_prepare_context(palette256_context_t *ctx, const uint8_t *png_data,
752 : size_t png_size, const pngx_options_t *opts,
753 : uint8_t **out_rgba, uint32_t *out_width,
754 : uint32_t *out_height, uint8_t **out_importance_map,
755 : size_t *out_importance_map_len, int32_t *out_speed,
756 : uint8_t *out_quality_min, uint8_t *out_quality_max,
757 : uint32_t *out_max_colors, float *out_dither_level,
758 : uint8_t **out_fixed_colors, size_t *out_fixed_colors_len)
759 : {
760 31 : pngx_bridge_quant_params_t params = {0};
761 : bool use_importance_map;
762 : float estimated_dither, gradient_dither_floor;
763 :
764 31 : if (!png_data || png_size == 0 || !opts || !out_rgba || !out_width || !out_height) {
765 0 : return false;
766 : }
767 :
768 31 : if (ctx->initialized) {
769 0 : palette256_context_reset(ctx);
770 : }
771 :
772 31 : if (!load_rgba_image(png_data, png_size, &ctx->image)) {
773 1 : return false;
774 : }
775 :
776 30 : alpha_bleed_rgb_from_opaque(ctx->image.rgba, ctx->image.width, ctx->image.height, opts);
777 :
778 30 : image_stats_reset(&ctx->stats);
779 30 : if (!prepare_quant_support(&ctx->image, opts, &ctx->support, &ctx->stats)) {
780 0 : rgba_image_reset(&ctx->image);
781 0 : quant_support_reset(&ctx->support);
782 :
783 0 : return false;
784 : }
785 :
786 30 : ctx->tuned_opts = *opts;
787 60 : ctx->prefer_uniform = opts->palette256_gradient_profile_enable
788 25 : ? is_smooth_gradient_profile(&ctx->stats, &ctx->tuned_opts)
789 55 : : false;
790 :
791 30 : if (ctx->prefer_uniform) {
792 2 : ctx->tuned_opts.saliency_map_enable = false;
793 2 : ctx->tuned_opts.chroma_anchor_enable = false;
794 2 : ctx->tuned_opts.postprocess_smooth_enable = false;
795 : } else {
796 28 : build_fixed_palette(opts, &ctx->support, &ctx->tuned_opts);
797 : }
798 :
799 30 : ctx->resolved_dither = resolve_quant_dither(opts, &ctx->stats);
800 :
801 30 : if (opts->lossy_dither_auto) {
802 : estimated_dither =
803 0 : estimate_bitdepth_dither_level(ctx->image.rgba, ctx->image.width, ctx->image.height, 8);
804 0 : if (estimated_dither > ctx->resolved_dither) {
805 0 : ctx->resolved_dither = estimated_dither;
806 : }
807 : }
808 :
809 30 : gradient_dither_floor = ctx->tuned_opts.palette256_gradient_profile_dither_floor;
810 30 : if (gradient_dither_floor < 0.0f) {
811 0 : gradient_dither_floor = PNGX_PALETTE256_GRADIENT_PROFILE_DITHER_FLOOR;
812 : }
813 :
814 30 : if (ctx->prefer_uniform && ctx->resolved_dither < gradient_dither_floor) {
815 2 : ctx->resolved_dither = gradient_dither_floor;
816 : }
817 :
818 30 : ctx->tuned_opts.lossy_dither_level = ctx->resolved_dither;
819 :
820 56 : use_importance_map = !ctx->prefer_uniform && ctx->tuned_opts.saliency_map_enable &&
821 26 : ctx->support.importance_map != NULL;
822 30 : fill_quant_params(¶ms, &ctx->tuned_opts,
823 : use_importance_map ? ctx->support.importance_map : NULL,
824 : use_importance_map ? ctx->support.importance_map_len : 0);
825 30 : params.dithering_level = ctx->resolved_dither;
826 30 : tune_quant_params_for_image(¶ms, &ctx->tuned_opts, &ctx->stats);
827 :
828 30 : *out_rgba = ctx->image.rgba;
829 30 : *out_width = ctx->image.width;
830 30 : *out_height = ctx->image.height;
831 :
832 30 : if (out_importance_map && out_importance_map_len) {
833 30 : if (use_importance_map) {
834 26 : *out_importance_map = ctx->support.importance_map;
835 26 : *out_importance_map_len = ctx->support.importance_map_len;
836 : } else {
837 4 : *out_importance_map = NULL;
838 4 : *out_importance_map_len = 0;
839 : }
840 : }
841 :
842 30 : if (out_speed) {
843 30 : *out_speed = params.speed;
844 : }
845 :
846 30 : if (out_quality_min) {
847 30 : *out_quality_min = params.quality_min;
848 : }
849 :
850 30 : if (out_quality_max) {
851 30 : *out_quality_max = params.quality_max;
852 : }
853 :
854 30 : if (out_max_colors) {
855 30 : *out_max_colors = params.max_colors;
856 : }
857 :
858 30 : if (out_dither_level) {
859 30 : *out_dither_level = params.dithering_level;
860 : }
861 :
862 30 : if (out_fixed_colors && out_fixed_colors_len) {
863 30 : if (params.fixed_colors && params.fixed_colors_len > 0) {
864 25 : *out_fixed_colors = (uint8_t *)params.fixed_colors;
865 25 : *out_fixed_colors_len = params.fixed_colors_len;
866 : } else {
867 5 : *out_fixed_colors = NULL;
868 5 : *out_fixed_colors_len = 0;
869 : }
870 : }
871 :
872 30 : ctx->initialized = true;
873 :
874 30 : return true;
875 : }
876 :
877 27 : static bool palette256_finalize_context(palette256_context_t *ctx, const uint8_t *indices,
878 : size_t indices_len, const cpres_rgba_color_t *palette,
879 : size_t palette_len, uint8_t **out_data, size_t *out_size)
880 : {
881 : cpres_rgba_color_t *mutable_palette;
882 : uint8_t *mutable_indices;
883 : bool success;
884 :
885 27 : if (!ctx->initialized) {
886 0 : return false;
887 : }
888 :
889 27 : if (!indices || indices_len == 0 || !palette || palette_len == 0 || palette_len > 256 ||
890 27 : !out_data || !out_size) {
891 0 : palette256_context_reset(ctx);
892 :
893 0 : return false;
894 : }
895 :
896 27 : if (indices_len != ctx->image.pixel_count) {
897 0 : palette256_context_reset(ctx);
898 :
899 0 : return false;
900 : }
901 :
902 27 : mutable_palette = (cpres_rgba_color_t *)malloc(sizeof(cpres_rgba_color_t) * palette_len);
903 27 : if (!mutable_palette) {
904 0 : palette256_context_reset(ctx);
905 :
906 0 : return false;
907 : }
908 :
909 27 : memcpy(mutable_palette, palette, sizeof(cpres_rgba_color_t) * palette_len);
910 27 : sanitize_transparent_palette(mutable_palette, palette_len);
911 :
912 27 : mutable_indices = (uint8_t *)malloc(indices_len);
913 27 : if (!mutable_indices) {
914 0 : free(mutable_palette);
915 0 : palette256_context_reset(ctx);
916 :
917 0 : return false;
918 : }
919 :
920 27 : memcpy(mutable_indices, indices, indices_len);
921 :
922 27 : postprocess_indices(ctx->tuned_opts.thread_count, mutable_indices, ctx->image.width,
923 27 : ctx->image.height, mutable_palette, palette_len, &ctx->support,
924 27 : &ctx->tuned_opts);
925 :
926 27 : success = pngx_create_palette_png(mutable_indices, indices_len, mutable_palette, palette_len,
927 27 : ctx->image.width, ctx->image.height, out_data, out_size);
928 :
929 27 : free(mutable_indices);
930 27 : free(mutable_palette);
931 :
932 27 : palette256_context_reset(ctx);
933 :
934 27 : return success;
935 : }
936 :
937 27 : extern bool pngx_create_palette_png(const uint8_t *indices, size_t indices_len,
938 : const cpres_rgba_color_t *palette, size_t palette_len,
939 : uint32_t width, uint32_t height, uint8_t **out_data,
940 : size_t *out_size)
941 : {
942 27 : png_write_job_t job = {0};
943 : png_color palette_data[256];
944 : png_byte alpha_data[256];
945 : size_t i;
946 :
947 27 : if (!indices || !palette || !out_data || !out_size || width == 0 || height == 0) {
948 0 : return false;
949 : }
950 :
951 27 : if ((size_t)width > SIZE_MAX / (size_t)height ||
952 27 : indices_len != (size_t)width * (size_t)height || palette_len == 0 || palette_len > 256) {
953 0 : return false;
954 : }
955 :
956 1932 : for (i = 0; i < palette_len; ++i) {
957 1905 : palette_data[i].red = palette[i].r;
958 1905 : palette_data[i].green = palette[i].g;
959 1905 : palette_data[i].blue = palette[i].b;
960 1905 : alpha_data[i] = palette[i].a;
961 1905 : if (palette[i].a != 255) {
962 1516 : job.num_trans = (int)(i + 1);
963 : }
964 : }
965 :
966 27 : job.pixels = indices;
967 27 : job.row_stride = (size_t)width;
968 27 : job.width = width;
969 27 : job.height = height;
970 27 : job.color_type = PNG_COLOR_TYPE_PALETTE;
971 27 : job.palette = palette_data;
972 27 : job.alpha = alpha_data;
973 27 : job.palette_len = (int)palette_len;
974 :
975 27 : return write_png_to_memory(&job, out_data, out_size);
976 : }
977 :
978 17 : extern bool create_rgba_png(const uint8_t *rgba, size_t pixel_count, uint32_t width,
979 : uint32_t height, uint8_t **out_data, size_t *out_size)
980 : {
981 17 : png_write_job_t job = {0};
982 :
983 17 : if (!rgba || !out_data || !out_size || width == 0 || height == 0) {
984 0 : return false;
985 : }
986 :
987 17 : if ((size_t)width > SIZE_MAX / (size_t)height / PNGX_RGBA_CHANNELS ||
988 17 : pixel_count != (size_t)width * (size_t)height) {
989 0 : return false;
990 : }
991 :
992 17 : job.pixels = rgba;
993 17 : job.row_stride = (size_t)width * PNGX_RGBA_CHANNELS;
994 17 : job.width = width;
995 17 : job.height = height;
996 17 : job.color_type = PNG_COLOR_TYPE_RGB_ALPHA;
997 :
998 17 : return write_png_to_memory(&job, out_data, out_size);
999 : }
1000 :
1001 29 : extern bool pngx_quantize_palette256(const uint8_t *png_data, size_t png_size,
1002 : const pngx_options_t *opts, uint8_t **out_data,
1003 : size_t *out_size, int *quant_quality)
1004 : {
1005 29 : palette256_context_t ctx = {0};
1006 29 : pngx_bridge_quant_params_t params = {0}, fallback_params = {0};
1007 29 : pngx_bridge_quant_output_t output = {0};
1008 : pngx_bridge_quant_status_t status;
1009 : uint32_t width, height, max_colors;
1010 : uint8_t *rgba, *importance_map, *fixed_colors, quality_min, quality_max;
1011 : int32_t speed;
1012 : size_t pixel_count, importance_map_len, fixed_colors_len;
1013 : bool relaxed_quality, success;
1014 : float dither_level;
1015 :
1016 29 : if (!png_data || png_size == 0 || !opts || !out_data || !out_size) {
1017 0 : return false;
1018 : }
1019 :
1020 29 : if (quant_quality) {
1021 29 : *quant_quality = -1;
1022 : }
1023 :
1024 29 : if (!palette256_prepare_context(&ctx, png_data, png_size, opts, &rgba, &width, &height,
1025 : &importance_map, &importance_map_len, &speed, &quality_min,
1026 : &quality_max, &max_colors, &dither_level, &fixed_colors,
1027 : &fixed_colors_len)) {
1028 1 : return false;
1029 : }
1030 :
1031 28 : pixel_count = (size_t)width * (size_t)height;
1032 :
1033 28 : params.speed = speed;
1034 28 : params.quality_min = quality_min;
1035 28 : params.quality_max = quality_max;
1036 28 : params.max_colors = max_colors;
1037 28 : params.min_posterization = -1;
1038 28 : params.dithering_level = dither_level;
1039 28 : params.importance_map = importance_map;
1040 28 : params.importance_map_len = importance_map_len;
1041 28 : params.fixed_colors = (const cpres_rgba_color_t *)fixed_colors;
1042 28 : params.fixed_colors_len = fixed_colors_len;
1043 28 : params.remap = true;
1044 :
1045 28 : output.quality = -1;
1046 28 : relaxed_quality = false;
1047 :
1048 28 : status = pngx_bridge_quantize((const cpres_rgba_color_t *)rgba, pixel_count, width, height,
1049 : ¶ms, &output);
1050 28 : pngx_set_last_error((int)status);
1051 :
1052 28 : if (status == PNGX_BRIDGE_QUANT_STATUS_QUALITY_TOO_LOW && params.quality_min > 0) {
1053 1 : fallback_params = params;
1054 1 : fallback_params.quality_min = 0;
1055 :
1056 1 : if (fallback_params.quality_max < fallback_params.quality_min) {
1057 0 : fallback_params.quality_max = fallback_params.quality_min;
1058 : }
1059 :
1060 1 : output.palette = NULL;
1061 1 : output.palette_len = 0;
1062 1 : output.indices = NULL;
1063 1 : output.indices_len = 0;
1064 1 : output.quality = -1;
1065 :
1066 1 : status = pngx_bridge_quantize((const cpres_rgba_color_t *)rgba, pixel_count, width, height,
1067 : &fallback_params, &output);
1068 1 : pngx_set_last_error((int)status);
1069 1 : if (status == PNGX_BRIDGE_QUANT_STATUS_OK) {
1070 1 : relaxed_quality = true;
1071 : }
1072 : }
1073 :
1074 28 : if (status != PNGX_BRIDGE_QUANT_STATUS_OK) {
1075 1 : free_quant_output(&output);
1076 1 : palette256_context_reset(&ctx);
1077 :
1078 1 : if (status == PNGX_BRIDGE_QUANT_STATUS_QUALITY_TOO_LOW) {
1079 0 : colopresso_log(CPRES_LOG_LEVEL_WARNING, "PNGX: Quantization quality too low");
1080 : }
1081 :
1082 1 : return false;
1083 : }
1084 :
1085 27 : if (relaxed_quality) {
1086 1 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Relaxed quantization quality floor");
1087 : }
1088 :
1089 27 : if (quant_quality) {
1090 27 : *quant_quality = output.quality;
1091 : }
1092 :
1093 27 : success = false;
1094 :
1095 27 : if (output.indices && output.indices_len == pixel_count && output.palette &&
1096 27 : output.palette_len > 0 && output.palette_len <= 256) {
1097 27 : success =
1098 27 : palette256_finalize_context(&ctx, output.indices, output.indices_len, output.palette,
1099 : output.palette_len, out_data, out_size);
1100 : } else {
1101 0 : palette256_context_reset(&ctx);
1102 : }
1103 :
1104 27 : free_quant_output(&output);
1105 :
1106 27 : return success;
1107 : }
1108 :
1109 2 : extern bool pngx_palette256_prepare(const uint8_t *png_data, size_t png_size,
1110 : const pngx_options_t *opts, uint8_t **out_rgba,
1111 : uint32_t *out_width, uint32_t *out_height,
1112 : uint8_t **out_importance_map, size_t *out_importance_map_len,
1113 : int32_t *out_speed, uint8_t *out_quality_min,
1114 : uint8_t *out_quality_max, uint32_t *out_max_colors,
1115 : float *out_dither_level, uint8_t **out_fixed_colors,
1116 : size_t *out_fixed_colors_len)
1117 : {
1118 2 : return palette256_prepare_context(
1119 : &g_palette256_ctx, png_data, png_size, opts, out_rgba, out_width, out_height,
1120 : out_importance_map, out_importance_map_len, out_speed, out_quality_min, out_quality_max,
1121 : out_max_colors, out_dither_level, out_fixed_colors, out_fixed_colors_len);
1122 : }
1123 :
1124 0 : extern bool pngx_palette256_finalize(const uint8_t *indices, size_t indices_len,
1125 : const cpres_rgba_color_t *palette, size_t palette_len,
1126 : uint8_t **out_data, size_t *out_size)
1127 : {
1128 0 : return palette256_finalize_context(&g_palette256_ctx, indices, indices_len, palette,
1129 : palette_len, out_data, out_size);
1130 : }
1131 :
1132 2 : extern void pngx_palette256_cleanup(void)
1133 : {
1134 2 : if (g_palette256_ctx.initialized) {
1135 2 : palette256_context_reset(&g_palette256_ctx);
1136 : }
1137 2 : }
|