LibGfx: Remove QOI image format support

This format is not supported by other browsers.
This commit is contained in:
Andreas Kling 2024-06-16 19:10:08 +02:00 committed by Andreas Kling
parent 2a888ca626
commit 4b4254c3d0
Notes: sideshowbarker 2024-07-17 00:59:43 +09:00
13 changed files with 1 additions and 601 deletions

View file

@ -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;
}

View file

@ -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)

View file

@ -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",

View file

@ -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()))));

View file

@ -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 },

View file

@ -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

View file

@ -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 },

View file

@ -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 {};
}
}

View file

@ -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; }
};

View file

@ -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;
}
}

View file

@ -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);
};
}

View file

@ -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)) {

View file

@ -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));