如何实现SVG转Mermaid图

最近工作中经常需要将原来的svg图转换为Mermaid,方便集成显示和分享,而且Mermaid格式更方便大模型进行解读,因此就做了这个转换工具,其他类型的转换工具详见之前文章,目前已经支持将近30种转换,涵盖各类office、图片、流程图、符号模式和专业格式的相互转换等等。主要内容如下,供参考。

一、主要功能

主要实现将 SVG格式 的组织架构图、流程图和泳道图转换为 Mermaid 格式。

具体支持:

  • Mermaid flowchart SVG(流程图):rect、circle、polygon(diamond)
  • Mermaid sequenceDiagram SVG(时序图)
  • 通用 SVG 流程图(手工绘制的组织架构图等)
  • 边标签(通过 data-id 精确匹配)
  • 控制结构(alt/loop/opt)
  • foreignObject 中的 HTML 内容(含换行等标签)
二、实现效果

原始svg

转换后的Mermaid

三、核心代码
python 复制代码
'''  
@author: sunjava  
用法:  
  python svg2Mermaid.py input.svg  
  python svg2Mermaid.py input.svg --output output.md  
  python svg2Mermaid.py input.svg --direction TD  
'''  
  
import argparse  
import xml.etree.ElementTree as ET  
import re  
import math  
import sys  
from typing import List, Optional, Tuple, Dict  
from dataclasses import dataclass    
  
# ==================== Data Classes ====================    
@dataclass  
class Node:  
    id: str  
    x: float  
    y: float  
    width: float  
    height: float  
    text: str  
    shape: str = "rect"  
  
  
@dataclass  
class Edge:  
    source: str  
    target: str  
    label: str = ""  
  
  
@dataclass  
class SeqParticipant:  
    name: str  
    x: float  
  
  
@dataclass  
class SeqMessage:  
    from_: str  
    to_: str  
    text: str  
    y: float  
    is_self: bool = False  
  
  
@dataclass  
class SeqControl:  
    type: str  
    label: str  
    y_start: float  
    y_end: float  
    branches: List[dict]  
  
  
# ==================== SVG Preprocessor ====================    
VOID_TAGS = [  
    'br', 'hr', 'img', 'input', 'meta', 'link',  
    'embed', 'param', 'source', 'track', 'wbr',  
    'area', 'base', 'col'  
]    
  
def preprocess_svg(svg_content: str) -> str:  
    """将 HTML 空标签转为 XML 自闭合形式,避免解析错误。"""  
    for tag in VOID_TAGS:  
        svg_content = re.sub(  
            rf'</{tag}\s*>', f'<{tag}/>', svg_content, flags=re.IGNORECASE  
        )  
    for tag in VOID_TAGS:  
        pattern = re.compile(rf'<{tag}\b[^>]*>', re.IGNORECASE)  
  
        def fix_tag(match: re.Match) -> str:  
            full = match.group(0)  
            if full.rstrip().endswith('/>'):  
                return full  
            return full[:-1] + '/>'  
  
        svg_content = pattern.sub(fix_tag, svg_content)  
    return svg_content  
  
  
# ==================== Text Extractor ====================  
  
def get_text(elem: ET.Element) -> str:  
    """递归提取元素文本,保留 <br> 换行标记。"""  
    texts: List[str] = []  
  
    def collect(e: ET.Element) -> None:  
        if e.text and e.text.strip():  
            texts.append(e.text.strip())  
        for child in e:  
            tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag  
            if tag.lower() in ('br', 'hr'):  
                texts.append('<br>')  
            else:  
                collect(child)  
            if child.tail and child.tail.strip():  
                texts.append(child.tail.strip())  
  
    collect(elem)  
    return ' '.join(texts) if texts else ""  
  
  
def get_text_simple(elem: ET.Element) -> str:  
    """简单文本提取(用于 tspan)。"""  
    txt = ""  
    for tspan in elem.findall('.//{http://www.w3.org/2000/svg}tspan'):  
        if tspan.text and tspan.text.strip():  
            txt = tspan.text.strip()  
            break  
    if not txt and elem.text and elem.text.strip():  
        txt = elem.text.strip()  
    return txt  
  
