mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-21 23:20:20 +00:00
LibGfx: Remove QOI image format support
This format is not supported by other browsers.
This commit is contained in:
parent
2a888ca626
commit
4b4254c3d0
Notes:
sideshowbarker
2024-07-17 00:59:43 +09:00
Author: https://github.com/awesomekling Commit: https://github.com/LadybirdBrowser/ladybird/commit/4b4254c3d0 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/191 Reviewed-by: https://github.com/nico
13 changed files with 1 additions and 601 deletions
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibGfx/ImageFormats/QOILoader.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size)
|
||||
{
|
||||
AK::set_debug_enabled(false);
|
||||
auto decoder_or_error = Gfx::QOIImageDecoderPlugin::create({ data, size });
|
||||
if (decoder_or_error.is_error())
|
||||
return 0;
|
||||
auto decoder = decoder_or_error.release_value();
|
||||
(void)decoder->frame(0);
|
||||
return 0;
|
||||
}
|
|
@ -33,7 +33,6 @@ set(FUZZER_TARGETS
|
|||
Poly1305
|
||||
PPMLoader
|
||||
QOALoader
|
||||
QOILoader
|
||||
RegexECMA262
|
||||
RegexPosixBasic
|
||||
RegexPosixExtended
|
||||
|
@ -98,7 +97,6 @@ set(FUZZER_DEPENDENCIES_PNGLoader LibGfx)
|
|||
set(FUZZER_DEPENDENCIES_Poly1305 LibCrypto)
|
||||
set(FUZZER_DEPENDENCIES_PPMLoader LibGfx)
|
||||
set(FUZZER_DEPENDENCIES_QOALoader LibAudio)
|
||||
set(FUZZER_DEPENDENCIES_QOILoader LibGfx)
|
||||
set(FUZZER_DEPENDENCIES_RegexECMA262 LibRegex)
|
||||
set(FUZZER_DEPENDENCIES_RegexPosixBasic LibRegex)
|
||||
set(FUZZER_DEPENDENCIES_RegexPosixExtended LibRegex)
|
||||
|
|
|
@ -82,8 +82,6 @@ shared_library("LibGfx") {
|
|||
"ImageFormats/PPMLoader.cpp",
|
||||
"ImageFormats/PortableFormatWriter.cpp",
|
||||
"ImageFormats/QMArithmeticDecoder.cpp",
|
||||
"ImageFormats/QOILoader.cpp",
|
||||
"ImageFormats/QOIWriter.cpp",
|
||||
"ImageFormats/TGALoader.cpp",
|
||||
"ImageFormats/TIFFLoader.cpp",
|
||||
"ImageFormats/TinyVGLoader.cpp",
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
#include <LibGfx/ImageFormats/JPEGWriter.h>
|
||||
#include <LibGfx/ImageFormats/PNGLoader.h>
|
||||
#include <LibGfx/ImageFormats/PNGWriter.h>
|
||||
#include <LibGfx/ImageFormats/QOILoader.h>
|
||||
#include <LibGfx/ImageFormats/QOIWriter.h>
|
||||
#include <LibGfx/ImageFormats/WebPLoader.h>
|
||||
#include <LibGfx/ImageFormats/WebPWriter.h>
|
||||
#include <LibTest/TestCase.h>
|
||||
|
@ -174,12 +172,6 @@ TEST_CASE(test_png)
|
|||
TRY_OR_FAIL((test_roundtrip<Gfx::PNGWriter, Gfx::PNGImageDecoderPlugin>(TRY_OR_FAIL(create_test_rgba_bitmap()))));
|
||||
}
|
||||
|
||||
TEST_CASE(test_qoi)
|
||||
{
|
||||
TRY_OR_FAIL((test_roundtrip<Gfx::QOIWriter, Gfx::QOIImageDecoderPlugin>(TRY_OR_FAIL(create_test_rgb_bitmap()))));
|
||||
TRY_OR_FAIL((test_roundtrip<Gfx::QOIWriter, Gfx::QOIImageDecoderPlugin>(TRY_OR_FAIL(create_test_rgba_bitmap()))));
|
||||
}
|
||||
|
||||
TEST_CASE(test_webp)
|
||||
{
|
||||
TRY_OR_FAIL((test_roundtrip<Gfx::WebPWriter, Gfx::WebPImageDecoderPlugin>(TRY_OR_FAIL(create_test_rgb_bitmap()))));
|
||||
|
|
|
@ -133,7 +133,6 @@ static Array const s_registered_mime_type = {
|
|||
MimeType { .name = "image/x-portable-bitmap"sv, .common_extensions = { ".pbm"sv }, .description = "PBM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x31, 0x0A } },
|
||||
MimeType { .name = "image/x-portable-graymap"sv, .common_extensions = { ".pgm"sv }, .description = "PGM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x32, 0x0A } },
|
||||
MimeType { .name = "image/x-portable-pixmap"sv, .common_extensions = { ".ppm"sv }, .description = "PPM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x33, 0x0A } },
|
||||
MimeType { .name = "image/x-qoi"sv, .common_extensions = { ".qoi"sv }, .description = "QOI image data"sv, .magic_bytes = Vector<u8> { 'q', 'o', 'i', 'f' } },
|
||||
MimeType { .name = "image/x-targa"sv, .common_extensions = { ".tga"sv }, .description = "Targa image data"sv },
|
||||
|
||||
MimeType { .name = "text/css"sv, .common_extensions = { ".css"sv }, .description = "Cascading Style Sheet"sv },
|
||||
|
|
|
@ -60,8 +60,6 @@ set(SOURCES
|
|||
ImageFormats/PAMLoader.cpp
|
||||
ImageFormats/PPMLoader.cpp
|
||||
ImageFormats/QMArithmeticDecoder.cpp
|
||||
ImageFormats/QOILoader.cpp
|
||||
ImageFormats/QOIWriter.cpp
|
||||
ImageFormats/TGALoader.cpp
|
||||
ImageFormats/TIFFLoader.cpp
|
||||
ImageFormats/TinyVGLoader.cpp
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
#include <LibGfx/ImageFormats/PGMLoader.h>
|
||||
#include <LibGfx/ImageFormats/PNGLoader.h>
|
||||
#include <LibGfx/ImageFormats/PPMLoader.h>
|
||||
#include <LibGfx/ImageFormats/QOILoader.h>
|
||||
#include <LibGfx/ImageFormats/TGALoader.h>
|
||||
#include <LibGfx/ImageFormats/TIFFLoader.h>
|
||||
#include <LibGfx/ImageFormats/TinyVGLoader.h>
|
||||
|
@ -50,7 +49,6 @@ static ErrorOr<OwnPtr<ImageDecoderPlugin>> probe_and_sniff_for_appropriate_plugi
|
|||
{ PGMImageDecoderPlugin::sniff, PGMImageDecoderPlugin::create },
|
||||
{ PNGImageDecoderPlugin::sniff, PNGImageDecoderPlugin::create },
|
||||
{ PPMImageDecoderPlugin::sniff, PPMImageDecoderPlugin::create },
|
||||
{ QOIImageDecoderPlugin::sniff, QOIImageDecoderPlugin::create },
|
||||
{ TIFFImageDecoderPlugin::sniff, TIFFImageDecoderPlugin::create },
|
||||
{ TinyVGImageDecoderPlugin::sniff, TinyVGImageDecoderPlugin::create },
|
||||
{ WebPImageDecoderPlugin::sniff, WebPImageDecoderPlugin::create },
|
||||
|
|
|
@ -1,227 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Endian.h>
|
||||
#include <AK/MemoryStream.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/ImageFormats/QOILoader.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
static constexpr auto QOI_MAGIC = "qoif"sv;
|
||||
static constexpr u8 QOI_OP_RGB = 0b11111110;
|
||||
static constexpr u8 QOI_OP_RGBA = 0b11111111;
|
||||
static constexpr u8 QOI_OP_INDEX = 0b00000000;
|
||||
static constexpr u8 QOI_OP_DIFF = 0b01000000;
|
||||
static constexpr u8 QOI_OP_LUMA = 0b10000000;
|
||||
static constexpr u8 QOI_OP_RUN = 0b11000000;
|
||||
static constexpr u8 QOI_MASK_2 = 0b11000000;
|
||||
static constexpr u8 END_MARKER[] = { 0, 0, 0, 0, 0, 0, 0, 1 };
|
||||
|
||||
static ErrorOr<QOIHeader> decode_qoi_header(Stream& stream)
|
||||
{
|
||||
auto header = TRY(stream.read_value<QOIHeader>());
|
||||
if (StringView { header.magic, array_size(header.magic) } != QOI_MAGIC)
|
||||
return Error::from_string_literal("Invalid QOI image: incorrect header magic");
|
||||
header.width = AK::convert_between_host_and_big_endian(header.width);
|
||||
header.height = AK::convert_between_host_and_big_endian(header.height);
|
||||
return header;
|
||||
}
|
||||
|
||||
static ErrorOr<Color> decode_qoi_op_rgb(Stream& stream, u8 first_byte, Color pixel)
|
||||
{
|
||||
VERIFY(first_byte == QOI_OP_RGB);
|
||||
u8 bytes[3];
|
||||
TRY(stream.read_until_filled({ &bytes, array_size(bytes) }));
|
||||
|
||||
// The alpha value remains unchanged from the previous pixel.
|
||||
return Color { bytes[0], bytes[1], bytes[2], pixel.alpha() };
|
||||
}
|
||||
|
||||
static ErrorOr<Color> decode_qoi_op_rgba(Stream& stream, u8 first_byte)
|
||||
{
|
||||
VERIFY(first_byte == QOI_OP_RGBA);
|
||||
u8 bytes[4];
|
||||
TRY(stream.read_until_filled({ &bytes, array_size(bytes) }));
|
||||
return Color { bytes[0], bytes[1], bytes[2], bytes[3] };
|
||||
}
|
||||
|
||||
static ErrorOr<u8> decode_qoi_op_index(Stream&, u8 first_byte)
|
||||
{
|
||||
VERIFY((first_byte & QOI_MASK_2) == QOI_OP_INDEX);
|
||||
u8 index = first_byte & ~QOI_MASK_2;
|
||||
VERIFY(index <= 63);
|
||||
return index;
|
||||
}
|
||||
|
||||
static ErrorOr<Color> decode_qoi_op_diff(Stream&, u8 first_byte, Color pixel)
|
||||
{
|
||||
VERIFY((first_byte & QOI_MASK_2) == QOI_OP_DIFF);
|
||||
u8 dr = (first_byte & 0b00110000) >> 4;
|
||||
u8 dg = (first_byte & 0b00001100) >> 2;
|
||||
u8 db = (first_byte & 0b00000011);
|
||||
VERIFY(dr <= 3 && dg <= 3 && db <= 3);
|
||||
|
||||
// Values are stored as unsigned integers with a bias of 2.
|
||||
return Color {
|
||||
static_cast<u8>(pixel.red() + static_cast<i8>(dr - 2)),
|
||||
static_cast<u8>(pixel.green() + static_cast<i8>(dg - 2)),
|
||||
static_cast<u8>(pixel.blue() + static_cast<i8>(db - 2)),
|
||||
pixel.alpha(),
|
||||
};
|
||||
}
|
||||
|
||||
static ErrorOr<Color> decode_qoi_op_luma(Stream& stream, u8 first_byte, Color pixel)
|
||||
{
|
||||
VERIFY((first_byte & QOI_MASK_2) == QOI_OP_LUMA);
|
||||
auto byte = TRY(stream.read_value<u8>());
|
||||
u8 diff_green = (first_byte & ~QOI_MASK_2);
|
||||
u8 dr_dg = (byte & 0b11110000) >> 4;
|
||||
u8 db_dg = (byte & 0b00001111);
|
||||
|
||||
// Values are stored as unsigned integers with a bias of 32 for the green channel and a bias of 8 for the red and blue channel.
|
||||
return Color {
|
||||
static_cast<u8>(pixel.red() + static_cast<i8>((diff_green - 32) + (dr_dg - 8))),
|
||||
static_cast<u8>(pixel.green() + static_cast<i8>(diff_green - 32)),
|
||||
static_cast<u8>(pixel.blue() + static_cast<i8>((diff_green - 32) + (db_dg - 8))),
|
||||
pixel.alpha(),
|
||||
};
|
||||
}
|
||||
|
||||
static ErrorOr<u8> decode_qoi_op_run(Stream&, u8 first_byte)
|
||||
{
|
||||
VERIFY((first_byte & QOI_MASK_2) == QOI_OP_RUN);
|
||||
u8 run = first_byte & ~QOI_MASK_2;
|
||||
|
||||
// The run-length is stored with a bias of -1.
|
||||
run += 1;
|
||||
|
||||
// Note that the run-lengths 63 and 64 (b111110 and b111111) are illegal as they are occupied by the QOI_OP_RGB and QOI_OP_RGBA tags.
|
||||
if (run == QOI_OP_RGB || run == QOI_OP_RGBA)
|
||||
return Error::from_string_literal("Invalid QOI image: illegal run length");
|
||||
|
||||
VERIFY(run >= 1 && run <= 62);
|
||||
return run;
|
||||
}
|
||||
|
||||
static ErrorOr<void> decode_qoi_end_marker(Stream& stream)
|
||||
{
|
||||
u8 bytes[array_size(END_MARKER)];
|
||||
TRY(stream.read_until_filled({ &bytes, array_size(bytes) }));
|
||||
if (!stream.is_eof())
|
||||
return Error::from_string_literal("Invalid QOI image: expected end of stream but more bytes are available");
|
||||
if (memcmp(&END_MARKER, &bytes, array_size(bytes)) != 0)
|
||||
return Error::from_string_literal("Invalid QOI image: incorrect end marker");
|
||||
return {};
|
||||
}
|
||||
|
||||
static ErrorOr<NonnullRefPtr<Bitmap>> decode_qoi_image(Stream& stream, u32 width, u32 height)
|
||||
{
|
||||
// FIXME: Why is Gfx::Bitmap's size signed? Makes no sense whatsoever.
|
||||
if (width > NumericLimits<int>::max())
|
||||
return Error::from_string_literal("Cannot create bitmap for QOI image of valid size, width exceeds maximum Gfx::Bitmap width");
|
||||
if (height > NumericLimits<int>::max())
|
||||
return Error::from_string_literal("Cannot create bitmap for QOI image of valid size, height exceeds maximum Gfx::Bitmap height");
|
||||
|
||||
auto bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, { width, height }));
|
||||
|
||||
u8 run = 0;
|
||||
Color pixel = { 0, 0, 0, 255 };
|
||||
Color previous_pixels[64] {};
|
||||
|
||||
for (u32 y = 0; y < height; ++y) {
|
||||
for (u32 x = 0; x < width; ++x) {
|
||||
if (run > 0)
|
||||
--run;
|
||||
if (run == 0) {
|
||||
auto first_byte = TRY(stream.read_value<u8>());
|
||||
if (first_byte == QOI_OP_RGB)
|
||||
pixel = TRY(decode_qoi_op_rgb(stream, first_byte, pixel));
|
||||
else if (first_byte == QOI_OP_RGBA)
|
||||
pixel = TRY(decode_qoi_op_rgba(stream, first_byte));
|
||||
else if ((first_byte & QOI_MASK_2) == QOI_OP_INDEX)
|
||||
pixel = previous_pixels[TRY(decode_qoi_op_index(stream, first_byte))];
|
||||
else if ((first_byte & QOI_MASK_2) == QOI_OP_DIFF)
|
||||
pixel = TRY(decode_qoi_op_diff(stream, first_byte, pixel));
|
||||
else if ((first_byte & QOI_MASK_2) == QOI_OP_LUMA)
|
||||
pixel = TRY(decode_qoi_op_luma(stream, first_byte, pixel));
|
||||
else if ((first_byte & QOI_MASK_2) == QOI_OP_RUN)
|
||||
run = TRY(decode_qoi_op_run(stream, first_byte));
|
||||
else
|
||||
return Error::from_string_literal("Invalid QOI image: unknown chunk tag");
|
||||
}
|
||||
auto index_position = (pixel.red() * 3 + pixel.green() * 5 + pixel.blue() * 7 + pixel.alpha() * 11) % 64;
|
||||
previous_pixels[index_position] = pixel;
|
||||
bitmap->set_pixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
TRY(decode_qoi_end_marker(stream));
|
||||
return { move(bitmap) };
|
||||
}
|
||||
|
||||
QOIImageDecoderPlugin::QOIImageDecoderPlugin(NonnullOwnPtr<Stream> stream)
|
||||
{
|
||||
m_context = make<QOILoadingContext>();
|
||||
m_context->stream = move(stream);
|
||||
}
|
||||
|
||||
IntSize QOIImageDecoderPlugin::size()
|
||||
{
|
||||
return { m_context->header.width, m_context->header.height };
|
||||
}
|
||||
|
||||
bool QOIImageDecoderPlugin::sniff(ReadonlyBytes data)
|
||||
{
|
||||
FixedMemoryStream stream { { data.data(), data.size() } };
|
||||
return !decode_qoi_header(stream).is_error();
|
||||
}
|
||||
|
||||
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> QOIImageDecoderPlugin::create(ReadonlyBytes data)
|
||||
{
|
||||
auto stream = TRY(try_make<FixedMemoryStream>(data));
|
||||
auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) QOIImageDecoderPlugin(move(stream))));
|
||||
TRY(plugin->decode_header_and_update_context());
|
||||
return plugin;
|
||||
}
|
||||
|
||||
ErrorOr<ImageFrameDescriptor> QOIImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
|
||||
{
|
||||
if (index > 0)
|
||||
return Error::from_string_literal("Invalid frame index");
|
||||
|
||||
// No one should try to decode the frame again after an error was already returned.
|
||||
VERIFY(m_context->state != QOILoadingContext::State::Error);
|
||||
|
||||
if (m_context->state == QOILoadingContext::State::HeaderDecoded)
|
||||
TRY(decode_image_and_update_context());
|
||||
|
||||
VERIFY(m_context->state == QOILoadingContext::State::ImageDecoded);
|
||||
VERIFY(m_context->bitmap);
|
||||
return ImageFrameDescriptor { m_context->bitmap, 0 };
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIImageDecoderPlugin::decode_header_and_update_context()
|
||||
{
|
||||
VERIFY(m_context->state < QOILoadingContext::State::HeaderDecoded);
|
||||
m_context->header = TRY(decode_qoi_header(*m_context->stream));
|
||||
m_context->state = QOILoadingContext::State::HeaderDecoded;
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIImageDecoderPlugin::decode_image_and_update_context()
|
||||
{
|
||||
VERIFY(m_context->state < QOILoadingContext::State::ImageDecoded);
|
||||
auto error_or_bitmap = decode_qoi_image(*m_context->stream, m_context->header.width, m_context->header.height);
|
||||
if (error_or_bitmap.is_error()) {
|
||||
m_context->state = QOILoadingContext::State::Error;
|
||||
return error_or_bitmap.release_error();
|
||||
}
|
||||
m_context->state = QOILoadingContext::State::ImageDecoded;
|
||||
m_context->bitmap = error_or_bitmap.release_value();
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <LibGfx/Forward.h>
|
||||
#include <LibGfx/ImageFormats/ImageDecoder.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
// Decoder for the "Quite OK Image" format (v1.0).
|
||||
// https://qoiformat.org/qoi-specification.pdf
|
||||
|
||||
struct [[gnu::packed]] QOIHeader {
|
||||
char magic[4];
|
||||
u32 width;
|
||||
u32 height;
|
||||
u8 channels;
|
||||
u8 colorspace;
|
||||
};
|
||||
|
||||
struct QOILoadingContext {
|
||||
enum class State {
|
||||
NotDecoded = 0,
|
||||
HeaderDecoded,
|
||||
ImageDecoded,
|
||||
Error,
|
||||
};
|
||||
State state { State::NotDecoded };
|
||||
OwnPtr<Stream> stream {};
|
||||
QOIHeader header {};
|
||||
RefPtr<Bitmap> bitmap;
|
||||
};
|
||||
|
||||
class QOIImageDecoderPlugin final : public ImageDecoderPlugin {
|
||||
public:
|
||||
static bool sniff(ReadonlyBytes);
|
||||
static ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> create(ReadonlyBytes);
|
||||
|
||||
virtual ~QOIImageDecoderPlugin() override = default;
|
||||
|
||||
virtual IntSize size() override;
|
||||
|
||||
virtual ErrorOr<ImageFrameDescriptor> frame(size_t index, Optional<IntSize> ideal_size = {}) override;
|
||||
|
||||
private:
|
||||
ErrorOr<void> decode_header_and_update_context();
|
||||
ErrorOr<void> decode_image_and_update_context();
|
||||
|
||||
QOIImageDecoderPlugin(NonnullOwnPtr<Stream>);
|
||||
|
||||
OwnPtr<QOILoadingContext> m_context;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
template<>
|
||||
struct AK::Traits<Gfx::QOIHeader> : public DefaultTraits<Gfx::QOIHeader> {
|
||||
static constexpr bool is_trivially_serializable() { return true; }
|
||||
};
|
|
@ -1,224 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Olivier De Cannière <olivier.decanniere96@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "QOIWriter.h"
|
||||
#include <AK/Endian.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
static constexpr Array<u8, 4> qoi_magic_bytes = { 'q', 'o', 'i', 'f' };
|
||||
static constexpr Array<u8, 8> qoi_end_marker = { 0, 0, 0, 0, 0, 0, 0, 1 };
|
||||
|
||||
enum class Colorspace {
|
||||
sRGB,
|
||||
Linear,
|
||||
};
|
||||
|
||||
enum class Channels {
|
||||
RGB,
|
||||
RGBA,
|
||||
};
|
||||
|
||||
ErrorOr<ByteBuffer> QOIWriter::encode(Bitmap const& bitmap)
|
||||
{
|
||||
QOIWriter writer;
|
||||
TRY(writer.add_header(bitmap.width(), bitmap.height(), Channels::RGBA, Colorspace::sRGB));
|
||||
|
||||
Color previous_pixel = { 0, 0, 0, 255 };
|
||||
|
||||
bool creating_run = false;
|
||||
int run_length = 0;
|
||||
|
||||
for (auto y = 0; y < bitmap.height(); y++) {
|
||||
for (auto x = 0; x < bitmap.width(); x++) {
|
||||
auto pixel = bitmap.get_pixel(x, y);
|
||||
|
||||
// Check for at most 62 consecutive identical pixels.
|
||||
if (pixel == previous_pixel) {
|
||||
if (!creating_run) {
|
||||
creating_run = true;
|
||||
run_length = 0;
|
||||
writer.insert_into_running_array(pixel);
|
||||
}
|
||||
|
||||
run_length++;
|
||||
|
||||
// If the run reaches a maximum length of 62 or if this is the last pixel then create the chunk.
|
||||
if (run_length == 62 || (y == bitmap.height() - 1 && x == bitmap.width() - 1)) {
|
||||
TRY(writer.add_run_chunk(run_length));
|
||||
creating_run = false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Run ended with the previous pixel. Create a chunk for it and continue processing this pixel.
|
||||
if (creating_run) {
|
||||
TRY(writer.add_run_chunk(run_length));
|
||||
creating_run = false;
|
||||
}
|
||||
|
||||
// Check if the pixel matches a pixel in the running array.
|
||||
auto index = pixel_hash_function(pixel);
|
||||
auto& array_pixel = writer.running_array[index];
|
||||
if (array_pixel == pixel) {
|
||||
TRY(writer.add_index_chunk(index));
|
||||
previous_pixel = pixel;
|
||||
continue;
|
||||
}
|
||||
|
||||
writer.running_array[index] = pixel;
|
||||
|
||||
// Check if pixel can be expressed as a difference of the previous pixel.
|
||||
if (pixel.alpha() == previous_pixel.alpha()) {
|
||||
int red_difference = pixel.red() - previous_pixel.red();
|
||||
int green_difference = pixel.green() - previous_pixel.green();
|
||||
int blue_difference = pixel.blue() - previous_pixel.blue();
|
||||
int relative_red_difference = red_difference - green_difference;
|
||||
int relative_blue_difference = blue_difference - green_difference;
|
||||
|
||||
if (red_difference > -3 && red_difference < 2
|
||||
&& green_difference > -3 && green_difference < 2
|
||||
&& blue_difference > -3 && blue_difference < 2) {
|
||||
TRY(writer.add_diff_chunk(red_difference, green_difference, blue_difference));
|
||||
previous_pixel = pixel;
|
||||
continue;
|
||||
}
|
||||
if (relative_red_difference > -9 && relative_red_difference < 8
|
||||
&& green_difference > -33 && green_difference < 32
|
||||
&& relative_blue_difference > -9 && relative_blue_difference < 8) {
|
||||
TRY(writer.add_luma_chunk(relative_red_difference, green_difference, relative_blue_difference));
|
||||
previous_pixel = pixel;
|
||||
continue;
|
||||
}
|
||||
|
||||
TRY(writer.add_rgb_chunk(pixel.red(), pixel.green(), pixel.blue()));
|
||||
previous_pixel = pixel;
|
||||
continue;
|
||||
}
|
||||
|
||||
previous_pixel = pixel;
|
||||
|
||||
// Write full color values.
|
||||
TRY(writer.add_rgba_chunk(pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()));
|
||||
}
|
||||
}
|
||||
|
||||
TRY(writer.add_end_marker());
|
||||
|
||||
return ByteBuffer::copy(writer.m_data);
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIWriter::add_header(u32 width, u32 height, Channels channels = Channels::RGBA, Colorspace color_space = Colorspace::sRGB)
|
||||
{
|
||||
// FIXME: Handle RGB and all linear channels.
|
||||
if (channels == Channels::RGB || color_space == Colorspace::Linear)
|
||||
TODO();
|
||||
|
||||
TRY(m_data.try_append(qoi_magic_bytes.data(), sizeof(qoi_magic_bytes)));
|
||||
|
||||
auto big_endian_width = AK::convert_between_host_and_big_endian(width);
|
||||
TRY(m_data.try_append(bit_cast<u8*>(&big_endian_width), sizeof(width)));
|
||||
|
||||
auto big_endian_height = AK::convert_between_host_and_big_endian(height);
|
||||
TRY(m_data.try_append(bit_cast<u8*>(&big_endian_height), sizeof(height)));
|
||||
|
||||
// Number of channels: 3 = RGB, 4 = RGBA.
|
||||
TRY(m_data.try_append(4));
|
||||
|
||||
// Colorspace: 0 = sRGB, 1 = all linear channels.
|
||||
TRY(m_data.try_append(color_space == Colorspace::sRGB ? 0 : 1));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIWriter::add_rgb_chunk(u8 r, u8 g, u8 b)
|
||||
{
|
||||
constexpr static u8 rgb_tag = 0b1111'1110;
|
||||
|
||||
TRY(m_data.try_append(rgb_tag));
|
||||
TRY(m_data.try_append(r));
|
||||
TRY(m_data.try_append(g));
|
||||
TRY(m_data.try_append(b));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIWriter::add_rgba_chunk(u8 r, u8 g, u8 b, u8 a)
|
||||
{
|
||||
constexpr static u8 rgba_tag = 0b1111'1111;
|
||||
|
||||
TRY(m_data.try_append(rgba_tag));
|
||||
TRY(m_data.try_append(r));
|
||||
TRY(m_data.try_append(g));
|
||||
TRY(m_data.try_append(b));
|
||||
TRY(m_data.try_append(a));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIWriter::add_index_chunk(unsigned int index)
|
||||
{
|
||||
constexpr static u8 index_tag = 0b0000'0000;
|
||||
|
||||
u8 chunk = index_tag | index;
|
||||
TRY(m_data.try_append(chunk));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIWriter::add_diff_chunk(i8 red_difference, i8 green_difference, i8 blue_difference)
|
||||
{
|
||||
constexpr static u8 diff_tag = 0b0100'0000;
|
||||
|
||||
u8 bias = 2;
|
||||
u8 red = red_difference + bias;
|
||||
u8 green = green_difference + bias;
|
||||
u8 blue = blue_difference + bias;
|
||||
|
||||
u8 chunk = diff_tag | (red << 4) | (green << 2) | blue;
|
||||
TRY(m_data.try_append(chunk));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIWriter::add_luma_chunk(i8 relative_red_difference, i8 green_difference, i8 relative_blue_difference)
|
||||
{
|
||||
constexpr static u8 luma_tag = 0b1000'0000;
|
||||
u8 green_bias = 32;
|
||||
u8 red_blue_bias = 8;
|
||||
|
||||
u8 chunk1 = luma_tag | (green_difference + green_bias);
|
||||
u8 chunk2 = ((relative_red_difference + red_blue_bias) << 4) | (relative_blue_difference + red_blue_bias);
|
||||
TRY(m_data.try_append(chunk1));
|
||||
TRY(m_data.try_append(chunk2));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIWriter::add_run_chunk(unsigned run_length)
|
||||
{
|
||||
constexpr static u8 run_tag = 0b1100'0000;
|
||||
int bias = -1;
|
||||
|
||||
u8 chunk = run_tag | (run_length + bias);
|
||||
TRY(m_data.try_append(chunk));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> QOIWriter::add_end_marker()
|
||||
{
|
||||
TRY(m_data.try_append(qoi_end_marker.data(), sizeof(qoi_end_marker)));
|
||||
return {};
|
||||
}
|
||||
|
||||
u32 QOIWriter::pixel_hash_function(Color pixel)
|
||||
{
|
||||
return (pixel.red() * 3 + pixel.green() * 5 + pixel.blue() * 7 + pixel.alpha() * 11) % 64;
|
||||
}
|
||||
|
||||
void QOIWriter::insert_into_running_array(Color pixel)
|
||||
{
|
||||
auto index = pixel_hash_function(pixel);
|
||||
running_array[index] = pixel;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Olivier De Cannière <olivier.decanniere96@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
enum class Colorspace;
|
||||
enum class Channels;
|
||||
|
||||
class QOIWriter {
|
||||
public:
|
||||
static ErrorOr<ByteBuffer> encode(Gfx::Bitmap const&);
|
||||
|
||||
private:
|
||||
QOIWriter() = default;
|
||||
|
||||
Vector<u8> m_data;
|
||||
ErrorOr<void> add_header(u32 width, u32 height, Channels, Colorspace);
|
||||
ErrorOr<void> add_rgb_chunk(u8, u8, u8);
|
||||
ErrorOr<void> add_rgba_chunk(u8, u8, u8, u8);
|
||||
ErrorOr<void> add_index_chunk(u32 index);
|
||||
ErrorOr<void> add_diff_chunk(i8 red_difference, i8 green_difference, i8 blue_difference);
|
||||
ErrorOr<void> add_luma_chunk(i8 relative_red_difference, i8 green_difference, i8 relative_blue_difference);
|
||||
ErrorOr<void> add_run_chunk(u32 run_length);
|
||||
ErrorOr<void> add_end_marker();
|
||||
|
||||
Array<Color, 64> running_array;
|
||||
static u32 pixel_hash_function(Color pixel);
|
||||
void insert_into_running_array(Color pixel);
|
||||
};
|
||||
|
||||
}
|
|
@ -97,11 +97,6 @@ void Resource::did_load(Badge<ResourceLoader>, ReadonlyBytes data, HTTP::HeaderM
|
|||
if (content_type.has_value()) {
|
||||
dbgln_if(RESOURCE_DEBUG, "Content-Type header: '{}'", content_type.value());
|
||||
m_mime_type = mime_type_from_content_type(content_type.value());
|
||||
// FIXME: "The Quite OK Image Format" doesn't have an official mime type yet,
|
||||
// and servers like nginx will send a generic octet-stream mime type instead.
|
||||
// Let's use image/x-qoi for now, which is also what our Core::MimeData uses & would guess.
|
||||
if (m_mime_type == "application/octet-stream" && url().serialize_path().ends_with(".qoi"sv))
|
||||
m_mime_type = "image/x-qoi";
|
||||
} else {
|
||||
auto content_type_options = headers.get("X-Content-Type-Options");
|
||||
if (content_type_options.value_or("").equals_ignoring_ascii_case("nosniff"sv)) {
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
#include <LibGfx/ImageFormats/JPEGWriter.h>
|
||||
#include <LibGfx/ImageFormats/PNGWriter.h>
|
||||
#include <LibGfx/ImageFormats/PortableFormatWriter.h>
|
||||
#include <LibGfx/ImageFormats/QOIWriter.h>
|
||||
#include <LibGfx/ImageFormats/WebPSharedLossless.h>
|
||||
#include <LibGfx/ImageFormats/WebPWriter.h>
|
||||
|
||||
|
@ -186,10 +185,8 @@ static ErrorOr<void> save_image(LoadedImage& image, StringView out_path, bool pp
|
|||
bytes = TRY(Gfx::BMPWriter::encode(*frame, { .icc_data = image.icc_data }));
|
||||
} else if (out_path.ends_with(".png"sv, CaseSensitivity::CaseInsensitive)) {
|
||||
bytes = TRY(Gfx::PNGWriter::encode(*frame, { .icc_data = image.icc_data }));
|
||||
} else if (out_path.ends_with(".qoi"sv, CaseSensitivity::CaseInsensitive)) {
|
||||
bytes = TRY(Gfx::QOIWriter::encode(*frame));
|
||||
} else {
|
||||
return Error::from_string_view("can only write .bmp, .gif, .jpg, .png, .ppm, .qoi, and .webp"sv);
|
||||
return Error::from_string_view("can only write .bmp, .gif, .jpg, .png, .ppm, and .webp"sv);
|
||||
}
|
||||
TRY(TRY(stream())->write_until_depleted(bytes));
|
||||
|
||||
|
|
Loading…
Reference in a new issue