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.
This commit is contained in:
Vlad Zahorodnii 2020-11-07 20:17:16 +02:00
parent 6f83132bd1
commit f037a69f1c
15 changed files with 654 additions and 0 deletions

View file

@ -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

View file

@ -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 <vlad.zahorodnii@kde.org>
#
# 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)

View file

@ -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

View file

@ -12,3 +12,6 @@ endif()
if (PipeWire_FOUND)
add_subdirectory(screencast)
endif()
if (lcms2_FOUND)
add_subdirectory(colord-integration)
endif()

View file

@ -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/)

View file

@ -0,0 +1,96 @@
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "colorddevice.h"
#include "abstract_output.h"
#include "colordlogging.h"
#include "colordprofileinterface.h"
#include <lcms2.h>
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<QDBusObjectPath> 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<cmsToneCurve **>(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

View file

@ -0,0 +1,38 @@
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "colorddeviceinterface.h"
#include <QDBusObjectPath>
#include <QObject>
#include <QPointer>
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<AbstractOutput> m_output;
};
} // namespace KWin

View file

@ -0,0 +1,82 @@
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
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 <QDBusPendingCallWatcher>
namespace KWin
{
ColordIntegration::ColordIntegration(QObject *parent)
: Plugin(parent)
{
qDBusRegisterMetaType<CdStringMap>();
const Platform *platform = kwinApp()->platform();
m_colordInterface = new CdInterface(QStringLiteral("org.freedesktop.ColorManager"),
QStringLiteral("/org/freedesktop/ColorManager"),
QDBusConnection::systemBus(), this);
const QVector<AbstractOutput *> 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<QDBusObjectPath> 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<QDBusObjectPath> 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

View file

@ -0,0 +1,37 @@
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "colordinterface.h"
#include "plugin.h"
#include <QHash>
#include <QObject>
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<AbstractOutput *, ColordDevice *> m_outputToDevice;
CdInterface *m_colordInterface;
};
} // namespace KWin

View file

@ -0,0 +1,14 @@
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QMap>
#include <QMetaType>
#include <QString>
typedef QMap<QString, QString> CdStringMap;
Q_DECLARE_METATYPE(CdStringMap)

View file

@ -0,0 +1,46 @@
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "colordintegration.h"
#include "main.h"
#include <KPluginFactory>
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"

View file

@ -0,0 +1,6 @@
{
"KPlugin": {
"EnabledByDefault": true,
"Id": "kwin5_plugin_colord"
}
}

View file

@ -0,0 +1,40 @@
<!DOCTYPE node PUBLIC
"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/" xmlns:doc="https://www.freedesktop.org/dbus/1.0/doc.dtd">
<interface name='org.freedesktop.ColorManager.Device'>
<doc:doc>
<doc:description>
<doc:para>
The interface used for querying color parameters for a specific device.
</doc:para>
</doc:description>
</doc:doc>
<!--***********************************************************-->
<property name='Profiles' type='ao' access='read'>
<doc:doc>
<doc:description>
<doc:para>
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.
</doc:para>
</doc:description>
</doc:doc>
</property>
<!-- ************************************************************ -->
<signal name='Changed'>
<doc:doc>
<doc:description>
<doc:para>
Some value on the interface has changed.
</doc:para>
</doc:description>
</doc:doc>
</signal>
</interface>
</node>

View file

@ -0,0 +1,26 @@
<!DOCTYPE node PUBLIC
"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/" xmlns:doc="https://www.freedesktop.org/dbus/1.0/doc.dtd">
<interface name='org.freedesktop.ColorManager.Profile'>
<doc:doc>
<doc:description>
<doc:para>
The interface used for querying color profiles.
</doc:para>
</doc:description>
</doc:doc>
<!--***********************************************************-->
<property name='Filename' type='s' access='read'>
<doc:doc>
<doc:description>
<doc:para>
The profile filename, if one exists.
</doc:para>
</doc:description>
</doc:doc>
</property>
</interface>
</node>

View file

@ -0,0 +1,125 @@
<!DOCTYPE node PUBLIC
"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/" xmlns:doc="https://www.freedesktop.org/dbus/1.0/doc.dtd">
<interface name='org.freedesktop.ColorManager'>
<doc:doc>
<doc:description>
<doc:para>
The interface used for querying color parameters for the system.
</doc:para>
</doc:description>
</doc:doc>
<!--***********************************************************-->
<method name='CreateDevice'>
<doc:doc>
<doc:description>
<doc:para>
Creates a device.
</doc:para>
<doc:para>
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 <doc:tt>RemoveProfile</doc:tt> on the relevant
device object.
</doc:para>
</doc:description>
</doc:doc>
<arg type='s' name='device_id' direction='in'>
<doc:doc>
<doc:summary>
<doc:para>
A device ID that is used to map to the device path.
</doc:para>
</doc:summary>
</doc:doc>
</arg>
<arg type='s' name='scope' direction='in'>
<doc:doc>
<doc:summary>
<doc:para>
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.
</doc:para>
</doc:summary>
<doc:list>
<doc:item>
<doc:term>normal</doc:term>
<doc:definition>
Normal device.
</doc:definition>
</doc:item>
<doc:item>
<doc:term>temp</doc:term>
<doc:definition>
Device that is removed if the user logs out.
</doc:definition>
</doc:item>
<doc:item>
<doc:term>disk</doc:term>
<doc:definition>
Device that is saved to disk, and restored if the
computer is restarted.
</doc:definition>
</doc:item>
</doc:list>
</doc:doc>
</arg>
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="CdStringMap"/>
<arg type='a{ss}' name='properties' direction='in'>
<doc:doc>
<doc:summary>
<doc:para>
Properties to be used when constructing the device.
</doc:para>
<doc:para>
This optional value allows the device to be created with
the latency of one bus round-trip, rather than doing
a few <doc:tt>SetProperty</doc:tt> methods indervidually.
</doc:para>
<doc:para>
Any properties not interstood by colord will be added as
dictionary values to the <doc:tt>Metadata</doc:tt>
property.
</doc:para>
</doc:summary>
</doc:doc>
</arg>
<arg type='o' name='object_path' direction='out'>
<doc:doc>
<doc:summary>
<doc:para>
A device path.
</doc:para>
</doc:summary>
</doc:doc>
</arg>
</method>
<!--***********************************************************-->
<method name='DeleteDevice'>
<doc:doc>
<doc:description>
<doc:para>
Deletes a device.
</doc:para>
</doc:description>
</doc:doc>
<arg type='o' name='object_path' direction='in'>
<doc:doc>
<doc:summary>
<doc:para>
A device path.
</doc:para>
</doc:summary>
</doc:doc>
</arg>
</method>
</interface>
</node>