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 --- TODO.md | 3 - doc/source/conf.py | 2 +- doc/source/documentation/gerber.rst | 36 ---- doc/source/documentation/index.rst | 2 +- doc/source/documentation/rs274x.rst | 37 ++++ examples/composite_bottom.png | Bin 145973 -> 146354 bytes examples/composite_bottom.svg | 2 + examples/composite_top.png | Bin 306805 -> 292317 bytes examples/composite_top.svg | 2 + gerber.md | 73 -------- gerber/__main__.py | 1 - gerber/common.py | 4 +- gerber/excellon.py | 3 +- gerber/gerber.py | 332 ------------------------------------ gerber/render/render.py | 19 ++- gerber/render/svgwrite_backend.py | 7 + gerber/rs274x.py | 327 +++++++++++++++++++++++++++++++++++ 17 files changed, 399 insertions(+), 451 deletions(-) delete mode 100644 TODO.md delete mode 100644 doc/source/documentation/gerber.rst create mode 100644 doc/source/documentation/rs274x.rst create mode 100644 examples/composite_bottom.svg create mode 100644 examples/composite_top.svg delete mode 100644 gerber.md delete mode 100644 gerber/gerber.py create mode 100644 gerber/rs274x.py diff --git a/TODO.md b/TODO.md deleted file mode 100644 index f5aa93b..0000000 --- a/TODO.md +++ /dev/null @@ -1,3 +0,0 @@ -* add command line utilities: gerber svg, gerber transform --rotate --scale --translate, gerber merge --blueprint - -* AM defined apertures \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index ac0fdf7..a118546 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -86,7 +86,7 @@ exclude_patterns = [] # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' diff --git a/doc/source/documentation/gerber.rst b/doc/source/documentation/gerber.rst deleted file mode 100644 index 78870a9..0000000 --- a/doc/source/documentation/gerber.rst +++ /dev/null @@ -1,36 +0,0 @@ -:mod:`gerber` --- RS-274X file handling -============================================== - -.. module:: gerber - :synopsis: Functions and classes for handling RS-274X files -.. sectionauthor:: Hamilton Kibbe - - -The RS-274X (Gerber) format is the most common format for exporting PCB -artwork. The Specification is published by Ucamco and is available -`here `_. -The :mod:`gerber` submodule implements calsses to read and write -RS-274X files without having to know the precise details of the format. - -The :mod:`gerber` submodule's :func:`read` function serves as a -simple interface for parsing gerber files. The :class:`GerberFile` class -stores all the information contained in a gerber file allowing the file to be -analyzed, modified, and updated. The :class:`GerberParser` class is used in -the background for parsing RS-274X files. - -.. _gerber-contents: -Functions ---------- -The :mod:`gerber` module defines the following functions: - -.. autofunction:: gerber.gerber.read - -Classes -------- -The :mod:`gerber` module defines the following classes: - -.. autoclass:: gerber.gerber.GerberFile - :members: - -.. autoclass:: gerber.gerber.GerberParser - :members: \ No newline at end of file diff --git a/doc/source/documentation/index.rst b/doc/source/documentation/index.rst index 110df87..3d8241a 100644 --- a/doc/source/documentation/index.rst +++ b/doc/source/documentation/index.rst @@ -4,7 +4,7 @@ Gerber Tools Reference .. toctree:: :maxdepth: 2 - Gerber (RS-274X) Files + Gerber (RS-274X) Files Excellon Files Rendering diff --git a/doc/source/documentation/rs274x.rst b/doc/source/documentation/rs274x.rst new file mode 100644 index 0000000..bc99519 --- /dev/null +++ b/doc/source/documentation/rs274x.rst @@ -0,0 +1,37 @@ +:mod:`rs274x` --- RS-274X file handling +============================================== + +.. module:: rs274x + :synopsis: Functions and classes for handling RS-274X files +.. sectionauthor:: Hamilton Kibbe + + +The RS-274X (Gerber) format is the most common format for exporting PCB +artwork. The Specification is published by Ucamco and is available +`here `_. +The :mod:`rs274x` submodule implements calsses to read and write +RS-274X files without having to know the precise details of the format. + +The :mod:`rs274x` submodule's :func:`read` function serves as a +simple interface for parsing gerber files. The :class:`GerberFile` class +stores all the information contained in a gerber file allowing the file to be +analyzed, modified, and updated. The :class:`GerberParser` class is used in +the background for parsing RS-274X files. + +.. _gerber-contents: + +Functions +--------- +The :mod:`rs274x` module defines the following functions: + +.. autofunction:: gerber.rs274x.read + +Classes +------- +The :mod:`rs274x` module defines the following classes: + +.. autoclass:: gerber.rs274x.GerberFile + :members: + +.. autoclass:: gerber.rs274x.GerberParser + :members: \ No newline at end of file diff --git a/examples/composite_bottom.png b/examples/composite_bottom.png index 76451a5..4d13bfd 100644 Binary files a/examples/composite_bottom.png and b/examples/composite_bottom.png differ diff --git a/examples/composite_bottom.svg b/examples/composite_bottom.svg new file mode 100644 index 0000000..c2e176d --- /dev/null +++ b/examples/composite_bottom.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/examples/composite_top.png b/examples/composite_top.png index a68bac8..d1dfe15 100644 Binary files a/examples/composite_top.png and b/examples/composite_top.png differ diff --git a/examples/composite_top.svg b/examples/composite_top.svg new file mode 100644 index 0000000..21b01fa --- /dev/null +++ b/examples/composite_top.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/gerber.md b/gerber.md deleted file mode 100644 index 865d177..0000000 --- a/gerber.md +++ /dev/null @@ -1,73 +0,0 @@ - -# Gerber (RS-274X or Extended Gerber) is a bilevel, resolution independent image format. - -# // graphic objects -# // draw: line segment, thickness, round or square line endings. (solid circle and rectangule apertures only) -# // arc: circular arc, thickness, round endings. (solid circle standard aperture only) -# // flash: replication of a given apertura (shape) -# // region: are defined by a countour (linear/arc segments.) -# -# // draw/arc: can have zero length (just flash the aperture) -# // flash: any aperture can be flashed -# -# // operation codes operates on coordinate data blocks. each operation code is for one coordinate data block pair and vice-versa. -# // D01: stroke an aperture from current point to coordinate pair. region mode off. lights-on move. -# // D02: move current point to this coordinate pair -# // D03: flash current aperture at this coordinate pair. -# -# // graphics state -# // all state controlled by codes and parameters, except current point -# // -# // state fixed? initial value -# // coordinate format fixed undefined -# // unit fixed undefined -# // image polarity fixed positive -# // steps/repeat variable 1,1,-,- -# // level polarity variable dark -# // region mode variable off -# // current aperture variable undefined -# // quadrant mode variable undefined -# // interpolation mode variable undefined -# // current point variable (0,0) -# -# // attributes: metadata, both standard and custom. No change on image. -# -# // G01: linear -# // G04: comment -# // M02: end of file -# // D: select aperture -# // G75: multi quadrant mode (circles) -# // G36: region begin -# // G37: region end -# -# // [G01] [Xnnfffff] [Ynnffff] D01* -# -# // ASCII 32-126, CR LF. -# // * end-of-block -# // % parameer delimiter -# // , field separator -# // only in comments -# // case sensitive -# -# // int: +/- 32 bit signed -# // decimal: +/- digits -# // names: [a-zA-Z_$]{[a-zA-Z_$0-9]+} (255) -# // strings: [a-zA-Z0-9_+-/!?<>”’(){}.\|&@# ]+ (65535) -# -# // data block: end in * -# // statement: one or more data block, if contain parameters starts and end in % (parameter statement) -# // statement: [%]{}[%] -# // statements: function code, coordinate data, parameters -# -# // function code: operation codes (D01..) or code that set state. -# // function codes applies before operation codes act on coordinates -# -# // coordinate data: : [X][Y][I][J](D01|D02|D03) -# // offsets are not modal -# -# // parameter: %Parameter code[optional modifiers]*% -# // code: 2 characters -# -# // parameters can have line separators: %{{}}% -# -# // function code: (GDM){1}[number], parameters: [AZ]{2} \ No newline at end of file diff --git a/gerber/__main__.py b/gerber/__main__.py index 1af4c0f..ab0f377 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -30,7 +30,6 @@ if __name__ == '__main__': print "parsing %s" % filename gerberfile = read(filename) gerberfile.render(ctx) - ctx.set_color(tuple([color * 0.4 for color in ctx.color])) print('Saving image to test.svg') ctx.dump('test.svg') diff --git a/gerber/common.py b/gerber/common.py index 0092ec8..6e8c862 100644 --- a/gerber/common.py +++ b/gerber/common.py @@ -30,12 +30,12 @@ def read(filename): CncFile object representing the file, either GerberFile or ExcellonFile. Returns None if file is not an Excellon or Gerber file. """ - import gerber + import rs274x import excellon from utils import detect_file_format fmt = detect_file_format(filename) if fmt == 'rs274x': - return gerber.read(filename) + return rs274x.read(filename) elif fmt == 'excellon': return excellon.read(filename) else: diff --git a/gerber/excellon.py b/gerber/excellon.py index f5d6c29..13aacc6 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -39,6 +39,7 @@ def read(filename): ------- file : :class:`gerber.excellon.ExcellonFile` An ExcellonFile created from the specified file. + """ detected_settings = detect_excellon_format(filename) settings = FileSettings(**detected_settings) @@ -317,7 +318,7 @@ def detect_excellon_format(filename): hole_area = 0.0 for hit in p.hits: tool = hit[0] - hole_area += math.pow(math.pi * tool.diameter, 2) + hole_area += math.pow(math.pi * tool.diameter / 2., 2) results[key] = (size, p.hole_count, hole_area) except: pass diff --git a/gerber/gerber.py b/gerber/gerber.py deleted file mode 100644 index 335b443..0000000 --- a/gerber/gerber.py +++ /dev/null @@ -1,332 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 Hamilton Kibbe -# Modified from parser.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. -""" This module provides an RS-274-X class and parser. -""" - - -import re -import json -from .gerber_statements import * -from .cam import CamFile, FileSettings - - - - -def read(filename): - """ Read data from filename and return a GerberFile - - Parameters - ---------- - filename : string - Filename of file to parse - - Returns - ------- - file : :class:`gerber.gerber.GerberFile` - A GerberFile created from the specified file. - """ - return GerberParser().parse(filename) - - -class GerberFile(CamFile): - """ A class representing a single gerber file - - The GerberFile class represents a single gerber file. - - Parameters - ---------- - statements : list - list of gerber file statements - - settings : dict - Dictionary of gerber file settings - - filename : string - Filename of the source gerber file - - Attributes - ---------- - comments: list of strings - List of comments contained in the gerber file. - - size : tuple, (, ) - Size in [self.units] of the layer described by the gerber file. - - bounds: tuple, ((, ), (, )) - boundaries of the layer described by the gerber file. - `bounds` is stored as ((min x, max x), (min y, max y)) - - """ - def __init__(self, statements, settings, filename=None): - super(GerberFile, self).__init__(statements, settings, filename) - - @property - def comments(self): - return [comment.comment for comment in self.statements - if isinstance(comment, CommentStmt)] - - @property - def size(self): - xbounds, ybounds = self.bounds - return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) - - @property - def bounds(self): - xbounds = [0.0, 0.0] - ybounds = [0.0, 0.0] - for stmt in [stmt for stmt in self.statements - if isinstance(stmt, CoordStmt)]: - if stmt.x is not None and stmt.x < xbounds[0]: - xbounds[0] = stmt.x - if stmt.x is not None and stmt.x > xbounds[1]: - xbounds[1] = stmt.x - if stmt.i is not None and stmt.i < xbounds[0]: - xbounds[0] = stmt.i - if stmt.i is not None and stmt.i > xbounds[1]: - xbounds[1] = stmt.i - if stmt.y is not None and stmt.y < ybounds[0]: - ybounds[0] = stmt.y - if stmt.y is not None and stmt.y > ybounds[1]: - ybounds[1] = stmt.y - if stmt.j is not None and stmt.j < ybounds[0]: - ybounds[0] = stmt.j - if stmt.j is not None and stmt.j > ybounds[1]: - ybounds[1] = stmt.j - return (xbounds, ybounds) - - def write(self, filename): - """ Write data out to a gerber file - """ - with open(filename, 'w') as f: - for statement in self.statements: - f.write(statement.to_gerber()) - - def render(self, ctx, filename=None): - """ Generate image of layer. - - Parameters - ---------- - ctx : :class:`GerberContext` - GerberContext subclass used for rendering the image - - filename : string - If provided, the rendered image will be saved to `filename` - """ - ctx.set_bounds(self.bounds) - for statement in self.statements: - ctx.evaluate(statement) - if filename is not None: - ctx.dump(filename) - - -class GerberParser(object): - """ GerberParser - """ - NUMBER = r"[\+-]?\d+" - DECIMAL = r"[\+-]?\d+([.]?\d+)?" - STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" - NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" - FUNCTION = r"G\d{2}" - - COORD_OP = r"D[0]?[123]" - - FS = r"(?PFS)(?P(L|T))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" - MO = r"(?PMO)(?P(MM|IN))" - IP = r"(?PIP)(?P(POS|NEG))" - LP = r"(?PLP)(?P(D|C))" - AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" - AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" - AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" - AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" - AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) - AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) - - # begin deprecated - OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) - IN = r"(?PIN)(?P.*)" - LN = r"(?PLN)(?P.*)" - # end deprecated - - PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) - PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] - - COORD_STMT = re.compile(( - r"(?P{function})?" - r"(X(?P{number}))?(Y(?P{number}))?" - r"(I(?P{number}))?(J(?P{number}))?" - r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) - - APERTURE_STMT = re.compile(r"(G54)?D(?P\d+)\*") - - COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") - - EOF_STMT = re.compile(r"(?PM02)\*") - - REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') - QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') - - def __init__(self): - self.settings = FileSettings() - self.statements = [] - - def parse(self, filename): - fp = open(filename, "r") - data = fp.readlines() - - for stmt in self._parse(data): - self.statements.append(stmt) - - return GerberFile(self.statements, self.settings, filename) - - def dump_json(self): - stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} - return json.dumps(stmts) - - def dump_str(self): - s = "" - for stmt in self.statements: - s += str(stmt) + "\n" - return s - - def _parse(self, data): - oldline = '' - - for i, line in enumerate(data): - line = oldline + line.strip() - - # skip empty lines - if not len(line): - continue - - # deal with multi-line parameters - if line.startswith("%") and not line.endswith("%"): - oldline = line - continue - - did_something = True # make sure we do at least one loop - while did_something and len(line) > 0: - did_something = False - - # Region Mode - (mode, r) = self._match_one(self.REGION_MODE_STMT, line) - if mode: - yield RegionModeStmt.from_gerber(line) - line = r - did_something = True - continue - - # Quadrant Mode - (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) - if mode: - yield QuadrantModeStmt.from_gerber(line) - line = r - did_something = True - continue - - # coord - (coord, r) = self._match_one(self.COORD_STMT, line) - if coord: - yield CoordStmt.from_dict(coord, self.settings) - line = r - did_something = True - continue - - # aperture selection - (aperture, r) = self._match_one(self.APERTURE_STMT, line) - if aperture: - yield ApertureStmt(**aperture) - - did_something = True - line = r - continue - - # comment - (comment, r) = self._match_one(self.COMMENT_STMT, line) - if comment: - yield CommentStmt(comment["comment"]) - did_something = True - line = r - continue - - # parameter - (param, r) = self._match_one_from_many(self.PARAM_STMT, line) - if param: - if param["param"] == "FS": - stmt = FSParamStmt.from_dict(param) - self.settings.zero_suppression = stmt.zero_suppression - self.settings.format = stmt.format - self.settings.notation = stmt.notation - yield stmt - elif param["param"] == "MO": - stmt = MOParamStmt.from_dict(param) - self.settings.units = stmt.mode - yield stmt - elif param["param"] == "IP": - yield IPParamStmt.from_dict(param) - elif param["param"] == "LP": - yield LPParamStmt.from_dict(param) - elif param["param"] == "AD": - yield ADParamStmt.from_dict(param) - elif param["param"] == "AM": - yield AMParamStmt.from_dict(param) - elif param["param"] == "OF": - yield OFParamStmt.from_dict(param) - elif param["param"] == "IN": - yield INParamStmt.from_dict(param) - elif param["param"] == "LN": - yield LNParamStmt.from_dict(param) - else: - yield UnknownStmt(line) - did_something = True - line = r - continue - - # eof - (eof, r) = self._match_one(self.EOF_STMT, line) - if eof: - yield EofStmt() - did_something = True - line = r - continue - - if False: - print self.COORD_STMT.pattern - print self.APERTURE_STMT.pattern - print self.COMMENT_STMT.pattern - print self.EOF_STMT.pattern - for i in self.PARAM_STMT: - print i.pattern - - if line.find('*') > 0: - yield UnknownStmt(line) - oldline = line - - def _match_one(self, expr, data): - match = expr.match(data) - if match is None: - return ({}, None) - else: - return (match.groupdict(), data[match.end(0):]) - - def _match_one_from_many(self, exprs, data): - for expr in exprs: - match = expr.match(data) - if match: - return (match.groupdict(), data[match.end(0):]) - - return ({}, None) 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': diff --git a/gerber/rs274x.py b/gerber/rs274x.py new file mode 100644 index 0000000..4076f77 --- /dev/null +++ b/gerber/rs274x.py @@ -0,0 +1,327 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# Modified from parser.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. +""" This module provides an RS-274-X class and parser. +""" + + +import re +import json +from .gerber_statements import * +from .cam import CamFile, FileSettings + + + + +def read(filename): + """ Read data from filename and return a GerberFile + + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.rs274x.GerberFile` + A GerberFile created from the specified file. + """ + return GerberParser().parse(filename) + + +class GerberFile(CamFile): + """ A class representing a single gerber file + + The GerberFile class represents a single gerber file. + + Parameters + ---------- + statements : list + list of gerber file statements + + settings : dict + Dictionary of gerber file settings + + filename : string + Filename of the source gerber file + + Attributes + ---------- + comments: list of strings + List of comments contained in the gerber file. + + size : tuple, (, ) + Size in [self.units] of the layer described by the gerber file. + + bounds: tuple, ((, ), (, )) + boundaries of the layer described by the gerber file. + `bounds` is stored as ((min x, max x), (min y, max y)) + + """ + def __init__(self, statements, settings, filename=None): + super(GerberFile, self).__init__(statements, settings, filename) + + @property + def comments(self): + return [comment.comment for comment in self.statements + if isinstance(comment, CommentStmt)] + + @property + def size(self): + xbounds, ybounds = self.bounds + return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) + + @property + def bounds(self): + xbounds = [0.0, 0.0] + ybounds = [0.0, 0.0] + for stmt in [stmt for stmt in self.statements + if isinstance(stmt, CoordStmt)]: + if stmt.x is not None: + if stmt.x < xbounds[0]: + xbounds[0] = stmt.x + elif stmt.x > xbounds[1]: + xbounds[1] = stmt.x + if stmt.y is not None: + if stmt.y < ybounds[0]: + ybounds[0] = stmt.y + elif stmt.y > ybounds[1]: + ybounds[1] = stmt.y + return (xbounds, ybounds) + + + def write(self, filename): + """ Write data out to a gerber file + """ + with open(filename, 'w') as f: + for statement in self.statements: + f.write(statement.to_gerber()) + + def render(self, ctx, filename=None): + """ Generate image of layer. + + Parameters + ---------- + ctx : :class:`GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, the rendered image will be saved to `filename` + """ + ctx.set_bounds(self.bounds) + for statement in self.statements: + ctx.evaluate(statement) + if filename is not None: + ctx.dump(filename) + + +class GerberParser(object): + """ GerberParser + """ + NUMBER = r"[\+-]?\d+" + DECIMAL = r"[\+-]?\d+([.]?\d+)?" + STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" + NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" + FUNCTION = r"G\d{2}" + + COORD_OP = r"D[0]?[123]" + + FS = r"(?PFS)(?P(L|T))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" + MO = r"(?PMO)(?P(MM|IN))" + IP = r"(?PIP)(?P(POS|NEG))" + LP = r"(?PLP)(?P(D|C))" + AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" + AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" + AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" + AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" + AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) + AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) + + # begin deprecated + OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) + IN = r"(?PIN)(?P.*)" + LN = r"(?PLN)(?P.*)" + # end deprecated + + PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) + PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] + + COORD_STMT = re.compile(( + r"(?P{function})?" + r"(X(?P{number}))?(Y(?P{number}))?" + r"(I(?P{number}))?(J(?P{number}))?" + r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) + + APERTURE_STMT = re.compile(r"(G54)?D(?P\d+)\*") + + COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") + + EOF_STMT = re.compile(r"(?PM02)\*") + + REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') + QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') + + def __init__(self): + self.settings = FileSettings() + self.statements = [] + + def parse(self, filename): + fp = open(filename, "r") + data = fp.readlines() + + for stmt in self._parse(data): + self.statements.append(stmt) + + return GerberFile(self.statements, self.settings, filename) + + def dump_json(self): + stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} + return json.dumps(stmts) + + def dump_str(self): + s = "" + for stmt in self.statements: + s += str(stmt) + "\n" + return s + + def _parse(self, data): + oldline = '' + + for i, line in enumerate(data): + line = oldline + line.strip() + + # skip empty lines + if not len(line): + continue + + # deal with multi-line parameters + if line.startswith("%") and not line.endswith("%"): + oldline = line + continue + + did_something = True # make sure we do at least one loop + while did_something and len(line) > 0: + did_something = False + + # Region Mode + (mode, r) = self._match_one(self.REGION_MODE_STMT, line) + if mode: + yield RegionModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # Quadrant Mode + (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) + if mode: + yield QuadrantModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # coord + (coord, r) = self._match_one(self.COORD_STMT, line) + if coord: + yield CoordStmt.from_dict(coord, self.settings) + line = r + did_something = True + continue + + # aperture selection + (aperture, r) = self._match_one(self.APERTURE_STMT, line) + if aperture: + yield ApertureStmt(**aperture) + + did_something = True + line = r + continue + + # comment + (comment, r) = self._match_one(self.COMMENT_STMT, line) + if comment: + yield CommentStmt(comment["comment"]) + did_something = True + line = r + continue + + # parameter + (param, r) = self._match_one_from_many(self.PARAM_STMT, line) + if param: + if param["param"] == "FS": + stmt = FSParamStmt.from_dict(param) + self.settings.zero_suppression = stmt.zero_suppression + self.settings.format = stmt.format + self.settings.notation = stmt.notation + yield stmt + elif param["param"] == "MO": + stmt = MOParamStmt.from_dict(param) + self.settings.units = stmt.mode + yield stmt + elif param["param"] == "IP": + yield IPParamStmt.from_dict(param) + elif param["param"] == "LP": + yield LPParamStmt.from_dict(param) + elif param["param"] == "AD": + yield ADParamStmt.from_dict(param) + elif param["param"] == "AM": + yield AMParamStmt.from_dict(param) + elif param["param"] == "OF": + yield OFParamStmt.from_dict(param) + elif param["param"] == "IN": + yield INParamStmt.from_dict(param) + elif param["param"] == "LN": + yield LNParamStmt.from_dict(param) + else: + yield UnknownStmt(line) + did_something = True + line = r + continue + + # eof + (eof, r) = self._match_one(self.EOF_STMT, line) + if eof: + yield EofStmt() + did_something = True + line = r + continue + + if False: + print self.COORD_STMT.pattern + print self.APERTURE_STMT.pattern + print self.COMMENT_STMT.pattern + print self.EOF_STMT.pattern + for i in self.PARAM_STMT: + print i.pattern + + if line.find('*') > 0: + yield UnknownStmt(line) + oldline = line + + def _match_one(self, expr, data): + match = expr.match(data) + if match is None: + return ({}, None) + else: + return (match.groupdict(), data[match.end(0):]) + + def _match_one_from_many(self, exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return (match.groupdict(), data[match.end(0):]) + + return ({}, None) -- cgit