LibGfx/ICC+image: Implement conversion to CMYK color spaces :^)

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.
This commit is contained in:
Nico Weber committed 2025-12-06 08:40:47 -05:00
1 parent 8cecb9359c
commit 3841430718
3 files changed
+63 -9

No files matched your search

+37
View File
@@ -1763,6 +1763,43 @@ ErrorOr<void> Profile::convert_cmyk_image(Bitmap& out, CMYKBitmap const& in, Pro
return {};
}
ErrorOr<void> Profile::convert_cmyk_image_to_cmyk_image(CMYKBitmap& bitmap, Profile const& source_profile) const
{
for (auto& pixel : bitmap) {
u8 cmyk[] = { pixel.c, pixel.m, pixel.y, pixel.k };
auto pcs = TRY(source_profile.to_pcs(cmyk));
TRY(from_pcs(source_profile, pcs, cmyk));
pixel = CMYK { cmyk[0], cmyk[1], cmyk[2], cmyk[3] };
}
return {};
}
ErrorOr<void> Profile::convert_image_to_cmyk_image(CMYKBitmap& out, Bitmap const& in, Profile const& source_profile) const
{
if (out.size() != in.size())
return Error::from_string_literal("ICC::Profile::convert_image_to_cmyk_image: out and in must have the same dimensions");
// Might fail if `out` has a scale_factor() != 1.
if (out.data_size() != in.data_size())
return Error::from_string_literal("ICC::Profile::convert_image_to_cmyk_image: out and in must have the same buffer size");
static_assert(sizeof(ARGB32) == sizeof(CMYK));
CMYK* out_data = out.begin();
ARGB32 const* in_data = in.begin();
for (size_t i = 0; i < in.data_size() / sizeof(CMYK); ++i) {
u8 rgb[] = { Color::from_argb(in_data[i]).red(), Color::from_argb(in_data[i]).green(), Color::from_argb(in_data[i]).blue() };
auto pcs = TRY(source_profile.to_pcs(rgb));
u8 cmyk[4];
TRY(from_pcs(source_profile, pcs, cmyk));
out_data[i] = CMYK { cmyk[0], cmyk[1], cmyk[2], cmyk[3] };
}
return {};
}
XYZ const& Profile::red_matrix_column() const { return xyz_data(redMatrixColumnTag); }
XYZ const& Profile::green_matrix_column() const { return xyz_data(greenMatrixColumnTag); }
XYZ const& Profile::blue_matrix_column() const { return xyz_data(blueMatrixColumnTag); }
+3
View File
@@ -295,6 +295,9 @@ public:
ErrorOr<void> convert_image(Bitmap&, Profile const& source_profile) const;
ErrorOr<void> convert_cmyk_image(Bitmap&, CMYKBitmap const&, Profile const& source_profile) const;
ErrorOr<void> convert_cmyk_image_to_cmyk_image(CMYKBitmap&, Profile const& source_profile) const;
ErrorOr<void> convert_image_to_cmyk_image(CMYKBitmap&, Bitmap const&, Profile const& source_profile) const;
// Only call these if you know that this is an RGB matrix-based profile.
XYZ const& red_matrix_column() const;
XYZ const& green_matrix_column() const;
+23 -9
View File
@@ -169,23 +169,37 @@ static ErrorOr<void> convert_image_profile(LoadedImage& image, StringView conver
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)
return Error::from_string_literal("Can only convert to RGB at the moment, but destination color space is not RGB");
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");
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;
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.
auto& frame = image.bitmap.get<RefPtr<Gfx::Bitmap>>();
TRY(destination_profile->convert_image(*frame, *source_profile));
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 {};