From 965d3ce23b92f8aff1063debd6d3364de15791fe Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 24 Jul 2016 22:08:31 +0800 Subject: Add more tests for rendering to PNG. Start adding tests for rendering to Gerber format. Changed definition of no hole to use None instead of 0 so we can differentiate when writing to Gerber format. Makde polygon use hole diameter instead of hole radius to match other primitives --- gerber/gerber_statements.py | 5 +- gerber/primitives.py | 30 +++- gerber/render/rs274x_backend.py | 19 ++- gerber/rs274x.py | 10 +- .../tests/golden/example_am_exposure_modifier.png | Bin 0 -> 10091 bytes gerber/tests/golden/example_holes_dont_clear.png | Bin 0 -> 11552 bytes gerber/tests/golden/example_two_square_boxes.gbr | 16 ++ .../resources/example_am_exposure_modifier.gbr | 16 ++ .../tests/resources/example_holes_dont_clear.gbr | 13 ++ gerber/tests/test_cairo_backend.py | 17 +- gerber/tests/test_primitives.py | 18 +- gerber/tests/test_rs274x_backend.py | 185 +++++++++++++++++++++ 12 files changed, 302 insertions(+), 27 deletions(-) create mode 100644 gerber/tests/golden/example_am_exposure_modifier.png create mode 100644 gerber/tests/golden/example_holes_dont_clear.png create mode 100644 gerber/tests/golden/example_two_square_boxes.gbr create mode 100644 gerber/tests/resources/example_am_exposure_modifier.gbr create mode 100644 gerber/tests/resources/example_holes_dont_clear.gbr create mode 100644 gerber/tests/test_rs274x_backend.py diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 3212c1c..fba2a3c 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -281,7 +281,10 @@ class ADParamStmt(ParamStmt): @classmethod def circle(cls, dcode, diameter, hole_diameter): '''Create a circular aperture definition statement''' - return cls('AD', dcode, 'C', ([diameter, hole_diameter],)) + + if hole_diameter != None: + return cls('AD', dcode, 'C', ([diameter, hole_diameter],)) + return cls('AD', dcode, 'C', ([diameter],)) @classmethod def obround(cls, dcode, width, height): diff --git a/gerber/primitives.py b/gerber/primitives.py index b8ee344..f259eff 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -370,7 +370,7 @@ class Arc(Primitive): class Circle(Primitive): """ """ - def __init__(self, position, diameter, hole_diameter = 0, **kwargs): + def __init__(self, position, diameter, hole_diameter = None, **kwargs): super(Circle, self).__init__(**kwargs) validate_coordinates(position) self.position = position @@ -388,7 +388,9 @@ class Circle(Primitive): @property def hole_radius(self): - return self.hole_diameter / 2. + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None @property def bounding_box(self): @@ -486,8 +488,10 @@ class Rectangle(Primitive): @property def hole_radius(self): - """The radius of the hole. If there is no hole, returns 0""" - return self.hole_diameter / 2. + """The radius of the hole. If there is no hole, returns None""" + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None @property def bounding_box(self): @@ -691,8 +695,10 @@ class Obround(Primitive): @property def hole_radius(self): - """The radius of the hole. If there is no hole, returns 0""" - return self.hole_diameter / 2. + """The radius of the hole. If there is no hole, returns None""" + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None @property def orientation(self): @@ -740,14 +746,14 @@ class Polygon(Primitive): """ Polygon flash defined by a set number of sides. """ - def __init__(self, position, sides, radius, hole_radius, **kwargs): + def __init__(self, position, sides, radius, hole_diameter, **kwargs): super(Polygon, self).__init__(**kwargs) validate_coordinates(position) self.position = position self.sides = sides self.radius = radius - self.hole_radius = hole_radius - self._to_convert = ['position', 'radius'] + self.hole_diameter = hole_diameter + self._to_convert = ['position', 'radius', 'hole_diameter'] @property def flashed(self): @@ -756,6 +762,12 @@ class Polygon(Primitive): @property def diameter(self): return self.radius * 2 + + @property + def hole_radius(self): + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None @property def bounding_box(self): diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 15e9154..5ab74f0 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -1,9 +1,17 @@ +"""Renders an in-memory Gerber file to statements which can be written to a string +""" +from copy import deepcopy +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + from .render import GerberContext from ..am_statements import * from ..gerber_statements import * from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon, Rectangle -from copy import deepcopy + class AMGroupContext(object): '''A special renderer to generate aperature macros from an AMGroup''' @@ -467,4 +475,13 @@ class Rs274xContext(GerberContext): def _render_inverted_layer(self): pass + + def dump(self): + """Write the rendered file to a StringIO steam""" + statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements) + stream = StringIO() + for statement in statements: + stream.write(statement + '\n') + + return stream \ No newline at end of file diff --git a/gerber/rs274x.py b/gerber/rs274x.py index e88bba7..f009232 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -486,7 +486,7 @@ class GerberParser(object): if len(modifiers[0]) >= 2: hole_diameter = modifiers[0][1] else: - hole_diameter = 0 + hole_diameter = None aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'R': @@ -496,7 +496,7 @@ class GerberParser(object): if len(modifiers[0]) >= 3: hole_diameter = modifiers[0][2] else: - hole_diameter = 0 + hole_diameter = None aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'O': @@ -506,7 +506,7 @@ class GerberParser(object): if len(modifiers[0]) >= 3: hole_diameter = modifiers[0][2] else: - hole_diameter = 0 + hole_diameter = None aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'P': @@ -520,8 +520,8 @@ class GerberParser(object): if len(modifiers[0]) > 3: hole_diameter = modifiers[0][3] else: - hole_diameter = 0 - aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_radius=hole_diameter/2.0, rotation=rotation) + hole_diameter = None + aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation) else: aperture = self.macros[shape].build(modifiers) diff --git a/gerber/tests/golden/example_am_exposure_modifier.png b/gerber/tests/golden/example_am_exposure_modifier.png new file mode 100644 index 0000000..dac951f Binary files /dev/null and b/gerber/tests/golden/example_am_exposure_modifier.png differ diff --git a/gerber/tests/golden/example_holes_dont_clear.png b/gerber/tests/golden/example_holes_dont_clear.png new file mode 100644 index 0000000..7efb67b Binary files /dev/null and b/gerber/tests/golden/example_holes_dont_clear.png differ diff --git a/gerber/tests/golden/example_two_square_boxes.gbr b/gerber/tests/golden/example_two_square_boxes.gbr new file mode 100644 index 0000000..b5c60d1 --- /dev/null +++ b/gerber/tests/golden/example_two_square_boxes.gbr @@ -0,0 +1,16 @@ +%FSLAX25Y25*% +%MOMM*% +%ADD10C,0.01*% +D10* +%LPD*% +G01X0Y0D02* +X500000D01* +Y500000D01* +X0D01* +Y0D01* +X600000D02* +X1100000D01* +Y500000D01* +X600000D01* +Y0D01* +M02* diff --git a/gerber/tests/resources/example_am_exposure_modifier.gbr b/gerber/tests/resources/example_am_exposure_modifier.gbr new file mode 100644 index 0000000..5f3f3dd --- /dev/null +++ b/gerber/tests/resources/example_am_exposure_modifier.gbr @@ -0,0 +1,16 @@ +G04 Umaco example for exposure modifier and clearing area* +%FSLAX26Y26*% +%MOIN*% +%AMSQUAREWITHHOLE* +21,0.1,1,1,0,0,0* +1,0,0.5,0,0*% +%ADD10SQUAREWITHHOLE*% +%ADD11C,1*% +G01* +%LPD*% +D11* +X-1000000Y-250000D02* +X1000000Y250000D01* +D10* +X0Y0D03* +M02* \ No newline at end of file diff --git a/gerber/tests/resources/example_holes_dont_clear.gbr b/gerber/tests/resources/example_holes_dont_clear.gbr new file mode 100644 index 0000000..deeebd0 --- /dev/null +++ b/gerber/tests/resources/example_holes_dont_clear.gbr @@ -0,0 +1,13 @@ +G04 Demonstrates that apertures with holes do not clear the area - only the aperture hole* +%FSLAX26Y26*% +%MOIN*% +%ADD10C,1X0.5*% +%ADD11C,0.1*% +G01* +%LPD*% +D11* +X-1000000Y-250000D02* +X1000000Y250000D01* +D10* +X0Y0D03* +M02* \ No newline at end of file diff --git a/gerber/tests/test_cairo_backend.py b/gerber/tests/test_cairo_backend.py index 38cffba..f358235 100644 --- a/gerber/tests/test_cairo_backend.py +++ b/gerber/tests/test_cairo_backend.py @@ -6,7 +6,7 @@ import io import os from ..render.cairo_backend import GerberCairoContext -from ..rs274x import read, GerberFile +from ..rs274x import read from .tests import * from nose.tools import assert_tuple_equal @@ -121,7 +121,20 @@ def test_flash_obround(): def test_flash_polygon(): """Umaco example a simple polygon flash with and without a hole""" - _test_render('resources/example_flash_polygon.gbr', 'golden/example_flash_polygon.png', 'golden/example_flash_polygon.png') + _test_render('resources/example_flash_polygon.gbr', 'golden/example_flash_polygon.png') + + +def test_holes_dont_clear(): + """Umaco example that an aperture with a hole does not clear the area""" + + _test_render('resources/example_holes_dont_clear.gbr', 'golden/example_holes_dont_clear.png') + + +def test_render_am_exposure_modifier(): + """Umaco example that an aperture macro with a hole does not clear the area""" + + _test_render('resources/example_am_exposure_modifier.gbr', 'golden/example_am_exposure_modifier.png') + def _resolve_path(path): return os.path.join(os.path.dirname(__file__), diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py index bc67891..61cf22d 100644 --- a/gerber/tests/test_primitives.py +++ b/gerber/tests/test_primitives.py @@ -256,18 +256,18 @@ def test_circle_conversion(): c.to_metric() #shouldn't do antyhing assert_equal(c.position, (2.54, 25.4)) assert_equal(c.diameter, 254.) - assert_equal(c.hole_diameter, 0.) + assert_equal(c.hole_diameter, None) c.to_inch() assert_equal(c.position, (0.1, 1.)) assert_equal(c.diameter, 10.) - assert_equal(c.hole_diameter, 0) + assert_equal(c.hole_diameter, None) #no effect c.to_inch() assert_equal(c.position, (0.1, 1.)) assert_equal(c.diameter, 10.) - assert_equal(c.hole_diameter, 0) + assert_equal(c.hole_diameter, None) # Circle initially metric, with hole c = Circle((2.54, 25.4), 254.0, 127.0, units='metric') @@ -294,18 +294,18 @@ def test_circle_conversion(): c.to_inch() assert_equal(c.position, (0.1, 1.)) assert_equal(c.diameter, 10.) - assert_equal(c.hole_diameter, 0) + assert_equal(c.hole_diameter, None) c.to_metric() assert_equal(c.position, (2.54, 25.4)) assert_equal(c.diameter, 254.) - assert_equal(c.hole_diameter, 0) + assert_equal(c.hole_diameter, None) #no effect c.to_metric() assert_equal(c.position, (2.54, 25.4)) assert_equal(c.diameter, 254.) - assert_equal(c.hole_diameter, 0) + assert_equal(c.hole_diameter, None) c = Circle((0.1, 1.0), 10.0, 5.0, units='inch') #No effect @@ -820,12 +820,12 @@ def test_polygon_ctor(): test_cases = (((0,0), 3, 5, 0), ((0, 0), 5, 6, 0), ((1,1), 7, 7, 45)) - for pos, sides, radius, hole_radius in test_cases: - p = Polygon(pos, sides, radius, hole_radius) + for pos, sides, radius, hole_diameter in test_cases: + p = Polygon(pos, sides, radius, hole_diameter) assert_equal(p.position, pos) assert_equal(p.sides, sides) assert_equal(p.radius, radius) - assert_equal(p.hole_radius, hole_radius) + assert_equal(p.hole_diameter, hole_diameter) def test_polygon_bounds(): """ Test polygon bounding box calculation diff --git a/gerber/tests/test_rs274x_backend.py b/gerber/tests/test_rs274x_backend.py new file mode 100644 index 0000000..89512f0 --- /dev/null +++ b/gerber/tests/test_rs274x_backend.py @@ -0,0 +1,185 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Garret Fick +import io +import os + +from ..render.rs274x_backend import Rs274xContext +from ..rs274x import read +from .tests import * + +def test_render_two_boxes(): + """Umaco exapmle of two boxes""" + _test_render('resources/example_two_square_boxes.gbr', 'golden/example_two_square_boxes.gbr') + + +def _test_render_single_quadrant(): + """Umaco exapmle of a single quadrant arc""" + + # TODO there is probably a bug here + _test_render('resources/example_single_quadrant.gbr', 'golden/example_single_quadrant.gbr') + + +def _test_render_simple_contour(): + """Umaco exapmle of a simple arrow-shaped contour""" + _test_render('resources/example_simple_contour.gbr', 'golden/example_simple_contour.gbr') + + +def _test_render_single_contour_1(): + """Umaco example of a single contour + + The resulting image for this test is used by other tests because they must generate the same output.""" + _test_render('resources/example_single_contour_1.gbr', 'golden/example_single_contour.gbr') + + +def _test_render_single_contour_2(): + """Umaco exapmle of a single contour, alternate contour end order + + The resulting image for this test is used by other tests because they must generate the same output.""" + _test_render('resources/example_single_contour_2.gbr', 'golden/example_single_contour.gbr') + + +def _test_render_single_contour_3(): + """Umaco exapmle of a single contour with extra line""" + _test_render('resources/example_single_contour_3.gbr', 'golden/example_single_contour_3.gbr') + + +def _test_render_not_overlapping_contour(): + """Umaco example of D02 staring a second contour""" + _test_render('resources/example_not_overlapping_contour.gbr', 'golden/example_not_overlapping_contour.gbr') + + +def _test_render_not_overlapping_touching(): + """Umaco example of D02 staring a second contour""" + _test_render('resources/example_not_overlapping_touching.gbr', 'golden/example_not_overlapping_touching.gbr') + + +def _test_render_overlapping_touching(): + """Umaco example of D02 staring a second contour""" + _test_render('resources/example_overlapping_touching.gbr', 'golden/example_overlapping_touching.gbr') + + +def _test_render_overlapping_contour(): + """Umaco example of D02 staring a second contour""" + _test_render('resources/example_overlapping_contour.gbr', 'golden/example_overlapping_contour.gbr') + + +def _DISABLED_test_render_level_holes(): + """Umaco example of using multiple levels to create multiple holes""" + + # TODO This is clearly rendering wrong. I'm temporarily checking this in because there are more + # rendering fixes in the related repository that may resolve these. + _test_render('resources/example_level_holes.gbr', 'golden/example_overlapping_contour.gbr') + + +def _DISABLED_test_render_cutin(): + """Umaco example of using a cutin""" + + # TODO This is clearly rendering wrong. + _test_render('resources/example_cutin.gbr', 'golden/example_cutin.gbr') + + +def _test_render_fully_coincident(): + """Umaco example of coincident lines rendering two contours""" + + _test_render('resources/example_fully_coincident.gbr', 'golden/example_fully_coincident.gbr') + + +def _test_render_coincident_hole(): + """Umaco example of coincident lines rendering a hole in the contour""" + + _test_render('resources/example_coincident_hole.gbr', 'golden/example_coincident_hole.gbr') + + +def _test_render_cutin_multiple(): + """Umaco example of a region with multiple cutins""" + + _test_render('resources/example_cutin_multiple.gbr', 'golden/example_cutin_multiple.gbr') + + +def _test_flash_circle(): + """Umaco example a simple circular flash with and without a hole""" + + _test_render('resources/example_flash_circle.gbr', 'golden/example_flash_circle.gbr') + + +def _test_flash_rectangle(): + """Umaco example a simple rectangular flash with and without a hole""" + + _test_render('resources/example_flash_rectangle.gbr', 'golden/example_flash_rectangle.gbr') + + +def _test_flash_obround(): + """Umaco example a simple obround flash with and without a hole""" + + _test_render('resources/example_flash_obround.gbr', 'golden/example_flash_obround.gbr') + + +def _test_flash_polygon(): + """Umaco example a simple polygon flash with and without a hole""" + + _test_render('resources/example_flash_polygon.gbr', 'golden/example_flash_polygon.gbr') + + +def _test_holes_dont_clear(): + """Umaco example that an aperture with a hole does not clear the area""" + + _test_render('resources/example_holes_dont_clear.gbr', 'golden/example_holes_dont_clear.gbr') + + +def _test_render_am_exposure_modifier(): + """Umaco example that an aperture macro with a hole does not clear the area""" + + _test_render('resources/example_am_exposure_modifier.gbr', 'golden/example_am_exposure_modifier.gbr') + + +def _resolve_path(path): + return os.path.join(os.path.dirname(__file__), + path) + + +def _test_render(gerber_path, png_expected_path, create_output_path = None): + """Render the gerber file and compare to the expected PNG output. + + Parameters + ---------- + gerber_path : string + Path to Gerber file to open + png_expected_path : string + Path to the PNG file to compare to + create_output : string|None + If not None, write the generated PNG to the specified path. + This is primarily to help with + """ + + gerber_path = _resolve_path(gerber_path) + png_expected_path = _resolve_path(png_expected_path) + if create_output_path: + create_output_path = _resolve_path(create_output_path) + + gerber = read(gerber_path) + + # Create GBR output from the input file + ctx = Rs274xContext(gerber.settings) + gerber.render(ctx) + + actual_contents = ctx.dump() + + # If we want to write the file bytes, do it now. This happens + if create_output_path: + with open(create_output_path, 'wb') as out_file: + out_file.write(actual_contents.getvalue()) + # Creating the output is dangerous - it could overwrite the expected result. + # So if we are creating the output, we make the test fail on purpose so you + # won't forget to disable this + assert_false(True, 'Test created the output %s. This needs to be disabled to make sure the test behaves correctly' % (create_output_path,)) + + # Read the expected PNG file + + with open(png_expected_path, 'r') as expected_file: + expected_contents = expected_file.read() + + assert_equal(expected_contents, actual_contents.getvalue()) + + return gerber -- cgit