381 lines
12 KiB
Python
381 lines
12 KiB
Python
#!/usr/bin/env python
|
|
|
|
import cairo
|
|
import numpy as np
|
|
from HersheyFonts import HersheyFonts
|
|
|
|
|
|
class Paper:
|
|
def __init__(self, width, height, margins=None):
|
|
self.width = width
|
|
self.height = height
|
|
self.margins = {
|
|
'top': 0,
|
|
'right': 0,
|
|
'bottom': 0,
|
|
'left': 0
|
|
}
|
|
self.set_margins(margins)
|
|
self.background_colour = None
|
|
|
|
def set_margins(self, margins=None, top=None, right=None, bottom=None,
|
|
left=None):
|
|
if isinstance(margins, int) or isinstance(margins, float):
|
|
self.margins['top'] = margins
|
|
self.margins['right'] = margins
|
|
self.margins['bottom'] = margins
|
|
self.margins['left'] = margins
|
|
elif isinstance(margins, list) and (isinstance(margins[0], int)
|
|
or isinstance(margins[0], float)):
|
|
if len(margins) == 2:
|
|
self.margins['top'] = margins[0]
|
|
self.margins['bottom'] = margins[0]
|
|
self.margins['left'] = margins[1]
|
|
self.margins['right'] = margins[1]
|
|
elif len(margins) == 3:
|
|
self.margins['top'] = margins[0]
|
|
self.margins['left'] = margins[1]
|
|
self.margins['right'] = margins[1]
|
|
self.margins['bottom'] = margins[2]
|
|
elif len(margins) == 4:
|
|
self.margins['top'] = margins[0]
|
|
self.margins['right'] = margins[1]
|
|
self.margins['bottom'] = margins[2]
|
|
self.margins['left'] = margins[3]
|
|
elif margins is not None:
|
|
self.margins = margins
|
|
|
|
if top is not None:
|
|
self.margins['top'] = top
|
|
if right is not None:
|
|
self.margins['right'] = right
|
|
if bottom is not None:
|
|
self.margins['bottom'] = bottom
|
|
if left is not None:
|
|
self.margins['left'] = left
|
|
|
|
@property
|
|
def content_width(self):
|
|
return self.width - self.margins['left'] - self.margins['right']
|
|
|
|
@property
|
|
def content_height(self):
|
|
return self.height - self.margins['top'] - self.margins['bottom']
|
|
|
|
def size(self, honour_margins=True):
|
|
if honour_margins:
|
|
return np.array([self.content_width, self.content_height])
|
|
else:
|
|
return np.array([self.width, self.height])
|
|
|
|
def is_within(self, point, honour_margins=True):
|
|
return ((self.left(honour_margins) < point[0])
|
|
and (point[0] < self.right(honour_margins))
|
|
and (self.top(honour_margins) < point[1])
|
|
and (point[1] < self.bottom(honour_margins)))
|
|
|
|
def top_left(self, honour_margins=True):
|
|
if honour_margins:
|
|
return np.array([self.margins['left'],
|
|
self.margins['top']])
|
|
else:
|
|
return np.array([0, 0])
|
|
|
|
def bottom_left(self, honour_margins=True):
|
|
if honour_margins:
|
|
return np.array([self.margins['left'],
|
|
self.height - self.margins['bottom']])
|
|
else:
|
|
return np.array([0, self.height])
|
|
|
|
def top_right(self, honour_margins=True):
|
|
if honour_margins:
|
|
return np.array([self.width - self.margins['right'],
|
|
self.margins['top']])
|
|
else:
|
|
return np.array([self.width, 0])
|
|
|
|
def bottom_right(self, honour_margins=True):
|
|
if honour_margins:
|
|
return np.array([self.width - self.margins['right'],
|
|
self.height - self.margins['bottom']])
|
|
else:
|
|
return np.array([self.width, self.height])
|
|
|
|
def centre(self, honour_margins=True):
|
|
return (self.bottom_left(honour_margins)
|
|
+ self.top_right(honour_margins)) / 2
|
|
|
|
def top(self, honour_margins=True):
|
|
if honour_margins:
|
|
return self.margins['top']
|
|
else:
|
|
return 0
|
|
|
|
def left(self, honour_margins=True):
|
|
if honour_margins:
|
|
return self.margins['left']
|
|
else:
|
|
return 0
|
|
|
|
def right(self, honour_margins=True):
|
|
if honour_margins:
|
|
return self.width - self.margins['right']
|
|
else:
|
|
return self.width
|
|
|
|
def bottom(self, honour_margins=True):
|
|
if honour_margins:
|
|
return self.height - self.margins['bottom']
|
|
else:
|
|
return self.height
|
|
|
|
|
|
A4_PORTRAIT = Paper(210, 297, 20)
|
|
A4_LANDSCAPE = Paper(297, 210, 20)
|
|
A6_PORTRAIT = Paper(105, 148, 8)
|
|
A6_LANDSCAPE = Paper(148, 105, 8)
|
|
|
|
|
|
def _measure_text(hf, text):
|
|
points = np.array([point for stroke in hf.strokes_for_text(text)
|
|
for point in stroke])
|
|
return {
|
|
'x': points[:, 0].min(),
|
|
'y': points[:, 1].min(),
|
|
'width': points[:, 0].max() - points[:, 0].min(),
|
|
'height': points[:, 1].max() - points[:, 1].min(),
|
|
}
|
|
|
|
|
|
class Plotter:
|
|
|
|
def __init__(self, paper, line_width=0, colour=[0, 0, 0, 1]):
|
|
self.paper = paper
|
|
self.pen_down = False
|
|
self.position = np.array([0, 0])
|
|
|
|
self.layers = []
|
|
self.add_layer(colour, line_width)
|
|
|
|
def add_layer(self, colour=[0, 0, 0, 1], line_width=0.5, name='',
|
|
switch=True):
|
|
self.layers.append({
|
|
'line_width': line_width,
|
|
'colour': colour,
|
|
'name': name
|
|
})
|
|
if switch:
|
|
self.switch_layer(len(self.layers) - 1)
|
|
return len(self.layers) - 1
|
|
|
|
def switch_layer(self, layer):
|
|
self.current_layer = layer
|
|
|
|
def move_to(self, point):
|
|
self.pen_down = False
|
|
self.position = point
|
|
|
|
def line_to(self, point):
|
|
self.pen_down = True
|
|
self.position = point
|
|
|
|
def draw_frame(self):
|
|
self.move_to(self.paper.bottom_left())
|
|
self.line_to(self.paper.bottom_right())
|
|
self.line_to(self.paper.top_right())
|
|
self.line_to(self.paper.top_left())
|
|
self.line_to(self.paper.bottom_left())
|
|
|
|
def draw_text(self, text, position, font='futural', height=10, x_scale=1,
|
|
fit_width=None, fixed_height=True, spacing=None,
|
|
v_align='baseline', h_align='left'):
|
|
x, y = position
|
|
hf = HersheyFonts()
|
|
hf.load_default_font(font)
|
|
hf.normalize_rendering(height)
|
|
hf.render_options['scaley'] *= -1
|
|
hf.render_options['scalex'] *= x_scale
|
|
if spacing is not None:
|
|
hf.render_options['spacing'] = spacing
|
|
|
|
hf.render_options['yofs'] -= hf.render_options['base_line']
|
|
|
|
metrics = _measure_text(hf, text)
|
|
|
|
if fit_width is not None:
|
|
hf.render_options['scalex'] *= fit_width / metrics['width']
|
|
hf.render_options['spacing'] *= fit_width / metrics['width']
|
|
metrics['x'] *= fit_width / metrics['width']
|
|
if not fixed_height:
|
|
hf.render_options['scaley'] *= fit_width / metrics['width']
|
|
metrics['y'] *= fit_width / text_width
|
|
metrics['height'] *= fit_width / metrics['width']
|
|
metrics['width'] = fit_width
|
|
|
|
if h_align == 'centre':
|
|
x -= metrics['width'] / 2
|
|
elif h_align == 'right':
|
|
x -= metrics['width']
|
|
|
|
x -= metrics['x']
|
|
|
|
if v_align == 'bottom':
|
|
y -= metrics['y'] + metrics['height']
|
|
elif v_align == 'top':
|
|
y -= metrics['y']
|
|
|
|
for s in hf.strokes_for_text(text):
|
|
self.move_to((s[0][0] + x, s[0][1] + y))
|
|
for p in s[1:]:
|
|
self.line_to((p[0] + x, p[1] + y))
|
|
|
|
|
|
class SVGPlotter(Plotter):
|
|
|
|
def __init__(self, file_name, paper, line_width=0.5, colour=[0, 0, 0, 1]):
|
|
dpi = 72
|
|
width = paper.width / 25.4 * dpi
|
|
height = paper.height / 25.4 * dpi
|
|
self.file_name = file_name
|
|
self.surface = cairo.SVGSurface(file_name, width, height)
|
|
self.context = cairo.Context(self.surface)
|
|
self.context.scale(dpi / 25.4, dpi / 25.4)
|
|
self.context.set_line_cap(cairo.LINE_CAP_ROUND)
|
|
self.context.set_line_join(cairo.LINE_JOIN_ROUND)
|
|
|
|
if paper.background_colour is not None:
|
|
self.context.set_source_rgba(*paper.background_colour)
|
|
self.context.rectangle(0, 0, width, height)
|
|
self.context.fill()
|
|
|
|
super().__init__(paper, line_width, colour)
|
|
|
|
def move_to(self, point):
|
|
if self.pen_down:
|
|
self.context.stroke()
|
|
self.context.move_to(point[0], point[1])
|
|
super().move_to(point)
|
|
|
|
def line_to(self, point):
|
|
self.context.line_to(point[0], point[1])
|
|
super().line_to(point)
|
|
|
|
def finalise(self):
|
|
if self.pen_down:
|
|
self.context.stroke()
|
|
self.pen_down = False
|
|
self.surface.finish()
|
|
|
|
def switch_layer(self, layer):
|
|
if self.pen_down:
|
|
self.context.stroke()
|
|
|
|
super().switch_layer(layer)
|
|
self.context.set_source_rgba(
|
|
*self.layers[self.current_layer]['colour'])
|
|
self.context.set_line_width(
|
|
self.layers[self.current_layer]['line_width'])
|
|
|
|
return len(self.layers) - 1
|
|
|
|
|
|
class HPGLPlotter(Plotter):
|
|
|
|
def __init__(self, paper, file_name_pattern):
|
|
self.file_name_pattern = file_name_pattern
|
|
self.resolution = (4000 / 81.5, 4000 / 80.5)
|
|
self.file = None
|
|
super().__init__(paper)
|
|
|
|
def _layer_file_name(self, index):
|
|
return self.file_name_pattern.format(
|
|
index=index,
|
|
name=self.layers[index]['name'],
|
|
colour=self.layers[index]['colour']
|
|
)
|
|
|
|
def _mm_to_plotter(self, mm):
|
|
p = (mm[0] * self.resolution[0],
|
|
(self.paper.height - mm[1]) * self.resolution[1])
|
|
return f'{p[0]:.0f},{p[1]:.0f}'
|
|
|
|
def move_to(self, point):
|
|
if self.pen_down:
|
|
self.file.write(';PU')
|
|
else:
|
|
self.file.write(',')
|
|
self.file.write(self._mm_to_plotter(point))
|
|
return super().move_to(point)
|
|
|
|
def line_to(self, point):
|
|
if not self.pen_down:
|
|
self.file.write(';PD')
|
|
else:
|
|
self.file.write(',')
|
|
self.file.write(self._mm_to_plotter(point))
|
|
return super().line_to(point)
|
|
|
|
def add_layer(self, colour='black', line_width=0.5, name='', switch=True):
|
|
new_index = super().add_layer(colour=colour, line_width=line_width,
|
|
name=name, switch=False)
|
|
layer_file = open(self._layer_file_name(new_index), 'wt')
|
|
|
|
layer_file.write('IN;PU0,0')
|
|
|
|
if switch:
|
|
if self.file:
|
|
self.file.close()
|
|
self.file = layer_file
|
|
self.current_layer = len(self.layers) - 1
|
|
else:
|
|
layer_file.close()
|
|
|
|
return len(self.layers) - 1
|
|
|
|
def switch_layer(self, layer):
|
|
if self.pen_down:
|
|
self.file.write(';PU')
|
|
self.pen_down = False
|
|
self.file.close()
|
|
super().switch_layer(layer)
|
|
self.file = open(self._layer_file_name(self.current_layer), 'at')
|
|
|
|
def finalise(self):
|
|
self.file.close()
|
|
for i in range(len(self.layers)):
|
|
with open(self._layer_file_name(i), 'at') as file:
|
|
file.write(';PU0,0;IN')
|
|
|
|
|
|
class MultiPlotter(Plotter):
|
|
def __init__(self):
|
|
self.plotters = []
|
|
|
|
def move_to(self, point):
|
|
for plotter in self.plotters:
|
|
plotter.move_to(point)
|
|
|
|
def line_to(self, point):
|
|
for plotter in self.plotters:
|
|
plotter.line_to(point)
|
|
|
|
def add_layer(self, *args, **kwargs):
|
|
for plotter in self.plotters:
|
|
plotter.add_layer(*args, **kwargs)
|
|
|
|
def switch_layer(self, *args, **kwargs):
|
|
for plotter in self.plotters:
|
|
plotter.switch_layer(*args, **kwargs)
|
|
|
|
def finalise(self):
|
|
for plotter in self.plotters:
|
|
plotter.finalise()
|
|
|
|
def register_plotter(self, plotter):
|
|
self.plotters.append(plotter)
|
|
|
|
@property
|
|
def paper(self):
|
|
return self.plotters[0].paper
|