mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-10-24 08:59:50 +00:00
If the image data to decode is incomplete, e.g. a corrupt image missing its last scanlines the decoder would previously keep looping for ever. By breaking out of the loop if no more scanlines were produced we can at least display the partial image up to that point.
227 lines
7.2 KiB
C++
227 lines
7.2 KiB
C++
/*
|
|
* Copyright (c) 2024, Andreas Kling <andreas@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibGfx/CMYKBitmap.h>
|
|
#include <LibGfx/ImageFormats/JPEGLoader.h>
|
|
#include <jpeglib.h>
|
|
#include <setjmp.h>
|
|
|
|
namespace Gfx {
|
|
|
|
struct JPEGLoadingContext {
|
|
enum class State {
|
|
NotDecoded,
|
|
Error,
|
|
Decoded,
|
|
};
|
|
|
|
State state { State::NotDecoded };
|
|
|
|
RefPtr<Gfx::Bitmap> rgb_bitmap;
|
|
RefPtr<Gfx::CMYKBitmap> cmyk_bitmap;
|
|
|
|
ReadonlyBytes data;
|
|
Vector<u8> icc_data;
|
|
|
|
JPEGLoadingContext(ReadonlyBytes data)
|
|
: data(data)
|
|
{
|
|
}
|
|
|
|
ErrorOr<void> decode();
|
|
};
|
|
|
|
struct JPEGErrorManager : jpeg_error_mgr {
|
|
jmp_buf setjmp_buffer {};
|
|
};
|
|
|
|
ErrorOr<void> JPEGLoadingContext::decode()
|
|
{
|
|
struct jpeg_decompress_struct cinfo;
|
|
struct JPEGErrorManager jerr;
|
|
cinfo.err = jpeg_std_error(&jerr);
|
|
|
|
jpeg_source_mgr source_manager {};
|
|
|
|
if (setjmp(jerr.setjmp_buffer)) {
|
|
jpeg_destroy_decompress(&cinfo);
|
|
state = State::Error;
|
|
return Error::from_string_literal("Failed to decode JPEG");
|
|
}
|
|
|
|
jerr.error_exit = [](j_common_ptr cinfo) {
|
|
char buffer[JMSG_LENGTH_MAX];
|
|
(*cinfo->err->format_message)(cinfo, buffer);
|
|
dbgln("JPEG error: {}", buffer);
|
|
longjmp(static_cast<JPEGErrorManager*>(cinfo->err)->setjmp_buffer, 1);
|
|
};
|
|
jpeg_create_decompress(&cinfo);
|
|
|
|
source_manager.next_input_byte = data.data();
|
|
source_manager.bytes_in_buffer = data.size();
|
|
source_manager.init_source = [](j_decompress_ptr) {};
|
|
source_manager.fill_input_buffer = [](j_decompress_ptr) -> boolean { return false; };
|
|
source_manager.skip_input_data = [](j_decompress_ptr context, long num_bytes) {
|
|
if (num_bytes > static_cast<long>(context->src->bytes_in_buffer)) {
|
|
context->src->bytes_in_buffer = 0;
|
|
return;
|
|
}
|
|
context->src->next_input_byte += num_bytes;
|
|
context->src->bytes_in_buffer -= num_bytes;
|
|
};
|
|
source_manager.resync_to_restart = jpeg_resync_to_restart;
|
|
source_manager.term_source = [](j_decompress_ptr) {};
|
|
|
|
cinfo.src = &source_manager;
|
|
|
|
jpeg_save_markers(&cinfo, JPEG_APP0 + 2, 0xFFFF);
|
|
if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
|
|
jpeg_destroy_decompress(&cinfo);
|
|
return Error::from_string_literal("Failed to read JPEG header");
|
|
}
|
|
|
|
if (cinfo.jpeg_color_space == JCS_CMYK || cinfo.jpeg_color_space == JCS_YCCK) {
|
|
cinfo.out_color_space = JCS_CMYK;
|
|
} else {
|
|
cinfo.out_color_space = JCS_EXT_BGRX;
|
|
}
|
|
|
|
jpeg_start_decompress(&cinfo);
|
|
bool could_read_all_scanlines = true;
|
|
|
|
if (cinfo.out_color_space == JCS_EXT_BGRX) {
|
|
rgb_bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRx8888, { static_cast<int>(cinfo.output_width), static_cast<int>(cinfo.output_height) }));
|
|
while (cinfo.output_scanline < cinfo.output_height) {
|
|
auto* row_ptr = (u8*)rgb_bitmap->scanline(cinfo.output_scanline);
|
|
auto out_size = jpeg_read_scanlines(&cinfo, &row_ptr, 1);
|
|
if (cinfo.output_scanline < cinfo.output_height && out_size == 0) {
|
|
dbgln("JPEG Warning: Decoding produced no more scanlines in scanline {}/{}.", cinfo.output_scanline, cinfo.output_height);
|
|
could_read_all_scanlines = false;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
cmyk_bitmap = TRY(CMYKBitmap::create_with_size({ static_cast<int>(cinfo.output_width), static_cast<int>(cinfo.output_height) }));
|
|
while (cinfo.output_scanline < cinfo.output_height) {
|
|
auto* row_ptr = (u8*)cmyk_bitmap->scanline(cinfo.output_scanline);
|
|
auto out_size = jpeg_read_scanlines(&cinfo, &row_ptr, 1);
|
|
if (cinfo.output_scanline < cinfo.output_height && out_size == 0) {
|
|
dbgln("JPEG Warning: Decoding produced no more scanlines in scanline {}/{}.", cinfo.output_scanline, cinfo.output_height);
|
|
could_read_all_scanlines = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
JOCTET* icc_data_ptr = nullptr;
|
|
unsigned int icc_data_length = 0;
|
|
if (jpeg_read_icc_profile(&cinfo, &icc_data_ptr, &icc_data_length)) {
|
|
icc_data.resize(icc_data_length);
|
|
memcpy(icc_data.data(), icc_data_ptr, icc_data_length);
|
|
free(icc_data_ptr);
|
|
}
|
|
|
|
if (could_read_all_scanlines)
|
|
jpeg_finish_decompress(&cinfo);
|
|
else
|
|
jpeg_abort_decompress(&cinfo);
|
|
jpeg_destroy_decompress(&cinfo);
|
|
|
|
if (cmyk_bitmap && !rgb_bitmap)
|
|
rgb_bitmap = TRY(cmyk_bitmap->to_low_quality_rgb());
|
|
|
|
state = State::Decoded;
|
|
return {};
|
|
}
|
|
|
|
JPEGImageDecoderPlugin::JPEGImageDecoderPlugin(NonnullOwnPtr<JPEGLoadingContext> context)
|
|
: m_context(move(context))
|
|
{
|
|
}
|
|
|
|
JPEGImageDecoderPlugin::~JPEGImageDecoderPlugin() = default;
|
|
|
|
IntSize JPEGImageDecoderPlugin::size()
|
|
{
|
|
if (m_context->state == JPEGLoadingContext::State::NotDecoded)
|
|
(void)frame(0);
|
|
|
|
if (m_context->state == JPEGLoadingContext::State::Error)
|
|
return {};
|
|
if (m_context->rgb_bitmap)
|
|
return m_context->rgb_bitmap->size();
|
|
if (m_context->cmyk_bitmap)
|
|
return m_context->cmyk_bitmap->size();
|
|
return {};
|
|
}
|
|
|
|
bool JPEGImageDecoderPlugin::sniff(ReadonlyBytes data)
|
|
{
|
|
return data.size() > 3
|
|
&& data.data()[0] == 0xFF
|
|
&& data.data()[1] == 0xD8
|
|
&& data.data()[2] == 0xFF;
|
|
}
|
|
|
|
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> JPEGImageDecoderPlugin::create(ReadonlyBytes data)
|
|
{
|
|
return adopt_own(*new JPEGImageDecoderPlugin(make<JPEGLoadingContext>(data)));
|
|
}
|
|
|
|
ErrorOr<ImageFrameDescriptor> JPEGImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
|
|
{
|
|
if (index > 0)
|
|
return Error::from_string_literal("JPEGImageDecoderPlugin: Invalid frame index");
|
|
|
|
if (m_context->state == JPEGLoadingContext::State::Error)
|
|
return Error::from_string_literal("JPEGImageDecoderPlugin: Decoding failed");
|
|
|
|
if (m_context->state < JPEGLoadingContext::State::Decoded) {
|
|
TRY(m_context->decode());
|
|
m_context->state = JPEGLoadingContext::State::Decoded;
|
|
}
|
|
|
|
return ImageFrameDescriptor { m_context->rgb_bitmap, 0 };
|
|
}
|
|
|
|
Optional<Metadata const&> JPEGImageDecoderPlugin::metadata()
|
|
{
|
|
return OptionalNone {};
|
|
}
|
|
|
|
ErrorOr<Optional<ReadonlyBytes>> JPEGImageDecoderPlugin::icc_data()
|
|
{
|
|
if (m_context->state == JPEGLoadingContext::State::NotDecoded)
|
|
(void)frame(0);
|
|
|
|
if (!m_context->icc_data.is_empty())
|
|
return m_context->icc_data;
|
|
return OptionalNone {};
|
|
}
|
|
|
|
NaturalFrameFormat JPEGImageDecoderPlugin::natural_frame_format() const
|
|
{
|
|
if (m_context->state == JPEGLoadingContext::State::NotDecoded)
|
|
(void)const_cast<JPEGImageDecoderPlugin&>(*this).frame(0);
|
|
|
|
if (m_context->cmyk_bitmap)
|
|
return NaturalFrameFormat::CMYK;
|
|
return NaturalFrameFormat::RGB;
|
|
}
|
|
|
|
ErrorOr<NonnullRefPtr<CMYKBitmap>> JPEGImageDecoderPlugin::cmyk_frame()
|
|
{
|
|
if (m_context->state == JPEGLoadingContext::State::NotDecoded)
|
|
(void)frame(0);
|
|
|
|
if (m_context->state == JPEGLoadingContext::State::Error)
|
|
return Error::from_string_literal("JPEGImageDecoderPlugin: Decoding failed");
|
|
if (!m_context->cmyk_bitmap)
|
|
return Error::from_string_literal("JPEGImageDecoderPlugin: No CMYK data available");
|
|
return *m_context->cmyk_bitmap;
|
|
}
|
|
|
|
}
|