commit 9c5e13f342ab559b193e770887df2ab774e4b196 Author: pjmalandrino Date: Tue May 13 15:20:48 2025 +0200 Push code produced this Week end diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e0b907 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +python analyzer.py --image document.pdf --page 13 + +python visualizer.py --doctags output.doctags.txt --pdf document.pdf --page 13 --adjust --show + + +python analyzer.py --image document.pdf --page 13 && python visualizer.py --doctags output.doctags.txt --pdf document.pdf --page 13 --adjust --show \ No newline at end of file diff --git a/analyzer.py b/analyzer.py new file mode 100644 index 0000000..e454aa7 --- /dev/null +++ b/analyzer.py @@ -0,0 +1,650 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "docling-core", +# "mlx-vlm", +# "pillow", +# "requests", +# "argparse", +# "pdf2image", +# "pymupdf", # Optional for better PDF handling +# ] +# /// +import argparse +import os +import tempfile +import re +import base64 +from io import BytesIO +from pathlib import Path +from urllib.parse import urlparse +import requests +from PIL import Image, UnidentifiedImageError +from pdf2image import convert_from_path, convert_from_bytes +from docling_core.types.doc import ImageRefMode +from docling_core.types.doc.document import DocTagsDocument, DoclingDocument + + +def parse_arguments(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description='Convert an image or PDF to docling format') + parser.add_argument('--image', '-i', type=str, required=True, + help='Path to local image file, PDF file, or URL') + parser.add_argument('--prompt', '-p', type=str, default="Convert this page to docling.", + help='Prompt for the model') + parser.add_argument('--output', '-o', type=str, default="./output.html", + help='Output file path') + parser.add_argument('--show', '-s', action='store_true', + help='Show output in browser') + parser.add_argument('--page', type=int, default=1, + help='Page number to process for PDF files (starts at 1)') + parser.add_argument('--dpi', type=int, default=200, + help='DPI for PDF rendering (higher values produce larger images)') + parser.add_argument('--debug', '-d', action='store_true', + help='Enable debug mode with extra output') + parser.add_argument('--doctags-only', action='store_true', + help='Generate only raw DocTags output without processing') + parser.add_argument('--all-pages', '-a', action='store_true', + help='Process all pages in a PDF without asking') + parser.add_argument('--start-page', type=int, default=1, + help='Start processing PDF from this page number') + parser.add_argument('--end-page', type=int, default=None, + help='Stop processing PDF at this page number') + parser.add_argument('--max-pages', type=int, default=None, + help='Maximum number of pages to process') + return parser.parse_args() + + +def load_image(image_path, page_num=1, dpi=200): + """Load image from URL, local image file, or PDF.""" + if urlparse(image_path).scheme in ['http', 'https']: # it is a URL + try: + response = requests.get(image_path, stream=True, timeout=10) + response.raise_for_status() + content = response.content + + # Check if it's a PDF + if image_path.lower().endswith('.pdf') or response.headers.get('Content-Type') == 'application/pdf': + print(f"Converting PDF from URL (page {page_num})...") + pdf_images = convert_from_bytes(content, dpi=dpi, first_page=page_num, last_page=page_num) + if not pdf_images: + raise Exception(f"Could not extract page {page_num} from PDF") + return pdf_images[0] # Return the first (and only) page + else: + return Image.open(BytesIO(content)) + except requests.exceptions.RequestException as e: + raise Exception(f"Error loading image from URL: {e}") + else: # it is a local file + image_path = Path(image_path) + if not image_path.exists(): + raise FileNotFoundError(f"File not found: {image_path}") + + # Check if it's a PDF + if image_path.suffix.lower() == '.pdf': + print(f"Converting PDF to image (page {page_num}, DPI: {dpi})...") + try: + pdf_images = convert_from_path( + image_path, + dpi=dpi, + first_page=page_num, + last_page=page_num + ) + if not pdf_images: + raise Exception(f"Could not extract page {page_num} from PDF") + return pdf_images[0] # Return the requested page + except Exception as e: + raise Exception(f"Error converting PDF to image: {e}") + else: + try: + return Image.open(image_path) + except UnidentifiedImageError: + raise Exception(f"Cannot identify image file: {image_path}. Make sure it's a valid image format or PDF.") + + +def cleanup_doctags(doctags_text): + """Clean up the DocTags structure.""" + print("Cleaning up DocTags structure...") + + # Simplified approach to extract valuable information + # Extract headers + headers = re.findall(r'.*?>(.*?)', doctags_text) + + # Extract text paragraphs + paragraphs = re.findall(r'.*?>(.*?)', doctags_text) + + # Extract list items + list_items = re.findall(r'.*?>(.*?)', doctags_text) + + # Extract footer + footer = re.search(r'.*?>(.*?)', doctags_text) + footer_text = footer.group(1) if footer else "" + + # Create a clean doctags structure + clean_doctags = "\n" + + # Add headers + for header in headers: + clean_doctags += f"{header}\n" + + # Add text + for paragraph in paragraphs: + clean_doctags += f"{paragraph}\n" + + # Add list if any items found + if list_items: + clean_doctags += "\n" + for item in list_items: + clean_doctags += f"{item}\n" + clean_doctags += "\n" + + # Add footer if present + if footer_text: + clean_doctags += f"{footer_text}\n" + + clean_doctags += "" + + return clean_doctags + + +def extract_all_tags(doctags_text): + """Extract all unique DocTags from the text.""" + print("Extracting all DocTags...") + + # Use regex to find all tags + tag_pattern = r']*)?>' + all_tags = re.findall(tag_pattern, doctags_text) + + # Remove duplicates and sort + unique_tags = sorted(set(all_tags)) + + # Create a list of tags with their frequencies + tag_counts = {} + for tag in all_tags: + tag_counts[tag] = tag_counts.get(tag, 0) + 1 + + # Format the output + tags_output = "# DocTags Found\n\n" + tags_output += "| Tag | Count |\n" + tags_output += "|-----|-------|\n" + + for tag in unique_tags: + tags_output += f"| {tag} | {tag_counts[tag]} |\n" + + # Add examples section + tags_output += "\n\n# DocTags Examples\n\n" + + # Find example usages for each tag + for tag in unique_tags: + # Find an opening tag with content + open_pattern = f'<{tag}(?:\\s[^>]*)?>.*?' + examples = re.findall(open_pattern, doctags_text, re.DOTALL) + + if examples: + # Limit to first example + example = examples[0] + # Truncate if too long + if len(example) > 200: + example = example[:197] + "..." + + tags_output += f"## {tag}\n\n```xml\n{example}\n```\n\n" + + return tags_output + + +def debug_doctags(doctags_text, debug_mode=False): + """Debug function to analyze doctag structure.""" + if not debug_mode: + return doctags_text + + print("\nAnalyzing DocTags structure:") + print(f"Total length: {len(doctags_text)} characters") + + # Check for valid opening and closing tags + opening_tags = [] + i = 0 + while i < len(doctags_text): + open_tag_start = doctags_text.find('<', i) + if open_tag_start == -1: + break + + open_tag_end = doctags_text.find('>', open_tag_start) + if open_tag_end == -1: + print(f"WARNING: Unclosed tag starting at position {open_tag_start}") + break + + tag_content = doctags_text[open_tag_start+1:open_tag_end] + if tag_content.startswith('/'): + # This is a closing tag + if not opening_tags: + print(f"WARNING: Closing tag {tag_content} without matching opening tag") + else: + last_open = opening_tags.pop() + if last_open != tag_content[1:]: + print(f"WARNING: Mismatched tags: opening <{last_open}> vs closing <{tag_content}>") + elif not tag_content.startswith('!') and not ' ' in tag_content: + # This is an opening tag (not a comment or self-closing) + opening_tags.append(tag_content) + + i = open_tag_end + 1 + + if opening_tags: + print(f"WARNING: Unclosed tags: {', '.join(opening_tags)}") + + # Count all tags + tag_counts = {} + tag_pattern = r'<(\w+)(?:\s|>)' + import re + for tag in re.findall(tag_pattern, doctags_text): + tag_counts[tag] = tag_counts.get(tag, 0) + 1 + + print("Tag counts:") + for tag, count in tag_counts.items(): + print(f" <{tag}>: {count}") + + # Special check for doctag + if doctags_text.count('') != 1: + print(f"WARNING: Expected 1 tag, found {doctags_text.count('')}") + if doctags_text.count('') != 1: + print(f"WARNING: Expected 1 tag, found {doctags_text.count('')}") + + return doctags_text + + +def create_html_document(image_path, doctags_text, pil_image, args): + """Create HTML directly from DocTags and image.""" + print("Creating HTML document directly...") + + # Get filename for document title + doc_name = Path(image_path).stem + + # Extract content from doctags + title_match = re.search(r'.*?>(.*?)', doctags_text) + title = title_match.group(1) if title_match else doc_name + + # Clean title by removing any location tags + title = re.sub(r'', '', title) + + # Extract text content + text_matches = re.findall(r'.*?>(.*?)', doctags_text) + paragraphs = [] + for text in text_matches: + # Clean text by removing any location tags + cleaned_text = re.sub(r'', '', text) + paragraphs.append(cleaned_text) + + # Extract list items + list_items = [] + item_matches = re.findall(r'.*?>(.*?)', doctags_text) + for item in item_matches: + # Clean item by removing any location tags + cleaned_item = re.sub(r'', '', item) + list_items.append(cleaned_item) + + # Extract footer + footer_match = re.search(r'.*?>(.*?)', doctags_text) + footer_text = "" + if footer_match: + footer_text = re.sub(r'', '', footer_match.group(1)) + + # Create image data URI + max_dimension = 1200 + img_for_embedding = pil_image + if max(pil_image.size) > max_dimension: + ratio = min(max_dimension / pil_image.size[0], max_dimension / pil_image.size[1]) + new_size = (int(pil_image.size[0] * ratio), int(pil_image.size[1] * ratio)) + img_for_embedding = pil_image.resize(new_size, Image.LANCZOS) + + buffered = BytesIO() + img_for_embedding.save(buffered, format="PNG", optimize=True, quality=90) + img_str = base64.b64encode(buffered.getvalue()).decode() + img_data_uri = f"data:image/png;base64,{img_str}" + + # Build HTML paragraphs + paragraphs_html = "".join([f"

