Basic SVG export seems to be working
This commit is contained in:
parent
f09ef6f117
commit
44006784f0
8 changed files with 158 additions and 71 deletions
|
|
@ -118,14 +118,15 @@ class ApertureMacro:
|
|||
primitive_defs = [ prim.to_gerber(unit) for prim in self.primitives ]
|
||||
return '*\n'.join(comments + variable_defs + primitive_defs)
|
||||
|
||||
def to_graphic_primitives(self, offset, rotation:'radians', parameters : [float], unit=None):
|
||||
def to_graphic_primitives(self, offset, rotation, parameters : [float], unit=None):
|
||||
variables = dict(self.variables)
|
||||
for number, value in enumerate(parameters):
|
||||
if i in variables:
|
||||
raise SyntaxError(f'Re-definition of aperture macro variable {i} through parameter {value}')
|
||||
variables[i] = value
|
||||
|
||||
return [ primitive.to_graphic_primitives(offset, rotation, variables, unit) for primitive in self.primitives ]
|
||||
for primitive in self.primitives:
|
||||
yield from primitive.to_graphic_primitives(offset, rotation, variables, unit)
|
||||
|
||||
def rotated(self, angle):
|
||||
dup = copy.deepcopy(self)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ class Primitive:
|
|||
attrs = ','.join(str(getattr(self, name)).strip('<>') for name in type(self).__annotations__)
|
||||
return f'<{type(self).__name__} {attrs}>'
|
||||
|
||||
@contextlib.contextmanager
|
||||
class Calculator:
|
||||
def __init__(self, instance, variable_binding={}, unit=None):
|
||||
self.instance = instance
|
||||
|
|
@ -91,10 +90,10 @@ class Circle(Primitive):
|
|||
self.rotation = ConstantExpression(0)
|
||||
|
||||
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None):
|
||||
with self.Calculator(variable_binding, unit) as calc:
|
||||
with self.Calculator(self, variable_binding, unit) as calc:
|
||||
x, y = gp.rotate_point(calc.x, calc.y, deg_to_rad(calc.rotation) + rotation, 0, 0)
|
||||
x, y = x+offset[0], y+offset[1]
|
||||
return [ gp.Circle(x, y, calc.r, polarity_dark=bool(calc.exposure)) ]
|
||||
return [ gp.Circle(x, y, calc.diameter/2, polarity_dark=bool(calc.exposure)) ]
|
||||
|
||||
def dilate(self, offset, unit):
|
||||
self.diameter += UnitExpression(offset, unit)
|
||||
|
|
@ -110,7 +109,7 @@ class VectorLine(Primitive):
|
|||
rotation : Expression
|
||||
|
||||
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None):
|
||||
with self.Calculator(variable_binding, unit) as calc:
|
||||
with self.Calculator(self, variable_binding, unit) as calc:
|
||||
center_x = (calc.end_x + calc.start_x) / 2
|
||||
center_y = (calc.end_y + calc.start_y) / 2
|
||||
delta_x = calc.end_x - calc.start_x
|
||||
|
|
@ -137,8 +136,8 @@ class CenterLine(Primitive):
|
|||
y : UnitExpression
|
||||
rotation : Expression
|
||||
|
||||
def to_graphic_primitives(self, variable_binding={}, unit=None):
|
||||
with self.Calculator(variable_binding, unit) as calc:
|
||||
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None):
|
||||
with self.Calculator(self, variable_binding, unit) as calc:
|
||||
rotation += deg_to_rad(calc.rotation)
|
||||
x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
|
||||
x, y = x+offset[0], y+offset[1]
|
||||
|
|
@ -161,7 +160,7 @@ class Polygon(Primitive):
|
|||
rotation : Expression
|
||||
|
||||
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None):
|
||||
with self.Calculator(variable_binding, unit) as calc:
|
||||
with self.Calculator(self, variable_binding, unit) as calc:
|
||||
rotation += deg_to_rad(calc.rotation)
|
||||
x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
|
||||
x, y = x+offset[0], y+offset[1]
|
||||
|
|
@ -184,7 +183,7 @@ class Thermal(Primitive):
|
|||
rotation : Expression
|
||||
|
||||
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None):
|
||||
with self.Calculator(variable_binding, unit) as calc:
|
||||
with self.Calculator(self, variable_binding, unit) as calc:
|
||||
rotation += deg_to_rad(calc.rotation)
|
||||
x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
|
||||
x, y = x+offset[0], y+offset[1]
|
||||
|
|
@ -236,7 +235,7 @@ class Outline(Primitive):
|
|||
return f'{self.code},{self.exposure.to_gerber()},{len(self.coords)//2-1},{coords},{self.rotation.to_gerber()}'
|
||||
|
||||
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None):
|
||||
with self.Calculator(variable_binding, unit) as calc:
|
||||
with self.Calculator(self, variable_binding, unit) as calc:
|
||||
bound_coords = [ (calc(x)+offset[0], calc(y)+offset[1]) for x, y in self.coords ]
|
||||
bound_radii = [None] * len(bound_coords)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,16 @@ from . import graphic_primitives as gp
|
|||
|
||||
|
||||
def _flash_hole(self, x, y, unit=None):
|
||||
if self.hole_rect_h is not None:
|
||||
if getattr(self, 'hole_rect_h', None) is not None:
|
||||
return [*self.primitives(x, y, unit),
|
||||
Rectangle((x, y),
|
||||
gp.Rectangle((x, y),
|
||||
(self.convert(self.hole_dia, unit), self.convert(self.hole_rect_h, unit)),
|
||||
rotation=self.rotation, polarity_dark=False)]
|
||||
elif self.hole_dia is not None:
|
||||
return [*self.primitives(x, y, unit),
|
||||
gp.Circle(x, y, self.convert(self.hole_dia/2, unit), polarity_dark=False)]
|
||||
else:
|
||||
return self.primitives(x, y), Circle((x, y), self.hole_dia, polarity_dark=False)
|
||||
return self.primitives(x, y, unit)
|
||||
|
||||
def strip_right(*args):
|
||||
args = list(args)
|
||||
|
|
@ -246,8 +249,11 @@ class PolygonAperture(Aperture):
|
|||
rotation : float = 0
|
||||
hole_dia : Length(float) = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.n_vertices = int(self.n_vertices)
|
||||
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.RegularPolygon(x, y, self.convert(diameter, unit), n_vertices, rotation=self.rotation) ]
|
||||
return [ gp.RegularPolygon(x, y, self.convert(self.diameter, unit)/2, self.n_vertices, rotation=self.rotation) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<{self.n_vertices}-gon aperture d={self.diameter:.3}'
|
||||
|
|
@ -284,8 +290,9 @@ class ApertureMacroInstance(Aperture):
|
|||
return self.macro.name
|
||||
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ primitive.with_offset(x, y).rotated(self.rotation, cx=0, cy=0)
|
||||
for primitive in self.macro.to_graphic_primitives(self.parameters, unit=unit) ]
|
||||
return self.macro.to_graphic_primitives(
|
||||
offset=(x, y), rotation=self.rotation,
|
||||
parameters=self.parameters, unit=unit)
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
return replace(self, macro=self.macro.dilated(offset, unit))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
import math
|
||||
from dataclasses import dataclass, KW_ONLY, astuple, replace
|
||||
from dataclasses import dataclass, KW_ONLY, astuple, replace, fields
|
||||
|
||||
from . import graphic_primitives as gp
|
||||
from .gerber_statements import *
|
||||
|
|
@ -28,7 +28,7 @@ class GerberObject:
|
|||
return replace(self,
|
||||
**{
|
||||
f.name: convert(getattr(self, f.name), self.unit, unit)
|
||||
for f in fields(self)
|
||||
for f in fields(self) if type(f.type) is Length
|
||||
})
|
||||
|
||||
def _conv(self, value, unit):
|
||||
|
|
@ -113,8 +113,16 @@ class Region(GerberObject):
|
|||
self.poly.arc_centers.append(None)
|
||||
|
||||
def to_primitives(self, unit=None):
|
||||
self.poly.polarity_dark = polarity_dark
|
||||
yield self.poly.converted(unit)
|
||||
self.poly.polarity_dark = self.polarity_dark # FIXME: is this the right spot to do this?
|
||||
if unit == self.unit:
|
||||
yield self.poly
|
||||
else:
|
||||
conv_outline = [ (convert(x, self.unit, unit), convert(y, self.unit, unit))
|
||||
for x, y in self.poly.outline ]
|
||||
convert_entry = lambda entry: (entry[0], (convert(entry[1][0], self.unit, unit), convert(entry[1][1], self.unit, unit)))
|
||||
conv_arc = [ None if entry is None else convert_entry(entry) for entry in self.poly.arc_centers ]
|
||||
|
||||
yield gp.ArcPoly(conv_outline, conv_arc)
|
||||
|
||||
def to_statements(self, gs):
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
|
|
@ -258,7 +266,12 @@ class Arc(GerberObject):
|
|||
|
||||
def to_primitives(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
yield gp.Arc(*astuple(conv)[:7], width=self.aperture.equivalent_width(unit), polarity_dark=self.polarity_dark)
|
||||
yield gp.Arc(x1=conv.x1, y1=conv.y1,
|
||||
x2=conv.x2, y2=conv.y2,
|
||||
cx=conv.cx, cy=conv.cy,
|
||||
clockwise=self.clockwise,
|
||||
width=self.aperture.equivalent_width(unit),
|
||||
polarity_dark=self.polarity_dark)
|
||||
|
||||
def to_statements(self, gs):
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from dataclasses import dataclass, KW_ONLY, replace
|
|||
from .gerber_statements import *
|
||||
|
||||
|
||||
@dataclass
|
||||
class GraphicPrimitive:
|
||||
_ : KW_ONLY
|
||||
polarity_dark : bool = True
|
||||
|
|
@ -48,8 +49,8 @@ class Circle(GraphicPrimitive):
|
|||
def bounding_box(self):
|
||||
return ((self.x-self.r, self.y-self.r), (self.x+self.r, self.y+self.r))
|
||||
|
||||
def to_svg(self):
|
||||
return 'circle', (), dict(cx=x, cy=y, r=r)
|
||||
def to_svg(self, tag, color='black'):
|
||||
return tag('circle', cx=self.x, cy=self.y, r=self.r, style=f'fill: {color}')
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -73,8 +74,8 @@ class Obround(GraphicPrimitive):
|
|||
def bounding_box(self):
|
||||
return self.to_line().bounding_box()
|
||||
|
||||
def to_svg(self):
|
||||
return self.to_line().to_svg()
|
||||
def to_svg(self, tag, color='black'):
|
||||
return self.to_line().to_svg(tag, color)
|
||||
|
||||
|
||||
def arc_bounds(x1, y1, x2, y2, cx, cy, clockwise):
|
||||
|
|
@ -167,7 +168,10 @@ def point_line_distance(l1, l2, p):
|
|||
x1, y1 = l1
|
||||
x2, y2 = l2
|
||||
x0, y0 = p
|
||||
return abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1))/point_distance(l1, l2)
|
||||
length = point_distance(l1, l2)
|
||||
if math.isclose(length, 0):
|
||||
return point_distance(l1, p)
|
||||
return abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / length
|
||||
|
||||
def svg_arc(old, new, center, clockwise):
|
||||
r = point_distance(old, new)
|
||||
|
|
@ -183,14 +187,14 @@ class ArcPoly(GraphicPrimitive):
|
|||
# list of (x : float, y : float) tuples. Describes closed outline, i.e. first and last point are considered
|
||||
# connected.
|
||||
outline : [(float,)]
|
||||
# list of radii of segments, must be either None (all segments are straight lines) or same length as outline.
|
||||
# must be either None (all segments are straight lines) or same length as outline.
|
||||
# Straight line segments have None entry.
|
||||
arc_centers : [(float,)] = None
|
||||
|
||||
@property
|
||||
def segments(self):
|
||||
ol = self.outline
|
||||
return itertools.zip_longest(ol, ol[1:] + [ol[0]], self.arc_centers)
|
||||
return itertools.zip_longest(ol, ol[1:] + [ol[0]], self.arc_centers or [])
|
||||
|
||||
def bounding_box(self):
|
||||
bbox = (None, None), (None, None)
|
||||
|
|
@ -213,7 +217,7 @@ class ArcPoly(GraphicPrimitive):
|
|||
if len(self.outline) == 0:
|
||||
return
|
||||
|
||||
yield f'M {outline[0][0]:.6}, {outline[0][1]:.6}'
|
||||
yield f'M {self.outline[0][0]:.6}, {self.outline[0][1]:.6}'
|
||||
for old, new, arc in self.segments:
|
||||
if not arc:
|
||||
yield f'L {new[0]:.6} {new[1]:.6}'
|
||||
|
|
@ -221,8 +225,8 @@ class ArcPoly(GraphicPrimitive):
|
|||
clockwise, center = arc
|
||||
yield svg_arc(old, new, center, clockwise)
|
||||
|
||||
def to_svg(self):
|
||||
return 'path', [], {'d': ' '.join(self._path_d())}
|
||||
def to_svg(self, tag, color='black'):
|
||||
return tag('path', d=' '.join(self._path_d()), style=f'fill: {color}')
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -237,10 +241,9 @@ class Line(GraphicPrimitive):
|
|||
r = self.width / 2
|
||||
return add_bounds(Circle(self.x1, self.y1, r).bounding_box(), Circle(self.x2, self.y2, r).bounding_box())
|
||||
|
||||
def to_svg(self):
|
||||
return 'path', [], dict(
|
||||
d=f'M {self.x1:.6} {self.y1:.6} L {self.x2:.6} {self.y2:.6}',
|
||||
style=f'stroke-width: {self.width:.6}; stroke-linecap: round')
|
||||
def to_svg(self, tag, color='black'):
|
||||
return tag('path', d=f'M {self.x1:.6} {self.y1:.6} L {self.x2:.6} {self.y2:.6}',
|
||||
style=f'stroke: {color}; stroke-width: {self.width:.6}; stroke-linecap: round')
|
||||
|
||||
@dataclass
|
||||
class Arc(GraphicPrimitive):
|
||||
|
|
@ -272,11 +275,10 @@ class Arc(GraphicPrimitive):
|
|||
arc = arc_bounds(x1, y1, x2, y2, cx, cy, self.clockwise)
|
||||
return add_bounds(endpoints, arc) # FIXME add "include_center" switch
|
||||
|
||||
def to_svg(self):
|
||||
def to_svg(self, tag, color='black'):
|
||||
arc = svg_arc((self.x1, self.y1), (self.x2, self.y2), (self.cx, self.cy), self.clockwise)
|
||||
return 'path', [], dict(
|
||||
d=f'M {self.x1:.6} {self.y1:.6} {arc}',
|
||||
style=f'stroke-width: {self.width:.6}; stroke-linecap: round')
|
||||
return tag('path', d=f'M {self.x1:.6} {self.y1:.6} {arc}',
|
||||
style=f'stroke: {color}, stroke-width: {self.width:.6}; stroke-linecap: round')
|
||||
|
||||
def svg_rotation(angle_rad):
|
||||
return f'rotation({angle_rad/math.pi*180:.4})'
|
||||
|
|
@ -309,11 +311,11 @@ class Rectangle(GraphicPrimitive):
|
|||
def center(self):
|
||||
return self.x + self.w/2, self.y + self.h/2
|
||||
|
||||
def to_svg(self):
|
||||
def to_svg(self, tag, color='black'):
|
||||
x, y = self.x - self.w/2, self.y - self.h/2
|
||||
return 'rect', [], dict(x=x, y=y, w=self.w, h=self.h, transform=svg_rotation(self.rotation))
|
||||
|
||||
return tag('rect', x=x, y=y, w=self.w, h=self.h, transform=svg_rotation(self.rotation), style=f'fill: {color}')
|
||||
|
||||
@dataclass
|
||||
class RegularPolygon(GraphicPrimitive):
|
||||
x : float
|
||||
y : float
|
||||
|
|
@ -334,6 +336,6 @@ class RegularPolygon(GraphicPrimitive):
|
|||
def bounding_box(self):
|
||||
return self.to_arc_poly().bounding_box()
|
||||
|
||||
def to_svg(self):
|
||||
return self.to_arc_poly().to_svg()
|
||||
def to_svg(self, tag, color='black'):
|
||||
return self.to_arc_poly().to_svg(tag, color)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import functools
|
|||
from pathlib import Path
|
||||
from itertools import count, chain
|
||||
from io import StringIO
|
||||
import textwrap
|
||||
|
||||
from .gerber_statements import *
|
||||
from .cam import CamFile, FileSettings
|
||||
|
|
@ -41,7 +42,7 @@ from . import graphic_objects as go
|
|||
from . import apertures
|
||||
|
||||
|
||||
def convert(self, value, src, dst):
|
||||
def convert(value, src, dst):
|
||||
if src == dst or src is None or dst is None or value is None:
|
||||
return value
|
||||
elif dst == 'mm':
|
||||
|
|
@ -60,16 +61,19 @@ def points_close(a, b):
|
|||
return math.isclose(a[0], b[0]) and math.isclose(a[1], b[1])
|
||||
|
||||
class Tag:
|
||||
def __init__(self, name, children=None, **attrs):
|
||||
self.name, self.children, self.attrs = name, children, attrs
|
||||
def __init__(self, name, children=None, root=False, **attrs):
|
||||
self.name, self.attrs = name, attrs
|
||||
self.children = children or []
|
||||
self.root = root
|
||||
|
||||
def __str__(self):
|
||||
opening = ' '.join([self.name] + [f'{key}="{value}"' for key, value in self.attrs.items()])
|
||||
prefix = '<?xml version="1.0" encoding="utf-8"?>\n' if self.root else ''
|
||||
opening = ' '.join([self.name] + [f'{key.replace("__", ":")}="{value}"' for key, value in self.attrs.items()])
|
||||
if self.children:
|
||||
children = '\n'.join(textwrap.indent(str(c), ' ') for c in children)
|
||||
return f'<{opening}>\n{children}\n</{self.name}>'
|
||||
children = '\n'.join(textwrap.indent(str(c), ' ') for c in self.children)
|
||||
return f'{prefix}<{opening}>\n{children}\n</{self.name}>'
|
||||
else:
|
||||
return f'<{opening}/>'
|
||||
return f'{prefix}<{opening}/>'
|
||||
|
||||
class GerberFile(CamFile):
|
||||
""" A class representing a single gerber file
|
||||
|
|
@ -83,12 +87,19 @@ class GerberFile(CamFile):
|
|||
self.comments = []
|
||||
self.objects = []
|
||||
|
||||
def to_svg(self, tag=Tag, margin=0, margin_unit='mm', svg_unit='mm'):
|
||||
def to_svg(self, tag=Tag, margin=0, arg_unit='mm', svg_unit='mm', force_bounds=None, color='black'):
|
||||
|
||||
(min_x, min_y), (max_x, max_y) = self.bounding_box(svg_unit)
|
||||
if force_bounds is None:
|
||||
(min_x, min_y), (max_x, max_y) = self.bounding_box(svg_unit)
|
||||
else:
|
||||
(min_x, min_y), (max_x, max_y) = force_bounds
|
||||
min_x = convert(min_x, arg_unit, svg_unit)
|
||||
min_y = convert(min_y, arg_unit, svg_unit)
|
||||
max_x = convert(max_x, arg_unit, svg_unit)
|
||||
max_y = convert(max_y, arg_unit, svg_unit)
|
||||
|
||||
if margin:
|
||||
margin = convert(margin, margin_unit, svg_unit)
|
||||
margin = convert(margin, arg_unit, svg_unit)
|
||||
min_x -= margin
|
||||
min_y -= margin
|
||||
max_x += margin
|
||||
|
|
@ -96,13 +107,17 @@ class GerberFile(CamFile):
|
|||
|
||||
w, h = max_x - min_x, max_y - min_y
|
||||
|
||||
primitives = [
|
||||
[ tag(*prim.to_svg()) for prim in obj.to_primitives(unit=svg_unit) ]
|
||||
for obj in self.objects ]
|
||||
primitives = [ prim.to_svg(tag, color) for obj in self.objects for prim in obj.to_primitives(unit=svg_unit) ]
|
||||
|
||||
# FIXME setup viewport transform flipping y axis
|
||||
# setup viewport transform flipping y axis
|
||||
xform = f'scale(0 -1) translate(0 {h})'
|
||||
|
||||
return tag('svg', [defs, *primitives], width=w, height=h, viewBox=f'{min_x} {min_y} {w} {h}')
|
||||
svg_unit = 'in' if svg_unit == 'inch' else 'mm'
|
||||
# TODO export apertures as <uses> where reasonable.
|
||||
return tag('svg', [*primitives],
|
||||
width=f'{w}{svg_unit}', height=f'{h}{svg_unit}',
|
||||
viewBox=f'{min_x} {min_y} {w} {h}', transform=xform,
|
||||
xmlns="http://www.w3.org/2000/svg", xmlns__xlink="http://www.w3.org/1999/xlink", root=True)
|
||||
|
||||
def merge(self, other):
|
||||
""" Merge other GerberFile into this one """
|
||||
|
|
|
|||
|
|
@ -60,16 +60,16 @@ def run_cargo_cmd(cmd, args, **kwargs):
|
|||
except FileNotFoundError:
|
||||
return subprocess.run([str(Path.home() / '.cargo' / 'bin' / cmd), *args], **kwargs)
|
||||
|
||||
def svg_to_png(in_svg, out_png):
|
||||
run_cargo_cmd('resvg', ['--dpi', '100', in_svg, out_png], check=True, stdout=subprocess.DEVNULL)
|
||||
def svg_to_png(in_svg, out_png, dpi=100):
|
||||
run_cargo_cmd('resvg', ['--dpi', str(dpi), in_svg, out_png], check=True, stdout=subprocess.DEVNULL)
|
||||
|
||||
def gbr_to_svg(in_gbr, out_svg, origin=(0, 0), size=(6, 6)):
|
||||
def gerbv_export(in_gbr, out_svg, format='svg', origin=(0, 0), size=(6, 6), fg='#ffffff'):
|
||||
x, y = origin
|
||||
w, h = size
|
||||
cmd = ['gerbv', '-x', 'svg',
|
||||
cmd = ['gerbv', '-x', format,
|
||||
'--border=0',
|
||||
f'--origin={x:.6f}x{y:.6f}', f'--window_inch={w:.6f}x{h:.6f}',
|
||||
'--foreground=#ffffff',
|
||||
f'--foreground={fg}',
|
||||
'-o', str(out_svg), str(in_gbr)]
|
||||
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
|
@ -94,12 +94,16 @@ def cleanup_clips(soup):
|
|||
# Apart from being graphically broken, this additionally causes very bad rendering performance.
|
||||
del group['clip-path']
|
||||
|
||||
def cleanup_gerbv_svg(filename):
|
||||
with svg_soup(filename) as soup:
|
||||
cleanup_clips(soup)
|
||||
|
||||
def gerber_difference(reference, actual, diff_out=None, svg_transform=None, size=(10,10)):
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as act_svg,\
|
||||
tempfile.NamedTemporaryFile(suffix='.svg') as ref_svg:
|
||||
|
||||
gbr_to_svg(reference, ref_svg.name, size=size)
|
||||
gbr_to_svg(actual, act_svg.name, size=size)
|
||||
gerbv_export(reference, ref_svg.name, size=size, format='svg')
|
||||
gerbv_export(actual, act_svg.name, size=size, format='svg')
|
||||
|
||||
with svg_soup(ref_svg.name) as soup:
|
||||
if svg_transform is not None:
|
||||
|
|
@ -116,9 +120,9 @@ def gerber_difference_merge(ref1, ref2, actual, diff_out=None, composite_out=Non
|
|||
tempfile.NamedTemporaryFile(suffix='.svg') as ref1_svg,\
|
||||
tempfile.NamedTemporaryFile(suffix='.svg') as ref2_svg:
|
||||
|
||||
gbr_to_svg(ref1, ref1_svg.name, size=size)
|
||||
gbr_to_svg(ref2, ref2_svg.name, size=size)
|
||||
gbr_to_svg(actual, act_svg.name, size=size)
|
||||
gerbv_export(ref1, ref1_svg.name, size=size, format='svg')
|
||||
gerbv_export(ref2, ref2_svg.name, size=size, format='svg')
|
||||
gerbv_export(actual, act_svg.name, size=size, format='svg')
|
||||
|
||||
with svg_soup(ref1_svg.name) as soup1:
|
||||
if svg_transform1 is not None:
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import pytest
|
|||
from ..rs274x import GerberFile
|
||||
from ..cam import FileSettings
|
||||
|
||||
from .image_support import gerber_difference, gerber_difference_merge
|
||||
from .image_support import *
|
||||
|
||||
|
||||
deg_to_rad = lambda a: a/180 * math.pi
|
||||
|
|
@ -62,6 +62,30 @@ def temp_files(request):
|
|||
else:
|
||||
print(f'gerbv {perm_path_gbr} {reference_path(args["file_a"])} {reference_path(args["file_b"])}')
|
||||
|
||||
@pytest.fixture
|
||||
def svg_temp_files(request):
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as out_svg,\
|
||||
tempfile.NamedTemporaryFile(suffix='.png') as out_png,\
|
||||
tempfile.NamedTemporaryFile(suffix='.png') as ref_png,\
|
||||
tempfile.NamedTemporaryFile(suffix='.png') as tmp_png:
|
||||
yield Path(out_svg.name), Path(out_png.name), Path(ref_png.name), Path(tmp_png.name)
|
||||
|
||||
if request.node.rep_call.failed:
|
||||
module, _, test_name = request.node.nodeid.rpartition('::')
|
||||
_test, _, test_name = test_name.partition('_')
|
||||
test_name, _, _ext = test_name.partition('.')
|
||||
test_name = re.sub(r'[^\w\d]', '_', test_name)
|
||||
fail_dir.mkdir(exist_ok=True)
|
||||
perm_path_out_svg = fail_dir / f'failure_{test_name}_actual.svg'
|
||||
perm_path_png = fail_dir / f'failure_{test_name}_difference.png'
|
||||
shutil.copy(out_svg.name, perm_path_out_svg)
|
||||
shutil.copy(tmp_png.name, perm_path_png)
|
||||
args = request.node.funcargs
|
||||
print(f'Reference file is {reference_path(args["reference"])}')
|
||||
print(f'Failing output saved to {perm_path_out_svg}')
|
||||
print(f'Difference image saved to {perm_path_png}')
|
||||
|
||||
|
||||
to_gerbv_svg_units = lambda val, unit='mm': val*72 if unit == 'inch' else val/25.4*72
|
||||
|
||||
REFERENCE_FILES = [ l.strip() for l in '''
|
||||
|
|
@ -284,4 +308,26 @@ def test_compositing(temp_files, file_a, file_b, angle, offset):
|
|||
assert hist[9] < 100
|
||||
assert hist[3:].sum() < 1e-3*hist.size
|
||||
|
||||
@pytest.mark.filterwarnings('ignore:Deprecated.*statement found.*:DeprecationWarning')
|
||||
@pytest.mark.filterwarnings('ignore::SyntaxWarning')
|
||||
@pytest.mark.parametrize('reference', REFERENCE_FILES)
|
||||
def test_svg_export(svg_temp_files, reference):
|
||||
ref = reference_path(reference)
|
||||
grb = GerberFile.open(ref)
|
||||
out_svg, out_png, ref_png, tmp_png = svg_temp_files
|
||||
|
||||
bounds = (0.0, 0.0), (6.0, 6.0) # bottom left, top right
|
||||
|
||||
with open(out_svg, 'w') as f:
|
||||
f.write(str(grb.to_svg(force_bounds=bounds, arg_unit='inch')))
|
||||
|
||||
gerbv_export(ref, ref_png, origin=bounds[0], size=bounds[1], format='png', fg='#000000')
|
||||
svg_to_png(out_svg, out_png, dpi=72) # make dpi match Cairo's default
|
||||
|
||||
mean, _max, hist = image_difference(ref_png, out_png, diff_out=tmp_png)
|
||||
assert mean < 1e-3
|
||||
assert hist[9] < 1
|
||||
assert hist[3:].sum() < 1e-3*hist.size
|
||||
|
||||
# FIXME test svg margin, bounding box computation
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue