mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-15 11:18:54 +02:00
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:
1 parent
3936220a94
commit
6898a0fca9
4 files changed
+81
-13
No files matched your search
@@ -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[] = [];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Vendored
+11
@@ -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[];
|
||||
}
|
||||
Reference in new issue
Block a user