/* * Vencord, a Discord client mod * Copyright (c) 2024 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import { FFmpeg } from "@ffmpeg/ffmpeg"; import { fetchFile } from "@ffmpeg/util"; import { findByPropsLazy } from "@webpack"; import { ChannelStore, UploadHandler } from "@webpack/common"; import { FFmpegState, Sticker } from "./types"; const PendingReplyStore = findByPropsLazy("getPendingReply"); const MessageUtils = findByPropsLazy("sendMessage"); const DraftStore = findByPropsLazy("getDraft", "getState"); export const ffmpeg = new FFmpeg(); async function resizeImage(url: string) { const originalImage = new Image(); originalImage.crossOrigin = "anonymous"; // If the image is hosted on a different domain, enable CORS const loadImage = new Promise((resolve, reject) => { originalImage.onload = resolve; originalImage.onerror = reject; originalImage.src = url; }); await loadImage; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Could not get canvas context"); // Determine the target size of the processed image (160x160) const targetSize = 160; // Calculate the scale factor to resize the image const scaleFactor = Math.min(targetSize / originalImage.width, targetSize / originalImage.height); // Calculate the dimensions for resizing the image while maintaining aspect ratio const resizedWidth = originalImage.width * scaleFactor; const resizedHeight = originalImage.height * scaleFactor; // Set the canvas size to the target dimensions canvas.width = targetSize; canvas.height = targetSize; // Draw the resized image onto the canvas ctx.drawImage(originalImage, 0, 0, resizedWidth, resizedHeight); // Get the canvas image data const imageData = ctx.getImageData(0, 0, targetSize, targetSize); const { data } = imageData; // Apply any additional image processing or filters here if desired // Convert the image data to a Blob const blob: Blob | null = await new Promise(resolve => { canvas.toBlob(resolve, "image/png"); }); if (!blob) throw new Error("Could not convert canvas to blob"); // return the object URL representing the Blob return blob; } async function toGIF(url: string, ffmpeg: FFmpeg): Promise { const filename = (new URL(url)).pathname.split("/").pop() ?? "image.png"; await ffmpeg.writeFile(filename, await fetchFile(url)); const outputFilename = "output.gif"; await ffmpeg.exec(["-i", filename, "-filter_complex", `split[s0][s1]; [s0]palettegen= stats_mode=single: transparency_color=000000[p]; [s1][p]paletteuse= new=1: alpha_threshold=10`, outputFilename]); const data = await ffmpeg.readFile(outputFilename); await ffmpeg.deleteFile(filename); await ffmpeg.deleteFile(outputFilename); if (typeof data === "string") { throw new Error("Could not read file"); } return new File([new Uint8Array(data.buffer)], outputFilename, { type: "image/gif" }); } export async function sendSticker({ channelId, sticker, sendAsLink, ctrlKey, shiftKey, ffmpegState }: { channelId: string; sticker: Sticker; sendAsLink?: boolean; ctrlKey: boolean; shiftKey: boolean; ffmpegState?: FFmpegState; }) { let messageContent = ""; const { textEditor } = Vencord.Plugins.plugins.MoreStickers as any; if (DraftStore) { messageContent = DraftStore.getDraft(channelId, 0); } let messageOptions = {}; if (PendingReplyStore) { const pendingReply = PendingReplyStore.getPendingReply(channelId); if (pendingReply) { messageOptions = MessageUtils.getSendMessageOptionsForReply(pendingReply); } } if ((ctrlKey || !sendAsLink) && !shiftKey) { let file: File | null = null; if (sticker?.isAnimated) { if (!ffmpegState) { throw new Error("FFmpeg state is not provided"); } if (!ffmpegState?.ffmpeg) { throw new Error("FFmpeg is not provided"); } if (!ffmpegState?.isLoaded) { throw new Error("FFmpeg is not loaded"); } file = await toGIF(sticker.image, ffmpegState.ffmpeg); } else { const url = new URL(sticker.image); url.searchParams.set("t", Date.now().toString()); // To prevent caching, in order to avoid CORS bug in Chrome const response = await fetch(sticker.image); const orgImageUrl = URL.createObjectURL(await response.blob()); const processedImage = await resizeImage(orgImageUrl); const filename = sticker.filename ?? (new URL(sticker.image)).pathname.split("/").pop(); let mimeType = "image/png"; switch (filename?.split(".").pop()?.toLowerCase()) { case "jpg": case "jpeg": mimeType = "image/jpeg"; break; case "gif": mimeType = "image/gif"; break; case "webp": mimeType = "image/webp"; break; case "svg": mimeType = "image/svg+xml"; break; } file = new File([processedImage], filename!, { type: mimeType }); } UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), 0); return; } else if (shiftKey) { if (!messageContent.endsWith(" ") || !messageContent.endsWith("\n")) messageContent += " "; messageContent += sticker.image; if (ctrlKey && textEditor && textEditor.insertText && typeof textEditor.insertText === "function") { textEditor.insertText(messageContent); } else { MessageUtils._sendMessage(channelId, { content: sticker.image }, messageOptions || {}); } } else { MessageUtils._sendMessage(channelId, { content: `${messageContent} ${sticker.image}`.trim() }, messageOptions || {}); } }