Equicord/src/equicordplugins/moreStickers/upload.ts
2025-06-17 00:04:57 -04:00

178 lines
6.2 KiB
TypeScript

/*
* 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<File> {
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 || {});
}
}