Skip to content

SVG节点

PID组件中,通常需要SVG类型的图片作为元素进行使用,通过 gojsgeometry 进行描述图形


SVG标签

SVG 是一种用于描述二维矢量图形的 XML 格式,每个元素都是一个独立的对象

SVG标签的解析

对于.SVG类型的图片,都有对应的 SVG 的代码,代码通常由一系列的标签组成:

ts
<?xml version="1.0" encoding="UTF-8"?>
<!-- Do not edit this file with editors other than draw.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="41px"
    height="96px" viewBox="-0.5 -0.5 41 96"
    content="&lt;mxfile host=&quot;app.diagrams.net&quot;">
    <defs />
    <g>
        <g>
            <rect x="0" y="0" width="40" height="95" fill="none" stroke="none" pointer-events="all" />
            <path
                d="M 40 7.66 L 40 87.34 C 40 91.57 31.05 95 20 95 C 8.95 95 0 91.57 0 87.34 L 0 7.66 C 0 3.43 8.95 0 20 0 C 31.05 0 40 3.43 40 7.66Z  M 0 17.62 L 40 17.62 M0 77.38 L 40 77.38 M 0 17.62 L 40 77.38 M 0 77.38 L 40 17.62"
                fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" />
        </g>
    </g>
</svg>

能够解析完成的只有以下几种标签:

  • path:路径是由一系列直线和曲线段组成的复杂形状,在图形编程中,路径可以用来创建各种复杂的图形和轮廓
  • rect:矩形是一个四边形,其所有内角都是直角(90度)
  • circle:圆,由一个固定点(圆心)和固定距离(半径)的所有点组成
  • ellipse:椭圆,由两个焦点和到这两个焦点的距离之和为常数的所有点组成
  • line:线是一条直的、没有宽度的几何图形
  • polygon:多边形是一个由直线边组成的封闭平面图形
  • polyline:折线是由一系列相连的直线段组成的开放或封闭的图形

最不容易出错的是 path 标签,所以建议使用 path 标签来绘制图形


Gojs geometry

gojs中 的 geometry 是用于描述图形的语法,geometry 是一个对象,它包含了一系列的 pathpath 是一个字符串,它描述了一条路径。path 由一系列的命令组成,每个命令由一个字母和一些数字组成(几何路径字符串)

几何路径字符串

几何路径字符串的语法是 SVG 路径字符串语法的扩展,该字符串由许多命令组成,每个命令都是一个字母后跟一些特定于命令的数值参数

  • M: 移动到(Move to),M x y 将画笔移动到指定的坐标点,不画线

  • L: 绘制线到(Line to),L x y 从当前位置绘制一条直线到指定的坐标点

  • H: 水平线到(Horizontal line to),H x 从当前位置绘制一条水平线到指定的 x 坐标

  • V: 垂直线到(Vertical line to),V y 从当前位置绘制一条垂直线到指定的 y 坐标

  • Z: 闭合路径(Close path),如果路径的起点和终点重合,则闭合路径

  • F: 填充路径(Fill path),这个命令通常用于闭合路径,但它的具体行为取决于上下文

  • B: 贝塞尔曲线到(Bezier curve to),B x1 y1 x2 y2 x y 从当前位置绘制一条贝塞尔曲线到指定的坐标点,B (startAngle, sweepAngle, centerX, centerY, radiusX, radiusY)

  • X: 不绘制(No draw),这个命令用于在路径中跳过一个点,不影响当前位置

具体实例:geometryString: "F M120 0 L80 80 0 50z",

上述字符串描述了一个三角形形状,它的顶点在坐标 (120, 0)、(80, 80) 和 (0, 50)

  • F:表示填充路径。在 SVG 中,F 通常用于闭合路径,这意味着路径的最后一个点会自动连接到第一个点,形成一个闭合的形状
  • M120 0M 是移动到点 (120, 0) 的命令,这通常是路径的起点
  • L80 80L 是绘制直线到点 (80, 80) 的命令
  • 0 50:这是另一个点,路径会从当前位置绘制一条直线到这个点
  • z:闭合路径的命令,它会从当前点绘制一条直线到路径的起点,将路径闭合

