Merge branch 'master' of https://github.com/hamiltonkibbe/gerber-tools
13
README.md
|
|
@ -5,8 +5,8 @@ gerber-tools
|
|||
|
||||
Tools to handle Gerber and Excellon files in Python.
|
||||
|
||||
Example:
|
||||
|
||||
Useage Example:
|
||||
---------------
|
||||
import gerber
|
||||
from gerber.render import GerberSvgContext
|
||||
|
||||
|
|
@ -20,3 +20,12 @@ Example:
|
|||
# Create SVG image
|
||||
top_copper.render(ctx)
|
||||
nc_drill.render(ctx, 'composite.svg')
|
||||
|
||||
|
||||
Rendering Examples:
|
||||
-------------------
|
||||
###Top Composite rendering
|
||||

|
||||
|
||||
###Bottom Composite rendering
|
||||

|
||||
3
TODO.md
|
|
@ -1,3 +0,0 @@
|
|||
* add command line utilities: gerber svg, gerber transform --rotate --scale --translate, gerber merge --blueprint
|
||||
|
||||
* AM defined apertures
|
||||
|
|
@ -30,6 +30,7 @@ sys.path.insert(0, os.path.abspath('../../'))
|
|||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.autosummary',
|
||||
'numpydoc',
|
||||
]
|
||||
|
||||
|
|
@ -85,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'
|
||||
|
|
|
|||
42
doc/source/documentation/excellon.rst
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
:mod:`excellon` --- Excellon file handling
|
||||
==============================================
|
||||
|
||||
.. module:: excellon
|
||||
:synopsis: Functions and classes for handling Excellon files
|
||||
.. sectionauthor:: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
|
||||
The Excellon format is the most common format for exporting PCB drill
|
||||
information. The Excellon format is used to program CNC drilling macines for
|
||||
drilling holes in PCBs. As such, excellon files are sometimes refererred to as
|
||||
NC-drill files. The Excellon format reference is available
|
||||
`here <http://www.excellon.com/manuals/program.htm>`_. The :mod:`excellon`
|
||||
submodule implements calsses to read and write excellon files without having
|
||||
to know the precise details of the format.
|
||||
|
||||
The :mod:`excellon` submodule's :func:`read` function serves as a
|
||||
simple interface for parsing excellon files. The :class:`ExcellonFile` class
|
||||
stores all the information contained in an Excellon file allowing the file to
|
||||
be analyzed, modified, and updated. The :class:`ExcellonParser` class is used
|
||||
in the background for parsing RS-274X files.
|
||||
|
||||
.. _excellon-contents:
|
||||
|
||||
Functions
|
||||
---------
|
||||
The :mod:`excellon` module defines the following functions:
|
||||
|
||||
.. autofunction:: gerber.excellon.read
|
||||
|
||||
|
||||
Classes
|
||||
-------
|
||||
The :mod:`excellon` module defines the following classes:
|
||||
|
||||
.. autoclass:: gerber.excellon.ExcellonFile
|
||||
:members:
|
||||
|
||||
|
||||
.. autoclass:: gerber.excellon.ExcellonParser
|
||||
:members:
|
||||
|
||||
11
doc/source/documentation/index.rst
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
Gerber Tools Reference
|
||||
======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
Gerber (RS-274X) Files <rs274x>
|
||||
Excellon Files <excellon>
|
||||
Rendering <render>
|
||||
|
||||
|
||||
11
doc/source/documentation/render.rst
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
:mod:`render` --- Gerber file Rendering
|
||||
==============================================
|
||||
|
||||
.. module:: render
|
||||
:synopsis: Functions and classes for handling Excellon files
|
||||
.. sectionauthor:: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
Render Module
|
||||
-------------
|
||||
.. automodule:: gerber.render.render
|
||||
:members:
|
||||
37
doc/source/documentation/rs274x.rst
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
:mod:`rs274x` --- RS-274X file handling
|
||||
==============================================
|
||||
|
||||
.. module:: rs274x
|
||||
:synopsis: Functions and classes for handling RS-274X files
|
||||
.. sectionauthor:: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
|
||||
The RS-274X (Gerber) format is the most common format for exporting PCB
|
||||
artwork. The Specification is published by Ucamco and is available
|
||||
`here <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_.
|
||||
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:
|
||||
|
|
@ -3,37 +3,16 @@
|
|||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to Gerber Tools's documentation!
|
||||
Gerber-Tools!
|
||||
========================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
.. automodule:: gerber
|
||||
:members:
|
||||
|
||||
.. automodule:: gerber.gerber
|
||||
:members:
|
||||
|
||||
.. automodule:: gerber.excellon
|
||||
:members:
|
||||
|
||||
.. automodule:: gerber.render.render
|
||||
:members:
|
||||
|
||||
.. automodule:: gerber.gerber_statements
|
||||
:members:
|
||||
:maxdepth: 1
|
||||
|
||||
.. automodule:: gerber.excellon_statements
|
||||
:members:
|
||||
|
||||
.. automodule:: gerber.cnc
|
||||
:members:
|
||||
|
||||
.. automodule:: gerber.utils
|
||||
:members:
|
||||
intro
|
||||
documentation/index
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
|
|
|||
19
doc/source/intro.rst
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Gerber Tools Intro
|
||||
==================
|
||||
|
||||
PCB CAM (Gerber) Files
|
||||
------------
|
||||
|
||||
PCB design files (artwork) are most often stored in `Gerber` files. This is
|
||||
a generic term that may refer to `RS-274X (Gerber) <http://en.wikipedia.org/wiki/Gerber_format>`_,
|
||||
`ODB++ <http://en.wikipedia.org/wiki/ODB%2B%2B>`_, or `Excellon <http://en.wikipedia.org/wiki/Excellon_format>`_
|
||||
files.
|
||||
|
||||
|
||||
Gerber-Tools
|
||||
------------
|
||||
|
||||
The gerber-tools module provides tools for working with and rendering Gerber
|
||||
and Excellon files.
|
||||
|
||||
|
||||
5128
examples/board.html
|
Before Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 331 KiB |
5128
examples/board.svg
|
Before Width: | Height: | Size: 477 KiB |
BIN
examples/composite_bottom.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
2
examples/composite_bottom.svg
Normal file
|
After Width: | Height: | Size: 832 KiB |
BIN
examples/composite_top.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
2
examples/composite_top.svg
Normal file
|
After Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 132 KiB |
2852
examples/silk.svg
|
Before Width: | Height: | Size: 265 KiB |
BIN
examples/top.png
|
Before Width: | Height: | Size: 211 KiB |
2278
examples/top.svg
|
Before Width: | Height: | Size: 212 KiB |
73
gerber.md
|
|
@ -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
|
||||
# // <space> 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: [%]<Data Block>{<Data Block>}[%]
|
||||
# // 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: <Coordinate data>: [X<Number>][Y<Number>][I<Number>][J<Number>](D01|D02|D03)
|
||||
# // offsets are not modal
|
||||
#
|
||||
# // parameter: %Parameter code<required modifiers>[optional modifiers]*%
|
||||
# // code: 2 characters
|
||||
#
|
||||
# // parameters can have line separators: %<Parameter>{{<Line separator>}<Parameter>}%
|
||||
#
|
||||
# // function code: (GDM){1}[number], parameters: [AZ]{2}
|
||||
|
|
@ -15,16 +15,16 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
gerber.cnc
|
||||
CAM File
|
||||
============
|
||||
**CNC file classes**
|
||||
**AM file classes**
|
||||
|
||||
This module provides common base classes for Excellon/Gerber CNC files
|
||||
"""
|
||||
|
||||
|
||||
class FileSettings(object):
|
||||
""" CNC File Settings
|
||||
""" CAM File Settings
|
||||
|
||||
Provides a common representation of gerber/excellon file settings
|
||||
"""
|
||||
|
|
@ -60,7 +60,7 @@ class FileSettings(object):
|
|||
raise KeyError()
|
||||
|
||||
|
||||
class CncFile(object):
|
||||
class CamFile(object):
|
||||
""" Base class for Gerber/Excellon files.
|
||||
|
||||
Provides a common set of settings parameters.
|
||||
|
|
@ -71,7 +71,10 @@ class CncFile(object):
|
|||
The current file configuration.
|
||||
|
||||
filename : string
|
||||
Name of the file that this CncFile represents.
|
||||
Name of the file that this CamFile represents.
|
||||
|
||||
layer_name : string
|
||||
Name of the PCB layer that the file represents
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
|
@ -92,7 +95,8 @@ class CncFile(object):
|
|||
decimal digits)
|
||||
"""
|
||||
|
||||
def __init__(self, statements=None, settings=None, filename=None):
|
||||
def __init__(self, statements=None, settings=None, filename=None,
|
||||
layer_name=None):
|
||||
if settings is not None:
|
||||
self.notation = settings['notation']
|
||||
self.units = settings['units']
|
||||
|
|
@ -105,6 +109,7 @@ class CncFile(object):
|
|||
self.format = (2, 5)
|
||||
self.statements = statements if statements is not None else []
|
||||
self.filename = filename
|
||||
self.layer_name = layer_name
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -24,16 +24,32 @@ This module provides Excellon file classes and parsing utilities
|
|||
|
||||
|
||||
from .excellon_statements import *
|
||||
from .cnc import CncFile, FileSettings
|
||||
from .cam import CamFile, FileSettings
|
||||
|
||||
import math
|
||||
|
||||
def read(filename):
|
||||
""" Read data from filename and return an ExcellonFile
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of file to parse
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.excellon.ExcellonFile`
|
||||
An ExcellonFile created from the specified file.
|
||||
|
||||
"""
|
||||
return ExcellonParser().parse(filename)
|
||||
detected_settings = detect_excellon_format(filename)
|
||||
settings = FileSettings(**detected_settings)
|
||||
zeros = ''
|
||||
print('Detected %d:%d format with %s zero suppression' %
|
||||
(settings.format[0], settings.format[1], settings.zero_suppression))
|
||||
return ExcellonParser(settings).parse(filename)
|
||||
|
||||
|
||||
class ExcellonFile(CncFile):
|
||||
class ExcellonFile(CamFile):
|
||||
""" A class representing a single excellon file
|
||||
|
||||
The ExcellonFile class represents a single excellon file.
|
||||
|
|
@ -69,6 +85,14 @@ class ExcellonFile(CncFile):
|
|||
|
||||
def render(self, ctx, filename=None):
|
||||
""" Generate image of file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : :class:`gerber.render.GerberContext`
|
||||
GerberContext subclass used for rendering the image
|
||||
|
||||
filename : string <optional>
|
||||
If provided, the rendered image will be saved to `filename`
|
||||
"""
|
||||
for tool, pos in self.hits:
|
||||
ctx.drill(pos[0], pos[1], tool.diameter)
|
||||
|
|
@ -83,8 +107,13 @@ class ExcellonFile(CncFile):
|
|||
|
||||
class ExcellonParser(object):
|
||||
""" Excellon File Parser
|
||||
|
||||
Parameters
|
||||
----------
|
||||
settings : FileSettings or dict-like
|
||||
Excellon file settings to use when interpreting the excellon file.
|
||||
"""
|
||||
def __init__(self):
|
||||
def __init__(self, settings=None):
|
||||
self.notation = 'absolute'
|
||||
self.units = 'inch'
|
||||
self.zero_suppression = 'trailing'
|
||||
|
|
@ -95,6 +124,37 @@ class ExcellonParser(object):
|
|||
self.hits = []
|
||||
self.active_tool = None
|
||||
self.pos = [0., 0.]
|
||||
if settings is not None:
|
||||
self.units = settings['units']
|
||||
self.zero_suppression = settings['zero_suppression']
|
||||
self.notation = settings['notation']
|
||||
self.format = settings['format']
|
||||
|
||||
|
||||
@property
|
||||
def coordinates(self):
|
||||
return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)]
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
xmin = ymin = 100000000000
|
||||
xmax = ymax = -100000000000
|
||||
for x, y in self.coordinates:
|
||||
if x is not None:
|
||||
xmin = x if x < xmin else xmin
|
||||
xmax = x if x > xmax else xmax
|
||||
if y is not None:
|
||||
ymin = y if y < ymin else ymin
|
||||
ymax = y if y > ymax else ymax
|
||||
return ((xmin, xmax), (ymin, ymax))
|
||||
|
||||
@property
|
||||
def hole_sizes(self):
|
||||
return [stmt.diameter for stmt in self.statements if isinstance(stmt, ExcellonTool)]
|
||||
|
||||
@property
|
||||
def hole_count(self):
|
||||
return len(self.hits)
|
||||
|
||||
def parse(self, filename):
|
||||
with open(filename, 'r') as f:
|
||||
|
|
@ -194,3 +254,105 @@ class ExcellonParser(object):
|
|||
return FileSettings(units=self.units, format=self.format,
|
||||
zero_suppression=self.zero_suppression,
|
||||
notation=self.notation)
|
||||
|
||||
|
||||
def detect_excellon_format(filename):
|
||||
""" Detect excellon file decimal format and zero-suppression settings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Name of the file to parse. This does not check if the file is actually
|
||||
an Excellon file, so do that before calling this.
|
||||
|
||||
Returns
|
||||
-------
|
||||
settings : dict
|
||||
Detected excellon file settings. Keys are
|
||||
- `format`: decimal format as tuple (<int part>, <decimal part>)
|
||||
- `zero_suppression`: zero suppression, 'leading' or 'trailing'
|
||||
"""
|
||||
results = {}
|
||||
detected_zeros = None
|
||||
detected_format = None
|
||||
zs_options = ('leading', 'trailing', )
|
||||
format_options = ((2, 4), (2, 5), (3, 3),)
|
||||
|
||||
# Check for obvious clues:
|
||||
p = ExcellonParser()
|
||||
p.parse(filename)
|
||||
|
||||
# Get zero_suppression from a unit statement
|
||||
zero_statements = [stmt.zero_suppression for stmt in p.statements
|
||||
if isinstance(stmt, UnitStmt)]
|
||||
|
||||
# get format from altium comment
|
||||
format_comment = [stmt.comment for stmt in p.statements
|
||||
if isinstance(stmt, CommentStmt)
|
||||
and 'FILE_FORMAT' in stmt.comment]
|
||||
|
||||
detected_format = (tuple([int(val) for val in
|
||||
format_comment[0].split('=')[1].split(':')])
|
||||
if len(format_comment) == 1 else None)
|
||||
detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None
|
||||
|
||||
# Bail out here if possible
|
||||
if detected_format is not None and detected_zeros is not None:
|
||||
return {'format': detected_format, 'zero_suppression': detected_zeros}
|
||||
|
||||
# Only look at remaining options
|
||||
if detected_format is not None:
|
||||
format_options = (detected_format,)
|
||||
if detected_zeros is not None:
|
||||
zs_options = (detected_zeros,)
|
||||
|
||||
# Brute force all remaining options, and pick the best looking one...
|
||||
for zs in zs_options:
|
||||
for fmt in format_options:
|
||||
key = (fmt, zs)
|
||||
settings = FileSettings(zero_suppression=zs, format=fmt)
|
||||
try:
|
||||
p = ExcellonParser(settings)
|
||||
p.parse(filename)
|
||||
size = tuple([t[1] - t[0] for t in p.bounds])
|
||||
hole_area = 0.0
|
||||
for hit in p.hits:
|
||||
tool = hit[0]
|
||||
hole_area += math.pow(math.pi * tool.diameter / 2., 2)
|
||||
results[key] = (size, p.hole_count, hole_area)
|
||||
except:
|
||||
pass
|
||||
|
||||
# See if any of the dimensions are left with only a single option
|
||||
formats = set(key[0] for key in results.iterkeys())
|
||||
zeros = set(key[1] for key in results.iterkeys())
|
||||
if len(formats) == 1:
|
||||
detected_format = formats.pop()
|
||||
if len(zeros) == 1:
|
||||
detected_zeros = zeros.pop()
|
||||
|
||||
# Bail out here if we got everything....
|
||||
if detected_format is not None and detected_zeros is not None:
|
||||
return {'format': detected_format, 'zero_suppression': detected_zeros}
|
||||
|
||||
# Otherwise score each option and pick the best candidate
|
||||
else:
|
||||
scores = {}
|
||||
for key in results.keys():
|
||||
size, count, diameter = results[key]
|
||||
scores[key] = _layer_size_score(size, count, diameter)
|
||||
minscore = min(scores.values())
|
||||
for key in scores.iterkeys():
|
||||
if scores[key] == minscore:
|
||||
return {'format': key[0], 'zero_suppression': key[1]}
|
||||
|
||||
|
||||
def _layer_size_score(size, hole_count, hole_area):
|
||||
""" Heuristic used for determining the correct file number interpretation.
|
||||
Lower is better.
|
||||
"""
|
||||
board_area = size[0] * size[1]
|
||||
hole_percentage = hole_area / board_area
|
||||
hole_score = (hole_percentage - 0.25) ** 2
|
||||
size_score = (board_area - 8) **2
|
||||
return hole_score * size_score
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt',
|
|||
|
||||
class Statement(object):
|
||||
""" Gerber statement Base class
|
||||
|
||||
|
||||
The statement class provides a type attribute.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
type : string
|
||||
|
|
@ -27,7 +27,7 @@ class Statement(object):
|
|||
|
||||
Attributes
|
||||
----------
|
||||
type : string
|
||||
type : string
|
||||
String identifying the statement type.
|
||||
"""
|
||||
def __init__(self, stype):
|
||||
|
|
@ -45,9 +45,9 @@ class Statement(object):
|
|||
|
||||
class ParamStmt(Statement):
|
||||
""" Gerber parameter statement Base class
|
||||
|
||||
|
||||
The parameter statement class provides a parameter type attribute.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
param : string
|
||||
|
|
@ -55,7 +55,7 @@ class ParamStmt(Statement):
|
|||
|
||||
Attributes
|
||||
----------
|
||||
param : string
|
||||
param : string
|
||||
Parameter type code
|
||||
"""
|
||||
def __init__(self, param):
|
||||
|
|
@ -133,7 +133,12 @@ class MOParamStmt(ParamStmt):
|
|||
@classmethod
|
||||
def from_dict(cls, stmt_dict):
|
||||
param = stmt_dict.get('param')
|
||||
mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric'
|
||||
if stmt_dict.get('mo').lower() == 'in':
|
||||
mo = 'inch'
|
||||
elif stmt_dict.get('mo').lower() == 'mm':
|
||||
mo = 'metric'
|
||||
else:
|
||||
mo = None
|
||||
return cls(param, mo)
|
||||
|
||||
def __init__(self, param, mo):
|
||||
|
|
@ -260,7 +265,7 @@ class LPParamStmt(ParamStmt):
|
|||
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict):
|
||||
param = stmt_dict.get('lp')
|
||||
param = stmt_dict['param']
|
||||
lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark'
|
||||
return cls(param, lp)
|
||||
|
||||
|
|
@ -667,6 +672,6 @@ class UnknownStmt(Statement):
|
|||
def __init__(self, line):
|
||||
Statement.__init__(self, "UNKNOWN")
|
||||
self.line = line
|
||||
|
||||
|
||||
def to_gerber(self):
|
||||
return self.line
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ class GerberContext(object):
|
|||
background_color : tuple (<float>, <float>, <float>)
|
||||
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 = {}
|
||||
|
|
@ -96,11 +99,12 @@ class GerberContext(object):
|
|||
self.level_polarity = 'dark'
|
||||
self.region_mode = 'off'
|
||||
self.quadrant_mode = 'multi-quadrant'
|
||||
|
||||
self.step_and_repeat = (1, 1, 0, 0)
|
||||
self.color = (0.7215, 0.451, 0.200)
|
||||
self.drill_color = (0.25, 0.25, 0.25)
|
||||
self.background_color = (0.0, 0.0, 0.0)
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -415,6 +432,12 @@ class GerberContext(object):
|
|||
"""
|
||||
pass
|
||||
|
||||
def region_contour(self, x, y):
|
||||
pass
|
||||
|
||||
def fill_region(self):
|
||||
pass
|
||||
|
||||
def evaluate(self, stmt):
|
||||
""" Evaluate Gerber statement and update image accordingly.
|
||||
|
||||
|
|
@ -450,7 +473,7 @@ class GerberContext(object):
|
|||
def _evaluate_mode(self, stmt):
|
||||
if stmt.type == 'RegionMode':
|
||||
if self.region_mode == 'on' and stmt.mode == 'off':
|
||||
self._fill_region()
|
||||
self.fill_region()
|
||||
self.region_mode = stmt.mode
|
||||
elif stmt.type == 'QuadrantMode':
|
||||
self.quadrant_mode = stmt.mode
|
||||
|
|
@ -460,11 +483,11 @@ class GerberContext(object):
|
|||
self.set_coord_format(stmt.zero_suppression, stmt.format,
|
||||
stmt.notation)
|
||||
self.set_coord_notation(stmt.notation)
|
||||
elif stmt.param == "MO:":
|
||||
elif stmt.param == "MO":
|
||||
self.set_coord_unit(stmt.mode)
|
||||
elif stmt.param == "IP:":
|
||||
elif stmt.param == "IP":
|
||||
self.set_image_polarity(stmt.ip)
|
||||
elif stmt.param == "LP:":
|
||||
elif stmt.param == "LP":
|
||||
self.set_level_polarity(stmt.lp)
|
||||
elif stmt.param == "AD":
|
||||
self.define_aperture(stmt.d, stmt.shape, stmt.modifiers)
|
||||
|
|
@ -477,7 +500,10 @@ class GerberContext(object):
|
|||
self.direction = ('clockwise' if stmt.function in ('G02', 'G2')
|
||||
else 'counterclockwise')
|
||||
if stmt.op == "D01":
|
||||
self.stroke(stmt.x, stmt.y, stmt.i, stmt.j)
|
||||
if self.region_mode == 'on':
|
||||
self.region_contour(stmt.x, stmt.y)
|
||||
else:
|
||||
self.stroke(stmt.x, stmt.y, stmt.i, stmt.j)
|
||||
elif stmt.op == "D02":
|
||||
self.move(stmt.x, stmt.y)
|
||||
elif stmt.op == "D03":
|
||||
|
|
@ -486,5 +512,3 @@ class GerberContext(object):
|
|||
def _evaluate_aperture(self, stmt):
|
||||
self.set_aperture(stmt.d)
|
||||
|
||||
def _fill_region(self):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -117,17 +117,25 @@ class GerberSvgContext(GerberContext):
|
|||
|
||||
self.apertures = {}
|
||||
self.dwg = svgwrite.Drawing()
|
||||
self.dwg.transform = 'scale 1 -1'
|
||||
self.background = False
|
||||
self.region_path = None
|
||||
|
||||
def set_bounds(self, bounds):
|
||||
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="black"))
|
||||
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':
|
||||
|
|
@ -173,7 +181,8 @@ class GerberSvgContext(GerberContext):
|
|||
ap = self.apertures.get(self.aperture, None)
|
||||
if ap is None:
|
||||
return
|
||||
color = (convert_color(self.color) if self.level_polarity == 'dark'
|
||||
|
||||
color = (convert_color(self.color) if self.level_polarity == 'dark'
|
||||
else convert_color(self.background_color))
|
||||
for shape in ap.flash(self, x, y, color):
|
||||
self.dwg.add(shape)
|
||||
|
|
@ -185,5 +194,21 @@ class GerberSvgContext(GerberContext):
|
|||
fill=convert_color(self.drill_color))
|
||||
self.dwg.add(hit)
|
||||
|
||||
def region_contour(self, x, y):
|
||||
super(GerberSvgContext, self).region_contour(x, y)
|
||||
x, y = self.resolve(x, y)
|
||||
color = (convert_color(self.color) if self.level_polarity == 'dark'
|
||||
else convert_color(self.background_color))
|
||||
if self.region_path is None:
|
||||
self.region_path = self.dwg.path(d = 'M %f, %f' %
|
||||
(self.x*SCALE, -self.y*SCALE),
|
||||
fill = color, stroke = 'none')
|
||||
self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE))
|
||||
self.move(x, y, resolve=False)
|
||||
|
||||
def fill_region(self):
|
||||
self.dwg.add(self.region_path)
|
||||
self.region_path = None
|
||||
|
||||
def dump(self, filename):
|
||||
self.dwg.saveas(filename)
|
||||
|
|
|
|||
|
|
@ -15,30 +15,35 @@
|
|||
# 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 File module
|
||||
==================
|
||||
**Gerber File module**
|
||||
|
||||
This module provides an RS-274-X class and parser
|
||||
""" This module provides an RS-274-X class and parser.
|
||||
"""
|
||||
|
||||
|
||||
import re
|
||||
import json
|
||||
from .gerber_statements import *
|
||||
from .cnc import CncFile, FileSettings
|
||||
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(CncFile):
|
||||
class GerberFile(CamFile):
|
||||
""" A class representing a single gerber file
|
||||
|
||||
The GerberFile class represents a single gerber file.
|
||||
|
|
@ -86,24 +91,19 @@ class GerberFile(CncFile):
|
|||
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
|
||||
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
|
||||
"""
|
||||
|
|
@ -113,6 +113,14 @@ class GerberFile(CncFile):
|
|||
|
||||
def render(self, ctx, filename=None):
|
||||
""" Generate image of layer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : :class:`GerberContext`
|
||||
GerberContext subclass used for rendering the image
|
||||
|
||||
filename : string <optional>
|
||||
If provided, the rendered image will be saved to `filename`
|
||||
"""
|
||||
ctx.set_bounds(self.bounds)
|
||||
for statement in self.statements:
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
from ..cnc import CncFile, FileSettings
|
||||
from ..cam import CamFile, FileSettings
|
||||
from tests import *
|
||||
|
||||
|
||||
|
|
@ -46,5 +46,5 @@ def test_filesettings_assign():
|
|||
assert_equal(fs.zero_suppression, 'test')
|
||||
assert_equal(fs.format, 'test')
|
||||
|
||||
def test_smoke_cncfile():
|
||||
pass
|
||||
def test_smoke_camfile():
|
||||
cf = CamFile
|
||||
|
|
@ -8,7 +8,7 @@ from ..gerber_statements import *
|
|||
|
||||
|
||||
def test_FSParamStmt_factory():
|
||||
""" Test FSParamStruct factory correctly handles parameters
|
||||
""" Test FSParamStruct factory
|
||||
"""
|
||||
stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'}
|
||||
fs = FSParamStmt.from_dict(stmt)
|
||||
|
|
@ -24,6 +24,18 @@ def test_FSParamStmt_factory():
|
|||
assert_equal(fs.notation, 'incremental')
|
||||
assert_equal(fs.format, (2, 7))
|
||||
|
||||
def test_FSParamStmt():
|
||||
""" Test FSParamStmt initialization
|
||||
"""
|
||||
param = 'FS'
|
||||
zeros = 'trailing'
|
||||
notation = 'absolute'
|
||||
fmt = (2, 5)
|
||||
stmt = FSParamStmt(param, zeros, notation, fmt)
|
||||
assert_equal(stmt.param, param)
|
||||
assert_equal(stmt.zero_suppression, zeros)
|
||||
assert_equal(stmt.notation, notation)
|
||||
assert_equal(stmt.format, fmt)
|
||||
|
||||
def test_FSParamStmt_dump():
|
||||
""" Test FSParamStmt to_gerber()
|
||||
|
|
@ -38,17 +50,31 @@ def test_FSParamStmt_dump():
|
|||
|
||||
|
||||
def test_MOParamStmt_factory():
|
||||
""" Test MOParamStruct factory correctly handles parameters
|
||||
""" Test MOParamStruct factory
|
||||
"""
|
||||
stmt = {'param': 'MO', 'mo': 'IN'}
|
||||
mo = MOParamStmt.from_dict(stmt)
|
||||
assert_equal(mo.param, 'MO')
|
||||
assert_equal(mo.mode, 'inch')
|
||||
stmts = [{'param': 'MO', 'mo': 'IN'}, {'param': 'MO', 'mo': 'in'}, ]
|
||||
for stmt in stmts:
|
||||
mo = MOParamStmt.from_dict(stmt)
|
||||
assert_equal(mo.param, 'MO')
|
||||
assert_equal(mo.mode, 'inch')
|
||||
|
||||
stmt = {'param': 'MO', 'mo': 'MM'}
|
||||
mo = MOParamStmt.from_dict(stmt)
|
||||
assert_equal(mo.param, 'MO')
|
||||
assert_equal(mo.mode, 'metric')
|
||||
stmts = [{'param': 'MO', 'mo': 'MM'}, {'param': 'MO', 'mo': 'mm'}, ]
|
||||
for stmt in stmts:
|
||||
mo = MOParamStmt.from_dict(stmt)
|
||||
assert_equal(mo.param, 'MO')
|
||||
assert_equal(mo.mode, 'metric')
|
||||
|
||||
def test_MOParamStmt():
|
||||
""" Test MOParamStmt initialization
|
||||
"""
|
||||
param = 'MO'
|
||||
mode = 'inch'
|
||||
stmt = MOParamStmt(param, mode)
|
||||
assert_equal(stmt.param, param)
|
||||
|
||||
for mode in ['inch', 'metric']:
|
||||
stmt = MOParamStmt(param, mode)
|
||||
assert_equal(stmt.mode, mode)
|
||||
|
||||
|
||||
def test_MOParamStmt_dump():
|
||||
|
|
@ -64,7 +90,7 @@ def test_MOParamStmt_dump():
|
|||
|
||||
|
||||
def test_IPParamStmt_factory():
|
||||
""" Test IPParamStruct factory correctly handles parameters
|
||||
""" Test IPParamStruct factory
|
||||
"""
|
||||
stmt = {'param': 'IP', 'ip': 'POS'}
|
||||
ip = IPParamStmt.from_dict(stmt)
|
||||
|
|
@ -74,6 +100,15 @@ def test_IPParamStmt_factory():
|
|||
ip = IPParamStmt.from_dict(stmt)
|
||||
assert_equal(ip.ip, 'negative')
|
||||
|
||||
def test_IPParamStmt():
|
||||
""" Test IPParamStmt initialization
|
||||
"""
|
||||
param = 'IP'
|
||||
for ip in ['positive', 'negative']:
|
||||
stmt = IPParamStmt(param, ip)
|
||||
assert_equal(stmt.param, param)
|
||||
assert_equal(stmt.ip, ip)
|
||||
|
||||
|
||||
def test_IPParamStmt_dump():
|
||||
""" Test IPParamStmt to_gerber()
|
||||
|
|
@ -88,14 +123,23 @@ def test_IPParamStmt_dump():
|
|||
|
||||
|
||||
def test_OFParamStmt_factory():
|
||||
""" Test OFParamStmt factory correctly handles parameters
|
||||
""" Test OFParamStmt factory
|
||||
"""
|
||||
stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'}
|
||||
of = OFParamStmt.from_dict(stmt)
|
||||
assert_equal(of.a, 0.1234567)
|
||||
assert_equal(of.b, 0.1234567)
|
||||
|
||||
|
||||
def test_OFParamStmt():
|
||||
""" Test IPParamStmt initialization
|
||||
"""
|
||||
param = 'OF'
|
||||
for val in [0.0, -3.4567]:
|
||||
stmt = OFParamStmt(param, val, val)
|
||||
assert_equal(stmt.param, param)
|
||||
assert_equal(stmt.a, val)
|
||||
assert_equal(stmt.b, val)
|
||||
|
||||
def test_OFParamStmt_dump():
|
||||
""" Test OFParamStmt to_gerber()
|
||||
"""
|
||||
|
|
@ -105,7 +149,7 @@ def test_OFParamStmt_dump():
|
|||
|
||||
|
||||
def test_LPParamStmt_factory():
|
||||
""" Test LPParamStmt factory correctly handles parameters
|
||||
""" Test LPParamStmt factory
|
||||
"""
|
||||
stmt = {'param': 'LP', 'lp': 'C'}
|
||||
lp = LPParamStmt.from_dict(stmt)
|
||||
|
|
@ -128,7 +172,7 @@ def test_LPParamStmt_dump():
|
|||
|
||||
|
||||
def test_INParamStmt_factory():
|
||||
""" Test INParamStmt factory correctly handles parameters
|
||||
""" Test INParamStmt factory
|
||||
"""
|
||||
stmt = {'param': 'IN', 'name': 'test'}
|
||||
inp = INParamStmt.from_dict(stmt)
|
||||
|
|
@ -143,7 +187,7 @@ def test_INParamStmt_dump():
|
|||
|
||||
|
||||
def test_LNParamStmt_factory():
|
||||
""" Test LNParamStmt factory correctly handles parameters
|
||||
""" Test LNParamStmt factory
|
||||
"""
|
||||
stmt = {'param': 'LN', 'name': 'test'}
|
||||
lnp = LNParamStmt.from_dict(stmt)
|
||||
|
|
|
|||