Method: rich.console.Console.export_svg
Calls: 3, Exceptions: 0, Paths: 2Back
Path 1: 2 calls (0.67)
'Rich' (2)
None (2)
True (2)
'
1def export_svg(
2 self,
3 *,
4 title: str = "Rich",
5 theme: Optional[TerminalTheme] = None,
6 clear: bool = True,
7 code_format: str = CONSOLE_SVG_FORMAT,
8 font_aspect_ratio: float = 0.61,
9 unique_id: Optional[str] = None,
10 ) -> str:
11 """
12 Generate an SVG from the console contents (requires record=True in Console constructor).
13
14 Args:
15 title (str, optional): The title of the tab in the output image
16 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
17 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
18 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables
19 into the string in order to form the final SVG output. The default template used and the variables
20 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
21 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format``
22 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font).
23 If you aren't specifying a different font inside ``code_format``, you probably don't need this.
24 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node
25 ids). If not set, this defaults to a computed value based on the recorded content.
26 """
27
28 from rich.cells import cell_len
29
30 style_cache: Dict[Style, str] = {}
31
32 def get_svg_style(style: Style) -> str:
33 """Convert a Style to CSS rules for SVG."""
34 if style in style_cache:
35 return style_cache[style]
36 css_rules = []
37 color = (
38 _theme.foreground_color
39 if (style.color is None or style.color.is_default)
40 else style.color.get_truecolor(_theme)
41 )
42 bgcolor = (
43 _theme.background_color
44 if (style.bgcolor is None or style.bgcolor.is_default)
45 else style.bgcolor.get_truecolor(_theme)
46 )
47 if style.reverse:
48 color, bgcolor = bgcolor, color
49 if style.dim:
50 color = blend_rgb(color, bgcolor, 0.4)
51 css_rules.append(f"fill: {color.hex}")
52 if style.bold:
53 css_rules.append("font-weight: bold")
54 if style.italic:
55 css_rules.append("font-style: italic;")
56 if style.underline:
57 css_rules.append("text-decoration: underline;")
58 if style.strike:
59 css_rules.append("text-decoration: line-through;")
60
61 css = ";".join(css_rules)
62 style_cache[style] = css
63 return css
64
65 _theme = theme or SVG_EXPORT_THEME
66
67 width = self.width
68 char_height = 20
69 char_width = char_height * font_aspect_ratio
70 line_height = char_height * 1.22
71
72 margin_top = 1
73 margin_right = 1
74 margin_bottom = 1
75 margin_left = 1
76
77 padding_top = 40
78 padding_right = 8
79 padding_bottom = 8
80 padding_left = 8
81
82 padding_width = padding_left + padding_right
83 padding_height = padding_top + padding_bottom
84 margin_width = margin_left + margin_right
85 margin_height = margin_top + margin_bottom
86
87 text_backgrounds: List[str] = []
88 text_group: List[str] = []
89 classes: Dict[str, int] = {}
90 style_no = 1
91
92 def escape_text(text: str) -> str:
93 """HTML escape text and replace spaces with nbsp."""
94 return escape(text).replace(" ", " ")
95
96 def make_tag(
97 name: str, content: Optional[str] = None, **attribs: object
98 ) -> str:
99 """Make a tag from name, content, and attributes."""
100
101 def stringify(value: object) -> str:
102 if isinstance(value, (float)):
103 return format(value, "g")
104 return str(value)
105
106 tag_attribs = " ".join(
107 f'{k.lstrip("_").replace("_", "-")}="{stringify(v)}"'
108 for k, v in attribs.items()
109 )
110 return (
111 f"<{name} {tag_attribs}>{content}</{name}>"
112 if content
113 else f"<{name} {tag_attribs}/>"
114 )
115
116 with self._record_buffer_lock:
117 segments = list(Segment.filter_control(self._record_buffer))
118 if clear:
119 self._record_buffer.clear()
120
121 if unique_id is None:
122 unique_id = "terminal-" + str(
123 zlib.adler32(
124 ("".join(repr(segment) for segment in segments)).encode(
125 "utf-8",
126 "ignore",
127 )
128 + title.encode("utf-8", "ignore")
129 )
130 )
131 y = 0
132 for y, line in enumerate(Segment.split_and_crop_lines(segments, length=width)):
133 x = 0
134 for text, style, _control in line:
135 style = style or Style()
136 rules = get_svg_style(style)
137 if rules not in classes:
138 classes[rules] = style_no
139 style_no += 1
140 class_name = f"r{classes[rules]}"
141
142 if style.reverse:
143 has_background = True
144 background = (
145 _theme.foreground_color.hex
146 if style.color is None
147 else style.color.get_truecolor(_theme).hex
148 )
149 else:
150 bgcolor = style.bgcolor
151 has_background = bgcolor is not None and not bgcolor.is_default
152 background = (
153 _theme.background_color.hex
154 if style.bgcolor is None
155 else style.bgcolor.get_truecolor(_theme).hex
156 )
157
158 text_length = cell_len(text)
159 if has_background:
160 text_backgrounds.append(
161 make_tag(
162 "rect",
163 fill=background,
164 x=x * char_width,
165 y=y * line_height + 1.5,
166 width=char_width * text_length,
167 height=line_height + 0.25,
168 shape_rendering="crispEdges",
169 )
170 )
171
172 if text != " " * len(text):
173 text_group.append(
174 make_tag(
175 "text",
176 escape_text(text),
177 _class=f"{unique_id}-{class_name}",
178 x=x * char_width,
179 y=y * line_height + char_height,
180 textLength=char_width * len(text),
181 clip_path=f"url(#{unique_id}-line-{y})",
182 )
183 )
184 x += cell_len(text)
185
186 line_offsets = [line_no * line_height + 1.5 for line_no in range(y)]
187 lines = "\n".join(
188 f"""<clipPath id="{unique_id}-line-{line_no}">
189 {make_tag("rect", x=0, y=offset, width=char_width * width, height=line_height + 0.25)}
190 </clipPath>"""
191 for line_no, offset in enumerate(line_offsets)
192 )
193
194 styles = "\n".join(
195 f".{unique_id}-r{rule_no} {{ {css} }}" for css, rule_no in classes.items()
196 )
197 backgrounds = "".join(text_backgrounds)
198 matrix = "".join(text_group)
199
200 terminal_width = ceil(width * char_width + padding_width)
201 terminal_height = (y + 1) * line_height + padding_height
202 chrome = make_tag(
203 "rect",
204 fill=_theme.background_color.hex,
205 stroke="rgba(255,255,255,0.35)",
206 stroke_width="1",
207 x=margin_left,
208 y=margin_top,
209 width=terminal_width,
210 height=terminal_height,
211 rx=8,
212 )
213
214 title_color = _theme.foreground_color.hex
215 if title:
216 chrome += make_tag(
217 "text",
218 escape_text(title),
219 _class=f"{unique_id}-title",
220 fill=title_color,
221 text_anchor="middle",
222 x=terminal_width // 2,
223 y=margin_top + char_height + 6,
224 )
225 chrome += f"""
226 <g transform="translate(26,22)">
227 <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
228 <circle cx="22" cy="0" r="7" fill="#febc2e"/>
229 <circle cx="44" cy="0" r="7" fill="#28c840"/>
230 </g>
231 """
232
233 svg = code_format.format(
234 unique_id=unique_id,
235 char_width=char_width,
236 char_height=char_height,
237 line_height=line_height,
238 terminal_width=char_width * width - 1,
239 terminal_height=(y + 1) * line_height - 1,
240 width=terminal_width + margin_width,
241 height=terminal_height + margin_height,
242 terminal_x=margin_left + padding_left,
243 terminal_y=margin_top + padding_top,
244 styles=styles,
245 chrome=chrome,
246 backgrounds=backgrounds,
247 matrix=matrix,
248 lines=lines,
249 )
250 return svg
Path 2: 1 calls (0.33)
'Rich' (1)
None (1)
True (1)
'
1def export_svg(
2 self,
3 *,
4 title: str = "Rich",
5 theme: Optional[TerminalTheme] = None,
6 clear: bool = True,
7 code_format: str = CONSOLE_SVG_FORMAT,
8 font_aspect_ratio: float = 0.61,
9 unique_id: Optional[str] = None,
10 ) -> str:
11 """
12 Generate an SVG from the console contents (requires record=True in Console constructor).
13
14 Args:
15 title (str, optional): The title of the tab in the output image
16 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
17 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
18 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables
19 into the string in order to form the final SVG output. The default template used and the variables
20 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
21 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format``
22 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font).
23 If you aren't specifying a different font inside ``code_format``, you probably don't need this.
24 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node
25 ids). If not set, this defaults to a computed value based on the recorded content.
26 """
27
28 from rich.cells import cell_len
29
30 style_cache: Dict[Style, str] = {}
31
32 def get_svg_style(style: Style) -> str:
33 """Convert a Style to CSS rules for SVG."""
34 if style in style_cache:
35 return style_cache[style]
36 css_rules = []
37 color = (
38 _theme.foreground_color
39 if (style.color is None or style.color.is_default)
40 else style.color.get_truecolor(_theme)
41 )
42 bgcolor = (
43 _theme.background_color
44 if (style.bgcolor is None or style.bgcolor.is_default)
45 else style.bgcolor.get_truecolor(_theme)
46 )
47 if style.reverse:
48 color, bgcolor = bgcolor, color
49 if style.dim:
50 color = blend_rgb(color, bgcolor, 0.4)
51 css_rules.append(f"fill: {color.hex}")
52 if style.bold:
53 css_rules.append("font-weight: bold")
54 if style.italic:
55 css_rules.append("font-style: italic;")
56 if style.underline:
57 css_rules.append("text-decoration: underline;")
58 if style.strike:
59 css_rules.append("text-decoration: line-through;")
60
61 css = ";".join(css_rules)
62 style_cache[style] = css
63 return css
64
65 _theme = theme or SVG_EXPORT_THEME
66
67 width = self.width
68 char_height = 20
69 char_width = char_height * font_aspect_ratio
70 line_height = char_height * 1.22
71
72 margin_top = 1
73 margin_right = 1
74 margin_bottom = 1
75 margin_left = 1
76
77 padding_top = 40
78 padding_right = 8
79 padding_bottom = 8
80 padding_left = 8
81
82 padding_width = padding_left + padding_right
83 padding_height = padding_top + padding_bottom
84 margin_width = margin_left + margin_right
85 margin_height = margin_top + margin_bottom
86
87 text_backgrounds: List[str] = []
88 text_group: List[str] = []
89 classes: Dict[str, int] = {}
90 style_no = 1
91
92 def escape_text(text: str) -> str:
93 """HTML escape text and replace spaces with nbsp."""
94 return escape(text).replace(" ", " ")
95
96 def make_tag(
97 name: str, content: Optional[str] = None, **attribs: object
98 ) -> str:
99 """Make a tag from name, content, and attributes."""
100
101 def stringify(value: object) -> str:
102 if isinstance(value, (float)):
103 return format(value, "g")
104 return str(value)
105
106 tag_attribs = " ".join(
107 f'{k.lstrip("_").replace("_", "-")}="{stringify(v)}"'
108 for k, v in attribs.items()
109 )
110 return (
111 f"<{name} {tag_attribs}>{content}</{name}>"
112 if content
113 else f"<{name} {tag_attribs}/>"
114 )
115
116 with self._record_buffer_lock:
117 segments = list(Segment.filter_control(self._record_buffer))
118 if clear:
119 self._record_buffer.clear()
120
121 if unique_id is None:
122 unique_id = "terminal-" + str(
123 zlib.adler32(
124 ("".join(repr(segment) for segment in segments)).encode(
125 "utf-8",
126 "ignore",
127 )
128 + title.encode("utf-8", "ignore")
129 )
130 )
131 y = 0
132 for y, line in enumerate(Segment.split_and_crop_lines(segments, length=width)):
133 x = 0
134 for text, style, _control in line:
135 style = style or Style()
136 rules = get_svg_style(style)
137 if rules not in classes:
138 classes[rules] = style_no
139 style_no += 1
140 class_name = f"r{classes[rules]}"
141
142 if style.reverse:
143 has_background = True
144 background = (
145 _theme.foreground_color.hex
146 if style.color is None
147 else style.color.get_truecolor(_theme).hex
148 )
149 else:
150 bgcolor = style.bgcolor
151 has_background = bgcolor is not None and not bgcolor.is_default
152 background = (
153 _theme.background_color.hex
154 if style.bgcolor is None
155 else style.bgcolor.get_truecolor(_theme).hex
156 )
157
158 text_length = cell_len(text)
159 if has_background:
160 text_backgrounds.append(
161 make_tag(
162 "rect",
163 fill=background,
164 x=x * char_width,
165 y=y * line_height + 1.5,
166 width=char_width * text_length,
167 height=line_height + 0.25,
168 shape_rendering="crispEdges",
169 )
170 )
171
172 if text != " " * len(text):
173 text_group.append(
174 make_tag(
175 "text",
176 escape_text(text),
177 _class=f"{unique_id}-{class_name}",
178 x=x * char_width,
179 y=y * line_height + char_height,
180 textLength=char_width * len(text),
181 clip_path=f"url(#{unique_id}-line-{y})",
182 )
183 )
184 x += cell_len(text)
185
186 line_offsets = [line_no * line_height + 1.5 for line_no in range(y)]
187 lines = "\n".join(
188 f"""<clipPath id="{unique_id}-line-{line_no}">
189 {make_tag("rect", x=0, y=offset, width=char_width * width, height=line_height + 0.25)}
190 </clipPath>"""
191 for line_no, offset in enumerate(line_offsets)
192 )
193
194 styles = "\n".join(
195 f".{unique_id}-r{rule_no} {{ {css} }}" for css, rule_no in classes.items()
196 )
197 backgrounds = "".join(text_backgrounds)
198 matrix = "".join(text_group)
199
200 terminal_width = ceil(width * char_width + padding_width)
201 terminal_height = (y + 1) * line_height + padding_height
202 chrome = make_tag(
203 "rect",
204 fill=_theme.background_color.hex,
205 stroke="rgba(255,255,255,0.35)",
206 stroke_width="1",
207 x=margin_left,
208 y=margin_top,
209 width=terminal_width,
210 height=terminal_height,
211 rx=8,
212 )
213
214 title_color = _theme.foreground_color.hex
215 if title:
216 chrome += make_tag(
217 "text",
218 escape_text(title),
219 _class=f"{unique_id}-title",
220 fill=title_color,
221 text_anchor="middle",
222 x=terminal_width // 2,
223 y=margin_top + char_height + 6,
224 )
225 chrome += f"""
226 <g transform="translate(26,22)">
227 <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
228 <circle cx="22" cy="0" r="7" fill="#febc2e"/>
229 <circle cx="44" cy="0" r="7" fill="#28c840"/>
230 </g>
231 """
232
233 svg = code_format.format(
234 unique_id=unique_id,
235 char_width=char_width,
236 char_height=char_height,
237 line_height=line_height,
238 terminal_width=char_width * width - 1,
239 terminal_height=(y + 1) * line_height - 1,
240 width=terminal_width + margin_width,
241 height=terminal_height + margin_height,
242 terminal_x=margin_left + padding_left,
243 terminal_y=margin_top + padding_top,
244 styles=styles,
245 chrome=chrome,
246 backgrounds=backgrounds,
247 matrix=matrix,
248 lines=lines,
249 )
250 return svg