SVG标签转化为geometry字符串

Gojs中不推荐直接引入SVG图片,更推荐使用geometry字符串来绘制SVG图形,因此我们就需要将我们的SVG标签进行转化,转成geometry字符串,可以使用以下的python脚本进行转化:

py
"""
1. 没有stroke属性的元素不会被转换。防止有些背景占位元素被转换。
2. 元素内的只有 fill stroke 和 特定元素会被识别。
    path: d
    rect: x y width height
    ellipse: cx cy rx ry
    line: x1 y1 x2 y2
    circle: cx cy r
    polyline: points
"""

import re
import xml.etree.ElementTree as ET

PATH = "{http://www.w3.org/2000/svg}path"
RECT = "{http://www.w3.org/2000/svg}rect"
ELLIPSE = "{http://www.w3.org/2000/svg}ellipse"
LINE = "{http://www.w3.org/2000/svg}line"
CIRCLE = "{http://www.w3.org/2000/svg}circle"
POLYLINE = "{http://www.w3.org/2000/svg}polyline"
POLYGON = "{http://www.w3.org/2000/svg}polygon"


# 读取svg文件.
def read_svg_file(svg_file_path) -> ET.ElementTree:
    tree = ET.parse(svg_file_path)
    return tree

# 判断元素是否有描边.
def element_has_stroke(element: ET.Element) -> bool:
    """
    判断元素是否有描边
    :param element: 元素
    :return: 是否有描边
    """
    try:
        stroke = element.attrib["stroke"]
        if isinstance(stroke, str) and stroke != "none":
            return True
    except KeyError:
        return False
    return False


# 将path标签转换为geometry.
def convert_path_tag_to_geometry(path: ET.Element):
    geometry = ""
    prefix = ""
    # 获取path元素的stroke属性,即描边颜色
    if not element_has_stroke(path):
        return None

    # 获取path元素的fill属性,即填充颜色
    fill = path.attrib["fill"]
    if isinstance(fill, str) and fill != "none":
        prefix = "F"
    else:
        prefix = ""
    # 获取d属性,即路径数据
    d = path.attrib["d"]
    if isinstance(d, str):
        geometry = "X" + prefix + d

    return geometry


def convert_rect_tag_to_geometry(rect: ET.Element):
    prefix = ""

    # 获取rect元素的stroke属性,即描边颜色
    # 不存在描边则直接返回
    if not element_has_stroke(rect):
        return None

    # 获取rect元素的fill属性,即填充颜色
    fill = rect.attrib["fill"]
    if isinstance(fill, str) and fill != "none":
        prefix = "F"
    else:
        prefix = ""

    # 获取rect元素的x、y、width、height属性,即矩形的位置和大小
    x = rect.attrib["x"]
    y = rect.attrib["y"]
    width = rect.attrib["width"]
    height = rect.attrib["height"]
    if isinstance(x, str):
        rect_right = float(x) + float(width)
        rect_bottom = float(y) + float(height)
        geometry = "X"+ prefix+ "M"+ x+ " "+ y+ " "+ "H "+ str(rect_right)+ " "+ "V "+ str(rect_bottom)+ " "+ "H "+ x+ " z"

    return geometry


def convert_ellipse_tag_to_geometry(ellipse: ET.Element):
    geometry = ''
    prefix = ""

    if not element_has_stroke(ellipse):
        return None
    # 获取ellipse元素的fill属性,即填充颜色
    fill = ellipse.attrib["fill"]
    if isinstance(fill, str) and fill != "none":
        prefix = "F"
    else:
        prefix = ""

    # 获取ellipse元素的cx、cy、rx、ry属性,即椭圆的位置和大小
    cx = ellipse.attrib["cx"]
    cy = ellipse.attrib["cy"]
    rx = ellipse.attrib["rx"]
    ry = ellipse.attrib["ry"]
    if (
        isinstance(cx, str)
        and isinstance(cy, str)
        and isinstance(rx, str)
        and isinstance(ry, str)
    ):
        start = str(float(cx) + float(rx))
        geometry = "X"+ prefix+ "M "+ start+ " "+ cy+ " "+ "B0 360 "+ cx+ " "+ cy+ " "+ rx+ " "+ ry

    return geometry


