跳转至

节点类型 API

本页面详细介绍所有节点类型的构造函数、属性和方法。

Rect

轴对齐矩形,左上角原点,y 轴向下。

from latticesvg import Rect

r = Rect(x=10, y=20, width=100, height=50)
print(r.right)   # 110.0
print(r.bottom)  # 70.0
r2 = r.copy()
属性 类型 说明
x float 左上角 x 坐标(默认 0.0)
y float 左上角 y 坐标(默认 0.0)
width float 宽度(默认 0.0)
height float 高度(默认 0.0)
right float 只读,等价于 x + width
bottom float 只读,等价于 y + height
方法 返回类型 说明
copy() Rect 返回副本

LayoutConstraints

布局约束,从父节点传递给子节点。

from latticesvg import LayoutConstraints

c = LayoutConstraints(available_width=800, available_height=600)
属性 类型 默认值 说明
available_width float \| None None 可用宽度
available_height float \| None None 可用高度

Node(基类)

所有可布局元素的抽象基类。子类需要实现 measure()layout()

from latticesvg import Node

node = Node(style={"width": 100, "height": 50, "background": "#f0f0f0"})

构造参数

参数 类型 默认值 说明
style dict[str, Any] \| None None CSS 样式属性字典
parent Node \| None None 父节点(通常由 add() 自动设置)

属性

属性 类型 说明
style ComputedStyle 计算后的样式对象
parent Node \| None 父节点
children list[Node] 子节点列表
border_box Rect 布局后的边框盒矩形
padding_box Rect 布局后的内边距盒矩形
content_box Rect 布局后的内容盒矩形
placement PlacementHint Grid 放置信息

方法

add(child, *, row=None, col=None, row_span=1, col_span=1, area=None)

将子节点添加到当前节点,可选地设置 Grid 放置位置。

参数 类型 说明
child Node 要添加的子节点
row int \| None 行起始位置(1-based CSS Grid 行号)
col int \| None 列起始位置(1-based CSS Grid 行号)
row_span int 跨行数(默认 1)
col_span int 跨列数(默认 1)
area str \| None 命名区域名称

返回值: 被添加的子节点(支持链式调用)

grid.add(child, row=1, col=2, row_span=1, col_span=2)
grid.add(child, area="header")

measure(constraints) → (min_w, max_w, intrinsic_h)

返回节点的最小内容宽度、最大内容宽度和固有高度。由 Grid 求解器调用。

layout(constraints)

计算 border_boxpadding_boxcontent_box。子类必须实现。


GridContainer

CSS Grid 布局容器节点。将子节点排列为网格布局。

from latticesvg import GridContainer

grid = GridContainer(style={
    "grid-template-columns": "200px 1fr",
    "grid-template-rows": "auto auto",
    "gap": 10,
    "width": 600,
    "padding": 20,
})

构造参数

继承自 Node,同样接受 styleparent 参数。display 自动设为 "grid"

方法

layout(constraints=None, available_width=None, available_height=None)

运行完整的布局计算。作为根节点时可以直接传入可用宽度:

grid.layout(available_width=800)

如果不提供 available_width,优先使用样式中的 width 属性,否则默认为 800。

measure(constraints) → (min_w, max_w, intrinsic_h)

用于嵌套 Grid 尺寸计算,结果会被缓存。


TextNode

文本节点,支持自动换行、富文本标记和垂直排版。

from latticesvg import TextNode

# 纯文本
text = TextNode("Hello, World!", style={"font-size": 24, "color": "#333"})

# HTML 富文本
rich = TextNode("<b>Bold</b> and <i>italic</i>", markup="html")

# Markdown 富文本
md = TextNode("**Bold** and *italic*", markup="markdown")

构造参数

参数 类型 默认值 说明
text str 必填 文本内容
style dict \| None None 样式属性
parent Node \| None None 父节点
markup str "none" 标记模式:"none", "html", "markdown"

属性

属性 类型 说明
text str 原始文本内容
markup str 标记模式
lines list[Line] 布局后的行列表(纯文本模式)

