|
51 | 51 | CONSOLE_SVG_FORMAT, |
52 | 52 | ) |
53 | 53 | from ._log_render import FormatTimeCallable, LogRender |
| 54 | +from ._loop import loop_last |
54 | 55 | from .align import Align, AlignMethod |
55 | 56 | from .color import ColorSystem |
56 | 57 | from .control import Control |
@@ -2364,6 +2365,142 @@ def export_svg( |
2364 | 2365 |
|
2365 | 2366 | return rendered_code |
2366 | 2367 |
|
| 2368 | + def export_svg( |
| 2369 | + self, |
| 2370 | + *, |
| 2371 | + title: str = "Rich", |
| 2372 | + theme: Optional[TerminalTheme] = None, |
| 2373 | + clear: bool = True, |
| 2374 | + code_format: str = CONSOLE_SVG_FORMAT, |
| 2375 | + id: str | None = None, |
| 2376 | + ) -> str: |
| 2377 | + |
| 2378 | + code_format = """ |
| 2379 | +<svg id="{unique_id}" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg"> |
| 2380 | + <style> |
| 2381 | + text {{ |
| 2382 | + font-family: Fira Code, monospace; |
| 2383 | + font-size: {char_height}px; |
| 2384 | + font-variant: east-asian-width-values; |
| 2385 | + line-height: {line_height}px; |
| 2386 | + dominant-baseline: text-before-edge; |
| 2387 | + }} |
| 2388 | + {styles} |
| 2389 | + </style> |
| 2390 | + <g transform="translate({terminal_x}, {terminal_y})"> |
| 2391 | + {backgrounds} |
| 2392 | + <text>{matrix}</text> |
| 2393 | + </g> |
| 2394 | +</svg> |
| 2395 | +""" |
| 2396 | + from rich.cells import cell_len |
| 2397 | + |
| 2398 | + style_cache: dict[Style, str] = {} |
| 2399 | + |
| 2400 | + def get_svg_style(style: Style) -> str: |
| 2401 | + if style in style_cache: |
| 2402 | + return style_cache[style] |
| 2403 | + css_rules = [] |
| 2404 | + if style.color is not None: |
| 2405 | + r, g, b = style.color.get_truecolor(_theme) |
| 2406 | + css_rules.append(f"fill: #{r:02X}{g:02x}{b:02X}") |
| 2407 | + |
| 2408 | + if style.bold: |
| 2409 | + css_rules.append("font-weight: bold") |
| 2410 | + if style.italic: |
| 2411 | + css_rules.append("font-style: italic;") |
| 2412 | + if style.underline: |
| 2413 | + css_rules.append("text-decoration: underline;") |
| 2414 | + |
| 2415 | + css = ";".join(css_rules) |
| 2416 | + style_cache[style] = css |
| 2417 | + return css |
| 2418 | + |
| 2419 | + _theme = theme or SVG_EXPORT_THEME |
| 2420 | + |
| 2421 | + width = 0 |
| 2422 | + char_height = 20 |
| 2423 | + char_width = char_height * 0.62 |
| 2424 | + line_height = char_height * 1.15 |
| 2425 | + |
| 2426 | + padding_top = 20 |
| 2427 | + padding_left = 10 |
| 2428 | + padding_right = 10 |
| 2429 | + padding_bottom = 20 |
| 2430 | + |
| 2431 | + text_backgrounds = [] |
| 2432 | + text_group: list[str] = [] |
| 2433 | + classes: dict[str, int] = {} |
| 2434 | + style_no = 1 |
| 2435 | + |
| 2436 | + with self._record_buffer_lock: |
| 2437 | + x = 0 |
| 2438 | + y = 1 |
| 2439 | + segments = list( |
| 2440 | + Segment.filter_control( |
| 2441 | + Segment.simplify(self._record_buffer), |
| 2442 | + ) |
| 2443 | + ) |
| 2444 | + |
| 2445 | + if id is None: |
| 2446 | + unique_id = "terminal-" + str( |
| 2447 | + zlib.adler32( |
| 2448 | + ("".join(segment.text for segment in segments)).encode( |
| 2449 | + "utf-8", "ignore" |
| 2450 | + ) |
| 2451 | + + title.encode("utf-8", "ignore") |
| 2452 | + ) |
| 2453 | + ) |
| 2454 | + else: |
| 2455 | + unique_id = id |
| 2456 | + for last, line in loop_last(Segment.split_lines(segments)): |
| 2457 | + x = 0 |
| 2458 | + for text, style, _control in line: |
| 2459 | + style = style or Style() |
| 2460 | + rules = get_svg_style(style) |
| 2461 | + if rules not in classes: |
| 2462 | + classes[rules] = style_no |
| 2463 | + style_no += 1 |
| 2464 | + class_name = f"r{classes[rules]}" |
| 2465 | + |
| 2466 | + for character in text: |
| 2467 | + if style.bgcolor is not None: |
| 2468 | + r, g, b = style.bgcolor.get_truecolor(_theme) |
| 2469 | + text_backgrounds.append( |
| 2470 | + f"""<rect fill="#{r:02X}{g:02x}{b:02X}" x="{x * char_width}" y="{y * line_height}" width="{char_width}" height="{line_height}"></rect> """ |
| 2471 | + ) |
| 2472 | + text_group.append( |
| 2473 | + f"""<tspan class="#{unique_id} {class_name}" x="{x * char_width}" y="{y * line_height}" textLength="{char_width}px">{character}</tspan>""" |
| 2474 | + ) |
| 2475 | + x += cell_len(character) |
| 2476 | + if not last: |
| 2477 | + text_group.append( |
| 2478 | + f"""<tspan x="{x * char_width}" y="{y * line_height}" textLength="{char_width}px">\n</tspan>""" |
| 2479 | + ) |
| 2480 | + width = max(x, width) |
| 2481 | + y += 1 |
| 2482 | + |
| 2483 | + styles = "\n".join( |
| 2484 | + f"#{unique_id} .r{rule_no} {{ {css} }}" for css, rule_no in classes.items() |
| 2485 | + ) |
| 2486 | + backgrounds = "".join(text_backgrounds) |
| 2487 | + matrix = "".join(text_group) |
| 2488 | + |
| 2489 | + svg = code_format.format( |
| 2490 | + unique_id=unique_id, |
| 2491 | + char_width=char_width, |
| 2492 | + char_height=char_height, |
| 2493 | + line_height=line_height, |
| 2494 | + width=width * char_width, |
| 2495 | + height=y * line_height, |
| 2496 | + terminal_x=0, |
| 2497 | + terminal_y=0, |
| 2498 | + styles=styles, |
| 2499 | + backgrounds=backgrounds, |
| 2500 | + matrix=matrix, |
| 2501 | + ) |
| 2502 | + return svg |
| 2503 | + |
2367 | 2504 | def save_svg( |
2368 | 2505 | self, |
2369 | 2506 | path: str, |
|
0 commit comments