diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68800beb..7c88f7ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,7 @@ Release date: UNRELEASED
### Bug fixes
-* ...
+* Fixed a design issue with the `read` command where disjoints groups of digit in layer names would be used to determine layer IDs. Only the first contiguous group of digit is used, so a layer named "01-layer1" would now have layer ID of 1 instead of 11 (#606)
### API changes
diff --git a/tests/test_files.py b/tests/test_files.py
index 8ad91946..97ebc762 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -614,3 +614,39 @@ def test_read_svg_by_attributes_default_page_size(
)
assert doc.page_size == pytest.approx(target)
+
+
+def test_read_layer_id_extraction():
+ from vpype.io import _extract_digit_group
+
+ assert _extract_digit_group("") is None
+ assert _extract_digit_group("layer") is None
+ assert _extract_digit_group("11layer") == "11"
+ assert _extract_digit_group("11layer332") == "11"
+ assert _extract_digit_group("lay11er332") == "11"
+ assert _extract_digit_group("layer332") == "332"
+
+
+@pytest.mark.parametrize(
+ ("attr", "exp"),
+ [
+ ("", 1),
+ ('id="5"', 5),
+ ('id="56adf5"', 56),
+ ('id="asdf53adf5"', 53),
+ ('inkscape:label="5" id="6"', 5),
+ ('inkscape:label="hello" id="world6"', 6),
+ ('inkscape:label="47hello25" id="world6"', 47),
+ ],
+)
+def test_read_layer_id_from_svg(attr, exp):
+ svg = f""""""
+
+ doc = vp.read_multilayer_svg(io.StringIO(svg), 0.1)
+ assert doc.layers.keys() == {exp}
+ assert len(doc.layers[exp]) == 1
diff --git a/vpype/io.py b/vpype/io.py
index d4cd9849..f2934523 100644
--- a/vpype/io.py
+++ b/vpype/io.py
@@ -165,7 +165,7 @@ def _merge_metadata(
def _element_to_paths(elem: svgelements.SVGElement) -> _PathType | None:
- """Convert a SVG element into a path object that can be processed by
+ """Convert an SVG element into a path object that can be processed by
:func:`_flattened_paths_to_line_collection`
Args:
@@ -382,7 +382,7 @@ def read_svg(
default_width: float | None = None,
default_height: float | None = None,
) -> tuple[LineCollection, float, float]:
- """Read a SVG file an return its content as a :class:`LineCollection` instance.
+ """Read an SVG file and return its content as a :class:`LineCollection` instance.
All curved geometries are chopped in segments no longer than the value of *quantization*.
Optionally, the geometries are simplified using Shapely, using the value of *quantization*
@@ -425,6 +425,21 @@ def read_svg(
return lc, svg.width, svg.height
+_LID_RE = re.compile(r"\d+")
+
+
+def _extract_digit_group(label: str) -> str | None:
+ """Extract a layer ID from a label.
+
+ The first continuous group of digit, if any, is returned.
+ """
+ match = _LID_RE.search(label)
+ if match is not None:
+ return match.group()
+ else:
+ return None
+
+
def read_multilayer_svg(
file: str | TextIO,
quantization: float,
@@ -441,9 +456,9 @@ def read_multilayer_svg(
in layer 1.
Groups are matched to layer ID according their `inkscape:label` attribute, their `id`
- attribute or their appearing order, in that order of priority. Labels are stripped of
- non-numeric characters and the remaining is used as layer ID. Lacking numeric characters,
- the appearing order is used. If the label is 0, its changed to 1.
+ attribute or their appearing order, in that order of priority. The first contiguous group
+ of digits in the label is used as layer ID. Lacking numeric characters, the appearing order
+ is used. If the label is 0, it is changed to 1.
All curved geometries are chopped in segments no longer than the value of *quantization*.
Optionally, the geometries are simplified using Shapely, using the value of *quantization*
@@ -467,8 +482,8 @@ def read_multilayer_svg(
provided
Returns:
- :class:`Document` instance with the imported geometries and its page size set the the
- SVG dimensions
+ :class:`Document` instance with the imported geometries and its page size set the SVG
+ dimensions
"""
svg = svgelements.SVG.parse(file, width=default_width, height=default_height)
@@ -491,9 +506,9 @@ def _find_groups(group: svgelements.Group) -> Iterator[svgelements.Group]:
layer_name = g.values.get("{http://www.inkscape.org/namespaces/inkscape}label", None)
# compute a decent layer ID
- lid_str = re.sub("[^0-9]", "", layer_name or "")
+ lid_str = _extract_digit_group(layer_name or "")
if not lid_str:
- lid_str = re.sub("[^0-9]", "", g.values.get("id") or "")
+ lid_str = _extract_digit_group(g.values.get("id") or "")
if lid_str:
lid = int(lid_str)
if lid == 0:
@@ -545,7 +560,7 @@ def read_svg_by_attributes(
default_width: float | None = None,
default_height: float | None = None,
) -> Document:
- """Read a SVG file by sorting geometries by unique combination of provided attributes.
+ """Read an SVG file by sorting geometries by unique combination of provided attributes.
All curved geometries are chopped in segments no longer than the value of *quantization*.
Optionally, the geometries are simplified using Shapely, using the value of *quantization*
@@ -570,8 +585,8 @@ def read_svg_by_attributes(
provided
Returns:
- :class:`Document` instance with the imported geometries and its page size set the the
- SVG dimensions
+ :class:`Document` instance with the imported geometries and its page size set the SVG
+ dimensions
"""
svg = svgelements.SVG.parse(file, width=default_width, height=default_height)
@@ -637,10 +652,10 @@ def write_svg(
use_svg_metadata: bool = False,
set_date: bool = True,
) -> None:
- """Create a SVG from a :py:class:`Document` instance.
+ """Create an SVG from a :py:class:`Document` instance.
If no page size is provided (or (0, 0) is passed), the SVG generated has bounds tightly
- fitted around the geometries. Otherwise the provided size (in pixel) is used. The width
+ fitted around the geometries. Otherwise, the provided size (in pixel) is used. The width
and height is capped to a minimum of 1 pixel.
By default, no translation is applied on the geometry. If `center=True`, geometries are
@@ -776,7 +791,7 @@ def write_svg(
color = Color(layer.property(METADATA_FIELD_COLOR))
# we want to avoid a subsequent layer whose color is undefined to have its color
- # affected by whether or not previous layer have their color defined
+ # affected by whether previous layer have their color defined
color_idx += 1
group.attribs["stroke"] = color.as_rgb_hex()
@@ -841,7 +856,7 @@ def write_hpgl(
absolute: bool = False,
quiet: bool = False,
) -> None:
- """Create a HPGL file from the :class:`Document` instance.
+ """Create an HPGL file from the :class:`Document` instance.
The ``device``/``page_size`` combination must be defined in the built-in or user-provided
config files or an exception will be raised.
@@ -857,7 +872,7 @@ def write_hpgl(
page_size: page size string (it must be configured for the selected device)
landscape: if True, the geometries are generated in landscape orientation
center: center geometries on page before export
- device: name of the device to use (the corresponding config must exists). If not
+ device: name of the device to use (the corresponding config must exist). If not
provided, a default device must be configured, which will be used.
velocity: if provided, a VS command will be generated with the corresponding value
absolute: if True, only use absolute coordinates
@@ -879,7 +894,7 @@ def write_hpgl(
# Handle flex paper size.
# If paper_size is not provided by the config, the paper size is then assumed to be the
- # same a the current page size. In this case, the config should provide paper_orientation
+ # same as the current page size. In this case, the config should provide paper_orientation
# since it may not be the same as the document's page size
paper_size = paper_config.paper_size
if paper_size is None:
diff --git a/vpype_cli/read.py b/vpype_cli/read.py
index 97444908..305c9dd7 100644
--- a/vpype_cli/read.py
+++ b/vpype_cli/read.py
@@ -91,9 +91,9 @@ def read(
display_size: tuple[float, float] | None,
display_landscape: bool,
) -> vp.Document:
- """Extract geometries from a SVG file.
+ """Extract geometries from an SVG file.
- FILE may be a file path path or a dash (-) to read from the standard input instead.
+ FILE may be a file path or a dash (-) to read from the standard input instead.
By default, the `read` command attempts to preserve the layer structure of the SVG. In this
context, top-level groups () are each considered a layer. If any, all non-group,
@@ -102,9 +102,9 @@ def read(
The following logic is used to determine in which layer each SVG top-level group is
imported:
- - If a `inkscape:label` attribute is present and contains digit characters, it is \
-stripped of non-digit characters the resulting number is used as target layer. If the \
-resulting number is 0, layer 1 is used instead.
+ - If a `inkscape:label` attribute is present and contains digit characters, the first \
+group of contiguous digits is used as target layer. If the resulting number is 0, layer 1 is \
+used instead.
- If the previous step fails, the same logic is applied to the `id` attribute.
@@ -141,7 +141,7 @@ def read(
length attributes. The crop operation can be disabled with the `--no-crop` option.
In general, SVG boundaries are determined by the `width` and `height` of the top-level
-