From 55407ddfae1206a0f84f50ebb7fd033e802cb9b2 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:08:29 -0700 Subject: [PATCH] feat: add setting to include client frame in screenshots (#525) Co-authored-by: pajlada --- CHANGELOG.md | 1 + .../java/dinkplugin/DinkPluginConfig.java | 11 +++++++ .../message/DiscordMessageHandler.java | 33 +++++-------------- .../dinkplugin/notifiers/TradeNotifier.java | 19 +++++------ src/main/java/dinkplugin/util/Utils.java | 24 ++++++++++++++ .../notifiers/MockedNotifierTest.java | 6 +++- 6 files changed, 59 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e674b497..27c2da61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java index a6ae9a64..4fed5a1e 100644 --- a/src/main/java/dinkplugin/DinkPluginConfig.java +++ b/src/main/java/dinkplugin/DinkPluginConfig.java @@ -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", diff --git a/src/main/java/dinkplugin/message/DiscordMessageHandler.java b/src/main/java/dinkplugin/message/DiscordMessageHandler.java index e1d35247..b2eff3be 100644 --- a/src/main/java/dinkplugin/message/DiscordMessageHandler.java +++ b/src/main/java/dinkplugin/message/DiscordMessageHandler.java @@ -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; @@ -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; @@ -76,10 +72,11 @@ 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; @@ -87,6 +84,7 @@ public DiscordMessageHandler(Gson gson, Client client, DrawManager drawManager, this.executor = executor; this.clientThread = clientThread; this.discordService = discordService; + this.imageCapture = imageCapture; this.httpClient = httpClient.newBuilder() .addInterceptor(chain -> { Request request = chain.request().newBuilder() @@ -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()) ) @@ -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> captureScreenshot(double scalePercent, boolean chatHidden, boolean whispersHidden, @Nullable Image screenshotOverride) { + private CompletableFuture> captureScreenshot(double scalePercent, @Nullable Image screenshotOverride) { CompletableFuture 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 { diff --git a/src/main/java/dinkplugin/notifiers/TradeNotifier.java b/src/main/java/dinkplugin/notifiers/TradeNotifier.java index 4da1dfa3..c06531ee 100644 --- a/src/main/java/dinkplugin/notifiers/TradeNotifier.java +++ b/src/main/java/dinkplugin/notifiers/TradeNotifier.java @@ -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; @@ -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 { @@ -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; @@ -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); } } diff --git a/src/main/java/dinkplugin/util/Utils.java b/src/main/java/dinkplugin/util/Utils.java index 56b0c7e7..2472a327 100644 --- a/src/main/java/dinkplugin/util/Utils.java +++ b/src/main/java/dinkplugin/util/Utils.java @@ -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; @@ -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; @@ -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; @@ -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. @@ -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 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(); diff --git a/src/test/java/dinkplugin/notifiers/MockedNotifierTest.java b/src/test/java/dinkplugin/notifiers/MockedNotifierTest.java index 76ec4ea1..21483b07 100644 --- a/src/test/java/dinkplugin/notifiers/MockedNotifierTest.java +++ b/src/test/java/dinkplugin/notifiers/MockedNotifierTest.java @@ -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; @@ -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; @@ -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() {