diff --git a/attached_assets/image_1763472701076.png b/attached_assets/image_1763472701076.png new file mode 100644 index 0000000..0a3ad3e Binary files /dev/null and b/attached_assets/image_1763472701076.png differ diff --git a/server/services/imageProcessor.ts b/server/services/imageProcessor.ts index e43211f..3e547ed 100644 --- a/server/services/imageProcessor.ts +++ b/server/services/imageProcessor.ts @@ -108,6 +108,21 @@ export class ImageProcessor { } } + /** + * Removes emojis from text since canvas doesn't support them natively + */ + private removeEmojis(text: string): string { + // Remove emojis and other unicode symbols that canvas can't render + // Using a regex that removes emoji-like characters + return text + .replace(/[\u2600-\u26FF]/g, '') // Misc symbols + .replace(/[\u2700-\u27BF]/g, '') // Dingbats + .replace(/[\uD800-\uDFFF]/g, '') // Surrogate pairs (emoji range) + .replace(/[\uFE00-\uFE0F]/g, '') // Variation selectors + .replace(/\s+/g, ' ') // Normalize whitespace + .trim(); + } + async addTextToStoryImage(imageUrl: string, text: string): Promise { const STORY_WIDTH = 1080; const STORY_HEIGHT = 1920; @@ -116,6 +131,9 @@ export class ImageProcessor { const MAX_FONT_SIZE = 72; const PADDING = 40; const LINE_HEIGHT_MULTIPLIER = 1.3; + + // Remove emojis from text as canvas doesn't support them + const cleanText = this.removeEmojis(text); const canvas = createCanvas(STORY_WIDTH, STORY_HEIGHT); const ctx = canvas.getContext('2d'); @@ -161,7 +179,7 @@ export class ImageProcessor { while (fontSize >= MIN_FONT_SIZE) { ctx.font = `bold ${fontSize}px "DejaVu Sans", Arial, sans-serif`; - lines = this.wrapText(ctx, text, maxTextWidth); + lines = this.wrapText(ctx, cleanText, maxTextWidth); const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER; totalTextHeight = lines.length * lineHeight; @@ -175,7 +193,7 @@ export class ImageProcessor { if (fontSize < MIN_FONT_SIZE) { fontSize = MIN_FONT_SIZE; ctx.font = `bold ${fontSize}px "DejaVu Sans", Arial, sans-serif`; - lines = this.wrapText(ctx, text, maxTextWidth); + lines = this.wrapText(ctx, cleanText, maxTextWidth); const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER; totalTextHeight = lines.length * lineHeight; @@ -214,8 +232,27 @@ export class ImageProcessor { const metrics = ctx.measureText(testLine); if (metrics.width > maxWidth && currentLine) { + // Current line would exceed width, push it and start new line lines.push(currentLine); currentLine = word; + + // Check if the word itself is too long for a single line + const wordMetrics = ctx.measureText(word); + if (wordMetrics.width > maxWidth) { + // Word is too long, need to break it into chunks + const chunks = this.breakLongWord(ctx, word, maxWidth); + for (let i = 0; i < chunks.length - 1; i++) { + lines.push(chunks[i]); + } + currentLine = chunks[chunks.length - 1]; + } + } else if (metrics.width > maxWidth && !currentLine) { + // First word is too long, need to break it + const chunks = this.breakLongWord(ctx, word, maxWidth); + for (let i = 0; i < chunks.length - 1; i++) { + lines.push(chunks[i]); + } + currentLine = chunks[chunks.length - 1]; } else { currentLine = testLine; } @@ -227,6 +264,33 @@ export class ImageProcessor { return lines; } + + /** + * Breaks a long word into chunks that fit within maxWidth + */ + private breakLongWord(ctx: any, word: string, maxWidth: number): string[] { + const chunks: string[] = []; + let currentChunk = ''; + + for (let i = 0; i < word.length; i++) { + const char = word[i]; + const testChunk = currentChunk + char; + const metrics = ctx.measureText(testChunk); + + if (metrics.width > maxWidth && currentChunk) { + chunks.push(currentChunk); + currentChunk = char; + } else { + currentChunk = testChunk; + } + } + + if (currentChunk) { + chunks.push(currentChunk); + } + + return chunks.length > 0 ? chunks : [word]; + } } export const imageProcessor = new ImageProcessor();