Initial commit: svg backend

This commit is contained in:
adminko
2026-03-19 13:39:32 +03:00
commit 85fb2f4bb9
78 changed files with 6161 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
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