From f037a69f1c2d267f5a9703b6dc7ed20897df07cf Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Sat, 7 Nov 2020 20:17:16 +0200 Subject: [PATCH] Introduce colord integration This change introduces basic colord integration in wayland session. It is implemented as a binary plugin. If an output is connected, the plugin will create the corresponding colord device using the D-Bus API and start monitoring the device for changes. When a colord devices changes, the plugin will read the VCGT tag of the current ICC color profile and apply it. --- CMakeLists.txt | 8 ++ cmake/modules/Findlcms2.cmake | 95 +++++++++++++ config-kwin.h.cmake | 1 + plugins/CMakeLists.txt | 3 + plugins/colord-integration/CMakeLists.txt | 37 ++++++ plugins/colord-integration/colorddevice.cpp | 96 ++++++++++++++ plugins/colord-integration/colorddevice.h | 38 ++++++ .../colord-integration/colordintegration.cpp | 82 ++++++++++++ .../colord-integration/colordintegration.h | 37 ++++++ plugins/colord-integration/colordtypes.h | 14 ++ plugins/colord-integration/main.cpp | 46 +++++++ plugins/colord-integration/metadata.json | 6 + .../org.freedesktop.ColorManager.Device.xml | 40 ++++++ .../org.freedesktop.ColorManager.Profile.xml | 26 ++++ .../org.freedesktop.ColorManager.xml | 125 ++++++++++++++++++ 15 files changed, 654 insertions(+) create mode 100644 cmake/modules/Findlcms2.cmake create mode 100644 plugins/colord-integration/CMakeLists.txt create mode 100644 plugins/colord-integration/colorddevice.cpp create mode 100644 plugins/colord-integration/colorddevice.h create mode 100644 plugins/colord-integration/colordintegration.cpp create mode 100644 plugins/colord-integration/colordintegration.h create mode 100644 plugins/colord-integration/colordtypes.h create mode 100644 plugins/colord-integration/main.cpp create mode 100644 plugins/colord-integration/metadata.json create mode 100644 plugins/colord-integration/org.freedesktop.ColorManager.Device.xml create mode 100644 plugins/colord-integration/org.freedesktop.ColorManager.Profile.xml create mode 100644 plugins/colord-integration/org.freedesktop.ColorManager.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index caa2104520..d2f11dfa72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -236,6 +236,14 @@ set_package_properties(X11 PROPERTIES add_feature_info("XInput" X11_Xinput_FOUND "Required for poll-free mouse cursor updates") set(HAVE_X11_XINPUT ${X11_Xinput_FOUND}) +find_package(lcms2) +set_package_properties(lcms2 PROPERTIES + DESCRIPTION "Small-footprint color management engine" + URL "http://www.littlecms.com" + TYPE OPTIONAL +) +set(HAVE_LCMS2 ${lcms2_FOUND}) + # All the required XCB components find_package(XCB 1.10 REQUIRED COMPONENTS COMPOSITE diff --git a/cmake/modules/Findlcms2.cmake b/cmake/modules/Findlcms2.cmake new file mode 100644 index 0000000000..2122021a4e --- /dev/null +++ b/cmake/modules/Findlcms2.cmake @@ -0,0 +1,95 @@ +#.rst: +# Findlcms2 +# ------- +# +# Try to find lcms2 on a Unix system. +# +# This will define the following variables: +# +# ``lcms2_FOUND`` +# True if (the requested version of) lcms2 is available +# ``lcms2_VERSION`` +# The version of lcms2 +# ``lcms2_LIBRARIES`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# ``lcms2_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``lcms2_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``lcms2_FOUND`` is TRUE, it will also define the following imported target: +# +# ``lcms2::lcms2`` +# The lcms2 library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +# Copyright (C) 2020 Vlad Zahorodnii +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the University nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +find_package(PkgConfig) +pkg_check_modules(PKG_lcms2 QUIET lcms2) + +set(lcms2_VERSION ${PKG_lcms2_VERSION}) +set(lcms2_DEFINITIONS ${PKG_lcms2_CFLAGS_OTHER}) + +find_path(lcms2_INCLUDE_DIR + NAMES lcms2.h + HINTS ${PKG_lcms2_INCLUDE_DIRS} +) + +find_library(lcms2_LIBRARY + NAMES lcms2 + HINTS ${PKG_lcms2_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(lcms2 + FOUND_VAR lcms2_FOUND + REQUIRED_VARS lcms2_LIBRARY + lcms2_INCLUDE_DIR + VERSION_VAR lcms2_VERSION +) + +if (lcms2_FOUND AND NOT TARGET lcms2::lcms2) + add_library(lcms2::lcms2 UNKNOWN IMPORTED) + set_target_properties(lcms2::lcms2 PROPERTIES + IMPORTED_LOCATION "${lcms2_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${lcms2_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${lcms2_INCLUDE_DIR}" + ) +endif() + +set(lcms2_INCLUDE_DIRS ${lcms2_INCLUDE_DIR}) +set(lcms2_LIBRARIES ${lcms2_LIBRARY}) + +mark_as_advanced(lcms2_INCLUDE_DIR) +mark_as_advanced(lcms2_LIBRARY) diff --git a/config-kwin.h.cmake b/config-kwin.h.cmake index 68b1ec1084..46c2674ee2 100644 --- a/config-kwin.h.cmake +++ b/config-kwin.h.cmake @@ -30,6 +30,7 @@ #cmakedefine01 HAVE_LIBCAP #cmakedefine01 HAVE_SCHED_RESET_ON_FORK #cmakedefine01 HAVE_ACCESSIBILITY +#cmakedefine01 HAVE_LCMS2 #if HAVE_BREEZE_DECO #define BREEZE_KDECORATION_PLUGIN_ID "${BREEZE_KDECORATION_PLUGIN_ID}" #endif diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index a80b502cdd..db379218d5 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -12,3 +12,6 @@ endif() if (PipeWire_FOUND) add_subdirectory(screencast) endif() +if (lcms2_FOUND) + add_subdirectory(colord-integration) +endif() diff --git a/plugins/colord-integration/CMakeLists.txt b/plugins/colord-integration/CMakeLists.txt new file mode 100644 index 0000000000..08759f8fed --- /dev/null +++ b/plugins/colord-integration/CMakeLists.txt @@ -0,0 +1,37 @@ +set(colordintegration_SOURCES + colorddevice.cpp + colordintegration.cpp + main.cpp +) + +ecm_qt_declare_logging_category(colordintegration_SOURCES + HEADER colordlogging.h + IDENTIFIER KWIN_COLORD + CATEGORY_NAME kwin_colord + DEFAULT_SEVERITY Warning + DESCRIPTION "KWin colord integration" +) + +set(COLORD_DEVICE_XML org.freedesktop.ColorManager.Device.xml) +set(COLORD_PROFILE_XML org.freedesktop.ColorManager.Profile.xml) +set(COLORD_MANAGER_XML org.freedesktop.ColorManager.xml) + +set_source_files_properties(${COLORD_MANAGER_XML} PROPERTIES INCLUDE "colordtypes.h") +set_source_files_properties(${COLORD_MANAGER_XML} PROPERTIES NO_NAMESPACE true) +set_source_files_properties(${COLORD_MANAGER_XML} PROPERTIES CLASSNAME CdInterface) +qt5_add_dbus_interface(colordintegration_SOURCES ${COLORD_MANAGER_XML} colordinterface) + +set_source_files_properties(${COLORD_DEVICE_XML} PROPERTIES INCLUDE "colordtypes.h") +set_source_files_properties(${COLORD_DEVICE_XML} PROPERTIES NO_NAMESPACE true) +set_source_files_properties(${COLORD_DEVICE_XML} PROPERTIES CLASSNAME CdDeviceInterface) +qt5_add_dbus_interface(colordintegration_SOURCES ${COLORD_DEVICE_XML} colorddeviceinterface) + +set_source_files_properties(${COLORD_PROFILE_XML} PROPERTIES INCLUDE "colordtypes.h") +set_source_files_properties(${COLORD_PROFILE_XML} PROPERTIES NO_NAMESPACE true) +set_source_files_properties(${COLORD_PROFILE_XML} PROPERTIES CLASSNAME CdProfileInterface) +qt5_add_dbus_interface(colordintegration_SOURCES ${COLORD_PROFILE_XML} colordprofileinterface) + +add_library(colordintegration MODULE ${colordintegration_SOURCES}) +set_target_properties(colordintegration PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/kwin/plugins/") +target_link_libraries(colordintegration kwin lcms2::lcms2) +install(TARGETS colordintegration DESTINATION ${PLUGIN_INSTALL_DIR}/kwin/plugins/) diff --git a/plugins/colord-integration/colorddevice.cpp b/plugins/colord-integration/colorddevice.cpp new file mode 100644 index 0000000000..60ffac5ddf --- /dev/null +++ b/plugins/colord-integration/colorddevice.cpp @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "colorddevice.h" +#include "abstract_output.h" +#include "colordlogging.h" +#include "colordprofileinterface.h" + +#include + +namespace KWin +{ + +ColordDevice::ColordDevice(AbstractOutput *output, QObject *parent) + : QObject(parent) + , m_output(output) +{ +} + +AbstractOutput *ColordDevice::output() const +{ + return m_output; +} + +QDBusObjectPath ColordDevice::objectPath() const +{ + return m_colordInterface ? QDBusObjectPath(m_colordInterface->path()) : QDBusObjectPath(); +} + +void ColordDevice::initialize(const QDBusObjectPath &devicePath) +{ + m_colordInterface = new CdDeviceInterface(QStringLiteral("org.freedesktop.ColorManager"), + devicePath.path(), QDBusConnection::systemBus(), this); + connect(m_colordInterface, &CdDeviceInterface::Changed, this, &ColordDevice::updateProfile); + + updateProfile(); +} + +void ColordDevice::updateProfile() +{ + const QList profiles = m_colordInterface->profiles(); + if (profiles.isEmpty()) { + qCDebug(KWIN_COLORD) << m_output->name() << "has no any color profile assigned"; + return; + } + + CdProfileInterface profile(QStringLiteral("org.freedesktop.ColorManager"), + profiles.first().path(), QDBusConnection::systemBus()); + if (!profile.isValid()) { + qCWarning(KWIN_COLORD) << profiles.first() << "is an invalid color profile"; + return; + } + + cmsHPROFILE handle = cmsOpenProfileFromFile(profile.filename().toUtf8(), "r"); + if (!handle) { + qCWarning(KWIN_COLORD) << "Failed to open profile file" << profile.filename(); + return; + } + + GammaRamp ramp(m_output->gammaRampSize()); + uint16_t *redChannel = ramp.red(); + uint16_t *greenChannel = ramp.green(); + uint16_t *blueChannel = ramp.blue(); + + cmsToneCurve **vcgt = static_cast(cmsReadTag(handle, cmsSigVcgtTag)); + if (!vcgt || !vcgt[0]) { + qCDebug(KWIN_COLORD) << "Profile" << profile.filename() << "has no VCGT tag"; + + for (uint32_t i = 0; i < ramp.size(); ++i) { + const uint16_t value = (i * 0xffff) / (ramp.size() - 1); + + redChannel[i] = value; + greenChannel[i] = value; + blueChannel[i] = value; + } + } else { + for (uint32_t i = 0; i < ramp.size(); ++i) { + const uint16_t index = (i * 0xffff) / (ramp.size() - 1); + + redChannel[i] = cmsEvalToneCurve16(vcgt[0], index); + greenChannel[i] = cmsEvalToneCurve16(vcgt[1], index); + blueChannel[i] = cmsEvalToneCurve16(vcgt[2], index); + } + } + + cmsCloseProfile(handle); + + if (!m_output->setGammaRamp(ramp)) { + qCWarning(KWIN_COLORD) << "Failed to apply color profilie on output" << m_output->name(); + } +} + +} // namespace KWin diff --git a/plugins/colord-integration/colorddevice.h b/plugins/colord-integration/colorddevice.h new file mode 100644 index 0000000000..dbb4fab446 --- /dev/null +++ b/plugins/colord-integration/colorddevice.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "colorddeviceinterface.h" + +#include +#include +#include + +namespace KWin +{ + +class AbstractOutput; + +class ColordDevice : public QObject +{ +public: + explicit ColordDevice(AbstractOutput *output, QObject *parent = nullptr); + + void initialize(const QDBusObjectPath &devicePath); + + AbstractOutput *output() const; + QDBusObjectPath objectPath() const; + +private Q_SLOTS: + void updateProfile(); + +private: + CdDeviceInterface *m_colordInterface = nullptr; + QPointer m_output; +}; + +} // namespace KWin diff --git a/plugins/colord-integration/colordintegration.cpp b/plugins/colord-integration/colordintegration.cpp new file mode 100644 index 0000000000..4d8a37ecb3 --- /dev/null +++ b/plugins/colord-integration/colordintegration.cpp @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "colordintegration.h" +#include "abstract_output.h" +#include "colorddevice.h" +#include "colordlogging.h" +#include "main.h" +#include "platform.h" + +#include + +namespace KWin +{ + +ColordIntegration::ColordIntegration(QObject *parent) + : Plugin(parent) +{ + qDBusRegisterMetaType(); + + const Platform *platform = kwinApp()->platform(); + + m_colordInterface = new CdInterface(QStringLiteral("org.freedesktop.ColorManager"), + QStringLiteral("/org/freedesktop/ColorManager"), + QDBusConnection::systemBus(), this); + + const QVector outputs = platform->outputs(); + for (AbstractOutput *output : outputs) { + handleOutputAdded(output); + } + + connect(platform, &Platform::outputAdded, this, &ColordIntegration::handleOutputAdded); + connect(platform, &Platform::outputRemoved, this, &ColordIntegration::handleOutputRemoved); +} + +void ColordIntegration::handleOutputAdded(AbstractOutput *output) +{ + ColordDevice *device = new ColordDevice(output, this); + + CdStringMap properties; + properties.insert(QStringLiteral("Kind"), QStringLiteral("display")); + properties.insert(QStringLiteral("Colorspace"), QStringLiteral("RGB")); + + QDBusPendingReply reply = + m_colordInterface->CreateDevice(output->name(), QStringLiteral("temp"), properties); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, device, watcher]() { + watcher->deleteLater(); + + const QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qCDebug(KWIN_COLORD) << "Failed to add a colord device:" << reply.error(); + delete device; + return; + } + + const QDBusObjectPath objectPath = reply.value(); + if (!device->output()) { + m_colordInterface->DeleteDevice(objectPath); + delete device; + return; + } + + device->initialize(objectPath); + m_outputToDevice.insert(device->output(), device); + }); +} + +void ColordIntegration::handleOutputRemoved(AbstractOutput *output) +{ + ColordDevice *device = m_outputToDevice.take(output); + if (device) { + m_colordInterface->DeleteDevice(device->objectPath()); + delete device; + } +} + +} // namespace KWin diff --git a/plugins/colord-integration/colordintegration.h b/plugins/colord-integration/colordintegration.h new file mode 100644 index 0000000000..9a4afee9df --- /dev/null +++ b/plugins/colord-integration/colordintegration.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "colordinterface.h" +#include "plugin.h" + +#include +#include + +namespace KWin +{ + +class AbstractOutput; +class ColordDevice; + +class KWIN_EXPORT ColordIntegration : public Plugin +{ + Q_OBJECT + +public: + explicit ColordIntegration(QObject *parent = nullptr); + +private Q_SLOTS: + void handleOutputAdded(AbstractOutput *output); + void handleOutputRemoved(AbstractOutput *output); + +private: + QHash m_outputToDevice; + CdInterface *m_colordInterface; +}; + +} // namespace KWin diff --git a/plugins/colord-integration/colordtypes.h b/plugins/colord-integration/colordtypes.h new file mode 100644 index 0000000000..af81af9c6a --- /dev/null +++ b/plugins/colord-integration/colordtypes.h @@ -0,0 +1,14 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +typedef QMap CdStringMap; +Q_DECLARE_METATYPE(CdStringMap) diff --git a/plugins/colord-integration/main.cpp b/plugins/colord-integration/main.cpp new file mode 100644 index 0000000000..07dad8f72d --- /dev/null +++ b/plugins/colord-integration/main.cpp @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "colordintegration.h" +#include "main.h" + +#include + +using namespace KWin; + +class KWIN_EXPORT ColordIntegrationFactory : public PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + explicit ColordIntegrationFactory(QObject *parent = nullptr); + + Plugin *create() const override; +}; + +ColordIntegrationFactory::ColordIntegrationFactory(QObject *parent) + : PluginFactory(parent) +{ +} + +Plugin *ColordIntegrationFactory::create() const +{ + switch (kwinApp()->operationMode()) { + case Application::OperationModeX11: + return nullptr; + case Application::OperationModeXwayland: + case Application::OperationModeWaylandOnly: + return new ColordIntegration(); + default: + return nullptr; + } +} + +K_EXPORT_PLUGIN_VERSION(KWIN_PLUGIN_API_VERSION) + +#include "main.moc" diff --git a/plugins/colord-integration/metadata.json b/plugins/colord-integration/metadata.json new file mode 100644 index 0000000000..d4cc7ac95b --- /dev/null +++ b/plugins/colord-integration/metadata.json @@ -0,0 +1,6 @@ +{ + "KPlugin": { + "EnabledByDefault": true, + "Id": "kwin5_plugin_colord" + } +} diff --git a/plugins/colord-integration/org.freedesktop.ColorManager.Device.xml b/plugins/colord-integration/org.freedesktop.ColorManager.Device.xml new file mode 100644 index 0000000000..f78bff7635 --- /dev/null +++ b/plugins/colord-integration/org.freedesktop.ColorManager.Device.xml @@ -0,0 +1,40 @@ + + + + + + + The interface used for querying color parameters for a specific device. + + + + + + + + + + The profile paths associated with this device. + Profiles are returned even if the device is disabled or + is profiling, and clients should not assume that the first + profile in this array should be applied. + + + + + + + + + + + Some value on the interface has changed. + + + + + + + diff --git a/plugins/colord-integration/org.freedesktop.ColorManager.Profile.xml b/plugins/colord-integration/org.freedesktop.ColorManager.Profile.xml new file mode 100644 index 0000000000..2ff44bce88 --- /dev/null +++ b/plugins/colord-integration/org.freedesktop.ColorManager.Profile.xml @@ -0,0 +1,26 @@ + + + + + + + The interface used for querying color profiles. + + + + + + + + + + The profile filename, if one exists. + + + + + + + diff --git a/plugins/colord-integration/org.freedesktop.ColorManager.xml b/plugins/colord-integration/org.freedesktop.ColorManager.xml new file mode 100644 index 0000000000..052bd1ab3e --- /dev/null +++ b/plugins/colord-integration/org.freedesktop.ColorManager.xml @@ -0,0 +1,125 @@ + + + + + + + The interface used for querying color parameters for the system. + + + + + + + + + + Creates a device. + + + If the device has profiles added to it in the past, and + that profiles exists already, then the new device will be + automatically have profiles added to the device. + To prevent this from happening, remove the assignment by + doing RemoveProfile on the relevant + device object. + + + + + + + + A device ID that is used to map to the device path. + + + + + + + + + Options for creating the device. This allows the session + color management component to have per-session virtual + devices cleaned up automatically or devices that are + re-created on each boot. + + + + + normal + + Normal device. + + + + temp + + Device that is removed if the user logs out. + + + + disk + + Device that is saved to disk, and restored if the + computer is restarted. + + + + + + + + + + + Properties to be used when constructing the device. + + + This optional value allows the device to be created with + the latency of one bus round-trip, rather than doing + a few SetProperty methods indervidually. + + + Any properties not interstood by colord will be added as + dictionary values to the Metadata + property. + + + + + + + + + A device path. + + + + + + + + + + + + Deletes a device. + + + + + + + + A device path. + + + + + + + +