Skip to content

Commit 9c67b37

Browse files
committedAug 25, 2024
bg remover
1 parent 6ba4ef5 commit 9c67b37

File tree

12 files changed

+447
-80
lines changed

12 files changed

+447
-80
lines changed
 

‎README_structure.txt

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
├── LICENSE
2+
├── Makefile
3+
├── README.md
4+
├── api
5+
│ └── protected
6+
│ └── edit-image
7+
├── app
8+
│ ├── (auth)
9+
│ │ ├── sign-in
10+
│ │ │ └── [[...sign-in]]
11+
│ │ │ └── page.tsx
12+
│ │ └── sign-up
13+
│ │ └── [[...sign-up]]
14+
│ │ └── page.tsx
15+
│ ├── (default)
16+
│ │ ├── edit
17+
│ │ │ └── [id]
18+
│ │ │ └── page.tsx
19+
│ │ ├── gallery
20+
│ │ │ └── page.tsx
21+
│ │ ├── layout.tsx
22+
│ │ └── page.tsx
23+
│ ├── api
24+
│ │ ├── get-public-logos
25+
│ │ │ └── route.ts
26+
│ │ ├── get-user-info
27+
│ │ │ └── route.ts
28+
│ │ └── protected
29+
│ │ ├── check-logo-status
30+
│ │ │ └── route.ts
31+
│ │ ├── gen-logo
32+
│ │ │ └── route.ts
33+
│ │ ├── get-user-logos
34+
│ │ │ └── route.ts
35+
│ │ └── update-logo
36+
│ │ └── route.ts
37+
│ ├── favicon.ico
38+
│ ├── globals.css
39+
│ └── layout.tsx
40+
├── components
41+
│ ├── ImageCanvas.tsx
42+
│ ├── ImageEditor
43+
│ ├── ImageEditor.tsx
44+
│ ├── ImageUploader.tsx
45+
│ ├── footer
46+
│ │ └── index.tsx
47+
│ ├── header
48+
│ │ └── index.tsx
49+
│ ├── hero
50+
│ │ └── index.tsx
51+
│ ├── input
52+
│ │ └── index.tsx
53+
│ ├── payment
54+
│ │ └── StripeButton.tsx
55+
│ ├── public-logos
56+
│ │ └── index.tsx
57+
│ ├── social
58+
│ │ └── index.tsx
59+
│ ├── ui
60+
│ │ ├── avatar.tsx
61+
│ │ ├── badge.tsx
62+
│ │ ├── button.tsx
63+
│ │ ├── dropdown-menu.tsx
64+
│ │ ├── input.tsx
65+
│ │ ├── skeleton.tsx
66+
│ │ ├── tabs.tsx
67+
│ │ └── toggle-group.tsx
68+
│ ├── user
69+
│ │ └── index.tsx
70+
│ └── user-logos
71+
│ └── index.tsx
72+
├── components.json
73+
├── contexts
74+
│ └── AppContext.tsx
75+
├── debug
76+
│ └── apitest.http
77+
├── deploy.sh
78+
├── extensions.json
79+
├── lib
80+
│ ├── image.ts
81+
│ ├── prompt.ts
82+
│ ├── resp.ts
83+
│ ├── s3.ts
84+
│ └── utils.ts
85+
├── log.txt
86+
├── middleware.ts
87+
├── models
88+
│ ├── db.ts
89+
│ ├── public_logo.ts
90+
│ └── user_logo.ts
91+
├── next-env.d.ts
92+
├── next.config.js
93+
├── package-lock.json
94+
├── package.json
95+
├── pnpm-lock.yaml
96+
├── postcss.config.js
97+
├── preview.png
98+
├── public
99+
│ ├── black_t.jpg
100+
│ ├── grey_t.jpg
101+
│ ├── logo.png
102+
│ ├── template.png
103+
│ └── white_t.jpg
104+
├── services
105+
│ ├── openai.ts
106+
│ └── user.ts
107+
├── settings.json
108+
├── tailwind.config.ts
109+
├── test.js
110+
├── todo.txt
111+
├── tsconfig.json
112+
└── types
113+
├── context.d.ts
114+
├── logo.d.ts
115+
├── tab.d.ts
116+
└── user.d.ts
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
export async function POST(req: NextRequest) {
4+
try {
5+
console.log('收到背景移除请求');
6+
7+
const formData = await req.formData();
8+
const file = formData.get('file') as File;
9+
10+
if (!file) {
11+
console.error('没有提供文件');
12+
return NextResponse.json({ error: '没有提供文件' }, { status: 400 });
13+
}
14+
15+
console.log('文件已接收,准备发送到外部 API');
16+
17+
// 创建一个新的 FormData 对象来发送到外部 API
18+
const apiFormData = new FormData();
19+
apiFormData.append('file', file);
20+
21+
// 发送请求到外部 API
22+
console.log('正在发送请求到外部 API');
23+
const apiResponse = await fetch('https://api-back.cashzineapp.com/removebg', {
24+
method: 'POST',
25+
body: apiFormData,
26+
});
27+
28+
console.log('收到外部 API 响应:', apiResponse.status, apiResponse.statusText);
29+
30+
if (!apiResponse.ok) {
31+
throw new Error(`API 请求失败: ${apiResponse.status} ${apiResponse.statusText}`);
32+
}
33+
34+
// 获取 API 响应的内容类型
35+
const contentType = apiResponse.headers.get('content-type');
36+
37+
// 创建一个 NextResponse 对象,将 API 的响应直接传递给客户端
38+
const response = new NextResponse(apiResponse.body);
39+
40+
// 设置正确的内容类型
41+
if (contentType) {
42+
response.headers.set('content-type', contentType);
43+
}
44+
45+
console.log('成功处理请求,返回响应');
46+
return response;
47+
48+
} catch (error) {
49+
console.error('移除背景时出错:', error);
50+
const errorMessage = error instanceof Error ? error.message : '未知错误';
51+
return NextResponse.json({ error: '移除背景失败', details: errorMessage }, { status: 500 });
52+
}
53+
}

