Skip to content

Commit

Permalink
Fixed layer ID determination to use only the first group of contiguou…
Browse files Browse the repository at this point in the history
…s digit (#606)
  • Loading branch information
abey79 authored Mar 11, 2023
1 parent 292f1b3 commit 88398fa
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 25 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 36 additions & 0 deletions tests/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""<?xml version="1.0"?><svg width="500" height="300"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
<g {attr}>
<line x1="0" y1="0" x2="10" y2="10" fill="red" />
</g>
</svg>"""

doc = vp.read_multilayer_svg(io.StringIO(svg), 0.1)
assert doc.layers.keys() == {exp}
assert len(doc.layers[exp]) == 1
51 changes: 33 additions & 18 deletions vpype/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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*
Expand Down Expand Up @@ -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,
Expand All @@ -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*
Expand All @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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*
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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:
Expand Down
12 changes: 6 additions & 6 deletions vpype_cli/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<g>) are each considered a layer. If any, all non-group,
Expand All @@ -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.
Expand Down Expand Up @@ -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
<svg> tag. However, the some SVG may have their width and/or height specified as percent
<svg> 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
Expand Down

0 comments on commit 88398fa

Please sign in to comment.