File indexing completed on 2024-10-13 05:06:07
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