mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-19 05:07:08 +02:00
Add ability to overlay text on images for social media stories
Integrate image processing to add text to story images with dynamic font sizing and text wrapping, and upload generated images to Cloudinary via a new service. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 4277243d-e609-4ee4-8cbc-3521773fc333 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/f2EnhiB
This commit is contained in:
1 parent
c62e59b8f1
commit
b581050eae
3 files changed
+167
-1
No files matched your search
@@ -2,6 +2,7 @@ import sharp from "sharp";
|
||||
import { randomUUID } from "crypto";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { createCanvas, loadImage, registerFont } from "canvas";
|
||||
|
||||
interface CropDimensions {
|
||||
width: number;
|
||||
@@ -97,6 +98,126 @@ export class ImageProcessor {
|
||||
console.error("Error deleting file:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async addTextToStoryImage(imageUrl: string, text: string): Promise<Buffer> {
|
||||
const STORY_WIDTH = 1080;
|
||||
const STORY_HEIGHT = 1920;
|
||||
const TEXT_BOX_HEIGHT_RATIO = 0.25;
|
||||
const MIN_FONT_SIZE = 24;
|
||||
const MAX_FONT_SIZE = 72;
|
||||
const PADDING = 40;
|
||||
const LINE_HEIGHT_MULTIPLIER = 1.3;
|
||||
|
||||
const canvas = createCanvas(STORY_WIDTH, STORY_HEIGHT);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const image = await loadImage(imageUrl);
|
||||
|
||||
const imgWidth = image.width;
|
||||
const imgHeight = image.height;
|
||||
const targetRatio = STORY_WIDTH / STORY_HEIGHT;
|
||||
const imgRatio = imgWidth / imgHeight;
|
||||
|
||||
let drawWidth, drawHeight, offsetX, offsetY;
|
||||
|
||||
if (imgRatio > targetRatio) {
|
||||
drawHeight = STORY_HEIGHT;
|
||||
drawWidth = imgWidth * (STORY_HEIGHT / imgHeight);
|
||||
offsetX = (STORY_WIDTH - drawWidth) / 2;
|
||||
offsetY = 0;
|
||||
} else {
|
||||
drawWidth = STORY_WIDTH;
|
||||
drawHeight = imgHeight * (STORY_WIDTH / imgWidth);
|
||||
offsetX = 0;
|
||||
offsetY = (STORY_HEIGHT - drawHeight) / 2;
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(0, 0, STORY_WIDTH, STORY_HEIGHT);
|
||||
ctx.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
|
||||
|
||||
const textBoxHeight = STORY_HEIGHT * TEXT_BOX_HEIGHT_RATIO;
|
||||
const textBoxY = STORY_HEIGHT - textBoxHeight;
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
|
||||
ctx.fillRect(0, textBoxY, STORY_WIDTH, textBoxHeight);
|
||||
|
||||
const maxTextWidth = STORY_WIDTH - (PADDING * 2);
|
||||
const maxTextHeight = textBoxHeight - (PADDING * 2);
|
||||
|
||||
let fontSize = MAX_FONT_SIZE;
|
||||
let lines: string[] = [];
|
||||
let totalTextHeight = 0;
|
||||
|
||||
while (fontSize >= MIN_FONT_SIZE) {
|
||||
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
|
||||
|
||||
lines = this.wrapText(ctx, text, maxTextWidth);
|
||||
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
|
||||
totalTextHeight = lines.length * lineHeight;
|
||||
|
||||
if (totalTextHeight <= maxTextHeight) {
|
||||
break;
|
||||
}
|
||||
|
||||
fontSize -= 2;
|
||||
}
|
||||
|
||||
if (fontSize < MIN_FONT_SIZE) {
|
||||
fontSize = MIN_FONT_SIZE;
|
||||
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
|
||||
lines = this.wrapText(ctx, text, maxTextWidth);
|
||||
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
|
||||
totalTextHeight = lines.length * lineHeight;
|
||||
|
||||
const maxLines = Math.floor(maxTextHeight / lineHeight);
|
||||
if (lines.length > maxLines) {
|
||||
lines = lines.slice(0, maxLines);
|
||||
if (lines[maxLines - 1]) {
|
||||
lines[maxLines - 1] = lines[maxLines - 1].substring(0, lines[maxLines - 1].length - 3) + '...';
|
||||
}
|
||||
totalTextHeight = maxLines * lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
const lineHeight = fontSize * LINE_HEIGHT_MULTIPLIER;
|
||||
const startY = textBoxY + (textBoxHeight - totalTextHeight) / 2 + lineHeight / 2;
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const y = startY + (index * lineHeight);
|
||||
ctx.fillText(line, STORY_WIDTH / 2, y);
|
||||
});
|
||||
|
||||
return canvas.toBuffer('image/png');
|
||||
}
|
||||
|
||||
private wrapText(ctx: any, text: string, maxWidth: number): string[] {
|
||||
const words = text.split(' ');
|
||||
const lines: string[] = [];
|
||||
let currentLine = '';
|
||||
|
||||
for (const word of words) {
|
||||
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
||||
const metrics = ctx.measureText(testLine);
|
||||
|
||||
if (metrics.width > maxWidth && currentLine) {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
} else {
|
||||
currentLine = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
export const imageProcessor = new ImageProcessor();
|
||||
Reference in new issue
Block a user