From 965d3ce23b92f8aff1063debd6d3364de15791fe Mon Sep 17 00:00:00 2001
From: Garret Fick <garret@ficksworkshop.com>
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 <garret@ficksworkshop.com>
+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