def convert_line_tag_to_geometry(line: ET.Element):
    geometry = ""
    prefix = ""

    if not element_has_stroke(line):
        return None

    # 获取line元素的x1、y1、x2、y2属性,即线段的位置
    x1 = line.attrib["x1"]
    y1 = line.attrib["y1"]
    x2 = line.attrib["x2"]
    y2 = line.attrib["y2"]
    if (
        isinstance(x1, str)
        and isinstance(y1, str)
        and isinstance(x2, str)
        and isinstance(y2, str)
    ):
        geometry = "X" + prefix + "M " + x1 + " " + y1 + " " + "L " + x2 + " " + y2
    return geometry


def convert_circle_to_geometry(circle: ET.Element):
    geometry = ""
    prefix = ""

    if not element_has_stroke(circle):
        return None

    # 获取circle元素的fill属性,即填充颜色
    fill = circle.attrib["fill"]
    if isinstance(fill, str) and fill != "none":
        prefix = "F"
    else:
        prefix = ""

    # 获取circle元素的cx、cy、r属性,即圆的位置和大小
    cx = circle.attrib["cx"]
    cy = circle.attrib["cy"]
    r = circle.attrib["r"]
    if isinstance(cx, str) and isinstance(cy, str) and isinstance(r, str):
        # 画圆时要先移动到圆的起始点,然后画一个圆弧。起始点为圆的右侧切点。 如 圆心 6 6 半径 3  则先移动到 9 6 然后画一个圆弧。
        geometry = "X"+ prefix+ "M"+ str(float(cx) + float(r))+ " "+ cy+ "B 0 360 "+ cx+ " "+ cy+ " "+ r+ " "+ r
    return geometry


def convert_polyline_to_geometry(polyline: ET.Element):
    geometry = ""
    prefix = ""

    if not element_has_stroke(polyline):
        return None

    # 获取polyline元素的fill属性,即填充颜色
    fill = polyline.attrib["fill"]
    if isinstance(fill, str) and fill != "none":
        prefix = "F"
    else:
        prefix = ""

    # 获取polyline元素的points属性,即折线的点
    points = polyline.attrib["points"]
    if isinstance(points, str):
        points_list = re.split(r" |,", points)
        if len(points_list) > 0:
            geometry = "X"+ prefix+ "M"+ " "+ " ".join(points_list[:2])+ "L"+ " ".join(points_list[2:])
    return geometry


def convert_polygon_to_geometry(polygon: ET.Element):
    geometry = ""
    prefix = ""

    if not element_has_stroke(polygon):
        return None

    # 获取polyline元素的fill属性,即填充颜色
    fill = polygon.attrib["fill"]
    if isinstance(fill, str) and fill != "none":
        prefix = "F"
    else:
        prefix = ""

    # 获取polyline元素的points属性,即折线的点
    points = polygon.attrib["points"]
    if isinstance(points, str):
        points_list = re.split(r" |,", points)
        if len(points_list) > 0:
            geometry = "X"+ prefix+ "M"+ " "+ " ".join(points_list[:2])+ "L"+ " ".join(points_list[2:])+ "z"
    return geometry


def convert_svg_to_geometry(svg_file_path):
    geometry_list = []
    svg_tree = read_svg_file(svg_file_path)

    handle_svg_element_map = {
        PATH: convert_path_tag_to_geometry,
        RECT: convert_rect_tag_to_geometry,
        ELLIPSE: convert_ellipse_tag_to_geometry,
        LINE: convert_line_tag_to_geometry,
        CIRCLE: convert_circle_to_geometry,
        POLYLINE: convert_polygon_to_geometry,
        POLYGON: convert_polygon_to_geometry,
    }

    for element in svg_tree.iter():
        if element.tag in handle_svg_element_map:
            geometry = handle_svg_element_map[element.tag](element)
            if geometry is not None:
                geometry_list.append(geometry)

    return " ".join(geometry_list)


if __name__ == "__main__":
    svg_file_path = "pid_node/渣池test2.svg"
    res = convert_svg_to_geometry(svg_file_path)
    print(res)

Released under the MIT License.