added ExcellonFile class

This commit is contained in:
Hamilton Kibbe 2014-09-28 21:17:13 -04:00
parent 695e3d9220
commit 3a5dbcf1e1
10 changed files with 345 additions and 265 deletions

View file

@ -2,6 +2,7 @@
PYTHON ?= python
NOSETESTS ?= nosetests
DOC_ROOT = doc
clean:
#$(PYTHON) setup.py clean
@ -15,3 +16,11 @@ test-coverage:
rm -rf coverage .coverage
$(NOSETESTS) -s -v --with-coverage gerber
doc-html:
(cd $(DOC_ROOT); make html)
doc-clean:
(cd $(DOC_ROOT); make clean)

View file

@ -14,3 +14,18 @@
# 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.
def read(filename):
""" Read a gerber or excellon file and return a representative object.
"""
import gerber
import excellon
from utils import detect_file_format
fmt = detect_file_format(filename)
if fmt == 'rs274x':
return gerber.read(filename)
elif fmt == 'excellon':
return excellon.read(filename)
else:
return None

View file

@ -10,10 +10,10 @@
# 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.
# 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.
if __name__ == '__main__':
from .gerber import GerberFile
@ -34,5 +34,3 @@ if __name__ == '__main__':
p = ExcellonParser(ctx)
p.parse('ncdrill.txt')
p.dump('testwithdrill.svg')

View file