‎app/(default)/edit/[id]/page.tsx

Whitespace-only changes.

‎app/(default)/page.tsx

+12-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dynamic from "next/dynamic";
44
import { useContext, useEffect, useState } from "react";
55
import { toast } from "sonner";
6+
import { useRouter } from 'next/navigation';
67

78
import ImageUploader from "@/components/ImageUploader";
89
import Hero from "@/components/hero";
@@ -21,8 +22,10 @@ export default function Page() {
2122
const [userLogos, setUserLogos] = useState<Logo[]>([]);
2223
const [loading, setLoading] = useState(true);
2324
const [pollLogoID, setPollLogoID] = useState("");
24-
const [imageFile, setImageFile] = useState<File | null>(null); // State to hold the selected image file
25+
const [imageFile, setImageFile] = useState<File | null>(null);
26+
const router = useRouter();
2527

28+
// ... (keep existing fetchLogos and checkLogoStatus functions)
2629
const fetchLogos = async function () {
2730
try {
2831
const uri = "/api/protected/get-user-logos";
@@ -79,6 +82,12 @@ export default function Page() {
7982
return () => clearInterval(pollInterval);
8083
}, [pollLogoID]);
8184

85+
const handleEditClick = (logoId: string) => {
86+
router.push(`/edit/${logoId}`);
87+
};
88+
89+
// ... (keep existing useEffect hooks)
90+
8291
return (
8392
<div className="flex gap-x-6">
8493
<div className="flex-1">
@@ -99,7 +108,6 @@ export default function Page() {
99108
setPollLogoID={setPollLogoID}
100109
onImageClick={(imgRef: HTMLImageElement | null) => {
101110
console.log("Image clicked", imgRef);
102-
// Read img as File and set it as imageFile
103111
if (!imgRef) return;
104112
fetch(imgRef.src)
105113
.then((res) => res.blob())
@@ -110,6 +118,7 @@ export default function Page() {
110118
setImageFile(file);
111119
});
112120
}}
121+
onEditClick={handleEditClick}
113122
/>
114123
</div>
115124
<h3 className="text-2xl font-bold">Upload and Edit Image</h3>
@@ -123,4 +132,4 @@ export default function Page() {
123132
</div>
124133
</div>
125134
);
126-
}
135+
}

‎app/api/proxy/removebg/route.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
export async function POST(request: NextRequest) {
4+
try {
5+
const formData = await request.formData();
6+
7+
const response = await fetch('https://api-back.cashzineapp.com/removebg', {
8+
method: 'POST',
9+
body: formData,
10+
});
11+
12+
if (!response.ok) {
13+
throw new Error(`HTTP error! status: ${response.status}`);
14+
}
15+
16+
const data = await response.blob();
17+
18+
return new NextResponse(data, {
19+
status: 200,
20+
headers: {
21+
'Content-Type': response.headers.get('Content-Type') || 'application/octet-stream'
22+
}
23+
});
24+
} catch (error) {
25+
console.error('代理请求失败:', error);
26+
const errorMessage = error instanceof Error ? error.message : '未知错误';
27+
return NextResponse.json({ error: '代理请求失败', details: errorMessage }, { status: 500 });
28+
}
29+
}

‎components/ImageCanvas.tsx

+96-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fabric } from "fabric";
22
import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react";
33
import React, { useCallback, useEffect, useRef, useState } from "react";
4-
import { FaRedo, FaTrash, FaUndo } from "react-icons/fa"; // 引入FontAwesome图标
4+
import { FaRedo, FaTrash, FaUndo, FaMagic } from "react-icons/fa";
55

