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 <errno.h>
13 : #include <stdbool.h>
14 : #include <stdint.h>
15 : #include <stdio.h>
16 : #include <stdlib.h>
17 : #include <string.h>
18 :
19 : #include <sys/stat.h>
20 : #include <sys/types.h>
21 :
22 : #include <colopresso.h>
23 : #include <colopresso/portable.h>
24 :
25 : #include "internal/avif.h"
26 : #include "internal/log.h"
27 : #include "internal/png.h"
28 : #include "internal/webp.h"
29 :
30 : #if COLOPRESSO_WITH_FILE_OPS
31 :
32 16 : static inline bool cpres_get_file_size_bytes(const char *path, size_t *size_out) {
33 : struct stat st;
34 :
35 16 : if (!path || !size_out) {
36 0 : return false;
37 : }
38 :
39 16 : if (stat(path, &st) != 0 || st.st_size < 0) {
40 3 : return false;
41 : }
42 :
43 13 : *size_out = (size_t)st.st_size;
44 :
45 13 : return true;
46 : }
47 :
48 4 : cpres_error_t cpres_read_file_to_memory(const char *path, uint8_t **data_out, size_t *size_out) {
49 : FILE *fp;
50 : uint64_t file_size64;
51 : uint8_t *buffer;
52 : size_t read_size, file_size;
53 :
54 4 : if (!path || !data_out || !size_out) {
55 0 : return CPRES_ERROR_INVALID_PARAMETER;
56 : }
57 :
58 4 : *data_out = NULL;
59 4 : *size_out = 0;
60 :
61 4 : fp = fopen(path, "rb");
62 4 : if (!fp) {
63 1 : return CPRES_ERROR_FILE_NOT_FOUND;
64 : }
65 :
66 3 : if (!colopresso_fseeko(fp, 0, SEEK_END)) {
67 0 : fclose(fp);
68 0 : return CPRES_ERROR_IO;
69 : }
70 :
71 3 : if (!colopresso_ftello(fp, &file_size64)) {
72 0 : cpres_log(CPRES_LOG_LEVEL_ERROR, "ftello failed for '%s': errno=%d", path, errno);
73 0 : fclose(fp);
74 0 : return CPRES_ERROR_INVALID_PARAMETER;
75 : }
76 :
77 3 : if (file_size64 <= 0 || file_size64 > (uint64_t)SIZE_MAX) {
78 0 : fclose(fp);
79 0 : return CPRES_ERROR_INVALID_PARAMETER;
80 : }
81 :
82 3 : if (!colopresso_fseeko(fp, 0, SEEK_SET)) {
83 0 : fclose(fp);
84 0 : return CPRES_ERROR_IO;
85 : }
86 :
87 3 : file_size = (size_t)file_size64;
88 :
89 3 : buffer = (uint8_t *)malloc(file_size);
90 3 : if (!buffer) {
91 0 : fclose(fp);
92 0 : return CPRES_ERROR_OUT_OF_MEMORY;
93 : }
94 :
95 3 : read_size = fread(buffer, 1, file_size, fp);
96 3 : fclose(fp);
97 :
98 3 : if (read_size != file_size) {
99 0 : free(buffer);
100 0 : return CPRES_ERROR_IO;
101 : }
102 :
103 3 : *data_out = buffer;
104 3 : *size_out = read_size;
105 :
106 3 : return CPRES_OK;
107 : }
108 :
109 12 : extern cpres_error_t cpres_encode_webp_file(const char *input_path, const char *output_path, const cpres_config_t *config) {
110 12 : FILE *fp = NULL;
111 : uint32_t width, height;
112 12 : uint8_t *rgba_data = NULL, *webp_data = NULL;
113 12 : size_t written, input_size = 0, webp_size = 0;
114 : bool have_input_size;
115 : cpres_error_t error;
116 :
117 12 : if (!input_path || !output_path || !config) {
118 6 : return CPRES_ERROR_INVALID_PARAMETER;
119 : }
120 :
121 6 : have_input_size = cpres_get_file_size_bytes(input_path, &input_size);
122 :
123 6 : error = cpres_png_decode_from_file(input_path, &rgba_data, &width, &height);
124 6 : if (error != CPRES_OK) {
125 1 : cpres_log(CPRES_LOG_LEVEL_ERROR, "PNG read failed: %s", cpres_error_string(error));
126 1 : return error;
127 : }
128 :
129 5 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNG loaded - %dx%d pixels", width, height);
130 :
131 5 : error = cpres_webp_encode_rgba_to_memory(rgba_data, width, height, &webp_data, &webp_size, config);
132 5 : free(rgba_data);
133 :
134 5 : if (error != CPRES_OK) {
135 0 : return error;
136 : }
137 :
138 5 : if (have_input_size && webp_size >= input_size) {
139 0 : cpres_log(CPRES_LOG_LEVEL_WARNING, "WebP: Encoded output larger than input (%zu > %zu)", webp_size, input_size);
140 0 : cpres_free(webp_data);
141 0 : return CPRES_ERROR_OUTPUT_NOT_SMALLER;
142 : }
143 :
144 5 : fp = fopen(output_path, "wb");
145 5 : if (!fp) {
146 0 : cpres_log(CPRES_LOG_LEVEL_ERROR, "Failed to open output file '%s'", output_path);
147 0 : cpres_free(webp_data);
148 0 : return CPRES_ERROR_IO;
149 : }
150 :
151 5 : written = fwrite(webp_data, 1, webp_size, fp);
152 5 : fclose(fp);
153 5 : cpres_free(webp_data);
154 :
155 5 : if (written != webp_size) {
156 0 : cpres_log(CPRES_LOG_LEVEL_ERROR, "Failed to write output file '%s'", output_path);
157 0 : return CPRES_ERROR_IO;
158 : }
159 :
160 5 : return CPRES_OK;
161 : }
162 :
163 9 : extern cpres_error_t cpres_encode_avif_file(const char *input_path, const char *output_path, const cpres_config_t *config) {
164 9 : FILE *fp = NULL;
165 : uint32_t width, height;
166 9 : uint8_t *rgba_data = NULL, *avif_data = NULL;
167 9 : size_t written, avif_size = 0, input_size = 0;
168 : bool have_input_size;
169 : cpres_error_t error;
170 :
171 9 : if (!input_path || !output_path || !config) {
172 3 : return CPRES_ERROR_INVALID_PARAMETER;
173 : }
174 :
175 6 : have_input_size = cpres_get_file_size_bytes(input_path, &input_size);
176 :
177 6 : error = cpres_png_decode_from_file(input_path, &rgba_data, &width, &height);
178 6 : if (error != CPRES_OK) {
179 1 : cpres_log(CPRES_LOG_LEVEL_ERROR, "PNG read (AVIF) failed: %s", cpres_error_string(error));
180 1 : return error;
181 : }
182 :
183 5 : cpres_log(CPRES_LOG_LEVEL_DEBUG, "PNG loaded (AVIF) - %dx%d pixels", width, height);
184 :
185 5 : error = cpres_avif_encode_rgba_to_memory(rgba_data, width, height, &avif_data, &avif_size, config);
186 5 : free(rgba_data);
187 :
188 5 : if (error != CPRES_OK) {
189 0 : return error;
190 : }
191 :
192 5 : if (have_input_size && avif_size >= input_size) {
193 0 : cpres_log(CPRES_LOG_LEVEL_WARNING, "AVIF: Encoded output larger than input (%zu > %zu)", avif_size, input_size);
194 0 : cpres_free(avif_data);
195 0 : return CPRES_ERROR_OUTPUT_NOT_SMALLER;
196 : }
197 :
198 5 : fp = fopen(output_path, "wb");
199 5 : if (!fp) {
200 0 : cpres_log(CPRES_LOG_LEVEL_ERROR, "Failed to open output file '%s'", output_path);
201 0 : cpres_free(avif_data);
202 0 : return CPRES_ERROR_IO;
203 : }
204 :
205 5 : written = fwrite(avif_data, 1, avif_size, fp);
206 5 : fclose(fp);
207 5 : cpres_free(avif_data);
208 :
209 5 : if (written != avif_size) {
210 0 : cpres_log(CPRES_LOG_LEVEL_ERROR, "Failed to write output file '%s'", output_path);
211 0 : return CPRES_ERROR_IO;
212 : }
213 :
214 5 : return CPRES_OK;
215 : }
216 :
217 6 : extern cpres_error_t cpres_encode_pngx_file(const char *input_path, const char *output_path, const cpres_config_t *config) {
218 6 : FILE *fp = NULL;
219 6 : uint8_t *input_data = NULL, *optimized_data = NULL;
220 6 : size_t input_size = 0, png_size = 0, optimized_size = 0, written = 0;
221 6 : bool have_input_size = false, allow_lossy_rgba_larger_output = false;
222 : cpres_error_t err;
223 :
224 6 : if (!input_path || !output_path || !config) {
225 2 : return CPRES_ERROR_INVALID_PARAMETER;
226 : }
227 :
228 4 : have_input_size = cpres_get_file_size_bytes(input_path, &input_size);
229 4 : allow_lossy_rgba_larger_output = (config && config->pngx_lossy_enable && (config->pngx_lossy_type == CPRES_PNGX_LOSSY_TYPE_LIMITED_RGBA4444));
230 :
231 4 : err = cpres_read_file_to_memory(input_path, &input_data, &png_size);
232 4 : if (err != CPRES_OK) {
233 1 : cpres_log(CPRES_LOG_LEVEL_ERROR, "PNG read (PNGX) failed: %s", cpres_error_string(err));
234 1 : return err;
235 : }
236 :
237 3 : err = cpres_encode_pngx_memory(input_data, png_size, &optimized_data, &optimized_size, config);
238 3 : free(input_data);
239 3 : if (err != CPRES_OK) {
240 0 : return err;
241 : }
242 :
243 3 : if (have_input_size && optimized_size >= input_size) {
244 0 : if (allow_lossy_rgba_larger_output) {
245 0 : cpres_log(CPRES_LOG_LEVEL_WARNING, "PNGX: RGBA lossy output larger than input (%zu > %zu) but forcing write per RGBA mode", optimized_size, input_size);
246 : } else {
247 0 : cpres_log(CPRES_LOG_LEVEL_WARNING, "PNGX: Optimized output larger than input (%zu > %zu)", optimized_size, input_size);
248 0 : cpres_free(optimized_data);
249 0 : return CPRES_ERROR_OUTPUT_NOT_SMALLER;
250 : }
251 : }
252 :
253 3 : fp = fopen(output_path, "wb");
254 3 : if (!fp) {
255 0 : cpres_log(CPRES_LOG_LEVEL_ERROR, "Failed to open output file '%s'", output_path);
256 0 : cpres_free(optimized_data);
257 0 : return CPRES_ERROR_IO;
258 : }
259 :
260 3 : written = fwrite(optimized_data, 1, optimized_size, fp);
261 3 : fclose(fp);
262 3 : cpres_free(optimized_data);
263 :
264 3 : if (written != optimized_size) {
265 0 : cpres_log(CPRES_LOG_LEVEL_ERROR, "Failed to write output file '%s'", output_path);
266 0 : return CPRES_ERROR_IO;
267 : }
268 :
269 3 : return CPRES_OK;
270 : }
271 :
272 : #endif /* COLOPRESSO_WITH_FILE_OPS */
|