feat: 图片编辑器添加去水印功能,去水印结果保存到相册,使用动态占位图

This commit is contained in:
Your Name 2026-02-03 10:38:49 +08:00
parent f2d7aff7c6
commit 1ba3d7ec83
4 changed files with 120 additions and 15 deletions

View File

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { db } from "@/lib/db";
import { systemSettings } from "@/lib/db/schema";
import { systemSettings, generatedImage } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { uploadFile } from "@/lib/storage/service";
@ -192,6 +192,21 @@ export async function POST(request: NextRequest) {
const processingTime = (Date.now() - startTime) / 1000;
console.log(`[WatermarkRemove] 处理完成,耗时: ${processingTime}s`);
// 保存到相册/历史出图记录
try {
await db.insert(generatedImage).values({
userId: session.user.id,
thumbnailUrl: uploadResult.url,
originalUrl: uploadResult.url,
imageSize: "2K",
prompt: "去水印处理",
modelId: "watermark-remove",
});
console.log(`[WatermarkRemove] 已保存到相册: ${uploadResult.url.substring(0, 80)}...`);
} catch (saveError) {
console.error("[WatermarkRemove] 保存到相册失败:", saveError);
}
return NextResponse.json({
success: true,
original_url: uploadResult.url,

View File

@ -4,7 +4,7 @@ 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, Move3D, Layers, Upload as UploadIcon, Image as ImageIcon } from "lucide-react";
import { X, Pencil, Circle, ArrowRight, Type, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand, Move, Maximize2, ZoomIn, ZoomOut, Loader2, Move3D, Layers, Upload as UploadIcon, Image as ImageIcon, Droplet } from "lucide-react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { toast } from "sonner";
@ -67,12 +67,13 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
const [isRemovingBg, setIsRemovingBg] = useState(false);
const [isEnhancing, setIsEnhancing] = useState(false);
const [isExtending, setIsExtending] = useState(false);
const [isRemovingWatermark, setIsRemovingWatermark] = useState(false);
const [showAngleEditDialog, setShowAngleEditDialog] = useState(false);
const [showSceneFusion, setShowSceneFusion] = useState(false);
const [originalImageUrl, setOriginalImageUrl] = useState<string | undefined>(imageUrl);
const [processedImageUrl, setProcessedImageUrl] = useState<string | null>(null);
const [processedImageBase64, setProcessedImageBase64] = useState<string | null>(null);
const [processType, setProcessType] = useState<"rmbg" | "enhance" | "extend" | "resize" | null>(null);
const [processType, setProcessType] = useState<"rmbg" | "enhance" | "extend" | "resize" | "watermark" | null>(null);
const [mounted, setMounted] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@ -642,6 +643,66 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
}
};
// 去水印功能
const handleWatermarkRemove = async () => {
if (!image || !originalImageUrl) return;
setIsRemovingWatermark(true);
try {
console.log("[去水印] 使用原图URL:", originalImageUrl);
// 调用去水印API
const response = await fetch("/api/watermark-remove", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
image_url: originalImageUrl,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "去水印失败");
}
const result = await response.json();
if (result.success && result.original_url) {
console.log("[去水印] API返回结果:", {
originalUrl: result.original_url?.substring(0, 100),
thumbnailUrl: result.thumbnail_url?.substring(0, 100)
});
setProcessedImageUrl(result.original_url);
// 下载图片获取base64用于显示
try {
const imgResponse = await fetch(result.original_url);
const blob = await imgResponse.blob();
const reader = new FileReader();
reader.onloadend = () => {
const base64 = (reader.result as string).split(',')[1];
setProcessedImageBase64(base64);
};
reader.readAsDataURL(blob);
} catch {
console.warn("[去水印] 无法获取Base64,仅使用URL");
}
setProcessType("watermark");
toast.success(`去水印完成!图片已保存到相册。处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`);
} else {
throw new Error(result.error || "去水印处理失败");
}
} catch (error) {
console.error("[去水印] 错误:", error);
toast.error(error instanceof Error ? error.message : "去水印失败,请稍后重试");
} finally {
setIsRemovingWatermark(false);
}
};
// 变清晰处理
const handleEnhance = async () => {
if (!image || !originalImageUrl) return;
@ -1044,6 +1105,8 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
? `变清晰-${Date.now()}.${fileExtension}`
: processType === "resize"
? `尺寸调整-${resizeCanvasWidth}x${resizeCanvasHeight}-${Date.now()}.${fileExtension}`
: processType === "watermark"
? `去水印-${Date.now()}.${fileExtension}`
: `图像拓展-${Date.now()}.${fileExtension}`;
// 如果尺寸调整使用Base64,直接下载(优先使用URL,如果没有URL则使用Base64)
@ -1241,7 +1304,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
variant="ghost"
size="sm"
onClick={handleRemoveBackground}
disabled={isRemovingBg || isEnhancing || isExtending || isResizeMode || showAngleEditDialog || showSceneFusion || !image}
disabled={isRemovingBg || isEnhancing || isExtending || isResizeMode || showAngleEditDialog || showSceneFusion || isRemovingWatermark || !image}
title="移除背景(抠图)"
className="text-zinc-300 hover:text-white hover:bg-zinc-800"
>
@ -1252,7 +1315,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
variant="ghost"
size="sm"
onClick={handleEnhance}
disabled={isEnhancing || isRemovingBg || isExtending || isResizeMode || showAngleEditDialog || showSceneFusion || !image}
disabled={isEnhancing || isRemovingBg || isExtending || isResizeMode || showAngleEditDialog || showSceneFusion || isRemovingWatermark || !image}
title="图片变清晰"
className="text-zinc-300 hover:text-white hover:bg-zinc-800"
>
@ -1263,7 +1326,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
variant="ghost"
size="sm"
onClick={handleExtend}
disabled={isExtending || isRemovingBg || isEnhancing || isResizeMode || showAngleEditDialog || showSceneFusion || !image}
disabled={isExtending || isRemovingBg || isEnhancing || isResizeMode || showAngleEditDialog || showSceneFusion || isRemovingWatermark || !image}
title="图像拓展"
className="text-zinc-300 hover:text-white hover:bg-zinc-800"
>
@ -1274,7 +1337,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
variant="ghost"
size="sm"
onClick={handleResize}
disabled={isResizeMode || isRemovingBg || isEnhancing || isExtending || showAngleEditDialog || showSceneFusion || !image}
disabled={isResizeMode || isRemovingBg || isEnhancing || isExtending || showAngleEditDialog || showSceneFusion || isRemovingWatermark || !image}
className="text-zinc-300 hover:text-white hover:bg-zinc-800"
title="尺寸调整"
>
@ -1285,13 +1348,24 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
variant="ghost"
size="sm"
onClick={handleAngleEdit}
disabled={showAngleEditDialog || isRemovingBg || isEnhancing || isExtending || isResizeMode || showSceneFusion || !image}
disabled={showAngleEditDialog || isRemovingBg || isEnhancing || isExtending || isResizeMode || showSceneFusion || isRemovingWatermark || !image}
title="角度调整"
className="text-zinc-300 hover:text-white hover:bg-zinc-800"
>
<Move3D className="mr-1 h-4 w-4" />
角度调整
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleWatermarkRemove}
disabled={isRemovingWatermark || isRemovingBg || isEnhancing || isExtending || isResizeMode || showAngleEditDialog || showSceneFusion || !image}
title="去水印"
className="text-zinc-300 hover:text-white hover:bg-zinc-800"
>
<Droplet className={`mr-1 h-4 w-4 ${isRemovingWatermark ? "animate-spin" : ""}`} />
{isRemovingWatermark ? "处理中..." : "去水印"}
</Button>
<Button
variant={showSceneFusion ? "secondary" : "ghost"}
size="sm"
@ -1685,7 +1759,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
<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 === "resize" ? "尺寸调整结果" : "拓展结果"}
{processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : processType === "watermark" ? "去水印结果" : "拓展结果"}
</span>
<Button
variant="default"
@ -1702,14 +1776,14 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
{processedImageUrl ? (
<img
src={processedImageUrl}
alt={processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : "拓展结果"}
alt={processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : processType === "watermark" ? "去水印结果" : "拓展结果"}
className="max-h-[350px] max-w-full rounded-lg"
style={{ maxWidth: canvasRef.current?.width || "400px" }}
/>
) : processedImageBase64 ? (
<img
src={`data:image/${processType === "rmbg" || processType === "resize" ? "png" : "jpeg"};base64,${processedImageBase64}`}
alt={processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : "拓展结果"}
alt={processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : processType === "watermark" ? "去水印结果" : "拓展结果"}
className="max-h-[350px] max-w-full rounded-lg"
style={{ maxWidth: canvasRef.current?.width || "400px" }}
/>

View File

@ -2,10 +2,10 @@
import { memo } from "react";
import { SparklesIcon } from "./icons";
import { Scissors, Expand } from "lucide-react";
import { Scissors, Expand, Droplet } from "lucide-react";
interface ImageProcessingLoadingProps {
type: "enhance" | "rmbg" | "extend"; // 处理类型:变清晰、抠图或图像拓展
type: "enhance" | "rmbg" | "extend" | "watermark"; // 处理类型:变清晰、抠图、图像拓展或去水印
}
/**
@ -16,6 +16,7 @@ export const ImageProcessingLoading = memo(({ type }: ImageProcessingLoadingProp
const isEnhance = type === "enhance";
const isRmbg = type === "rmbg";
const isExtend = type === "extend";
const isWatermark = type === "watermark";
let text: string;
let icon: any;
@ -47,6 +48,19 @@ export const ImageProcessingLoading = memo(({ type }: ImageProcessingLoadingProp
gradientFromOverlay: "from-green-500/40",
gradientToOverlay: "to-green-500/30",
};
} else if (isWatermark) {
text = "正在去水印";
icon = Droplet;
colorClasses = {
ping: "bg-cyan-500/20",
pulse: "bg-cyan-500/10",
ring: "ring-cyan-500/30",
text: "text-cyan-500",
dot: "bg-cyan-500",
gradientFrom: "from-cyan-400",
gradientFromOverlay: "from-cyan-500/40",
gradientToOverlay: "to-cyan-500/30",
};
} else {
text = "正在图像拓展";
icon = Expand;

View File

@ -257,12 +257,14 @@ const PurePreviewMessage = ({
if (type === "text") {
if (mode === "view") {
// 检查是否是图片处理的加载消息
if (part.text === "正在变清晰..." || part.text === "正在抠图..." || part.text === "正在图像拓展...") {
let type: "enhance" | "rmbg" | "extend";
if (part.text === "正在变清晰..." || part.text === "正在抠图..." || part.text === "正在图像拓展..." || part.text === "正在去水印...") {
let type: "enhance" | "rmbg" | "extend" | "watermark";
if (part.text === "正在变清晰...") {
type = "enhance";
} else if (part.text === "正在抠图...") {
type = "rmbg";
} else if (part.text === "正在去水印...") {
type = "watermark";
} else {
type = "extend";
}