From 83dbd98c8e67f9c3a735925e2ad93f09259ee58e Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Sat, 11 Oct 2025 09:31:14 +0000 Subject: [PATCH] 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 --- client/src/pages/image-editor.tsx | 133 +++++---------------------- server/routes.ts | 143 +++++++++++++++++++++++++----- 2 files changed, 141 insertions(+), 135 deletions(-) diff --git a/client/src/pages/image-editor.tsx b/client/src/pages/image-editor.tsx index 8bccf62..5698831 100644 --- a/client/src/pages/image-editor.tsx +++ b/client/src/pages/image-editor.tsx @@ -93,129 +93,40 @@ export default function ImageEditor() { const [ribbonPublicId, setRibbonPublicId] = useState(null); const [isGeneratingRibbon, setIsGeneratingRibbon] = useState(false); - // Save edited image mutation + // Save edited image mutation - Server-side processing with Sharp const saveImageMutation = useMutation({ mutationFn: async () => { if (!selectedMedia) { throw new Error("Aucune image sΓ©lectionnΓ©e"); } - // IMPORTANT: Always load the ORIGINAL image to preserve exact dimensions - // Do NOT use previewUrl as it may have filters that change dimensions - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.src = selectedMedia.originalUrl; - - await new Promise((resolve, reject) => { - img.onload = resolve; - img.onerror = reject; - }); + // Send overlay configuration to server - let server handle image processing + const overlayConfig = { + mediaId: selectedMedia.id, + imageUrl: previewUrl || selectedMedia.originalUrl, + ribbon: ribbon.enabled ? { + text: ribbon.text, + color: ribbon.color, + position: ribbon.position + } : null, + priceBadge: priceBadge.enabled ? { + price: priceBadge.price, + color: priceBadge.color, + position: priceBadge.position, + size: priceBadge.size + } : null + }; - const width = img.naturalWidth; - const height = img.naturalHeight; - - // Create canvas at exact image size - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d')!; - - // Draw the base image - ctx.drawImage(img, 0, 0, width, height); - - // Draw ribbon if enabled - if (ribbon.enabled && ribbon.text) { - const ribbonSize = 150; - const positions: Record = { - top_left: { x: 0, y: 0, rotation: 0 }, - top_right: { x: width - ribbonSize, y: 0, rotation: 90 }, - bottom_left: { x: 0, y: height - ribbonSize, rotation: -90 }, - bottom_right: { x: width - ribbonSize, y: height - ribbonSize, rotation: 180 } - }; - - const pos = positions[ribbon.position] || positions.top_left; - - ctx.save(); - ctx.translate(pos.x + ribbonSize / 2, pos.y + ribbonSize / 2); - ctx.rotate((pos.rotation * Math.PI) / 180); - - // Draw triangle - ctx.beginPath(); - ctx.moveTo(-ribbonSize / 2, -ribbonSize / 2); - ctx.lineTo(ribbonSize / 2, ribbonSize / 2); - ctx.lineTo(-ribbonSize / 2, ribbonSize / 2); - ctx.closePath(); - ctx.fillStyle = ribbon.color === 'red' ? '#dc2626' : '#eab308'; - ctx.fill(); - - // Draw text - ctx.rotate(-45 * Math.PI / 180); - ctx.fillStyle = 'white'; - ctx.font = `bold ${ribbon.text.length <= 5 ? 22 : ribbon.text.length <= 8 ? 18 : ribbon.text.length <= 11 ? 15 : 12}px sans-serif`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(ribbon.text, 0, 0); - - ctx.restore(); - } - - // Draw price badge if enabled - if (priceBadge.enabled && priceBadge.price) { - const padding = 16; - const badgeText = `€${priceBadge.price}`; - ctx.font = `bold ${priceBadge.size}px sans-serif`; - const textWidth = ctx.measureText(badgeText).width; - const badgeWidth = textWidth + padding * 2; - const badgeHeight = priceBadge.size + padding; - - 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; - } - - // Draw rounded rectangle background - const radius = badgeHeight / 2; - ctx.fillStyle = priceBadge.color === 'red' ? '#dc2626' : '#eab308'; - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + badgeWidth - radius, y); - ctx.arc(x + badgeWidth - radius, y + radius, radius, -Math.PI / 2, Math.PI / 2); - ctx.lineTo(x + radius, y + badgeHeight); - ctx.arc(x + radius, y + radius, radius, Math.PI / 2, -Math.PI / 2); - ctx.closePath(); - ctx.fill(); - - // Draw text - ctx.fillStyle = 'white'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(badgeText, x + badgeWidth / 2, y + badgeHeight / 2); - } - - // Convert canvas to blob - const blob = await new Promise((resolve) => { - canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.95); - }); - - // Upload to server - const formData = new FormData(); - formData.append('file', blob, `edited_${Date.now()}.jpg`); - - const response = await fetch('/api/media/upload-edited', { + const response = await fetch('/api/media/apply-overlays', { method: 'POST', - body: formData, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(overlayConfig), credentials: 'include' }); if (!response.ok) { - throw new Error("Erreur lors de l'upload"); + const error = await response.json(); + throw new Error(error.error || "Erreur lors de l'application des overlays"); } return response.json(); diff --git a/server/routes.ts b/server/routes.ts index a4e7875..3edbeba 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1171,43 +1171,138 @@ export async function registerRoutes(app: Express): Promise { } }); - // 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 = { + 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 = ` + + + + ${ribbon.text} + + + `; + + 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 = ` + + + + ${badgeText} + + + `; + + 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 { 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 }); } });