#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Inject legends and apply padding to Flash Indexer SVGs.
Reads each diagram SVG, adjusts the viewBox for padding, and injects
a legend group at the specified coordinates. Fully idempotent: uses
marker comments to strip previous injections before re-applying.
Coordinates were captured with the interactive legend_editor.py tool.
Usage:
python3 inject_legends.py
"""
import re
from pathlib import Path
from typing import Any
HERE = Path(__file__).parent
OUT = HERE.parent / "images"
ROW_H = 22
LABEL_W = 80
LINE_W = 40
ARROW_W = 6
PAD = 8
# Marker comments for idempotent injection
BG_START = ""
BG_END = ""
LEG_START = ""
LEG_END = ""
ORIG_VB_ATTR = "data-orig-vb"
CONFIG: dict[str, dict[str, Any]] = {
"event-flow": {
"file": "event-flow.svg",
"out": "fig-2-kv-event-flow.svg",
"legend": {"x": 823, "y": 50},
"padding": {"top": -50, "right": -75, "bottom": -50, "left": -76},
"entries": [
("Data Flow", "#c4a035", "solid"),
("Control Flow", "#5a90c0", "dashed"),
],
},
"radix-tree": {
"file": "radix-tree.svg",
"out": "fig-3-prefix-tree.svg",
"legend": {"x": 315, "y": 100},
"padding": {"top": -65, "right": 10, "bottom": -65, "left": -10},
"entries": [("Traversal", "#e0e0e0", "dotted")],
},
"write-read-path": {
"file": "write-read-path.svg",
"out": "fig-4-concurrency-model.svg",
"legend": {"x": 831, "y": 16},
"padding": {"top": -7, "right": -25, "bottom": -21, "left": -25},
"entries": [
("Data Flow", "#c4a035", "solid"),
("Control Flow", "#5a90c0", "dashed"),
("Contention", "#d06060", "solid"),
],
},
"jump-search": {
"file": "jump-search.svg",
"out": "fig-5-jump-search.svg",
"legend": {"x": 588, "y": -42},
"padding": {"top": 22, "right": 22, "bottom": 22, "left": 22},
"entries": [("Traversal", "#e0e0e0", "dotted")],
},
}
def build_legend_group(entries: list[tuple[str, str, str]], x: int, y: int) -> str:
"""Build SVG elements for a legend at the given position."""
parts = [
f'',
]
for i, (label, color, style) in enumerate(entries):
ey = PAD + i * ROW_H + 14
parts.append(
f'{label}'
)
lx = PAD + LABEL_W
dash = ""
if style == "dashed":
dash = ' stroke-dasharray="6,4"'
elif style == "dotted":
dash = ' stroke-dasharray="3,4"'
parts.append(
f''
)
ax = lx + LINE_W + 4
ay = ey - 4
parts.append(
f''
)
parts.append("")
return "".join(parts)
def strip_injections(svg: str) -> str:
"""Remove all previously injected content and restore original viewBox."""
svg = re.sub(
re.escape(BG_START) + r".*?" + re.escape(BG_END),
"",
svg,
flags=re.DOTALL,
)
svg = re.sub(
re.escape(LEG_START) + r".*?" + re.escape(LEG_END),
"",
svg,
flags=re.DOTALL,
)
m = re.search(rf'{ORIG_VB_ATTR}="([^"]*)"', svg)
if m:
orig_vb = m.group(1)
svg = re.sub(rf'\s*{ORIG_VB_ATTR}="[^"]*"', "", svg)
svg = re.sub(r'viewBox="[^"]*"', f'viewBox="{orig_vb}"', svg, count=1)
return svg
def inject(svg: str, cfg: dict) -> str:
"""Inject legend and padding into an SVG string."""
svg = strip_injections(svg)
vb_m = re.search(r'viewBox="([^"]*)"', svg)
if not vb_m:
raise ValueError("No viewBox found in SVG")
orig_vb = vb_m.group(1)
vbx, vby, vbw, vbh = (float(v) for v in orig_vb.split())
p = cfg["padding"]
nx = vbx - p["left"]
ny = vby - p["top"]
nw = vbw + p["left"] + p["right"]
nh = vbh + p["top"] + p["bottom"]
new_vb = f"{nx:g} {ny:g} {nw:g} {nh:g}"
svg = re.sub(
r'viewBox="[^"]*"',
f'viewBox="{new_vb}" {ORIG_VB_ATTR}="{orig_vb}"',
svg,
count=1,
)
bg_rect = (
f'{BG_START}{BG_END}'
)
legend = build_legend_group(cfg["entries"], cfg["legend"]["x"], cfg["legend"]["y"])
legend_block = f"{LEG_START}{legend}{LEG_END}"
nested = "" in svg
if nested:
inner_pos = svg.index(")
outer_close = svg.rfind("") + len("")
svg = svg[:outer_close] + legend_block + svg[outer_close:]
else:
open_end = svg.index(">", svg.index("