From 926c0d8676cc97ce92d859c35078d24f12eb7779 Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Mon, 1 May 2023 15:42:29 -0400 Subject: [PATCH] ICC+image: Add conversion between color spaces for images :^) For now, only for color spaces that are supported by Profile::to_pcs() and Profile::from_pcs(), which currently means that all matrix profiles (but not LUT profiles) in the source color space work, and that matrix profiles with parametric curves in the destination color space work. This adds Profile::convert_image(Bitmap, source_profile), and adds a `--convert-to-color-profile file.icc` flag to `image`. It only takes a file path, so to use it with the built-in sRGB profile, you have to write it to a file first: % Build/lagom/icc -n sRGB --reencode-to serenity-sRGB.icc `image` by default writes the source image's color profile to the output image, and most image viewers display images looking at the profile. For example, take `Seven_Coloured_Pencils_(rg-switch_sRGB).jpg` from https://commons.wikimedia.org/wiki/User:Colin/BrowserTest. It looks normal in image viewers because they apply the unusual profile embedded in the profile. But if you run % Build/lagom/image -o huh.png --strip-color-profile \ 'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg' and then look at huh.png, you can see how the image's colors look like when interpreted as sRGB (which is the color space PNG data is in if the PNG doesn't store an embedded profile). If you now run % Build/lagom/image -o wow.png \ --convert-to-color-profile serenity-sRGB.icc --strip-color-profile \ 'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg' this will convert that image to sRGB, but then not write the profile to the output image (verify with `Build/lagom/icc wow.png`). It will look correct in image viewers, since they display PNGs without an embedded color profile as sRGB. (This works because 'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg' contains a matrix profile, and Serenity's built-in sRGB profile uses a matrix profile with a parametric curve.) --- Userland/Libraries/LibGfx/ICC/Profile.cpp | 17 +++++++++++++++++ Userland/Libraries/LibGfx/ICC/Profile.h | 3 +++ Userland/Utilities/image.cpp | 20 ++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/Userland/Libraries/LibGfx/ICC/Profile.cpp b/Userland/Libraries/LibGfx/ICC/Profile.cpp index d7708e61ec..5fa6b66621 100644 --- a/Userland/Libraries/LibGfx/ICC/Profile.cpp +++ b/Userland/Libraries/LibGfx/ICC/Profile.cpp @@ -1664,6 +1664,23 @@ ErrorOr Profile::to_lab(ReadonlyBytes color) const return CIELAB { L, a, b }; } +ErrorOr Profile::convert_image(Gfx::Bitmap& bitmap, Profile const& source_profile) const +{ + // FIXME: Convert XYZ<->Lab conversion when needed. + // Currently, to_pcs() and from_pcs() are only implemented for matrix profiles, which are always XYZ anyways. + if (connection_space() != source_profile.connection_space()) + return Error::from_string_literal("ICC::Profile::convert_image: mismatching profile connection spaces not yet implemented"); + + for (auto& pixel : bitmap) { + u8 rgb[] = { Color::from_argb(pixel).red(), Color::from_argb(pixel).green(), Color::from_argb(pixel).blue() }; + auto pcs = TRY(source_profile.to_pcs(rgb)); + TRY(from_pcs(pcs, rgb)); + pixel = Color(rgb[0], rgb[1], rgb[2], Color::from_argb(pixel).alpha()).value(); + } + + 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); } diff --git a/Userland/Libraries/LibGfx/ICC/Profile.h b/Userland/Libraries/LibGfx/ICC/Profile.h index de2ee1451a..264454029a 100644 --- a/Userland/Libraries/LibGfx/ICC/Profile.h +++ b/Userland/Libraries/LibGfx/ICC/Profile.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -273,6 +274,8 @@ public: ErrorOr to_lab(ReadonlyBytes) const; + ErrorOr convert_image(Bitmap&, 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; diff --git a/Userland/Utilities/image.cpp b/Userland/Utilities/image.cpp index 2462f9e2c1..462e61d7c6 100644 --- a/Userland/Utilities/image.cpp +++ b/Userland/Utilities/image.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,9 @@ ErrorOr serenity_main(Main::Arguments arguments) StringView assign_color_profile_path; args_parser.add_option(assign_color_profile_path, "Load color profile from file and assign it to output image", "assign-color-profile", {}, "FILE"); + StringView convert_color_profile_path; + args_parser.add_option(convert_color_profile_path, "Load color profile from file and convert output image from current profile to loaded profile", "convert-to-color-profile", {}, "FILE"); + bool strip_color_profile; args_parser.add_option(strip_color_profile, "Do not write color profile to output", "strip-color-profile", {}); @@ -57,6 +61,22 @@ ErrorOr serenity_main(Main::Arguments arguments) icc_data = icc_file->bytes(); } + if (!convert_color_profile_path.is_empty()) { + if (!icc_data.has_value()) { + warnln("No source color space embedded in image. Pass one with --assign-color-profile."); + return 1; + } + + auto source_icc_file = icc_file; + auto source_icc_data = icc_data.value(); + icc_file = TRY(Core::MappedFile::map(convert_color_profile_path)); + icc_data = icc_file->bytes(); + + 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(icc_file->bytes())); + TRY(destination_profile->convert_image(*frame, *source_profile)); + } + if (strip_color_profile) icc_data.clear();