@ -2,19 +2,60 @@
import re
from itertools import tee, izip
from .utils import parse_gerber_value
INCH = 0
METRIC = 1
ABSOLUTE = 0
INCREMENTAL = 1
LZ = 0
TZ = 1
def read(filename):
""" Read data from filename and return an ExcellonFile
"""
return ExcellonParser().parse(filename)
class ExcellonFile(object):
""" A class representing a single excellon file
The ExcellonFile class represents a single excellon file.
Parameters
----------
tools : list
list of gerber file statements
hits : list of tuples
list of drill hits as (<Tool>, (x, y))
settings : dict
Dictionary of gerber file settings
filename : string
Filename of the source gerber file
Attributes
----------
units : string
either 'inch' or 'metric'.
"""
def __init__(self, tools, hits, settings, filename):
self.tools = tools
self.hits = hits
self.settings = settings
self.filename = filename
def report(self):
""" Print drill report
"""
pass
def render(self, filename, ctx):
""" Generate image of file
"""
for tool, pos in self.hits:
ctx.drill(pos[0], pos[1], tool.diameter)
ctx.dump(filename)
class Tool(object):
""" Excellon Tool class
"""
@classmethod
def from_line(cls, line, settings):
commands = re.split('([BCFHSTZ])', line)[1:]
@ -38,7 +79,7 @@ class Tool(object):
elif cmd == 'Z':
args['depth_offset'] = parse_gerber_value(val, format, zero_suppression)
return cls(settings, **args)
def __init__(self, settings, **kwargs):
self.number = kwargs.get('number')
self.feed_rate = kwargs.get('feed_rate')
@ -47,79 +88,83 @@ class Tool(object):
self.diameter = kwargs.get('diameter')
self.max_hit_count = kwargs.get('max_hit_count')
self.depth_offset = kwargs.get('depth_offset')
self.units = settings.get('units', INCH)
self.units = settings.get('units', 'inch')
def __repr__(self):
unit = 'in.' if self.units == INCH else 'mm'
unit = 'in.' if self.units == 'inch' else 'mm'
return '<Tool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
class ExcellonParser(object):
def __init__(self, ctx=None):
self.ctx=ctx
self.ctx = ctx
self.notation = 'absolute'
self.units = 'inch'
self.zero_suppression = 'trailing'
self.format = (2,5)
self.format = (2, 5)
self.state = 'INIT'
self.tools = {}
self.tools = []
self.hits = []
self.active_tool = None
self.pos = [0., 0.]
if ctx is not None:
self.ctx.set_coord_format(zero_suppression='trailing', format=[2,5], notation='absolute')
self.ctx.set_coord_format(zero_suppression='trailing',
format=(2, 5), notation='absolute')
def parse(self, filename):
with open(filename, 'r') as f:
for line in f:
self._parse(line)
settings = {'notation': self.notation, 'units': self.units,
'zero_suppression': self.zero_suppression,
'format': self.format}
return ExcellonFile(self.tools, self.hits, settings, filename)
def dump(self, filename):
self.ctx.dump(filename)
def _parse(self, line):
if 'M48' in line:
self.state = 'HEADER'
if 'G00' in line:
self.state = 'ROUT'
if 'G05' in line:
self.state = 'DRILL'
elif line[0] == '%' and self.state == 'HEADER':
self.state = 'DRILL'
if 'INCH' in line or line.strip() == 'M72':
self.units = 'INCH'
self.units = 'inch'
elif 'METRIC' in line or line.strip() == 'M71':
self.units = 'METRIC'
self.units = 'metric'
if 'LZ' in line:
self.zeros = 'L'
elif 'TZ' in line:
self.zeros = 'T'
if 'ICI' in line and 'ON' in line or line.strip() == 'G91':
self.notation = 'incremental'
if 'ICI' in line and 'OFF' in line or line.strip() == 'G90':
self.notation = 'incremental'
zs = self._settings()['zero_suppression']
fmt = self._settings()['format']
# tool definition
if line[0] == 'T' and self.state == 'HEADER':
tool = Tool.from_line(line,self._settings())
tool = Tool.from_line(line, self._settings())
self.tools[tool.number] = tool
elif line[0] == 'T' and self.state != 'HEADER':
self.active_tool = self.tools[int(line.strip().split('T')[1])]
if line[0] in ['X', 'Y']:
x = None
y = None
@ -127,10 +172,9 @@ class ExcellonParser(object):
splitline = line.strip('X').split('Y')
x = parse_gerber_value(splitline[0].strip(), fmt, zs)
if len(splitline) == 2:
y = parse_gerber_value(splitline[1].strip(), fmt,zs)
y = parse_gerber_value(splitline[1].strip(), fmt, zs)
else:
y = parse_gerber_value(line.strip(' Y'), fmt,zs)
y = parse_gerber_value(line.strip(' Y'), fmt, zs)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
@ -146,20 +190,17 @@ class ExcellonParser(object):
if self.ctx is not None:
self.ctx.drill(self.pos[0], self.pos[1],
self.active_tool.diameter)
def _settings(self):
return {'units':self.units, 'zero_suppression':self.zero_suppression,
return {'units': self.units, 'zero_suppression': self.zero_suppression,
'format': self.format}
def pairwise(iterator):
itr = iter(iterator)
while True:
yield tuple([itr.next() for i in range(2)])
if __name__ == '__main__':
tools = []
settings = {'units':INCH, 'zeros':LZ}
p = parser()
p.parse('examples/ncdrill.txt')

View file

@ -3,7 +3,7 @@
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com>
#
#
# 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
@ -15,33 +15,41 @@
# 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.gerber
============
**Gerber File module**
This module provides an RS-274-X class and parser
"""
import re
import json
from .statements import *
def read(filename):
""" Read data from filename and return a GerberFile
"""
return GerberParser().parse(filename)
class GerberFile(object):
""" A class representing a single gerber file
The GerberFile class represents a single gerber file.
The GerberFile class represents a single gerber file.
Parameters
----------
filename : string
Parameter.
zero_suppression : string
Zero-suppression mode. May be either 'leading' or 'trailing'
statements : list
list of gerber file statements
notation : string
Notation mode. May be either 'absolute' or 'incremental'
format : tuple (int, int)
Gerber precision format expressed as a tuple containing:
(number of integer-part digits, number of decimal-part digits)
settings : dict
Dictionary of gerber file settings
filename : string
Filename of the source gerber file
Attributes
----------
@ -50,7 +58,7 @@ class GerberFile(object):
units : string
either 'inch' or 'metric'.
size : tuple, (<float>, <float>)
Size in [self.units] of the layer described by the gerber file.
@ -59,32 +67,25 @@ class GerberFile(object):
`bounds` is stored as ((min x, max x), (min y, max y))
"""
@classmethod
def read(cls, filename):
""" Read data from filename and return a GerberFile
"""
return GerberParser().parse(filename)
def __init__(self, statements, settings, filename=None):
self.filename = filename
self.statements = statements
self.settings = settings
@property
def comments(self):
return [comment.comment for comment in self.statements
if isinstance(comment, CommentStmt)]
@property
def units(self):
return self.settings['units']
@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]
@ -106,9 +107,8 @@ class GerberFile(object):
ybounds[0] = stmt.j
if stmt.j is not None and stmt.j > ybounds[1]:
ybounds[1] = stmt.j
return (xbounds, ybounds)
return (xbounds, ybounds)
def write(self, filename):
""" Write data out to a gerber file
"""
@ -123,8 +123,7 @@ class GerberFile(object):
for statement in self.statements:
ctx.evaluate(statement)
ctx.dump(filename)
class GerberParser(object):
""" GerberParser
@ -179,7 +178,7 @@ class GerberParser(object):
for stmt in self._parse(data):
self.statements.append(stmt)
return GerberFile(self.statements, self.settings, filename)
def dump_json(self):
@ -197,7 +196,7 @@ class GerberParser(object):
for i, line in enumerate(data):
line = oldline + line.strip()
# skip empty lines
if not len(line):
continue
@ -207,10 +206,10 @@ class GerberParser(object):
oldline = line
continue
did_something = True # make sure we do at least one loop
did_something = True # make sure we do at least one loop
while did_something and len(line) > 0:
did_something = False
# coord
(coord, r) = self._match_one(self.COORD_STMT, line)
if coord:
@ -223,7 +222,7 @@ class GerberParser(object):
(aperture, r) = self._match_one(self.APERTURE_STMT, line)
if aperture:
yield ApertureStmt(**aperture)
did_something = True
line = r
continue
@ -240,7 +239,7 @@ class GerberParser(object):
(param, r) = self._match_one_from_many(self.PARAM_STMT, line)
if param:
if param["param"] == "FS":
stmt = FSParamStmt.from_dict(param)
stmt = FSParamStmt.from_dict(param)
self.settings = {'zero_suppression': stmt.zero_suppression,
'format': stmt.format,
'notation': stmt.notation}
@ -276,7 +275,7 @@ class GerberParser(object):
did_something = True
line = r
continue
if False:
print self.COORD_STMT.pattern
print self.APERTURE_STMT.pattern

View file

@ -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
pass

View file

@ -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

View file

@ -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)

