Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Advanced Real-Time OCR Scanner</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/tesseract.js@4.1.1/dist/tesseract.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .video-container { | |
| position: relative; | |
| width: 100%; | |
| max-width: 640px; | |
| margin: 0 auto; | |
| background: #111827; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| #video { | |
| width: 100%; | |
| height: auto; | |
| transform: rotateY(180deg); | |
| -webkit-transform: rotateY(180deg); | |
| } | |
| #canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: none; | |
| } | |
| .scan-box { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 80%; | |
| height: 30%; | |
| border: 3px dashed rgba(59, 130, 246, 0.7); | |
| border-radius: 8px; | |
| pointer-events: none; | |
| box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.5); | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); | |
| } | |
| 70% { | |
| box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); | |
| } | |
| 100% { | |
| box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); | |
| } | |
| } | |
| .result-text { | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| } | |
| .language-selector { | |
| background-color: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(5px); | |
| } | |
| .preview-image { | |
| max-height: 300px; | |
| object-fit: contain; | |
| border-radius: 8px; | |
| margin: 0 auto; | |
| display: block; | |
| } | |
| .tab-button { | |
| transition: all 0.3s ease; | |
| } | |
| .tab-button.active { | |
| background-color: #3b82f6; | |
| color: white; | |
| } | |
| .progress-bar { | |
| height: 4px; | |
| background-color: #3b82f6; | |
| transition: width 0.3s ease; | |
| } | |
| .dropzone { | |
| border: 2px dashed #4b5563; | |
| border-radius: 8px; | |
| padding: 2rem; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .dropzone.active { | |
| border-color: #3b82f6; | |
| background-color: rgba(59, 130, 246, 0.1); | |
| } | |
| .enhance-options { | |
| transition: all 0.3s ease; | |
| max-height: 0; | |
| overflow: hidden; | |
| } | |
| .enhance-options.open { | |
| max-height: 300px; | |
| padding: 1rem 0; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="text-center mb-8"> | |
| <h1 class="text-4xl font-bold mb-2 bg-gradient-to-r from-blue-400 to-purple-600 bg-clip-text text-transparent"> | |
| <i class="fas fa-camera-retro mr-2"></i> Advanced OCR Scanner | |
| </h1> | |
| <p class="text-gray-400 max-w-2xl mx-auto"> | |
| Extract text from camera or uploaded images with enhanced precision and editing tools | |
| </p> | |
| </header> | |
| <!-- Tab Navigation --> | |
| <div class="flex justify-center mb-8"> | |
| <div class="inline-flex rounded-full bg-gray-800 p-1"> | |
| <button id="cameraTab" class="tab-button px-6 py-2 rounded-full font-medium flex items-center active"> | |
| <i class="fas fa-camera mr-2"></i> Camera | |
| </button> | |
| <button id="uploadTab" class="tab-button px-6 py-2 rounded-full font-medium flex items-center"> | |
| <i class="fas fa-upload mr-2"></i> Upload | |
| </button> | |
| </div> | |
| </div> | |
| <div class="flex flex-col lg:flex-row gap-8"> | |
| <!-- Input Section --> | |
| <div class="flex-1"> | |
| <div class="bg-gray-800 rounded-xl p-4 shadow-xl"> | |
| <!-- Camera Section --> | |
| <div id="cameraSection"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold"> | |
| <i class="fas fa-video mr-2 text-blue-400"></i> Camera Feed | |
| </h2> | |
| <div class="flex items-center gap-2"> | |
| <div class="language-selector px-3 py-1 rounded-full"> | |
| <select id="language" class="bg-transparent text-white focus:outline-none"> | |
| <option value="eng">English</option> | |
| <option value="spa">Spanish</option> | |
| <option value="fra">French</option> | |
| <option value="deu">German</option> | |
| <option value="chi_sim">Chinese</option> | |
| <option value="jpn">Japanese</option> | |
| <option value="kor">Korean</option> | |
| <option value="ara">Arabic</option> | |
| </select> | |
| </div> | |
| <button id="enhanceBtn" class="bg-gray-700 hover:bg-gray-600 text-white px-3 py-1 rounded-full text-sm"> | |
| <i class="fas fa-sliders-h mr-1"></i> Enhance | |
| </button> | |
| </div> | |
| </div> | |
| <div class="enhance-options mb-4"> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Brightness</label> | |
| <input type="range" id="brightness" min="-100" max="100" value="0" class="w-full"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Contrast</label> | |
| <input type="range" id="contrast" min="-100" max="100" value="0" class="w-full"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Threshold</label> | |
| <input type="range" id="threshold" min="0" max="255" value="0" class="w-full"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Sharpness</label> | |
| <input type="range" id="sharpness" min="0" max="200" value="100" class="w-full"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="video-container"> | |
| <video id="video" autoplay playsinline></video> | |
| <canvas id="canvas"></canvas> | |
| <div class="scan-box"></div> | |
| </div> | |
| <div class="flex justify-center mt-4 gap-4"> | |
| <button id="startBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-full font-medium flex items-center pulse"> | |
| <i class="fas fa-play mr-2"></i> Start Camera | |
| </button> | |
| <button id="captureBtn" class="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-full font-medium flex items-center opacity-50 cursor-not-allowed" disabled> | |
| <i class="fas fa-camera mr-2"></i> Capture Text | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Upload Section --> | |
| <div id="uploadSection" class="hidden"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold"> | |
| <i class="fas fa-upload mr-2 text-blue-400"></i> Upload Image | |
| </h2> | |
| <div class="flex items-center gap-2"> | |
| <div class="language-selector px-3 py-1 rounded-full"> | |
| <select id="uploadLanguage" class="bg-transparent text-white focus:outline-none"> | |
| <option value="eng">English</option> | |
| <option value="spa">Spanish</option> | |
| <option value="fra">French</option> | |
| <option value="deu">German</option> | |
| <option value="chi_sim">Chinese</option> | |
| <option value="jpn">Japanese</option> | |
| <option value="kor">Korean</option> | |
| <option value="ara">Arabic</option> | |
| </select> | |
| </div> | |
| <button id="uploadEnhanceBtn" class="bg-gray-700 hover:bg-gray-600 text-white px-3 py-1 rounded-full text-sm"> | |
| <i class="fas fa-sliders-h mr-1"></i> Enhance | |
| </button> | |
| </div> | |
| </div> | |
| <div class="enhance-options mb-4"> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Brightness</label> | |
| <input type="range" id="uploadBrightness" min="-100" max="100" value="0" class="w-full"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Contrast</label> | |
| <input type="range" id="uploadContrast" min="-100" max="100" value="0" class="w-full"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Threshold</label> | |
| <input type="range" id="uploadThreshold" min="0" max="255" value="0" class="w-full"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Sharpness</label> | |
| <input type="range" id="uploadSharpness" min="0" max="200" value="100" class="w-full"> | |
| </div> | |
| </div> | |
| <button id="applyEnhanceBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-full text-sm mt-2"> | |
| <i class="fas fa-magic mr-1"></i> Apply Enhancements | |
| </button> | |
| </div> | |
| <div class="dropzone" id="dropzone"> | |
| <i class="fas fa-cloud-upload-alt text-4xl text-blue-400 mb-2"></i> | |
| <p class="font-medium">Drag & drop your image here</p> | |
| <p class="text-sm text-gray-400 mt-1">or</p> | |
| <label for="fileInput" class="inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-full mt-2 cursor-pointer"> | |
| <i class="fas fa-folder-open mr-1"></i> Browse Files | |
| </label> | |
| <input type="file" id="fileInput" accept="image/*" class="hidden"> | |
| </div> | |
| <div id="imagePreviewContainer" class="hidden mt-4"> | |
| <div class="relative"> | |
| <img id="imagePreview" class="preview-image" src="" alt="Preview"> | |
| <canvas id="uploadCanvas" class="hidden"></canvas> | |
| <div class="absolute top-2 right-2"> | |
| <button id="removeImageBtn" class="bg-red-600 hover:bg-red-700 text-white p-2 rounded-full"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <button id="processImageBtn" class="w-full bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-full font-medium flex items-center justify-center mt-4"> | |
| <i class="fas fa-cogs mr-2"></i> Process Image | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Results Section --> | |
| <div class="flex-1"> | |
| <div class="bg-gray-800 rounded-xl p-4 shadow-xl h-full"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold"> | |
| <i class="fas fa-file-alt mr-2 text-green-400"></i> Extracted Text | |
| </h2> | |
| <div class="text-sm text-gray-400"> | |
| <span id="status">Status: Ready</span> | |
| </div> | |
| </div> | |
| <div class="progress-bar-container bg-gray-700 rounded-full h-1 mb-4"> | |
| <div id="progressBar" class="progress-bar rounded-full" style="width: 0%"></div> | |
| </div> | |
| <div class="bg-gray-900 rounded-lg p-4 h-96 overflow-y-auto mb-4"> | |
| <div id="results" class="result-text text-gray-300"> | |
| <div class="text-center text-gray-500 py-16"> | |
| <i class="fas fa-align-left text-4xl mb-2"></i> | |
| <p>Extracted text will appear here</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <div class="text-sm text-gray-400"> | |
| <span id="confidence">Confidence: --</span> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button id="copyBtn" class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-full text-sm flex items-center" disabled> | |
| <i class="fas fa-copy mr-1"></i> Copy | |
| </button> | |
| <button id="clearBtn" class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-full text-sm flex items-center" disabled> | |
| <i class="fas fa-trash-alt mr-1"></i> Clear | |
| </button> | |
| <button id="downloadBtn" class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-full text-sm flex items-center" disabled> | |
| <i class="fas fa-download mr-1"></i> Save | |
| </button> | |
| <button id="editBtn" class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-full text-sm flex items-center" disabled> | |
| <i class="fas fa-edit mr-1"></i> Edit | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Features Section --> | |
| <div class="mt-12"> | |
| <h2 class="text-2xl font-bold text-center mb-6 bg-gradient-to-r from-blue-400 to-purple-600 bg-clip-text text-transparent"> | |
| <i class="fas fa-star mr-2"></i> Key Features | |
| </h2> | |
| <div class="grid md:grid-cols-3 gap-6"> | |
| <div class="bg-gray-800 rounded-xl p-6 hover:bg-gray-700 transition-all"> | |
| <div class="text-blue-400 text-3xl mb-4"> | |
| <i class="fas fa-camera"></i> | |
| </div> | |
| <h3 class="text-xl font-semibold mb-2">Real-Time Scanning</h3> | |
| <p class="text-gray-400">Capture text instantly using your device's camera with auto-focus and enhancement tools.</p> | |
| </div> | |
| <div class="bg-gray-800 rounded-xl p-6 hover:bg-gray-700 transition-all"> | |
| <div class="text-purple-400 text-3xl mb-4"> | |
| <i class="fas fa-image"></i> | |
| </div> | |
| <h3 class="text-xl font-semibold mb-2">Image Upload</h3> | |
| <p class="text-gray-400">Extract text from existing images in your gallery with drag & drop support.</p> | |
| </div> | |
| <div class="bg-gray-800 rounded-xl p-6 hover:bg-gray-700 transition-all"> | |
| <div class="text-green-400 text-3xl mb-4"> | |
| <i class="fas fa-magic"></i> | |
| </div> | |
| <h3 class="text-xl font-semibold mb-2">Image Enhancement</h3> | |
| <p class="text-gray-400">Adjust brightness, contrast, threshold and sharpness for better OCR results.</p> | |
| </div> | |
| <div class="bg-gray-800 rounded-xl p-6 hover:bg-gray-700 transition-all"> | |
| <div class="text-yellow-400 text-3xl mb-4"> | |
| <i class="fas fa-language"></i> | |
| </div> | |
| <h3 class="text-xl font-semibold mb-2">Multi-Language</h3> | |
| <p class="text-gray-400">Supports 8 languages including English, Spanish, Chinese, Arabic and more.</p> | |
| </div> | |
| <div class="bg-gray-800 rounded-xl p-6 hover:bg-gray-700 transition-all"> | |
| <div class="text-red-400 text-3xl mb-4"> | |
| <i class="fas fa-chart-line"></i> | |
| </div> | |
| <h3 class="text-xl font-semibold mb-2">Confidence Score</h3> | |
| <p class="text-gray-400">See how confident the OCR engine is about each text extraction.</p> | |
| </div> | |
| <div class="bg-gray-800 rounded-xl p-6 hover:bg-gray-700 transition-all"> | |
| <div class="text-indigo-400 text-3xl mb-4"> | |
| <i class="fas fa-file-export"></i> | |
| </div> | |
| <h3 class="text-xl font-semibold mb-2">Export Options</h3> | |
| <p class="text-gray-400">Copy, edit or download your extracted text for further use.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const video = document.getElementById('video'); | |
| const canvas = document.getElementById('canvas'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const captureBtn = document.getElementById('captureBtn'); | |
| const resultsDiv = document.getElementById('results'); | |
| const statusSpan = document.getElementById('status'); | |
| const confidenceSpan = document.getElementById('confidence'); | |
| const copyBtn = document.getElementById('copyBtn'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const editBtn = document.getElementById('editBtn'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const languageSelect = document.getElementById('language'); | |
| const cameraTab = document.getElementById('cameraTab'); | |
| const uploadTab = document.getElementById('uploadTab'); | |
| const cameraSection = document.getElementById('cameraSection'); | |
| const uploadSection = document.getElementById('uploadSection'); | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const imagePreviewContainer = document.getElementById('imagePreviewContainer'); | |
| const imagePreview = document.getElementById('imagePreview'); | |
| const uploadCanvas = document.getElementById('uploadCanvas'); | |
| const removeImageBtn = document.getElementById('removeImageBtn'); | |
| const processImageBtn = document.getElementById('processImageBtn'); | |
| const uploadLanguage = document.getElementById('uploadLanguage'); | |
| const enhanceBtn = document.getElementById('enhanceBtn'); | |
| const uploadEnhanceBtn = document.getElementById('uploadEnhanceBtn'); | |
| const enhanceOptions = document.querySelector('#cameraSection .enhance-options'); | |
| const uploadEnhanceOptions = document.querySelector('#uploadSection .enhance-options'); | |
| const applyEnhanceBtn = document.getElementById('applyEnhanceBtn'); | |
| // State variables | |
| let stream = null; | |
| let isScanning = false; | |
| let scanInterval = null; | |
| let currentImageData = null; | |
| // Tab switching | |
| cameraTab.addEventListener('click', function() { | |
| cameraTab.classList.add('active'); | |
| uploadTab.classList.remove('active'); | |
| cameraSection.classList.remove('hidden'); | |
| uploadSection.classList.add('hidden'); | |
| stopCamera(); // Stop camera when switching to upload tab | |
| }); | |
| uploadTab.addEventListener('click', function() { | |
| uploadTab.classList.add('active'); | |
| cameraTab.classList.remove('active'); | |
| cameraSection.classList.add('hidden'); | |
| uploadSection.classList.remove('hidden'); | |
| stopCamera(); // Stop camera when switching to upload tab | |
| }); | |
| // Enhance options toggle | |
| enhanceBtn.addEventListener('click', function() { | |
| enhanceOptions.classList.toggle('open'); | |
| }); | |
| uploadEnhanceBtn.addEventListener('click', function() { | |
| uploadEnhanceOptions.classList.toggle('open'); | |
| }); | |
| // Start camera | |
| startBtn.addEventListener('click', async function() { | |
| try { | |
| if (stream) { | |
| stopCamera(); | |
| return; | |
| } | |
| statusSpan.textContent = 'Status: Accessing camera...'; | |
| stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| facingMode: 'environment', | |
| width: { ideal: 1280 }, | |
| height: { ideal: 720 } | |
| }, | |
| audio: false | |
| }); | |
| video.srcObject = stream; | |
| startBtn.innerHTML = '<i class="fas fa-stop mr-2"></i> Stop Camera'; | |
| startBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); | |
| startBtn.classList.add('bg-red-600', 'hover:bg-red-700'); | |
| captureBtn.disabled = false; | |
| captureBtn.classList.remove('opacity-50', 'cursor-not-allowed'); | |
| statusSpan.textContent = 'Status: Camera ready'; | |
| // Auto-focus every 2 seconds (simulated) | |
| scanInterval = setInterval(() => { | |
| if (isScanning) return; | |
| autoScan(); | |
| }, 2000); | |
| } catch (err) { | |
| console.error('Error accessing camera:', err); | |
| statusSpan.textContent = 'Status: Error accessing camera'; | |
| showError(`Could not access camera: ${err.message}`); | |
| } | |
| }); | |
| function stopCamera() { | |
| if (stream) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| stream = null; | |
| video.srcObject = null; | |
| startBtn.innerHTML = '<i class="fas fa-play mr-2"></i> Start Camera'; | |
| startBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); | |
| startBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); | |
| captureBtn.disabled = true; | |
| captureBtn.classList.add('opacity-50', 'cursor-not-allowed'); | |
| statusSpan.textContent = 'Status: Camera stopped'; | |
| clearInterval(scanInterval); | |
| isScanning = false; | |
| } | |
| } | |
| // Capture button | |
| captureBtn.addEventListener('click', function() { | |
| if (!stream) return; | |
| captureFromCamera(); | |
| }); | |
| function captureFromCamera() { | |
| isScanning = true; | |
| statusSpan.textContent = 'Status: Processing image...'; | |
| captureBtn.disabled = true; | |
| captureBtn.classList.add('opacity-50', 'cursor-not-allowed'); | |
| progressBar.style.width = '0%'; | |
| // Get enhancement values | |
| const brightness = parseInt(document.getElementById('brightness').value); | |
| const contrast = parseInt(document.getElementById('contrast').value); | |
| const threshold = parseInt(document.getElementById('threshold').value); | |
| const sharpness = parseInt(document.getElementById('sharpness').value) / 100; | |
| // Get the dimensions of the scan box | |
| const videoWidth = video.videoWidth; | |
| const videoHeight = video.videoHeight; | |
| const boxWidth = video.offsetWidth * 0.8; | |
| const boxHeight = video.offsetHeight * 0.3; | |
| const boxLeft = (video.offsetWidth - boxWidth) / 2; | |
| const boxTop = (video.offsetHeight - boxHeight) / 2; | |
| // Calculate the actual capture area in video coordinates | |
| const scaleX = videoWidth / video.offsetWidth; | |
| const scaleY = videoHeight / video.offsetHeight; | |
| const captureWidth = boxWidth * scaleX; | |
| const captureHeight = boxHeight * scaleY; | |
| const captureLeft = boxLeft * scaleX; | |
| const captureTop = boxTop * scaleY; | |
| // Set canvas dimensions | |
| canvas.width = captureWidth; | |
| canvas.height = captureHeight; | |
| // Draw video frame to canvas (only the scan box area) | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage( | |
| video, | |
| captureLeft, captureTop, captureWidth, captureHeight, | |
| 0, 0, captureWidth, captureHeight | |
| ); | |
| // Apply image enhancements | |
| if (brightness !== 0 || contrast !== 0 || threshold > 0 || sharpness !== 1) { | |
| applyImageEnhancements(ctx, canvas, brightness, contrast, threshold, sharpness); | |
| } | |
| // Get image data from canvas | |
| const imageData = canvas.toDataURL('image/jpeg', 0.8); | |
| currentImageData = imageData; | |
| // Process with Tesseract | |
| processImage(imageData, languageSelect.value); | |
| } | |
| // Auto-scan function | |
| function autoScan() { | |
| if (!stream || isScanning) return; | |
| isScanning = true; | |
| statusSpan.textContent = 'Status: Auto-scanning...'; | |
| // Get the dimensions of the scan box (same as manual capture) | |
| const videoWidth = video.videoWidth; | |
| const videoHeight = video.videoHeight; | |
| const boxWidth = video.offsetWidth * 0.8; | |
| const boxHeight = video.offsetHeight * 0.3; | |
| const boxLeft = (video.offsetWidth - boxWidth) / 2; | |
| const boxTop = (video.offsetHeight - boxHeight) / 2; | |
| const scaleX = videoWidth / video.offsetWidth; | |
| const scaleY = videoHeight / video.offsetHeight; | |
| const captureWidth = boxWidth * scaleX; | |
| const captureHeight = boxHeight * scaleY; | |
| const captureLeft = boxLeft * scaleX; | |
| const captureTop = boxTop * scaleY; | |
| canvas.width = captureWidth; | |
| canvas.height = captureHeight; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage( | |
| video, | |
| captureLeft, captureTop, captureWidth, captureHeight, | |
| 0, 0, captureWidth, captureHeight | |
| ); | |
| const imageData = canvas.toDataURL('image/jpeg', 0.8); | |
| currentImageData = imageData; | |
| Tesseract.recognize( | |
| imageData, | |
| languageSelect.value, | |
| { logger: m => {} } | |
| ).then(({ data: { text, confidence } }) => { | |
| if (text.trim()) { | |
| resultsDiv.innerHTML = ` | |
| <div class="bg-blue-900/20 border border-blue-700 rounded p-3 mb-3 text-blue-100"> | |
| <i class="fas fa-robot mr-2"></i> | |
| Auto-detected text! (Confidence: ${confidence.toFixed(1)}%) | |
| </div> | |
| <div class="result-text bg-gray-800 p-3 rounded">${text}</div> | |
| `; | |
| enableActionButtons(); | |
| confidenceSpan.textContent = `Confidence: ${confidence.toFixed(1)}%`; | |
| } | |
| statusSpan.textContent = 'Status: Ready (auto-scan)'; | |
| isScanning = false; | |
| }).catch(err => { | |
| console.error('Auto-scan error:', err); | |
| isScanning = false; | |
| statusSpan.textContent = 'Status: Ready (auto-scan failed)'; | |
| }); | |
| } | |
| // File upload handling | |
| fileInput.addEventListener('change', handleFileSelect); | |
| // Drag and drop handling | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, unhighlight, false); | |
| }); | |
| function highlight() { | |
| dropzone.classList.add('active'); | |
| } | |
| function unhighlight() { | |
| dropzone.classList.remove('active'); | |
| } | |
| dropzone.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| if (files.length) { | |
| handleFileSelect({ target: { files } }); | |
| } | |
| } | |
| function handleFileSelect(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| if (!file.type.match('image.*')) { | |
| showError('Please select an image file (JPEG, PNG, etc.)'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| imagePreview.src = e.target.result; | |
| imagePreviewContainer.classList.remove('hidden'); | |
| dropzone.classList.add('hidden'); | |
| // Set up canvas for processing | |
| const img = new Image(); | |
| img.onload = function() { | |
| uploadCanvas.width = img.width; | |
| uploadCanvas.height = img.height; | |
| const ctx = uploadCanvas.getContext('2d'); | |
| ctx.drawImage(img, 0, 0); | |
| currentImageData = uploadCanvas.toDataURL('image/jpeg', 0.8); | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| removeImageBtn.addEventListener('click', function() { | |
| imagePreviewContainer.classList.add('hidden'); | |
| dropzone.classList.remove('hidden'); | |
| fileInput.value = ''; | |
| currentImageData = null; | |
| }); | |
| processImageBtn.addEventListener('click', function() { | |
| if (!currentImageData) return; | |
| // Get enhancement values | |
| const brightness = parseInt(document.getElementById('uploadBrightness').value); | |
| const contrast = parseInt(document.getElementById('uploadContrast').value); | |
| const threshold = parseInt(document.getElementById('uploadThreshold').value); | |
| const sharpness = parseInt(document.getElementById('uploadSharpness').value) / 100; | |
| // Apply enhancements if needed | |
| if (brightness !== 0 || contrast !== 0 || threshold > 0 || sharpness !== 1) { | |
| const ctx = uploadCanvas.getContext('2d'); | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = uploadCanvas.width; | |
| tempCanvas.height = uploadCanvas.height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCtx.drawImage(uploadCanvas, 0, 0); | |
| applyImageEnhancements(tempCtx, tempCanvas, brightness, contrast, threshold, sharpness); | |
| currentImageData = tempCanvas.toDataURL('image/jpeg', 0.8); | |
| } | |
| processImage(currentImageData, uploadLanguage.value); | |
| }); | |
| applyEnhanceBtn.addEventListener('click', function() { | |
| if (!currentImageData) return; | |
| // Get enhancement values | |
| const brightness = parseInt(document.getElementById('uploadBrightness').value); | |
| const contrast = parseInt(document.getElementById('uploadContrast').value); | |
| const threshold = parseInt(document.getElementById('uploadThreshold').value); | |
| const sharpness = parseInt(document.getElementById('uploadSharpness').value) / 100; | |
| // Apply to preview | |
| const img = new Image(); | |
| img.onload = function() { | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = img.width; | |
| tempCanvas.height = img.height; | |
| const ctx = tempCanvas.getContext('2d'); | |
| ctx.drawImage(img, 0, 0); | |
| applyImageEnhancements(ctx, tempCanvas, brightness, contrast, threshold, sharpness); | |
| imagePreview.src = tempCanvas.toDataURL('image/jpeg', 0.8); | |
| }; | |
| img.src = currentImageData; | |
| }); | |
| function processImage(imageData, language) { | |
| isScanning = true; | |
| statusSpan.textContent = 'Status: Processing image...'; | |
| progressBar.style.width = '0%'; | |
| if (processImageBtn) { | |
| processImageBtn.disabled = true; | |
| processImageBtn.classList.add('opacity-50', 'cursor-not-allowed'); | |
| } | |
| if (captureBtn) { | |
| captureBtn.disabled = true; | |
| captureBtn.classList.add('opacity-50', 'cursor-not-allowed'); | |
| } | |
| Tesseract.recognize( | |
| imageData, | |
| language, | |
| { | |
| logger: m => { | |
| if (m.status === 'recognizing text') { | |
| statusSpan.textContent = `Status: ${m.status} (${Math.round(m.progress * 100)}%)`; | |
| progressBar.style.width = `${m.progress * 100}%`; | |
| } else { | |
| statusSpan.textContent = `Status: ${m.status}`; | |
| } | |
| } | |
| } | |
| ).then(({ data: { text, confidence, hocr } }) => { | |
| if (text.trim()) { | |
| resultsDiv.innerHTML = ` | |
| <div class="bg-green-900/20 border border-green-700 rounded p-3 mb-3 text-green-100"> | |
| <i class="fas fa-check-circle mr-2"></i> | |
| Successfully extracted text! (Confidence: ${confidence.toFixed(1)}%) | |
| </div> | |
| <div class="result-text bg-gray-800 p-3 rounded">${text}</div> | |
| `; | |
| enableActionButtons(); | |
| confidenceSpan.textContent = `Confidence: ${confidence.toFixed(1)}%`; | |
| } else { | |
| resultsDiv.innerHTML = ` | |
| <div class="bg-yellow-900/20 border border-yellow-700 rounded p-3 text-yellow-100"> | |
| <i class="fas fa-exclamation-circle mr-2"></i> | |
| No text was detected. Try adjusting the position or lighting. | |
| </div> | |
| `; | |
| } | |
| statusSpan.textContent = 'Status: Ready'; | |
| isScanning = false; | |
| if (processImageBtn) { | |
| processImageBtn.disabled = false; | |
| processImageBtn.classList.remove('opacity-50', 'cursor-not-allowed'); | |
| } | |
| if (captureBtn) { | |
| captureBtn.disabled = false; | |
| captureBtn.classList.remove('opacity-50', 'cursor-not-allowed'); | |
| } | |
| }).catch(err => { | |
| console.error('OCR Error:', err); | |
| showError(`Error processing image: ${err.message}`); | |
| statusSpan.textContent = 'Status: Error processing image'; | |
| isScanning = false; | |
| if (processImageBtn) { | |
| processImageBtn.disabled = false; | |
| processImageBtn.classList.remove('opacity-50', 'cursor-not-allowed'); | |
| } | |
| if (captureBtn) { | |
| captureBtn.disabled = false; | |
| captureBtn.classList.remove('opacity-50', 'cursor-not-allowed'); | |
| } | |
| }); | |
| } | |
| function applyImageEnhancements(ctx, canvas, brightness, contrast, threshold, sharpness) { | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| // Apply brightness and contrast | |
| if (brightness !== 0 || contrast !== 0) { | |
| const factor = (259 * (contrast + 255)) / (255 * (259 - contrast)); | |
| for (let i = 0; i < data.length; i += 4) { | |
| // Apply contrast | |
| data[i] = factor * (data[i] - 128) + 128 + brightness; | |
| data[i+1] = factor * (data[i+1] - 128) + 128 + brightness; | |
| data[i+2] = factor * (data[i+2] - 128) + 128 + brightness; | |
| // Clamp values between 0-255 | |
| data[i] = Math.max(0, Math.min(255, data[i])); | |
| data[i+1] = Math.max(0, Math.min(255, data[i+1])); | |
| data[i+2] = Math.max(0, Math.min(255, data[i+2])); | |
| } | |
| } | |
| // Apply threshold (convert to black and white) | |
| if (threshold > 0) { | |
| for (let i = 0; i < data.length; i += 4) { | |
| const avg = (data[i] + data[i+1] + data[i+2]) / 3; | |
| const value = avg > threshold ? 255 : 0; | |
| data[i] = data[i+1] = data[i+2] = value; | |
| } | |
| } | |
| ctx.putImageData(imageData, 0, 0); | |
| // Apply sharpness (using a simple convolution filter) | |
| if (sharpness !== 1) { | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCtx.drawImage(canvas, 0, 0); | |
| // Apply sharpening filter | |
| const weights = [0, -1 * sharpness, 0, -1 * sharpness, 1 + 4 * sharpness, -1 * sharpness, 0, -1 * sharpness, 0]; | |
| const divisor = 1; | |
| const bias = 0; | |
| ctx.filter = `contrast(${100 + contrast}%) brightness(${100 + brightness}%)`; | |
| ctx.drawImage(tempCanvas, 0, 0); | |
| } | |
| } | |
| function enableActionButtons() { | |
| copyBtn.disabled = false; | |
| clearBtn.disabled = false; | |
| downloadBtn.disabled = false; | |
| editBtn.disabled = false; | |
| } | |
| function showError(message) { | |
| resultsDiv.innerHTML = ` | |
| <div class="bg-red-900/50 border border-red-700 rounded p-3 text-red-100"> | |
| <i class="fas fa-exclamation-triangle mr-2"></i> | |
| ${message} | |
| </div> | |
| `; | |
| } | |
| // Copy text | |
| copyBtn.addEventListener('click', function() { | |
| const textToCopy = resultsDiv.querySelector('.result-text')?.textContent; | |
| if (textToCopy) { | |
| navigator.clipboard.writeText(textToCopy).then(() => { | |
| const originalText = copyBtn.innerHTML; | |
| copyBtn.innerHTML = '<i class="fas fa-check mr-1"></i> Copied!'; | |
| setTimeout(() => { | |
| copyBtn.innerHTML = originalText; | |
| }, 2000); | |
| }); | |
| } | |
| }); | |
| // Clear results | |
| clearBtn.addEventListener('click', function() { | |
| resultsDiv.innerHTML = ` | |
| <div class="text-center text-gray-500 py-16"> | |
| <i class="fas fa-align-left text-4xl mb-2"></i> | |
| <p>Extracted text will appear here</p> | |
| </div> | |
| `; | |
| copyBtn.disabled = true; | |
| clearBtn.disabled = true; | |
| downloadBtn.disabled = true; | |
| editBtn.disabled = true; | |
| confidenceSpan.textContent = 'Confidence: --'; | |
| }); | |
| // Download text | |
| downloadBtn.addEventListener('click', function() { | |
| const textToDownload = resultsDiv.querySelector('.result-text')?.textContent; | |
| if (textToDownload) { | |
| const blob = new Blob([textToDownload], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `ocr-extracted-text-${new Date().toISOString().slice(0,10)}.txt`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| }); | |
| // Edit text | |
| editBtn.addEventListener('click', function() { | |
| const resultText = resultsDiv.querySelector('.result-text'); | |
| if (!resultText) return; | |
| const currentText = resultText.textContent; | |
| resultText.innerHTML = ` | |
| <textarea id="textEditor" class="w-full h-64 bg-gray-700 text-white p-3 rounded">${currentText}</textarea> | |
| <div class="flex justify-end mt-2 gap-2"> | |
| <button id="cancelEditBtn" class="bg-gray-600 hover:bg-gray-500 text-white px-4 py-1 rounded text-sm"> | |
| Cancel | |
| </button> | |
| <button id="saveEditBtn" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-1 rounded text-sm"> | |
| Save Changes | |
| </button> | |
| </div> | |
| `; | |
| document.getElementById('cancelEditBtn').addEventListener('click', function() { | |
| resultText.textContent = currentText; | |
| }); | |
| document.getElementById('saveEditBtn').addEventListener('click', function() { | |
| const editedText = document.getElementById('textEditor').value; | |
| resultText.textContent = editedText; | |
| }); | |
| }); | |
| // Language change | |
| languageSelect.addEventListener('change', function() { | |
| statusSpan.textContent = `Status: Language set to ${this.options[this.selectedIndex].text}`; | |
| }); | |
| uploadLanguage.addEventListener('change', function() { | |
| statusSpan.textContent = `Status: Language set to ${this.options[this.selectedIndex].text}`; | |
| }); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=podsni/realtime-ocr" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |