mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-22 09:12:13 -05:00
AK: Remove Statistics.h
This also removes MedianCut and GIFWriter
This commit is contained in:
parent
ae6edfb845
commit
ede0dbafc6
Notes:
github-actions[bot]
2024-12-05 15:54:23 +00:00
Author: https://github.com/shlyakpavel Commit: https://github.com/LadybirdBrowser/ladybird/commit/ede0dbafc69 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2772
13 changed files with 0 additions and 845 deletions
129
AK/Statistics.h
129
AK/Statistics.h
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -66,7 +66,6 @@ set(AK_TEST_SOURCES
|
|||
TestSourceLocation.cpp
|
||||
TestSpan.cpp
|
||||
TestStack.cpp
|
||||
TestStatistics.cpp
|
||||
TestStdLibExtras.cpp
|
||||
TestString.cpp
|
||||
TestStringFloatingPointConversions.cpp
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -5,7 +5,6 @@ set(TEST_SOURCES
|
|||
TestICCProfile.cpp
|
||||
TestImageDecoder.cpp
|
||||
TestImageWriter.cpp
|
||||
TestMedianCut.cpp
|
||||
TestRect.cpp
|
||||
TestWOFF.cpp
|
||||
TestWOFF2.cpp
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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.");
|
||||
}());
|
||||
|
||||
|
|
|
@ -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 {};
|
||||
|
|
Loading…
Reference in a new issue