{text}

\n" for text in paragraphs]) if paragraphs else "" + + # Build list HTML + list_html = "" + if list_items: + list_html = "
    \n" + for item in list_items: + list_html += f"
  • {item}
  • \n" + list_html += "
\n" + + # Create footer HTML + footer_html = f"\n" if footer_text else "" + + # Create HTML document + html_content = f''' + + + +{title} + + + +
+

{title}

+
+ Document Image +
Page {args.page}
+
+{paragraphs_html} +{list_html} +{footer_html} +
+ +''' + + return html_content + + +def create_markdown_document(image_path, doctags_text, args): + """Create Markdown document from DocTags.""" + print("Creating Markdown document...") + + # Get filename for document title + doc_name = Path(image_path).stem + + # Extract content from doctags + title_match = re.search(r'.*?>(.*?)', doctags_text) + title = title_match.group(1) if title_match else doc_name + + # Clean title by removing any location tags + title = re.sub(r'', '', title) + + # Extract text content + text_matches = re.findall(r'.*?>(.*?)', doctags_text) + paragraphs = [] + for text in text_matches: + # Clean text by removing any location tags + cleaned_text = re.sub(r'', '', text) + paragraphs.append(cleaned_text) + + # Extract list items + list_items = [] + item_matches = re.findall(r'.*?>(.*?)', doctags_text) + for item in item_matches: + # Clean item by removing any location tags + cleaned_item = re.sub(r'', '', item) + list_items.append(cleaned_item) + + # Extract footer + footer_match = re.search(r'.*?>(.*?)', doctags_text) + footer_text = "" + if footer_match: + footer_text = re.sub(r'', '', footer_match.group(1)) + + # Build markdown content + markdown = f"# {title}\n\n" + markdown += f"*Page {args.page}*\n\n" + + # Add paragraphs + for p in paragraphs: + markdown += f"{p}\n\n" + + # Add list + if list_items: + for item in list_items: + markdown += f"* {item}\n" + markdown += "\n" + + # Add footer + if footer_text: + markdown += f"---\n{footer_text}\n" + + return markdown + + +def process_page(args, model, processor, config, image_path, pil_image, page_num=1): + """Process a single page from a PDF or image file.""" + from mlx_vlm.prompt_utils import apply_chat_template + from mlx_vlm.utils import stream_generate + + # Prepare input + prompt = args.prompt + output_base = Path(args.output) + output_path = output_base + + # If processing PDF and output is a path without explicit numbering, add page numbers + if Path(image_path).suffix.lower() == '.pdf' and page_num > 1: + # Get base filename without extension + base_name = output_base.stem + output_path = output_base.parent / f"{base_name}_page{page_num}{output_base.suffix}" + + print(f"Processing page {page_num}, output will be saved to {output_path}") + + # Create a temporary file for the image + with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_img_file: + temp_img_path = temp_img_file.name + pil_image.save(temp_img_path, format='PNG') + print(f"Saved temporary image to: {temp_img_path}") + + try: + # Apply chat template + formatted_prompt = apply_chat_template(processor, config, prompt, num_images=1) + + # Generate output + print(f"Generating DocTags for page {page_num}: \n\n") + output = "" + for token in stream_generate( + model, processor, formatted_prompt, [temp_img_path], max_tokens=4096, verbose=False + ): + output += token.text + print(token.text, end="") + if "" in token.text: + break + print("\n\n") + + # Debug and clean the doctags content + output = debug_doctags(output, args.debug) + + # Extract all tags if in DocTags-only mode + if args.doctags_only: + tags_analysis = extract_all_tags(output) + + # Clean the output for document creation + cleaned_output = cleanup_doctags(output) + finally: + # Clean up the temporary file + if os.path.exists(temp_img_path): + os.unlink(temp_img_path) + print(f"Removed temporary image file") + + # Save the raw DocTags to a txt file + doctags_path = output_path.with_suffix('.doctags.txt') + with open(doctags_path, 'w', encoding='utf-8') as f: + f.write(output) + print(f"Raw DocTags saved to: {doctags_path}") + + # Save the tag analysis if in DocTags-only mode + if args.doctags_only: + tags_path = output_path.with_suffix('.tags.md') + with open(tags_path, 'w', encoding='utf-8') as f: + f.write(tags_analysis) + print(f"DocTags analysis saved to: {tags_path}") + + # If not in DocTags-only mode, create documents + if not args.doctags_only: + # Create markdown document + md_content = create_markdown_document(image_path, output, args) + md_path = output_path.with_suffix('.md') + with open(md_path, 'w', encoding='utf-8') as f: + f.write(md_content) + print(f"Markdown saved to: {md_path}") + + # Create HTML document + html_content = create_html_document(image_path, output, pil_image, args) + html_path = output_path.with_suffix('.html') + with open(html_path, 'w', encoding='utf-8') as f: + f.write(html_content) + print(f"HTML saved to: {html_path}") + + # Calculate final file size + html_size = os.path.getsize(html_path) + print(f"Final HTML file size: {html_size} bytes") + + # Open in browser if requested (only for the first page or single pages) + if args.show and html_size > 100 and page_num <= 1: + import webbrowser + webbrowser.open(f"file:///{str(html_path.resolve())}") + + # Save a copy of the processed image for reference if in debug mode + if args.debug: + img_debug_path = output_path.with_suffix('.debug.png') + pil_image.save(img_debug_path) + print(f"Saved debug image to: {img_debug_path}") + + return output_path + + +def main(): + # Parse arguments + args = parse_arguments() + + # Settings + DEBUG_MODE = args.debug + DOCTAGS_ONLY = args.doctags_only + image_path = args.image + + # Load the model + print("Loading model...") + try: + from mlx_vlm import load, generate + from mlx_vlm.utils import load_config + + model_path = "ds4sd/SmolDocling-256M-preview-mlx-bf16" + model, processor = load(model_path) + config = load_config(model_path) + except Exception as e: + print(f"Error loading model: {e}") + import traceback + traceback.print_exc() + return + + # Check if the input is a PDF + pdf_path = Path(image_path) + is_pdf = False + pdf_page_count = 0 + + if pdf_path.suffix.lower() == '.pdf': + is_pdf = True + # Try to get page count + try: + try: + import fitz # PyMuPDF + pdf_document = fitz.open(pdf_path) + pdf_page_count = len(pdf_document) + print(f"PDF detected with {pdf_page_count} pages") + pdf_document.close() + except ImportError: + print("PyMuPDF not installed. Using pdf2image to estimate page count...") + from pdf2image import pdfinfo_from_path + pdf_info = pdfinfo_from_path(pdf_path) + pdf_page_count = pdf_info["Pages"] + print(f"PDF detected with {pdf_page_count} pages") + except Exception as e: + print(f"Could not determine PDF page count: {e}") + print("Will process the specified page only.") + pdf_page_count = args.page + + # Determine which pages to process + process_all_pages = args.all_pages + start_page = args.start_page + end_page = args.end_page if args.end_page else pdf_page_count + max_pages = args.max_pages + + # Validate page ranges + if start_page < 1: + start_page = 1 + if end_page and end_page > pdf_page_count: + end_page = pdf_page_count + + # Ask user if they want to process all pages or just one (if not specified by arguments) + if is_pdf and pdf_page_count > 1 and not process_all_pages and args.start_page == 1 and not args.end_page: + if args.page > 1: + print(f"You specified to process page {args.page}.") + process_all_pages = False + start_page = args.page + end_page = args.page + else: + user_input = input("Do you want to process all pages? [y/N]: ") + process_all_pages = user_input.lower() in ['y', 'yes'] + if not process_all_pages: + # Ask for specific page or range + page_input = input(f"Enter page number(s) to process (e.g., 3 or 1-5) [1]: ") + if page_input.strip(): + if '-' in page_input: + try: + start_str, end_str = page_input.split('-') + start_page = int(start_str.strip()) + end_page = int(end_str.strip()) + except ValueError: + print("Invalid range format. Using default page 1.") + start_page = end_page = 1 + else: + try: + start_page = end_page = int(page_input.strip()) + except ValueError: + print("Invalid page number. Using default page 1.") + start_page = end_page = 1 + + # Apply max_pages limit if specified + if max_pages and end_page - start_page + 1 > max_pages: + end_page = start_page + max_pages - 1 + + # Process pages + if is_pdf and (process_all_pages or start_page != end_page): + page_range = range(start_page, end_page + 1) + print(f"Processing pages {start_page} to {end_page} ({len(page_range)} pages)...") + processed_pages = [] + + for page_num in page_range: + print(f"\n{'='*50}\nProcessing PDF page {page_num}/{end_page}\n{'='*50}\n") + + # Update the page argument + args.page = page_num + + # Load the specific page + try: + pil_image = load_image(image_path, page_num=page_num, dpi=args.dpi) + print(f"Page {page_num} loaded: {pil_image.size}") + + # Process the page + output_path = process_page(args, model, processor, config, image_path, pil_image, page_num) + processed_pages.append(output_path) + except Exception as e: + print(f"Error processing page {page_num}: {e}") + import traceback + traceback.print_exc() + continue + + print(f"\nProcessed {len(processed_pages)} pages from PDF.") + print(f"Output files: {', '.join([str(p) for p in processed_pages])}") + else: + # Process just one page (either it's not a PDF or user only wants one page) + try: + # Load image resource + print(f"Loading {'PDF page' if is_pdf else 'image'} from: {image_path}") + pil_image = load_image(image_path, page_num=args.page, dpi=args.dpi) + print(f"Image loaded: {pil_image.size}") + + # Process the single page + process_page(args, model, processor, config, image_path, pil_image) + except Exception as e: + print(f"Error processing {image_path}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/document.pdf b/document.pdf new file mode 100644 index 0000000..aab3ca1 Binary files /dev/null and b/document.pdf differ diff --git a/visualizer.py b/visualizer.py new file mode 100644 index 0000000..57526ce --- /dev/null +++ b/visualizer.py @@ -0,0 +1,774 @@ +#!/usr/bin/env python3 +""" +DocTags Zone Visualizer - Simple script to visualize zones identified in DocTags format. + +Usage: + python doctags_visualizer.py --doctags output.doctags.txt --pdf document.pdf --page 8 --output visualization.html +""" + +import argparse +import os +import re +import sys # Add missing import +import base64 +from io import BytesIO +import tempfile +from pathlib import Path +import xml.etree.ElementTree as ET +from PIL import Image, ImageDraw +import pdf2image + +# Regular expression to extract location data +LOC_PATTERN = r'' + +def parse_arguments(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description='Visualize zones identified in DocTags format') + parser.add_argument('--doctags', '-d', type=str, required=True, + help='Path to DocTags file') + parser.add_argument('--pdf', '-p', type=str, required=True, + help='Path to original PDF file') + parser.add_argument('--page', type=int, default=8, + help='Page number in PDF (starts at 1, default: 8)') + parser.add_argument('--output', '-o', type=str, default="visualization.html", + help='Output HTML file path') + parser.add_argument('--dpi', type=int, default=200, + help='DPI for PDF rendering') + parser.add_argument('--show', '-s', action='store_true', + help='Open HTML in default browser') + parser.add_argument('--page-count', action='store_true', + help='Just count pages in the PDF and exit') + parser.add_argument('--scale', type=float, default=1.0, + help='Scaling factor for zone coordinates (default: 1.0)') + parser.add_argument('--scale-x', type=float, default=None, + help='X-axis scaling factor (overrides --scale)') + parser.add_argument('--scale-y', type=float, default=None, + help='Y-axis scaling factor (overrides --scale)') + parser.add_argument('--adjust', action='store_true', + help='Try to automatically adjust scaling') + return parser.parse_args() + +def count_pdf_pages(pdf_path): + """Count the number of pages in a PDF file.""" + if not os.path.exists(pdf_path): + print(f"Error: PDF file not found: {pdf_path}") + return 0 + + try: + from pdf2image.pdf2image import pdfinfo_from_path + info = pdfinfo_from_path(pdf_path, userpw=None, poppler_path=None) + return info["Pages"] + except Exception as e: + print(f"Warning: pdfinfo failed: {e}") + # Fallback method if pdfinfo fails + try: + images = pdf2image.convert_from_path(pdf_path, dpi=72, first_page=1, last_page=1) + # Try to load the last page - increment until we get an error + page_count = 1 + while True: + try: + images = pdf2image.convert_from_path(pdf_path, dpi=72, first_page=page_count+1, last_page=page_count+1) + if not images: + break + page_count += 1 + except: + break + return page_count + except Exception as e2: + print(f"Error counting PDF pages: {e2}") + return 0 + +def load_image_from_pdf(pdf_path, page_num=1, dpi=200): + """Load a specific page from PDF as an image.""" + if not os.path.exists(pdf_path): + raise FileNotFoundError(f"PDF file not found: {pdf_path}") + + print(f"Converting PDF page {page_num} to image (DPI: {dpi})...") + try: + pdf_images = pdf2image.convert_from_path( + pdf_path, + dpi=dpi, + first_page=page_num, + last_page=page_num + ) + if not pdf_images: + raise Exception(f"Could not extract page {page_num} from PDF") + return pdf_images[0] # Return the requested page + except Exception as e: + raise Exception(f"Error converting PDF to image: {e}") + +def parse_doctags(doctags_path): + """Parse DocTags file and extract zones with their coordinates.""" + if not os.path.exists(doctags_path): + raise FileNotFoundError(f"DocTags file not found: {doctags_path}") + + with open(doctags_path, 'r', encoding='utf-8') as f: + doctags_content = f.read() + + # Extract content between tags + doctag_pattern = r'(.*?)' + doctag_match = re.search(doctag_pattern, doctags_content, re.DOTALL) + + if not doctag_match: + raise ValueError("No tags found in the file") + + doctag_content = doctag_match.group(1) + + # Find all tags with location information + zones = [] + + # Find all tag starts + tag_starts = re.finditer(r'<(\w+)>', doctag_content) + + for tag_match in tag_starts: + tag_name = tag_match.group(1) + # Skip location tags themselves + if tag_name.startswith('loc_'): + continue + + # Find the end of the tag + tag_start_pos = tag_match.start() + tag_end_pattern = f'' + tag_end_match = re.search(tag_end_pattern, doctag_content[tag_start_pos:]) + + if not tag_end_match: + continue # Skip if no closing tag + + # Extract the tag content + tag_content = doctag_content[tag_start_pos:tag_start_pos + tag_end_match.end()] + + # Look for location pattern + loc_match = re.search(LOC_PATTERN, tag_content) + + if loc_match: + # Extract coordinates + x1, y1, x2, y2 = map(int, loc_match.groups()) + + # Extract text content if available + text_content = "" + # Look for content between the location info and the closing tag + content_pattern = f'{LOC_PATTERN}(.*?)' + content_match = re.search(content_pattern, tag_content, re.DOTALL) + + if content_match: + text_content = content_match.group(5).strip() + + zones.append({ + 'type': tag_name, + 'x1': x1, + 'y1': y1, + 'x2': x2, + 'y2': y2, + 'content': text_content + }) + + return zones + +def create_visualization(image, zones, pdf_path, page_num, total_pages, output_path): + """Create HTML visualization with the image and overlay zones.""" + # Convert image to base64 for embedding + buffered = BytesIO() + image.save(buffered, format="PNG") + img_base64 = base64.b64encode(buffered.getvalue()).decode() + + # Image dimensions + img_width, img_height = image.size + + # Define colors for different zone types + zone_colors = { + 'section_header_level_1': '#FF5722', # Orange + 'text': '#2196F3', # Blue + 'picture': '#4CAF50', # Green + 'table': '#9C27B0', # Purple + 'page_header': '#FFC107', # Amber + 'page_footer': '#795548', # Brown + 'default': '#607D8B' # Blue Grey + } + + # Create HTML with CSS for zones + html = f""" + + + + DocTags Zone Visualization - Page {page_num} + + + +
+

DocTags Zone Visualization - {Path(pdf_path).stem}

+
+
+ + + + +
""" + + # Add legend items for each zone type + zone_types = set(zone['type'] for zone in zones) + for zone_type in zone_types: + color = zone_colors.get(zone_type, zone_colors['default']) + html += f""" +
+
+ {zone_type} +
""" + + html += """ +
+
+ + +
+
+ +
+ +""" + + # Add zone overlays + for i, zone in enumerate(zones): + zone_type = zone['type'] + color = zone_colors.get(zone_type, zone_colors['default']) + + width = zone['x2'] - zone['x1'] + height = zone['y2'] - zone['y1'] + + html += f""" +
+
+ {zone_type} +
+
""" + + html += """ +
+ +
+

Detected Zones:

+""" + + # Add zone details + for zone in zones: + zone_type = zone['type'] + color = zone_colors.get(zone_type, zone_colors['default']) + + html += f""" +
+
{zone_type}
+
Position: ({zone['x1']}, {zone['y1']}) - ({zone['x2']}, {zone['y2']})
+""" + if zone['content']: + html += f""" +
{zone['content']}
+""" + + html += """ +
+""" + + html += """ +
+ + + + +""" + + # Save HTML file + with open(output_path, 'w', encoding='utf-8') as f: + f.write(html) + + print(f"Visualization saved to: {output_path}") + + return output_path + +def create_debug_image(image, zones, page_num, output_path): + """Create a debug image with rectangles around zones.""" + # Create a copy of the input image + debug_img = image.copy() + draw = ImageDraw.Draw(debug_img) + + # Define colors for different zone types + zone_colors = { + 'section_header_level_1': (255, 87, 34), # Orange + 'text': (33, 150, 243), # Blue + 'picture': (76, 175, 80), # Green + 'table': (156, 39, 176), # Purple + 'page_header': (255, 193, 7), # Amber + 'page_footer': (121, 85, 72), # Brown + 'default': (96, 125, 139) # Blue Grey + } + + # Draw rectangles for each zone + for zone in zones: + zone_type = zone['type'] + color = zone_colors.get(zone_type, zone_colors['default']) + + draw.rectangle( + [(zone['x1'], zone['y1']), (zone['x2'], zone['y2'])], + outline=color, + width=2 + ) + + # Add zone type label + label_width = len(zone_type) * 7 + 6 + label_x = min(zone['x1'], image.width - label_width) # Keep label on image + + draw.rectangle( + [(label_x, zone['y1']), (label_x + label_width, zone['y1'] + 20)], + fill=(255, 255, 255, 180), + outline=color + ) + draw.text( + (label_x + 3, zone['y1'] + 3), + zone_type, + fill=color + ) + + # Draw page number on the debug image + draw.rectangle( + [(10, 10), (100, 40)], + fill=(0, 0, 0, 180), + outline=(255, 255, 255) + ) + draw.text( + (15, 15), + f"Page {page_num}", + fill=(255, 255, 255) + ) + + # Save the debug image + debug_img.save(output_path) + print(f"Debug image saved to: {output_path}") + + return debug_img + +def process_page(pdf_path, page_num, doctags_path, output_base, dpi=200, show=False, scale=1.0, scale_x=None, scale_y=None, adjust=False): + """Process a single page of the PDF with visualization.""" + # Generate output paths for this page + output_name = f"{Path(output_base).stem}_page_{page_num}{Path(output_base).suffix}" + output_path = Path(output_base).parent / output_name + debug_output = output_path.with_suffix('.debug.png') + + # Load the page image + try: + image = load_image_from_pdf(pdf_path, page_num, dpi) + print(f"Page {page_num} loaded: {image.size}") + except Exception as e: + print(f"Error loading page {page_num}: {e}") + return False + + # Parse DocTags + try: + zones = parse_doctags(doctags_path) + print(f"Found {len(zones)} zones in DocTags") + + # Debug output to understand scaling issues + if zones: + max_x = max([zone['x2'] for zone in zones]) + max_y = max([zone['y2'] for zone in zones]) + min_x = min([zone['x1'] for zone in zones]) + min_y = min([zone['y1'] for zone in zones]) + img_width, img_height = image.size + + print(f"Image dimensions: {img_width}x{img_height}") + print(f"Zone boundaries: X({min_x}-{max_x}), Y({min_y}-{max_y})") + print(f"Suggested scale factors: X={img_width/max_x:.3f}, Y={img_height/max_y:.3f}") + + # Apply scaling to coordinates if required + if scale != 1.0 or scale_x is not None or scale_y is not None: + x_scale = scale_x if scale_x is not None else scale + y_scale = scale_y if scale_y is not None else scale + + for zone in zones: + zone['x1'] = int(zone['x1'] * x_scale) + zone['y1'] = int(zone['y1'] * y_scale) + zone['x2'] = int(zone['x2'] * x_scale) + zone['y2'] = int(zone['y2'] * y_scale) + + print(f"Applied scaling: X={x_scale}, Y={y_scale}") + + # Auto-adjust scaling if requested + if adjust: + # Find the maximum coordinates in the zones + max_x = max([zone['x2'] for zone in zones]) if zones else 0 + max_y = max([zone['y2'] for zone in zones]) if zones else 0 + + # If max coordinates exceed image dimensions or are too small + if max_x > 0 and max_y > 0: + width, height = image.size + + # Calculate appropriate scaling factors + if max_x > width * 1.1 or max_x < width * 0.5: + x_scale = width / max_x + print(f"Auto-adjusted X scale to {x_scale:.3f} (image width: {width}, max zone x: {max_x})") + else: + x_scale = 1.0 + + if max_y > height * 1.1 or max_y < height * 0.5: + y_scale = height / max_y + print(f"Auto-adjusted Y scale to {y_scale:.3f} (image height: {height}, max zone y: {max_y})") + else: + y_scale = 1.0 + + # Apply the scaling to all zones + if x_scale != 1.0 or y_scale != 1.0: + for zone in zones: + zone['x1'] = int(zone['x1'] * x_scale) + zone['y1'] = int(zone['y1'] * y_scale) + zone['x2'] = int(zone['x2'] * x_scale) + zone['y2'] = int(zone['y2'] * y_scale) + print(f"Applied auto-scaling: X={x_scale}, Y={y_scale}") + + except Exception as e: + print(f"Error parsing DocTags: {e}") + return False + + # Get total page count + total_pages = count_pdf_pages(pdf_path) + + # Create debug image with zones + create_debug_image(image, zones, page_num, debug_output) + + # Create HTML visualization + output_html = create_visualization(image, zones, pdf_path, page_num, total_pages, output_path) + + # Open in browser if requested + if show: + import webbrowser + webbrowser.open(f"file:///{os.path.abspath(output_html)}") + + return True + +def process_all_pages(pdf_path, doctags_path, output_base, dpi=200, show_last=False, scale=1.0, scale_x=None, scale_y=None, adjust=False): + """Process all pages of the PDF and create visualizations.""" + # Get total page count + total_pages = count_pdf_pages(pdf_path) + if total_pages == 0: + print("Error: Could not determine the number of pages in the PDF.") + return False + + print(f"Processing all {total_pages} pages of the PDF...") + + last_html = None + + # Process each page + for page_num in range(1, total_pages + 1): + print(f"\nProcessing page {page_num} of {total_pages}...") + + # Generate output paths for this page + output_name = f"{Path(output_base).stem}_page_{page_num}{Path(output_base).suffix}" + output_path = Path(output_base).parent / output_name + + # Process the page + success = process_page(pdf_path, page_num, doctags_path, output_path, dpi, False, scale, scale_x, scale_y, adjust) + + if success: + last_html = output_path + + # Open the last successful page in browser if requested + if show_last and last_html: + import webbrowser + webbrowser.open(f"file:///{os.path.abspath(last_html)}") + + return True + +def main(): + # Parse arguments + args = parse_arguments() + + # If just counting pages + if args.page_count: + page_count = count_pdf_pages(args.pdf) + print(f"The PDF has {page_count} pages.") + return + + # Check if files exist + if not os.path.exists(args.pdf): + print(f"Error: PDF file not found: {args.pdf}") + return + + if not os.path.exists(args.doctags): + print(f"Error: DocTags file not found: {args.doctags}") + return + + # Process page(s) + if args.page == 0: # Special case: process all pages + process_all_pages( + args.pdf, + args.doctags, + args.output, + args.dpi, + args.show, + args.scale, + args.scale_x, + args.scale_y, + args.adjust + ) + else: + process_page( + args.pdf, + args.page, + args.doctags, + args.output, + args.dpi, + args.show, + args.scale, + args.scale_x, + args.scale_y, + args.adjust + ) + +if __name__ == "__main__": + main() + +# -------------------------------------------------------- +# Helper script to generate DocTags from a PDF +# -------------------------------------------------------- + +def generate_doctags_from_pdf(input_pdf, page_num, output_doctags, prompt="Convert this page to docling."): + """ + Helper function to generate DocTags from a PDF page using the analyzer.py script. + This is a wrapper for the original script functionality. + + Args: + input_pdf: Path to the PDF file + page_num: Page number to process (starting from 1) + output_doctags: Path to save the generated DocTags + prompt: Prompt for the model (default: "Convert this page to docling.") + + Returns: + bool: True if successful, False otherwise + """ + import subprocess + import sys + + try: + print(f"Generating DocTags for page {page_num} of {input_pdf}...") + + # Construct command to run analyzer.py + cmd = [ + sys.executable, # Use the current Python interpreter + "analyzer.py", # Path to the script + "--image", str(input_pdf), + "--page", str(page_num), + "--prompt", prompt + ] + + # Run the command + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False + ) + + # Check if the command was successful + if result.returncode != 0: + print(f"Error running analyzer.py: {result.stderr}") + return False + + # Extract DocTags from output + doctags_content = None + output_lines = result.stdout.split('\n') + in_doctags = False + doctags_lines = [] + + for line in output_lines: + if '' in line: + in_doctags = True + + if in_doctags: + doctags_lines.append(line) + + if '' in line: + in_doctags = False + + if not doctags_lines: + print("No DocTags found in the output") + return False + + # Save DocTags to file + with open(output_doctags, 'w', encoding='utf-8') as f: + f.write('\n'.join(doctags_lines)) + + print(f"DocTags saved to: {output_doctags}") + return True + + except Exception as e: + print(f"Error generating DocTags: {e}") + return False + +# Command-line interface for the DocTags generator +def generate_doctags_cli(): + """Command-line interface for generating DocTags.""" + parser = argparse.ArgumentParser(description='Generate DocTags from a PDF page') + parser.add_argument('--pdf', type=str, required=True, + help='Path to PDF file') + parser.add_argument('--page', type=int, default=1, + help='Page number (starts at 1)') + parser.add_argument('--output', type=str, default='output.doctags.txt', + help='Output DocTags file') + parser.add_argument('--prompt', type=str, default='Convert this page to docling.', + help='Prompt for the model') + + args = parser.parse_args() + + # Generate DocTags + success = generate_doctags_from_pdf( + args.pdf, + args.page, + args.output, + args.prompt + ) + + return 0 if success else 1 + +# Run the generator if called directly with --generate flag +if __name__ == "__main__" and '--generate' in sys.argv: + sys.argv.remove('--generate') + sys.exit(generate_doctags_cli()) \ No newline at end of file