2020-06-15 21:36:05 -04:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2020, Paul Roukema <roukemap@gmail.com>
|
|
|
|
*
|
2021-04-22 01:24:48 -07:00
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
2020-06-15 21:36:05 -04:00
|
|
|
*/
|
|
|
|
|
2021-01-24 15:28:26 +01:00
|
|
|
#include <AK/Debug.h>
|
2023-01-25 22:36:59 +01:00
|
|
|
#include <AK/MemoryStream.h>
|
2020-06-18 21:18:35 -04:00
|
|
|
#include <AK/Types.h>
|
2023-03-21 14:58:06 -04:00
|
|
|
#include <LibGfx/ImageFormats/BMPLoader.h>
|
|
|
|
#include <LibGfx/ImageFormats/ICOLoader.h>
|
|
|
|
#include <LibGfx/ImageFormats/PNGLoader.h>
|
2020-06-15 21:36:05 -04:00
|
|
|
|
|
|
|
namespace Gfx {
|
|
|
|
|
2025-05-06 17:49:14 +02:00
|
|
|
enum class IconType : u16 {
|
|
|
|
ICO = 1,
|
|
|
|
CUR = 2,
|
|
|
|
};
|
|
|
|
|
2020-06-15 21:36:05 -04:00
|
|
|
// FIXME: This is in little-endian order. Maybe need a NetworkOrdered<T> equivalent eventually.
|
|
|
|
struct ICONDIR {
|
2020-09-20 12:19:01 +02:00
|
|
|
u16 must_be_0 = 0;
|
2025-05-06 17:49:14 +02:00
|
|
|
IconType type = IconType::ICO;
|
2020-09-20 12:19:01 +02:00
|
|
|
u16 image_count = 0;
|
2020-06-15 21:36:05 -04:00
|
|
|
};
|
2021-09-05 01:00:46 -07:00
|
|
|
static_assert(AssertSize<ICONDIR, 6>());
|
2020-06-15 21:36:05 -04:00
|
|
|
|
|
|
|
struct ICONDIRENTRY {
|
|
|
|
u8 width;
|
|
|
|
u8 height;
|
|
|
|
u8 color_count;
|
|
|
|
u8 reserved_0;
|
|
|
|
u16 planes;
|
|
|
|
u16 bits_per_pixel;
|
|
|
|
u32 size;
|
|
|
|
u32 offset;
|
|
|
|
};
|
2021-09-05 01:00:46 -07:00
|
|
|
static_assert(AssertSize<ICONDIRENTRY, 16>());
|
2020-06-15 21:36:05 -04:00
|
|
|
|
2023-01-25 22:36:59 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
template<>
|
2023-11-08 20:29:12 +01:00
|
|
|
class AK::Traits<Gfx::ICONDIR> : public DefaultTraits<Gfx::ICONDIR> {
|
2023-01-25 22:36:59 +01:00
|
|
|
public:
|
|
|
|
static constexpr bool is_trivially_serializable() { return true; }
|
|
|
|
};
|
|
|
|
|
|
|
|
template<>
|
2023-11-08 20:29:12 +01:00
|
|
|
class AK::Traits<Gfx::ICONDIRENTRY> : public DefaultTraits<Gfx::ICONDIRENTRY> {
|
2023-01-25 22:36:59 +01:00
|
|
|
public:
|
|
|
|
static constexpr bool is_trivially_serializable() { return true; }
|
|
|
|
};
|
|
|
|
|
|
|
|
namespace Gfx {
|
|
|
|
|
2021-05-28 07:01:52 +02:00
|
|
|
struct ICOImageDescriptor {
|
2020-06-15 21:36:05 -04:00
|
|
|
u16 width;
|
|
|
|
u16 height;
|
2023-01-06 16:04:02 +01:00
|
|
|
u16 bits_per_pixel;
|
2025-05-06 17:49:14 +02:00
|
|
|
u16 hotspot_x { 0 };
|
|
|
|
u16 hotspot_y { 0 };
|
2020-06-15 21:36:05 -04:00
|
|
|
size_t offset;
|
|
|
|
size_t size;
|
|
|
|
RefPtr<Gfx::Bitmap> bitmap;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct ICOLoadingContext {
|
|
|
|
enum State {
|
|
|
|
NotDecoded = 0,
|
|
|
|
Error,
|
|
|
|
DirectoryDecoded,
|
|
|
|
BitmapDecoded
|
|
|
|
};
|
|
|
|
State state { NotDecoded };
|
2022-04-01 20:58:27 +03:00
|
|
|
u8 const* data { nullptr };
|
2020-06-15 21:36:05 -04:00
|
|
|
size_t data_size { 0 };
|
2025-05-06 17:49:14 +02:00
|
|
|
IconType file_type { IconType::ICO };
|
2021-05-28 07:01:52 +02:00
|
|
|
Vector<ICOImageDescriptor> images;
|
2020-06-16 23:24:01 -04:00
|
|
|
size_t largest_index;
|
2020-06-15 21:36:05 -04:00
|
|
|
};
|
|
|
|
|
2025-05-06 17:49:14 +02:00
|
|
|
static ErrorOr<size_t> decode_ico_header(Stream& stream, IconType& out_type)
|
2020-06-15 21:36:05 -04:00
|
|
|
{
|
2023-01-25 22:36:59 +01:00
|
|
|
auto header = TRY(stream.read_value<ICONDIR>());
|
2025-05-06 17:49:14 +02:00
|
|
|
if (header.must_be_0 != 0 || (header.type != IconType::ICO && header.type != IconType::CUR))
|
|
|
|
return Error::from_string_literal("Invalid ICO/CUR header");
|
|
|
|
|
|
|
|
out_type = header.type;
|
2020-06-15 21:36:05 -04:00
|
|
|
return { header.image_count };
|
|
|
|
}
|
|
|
|
|
2025-05-06 17:49:14 +02:00
|
|
|
static ErrorOr<ICOImageDescriptor> decode_ico_direntry(Stream& stream, IconType file_type)
|
2020-06-15 21:36:05 -04:00
|
|
|
{
|
2023-01-25 22:36:59 +01:00
|
|
|
auto entry = TRY(stream.read_value<ICONDIRENTRY>());
|
2025-05-06 17:49:14 +02:00
|
|
|
ICOImageDescriptor desc = {
|
|
|
|
entry.width,
|
|
|
|
entry.height,
|
|
|
|
entry.bits_per_pixel,
|
|
|
|
0, 0,
|
|
|
|
entry.offset,
|
|
|
|
entry.size,
|
|
|
|
nullptr
|
|
|
|
};
|
|
|
|
|
|
|
|
if (file_type == IconType::CUR) {
|
|
|
|
// For cursor files, hotsport coordinates are stored in the planes and bits_per_pixel fields
|
|
|
|
desc.hotspot_x = entry.planes;
|
|
|
|
desc.hotspot_y = entry.bits_per_pixel;
|
|
|
|
}
|
|
|
|
|
2020-06-15 21:36:05 -04:00
|
|
|
if (desc.width == 0)
|
|
|
|
desc.width = 256;
|
|
|
|
if (desc.height == 0)
|
|
|
|
desc.height = 256;
|
|
|
|
|
|
|
|
return { desc };
|
|
|
|
}
|
|
|
|
|
2022-04-01 20:58:27 +03:00
|
|
|
static size_t find_largest_image(ICOLoadingContext const& context)
|
2020-06-16 23:24:01 -04:00
|
|
|
{
|
|
|
|
size_t max_area = 0;
|
|
|
|
size_t index = 0;
|
|
|
|
size_t largest_index = 0;
|
2023-01-06 16:04:02 +01:00
|
|
|
u16 max_bits_per_pixel = 0;
|
2022-04-01 20:58:27 +03:00
|
|
|
for (auto const& desc : context.images) {
|
2023-01-06 16:04:02 +01:00
|
|
|
if (static_cast<size_t>(desc.width) * static_cast<size_t>(desc.height) >= max_area) {
|
|
|
|
if (desc.bits_per_pixel > max_bits_per_pixel) {
|
|
|
|
max_area = desc.width * desc.height;
|
|
|
|
largest_index = index;
|
|
|
|
max_bits_per_pixel = desc.bits_per_pixel;
|
|
|
|
}
|
2020-06-16 23:24:01 -04:00
|
|
|
}
|
|
|
|
++index;
|
|
|
|
}
|
|
|
|
return largest_index;
|
|
|
|
}
|
|
|
|
|
2023-01-25 22:31:45 +01:00
|
|
|
static ErrorOr<void> load_ico_directory(ICOLoadingContext& context)
|
2020-06-15 21:36:05 -04:00
|
|
|
{
|
2023-01-30 11:05:43 +01:00
|
|
|
FixedMemoryStream stream { { context.data, context.data_size } };
|
2020-09-20 12:19:01 +02:00
|
|
|
|
2025-05-06 17:49:14 +02:00
|
|
|
auto image_count = TRY(decode_ico_header(stream, context.file_type));
|
2023-01-25 22:31:45 +01:00
|
|
|
if (image_count == 0)
|
2025-05-06 17:49:14 +02:00
|
|
|
return Error::from_string_literal("ICO/CUR file has no images");
|
2020-06-15 21:36:05 -04:00
|
|
|
|
2023-01-25 22:31:45 +01:00
|
|
|
for (size_t i = 0; i < image_count; ++i) {
|
2025-05-06 17:49:14 +02:00
|
|
|
auto desc = TRY(decode_ico_direntry(stream, context.file_type));
|
2020-06-15 21:36:05 -04:00
|
|
|
if (desc.offset + desc.size < desc.offset // detect integer overflow
|
|
|
|
|| (desc.offset + desc.size) > context.data_size) {
|
2021-05-29 17:54:33 +03:00
|
|
|
dbgln_if(ICO_DEBUG, "load_ico_directory: offset: {} size: {} doesn't fit in ICO size: {}", desc.offset, desc.size, context.data_size);
|
2023-01-25 22:31:45 +01:00
|
|
|
return Error::from_string_literal("ICO size too large");
|
2020-06-15 21:36:05 -04:00
|
|
|
}
|
2021-05-29 17:54:33 +03:00
|
|
|
dbgln_if(ICO_DEBUG, "load_ico_directory: index {} width: {} height: {} offset: {} size: {}", i, desc.width, desc.height, desc.offset, desc.size);
|
2023-01-25 22:31:45 +01:00
|
|
|
TRY(context.images.try_append(desc));
|
2020-06-15 21:36:05 -04:00
|
|
|
}
|
2020-06-16 23:24:01 -04:00
|
|
|
context.largest_index = find_largest_image(context);
|
2020-06-18 21:18:35 -04:00
|
|
|
context.state = ICOLoadingContext::State::DirectoryDecoded;
|
2023-01-25 22:31:45 +01:00
|
|
|
return {};
|
2020-06-18 21:18:35 -04:00
|
|
|
}
|
|
|
|
|
2023-07-19 00:28:17 -04:00
|
|
|
ErrorOr<void> ICOImageDecoderPlugin::load_ico_bitmap(ICOLoadingContext& context)
|
2020-06-15 21:36:05 -04:00
|
|
|
{
|
2023-07-17 12:24:16 -04:00
|
|
|
VERIFY(context.state >= ICOLoadingContext::State::DirectoryDecoded);
|
2023-01-25 22:31:45 +01:00
|
|
|
|
2023-07-19 00:28:17 -04:00
|
|
|
size_t const real_index = context.largest_index;
|
2023-01-25 22:31:45 +01:00
|
|
|
if (real_index >= context.images.size())
|
|
|
|
return Error::from_string_literal("Index out of bounds");
|
2020-06-15 21:36:05 -04:00
|
|
|
|
2021-05-28 07:01:52 +02:00
|
|
|
ICOImageDescriptor& desc = context.images[real_index];
|
2023-02-26 18:02:50 +00:00
|
|
|
if (PNGImageDecoderPlugin::sniff({ context.data + desc.offset, desc.size })) {
|
2023-01-25 22:31:45 +01:00
|
|
|
auto png_decoder = TRY(PNGImageDecoderPlugin::create({ context.data + desc.offset, desc.size }));
|
2023-05-07 19:27:07 +02:00
|
|
|
auto decoded_png_frame = TRY(png_decoder->frame(0));
|
|
|
|
desc.bitmap = decoded_png_frame.image;
|
|
|
|
return {};
|
2020-06-15 21:36:05 -04:00
|
|
|
} else {
|
2023-01-25 22:31:45 +01:00
|
|
|
auto bmp_decoder = TRY(BMPImageDecoderPlugin::create_as_included_in_ico({}, { context.data + desc.offset, desc.size }));
|
2023-01-20 10:13:14 +02:00
|
|
|
// NOTE: We don't initialize a BMP decoder in the usual way, but rather
|
|
|
|
// we just create an object and try to sniff for a frame when it's included
|
|
|
|
// inside an ICO image.
|
|
|
|
if (bmp_decoder->sniff_dib()) {
|
2023-01-25 22:31:45 +01:00
|
|
|
auto decoded_bmp_frame = TRY(bmp_decoder->frame(0));
|
|
|
|
desc.bitmap = decoded_bmp_frame.image;
|
2022-12-18 18:13:19 +00:00
|
|
|
} else {
|
|
|
|
dbgln_if(ICO_DEBUG, "load_ico_bitmap: encoded image not supported at index: {}", real_index);
|
2023-01-25 22:31:45 +01:00
|
|
|
return Error::from_string_literal("Encoded image not supported");
|
2020-06-18 21:18:35 -04:00
|
|
|
}
|
2023-01-25 22:31:45 +01:00
|
|
|
return {};
|
2020-06-15 21:36:05 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-26 18:02:50 +00:00
|
|
|
bool ICOImageDecoderPlugin::sniff(ReadonlyBytes data)
|
2023-01-20 10:13:14 +02:00
|
|
|
{
|
2023-01-30 11:05:43 +01:00
|
|
|
FixedMemoryStream stream { data };
|
2025-05-06 17:49:14 +02:00
|
|
|
IconType file_type;
|
|
|
|
return !decode_ico_header(stream, file_type).is_error();
|
2023-01-20 10:13:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> ICOImageDecoderPlugin::create(ReadonlyBytes data)
|
|
|
|
{
|
2023-07-17 12:24:16 -04:00
|
|
|
auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) ICOImageDecoderPlugin(data.data(), data.size())));
|
|
|
|
TRY(load_ico_directory(*plugin->m_context));
|
|
|
|
return plugin;
|
2023-01-20 10:13:14 +02:00
|
|
|
}
|
|
|
|
|
2022-04-01 20:58:27 +03:00
|
|
|
ICOImageDecoderPlugin::ICOImageDecoderPlugin(u8 const* data, size_t size)
|
2020-06-15 21:36:05 -04:00
|
|
|
{
|
|
|
|
m_context = make<ICOLoadingContext>();
|
|
|
|
m_context->data = data;
|
|
|
|
m_context->data_size = size;
|
|
|
|
}
|
|
|
|
|
2022-03-14 13:26:37 -06:00
|
|
|
ICOImageDecoderPlugin::~ICOImageDecoderPlugin() = default;
|
2020-06-15 21:36:05 -04:00
|
|
|
|
|
|
|
IntSize ICOImageDecoderPlugin::size()
|
|
|
|
{
|
2020-06-16 23:24:01 -04:00
|
|
|
return { m_context->images[m_context->largest_index].width, m_context->images[m_context->largest_index].height };
|
2020-06-15 21:36:05 -04:00
|
|
|
}
|
|
|
|
|
2023-07-02 22:20:06 +01:00
|
|
|
ErrorOr<ImageFrameDescriptor> ICOImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
|
2020-06-15 21:36:05 -04:00
|
|
|
{
|
2021-11-20 14:29:33 +01:00
|
|
|
if (index > 0)
|
2022-07-11 17:57:32 +00:00
|
|
|
return Error::from_string_literal("ICOImageDecoderPlugin: Invalid frame index");
|
2021-11-18 13:47:29 +01:00
|
|
|
|
|
|
|
if (m_context->state == ICOLoadingContext::State::Error)
|
2022-07-11 17:57:32 +00:00
|
|
|
return Error::from_string_literal("ICOImageDecoderPlugin: Decoding failed");
|
2021-11-18 13:47:29 +01:00
|
|
|
|
|
|
|
if (m_context->state < ICOLoadingContext::State::BitmapDecoded) {
|
|
|
|
// NOTE: This forces the chunk decoding to happen.
|
2023-07-19 00:28:17 -04:00
|
|
|
auto maybe_error = load_ico_bitmap(*m_context);
|
2023-01-25 22:31:45 +01:00
|
|
|
if (maybe_error.is_error()) {
|
2021-11-18 13:47:29 +01:00
|
|
|
m_context->state = ICOLoadingContext::State::Error;
|
2022-07-11 17:57:32 +00:00
|
|
|
return Error::from_string_literal("ICOImageDecoderPlugin: Decoding failed");
|
2021-11-18 13:47:29 +01:00
|
|
|
}
|
|
|
|
m_context->state = ICOLoadingContext::State::BitmapDecoded;
|
|
|
|
}
|
|
|
|
|
|
|
|
VERIFY(m_context->images[m_context->largest_index].bitmap);
|
2025-06-25 10:25:28 +02:00
|
|
|
return ImageFrameDescriptor { *m_context->images[m_context->largest_index].bitmap, 0 };
|
2020-06-15 21:36:05 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|