Skip to content

Commit

Permalink
Make --page-size optional for HPGL output (#132)
Browse files Browse the repository at this point in the history
Also, fixed a bug where empty hpgl file would be generated in some cases
  • Loading branch information
abey79 authored Dec 17, 2020
1 parent 87db26f commit 5af3737
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 81 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

New features and improvements:
* A Windows installer is now available (#120)
* HPGL output: `--page-size` is no longer mandatory and `write` will try to infer which paper to use based on the current page size (#132)
* Added `reverse` command (#129)

Bug fixes:
* Fixed crash for SVG with <desc> element (#127)
* Fixed an issue where output HPGL file could be empty (#132)


#### 1.1 (2020-12-10)
Expand Down
26 changes: 17 additions & 9 deletions docs/cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ file::

$ vpype read --single-layer --layer 1 input1.svg read --single-layer --layer 2 input2.svg write output.svg

Note the use of ``--single-layer``. It is necessary to make sure that the input SVG is merged into a single layer and is
necessary to enable the ``--layer`` option.
Note the use of :option:`--single-layer <read --single-layer>`. It is necessary to make sure that the input SVG is
merged into a single layer and is necessary to enable the :option:`--layer <read --layer>` option.

This command will :ref:`cmd_read` two SVG files onto two different layers, rotate one layer 180 degrees, then
:ref:`cmd_write` both layers into a single SVG file::
Expand Down Expand Up @@ -120,13 +120,20 @@ Converting a SVG to HPGL

For vintage plotters, the :ref:`cmd_write` command is capable of generating HPGL code instead of SVG. HPGL output format
is automatically selected if the output path file extension is ``.hpgl``. Since HPGL coordinate systems vary widely from
plotter to plotter and even for different physical paper format, the plotter model and the paper format must be provided
plotter to plotter and even for different physical paper format, the plotter model must be provided
to the :ref:`cmd_write` command::

$ vpype read input.svg write --device hp7475a --page-size a4 --landscape --center output.hpgl
$ vpype read input.svg write --device hp7475a output.hpgl

The plotter paper size will be inferred from the current page size (which is set by the input SVG in this case).
The plotter type/paper format combination must exist in the built-in or user-provided configuration file. See
:ref:`faq_custom_hpgl_config` for information on how to create one.
:ref:`faq_custom_hpgl_config` for information on how to create one. If a matching plotter paper size cannot be found,
an error will be generated. In this case, the paper size must manually specified with the :option:`--page-size <write --page-size>` option::

$ vpype read input.svg write --device hp7475a --page-size a4 --landscape output.hpgl

Here the :option:`--landscape <write --landscape>` is also used to indicate that landscape orientation is desired. As
for SVG output, the :option:`--center <write --center>` is often use to center the geometries in the middle of the page.

It is typically useful to optimize the input SVG during the conversion. The following example is typical of real-world
use::
Expand All @@ -137,9 +144,9 @@ use::
Defining a default HPGL plotter device
======================================

If you are using the same type of plotter regularly, it may be cumbersome to systematically add the ``--device`` option
to the :ref:`cmd_write` command. The default device can be set in the ``~/.vpype.toml`` configuration file by adding the
following section:
If you are using the same type of plotter regularly, it may be cumbersome to systematically add the :option:`--device
<write --device>` option to the :ref:`cmd_write` command. The default device can be set in the ``~/.vpype.toml``
configuration file by adding the following section:

.. code-block:: toml
Expand All @@ -154,7 +161,8 @@ Creating a custom configuration file for a HPGL plotter

The configuration for a number of HPGL plotter is bundled with vpype (run ``vpype write --help`` for a list). If your
plotter is not included, it is possible to define your own plotter configuration either in `~/.vpype.toml` or any other
file. In the latter case, you must instruct vpype to load the configuration using the ``--config`` option::
file. In the latter case, you must instruct vpype to load the configuration using the :option:`--config <vpype
--config>` global option::

$ vpype --config my_config_file.toml read input.svg [...] write --device my_plotter --page-size a4 output.hpgl

Expand Down
13 changes: 13 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,19 @@ def test_snap():
assert np.all(lc[0] == np.array([0, 1 + 1j, 2j]))


def test_filter():
assert len(execute_single_line("filter --min-length 10", [0, 15])) == 1
assert len(execute_single_line("filter --min-length 10", [0, 10])) == 1
assert len(execute_single_line("filter --min-length 10", [0, 5])) == 0
assert len(execute_single_line("filter --max-length 10", [0, 15])) == 0
assert len(execute_single_line("filter --max-length 10", [0, 10])) == 1
assert len(execute_single_line("filter --max-length 10", [0, 5])) == 1
assert len(execute_single_line("filter --closed", [0, 5, 5j, 0])) == 1
assert len(execute_single_line("filter --closed", [0, 5, 5j])) == 0
assert len(execute_single_line("filter --not-closed", [0, 5, 5j, 0])) == 0
assert len(execute_single_line("filter --not-closed", [0, 5, 5j])) == 1


@pytest.mark.parametrize("pitch", [0.1, 1, 5, 10, 20, 50, 100, 200, 500])
def test_snap_no_duplicate(pitch: float):
"""Snap should return no duplicated points and reject lines that degenerate into a single
Expand Down
104 changes: 75 additions & 29 deletions tests/test_hpgl.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@
import vpype as vp
from vpype_cli import cli

HPGL_TEST_CASES = [
("line 2 3 6 4", "-d simple -p simple", "IN;DF;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
("line 2 3 50 3", "-d simple -p simple", "IN;DF;SP1;PU2,3;PD10,3;PU;SP0;IN;"),
("line 2 3 6 4", "-d simple -p aka_simple", "IN;DF;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
("line 2 3 6 4", "-d simple -p simple_ps10", "IN;DF;PS10;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
(
"line 2 3 6 4 line -l 2 4 5 2 3",
"-d simple -p simple",
"IN;DF;SP1;PU2,3;PD6,4;PU;SP2;PU4,5;PD2,3;PU;SP0;IN;",
),
("line 2 3 6 4", "-d simple -p simple_landscape", "IN;DF;SP1;PU3,8;PD4,4;PU;SP0;IN;"),
("line 2 3 6 4", "-d simple -p simple_y_up", "IN;DF;SP1;PU2,12;PD6,11;PU;SP0;IN;"),
(
"line 2 3 6 4",
"-d simple -p simple_rotate_180",
"IN;DF;SP1;PU8,12;PD4,11;PU;SP0;IN;",
),
(
"line 2 3 6 4",
"-d simple -p simple_final_pu",
"IN;DF;SP1;PU2,3;PD6,4;PU0,0;SP0;IN;",
),
("line 3 5 4 6", "-d double -p simple", "IN;DF;SP1;PU6,10;PD8,12;PU;SP0;IN;"),
]


@pytest.fixture
def simple_printer_config(config_file_factory):
Expand Down Expand Up @@ -105,44 +130,38 @@ def simple_printer_config(config_file_factory):
)


@pytest.mark.parametrize(
["commands", "write_opts", "expected"],
[
("line 2 3 6 4", "-d simple -p simple", "IN;DF;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
("line 2 3 50 3", "-d simple -p simple", "IN;DF;SP1;PU2,3;PD10,3;PU;SP0;IN;"),
("line 2 3 6 4", "-d simple -p aka_simple", "IN;DF;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
("line 2 3 6 4", "-d simple -p simple_ps10", "IN;DF;PS10;SP1;PU2,3;PD6,4;PU;SP0;IN;"),
(
"line 2 3 6 4 line -l 2 4 5 2 3",
"-d simple -p simple",
"IN;DF;SP1;PU2,3;PD6,4;PU;SP2;PU4,5;PD2,3;PU;SP0;IN;",
),
("line 2 3 6 4", "-d simple -p simple_landscape", "IN;DF;SP1;PU3,8;PD4,4;PU;SP0;IN;"),
("line 2 3 6 4", "-d simple -p simple_y_up", "IN;DF;SP1;PU2,12;PD6,11;PU;SP0;IN;"),
(
"line 2 3 6 4",
"-d simple -p simple_rotate_180",
"IN;DF;SP1;PU8,12;PD4,11;PU;SP0;IN;",
),
(
"line 2 3 6 4",
"-d simple -p simple_final_pu",
"IN;DF;SP1;PU2,3;PD6,4;PU0,0;SP0;IN;",
),
("line 3 5 4 6", "-d double -p simple", "IN;DF;SP1;PU6,10;PD8,12;PU;SP0;IN;"),
],
)
@pytest.mark.parametrize(["commands", "write_opts", "expected"], HPGL_TEST_CASES)
def test_hpgl_simple(runner, simple_printer_config, commands, write_opts, expected):
# Note: passing a single string to invoke runs it through shlex and removes the windows
# path back-slash, thus the split.
res = runner.invoke(
cli, f"-c {simple_printer_config} {commands} write -f hpgl {write_opts} -".split(" ")
)

assert res.exit_code == 0
assert res.stdout.strip() == expected
assert res.stderr.strip() == ""


@pytest.mark.parametrize(["commands", "write_opts", "expected"], HPGL_TEST_CASES)
def test_hpgl_file_written(
runner, simple_printer_config, tmp_path, commands, write_opts, expected
):
file_path = str(tmp_path / "output.hpgl")

res = runner.invoke(
cli,
f"-c {simple_printer_config} {commands} write -f hpgl {write_opts} {file_path}".split(
" "
),
)

assert res.exit_code == 0

with open(file_path) as fp:
assert fp.read().strip() == expected


def test_hpgl_info(runner, simple_printer_config):
res = runner.invoke(
cli,
Expand Down Expand Up @@ -170,9 +189,36 @@ def test_hpgl_info_quiet(runner, simple_printer_config):
[(15, 10), "simple"],
[(20, 30), "simple_big"],
[(30, 20), "simple_big"],
[None, None],
],
)
def test_hpgl_paper_config_from_format(simple_printer_config, paper_format, expected_name):
def test_hpgl_paper_config_from_size(simple_printer_config, paper_format, expected_name):
vp.CONFIG_MANAGER.load_config_file(simple_printer_config)
pc = vp.CONFIG_MANAGER.get_plotter_config("simple").paper_config_from_size(paper_format)
assert pc.name == expected_name
if expected_name is None:
assert pc is None
else:
assert pc.name == expected_name


def test_hpgl_paper_config(simple_printer_config):
vp.CONFIG_MANAGER.load_config_file(simple_printer_config)
assert vp.CONFIG_MANAGER.get_plotter_config("simple").paper_config("simple") is not None
assert vp.CONFIG_MANAGER.get_plotter_config("simple").paper_config("DOESNTEXIST") is None


def test_hpgl_paper_size_inference(runner):
res = runner.invoke(cli, "rect 5cm 5cm 5cm 5cm pagesize a4 write -f hpgl -d hp7475a -")

assert res.exit_code == 0
assert res.stdout.strip() == (
"IN;DF;PS4;SP1;PU1608,1849;PD3617,1849,3617,3859,1608,3859,1608,1849;PU11040,7721;"
"SP0;IN;"
)


def test_hpgl_paper_size_inference_fail(runner):
res = runner.invoke(cli, "rect 5cm 5cm 5cm 5cm pagesize a6 write -f hpgl -d hp7475a -")

assert res.exit_code == 0 # this should probably be non-zero, see #131
assert res.stdout.strip() == ""
2 changes: 1 addition & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def line_collection_contains(lc: vp.LineCollection, line: Sequence[complex]) ->
return False


def execute_single_line(pipeline: str, line: np.ndarray) -> vp.LineCollection:
def execute_single_line(pipeline: str, line: vp.LineLike) -> vp.LineCollection:
"""Execute a pipeline on a single line. The pipeline is expected to remain single layer.
Returns:
Expand Down
14 changes: 10 additions & 4 deletions vpype/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,23 @@ def paper_config(self, paper: str) -> Optional[PaperConfig]:
return pc
return None

def paper_config_from_size(self, page_size: Tuple[float, float]) -> Optional[PaperConfig]:
def paper_config_from_size(
self, page_size: Optional[Tuple[float, float]]
) -> Optional[PaperConfig]:
"""Look for a paper configuration matching ``paper_format`` and return it if found.
Args:
page_size: desired page size
page_size: tuple of desired page size (may be ``None``, in which case ``None`` is
returned
Returns:
the :class:`PaperConfig` instance corresponding to ``paper_format`` or None if not
found
"""

if page_size is None:
return None

def _isclose_tuple(a, b):
return all(math.isclose(aa, bb) for aa, bb in zip(a, b))

Expand Down Expand Up @@ -187,11 +193,11 @@ def get_plotter_list(self) -> List[str]:
"""
return list(self.config.get("device", {}).keys())

def get_plotter_config(self, name: str) -> Optional[PlotterConfig]:
def get_plotter_config(self, name: Optional[str]) -> Optional[PlotterConfig]:
"""Returns a :class:`PlotterConfig` instance for plotter ``name``.
Args:
name: name of desired plotter
name: name of desired plotter (may be ``None``, in which case ``None`` is returned)
Returns:
:class:`PlotterConfig` instance or None if not found
Expand Down
1 change: 1 addition & 0 deletions vpype/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,3 +604,4 @@ def complex_to_str(p: complex) -> str:
)

output.write("SP0;IN;\n")
output.flush()
Loading

0 comments on commit 5af3737

Please sign in to comment.