Skip to content

Commit ef6be1f

Browse files
committed
improve hpgl2 add-on and add plt2dxf and plt2svg commands
add mirror modes to hpgl2 converter commands add rotation support to hpgl2.to_svg() function add rotation support to hpgl2.to_dxf() function remove all unsupported commands improve hpgl2 parser add setup of "Layout0" to dxf_backend add plt2dxf command to ezdxf launcher add plt2svg command to ezdxf launcher profile new fast_bbox() function move fill_method to Properties
1 parent d04d9f5 commit ef6be1f

File tree

15 files changed

+401
-169
lines changed

15 files changed

+401
-169
lines changed

NEWS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ Version 1.0.4 - dev
1010
- BUGFIX: invalid bulge to Bezier curve conversion for bulge values >= 1
1111
- BUGFIX: [#855](https://github.com/mozman/ezdxf/issues/855)
1212
scale `MTEXT/MLEADER` inline commands "absolute text height" at transformation
13-
- PREVIEW: `ezdxf.addons.hpgl2` add-on to convert HPGL/2 plot files to DXF,
13+
- PREVIEW: `ezdxf.addons.hpgl2` add-on to convert HPGL/2 plot files to DXF or SVG,
1414
final release in v1.1
15+
- PREVIEW: `ezdxf plt2dxf` command to convert HPGL/2 plot files to DXF, final release in v1.1
16+
- PREVIEW: `ezdxf plt2svg` command to convert HPGL/2 plot files to SVG, final release in v1.1
1517

1618
Version 1.0.3 - 2023-03-26
1719
--------------------------

profiling/path_bbox.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (c) 2021, Manfred Moitzi
2+
# License: MIT License
3+
import time
4+
import math
5+
from ezdxf.math import BoundingBox, Vec3
6+
from ezdxf.render.forms import circle
7+
import ezdxf.path
8+
9+
LARGE_PATH = ezdxf.path.from_vertices(circle(100, radius=10))
10+
11+
def current_fast_bbox():
12+
ezdxf.path.bbox((LARGE_PATH, ), fast=True)
13+
14+
def current_precise_bbox():
15+
ezdxf.path.bbox((LARGE_PATH, ), fast=False)
16+
17+
def new_fast_bbox():
18+
fast_bbox(LARGE_PATH)
19+
20+
21+
def fast_bbox(path) -> BoundingBox:
22+
# it's much slower!
23+
bbox = BoundingBox()
24+
if len(path) == 0:
25+
return bbox
26+
min_x = math.inf
27+
min_y = math.inf
28+
min_z = math.inf
29+
max_x = -math.inf
30+
max_y = -math.inf
31+
max_z = -math.inf
32+
for x, y, z in path.control_vertices():
33+
min_x = min(min_x, x)
34+
min_y = min(min_y, y)
35+
min_z = min(min_z, z)
36+
max_x = max(max_x, x)
37+
max_y = max(max_y, y)
38+
max_z = max(max_z, z)
39+
bbox.extend([Vec3(min_x, min_y, min_z), Vec3(max_x, max_y, max_z)])
40+
return bbox
41+
42+
43+
def profile(text, func, runs):
44+
t0 = time.perf_counter()
45+
for _ in range(runs):
46+
func()
47+
t1 = time.perf_counter()
48+
t = t1 - t0
49+
print(f"{text} {t:.3f}s")
50+
51+
52+
RUNS = 10_000
53+
54+
print(f"Profiling bounding box calculation:")
55+
profile(f"current_precise_bbox() {RUNS}:", current_precise_bbox, RUNS)
56+
profile(f"current_fast_bbox() {RUNS}:", current_fast_bbox, RUNS)
57+
profile(f"new_fast_bbox() {RUNS}:", new_fast_bbox, RUNS)

src/ezdxf/addons/hpgl2/api.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import ezdxf
55
from ezdxf.document import Drawing
66
from ezdxf import zoom, transform
7-
from ezdxf.math import Matrix44
7+
from ezdxf.math import Matrix44, BoundingBox
8+
89
from .tokenizer import hpgl2_commands
910
from .plotter import Plotter
1011
from .interpreter import Interpreter
@@ -18,9 +19,14 @@ def to_dxf(
1819
b: bytes,
1920
scale: float = 1.0,
2021
*,
22+
rotation: int=0,
23+
flip_horizontal=False,
24+
flip_vertical=False,
2125
color_mode=ColorMode.RGB,
2226
map_black_rgb_to_white_rgb=False,
2327
) -> Drawing:
28+
if rotation not in (0, 90, 180, 270):
29+
raise ValueError("invalid rotation angle: should be 0, 90, 180, or 270")
2430
doc = ezdxf.new()
2531
msp = doc.modelspace()
2632
plotter = Plotter(
@@ -30,9 +36,16 @@ def to_dxf(
3036
map_black_rgb_to_white_rgb=map_black_rgb_to_white_rgb,
3137
)
3238
)
33-
Interpreter(plotter).run(hpgl2_commands(b))
39+
plotter.set_page_rotation(rotation)
40+
interpreter = Interpreter(plotter)
41+
if flip_vertical or flip_horizontal:
42+
plotter.set_page_flip(vertical=flip_vertical, horizontal=flip_horizontal)
43+
# disable embedded scaling commands
44+
interpreter.disable_commands(["SC", "IP", "IR"])
45+
interpreter.run(hpgl2_commands(b))
3446
if plotter.bbox.has_data: # non-empty page
35-
_scale_and_zoom(msp, scale, plotter.bbox)
47+
bbox = _scale_and_zoom(msp, scale, plotter.bbox)
48+
_reset_doc(doc, bbox)
3649
return doc
3750

3851

@@ -45,13 +58,35 @@ def _scale_and_zoom(layout, scale, bbox):
4558
extmin = m.transform(extmin)
4659
extmax = m.transform(extmax)
4760
zoom.window(layout, extmin, extmax)
61+
return BoundingBox([extmin, extmax])
62+
63+
def _reset_doc(doc, bbox):
64+
doc.header["$EXTMIN"] = bbox.extmin
65+
doc.header["$EXTMAX"] = bbox.extmax
66+
67+
psp_size = bbox.size / 40.0 # plu to mm
68+
psp_center = psp_size * 0.5
69+
psp = doc.paperspace()
70+
psp.page_setup(size=(psp_size.x, psp_size.y), margins=(0, 0, 0, 0), units="mm")
71+
psp.add_viewport(
72+
center=psp_center,
73+
size=(psp_size.x, psp_size.y),
74+
view_center_point=bbox.center,
75+
view_height=bbox.size.y,
76+
)
4877

4978

50-
def to_svg(b: bytes) -> str:
79+
def to_svg(b: bytes, *, rotation: int=0, flip_horizontal=False, flip_vertical=False) -> str:
5180
# 1st pass records the plotting commands and detects the bounding box
5281
recorder = Recorder()
5382
plotter = Plotter(recorder)
54-
Interpreter(plotter).run(hpgl2_commands(b))
83+
plotter.set_page_rotation(rotation)
84+
interpreter = Interpreter(plotter)
85+
if flip_vertical or flip_horizontal:
86+
plotter.set_page_flip(vertical=flip_vertical, horizontal=flip_horizontal)
87+
# disable embedded scaling commands
88+
interpreter.disable_commands(["SC", "IP", "IR"])
89+
interpreter.run(hpgl2_commands(b))
5590
if not plotter.bbox.has_data:
5691
return ""
5792

src/ezdxf/addons/hpgl2/backend.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import abc
66
import enum
77

8-
from .deps import Vec2, Path, Bezier4P, BoundingBox2d
8+
from .deps import Vec2, Path
99
from .properties import Properties
1010

1111
# Page coordinates are always plot units:
@@ -26,7 +26,7 @@ def draw_polyline(self, properties: Properties, points: Sequence[Vec2]) -> None:
2626

2727
@abc.abstractmethod
2828
def draw_filled_polygon_buffer(
29-
self, properties: Properties, paths: Sequence[Path], fill_method: int
29+
self, properties: Properties, paths: Sequence[Path]
3030
) -> None:
3131
# input coordinates are page coordinates
3232
...
@@ -38,16 +38,19 @@ def draw_outline_polygon_buffer(
3838
# input coordinates are page coordinates
3939
...
4040

41+
4142
class RecordType(enum.Enum):
4243
POLYLINE = enum.auto()
4344
FILLED_POLYGON = enum.auto()
4445
OUTLINE_POLYGON = enum.auto()
4546

47+
4648
class DataRecord(NamedTuple):
4749
type: RecordType
4850
property_hash: int
4951
args: Any
5052

53+
5154
class Recorder(Backend):
5255
def __init__(self) -> None:
5356
self.records: list[DataRecord] = []
@@ -56,11 +59,10 @@ def __init__(self) -> None:
5659
def draw_polyline(self, properties: Properties, points: Sequence[Vec2]) -> None:
5760
self.store(RecordType.POLYLINE, properties, tuple(points))
5861

59-
6062
def draw_filled_polygon_buffer(
61-
self, properties: Properties, paths: Sequence[Path], fill_method: int
63+
self, properties: Properties, paths: Sequence[Path]
6264
) -> None:
63-
self.store(RecordType.FILLED_POLYGON, properties, tuple(paths), fill_method)
65+
self.store(RecordType.FILLED_POLYGON, properties, tuple(paths))
6466

6567
def draw_outline_polygon_buffer(
6668
self, properties: Properties, paths: Sequence[Path]
@@ -74,12 +76,13 @@ def store(self, record_type: RecordType, properties: Properties, *args) -> None:
7476
self.records.append(DataRecord(record_type, prop_hash, args))
7577

7678
def replay(self, backend: Backend) -> None:
77-
properties = Properties()
79+
current_props = Properties()
80+
props = self.properties
7881
draw = {
7982
RecordType.POLYLINE: backend.draw_polyline,
8083
RecordType.FILLED_POLYGON: backend.draw_filled_polygon_buffer,
8184
RecordType.OUTLINE_POLYGON: backend.draw_outline_polygon_buffer,
8285
}
8386
for record in self.records:
84-
properties = self.properties.get(record.property_hash, properties)
85-
draw[record.type](properties, *record.args) # type: ignore
87+
current_props = props.get(record.property_hash, current_props)
88+
draw[record.type](current_props, *record.args) # type: ignore

src/ezdxf/addons/hpgl2/dxf_backend.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def draw_polyline(self, properties: Properties, points: Sequence[Vec2]) -> None:
6363
self.layout.add_point(points[0], dxfattribs=attribs)
6464

6565
def draw_filled_polygon_buffer(
66-
self, properties: Properties, paths: Sequence[Path], fill_method: int
66+
self, properties: Properties, paths: Sequence[Path]
6767
) -> None:
6868
attribs = self.make_dxf_attribs(properties)
6969
# max sagitta distance of 10 plu = 0.25 mm
@@ -73,6 +73,8 @@ def draw_filled_polygon_buffer(
7373
for hatch in hatches:
7474
assert isinstance(hatch, Hatch)
7575
rgb: Optional[RGB] = properties.pen_color
76+
if self.color_mode == ColorMode.ACI:
77+
rgb = RGB_NONE
7678
if self.map_black_rgb_to_white_rgb and rgb == BLACK_RGB:
7779
rgb = WHITE_RGB
7880
if rgb is RGB_NONE:

src/ezdxf/addons/hpgl2/interpreter.py

Lines changed: 12 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,76 +14,24 @@ class Interpreter:
1414
def __init__(self, plotter: Plotter) -> None:
1515
self.errors: list[str] = []
1616
self.not_implemented_commands: set[str] = set()
17+
self._disabled_commands: set[str] = set()
1718
self.plotter = plotter
18-
plotter.reset()
1919

2020
def add_error(self, error: str) -> None:
2121
self.errors.append(error)
2222

2323
def run(self, commands: list[Command]) -> None:
2424
for name, args in commands:
25+
if name in self._disabled_commands:
26+
continue
2527
method = getattr(self, f"cmd_{name.lower()}", None)
2628
if method:
2729
method(args)
2830
elif name[0] in string.ascii_letters:
2931
self.not_implemented_commands.add(name)
3032

31-
def cmd_escape(self, args: list[bytes]):
32-
"""Embedded PCL5 commands."""
33-
if len(args):
34-
self.plotter.execute_pcl5_command(args[0])
35-
36-
def cmd_pg(self, _):
37-
"""Advance full page."""
38-
self.plotter.advance_full_page()
39-
40-
def cmd_rp(self, _):
41-
"""Replot."""
42-
self.plotter.replot()
43-
44-
def cmd_wu(self, _):
45-
"""Pen width unit selection - not supported."""
46-
pass
47-
48-
def cmd_la(self, _):
49-
"""Line attribute selection (line ends and line joins) - not supported."""
50-
pass
51-
52-
def cmd_rf(self, _):
53-
"""Define raster fill - not supported."""
54-
pass
55-
56-
def cmd_tr(self, _):
57-
"""Set transparency mode - not supported. Transparency mode is off, white
58-
fillings cover graphics beneath.
59-
"""
60-
pass
61-
62-
def cmd_ro(self, args: list[bytes]):
63-
"""Set rotation."""
64-
angle: int = 0
65-
if len(args):
66-
angle = to_int(args[0], angle)
67-
if angle in (0, 90, 180, 270):
68-
self.plotter.rotate_coordinate_system(angle)
69-
else:
70-
self.add_error(f"invalid rotation angle {angle} for RO command")
71-
72-
def cmd_in(self, _):
73-
"""Initialize plotter."""
74-
self.plotter.initialize()
75-
76-
def cmd_df(self, _):
77-
"""Reset to defaults."""
78-
self.plotter.defaults()
79-
80-
def cmd_ps(self, args: list[bytes]):
81-
"""Set page size in plotter units - not documented."""
82-
values = tuple(to_ints(args))
83-
if len(values) != 2:
84-
self.add_error("invalid arguments for command PS")
85-
return
86-
self.plotter.setup_page(values[0], values[1])
33+
def disable_commands(self, commands: Iterable[str]) ->None:
34+
self._disabled_commands.update(commands)
8735

8836
# Configure pens, line types, fill types
8937
def cmd_ft(self, args: list[bytes]):
@@ -402,7 +350,7 @@ def cmd_fp(self, args: list[bytes]) -> None:
402350
"""Plot filled polygon."""
403351
fill_method = 0
404352
if len(args):
405-
fill_method = to_int(args[0], fill_method)
353+
fill_method = one_of(to_int(args[0], fill_method), (0, 1))
406354
self.plotter.fill_polygon(fill_method)
407355

408356
def cmd_ep(self, _) -> None:
@@ -456,3 +404,9 @@ def to_int(s: bytes, default=0) -> int:
456404

457405
def clamp(v, v_min, v_max):
458406
return max(min(v_max, v), v_min)
407+
408+
409+
def one_of(value, choice):
410+
if value in choice:
411+
return value
412+
return choice[0]

src/ezdxf/addons/hpgl2/page.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
MM_TO_PLU = 40
1515

1616

17-
1817
class Page:
1918
def __init__(self, size_x: int, size_y: int):
2019
self.size_x = int(size_x) # in plotter units (plu)
@@ -26,12 +25,16 @@ def __init__(self, size_x: int, size_y: int):
2625
self.user_scale_x: float = 1.0
2726
self.user_scale_y: float = 1.0
2827
self.user_origin = NULLVEC2 # plu
28+
self.rotation: int = 0
2929

3030
def set_scaling_points(self, p1: Vec2, p2: Vec2) -> None:
3131
self.reset_scaling()
3232
self.p1 = Vec2(p1)
3333
self.p2 = Vec2(p2)
3434

35+
def apply_scaling_factors(self, sx: float, sy: float) -> None:
36+
self.set_ucs(self.user_origin, self.user_scale_x * sx, self.user_scale_y * sy)
37+
3538
def set_scaling_points_relative_1(self, xp1: float, yp1: float) -> None:
3639
size = self.p2 - self.p1
3740
p1 = Vec2(self.size_x * xp1, self.size_y * yp1)
@@ -110,7 +113,7 @@ def set_ucs(self, origin: Vec2, sx: float = 1.0, sy: float = 1.0):
110113

111114
def set_rotation(self, angle: int) -> None:
112115
"""Page rotation is not supported."""
113-
pass
116+
self.rotation = angle
114117

115118
def page_point(self, x: float, y: float) -> Vec2:
116119
"""Returns the page location as page point in plotter units."""
@@ -121,6 +124,8 @@ def page_vector(self, x: float, y: float) -> Vec2:
121124
if self.user_scaling:
122125
x = self.user_scale_x * x
123126
y = self.user_scale_y * y
127+
if self.rotation:
128+
return Vec2(x, y).rotate_deg(self.rotation)
124129
return Vec2(x, y)
125130

126131
def page_points(self, points: Sequence[Vec2]) -> list[Vec2]:

0 commit comments

Comments
 (0)