"""Astrazioni del sottosistema di rendering PDF (SOLID). Ogni metodo di visualizzazione/redazione e' una *strategia* isolata che implementa `RenderMethod`. Sono registrate in un `RenderRegistry` e abilitate o escluse da configurazione: aggiungere/togliere un metodo non tocca il codice esistente (Open/Closed) ne' il livello web (Dependency Inversion). - Single Responsibility: ogni metodo vive in un proprio file e sa solo produrre il proprio output. - Liskov: tutti i metodi rispettano la stessa firma `render(params) -> output`. - Interface Segregation: l'interfaccia espone solo `info` e `render`. """ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import Enum from core.contracts import PageGeometry class RenderKind(str, Enum): """Natura dell'output di un metodo.""" VIEW = "view" # immagine renderizzata inline (SVG/PNG) CLIENT = "client" # rendering lato client (solo dati: raw + overlay) DOWNLOAD = "download" # file scaricabile (PDF) @dataclass(frozen=True, slots=True) class MethodInfo: """Metadati di un metodo, usati anche dalla UI per costruire i controlli.""" key: str label: str kind: RenderKind media_type: str styles: tuple[str, ...] default_style: str description: str destructive: bool = False # True solo per la hard redaction @dataclass(frozen=True, slots=True) class RenderParams: """Parametri di una richiesta di rendering. Ogni metodo usa cio' che serve.""" pdf_bytes: bytes boxes: list[dict] = field(default_factory=list) pages: tuple[PageGeometry, ...] = field(default_factory=tuple) page: int = 0 style: str = "fill" scale: float = 1.5 dpi: int = 144 opacity: float = 0.85 @dataclass(frozen=True, slots=True) class RenderOutput: """Risultato neutro di un rendering.""" data: bytes media_type: str headers: dict[str, str] = field(default_factory=dict) filename: str | None = None def boxes_on_page(params: RenderParams) -> list[dict]: """Box della richiesta che appartengono alla pagina corrente (`params.page`). Filtro condiviso dai metodi che renderizzano una singola pagina (SVG/PNG), dove prima era ripetuto inline (`if b.get("page") != params.page`). """ return [b for b in params.boxes if b.get("page") == params.page] class RenderMethod(ABC): """Interfaccia comune a tutti i metodi di rendering PDF.""" #: Metadati statici del metodo (override nelle sottoclassi o via __init__). info: MethodInfo def supports_style(self, style: str) -> bool: return style in self.info.styles @abstractmethod def render(self, params: RenderParams) -> RenderOutput: """Produce l'output del metodo a partire dai parametri.""" class RenderRegistry: """Collezione ordinata di metodi abilitati (Open/Closed).""" def __init__(self) -> None: self._methods: dict[str, RenderMethod] = {} def register(self, method: RenderMethod) -> RenderRegistry: if method.info.key in self._methods: raise ValueError(f"Metodo gia' registrato: {method.info.key}") self._methods[method.info.key] = method return self def has(self, key: str) -> bool: return key in self._methods def get(self, key: str) -> RenderMethod | None: return self._methods.get(key) def infos(self) -> list[MethodInfo]: return [m.info for m in self._methods.values()] def __len__(self) -> int: return len(self._methods)