| 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 | |
| 32 | struct 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 |
| 297 | CPNGFile::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 | |
| 301 | void CPNGFile::Encode(const void *pixels) { impl->Encode(pixels); } |
| 302 | |
| 303 | // Initialize for decoding |
| 304 | CPNGFile::CPNGFile(const void *const fileContents, const std::size_t fileSize) |
| 305 | : impl(new Impl(fileContents, fileSize)) {} |
| 306 | |
| 307 | void CPNGFile::Decode(void *const pixels) { impl->Decode(pixels); } |
| 308 | |
| 309 | CPNGFile::~CPNGFile() = default; |
| 310 | |
| 311 | std::uint32_t CPNGFile::Width() const { return impl->width; } |
| 312 | std::uint32_t CPNGFile::Height() const { return impl->height; } |
| 313 | bool CPNGFile::UsesAlpha() const { return impl->useAlpha; } |
| 314 | |