Process image edits server-side for consistent results

Refactor client-side image editing to server-side processing using Sharp for applying overlays and text, ensuring consistency between the editor preview and saved image.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/Hpj2VzR
This commit is contained in:
michaelschal committed 2025-10-11 09:31:14 +00:00
1 parent ef1185d886
commit 83dbd98c8e
2 files changed
+141 -135

No files matched your search

+119 -24
View File
@@ -1171,43 +1171,138 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// Upload edited image with overlays
app.post("/api/media/upload-edited", requireAuth, upload.single("file"), async (req, res) => {
// Apply overlays to image using Sharp (server-side processing)
app.post("/api/media/apply-overlays", requireAuth, async (req, res) => {
try {
console.log("📸 Receiving edited image upload...");
if (!req.file) {
console.log("❌ No file in request");
return res.status(400).json({ error: "No file uploaded" });
}
console.log("📁 File received:", req.file.originalname, req.file.size, "bytes");
const { imageUrl, ribbon, priceBadge } = req.body;
const user = req.user as User;
const userId = user.id;
if (!imageUrl) {
return res.status(400).json({ error: "Image URL required" });
}
console.log("🎨 Applying overlays server-side with Sharp...");
// Download image from URL
const imageResponse = await fetch(imageUrl);
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
// Load with Sharp to process
const sharp = (await import("sharp")).default;
let image = sharp(imageBuffer);
// Get image metadata for dimensions
const metadata = await image.metadata();
const width = metadata.width!;
const height = metadata.height!;
console.log(`📐 Image dimensions: ${width}x${height}`);
// Create SVG overlays
const overlays: any[] = [];
// Add ribbon overlay
if (ribbon) {
const ribbonSize = 150;
const positions: Record<string, { x: number; y: number; rotation: number }> = {
top_left: { x: 0, y: 0, rotation: -45 },
top_right: { x: width - ribbonSize, y: 0, rotation: 45 },
bottom_left: { x: 0, y: height - ribbonSize, rotation: -135 },
bottom_right: { x: width - ribbonSize, y: height - ribbonSize, rotation: 135 }
};
const pos = positions[ribbon.position] || positions.top_left;
const color = ribbon.color === 'red' ? '#dc2626' : '#eab308';
const fontSize = ribbon.text.length <= 5 ? 22 : ribbon.text.length <= 8 ? 18 : ribbon.text.length <= 11 ? 15 : 12;
const ribbonSvg = `
<svg width="${ribbonSize}" height="${ribbonSize}">
<polygon points="0,0 ${ribbonSize},${ribbonSize} 0,${ribbonSize}" fill="${color}"/>
<text x="${ribbonSize/2}" y="${ribbonSize/2}"
font-family="Arial, sans-serif" font-size="${fontSize}" font-weight="bold"
fill="white" text-anchor="middle" dominant-baseline="middle"
transform="rotate(${pos.rotation} ${ribbonSize/2} ${ribbonSize/2})">
${ribbon.text}
</text>
</svg>
`;
overlays.push({
input: Buffer.from(ribbonSvg),
top: pos.y,
left: pos.x
});
}
// Add price badge overlay
if (priceBadge) {
const padding = 16;
const badgeText = `€${priceBadge.price}`;
const fontSize = priceBadge.size;
const textWidth = badgeText.length * fontSize * 0.6; // Approximate
const badgeWidth = textWidth + padding * 2;
const badgeHeight = fontSize + padding;
const color = priceBadge.color === 'red' ? '#dc2626' : '#eab308';
let x = padding;
let y = padding;
if (priceBadge.position === 'north_east') {
x = width - badgeWidth - padding;
} else if (priceBadge.position === 'south_west') {
y = height - badgeHeight - padding;
} else if (priceBadge.position === 'south_east') {
x = width - badgeWidth - padding;
y = height - badgeHeight - padding;
}
const badgeSvg = `
<svg width="${badgeWidth}" height="${badgeHeight}">
<rect x="0" y="0" width="${badgeWidth}" height="${badgeHeight}"
rx="${badgeHeight/2}" ry="${badgeHeight/2}" fill="${color}"/>
<text x="${badgeWidth/2}" y="${badgeHeight/2}"
font-family="Arial, sans-serif" font-size="${fontSize}" font-weight="bold"
fill="white" text-anchor="middle" dominant-baseline="middle">
${badgeText}
</text>
</svg>
`;
overlays.push({
input: Buffer.from(badgeSvg),
top: y,
left: x
});
}
// Apply all overlays
if (overlays.length > 0) {
image = image.composite(overlays);
}
// Convert to buffer
const outputBuffer = await image.jpeg({ quality: 95 }).toBuffer();
console.log("✅ Overlays applied, uploading to Cloudinary...");
// Check if Cloudinary is configured
const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
if (!cloudinaryConfig) {
console.log("❌ Cloudinary not configured for user:", userId);
return res.status(400).json({
error: "Cloudinary not configured. Please configure Cloudinary in Settings first."
});
}
console.log("☁️ Uploading to Cloudinary...");
// Upload edited image to Cloudinary
// Upload to Cloudinary
const uploadResult = await cloudinaryService.uploadMedia(
req.file.buffer,
outputBuffer,
`edited_${Date.now()}.jpg`,
userId,
req.file.mimetype
'image/jpeg'
);
console.log("✅ Cloudinary upload success:", uploadResult.publicId);
console.log("💾 Saving to database...");
// Save to database
const mediaItem = await storage.createMedia({
userId,
type: "image",
@@ -1217,14 +1312,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
instagramFeedUrl: uploadResult.instagramFeedUrl,
instagramStoryUrl: uploadResult.instagramStoryUrl,
fileName: `edited_${Date.now()}.jpg`,
fileSize: req.file.size,
fileSize: outputBuffer.length,
});
console.log("✅ Media saved to DB with ID:", mediaItem.id);
console.log("✅ Image saved with ID:", mediaItem.id);
res.json(mediaItem);
} catch (error) {
console.error("💥 Error uploading edited image:", error);
const errorMessage = error instanceof Error ? error.message : "Failed to upload edited image";
console.error("💥 Error applying overlays:", error);
const errorMessage = error instanceof Error ? error.message : "Failed to apply overlays";
res.status(500).json({ error: errorMessage });
}
});