From 44006784f0b72a3fe7e29c818e45a533a02641a7 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 8 Jan 2022 23:30:38 +0100 Subject: Basic SVG export seems to be working --- gerbonara/gerber/aperture_macros/parse.py | 5 +-- gerbonara/gerber/aperture_macros/primitive.py | 17 +++++----- gerbonara/gerber/apertures.py | 19 +++++++---- gerbonara/gerber/graphic_objects.py | 23 ++++++++++--- gerbonara/gerber/graphic_primitives.py | 48 ++++++++++++++------------- gerbonara/gerber/rs274x.py | 45 ++++++++++++++++--------- gerbonara/gerber/tests/image_support.py | 24 ++++++++------ gerbonara/gerber/tests/test_rs274x.py | 48 ++++++++++++++++++++++++++- 8 files changed, 158 insertions(+), 71 deletions(-) diff --git a/gerbonara/gerber/aperture_macros/parse.py b/gerbonara/gerber/aperture_macros/parse.py index 00227c6..375bb5b 100644 --- a/gerbonara/gerber/aperture_macros/parse.py +++ b/gerbonara/gerber/aperture_macros/parse.py @@ -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) diff --git a/gerbonara/gerber/aperture_macros/primitive.py b/gerbonara/gerber/aperture_macros/primitive.py index b28fdb5..b569637 100644 --- a/gerbonara/gerber/aperture_macros/primitive.py +++ b/gerbonara/gerber/aperture_macros/primitive.py @@ -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) diff --git a/gerbonara/gerber/apertures.py b/gerbonara/gerber/apertures.py index 104b021..b18b7a1 100644 --- a/gerbonara/gerber/apertures.py +++ b/gerbonara/gerber/apertures.py @@ -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)) diff --git a/gerbonara/gerber/graphic_objects.py b/gerbonara/gerber/graphic_objects.py index 81d68f3..278401c 100644 --- a/gerbonara/gerber/graphic_objects.py +++ b/gerbonara/gerber/graphic_objects.py @@ -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) diff --git a/gerbonara/gerber/graphic_primitives.py b/gerbonara/gerber/graphic_primitives.py index 3052322..98b8aa1 100644 --- a/gerbonara/gerber/graphic_primitives.py +++ b/gerbonara/gerber/graphic_primitives.py @@ -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) diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py index 53b4e5e..3ee8c4d 100644 --- a/gerbonara/gerber/rs274x.py +++ b/gerbonara/gerber/rs274x.py @@ -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 = '\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' + children = '\n'.join(textwrap.indent(str(c), ' ') for c in self.children) + return f'{prefix}<{opening}>\n{children}\n' 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 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 """ diff --git a/gerbonara/gerber/tests/image_support.py b/gerbonara/gerber/tests/image_support.py index db44157..dc2cbdb 100644 --- a/gerbonara/gerber/tests/image_support.py +++ b/gerbonara/gerber/tests/image_support.py @@ -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: diff --git a/gerbonara/gerber/tests/test_rs274x.py b/gerbonara/gerber/tests/test_rs274x.py index 90ccdb9..de06a16 100644 --- a/gerbonara/gerber/tests/test_rs274x.py +++ b/gerbonara/gerber/tests/test_rs274x.py @@ -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 -- cgit