Skip to content

Commit

Permalink
feat: add setting to include client frame in screenshots (pajlads#525)
Browse files Browse the repository at this point in the history
Co-authored-by: pajlada <[email protected]>
  • Loading branch information
iProdigy and pajlada authored Aug 21, 2024
1 parent 1907ef7 commit 55407dd
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Unreleased

- Minor: Add setting to include client frame in screenshots. (#525)
- Dev: Perform http notifications from okhttp's thread pool to aid users with transient network issues. (#523)

## 1.10.6
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/dinkplugin/DinkPluginConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,17 @@ default String dynamicConfigUrl() {
return "";
}

@ConfigItem(
keyName = "includeClientFrame",
name = "Include Client Frame",
description = "Whether to include the client frame in screenshots.",
position = 1018,
section = advancedSection
)
default boolean includeClientFrame() {
return false;
}

@ConfigItem(
keyName = "discordWebhook", // do not rename; would break old configs
name = "Primary Webhook URLs",
Expand Down
33 changes: 9 additions & 24 deletions src/main/java/dinkplugin/message/DiscordMessageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,12 @@
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.WorldType;
import net.runelite.api.annotations.Component;
import net.runelite.api.clan.ClanChannel;
import net.runelite.api.clan.ClanID;
import net.runelite.api.widgets.ComponentID;
import net.runelite.api.widgets.InterfaceID;
import net.runelite.api.widgets.WidgetUtil;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.discord.DiscordService;
import net.runelite.client.ui.DrawManager;
import net.runelite.client.util.ImageCapture;
import net.runelite.client.util.ImageUtil;
import okhttp3.Call;
import okhttp3.Callback;
Expand Down Expand Up @@ -66,7 +63,6 @@
@Slf4j
@Singleton
public class DiscordMessageHandler {
public static final @Component int PRIVATE_CHAT_WIDGET = WidgetUtil.packComponentId(InterfaceID.PRIVATE_CHAT, 0);

private final Gson gson;
private final Client client;
Expand All @@ -76,17 +72,19 @@ public class DiscordMessageHandler {
private final ScheduledExecutorService executor;
private final ClientThread clientThread;
private final DiscordService discordService;
private final ImageCapture imageCapture;

@Inject
@VisibleForTesting
public DiscordMessageHandler(Gson gson, Client client, DrawManager drawManager, OkHttpClient httpClient, DinkPluginConfig config, ScheduledExecutorService executor, ClientThread clientThread, DiscordService discordService) {
public DiscordMessageHandler(Gson gson, Client client, DrawManager drawManager, OkHttpClient httpClient, DinkPluginConfig config, ScheduledExecutorService executor, ClientThread clientThread, DiscordService discordService, ImageCapture imageCapture) {
this.gson = gson;
this.client = client;
this.drawManager = drawManager;
this.config = config;
this.executor = executor;
this.clientThread = clientThread;
this.discordService = discordService;
this.imageCapture = imageCapture;
this.httpClient = httpClient.newBuilder()
.addInterceptor(chain -> {
Request request = chain.request().newBuilder()
Expand Down Expand Up @@ -134,11 +132,7 @@ public void createMessage(String webhookUrl, boolean sendImage, @NonNull Notific
NotificationBody<?> mBody = enrichBody(inputBody, sendImage);
if (sendImage) {
// optionally hide chat for privacy in screenshot
boolean alreadyCaptured = mBody.getScreenshotOverride() != null;
boolean chatHidden = Utils.hideWidget(!alreadyCaptured && config.screenshotHideChat(), client, ComponentID.CHATBOX_FRAME);
boolean whispersHidden = Utils.hideWidget(!alreadyCaptured && config.screenshotHideChat(), client, PRIVATE_CHAT_WIDGET);

captureScreenshot(config.screenshotScale() / 100.0, chatHidden, whispersHidden, mBody.getScreenshotOverride())
captureScreenshot(config.screenshotScale() / 100.0, mBody.getScreenshotOverride())
.thenApply(image ->
RequestBody.create(MediaType.parse("image/" + image.getKey()), image.getValue())
)
Expand Down Expand Up @@ -322,28 +316,19 @@ private MultipartBody createBody(NotificationBody<?> mBody, @Nullable RequestBod
* while abiding by {@link Embed#MAX_IMAGE_SIZE}.
*
* @param scalePercent {@link DinkPluginConfig#screenshotScale()} divided by 100.0
* @param chatHidden Whether the chat widget should be unhidden
* @param whispersHidden Whether the whispers widget should be unhidden
* @param screenshotOverride an optional image to use instead of grabbing a frame from {@link DrawManager}
* @return future of the image byte array by the image format name
* @apiNote scalePercent should be in (0, 1]
* @implNote the image format is either "png" (lossless) or "jpeg" (lossy), both of which can be used in MIME type
*/
private CompletableFuture<Map.Entry<String, byte[]>> captureScreenshot(double scalePercent, boolean chatHidden, boolean whispersHidden, @Nullable Image screenshotOverride) {
private CompletableFuture<Map.Entry<String, byte[]>> captureScreenshot(double scalePercent, @Nullable Image screenshotOverride) {
CompletableFuture<Image> future = new CompletableFuture<>();
if (screenshotOverride != null) {
executor.execute(() -> future.complete(screenshotOverride));
future.complete(screenshotOverride);
} else {
drawManager.requestNextFrameListener(img -> {
// unhide any widgets we hid (scheduled for client thread)
Utils.unhideWidget(chatHidden, client, clientThread, ComponentID.CHATBOX_FRAME);
Utils.unhideWidget(whispersHidden, client, clientThread, PRIVATE_CHAT_WIDGET);

// resolve future on separate thread
executor.execute(() -> future.complete(img));
});
Utils.captureScreenshot(client, clientThread, drawManager, imageCapture, executor, config, future::complete);
}
return future.thenApply(ImageUtil::bufferedImageFromImage)
return future.thenApplyAsync(ImageUtil::bufferedImageFromImage, executor)
.thenApply(input -> Utils.rescale(input, scalePercent))
.thenApply(image -> {
try {
Expand Down
19 changes: 9 additions & 10 deletions src/main/java/dinkplugin/notifiers/TradeNotifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
import net.runelite.api.annotations.VarCStr;
import net.runelite.api.events.WidgetClosed;
import net.runelite.api.events.WidgetLoaded;
import net.runelite.api.widgets.ComponentID;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.game.ItemManager;
import net.runelite.client.ui.DrawManager;
import net.runelite.client.util.ImageCapture;
import net.runelite.client.util.QuantityFormatter;
import org.jetbrains.annotations.VisibleForTesting;

Expand All @@ -31,10 +31,9 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicReference;

import static dinkplugin.message.DiscordMessageHandler.PRIVATE_CHAT_WIDGET;

@Slf4j
@Singleton
public class TradeNotifier extends BaseNotifier {
Expand All @@ -53,6 +52,12 @@ public class TradeNotifier extends BaseNotifier {
@Inject
private DrawManager drawManager;

@Inject
private ImageCapture imageCapture;

@Inject
private ScheduledExecutorService executor;

@Inject
private ItemManager itemManager;

Expand Down Expand Up @@ -121,13 +126,7 @@ public void onTradeMessage(String message) {

public void onWidgetLoad(WidgetLoaded event) {
if (event.getGroupId() == TRADE_CONFIRMATION_GROUP) {
boolean chatHidden = Utils.hideWidget(config.screenshotHideChat(), client, ComponentID.CHATBOX_FRAME);
boolean whispersHidden = Utils.hideWidget(config.screenshotHideChat(), client, PRIVATE_CHAT_WIDGET);
drawManager.requestNextFrameListener(frame -> {
image.set(frame);
Utils.unhideWidget(chatHidden, client, clientThread, ComponentID.CHATBOX_FRAME);
Utils.unhideWidget(whispersHidden, client, clientThread, PRIVATE_CHAT_WIDGET);
});
Utils.captureScreenshot(client, clientThread, drawManager, imageCapture, executor, config, image::set);
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/main/java/dinkplugin/util/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
import net.runelite.api.Varbits;
import net.runelite.api.annotations.Component;
import net.runelite.api.annotations.VarCStr;
import net.runelite.api.widgets.ComponentID;
import net.runelite.api.widgets.InterfaceID;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetUtil;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.ui.DrawManager;
import net.runelite.client.util.ColorUtil;
import net.runelite.client.util.ImageCapture;
import net.runelite.client.util.Text;
import okhttp3.Call;
import okhttp3.Callback;
Expand All @@ -29,6 +33,7 @@
import javax.imageio.ImageIO;
import javax.swing.SwingUtilities;
import java.awt.Color;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
Expand All @@ -49,6 +54,8 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;

Expand All @@ -68,6 +75,8 @@ public class Utils {
private final @VarCStr int TOA_MEMBER_NAME = 1099, TOB_MEMBER_NAME = 330;
private final int TOA_PARTY_MAX_SIZE = 8, TOB_PARTY_MAX_SIZE = 5;

private final @Component int PRIVATE_CHAT_WIDGET = WidgetUtil.packComponentId(InterfaceID.PRIVATE_CHAT, 0);

/**
* Custom padding for applying SHA-256 to a long.
* SHA-256 adds '0' padding bits such that L+1+K+64 mod 512 == 0.
Expand Down Expand Up @@ -282,6 +291,21 @@ public byte[] convertImageToByteArray(BufferedImage bufferedImage, String format
return byteArrayOutputStream.toByteArray();
}

public void captureScreenshot(Client client, ClientThread clientThread, DrawManager drawManager, ImageCapture imageCapture, ExecutorService executor, DinkPluginConfig config, Consumer<Image> consumer) {
boolean chatHidden = hideWidget(config.screenshotHideChat(), client, ComponentID.CHATBOX_FRAME);
boolean whispersHidden = hideWidget(config.screenshotHideChat(), client, PRIVATE_CHAT_WIDGET);
drawManager.requestNextFrameListener(frame -> {
if (config.includeClientFrame()) {
executor.execute(() -> consumer.accept(imageCapture.addClientFrame(frame)));
} else {
consumer.accept(frame);
}

unhideWidget(chatHidden, client, clientThread, ComponentID.CHATBOX_FRAME);
unhideWidget(whispersHidden, client, clientThread, PRIVATE_CHAT_WIDGET);
});
}

public boolean hasImage(@NotNull MultipartBody body) {
return body.parts().stream().anyMatch(part -> {
MediaType type = part.body().contentType();
Expand Down
6 changes: 5 additions & 1 deletion src/test/java/dinkplugin/notifiers/MockedNotifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import net.runelite.client.game.ItemManager;
import net.runelite.client.game.NPCManager;
import net.runelite.client.ui.DrawManager;
import net.runelite.client.util.ImageCapture;
import net.runelite.http.api.RuneLiteAPI;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
Expand Down Expand Up @@ -68,6 +69,9 @@ abstract class MockedNotifierTest extends MockedTestBase {
@Bind
protected DrawManager drawManager = Mockito.mock(DrawManager.class);

@Bind
protected ImageCapture imageCapture = Mockito.mock(ImageCapture.class);

@Bind
protected Gson gson = RuneLiteAPI.GSON;

Expand Down Expand Up @@ -99,7 +103,7 @@ abstract class MockedNotifierTest extends MockedTestBase {
protected SettingsManager settingsManager = Mockito.spy(new SettingsManager(gson, client, clientThread, plugin, config, configManager, httpClient));

@Bind
protected DiscordMessageHandler messageHandler = Mockito.spy(new DiscordMessageHandler(gson, client, drawManager, httpClient, config, executor, clientThread, discordService));
protected DiscordMessageHandler messageHandler = Mockito.spy(new DiscordMessageHandler(gson, client, drawManager, httpClient, config, executor, clientThread, discordService, imageCapture));

@Override
protected void setUp() {
Expand Down

0 comments on commit 55407dd

Please sign in to comment.