diff --git a/emoji_test.png b/emoji_test.png new file mode 100644 index 0000000..87bff18 Binary files /dev/null and b/emoji_test.png differ diff --git a/server/reproduce_emoji.ts b/server/reproduce_emoji.ts new file mode 100644 index 0000000..2bd1008 --- /dev/null +++ b/server/reproduce_emoji.ts @@ -0,0 +1,32 @@ + +import { emojiRenderer } from "./services/emojiRenderer"; +import { createCanvas } from "canvas"; +import fs from "fs"; +import path from "path"; + +async function run() { + const width = 500; + const height = 200; + const canvas = createCanvas(width, height); + const ctx = canvas.getContext("2d"); + + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, width, height); + + ctx.fillStyle = "black"; + ctx.font = "30px Arial"; + + const text = "Hello 🌍 World! 😂"; + console.log(`Rendering text: ${text}`); + + try { + await emojiRenderer.drawText(text, ctx, 50, 100, 30); + const buffer = canvas.toBuffer("image/png"); + fs.writeFileSync("emoji_test.png", buffer); + console.log("Image saved to emoji_test.png"); + } catch (error) { + console.error("Error rendering emoji:", error); + } +} + +run(); diff --git a/server/services/emojiRenderer.ts b/server/services/emojiRenderer.ts index a4e303e..5574309 100644 --- a/server/services/emojiRenderer.ts +++ b/server/services/emojiRenderer.ts @@ -1,10 +1,6 @@ import { parse } from 'twemoji-parser'; import { createCanvas, loadImage, CanvasRenderingContext2D } from 'canvas'; -import path from 'path'; -import fs from 'fs/promises'; -import { createWriteStream } from 'fs'; -import { Readable } from 'stream'; -import { finished } from 'stream/promises'; +import sharp from 'sharp'; interface EmojiSegment { type: 'text' | 'emoji'; @@ -18,18 +14,12 @@ interface EmojiSegment { */ export class EmojiRenderer { private emojiCache = new Map(); - private emojiAssetsDir = path.join(process.cwd(), 'server', 'assets', 'emoji'); /** * Parse text into segments of text and emojis */ private parseTextWithEmojis(text: string): EmojiSegment[] { const emojis = parse(text); - - if (emojis.length > 0) { - console.log(`📝 Texte avec ${emojis.length} emoji(s) détecté(s)`); - } - if (emojis.length === 0) { return [{ type: 'text', content: text }]; } @@ -68,90 +58,31 @@ export class EmojiRenderer { } /** - * Extract emoji filename from URL - * Example: https://twemoji.maxcdn.com/v/latest/72x72/1f600.png => 1f600.png + * Download and cache emoji image */ - private getEmojiFilename(url: string): string | null { - const match = url.match(/\/([a-f0-9\-]+\.png)$/i); - return match ? match[1] : null; - } - - /** - * Ensure emoji assets directory exists - */ - private async ensureEmojiDir(): Promise { - try { - await fs.access(this.emojiAssetsDir); - } catch { - await fs.mkdir(this.emojiAssetsDir, { recursive: true }); + async getEmojiImage(url: string): Promise { + if (this.emojiCache.has(url)) { + return this.emojiCache.get(url)!; } - } - - /** - * Download emoji from CDN and save locally - */ - private async downloadEmoji(filename: string): Promise { - const url = `https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/72x72/${filename}`; - const localPath = path.join(this.emojiAssetsDir, filename); - - console.log(`🔍 Téléchargement emoji: ${filename}`); try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 15000); // 15 second timeout - - const response = await fetch(url, { signal: controller.signal }); - clearTimeout(timeout); - + const response = await fetch(url); if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); + throw new Error(`Failed to fetch emoji: ${response.status} ${response.statusText}`); } - const buffer = Buffer.from(await response.arrayBuffer()); - // Save to disk for future use - await fs.writeFile(localPath, buffer); - console.log(`✅ Emoji sauvegardé: ${filename}`); + // Convert SVG to PNG using sharp because node-canvas struggles with SVGs lacking dimensions + const pngBuffer = await sharp(buffer).png().toBuffer(); - return buffer; + this.emojiCache.set(url, pngBuffer); + return pngBuffer; } catch (error) { - console.error(`❌ Erreur téléchargement ${filename}:`, error instanceof Error ? error.message : error); + console.error(`Error downloading emoji from ${url}:`, error); throw error; } } - /** - * Get emoji image - from cache, disk, or download - */ - private async getEmojiImage(url: string): Promise { - const filename = this.getEmojiFilename(url); - if (!filename) { - throw new Error(`Invalid emoji URL: ${url}`); - } - - // Check memory cache first - const cacheKey = filename; - if (this.emojiCache.has(cacheKey)) { - return this.emojiCache.get(cacheKey)!; - } - - // Ensure directory exists - await this.ensureEmojiDir(); - - // Check if file exists locally - const localPath = path.join(this.emojiAssetsDir, filename); - try { - const buffer = await fs.readFile(localPath); - this.emojiCache.set(cacheKey, buffer); - return buffer; - } catch { - // File doesn't exist locally, download it - const buffer = await this.downloadEmoji(filename); - this.emojiCache.set(cacheKey, buffer); - return buffer; - } - } - /** * Measure the width of text with emojis */ @@ -193,15 +124,14 @@ export class EmojiRenderer { const emojiBuffer = await this.getEmojiImage(segment.url); const emojiImage = await loadImage(emojiBuffer); - // Draw emoji centered vertically with text - // When textBaseline is 'middle', y represents the vertical center + // Draw emoji slightly above baseline to align with text const emojiSize = fontSize; - const emojiY = y - (emojiSize / 2); // Center the emoji around y + const emojiY = y - fontSize * 0.8; // Adjust vertical position ctx.drawImage(emojiImage, currentX, emojiY, emojiSize, emojiSize); currentX += emojiSize; } catch (error) { - console.error(`❌ Impossible de charger l'emoji "${segment.content}":`, error instanceof Error ? error.message : error); + console.error('Error loading emoji:', error); // Fallback: draw text representation ctx.fillText(segment.content, currentX, y); currentX += ctx.measureText(segment.content).width;