import React, { useState, useRef, useEffect } from 'react'; import Tesseract from 'tesseract.js'; interface CameraScannerProps { onCardScanned: (cardData: any) => void; onError: (error: string) => void; } interface ScanResult { text: string; confidence: number; cardName?: string; setName?: string; } const CameraScanner: React.FC = ({ onCardScanned, onError }) => { const [isStreaming, setIsStreaming] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [scanResult, setScanResult] = useState(null); const [capturedImage, setCapturedImage] = useState(null); const videoRef = useRef(null); const canvasRef = useRef(null); const streamRef = useRef(null); // Start camera stream const startCamera = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', // Use back camera on mobile width: { ideal: 1920 }, height: { ideal: 1080 } } }); if (videoRef.current) { videoRef.current.srcObject = stream; streamRef.current = stream; setIsStreaming(true); } } catch (err) { console.error('Camera access error:', err); onError('Unable to access camera. Please check permissions.'); } }; // Stop camera stream const stopCamera = () => { if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); streamRef.current = null; } setIsStreaming(false); setCapturedImage(null); setScanResult(null); }; // Capture image from video stream const captureImage = () => { if (!videoRef.current || !canvasRef.current) return; const video = videoRef.current; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); // Set canvas dimensions to match video canvas.width = video.videoWidth; canvas.height = video.videoHeight; // Draw current video frame to canvas ctx?.drawImage(video, 0, 0, canvas.width, canvas.height); // Get image data URL const imageDataUrl = canvas.toDataURL('image/jpeg', 0.8); setCapturedImage(imageDataUrl); // Process with OCR processImage(imageDataUrl); }; // Process image with Tesseract OCR const processImage = async (imageData: string) => { setIsProcessing(true); setScanResult(null); try { const { data: { text, confidence } } = await Tesseract.recognize(imageData, 'eng', { logger: m => console.log(m) // Optional: log progress }); console.log('OCR Result:', text); console.log('Confidence:', confidence); // Extract card information from OCR text const cardInfo = extractCardInfo(text); const result: ScanResult = { text: text.trim(), confidence, cardName: cardInfo.name, setName: cardInfo.set }; setScanResult(result); // If we found card info, try to match against database if (cardInfo.name) { onCardScanned({ name: cardInfo.name, set: cardInfo.set, ocrText: text, confidence: confidence }); } } catch (error) { console.error('OCR processing error:', error); onError('Failed to process image. Please try again.'); } finally { setIsProcessing(false); } }; // Extract card name and set from OCR text const extractCardInfo = (text: string) => { const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0); // Common patterns for different card games const patterns = { // Magic: The Gathering patterns mtg: { cardName: /^[A-Z][a-zA-Z\s,'-]+(?=\s|$)/, setInfo: /\b([A-Z]{3}|[A-Z]{4})\b/, // 3-4 letter set codes }, // Pokemon patterns pokemon: { cardName: /^[A-Z][a-zA-Z\s]+(?=\s+[0-9])/, hpPattern: /HP\s*[0-9]+/, }, // Lorcana patterns lorcana: { cardName: /^[A-Z][a-zA-Z\s,'-]+(?=\s*-)/, subtitle: /-\s*([A-Z][a-zA-Z\s]+)/, } }; let cardName = ''; let setName = ''; // Try to find the card name (usually the first substantial line) for (const line of lines) { if (line.length > 3 && line.length < 50) { // Reasonable card name length // Skip common non-name text if (!line.match(/^(hp|©|legendary|instant|sorcery|creature|artifact|enchantment)$/i)) { if (!cardName || line.length > cardName.length) { cardName = line; } } } } // Try to extract set information const setText = lines.find(line => patterns.mtg.setInfo.test(line)); if (setText) { const setMatch = setText.match(patterns.mtg.setInfo); if (setMatch) { setName = setMatch[1]; } } return { name: cardName || '', set: setName || '' }; }; // Cleanup on unmount useEffect(() => { return () => { stopCamera(); }; }, []); return (
{/* Camera Controls */}
{!isStreaming ? ( ) : ( <> )}
{/* Camera Preview */} {isStreaming && (
)} {/* Hidden canvas for image capture */} {/* Processing Status */} {isProcessing && (
Processing image...
Extracting card information
)} {/* Captured Image Preview */} {capturedImage && !isProcessing && (
Captured Image
Captured card
)} {/* OCR Results */} {scanResult && (
Scan Results
{scanResult.cardName ? (
Card Found: {scanResult.cardName}
{scanResult.setName && (
Set: {scanResult.setName}
)}
Confidence: {Math.round(scanResult.confidence)}%
) : (
⚠️
Card not recognized
View OCR text
                  {scanResult.text}
                
)}
)} {/* Instructions */}
📋 Scanning Tips
  • • Ensure good lighting and avoid shadows
  • • Keep the card flat and in focus
  • • Position the card name clearly in view
  • • Avoid glare and reflections
  • • Works best with English cards
); }; export default CameraScanner;