# 1.FlowchartSVG
# 2.GenericSVG
# 3.SequenceDiagramSVG
# ==================== Flowchart Parser (Mermaid生成) ====================  
  
class FlowchartParser:  
    def __init__(self, root: ET.Element):  
        self.root = root  
        self.ns = {"svg": "http://www.w3.org/2000/svg"}  
        self.nodes: List[Node] = []  
        self.edges: List[Edge] = []  
        self._node_counter = 0  
  
    def _next_id(self) -> str:  
        self._node_counter += 1  
        return f"N{self._node_counter}"  
  
    def _parse_transform(self, transform: str) -> Tuple[float, float]:  
        match = re.search(r"translate\(([^,]+),\s*([^)]+)\)", transform)  
        if match:  
            return float(match.group(1)), float(match.group(2))  
        return 0.0, 0.0  
  
    def _parse_path_endpoints(self, d: str) -> Optional[Tuple[Tuple[float, float], Tuple[float, float]]]:  
        d = re.sub(r'([MmLlHhVvCcSsQqTtAaZz])', r' \1 ', d)  
        d = re.sub(r'\s+', ' ', d).strip()  
        tokens = d.split()  
        points: List[Tuple[float, float]] = []  
        i = 0  
        current_x, current_y = 0.0, 0.0  
        last_cmd: Optional[str] = None  
        while i < len(tokens):  
            token = tokens[i]  
            if token in 'MmLlHhVvCcSsQqTtAaZz':  
                last_cmd = token  
                i += 1  
                continue  
            coords = token.split(',')  
            if last_cmd in 'Mm':  
                if len(coords) >= 2:  
                    current_x, current_y = float(coords[0]), float(coords[1])  
                    points.append((current_x, current_y))  
                i += 1  
            elif last_cmd in 'Ll':  
                if len(coords) >= 2:  
                    current_x, current_y = float(coords[0]), float(coords[1])  
                    points.append((current_x, current_y))  
                i += 1  
            elif last_cmd in 'Hh':  
                current_x = float(token)  
                points.append((current_x, current_y))  
                i += 1  
            elif last_cmd in 'Vv':  
                current_y = float(token)  
                points.append((current_x, current_y))  
                i += 1  
            elif last_cmd in 'Cc':  
                for _ in range(3):  
                    if i < len(tokens):  
                        c = tokens[i].split(',')  
                        if len(c) >= 2:  
                            current_x, current_y = float(c[0]), float(c[1])  
                        i += 1  
                points.append((current_x, current_y))  
            elif last_cmd in 'Qq':  
                for _ in range(2):  
                    if i < len(tokens):  
                        c = tokens[i].split(',')  
                        if len(c) >= 2:  
                            current_x, current_y = float(c[0]), float(c[1])  
                        i += 1  
                points.append((current_x, current_y))  
            else:  
                if len(coords) >= 2:  
                    try:  
                        current_x, current_y = float(coords[0]), float(coords[1])  
                        points.append((current_x, current_y))  
                    except ValueError:  
                        pass  
                i += 1  
        if len(points) >= 2:  
            return points[0], points[-1]  
        return None  
  
    def _get_absolute_position(self, elem: ET.Element) -> Tuple[float, float]:  
        parent_map = {}  
        for parent in self.root.iter():  
            for child in parent:  
                parent_map[child] = parent  
        tx, ty = 0.0, 0.0  
        current: Optional[ET.Element] = elem  
        while current is not None:  
            transform = current.get("transform", "")  
            dx, dy = self._parse_transform(transform)  
            tx += dx  
            ty += dy  
            current = parent_map.get(current)  
        return tx, ty  
  
    def _point_in_node(self, x: float, y: float, node: Node, margin: float = 20) -> bool:  
        return (node.x - margin <= x <= node.x + node.width + margin and  
                node.y - margin <= y <= node.y + node.height + margin)  
  
    def _find_node_at_point(self, x: float, y: float, exclude: Optional[Node] = None) -> Optional[Node]:  
        for node in self.nodes:  
            if exclude and node.id == exclude.id:  
                continue  
            if self._point_in_node(x, y, node, margin=30):  
                return node  
        best: Optional[Node] = None  
        best_dist = float("inf")  
        for node in self.nodes:  
            if exclude and node.id == exclude.id:  
                continue  
            cx = node.x + node.width / 2  
            cy = node.y + node.height / 2  
            d = math.sqrt((x - cx) ** 2 + (y - cy) ** 2)  
            if d < best_dist:  
                best_dist = d  
                best = node  
        return best  
  
    def _parse_polygon_bbox(self, points_str: str) -> Tuple[float, float, float, float]:  
        """Parse polygon points to bounding box."""  
        coords = re.findall(r'(-?\d+\.?\d*),(-?\d+\.?\d*)', points_str)  
        xs = [float(c[0]) for c in coords]  
        ys = [float(c[1]) for c in coords]  
        return min(xs), min(ys), max(xs), max(ys)  
  
    def parse(self) -> None:  
        # Edge labels: key = data-id (e.g. "L_F_A_0"), value = text  
        edge_labels: Dict[str, str] = {}  
        for g in self.root.findall(".//svg:g", self.ns):  
            data_id = g.get("data-id", "")  
            if not (data_id.startswith("L_") and data_id.endswith("_0")):  
                continue  
            fo = g.find(".//svg:foreignObject", self.ns)  
            if fo is None:  
                continue  
            txt = get_text(fo)  
            if txt:  
                edge_labels[data_id] = txt  
  
        # Nodes: rect, circle, polygon(diamond)  
        for g in self.root.findall(".//svg:g", self.ns):  
            gid = g.get("id", "")  
            if "flowchart-" not in gid:  
                continue  
  
            outer_tx, outer_ty = self._parse_transform(g.get("transform", ""))  
  
            shape_elem: Optional[ET.Element] = None  
            shape_type = "rect"  
  
            for child in g:  
                tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag  
                if tag == "rect":  
                    shape_elem = child  
                    shape_type = "rect"  
                    rx_val = child.get("rx", "0")  
                    try:  
                        if float(rx_val) > 3:  
                            shape_type = "roundrect"  
                    except ValueError:  
                        pass  
                    break                elif tag == "circle":  
                    shape_elem = child  
                    shape_type = "circle"  
                    break  
                elif tag == "polygon":  
                    shape_elem = child  
                    shape_type = "diamond"  
                    break  
  
            if shape_elem is None:  
                continue  
  
            tag = shape_elem.tag.split('}')[-1] if '}' in shape_elem.tag else shape_elem.tag  
            if tag == "rect":  
                rx = float(shape_elem.get("x", 0))  
                ry = float(shape_elem.get("y", 0))  
                rw = float(shape_elem.get("width", 0))  
                rh = float(shape_elem.get("height", 0))  
            elif tag == "circle":  
                cx = float(shape_elem.get("cx", 0))  
                cy = float(shape_elem.get("cy", 0))  
                r = float(shape_elem.get("r", 0))  
                rx, ry = cx - r, cy - r  
                rw, rh = r * 2, r * 2  
            elif tag == "polygon":  
                points_str = shape_elem.get("points", "")  
                min_x, min_y, max_x, max_y = self._parse_polygon_bbox(points_str)  
                ptx, pty = self._parse_transform(shape_elem.get("transform", ""))  
                rx = min_x + ptx  
                ry = min_y + pty  
                rw = max_x - min_x  
                rh = max_y - min_y  
            else:  
                continue  
  
            text_content = ""  
            for child in g:  
                ctag = child.tag.split('}')[-1] if '}' in child.tag else child.tag  
                if ctag != "g":  
                    continue  
  
                fo = child.find(".//svg:foreignObject", self.ns)  
                if fo is not None:  
                    txt = get_text(fo)  
                    if txt:  
                        text_content = txt  
                        break  
  
                text_elem = child.find(".//svg:text", self.ns)  
                if text_elem is not None:  
                    txt = get_text(text_elem)  
                    if txt:  
                        text_content = txt  
                        break  
  
            if text_content:  
                self.nodes.append(Node(  
                    id=self._next_id(),  
                    x=outer_tx + rx,  
                    y=outer_ty + ry,  
                    width=rw,  
                    height=rh,  
                    text=text_content,  
                    shape=shape_type  
                ))  
  
        # Edges: match labels by data-id for 100% accuracy  
        for path in self.root.findall(".//svg:path", self.ns):  
            marker_end = path.get("marker-end", "")  
            data_et = path.get("data-et", "")  
            data_edge = path.get("data-edge", "")  
            is_edge = ("pointEnd" in marker_end or data_et == "edge" or data_edge == "true")  
            if not is_edge:  
                continue  
            endpoints = self._parse_path_endpoints(path.get("d", ""))  
            if endpoints is None:  
                continue  
            (sx, sy), (ex, ey) = endpoints  
            source = self._find_node_at_point(sx, sy)  
            target = self._find_node_at_point(ex, ey)  
            if source and target and source.id != target.id:  
                data_id = path.get("data-id", "")  
                label = edge_labels.get(data_id, "")  
                self.edges.append(Edge(source=source.id, target=target.id, label=label))  
  
    def to_mermaid(self, direction: str = "LR") -> str:  
        if not self.nodes:  
            return "```mermaid\nflowchart LR\n    %% 未找到节点\n```"  
        lines: List[str] = [f"flowchart {direction}"]  
        for node in self.nodes:  
            text = node.text.replace("[", "&#91;").replace("]", "&#93;")  
            text = text.replace("(", "&#40;").replace(")", "&#41;")  
            text = text.replace("{", "&#123;").replace("}", "&#125;")  
            text = text.replace("|", "&#124;")  
            if node.shape == "circle":  
                shape = f"({text})"  
            elif node.shape == "roundrect":  
                shape = f"({text})"  
            elif node.shape == "diamond":  
                shape = f"{{{text}}}"  
            else:  
                shape = f"[{text}]"  
            lines.append(f"    {node.id}{shape}")  
        if self.edges:  
            lines.append("")  
            for edge in self.edges:  
                if edge.label:  
                    lines.append(f"    {edge.source} -->|{edge.label}| {edge.target}")  
                else:  
                    lines.append(f"    {edge.source} --> {edge.target}")  
        lines.insert(0, "```mermaid")  
        lines.append("```")  
        return "\n".join(lines)  
  
  
