duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

image.cpp (24055B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR PolyForm-Strict-1.0.0)
      3 
      4 #include "image.h"
      5 
      6 #include "common/assert.h"
      7 #include "common/bitutils.h"
      8 #include "common/fastjmp.h"
      9 #include "common/file_system.h"
     10 #include "common/log.h"
     11 #include "common/path.h"
     12 #include "common/scoped_guard.h"
     13 #include "common/string_util.h"
     14 
     15 #include <jpeglib.h>
     16 #include <png.h>
     17 #include <webp/decode.h>
     18 #include <webp/encode.h>
     19 
     20 // clang-format off
     21 #ifdef _MSC_VER
     22 #pragma warning(disable : 4611) // warning C4611: interaction between '_setjmp' and C++ object destruction is non-portable
     23 #endif
     24 // clang-format on
     25 
     26 Log_SetChannel(Image);
     27 
     28 static bool PNGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size);
     29 static bool PNGBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality);
     30 static bool PNGFileLoader(RGBA8Image* image, std::string_view filename, std::FILE* fp);
     31 static bool PNGFileSaver(const RGBA8Image& image, std::string_view filename, std::FILE* fp, u8 quality);
     32 
     33 static bool JPEGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size);
     34 static bool JPEGBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality);
     35 static bool JPEGFileLoader(RGBA8Image* image, std::string_view filename, std::FILE* fp);
     36 static bool JPEGFileSaver(const RGBA8Image& image, std::string_view filename, std::FILE* fp, u8 quality);
     37 
     38 static bool WebPBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size);
     39 static bool WebPBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality);
     40 static bool WebPFileLoader(RGBA8Image* image, std::string_view filename, std::FILE* fp);
     41 static bool WebPFileSaver(const RGBA8Image& image, std::string_view filename, std::FILE* fp, u8 quality);
     42 
     43 struct FormatHandler
     44 {
     45   const char* extension;
     46   bool (*buffer_loader)(RGBA8Image*, const void*, size_t);
     47   bool (*buffer_saver)(const RGBA8Image&, std::vector<u8>*, u8);
     48   bool (*file_loader)(RGBA8Image*, std::string_view, std::FILE*);
     49   bool (*file_saver)(const RGBA8Image&, std::string_view, std::FILE*, u8);
     50 };
     51 
     52 static constexpr FormatHandler s_format_handlers[] = {
     53   {"png", PNGBufferLoader, PNGBufferSaver, PNGFileLoader, PNGFileSaver},
     54   {"jpg", JPEGBufferLoader, JPEGBufferSaver, JPEGFileLoader, JPEGFileSaver},
     55   {"jpeg", JPEGBufferLoader, JPEGBufferSaver, JPEGFileLoader, JPEGFileSaver},
     56   {"webp", WebPBufferLoader, WebPBufferSaver, WebPFileLoader, WebPFileSaver},
     57 };
     58 
     59 static const FormatHandler* GetFormatHandler(std::string_view extension)
     60 {
     61   for (const FormatHandler& handler : s_format_handlers)
     62   {
     63     if (StringUtil::Strncasecmp(extension.data(), handler.extension, extension.size()) == 0)
     64       return &handler;
     65   }
     66 
     67   return nullptr;
     68 }
     69 
     70 RGBA8Image::RGBA8Image() = default;
     71 
     72 RGBA8Image::RGBA8Image(const RGBA8Image& copy) : Image(copy)
     73 {
     74 }
     75 
     76 RGBA8Image::RGBA8Image(u32 width, u32 height, const u32* pixels) : Image(width, height, pixels)
     77 {
     78 }
     79 
     80 RGBA8Image::RGBA8Image(RGBA8Image&& move) : Image(move)
     81 {
     82 }
     83 
     84 RGBA8Image::RGBA8Image(u32 width, u32 height) : Image(width, height)
     85 {
     86 }
     87 
     88 RGBA8Image::RGBA8Image(u32 width, u32 height, std::vector<u32> pixels) : Image(width, height, std::move(pixels))
     89 {
     90 }
     91 
     92 RGBA8Image& RGBA8Image::operator=(const RGBA8Image& copy)
     93 {
     94   Image<u32>::operator=(copy);
     95   return *this;
     96 }
     97 
     98 RGBA8Image& RGBA8Image::operator=(RGBA8Image&& move)
     99 {
    100   Image<u32>::operator=(move);
    101   return *this;
    102 }
    103 
    104 bool RGBA8Image::LoadFromFile(const char* filename)
    105 {
    106   auto fp = FileSystem::OpenManagedCFile(filename, "rb");
    107   if (!fp)
    108     return false;
    109 
    110   return LoadFromFile(filename, fp.get());
    111 }
    112 
    113 bool RGBA8Image::SaveToFile(const char* filename, u8 quality) const
    114 {
    115   auto fp = FileSystem::OpenManagedCFile(filename, "wb");
    116   if (!fp)
    117     return false;
    118 
    119   if (SaveToFile(filename, fp.get(), quality))
    120     return true;
    121 
    122   // save failed
    123   fp.reset();
    124   FileSystem::DeleteFile(filename);
    125   return false;
    126 }
    127 
    128 bool RGBA8Image::LoadFromFile(std::string_view filename, std::FILE* fp)
    129 {
    130   const std::string_view extension(Path::GetExtension(filename));
    131   const FormatHandler* handler = GetFormatHandler(extension);
    132   if (!handler || !handler->file_loader)
    133   {
    134     ERROR_LOG("Unknown extension '{}'", extension);
    135     return false;
    136   }
    137 
    138   return handler->file_loader(this, filename, fp);
    139 }
    140 
    141 bool RGBA8Image::LoadFromBuffer(std::string_view filename, const void* buffer, size_t buffer_size)
    142 {
    143   const std::string_view extension(Path::GetExtension(filename));
    144   const FormatHandler* handler = GetFormatHandler(extension);
    145   if (!handler || !handler->buffer_loader)
    146   {
    147     ERROR_LOG("Unknown extension '{}'", extension);
    148     return false;
    149   }
    150 
    151   return handler->buffer_loader(this, buffer, buffer_size);
    152 }
    153 
    154 bool RGBA8Image::SaveToFile(std::string_view filename, std::FILE* fp, u8 quality) const
    155 {
    156   const std::string_view extension(Path::GetExtension(filename));
    157   const FormatHandler* handler = GetFormatHandler(extension);
    158   if (!handler || !handler->file_saver)
    159   {
    160     ERROR_LOG("Unknown extension '{}'", extension);
    161     return false;
    162   }
    163 
    164   if (!handler->file_saver(*this, filename, fp, quality))
    165     return false;
    166 
    167   return (std::fflush(fp) == 0);
    168 }
    169 
    170 std::optional<std::vector<u8>> RGBA8Image::SaveToBuffer(std::string_view filename, u8 quality) const
    171 {
    172   std::optional<std::vector<u8>> ret;
    173 
    174   const std::string_view extension(Path::GetExtension(filename));
    175   const FormatHandler* handler = GetFormatHandler(extension);
    176   if (!handler || !handler->file_saver)
    177   {
    178     ERROR_LOG("Unknown extension '{}'", extension);
    179     return ret;
    180   }
    181 
    182   ret = std::vector<u8>();
    183   if (!handler->buffer_saver(*this, &ret.value(), quality))
    184     ret.reset();
    185 
    186   return ret;
    187 }
    188 
    189 #if 0
    190 
    191 void RGBA8Image::Resize(u32 new_width, u32 new_height)
    192 {
    193   if (m_width == new_width && m_height == new_height)
    194     return;
    195 
    196   std::vector<u32> resized_texture_data(new_width * new_height);
    197   u32 resized_texture_stride = sizeof(u32) * new_width;
    198   if (!stbir_resize_uint8(reinterpret_cast<u8*>(m_pixels.data()), m_width, m_height, GetPitch(),
    199                           reinterpret_cast<u8*>(resized_texture_data.data()), new_width, new_height,
    200                           resized_texture_stride, 4))
    201   {
    202     Panic("stbir_resize_uint8 failed");
    203     return;
    204   }
    205 
    206   SetPixels(new_width, new_height, std::move(resized_texture_data));
    207 }
    208 
    209 void RGBA8Image::Resize(const RGBA8Image* src_image, u32 new_width, u32 new_height)
    210 {
    211   if (src_image->m_width == new_width && src_image->m_height == new_height)
    212   {
    213     SetPixels(src_image->m_width, src_image->m_height, src_image->m_pixels.data());
    214     return;
    215   }
    216 
    217   SetSize(new_width, new_height);
    218   if (!stbir_resize_uint8(reinterpret_cast<const u8*>(src_image->m_pixels.data()), src_image->m_width,
    219                           src_image->m_height, src_image->GetPitch(), reinterpret_cast<u8*>(m_pixels.data()), new_width,
    220                           new_height, GetPitch(), 4))
    221   {
    222     Panic("stbir_resize_uint8 failed");
    223     return;
    224   }
    225 }
    226 
    227 #endif
    228 
    229 static bool PNGCommonLoader(RGBA8Image* image, png_structp png_ptr, png_infop info_ptr, std::vector<u32>& new_data,
    230                             std::vector<png_bytep>& row_pointers)
    231 {
    232   png_read_info(png_ptr, info_ptr);
    233 
    234   const u32 width = png_get_image_width(png_ptr, info_ptr);
    235   const u32 height = png_get_image_height(png_ptr, info_ptr);
    236   const png_byte color_type = png_get_color_type(png_ptr, info_ptr);
    237   const png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
    238 
    239   // Read any color_type into 8bit depth, RGBA format.
    240   // See http://www.libpng.org/pub/png/libpng-manual.txt
    241 
    242   if (bit_depth == 16)
    243     png_set_strip_16(png_ptr);
    244 
    245   if (color_type == PNG_COLOR_TYPE_PALETTE)
    246     png_set_palette_to_rgb(png_ptr);
    247 
    248   // PNG_COLOR_TYPE_GRAY_ALPHA is always 8 or 16bit depth.
    249   if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
    250     png_set_expand_gray_1_2_4_to_8(png_ptr);
    251 
    252   if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
    253     png_set_tRNS_to_alpha(png_ptr);
    254 
    255   // These color_type don't have an alpha channel then fill it with 0xff.
    256   if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)
    257     png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
    258 
    259   if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
    260     png_set_gray_to_rgb(png_ptr);
    261 
    262   png_read_update_info(png_ptr, info_ptr);
    263 
    264   new_data.resize(width * height);
    265   row_pointers.reserve(height);
    266   for (u32 y = 0; y < height; y++)
    267     row_pointers.push_back(reinterpret_cast<png_bytep>(new_data.data() + y * width));
    268 
    269   png_read_image(png_ptr, row_pointers.data());
    270   image->SetPixels(width, height, std::move(new_data));
    271   return true;
    272 }
    273 
    274 bool PNGFileLoader(RGBA8Image* image, std::string_view filename, std::FILE* fp)
    275 {
    276   png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    277   if (!png_ptr)
    278     return false;
    279 
    280   png_infop info_ptr = png_create_info_struct(png_ptr);
    281   if (!info_ptr)
    282   {
    283     png_destroy_read_struct(&png_ptr, nullptr, nullptr);
    284     return false;
    285   }
    286 
    287   ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
    288 
    289   std::vector<u32> new_data;
    290   std::vector<png_bytep> row_pointers;
    291 
    292   if (setjmp(png_jmpbuf(png_ptr)))
    293     return false;
    294 
    295   png_set_read_fn(png_ptr, fp, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
    296     std::FILE* fp = static_cast<std::FILE*>(png_get_io_ptr(png_ptr));
    297     if (std::fread(data_ptr, size, 1, fp) != 1)
    298       png_error(png_ptr, "Read error");
    299   });
    300 
    301   return PNGCommonLoader(image, png_ptr, info_ptr, new_data, row_pointers);
    302 }
    303 
    304 bool PNGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
    305 {
    306   png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    307   if (!png_ptr)
    308     return false;
    309 
    310   png_infop info_ptr = png_create_info_struct(png_ptr);
    311   if (!info_ptr)
    312   {
    313     png_destroy_read_struct(&png_ptr, nullptr, nullptr);
    314     return false;
    315   }
    316 
    317   ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
    318 
    319   std::vector<u32> new_data;
    320   std::vector<png_bytep> row_pointers;
    321 
    322   if (setjmp(png_jmpbuf(png_ptr)))
    323     return false;
    324 
    325   struct IOData
    326   {
    327     const u8* buffer;
    328     size_t buffer_size;
    329     size_t buffer_pos;
    330   };
    331   IOData data = {static_cast<const u8*>(buffer), buffer_size, 0};
    332 
    333   png_set_read_fn(png_ptr, &data, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
    334     IOData* data = static_cast<IOData*>(png_get_io_ptr(png_ptr));
    335     const size_t read_size = std::min<size_t>(data->buffer_size - data->buffer_pos, size);
    336     if (read_size > 0)
    337     {
    338       std::memcpy(data_ptr, data->buffer + data->buffer_pos, read_size);
    339       data->buffer_pos += read_size;
    340     }
    341   });
    342 
    343   return PNGCommonLoader(image, png_ptr, info_ptr, new_data, row_pointers);
    344 }
    345 
    346 static void PNGSaveCommon(const RGBA8Image& image, png_structp png_ptr, png_infop info_ptr, u8 quality)
    347 {
    348   png_set_compression_level(png_ptr, std::clamp(quality / 10, 0, 9));
    349   png_set_IHDR(png_ptr, info_ptr, image.GetWidth(), image.GetHeight(), 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
    350                PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
    351   png_write_info(png_ptr, info_ptr);
    352 
    353   for (u32 y = 0; y < image.GetHeight(); ++y)
    354     png_write_row(png_ptr, (png_bytep)image.GetRowPixels(y));
    355 
    356   png_write_end(png_ptr, nullptr);
    357 }
    358 
    359 bool PNGFileSaver(const RGBA8Image& image, std::string_view filename, std::FILE* fp, u8 quality)
    360 {
    361   png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    362   png_infop info_ptr = nullptr;
    363   if (!png_ptr)
    364     return false;
    365 
    366   ScopedGuard cleanup([&png_ptr, &info_ptr]() {
    367     if (png_ptr)
    368       png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
    369   });
    370 
    371   info_ptr = png_create_info_struct(png_ptr);
    372   if (!info_ptr)
    373     return false;
    374 
    375   if (setjmp(png_jmpbuf(png_ptr)))
    376     return false;
    377 
    378   png_set_write_fn(
    379     png_ptr, fp,
    380     [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
    381       if (std::fwrite(data_ptr, size, 1, static_cast<std::FILE*>(png_get_io_ptr(png_ptr))) != 1)
    382         png_error(png_ptr, "file write error");
    383     },
    384     [](png_structp png_ptr) {});
    385 
    386   PNGSaveCommon(image, png_ptr, info_ptr, quality);
    387   return true;
    388 }
    389 
    390 bool PNGBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality)
    391 {
    392   png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    393   png_infop info_ptr = nullptr;
    394   if (!png_ptr)
    395     return false;
    396 
    397   ScopedGuard cleanup([&png_ptr, &info_ptr]() {
    398     if (png_ptr)
    399       png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
    400   });
    401 
    402   info_ptr = png_create_info_struct(png_ptr);
    403   if (!info_ptr)
    404     return false;
    405 
    406   buffer->reserve(image.GetWidth() * image.GetHeight() * 2);
    407 
    408   if (setjmp(png_jmpbuf(png_ptr)))
    409     return false;
    410 
    411   png_set_write_fn(
    412     png_ptr, buffer,
    413     [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
    414       std::vector<u8>* buffer = static_cast<std::vector<u8>*>(png_get_io_ptr(png_ptr));
    415       const size_t old_pos = buffer->size();
    416       buffer->resize(old_pos + size);
    417       std::memcpy(buffer->data() + old_pos, data_ptr, size);
    418     },
    419     [](png_structp png_ptr) {});
    420 
    421   PNGSaveCommon(image, png_ptr, info_ptr, quality);
    422   return true;
    423 }
    424 
    425 namespace {
    426 struct JPEGErrorHandler
    427 {
    428   jpeg_error_mgr err;
    429   fastjmp_buf jbuf;
    430 
    431   JPEGErrorHandler()
    432   {
    433     jpeg_std_error(&err);
    434     err.error_exit = &ErrorExit;
    435   }
    436 
    437   static void ErrorExit(j_common_ptr cinfo)
    438   {
    439     JPEGErrorHandler* eh = (JPEGErrorHandler*)cinfo->err;
    440     char msg[JMSG_LENGTH_MAX];
    441     eh->err.format_message(cinfo, msg);
    442     ERROR_LOG("libjpeg fatal error: {}", msg);
    443     fastjmp_jmp(&eh->jbuf, 1);
    444   }
    445 };
    446 } // namespace
    447 
    448 template<typename T>
    449 static bool WrapJPEGDecompress(RGBA8Image* image, T setup_func)
    450 {
    451   std::vector<u8> scanline;
    452   jpeg_decompress_struct info = {};
    453 
    454   // NOTE: Be **very** careful not to allocate memory after calling this function.
    455   // It won't get freed, because fastjmp does not unwind the stack.
    456   JPEGErrorHandler errhandler;
    457   if (fastjmp_set(&errhandler.jbuf) != 0)
    458   {
    459     jpeg_destroy_decompress(&info);
    460     return false;
    461   }
    462 
    463   info.err = &errhandler.err;
    464   jpeg_create_decompress(&info);
    465   setup_func(info);
    466 
    467   const int herr = jpeg_read_header(&info, TRUE);
    468   if (herr != JPEG_HEADER_OK)
    469   {
    470     ERROR_LOG("jpeg_read_header() returned {}", herr);
    471     return false;
    472   }
    473 
    474   if (info.image_width == 0 || info.image_height == 0 || info.num_components < 3)
    475   {
    476     ERROR_LOG("Invalid image dimensions: {}x{}x{}", info.image_width, info.image_height, info.num_components);
    477     return false;
    478   }
    479 
    480   info.out_color_space = JCS_RGB;
    481   info.out_color_components = 3;
    482 
    483   if (!jpeg_start_decompress(&info))
    484   {
    485     ERROR_LOG("jpeg_start_decompress() returned failure");
    486     return false;
    487   }
    488 
    489   image->SetSize(info.image_width, info.image_height);
    490   scanline.resize(info.image_width * 3);
    491 
    492   u8* scanline_buffer[1] = {scanline.data()};
    493   bool result = true;
    494   for (u32 y = 0; y < info.image_height; y++)
    495   {
    496     if (jpeg_read_scanlines(&info, scanline_buffer, 1) != 1)
    497     {
    498       ERROR_LOG("jpeg_read_scanlines() failed at row {}", y);
    499       result = false;
    500       break;
    501     }
    502 
    503     // RGB -> RGBA
    504     const u8* src_ptr = scanline.data();
    505     u32* dst_ptr = image->GetRowPixels(y);
    506     for (u32 x = 0; x < info.image_width; x++)
    507     {
    508       *(dst_ptr++) =
    509         (ZeroExtend32(src_ptr[0]) | (ZeroExtend32(src_ptr[1]) << 8) | (ZeroExtend32(src_ptr[2]) << 16) | 0xFF000000u);
    510       src_ptr += 3;
    511     }
    512   }
    513 
    514   jpeg_finish_decompress(&info);
    515   jpeg_destroy_decompress(&info);
    516   return result;
    517 }
    518 
    519 bool JPEGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
    520 {
    521   return WrapJPEGDecompress(image, [buffer, buffer_size](jpeg_decompress_struct& info) {
    522     jpeg_mem_src(&info, static_cast<const unsigned char*>(buffer), static_cast<unsigned long>(buffer_size));
    523   });
    524 }
    525 
    526 bool JPEGFileLoader(RGBA8Image* image, std::string_view filename, std::FILE* fp)
    527 {
    528   static constexpr u32 BUFFER_SIZE = 16384;
    529 
    530   struct FileCallback
    531   {
    532     jpeg_source_mgr mgr;
    533 
    534     std::FILE* fp;
    535     std::unique_ptr<u8[]> buffer;
    536     bool end_of_file;
    537   };
    538 
    539   FileCallback cb = {
    540     .mgr = {
    541       .init_source = [](j_decompress_ptr cinfo) {},
    542       .fill_input_buffer = [](j_decompress_ptr cinfo) -> boolean {
    543         FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->src, FileCallback, mgr);
    544         cb->mgr.next_input_byte = cb->buffer.get();
    545         if (cb->end_of_file)
    546         {
    547           cb->buffer[0] = 0xFF;
    548           cb->buffer[1] = JPEG_EOI;
    549           cb->mgr.bytes_in_buffer = 2;
    550           return TRUE;
    551         }
    552 
    553         const size_t r = std::fread(cb->buffer.get(), 1, BUFFER_SIZE, cb->fp);
    554         cb->end_of_file |= (std::feof(cb->fp) != 0);
    555         cb->mgr.bytes_in_buffer = r;
    556         return TRUE;
    557       },
    558       .skip_input_data =
    559         [](j_decompress_ptr cinfo, long num_bytes) {
    560           FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->src, FileCallback, mgr);
    561           const size_t skip_in_buffer = std::min<size_t>(cb->mgr.bytes_in_buffer, static_cast<size_t>(num_bytes));
    562           cb->mgr.next_input_byte += skip_in_buffer;
    563           cb->mgr.bytes_in_buffer -= skip_in_buffer;
    564 
    565           const size_t seek_cur = static_cast<size_t>(num_bytes) - skip_in_buffer;
    566           if (seek_cur > 0)
    567           {
    568             if (FileSystem::FSeek64(cb->fp, static_cast<size_t>(seek_cur), SEEK_CUR) != 0)
    569             {
    570               cb->end_of_file = true;
    571               return;
    572             }
    573           }
    574         },
    575       .resync_to_restart = jpeg_resync_to_restart,
    576       .term_source = [](j_decompress_ptr cinfo) {},
    577     },
    578     .fp = fp,
    579     .buffer = std::make_unique<u8[]>(BUFFER_SIZE),
    580     .end_of_file = false,
    581   };
    582 
    583   return WrapJPEGDecompress(image, [&cb](jpeg_decompress_struct& info) { info.src = &cb.mgr; });
    584 }
    585 
    586 template<typename T>
    587 static bool WrapJPEGCompress(const RGBA8Image& image, u8 quality, T setup_func)
    588 {
    589   std::vector<u8> scanline;
    590   jpeg_compress_struct info = {};
    591 
    592   // NOTE: Be **very** careful not to allocate memory after calling this function.
    593   // It won't get freed, because fastjmp does not unwind the stack.
    594   JPEGErrorHandler errhandler;
    595   if (fastjmp_set(&errhandler.jbuf) != 0)
    596   {
    597     jpeg_destroy_compress(&info);
    598     return false;
    599   }
    600 
    601   info.err = &errhandler.err;
    602   jpeg_create_compress(&info);
    603   setup_func(info);
    604 
    605   info.image_width = image.GetWidth();
    606   info.image_height = image.GetHeight();
    607   info.in_color_space = JCS_RGB;
    608   info.input_components = 3;
    609 
    610   jpeg_set_defaults(&info);
    611   jpeg_set_quality(&info, quality, TRUE);
    612   jpeg_start_compress(&info, TRUE);
    613 
    614   scanline.resize(image.GetWidth() * 3);
    615   u8* scanline_buffer[1] = {scanline.data()};
    616   bool result = true;
    617   for (u32 y = 0; y < info.image_height; y++)
    618   {
    619     // RGBA -> RGB
    620     u8* dst_ptr = scanline.data();
    621     const u32* src_ptr = image.GetRowPixels(y);
    622     for (u32 x = 0; x < info.image_width; x++)
    623     {
    624       const u32 rgba = *(src_ptr++);
    625       *(dst_ptr++) = Truncate8(rgba);
    626       *(dst_ptr++) = Truncate8(rgba >> 8);
    627       *(dst_ptr++) = Truncate8(rgba >> 16);
    628     }
    629 
    630     if (jpeg_write_scanlines(&info, scanline_buffer, 1) != 1)
    631     {
    632       ERROR_LOG("jpeg_write_scanlines() failed at row {}", y);
    633       result = false;
    634       break;
    635     }
    636   }
    637 
    638   jpeg_finish_compress(&info);
    639   jpeg_destroy_compress(&info);
    640   return result;
    641 }
    642 
    643 bool JPEGBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality)
    644 {
    645   // give enough space to avoid reallocs
    646   buffer->resize(image.GetWidth() * image.GetHeight() * 2);
    647 
    648   struct MemCallback
    649   {
    650     jpeg_destination_mgr mgr;
    651     std::vector<u8>* buffer;
    652     size_t buffer_used;
    653   };
    654 
    655   MemCallback cb;
    656   cb.buffer = buffer;
    657   cb.buffer_used = 0;
    658   cb.mgr.next_output_byte = buffer->data();
    659   cb.mgr.free_in_buffer = buffer->size();
    660   cb.mgr.init_destination = [](j_compress_ptr cinfo) {};
    661   cb.mgr.empty_output_buffer = [](j_compress_ptr cinfo) -> boolean {
    662     MemCallback* cb = (MemCallback*)cinfo->dest;
    663 
    664     // double size
    665     cb->buffer_used = cb->buffer->size();
    666     cb->buffer->resize(cb->buffer->size() * 2);
    667     cb->mgr.next_output_byte = cb->buffer->data() + cb->buffer_used;
    668     cb->mgr.free_in_buffer = cb->buffer->size() - cb->buffer_used;
    669     return TRUE;
    670   };
    671   cb.mgr.term_destination = [](j_compress_ptr cinfo) {
    672     MemCallback* cb = (MemCallback*)cinfo->dest;
    673 
    674     // get final size
    675     cb->buffer->resize(cb->buffer->size() - cb->mgr.free_in_buffer);
    676   };
    677 
    678   return WrapJPEGCompress(image, quality, [&cb](jpeg_compress_struct& info) { info.dest = &cb.mgr; });
    679 }
    680 
    681 bool JPEGFileSaver(const RGBA8Image& image, std::string_view filename, std::FILE* fp, u8 quality)
    682 {
    683   static constexpr u32 BUFFER_SIZE = 16384;
    684 
    685   struct FileCallback
    686   {
    687     jpeg_destination_mgr mgr;
    688 
    689     std::FILE* fp;
    690     std::unique_ptr<u8[]> buffer;
    691     bool write_error;
    692   };
    693 
    694   FileCallback cb = {
    695     .mgr = {
    696       .init_destination =
    697         [](j_compress_ptr cinfo) {
    698           FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->dest, FileCallback, mgr);
    699           cb->mgr.next_output_byte = cb->buffer.get();
    700           cb->mgr.free_in_buffer = BUFFER_SIZE;
    701         },
    702       .empty_output_buffer = [](j_compress_ptr cinfo) -> boolean {
    703         FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->dest, FileCallback, mgr);
    704         if (!cb->write_error)
    705           cb->write_error |= (std::fwrite(cb->buffer.get(), 1, BUFFER_SIZE, cb->fp) != BUFFER_SIZE);
    706 
    707         cb->mgr.next_output_byte = cb->buffer.get();
    708         cb->mgr.free_in_buffer = BUFFER_SIZE;
    709         return TRUE;
    710       },
    711       .term_destination =
    712         [](j_compress_ptr cinfo) {
    713           FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->dest, FileCallback, mgr);
    714           const size_t left = BUFFER_SIZE - cb->mgr.free_in_buffer;
    715           if (left > 0 && !cb->write_error)
    716             cb->write_error |= (std::fwrite(cb->buffer.get(), 1, left, cb->fp) != left);
    717         },
    718     },
    719     .fp = fp,
    720     .buffer = std::make_unique<u8[]>(BUFFER_SIZE),
    721     .write_error = false,
    722   };
    723 
    724   return (WrapJPEGCompress(image, quality, [&cb](jpeg_compress_struct& info) { info.dest = &cb.mgr; }) &&
    725           !cb.write_error);
    726 }
    727 
    728 bool WebPBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
    729 {
    730   int width, height;
    731   if (!WebPGetInfo(static_cast<const u8*>(buffer), buffer_size, &width, &height) || width <= 0 || height <= 0)
    732   {
    733     ERROR_LOG("WebPGetInfo() failed");
    734     return false;
    735   }
    736 
    737   std::vector<u32> pixels;
    738   pixels.resize(static_cast<u32>(width) * static_cast<u32>(height));
    739   if (!WebPDecodeRGBAInto(static_cast<const u8*>(buffer), buffer_size, reinterpret_cast<u8*>(pixels.data()),
    740                           sizeof(u32) * pixels.size(), sizeof(u32) * static_cast<u32>(width)))
    741   {
    742     ERROR_LOG("WebPDecodeRGBAInto() failed");
    743     return false;
    744   }
    745 
    746   image->SetPixels(static_cast<u32>(width), static_cast<u32>(height), std::move(pixels));
    747   return true;
    748 }
    749 
    750 bool WebPBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality)
    751 {
    752   u8* encoded_data;
    753   const size_t encoded_size =
    754     WebPEncodeRGBA(reinterpret_cast<const u8*>(image.GetPixels()), image.GetWidth(), image.GetHeight(),
    755                    image.GetPitch(), static_cast<float>(quality), &encoded_data);
    756   if (encoded_size == 0)
    757     return false;
    758 
    759   buffer->resize(encoded_size);
    760   std::memcpy(buffer->data(), encoded_data, encoded_size);
    761   WebPFree(encoded_data);
    762   return true;
    763 }
    764 
    765 bool WebPFileLoader(RGBA8Image* image, std::string_view filename, std::FILE* fp)
    766 {
    767   std::optional<DynamicHeapArray<u8>> data = FileSystem::ReadBinaryFile(fp);
    768   if (!data.has_value())
    769     return false;
    770 
    771   return WebPBufferLoader(image, data->data(), data->size());
    772 }
    773 
    774 bool WebPFileSaver(const RGBA8Image& image, std::string_view filename, std::FILE* fp, u8 quality)
    775 {
    776   std::vector<u8> buffer;
    777   if (!WebPBufferSaver(image, &buffer, quality))
    778     return false;
    779 
    780   return (std::fwrite(buffer.data(), buffer.size(), 1, fp) == 1);
    781 }