关键样式属性

文本节点响应以下样式属性(详见 CSS 属性参考):

  • 字体: font-family, font-size, font-weight, font-style
  • 排版: text-align, line-height, letter-spacing, word-spacing
  • 换行: white-space, overflow-wrap, hyphens, lang
  • 溢出: overflow, text-overflow
  • 垂直: writing-mode, text-orientation, text-combine-upright

ImageNode

图片节点,支持多种图片来源和 object-fit 缩放模式。

from latticesvg import ImageNode

# 文件路径
img = ImageNode("photo.png", style={"width": 200, "height": 150})

# URL
img = ImageNode("https://example.com/image.png")

# bytes
img = ImageNode(raw_bytes, object_fit="contain")

# PIL Image
from PIL import Image
img = ImageNode(Image.open("photo.png"), object_fit="cover")

构造参数

参数 类型 默认值 说明
src str \| bytes \| PIL.Image 必填 图片源(路径/URL/bytes/PIL)
style dict \| None None 样式属性
parent Node \| None None 父节点
object_fit str \| None None 缩放模式

object-fit 模式

说明
"fill" 拉伸填满(默认,可能变形)
"contain" 等比缩放,完全包含在区域内
"cover" 等比缩放,完全覆盖区域
"none" 原始尺寸,不缩放

属性

属性 类型 说明
intrinsic_width float 图片固有宽度(只读,惰性加载)
intrinsic_height float 图片固有高度(只读,惰性加载)

SVGNode

嵌入外部 SVG 内容的节点。

from latticesvg import SVGNode

# SVG 字符串
svg = SVGNode('<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="40"/></svg>')

# 文件
svg = SVGNode("icon.svg", is_file=True)

# URL
svg = SVGNode("https://example.com/icon.svg")

构造参数

参数 类型 默认值 说明
svg str 必填 SVG 内容(字符串/路径/URL)
style dict \| None None 样式属性(仅限关键字参数)
parent Node \| None None 父节点(仅限关键字参数)
is_file bool False 是否视为文件路径(仅限关键字参数)

SVG 的固有尺寸从 viewBoxwidth/height 属性解析。


MplNode

Matplotlib 图形嵌入节点。

import matplotlib.pyplot as plt
from latticesvg import MplNode

fig, ax = plt.subplots(figsize=(4, 3))
ax.plot([1, 2, 3], [1, 4, 9])

node = MplNode(fig, style={"padding": 10})

构造参数

参数 类型 默认值 说明
figure matplotlib.figure.Figure 必填 Matplotlib 图形对象
style dict \| None None 样式属性
parent Node \| None None 父节点

注意事项

  • 所有 Matplotlib 自定义应在创建节点之前完成
  • 布局时会根据分配的空间自动调整 figure 大小
  • SVG 坐标统一使用 72 DPI

MathNode

LaTeX 公式渲染节点,基于 QuickJax(MathJax v4)。

from latticesvg import MathNode

# display 模式(独立公式)
formula = MathNode(r"E = mc^2", style={"font-size": 24})

# inline 模式
inline = MathNode(r"\alpha + \beta", display=False)

构造参数

参数 类型 默认值 说明
latex str 必填 LaTeX 数学表达式
style dict \| None None 样式属性(仅限关键字参数)
backend str \| None None 后端名称,None 使用默认(仅限关键字参数)
display bool True 是否为 display 模式(仅限关键字参数)
parent Node \| None None 父节点(仅限关键字参数)

属性

属性 类型 说明
latex str LaTeX 源码
display bool display/inline 模式
scale_x float 布局后的水平缩放因子
scale_y float 布局后的垂直缩放因子

自动生成的 API 文档

nodes

Node

Node(style: Optional[Dict[str, Any]] = None, parent: Optional['Node'] = None)

Abstract base for all layoutable elements.

Subclasses must implement :meth:measure and :meth:layout.

