Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handling of SVG with missing width and height. #446

Merged
merged 2 commits into from
Apr 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Release date: UNRELEASED

* Added HPGL configuration for the Calcomp Artisan plotter (thanks to Andee Collard and @ithinkido) (#418)
* Added the `--dont-set-date` option to the `write` command (#442)
* The `read` command now better handles SVGs with missing `width` or `height` attributes (#446)

When the `width` or `height` attribute is missing or expressed as percent, the `read` command now attempts to use the `viewBox` attribute to set the page size, defaulting to 1000x1000px if missing. This behavior can be overridden with the `--display-size` and the `--display-landscape` parameters.


### Bug fixes
Expand All @@ -23,6 +26,7 @@ Release date: UNRELEASED
* Added `vpype_cli.FloatType()`, `vpype_cli.IntRangeType()`, and `vpype_cli.ChoiceType()` (#430)
* Changed `vpype.Document.add_to_sources()` to also modify the `vp_source` property (#431)
* Added a `set_date:bool = True` argument to `vpype.write_svg()` (#442)
* Changed the default value of `default_width` and `default_height` arguments of `vpype.read_svg()` (and friends) to `None` to allow `svgelement` better handle missing `width`/`height` attributes (#446)


### Other changes
Expand Down
108 changes: 108 additions & 0 deletions tests/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,3 +506,111 @@ def test_write_dont_set_date(capsys):
vpype_cli.execute("line 0 0 10 10 write -f svg --dont-set-date -")
output = capsys.readouterr().out
assert "<dc:date>" not in output


_PAGE_SIZE_EXAMPLES = [
(
"""<?xml version="1.0"?><svg>
<line x1="0" y1="0" x2="10" y2="10" fill="red" />
</svg>""",
(None, None),
(1000.0, 1000.0),
),
(
"""<?xml version="1.0"?><svg viewBox="0 0 900 800">
<line x1="0" y1="0" x2="10" y2="10" fill="red" />
</svg>""",
(None, None),
(900.0, 800.0),
),
(
"""<?xml version="1.0"?><svg viewBox="0 0 900 800">
<line x1="0" y1="0" x2="10" y2="10" fill="red" />
</svg>""",
(700, 600),
(700.0, 600.0),
),
(
"""<?xml version="1.0"?><svg width="500" height="300">
<line x1="0" y1="0" x2="10" y2="10" fill="red" />
</svg>""",
(700, 650),
(500, 300),
),
(
"""<?xml version="1.0"?><svg width="500" height="300">
<line x1="0" y1="0" x2="10" y2="10" fill="red" />
</svg>""",
(None, None),
(500, 300),
),
(
"""<?xml version="1.0"?><svg width="500" height="300" viewBox="0 0 600 750">
<line x1="0" y1="0" x2="10" y2="10" fill="red" />
</svg>""",
(700, 600),
(500, 300),
),
(
"""<?xml version="1.0"?><svg width="500" height="300" viewBox="0 0 600 750">
<line x1="0" y1="0" x2="10" y2="10" fill="red" />
</svg>""",
(None, None),
(500, 300),
),
]


@pytest.mark.parametrize(["svg", "default", "target"], _PAGE_SIZE_EXAMPLES)
def test_read_command_page_size(
tmp_path, svg: str, default: tuple[float, float], target: tuple[float, float]
):
path = _write_svg_file(tmp_path, svg)
args = ""
if default[0] is not None and default[1] is not None:
args += f"--display-size {default[0]:.3f}x{default[1]:.3f} "
if default[0] > default[1]:
args += f"--display-landscape"
doc = vpype_cli.execute(f"read {args} {path}")

assert doc.page_size == pytest.approx(target)


@pytest.mark.parametrize(["svg", "default", "target"], _PAGE_SIZE_EXAMPLES)
def test_read_multilayer_svg_default_page_size(
tmp_path, svg: str, default: tuple[float, float], target: tuple[float, float]
):
path = _write_svg_file(tmp_path, svg)
doc = vp.read_multilayer_svg(
path, quantization=0.1, default_width=default[0], default_height=default[1]
)

assert doc.page_size == pytest.approx(target)


@pytest.mark.parametrize(["svg", "default", "target"], _PAGE_SIZE_EXAMPLES)
def test_read_svg_default_page_size(
tmp_path, svg: str, default: tuple[float, float], target: tuple[float, float]
):
path = _write_svg_file(tmp_path, svg)
_, width, height = vp.read_svg(
path, quantization=0.1, default_width=default[0], default_height=default[1]
)

assert (width, height) == pytest.approx(target)


@pytest.mark.parametrize(["svg", "default", "target"], _PAGE_SIZE_EXAMPLES)
def test_read_svg_by_attributes_default_page_size(
tmp_path, svg: str, default: tuple[float, float], target: tuple[float, float]
):
path = _write_svg_file(tmp_path, svg)
doc = vp.read_svg_by_attributes(
path,
quantization=0.1,
attributes=["fill"],
default_width=default[0],
default_height=default[1],
)

assert doc.page_size == pytest.approx(target)
34 changes: 24 additions & 10 deletions vpype/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@
]


_DEFAULT_WIDTH = 1000
_DEFAULT_HEIGHT = 1000


class _ComplexStack:
"""Complex number stack implemented with a numpy array"""

Expand Down Expand Up @@ -383,15 +379,21 @@ def read_svg(
crop: bool = True,
simplify: bool = False,
parallel: bool = False,
default_width: float = _DEFAULT_WIDTH,
default_height: float = _DEFAULT_HEIGHT,
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.

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*
as tolerance.

The page size is set based on the ``width`` and ``height`` attributes of the ``<svg>`` tag.
If these attributes are missing or expressed in percent, ``svgelements`` attempts to use
the ``viewBox`` attribute instead, or reverts to a 1000x1000px page size. This behaviour
can be overridden by providing values for the ``default_width`` and ``default_height``
arguments.

Args:
file: path of the SVG file or stream object
quantization: maximum size of segment used to approximate curved geometries
Expand Down Expand Up @@ -429,8 +431,8 @@ def read_multilayer_svg(
crop: bool = True,
simplify: bool = False,
parallel: bool = False,
default_width: float = _DEFAULT_WIDTH,
default_height: float = _DEFAULT_HEIGHT,
default_width: float | None = None,
default_height: float | None = None,
) -> Document:
"""Read a multilayer SVG file and return its content as a :class:`Document` instance
retaining the SVG's layer structure and its dimension.
Expand All @@ -447,6 +449,12 @@ def read_multilayer_svg(
Optionally, the geometries are simplified using Shapely, using the value of *quantization*
as tolerance.

The page size is set based on the ``width`` and ``height`` attributes of the ``<svg>`` tag.
If these attributes are missing or expressed in percent, ``svgelements`` attempts to use
the ``viewBox`` attribute instead, or reverts to a 1000x1000px page size. This behaviour
can be overridden by providing values for the ``default_width`` and ``default_height``
arguments.

Args:
file: path of the SVG file or stream object
quantization: maximum size of segment used to approximate curved geometries
Expand Down Expand Up @@ -534,15 +542,21 @@ def read_svg_by_attributes(
crop: bool = True,
simplify: bool = False,
parallel: bool = False,
default_width: float = _DEFAULT_WIDTH,
default_height: float = _DEFAULT_HEIGHT,
default_width: float | None = None,
default_height: float | None = None,
) -> Document:
"""Read a 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*
as tolerance.

The page size is set based on the ``width`` and ``height`` attributes of the ``<svg>`` tag.
If these attributes are missing or expressed in percent, ``svgelements`` attempts to use
the ``viewBox`` attribute instead, or reverts to a 1000x1000px page size. This behaviour
can be overridden by providing values for the ``default_width`` and ``default_height``
arguments.

Args:
file: path of the SVG file or stream object
attributes: attributes by which the object should be sorted
Expand Down
29 changes: 15 additions & 14 deletions vpype_cli/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"-ds",
"--display-size",
type=PageSizeType(),
default="a4",
help=(
"Display size to use for SVG with width/height expressed as percentage or missing "
"altogether (see `write` command for possible format)."
Expand All @@ -89,7 +88,7 @@ def read(
simplify: bool,
parallel: bool,
no_crop: bool,
display_size: tuple[float, float],
display_size: tuple[float, float] | None,
display_landscape: bool,
) -> vp.Document:
"""Extract geometries from a SVG file.
Expand Down Expand Up @@ -144,9 +143,9 @@ def read(
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
value or even miss them altogether (in which case they are assumed to be set to 100%). In
these cases, vpype considers by default that 100% corresponds to a A4 page in portrait
orientation. The options `--display-size FORMAT` and `--display-landscape` can be used
to specify a different format.
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
`--display-landscape` can be used to specify a different format in such instances.

When importing the SVG, the `read` commands attempts to extract the SVG attributes that
are common to all paths within a layer. The "stroke", "stroke-width" and "inkscape:label"
Expand Down Expand Up @@ -186,9 +185,11 @@ def read(
vpype read --no-crop input_file.svg [...]
"""

width, height = display_size
if display_landscape:
width, height = height, width
default_width = default_height = None
if display_size is not None:
default_width, default_height = display_size
if display_landscape and default_width < default_height:
default_width, default_height = default_height, default_width

if file == "-":
file = sys.stdin
Expand All @@ -214,8 +215,8 @@ def read(
crop=not no_crop,
simplify=simplify,
parallel=parallel,
default_width=width,
default_height=height,
default_width=default_width,
default_height=default_height,
)

document.add(lc, single_to_layer_id(layer, document), with_metadata=True)
Expand All @@ -232,8 +233,8 @@ def read(
crop=not no_crop,
simplify=simplify,
parallel=parallel,
default_width=width,
default_height=height,
default_width=default_width,
default_height=default_height,
)
else:
doc = vp.read_svg_by_attributes(
Expand All @@ -243,8 +244,8 @@ def read(
crop=not no_crop,
simplify=simplify,
parallel=parallel,
default_width=width,
default_height=height,
default_width=default_width,
default_height=default_height,
)
document.extend(doc)

Expand Down