View file

@ -3,18 +3,18 @@
"""
gerber.statements
=================
**Gerber file statement classes **
**Gerber file statement classes**
"""
from .utils import parse_gerber_value, write_gerber_value, decimal_string
__all__ = ['FSParamStmt', 'MOParamStmt','IPParamStmt', 'OFParamStmt',
__all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt',
'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt',
'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt',
'EofStmt', 'UnknownStmt']
class Statement(object):
def __init__(self, type):
self.type = type
@ -38,15 +38,15 @@ class ParamStmt(Statement):
class FSParamStmt(ParamStmt):
""" FS - Gerber Format Specification Statement
"""
@classmethod
def from_dict(cls, stmt_dict):
"""
"""
"""
param = stmt_dict.get('param').strip()
zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing'
notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental'
x = map(int,stmt_dict.get('x').strip())
x = map(int, stmt_dict.get('x').strip())
format = (x[0], x[1])
if notation == 'incremental':
print('This file uses incremental notation. To quote the gerber \
@ -54,36 +54,36 @@ class FSParamStmt(ParamStmt):
endless confusion. Always use absolute notation.\n\nYou \
have been warned')
return cls(param, zeros, notation, format)
def __init__(self, param, zero_suppression='leading',
notation='absolute', format=(2,4)):
notation='absolute', format=(2, 4)):
""" Initialize FSParamStmt class
.. note::
The FS command specifies the format of the coordinate data. It
must only be used once at the beginning of a file. It must be
specified before the first use of coordinate data.
Parameters
----------
param : string
Parameter.
zero_suppression : string
Zero-suppression mode. May be either 'leading' or 'trailing'
notation : string
Notation mode. May be either 'absolute' or 'incremental'
format : tuple (int, int)
Gerber precision format expressed as a tuple containing:
Gerber precision format expressed as a tuple containing:
(number of integer-part digits, number of decimal-part digits)
Returns
-------
ParamStmt : FSParamStmt
Initialized FSParamStmt class.
"""
ParamStmt.__init__(self, param)
self.zero_suppression = zero_suppression
@ -93,7 +93,7 @@ class FSParamStmt(ParamStmt):
def to_gerber(self):
zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T'
notation = 'A' if self.notation == 'absolute' else 'I'
format = ''.join(map(str,self.format))
format = ''.join(map(str, self.format))
return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation,
format, format)
@ -104,23 +104,23 @@ class FSParamStmt(ParamStmt):
class MOParamStmt(ParamStmt):
""" MO - Gerber Mode (measurement units) Statement.
""" MO - Gerber Mode (measurement units) Statement.
"""
@classmethod
def from_dict(cls, stmt_dict):
param = stmt_dict.get('param')
mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric'
return cls(param, mo)
def __init__(self, param, mo):
""" Initialize MOParamStmt class
Parameters
----------
param : string
Parameter.
mo : string
Measurement units. May be either 'inch' or 'metric'
@ -128,11 +128,11 @@ class MOParamStmt(ParamStmt):
-------
ParamStmt : MOParamStmt
Initialized MOParamStmt class.
"""
ParamStmt.__init__(self, param)
self.mode = mo
def to_gerber(self):
mode = 'MM' if self.mode == 'metric' else 'IN'
return '%MO{0}*%'.format(mode)
@ -140,7 +140,7 @@ class MOParamStmt(ParamStmt):
def __str__(self):
mode_str = 'millimeters' if self.mode == 'metric' else 'inches'
return ('<Mode: %s>' % mode_str)
class IPParamStmt(ParamStmt):
""" IP - Gerber Image Polarity Statement. (Deprecated)
@ -150,15 +150,15 @@ class IPParamStmt(ParamStmt):
param = stmt_dict.get('param')
ip = 'positive' if stmt_dict.get('ip') == 'POS' else 'negative'
return cls(param, ip)
def __init__(self, param, ip):
""" Initialize IPParamStmt class
Parameters
----------
param : string
Parameter string.
ip : string
Image polarity. May be either'positive' or 'negative'
@ -166,12 +166,11 @@ class IPParamStmt(ParamStmt):
-------
ParamStmt : IPParamStmt
Initialized IPParamStmt class.
"""
ParamStmt.__init__(self, param)
self.ip = ip
def to_gerber(self):
ip = 'POS' if self.ip == 'positive' else 'negative'
return '%IP{0}*%'.format(ip)
@ -183,33 +182,33 @@ class IPParamStmt(ParamStmt):
class OFParamStmt(ParamStmt):
""" OF - Gerber Offset statement (Deprecated)
"""
@classmethod
def from_dict(cls, stmt_dict):
param = stmt_dict.get('param')
a = float(stmt_dict.get('a'))
b = float(stmt_dict.get('b'))
return cls(param, a, b)
def __init__(self, param, a, b):
""" Initialize OFParamStmt class
Parameters
----------
param : string
Parameter
a : float
Offset along the output device A axis
b : float
Offset along the output device B axis
Returns
-------
ParamStmt : OFParamStmt
Initialized OFParamStmt class.
"""
ParamStmt.__init__(self, param)
self.a = a
@ -231,24 +230,25 @@ class OFParamStmt(ParamStmt):
offset_str += ('Y: %f' % self.b)
return ('<Offset: %s>' % offset_str)
class LPParamStmt(ParamStmt):
""" LP - Gerber Level Polarity statement
"""
@classmethod
def from_dict(cls, stmt_dict):
param = stmt_dict.get('lp')
lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark'
lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark'
return cls(param, lp)
def __init__(self, param, lp):
""" Initialize LPParamStmt class
Parameters
----------
param : string
Parameter
lp : string
Level polarity. May be either 'clear' or 'dark'
@ -256,12 +256,11 @@ class LPParamStmt(ParamStmt):
-------
ParamStmt : LPParamStmt
Initialized LPParamStmt class.
"""
ParamStmt.__init__(self, param)
self.lp = lp
def to_gerber(self, settings):
lp = 'C' if self.lp == 'clear' else 'dark'
return '%LP{0}*%'.format(self.lp)
@ -273,7 +272,7 @@ class LPParamStmt(ParamStmt):
class ADParamStmt(ParamStmt):
""" AD - Gerber Aperture Definition Statement
"""
@classmethod
def from_dict(cls, stmt_dict):
param = stmt_dict.get('param')
@ -282,38 +281,36 @@ class ADParamStmt(ParamStmt):
modifiers = stmt_dict.get('modifiers')
if modifiers is not None:
modifiers = [[float(x) for x in m.split('X')]
for m in modifiers.split(',')]
for m in modifiers.split(',')]
return cls(param, d, shape, modifiers)
def __init__(self, param, d, shape, modifiers):
""" Initialize ADParamStmt class
Parameters
----------
param : string
Parameter code
d : int
Aperture D-code
shape : string
aperture name
modifiers : list of lists of floats
Shape modifiers
Returns
-------
ParamStmt : LPParamStmt
Initialized LPParamStmt class.
"""
ParamStmt.__init__(self, param)
self.d = d
self.shape = shape
self.modifiers = modifiers
self.modifiers = modifiers
def to_gerber(self, settings):
return '%ADD{0}{1},{2}*%'.format(self.d, self.shape,
@ -328,72 +325,72 @@ class ADParamStmt(ParamStmt):
shape = 'oblong'
else:
shape = self.shape
return '<Aperture Definition: %d: %s>' % (self.d, shape)
class AMParamStmt(ParamStmt):
""" AM - Aperture Macro Statement
"""
@classmethod
def from_dict(cls, stmt_dict):
return cls(**stmt_dict)
def __init__(self, param, name, macro):
""" Initialize AMParamStmt class
Parameters
----------
param : string
Parameter code
name : string
Aperture macro name
macro : string
Aperture macro string
Returns
-------
ParamStmt : AMParamStmt
Initialized AMParamStmt class.
"""
ParamStmt.__init__(self, param)
self.name = name
self.macro = macro
def to_gerber(self):
return '%AM{0}*{1}*%'.format(self.name, self.macro)
def __str__(self):
return '<Aperture Macro %s: %s>' % (self.name, macro)
class INParamStmt(ParamStmt):
""" IN - Image Name Statement
"""
@classmethod
def from_dict(cls, stmt_dict):
return cls(**stmt_dict)
def __init__(self, param, name):
""" Initialize INParamStmt class
Parameters
----------
param : string
Parameter code
name : string
Image name
Returns
-------
ParamStmt : INParamStmt
Initialized INParamStmt class.
"""
ParamStmt.__init__(self, param)
self.name = name
@ -404,29 +401,30 @@ class INParamStmt(ParamStmt):
def __str__(self):
return '<Image Name: %s>' % self.name
class LNParamStmt(ParamStmt):
""" LN - Level Name Statement (Deprecated)
"""
@classmethod
def from_dict(cls, stmt_dict):
return cls(**stmt_dict)
def __init__(self, param, name):
""" Initialize LNParamStmt class
Parameters
----------
param : string
Parameter code
name : string
Level name
Returns
-------
ParamStmt : LNParamStmt
Initialized LNParamStmt class.
"""
ParamStmt.__init__(self, param)
self.name = name
@ -437,10 +435,11 @@ class LNParamStmt(ParamStmt):
def __str__(self):
return '<Level Name: %s>' % self.name
class CoordStmt(Statement):
""" Coordinate Data Block
"""
@classmethod
def from_dict(cls, stmt_dict, settings):
zeros = settings['zero_suppression']
@ -451,7 +450,7 @@ class CoordStmt(Statement):
i = stmt_dict.get('i')
j = stmt_dict.get('j')
op = stmt_dict.get('op')
if x is not None:
x = parse_gerber_value(stmt_dict.get('x'),
format, zeros)
@ -465,39 +464,38 @@ class CoordStmt(Statement):
j = parse_gerber_value(stmt_dict.get('j'),
format, zeros)
return cls(function, x, y, i, j, op, settings)
def __init__(self, function, x, y, i, j, op, settings):
""" Initialize CoordStmt class
Parameters
----------
function : string
function
x : float
X coordinate
y : float
Y coordinate
Y coordinate
i : float
Coordinate offset in the X direction
j : float
Coordinate offset in the Y direction
op : string
Operation code
settings : dict {'zero_suppression', 'format'}
Gerber file coordinate format
Gerber file coordinate format
Returns
-------
Statement : CoordStmt
Initialized CoordStmt class.
"""
Statement.__init__(self, "COORD")
self.zero_suppression = settings['zero_suppression']
@ -509,7 +507,6 @@ class CoordStmt(Statement):
self.j = j
self.op = op
def to_gerber(self):
ret = ''
if self.function:
@ -518,7 +515,7 @@ class CoordStmt(Statement):
ret += 'X{0}'.format(write_gerber_value(self.x, self.zeros,
self.format))
if self.y:
ret += 'Y{0}'.format(write_gerber_value(self.y,self. zeros,
ret += 'Y{0}'.format(write_gerber_value(self.y, self. zeros,
self.format))
if self.i:
ret += 'I{0}'.format(write_gerber_value(self.i, self.zeros,
@ -552,7 +549,7 @@ class CoordStmt(Statement):
else:
op = self.op
coord_str += 'Op: %s' % op
return '<Coordinate Statement: %s>' % coord_str
@ -562,20 +559,21 @@ class ApertureStmt(Statement):
def __init__(self, d):
Statement.__init__(self, "APERTURE")
self.d = int(d)
def to_gerber(self):
return 'G54D{0}*'.format(self.d)
def __str__(self):
return '<Aperture: %d>' % self.d
class CommentStmt(Statement):
""" Comment Statment
"""
def __init__(self, comment):
Statement.__init__(self, "COMMENT")
self.comment = comment
def to_gerber(self):
return 'G04{0}*'.format(self.comment)
@ -594,12 +592,11 @@ class EofStmt(Statement):
def __str__(self):
return '<EOF Statement>'
class UnknownStmt(Statement):
""" Unknown Statement
"""
def __init__(self, line):
Statement.__init__(self, "UNKNOWN")
self.line = line

View file

@ -10,28 +10,29 @@ files.
"""
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
# License:
# License:
def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
""" Convert gerber/excellon formatted string to floating-point number
.. note::
Format and zero suppression are configurable. Note that the Excellon
and Gerber formats use opposite terminology with respect to leading
and trailing zeros. The Gerber format specifies which zeros are
suppressed, while the Excellon format specifies which zeros are
included. This function uses the Gerber-file convention, so an
Excellon file in LZ (leading zeros) mode would use
`zero_suppression='trailing'`
Format and zero suppression are configurable. Note that the Excellon
and Gerber formats use opposite terminology with respect to leading
and trailing zeros. The Gerber format specifies which zeros are
suppressed, while the Excellon format specifies which zeros are
included. This function uses the Gerber-file convention, so an
Excellon file in LZ (leading zeros) mode would use
`zero_suppression='trailing'`
Parameters
----------
value : string
A Gerber/Excellon-formatted string representing a numerical value.
format : tuple (int,int)
Gerber/Excellon precision format expressed as a tuple containing:
Gerber/Excellon precision format expressed as a tuple containing:
(number of integer-part digits, number of decimal-part digits)
zero_suppression : string
@ -41,12 +42,12 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
-------
value : float
The specified value as a floating-point number.
"""
# Format precision
integer_digits, decimal_digits = format
MAX_DIGITS = integer_digits + decimal_digits
# Absolute maximum number of digits supported. This will handle up to
# 6:7 format, which is somewhat supported, even though the gerber spec
# only allows up to 6:6
@ -59,40 +60,39 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
negative = '-' in value
if negative:
value = value.strip(' -')
# Handle excellon edge case with explicit decimal. "That was easy!"
if '.' in value:
return float(value)
digits = [digit for digit in '0' * MAX_DIGITS]
offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value))
for i, digit in enumerate(value):
digits[i + offset] = digit
result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:]))
return -1.0 * result if negative else result
def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
""" Convert a floating point number to a Gerber/Excellon-formatted string.
.. note::
Format and zero suppression are configurable. Note that the Excellon
and Gerber formats use opposite terminology with respect to leading
and trailing zeros. The Gerber format specifies which zeros are
suppressed, while the Excellon format specifies which zeros are
included. This function uses the Gerber-file convention, so an
Excellon file in LZ (leading zeros) mode would use
Format and zero suppression are configurable. Note that the Excellon
and Gerber formats use opposite terminology with respect to leading
and trailing zeros. The Gerber format specifies which zeros are
suppressed, while the Excellon format specifies which zeros are
included. This function uses the Gerber-file convention, so an
Excellon file in LZ (leading zeros) mode would use
`zero_suppression='trailing'`
Parameters
----------
value : float
A floating point value.
format : tuple (n=2)
Gerber/Excellon precision format expressed as a tuple containing:
Gerber/Excellon precision format expressed as a tuple containing:
(number of integer-part digits, number of decimal-part digits)
zero_suppression : string
@ -106,12 +106,12 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
# Format precision
integer_digits, decimal_digits = format
MAX_DIGITS = integer_digits + decimal_digits
if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7:
raise ValueError('Parser only supports precision up to 6:7 format')
# negative sign affects padding, so deal with it at the end...
negative = value < 0.0
negative = value < 0.0
if negative:
value = -1.0 * value
@ -119,48 +119,72 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits)
digits = [val for val in fmtstring % value if val != '.']
# Suppression...
# Suppression...
if zero_suppression == 'trailing':
while digits[-1] == '0':
digits.pop()
else:
while digits[0] == '0':
digits.pop(0)
return ''.join(digits) if not negative else ''.join(['-'] + digits)
def decimal_string(value, precision=6):
""" Convert float to string with limited precision
Parameters
----------
value : float
A floating point value.
precision :
precision :
Maximum number of decimal places to print
Returns
-------
value : string
The specified value as a string.
"""
floatstr = '%0.20g' % value
integer = None
decimal = None
if '.' in floatstr:
integer, decimal = floatstr.split('.')
integer, decimal = floatstr.split('.')
elif ',' in floatstr:
integer, decimal = floatstr.split(',')
integer, decimal = floatstr.split(',')
if len(decimal) > precision:
decimal = decimal[:precision]
if integer or decimal:
return ''.join([integer, '.', decimal])
else:
return int(floatstr)
def detect_file_format(filename):
""" Determine format of a file
Parameters
----------
filename : string
Filename of the file to read.
Returns
-------
format : string
File format. either 'excellon' or 'rs274x'
"""
# Read the first 20 lines
with open(filename, 'r') as f:
lines = [next(f) for x in xrange(20)]
# Look for
for line in lines:
if 'M48' in line:
return 'excellon'
elif '%FS' in line:
return'rs274x'
return 'unknown'