# ==================== Generic SVG Parser (通用SVG解析器) ====================  
  
class GenericSVGParser:  
    """通用SVG解析器,用于处理手工绘制的组织架构图等非Mermaid生成的SVG。"""  
    def __init__(self, root: ET.Element):  
        self.root = root  
        self.ns = {"svg": "http://www.w3.org/2000/svg"}  
        self.nodes: List[Node] = []  
        self.edges: List[Edge] = []  
        self._node_counter = 0  
  
    def _next_id(self) -> str:  
        self._node_counter += 1  
        return f"N{self._node_counter}"  
  
    def _parse_transform(self, transform: str) -> Tuple[float, float]:  
        """解析transform属性中的translate值。"""  
        match = re.search(r"translate\(([^,]+),\s*([^)]+)\)", transform)  
        if match:  
            return float(match.group(1)), float(match.group(2))  
        return 0.0, 0.0  
  
    def _get_text_inside_rect(self, rect_x: float, rect_y: float, rect_w: float, rect_h: float) -> str:  
        """查找矩形内的所有文本,按y坐标排序后拼接。"""  
        texts_in_rect = []  
        for text in self.root.findall(".//svg:text", self.ns):  
            tx = float(text.get("x", 0))  
            ty = float(text.get("y", 0))  
            # 检查文本是否在矩形内  
            if rect_x <= tx <= rect_x + rect_w and rect_y <= ty <= rect_y + rect_h:  
                txt = get_text_simple(text)  
                if txt:  
                    texts_in_rect.append((ty, txt))  
          
        # 按y坐标排序,拼接多行文本  
        texts_in_rect.sort(key=lambda x: x[0])  
          
        # 过滤掉装饰性文本  
        decorative_keywords = ['战略层', '业务层', '执行层', '协同层']  
        filtered_texts = []  
        for ty, txt in texts_in_rect:  
            is_decorative = False  
            for keyword in decorative_keywords:  
                if keyword in txt and len(txt) < 10:  
                    is_decorative = True  
                    break            if not is_decorative:  
                filtered_texts.append((ty, txt))  
          
        result = "<br>".join([t[1] for t in filtered_texts]) if filtered_texts else ""  
        return result  
  
    def parse(self) -> None:  
        """解析通用SVG,提取节点和连线。"""  
        # 解析矩形节点  
        for rect in self.root.findall(".//svg:rect", self.ns):  
            x = float(rect.get("x", 0))  
            y = float(rect.get("y", 0))  
            w = float(rect.get("width", 0))  
            h = float(rect.get("height", 0))  
              
            # 跳过太小的矩形(可能是装饰元素)  
            if w < 30 or h < 15:  
                continue  
            # 跳过背景矩形(通常覆盖整个画布)  
            if w >= 700 and h >= 500:  
                continue  
            # 跳过图例区域(通常在底部)  
            if y > 500:  
                continue  
            # 获取transform偏移  
            tx, ty = self._parse_transform(rect.get("transform", ""))  
              
            actual_x = x + tx  
            actual_y = y + ty  
              
            # 查找矩形内的文本  
            text = self._get_text_inside_rect(actual_x, actual_y, w, h)  
              
            # 过滤掉标题和装饰性文本  
            if text and not self._is_decorative_text(text):  
                self.nodes.append(Node(  
                    id=self._next_id(),  
                    x=actual_x,  
                    y=actual_y,  
                    width=w,  
                    height=h,  
                    text=text,  
                    shape="rect"  
                ))  
  
        # 解析连线(line)  
        # 收集所有线段  
        lines_data = []  
        for line in self.root.findall(".//svg:line", self.ns):  
            x1 = float(line.get("x1", 0))  
            y1 = float(line.get("y1", 0))  
            x2 = float(line.get("x2", 0))  
            y2 = float(line.get("y2", 0))  
              
            # 跳过图例区域的线  
            if y1 > 500 or y2 > 500:  
                continue  
            lines_data.append((x1, y1, x2, y2))  
          
        # 处理所有线段(包括斜线)  
        for x1, y1, x2, y2 in lines_data:  
            # 确定上方点为起点,下方点为终点  
            if y1 < y2:  
                top_x, top_y = x1, y1  
                bottom_x, bottom_y = x2, y2  
            elif y2 < y1:  
                top_x, top_y = x2, y2  
                bottom_x, bottom_y = x1, y1  
            else:  
                # 水平线,跳过  
                continue  
            # 查找上方节点(父节点)  
            parent = self._find_node_at_point(top_x, top_y)  
            # 查找下方节点(子节点)  
            child = self._find_node_at_point(bottom_x, bottom_y)  
              
            if parent and child and parent.id != child.id:  
                edge_key = (parent.id, child.id)  
                if not any(e.source == edge_key[0] and e.target == edge_key[1] for e in self.edges):  
                    self.edges.append(Edge(source=parent.id, target=child.id))  
  
        # 解析path连线(曲线)  
        for path in self.root.findall(".//svg:path", self.ns):  
            d = path.get("d", "")  
            if not d:  
                continue  
            # 提取起点和终点  
            points = re.findall(r'[ML]\s*([\d.]+),([\d.]+)', d)  
            if len(points) >= 2:  
                x1, y1 = float(points[0][0]), float(points[0][1])  
                x2, y2 = float(points[-1][0]), float(points[-1][1])  
                  
                # 跳过图例区域的路径  
                if y1 > 500 or y2 > 500:  
                    continue  
                source = self._find_node_at_point(x1, y1)  
                target = self._find_node_at_point(x2, y2)  
                  
                if source and target and source.id != target.id:  
                    edge_key = (source.id, target.id)  
                    if not any(e.source == edge_key[0] and e.target == edge_key[1] for e in self.edges):  
                        self.edges.append(Edge(source=source.id, target=target.id))  
      
    def _is_decorative_text(self, text: str) -> bool:  
        """判断是否为装饰性文本(标题、层级标注等)。"""  
        # 过滤掉纯标题(单行且包含关键词)  
        decorative_keywords = ['关系图', '图例']  
        for keyword in decorative_keywords:  
            if keyword in text and '<br>' not in text:  
                return True  
        return False  
    def _find_node_at_point(self, x: float, y: float, margin: float = 10) -> Optional[Node]:  
        """查找指定坐标处的节点,优先匹配边界上的节点。"""  
        # 首先尝试精确匹配(点在节点边界上)  
        for node in self.nodes:  
            # 检查点是否在节点的上边界或下边界附近  
            on_top_edge = abs(y - node.y) <= margin and node.x - margin <= x <= node.x + node.width + margin  
            on_bottom_edge = abs(y - (node.y + node.height)) <= margin and node.x - margin <= x <= node.x + node.width + margin  
            on_left_edge = abs(x - node.x) <= margin and node.y - margin <= y <= node.y + node.height + margin  
            on_right_edge = abs(x - (node.x + node.width)) <= margin and node.y - margin <= y <= node.y + node.height + margin  
              
            if on_top_edge or on_bottom_edge or on_left_edge or on_right_edge:  
                return node  
          
        # 如果边界匹配失败,尝试区域匹配  
        for node in self.nodes:  
            if (node.x - margin <= x <= node.x + node.width + margin and  
                node.y - margin <= y <= node.y + node.height + margin):  
                return node  
          
        # 最后找最近的节点(限制距离)  
        best_node = None  
        best_dist = float('inf')  
        for node in self.nodes:  
            cx = node.x + node.width / 2  
            cy = node.y + node.height / 2  
            dist = math.sqrt((x - cx) ** 2 + (y - cy) ** 2)  
            if dist < best_dist:  
                best_dist = dist  
                best_node = node  
          
        # 只有当距离足够近时才返回  
        if best_dist < 100:  
            return best_node  
        return None  
  
    def to_mermaid(self, direction: str = "TD") -> str:  
        """转换为Mermaid代码。"""  
        if not self.nodes:  
            return "```mermaid\nflowchart TD\n    %% 未找到节点\n```"  
        lines: List[str] = [f"flowchart {direction}"]  
          
        for node in self.nodes:  
            text = node.text.replace("[", "&#91;").replace("]", "&#93;")  
            text = text.replace("(", "&#40;").replace(")", "&#41;")  
            text = text.replace("{", "&#123;").replace("}", "&#125;")  
            text = text.replace("|", "&#124;")  
            shape = f"[{text}]"  
            lines.append(f"    {node.id}{shape}")  
          
        if self.edges:  
            lines.append("")  
            for edge in self.edges:  
                lines.append(f"    {edge.source} --> {edge.target}")  
          
        lines.insert(0, "```mermaid")  
        lines.append("```")  
        return "\n".join(lines)  
  
  