Source code in src/latticesvg/nodes/base.py
def __init__(self, style: Optional[Dict[str, Any]] = None, parent: Optional["Node"] = None) -> None:
    parent_computed = parent.style if parent else None
    self.style: ComputedStyle = ComputedStyle(style, parent_style=parent_computed)
    self.parent: Optional[Node] = parent
    self.children: List[Node] = []

    # Populated after layout
    self.border_box: Rect = Rect()
    self.padding_box: Rect = Rect()
    self.content_box: Rect = Rect()

    # Grid placement (set via ``add()``)
    self.placement: PlacementHint = PlacementHint()
add
add(child: 'Node', *, row: Optional[int] = None, col: Optional[int] = None, row_span: int = 1, col_span: int = 1, area: Optional[str] = None) -> 'Node'

Append child to this node and optionally set grid placement.

row and col use 1-based line numbers consistent with CSS Grid. area places the child in a named grid area defined by grid-template-areas on the container.

Source code in src/latticesvg/nodes/base.py
def add(self, child: "Node", *, row: Optional[int] = None, col: Optional[int] = None,
        row_span: int = 1, col_span: int = 1, area: Optional[str] = None) -> "Node":
    """Append *child* to this node and optionally set grid placement.

    ``row`` and ``col`` use 1-based line numbers consistent with CSS Grid.
    ``area`` places the child in a named grid area defined by
    ``grid-template-areas`` on the container.
    """
    child.parent = self
    # Re-compute style with parent inheritance
    if isinstance(child.style, ComputedStyle):
        child.style._rebind_parent(self.style)
    else:
        child.style = ComputedStyle(None, parent_style=self.style)

    child.placement = PlacementHint(
        row_start=row,
        row_span=row_span,
        col_start=col,
        col_span=col_span,
        area=area,
    )

    # Also honour grid-row / grid-column / grid-area from the child's own style
    gr = child.style.get("grid-row")
    gc = child.style.get("grid-column")
    ga = child.style.get("grid-area")
    if gr is not None and isinstance(gr, tuple):
        child.placement.row_start = gr[0]
        child.placement.row_span = gr[1]
    if gc is not None and isinstance(gc, tuple):
        child.placement.col_start = gc[0]
        child.placement.col_span = gc[1]
    if ga is not None and isinstance(ga, str) and ga != "auto":
        child.placement.area = ga

    self.children.append(child)
    return child
measure
measure(constraints: LayoutConstraints) -> Tuple[float, float, float]

Return (min_content_width, max_content_width, intrinsic_height).

Called by the grid solver to determine how much space this node needs. Default implementation returns zero sizes.

Source code in src/latticesvg/nodes/base.py
def measure(self, constraints: LayoutConstraints) -> Tuple[float, float, float]:
    """Return ``(min_content_width, max_content_width, intrinsic_height)``.

    Called by the grid solver to determine how much space this node needs.
    Default implementation returns zero sizes.
    """
    return (0.0, 0.0, 0.0)
layout
layout(constraints: LayoutConstraints) -> None

Compute border_box, padding_box, and content_box.

Must be implemented by subclasses.

Source code in src/latticesvg/nodes/base.py
def layout(self, constraints: LayoutConstraints) -> None:
    """Compute ``border_box``, ``padding_box``, and ``content_box``.

    Must be implemented by subclasses.
    """
    pass

Rect dataclass

Rect(x: float = 0.0, y: float = 0.0, width: float = 0.0, height: float = 0.0)

An axis-aligned rectangle (top-left origin, y-axis points down).

LayoutConstraints dataclass

LayoutConstraints(available_width: Optional[float] = None, available_height: Optional[float] = None)

Constraints passed from parent to child during layout.

GridContainer

GridContainer(style: Optional[Dict[str, Any]] = None, parent: Optional[Node] = None)

Bases: Node

A container node that arranges its children using CSS Grid layout.

Delegates the heavy-lifting (track sizing, placement, alignment) to :class:~latticesvg.layout.grid_solver.GridSolver.

