Improve text rendering by handling long words and centering text

Update emoji rendering to correctly handle long words and center text, and adjust the client-side preview to remove hashtags instead of emojis.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 1a295c79-6a41-4cf1-94cd-94aa3c4f96e6
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/JcM96XE
This commit is contained in:
michaelschal committed 2025-11-21 06:10:39 +00:00
1 parent 3936220a94
commit 6898a0fca9
4 files changed
+81 -13

No files matched your search

+3 -3
View File
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ChevronLeft, ChevronRight, Send, Facebook, Instagram, Heart, MessageCircle, Share2, Bookmark } from 'lucide-react';
import type { Media } from '@shared/schema';
import { removeEmojis } from '@shared/emoji';
import { removeHashtags } from '@shared/emoji';
interface PreviewModalProps {
open: boolean;
@@ -228,8 +228,8 @@ export function PreviewModal({
const ctx = canvas.getContext('2d');
if (!ctx) return [text];
// Remove emojis for better text rendering
const cleanText = removeEmojis(text);
// Remove hashtags while keeping emojis
const cleanText = removeHashtags(text);
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
const words = cleanText.split(' ');
const lines: string[] = [];
+54 -1
View File
@@ -1,6 +1,5 @@
import { parse } from 'twemoji-parser';
import { createCanvas, loadImage, CanvasRenderingContext2D } from 'canvas';
import fetch from 'node-fetch';
interface EmojiSegment {
type: 'text' | 'emoji';
@@ -128,8 +127,43 @@ export class EmojiRenderer {
}
}
/**
* Break a long word that exceeds maxWidth into chunks
*/
private async breakLongWord(
word: string,
ctx: CanvasRenderingContext2D,
maxWidth: number,
fontSize: number
): Promise<string[]> {
const chunks: string[] = [];
let currentChunk = '';
// Split word into individual characters (graphemes for emoji support)
const chars = Array.from(word);
for (const char of chars) {
const testChunk = currentChunk + char;
const width = await this.measureText(testChunk, ctx, fontSize);
if (width > maxWidth && currentChunk) {
chunks.push(currentChunk);
currentChunk = char;
} else {
currentChunk = testChunk;
}
}
if (currentChunk) {
chunks.push(currentChunk);
}
return chunks;
}
/**
* Wrap text with emojis into multiple lines
* Handles long words/emoji sequences by breaking them when needed
*/
async wrapText(
text: string,
@@ -146,8 +180,27 @@ export class EmojiRenderer {
const testWidth = await this.measureText(testLine, ctx, fontSize);
if (testWidth > 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 wordWidth = await this.measureText(word, ctx, fontSize);
if (wordWidth > maxWidth) {
// Word is too long, need to break it into chunks
const chunks = await this.breakLongWord(word, ctx, maxWidth, fontSize);
for (let i = 0; i < chunks.length - 1; i++) {
lines.push(chunks[i]);
}
currentLine = chunks[chunks.length - 1];
}
} else if (testWidth > maxWidth && !currentLine) {
// First word is too long, need to break it
const chunks = await this.breakLongWord(word, ctx, maxWidth, fontSize);
for (let i = 0; i < chunks.length - 1; i++) {
lines.push(chunks[i]);
}
currentLine = chunks[chunks.length - 1];
} else {
currentLine = testLine;
}
+13 -9
View File
@@ -3,7 +3,8 @@ import { randomUUID } from "crypto";
import path from "path";
import fs from "fs/promises";
import { createCanvas, loadImage, registerFont } from "canvas";
import { removeEmojis } from "@shared/emoji";
import { removeHashtags } from "@shared/emoji";
import { emojiRenderer } from "./emojiRenderer";
interface CropDimensions {
width: number;
@@ -118,8 +119,8 @@ export class ImageProcessor {
const PADDING = 40;
const LINE_HEIGHT_MULTIPLIER = 1.3;
// Remove emojis from text as canvas doesn't support them
const cleanText = removeEmojis(text);
// Remove hashtags from text while keeping emojis
const cleanText = removeHashtags(text);
const canvas = createCanvas(STORY_WIDTH, STORY_HEIGHT);
const ctx = canvas.getContext('2d');
@@ -165,7 +166,7 @@ export class ImageProcessor {
while (fontSize >= MIN_FONT_SIZE) {
ctx.font = `bold ${fontSize}px "DejaVu Sans", Arial, sans-serif`;
lines = this.wrapText(ctx, cleanText, maxTextWidth);
lines = await emojiRenderer.wrapText(cleanText, ctx, maxTextWidth, fontSize);
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
totalTextHeight = lines.length * lineHeight;
@@ -179,7 +180,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, cleanText, maxTextWidth);
lines = await emojiRenderer.wrapText(cleanText, ctx, maxTextWidth, fontSize);
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
totalTextHeight = lines.length * lineHeight;
@@ -194,16 +195,19 @@ export class ImageProcessor {
}
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
const startY = textBoxY + (textBoxHeight - totalTextHeight) / 2 + lineHeight / 2;
lines.forEach((line, index) => {
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
const y = startY + (index * lineHeight);
ctx.fillText(line, STORY_WIDTH / 2, y);
});
const lineWidth = await emojiRenderer.measureText(line, ctx, fontSize);
const x = (STORY_WIDTH - lineWidth) / 2; // Center the line
await emojiRenderer.drawText(line, ctx, x, y, fontSize);
}
return canvas.toBuffer('image/png');
}
+11
View File
@@ -0,0 +1,11 @@
declare module 'twemoji-parser' {
export interface EmojiEntity {
text: string;
url: string;
indices: [number, number];
type: string;
}
export function parse(text: string, options?: { buildUrl?: (codePoint: string, assetType: string) => string }): EmojiEntity[];
export function toCodePoints(unicodeSurrogates: string): string[];
}