From 6f876edd09d9b81649691e529f85653f14b8fd1c Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 22 Dec 2015 02:45:48 -0500 Subject: Add PCB interface this incorporates some of @chintal's layers.py changes PCB.from_directory() simplifies loading of multiple gerbers the PCB() class should be pretty helpful going forward... the context classes could use some cleaning up, although I'd like to wait until the freecad stuff gets merged, that way we can try to refactor the context base to support more use cases --- gerber/render/cairo_backend.py | 62 ++++++++++++++++++++++++++++++++---------- gerber/render/render.py | 19 ++++--------- gerber/render/theme.py | 34 +++++++++++++++-------- 3 files changed, 75 insertions(+), 40 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 8283ae0..7acf29a 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -17,7 +17,7 @@ import cairocffi as cairo -from operator import mul +from operator import mul, div import math import tempfile @@ -39,16 +39,16 @@ class GerberCairoContext(GerberContext): self.bg = False self.mask = None self.mask_ctx = None - self.origin_in_pixels = None - self.size_in_pixels = None + self.origin_in_inch = None + self.size_in_inch = None - def set_bounds(self, bounds): + def set_bounds(self, bounds, new_surface=False): origin_in_inch = (bounds[0][0], bounds[1][0]) size_in_inch = (abs(bounds[0][1] - bounds[0][0]), abs(bounds[1][1] - bounds[1][0])) size_in_pixels = map(mul, size_in_inch, self.scale) - self.origin_in_pixels = tuple(map(mul, origin_in_inch, self.scale)) if self.origin_in_pixels is None else self.origin_in_pixels - self.size_in_pixels = size_in_pixels if self.size_in_pixels is None else self.size_in_pixels - if self.surface is None: + self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch + self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch + if (self.surface is None) or new_surface: self.surface_buffer = tempfile.NamedTemporaryFile() self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) self.ctx = cairo.Context(self.surface) @@ -61,6 +61,36 @@ class GerberCairoContext(GerberContext): self.mask_ctx.scale(1, -1) self.mask_ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1]*self.scale[0]) - size_in_pixels[1]) + def render_layers(self, layers, filename): + """ Render a set of layers + """ + self.set_bounds(layers[0].bounds, True) + self._paint_background(True) + for layer in layers: + self._render_layer(layer) + self.dump(filename) + + @property + def origin_in_pixels(self): + return tuple(map(mul, self.origin_in_inch, self.scale)) if self.origin_in_inch is not None else (0.0, 0.0) + + @property + def size_in_pixels(self): + return tuple(map(mul, self.size_in_inch, self.scale)) if self.size_in_inch is not None else (0.0, 0.0) + + def _render_layer(self, layer): + self.color = layer.settings.color + self.alpha = layer.settings.alpha + self.invert = layer.settings.invert + if layer.settings.mirror: + raise Warning('mirrored layers aren\'t supported yet...') + if self.invert: + self._clear_mask() + for p in layer.primitives: + self.render(p) + if self.invert: + self._render_mask() + def _render_line(self, line, color): start = map(mul, line.start, self.scale) end = map(mul, line.end, self.scale) @@ -178,12 +208,13 @@ class GerberCairoContext(GerberContext): self._render_circle(circle, color) def _render_test_record(self, primitive, color): - self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) - self.ctx.set_font_size(200) - self._render_circle(Circle(primitive.position, 0.01), color) + position = tuple(map(add, primitive.position, self.origin_in_inch)) + self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + self.ctx.set_font_size(13) + self._render_circle(Circle(position, 0.015), color) self.ctx.set_source_rgb(*color) self.ctx.set_operator(cairo.OPERATOR_OVER if primitive.level_polarity == "dark" else cairo.OPERATOR_CLEAR) - self.ctx.move_to(*[self.scale[0] * (coord + 0.01) for coord in primitive.position]) + self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position]) self.ctx.scale(1, -1) self.ctx.show_text(primitive.net_name) self.ctx.scale(1, -1) @@ -196,14 +227,15 @@ class GerberCairoContext(GerberContext): def _render_mask(self): self.ctx.set_operator(cairo.OPERATOR_OVER) ptn = cairo.SurfacePattern(self.mask) - ptn.set_matrix(cairo.Matrix(xx=1.0, yy=-1.0, x0=-self.origin_in_pixels[0], y0=self.size_in_pixels[1] + self.origin_in_pixels[1])) + ptn.set_matrix(cairo.Matrix(xx=1.0, yy=-1.0, x0=-self.origin_in_pixels[0], + y0=self.size_in_pixels[1] + self.origin_in_pixels[1])) self.ctx.set_source(ptn) self.ctx.paint() - def _paint_background(self): - if not self.bg: + def _paint_background(self, force=False): + if (not self.bg) or force: self.bg = True - self.ctx.set_source_rgba(*self.background_color) + self.ctx.set_source_rgba(*self.background_color, alpha=1.0) self.ctx.paint() def dump(self, filename): diff --git a/gerber/render/render.py b/gerber/render/render.py index 737061e..c76ead5 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -60,7 +60,6 @@ class GerberContext(object): def __init__(self, units='inch'): self._units = units self._color = (0.7215, 0.451, 0.200) - self._drill_color = (0.25, 0.25, 0.25) self._background_color = (0.0, 0.0, 0.0) self._alpha = 1.0 self._invert = False @@ -150,7 +149,7 @@ class GerberContext(object): elif isinstance(primitive, Polygon): self._render_polygon(primitive, color) elif isinstance(primitive, Drill): - self._render_drill(primitive, self.drill_color) + self._render_drill(primitive, color) elif isinstance(primitive, TestRecord): self._render_test_record(primitive, color) else: @@ -185,15 +184,7 @@ class GerberContext(object): class Renderable(object): - def __init__(self, color=None, alpha=None, invert=False): - self.color = color - self.alpha = alpha - self.invert = invert - - def to_render(self): - """ Override this in subclass. Should return a list of Primitives or Renderables - """ - raise NotImplementedError('to_render() must be implemented in subclass') - - def apply_theme(self, theme): - raise NotImplementedError('apply_theme() must be implemented in subclass') + def __init__(self, settings=None): + self.settings = settings + self.primitives = [] + diff --git a/gerber/render/theme.py b/gerber/render/theme.py index eae3735..5978831 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -19,6 +19,9 @@ COLORS = { 'black': (0.0, 0.0, 0.0), 'white': (1.0, 1.0, 1.0), + 'red': (1.0, 0.0, 0.0), + 'green': (0.0, 1.0, 0.0), + 'blue' : (0.0, 0.0, 1.0), 'fr-4': (0.290, 0.345, 0.0), 'green soldermask': (0.0, 0.612, 0.396), 'blue soldermask': (0.059, 0.478, 0.651), @@ -31,29 +34,38 @@ COLORS = { class RenderSettings(object): - def __init__(self, color, alpha=1.0, invert=False): + def __init__(self, color, alpha=1.0, invert=False, mirror=False): self.color = color self.alpha = alpha - self.invert = False + self.invert = invert + self.mirror = mirror class Theme(object): - def __init__(self, **kwargs): - self.background = kwargs.get('background', RenderSettings(COLORS['black'], 0.0)) + def __init__(self, name=None, **kwargs): + self.name = 'Default' if name is None else name + self.background = kwargs.get('background', RenderSettings(COLORS['black'], alpha=0.0)) self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white'])) self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'])) - self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], 0.8, True)) - self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], 0.8, True)) + self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True)) + self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True)) self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) self.bottom = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) - self.drill = kwargs.get('drill', self.background) + self.drill = kwargs.get('drill', RenderSettings(COLORS['black'])) + self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red'])) + def __getitem__(self, key): + return getattr(self, key) THEMES = { 'Default': Theme(), - 'Osh Park': Theme(top=COLORS['enig copper'], - bottom=COLORS['enig copper'], - topmask=COLORS['purple soldermask'], - bottommask=COLORS['purple soldermask']), + 'OSH Park': Theme(name='OSH Park', + top=RenderSettings(COLORS['enig copper']), + bottom=RenderSettings(COLORS['enig copper']), + topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True), + bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True)), + 'Blue': Theme(name='Blue', + topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True), + bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)), } -- cgit From 5430fa6738b74f324c47c947477dd5b779db5d1c Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 22 Dec 2015 10:18:51 -0500 Subject: Python3 fix --- gerber/render/cairo_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 7acf29a..c3e9ac2 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -17,7 +17,7 @@ import cairocffi as cairo -from operator import mul, div +from operator import mul import math import tempfile @@ -45,7 +45,7 @@ class GerberCairoContext(GerberContext): def set_bounds(self, bounds, new_surface=False): origin_in_inch = (bounds[0][0], bounds[1][0]) size_in_inch = (abs(bounds[0][1] - bounds[0][0]), abs(bounds[1][1] - bounds[1][0])) - size_in_pixels = map(mul, size_in_inch, self.scale) + size_in_pixels = tuple(map(mul, size_in_inch, self.scale)) self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch if (self.surface is None) or new_surface: -- cgit From 6a005436b475e3517fd6a583473b60e601bcc661 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 1 Jan 2016 12:25:38 -0500 Subject: Refactor a little pulled all rendering stuff out of the pcb/layer objects --- gerber/render/cairo_backend.py | 96 ++++++++++++++++++++++-------------------- gerber/render/render.py | 10 +++-- gerber/render/theme.py | 17 ++++---- 3 files changed, 65 insertions(+), 58 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index c3e9ac2..4e71e75 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -21,7 +21,8 @@ from operator import mul import math import tempfile -from .render import GerberContext +from .render import GerberContext, RenderSettings +from .theme import THEMES from ..primitives import * try: @@ -41,6 +42,15 @@ class GerberCairoContext(GerberContext): self.mask_ctx = None self.origin_in_inch = None self.size_in_inch = None + self._xform_matrix = None + + @property + def origin_in_pixels(self): + return tuple(map(mul, self.origin_in_inch, self.scale)) if self.origin_in_inch is not None else (0.0, 0.0) + + @property + def size_in_pixels(self): + return tuple(map(mul, self.size_in_inch, self.scale)) if self.size_in_inch is not None else (0.0, 0.0) def set_bounds(self, bounds, new_surface=False): origin_in_inch = (bounds[0][0], bounds[1][0]) @@ -60,34 +70,56 @@ class GerberCairoContext(GerberContext): self.mask_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) self.mask_ctx.scale(1, -1) self.mask_ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1]*self.scale[0]) - size_in_pixels[1]) + self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, x0=-self.origin_in_pixels[0], y0=self.size_in_pixels[1] + self.origin_in_pixels[1]) - def render_layers(self, layers, filename): + def render_layers(self, layers, filename, theme=THEMES['default']): """ Render a set of layers """ self.set_bounds(layers[0].bounds, True) self._paint_background(True) for layer in layers: - self._render_layer(layer) + self._render_layer(layer, theme) self.dump(filename) - @property - def origin_in_pixels(self): - return tuple(map(mul, self.origin_in_inch, self.scale)) if self.origin_in_inch is not None else (0.0, 0.0) + def dump(self, filename): + """ Save image as `filename` + """ + is_svg = filename.lower().endswith(".svg") + if is_svg: + self.surface.finish() + self.surface_buffer.flush() + with open(filename, "w") as f: + self.surface_buffer.seek(0) + f.write(self.surface_buffer.read()) + f.flush() + else: + self.surface.write_to_png(filename) - @property - def size_in_pixels(self): - return tuple(map(mul, self.size_in_inch, self.scale)) if self.size_in_inch is not None else (0.0, 0.0) + def dump_str(self): + """ Return a string containing the rendered image. + """ + fobj = StringIO() + self.surface.write_to_png(fobj) + return fobj.getvalue() + + def dump_svg_str(self): + """ Return a string containg the rendered SVG. + """ + self.surface.finish() + self.surface_buffer.flush() + return self.surface_buffer.read() - def _render_layer(self, layer): - self.color = layer.settings.color - self.alpha = layer.settings.alpha - self.invert = layer.settings.invert - if layer.settings.mirror: + def _render_layer(self, layer, theme=THEMES['default']): + settings = theme.get(layer.layer_class, RenderSettings()) + self.color = settings.color + self.alpha = settings.alpha + self.invert = settings.invert + if settings.mirror: raise Warning('mirrored layers aren\'t supported yet...') if self.invert: self._clear_mask() - for p in layer.primitives: - self.render(p) + for prim in layer.primitives: + self.render(prim) if self.invert: self._render_mask() @@ -209,10 +241,11 @@ class GerberCairoContext(GerberContext): def _render_test_record(self, primitive, color): position = tuple(map(add, primitive.position, self.origin_in_inch)) + self.ctx.set_operator(cairo.OPERATOR_OVER) self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) self.ctx.set_font_size(13) self._render_circle(Circle(position, 0.015), color) - self.ctx.set_source_rgb(*color) + self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if primitive.level_polarity == "dark" else cairo.OPERATOR_CLEAR) self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position]) self.ctx.scale(1, -1) @@ -227,8 +260,7 @@ class GerberCairoContext(GerberContext): def _render_mask(self): self.ctx.set_operator(cairo.OPERATOR_OVER) ptn = cairo.SurfacePattern(self.mask) - ptn.set_matrix(cairo.Matrix(xx=1.0, yy=-1.0, x0=-self.origin_in_pixels[0], - y0=self.size_in_pixels[1] + self.origin_in_pixels[1])) + ptn.set_matrix(self._xform_matrix) self.ctx.set_source(ptn) self.ctx.paint() @@ -237,29 +269,3 @@ class GerberCairoContext(GerberContext): self.bg = True self.ctx.set_source_rgba(*self.background_color, alpha=1.0) self.ctx.paint() - - def dump(self, filename): - is_svg = filename.lower().endswith(".svg") - if is_svg: - self.surface.finish() - self.surface_buffer.flush() - with open(filename, "w") as f: - self.surface_buffer.seek(0) - f.write(self.surface_buffer.read()) - f.flush() - else: - self.surface.write_to_png(filename) - - def dump_str(self): - """ Return a string containing the rendered image. - """ - fobj = StringIO() - self.surface.write_to_png(fobj) - return fobj.getvalue() - - def dump_svg_str(self): - """ Return a string containg the rendered SVG. - """ - self.surface.finish() - self.surface_buffer.flush() - return self.surface_buffer.read() diff --git a/gerber/render/render.py b/gerber/render/render.py index c76ead5..6af8bf1 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -183,8 +183,10 @@ class GerberContext(object): pass -class Renderable(object): - def __init__(self, settings=None): - self.settings = settings - self.primitives = [] +class RenderSettings(object): + def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, mirror=False): + self.color = color + self.alpha = alpha + self.invert = invert + self.mirror = mirror diff --git a/gerber/render/theme.py b/gerber/render/theme.py index 5978831..e538df8 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -16,6 +16,8 @@ # limitations under the License. +from .render import RenderSettings + COLORS = { 'black': (0.0, 0.0, 0.0), 'white': (1.0, 1.0, 1.0), @@ -33,14 +35,6 @@ COLORS = { } -class RenderSettings(object): - def __init__(self, color, alpha=1.0, invert=False, mirror=False): - self.color = color - self.alpha = alpha - self.invert = invert - self.mirror = mirror - - class Theme(object): def __init__(self, name=None, **kwargs): self.name = 'Default' if name is None else name @@ -57,8 +51,13 @@ class Theme(object): def __getitem__(self, key): return getattr(self, key) + def get(self, key, noneval=None): + val = getattr(self, key) + return val if val is not None else noneval + + THEMES = { - 'Default': Theme(), + 'default': Theme(), 'OSH Park': Theme(name='OSH Park', top=RenderSettings(COLORS['enig copper']), bottom=RenderSettings(COLORS['enig copper']), -- cgit