66
const MAX_HISTORY_LENGTH = 50;
77

@@ -20,6 +20,7 @@ const ImageCanvas = ({ imageFile }: ImageCanvasProps) => {
2020
const [redoStack, setRedoStack] = useState([] as CanvasHistory[]);
2121
const [isUndoRedoAction, setIsUndoRedoAction] = useState(false);
2222
const [backgroundColor, setBackgroundColor] = useState("white_t");
23+
const [isRemovingBackground, setIsRemovingBackground] = useState(false);
2324
const canvasContainerRef = useRef<HTMLDivElement>(null);
2425

2526
const backgroundMap: Record<string, string> = {
@@ -36,7 +37,6 @@ const ImageCanvas = ({ imageFile }: ImageCanvasProps) => {
3637
editor.canvas.setWidth(containerWidth);
3738
editor.canvas.setHeight(containerHeight);
3839

39-
// if not null and is an instance of Image
4040
if (editor.canvas.backgroundImage instanceof fabric.Image) {
4141
const backgroundImage = editor.canvas.backgroundImage;
4242
const scaleFactor =
@@ -121,6 +121,86 @@ const ImageCanvas = ({ imageFile }: ImageCanvasProps) => {
121121
[editor]
122122
);
123123

124+
const handleRemoveBackground = useCallback(async () => {
125+
console.log('handleRemoveBackground 被调用');
126+
if (!editor) {
127+
console.log('编辑器未初始化');
128+
return;
129+
}
130+
131+
const activeObject = editor.canvas.getActiveObject();
132+
if (!(activeObject instanceof fabric.Image)) {
133+
console.log('没有选中图像对象');
134+
alert('请选择一个图像以移除背景。');
135+
return;
136+
}
137+
138+
setIsRemovingBackground(true);
139+
140+
try {
141+
console.log('开始移除背景过程');
142+
143+
// 获取原始图像格式
144+
const originalFormat = (activeObject as fabric.Image).getSrc().split('.').pop()?.toLowerCase() || 'png';
145+
146+
// 将 Fabric.js 图像对象转换为 Blob,使用原始格式
147+
const dataURL = (activeObject as fabric.Image).toDataURL({ format: originalFormat as 'png' | 'jpeg' });
148+
const blob = await fetch(dataURL).then(res => res.blob());
149+
console.log('图像已转换为 Blob');
150+
151+
// 创建 FormData 对象
152+
const formData = new FormData();
153+
formData.append('file', blob, 'image.png');
154+
console.log('FormData 已创建');
155+
156+
// 发送请求到我们的 API 路由
157+
console.log('正在发送请求到 API');
158+
const response = await fetch('/api/proxy/removebg', {
159+
method: 'POST',
160+
body: formData,
161+
});
162+
163+
console.log('收到 API 响应:', response.status, response.statusText);
164+
165+
if (!response.ok) {
166+
throw new Error(`移除背景失败: ${response.status} ${response.statusText}`);
167+
}
168+
169+
// 获取处理后的图像数据
170+
const resultBlob = await response.blob();
171+
console.log('已接收处理后的图像数据');
172+
173+
const url = URL.createObjectURL(resultBlob);
174+
175+
// 加载处理后的图像
176+
fabric.Image.fromURL(url, (img) => {
177+
console.log('正在将处理后的图像添加到画布');
178+
// 保持原始图像的位置和大小
179+
img.set({
180+
left: activeObject.left,
181+
top: activeObject.top,
182+
scaleX: activeObject.scaleX,
183+
scaleY: activeObject.scaleY,
184+
angle: activeObject.angle,
185+
selectable: true,
186+
});
187+
188+
// 替换原始图像
189+
editor.canvas.remove(activeObject);
190+
editor.canvas.add(img);
191+
editor.canvas.setActiveObject(img);
192+
editor.canvas.renderAll();
193+
console.log('背景移除完成');
194+
});
195+
196+
} catch (error) {
197+
console.error('移除背景时出错:', error);
198+
alert('移除背景失败。请重试。');
199+
} finally {
200+
setIsRemovingBackground(false);
201+
}
202+
}, [editor]);
203+
124204
useEffect(() => {
125205
if (editor) {
126206
addBackground();
@@ -152,10 +232,9 @@ const ImageCanvas = ({ imageFile }: ImageCanvasProps) => {
152232
if (editor && imageFile) {
153233
const reader = new FileReader();
154234
reader.onload = (event) => {
155-
if (typeof event.target?.result !== "string") {
156-
return;
235+
if (typeof event.target?.result === "string") {
236+
addImageToCanvas(event.target?.result);
157237
}
158-
addImageToCanvas(event.target?.result);
159238
};
160239
reader.readAsDataURL(imageFile);
161240
}
@@ -234,6 +313,12 @@ const ImageCanvas = ({ imageFile }: ImageCanvasProps) => {
234313
}
235314
}, [backgroundColor, editor, addBackground]);
236315

316+
useEffect(() => {
317+
if (editor) {
318+
console.log('编辑器已初始化');
319+
}
320+
}, [editor]);
321+
237322
return (
238323
<div
239324
ref={canvasContainerRef}
@@ -266,11 +351,16 @@ const ImageCanvas = ({ imageFile }: ImageCanvasProps) => {
266351
<FaUndo onClick={handleUndo} className="cursor-pointer" />
267352
<FaRedo onClick={handleRedo} className="cursor-pointer" />
268353
<FaTrash onClick={handleDelete} className="cursor-pointer" />
354+
<FaMagic
355+
onClick={handleRemoveBackground}
356+
className={`cursor-pointer ${isRemovingBackground ? 'opacity-50' : ''}`}
357+
title="Remove Background"
358+
/>
269359
</div>
270360
</div>
271361
<FabricJSCanvas className="sample-canvas" onReady={onReady} />
272362
</div>
273363
);
274364
};
275365

276-
export default ImageCanvas;
366+
export default ImageCanvas;

‎components/ImageEditor.tsx

+72-28
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ interface ImageEditorProps {
99
const ImageEditor: React.FC<ImageEditorProps> = ({ imageFile }) => {
1010
const { editor, onReady } = useFabricJSEditor();
1111
const [imageSrc, setImageSrc] = useState<string | null>(null);
12-
12+
const [isLoading, setIsLoading] = useState(false);
1313
const maxCanvasWidth = 800;
1414
const maxCanvasHeight = 600;
1515

@@ -25,38 +25,82 @@ const ImageEditor: React.FC<ImageEditorProps> = ({ imageFile }) => {
2525

2626
useEffect(() => {
2727
if (editor && imageSrc) {
28-
fabric.Image.fromURL(imageSrc, (img) => {
29-
const canvasWidth = editor.canvas.getWidth();
30-
const canvasHeight = editor.canvas.getHeight();
31-
const imgHeight = img.height || canvasHeight;
32-
const imgWidth = img.width || canvasWidth;
33-
34-
let scale = 1;
35-
if (imgWidth > canvasWidth || imgHeight > canvasHeight) {
36-
const widthScale = canvasWidth / imgWidth;
37-
const heightScale = canvasHeight / imgHeight;
38-
scale = Math.min(widthScale, heightScale);
39-
}
40-
41-
img.scale(scale);
42-
img.set({
43-
left: canvasWidth / 2 - (imgWidth * scale) / 2,
44-
top: canvasHeight / 2 - (imgHeight * scale) / 2,
45-
selectable: true,
46-
});
47-
48-
editor.canvas.add(img);
49-
editor.canvas.setActiveObject(img);
50-
editor.canvas.renderAll();
51-
});
28+
loadImageToCanvas(imageSrc);
5229
}
5330
}, [editor, imageSrc]);
5431

32+
const loadImageToCanvas = (src: string) => {
33+
fabric.Image.fromURL(src, (img) => {
34+
const canvasWidth = editor!.canvas.getWidth();
35+
const canvasHeight = editor!.canvas.getHeight();
36+
const imgHeight = img.height || canvasHeight;
37+
const imgWidth = img.width || canvasWidth;
38+
39+
let scale = 1;
40+
if (imgWidth > canvasWidth || imgHeight > canvasHeight) {
41+
const widthScale = canvasWidth / imgWidth;
42+
const heightScale = canvasHeight / imgHeight;
43+
scale = Math.min(widthScale, heightScale);
44+
}
45+
46+
img.scale(scale);
47+
img.set({
48+
left: canvasWidth / 2 - (imgWidth * scale) / 2,
49+
top: canvasHeight / 2 - (imgHeight * scale) / 2,
50+
selectable: true,
51+
});
52+
53+
editor!.canvas.clear();
54+
editor!.canvas.add(img);
55+
editor!.canvas.setActiveObject(img);
56+
editor!.canvas.renderAll();
57+
});
58+
};
59+
60+
const handleRemoveBackground = async () => {
61+
if (!imageFile) return;
62+
63+
setIsLoading(true);
64+
65+
const formData = new FormData();
66+
formData.append('file', imageFile);
67+
68+
try {
69+
const response = await fetch('/api/protected/remove-background', {
70+
method: 'POST',
71+
body: formData,
72+
});
73+
74+
if (!response.ok) {
75+
throw new Error('Failed to remove background');
76+
}
77+
78+
const blob = await response.blob();
79+
const newImageSrc = URL.createObjectURL(blob);
80+
setImageSrc(newImageSrc);
81+
loadImageToCanvas(newImageSrc);
82+
} catch (error) {
83+
console.error('Error removing background:', error);
84+
alert('Failed to remove background. Please try again.');
85+
} finally {
86+
setIsLoading(false);
87+
}
88+
};
89+
5590
return (
56-
<div style={{ width: maxCanvasWidth, height: maxCanvasHeight }}>
57-
<FabricJSCanvas className="sample-canvas" onReady={onReady} />
91+
<div>
92+
<div style={{ width: maxCanvasWidth, height: maxCanvasHeight }}>
93+
<FabricJSCanvas className="sample-canvas" onReady={onReady} />
94+
</div>
95+
<button
96+
onClick={handleRemoveBackground}
97+
disabled={!imageFile || isLoading}
98+
style={{ marginTop: '10px' }}
99+
>
100+
{isLoading ? 'Processing...' : 'Remove Background'}
101+
</button>
58102
</div>
59103
);
60104
};
61105

62-
export default ImageEditor;
106+
export default ImageEditor;

‎package-lock.json

+18-43
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+2
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
"clsx": "^2.1.0",
2525
"fabric": "^5.3.0",
2626
"fabricjs-react": "^1.2.2",
27+
"form-data": "^4.0.0",
2728
"konva": "^9.3.12",
2829
"lucide-react": "^0.307.0",
2930
"next": "14.2.4",
31+
"node-fetch": "^2.7.0",
3032
"openai": "^4.24.1",
3133
"react": "^18.3.1",
3234
"react-copy-to-clipboard": "^5.1.0",

‎public/tree.jpg

70.8 KB
Loading

‎tests/api/removeBackground.test.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const fetch = require('node-fetch');
2+
const fs = require('fs');
3+
const path = require('path');
4+
const FormData = require('form-data');
5+
6+
async function testRemoveBackgroundAPI() {
7+
try {
8+
// 读取测试图像
9+
const testImagePath = path.join(__dirname, '..', '..', 'public', 'tree.jpg');
10+
const testImageBuffer = fs.readFileSync(testImagePath);
11+
12+
// 创建 FormData 对象
13+
const formData = new FormData();
14+
formData.append('file', testImageBuffer, 'tree.jpg');
15+
16+
// 发送请求到 API
17+
const response = await fetch('https://api-back.cashzineapp.com/removebg', {
18+
method: 'POST',
19+
body: formData,
20+
});
21+
22+
if (!response.ok) {
23+
throw new Error(`API 请求失败: ${response.status} ${response.statusText}`);
24+
}
25+
26+
// 获取响应数据
27+
const resultBuffer = await response.buffer();
28+
29+
// 检查响应
30+
if (resultBuffer.length === 0) {
31+
throw new Error('返回的图像为空');
32+
}
33+
34+
console.log('API 测试成功!');
35+
console.log(`原始图像大小: ${testImageBuffer.length} 字节`);
36+
console.log(`处理后图像大小: ${resultBuffer.length} 字节`);
37+
38+
// 保存处理后的图像
39+
const outputPath = path.join(__dirname, 'resources/output', 'tree_remove.jpg');
40+
fs.writeFileSync(outputPath, resultBuffer);
41+
console.log(`处理后的图像已保存到: ${outputPath}`);
42+
43+
} catch (error) {
44+
console.error('API 测试失败:', error);
45+
}
46+
}
47+
48+
// 运行测试
49+
testRemoveBackgroundAPI();
299 KB
Loading

0 commit comments

Comments
 (0)
Please sign in to comment.