File indexing completed on 2024-03-24 17:21:55

0001 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0002 // SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0003 
0004 #include <array>
0005 #include <chrono>
0006 #include <optional>
0007 
0008 #include <QBuffer>
0009 #include <QDBusConnection>
0010 #include <QDBusInterface>
0011 #include <QDBusMessage>
0012 #include <QDBusReply>
0013 #include <QDBusUnixFileDescriptor>
0014 #include <QDebug>
0015 #include <QFile>
0016 #include <QGuiApplication>
0017 #include <QImage>
0018 #include <qplatformdefs.h>
0019 
0020 using namespace std::chrono_literals;
0021 
0022 // When the tests are run under an existing session, the well known org.kde.KWin name will be claimed by the real
0023 // kwin, figure out where our test kwin resides on the bus by reverse looking up the PID.
0024 std::optional<QString> kwinService()
0025 {
0026     auto bus = QDBusConnection::sessionBus();
0027 
0028     const QString kwinPid = qEnvironmentVariable("KWIN_PID");
0029     if (kwinPid.isEmpty()) {
0030         return QStringLiteral("org.kde.KWin");
0031     }
0032 
0033     QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.DBus"),
0034                                                             QStringLiteral("/org/freedesktop/DBus"),
0035                                                             QStringLiteral("org.freedesktop.DBus"),
0036                                                             QStringLiteral("ListNames"));
0037     QDBusReply<QStringList> namesReply = bus.call(message);
0038     if (namesReply.isValid()) {
0039         const auto names = namesReply.value();
0040         for (const auto &name : names) {
0041             QDBusMessage getPid = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.DBus"),
0042                                                                     QStringLiteral("/org/freedesktop/DBus"),
0043                                                                     QStringLiteral("org.freedesktop.DBus"),
0044                                                                     QStringLiteral("GetConnectionUnixProcessID"));
0045             getPid << name;
0046             QDBusReply<quint32> pid = bus.call(getPid);
0047             if (pid.isValid() && QString::number(pid.value()) == kwinPid) {
0048                 return name;
0049             }
0050         }
0051     } else {
0052         qWarning() << namesReply.error();
0053     }
0054 
0055     return std::nullopt;
0056 }
0057 
0058 static QImage allocateImage(const QVariantMap &metadata)
0059 {
0060     bool ok = false;
0061 
0062     const int width = metadata.value(QStringLiteral("width")).toInt(&ok);
0063     if (!ok) {
0064         return {};
0065     }
0066 
0067     const int height = metadata.value(QStringLiteral("height")).toInt(&ok);
0068     if (!ok) {
0069         return {};
0070     }
0071 
0072     const int format = metadata.value(QStringLiteral("format")).toInt(&ok);
0073     if (!ok) {
0074         return {};
0075     }
0076 
0077     return {width, height, QImage::Format(format)};
0078 }
0079 
0080 static QImage readImage(int pipeFd, const QVariantMap &metadata)
0081 {
0082     QFile out;
0083     if (!out.open(pipeFd, QFileDevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
0084         qWarning() << "failed to open out pipe for reading";
0085         ::close(pipeFd);
0086         return {};
0087     }
0088 
0089     QImage result = allocateImage(metadata);
0090     if (result.isNull()) {
0091         qWarning() << "failed to allocate image";
0092         return {};
0093     }
0094 
0095     auto readData = 0;
0096     while (readData < result.sizeInBytes()) {
0097         if (const int ret = out.read(reinterpret_cast<char *>(result.bits() + readData), result.sizeInBytes() - readData); ret >= 0) {
0098             readData += ret;
0099         }
0100     }
0101     return result;
0102 }
0103 
0104 int main(int argc, char **argv)
0105 {
0106     const QGuiApplication app(argc, argv);
0107 
0108     // Unfortunately since the geometries are not including the DPR we can only look at one screen
0109     // and hope that they are all the same :(
0110     //
0111     // Also since the position is entirely wrong on wayland (always 0,0) we currently ignore all of this
0112     // and instead make full screen shots.
0113     //
0114     // const auto dpr = app.primaryScreen()->devicePixelRatio();
0115     //
0116     // auto args const = app.arguments().mid(1);
0117     // const auto positionX = int(args.takeFirst().toInt() * dpr);
0118     // const auto positionY = int(args.takeFirst().toInt() * dpr);
0119     // const auto width = int(args.takeFirst().toInt() * dpr);
0120     // const auto height = int(args.takeFirst().toInt() * dpr);
0121     // Q_ASSERT(args.isEmpty());
0122 
0123     const auto service = kwinService();
0124     if (!service.has_value()) {
0125         qWarning() << "kwin dbus service not resolved";
0126         return 1;
0127     }
0128 
0129     auto pipeFds = std::to_array<int>({0, 0});
0130     if (pipe2(pipeFds.data(), O_CLOEXEC | O_NONBLOCK) != 0) {
0131         qWarning() << "failed to open pipe" << strerror(errno);
0132         return 1;
0133     }
0134 
0135     QVariantList arguments;
0136     arguments.append(QVariantMap());
0137     arguments.append(QVariant::fromValue(QDBusUnixFileDescriptor(pipeFds.at(1))));
0138 
0139     auto bus = QDBusConnection::sessionBus();
0140     QDBusMessage message = QDBusMessage::createMethodCall(service.value(),
0141                                                           QStringLiteral("/org/kde/KWin/ScreenShot2"),
0142                                                           QStringLiteral("org.kde.KWin.ScreenShot2"),
0143                                                           QStringLiteral("CaptureActiveScreen")); // CaptureWorkspace is nicer but only available in plasma6
0144     message << QVariantMap() << QVariant::fromValue(QDBusUnixFileDescriptor(pipeFds.at(1)));
0145 
0146     QDBusPendingCall msg = bus.asyncCall(message);
0147     msg.waitForFinished();
0148     QDBusReply<QVariantMap> reply = msg.reply();
0149     if (!reply.isValid()) {
0150         qWarning() << reply.error();
0151         return 1;
0152     }
0153 
0154     ::close(pipeFds.at(1));
0155     const auto image = readImage(pipeFds.at(0), reply.value());
0156     QBuffer buf;
0157     image.save(&buf, "PNG");
0158     printf("%s", buf.data().toBase64().constData()); // intentionally no newline so we don't need to strip on the py side
0159     return 0;
0160 }
0161 
0162