浏览代码

LibPDF+Meta: Use a CMYK ICC profile to convert CMYK to RGB

CMYK data describes which inks a printer should use to print a color.
If a screen should display a color that's supposed to look similar
to what the printer produces, it results in a color very different
to what Color::from_cmyk() produces. (It's also printer-dependent.)

There are many ICC profiles describing printing processes. It doesn't
matter too much which one we use -- most of them look somewhat
similar, and they all look dramatically better than Color::from_cmyk().

This patch adds a function to download a zip file that Adobe offers
on their web site. They even have a page for redistribution:
https://www.adobe.com/support/downloads/iccprofiles/icc_eula_win_dist.html

(That one leads to a broken download though, so this downloads the
end-user version.)

In case we have to move off this download at some point, there are also
a whole bunch of profiles at https://www.color.org/registry/index.xalter
that "may be used, embedded, exchanged, and shared without restriction".

The adobe zip contains a whole bunch of other useful and fun profiles,
so I went with it.

For now, this only unzips the USWebCoatedSWOP.icc file though, and
installs it in ${CMAKE_BINARY_DIR}/Root/res/icc/Adobe/CMYK/. In
Serenity builds, this will make it to /res/icc/Adobe/CMYK in the
disk image. And in lagom build, after #23016 this is the
lagom res staging directory that tools can install via
Core::ResourceImplementation. `pdf` and `MacPDF` already do that,
`TestPDF` now does it too.

The final piece is that LibPDF then loads the profile from there
and uses it for DeviceCMYK color conversions.

(Doing file access from the bowels of a library is a bit weird,
especially in a system that has sandboxing built in. But LibGfx does
that in FontDatabase too already, and LibPDF uses that, so it's not a
new problem.)
Nico Weber 1 年之前
父节点
当前提交
9c762b9650

+ 1 - 0
Meta/CMake/common_options.cmake

@@ -13,6 +13,7 @@ serenity_option(UNDEFINED_BEHAVIOR_IS_FATAL OFF CACHE BOOL "Make undefined behav
 
 
 serenity_option(ENABLE_ALL_THE_DEBUG_MACROS OFF CACHE BOOL "Enable all debug macros to validate they still compile")
 serenity_option(ENABLE_ALL_THE_DEBUG_MACROS OFF CACHE BOOL "Enable all debug macros to validate they still compile")
 serenity_option(ENABLE_ALL_DEBUG_FACILITIES OFF CACHE BOOL "Enable all noisy debug symbols and options. Not recommended for normal developer use")
 serenity_option(ENABLE_ALL_DEBUG_FACILITIES OFF CACHE BOOL "Enable all noisy debug symbols and options. Not recommended for normal developer use")
+serenity_option(ENABLE_ADOBE_ICC_PROFILES_DOWNLOAD ON CACHE BOOL "Enable download of Adobe's ICC profiles")
 serenity_option(ENABLE_COMPILETIME_HEADER_CHECK OFF CACHE BOOL "Enable compiletime check that each library header compiles stand-alone")
 serenity_option(ENABLE_COMPILETIME_HEADER_CHECK OFF CACHE BOOL "Enable compiletime check that each library header compiles stand-alone")
 
 
 serenity_option(ENABLE_TIME_ZONE_DATABASE_DOWNLOAD ON CACHE BOOL "Enable download of the IANA Time Zone Database at build time")
 serenity_option(ENABLE_TIME_ZONE_DATABASE_DOWNLOAD ON CACHE BOOL "Enable download of the IANA Time Zone Database at build time")

+ 25 - 0
Meta/CMake/download_icc_profiles.cmake

@@ -0,0 +1,25 @@
+include(${CMAKE_CURRENT_LIST_DIR}/utils.cmake)
+
+if (ENABLE_ADOBE_ICC_PROFILES_DOWNLOAD)
+    set(ADOBE_ICC_PROFILES_PATH "${SERENITY_CACHE_DIR}/AdobeICCProfiles" CACHE PATH "Download location for Adobe ICC profiles")
+    set(ADOBE_ICC_PROFILES_DATA_URL "https://download.adobe.com/pub/adobe/iccprofiles/win/AdobeICCProfilesCS4Win_end-user.zip")
+    set(ADOBE_ICC_PROFILES_ZIP_PATH "${ADOBE_ICC_PROFILES_PATH}/adobe-icc-profiles.zip")
+    if (ENABLE_NETWORK_DOWNLOADS)
+        download_file("${ADOBE_ICC_PROFILES_DATA_URL}" "${ADOBE_ICC_PROFILES_ZIP_PATH}")
+    else()
+        message(STATUS "Skipping download of ${ADOBE_ICC_PROFILES_DATA_URL}, expecting the archive to have been donwloaded to ${ADOBE_ICC_PROFILES_ZIP_PATH}")
+    endif()
+
+    function(extract_adobe_icc_profiles source path)
+        if(EXISTS "${ADOBE_ICC_PROFILES_ZIP_PATH}" AND NOT EXISTS "${path}")
+            file(ARCHIVE_EXTRACT INPUT "${ADOBE_ICC_PROFILES_ZIP_PATH}" DESTINATION "${ADOBE_ICC_PROFILES_PATH}" PATTERNS "${source}")
+        endif()
+    endfunction()
+
+    set(ADOBE_ICC_CMYK_SWOP "CMYK/USWebCoatedSWOP.icc")
+    set(ADOBE_ICC_CMYK_SWOP_PATH "${ADOBE_ICC_PROFILES_PATH}/Adobe ICC Profiles (end-user)/${ADOBE_ICC_CMYK_SWOP}")
+    extract_adobe_icc_profiles("Adobe ICC Profiles (end-user)/${ADOBE_ICC_CMYK_SWOP}" "${ADOBE_ICC_CMYK_SWOP_PATH}")
+
+    set(ADOBE_ICC_CMYK_SWOP_INSTALL_PATH "${CMAKE_BINARY_DIR}/Root/res/icc/Adobe/${ADOBE_ICC_CMYK_SWOP}")
+    configure_file("${ADOBE_ICC_CMYK_SWOP_PATH}" "${ADOBE_ICC_CMYK_SWOP_INSTALL_PATH}" COPYONLY)
+endif()

+ 1 - 1
Meta/Lagom/Contrib/MacPDF/AppDelegate.mm

@@ -16,7 +16,7 @@
 
 
 - (void)applicationDidFinishLaunching:(NSNotification*)aNotification
 - (void)applicationDidFinishLaunching:(NSNotification*)aNotification
 {
 {
-    // FIXME: Copy the fonts to the bundle or something
+    // FIXME: Copy fonts and icc file to the bundle or something
 
 
     // Get from `Build/lagom/bin/MacPDF.app/Contents/MacOS/MacPDF` to `Build/lagom/Root/res`.
     // Get from `Build/lagom/bin/MacPDF.app/Contents/MacOS/MacPDF` to `Build/lagom/Root/res`.
     NSString* source_root = [[NSBundle mainBundle] executablePath];
     NSString* source_root = [[NSBundle mainBundle] executablePath];

+ 9 - 0
Tests/LibPDF/TestPDF.cpp

@@ -6,7 +6,10 @@
 
 
 #include <AK/ByteString.h>
 #include <AK/ByteString.h>
 #include <AK/Forward.h>
 #include <AK/Forward.h>
+#include <AK/LexicalPath.h>
 #include <LibCore/MappedFile.h>
 #include <LibCore/MappedFile.h>
+#include <LibCore/ResourceImplementationFile.h>
+#include <LibCore/System.h>
 #include <LibGfx/Bitmap.h>
 #include <LibGfx/Bitmap.h>
 #include <LibPDF/CommonNames.h>
 #include <LibPDF/CommonNames.h>
 #include <LibPDF/Document.h>
 #include <LibPDF/Document.h>
@@ -293,6 +296,12 @@ TEST_CASE(postscript)
 
 
 TEST_CASE(render)
 TEST_CASE(render)
 {
 {
+#if !defined(AK_OS_SERENITY)
+    // Get from Build/lagom/bin/TestPDF to Build/lagom/Root/res.
+    auto source_root = LexicalPath(MUST(Core::System::current_executable_path())).parent().parent().string();
+    Core::ResourceImplementation::install(make<Core::ResourceImplementationFile>(MUST(String::formatted("{}/Root/res", source_root))));
+#endif
+
     auto file = MUST(Core::MappedFile::map("colorspaces.pdf"sv));
     auto file = MUST(Core::MappedFile::map("colorspaces.pdf"sv));
     auto document = MUST(PDF::Document::create(file->bytes()));
     auto document = MUST(PDF::Document::create(file->bytes()));
     MUST(document->initialize());
     MUST(document->initialize());

+ 3 - 1
Userland/Libraries/LibPDF/CMakeLists.txt

@@ -26,4 +26,6 @@ set(SOURCES
     )
     )
 
 
 serenity_lib(LibPDF pdf)
 serenity_lib(LibPDF pdf)
-target_link_libraries(LibPDF PRIVATE LibCompress LibIPC LibGfx LibTextCodec LibCrypto)
+target_link_libraries(LibPDF PRIVATE LibCompress LibCore LibIPC LibGfx LibTextCodec LibCrypto)
+
+include(${SerenityOS_SOURCE_DIR}/Meta/CMake/download_icc_profiles.cmake)

+ 26 - 5
Userland/Libraries/LibPDF/ColorSpace.cpp

@@ -134,8 +134,23 @@ Vector<float> DeviceRGBColorSpace::default_decode() const
     return { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f };
     return { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f };
 }
 }
 
 
+static RefPtr<Gfx::ICC::Profile> s_default_cmyk_profile;
+static RefPtr<Core::Resource> s_default_cmyk_resource;
+
+static ErrorOr<void> load_default_cmyk_profile()
+{
+    auto resource = TRY(Core::Resource::load_from_uri("resource://icc/Adobe/CMYK/USWebCoatedSWOP.icc"sv));
+    auto profile = TRY(Gfx::ICC::Profile::try_load_from_externally_owned_memory(resource->data()));
+    s_default_cmyk_resource = move(resource);
+    s_default_cmyk_profile = move(profile);
+    return {};
+}
+
 ErrorOr<NonnullRefPtr<DeviceCMYKColorSpace>> DeviceCMYKColorSpace::the()
 ErrorOr<NonnullRefPtr<DeviceCMYKColorSpace>> DeviceCMYKColorSpace::the()
 {
 {
+    if (s_default_cmyk_profile.is_null())
+        TRY(load_default_cmyk_profile());
+
     static auto instance = adopt_ref(*new DeviceCMYKColorSpace());
     static auto instance = adopt_ref(*new DeviceCMYKColorSpace());
     return instance;
     return instance;
 }
 }
@@ -143,11 +158,17 @@ ErrorOr<NonnullRefPtr<DeviceCMYKColorSpace>> DeviceCMYKColorSpace::the()
 PDFErrorOr<ColorOrStyle> DeviceCMYKColorSpace::style(ReadonlySpan<float> arguments) const
 PDFErrorOr<ColorOrStyle> DeviceCMYKColorSpace::style(ReadonlySpan<float> arguments) const
 {
 {
     VERIFY(arguments.size() == 4);
     VERIFY(arguments.size() == 4);
-    auto c = arguments[0];
-    auto m = arguments[1];
-    auto y = arguments[2];
-    auto k = arguments[3];
-    return Color::from_cmyk(c, m, y, k);
+
+    u8 bytes[4];
+    bytes[0] = static_cast<u8>(arguments[0] * 255.0f);
+    bytes[1] = static_cast<u8>(arguments[1] * 255.0f);
+    bytes[2] = static_cast<u8>(arguments[2] * 255.0f);
+    bytes[3] = static_cast<u8>(arguments[3] * 255.0f);
+    auto pcs = TRY(s_default_cmyk_profile->to_pcs(bytes));
+
+    Array<u8, 3> output;
+    TRY(ICCBasedColorSpace::sRGB()->from_pcs(*s_default_cmyk_profile, pcs, output.span()));
+    return Color(output[0], output[1], output[2]);
 }
 }
 
 
 Vector<float> DeviceCMYKColorSpace::default_decode() const
 Vector<float> DeviceCMYKColorSpace::default_decode() const