summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile9
-rw-r--r--gerber/__init__.py15
-rw-r--r--gerber/__main__.py10
-rwxr-xr-xgerber/excellon.py133
-rw-r--r--gerber/gerber.py77
-rw-r--r--gerber/render/apertures.py9
-rw-r--r--gerber/render/render.py12
-rw-r--r--gerber/render/svg.py50
-rw-r--r--gerber/statements.py187
-rw-r--r--gerber/utils.py108
10 files changed, 345 insertions, 265 deletions
diff --git a/Makefile b/Makefile
index 3a5e411..162094c 100644
--- a/Makefile
+++ b/Makefile
@@ -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)
+
+
diff --git a/gerber/__init__.py b/gerber/__init__.py
index 0bf7c24..089d7b6 100644
--- a/gerber/__init__.py
+++ b/gerber/__init__.py
@@ -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
diff --git a/gerber/__main__.py b/gerber/__main__.py
index d32fa01..31b70f8 100644
--- a/gerber/__main__.py
+++ b/gerber/__main__.py
@@ -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')
-
-
diff --git a/gerber/excellon.py b/gerber/excellon.py
index fef5844..d92d57c 100755
--- a/gerber/excellon.py
+++ b/gerber/excellon.py
@@ -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 Tool(object):
+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')
-
- \ No newline at end of file
diff --git a/gerber/gerber.py b/gerber/gerber.py
index 31d9b82..949037b 100644
--- a/gerber/gerber.py
+++ b/gerber/gerber.py
@@ -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
----------
+ statements : list
+ list of gerber file statements
+
+ settings : dict
+ Dictionary of gerber file settings
+
filename : 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:
- (number of integer-part digits, number of decimal-part digits)
+ 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
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)
-
-
diff --git a/gerber/statements.py b/gerber/statements.py
index 53f7f78..418a852 100644
--- a/gerber/statements.py
+++ b/gerber/statements.py
@@ -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
- \ No newline at end of file
diff --git a/gerber/utils.py b/gerber/utils.py
index 35b4fd0..625a9e1 100644
--- a/gerber/utils.py
+++ b/gerber/utils.py
@@ -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'