# ==================== Sequence Diagram Parser ====================  
  
# ==================== Main Converter ====================  
  
class SVGToMermaid:  
    def __init__(self, svg_content: str):  
        self.svg_content = preprocess_svg(svg_content)  
        self.diagram_type = "unknown"  
        self.output = ""  
  
    def detect_type(self, root: ET.Element) -> str:  
        """检测SVG类型:Mermaid生成、时序图、通用SVG。"""  
        aria = root.get('aria-roledescription', '')  
        if 'sequence' in aria.lower():  
            return "sequence"  
        if 'flowchart' in aria.lower():  
            return "flowchart"  
        # 检查是否有Mermaid特有的标记  
        ns = {"svg": "http://www.w3.org/2000/svg"}  
          
        # 检查participant元素(时序图)  
        for g in root.findall(".//svg:g", ns):  
            if g.get('data-et') == 'participant':  
                return "sequence"  
        # 检查flowchart节点  
        for g in root.findall(".//svg:g", ns):  
            if "flowchart-" in g.get('id', ''):  
                return "flowchart"  
        # 检查是否有data-et="edge"标记  
        for path in root.findall(".//svg:path", ns):  
            if path.get('data-et') == 'edge':  
                return "flowchart"  
        # 如果有rect和text元素,但没有Mermaid标记,认为是通用SVG  
        has_rect = len(root.findall(".//svg:rect", ns)) > 0  
        has_text = len(root.findall(".//svg:text", ns)) > 0  
        if has_rect and has_text:  
            return "generic"  
        return "unknown"  
  
    def parse(self) -> "SVGToMermaid":  
        root = ET.fromstring(self.svg_content)  
        self.diagram_type = self.detect_type(root)  
  
        if self.diagram_type == "sequence":  
            parser = SequenceParser(root)  
            parser.parse()  
            self.output = parser.to_mermaid()  
        elif self.diagram_type == "flowchart":  
            parser = FlowchartParser(root)  
            parser.parse()  
            self.output = parser.to_mermaid("LR")  
        elif self.diagram_type == "generic":  
            parser = GenericSVGParser(root)  
            parser.parse()  
            self.output = parser.to_mermaid("TD")  
        else:  
            self.output = "```mermaid\n%% 无法识别 SVG 类型\n```"  
  
        return self  
  
    def to_mermaid(self) -> str:  
        return self.output  
  
  
