| |
| |
| |
| |
| |
| |
|
|
| 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 }; |