diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index fee5aec79b1..75503cdd89c 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -363,7 +363,10 @@ TEST_CASE(test_png_malformed_frame) for (auto test_input : test_inputs) { auto file = TRY_OR_FAIL(Core::MappedFile::map(test_input)); - auto plugin_decoder = TRY_OR_FAIL(Gfx::PNGImageDecoderPlugin::create(file->bytes())); + auto plugin_decoder_or_error = Gfx::PNGImageDecoderPlugin::create(file->bytes()); + if (plugin_decoder_or_error.is_error()) + continue; + auto plugin_decoder = plugin_decoder_or_error.release_value(); auto frame_or_error = plugin_decoder->frame(0); EXPECT(frame_or_error.is_error()); } diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index 892af1ffaa3..4e2fb21beb8 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -103,3 +103,8 @@ find_package(JPEG REQUIRED) target_include_directories(LibGfx PRIVATE ${JPEG_INCLUDE_DIRS}) target_link_libraries(LibGfx PRIVATE ${JPEG_LIBRARIES}) target_link_directories(LibGfx PRIVATE ${JPEG_LIBRARY_DIRS}) + +find_package(PNG REQUIRED) +target_include_directories(LibGfx PRIVATE ${PNG_INCLUDE_DIRS}) +target_link_libraries(LibGfx PRIVATE ${PNG_LIBRARIES}) +target_link_directories(LibGfx PRIVATE ${PNG_LIBRARY_DIRS}) diff --git a/Userland/Libraries/LibGfx/ImageFormats/PNGLoader.cpp b/Userland/Libraries/LibGfx/ImageFormats/PNGLoader.cpp index a8c899d811b..6ab4734be0c 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/PNGLoader.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/PNGLoader.cpp @@ -1,1453 +1,303 @@ /* - * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2018-2024, Andreas Kling * Copyright (c) 2022, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include -#include #include -#include #include #include #include #include +#include namespace Gfx { -struct PNG_IHDR { - NetworkOrdered width; - NetworkOrdered height; - u8 bit_depth { 0 }; - PNG::ColorType color_type { 0 }; - u8 compression_method { 0 }; - u8 filter_method { 0 }; - u8 interlace_method { 0 }; -}; - -static_assert(AssertSize()); - -struct acTL_Chunk { - NetworkOrdered num_frames; - NetworkOrdered num_plays; -}; -static_assert(AssertSize()); - -struct fcTL_Chunk { - enum class DisposeOp : u8 { - APNG_DISPOSE_OP_NONE = 0, - APNG_DISPOSE_OP_BACKGROUND, - APNG_DISPOSE_OP_PREVIOUS - }; - enum class BlendOp : u8 { - APNG_BLEND_OP_SOURCE = 0, - APNG_BLEND_OP_OVER - }; - NetworkOrdered sequence_number; - NetworkOrdered width; - NetworkOrdered height; - NetworkOrdered x_offset; - NetworkOrdered y_offset; - NetworkOrdered delay_num; - NetworkOrdered delay_den; - DisposeOp dispose_op { DisposeOp::APNG_DISPOSE_OP_NONE }; - BlendOp blend_op { BlendOp::APNG_BLEND_OP_SOURCE }; -}; -static_assert(AssertSize()); - -struct ChromaticitiesAndWhitepoint { - NetworkOrdered white_point_x; - NetworkOrdered white_point_y; - NetworkOrdered red_x; - NetworkOrdered red_y; - NetworkOrdered green_x; - NetworkOrdered green_y; - NetworkOrdered blue_x; - NetworkOrdered blue_y; -}; -static_assert(AssertSize()); - -struct CodingIndependentCodePoints { - u8 color_primaries; - u8 transfer_function; - u8 matrix_coefficients; - u8 video_full_range_flag; -}; -static_assert(AssertSize()); - -struct EmbeddedICCProfile { - StringView profile_name; - ReadonlyBytes compressed_data; -}; - -struct Scanline { - PNG::FilterType filter; - ReadonlyBytes data {}; -}; - -struct [[gnu::packed]] PaletteEntry { - u8 r; - u8 g; - u8 b; - // u8 a; -}; - -template -struct [[gnu::packed]] Tuple { - T gray; - T a; -}; - -template -struct [[gnu::packed]] Triplet { - T r; - T g; - T b; - - bool operator==(Triplet const& other) const = default; -}; - -template -struct [[gnu::packed]] Quartet { - T r; - T g; - T b; - T a; -}; - -enum PngInterlaceMethod { - Null = 0, - Adam7 = 1 -}; - -enum RenderingIntent { - Perceptual = 0, - RelativeColorimetric = 1, - Saturation = 2, - AbsoluteColorimetric = 3, -}; - struct AnimationFrame { - fcTL_Chunk const& fcTL; RefPtr bitmap; - ByteBuffer compressed_data; + int x_offset { 0 }; + int y_offset { 0 }; + int width { 0 }; + int height { 0 }; + int delay_den { 0 }; + int delay_num { 0 }; + u8 blend_op { 0 }; + u8 dispose_op { 0 }; - AnimationFrame(fcTL_Chunk const& fcTL) - : fcTL(fcTL) + AnimationFrame(RefPtr bitmap, int x_offset, int y_offset, int width, int height, int delay_den, int delay_num, u8 blend_op, u8 dispose_op) + : bitmap(move(bitmap)) + , x_offset(x_offset) + , y_offset(y_offset) + , width(width) + , height(height) + , delay_den(delay_den) + , delay_num(delay_num) + , blend_op(blend_op) + , dispose_op(dispose_op) { } - u32 duration_ms() const + [[nodiscard]] int duration_ms() const { - u32 num = fcTL.delay_num; - if (num == 0) + if (delay_num == 0) return 1; - u32 denom = fcTL.delay_den != 0 ? static_cast(fcTL.delay_den) : 100u; - return (num * 1000) / denom; + u32 const denominator = delay_den != 0 ? static_cast(delay_den) : 100u; + auto unsigned_duration_ms = (delay_num * 1000) / denominator; + if (unsigned_duration_ms > INT_MAX) + return INT_MAX; + return static_cast(unsigned_duration_ms); } - IntRect rect() const - { - return { fcTL.x_offset, fcTL.y_offset, fcTL.width, fcTL.height }; - } + [[nodiscard]] IntRect rect() const { return { x_offset, y_offset, width, height }; } }; struct PNGLoadingContext { - enum State { - NotDecoded = 0, - Error, - IHDRDecoded, - ImageDataChunkDecoded, - ChunksDecoded, - BitmapDecoded, - }; - State state { State::NotDecoded }; - u8 const* data { nullptr }; - u8 const* data_current_ptr { nullptr }; - size_t data_size { 0 }; - i32 width { -1 }; - i32 height { -1 }; - u8 bit_depth { 0 }; - PNG::ColorType color_type { 0 }; - u8 compression_method { 0 }; - u8 filter_method { 0 }; - u8 interlace_method { 0 }; - u8 channels { 0 }; - u32 animation_next_expected_seq { 0 }; - u32 animation_next_frame_to_render { 0 }; - u32 animation_frame_count { 0 }; - u32 animation_loop_count { 0 }; - Optional last_completed_animation_frame_index; - bool is_first_idat_part_of_animation { false }; - bool has_seen_iend { false }; - bool has_seen_idat_chunk { false }; - bool has_seen_actl_chunk_before_idat { false }; - bool has_alpha() const { return to_underlying(color_type) & 4 || palette_transparency_data.size() > 0; } - Vector scanlines; - ByteBuffer unfiltered_data; - RefPtr bitmap; - ByteBuffer compressed_data; - Vector palette_data; - ByteBuffer palette_transparency_data; - Vector animation_frames; - - Optional chromaticities_and_whitepoint; - Optional coding_independent_code_points; - Optional gamma; - Optional embedded_icc_profile; - Optional decompressed_icc_profile; - Optional sRGB_rendering_intent; - + ReadonlyBytes data; + IntSize size; + u32 frame_count { 0 }; + u32 loop_count { 0 }; + Vector frame_descriptors; + Optional icc_profile; OwnPtr exif_metadata; - Checked compute_row_size_for_width(int width) - { - Checked row_size = width; - row_size *= channels; - row_size *= bit_depth; - row_size += 7; - row_size /= 8; - if (row_size.has_overflow()) { - dbgln("PNG too large, integer overflow while computing row size"); - state = State::Error; - } - return row_size; - } + Vector animation_frames; + Vector row_pointers; + Vector image_data; + RefPtr decoded_frame_bitmap; - PNGLoadingContext create_subimage_context(int width, int height) - { - PNGLoadingContext subimage_context; - subimage_context.state = State::ChunksDecoded; - subimage_context.width = width; - subimage_context.height = height; - subimage_context.channels = channels; - subimage_context.color_type = color_type; - subimage_context.palette_data = palette_data; - subimage_context.palette_transparency_data = palette_transparency_data; - subimage_context.bit_depth = bit_depth; - subimage_context.filter_method = filter_method; - return subimage_context; - } + ErrorOr read_frames(png_structp, png_infop); }; -class Streamer { -public: - Streamer(u8 const* data, size_t size) - : m_data_ptr(data) - , m_size_remaining(size) - { - } - - template - bool read(T& value) - { - if (m_size_remaining < sizeof(T)) - return false; - value = *((NetworkOrdered const*)m_data_ptr); - m_data_ptr += sizeof(T); - m_size_remaining -= sizeof(T); - return true; - } - - bool read_bytes(u8* buffer, size_t count) - { - if (m_size_remaining < count) - return false; - memcpy(buffer, m_data_ptr, count); - m_data_ptr += count; - m_size_remaining -= count; - return true; - } - - bool wrap_bytes(ReadonlyBytes& buffer, size_t count) - { - if (m_size_remaining < count) - return false; - buffer = ReadonlyBytes { m_data_ptr, count }; - m_data_ptr += count; - m_size_remaining -= count; - return true; - } - - u8 const* current_data_ptr() const { return m_data_ptr; } - bool at_end() const { return !m_size_remaining; } - -private: - u8 const* m_data_ptr { nullptr }; - size_t m_size_remaining { 0 }; -}; - -static ErrorOr process_chunk(Streamer&, PNGLoadingContext& context); - -union [[gnu::packed]] Pixel { - ARGB32 rgba { 0 }; - u8 v[4]; - struct { - u8 r; - u8 g; - u8 b; - u8 a; - }; -}; -static_assert(AssertSize()); - -void PNGImageDecoderPlugin::unfilter_scanline(PNG::FilterType filter, Bytes scanline_data, ReadonlyBytes previous_scanlines_data, u8 bytes_per_complete_pixel) +ErrorOr> PNGImageDecoderPlugin::create(ReadonlyBytes bytes) { - // https://www.w3.org/TR/png-3/#9Filter-types - // "Filters are applied to bytes, not to pixels, regardless of the bit depth or colour type of the image." - switch (filter) { - case PNG::FilterType::None: - break; - case PNG::FilterType::Sub: - // This loop starts at bytes_per_complete_pixel because all bytes before that are - // guaranteed to have no valid byte at index (i - bytes_per_complete pixel). - // All such invalid byte indexes should be treated as 0, and adding 0 to the current - // byte would do nothing, so the first bytes_per_complete_pixel bytes can instead - // just be skipped. - for (size_t i = bytes_per_complete_pixel; i < scanline_data.size(); ++i) { - u8 left = scanline_data[i - bytes_per_complete_pixel]; - scanline_data[i] += left; - } - break; - case PNG::FilterType::Up: - for (size_t i = 0; i < scanline_data.size(); ++i) { - u8 above = previous_scanlines_data[i]; - scanline_data[i] += above; - } - break; - case PNG::FilterType::Average: - for (size_t i = 0; i < scanline_data.size(); ++i) { - u32 left = (i < bytes_per_complete_pixel) ? 0 : scanline_data[i - bytes_per_complete_pixel]; - u32 above = previous_scanlines_data[i]; - u8 average = (left + above) / 2; - scanline_data[i] += average; - } - break; - case PNG::FilterType::Paeth: - for (size_t i = 0; i < scanline_data.size(); ++i) { - u8 left = (i < bytes_per_complete_pixel) ? 0 : scanline_data[i - bytes_per_complete_pixel]; - u8 above = previous_scanlines_data[i]; - u8 upper_left = (i < bytes_per_complete_pixel) ? 0 : previous_scanlines_data[i - bytes_per_complete_pixel]; - scanline_data[i] += PNG::paeth_predictor(left, above, upper_left); - } - break; - } + auto decoder = adopt_own(*new PNGImageDecoderPlugin(bytes)); + if (!TRY(decoder->initialize())) + return Error::from_string_literal("PNG load error"); + + return decoder; } -template -ALWAYS_INLINE static void unpack_grayscale_without_alpha(PNGLoadingContext& context) +PNGImageDecoderPlugin::PNGImageDecoderPlugin(ReadonlyBytes data) + : m_context(adopt_own(*new PNGLoadingContext)) { - for (int y = 0; y < context.height; ++y) { - auto* gray_values = reinterpret_cast(context.scanlines[y].data.data()); - for (int i = 0; i < context.width; ++i) { - auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; - pixel.r = gray_values[i]; - pixel.g = gray_values[i]; - pixel.b = gray_values[i]; - pixel.a = 0xff; - } - } -} - -template -ALWAYS_INLINE static void unpack_grayscale_with_alpha(PNGLoadingContext& context) -{ - for (int y = 0; y < context.height; ++y) { - auto* tuples = reinterpret_cast const*>(context.scanlines[y].data.data()); - for (int i = 0; i < context.width; ++i) { - auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; - pixel.r = tuples[i].gray; - pixel.g = tuples[i].gray; - pixel.b = tuples[i].gray; - pixel.a = tuples[i].a; - } - } -} - -template -ALWAYS_INLINE static void unpack_triplets_without_alpha(PNGLoadingContext& context) -{ - for (int y = 0; y < context.height; ++y) { - auto* triplets = reinterpret_cast const*>(context.scanlines[y].data.data()); - for (int i = 0; i < context.width; ++i) { - auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; - pixel.r = triplets[i].r; - pixel.g = triplets[i].g; - pixel.b = triplets[i].b; - pixel.a = 0xff; - } - } -} - -template -ALWAYS_INLINE static void unpack_triplets_with_transparency_value(PNGLoadingContext& context, Triplet transparency_value) -{ - for (int y = 0; y < context.height; ++y) { - auto* triplets = reinterpret_cast const*>(context.scanlines[y].data.data()); - for (int i = 0; i < context.width; ++i) { - auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; - pixel.r = triplets[i].r; - pixel.g = triplets[i].g; - pixel.b = triplets[i].b; - if (triplets[i] == transparency_value) - pixel.a = 0x00; - else - pixel.a = 0xff; - } - } -} - -NEVER_INLINE FLATTEN static ErrorOr unfilter(PNGLoadingContext& context) -{ - // First unfilter the scanlines: - - // FIXME: Instead of creating a separate buffer for the scanlines that need to be - // mutated, the mutation could be done in place (if the data was non-const). - size_t bytes_per_scanline = context.scanlines[0].data.size(); - size_t bytes_needed_for_all_unfiltered_scanlines = 0; - for (int y = 0; y < context.height; ++y) { - if (context.scanlines[y].filter != PNG::FilterType::None) { - bytes_needed_for_all_unfiltered_scanlines += bytes_per_scanline; - } - } - context.unfiltered_data = TRY(ByteBuffer::create_uninitialized(bytes_needed_for_all_unfiltered_scanlines)); - - // From section 6.3 of http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html - // "bpp is defined as the number of bytes per complete pixel, rounding up to one. - // For example, for color type 2 with a bit depth of 16, bpp is equal to 6 - // (three samples, two bytes per sample); for color type 0 with a bit depth of 2, - // bpp is equal to 1 (rounding up); for color type 4 with a bit depth of 16, bpp - // is equal to 4 (two-byte grayscale sample, plus two-byte alpha sample)." - u8 bytes_per_complete_pixel = ceil_div(context.bit_depth, (u8)8) * context.channels; - - u8 dummy_scanline_bytes[bytes_per_scanline]; - memset(dummy_scanline_bytes, 0, sizeof(dummy_scanline_bytes)); - auto previous_scanlines_data = ReadonlyBytes { dummy_scanline_bytes, sizeof(dummy_scanline_bytes) }; - - for (int y = 0, data_start = 0; y < context.height; ++y) { - if (context.scanlines[y].filter != PNG::FilterType::None) { - auto scanline_data_slice = context.unfiltered_data.bytes().slice(data_start, bytes_per_scanline); - - // Copy the current values over and set the scanline's data to the to-be-mutated slice - context.scanlines[y].data.copy_to(scanline_data_slice); - context.scanlines[y].data = scanline_data_slice; - - PNGImageDecoderPlugin::unfilter_scanline(context.scanlines[y].filter, scanline_data_slice, previous_scanlines_data, bytes_per_complete_pixel); - - data_start += bytes_per_scanline; - } - previous_scanlines_data = context.scanlines[y].data; - } - - // Now unpack the scanlines to RGBA: - switch (context.color_type) { - case PNG::ColorType::Greyscale: - if (context.bit_depth == 8) { - unpack_grayscale_without_alpha(context); - } else if (context.bit_depth == 16) { - unpack_grayscale_without_alpha(context); - } else if (context.bit_depth == 1 || context.bit_depth == 2 || context.bit_depth == 4) { - auto bit_depth_squared = context.bit_depth * context.bit_depth; - auto pixels_per_byte = 8 / context.bit_depth; - auto mask = (1 << context.bit_depth) - 1; - for (int y = 0; y < context.height; ++y) { - auto* gray_values = context.scanlines[y].data.data(); - for (int x = 0; x < context.width; ++x) { - auto bit_offset = (8 - context.bit_depth) - (context.bit_depth * (x % pixels_per_byte)); - auto value = (gray_values[x / pixels_per_byte] >> bit_offset) & mask; - auto& pixel = (Pixel&)context.bitmap->scanline(y)[x]; - pixel.r = value * (0xff / bit_depth_squared); - pixel.g = value * (0xff / bit_depth_squared); - pixel.b = value * (0xff / bit_depth_squared); - pixel.a = 0xff; - } - } - } else { - VERIFY_NOT_REACHED(); - } - break; - case PNG::ColorType::GreyscaleWithAlpha: - if (context.bit_depth == 8) { - unpack_grayscale_with_alpha(context); - } else if (context.bit_depth == 16) { - unpack_grayscale_with_alpha(context); - } else { - VERIFY_NOT_REACHED(); - } - break; - case PNG::ColorType::Truecolor: - if (context.palette_transparency_data.size() == 6) { - if (context.bit_depth == 8) { - unpack_triplets_with_transparency_value(context, Triplet { context.palette_transparency_data[0], context.palette_transparency_data[2], context.palette_transparency_data[4] }); - } else if (context.bit_depth == 16) { - u16 tr = context.palette_transparency_data[0] | context.palette_transparency_data[1] << 8; - u16 tg = context.palette_transparency_data[2] | context.palette_transparency_data[3] << 8; - u16 tb = context.palette_transparency_data[4] | context.palette_transparency_data[5] << 8; - unpack_triplets_with_transparency_value(context, Triplet { tr, tg, tb }); - } else { - VERIFY_NOT_REACHED(); - } - } else { - if (context.bit_depth == 8) - unpack_triplets_without_alpha(context); - else if (context.bit_depth == 16) - unpack_triplets_without_alpha(context); - else - VERIFY_NOT_REACHED(); - } - break; - case PNG::ColorType::TruecolorWithAlpha: - if (context.bit_depth == 8) { - for (int y = 0; y < context.height; ++y) { - memcpy(context.bitmap->scanline(y), context.scanlines[y].data.data(), context.scanlines[y].data.size()); - } - } else if (context.bit_depth == 16) { - for (int y = 0; y < context.height; ++y) { - auto* quartets = reinterpret_cast const*>(context.scanlines[y].data.data()); - for (int i = 0; i < context.width; ++i) { - auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; - pixel.r = quartets[i].r & 0xFF; - pixel.g = quartets[i].g & 0xFF; - pixel.b = quartets[i].b & 0xFF; - pixel.a = quartets[i].a & 0xFF; - } - } - } else { - VERIFY_NOT_REACHED(); - } - break; - case PNG::ColorType::IndexedColor: - if (context.bit_depth == 8) { - for (int y = 0; y < context.height; ++y) { - auto* palette_index = context.scanlines[y].data.data(); - for (int i = 0; i < context.width; ++i) { - auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; - if (palette_index[i] >= context.palette_data.size()) - return Error::from_string_literal("PNGImageDecoderPlugin: Palette index out of range"); - auto& color = context.palette_data.at((int)palette_index[i]); - auto transparency = context.palette_transparency_data.size() >= palette_index[i] + 1u - ? context.palette_transparency_data[palette_index[i]] - : 0xff; - pixel.r = color.r; - pixel.g = color.g; - pixel.b = color.b; - pixel.a = transparency; - } - } - } else if (context.bit_depth == 1 || context.bit_depth == 2 || context.bit_depth == 4) { - auto pixels_per_byte = 8 / context.bit_depth; - auto mask = (1 << context.bit_depth) - 1; - for (int y = 0; y < context.height; ++y) { - auto* palette_indices = context.scanlines[y].data.data(); - for (int i = 0; i < context.width; ++i) { - auto bit_offset = (8 - context.bit_depth) - (context.bit_depth * (i % pixels_per_byte)); - auto palette_index = (palette_indices[i / pixels_per_byte] >> bit_offset) & mask; - auto& pixel = (Pixel&)context.bitmap->scanline(y)[i]; - if ((size_t)palette_index >= context.palette_data.size()) - return Error::from_string_literal("PNGImageDecoderPlugin: Palette index out of range"); - auto& color = context.palette_data.at(palette_index); - auto transparency = context.palette_transparency_data.size() >= palette_index + 1u - ? context.palette_transparency_data[palette_index] - : 0xff; - pixel.r = color.r; - pixel.g = color.g; - pixel.b = color.b; - pixel.a = transparency; - } - } - } else { - VERIFY_NOT_REACHED(); - } - break; - default: - VERIFY_NOT_REACHED(); - break; - } - - // Swap r and b values: - for (int y = 0; y < context.height; ++y) { - auto* pixels = (Pixel*)context.bitmap->scanline(y); - for (int i = 0; i < context.bitmap->width(); ++i) { - auto& x = pixels[i]; - swap(x.r, x.b); - } - } - - return {}; -} - -static bool decode_png_header(PNGLoadingContext& context) -{ - if (!context.data || context.data_size < sizeof(PNG::header)) { - dbgln_if(PNG_DEBUG, "Missing PNG header"); - context.state = PNGLoadingContext::State::Error; - return false; - } - - if (memcmp(context.data, PNG::header.span().data(), sizeof(PNG::header)) != 0) { - dbgln_if(PNG_DEBUG, "Invalid PNG header"); - context.state = PNGLoadingContext::State::Error; - return false; - } - - context.data_current_ptr = context.data + sizeof(PNG::header); - return true; -} - -static ErrorOr decode_png_ihdr(PNGLoadingContext& context) -{ - size_t data_remaining = context.data_size - (context.data_current_ptr - context.data); - - Streamer streamer(context.data_current_ptr, data_remaining); - - // https://www.w3.org/TR/png/#11IHDR - // The IHDR chunk shall be the first chunk in the PNG datastream. - TRY(process_chunk(streamer, context)); - - context.data_current_ptr = streamer.current_data_ptr(); - - VERIFY(context.state == PNGLoadingContext::State::IHDRDecoded); - return {}; -} - -static bool decode_png_image_data_chunk(PNGLoadingContext& context) -{ - VERIFY(context.state >= PNGLoadingContext::IHDRDecoded); - - if (context.state >= PNGLoadingContext::ImageDataChunkDecoded) - return true; - - size_t data_remaining = context.data_size - (context.data_current_ptr - context.data); - - Streamer streamer(context.data_current_ptr, data_remaining); - while (!streamer.at_end() && !context.has_seen_iend) { - if (auto result = process_chunk(streamer, context); result.is_error()) { - context.state = PNGLoadingContext::State::Error; - return false; - } - - context.data_current_ptr = streamer.current_data_ptr(); - - if (context.state >= PNGLoadingContext::State::ImageDataChunkDecoded) - return true; - } - - return false; -} - -static bool decode_png_animation_data_chunks(PNGLoadingContext& context, u32 requested_animation_frame_index) -{ - if (context.state >= PNGLoadingContext::ImageDataChunkDecoded) { - if (context.last_completed_animation_frame_index.has_value()) { - if (requested_animation_frame_index <= context.last_completed_animation_frame_index.value()) - return true; - } - } else if (!decode_png_image_data_chunk(context)) { - return false; - } - - size_t data_remaining = context.data_size - (context.data_current_ptr - context.data); - - Streamer streamer(context.data_current_ptr, data_remaining); - while (!streamer.at_end() && !context.has_seen_iend) { - if (auto result = process_chunk(streamer, context); result.is_error()) { - context.state = PNGLoadingContext::State::Error; - return false; - } - - context.data_current_ptr = streamer.current_data_ptr(); - - if (context.last_completed_animation_frame_index.has_value()) { - if (requested_animation_frame_index <= context.last_completed_animation_frame_index.value()) - break; - } - } - - if (!context.last_completed_animation_frame_index.has_value()) - return false; - return requested_animation_frame_index <= context.last_completed_animation_frame_index.value(); -} - -static bool decode_png_chunks(PNGLoadingContext& context) -{ - VERIFY(context.state >= PNGLoadingContext::IHDRDecoded); - - if (context.state >= PNGLoadingContext::State::ChunksDecoded) - return true; - - size_t data_remaining = context.data_size - (context.data_current_ptr - context.data); - - context.compressed_data.ensure_capacity(context.data_size); - - Streamer streamer(context.data_current_ptr, data_remaining); - while (!streamer.at_end() && !context.has_seen_iend) { - if (auto result = process_chunk(streamer, context); result.is_error()) { - // Ignore failed chunk and just consider chunk decoding being done. - // decode_png_bitmap() will check whether we got all required ones anyway. - break; - } - - context.data_current_ptr = streamer.current_data_ptr(); - } - - context.state = PNGLoadingContext::State::ChunksDecoded; - return true; -} - -static ErrorOr decode_png_bitmap_simple(PNGLoadingContext& context, ByteBuffer& decompression_buffer) -{ - Streamer streamer(decompression_buffer.data(), decompression_buffer.size()); - - for (int y = 0; y < context.height; ++y) { - u8 filter_byte; - if (!streamer.read(filter_byte)) { - context.state = PNGLoadingContext::State::Error; - return Error::from_string_literal("PNGImageDecoderPlugin: Decoding failed"); - } - - if (filter_byte > 4) { - context.state = PNGLoadingContext::State::Error; - return Error::from_string_literal("PNGImageDecoderPlugin: Invalid PNG filter"); - } - - context.scanlines.append({ MUST(PNG::filter_type(filter_byte)) }); - auto& scanline_buffer = context.scanlines.last().data; - auto row_size = context.compute_row_size_for_width(context.width); - if (row_size.has_overflow()) - return Error::from_string_literal("PNGImageDecoderPlugin: Row size overflow"); - - if (!streamer.wrap_bytes(scanline_buffer, row_size.value())) { - context.state = PNGLoadingContext::State::Error; - return Error::from_string_literal("PNGImageDecoderPlugin: Decoding failed"); - } - } - - context.bitmap = TRY(Bitmap::create(context.has_alpha() ? BitmapFormat::BGRA8888 : BitmapFormat::BGRx8888, { context.width, context.height })); - return unfilter(context); -} - -static int adam7_height(PNGLoadingContext& context, int pass) -{ - switch (pass) { - case 1: - return (context.height + 7) / 8; - case 2: - return (context.height + 7) / 8; - case 3: - return (context.height + 3) / 8; - case 4: - return (context.height + 3) / 4; - case 5: - return (context.height + 1) / 4; - case 6: - return (context.height + 1) / 2; - case 7: - return context.height / 2; - default: - VERIFY_NOT_REACHED(); - } -} - -static int adam7_width(PNGLoadingContext& context, int pass) -{ - switch (pass) { - case 1: - return (context.width + 7) / 8; - case 2: - return (context.width + 3) / 8; - case 3: - return (context.width + 3) / 4; - case 4: - return (context.width + 1) / 4; - case 5: - return (context.width + 1) / 2; - case 6: - return context.width / 2; - case 7: - return context.width; - default: - VERIFY_NOT_REACHED(); - } -} - -// Index 0 unused (non-interlaced case) -static int adam7_starty[8] = { 0, 0, 0, 4, 0, 2, 0, 1 }; -static int adam7_startx[8] = { 0, 0, 4, 0, 2, 0, 1, 0 }; -static int adam7_stepy[8] = { 1, 8, 8, 8, 4, 4, 2, 2 }; -static int adam7_stepx[8] = { 1, 8, 8, 4, 4, 2, 2, 1 }; - -static ErrorOr decode_adam7_pass(PNGLoadingContext& context, Streamer& streamer, int pass) -{ - auto subimage_context = context.create_subimage_context(adam7_width(context, pass), adam7_height(context, pass)); - - // For small images, some passes might be empty - if (!subimage_context.width || !subimage_context.height) - return {}; - - for (int y = 0; y < subimage_context.height; ++y) { - u8 filter_byte; - if (!streamer.read(filter_byte)) { - context.state = PNGLoadingContext::State::Error; - return Error::from_string_literal("PNGImageDecoderPlugin: Decoding failed"); - } - - if (filter_byte > 4) { - context.state = PNGLoadingContext::State::Error; - return Error::from_string_literal("PNGImageDecoderPlugin: Invalid PNG filter"); - } - - subimage_context.scanlines.append({ MUST(PNG::filter_type(filter_byte)) }); - auto& scanline_buffer = subimage_context.scanlines.last().data; - - auto row_size = context.compute_row_size_for_width(subimage_context.width); - if (row_size.has_overflow()) - return Error::from_string_literal("PNGImageDecoderPlugin: Row size overflow"); - if (!streamer.wrap_bytes(scanline_buffer, row_size.value())) { - context.state = PNGLoadingContext::State::Error; - return Error::from_string_literal("PNGImageDecoderPlugin: Decoding failed"); - } - } - - subimage_context.bitmap = TRY(Bitmap::create(context.bitmap->format(), { subimage_context.width, subimage_context.height })); - TRY(unfilter(subimage_context)); - - // Copy the subimage data into the main image according to the pass pattern - for (int y = 0, dy = adam7_starty[pass]; y < subimage_context.height && dy < context.height; ++y, dy += adam7_stepy[pass]) { - for (int x = 0, dx = adam7_startx[pass]; x < subimage_context.width && dx < context.width; ++x, dx += adam7_stepx[pass]) { - context.bitmap->set_pixel(dx, dy, subimage_context.bitmap->get_pixel(x, y)); - } - } - return {}; -} - -static ErrorOr decode_png_adam7(PNGLoadingContext& context, ByteBuffer& decompression_buffer) -{ - Streamer streamer(decompression_buffer.data(), decompression_buffer.size()); - context.bitmap = TRY(Bitmap::create(context.has_alpha() ? BitmapFormat::BGRA8888 : BitmapFormat::BGRx8888, { context.width, context.height })); - for (int pass = 1; pass <= 7; ++pass) - TRY(decode_adam7_pass(context, streamer, pass)); - return {}; -} - -static ErrorOr decode_png_bitmap(PNGLoadingContext& context) -{ - if (context.state < PNGLoadingContext::State::ChunksDecoded) { - if (!decode_png_chunks(context)) - return Error::from_string_literal("PNGImageDecoderPlugin: Decoding failed"); - } - - if (context.state >= PNGLoadingContext::State::BitmapDecoded) - return {}; - - if (context.color_type == PNG::ColorType::IndexedColor && context.palette_data.is_empty()) - return Error::from_string_literal("PNGImageDecoderPlugin: Didn't see a PLTE chunk for a palletized image, or it was empty."); - - auto compressed_data_stream = make(context.compressed_data.span()); - auto decompressor_or_error = Compress::ZlibDecompressor::create(move(compressed_data_stream)); - if (decompressor_or_error.is_error()) { - context.state = PNGLoadingContext::State::Error; - return decompressor_or_error.release_error(); - } - auto decompressor = decompressor_or_error.release_value(); - auto result_or_error = decompressor->read_until_eof(); - if (result_or_error.is_error()) { - context.state = PNGLoadingContext::State::Error; - return result_or_error.release_error(); - } - auto decompression_buffer = result_or_error.release_value(); - context.compressed_data.clear(); - - context.scanlines.ensure_capacity(context.height); - switch (context.interlace_method) { - case PngInterlaceMethod::Null: - TRY(decode_png_bitmap_simple(context, decompression_buffer)); - break; - case PngInterlaceMethod::Adam7: - TRY(decode_png_adam7(context, decompression_buffer)); - break; - default: - context.state = PNGLoadingContext::State::Error; - return Error::from_string_literal("PNGImageDecoderPlugin: Invalid interlace method"); - } - - context.state = PNGLoadingContext::State::BitmapDecoded; - return {}; -} - -static ErrorOr> decode_png_animation_frame_bitmap(PNGLoadingContext& context, AnimationFrame& animation_frame) -{ - if (context.color_type == PNG::ColorType::IndexedColor && context.palette_data.is_empty()) - return Error::from_string_literal("PNGImageDecoderPlugin: Didn't see a PLTE chunk for a palletized image, or it was empty."); - - VERIFY(!animation_frame.bitmap); - - auto frame_rect = animation_frame.rect(); - auto frame_context = context.create_subimage_context(frame_rect.width(), frame_rect.height()); - - auto compressed_data_stream = make(animation_frame.compressed_data.span()); - auto decompressor = TRY(Compress::ZlibDecompressor::create(move(compressed_data_stream))); - auto decompression_buffer = TRY(decompressor->read_until_eof()); - frame_context.compressed_data.clear(); - - frame_context.scanlines.ensure_capacity(frame_context.height); - switch (context.interlace_method) { - case PngInterlaceMethod::Null: - TRY(decode_png_bitmap_simple(frame_context, decompression_buffer)); - break; - case PngInterlaceMethod::Adam7: - TRY(decode_png_adam7(frame_context, decompression_buffer)); - break; - default: - return Error::from_string_literal("PNGImageDecoderPlugin: Invalid interlace method"); - } - - context.state = PNGLoadingContext::State::BitmapDecoded; - return move(frame_context.bitmap); -} - -static bool is_valid_compression_method(u8 compression_method) -{ - return compression_method == 0; -} - -static bool is_valid_filter_method(u8 filter_method) -{ - return filter_method == 0; -} - -static ErrorOr process_IHDR(ReadonlyBytes data, PNGLoadingContext& context) -{ - if (data.size() < (int)sizeof(PNG_IHDR)) - return Error::from_string_literal("IHDR chunk has an abnormal size"); - - auto const& ihdr = *(const PNG_IHDR*)data.data(); - - if (ihdr.width == 0 || ihdr.width > NumericLimits::max()) { - dbgln("PNG has invalid width {}", ihdr.width); - return Error::from_string_literal("Invalid width"); - } - - if (ihdr.height == 0 || ihdr.height > NumericLimits::max()) { - dbgln("PNG has invalid height {}", ihdr.height); - return Error::from_string_literal("Invalid height"); - } - - if (!is_valid_compression_method(ihdr.compression_method)) { - dbgln("PNG has invalid compression method {}", ihdr.compression_method); - return Error::from_string_literal("Unsupported compression method"); - } - - if (!is_valid_filter_method(ihdr.filter_method)) { - dbgln("PNG has invalid filter method {}", ihdr.filter_method); - return Error::from_string_literal("Unsupported filter method"); - } - - context.width = ihdr.width; - context.height = ihdr.height; - context.bit_depth = ihdr.bit_depth; - context.color_type = ihdr.color_type; - context.compression_method = ihdr.compression_method; - context.filter_method = ihdr.filter_method; - context.interlace_method = ihdr.interlace_method; - - dbgln_if(PNG_DEBUG, "PNG: {}x{} ({} bpp)", context.width, context.height, context.bit_depth); - dbgln_if(PNG_DEBUG, " Color type: {}", to_underlying(context.color_type)); - dbgln_if(PNG_DEBUG, "Compress Method: {}", context.compression_method); - dbgln_if(PNG_DEBUG, " Filter Method: {}", context.filter_method); - dbgln_if(PNG_DEBUG, " Interlace type: {}", context.interlace_method); - - if (context.interlace_method != PngInterlaceMethod::Null && context.interlace_method != PngInterlaceMethod::Adam7) { - dbgln_if(PNG_DEBUG, "PNGLoader::process_IHDR: unknown interlace method: {}", context.interlace_method); - return Error::from_string_literal("Unsupported interlacing method"); - } - - switch (context.color_type) { - case PNG::ColorType::Greyscale: - if (context.bit_depth != 1 && context.bit_depth != 2 && context.bit_depth != 4 && context.bit_depth != 8 && context.bit_depth != 16) - return Error::from_string_literal("Unsupported bit depth for a greyscale image"); - context.channels = 1; - break; - case PNG::ColorType::GreyscaleWithAlpha: - if (context.bit_depth != 8 && context.bit_depth != 16) - return Error::from_string_literal("Unsupported bit depth for a greyscale image with alpha"); - context.channels = 2; - break; - case PNG::ColorType::Truecolor: - if (context.bit_depth != 8 && context.bit_depth != 16) - return Error::from_string_literal("Unsupported bit depth for a true color image"); - context.channels = 3; - break; - case PNG::ColorType::IndexedColor: - if (context.bit_depth != 1 && context.bit_depth != 2 && context.bit_depth != 4 && context.bit_depth != 8) - return Error::from_string_literal("Unsupported bit depth for a indexed color image"); - context.channels = 1; - break; - case PNG::ColorType::TruecolorWithAlpha: - if (context.bit_depth != 8 && context.bit_depth != 16) - return Error::from_string_literal("Unsupported bit depth for a true color image with alpha"); - context.channels = 4; - break; - default: - return Error::from_string_literal("Unsupported color type"); - } - - context.state = PNGLoadingContext::IHDRDecoded; - - return {}; -} - -static ErrorOr process_IDAT(ReadonlyBytes data, PNGLoadingContext& context) -{ - context.compressed_data.append(data); - if (context.state < PNGLoadingContext::State::ImageDataChunkDecoded) - context.state = PNGLoadingContext::State::ImageDataChunkDecoded; - return {}; -} - -static ErrorOr process_PLTE(ReadonlyBytes data, PNGLoadingContext& context) -{ - TRY(context.palette_data.try_append((PaletteEntry const*)data.data(), data.size() / 3)); - return {}; -} - -static ErrorOr process_tRNS(ReadonlyBytes data, PNGLoadingContext& context) -{ - switch (context.color_type) { - case PNG::ColorType::Greyscale: - case PNG::ColorType::Truecolor: - case PNG::ColorType::IndexedColor: - TRY(context.palette_transparency_data.try_append(data)); - break; - default: - break; - } - return {}; -} - -static ErrorOr process_cHRM(ReadonlyBytes data, PNGLoadingContext& context) -{ - // https://www.w3.org/TR/png/#11cHRM - if (data.size() != 32) - return Error::from_string_literal("cHRM chunk has an abnormal size"); - context.chromaticities_and_whitepoint = *bit_cast(data.data()); - return {}; -} - -static ErrorOr process_cICP(ReadonlyBytes data, PNGLoadingContext& context) -{ - // https://www.w3.org/TR/png/#cICP-chunk - if (data.size() != 4) - return Error::from_string_literal("cICP chunk has an abnormal size"); - context.coding_independent_code_points = *bit_cast(data.data()); - return {}; -} - -static ErrorOr process_iCCP(ReadonlyBytes data, PNGLoadingContext& context) -{ - // https://www.w3.org/TR/png/#11iCCP - size_t profile_name_length_max = min(80u, data.size()); - size_t profile_name_length = strnlen((char const*)data.data(), profile_name_length_max); - if (profile_name_length == 0 || profile_name_length == profile_name_length_max) - return Error::from_string_literal("iCCP chunk does not contain a profile name"); - - if (data.size() < profile_name_length + 2) - return Error::from_string_literal("iCCP chunk is too small"); - - u8 compression_method = data[profile_name_length + 1]; - if (compression_method != 0) - return Error::from_string_literal("Unsupported compression method in the iCCP chunk"); - - context.embedded_icc_profile = EmbeddedICCProfile { { data.data(), profile_name_length }, data.slice(profile_name_length + 2) }; - - return {}; -} - -static ErrorOr process_gAMA(ReadonlyBytes data, PNGLoadingContext& context) -{ - // https://www.w3.org/TR/png/#11gAMA - if (data.size() != 4) - return Error::from_string_literal("gAMA chunk has an abnormal size"); - - u32 gamma = *bit_cast const*>(data.data()); - if (gamma & 0x8000'0000) - return Error::from_string_literal("Gamma value is too high"); - context.gamma = gamma; - - return {}; -} - -static ErrorOr process_sRGB(ReadonlyBytes data, PNGLoadingContext& context) -{ - // https://www.w3.org/TR/png/#srgb-standard-colour-space - if (data.size() != 1) { - // Invalid per spec, but (rarely) happens in the wild. Log and ignore. - warnln("warning: PNG sRGB chunk has an abnormal size; ignoring"); - return {}; - } - - u8 rendering_intent = data[0]; - if (rendering_intent > 3) - return Error::from_string_literal("Unsupported rendering intent"); - - context.sRGB_rendering_intent = (RenderingIntent)rendering_intent; - - return {}; -} - -static ErrorOr process_acTL(ReadonlyBytes data, PNGLoadingContext& context) -{ - // https://www.w3.org/TR/png/#acTL-chunk - if (context.has_seen_idat_chunk) - return {}; // Ignore if we encounter it after the first idat - if (data.size() != sizeof(acTL_Chunk)) - return Error::from_string_literal("acTL chunk has an abnormal size"); - - auto const& acTL = *bit_cast(data.data()); - context.animation_frame_count = acTL.num_frames; - context.animation_loop_count = acTL.num_plays; - context.has_seen_actl_chunk_before_idat = true; - TRY(context.animation_frames.try_ensure_capacity(context.animation_frame_count)); - return {}; -} - -static ErrorOr process_fcTL(ReadonlyBytes data, PNGLoadingContext& context) -{ - // https://www.w3.org/TR/png/#fcTL-chunk - if (!context.has_seen_actl_chunk_before_idat) - return {}; // Ignore if it's not a valid animated png - - if (data.size() != sizeof(fcTL_Chunk)) - return Error::from_string_literal("fcTL chunk has an abnormal size"); - - auto const& fcTL = *bit_cast(data.data()); - if (fcTL.sequence_number != context.animation_next_expected_seq) - return Error::from_string_literal("Unexpected sequence number"); - - context.animation_next_expected_seq++; - - if (fcTL.width == 0 || fcTL.height == 0) - return Error::from_string_literal("width and height must be greater than zero in fcTL chunk"); - - Checked left { static_cast(fcTL.x_offset) }; - Checked top { static_cast(fcTL.y_offset) }; - Checked width { static_cast(fcTL.width) }; - Checked height { static_cast(fcTL.height) }; - auto right = left + width; - auto bottom = top + height; - if (left < 0 || width <= 0 || right.has_overflow() || right > context.width) - return Error::from_string_literal("Invalid x_offset value in fcTL chunk"); - if (top < 0 || height <= 0 || bottom.has_overflow() || bottom > context.height) - return Error::from_string_literal("Invalid y_offset value in fcTL chunk"); - - bool is_first_animation_frame = context.animation_frames.is_empty(); - if (!is_first_animation_frame) - context.last_completed_animation_frame_index = context.animation_frames.size() - 1; - - context.animation_frames.append({ fcTL }); - - if (!context.has_seen_idat_chunk && is_first_animation_frame) - context.is_first_idat_part_of_animation = true; - return {}; -} - -static ErrorOr process_fdAT(ReadonlyBytes data, PNGLoadingContext& context) -{ - // https://www.w3.org/TR/png/#fdAT-chunk - - if (data.size() <= 4) - return Error::from_string_literal("fdAT chunk has an abnormal size"); - - u32 sequence_number = *bit_cast const*>(data.data()); - if (sequence_number != context.animation_next_expected_seq) - return Error::from_string_literal("Unexpected sequence number"); - context.animation_next_expected_seq++; - - if (context.animation_frames.is_empty()) - return Error::from_string_literal("No frame available"); - auto& current_animation_frame = context.animation_frames[context.animation_frames.size() - 1]; - auto compressed_data = data.slice(4); - current_animation_frame.compressed_data.append(compressed_data.data(), compressed_data.size()); - return {}; -} - -static ErrorOr process_eXIf(ReadonlyBytes bytes, PNGLoadingContext& context) -{ - context.exif_metadata = TRY(TIFFImageDecoderPlugin::read_exif_metadata(bytes)); - return {}; -} - -static void process_IEND(ReadonlyBytes, PNGLoadingContext& context) -{ - // https://www.w3.org/TR/png/#11IEND - if (context.has_seen_actl_chunk_before_idat) - context.last_completed_animation_frame_index = context.animation_frames.size(); - - context.has_seen_iend = true; -} - -static ErrorOr process_chunk(Streamer& streamer, PNGLoadingContext& context) -{ - u32 chunk_size; - if (!streamer.read(chunk_size)) { - dbgln_if(PNG_DEBUG, "Bail at chunk_size"); - return Error::from_string_literal("Error while reading from Streamer"); - } - - Array chunk_type_buffer; - StringView const chunk_type { chunk_type_buffer.span() }; - if (!streamer.read_bytes(chunk_type_buffer.data(), chunk_type_buffer.size())) { - dbgln_if(PNG_DEBUG, "Bail at chunk_type"); - return Error::from_string_literal("Error while reading from Streamer"); - } - ReadonlyBytes chunk_data; - if (!streamer.wrap_bytes(chunk_data, chunk_size)) { - dbgln_if(PNG_DEBUG, "Bail at chunk_data"); - return Error::from_string_literal("Error while reading from Streamer"); - } - u32 chunk_crc; - if (!streamer.read(chunk_crc)) { - dbgln_if(PNG_DEBUG, "Bail at chunk_crc"); - return Error::from_string_literal("Error while reading from Streamer"); - } - dbgln_if(PNG_DEBUG, "Chunk type: '{}', size: {}, crc: {:x}", chunk_type, chunk_size, chunk_crc); - - if (chunk_type == "IHDR"sv) { - if (context.state >= PNGLoadingContext::IHDRDecoded) - return Error::from_string_literal("Multiple IHDR chunks"); - - return process_IHDR(chunk_data, context); - } - - if (context.state < PNGLoadingContext::IHDRDecoded) - return Error::from_string_literal("IHDR is not the first chunk of the file"); - - if (chunk_type == "IDAT"sv) - return process_IDAT(chunk_data, context); - if (chunk_type == "PLTE"sv) - return process_PLTE(chunk_data, context); - if (chunk_type == "cHRM"sv) - return process_cHRM(chunk_data, context); - if (chunk_type == "cICP"sv) - return process_cICP(chunk_data, context); - if (chunk_type == "iCCP"sv) - return process_iCCP(chunk_data, context); - if (chunk_type == "gAMA"sv) - return process_gAMA(chunk_data, context); - if (chunk_type == "sRGB"sv) - return process_sRGB(chunk_data, context); - if (chunk_type == "tRNS"sv) - return process_tRNS(chunk_data, context); - if (chunk_type == "acTL"sv) - return process_acTL(chunk_data, context); - if (chunk_type == "fcTL"sv) - return process_fcTL(chunk_data, context); - if (chunk_type == "fdAT"sv) - return process_fdAT(chunk_data, context); - if (chunk_type == "eXIf"sv) - return process_eXIf(chunk_data, context); - if (chunk_type == "IEND"sv) - process_IEND(chunk_data, context); - return {}; -} - -PNGImageDecoderPlugin::PNGImageDecoderPlugin(u8 const* data, size_t size) -{ - m_context = make(); - m_context->data = m_context->data_current_ptr = data; - m_context->data_size = size; -} - -PNGImageDecoderPlugin::~PNGImageDecoderPlugin() = default; - -bool PNGImageDecoderPlugin::ensure_image_data_chunk_was_decoded() -{ - if (m_context->state == PNGLoadingContext::State::Error) - return false; - - if (m_context->state < PNGLoadingContext::State::ImageDataChunkDecoded) { - if (!decode_png_image_data_chunk(*m_context)) - return false; - } - return true; -} - -bool PNGImageDecoderPlugin::ensure_animation_frame_was_decoded(u32 animation_frame_index) -{ - if (m_context->state == PNGLoadingContext::State::Error) - return false; - - if (m_context->state < PNGLoadingContext::State::ImageDataChunkDecoded) { - if (!decode_png_image_data_chunk(*m_context)) - return false; - } - - if (m_context->last_completed_animation_frame_index.has_value()) { - if (m_context->last_completed_animation_frame_index.value() >= animation_frame_index) - return true; - } - - return decode_png_animation_data_chunks(*m_context, animation_frame_index); -} - -IntSize PNGImageDecoderPlugin::size() -{ - return { m_context->width, m_context->height }; -} - -bool PNGImageDecoderPlugin::sniff(ReadonlyBytes data) -{ - PNGLoadingContext context; - context.data = context.data_current_ptr = data.data(); - context.data_size = data.size(); - return decode_png_header(context); -} - -ErrorOr> PNGImageDecoderPlugin::create(ReadonlyBytes data) -{ - auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) PNGImageDecoderPlugin(data.data(), data.size()))); - if (!decode_png_header(*plugin->m_context)) - return Error::from_string_literal("Invalid header for a PNG file"); - TRY(decode_png_ihdr(*plugin->m_context)); - return plugin; -} - -bool PNGImageDecoderPlugin::is_animated() -{ - if (!ensure_image_data_chunk_was_decoded()) - return false; - return m_context->has_seen_actl_chunk_before_idat; -} - -size_t PNGImageDecoderPlugin::loop_count() -{ - if (!ensure_image_data_chunk_was_decoded()) - return 0; - return m_context->animation_loop_count; -} - -size_t PNGImageDecoderPlugin::frame_count() -{ - if (!ensure_image_data_chunk_was_decoded()) - return 0; - - if (!m_context->has_seen_actl_chunk_before_idat) - return 1; - - auto total_frames = m_context->animation_frame_count; - if (!m_context->is_first_idat_part_of_animation) - total_frames++; - return total_frames; + m_context->data = data; } size_t PNGImageDecoderPlugin::first_animated_frame_index() { - if (!ensure_image_data_chunk_was_decoded()) - return 0; - if (!m_context->has_seen_actl_chunk_before_idat) - return 0; - return m_context->is_first_idat_part_of_animation ? 0 : 1; + return 0; } -static ErrorOr> render_animation_frame(AnimationFrame const& prev_animation_frame, AnimationFrame& animation_frame, Bitmap const& decoded_frame_bitmap) +IntSize PNGImageDecoderPlugin::size() +{ + return m_context->size; +} + +bool PNGImageDecoderPlugin::is_animated() +{ + return m_context->frame_count > 1; +} + +size_t PNGImageDecoderPlugin::loop_count() +{ + return m_context->loop_count; +} + +size_t PNGImageDecoderPlugin::frame_count() +{ + return m_context->frame_count; +} + +ErrorOr PNGImageDecoderPlugin::frame(size_t index, Optional) +{ + if (index >= m_context->frame_descriptors.size()) + return Error::from_errno(EINVAL); + + return m_context->frame_descriptors[index]; +} + +ErrorOr> PNGImageDecoderPlugin::icc_data() +{ + if (m_context->icc_profile.has_value()) + return Optional(*m_context->icc_profile); + return OptionalNone {}; +} + +ErrorOr PNGImageDecoderPlugin::initialize() +{ + png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png_ptr) + return false; + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + return false; + } + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return false; + } + + png_set_read_fn(png_ptr, &m_context->data, [](png_structp png_ptr, png_bytep data, png_size_t length) { + auto* read_data = reinterpret_cast(png_get_io_ptr(png_ptr)); + if (read_data->size() < length) { + png_error(png_ptr, "Read error"); + return; + } + memcpy(data, read_data->data(), length); + *read_data = read_data->slice(length); + }); + + png_read_info(png_ptr, info_ptr); + + u32 width = 0; + u32 height = 0; + int bit_depth = 0; + int color_type = 0; + png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, nullptr, nullptr, nullptr); + m_context->size = { static_cast(width), static_cast(height) }; + + if (color_type == PNG_COLOR_TYPE_PALETTE) + png_set_palette_to_rgb(png_ptr); + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) + png_set_expand_gray_1_2_4_to_8(png_ptr); + + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) + png_set_tRNS_to_alpha(png_ptr); + + if (bit_depth == 16) + png_set_strip_16(png_ptr); + + if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) + png_set_gray_to_rgb(png_ptr); + + png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER); + png_set_bgr(png_ptr); + + char* profile_name = nullptr; + int compression_type = 0; + u8* profile_data = nullptr; + u32 profile_len = 0; + if (png_get_iCCP(png_ptr, info_ptr, &profile_name, &compression_type, &profile_data, &profile_len)) { + m_context->icc_profile = TRY(ByteBuffer::copy(profile_data, profile_len)); + } + + png_read_update_info(png_ptr, info_ptr); + m_context->frame_count = TRY(m_context->read_frames(png_ptr, info_ptr)); + + u8* exif_data = nullptr; + u32 exif_length = 0; + int const num_exif_chunks = png_get_eXIf_1(png_ptr, info_ptr, &exif_length, &exif_data); + if (num_exif_chunks > 0) { + m_context->exif_metadata = TRY(TIFFImageDecoderPlugin::read_exif_metadata({ exif_data, exif_length })); + } + + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return true; +} + +static ErrorOr> render_animation_frame(AnimationFrame const& prev_animation_frame, AnimationFrame const& animation_frame, Bitmap const& decoded_frame_bitmap) { auto rendered_bitmap = TRY(prev_animation_frame.bitmap->clone()); Painter painter(rendered_bitmap); - static constexpr Color transparent_black = { 0, 0, 0, 0 }; - auto frame_rect = animation_frame.rect(); - switch (prev_animation_frame.fcTL.dispose_op) { - case fcTL_Chunk::DisposeOp::APNG_DISPOSE_OP_NONE: + switch (prev_animation_frame.dispose_op) { + case PNG_DISPOSE_OP_BACKGROUND: + painter.clear_rect(rendered_bitmap->rect(), Color::NamedColor::Transparent); break; - case fcTL_Chunk::DisposeOp::APNG_DISPOSE_OP_BACKGROUND: - painter.clear_rect(rendered_bitmap->rect(), transparent_black); + case PNG_DISPOSE_OP_PREVIOUS: + painter.blit(frame_rect.location(), decoded_frame_bitmap, frame_rect, 1.0f, false); break; - case fcTL_Chunk::DisposeOp::APNG_DISPOSE_OP_PREVIOUS: { - painter.blit(frame_rect.location(), *prev_animation_frame.bitmap, frame_rect, 1.0f, false); + default: break; } - } - switch (animation_frame.fcTL.blend_op) { - case fcTL_Chunk::BlendOp::APNG_BLEND_OP_SOURCE: + switch (animation_frame.blend_op) { + case PNG_BLEND_OP_SOURCE: painter.blit(frame_rect.location(), decoded_frame_bitmap, decoded_frame_bitmap.rect(), 1.0f, false); break; - case fcTL_Chunk::BlendOp::APNG_BLEND_OP_OVER: + case PNG_BLEND_OP_OVER: painter.blit(frame_rect.location(), decoded_frame_bitmap, decoded_frame_bitmap.rect(), 1.0f, true); break; + default: + break; } return rendered_bitmap; } -ErrorOr PNGImageDecoderPlugin::frame(size_t index, Optional) +ErrorOr PNGLoadingContext::read_frames(png_structp png_ptr, png_infop info_ptr) { - if (m_context->state == PNGLoadingContext::State::Error) - return Error::from_string_literal("PNGImageDecoderPlugin: Decoding failed"); + if (png_get_acTL(png_ptr, info_ptr, &frame_count, &loop_count)) { + // acTL chunk present: This is an APNG. - if (!ensure_image_data_chunk_was_decoded()) - return Error::from_string_literal("PNGImageDecoderPlugin: Decoding image data chunk"); + png_set_acTL(png_ptr, info_ptr, frame_count, loop_count); - auto set_descriptor_duration = [](ImageFrameDescriptor& descriptor, AnimationFrame const& animation_frame) { - descriptor.duration = static_cast(animation_frame.duration_ms()); - if (descriptor.duration < 0) - descriptor.duration = NumericLimits::min(); - }; - auto load_default_image = [&]() -> ErrorOr { - if (m_context->state < PNGLoadingContext::State::BitmapDecoded) { - // NOTE: This forces the chunk decoding to happen. - TRY(decode_png_bitmap(*m_context)); + for (size_t frame_index = 0; frame_index < frame_count; ++frame_index) { + png_read_frame_head(png_ptr, info_ptr); + u32 width = 0; + u32 height = 0; + u32 x = 0; + u32 y = 0; + u16 delay_num = 0; + u16 delay_den = 0; + u8 dispose_op = 0; + u8 blend_op = 0; + + if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_fcTL)) { + return Error::from_string_literal("Missing fcTL chunk in APNG frame"); + } + + png_get_next_frame_fcTL(png_ptr, info_ptr, &width, &height, &x, &y, &delay_num, &delay_den, &dispose_op, &blend_op); + + decoded_frame_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, IntSize { static_cast(width), static_cast(height) })); + + row_pointers.resize(height); + for (u32 i = 0; i < height; ++i) { + row_pointers[i] = decoded_frame_bitmap->scanline_u8(i); + } + + png_read_image(png_ptr, row_pointers.data()); + + auto animation_frame = AnimationFrame(nullptr, x, y, width, height, delay_den, delay_num, blend_op, dispose_op); + + if (frame_index == 0) { + animation_frame.bitmap = decoded_frame_bitmap; + frame_descriptors.append({ decoded_frame_bitmap, animation_frame.duration_ms() }); + } else { + animation_frame.bitmap = TRY(render_animation_frame(animation_frames.last(), animation_frame, *decoded_frame_bitmap)); + frame_descriptors.append({ animation_frame.bitmap, animation_frame.duration_ms() }); + } + animation_frames.append(move(animation_frame)); } + } else { + // This is a single-frame PNG. - VERIFY(m_context->bitmap); - return {}; - }; + frame_count = 1; + loop_count = 0; - if (index == 0) { - TRY(load_default_image()); + decoded_frame_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, size)); + row_pointers.resize(size.height()); + for (int i = 0; i < size.height(); ++i) + row_pointers[i] = decoded_frame_bitmap->scanline_u8(i); - ImageFrameDescriptor descriptor { m_context->bitmap }; - if (m_context->has_seen_actl_chunk_before_idat && m_context->is_first_idat_part_of_animation) - set_descriptor_duration(descriptor, m_context->animation_frames[0]); - return descriptor; + png_read_image(png_ptr, row_pointers.data()); + frame_descriptors.append({ move(decoded_frame_bitmap), 0 }); } + return this->frame_count; +} - if (!m_context->has_seen_actl_chunk_before_idat) - return Error::from_string_literal("PNGImageDecoderPlugin: Invalid frame index"); +PNGImageDecoderPlugin::~PNGImageDecoderPlugin() = default; - if (!ensure_animation_frame_was_decoded(index)) - return Error::from_string_literal("PNGImageDecoderPlugin: Decoding image data chunk"); - - if (index >= m_context->animation_frames.size()) - return Error::from_string_literal("PNGImageDecoderPlugin: Invalid animation frame index"); - - // We need to assemble each frame up until the one requested, - // so decode all bitmaps that haven't been decoded yet. - for (size_t i = m_context->animation_next_frame_to_render; i <= index; i++) { - if (i == 0) { - // If the default image hasn't been loaded, load it now - TRY(load_default_image()); // May modify animation_frames! - - auto& animation_frame = m_context->animation_frames[i]; - animation_frame.bitmap = m_context->bitmap; - } else { - auto& animation_frame = m_context->animation_frames[i]; - VERIFY(!animation_frame.bitmap); - - auto decoded_bitmap = TRY(decode_png_animation_frame_bitmap(*m_context, animation_frame)); - - auto prev_animation_frame = m_context->animation_frames[i - 1]; - animation_frame.bitmap = TRY(render_animation_frame(prev_animation_frame, animation_frame, *decoded_bitmap)); - } - m_context->animation_next_frame_to_render = i + 1; - } - - auto const& animation_frame = m_context->animation_frames[index]; - VERIFY(animation_frame.bitmap); - - ImageFrameDescriptor descriptor { animation_frame.bitmap }; - set_descriptor_duration(descriptor, animation_frame); - return descriptor; +bool PNGImageDecoderPlugin::sniff(ReadonlyBytes data) +{ + Array png_signature { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a }; + if (data.size() < png_signature.size()) + return false; + return data.slice(0, png_signature.size()) == ReadonlyBytes(png_signature.data(), png_signature.size()); } Optional PNGImageDecoderPlugin::metadata() @@ -1457,40 +307,4 @@ Optional PNGImageDecoderPlugin::metadata() return OptionalNone {}; } -ErrorOr> PNGImageDecoderPlugin::icc_data() -{ - if (!decode_png_chunks(*m_context)) - return Error::from_string_literal("PNGImageDecoderPlugin: Decoding chunks failed"); - - if (m_context->embedded_icc_profile.has_value()) { - if (!m_context->decompressed_icc_profile.has_value()) { - auto compressed_data_stream = make(m_context->embedded_icc_profile->compressed_data); - auto decompressor_or_error = Compress::ZlibDecompressor::create(move(compressed_data_stream)); - if (decompressor_or_error.is_error()) { - m_context->embedded_icc_profile.clear(); - return decompressor_or_error.release_error(); - } - auto decompressor = decompressor_or_error.release_value(); - auto result_or_error = decompressor->read_until_eof(); - if (result_or_error.is_error()) { - m_context->embedded_icc_profile.clear(); - return result_or_error.release_error(); - } - m_context->decompressed_icc_profile = result_or_error.release_value(); - } - - return m_context->decompressed_icc_profile.value(); - } - - // FIXME: Eventually, look at coding_independent_code_points, chromaticities_and_whitepoint, gamma, sRGB_rendering_intent too. - // The order is: - // 1. Use coding_independent_code_points if it exists, ignore the rest. - // 2. Use embedded_icc_profile if it exists, ignore the rest. - // 3. Use sRGB_rendering_intent if it exists, ignore the rest. - // 4. Use gamma to adjust gamma and chromaticities_and_whitepoint to adjust color. - // (Order between 2 and 3 isn't fully clear, but "It is recommended that the sRGB and iCCP chunks do not appear simultaneously in a PNG datastream." - - return OptionalNone {}; -} - } diff --git a/Userland/Libraries/LibGfx/ImageFormats/PNGLoader.h b/Userland/Libraries/LibGfx/ImageFormats/PNGLoader.h index b3f0c0d1aae..00caf4dd917 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/PNGLoader.h +++ b/Userland/Libraries/LibGfx/ImageFormats/PNGLoader.h @@ -7,7 +7,6 @@ #pragma once #include -#include namespace Gfx { @@ -30,12 +29,10 @@ public: virtual Optional metadata() override; virtual ErrorOr> icc_data() override; - static void unfilter_scanline(PNG::FilterType filter, Bytes scanline_data, ReadonlyBytes previous_scanlines_data, u8 bytes_per_complete_pixel); - private: - PNGImageDecoderPlugin(u8 const*, size_t); - bool ensure_image_data_chunk_was_decoded(); - bool ensure_animation_frame_was_decoded(u32); + explicit PNGImageDecoderPlugin(ReadonlyBytes); + + ErrorOr initialize(); OwnPtr m_context; }; diff --git a/Userland/Libraries/LibGfx/ImageFormats/PNGShared.h b/Userland/Libraries/LibGfx/ImageFormats/PNGShared.h deleted file mode 100644 index b8cac67c186..00000000000 --- a/Userland/Libraries/LibGfx/ImageFormats/PNGShared.h +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2022, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include - -namespace Gfx::PNG { - -// https://www.w3.org/TR/PNG/#5PNG-file-signature -static constexpr Array header = { 0x89, 'P', 'N', 'G', 13, 10, 26, 10 }; - -// https://www.w3.org/TR/PNG/#6Colour-values -enum class ColorType : u8 { - Greyscale = 0, - Truecolor = 2, // RGB - IndexedColor = 3, - GreyscaleWithAlpha = 4, - TruecolorWithAlpha = 6, -}; - -// https://www.w3.org/TR/PNG/#9Filter-types -enum class FilterType : u8 { - None, - Sub, - Up, - Average, - Paeth, -}; - -inline ErrorOr filter_type(u8 byte) -{ - if (byte <= 4) - return static_cast(byte); - return Error::from_string_literal("PNGImageDecoderPlugin: Invalid PNG filter"); -} - -// https://www.w3.org/TR/PNG/#9Filter-type-4-Paeth -ALWAYS_INLINE u8 paeth_predictor(u8 a, u8 b, u8 c) -{ - int p = a + b - c; - int pa = AK::abs(p - a); - int pb = AK::abs(p - b); - int pc = AK::abs(p - c); - if (pa <= pb && pa <= pc) - return a; - if (pb <= pc) - return b; - return c; -} - -ALWAYS_INLINE AK::SIMD::u8x4 paeth_predictor(AK::SIMD::u8x4 a, AK::SIMD::u8x4 b, AK::SIMD::u8x4 c) -{ - return AK::SIMD::u8x4 { - paeth_predictor(a[0], b[0], c[0]), - paeth_predictor(a[1], b[1], c[1]), - paeth_predictor(a[2], b[2], c[2]), - paeth_predictor(a[3], b[3], c[3]), - }; -} - -}; diff --git a/Userland/Libraries/LibGfx/ImageFormats/PNGWriter.cpp b/Userland/Libraries/LibGfx/ImageFormats/PNGWriter.cpp index 7128e547080..f768709bef4 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/PNGWriter.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/PNGWriter.cpp @@ -1,284 +1,69 @@ /* - * Copyright (c) 2021, Pierre Hoffmeister - * Copyright (c) 2021, Andreas Kling - * Copyright (c) 2021, Aziz Berkay Yesilyurt + * Copyright (c) 2024, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include -#include -#include -#include -#include +#include +#include #include #include - -#pragma GCC diagnostic ignored "-Wpsabi" +#include namespace Gfx { -class PNGChunk { - using data_length_type = u32; - -public: - explicit PNGChunk(String); - auto const& data() const { return m_data; } - String const& type() const { return m_type; } - ErrorOr reserve(size_t bytes) { return m_data.try_ensure_capacity(bytes); } - - template - ErrorOr add_as_big_endian(T); - - ErrorOr add_u8(u8); - - ErrorOr compress_and_add(ReadonlyBytes); - ErrorOr add(ReadonlyBytes); - - ErrorOr store_type(); - void store_data_length(); - u32 crc(); - -private: - ByteBuffer m_data; - String m_type; +struct WriterContext { + Vector row_pointers; + ByteBuffer png_data; }; -PNGChunk::PNGChunk(String type) - : m_type(move(type)) -{ - VERIFY(m_type.bytes().size() == 4); - - // NOTE: These are MUST() because they should always be able to fit in m_data's inline capacity. - MUST(add_as_big_endian(0)); - MUST(store_type()); -} - -ErrorOr PNGChunk::store_type() -{ - TRY(add(type().bytes())); - return {}; -} - -void PNGChunk::store_data_length() -{ - auto data_length = BigEndian(m_data.size() - sizeof(data_length_type) - m_type.bytes().size()); - __builtin_memcpy(m_data.offset_pointer(0), &data_length, sizeof(u32)); -} - -u32 PNGChunk::crc() -{ - u32 crc = Crypto::Checksum::CRC32({ m_data.offset_pointer(sizeof(data_length_type)), m_data.size() - sizeof(data_length_type) }).digest(); - return crc; -} - -ErrorOr PNGChunk::compress_and_add(ReadonlyBytes uncompressed_bytes) -{ - return add(TRY(Compress::ZlibCompressor::compress_all(uncompressed_bytes, Compress::ZlibCompressionLevel::Best))); -} - -ErrorOr PNGChunk::add(ReadonlyBytes bytes) -{ - TRY(m_data.try_append(bytes)); - return {}; -} - -template -ErrorOr PNGChunk::add_as_big_endian(T data) -{ - auto data_out = AK::convert_between_host_and_big_endian(data); - TRY(m_data.try_append(&data_out, sizeof(T))); - return {}; -} - -ErrorOr PNGChunk::add_u8(u8 data) -{ - TRY(m_data.try_append(data)); - return {}; -} - -ErrorOr PNGWriter::add_chunk(PNGChunk& png_chunk) -{ - png_chunk.store_data_length(); - u32 crc = png_chunk.crc(); - TRY(png_chunk.add_as_big_endian(crc)); - TRY(m_data.try_append(png_chunk.data().data(), png_chunk.data().size())); - return {}; -} - -ErrorOr PNGWriter::add_png_header() -{ - TRY(m_data.try_append(PNG::header.data(), PNG::header.size())); - return {}; -} - -ErrorOr PNGWriter::add_IHDR_chunk(u32 width, u32 height, u8 bit_depth, PNG::ColorType color_type, u8 compression_method, u8 filter_method, u8 interlace_method) -{ - PNGChunk png_chunk { "IHDR"_string }; - TRY(png_chunk.add_as_big_endian(width)); - TRY(png_chunk.add_as_big_endian(height)); - TRY(png_chunk.add_u8(bit_depth)); - TRY(png_chunk.add_u8(to_underlying(color_type))); - TRY(png_chunk.add_u8(compression_method)); - TRY(png_chunk.add_u8(filter_method)); - TRY(png_chunk.add_u8(interlace_method)); - TRY(add_chunk(png_chunk)); - return {}; -} - -ErrorOr PNGWriter::add_iCCP_chunk(ReadonlyBytes icc_data) -{ - // https://www.w3.org/TR/png/#11iCCP - PNGChunk chunk { "iCCP"_string }; - - TRY(chunk.add("embedded profile"sv.bytes())); - TRY(chunk.add_u8(0)); // \0-terminate profile name - - TRY(chunk.add_u8(0)); // compression method deflate - TRY(chunk.compress_and_add(icc_data)); - - TRY(add_chunk(chunk)); - return {}; -} - -ErrorOr PNGWriter::add_IEND_chunk() -{ - PNGChunk png_chunk { "IEND"_string }; - TRY(add_chunk(png_chunk)); - return {}; -} - -union [[gnu::packed]] Pixel { - ARGB32 rgba { 0 }; - struct { - u8 red; - u8 green; - u8 blue; - u8 alpha; - }; - AK::SIMD::u8x4 simd; - - ALWAYS_INLINE static AK::SIMD::u8x4 gfx_to_png(Pixel pixel) - { - swap(pixel.red, pixel.blue); - return pixel.simd; - } -}; -static_assert(AssertSize()); - -ErrorOr PNGWriter::add_IDAT_chunk(Gfx::Bitmap const& bitmap) -{ - PNGChunk png_chunk { "IDAT"_string }; - TRY(png_chunk.reserve(bitmap.size_in_bytes())); - - ByteBuffer uncompressed_block_data; - TRY(uncompressed_block_data.try_ensure_capacity(bitmap.size_in_bytes() + bitmap.height())); - - auto dummy_scanline = TRY(FixedArray::create(bitmap.width())); - auto const* scanline_minus_1 = dummy_scanline.data(); - - for (int y = 0; y < bitmap.height(); ++y) { - auto* scanline = reinterpret_cast(bitmap.scanline(y)); - - struct Filter { - PNG::FilterType type; - ByteBuffer buffer {}; - int sum = 0; - - ErrorOr append(u8 byte) - { - TRY(buffer.try_append(byte)); - sum += static_cast(byte); - return {}; - } - - ErrorOr append(AK::SIMD::u8x4 simd) - { - TRY(append(simd[0])); - TRY(append(simd[1])); - TRY(append(simd[2])); - TRY(append(simd[3])); - return {}; - } - }; - - Filter none_filter { .type = PNG::FilterType::None }; - TRY(none_filter.buffer.try_ensure_capacity(sizeof(Pixel) * bitmap.width())); - - Filter sub_filter { .type = PNG::FilterType::Sub }; - TRY(sub_filter.buffer.try_ensure_capacity(sizeof(Pixel) * bitmap.width())); - - Filter up_filter { .type = PNG::FilterType::Up }; - TRY(up_filter.buffer.try_ensure_capacity(sizeof(Pixel) * bitmap.width())); - - Filter average_filter { .type = PNG::FilterType::Average }; - TRY(average_filter.buffer.try_ensure_capacity(sizeof(ARGB32) * bitmap.width())); - - Filter paeth_filter { .type = PNG::FilterType::Paeth }; - TRY(paeth_filter.buffer.try_ensure_capacity(sizeof(ARGB32) * bitmap.width())); - - auto pixel_x_minus_1 = Pixel::gfx_to_png(dummy_scanline[0]); - auto pixel_xy_minus_1 = Pixel::gfx_to_png(dummy_scanline[0]); - - for (int x = 0; x < bitmap.width(); ++x) { - auto pixel = Pixel::gfx_to_png(scanline[x]); - auto pixel_y_minus_1 = Pixel::gfx_to_png(scanline_minus_1[x]); - - TRY(none_filter.append(pixel)); - - TRY(sub_filter.append(pixel - pixel_x_minus_1)); - - TRY(up_filter.append(pixel - pixel_y_minus_1)); - - // The sum Orig(a) + Orig(b) shall be performed without overflow (using at least nine-bit arithmetic). - auto sum = AK::SIMD::to_u16x4(pixel_x_minus_1) + AK::SIMD::to_u16x4(pixel_y_minus_1); - auto average = AK::SIMD::to_u8x4(sum / 2); - TRY(average_filter.append(pixel - average)); - - TRY(paeth_filter.append(pixel - PNG::paeth_predictor(pixel_x_minus_1, pixel_y_minus_1, pixel_xy_minus_1))); - - pixel_x_minus_1 = pixel; - pixel_xy_minus_1 = pixel_y_minus_1; - } - - scanline_minus_1 = scanline; - - // 12.8 Filter selection: https://www.w3.org/TR/PNG/#12Filter-selection - // For best compression of truecolour and greyscale images, the recommended approach - // is adaptive filtering in which a filter is chosen for each scanline. - // The following simple heuristic has performed well in early tests: - // compute the output scanline using all five filters, and select the filter that gives the smallest sum of absolute values of outputs. - // (Consider the output bytes as signed differences for this test.) - Filter& best_filter = none_filter; - if (abs(best_filter.sum) > abs(sub_filter.sum)) - best_filter = sub_filter; - if (abs(best_filter.sum) > abs(up_filter.sum)) - best_filter = up_filter; - if (abs(best_filter.sum) > abs(average_filter.sum)) - best_filter = average_filter; - if (abs(best_filter.sum) > abs(paeth_filter.sum)) - best_filter = paeth_filter; - - TRY(uncompressed_block_data.try_append(to_underlying(best_filter.type))); - TRY(uncompressed_block_data.try_append(best_filter.buffer)); - } - - TRY(png_chunk.compress_and_add(uncompressed_block_data)); - TRY(add_chunk(png_chunk)); - return {}; -} - ErrorOr PNGWriter::encode(Gfx::Bitmap const& bitmap, Options options) { - PNGWriter writer; - TRY(writer.add_png_header()); - TRY(writer.add_IHDR_chunk(bitmap.width(), bitmap.height(), 8, PNG::ColorType::TruecolorWithAlpha, 0, 0, 0)); - if (options.icc_data.has_value()) - TRY(writer.add_iCCP_chunk(options.icc_data.value())); - TRY(writer.add_IDAT_chunk(bitmap)); - TRY(writer.add_IEND_chunk()); - return ByteBuffer::copy(writer.m_data); + auto context = make(); + int width = bitmap.width(); + int height = bitmap.height(); + + png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png_ptr) { + return Error::from_string_literal("Failed to create PNG write struct"); + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_write_struct(&png_ptr, nullptr); + return Error::from_string_literal("Failed to create PNG info struct"); + } + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_write_struct(&png_ptr, &info_ptr); + return Error::from_string_literal("Error during PNG encoding"); + } + + if (options.icc_data.has_value()) { + png_set_iCCP(png_ptr, info_ptr, "embedded profile", 0, options.icc_data->data(), options.icc_data->size()); + } + + if (bitmap.format() == BitmapFormat::BGRA8888 || bitmap.format() == BitmapFormat::BGRx8888) { + png_set_bgr(png_ptr); + } + + png_set_write_fn(png_ptr, &context->png_data, [](png_structp png_ptr, u8* data, size_t length) { + auto* buffer = reinterpret_cast(png_get_io_ptr(png_ptr)); + buffer->append(data, length); }, nullptr); + + png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + context->row_pointers.resize(height); + for (int y = 0; y < height; ++y) { + context->row_pointers[y] = const_cast(bitmap.scanline_u8(y)); + } + + png_set_rows(png_ptr, info_ptr, context->row_pointers.data()); + png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, nullptr); + + png_destroy_write_struct(&png_ptr, &info_ptr); + + return context->png_data; } } diff --git a/Userland/Libraries/LibGfx/ImageFormats/PNGWriter.h b/Userland/Libraries/LibGfx/ImageFormats/PNGWriter.h index 0891261ed29..054425fbe63 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/PNGWriter.h +++ b/Userland/Libraries/LibGfx/ImageFormats/PNGWriter.h @@ -1,21 +1,18 @@ /* - * Copyright (c) 2021, Pierre Hoffmeister - * Copyright (c) 2021, Andreas Kling + * Copyright (c) 2024, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once +#include #include -#include +#include #include -#include namespace Gfx { -class PNGChunk; - // This is not a nested struct to work around https://llvm.org/PR36684 struct PNGWriterOptions { // Data for the iCCP chunk. @@ -31,14 +28,6 @@ public: private: PNGWriter() = default; - - Vector m_data; - ErrorOr add_chunk(PNGChunk&); - ErrorOr add_png_header(); - ErrorOr add_IHDR_chunk(u32 width, u32 height, u8 bit_depth, PNG::ColorType color_type, u8 compression_method, u8 filter_method, u8 interlace_method); - ErrorOr add_iCCP_chunk(ReadonlyBytes icc_data); - ErrorOr add_IDAT_chunk(Gfx::Bitmap const&); - ErrorOr add_IEND_chunk(); }; } diff --git a/vcpkg.json b/vcpkg.json index 5d580b09110..e0f99ebe465 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -7,6 +7,10 @@ }, "icu", "libjpeg-turbo", + { + "name": "libpng", + "features": [ "apng" ] + }, "skia", "sqlite3", "woff2" @@ -24,6 +28,10 @@ "name": "libjpeg-turbo", "version": "3.0.2" }, + { + "name": "libpng", + "version": "1.6.43#1" + }, { "name": "skia", "version": "124#0"