/* * Copyright (c) 2024, Lucas Chollet * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include namespace Gfx { namespace { using Bucket = Vector; using Buckets = Vector; 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 struct MaxAndIndex { T maximum; u32 index; }; template MaxAndIndex max_and_index(Span values, GreaterThan greater_than) { VERIFY(values.size() != 0); u32 max_index = 0; RemoveCV 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 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> find_largest_bucket(Buckets const& buckets) { Vector bucket_stats {}; for (u32 i = 0; i < buckets.size(); ++i) { auto const& bucket = buckets[i]; if (bucket.size() == 1) continue; Statistics red {}; Statistics green {}; Statistics 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(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 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 color_palette_from_buckets(Buckets const& buckets) { Vector palette; HashMap 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(static_cast(average_r) / bucket_size), round_to(static_cast(average_g) / bucket_size), round_to(static_cast(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 median_cut(Bitmap const& bitmap, u16 palette_size) { HashTable color_set; for (auto color : bitmap) TRY(color_set.try_set(color)); Vector 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); } }