Source code in src/latticesvg/nodes/grid.py
def __init__(
    self,
    style: Optional[Dict[str, Any]] = None,
    parent: Optional[Node] = None,
) -> None:
    if style is None:
        style = {}
    # Ensure display is grid
    style.setdefault("display", "grid")
    super().__init__(style=style, parent=parent)
layout
layout(constraints: Optional[LayoutConstraints] = None, available_width: Optional[float] = None, available_height: Optional[float] = None) -> None

Run the full layout pass for this container and all descendants.

Can be called directly as grid.layout(available_width=800).

Source code in src/latticesvg/nodes/grid.py
def layout(self, constraints: Optional[LayoutConstraints] = None,
           available_width: Optional[float] = None,
           available_height: Optional[float] = None) -> None:
    """Run the full layout pass for this container and all descendants.

    Can be called directly as ``grid.layout(available_width=800)``.
    """
    if constraints is None:
        # Derive from explicit width or arguments
        aw = available_width
        if aw is None:
            explicit_w = self._resolve_width(LayoutConstraints())
            aw = explicit_w if explicit_w is not None else 800.0
        constraints = LayoutConstraints(
            available_width=aw,
            available_height=available_height,
        )

    self._layout_grid(constraints)

TextNode

TextNode(text: str, style: Optional[Dict[str, Any]] = None, parent: Optional[Node] = None, markup: str = 'none')

Bases: Node

A node that displays text content.

The text is measured using FreeType (or Pillow fallback) and automatically wrapped according to the white-space property.

When markup is "html" or "markdown", inline tags/syntax are parsed to produce styled spans (bold, italic, colour, …).

Source code in src/latticesvg/nodes/text.py
def __init__(
    self,
    text: str,
    style: Optional[Dict[str, Any]] = None,
    parent: Optional[Node] = None,
    markup: str = "none",
) -> None:
    super().__init__(style=style, parent=parent)
    self.text: str = text
    self.markup: str = markup
    self.lines: List[Line] = []  # populated after layout (plain mode)

    # Rich-text fields (populated when markup != "none")
    self._spans: Optional[list] = None
    self._rich_lines: Optional[List[RichLine]] = None

    if markup in ("html", "markdown"):
        from ..markup.parser import parse_markup
        self._spans = parse_markup(text, markup)
measure
measure(constraints: LayoutConstraints) -> Tuple[float, float, float]

Return (min_content_width, max_content_width, intrinsic_height).

For vertical/sideways writing modes the axes are swapped internally so that the grid solver always receives values in the physical (horizontal/vertical) coordinate system.

The result is cached because it depends only on text content and style properties, not on constraints.

