From 6be8bbf6284fc25915c7ef2249616d067019966b Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Thu, 4 Dec 2025 14:27:48 +0100 Subject: [PATCH 1/2] feat: add script to reproduce emoji rendering to a PNG image file. --- server/reproduce_emoji.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 server/reproduce_emoji.ts 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(); From ba618bfe796504e8cc046786325c845de94c2270 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 30 Dec 2025 13:03:11 +0100 Subject: [PATCH 2/2] feat: Introduce `EmojiRenderer` service for canvas, enabling text rendering with Twemoji emojis, caching, and wrapping. --- emoji_test.png | Bin 0 -> 21379 bytes server/services/emojiRenderer.ts | 27 ++++++++++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 emoji_test.png diff --git a/emoji_test.png b/emoji_test.png new file mode 100644 index 0000000000000000000000000000000000000000..87bff1882e93184237dfc839d93da60d7d127264 GIT binary patch literal 21379 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 literal 0 HcmV?d00001 diff --git a/server/services/emojiRenderer.ts b/server/services/emojiRenderer.ts index 909dc03..5574309 100644 --- a/server/services/emojiRenderer.ts +++ b/server/services/emojiRenderer.ts @@ -1,5 +1,6 @@ import { parse } from 'twemoji-parser'; import { createCanvas, loadImage, CanvasRenderingContext2D } from 'canvas'; +import sharp from 'sharp'; interface EmojiSegment { type: 'text' | 'emoji'; @@ -59,15 +60,27 @@ export class EmojiRenderer { /** * Download and cache emoji image */ - private async getEmojiImage(url: string): Promise { + async getEmojiImage(url: string): Promise { if (this.emojiCache.has(url)) { return this.emojiCache.get(url)!; } - const response = await fetch(url); - const buffer = Buffer.from(await response.arrayBuffer()); - this.emojiCache.set(url, buffer); - return buffer; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch emoji: ${response.status} ${response.statusText}`); + } + const buffer = Buffer.from(await response.arrayBuffer()); + + // Convert SVG to PNG using sharp because node-canvas struggles with SVGs lacking dimensions + const pngBuffer = await sharp(buffer).png().toBuffer(); + + this.emojiCache.set(url, pngBuffer); + return pngBuffer; + } catch (error) { + console.error(`Error downloading emoji from ${url}:`, error); + throw error; + } } /** @@ -110,11 +123,11 @@ export class EmojiRenderer { try { const emojiBuffer = await this.getEmojiImage(segment.url); const emojiImage = await loadImage(emojiBuffer); - + // Draw emoji slightly above baseline to align with text const emojiSize = fontSize; const emojiY = y - fontSize * 0.8; // Adjust vertical position - + ctx.drawImage(emojiImage, currentX, emojiY, emojiSize, emojiSize); currentX += emojiSize; } catch (error) {