1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 2001, Sven2
5 * Copyright (c) 2017-2020, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17// PNG encoding/decoding using libpng
18
19#include "C4Application.h"
20#include <Standard.h>
21#include <StdPNG.h>
22
23#include <C4Log.h>
24
25#include <png.h>
26
27#include <algorithm>
28#include <memory>
29#include <stdexcept>
30#include <string>
31
32struct CPNGFile::Impl
33{
34 std::shared_ptr<spdlog::logger> logger;
35
36 // true if this instance is used for writing a PNG file or false if it is used for reading
37 bool writeMode;
38 // Pointer to the output file if this instance is used for writing
39 FILE *outputFile;
40 // Current read position in the input file contents
41 const png_byte *inputFileContents;
42 std::size_t inputFilePos, inputFileSize;
43
44 std::uint32_t width, height, rowSize;
45 bool useAlpha;
46
47 // libpng structs
48 png_structp png_ptr;
49 png_infop info_ptr;
50
51 // Initializes attributes to zero
52 Impl() :
53 logger(Application.LogSystem.CreateLogger(config&: Config.Logging.PNGFile)),
54 outputFile(nullptr), inputFileContents(nullptr),
55 png_ptr(nullptr), info_ptr(nullptr) {}
56
57 ~Impl()
58 {
59 ClearNoExcept();
60 }
61
62 // Frees resources. Can be called from destructor or unfinished constructor.
63 void Clear()
64 {
65 // Clear internal png ptrs
66 if (png_ptr || info_ptr)
67 {
68 if (writeMode)
69 {
70 png_destroy_write_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: &info_ptr);
71 png_ptr = nullptr; info_ptr = nullptr;
72 }
73 else
74 {
75 png_destroy_read_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: &info_ptr, end_info_ptr_ptr: nullptr);
76 png_ptr = nullptr; info_ptr = nullptr;
77 }
78 }
79 // Close file if open
80 if (outputFile)
81 {
82 const auto result = fclose(stream: outputFile);
83 outputFile = nullptr;
84 if (result != 0) throw std::runtime_error("fclose failed");
85 }
86 }
87
88 void ClearNoExcept() noexcept
89 {
90 try
91 {
92 Clear();
93 }
94 catch (const std::runtime_error &)
95 {
96 }
97 }
98
99 // Initialize for encoding
100 Impl(const std::string &filename,
101 const unsigned int width, const unsigned int height, const bool useAlpha)
102 : Impl()
103 {
104 try
105 {
106 PrepareEncoding(filename, width, height, useAlpha);
107 }
108 catch (const std::runtime_error &)
109 {
110 ClearNoExcept();
111 throw;
112 }
113 }
114
115 // Prepares writing to the specified file
116 void PrepareEncoding(const std::string &filename,
117 const unsigned int width, const unsigned int height, const bool useAlpha)
118 {
119 // open the file
120 outputFile = fopen(filename: filename.c_str(), modes: "wb");
121 if (!outputFile) throw std::runtime_error("fopen failed");
122 // init png-structs for writing
123 writeMode = true;
124 png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, error_ptr: this, error_fn: ErrorCallbackFn, warn_fn: WarningCallbackFn);
125 if (!png_ptr) throw std::runtime_error("png_create_write_struct failed");
126 info_ptr = png_create_info_struct(png_ptr);
127 if (!info_ptr) throw std::runtime_error("png_create_info_struct failed");
128 // io initialization
129 png_init_io(png_ptr, fp: outputFile);
130 // set header
131 png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth: 8,
132 color_type: (useAlpha ? PNG_COLOR_TYPE_RGB_ALPHA : PNG_COLOR_TYPE_RGB),
133 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
134 // Store image properties
135 this->width = width;
136 this->height = height;
137 this->useAlpha = useAlpha;
138 CalculateRowSize();
139 // Clonk expects the alpha channel to be inverted
140 png_set_invert_alpha(png_ptr);
141 // Write header
142 png_write_info(png_ptr, info_ptr);
143 // image data is given as bgr...
144 png_set_bgr(png_ptr);
145 }
146
147 void Encode(const void *const pixels)
148 {
149 // Write the whole image
150 png_write_image(png_ptr, image: CreateRowBuffer(pixels).get());
151 // Write footer
152 png_write_end(png_ptr, info_ptr);
153 // Close the file
154 Clear();
155 }
156
157 // Initialize for decoding
158 Impl(const void *const fileContents, const std::size_t fileSize)
159 : Impl()
160 {
161 try
162 {
163 PrepareDecoding(fileContents, fileSize);
164 }
165 catch (const std::runtime_error &)
166 {
167 ClearNoExcept();
168 throw;
169 }
170 }
171
172 // Prepares reading from the specified file contents
173 void PrepareDecoding(const void *const fileContents, const std::size_t fileSize)
174 {
175 // store file ptr
176 inputFileContents = static_cast<const png_byte *>(fileContents);
177 inputFilePos = 0;
178 inputFileSize = fileSize;
179 // check file
180 if (fileSize < 8 || png_sig_cmp(sig: const_cast<png_bytep>(inputFileContents), start: 0, num_to_check: 8) != 0)
181 {
182 throw std::runtime_error("File is not a PNG file");
183 }
184 // setup png for reading
185 writeMode = false;
186 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, error_ptr: this, error_fn: ErrorCallbackFn, warn_fn: WarningCallbackFn);
187 if (!png_ptr) throw std::runtime_error("png_create_read_struct failed");
188 info_ptr = png_create_info_struct(png_ptr);
189 if (!info_ptr) throw std::runtime_error("png_create_info_struct failed");
190 // set file-reading proc
191 png_set_read_fn(png_ptr, io_ptr: this, read_data_fn: &ReadCallbackFn);
192 // Clonk expects the alpha channel to be inverted
193 png_set_invert_alpha(png_ptr);
194 // read info
195 png_read_info(png_ptr, info_ptr);
196 // assign local vars
197 int bitsPerChannel, colorType;
198 png_get_IHDR(png_ptr, info_ptr, width: nullptr, height: nullptr, bit_depth: &bitsPerChannel, color_type: &colorType,
199 interlace_method: nullptr, compression_method: nullptr, filter_method: nullptr);
200 // convert to bgra
201 if (colorType == PNG_COLOR_TYPE_PALETTE && bitsPerChannel <= 8 ||
202 colorType == PNG_COLOR_TYPE_GRAY && bitsPerChannel < 8)
203 {
204 png_set_expand(png_ptr);
205 }
206 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
207 {
208 png_set_expand(png_ptr);
209 }
210 if (bitsPerChannel == 16)
211 {
212 png_set_strip_16(png_ptr);
213 }
214 else if (bitsPerChannel < 8)
215 {
216 png_set_packing(png_ptr);
217 }
218 if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA)
219 {
220 png_set_gray_to_rgb(png_ptr);
221 }
222 png_set_bgr(png_ptr);
223 // update info
224 png_read_update_info(png_ptr, info_ptr);
225 png_uint_32 ihdrWidth, ihdrHeight;
226 png_get_IHDR(png_ptr, info_ptr, width: &ihdrWidth, height: &ihdrHeight, bit_depth: &bitsPerChannel, color_type: &colorType,
227 interlace_method: nullptr, compression_method: nullptr, filter_method: nullptr);
228 // check format
229 if (colorType != PNG_COLOR_TYPE_RGB && colorType != PNG_COLOR_TYPE_RGB_ALPHA)
230 {
231 throw std::runtime_error("Unsupported color type");
232 }
233 // Store image properties
234 width = ihdrWidth;
235 height = ihdrHeight;
236 useAlpha = (colorType == PNG_COLOR_TYPE_RGB_ALPHA);
237 CalculateRowSize();
238 }
239
240 void Decode(void *const pixels)
241 {
242 png_read_image(png_ptr, image: CreateRowBuffer(pixels).get());
243 Clear();
244 }
245
246 // Calculates the row size and assures that it is the same as reported by libpng.
247 void CalculateRowSize()
248 {
249 const auto expectedRowSize = (useAlpha ? 4 : 3) * width;
250 rowSize = png_get_rowbytes(png_ptr, info_ptr);
251 if (expectedRowSize != rowSize) throw std::runtime_error("libpng uses unexpected row size");
252 }
253
254 // Creates the row pointer array that is needed by libpng for reading and writing PNG files
255 std::unique_ptr<png_bytep[]> CreateRowBuffer(const void *const pixels)
256 {
257 std::unique_ptr<png_bytep[]> rowBuf(new png_bytep[height]);
258 uint32_t rowIndex = 0;
259 std::generate_n(first: rowBuf.get(), n: height,
260 gen: [&] { return static_cast<png_bytep>(const_cast<void *>(pixels)) + rowIndex++ * rowSize; });
261 return rowBuf;
262 }
263
264 // Called by libpng when more input is needed to decode a file
265 static void PNGAPI ReadCallbackFn(const png_structp png_ptr,
266 const png_bytep data, const png_size_t length)
267 {
268 const auto pngFile = static_cast<Impl *>(png_get_io_ptr(png_ptr));
269 // Do not try to read beyond end of file
270 const std::size_t newPos = pngFile->inputFilePos + length;
271 if (newPos < pngFile->inputFilePos || newPos > pngFile->inputFileSize)
272 {
273 png_error(png_ptr, error_message: "Cannot read beyond end of file");
274 }
275 // Copy bytes and update position
276 std::copy_n(first: pngFile->inputFileContents + pngFile->inputFilePos, n: length, result: data);
277 pngFile->inputFilePos = newPos;
278 }
279
280 // Error callback for libpng
281 static void PNGAPI ErrorCallbackFn(const png_structp png_ptr, const png_const_charp msg)
282 {
283 const auto pngFile = static_cast<Impl *>(png_get_io_ptr(png_ptr));
284 pngFile->logger->error(msg);
285 throw std::runtime_error(std::string() + "libpng error: " + msg);
286 }
287
288 // Warning callback for libpng
289 static void PNGAPI WarningCallbackFn(const png_structp png_ptr, const png_const_charp msg)
290 {
291 const auto pngFile = static_cast<Impl *>(png_get_io_ptr(png_ptr));
292 pngFile->logger->debug(msg);
293 }
294};
295
296// Initialize for encoding
297CPNGFile::CPNGFile(const std::string &filename,
298 const std::uint32_t width, const std::uint32_t height, const bool useAlpha)
299 : impl(new Impl(filename, width, height, useAlpha)) {}
300
301void CPNGFile::Encode(const void *pixels) { impl->Encode(pixels); }
302
303// Initialize for decoding
304CPNGFile::CPNGFile(const void *const fileContents, const std::size_t fileSize)
305 : impl(new Impl(fileContents, fileSize)) {}
306
307void CPNGFile::Decode(void *const pixels) { impl->Decode(pixels); }
308
309CPNGFile::~CPNGFile() = default;
310
311std::uint32_t CPNGFile::Width() const { return impl->width; }
312std::uint32_t CPNGFile::Height() const { return impl->height; }
313bool CPNGFile::UsesAlpha() const { return impl->useAlpha; }
314