From 1cb269131bc52f0b1a1e69cef0466f2d994d52a8 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 19 Dec 2015 21:54:29 -0500 Subject: Allow negative render of soldermask per #50 Update example code and rendering to show change --- gerber/render/cairo_backend.py | 121 ++++++++++++++++++++++++++++------------- gerber/render/render.py | 21 ++++++- gerber/render/theme.py | 50 +++++++++++++++++ 3 files changed, 152 insertions(+), 40 deletions(-) create mode 100644 gerber/render/theme.py (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 345f331..6aaffd4 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -25,6 +25,7 @@ import tempfile from ..primitives import * + class GerberCairoContext(GerberContext): def __init__(self, scale=300): GerberContext.__init__(self) @@ -32,42 +33,72 @@ class GerberCairoContext(GerberContext): self.surface = None self.ctx = None self.bg = False - + self.mask = None + self.mask_ctx = None + self.origin_in_pixels = None + self.size_in_pixels = None + def set_bounds(self, bounds): 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.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) self.ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) self.ctx.scale(1, -1) self.ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1]*self.scale[0]) - size_in_pixels[1]) - # self.ctx.translate(-(origin_in_inch[0] * self.scale[0]), -origin_in_inch[1]*self.scale[1]) + + self.mask_buffer = tempfile.NamedTemporaryFile() + self.mask = cairo.SVGSurface(self.mask_buffer, size_in_pixels[0], size_in_pixels[1]) + self.mask_ctx = cairo.Context(self.mask) + 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]) def _render_line(self, line, color): start = map(mul, line.start, self.scale) end = map(mul, line.end, self.scale) - if isinstance(line.aperture, Circle): - width = line.aperture.diameter + if not self.invert: self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if (line.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(width * self.scale[0]) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - self.ctx.move_to(*start) - self.ctx.line_to(*end) - self.ctx.stroke() - elif isinstance(line.aperture, Rectangle): - points = [tuple(map(mul, x, self.scale)) for x in line.vertices] - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER if (line.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.move_to(*points[0]) - for point in points[1:]: - self.ctx.line_to(*point) - self.ctx.fill() + if isinstance(line.aperture, Circle): + width = line.aperture.diameter + self.ctx.set_line_width(width * self.scale[0]) + self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + self.ctx.move_to(*start) + self.ctx.line_to(*end) + self.ctx.stroke() + elif isinstance(line.aperture, Rectangle): + points = [tuple(map(mul, x, self.scale)) for x in line.vertices] + self.ctx.set_line_width(0) + self.ctx.move_to(*points[0]) + for point in points[1:]: + self.ctx.line_to(*point) + self.ctx.fill() + else: + self.mask_ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.mask_ctx.set_operator(cairo.OPERATOR_CLEAR) + if isinstance(line.aperture, Circle): + width = line.aperture.diameter + self.mask_ctx.set_line_width(0) + self.mask_ctx.set_line_width(width * self.scale[0]) + self.mask_ctx.set_line_cap(cairo.LINE_CAP_ROUND) + self.mask_ctx.move_to(*start) + self.mask_ctx.line_to(*end) + self.mask_ctx.stroke() + elif isinstance(line.aperture, Rectangle): + points = [tuple(map(mul, x, self.scale)) for x in line.vertices] + self.mask_ctx.set_line_width(0) + self.mask_ctx.move_to(*points[0]) + for point in points[1:]: + self.mask_ctx.line_to(*point) + self.mask_ctx.fill() def _render_arc(self, arc, color): center = map(mul, arc.center, self.scale) @@ -78,7 +109,7 @@ class GerberCairoContext(GerberContext): angle2 = arc.end_angle width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER if (arc.level_polarity == "dark" and not self.invert)else cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_OVER if (arc.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.set_line_width(width * self.scale[0]) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*start) # You actually have to do this... @@ -112,20 +143,34 @@ class GerberCairoContext(GerberContext): def _render_circle(self, circle, color): center = tuple(map(mul, circle.position, self.scale)) - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER if (circle.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.arc(*center, radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) - self.ctx.fill() + if not self.invert: + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER if (circle.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.arc(*center, radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) + self.ctx.fill() + else: + self.mask_ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.mask_ctx.set_operator(cairo.OPERATOR_CLEAR) + self.mask_ctx.set_line_width(0) + self.mask_ctx.arc(*center, radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) + self.mask_ctx.fill() def _render_rectangle(self, rectangle, color): ll = map(mul, rectangle.lower_left, self.scale) width, height = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale))) - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER if (rectangle.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.rectangle(*ll,width=width, height=height) - self.ctx.fill() + if not self.invert: + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER if (rectangle.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.rectangle(*ll, width=width, height=height) + self.ctx.fill() + else: + self.mask_ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.mask_ctx.set_operator(cairo.OPERATOR_CLEAR) + self.mask_ctx.set_line_width(0) + self.mask_ctx.rectangle(*ll, width=width, height=height) + self.mask_ctx.fill() def _render_obround(self, obround, color): self._render_circle(obround.subshapes['circle1'], color) @@ -140,17 +185,23 @@ class GerberCairoContext(GerberContext): self.ctx.set_font_size(200) self._render_circle(Circle(primitive.position, 0.01), color) self.ctx.set_source_rgb(*color) - self.ctx.set_operator(cairo.OPERATOR_OVER if (primitive.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_OVER if (primitive.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.move_to(*[self.scale[0] * (coord + 0.01) for coord in primitive.position]) self.ctx.scale(1, -1) self.ctx.show_text(primitive.net_name) self.ctx.scale(1, -1) def _paint_inverted_layer(self): - self.ctx.set_source_rgba(*self.background_color) + self.mask_ctx.set_operator(cairo.OPERATOR_OVER) + self.mask_ctx.set_source_rgba(*self.color, alpha=self.alpha) + self.mask_ctx.paint() + + 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])) + self.ctx.set_source(ptn) self.ctx.paint() - self.ctx.set_operator(cairo.OPERATOR_CLEAR) def _paint_background(self): if not self.bg: @@ -160,21 +211,17 @@ class GerberCairoContext(GerberContext): 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_svg_str(self): self.surface.finish() self.surface_buffer.flush() return self.surface_buffer.read() - \ No newline at end of file diff --git a/gerber/render/render.py b/gerber/render/render.py index 8f49796..737061e 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -23,12 +23,13 @@ Rendering Render Gerber and Excellon files to a variety of formats. The render module currently supports SVG rendering using the `svgwrite` library. """ + + +from ..primitives import * from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt, RegionModeStmt, - QuadrantModeStmt, -) + QuadrantModeStmt,) -from ..primitives import * class GerberContext(object): """ Gerber rendering context base class @@ -182,3 +183,17 @@ class GerberContext(object): def _render_test_record(self, primitive, color): pass + +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') diff --git a/gerber/render/theme.py b/gerber/render/theme.py new file mode 100644 index 0000000..5d39bb6 --- /dev/null +++ b/gerber/render/theme.py @@ -0,0 +1,50 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2013-2014 Paulo Henrique Silva + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +COLORS = { + 'black': (0.0, 0.0, 0.0), + 'white': (1.0, 1.0, 1.0), + 'fr-4': (0.702, 0.655, 0.192), + 'green soldermask': (0.0, 0.612, 0.396), + 'blue soldermask': (0.059, 0.478, 0.651), + 'red soldermask': (0.968, 0.169, 0.165), + 'black soldermask': (0.298, 0.275, 0.282), + 'enig copper': (0.780, 0.588, 0.286), + 'hasl copper': (0.871, 0.851, 0.839) +} + + +class RenderSettings(object): + def __init__(self, color, alpha=1.0, invert=False): + self.color = color + self.alpha = alpha + self.invert = False + + +class Theme(object): + def __init__(self, **kwargs): + self.background = kwargs.get('background', RenderSettings(COLORS['black'], 0.0)) + self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white'])) + self.topsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'])) + self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], 0.8, True)) + self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], 0.8, 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) + + -- cgit