Source code in src/latticesvg/nodes/text.py
def measure(self, constraints: LayoutConstraints) -> Tuple[float, float, float]:
    """Return ``(min_content_width, max_content_width, intrinsic_height)``.

    For vertical/sideways writing modes the axes are swapped
    internally so that the grid solver always receives values in
    the physical (horizontal/vertical) coordinate system.

    The result is cached because it depends only on text content
    and style properties, not on *constraints*.
    """
    cached = getattr(self, '_measure_cache', None)
    if cached is not None:
        return cached
    font_chain = self._font_chain()
    if not font_chain:
        return (0.0, 0.0, 0.0)

    size = self._font_size_int()
    ws = self.style.get("white-space") or "normal"
    ow = self.style.get("overflow-wrap") or "normal"
    fm = FontManager.instance()
    ls_val, ws_val = self._spacing_values()
    hyph = self._hyphens_value()
    lang_v = self._lang_value()
    wm = self._writing_mode()

    # Add padding + border
    ph = self.style.padding_horizontal + self.style.border_horizontal
    pv = self.style.padding_vertical + self.style.border_vertical

    # -- sideways-rl / sideways-lr --
    # Shape horizontally, then swap width ↔ height.
    if wm in ("sideways-rl", "sideways-lr"):
        if self._spans is not None:
            math_be = self._math_backend()
            min_w = get_min_content_width_rich(self._spans, font_chain, size, ws, fm=fm, math_backend=math_be,
                                               letter_spacing=ls_val, word_spacing=ws_val,
                                               hyphens=hyph, lang=lang_v)
            max_w = get_max_content_width_rich(self._spans, font_chain, size, ws, fm=fm, math_backend=math_be,
                                               letter_spacing=ls_val, word_spacing=ws_val)
            lines = break_lines_rich(self._spans, max_w + 1, font_chain, size, ws, ow, fm=fm, math_backend=math_be,
                                     letter_spacing=ls_val, word_spacing=ws_val,
                                     hyphens=hyph, lang=lang_v)
            lh = self._line_height()
            text_w, text_h = compute_rich_block_size(lines, lh, float(size))
        else:
            min_w = get_min_content_width(self.text, font_chain, size, ws, fm=fm,
                                          letter_spacing=ls_val, word_spacing=ws_val,
                                          hyphens=hyph, lang=lang_v)
            max_w = get_max_content_width(self.text, font_chain, size, ws, fm=fm,
                                          letter_spacing=ls_val, word_spacing=ws_val)
            lines = break_lines(self.text, max_w + 1, font_chain, size, ws, ow, fm=fm,
                                letter_spacing=ls_val, word_spacing=ws_val,
                                hyphens=hyph, lang=lang_v)
            lh = self._line_height()
            text_w, text_h = compute_text_block_size(lines, lh, float(size))
        # Swap axes: horizontal text_h becomes physical width,
        # horizontal text_w becomes physical height.
        result = (text_h + ph, text_h + ph, max_w + pv)
        self._measure_cache = result
        return result

    # -- vertical-rl / vertical-lr --
    if wm in ("vertical-rl", "vertical-lr"):
        orient = self._text_orientation()
        min_h = get_min_content_height(self.text, font_chain, size, ws, fm=fm,
                                       orientation=orient,
                                       letter_spacing=ls_val,
                                       word_spacing=ws_val)
        max_h = get_max_content_height(self.text, font_chain, size, ws, fm=fm,
                                       orientation=orient,
                                       letter_spacing=ls_val,
                                       word_spacing=ws_val)
        # Intrinsic: one column at max height
        cols = break_columns(self.text, max_h + 1, font_chain, size, ws, ow, fm=fm,
                             orientation=orient, letter_spacing=ls_val,
                             word_spacing=ws_val)
        lh = self._line_height()
        block_w, block_h = compute_vertical_block_size(cols, lh, float(size))
        # In physical coords: width = block_w, height = block_h (= max_h)
        result = (block_w + ph, block_w + ph, max_h + pv)
        self._measure_cache = result
        return result

    # -- horizontal-tb (default) --
    if self._spans is not None:
        math_be = self._math_backend()
        min_w = get_min_content_width_rich(self._spans, font_chain, size, ws, fm=fm, math_backend=math_be,
                                           letter_spacing=ls_val, word_spacing=ws_val,
                                           hyphens=hyph, lang=lang_v)
        max_w = get_max_content_width_rich(self._spans, font_chain, size, ws, fm=fm, math_backend=math_be,
                                           letter_spacing=ls_val, word_spacing=ws_val)
        lines = break_lines_rich(self._spans, max_w + 1, font_chain, size, ws, ow, fm=fm, math_backend=math_be,
                                 letter_spacing=ls_val, word_spacing=ws_val,
                                 hyphens=hyph, lang=lang_v)
        lh = self._line_height()
        _, h = compute_rich_block_size(lines, lh, float(size))
    else:
        min_w = get_min_content_width(self.text, font_chain, size, ws, fm=fm,
                                      letter_spacing=ls_val, word_spacing=ws_val,
                                      hyphens=hyph, lang=lang_v)
        max_w = get_max_content_width(self.text, font_chain, size, ws, fm=fm,
                                      letter_spacing=ls_val, word_spacing=ws_val)
        lines = break_lines(self.text, max_w + 1, font_chain, size, ws, ow, fm=fm,
                            letter_spacing=ls_val, word_spacing=ws_val,
                            hyphens=hyph, lang=lang_v)
        lh = self._line_height()
        _, h = compute_text_block_size(lines, lh, float(size))

    result = (min_w + ph, max_w + ph, h + pv)
    self._measure_cache = result
    return result

