AK: Remove Statistics.h

This also removes MedianCut and GIFWriter
This commit is contained in:
Pavel Shliak 2024-12-05 03:19:36 +04:00 committed by Alexander Kalenik
parent ae6edfb845
commit ede0dbafc6
Notes: github-actions[bot] 2024-12-05 15:54:23 +00:00
13 changed files with 0 additions and 845 deletions

View file

@ -1,129 +0,0 @@
/*
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Concepts.h>
#include <AK/Math.h>
#include <AK/QuickSelect.h>
#include <AK/QuickSort.h>
#include <AK/Vector.h>
namespace AK {
static constexpr int ODD_NAIVE_MEDIAN_CUTOFF = 200;
static constexpr int EVEN_NAIVE_MEDIAN_CUTOFF = 350;
template<Arithmetic T = float, typename ContainerType = Vector<T>>
class Statistics {
public:
Statistics() = default;
~Statistics() = default;
explicit Statistics(ContainerType&& existing_container)
: m_values(forward<ContainerType>(existing_container))
{
for (auto const& value : m_values)
m_sum += value;
}
void add(T const& value)
{
// FIXME: Check for an overflow
m_sum += value;
m_values.append(value);
}
T const sum() const { return m_sum; }
// FIXME: Unclear Wording, average can mean a lot of different things
// Median, Arithmetic Mean (which this is), Geometric Mean, Harmonic Mean etc
float average() const
{
// Let's assume the average of an empty dataset is 0
if (size() == 0)
return 0;
// TODO: sum might overflow so maybe do multiple partial sums and intermediate divisions here
return (float)sum() / size();
}
T const min() const
{
// Lets Rather fail than read over the end of a collection
VERIFY(size() != 0);
T minimum = m_values[0];
for (T number : values()) {
if (number < minimum) {
minimum = number;
}
}
return minimum;
}
T const max() const
{
// Lets Rather fail than read over the end of a collection
VERIFY(size() != 0);
T maximum = m_values[0];
for (T number : values()) {
if (number > maximum) {
maximum = number;
}
}
return maximum;
}
T const median()
{
// Let's assume the Median of an empty dataset is 0
if (size() == 0)
return 0;
// If the number of values is even, the median is the arithmetic mean of the two middle values
if (size() <= EVEN_NAIVE_MEDIAN_CUTOFF && size() % 2 == 0) {
quick_sort(m_values);
return (m_values.at(size() / 2) + m_values.at(size() / 2 - 1)) / 2;
} else if (size() <= ODD_NAIVE_MEDIAN_CUTOFF && size() % 2 == 1) {
quick_sort(m_values);
return m_values.at(m_values.size() / 2);
} else if (size() % 2 == 0) {
auto index = size() / 2;
auto median1 = m_values.at(AK::quickselect_inplace(m_values, index));
auto median2 = m_values.at(AK::quickselect_inplace(m_values, index - 1));
return (median1 + median2) / 2;
}
return m_values.at(AK::quickselect_inplace(m_values, size() / 2));
}
float standard_deviation() const { return sqrt(variance()); }
float variance() const
{
float summation = 0;
float avg = average();
for (T number : values()) {
float difference = (float)number - avg;
summation += (difference * difference);
}
summation = summation / size();
return summation;
}
ContainerType const& values() const { return m_values; }
size_t size() const { return m_values.size(); }
private:
ContainerType m_values;
T m_sum {};
};
}
#if USING_AK_GLOBALLY
using AK::Statistics;
#endif

View file

@ -32,7 +32,6 @@ set(SOURCES
ImageFormats/BooleanDecoder.cpp
ImageFormats/CCITTDecoder.cpp
ImageFormats/GIFLoader.cpp
ImageFormats/GIFWriter.cpp
ImageFormats/ICOLoader.cpp
ImageFormats/ImageDecoder.cpp
ImageFormats/JPEGLoader.cpp
@ -48,7 +47,6 @@ set(SOURCES
ImageFormats/WebPWriterLossless.cpp
ImageFormats/AVIFLoader.cpp
ImmutableBitmap.cpp
MedianCut.cpp
PaintingSurface.cpp
Palette.cpp
Path.cpp

View file

@ -1,264 +0,0 @@
/*
* Copyright (c) 2024, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/BitStream.h>
#include <LibCompress/Lzw.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/ImageFormats/GIFWriter.h>
#include <LibGfx/MedianCut.h>
namespace Gfx {
namespace {
ErrorOr<void> write_header(Stream& stream)
{
// 17. Header
TRY(stream.write_until_depleted("GIF89a"sv));
return {};
}
ErrorOr<void> write_logical_descriptor(BigEndianOutputBitStream& stream, IntSize size)
{
// 18. Logical Screen Descriptor
if (size.width() > NumericLimits<u16>::max() || size.height() > NumericLimits<u16>::max())
return Error::from_string_literal("Bitmap size is too big for a GIF");
TRY(stream.write_value<u16>(size.width()));
TRY(stream.write_value<u16>(size.height()));
// Global Color Table Flag
TRY(stream.write_bits(false, 1));
// Color Resolution
TRY(stream.write_bits(6u, 3));
// Sort Flag
TRY(stream.write_bits(false, 1));
// Size of Global Color Table
TRY(stream.write_bits(0u, 3));
// Background Color Index
TRY(stream.write_value<u8>(0));
// Pixel Aspect Ratio
// NOTE: We can write a zero as most decoders discard the value.
TRY(stream.write_value<u8>(0));
return {};
}
ErrorOr<void> write_color_table(Stream& stream, ColorPalette const& palette)
{
// 19. Global Color Table or 21. Local Color Table.
for (u16 i = 0; i < 256; ++i) {
auto const color = i < palette.palette().size() ? palette.palette()[i] : Color::NamedColor::White;
TRY(stream.write_value<u8>(color.red()));
TRY(stream.write_value<u8>(color.green()));
TRY(stream.write_value<u8>(color.blue()));
}
return {};
}
ErrorOr<void> write_image_data(Stream& stream, Bitmap const& bitmap, ColorPalette const& palette)
{
// 22. Table Based Image Data
auto const pixel_number = static_cast<u32>(bitmap.width() * bitmap.height());
auto indexes = TRY(ByteBuffer::create_uninitialized(pixel_number));
for (u32 i = 0; i < pixel_number; ++i) {
auto const color = Color::from_argb(*(bitmap.begin() + i));
indexes[i] = palette.index_of_closest_color(color);
}
constexpr u8 lzw_minimum_code_size = 8;
auto const encoded = TRY(Compress::LzwCompressor::compress_all(move(indexes), lzw_minimum_code_size));
auto const number_of_subblocks = ceil_div(encoded.size(), 255ul);
TRY(stream.write_value<u8>(lzw_minimum_code_size));
for (u32 i = 0; i < number_of_subblocks; ++i) {
auto const offset = i * 255;
auto const to_write = min(255, encoded.size() - offset);
TRY(stream.write_value<u8>(to_write));
TRY(stream.write_until_depleted(encoded.bytes().slice(offset, to_write)));
}
// Block terminator
TRY(stream.write_value<u8>(0));
return {};
}
ErrorOr<void> write_image_descriptor(BigEndianOutputBitStream& stream, Bitmap const& bitmap, IntPoint at = {})
{
// 20. Image Descriptor
// Image Separator
TRY(stream.write_value<u8>(0x2c));
// Image Left Position
TRY(stream.write_value<u16>(at.x()));
// Image Top Position
TRY(stream.write_value<u16>(at.y()));
// Image Width
TRY(stream.write_value<u16>(bitmap.width()));
// Image Height
TRY(stream.write_value<u16>(bitmap.height()));
// Local Color Table Flag
TRY(stream.write_bits(true, 1));
// Interlace Flag
TRY(stream.write_bits(false, 1));
// Sort Flag
TRY(stream.write_bits(false, 1));
// Reserved
TRY(stream.write_bits(0u, 2));
// Size of Local Color Table
TRY(stream.write_bits(7u, 3));
return {};
}
ErrorOr<void> write_graphic_control_extension(BigEndianOutputBitStream& stream, int duration_ms)
{
// 23. Graphic Control Extension
// Extension Introducer
TRY(stream.write_value<u8>(0x21));
// Graphic Control Label
TRY(stream.write_value<u8>(0xF9));
// Block Size
TRY(stream.write_value<u8>(4));
// Packed Field
// Reserved
TRY(stream.write_bits(0u, 3));
// Disposal Method
TRY(stream.write_bits(0u, 3));
// User Input Flag
TRY(stream.write_bits(false, 1));
// Transparency Flag
TRY(stream.write_bits(false, 1));
// Delay Time
TRY(stream.write_value<u16>(duration_ms / 10));
// Transparent Color Index
TRY(stream.write_value<u8>(0));
// Block Terminator
TRY(stream.write_value<u8>(0));
return {};
}
ErrorOr<void> write_trailer(Stream& stream)
{
TRY(stream.write_value<u8>(0x3B));
return {};
}
class GIFAnimationWriter : public AnimationWriter {
public:
GIFAnimationWriter(SeekableStream& stream)
: m_stream(stream)
{
}
virtual ErrorOr<void> add_frame(Bitmap&, int, IntPoint) override;
private:
SeekableStream& m_stream;
bool m_is_first_frame { true };
};
ErrorOr<void> GIFAnimationWriter::add_frame(Bitmap& bitmap, int duration_ms, IntPoint at = {})
{
// Let's get rid of the previously written trailer
if (!m_is_first_frame)
TRY(m_stream.seek(-1, SeekMode::FromCurrentPosition));
m_is_first_frame = false;
// Write a Table-Based Image
BigEndianOutputBitStream bit_stream { MaybeOwned { m_stream } };
TRY(write_graphic_control_extension(bit_stream, duration_ms));
TRY(write_image_descriptor(bit_stream, bitmap, at));
auto const palette = TRY(median_cut(bitmap, 256));
TRY(write_color_table(m_stream, palette));
TRY(write_image_data(m_stream, bitmap, palette));
// We always write a trailer to ensure that the file is valid.
TRY(write_trailer(m_stream));
return {};
}
ErrorOr<void> write_netscape_extension(BigEndianOutputBitStream& stream, u16 loop_count)
{
// This is a vendor extension, its sole usage is to provide the loop count.
// I used this link as a source: https://web.archive.org/web/19990418091037/http://www6.uniovi.es/gifanim/gifabout.htm
// Extension Introducer
TRY(stream.write_value<u8>(0x21));
// Application Extension Label
TRY(stream.write_value<u8>(0xFF));
// Block Size
constexpr auto netscape_signature = "NETSCAPE2.0"sv;
TRY(stream.write_value<u8>(netscape_signature.length()));
TRY(stream.write_until_depleted(netscape_signature));
// Length of Data Sub-Block
TRY(stream.write_value<u8>(3));
// Undocumented
TRY(stream.write_value<u8>(1));
// Number of loops, 0 means infinite
TRY(stream.write_value<u16>(loop_count));
// Block Terminator
TRY(stream.write_value<u8>(0));
return {};
}
}
ErrorOr<void> GIFWriter::encode(Stream& stream, Bitmap const& bitmap)
{
auto const palette = TRY(median_cut(bitmap, 256));
TRY(write_header(stream));
BigEndianOutputBitStream bit_stream { MaybeOwned<Stream> { stream } };
TRY(write_logical_descriptor(bit_stream, bitmap.size()));
// Write a Table-Based Image
TRY(write_image_descriptor(bit_stream, bitmap));
TRY(write_color_table(bit_stream, palette));
TRY(write_image_data(stream, bitmap, palette));
TRY(write_trailer(bit_stream));
return {};
}
ErrorOr<NonnullOwnPtr<AnimationWriter>> GIFWriter::start_encoding_animation(SeekableStream& stream, IntSize dimensions, u16 loop_count)
{
TRY(write_header(stream));
BigEndianOutputBitStream bit_stream { MaybeOwned<Stream> { stream } };
TRY(write_logical_descriptor(bit_stream, dimensions));
// Vendor extension to support looping
TRY(write_netscape_extension(bit_stream, loop_count));
return make<GIFAnimationWriter>(stream);
}
}

View file

@ -1,23 +0,0 @@
/*
* Copyright (c) 2024, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <LibGfx/Forward.h>
#include <LibGfx/ImageFormats/AnimationWriter.h>
namespace Gfx {
// Specified at: https://www.w3.org/Graphics/GIF/spec-gif89a.txt
class GIFWriter {
public:
static ErrorOr<void> encode(Stream&, Bitmap const&);
static ErrorOr<NonnullOwnPtr<AnimationWriter>> start_encoding_animation(SeekableStream&, IntSize dimensions, u16 loop_count);
};
}

View file

@ -1,185 +0,0 @@
/*
* Copyright (c) 2024, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/QuickSort.h>
#include <AK/Statistics.h>
#include <LibGfx/MedianCut.h>
namespace Gfx {
namespace {
using Bucket = Vector<ARGB32>;
using Buckets = Vector<Bucket>;
void sort_along_color(Bucket& bucket, u8 color_index)
{
auto less_than = [=](ARGB32 first, ARGB32 second) {
auto const first_color = Color::from_argb(first);
auto const second_color = Color::from_argb(second);
switch (color_index) {
case 0:
return first_color.red() < second_color.red();
case 1:
return first_color.green() < second_color.green();
case 2:
return first_color.blue() < second_color.blue();
default:
VERIFY_NOT_REACHED();
}
};
AK::quick_sort(bucket, less_than);
}
template<typename T>
struct MaxAndIndex {
T maximum;
u32 index;
};
template<typename T, class GreaterThan>
MaxAndIndex<T> max_and_index(Span<T> values, GreaterThan greater_than)
{
VERIFY(values.size() != 0);
u32 max_index = 0;
RemoveCV<T> max_value = values[0];
for (u32 i = 0; i < values.size(); ++i) {
if (greater_than(values[i], max_value)) {
max_value = values[i];
max_index = i;
}
}
return { max_value, max_index };
}
ErrorOr<void> split_bucket(Buckets& buckets, u32 index_to_split_at, u8 color_index)
{
auto& to_split = buckets[index_to_split_at];
sort_along_color(to_split, color_index);
Bucket new_bucket {};
auto const middle = to_split.size() / 2;
auto const span_to_move = to_split.span().slice(middle);
// FIXME: Make Vector::try_extend() take a span
for (u32 i = 0; i < span_to_move.size(); ++i)
TRY(new_bucket.try_append(span_to_move[i]));
to_split.remove(middle, span_to_move.size());
TRY(buckets.try_append(move(new_bucket)));
return {};
}
struct IndexAndChannel {
u32 bucket_index {};
float score {};
u8 color_index {};
};
ErrorOr<Optional<IndexAndChannel>> find_largest_bucket(Buckets const& buckets)
{
Vector<IndexAndChannel> bucket_stats {};
for (u32 i = 0; i < buckets.size(); ++i) {
auto const& bucket = buckets[i];
if (bucket.size() == 1)
continue;
Statistics<u32> red {};
Statistics<u32> green {};
Statistics<u32> blue {};
for (auto const argb : bucket) {
auto const color = Color::from_argb(argb);
red.add(color.red());
green.add(color.green());
blue.add(color.blue());
}
Array const variances = { red.variance(), green.variance(), blue.variance() };
auto const stats = max_and_index(variances.span(), [](auto a, auto b) { return a > b; });
TRY(bucket_stats.try_append({ i, stats.maximum, static_cast<u8>(stats.index) }));
}
if (bucket_stats.size() == 0)
return OptionalNone {};
return bucket_stats[max_and_index(bucket_stats.span(), [](auto a, auto b) { return a.score > b.score; }).index];
}
ErrorOr<void> split_largest_bucket(Buckets& buckets)
{
if (auto const bucket_info = TRY(find_largest_bucket(buckets)); bucket_info.has_value())
TRY(split_bucket(buckets, bucket_info->bucket_index, bucket_info->color_index));
return {};
}
ErrorOr<ColorPalette> color_palette_from_buckets(Buckets const& buckets)
{
Vector<Color> palette;
HashMap<Color, ColorPalette::ColorAndIndex> conversion_table;
for (auto const& bucket : buckets) {
u32 average_r {};
u32 average_g {};
u32 average_b {};
for (auto const argb : bucket) {
auto const color = Color::from_argb(argb);
average_r += color.red();
average_g += color.green();
average_b += color.blue();
}
auto const bucket_size = bucket.size();
auto const average_color = Color(
round_to<u32>(static_cast<double>(average_r) / bucket_size),
round_to<u32>(static_cast<double>(average_g) / bucket_size),
round_to<u32>(static_cast<double>(average_b) / bucket_size));
TRY(palette.try_append(average_color));
for (auto const color : bucket)
TRY(conversion_table.try_set(Color::from_argb(color), { average_color, palette.size() - 1 }));
}
return ColorPalette { move(palette), move(conversion_table) };
}
}
ErrorOr<ColorPalette> median_cut(Bitmap const& bitmap, u16 palette_size)
{
HashTable<ARGB32> color_set;
for (auto color : bitmap)
TRY(color_set.try_set(color));
Vector<ARGB32> first_bucket;
TRY(first_bucket.try_ensure_capacity(color_set.size()));
for (auto const color : color_set)
first_bucket.append(color);
Buckets bucket_list;
TRY(bucket_list.try_append(first_bucket));
u16 old_bucket_size = 0;
while (bucket_list.size() > old_bucket_size && bucket_list.size() < palette_size) {
old_bucket_size = bucket_list.size();
TRY(split_largest_bucket(bucket_list));
}
return color_palette_from_buckets(bucket_list);
}
}

View file

@ -1,53 +0,0 @@
/*
* Copyright (c) 2024, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/Vector.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Color.h>
namespace Gfx {
class ColorPalette {
public:
struct ColorAndIndex {
Color color;
size_t index;
};
ColorPalette(Vector<Color> palette, HashMap<Color, ColorAndIndex> conversion_table)
: m_palette(move(palette))
, m_conversion_table(move(conversion_table))
{
}
Vector<Color> const& palette() const
{
return m_palette;
}
Color closest_color(Color input) const
{
return m_palette[index_of_closest_color(input)];
}
u32 index_of_closest_color(Color input) const
{
if (auto const result = m_conversion_table.get(input); result.has_value())
return result->index;
TODO();
}
private:
Vector<Color> m_palette;
HashMap<Color, ColorAndIndex> m_conversion_table;
};
ErrorOr<ColorPalette> median_cut(Bitmap const& bitmap, u16 palette_size);
}

View file

@ -66,7 +66,6 @@ set(AK_TEST_SOURCES
TestSourceLocation.cpp
TestSpan.cpp
TestStack.cpp
TestStatistics.cpp
TestStdLibExtras.cpp
TestString.cpp
TestStringFloatingPointConversions.cpp

View file

@ -1,70 +0,0 @@
/*
* Copyright (c) 2023, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Statistics.h>
#include <LibTest/TestSuite.h>
TEST_CASE(Statistics)
{
// Setup Test Data
AK::Statistics<double> odd_number_elements;
AK::Statistics<double> even_number_elements;
AK::Statistics<double> odd_number_elements_large;
AK::Statistics<double> even_number_elements_large;
odd_number_elements.add(5.0);
odd_number_elements.add(4.0);
odd_number_elements.add(3.0);
odd_number_elements.add(2.0);
odd_number_elements.add(1.0);
even_number_elements.add(6.0);
even_number_elements.add(5.0);
even_number_elements.add(4.0);
even_number_elements.add(3.0);
even_number_elements.add(2.0);
even_number_elements.add(1.0);
for (int i = 201; i > 0; i--) {
odd_number_elements_large.add(i);
}
for (int i = 360; i > 0; i--) {
even_number_elements_large.add(i);
}
// Sum
EXPECT_APPROXIMATE(odd_number_elements.sum(), 15.0);
EXPECT_APPROXIMATE(even_number_elements.sum(), 21.0);
// Average
EXPECT_APPROXIMATE(odd_number_elements.average(), 3.0);
EXPECT_APPROXIMATE(even_number_elements.average(), 3.5);
// Min
EXPECT_APPROXIMATE(odd_number_elements.min(), 1.0);
EXPECT_APPROXIMATE(even_number_elements.min(), 1.0);
// Max
EXPECT_APPROXIMATE(odd_number_elements.max(), 5.0);
EXPECT_APPROXIMATE(even_number_elements.max(), 6.0);
// Median
EXPECT_APPROXIMATE(odd_number_elements.median(), 3.0);
EXPECT_APPROXIMATE(even_number_elements.median(), 3.5);
EXPECT_APPROXIMATE(odd_number_elements_large.median(), 101.0);
EXPECT_APPROXIMATE(even_number_elements_large.median(), 180.5);
// The expected values for standard deviation and variance were calculated by my school issued scientific calculator
// Standard Deviation
EXPECT_APPROXIMATE(odd_number_elements.standard_deviation(), 1.4142135623731);
EXPECT_APPROXIMATE(even_number_elements.standard_deviation(), 1.7078251276599);
// Variance
EXPECT_APPROXIMATE(odd_number_elements.variance(), 2.0);
EXPECT_APPROXIMATE(even_number_elements.variance(), 2.9166666666667);
}

View file

@ -5,7 +5,6 @@ set(TEST_SOURCES
TestICCProfile.cpp
TestImageDecoder.cpp
TestImageWriter.cpp
TestMedianCut.cpp
TestRect.cpp
TestWOFF.cpp
TestWOFF2.cpp

View file

@ -13,7 +13,6 @@
#include <LibGfx/ImageFormats/BMPLoader.h>
#include <LibGfx/ImageFormats/BMPWriter.h>
#include <LibGfx/ImageFormats/GIFLoader.h>
#include <LibGfx/ImageFormats/GIFWriter.h>
#include <LibGfx/ImageFormats/JPEGLoader.h>
#include <LibGfx/ImageFormats/JPEGWriter.h>
#include <LibGfx/ImageFormats/PNGLoader.h>
@ -108,58 +107,6 @@ TEST_CASE(test_bmp)
TRY_OR_FAIL((test_roundtrip<Gfx::BMPWriter, Gfx::BMPImageDecoderPlugin>(TRY_OR_FAIL(create_test_rgba_bitmap()))));
}
TEST_CASE(test_gif)
{
// Let's limit the size of the image so every color can fit in a color table of 256 elements.
auto bitmap = TRY_OR_FAIL(TRY_OR_FAIL(create_test_rgb_bitmap())->cropped({ 0, 0, 16, 16 }));
auto encoded_bitmap = TRY_OR_FAIL((encode_bitmap<Gfx::GIFWriter>(bitmap)));
auto decoder = TRY_OR_FAIL(Gfx::GIFImageDecoderPlugin::create(encoded_bitmap));
EXPECT_EQ(decoder->size(), bitmap->size());
EXPECT_EQ(decoder->frame_count(), 1u);
EXPECT(!decoder->is_animated());
expect_bitmaps_equal(*TRY_OR_FAIL(decoder->frame(0)).image, bitmap);
}
TEST_CASE(test_gif_animated)
{
auto bitmap_1 = TRY_OR_FAIL(TRY_OR_FAIL(create_test_rgb_bitmap())->cropped({ 0, 0, 16, 16 }));
auto bitmap_2 = TRY_OR_FAIL(TRY_OR_FAIL(create_test_rgb_bitmap())->cropped({ 16, 16, 16, 16 }));
auto bitmap_3 = TRY_OR_FAIL(bitmap_2->clone());
bitmap_3->scanline(3)[3] = Color(Color::NamedColor::Red).value();
auto stream_buffer = TRY_OR_FAIL(ByteBuffer::create_uninitialized(3072));
FixedMemoryStream stream { Bytes { stream_buffer } };
auto animation_writer = TRY_OR_FAIL(Gfx::GIFWriter::start_encoding_animation(stream, bitmap_1->size(), 0));
TRY_OR_FAIL(animation_writer->add_frame(*bitmap_1, 100));
TRY_OR_FAIL(animation_writer->add_frame(*bitmap_2, 200));
TRY_OR_FAIL(animation_writer->add_frame_relative_to_last_frame(*bitmap_3, 200, *bitmap_2));
auto encoded_animation = ReadonlyBytes { stream_buffer.data(), stream.offset() };
auto decoder = TRY_OR_FAIL(Gfx::GIFImageDecoderPlugin::create(encoded_animation));
EXPECT_EQ(decoder->size(), bitmap_1->size());
EXPECT_EQ(decoder->frame_count(), 3u);
EXPECT_EQ(decoder->loop_count(), 0u);
EXPECT(decoder->is_animated());
auto const frame_1 = TRY_OR_FAIL(decoder->frame(0));
EXPECT_EQ(frame_1.duration, 100);
expect_bitmaps_equal(*frame_1.image, bitmap_1);
auto const frame_2 = TRY_OR_FAIL(decoder->frame(1));
EXPECT_EQ(frame_2.duration, 200);
expect_bitmaps_equal(*frame_2.image, bitmap_2);
auto const frame_3 = TRY_OR_FAIL(decoder->frame(2));
EXPECT_EQ(frame_3.duration, 200);
expect_bitmaps_equal(*frame_3.image, bitmap_3);
}
TEST_CASE(test_jpeg)
{
// JPEG is lossy, so the roundtripped bitmap won't match the original bitmap. But it should still have the same size.

View file

@ -1,56 +0,0 @@
/*
* Copyright (c) 2024, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Bitmap.h>
#include <LibGfx/MedianCut.h>
#include <LibTest/TestCase.h>
TEST_CASE(single_element)
{
auto const bitmap = TRY_OR_FAIL(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { 1, 1 }));
bitmap->set_pixel(0, 0, Gfx::Color::NamedColor::White);
auto const result = TRY_OR_FAIL(Gfx::median_cut(bitmap, 1));
EXPECT_EQ(result.palette().size(), 1ul);
EXPECT_EQ(result.closest_color(Gfx::Color::NamedColor::White), Gfx::Color::NamedColor::White);
}
namespace {
constexpr auto colors = to_array<Gfx::Color>({ { 253, 0, 0 }, { 255, 0, 0 }, { 0, 253, 0 }, { 0, 255, 0 } });
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> create_test_bitmap()
{
auto bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { colors.size(), 1 }));
for (u8 i = 0; i < colors.size(); ++i)
bitmap->set_pixel(i, 0, colors[i]);
return bitmap;
}
}
TEST_CASE(four_in_four_out)
{
auto const bitmap = TRY_OR_FAIL(create_test_bitmap());
auto const result = TRY_OR_FAIL(Gfx::median_cut(bitmap, 4));
EXPECT_EQ(result.palette().size(), 4ul);
for (auto const color : colors)
EXPECT_EQ(result.closest_color(color), color);
}
TEST_CASE(four_in_two_out)
{
auto const bitmap = TRY_OR_FAIL(create_test_bitmap());
auto const result = TRY_OR_FAIL(Gfx::median_cut(bitmap, 2));
EXPECT_EQ(result.palette().size(), 2ul);
EXPECT_EQ(result.closest_color(Gfx::Color(253, 0, 0)), Gfx::Color(254, 0, 0));
EXPECT_EQ(result.closest_color(Gfx::Color(255, 0, 0)), Gfx::Color(254, 0, 0));
EXPECT_EQ(result.closest_color(Gfx::Color(0, 253, 0)), Gfx::Color(0, 254, 0));
EXPECT_EQ(result.closest_color(Gfx::Color(0, 255, 0)), Gfx::Color(0, 254, 0));
}

View file

@ -8,7 +8,6 @@
#include <LibCore/File.h>
#include <LibCore/MappedFile.h>
#include <LibGfx/ImageFormats/AnimationWriter.h>
#include <LibGfx/ImageFormats/GIFWriter.h>
#include <LibGfx/ImageFormats/ImageDecoder.h>
#include <LibGfx/ImageFormats/WebPWriter.h>
@ -50,8 +49,6 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
auto animation_writer = TRY([&]() -> ErrorOr<NonnullOwnPtr<Gfx::AnimationWriter>> {
if (options.out_path.ends_with(".webp"sv))
return Gfx::WebPWriter::start_encoding_animation(*output_stream, decoder->size(), decoder->loop_count());
if (options.out_path.ends_with(".gif"sv))
return Gfx::GIFWriter::start_encoding_animation(*output_stream, decoder->size(), decoder->loop_count());
return Error::from_string_literal("Unable to find a encoder for the requested extension.");
}());

View file

@ -9,7 +9,6 @@
#include <LibCore/MappedFile.h>
#include <LibGfx/ICC/Profile.h>
#include <LibGfx/ImageFormats/BMPWriter.h>
#include <LibGfx/ImageFormats/GIFWriter.h>
#include <LibGfx/ImageFormats/ImageDecoder.h>
#include <LibGfx/ImageFormats/JPEGWriter.h>
#include <LibGfx/ImageFormats/PNGWriter.h>
@ -167,10 +166,6 @@ static ErrorOr<void> save_image(LoadedImage& image, StringView out_path, u8 jpeg
auto& frame = image.bitmap.get<RefPtr<Gfx::Bitmap>>();
if (out_path.ends_with(".gif"sv, CaseSensitivity::CaseInsensitive)) {
TRY(Gfx::GIFWriter::encode(*TRY(stream()), *frame));
return {};
}
if (out_path.ends_with(".jpg"sv, CaseSensitivity::CaseInsensitive) || out_path.ends_with(".jpeg"sv, CaseSensitivity::CaseInsensitive)) {
TRY(Gfx::JPEGWriter::encode(*TRY(stream()), *frame, { .icc_data = image.icc_data, .quality = jpeg_quality }));
return {};