1#include "image_loader.h"
2
3#include <base/log.h>
4#include <base/system.h>
5
6#include <png.h>
7
8#include <csetjmp>
9#include <cstdlib>
10
11bool CByteBufferReader::Read(void *pData, size_t Size)
12{
13 if(m_Error)
14 return false;
15
16 if(m_ReadOffset + Size <= m_Size)
17 {
18 mem_copy(dest: pData, source: &m_pData[m_ReadOffset], size: Size);
19 m_ReadOffset += Size;
20 return true;
21 }
22 else
23 {
24 m_Error = true;
25 return false;
26 }
27}
28
29void CByteBufferWriter::Write(const void *pData, size_t Size)
30{
31 if(!Size)
32 return;
33
34 const size_t WriteOffset = m_vBuffer.size();
35 m_vBuffer.resize(new_size: WriteOffset + Size);
36 mem_copy(dest: &m_vBuffer[WriteOffset], source: pData, size: Size);
37}
38
39class CUserErrorStruct
40{
41public:
42 CByteBufferReader *m_pReader;
43 const char *m_pContextName;
44 std::jmp_buf m_JmpBuf;
45};
46
47[[noreturn]] static void PngErrorCallback(png_structp pPngStruct, png_const_charp pErrorMessage)
48{
49 CUserErrorStruct *pUserStruct = static_cast<CUserErrorStruct *>(png_get_error_ptr(png_ptr: pPngStruct));
50 log_error("png", "error for file \"%s\": %s", pUserStruct->m_pContextName, pErrorMessage);
51 std::longjmp(env: pUserStruct->m_JmpBuf, val: 1);
52}
53
54static void PngWarningCallback(png_structp pPngStruct, png_const_charp pWarningMessage)
55{
56 CUserErrorStruct *pUserStruct = static_cast<CUserErrorStruct *>(png_get_error_ptr(png_ptr: pPngStruct));
57 log_warn("png", "warning for file \"%s\": %s", pUserStruct->m_pContextName, pWarningMessage);
58}
59
60static void PngReadDataCallback(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToRead)
61{
62 CByteBufferReader *pReader = static_cast<CByteBufferReader *>(png_get_io_ptr(png_ptr: pPngStruct));
63 if(!pReader->Read(pData: pOutBytes, Size: ByteCountToRead))
64 {
65 png_error(png_ptr: pPngStruct, error_message: "Could not read all bytes, file was too small");
66 }
67}
68
69static CImageInfo::EImageFormat ImageFormatFromChannelCount(int ColorChannelCount)
70{
71 switch(ColorChannelCount)
72 {
73 case 1:
74 return CImageInfo::FORMAT_R;
75 case 2:
76 return CImageInfo::FORMAT_RA;
77 case 3:
78 return CImageInfo::FORMAT_RGB;
79 case 4:
80 return CImageInfo::FORMAT_RGBA;
81 default:
82 dbg_assert_failed("ColorChannelCount invalid");
83 }
84}
85
86static int PngliteIncompatibility(png_structp pPngStruct, png_infop pPngInfo)
87{
88 int Result = 0;
89
90 const int ColorType = png_get_color_type(png_ptr: pPngStruct, info_ptr: pPngInfo);
91 switch(ColorType)
92 {
93 case PNG_COLOR_TYPE_GRAY:
94 case PNG_COLOR_TYPE_RGB:
95 case PNG_COLOR_TYPE_RGB_ALPHA:
96 case PNG_COLOR_TYPE_GRAY_ALPHA:
97 break;
98 default:
99 log_debug("png", "color type %d unsupported by pnglite", ColorType);
100 Result |= CImageLoader::PNGLITE_COLOR_TYPE;
101 }
102
103 const int BitDepth = png_get_bit_depth(png_ptr: pPngStruct, info_ptr: pPngInfo);
104 switch(BitDepth)
105 {
106 case 8:
107 case 16:
108 break;
109 default:
110 log_debug("png", "bit depth %d unsupported by pnglite", BitDepth);
111 Result |= CImageLoader::PNGLITE_BIT_DEPTH;
112 }
113
114 const int InterlaceType = png_get_interlace_type(png_ptr: pPngStruct, info_ptr: pPngInfo);
115 if(InterlaceType != PNG_INTERLACE_NONE)
116 {
117 log_debug("png", "interlace type %d unsupported by pnglite", InterlaceType);
118 Result |= CImageLoader::PNGLITE_INTERLACE_TYPE;
119 }
120
121 if(png_get_compression_type(png_ptr: pPngStruct, info_ptr: pPngInfo) != PNG_COMPRESSION_TYPE_BASE)
122 {
123 log_debug("png", "non-default compression type unsupported by pnglite");
124 Result |= CImageLoader::PNGLITE_COMPRESSION_TYPE;
125 }
126
127 if(png_get_filter_type(png_ptr: pPngStruct, info_ptr: pPngInfo) != PNG_FILTER_TYPE_BASE)
128 {
129 log_debug("png", "non-default filter type unsupported by pnglite");
130 Result |= CImageLoader::PNGLITE_FILTER_TYPE;
131 }
132
133 return Result;
134}
135
136bool CImageLoader::LoadPng(CByteBufferReader &Reader, const char *pContextName, CImageInfo &Image, int &PngliteIncompatible)
137{
138 CUserErrorStruct UserErrorStruct = {.m_pReader: &Reader, .m_pContextName: pContextName, .m_JmpBuf: {}};
139
140 if(setjmp(UserErrorStruct.m_JmpBuf))
141 {
142 return false;
143 }
144
145 png_structp pPngStruct = png_create_read_struct(PNG_LIBPNG_VER_STRING, error_ptr: &UserErrorStruct, error_fn: PngErrorCallback, warn_fn: PngWarningCallback);
146 if(pPngStruct == nullptr)
147 {
148 log_error("png", "libpng internal failure: png_create_read_struct failed.");
149 return false;
150 }
151
152 png_infop pPngInfo = nullptr;
153 png_bytepp pRowPointers = nullptr;
154 int Height = 0; // ensure this is not undefined for the Cleanup function
155 const auto &&Cleanup = [&]() {
156 if(pRowPointers != nullptr)
157 {
158 for(int y = 0; y < Height; ++y)
159 {
160 delete[] pRowPointers[y];
161 }
162 }
163 delete[] pRowPointers;
164 if(pPngInfo != nullptr)
165 {
166 png_destroy_info_struct(png_ptr: pPngStruct, info_ptr_ptr: &pPngInfo);
167 }
168 png_destroy_read_struct(png_ptr_ptr: &pPngStruct, info_ptr_ptr: nullptr, end_info_ptr_ptr: nullptr);
169 };
170 if(setjmp(UserErrorStruct.m_JmpBuf))
171 {
172 Cleanup();
173 return false;
174 }
175
176 pPngInfo = png_create_info_struct(png_ptr: pPngStruct);
177 if(pPngInfo == nullptr)
178 {
179 Cleanup();
180 log_error("png", "libpng internal failure: png_create_info_struct failed.");
181 return false;
182 }
183
184 png_byte aSignature[8];
185 if(!Reader.Read(pData: aSignature, Size: sizeof(aSignature)) || png_sig_cmp(sig: aSignature, start: 0, num_to_check: sizeof(aSignature)) != 0)
186 {
187 Cleanup();
188 log_error("png", "file is not a valid PNG file (signature mismatch).");
189 return false;
190 }
191
192 png_set_read_fn(png_ptr: pPngStruct, io_ptr: (png_bytep)&Reader, read_data_fn: PngReadDataCallback);
193 png_set_sig_bytes(png_ptr: pPngStruct, num_bytes: sizeof(aSignature));
194
195 png_read_info(png_ptr: pPngStruct, info_ptr: pPngInfo);
196
197 if(Reader.Error())
198 {
199 // error already logged
200 Cleanup();
201 return false;
202 }
203
204 const int Width = png_get_image_width(png_ptr: pPngStruct, info_ptr: pPngInfo);
205 Height = png_get_image_height(png_ptr: pPngStruct, info_ptr: pPngInfo);
206 const png_byte BitDepth = png_get_bit_depth(png_ptr: pPngStruct, info_ptr: pPngInfo);
207 const int ColorType = png_get_color_type(png_ptr: pPngStruct, info_ptr: pPngInfo);
208
209 if(Width == 0 || Height == 0)
210 {
211 log_error("png", "image has width (%d) or height (%d) of 0.", Width, Height);
212 Cleanup();
213 return false;
214 }
215
216 if(BitDepth == 16)
217 {
218 png_set_strip_16(png_ptr: pPngStruct);
219 }
220 else if(BitDepth > 8 || BitDepth == 0)
221 {
222 log_error("png", "bit depth %d not supported.", BitDepth);
223 Cleanup();
224 return false;
225 }
226
227 if(ColorType == PNG_COLOR_TYPE_PALETTE)
228 {
229 png_set_palette_to_rgb(png_ptr: pPngStruct);
230 }
231
232 if(ColorType == PNG_COLOR_TYPE_GRAY && BitDepth < 8)
233 {
234 png_set_expand_gray_1_2_4_to_8(png_ptr: pPngStruct);
235 }
236
237 if(png_get_valid(png_ptr: pPngStruct, info_ptr: pPngInfo, PNG_INFO_tRNS))
238 {
239 png_set_tRNS_to_alpha(png_ptr: pPngStruct);
240 }
241
242 png_read_update_info(png_ptr: pPngStruct, info_ptr: pPngInfo);
243
244 const int ColorChannelCount = png_get_channels(png_ptr: pPngStruct, info_ptr: pPngInfo);
245 const int BytesInRow = png_get_rowbytes(png_ptr: pPngStruct, info_ptr: pPngInfo);
246 dbg_assert(BytesInRow == Width * ColorChannelCount, "bytes in row incorrect.");
247
248 pRowPointers = new png_bytep[Height];
249 for(int y = 0; y < Height; ++y)
250 {
251 pRowPointers[y] = new png_byte[BytesInRow];
252 }
253
254 png_read_image(png_ptr: pPngStruct, image: pRowPointers);
255
256 if(!Reader.Error())
257 {
258 Image.m_Width = Width;
259 Image.m_Height = Height;
260 Image.m_Format = ImageFormatFromChannelCount(ColorChannelCount);
261 Image.m_pData = static_cast<uint8_t *>(malloc(size: Image.DataSize()));
262 for(int y = 0; y < Height; ++y)
263 {
264 mem_copy(dest: &Image.m_pData[y * BytesInRow], source: pRowPointers[y], size: BytesInRow);
265 }
266 PngliteIncompatible = PngliteIncompatibility(pPngStruct, pPngInfo);
267 }
268
269 Cleanup();
270
271 return !Reader.Error();
272}
273
274bool CImageLoader::LoadPng(IOHANDLE File, const char *pFilename, CImageInfo &Image, int &PngliteIncompatible)
275{
276 if(!File)
277 {
278 log_error("png", "failed to open file for reading. filename='%s'", pFilename);
279 return false;
280 }
281
282 void *pFileData;
283 unsigned FileDataSize;
284 const bool ReadSuccess = io_read_all(io: File, result: &pFileData, result_len: &FileDataSize);
285 io_close(io: File);
286 if(!ReadSuccess)
287 {
288 log_error("png", "failed to read file. filename='%s'", pFilename);
289 return false;
290 }
291
292 CByteBufferReader ImageReader(static_cast<const uint8_t *>(pFileData), FileDataSize);
293
294 const bool LoadResult = CImageLoader::LoadPng(Reader&: ImageReader, pContextName: pFilename, Image, PngliteIncompatible);
295 free(ptr: pFileData);
296 if(!LoadResult)
297 {
298 log_error("png", "failed to load image from file. filename='%s'", pFilename);
299 return false;
300 }
301
302 if(Image.m_Format != CImageInfo::FORMAT_RGB && Image.m_Format != CImageInfo::FORMAT_RGBA)
303 {
304 log_error("png", "image has unsupported format. filename='%s' format='%s'", pFilename, Image.FormatName());
305 Image.Free();
306 return false;
307 }
308
309 return true;
310}
311
312static void PngWriteDataCallback(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToWrite)
313{
314 CByteBufferWriter *pWriter = static_cast<CByteBufferWriter *>(png_get_io_ptr(png_ptr: pPngStruct));
315 pWriter->Write(pData: pOutBytes, Size: ByteCountToWrite);
316}
317
318static void PngOutputFlushCallback(png_structp pPngStruct)
319{
320 // no need to flush memory buffer
321}
322
323static int PngColorTypeFromFormat(CImageInfo::EImageFormat Format)
324{
325 switch(Format)
326 {
327 case CImageInfo::FORMAT_R:
328 return PNG_COLOR_TYPE_GRAY;
329 case CImageInfo::FORMAT_RA:
330 return PNG_COLOR_TYPE_GRAY_ALPHA;
331 case CImageInfo::FORMAT_RGB:
332 return PNG_COLOR_TYPE_RGB;
333 case CImageInfo::FORMAT_RGBA:
334 return PNG_COLOR_TYPE_RGBA;
335 default:
336 dbg_assert_failed("Format invalid");
337 }
338}
339
340bool CImageLoader::SavePng(CByteBufferWriter &Writer, const CImageInfo &Image)
341{
342 png_structp pPngStruct = png_create_write_struct(PNG_LIBPNG_VER_STRING, error_ptr: nullptr, error_fn: nullptr, warn_fn: nullptr);
343 if(pPngStruct == nullptr)
344 {
345 log_error("png", "libpng internal failure: png_create_write_struct failed.");
346 return false;
347 }
348
349 png_infop pPngInfo = png_create_info_struct(png_ptr: pPngStruct);
350 if(pPngInfo == nullptr)
351 {
352 png_destroy_read_struct(png_ptr_ptr: &pPngStruct, info_ptr_ptr: nullptr, end_info_ptr_ptr: nullptr);
353 log_error("png", "libpng internal failure: png_create_info_struct failed.");
354 return false;
355 }
356
357 png_set_write_fn(png_ptr: pPngStruct, io_ptr: (png_bytep)&Writer, write_data_fn: PngWriteDataCallback, output_flush_fn: PngOutputFlushCallback);
358
359 png_set_IHDR(png_ptr: pPngStruct, info_ptr: pPngInfo, width: Image.m_Width, height: Image.m_Height, bit_depth: 8, color_type: PngColorTypeFromFormat(Format: Image.m_Format), PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
360 png_write_info(png_ptr: pPngStruct, info_ptr: pPngInfo);
361
362 png_bytepp pRowPointers = new png_bytep[Image.m_Height];
363 const int WidthBytes = Image.m_Width * Image.PixelSize();
364 ptrdiff_t BufferOffset = 0;
365 for(size_t y = 0; y < Image.m_Height; ++y)
366 {
367 pRowPointers[y] = new png_byte[WidthBytes];
368 mem_copy(dest: pRowPointers[y], source: Image.m_pData + BufferOffset, size: WidthBytes);
369 BufferOffset += (ptrdiff_t)WidthBytes;
370 }
371 png_write_image(png_ptr: pPngStruct, image: pRowPointers);
372 png_write_end(png_ptr: pPngStruct, info_ptr: pPngInfo);
373
374 for(size_t y = 0; y < Image.m_Height; ++y)
375 {
376 delete[] pRowPointers[y];
377 }
378 delete[] pRowPointers;
379
380 png_destroy_info_struct(png_ptr: pPngStruct, info_ptr_ptr: &pPngInfo);
381 png_destroy_write_struct(png_ptr_ptr: &pPngStruct, info_ptr_ptr: nullptr);
382
383 return true;
384}
385
386bool CImageLoader::SavePng(IOHANDLE File, const char *pFilename, const CImageInfo &Image)
387{
388 if(!File)
389 {
390 log_error("png", "failed to open file for writing. filename='%s'", pFilename);
391 return false;
392 }
393
394 CByteBufferWriter Writer;
395 if(!CImageLoader::SavePng(Writer, Image))
396 {
397 // error already logged
398 io_close(io: File);
399 return false;
400 }
401
402 const bool WriteSuccess = io_write(io: File, buffer: Writer.Data(), size: Writer.Size()) == Writer.Size();
403 if(!WriteSuccess)
404 {
405 log_error("png", "failed to write PNG data to file. filename='%s'", pFilename);
406 }
407 io_close(io: File);
408 return WriteSuccess;
409}
410