添加尺寸调整功能,统一图片处理按钮样式
- 新增尺寸调整功能:用户可以自定义画布尺寸、背景颜色、图片位置和缩放 - 创建/api/resize API端点:上传处理后的图片到存储桶并保存到数据库 - 统一图片处理按钮样式:将变清晰、抠图、图像拓展按钮底色改为70%透明黑色 - 支持透明背景和彩色背景的尺寸调整 - 图片可拖拽移动和滚轮缩放 - 处理后的图片自动保存到相册和生图记录
This commit is contained in:
parent
f85d1732c4
commit
bbf9768bc9
199
app/api/resize/route.ts
Normal file
199
app/api/resize/route.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
import { db } from "@/lib/db";
|
||||
import { generatedImage } from "@/lib/db/schema";
|
||||
|
||||
/**
|
||||
* 压缩图片生成缩略图(使用 sharp)
|
||||
*/
|
||||
async function createThumbnail(buffer: Buffer, mimeType: string): Promise<Buffer> {
|
||||
try {
|
||||
const sharp = await import("sharp");
|
||||
|
||||
// 创建缩略图(宽度 500px,高度按比例,质量 80%)
|
||||
const thumbnailBuffer = await sharp.default(buffer)
|
||||
.resize(500, null, {
|
||||
fit: "inside",
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.png({ quality: 80 }) // PNG格式保持透明背景
|
||||
.toBuffer();
|
||||
|
||||
console.log(`[Resize] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`);
|
||||
return thumbnailBuffer;
|
||||
} catch (error) {
|
||||
console.error("[Resize] Failed to create thumbnail:", error);
|
||||
// 如果失败,返回原图
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 base64 图片上传到 S3,返回缩略图和原图的 URL
|
||||
*/
|
||||
async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/png"): Promise<{ thumbnail: string; original: string }> {
|
||||
try {
|
||||
// 解析 base64 数据
|
||||
const buffer = Buffer.from(base64Data, "base64");
|
||||
|
||||
// 生成文件名
|
||||
const ext = mimeType.split("/")[1] || "png";
|
||||
const timestamp = Date.now();
|
||||
const randomStr = Math.random().toString(36).slice(2, 8);
|
||||
const thumbnailFilename = `resize-thumb-${timestamp}-${randomStr}.${ext}`;
|
||||
const originalFilename = `resize-orig-${timestamp}-${randomStr}.${ext}`;
|
||||
|
||||
console.log(`[Resize] Original size: ${buffer.length} bytes`);
|
||||
|
||||
// 生成缩略图
|
||||
const thumbnailBuffer = await createThumbnail(buffer, mimeType);
|
||||
|
||||
// 转换为 ArrayBuffer
|
||||
const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice(
|
||||
thumbnailBuffer.byteOffset,
|
||||
thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength
|
||||
) as ArrayBuffer;
|
||||
const originalArrayBuffer = buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.byteLength
|
||||
) as ArrayBuffer;
|
||||
|
||||
// 并行上传缩略图和原图到 S3
|
||||
console.log(`[Resize] Uploading thumbnail and original to S3...`);
|
||||
const [thumbnailResult, originalResult] = await Promise.all([
|
||||
uploadFile(thumbnailFilename, thumbnailArrayBuffer),
|
||||
uploadFile(originalFilename, originalArrayBuffer),
|
||||
]);
|
||||
|
||||
console.log(`[Resize] Uploaded to S3:`);
|
||||
console.log(` - Thumbnail: ${thumbnailResult.url}`);
|
||||
console.log(` - Original: ${originalResult.url}`);
|
||||
|
||||
return {
|
||||
thumbnail: thumbnailResult.url,
|
||||
original: originalResult.url,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[Resize] Failed to upload to S3:", error);
|
||||
throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尺寸调整API路由
|
||||
* 接收前端处理后的Base64图片,上传到S3并保存到数据库
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// 0. 获取用户会话
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: "未授权,请先登录" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = session.user.id;
|
||||
|
||||
// 1. 获取请求体
|
||||
const contentType = request.headers.get("content-type") || "";
|
||||
|
||||
let imageBase64: string;
|
||||
let prompt = "尺寸调整处理";
|
||||
let chatId: string | null = null;
|
||||
let messageId: string | null = null;
|
||||
let canvasWidth: number;
|
||||
let canvasHeight: number;
|
||||
let mimeType = "image/png";
|
||||
|
||||
if (contentType.includes("application/json")) {
|
||||
const body = await request.json();
|
||||
imageBase64 = body.image_base64;
|
||||
prompt = body.prompt || "尺寸调整处理";
|
||||
chatId = body.chatId || null;
|
||||
messageId = body.messageId || null;
|
||||
canvasWidth = body.canvasWidth;
|
||||
canvasHeight = body.canvasHeight;
|
||||
mimeType = body.mimeType || "image/png";
|
||||
|
||||
if (!imageBase64) {
|
||||
return NextResponse.json(
|
||||
{ error: "缺少image_base64参数" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!canvasWidth || !canvasHeight) {
|
||||
return NextResponse.json(
|
||||
{ error: "缺少canvasWidth或canvasHeight参数" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: "不支持的Content-Type,请使用application/json" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[Resize API] 图片Base64长度: ${imageBase64.length} 字符`);
|
||||
console.log(`[Resize API] 画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
||||
|
||||
// 2. 上传处理后的图片到S3存储桶
|
||||
let thumbnailUrl: string;
|
||||
let originalUrl: string;
|
||||
|
||||
try {
|
||||
const uploadResult = await uploadBase64ToS3(imageBase64, mimeType);
|
||||
thumbnailUrl = uploadResult.thumbnail;
|
||||
originalUrl = uploadResult.original;
|
||||
console.log(`[Resize] ✅ Images uploaded to S3 successfully`);
|
||||
} catch (uploadError) {
|
||||
console.error("[Resize] ❌ Failed to upload to S3:", uploadError);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "图片上传到存储桶失败",
|
||||
detail: uploadError instanceof Error ? uploadError.message : "未知错误"
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 保存图片记录到数据库(相册和生图记录)
|
||||
try {
|
||||
const [savedImage] = await db.insert(generatedImage).values({
|
||||
userId,
|
||||
chatId: chatId || null,
|
||||
messageId: messageId || null,
|
||||
thumbnailUrl,
|
||||
originalUrl,
|
||||
imageSize: `${canvasWidth}x${canvasHeight}`, // 使用实际尺寸
|
||||
prompt: prompt || "尺寸调整处理",
|
||||
modelId: "resize-1.0", // 标记为尺寸调整处理
|
||||
}).returning();
|
||||
console.log(`[Resize] ✅ 图片已保存到相册和生图记录,ID: ${savedImage.id}`);
|
||||
} catch (dbError) {
|
||||
console.error("[Resize] ❌ 保存到数据库失败:", dbError);
|
||||
// 数据库保存失败时仍然返回成功,但记录错误(图片已上传到S3,用户可以访问)
|
||||
}
|
||||
|
||||
// 4. 返回结果(包含S3 URL和Base64,前端可以选择使用)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
image_base64: imageBase64, // 保留Base64用于前端立即显示
|
||||
thumbnail_url: thumbnailUrl, // S3缩略图URL
|
||||
original_url: originalUrl, // S3原图URL
|
||||
canvas_width: canvasWidth,
|
||||
canvas_height: canvasHeight,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Resize API] 处理错误:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
|
||||
import React, { useRef, useState, useEffect, useCallback } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { X, Pencil, Circle, ArrowRight, Type, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand } from "lucide-react";
|
||||
import { X, Pencil, Circle, ArrowRight, Type, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand, Move, Maximize2 } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type Tool = "pen" | "arrow" | "circle" | "text";
|
||||
@ -64,8 +65,21 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
const [originalImageUrl, setOriginalImageUrl] = useState<string>(imageUrl);
|
||||
const [processedImageUrl, setProcessedImageUrl] = useState<string | null>(null);
|
||||
const [processedImageBase64, setProcessedImageBase64] = useState<string | null>(null);
|
||||
const [processType, setProcessType] = useState<"rmbg" | "enhance" | "extend" | null>(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<number>(1920);
|
||||
const [resizeCanvasHeight, setResizeCanvasHeight] = useState<number>(1080);
|
||||
const [resizeBackgroundColor, setResizeBackgroundColor] = useState<string>("#FFFFFF");
|
||||
const [resizeBackgroundTransparent, setResizeBackgroundTransparent] = useState<boolean>(false);
|
||||
const [resizeImageScale, setResizeImageScale] = useState<number>(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 resizeCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const resizeContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 确保在客户端渲染
|
||||
useEffect(() => {
|
||||
@ -361,10 +375,15 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.image_base64) {
|
||||
// 保存处理后的图片URL和Base64
|
||||
// 保存处理后的图片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);
|
||||
}
|
||||
setProcessedImageBase64(result.image_base64);
|
||||
@ -415,10 +434,15 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.image_base64) {
|
||||
// 保存处理后的图片URL和Base64
|
||||
// 保存处理后的图片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);
|
||||
}
|
||||
setProcessedImageBase64(result.image_base64);
|
||||
@ -436,6 +460,233 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
}
|
||||
};
|
||||
|
||||
// 尺寸调整功能
|
||||
const handleResize = () => {
|
||||
if (!image) return;
|
||||
setIsResizeMode(true);
|
||||
// 初始化尺寸为原图尺寸
|
||||
setResizeCanvasWidth(image.width);
|
||||
setResizeCanvasHeight(image.height);
|
||||
// 初始化图片位置为画布中心
|
||||
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 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) return;
|
||||
|
||||
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 : "尺寸调整失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 图像拓展处理
|
||||
const handleExtend = async () => {
|
||||
if (!image || !canvasRef.current) return;
|
||||
@ -471,8 +722,13 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
if (result.success && result.base64) {
|
||||
// 保存处理后的图片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);
|
||||
}
|
||||
setProcessedImageBase64(result.base64);
|
||||
@ -490,29 +746,83 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
}
|
||||
};
|
||||
|
||||
// 下载处理后的图片(必须使用原图URL)
|
||||
// 下载处理后的图片(必须使用原图URL或Base64)
|
||||
const handleDownloadProcessed = async () => {
|
||||
if (!processedImageUrl) {
|
||||
toast.error("原图地址不可用,无法下载");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 确定文件类型和MIME类型
|
||||
const isPng = processType === "rmbg";
|
||||
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(检查URL中是否包含thumb标识)
|
||||
let downloadUrl = processedImageUrl;
|
||||
if (downloadUrl.includes("-thumb") || downloadUrl.includes("/thumb/") || downloadUrl.includes("thumbnail")) {
|
||||
// 如果URL包含thumb标识,尝试转换为原图URL
|
||||
downloadUrl = downloadUrl
|
||||
.replace(/-thumb-/g, "-orig-")
|
||||
.replace(/\/thumb\//g, "/orig/")
|
||||
.replace(/thumbnail/g, "original");
|
||||
console.warn("[下载] ⚠️ 检测到缩略图URL,已转换为原图URL:", downloadUrl);
|
||||
}
|
||||
|
||||
console.log("[下载] 下载URL:", downloadUrl);
|
||||
|
||||
// 从原图URL下载(原图已上传到S3)
|
||||
const response = await fetch(processedImageUrl);
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error("下载原图失败");
|
||||
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;
|
||||
@ -525,7 +835,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
toast.success("原图下载成功");
|
||||
} catch (error) {
|
||||
console.error("[下载] 错误:", error);
|
||||
toast.error("下载失败,请稍后重试");
|
||||
toast.error(`下载失败: ${error instanceof Error ? error.message : "请稍后重试"}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -540,7 +850,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
|
||||
if (processedImageBase64) {
|
||||
// 如果有处理后的图片Base64,转换为data URL
|
||||
const mimeType = processType === "rmbg" ? "image/png" : "image/jpeg";
|
||||
const mimeType = processType === "rmbg" || processType === "resize" ? "image/png" : "image/jpeg";
|
||||
const dataUrl = `data:${mimeType};base64,${processedImageBase64}`;
|
||||
onConfirm(dataUrl);
|
||||
return;
|
||||
@ -595,7 +905,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRemoveBackground}
|
||||
disabled={isRemovingBg || isEnhancing || isExtending || !image}
|
||||
disabled={isRemovingBg || isEnhancing || isExtending || isResizeMode || !image}
|
||||
title="移除背景(抠图)"
|
||||
>
|
||||
<Scissors className={`mr-1 h-4 w-4 ${isRemovingBg ? "animate-spin" : ""}`} />
|
||||
@ -605,7 +915,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleEnhance}
|
||||
disabled={isEnhancing || isRemovingBg || isExtending || !image}
|
||||
disabled={isEnhancing || isRemovingBg || isExtending || isResizeMode || !image}
|
||||
title="图片变清晰"
|
||||
>
|
||||
<Sparkles className={`mr-1 h-4 w-4 ${isEnhancing ? "animate-spin" : ""}`} />
|
||||
@ -615,12 +925,22 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleExtend}
|
||||
disabled={isExtending || isRemovingBg || isEnhancing || !image}
|
||||
disabled={isExtending || isRemovingBg || isEnhancing || isResizeMode || !image}
|
||||
title="图像拓展"
|
||||
>
|
||||
<Expand className={`mr-1 h-4 w-4 ${isExtending ? "animate-spin" : ""}`} />
|
||||
{isExtending ? "处理中..." : "图像拓展"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleResize}
|
||||
disabled={isResizeMode || isRemovingBg || isEnhancing || isExtending || !image}
|
||||
title="尺寸调整"
|
||||
>
|
||||
<Maximize2 className="mr-1 h-4 w-4" />
|
||||
尺寸调整
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={handleUndo} disabled={actions.length === 0}>
|
||||
<Undo className="mr-1 h-4 w-4" />
|
||||
撤销
|
||||
@ -635,7 +955,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleConfirm} className="bg-primary text-primary-foreground hover:bg-primary/90">
|
||||
<Check className="mr-1 h-4 w-4" />
|
||||
确定
|
||||
发送到新对话
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -729,6 +1049,102 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">正在加载图片...</span>
|
||||
</div>
|
||||
) : isResizeMode ? (
|
||||
/* 尺寸调整模式 */
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-4 w-full h-full">
|
||||
{/* 尺寸调整控制面板 */}
|
||||
<div className="flex items-center gap-4 p-4 bg-muted/50 rounded-lg w-full max-w-2xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">宽度:</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={resizeCanvasWidth}
|
||||
onChange={(e) => setResizeCanvasWidth(Number.parseInt(e.target.value) || 1)}
|
||||
className="w-24 h-8"
|
||||
min={1}
|
||||
max={10000}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">px</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">高度:</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={resizeCanvasHeight}
|
||||
onChange={(e) => setResizeCanvasHeight(Number.parseInt(e.target.value) || 1)}
|
||||
className="w-24 h-8"
|
||||
min={1}
|
||||
max={10000}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">px</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">背景:</label>
|
||||
<input
|
||||
type="color"
|
||||
value={resizeBackgroundColor}
|
||||
onChange={(e) => setResizeBackgroundColor(e.target.value)}
|
||||
disabled={resizeBackgroundTransparent}
|
||||
className="w-12 h-8 rounded border cursor-pointer disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium flex items-center gap-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={resizeBackgroundTransparent}
|
||||
onChange={(e) => setResizeBackgroundTransparent(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
透明
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsResizeMode(false);
|
||||
setResizeImageScale(1);
|
||||
setResizeImagePosition({ x: 0, y: 0 });
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleResizeConfirm}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
确认调整
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 尺寸调整预览画布 */}
|
||||
<div
|
||||
ref={resizeContainerRef}
|
||||
className="flex-1 w-full max-w-4xl border-2 border-dashed border-primary/50 rounded-lg bg-muted/30 p-4 overflow-auto"
|
||||
onWheel={handleResizeImageWheel}
|
||||
>
|
||||
<div className="relative inline-block">
|
||||
<canvas
|
||||
ref={resizeCanvasRef}
|
||||
className="cursor-move rounded-lg shadow-lg"
|
||||
onMouseDown={handleResizeImageDragStart}
|
||||
onMouseMove={handleResizeImageDrag}
|
||||
onMouseUp={handleResizeImageDragEnd}
|
||||
onMouseLeave={handleResizeImageDragEnd}
|
||||
onTouchStart={handleResizeImageDragStart}
|
||||
onTouchMove={handleResizeImageDrag}
|
||||
onTouchEnd={handleResizeImageDragEnd}
|
||||
/>
|
||||
<div className="absolute top-2 left-2 bg-black/70 text-white text-xs px-2 py-1 rounded">
|
||||
画布: {resizeCanvasWidth} x {resizeCanvasHeight} | 缩放: {(resizeImageScale * 100).toFixed(0)}% | 拖拽移动图片,滚轮缩放
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 原图画布 */}
|
||||
@ -752,7 +1168,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
<div className="mb-2 flex items-center justify-between gap-2 w-full">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : "拓展结果"}
|
||||
{processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : "拓展结果"}
|
||||
</span>
|
||||
<Button
|
||||
variant="default"
|
||||
@ -769,14 +1185,14 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
|
||||
{processedImageUrl ? (
|
||||
<img
|
||||
src={processedImageUrl}
|
||||
alt={processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : "拓展结果"}
|
||||
alt={processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : "拓展结果"}
|
||||
className="max-h-[350px] max-w-full rounded-lg"
|
||||
style={{ maxWidth: canvasRef.current?.width || "400px" }}
|
||||
/>
|
||||
) : processedImageBase64 ? (
|
||||
<img
|
||||
src={`data:image/${processType === "rmbg" ? "png" : "jpeg"};base64,${processedImageBase64}`}
|
||||
alt={processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : "拓展结果"}
|
||||
src={`data:image/${processType === "rmbg" || processType === "resize" ? "png" : "jpeg"};base64,${processedImageBase64}`}
|
||||
alt={processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : "拓展结果"}
|
||||
className="max-h-[350px] max-w-full rounded-lg"
|
||||
style={{ maxWidth: canvasRef.current?.width || "400px" }}
|
||||
/>
|
||||
|
||||
@ -232,7 +232,7 @@ export function ImagePreview({
|
||||
<div className="absolute top-2 left-2 flex gap-1.5 z-10">
|
||||
{onImageEnhance && (
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded-full bg-blue-600/90 hover:bg-blue-700 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex items-center gap-1.5 rounded-full bg-black/70 hover:bg-black/80 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) {
|
||||
@ -250,7 +250,7 @@ export function ImagePreview({
|
||||
)}
|
||||
{onImageRmbg && (
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded-full bg-purple-600/90 hover:bg-purple-700 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex items-center gap-1.5 rounded-full bg-black/70 hover:bg-black/80 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) {
|
||||
@ -268,7 +268,7 @@ export function ImagePreview({
|
||||
)}
|
||||
{onImageExtend && (
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded-full bg-orange-600/90 hover:bg-orange-700 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex items-center gap-1.5 rounded-full bg-black/70 hover:bg-black/80 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user