"""Vector-PDF composition for a main roster plus a nursery-only strip.""" from __future__ import annotations from dataclasses import dataclass from os import PathLike from pathlib import Path import fitz # type: ignore @dataclass(frozen=True) class SectionPlacement: x: float y: float scale: float @dataclass(frozen=True) class PlacedSection: source: fitz.Rect destination: fitz.Rect @dataclass(frozen=True) class CombinedComposition: main: PlacedSection nursery: PlacedSection def useful_content_bounds(page: fitz.Page, padding: float = 1.5) -> fitz.Rect: """Find useful table bounds from vector drawings and extracted content. Near-full-page rectangles are ignored so a PDF background cannot defeat the crop. Subsplash table cells and text remain part of the union. """ page_rect = page.rect maximum_background_area = page_rect.get_area() * 0.95 text_rectangles: list[fitz.Rect] = [] for block in page.get_text("blocks"): if len(block) > 6 and block[6] != 0: continue rect = fitz.Rect(block[:4]) & page_rect if not rect.is_empty and rect.get_area() < maximum_background_area: text_rectangles.append(rect) text_bounds: fitz.Rect | None = None if text_rectangles: text_bounds = fitz.Rect(text_rectangles[0]) for rectangle in text_rectangles[1:]: text_bounds.include_rect(rectangle) # A clipped PDF form can expose drawing metadata for content outside its # visible clip. Anchor drawings to the actual extracted-text rows so hidden # table lines cannot expand a nursery strip back to a full page. visible_rows = None if text_bounds is not None: visible_rows = fitz.Rect( page_rect.x0, max(page_rect.y0, text_bounds.y0 - 8), page_rect.x1, min(page_rect.y1, text_bounds.y1 + 8), ) drawing_rectangles: list[fitz.Rect] = [] for drawing in page.get_drawings(): rect = fitz.Rect(drawing["rect"]) if rect.width == 0 and rect.height > 0: rect.x0 -= 0.35 rect.x1 += 0.35 if rect.height == 0 and rect.width > 0: rect.y0 -= 0.35 rect.y1 += 0.35 rect &= page_rect if rect.is_empty or rect.get_area() >= maximum_background_area: continue if visible_rows is not None and (rect & visible_rows).is_empty: continue drawing_rectangles.append(rect) candidates = text_rectangles + drawing_rectangles if not candidates: raise ValueError("No table content could be found in the roster PDF.") bounds = fitz.Rect(candidates[0]) for candidate in candidates[1:]: bounds.include_rect(candidate) if text_bounds is not None: bounds.y1 = min(bounds.y1, text_bounds.y1 + 8) return fitz.Rect( max(page_rect.x0, bounds.x0 - padding), max(page_rect.y0, bounds.y0 - padding), min(page_rect.x1, bounds.x1 + padding), min(page_rect.y1, bounds.y1 + padding), ) def _place_section( destination_page: fitz.Page, source_document: fitz.Document, placement: SectionPlacement, ) -> PlacedSection: source = useful_content_bounds(source_document[0]) destination = fitz.Rect( placement.x, placement.y, placement.x + source.width * placement.scale, placement.y + source.height * placement.scale, ) destination_page.show_pdf_page( destination, source_document, 0, clip=source, keep_proportion=False, overlay=True, ) return PlacedSection(source=source, destination=destination) def compose_combined_pdf( main_pdf: str | PathLike[str], nursery_pdf: str | PathLike[str], output_pdf: str | PathLike[str], *, page_width: float, page_height: float, main_placement: SectionPlacement, nursery_placement: SectionPlacement, logo_path: str | PathLike[str], logo_position: tuple[float, float], logo_scale: float, month_label: str, font_path: str | PathLike[str], date_position: tuple[float, float], date_font_size: float, ) -> CombinedComposition: """Compose both source tables and the shared header into one vector PDF.""" main_document = fitz.open(main_pdf) nursery_document = fitz.open(nursery_pdf) output_document = fitz.open() try: page = output_document.new_page(width=page_width, height=page_height) page.draw_rect(page.rect, color=None, fill=(1, 1, 1), overlay=False) main_section = _place_section(page, main_document, main_placement) nursery_section = _place_section(page, nursery_document, nursery_placement) logo_pixmap = fitz.Pixmap(str(Path(logo_path).resolve())) logo_width = logo_pixmap.width * logo_scale logo_height = logo_pixmap.height * logo_scale logo_pixmap = None logo_x, logo_y = logo_position page.insert_image( fitz.Rect(logo_x, logo_y, logo_x + logo_width, logo_y + logo_height), filename=str(Path(logo_path).resolve()), overlay=True, ) resolved_font = str(Path(font_path).resolve()) page.insert_font(fontname="custom", fontfile=resolved_font) date_x, date_y = date_position page.insert_text( (date_x, date_y), month_label, fontname="custom", fontsize=date_font_size, color=(0, 0, 0), overlay=True, ) output_document.save(output_pdf) return CombinedComposition(main=main_section, nursery=nursery_section) finally: output_document.close() nursery_document.close() main_document.close()