"""Pydantic schema for template annotation files (``annotations/.json``).""" from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field, model_validator class SignatureBoxAnnotation(BaseModel): """A single signature field on a page, in pixel coordinates of the template image.""" model_config = ConfigDict(extra="forbid") id: str x1: float y1: float x2: float y2: float role: str = "Signer" name: str = "Signer" @model_validator(mode="after") def _normalize_box(self) -> "SignatureBoxAnnotation": x1, x2 = sorted((self.x1, self.x2)) y1, y2 = sorted((self.y1, self.y2)) self.x1, self.x2, self.y1, self.y2 = x1, x2, y1, y2 if x2 <= x1 or y2 <= y1: raise ValueError(f"Signature box '{self.id}' has non-positive area: {self.as_tuple()}") return self def as_tuple(self) -> tuple[float, float, float, float]: """Return the box as ``(x1, y1, x2, y2)``.""" return (self.x1, self.y1, self.x2, self.y2) @property def width(self) -> float: return self.x2 - self.x1 @property def height(self) -> float: return self.y2 - self.y1 class TemplatePageAnnotation(BaseModel): """A single page of a template document.""" model_config = ConfigDict(extra="forbid") page: int = 1 page_size: tuple[int, int] dpi: int = 300 signature_boxes: list[SignatureBoxAnnotation] = Field(default_factory=list) class TemplateAnnotation(BaseModel): """Full annotation for one template document (possibly multi-page).""" model_config = ConfigDict(extra="forbid") id: str source_image: "str | None" = None document_type: "str | None" = None pages: list[TemplatePageAnnotation] auto_detected: bool = False def page_by_number(self, page: int) -> TemplatePageAnnotation: for p in self.pages: if p.page == page: return p raise KeyError(f"Template '{self.id}' has no page {page}")