| Server IP : 127.0.0.1 / Your IP : 216.73.216.48 Web Server : Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.2.12 System : Windows NT DESKTOP-3H4FHQJ 10.0 build 19045 (Windows 10) AMD64 User : win 10 ( 0) PHP Version : 8.2.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : D:/xampp/htdocs-coblaa/text_pdf/ |
Upload File : |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Document Scanner & PDF Converter</title>
<!-- Load Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Load Inter Font from Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Load jsPDF for client-side PDF generation -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Custom scrollbar for text area */
#extracted-text::-webkit-scrollbar { width: 8px; }
#extracted-text::-webkit-scrollbar-thumb { background-color: #6366f1; border-radius: 4px; }
#extracted-text::-webkit-scrollbar-track { background-color: #e0e7ff; } /* Lighter indigo for track */
.loading-dot {
animation: bounce 1s infinite;
}
.loading-dot:nth-child(2) { animation-delay: 0.2s; }
.loading-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-8px); }
}
/* Gradient for header/buttons */
.gradient-bg-indigo {
background-image: linear-gradient(to right top, #6d28d9, #7c3aed, #8b5cf6); /* Deeper indigo/purple */
}
.gradient-bg-emerald {
background-image: linear-gradient(to right top, #059669, #10b981, #34d399); /* Rich emerald */
}
</style>
</head>
<body class="bg-gradient-to-br from-indigo-100 to-purple-100 min-h-screen font-sans antialiased p-4">
<!-- Main Application Container -->
<div id="app-container" class="max-w-4xl mx-auto bg-white shadow-2xl rounded-3xl p-6 md:p-10 mt-8 transform transition-all duration-300 hover:shadow-3xl">
<header class="text-center mb-10">
<h1 class="text-5xl font-extrabold text-transparent bg-clip-text gradient-bg-indigo mb-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 inline-block align-middle mr-3 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
AI Document Scanner
</h1>
<p class="text-gray-700 text-lg md:text-xl font-medium">Capture text from paper, powered by Gemini Vision, convert to editable PDF.</p>
</header>
<!-- 1. Image Upload & Scan Button Area -->
<div class="space-y-8">
<!-- Input Mode Toggle -->
<div class="flex justify-center space-x-4 p-2 bg-indigo-50 rounded-xl shadow-inner">
<button id="mode-file" onclick="setMode('file')" class="flex-1 py-3 px-4 rounded-lg font-semibold border-2 border-transparent text-gray-700 hover:bg-white transition duration-300">
<svg class="w-5 h-5 inline-block mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 0115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg>
File Upload
</button>
<button id="mode-camera" onclick="setMode('camera')" class="flex-1 py-3 px-4 rounded-lg font-semibold border-2 border-transparent text-gray-700 hover:bg-white transition duration-300">
<svg class="w-5 h-5 inline-block mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.865-1.297a2 2 0 011.664-.89h1.77a2 2 0 011.664.89l.865 1.297A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
Camera Scan
</button>
</div>
<!-- File Upload Section -->
<div id="file-input-section" class="hidden">
<div class="p-8 border-4 border-dashed border-indigo-400 rounded-2xl bg-indigo-50 hover:bg-indigo-100 transition duration-300 ease-in-out cursor-pointer group">
<label for="image-upload" class="block w-full h-full">
<div class="text-center flex flex-col items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-16 w-16 text-indigo-500 group-hover:text-indigo-700 transition duration-300 ease-in-out transform group-hover:scale-110" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 0115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p class="mt-4 text-xl font-semibold text-gray-700 group-hover:text-indigo-800 transition duration-300">
Drop your document image here or click to upload
</p>
<p id="file-name" class="mt-2 text-sm text-indigo-600 font-medium h-5 italic"></p>
</div>
</label>
<input type="file" id="image-upload" accept="image/jpeg,image/png,image/webp" class="hidden" onchange="handleFileChange(event)">
</div>
</div>
<!-- Camera Capture Section -->
<div id="camera-section" class="hidden text-center space-y-4">
<div class="relative w-full overflow-hidden rounded-2xl shadow-xl border-4 border-indigo-400">
<!-- Video and Canvas for camera/capture. Canvas is hidden. -->
<video id="camera-stream" autoplay playsinline class="w-full h-auto rounded-lg hidden"></video>
<canvas id="camera-canvas" class="hidden"></canvas>
<!-- Placeholder when camera is off -->
<div id="camera-placeholder" class="bg-gray-100 p-12 text-gray-500 rounded-lg flex flex-col items-center justify-center h-80">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.865-1.297a2 2 0 011.664-.89h1.77a2 2 0 011.664.89l.865 1.297A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
<p class="text-xl font-medium">Click 'Start Camera' to begin scanning documents.</p>
</div>
</div>
<div class="flex space-x-4 justify-center">
<button id="start-camera-button" onclick="startCamera()" class="py-3 px-6 bg-indigo-600 text-white font-semibold rounded-xl shadow-md hover:bg-indigo-700 transition duration-300 focus:outline-none focus:ring-4 focus:ring-indigo-300">
<svg class="w-5 h-5 inline-block mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
Start Camera
</button>
<button id="capture-button" onclick="captureImage()" class="py-3 px-6 bg-emerald-600 text-white font-semibold rounded-xl shadow-md hover:bg-emerald-700 transition duration-300 focus:outline-none focus:ring-4 focus:ring-emerald-300 hidden" disabled>
<svg class="w-5 h-5 inline-block mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
Capture Photo
</button>
</div>
</div>
<!-- Scan Button -->
<button id="scan-button" onclick="extractTextFromImage()" class="w-full py-4 px-6 gradient-bg-indigo text-white font-bold text-lg rounded-xl shadow-lg hover:shadow-xl hover:scale-105 transition duration-300 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center transform focus:outline-none focus:ring-4 focus:ring-indigo-300" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5H7l2-5m2 2l-2-5m7 7l-2 5L9 9l11 4-5 2zm0 0l5 5H7l2-5m2 2l-2-5" />
</svg>
<span id="scan-text">1. Scan & Extract Text with AI</span>
<div id="loading-indicator" class="hidden ml-4 flex space-x-1">
<div class="loading-dot w-3 h-3 bg-white rounded-full"></div>
<div class="loading-dot w-3 h-3 bg-white rounded-full"></div>
<div class="loading-dot w-3 h-3 bg-white rounded-full"></div>
</div>
</button>
</div>
<!-- 2. Result and Output Area -->
<div id="result-area" class="mt-10 space-y-6 hidden">
<h2 class="text-3xl font-bold text-gray-800 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mr-3 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
2. Extracted Text (Review & Edit)
</h2>
<textarea id="extracted-text" rows="12" class="w-full p-5 border-2 border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 transition duration-200 text-gray-800 bg-gray-50 text-base shadow-inner" placeholder="Text extracted from the image will appear here..."></textarea>
<!-- PDF Button -->
<button id="pdf-button" onclick="generatePdf()" class="w-full py-4 px-6 gradient-bg-emerald text-white font-bold text-lg rounded-xl shadow-lg hover:shadow-xl hover:scale-105 transition duration-300 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center transform focus:outline-none focus:ring-4 focus:ring-emerald-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
3. Convert to PDF and Download
</button>
</div>
<!-- Custom Message Box -->
<div id="message-box" class="fixed inset-0 bg-gray-900 bg-opacity-70 hidden items-center justify-center p-4 z-50 transition-opacity duration-300">
<div class="bg-white rounded-xl shadow-2xl max-w-sm w-full p-7 space-y-5 transform scale-95 opacity-0 transition-all duration-300" id="message-modal-content">
<h3 id="message-title" class="text-2xl font-bold text-gray-900 flex items-center"></h3>
<p id="message-content" class="text-gray-700 text-base"></p>
<button onclick="hideMessage()" class="w-full py-3 bg-indigo-600 text-white font-semibold rounded-lg hover:bg-indigo-700 transition duration-300 focus:outline-none focus:ring-2 focus:ring-indigo-500">Close</button>
</div>
</div>
</div>
<script type="module">
// --- API Security Modification ---
// The API key is set to an empty string. The environment will automatically
// inject a temporary, secured key at runtime when using the supported model
// (gemini-2.5-flash-preview-05-20) in the fetch call. This is the secure
// way to handle it in this client-side environment.
const apiKey = "";
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;
let base64Image = null;
let selectedMimeType = null;
let cameraStream = null; // Stores the active media stream
let currentMode = 'file'; // Tracks the current input mode
// --- Utility Functions ---
/** Displays a custom message box instead of using alert() or confirm() */
window.showMessage = function(title, content, type = "info") {
const messageBox = document.getElementById('message-box');
const messageModalContent = document.getElementById('message-modal-content');
const messageTitle = document.getElementById('message-title');
messageTitle.textContent = title;
document.getElementById('message-content').textContent = content;
// Clear previous icons and add new one based on type
messageTitle.innerHTML = ''; // Clear existing content
let iconSvg = '';
let titleColorClass = 'text-gray-900';
if (type === "success") {
iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 mr-3 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>`;
titleColorClass = 'text-green-700';
} else if (type === "error") {
iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 mr-3 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>`;
titleColorClass = 'text-red-700';
} else { // info
iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 mr-3 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>`;
titleColorClass = 'text-indigo-700';
}
messageTitle.innerHTML = iconSvg + title;
messageTitle.className = `text-2xl font-bold flex items-center ${titleColorClass}`;
messageBox.classList.remove('hidden');
messageBox.classList.add('flex', 'opacity-100');
messageModalContent.classList.remove('scale-95', 'opacity-0');
messageModalContent.classList.add('scale-100', 'opacity-100');
}
/** Hides the custom message box */
window.hideMessage = function() {
const messageBox = document.getElementById('message-box');
const messageModalContent = document.getElementById('message-modal-content');
messageModalContent.classList.remove('scale-100', 'opacity-100');
messageModalContent.classList.add('scale-95', 'opacity-0');
messageBox.classList.remove('opacity-100');
messageBox.classList.add('opacity-0');
// Hide fully after transition
setTimeout(() => {
messageBox.classList.add('hidden');
messageBox.classList.remove('flex');
}, 300); // Duration of the opacity transition
}
/** Converts a File object to a Base64 string */
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(',')[1]); // Only the data part
reader.onerror = error => reject(error);
});
}
/** Sets the state after a successful capture or file upload */
function handleCapturedImage(data, mimeType, isCamera = false) {
base64Image = data;
selectedMimeType = mimeType;
document.getElementById('scan-button').disabled = data ? false : true; // Enable if data is present
document.getElementById('result-area').classList.add('hidden');
const fileNameDisplay = document.getElementById('file-name');
if (isCamera) {
fileNameDisplay.textContent = 'Captured from camera.';
} else if (data) {
// Name is set in handleFileChange
} else {
fileNameDisplay.textContent = '';
}
}
/** Stops the camera stream */
function stopCamera() {
if (cameraStream) {
cameraStream.getTracks().forEach(track => track.stop());
cameraStream = null;
document.getElementById('camera-stream').srcObject = null;
}
}
/** Resets the camera section state to placeholder view */
function stopCameraAndSetPlaceholder() {
stopCamera();
const video = document.getElementById('camera-stream');
const placeholder = document.getElementById('camera-placeholder');
const startButton = document.getElementById('start-camera-button');
const captureButton = document.getElementById('capture-button');
video.classList.add('hidden');
placeholder.classList.remove('hidden');
captureButton.classList.add('hidden');
startButton.textContent = 'Start Camera';
startButton.onclick = startCamera;
// Do not clear the file upload state if in file mode
if (currentMode === 'camera') {
document.getElementById('scan-button').disabled = true;
handleCapturedImage(null, null); // Clear image state only if in camera mode
}
}
/** Starts the camera stream (UPDATED with better permission error handling) */
window.startCamera = async function() {
const video = document.getElementById('camera-stream');
const placeholder = document.getElementById('camera-placeholder');
const startButton = document.getElementById('start-camera-button');
const captureButton = document.getElementById('capture-button');
stopCamera();
try {
// Prefer the environment-facing camera (if available on mobile)
const constraints = {
video: {
facingMode: { ideal: "environment" },
width: { ideal: 1280 },
height: { ideal: 720 }
},
audio: false
};
cameraStream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = cameraStream;
// Wait for video metadata to load before playing
await new Promise(resolve => video.onloadedmetadata = resolve);
video.play();
placeholder.classList.add('hidden');
video.classList.remove('hidden');
captureButton.classList.remove('hidden');
captureButton.disabled = false; // Enabled only on successful stream start
startButton.textContent = 'Stop Camera';
startButton.onclick = stopCameraAndSetPlaceholder;
document.getElementById('scan-button').disabled = true;
showMessage("Camera Active", "Point your camera at the document and press 'Capture Photo'.", "info");
} catch (err) {
console.error("Error accessing camera:", err);
let errorTitle = "Camera Access Denied";
let errorMessage = "The camera could not be started. Please ensure you have granted **camera permission** to this browser/site and try reloading the page.";
if (err.name === 'NotFoundError') {
errorTitle = "No Camera Found";
errorMessage = "No suitable camera device was found on your system. Please connect one or ensure device drivers are installed.";
} else if (err.name === 'NotAllowedError' || err.name === 'SecurityError') {
errorTitle = "Permission Required";
errorMessage = "You denied camera access. Please check your browser's site settings to allow access for this page, or try reloading the page to prompt for permission again.";
}
showMessage(errorTitle, errorMessage, "error");
stopCameraAndSetPlaceholder(); // Revert to initial state
}
}
/** Captures the current frame from the video stream */
window.captureImage = function() {
const video = document.getElementById('camera-stream');
const canvas = document.getElementById('camera-canvas');
if (!cameraStream || video.paused || video.ended) {
showMessage("Capture Failed", "Camera stream is not active. Please click 'Start Camera' first.", "error");
return;
}
// Set canvas dimensions to match video dimensions
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// Convert canvas content to JPEG base64 (quality 0.9)
const imageMimeType = 'image/jpeg';
const base64Data = canvas.toDataURL(imageMimeType, 0.9).split(',')[1];
handleCapturedImage(base64Data, imageMimeType, true);
showMessage("Photo Captured", "The image has been captured and is ready for text extraction. You can now press the Scan button.", "success");
}
/** Handles mode switching (File vs Camera) */
window.setMode = function(mode) {
currentMode = mode;
const fileSection = document.getElementById('file-input-section');
const cameraSection = document.getElementById('camera-section');
const modeFileBtn = document.getElementById('mode-file');
const modeCameraBtn = document.getElementById('mode-camera');
// Stop camera if switching away from camera mode
if (mode !== 'camera') {
stopCameraAndSetPlaceholder();
} else {
// If switching to camera mode, clear any file state
document.getElementById('image-upload').value = null;
document.getElementById('file-name').textContent = '';
base64Image = null;
selectedMimeType = null;
}
// Style and visibility updates
const activeClasses = ['bg-white', 'text-indigo-700', 'border-2', 'border-indigo-500', 'shadow-md'];
const inactiveClasses = ['bg-transparent', 'text-gray-700', 'border-2', 'border-transparent', 'hover:bg-white'];
if (mode === 'file') {
fileSection.classList.remove('hidden');
cameraSection.classList.add('hidden');
modeFileBtn.classList.add(...activeClasses);
modeFileBtn.classList.remove(...inactiveClasses);
modeCameraBtn.classList.add(...inactiveClasses);
modeCameraBtn.classList.remove(...activeClasses);
} else if (mode === 'camera') {
fileSection.classList.add('hidden');
cameraSection.classList.remove('hidden');
modeCameraBtn.classList.add(...activeClasses);
modeCameraBtn.classList.remove(...inactiveClasses);
modeFileBtn.classList.add(...inactiveClasses);
modeFileBtn.classList.remove(...activeClasses);
}
// Ensure scan button reflects current state, disabled if no image is loaded
document.getElementById('scan-button').disabled = !base64Image;
}
/** Handles file selection and updates the UI */
window.handleFileChange = async function(event) {
stopCameraAndSetPlaceholder(); // Ensure camera is off if file is selected
const file = event.target.files[0];
const scanButton = document.getElementById('scan-button');
const fileNameDisplay = document.getElementById('file-name');
const resultArea = document.getElementById('result-area');
resultArea.classList.add('hidden'); // Hide result area on new file selection
if (file) {
if (file.size > 5 * 1024 * 1024) { // 5MB limit to prevent huge API payloads
showMessage("File Too Large", "Please select an image smaller than 5MB.", "error");
fileNameDisplay.textContent = '';
scanButton.disabled = true;
return;
}
try {
fileNameDisplay.textContent = `Selected: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`;
const base64Data = await fileToBase64(file);
handleCapturedImage(base64Data, file.type, false);
} catch (error) {
console.error("Error reading file:", error);
showMessage("File Error", "Could not read the selected file. Please try a different image.", "error");
fileNameDisplay.textContent = '';
scanButton.disabled = true;
handleCapturedImage(null, null);
}
} else {
fileNameDisplay.textContent = '';
scanButton.disabled = true;
handleCapturedImage(null, null);
}
}
/** Exponential Backoff Retry Function for API calls */
async function fetchWithRetry(url, options, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// Handle 429 (Too Many Requests) or other transient errors
if (response.status === 429 || response.status >= 500) {
throw new Error(`Transient error: Status ${response.status} - ${response.statusText}`);
}
// For non-transient errors (4xx other than 429), just throw
const errorJson = await response.json();
throw new Error(`API Error: ${response.status} - ${errorJson.error?.message || response.statusText}`);
} catch (error) {
console.error(`Attempt ${i + 1} failed:`, error.message);
if (i === maxRetries - 1) throw error; // Re-throw if all retries failed
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000; // Exponential backoff with jitter
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
/** Calls Gemini API to extract text from the image */
window.extractTextFromImage = async function() {
if (!base64Image || !selectedMimeType) {
showMessage("Missing Image", "Please select or capture an image file first.", "error");
return;
}
// Stop camera if it's running before scanning
if (currentMode === 'camera') {
stopCameraAndSetPlaceholder();
}
const scanButton = document.getElementById('scan-button');
const scanText = document.getElementById('scan-text');
const loadingIndicator = document.getElementById('loading-indicator');
const extractedTextarea = document.getElementById('extracted-text');
const resultArea = document.getElementById('result-area');
scanButton.disabled = true;
scanText.textContent = 'Scanning...';
loadingIndicator.classList.remove('hidden');
resultArea.classList.add('hidden');
extractedTextarea.value = '';
const userQuery = "Transcribe all text from this scanned document image exactly as it appears. Maintain original line breaks and formatting (like paragraph structure). Do not add any commentary or prefix/suffix text.";
const payload = {
contents: [
{
role: "user",
parts: [
{ text: userQuery },
{
inlineData: {
mimeType: selectedMimeType,
data: base64Image
}
}
]
}
],
// Safety settings adjusted to reduce the chance of blocking valid OCR results
safetySettings: [
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" },
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" }
],
systemInstruction: {
parts: [{ text: "You are an extremely accurate OCR transcription engine. Your sole task is to return the plain text content found in the image. You must be silent and return only the transcribed text." }]
}
};
try {
const response = await fetchWithRetry(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
// Safely extract the text from the response structure
const extractedText = result.candidates?.[0]?.content?.parts?.[0]?.text;
if (extractedText) {
extractedTextarea.value = extractedText;
resultArea.classList.remove('hidden');
showMessage("Extraction Complete", "The text was successfully extracted from your document. You can now edit it or download the PDF.", "success");
} else {
// Handle cases where the model returns a block reason instead of text
const blockReason = result.candidates?.[0]?.finishReason;
if (blockReason) {
throw new Error(`Request blocked by safety settings. Reason: ${blockReason}`);
}
throw new Error("API response was empty or malformed. No text could be extracted.");
}
} catch (error) {
console.error("Extraction failed:", error);
showMessage("Extraction Failed", `An error occurred during text extraction: ${error.message}. Please try again with a clearer image.`, "error");
} finally {
scanText.textContent = '1. Scan & Extract Text with AI';
loadingIndicator.classList.add('hidden');
// Only enable the scan button if an image is still loaded
if (base64Image) {
scanButton.disabled = false;
}
}
}
/** Generates and downloads a PDF from the extracted text using jsPDF */
window.generatePdf = function() {
const text = document.getElementById('extracted-text').value;
if (!text.trim()) {
showMessage("Empty Content", "Please extract text from an image or type content into the text area before generating a PDF.", "error");
return;
}
// The jsPDF library is loaded via a script tag in the HTML head
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
doc.setFont('Inter', 'normal'); // Set font to Inter
doc.setFontSize(12);
const pageWidth = doc.internal.pageSize.getWidth();
const margin = 20;
const textWidth = pageWidth - 2 * margin;
// Split the text into lines that fit the page width
const lines = doc.splitTextToSize(text, textWidth);
let y = margin;
const lineHeight = 10;
const pageHeight = doc.internal.pageSize.getHeight();
lines.forEach(line => {
// Check if the text will overflow the current page
if (y + lineHeight > pageHeight - margin) {
doc.addPage();
y = margin; // Reset Y position for new page
}
doc.text(line, margin, y);
y += lineHeight;
});
doc.save("Scanned_Document.pdf");
showMessage("PDF Downloaded", "Your extracted text has been converted to PDF and downloaded successfully.", "success");
}
// Initial setup
setMode('file');
</script>
</body>
</html>