Commit 06835970 authored by Vlad Zahorodnii's avatar Vlad Zahorodnii Committed by Méven Car
Browse files

effects/screenshot: Introduce dbus interface version 2

With the new interface, the compositor sends raw image data instead of
encoding it as a png image, which causes a lot of overhead on Wayland.

In addition to that, the new interface is more extensible, for example
we can add new options or change the written image data format, however
the latter is less likely to happen.

BUG: 433776
parent eb7703cd
......@@ -6,4 +6,7 @@ set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources}
../service_utils.cpp
screenshot/screenshot.cpp
screenshot/screenshotdbusinterface1.cpp
screenshot/screenshotdbusinterface2.cpp
)
qt5_add_dbus_adaptor(kwin4_effect_builtins_sources screenshot/org.kde.KWin.ScreenShot2.xml screenshot/screenshotdbusinterface2.h KWin::ScreenShotDBusInterface2)
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!--
SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
-->
<node name="/org/kde/KWin/ScreenShot2">
<!--
org.kde.KWin.ScreenShot2:
@short_description: Screen shot interface
This interface provides a way to request a screenshot of a rectangular area,
a screen, or a window.
-->
<interface name="org.kde.KWin.ScreenShot2">
<!--
CaptureWindow:
@handle: The unique handle that identified the window
@options: Optional vardict with screenshot options
@pipe: The pipe file descriptor where the screenshot will be written
Take a screenshot of the specified window. The application that
requests the screenshot must have the org.kde.KWin.ScreenShot2
interface listed in the X-KDE-DBUS-Restricted-Interfaces desktop
file entry.
Available @options include:
* "include-cursor" (b): Whether the cursor should be included.
Defaults to false
* "include-decoration" (b): Whether the decoration should be included.
Defaults to false
* "native-resolution" (b): Whether the screenshot should be in
native size. Defaults to false
The following results get returned via the @results vardict:
* "type" (s): The type of the image written to the pipe. Currently,
the only supported type is "raw"
* "width" (u): The width of the image. Available only if the image
type is "raw"
* "height" (u): The height of the image. Available only if the image
type is "raw"
* "stride" (u): The number of bytes per row. Available only if the
image type is "raw"
* "format" (u): The image format, as defined in QImage::Format.
Available only if the image type is "raw"
-->
<method name="CaptureWindow">
<arg name="handle" type="s" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap" />
<arg name="options" type="a{sv}" direction="in" />
<arg name="pipe" type="h" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
<arg name="results" type="a{sv}" direction="out" />
</method>
<!--
CaptureArea:
@x: The x coordinate of the upper left corner of the area
@y: The y coordinate of the upper left corner of the area
@width: The width of the screenshot area
@height: The height of the screenshot area
@options: Optional vardict with screenshot options
@pipe: The pipe file descriptor where the screenshot will be written
Take a screenshot of the specified rectangular area. The application
that requests the screenshot must have the org.kde.KWin.ScreenShot2
interface listed in the X-KDE-DBUS-Restricted-Interfaces desktop file
entry.
Available @options include:
* "include-cursor" (b): Whether the cursor should be included.
Defaults to false
* "native-resolution" (b): Whether the screenshot should be in
native size. Defaults to false
The following results get returned via the @results vardict:
* "type" (s): The type of the image written to the pipe. Currently,
the only supported type is "raw"
* "width" (u): The width of the image. Available only if the image
type is "raw"
* "height" (u): The height of the image. Available only if the image
type is "raw"
* "stride" (u): The number of bytes per row. Available only if the
image type is "raw"
* "format" (u): The image format, as defined in QImage::Format.
Available only if the image type is "raw"
-->
<method name="CaptureArea">
<arg name="x" type="i" direction="in" />
<arg name="y" type="i" direction="in" />
<arg name="width" type="u" direction="in" />
<arg name="height" type="u" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.In4" value="QVariantMap" />
<arg name="options" type="a{sv}" direction="in" />
<arg name="pipe" type="h" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
<arg name="results" type="a{sv}" direction="out" />
</method>
<!--
CaptureScreen:
@name: The name of the screen assigned by the compositor
@options: Optional vardict with screenshot options
@pipe: The pipe file descriptor where the screenshot will be written
Take a screenshot of the specified monitor. The application that
requests the screenshot must have the org.kde.KWin.ScreenShot2
interface listed in the X-KDE-DBUS-Restricted-Interfaces desktop file
entry.
Available @options include:
* "include-cursor" (b): Whether the cursor should be included.
Defaults to false
* "native-resolution" (b): Whether the screenshot should be in
native size. Defaults to false
The following results get returned via the @results vardict:
* "type" (s): The type of the image written to the pipe. Currently,
the only supported type is "raw"
* "width" (u): The width of the image. Available only if the image
type is "raw"
* "height" (u): The height of the image. Available only if the image
type is "raw"
* "stride" (u): The number of bytes per row. Available only if the
image type is "raw"
* "format" (u): The image format, as defined in QImage::Format.
Available only if the image type is "raw"
-->
<method name="CaptureScreen">
<arg name="name" type="s" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap" />
<arg name="options" type="a{sv}" direction="in" />
<arg name="pipe" type="h" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
<arg name="results" type="a{sv}" direction="out" />
</method>
<!--
CaptureInteractive:
@kind: 0 - window, 1 - screen
@options: Optional vardict with screenshot options
@pipe: The pipe file descriptor where the screenshot will be written
Take a screenshot of a screen or a window as selected by the user.
Available @options include:
* "include-cursor" (b): Whether the cursor should be included.
Defaults to false
* "include-decoration" (b): Whether the decoration should be included.
Defaults to false
* "native-resolution" (b): Whether the screenshot should be in
native size. Defaults to false
The following results get returned via the @results vardict:
* "type" (s): The type of the image written to the pipe. Currently,
the only supported type is "raw"
* "width" (u): The width of the image. Available only if the image
type is "raw"
* "height" (u): The height of the image. Available only if the image
type is "raw"
* "stride" (u): The number of bytes per row. Available only if the
image type is "raw"
* "format" (u): The image format, as defined in QImage::Format.
Available only if the image type is "raw"
-->
<method name="CaptureInteractive">
<arg name="kind" type="u" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap" />
<arg name="options" type="a{sv}" direction="in" />
<arg name="pipe" type="h" direction="in" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
<arg name="results" type="a{sv}" direction="out" />
</method>
</interface>
</node>
......@@ -10,6 +10,7 @@
*/
#include "screenshot.h"
#include "screenshotdbusinterface1.h"
#include "screenshotdbusinterface2.h"
#include <kwinglplatform.h>
#include <kwinglutils.h>
......@@ -111,6 +112,7 @@ bool ScreenShotEffect::supported()
ScreenShotEffect::ScreenShotEffect()
: m_dbusInterface1(new ScreenShotDBusInterface1(this))
, m_dbusInterface2(new ScreenShotDBusInterface2(this))
{
connect(effects, &EffectsHandler::screenAdded, this, &ScreenShotEffect::handleScreenAdded);
connect(effects, &EffectsHandler::screenRemoved, this, &ScreenShotEffect::handleScreenRemoved);
......
......@@ -31,6 +31,7 @@ enum ScreenShotFlag {
Q_DECLARE_FLAGS(ScreenShotFlags, ScreenShotFlag)
class ScreenShotDBusInterface1;
class ScreenShotDBusInterface2;
struct ScreenShotWindowData;
struct ScreenShotAreaData;
struct ScreenShotScreenData;
......@@ -100,6 +101,7 @@ private:
QVector<ScreenShotScreenData> m_screenScreenShots;
QScopedPointer<ScreenShotDBusInterface1> m_dbusInterface1;
QScopedPointer<ScreenShotDBusInterface2> m_dbusInterface2;
EffectScreen *m_paintedScreen = nullptr;
};
......
......@@ -25,6 +25,8 @@ class ScreenShotSource1;
*
* An application that requests a screenshot must have "org.kde.kwin.Screenshot" listed in its
* X-KDE-DBUS-Restricted-Interfaces desktop file field.
*
* @deprecated Use org.kde.KWin.ScreenShot2 interface instead.
*/
class ScreenShotDBusInterface1 : public QObject, protected QDBusContext
{
......
/*
SPDX-FileCopyrightText: 2010 Martin Gräßlin <mgraesslin@kde.org>
SPDX-FileCopyrightText: 2021 Méven Car <meven.car@enioka.com>
SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "screenshotdbusinterface2.h"
#include "../service_utils.h"
#include "screenshot2adaptor.h"
#include <KLocalizedString>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QtConcurrent>
#include <errno.h>
#include <poll.h>
#include <string.h>
#include <unistd.h>
namespace KWin
{
static ScreenShotFlags screenShotFlagsFromOptions(const QVariantMap &options)
{
ScreenShotFlags flags = ScreenShotFlags();
const QVariant includeDecoration = options.value(QStringLiteral("include-decoration"));
if (includeDecoration.toBool()) {
flags |= ScreenShotIncludeDecoration;
}
const QVariant includeCursor = options.value(QStringLiteral("include-cursor"));
if (includeCursor.toBool()) {
flags |= ScreenShotIncludeCursor;
}
const QVariant nativeResolution = options.value(QStringLiteral("native-resolution"));
if (nativeResolution.toBool()) {
flags |= ScreenShotNativeResolution;
}
return flags;
}
static void writeBufferToPipe(int fileDescriptor, const QByteArray &buffer)
{
QFile file;
if (!file.open(fileDescriptor, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) {
close(fileDescriptor);
qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "failed to open pipe:" << file.errorString();
return;
}
qint64 remainingSize = buffer.size();
pollfd pfds[1];
pfds[0].fd = fileDescriptor;
pfds[0].events = POLLOUT;
while (true) {
const int ready = poll(pfds, 1, 60000);
if (ready < 0) {
if (errno != EINTR) {
qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "poll() failed:" << strerror(errno);
return;
}
} else if (ready == 0) {
qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "timed out writing to pipe";
return;
} else if (!(pfds[0].revents & POLLOUT)) {
qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "pipe is broken";
return;
} else {
const char *chunk = buffer.constData() + (buffer.size() - remainingSize);
const qint64 writtenCount = file.write(chunk, remainingSize);
if (writtenCount < 0) {
qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "write() failed:" << file.errorString();
return;
}
remainingSize -= writtenCount;
if (writtenCount == 0 || remainingSize == 0) {
return;
}
}
}
}
static const QString s_dbusServiceName = QStringLiteral("org.kde.KWin.ScreenShot2");
static const QString s_dbusInterface = QStringLiteral("org.kde.KWin.ScreenShot2");
static const QString s_dbusObjectPath = QStringLiteral("/org/kde/KWin/ScreenShot2");
static const QString s_errorNotAuthorized = QStringLiteral("org.kde.KWin.ScreenShot2.Error.NoAuthorized");
static const QString s_errorNotAuthorizedMessage = QStringLiteral("The process is not authorized to take a screenshot");
static const QString s_errorCancelled = QStringLiteral("org.kde.KWin.ScreenShot2.Error.Cancelled");
static const QString s_errorCancelledMessage = QStringLiteral("Screenshot got cancelled");
static const QString s_errorInvalidWindow = QStringLiteral("org.kde.KWin.ScreenShot2.Error.InvalidWindow");
static const QString s_errorInvalidWindowMessage = QStringLiteral("Invalid window requested");
static const QString s_errorInvalidArea = QStringLiteral("org.kde.KWin.ScreenShot2.Error.InvalidArea");
static const QString s_errorInvalidAreaMessage = QStringLiteral("Invalid area requested");
static const QString s_errorInvalidScreen = QStringLiteral("org.kde.KWin.ScreenShot2.Error.InvalidScreen");
static const QString s_errorInvalidScreenMessage = QStringLiteral("Invalid screen requested");
static const QString s_errorFileDescriptor = QStringLiteral("org.kde.KWin.ScreenShot2.Error.FileDescriptor");
static const QString s_errorFileDescriptorMessage = QStringLiteral("No valid file descriptor");
class ScreenShotSource2 : public QObject
{
Q_OBJECT
public:
explicit ScreenShotSource2(const QFuture<QImage> &future);
bool isCancelled() const;
bool isCompleted() const;
void marshal(ScreenShotSinkPipe2 *sink);
Q_SIGNALS:
void cancelled();
void completed();
private:
QFuture<QImage> m_future;
QFutureWatcher<QImage> *m_watcher;
};
class ScreenShotSourceScreen2 : public ScreenShotSource2
{
Q_OBJECT
public:
ScreenShotSourceScreen2(ScreenShotEffect *effect, EffectScreen *screen, ScreenShotFlags flags);
};
class ScreenShotSourceArea2 : public ScreenShotSource2
{
Q_OBJECT
public:
ScreenShotSourceArea2(ScreenShotEffect *effect, const QRect &area, ScreenShotFlags flags);
};
class ScreenShotSourceWindow2 : public ScreenShotSource2
{
Q_OBJECT
public:
ScreenShotSourceWindow2(ScreenShotEffect *effect, EffectWindow *window, ScreenShotFlags flags);
};
class ScreenShotSinkPipe2 : public QObject
{
Q_OBJECT
public:
ScreenShotSinkPipe2(int fileDescriptor, QDBusMessage replyMessage);
~ScreenShotSinkPipe2();
void cancel();
void flush(const QImage &image);
private:
QDBusMessage m_replyMessage;
int m_fileDescriptor;
};
ScreenShotSource2::ScreenShotSource2(const QFuture<QImage> &future)
: m_future(future)
{
m_watcher = new QFutureWatcher<QImage>(this);
connect(m_watcher, &QFutureWatcher<QImage>::finished, this, &ScreenShotSource2::completed);
connect(m_watcher, &QFutureWatcher<QImage>::canceled, this, &ScreenShotSource2::cancelled);
m_watcher->setFuture(m_future);
}
bool ScreenShotSource2::isCancelled() const
{
return m_future.isCanceled();
}
bool ScreenShotSource2::isCompleted() const
{
return m_future.isFinished();
}
void ScreenShotSource2::marshal(ScreenShotSinkPipe2 *sink)
{
sink->flush(m_future.result());
}
ScreenShotSourceScreen2::ScreenShotSourceScreen2(ScreenShotEffect *effect,
EffectScreen *screen,
ScreenShotFlags flags)
: ScreenShotSource2(effect->scheduleScreenShot(screen, flags))
{
}
ScreenShotSourceArea2::ScreenShotSourceArea2(ScreenShotEffect *effect,
const QRect &area,
ScreenShotFlags flags)
: ScreenShotSource2(effect->scheduleScreenShot(area, flags))
{
}
ScreenShotSourceWindow2::ScreenShotSourceWindow2(ScreenShotEffect *effect,
EffectWindow *window,
ScreenShotFlags flags)
: ScreenShotSource2(effect->scheduleScreenShot(window, flags))
{
}
ScreenShotSinkPipe2::ScreenShotSinkPipe2(int fileDescriptor, QDBusMessage replyMessage)
: m_replyMessage(replyMessage)
, m_fileDescriptor(fileDescriptor)
{
}
ScreenShotSinkPipe2::~ScreenShotSinkPipe2()
{
if (m_fileDescriptor != -1) {
close(m_fileDescriptor);
}
}
void ScreenShotSinkPipe2::cancel()
{
QDBusConnection::sessionBus().send(m_replyMessage.createErrorReply(s_errorCancelled,
s_errorCancelledMessage));
}
void ScreenShotSinkPipe2::flush(const QImage &image)
{
if (m_fileDescriptor == -1) {
return;
}
// Note that the type of the data stored in the vardict matters. Be careful.
QVariantMap results;
results.insert(QStringLiteral("type"), QStringLiteral("raw"));
results.insert(QStringLiteral("format"), quint32(image.format()));
results.insert(QStringLiteral("width"), quint32(image.width()));
results.insert(QStringLiteral("height"), quint32(image.height()));
results.insert(QStringLiteral("stride"), quint32(image.bytesPerLine()));
QDBusConnection::sessionBus().send(m_replyMessage.createReply(results));
QtConcurrent::run([](int fileDescriptor, const QImage &image) {
const QByteArray buffer(reinterpret_cast<const char *>(image.constBits()),
image.sizeInBytes());
writeBufferToPipe(fileDescriptor, buffer);
}, m_fileDescriptor, image);
// The ownership of the pipe file descriptor has been moved to the worker thread.
m_fileDescriptor = -1;
}
ScreenShotDBusInterface2::ScreenShotDBusInterface2(ScreenShotEffect *effect)
: QObject(effect)
, m_effect(effect)
{
new ScreenShot2Adaptor(this);
QDBusConnection::sessionBus().registerObject(s_dbusObjectPath, this);
QDBusConnection::sessionBus().registerService(s_dbusServiceName);
}
ScreenShotDBusInterface2::~ScreenShotDBusInterface2()
{
QDBusConnection::sessionBus().unregisterService(s_dbusServiceName);
QDBusConnection::sessionBus().unregisterObject(s_dbusObjectPath);
}
bool ScreenShotDBusInterface2::checkPermissions() const
{
if (!calledFromDBus()) {
return false;
}
const QDBusReply<uint> reply = connection().interface()->servicePid(message().service());
if (reply.isValid()) {
const uint pid = reply.value();
const auto interfaces = KWin::fetchRestrictedDBusInterfacesFromPid(pid);
if (!interfaces.contains(s_dbusInterface)) {
sendErrorReply(s_errorNotAuthorized, s_errorNotAuthorizedMessage);
return false;
}
} else {
return false;
}
return true;
}
QVariantMap ScreenShotDBusInterface2::CaptureWindow(const QString &handle,
const QVariantMap &options,
QDBusUnixFileDescriptor pipe)
{
if (!checkPermissions()) {
return QVariantMap();
}
EffectWindow *window = effects->findWindow(handle);
if (!window) {
bool ok;
const int winId = handle.toInt(&ok);
if (ok) {
window = effects->findWindow(winId);
} else {
qCWarning(KWINEFFECTS) << "Invalid handle:" << handle;
}
}
if (!window) {
sendErrorReply(s_errorInvalidWindow, s_errorInvalidWindowMessage);
return QVariantMap();
}
const int fileDescriptor = dup(pipe.fileDescriptor());
if (fileDescriptor == -1) {
sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage);
return QVariantMap();
}
takeScreenShot(window, screenShotFlagsFromOptions(options),
new ScreenShotSinkPipe2(fileDescriptor, message()));
setDelayedReply(true);
return QVariantMap();
}
QVariantMap ScreenShotDBusInterface2::CaptureArea(int x, int y, int width, int height,
const QVariantMap &options,
QDBusUnixFileDescriptor pipe)
{
if (!checkPermissions()) {
return QVariantMap();
}
const QRect area(x, y, width, height);
if (area.isEmpty()) {
sendErrorReply(s_errorInvalidArea, s_errorInvalidAreaMessage);
return QVariantMap();
}
const int fileDescriptor = dup(pipe.fileDescriptor());
if (fileDescriptor == -1) {
sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage);
return QVariantMap();
}
takeScreenShot(area, screenShotFlagsFromOptions(options),
new ScreenShotSinkPipe2(fileDescriptor, message()));
setDelayedReply(true);
return QVariantMap();
}
QVariantMap ScreenShotDBusInterface2::CaptureScreen(const QString &name,
const QVariantMap &options,
QDBusUnixFileDescriptor pipe)
{
if (!checkPermissions()) {
return QVariantMap();