ImageNode

ImageNode(src: Union[str, bytes, Any], style: Optional[Dict[str, Any]] = None, parent: Optional[Node] = None, object_fit: Optional[str] = None)

Bases: Node

Node that displays a raster image (PNG, JPEG, etc.).

The image's intrinsic size is read lazily via Pillow. The object-fit property controls how the image is scaled within the allocated space.

PARAMETER DESCRIPTION
src

Image source: - File path (str) - URL starting with 'http://' or 'https://' (str) - Raw image bytes (bytes) - PIL Image object

TYPE: str, bytes, or PIL.Image

style

Style properties

TYPE: dict DEFAULT: None

parent

Parent node

TYPE: Node DEFAULT: None

object_fit

Object-fit mode ('fill', 'contain', 'cover', 'none')

TYPE: str DEFAULT: None

Source code in src/latticesvg/nodes/image.py
def __init__(
    self,
    src: Union[str, bytes, Any],
    style: Optional[Dict[str, Any]] = None,
    parent: Optional[Node] = None,
    object_fit: Optional[str] = None,
) -> None:
    super().__init__(style=style, parent=parent)
    self.src: Union[str, bytes, Any] = src
    if object_fit:
        self.style.set("object-fit", object_fit)
    self._intrinsic_width: Optional[float] = None
    self._intrinsic_height: Optional[float] = None
    self._base64_data: Optional[str] = None
    self._image_bytes: Optional[bytes] = None
    self._pil_image: Optional[Any] = None
get_base64
get_base64() -> str

Return the image as a base64-encoded data URI.

Source code in src/latticesvg/nodes/image.py
def get_base64(self) -> str:
    """Return the image as a base64-encoded data URI."""
    if self._base64_data is not None:
        return self._base64_data

    # Determine MIME type
    mime = "image/png"
    if isinstance(self.src, str):
        if self.src.startswith('http://') or self.src.startswith('https://'):
            # Guess from URL
            lower = self.src.lower()
            if '.jpg' in lower or '.jpeg' in lower:
                mime = "image/jpeg"
            elif '.gif' in lower:
                mime = "image/gif"
            elif '.webp' in lower:
                mime = "image/webp"
            elif '.svg' in lower:
                mime = "image/svg+xml"
        else:
            # File path
            ext = os.path.splitext(self.src)[1].lower()
            mime = {
                ".png": "image/png",
                ".jpg": "image/jpeg",
                ".jpeg": "image/jpeg",
                ".gif": "image/gif",
                ".webp": "image/webp",
                ".svg": "image/svg+xml",
            }.get(ext, "image/png")

    # Get bytes
    img_bytes: bytes
    if isinstance(self.src, bytes):
        img_bytes = self.src
    elif hasattr(self.src, 'size') and hasattr(self.src, 'mode'):
        # PIL Image — save to bytes
        from PIL import Image  # type: ignore
        buf = io.BytesIO()
        # Determine format from mime
        fmt = "PNG"
        if "jpeg" in mime:
            fmt = "JPEG"
        elif "gif" in mime:
            fmt = "GIF"
        elif "webp" in mime:
            fmt = "WEBP"
        self.src.save(buf, format=fmt)
        img_bytes = buf.getvalue()
    elif isinstance(self.src, str) and (self.src.startswith('http://') or self.src.startswith('https://')):
        # URL — use cached bytes if available
        if self._image_bytes is None:
            import urllib.request
            with urllib.request.urlopen(self.src) as response:
                self._image_bytes = response.read()
        img_bytes = self._image_bytes
    else:
        # File path
        with open(self.src, "rb") as f:
            img_bytes = f.read()

    data = base64.b64encode(img_bytes).decode("ascii")
    self._base64_data = f"data:{mime};base64,{data}"
    return self._base64_data

MplNode

