Spaces:
Sleeping
Sleeping
SVG: standalone download (XML prolog, white bg, fixed dims) + Plotly first
Browse files
src/visualization/svg_theme.py
CHANGED
|
@@ -132,10 +132,8 @@ def _normalize_strokes(svg: str) -> str:
|
|
| 132 |
|
| 133 |
|
| 134 |
def _wrap_with_theme(svg: str) -> str:
|
| 135 |
-
"""
|
| 136 |
-
|
| 137 |
-
CSS variables that the host document can override (light/dark themes).
|
| 138 |
-
"""
|
| 139 |
style = f"""<style>
|
| 140 |
.chart-bg {{ fill: transparent; }}
|
| 141 |
.chart-ink, text {{ fill: {INK}; font-family: {FONT_FAMILY}; }}
|
|
@@ -146,6 +144,63 @@ def _wrap_with_theme(svg: str) -> str:
|
|
| 146 |
return re.sub(r"(<svg[^>]*>)", r"\1" + style, svg, count=1, flags=re.IGNORECASE)
|
| 147 |
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
def _attr(svg: str, name: str) -> str | None:
|
| 150 |
m = re.search(rf'\s{name}\s*=\s*"([^"]+)"', svg, re.IGNORECASE)
|
| 151 |
return m.group(1) if m else None
|
|
|
|
| 132 |
|
| 133 |
|
| 134 |
def _wrap_with_theme(svg: str) -> str:
|
| 135 |
+
"""Inject a <style> block scoped to the SVG with explicit hex colors.
|
| 136 |
+
These work both in-app and as a standalone downloaded file."""
|
|
|
|
|
|
|
| 137 |
style = f"""<style>
|
| 138 |
.chart-bg {{ fill: transparent; }}
|
| 139 |
.chart-ink, text {{ fill: {INK}; font-family: {FONT_FAMILY}; }}
|
|
|
|
| 144 |
return re.sub(r"(<svg[^>]*>)", r"\1" + style, svg, count=1, flags=re.IGNORECASE)
|
| 145 |
|
| 146 |
|
| 147 |
+
def to_standalone_svg(svg: str) -> str:
|
| 148 |
+
"""Make the SVG self-contained for download:
|
| 149 |
+
- Add XML namespace declarations
|
| 150 |
+
- Ensure xmlns:xlink for href attributes
|
| 151 |
+
- Add explicit white background for visibility outside the app
|
| 152 |
+
- Add explicit width/height for file viewers that ignore viewBox
|
| 153 |
+
"""
|
| 154 |
+
if not svg or "<svg" not in svg:
|
| 155 |
+
return svg
|
| 156 |
+
|
| 157 |
+
out = svg
|
| 158 |
+
|
| 159 |
+
# Add XML namespace if missing
|
| 160 |
+
if "xmlns=" not in out:
|
| 161 |
+
out = re.sub(
|
| 162 |
+
r"<svg",
|
| 163 |
+
'<svg xmlns="http://www.w3.org/2000/svg"',
|
| 164 |
+
out, count=1, flags=re.IGNORECASE,
|
| 165 |
+
)
|
| 166 |
+
if "xmlns:xlink=" not in out:
|
| 167 |
+
out = re.sub(
|
| 168 |
+
r"<svg",
|
| 169 |
+
'<svg xmlns:xlink="http://www.w3.org/1999/xlink"',
|
| 170 |
+
out, count=1, flags=re.IGNORECASE,
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
# Pull viewBox dims for width/height fallback
|
| 174 |
+
vb = re.search(r'viewBox\s*=\s*"([\d.\s\-]+)"', out)
|
| 175 |
+
if vb:
|
| 176 |
+
parts = vb.group(1).split()
|
| 177 |
+
if len(parts) == 4:
|
| 178 |
+
w, h = parts[2], parts[3]
|
| 179 |
+
# Replace any existing width/height with explicit pixel values
|
| 180 |
+
out = re.sub(r'\s(width|height)="[^"]*"', "", out, flags=re.IGNORECASE)
|
| 181 |
+
out = re.sub(
|
| 182 |
+
r"<svg",
|
| 183 |
+
f'<svg width="{w}" height="{h}"',
|
| 184 |
+
out, count=1, flags=re.IGNORECASE,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# Drop the responsive style so file viewers get fixed dims
|
| 188 |
+
out = re.sub(r'\sstyle="width:100%[^"]*"', "", out, flags=re.IGNORECASE)
|
| 189 |
+
|
| 190 |
+
# Add explicit white background rect at start (for opaque export)
|
| 191 |
+
out = re.sub(
|
| 192 |
+
r"(<svg[^>]*>)",
|
| 193 |
+
r'\1<rect width="100%" height="100%" fill="#FAFAF9"/>',
|
| 194 |
+
out, count=1, flags=re.IGNORECASE,
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
# XML prolog
|
| 198 |
+
if not out.startswith("<?xml"):
|
| 199 |
+
out = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' + out
|
| 200 |
+
|
| 201 |
+
return out
|
| 202 |
+
|
| 203 |
+
|
| 204 |
def _attr(svg: str, name: str) -> str | None:
|
| 205 |
m = re.search(rf'\s{name}\s*=\s*"([^"]+)"', svg, re.IGNORECASE)
|
| 206 |
return m.group(1) if m else None
|