From 7d9b5c859f228898b1b12290afa7273463d37479 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Dec 2025 08:20:24 +0000 Subject: [PATCH] feat: Add option to disable background removal for GIFs Co-authored-by: achimala --- src/discord/commands.ts | 12 +++++++ src/features/gif-command.ts | 3 +- src/features/gif-emoji.ts | 9 ++++- src/utils/emoji-generator.ts | 17 ++++++++- src/utils/image-processing.ts | 68 ++++++++++++++++++++++------------- 5 files changed, 82 insertions(+), 27 deletions(-) diff --git a/src/discord/commands.ts b/src/discord/commands.ts index 1921455..a414ed3 100644 --- a/src/discord/commands.ts +++ b/src/discord/commands.ts @@ -94,6 +94,12 @@ export const commandDefinitions = [ .setRequired(false) .setMinValue(0) .setMaxValue(30), + ) + .addBooleanOption((option) => + option + .setName("remove_background") + .setDescription("Remove magenta background (default: true)") + .setRequired(false), ), new SlashCommandBuilder() .setName("gif") @@ -137,5 +143,11 @@ export const commandDefinitions = [ .setRequired(false) .setMinValue(0) .setMaxValue(30), + ) + .addBooleanOption((option) => + option + .setName("remove_background") + .setDescription("Remove magenta background (default: true)") + .setRequired(false), ), ].map((builder) => builder.toJSON()); diff --git a/src/features/gif-command.ts b/src/features/gif-command.ts index 3521596..57a2c9a 100644 --- a/src/features/gif-command.ts +++ b/src/features/gif-command.ts @@ -30,8 +30,9 @@ export class GifCommandFeature implements Feature { const frames = interaction.options.getInteger("frames") ?? DEFAULT_GIF_OPTIONS.frames; const fps = interaction.options.getInteger("fps") ?? DEFAULT_GIF_OPTIONS.fps; const loopDelay = interaction.options.getInteger("loop_delay") ?? DEFAULT_GIF_OPTIONS.loopDelay; + const removeBackground = interaction.options.getBoolean("remove_background") ?? true; - const gifOptions: GifOptions = { frames, fps, loopDelay }; + const gifOptions: GifOptions = { frames, fps, loopDelay, removeBackground }; await interaction.deferReply(); diff --git a/src/features/gif-emoji.ts b/src/features/gif-emoji.ts index 3ea5218..32ed084 100644 --- a/src/features/gif-emoji.ts +++ b/src/features/gif-emoji.ts @@ -60,8 +60,9 @@ export class GifEmojiFeature implements Feature { const frames = interaction.options.getInteger("frames") ?? DEFAULT_GIF_OPTIONS.frames; const fps = interaction.options.getInteger("fps") ?? DEFAULT_GIF_OPTIONS.fps; const loopDelay = interaction.options.getInteger("loop_delay") ?? DEFAULT_GIF_OPTIONS.loopDelay; + const removeBackground = interaction.options.getBoolean("remove_background") ?? true; - const gifOptions: GifOptions = { frames, fps, loopDelay }; + const gifOptions: GifOptions = { frames, fps, loopDelay, removeBackground }; let referenceImages: ReferenceImage[] | undefined; if ( @@ -199,8 +200,13 @@ export class GifEmojiFeature implements Feature { const nameInput = interaction.fields.getTextInputValue("emoji-name"); const promptInput = interaction.fields.getTextInputValue("emoji-prompt"); const gifSettingsInput = interaction.fields.getTextInputValue("gif-settings"); + const removeBackgroundInput = interaction.fields.getTextInputValue("gif-remove-background"); const gifOptions = this.parseGifSettings(gifSettingsInput, preview.gifOptions); + if (removeBackgroundInput) { + const removeBackgroundValue = removeBackgroundInput.toLowerCase().trim(); + gifOptions.removeBackground = removeBackgroundValue === "true" || removeBackgroundValue === "yes" || removeBackgroundValue === "1"; + } const message = await interaction.channel.messages.fetch(messageId); if (!message) { @@ -274,6 +280,7 @@ export class GifEmojiFeature implements Feature { frames: validFrames.includes(frames) ? frames : defaults.frames, fps: fps >= 1 && fps <= 20 ? fps : defaults.fps, loopDelay: loopDelay >= 0 && loopDelay <= 30 ? loopDelay : defaults.loopDelay, + removeBackground: defaults.removeBackground, }; } diff --git a/src/utils/emoji-generator.ts b/src/utils/emoji-generator.ts index da8c1ff..09db686 100644 --- a/src/utils/emoji-generator.ts +++ b/src/utils/emoji-generator.ts @@ -34,6 +34,7 @@ export const DEFAULT_GIF_OPTIONS: GifOptions = { frames: 9, fps: 5, loopDelay: 0, + removeBackground: true, }; export interface EmojiPreview { @@ -166,7 +167,8 @@ export class EmojiGenerator { })(); const gridSize = Math.sqrt(gifOptions.frames); - const gifPrompt = buildGifPrompt(effectivePrompt, gridSize, true); + const removeBackground = gifOptions.removeBackground !== false; + const gifPrompt = buildGifPrompt(effectivePrompt, gridSize, true, removeBackground); const imageOptions: Parameters[0] = { prompt: gifPrompt, aspectRatio: "1:1", @@ -373,6 +375,19 @@ export class EmojiGenerator { value: `${gifOptions.frames},${gifOptions.fps},${gifOptions.loopDelay}`, }, }); + baseComponents.push({ + type: 18, + label: "Remove Background", + component: { + type: 4, + custom_id: "gif-remove-background", + style: 1, + placeholder: "true or false (default: true)", + required: false, + max_length: 5, + value: String(gifOptions.removeBackground !== false), + }, + }); } return { diff --git a/src/utils/image-processing.ts b/src/utils/image-processing.ts index 3750588..fb8037c 100644 --- a/src/utils/image-processing.ts +++ b/src/utils/image-processing.ts @@ -103,18 +103,24 @@ export interface GifOptions { frames: number; fps: number; loopDelay: number; + removeBackground?: boolean; } export function buildGifPrompt( prompt: string, gridSize: number, isEmoji: boolean = false, + removeBackground: boolean = true, ): string { const parts: string[] = [prompt]; if (isEmoji) { + if (removeBackground) { + parts.push( + "solid bright magenta background (#FF00FF) wherever it should be transparent", + ); + } parts.push( - "solid bright magenta background (#FF00FF) wherever it should be transparent", "suitable as a Discord emoji", "will be displayed very small so make things clear and avoid fine details or small text", "", @@ -150,6 +156,7 @@ export async function processGifEmojiGrid( const frameHeight = Math.floor(metadata.height / gridSize); const targetSize = 128; const frameDelay = Math.round(1000 / options.fps); + const removeBackground = options.removeBackground !== false; const encoder = new GIFEncoder(targetSize, targetSize, "neuquant", true); encoder.start(); @@ -165,6 +172,16 @@ export async function processGifEmojiGrid( const left = col * frameWidth; const top = row * frameHeight; + const resizeOptions: { + fit: "contain"; + background?: { r: number; g: number; b: number; alpha: number }; + } = { + fit: "contain", + }; + if (removeBackground) { + resizeOptions.background = { r: 255, g: 0, b: 255, alpha: 1 }; + } + const { data } = await image .clone() .extract({ @@ -173,35 +190,38 @@ export async function processGifEmojiGrid( width: frameWidth, height: frameHeight, }) - .resize(targetSize, targetSize, { - fit: "contain", - background: { r: 255, g: 0, b: 255, alpha: 1 }, - }) + .resize(targetSize, targetSize, resizeOptions) .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }); const pixels = new Uint8Array(data); - for (let i = 0; i < pixels.length; i += 4) { - const red = pixels[i] ?? 0; - const green = pixels[i + 1] ?? 0; - const blue = pixels[i + 2] ?? 0; - - const magentaScore = calculateMagentaScore(red, green, blue); - - if (magentaScore > 0.5) { - pixels[i] = 1; - pixels[i + 1] = 1; - pixels[i + 2] = 1; - pixels[i + 3] = 0; - } else if (magentaScore > 0.1) { - const despilled = despillMagenta(red, green, blue, magentaScore); - pixels[i] = despilled.red; - pixels[i + 1] = despilled.green; - pixels[i + 2] = despilled.blue; - pixels[i + 3] = 255; - } else { + if (removeBackground) { + for (let i = 0; i < pixels.length; i += 4) { + const red = pixels[i] ?? 0; + const green = pixels[i + 1] ?? 0; + const blue = pixels[i + 2] ?? 0; + + const magentaScore = calculateMagentaScore(red, green, blue); + + if (magentaScore > 0.5) { + pixels[i] = 1; + pixels[i + 1] = 1; + pixels[i + 2] = 1; + pixels[i + 3] = 0; + } else if (magentaScore > 0.1) { + const despilled = despillMagenta(red, green, blue, magentaScore); + pixels[i] = despilled.red; + pixels[i + 1] = despilled.green; + pixels[i + 2] = despilled.blue; + pixels[i + 3] = 255; + } else { + pixels[i + 3] = 255; + } + } + } else { + for (let i = 0; i < pixels.length; i += 4) { pixels[i + 3] = 255; } }