MplNode(figure: Any, style: Optional[Dict[str, Any]] = None, parent: Optional[Node] = None, auto_mpl_font: bool = True, tight_layout: bool = True)

Bases: Node

Node that embeds a Matplotlib figure as an SVG fragment.

The figure is rendered to an in-memory SVG buffer during the render phase, so all Matplotlib customisation should be done before creating this node.

PARAMETER DESCRIPTION
figure

A matplotlib.figure.Figure instance.

TYPE: Any

style

Optional CSS-like style dictionary.

TYPE: Optional[Dict[str, Any]] DEFAULT: None

parent

Optional parent node.

TYPE: Optional[Node] DEFAULT: None

auto_mpl_font

When True (default), the node reads the inherited font-family CSS property and configures matplotlib's font settings via rc_context so that text rendered inside the figure uses the same font family as surrounding :class:TextNode elements.

TYPE: bool DEFAULT: True

tight_layout

When True (default), figure.tight_layout() is called inside the font rc_context before exporting, so that text metrics match the final font. Set to False if you manage layout yourself or use constrained_layout.

TYPE: bool DEFAULT: True

Source code in src/latticesvg/nodes/mpl.py
def __init__(
    self,
    figure: Any,  # matplotlib.figure.Figure
    style: Optional[Dict[str, Any]] = None,
    parent: Optional[Node] = None,
    auto_mpl_font: bool = True,
    tight_layout: bool = True,
) -> None:
    super().__init__(style=style, parent=parent)
    self.figure = figure
    self._auto_mpl_font = auto_mpl_font
    self._tight_layout = tight_layout
    self._svg_cache: Optional[str] = None
get_svg_fragment
get_svg_fragment() -> str

Export the figure to an SVG string (cached).

When auto_mpl_font is enabled the inherited font-family CSS property is translated into matplotlib rcParams so that text inside the figure uses matching fonts. svg.fonttype is always set to "path" so that glyphs are converted to vector paths for cross-platform consistency.

Source code in src/latticesvg/nodes/mpl.py
def get_svg_fragment(self) -> str:
    """Export the figure to an SVG string (cached).

    When *auto_mpl_font* is enabled the inherited ``font-family``
    CSS property is translated into matplotlib ``rcParams`` so that
    text inside the figure uses matching fonts.  ``svg.fonttype`` is
    always set to ``"path"`` so that glyphs are converted to vector
    paths for cross-platform consistency.
    """
    if self._svg_cache is None:
        import matplotlib as mpl

        rc_overrides: Dict[str, Any] = {"svg.fonttype": "path"}
        if self._auto_mpl_font:
            font_rc, font_paths = self._resolve_mpl_font_rc()
            self._register_fonts_with_mpl(font_paths)
            rc_overrides.update(font_rc)

        buf = io.BytesIO()
        with mpl.rc_context(rc_overrides):
            if self._tight_layout:
                try:
                    self.figure.tight_layout()
                except Exception:
                    pass
            self.figure.savefig(buf, format="svg", transparent=True)
        buf.seek(0)
        svg = buf.read().decode("utf-8")
        # Strip the XML declaration and outer <svg> tags for embedding
        self._svg_cache = self._strip_svg_wrapper(svg)
    return self._svg_cache

SVGNode

SVGNode(svg: str, *, style: Optional[Dict[str, Any]] = None, parent: Optional[Node] = None, is_file: bool = False)

Bases: Node

Node that embeds raw SVG content (from a string or file).

The SVG's viewBox or width/height attributes are used to determine its intrinsic size. During rendering the content is scaled to fit the allocated space.

PARAMETER DESCRIPTION
svg

SVG source: - File path (when is_file=True) - URL starting with 'http://' or 'https://' - Raw SVG string

TYPE: str

style

Style properties

TYPE: dict DEFAULT: None

parent

Parent node

TYPE: Node DEFAULT: None

is_file

If True, treat svg as a file path. Default False.

TYPE: bool DEFAULT: False

