DanielRegaladoCardoso commited on
Commit
89360e0
·
verified ·
1 Parent(s): 9b0542e

SVG: standalone download (XML prolog, white bg, fixed dims) + Plotly first

Browse files
Files changed (1) hide show
  1. src/visualization/svg_theme.py +59 -4
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
- Inject a <style> block scoped to the SVG that sets default colors using
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