521 lines
15 KiB
Python
521 lines
15 KiB
Python
import math
|
|
from affine import Affine
|
|
from boxes.extents import Extents
|
|
|
|
EPS = 1e-4
|
|
PADDING = 10
|
|
|
|
RANDOMIZE_COLORS = False # enable to ease check for continuity of pathes
|
|
|
|
|
|
def points_equal(x1, y1, x2, y2):
|
|
return abs(x1 - x2) < EPS and abs(y1 - y2) < EPS
|
|
|
|
|
|
def pdiff(p1, p2):
|
|
x1, y1 = p1
|
|
x2, y2 = p2
|
|
return (x1 - x2, y1 - y2)
|
|
|
|
|
|
class Surface:
|
|
def __init__(self, fname, width, height):
|
|
self._fname = fname
|
|
self.parts = []
|
|
self._p = self.new_part("default")
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
def finish(self):
|
|
pass
|
|
|
|
def render(self, renderer):
|
|
renderer.init(**self.args)
|
|
for p in self.parts:
|
|
p.render(renderer)
|
|
renderer.finish()
|
|
|
|
def move_offset(self, dx, dy):
|
|
for p in self.parts:
|
|
p.move_offset(dx, dy)
|
|
|
|
def new_part(self, name="part"):
|
|
if self.parts and len(self.parts[-1].pathes) == 0:
|
|
return self._p
|
|
p = Part(name)
|
|
self.parts.append(p)
|
|
self._p = p
|
|
return p
|
|
|
|
def append(self, *path):
|
|
self._p.append(*path)
|
|
|
|
def stroke(self, **params):
|
|
return self._p.stroke(**params)
|
|
|
|
def move_to(self, *xy):
|
|
self._p.move_to(*xy)
|
|
|
|
def extents(self):
|
|
if not self.parts:
|
|
return Extents()
|
|
return sum([p.extents() for p in self.parts])
|
|
|
|
|
|
class Part:
|
|
def __init__(self, name):
|
|
self.pathes = []
|
|
self.path = []
|
|
|
|
def extents(self):
|
|
if not self.pathes:
|
|
return Extents()
|
|
return sum([p.extents() for p in self.pathes])
|
|
|
|
def move_offset(self, dx, dy):
|
|
assert(not self.path)
|
|
for p in self.pathes:
|
|
p.move_offset(dx, dy)
|
|
|
|
def append(self, *path):
|
|
self.path.append(list(path))
|
|
|
|
def stroke(self, **params):
|
|
if len(self.path) == 0:
|
|
return
|
|
# search for path ending at new start coordinates to append this path to
|
|
xy0 = self.path[0][1:3]
|
|
for p in reversed(self.pathes):
|
|
if self.path[0][0] == "T":
|
|
break
|
|
xy1 = p.path[-1][1:3]
|
|
if points_equal(*xy0, *xy1):
|
|
# todo: check for same color and linewidth
|
|
p.path.extend(self.path[1:])
|
|
self.path = []
|
|
return p
|
|
p = Path(self.path, params)
|
|
self.pathes.append(p)
|
|
self.path = []
|
|
return p
|
|
|
|
def move_to(self, *xy):
|
|
if len(self.path) == 0:
|
|
self.path.append(["M", *xy])
|
|
elif self.path[-1][0] == "M":
|
|
self.path[-1] = ["M", *xy]
|
|
else:
|
|
xy0 = self.path[-1][1:3]
|
|
if not points_equal(*xy0, *xy):
|
|
self.path.append(["M", *xy])
|
|
|
|
|
|
class Path:
|
|
def __init__(self, path, params):
|
|
self.path = path
|
|
self.params = params
|
|
# self._extents = None
|
|
|
|
def __repr__(self):
|
|
l = len(self.path)
|
|
# x1,y1 = self.path[0][1:3]
|
|
x2, y2 = self.path[-1][1:3]
|
|
return f"Path[{l}] to ({x2:.2f},{y2:.2f})"
|
|
|
|
def extents(self):
|
|
# if self._extents is not None: return self._extents
|
|
e = Extents()
|
|
for p in self.path:
|
|
e.add(*p[1:3])
|
|
return e
|
|
|
|
def move_offset(self, dx, dy):
|
|
for c in self.path:
|
|
C = c[0]
|
|
c[1] += dx
|
|
c[2] += dy
|
|
if C == 'C':
|
|
c[3] += dx
|
|
c[4] += dy
|
|
c[5] += dx
|
|
c[6] += dy
|
|
|
|
def faster_edges(self):
|
|
for (i, p) in enumerate(self.path):
|
|
if p[0] == "C" and i > 1 and i < len(self.path) - 1:
|
|
if self.path[i - 1][0] == "L" and self.path[i + 1][0] == "L":
|
|
p11 = self.path[i - 2][1:3]
|
|
p12 = self.path[i - 1][1:3]
|
|
p21 = p[1:3]
|
|
p22 = self.path[i + 1][1:3]
|
|
if (((p12[0]-p21[0])**2 + (p12[1]-p21[1])**2) >
|
|
self.params["lw"]**2):
|
|
continue
|
|
lines_intersect, x, y = line_intersection((p11, p12), (p21, p22))
|
|
if lines_intersect:
|
|
self.path[i - 1] = ("L", x, y)
|
|
self.path[i] = ("C", x, y, *p12, *p21)
|
|
|
|
|
|
class Context:
|
|
def __init__(self, surface, *al, **ad):
|
|
self._renderer = self._dwg = surface
|
|
|
|
self._bounds = Extents()
|
|
self._padding = PADDING
|
|
|
|
self._stack = []
|
|
self._m = Affine.translation(0, 0)
|
|
self._xy = (0, 0)
|
|
self._mxy = self._m * self._xy
|
|
self._lw = 0
|
|
self._rgb = (0, 0, 0)
|
|
self._ff = "sans-serif"
|
|
self._last_path = None
|
|
|
|
def _update_bounds_(self, mx, my):
|
|
self._bounds.update(mx, my)
|
|
|
|
def save(self):
|
|
self._stack.append(
|
|
(self._m, self._xy, self._lw, self._rgb, self._mxy, self._last_path)
|
|
)
|
|
self._xy = (0, 0)
|
|
|
|
def restore(self):
|
|
(
|
|
self._m,
|
|
self._xy,
|
|
self._lw,
|
|
self._rgb,
|
|
self._mxy,
|
|
self._last_path,
|
|
) = self._stack.pop()
|
|
|
|
## transformations
|
|
|
|
def translate(self, x, y):
|
|
self._m *= Affine.translation(x, y)
|
|
self._xy = (0, 0)
|
|
|
|
def scale(self, sx, sy):
|
|
self._m *= Affine.scale(sx, sy)
|
|
|
|
def rotate(self, r):
|
|
self._m *= Affine.rotation(180 * r / math.pi)
|
|
|
|
def set_line_width(self, lw):
|
|
self._lw = lw
|
|
|
|
def set_source_rgb(self, r, g, b):
|
|
self._rgb = (r, g, b)
|
|
|
|
## path methods
|
|
|
|
def _line_to(self, x, y):
|
|
self._add_move()
|
|
x1, y1 = self._mxy
|
|
self._xy = x, y
|
|
x2, y2 = self._mxy = self._m * self._xy
|
|
if not points_equal(x1, y1, x2, y2):
|
|
self._dwg.append("L", x2, y2)
|
|
|
|
def _add_move(self):
|
|
self._dwg.move_to(*self._mxy)
|
|
|
|
def move_to(self, x, y):
|
|
self._xy = (x, y)
|
|
self._mxy = self._m * self._xy
|
|
|
|
def line_to(self, x, y):
|
|
self._line_to(x, y)
|
|
|
|
def _arc(self, xc, yc, radius, angle1, angle2, direction):
|
|
x1, y1 = radius * math.cos(angle1) + xc, radius * math.sin(angle1) + yc
|
|
x4, y4 = radius * math.cos(angle2) + xc, radius * math.sin(angle2) + yc
|
|
|
|
# XXX direction seems not needed for small arcs
|
|
ax = x1 - xc
|
|
ay = y1 - yc
|
|
bx = x4 - xc
|
|
by = y4 - yc
|
|
q1 = ax * ax + ay * ay
|
|
q2 = q1 + ax * bx + ay * by
|
|
k2 = 4/3 * ((2 * q1 * q2)**0.5 - q2) / (ax * by - ay * bx)
|
|
|
|
x2 = xc + ax - k2 * ay
|
|
y2 = yc + ay + k2 * ax
|
|
x3 = xc + bx + k2 * by
|
|
y3 = yc + by - k2 * bx
|
|
|
|
mx1, my1 = self._m * (x1, y1)
|
|
mx2, my2 = self._m * (x2, y2)
|
|
mx3, my3 = self._m * (x3, y3)
|
|
mx4, my4 = self._m * (x4, y4)
|
|
mxc, myc = self._m * (xc, yc)
|
|
|
|
self._add_move()
|
|
self._dwg.append("C", mx4, my4, mx2, my2, mx3, my3)
|
|
self._xy = (x4, y4)
|
|
self._mxy = (mx4, my4)
|
|
|
|
def arc(self, xc, yc, radius, angle1, angle2):
|
|
self._arc(xc, yc, radius, angle1, angle2, 1)
|
|
|
|
def arc_negative(self, xc, yc, radius, angle1, angle2):
|
|
self._arc(xc, yc, radius, angle1, angle2, -1)
|
|
|
|
def curve_to(self, x1, y1, x2, y2, x3, y3):
|
|
# mx0,my0 = self._m*self._xy
|
|
mx1, my1 = self._m * (x1, y1)
|
|
mx2, my2 = self._m * (x2, y2)
|
|
mx3, my3 = self._m * (x3, y3)
|
|
self._add_move()
|
|
self._dwg.append("C", mx3, my3, mx1, my1, mx2, my2) # destination first!
|
|
self._xy = (x3, y3)
|
|
|
|
def stroke(self):
|
|
# print('stroke stack-level=',len(self._stack),'lastpath=',self._last_path,)
|
|
self._last_path = self._dwg.stroke(rgb=self._rgb, lw=self._lw)
|
|
self._xy = (0, 0)
|
|
|
|
def fill(self):
|
|
self._xy = (0, 0)
|
|
raise NotImplementedError()
|
|
|
|
def select_font_face(self, ff):
|
|
self._ff = ff
|
|
|
|
def set_font_size(self, fs):
|
|
self._fs = fs
|
|
|
|
def show_text(self, text, **args):
|
|
params = {"ff": self._ff, "fs": self._fs, "lw": self._lw, "rgb": self._rgb}
|
|
params.update(args)
|
|
mx0, my0 = self._m * self._xy
|
|
self._dwg.append("T", mx0, my0, text, params)
|
|
|
|
def text_extents(self, text):
|
|
fs = self._fs
|
|
# XXX ugly hack! Fix Boxes.text() !
|
|
return (0, 0, 0.6 * fs * len(text), 0.65 * fs, fs * 0.1, 0)
|
|
|
|
def rectangle(self, x, y, width, height):
|
|
|
|
# todo: better check for empty path?
|
|
self.stroke()
|
|
|
|
self.move_to(x, y)
|
|
self.line_to(x + width, y)
|
|
self.line_to(x + width, y + height)
|
|
self.line_to(x, y + height)
|
|
self.line_to(x, y)
|
|
self.stroke()
|
|
|
|
def get_current_point(self):
|
|
return self._xy
|
|
|
|
def flush(self):
|
|
pass
|
|
# todo: check, if needed
|
|
# self.stroke()
|
|
|
|
## additional methods
|
|
def new_part(self):
|
|
self._dwg.new_part()
|
|
|
|
|
|
class SVGSurface(Surface):
|
|
def finish(self):
|
|
import svgwrite
|
|
|
|
extents = self.extents()
|
|
|
|
self.move_offset(-extents.xmin + PADDING,
|
|
-extents.ymin + PADDING)
|
|
w = extents.width + 2 * PADDING
|
|
h = extents.height + 2 * PADDING
|
|
|
|
dwg = svgwrite.Drawing(filename=self._fname, debug=False)
|
|
# dwg.debug = False
|
|
|
|
dwg["width"] = f"{w:.2f}mm"
|
|
dwg["height"] = f"{h:.2f}mm"
|
|
dwg["viewBox"] = f"0.0 0.0 {w:.2f} {h:.2f}"
|
|
|
|
for i, part in enumerate(self.parts):
|
|
if not part.pathes:
|
|
continue
|
|
g = dwg.add(
|
|
dwg.g(
|
|
id=f"p-{i}",
|
|
style="fill:none;stroke-linecap:round;stroke-linejoin:round;",
|
|
)
|
|
)
|
|
for j, path in enumerate(part.pathes):
|
|
p = []
|
|
x, y = 0, 0
|
|
path.faster_edges()
|
|
for c in path.path:
|
|
x0, y0 = x, y
|
|
C, x, y = c[0:3]
|
|
if C == "M":
|
|
p.append(f"M {x:.3f} {y:.3f}")
|
|
elif C == "L":
|
|
p.append(f"L {x:.3f} {y:.3f}")
|
|
elif C == "C":
|
|
x1, y1, x2, y2 = c[3:]
|
|
p.append(
|
|
f"C {x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x:.3f} {y:.3f}"
|
|
)
|
|
elif C == "T":
|
|
text, params = c[3:]
|
|
style = f"font: {params['ff']} ; fill: {rgb_to_svg_color(*params['rgb'])}"
|
|
g.add(
|
|
dwg.text(
|
|
text,
|
|
x=[x],
|
|
y=[y],
|
|
font_size=f"{params['fs']}px",
|
|
style=style,
|
|
)
|
|
)
|
|
else:
|
|
print("Unknown", c)
|
|
color = (
|
|
random_svg_color()
|
|
if RANDOMIZE_COLORS
|
|
else rgb_to_svg_color(*path.params["rgb"])
|
|
)
|
|
if p: # todo: might be empty since text is not implemented yet
|
|
g.add(
|
|
dwg.path(
|
|
d=" ".join(p), stroke=color, stroke_width=path.params["lw"]
|
|
)
|
|
)
|
|
dwg.save(pretty=True)
|
|
|
|
|
|
class PSSurface(Surface):
|
|
|
|
def finish(self):
|
|
|
|
extents = self.extents()
|
|
|
|
self.move_offset(-extents.xmin + PADDING,
|
|
-extents.ymin + PADDING)
|
|
|
|
w = extents.width + 2 * PADDING
|
|
h = extents.height + 2 * PADDING
|
|
|
|
f = open(self._fname, "w")
|
|
|
|
f.write("%!PS-Adobe-2.0\n")
|
|
f.write(
|
|
f"%%BoundingBox: 0 0 {w:.0f} {h:.0f}\n\n"
|
|
)
|
|
# f.write(f"%%DocumentMedia: \d+x\d+mm ((\d+) (\d+)) 0 \("
|
|
# dwg['width']=f'{w:.2f}mm'
|
|
# dwg['height']=f'{h:.2f}mm'
|
|
|
|
for i, part in enumerate(self.parts):
|
|
if not part.pathes:
|
|
continue
|
|
# g = dwg.add( dwg.g(id=f'p-{i}',style='fill:none;stroke-linecap:round;stroke-linejoin:round;') )
|
|
for j, path in enumerate(part.pathes):
|
|
p = []
|
|
x, y = 0, 0
|
|
path.faster_edges()
|
|
|
|
for c in path.path:
|
|
x0, y0 = x, y
|
|
C, x, y = c[0:3]
|
|
if C == "M":
|
|
p.append(f"{x:.3f} {y:.3f} moveto")
|
|
elif C == "L":
|
|
p.append(f"{x:.3f} {y:.3f} lineto")
|
|
elif C == "C":
|
|
x1, y1, x2, y2 = c[3:]
|
|
p.append(
|
|
f"{x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x:.3f} {y:.3f} curveto"
|
|
)
|
|
elif C == "T":
|
|
text, params = c[3:]
|
|
text = text.replace("(", "r\(").replace(")", r"\)")
|
|
color = " ".join((f"{c:.2f}"
|
|
for c in params["rgb"]))
|
|
f.write(f"/{params['ff']} findfont\n")
|
|
f.write(f"{params['fs']*72 / 25.4} scalefont\n")
|
|
f.write("setfont\n")
|
|
f.write(f"{color} setrgbcolor\n")
|
|
f.write(f"{x:.3f} {y:.3f} moveto\n")
|
|
f.write(f"({text}) show\n\n")
|
|
else:
|
|
print("Unknown", c)
|
|
color = (
|
|
random_svg_color()
|
|
if RANDOMIZE_COLORS
|
|
else rgb_to_svg_color(*path.params["rgb"])
|
|
)
|
|
if p: # todo: might be empty since text is not implemented yet
|
|
color = " ".join((f"{c:.2f}"
|
|
for c in path.params["rgb"]))
|
|
f.write("newpath\n")
|
|
f.write("\n".join(p))
|
|
f.write("\n")
|
|
f.write(f"{path.params['lw']} setlinewidth\n")
|
|
f.write(f"{color} setrgbcolor\n")
|
|
f.write("stroke\n\n")
|
|
f.write(
|
|
"""
|
|
showpage
|
|
%%Trailer
|
|
%%EOF
|
|
"""
|
|
)
|
|
f.close()
|
|
|
|
|
|
from random import random
|
|
|
|
|
|
def random_svg_color():
|
|
r, g, b = random(), random(), random()
|
|
return f"rgb({r*255:.0f},{g*255:.0f},{b*255:.0f})"
|
|
|
|
|
|
def rgb_to_svg_color(r, g, b):
|
|
return f"rgb({r*255:.0f},{g*255:.0f},{b*255:.0f})"
|
|
|
|
|
|
def line_intersection(line1, line2):
|
|
|
|
xdiff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0])
|
|
ydiff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1])
|
|
|
|
def det(a, b):
|
|
return a[0] * b[1] - a[1] * b[0]
|
|
|
|
div = det(xdiff, ydiff)
|
|
if div == 0:
|
|
# todo: deal with paralel line intersection / overlay
|
|
return False, None, None
|
|
|
|
d = (det(*line1), det(*line2))
|
|
x = det(d, xdiff) / div
|
|
y = det(d, ydiff) / div
|
|
|
|
on_segments = (
|
|
(x + EPS >= min(line1[0][0], line1[1][0])),
|
|
(x + EPS >= min(line2[0][0], line2[1][0])),
|
|
(x - EPS <= max(line1[0][0], line1[1][0])),
|
|
(x - EPS <= max(line2[0][0], line2[1][0])),
|
|
(y + EPS >= min(line1[0][1], line1[1][1])),
|
|
(y + EPS >= min(line2[0][1], line2[1][1])),
|
|
(y - EPS <= max(line1[0][1], line1[1][1])),
|
|
(y - EPS <= max(line2[0][1], line2[1][1])),
|
|
)
|
|
|
|
return min(on_segments), x, y
|