From 695e3d9220be8773f6630bb5c512d122b8576742 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 28 Sep 2014 18:07:15 -0400 Subject: Added excellon support and refactored project --- gerber/render/__init__.py | 28 ++++++++ gerber/render/apertures.py | 58 +++++++++++++++ gerber/render/render.py | 133 +++++++++++++++++++++++++++++++++++ gerber/render/svg.py | 171 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 390 insertions(+) create mode 100644 gerber/render/__init__.py create mode 100644 gerber/render/apertures.py create mode 100644 gerber/render/render.py create mode 100644 gerber/render/svg.py (limited to 'gerber/render') diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py new file mode 100644 index 0000000..cc87ee0 --- /dev/null +++ b/gerber/render/__init__.py @@ -0,0 +1,28 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe + +# 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. +""" +gerber.render +============ +**Gerber Renderers** + +This module provides contexts for rendering images of gerber layers. Currently +SVG is the only supported format. +""" + + +from svg import GerberSvgContext + diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py new file mode 100644 index 0000000..55e6a30 --- /dev/null +++ b/gerber/render/apertures.py @@ -0,0 +1,58 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe + +# 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. +""" +gerber.render.apertures +============ +**Gerber Aperture base classes** + +This module provides base classes for gerber apertures. These are used by +the rendering engine to draw the gerber file. +""" + + +class Aperture(object): + """ Gerber Aperture base class + """ + def draw(self, ctx, x, y): + raise NotImplementedError('The draw method must be implemented in an Aperture subclass.') + + def flash(self, ctx, x, y): + raise NotImplementedError('The flash method must be implemented in an Aperture subclass.') + + +class Circle(Aperture): + """ Circular Aperture base class + """ + def __init__(self, diameter=0.0): + self.diameter = diameter + +class Rect(Aperture): + """ Rectangular Aperture base class + """ + def __init__(self, size=(0, 0)): + self.size = size + +class Obround(Aperture): + """ Obround Aperture base class + """ + def __init__(self, size=(0, 0)): + self.size = size + +class Polygon(Aperture): + """ Polygon Aperture base class + """ + pass \ No newline at end of file diff --git a/gerber/render/render.py b/gerber/render/render.py new file mode 100644 index 0000000..e15a36f --- /dev/null +++ b/gerber/render/render.py @@ -0,0 +1,133 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# Modified from code by 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. + +from ..statements import ( + CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt +) + + +class GerberContext(object): + settings = {} + + x = 0 + y = 0 + + aperture = 0 + interpolation = 'linear' + + image_polarity = 'positive' + level_polarity = 'dark' + + def __init__(self): + pass + + def set_format(self, settings): + self.settings = settings + + def set_coord_format(self, zero_suppression, format, notation): + self.settings['zero_suppression'] = zero_suppression + self.settings['format'] = format + self.settings['notation'] = notation + + def set_coord_notation(self, notation): + self.settings['notation'] = notation + + def set_coord_unit(self, unit): + self.settings['units'] = unit + + def set_image_polarity(self, polarity): + self.image_polarity = polarity + + def set_level_polarity(self, polarity): + self.level_polarity = polarity + + def set_interpolation(self, interpolation): + self.interpolation = 'linear' if interpolation in ("G01", "G1") else 'arc' + + def set_aperture(self, d): + self.aperture = d + + def resolve(self, x, y): + return x or self.x, y or self.y + + def define_aperture(self, d, shape, modifiers): + pass + + def move(self, x, y, resolve=True): + if resolve: + self.x, self.y = self.resolve(x, y) + else: + self.x, self.y = x, y + + def stroke(self, x, y): + pass + + def line(self, x, y): + pass + + def arc(self, x, y): + pass + + def flash(self, x, y): + pass + + def drill(self, x, y, diameter): + pass + + def evaluate(self, stmt): + if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): + return + + elif isinstance(stmt, ParamStmt): + self._evaluate_param(stmt) + + elif isinstance(stmt, CoordStmt): + self._evaluate_coord(stmt) + + elif isinstance(stmt, ApertureStmt): + self._evaluate_aperture(stmt) + + else: + raise Exception("Invalid statement to evaluate") + + def _evaluate_param(self, stmt): + if stmt.param == "FS": + self.set_coord_format(stmt.zero_suppression, stmt.format, stmt.notation) + self.set_coord_notation(stmt.notation) + elif stmt.param == "MO:": + self.set_coord_unit(stmt.mode) + elif stmt.param == "IP:": + self.set_image_polarity(stmt.ip) + elif stmt.param == "LP:": + self.set_level_polarity(stmt.lp) + elif stmt.param == "AD": + self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) + + def _evaluate_coord(self, stmt): + if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): + self.set_interpolation(stmt.function) + + if stmt.op == "D01": + self.stroke(stmt.x, stmt.y) + elif stmt.op == "D02": + self.move(stmt.x, stmt.y) + elif stmt.op == "D03": + self.flash(stmt.x, stmt.y) + + def _evaluate_aperture(self, stmt): + self.set_aperture(stmt.d) diff --git a/gerber/render/svg.py b/gerber/render/svg.py new file mode 100644 index 0000000..b16e534 --- /dev/null +++ b/gerber/render/svg.py @@ -0,0 +1,171 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe +# Based on render_svg.py by 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. + +from .render import GerberContext +from .apertures import Circle, Rect, Obround, Polygon +import svgwrite + +SCALE = 300 + + +class SvgCircle(Circle): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", + stroke_width=SCALE * self.diameter, + stroke_linecap="round") + + def flash(self, ctx, x, y): + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (self.diameter / 2.0), + fill='rgb(184, 115, 51)'),] + + +class SvgRect(Rect): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", stroke_width=2, + stroke_linecap="butt") + + def flash(self, ctx, x, y): + xsize, ysize = self.size + return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), + -SCALE * (y + (ysize / 2))), + size=(SCALE * xsize, SCALE * ysize), + fill="rgb(184, 115, 51)"),] + +class SvgObround(Obround): + def draw(self, ctx, x, y): + pass + + def flash(self, ctx, x, y): + xsize, ysize = self.size + + # horizontal obround + if xsize == ysize: + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (x / 2.0), + fill='rgb(184, 115, 51)'),] + if xsize > ysize: + rectx = xsize - ysize + recty = ysize + lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, rcircle, rect,] + + # Vertical obround + else: + rectx = xsize + recty = ysize - xsize + lcircle = ctx.dwg.circle(center=(x * SCALE, + (y - (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + ucircle = ctx.dwg.circle(center=(x * SCALE, + (y + (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, ucircle, rect,] + + +class GerberSvgContext(GerberContext): + def __init__(self): + GerberContext.__init__(self) + + self.apertures = {} + self.dwg = svgwrite.Drawing() + #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) + + def set_bounds(self, bounds): + xbounds, ybounds = bounds + size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + + + def define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + aperture = SvgCircle(diameter=float(modifiers[0][0])) + elif shape == 'R': + aperture = SvgRect(size=modifiers[0][0:2]) + elif shape == 'O': + aperture = SvgObround(size=modifiers[0][0:2]) + self.apertures[d] = aperture + + def stroke(self, x, y): + super(GerberSvgContext, self).stroke(x, y) + + if self.interpolation == 'linear': + self.line(x, y) + elif self.interpolation == 'arc': + #self.arc(x, y) + self.line(x,y) + + def line(self, x, y): + super(GerberSvgContext, self).line(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + self.dwg.add(ap.draw(self, x, y)) + self.move(x, y, resolve=False) + + + def arc(self, x, y): + super(GerberSvgContext, self).arc(x, y) + + + def flash(self, x, y): + super(GerberSvgContext, self).flash(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + for shape in ap.flash(self, x, y): + self.dwg.add(shape) + self.move(x, y, resolve=False) + + + def drill(self, x, y, diameter): + hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') + self.dwg.add(hit) + + def dump(self, filename): + self.dwg.saveas(filename) + + -- cgit From 3a5dbcf1e13704b7352d5fb3c4777d7df3fed081 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 28 Sep 2014 21:17:13 -0400 Subject: added ExcellonFile class --- gerber/render/apertures.py | 9 ++++++--- gerber/render/render.py | 12 +++++------ gerber/render/svg.py | 50 ++++++++++++++++++++-------------------------- 3 files changed, 34 insertions(+), 37 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py index 55e6a30..f163b1f 100644 --- a/gerber/render/apertures.py +++ b/gerber/render/apertures.py @@ -29,7 +29,7 @@ class Aperture(object): """ def draw(self, ctx, x, y): raise NotImplementedError('The draw method must be implemented in an Aperture subclass.') - + def flash(self, ctx, x, y): raise NotImplementedError('The flash method must be implemented in an Aperture subclass.') @@ -40,19 +40,22 @@ class Circle(Aperture): def __init__(self, diameter=0.0): self.diameter = diameter + class Rect(Aperture): """ Rectangular Aperture base class """ def __init__(self, size=(0, 0)): self.size = size + class Obround(Aperture): """ Obround Aperture base class """ def __init__(self, size=(0, 0)): self.size = size - + + class Polygon(Aperture): """ Polygon Aperture base class """ - pass \ No newline at end of file + pass diff --git a/gerber/render/render.py b/gerber/render/render.py index e15a36f..c372783 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -34,11 +34,11 @@ class GerberContext(object): level_polarity = 'dark' def __init__(self): - pass + pass def set_format(self, settings): self.settings = settings - + def set_coord_format(self, zero_suppression, format, notation): self.settings['zero_suppression'] = zero_suppression self.settings['format'] = format @@ -52,9 +52,9 @@ class GerberContext(object): def set_image_polarity(self, polarity): self.image_polarity = polarity - + def set_level_polarity(self, polarity): - self.level_polarity = polarity + self.level_polarity = polarity def set_interpolation(self, interpolation): self.interpolation = 'linear' if interpolation in ("G01", "G1") else 'arc' @@ -63,8 +63,8 @@ class GerberContext(object): self.aperture = d def resolve(self, x, y): - return x or self.x, y or self.y - + return x or self.x, y or self.y + def define_aperture(self, d, shape, modifiers): pass diff --git a/gerber/render/svg.py b/gerber/render/svg.py index b16e534..7d5c8fd 100644 --- a/gerber/render/svg.py +++ b/gerber/render/svg.py @@ -33,8 +33,8 @@ class SvgCircle(Circle): def flash(self, ctx, x, y): return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (self.diameter / 2.0), - fill='rgb(184, 115, 51)'),] + r = SCALE * (self.diameter / 2.0), + fill='rgb(184, 115, 51)'), ] class SvgRect(Rect): @@ -47,41 +47,42 @@ class SvgRect(Rect): def flash(self, ctx, x, y): xsize, ysize = self.size return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), - -SCALE * (y + (ysize / 2))), - size=(SCALE * xsize, SCALE * ysize), - fill="rgb(184, 115, 51)"),] + -SCALE * (y + (ysize / 2))), + size=(SCALE * xsize, SCALE * ysize), + fill="rgb(184, 115, 51)"), ] + class SvgObround(Obround): def draw(self, ctx, x, y): pass - + def flash(self, ctx, x, y): xsize, ysize = self.size - + # horizontal obround if xsize == ysize: return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (x / 2.0), - fill='rgb(184, 115, 51)'),] + r = SCALE * (x / 2.0), + fill='rgb(184, 115, 51)'), ] if xsize > ysize: rectx = xsize - ysize recty = ysize lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, - -y * SCALE), + -y * SCALE), r = SCALE * (ysize / 2.0), fill='rgb(184, 115, 51)') - + rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, - -y * SCALE), + -y * SCALE), r = SCALE * (ysize / 2.0), fill='rgb(184, 115, 51)') - + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill='rgb(184, 115, 51)') - return [lcircle, rcircle, rect,] - + return [lcircle, rcircle, rect, ] + # Vertical obround else: rectx = xsize @@ -90,18 +91,18 @@ class SvgObround(Obround): (y - (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), fill='rgb(184, 115, 51)') - + ucircle = ctx.dwg.circle(center=(x * SCALE, (y + (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), fill='rgb(184, 115, 51)') - + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill='rgb(184, 115, 51)') - return [lcircle, ucircle, rect,] - + return [lcircle, ucircle, rect, ] + class GerberSvgContext(GerberContext): def __init__(self): @@ -112,10 +113,9 @@ class GerberSvgContext(GerberContext): #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) def set_bounds(self, bounds): - xbounds, ybounds = bounds + xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) - def define_aperture(self, d, shape, modifiers): aperture = None @@ -133,8 +133,7 @@ class GerberSvgContext(GerberContext): if self.interpolation == 'linear': self.line(x, y) elif self.interpolation == 'arc': - #self.arc(x, y) - self.line(x,y) + self.arc(x, y) def line(self, x, y): super(GerberSvgContext, self).line(x, y) @@ -145,11 +144,9 @@ class GerberSvgContext(GerberContext): self.dwg.add(ap.draw(self, x, y)) self.move(x, y, resolve=False) - def arc(self, x, y): super(GerberSvgContext, self).arc(x, y) - def flash(self, x, y): super(GerberSvgContext, self).flash(x, y) x, y = self.resolve(x, y) @@ -160,12 +157,9 @@ class GerberSvgContext(GerberContext): self.dwg.add(shape) self.move(x, y, resolve=False) - def drill(self, x, y, diameter): hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') self.dwg.add(hit) def dump(self, filename): self.dwg.saveas(filename) - - -- cgit From f8449ad2b60b8a715d0867325e257a8297193b49 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 30 Sep 2014 23:42:02 -0400 Subject: tests update --- gerber/render/__init__.py | 2 +- gerber/render/svg.py | 165 -------------------------------------- gerber/render/svgwrite_backend.py | 165 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 166 deletions(-) delete mode 100644 gerber/render/svg.py create mode 100644 gerber/render/svgwrite_backend.py (limited to 'gerber/render') diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index cc87ee0..0d3527b 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -24,5 +24,5 @@ SVG is the only supported format. """ -from svg import GerberSvgContext +from svgwrite_backend import GerberSvgContext diff --git a/gerber/render/svg.py b/gerber/render/svg.py deleted file mode 100644 index 7d5c8fd..0000000 --- a/gerber/render/svg.py +++ /dev/null @@ -1,165 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2014 Hamilton Kibbe -# Based on render_svg.py by 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. - -from .render import GerberContext -from .apertures import Circle, Rect, Obround, Polygon -import svgwrite - -SCALE = 300 - - -class SvgCircle(Circle): - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", - stroke_width=SCALE * self.diameter, - stroke_linecap="round") - - def flash(self, ctx, x, y): - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (self.diameter / 2.0), - fill='rgb(184, 115, 51)'), ] - - -class SvgRect(Rect): - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", stroke_width=2, - stroke_linecap="butt") - - def flash(self, ctx, x, y): - xsize, ysize = self.size - return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), - -SCALE * (y + (ysize / 2))), - size=(SCALE * xsize, SCALE * ysize), - fill="rgb(184, 115, 51)"), ] - - -class SvgObround(Obround): - def draw(self, ctx, x, y): - pass - - def flash(self, ctx, x, y): - xsize, ysize = self.size - - # horizontal obround - if xsize == ysize: - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (x / 2.0), - fill='rgb(184, 115, 51)'), ] - if xsize > ysize: - rectx = xsize - ysize - recty = ysize - lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') - - rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') - return [lcircle, rcircle, rect, ] - - # Vertical obround - else: - rectx = xsize - recty = ysize - xsize - lcircle = ctx.dwg.circle(center=(x * SCALE, - (y - (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') - - ucircle = ctx.dwg.circle(center=(x * SCALE, - (y + (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') - return [lcircle, ucircle, rect, ] - - -class GerberSvgContext(GerberContext): - def __init__(self): - GerberContext.__init__(self) - - self.apertures = {} - self.dwg = svgwrite.Drawing() - #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) - - def set_bounds(self, bounds): - xbounds, ybounds = bounds - size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) - - def define_aperture(self, d, shape, modifiers): - aperture = None - if shape == 'C': - aperture = SvgCircle(diameter=float(modifiers[0][0])) - elif shape == 'R': - aperture = SvgRect(size=modifiers[0][0:2]) - elif shape == 'O': - aperture = SvgObround(size=modifiers[0][0:2]) - self.apertures[d] = aperture - - def stroke(self, x, y): - super(GerberSvgContext, self).stroke(x, y) - - if self.interpolation == 'linear': - self.line(x, y) - elif self.interpolation == 'arc': - self.arc(x, y) - - def line(self, x, y): - super(GerberSvgContext, self).line(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - self.dwg.add(ap.draw(self, x, y)) - self.move(x, y, resolve=False) - - def arc(self, x, y): - super(GerberSvgContext, self).arc(x, y) - - def flash(self, x, y): - super(GerberSvgContext, self).flash(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - for shape in ap.flash(self, x, y): - self.dwg.add(shape) - self.move(x, y, resolve=False) - - def drill(self, x, y, diameter): - hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') - self.dwg.add(hit) - - def dump(self, filename): - self.dwg.saveas(filename) diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py new file mode 100644 index 0000000..7d5c8fd --- /dev/null +++ b/gerber/render/svgwrite_backend.py @@ -0,0 +1,165 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe +# Based on render_svg.py by 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. + +from .render import GerberContext +from .apertures import Circle, Rect, Obround, Polygon +import svgwrite + +SCALE = 300 + + +class SvgCircle(Circle): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", + stroke_width=SCALE * self.diameter, + stroke_linecap="round") + + def flash(self, ctx, x, y): + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (self.diameter / 2.0), + fill='rgb(184, 115, 51)'), ] + + +class SvgRect(Rect): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", stroke_width=2, + stroke_linecap="butt") + + def flash(self, ctx, x, y): + xsize, ysize = self.size + return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), + -SCALE * (y + (ysize / 2))), + size=(SCALE * xsize, SCALE * ysize), + fill="rgb(184, 115, 51)"), ] + + +class SvgObround(Obround): + def draw(self, ctx, x, y): + pass + + def flash(self, ctx, x, y): + xsize, ysize = self.size + + # horizontal obround + if xsize == ysize: + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (x / 2.0), + fill='rgb(184, 115, 51)'), ] + if xsize > ysize: + rectx = xsize - ysize + recty = ysize + lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, rcircle, rect, ] + + # Vertical obround + else: + rectx = xsize + recty = ysize - xsize + lcircle = ctx.dwg.circle(center=(x * SCALE, + (y - (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + ucircle = ctx.dwg.circle(center=(x * SCALE, + (y + (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, ucircle, rect, ] + + +class GerberSvgContext(GerberContext): + def __init__(self): + GerberContext.__init__(self) + + self.apertures = {} + self.dwg = svgwrite.Drawing() + #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) + + def set_bounds(self, bounds): + xbounds, ybounds = bounds + size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + + def define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + aperture = SvgCircle(diameter=float(modifiers[0][0])) + elif shape == 'R': + aperture = SvgRect(size=modifiers[0][0:2]) + elif shape == 'O': + aperture = SvgObround(size=modifiers[0][0:2]) + self.apertures[d] = aperture + + def stroke(self, x, y): + super(GerberSvgContext, self).stroke(x, y) + + if self.interpolation == 'linear': + self.line(x, y) + elif self.interpolation == 'arc': + self.arc(x, y) + + def line(self, x, y): + super(GerberSvgContext, self).line(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + self.dwg.add(ap.draw(self, x, y)) + self.move(x, y, resolve=False) + + def arc(self, x, y): + super(GerberSvgContext, self).arc(x, y) + + def flash(self, x, y): + super(GerberSvgContext, self).flash(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + for shape in ap.flash(self, x, y): + self.dwg.add(shape) + self.move(x, y, resolve=False) + + def drill(self, x, y, diameter): + hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') + self.dwg.add(hit) + + def dump(self, filename): + self.dwg.saveas(filename) -- cgit From e565624b8181ea0a9dd5ea1585025a4eec72ac18 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Oct 2014 11:50:38 -0400 Subject: Fix import error --- gerber/render/render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index c372783..eab7d33 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -16,7 +16,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ..statements import ( +from ..gerber_statements import ( CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt ) -- cgit From 5ff44efbcfca5316796a1ea0191b2a92894a59ee Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 7 Oct 2014 18:41:14 -0400 Subject: Fix resolve error --- gerber/render/render.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index eab7d33..e40960d 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -63,7 +63,9 @@ class GerberContext(object): self.aperture = d def resolve(self, x, y): - return x or self.x, y or self.y + x = x if x is not None else self.x + y = y if y is not None else self.y + return x, y def define_aperture(self, d, shape, modifiers): pass -- cgit From 1653ae5cbe88757e453bccf499dc1b8ccb278e58 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 8 Oct 2014 09:27:52 -0400 Subject: Update readme and example --- gerber/render/svgwrite_backend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 7d5c8fd..8d84da1 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -110,12 +110,14 @@ class GerberSvgContext(GerberContext): self.apertures = {} self.dwg = svgwrite.Drawing() - #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) + self.background = False def set_bounds(self, bounds): xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + if not self.background: + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + self.background = True def define_aperture(self, d, shape, modifiers): aperture = None -- cgit From bcb6cbc50dea975954b8a3864690f68ab5e984b7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 8 Oct 2014 22:49:49 -0400 Subject: start arc --- gerber/render/apertures.py | 21 ++++++++++-- gerber/render/render.py | 42 ++++++++++++++--------- gerber/render/svgwrite_backend.py | 70 ++++++++++++++++++++++++--------------- 3 files changed, 88 insertions(+), 45 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py index f163b1f..52ae50c 100644 --- a/gerber/render/apertures.py +++ b/gerber/render/apertures.py @@ -22,16 +22,31 @@ gerber.render.apertures This module provides base classes for gerber apertures. These are used by the rendering engine to draw the gerber file. """ - +import math class Aperture(object): """ Gerber Aperture base class """ def draw(self, ctx, x, y): - raise NotImplementedError('The draw method must be implemented in an Aperture subclass.') + raise NotImplementedError('The draw method must be implemented \ + in an Aperture subclass.') def flash(self, ctx, x, y): - raise NotImplementedError('The flash method must be implemented in an Aperture subclass.') + raise NotImplementedError('The flash method must be implemented \ + in an Aperture subclass.') + + def _arc_params(self, startx, starty, x, y, i, j): + center = (startx + i, starty + j) + radius = math.sqrt(math.pow(center[0] - x, 2) + + math.pow(center[1] - y, 2)) + delta_x0 = startx - center[0] + delta_y0 = center[1] - starty + delta_x1 = x - center[0] + delta_y1 = center[1] - y + start_angle = math.atan2(delta_y0, delta_x0) + end_angle = math.atan2(delta_y1, delta_x1) + return {'center': center, 'radius': radius, + 'start_angle': start_angle, 'end_angle': end_angle} class Circle(Aperture): diff --git a/gerber/render/render.py b/gerber/render/render.py index e40960d..e7ec6ee 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -22,19 +22,20 @@ from ..gerber_statements import ( class GerberContext(object): - settings = {} - - x = 0 - y = 0 - - aperture = 0 - interpolation = 'linear' - - image_polarity = 'positive' - level_polarity = 'dark' def __init__(self): - pass + self.settings = {} + self.x = 0 + self.y = 0 + + self.aperture = 0 + self.interpolation = 'linear' + self.direction = 'clockwise' + self.image_polarity = 'positive' + self.level_polarity = 'dark' + self.region_mode = 'off' + self.color = (0.7215, 0.451, 0.200) + self.drill_color = (0.25, 0.25, 0.25) def set_format(self, settings): self.settings = settings @@ -62,6 +63,12 @@ class GerberContext(object): def set_aperture(self, d): self.aperture = d + def set_color(self, color): + self.color = color + + def set_drill_color(self, color): + self.drill_color = color + def resolve(self, x, y): x = x if x is not None else self.x y = y if y is not None else self.y @@ -76,13 +83,13 @@ class GerberContext(object): else: self.x, self.y = x, y - def stroke(self, x, y): + def stroke(self, x, y, i, j): pass def line(self, x, y): pass - def arc(self, x, y): + def arc(self, x, y, i, j): pass def flash(self, x, y): @@ -109,7 +116,8 @@ class GerberContext(object): def _evaluate_param(self, stmt): if stmt.param == "FS": - self.set_coord_format(stmt.zero_suppression, stmt.format, stmt.notation) + self.set_coord_format(stmt.zero_suppression, stmt.format, + stmt.notation) self.set_coord_notation(stmt.notation) elif stmt.param == "MO:": self.set_coord_unit(stmt.mode) @@ -123,9 +131,11 @@ class GerberContext(object): def _evaluate_coord(self, stmt): if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): self.set_interpolation(stmt.function) - + if stmt.function not in ('G01', 'G1'): + self.direction = ('clockwise' if stmt.function in ('G02', 'G2') + else 'counterclockwise') if stmt.op == "D01": - self.stroke(stmt.x, stmt.y) + self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) elif stmt.op == "D02": self.move(stmt.x, stmt.y) elif stmt.op == "D03": diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 8d84da1..3b2f3c1 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -23,64 +23,71 @@ import svgwrite SCALE = 300 +def convert_color(color): + color = tuple([int(ch * 255) for ch in color]) + return 'rgb(%d, %d, %d)' % color + class SvgCircle(Circle): - def draw(self, ctx, x, y): + def line(self, ctx, x, y, color='rgb(184, 115, 51)'): return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", + stroke=color, stroke_width=SCALE * self.diameter, stroke_linecap="round") - def flash(self, ctx, x, y): + def arc(self, ctx, x, y, i, j, direction, color='rgb(184, 115, 51)'): + pass + + def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), r = SCALE * (self.diameter / 2.0), - fill='rgb(184, 115, 51)'), ] + fill=color), ] class SvgRect(Rect): - def draw(self, ctx, x, y): + def line(self, ctx, x, y, color='rgb(184, 115, 51)'): return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", stroke_width=2, + stroke=color, stroke_width=2, stroke_linecap="butt") - def flash(self, ctx, x, y): + def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): xsize, ysize = self.size return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), -SCALE * (y + (ysize / 2))), size=(SCALE * xsize, SCALE * ysize), - fill="rgb(184, 115, 51)"), ] + fill=color), ] class SvgObround(Obround): - def draw(self, ctx, x, y): + def line(self, ctx, x, y, color='rgb(184, 115, 51)'): pass - def flash(self, ctx, x, y): + def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): xsize, ysize = self.size # horizontal obround if xsize == ysize: return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), r = SCALE * (x / 2.0), - fill='rgb(184, 115, 51)'), ] + fill=color), ] if xsize > ysize: rectx = xsize - ysize recty = ysize lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, -y * SCALE), r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') + fill=color) rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, -y * SCALE), r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') + fill=color) rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') + fill=color) return [lcircle, rcircle, rect, ] # Vertical obround @@ -90,17 +97,17 @@ class SvgObround(Obround): lcircle = ctx.dwg.circle(center=(x * SCALE, (y - (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') + fill=color) ucircle = ctx.dwg.circle(center=(x * SCALE, (y + (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') + fill=color) rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') + fill=color) return [lcircle, ucircle, rect, ] @@ -116,7 +123,9 @@ class GerberSvgContext(GerberContext): xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) if not self.background: - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], + -SCALE * ybounds[1]), + size=size, fill="black")) self.background = True def define_aperture(self, d, shape, modifiers): @@ -129,13 +138,13 @@ class GerberSvgContext(GerberContext): aperture = SvgObround(size=modifiers[0][0:2]) self.apertures[d] = aperture - def stroke(self, x, y): - super(GerberSvgContext, self).stroke(x, y) + def stroke(self, x, y, i, j): + super(GerberSvgContext, self).stroke(x, y, i, j) if self.interpolation == 'linear': self.line(x, y) elif self.interpolation == 'arc': - self.arc(x, y) + self.arc(x, y, i, j) def line(self, x, y): super(GerberSvgContext, self).line(x, y) @@ -143,11 +152,18 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - self.dwg.add(ap.draw(self, x, y)) + self.dwg.add(ap.line(self, x, y, convert_color(self.color))) self.move(x, y, resolve=False) - def arc(self, x, y): - super(GerberSvgContext, self).arc(x, y) + def arc(self, x, y, i, j): + super(GerberSvgContext, self).arc(x, y, i, j) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + #self.dwg.add(ap.arc(self, x, y, i, j, self.direction, + # convert_color(self.color))) + self.move(x, y, resolve=False) def flash(self, x, y): super(GerberSvgContext, self).flash(x, y) @@ -155,12 +171,14 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - for shape in ap.flash(self, x, y): + for shape in ap.flash(self, x, y, convert_color(self.color)): self.dwg.add(shape) self.move(x, y, resolve=False) def drill(self, x, y, diameter): - hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') + hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), + r=SCALE*(diameter/2.0), + fill=convert_color(self.drill_color)) self.dwg.add(hit) def dump(self, filename): -- cgit From 84bfd34e918251ff82f4b3818bc6268feab72efe Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 9 Oct 2014 09:51:29 -0400 Subject: Add mode statement parsing --- gerber/render/render.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index e7ec6ee..e91c71e 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -16,8 +16,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ..gerber_statements import ( - CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt +from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, + CoordStmt, ApertureStmt, RegionModeStmt, + QuadrantModeStmt, ) @@ -111,9 +112,18 @@ class GerberContext(object): elif isinstance(stmt, ApertureStmt): self._evaluate_aperture(stmt) + elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): + self._evaluate_mode(stmt) + else: raise Exception("Invalid statement to evaluate") + def _evaluate_mode(self, stmt): + if stmt.type == 'RegionMode': + self.region_mode = stmt.mode + elif stmt.type == 'QuadrantMode': + self.quadrant_mode = stmt.mode + def _evaluate_param(self, stmt): if stmt.param == "FS": self.set_coord_format(stmt.zero_suppression, stmt.format, -- cgit From 8851bc17b94a921453b0afd9c2421cb30f8d4425 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 9 Oct 2014 18:09:17 -0400 Subject: Doc update --- gerber/render/render.py | 86 ++++++++++++++++++++++++++++++++++++++- gerber/render/svgwrite_backend.py | 8 +++- 2 files changed, 91 insertions(+), 3 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index e91c71e..8cfc5de 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -23,7 +23,60 @@ from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, class GerberContext(object): - + """ Gerber rendering context base class + + Provides basic functionality and API for rendering gerber files. Medium- + specific renderers should subclass GerberContext and implement the drawing + functions. Colors are stored internally as 32-bit RGB and may need to be + converted to a native format in the rendering subclass. + + Attributes + ---------- + settings : FileSettings (dict-like) + Gerber file settings + + x : float + X-coordinate of the "photoplotter" head. + + y : float + Y-coordinate of the "photoplotter" head + + aperture : int + The aperture that is currently in use + + interpolation : str + Current interpolation mode. may be 'linear' or 'arc' + + direction : string + Current arc direction. May be either 'clockwise' or 'counterclockwise' + + image_polarity : string + Current image polarity setting. May be 'positive' or 'negative' + + level_polarity : string + Level polarity. May be 'dark' or 'clear'. Dark polarity indicates the + existance of copper/silkscreen/etc. in the exposed area, whereas clear + polarity indicates material should be removed from the exposed area. + + region_mode : string + Region mode. May be 'on' or 'off'. When region mode is set to 'on' the + following "contours" define the outline of a region. When region mode + is subsequently turned 'off', the defined area is filled. + + quadrant_mode : string + Quadrant mode. May be 'single-quadrant' or 'multi-quadrant'. Defines + how arcs are specified. + + color : tuple (, , ) + Color used for rendering as a tuple of normalized (red, green, blue) values. + + drill_color : tuple (, , ) + Color used for rendering drill hits. Format is the same as for `color`. + + background_color : tuple (, , ) + Color of the background. Used when exposing areas in 'clear' level + polarity mode. Format is the same as for `color`. + """ def __init__(self): self.settings = {} self.x = 0 @@ -35,13 +88,36 @@ class GerberContext(object): self.image_polarity = 'positive' self.level_polarity = 'dark' self.region_mode = 'off' + self.quadrant_mode = 'multi-quadrant' + 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) def set_format(self, settings): + """ Set source file format. + + Parameters + ---------- + settings : FileSettings instance or dict-like + Gerber file settings used in source file. + """ self.settings = settings def set_coord_format(self, zero_suppression, format, notation): + """ Set coordinate format used in source gerber file + + Parameters + ---------- + zero_suppression : string + Zero suppression mode. may be 'leading' or 'trailling' + + format : tuple (, ) + decimal precision format + + notation : string + notation mode. 'absolute' or 'incremental' + """ self.settings['zero_suppression'] = zero_suppression self.settings['format'] = format self.settings['notation'] = notation @@ -69,6 +145,9 @@ class GerberContext(object): def set_drill_color(self, color): self.drill_color = color + + def set_background_color(self, color): + self.background_color = color def resolve(self, x, y): x = x if x is not None else self.x @@ -120,6 +199,8 @@ class GerberContext(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': + if self.region_mode == 'on' and stmt.mode == 'off': + self._fill_region() self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': self.quadrant_mode = stmt.mode @@ -153,3 +234,6 @@ class GerberContext(object): def _evaluate_aperture(self, stmt): self.set_aperture(stmt.d) + + def _fill_region(self): + pass diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 3b2f3c1..7570c84 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -152,7 +152,9 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - self.dwg.add(ap.line(self, x, y, convert_color(self.color))) + color = (convert_color(self.color) if self.level_polarity == 'dark' + else convert_color(self.background_color)) + self.dwg.add(ap.line(self, x, y, color)) self.move(x, y, resolve=False) def arc(self, x, y, i, j): @@ -171,7 +173,9 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - for shape in ap.flash(self, x, y, convert_color(self.color)): + color = (convert_color(self.color) if self.level_polarity == 'dark' + else convert_color(self.background_color)) + for shape in ap.flash(self, x, y, color): self.dwg.add(shape) self.move(x, y, resolve=False) -- cgit From bf9f9451f555a47651e414faf839d8d83441c737 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 9 Oct 2014 21:52:04 -0400 Subject: doc update --- gerber/render/render.py | 227 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 187 insertions(+), 40 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index 8cfc5de..db3c743 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -24,57 +24,57 @@ from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, class GerberContext(object): """ Gerber rendering context base class - + Provides basic functionality and API for rendering gerber files. Medium- specific renderers should subclass GerberContext and implement the drawing - functions. Colors are stored internally as 32-bit RGB and may need to be + functions. Colors are stored internally as 32-bit RGB and may need to be converted to a native format in the rendering subclass. - + Attributes ---------- settings : FileSettings (dict-like) Gerber file settings - + x : float - X-coordinate of the "photoplotter" head. - + X-coordinate of the "photoplotter" head. + y : float Y-coordinate of the "photoplotter" head - + aperture : int The aperture that is currently in use - + interpolation : str Current interpolation mode. may be 'linear' or 'arc' - + direction : string Current arc direction. May be either 'clockwise' or 'counterclockwise' - + image_polarity : string Current image polarity setting. May be 'positive' or 'negative' - + level_polarity : string - Level polarity. May be 'dark' or 'clear'. Dark polarity indicates the - existance of copper/silkscreen/etc. in the exposed area, whereas clear - polarity indicates material should be removed from the exposed area. - + Level polarity. May be 'dark' or 'clear'. Dark polarity indicates the + existance of copper/silkscreen/etc. in the exposed area, whereas clear + polarity indicates material should be removed from the exposed area. + region_mode : string Region mode. May be 'on' or 'off'. When region mode is set to 'on' the - following "contours" define the outline of a region. When region mode + following "contours" define the outline of a region. When region mode is subsequently turned 'off', the defined area is filled. - + quadrant_mode : string Quadrant mode. May be 'single-quadrant' or 'multi-quadrant'. Defines how arcs are specified. - + color : tuple (, , ) Color used for rendering as a tuple of normalized (red, green, blue) values. - + drill_color : tuple (, , ) Color used for rendering drill hits. Format is the same as for `color`. - + background_color : tuple (, , ) - Color of the background. Used when exposing areas in 'clear' level + Color of the background. Used when exposing areas in 'clear' level polarity mode. Format is the same as for `color`. """ def __init__(self): @@ -89,14 +89,14 @@ class GerberContext(object): self.level_polarity = 'dark' self.region_mode = 'off' self.quadrant_mode = 'multi-quadrant' - + 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) def set_format(self, settings): """ Set source file format. - + Parameters ---------- settings : FileSettings instance or dict-like @@ -104,52 +104,178 @@ class GerberContext(object): """ self.settings = settings - def set_coord_format(self, zero_suppression, format, notation): + def set_coord_format(self, zero_suppression, decimal_format, notation): """ Set coordinate format used in source gerber file - + Parameters ---------- zero_suppression : string Zero suppression mode. may be 'leading' or 'trailling' - - format : tuple (, ) - decimal precision format - + + decimal_format : tuple (, ) + Decimal precision format specified as (integer digits, decimal digits) + notation : string - notation mode. 'absolute' or 'incremental' + Notation mode. 'absolute' or 'incremental' """ + if zero_suppression not in ('leading', 'trailling'): + raise ValueError('Zero suppression must be "leading" or "trailing"') self.settings['zero_suppression'] = zero_suppression - self.settings['format'] = format + self.settings['format'] = decimal_format self.settings['notation'] = notation def set_coord_notation(self, notation): + """ Set context notation mode + + Parameters + ---------- + notation : string + Notation mode. may be 'absolute' or 'incremental' + + Raises + ------ + ValueError + If `notation` is not either "absolute" or "incremental" + + """ + if notation not in ('absolute', 'incremental'): + raise ValueError('Notation may be "absolute" or "incremental"') self.settings['notation'] = notation def set_coord_unit(self, unit): + """ Set context measurement units + + Parameters + ---------- + unit : string + Measurement units. may be 'inch' or 'metric' + + Raises + ------ + ValueError + If `unit` is not 'inch' or 'metric' + """ + if unit not in ('inch', 'metric'): + raise ValueError('Unit may be "inch" or "metric"') self.settings['units'] = unit def set_image_polarity(self, polarity): + """ Set context image polarity + + Parameters + ---------- + polarity : string + Image polarity. May be "positive" or "negative" + + Raises + ------ + ValueError + If polarity is not 'positive' or 'negative' + """ + if polarity not in ('positive', 'negative'): + raise ValueError('Polarity may be "positive" or "negative"') self.image_polarity = polarity def set_level_polarity(self, polarity): + """ Set context level polarity + + Parameters + ---------- + polarity : string + Level polarity. May be "dark" or "clear" + + Raises + ------ + ValueError + If polarity is not 'dark' or 'clear' + """ + if polarity not in ('dark', 'clear'): + raise ValueError('Polarity may be "dark" or "clear"') self.level_polarity = polarity def set_interpolation(self, interpolation): - self.interpolation = 'linear' if interpolation in ("G01", "G1") else 'arc' + """ Set arc interpolation mode + + Parameters + ---------- + interpolation : string + Interpolation mode. May be 'linear' or 'arc' + + Raises + ------ + ValueError + If `interpolation` is not 'linear' or 'arc' + """ + if interpolation not in ('linear', 'arc'): + raise ValueError('Interpolation may be "linear" or "arc"') + self.interpolation = interpolation def set_aperture(self, d): + """ Set active aperture + + Parameters + ---------- + aperture : int + Aperture number to activate. + """ self.aperture = d def set_color(self, color): + """ Set rendering color. + + Parameters + ---------- + color : tuple (, , ) + Color as a tuple of (red, green, blue) values. Each channel is + represented as a float value in (0, 1) + """ self.color = color def set_drill_color(self, color): - self.drill_color = color - + """ Set color used for rendering drill hits. + + Parameters + ---------- + color : tuple (, , ) + Color as a tuple of (red, green, blue) values. Each channel is + represented as a float value in (0, 1) + """ + self.drill_color = color + def set_background_color(self, color): + """ Set rendering background color + + Parameters + ---------- + color : tuple (, , ) + Color as a tuple of (red, green, blue) values. Each channel is + represented as a float value in (0, 1) + """ self.background_color = color def resolve(self, x, y): + """ Resolve missing x or y coordinates in a coordinate command. + + Replace missing x or y values with the current x or y position. This + is the default method for handling coordinate pairs pulled from gerber + file statments, as a move/line/arc involving a change in only one axis + will drop the redundant axis coordinate to reduce file size. + + Parameters + ---------- + x : float + X-coordinate. If `None`, will be replaced with current + "photoplotter" head x-coordinate + + y : float + Y-coordinate. If `None`, will be replaced with current + "photoplotter" head y-coordinate + + Returns + ------- + coordinates : tuple (, ) + Coordinates in absolute notation + """ x = x if x is not None else self.x y = y if y is not None else self.y return x, y @@ -158,6 +284,26 @@ class GerberContext(object): pass def move(self, x, y, resolve=True): + """ Lights-off move. + + Move the "photoplotter" head to (x, y) without drawing a line. If x or + y is `None`, remain at the same point in that axis. + + Parameters + ----------- + x : float + X-coordinate to move to. If x is `None`, do not move in the X + direction + + y : float + Y-coordinate to move to. if y is `None`, do not move in the Y + direction + + resolve : bool + If resolve is `True` the context will replace missing x or y + coordinates with the current plotter head position. This is the + default behavior. + """ if resolve: self.x, self.y = self.resolve(x, y) else: @@ -220,11 +366,12 @@ class GerberContext(object): self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) def _evaluate_coord(self, stmt): - if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): - self.set_interpolation(stmt.function) - if stmt.function not in ('G01', 'G1'): - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') - else 'counterclockwise') + if stmt.function in ("G01", "G1"): + self.set_interpolation('linear') + elif stmt.function in ('G02', 'G2', 'G03', 'G3'): + self.set_interpolation('arc') + self.direction = ('clockwise' if stmt.function in ('G02', 'G2') + else 'counterclockwise') if stmt.op == "D01": self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) elif stmt.op == "D02": @@ -234,6 +381,6 @@ class GerberContext(object): def _evaluate_aperture(self, stmt): self.set_aperture(stmt.d) - + def _fill_region(self): pass -- cgit From f2f411493ea303075d5dbdd7656c572dda61cf67 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 9 Oct 2014 22:10:28 -0400 Subject: doc update --- gerber/render/render.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index db3c743..f2d23b4 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -15,7 +15,13 @@ # 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. +""" +Rendering +============ +**Gerber (RS-274X) and Excellon file rendering** +Render Gerber and Excellon files to a variety of formats. +""" from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt, RegionModeStmt, QuadrantModeStmt, -- cgit From a9059df190be0238ce0e6fca8c59700e92ddf205 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 10 Oct 2014 09:35:06 -0400 Subject: doc update --- gerber/render/render.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index f2d23b4..e76aed1 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -20,7 +20,8 @@ Rendering ============ **Gerber (RS-274X) and Excellon file rendering** -Render Gerber and Excellon files to a variety of formats. +Render Gerber and Excellon files to a variety of formats. The render module +currently supports SVG rendering using the `svgwrite` library. """ from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt, RegionModeStmt, @@ -316,21 +317,118 @@ class GerberContext(object): self.x, self.y = x, y def stroke(self, x, y, i, j): + """ Lights-on move. (draws a line or arc) + + The stroke method is called when a Lights-on move statement is + encountered. This will call the `line` or `arc` method as necessary + based on the move statement's parameters. The `stroke` method should + be overridden in `GerberContext` subclasses. + + Parameters + ---------- + x : float + X coordinate of target position + + y : float + Y coordinate of target position + + i : float + Offset in X-direction from current position of arc center. + + j : float + Offset in Y-direction from current position of arc center. + """ pass def line(self, x, y): + """ Draw a line + + Draws a line from the current position to (x, y) using the currently + selected aperture. The `line` method should be overridden in + `GerberContext` subclasses. + + Parameters + ---------- + x : float + X coordinate of target position + + y : float + Y coordinate of target position + """ pass def arc(self, x, y, i, j): + """ Draw an arc + + Draw an arc from the current position to (x, y) using the currently + selected aperture. `i` and `j` specify the offset from the starting + position to the center of the arc.The `arc` method should be + overridden in `GerberContext` subclasses. + + Parameters + ---------- + x : float + X coordinate of target position + + y : float + Y coordinate of target position + + i : float + Offset in X-direction from current position of arc center. + + j : float + Offset in Y-direction from current position of arc center. + """ pass def flash(self, x, y): + """ Flash the current aperture + + Draw a filled shape defined by the currently selected aperture. + + Parameters + ---------- + x : float + X coordinate of the position at which to flash + + y : float + Y coordinate of the position at which to flash + """ pass def drill(self, x, y, diameter): + """ Draw a drill hit + + Draw a filled circle representing a drill hit at the specified + position and with the specified diameter. + + Parameters + ---------- + x : float + X coordinate of the drill hit + + y : float + Y coordinate of the drill hit + + diameter : float + Finished hole diameter to draw. + """ pass def evaluate(self, stmt): + """ Evaluate Gerber statement and update image accordingly. + + This method is called once for each statement in a Gerber/Excellon + file when the file's `render` method is called. The evaluate method + should forward the statement on to the relevant handling method based + on the statement type. + + Parameters + ---------- + statement : Statement + Gerber/Excellon statement to evaluate. + + """ if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): return -- cgit From 76c03a55c91addff71339d80cf17560926f1580b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 10 Oct 2014 20:36:38 -0400 Subject: Working region fills and level polarity. Renders Altium-generated gerbers like a champ! --- gerber/render/render.py | 23 +++++++++++++++-------- gerber/render/svgwrite_backend.py | 22 ++++++++++++++++++++-- 2 files changed, 35 insertions(+), 10 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index e76aed1..48a53f8 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -96,7 +96,7 @@ class GerberContext(object): self.level_polarity = 'dark' self.region_mode = 'off' self.quadrant_mode = 'multi-quadrant' - + self.step_and_repeat = (1, 1, 0, 0) 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) @@ -415,6 +415,12 @@ class GerberContext(object): """ pass + def region_contour(self, x, y): + pass + + def fill_region(self): + pass + def evaluate(self, stmt): """ Evaluate Gerber statement and update image accordingly. @@ -450,7 +456,7 @@ class GerberContext(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self._fill_region() + self.fill_region() self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': self.quadrant_mode = stmt.mode @@ -460,11 +466,11 @@ class GerberContext(object): self.set_coord_format(stmt.zero_suppression, stmt.format, stmt.notation) self.set_coord_notation(stmt.notation) - elif stmt.param == "MO:": + elif stmt.param == "MO": self.set_coord_unit(stmt.mode) - elif stmt.param == "IP:": + elif stmt.param == "IP": self.set_image_polarity(stmt.ip) - elif stmt.param == "LP:": + elif stmt.param == "LP": self.set_level_polarity(stmt.lp) elif stmt.param == "AD": self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) @@ -477,7 +483,10 @@ class GerberContext(object): self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') if stmt.op == "D01": - self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) + if self.region_mode == 'on': + self.region_contour(stmt.x, stmt.y) + else: + self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) elif stmt.op == "D02": self.move(stmt.x, stmt.y) elif stmt.op == "D03": @@ -486,5 +495,3 @@ class GerberContext(object): def _evaluate_aperture(self, stmt): self.set_aperture(stmt.d) - def _fill_region(self): - pass diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 7570c84..886b4f8 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -118,6 +118,7 @@ class GerberSvgContext(GerberContext): self.apertures = {} self.dwg = svgwrite.Drawing() self.background = False + self.region_path = None def set_bounds(self, bounds): xbounds, ybounds = bounds @@ -125,7 +126,7 @@ class GerberSvgContext(GerberContext): if not self.background: self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), - size=size, fill="black")) + size=size, fill=convert_color(self.background_color))) self.background = True def define_aperture(self, d, shape, modifiers): @@ -173,7 +174,8 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - color = (convert_color(self.color) if self.level_polarity == 'dark' + + color = (convert_color(self.color) if self.level_polarity == 'dark' else convert_color(self.background_color)) for shape in ap.flash(self, x, y, color): self.dwg.add(shape) @@ -185,5 +187,21 @@ class GerberSvgContext(GerberContext): fill=convert_color(self.drill_color)) self.dwg.add(hit) + def region_contour(self, x, y): + super(GerberSvgContext, self).region_contour(x, y) + x, y = self.resolve(x, y) + color = (convert_color(self.color) if self.level_polarity == 'dark' + else convert_color(self.background_color)) + if self.region_path is None: + self.region_path = self.dwg.path(d = 'M %f, %f' % + (self.x*SCALE, -self.y*SCALE), + fill = color, stroke = 'none') + self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE)) + self.move(x, y, resolve=False) + + def fill_region(self): + self.dwg.add(self.region_path) + self.region_path = None + def dump(self, filename): self.dwg.saveas(filename) -- cgit From 8c5c7ec8bbc8a074884ef04b566f9c0ecd6e78bb Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 12 Oct 2014 12:38:40 -0400 Subject: update docs and example images --- gerber/render/render.py | 19 ++++++++++++++++++- gerber/render/svgwrite_backend.py | 7 +++++++ 2 files changed, 25 insertions(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index 48a53f8..f7e4485 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -83,6 +83,9 @@ class GerberContext(object): background_color : tuple (, , ) Color of the background. Used when exposing areas in 'clear' level polarity mode. Format is the same as for `color`. + + alpha : float + Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ def __init__(self): self.settings = {} @@ -100,7 +103,8 @@ class GerberContext(object): 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 + def set_format(self, settings): """ Set source file format. @@ -260,6 +264,19 @@ class GerberContext(object): """ self.background_color = color + def set_alpha(self, alpha): + """ Set layer rendering opacity + + .. note:: + Not all backends/rendering devices support this parameter. + + Parameters + ---------- + alpha : float + Rendering opacity. must be between 0.0 (transparent) and 1.0 (opaque) + """ + self.alpha = alpha + def resolve(self, x, y): """ Resolve missing x or y coordinates in a coordinate command. diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 886b4f8..78961da 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -117,6 +117,7 @@ class GerberSvgContext(GerberContext): self.apertures = {} self.dwg = svgwrite.Drawing() + self.dwg.transform = 'scale 1 -1' self.background = False self.region_path = None @@ -124,11 +125,17 @@ class GerberSvgContext(GerberContext): xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) if not self.background: + self.dwg = svgwrite.Drawing(viewBox='%f, %f, %f, %f' % (SCALE*xbounds[0], -SCALE*ybounds[1],size[0], size[1])) self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill=convert_color(self.background_color))) self.background = True + def set_alpha(self, alpha): + super(GerberSvgContext, self).set_alpha(alpha) + import warnings + warnings.warn('SVG output does not support transparency') + def define_aperture(self, d, shape, modifiers): aperture = None if shape == 'C': -- cgit From c50949e15a839ecd27a6da273ccaf1dc3a7d7853 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 13 Oct 2014 13:26:32 -0400 Subject: Add SVG transparency --- gerber/render/svgwrite_backend.py | 72 ++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 28 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 78961da..15d7bd3 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -28,49 +28,59 @@ def convert_color(color): return 'rgb(%d, %d, %d)' % color class SvgCircle(Circle): - def line(self, ctx, x, y, color='rgb(184, 115, 51)'): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke=color, - stroke_width=SCALE * self.diameter, - stroke_linecap="round") - - def arc(self, ctx, x, y, i, j, direction, color='rgb(184, 115, 51)'): + def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): + aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke=color, + stroke_width=SCALE * self.diameter, + stroke_linecap="round") + aline.stroke(opacity=alpha) + return aline + + def arc(self, ctx, x, y, i, j, direction, color='rgb(184, 115, 51)', alpha=1.0): pass - def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): + circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), r = SCALE * (self.diameter / 2.0), - fill=color), ] + fill=color) + circle.fill(opacity=alpha) + return [circle, ] class SvgRect(Rect): - def line(self, ctx, x, y, color='rgb(184, 115, 51)'): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): + aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), end=(x * SCALE, -y * SCALE), stroke=color, stroke_width=2, stroke_linecap="butt") + aline.stroke(opacity=alpha) + return aline - def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): + def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): xsize, ysize = self.size - return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), + rectangle = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), -SCALE * (y + (ysize / 2))), size=(SCALE * xsize, SCALE * ysize), - fill=color), ] + fill=color) + rectangle.fill(opacity=alpha) + return [rectangle, ] class SvgObround(Obround): - def line(self, ctx, x, y, color='rgb(184, 115, 51)'): + def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): pass - def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): + def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): xsize, ysize = self.size # horizontal obround if xsize == ysize: - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), r = SCALE * (x / 2.0), - fill=color), ] + fill=color) + circle.fill(opacity=alpha) + return [circle, ] if xsize > ysize: rectx = xsize - ysize recty = ysize @@ -88,6 +98,9 @@ class SvgObround(Obround): -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill=color) + lcircle.fill(opacity=alpha) + rcircle.fill(opacity=alpha) + rect.fill(opacity=alpha) return [lcircle, rcircle, rect, ] # Vertical obround @@ -108,6 +121,9 @@ class SvgObround(Obround): -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill=color) + lcircle.fill(opacity=alpha) + ucircle.fill(opacity=alpha) + rect.fill(opacity=alpha) return [lcircle, ucircle, rect, ] @@ -131,11 +147,6 @@ class GerberSvgContext(GerberContext): size=size, fill=convert_color(self.background_color))) self.background = True - def set_alpha(self, alpha): - super(GerberSvgContext, self).set_alpha(alpha) - import warnings - warnings.warn('SVG output does not support transparency') - def define_aperture(self, d, shape, modifiers): aperture = None if shape == 'C': @@ -162,7 +173,8 @@ class GerberSvgContext(GerberContext): return color = (convert_color(self.color) if self.level_polarity == 'dark' else convert_color(self.background_color)) - self.dwg.add(ap.line(self, x, y, color)) + alpha = self.alpha if self.level_polarity == 'dark' else 1.0 + self.dwg.add(ap.line(self, x, y, color, alpha)) self.move(x, y, resolve=False) def arc(self, x, y, i, j): @@ -172,7 +184,7 @@ class GerberSvgContext(GerberContext): if ap is None: return #self.dwg.add(ap.arc(self, x, y, i, j, self.direction, - # convert_color(self.color))) + # convert_color(self.color), self.alpha)) self.move(x, y, resolve=False) def flash(self, x, y): @@ -184,7 +196,8 @@ class GerberSvgContext(GerberContext): color = (convert_color(self.color) if self.level_polarity == 'dark' else convert_color(self.background_color)) - for shape in ap.flash(self, x, y, color): + alpha = self.alpha if self.level_polarity == 'dark' else 1.0 + for shape in ap.flash(self, x, y, color, alpha): self.dwg.add(shape) self.move(x, y, resolve=False) @@ -192,6 +205,7 @@ class GerberSvgContext(GerberContext): hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill=convert_color(self.drill_color)) + #hit.fill(opacity=self.alpha) self.dwg.add(hit) def region_contour(self, x, y): @@ -199,10 +213,12 @@ class GerberSvgContext(GerberContext): x, y = self.resolve(x, y) color = (convert_color(self.color) if self.level_polarity == 'dark' else convert_color(self.background_color)) + alpha = self.alpha if self.level_polarity == 'dark' else 1.0 if self.region_path is None: self.region_path = self.dwg.path(d = 'M %f, %f' % (self.x*SCALE, -self.y*SCALE), fill = color, stroke = 'none') + self.region_path.fill(opacity=alpha) self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE)) self.move(x, y, resolve=False) -- cgit From 6d2db67e6d0973ce26ce3a6700ca44295f73fea7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 18 Oct 2014 01:44:51 -0400 Subject: Refactor rendering --- gerber/render/render.py | 418 ++++---------------------------------- gerber/render/svgwrite_backend.py | 305 +++++++++++---------------- 2 files changed, 154 insertions(+), 569 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index f7e4485..f5c58d8 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -28,6 +28,7 @@ from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, QuadrantModeStmt, ) +from ..primitives import * class GerberContext(object): """ Gerber rendering context base class @@ -39,40 +40,8 @@ class GerberContext(object): Attributes ---------- - settings : FileSettings (dict-like) - Gerber file settings - - x : float - X-coordinate of the "photoplotter" head. - - y : float - Y-coordinate of the "photoplotter" head - - aperture : int - The aperture that is currently in use - - interpolation : str - Current interpolation mode. may be 'linear' or 'arc' - - direction : string - Current arc direction. May be either 'clockwise' or 'counterclockwise' - - image_polarity : string - Current image polarity setting. May be 'positive' or 'negative' - - level_polarity : string - Level polarity. May be 'dark' or 'clear'. Dark polarity indicates the - existance of copper/silkscreen/etc. in the exposed area, whereas clear - polarity indicates material should be removed from the exposed area. - - region_mode : string - Region mode. May be 'on' or 'off'. When region mode is set to 'on' the - following "contours" define the outline of a region. When region mode - is subsequently turned 'off', the defined area is filled. - - quadrant_mode : string - Quadrant mode. May be 'single-quadrant' or 'multi-quadrant'. Defines - how arcs are specified. + units : string + Measurement units color : tuple (, , ) Color used for rendering as a tuple of normalized (red, green, blue) values. @@ -87,73 +56,14 @@ class GerberContext(object): alpha : float Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ - def __init__(self): - self.settings = {} - self.x = 0 - self.y = 0 - - self.aperture = 0 - self.interpolation = 'linear' - self.direction = 'clockwise' - self.image_polarity = 'positive' - self.level_polarity = 'dark' - self.region_mode = 'off' - self.quadrant_mode = 'multi-quadrant' - self.step_and_repeat = (1, 1, 0, 0) + 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 - - def set_format(self, settings): - """ Set source file format. - - Parameters - ---------- - settings : FileSettings instance or dict-like - Gerber file settings used in source file. - """ - self.settings = settings - - def set_coord_format(self, zero_suppression, decimal_format, notation): - """ Set coordinate format used in source gerber file - Parameters - ---------- - zero_suppression : string - Zero suppression mode. may be 'leading' or 'trailling' - - decimal_format : tuple (, ) - Decimal precision format specified as (integer digits, decimal digits) - - notation : string - Notation mode. 'absolute' or 'incremental' - """ - if zero_suppression not in ('leading', 'trailling'): - raise ValueError('Zero suppression must be "leading" or "trailing"') - self.settings['zero_suppression'] = zero_suppression - self.settings['format'] = decimal_format - self.settings['notation'] = notation - - def set_coord_notation(self, notation): - """ Set context notation mode - - Parameters - ---------- - notation : string - Notation mode. may be 'absolute' or 'incremental' - - Raises - ------ - ValueError - If `notation` is not either "absolute" or "incremental" - - """ - if notation not in ('absolute', 'incremental'): - raise ValueError('Notation may be "absolute" or "incremental"') - self.settings['notation'] = notation - - def set_coord_unit(self, unit): + def set_units(self, units): """ Set context measurement units Parameters @@ -166,70 +76,9 @@ class GerberContext(object): ValueError If `unit` is not 'inch' or 'metric' """ - if unit not in ('inch', 'metric'): - raise ValueError('Unit may be "inch" or "metric"') - self.settings['units'] = unit - - def set_image_polarity(self, polarity): - """ Set context image polarity - - Parameters - ---------- - polarity : string - Image polarity. May be "positive" or "negative" - - Raises - ------ - ValueError - If polarity is not 'positive' or 'negative' - """ - if polarity not in ('positive', 'negative'): - raise ValueError('Polarity may be "positive" or "negative"') - self.image_polarity = polarity - - def set_level_polarity(self, polarity): - """ Set context level polarity - - Parameters - ---------- - polarity : string - Level polarity. May be "dark" or "clear" - - Raises - ------ - ValueError - If polarity is not 'dark' or 'clear' - """ - if polarity not in ('dark', 'clear'): - raise ValueError('Polarity may be "dark" or "clear"') - self.level_polarity = polarity - - def set_interpolation(self, interpolation): - """ Set arc interpolation mode - - Parameters - ---------- - interpolation : string - Interpolation mode. May be 'linear' or 'arc' - - Raises - ------ - ValueError - If `interpolation` is not 'linear' or 'arc' - """ - if interpolation not in ('linear', 'arc'): - raise ValueError('Interpolation may be "linear" or "arc"') - self.interpolation = interpolation - - def set_aperture(self, d): - """ Set active aperture - - Parameters - ---------- - aperture : int - Aperture number to activate. - """ - self.aperture = d + if units not in ('inch', 'metric'): + raise ValueError('Units may be "inch" or "metric"') + self.units = units def set_color(self, color): """ Set rendering color. @@ -277,238 +126,49 @@ class GerberContext(object): """ self.alpha = alpha - def resolve(self, x, y): - """ Resolve missing x or y coordinates in a coordinate command. - - Replace missing x or y values with the current x or y position. This - is the default method for handling coordinate pairs pulled from gerber - file statments, as a move/line/arc involving a change in only one axis - will drop the redundant axis coordinate to reduce file size. - - Parameters - ---------- - x : float - X-coordinate. If `None`, will be replaced with current - "photoplotter" head x-coordinate - - y : float - Y-coordinate. If `None`, will be replaced with current - "photoplotter" head y-coordinate - - Returns - ------- - coordinates : tuple (, ) - Coordinates in absolute notation - """ - x = x if x is not None else self.x - y = y if y is not None else self.y - return x, y - - def define_aperture(self, d, shape, modifiers): - pass - - def move(self, x, y, resolve=True): - """ Lights-off move. - - Move the "photoplotter" head to (x, y) without drawing a line. If x or - y is `None`, remain at the same point in that axis. - - Parameters - ----------- - x : float - X-coordinate to move to. If x is `None`, do not move in the X - direction - - y : float - Y-coordinate to move to. if y is `None`, do not move in the Y - direction - - resolve : bool - If resolve is `True` the context will replace missing x or y - coordinates with the current plotter head position. This is the - default behavior. - """ - if resolve: - self.x, self.y = self.resolve(x, y) + def render(self, primitive): + color = (self.color if primitive.level_polarity == 'dark' + else self.background_color) + if isinstance(primitive, Line): + self._render_line(primitive, color) + elif isinstance(primitive, Arc): + self._render_arc(primitive, color) + elif isinstance(primitive, Region): + self._render_region(primitive, color) + elif isinstance(primitive, Circle): + self._render_circle(primitive, color) + elif isinstance(primitive, Rectangle): + self._render_rectangle(primitive, color) + elif isinstance(primitive, Obround): + self._render_obround(primitive, color) + elif isinstance(primitive, Polygon): + self._render_polygon(Polygon, color) + elif isinstance(primitive, Drill): + self._render_drill(primitive, self.drill_color) else: - self.x, self.y = x, y - - def stroke(self, x, y, i, j): - """ Lights-on move. (draws a line or arc) - - The stroke method is called when a Lights-on move statement is - encountered. This will call the `line` or `arc` method as necessary - based on the move statement's parameters. The `stroke` method should - be overridden in `GerberContext` subclasses. - - Parameters - ---------- - x : float - X coordinate of target position - - y : float - Y coordinate of target position - - i : float - Offset in X-direction from current position of arc center. + return - j : float - Offset in Y-direction from current position of arc center. - """ + def _render_line(self, primitive, color): pass - def line(self, x, y): - """ Draw a line - - Draws a line from the current position to (x, y) using the currently - selected aperture. The `line` method should be overridden in - `GerberContext` subclasses. - - Parameters - ---------- - x : float - X coordinate of target position - - y : float - Y coordinate of target position - """ + def _render_arc(self, primitive, color): pass - def arc(self, x, y, i, j): - """ Draw an arc - - Draw an arc from the current position to (x, y) using the currently - selected aperture. `i` and `j` specify the offset from the starting - position to the center of the arc.The `arc` method should be - overridden in `GerberContext` subclasses. - - Parameters - ---------- - x : float - X coordinate of target position - - y : float - Y coordinate of target position - - i : float - Offset in X-direction from current position of arc center. - - j : float - Offset in Y-direction from current position of arc center. - """ + def _render_region(self, primitive, color): pass - def flash(self, x, y): - """ Flash the current aperture - - Draw a filled shape defined by the currently selected aperture. - - Parameters - ---------- - x : float - X coordinate of the position at which to flash - - y : float - Y coordinate of the position at which to flash - """ + def _render_circle(self, primitive, color): pass - def drill(self, x, y, diameter): - """ Draw a drill hit - - Draw a filled circle representing a drill hit at the specified - position and with the specified diameter. - - Parameters - ---------- - x : float - X coordinate of the drill hit - - y : float - Y coordinate of the drill hit - - diameter : float - Finished hole diameter to draw. - """ + def _render_rectangle(self, primitive, color): pass - def region_contour(self, x, y): - pass - - def fill_region(self): + def _render_obround(self, primitive, color): pass - - def evaluate(self, stmt): - """ Evaluate Gerber statement and update image accordingly. - - This method is called once for each statement in a Gerber/Excellon - file when the file's `render` method is called. The evaluate method - should forward the statement on to the relevant handling method based - on the statement type. - - Parameters - ---------- - statement : Statement - Gerber/Excellon statement to evaluate. - """ - if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): - return - - elif isinstance(stmt, ParamStmt): - self._evaluate_param(stmt) - - elif isinstance(stmt, CoordStmt): - self._evaluate_coord(stmt) - - elif isinstance(stmt, ApertureStmt): - self._evaluate_aperture(stmt) - - elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): - self._evaluate_mode(stmt) + def _render_polygon(self, primitive, color): + pass - else: - raise Exception("Invalid statement to evaluate") - - def _evaluate_mode(self, stmt): - if stmt.type == 'RegionMode': - if self.region_mode == 'on' and stmt.mode == 'off': - self.fill_region() - self.region_mode = stmt.mode - elif stmt.type == 'QuadrantMode': - self.quadrant_mode = stmt.mode - - def _evaluate_param(self, stmt): - if stmt.param == "FS": - self.set_coord_format(stmt.zero_suppression, stmt.format, - stmt.notation) - self.set_coord_notation(stmt.notation) - elif stmt.param == "MO": - self.set_coord_unit(stmt.mode) - elif stmt.param == "IP": - self.set_image_polarity(stmt.ip) - elif stmt.param == "LP": - self.set_level_polarity(stmt.lp) - elif stmt.param == "AD": - self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) - - def _evaluate_coord(self, stmt): - if stmt.function in ("G01", "G1"): - self.set_interpolation('linear') - elif stmt.function in ('G02', 'G2', 'G03', 'G3'): - self.set_interpolation('arc') - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') - else 'counterclockwise') - if stmt.op == "D01": - if self.region_mode == 'on': - self.region_contour(stmt.x, stmt.y) - else: - self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) - elif stmt.op == "D02": - self.move(stmt.x, stmt.y) - elif stmt.op == "D03": - self.flash(stmt.x, stmt.y) - - def _evaluate_aperture(self, stmt): - self.set_aperture(stmt.d) + def _render_drill(self, primitive, color): + pass diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 15d7bd3..d9456a5 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -17,214 +17,139 @@ # limitations under the License. from .render import GerberContext -from .apertures import Circle, Rect, Obround, Polygon +from operator import mul import svgwrite SCALE = 300 -def convert_color(color): +def svg_color(color): color = tuple([int(ch * 255) for ch in color]) return 'rgb(%d, %d, %d)' % color -class SvgCircle(Circle): - def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke=color, - stroke_width=SCALE * self.diameter, - stroke_linecap="round") - aline.stroke(opacity=alpha) - return aline - - def arc(self, ctx, x, y, i, j, direction, color='rgb(184, 115, 51)', alpha=1.0): - pass - - def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (self.diameter / 2.0), - fill=color) - circle.fill(opacity=alpha) - return [circle, ] - - -class SvgRect(Rect): - def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke=color, stroke_width=2, - stroke_linecap="butt") - aline.stroke(opacity=alpha) - return aline - - def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - xsize, ysize = self.size - rectangle = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), - -SCALE * (y + (ysize / 2))), - size=(SCALE * xsize, SCALE * ysize), - fill=color) - rectangle.fill(opacity=alpha) - return [rectangle, ] - - -class SvgObround(Obround): - def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - pass - - def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - xsize, ysize = self.size - - # horizontal obround - if xsize == ysize: - circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (x / 2.0), - fill=color) - circle.fill(opacity=alpha) - return [circle, ] - if xsize > ysize: - rectx = xsize - ysize - recty = ysize - lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill=color) - - rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill=color) - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill=color) - lcircle.fill(opacity=alpha) - rcircle.fill(opacity=alpha) - rect.fill(opacity=alpha) - return [lcircle, rcircle, rect, ] - - # Vertical obround - else: - rectx = xsize - recty = ysize - xsize - lcircle = ctx.dwg.circle(center=(x * SCALE, - (y - (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill=color) - - ucircle = ctx.dwg.circle(center=(x * SCALE, - (y + (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill=color) - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill=color) - lcircle.fill(opacity=alpha) - ucircle.fill(opacity=alpha) - rect.fill(opacity=alpha) - return [lcircle, ucircle, rect, ] - class GerberSvgContext(GerberContext): def __init__(self): GerberContext.__init__(self) - - self.apertures = {} + self.scale = (SCALE, -SCALE) self.dwg = svgwrite.Drawing() - self.dwg.transform = 'scale 1 -1' self.background = False - self.region_path = None + + def dump(self, filename): + self.dwg.saveas(filename) def set_bounds(self, bounds): xbounds, ybounds = bounds - size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + size = (SCALE * (xbounds[1] - xbounds[0]), + SCALE * (ybounds[1] - ybounds[0])) if not self.background: - self.dwg = svgwrite.Drawing(viewBox='%f, %f, %f, %f' % (SCALE*xbounds[0], -SCALE*ybounds[1],size[0], size[1])) - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], - -SCALE * ybounds[1]), - size=size, fill=convert_color(self.background_color))) + vbox = '%f, %f, %f, %f' % (SCALE * xbounds[0], -SCALE * ybounds[1], + size[0], size[1]) + self.dwg = svgwrite.Drawing(viewBox=vbox) + rect = self.dwg.rect(insert=(SCALE * xbounds[0], + -SCALE * ybounds[1]), + size=size, + fill=svg_color(self.background_color)) + self.dwg.add(rect) self.background = True - def define_aperture(self, d, shape, modifiers): - aperture = None - if shape == 'C': - aperture = SvgCircle(diameter=float(modifiers[0][0])) - elif shape == 'R': - aperture = SvgRect(size=modifiers[0][0:2]) - elif shape == 'O': - aperture = SvgObround(size=modifiers[0][0:2]) - self.apertures[d] = aperture - - def stroke(self, x, y, i, j): - super(GerberSvgContext, self).stroke(x, y, i, j) - - if self.interpolation == 'linear': - self.line(x, y) - elif self.interpolation == 'arc': - self.arc(x, y, i, j) - - def line(self, x, y): - super(GerberSvgContext, self).line(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - color = (convert_color(self.color) if self.level_polarity == 'dark' - else convert_color(self.background_color)) - alpha = self.alpha if self.level_polarity == 'dark' else 1.0 - self.dwg.add(ap.line(self, x, y, color, alpha)) - self.move(x, y, resolve=False) - - def arc(self, x, y, i, j): - super(GerberSvgContext, self).arc(x, y, i, j) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - #self.dwg.add(ap.arc(self, x, y, i, j, self.direction, - # convert_color(self.color), self.alpha)) - self.move(x, y, resolve=False) - - def flash(self, x, y): - super(GerberSvgContext, self).flash(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - - color = (convert_color(self.color) if self.level_polarity == 'dark' - else convert_color(self.background_color)) - alpha = self.alpha if self.level_polarity == 'dark' else 1.0 - for shape in ap.flash(self, x, y, color, alpha): - self.dwg.add(shape) - self.move(x, y, resolve=False) - - def drill(self, x, y, diameter): - hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), - r=SCALE*(diameter/2.0), - fill=convert_color(self.drill_color)) - #hit.fill(opacity=self.alpha) - self.dwg.add(hit) - - def region_contour(self, x, y): - super(GerberSvgContext, self).region_contour(x, y) - x, y = self.resolve(x, y) - color = (convert_color(self.color) if self.level_polarity == 'dark' - else convert_color(self.background_color)) - alpha = self.alpha if self.level_polarity == 'dark' else 1.0 - if self.region_path is None: - self.region_path = self.dwg.path(d = 'M %f, %f' % - (self.x*SCALE, -self.y*SCALE), - fill = color, stroke = 'none') - self.region_path.fill(opacity=alpha) - self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE)) - self.move(x, y, resolve=False) - - def fill_region(self): - self.dwg.add(self.region_path) - self.region_path = None + def _render_line(self, line, color): + start = map(mul, line.start, self.scale) + end = map(mul, line.end, self.scale) + aline = self.dwg.line(start=start, end=end, + stroke=svg_color(color), + stroke_width=SCALE * line.width, + stroke_linecap='round') + aline.stroke(opacity=self.alpha) + self.dwg.add(aline) + + def _render_region(self, region, color): + points = [tuple(map(mul, point, self.scale)) for point in region.points] + region_path = self.dwg.path(d='M %f, %f' % points[0], + fill=svg_color(color), + stroke='none') + region_path.fill(opacity=self.alpha) + for point in points[1:]: + region_path.push('L %f, %f' % point) + self.dwg.add(region_path) + + def _render_circle(self, circle, color): + center = map(mul, circle.position, self.scale) + acircle = self.dwg.circle(center=center, + r = SCALE * circle.radius, + fill=svg_color(color)) + acircle.fill(opacity=self.alpha) + self.dwg.add(acircle) + + def _render_rectangle(self, rectangle, color): + center = map(mul, rectangle.position, self.scale) + size = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale))) + insert = center[0] - size[0] / 2., center[1] - size[1] / 2. + arect = self.dwg.rect(insert=insert, size=size, + fill=svg_color(color)) + arect.fill(opacity=self.alpha) + self.dwg.add(arect) + + def _render_obround(self, obround, color): + x, y = tuple(map(mul, obround.position, self.scale)) + xsize, ysize = tuple(map(mul, (obround.width, obround.height), + self.scale)) + xscale, yscale = self.scale + + # Corner case... + if xsize == ysize: + circle = self.dwg.circle(center=(x, y), + r = (xsize / 2.0), + fill=svg_color(color)) + circle.fill(opacity=self.alpha) + self.dwg.add(circle) + + # Horizontal obround + elif xsize > ysize: + rectx = xsize - ysize + recty = ysize + c1 = self.dwg.circle(center=(x - (rectx / 2.0), y), + r = (ysize / 2.0), + fill=svg_color(color)) + + c2 = self.dwg.circle(center=(x + (rectx / 2.0), y), + r = (ysize / 2.0), + fill=svg_color(color)) + + rect = self.dwg.rect(insert=(x, y), + size=(xsize, ysize), + fill=svg_color(color)) + c1.fill(opacity=self.alpha) + c2.fill(opacity=self.alpha) + rect.fill(opacity=self.alpha) + self.dwg.add(c1) + self.dwg.add(c2) + self.dwg.add(rect) - def dump(self, filename): - self.dwg.saveas(filename) + # Vertical obround + else: + rectx = xsize + recty = ysize - xsize + c1 = self.dwg.circle(center=(x, y - (recty / 2.)), + r = (xsize / 2.), + fill=svg_color(color)) + + c2 = self.dwg.circle(center=(x, y + (recty / 2.)), + r = (xsize / 2.), + fill=svg_color(color)) + + rect = self.dwg.rect(insert=(x, y), + size=(xsize, ysize), + fill=svg_color(color)) + c1.fill(opacity=self.alpha) + c2.fill(opacity=self.alpha) + rect.fill(opacity=self.alpha) + self.dwg.add(c1) + self.dwg.add(c2) + self.dwg.add(rect) + + def _render_drill(self, primitive, color): + center = map(mul, primitive.position, self.scale) + hit = self.dwg.circle(center=center, r=SCALE * primitive.radius, + fill=svg_color(color)) + self.dwg.add(hit) -- cgit From b5f8451c8f37acf9148bbd09f3326eb5aba3e053 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 8 Oct 2014 18:34:16 -0400 Subject: cairo support --- gerber/render/cairo_backend.py | 156 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 gerber/render/cairo_backend.py (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py new file mode 100644 index 0000000..d5efc2d --- /dev/null +++ b/gerber/render/cairo_backend.py @@ -0,0 +1,156 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe + +# 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. + +from .render import GerberContext +from .apertures import Circle, Rect, Obround, Polygon +import cairo +import math + +SCALE = 200. + +class CairoCircle(Circle): + def line(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): + ctx.set_source_rgb (*color) + ctx.set_line_width(self.diameter * SCALE) + ctx.set_line_cap(cairo.LINE_CAP_ROUND) + ctx.line_to(x * SCALE, y * SCALE) + ctx.stroke() + + def arc(self, ctx, x, y, i, j, direction, color=(184/255., 115/255., 51/255.)): + ctx_x, ctx_y = ctx.get_current_point() + + # Do the math + center = ((x + i) * SCALE, (y + j) * SCALE) + radius = math.sqrt(math.pow(ctx_x - center[0], 2) + math.pow(ctx_y - center[1], 2)) + delta_x0 = (ctx_x - center[0]) + delta_y0 = (ctx_y - center[1]) + delta_x1 = (x * SCALE - center[0]) + delta_y1 = (y * SCALE - center[1]) + theta0 = math.atan2(delta_y0, delta_x0) + theta1 = math.atan2(delta_y1, delta_x1) + # Draw the arc + ctx.set_source_rgb (*color) + ctx.set_line_width(self.diameter * SCALE) + ctx.set_line_cap(cairo.LINE_CAP_ROUND) + if direction == 'clockwise': + ctx.arc_negative(center[0], center[1], radius, theta0, theta1) + else: + ctx.arc(center[0], center[1], radius, theta0, theta1) + ctx.stroke() + + def flash(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): + ctx.set_source_rgb (*color) + ctx.set_line_width(0) + ctx.arc(x * SCALE, y * SCALE, (self.diameter/2.) * SCALE, 0, 2 * math.pi) + ctx.fill() + +class CairoRect(Rect): + def line(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): + ctx.set_source_rgb (*color) + ctx.set_line_width(self.diameter * SCALE) + ctx.set_line_cap(cairo.LINE_CAP_SQUARE) + ctx.line_to(x * SCALE, y * SCALE) + ctx.stroke() + + def flash(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): + xsize, ysize = self.size + ctx.set_source_rgb (*color) + ctx.set_line_width(0) + x0 = SCALE * (x - (xsize / 2.)) + y0 = SCALE * (y - (ysize / 2.)) + + ctx.rectangle(x0,y0,SCALE * xsize, SCALE * ysize) + ctx.fill() + + + +class GerberCairoContext(GerberContext): + def __init__(self, surface=None, size=(1000, 1000), + color='rgb(184, 115, 51)', drill_color='gray'): + GerberContext.__init__(self) + if surface is None: + self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, + size[0], size[1]) + else: + self.surface = surface + self.ctx = cairo.Context(self.surface) + self.size = size + self.ctx.translate(0, self.size[1]) + self.ctx.scale(1,-1) + self.apertures = {} + self.color = color + self.drill_color = drill_color + self.background = False + + def set_bounds(self, bounds): + xbounds, ybounds = bounds + self.ctx.rectangle(SCALE * xbounds[0], SCALE * ybounds[0], SCALE * (xbounds[1]- xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + self.ctx.set_source_rgb(0,0,0) + self.ctx.fill() + + def define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + aperture = CairoCircle(diameter=float(modifiers[0][0])) + elif shape == 'R': + aperture = CairoRect(size=modifiers[0][0:2]) + self.apertures[d] = aperture + + def stroke(self, x, y, i, j): + super(GerberCairoContext, self).stroke(x, y, i, j) + + if self.interpolation == 'linear': + self.line(x, y) + elif self.interpolation == 'arc': + self.arc(x, y, i, j) + self.move(x,y) + + def line(self, x, y): + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + ap.line(self.ctx, x, y) + + def arc(self, x, y, i, j): + super(GerberCairoContext, self).arc(x, y, i, j) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + ap.arc(self.ctx, x, y, i, j, self.direction) + + + def flash(self, x, y): + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + ap.flash(self.ctx, x, y) + self.move(x, y, resolve=False) + + def move(self, x, y, resolve=True): + super(GerberCairoContext, self).move(x, y, resolve) + if x is None: + x = self.x + if y is None: + y = self.y + if self.x is not None and self.y is not None: + self.ctx.move_to(x * SCALE, y * SCALE) + + + def dump(self, filename): + self.surface.write_to_png(filename) \ No newline at end of file -- cgit From 0437e4198a0ff5d909d4321768341a173930904c Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 26 Oct 2014 22:35:56 -0400 Subject: cairo working --- gerber/render/__init__.py | 2 +- gerber/render/cairo_backend.py | 213 ++++++++++++++++++-------------------- gerber/render/svgwrite_backend.py | 6 +- 3 files changed, 106 insertions(+), 115 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index 0d3527b..b4af4ad 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -25,4 +25,4 @@ SVG is the only supported format. from svgwrite_backend import GerberSvgContext - +from cairo_backend import GerberCairoContext diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index d5efc2d..f5e5aca 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -16,71 +16,71 @@ # limitations under the License. from .render import GerberContext -from .apertures import Circle, Rect, Obround, Polygon -import cairo +from operator import mul +import cairocffi as cairo import math -SCALE = 200. - -class CairoCircle(Circle): - def line(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): - ctx.set_source_rgb (*color) - ctx.set_line_width(self.diameter * SCALE) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.line_to(x * SCALE, y * SCALE) - ctx.stroke() - - def arc(self, ctx, x, y, i, j, direction, color=(184/255., 115/255., 51/255.)): - ctx_x, ctx_y = ctx.get_current_point() - - # Do the math - center = ((x + i) * SCALE, (y + j) * SCALE) - radius = math.sqrt(math.pow(ctx_x - center[0], 2) + math.pow(ctx_y - center[1], 2)) - delta_x0 = (ctx_x - center[0]) - delta_y0 = (ctx_y - center[1]) - delta_x1 = (x * SCALE - center[0]) - delta_y1 = (y * SCALE - center[1]) - theta0 = math.atan2(delta_y0, delta_x0) - theta1 = math.atan2(delta_y1, delta_x1) - # Draw the arc - ctx.set_source_rgb (*color) - ctx.set_line_width(self.diameter * SCALE) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - if direction == 'clockwise': - ctx.arc_negative(center[0], center[1], radius, theta0, theta1) - else: - ctx.arc(center[0], center[1], radius, theta0, theta1) - ctx.stroke() - - def flash(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): - ctx.set_source_rgb (*color) - ctx.set_line_width(0) - ctx.arc(x * SCALE, y * SCALE, (self.diameter/2.) * SCALE, 0, 2 * math.pi) - ctx.fill() - -class CairoRect(Rect): - def line(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): - ctx.set_source_rgb (*color) - ctx.set_line_width(self.diameter * SCALE) - ctx.set_line_cap(cairo.LINE_CAP_SQUARE) - ctx.line_to(x * SCALE, y * SCALE) - ctx.stroke() - - def flash(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): - xsize, ysize = self.size - ctx.set_source_rgb (*color) - ctx.set_line_width(0) - x0 = SCALE * (x - (xsize / 2.)) - y0 = SCALE * (y - (ysize / 2.)) - - ctx.rectangle(x0,y0,SCALE * xsize, SCALE * ysize) - ctx.fill() - +SCALE = 300. + + +#class CairoCircle(Circle): +# def line(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): +# ctx.set_source_rgb (*color) +# ctx.set_line_width(self.diameter * SCALE) +# ctx.set_line_cap(cairo.LINE_CAP_ROUND) +# ctx.line_to(x * SCALE, y * SCALE) +# ctx.stroke() +# +# def arc(self, ctx, x, y, i, j, direction, color=(184/255., 115/255., 51/255.)): +# ctx_x, ctx_y = ctx.get_current_point() +# +# # Do the math +# center = ((x + i) * SCALE, (y + j) * SCALE) +# radius = math.sqrt(math.pow(ctx_x - center[0], 2) + math.pow(ctx_y - center[1], 2)) +# delta_x0 = (ctx_x - center[0]) +# delta_y0 = (ctx_y - center[1]) +# delta_x1 = (x * SCALE - center[0]) +# delta_y1 = (y * SCALE - center[1]) +# theta0 = math.atan2(delta_y0, delta_x0) +# theta1 = math.atan2(delta_y1, delta_x1) +# # Draw the arc +# ctx.set_source_rgb (*color) +# ctx.set_line_width(self.diameter * SCALE) +# ctx.set_line_cap(cairo.LINE_CAP_ROUND) +# if direction == 'clockwise': +# ctx.arc_negative(center[0], center[1], radius, theta0, theta1) +# else: +# ctx.arc(center[0], center[1], radius, theta0, theta1) +# ctx.stroke() +# +# def flash(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): +# ctx.set_source_rgb (*color) +# ctx.set_line_width(0) +# ctx.arc(x * SCALE, y * SCALE, (self.diameter/2.) * SCALE, 0, 2 * math.pi) +# ctx.fill() +# +#class CairoRect(Rect): +# def line(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): +# ctx.set_source_rgb (*color) +# ctx.set_line_width(self.diameter * SCALE) +# ctx.set_line_cap(cairo.LINE_CAP_SQUARE) +# ctx.line_to(x * SCALE, y * SCALE) +# ctx.stroke() +# +# def flash(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): +# xsize, ysize = self.size +# ctx.set_source_rgb (*color) +# ctx.set_line_width(0) +# x0 = SCALE * (x - (xsize / 2.)) +# y0 = SCALE * (y - (ysize / 2.)) +# +# ctx.rectangle(x0,y0,SCALE * xsize, SCALE * ysize) +# ctx.fill() +# class GerberCairoContext(GerberContext): - def __init__(self, surface=None, size=(1000, 1000), - color='rgb(184, 115, 51)', drill_color='gray'): + def __init__(self, surface=None, size=(1000, 1000)): GerberContext.__init__(self) if surface is None: self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, @@ -90,10 +90,9 @@ class GerberCairoContext(GerberContext): self.ctx = cairo.Context(self.surface) self.size = size self.ctx.translate(0, self.size[1]) - self.ctx.scale(1,-1) + self.scale = (SCALE,SCALE) + self.ctx.scale(1, -1) self.apertures = {} - self.color = color - self.drill_color = drill_color self.background = False def set_bounds(self, bounds): @@ -102,55 +101,47 @@ class GerberCairoContext(GerberContext): self.ctx.set_source_rgb(0,0,0) self.ctx.fill() - def define_aperture(self, d, shape, modifiers): - aperture = None - if shape == 'C': - aperture = CairoCircle(diameter=float(modifiers[0][0])) - elif shape == 'R': - aperture = CairoRect(size=modifiers[0][0:2]) - self.apertures[d] = aperture - - def stroke(self, x, y, i, j): - super(GerberCairoContext, self).stroke(x, y, i, j) - - if self.interpolation == 'linear': - self.line(x, y) - elif self.interpolation == 'arc': - self.arc(x, y, i, j) - self.move(x,y) - - def line(self, x, y): - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - ap.line(self.ctx, x, y) - - def arc(self, x, y, i, j): - super(GerberCairoContext, self).arc(x, y, i, j) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - ap.arc(self.ctx, x, y, i, j, self.direction) - - - def flash(self, x, y): - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - ap.flash(self.ctx, x, y) - self.move(x, y, resolve=False) - - def move(self, x, y, resolve=True): - super(GerberCairoContext, self).move(x, y, resolve) - if x is None: - x = self.x - if y is None: - y = self.y - if self.x is not None and self.y is not None: - self.ctx.move_to(x * SCALE, y * SCALE) + def _render_line(self, line, color): + start = map(mul, line.start, self.scale) + end = map(mul, line.end, self.scale) + self.ctx.set_source_rgb (*color) + self.ctx.set_line_width(line.width * SCALE) + self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + self.ctx.move_to(*start) + self.ctx.line_to(*end) + self.ctx.stroke() + + def _render_region(self, region, color): + points = [tuple(map(mul, point, self.scale)) for point in region.points] + self.ctx.set_source_rgb (*color) + self.ctx.set_line_width(0) + self.ctx.move_to(*points[0]) + for point in points[1:]: + self.ctx.move_to(*point) + self.ctx.fill() + + def _render_circle(self, circle, color): + center = map(mul, circle.position, self.scale) + self.ctx.set_source_rgb (*color) + self.ctx.set_line_width(0) + self.ctx.arc(*center, radius=circle.radius * SCALE, angle1=0, angle2=2 * math.pi) + self.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_rgb (*color) + self.ctx.set_line_width(0) + self.ctx.rectangle(*ll,width=width, height=height) + self.ctx.fill() + + def _render_obround(self, obround, color): + self._render_circle(obround.subshapes['circle1'], color) + self._render_circle(obround.subshapes['circle2'], color) + self._render_rectangle(obround.subshapes['rectangle'], color) + def _render_drill(self, circle, color): + self._render_circle(circle, color) def dump(self, filename): self.surface.write_to_png(filename) \ No newline at end of file diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index d9456a5..2df87b3 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -148,8 +148,8 @@ class GerberSvgContext(GerberContext): self.dwg.add(c2) self.dwg.add(rect) - def _render_drill(self, primitive, color): - center = map(mul, primitive.position, self.scale) - hit = self.dwg.circle(center=center, r=SCALE * primitive.radius, + def _render_drill(self, circle, color): + center = map(mul, circle.position, self.scale) + hit = self.dwg.circle(center=center, r=SCALE * circle.radius, fill=svg_color(color)) self.dwg.add(hit) -- cgit From c08cdf84bceb43ef452e48396bfbe508f0bdd338 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 26 Oct 2014 22:40:55 -0400 Subject: removed dead code --- gerber/render/cairo_backend.py | 64 +++--------------------------------------- 1 file changed, 4 insertions(+), 60 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index f5e5aca..df513bb 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -23,70 +23,14 @@ import math SCALE = 300. -#class CairoCircle(Circle): -# def line(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): -# ctx.set_source_rgb (*color) -# ctx.set_line_width(self.diameter * SCALE) -# ctx.set_line_cap(cairo.LINE_CAP_ROUND) -# ctx.line_to(x * SCALE, y * SCALE) -# ctx.stroke() -# -# def arc(self, ctx, x, y, i, j, direction, color=(184/255., 115/255., 51/255.)): -# ctx_x, ctx_y = ctx.get_current_point() -# -# # Do the math -# center = ((x + i) * SCALE, (y + j) * SCALE) -# radius = math.sqrt(math.pow(ctx_x - center[0], 2) + math.pow(ctx_y - center[1], 2)) -# delta_x0 = (ctx_x - center[0]) -# delta_y0 = (ctx_y - center[1]) -# delta_x1 = (x * SCALE - center[0]) -# delta_y1 = (y * SCALE - center[1]) -# theta0 = math.atan2(delta_y0, delta_x0) -# theta1 = math.atan2(delta_y1, delta_x1) -# # Draw the arc -# ctx.set_source_rgb (*color) -# ctx.set_line_width(self.diameter * SCALE) -# ctx.set_line_cap(cairo.LINE_CAP_ROUND) -# if direction == 'clockwise': -# ctx.arc_negative(center[0], center[1], radius, theta0, theta1) -# else: -# ctx.arc(center[0], center[1], radius, theta0, theta1) -# ctx.stroke() -# -# def flash(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): -# ctx.set_source_rgb (*color) -# ctx.set_line_width(0) -# ctx.arc(x * SCALE, y * SCALE, (self.diameter/2.) * SCALE, 0, 2 * math.pi) -# ctx.fill() -# -#class CairoRect(Rect): -# def line(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): -# ctx.set_source_rgb (*color) -# ctx.set_line_width(self.diameter * SCALE) -# ctx.set_line_cap(cairo.LINE_CAP_SQUARE) -# ctx.line_to(x * SCALE, y * SCALE) -# ctx.stroke() -# -# def flash(self, ctx, x, y, color=(184/255., 115/255., 51/255.)): -# xsize, ysize = self.size -# ctx.set_source_rgb (*color) -# ctx.set_line_width(0) -# x0 = SCALE * (x - (xsize / 2.)) -# y0 = SCALE * (y - (ysize / 2.)) -# -# ctx.rectangle(x0,y0,SCALE * xsize, SCALE * ysize) -# ctx.fill() -# - - class GerberCairoContext(GerberContext): def __init__(self, surface=None, size=(1000, 1000)): GerberContext.__init__(self) if surface is None: - self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, + self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size[0], size[1]) else: - self.surface = surface + self.surface = surface self.ctx = cairo.Context(self.surface) self.size = size self.ctx.translate(0, self.size[1]) @@ -94,7 +38,7 @@ class GerberCairoContext(GerberContext): self.ctx.scale(1, -1) self.apertures = {} self.background = False - + def set_bounds(self, bounds): xbounds, ybounds = bounds self.ctx.rectangle(SCALE * xbounds[0], SCALE * ybounds[0], SCALE * (xbounds[1]- xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) @@ -144,4 +88,4 @@ class GerberCairoContext(GerberContext): self._render_circle(circle, color) def dump(self, filename): - self.surface.write_to_png(filename) \ No newline at end of file + self.surface.write_to_png(filename) -- cgit From 95de179bb08157c3f6716b0645ec00794acc83e6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 27 Oct 2014 08:29:43 -0400 Subject: Fix rendering of 0-width lines (e.g. board outlines) in SVG and Cairo renderer --- gerber/render/cairo_backend.py | 13 ++++---- gerber/render/svgwrite_backend.py | 63 ++++----------------------------------- 2 files changed, 13 insertions(+), 63 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index df513bb..1c69725 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -20,7 +20,7 @@ from operator import mul import cairocffi as cairo import math -SCALE = 300. +SCALE = 400. class GerberCairoContext(GerberContext): @@ -48,8 +48,9 @@ class GerberCairoContext(GerberContext): def _render_line(self, line, color): start = map(mul, line.start, self.scale) end = map(mul, line.end, self.scale) - self.ctx.set_source_rgb (*color) - self.ctx.set_line_width(line.width * SCALE) + width = line.width if line.width != 0 else 0.001 + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_line_width(width * SCALE) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*start) self.ctx.line_to(*end) @@ -57,7 +58,7 @@ class GerberCairoContext(GerberContext): def _render_region(self, region, color): points = [tuple(map(mul, point, self.scale)) for point in region.points] - self.ctx.set_source_rgb (*color) + self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_line_width(0) self.ctx.move_to(*points[0]) for point in points[1:]: @@ -66,7 +67,7 @@ class GerberCairoContext(GerberContext): def _render_circle(self, circle, color): center = map(mul, circle.position, self.scale) - self.ctx.set_source_rgb (*color) + self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_line_width(0) self.ctx.arc(*center, radius=circle.radius * SCALE, angle1=0, angle2=2 * math.pi) self.ctx.fill() @@ -74,7 +75,7 @@ class GerberCairoContext(GerberContext): 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_rgb (*color) + self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_line_width(0) self.ctx.rectangle(*ll,width=width, height=height) self.ctx.fill() diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 2df87b3..aeb680c 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -20,7 +20,7 @@ from .render import GerberContext from operator import mul import svgwrite -SCALE = 300 +SCALE = 400. def svg_color(color): @@ -56,9 +56,10 @@ class GerberSvgContext(GerberContext): def _render_line(self, line, color): start = map(mul, line.start, self.scale) end = map(mul, line.end, self.scale) + width = line.width if line.width != 0 else 0.001 aline = self.dwg.line(start=start, end=end, stroke=svg_color(color), - stroke_width=SCALE * line.width, + stroke_width=SCALE * width, stroke_linecap='round') aline.stroke(opacity=self.alpha) self.dwg.add(aline) @@ -91,62 +92,10 @@ class GerberSvgContext(GerberContext): self.dwg.add(arect) def _render_obround(self, obround, color): - x, y = tuple(map(mul, obround.position, self.scale)) - xsize, ysize = tuple(map(mul, (obround.width, obround.height), - self.scale)) - xscale, yscale = self.scale - - # Corner case... - if xsize == ysize: - circle = self.dwg.circle(center=(x, y), - r = (xsize / 2.0), - fill=svg_color(color)) - circle.fill(opacity=self.alpha) - self.dwg.add(circle) - - # Horizontal obround - elif xsize > ysize: - rectx = xsize - ysize - recty = ysize - c1 = self.dwg.circle(center=(x - (rectx / 2.0), y), - r = (ysize / 2.0), - fill=svg_color(color)) - - c2 = self.dwg.circle(center=(x + (rectx / 2.0), y), - r = (ysize / 2.0), - fill=svg_color(color)) - - rect = self.dwg.rect(insert=(x, y), - size=(xsize, ysize), - fill=svg_color(color)) - c1.fill(opacity=self.alpha) - c2.fill(opacity=self.alpha) - rect.fill(opacity=self.alpha) - self.dwg.add(c1) - self.dwg.add(c2) - self.dwg.add(rect) + self._render_circle(obround.subshapes['circle1'], color) + self._render_circle(obround.subshapes['circle2'], color) + self._render_rectangle(obround.subshapes['rectangle'], color) - # Vertical obround - else: - rectx = xsize - recty = ysize - xsize - c1 = self.dwg.circle(center=(x, y - (recty / 2.)), - r = (xsize / 2.), - fill=svg_color(color)) - - c2 = self.dwg.circle(center=(x, y + (recty / 2.)), - r = (xsize / 2.), - fill=svg_color(color)) - - rect = self.dwg.rect(insert=(x, y), - size=(xsize, ysize), - fill=svg_color(color)) - c1.fill(opacity=self.alpha) - c2.fill(opacity=self.alpha) - rect.fill(opacity=self.alpha) - self.dwg.add(c1) - self.dwg.add(c2) - self.dwg.add(rect) def _render_drill(self, circle, color): center = map(mul, circle.position, self.scale) -- cgit From f5abd5b0bdc0b9f524456dc9216bd0f3732e82a0 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 28 Oct 2014 22:11:43 -0400 Subject: Add arc rendering and tests --- gerber/render/cairo_backend.py | 20 +++++++++++++++++++- gerber/render/svgwrite_backend.py | 14 ++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 1c69725..125a125 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -56,13 +56,31 @@ class GerberCairoContext(GerberContext): self.ctx.line_to(*end) self.ctx.stroke() + def _render_arc(self, arc, color): + center = map(mul, arc.center, self.scale) + start = map(mul, arc.start, self.scale) + end = map(mul, arc.end, self.scale) + radius = SCALE * arc.radius + angle1 = arc.start_angle + angle2 = arc.end_angle + width = arc.width if arc.width != 0 else 0.001 + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_line_width(width * SCALE) + self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + self.ctx.move_to(*start) # You actually have to do this... + if arc.direction == 'counterclockwise': + self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + else: + self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) + self.ctx.move_to(*end) # ...lame + def _render_region(self, region, color): points = [tuple(map(mul, point, self.scale)) for point in region.points] self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_line_width(0) self.ctx.move_to(*points[0]) for point in points[1:]: - self.ctx.move_to(*point) + self.ctx.line_to(*point) self.ctx.fill() def _render_circle(self, circle, color): diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index aeb680c..27783d6 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -18,6 +18,7 @@ from .render import GerberContext from operator import mul +import math import svgwrite SCALE = 400. @@ -64,6 +65,19 @@ class GerberSvgContext(GerberContext): aline.stroke(opacity=self.alpha) self.dwg.add(aline) + def _render_arc(self, arc, color): + start = tuple(map(mul, arc.start, self.scale)) + end = tuple(map(mul, arc.end, self.scale)) + radius = SCALE * arc.radius + width = arc.width if arc.width != 0 else 0.001 + arc_path = self.dwg.path(d='M %f, %f' % start, + stroke=svg_color(color), + stroke_width=SCALE * width) + large_arc = arc.sweep_angle >= 2 * math.pi + direction = '-' if arc.direction == 'clockwise' else '+' + arc_path.push_arc(end, 0, radius, large_arc, direction, True) + self.dwg.add(arc_path) + def _render_region(self, region, color): points = [tuple(map(mul, point, self.scale)) for point in region.points] region_path = self.dwg.path(d='M %f, %f' % points[0], -- cgit From d98d23f8b5d61bb9d20e743a3c44bf04b6b2330a Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 2 Feb 2015 00:43:08 -0500 Subject: More tests and bugfixes --- gerber/render/render.py | 130 +++++++++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 67 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index f5c58d8..2e4abfa 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -41,7 +41,7 @@ class GerberContext(object): Attributes ---------- units : string - Measurement units + Measurement units. 'inch' or 'metric' color : tuple (, , ) Color used for rendering as a tuple of normalized (red, green, blue) values. @@ -57,74 +57,70 @@ class GerberContext(object): Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ 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 - - def set_units(self, units): - """ Set context measurement units - - Parameters - ---------- - unit : string - Measurement units. may be 'inch' or 'metric' - - Raises - ------ - ValueError - If `unit` is not 'inch' or 'metric' - """ + 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 + + @property + def units(self): + return self._units + + @units.setter + def units(self, units): if units not in ('inch', 'metric'): raise ValueError('Units may be "inch" or "metric"') - self.units = units - - def set_color(self, color): - """ Set rendering color. - - Parameters - ---------- - color : tuple (, , ) - Color as a tuple of (red, green, blue) values. Each channel is - represented as a float value in (0, 1) - """ - self.color = color - - def set_drill_color(self, color): - """ Set color used for rendering drill hits. - - Parameters - ---------- - color : tuple (, , ) - Color as a tuple of (red, green, blue) values. Each channel is - represented as a float value in (0, 1) - """ - self.drill_color = color - - def set_background_color(self, color): - """ Set rendering background color - - Parameters - ---------- - color : tuple (, , ) - Color as a tuple of (red, green, blue) values. Each channel is - represented as a float value in (0, 1) - """ - self.background_color = color - - def set_alpha(self, alpha): - """ Set layer rendering opacity - - .. note:: - Not all backends/rendering devices support this parameter. - - Parameters - ---------- - alpha : float - Rendering opacity. must be between 0.0 (transparent) and 1.0 (opaque) - """ - self.alpha = alpha + self._units = units + + @property + def color(self): + return self._color + + @color.setter + def color(self, color): + if len(color) != 3: + raise TypeError('Color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._color = color + + @property + def drill_color(self): + return self._drill_color + + @drill_color.setter + def drill_color(self, color): + if len(color) != 3: + raise TypeError('Drill color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._drill_color = color + + @property + def background_color(self): + return self._background_color + + @background_color.setter + def background_color(self, color): + if len(color) != 3: + raise TypeError('Background color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._background_color = color + + @property + def alpha(self): + return self._alpha + + @alpha.setter + def alpha(self, alpha): + if alpha < 0 or alpha > 1: + raise ValueError('Alpha must be between 0.0 and 1.0') + self._alpha = alpha def render(self, primitive): color = (self.color if primitive.level_polarity == 'dark' -- cgit From d7a453e5ab1eb52c121165f7d027fc66906edc81 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sun, 8 Feb 2015 00:28:17 -0200 Subject: Remove unused file --- gerber/render/apertures.py | 76 ---------------------------------------------- 1 file changed, 76 deletions(-) delete mode 100644 gerber/render/apertures.py (limited to 'gerber/render') diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py deleted file mode 100644 index 52ae50c..0000000 --- a/gerber/render/apertures.py +++ /dev/null @@ -1,76 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2014 Hamilton Kibbe - -# 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. -""" -gerber.render.apertures -============ -**Gerber Aperture base classes** - -This module provides base classes for gerber apertures. These are used by -the rendering engine to draw the gerber file. -""" -import math - -class Aperture(object): - """ Gerber Aperture base class - """ - def draw(self, ctx, x, y): - raise NotImplementedError('The draw method must be implemented \ - in an Aperture subclass.') - - def flash(self, ctx, x, y): - raise NotImplementedError('The flash method must be implemented \ - in an Aperture subclass.') - - def _arc_params(self, startx, starty, x, y, i, j): - center = (startx + i, starty + j) - radius = math.sqrt(math.pow(center[0] - x, 2) + - math.pow(center[1] - y, 2)) - delta_x0 = startx - center[0] - delta_y0 = center[1] - starty - delta_x1 = x - center[0] - delta_y1 = center[1] - y - start_angle = math.atan2(delta_y0, delta_x0) - end_angle = math.atan2(delta_y1, delta_x1) - return {'center': center, 'radius': radius, - 'start_angle': start_angle, 'end_angle': end_angle} - - -class Circle(Aperture): - """ Circular Aperture base class - """ - def __init__(self, diameter=0.0): - self.diameter = diameter - - -class Rect(Aperture): - """ Rectangular Aperture base class - """ - def __init__(self, size=(0, 0)): - self.size = size - - -class Obround(Aperture): - """ Obround Aperture base class - """ - def __init__(self, size=(0, 0)): - self.size = size - - -class Polygon(Aperture): - """ Polygon Aperture base class - """ - pass -- cgit From 8f69c1dfa281b6486c8fce16c1d58acef70c7ae7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 12 Feb 2015 11:28:50 -0500 Subject: Update line primitive to take aperture parameter This fixes the exception referenced in #12. Still need to add rendering code for rectangle aperture lines and arcs. Rectangle strokes will be drawn as polygons by the rendering backends. --- gerber/render/cairo_backend.py | 20 +++++++++++++------- gerber/render/svgwrite_backend.py | 20 +++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 125a125..c1df87a 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -20,6 +20,8 @@ from operator import mul import cairocffi as cairo import math +from ..primitives import * + SCALE = 400. @@ -48,13 +50,17 @@ class GerberCairoContext(GerberContext): def _render_line(self, line, color): start = map(mul, line.start, self.scale) end = map(mul, line.end, self.scale) - width = line.width if line.width != 0 else 0.001 - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_line_width(width * SCALE) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - self.ctx.move_to(*start) - self.ctx.line_to(*end) - self.ctx.stroke() + if isinstance(line.aperture, Circle): + width = line.aperture.diameter if line.aperture.diameter != 0 else 0.001 + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_line_width(width * SCALE) + 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): + # TODO: Render rectangle strokes as a polygon... + pass def _render_arc(self, arc, color): center = map(mul, arc.center, self.scale) diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 27783d6..279d90f 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -21,6 +21,8 @@ from operator import mul import math import svgwrite +from ..primitives import * + SCALE = 400. @@ -57,13 +59,17 @@ class GerberSvgContext(GerberContext): def _render_line(self, line, color): start = map(mul, line.start, self.scale) end = map(mul, line.end, self.scale) - width = line.width if line.width != 0 else 0.001 - aline = self.dwg.line(start=start, end=end, - stroke=svg_color(color), - stroke_width=SCALE * width, - stroke_linecap='round') - aline.stroke(opacity=self.alpha) - self.dwg.add(aline) + if isinstance(line.aperture, Circle): + width = line.aperture.diameter if line.aperture.diameter != 0 else 0.001 + aline = self.dwg.line(start=start, end=end, + stroke=svg_color(color), + stroke_width=SCALE * width, + stroke_linecap='round') + aline.stroke(opacity=self.alpha) + self.dwg.add(aline) + elif isinstance(line.aperture, Rectangle): + # TODO: Render rectangle strokes as a polygon... + pass def _render_arc(self, arc, color): start = tuple(map(mul, arc.start, self.scale)) -- cgit From 5e23d07bcb5103b4607c6ad591a2a547c97ee1f6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 13 Feb 2015 09:37:27 -0500 Subject: Fix rendering for line with rectangular aperture per #12. Still need to do the same for arcs. --- gerber/render/cairo_backend.py | 13 +++++++++---- gerber/render/svgwrite_backend.py | 10 ++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index c1df87a..999269b 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -26,7 +26,7 @@ SCALE = 400. class GerberCairoContext(GerberContext): - def __init__(self, surface=None, size=(1000, 1000)): + def __init__(self, surface=None, size=(10000, 10000)): GerberContext.__init__(self) if surface is None: self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, @@ -58,9 +58,14 @@ class GerberCairoContext(GerberContext): self.ctx.move_to(*start) self.ctx.line_to(*end) self.ctx.stroke() - elif isinstance(line.aperture, rectangle): - # TODO: Render rectangle strokes as a polygon... - pass + 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_line_width(0) + self.ctx.move_to(*points[0]) + for point in points[1:]: + self.ctx.line_to(*point) + self.ctx.fill() def _render_arc(self, arc, color): center = map(mul, arc.center, self.scale) diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 279d90f..9e6a5e4 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -68,8 +68,14 @@ class GerberSvgContext(GerberContext): aline.stroke(opacity=self.alpha) self.dwg.add(aline) elif isinstance(line.aperture, Rectangle): - # TODO: Render rectangle strokes as a polygon... - pass + points = [tuple(map(mul, point, self.scale)) for point in line.vertices] + path = self.dwg.path(d='M %f, %f' % points[0], + fill=svg_color(color), + stroke='none') + path.fill(opacity=self.alpha) + for point in points[1:]: + path.push('L %f, %f' % point) + self.dwg.add(path) def _render_arc(self, arc, color): start = tuple(map(mul, arc.start, self.scale)) -- cgit From 5cf1fa74b42eb8feaab23078bef6f31f6d647c33 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 15 Feb 2015 02:20:02 -0500 Subject: Tests and bugfixes --- gerber/render/svgwrite_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 9e6a5e4..ae7c377 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -89,7 +89,7 @@ class GerberSvgContext(GerberContext): direction = '-' if arc.direction == 'clockwise' else '+' arc_path.push_arc(end, 0, radius, large_arc, direction, True) self.dwg.add(arc_path) - + def _render_region(self, region, color): points = [tuple(map(mul, point, self.scale)) for point in region.points] region_path = self.dwg.path(d='M %f, %f' % points[0], -- cgit From bfe14841604b6be403e7123e8b6667b1f0aff6f6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 15 Feb 2015 03:29:47 -0500 Subject: Add cairo example code, and use example-generated image in readme --- gerber/render/cairo_backend.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 999269b..18d1ceb 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -22,7 +22,7 @@ import math from ..primitives import * -SCALE = 400. +SCALE = 4000. class GerberCairoContext(GerberContext): @@ -42,10 +42,12 @@ class GerberCairoContext(GerberContext): self.background = False def set_bounds(self, bounds): - xbounds, ybounds = bounds - self.ctx.rectangle(SCALE * xbounds[0], SCALE * ybounds[0], SCALE * (xbounds[1]- xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) - self.ctx.set_source_rgb(0,0,0) - self.ctx.fill() + if not self.background: + xbounds, ybounds = bounds + self.ctx.rectangle(SCALE * xbounds[0], SCALE * ybounds[0], SCALE * (xbounds[1]- xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + self.ctx.set_source_rgb(0,0,0) + self.ctx.fill() + self.background = True def _render_line(self, line, color): start = map(mul, line.start, self.scale) -- cgit From d63bf0d68ae100c0413c7619f96d5d1c65da6c4e Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 15 Feb 2015 13:29:50 -0500 Subject: Fix cairo image size --- gerber/render/cairo_backend.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 18d1ceb..f79dfbe 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -44,7 +44,14 @@ class GerberCairoContext(GerberContext): def set_bounds(self, bounds): if not self.background: xbounds, ybounds = bounds - self.ctx.rectangle(SCALE * xbounds[0], SCALE * ybounds[0], SCALE * (xbounds[1]- xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + width = SCALE * (xbounds[1] - xbounds[0]) + height = SCALE * (ybounds[1] - ybounds[0]) + self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(width), int(height)) + self.ctx = cairo.Context(self.surface) + self.ctx.translate(0, height) + self.scale = (SCALE,SCALE) + self.ctx.scale(1, -1) + self.ctx.rectangle(SCALE * xbounds[0], SCALE * ybounds[0], width, height) self.ctx.set_source_rgb(0,0,0) self.ctx.fill() self.background = True -- cgit From e6fa61c82b41473e5d6b37846b2ee372a5dc417f Mon Sep 17 00:00:00 2001 From: Philipp Klaus Date: Wed, 18 Feb 2015 15:54:36 +0100 Subject: Fixing more relative import statements --- gerber/render/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index b4af4ad..1e60792 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -24,5 +24,5 @@ SVG is the only supported format. """ -from svgwrite_backend import GerberSvgContext -from cairo_backend import GerberCairoContext +from .svgwrite_backend import GerberSvgContext +from .cairo_backend import GerberCairoContext -- cgit From d830375c4c33e6a863e02c9f767c127841a070f7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 20 Feb 2015 10:07:26 -0500 Subject: Fix arc width per comment in #12 --- gerber/render/cairo_backend.py | 2 +- gerber/render/svgwrite_backend.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index f79dfbe..326f44e 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -83,7 +83,7 @@ class GerberCairoContext(GerberContext): radius = SCALE * arc.radius angle1 = arc.start_angle angle2 = arc.end_angle - width = arc.width if arc.width != 0 else 0.001 + width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_line_width(width * SCALE) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index ae7c377..a88abfe 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -81,7 +81,7 @@ class GerberSvgContext(GerberContext): start = tuple(map(mul, arc.start, self.scale)) end = tuple(map(mul, arc.end, self.scale)) radius = SCALE * arc.radius - width = arc.width if arc.width != 0 else 0.001 + width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 arc_path = self.dwg.path(d='M %f, %f' % start, stroke=svg_color(color), stroke_width=SCALE * width) -- cgit From feb5b3d57117c1e47f1292d825d5675e88baa8c9 Mon Sep 17 00:00:00 2001 From: hbc Date: Wed, 25 Feb 2015 09:39:53 +0800 Subject: Convert py3k's map object to tuple explicitly. --- gerber/render/svgwrite_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index a88abfe..f8136e0 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -109,7 +109,7 @@ class GerberSvgContext(GerberContext): self.dwg.add(acircle) def _render_rectangle(self, rectangle, color): - center = map(mul, rectangle.position, self.scale) + center = tuple(map(mul, rectangle.position, self.scale)) size = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale))) insert = center[0] - size[0] / 2., center[1] - size[1] / 2. arect = self.dwg.rect(insert=insert, size=size, -- cgit From 68619d4d5a7beb38dc81d953b43bf4196ca1d3a6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 5 Mar 2015 22:42:42 -0500 Subject: Fix parsing for multiline ipc-d-356 records --- gerber/render/cairo_backend.py | 12 +++++++++++- gerber/render/render.py | 6 +++++- 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 326f44e..fa1aecc 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -104,7 +104,7 @@ class GerberCairoContext(GerberContext): self.ctx.fill() def _render_circle(self, circle, color): - center = map(mul, circle.position, self.scale) + center = tuple(map(mul, circle.position, self.scale)) self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_line_width(0) self.ctx.arc(*center, radius=circle.radius * SCALE, angle1=0, angle2=2 * math.pi) @@ -126,5 +126,15 @@ class GerberCairoContext(GerberContext): def _render_drill(self, circle, color): 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) + self.ctx.set_source_rgb(*color) + self.ctx.move_to(*[SCALE * (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 dump(self, filename): self.surface.write_to_png(filename) diff --git a/gerber/render/render.py b/gerber/render/render.py index 2e4abfa..68c2115 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -138,9 +138,11 @@ class GerberContext(object): elif isinstance(primitive, Obround): self._render_obround(primitive, color) elif isinstance(primitive, Polygon): - self._render_polygon(Polygon, color) + self._render_polygon(primitive, color) elif isinstance(primitive, Drill): self._render_drill(primitive, self.drill_color) + elif isinstance(primitive, TestRecord): + self._render_test_record(primitive, color) else: return @@ -168,3 +170,5 @@ class GerberContext(object): def _render_drill(self, primitive, color): pass + def _render_test_record(self, primitive, color): + pass -- cgit From 5aaf18889c3cdc31ae61b9593bf5848bc57ec09a Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 9 Jul 2015 03:54:47 -0300 Subject: Initial patch to unify our render towards cairo This branch allows a pure cairo based render for both PNG and SVG. Cairo backend is mostly the same but with improved support for configurable scale, orientation and inverted color drawing. API is not yet final. --- gerber/render/cairo_backend.py | 87 ++++++++++++++++++++++++------------------ gerber/render/render.py | 9 +++++ 2 files changed, 59 insertions(+), 37 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index fa1aecc..939863b 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -16,53 +16,44 @@ # limitations under the License. from .render import GerberContext -from operator import mul + import cairocffi as cairo + +from operator import mul import math +import tempfile from ..primitives import * -SCALE = 4000. - - class GerberCairoContext(GerberContext): - def __init__(self, surface=None, size=(10000, 10000)): + def __init__(self, scale=300): GerberContext.__init__(self) - if surface is None: - self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, - size[0], size[1]) - else: - self.surface = surface + self.scale = (scale, scale) + self.surface = None + self.ctx = 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.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.size = size - self.ctx.translate(0, self.size[1]) - self.scale = (SCALE,SCALE) + self.ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) self.ctx.scale(1, -1) - self.apertures = {} - self.background = False - - def set_bounds(self, bounds): - if not self.background: - xbounds, ybounds = bounds - width = SCALE * (xbounds[1] - xbounds[0]) - height = SCALE * (ybounds[1] - ybounds[0]) - self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(width), int(height)) - self.ctx = cairo.Context(self.surface) - self.ctx.translate(0, height) - self.scale = (SCALE,SCALE) - self.ctx.scale(1, -1) - self.ctx.rectangle(SCALE * xbounds[0], SCALE * ybounds[0], width, height) - self.ctx.set_source_rgb(0,0,0) - self.ctx.fill() - self.background = True + 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]) 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 line.aperture.diameter != 0 else 0.001 + width = line.aperture.diameter self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_line_width(width * SCALE) + 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) @@ -70,6 +61,7 @@ class GerberCairoContext(GerberContext): 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:]: @@ -80,12 +72,13 @@ class GerberCairoContext(GerberContext): center = map(mul, arc.center, self.scale) start = map(mul, arc.start, self.scale) end = map(mul, arc.end, self.scale) - radius = SCALE * arc.radius + radius = self.scale * arc.radius angle1 = arc.start_angle 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_line_width(width * SCALE) + 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... if arc.direction == 'counterclockwise': @@ -97,6 +90,7 @@ class GerberCairoContext(GerberContext): def _render_region(self, region, color): points = [tuple(map(mul, point, self.scale)) for point in region.points] self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER if (region.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:]: @@ -106,14 +100,16 @@ 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 * SCALE, angle1=0, angle2=2 * math.pi) + self.ctx.arc(*center, radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) self.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() @@ -131,10 +127,27 @@ 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.move_to(*[SCALE * (coord + 0.01) for coord in primitive.position]) + 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.ctx.set_operator(cairo.OPERATOR_OVER) + self.ctx.paint() + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + def dump(self, filename): - self.surface.write_to_png(filename) + is_svg = filename.lower().endswith(".svg") + + if is_svg: + self.surface.finish() + self.surface_buffer.flush() + + with open(filename, "w") as f: + f.write(open(self.surface_buffer.name, "r").read()) + f.flush() + else: + self.surface.write_to_png(filename) diff --git a/gerber/render/render.py b/gerber/render/render.py index 68c2115..124e743 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -62,6 +62,7 @@ class GerberContext(object): 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 @property def units(self): @@ -122,6 +123,14 @@ class GerberContext(object): raise ValueError('Alpha must be between 0.0 and 1.0') self._alpha = alpha + @property + def invert(self): + return self._invert + + @invert.setter + def invert(self, invert): + self._invert = invert + def render(self, primitive): color = (self.color if primitive.level_polarity == 'dark' else self.background_color) -- cgit From b3f6ec558ca35a19bd60440f2a114eb98c0a4263 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 9 Jul 2015 04:05:15 -0300 Subject: Fix arcs and ackground painting --- gerber/render/cairo_backend.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 939863b..16638f5 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -72,7 +72,7 @@ class GerberCairoContext(GerberContext): center = map(mul, arc.center, self.scale) start = map(mul, arc.start, self.scale) end = map(mul, arc.end, self.scale) - radius = self.scale * arc.radius + radius = self.scale[0] * arc.radius angle1 = arc.start_angle angle2 = arc.end_angle width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 @@ -139,6 +139,10 @@ class GerberCairoContext(GerberContext): self.ctx.paint() self.ctx.set_operator(cairo.OPERATOR_CLEAR) + def _paint_background(self): + self.ctx.set_source_rgba(*self.background_color) + self.ctx.paint() + def dump(self, filename): is_svg = filename.lower().endswith(".svg") -- cgit From 39726e3936c5fa5c50158727e8eb7f5d01cb1b49 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 22 Jul 2015 22:13:09 -0400 Subject: Fix multiple layer issue in cairo-unification branch (see #33) --- gerber/render/cairo_backend.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 16638f5..2791d76 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -31,20 +31,21 @@ class GerberCairoContext(GerberContext): self.scale = (scale, scale) self.surface = None self.ctx = None + self.bg = False 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.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]) + 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]) def _render_line(self, line, color): start = map(mul, line.start, self.scale) @@ -140,8 +141,10 @@ class GerberCairoContext(GerberContext): self.ctx.set_operator(cairo.OPERATOR_CLEAR) def _paint_background(self): - self.ctx.set_source_rgba(*self.background_color) - self.ctx.paint() + if not self.bg: + self.bg = True + self.ctx.set_source_rgba(*self.background_color) + self.ctx.paint() def dump(self, filename): is_svg = filename.lower().endswith(".svg") -- cgit From d4a870570855265b9b37f1609dd2bc9f49699bb6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 25 Jul 2015 09:48:58 -0400 Subject: Fix windows permission error per #33 the issue was trying to re-open the temporary file. it works on everything but windows. I've changed it to seek to the beginning and read from the file without re-opening, which should fix the issue. --- gerber/render/cairo_backend.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 2791d76..0ae5d40 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -154,7 +154,9 @@ class GerberCairoContext(GerberContext): self.surface_buffer.flush() with open(filename, "w") as f: - f.write(open(self.surface_buffer.name, "r").read()) + self.surface_buffer.seek(0) + f.write(self.surface_buffer.read()) f.flush() + else: self.surface.write_to_png(filename) -- cgit From cb2fa34e881a389cf8a4bc98fd12be662ff687f8 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 9 Aug 2015 15:11:13 -0400 Subject: Add support for arcs in regions. This fixes the circular cutout issue described in #32. Regions were previously stored as a collection of points, now they are stored as a collection of line and arc primitives. --- gerber/render/cairo_backend.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 0ae5d40..a97e552 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -89,13 +89,25 @@ class GerberCairoContext(GerberContext): self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): - points = [tuple(map(mul, point, self.scale)) for point in region.points] self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER if (region.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_OVER if (region.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.set_line_cap(cairo.LINE_CAP_ROUND) + self.ctx.move_to(*tuple(map(mul, region.primitives[0].start, self.scale))) + for p in region.primitives: + if isinstance(p, Line): + self.ctx.line_to(*tuple(map(mul, p.end, self.scale))) + else: + center = map(mul, p.center, self.scale) + start = map(mul, p.start, self.scale) + end = map(mul, p.end, self.scale) + radius = self.scale[0] * p.radius + angle1 = p.start_angle + angle2 = p.end_angle + if p.direction == 'counterclockwise': + self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + else: + self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) self.ctx.fill() def _render_circle(self, circle, color): -- cgit From dd63b169f177389602e17bc6ced53bd0f1ba0de3 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 10 Oct 2015 16:51:21 -0400 Subject: Allow files to be read from strings per #37 Adds a loads() method to the top level module which generates a GerberFile or ExcellonFile from a string --- gerber/render/cairo_backend.py | 6 ++++++ gerber/render/render.py | 1 + 2 files changed, 7 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index a97e552..345f331 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -172,3 +172,9 @@ class GerberCairoContext(GerberContext): 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 124e743..8f49796 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -181,3 +181,4 @@ class GerberContext(object): def _render_test_record(self, primitive, color): pass + -- cgit From 7e2e469f5e705bcede137f15555da19898bf1f44 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sun, 15 Nov 2015 22:31:36 -0200 Subject: Remove svgwrite backend We moved the functionality to cairo backend, it can write png and svg and maybe more (pdfs?) --- gerber/render/__init__.py | 1 - gerber/render/svgwrite_backend.py | 130 -------------------------------------- 2 files changed, 131 deletions(-) delete mode 100644 gerber/render/svgwrite_backend.py (limited to 'gerber/render') diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index 1e60792..f76d28f 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -24,5 +24,4 @@ SVG is the only supported format. """ -from .svgwrite_backend import GerberSvgContext from .cairo_backend import GerberCairoContext diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py deleted file mode 100644 index f8136e0..0000000 --- a/gerber/render/svgwrite_backend.py +++ /dev/null @@ -1,130 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2014 Hamilton Kibbe -# Based on render_svg.py by 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. - -from .render import GerberContext -from operator import mul -import math -import svgwrite - -from ..primitives import * - -SCALE = 400. - - -def svg_color(color): - color = tuple([int(ch * 255) for ch in color]) - return 'rgb(%d, %d, %d)' % color - - -class GerberSvgContext(GerberContext): - def __init__(self): - GerberContext.__init__(self) - self.scale = (SCALE, -SCALE) - self.dwg = svgwrite.Drawing() - self.background = False - - def dump(self, filename): - self.dwg.saveas(filename) - - def set_bounds(self, bounds): - xbounds, ybounds = bounds - size = (SCALE * (xbounds[1] - xbounds[0]), - SCALE * (ybounds[1] - ybounds[0])) - if not self.background: - vbox = '%f, %f, %f, %f' % (SCALE * xbounds[0], -SCALE * ybounds[1], - size[0], size[1]) - self.dwg = svgwrite.Drawing(viewBox=vbox) - rect = self.dwg.rect(insert=(SCALE * xbounds[0], - -SCALE * ybounds[1]), - size=size, - fill=svg_color(self.background_color)) - self.dwg.add(rect) - self.background = True - - 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 line.aperture.diameter != 0 else 0.001 - aline = self.dwg.line(start=start, end=end, - stroke=svg_color(color), - stroke_width=SCALE * width, - stroke_linecap='round') - aline.stroke(opacity=self.alpha) - self.dwg.add(aline) - elif isinstance(line.aperture, Rectangle): - points = [tuple(map(mul, point, self.scale)) for point in line.vertices] - path = self.dwg.path(d='M %f, %f' % points[0], - fill=svg_color(color), - stroke='none') - path.fill(opacity=self.alpha) - for point in points[1:]: - path.push('L %f, %f' % point) - self.dwg.add(path) - - def _render_arc(self, arc, color): - start = tuple(map(mul, arc.start, self.scale)) - end = tuple(map(mul, arc.end, self.scale)) - radius = SCALE * arc.radius - width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 - arc_path = self.dwg.path(d='M %f, %f' % start, - stroke=svg_color(color), - stroke_width=SCALE * width) - large_arc = arc.sweep_angle >= 2 * math.pi - direction = '-' if arc.direction == 'clockwise' else '+' - arc_path.push_arc(end, 0, radius, large_arc, direction, True) - self.dwg.add(arc_path) - - def _render_region(self, region, color): - points = [tuple(map(mul, point, self.scale)) for point in region.points] - region_path = self.dwg.path(d='M %f, %f' % points[0], - fill=svg_color(color), - stroke='none') - region_path.fill(opacity=self.alpha) - for point in points[1:]: - region_path.push('L %f, %f' % point) - self.dwg.add(region_path) - - def _render_circle(self, circle, color): - center = map(mul, circle.position, self.scale) - acircle = self.dwg.circle(center=center, - r = SCALE * circle.radius, - fill=svg_color(color)) - acircle.fill(opacity=self.alpha) - self.dwg.add(acircle) - - def _render_rectangle(self, rectangle, color): - center = tuple(map(mul, rectangle.position, self.scale)) - size = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale))) - insert = center[0] - size[0] / 2., center[1] - size[1] / 2. - arect = self.dwg.rect(insert=insert, size=size, - fill=svg_color(color)) - arect.fill(opacity=self.alpha) - self.dwg.add(arect) - - def _render_obround(self, obround, color): - self._render_circle(obround.subshapes['circle1'], color) - self._render_circle(obround.subshapes['circle2'], color) - self._render_rectangle(obround.subshapes['rectangle'], color) - - - def _render_drill(self, circle, color): - center = map(mul, circle.position, self.scale) - hit = self.dwg.circle(center=center, r=SCALE * circle.radius, - fill=svg_color(color)) - self.dwg.add(hit) -- cgit From d5f382f4b413d73a96613dd86aa207bb9e665b0d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Mon, 23 Nov 2015 16:17:31 +0800 Subject: Render with cairo instead of cairocffi - I would like to make it use either, but for now, using the one that works with wxpython --- gerber/render/cairo_backend.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 345f331..e4a5eff 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -17,7 +17,7 @@ from .render import GerberContext -import cairocffi as cairo +import cairo from operator import mul import math @@ -52,7 +52,7 @@ class GerberCairoContext(GerberContext): end = map(mul, line.end, self.scale) if isinstance(line.aperture, Circle): width = line.aperture.diameter - self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_source_rgba(color[0], color[1], color[2], 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) @@ -61,7 +61,7 @@ class GerberCairoContext(GerberContext): 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_source_rgba(color[0], color[1], color[2], 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]) @@ -77,7 +77,7 @@ class GerberCairoContext(GerberContext): angle1 = arc.start_angle 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_source_rgba(color[0], color[1], color[2], 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_line_width(width * self.scale[0]) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) @@ -89,7 +89,8 @@ class GerberCairoContext(GerberContext): self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): - self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + #self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if (region.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.set_line_width(0) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) @@ -112,7 +113,7 @@ 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_source_rgba(color[0], color[1], color[2], 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) @@ -121,7 +122,7 @@ class GerberCairoContext(GerberContext): 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_source_rgba(color[0], color[1], color[2], 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) -- cgit From 8eede187f3f644c4f8a0de0dc5825dc4c00c7b8f Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Mon, 23 Nov 2015 22:22:30 +0800 Subject: More fixes to work with cairo --- gerber/render/cairo_backend.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index e4a5eff..81c5ce4 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -61,7 +61,7 @@ class GerberCairoContext(GerberContext): 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[0], color[1], color[2], alpha=self.alpha) + self.ctx.set_source_rgba(color[0], color[1], color[2], 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]) @@ -83,14 +83,13 @@ class GerberCairoContext(GerberContext): self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*start) # You actually have to do this... if arc.direction == 'counterclockwise': - self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + self.ctx.arc(center[0], center[1], radius, angle1, angle2) else: - self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) + self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2) self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - #self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if (region.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.set_line_width(0) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) @@ -106,9 +105,9 @@ class GerberCairoContext(GerberContext): angle1 = p.start_angle angle2 = p.end_angle if p.direction == 'counterclockwise': - self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + self.ctx.arc(center[0], center[1], radius, angle1, angle2) else: - self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) + self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2) self.ctx.fill() def _render_circle(self, circle, color): @@ -116,7 +115,7 @@ class GerberCairoContext(GerberContext): self.ctx.set_source_rgba(color[0], color[1], color[2], 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.arc(center[0], center[1], circle.radius * self.scale[0], 0, 2 * math.pi) self.ctx.fill() def _render_rectangle(self, rectangle, color): @@ -148,7 +147,7 @@ class GerberCairoContext(GerberContext): self.ctx.scale(1, -1) def _paint_inverted_layer(self): - self.ctx.set_source_rgba(*self.background_color) + self.ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2]) self.ctx.set_operator(cairo.OPERATOR_OVER) self.ctx.paint() self.ctx.set_operator(cairo.OPERATOR_CLEAR) @@ -156,7 +155,7 @@ class GerberCairoContext(GerberContext): def _paint_background(self): if not self.bg: self.bg = True - self.ctx.set_source_rgba(*self.background_color) + self.ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2]) self.ctx.paint() def dump(self, filename): -- cgit From d69f50e0f62570a4c327cb8fe4f886f439196010 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 2 Dec 2015 12:44:30 +0800 Subject: Make the hit accessible from the drawable Hit, fix crash with cario drawing rect --- gerber/render/cairo_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 81c5ce4..4a0724f 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -124,7 +124,7 @@ class GerberCairoContext(GerberContext): self.ctx.set_source_rgba(color[0], color[1], color[2], 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.rectangle(ll[0], ll[1], width, height) self.ctx.fill() def _render_obround(self, obround, color): -- cgit From 206f4c57ab66f8a6753015340315991b40178c9b Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 16 Dec 2015 18:59:25 +0800 Subject: Fix drawing arcs. Dont crash for arcs with rectangular apertures. Fix crash with board size of zero for only one drill --- gerber/render/cairo_backend.py | 1 + 1 file changed, 1 insertion(+) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 4a0724f..4d71199 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -87,6 +87,7 @@ class GerberCairoContext(GerberContext): else: self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2) self.ctx.move_to(*end) # ...lame + self.ctx.stroke() def _render_region(self, region, color): self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) -- cgit 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 From 60c5906b29b6427a5190d31c7bb5511e0bf78fd4 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 20 Dec 2015 15:24:20 -0500 Subject: Clean up negative render code --- gerber/render/cairo_backend.py | 145 ++++++++++++++++++++--------------------- 1 file changed, 69 insertions(+), 76 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 6aaffd4..c8f94ff 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -44,18 +44,14 @@ class GerberCairoContext(GerberContext): 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.mask_buffer = tempfile.NamedTemporaryFile() - self.mask = cairo.SVGSurface(self.mask_buffer, size_in_pixels[0], size_in_pixels[1]) + self.mask = cairo.SVGSurface(None, 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) @@ -65,40 +61,27 @@ class GerberCairoContext(GerberContext): start = map(mul, line.start, self.scale) end = map(mul, line.end, self.scale) 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) - 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() + ctx = self.ctx + ctx.set_source_rgba(*color, alpha=self.alpha) + ctx.set_operator(cairo.OPERATOR_OVER if line.level_polarity == "dark" else cairo.OPERATOR_CLEAR) 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() + ctx = self.mask_ctx + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_operator(cairo.OPERATOR_CLEAR) + if isinstance(line.aperture, Circle): + width = line.aperture.diameter + ctx.set_line_width(width * self.scale[0]) + ctx.set_line_cap(cairo.LINE_CAP_ROUND) + ctx.move_to(*start) + ctx.line_to(*end) + ctx.stroke() + elif isinstance(line.aperture, Rectangle): + points = [tuple(map(mul, x, self.scale)) for x in line.vertices] + ctx.set_line_width(0) + ctx.move_to(*points[0]) + for point in points[1:]: + ctx.line_to(*point) + ctx.fill() def _render_arc(self, arc, color): center = map(mul, arc.center, self.scale) @@ -108,26 +91,38 @@ class GerberCairoContext(GerberContext): angle1 = arc.start_angle 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_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... + if not self.invert: + ctx = self.ctx + ctx.set_source_rgba(*color, alpha=self.alpha) + ctx.set_operator(cairo.OPERATOR_OVER if arc.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + else: + ctx = self.mask_ctx + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_operator(cairo.OPERATOR_CLEAR) + ctx.set_line_width(width * self.scale[0]) + ctx.set_line_cap(cairo.LINE_CAP_ROUND) + ctx.move_to(*start) # You actually have to do this... if arc.direction == 'counterclockwise': - self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) else: - self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) - self.ctx.move_to(*end) # ...lame + ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) + ctx.move_to(*end) # ...lame def _render_region(self, region, color): - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER if (region.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - self.ctx.move_to(*tuple(map(mul, region.primitives[0].start, self.scale))) + if not self.invert: + ctx = self.ctx + ctx.set_source_rgba(*color, alpha=self.alpha) + ctx.set_operator(cairo.OPERATOR_OVER if region.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + else: + ctx = self.mask_ctx + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_operator(cairo.OPERATOR_CLEAR) + ctx.set_line_width(0) + ctx.set_line_cap(cairo.LINE_CAP_ROUND) + ctx.move_to(*tuple(map(mul, region.primitives[0].start, self.scale))) for p in region.primitives: if isinstance(p, Line): - self.ctx.line_to(*tuple(map(mul, p.end, self.scale))) + ctx.line_to(*tuple(map(mul, p.end, self.scale))) else: center = map(mul, p.center, self.scale) start = map(mul, p.start, self.scale) @@ -136,41 +131,39 @@ class GerberCairoContext(GerberContext): angle1 = p.start_angle angle2 = p.end_angle if p.direction == 'counterclockwise': - self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) else: - self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) - self.ctx.fill() + ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) + ctx.fill() def _render_circle(self, circle, color): center = tuple(map(mul, circle.position, self.scale)) 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() + ctx = self.ctx + ctx.set_source_rgba(*color, alpha=self.alpha) + ctx.set_operator(cairo.OPERATOR_OVER if circle.level_polarity == "dark" else cairo.OPERATOR_CLEAR) 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() + ctx = self.mask_ctx + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_operator(cairo.OPERATOR_CLEAR) + ctx.set_line_width(0) + ctx.arc(*center, radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) + 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))) 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() + ctx = self.ctx + ctx.set_source_rgba(*color, alpha=self.alpha) + ctx.set_operator(cairo.OPERATOR_OVER if rectangle.level_polarity == "dark" else cairo.OPERATOR_CLEAR) 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() + ctx = self.mask_ctx + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_operator(cairo.OPERATOR_CLEAR) + ctx.set_line_width(0) + ctx.rectangle(*ll, width=width, height=height) + ctx.fill() def _render_obround(self, obround, color): self._render_circle(obround.subshapes['circle1'], color) @@ -185,7 +178,7 @@ 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" 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) -- cgit From af5541ac93b222c05229ee05c9def8dbae5f6e25 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 20 Dec 2015 23:54:20 -0500 Subject: Allow renderer to write to memory per #38 Some updates to rendering colors/themes --- gerber/render/cairo_backend.py | 19 ++++++++++++++++--- gerber/render/theme.py | 17 +++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index c8f94ff..8283ae0 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -15,16 +15,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .render import GerberContext import cairocffi as cairo - from operator import mul import math import tempfile +from .render import GerberContext from ..primitives import * +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + class GerberCairoContext(GerberContext): def __init__(self, scale=300): @@ -184,7 +188,7 @@ class GerberCairoContext(GerberContext): self.ctx.show_text(primitive.net_name) self.ctx.scale(1, -1) - def _paint_inverted_layer(self): + def _clear_mask(self): self.mask_ctx.set_operator(cairo.OPERATOR_OVER) self.mask_ctx.set_source_rgba(*self.color, alpha=self.alpha) self.mask_ctx.paint() @@ -214,7 +218,16 @@ class GerberCairoContext(GerberContext): 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/theme.py b/gerber/render/theme.py index 5d39bb6..eae3735 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -19,12 +19,13 @@ COLORS = { 'black': (0.0, 0.0, 0.0), 'white': (1.0, 1.0, 1.0), - 'fr-4': (0.702, 0.655, 0.192), + 'fr-4': (0.290, 0.345, 0.0), '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), + 'purple soldermask': (0.2, 0.0, 0.334), + 'enig copper': (0.686, 0.525, 0.510), 'hasl copper': (0.871, 0.851, 0.839) } @@ -40,11 +41,19 @@ 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.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.top = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) self.bottom = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) self.drill = kwargs.get('drill', self.background) +THEMES = { + 'Default': Theme(), + 'Osh Park': Theme(top=COLORS['enig copper'], + bottom=COLORS['enig copper'], + topmask=COLORS['purple soldermask'], + bottommask=COLORS['purple soldermask']), +} + -- cgit 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 4a815bf25ddd1d378ec6ad5af008e5bbcd362b51 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 30 Dec 2015 14:05:00 +0800 Subject: First time any macro renders --- gerber/render/cairo_backend.py | 20 ++++++++++++++++++++ gerber/render/render.py | 5 +++++ 2 files changed, 25 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 4d71199..3ee38ae 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -122,11 +122,27 @@ class GerberCairoContext(GerberContext): 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))) + + if rectangle.rotation != 0: + self.ctx.save() + + center = map(mul, rectangle.position, self.scale) + matrix = cairo.Matrix() + matrix.translate(center[0], center[1]) + # For drawing, we already handles the translation + ll[0] = ll[0] - center[0] + ll[1] = ll[1] - center[1] + matrix.rotate(rectangle.rotation) + self.ctx.transform(matrix) + self.ctx.set_source_rgba(color[0], color[1], color[2], 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[0], ll[1], width, height) self.ctx.fill() + + if rectangle.rotation != 0: + self.ctx.restore() def _render_obround(self, obround, color): self._render_circle(obround.subshapes['circle1'], color) @@ -135,6 +151,10 @@ class GerberCairoContext(GerberContext): def _render_drill(self, circle, color): self._render_circle(circle, color) + + def _render_amgroup(self, amgroup, color): + for primitive in amgroup.primitives: + self.render(primitive) def _render_test_record(self, primitive, color): self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) diff --git a/gerber/render/render.py b/gerber/render/render.py index 8f49796..ac01e52 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -150,6 +150,8 @@ class GerberContext(object): self._render_polygon(primitive, color) elif isinstance(primitive, Drill): self._render_drill(primitive, self.drill_color) + elif isinstance(primitive, AMGroup): + self._render_amgroup(primitive, color) elif isinstance(primitive, TestRecord): self._render_test_record(primitive, color) else: @@ -178,6 +180,9 @@ class GerberContext(object): def _render_drill(self, primitive, color): pass + + def _render_amgroup(self, primitive, color): + pass def _render_test_record(self, primitive, color): pass -- cgit From 96692b22216fdfe11f2ded104ac0bdba3b7866a5 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 30 Dec 2015 15:32:44 +0800 Subject: Render primitives for some aperture macros --- gerber/render/render.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index ac01e52..b518385 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -152,6 +152,8 @@ class GerberContext(object): self._render_drill(primitive, self.drill_color) elif isinstance(primitive, AMGroup): self._render_amgroup(primitive, color) + elif isinstance(primitive, Outline): + self._render_region(primitive, color) elif isinstance(primitive, TestRecord): self._render_test_record(primitive, color) else: -- cgit From f61eee807f87c329f6f88645ecdb48f01b887c52 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 30 Dec 2015 18:44:07 +0800 Subject: Render polygon flashes --- gerber/render/cairo_backend.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 3ee38ae..68e9e98 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -148,6 +148,22 @@ class GerberCairoContext(GerberContext): self._render_circle(obround.subshapes['circle1'], color) self._render_circle(obround.subshapes['circle2'], color) self._render_rectangle(obround.subshapes['rectangle'], color) + + def _render_polygon(self, polygon, color): + vertices = polygon.vertices + + self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER if (polygon.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + + # Start from before the end so it is easy to iterate and make sure it is closed + self.ctx.move_to(*map(mul, vertices[-1], self.scale)) + for v in vertices: + self.ctx.line_to(*map(mul, v, self.scale)) + + self.ctx.fill() + def _render_drill(self, circle, color): self._render_circle(circle, color) -- 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 From 83ae0670d11b5f5ef8ba3a6c362b7129a9e31ab3 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Fri, 8 Jan 2016 00:19:47 +0800 Subject: More stability fixes for poorly constructed files --- gerber/render/cairo_backend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 68e9e98..fbc4271 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -76,7 +76,10 @@ class GerberCairoContext(GerberContext): radius = self.scale[0] * arc.radius angle1 = arc.start_angle angle2 = arc.end_angle - width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 + if isinstance(arc.aperture, Circle): + width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 + else: + width = max(arc.aperture.width, arc.aperture.height, 0.001) self.ctx.set_source_rgba(color[0], color[1], color[2], 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_line_width(width * self.scale[0]) @@ -163,7 +166,6 @@ class GerberCairoContext(GerberContext): self.ctx.line_to(*map(mul, v, self.scale)) self.ctx.fill() - def _render_drill(self, circle, color): self._render_circle(circle, color) -- cgit From 5476da8aa3f4ee424f56f4f2491e7af1c4b7b758 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Fix a bunch of rendering bugs. - 'clear' polarity primitives no longer erase background - Added aperture macro support for polygons - Added aperture macro rendring support - Renderer now creates a new surface for each layer and merges them instead of working directly on a single surface - Updated examples accordingly --- gerber/render/cairo_backend.py | 255 +++++++++++++++++++++++------------------ gerber/render/render.py | 8 +- gerber/render/theme.py | 4 +- 3 files changed, 148 insertions(+), 119 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 4e71e75..cc2722a 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -17,8 +17,6 @@ import cairocffi as cairo -from operator import mul -import math import tempfile from .render import GerberContext, RenderSettings @@ -32,11 +30,14 @@ except(ImportError): class GerberCairoContext(GerberContext): + def __init__(self, scale=300): - GerberContext.__init__(self) + super(GerberCairoContext, self).__init__() self.scale = (scale, scale) self.surface = None self.ctx = None + self.active_layer = None + self.output_ctx = None self.bg = False self.mask = None self.mask_ctx = None @@ -46,37 +47,40 @@ class GerberCairoContext(GerberContext): @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) + return (self.scale_point(self.origin_in_inch) + 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) + return (self.scale_point(self.size_in_inch) + 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]) - size_in_inch = (abs(bounds[0][1] - bounds[0][0]), abs(bounds[1][1] - bounds[1][0])) - size_in_pixels = tuple(map(mul, size_in_inch, self.scale)) + size_in_inch = (abs(bounds[0][1] - bounds[0][0]), + abs(bounds[1][1] - bounds[1][0])) + size_in_pixels = self.scale_point(size_in_inch) 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) - 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.mask = cairo.SVGSurface(None, 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]) - 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]) + self.surface = cairo.SVGSurface( + self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) + self.output_ctx = cairo.Context(self.surface) + self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + self.output_ctx.scale(1, -1) + self.output_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, 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, theme) self.dump(filename) @@ -114,158 +118,181 @@ class GerberCairoContext(GerberContext): self.color = settings.color self.alpha = settings.alpha self.invert = settings.invert + + # Get a new clean layer to render on + self._new_render_layer() if settings.mirror: raise Warning('mirrored layers aren\'t supported yet...') - if self.invert: - self._clear_mask() for prim in layer.primitives: self.render(prim) - if self.invert: - self._render_mask() + # Add layer to image + self._flatten() def _render_line(self, line, color): - start = map(mul, line.start, self.scale) - end = map(mul, line.end, self.scale) + start = [pos * scale for pos, scale in zip(line.start, self.scale)] + end = [pos * scale for pos, scale in zip(line.end, self.scale)] if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if line.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if line.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) if isinstance(line.aperture, Circle): width = line.aperture.diameter - ctx.set_line_width(width * self.scale[0]) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.move_to(*start) - ctx.line_to(*end) - ctx.stroke() + 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] - ctx.set_line_width(0) - ctx.move_to(*points[0]) + points = [self.scale_point(x) for x in line.vertices] + self.ctx.set_line_width(0) + self.ctx.move_to(*points[0]) for point in points[1:]: - ctx.line_to(*point) - ctx.fill() + self.ctx.line_to(*point) + self.ctx.fill() def _render_arc(self, arc, color): - center = map(mul, arc.center, self.scale) - start = map(mul, arc.start, self.scale) - end = map(mul, arc.end, self.scale) + center = self.scale_point(arc.center) + start = self.scale_point(arc.start) + end = self.scale_point(arc.end) radius = self.scale[0] * arc.radius angle1 = arc.start_angle angle2 = arc.end_angle width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if arc.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if arc.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(width * self.scale[0]) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.move_to(*start) # You actually have to do this... + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(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... if arc.direction == 'counterclockwise': - ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) else: - ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) - ctx.move_to(*end) # ...lame + self.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if region.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if region.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(0) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.move_to(*tuple(map(mul, region.primitives[0].start, self.scale))) - for p in region.primitives: - if isinstance(p, Line): - ctx.line_to(*tuple(map(mul, p.end, self.scale))) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + self.ctx.move_to(*self.scale_point(region.primitives[0].start)) + for prim in region.primitives: + if isinstance(prim, Line): + self.ctx.line_to(*self.scale_point(prim.end)) else: - center = map(mul, p.center, self.scale) - start = map(mul, p.start, self.scale) - end = map(mul, p.end, self.scale) - radius = self.scale[0] * p.radius - angle1 = p.start_angle - angle2 = p.end_angle - if p.direction == 'counterclockwise': - ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + center = self.scale_point(prim.center) + radius = self.scale[0] * prim.radius + angle1 = prim.start_angle + angle2 = prim.end_angle + if prim.direction == 'counterclockwise': + self.ctx.arc(*center, radius=radius, + angle1=angle1, angle2=angle2) else: - ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) - ctx.fill() + self.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + self.ctx.fill() def _render_circle(self, circle, color): - center = tuple(map(mul, circle.position, self.scale)) + center = self.scale_point(circle.position) if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if circle.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator( + cairo.OPERATOR_OVER if circle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(0) - ctx.arc(*center, radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) - ctx.fill() + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(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() 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))) + lower_left = self.scale_point(rectangle.lower_left) + width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) + if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if rectangle.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator( + cairo.OPERATOR_OVER if rectangle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(0) - ctx.rectangle(*ll, width=width, height=height) - ctx.fill() + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.rectangle(*lower_left, width=width, height=height) + self.ctx.fill() def _render_obround(self, obround, color): self._render_circle(obround.subshapes['circle1'], color) self._render_circle(obround.subshapes['circle2'], color) self._render_rectangle(obround.subshapes['rectangle'], color) - def _render_drill(self, circle, color): + def _render_drill(self, circle, color=None): + color = color if color is not None else self.drill_color self._render_circle(circle, color) def _render_test_record(self, primitive, color): - position = tuple(map(add, primitive.position, self.origin_in_inch)) + position = [pos + origin for pos, origin in zip(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.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_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.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) self.ctx.show_text(primitive.net_name) self.ctx.scale(1, -1) - def _clear_mask(self): - self.mask_ctx.set_operator(cairo.OPERATOR_OVER) - self.mask_ctx.set_source_rgba(*self.color, alpha=self.alpha) - self.mask_ctx.paint() + def _new_render_layer(self, color=None): + size_in_pixels = self.scale_point(self.size_in_inch) + layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) + ctx = cairo.Context(layer) + ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + ctx.scale(1, -1) + ctx.translate(-(self.origin_in_inch[0] * self.scale[0]), + (-self.origin_in_inch[1] * self.scale[0]) + - size_in_pixels[1]) + if self.invert: + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.set_source_rgba(*self.color, alpha=self.alpha) + ctx.paint() + self.ctx = ctx + self.active_layer = layer - def _render_mask(self): - self.ctx.set_operator(cairo.OPERATOR_OVER) - ptn = cairo.SurfacePattern(self.mask) + def _flatten(self): + self.output_ctx.set_operator(cairo.OPERATOR_OVER) + ptn = cairo.SurfacePattern(self.active_layer) ptn.set_matrix(self._xform_matrix) - self.ctx.set_source(ptn) - self.ctx.paint() + self.output_ctx.set_source(ptn) + self.output_ctx.paint() + self.ctx = None + self.active_layer = None def _paint_background(self, force=False): if (not self.bg) or force: self.bg = True - self.ctx.set_source_rgba(*self.background_color, alpha=1.0) - self.ctx.paint() + self.output_ctx.set_operator(cairo.OPERATOR_OVER) + self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0) + self.output_ctx.paint() + + def scale_point(self, point): + return tuple([coord * scale for coord, scale in zip(point, self.scale)]) diff --git a/gerber/render/render.py b/gerber/render/render.py index 6af8bf1..d7a62e1 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -57,12 +57,14 @@ class GerberContext(object): alpha : float Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ + def __init__(self, units='inch'): self._units = units self._color = (0.7215, 0.451, 0.200) self._background_color = (0.0, 0.0, 0.0) self._alpha = 1.0 self._invert = False + self.ctx = None @property def units(self): @@ -132,8 +134,7 @@ class GerberContext(object): self._invert = invert def render(self, primitive): - color = (self.color if primitive.level_polarity == 'dark' - else self.background_color) + color = self.color if isinstance(primitive, Line): self._render_line(primitive, color) elif isinstance(primitive, Arc): @@ -155,6 +156,7 @@ class GerberContext(object): else: return + def _render_line(self, primitive, color): pass @@ -184,9 +186,9 @@ class GerberContext(object): 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 e538df8..6135ccb 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -23,7 +23,7 @@ COLORS = { '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), + '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), @@ -36,6 +36,7 @@ COLORS = { class Theme(object): + 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)) @@ -67,4 +68,3 @@ THEMES = { topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True), bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)), } - -- cgit From 66a0d09e72b078da5820820aa5c6a2a7d7430507 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 04:39:55 -0500 Subject: Add support for mirrored rendering - The default theme now renders the bottom layers mirrored. - see https://github.com/curtacircuitos/pcb-tools/blob/master/examples/pcb_bottom.png for an example. --- gerber/render/cairo_backend.py | 19 ++++++++++++------- gerber/render/theme.py | 11 ++++++----- 2 files changed, 18 insertions(+), 12 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index cc2722a..2370eb9 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -18,6 +18,7 @@ import cairocffi as cairo import tempfile +import copy from .render import GerberContext, RenderSettings from .theme import THEMES @@ -37,6 +38,7 @@ class GerberCairoContext(GerberContext): self.surface = None self.ctx = None self.active_layer = None + self.active_matrix = None self.output_ctx = None self.bg = False self.mask = None @@ -120,9 +122,7 @@ class GerberCairoContext(GerberContext): self.invert = settings.invert # Get a new clean layer to render on - self._new_render_layer() - if settings.mirror: - raise Warning('mirrored layers aren\'t supported yet...') + self._new_render_layer(mirror=settings.mirror) for prim in layer.primitives: self.render(prim) # Add layer to image @@ -262,30 +262,35 @@ class GerberCairoContext(GerberContext): self.ctx.show_text(primitive.net_name) self.ctx.scale(1, -1) - def _new_render_layer(self, color=None): + def _new_render_layer(self, color=None, mirror=False): size_in_pixels = self.scale_point(self.size_in_inch) layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) ctx = cairo.Context(layer) ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) ctx.scale(1, -1) ctx.translate(-(self.origin_in_inch[0] * self.scale[0]), - (-self.origin_in_inch[1] * self.scale[0]) - - size_in_pixels[1]) + (-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) if self.invert: ctx.set_operator(cairo.OPERATOR_OVER) ctx.set_source_rgba(*self.color, alpha=self.alpha) ctx.paint() + matrix = copy.copy(self._xform_matrix) + if mirror: + matrix.xx = -1.0 + matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] self.ctx = ctx self.active_layer = layer + self.active_matrix = matrix def _flatten(self): self.output_ctx.set_operator(cairo.OPERATOR_OVER) ptn = cairo.SurfacePattern(self.active_layer) - ptn.set_matrix(self._xform_matrix) + ptn.set_matrix(self.active_matrix) self.output_ctx.set_source(ptn) self.output_ctx.paint() self.ctx = None self.active_layer = None + self.active_matrix = None def _paint_background(self, force=False): if (not self.bg) or force: diff --git a/gerber/render/theme.py b/gerber/render/theme.py index 6135ccb..4d325c5 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -41,11 +41,11 @@ class Theme(object): 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.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=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.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True, mirror=True)) self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) - self.bottom = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) + self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True)) self.drill = kwargs.get('drill', RenderSettings(COLORS['black'])) self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red'])) @@ -61,9 +61,10 @@ THEMES = { 'default': Theme(), 'OSH Park': Theme(name='OSH Park', top=RenderSettings(COLORS['enig copper']), - bottom=RenderSettings(COLORS['enig copper']), + bottom=RenderSettings(COLORS['enig copper'], mirror=True), topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True), - bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True)), + bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True, mirror=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 5df38c014fd09792995b2b12b1982c535c962c9a Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 28 Jan 2016 12:19:03 -0500 Subject: Cleanup, rendering fixes. fixed rendering of tented vias fixed rendering of semi-transparent layers fixed file type detection issues added some examples --- gerber/render/__init__.py | 1 + gerber/render/cairo_backend.py | 172 +++++++++++++++++++++-------------------- gerber/render/render.py | 19 ++--- gerber/render/theme.py | 23 ++++-- 4 files changed, 113 insertions(+), 102 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index f76d28f..3598c4d 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -25,3 +25,4 @@ SVG is the only supported format. from .cairo_backend import GerberCairoContext +from .render import RenderSettings diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 2370eb9..df4fcf1 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -17,6 +17,7 @@ import cairocffi as cairo +import os import tempfile import copy @@ -36,16 +37,16 @@ class GerberCairoContext(GerberContext): super(GerberCairoContext, self).__init__() self.scale = (scale, scale) self.surface = None + self.surface_buffer = None self.ctx = None self.active_layer = None self.active_matrix = None self.output_ctx = None - self.bg = False - self.mask = None - self.mask_ctx = None + self.has_bg = False self.origin_in_inch = None self.size_in_inch = None self._xform_matrix = None + self._render_count = 0 @property def origin_in_pixels(self): @@ -66,10 +67,8 @@ class GerberCairoContext(GerberContext): 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.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) self.output_ctx = cairo.Context(self.surface) - self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) self.output_ctx.scale(1, -1) self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) @@ -77,20 +76,44 @@ class GerberCairoContext(GerberContext): x0=-self.origin_in_pixels[0], y0=self.size_in_pixels[1] + self.origin_in_pixels[1]) - def render_layers(self, layers, filename, theme=THEMES['default']): + def render_layer(self, layer, filename=None, settings=None, bgsettings=None, + verbose=False): + if settings is None: + settings = THEMES['default'].get(layer.layer_class, RenderSettings()) + if bgsettings is None: + bgsettings = THEMES['default'].get('background', RenderSettings()) + + if self._render_count == 0: + if verbose: + print('[Render]: Rendering Background.') + self.clear() + self.set_bounds(layer.bounds) + self._paint_background(bgsettings) + if verbose: + print('[Render]: Rendering {} Layer.'.format(layer.layer_class)) + self._render_count += 1 + self._render_layer(layer, settings) + if filename is not None: + self.dump(filename, verbose) + + def render_layers(self, layers, filename, theme=THEMES['default'], + verbose=False): """ Render a set of layers """ - self.set_bounds(layers[0].bounds, True) - self._paint_background(True) - + self.clear() + bgsettings = theme['background'] for layer in layers: - self._render_layer(layer, theme) - self.dump(filename) + settings = theme.get(layer.layer_class, RenderSettings()) + self.render_layer(layer, settings=settings, bgsettings=bgsettings, + verbose=verbose) + self.dump(filename, verbose) - def dump(self, filename): + def dump(self, filename, verbose=False): """ Save image as `filename` """ - is_svg = filename.lower().endswith(".svg") + is_svg = os.path.splitext(filename.lower())[1] == '.svg' + if verbose: + print('[Render]: Writing image to {}'.format(filename)) if is_svg: self.surface.finish() self.surface_buffer.flush() @@ -115,30 +138,33 @@ class GerberCairoContext(GerberContext): self.surface_buffer.flush() return self.surface_buffer.read() - 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 + def clear(self): + self.surface = None + self.output_ctx = None + self.has_bg = False + self.origin_in_inch = None + self.size_in_inch = None + self._xform_matrix = None + self._render_count = 0 + if hasattr(self.surface_buffer, 'close'): + self.surface_buffer.close() + self.surface_buffer = None + def _render_layer(self, layer, settings): + self.invert = settings.invert # Get a new clean layer to render on self._new_render_layer(mirror=settings.mirror) for prim in layer.primitives: self.render(prim) # Add layer to image - self._flatten() + self._paint(settings.color, settings.alpha) def _render_line(self, line, color): start = [pos * scale for pos, scale in zip(line.start, self.scale)] end = [pos * scale for pos, scale in zip(line.end, self.scale)] - 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' - else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + if line.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) if isinstance(line.aperture, Circle): width = line.aperture.diameter self.ctx.set_line_width(width * self.scale[0]) @@ -162,14 +188,9 @@ class GerberCairoContext(GerberContext): angle1 = arc.start_angle angle2 = arc.end_angle width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 - if not self.invert: - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER - if arc.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + 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... @@ -181,14 +202,9 @@ class GerberCairoContext(GerberContext): self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): - if not self.invert: - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER - if region.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + if region.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.set_line_width(0) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*self.scale_point(region.primitives[0].start)) @@ -210,29 +226,22 @@ class GerberCairoContext(GerberContext): def _render_circle(self, circle, color): center = self.scale_point(circle.position) - 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' else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + 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.arc(*center, radius=(circle.radius * self.scale[0]), angle1=0, + angle2=(2 * math.pi)) self.ctx.fill() def _render_rectangle(self, rectangle, color): lower_left = self.scale_point(rectangle.lower_left) - width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) - - 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' else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + width, height = tuple([abs(coord) for coord in + self.scale_point((rectangle.width, + rectangle.height))]) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + if rectangle.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.set_line_width(0) self.ctx.rectangle(*lower_left, width=width, height=height) self.ctx.fill() @@ -247,34 +256,31 @@ class GerberCairoContext(GerberContext): self._render_circle(circle, color) def _render_test_record(self, primitive, color): - position = [pos + origin for pos, origin in zip(primitive.position, self.origin_in_inch)] - self.ctx.set_operator(cairo.OPERATOR_OVER) + position = [pos + origin for pos, origin in + zip(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_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.set_operator(cairo.OPERATOR_SOURCE + if primitive.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) + 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) def _new_render_layer(self, color=None, mirror=False): size_in_pixels = self.scale_point(self.size_in_inch) + matrix = copy.copy(self._xform_matrix) layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) ctx = cairo.Context(layer) - ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) ctx.scale(1, -1) ctx.translate(-(self.origin_in_inch[0] * self.scale[0]), - (-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) + (-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) if self.invert: ctx.set_operator(cairo.OPERATOR_OVER) - ctx.set_source_rgba(*self.color, alpha=self.alpha) ctx.paint() - matrix = copy.copy(self._xform_matrix) if mirror: matrix.xx = -1.0 matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] @@ -282,21 +288,23 @@ class GerberCairoContext(GerberContext): self.active_layer = layer self.active_matrix = matrix - def _flatten(self): - self.output_ctx.set_operator(cairo.OPERATOR_OVER) + def _paint(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 ptn = cairo.SurfacePattern(self.active_layer) ptn.set_matrix(self.active_matrix) - self.output_ctx.set_source(ptn) - self.output_ctx.paint() + self.output_ctx.set_source_rgba(*color, alpha=alpha) + self.output_ctx.mask(ptn) self.ctx = None self.active_layer = None self.active_matrix = None - def _paint_background(self, force=False): - if (not self.bg) or force: - self.bg = True - self.output_ctx.set_operator(cairo.OPERATOR_OVER) - self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0) + def _paint_background(self, settings=None): + color = settings.color if settings is not None else self.background_color + 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.paint() def scale_point(self, point): diff --git a/gerber/render/render.py b/gerber/render/render.py index d7a62e1..724aaea 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -45,7 +45,8 @@ class GerberContext(object): Measurement units. 'inch' or 'metric' color : tuple (, , ) - Color used for rendering as a tuple of normalized (red, green, blue) values. + Color used for rendering as a tuple of normalized (red, green, blue) + values. drill_color : tuple (, , ) Color used for rendering drill hits. Format is the same as for `color`. @@ -62,8 +63,9 @@ class GerberContext(object): self._units = units self._color = (0.7215, 0.451, 0.200) self._background_color = (0.0, 0.0, 0.0) + self._drill_color = (0.0, 0.0, 0.0) self._alpha = 1.0 - self._invert = False + self.invert = False self.ctx = None @property @@ -125,14 +127,6 @@ class GerberContext(object): raise ValueError('Alpha must be between 0.0 and 1.0') self._alpha = alpha - @property - def invert(self): - return self._invert - - @invert.setter - def invert(self, invert): - self._invert = invert - def render(self, primitive): color = self.color if isinstance(primitive, Line): @@ -156,7 +150,6 @@ class GerberContext(object): else: return - def _render_line(self, primitive, color): pass @@ -186,8 +179,8 @@ class GerberContext(object): class RenderSettings(object): - - def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, mirror=False): + 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 diff --git a/gerber/render/theme.py b/gerber/render/theme.py index 4d325c5..d382a8d 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -25,12 +25,12 @@ COLORS = { '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), + 'green soldermask': (0.0, 0.412, 0.278), 'blue soldermask': (0.059, 0.478, 0.651), 'red soldermask': (0.968, 0.169, 0.165), 'black soldermask': (0.298, 0.275, 0.282), 'purple soldermask': (0.2, 0.0, 0.334), - 'enig copper': (0.686, 0.525, 0.510), + 'enig copper': (0.694, 0.533, 0.514), 'hasl copper': (0.871, 0.851, 0.839) } @@ -39,11 +39,11 @@ class Theme(object): 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.background = kwargs.get('background', RenderSettings(COLORS['fr-4'])) self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white'])) self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=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, mirror=True)) + self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True)) + self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True, mirror=True)) self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True)) self.drill = kwargs.get('drill', RenderSettings(COLORS['black'])) @@ -60,12 +60,21 @@ class Theme(object): THEMES = { 'default': Theme(), 'OSH Park': Theme(name='OSH Park', + background=RenderSettings(COLORS['purple soldermask']), top=RenderSettings(COLORS['enig copper']), bottom=RenderSettings(COLORS['enig copper'], mirror=True), - topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True), - bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True, mirror=True)), + topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True), + bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True, mirror=True), + topsilk=RenderSettings(COLORS['white'], alpha=0.8), + bottomsilk=RenderSettings(COLORS['white'], alpha=0.8, mirror=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)), + + 'Transparent Copper': Theme(name='Transparent', + background=RenderSettings((0.9, 0.9, 0.9)), + top=RenderSettings(COLORS['red'], alpha=0.5), + bottom=RenderSettings(COLORS['blue'], alpha=0.5), + drill=RenderSettings((0.3, 0.3, 0.3))), } -- cgit From 96bdd0f59dbda9b755b0eb28eb44cb9a6eae1410 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 31 Jan 2016 15:24:57 +0800 Subject: Keep track of quadrant mode so we can draw full circles --- gerber/render/cairo_backend.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index fbc4271..7be7e6a 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -76,6 +76,9 @@ class GerberCairoContext(GerberContext): radius = self.scale[0] * arc.radius angle1 = arc.start_angle angle2 = arc.end_angle + if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant': + # Make the angles slightly different otherwise Cario will draw nothing + angle2 -= 0.000000001 if isinstance(arc.aperture, Circle): width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 else: -- cgit From 29c0d82bf53907030d11df9eb09471b716a0be2e Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 27 Feb 2016 15:24:36 +0800 Subject: RS274X backend for rendering. Incompelte still --- gerber/render/rs274x_backend.py | 290 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 gerber/render/rs274x_backend.py (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py new file mode 100644 index 0000000..0094192 --- /dev/null +++ b/gerber/render/rs274x_backend.py @@ -0,0 +1,290 @@ + +from .render import GerberContext +from ..gerber_statements import * +from ..primitives import AMGroup, Arc, Circle, Line, Rectangle + +class Rs274xContext(GerberContext): + + def __init__(self, settings): + GerberContext.__init__(self) + self.header = [] + self.body = [] + self.end = [EofStmt()] + + # Current values so we know if we have to execute + # moves, levey changes before anything else + self._level_polarity = None + self._pos = (None, None) + self._func = None + self._quadrant_mode = None + self._dcode = None + + self._next_dcode = 10 + self._rects = {} + self._circles = {} + self._macros = {} + + self._i_none = 0 + self._j_none = 0 + + self._define_dcodes() + + + def _define_dcodes(self): + + self._get_circle(.1575, 10) + self._get_circle(.035, 17) + self._get_rectangle(0.1575, 0.1181, 15) + self._get_rectangle(0.0492, 0.0118, 16) + self._get_circle(.0197, 11) + self._get_rectangle(0.0236, 0.0591, 12) + self._get_circle(.005, 18) + self._get_circle(.008, 19) + self._get_circle(.009, 20) + self._get_circle(.01, 21) + self._get_circle(.02, 22) + self._get_circle(.006, 23) + self._get_circle(.015, 24) + self._get_rectangle(0.1678, 0.1284, 26) + self._get_rectangle(0.0338, 0.0694, 25) + + def _simplify_point(self, point): + return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) + + def _simplify_offset(self, point, offset): + + if point[0] != offset[0]: + xoffset = point[0] - offset[0] + else: + xoffset = self._i_none + + if point[1] != offset[1]: + yoffset = point[1] - offset[1] + else: + yoffset = self._j_none + + return (xoffset, yoffset) + + @property + def statements(self): + return self.header + self.body + self.end + + def set_bounds(self, bounds): + pass + + def _paint_background(self): + pass + + def _select_aperture(self, aperture): + + # Select the right aperture if not already selected + if aperture: + if isinstance(aperture, Circle): + aper = self._get_circle(aperture.diameter) + elif isinstance(aperture, Rectangle): + aper = self._get_rectangle(aperture.width, aperture.height) + else: + raise NotImplementedError('Line with invalid aperture type') + + if aper.d != self._dcode: + self.body.append(ApertureStmt(aper.d)) + self._dcode = aper.d + + def _render_line(self, line, color): + + self._select_aperture(line.aperture) + + # Get the right function + if self._func != CoordStmt.FUNC_LINEAR: + func = CoordStmt.FUNC_LINEAR + else: + func = None + self._func = CoordStmt.FUNC_LINEAR + + if self._pos != line.start: + self.body.append(CoordStmt.move(func, self._simplify_point(line.start))) + self._pos = line.start + # We already set the function, so the next command doesn't require that + func = None + + self.body.append(CoordStmt.line(func, self._simplify_point(line.end))) + self._pos = line.end + + def _render_arc(self, arc, color): + + # Optionally set the quadrant mode if it has changed: + if arc.quadrant_mode != self._quadrant_mode: + + if arc.quadrant_mode != 'multi-quadrant': + self.body.append(QuadrantModeStmt.single()) + else: + self.body.append(QuadrantModeStmt.multi()) + + self._quadrant_mode = arc.quadrant_mode + + # Select the right aperture if not already selected + self._select_aperture(arc.aperture) + + # Find the right movement mode. Always set to be sure it is really right + dir = arc.direction + if dir == 'clockwise': + func = CoordStmt.FUNC_ARC_CW + self._func = CoordStmt.FUNC_ARC_CW + elif dir == 'counterclockwise': + func = CoordStmt.FUNC_ARC_CCW + self._func = CoordStmt.FUNC_ARC_CCW + else: + raise ValueError('Invalid circular interpolation mode') + + if self._pos != arc.start: + # TODO I'm not sure if this is right + self.body.append(CoordStmt.move(CoordStmt.FUNC_LINEAR, self._simplify_point(arc.start))) + self._pos = arc.start + + center = self._simplify_offset(arc.center, arc.start) + end = self._simplify_point(arc.end) + self.body.append(CoordStmt.arc(func, end, center)) + self._pos = arc.end + + def _render_region(self, region, color): + + self._render_level_polarity(region) + + self.body.append(RegionModeStmt.on()) + + for p in region.primitives: + + if isinstance(p, Line): + self._render_line(p, color) + else: + self._render_arc(p, color) + + + self.body.append(RegionModeStmt.off()) + + def _render_level_polarity(self, region): + if region.level_polarity != self._level_polarity: + self._level_polarity = region.level_polarity + self.body.append(LPParamStmt.from_region(region)) + + def _render_flash(self, primitive, aperture): + + if aperture.d != self._dcode: + self.body.append(ApertureStmt(aperture.d)) + self._dcode = aperture.d + + self.body.append(CoordStmt.flash( self._simplify_point(primitive.position))) + self._pos = primitive.position + + def _get_circle(self, diameter, dcode = None): + '''Define a circlar aperture''' + + aper = self._circles.get(diameter, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.circle(dcode, diameter) + self._circles[diameter] = aper + self.header.append(aper) + + return aper + + def _render_circle(self, circle, color): + + aper = self._get_circle(circle.diameter) + self._render_flash(circle, aper) + + def _get_rectangle(self, width, height, dcode = None): + '''Get a rectanglar aperture. If it isn't defined, create it''' + + key = (width, height) + aper = self._rects.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.rect(dcode, width, height) + self._rects[(width, height)] = aper + self.header.append(aper) + + return aper + + def _render_rectangle(self, rectangle, color): + + aper = self._get_rectangle(rectangle.width, rectangle.height) + self._render_flash(rectangle, aper) + + def _render_obround(self, obround, color): + pass + + def _render_polygon(self, polygon, color): + pass + + def _render_drill(self, circle, color): + pass + + def _hash_amacro(self, amgroup): + '''Calculate a very quick hash code for deciding if we should even check AM groups for comparision''' + + hash = '' + for primitive in amgroup.primitives: + + hash += primitive.__class__.__name__[0] + if hasattr(primitive, 'primitives'): + hash += str(len(primitive.primitives)) + + return hash + + def _get_amacro(self, amgroup, dcode = None): + # Macros are a little special since we don't have a good way to compare them quickly + # but in most cases, this should work + + hash = self._hash_amacro(amgroup) + macro = self._macros.get(hash, None) + + if not macro: + # This is a new macro, so define it + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + # Create the statements + # TODO + statements = [] + aperdef = ADParamStmt.macro(dcode, hash) + + # Store the dcode and the original so we can check if it really is the same + macro = (aperdef, amgroup) + self._macros[hash] = macro + + else: + # We hae a definition, but check that the groups actually are the same + offset = (amgroup.position[0] - macro[1].position[0], amgroup.position[1] - macro[1].position[1]) + if not amgroup.equivalent(macro[1], offset): + raise ValueError('Two AMGroup have the same hash but are not equivalent') + + return macro[0] + + def _render_amgroup(self, amgroup, color): + + aper = self._get_amacro(amgroup) + self._render_flash(amgroup, aper) + + def _render_inverted_layer(self): + pass + + def post_render_primitives(self): + '''No more primitives, so set the end marker''' + + self.body.append() \ No newline at end of file -- cgit From 223a010831f0d9dae4bd6d2e626a603a78eb0b1d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 27 Feb 2016 18:18:04 +0800 Subject: Fix critical issue with rotatin points (when the angle is zero the y would be flipped). Render AM with outline to gerber --- gerber/render/rs274x_backend.py | 61 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 0094192..bdb77f4 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -1,12 +1,49 @@ from .render import GerberContext +from ..am_statements import * from ..gerber_statements import * -from ..primitives import AMGroup, Arc, Circle, Line, Rectangle +from ..primitives import AMGroup, Arc, Circle, Line, Outline, Rectangle +from copy import deepcopy + +class AMGroupContext(object): + '''A special renderer to generate aperature macros from an AMGroup''' + + def __init__(self): + self.statements = [] + + def render(self, amgroup, name): + + # Clone ourselves, then offset by the psotion so that + # our render doesn't have to consider offset. Just makes things simplder + nooffset_group = deepcopy(amgroup) + nooffset_group.position = (0, 0) + + # Now draw the shapes + for primitive in nooffset_group.primitives: + if isinstance(primitive, Outline): + self._render_outline(primitive) + + statement = AMParamStmt('AM', name, self._statements_to_string()) + return statement + + def _statements_to_string(self): + macro = '' + + for statement in self.statements: + macro += statement.to_gerber() + + return macro + + def _render_outline(self, outline): + self.statements.append(AMOutlinePrimitive.from_primitive(outline)) + + class Rs274xContext(GerberContext): def __init__(self, settings): GerberContext.__init__(self) + self.comments = [] self.header = [] self.body = [] self.end = [EofStmt()] @@ -27,8 +64,13 @@ class Rs274xContext(GerberContext): self._i_none = 0 self._j_none = 0 + self.settings = settings + + self._start_header(settings) self._define_dcodes() + def _start_header(self, settings): + self.header.append(MOParamStmt.from_units(settings.units)) def _define_dcodes(self): @@ -67,7 +109,7 @@ class Rs274xContext(GerberContext): @property def statements(self): - return self.header + self.body + self.end + return self.comments + self.header + self.body + self.end def set_bounds(self, bounds): pass @@ -93,6 +135,8 @@ class Rs274xContext(GerberContext): def _render_line(self, line, color): self._select_aperture(line.aperture) + + self._render_level_polarity(line) # Get the right function if self._func != CoordStmt.FUNC_LINEAR: @@ -125,6 +169,8 @@ class Rs274xContext(GerberContext): # Select the right aperture if not already selected self._select_aperture(arc.aperture) + self._render_level_polarity(arc) + # Find the right movement mode. Always set to be sure it is really right dir = arc.direction if dir == 'clockwise': @@ -243,7 +289,7 @@ class Rs274xContext(GerberContext): hash += str(len(primitive.primitives)) return hash - + def _get_amacro(self, amgroup, dcode = None): # Macros are a little special since we don't have a good way to compare them quickly # but in most cases, this should work @@ -261,8 +307,13 @@ class Rs274xContext(GerberContext): # Create the statements # TODO - statements = [] + amrenderer = AMGroupContext() + statement = amrenderer.render(amgroup, hash) + + self.header.append(statement) + aperdef = ADParamStmt.macro(dcode, hash) + self.header.append(aperdef) # Store the dcode and the original so we can check if it really is the same macro = (aperdef, amgroup) @@ -281,6 +332,8 @@ class Rs274xContext(GerberContext): aper = self._get_amacro(amgroup) self._render_flash(amgroup, aper) + + def _render_inverted_layer(self): pass -- cgit From 20a9af279ac2217a39b73903ff94b916a3025be2 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Tue, 1 Mar 2016 00:06:14 +0800 Subject: More rendering of AMGroup to statements --- gerber/render/rs274x_backend.py | 72 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 7 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index bdb77f4..2a0420e 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -22,6 +22,14 @@ class AMGroupContext(object): for primitive in nooffset_group.primitives: if isinstance(primitive, Outline): self._render_outline(primitive) + elif isinstance(primitive, Circle): + self._render_circle(primitive) + elif isinstance(primitive, Rectangle): + self._render_rectangle(primitive) + elif isinstance(primitive, Line): + self._render_line(primitive) + else: + raise ValueError('amgroup') statement = AMParamStmt('AM', name, self._statements_to_string()) return statement @@ -33,10 +41,21 @@ class AMGroupContext(object): macro += statement.to_gerber() return macro + + def _render_circle(self, circle): + self.statements.append(AMCirclePrimitive.from_primitive(circle)) + + def _render_rectangle(self, rectangle): + self.statements.append(AMCenterLinePrimitive.from_primitive(rectangle)) + + def _render_line(self, line): + self.statements.append(AMVectorLinePrimitive.from_primitive(line)) def _render_outline(self, outline): self.statements.append(AMOutlinePrimitive.from_primitive(outline)) - + + def _render_thermal(self, thermal): + pass class Rs274xContext(GerberContext): @@ -59,6 +78,8 @@ class Rs274xContext(GerberContext): self._next_dcode = 10 self._rects = {} self._circles = {} + self._obrounds = {} + self._polygons = {} self._macros = {} self._i_none = 0 @@ -67,9 +88,10 @@ class Rs274xContext(GerberContext): self.settings = settings self._start_header(settings) - self._define_dcodes() + #self._define_dcodes() def _start_header(self, settings): + self.header.append(FSParamStmt.from_settings(settings)) self.header.append(MOParamStmt.from_units(settings.units)) def _define_dcodes(self): @@ -151,8 +173,12 @@ class Rs274xContext(GerberContext): # We already set the function, so the next command doesn't require that func = None - self.body.append(CoordStmt.line(func, self._simplify_point(line.end))) - self._pos = line.end + point = self._simplify_point(line.end) + + # In some files, we see a lot of duplicated ponts, so omit those + if point[0] != None or point[1] != None: + self.body.append(CoordStmt.line(func, self._simplify_point(line.end))) + self._pos = line.end def _render_arc(self, arc, color): @@ -269,10 +295,33 @@ class Rs274xContext(GerberContext): aper = self._get_rectangle(rectangle.width, rectangle.height) self._render_flash(rectangle, aper) + def _get_obround(self, width, height, dcode = None): + + key = (width, height) + aper = self._obrounds.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.obround(dcode, width, height) + self._obrounds[(width, height)] = aper + self.header.append(aper) + + return aper + def _render_obround(self, obround, color): + + aper = self._get_obround(obround.width, obround.height) + self._render_flash(obround, aper) + pass def _render_polygon(self, polygon, color): + raise NotImplementedError('Not implemented yet') pass def _render_drill(self, circle, color): @@ -285,8 +334,19 @@ class Rs274xContext(GerberContext): for primitive in amgroup.primitives: hash += primitive.__class__.__name__[0] + + bbox = primitive.bounding_box + hash += str((bbox[0][1] - bbox[0][0]) * 100000)[0:2] + hash += str((bbox[1][1] - bbox[1][0]) * 100000)[0:2] + if hasattr(primitive, 'primitives'): hash += str(len(primitive.primitives)) + + if isinstance(primitive, Rectangle): + hash += str(primitive.width * 1000000)[0:2] + hash += str(primitive.height * 1000000)[0:2] + elif isinstance(primitive, Circle): + hash += str(primitive.diameter * 1000000)[0:2] return hash @@ -331,9 +391,7 @@ class Rs274xContext(GerberContext): aper = self._get_amacro(amgroup) self._render_flash(amgroup, aper) - - - + def _render_inverted_layer(self): pass -- cgit From 7b88509c4acb4edbbe1a39762758bf28efecfc7f Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 5 Mar 2016 09:24:54 +0800 Subject: Make writer resilient to similar macro defs --- gerber/render/rs274x_backend.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 2a0420e..d4456e2 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -355,8 +355,19 @@ class Rs274xContext(GerberContext): # but in most cases, this should work hash = self._hash_amacro(amgroup) - macro = self._macros.get(hash, None) + macro = None + macroinfo = self._macros.get(hash, None) + if macroinfo: + + # We hae a definition, but check that the groups actually are the same + for macro in macroinfo: + offset = (amgroup.position[0] - macro[1].position[0], amgroup.position[1] - macro[1].position[1]) + if amgroup.equivalent(macro[1], offset): + break + macro = None + + # Did we find one in the group0 if not macro: # This is a new macro, so define it if not dcode: @@ -377,13 +388,11 @@ class Rs274xContext(GerberContext): # Store the dcode and the original so we can check if it really is the same macro = (aperdef, amgroup) - self._macros[hash] = macro - - else: - # We hae a definition, but check that the groups actually are the same - offset = (amgroup.position[0] - macro[1].position[0], amgroup.position[1] - macro[1].position[1]) - if not amgroup.equivalent(macro[1], offset): - raise ValueError('Two AMGroup have the same hash but are not equivalent') + + if macroinfo: + macroinfo.append(macro) + else: + self._macros[hash] = [macro] return macro[0] -- cgit From 7f47aea332ee1df45c87baa304d95ed03cc59865 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 5 Mar 2016 10:04:58 +0800 Subject: Write polygons to macros --- gerber/render/rs274x_backend.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index d4456e2..04ecbe6 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -2,7 +2,7 @@ from .render import GerberContext from ..am_statements import * from ..gerber_statements import * -from ..primitives import AMGroup, Arc, Circle, Line, Outline, Rectangle +from ..primitives import AMGroup, Arc, Circle, Line, Outline, Polygon, Rectangle from copy import deepcopy class AMGroupContext(object): @@ -28,6 +28,8 @@ class AMGroupContext(object): self._render_rectangle(primitive) elif isinstance(primitive, Line): self._render_line(primitive) + elif isinstance(primitive, Polygon): + self._render_polygon(primitive) else: raise ValueError('amgroup') @@ -53,6 +55,9 @@ class AMGroupContext(object): def _render_outline(self, outline): self.statements.append(AMOutlinePrimitive.from_primitive(outline)) + + def _render_polygon(self, polygon): + self.statements.append(AMPolygonPrimitive.from_primitive(polygon)) def _render_thermal(self, thermal): pass -- cgit From 97355475686dd4bdad1b0bd9a307843ea3c234f2 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 5 Mar 2016 10:28:38 +0800 Subject: Make rendering more robust for bad gerber files --- gerber/render/rs274x_backend.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 04ecbe6..5a15fe5 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -2,7 +2,7 @@ from .render import GerberContext from ..am_statements import * from ..gerber_statements import * -from ..primitives import AMGroup, Arc, Circle, Line, Outline, Polygon, Rectangle +from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon, Rectangle from copy import deepcopy class AMGroupContext(object): @@ -152,6 +152,10 @@ class Rs274xContext(GerberContext): aper = self._get_circle(aperture.diameter) elif isinstance(aperture, Rectangle): aper = self._get_rectangle(aperture.width, aperture.height) + elif isinstance(aperture, Obround): + aper = self._get_obround(aperture.width, aperture.height) + elif isinstance(aperture, AMGroup): + aper = self._get_amacro(aperture) else: raise NotImplementedError('Line with invalid aperture type') @@ -367,7 +371,15 @@ class Rs274xContext(GerberContext): # We hae a definition, but check that the groups actually are the same for macro in macroinfo: - offset = (amgroup.position[0] - macro[1].position[0], amgroup.position[1] - macro[1].position[1]) + + # Macros should have positions, right? But if the macro is selected for non-flashes + # then it won't have a position. This is of course a bad gerber, but they do exist + if amgroup.position: + position = amgroup.position + else: + position = (0, 0) + + offset = (position[0] - macro[1].position[0], position[1] - macro[1].position[1]) if amgroup.equivalent(macro[1], offset): break macro = None -- cgit From 0f1d1c3a29017ea82e1f0f7795798405ef346706 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 5 Mar 2016 14:56:08 +0800 Subject: Remove some testing code from gerber writer. More implementation for excellon writer - not working yet --- gerber/render/excellon_backend.py | 76 +++++++++++++++++++++++++++++++++++++++ gerber/render/rs274x_backend.py | 32 +++-------------- 2 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 gerber/render/excellon_backend.py (limited to 'gerber/render') diff --git a/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py new file mode 100644 index 0000000..bec8367 --- /dev/null +++ b/gerber/render/excellon_backend.py @@ -0,0 +1,76 @@ + +from .render import GerberContext +from ..excellon_statements import * + +class ExcellonContext(GerberContext): + + def __init__(self, settings): + GerberContext.__init__(self) + self.comments = [] + self.header = [] + self.tool_def = [] + self.body = [] + self.end = [EndOfProgramStmt()] + + self.handled_tools = set() + self.cur_tool = None + self.pos = (None, None) + + self.settings = settings + + self._start_header(settings) + + def _start_header(self, settings): + pass + + @property + def statements(self): + return self.comments + self.header + self.body + self.end + + def set_bounds(self, bounds): + pass + + def _paint_background(self): + pass + + def _render_line(self, line, color): + raise ValueError('Invalid Excellon object') + def _render_arc(self, arc, color): + raise ValueError('Invalid Excellon object') + + def _render_region(self, region, color): + raise ValueError('Invalid Excellon object') + + def _render_level_polarity(self, region): + raise ValueError('Invalid Excellon object') + + def _render_circle(self, circle, color): + raise ValueError('Invalid Excellon object') + + def _render_rectangle(self, rectangle, color): + raise ValueError('Invalid Excellon object') + + def _render_obround(self, obround, color): + raise ValueError('Invalid Excellon object') + + def _render_polygon(self, polygon, color): + raise ValueError('Invalid Excellon object') + + def _simplify_point(self, point): + return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) + + def _render_drill(self, drill, color): + + if not drill in self.handled_tools: + self.tool_def.append(drill.tool) + + if drill.tool != self.cur_tool: + self.body.append(ToolSelectionStmt(drill.tool.number)) + + point = self._simplify_point(drill.position) + self._pos = drill.position + self.body.append(CoordinateStmt.from_point()) + + def _render_inverted_layer(self): + pass + \ No newline at end of file diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 5a15fe5..81e86f2 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -93,30 +93,11 @@ class Rs274xContext(GerberContext): self.settings = settings self._start_header(settings) - #self._define_dcodes() def _start_header(self, settings): self.header.append(FSParamStmt.from_settings(settings)) self.header.append(MOParamStmt.from_units(settings.units)) - def _define_dcodes(self): - - self._get_circle(.1575, 10) - self._get_circle(.035, 17) - self._get_rectangle(0.1575, 0.1181, 15) - self._get_rectangle(0.0492, 0.0118, 16) - self._get_circle(.0197, 11) - self._get_rectangle(0.0236, 0.0591, 12) - self._get_circle(.005, 18) - self._get_circle(.008, 19) - self._get_circle(.009, 20) - self._get_circle(.01, 21) - self._get_circle(.02, 22) - self._get_circle(.006, 23) - self._get_circle(.015, 24) - self._get_rectangle(0.1678, 0.1284, 26) - self._get_rectangle(0.0338, 0.0694, 25) - def _simplify_point(self, point): return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) @@ -330,11 +311,10 @@ class Rs274xContext(GerberContext): pass def _render_polygon(self, polygon, color): - raise NotImplementedError('Not implemented yet') - pass + raise ValueError('Polygons can only exist in the context of aperture macro') - def _render_drill(self, circle, color): - pass + def _render_drill(self, drill, color): + raise ValueError('Drills are not valid in RS274X files') def _hash_amacro(self, amgroup): '''Calculate a very quick hash code for deciding if we should even check AM groups for comparision''' @@ -420,8 +400,4 @@ class Rs274xContext(GerberContext): def _render_inverted_layer(self): pass - - def post_render_primitives(self): - '''No more primitives, so set the end marker''' - - self.body.append() \ No newline at end of file + \ No newline at end of file -- cgit From 97924d188bf8fcc7d7537007464e65cbdc8c7bbb Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 5 Mar 2016 16:26:30 +0800 Subject: More robust writing, even for bad files. Remove accidentally added imports --- gerber/render/rs274x_backend.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 81e86f2..48a55e7 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -384,6 +384,9 @@ class Rs274xContext(GerberContext): self.header.append(aperdef) # Store the dcode and the original so we can check if it really is the same + # If it didn't have a postition, set it to 0, 0 + if amgroup.position == None: + amgroup.position = (0, 0) macro = (aperdef, amgroup) if macroinfo: -- cgit From d12f6385a434c02677bfbb7b075dd9d8e49627fe Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Thu, 24 Mar 2016 00:10:34 +0800 Subject: Basic rendering of excellon works, but still has issues --- gerber/render/excellon_backend.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py index bec8367..f5cec1a 100644 --- a/gerber/render/excellon_backend.py +++ b/gerber/render/excellon_backend.py @@ -10,11 +10,12 @@ class ExcellonContext(GerberContext): self.header = [] self.tool_def = [] self.body = [] + self.start = [HeaderBeginStmt()] self.end = [EndOfProgramStmt()] self.handled_tools = set() self.cur_tool = None - self.pos = (None, None) + self._pos = (None, None) self.settings = settings @@ -25,7 +26,7 @@ class ExcellonContext(GerberContext): @property def statements(self): - return self.comments + self.header + self.body + self.end + return self.start + self.comments + self.header + self.body + self.end def set_bounds(self, bounds): pass @@ -61,15 +62,17 @@ class ExcellonContext(GerberContext): def _render_drill(self, drill, color): - if not drill in self.handled_tools: - self.tool_def.append(drill.tool) + tool = drill.hit.tool + if not tool in self.handled_tools: + self.handled_tools.add(tool) + self.header.append(ExcellonTool.from_tool(tool)) - if drill.tool != self.cur_tool: - self.body.append(ToolSelectionStmt(drill.tool.number)) + if tool != self.cur_tool: + self.body.append(ToolSelectionStmt(tool.number)) point = self._simplify_point(drill.position) self._pos = drill.position - self.body.append(CoordinateStmt.from_point()) + self.body.append(CoordinateStmt.from_point(point)) def _render_inverted_layer(self): pass -- cgit From acde19f205898188c03a46e5d8a7a6a4d4637a2d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 26 Mar 2016 15:59:42 +0800 Subject: Support for the G85 slot statement --- gerber/render/cairo_backend.py | 14 ++++++++++++++ gerber/render/excellon_backend.py | 33 +++++++++++++++++++++++++++++---- gerber/render/render.py | 5 +++++ 3 files changed, 48 insertions(+), 4 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 7be7e6a..d895e5c 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -173,6 +173,20 @@ class GerberCairoContext(GerberContext): def _render_drill(self, circle, color): self._render_circle(circle, color) + def _render_slot(self, slot, color): + start = map(mul, slot.start, self.scale) + end = map(mul, slot.end, self.scale) + + width = slot.diameter + + self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER if (slot.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() + def _render_amgroup(self, amgroup, color): for primitive in amgroup.primitives: self.render(primitive) diff --git a/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py index f5cec1a..eb79f1b 100644 --- a/gerber/render/excellon_backend.py +++ b/gerber/render/excellon_backend.py @@ -9,6 +9,7 @@ class ExcellonContext(GerberContext): self.comments = [] self.header = [] self.tool_def = [] + self.body_start = [RewindStopStmt()] self.body = [] self.start = [HeaderBeginStmt()] self.end = [EndOfProgramStmt()] @@ -19,14 +20,22 @@ class ExcellonContext(GerberContext): self.settings = settings - self._start_header(settings) + self._start_header() + self._start_comments() - def _start_header(self, settings): - pass + def _start_header(self): + """Create the header from the settings""" + + self.header.append(UnitStmt.from_settings(self.settings)) + + def _start_comments(self): + + # Write the digits used - this isn't valid Excellon statement, so we write as a comment + self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1]))) @property def statements(self): - return self.start + self.comments + self.header + self.body + self.end + return self.start + self.comments + self.header + self.body_start + self.body + self.end def set_bounds(self, bounds): pass @@ -69,10 +78,26 @@ class ExcellonContext(GerberContext): if tool != self.cur_tool: self.body.append(ToolSelectionStmt(tool.number)) + self.cur_tool = tool point = self._simplify_point(drill.position) self._pos = drill.position self.body.append(CoordinateStmt.from_point(point)) + + def _render_slot(self, slot, color): + + tool = slot.hit.tool + if not tool in self.handled_tools: + self.handled_tools.add(tool) + self.header.append(ExcellonTool.from_tool(tool)) + + if tool != self.cur_tool: + self.body.append(ToolSelectionStmt(tool.number)) + self.cur_tool = tool + + # Slots don't use simplified points + self._pos = slot.end + self.body.append(SlotStmt.from_points(slot.start, slot.end)) def _render_inverted_layer(self): pass diff --git a/gerber/render/render.py b/gerber/render/render.py index b518385..a5ae38e 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -150,6 +150,8 @@ class GerberContext(object): self._render_polygon(primitive, color) elif isinstance(primitive, Drill): self._render_drill(primitive, self.drill_color) + elif isinstance(primitive, Slot): + self._render_slot(primitive, self.drill_color) elif isinstance(primitive, AMGroup): self._render_amgroup(primitive, color) elif isinstance(primitive, Outline): @@ -183,6 +185,9 @@ class GerberContext(object): def _render_drill(self, primitive, color): pass + def _render_slot(self, primitive, color): + pass + def _render_amgroup(self, primitive, color): pass -- cgit From 288f49955ecc1a811752aa4b1e713f9954e3033b Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 27 Mar 2016 14:24:11 +0800 Subject: Actually fix the rout rendering to be correct --- gerber/render/excellon_backend.py | 98 +++++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 9 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py index eb79f1b..b2c213f 100644 --- a/gerber/render/excellon_backend.py +++ b/gerber/render/excellon_backend.py @@ -1,21 +1,29 @@ from .render import GerberContext +from ..excellon import DrillSlot from ..excellon_statements import * class ExcellonContext(GerberContext): + MODE_DRILL = 1 + MODE_SLOT =2 + def __init__(self, settings): GerberContext.__init__(self) + + # Statements that we write self.comments = [] self.header = [] self.tool_def = [] self.body_start = [RewindStopStmt()] self.body = [] self.start = [HeaderBeginStmt()] - self.end = [EndOfProgramStmt()] + # Current tool and position self.handled_tools = set() self.cur_tool = None + self.drill_mode = ExcellonContext.MODE_DRILL + self.drill_down = False self._pos = (None, None) self.settings = settings @@ -33,9 +41,22 @@ class ExcellonContext(GerberContext): # Write the digits used - this isn't valid Excellon statement, so we write as a comment self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1]))) + def _get_end(self): + """How we end depends on our mode""" + + end = [] + + if self.drill_down: + end.append(RetractWithClampingStmt()) + end.append(RetractWithoutClampingStmt()) + + end.append(EndOfProgramStmt()) + + return end + @property def statements(self): - return self.start + self.comments + self.header + self.body_start + self.body + self.end + return self.start + self.comments + self.header + self.body_start + self.body + self._get_end() def set_bounds(self, bounds): pass @@ -71,6 +92,9 @@ class ExcellonContext(GerberContext): def _render_drill(self, drill, color): + if self.drill_mode != ExcellonContext.MODE_DRILL: + self._start_drill_mode() + tool = drill.hit.tool if not tool in self.handled_tools: self.handled_tools.add(tool) @@ -84,20 +108,76 @@ class ExcellonContext(GerberContext): self._pos = drill.position self.body.append(CoordinateStmt.from_point(point)) + def _start_drill_mode(self): + """ + If we are not in drill mode, then end the ROUT so we can do basic drilling + """ + + if self.drill_mode == ExcellonContext.MODE_SLOT: + + # Make sure we are retracted before changing modes + last_cmd = self.body[-1] + if self.drill_down: + self.body.append(RetractWithClampingStmt()) + self.body.append(RetractWithoutClampingStmt()) + self.drill_down = False + + # Switch to drill mode + self.body.append(DrillModeStmt()) + self.drill_mode = ExcellonContext.MODE_DRILL + + else: + raise ValueError('Should be in slot mode') + def _render_slot(self, slot, color): + # Set the tool first, before we might go into drill mode tool = slot.hit.tool if not tool in self.handled_tools: self.handled_tools.add(tool) self.header.append(ExcellonTool.from_tool(tool)) - - if tool != self.cur_tool: - self.body.append(ToolSelectionStmt(tool.number)) - self.cur_tool = tool - # Slots don't use simplified points - self._pos = slot.end - self.body.append(SlotStmt.from_points(slot.start, slot.end)) + if tool != self.cur_tool: + self.body.append(ToolSelectionStmt(tool.number)) + self.cur_tool = tool + + # Two types of drilling - normal drill and slots + if slot.hit.slot_type == DrillSlot.TYPE_ROUT: + + # For ROUT, setting the mode is part of the actual command. + + # Are we in the right position? + if slot.start != self._pos: + if self.drill_down: + # We need to move into the right position, so retract + self.body.append(RetractWithClampingStmt()) + self.drill_down = False + + # Move to the right spot + point = self._simplify_point(slot.start) + self._pos = slot.start + self.body.append(CoordinateStmt.from_point(point, mode="ROUT")) + + # Now we are in the right spot, so drill down + if not self.drill_down: + self.body.append(ZAxisRoutPositionStmt()) + self.drill_down = True + + # Do a linear move from our current position to the end position + point = self._simplify_point(slot.end) + self._pos = slot.end + self.body.append(CoordinateStmt.from_point(point, mode="LINEAR")) + + self.drill_down = ExcellonContext.MODE_SLOT + + else: + # This is a G85 slot, so do this in normally drilling mode + if self.drill_mode != ExcellonContext.MODE_DRILL: + self._start_drill_mode() + + # Slots don't use simplified points + self._pos = slot.end + self.body.append(SlotStmt.from_points(slot.start, slot.end)) def _render_inverted_layer(self): pass -- cgit From 2eac1e427ca3264cb6dc36e0712020c1ca73fa9c Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Tue, 5 Apr 2016 22:40:12 +0800 Subject: Fix converting values for excellon files. Give error for incremental mode --- gerber/render/excellon_backend.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py index b2c213f..c477036 100644 --- a/gerber/render/excellon_backend.py +++ b/gerber/render/excellon_backend.py @@ -36,6 +36,11 @@ class ExcellonContext(GerberContext): self.header.append(UnitStmt.from_settings(self.settings)) + if self.settings.notation == 'incremental': + raise NotImplementedError('Incremental mode is not implemented') + else: + self.body.append(AbsoluteModeStmt()) + def _start_comments(self): # Write the digits used - this isn't valid Excellon statement, so we write as a comment -- cgit From 199a0f3d3c5d4dbbc4ac6e8d1b4548523e44f00f Mon Sep 17 00:00:00 2001 From: Qau Lau Date: Fri, 8 Apr 2016 20:02:04 +0800 Subject: Update cairo_backend.py If cairo module import error use cairocffi --- gerber/render/cairo_backend.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index d895e5c..1d168ca 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -17,7 +17,10 @@ from .render import GerberContext -import cairo +try: + import cairo +except ImportError: + import cairocffi as cairo from operator import mul import math @@ -233,4 +236,4 @@ class GerberCairoContext(GerberContext): self.surface.finish() self.surface_buffer.flush() return self.surface_buffer.read() - \ No newline at end of file + -- cgit From 7fda8eb9f52b7be9cdf95807c036e3e1cfce3e7c Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 8 May 2016 22:13:08 +0800 Subject: Don't render null items --- gerber/render/render.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index a5ae38e..41b632c 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -132,6 +132,8 @@ class GerberContext(object): self._invert = invert def render(self, primitive): + if not primitive: + return color = (self.color if primitive.level_polarity == 'dark' else self.background_color) if isinstance(primitive, Line): -- cgit From 74c638c7181e7a8ca4d0f791545bbf5db8b86c2a Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Thu, 19 May 2016 23:19:28 +0800 Subject: Fix issue where did not always switch into the G01 mode after G03 when the point was unchanged --- gerber/render/rs274x_backend.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 48a55e7..3dc8c1a 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -169,6 +169,8 @@ class Rs274xContext(GerberContext): if point[0] != None or point[1] != None: self.body.append(CoordStmt.line(func, self._simplify_point(line.end))) self._pos = line.end + elif func: + self.body.append(CoordStmt.mode(func)) def _render_arc(self, arc, color): -- cgit From c9c1313d598d5afa8cb387a2cfcd4a4281086e01 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 28 May 2016 12:36:31 +0800 Subject: Fix units statement. Keep track of original macro statement in the AMGroup --- gerber/render/rs274x_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 3dc8c1a..43695c3 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -14,7 +14,7 @@ class AMGroupContext(object): def render(self, amgroup, name): # Clone ourselves, then offset by the psotion so that - # our render doesn't have to consider offset. Just makes things simplder + # our render doesn't have to consider offset. Just makes things simpler nooffset_group = deepcopy(amgroup) nooffset_group.position = (0, 0) -- cgit From 3fc296918e7d0d343840c5daa08eb6d564660a29 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 28 May 2016 13:06:08 +0800 Subject: Use the known macro statement to render. Fix thermal not setting rotation --- gerber/render/rs274x_backend.py | 54 ++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 22 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 43695c3..2ca7014 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -13,28 +13,38 @@ class AMGroupContext(object): def render(self, amgroup, name): - # Clone ourselves, then offset by the psotion so that - # our render doesn't have to consider offset. Just makes things simpler - nooffset_group = deepcopy(amgroup) - nooffset_group.position = (0, 0) - - # Now draw the shapes - for primitive in nooffset_group.primitives: - if isinstance(primitive, Outline): - self._render_outline(primitive) - elif isinstance(primitive, Circle): - self._render_circle(primitive) - elif isinstance(primitive, Rectangle): - self._render_rectangle(primitive) - elif isinstance(primitive, Line): - self._render_line(primitive) - elif isinstance(primitive, Polygon): - self._render_polygon(primitive) - else: - raise ValueError('amgroup') - - statement = AMParamStmt('AM', name, self._statements_to_string()) - return statement + if amgroup.stmt: + # We know the statement it was generated from, so use that to create the AMParamStmt + # It will give a much better result + + stmt = deepcopy(amgroup.stmt) + stmt.name = name + + return stmt + + else: + # Clone ourselves, then offset by the psotion so that + # our render doesn't have to consider offset. Just makes things simpler + nooffset_group = deepcopy(amgroup) + nooffset_group.position = (0, 0) + + # Now draw the shapes + for primitive in nooffset_group.primitives: + if isinstance(primitive, Outline): + self._render_outline(primitive) + elif isinstance(primitive, Circle): + self._render_circle(primitive) + elif isinstance(primitive, Rectangle): + self._render_rectangle(primitive) + elif isinstance(primitive, Line): + self._render_line(primitive) + elif isinstance(primitive, Polygon): + self._render_polygon(primitive) + else: + raise ValueError('amgroup') + + statement = AMParamStmt('AM', name, self._statements_to_string()) + return statement def _statements_to_string(self): macro = '' -- cgit From ea97d9d0376db6ff7afcc7669eec84a228f8d201 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 28 May 2016 17:03:40 +0800 Subject: Fix issue with switching between ROUT and normal drill modes --- gerber/render/excellon_backend.py | 10 +++++----- gerber/render/rs274x_backend.py | 13 +++++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py index c477036..da5b22b 100644 --- a/gerber/render/excellon_backend.py +++ b/gerber/render/excellon_backend.py @@ -142,9 +142,9 @@ class ExcellonContext(GerberContext): self.handled_tools.add(tool) self.header.append(ExcellonTool.from_tool(tool)) - if tool != self.cur_tool: - self.body.append(ToolSelectionStmt(tool.number)) - self.cur_tool = tool + if tool != self.cur_tool: + self.body.append(ToolSelectionStmt(tool.number)) + self.cur_tool = tool # Two types of drilling - normal drill and slots if slot.hit.slot_type == DrillSlot.TYPE_ROUT: @@ -172,8 +172,8 @@ class ExcellonContext(GerberContext): point = self._simplify_point(slot.end) self._pos = slot.end self.body.append(CoordinateStmt.from_point(point, mode="LINEAR")) - - self.drill_down = ExcellonContext.MODE_SLOT + + self.drill_mode = ExcellonContext.MODE_SLOT else: # This is a G85 slot, so do this in normally drilling mode diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 2ca7014..bb784b1 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -331,7 +331,11 @@ class Rs274xContext(GerberContext): def _hash_amacro(self, amgroup): '''Calculate a very quick hash code for deciding if we should even check AM groups for comparision''' - hash = '' + # We always start with an X because this forms part of the name + # Basically, in some cases, the name might start with a C, R, etc. That can appear + # to conflict with normal aperture definitions. Technically, it shouldn't because normal + # aperture definitions should have a comma, but in some cases the commit is omitted + hash = 'X' for primitive in amgroup.primitives: hash += primitive.__class__.__name__[0] @@ -348,6 +352,11 @@ class Rs274xContext(GerberContext): hash += str(primitive.height * 1000000)[0:2] elif isinstance(primitive, Circle): hash += str(primitive.diameter * 1000000)[0:2] + + if len(hash) > 20: + # The hash might actually get quite complex, so stop before + # it gets too long + break return hash @@ -361,7 +370,7 @@ class Rs274xContext(GerberContext): if macroinfo: - # We hae a definition, but check that the groups actually are the same + # We have a definition, but check that the groups actually are the same for macro in macroinfo: # Macros should have positions, right? But if the macro is selected for non-flashes -- cgit From b01c4822b6da6b7be37becb73c58f60621f6366f Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 25 Jun 2016 12:27:28 +0800 Subject: Render aperture macros with clear regions --- gerber/render/cairo_backend.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 1d168ca..c1bd60c 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -191,8 +191,11 @@ class GerberCairoContext(GerberContext): self.ctx.stroke() def _render_amgroup(self, amgroup, color): + self.ctx.push_group() for primitive in amgroup.primitives: self.render(primitive) + self.ctx.pop_group_to_source() + self.ctx.paint_with_alpha(1) def _render_test_record(self, primitive, color): self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) -- cgit From ccb6eb7a766bd6edf314978f3ec4fc0dcd61652d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 25 Jun 2016 16:46:44 +0800 Subject: Add support for polygon apertures --- gerber/render/cairo_backend.py | 15 +++++++++++++++ gerber/render/rs274x_backend.py | 26 ++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index c1bd60c..2b7c2ff 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -159,6 +159,9 @@ class GerberCairoContext(GerberContext): self._render_rectangle(obround.subshapes['rectangle'], color) def _render_polygon(self, polygon, color): + if polygon.hole_radius > 0: + self.ctx.push_group() + vertices = polygon.vertices self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) @@ -172,6 +175,18 @@ class GerberCairoContext(GerberContext): self.ctx.line_to(*map(mul, v, self.scale)) self.ctx.fill() + + if polygon.hole_radius > 0: + # Render the center clear + center = tuple(map(mul, polygon.position, self.scale)) + self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi) + self.ctx.fill() + + self.ctx.pop_group_to_source() + self.ctx.paint_with_alpha(1) def _render_drill(self, circle, color): self._render_circle(circle, color) diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index bb784b1..c37529e 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -310,7 +310,7 @@ class Rs274xContext(GerberContext): self._next_dcode = max(dcode + 1, self._next_dcode) aper = ADParamStmt.obround(dcode, width, height) - self._obrounds[(width, height)] = aper + self._obrounds[key] = aper self.header.append(aper) return aper @@ -320,10 +320,28 @@ class Rs274xContext(GerberContext): aper = self._get_obround(obround.width, obround.height) self._render_flash(obround, aper) - pass - def _render_polygon(self, polygon, color): - raise ValueError('Polygons can only exist in the context of aperture macro') + + aper = self._get_polygon(polygon.radius, polygon.sides, polygon.rotation, polygon.hole_radius) + self._render_flash(polygon, aper) + + def _get_polygon(self, radius, num_vertices, rotation, hole_radius, dcode = None): + + key = (radius, num_vertices, rotation, hole_radius) + aper = self._polygons.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, rotation, hole_radius * 2) + self._polygons[key] = aper + self.header.append(aper) + + return aper def _render_drill(self, drill, color): raise ValueError('Drills are not valid in RS274X files') -- cgit From 9b0d3b1122ffc3b7c2211b0cdc2cb6de6be9b242 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 10 Jul 2016 15:07:17 +0800 Subject: Fix issue with chaning region mode via flash. Add options for controlling output from rendered gerber --- gerber/render/render.py | 21 +++++++++++++++++++-- gerber/render/rs274x_backend.py | 27 ++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/render.py b/gerber/render/render.py index 41b632c..128496f 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -136,6 +136,9 @@ class GerberContext(object): return color = (self.color if primitive.level_polarity == 'dark' else self.background_color) + + self._pre_render_primitive(primitive) + if isinstance(primitive, Line): self._render_line(primitive, color) elif isinstance(primitive, Arc): @@ -160,8 +163,22 @@ class GerberContext(object): self._render_region(primitive, color) elif isinstance(primitive, TestRecord): self._render_test_record(primitive, color) - else: - return + + self._post_render_primitive(primitive) + + def _pre_render_primitive(self, primitive): + """ + Called before rendering a primitive. Use the callback to perform some action before rendering + a primitive, for example adding a comment. + """ + return + + def _post_render_primitive(self, primitive): + """ + Called after rendering a primitive. Use the callback to perform some action after rendering + a primitive + """ + return def _render_line(self, primitive, color): pass diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index c37529e..972edcb 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -90,6 +90,17 @@ class Rs274xContext(GerberContext): self._quadrant_mode = None self._dcode = None + # Primarily for testing and comarison to files, should we write + # flashes as a single statement or a move plus flash? Set to true + # to do in a single statement. Normally this can be false + self.condensed_flash = True + + # When closing a region, force a D02 staement to close a region. + # This is normally not necessary because regions are closed with a G37 + # staement, but this will add an extra statement for doubly close + # the region + self.explicit_region_move_end = False + self._next_dcode = 10 self._rects = {} self._circles = {} @@ -153,6 +164,11 @@ class Rs274xContext(GerberContext): if aper.d != self._dcode: self.body.append(ApertureStmt(aper.d)) self._dcode = aper.d + + def _pre_render_primitive(self, primitive): + + if hasattr(primitive, 'comment'): + self.body.append(CommentStmt(primitive.comment)) def _render_line(self, line, color): @@ -233,6 +249,8 @@ class Rs274xContext(GerberContext): else: self._render_arc(p, color) + if self.explicit_region_move_end: + self.body.append(CoordStmt.move(None, None)) self.body.append(RegionModeStmt.off()) @@ -243,11 +261,18 @@ class Rs274xContext(GerberContext): def _render_flash(self, primitive, aperture): + self._render_level_polarity(primitive) + if aperture.d != self._dcode: self.body.append(ApertureStmt(aperture.d)) self._dcode = aperture.d - self.body.append(CoordStmt.flash( self._simplify_point(primitive.position))) + if self.condensed_flash: + self.body.append(CoordStmt.flash(self._simplify_point(primitive.position))) + else: + self.body.append(CoordStmt.move(None, self._simplify_point(primitive.position))) + self.body.append(CoordStmt.flash(None)) + self._pos = primitive.position def _get_circle(self, diameter, dcode = None): -- cgit From f0585baefa54c5cd891ba04c81053956b1a59977 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 17 Jul 2016 13:14:54 +0800 Subject: Create first test that renders and validates the the rendered PNG is correct. --- gerber/render/cairo_backend.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 5a3c7a1..3c4a395 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -292,8 +292,7 @@ class GerberCairoContext(GerberContext): self.ctx.paint() def dump(self, filename): - is_svg = filename.lower().endswith(".svg") - if is_svg: + if filename and filename.lower().endswith(".svg"): self.surface.finish() self.surface_buffer.flush() with open(filename, "w") as f: @@ -301,7 +300,7 @@ class GerberCairoContext(GerberContext): f.write(self.surface_buffer.read()) f.flush() else: - self.surface.write_to_png(filename) + return self.surface.write_to_png(filename) def dump_svg_str(self): self.surface.finish() -- cgit From 7cd6acf12670f3773113f67ed2acb35cb21c32a0 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 24 Jul 2016 17:08:47 +0800 Subject: Add many render tests based on the Umaco gerger specification. Fix multiple rendering bugs, especially related to holes in flashed apertures --- gerber/render/cairo_backend.py | 105 ++++++++++++++++++++++++++++++++-------- gerber/render/rs274x_backend.py | 12 ++--- 2 files changed, 90 insertions(+), 27 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 0cb230b..78ccf34 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -20,13 +20,14 @@ try: except ImportError: import cairocffi as cairo -from operator import mul, div import math +from operator import mul, div import tempfile +from ..primitives import * from .render import GerberContext, RenderSettings from .theme import THEMES -from ..primitives import * + try: from cStringIO import StringIO @@ -219,15 +220,30 @@ class GerberCairoContext(GerberContext): center = tuple(map(mul, circle.position, self.scale)) if not self.invert: ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) + ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) ctx.set_operator(cairo.OPERATOR_OVER if circle.level_polarity == "dark" else cairo.OPERATOR_CLEAR) else: ctx = self.mask_ctx ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) ctx.set_operator(cairo.OPERATOR_CLEAR) + + if circle.hole_diameter > 0: + ctx.push_group() + ctx.set_line_width(0) ctx.arc(center[0], center[1], radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) - ctx.fill() + ctx.fill() + + if circle.hole_diameter > 0: + # Render the center clear + + ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + ctx.set_operator(cairo.OPERATOR_CLEAR) + ctx.arc(center[0], center[1], radius=circle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) + ctx.fill() + + ctx.pop_group_to_source() + ctx.paint_with_alpha(1) def _render_rectangle(self, rectangle, color): ll = map(mul, rectangle.lower_left, self.scale) @@ -253,48 +269,95 @@ class GerberCairoContext(GerberContext): ll[1] = ll[1] - center[1] matrix.rotate(rectangle.rotation) ctx.transform(matrix) - + + if rectangle.hole_diameter > 0: + ctx.push_group() + ctx.set_line_width(0) ctx.rectangle(ll[0], ll[1], width, height) ctx.fill() + if rectangle.hole_diameter > 0: + # Render the center clear + ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + ctx.set_operator(cairo.OPERATOR_CLEAR) + center = map(mul, rectangle.position, self.scale) + ctx.arc(center[0], center[1], radius=rectangle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) + ctx.fill() + + ctx.pop_group_to_source() + ctx.paint_with_alpha(1) + if rectangle.rotation != 0: ctx.restore() def _render_obround(self, obround, color): + + if not self.invert: + ctx = self.ctx + ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + ctx.set_operator(cairo.OPERATOR_OVER if obround.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + else: + ctx = self.mask_ctx + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_operator(cairo.OPERATOR_CLEAR) + + if obround.hole_diameter > 0: + ctx.push_group() + self._render_circle(obround.subshapes['circle1'], color) self._render_circle(obround.subshapes['circle2'], color) self._render_rectangle(obround.subshapes['rectangle'], color) + if obround.hole_diameter > 0: + # Render the center clear + ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + ctx.set_operator(cairo.OPERATOR_CLEAR) + center = map(mul, obround.position, self.scale) + ctx.arc(center[0], center[1], radius=obround.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) + ctx.fill() + + ctx.pop_group_to_source() + ctx.paint_with_alpha(1) + def _render_polygon(self, polygon, color): + + # TODO Ths does not handle rotation of a polygon + if not self.invert: + ctx = self.ctx + ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + ctx.set_operator(cairo.OPERATOR_OVER if polygon.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + else: + ctx = self.mask_ctx + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_operator(cairo.OPERATOR_CLEAR) + if polygon.hole_radius > 0: - self.ctx.push_group() + ctx.push_group() vertices = polygon.vertices - - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER if (polygon.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + + ctx.set_line_width(0) + ctx.set_line_cap(cairo.LINE_CAP_ROUND) # Start from before the end so it is easy to iterate and make sure it is closed - self.ctx.move_to(*map(mul, vertices[-1], self.scale)) + ctx.move_to(*map(mul, vertices[-1], self.scale)) for v in vertices: - self.ctx.line_to(*map(mul, v, self.scale)) + ctx.line_to(*map(mul, v, self.scale)) - self.ctx.fill() + ctx.fill() if polygon.hole_radius > 0: # Render the center clear center = tuple(map(mul, polygon.position, self.scale)) - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi) - self.ctx.fill() + ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + ctx.set_operator(cairo.OPERATOR_CLEAR) + ctx.set_line_width(0) + ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi) + ctx.fill() - self.ctx.pop_group_to_source() - self.ctx.paint_with_alpha(1) + ctx.pop_group_to_source() + ctx.paint_with_alpha(1) def _render_drill(self, circle, color): self._render_circle(circle, color) diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 972edcb..15e9154 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -151,7 +151,7 @@ class Rs274xContext(GerberContext): # Select the right aperture if not already selected if aperture: if isinstance(aperture, Circle): - aper = self._get_circle(aperture.diameter) + aper = self._get_circle(aperture.diameter, aperture.hole_diameter) elif isinstance(aperture, Rectangle): aper = self._get_rectangle(aperture.width, aperture.height) elif isinstance(aperture, Obround): @@ -275,10 +275,10 @@ class Rs274xContext(GerberContext): self._pos = primitive.position - def _get_circle(self, diameter, dcode = None): + def _get_circle(self, diameter, hole_diameter, dcode = None): '''Define a circlar aperture''' - aper = self._circles.get(diameter, None) + aper = self._circles.get((diameter, hole_diameter), None) if not aper: if not dcode: @@ -287,15 +287,15 @@ class Rs274xContext(GerberContext): else: self._next_dcode = max(dcode + 1, self._next_dcode) - aper = ADParamStmt.circle(dcode, diameter) - self._circles[diameter] = aper + aper = ADParamStmt.circle(dcode, diameter, hole_diameter) + self._circles[(diameter, hole_diameter)] = aper self.header.append(aper) return aper def _render_circle(self, circle, color): - aper = self._get_circle(circle.diameter) + aper = self._get_circle(circle.diameter, circle.hole_diameter) self._render_flash(circle, aper) def _get_rectangle(self, width, height, dcode = None): -- cgit 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/render/rs274x_backend.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'gerber/render') 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 -- cgit From 8cd842a41a55ab3d8f558a2e3e198beba7da58a1 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Manually mere rendering changes --- gerber/render/cairo_backend.py | 395 +++++++++++++++++++++-------------------- gerber/render/render.py | 8 +- gerber/render/theme.py | 4 +- 3 files changed, 214 insertions(+), 193 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 78ccf34..349640a 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -12,7 +12,7 @@ # 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 +# See the License for the specific language governing permissions and # limitations under the License. try: @@ -21,7 +21,8 @@ except ImportError: import cairocffi as cairo import math -from operator import mul, div +from operator import mul, di + import tempfile from ..primitives import * @@ -36,11 +37,14 @@ except(ImportError): class GerberCairoContext(GerberContext): + def __init__(self, scale=300): - GerberContext.__init__(self) + super(GerberCairoContext, self).__init__() self.scale = (scale, scale) self.surface = None self.ctx = None + self.active_layer = None + self.output_ctx = None self.bg = False self.mask = None self.mask_ctx = None @@ -50,37 +54,40 @@ class GerberCairoContext(GerberContext): @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) + return (self.scale_point(self.origin_in_inch) + 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) + return (self.scale_point(self.size_in_inch) + 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]) - size_in_inch = (abs(bounds[0][1] - bounds[0][0]), abs(bounds[1][1] - bounds[1][0])) - size_in_pixels = tuple(map(mul, size_in_inch, self.scale)) + size_in_inch = (abs(bounds[0][1] - bounds[0][0]), + abs(bounds[1][1] - bounds[1][0])) + size_in_pixels = self.scale_point(size_in_inch) 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) - 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.mask = cairo.SVGSurface(None, 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]) - 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]) + self.surface = cairo.SVGSurface( + self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) + self.output_ctx = cairo.Context(self.surface) + self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + self.output_ctx.scale(1, -1) + self.output_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, 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, theme) self.dump(filename) @@ -117,46 +124,46 @@ class GerberCairoContext(GerberContext): self.color = settings.color self.alpha = settings.alpha self.invert = settings.invert + + # Get a new clean layer to render on + self._new_render_layer() if settings.mirror: raise Warning('mirrored layers aren\'t supported yet...') - if self.invert: - self._clear_mask() for prim in layer.primitives: self.render(prim) - if self.invert: - self._render_mask() + # Add layer to image + self._flatten() def _render_line(self, line, color): - start = map(mul, line.start, self.scale) - end = map(mul, line.end, self.scale) + start = [pos * scale for pos, scale in zip(line.start, self.scale)] + end = [pos * scale for pos, scale in zip(line.end, self.scale)] if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if line.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if line.level_polarity == "dark" + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) if isinstance(line.aperture, Circle): - width = line.aperture.diameter - ctx.set_line_width(width * self.scale[0]) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - - ctx.move_to(*start) - ctx.line_to(*end) - ctx.stroke() + 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] - ctx.set_line_width(0) - ctx.move_to(*points[0]) + points = [self.scale_point(x) for x in line.vertices] + self.ctx.set_line_width(0) + self.ctx.move_to(*points[0]) for point in points[1:]: - ctx.line_to(*point) - ctx.fill() + self.ctx.line_to(*point) + self.ctx.fill() def _render_arc(self, arc, color): - center = map(mul, arc.center, self.scale) - start = map(mul, arc.start, self.scale) - end = map(mul, arc.end, self.scale) + center = self.scale_point(arc.center) + start = self.scale_point(arc.start) + end = self.scale_point(arc.end) radius = self.scale[0] * arc.radius angle1 = arc.start_angle angle2 = arc.end_angle @@ -169,141 +176,137 @@ class GerberCairoContext(GerberContext): width = max(arc.aperture.width, arc.aperture.height, 0.001) if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if arc.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if arc.level_polarity == "dark"\ + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(width * self.scale[0]) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.move_to(*start) # You actually have to do this... + 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... if arc.direction == 'counterclockwise': - ctx.arc(center[0], center[1], radius, angle1, angle2) + self.ctx.arc(center[0], center[1], radius, angle1, angle2) else: - ctx.arc_negative(center[0], center[1], radius, angle1, angle2) - ctx.move_to(*end) # ...lame - ctx.stroke() + self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2) + self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if region.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if region.level_polarity == "dark" + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - - ctx.set_line_width(0) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.move_to(*tuple(map(mul, region.primitives[0].start, self.scale))) - for p in region.primitives: - if isinstance(p, Line): - ctx.line_to(*tuple(map(mul, p.end, self.scale))) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + + self.ctx.set_line_width(0) + self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + self.ctx.move_to(*self.scale_point(region.primitives[0].start)) + for prim in region.primitives: + if isinstance(prim, Line): + self.ctx.line_to(*self.scale_point(prim.end)) else: - center = map(mul, p.center, self.scale) - start = map(mul, p.start, self.scale) - end = map(mul, p.end, self.scale) - radius = self.scale[0] * p.radius - angle1 = p.start_angle - angle2 = p.end_angle - if p.direction == 'counterclockwise': - ctx.arc(center[0], center[1], radius, angle1, angle2) + center = self.scale_point(prim.center) + radius = self.scale[0] * prim.radius + angle1 = prim.start_angle + angle2 = prim.end_angle + if prim.direction == 'counterclockwise': + self.ctx.arc(*center, radius=radius, + angle1=angle1, angle2=angle2) else: - ctx.arc_negative(center[0], center[1], radius, angle1, angle2) - ctx.fill() - + self.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + self.ctx.fill() def _render_circle(self, circle, color): - center = tuple(map(mul, circle.position, self.scale)) + center = self.scale_point(circle.position) if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if circle.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if circle.level_polarity == "dark" + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) if circle.hole_diameter > 0: - ctx.push_group() + self.ctx.push_group() - ctx.set_line_width(0) - ctx.arc(center[0], center[1], radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) - ctx.fill() + self.ctx.set_line_width(0) + self.ctx.arc(center[0], center[1], radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) + self.ctx.fill() if circle.hole_diameter > 0: # Render the center clear - ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.arc(center[0], center[1], radius=circle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) - ctx.fill() + self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.arc(center[0], center[1], radius=circle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) + self.ctx.fill() - ctx.pop_group_to_source() - ctx.paint_with_alpha(1) + self.ctx.pop_group_to_source() + self.ctx.paint_with_alpha(1) 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))) + lower_left = self.scale_point(rectangle.lower_left) + width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if rectangle.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if rectangle.level_polarity == "dark" + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) if rectangle.rotation != 0: - ctx.save() + self.ctx.save() center = map(mul, rectangle.position, self.scale) matrix = cairo.Matrix() matrix.translate(center[0], center[1]) # For drawing, we already handles the translation - ll[0] = ll[0] - center[0] - ll[1] = ll[1] - center[1] + lower_left[0] = lower_left[0] - center[0] + lower_left[1] = lower_left[1] - center[1] matrix.rotate(rectangle.rotation) - ctx.transform(matrix) + self.ctx.transform(matrix) if rectangle.hole_diameter > 0: - ctx.push_group() + self.ctx.push_group() - ctx.set_line_width(0) - ctx.rectangle(ll[0], ll[1], width, height) - ctx.fill() + self.ctx.set_line_width(0) + self.ctx.rectangle(lower_left[0], lower_left[1], width, height) + self.ctx.fill() if rectangle.hole_diameter > 0: # Render the center clear - ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) center = map(mul, rectangle.position, self.scale) - ctx.arc(center[0], center[1], radius=rectangle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) - ctx.fill() + self.ctx.arc(center[0], center[1], radius=rectangle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) + self.ctx.fill() - ctx.pop_group_to_source() - ctx.paint_with_alpha(1) + self.ctx.pop_group_to_source() + self.ctx.paint_with_alpha(1) if rectangle.rotation != 0: - ctx.restore() + self.ctx.restore() def _render_obround(self, obround, color): if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if obround.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER if obround.level_polarity == "dark" else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) if obround.hole_diameter > 0: - ctx.push_group() + self.ctx.push_group() self._render_circle(obround.subshapes['circle1'], color) self._render_circle(obround.subshapes['circle2'], color) @@ -311,55 +314,54 @@ class GerberCairoContext(GerberContext): if obround.hole_diameter > 0: # Render the center clear - ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) center = map(mul, obround.position, self.scale) - ctx.arc(center[0], center[1], radius=obround.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) - ctx.fill() + self.ctx.arc(center[0], center[1], radius=obround.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) + self.ctx.fill() - ctx.pop_group_to_source() - ctx.paint_with_alpha(1) + self.ctx.pop_group_to_source() + self.ctx.paint_with_alpha(1) def _render_polygon(self, polygon, color): # TODO Ths does not handle rotation of a polygon if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if polygon.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER if polygon.level_polarity == "dark" else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) if polygon.hole_radius > 0: - ctx.push_group() + self.ctx.push_group() vertices = polygon.vertices - ctx.set_line_width(0) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) + self.ctx.set_line_width(0) + self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) # Start from before the end so it is easy to iterate and make sure it is closed - ctx.move_to(*map(mul, vertices[-1], self.scale)) + self.ctx.move_to(*map(mul, vertices[-1], self.scale)) for v in vertices: - ctx.line_to(*map(mul, v, self.scale)) + self.ctx.line_to(*map(mul, v, self.scale)) - ctx.fill() + self.ctx.fill() if polygon.hole_radius > 0: # Render the center clear center = tuple(map(mul, polygon.position, self.scale)) - ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(0) - ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi) - ctx.fill() + self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi) + self.ctx.fill() - ctx.pop_group_to_source() - ctx.paint_with_alpha(1) + self.ctx.pop_group_to_source() + self.ctx.paint_with_alpha(1) - def _render_drill(self, circle, color): + def _render_drill(self, circle, color=None): + color = color if color is not None else self.drill_color self._render_circle(circle, color) def _render_slot(self, slot, color): @@ -369,19 +371,17 @@ class GerberCairoContext(GerberContext): width = slot.diameter if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if slot.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER if slot.level_polarity == "dark" else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - - ctx.set_line_width(width * self.scale[0]) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.move_to(*start) - ctx.line_to(*end) - ctx.stroke() + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(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() def _render_amgroup(self, amgroup, color): self.ctx.push_group() @@ -391,33 +391,52 @@ class GerberCairoContext(GerberContext): self.ctx.paint_with_alpha(1) def _render_test_record(self, primitive, color): - position = tuple(map(add, primitive.position, self.origin_in_inch)) + position = [pos + origin for pos, origin in zip(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.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_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.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) self.ctx.show_text(primitive.net_name) - self.ctx.scale(1, -1) - - def _clear_mask(self): - self.mask_ctx.set_operator(cairo.OPERATOR_OVER) - self.mask_ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=self.alpha) - self.mask_ctx.paint() - - def _render_mask(self): - self.ctx.set_operator(cairo.OPERATOR_OVER) - ptn = cairo.SurfacePattern(self.mask) + self.ctx.scale(1, -1) + + def _new_render_layer(self, color=None): + size_in_pixels = self.scale_point(self.size_in_inch) + layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) + ctx = cairo.Context(layer) + ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + ctx.scale(1, -1) + ctx.translate(-(self.origin_in_inch[0] * self.scale[0]), + (-self.origin_in_inch[1] * self.scale[0]) + - size_in_pixels[1]) + if self.invert: + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.set_source_rgba(*self.color, alpha=self.alpha) + ctx.paint() + self.ctx = ctx + self.active_layer = layer + + def _flatten(self): + self.output_ctx.set_operator(cairo.OPERATOR_OVER) + ptn = cairo.SurfacePattern(self.active_layer) ptn.set_matrix(self._xform_matrix) - self.ctx.set_source(ptn) - self.ctx.paint() + self.output_ctx.set_source(ptn) + self.output_ctx.paint() + self.ctx = None + self.active_layer = None def _paint_background(self, force=False): - if (not self.bg) or force: - self.bg = True - self.ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=1.0) - self.ctx.paint() - + if (not self.bg) or force: + self.bg = True + self.output_ctx.set_operator(cairo.OPERATOR_OVER) + self.output_ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=1.0) + self.output_ctx.paint() + + def scale_point(self, point): + return tuple([coord * scale for coord, scale in zip(point, self.scale)]) \ No newline at end of file diff --git a/gerber/render/render.py b/gerber/render/render.py index f521c44..7bd4c00 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -57,12 +57,14 @@ class GerberContext(object): alpha : float Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ + def __init__(self, units='inch'): self._units = units self._color = (0.7215, 0.451, 0.200) self._background_color = (0.0, 0.0, 0.0) self._alpha = 1.0 self._invert = False + self.ctx = None @property def units(self): @@ -134,11 +136,10 @@ class GerberContext(object): def render(self, primitive): if not primitive: return - color = (self.color if primitive.level_polarity == 'dark' - else self.background_color) self._pre_render_primitive(primitive) + color = self.color if isinstance(primitive, Line): self._render_line(primitive, color) elif isinstance(primitive, Arc): @@ -180,6 +181,7 @@ class GerberContext(object): """ return + def _render_line(self, primitive, color): pass @@ -215,9 +217,9 @@ class GerberContext(object): 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 e538df8..6135ccb 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -23,7 +23,7 @@ COLORS = { '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), + '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), @@ -36,6 +36,7 @@ COLORS = { class Theme(object): + 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)) @@ -67,4 +68,3 @@ THEMES = { topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True), bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)), } - -- cgit From 8d5e782ccf220d77f0aad5a4e5605dc5cbe0f410 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 6 Aug 2016 09:51:58 +0800 Subject: Fix multiple problems with the merge. There are still errors, but I will intentionally leave them because future merges might resolve them --- gerber/render/cairo_backend.py | 2 +- gerber/render/rs274x_backend.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 349640a..dc39607 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -21,7 +21,7 @@ except ImportError: import cairocffi as cairo import math -from operator import mul, di +from operator import mul, div import tempfile diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 5ab74f0..b4b4612 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -476,6 +476,14 @@ class Rs274xContext(GerberContext): def _render_inverted_layer(self): pass + def _new_render_layer(self): + # TODO Might need to implement this + pass + + def _flatten(self): + # TODO Might need to implement this + pass + def dump(self): """Write the rendered file to a StringIO steam""" statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements) -- cgit From 5af19af190c1fb0f0c5be029d46d63e657dde4d9 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Commit partial merge so I can work on the plane --- gerber/render/cairo_backend.py | 92 ++++++++++++++++++++++++++++++++++++++++-- gerber/render/render.py | 1 + 2 files changed, 90 insertions(+), 3 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index dc39607..8c7232f 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -12,6 +12,7 @@ # 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. @@ -22,14 +23,14 @@ except ImportError: import math from operator import mul, div - import tempfile +import cairocffi as cairo + from ..primitives import * from .render import GerberContext, RenderSettings from .theme import THEMES - try: from cStringIO import StringIO except(ImportError): @@ -138,20 +139,35 @@ class GerberCairoContext(GerberContext): start = [pos * scale for pos, scale in zip(line.start, self.scale)] end = [pos * scale for pos, scale in zip(line.end, self.scale)] if not self.invert: +<<<<<<< HEAD self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if line.level_polarity == "dark" else cairo.OPERATOR_CLEAR) +======= + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if line.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) if isinstance(line.aperture, Circle): +<<<<<<< HEAD width = line.aperture.diameter +======= + width = line.aperture.diameter +>>>>>>> 5476da8... Fix a bunch of rendering bugs. 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) +<<<<<<< HEAD self.ctx.stroke() +======= + self.ctx.stroke() +>>>>>>> 5476da8... Fix a bunch of rendering bugs. elif isinstance(line.aperture, Rectangle): points = [self.scale_point(x) for x in line.vertices] self.ctx.set_line_width(0) @@ -176,6 +192,7 @@ class GerberCairoContext(GerberContext): width = max(arc.aperture.width, arc.aperture.height, 0.001) if not self.invert: +<<<<<<< HEAD self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if arc.level_polarity == "dark"\ @@ -184,25 +201,50 @@ class GerberCairoContext(GerberContext): self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) +======= + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if arc.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + else: + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. 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... if arc.direction == 'counterclockwise': +<<<<<<< HEAD self.ctx.arc(center[0], center[1], radius, angle1, angle2) else: self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2) +======= + self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + else: + self.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): if not self.invert: +<<<<<<< HEAD self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if region.level_polarity == "dark" +======= + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if region.level_polarity == 'dark' +>>>>>>> 5476da8... Fix a bunch of rendering bugs. else cairo.OPERATOR_CLEAR) else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) +<<<<<<< HEAD +======= +>>>>>>> 5476da8... Fix a bunch of rendering bugs. self.ctx.set_line_width(0) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*self.scale_point(region.primitives[0].start)) @@ -220,6 +262,7 @@ class GerberCairoContext(GerberContext): else: self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) +<<<<<<< HEAD self.ctx.fill() def _render_circle(self, circle, color): center = self.scale_point(circle.position) @@ -249,10 +292,28 @@ class GerberCairoContext(GerberContext): self.ctx.pop_group_to_source() self.ctx.paint_with_alpha(1) +======= + self.ctx.fill() + + def _render_circle(self, circle, color): + center = self.scale_point(circle.position) + 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' else cairo.OPERATOR_CLEAR) + else: + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(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() +>>>>>>> 5476da8... Fix a bunch of rendering bugs. def _render_rectangle(self, rectangle, color): lower_left = self.scale_point(rectangle.lower_left) width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) +<<<<<<< HEAD if not self.invert: self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) @@ -295,6 +356,19 @@ class GerberCairoContext(GerberContext): if rectangle.rotation != 0: self.ctx.restore() +======= + + 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' else cairo.OPERATOR_CLEAR) + else: + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.rectangle(*lower_left, width=width, height=height) + self.ctx.fill() +>>>>>>> 5476da8... Fix a bunch of rendering bugs. def _render_obround(self, obround, color): @@ -424,7 +498,11 @@ class GerberCairoContext(GerberContext): def _flatten(self): self.output_ctx.set_operator(cairo.OPERATOR_OVER) +<<<<<<< HEAD ptn = cairo.SurfacePattern(self.active_layer) +======= + ptn = cairo.SurfacePattern(self.active_layer) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. ptn.set_matrix(self._xform_matrix) self.output_ctx.set_source(ptn) self.output_ctx.paint() @@ -435,8 +513,16 @@ class GerberCairoContext(GerberContext): if (not self.bg) or force: self.bg = True self.output_ctx.set_operator(cairo.OPERATOR_OVER) +<<<<<<< HEAD self.output_ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=1.0) self.output_ctx.paint() def scale_point(self, point): - return tuple([coord * scale for coord, scale in zip(point, self.scale)]) \ No newline at end of file + return tuple([coord * scale for coord, scale in zip(point, self.scale)]) +======= + self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0) + self.output_ctx.paint() + + def scale_point(self, point): + return tuple([coord * scale for coord, scale in zip(point, self.scale)]) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. diff --git a/gerber/render/render.py b/gerber/render/render.py index 7bd4c00..b319648 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -182,6 +182,7 @@ class GerberContext(object): return + def _render_line(self, primitive, color): pass -- cgit From 0fedaedb6ebb8cc6abfc218d224a3ab69bb71b56 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 29 Sep 2016 19:43:28 -0400 Subject: Add more layer hints as seen in the wild --- gerber/render/theme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/theme.py b/gerber/render/theme.py index d382a8d..2887216 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -53,7 +53,7 @@ class Theme(object): return getattr(self, key) def get(self, key, noneval=None): - val = getattr(self, key) + val = getattr(self, key, None) return val if val is not None else noneval -- cgit From 724c2b3bced319ed0b50c4302fed9b0e1aa9ce9c Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 5 Nov 2016 20:56:47 -0400 Subject: Finish Merge, most tests passing --- gerber/render/cairo_backend.py | 167 +++++++++++------------------------------ 1 file changed, 43 insertions(+), 124 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 8c7232f..77d413e 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -12,15 +12,15 @@ # 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. - + +# See the License for the specific language governing permissions and +# limitations under the License. + try: import cairo except ImportError: import cairocffi as cairo - + import math from operator import mul, div import tempfile @@ -139,35 +139,20 @@ class GerberCairoContext(GerberContext): start = [pos * scale for pos, scale in zip(line.start, self.scale)] end = [pos * scale for pos, scale in zip(line.end, self.scale)] if not self.invert: -<<<<<<< HEAD self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if line.level_polarity == "dark" - else cairo.OPERATOR_CLEAR) -======= - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER - if line.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) ->>>>>>> 5476da8... Fix a bunch of rendering bugs. + else cairo.OPERATOR_CLEAR) else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) if isinstance(line.aperture, Circle): -<<<<<<< HEAD - width = line.aperture.diameter -======= width = line.aperture.diameter ->>>>>>> 5476da8... Fix a bunch of rendering bugs. 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) -<<<<<<< HEAD - self.ctx.stroke() -======= self.ctx.stroke() ->>>>>>> 5476da8... Fix a bunch of rendering bugs. elif isinstance(line.aperture, Rectangle): points = [self.scale_point(x) for x in line.vertices] self.ctx.set_line_width(0) @@ -190,9 +175,8 @@ class GerberCairoContext(GerberContext): width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 else: width = max(arc.aperture.width, arc.aperture.height, 0.001) - + if not self.invert: -<<<<<<< HEAD self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if arc.level_polarity == "dark"\ @@ -200,51 +184,26 @@ class GerberCairoContext(GerberContext): else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) - -======= - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER - if arc.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) ->>>>>>> 5476da8... Fix a bunch of rendering bugs. + 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... if arc.direction == 'counterclockwise': -<<<<<<< HEAD self.ctx.arc(center[0], center[1], radius, angle1, angle2) else: self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2) -======= - self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) - else: - self.ctx.arc_negative(*center, radius=radius, - angle1=angle1, angle2=angle2) ->>>>>>> 5476da8... Fix a bunch of rendering bugs. self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): if not self.invert: -<<<<<<< HEAD self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if region.level_polarity == "dark" -======= - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER - if region.level_polarity == 'dark' ->>>>>>> 5476da8... Fix a bunch of rendering bugs. else cairo.OPERATOR_CLEAR) else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) -<<<<<<< HEAD -======= ->>>>>>> 5476da8... Fix a bunch of rendering bugs. self.ctx.set_line_width(0) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*self.scale_point(region.primitives[0].start)) @@ -262,8 +221,9 @@ class GerberCairoContext(GerberContext): else: self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) -<<<<<<< HEAD - self.ctx.fill() + + self.ctx.fill() + def _render_circle(self, circle, color): center = self.scale_point(circle.position) if not self.invert: @@ -274,47 +234,30 @@ class GerberCairoContext(GerberContext): else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) - + if circle.hole_diameter > 0: self.ctx.push_group() self.ctx.set_line_width(0) self.ctx.arc(center[0], center[1], radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) self.ctx.fill() - + if circle.hole_diameter > 0: # Render the center clear self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) self.ctx.arc(center[0], center[1], radius=circle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) self.ctx.fill() - - self.ctx.pop_group_to_source() - self.ctx.paint_with_alpha(1) -======= - self.ctx.fill() - def _render_circle(self, circle, color): - center = self.scale_point(circle.position) - 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' else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(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() ->>>>>>> 5476da8... Fix a bunch of rendering bugs. + self.ctx.pop_group_to_source() + self.ctx.paint_with_alpha(1) def _render_rectangle(self, rectangle, color): lower_left = self.scale_point(rectangle.lower_left) width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) -<<<<<<< HEAD - + + if not self.invert: self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER @@ -323,10 +266,10 @@ class GerberCairoContext(GerberContext): else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) - + if rectangle.rotation != 0: self.ctx.save() - + center = map(mul, rectangle.position, self.scale) matrix = cairo.Matrix() matrix.translate(center[0], center[1]) @@ -335,14 +278,14 @@ class GerberCairoContext(GerberContext): lower_left[1] = lower_left[1] - center[1] matrix.rotate(rectangle.rotation) self.ctx.transform(matrix) - + if rectangle.hole_diameter > 0: self.ctx.push_group() self.ctx.set_line_width(0) self.ctx.rectangle(lower_left[0], lower_left[1], width, height) self.ctx.fill() - + if rectangle.hole_diameter > 0: # Render the center clear self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) @@ -350,42 +293,30 @@ class GerberCairoContext(GerberContext): center = map(mul, rectangle.position, self.scale) self.ctx.arc(center[0], center[1], radius=rectangle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) self.ctx.fill() - + self.ctx.pop_group_to_source() self.ctx.paint_with_alpha(1) - + if rectangle.rotation != 0: - self.ctx.restore() -======= + self.ctx.restore() - 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' else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.rectangle(*lower_left, width=width, height=height) - self.ctx.fill() ->>>>>>> 5476da8... Fix a bunch of rendering bugs. def _render_obround(self, obround, color): - + if not self.invert: self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if obround.level_polarity == "dark" else cairo.OPERATOR_CLEAR) else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) - + if obround.hole_diameter > 0: self.ctx.push_group() self._render_circle(obround.subshapes['circle1'], color) self._render_circle(obround.subshapes['circle2'], color) self._render_rectangle(obround.subshapes['rectangle'], color) - + if obround.hole_diameter > 0: # Render the center clear self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) @@ -393,12 +324,12 @@ class GerberCairoContext(GerberContext): center = map(mul, obround.position, self.scale) self.ctx.arc(center[0], center[1], radius=obround.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi) self.ctx.fill() - + self.ctx.pop_group_to_source() self.ctx.paint_with_alpha(1) def _render_polygon(self, polygon, color): - + # TODO Ths does not handle rotation of a polygon if not self.invert: self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) @@ -406,44 +337,44 @@ class GerberCairoContext(GerberContext): else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) - + if polygon.hole_radius > 0: self.ctx.push_group() - - vertices = polygon.vertices + + vertices = polygon.vertices self.ctx.set_line_width(0) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - + # Start from before the end so it is easy to iterate and make sure it is closed self.ctx.move_to(*map(mul, vertices[-1], self.scale)) for v in vertices: self.ctx.line_to(*map(mul, v, self.scale)) self.ctx.fill() - + if polygon.hole_radius > 0: # Render the center clear center = tuple(map(mul, polygon.position, self.scale)) self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) self.ctx.set_line_width(0) self.ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi) self.ctx.fill() - + self.ctx.pop_group_to_source() self.ctx.paint_with_alpha(1) def _render_drill(self, circle, color=None): color = color if color is not None else self.drill_color self._render_circle(circle, color) - + def _render_slot(self, slot, color): start = map(mul, slot.start, self.scale) end = map(mul, slot.end, self.scale) - + width = slot.diameter - + if not self.invert: self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if slot.level_polarity == "dark" else cairo.OPERATOR_CLEAR) @@ -456,7 +387,7 @@ class GerberCairoContext(GerberContext): self.ctx.move_to(*start) self.ctx.line_to(*end) self.ctx.stroke() - + def _render_amgroup(self, amgroup, color): self.ctx.push_group() for primitive in amgroup.primitives: @@ -478,7 +409,7 @@ class GerberCairoContext(GerberContext): for coord in position]) self.ctx.scale(1, -1) self.ctx.show_text(primitive.net_name) - self.ctx.scale(1, -1) + self.ctx.scale(1, -1) def _new_render_layer(self, color=None): size_in_pixels = self.scale_point(self.size_in_inch) @@ -498,11 +429,7 @@ class GerberCairoContext(GerberContext): def _flatten(self): self.output_ctx.set_operator(cairo.OPERATOR_OVER) -<<<<<<< HEAD - ptn = cairo.SurfacePattern(self.active_layer) -======= ptn = cairo.SurfacePattern(self.active_layer) ->>>>>>> 5476da8... Fix a bunch of rendering bugs. ptn.set_matrix(self._xform_matrix) self.output_ctx.set_source(ptn) self.output_ctx.paint() @@ -510,19 +437,11 @@ class GerberCairoContext(GerberContext): self.active_layer = None def _paint_background(self, force=False): - if (not self.bg) or force: + if (not self.bg) or force: self.bg = True self.output_ctx.set_operator(cairo.OPERATOR_OVER) -<<<<<<< HEAD self.output_ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=1.0) self.output_ctx.paint() def scale_point(self, point): return tuple([coord * scale for coord, scale in zip(point, self.scale)]) -======= - self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0) - self.output_ctx.paint() - - def scale_point(self, point): - return tuple([coord * scale for coord, scale in zip(point, self.scale)]) ->>>>>>> 5476da8... Fix a bunch of rendering bugs. -- cgit From d7a0f3ad2b62402f4af9abe2729f9a7c14814fe6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 6 Nov 2016 14:58:32 -0500 Subject: Remove debug print" --- gerber/render/cairo_backend.py | 1 - 1 file changed, 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 2ba022f..810351b 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -131,7 +131,6 @@ class GerberCairoContext(GerberContext): f.write(self.surface_buffer.read()) f.flush() else: - print("Wriitng To Png: filename: {}".format(filename)) return self.surface.write_to_png(filename) def dump_str(self): -- cgit From 6db0658e2336d6405fe1a6acd10dfab39ba8e7ff Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 6 Nov 2016 15:08:00 -0500 Subject: Fix tests on python3 --- gerber/render/cairo_backend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 810351b..6e95446 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -21,8 +21,7 @@ try: except ImportError: import cairocffi as cairo -import math -from operator import mul, div +from operator import mul import tempfile import copy import os -- cgit From 369ac7b2a33b0de2e95eb2f0ec38d543ad7ca98d Mon Sep 17 00:00:00 2001 From: Girts Folkmanis Date: Mon, 7 Nov 2016 17:11:07 -0800 Subject: cairo_backend.py: use BytesIO instead of StringIO This fixes a crash in cairocffi on Python3, and should be compatible with both python2 and python3. In python2, byte strings are just strings. In python3, when getting binary data, the user probably wants a byte string instead of a regular string. --- gerber/render/cairo_backend.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index df4fcf1..d7026b8 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -25,10 +25,7 @@ from .render import GerberContext, RenderSettings from .theme import THEMES from ..primitives import * -try: - from cStringIO import StringIO -except(ImportError): - from io import StringIO +from io import BytesIO class GerberCairoContext(GerberContext): @@ -125,9 +122,9 @@ class GerberCairoContext(GerberContext): self.surface.write_to_png(filename) def dump_str(self): - """ Return a string containing the rendered image. + """ Return a byte-string containing the rendered image. """ - fobj = StringIO() + fobj = BytesIO() self.surface.write_to_png(fobj) return fobj.getvalue() -- cgit From 5696fc7064af674d02cf84cf7934c1ac7446259e Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:09:03 -0500 Subject: Fix a bunch of bugs in rendering that showed up when rendering the gerbv test suite --- gerber/render/cairo_backend.py | 488 +++++++++++++++++++++++++---------------- 1 file changed, 305 insertions(+), 183 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 31a1e77..a2baa47 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright 2014 Hamilton Kibbe @@ -29,6 +29,7 @@ import os from .render import GerberContext, RenderSettings from .theme import THEMES from ..primitives import * +from ..utils import rotate_point from io import BytesIO @@ -67,16 +68,13 @@ class GerberCairoContext(GerberContext): size_in_pixels = self.scale_point(size_in_inch) 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 + self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, + x0=-self.origin_in_pixels[0], + y0=self.size_in_pixels[1]) 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.output_ctx = cairo.Context(self.surface) - self.output_ctx.scale(1, -1) - self.output_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_layer(self, layer, filename=None, settings=None, bgsettings=None, verbose=False): @@ -155,6 +153,23 @@ class GerberCairoContext(GerberContext): self.surface_buffer.close() self.surface_buffer = None + def _new_mask(self): + class Mask: + def __enter__(msk): + size_in_pixels = self.size_in_pixels + msk.surface = cairo.SVGSurface(None, size_in_pixels[0], + size_in_pixels[1]) + msk.ctx = cairo.Context(msk.surface) + msk.ctx.translate(-self.origin_in_pixels[0], -self.origin_in_pixels[1]) + return msk + + + def __exit__(msk, exc_type, exc_val, traceback): + if hasattr(msk.surface, 'finish'): + msk.surface.finish() + + return Mask() + def _render_layer(self, layer, settings): self.invert = settings.invert # Get a new clean layer to render on @@ -167,31 +182,36 @@ class GerberCairoContext(GerberContext): def _render_line(self, line, color): start = [pos * scale for pos, scale in zip(line.start, self.scale)] end = [pos * scale for pos, scale in zip(line.end, self.scale)] - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if line.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - 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 = [self.scale_point(x) 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() + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and line.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + if isinstance(line.aperture, Circle): + width = line.aperture.diameter + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*start) + mask.ctx.line_to(*end) + mask.ctx.stroke() + + elif hasattr(line, 'vertices') and line.vertices is not None: + points = [self.scale_point(x) for x in line.vertices] + mask.ctx.set_line_width(0) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_arc(self, arc, color): center = self.scale_point(arc.center) start = self.scale_point(arc.start) end = self.scale_point(arc.end) radius = self.scale[0] * arc.radius - angle1 = arc.start_angle - angle2 = arc.end_angle + two_pi = 2 * math.pi + angle1 = (arc.start_angle + two_pi) % two_pi + angle2 = (arc.end_angle + two_pi) % two_pi if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant': # Make the angles slightly different otherwise Cario will draw nothing angle2 -= 0.000000001 @@ -200,61 +220,111 @@ class GerberCairoContext(GerberContext): else: width = max(arc.aperture.width, arc.aperture.height, 0.001) - self.ctx.set_operator(cairo.OPERATOR_SOURCE - 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... - if arc.direction == 'counterclockwise': - self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) - else: - self.ctx.arc_negative(*center, radius=radius, - angle1=angle1, angle2=angle2) - self.ctx.move_to(*end) # ...lame + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and arc.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + + with self._new_mask() as mask: + mask.ctx.set_line_width(width * self.scale[0]) + 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) + else: + mask.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + mask.ctx.move_to(*end) # ...lame + mask.ctx.stroke() + + #if isinstance(arc.aperture, Rectangle): + # print("Flash Rectangle Ends") + # print(arc.aperture.rotation * 180/math.pi) + # rect = arc.aperture + # width = self.scale[0] * rect.width + # height = self.scale[1] * rect.height + # for point, angle in zip((start, end), (angle1, angle2)): + # print("{} w {} h{}".format(point, rect.width, rect.height)) + # mask.ctx.rectangle(point[0] - width/2.0, + # point[1] - height/2.0, width, height) + # mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + def _render_region(self, region, color): - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if region.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - self.ctx.move_to(*self.scale_point(region.primitives[0].start)) - for prim in region.primitives: - if isinstance(prim, Line): - self.ctx.line_to(*self.scale_point(prim.end)) - else: - center = self.scale_point(prim.center) - radius = self.scale[0] * prim.radius - angle1 = prim.start_angle - angle2 = prim.end_angle - if prim.direction == 'counterclockwise': - self.ctx.arc(*center, radius=radius, - angle1=angle1, angle2=angle2) + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) and region.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*self.scale_point(region.primitives[0].start)) + for prim in region.primitives: + if isinstance(prim, Line): + mask.ctx.line_to(*self.scale_point(prim.end)) else: - self.ctx.arc_negative(*center, radius=radius, - angle1=angle1, angle2=angle2) - self.ctx.fill() + center = self.scale_point(prim.center) + radius = self.scale[0] * prim.radius + angle1 = prim.start_angle + angle2 = prim.end_angle + if prim.direction == 'counterclockwise': + mask.ctx.arc(*center, radius=radius, + angle1=angle1, angle2=angle2) + else: + mask.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_circle(self, circle, color): center = self.scale_point(circle.position) - self.ctx.set_operator(cairo.OPERATOR_SOURCE - 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 circle.hole_diameter > 0: - # Render the center clear - - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) - self.ctx.arc(center[0], center[1], - radius=circle.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) - self.ctx.fill() + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and circle.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + mask.ctx.arc(center[0], + center[1], + radius=(circle.radius * self.scale[0]), + angle1=0, + angle2=(2 * math.pi)) + mask.ctx.fill() + + if hasattr(circle, 'hole_diameter') and circle.hole_diameter > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR) + mask.ctx.arc(center[0], + center[1], + radius=circle.hole_radius * self.scale[0], + angle1=0, + angle2=2 * math.pi) + mask.ctx.fill() + + if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height') + and circle.hole_width > 0 and circle.hole_height > 0): + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if circle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((circle.hole_width, circle.hole_height)) + lower_left = rotate_point( + (center[0] - width / 2.0, center[1] - height / 2.0), + circle.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + circle.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + circle.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + circle.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_rectangle(self, rectangle, color): @@ -262,101 +332,156 @@ class GerberCairoContext(GerberContext): width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if rectangle.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - - if rectangle.rotation != 0: - self.ctx.save() - - center = map(mul, rectangle.position, self.scale) - matrix = cairo.Matrix() - matrix.translate(center[0], center[1]) - # For drawing, we already handles the translation - lower_left[0] = lower_left[0] - center[0] - lower_left[1] = lower_left[1] - center[1] - matrix.rotate(rectangle.rotation) - self.ctx.transform(matrix) - - if rectangle.hole_diameter > 0: - self.ctx.push_group() - - self.ctx.set_line_width(0) - self.ctx.rectangle(*lower_left, width=width, height=height) - self.ctx.fill() - - if rectangle.hole_diameter > 0: - # Render the center clear - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR - if rectangle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_SOURCE) - center = map(mul, rectangle.position, self.scale) - self.ctx.arc(center[0], center[1], - radius=rectangle.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) - self.ctx.fill() - - if rectangle.rotation != 0: - self.ctx.restore() + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and rectangle.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + + mask.ctx.set_line_width(0) + mask.ctx.rectangle(*lower_left, width=width, height=height) + mask.ctx.fill() + + center = self.scale_point(rectangle.position) + if rectangle.hole_diameter > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + + mask.ctx.arc(center[0], center[1], + radius=rectangle.hole_radius * self.scale[0], angle1=0, + angle2=2 * math.pi) + mask.ctx.fill() + + if rectangle.hole_width > 0 and rectangle.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((rectangle.hole_width, rectangle.hole_height)) + lower_left = rotate_point((center[0] - width/2.0, center[1] - height/2.0), rectangle.rotation, center) + lower_right = rotate_point((center[0] + width/2.0, center[1] - height/2.0), rectangle.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_obround(self, obround, color): - if obround.hole_diameter > 0: - self.ctx.push_group() - - self._render_circle(obround.subshapes['circle1'], color) - self._render_circle(obround.subshapes['circle2'], color) - self._render_rectangle(obround.subshapes['rectangle'], color) - - if obround.hole_diameter > 0: - # Render the center clear - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) - center = map(mul, obround.position, self.scale) - self.ctx.arc(center[0], center[1], - radius=obround.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) - self.ctx.fill() - - self.ctx.pop_group_to_source() - self.ctx.paint_with_alpha(1) - + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and obround.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + + # Render circles + for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']): + center = self.scale_point(circle.position) + mask.ctx.arc(center[0], + center[1], + radius=(circle.radius * self.scale[0]), + angle1=0, + angle2=(2 * math.pi)) + mask.ctx.fill() + + # Render Rectangle + rectangle = obround.subshapes['rectangle'] + lower_left = self.scale_point(rectangle.lower_left) + 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.fill() + + center = self.scale_point(obround.position) + if obround.hole_diameter > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR) + mask.ctx.arc(center[0], center[1], + radius=obround.hole_radius * self.scale[0], angle1=0, + angle2=2 * math.pi) + mask.ctx.fill() + + if obround.hole_width > 0 and obround.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height =self.scale_point((obround.hole_width, obround.hole_height)) + lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), + obround.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + obround.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + obround.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + obround.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_polygon(self, polygon, color): - - # TODO Ths does not handle rotation of a polygon - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if polygon.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - if polygon.hole_radius > 0: - self.ctx.push_group() - - vertices = polygon.vertices - - self.ctx.set_line_width(0) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - - # Start from before the end so it is easy to iterate and make sure it is closed - self.ctx.move_to(*map(mul, vertices[-1], self.scale)) - for v in vertices: - self.ctx.line_to(*map(mul, v, self.scale)) - - self.ctx.fill() - - if polygon.hole_radius > 0: - # Render the center clear - center = tuple(map(mul, polygon.position, self.scale)) - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR - if polygon.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_SOURCE) - self.ctx.set_line_width(0) - self.ctx.arc(center[0], - center[1], - polygon.hole_radius * self.scale[0], 0, 2 * math.pi) - self.ctx.fill() + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and polygon.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + + vertices = polygon.vertices + mask.ctx.set_line_width(0) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + # Start from before the end so it is easy to iterate and make sure + # it is closed + mask.ctx.move_to(*self.scale_point(vertices[-1])) + for v in vertices: + mask.ctx.line_to(*self.scale_point(v)) + mask.ctx.fill() + + center = self.scale_point(polygon.position) + if polygon.hole_radius > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if polygon.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + mask.ctx.set_line_width(0) + mask.ctx.arc(center[0], + center[1], + polygon.hole_radius * self.scale[0], 0, 2 * math.pi) + mask.ctx.fill() + + if polygon.hole_width > 0 and polygon.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if polygon.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((polygon.hole_width, polygon.hole_height)) + lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), + polygon.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + polygon.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + polygon.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + polygon.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_drill(self, circle, color=None): color = color if color is not None else self.drill_color @@ -368,22 +493,20 @@ class GerberCairoContext(GerberContext): width = slot.diameter - self.ctx.set_operator(cairo.OPERATOR_SOURCE + self.ctx.set_operator(cairo.OPERATOR_OVER if slot.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() + with self._new_mask() as mask: + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*start) + mask.ctx.line_to(*end) + mask.ctx.stroke() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_amgroup(self, amgroup, color): - self.ctx.push_group() for primitive in amgroup.primitives: self.render(primitive) - self.ctx.pop_group_to_source() - self.ctx.paint_with_alpha(1) def _render_test_record(self, primitive, color): position = [pos + origin for pos, origin in @@ -392,7 +515,7 @@ class GerberCairoContext(GerberContext): '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_operator(cairo.OPERATOR_SOURCE + 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.015) for coord in position]) @@ -405,26 +528,25 @@ class GerberCairoContext(GerberContext): matrix = copy.copy(self._xform_matrix) layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) ctx = cairo.Context(layer) - ctx.scale(1, -1) - ctx.translate(-(self.origin_in_inch[0] * self.scale[0]), - (-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) + if self.invert: + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) ctx.set_operator(cairo.OPERATOR_OVER) ctx.paint() if mirror: matrix.xx = -1.0 matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] self.ctx = ctx + self.ctx.set_matrix(matrix) self.active_layer = layer self.active_matrix = matrix + 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 - ptn = cairo.SurfacePattern(self.active_layer) - ptn.set_matrix(self.active_matrix) self.output_ctx.set_source_rgba(*color, alpha=alpha) - self.output_ctx.mask(ptn) + self.output_ctx.mask_surface(self.active_layer) self.ctx = None self.active_layer = None self.active_matrix = None -- cgit From 0ae5c48a65d59df8624a17c2b5a6aabff4c05e25 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:10:32 -0500 Subject: Fix rs274x output bugs --- gerber/render/rs274x_backend.py | 51 ++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 19 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 13e871c..d32602a 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -159,7 +159,7 @@ class Rs274xContext(GerberContext): # Select the right aperture if not already selected if aperture: if isinstance(aperture, Circle): - aper = self._get_circle(aperture.diameter, aperture.hole_diameter) + aper = self._get_circle(aperture.diameter, aperture.hole_diameter, aperture.hole_width, aperture.hole_height) elif isinstance(aperture, Rectangle): aper = self._get_rectangle(aperture.width, aperture.height) elif isinstance(aperture, Obround): @@ -283,10 +283,12 @@ class Rs274xContext(GerberContext): self._pos = primitive.position - def _get_circle(self, diameter, hole_diameter, dcode = None): + def _get_circle(self, diameter, hole_diameter=None, hole_width=None, + hole_height=None, dcode = None): '''Define a circlar aperture''' - aper = self._circles.get((diameter, hole_diameter), None) + key = (diameter, hole_diameter, hole_width, hole_height) + aper = self._circles.get(key, None) if not aper: if not dcode: @@ -295,21 +297,22 @@ class Rs274xContext(GerberContext): else: self._next_dcode = max(dcode + 1, self._next_dcode) - aper = ADParamStmt.circle(dcode, diameter, hole_diameter) - self._circles[(diameter, hole_diameter)] = aper + aper = ADParamStmt.circle(dcode, diameter, hole_diameter, hole_width, hole_height) + self._circles[(diameter, hole_diameter, hole_width, hole_height)] = aper self.header.append(aper) return aper def _render_circle(self, circle, color): - aper = self._get_circle(circle.diameter, circle.hole_diameter) + aper = self._get_circle(circle.diameter, circle.hole_diameter, circle.hole_width, circle.hole_height) self._render_flash(circle, aper) - def _get_rectangle(self, width, height, dcode = None): + def _get_rectangle(self, width, height, hole_diameter=None, hole_width=None, + hole_height=None, dcode = None): '''Get a rectanglar aperture. If it isn't defined, create it''' - key = (width, height) + key = (width, height, hole_diameter, hole_width, hole_height) aper = self._rects.get(key, None) if not aper: @@ -319,20 +322,23 @@ class Rs274xContext(GerberContext): else: self._next_dcode = max(dcode + 1, self._next_dcode) - aper = ADParamStmt.rect(dcode, width, height) - self._rects[(width, height)] = aper + aper = ADParamStmt.rect(dcode, width, height, hole_diameter, hole_width, hole_height) + self._rects[(width, height, hole_diameter, hole_width, hole_height)] = aper self.header.append(aper) return aper def _render_rectangle(self, rectangle, color): - aper = self._get_rectangle(rectangle.width, rectangle.height) + aper = self._get_rectangle(rectangle.width, rectangle.height, + rectangle.hole_diameter, + rectangle.hole_width, rectangle.hole_height) self._render_flash(rectangle, aper) - def _get_obround(self, width, height, dcode = None): + def _get_obround(self, width, height, hole_diameter=None, hole_width=None, + hole_height=None, dcode = None): - key = (width, height) + key = (width, height, hole_diameter, hole_width, hole_height) aper = self._obrounds.get(key, None) if not aper: @@ -342,7 +348,7 @@ class Rs274xContext(GerberContext): else: self._next_dcode = max(dcode + 1, self._next_dcode) - aper = ADParamStmt.obround(dcode, width, height) + aper = ADParamStmt.obround(dcode, width, height, hole_diameter, hole_width, hole_height) self._obrounds[key] = aper self.header.append(aper) @@ -350,17 +356,22 @@ class Rs274xContext(GerberContext): def _render_obround(self, obround, color): - aper = self._get_obround(obround.width, obround.height) + aper = self._get_obround(obround.width, obround.height, + obround.hole_diameter, obround.hole_width, + obround.hole_height) self._render_flash(obround, aper) def _render_polygon(self, polygon, color): - aper = self._get_polygon(polygon.radius, polygon.sides, polygon.rotation, polygon.hole_radius) + aper = self._get_polygon(polygon.radius, polygon.sides, + polygon.rotation, polygon.hole_diameter, + polygon.hole_width, polygon.hole_height) self._render_flash(polygon, aper) - def _get_polygon(self, radius, num_vertices, rotation, hole_radius, dcode = None): + def _get_polygon(self, radius, num_vertices, rotation, hole_diameter=None, + hole_width=None, hole_height=None, dcode = None): - key = (radius, num_vertices, rotation, hole_radius) + key = (radius, num_vertices, rotation, hole_diameter, hole_width, hole_height) aper = self._polygons.get(key, None) if not aper: @@ -370,7 +381,9 @@ class Rs274xContext(GerberContext): else: self._next_dcode = max(dcode + 1, self._next_dcode) - aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, rotation, hole_radius * 2) + aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, + rotation, hole_diameter, hole_width, + hole_height) self._polygons[key] = aper self.header.append(aper) -- cgit From 7c4ec8a768c7dfd3124c6b68805031367a80a890 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 13 Dec 2016 00:01:05 -0500 Subject: Clip context to axis- and pixel- aligned bounds before rendering primitives. Significantly speeds up render --- gerber/render/cairo_backend.py | 1173 +++++++++++++++++++++------------------- 1 file changed, 610 insertions(+), 563 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index a2baa47..e6af67f 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -1,563 +1,610 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2014 Hamilton Kibbe - -# 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. - -try: - import cairo -except ImportError: - import cairocffi as cairo - -from operator import mul -import tempfile -import copy -import os - -from .render import GerberContext, RenderSettings -from .theme import THEMES -from ..primitives import * -from ..utils import rotate_point - -from io import BytesIO - - -class GerberCairoContext(GerberContext): - - def __init__(self, scale=300): - super(GerberCairoContext, self).__init__() - self.scale = (scale, scale) - self.surface = None - self.surface_buffer = None - self.ctx = None - self.active_layer = None - self.active_matrix = None - self.output_ctx = None - self.has_bg = False - self.origin_in_inch = None - self.size_in_inch = None - self._xform_matrix = None - self._render_count = 0 - - @property - def origin_in_pixels(self): - return (self.scale_point(self.origin_in_inch) - if self.origin_in_inch is not None else (0.0, 0.0)) - - @property - def size_in_pixels(self): - return (self.scale_point(self.size_in_inch) - 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]) - size_in_inch = (abs(bounds[0][1] - bounds[0][0]), - abs(bounds[1][1] - bounds[1][0])) - size_in_pixels = self.scale_point(size_in_inch) - 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 - self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, - x0=-self.origin_in_pixels[0], - y0=self.size_in_pixels[1]) - 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.output_ctx = cairo.Context(self.surface) - - def render_layer(self, layer, filename=None, settings=None, bgsettings=None, - verbose=False): - if settings is None: - settings = THEMES['default'].get(layer.layer_class, RenderSettings()) - if bgsettings is None: - bgsettings = THEMES['default'].get('background', RenderSettings()) - - if self._render_count == 0: - if verbose: - print('[Render]: Rendering Background.') - self.clear() - self.set_bounds(layer.bounds) - self._paint_background(bgsettings) - if verbose: - print('[Render]: Rendering {} Layer.'.format(layer.layer_class)) - self._render_count += 1 - self._render_layer(layer, settings) - if filename is not None: - self.dump(filename, verbose) - - def render_layers(self, layers, filename, theme=THEMES['default'], - verbose=False): - """ Render a set of layers - """ - self.clear() - bgsettings = theme['background'] - for layer in layers: - settings = theme.get(layer.layer_class, RenderSettings()) - self.render_layer(layer, settings=settings, bgsettings=bgsettings, - verbose=verbose) - self.dump(filename, verbose) - - def dump(self, filename=None, verbose=False): - """ Save image as `filename` - """ - try: - is_svg = os.path.splitext(filename.lower())[1] == '.svg' - except: - is_svg = False - if verbose: - print('[Render]: Writing image to {}'.format(filename)) - 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: - return self.surface.write_to_png(filename) - - def dump_str(self): - """ Return a byte-string containing the rendered image. - """ - fobj = BytesIO() - 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 clear(self): - self.surface = None - self.output_ctx = None - self.has_bg = False - self.origin_in_inch = None - self.size_in_inch = None - self._xform_matrix = None - self._render_count = 0 - if hasattr(self.surface_buffer, 'close'): - self.surface_buffer.close() - self.surface_buffer = None - - def _new_mask(self): - class Mask: - def __enter__(msk): - size_in_pixels = self.size_in_pixels - msk.surface = cairo.SVGSurface(None, size_in_pixels[0], - size_in_pixels[1]) - msk.ctx = cairo.Context(msk.surface) - msk.ctx.translate(-self.origin_in_pixels[0], -self.origin_in_pixels[1]) - return msk - - - def __exit__(msk, exc_type, exc_val, traceback): - if hasattr(msk.surface, 'finish'): - msk.surface.finish() - - return Mask() - - def _render_layer(self, layer, settings): - self.invert = settings.invert - # Get a new clean layer to render on - self._new_render_layer(mirror=settings.mirror) - for prim in layer.primitives: - self.render(prim) - # Add layer to image - self._flatten(settings.color, settings.alpha) - - def _render_line(self, line, color): - start = [pos * scale for pos, scale in zip(line.start, self.scale)] - end = [pos * scale for pos, scale in zip(line.end, self.scale)] - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and line.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._new_mask() as mask: - if isinstance(line.aperture, Circle): - width = line.aperture.diameter - mask.ctx.set_line_width(width * self.scale[0]) - mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - mask.ctx.move_to(*start) - mask.ctx.line_to(*end) - mask.ctx.stroke() - - elif hasattr(line, 'vertices') and line.vertices is not None: - points = [self.scale_point(x) for x in line.vertices] - mask.ctx.set_line_width(0) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_arc(self, arc, color): - center = self.scale_point(arc.center) - start = self.scale_point(arc.start) - end = self.scale_point(arc.end) - radius = self.scale[0] * arc.radius - two_pi = 2 * math.pi - angle1 = (arc.start_angle + two_pi) % two_pi - angle2 = (arc.end_angle + two_pi) % two_pi - if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant': - # Make the angles slightly different otherwise Cario will draw nothing - angle2 -= 0.000000001 - if isinstance(arc.aperture, Circle): - width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 - else: - width = max(arc.aperture.width, arc.aperture.height, 0.001) - - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and arc.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - - with self._new_mask() as mask: - mask.ctx.set_line_width(width * self.scale[0]) - 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) - else: - mask.ctx.arc_negative(*center, radius=radius, - angle1=angle1, angle2=angle2) - mask.ctx.move_to(*end) # ...lame - mask.ctx.stroke() - - #if isinstance(arc.aperture, Rectangle): - # print("Flash Rectangle Ends") - # print(arc.aperture.rotation * 180/math.pi) - # rect = arc.aperture - # width = self.scale[0] * rect.width - # height = self.scale[1] * rect.height - # for point, angle in zip((start, end), (angle1, angle2)): - # print("{} w {} h{}".format(point, rect.width, rect.height)) - # mask.ctx.rectangle(point[0] - width/2.0, - # point[1] - height/2.0, width, height) - # mask.ctx.fill() - - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - - def _render_region(self, region, color): - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) and region.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._new_mask() as mask: - mask.ctx.set_line_width(0) - mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - mask.ctx.move_to(*self.scale_point(region.primitives[0].start)) - for prim in region.primitives: - if isinstance(prim, Line): - mask.ctx.line_to(*self.scale_point(prim.end)) - else: - center = self.scale_point(prim.center) - radius = self.scale[0] * prim.radius - angle1 = prim.start_angle - angle2 = prim.end_angle - if prim.direction == 'counterclockwise': - mask.ctx.arc(*center, radius=radius, - angle1=angle1, angle2=angle2) - else: - mask.ctx.arc_negative(*center, radius=radius, - angle1=angle1, angle2=angle2) - mask.ctx.fill() - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_circle(self, circle, color): - center = self.scale_point(circle.position) - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and circle.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - - with self._new_mask() as mask: - mask.ctx.set_line_width(0) - mask.ctx.arc(center[0], - center[1], - radius=(circle.radius * self.scale[0]), - angle1=0, - angle2=(2 * math.pi)) - mask.ctx.fill() - - if hasattr(circle, 'hole_diameter') and circle.hole_diameter > 0: - mask.ctx.set_operator(cairo.OPERATOR_CLEAR) - mask.ctx.arc(center[0], - center[1], - radius=circle.hole_radius * self.scale[0], - angle1=0, - angle2=2 * math.pi) - mask.ctx.fill() - - if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height') - and circle.hole_width > 0 and circle.hole_height > 0): - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if circle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - width, height = self.scale_point((circle.hole_width, circle.hole_height)) - lower_left = rotate_point( - (center[0] - width / 2.0, center[1] - height / 2.0), - circle.rotation, center) - lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), - circle.rotation, center) - upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), - circle.rotation, center) - upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), - circle.rotation, center) - points = (lower_left, lower_right, upper_right, upper_left) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - - def _render_rectangle(self, rectangle, color): - lower_left = self.scale_point(rectangle.lower_left) - width, height = tuple([abs(coord) for coord in - self.scale_point((rectangle.width, - rectangle.height))]) - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and rectangle.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._new_mask() as mask: - - mask.ctx.set_line_width(0) - mask.ctx.rectangle(*lower_left, width=width, height=height) - mask.ctx.fill() - - center = self.scale_point(rectangle.position) - if rectangle.hole_diameter > 0: - # Render the center clear - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if rectangle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - - mask.ctx.arc(center[0], center[1], - radius=rectangle.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) - mask.ctx.fill() - - if rectangle.hole_width > 0 and rectangle.hole_height > 0: - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if rectangle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - width, height = self.scale_point((rectangle.hole_width, rectangle.hole_height)) - lower_left = rotate_point((center[0] - width/2.0, center[1] - height/2.0), rectangle.rotation, center) - lower_right = rotate_point((center[0] + width/2.0, center[1] - height/2.0), rectangle.rotation, center) - upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) - upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) - points = (lower_left, lower_right, upper_right, upper_left) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_obround(self, obround, color): - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and obround.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._new_mask() as mask: - mask.ctx.set_line_width(0) - - # Render circles - for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']): - center = self.scale_point(circle.position) - mask.ctx.arc(center[0], - center[1], - radius=(circle.radius * self.scale[0]), - angle1=0, - angle2=(2 * math.pi)) - mask.ctx.fill() - - # Render Rectangle - rectangle = obround.subshapes['rectangle'] - lower_left = self.scale_point(rectangle.lower_left) - 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.fill() - - center = self.scale_point(obround.position) - if obround.hole_diameter > 0: - # Render the center clear - mask.ctx.set_operator(cairo.OPERATOR_CLEAR) - mask.ctx.arc(center[0], center[1], - radius=obround.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) - mask.ctx.fill() - - if obround.hole_width > 0 and obround.hole_height > 0: - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if rectangle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - width, height =self.scale_point((obround.hole_width, obround.hole_height)) - lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), - obround.rotation, center) - lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), - obround.rotation, center) - upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), - obround.rotation, center) - upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), - obround.rotation, center) - points = (lower_left, lower_right, upper_right, upper_left) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_polygon(self, polygon, color): - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and polygon.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._new_mask() as mask: - - vertices = polygon.vertices - mask.ctx.set_line_width(0) - mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - # Start from before the end so it is easy to iterate and make sure - # it is closed - mask.ctx.move_to(*self.scale_point(vertices[-1])) - for v in vertices: - mask.ctx.line_to(*self.scale_point(v)) - mask.ctx.fill() - - center = self.scale_point(polygon.position) - if polygon.hole_radius > 0: - # Render the center clear - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if polygon.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - mask.ctx.set_line_width(0) - mask.ctx.arc(center[0], - center[1], - polygon.hole_radius * self.scale[0], 0, 2 * math.pi) - mask.ctx.fill() - - if polygon.hole_width > 0 and polygon.hole_height > 0: - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if polygon.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - width, height = self.scale_point((polygon.hole_width, polygon.hole_height)) - lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), - polygon.rotation, center) - lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), - polygon.rotation, center) - upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), - polygon.rotation, center) - upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), - polygon.rotation, center) - points = (lower_left, lower_right, upper_right, upper_left) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_drill(self, circle, color=None): - color = color if color is not None else self.drill_color - self._render_circle(circle, color) - - def _render_slot(self, slot, color): - start = map(mul, slot.start, self.scale) - end = map(mul, slot.end, self.scale) - - width = slot.diameter - - self.ctx.set_operator(cairo.OPERATOR_OVER - if slot.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - with self._new_mask() as mask: - mask.ctx.set_line_width(width * self.scale[0]) - mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - mask.ctx.move_to(*start) - mask.ctx.line_to(*end) - mask.ctx.stroke() - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_amgroup(self, amgroup, color): - for primitive in amgroup.primitives: - self.render(primitive) - - def _render_test_record(self, primitive, color): - position = [pos + origin for pos, origin in - zip(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_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.015) for coord in position]) - self.ctx.scale(1, -1) - self.ctx.show_text(primitive.net_name) - self.ctx.scale(1, -1) - - def _new_render_layer(self, color=None, mirror=False): - size_in_pixels = self.scale_point(self.size_in_inch) - matrix = copy.copy(self._xform_matrix) - layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) - ctx = cairo.Context(layer) - - if self.invert: - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_OVER) - ctx.paint() - if mirror: - matrix.xx = -1.0 - matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] - self.ctx = ctx - self.ctx.set_matrix(matrix) - self.active_layer = layer - self.active_matrix = matrix - - - 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.mask_surface(self.active_layer) - self.ctx = None - self.active_layer = None - self.active_matrix = None - - def _paint_background(self, settings=None): - color = settings.color if settings is not None else self.background_color - 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.paint() - - def scale_point(self, point): - return tuple([coord * scale for coord, scale in zip(point, self.scale)]) +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe + +# 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. + +try: + import cairo +except ImportError: + import cairocffi as cairo + +from operator import mul +import tempfile +import copy +import os + +from .render import GerberContext, RenderSettings +from .theme import THEMES +from ..primitives import * +from ..utils import rotate_point + +from io import BytesIO + + +class GerberCairoContext(GerberContext): + + def __init__(self, scale=300): + super(GerberCairoContext, self).__init__() + self.scale = (scale, scale) + self.surface = None + self.surface_buffer = None + self.ctx = None + self.active_layer = None + self.active_matrix = None + self.output_ctx = None + self.has_bg = False + self.origin_in_inch = None + self.size_in_inch = None + self._xform_matrix = None + self._render_count = 0 + + @property + def origin_in_pixels(self): + return (self.scale_point(self.origin_in_inch) + if self.origin_in_inch is not None else (0.0, 0.0)) + + @property + def size_in_pixels(self): + return (self.scale_point(self.size_in_inch) + 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]) + size_in_inch = (abs(bounds[0][1] - bounds[0][0]), + abs(bounds[1][1] - bounds[1][0])) + size_in_pixels = self.scale_point(size_in_inch) + 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 + self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, + x0=-self.origin_in_pixels[0], + y0=self.size_in_pixels[1]) + 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.output_ctx = cairo.Context(self.surface) + + def render_layer(self, layer, filename=None, settings=None, bgsettings=None, + verbose=False): + if settings is None: + settings = THEMES['default'].get(layer.layer_class, RenderSettings()) + if bgsettings is None: + bgsettings = THEMES['default'].get('background', RenderSettings()) + + if self._render_count == 0: + if verbose: + print('[Render]: Rendering Background.') + self.clear() + self.set_bounds(layer.bounds) + self._paint_background(bgsettings) + if verbose: + print('[Render]: Rendering {} Layer.'.format(layer.layer_class)) + self._render_count += 1 + self._render_layer(layer, settings) + if filename is not None: + self.dump(filename, verbose) + + def render_layers(self, layers, filename, theme=THEMES['default'], + verbose=False): + """ Render a set of layers + """ + self.clear() + bgsettings = theme['background'] + for layer in layers: + settings = theme.get(layer.layer_class, RenderSettings()) + self.render_layer(layer, settings=settings, bgsettings=bgsettings, + verbose=verbose) + self.dump(filename, verbose) + + def dump(self, filename=None, verbose=False): + """ Save image as `filename` + """ + try: + is_svg = os.path.splitext(filename.lower())[1] == '.svg' + except: + is_svg = False + if verbose: + print('[Render]: Writing image to {}'.format(filename)) + 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: + return self.surface.write_to_png(filename) + + def dump_str(self): + """ Return a byte-string containing the rendered image. + """ + fobj = BytesIO() + 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 clear(self): + self.surface = None + self.output_ctx = None + self.has_bg = False + self.origin_in_inch = None + self.size_in_inch = None + self._xform_matrix = None + self._render_count = 0 + self.surface_buffer = None + + def _new_mask(self): + class Mask: + def __enter__(msk): + size_in_pixels = self.size_in_pixels + msk.surface = cairo.SVGSurface(None, size_in_pixels[0], + size_in_pixels[1]) + msk.ctx = cairo.Context(msk.surface) + msk.ctx.translate(-self.origin_in_pixels[0], -self.origin_in_pixels[1]) + return msk + + + def __exit__(msk, exc_type, exc_val, traceback): + if hasattr(msk.surface, 'finish'): + msk.surface.finish() + + return Mask() + + def _render_layer(self, layer, settings): + self.invert = settings.invert + # Get a new clean layer to render on + self._new_render_layer(mirror=settings.mirror) + for prim in layer.primitives: + self.render(prim) + # Add layer to image + self._flatten(settings.color, settings.alpha) + + def _render_line(self, line, color): + start = self.scale_point(line.start) + end = self.scale_point(line.end) + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and line.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + + with self._clip_primitive(line): + with self._new_mask() as mask: + if isinstance(line.aperture, Circle): + width = line.aperture.diameter + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*start) + mask.ctx.line_to(*end) + mask.ctx.stroke() + + elif hasattr(line, 'vertices') and line.vertices is not None: + points = [self.scale_point(x) for x in line.vertices] + mask.ctx.set_line_width(0) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_arc(self, arc, color): + center = self.scale_point(arc.center) + start = self.scale_point(arc.start) + end = self.scale_point(arc.end) + radius = self.scale[0] * arc.radius + two_pi = 2 * math.pi + angle1 = (arc.start_angle + two_pi) % two_pi + angle2 = (arc.end_angle + two_pi) % two_pi + if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant': + # Make the angles slightly different otherwise Cario will draw nothing + angle2 -= 0.000000001 + if isinstance(arc.aperture, Circle): + width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 + else: + width = max(arc.aperture.width, arc.aperture.height, 0.001) + + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and arc.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(arc): + with self._new_mask() as mask: + mask.ctx.set_line_width(width * self.scale[0]) + 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) + else: + mask.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + mask.ctx.move_to(*end) # ...lame + mask.ctx.stroke() + + #if isinstance(arc.aperture, Rectangle): + # print("Flash Rectangle Ends") + # print(arc.aperture.rotation * 180/math.pi) + # rect = arc.aperture + # width = self.scale[0] * rect.width + # height = self.scale[1] * rect.height + # for point, angle in zip((start, end), (angle1, angle2)): + # print("{} w {} h{}".format(point, rect.width, rect.height)) + # mask.ctx.rectangle(point[0] - width/2.0, + # point[1] - height/2.0, width, height) + # mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_region(self, region, color): + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) and region.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(region): + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*self.scale_point(region.primitives[0].start)) + for prim in region.primitives: + if isinstance(prim, Line): + mask.ctx.line_to(*self.scale_point(prim.end)) + else: + center = self.scale_point(prim.center) + radius = self.scale[0] * prim.radius + angle1 = prim.start_angle + angle2 = prim.end_angle + if prim.direction == 'counterclockwise': + mask.ctx.arc(*center, radius=radius, + angle1=angle1, angle2=angle2) + else: + mask.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_circle(self, circle, color): + center = self.scale_point(circle.position) + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and circle.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(circle): + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + mask.ctx.arc(center[0], + center[1], + radius=(circle.radius * self.scale[0]), + angle1=0, + angle2=(2 * math.pi)) + mask.ctx.fill() + + if hasattr(circle, 'hole_diameter') and circle.hole_diameter > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR) + mask.ctx.arc(center[0], + center[1], + radius=circle.hole_radius * self.scale[0], + angle1=0, + angle2=2 * math.pi) + mask.ctx.fill() + + if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height') + and circle.hole_width > 0 and circle.hole_height > 0): + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if circle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((circle.hole_width, circle.hole_height)) + lower_left = rotate_point( + (center[0] - width / 2.0, center[1] - height / 2.0), + circle.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + circle.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + circle.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + circle.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_rectangle(self, rectangle, color): + lower_left = self.scale_point(rectangle.lower_left) + width, height = tuple([abs(coord) for coord in + self.scale_point((rectangle.width, + rectangle.height))]) + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and rectangle.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + 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.fill() + + center = self.scale_point(rectangle.position) + if rectangle.hole_diameter > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + + mask.ctx.arc(center[0], center[1], + radius=rectangle.hole_radius * self.scale[0], angle1=0, + angle2=2 * math.pi) + mask.ctx.fill() + + if rectangle.hole_width > 0 and rectangle.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((rectangle.hole_width, rectangle.hole_height)) + lower_left = rotate_point((center[0] - width/2.0, center[1] - height/2.0), rectangle.rotation, center) + lower_right = rotate_point((center[0] + width/2.0, center[1] - height/2.0), rectangle.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_obround(self, obround, color): + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and obround.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(obround): + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + + # Render circles + for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']): + center = self.scale_point(circle.position) + mask.ctx.arc(center[0], + center[1], + radius=(circle.radius * self.scale[0]), + angle1=0, + angle2=(2 * math.pi)) + mask.ctx.fill() + + # Render Rectangle + rectangle = obround.subshapes['rectangle'] + lower_left = self.scale_point(rectangle.lower_left) + 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.fill() + + center = self.scale_point(obround.position) + if obround.hole_diameter > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR) + mask.ctx.arc(center[0], center[1], + radius=obround.hole_radius * self.scale[0], angle1=0, + angle2=2 * math.pi) + mask.ctx.fill() + + if obround.hole_width > 0 and obround.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height =self.scale_point((obround.hole_width, obround.hole_height)) + lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), + obround.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + obround.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + obround.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + obround.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_polygon(self, polygon, color): + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and polygon.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(polygon): + with self._new_mask() as mask: + + vertices = polygon.vertices + mask.ctx.set_line_width(0) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + # Start from before the end so it is easy to iterate and make sure + # it is closed + mask.ctx.move_to(*self.scale_point(vertices[-1])) + for v in vertices: + mask.ctx.line_to(*self.scale_point(v)) + mask.ctx.fill() + + center = self.scale_point(polygon.position) + if polygon.hole_radius > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if polygon.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + mask.ctx.set_line_width(0) + mask.ctx.arc(center[0], + center[1], + polygon.hole_radius * self.scale[0], 0, 2 * math.pi) + mask.ctx.fill() + + if polygon.hole_width > 0 and polygon.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if polygon.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((polygon.hole_width, polygon.hole_height)) + lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), + polygon.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + polygon.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + polygon.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + polygon.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_drill(self, circle, color=None): + color = color if color is not None else self.drill_color + self._render_circle(circle, color) + + def _render_slot(self, slot, color): + start = map(mul, slot.start, self.scale) + end = map(mul, slot.end, self.scale) + + width = slot.diameter + + self.ctx.set_operator(cairo.OPERATOR_OVER + if slot.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) + with self._clip_primitive(slot): + with self._new_mask() as mask: + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*start) + mask.ctx.line_to(*end) + mask.ctx.stroke() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_amgroup(self, amgroup, color): + for primitive in amgroup.primitives: + self.render(primitive) + + def _render_test_record(self, primitive, color): + position = [pos + origin for pos, origin in + zip(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_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.015) for coord in position]) + self.ctx.scale(1, -1) + self.ctx.show_text(primitive.net_name) + self.ctx.scale(1, -1) + + def _new_render_layer(self, color=None, mirror=False): + size_in_pixels = self.scale_point(self.size_in_inch) + matrix = copy.copy(self._xform_matrix) + layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) + ctx = cairo.Context(layer) + + if self.invert: + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.paint() + if mirror: + matrix.xx = -1.0 + matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] + self.ctx = ctx + self.ctx.set_matrix(matrix) + self.active_layer = layer + self.active_matrix = matrix + + + 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.mask_surface(self.active_layer) + self.ctx = None + self.active_layer = None + self.active_matrix = None + + def _paint_background(self, settings=None): + color = settings.color if settings is not None else self.background_color + 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.paint() + + def _clip_primitive(self, primitive): + """ Clip rendering context to pixel-aligned bounding box + + Calculates pixel- and axis- aligned bounding box, and clips current + context to that region. Improves rendering speed significantly. This + returns a context manager, use as follows: + + with self._clip_primitive(some_primitive): + do_rendering_stuff() + do_more_rendering stuff(with, arguments) + + The context manager will reset the context's clipping region when it + goes out of scope. + + """ + class Clip: + def __init__(clp, primitive): + x_range, y_range = primitive.bounding_box + xmin, xmax = x_range + ymin, ymax = y_range + + # Round bounds to the nearest pixel outside of the primitive + clp.xmin = math.floor(self.scale[0] * xmin) + clp.xmax = math.ceil(self.scale[0] * xmax) + + # We need to offset Y to take care of the difference in y-pos + # caused by flipping the axis. + clp.ymin = math.floor( + (self.scale[1] * ymin) - math.ceil(self.origin_in_pixels[1])) + clp.ymax = math.floor( + (self.scale[1] * ymax) - math.floor(self.origin_in_pixels[1])) + + # Calculate width and height, rounded to the nearest pixel + clp.width = abs(clp.xmax - clp.xmin) + clp.height = abs(clp.ymax - clp.ymin) + + def __enter__(clp): + # Clip current context to primitive's bounding box + self.ctx.rectangle(clp.xmin, clp.ymin, clp.width, clp.height) + self.ctx.clip() + + def __exit__(clp, exc_type, exc_val, traceback): + # Reset context clip region + self.ctx.reset_clip() + + return Clip(primitive) + + def scale_point(self, point): + return tuple([coord * scale for coord, scale in zip(point, self.scale)]) -- cgit From 19a8fb00487ca182bd3127b4def52719d8be3e30 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 13 Dec 2016 20:22:54 -0500 Subject: Add max_width and max_height arguments to --- gerber/render/cairo_backend.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index e6af67f..76be60a 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -97,10 +97,29 @@ class GerberCairoContext(GerberContext): self.dump(filename, verbose) def render_layers(self, layers, filename, theme=THEMES['default'], - verbose=False): + verbose=False, max_width=800, max_height=600): """ Render a set of layers """ + # Calculate scale parameter + x_range = [10000, -10000] + y_range = [10000, -10000] + for layer in layers: + bounds = layer.bounds + if bounds is not None: + layer_x, layer_y = bounds + x_range[0] = min(x_range[0], layer_x[0]) + x_range[1] = max(x_range[1], layer_x[1]) + y_range[0] = min(y_range[0], layer_y[0]) + y_range[1] = max(y_range[1], layer_y[1]) + width = x_range[1] - x_range[0] + height = y_range[1] - y_range[0] + + scale = math.floor(min(float(max_width)/width, float(max_height)/height)) + self.scale = (scale, scale) + self.clear() + + # Render layers bgsettings = theme['background'] for layer in layers: settings = theme.get(layer.layer_class, RenderSettings()) @@ -293,7 +312,7 @@ class GerberCairoContext(GerberContext): angle2=(2 * math.pi)) mask.ctx.fill() - if hasattr(circle, 'hole_diameter') and circle.hole_diameter > 0: + if hasattr(circle, 'hole_diameter') and circle.hole_diameter is not None and circle.hole_diameter > 0: mask.ctx.set_operator(cairo.OPERATOR_CLEAR) mask.ctx.arc(center[0], center[1], @@ -303,6 +322,7 @@ class GerberCairoContext(GerberContext): mask.ctx.fill() if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height') + and circle.hole_width is not None and circle.hole_height is not None and circle.hole_width > 0 and circle.hole_height > 0): mask.ctx.set_operator(cairo.OPERATOR_CLEAR if circle.level_polarity == 'dark' -- cgit From 787399992662f1963cfbd2dd9e9b9002b6b131e4 Mon Sep 17 00:00:00 2001 From: Jan Margeta Date: Sat, 15 Apr 2017 14:53:08 +0200 Subject: Fix Cairo backend for svg saving and Python 3 --- gerber/render/cairo_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 76be60a..8b6c81e 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -139,7 +139,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() -- cgit From e0b45108d2fb96ffcf4e6af02dd55fc6aca3e4b2 Mon Sep 17 00:00:00 2001 From: Tom Anderson Date: Wed, 24 May 2017 09:42:23 -0700 Subject: Added bounds argument to render_layer() --- gerber/render/cairo_backend.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 76be60a..3d87f5c 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)) -- cgit From f7a719e6f72dc80c34bd6a1015e81f0d2b370689 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 12 Jun 2017 07:58:06 -0400 Subject: Fix error when unpacking colors in cairo backend --- 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 76be60a..7c97cc9 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -565,7 +565,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 +576,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): -- cgit From e754f5946885a5c5a9cfa49202ccef4c447d8116 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 4 Jul 2017 01:22:47 -0400 Subject: Remove rest of mixed unpack/kwarg syntax to fix #72 --- gerber/render/cairo_backend.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 0a5e550..2e9b143 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -252,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() @@ -291,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]) @@ -360,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) @@ -418,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) -- cgit From e5597e84a81dbe2f031d1e8bba58a9e1d384f798 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 25 Nov 2017 16:15:00 +0100 Subject: Use positional arguments for cairo.Context.arc cairocffi 0.6 does not support keyword args. --- gerber/render/cairo_backend.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 2e9b143..0e3a721 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -308,20 +308,12 @@ class GerberCairoContext(GerberContext): with self._clip_primitive(circle): with self._new_mask() as mask: mask.ctx.set_line_width(0) - mask.ctx.arc(center[0], - center[1], - radius=(circle.radius * self.scale[0]), - angle1=0, - angle2=(2 * math.pi)) + mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi)) mask.ctx.fill() if hasattr(circle, 'hole_diameter') and circle.hole_diameter is not None and circle.hole_diameter > 0: mask.ctx.set_operator(cairo.OPERATOR_CLEAR) - mask.ctx.arc(center[0], - center[1], - radius=circle.hole_radius * self.scale[0], - angle1=0, - angle2=2 * math.pi) + mask.ctx.arc(center[0], center[1], circle.hole_radius * self.scale[0], 0, 2 * math.pi) mask.ctx.fill() if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height') @@ -371,9 +363,7 @@ class GerberCairoContext(GerberContext): and (not self.invert) else cairo.OPERATOR_OVER) - mask.ctx.arc(center[0], center[1], - radius=rectangle.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) + mask.ctx.arc(center[0], center[1], rectangle.hole_radius * self.scale[0], 0, 2 * math.pi) mask.ctx.fill() if rectangle.hole_width > 0 and rectangle.hole_height > 0: @@ -405,11 +395,7 @@ class GerberCairoContext(GerberContext): # Render circles for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']): center = self.scale_point(circle.position) - mask.ctx.arc(center[0], - center[1], - radius=(circle.radius * self.scale[0]), - angle1=0, - angle2=(2 * math.pi)) + mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi)) mask.ctx.fill() # Render Rectangle @@ -425,9 +411,7 @@ class GerberCairoContext(GerberContext): if obround.hole_diameter > 0: # Render the center clear mask.ctx.set_operator(cairo.OPERATOR_CLEAR) - mask.ctx.arc(center[0], center[1], - radius=obround.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) + mask.ctx.arc(center[0], center[1], obround.hole_radius * self.scale[0], 0, 2 * math.pi) mask.ctx.fill() if obround.hole_width > 0 and obround.hole_height > 0: -- cgit From 5245fb925684b4ebe056e6509bfeca6b167903b5 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 5 Jun 2018 08:57:37 -0400 Subject: Fix hard requirement of cairo per #83, and add stubs for required subclass methods to GerberContext per #84 --- gerber/render/__init__.py | 2 - gerber/render/cairo_backend.py | 13 +++--- gerber/render/excellon_backend.py | 91 +++++++++++++++++++-------------------- gerber/render/render.py | 27 ++++++++++-- gerber/render/rs274x_backend.py | 10 ++--- 5 files changed, 79 insertions(+), 64 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index 3598c4d..fe08d50 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -23,6 +23,4 @@ This module provides contexts for rendering images of gerber layers. Currently SVG is the only supported format. """ - -from .cairo_backend import GerberCairoContext from .render import RenderSettings diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 0e3a721..e1d1408 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -91,7 +91,7 @@ class GerberCairoContext(GerberContext): self.set_bounds(bounds) else: self.set_bounds(layer.bounds) - self._paint_background(bgsettings) + self.paint_background(bgsettings) if verbose: print('[Render]: Rendering {} Layer.'.format(layer.layer_class)) self._render_count += 1 @@ -193,11 +193,11 @@ class GerberCairoContext(GerberContext): def _render_layer(self, layer, settings): self.invert = settings.invert # Get a new clean layer to render on - self._new_render_layer(mirror=settings.mirror) + self.new_render_layer(mirror=settings.mirror) for prim in layer.primitives: self.render(prim) # Add layer to image - self._flatten(settings.color, settings.alpha) + self.flatten(settings.color, settings.alpha) def _render_line(self, line, color): start = self.scale_point(line.start) @@ -530,7 +530,7 @@ class GerberCairoContext(GerberContext): self.ctx.show_text(primitive.net_name) self.ctx.scale(1, -1) - def _new_render_layer(self, color=None, mirror=False): + def new_render_layer(self, color=None, mirror=False): size_in_pixels = self.scale_point(self.size_in_inch) matrix = copy.copy(self._xform_matrix) layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) @@ -548,8 +548,7 @@ class GerberCairoContext(GerberContext): self.active_layer = layer self.active_matrix = matrix - - def _flatten(self, color=None, alpha=None): + 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[0], color[1], color[2], alpha) @@ -558,7 +557,7 @@ class GerberCairoContext(GerberContext): self.active_layer = None self.active_matrix = None - def _paint_background(self, settings=None): + def paint_background(self, settings=None): color = settings.color if settings is not None else self.background_color alpha = settings.alpha if settings is not None else 1.0 if not self.has_bg: diff --git a/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py index da5b22b..765d68c 100644 --- a/gerber/render/excellon_backend.py +++ b/gerber/render/excellon_backend.py @@ -4,13 +4,13 @@ from ..excellon import DrillSlot from ..excellon_statements import * class ExcellonContext(GerberContext): - + MODE_DRILL = 1 - MODE_SLOT =2 - + MODE_SLOT =2 + def __init__(self, settings): GerberContext.__init__(self) - + # Statements that we write self.comments = [] self.header = [] @@ -18,57 +18,57 @@ class ExcellonContext(GerberContext): self.body_start = [RewindStopStmt()] self.body = [] self.start = [HeaderBeginStmt()] - + # Current tool and position self.handled_tools = set() self.cur_tool = None self.drill_mode = ExcellonContext.MODE_DRILL self.drill_down = False self._pos = (None, None) - + self.settings = settings self._start_header() self._start_comments() - + def _start_header(self): """Create the header from the settings""" - + self.header.append(UnitStmt.from_settings(self.settings)) - + if self.settings.notation == 'incremental': raise NotImplementedError('Incremental mode is not implemented') else: self.body.append(AbsoluteModeStmt()) - + def _start_comments(self): - + # Write the digits used - this isn't valid Excellon statement, so we write as a comment self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1]))) - + def _get_end(self): """How we end depends on our mode""" - + end = [] - + if self.drill_down: end.append(RetractWithClampingStmt()) end.append(RetractWithoutClampingStmt()) - + end.append(EndOfProgramStmt()) - + return end - + @property def statements(self): return self.start + self.comments + self.header + self.body_start + self.body + self._get_end() - - def set_bounds(self, bounds): + + def set_bounds(self, bounds, *args, **kwargs): pass - - def _paint_background(self): + + def paint_background(self): pass - + def _render_line(self, line, color): raise ValueError('Invalid Excellon object') def _render_arc(self, arc, color): @@ -76,7 +76,7 @@ class ExcellonContext(GerberContext): def _render_region(self, region, color): raise ValueError('Invalid Excellon object') - + def _render_level_polarity(self, region): raise ValueError('Invalid Excellon object') @@ -85,105 +85,104 @@ class ExcellonContext(GerberContext): def _render_rectangle(self, rectangle, color): raise ValueError('Invalid Excellon object') - + def _render_obround(self, obround, color): raise ValueError('Invalid Excellon object') - + def _render_polygon(self, polygon, color): raise ValueError('Invalid Excellon object') - + def _simplify_point(self, point): return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) def _render_drill(self, drill, color): - + if self.drill_mode != ExcellonContext.MODE_DRILL: self._start_drill_mode() - + tool = drill.hit.tool if not tool in self.handled_tools: self.handled_tools.add(tool) self.header.append(ExcellonTool.from_tool(tool)) - + if tool != self.cur_tool: self.body.append(ToolSelectionStmt(tool.number)) self.cur_tool = tool - + point = self._simplify_point(drill.position) self._pos = drill.position self.body.append(CoordinateStmt.from_point(point)) - + def _start_drill_mode(self): """ If we are not in drill mode, then end the ROUT so we can do basic drilling """ - + if self.drill_mode == ExcellonContext.MODE_SLOT: - + # Make sure we are retracted before changing modes last_cmd = self.body[-1] if self.drill_down: self.body.append(RetractWithClampingStmt()) self.body.append(RetractWithoutClampingStmt()) self.drill_down = False - + # Switch to drill mode self.body.append(DrillModeStmt()) self.drill_mode = ExcellonContext.MODE_DRILL - + else: raise ValueError('Should be in slot mode') - + def _render_slot(self, slot, color): - + # Set the tool first, before we might go into drill mode tool = slot.hit.tool if not tool in self.handled_tools: self.handled_tools.add(tool) self.header.append(ExcellonTool.from_tool(tool)) - + if tool != self.cur_tool: self.body.append(ToolSelectionStmt(tool.number)) self.cur_tool = tool - + # Two types of drilling - normal drill and slots if slot.hit.slot_type == DrillSlot.TYPE_ROUT: # For ROUT, setting the mode is part of the actual command. - + # Are we in the right position? if slot.start != self._pos: if self.drill_down: # We need to move into the right position, so retract self.body.append(RetractWithClampingStmt()) self.drill_down = False - + # Move to the right spot point = self._simplify_point(slot.start) self._pos = slot.start self.body.append(CoordinateStmt.from_point(point, mode="ROUT")) - + # Now we are in the right spot, so drill down if not self.drill_down: self.body.append(ZAxisRoutPositionStmt()) self.drill_down = True - + # Do a linear move from our current position to the end position point = self._simplify_point(slot.end) self._pos = slot.end self.body.append(CoordinateStmt.from_point(point, mode="LINEAR")) self.drill_mode = ExcellonContext.MODE_SLOT - + else: # This is a G85 slot, so do this in normally drilling mode if self.drill_mode != ExcellonContext.MODE_DRILL: self._start_drill_mode() - + # Slots don't use simplified points self._pos = slot.end self.body.append(SlotStmt.from_points(slot.start, slot.end)) def _render_inverted_layer(self): pass - \ No newline at end of file diff --git a/gerber/render/render.py b/gerber/render/render.py index 79f43d6..580a7ea 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -139,7 +139,7 @@ class GerberContext(object): if not primitive: return - self._pre_render_primitive(primitive) + self.pre_render_primitive(primitive) color = self.color if isinstance(primitive, Line): @@ -167,16 +167,35 @@ class GerberContext(object): elif isinstance(primitive, TestRecord): self._render_test_record(primitive, color) - self._post_render_primitive(primitive) + self.post_render_primitive(primitive) - def _pre_render_primitive(self, primitive): + def set_bounds(self, bounds, *args, **kwargs): + """Called by the renderer to set the extents of the file to render. + + Parameters + ---------- + bounds: Tuple[Tuple[float, float], Tuple[float, float]] + ( (x_min, x_max), (y_min, y_max) + """ + pass + + def paint_background(self): + pass + + def new_render_layer(self): + pass + + def flatten(self): + pass + + def pre_render_primitive(self, primitive): """ Called before rendering a primitive. Use the callback to perform some action before rendering a primitive, for example adding a comment. """ return - def _post_render_primitive(self, primitive): + def post_render_primitive(self, primitive): """ Called after rendering a primitive. Use the callback to perform some action after rendering a primitive diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index d32602a..30048c4 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -148,10 +148,10 @@ class Rs274xContext(GerberContext): def statements(self): return self.comments + self.header + self.body + self.end - def set_bounds(self, bounds): + def set_bounds(self, bounds, *args, **kwargs): pass - def _paint_background(self): + def paint_background(self): pass def _select_aperture(self, aperture): @@ -173,7 +173,7 @@ class Rs274xContext(GerberContext): self.body.append(ApertureStmt(aper.d)) self._dcode = aper.d - def _pre_render_primitive(self, primitive): + def pre_render_primitive(self, primitive): if hasattr(primitive, 'comment'): self.body.append(CommentStmt(primitive.comment)) @@ -489,11 +489,11 @@ class Rs274xContext(GerberContext): def _render_inverted_layer(self): pass - def _new_render_layer(self): + def new_render_layer(self): # TODO Might need to implement this pass - def _flatten(self): + def flatten(self): # TODO Might need to implement this pass -- cgit From 17924398fa2fe55c933ce004c59c70c2a663f28a Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 6 Jul 2018 19:57:01 +0200 Subject: Fix cairo matrix clone op to not use copy.copy For some reason, copy.copy would barf saying it can't deepcopy cairo matrices. --- gerber/render/cairo_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 0e3a721..b450be0 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -532,7 +532,7 @@ class GerberCairoContext(GerberContext): def _new_render_layer(self, color=None, mirror=False): size_in_pixels = self.scale_point(self.size_in_inch) - matrix = copy.copy(self._xform_matrix) + matrix = cairo.Matrix() * self._xform_matrix layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) ctx = cairo.Context(layer) -- cgit From a7a5981e0eb2b112a57c6ea1151eb2b88f798857 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 3 Feb 2019 13:42:44 +0900 Subject: Make primitives with unset level polarity inherit from region This fixes region rendering with programatically generated primitives such that clear level polarity works in an intuitive way. This is useful for e.g. cutouts in regions. Before, the renderer would set level polarity twice, both when starting the region and then again once for each region primitive (line or arc). The problem was that the primitives in a region with "clear" polarity would when constructed with unset polarity default to "dark". Thus the renderer would emit something like LPC (clear polarity) -> G36 (start region) -> LPD (dark polarity) -> {lines...} instead of LPC -> G36 -> {lines...}. After this commit, Line and Arc will retain None as level polarity when created with unset level polarity, and region rendering will override None with the region's polarity. Outside regions, the old dark default remains unchanged. Note on verification: Somehow, gEDA gerbv would still render the broken regions the way one would have intended, but other viewers (KiCAD gerbview, the online EasyEDA one and whatever JLC uses to make their silkscreens) would not. --- gerber/render/rs274x_backend.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) (limited to 'gerber/render') diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 30048c4..c7af2ea 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -178,11 +178,11 @@ class Rs274xContext(GerberContext): if hasattr(primitive, 'comment'): self.body.append(CommentStmt(primitive.comment)) - def _render_line(self, line, color): + def _render_line(self, line, color, default_polarity='dark'): self._select_aperture(line.aperture) - self._render_level_polarity(line) + self._render_level_polarity(line, default_polarity) # Get the right function if self._func != CoordStmt.FUNC_LINEAR: @@ -206,7 +206,7 @@ class Rs274xContext(GerberContext): elif func: self.body.append(CoordStmt.mode(func)) - def _render_arc(self, arc, color): + def _render_arc(self, arc, color, default_polarity='dark'): # Optionally set the quadrant mode if it has changed: if arc.quadrant_mode != self._quadrant_mode: @@ -221,7 +221,7 @@ class Rs274xContext(GerberContext): # Select the right aperture if not already selected self._select_aperture(arc.aperture) - self._render_level_polarity(arc) + self._render_level_polarity(arc, default_polarity) # Find the right movement mode. Always set to be sure it is really right dir = arc.direction @@ -252,20 +252,23 @@ class Rs274xContext(GerberContext): for p in region.primitives: + # Make programmatically generated primitives within a region with + # unset level polarity inherit the region's level polarity if isinstance(p, Line): - self._render_line(p, color) + self._render_line(p, color, default_polarity=region.level_polarity) else: - self._render_arc(p, color) + self._render_arc(p, color, default_polarity=region.level_polarity) if self.explicit_region_move_end: self.body.append(CoordStmt.move(None, None)) self.body.append(RegionModeStmt.off()) - def _render_level_polarity(self, region): - if region.level_polarity != self._level_polarity: - self._level_polarity = region.level_polarity - self.body.append(LPParamStmt.from_region(region)) + def _render_level_polarity(self, obj, default='dark'): + obj_polarity = obj.level_polarity if obj.level_polarity is not None else default + if obj_polarity != self._level_polarity: + self._level_polarity = obj_polarity + self.body.append(LPParamStmt('LP', obj_polarity)) def _render_flash(self, primitive, aperture): -- cgit From 2601ae8eab8d7be807bdbed264cd943e441a8da0 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 2 Mar 2019 10:41:37 -0500 Subject: fix reversed layer bug --- gerber/render/cairo_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/render') diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 7c01319..e1d1408 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -532,7 +532,7 @@ class GerberCairoContext(GerberContext): def new_render_layer(self, color=None, mirror=False): size_in_pixels = self.scale_point(self.size_in_inch) - matrix = cairo.Matrix() * self._xform_matrix + matrix = copy.copy(self._xform_matrix) layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) ctx = cairo.Context(layer) -- cgit From 0c862895651a1d5b7f13e42a1af94fd4418b0400 Mon Sep 17 00:00:00 2001 From: Chintalagiri Shashank Date: Sat, 11 May 2019 03:18:09 +0530 Subject: Add a new transparant theme for multilayer renders. --- gerber/render/theme.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/theme.py b/gerber/render/theme.py index 2887216..2f558a1 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -23,6 +23,7 @@ COLORS = { 'white': (1.0, 1.0, 1.0), 'red': (1.0, 0.0, 0.0), 'green': (0.0, 1.0, 0.0), + 'yellow': (1.0, 1.0, 0), 'blue': (0.0, 0.0, 1.0), 'fr-4': (0.290, 0.345, 0.0), 'green soldermask': (0.0, 0.412, 0.278), @@ -35,6 +36,18 @@ COLORS = { } +SPECTRUM = [ + (0.804, 0.216, 0), + (0.78, 0.776, 0.251), + (0.545, 0.451, 0.333), + (0.545, 0.137, 0.137), + (0.329, 0.545, 0.329), + (0.133, 0.545, 0.133), + (0, 0.525, 0.545), + (0.227, 0.373, 0.804), +] + + class Theme(object): def __init__(self, name=None, **kwargs): @@ -48,10 +61,22 @@ class Theme(object): self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True)) self.drill = kwargs.get('drill', RenderSettings(COLORS['black'])) self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red'])) + self._internal = kwargs.get('internal', [RenderSettings(x) for x in SPECTRUM]) + self._internal_gen = None def __getitem__(self, key): return getattr(self, key) + @property + def internal(self): + if not self._internal_gen: + self._internal_gen = self._internal_gen_func() + return next(self._internal_gen) + + def _internal_gen_func(self): + for setting in self._internal: + yield setting + def get(self, key, noneval=None): val = getattr(self, key, None) return val if val is not None else noneval @@ -77,4 +102,11 @@ THEMES = { top=RenderSettings(COLORS['red'], alpha=0.5), bottom=RenderSettings(COLORS['blue'], alpha=0.5), drill=RenderSettings((0.3, 0.3, 0.3))), + + 'Transparent Multilayer': Theme(name='Transparent Multilayer', + background=RenderSettings((0, 0, 0)), + top=RenderSettings(SPECTRUM[0], alpha=0.8), + bottom=RenderSettings(SPECTRUM[-1], alpha=0.8), + drill=RenderSettings((0.3, 0.3, 0.3)), + internal=[RenderSettings(x, alpha=0.5) for x in SPECTRUM[1:-1]]), } -- cgit From 9cc42d9b7752603ae409e7c95a8cb5bec4d7b5b2 Mon Sep 17 00:00:00 2001 From: Chintalagiri Shashank Date: Sat, 11 May 2019 04:23:19 +0530 Subject: Make __main__ functional again and install a script entry point. --- gerber/render/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'gerber/render') diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index fe08d50..c7dbdd5 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -24,3 +24,8 @@ SVG is the only supported format. """ from .render import RenderSettings +from .cairo_backend import GerberCairoContext + +available_renderers = { + 'cairo': GerberCairoContext, +} -- cgit