mirror of
https://github.com/SerenityOS/serenity
synced 2026-04-25 17:15:42 +02:00
This used to fail:
% Build/lagom/bin/image \
--assign-color-profile serenity-sRGB.icc \
--convert-to-color-profile \
./Build/lagom/Root/res/icc/Adobe/CMYK/USWebCoatedSWOP.icc \
-o buggie-cmyk.jpg \
Base/res/graphics/buggie.png
Runtime error: Can only convert to RGB at the moment,
but destination color space is not RGB
Now it works.
It only works for CMYK profiles that use an mft1 tag for the BToA tags,
because #26452 added support for converting from PCS to mft1.
Most CMYK profiles use mft1, but not all of them. Support for `mft2` and
`mBA ` tag types will be in a (straightforward) follow-up.
Implementation-wise, Profile grows two more methods for converting RGB
and CMYK bitmaps to CMYK. We now have 2x2 implementations for
{RGB,CMYK} -> {RGB,CMYK}. For N different channel types, we need O(N^2)
of these methods.
Instead, we could have conversion methods between RGB bitmap and PCS
bitmap and between CMYK and PCS bitmap. With this approach, we'd only
need O(N) (2*N) of these methods. Concretely, that'd be 2x2 methods too
though, and since there are two PCS types, depending on how it's
implemented it's actually 4*N (i.e. 8 for RGB and CMYK).
So the current 2x2 approach seems not terrible. It *is* a bit
repetitive.
We then call the right of these 4 methods in `image`.
See the PR that added this commit for rough quantitative quality
evaluation of the implementation.
466 lines
21 KiB
C++
466 lines
21 KiB
C++
/*
|
|
* Copyright (c) 2023-2025, Nico Weber <thakis@chromium.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibCore/ArgsParser.h>
|
|
#include <LibCore/File.h>
|
|
#include <LibCore/MappedFile.h>
|
|
#include <LibCore/MimeData.h>
|
|
#include <LibGfx/ICC/BinaryWriter.h>
|
|
#include <LibGfx/ICC/Profile.h>
|
|
#include <LibGfx/ICC/WellKnownProfiles.h>
|
|
#include <LibGfx/ImageFormats/BMPWriter.h>
|
|
#include <LibGfx/ImageFormats/BilevelImage.h>
|
|
#include <LibGfx/ImageFormats/GIFWriter.h>
|
|
#include <LibGfx/ImageFormats/ImageDecoder.h>
|
|
#include <LibGfx/ImageFormats/JBIG2Writer.h>
|
|
#include <LibGfx/ImageFormats/JPEGWriter.h>
|
|
#include <LibGfx/ImageFormats/PNGWriter.h>
|
|
#include <LibGfx/ImageFormats/PortableFormatWriter.h>
|
|
#include <LibGfx/ImageFormats/QOIWriter.h>
|
|
#include <LibGfx/ImageFormats/TIFFWriter.h>
|
|
#include <LibGfx/ImageFormats/WebPSharedLossless.h>
|
|
#include <LibGfx/ImageFormats/WebPWriter.h>
|
|
|
|
using AnyBitmap = Variant<RefPtr<Gfx::Bitmap>, RefPtr<Gfx::CMYKBitmap>>;
|
|
struct LoadedImage {
|
|
Gfx::NaturalFrameFormat internal_format;
|
|
AnyBitmap bitmap;
|
|
|
|
Optional<ReadonlyBytes> icc_data;
|
|
Variant<Empty, NonnullOwnPtr<Core::MappedFile>, ByteBuffer> icc_handle {};
|
|
};
|
|
|
|
static ErrorOr<LoadedImage> load_image(RefPtr<Gfx::ImageDecoder> const& decoder, Optional<int> maybe_frame_index)
|
|
{
|
|
if (decoder->frame_count() > 1 && !maybe_frame_index.has_value())
|
|
dbgln("image has {} frames, defaulting to `--frame-index 0`", decoder->frame_count());
|
|
|
|
auto internal_format = decoder->natural_frame_format();
|
|
|
|
auto bitmap = TRY([&]() -> ErrorOr<AnyBitmap> {
|
|
switch (internal_format) {
|
|
case Gfx::NaturalFrameFormat::RGB:
|
|
case Gfx::NaturalFrameFormat::Grayscale:
|
|
case Gfx::NaturalFrameFormat::Vector:
|
|
return TRY(decoder->frame(maybe_frame_index.value_or(0))).image;
|
|
case Gfx::NaturalFrameFormat::CMYK:
|
|
return RefPtr(TRY(decoder->cmyk_frame()));
|
|
}
|
|
VERIFY_NOT_REACHED();
|
|
}());
|
|
|
|
return LoadedImage { internal_format, move(bitmap), TRY(decoder->icc_data()) };
|
|
}
|
|
|
|
static ErrorOr<void> invert_cmyk(LoadedImage& image)
|
|
{
|
|
if (!image.bitmap.has<RefPtr<Gfx::CMYKBitmap>>())
|
|
return Error::from_string_literal("Can't --invert-cmyk with RGB bitmaps");
|
|
auto& frame = image.bitmap.get<RefPtr<Gfx::CMYKBitmap>>();
|
|
|
|
for (auto& pixel : *frame) {
|
|
pixel.c = ~pixel.c;
|
|
pixel.m = ~pixel.m;
|
|
pixel.y = ~pixel.y;
|
|
pixel.k = ~pixel.k;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static ErrorOr<void> crop_image(LoadedImage& image, Gfx::IntRect const& rect)
|
|
{
|
|
if (!image.bitmap.has<RefPtr<Gfx::Bitmap>>())
|
|
return Error::from_string_literal("Can't --crop CMYK bitmaps yet");
|
|
auto& frame = image.bitmap.get<RefPtr<Gfx::Bitmap>>();
|
|
frame = TRY(frame->cropped(rect));
|
|
return {};
|
|
}
|
|
|
|
static ErrorOr<void> move_alpha_to_rgb(LoadedImage& image)
|
|
{
|
|
if (!image.bitmap.has<RefPtr<Gfx::Bitmap>>())
|
|
return Error::from_string_literal("Can't --move-alpha-to-rgb with CMYK bitmaps");
|
|
auto& frame = image.bitmap.get<RefPtr<Gfx::Bitmap>>();
|
|
|
|
switch (frame->format()) {
|
|
case Gfx::BitmapFormat::Invalid:
|
|
return Error::from_string_literal("Can't --move-alpha-to-rgb with invalid bitmaps");
|
|
case Gfx::BitmapFormat::RGBA8888:
|
|
// No image decoder currently produces bitmaps with this format.
|
|
// If that ever changes, preferrably fix the image decoder to use BGRA8888 instead :)
|
|
// If there's a good reason for not doing that, implement support for this, I suppose.
|
|
return Error::from_string_literal("--move-alpha-to-rgb not implemented for RGBA8888");
|
|
case Gfx::BitmapFormat::BGRA8888:
|
|
case Gfx::BitmapFormat::BGRx8888:
|
|
// FIXME: If BitmapFormat::Gray8 existed (and image encoders made use of it to write grayscale images), we could use it here.
|
|
for (auto& pixel : *frame) {
|
|
u8 alpha = pixel >> 24;
|
|
pixel = 0xff000000 | (alpha << 16) | (alpha << 8) | alpha;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static ErrorOr<void> strip_alpha(LoadedImage& image)
|
|
{
|
|
if (!image.bitmap.has<RefPtr<Gfx::Bitmap>>())
|
|
return Error::from_string_literal("Can't --strip-alpha with CMYK bitmaps");
|
|
auto& frame = image.bitmap.get<RefPtr<Gfx::Bitmap>>();
|
|
|
|
switch (frame->format()) {
|
|
case Gfx::BitmapFormat::Invalid:
|
|
return Error::from_string_literal("Can't --strip-alpha with invalid bitmaps");
|
|
case Gfx::BitmapFormat::RGBA8888:
|
|
// No image decoder currently produces bitmaps with this format.
|
|
// If that ever changes, preferably fix the image decoder to use BGRA8888 instead :)
|
|
// If there's a good reason for not doing that, implement support for this, I suppose.
|
|
return Error::from_string_literal("--strip-alpha not implemented for RGBA8888");
|
|
case Gfx::BitmapFormat::BGRA8888:
|
|
case Gfx::BitmapFormat::BGRx8888:
|
|
frame->strip_alpha_channel();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static ErrorOr<void> to_bilevel(LoadedImage& image, Gfx::DitheringAlgorithm algorithm)
|
|
{
|
|
if (!image.bitmap.has<RefPtr<Gfx::Bitmap>>())
|
|
return Error::from_string_literal("Can't --to-bilevel with CMYK bitmaps");
|
|
auto& frame = image.bitmap.get<RefPtr<Gfx::Bitmap>>();
|
|
|
|
switch (frame->format()) {
|
|
case Gfx::BitmapFormat::Invalid:
|
|
return Error::from_string_literal("Can't --to-bilevel with invalid bitmaps");
|
|
case Gfx::BitmapFormat::RGBA8888:
|
|
case Gfx::BitmapFormat::BGRA8888:
|
|
case Gfx::BitmapFormat::BGRx8888:
|
|
frame = RefPtr { TRY(TRY(Gfx::BilevelImage::create_from_bitmap(*frame, algorithm))->to_gfx_bitmap()) };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static ErrorOr<void> load_icc_profile(LoadedImage& image, StringView color_profile_path)
|
|
{
|
|
image.icc_handle = TRY(([&]() -> ErrorOr<Variant<NonnullOwnPtr<Core::MappedFile>, ByteBuffer>> {
|
|
if (color_profile_path == "sRGB"sv) {
|
|
auto buffer = TRY(Gfx::ICC::encode(TRY(Gfx::ICC::sRGB())));
|
|
image.icc_data = buffer.span();
|
|
return buffer;
|
|
}
|
|
auto icc_file = TRY(Core::MappedFile::map(color_profile_path));
|
|
image.icc_data = icc_file->bytes();
|
|
return icc_file;
|
|
}()));
|
|
return {};
|
|
}
|
|
|
|
static ErrorOr<void> convert_image_profile(LoadedImage& image, StringView convert_color_profile_path)
|
|
{
|
|
if (!image.icc_data.has_value())
|
|
return Error::from_string_literal("No source color space embedded in image. Pass one with --assign-color-profile.");
|
|
|
|
auto source_icc_handle = move(image.icc_handle);
|
|
auto source_icc_data = image.icc_data.value();
|
|
TRY(load_icc_profile(image, convert_color_profile_path));
|
|
|
|
auto source_profile = TRY(Gfx::ICC::Profile::try_load_from_externally_owned_memory(source_icc_data));
|
|
auto destination_profile = TRY(Gfx::ICC::Profile::try_load_from_externally_owned_memory(*image.icc_data));
|
|
|
|
if (destination_profile->data_color_space() != Gfx::ICC::ColorSpace::RGB
|
|
&& destination_profile->data_color_space() != Gfx::ICC::ColorSpace::CMYK)
|
|
return Error::from_string_literal("Can only convert to RGB and CMYK at the moment, but destination color space is neither");
|
|
|
|
if (image.bitmap.has<RefPtr<Gfx::CMYKBitmap>>()) {
|
|
if (source_profile->data_color_space() != Gfx::ICC::ColorSpace::CMYK)
|
|
return Error::from_string_literal("Source image data is CMYK but source color space is not CMYK");
|
|
|
|
if (destination_profile->data_color_space() == Gfx::ICC::ColorSpace::CMYK) {
|
|
auto& cmyk_frame = image.bitmap.get<RefPtr<Gfx::CMYKBitmap>>();
|
|
TRY(destination_profile->convert_cmyk_image_to_cmyk_image(*cmyk_frame, *source_profile));
|
|
} else {
|
|
auto& cmyk_frame = image.bitmap.get<RefPtr<Gfx::CMYKBitmap>>();
|
|
auto rgb_frame = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRx8888, cmyk_frame->size()));
|
|
TRY(destination_profile->convert_cmyk_image(*rgb_frame, *cmyk_frame, *source_profile));
|
|
image.bitmap = RefPtr(move(rgb_frame));
|
|
image.internal_format = Gfx::NaturalFrameFormat::RGB;
|
|
}
|
|
} else {
|
|
// FIXME: This likely wrong for grayscale images because they've been converted to
|
|
// RGB at this point, but their embedded color profile is still for grayscale.
|
|
if (destination_profile->data_color_space() == Gfx::ICC::ColorSpace::CMYK) {
|
|
auto& rgb_frame = image.bitmap.get<RefPtr<Gfx::Bitmap>>();
|
|
auto cmyk_frame = TRY(Gfx::CMYKBitmap::create_with_size(rgb_frame->size()));
|
|
TRY(destination_profile->convert_image_to_cmyk_image(*cmyk_frame, *rgb_frame, *source_profile));
|
|
image.bitmap = RefPtr(move(cmyk_frame));
|
|
image.internal_format = Gfx::NaturalFrameFormat::CMYK;
|
|
} else {
|
|
auto& frame = image.bitmap.get<RefPtr<Gfx::Bitmap>>();
|
|
TRY(destination_profile->convert_image(*frame, *source_profile));
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
static ErrorOr<void> save_image(LoadedImage& image, StringView out_path, bool force_alpha, bool ppm_ascii, u8 jpeg_quality, Optional<unsigned> webp_allowed_transforms, unsigned webp_color_cache_bits, Compress::ZlibCompressionLevel png_compression_level)
|
|
{
|
|
auto stream = [out_path]() -> ErrorOr<NonnullOwnPtr<Core::OutputBufferedFile>> {
|
|
auto output_stream = TRY(Core::File::open(out_path, Core::File::OpenMode::Write));
|
|
return Core::OutputBufferedFile::create(move(output_stream));
|
|
};
|
|
|
|
if (image.bitmap.has<RefPtr<Gfx::CMYKBitmap>>()) {
|
|
auto& cmyk_frame = image.bitmap.get<RefPtr<Gfx::CMYKBitmap>>();
|
|
|
|
if (out_path.ends_with(".jpg"sv, CaseSensitivity::CaseInsensitive) || out_path.ends_with(".jpeg"sv, CaseSensitivity::CaseInsensitive)) {
|
|
TRY(Gfx::JPEGWriter::encode(*TRY(stream()), *cmyk_frame, { .icc_data = image.icc_data, .quality = jpeg_quality }));
|
|
return {};
|
|
}
|
|
if (out_path.ends_with(".tif"sv, CaseSensitivity::CaseInsensitive) || out_path.ends_with(".tiff"sv, CaseSensitivity::CaseInsensitive)) {
|
|
Gfx::TIFFWriter::Options options;
|
|
options.icc_data = image.icc_data;
|
|
TRY(Gfx::TIFFWriter::encode(*TRY(stream()), *cmyk_frame, options));
|
|
return {};
|
|
}
|
|
|
|
return Error::from_string_view("Can save CMYK bitmaps only as .jpg or .tiff, convert to RGB first with --convert-to-color-profile"sv);
|
|
}
|
|
|
|
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(".jb2"sv, CaseSensitivity::CaseInsensitive) || out_path.ends_with(".jbig2"sv, CaseSensitivity::CaseInsensitive)) {
|
|
TRY(Gfx::JBIG2Writer::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 {};
|
|
}
|
|
if (out_path.ends_with(".png"sv, CaseSensitivity::CaseInsensitive)) {
|
|
TRY(Gfx::PNGWriter::encode(*TRY(stream()), *frame, { .compression_level = png_compression_level, .force_alpha = force_alpha, .icc_data = image.icc_data }));
|
|
return {};
|
|
}
|
|
if (out_path.ends_with(".ppm"sv, CaseSensitivity::CaseInsensitive)) {
|
|
auto const format = ppm_ascii ? Gfx::PortableFormatWriter::Options::Format::ASCII : Gfx::PortableFormatWriter::Options::Format::Raw;
|
|
TRY(Gfx::PortableFormatWriter::encode(*TRY(stream()), *frame, { .format = format }));
|
|
return {};
|
|
}
|
|
if (out_path.ends_with(".tif"sv, CaseSensitivity::CaseInsensitive) || out_path.ends_with(".tiff"sv, CaseSensitivity::CaseInsensitive)) {
|
|
Gfx::TIFFWriter::Options options;
|
|
options.icc_data = image.icc_data;
|
|
TRY(Gfx::TIFFWriter::encode(*TRY(stream()), *frame, options));
|
|
return {};
|
|
}
|
|
if (out_path.ends_with(".webp"sv, CaseSensitivity::CaseInsensitive)) {
|
|
Gfx::WebPWriter::Options options;
|
|
options.icc_data = image.icc_data;
|
|
if (webp_allowed_transforms.has_value())
|
|
options.vp8l_options.allowed_transforms = webp_allowed_transforms.value();
|
|
if (webp_color_cache_bits == 0)
|
|
options.vp8l_options.color_cache_bits = {};
|
|
else
|
|
options.vp8l_options.color_cache_bits = webp_color_cache_bits;
|
|
TRY(Gfx::WebPWriter::encode(*TRY(stream()), *frame, options));
|
|
return {};
|
|
}
|
|
|
|
ByteBuffer bytes;
|
|
if (out_path.ends_with(".bmp"sv, CaseSensitivity::CaseInsensitive)) {
|
|
bytes = TRY(Gfx::BMPWriter::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_literal("can only write .bmp, .gif, .jpg, .png, .ppm, .qoi, .tiff, and .webp");
|
|
}
|
|
TRY(TRY(stream())->write_until_depleted(bytes));
|
|
|
|
return {};
|
|
}
|
|
|
|
struct Options {
|
|
StringView in_path;
|
|
StringView out_path;
|
|
bool no_output = false;
|
|
Optional<int> frame_index;
|
|
bool invert_cmyk = false;
|
|
Optional<Gfx::IntRect> crop_rect;
|
|
bool move_alpha_to_rgb = false;
|
|
bool force_alpha = false;
|
|
bool strip_alpha = false;
|
|
StringView assign_color_profile_path;
|
|
StringView convert_color_profile_path;
|
|
bool strip_color_profile = false;
|
|
Compress::ZlibCompressionLevel png_compression_level { Compress::ZlibCompressionLevel::Default };
|
|
bool ppm_ascii = false;
|
|
u8 quality = 75;
|
|
Optional<Gfx::DitheringAlgorithm> to_bilevel;
|
|
unsigned webp_color_cache_bits = 6;
|
|
Optional<unsigned> webp_allowed_transforms;
|
|
};
|
|
|
|
template<class T>
|
|
static ErrorOr<Vector<T>> parse_comma_separated_numbers(StringView rect_string)
|
|
{
|
|
auto parts = rect_string.split_view(',');
|
|
Vector<T> part_numbers;
|
|
for (size_t i = 0; i < parts.size(); ++i) {
|
|
auto part = parts[i].to_number<T>();
|
|
if (!part.has_value())
|
|
return Error::from_string_literal("comma-separated parts must be numbers");
|
|
TRY(part_numbers.try_append(part.value()));
|
|
}
|
|
return part_numbers;
|
|
}
|
|
|
|
static ErrorOr<Gfx::IntRect> parse_rect_string(StringView rect_string)
|
|
{
|
|
auto numbers = TRY(parse_comma_separated_numbers<i32>(rect_string));
|
|
if (numbers.size() != 4)
|
|
return Error::from_string_literal("rect must have 4 comma-separated parts");
|
|
return Gfx::IntRect { numbers[0], numbers[1], numbers[2], numbers[3] };
|
|
}
|
|
|
|
static ErrorOr<unsigned> parse_webp_allowed_transforms_string(StringView string)
|
|
{
|
|
unsigned allowed_transforms = 0;
|
|
for (StringView part : string.split_view(',')) {
|
|
if (part == "predictor" || part == "p")
|
|
allowed_transforms |= 1 << Gfx::PREDICTOR_TRANSFORM;
|
|
else if (part == "color" || part == "c")
|
|
allowed_transforms |= 1 << Gfx::COLOR_TRANSFORM;
|
|
else if (part == "subtract-green" || part == "sg")
|
|
allowed_transforms |= 1 << Gfx::SUBTRACT_GREEN_TRANSFORM;
|
|
else if (part == "color-indexing" || part == "ci")
|
|
allowed_transforms |= 1 << Gfx::COLOR_INDEXING_TRANSFORM;
|
|
else
|
|
return Error::from_string_literal("unknown WebP transform; valid values: predictor, p, color, c, subtract-green, sg, color-indexing, ci");
|
|
}
|
|
return allowed_transforms;
|
|
}
|
|
|
|
static ErrorOr<Options> parse_options(Main::Arguments arguments)
|
|
{
|
|
Options options;
|
|
Core::ArgsParser args_parser;
|
|
args_parser.add_positional_argument(options.in_path, "Path to input image file", "FILE");
|
|
args_parser.add_option(options.out_path, "Path to output image file", "output", 'o', "FILE");
|
|
args_parser.add_option(options.no_output, "Do not write output (only useful for benchmarking image decoding)", "no-output", {});
|
|
args_parser.add_option(options.frame_index, "Which frame of a multi-frame input image (0-based)", "frame-index", {}, "INDEX");
|
|
args_parser.add_option(options.invert_cmyk, "Invert CMYK channels", "invert-cmyk", {});
|
|
StringView crop_rect_string;
|
|
args_parser.add_option(crop_rect_string, "Crop to a rectangle", "crop", {}, "x,y,w,h");
|
|
args_parser.add_option(options.move_alpha_to_rgb, "Copy alpha channel to rgb, clear alpha", "move-alpha-to-rgb", {});
|
|
args_parser.add_option(options.force_alpha, "Force alpha channel", "force-alpha", {});
|
|
args_parser.add_option(options.strip_alpha, "Remove alpha channel", "strip-alpha", {});
|
|
args_parser.add_option(options.assign_color_profile_path, "Load color profile and assign it to output image", "assign-color-profile", {}, "(FILE | \"sRGB\")");
|
|
args_parser.add_option(options.convert_color_profile_path, "Load color profile and convert output image from current profile to loaded profile", "convert-to-color-profile", {}, "(FILE | \"sRGB\")");
|
|
args_parser.add_option(options.strip_color_profile, "Do not write color profile to output", "strip-color-profile", {});
|
|
|
|
Optional<Gfx::DitheringAlgorithm> to_bilevel;
|
|
args_parser.add_option({
|
|
Core::ArgsParser::OptionArgumentMode::Optional,
|
|
"Convert to bilevel image, with optionally explicit dithering algorithm",
|
|
"to-bilevel",
|
|
{},
|
|
"global-threshold, bayer2x2, bayer4x4, bayer8x8, floyd-steinberg",
|
|
[&to_bilevel](StringView s) {
|
|
if (s.is_empty())
|
|
to_bilevel = Gfx::DitheringAlgorithm::FloydSteinberg;
|
|
else if (s == "global-threshold"sv)
|
|
to_bilevel = Gfx::DitheringAlgorithm::None;
|
|
else if (s == "bayer2x2"sv)
|
|
to_bilevel = Gfx::DitheringAlgorithm::Bayer2x2;
|
|
else if (s == "bayer4x4"sv)
|
|
to_bilevel = Gfx::DitheringAlgorithm::Bayer4x4;
|
|
else if (s == "bayer8x8"sv)
|
|
to_bilevel = Gfx::DitheringAlgorithm::Bayer8x8;
|
|
else if (s == "floyd-steinberg"sv)
|
|
to_bilevel = Gfx::DitheringAlgorithm::FloydSteinberg;
|
|
else
|
|
return false;
|
|
return true;
|
|
},
|
|
Core::ArgsParser::OptionHideMode::None,
|
|
});
|
|
|
|
auto png_compression_level = static_cast<unsigned>(Compress::ZlibCompressionLevel::Default);
|
|
args_parser.add_option(png_compression_level, "PNG compression level, in [0, 3]. Higher values take longer and produce smaller outputs. Default: 2", "png-compression-level", {}, {});
|
|
args_parser.add_option(options.ppm_ascii, "Convert to a PPM in ASCII", "ppm-ascii", {});
|
|
args_parser.add_option(options.quality, "Quality used for the JPEG encoder, the default value is 75 on a scale from 0 to 100", "quality", {}, {});
|
|
args_parser.add_option(options.webp_color_cache_bits, "Size of the webp color cache (in [0, 11], higher values tend to be slower and produce smaller output, default: 6)", "webp-color-cache-bits", {}, {});
|
|
StringView webp_allowed_transforms = "default"sv;
|
|
args_parser.add_option(webp_allowed_transforms, "Comma-separated list of allowed transforms (predictor,p,color,c,subtract-green,sg,color-indexing,ci) for WebP output (default: all allowed)", "webp-allowed-transforms", {}, {});
|
|
args_parser.parse(arguments);
|
|
|
|
if (options.out_path.is_empty() ^ options.no_output)
|
|
return Error::from_string_literal("exactly one of -o or --no-output is required");
|
|
|
|
if (!crop_rect_string.is_empty())
|
|
options.crop_rect = TRY(parse_rect_string(crop_rect_string));
|
|
|
|
if (options.force_alpha && !options.out_path.ends_with(".png"sv, CaseSensitivity::CaseInsensitive))
|
|
return Error::from_string_literal("--force_alpha is only supported with the PNG encoder");
|
|
|
|
if (png_compression_level > 3)
|
|
return Error::from_string_view("--png-compression-level must be in [0, 3]"sv);
|
|
options.png_compression_level = static_cast<Compress::ZlibCompressionLevel>(png_compression_level);
|
|
|
|
options.to_bilevel = to_bilevel;
|
|
|
|
if (webp_allowed_transforms != "default"sv)
|
|
options.webp_allowed_transforms = TRY(parse_webp_allowed_transforms_string(webp_allowed_transforms));
|
|
|
|
return options;
|
|
}
|
|
|
|
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
|
{
|
|
Options options = TRY(parse_options(arguments));
|
|
|
|
auto file = TRY(Core::MappedFile::map(options.in_path));
|
|
auto guessed_mime_type = Core::guess_mime_type_based_on_filename(options.in_path);
|
|
auto decoder = TRY(Gfx::ImageDecoder::try_create_for_raw_bytes(file->bytes(), guessed_mime_type));
|
|
if (!decoder)
|
|
return Error::from_string_literal("Could not find decoder for input file");
|
|
|
|
LoadedImage image = TRY(load_image(*decoder, options.frame_index));
|
|
|
|
if (options.invert_cmyk)
|
|
TRY(invert_cmyk(image));
|
|
|
|
if (options.crop_rect.has_value())
|
|
TRY(crop_image(image, options.crop_rect.value()));
|
|
|
|
if (options.move_alpha_to_rgb)
|
|
TRY(move_alpha_to_rgb(image));
|
|
|
|
if (options.strip_alpha)
|
|
TRY(strip_alpha(image));
|
|
|
|
if (options.to_bilevel.has_value())
|
|
TRY(to_bilevel(image, options.to_bilevel.value()));
|
|
|
|
if (!options.assign_color_profile_path.is_empty())
|
|
TRY(load_icc_profile(image, options.assign_color_profile_path));
|
|
|
|
if (!options.convert_color_profile_path.is_empty())
|
|
TRY(convert_image_profile(image, options.convert_color_profile_path));
|
|
|
|
if (options.strip_color_profile)
|
|
image.icc_data.clear();
|
|
|
|
if (options.no_output)
|
|
return 0;
|
|
|
|
TRY(save_image(image, options.out_path, options.force_alpha, options.ppm_ascii, options.quality, options.webp_allowed_transforms, options.webp_color_cache_bits, options.png_compression_level));
|
|
|
|
return 0;
|
|
}
|