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 }