diff options
-rw-r--r-- | README.md | 120 | ||||
-rw-r--r-- | gerber/render/cairo_backend.py | 31 | ||||
-rw-r--r-- | gerber/rs274x.py | 11 | ||||
-rw-r--r-- | gerber/tests/test_cairo_backend.py | 40 |
4 files changed, 123 insertions, 79 deletions
@@ -1,60 +1,60 @@ -pcb-tools
-============
-[![Travis CI Build Status](https://travis-ci.org/curtacircuitos/pcb-tools.svg?branch=master)](https://travis-ci.org/curtacircuitos/pcb-tools)
-[![Coverage Status](https://coveralls.io/repos/curtacircuitos/pcb-tools/badge.png?branch=master)](https://coveralls.io/r/curtacircuitos/pcb-tools?branch=master)
-[![Documentation Status](https://readthedocs.org/projects/pcb-tools/badge/?version=latest)](https://readthedocs.org/projects/pcb-tools/?badge=latest)
-
-Tools to handle Gerber and Excellon files in Python.
-
-Usage Example:
----------------
- import gerber
- from gerber.render import GerberCairoContext
-
- # Read gerber and Excellon files
- top_copper = gerber.read('example.GTL')
- nc_drill = gerber.read('example.txt')
-
- # Rendering context
- ctx = GerberCairoContext()
-
- # Create SVG image
- top_copper.render(ctx)
- nc_drill.render(ctx, 'composite.svg')
-
-
-Rendering Examples:
--------------------
-###Top Composite rendering
-![Composite Top Image](examples/cairo_example.png)
-![Composite Bottom Image](examples/cairo_bottom.png)
-
-Source code for this example can be found [here](examples/cairo_example.py).
-
-
-Install from source:
-```
-$ git clone https://github.com/curtacircuitos/pcb-tools.git
-$ cd pcb-tools
-$ pip install -r requirements.txt
-$ python setup.py install
-```
-
-Documentation:
---------------
-[PCB Tools Documentation](http://pcb-tools.readthedocs.org/en/latest/)
-
-
-Development and Testing:
-------------------------
-
-Dependencies for developing and testing pcb-tools are listed in test-requirements.txt. Use of a virtual environment is strongly recommended.
-
- $ virtualenv venv
- $ source venv/bin/activate
- (venv)$ pip install -r test-requirements.txt
- (venv)$ pip install -e .
-
-We use nose to run pcb-tools's suite of unittests and doctests.
-
- (venv)$ nosetests
+pcb-tools +============ +[![Travis CI Build Status](https://travis-ci.org/curtacircuitos/pcb-tools.svg?branch=master)](https://travis-ci.org/curtacircuitos/pcb-tools) +[![Coverage Status](https://coveralls.io/repos/curtacircuitos/pcb-tools/badge.png?branch=master)](https://coveralls.io/r/curtacircuitos/pcb-tools?branch=master) +[![Documentation Status](https://readthedocs.org/projects/pcb-tools/badge/?version=latest)](https://readthedocs.org/projects/pcb-tools/?badge=latest) + +Tools to handle Gerber and Excellon files in Python. + +Usage Example: +--------------- + import gerber + from gerber.render import GerberCairoContext + + # Read gerber and Excellon files + top_copper = gerber.read('example.GTL') + nc_drill = gerber.read('example.txt') + + # Rendering context + ctx = GerberCairoContext() + + # Create SVG image + top_copper.render(ctx) + nc_drill.render(ctx, 'composite.svg') + + +Rendering Examples: +------------------- +### Top Composite rendering +![Composite Top Image](examples/cairo_example.png) +![Composite Bottom Image](examples/cairo_bottom.png) + +Source code for this example can be found [here](examples/cairo_example.py). + + +Install from source: +``` +$ git clone https://github.com/curtacircuitos/pcb-tools.git +$ cd pcb-tools +$ pip install -r requirements.txt +$ python setup.py install +``` + +Documentation: +-------------- +[PCB Tools Documentation](http://pcb-tools.readthedocs.org/en/latest/) + + +Development and Testing: +------------------------ + +Dependencies for developing and testing pcb-tools are listed in test-requirements.txt. Use of a virtual environment is strongly recommended. + + $ virtualenv venv + $ source venv/bin/activate + (venv)$ pip install -r test-requirements.txt + (venv)$ pip install -e . + +We use nose to run pcb-tools's suite of unittests and doctests. + + (venv)$ nosetests diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 76be60a..2e9b143 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -77,7 +77,7 @@ class GerberCairoContext(GerberContext): self.output_ctx = cairo.Context(self.surface) def render_layer(self, layer, filename=None, settings=None, bgsettings=None, - verbose=False): + verbose=False, bounds=None): if settings is None: settings = THEMES['default'].get(layer.layer_class, RenderSettings()) if bgsettings is None: @@ -87,7 +87,10 @@ class GerberCairoContext(GerberContext): if verbose: print('[Render]: Rendering Background.') self.clear() - self.set_bounds(layer.bounds) + if bounds is not None: + self.set_bounds(bounds) + else: + self.set_bounds(layer.bounds) self._paint_background(bgsettings) if verbose: print('[Render]: Rendering {} Layer.'.format(layer.layer_class)) @@ -139,7 +142,7 @@ class GerberCairoContext(GerberContext): if is_svg: self.surface.finish() self.surface_buffer.flush() - with open(filename, "w") as f: + with open(filename, "wb") as f: self.surface_buffer.seek(0) f.write(self.surface_buffer.read()) f.flush() @@ -249,10 +252,10 @@ class GerberCairoContext(GerberContext): mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND if isinstance(arc.aperture, Circle) else cairo.LINE_CAP_SQUARE) mask.ctx.move_to(*start) # You actually have to do this... if arc.direction == 'counterclockwise': - mask.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + mask.ctx.arc(center[0], center[1], radius, angle1, angle2) else: - mask.ctx.arc_negative(*center, radius=radius, - angle1=angle1, angle2=angle2) + mask.ctx.arc_negative(center[0], center[1], radius, + angle1, angle2) mask.ctx.move_to(*end) # ...lame mask.ctx.stroke() @@ -288,11 +291,11 @@ class GerberCairoContext(GerberContext): angle1 = prim.start_angle angle2 = prim.end_angle if prim.direction == 'counterclockwise': - mask.ctx.arc(*center, radius=radius, - angle1=angle1, angle2=angle2) + mask.ctx.arc(center[0], center[1], radius, + angle1, angle2) else: - mask.ctx.arc_negative(*center, radius=radius, - angle1=angle1, angle2=angle2) + mask.ctx.arc_negative(center[0], center[1], radius, + angle1, angle2) mask.ctx.fill() self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) @@ -357,7 +360,7 @@ class GerberCairoContext(GerberContext): with self._clip_primitive(rectangle): with self._new_mask() as mask: mask.ctx.set_line_width(0) - mask.ctx.rectangle(*lower_left, width=width, height=height) + mask.ctx.rectangle(lower_left[0], lower_left[1], width, height) mask.ctx.fill() center = self.scale_point(rectangle.position) @@ -415,7 +418,7 @@ class GerberCairoContext(GerberContext): width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) - mask.ctx.rectangle(*lower_left, width=width, height=height) + mask.ctx.rectangle(lower_left[0], lower_left[1], width, height) mask.ctx.fill() center = self.scale_point(obround.position) @@ -565,7 +568,7 @@ class GerberCairoContext(GerberContext): def _flatten(self, color=None, alpha=None): color = color if color is not None else self.color alpha = alpha if alpha is not None else self.alpha - self.output_ctx.set_source_rgba(*color, alpha=alpha) + self.output_ctx.set_source_rgba(color[0], color[1], color[2], alpha) self.output_ctx.mask_surface(self.active_layer) self.ctx = None self.active_layer = None @@ -576,7 +579,7 @@ class GerberCairoContext(GerberContext): alpha = settings.alpha if settings is not None else 1.0 if not self.has_bg: self.has_bg = True - self.output_ctx.set_source_rgba(*color, alpha=alpha) + self.output_ctx.set_source_rgba(color[0], color[1], color[2], alpha) self.output_ctx.paint() def _clip_primitive(self, primitive): diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 813b246..afdf45f 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -765,14 +765,17 @@ class GerberParser(object): # Calculate the radius error sqdist_start = sq_distance(start, test_center) sqdist_end = sq_distance(end, test_center) + sqdist_diff = abs(sqdist_start - sqdist_end) # Take the option with the lowest radius error from the set of # options with a valid sweep angle - if ((abs(sqdist_start - sqdist_end) < sqdist_diff_min) - and (sweep_angle >= 0) - and (sweep_angle <= math.pi / 2.0)): + # In some rare cases, the sweep angle is numerically (10**-14) above pi/2 + # So it is safer to compare the angles with some tolerance + is_lowest_radius_error = sqdist_diff < sqdist_diff_min + is_valid_sweep_angle = sweep_angle >= 0 and sweep_angle <= math.pi / 2.0 + 1e-6 + if is_lowest_radius_error and is_valid_sweep_angle: center = test_center - sqdist_diff_min = abs(sqdist_start - sqdist_end) + sqdist_diff_min = sqdist_diff return center else: return (start[0] + offsets[0], start[1] + offsets[1]) diff --git a/gerber/tests/test_cairo_backend.py b/gerber/tests/test_cairo_backend.py index 9195b93..d5ce4ed 100644 --- a/gerber/tests/test_cairo_backend.py +++ b/gerber/tests/test_cairo_backend.py @@ -3,7 +3,8 @@ # Author: Garret Fick <garret@ficksworkshop.com> import os - +import shutil +import tempfile from ..render.cairo_backend import GerberCairoContext from ..rs274x import read @@ -136,6 +137,11 @@ def _DISABLED_test_render_am_exposure_modifier(): _test_render('resources/example_am_exposure_modifier.gbr', 'golden/example_am_exposure_modifier.png') +def test_render_svg_simple_contour(): + """Example of rendering to an SVG file""" + _test_simple_render_svg('resources/example_simple_contour.gbr') + + def _resolve_path(path): return os.path.join(os.path.dirname(__file__), path) @@ -187,3 +193,35 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None): assert_true(equal) return gerber + + +def _test_simple_render_svg(gerber_path): + """Render the gerber file as SVG + + Note: verifies only the header, not the full content. + + Parameters + ---------- + gerber_path : string + Path to Gerber file to open + """ + + gerber_path = _resolve_path(gerber_path) + gerber = read(gerber_path) + + # Create SVG image to the memory stream + ctx = GerberCairoContext() + gerber.render(ctx) + + temp_dir = tempfile.mkdtemp() + svg_temp_path = os.path.join(temp_dir, 'output.svg') + + assert_false(os.path.exists(svg_temp_path)) + ctx.dump(svg_temp_path) + assert_true(os.path.exists(svg_temp_path)) + + with open(svg_temp_path, 'r') as expected_file: + expected_bytes = expected_file.read() + assert_equal(expected_bytes[:38], '<?xml version="1.0" encoding="UTF-8"?>') + + shutil.rmtree(temp_dir) |