6d20b977ab
It's not guaranteed that there will be current render target in postPaintScreen() as all painting have been completed. Furthermore, even the docs of the postPaintScreen() function indicate that no painting should be done there, you can do only cleanup things, e.g. schedule a repaint, etc. paintScreen() is a much safer place to capture screenshot.
430 lines
14 KiB
C++
430 lines
14 KiB
C++
/*
|
|
KWin - the KDE window manager
|
|
This file is part of the KDE project.
|
|
|
|
SPDX-FileCopyrightText: 2010 Martin Gräßlin <mgraesslin@kde.org>
|
|
SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies)
|
|
SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
#include "screenshot.h"
|
|
#include "screenshotdbusinterface1.h"
|
|
#include "screenshotdbusinterface2.h"
|
|
|
|
#include <kwinglplatform.h>
|
|
#include <kwinglutils.h>
|
|
|
|
#include <QPainter>
|
|
|
|
namespace KWin
|
|
{
|
|
|
|
struct ScreenShotWindowData
|
|
{
|
|
QFutureInterface<QImage> promise;
|
|
ScreenShotFlags flags;
|
|
EffectWindow *window = nullptr;
|
|
};
|
|
|
|
struct ScreenShotAreaData
|
|
{
|
|
QFutureInterface<QImage> promise;
|
|
ScreenShotFlags flags;
|
|
QRect area;
|
|
QImage result;
|
|
QList<EffectScreen *> screens;
|
|
};
|
|
|
|
struct ScreenShotScreenData
|
|
{
|
|
QFutureInterface<QImage> promise;
|
|
ScreenShotFlags flags;
|
|
EffectScreen *screen = nullptr;
|
|
};
|
|
|
|
static void convertFromGLImage(QImage &img, int w, int h)
|
|
{
|
|
// from QtOpenGL/qgl.cpp
|
|
// SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies)
|
|
// see https://github.com/qt/qtbase/blob/dev/src/opengl/qgl.cpp
|
|
if (QSysInfo::ByteOrder == QSysInfo::BigEndian) {
|
|
// OpenGL gives RGBA; Qt wants ARGB
|
|
uint *p = reinterpret_cast<uint *>(img.bits());
|
|
uint *end = p + w * h;
|
|
while (p < end) {
|
|
uint a = *p << 24;
|
|
*p = (*p >> 8) | a;
|
|
p++;
|
|
}
|
|
} else {
|
|
// OpenGL gives ABGR (i.e. RGBA backwards); Qt wants ARGB
|
|
for (int y = 0; y < h; y++) {
|
|
uint *q = reinterpret_cast<uint *>(img.scanLine(y));
|
|
for (int x = 0; x < w; ++x) {
|
|
const uint pixel = *q;
|
|
*q = ((pixel << 16) & 0xff0000) | ((pixel >> 16) & 0xff)
|
|
| (pixel & 0xff00ff00);
|
|
|
|
q++;
|
|
}
|
|
}
|
|
|
|
}
|
|
img = img.mirrored();
|
|
}
|
|
|
|
bool ScreenShotEffect::supported()
|
|
{
|
|
return effects->isOpenGLCompositing() && GLRenderTarget::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);
|
|
connect(effects, &EffectsHandler::windowClosed, this, &ScreenShotEffect::handleWindowClosed);
|
|
}
|
|
|
|
ScreenShotEffect::~ScreenShotEffect()
|
|
{
|
|
cancelWindowScreenShots();
|
|
cancelAreaScreenShots();
|
|
cancelScreenScreenShots();
|
|
}
|
|
|
|
QFuture<QImage> ScreenShotEffect::scheduleScreenShot(EffectScreen *screen, ScreenShotFlags flags)
|
|
{
|
|
for (ScreenShotScreenData &data : m_screenScreenShots) {
|
|
if (data.screen == screen && data.flags == flags) {
|
|
return data.promise.future();
|
|
}
|
|
}
|
|
|
|
ScreenShotScreenData data;
|
|
data.screen = screen;
|
|
data.flags = flags;
|
|
|
|
m_screenScreenShots.append(data);
|
|
effects->addRepaint(screen->geometry());
|
|
|
|
data.promise.reportStarted();
|
|
return data.promise.future();
|
|
}
|
|
|
|
QFuture<QImage> ScreenShotEffect::scheduleScreenShot(const QRect &area, ScreenShotFlags flags)
|
|
{
|
|
for (ScreenShotAreaData &data : m_areaScreenShots) {
|
|
if (data.area == area && data.flags == flags) {
|
|
return data.promise.future();
|
|
}
|
|
}
|
|
|
|
ScreenShotAreaData data;
|
|
data.area = area;
|
|
data.flags = flags;
|
|
|
|
const QList<EffectScreen *> screens = effects->screens();
|
|
for (EffectScreen *screen : screens) {
|
|
if (screen->geometry().intersects(area)) {
|
|
data.screens.append(screen);
|
|
}
|
|
}
|
|
|
|
qreal devicePixelRatio = 1.0;
|
|
if (flags & ScreenShotNativeResolution) {
|
|
for (const EffectScreen *screen : qAsConst(data.screens)) {
|
|
if (screen->devicePixelRatio() > devicePixelRatio) {
|
|
devicePixelRatio = screen->devicePixelRatio();
|
|
}
|
|
}
|
|
}
|
|
|
|
data.result = QImage(area.size() * devicePixelRatio, QImage::Format_ARGB32_Premultiplied);
|
|
data.result.fill(Qt::transparent);
|
|
data.result.setDevicePixelRatio(devicePixelRatio);
|
|
|
|
m_areaScreenShots.append(data);
|
|
effects->addRepaint(area);
|
|
|
|
data.promise.reportStarted();
|
|
return data.promise.future();
|
|
}
|
|
|
|
QFuture<QImage> ScreenShotEffect::scheduleScreenShot(EffectWindow *window, ScreenShotFlags flags)
|
|
{
|
|
for (ScreenShotWindowData &data : m_windowScreenShots) {
|
|
if (data.window == window && data.flags == flags) {
|
|
return data.promise.future();
|
|
}
|
|
}
|
|
|
|
ScreenShotWindowData data;
|
|
data.window = window;
|
|
data.flags = flags;
|
|
|
|
m_windowScreenShots.append(data);
|
|
window->addRepaintFull();
|
|
|
|
data.promise.reportStarted();
|
|
return data.promise.future();
|
|
}
|
|
|
|
void ScreenShotEffect::cancelWindowScreenShots()
|
|
{
|
|
while (!m_windowScreenShots.isEmpty()) {
|
|
ScreenShotWindowData screenshot = m_windowScreenShots.takeLast();
|
|
screenshot.promise.reportCanceled();
|
|
}
|
|
}
|
|
|
|
void ScreenShotEffect::cancelAreaScreenShots()
|
|
{
|
|
while (!m_areaScreenShots.isEmpty()) {
|
|
ScreenShotAreaData screenshot = m_areaScreenShots.takeLast();
|
|
screenshot.promise.reportCanceled();
|
|
}
|
|
}
|
|
|
|
void ScreenShotEffect::cancelScreenScreenShots()
|
|
{
|
|
while (!m_screenScreenShots.isEmpty()) {
|
|
ScreenShotScreenData screenshot = m_screenScreenShots.takeLast();
|
|
screenshot.promise.reportCanceled();
|
|
}
|
|
}
|
|
|
|
void ScreenShotEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data)
|
|
{
|
|
m_paintedScreen = data.screen();
|
|
effects->paintScreen(mask, region, data);
|
|
|
|
while (!m_windowScreenShots.isEmpty()) {
|
|
ScreenShotWindowData screenshot = m_windowScreenShots.takeLast();
|
|
takeScreenShot(&screenshot);
|
|
}
|
|
|
|
for (int i = m_areaScreenShots.count() - 1; i >= 0; --i) {
|
|
if (takeScreenShot(&m_areaScreenShots[i])) {
|
|
m_areaScreenShots.removeAt(i);
|
|
}
|
|
}
|
|
|
|
for (int i = m_screenScreenShots.count() - 1; i >= 0; --i) {
|
|
if (takeScreenShot(&m_screenScreenShots[i])) {
|
|
m_screenScreenShots.removeAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScreenShotEffect::takeScreenShot(ScreenShotWindowData *screenshot)
|
|
{
|
|
EffectWindow *window = screenshot->window;
|
|
|
|
WindowPaintData d(window);
|
|
QRect geometry = window->expandedGeometry();
|
|
qreal devicePixelRatio = 1;
|
|
if (window->hasDecoration() && !(screenshot->flags & ScreenShotIncludeDecoration)) {
|
|
geometry = window->clientGeometry();
|
|
}
|
|
if (screenshot->flags & ScreenShotNativeResolution) {
|
|
if (const EffectScreen *screen = window->screen()) {
|
|
devicePixelRatio = screen->devicePixelRatio();
|
|
}
|
|
}
|
|
bool validTarget = true;
|
|
QScopedPointer<GLTexture> offscreenTexture;
|
|
QScopedPointer<GLRenderTarget> target;
|
|
if (effects->isOpenGLCompositing()) {
|
|
offscreenTexture.reset(new GLTexture(GL_RGBA8, geometry.size() * devicePixelRatio));
|
|
offscreenTexture->setFilter(GL_LINEAR);
|
|
offscreenTexture->setWrapMode(GL_CLAMP_TO_EDGE);
|
|
target.reset(new GLRenderTarget(offscreenTexture.data()));
|
|
validTarget = target->valid();
|
|
}
|
|
if (validTarget) {
|
|
d.setXTranslation(-geometry.x());
|
|
d.setYTranslation(-geometry.y());
|
|
|
|
// render window into offscreen texture
|
|
int mask = PAINT_WINDOW_TRANSFORMED | PAINT_WINDOW_TRANSLUCENT;
|
|
QImage img;
|
|
if (effects->isOpenGLCompositing()) {
|
|
GLRenderTarget::pushRenderTarget(target.data());
|
|
glClearColor(0.0, 0.0, 0.0, 0.0);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
glClearColor(0.0, 0.0, 0.0, 1.0);
|
|
|
|
QMatrix4x4 projection;
|
|
projection.ortho(QRect(0, 0, geometry.width(), geometry.height()));
|
|
d.setProjectionMatrix(projection);
|
|
|
|
effects->drawWindow(window, mask, infiniteRegion(), d);
|
|
|
|
// copy content from framebuffer into image
|
|
img = QImage(offscreenTexture->size(), QImage::Format_ARGB32);
|
|
img.setDevicePixelRatio(devicePixelRatio);
|
|
glReadnPixels(0, 0, img.width(), img.height(), GL_RGBA, GL_UNSIGNED_BYTE, img.sizeInBytes(),
|
|
static_cast<GLvoid *>(img.bits()));
|
|
GLRenderTarget::popRenderTarget();
|
|
convertFromGLImage(img, img.width(), img.height());
|
|
}
|
|
|
|
if (screenshot->flags & ScreenShotIncludeCursor) {
|
|
grabPointerImage(img, geometry.x(), geometry.y());
|
|
}
|
|
|
|
screenshot->promise.reportResult(img);
|
|
screenshot->promise.reportFinished();
|
|
} else {
|
|
screenshot->promise.reportCanceled();
|
|
}
|
|
}
|
|
|
|
bool ScreenShotEffect::takeScreenShot(ScreenShotAreaData *screenshot)
|
|
{
|
|
if (!effects->waylandDisplay()) {
|
|
// On X11, all screens are painted simultaneously and there is no native HiDPI support.
|
|
QImage snapshot = blitScreenshot(screenshot->area);
|
|
if (screenshot->flags & ScreenShotIncludeCursor) {
|
|
grabPointerImage(snapshot, screenshot->area.x(), screenshot->area.y());
|
|
}
|
|
screenshot->promise.reportResult(snapshot);
|
|
screenshot->promise.reportFinished();
|
|
} else {
|
|
if (!screenshot->screens.contains(m_paintedScreen)) {
|
|
return false;
|
|
}
|
|
screenshot->screens.removeOne(m_paintedScreen);
|
|
|
|
const QRect sourceRect = screenshot->area & m_paintedScreen->geometry();
|
|
qreal sourceDevicePixelRatio = 1.0;
|
|
if (screenshot->flags & ScreenShotNativeResolution) {
|
|
sourceDevicePixelRatio = m_paintedScreen->devicePixelRatio();
|
|
}
|
|
|
|
const QImage snapshot = blitScreenshot(sourceRect, sourceDevicePixelRatio);
|
|
const QRect nativeArea(screenshot->area.topLeft(),
|
|
screenshot->area.size() * screenshot->result.devicePixelRatio());
|
|
|
|
QPainter painter(&screenshot->result);
|
|
painter.setWindow(nativeArea);
|
|
painter.drawImage(sourceRect, snapshot);
|
|
painter.end();
|
|
|
|
if (screenshot->screens.isEmpty()) {
|
|
if (screenshot->flags & ScreenShotIncludeCursor) {
|
|
grabPointerImage(screenshot->result, screenshot->area.x(), screenshot->area.y());
|
|
}
|
|
screenshot->promise.reportResult(screenshot->result);
|
|
screenshot->promise.reportFinished();
|
|
}
|
|
}
|
|
|
|
return screenshot->promise.isFinished();
|
|
}
|
|
|
|
bool ScreenShotEffect::takeScreenShot(ScreenShotScreenData *screenshot)
|
|
{
|
|
if (!m_paintedScreen || screenshot->screen == m_paintedScreen) {
|
|
qreal devicePixelRatio = 1.0;
|
|
if (screenshot->flags & ScreenShotNativeResolution) {
|
|
devicePixelRatio = screenshot->screen->devicePixelRatio();
|
|
}
|
|
|
|
QImage snapshot = blitScreenshot(screenshot->screen->geometry(), devicePixelRatio);
|
|
if (screenshot->flags & ScreenShotIncludeCursor) {
|
|
const int xOffset = screenshot->screen->geometry().x();
|
|
const int yOffset = screenshot->screen->geometry().y();
|
|
grabPointerImage(snapshot, xOffset, yOffset);
|
|
}
|
|
|
|
screenshot->promise.reportResult(snapshot);
|
|
screenshot->promise.reportFinished();
|
|
}
|
|
|
|
return screenshot->promise.isFinished();
|
|
}
|
|
|
|
QImage ScreenShotEffect::blitScreenshot(const QRect &geometry, qreal devicePixelRatio) const
|
|
{
|
|
QImage image;
|
|
|
|
if (effects->isOpenGLCompositing()) {
|
|
const QSize nativeSize = geometry.size() * devicePixelRatio;
|
|
|
|
if (GLRenderTarget::blitSupported() && !GLPlatform::instance()->isGLES()) {
|
|
image = QImage(nativeSize.width(), nativeSize.height(), QImage::Format_ARGB32);
|
|
GLTexture texture(GL_RGBA8, nativeSize.width(), nativeSize.height());
|
|
GLRenderTarget target(&texture);
|
|
target.blitFromFramebuffer(effects->mapToRenderTarget(geometry));
|
|
// copy content from framebuffer into image
|
|
texture.bind();
|
|
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
|
static_cast<GLvoid *>(image.bits()));
|
|
texture.unbind();
|
|
} else {
|
|
image = QImage(nativeSize.width(), nativeSize.height(), QImage::Format_ARGB32);
|
|
glReadPixels(0, 0, nativeSize.width(), nativeSize.height(), GL_RGBA,
|
|
GL_UNSIGNED_BYTE, static_cast<GLvoid *>(image.bits()));
|
|
}
|
|
convertFromGLImage(image, nativeSize.width(), nativeSize.height());
|
|
}
|
|
|
|
image.setDevicePixelRatio(devicePixelRatio);
|
|
return image;
|
|
}
|
|
|
|
void ScreenShotEffect::grabPointerImage(QImage &snapshot, int xOffset, int yOffset) const
|
|
{
|
|
const PlatformCursorImage cursor = effects->cursorImage();
|
|
if (cursor.image().isNull()) {
|
|
return;
|
|
}
|
|
|
|
QPainter painter(&snapshot);
|
|
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
|
painter.drawImage(effects->cursorPos() - cursor.hotSpot() - QPoint(xOffset, yOffset), cursor.image());
|
|
}
|
|
|
|
bool ScreenShotEffect::isActive() const
|
|
{
|
|
return (!m_windowScreenShots.isEmpty() || !m_areaScreenShots.isEmpty() || !m_screenScreenShots.isEmpty())
|
|
&& !effects->isScreenLocked();
|
|
}
|
|
|
|
int ScreenShotEffect::requestedEffectChainPosition() const
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
void ScreenShotEffect::handleScreenAdded()
|
|
{
|
|
cancelAreaScreenShots();
|
|
}
|
|
|
|
void ScreenShotEffect::handleScreenRemoved(EffectScreen *screen)
|
|
{
|
|
cancelAreaScreenShots();
|
|
|
|
for (int i = m_screenScreenShots.count() - 1; i >= 0; --i) {
|
|
if (m_screenScreenShots[i].screen == screen) {
|
|
m_screenScreenShots[i].promise.reportCanceled();
|
|
m_screenScreenShots.removeAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScreenShotEffect::handleWindowClosed(EffectWindow *window)
|
|
{
|
|
for (int i = m_windowScreenShots.count() - 1; i >= 0; --i) {
|
|
if (m_windowScreenShots[i].window == window) {
|
|
m_windowScreenShots[i].promise.reportCanceled();
|
|
m_windowScreenShots.removeAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace KWin
|