mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
Add support for rendering emojis in stories and remove hashtags
Install twemoji-parser and node-fetch, create a new EmojiRenderer service for handling emoji rendering on canvas using Twemoji, and update the text processing logic to remove hashtags instead of emojis in shared/emoji.ts. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: ff08c9a2-11f7-4c75-8c8f-f60e79069dce Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/C5fWcTz
This commit is contained in:
1 parent
3d26473967
commit
3936220a94
4 files changed
+345
-35
No files matched your search
@@ -0,0 +1,172 @@
|
||||
import { parse } from 'twemoji-parser';
|
||||
import { createCanvas, loadImage, CanvasRenderingContext2D } from 'canvas';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
interface EmojiSegment {
|
||||
type: 'text' | 'emoji';
|
||||
content: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to render text with emojis on canvas using Twemoji
|
||||
* Converts emojis to images and renders them alongside text
|
||||
*/
|
||||
export class EmojiRenderer {
|
||||
private emojiCache = new Map<string, Buffer>();
|
||||
|
||||
/**
|
||||
* Parse text into segments of text and emojis
|
||||
*/
|
||||
private parseTextWithEmojis(text: string): EmojiSegment[] {
|
||||
const emojis = parse(text);
|
||||
if (emojis.length === 0) {
|
||||
return [{ type: 'text', content: text }];
|
||||
}
|
||||
|
||||
const segments: EmojiSegment[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const emoji of emojis) {
|
||||
// Add text before emoji
|
||||
if (emoji.indices[0] > lastIndex) {
|
||||
const textContent = text.substring(lastIndex, emoji.indices[0]);
|
||||
if (textContent) {
|
||||
segments.push({ type: 'text', content: textContent });
|
||||
}
|
||||
}
|
||||
|
||||
// Add emoji
|
||||
segments.push({
|
||||
type: 'emoji',
|
||||
content: emoji.text,
|
||||
url: emoji.url
|
||||
});
|
||||
|
||||
lastIndex = emoji.indices[1];
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < text.length) {
|
||||
const textContent = text.substring(lastIndex);
|
||||
if (textContent) {
|
||||
segments.push({ type: 'text', content: textContent });
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and cache emoji image
|
||||
*/
|
||||
private async getEmojiImage(url: string): Promise<Buffer> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the width of text with emojis
|
||||
*/
|
||||
async measureText(text: string, ctx: CanvasRenderingContext2D, fontSize: number): Promise<number> {
|
||||
const segments = this.parseTextWithEmojis(text);
|
||||
let totalWidth = 0;
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.type === 'text') {
|
||||
totalWidth += ctx.measureText(segment.content).width;
|
||||
} else {
|
||||
// Emoji width is approximately equal to font size
|
||||
totalWidth += fontSize;
|
||||
}
|
||||
}
|
||||
|
||||
return totalWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw text with emojis on canvas
|
||||
*/
|
||||
async drawText(
|
||||
text: string,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
fontSize: number
|
||||
): Promise<void> {
|
||||
const segments = this.parseTextWithEmojis(text);
|
||||
let currentX = x;
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.type === 'text') {
|
||||
ctx.fillText(segment.content, currentX, y);
|
||||
currentX += ctx.measureText(segment.content).width;
|
||||
} else if (segment.url) {
|
||||
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) {
|
||||
console.error('Error loading emoji:', error);
|
||||
// Fallback: draw text representation
|
||||
ctx.fillText(segment.content, currentX, y);
|
||||
currentX += ctx.measureText(segment.content).width;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap text with emojis into multiple lines
|
||||
*/
|
||||
async wrapText(
|
||||
text: string,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
maxWidth: number,
|
||||
fontSize: number
|
||||
): Promise<string[]> {
|
||||
const words = text.split(' ');
|
||||
const lines: string[] = [];
|
||||
let currentLine = '';
|
||||
|
||||
for (const word of words) {
|
||||
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
||||
const testWidth = await this.measureText(testLine, ctx, fontSize);
|
||||
|
||||
if (testWidth > maxWidth && currentLine) {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
} else {
|
||||
currentLine = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear emoji cache
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.emojiCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const emojiRenderer = new EmojiRenderer();
|
||||
Reference in new issue
Block a user