"use client"; import React, { useRef, useState, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; import Link from "next/link"; import { useTheme } from "next-themes"; import { X, Pencil, Circle, ArrowRight, Type, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand, Move, Maximize2, ZoomIn, ZoomOut, Loader2 } from "lucide-react"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { toast } from "sonner"; type Tool = "pen" | "arrow" | "circle" | "text"; type DrawingAction = { type: Tool; color: string; lineWidth: number; points?: { x: number; y: number }[]; startX?: number; startY?: number; endX?: number; endY?: number; radius?: number; text?: string; fontSize?: number; }; interface ImageAnnotatorProps { imageUrl: string; onConfirm: (annotatedImageUrl: string) => void; onCancel: () => void; initialResizeMode?: boolean; // 是否直接进入尺寸调整模式 } const COLORS = [ "#FF0000", // 红色 "#FF6B00", // 橙色 "#FFCC00", // 黄色 "#00CC00", // 绿色 "#0099FF", // 蓝色 "#9933FF", // 紫色 "#FF0099", // 粉色 "#FFFFFF", // 白色 "#000000", // 黑色 ]; const LINE_WIDTHS = [2, 4, 6, 8]; export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMode = false }: ImageAnnotatorProps) { const canvasRef = useRef(null); const containerRef = useRef(null); const [ctx, setCtx] = useState(null); const [image, setImage] = useState(null); const [isDrawing, setIsDrawing] = useState(false); const [tool, setTool] = useState("pen"); const [color, setColor] = useState("#FF0000"); const [lineWidth, setLineWidth] = useState(4); const [actions, setActions] = useState([]); const [currentAction, setCurrentAction] = useState(null); const [textInput, setTextInput] = useState(""); const [textPosition, setTextPosition] = useState<{ x: number; y: number } | null>(null); const [scale, setScale] = useState(1); const [offset, setOffset] = useState({ x: 0, y: 0 }); const [isLoading, setIsLoading] = useState(true); const [isRemovingBg, setIsRemovingBg] = useState(false); const [isEnhancing, setIsEnhancing] = useState(false); const [isExtending, setIsExtending] = useState(false); const [originalImageUrl, setOriginalImageUrl] = useState(imageUrl); const [processedImageUrl, setProcessedImageUrl] = useState(null); const [processedImageBase64, setProcessedImageBase64] = useState(null); const [processType, setProcessType] = useState<"rmbg" | "enhance" | "extend" | "resize" | null>(null); const [mounted, setMounted] = useState(false); // 尺寸调整相关状态 const [isResizeMode, setIsResizeMode] = useState(false); const [resizeCanvasWidth, setResizeCanvasWidth] = useState(1920); const [resizeCanvasHeight, setResizeCanvasHeight] = useState(1080); const [resizeBackgroundColor, setResizeBackgroundColor] = useState("#FFFFFF"); const [resizeBackgroundTransparent, setResizeBackgroundTransparent] = useState(false); const [resizeImageScale, setResizeImageScale] = useState(1); const [resizeImagePosition, setResizeImagePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); const [isDraggingResize, setIsDraggingResize] = useState(false); const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null); const [selectedAspectRatio, setSelectedAspectRatio] = useState(null); // 选中的比例,null表示自定义 const [isResizing, setIsResizing] = useState(false); // 尺寸调整处理中状态 const resizeCanvasRef = useRef(null); const resizeContainerRef = useRef(null); // 网站设置相关状态 const [siteName, setSiteName] = useState("AI Chatbot"); const [siteLogo, setSiteLogo] = useState(null); const [siteLogoDark, setSiteLogoDark] = useState(null); const { resolvedTheme } = useTheme(); // 比例选项定义 const aspectRatios = [ { label: "智能", value: "smart", ratio: null }, // 智能:根据原图比例 { label: "21:9", value: "21:9", ratio: 21 / 9 }, { label: "16:9", value: "16:9", ratio: 16 / 9 }, { label: "3:2", value: "3:2", ratio: 3 / 2 }, { label: "4:3", value: "4:3", ratio: 4 / 3 }, { label: "1:1", value: "1:1", ratio: 1 / 1 }, { label: "3:4", value: "3:4", ratio: 3 / 4 }, { label: "2:3", value: "2:3", ratio: 2 / 3 }, { label: "9:16", value: "9:16", ratio: 9 / 16 }, ]; // 根据比例和宽度计算高度 const calculateHeightFromWidth = (width: number, ratio: number | null) => { if (ratio === null) { // 智能模式:使用原图比例 if (image) { return Math.round(width * (image.height / image.width)); } return resizeCanvasHeight; // 如果没有原图,保持当前高度 } return Math.round(width / ratio); }; // 处理宽度变化 const handleWidthChange = (newWidth: number) => { setResizeCanvasWidth(newWidth); if (selectedAspectRatio) { const ratioConfig = aspectRatios.find((r) => r.value === selectedAspectRatio); if (ratioConfig) { const newHeight = calculateHeightFromWidth(newWidth, ratioConfig.ratio); setResizeCanvasHeight(newHeight); } } }; // 处理高度变化(如果用户手动修改高度,取消比例锁定) const handleHeightChange = (newHeight: number) => { setResizeCanvasHeight(newHeight); // 如果用户手动修改高度,取消比例锁定 if (selectedAspectRatio) { setSelectedAspectRatio(null); } }; // 处理比例选择 const handleAspectRatioSelect = (value: string | null) => { setSelectedAspectRatio(value); if (value) { const ratioConfig = aspectRatios.find((r) => r.value === value); if (ratioConfig) { const newHeight = calculateHeightFromWidth(resizeCanvasWidth, ratioConfig.ratio); setResizeCanvasHeight(newHeight); } } }; // 确保在客户端渲染 useEffect(() => { setMounted(true); // 获取网站设置 fetch(`/api/site-settings?t=${Date.now()}`) .then((res) => res.json()) .then((data) => { if (data.siteName) setSiteName(data.siteName); if (data.siteLogo) setSiteLogo(data.siteLogo); if (data.siteLogoDark) setSiteLogoDark(data.siteLogoDark); }) .catch((err) => { console.error("[ImageAnnotator] Failed to fetch site settings:", err); }); }, []); // 加载图片 useEffect(() => { setIsLoading(true); setOriginalImageUrl(imageUrl); setProcessedImageUrl(null); setProcessedImageBase64(null); setProcessType(null); const img = new window.Image(); img.crossOrigin = "anonymous"; img.onload = () => { setImage(img); setIsLoading(false); // 如果指定了初始进入尺寸调整模式,自动进入 if (initialResizeMode && img) { setIsResizeMode(true); setResizeCanvasWidth(img.width); setResizeCanvasHeight(img.height); setSelectedAspectRatio(null); } }; img.onerror = () => { setIsLoading(false); }; img.src = imageUrl; }, [imageUrl, initialResizeMode]); // 初始化 canvas - 适配弹窗大小,固定编辑器大小,图片缩小显示 useEffect(() => { if (!canvasRef.current || !image || !containerRef.current) return; const canvas = canvasRef.current; const container = containerRef.current; const context = canvas.getContext("2d"); if (!context) return; // 固定弹窗内画布区域大小(减去工具栏和边距,考虑并排显示) // 弹窗固定为 1200px x 550px,画布区域约为 1100px x 350px(单图)或 550px x 350px(双图) const containerWidth = processedImageBase64 ? 550 : 1100; // 如果有处理后图片,每个区域550px;否则1100px const containerHeight = 350; // 550 - 工具栏高度(约200px) const scaleX = containerWidth / image.width; const scaleY = containerHeight / image.height; const newScale = Math.min(scaleX, scaleY, 1); // 不放大,只缩小以适应固定容器 setScale(newScale); canvas.width = image.width * newScale; canvas.height = image.height * newScale; // 计算居中偏移 const offsetX = (containerWidth - canvas.width) / 2; const offsetY = (containerHeight - canvas.height) / 2; setOffset({ x: Math.max(0, offsetX), y: Math.max(0, offsetY) }); setCtx(context); redrawCanvas(context, image, newScale, []); }, [image]); // 重绘画布 const redrawCanvas = useCallback( (context: CanvasRenderingContext2D, img: HTMLImageElement, currentScale: number, drawActions: DrawingAction[]) => { const canvas = context.canvas; context.clearRect(0, 0, canvas.width, canvas.height); // 绘制图片 context.drawImage(img, 0, 0, canvas.width, canvas.height); // 绘制所有动作 for (const action of drawActions) { drawAction(context, action, currentScale); } }, [] ); // 当 actions 变化时重绘 useEffect(() => { if (ctx && image) { redrawCanvas(ctx, image, scale, actions); } }, [actions, ctx, image, scale, redrawCanvas]); // 绘制单个动作 const drawAction = (context: CanvasRenderingContext2D, action: DrawingAction, currentScale: number) => { context.strokeStyle = action.color; context.fillStyle = action.color; context.lineWidth = action.lineWidth; context.lineCap = "round"; context.lineJoin = "round"; switch (action.type) { case "pen": if (action.points && action.points.length > 0) { context.beginPath(); context.moveTo(action.points[0].x * currentScale, action.points[0].y * currentScale); for (let i = 1; i < action.points.length; i++) { context.lineTo(action.points[i].x * currentScale, action.points[i].y * currentScale); } context.stroke(); } break; case "arrow": if (action.startX !== undefined && action.startY !== undefined && action.endX !== undefined && action.endY !== undefined) { const sx = action.startX * currentScale; const sy = action.startY * currentScale; const ex = action.endX * currentScale; const ey = action.endY * currentScale; // 绘制线条 context.beginPath(); context.moveTo(sx, sy); context.lineTo(ex, ey); context.stroke(); // 绘制箭头 const angle = Math.atan2(ey - sy, ex - sx); const arrowLength = 15 + action.lineWidth * 2; context.beginPath(); context.moveTo(ex, ey); context.lineTo(ex - arrowLength * Math.cos(angle - Math.PI / 6), ey - arrowLength * Math.sin(angle - Math.PI / 6)); context.lineTo(ex - arrowLength * Math.cos(angle + Math.PI / 6), ey - arrowLength * Math.sin(angle + Math.PI / 6)); context.closePath(); context.fill(); } break; case "circle": if (action.startX !== undefined && action.startY !== undefined && action.radius !== undefined) { context.beginPath(); context.arc(action.startX * currentScale, action.startY * currentScale, action.radius * currentScale, 0, 2 * Math.PI); context.stroke(); } break; case "text": if (action.startX !== undefined && action.startY !== undefined && action.text) { const fontSize = (action.fontSize || 20) * currentScale; context.font = `bold ${fontSize}px sans-serif`; context.fillText(action.text, action.startX * currentScale, action.startY * currentScale); } break; } }; // 获取鼠标/触摸位置(转换为原始图片坐标) const getPosition = (e: React.MouseEvent | React.TouchEvent) => { const canvas = canvasRef.current; if (!canvas) return { x: 0, y: 0 }; const rect = canvas.getBoundingClientRect(); let clientX: number; let clientY: number; if ("touches" in e) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } // 转换为原始图片坐标 return { x: (clientX - rect.left) / scale, y: (clientY - rect.top) / scale, }; }; // 开始绘制 const handleStart = (e: React.MouseEvent | React.TouchEvent) => { if (tool === "text") { const pos = getPosition(e); setTextPosition(pos); return; } setIsDrawing(true); const pos = getPosition(e); const newAction: DrawingAction = { type: tool, color, lineWidth, }; if (tool === "pen") { newAction.points = [pos]; } else { newAction.startX = pos.x; newAction.startY = pos.y; newAction.endX = pos.x; newAction.endY = pos.y; if (tool === "circle") { newAction.radius = 0; } } setCurrentAction(newAction); }; // 绘制中 const handleMove = (e: React.MouseEvent | React.TouchEvent) => { if (!isDrawing || !currentAction || !ctx || !image) return; const pos = getPosition(e); const updatedAction = { ...currentAction }; if (tool === "pen" && updatedAction.points) { updatedAction.points = [...updatedAction.points, pos]; } else if (tool === "arrow") { updatedAction.endX = pos.x; updatedAction.endY = pos.y; } else if (tool === "circle" && updatedAction.startX !== undefined && updatedAction.startY !== undefined) { const dx = pos.x - updatedAction.startX; const dy = pos.y - updatedAction.startY; updatedAction.radius = Math.sqrt(dx * dx + dy * dy); } setCurrentAction(updatedAction); // 实时重绘 redrawCanvas(ctx, image, scale, [...actions, updatedAction]); }; // 结束绘制 const handleEnd = () => { if (!isDrawing || !currentAction) return; setIsDrawing(false); setActions((prev) => [...prev, currentAction]); setCurrentAction(null); }; // 添加文字 const handleAddText = () => { if (!textInput.trim() || !textPosition) return; const textAction: DrawingAction = { type: "text", color, lineWidth, startX: textPosition.x, startY: textPosition.y, text: textInput, fontSize: 20 + lineWidth * 2, }; setActions((prev) => [...prev, textAction]); setTextInput(""); setTextPosition(null); }; // 撤销 const handleUndo = () => { setActions((prev) => prev.slice(0, -1)); }; // 清空 const handleClear = () => { setActions([]); }; // 将图片URL转换为Base64(获取原图数据) const imageUrlToBase64 = async (url: string): Promise => { // 如果已经是data URL,直接返回 if (url.startsWith("data:")) { return url.split(",")[1]; } // 如果是HTTP URL,需要先fetch获取 const response = await fetch(url); if (!response.ok) { throw new Error(`获取原图失败: ${response.status} ${response.statusText}`); } const blob = await response.blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const base64String = (reader.result as string).split(",")[1]; resolve(base64String); }; reader.onerror = reject; reader.readAsDataURL(blob); }); }; // 抠图功能 const handleRemoveBackground = async () => { if (!image) return; setIsRemovingBg(true); try { // 使用原图URL获取原图数据,而不是canvas的预览图 console.log("[抠图] 使用原图URL获取数据:", originalImageUrl); const base64Data = await imageUrlToBase64(originalImageUrl); console.log("[抠图] 原图Base64长度:", base64Data.length, "字符"); // 调用抠图API const response = await fetch("/api/rmbg", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ image_base64: base64Data, prompt: "画图标注-抠图处理", // 可选:添加提示词 }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "抠图失败"); } const result = await response.json(); if (result.success && result.image_base64) { // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) console.log("[抠图] API返回结果:", { hasOriginalUrl: !!result.original_url, hasThumbnailUrl: !!result.thumbnail_url, originalUrl: result.original_url?.substring(0, 100), thumbnailUrl: result.thumbnail_url?.substring(0, 100) }); if (result.original_url) { console.log("[抠图] ✅ 使用原图URL:", result.original_url); setProcessedImageUrl(result.original_url); } else if (result.thumbnail_url) { console.warn("[抠图] ⚠️ 未找到original_url,使用thumbnail_url作为备选:", result.thumbnail_url); setProcessedImageUrl(result.thumbnail_url); } else { console.warn("[抠图] ⚠️ 未找到图片URL"); setProcessedImageUrl(null); } setProcessedImageBase64(result.image_base64); setProcessType("rmbg"); toast.success(`抠图完成!图片已保存到相册。处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`); } else { throw new Error(result.error || result.detail || "抠图处理失败"); } } catch (error) { console.error("[抠图] 错误:", error); toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试"); } finally { setIsRemovingBg(false); } }; // 变清晰处理 const handleEnhance = async () => { if (!image) return; setIsEnhancing(true); try { // 使用原图URL获取原图数据,而不是canvas的预览图 console.log("[变清晰] 使用原图URL获取数据:", originalImageUrl); const base64Data = await imageUrlToBase64(originalImageUrl); console.log("[变清晰] 原图Base64长度:", base64Data.length, "字符"); // 调用变清晰API const response = await fetch("/api/enhance", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ image_base64: base64Data, prompt: "画图标注-变清晰处理", // 可选:添加提示词 }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "变清晰处理失败"); } const result = await response.json(); if (result.success && result.image_base64) { // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是处理后的原图) console.log("[变清晰] API返回结果:", { hasOriginalUrl: !!result.original_url, hasThumbnailUrl: !!result.thumbnail_url, originalUrl: result.original_url, thumbnailUrl: result.thumbnail_url, base64Length: result.image_base64?.length || 0 }); // ✅ 验证:检查返回的URL是否是处理后的图片 if (result.original_url) { console.log("[变清晰] ✅ 使用处理后的原图URL:", result.original_url); console.log("[变清晰] ✅ URL应该包含 'enhance-orig' 标识,确认是处理后的图片"); // 验证URL格式(应该包含enhance-orig) if (!result.original_url.includes("enhance-orig") && !result.original_url.includes("enhance-orig")) { console.warn("[变清晰] ⚠️ 警告:URL不包含 'enhance-orig' 标识,可能不是处理后的图片!"); } setProcessedImageUrl(result.original_url); } else if (result.thumbnail_url) { console.warn("[变清晰] ⚠️ 未找到original_url,使用thumbnail_url作为备选:", result.thumbnail_url); setProcessedImageUrl(result.thumbnail_url); } else { console.warn("[变清晰] ⚠️ 未找到图片URL"); setProcessedImageUrl(null); } setProcessedImageBase64(result.image_base64); setProcessType("enhance"); toast.success(`变清晰完成!图片已保存到相册。处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`); } else { throw new Error(result.error || result.detail || "变清晰处理失败"); } } catch (error) { console.error("[变清晰] 错误:", error); toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试"); } finally { setIsEnhancing(false); } }; // 尺寸调整功能 const handleResize = () => { if (!image) return; setIsResizeMode(true); // 初始化尺寸为原图尺寸 setResizeCanvasWidth(image.width); setResizeCanvasHeight(image.height); setSelectedAspectRatio(null); // 重置比例选择 // 初始化图片位置为画布中心 setResizeImagePosition({ x: 0, y: 0 }); setResizeImageScale(1); }; // 尺寸调整:处理图片拖拽 const handleResizeImageDragStart = (e: React.MouseEvent | React.TouchEvent) => { if (!isResizeMode || !resizeCanvasRef.current) return; e.stopPropagation(); setIsDraggingResize(true); const canvas = resizeCanvasRef.current; const rect = canvas.getBoundingClientRect(); const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; const clientY = "touches" in e ? e.touches[0].clientY : e.clientY; // 计算相对于画布的坐标 const canvasX = ((clientX - rect.left) / rect.width) * canvas.width; const canvasY = ((clientY - rect.top) / rect.height) * canvas.height; // 计算画布中心到图片中心的偏移(考虑显示缩放) const containerWidth = resizeContainerRef.current?.clientWidth || canvas.width; const containerHeight = resizeContainerRef.current?.clientHeight || canvas.height; const canvasAspect = resizeCanvasWidth / resizeCanvasHeight; const containerAspect = containerWidth / containerHeight; const displayScale = canvasAspect > containerAspect ? (containerWidth / resizeCanvasWidth) : (containerHeight / resizeCanvasHeight); // 保存拖拽起始点(相对于画布中心) const centerX = canvas.width / 2; const centerY = canvas.height / 2; const offsetX = (canvasX - centerX) / displayScale - resizeImagePosition.x; const offsetY = (canvasY - centerY) / displayScale - resizeImagePosition.y; setDragStart({ x: offsetX, y: offsetY }); }; const handleResizeImageDrag = (e: React.MouseEvent | React.TouchEvent) => { if (!isDraggingResize || !dragStart || !resizeCanvasRef.current) return; e.preventDefault(); e.stopPropagation(); const canvas = resizeCanvasRef.current; const rect = canvas.getBoundingClientRect(); const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; const clientY = "touches" in e ? e.touches[0].clientY : e.clientY; // 计算相对于画布的坐标 const canvasX = ((clientX - rect.left) / rect.width) * canvas.width; const canvasY = ((clientY - rect.top) / rect.height) * canvas.height; // 计算显示缩放 const containerWidth = resizeContainerRef.current?.clientWidth || canvas.width; const containerHeight = resizeContainerRef.current?.clientHeight || canvas.height; const canvasAspect = resizeCanvasWidth / resizeCanvasHeight; const containerAspect = containerWidth / containerHeight; const displayScale = canvasAspect > containerAspect ? (containerWidth / resizeCanvasWidth) : (containerHeight / resizeCanvasHeight); // 计算新的图片位置(相对于画布中心) const centerX = canvas.width / 2; const centerY = canvas.height / 2; const newX = ((canvasX - centerX) / displayScale) - dragStart.x; const newY = ((canvasY - centerY) / displayScale) - dragStart.y; setResizeImagePosition({ x: newX, y: newY }); }; const handleResizeImageDragEnd = () => { setIsDraggingResize(false); setDragStart(null); }; // 尺寸调整:放大图片 const handleResizeZoomIn = () => { if (!isResizeMode) return; setResizeImageScale((prev) => Math.min(prev * 1.2, 5)); // 最大5倍 }; // 尺寸调整:缩小图片 const handleResizeZoomOut = () => { if (!isResizeMode) return; setResizeImageScale((prev) => Math.max(prev / 1.2, 0.1)); // 最小0.1倍 }; // 尺寸调整:处理图片缩放(鼠标滚轮) const handleResizeImageWheel = (e: React.WheelEvent) => { if (!isResizeMode || !image) return; e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; const newScale = Math.max(0.1, Math.min(5, resizeImageScale + delta)); setResizeImageScale(newScale); }; // 尺寸调整:渲染预览 useEffect(() => { if (!isResizeMode || !resizeCanvasRef.current || !image || !resizeContainerRef.current) return; const canvas = resizeCanvasRef.current; const container = resizeContainerRef.current; const ctx = canvas.getContext("2d"); if (!ctx) return; // 计算画布显示尺寸(适应容器) const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; const canvasAspect = resizeCanvasWidth / resizeCanvasHeight; const containerAspect = containerWidth / containerHeight; let displayWidth: number; let displayHeight: number; let displayScale: number; if (canvasAspect > containerAspect) { displayWidth = containerWidth; displayHeight = containerWidth / canvasAspect; displayScale = displayWidth / resizeCanvasWidth; } else { displayHeight = containerHeight; displayWidth = containerHeight * canvasAspect; displayScale = displayHeight / resizeCanvasHeight; } canvas.width = displayWidth; canvas.height = displayHeight; // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制背景(如果设置了背景颜色且不是透明) if (!resizeBackgroundTransparent) { ctx.fillStyle = resizeBackgroundColor; ctx.fillRect(0, 0, canvas.width, canvas.height); } // 绘制图片(考虑缩放和位置) const imageDisplayWidth = image.width * resizeImageScale * displayScale; const imageDisplayHeight = image.height * resizeImageScale * displayScale; const imageX = (canvas.width / 2) + (resizeImagePosition.x * displayScale); const imageY = (canvas.height / 2) + (resizeImagePosition.y * displayScale); // 绘制图片(不设置透明度,保持原图清晰度) ctx.drawImage( image, imageX - imageDisplayWidth / 2, imageY - imageDisplayHeight / 2, imageDisplayWidth, imageDisplayHeight ); }, [isResizeMode, resizeCanvasWidth, resizeCanvasHeight, resizeBackgroundColor, resizeBackgroundTransparent, resizeImageScale, resizeImagePosition, image]); // 尺寸调整:导出为指定尺寸的图片并上传到存储桶 const handleResizeConfirm = async () => { if (!image || !isResizeMode || isResizing) return; setIsResizing(true); try { // 创建指定尺寸的画布 const exportCanvas = document.createElement("canvas"); exportCanvas.width = resizeCanvasWidth; exportCanvas.height = resizeCanvasHeight; const exportCtx = exportCanvas.getContext("2d"); if (!exportCtx) return; // 绘制背景 if (!resizeBackgroundTransparent) { exportCtx.fillStyle = resizeBackgroundColor; exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); } // 计算图片在画布上的实际尺寸和位置 const imageWidth = image.width * resizeImageScale; const imageHeight = image.height * resizeImageScale; const imageX = (exportCanvas.width / 2) + resizeImagePosition.x; const imageY = (exportCanvas.height / 2) + resizeImagePosition.y; // 绘制图片 exportCtx.drawImage( image, imageX - imageWidth / 2, imageY - imageHeight / 2, imageWidth, imageHeight ); // 导出为图片 const mimeType = resizeBackgroundTransparent ? "image/png" : "image/jpeg"; const dataUrl = exportCanvas.toDataURL(mimeType, 0.95); const base64Data = dataUrl.split(",")[1]; // 调用API上传到存储桶并保存到数据库 const response = await fetch("/api/resize", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ image_base64: base64Data, prompt: `尺寸调整处理 - ${resizeCanvasWidth}x${resizeCanvasHeight}`, canvasWidth: resizeCanvasWidth, canvasHeight: resizeCanvasHeight, mimeType: mimeType, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "上传失败"); } const result = await response.json(); if (result.success) { // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) if (result.original_url) { console.log("[尺寸调整] 原图URL:", result.original_url); setProcessedImageUrl(result.original_url); } else { setProcessedImageUrl(null); } setProcessedImageBase64(base64Data); setProcessType("resize"); // 不退出尺寸调整模式,保持原图显示 // setIsResizeMode(false); toast.success(`尺寸调整完成!图片已调整为 ${resizeCanvasWidth}x${resizeCanvasHeight} 像素,已保存到相册`); } else { throw new Error(result.error || "尺寸调整处理失败"); } } catch (error) { console.error("[尺寸调整] 错误:", error); toast.error(error instanceof Error ? error.message : "尺寸调整失败,请稍后重试"); } finally { setIsResizing(false); } }; // 图像拓展处理 const handleExtend = async () => { if (!image) return; setIsExtending(true); try { // 直接使用原图URL,而不是转换为base64 console.log("[图像拓展] 使用原图URL:", originalImageUrl); // 调用图像拓展API(直接发送URL) const response = await fetch("/api/extend", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ image_url: originalImageUrl, // 直接发送图片URL prompt: "画图标注-图像拓展处理", // 可选:添加提示词 }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "图像拓展处理失败"); } const result = await response.json(); if (result.success) { // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) if (result.original_url) { console.log("[图像拓展] 原图URL:", result.original_url); setProcessedImageUrl(result.original_url); } else if (result.thumbnail_url) { console.warn("[图像拓展] ⚠️ 未找到original_url,使用thumbnail_url作为备选"); setProcessedImageUrl(result.thumbnail_url); } else { console.warn("[图像拓展] ⚠️ 未找到图片URL"); setProcessedImageUrl(null); } // 优先使用返回的base64,如果没有则使用thumbnail_url或original_url if (result.base64) { setProcessedImageBase64(result.base64); } setProcessType("extend"); toast.success(`图像拓展完成!图片已保存到相册。处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`); } else { throw new Error(result.error || result.detail || "图像拓展处理失败"); } } catch (error) { console.error("[图像拓展] 错误:", error); toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试"); } finally { setIsExtending(false); } }; // 下载处理后的图片(必须使用原图URL或Base64) const handleDownloadProcessed = async () => { try { // 确定文件类型和MIME类型 const isPng = processType === "rmbg" || processType === "resize"; const fileExtension = isPng ? "png" : "jpg"; const mimeType = isPng ? "image/png" : "image/jpeg"; const fileName = processType === "rmbg" ? `抠图-${Date.now()}.${fileExtension}` : processType === "enhance" ? `变清晰-${Date.now()}.${fileExtension}` : processType === "resize" ? `尺寸调整-${resizeCanvasWidth}x${resizeCanvasHeight}-${Date.now()}.${fileExtension}` : `图像拓展-${Date.now()}.${fileExtension}`; // 如果尺寸调整使用Base64,直接下载(优先使用URL,如果没有URL则使用Base64) if (processType === "resize") { if (processedImageUrl) { // 如果有URL,使用URL下载 const response = await fetch(processedImageUrl); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); toast.success("图片下载成功"); return; } else if (processedImageBase64) { // 如果没有URL,使用Base64下载 const dataUrl = `data:${mimeType};base64,${processedImageBase64}`; const link = document.createElement("a"); link.href = dataUrl; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); toast.success("图片下载成功"); return; } else { toast.error("图片数据不可用,无法下载"); return; } } // 其他情况使用URL下载 if (!processedImageUrl) { toast.error("原图地址不可用,无法下载"); return; } // ✅ 验证:确认下载的是处理后的图片URL console.log("[下载] 📥 开始下载,processType:", processType); console.log("[下载] 📥 processedImageUrl:", processedImageUrl); console.log("[下载] 📥 这个URL应该是处理后的图片,不是原图!"); // 确保使用的是处理后的原图URL(检查URL中是否包含thumb标识) let downloadUrl = processedImageUrl; // 更完善的缩略图URL转换逻辑 if (downloadUrl.includes("-thumb") || downloadUrl.includes("/thumb/") || downloadUrl.includes("thumbnail")) { // 如果URL包含thumb标识,尝试转换为原图URL // 处理各种可能的格式: // - rmbg-thumb-xxx.png -> rmbg-orig-xxx.png // - enhance-thumb-xxx.jpg -> enhance-orig-xxx.jpg // - extend-thumb-xxx.jpg -> extend-orig-xxx.jpg // - xxx/thumb/xxx -> xxx/orig/xxx downloadUrl = downloadUrl .replace(/-thumb-/g, "-orig-") .replace(/-thumb\./g, "-orig.") .replace(/\/thumb\//g, "/orig/") .replace(/thumbnail/g, "original"); console.warn("[下载] ⚠️ 检测到缩略图URL,已转换为原图URL:", downloadUrl); } // 如果转换后的URL仍然包含thumb标识,尝试更激进的转换 if (downloadUrl.includes("thumb")) { try { // 尝试直接替换文件名中的thumb为orig const urlObj = new URL(downloadUrl); const pathname = urlObj.pathname; if (pathname.includes("thumb")) { const newPathname = pathname.replace(/thumb/g, "orig"); downloadUrl = downloadUrl.replace(pathname, newPathname); console.warn("[下载] ⚠️ 二次转换原图URL:", downloadUrl); } } catch (urlError) { // 如果URL解析失败,尝试简单的字符串替换 downloadUrl = downloadUrl.replace(/thumb/g, "orig"); console.warn("[下载] ⚠️ 使用简单替换转换原图URL:", downloadUrl); } } // ✅ 验证:确认下载URL是正确的 console.log("[下载] ✅ 最终下载URL:", downloadUrl); console.log("[下载] ✅ 这个URL应该指向处理后的图片(不是原图)"); // 如果是变清晰,验证URL应该包含enhance相关标识 if (processType === "enhance") { if (!downloadUrl.includes("enhance") && !downloadUrl.includes("1767358346590")) { console.warn("[下载] ⚠️ 警告:变清晰下载URL不包含 'enhance' 标识,可能下载了错误的图片!"); } } // 从原图URL下载(原图已上传到S3) const response = await fetch(downloadUrl); if (!response.ok) { throw new Error(`下载原图失败: ${response.status} ${response.statusText}`); } const blob = await response.blob(); console.log("[下载] 下载的图片大小:", blob.size, "bytes"); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); toast.success("原图下载成功"); } catch (error) { console.error("[下载] 错误:", error); toast.error(`下载失败: ${error instanceof Error ? error.message : "请稍后重试"}`); } }; // 确认 const handleConfirm = () => { // 优先使用处理后的图片(如果有) if (processedImageUrl) { // 如果有处理后的图片URL,直接使用它 onConfirm(processedImageUrl); return; } if (processedImageBase64) { // 如果有处理后的图片Base64,转换为data URL const mimeType = processType === "rmbg" || processType === "resize" ? "image/png" : "image/jpeg"; const dataUrl = `data:${mimeType};base64,${processedImageBase64}`; onConfirm(dataUrl); return; } // 否则,导出标注后的图片 const canvas = canvasRef.current; if (!canvas || !image) return; // 创建一个新的 canvas 用于导出原始尺寸 const exportCanvas = document.createElement("canvas"); exportCanvas.width = image.width; exportCanvas.height = image.height; const exportCtx = exportCanvas.getContext("2d"); if (!exportCtx) return; // 绘制原始图片 exportCtx.drawImage(image, 0, 0); // 绘制所有动作(使用原始尺寸) for (const action of actions) { drawAction(exportCtx, action, 1); } // 导出为 data URL const dataUrl = exportCanvas.toDataURL("image/png"); onConfirm(dataUrl); }; // 决定显示哪个 LOGO(根据主题) const displayLogo = resolvedTheme === "dark" && siteLogoDark ? siteLogoDark : siteLogo; // 弹窗内容 const modalContent = (
{/* 弹窗容器 - 全屏显示 */}
e.stopPropagation()} > {/* 顶部标题栏 */}
e.stopPropagation()} > {displayLogo ? ( <> {siteName} {siteName} ) : ( {siteName} )}
{/* 工具选择栏 / 尺寸调整工具栏 */} {isResizeMode ? ( /* 尺寸调整模式下显示尺寸调整工具栏 */
{/* 比例选择和尺寸输入在同一行 */}
{/* 比例选择 */}
{aspectRatios.map((ratio) => ( ))}
{/* 尺寸输入 */}
handleWidthChange(Number.parseInt(e.target.value) || 1)} className="w-24 h-8" min={1} max={10000} /> px
handleHeightChange(Number.parseInt(e.target.value) || 1)} className="w-24 h-8" min={1} max={10000} /> px
setResizeBackgroundColor(e.target.value)} disabled={resizeBackgroundTransparent} className="w-12 h-8 rounded border cursor-pointer disabled:opacity-50" />
{/* 操作按钮 */}
) : ( /* 非尺寸调整模式下显示图片标注工具栏 */
{/* 工具按钮 */}
{/* 颜色选择 */}
{COLORS.map((c) => (
{/* 线宽选择 */}
{LINE_WIDTHS.map((w) => ( ))}
)} {/* 画布区域 - 并排显示原图和处理后的图片 */}
{isLoading ? ( /* 加载动画 */
正在加载图片...
) : isResizeMode ? ( /* 尺寸调整模式 - 左右对比布局 */ <> {/* 左侧:原图 */}
原图
{image && ( 原图 )}
{/* 右侧:尺寸调整预览画布 */}
调整尺寸 {(processedImageUrl || processedImageBase64) && processType === "resize" && ( )}
画布: {resizeCanvasWidth} x {resizeCanvasHeight} | 缩放: {(resizeImageScale * 100).toFixed(0)}% | 拖拽移动图片,滚轮缩放
) : ( <> {/* 原图画布 */}
原图
{/* 处理后的图片展示区域 */} {processedImageBase64 && (
{processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : "拓展结果"}
{processedImageUrl ? ( {processType ) : processedImageBase64 ? ( {processType ) : null}
)} )}
{/* 文字输入弹窗 */} {textPosition && (
setTextPosition(null)}>
e.stopPropagation()}>
输入标注文字
setTextInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleAddText(); }} className="mb-3 w-64 rounded-md border bg-muted px-3 py-2 text-foreground outline-none focus:border-primary" placeholder="输入文字..." autoFocus />
)}
); // 使用 Portal 渲染到 document.body,避免受父元素定位影响 if (!mounted) { return null; } return createPortal(modalContent, document.body); }