""" pdf_processor.py - Converts an uploaded PDF drawing set into page images. Each page becomes a base64-encoded JPEG that the vision model can read. Higher DPI = better accuracy but slower/heavier requests. Adapted from the IronBid pipeline (AI_Takeoffs); behavior is intentionally identical so the two projects stay comparable. """ import base64 from io import BytesIO from typing import List, Dict from pdf2image import convert_from_path from PIL import Image from backend import config def convert_pdf_to_images(pdf_path: str) -> List[Dict]: """ Convert each page of a PDF into a base64-encoded JPEG. Returns a list of dicts: { "page_number": int, "base64": str, "width": int, "height": int } """ print(f"[PDF] Converting {pdf_path} at {config.PDF_DPI} DPI...") pages = convert_from_path( pdf_path, dpi=config.PDF_DPI, fmt="jpeg", thread_count=4, use_pdftocairo=True, ) results: List[Dict] = [] total = min(len(pages), config.MAX_PAGES) for i, page in enumerate(pages[:total]): # 2400px gives ~100px/inch on a 24x36 sheet, the minimum needed to # read 8pt room annotations and dimension strings. page = _resize_if_needed(page, max_dimension=config.MAX_DIMENSION) buffer = BytesIO() page.save(buffer, format="JPEG", quality=85) b64 = base64.b64encode(buffer.getvalue()).decode("utf-8") results.append({ "page_number": i + 1, "base64": b64, "width": page.width, "height": page.height, }) print(f"[PDF] Page {i+1}/{total} encoded ({page.width}x{page.height})") print(f"[PDF] Done - {len(results)} pages ready for analysis") return results def render_page_jpeg(pdf_path: str, page: int, dpi: int = 150, max_dimension: int = 3000) -> bytes: """ Render a single 1-based page of a PDF to JPEG bytes, for the sheet viewer. Higher DPI than the analysis pass (default 150) so text is legible on screen. Raises IndexError if the page is out of range. """ if page < 1: raise IndexError(f"page {page} out of range") pages = convert_from_path(pdf_path, dpi=dpi, fmt="jpeg", first_page=page, last_page=page, use_pdftocairo=True) if not pages: raise IndexError(f"page {page} out of range") img = _resize_if_needed(pages[0], max_dimension=max_dimension) buffer = BytesIO() img.save(buffer, format="JPEG", quality=85) return buffer.getvalue() def _resize_if_needed(image: Image.Image, max_dimension: int = 2400) -> Image.Image: """Resize image proportionally if either dimension exceeds max_dimension.""" w, h = image.size if w <= max_dimension and h <= max_dimension: return image ratio = min(max_dimension / w, max_dimension / h) new_size = (int(w * ratio), int(h * ratio)) return image.resize(new_size, Image.LANCZOS)