LibGfx: Treat PNG files with invalid frame data as transparent

If we have a valid PNG header with geometry info etc, we should still
display it as *something*, even if the image data itself is missing or
corrupted.

This matches the behavior of other browsers, and is something that
Cloudflare Turnstile checks for.

To achieve this, we split the PNG decoder's initialization into two
steps: "everything except reading frame data" and "reading frame data".
If the latter step fails, we yield a transparent bitmap with the
geometry from the PNG's IHDR chunk.
This commit is contained in:
Andreas Kling 2024-12-18 08:54:05 +01:00 committed by Andreas Kling
parent f44166ebd0
commit 9164c9784d
Notes: github-actions[bot] 2024-12-19 15:50:30 +00:00
3 changed files with 73 additions and 29 deletions

View file

@ -17,6 +17,14 @@
namespace Gfx {
struct PNGLoadingContext {
~PNGLoadingContext()
{
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
}
png_structp png_ptr { nullptr };
png_infop info_ptr { nullptr };
ReadonlyBytes data;
IntSize size;
u32 frame_count { 0 };
@ -27,12 +35,40 @@ struct PNGLoadingContext {
ErrorOr<size_t> read_frames(png_structp, png_infop);
ErrorOr<void> apply_exif_orientation();
ErrorOr<void> read_all_frames()
{
// NOTE: We need to setjmp() here because libpng uses longjmp() for error handling.
if (auto error_value = setjmp(png_jmpbuf(png_ptr)); error_value) {
return Error::from_errno(error_value);
}
png_read_update_info(png_ptr, info_ptr);
frame_count = TRY(read_frames(png_ptr, info_ptr));
if (exif_metadata)
TRY(apply_exif_orientation());
return {};
}
};
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> PNGImageDecoderPlugin::create(ReadonlyBytes bytes)
{
auto decoder = adopt_own(*new PNGImageDecoderPlugin(bytes));
TRY(decoder->initialize());
auto result = decoder->m_context->read_all_frames();
if (result.is_error()) {
// NOTE: If we didn't fail in initialize(), that means we have size information.
// We can create a single-frame bitmap with that size and return it.
// This is weird, but kinda matches the behavior of other browsers.
auto bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Premultiplied, decoder->m_context->size));
decoder->m_context->frame_descriptors.append({ move(bitmap), 0 });
decoder->m_context->frame_count = 1;
return decoder;
}
return decoder;
}
@ -95,22 +131,20 @@ static void log_png_warning(png_structp, char const* warning_message)
ErrorOr<void> PNGImageDecoderPlugin::initialize()
{
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png_ptr)
m_context->png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!m_context->png_ptr)
return Error::from_string_view("Failed to allocate read struct"sv);
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
m_context->info_ptr = png_create_info_struct(m_context->png_ptr);
if (!m_context->info_ptr) {
return Error::from_string_view("Failed to allocate info struct"sv);
}
if (auto error_value = setjmp(png_jmpbuf(png_ptr)); error_value) {
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
if (auto error_value = setjmp(png_jmpbuf(m_context->png_ptr)); error_value) {
return Error::from_errno(error_value);
}
png_set_read_fn(png_ptr, &m_context->data, [](png_structp png_ptr, png_bytep data, png_size_t length) {
png_set_read_fn(m_context->png_ptr, &m_context->data, [](png_structp png_ptr, png_bytep data, png_size_t length) {
auto* read_data = reinterpret_cast<ReadonlyBytes*>(png_get_io_ptr(png_ptr));
if (read_data->size() < length) {
png_error(png_ptr, "Read error");
@ -120,59 +154,52 @@ ErrorOr<void> PNGImageDecoderPlugin::initialize()
*read_data = read_data->slice(length);
});
png_set_error_fn(png_ptr, nullptr, log_png_error, log_png_warning);
png_set_error_fn(m_context->png_ptr, nullptr, log_png_error, log_png_warning);
png_read_info(png_ptr, info_ptr);
png_read_info(m_context->png_ptr, m_context->info_ptr);
u32 width = 0;
u32 height = 0;
int bit_depth = 0;
int color_type = 0;
int interlace_type = 0;
png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, &interlace_type, nullptr, nullptr);
png_get_IHDR(m_context->png_ptr, m_context->info_ptr, &width, &height, &bit_depth, &color_type, &interlace_type, nullptr, nullptr);
m_context->size = { static_cast<int>(width), static_cast<int>(height) };
if (color_type == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb(png_ptr);
png_set_palette_to_rgb(m_context->png_ptr);
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
png_set_expand_gray_1_2_4_to_8(png_ptr);
png_set_expand_gray_1_2_4_to_8(m_context->png_ptr);
if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png_ptr);
if (png_get_valid(m_context->png_ptr, m_context->info_ptr, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(m_context->png_ptr);
if (bit_depth == 16)
png_set_strip_16(png_ptr);
png_set_strip_16(m_context->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_gray_to_rgb(m_context->png_ptr);
if (interlace_type != PNG_INTERLACE_NONE)
png_set_interlace_handling(png_ptr);
png_set_interlace_handling(m_context->png_ptr);
png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
png_set_bgr(png_ptr);
png_set_filler(m_context->png_ptr, 0xFF, PNG_FILLER_AFTER);
png_set_bgr(m_context->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))
if (png_get_iCCP(m_context->png_ptr, m_context->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);
int const num_exif_chunks = png_get_eXIf_1(m_context->png_ptr, m_context->info_ptr, &exif_length, &exif_data);
if (num_exif_chunks > 0)
m_context->exif_metadata = TRY(TIFFImageDecoderPlugin::read_exif_metadata({ exif_data, exif_length }));
if (m_context->exif_metadata)
TRY(m_context->apply_exif_orientation());
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
return {};
}

View file

@ -0,0 +1 @@
loaded, width=62, height=97

View file

@ -0,0 +1,16 @@
<script src="../include.js"></script>
<script>
asyncTest(done => {
const image = document.createElement("img");
image.onload = () => {
println(`loaded, width=${image.width}, height=${image.height}`);
done();
};
image.onerror = () => {
println("error :^(");
done();
};
image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABhCAIAAAABe4UxAAAABElEQVQAAAABnSTXkQAAAABJRU5ErkJggg==";
});
</script>