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 <stdbool.h>
13 : #include <stdio.h>
14 : #include <stdlib.h>
15 : #include <string.h>
16 :
17 : #include <avif/avif.h>
18 : #include <png.h>
19 : #include <webp/encode.h>
20 :
21 : #include <colopresso.h>
22 : #include <colopresso/portable.h>
23 :
24 : #include "internal/log.h"
25 : #include "internal/png.h"
26 :
27 : #include "internal/avif.h"
28 : #include "internal/pngx.h"
29 : #include "internal/webp.h"
30 :
31 150 : extern void cpres_config_init_defaults(cpres_config_t *config)
32 : {
33 150 : if (!config) {
34 1 : return;
35 : }
36 :
37 149 : memset(config, 0, sizeof(*config));
38 :
39 149 : config->webp_quality = COLOPRESSO_WEBP_DEFAULT_QUALITY;
40 149 : config->webp_lossless = COLOPRESSO_WEBP_DEFAULT_LOSSLESS;
41 149 : config->webp_method = COLOPRESSO_WEBP_DEFAULT_METHOD;
42 149 : config->webp_target_size = COLOPRESSO_WEBP_DEFAULT_TARGET_SIZE;
43 149 : config->webp_target_psnr = COLOPRESSO_WEBP_DEFAULT_TARGET_PSNR;
44 149 : config->webp_segments = COLOPRESSO_WEBP_DEFAULT_SEGMENTS;
45 149 : config->webp_sns_strength = COLOPRESSO_WEBP_DEFAULT_SNS_STRENGTH;
46 149 : config->webp_filter_strength = COLOPRESSO_WEBP_DEFAULT_FILTER_STRENGTH;
47 149 : config->webp_filter_sharpness = COLOPRESSO_WEBP_DEFAULT_FILTER_SHARPNESS;
48 149 : config->webp_filter_type = COLOPRESSO_WEBP_DEFAULT_FILTER_TYPE;
49 149 : config->webp_autofilter = COLOPRESSO_WEBP_DEFAULT_AUTOFILTER;
50 149 : config->webp_alpha_compression = COLOPRESSO_WEBP_DEFAULT_ALPHA_COMPRESSION;
51 149 : config->webp_alpha_filtering = COLOPRESSO_WEBP_DEFAULT_ALPHA_FILTERING;
52 149 : config->webp_alpha_quality = COLOPRESSO_WEBP_DEFAULT_ALPHA_QUALITY;
53 149 : config->webp_pass = COLOPRESSO_WEBP_DEFAULT_PASS;
54 149 : config->webp_preprocessing = COLOPRESSO_WEBP_DEFAULT_PREPROCESSING;
55 149 : config->webp_partitions = COLOPRESSO_WEBP_DEFAULT_PARTITIONS;
56 149 : config->webp_partition_limit = COLOPRESSO_WEBP_DEFAULT_PARTITION_LIMIT;
57 149 : config->webp_emulate_jpeg_size = COLOPRESSO_WEBP_DEFAULT_EMULATE_JPEG_SIZE;
58 149 : config->webp_thread_level = COLOPRESSO_WEBP_DEFAULT_THREAD_LEVEL;
59 149 : config->webp_low_memory = COLOPRESSO_WEBP_DEFAULT_LOW_MEMORY;
60 149 : config->webp_near_lossless = COLOPRESSO_WEBP_DEFAULT_NEAR_LOSSLESS;
61 149 : config->webp_exact = COLOPRESSO_WEBP_DEFAULT_EXACT;
62 149 : config->webp_use_delta_palette = COLOPRESSO_WEBP_DEFAULT_USE_DELTA_PALETTE;
63 149 : config->webp_use_sharp_yuv = COLOPRESSO_WEBP_DEFAULT_USE_SHARP_YUV;
64 :
65 149 : config->avif_quality = COLOPRESSO_AVIF_DEFAULT_QUALITY;
66 149 : config->avif_alpha_quality = COLOPRESSO_AVIF_DEFAULT_ALPHA_QUALITY;
67 149 : config->avif_lossless = COLOPRESSO_AVIF_DEFAULT_LOSSLESS;
68 149 : config->avif_speed = COLOPRESSO_AVIF_DEFAULT_SPEED;
69 149 : config->avif_threads = COLOPRESSO_AVIF_DEFAULT_THREADS;
70 :
71 149 : config->pngx_level = COLOPRESSO_PNGX_DEFAULT_LEVEL;
72 149 : config->pngx_strip_safe = COLOPRESSO_PNGX_DEFAULT_STRIP_SAFE;
73 149 : config->pngx_optimize_alpha = COLOPRESSO_PNGX_DEFAULT_OPTIMIZE_ALPHA;
74 149 : config->pngx_lossy_enable = COLOPRESSO_PNGX_DEFAULT_LOSSY_ENABLE;
75 149 : config->pngx_lossy_type = COLOPRESSO_PNGX_DEFAULT_LOSSY_TYPE;
76 149 : config->pngx_lossy_max_colors = COLOPRESSO_PNGX_DEFAULT_LOSSY_MAX_COLORS;
77 149 : config->pngx_lossy_reduced_colors = COLOPRESSO_PNGX_DEFAULT_REDUCED_COLORS;
78 149 : config->pngx_lossy_reduced_bits_rgb = COLOPRESSO_PNGX_DEFAULT_REDUCED_BITS_RGB;
79 149 : config->pngx_lossy_reduced_alpha_bits = COLOPRESSO_PNGX_DEFAULT_REDUCED_ALPHA_BITS;
80 149 : config->pngx_lossy_quality_min = COLOPRESSO_PNGX_DEFAULT_LOSSY_QUALITY_MIN;
81 149 : config->pngx_lossy_quality_max = COLOPRESSO_PNGX_DEFAULT_LOSSY_QUALITY_MAX;
82 149 : config->pngx_lossy_speed = COLOPRESSO_PNGX_DEFAULT_LOSSY_SPEED;
83 149 : config->pngx_lossy_dither_level = COLOPRESSO_PNGX_DEFAULT_LOSSY_DITHER_LEVEL;
84 149 : config->pngx_saliency_map_enable = COLOPRESSO_PNGX_DEFAULT_SALIENCY_MAP_ENABLE;
85 149 : config->pngx_chroma_anchor_enable = COLOPRESSO_PNGX_DEFAULT_CHROMA_ANCHOR_ENABLE;
86 149 : config->pngx_adaptive_dither_enable = COLOPRESSO_PNGX_DEFAULT_ADAPTIVE_DITHER_ENABLE;
87 149 : config->pngx_gradient_boost_enable = COLOPRESSO_PNGX_DEFAULT_GRADIENT_BOOST_ENABLE;
88 149 : config->pngx_chroma_weight_enable = COLOPRESSO_PNGX_DEFAULT_CHROMA_WEIGHT_ENABLE;
89 149 : config->pngx_postprocess_smooth_enable = COLOPRESSO_PNGX_DEFAULT_POSTPROCESS_SMOOTH_ENABLE;
90 149 : config->pngx_postprocess_smooth_importance_cutoff =
91 : COLOPRESSO_PNGX_DEFAULT_POSTPROCESS_SMOOTH_IMPORTANCE_CUTOFF;
92 149 : config->pngx_palette256_gradient_profile_enable =
93 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_GRADIENT_PROFILE_ENABLE;
94 149 : config->pngx_palette256_gradient_dither_floor =
95 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_GRADIENT_DITHER_FLOOR;
96 149 : config->pngx_palette256_alpha_bleed_enable =
97 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_ENABLE;
98 149 : config->pngx_palette256_alpha_bleed_max_distance =
99 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_MAX_DISTANCE;
100 149 : config->pngx_palette256_alpha_bleed_opaque_threshold =
101 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_OPAQUE_THRESHOLD;
102 149 : config->pngx_palette256_alpha_bleed_soft_limit =
103 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_ALPHA_BLEED_SOFT_LIMIT;
104 149 : config->pngx_palette256_profile_opaque_ratio_threshold =
105 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_PROFILE_OPAQUE_RATIO_THRESHOLD;
106 149 : config->pngx_palette256_profile_gradient_mean_max =
107 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_PROFILE_GRADIENT_MEAN_MAX;
108 149 : config->pngx_palette256_profile_saturation_mean_max =
109 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_PROFILE_SATURATION_MEAN_MAX;
110 149 : config->pngx_palette256_tune_opaque_ratio_threshold =
111 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_OPAQUE_RATIO_THRESHOLD;
112 149 : config->pngx_palette256_tune_gradient_mean_max =
113 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_GRADIENT_MEAN_MAX;
114 149 : config->pngx_palette256_tune_saturation_mean_max =
115 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_SATURATION_MEAN_MAX;
116 149 : config->pngx_palette256_tune_speed_max = COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_SPEED_MAX;
117 149 : config->pngx_palette256_tune_quality_min_floor =
118 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_QUALITY_MIN_FLOOR;
119 149 : config->pngx_palette256_tune_quality_max_target =
120 : COLOPRESSO_PNGX_DEFAULT_PALETTE256_TUNE_QUALITY_MAX_TARGET;
121 149 : config->pngx_threads = COLOPRESSO_PNGX_DEFAULT_THREADS;
122 : }
123 :
124 40 : extern cpres_error_t cpres_encode_webp_memory(const uint8_t *png_data, size_t png_size,
125 : uint8_t **webp_data, size_t *webp_size,
126 : const cpres_config_t *config)
127 : {
128 : cpres_error_t error;
129 : uint32_t width, height;
130 : uint8_t *rgba_data;
131 : size_t encoded_size;
132 :
133 40 : if (!png_data || !webp_data || !webp_size || !config) {
134 8 : return CPRES_ERROR_INVALID_PARAMETER;
135 : }
136 :
137 32 : if (png_size == 0) {
138 2 : return CPRES_ERROR_INVALID_PARAMETER;
139 : }
140 :
141 30 : if (png_size > COLOPRESSO_PNG_MAX_MEMORY_INPUT_SIZE) {
142 2 : return CPRES_ERROR_INVALID_PARAMETER;
143 : }
144 :
145 28 : *webp_data = NULL;
146 28 : *webp_size = 0;
147 28 : encoded_size = 0;
148 :
149 28 : rgba_data = NULL;
150 28 : error = png_decode_from_memory(png_data, png_size, &rgba_data, &width, &height);
151 28 : if (error != CPRES_OK) {
152 4 : colopresso_log(CPRES_LOG_LEVEL_ERROR, "PNG decode from memory failed: %s",
153 : cpres_error_string(error));
154 :
155 4 : return error;
156 : }
157 :
158 24 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNG decoded from memory - %dx%d pixels", width, height);
159 :
160 24 : error = webp_encode_rgba_to_memory(rgba_data, width, height, webp_data, &encoded_size, config);
161 24 : if (error == CPRES_OK) {
162 22 : if (*webp_data && encoded_size >= png_size) {
163 1 : colopresso_log(CPRES_LOG_LEVEL_WARNING,
164 : "WebP: Encoded output larger than input (%zu > %zu)", encoded_size,
165 : png_size);
166 1 : cpres_free(*webp_data);
167 1 : *webp_data = NULL;
168 1 : error = CPRES_ERROR_OUTPUT_NOT_SMALLER;
169 : }
170 :
171 22 : *webp_size = encoded_size;
172 : }
173 :
174 24 : free(rgba_data);
175 :
176 24 : return error;
177 : }
178 :
179 20 : extern cpres_error_t cpres_encode_avif_memory(const uint8_t *png_data, size_t png_size,
180 : uint8_t **avif_data, size_t *avif_size,
181 : const cpres_config_t *config)
182 : {
183 : cpres_error_t error;
184 : uint32_t width, height;
185 : uint8_t *rgba_data;
186 : size_t encoded_size;
187 :
188 20 : if (!png_data || !avif_data || !avif_size || !config) {
189 4 : return CPRES_ERROR_INVALID_PARAMETER;
190 : }
191 :
192 16 : if (png_size == 0) {
193 1 : return CPRES_ERROR_INVALID_PARAMETER;
194 : }
195 :
196 15 : if (png_size > COLOPRESSO_PNG_MAX_MEMORY_INPUT_SIZE) {
197 1 : return CPRES_ERROR_INVALID_PARAMETER;
198 : }
199 :
200 14 : *avif_data = NULL;
201 14 : *avif_size = 0;
202 14 : encoded_size = 0;
203 :
204 14 : rgba_data = NULL;
205 14 : error = png_decode_from_memory(png_data, png_size, &rgba_data, &width, &height);
206 14 : if (error != CPRES_OK) {
207 1 : colopresso_log(CPRES_LOG_LEVEL_ERROR, "PNG decode (AVIF) from memory failed: %s",
208 : cpres_error_string(error));
209 :
210 1 : return error;
211 : }
212 :
213 13 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNG decoded (AVIF) from memory - %dx%d pixels", width,
214 : height);
215 :
216 13 : error = avif_encode_rgba_to_memory(rgba_data, width, height, avif_data, &encoded_size, config);
217 13 : if (error == CPRES_OK) {
218 12 : if (*avif_data && encoded_size >= png_size) {
219 1 : colopresso_log(CPRES_LOG_LEVEL_WARNING,
220 : "AVIF: Encoded output larger than input (%zu > %zu)", encoded_size,
221 : png_size);
222 1 : cpres_free(*avif_data);
223 1 : *avif_data = NULL;
224 1 : error = CPRES_ERROR_OUTPUT_NOT_SMALLER;
225 : }
226 :
227 12 : *avif_size = encoded_size;
228 : }
229 :
230 13 : free(rgba_data);
231 :
232 13 : return error;
233 : }
234 :
235 41 : extern cpres_error_t cpres_encode_pngx_memory(const uint8_t *png_data, size_t png_size,
236 : uint8_t **optimized_data, size_t *optimized_size,
237 : const cpres_config_t *config)
238 : {
239 : pngx_options_t opts;
240 : uint8_t *lossless_data, *quant_data, *quant_optimized, *final_data;
241 : int quant_quality, threads;
242 : size_t lossless_size, quant_size, quant_optimized_size, final_size, candidate_size;
243 : bool lossless_ok, quant_ok, quant_lossless_ok, quant_is_rgba_lossy, final_is_quantized;
244 :
245 41 : if (!png_data || png_size == 0 || !optimized_data || !optimized_size || !config) {
246 5 : return CPRES_ERROR_INVALID_PARAMETER;
247 : }
248 :
249 36 : if (png_size > COLOPRESSO_PNG_MAX_MEMORY_INPUT_SIZE) {
250 0 : return CPRES_ERROR_INVALID_PARAMETER;
251 : }
252 :
253 36 : *optimized_data = NULL;
254 36 : *optimized_size = 0;
255 :
256 36 : lossless_data = NULL;
257 36 : lossless_size = 0;
258 36 : quant_data = NULL;
259 36 : quant_size = 0;
260 36 : quant_optimized = NULL;
261 36 : quant_optimized_size = 0;
262 36 : final_data = NULL;
263 36 : final_size = 0;
264 36 : candidate_size = 0;
265 36 : lossless_ok = false;
266 36 : quant_ok = false;
267 36 : quant_lossless_ok = false;
268 36 : quant_is_rgba_lossy = false;
269 36 : final_is_quantized = false;
270 36 : quant_quality = -1;
271 36 : threads = 0;
272 :
273 36 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Starting optimization - input size: %zu bytes",
274 : png_size);
275 :
276 36 : pngx_fill_pngx_options(&opts, config);
277 :
278 36 : threads = config->pngx_threads;
279 : #if !defined(PNGX_BRIDGE_WASM_SEPARATION)
280 36 : if (threads >= 0) {
281 36 : pngx_bridge_init_threads((int)opts.thread_count);
282 : }
283 : #else
284 : (void)threads;
285 : #endif
286 :
287 70 : quant_is_rgba_lossy = (opts.lossy_type == PNGX_LOSSY_TYPE_LIMITED_RGBA4444 ||
288 34 : opts.lossy_type == PNGX_LOSSY_TYPE_REDUCED_RGBA32);
289 :
290 36 : quant_ok =
291 71 : pngx_should_attempt_quantization(&opts) &&
292 35 : pngx_run_quantization(png_data, png_size, &opts, &quant_data, &quant_size, &quant_quality);
293 36 : if (quant_ok) {
294 33 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Quantization produced %zu bytes (quality=%d)",
295 : quant_size, quant_quality);
296 : }
297 :
298 : lossless_ok =
299 36 : pngx_run_lossless_optimization(png_data, png_size, &opts, &lossless_data, &lossless_size);
300 36 : if (!lossless_ok) {
301 0 : lossless_data = (uint8_t *)malloc(png_size);
302 0 : if (!lossless_data) {
303 0 : free(quant_data);
304 :
305 0 : return CPRES_ERROR_OUT_OF_MEMORY;
306 : }
307 :
308 0 : memcpy(lossless_data, png_data, png_size);
309 0 : lossless_size = png_size;
310 : }
311 :
312 36 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Lossless optimization produced %zu bytes",
313 : lossless_size);
314 :
315 36 : final_data = lossless_data;
316 36 : final_size = lossless_size;
317 36 : final_is_quantized = false;
318 :
319 36 : if (quant_ok) {
320 33 : if (!quant_is_rgba_lossy) {
321 26 : quant_lossless_ok = pngx_run_lossless_optimization(
322 : quant_data, quant_size, &opts, &quant_optimized, &quant_optimized_size);
323 26 : if (quant_lossless_ok) {
324 26 : colopresso_log(CPRES_LOG_LEVEL_DEBUG,
325 : "PNGX: Lossless optimization on quantized data produced %zu bytes",
326 : quant_optimized_size);
327 26 : free(quant_data);
328 26 : quant_data = NULL;
329 : } else {
330 0 : quant_optimized = quant_data;
331 0 : quant_optimized_size = quant_size;
332 0 : quant_data = NULL;
333 : }
334 : } else {
335 7 : quant_optimized = quant_data;
336 7 : quant_optimized_size = quant_size;
337 7 : quant_data = NULL;
338 : }
339 : }
340 :
341 36 : if (quant_ok) {
342 33 : candidate_size = quant_optimized_size;
343 33 : if (quant_is_rgba_lossy || pngx_quantization_better(lossless_size, candidate_size)) {
344 29 : final_data = quant_optimized;
345 29 : final_size = candidate_size;
346 29 : final_is_quantized = true;
347 29 : free(lossless_data);
348 29 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Selected quantized result (%zu bytes)",
349 : final_size);
350 : } else {
351 4 : free(quant_optimized);
352 4 : quant_optimized = NULL;
353 4 : colopresso_log(CPRES_LOG_LEVEL_DEBUG, "PNGX: Selected lossless result (%zu bytes)",
354 : final_size);
355 : }
356 : }
357 :
358 36 : if (!final_data) {
359 0 : return CPRES_ERROR_ENCODE_FAILED;
360 : }
361 :
362 36 : if (final_size >= png_size) {
363 6 : if (!(quant_is_rgba_lossy && final_is_quantized)) {
364 3 : colopresso_log(CPRES_LOG_LEVEL_WARNING,
365 : "PNGX: Optimized output larger than input (%zu > %zu)", final_size,
366 : png_size);
367 3 : free(final_data);
368 3 : *optimized_data = NULL;
369 3 : *optimized_size = final_size;
370 :
371 3 : return CPRES_ERROR_OUTPUT_NOT_SMALLER;
372 : }
373 :
374 3 : colopresso_log(
375 : CPRES_LOG_LEVEL_WARNING,
376 : "PNGX: RGBA lossy output larger than input (%zu > %zu) but forcing write per RGBA mode",
377 : final_size, png_size);
378 : }
379 :
380 33 : *optimized_data = final_data;
381 33 : *optimized_size = final_size;
382 :
383 33 : return CPRES_OK;
384 : }
385 :
386 91 : extern void cpres_free(uint8_t *data)
387 : {
388 91 : if (data) {
389 87 : free(data);
390 : }
391 91 : }
392 :
393 22 : extern const char *cpres_error_string(cpres_error_t error)
394 : {
395 22 : switch (error) {
396 1 : case CPRES_OK:
397 1 : return "Success";
398 4 : case CPRES_ERROR_FILE_NOT_FOUND:
399 4 : return "File not found";
400 8 : case CPRES_ERROR_INVALID_PNG:
401 8 : return "Invalid PNG file";
402 1 : case CPRES_ERROR_INVALID_FORMAT:
403 1 : return "Invalid WebP file";
404 1 : case CPRES_ERROR_OUT_OF_MEMORY:
405 1 : return "Out of memory";
406 1 : case CPRES_ERROR_ENCODE_FAILED:
407 1 : return "Encoding failed";
408 1 : case CPRES_ERROR_DECODE_FAILED:
409 1 : return "Decoding failed";
410 1 : case CPRES_ERROR_IO:
411 1 : return "I/O error";
412 1 : case CPRES_ERROR_INVALID_PARAMETER:
413 1 : return "Invalid parameter";
414 2 : case CPRES_ERROR_OUTPUT_NOT_SMALLER:
415 2 : return "Output image would be larger than input";
416 1 : default:
417 1 : return "Unknown error";
418 : }
419 : }
420 :
421 1 : extern uint32_t cpres_get_version(void)
422 : {
423 1 : return (uint32_t)COLOPRESSO_VERSION;
424 : }
425 :
426 1 : extern uint32_t cpres_get_libwebp_version(void)
427 : {
428 1 : return (uint32_t)WebPGetEncoderVersion();
429 : }
430 :
431 1 : extern uint32_t cpres_get_libpng_version(void)
432 : {
433 1 : return (uint32_t)png_access_version_number();
434 : }
435 :
436 1 : extern uint32_t cpres_get_libavif_version(void)
437 : {
438 1 : return (uint32_t)AVIF_VERSION;
439 : }
440 :
441 1 : extern uint32_t cpres_get_pngx_oxipng_version(void)
442 : {
443 : #if !defined(PNGX_BRIDGE_WASM_SEPARATION)
444 1 : return pngx_bridge_oxipng_version();
445 : #else
446 : return 0;
447 : #endif
448 : }
449 :
450 1 : extern uint32_t cpres_get_pngx_libimagequant_version(void)
451 : {
452 : #if !defined(PNGX_BRIDGE_WASM_SEPARATION)
453 1 : return pngx_bridge_libimagequant_version();
454 : #else
455 : return 0;
456 : #endif
457 : }
458 :
459 1 : extern uint32_t cpres_get_buildtime(void)
460 : {
461 : #ifdef COLOPRESSO_BUILDTIME
462 1 : return (uint32_t)COLOPRESSO_BUILDTIME;
463 : #else
464 : return 0;
465 : #endif
466 : }
467 :
468 1 : extern const char *cpres_get_compiler_version_string(void)
469 : {
470 : #ifdef COLOPRESSO_COMPILER_VERSION_STRING
471 1 : return COLOPRESSO_COMPILER_VERSION_STRING;
472 : #else
473 : return "unknown";
474 : #endif
475 : }
476 :
477 1 : extern const char *cpres_get_rust_version_string(void)
478 : {
479 : #if !defined(PNGX_BRIDGE_WASM_SEPARATION)
480 1 : return pngx_bridge_rust_version_string();
481 : #else
482 : return "unknown";
483 : #endif
484 : }
|