diff --git a/src/equicordplugins/imgToGif/index.tsx b/src/equicordplugins/imgToGif/index.tsx
new file mode 100644
index 00000000..faa15b31
--- /dev/null
+++ b/src/equicordplugins/imgToGif/index.tsx
@@ -0,0 +1,145 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, sendBotMessage } from "@api/Commands";
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { findByPropsLazy } from "@webpack";
+import { DraftType, UploadHandler, UploadManager } from "@webpack/common";
+import { applyPalette, GIFEncoder, quantize } from "gifenc";
+
+const DEFAULT_RESOLUTION = 512;
+const FRAMES = 1;
+
+const UploadStore = findByPropsLazy("getUploads");
+
+function loadImage(source: File | string) {
+ const isFile = source instanceof File;
+ const url = isFile ? URL.createObjectURL(source) : source;
+
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => {
+ if (isFile) URL.revokeObjectURL(url);
+ resolve(img);
+ };
+ img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event);
+ img.crossOrigin = "Anonymous";
+ img.src = url;
+ });
+}
+
+async function resolveImage(options: Argument[], ctx: CommandContext): Promise<{ image: File | null; width: number | null; height: number | null; }> {
+ let image: File | null = null;
+ let width: number | null = null;
+ let height: number | null = null;
+
+ for (const opt of options) {
+ switch (opt.name) {
+ case "image":
+ const upload = UploadStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand);
+ if (upload) {
+ if (!upload.isImage) {
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
+ throw "Upload is not an image";
+ }
+ image = upload.item.file;
+ }
+ break;
+ case "width":
+ width = Number(opt.value);
+ break;
+ case "height":
+ height = Number(opt.value);
+ break;
+ }
+ }
+
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
+ return { image, width, height };
+}
+
+export default definePlugin({
+ name: "imgtogif",
+ description: "Adds a /imgtogif slash command to create a gif from any image",
+ authors: [Devs.zyqunix],
+ commands: [
+ {
+ inputType: ApplicationCommandInputType.BUILT_IN,
+ name: "imgtogif",
+ description: "Allows you to turn an image to a gif",
+ options: [
+ {
+ name: "image",
+ description: "Image attachment to use",
+ type: ApplicationCommandOptionType.ATTACHMENT
+ },
+ {
+ name: "width",
+ description: "Width of the gif",
+ type: ApplicationCommandOptionType.INTEGER
+ },
+ {
+ name: "height",
+ description: "Height of the gif",
+ type: ApplicationCommandOptionType.INTEGER
+ }
+ ],
+ execute: async (opts, cmdCtx) => {
+ try {
+ const { image, width, height } = await resolveImage(opts, cmdCtx);
+ if (!image) throw "No Image specified!";
+
+ const avatar = await loadImage(image);
+
+ const gifHeight = height ?? DEFAULT_RESOLUTION;
+ const gifWidth = width ?? DEFAULT_RESOLUTION;
+
+ const gif = GIFEncoder();
+ const canvas = document.createElement("canvas");
+ canvas.width = gifWidth;
+ canvas.height = gifHeight;
+ const ctx = canvas.getContext("2d")!;
+
+ UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
+
+ for (let i = 0; i < FRAMES; i++) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(avatar, 0, 0, avatar.width, avatar.height, 0, 0, canvas.width, canvas.height);
+
+ const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const palette = quantize(data, 256);
+ const index = applyPalette(data, palette);
+
+ gif.writeFrame(index, canvas.width, canvas.height, {
+ transparent: true,
+ palette,
+ });
+ }
+
+ gif.finish();
+ const file = new File([gif.bytesView()], "converted.gif", { type: "image/gif" });
+ setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
+ } catch (err) {
+ UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
+ sendBotMessage(cmdCtx.channel.id, { content: String(err) });
+ }
+ },
+ },
+ ]
+});
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index e58401d0..d6de661f 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -256,6 +256,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Alyxia Sother",
id: 952185386350829688n
},
+ zyqunix: {
+ name: "zyqunix",
+ id: 1201415921802170388n
+ },
Remty: {
name: "Remty",
id: 335055032204656642n