|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useCallback, useRef } from 'react'; |
|
|
import axios from 'axios'; |
|
|
|
|
|
|
|
|
const API_BASE_URL = process.env.REACT_APP_BACKGROUNDFX_API_URL || 'https://api.backgroundfx.pro/v1'; |
|
|
const API_KEY = process.env.REACT_APP_BACKGROUNDFX_API_KEY; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const useBackgroundFX = (apiKey) => { |
|
|
const [isProcessing, setIsProcessing] = useState(false); |
|
|
const [progress, setProgress] = useState(0); |
|
|
const [error, setError] = useState(null); |
|
|
|
|
|
const client = useRef( |
|
|
axios.create({ |
|
|
baseURL: API_BASE_URL, |
|
|
headers: { |
|
|
'Authorization': `Bearer ${apiKey}`, |
|
|
}, |
|
|
}) |
|
|
); |
|
|
|
|
|
const removeBackground = useCallback(async (file, options = {}) => { |
|
|
setIsProcessing(true); |
|
|
setError(null); |
|
|
setProgress(0); |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('file', file); |
|
|
Object.keys(options).forEach(key => { |
|
|
formData.append(key, options[key]); |
|
|
}); |
|
|
|
|
|
try { |
|
|
const response = await client.current.post('/process/remove-background', formData, { |
|
|
onUploadProgress: (progressEvent) => { |
|
|
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); |
|
|
setProgress(percentCompleted); |
|
|
}, |
|
|
}); |
|
|
|
|
|
setIsProcessing(false); |
|
|
setProgress(100); |
|
|
return response.data; |
|
|
} catch (err) { |
|
|
setError(err.response?.data?.message || err.message); |
|
|
setIsProcessing(false); |
|
|
throw err; |
|
|
} |
|
|
}, []); |
|
|
|
|
|
return { |
|
|
removeBackground, |
|
|
isProcessing, |
|
|
progress, |
|
|
error, |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const DropZone = ({ onDrop, disabled }) => { |
|
|
const [isDragging, setIsDragging] = useState(false); |
|
|
const fileInputRef = useRef(null); |
|
|
|
|
|
const handleDragEnter = useCallback((e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
setIsDragging(true); |
|
|
}, []); |
|
|
|
|
|
const handleDragLeave = useCallback((e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
setIsDragging(false); |
|
|
}, []); |
|
|
|
|
|
const handleDragOver = useCallback((e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
}, []); |
|
|
|
|
|
const handleDrop = useCallback((e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
setIsDragging(false); |
|
|
|
|
|
const files = e.dataTransfer.files; |
|
|
if (files && files[0]) { |
|
|
onDrop(files[0]); |
|
|
} |
|
|
}, [onDrop]); |
|
|
|
|
|
const handleFileSelect = useCallback((e) => { |
|
|
const files = e.target.files; |
|
|
if (files && files[0]) { |
|
|
onDrop(files[0]); |
|
|
} |
|
|
}, [onDrop]); |
|
|
|
|
|
return ( |
|
|
<div |
|
|
className={`dropzone ${isDragging ? 'dragging' : ''} ${disabled ? 'disabled' : ''}`} |
|
|
onDragEnter={handleDragEnter} |
|
|
onDragLeave={handleDragLeave} |
|
|
onDragOver={handleDragOver} |
|
|
onDrop={handleDrop} |
|
|
onClick={() => !disabled && fileInputRef.current?.click()} |
|
|
style={{ |
|
|
border: '2px dashed #ccc', |
|
|
borderRadius: '8px', |
|
|
padding: '40px', |
|
|
textAlign: 'center', |
|
|
cursor: disabled ? 'not-allowed' : 'pointer', |
|
|
backgroundColor: isDragging ? '#f0f0f0' : 'white', |
|
|
transition: 'all 0.3s ease', |
|
|
}} |
|
|
> |
|
|
<input |
|
|
ref={fileInputRef} |
|
|
type="file" |
|
|
accept="image/*" |
|
|
onChange={handleFileSelect} |
|
|
style={{ display: 'none' }} |
|
|
disabled={disabled} |
|
|
/> |
|
|
|
|
|
<div className="dropzone-content"> |
|
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor"> |
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> |
|
|
<polyline points="17 8 12 3 7 8" /> |
|
|
<line x1="12" y1="3" x2="12" y2="15" /> |
|
|
</svg> |
|
|
<p style={{ marginTop: '16px', fontSize: '18px', fontWeight: '500' }}> |
|
|
{disabled ? 'Processing...' : 'Drop image here or click to browse'} |
|
|
</p> |
|
|
<p style={{ marginTop: '8px', fontSize: '14px', color: '#666' }}> |
|
|
Supports PNG, JPG, WebP (max 50MB) |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const ImagePreview = ({ originalUrl, processedUrl, maskUrl }) => { |
|
|
const [viewMode, setViewMode] = useState('processed'); |
|
|
|
|
|
return ( |
|
|
<div className="image-preview" style={{ marginTop: '20px' }}> |
|
|
<div className="preview-controls" style={{ marginBottom: '16px' }}> |
|
|
<button |
|
|
onClick={() => setViewMode('original')} |
|
|
style={{ |
|
|
padding: '8px 16px', |
|
|
marginRight: '8px', |
|
|
backgroundColor: viewMode === 'original' ? '#007bff' : '#f0f0f0', |
|
|
color: viewMode === 'original' ? 'white' : 'black', |
|
|
border: 'none', |
|
|
borderRadius: '4px', |
|
|
cursor: 'pointer', |
|
|
}} |
|
|
> |
|
|
Original |
|
|
</button> |
|
|
<button |
|
|
onClick={() => setViewMode('processed')} |
|
|
style={{ |
|
|
padding: '8px 16px', |
|
|
marginRight: '8px', |
|
|
backgroundColor: viewMode === 'processed' ? '#007bff' : '#f0f0f0', |
|
|
color: viewMode === 'processed' ? 'white' : 'black', |
|
|
border: 'none', |
|
|
borderRadius: '4px', |
|
|
cursor: 'pointer', |
|
|
}} |
|
|
> |
|
|
Processed |
|
|
</button> |
|
|
{maskUrl && ( |
|
|
<button |
|
|
onClick={() => setViewMode('mask')} |
|
|
style={{ |
|
|
padding: '8px 16px', |
|
|
backgroundColor: viewMode === 'mask' ? '#007bff' : '#f0f0f0', |
|
|
color: viewMode === 'mask' ? 'white' : 'black', |
|
|
border: 'none', |
|
|
borderRadius: '4px', |
|
|
cursor: 'pointer', |
|
|
}} |
|
|
> |
|
|
Mask |
|
|
</button> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
<div className="preview-image" style={{ position: 'relative', display: 'inline-block' }}> |
|
|
{viewMode === 'original' && originalUrl && ( |
|
|
<img src={originalUrl} alt="Original" style={{ maxWidth: '100%', height: 'auto' }} /> |
|
|
)} |
|
|
{viewMode === 'processed' && processedUrl && ( |
|
|
<img src={processedUrl} alt="Processed" style={{ maxWidth: '100%', height: 'auto' }} /> |
|
|
)} |
|
|
{viewMode === 'mask' && maskUrl && ( |
|
|
<img src={maskUrl} alt="Mask" style={{ maxWidth: '100%', height: 'auto' }} /> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const ProgressBar = ({ progress }) => { |
|
|
return ( |
|
|
<div className="progress-bar" style={{ marginTop: '20px' }}> |
|
|
<div |
|
|
style={{ |
|
|
width: '100%', |
|
|
height: '8px', |
|
|
backgroundColor: '#f0f0f0', |
|
|
borderRadius: '4px', |
|
|
overflow: 'hidden', |
|
|
}} |
|
|
> |
|
|
<div |
|
|
style={{ |
|
|
width: `${progress}%`, |
|
|
height: '100%', |
|
|
backgroundColor: '#007bff', |
|
|
transition: 'width 0.3s ease', |
|
|
}} |
|
|
/> |
|
|
</div> |
|
|
<p style={{ marginTop: '8px', textAlign: 'center', fontSize: '14px' }}> |
|
|
{progress}% Complete |
|
|
</p> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const BackgroundRemover = ({ apiKey = API_KEY }) => { |
|
|
const [originalImage, setOriginalImage] = useState(null); |
|
|
const [processedImage, setProcessedImage] = useState(null); |
|
|
const [maskImage, setMaskImage] = useState(null); |
|
|
const [selectedQuality, setSelectedQuality] = useState('high'); |
|
|
const [selectedModel, setSelectedModel] = useState('auto'); |
|
|
const [returnMask, setReturnMask] = useState(false); |
|
|
|
|
|
const { removeBackground, isProcessing, progress, error } = useBackgroundFX(apiKey); |
|
|
|
|
|
const handleFileDrop = useCallback(async (file) => { |
|
|
|
|
|
if (!file.type.startsWith('image/')) { |
|
|
alert('Please upload an image file'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (file.size > 50 * 1024 * 1024) { |
|
|
alert('File size must be less than 50MB'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = (e) => { |
|
|
setOriginalImage(e.target.result); |
|
|
}; |
|
|
reader.readAsDataURL(file); |
|
|
|
|
|
|
|
|
try { |
|
|
const result = await removeBackground(file, { |
|
|
quality: selectedQuality, |
|
|
model: selectedModel, |
|
|
return_mask: returnMask, |
|
|
}); |
|
|
|
|
|
setProcessedImage(result.image); |
|
|
if (result.mask) { |
|
|
setMaskImage(result.mask); |
|
|
} |
|
|
} catch (err) { |
|
|
console.error('Processing failed:', err); |
|
|
} |
|
|
}, [removeBackground, selectedQuality, selectedModel, returnMask]); |
|
|
|
|
|
const handleDownload = useCallback((url, filename) => { |
|
|
const link = document.createElement('a'); |
|
|
link.href = url; |
|
|
link.download = filename; |
|
|
document.body.appendChild(link); |
|
|
link.click(); |
|
|
document.body.removeChild(link); |
|
|
}, []); |
|
|
|
|
|
const handleReset = useCallback(() => { |
|
|
setOriginalImage(null); |
|
|
setProcessedImage(null); |
|
|
setMaskImage(null); |
|
|
}, []); |
|
|
|
|
|
return ( |
|
|
<div className="background-remover" style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}> |
|
|
<h1 style={{ textAlign: 'center', marginBottom: '32px' }}> |
|
|
BackgroundFX Pro - React Example |
|
|
</h1> |
|
|
|
|
|
{/* Settings */} |
|
|
<div className="settings" style={{ marginBottom: '24px' }}> |
|
|
<div style={{ marginBottom: '16px' }}> |
|
|
<label style={{ marginRight: '16px' }}> |
|
|
Quality: |
|
|
<select |
|
|
value={selectedQuality} |
|
|
onChange={(e) => setSelectedQuality(e.target.value)} |
|
|
style={{ marginLeft: '8px', padding: '4px 8px' }} |
|
|
> |
|
|
<option value="low">Low</option> |
|
|
<option value="medium">Medium</option> |
|
|
<option value="high">High</option> |
|
|
<option value="ultra">Ultra</option> |
|
|
</select> |
|
|
</label> |
|
|
|
|
|
<label style={{ marginRight: '16px' }}> |
|
|
Model: |
|
|
<select |
|
|
value={selectedModel} |
|
|
onChange={(e) => setSelectedModel(e.target.value)} |
|
|
style={{ marginLeft: '8px', padding: '4px 8px' }} |
|
|
> |
|
|
<option value="auto">Auto</option> |
|
|
<option value="rembg">RemBG</option> |
|
|
<option value="u2net">U2Net</option> |
|
|
<option value="sam2">SAM2</option> |
|
|
</select> |
|
|
</label> |
|
|
|
|
|
<label> |
|
|
<input |
|
|
type="checkbox" |
|
|
checked={returnMask} |
|
|
onChange={(e) => setReturnMask(e.target.checked)} |
|
|
style={{ marginRight: '8px' }} |
|
|
/> |
|
|
Return Mask |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Drop Zone */} |
|
|
{!originalImage && ( |
|
|
<DropZone onDrop={handleFileDrop} disabled={isProcessing} /> |
|
|
)} |
|
|
|
|
|
{/* Progress Bar */} |
|
|
{isProcessing && <ProgressBar progress={progress} />} |
|
|
|
|
|
{/* Error Message */} |
|
|
{error && ( |
|
|
<div |
|
|
style={{ |
|
|
marginTop: '16px', |
|
|
padding: '12px', |
|
|
backgroundColor: '#fee', |
|
|
border: '1px solid #fcc', |
|
|
borderRadius: '4px', |
|
|
color: '#c00', |
|
|
}} |
|
|
> |
|
|
Error: {error} |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Image Preview */} |
|
|
{(originalImage || processedImage) && ( |
|
|
<ImagePreview |
|
|
originalUrl={originalImage} |
|
|
processedUrl={processedImage} |
|
|
maskUrl={maskImage} |
|
|
/> |
|
|
)} |
|
|
|
|
|
{/* Action Buttons */} |
|
|
{processedImage && ( |
|
|
<div className="actions" style={{ marginTop: '24px', textAlign: 'center' }}> |
|
|
<button |
|
|
onClick={() => handleDownload(processedImage, 'processed.png')} |
|
|
style={{ |
|
|
padding: '10px 20px', |
|
|
marginRight: '8px', |
|
|
backgroundColor: '#28a745', |
|
|
color: 'white', |
|
|
border: 'none', |
|
|
borderRadius: '4px', |
|
|
cursor: 'pointer', |
|
|
}} |
|
|
> |
|
|
Download Result |
|
|
</button> |
|
|
|
|
|
{maskImage && ( |
|
|
<button |
|
|
onClick={() => handleDownload(maskImage, 'mask.png')} |
|
|
style={{ |
|
|
padding: '10px 20px', |
|
|
marginRight: '8px', |
|
|
backgroundColor: '#17a2b8', |
|
|
color: 'white', |
|
|
border: 'none', |
|
|
borderRadius: '4px', |
|
|
cursor: 'pointer', |
|
|
}} |
|
|
> |
|
|
Download Mask |
|
|
</button> |
|
|
)} |
|
|
|
|
|
<button |
|
|
onClick={handleReset} |
|
|
style={{ |
|
|
padding: '10px 20px', |
|
|
backgroundColor: '#dc3545', |
|
|
color: 'white', |
|
|
border: 'none', |
|
|
borderRadius: '4px', |
|
|
cursor: 'pointer', |
|
|
}} |
|
|
> |
|
|
Process Another |
|
|
</button> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Instructions */} |
|
|
<div className="instructions" style={{ marginTop: '40px', padding: '20px', backgroundColor: '#f8f9fa', borderRadius: '8px' }}> |
|
|
<h3>How to use:</h3> |
|
|
<ol> |
|
|
<li>Select quality and model settings</li> |
|
|
<li>Drag and drop an image or click to browse</li> |
|
|
<li>Wait for processing to complete</li> |
|
|
<li>Preview and download your results</li> |
|
|
</ol> |
|
|
|
|
|
<h3>Integration:</h3> |
|
|
<pre style={{ backgroundColor: '#fff', padding: '12px', borderRadius: '4px', overflow: 'auto' }}> |
|
|
{`npm install axios |
|
|
import BackgroundRemover from './BackgroundRemover'; |
|
|
|
|
|
<BackgroundRemover apiKey="your-api-key" />`} |
|
|
</pre> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default BackgroundRemover; |
|
|
export { useBackgroundFX, DropZone, ImagePreview, ProgressBar }; |