diff --git a/README.md b/README.md index c88ae5c..7d703c3 100644 --- a/README.md +++ b/README.md @@ -96,36 +96,29 @@ Export your OCR results in the format you need: 5. View results with bounding boxes (if enabled) 6. Copy or download the extracted text -### Processing PDFs (Multi-Page Documents) - NEW! +### Processing PDFs (Multi-Page Documents) -1. Select **"PDF Processing"** mode in the toggle +Processing a PDF is just another way to create an OCR Job — it produces plain, +editable text exactly like single-image OCR, so it flows into the same review and +commit workflow. + +1. Select **"PDF Processing"** in the file-type toggle 2. Upload a PDF file (up to 100MB) -3. Choose your OCR mode (same as above) -4. Select **output format**: - - 📝 **Markdown** - For documentation, wikis, GitHub - - 🌐 **HTML** - For web publishing, styled viewing - - 📄 **DOCX** - For Word editing, professional documents - - 📊 **JSON** - For programmatic access, data extraction -5. Click **"Process PDF"** -6. Watch the progress bar as pages are processed -7. Your file downloads automatically when complete! +3. Pick a **Model** and **Mode** (same as image OCR) +4. Click **"Process PDF"** +5. All pages are OCR'd and concatenated into one plain-text result +6. Edit the text, fill in metadata, and **Commit Job** — the PDF is stored as the + job's source document (shown alongside the text in Browse Jobs) + +> The `/api/process-pdf` endpoint still supports Markdown / HTML / DOCX / JSON +> exports for programmatic use (see the API reference below); the UI uses the +> plain-text (JSON) path to drive the job workflow. ### Tips for Best Results - **For scanned documents**: Use higher DPI (144-300) in advanced settings - **For tables**: The model excels at extracting structured data - **For formulas**: Mathematical notation is preserved in output -- **For images in PDFs**: Enable "Extract Images" to include them in output -- **For large PDFs**: JSON format is fastest, DOCX takes longer due to formatting - -### Output Format Comparison - -| Format | Best For | Features | File Size | -|--------|----------|----------|-----------| -| **Markdown** | Documentation, GitHub, wikis | Clean text, tables, code blocks | Smallest | -| **HTML** | Web viewing, sharing | Styled output, embedded images, tables | Medium | -| **DOCX** | Editing, professional docs | Full formatting, images, tables | Largest | -| **JSON** | Data processing, APIs | Structured data, metadata, page info | Small | ## Features diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7144e10..09bc893 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from 'react' +import { useState, useCallback, useEffect, useMemo } from 'react' import { useSuggestions } from './hooks/useSuggestions' import { useModels } from './hooks/useModels' import { motion, AnimatePresence } from 'framer-motion' @@ -11,7 +11,6 @@ import ModeSelector from './components/ModeSelector' import ModelSelector from './components/ModelSelector' import ResultPanel from './components/ResultPanel' import AdvancedSettings from './components/AdvancedSettings' -import PDFProcessor from './components/PDFProcessor' import MetadataForm from './components/MetadataForm' import JobsPanel from './components/JobsPanel' import axios from 'axios' @@ -65,6 +64,13 @@ function App() { } }, [models, model]) + // Blob URL for previewing an uploaded PDF in the result/review view + const pdfUrl = useMemo( + () => (fileType === 'pdf' && image ? URL.createObjectURL(image) : null), + [fileType, image], + ) + useEffect(() => () => { if (pdfUrl) URL.revokeObjectURL(pdfUrl) }, [pdfUrl]) + // Show the full-screen result view once at least one committable mode has a result const showResultView = view === 'new_job' && Object.keys(modeResults).length > 0 @@ -101,31 +107,60 @@ function App() { }, [imagePreview, fileType]) const handleSubmit = async () => { - if (!image) { setError('Please upload an image first'); return } + if (!image) { setError(`Please upload ${fileType === 'pdf' ? 'a PDF' : 'an image'} first`); return } setLoading(true) setError(null) setCommitResult(null) try { - const formData = new FormData() - formData.append('image', image) - if (model) formData.append('model', model) - formData.append('mode', mode) - formData.append('prompt', prompt) - formData.append('grounding', mode === 'find_ref') - formData.append('include_caption', includeCaption) - formData.append('find_term', findTerm) - formData.append('schema', '') - formData.append('base_size', advancedSettings.base_size) - formData.append('image_size', advancedSettings.image_size) - formData.append('crop_mode', advancedSettings.crop_mode) - formData.append('test_compress', advancedSettings.test_compress) + let text = '' + let responseData = null - const response = await axios.post(`${API_BASE}/ocr`, formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }) - setResult(response.data) + if (fileType === 'pdf') { + // Process the whole PDF into one plain-text blob (all pages), then feed it + // into the same edit/review/commit flow as image OCR. + const formData = new FormData() + formData.append('pdf_file', image) + if (model) formData.append('model', model) + formData.append('mode', mode) + formData.append('output_format', 'json') + formData.append('extract_images', false) + formData.append('grounding', false) + formData.append('include_caption', includeCaption) + formData.append('dpi', 144) + formData.append('base_size', advancedSettings.base_size) + formData.append('image_size', advancedSettings.image_size) + formData.append('crop_mode', advancedSettings.crop_mode) + + const response = await axios.post(`${API_BASE}/process-pdf`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + const pages = response.data?.pages || [] + text = pages.map(p => (p.text || '').trim()).filter(Boolean).join('\n\n') + responseData = { text, metadata: response.data?.metadata, total_pages: response.data?.total_pages } + } else { + const formData = new FormData() + formData.append('image', image) + if (model) formData.append('model', model) + formData.append('mode', mode) + formData.append('prompt', prompt) + formData.append('grounding', mode === 'find_ref') + formData.append('include_caption', includeCaption) + formData.append('find_term', findTerm) + formData.append('schema', '') + formData.append('base_size', advancedSettings.base_size) + formData.append('image_size', advancedSettings.image_size) + formData.append('crop_mode', advancedSettings.crop_mode) + formData.append('test_compress', advancedSettings.test_compress) + + const response = await axios.post(`${API_BASE}/ocr`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + responseData = response.data + text = response.data.text || '' + } + + setResult(responseData) if (COMMITTABLE_MODES.has(mode)) { - const text = response.data.text || '' setModeResults(prev => ({ ...prev, [mode]: text })) setEditedResults(prev => ({ ...prev, [mode]: text })) setActiveResultMode(mode) @@ -300,7 +335,15 @@ function App() { {/* Image + Text */}
- {imagePreview && typeof imagePreview === 'string' ? ( + {fileType === 'pdf' && pdfUrl ? ( +
+ +
+

PDF preview unavailable

+
+
+
+ ) : imagePreview && typeof imagePreview === 'string' ? (
- {fileType === 'pdf' ? ( - - ) : ( - <> - -
-
- {loading ? ( - <>Processing Magic... - ) : ( - <>Analyze Image - )} -
- - - {error && ( - -

{error}

-
+ +
+
+ {loading ? ( + <>Processing... + ) : ( + <>{fileType === 'pdf' ? 'Process PDF' : 'Analyze Image'} )} - +
+ + + {error && ( + +

{error}

+
)} diff --git a/frontend/src/components/JobsPanel.jsx b/frontend/src/components/JobsPanel.jsx index 798d5e8..b68d2a2 100644 --- a/frontend/src/components/JobsPanel.jsx +++ b/frontend/src/components/JobsPanel.jsx @@ -318,12 +318,24 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} }) {/* Image + Text */}
- Job source { e.target.style.display = 'none' }} - /> + {(job.original_filename || '').toLowerCase().endsWith('.pdf') ? ( + +
+

PDF preview unavailable

+
+
+ ) : ( + Job source { e.target.style.display = 'none' }} + /> + )}
{/* Tabs — only show tabs that have content */} diff --git a/frontend/src/components/PDFProcessor.jsx b/frontend/src/components/PDFProcessor.jsx deleted file mode 100644 index 6e9b35d..0000000 --- a/frontend/src/components/PDFProcessor.jsx +++ /dev/null @@ -1,234 +0,0 @@ -import { useState, useCallback } from 'react' -import { motion, AnimatePresence } from 'framer-motion' -import { FileText, Download, Loader2, CheckCircle2, AlertCircle } from 'lucide-react' -import axios from 'axios' - -const API_BASE = import.meta.env.VITE_API_URL || '/api' - -function PDFProcessor({ pdfFile, mode, prompt, model, advancedSettings, includeCaption }) { - const [processing, setProcessing] = useState(false) - const [progress, setProgress] = useState(0) - const [result, setResult] = useState(null) - const [error, setError] = useState(null) - const [outputFormat, setOutputFormat] = useState('markdown') - - const formats = [ - { value: 'markdown', label: 'Markdown', ext: 'md', icon: '📝' }, - { value: 'html', label: 'HTML', ext: 'html', icon: '🌐' }, - { value: 'docx', label: 'Word', ext: 'docx', icon: '📄' }, - { value: 'json', label: 'JSON', ext: 'json', icon: '📊' } - ] - - const handleProcess = useCallback(async () => { - if (!pdfFile) return - - setProcessing(true) - setError(null) - setProgress(0) - - try { - const formData = new FormData() - formData.append('pdf_file', pdfFile) - if (model) formData.append('model', model) - formData.append('mode', mode) - formData.append('prompt', prompt) - formData.append('output_format', outputFormat) - formData.append('grounding', mode === 'find_ref') - formData.append('include_caption', includeCaption) - formData.append('extract_images', true) - formData.append('dpi', 144) - formData.append('base_size', advancedSettings.base_size) - formData.append('image_size', advancedSettings.image_size) - formData.append('crop_mode', advancedSettings.crop_mode) - - const response = await axios.post(`${API_BASE}/process-pdf`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - responseType: outputFormat === 'json' ? 'json' : 'blob', - onUploadProgress: (progressEvent) => { - const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total) - setProgress(percentCompleted) - } - }) - - if (outputFormat === 'json') { - setResult(response.data) - } else { - // For file downloads (markdown, html, docx) - const format = formats.find(f => f.value === outputFormat) - const blob = new Blob([response.data], { - type: response.headers['content-type'] - }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `ocr_result.${format.ext}` - a.click() - URL.revokeObjectURL(url) - - setResult({ - success: true, - message: `Document downloaded as ${format.label}`, - format: outputFormat - }) - } - - setProgress(100) - } catch (err) { - console.error('PDF processing error:', err) - setError(err.response?.data?.detail || err.message || 'Failed to process PDF') - } finally { - setProcessing(false) - } - }, [pdfFile, mode, prompt, model, outputFormat, includeCaption, advancedSettings]) - - const handleDownloadJSON = useCallback(() => { - if (!result || outputFormat !== 'json') return - - const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = 'ocr_result.json' - a.click() - URL.revokeObjectURL(url) - }, [result, outputFormat]) - - return ( -
- {/* Format Selector */} -
- -
- {formats.map((format) => ( - setOutputFormat(format.value)} - className={`p-3 rounded-xl text-sm font-medium transition-all ${ - outputFormat === format.value - ? 'bg-gradient-to-r from-purple-600 to-cyan-600 text-white' - : 'glass text-gray-400 hover:bg-white/5' - }`} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - {format.icon} - {format.label} - - ))} -
-
- - {/* Process Button */} - -
-
- {processing ? ( - <> - - Processing PDF... - - ) : ( - <> - - Process PDF - - )} -
- - - {/* Progress Bar */} - - {processing && progress > 0 && ( - -
- Processing... - {progress}% -
-
- -
-
- )} -
- - {/* Error Display */} - - {error && ( - - -
-

Processing Failed

-

{error}

-
-
- )} -
- - {/* Success Display */} - - {result && !error && ( - -
- -
-

- {result.message || 'PDF processed successfully!'} -

- {outputFormat === 'json' && result.pages && ( -
-

- Processed {result.total_pages} page{result.total_pages > 1 ? 's' : ''} -

- - - Download JSON - -
- )} -
-
-
- )} -
-
- ) -} - -export default PDFProcessor