API Reference¶
Functions¶
parse(path) -> LinesDocument¶
Parse a .lines file and return a fully populated LinesDocument.
from vexy_lines import parse
doc = parse("artwork.lines")
print(doc.caption, doc.dpi)
Args:
| Parameter | Type | Description |
|---|---|---|
path |
str \| Path |
Path to the .lines file |
Returns: LinesDocument with the full layer tree, document properties, and decoded images.
Raises:
FileNotFoundError: path does not existxml.etree.ElementTree.ParseError: file is not valid XML
Type hint:
def parse(path: str | Path) -> LinesDocument: ...
parse_string(xml) -> LinesDocument¶
Parse a .lines XML string and return a LinesDocument. Use it when you already have the XML content in memory (from a network response, database, or test fixture) rather than a file path.
from vexy_lines import parse_string
xml = open("artwork.lines").read()
doc = parse_string(xml)
print(doc.caption, doc.dpi)
Args:
| Parameter | Type | Description |
|---|---|---|
xml |
str |
Raw XML string from a .lines file |
Returns: LinesDocument with the full layer tree, document properties, and decoded images.
Raises:
xml.etree.ElementTree.ParseError: string is not valid XML
Type hint:
def parse_string(xml: str) -> LinesDocument: ...
extract_source_image(path, output) -> Path¶
Parse a .lines file and save the embedded JPEG source image.
from vexy_lines import extract_source_image
extract_source_image("artwork.lines", "source.jpg")
Args:
| Parameter | Type | Description |
|---|---|---|
path |
str \| Path |
Path to the .lines file |
output |
str \| Path |
Destination path for the JPEG |
Returns: Resolved output Path.
Raises:
FileNotFoundError:.linesfile does not existValueError: no source image embedded in the file
extract_preview_image(path, output) -> Path¶
Parse a .lines file and save the embedded PNG preview image.
from vexy_lines import extract_preview_image
extract_preview_image("artwork.lines", "preview.png")
Args:
| Parameter | Type | Description |
|---|---|---|
path |
str \| Path |
Path to the .lines file |
output |
str \| Path |
Destination path for the PNG |
Returns: Resolved output Path.
Raises:
FileNotFoundError:.linesfile does not existValueError: no preview image embedded in the file
replace_source_image(lines_path, new_image_path, output_path, *, target_size) -> Path¶
Replace the embedded source image in a .lines file, writing the result to a new file. All fill parameters, layers, groups, masks, and document settings are preserved. The new image is converted to JPEG if needed (the .lines format requires JPEG for source images).
Requires the pillow package (installed as a dependency).
from vexy_lines import replace_source_image
replace_source_image(
"template.lines",
"new_photo.png",
"output.lines",
target_size=(1025, 1025),
)
Args:
| Parameter | Type | Description |
|---|---|---|
lines_path |
str \| Path |
Path to the source .lines file (template) |
new_image_path |
str \| Path |
Path to the replacement image (JPEG, PNG, etc.) |
output_path |
str \| Path |
Where to write the modified .lines file |
target_size |
tuple[int, int] \| None |
Optional (width, height) to resize the new image to fit |
When target_size is provided and the new image dimensions differ, the image is scaled to fit within the target dimensions (maintaining aspect ratio) and centred on a white canvas. White padding is correct for Vexy Lines: fill algorithms treat white as "no strokes".
Returns: Resolved output Path.
Raises:
FileNotFoundError: either input file does not existValueError: the.linesfile has no<SourcePict>element
Type hint:
def replace_source_image(
lines_path: str | Path,
new_image_path: str | Path,
output_path: str | Path,
*,
target_size: tuple[int, int] | None = None,
) -> Path: ...
rename_objects(lines_path, output_path, renames) -> int¶
Rename groups, layers, and/or fills in a .lines file by object ID. For every element carrying an object_id attribute whose value is a key in renames, sets its caption attribute to the new name. Everything else (fill parameters, masks, image data, document settings) is preserved byte-for-byte (the file is copied first, then only the matched caption attributes are rewritten).
Object IDs come from the parsed tree: GroupInfo.object_id, LayerInfo.object_id, and FillNode.object_id. href reference elements never carry an object_id of their own, so only the canonical definition of each object is touched. See the official Layers Panel help for how captions appear in the app.
from vexy_lines import rename_objects
# Rename layer 1 and fill 10 in place; returns the count actually changed.
n = rename_objects("artwork.lines", "artwork.lines", {1: "Background", 10: "Sky lines"})
print(f"Renamed {n} object(s)")
Args:
| Parameter | Type | Description |
|---|---|---|
lines_path |
str \| Path |
Path to the source .lines file |
output_path |
str \| Path |
Where to write the renamed file; may equal lines_path to edit in place |
renames |
Mapping[int, str] |
Mapping of object_id to the new caption string |
Returns: The number of elements actually renamed. A caption already equal to its target is not counted. An empty renames mapping copies the file unchanged and returns 0.
Raises:
FileNotFoundError:lines_pathdoes not exist
Type hint:
def rename_objects(
lines_path: str | Path,
output_path: str | Path,
renames: Mapping[int, str],
) -> int: ...
set_visibility(lines_path, output_path, visible) -> int¶
Set the visible attribute on objects in a .lines file by object ID. For every element whose object_id is a key in visible, sets the visible attribute to "1" (True) or "0" (False). Everything else is preserved byte-for-byte, with the same preservation, in-place, empty-mapping, and FileNotFoundError semantics as rename_objects.
Vexy Lines stores per-object visibility as a visible="0" / visible="1" XML attribute (absent means visible). Toggling visibility live over the app's MCP API does not change what the app exports; baking the attribute into a copy of the file and re-opening it does. This is how the AI-rename feature renders one fill in isolation: write a copy with every other fill set to visible="0", open that file, and export. See Document Structure for how groups, layers, and fills nest.
from vexy_lines import set_visibility
# Write a copy with only fill 10 visible (everything else hidden).
n = set_visibility("artwork.lines", "fill_10_only.lines", {10: True, 11: False, 12: False})
print(f"Changed visibility on {n} object(s)")
Args:
| Parameter | Type | Description |
|---|---|---|
lines_path |
str \| Path |
Path to the source .lines file |
output_path |
str \| Path |
Where to write the modified file; may equal lines_path to edit in place |
visible |
Mapping[int, bool] |
Mapping of object_id to desired visibility |
Returns: The number of elements whose visible attribute was changed.
Raises:
FileNotFoundError:lines_pathdoes not exist
Type hint:
def set_visibility(
lines_path: str | Path,
output_path: str | Path,
visible: Mapping[int, bool],
) -> int: ...
Dataclasses¶
LinesDocument¶
Everything parsed from a .lines file.
| Field | Type | Description |
|---|---|---|
caption |
str |
Project name from the root <Project> element |
version |
str |
Vexy Lines app version that created the file |
dpi |
int |
Document resolution (default 300) |
props |
DocumentProps |
Canvas dimensions and stroke limits |
groups |
list[GroupInfo \| LayerInfo] |
Top-level layer tree |
source_image_data |
bytes \| None |
Decoded JPEG bytes of the source image |
preview_image_data |
bytes \| None |
Decoded PNG bytes of the preview |
All fields have defaults, so LinesDocument() creates a valid empty document.
DocumentProps¶
Canvas dimensions and stroke range limits from the <Document> element.
| Field | Type | Default | Description |
|---|---|---|---|
width_mm |
float |
0.0 |
Canvas width in millimetres |
height_mm |
float |
0.0 |
Canvas height in millimetres |
dpi |
int |
300 |
Document resolution |
thickness_min |
float |
0.0 |
Minimum stroke thickness (mm) |
thickness_max |
float |
0.0 |
Maximum stroke thickness (mm) |
interval_min |
float |
0.0 |
Minimum line spacing (mm) |
interval_max |
float |
0.0 |
Maximum line spacing (mm) |
Maps from XML attributes: thicknessMin -> thickness_min, thicknessMax -> thickness_max, intervalMin -> interval_min, intervalMax -> interval_max.
GroupInfo¶
A group (<LrSection> element) containing layers and sub-groups.
| Field | Type | Default | Description |
|---|---|---|---|
caption |
str |
-- | Group name |
object_id |
int \| None |
None |
Unique ID, or None for href references |
expanded |
bool |
True |
Whether the group is expanded in the UI |
children |
list[GroupInfo \| LayerInfo] |
[] |
Child nodes |
LayerInfo¶
A single layer (<FreeMesh> element) with fills and optional mask.
| Field | Type | Default | Description |
|---|---|---|---|
caption |
str |
-- | Layer name |
object_id |
int \| None |
None |
Unique ID |
visible |
bool |
True |
Whether the layer is visible |
mask |
MaskInfo \| None |
None |
Mask settings |
fills |
list[FillNode] |
[] |
Ordered fill algorithms |
grid_edges |
list[dict[str, str]] |
[] |
Mesh deformation data |
Each entry in grid_edges is a dict with a "type" key ("row_grid_edge" or "col_grid_edge") plus all XML attributes as string key-value pairs.
FillNode¶
A single fill algorithm instance inside a layer.
| Field | Type | Default | Description |
|---|---|---|---|
xml_tag |
str |
-- | Original XML tag (e.g. "LinearStrokesTmpl") |
caption |
str |
-- | Fill name as shown in the UI |
params |
FillParams |
-- | All fill parameters |
object_id |
int \| None |
None |
Unique ID |
FillParams¶
Numeric and colour parameters for a fill algorithm.
| Field | Type | Default | Description |
|---|---|---|---|
fill_type |
str |
-- | Algorithm name (e.g. "linear", "circular") |
color |
str |
-- | Normalised hex colour (#RRGGBB or #RRGGBBAA) |
interval |
float |
0.0 |
Line spacing in mm |
angle |
float |
0.0 |
Stroke angle in degrees |
thickness |
float |
0.0 |
Stroke thickness (from XML thick_gap) |
thickness_min |
float |
0.0 |
Minimum thickness (from XML base_width) |
smoothness |
float |
0.0 |
Curve smoothness factor |
uplimit |
float |
0.0 |
Upper brightness threshold (0--255) |
downlimit |
float |
255.0 |
Lower brightness threshold (0--255) |
multiplier |
float |
1.0 |
Width multiplier |
base_width |
float |
0.0 |
Baseline stroke width in mm |
dispersion |
float |
0.0 |
Random perpendicular offset |
shear |
float |
0.0 |
Shear distortion angle in degrees |
raw |
dict[str, str] |
{} |
All original XML attributes |
raw includes every attribute from the XML element, including algorithm-specific keys not promoted to named fields (e.g. vert_disp, enbl_dotted, type_conv). See the File Format reference for the complete attribute catalogue.
MaskInfo¶
Mask settings from a <MaskData> element.
| Field | Type | Default | Description |
|---|---|---|---|
mask_type |
int |
0 |
Mask mode: 0 = none, 1 = raster |
invert |
bool |
False |
Whether the mask is inverted |
tolerance |
float |
0.0 |
Edge tolerance |
Maps from XML: invert_mask -> invert (any value other than "0" is True).
Constants¶
FILL_TAG_MAP¶
dict[str, str] mapping XML element tags to human-readable fill type names.
from vexy_lines import FILL_TAG_MAP
FILL_TAG_MAP["LinearStrokesTmpl"] # "linear"
FILL_TAG_MAP["SigmoidStrokesTmpl"] # "wave"
FILL_TAG_MAP["CircleStrokesTmpl"] # "circular"
FILL_TAG_MAP["RadialStrokesTmpl"] # "radial"
FILL_TAG_MAP["SpiralStrokesTmpl"] # "spiral"
FILL_TAG_MAP["ScribbleStrokesTmpl"] # "scribble"
FILL_TAG_MAP["HalftoneStrokesTmpl"] # "halftone"
FILL_TAG_MAP["FreeCurveStrokesTmpl"] # "handmade"
FILL_TAG_MAP["PeanoStrokesTmpl"] # "fractals"
FILL_TAG_MAP["TracedAreaTmpl"] # "trace"
FILL_TAG_MAP["SourceStrokes"] # "source_strokes"
Note: FreeCurveStrokesTmpl resolves to "trace" at parse time when type_conv="9". The map holds the base mapping ("handmade"); the parser applies the override.
FILL_TAGS¶
set[str] of all recognised XML fill element tag names (keys of FILL_TAG_MAP).
from vexy_lines import FILL_TAGS
"LinearStrokesTmpl" in FILL_TAGS # True
"UnknownElement" in FILL_TAGS # False
NUMERIC_PARAMS¶
list[str] of XML attribute names whose values are numeric and can be interpolated between two fills of the same type.
from vexy_lines import NUMERIC_PARAMS
print(NUMERIC_PARAMS)
# ['interval', 'angle', 'thick_gap', 'smoothness', 'uplimit',
# 'downlimit', 'multiplier', 'base_width', 'dispersion',
# 'vert_disp', 'shear']
These are the XML attribute names (not the Python field names). Most map directly to FillParams fields; vert_disp is only available in FillParams.raw.
Error handling¶
The parser is lenient where possible and strict where data integrity matters.
Lenient behaviour:
- Missing numeric attributes default to
0.0(or field-specific defaults like255.0fordownlimit) - Missing
color_namedefaults to"#000000" - Unparseable numeric strings fall back to the default value
- Missing
<Document>element produces a defaultDocumentProps() - Failed source/preview image decoding logs a warning and sets the field to
None
Strict behaviour:
parse()raisesFileNotFoundErrorif the path does not existparse()raisesET.ParseErrorif the XML is malformedextract_source_image()/extract_preview_image()raiseValueErrorif no image is embedded
Pattern for safe parsing:
from vexy_lines import parse
try:
doc = parse("artwork.lines")
except FileNotFoundError:
print("File not found")
except Exception as e:
print(f"Parse error: {e}")
else:
if doc.source_image_data:
print(f"Source image: {len(doc.source_image_data)} bytes")
else:
print("No source image embedded")
Thread safety¶
The parser is stateless and safe to call from multiple threads concurrently. Each call to parse() or parse_string() creates its own XML tree and dataclass instances with no shared mutable state.
The loguru logger used internally is thread-safe.
replace_source_image() performs file I/O (copy + XML write) and is not safe to call concurrently on the same output file.
Performance¶
Parsing performance depends on file size, which is dominated by embedded images:
- XML parsing:
xml.etree.ElementTree(C-accelerated in CPython) handles the structural parsing. Typical layer trees with hundreds of fills parse in under 10ms. - Image decoding: The base64 decode + zlib decompress of the source image is the main cost. A 5MB JPEG source adds ~50--100ms.
- Memory: The decoded JPEG and PNG bytes are held in memory as
bytesobjects on theLinesDocument. For files with large embedded images, expect memory usage roughly 2x the uncompressed image size (compressed + decompressed copies during decode, then only decompressed is retained).
Tips for large files:
- If you only need metadata (fills, layer tree), the overhead is the image decoding. The parser currently always decodes images when present. Access
doc.groupsanddoc.propswithout touching the image fields. - For batch processing many files, parse in a process pool to parallelise the zlib decompression.
parse_string()avoids the file I/O overhead when you already have the XML in memory.