Commit 3db13667 authored by fisherdaddy's avatar fisherdaddy

feature: 新增text behind image 功能

parent e7b159c4
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@imgly/background-removal": "^1.5.5",
"@react-oauth/google": "^0.12.1", "@react-oauth/google": "^0.12.1",
"antd": "^5.21.6", "antd": "^5.21.6",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
......
No preview for this file type
...@@ -25,6 +25,7 @@ const TextDiff = lazy(() => import('./components/TextDiff')); ...@@ -25,6 +25,7 @@ const TextDiff = lazy(() => import('./components/TextDiff'));
const SubtitleGenerator = lazy(() => import('./components/SubtitleGenerator')); const SubtitleGenerator = lazy(() => import('./components/SubtitleGenerator'));
const ImageCompressor = lazy(() => import('./components/ImageCompressor')); const ImageCompressor = lazy(() => import('./components/ImageCompressor'));
const ImageWatermark = lazy(() => import('./components/ImageWatermark')); const ImageWatermark = lazy(() => import('./components/ImageWatermark'));
const TextBehindImage = lazy(() => import('./components/TextBehindImage'));
function App() { function App() {
return ( return (
...@@ -56,7 +57,7 @@ function App() { ...@@ -56,7 +57,7 @@ function App() {
<Route path="/subtitle-to-image" element={<SubtitleGenerator />} /> <Route path="/subtitle-to-image" element={<SubtitleGenerator />} />
<Route path="/image-compressor" element={<ImageCompressor />} /> <Route path="/image-compressor" element={<ImageCompressor />} />
<Route path="/image-watermark" element={<ImageWatermark />} /> <Route path="/image-watermark" element={<ImageWatermark />} />
<Route path="/text-behind-image" element={<TextBehindImage />} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
......
import React, { useState, useRef, useEffect } from 'react';
import { removeBackground } from "@imgly/background-removal";
import styled from 'styled-components';
import { useTranslation } from '../js/i18n';
import '../styles/fonts.css';
// 复用现有的基础容器样式
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 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;
max-height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(99, 102, 241, 0.3);
border-radius: 4px;
&:hover {
background: rgba(99, 102, 241, 0.5);
}
}
`;
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: 1;
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;
.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 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 InputWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 0.4rem;
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
`;
const Label = styled.label`
font-size: 0.9rem;
color: #4B5563;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
span {
color: #9CA3AF;
font-size: 0.8rem;
}
`;
const Input = styled.input`
width: 100%;
padding: 0.5rem;
border: 1px solid #dadce0;
border-radius: 4px;
font-size: 14px;
`;
const Button = styled.button`
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: white;
border: none;
padding: 1rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
&: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 Canvas = styled.canvas`
max-width: 100%;
height: auto;
`;
const DownloadButton = styled.button`
position: absolute;
top: 1rem;
right: 1rem;
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;
z-index: 3;
&: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 SettingsGroup = styled.div`
background: rgba(255, 255, 255, 0.5);
border-radius: 12px;
padding: 1.2rem;
margin-bottom: 1.2rem;
border: 1px solid rgba(99, 102, 241, 0.1);
&:last-child {
margin-bottom: 0;
}
`;
const GroupTitle = styled.h3`
font-size: 1.1rem;
color: #4F46E5;
margin-bottom: 1rem;
font-weight: 600;
`;
function TextBehindImage() {
const { t } = useTranslation();
const [selectedImage, setSelectedImage] = useState(null);
const [isImageSetupDone, setIsImageSetupDone] = useState(false);
const [removedBgImageUrl, setRemovedBgImageUrl] = useState(null);
const [textSets, setTextSets] = useState([{
id: 1,
text: 'EDIT',
fontSize: 200,
fontWeight: 800,
rotation: 0,
color: '#FFFFFF',
opacity: 1,
position: { x: 50, y: 50 }
}]);
const canvasRef = useRef(null);
const fileInputRef = useRef(null);
const [loading, setLoading] = useState(false);
const [imageLoading, setImageLoading] = useState(false);
const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 });
// 处理图片上传
const handleImageUpload = async (e) => {
const file = e.target.files?.[0];
if (file) {
try {
setImageLoading(true);
const imageUrl = URL.createObjectURL(file);
// 获取图片尺寸
await getImageDimensions(imageUrl);
setSelectedImage(imageUrl);
await setupImage(imageUrl);
} catch (error) {
console.error('Error uploading image:', error);
} finally {
setImageLoading(false);
}
}
};
// 获取图片尺寸
const getImageDimensions = (imageUrl) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
setImageDimensions({
width: img.width,
height: img.height
});
resolve({ width: img.width, height: img.height });
};
img.onerror = reject;
img.src = imageUrl;
});
};
// 处理图片背景移除
const setupImage = async (imageUrl) => {
try {
const imageBlob = await removeBackground(imageUrl);
const url = URL.createObjectURL(imageBlob);
setRemovedBgImageUrl(url);
setIsImageSetupDone(true);
} catch (error) {
console.error('Error processing image:', error);
}
};
// 更新文本属性
const updateTextSet = (id, attribute, value) => {
setTextSets(prev => prev.map(set =>
set.id === id ? { ...set, [attribute]: value } : set
));
};
// 处理画布点击,更新文本位置
const handleCanvasClick = (e) => {
if (!canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
// 更新第一个文本集的位置(可以扩展为更新当前选中的文本集)
updateTextSet(textSets[0].id, 'position', { x, y });
};
// 计算图片显示尺寸以适应预览区域,减少空白
const calculateImageDimensions = () => {
if (!imageDimensions.width || !imageDimensions.height) return {};
const previewArea = document.querySelector('.preview-area');
if (!previewArea) return {};
const containerWidth = previewArea.clientWidth - 40; // 减少左右边距
const containerHeight = previewArea.clientHeight - 40; // 减少上下边距
const imageRatio = imageDimensions.width / imageDimensions.height;
const containerRatio = containerWidth / containerHeight;
let width, height;
if (imageRatio > containerRatio) {
width = containerWidth * 0.9; // 稍微缩小以避免完全贴边
height = containerWidth * 0.9 / imageRatio;
} else {
height = containerHeight * 0.9;
width = containerHeight * 0.9 * imageRatio;
}
return {
width: `${width}px`,
height: `${height}px`
};
};
// 添加下载分辨率选择
const handleDownload = () => {
if (!canvasRef.current || !isImageSetupDone) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const bgImg = new Image();
bgImg.crossOrigin = "anonymous";
bgImg.onload = () => {
// 设置画布尺寸为原始图片尺寸
canvas.width = bgImg.width;
canvas.height = bgImg.height;
// 获取预览图元素和尺寸
const previewImg = document.querySelector('img[alt="Background"]');
if (!previewImg) return;
// 计算预览图和原始图片的比例
const scaleRatio = bgImg.height / previewImg.offsetHeight;
// 绘制背景图
ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height);
// 绘制文本
textSets.forEach(textSet => {
ctx.save();
// 计算位置
const x = (textSet.position.x / 100) * canvas.width;
const y = (textSet.position.y / 100) * canvas.height;
// 根据比例计算字体大小
const scaledFontSize = textSet.fontSize * scaleRatio;
// 设置文本样式
ctx.font = `${textSet.fontWeight} ${scaledFontSize}px Inter`;
ctx.fillStyle = textSet.color;
ctx.globalAlpha = textSet.opacity;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 移动到指定位置并旋转
ctx.translate(x, y);
ctx.rotate((textSet.rotation * Math.PI) / 180);
// 绘制文本
ctx.fillText(textSet.text, 0, 0);
ctx.restore();
});
// 绘制移除背景后的图片
if (removedBgImageUrl) {
const fgImg = new Image();
fgImg.crossOrigin = "anonymous";
fgImg.onload = () => {
ctx.drawImage(fgImg, 0, 0, canvas.width, canvas.height);
// 创建下载链接
const dataUrl = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'text-behind-image.png';
link.href = dataUrl;
link.click();
};
fgImg.src = removedBgImageUrl;
}
};
bgImg.src = selectedImage;
};
// 添加一个检测文本宽度的函数
const measureText = (text, fontSize, fontWeight) => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `${fontWeight} ${fontSize}px Inter`;
return context.measureText(text).width;
};
useEffect(() => {
// 预加载 Inter 字体
const font = new FontFace('Inter', 'url(/path/to/your/Inter-font.woff2)');
font.load().then(() => {
document.fonts.add(font);
});
}, []);
return (
<Container>
<ContentWrapper>
<ControlPanel>
<Title>{t('tools.textBehindImage.title')}</Title>
<SettingsGroup>
<GroupTitle>{t('tools.textBehindImage.imageUpload')}</GroupTitle>
<ImageUploadArea onClick={() => fileInputRef.current.click()}>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
accept="image/*"
onChange={handleImageUpload}
/>
{t('tools.textBehindImage.uploadPrompt')}
</ImageUploadArea>
</SettingsGroup>
{textSets.map(textSet => (
<SettingsGroup key={textSet.id}>
<GroupTitle>{t('tools.textBehindImage.textSettings')}</GroupTitle>
<InputWrapper>
<Label>
{t('tools.textBehindImage.text')}
<span>{textSet.text.length} {t('tools.textBehindImage.characters')}</span>
</Label>
<Input
type="text"
value={textSet.text}
onChange={(e) => updateTextSet(textSet.id, 'text', e.target.value)}
placeholder={t('tools.textBehindImage.textPlaceholder')}
/>
</InputWrapper>
<InputWrapper>
<Label>
{t('tools.textBehindImage.fontSize')}
<span>{textSet.fontSize}px</span>
</Label>
<Input
type="number"
value={textSet.fontSize}
onChange={(e) => updateTextSet(textSet.id, 'fontSize', Number(e.target.value))}
/>
</InputWrapper>
<InputWrapper>
<Label>
{t('tools.textBehindImage.fontWeight')}
<span>{textSet.fontWeight}</span>
</Label>
<Input
type="range"
min="100"
max="900"
step="100"
value={textSet.fontWeight}
onChange={(e) => updateTextSet(textSet.id, 'fontWeight', Number(e.target.value))}
/>
</InputWrapper>
<InputWrapper>
<Label>
{t('tools.textBehindImage.rotation')}
<span>{textSet.rotation}°</span>
</Label>
<Input
type="range"
min="-180"
max="180"
value={textSet.rotation}
onChange={(e) => updateTextSet(textSet.id, 'rotation', Number(e.target.value))}
/>
</InputWrapper>
<InputWrapper>
<Label>{t('tools.textBehindImage.color')}</Label>
<Input
type="color"
value={textSet.color}
onChange={(e) => updateTextSet(textSet.id, 'color', e.target.value)}
/>
</InputWrapper>
<InputWrapper>
<Label>
{t('tools.textBehindImage.opacity')}
<span>{textSet.opacity}</span>
</Label>
<Input
type="range"
min="0"
max="1"
step="0.1"
value={textSet.opacity}
onChange={(e) => updateTextSet(textSet.id, 'opacity', Number(e.target.value))}
/>
</InputWrapper>
<InputWrapper>
<Label>
{t('tools.textBehindImage.positionX')}
<span>{textSet.position.x}%</span>
</Label>
<Input
type="range"
min="0"
max="100"
value={textSet.position.x}
onChange={(e) => updateTextSet(textSet.id, 'position', {
...textSet.position,
x: Number(e.target.value)
})}
/>
</InputWrapper>
<InputWrapper>
<Label>
{t('tools.textBehindImage.positionY')}
<span>{textSet.position.y}%</span>
</Label>
<Input
type="range"
min="0"
max="100"
value={textSet.position.y}
onChange={(e) => updateTextSet(textSet.id, 'position', {
...textSet.position,
y: Number(e.target.value)
})}
/>
</InputWrapper>
</SettingsGroup>
))}
</ControlPanel>
<PreviewArea className="preview-area">
<div className="preview-content">
{imageLoading ? (
<div className="loading-container">
<span> {t('tools.textBehindImage.processing')}</span>
</div>
) : selectedImage ? (
<div style={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<DownloadButton onClick={() => handleDownload('original')}>
{t('tools.textBehindImage.download')}
</DownloadButton>
<img
src={selectedImage}
alt="Background"
style={{
...calculateImageDimensions(),
objectFit: 'contain',
position: 'relative'
}}
/>
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none'
}}>
{textSets.map(textSet => {
const imageElement = document.querySelector('img[alt="Background"]');
const imageRect = imageElement?.getBoundingClientRect();
const imageWidth = imageRect?.width || 0;
// 计算文本的中心字符位置
const text = textSet.text;
const centerIndex = Math.floor(text.length / 2);
const leftPart = text.substring(0, centerIndex);
const centerChar = text.charAt(centerIndex);
const rightPart = text.substring(centerIndex + 1);
return (
<div
key={textSet.id}
style={{
position: 'absolute',
left: `${textSet.position.x}%`,
top: `${textSet.position.y}%`,
transform: `translate(-50%, -50%) rotate(${textSet.rotation}deg)`,
color: textSet.color,
fontSize: `${textSet.fontSize}px`,
fontFamily: 'Inter',
fontWeight: textSet.fontWeight,
opacity: textSet.opacity,
zIndex: 1,
whiteSpace: 'pre',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transformOrigin: 'center',
}}
>
<span>{leftPart}</span>
<span style={{
display: 'inline-block',
position: 'relative'
}}>{centerChar}</span>
<span>{rightPart}</span>
</div>
);
})}
</div>
{removedBgImageUrl && (
<img
src={removedBgImageUrl}
alt="Foreground"
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
...calculateImageDimensions(),
objectFit: 'contain',
zIndex: 2
}}
/>
)}
</div>
) : (
<div className="upload-prompt">
{t('tools.textBehindImage.noImage')}
</div>
)}
<canvas ref={canvasRef} style={{ display: 'none' }} />
</div>
</PreviewArea>
</ContentWrapper>
</Container>
);
}
export default TextBehindImage;
\ No newline at end of file
...@@ -189,5 +189,25 @@ ...@@ -189,5 +189,25 @@
"imageBackgroundRemover": { "imageBackgroundRemover": {
"title": "Image Background Remover", "title": "Image Background Remover",
"description": "Remove image background" "description": "Remove image background"
},
"textBehindImage": {
"title": "Text Behind Image",
"description": "Add text between the main subject and background of the image to create a 3D effect",
"imageUpload": "Image Upload",
"uploadPrompt": "Click or drag to upload an image",
"textSettings": "Text Settings",
"text": "Text Content",
"textPlaceholder": "Enter text",
"characters": "characters",
"fontSize": "Font Size",
"fontWeight": "Font Weight",
"rotation": "Rotation Angle",
"color": "Color",
"opacity": "Opacity",
"positionX": "Horizontal Position",
"positionY": "Vertical Position",
"download": "Download Image",
"processing": "Processing...",
"noImage": "Please upload an image first"
} }
} }
...@@ -189,5 +189,25 @@ ...@@ -189,5 +189,25 @@
"imageBackgroundRemover": { "imageBackgroundRemover": {
"title": "画像背景の削除", "title": "画像背景の削除",
"description": "画像の背景を削除" "description": "画像の背景を削除"
},
"textBehindImage": {
"title": "画像の後ろの文字",
"description": "画像のメイン主体と背景の間に文字を追加し、3D効果を作成します",
"imageUpload": "画像アップロード",
"uploadPrompt": "クリックまたはドラッグして画像をアップロード",
"textSettings": "文字設定",
"text": "文字内容",
"textPlaceholder": "文字を入力してください",
"characters": "文字数",
"fontSize": "フォントサイズ",
"fontWeight": "フォントの太さ",
"rotation": "回転角度",
"color": "色",
"opacity": "不透明度",
"positionX": "水平位置",
"positionY": "垂直位置",
"download": "画像をダウンロード",
"processing": "処理中...",
"noImage": "最初に画像をアップロードしてください"
} }
} }
...@@ -190,5 +190,25 @@ ...@@ -190,5 +190,25 @@
"imageBackgroundRemover": { "imageBackgroundRemover": {
"title": "이미지 배경 제거", "title": "이미지 배경 제거",
"description": "이미지 배경 제거" "description": "이미지 배경 제거"
},
"textBehindImage": {
"title": "이미지 뒤의 텍스트",
"description": "이미지의 주요 객체와 배경 사이에 텍스트를 추가하여 3D 효과를 만듭니다",
"imageUpload": "이미지 업로드",
"uploadPrompt": "클릭하거나 드래그하여 이미지를 업로드하세요",
"textSettings": "텍스트 설정",
"text": "텍스트 내용",
"textPlaceholder": "텍스트를 입력하세요",
"characters": "문자 수",
"fontSize": "글자 크기",
"fontWeight": "글자 굵기",
"rotation": "회전 각도",
"color": "색상",
"opacity": "불투명도",
"positionX": "수평 위치",
"positionY": "수직 위치",
"download": "이미지 다운로드",
"processing": "처리 중...",
"noImage": "먼저 이미지를 업로드하세요"
} }
} }
...@@ -188,5 +188,25 @@ ...@@ -188,5 +188,25 @@
"imageBackgroundRemover": { "imageBackgroundRemover": {
"title": "图片背景去除", "title": "图片背景去除",
"description": "去除图片背景" "description": "去除图片背景"
},
"textBehindImage": {
"title": "文字穿越图片",
"description": "在图片主体与背景之间添加文字,创造3D效果",
"imageUpload": "图片上传",
"uploadPrompt": "点击或拖拽上传图片",
"textSettings": "文字设置",
"text": "文字内容",
"textPlaceholder": "请输入文字",
"characters": "个字符",
"fontSize": "字体大小",
"fontWeight": "字体粗细",
"rotation": "旋转角度",
"color": "颜色",
"opacity": "透明度",
"positionX": "水平位置",
"positionY": "垂直位置",
"download": "下载图片",
"processing": "正在处理...",
"noImage": "请先上传图片"
} }
} }
...@@ -11,6 +11,7 @@ const tools = [ ...@@ -11,6 +11,7 @@ const tools = [
{ id: 'imageCompressor', icon: '/assets/icon/image-compressor.png', path: '/image-compressor' }, { id: 'imageCompressor', icon: '/assets/icon/image-compressor.png', path: '/image-compressor' },
{ id: 'imageWatermark', icon: '/assets/icon/image-watermark.png', path: '/image-watermark' }, { 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: 'https://huggingface.co/spaces/briaai/BRIA-RMBG-2.0', external: true },
{ id: 'textBehindImage', icon: '/assets/icon/text-behind-image.png', path: '/text-behind-image' },
{ id: 'latex2image', icon: '/assets/icon/latex2image.png', path: '/latex-to-image' }, { id: 'latex2image', icon: '/assets/icon/latex2image.png', path: '/latex-to-image' },
{ id: 'jsonFormatter', icon: '/assets/icon/json-format.png', path: '/json-formatter' }, { id: 'jsonFormatter', icon: '/assets/icon/json-format.png', path: '/json-formatter' },
......
...@@ -12,6 +12,7 @@ const tools = [ ...@@ -12,6 +12,7 @@ const tools = [
{ id: 'imageCompressor', icon: '/assets/icon/image-compressor.png', path: '/image-compressor' }, { id: 'imageCompressor', icon: '/assets/icon/image-compressor.png', path: '/image-compressor' },
{ id: 'imageWatermark', icon: '/assets/icon/image-watermark.png', path: '/image-watermark' }, { 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: 'https://huggingface.co/spaces/briaai/BRIA-RMBG-2.0', external: true },
{ id: 'textBehindImage', icon: '/assets/icon/text-behind-image.png', path: '/text-behind-image' },
]; ];
const ImageTools = () => { const ImageTools = () => {
......
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
/* 可以添加其他粗重的字体作为备选 */
@font-face {
font-family: 'Inter';
src: url('https://fonts.googleapis.com/css2?family=Inter:wght@800&display=swap');
font-weight: 800;
font-style: normal;
}
/* 添加其他粗重字体选项 */
@font-face {
font-family: 'Noto Sans';
src: url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@700;800;900&display=swap');
}
@font-face {
font-family: 'Montserrat';
src: url('https://fonts.googleapis.com/css2?family=Montserrat:wght@700;800;900&display=swap');
}
\ No newline at end of file
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