Commit b091178c authored by fisherdaddy's avatar fisherdaddy

feat: add ID Photo Maker tool with multi-size options and localization support

parent f6659dfd
......@@ -29,6 +29,7 @@ const ImageCompressor = lazy(() => import('./components/ImageCompressor'));
const ImageWatermark = lazy(() => import('./components/ImageWatermark'));
const TextBehindImage = lazy(() => import('./components/TextBehindImage'));
const BackgroundRemover = lazy(() => import('./components/BackgroundRemover'));
const IDPhotoMaker = lazy(() => import('./components/IDPhotoMaker'));
const AnthropicTimeline = lazy(() => import('./components/AnthropicTimeline'));
const DrugsList = lazy(() => import('./components/DrugsList'));
const DeepSeekTimeline = lazy(() => import('./components/DeepSeekTimeline'));
......@@ -74,6 +75,7 @@ function App() {
<Route path="/image-watermark" element={<ImageWatermark />} />
<Route path="/text-behind-image" element={<TextBehindImage />} />
<Route path="/background-remover" element={<BackgroundRemover />} />
<Route path="/id-photo-maker" element={<IDPhotoMaker />} />
<Route path="/deepseek-timeline" element={<DeepSeekTimeline />} />
<Route path="/wechat-formatter" element={<WechatFormatter />} />
<Route path="/image-annotator" element={<ImageAnnotator />} />
......
import React, { useState, useRef, useCallback } from 'react';
import { removeBackground } from "@imgly/background-removal";
import styled from 'styled-components';
import { useTranslation } from '../js/i18n';
import SEO from './SEO';
import { useScrollToTop } from '../hooks/useScrollToTop';
import { usePageLoading } from '../hooks/usePageLoading';
import LoadingOverlay from './LoadingOverlay';
// 证件照标准尺寸 (宽 x 高,单位:像素,按300DPI计算)
const ID_PHOTO_SIZES = {
'small1inch': { width: 260, height: 378, ratio: 260/378 }, // 小一寸 2.2cm × 3.2cm
'1inch': { width: 295, height: 413, ratio: 295/413 }, // 一寸 2.5cm × 3.5cm
'large1inch': { width: 390, height: 567, ratio: 390/567 }, // 大一寸 3.3cm × 4.8cm
'small2inch': { width: 413, height: 532, ratio: 413/532 }, // 小二寸 3.5cm × 4.5cm
'2inch': { width: 413, height: 579, ratio: 413/579 }, // 二寸 3.5cm × 4.9cm
'large2inch': { width: 413, height: 626, ratio: 413/626 }, // 大二寸 3.5cm × 5.3cm
'3inch': { width: 649, height: 991, ratio: 649/991 }, // 三寸 5.5cm × 8.4cm
'4inch': { width: 898, height: 1205, ratio: 898/1205 }, // 四寸 7.6cm × 10.2cm
'5inch': { width: 1051, height: 1500, ratio: 1051/1500 } // 五寸 8.9cm × 12.7cm
};
// 复用容器样式
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 4rem 2rem 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 - 10rem);
@media (max-width: 768px) {
flex-direction: column;
height: auto;
min-height: calc(100vh - 12rem);
}
`;
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;
background: #f8f9fa;
border-radius: 8px;
}
.upload-prompt {
color: #666;
font-size: 1.2rem;
text-align: center;
}
`;
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;
height: 100%;
overflow-y: auto;
`;
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 SizeSelector = styled.div`
.size-label {
font-weight: 600;
margin-bottom: 0.5rem;
color: #374151;
}
.size-select {
width: 100%;
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: white;
color: #374151;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
&:hover, &:focus {
border-color: #6366F1;
outline: none;
}
}
`;
const ProcessButton = styled.button`
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: white;
border: none;
padding: 0.875rem 1.5rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 0.5rem;
&:hover:not(:disabled) {
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 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: 0.75rem;
margin-top: 0.5rem;
border-radius: 0 8px 8px 0;
color: #4F46E5;
font-size: 0.85rem;
line-height: 1.4;
`;
const Instructions = styled.div`
background: rgba(34, 197, 94, 0.1);
border-left: 4px solid #22c55e;
padding: 0.75rem;
margin-top: 0.5rem;
border-radius: 0 8px 8px 0;
color: #15803d;
font-size: 0.85rem;
line-height: 1.4;
white-space: pre-line;
`;
const StatusMessage = styled.div`
text-align: center;
color: #6366F1;
font-weight: 500;
margin: 1rem 0;
`;
function IDPhotoMaker() {
useScrollToTop();
const { t } = useTranslation();
const isPageLoading = usePageLoading();
const [selectedImage, setSelectedImage] = useState(null);
const [processedImage, setProcessedImage] = useState(null);
const [selectedSize, setSelectedSize] = useState('1inch');
const [isProcessing, setIsProcessing] = useState(false);
const [processingStatus, setProcessingStatus] = useState('');
const fileInputRef = useRef(null);
const handleImageUpload = (e) => {
const file = e.target.files?.[0];
if (file) {
const imageUrl = URL.createObjectURL(file);
setSelectedImage(imageUrl);
setProcessedImage(null); // 清除之前的处理结果
}
};
const processImage = useCallback(async () => {
if (!selectedImage) return;
try {
setIsProcessing(true);
setProcessingStatus(t('tools.idPhotoMaker.backgroundRemoval'));
// 步骤1: 去除背景
const imageBlob = await removeBackground(selectedImage);
setProcessingStatus(t('tools.idPhotoMaker.addingBackground'));
// 步骤2: 创建画布,添加白色背景并调整尺寸
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = URL.createObjectURL(imageBlob);
});
setProcessingStatus(t('tools.idPhotoMaker.resizing'));
// 获取目标尺寸
const targetSize = ID_PHOTO_SIZES[selectedSize];
canvas.width = targetSize.width;
canvas.height = targetSize.height;
// 填充白色背景
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 计算图片缩放和位置
const imgRatio = img.width / img.height;
const targetRatio = targetSize.ratio;
let drawWidth, drawHeight, drawX, drawY;
if (imgRatio > targetRatio) {
// 图片更宽,以高度为准
drawHeight = canvas.height;
drawWidth = drawHeight * imgRatio;
drawX = (canvas.width - drawWidth) / 2;
drawY = 0;
} else {
// 图片更高,以宽度为准
drawWidth = canvas.width;
drawHeight = drawWidth / imgRatio;
drawX = 0;
drawY = (canvas.height - drawHeight) / 2;
}
// 绘制图片
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
// 转换为blob
canvas.toBlob((blob) => {
const processedUrl = URL.createObjectURL(blob);
setProcessedImage(processedUrl);
setProcessingStatus('');
}, 'image/png', 1.0);
} catch (error) {
console.error('Error processing image:', error);
setProcessingStatus('');
} finally {
setIsProcessing(false);
}
}, [selectedImage, selectedSize, t]);
const handleDownload = () => {
if (processedImage) {
const link = document.createElement('a');
link.href = processedImage;
link.download = `id-photo-${selectedSize}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
return (
<>
<SEO
title={t('tools.idPhotoMaker.title')}
description={t('tools.idPhotoMaker.description')}
/>
{(isPageLoading || isProcessing) && <LoadingOverlay />}
<Container>
<ContentWrapper>
<ControlPanel>
<Title>{t('tools.idPhotoMaker.title')}</Title>
<ImageUploadArea onClick={() => fileInputRef.current.click()}>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
accept="image/*"
onChange={handleImageUpload}
/>
{t('tools.idPhotoMaker.uploadPrompt')}
</ImageUploadArea>
<SizeSelector>
<div className="size-label">{t('tools.idPhotoMaker.selectSize')}</div>
<select
className="size-select"
value={selectedSize}
onChange={(e) => setSelectedSize(e.target.value)}
>
{Object.entries(ID_PHOTO_SIZES).map(([key, size]) => (
<option key={key} value={key}>
{t(`tools.idPhotoMaker.sizes.${key}`)}
</option>
))}
</select>
</SizeSelector>
<ProcessButton
onClick={processImage}
disabled={!selectedImage || isProcessing}
>
{isProcessing ? t('tools.idPhotoMaker.processing') : t('tools.idPhotoMaker.preview')}
</ProcessButton>
<Instructions>
{t('tools.idPhotoMaker.instructions')}
</Instructions>
<PrivacyNote>
{t('tools.idPhotoMaker.privacyNote')}
</PrivacyNote>
</ControlPanel>
<PreviewArea>
{processedImage && (
<DownloadButton onClick={handleDownload}>
{t('tools.idPhotoMaker.download')}
</DownloadButton>
)}
<div className="preview-content">
{processingStatus && (
<StatusMessage>{processingStatus}</StatusMessage>
)}
{processedImage ? (
<div style={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<img
src={processedImage}
alt="ID Photo"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
border: '1px solid #e5e7eb',
borderRadius: '4px'
}}
/>
</div>
) : selectedImage && !isProcessing ? (
<div style={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<img
src={selectedImage}
alt="Original"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
opacity: 0.7,
border: '1px solid #e5e7eb',
borderRadius: '4px'
}}
/>
</div>
) : (
<div className="upload-prompt">
{t('tools.idPhotoMaker.noImage')}
</div>
)}
</div>
</PreviewArea>
</ContentWrapper>
</Container>
</>
);
}
export default IDPhotoMaker;
\ No newline at end of file
......@@ -283,5 +283,31 @@
"perpetualCalendar": {
"title": "Perpetual Calendar",
"description": "View any month of any year with this perpetual calendar"
},
"idPhotoMaker": {
"title": "ID Photo Maker",
"description": "Convert any photo into standard ID photos with various size options",
"uploadPrompt": "Click or drag to upload photo",
"selectSize": "Select Size",
"sizes": {
"small1inch": "Small 1 Inch (22×32mm)",
"1inch": "1 Inch (25×35mm)",
"large1inch": "Large 1 Inch (33×48mm)",
"small2inch": "Small 2 Inch (35×45mm)",
"2inch": "2 Inch (35×49mm)",
"large2inch": "Large 2 Inch (35×53mm)",
"3inch": "3 Inch (55×84mm)",
"4inch": "4 Inch (76×102mm)",
"5inch": "5 Inch (89×127mm)"
},
"processing": "Processing...",
"noImage": "Please upload a photo first",
"download": "Download ID Photo",
"preview": "Preview",
"backgroundRemoval": "Removing background...",
"addingBackground": "Adding white background...",
"resizing": "Resizing...",
"privacyNote": "This feature runs entirely in your browser with no risk of privacy data leakage. Feel free to use it.",
"instructions": "Instructions:\n1. Upload a portrait photo\n2. Select the desired ID photo size\n3. The system will automatically remove background and add white background\n4. Click download button to save the ID photo"
}
}
......@@ -279,5 +279,31 @@
"CULTURE": "文化",
"OPEN_SOURCE": "オープンソース"
}
},
"idPhotoMaker": {
"title": "証明写真作成",
"description": "任意の写真を標準的な証明写真に変換し、さまざまなサイズに対応",
"uploadPrompt": "クリックまたはドラッグして写真をアップロード",
"selectSize": "サイズを選択",
"sizes": {
"small1inch": "小1インチ (22×32mm)",
"1inch": "1インチ (25×35mm)",
"large1inch": "大1インチ (33×48mm)",
"small2inch": "小2インチ (35×45mm)",
"2inch": "2インチ (35×49mm)",
"large2inch": "大2インチ (35×53mm)",
"3inch": "3インチ (55×84mm)",
"4inch": "4インチ (76×102mm)",
"5inch": "5インチ (89×127mm)"
},
"processing": "処理中...",
"noImage": "先に写真をアップロードしてください",
"download": "証明写真をダウンロード",
"preview": "プレビュー",
"backgroundRemoval": "背景削除中...",
"addingBackground": "白い背景を追加中...",
"resizing": "サイズ調整中...",
"privacyNote": "この機能は完全にブラウザ内で実行され、プライバシーの漏洩リスクはありません。安心してご利用ください。",
"instructions": "使用方法:\n1. 人物写真をアップロード\n2. 希望する証明写真のサイズを選択\n3. システムが自動的に背景を削除し、白い背景を追加\n4. ダウンロードボタンをクリックして証明写真を保存"
}
}
......@@ -280,5 +280,31 @@
"CULTURE": "문화",
"OPEN_SOURCE": "오픈 소스"
}
},
"idPhotoMaker": {
"title": "증명사진 제작",
"description": "임의의 사진을 표준 증명사진으로 변환하며, 다양한 크기 옵션을 제공",
"uploadPrompt": "클릭하거나 드래그하여 사진을 업로드하세요",
"selectSize": "크기 선택",
"sizes": {
"small1inch": "소형 1인치 (22×32mm)",
"1inch": "1인치 (25×35mm)",
"large1inch": "대형 1인치 (33×48mm)",
"small2inch": "소형 2인치 (35×45mm)",
"2inch": "2인치 (35×49mm)",
"large2inch": "대형 2인치 (35×53mm)",
"3inch": "3인치 (55×84mm)",
"4inch": "4인치 (76×102mm)",
"5inch": "5인치 (89×127mm)"
},
"processing": "처리 중...",
"noImage": "먼저 사진을 업로드하세요",
"download": "증명사진 다운로드",
"preview": "미리보기",
"backgroundRemoval": "배경 제거 중...",
"addingBackground": "흰색 배경 추가 중...",
"resizing": "크기 조정 중...",
"privacyNote": "이 기능은 완전히 브라우저 내에서 실행되며, 개인정보 유출 위험이 없습니다. 안심하고 사용하세요.",
"instructions": "사용 방법:\n1. 인물 사진을 업로드하세요\n2. 원하는 증명사진 크기를 선택하세요\n3. 시스템이 자동으로 배경을 제거하고 흰색 배경을 추가합니다\n4. 다운로드 버튼을 클릭하여 증명사진을 저장하세요"
}
}
......@@ -285,5 +285,31 @@
"perpetualCalendar": {
"title": "万年历",
"description": "查看任意年份任意月份的日历"
},
"idPhotoMaker": {
"title": "证件照制作",
"description": "将任意照片制作成标准证件照,支持多种尺寸规格",
"uploadPrompt": "点击或拖拽上传照片",
"selectSize": "选择尺寸",
"sizes": {
"small1inch": "小一寸 (22×32mm)",
"1inch": "一寸 (25×35mm)",
"large1inch": "大一寸 (33×48mm)",
"small2inch": "小二寸 (35×45mm)",
"2inch": "二寸 (35×49mm)",
"large2inch": "大二寸 (35×53mm)",
"3inch": "三寸 (55×84mm)",
"4inch": "四寸 (76×102mm)",
"5inch": "五寸 (89×127mm)"
},
"processing": "正在处理...",
"noImage": "请先上传照片",
"download": "下载证件照",
"preview": "预览效果",
"backgroundRemoval": "背景去除中...",
"addingBackground": "添加白色背景...",
"resizing": "调整尺寸...",
"privacyNote": "本功能完全在浏览器本地执行,无隐私数据泄露风险,请放心使用。",
"instructions": "操作说明:\n1. 上传一张人像照片\n2. 选择所需的证件照尺寸\n3. 系统会自动去除背景并添加白色背景\n4. 点击下载按钮保存证件照"
}
}
......@@ -8,6 +8,7 @@ const tools = [
{ id: 'quoteCard', icon: '/assets/icon/quotecard.png', path: '/quote-card' },
{ id: 'markdown2image', icon: '/assets/icon/markdown2image.png', path: '/markdown-to-image' },
{ id: 'wechatFormatter', icon: '/assets/icon/editor.png', path: '/wechat-formatter' },
{ id: 'idPhotoMaker', icon: '/assets/icon/idcard.png', path: '/id-photo-maker' },
{ id: 'perpetualCalendar', icon: '/assets/icon/calendar.jpg', path: '/perpetual-calendar' },
{ id: 'imageAnnotator', icon: '/assets/icon/image-annotator.png', path: '/image-annotator' },
{ id: 'subtitleGenerator', icon: '/assets/icon/subtitle2image.png', path: '/subtitle-to-image' },
......
......@@ -12,6 +12,7 @@ const tools = [
{ 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: '/background-remover' },
{ id: 'idPhotoMaker', icon: '/assets/icon/idcard.png', path: '/id-photo-maker' },
{ id: 'textBehindImage', icon: '/assets/icon/text-behind-image.png', path: '/text-behind-image' },
{ id: 'imageAnnotator', icon: '/assets/icon/image-annotator.png', path: '/image-annotator' },
];
......
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