Commit f11990d2 authored by fisherdaddy's avatar fisherdaddy

feature: 新增图像背景移除工具

parent 3db13667
......@@ -26,7 +26,7 @@ const SubtitleGenerator = lazy(() => import('./components/SubtitleGenerator'));
const ImageCompressor = lazy(() => import('./components/ImageCompressor'));
const ImageWatermark = lazy(() => import('./components/ImageWatermark'));
const TextBehindImage = lazy(() => import('./components/TextBehindImage'));
const BackgroundRemover = lazy(() => import('./components/BackgroundRemover'));
function App() {
return (
<div className="app-container">
......@@ -58,6 +58,7 @@ function App() {
<Route path="/image-compressor" element={<ImageCompressor />} />
<Route path="/image-watermark" element={<ImageWatermark />} />
<Route path="/text-behind-image" element={<TextBehindImage />} />
<Route path="/background-remover" element={<BackgroundRemover />} />
<Route path="*" element={<NotFound />} />
</Routes>
......
import React, { useState, useRef } from 'react';
import { removeBackground } from "@imgly/background-removal";
import styled from 'styled-components';
import { useTranslation } from '../js/i18n';
import SEO from './SEO';
// Reuse container style
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px),
linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
}
`;
const ContentWrapper = styled.div`
display: flex;
gap: 2rem;
max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 1;
height: calc(100vh - 6rem);
@media (max-width: 768px) {
flex-direction: column;
height: auto;
}
`;
const Title = styled.h2`
font-size: 1.8rem;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
letter-spacing: -0.02em;
`;
const PreviewArea = styled.div`
flex: 2;
display: flex;
flex-direction: column;
gap: 1rem;
background: white;
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
height: 100%;
overflow: hidden;
position: relative;
.preview-content {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
}
.upload-prompt {
color: #666;
font-size: 1.2rem;
}
`;
const ControlPanel = styled.div`
flex: 1;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
flex-direction: column;
gap: 1rem;
`;
const ImageUploadArea = styled.div`
border: 2px dashed #6366F1;
border-radius: 8px;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(99, 102, 241, 0.05);
}
`;
const DownloadButton = styled.button`
position: absolute;
top: 1.5rem;
right: 1.5rem;
z-index: 10;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
}
&:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
`;
// 添加提示语样式
const PrivacyNote = styled.div`
background: rgba(99, 102, 241, 0.1);
border-left: 4px solid #6366F1;
padding: 1rem;
margin-top: 1rem;
border-radius: 0 8px 8px 0;
color: #4F46E5;
font-size: 0.9rem;
line-height: 1.5;
`;
function BackgroundRemover() {
const { t } = useTranslation();
const [selectedImage, setSelectedImage] = useState(null);
const [removedBgImage, setRemovedBgImage] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
const fileInputRef = useRef(null);
const handleImageUpload = async (e) => {
const file = e.target.files?.[0];
if (file) {
try {
setIsProcessing(true);
const imageUrl = URL.createObjectURL(file);
setSelectedImage(imageUrl);
// Remove background
const imageBlob = await removeBackground(imageUrl);
const removedBgUrl = URL.createObjectURL(imageBlob);
setRemovedBgImage(removedBgUrl);
} catch (error) {
console.error('Error processing image:', error);
} finally {
setIsProcessing(false);
}
}
};
const handleDownload = () => {
if (removedBgImage) {
const link = document.createElement('a');
link.href = removedBgImage;
link.download = 'removed-background.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
return (
<>
<SEO
title={t('tools.imageBackgroundRemover.title')}
description={t('tools.imageBackgroundRemover.description')}
/>
<Container>
<ContentWrapper>
<ControlPanel>
<Title>{t('tools.imageBackgroundRemover.title')}</Title>
<ImageUploadArea onClick={() => fileInputRef.current.click()}>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
accept="image/*"
onChange={handleImageUpload}
/>
{t('tools.imageBackgroundRemover.uploadPrompt')}
</ImageUploadArea>
{/* 添加提示语 */}
<PrivacyNote>
{t('tools.imageBackgroundRemover.privacyNote')}
</PrivacyNote>
</ControlPanel>
<PreviewArea>
{removedBgImage && (
<DownloadButton onClick={handleDownload}>
{t('tools.imageBackgroundRemover.download')}
</DownloadButton>
)}
<div className="preview-content">
{isProcessing ? (
<div className="loading-container">
<span>{t('tools.imageBackgroundRemover.processing')}</span>
</div>
) : removedBgImage ? (
<div style={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<img
src={removedBgImage}
alt="Processed"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain'
}}
/>
</div>
) : (
<div className="upload-prompt">
{t('tools.imageBackgroundRemover.noImage')}
</div>
)}
</div>
</PreviewArea>
</ContentWrapper>
</Container>
</>
);
}
export default BackgroundRemover;
\ No newline at end of file
......@@ -150,6 +150,18 @@ const UploadButton = styled.div`
}
`;
// 添加隐私提示样式
const PrivacyNote = styled.div`
background: rgba(99, 102, 241, 0.1);
border-left: 4px solid #6366F1;
padding: 1rem;
margin-top: 0.5rem;
border-radius: 0 8px 8px 0;
color: #4F46E5;
font-size: 0.9rem;
line-height: 1.5;
`;
function ImageWatermark() {
const { t } = useTranslation();
const [image, setImage] = useState(null);
......@@ -347,6 +359,11 @@ function ImageWatermark() {
/>
{t('tools.imageWatermark.dropOrClick')}
</UploadButton>
{/* 添加隐私提示 */}
<PrivacyNote>
{t('tools.imageWatermark.privacyNote')}
</PrivacyNote>
</Section>
<Section>
......
......@@ -16,6 +16,8 @@ const SubtitleMaker = () => {
strokeColor: '#000000'
});
const fileInputRef = useRef(null);
// 预设的字幕颜色选项 - 只保留最常用的几个
const presetColors = [
{ name: t('tools.subtitleGenerator.presetColors.classicYellow'), value: '#FFE135' },
......@@ -138,7 +140,15 @@ useEffect(() => {
<Container>
<SettingsPanel>
<h3>{t('tools.subtitleGenerator.uploadImage')}</h3>
<FileInput type="file" accept="image/*" onChange={handleImageUpload} />
<UploadButton onClick={() => fileInputRef.current.click()}>
<HiddenFileInput
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
/>
{t('tools.subtitleGenerator.dropOrClick')}
</UploadButton>
{imageSrc && (
<>
......@@ -322,33 +332,25 @@ const PreviewImage = styled.img`
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
`;
const FileInput = styled.input`
width: 100%;
padding: 0.5rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 0.9rem;
margin-bottom: 1rem;
cursor: pointer;
&::-webkit-file-upload-button {
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
const UploadButton = styled.div`
border: 2px dashed rgba(99, 102, 241, 0.2);
border-radius: 12px;
padding: 2rem;
text-align: center;
cursor: pointer;
margin-right: 1rem;
font-weight: 500;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.5);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
}
border-color: rgba(99, 102, 241, 0.4);
background: rgba(99, 102, 241, 0.05);
}
`;
const HiddenFileInput = styled.input`
display: none;
`;
const SettingGroup = styled.div`
margin-bottom: 1.5rem;
......
......@@ -241,6 +241,18 @@ const GroupTitle = styled.h3`
font-weight: 600;
`;
// 添加隐私提示样式
const PrivacyNote = styled.div`
background: rgba(99, 102, 241, 0.1);
border-left: 4px solid #6366F1;
padding: 1rem;
margin-top: 0.5rem;
border-radius: 0 8px 8px 0;
color: #4F46E5;
font-size: 0.9rem;
line-height: 1.5;
`;
function TextBehindImage() {
const { t } = useTranslation();
const [selectedImage, setSelectedImage] = useState(null);
......@@ -465,6 +477,11 @@ function TextBehindImage() {
/>
{t('tools.textBehindImage.uploadPrompt')}
</ImageUploadArea>
{/* 添加隐私提示 */}
<PrivacyNote>
{t('tools.textBehindImage.privacyNote')}
</PrivacyNote>
</SettingsGroup>
{textSets.map(textSet => (
......
......@@ -117,6 +117,7 @@
"title": "Subtitle Generator",
"description": "Quickly generate multi-line subtitle images with customizable styles",
"uploadImage": "Upload Background Image",
"dragOrClick": "Drag and drop or click to upload",
"removeImage": "Remove Image",
"globalSettings": "Global Settings",
"fontColor": "Font Color",
......@@ -184,11 +185,17 @@
"bottomRight": "Bottom Right"
},
"download": "Download Image",
"noImage": "Please upload an image"
"noImage": "Please upload an image",
"privacyNote": "This feature runs entirely in your browser with no risk of privacy data leakage. Feel free to use it."
},
"imageBackgroundRemover": {
"title": "Image Background Remover",
"description": "Remove image background"
"description": "Remove the background of images",
"uploadPrompt": "Click or drag to upload an image",
"processing": "Processing...",
"noImage": "Please upload an image first",
"download": "Download Image",
"privacyNote": "This feature runs entirely in your browser with no risk of privacy data leakage. Feel free to use it."
},
"textBehindImage": {
"title": "Text Behind Image",
......@@ -208,6 +215,7 @@
"positionY": "Vertical Position",
"download": "Download Image",
"processing": "Processing...",
"noImage": "Please upload an image first"
"noImage": "Please upload an image first",
"privacyNote": "This feature runs entirely in your browser with no risk of privacy data leakage. Feel free to use it."
}
}
......@@ -117,6 +117,7 @@
"title": "字幕生成ツール",
"description": "複数行の字幕画像を素早く生成、スタイルのカスタマイズも可能",
"uploadImage": "背景画像をアップロード",
"dropOrClick": "ドラッグまたはクリックして画像をアップロード",
"removeImage": "画像を削除",
"globalSettings": "グローバル設定",
"fontColor": "フォントカラー",
......@@ -184,11 +185,17 @@
"bottomRight": "右下"
},
"download": "画像をダウンロード",
"noImage": "画像をアップロードしてください"
"noImage": "画像をアップロードしてください",
"privacyNote": "この機能は完全にブラウザ内で実行され、プライバシーの漏洩リスクはありません。安心してご利用ください。"
},
"imageBackgroundRemover": {
"title": "画像背景の削除",
"description": "画像の背景を削除"
"title": "画像背景削除",
"description": "画像の背景を削除",
"uploadPrompt": "クリックまたはドラッグして画像をアップロード",
"processing": "処理中...",
"noImage": "まず画像をアップロードしてください",
"download": "画像をダウンロード",
"privacyNote": "この機能は完全にブラウザ内で実行され、プライバシーの漏洩リスクはありません。安心してご利用ください。"
},
"textBehindImage": {
"title": "画像の後ろの文字",
......@@ -208,6 +215,7 @@
"positionY": "垂直位置",
"download": "画像をダウンロード",
"processing": "処理中...",
"noImage": "最初に画像をアップロードしてください"
"noImage": "最初に画像をアップロードしてください",
"privacyNote": "この機能は完全にブラウザ内で実行され、プライバシーの漏洩リスクはありません。安心してご利用ください。"
}
}
......@@ -118,6 +118,7 @@
"title": "자막 생성기",
"description": "여러 줄의 자막 이미지를 빠르게 생성, 스타일 커스터마이징 지원",
"uploadImage": "배경 이미지 업로드",
"dropOrClick": "드래그하거나 클릭하여 이미지 업로드",
"removeImage": "이미지 제거",
"globalSettings": "전역 설정",
"fontColor": "글자 색상",
......@@ -185,11 +186,17 @@
"bottomRight": "오른쪽 아래"
},
"download": "이미지 다운로드",
"noImage": "이미지를 업로드하세요"
"noImage": "이미지를 업로드하세요",
"privacyNote": "이 기능은 완전히 브라우저 내에서 실행되며, 개인정보 유출 위험이 없습니다. 안심하고 사용하세요."
},
"imageBackgroundRemover": {
"title": "이미지 배경 제거",
"description": "이미지 배경 제거"
"description": "이미지의 배경을 제거",
"uploadPrompt": "클릭 또는 드래그하여 이미지를 업로드하세요",
"processing": "처리 중...",
"noImage": "먼저 이미지를 업로드하세요",
"download": "이미지 다운로드",
"privacyNote": "이 기능은 완전히 브라우저 내에서 실행되며, 개인정보 유출 위험이 없습니다. 안심하고 사용하세요."
},
"textBehindImage": {
"title": "이미지 뒤의 텍스트",
......@@ -209,6 +216,7 @@
"positionY": "수직 위치",
"download": "이미지 다운로드",
"processing": "처리 중...",
"noImage": "먼저 이미지를 업로드하세요"
"noImage": "먼저 이미지를 업로드하세요",
"privacyNote": "이 기능은 완전히 브라우저 내에서 실행되며, 개인정보 유출 위험이 없습니다. 안심하고 사용하세요."
}
}
......@@ -116,6 +116,7 @@
"title": "字幕拼接",
"description": "快速生成多行字幕图片,支持自定义样式",
"uploadImage": "上传背景图片",
"dropOrClick": "拖拽或点击上传图片",
"removeImage": "移除图片",
"globalSettings": "全局设置",
"fontColor": "字体颜色",
......@@ -183,11 +184,17 @@
"bottomRight": "右下角"
},
"download": "下载图片",
"noImage": "请上传图片"
"noImage": "请上传图片",
"privacyNote": "本功能完全在浏览器本地执行,无隐私数据泄露风险,请放心使用。"
},
"imageBackgroundRemover": {
"title": "图片背景去除",
"description": "去除图片背景"
"description": "去除图片背景",
"uploadPrompt": "点击或拖拽上传图片",
"processing": "正在处理...",
"noImage": "请先上传图片",
"download": "下载图片",
"privacyNote": "本功能完全在浏览器本地执行,无隐私数据泄露风险,请放心使用。"
},
"textBehindImage": {
"title": "文字穿越图片",
......@@ -207,6 +214,7 @@
"positionY": "垂直位置",
"download": "下载图片",
"processing": "正在处理...",
"noImage": "请先上传图片"
"noImage": "请先上传图片",
"privacyNote": "本功能完全在浏览器本地执行,无隐私数据泄露风险,请放心使用。"
}
}
......@@ -10,7 +10,7 @@ const tools = [
{ id: 'subtitleGenerator', icon: '/assets/icon/subtitle2image.png', path: '/subtitle-to-image' },
{ id: 'imageCompressor', icon: '/assets/icon/image-compressor.png', path: '/image-compressor' },
{ id: 'imageWatermark', icon: '/assets/icon/image-watermark.png', path: '/image-watermark' },
{ id: 'imageBackgroundRemover', icon: '/assets/icon/image-background-remover.png', path: 'https://huggingface.co/spaces/briaai/BRIA-RMBG-2.0', external: true },
{ id: 'imageBackgroundRemover', icon: '/assets/icon/image-background-remover.png', path: '/background-remover' },
{ id: 'textBehindImage', icon: '/assets/icon/text-behind-image.png', path: '/text-behind-image' },
{ id: 'latex2image', icon: '/assets/icon/latex2image.png', path: '/latex-to-image' },
......
......@@ -11,7 +11,7 @@ const tools = [
{ id: 'subtitleGenerator', icon: '/assets/icon/subtitle2image.png', path: '/subtitle-to-image' },
{ id: 'imageCompressor', icon: '/assets/icon/image-compressor.png', path: '/image-compressor' },
{ id: 'imageWatermark', icon: '/assets/icon/image-watermark.png', path: '/image-watermark' },
{ id: 'imageBackgroundRemover', icon: '/assets/icon/image-background-remover.png', path: 'https://huggingface.co/spaces/briaai/BRIA-RMBG-2.0', external: true },
{ id: 'imageBackgroundRemover', icon: '/assets/icon/image-background-remover.png', path: '/background-remover' },
{ id: 'textBehindImage', icon: '/assets/icon/text-behind-image.png', path: '/text-behind-image' },
];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment