Skip to content

Data Model

vexy-lines-py represents a parsed .lines document as a tree of typed dataclasses. All types are importable from vexy_lines directly.

Type hierarchy

Text Only
LinesDocument
├── caption: str                    project name
├── version: str                    app version that wrote the file
├── dpi: int                        root-level DPI (from <Project dpi="...">)
├── props: DocumentProps            canvas dimensions and stroke limits
├── groups: list[GroupInfo | LayerInfo]   top-level layer tree
├── source_image_data: bytes | None first document-scope JPEG (convenience)
├── source_images: list[SourceImageInfo]  all decoded <SourcePict> payloads
└── preview_image_data: bytes | None decoded PNG preview thumbnail

DocumentProps
├── width_mm: float
├── height_mm: float
├── dpi: int
├── thickness_min: float
├── thickness_max: float
├── interval_min: float
└── interval_max: float

GroupInfo                           maps to <LrSection>
├── caption: str
├── object_id: int | None
├── expanded: bool
└── children: list[GroupInfo | LayerInfo]   recursive

LayerInfo                           maps to <FreeMesh>
├── caption: str
├── object_id: int | None
├── visible: bool
├── mask: MaskInfo | None
├── fills: list[FillNode]
└── grid_edges: list[dict[str, str]]   raw row_grid_edge / col_grid_edge attrs

FillNode                            one fill algorithm instance
├── xml_tag: str                    original XML element name
├── caption: str
├── object_id: int | None
├── params: FillParams
└── image_filters: list[ImageFilterEntry]

FillParams
├── fill_type: str                  e.g. "linear", "circular", "handmade"
├── color: str                      normalised hex: #RRGGBB or #RRGGBBAA
├── interval: float                 line spacing in mm
├── angle: float                    stroke angle in degrees
├── thickness: float                XML thick_gap
├── thickness_min: float            XML base_width
├── smoothness: float
├── uplimit: float                  upper brightness threshold (0–255)
├── downlimit: float                lower brightness threshold (0–255)
├── multiplier: float               width multiplier
├── base_width: float               same source as thickness_min
├── dispersion: float               random perpendicular offset
├── shear: float                    shear distortion angle in degrees
└── raw: dict[str, str]             all XML attributes (unmodified)

ImageFilterEntry                    one entry in a fill's filter chain
├── type_id: int                    upstream numeric type (0–9)
├── name: str                       MCP-compatible name e.g. "brightness"
├── params: dict[str, bool|int|float|str]   typed parameter values
└── raw: dict[str, str]             original XML attributes

MaskInfo                            from <MaskData>
├── mask_type: int                  0=none, 1=raster
├── invert: bool
└── tolerance: float

SourceImageInfo                     one decoded <SourcePict> payload
├── index: int                      1-based position in source_images list
├── data: bytes                     decoded JPEG bytes
├── scope: str                      "document" or "group"
├── caption: str
├── object_id: int | None
├── owner_caption: str
├── owner_path: str                 slash-separated group path
├── width: int | None               pixels
└── height: int | None              pixels

Walking the layer tree

LinesDocument.groups is a flat list of top-level nodes. Each node is either a GroupInfo (with its own children list) or a LayerInfo (with a fills list). Groups nest recursively.

from vexy_lines import parse, GroupInfo, LayerInfo

doc = parse("artwork.lines")

def walk(nodes, depth=0):
    indent = "  " * depth
    for node in nodes:
        if isinstance(node, GroupInfo):
            print(f"{indent}[group] {node.caption!r}")
            walk(node.children, depth + 1)
        elif isinstance(node, LayerInfo):
            vis = "" if node.visible else " (hidden)"
            print(f"{indent}[layer] {node.caption!r}{vis}")
            for fill in node.fills:
                p = fill.params
                print(f"{indent}  [{p.fill_type}] {p.color}")

walk(doc.groups)

Fill type names

The fill_type field on FillParams is the MCP-compatible algorithm name:

fill_type XML element
"linear" LinearStrokesTmpl
"wave" SigmoidStrokesTmpl
"circular" CircleStrokesTmpl
"radial" RadialStrokesTmpl
"spiral" SpiralStrokesTmpl
"scribble" ScribbleStrokesTmpl
"halftone" HalftoneStrokesTmpl
"handmade" FreeCurveStrokesTmpl (default)
"trace" FreeCurveStrokesTmpl with type_conv="9", or TracedAreaTmpl
"fractals" PeanoStrokesTmpl
"source_strokes" SourceStrokes

The complete mapping is in vexy_lines.types.FILL_TAG_MAP.

Image filter chain

Each fill can carry an ordered list of ImageFilterEntry objects that are applied before stroke rendering. Filter IDs 0–9 map to named operations:

from vexy_lines.types import IMAGE_FILTER_TYPE_MAP
# {0: "brightness", 1: "contrast", 2: "blur", 3: "sharpen",
#  4: "levels", 5: "shadows_highlights", 6: "invert",
#  7: "remove_background", 8: "color", 9: "gradient"}

Parameter values are typed: bool for "inverted", int for "left" / "right" / "direction", str for "color", float for everything else.

for fill in layer.fills:
    for f in fill.image_filters:
        print(f.name, f.params)
        # "brightness" {"value": -12.5}
        # "levels"     {"left": 12, "right": 224}

Source images

A .lines file can embed more than one source image: one at document level and one per group. LinesDocument.source_images lists them all:

for img in doc.source_images:
    print(img.index, img.scope, img.owner_path,
          img.width, img.height, len(img.data), "bytes")
    # 1  document  ""          1025 1025  48392 bytes
    # 2  group     "Background"  512  512  12000 bytes

LinesDocument.source_image_data is a convenience shortcut to the first document-scope image's bytes (or None when absent).

Accessing raw attributes

Every FillParams.raw dict and every LayerInfo.grid_edges entry holds the unmodified XML attribute strings for attributes the parser does not promote to named fields. This is the escape hatch for algorithm-specific or undocumented parameters:

fill.params.raw.get("vert_disp")        # "0.5"
fill.params.raw.get("crossed_dir")      # "1"
layer.grid_edges[0]["allocation_t"]     # "0.5"