Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Taking Screenshots on Wayland #84

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ dependencies {
implementation 'org.imgscalr:imgscalr-lib:4.2'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'com.opencsv:opencsv:5.7.1'

// DBUS interaction libraries
implementation 'com.github.hypfvieh:dbus-java-core:5.1.0'
implementation 'com.github.hypfvieh:dbus-java-transport-junixsocket:5.1.0'

// Tests
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package tools.sctrade.companion.input;

import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tools.sctrade.companion.domain.image.ImageManipulation;
import tools.sctrade.companion.domain.image.ImageType;
import tools.sctrade.companion.domain.image.ImageWriter;
import tools.sctrade.companion.domain.notification.NotificationService;
import tools.sctrade.companion.utils.AsynchronousProcessor;
import tools.sctrade.companion.utils.LocalizationUtil;
import tools.sctrade.companion.utils.ScreenshotUtil;
import tools.sctrade.companion.utils.SoundUtil;

public class ScreenPrinter implements Runnable {
Expand Down Expand Up @@ -48,8 +48,7 @@ public void run() {
try {
logger.debug("Printing screen...");
soundPlayer.play(CAMERA_SHUTTER);
var screenRectangle = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
var screenCapture = postProcess(new Robot().createScreenCapture(screenRectangle));
var screenCapture = postProcess(ScreenshotUtil.createScreenshot());
logger.debug("Printed screen");

logger.debug("Calling image processors...");
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/tools/sctrade/companion/utils/ImageUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,33 @@ public static BufferedImage toBufferedImage(Mat mat) throws IOException {
public static void writeToDiskNoFail(BufferedImage image, Path path) {
try {
writeToDisk(image, path.toAbsolutePath());
} catch (Exception e) {
} catch (IOException e) {
logger.error("There was an error writing to disk", e);
}
}

static void writeToDisk(BufferedImage image, Path path) throws IOException {
Files.createDirectories(path.getParent());
File imageFile = new File(path.toString());
String format = path.toString().substring(path.toString().lastIndexOf(".") + 1);

// Get the format from the file extension
String format = path.toString().substring(path.toString().lastIndexOf(".") + 1).toLowerCase();

// If the format is JPG, ensure no alpha channel is present
if ("jpg".equals(format) || "jpeg".equals(format)) {
if (image.getTransparency() != BufferedImage.OPAQUE) {
image = convertToJpgCompatible(image);
}
}

// Write the image to disk in the desired format
ImageIO.write(image, format, imageFile);
}

// Helper method to convert an image to a JPG-compatible format (no alpha channel)
private static BufferedImage convertToJpgCompatible(BufferedImage image) {
BufferedImage newImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
newImage.createGraphics().drawImage(image, 0, 0, null);
return newImage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package tools.sctrade.companion.utils;

import java.util.Map;

import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.annotations.DBusInterfaceName;
import org.freedesktop.dbus.interfaces.DBusInterface;
import org.freedesktop.dbus.types.Variant;

@DBusInterfaceName(value = "org.freedesktop.portal.Screenshot")
public interface ScreenshotInterface extends DBusInterface {

// https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.portal.Screenshot
// Screenshot (IN s parent_window,
// IN a{sv} options,
// OUT o handle);
DBusPath Screenshot(String parentWindow, Map<String, Variant<?>> options);
}
133 changes: 133 additions & 0 deletions src/main/java/tools/sctrade/companion/utils/ScreenshotUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package tools.sctrade.companion.utils;

import java.awt.AWTException;
import java.awt.Dimension;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;

import javax.imageio.ImageIO;

import org.freedesktop.dbus.DBusMatchRule;
import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.interfaces.DBusSigHandler;
import org.freedesktop.dbus.messages.DBusSignal;
import org.freedesktop.dbus.types.UInt32;
import org.freedesktop.dbus.types.Variant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ScreenshotUtil {

private static final Logger LOG = LoggerFactory.getLogger(ScreenshotUtil.class);

public static BufferedImage createScreenshot() {
if (isWayland()) {
LOG.debug("Wayland detected");
return createDbusScreenshot();
}
return createRobotScreenshot();
}

private static boolean isWayland() {
return "wayland".equalsIgnoreCase(System.getenv("XDG_SESSION_TYPE"));
}

private static BufferedImage createRobotScreenshot() {
try {
LOG.debug("Taking robot screenshot");
Robot robot = new Robot();
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Rectangle screenRect = new Rectangle(screenSize);
BufferedImage shot = robot.createScreenCapture(screenRect);
return shot;
} catch (AWTException | HeadlessException ex) {
LOG.error("Error creating robot screenshot", ex);
return null;
}
}

private static BufferedImage createDbusScreenshot() {
try {
LOG.debug("Taking DBus Screenshot...");
DBusConnection bus = DBusConnectionBuilder.forSessionBus().build();
LOG.debug("Unique name: {}", bus.getUniqueName());

String token = UUID.randomUUID().toString().replaceAll("-", "");
String sender = bus.getUniqueName().substring(1).replace('.', '_');
String path = String.format("/org/freedesktop/portal/desktop/request/%s/%s", sender, token);

TransferQueue<Optional<BufferedImage>> queue = new LinkedTransferQueue<>();

DBusMatchRule matchRule = new DBusMatchRule("signal", "org.freedesktop.portal.Request", "Response");
bus.addGenericSigHandler(matchRule, new DBusSigHandler<DBusSignal>() {
@Override
public void handle(DBusSignal t) {
LOG.debug("DBUS signal received");
if (path.equals(t.getPath())) {
try {
Object[] params = t.getParameters();
LOG.debug("params: size={}", params.length);
UInt32 response = (UInt32) params[0];
@SuppressWarnings("unchecked")
LinkedHashMap<String, Variant<?>> results = (LinkedHashMap<String, Variant<?>>) params[1];

if (response.intValue() == 0) {
LOG.debug("Screenshot successful");
Variant<?> vuri = results.get("uri");
String uri = (String) vuri.getValue();
LOG.debug("uri: {}", uri);

BufferedImage shot = ImageIO.read(URI.create(uri).toURL());
queue.add(Optional.of(shot));

if (Files.deleteIfExists(Path.of(URI.create(uri)))) {
LOG.debug("deleted temporary file");
}
} else {
LOG.error("Failed: response={}", response);
queue.add(Optional.empty());
}

bus.removeGenericSigHandler(matchRule, this);
} catch (IOException | DBusException e) {
LOG.error("Error handling DBus signal", e);
}
}
}
});

ScreenshotInterface iface = bus.getRemoteObject("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", ScreenshotInterface.class);
Map<String, Variant<?>> options = new HashMap<>();
options.put("interactive", new Variant<>(Boolean.FALSE));
options.put("handle_token", new Variant<>(token));
DBusPath result = iface.Screenshot("", options);
LOG.debug("result: {}", result);
LOG.debug("expected path: {}", path);

Optional<BufferedImage> shotResult = queue.take();
if (shotResult.isPresent()) {
return shotResult.get();
}
} catch (IllegalArgumentException | InterruptedException | DBusException e) {
LOG.error("Error taking DBus screenshot", e);
}
return null;
}
}