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