Source code in src/latticesvg/nodes/svg.py
def __init__(
    self,
    svg: str,
    *,
    style: Optional[Dict[str, Any]] = None,
    parent: Optional[Node] = None,
    is_file: bool = False,
) -> None:
    super().__init__(style=style, parent=parent)

    # Load SVG content from various sources
    if is_file:
        with open(svg, "r", encoding="utf-8") as f:
            self.svg_content: str = f.read()
    elif svg.startswith('http://') or svg.startswith('https://'):
        # Load from URL
        import urllib.request
        with urllib.request.urlopen(svg) as response:
            self.svg_content = response.read().decode('utf-8')
    else:
        self.svg_content = svg

    self._intrinsic: Optional[Tuple[float, float]] = None
    self._vb_min_x: float = 0.0
    self._vb_min_y: float = 0.0
get_inner_svg
get_inner_svg() -> str

Return SVG content with the outer <svg> wrapper stripped.

This is used for embedding inside another SVG document. Presentation attributes (fill, stroke, etc.) from the outer <svg> element are preserved by wrapping inner content in a <g> that carries those attributes forward.

Source code in src/latticesvg/nodes/svg.py
def get_inner_svg(self) -> str:
    """Return SVG content with the outer ``<svg>`` wrapper stripped.

    This is used for embedding inside another SVG document.
    Presentation attributes (fill, stroke, etc.) from the outer
    ``<svg>`` element are preserved by wrapping inner content in
    a ``<g>`` that carries those attributes forward.
    """
    content = self.svg_content
    # Remove XML declaration
    content = re.sub(r'<\?xml[^?]*\?>\s*', '', content)
    # Remove DOCTYPE
    content = re.sub(r'<!DOCTYPE[^>]*>\s*', '', content)
    # Remove HTML comments (license headers etc.)
    content = re.sub(r'<!--.*?-->', '', content, flags=re.DOTALL)
    # Extract content inside <svg ...>...</svg>
    m = re.search(r'<svg([^>]*)>(.*)</svg>', content, re.DOTALL)
    if m:
        svg_attrs = m.group(1)
        inner = m.group(2).strip()
        # Extract presentation attributes from <svg> to carry forward
        pres_attrs = []
        for attr in ('fill', 'stroke', 'stroke-width', 'stroke-linecap',
                     'stroke-linejoin', 'opacity', 'color'):
            match = re.search(rf'\b{attr}\s*=\s*"([^"]*)"', svg_attrs)
            if match:
                val = match.group(1)
                # Replace currentColor with a usable default
                if val == 'currentColor':
                    val = '#000000'
                pres_attrs.append(f'{attr}="{val}"')
        if pres_attrs:
            attrs_str = ' '.join(pres_attrs)
            return f'<g {attrs_str}>{inner}</g>'
        return inner
    return content.strip()

MathNode

MathNode(latex: str, *, style: Optional[Dict[str, Any]] = None, backend: Optional[str] = None, display: bool = True, parent: Optional[Node] = None)

Bases: Node

Node that renders a LaTeX math expression to SVG.

By default the QuickJax backend (in-process MathJax v4) is used. A different backend can be selected per-node or globally via :func:latticesvg.math.set_default_backend.

Usage::

formula = MathNode(r"E = mc^2", style={"font-size": "20px"})
Source code in src/latticesvg/nodes/math.py
def __init__(
    self,
    latex: str,
    *,
    style: Optional[Dict[str, Any]] = None,
    backend: Optional[str] = None,
    display: bool = True,
    parent: Optional[Node] = None,
) -> None:
    super().__init__(style=style or {}, parent=parent)
    self.latex: str = latex
    self.display: bool = display
    self._backend_name: Optional[str] = backend
    self._svg_cache: Optional[object] = None  # SVGFragment
    self.scale_x: float = 1.0
    self.scale_y: float = 1.0
get_svg_fragment
get_svg_fragment() -> str

Return the rendered SVG content for embedding.

Source code in src/latticesvg/nodes/math.py
def get_svg_fragment(self) -> str:
    """Return the rendered SVG content for embedding."""
    frag = self._get_fragment()
    return frag.svg_content