From b9c61e2f62ed44368dc950fc106426b5b5ceca53 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Sat, 11 Oct 2025 10:25:20 +0000 Subject: [PATCH] Improve image editing by using a new capture method Replace html2canvas with html-to-image for DOM capture in the image editor. 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/RUoRc2A --- client/src/pages/image-editor.tsx | 168 ++++++------------------------ package-lock.json | 7 ++ package.json | 1 + 3 files changed, 42 insertions(+), 134 deletions(-) diff --git a/client/src/pages/image-editor.tsx b/client/src/pages/image-editor.tsx index de3aaa3..919c173 100644 --- a/client/src/pages/image-editor.tsx +++ b/client/src/pages/image-editor.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { apiRequest, queryClient } from "@/lib/queryClient"; -import html2canvas from "html2canvas"; +import { toPng } from 'html-to-image'; import Sidebar from "@/components/sidebar"; import TopBar from "@/components/topbar"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -93,165 +93,65 @@ export default function ImageEditor() { const [ribbonPublicId, setRibbonPublicId] = useState(null); const [isGeneratingRibbon, setIsGeneratingRibbon] = useState(false); - // Save edited image mutation - Client-side capture matching preview + // Save edited image mutation - DOM capture with html-to-image const saveImageMutation = useMutation({ mutationFn: async () => { if (!selectedMedia || !previewRef.current) { throw new Error("Aucune image sélectionnée"); } - // Load original image to get real dimensions - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.src = selectedMedia.originalUrl; + // Get the preview image element to capture its dimensions + const previewImg = previewRef.current.querySelector('img'); + if (!previewImg) { + throw new Error("Image de preview non trouvée"); + } + + // Load original image to get natural dimensions + const originalImg = new Image(); + originalImg.crossOrigin = 'anonymous'; + originalImg.src = selectedMedia.originalUrl; await new Promise((resolve, reject) => { - img.onload = resolve; - img.onerror = reject; + originalImg.onload = resolve; + originalImg.onerror = reject; }); - const width = img.naturalWidth; - const height = img.naturalHeight; + const naturalWidth = originalImg.naturalWidth; + const naturalHeight = originalImg.naturalHeight; + const displayWidth = previewImg.clientWidth; + const displayHeight = previewImg.clientHeight; - // Create canvas at exact image size - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d')!; + // Calculate scale factor + const scale = naturalWidth / displayWidth; - // Draw base image - ctx.drawImage(img, 0, 0, width, height); - - // Draw ribbon if enabled (matching CSS preview) - if (ribbon.enabled && ribbon.text) { - const ribbonSize = 150; - - // Calculate position based on corner - let x = 0; - let y = 0; - if (ribbon.position === 'north_east') { - x = width - ribbonSize; - } else if (ribbon.position === 'south_west') { - y = height - ribbonSize; - } else if (ribbon.position === 'south_east') { - x = width - ribbonSize; - y = height - ribbonSize; - } - - // Draw triangle - ctx.save(); - ctx.beginPath(); - if (ribbon.position === 'north_west') { - ctx.moveTo(x, y); - ctx.lineTo(x + ribbonSize, y); - ctx.lineTo(x, y + ribbonSize); - } else if (ribbon.position === 'north_east') { - ctx.moveTo(x, y); - ctx.lineTo(x + ribbonSize, y); - ctx.lineTo(x + ribbonSize, y + ribbonSize); - } else if (ribbon.position === 'south_west') { - ctx.moveTo(x, y); - ctx.lineTo(x, y + ribbonSize); - ctx.lineTo(x + ribbonSize, y + ribbonSize); - } else { // south_east - ctx.moveTo(x + ribbonSize, y); - ctx.lineTo(x, y + ribbonSize); - ctx.lineTo(x + ribbonSize, y + ribbonSize); - } - ctx.closePath(); - ctx.fillStyle = ribbon.color === 'red' ? '#dc2626' : '#eab308'; - ctx.fill(); - - // Draw text (rotated to match CSS) - const fontSize = ribbon.text.length <= 5 ? 22 : ribbon.text.length <= 8 ? 18 : ribbon.text.length <= 11 ? 15 : 12; - ctx.font = `bold ${fontSize}px sans-serif`; - ctx.fillStyle = 'white'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - const centerX = x + ribbonSize / 2; - const centerY = y + ribbonSize / 2; - const textOffset = ribbonSize * 0.2; - - ctx.save(); - ctx.translate(centerX, centerY); - if (ribbon.position === 'north_west') { - ctx.rotate(-45 * Math.PI / 180); - ctx.fillText(ribbon.text, -textOffset, 0); - } else if (ribbon.position === 'north_east') { - ctx.rotate(45 * Math.PI / 180); - ctx.fillText(ribbon.text, textOffset, 0); - } else if (ribbon.position === 'south_west') { - ctx.rotate(-135 * Math.PI / 180); - ctx.fillText(ribbon.text, -textOffset, 0); - } else { // south_east - ctx.rotate(135 * Math.PI / 180); - ctx.fillText(ribbon.text, textOffset, 0); - } - ctx.restore(); - ctx.restore(); - } - - // Draw price badge if enabled (matching CSS preview) - 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 - 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 to blob - const blob = await new Promise((resolve) => { - canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.95); + // Capture preview as data URL using html-to-image at high resolution + const dataUrl = await toPng(previewRef.current, { + quality: 1, + pixelRatio: scale, // Render at natural image resolution + canvasWidth: naturalWidth, + canvasHeight: naturalHeight, + skipAutoScale: true }); + // Convert data URL to blob + const response = await fetch(dataUrl); + const blob = await response.blob(); + // Upload const formData = new FormData(); formData.append('file', blob, `edited_${Date.now()}.jpg`); - const response = await fetch('/api/media/upload', { + const uploadResponse = await fetch('/api/media/upload', { method: 'POST', body: formData, credentials: 'include' }); - if (!response.ok) { + if (!uploadResponse.ok) { throw new Error("Erreur lors de l'upload"); } - return response.json(); + return uploadResponse.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/media"] }); diff --git a/package-lock.json b/package-lock.json index 4e4a286..45fd703 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "express": "^4.21.2", "express-session": "^1.18.1", "framer-motion": "^11.13.1", + "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", "input-otp": "^1.4.2", "lucide-react": "^0.453.0", @@ -5962,6 +5963,12 @@ "node": ">= 0.4" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", diff --git a/package.json b/package.json index 74e34e6..e688d05 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "express": "^4.21.2", "express-session": "^1.18.1", "framer-motion": "^11.13.1", + "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", "input-otp": "^1.4.2", "lucide-react": "^0.453.0",