doctra 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
doctra/ui/docres_ui.py ADDED
@@ -0,0 +1,338 @@
1
+ """
2
+ DocRes Image Restoration UI Module
3
+
4
+ This module contains all functionality for the DocRes image restoration tab in the Doctra Gradio interface.
5
+ It handles PDF restoration, before/after comparison, and enhanced file management.
6
+ """
7
+
8
+ import tempfile
9
+ from pathlib import Path
10
+ from typing import Tuple, List, Optional
11
+
12
+ import gradio as gr
13
+
14
+ from doctra.ui.docres_wrapper import DocResUIWrapper
15
+ from doctra.utils.pdf_io import render_pdf_to_images
16
+
17
+
18
+ def render_pdf_pages(pdf_path: str, max_pages: int = 10) -> Tuple[List[str], List[str]]:
19
+ """
20
+ Render PDF pages to images for display.
21
+
22
+ Args:
23
+ pdf_path: Path to PDF file
24
+ max_pages: Maximum number of pages to render
25
+
26
+ Returns:
27
+ Tuple of (image_paths, page_options)
28
+ """
29
+ if not pdf_path or not Path(pdf_path).exists():
30
+ return [], []
31
+
32
+ try:
33
+ # render_pdf_to_images returns (pil_image, width, height) tuples
34
+ image_tuples = render_pdf_to_images(pdf_path)
35
+
36
+ # Limit to max_pages if specified
37
+ if max_pages and len(image_tuples) > max_pages:
38
+ image_tuples = image_tuples[:max_pages]
39
+
40
+ # Convert PIL images to file paths for display
41
+ images = []
42
+ page_options = []
43
+
44
+ for i, (pil_image, width, height) in enumerate(image_tuples):
45
+ # Save PIL image to temporary file
46
+ temp_file = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
47
+ pil_image.save(temp_file.name, 'PNG')
48
+ images.append(temp_file.name)
49
+ page_options.append(f"Page {i+1}")
50
+
51
+ return images, page_options
52
+ except Exception as e:
53
+ print(f"Error rendering PDF pages: {e}")
54
+ return [], []
55
+
56
+
57
+ def update_docres_page_selector(original_pdf: str, enhanced_pdf: str) -> Tuple[gr.Dropdown, List[str], List[str], str, str, Optional[str], Optional[str]]:
58
+ """
59
+ Update single page selector when PDFs are loaded.
60
+
61
+ Args:
62
+ original_pdf: Path to original PDF file
63
+ enhanced_pdf: Path to enhanced PDF file
64
+
65
+ Returns:
66
+ Tuple of (dropdown, original_pages, enhanced_pages, original_pdf_path, enhanced_pdf_path, first_original_image, first_enhanced_image)
67
+ """
68
+ original_pages, original_options = render_pdf_pages(original_pdf) if original_pdf else ([], [])
69
+ enhanced_pages, enhanced_options = render_pdf_pages(enhanced_pdf) if enhanced_pdf else ([], [])
70
+
71
+ # Use the same page options for the single selector (use the longer list)
72
+ if len(original_options) >= len(enhanced_options):
73
+ common_options = original_options
74
+ else:
75
+ common_options = enhanced_options
76
+
77
+ # Set default to first page if available
78
+ default_page = common_options[0] if common_options else None
79
+
80
+ return (
81
+ gr.Dropdown(choices=common_options, value=default_page, visible=bool(common_options)),
82
+ original_pages,
83
+ enhanced_pages,
84
+ original_pdf or "",
85
+ enhanced_pdf or "",
86
+ original_pages[0] if original_pages else None, # First page image
87
+ enhanced_pages[0] if enhanced_pages else None # First page image
88
+ )
89
+
90
+
91
+ def display_docres_page(page_selector: str, pages_data: List[str], pdf_path: str) -> Optional[str]:
92
+ """
93
+ Display selected page from PDF.
94
+
95
+ Args:
96
+ page_selector: Selected page identifier
97
+ pages_data: List of page image paths
98
+ pdf_path: Path to PDF file
99
+
100
+ Returns:
101
+ Path to selected page image
102
+ """
103
+ if not page_selector or not pages_data or not pdf_path:
104
+ return None
105
+
106
+ try:
107
+ page_index = int(page_selector.split()[1]) - 1 # "Page 1" -> index 0
108
+ if 0 <= page_index < len(pages_data):
109
+ return pages_data[page_index]
110
+ except (ValueError, IndexError):
111
+ pass
112
+
113
+ return None
114
+
115
+
116
+ def sync_page_changes(
117
+ page_selector: str,
118
+ original_pages: List[str],
119
+ enhanced_pages: List[str],
120
+ original_pdf_path: str,
121
+ enhanced_pdf_path: str
122
+ ) -> Tuple[Optional[str], Optional[str]]:
123
+ """
124
+ Synchronize page changes between original and enhanced PDFs.
125
+
126
+ Args:
127
+ page_selector: Selected page identifier
128
+ original_pages: List of original page image paths
129
+ enhanced_pages: List of enhanced page image paths
130
+ original_pdf_path: Path to original PDF
131
+ enhanced_pdf_path: Path to enhanced PDF
132
+
133
+ Returns:
134
+ Tuple of (original_page_image, enhanced_page_image)
135
+ """
136
+ if not page_selector:
137
+ return None, None
138
+
139
+ # Get the page index
140
+ try:
141
+ page_index = int(page_selector.split()[1]) - 1 # "Page 1" -> index 0
142
+ except (ValueError, IndexError):
143
+ return None, None
144
+
145
+ # Get the corresponding page from each PDF
146
+ original_page = None
147
+ enhanced_page = None
148
+
149
+ if original_pages and 0 <= page_index < len(original_pages):
150
+ original_page = original_pages[page_index]
151
+
152
+ if enhanced_pages and 0 <= page_index < len(enhanced_pages):
153
+ enhanced_page = enhanced_pages[page_index]
154
+
155
+ return original_page, enhanced_page
156
+
157
+
158
+ def run_docres_restoration(
159
+ pdf_file: str,
160
+ task: str,
161
+ device: str,
162
+ dpi: int,
163
+ save_enhanced: bool,
164
+ save_images: bool
165
+ ) -> Tuple[str, Optional[str], Optional[str], Optional[dict], List[str]]:
166
+ """
167
+ Run DocRes image restoration on PDF.
168
+
169
+ Args:
170
+ pdf_file: Path to input PDF file
171
+ task: Restoration task type
172
+ device: Device to use for processing
173
+ dpi: DPI for processing
174
+ save_enhanced: Whether to save enhanced PDF
175
+ save_images: Whether to save enhanced images
176
+
177
+ Returns:
178
+ Tuple of (status_message, original_pdf_path, enhanced_pdf_path, metadata, file_paths)
179
+ """
180
+ if not pdf_file:
181
+ return ("No file provided.", None, [], None, [])
182
+
183
+ try:
184
+ # Initialize DocRes engine
185
+ device_str = None if device == "auto" else device
186
+ docres = DocResUIWrapper(device=device_str)
187
+
188
+ # Extract filename
189
+ original_filename = Path(pdf_file).stem
190
+
191
+ # Create output directory
192
+ output_dir = Path("outputs") / f"{original_filename}_docres"
193
+ output_dir.mkdir(parents=True, exist_ok=True)
194
+
195
+ # Run DocRes restoration
196
+ enhanced_pdf_path = output_dir / f"{original_filename}_enhanced.pdf"
197
+ docres.restore_pdf(
198
+ pdf_path=pdf_file,
199
+ output_path=str(enhanced_pdf_path),
200
+ task=task,
201
+ dpi=dpi
202
+ )
203
+
204
+ # Prepare outputs
205
+ file_paths = []
206
+
207
+ if save_enhanced and enhanced_pdf_path.exists():
208
+ file_paths.append(str(enhanced_pdf_path))
209
+
210
+ if save_images:
211
+ # Look for enhanced images
212
+ images_dir = output_dir / "enhanced_images"
213
+ if images_dir.exists():
214
+ for img_path in sorted(images_dir.glob("*.jpg")):
215
+ file_paths.append(str(img_path))
216
+
217
+ # Create metadata
218
+ metadata = {
219
+ "task": task,
220
+ "device": str(docres.device),
221
+ "dpi": dpi,
222
+ "original_file": pdf_file,
223
+ "enhanced_file": str(enhanced_pdf_path) if enhanced_pdf_path.exists() else None,
224
+ "output_directory": str(output_dir)
225
+ }
226
+
227
+ status_msg = f"✅ DocRes restoration completed successfully!\n📁 Output directory: {output_dir}"
228
+
229
+ enhanced_pdf_file = str(enhanced_pdf_path) if enhanced_pdf_path.exists() else None
230
+ return (status_msg, pdf_file, enhanced_pdf_file, metadata, file_paths)
231
+
232
+ except Exception as e:
233
+ error_msg = f"❌ DocRes restoration failed: {str(e)}"
234
+ return (error_msg, None, None, None, [])
235
+
236
+
237
+ def create_docres_tab() -> Tuple[gr.Tab, dict]:
238
+ """
239
+ Create the DocRes Image Restoration tab with all its components and functionality.
240
+
241
+ Returns:
242
+ Tuple of (tab_component, state_variables_dict)
243
+ """
244
+ with gr.Tab("DocRes Image Restoration") as tab:
245
+ # Input controls
246
+ with gr.Row():
247
+ pdf_docres = gr.File(file_types=[".pdf"], label="PDF")
248
+ docres_task_standalone = gr.Dropdown(
249
+ ["appearance", "dewarping", "deshadowing", "deblurring", "binarization", "end2end"],
250
+ value="appearance",
251
+ label="Restoration Task"
252
+ )
253
+ docres_device_standalone = gr.Dropdown(
254
+ ["auto", "cuda", "cpu"],
255
+ value="auto",
256
+ label="Device"
257
+ )
258
+
259
+ # Additional settings
260
+ with gr.Row():
261
+ docres_dpi = gr.Slider(100, 400, value=200, step=10, label="DPI")
262
+ docres_save_enhanced = gr.Checkbox(label="Save Enhanced PDF", value=True)
263
+ docres_save_images = gr.Checkbox(label="Save Enhanced Images", value=True)
264
+
265
+ # Action button
266
+ run_docres_btn = gr.Button("▶ Run DocRes Restoration", variant="primary")
267
+ docres_status = gr.Textbox(label="Status", elem_classes=["status-ok"])
268
+
269
+ # Single page selector for both PDFs
270
+ with gr.Row():
271
+ docres_page_selector = gr.Dropdown(label="Select Page", interactive=True, visible=False)
272
+
273
+ # Before/After PDF comparison
274
+ with gr.Row():
275
+ with gr.Column():
276
+ gr.Markdown("### 📄 Original PDF")
277
+ docres_original_pdf = gr.File(label="Original PDF File", interactive=False, visible=False)
278
+ docres_original_page_image = gr.Image(label="Original PDF Page", interactive=False, height=800)
279
+ with gr.Column():
280
+ gr.Markdown("### ✨ Enhanced PDF")
281
+ docres_enhanced_pdf = gr.File(label="Enhanced PDF File", interactive=False, visible=False)
282
+ docres_enhanced_page_image = gr.Image(label="Enhanced PDF Page", interactive=False, height=800)
283
+
284
+ # DocRes outputs
285
+ with gr.Row():
286
+ with gr.Column():
287
+ docres_metadata = gr.JSON(label="Restoration Metadata", visible=False)
288
+
289
+ docres_files_out = gr.Files(label="Download enhanced files")
290
+
291
+ # State variables for PDF page data
292
+ docres_original_pages_state = gr.State([])
293
+ docres_enhanced_pages_state = gr.State([])
294
+ docres_original_pdf_path_state = gr.State("")
295
+ docres_enhanced_pdf_path_state = gr.State("")
296
+
297
+ # Event handlers
298
+ run_docres_btn.click(
299
+ fn=run_docres_restoration,
300
+ inputs=[pdf_docres, docres_task_standalone, docres_device_standalone, docres_dpi, docres_save_enhanced, docres_save_images],
301
+ outputs=[docres_status, docres_original_pdf, docres_enhanced_pdf, docres_metadata, docres_files_out]
302
+ ).then(
303
+ fn=update_docres_page_selector,
304
+ inputs=[docres_original_pdf, docres_enhanced_pdf],
305
+ outputs=[docres_page_selector, docres_original_pages_state, docres_enhanced_pages_state, docres_original_pdf_path_state, docres_enhanced_pdf_path_state, docres_original_page_image, docres_enhanced_page_image]
306
+ )
307
+
308
+ # Handle single page selector changes
309
+ docres_page_selector.change(
310
+ fn=sync_page_changes,
311
+ inputs=[docres_page_selector, docres_original_pages_state, docres_enhanced_pages_state, docres_original_pdf_path_state, docres_enhanced_pdf_path_state],
312
+ outputs=[docres_original_page_image, docres_enhanced_page_image]
313
+ )
314
+
315
+ # Return state variables for external access
316
+ state_vars = {
317
+ 'pdf_docres': pdf_docres,
318
+ 'docres_task_standalone': docres_task_standalone,
319
+ 'docres_device_standalone': docres_device_standalone,
320
+ 'docres_dpi': docres_dpi,
321
+ 'docres_save_enhanced': docres_save_enhanced,
322
+ 'docres_save_images': docres_save_images,
323
+ 'run_docres_btn': run_docres_btn,
324
+ 'docres_status': docres_status,
325
+ 'docres_page_selector': docres_page_selector,
326
+ 'docres_original_pdf': docres_original_pdf,
327
+ 'docres_original_page_image': docres_original_page_image,
328
+ 'docres_enhanced_pdf': docres_enhanced_pdf,
329
+ 'docres_enhanced_page_image': docres_enhanced_page_image,
330
+ 'docres_metadata': docres_metadata,
331
+ 'docres_files_out': docres_files_out,
332
+ 'docres_original_pages_state': docres_original_pages_state,
333
+ 'docres_enhanced_pages_state': docres_enhanced_pages_state,
334
+ 'docres_original_pdf_path_state': docres_original_pdf_path_state,
335
+ 'docres_enhanced_pdf_path_state': docres_enhanced_pdf_path_state
336
+ }
337
+
338
+ return tab, state_vars
@@ -0,0 +1,120 @@
1
+ """
2
+ DocRes wrapper for Gradio UI context
3
+
4
+ This module provides a wrapper around DocResEngine that handles
5
+ path issues when running in Gradio UI context.
6
+ """
7
+
8
+ import os
9
+ import tempfile
10
+ import shutil
11
+ from pathlib import Path
12
+ from typing import Optional, Union
13
+ import numpy as np
14
+
15
+ from doctra.engines.image_restoration.docres_engine import DocResEngine
16
+
17
+
18
+ class DocResUIWrapper:
19
+ """
20
+ Wrapper for DocResEngine that handles path issues in Gradio UI context
21
+ """
22
+
23
+ def __init__(self, device: Optional[str] = None):
24
+ """
25
+ Initialize the DocRes wrapper
26
+
27
+ Args:
28
+ device: Device to use ('cuda', 'cpu', or None for auto-detect)
29
+ """
30
+ self.device = device
31
+ self._docres = None
32
+ self._setup_model_paths()
33
+
34
+ def _setup_model_paths(self):
35
+ """Setup model paths to work in Gradio context"""
36
+ try:
37
+ # Initialize DocRes to download models
38
+ self._docres = DocResEngine(device=self.device)
39
+
40
+ # Get the actual model paths
41
+ mbd_path = self._docres.mbd_path
42
+ model_path = self._docres.model_path
43
+
44
+ # Get DocRes directory
45
+ current_dir = Path(__file__).parent.parent
46
+ docres_dir = current_dir / "third_party" / "docres"
47
+
48
+ # Create expected directory structure
49
+ expected_mbd_dir = docres_dir / "data" / "MBD" / "checkpoint"
50
+ expected_model_dir = docres_dir / "checkpoints"
51
+
52
+ expected_mbd_dir.mkdir(parents=True, exist_ok=True)
53
+ expected_model_dir.mkdir(parents=True, exist_ok=True)
54
+
55
+ # Copy model files to expected locations if they don't exist
56
+ expected_mbd_path = expected_mbd_dir / "mbd.pkl"
57
+ expected_model_path = expected_model_dir / "docres.pkl"
58
+
59
+ if not expected_mbd_path.exists() and Path(mbd_path).exists():
60
+ shutil.copy2(mbd_path, expected_mbd_path)
61
+
62
+ if not expected_model_path.exists() and Path(model_path).exists():
63
+ shutil.copy2(model_path, expected_model_path)
64
+
65
+ except Exception as e:
66
+ print(f"⚠️ DocRes setup warning: {e}")
67
+ # Continue anyway, the original paths might work
68
+
69
+ def restore_pdf(self, pdf_path: str, output_path: str, task: str = "appearance", dpi: int = 200) -> str:
70
+ """
71
+ Restore a PDF using DocRes with proper path handling
72
+
73
+ Args:
74
+ pdf_path: Path to input PDF
75
+ output_path: Path to output enhanced PDF
76
+ task: Restoration task
77
+ dpi: DPI for processing
78
+
79
+ Returns:
80
+ Path to enhanced PDF
81
+ """
82
+ if self._docres is None:
83
+ raise RuntimeError("DocRes not properly initialized")
84
+
85
+ try:
86
+ # Use the original DocRes method
87
+ return self._docres.restore_pdf(pdf_path, output_path, task, dpi)
88
+ except Exception as e:
89
+ # If it fails due to path issues, try to fix them and retry once
90
+ if "No such file or directory" in str(e) and "mbd.pkl" in str(e):
91
+ print("🔧 Attempting to fix DocRes model paths...")
92
+ self._setup_model_paths()
93
+ return self._docres.restore_pdf(pdf_path, output_path, task, dpi)
94
+ else:
95
+ raise e
96
+
97
+ def restore_image(self, image: Union[str, np.ndarray], task: str = "appearance") -> tuple:
98
+ """
99
+ Restore a single image using DocRes
100
+
101
+ Args:
102
+ image: Image path or numpy array
103
+ task: Restoration task
104
+
105
+ Returns:
106
+ Tuple of (restored_image, metadata)
107
+ """
108
+ if self._docres is None:
109
+ raise RuntimeError("DocRes not properly initialized")
110
+
111
+ try:
112
+ return self._docres.restore_image(image, task)
113
+ except Exception as e:
114
+ # If it fails due to path issues, try to fix them and retry once
115
+ if "No such file or directory" in str(e) and "mbd.pkl" in str(e):
116
+ print("🔧 Attempting to fix DocRes model paths...")
117
+ self._setup_model_paths()
118
+ return self._docres.restore_image(image, task)
119
+ else:
120
+ raise e