From 88398fa0f910ded3a21fe45896a09286cd063c1c Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Sat, 11 Mar 2023 17:15:10 +0100 Subject: [PATCH] Fixed layer ID determination to use only the first group of contiguous digit (#606) --- CHANGELOG.md | 2 +- tests/test_files.py | 36 ++++++++++++++++++++++++++++++++ vpype/io.py | 51 +++++++++++++++++++++++++++++---------------- vpype_cli/read.py | 12 +++++------ 4 files changed, 76 insertions(+), 25 deletions(-) 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 - tag. However, the some SVG may have their width and/or height specified as percent + tag. However, some SVGs may have their width and/or height specified as percent value or even miss them altogether (in which case they are assumed to be set to 100%). In these cases, vpype attempts to use the `viewBox` attribute to determine the page size, or revert to a 1000x1000px default. The options `--display-size FORMAT` and