# ==================== CLI ====================  
  
def main():  
    parser = argparse.ArgumentParser(  
        description="将 SVG 流程图/时序图转换为 Mermaid 代码",  
        formatter_class=argparse.RawDescriptionHelpFormatter,  
        epilog="""  
				示例:  
				  python svg2Mermaid2.py diagram.svg  
				  python svg2Mermaid2.py diagram.svg -o result.md  .
				  python svg2Mermaid2.py diagram.svg --direction TD        """    )  
    parser.add_argument("input", help="输入 SVG 文件路径")  
    parser.add_argument("-o", "--output", help="输出文件路径(默认输出到控制台)")  
    parser.add_argument(  
        "--direction", "-d",  
        default="TD",  
        choices=["LR", "RL", "TD", "DT", "TB", "BT"],  
        help="流程图方向(默认: TD,时序图忽略此参数)"  
    )  
    args = parser.parse_args()  
  
    try:  
        with open(args.input, "r", encoding="utf-8") as f:  
            svg_content = f.read()  
    except FileNotFoundError:  
        print(f"错误: 文件未找到 '{args.input}'", file=sys.stderr)  
        sys.exit(1)  
    except Exception as e:  
        print(f"错误: 无法读取文件: {e}", file=sys.stderr)  
        sys.exit(1)  
  
    converter = SVGToMermaid(svg_content)  
    converter.parse()  
    mermaid = converter.to_mermaid()  
  
    if args.output:  
        with open(args.output, "w", encoding="utf-8") as f:  
            f.write(mermaid)  
        print(f"已保存到: {args.output}")  
    else:  
        print(mermaid)  
  
  
# 使用示例  
# python svg2Mermaid.py data/flow1.svg --output data/flow1.md  
# python svg2Mermaid.py data/flow3.svg --output data/flow3.md  
if __name__ == "__main__":  
    main()
相关推荐
PFFstronger1 小时前
基于 Dify 知识库 + Ollama 大模型,自动生成测试用例并导出 XMind 的完整方案
人工智能·python·测试用例·xmind
SomeBottle2 小时前
【小记】图片托管从又拍云迁移到腾讯云 EO + COS(兼容图片处理参数)
python·计算机网络·学习笔记·对象存储·cdn·折腾
cts6183 小时前
FDE架构师前端选型指南
python
2601_962293793 小时前
【Python教程:自动化处理文件】
python·自动化·编程·文件处理·os模块
禹凕3 小时前
快速幂算法详解与实战
python·算法
sylviiiiiia3 小时前
leetcode hot 100
python·算法·leetcode
半摆烂日常3 小时前
自建WMS和买成品:三年成本对比
大数据·服务器·数据库·python·深度学习·低代码·numpy
2601_962296514 小时前
Python的哈希hashlib模块详细解读
python·md5·sha·加密算法·hashlib
chinesegf4 小时前
comfyui便携版的基础使用
python