from __future__ import annotations import logging import re from typing import Any from lxml import etree from app.core.config import settings logger = logging.getLogger(__name__) ALLOWED_MODES = {"passthrough", "optimized"} def _parse_length(value: str | None) -> float | None: if not value: return None cleaned = value.strip().replace("px", "") try: return float(cleaned) except ValueError: return None def _local_name(tag: str) -> str: if "}" in tag: return tag.split("}", 1)[1] return tag def _is_hidden(node: etree._Element) -> bool: display = (node.attrib.get("display") or "").strip().lower() visibility = (node.attrib.get("visibility") or "").strip().lower() style = (node.attrib.get("style") or "").replace(" ", "").lower() return ( display == "none" or visibility == "hidden" or "display:none" in style or "visibility:hidden" in style ) def _is_seat_related(node: etree._Element) -> bool: probe = " ".join( [ node.attrib.get("id", ""), node.attrib.get("class", ""), node.attrib.get("data-seat-id", ""), node.attrib.get("data-sector-id", ""), node.attrib.get("data-group-id", ""), ] ).lower() return any(token in probe for token in ["seat", "sector", "group", "place"]) def _font_size(node: etree._Element) -> float | None: direct = _parse_length(node.attrib.get("font-size")) if direct is not None: return direct style = node.attrib.get("style") or "" match = re.search(r"font-size\s*:\s*([0-9.]+)", style, flags=re.IGNORECASE) if match: return _parse_length(match.group(1)) return None def _is_technical_text(node: etree._Element) -> bool: patterns = [ item.strip().lower() for item in settings.svg_display_technical_text_patterns.split(",") if item.strip() ] haystack = " ".join( [ node.attrib.get("id", ""), node.attrib.get("class", ""), "".join(node.itertext()), ] ).lower() return any(pattern in haystack for pattern in patterns) def _force_viewbox(root: etree._Element) -> None: if not settings.svg_display_force_viewbox: return if root.attrib.get("viewBox"): return width = _parse_length(root.attrib.get("width")) height = _parse_length(root.attrib.get("height")) if width and height: w = int(width) if width.is_integer() else width h = int(height) if height.is_integer() else height root.attrib["viewBox"] = f"0 0 {w} {h}" def _extract_meta(root: etree._Element) -> dict[str, Any]: return { "view_box": root.attrib.get("viewBox"), "width": root.attrib.get("width"), "height": root.attrib.get("height"), } def generate_display_svg(content: bytes, mode: str) -> tuple[bytes, dict[str, Any]]: if mode not in ALLOWED_MODES: raise ValueError(f"Unsupported display mode: {mode}") parser = etree.XMLParser( resolve_entities=False, remove_blank_text=False, remove_comments=False, no_network=True, recover=False, huge_tree=True, ) root = etree.fromstring(content, parser=parser) defs_count = len(root.xpath("//*[local-name()='defs']")) use_count = len(root.xpath("//*[local-name()='use']")) style_count = len(root.xpath("//*[local-name()='style']")) clip_count = len(root.xpath("//*[local-name()='clipPath']")) logger.info( "display_svg.generate mode=%s size_bytes=%s has_style=%s defs=%s use=%s clipPath=%s", mode, len(content), bool(style_count), defs_count, use_count, clip_count, ) removed_hidden_count = 0 removed_small_text_count = 0 removed_technical_text_count = 0 if mode == "optimized": for node in list(root.iter()): tag_name = _local_name(node.tag) if settings.svg_display_remove_hidden_elements and not _is_seat_related(node) and _is_hidden(node): parent = node.getparent() if parent is not None: parent.remove(node) removed_hidden_count += 1 continue if tag_name in {"text", "tspan"}: if settings.svg_display_hide_small_text and not _is_seat_related(node): size = _font_size(node) if size is not None and size < settings.svg_display_min_text_font_size: parent = node.getparent() if parent is not None: parent.remove(node) removed_small_text_count += 1 continue if settings.svg_display_hide_technical_text and not _is_seat_related(node) and _is_technical_text(node): parent = node.getparent() if parent is not None: parent.remove(node) removed_technical_text_count += 1 continue _force_viewbox(root) output = etree.tostring( root, encoding="utf-8", xml_declaration=True, pretty_print=False, ) meta = _extract_meta(root) meta.update( { "mode": mode, "removed_hidden_count": removed_hidden_count, "removed_small_text_count": removed_small_text_count, "removed_technical_text_count": removed_technical_text_count, } ) return output, meta