Merge pull request #4 from hamiltonkibbe/master

Many fixes in parsing, rendering and new features
This commit is contained in:
Paulo Henrique Silva 2014-10-26 20:55:22 -02:00
commit d5c8d896d8
64 changed files with 18842 additions and 16348 deletions

4
.coveragerc Normal file
View file

@ -0,0 +1,4 @@
[report]
omit =
*/python?.?/*
*/site-packages/nose/*

3
.gitignore vendored
View file

@ -38,6 +38,9 @@ nosetests.xml
.idea/misc.xml
.idea
# Komodo Files
*.komodoproject
# OS Files
.DS_Store
Thumbs.db

16
.travis.yml Normal file
View file

@ -0,0 +1,16 @@
language: python
python:
- "2.7"
# command to install dependencies
install:
- "pip install -r requirements.txt"
- "pip install coveralls"
# command to run tests
script:
- make test-coverage
# Coveralls
after_success:
- coveralls

27
Makefile Normal file
View file

@ -0,0 +1,27 @@
PYTHON ?= python
NOSETESTS ?= nosetests
DOC_ROOT = doc
clean: doc-clean
#$(PYTHON) setup.py clean
find . -name '*.pyc' -delete
rm -rf coverage .coverage
rm -rf *.egg-info
test:
$(NOSETESTS) -s -v gerber
test-coverage:
rm -rf coverage .coverage
$(NOSETESTS) -s -v --with-coverage --cover-package=gerber
doc-html:
(cd $(DOC_ROOT); make html)
doc-clean:
(cd $(DOC_ROOT); make clean)

View file

@ -1,9 +1,31 @@
gerber-tools
============
This hopefully will be a useful set of tools to handle Gerber files in Python.
Right now we have a working parser and I am working on simple Gerber to SVG converter.
See gerber.md for some random information regardind Gerber format.
gerber-tools
============
![Travis CI Build Status](https://travis-ci.org/hamiltonkibbe/gerber-tools.svg?branch=master)
[![Coverage Status](https://coveralls.io/repos/hamiltonkibbe/gerber-tools/badge.png?branch=master)](https://coveralls.io/r/hamiltonkibbe/gerber-tools?branch=master)
Tools to handle Gerber and Excellon files in Python.
Useage Example:
---------------
import gerber
from gerber.render import GerberSvgContext
# Read gerber and Excellon files
top_copper = gerber.read('example.GTL')
nc_drill = gerber.read('example.txt')
# Rendering context
ctx = GerberSvgContext()
# Create SVG image
top_copper.render(ctx)
nc_drill.render(ctx, 'composite.svg')
Rendering Examples:
-------------------
###Top Composite rendering
![Composite Top Image](examples/composite_top.png)
###Bottom Composite rendering
![Composite Bottom Image](examples/composite_bottom.png)

View file

@ -1,3 +0,0 @@
* add command line utilities: gerber svg, gerber transform --rotate --scale --translate, gerber merge --blueprint
* AM defined apertures

177
doc/Makefile Normal file
View file

@ -0,0 +1,177 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/GerberTools.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/GerberTools.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/GerberTools"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/GerberTools"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

242
doc/make.bat Normal file
View file

@ -0,0 +1,242 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
set I18NSPHINXOPTS=%SPHINXOPTS% source
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\GerberTools.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\GerberTools.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end

262
doc/source/conf.py Normal file
View file

@ -0,0 +1,262 @@
# -*- coding: utf-8 -*-
#
# Gerber Tools documentation build configuration file, created by
# sphinx-quickstart on Sun Sep 28 18:16:46 2014.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('../../'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'numpydoc',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Gerber Tools'
copyright = u'2014, Hamilton Kibbe'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'GerberToolsdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'GerberTools.tex', u'Gerber Tools Documentation',
u'Hamilton Kibbe', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'gerbertools', u'Gerber Tools Documentation',
[u'Hamilton Kibbe'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'GerberTools', u'Gerber Tools Documentation',
u'Hamilton Kibbe', 'GerberTools', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

View 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:

View file

@ -0,0 +1,11 @@
Gerber Tools Reference
======================
.. toctree::
:maxdepth: 2
Gerber (RS-274X) Files <rs274x>
Excellon Files <excellon>
Rendering <render>

View 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:

View 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:

23
doc/source/index.rst Normal file
View file

@ -0,0 +1,23 @@
.. Gerber Tools documentation master file, created by
sphinx-quickstart on Sun Sep 28 18:16:46 2014.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Gerber-Tools!
========================================
Contents:
.. toctree::
:maxdepth: 1
intro
documentation/index
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

19
doc/source/intro.rst Normal file
View 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.

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 832 KiB

BIN
examples/composite_top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 212 KiB

View file

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

View file

@ -14,3 +14,13 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Gerber Tools
============
**Gerber Tools**
gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon
files in python.
"""
from .common import read

View file

@ -10,22 +10,32 @@
# 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 .parser import GerberParser
from .render import GerberContext
from .common import read
from .render import GerberSvgContext
import sys
if len(sys.argv) < 2:
print >> sys.stderr, "Usage: python -m gerber <filename> <filename>..."
sys.exit(1)
ctx = GerberSvgContext()
ctx.set_alpha(0.95)
for filename in sys.argv[1:]:
print "parsing %s" % filename
g = GerberParser(GerberContext())
g.parse(filename)
if 'GTO' in filename or 'GBO' in filename:
ctx.set_color((1, 1, 1))
ctx.set_alpha(0.8)
elif 'GTS' in filename or 'GBS' in filename:
ctx.set_color((0.2, 0.2, 0.75))
ctx.set_alpha(0.8)
gerberfile = read(filename)
gerberfile.render(ctx)
print('Saving image to test.svg')
ctx.dump('test.svg')

178
gerber/cam.py Normal file
View file

@ -0,0 +1,178 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
CAM File
============
**AM file classes**
This module provides common base classes for Excellon/Gerber CNC files
"""
class FileSettings(object):
""" CAM File Settings
Provides a common representation of gerber/excellon file settings
"""
def __init__(self, notation='absolute', units='inch',
zero_suppression='trailing', format=(2, 5)):
if notation not in ['absolute', 'incremental']:
raise ValueError('Notation must be either absolute or incremental')
self.notation = notation
if units not in ['inch', 'metric']:
raise ValueError('Units must be either inch or metric')
self.units = units
if zero_suppression not in ['leading', 'trailing']:
raise ValueError('Zero suppression must be either leading or \
trailling')
self.zero_suppression = zero_suppression
if len(format) != 2:
raise ValueError('Format must be a tuple(n=2) of integers')
self.format = format
def __getitem__(self, key):
if key == 'notation':
return self.notation
elif key == 'units':
return self.units
elif key == 'zero_suppression':
return self.zero_suppression
elif key == 'format':
return self.format
else:
raise KeyError()
def __setitem__(self, key, value):
if key == 'notation':
if value not in ['absolute', 'incremental']:
raise ValueError('Notation must be either \
absolute or incremental')
self.notation = value
elif key == 'units':
if value not in ['inch', 'metric']:
raise ValueError('Units must be either inch or metric')
self.units = value
elif key == 'zero_suppression':
if value not in ['leading', 'trailing']:
raise ValueError('Zero suppression must be either leading or \
trailling')
self.zero_suppression = value
elif key == 'format':
if len(value) != 2:
raise ValueError('Format must be a tuple(n=2) of integers')
self.format = value
def __eq__(self, other):
return (self.notation == other.notation and
self.units == other.units and
self.zero_suppression == other.zero_suppression and
self.format == other.format)
class CamFile(object):
""" Base class for Gerber/Excellon files.
Provides a common set of settings parameters.
Parameters
----------
settings : FileSettings
The current file configuration.
primitives : iterable
List of primitives in the file.
filename : string
Name of the file that this CamFile represents.
layer_name : string
Name of the PCB layer that the file represents
Attributes
----------
settings : FileSettings
File settings as a FileSettings object
notation : string
File notation setting. May be either 'absolute' or 'incremental'
units : string
File units setting. May be 'inch' or 'metric'
zero_suppression : string
File zero-suppression setting. May be either 'leading' or 'trailling'
format : tuple (<int>, <int>)
File decimal representation format as a tuple of (integer digits,
decimal digits)
"""
def __init__(self, statements=None, settings=None, primitives=None,
filename=None, layer_name=None):
if settings is not None:
self.notation = settings['notation']
self.units = settings['units']
self.zero_suppression = settings['zero_suppression']
self.format = settings['format']
else:
self.notation = 'absolute'
self.units = 'inch'
self.zero_suppression = 'trailing'
self.format = (2, 5)
self.statements = statements if statements is not None else []
self.primitives = primitives
self.filename = filename
self.layer_name = layer_name
@property
def settings(self):
""" File settings
Returns
-------
settings : FileSettings (dict-like)
A FileSettings object with the specified configuration.
"""
return FileSettings(self.notation, self.units, self.zero_suppression,
self.format)
@property
def bounds(self):
""" File baundaries
"""
pass
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, save the rendered image to `filename`
"""
ctx.set_bounds(self.bounds)
for p in self.primitives:
ctx.render(p)
if filename is not None:
ctx.dump(filename)

42
gerber/common.py Normal file
View file

@ -0,0 +1,42 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
def read(filename):
""" Read a gerber or excellon file and return a representative object.
Parameters
----------
filename : string
Filename of the file to read.
Returns
-------
file : CncFile subclass
CncFile object representing the file, either GerberFile or
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
"""
import rs274x
import excellon
from utils import detect_file_format
fmt = detect_file_format(filename)
if fmt == 'rs274x':
return rs274x.read(filename)
elif fmt == 'excellon':
return excellon.read(filename)
else:
return None

View file

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# 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
@ -13,138 +13,229 @@
# 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.
# limitations under the License.
"""
Excellon File module
====================
**Excellon file classes**
This module provides Excellon file classes and parsing utilities
"""
import re
from itertools import tee, izip
from .utils import parse_gerber_value
from .excellon_statements import *
from .cam import CamFile, FileSettings
from .primitives import Drill
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.
"""
detected_settings = detect_excellon_format(filename)
settings = FileSettings(**detected_settings)
zeros = ''
return ExcellonParser(settings).parse(filename)
class ExcellonFile(CamFile):
""" A class representing a single excellon file
class Tool(object):
The ExcellonFile class represents a single excellon file.
@classmethod
def from_line(cls, line, settings):
commands = re.split('([BCFHSTZ])', line)[1:]
commands = [(command, value) for command, value in pairwise(commands)]
args = {}
format = settings['format']
zero_suppression = settings['zero_suppression']
for cmd, val in commands:
if cmd == 'B':
args['retract_rate'] = parse_gerber_value(val, format, zero_suppression)
elif cmd == 'C':
args['diameter'] = parse_gerber_value(val, format, zero_suppression)
elif cmd == 'F':
args['feed_rate'] = parse_gerber_value(val, format, zero_suppression)
elif cmd == 'H':
args['max_hit_count'] = parse_gerber_value(val, format, zero_suppression)
elif cmd == 'S':
args['rpm'] = 1000 * parse_gerber_value(val, format, zero_suppression)
elif cmd == 'T':
args['number'] = int(val)
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')
self.retract_rate = kwargs.get('retract_rate')
self.rpm = kwargs.get('rpm')
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')
def __repr__(self):
unit = 'in.' if self.units == 'inch' else 'mm'
return '<Tool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
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, statements, tools, hits, settings, filename=None):
super(ExcellonFile, self).__init__(statements=statements,
settings=settings,
filename=filename)
self.tools = tools
self.hits = hits
self.primitives = [Drill(position, tool.diameter)
for tool, position in self.hits]
@property
def bounds(self):
xmin = ymin = 100000000000
xmax = ymax = -100000000000
for tool, position in self.hits:
radius = tool.diameter / 2.
x = position[0]
y = position[1]
xmin = min(x - radius, xmin)
xmax = max(x + radius, xmax)
ymin = min(y - radius, ymin)
ymax = max(y + radius, ymax)
return ((xmin, xmax), (ymin, ymax))
def report(self):
""" Print drill report
"""
pass
def write(self, filename):
with open(filename, 'w') as f:
for statement in self.statements:
f.write(statement.to_excellon() + '\n')
class ExcellonParser(object):
def __init__(self, ctx=None):
self.ctx=ctx
""" Excellon File Parser
Parameters
----------
settings : FileSettings or dict-like
Excellon file settings to use when interpreting the excellon file.
"""
def __init__(self, settings=None):
self.notation = 'absolute'
self.units = 'inch'
self.zero_suppression = 'trailing'
self.format = (2,5)
self.format = (2, 5)
self.state = 'INIT'
self.statements = []
self.tools = {}
self.hits = []
self.active_tool = None
self.pos = [0., 0.]
if ctx is not None:
zeros = 'L' if self.zero_suppression == 'leading' else 'T'
x = self.format
y = self.format
self.ctx.set_coord_format(zeros, x, y)
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:
for line in f:
self._parse(line)
def dump(self, filename='teste.svg'):
if self.ctx is not None:
self.ctx.dump(filename)
self._parse(line.strip())
return ExcellonFile(self.statements, self.tools, self.hits,
self._settings(), filename)
def _parse(self, line):
if 'M48' in line:
#line = line.strip()
zs = self._settings().zero_suppression
fmt = self._settings().format
if line[0] == ';':
self.statements.append(CommentStmt.from_excellon(line))
elif line[:3] == 'M48':
self.statements.append(HeaderBeginStmt())
self.state = 'HEADER'
if 'G00' in line:
elif line[0] == '%':
self.statements.append(RewindStopStmt())
if self.state == 'HEADER':
self.state = 'DRILL'
elif line[:3] == 'M95':
self.statements.append(HeaderEndStmt())
if self.state == 'HEADER':
self.state = 'DRILL'
elif line[:3] == 'M30':
stmt = EndOfProgramStmt.from_excellon(line)
self.statements.append(stmt)
elif line[:3] == 'G00':
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'
elif 'METRIC' in line or line.strip() == 'M71':
self.units = 'metric'
if 'LZ' in line:
self.zero_suppression = 'trailing'
elif 'TZ' in line:
self.zero_suppression = 'leading'
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 = 'absolute'
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())
elif line[:3] == 'G05':
self.state = 'DRILL'
elif (('INCH' in line or 'METRIC' in line) and
('LZ' in line or 'TZ' in line)):
stmt = UnitStmt.from_excellon(line)
self.units = stmt.units
self.zero_suppression = stmt.zero_suppression
self.statements.append(stmt)
elif line[:3] == 'M71' or line [:3] == 'M72':
stmt = MeasuringModeStmt.from_excellon(line)
self.units = stmt.units
self.statements.append(stmt)
elif line[:3] == 'ICI':
stmt = IncrementalModeStmt.from_excellon(line)
self.notation = 'incremental' if stmt.mode == 'on' else 'absolute'
self.statements.append(stmt)
elif line[:3] == 'VER':
stmt = VersionStmt.from_excellon(line)
self.statements.append(stmt)
elif line[:4] == 'FMAT':
stmt = FormatStmt.from_excellon(line)
self.statements.append(stmt)
elif line[0] == 'T' and self.state == 'HEADER':
tool = ExcellonTool.from_excellon(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])]
self.statements.append(tool)
if line[0] in ['X', 'Y']:
x = None
y = None
if line[0] == 'X':
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)
else:
y = parse_gerber_value(line.strip(' Y'), fmt,zs)
elif line[0] == 'T' and self.state != 'HEADER':
stmt = ToolSelectionStmt.from_excellon(line)
self.active_tool = self.tools[stmt.tool]
self.statements.append(stmt)
elif line[0] in ['X', 'Y']:
stmt = CoordinateStmt.from_excellon(line, fmt, zs)
x = stmt.x
y = stmt.y
self.statements.append(stmt)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
@ -155,26 +246,115 @@ class ExcellonParser(object):
self.pos[0] += x
if y is not None:
self.pos[1] += y
if self.state == 'DRILL':
self.hits.append((self.active_tool, self.pos))
if self.ctx is not None:
self.ctx.drill(self.pos[0], self.pos[1],
self.active_tool.diameter)
if self.state == 'DRILL':
self.hits.append((self.active_tool, tuple(self.pos)))
self.active_tool._hit()
else:
self.statements.append(UnknownStmt.from_excellon(line))
def _settings(self):
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__':
from .render_svg import GerberSvgContext
tools = []
p = ExcellonParser(GerberSvgContext())
p.parse('examples/ncdrill.txt')
p.dump('excellon.svg')
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

View file

@ -0,0 +1,431 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Excellon Statements
====================
**Excellon file statement classes**
"""
from .utils import parse_gerber_value, write_gerber_value, decimal_string
import re
__all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt',
'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt',
'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt',
'MeasuringModeStmt', 'UnknownStmt',
]
class ExcellonStatement(object):
""" Excellon Statement abstract base class
"""
@classmethod
def from_excellon(cls, line):
pass
def to_excellon(self):
pass
class ExcellonTool(ExcellonStatement):
""" Excellon Tool class
Parameters
----------
settings : FileSettings (dict-like)
File-wide settings.
kwargs : dict-like
Tool settings from the excellon statement. Valid keys are:
- `diameter` : Tool diameter [expressed in file units]
- `rpm` : Tool RPM
- `feed_rate` : Z-axis tool feed rate
- `retract_rate` : Z-axis tool retraction rate
- `max_hit_count` : Number of hits allowed before a tool change
- `depth_offset` : Offset of tool depth from tip of tool.
Attributes
----------
number : integer
Tool number from the excellon file
diameter : float
Tool diameter in file units
rpm : float
Tool RPM
feed_rate : float
Tool Z-axis feed rate.
retract_rate : float
Tool Z-axis retract rate
depth_offset : float
Offset of depth measurement from tip of tool
max_hit_count : integer
Maximum number of tool hits allowed before a tool change
hit_count : integer
Number of tool hits in excellon file.
"""
@classmethod
def from_excellon(cls, line, settings):
""" Create a Tool from an excellon file tool definition line.
Parameters
----------
line : string
Tool definition line from an excellon file.
settings : FileSettings (dict-like)
Excellon file-wide settings
Returns
-------
tool : Tool
An ExcellonTool representing the tool defined in `line`
"""
commands = re.split('([BCFHSTZ])', line)[1:]
commands = [(command, value) for command, value in pairwise(commands)]
args = {}
nformat = settings.format
zero_suppression = settings.zero_suppression
for cmd, val in commands:
if cmd == 'B':
args['retract_rate'] = parse_gerber_value(val, nformat, zero_suppression)
elif cmd == 'C':
args['diameter'] = parse_gerber_value(val, nformat, zero_suppression)
elif cmd == 'F':
args['feed_rate'] = parse_gerber_value(val, nformat, zero_suppression)
elif cmd == 'H':
args['max_hit_count'] = parse_gerber_value(val, nformat, zero_suppression)
elif cmd == 'S':
args['rpm'] = 1000 * parse_gerber_value(val, nformat, zero_suppression)
elif cmd == 'T':
args['number'] = int(val)
elif cmd == 'Z':
args['depth_offset'] = parse_gerber_value(val, nformat, zero_suppression)
return cls(settings, **args)
@classmethod
def from_dict(cls, settings, tool_dict):
""" Create an ExcellonTool from a dict.
Parameters
----------
settings : FileSettings (dict-like)
Excellon File-wide settings
tool_dict : dict
Excellon tool parameters as a dict
Returns
-------
tool : ExcellonTool
An ExcellonTool initialized with the parameters in tool_dict.
"""
return cls(settings, tool_dict)
def __init__(self, settings, **kwargs):
self.settings = settings
self.number = kwargs.get('number')
self.feed_rate = kwargs.get('feed_rate')
self.retract_rate = kwargs.get('retract_rate')
self.rpm = kwargs.get('rpm')
self.diameter = kwargs.get('diameter')
self.max_hit_count = kwargs.get('max_hit_count')
self.depth_offset = kwargs.get('depth_offset')
self.hit_count = 0
def to_excellon(self):
fmt = self.settings.format
zs = self.settings.format
stmt = 'T%d' % self.number
if self.retract_rate is not None:
stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs)
if self.feed_rate is not None:
stmt += 'F%s' % write_gerber_value(self.feed_rate, fmt, zs)
if self.max_hit_count is not None:
stmt += 'H%s' % write_gerber_value(self.max_hit_count, fmt, zs)
if self.rpm is not None:
if self.rpm < 100000.:
stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs)
else:
stmt += 'S%g' % self.rpm / 1000.
if self.diameter is not None:
stmt += 'C%s' % decimal_string(self.diameter, fmt[1], True)
if self.depth_offset is not None:
stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs)
return stmt
def _hit(self):
self.hit_count += 1
def __repr__(self):
unit = 'in.' if self.settings.units == 'inch' else 'mm'
return '<ExcellonTool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
class ToolSelectionStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
""" Create a ToolSelectionStmt from an excellon file line.
Parameters
----------
line : string
Line from an Excellon file
Returns
-------
tool_statement : ToolSelectionStmt
ToolSelectionStmt representation of `line.`
"""
line = line[1:]
compensation_index = None
tool = int(line[:2])
if len(line) > 2:
compensation_index = int(line[2:])
return cls(tool, compensation_index)
def __init__(self, tool, compensation_index=None):
tool = int(tool)
compensation_index = (int(compensation_index) if compensation_index
is not None else None)
self.tool = tool
self.compensation_index = compensation_index
def to_excellon(self):
stmt = 'T%02d' % self.tool
if self.compensation_index is not None:
stmt += '%02d' % self.compensation_index
return stmt
class CoordinateStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, nformat=(2, 5), zero_suppression='trailing'):
x_coord = None
y_coord = None
if line[0] == 'X':
splitline = line.strip('X').split('Y')
x_coord = parse_gerber_value(splitline[0], nformat,
zero_suppression)
if len(splitline) == 2:
y_coord = parse_gerber_value(splitline[1], nformat,
zero_suppression)
else:
y_coord = parse_gerber_value(line.strip(' Y'), nformat,
zero_suppression)
return cls(x_coord, y_coord)
def __init__(self, x=None, y=None):
self.x = x
self.y = y
def to_excellon(self):
stmt = ''
if self.x is not None:
stmt += 'X%s' % write_gerber_value(self.x)
if self.y is not None:
stmt += 'Y%s' % write_gerber_value(self.y)
return stmt
class CommentStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
return cls(line.lstrip(';'))
def __init__(self, comment):
self.comment = comment
def to_excellon(self):
return ';%s' % self.comment
class HeaderBeginStmt(ExcellonStatement):
def __init__(self):
pass
def to_excellon(self):
return 'M48'
class HeaderEndStmt(ExcellonStatement):
def __init__(self):
pass
def to_excellon(self):
return 'M95'
class RewindStopStmt(ExcellonStatement):
def __init__(self):
pass
def to_excellon(self):
return '%'
class EndOfProgramStmt(ExcellonStatement):
def __init__(self, x=None, y=None):
self.x = x
self.y = y
def to_excellon(self):
stmt = 'M30'
if self.x is not None:
stmt += 'X%s' % write_gerber_value(self.x)
if self.y is not None:
stmt += 'Y%s' % write_gerber_value(self.y)
return stmt
class UnitStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
units = 'inch' if 'INCH' in line else 'metric'
zero_suppression = 'trailing' if 'LZ' in line else 'leading'
return cls(units, zero_suppression)
def __init__(self, units='inch', zero_suppression='trailing'):
self.units = units.lower()
self.zero_suppression = zero_suppression
def to_excellon(self):
stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC',
'LZ' if self.zero_suppression == 'trailing'
else 'TZ')
return stmt
class IncrementalModeStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
return cls('off') if 'OFF' in line else cls('on')
def __init__(self, mode='off'):
if mode.lower() not in ['on', 'off']:
raise ValueError('Mode may be "on" or "off"')
self.mode = mode
def to_excellon(self):
return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON')
class VersionStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
version = int(line.split(',')[1])
return cls(version)
def __init__(self, version=1):
version = int(version)
if version not in [1, 2]:
raise ValueError('Valid versions are 1 or 2')
self.version = version
def to_excellon(self):
return 'VER,%d' % self.version
class FormatStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
fmt = int(line.split(',')[1])
return cls(fmt)
def __init__(self, format=1):
format = int(format)
if format not in [1, 2]:
raise ValueError('Valid formats are 1 or 2')
self.format = format
def to_excellon(self):
return 'FMAT,%d' % self.format
class LinkToolStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
linked = [int(tool) for tool in line.split('/')]
return cls(linked)
def __init__(self, linked_tools):
self.linked_tools = [int(x) for x in linked_tools]
def to_excellon(self):
return '/'.join([str(x) for x in self.linked_tools])
class MeasuringModeStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
if not ('M71' in line or 'M72' in line):
raise ValueError('Not a measuring mode statement')
return cls('inch') if 'M72' in line else cls('metric')
def __init__(self, units='inch'):
units = units.lower()
if units not in ['inch', 'metric']:
raise ValueError('units must be "inch" or "metric"')
self.units = units
def to_excellon(self):
return 'M72' if self.units == 'inch' else 'M71'
class UnknownStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
return cls(line)
def __init__(self, stmt):
self.stmt = stmt
def to_excellon(self):
return self.stmt
def pairwise(iterator):
""" Iterate over list taking two elements at a time.
e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)]
"""
itr = iter(iterator)
while True:
yield tuple([itr.next() for i in range(2)])

676
gerber/gerber_statements.py Normal file
View file

@ -0,0 +1,676 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gerber (RS-274X) Statements
===========================
**Gerber RS-274X file statement classes**
"""
from .utils import parse_gerber_value, write_gerber_value, decimal_string
__all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt',
'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt',
'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt',
'EofStmt', 'QuadrantModeStmt', 'RegionModeStmt', 'UnknownStmt',
'ParamStmt']
class Statement(object):
""" Gerber statement Base class
The statement class provides a type attribute.
Parameters
----------
type : string
String identifying the statement type.
Attributes
----------
type : string
String identifying the statement type.
"""
def __init__(self, stype):
self.type = stype
def __str__(self):
s = "<{0} ".format(self.__class__.__name__)
for key, value in self.__dict__.items():
s += "{0}={1} ".format(key, value)
s = s.rstrip() + ">"
return s
class ParamStmt(Statement):
""" Gerber parameter statement Base class
The parameter statement class provides a parameter type attribute.
Parameters
----------
param : string
two-character code identifying the parameter statement type.
Attributes
----------
param : string
Parameter type code
"""
def __init__(self, param):
Statement.__init__(self, "PARAM")
self.param = param
class FSParamStmt(ParamStmt):
""" FS - Gerber Format Specification Statement
"""
@classmethod
def from_dict(cls, stmt_dict):
"""
"""
param = stmt_dict.get('param')
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'))
fmt = (x[0], x[1])
return cls(param, zeros, notation, fmt)
def __init__(self, param, zero_suppression='leading',
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:
(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
self.notation = notation
self.format = format
def to_gerber(self):
zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T'
notation = 'A' if self.notation == 'absolute' else 'I'
fmt = ''.join(map(str, self.format))
return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation,
fmt, fmt)
def __str__(self):
return ('<Format Spec: %d:%d %s zero suppression %s notation>' %
(self.format[0], self.format[1], self.zero_suppression,
self.notation))
class MOParamStmt(ParamStmt):
""" MO - Gerber Mode (measurement units) Statement.
"""
@classmethod
def from_dict(cls, stmt_dict):
param = stmt_dict.get('param')
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):
""" Initialize MOParamStmt class
Parameters
----------
param : string
Parameter.
mo : string
Measurement units. May be either 'inch' or 'metric'
Returns
-------
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)
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)
"""
@classmethod
def from_dict(cls, stmt_dict):
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'
Returns
-------
ParamStmt : IPParamStmt
Initialized IPParamStmt class.
"""
ParamStmt.__init__(self, param)
self.ip = ip
def to_gerber(self):
ip = 'POS' if self.ip == 'positive' else 'NEG'
return '%IP{0}*%'.format(ip)
def __str__(self):
return ('<Image Polarity: %s>' % self.ip)
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
self.b = b
def to_gerber(self):
ret = '%OF'
if self.a:
ret += 'A' + decimal_string(self.a, precision=6)
if self.b:
ret += 'B' + decimal_string(self.b, precision=6)
return ret + '*%'
def __str__(self):
offset_str = ''
if self.a:
offset_str += ('X: %f' % self.a)
if self.b:
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['param']
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'
Returns
-------
ParamStmt : LPParamStmt
Initialized LPParamStmt class.
"""
ParamStmt.__init__(self, param)
self.lp = lp
def to_gerber(self):
lp = 'C' if self.lp == 'clear' else 'D'
return '%LP{0}*%'.format(lp)
def __str__(self):
return '<Level Polarity: %s>' % self.lp
class ADParamStmt(ParamStmt):
""" AD - Gerber Aperture Definition Statement
"""
@classmethod
def from_dict(cls, stmt_dict):
param = stmt_dict.get('param')
d = int(stmt_dict.get('d'))
shape = stmt_dict.get('shape')
modifiers = stmt_dict.get('modifiers')
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
if modifiers is not None:
self.modifiers = [[x for x in m.split("X")] for m in modifiers.split(",")]
else:
self.modifiers = []
def to_gerber(self, settings):
return '%ADD{0}{1},{2}*%'.format(self.d, self.shape,
','.join(['X'.join(e) for e in self.modifiers]))
def __str__(self):
if self.shape == 'C':
shape = 'circle'
elif self.shape == 'R':
shape = 'rectangle'
elif self.shape == 'O':
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
def to_gerber(self):
return '%IN{0}*%'.format(self.name)
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
def to_gerber(self):
return '%LN{0}*%'.format(self.name)
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
format = settings.format
function = stmt_dict['function']
x = stmt_dict.get('x')
y = stmt_dict.get('y')
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)
if y is not None:
y = parse_gerber_value(stmt_dict.get('y'),
format, zeros)
if i is not None:
i = parse_gerber_value(stmt_dict.get('i'),
format, zeros)
if j is not None:
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
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
Returns
-------
Statement : CoordStmt
Initialized CoordStmt class.
"""
Statement.__init__(self, "COORD")
self.zero_suppression = settings.zero_suppression
self.format = settings.format
self.function = function
self.x = x
self.y = y
self.i = i
self.j = j
self.op = op
def to_gerber(self):
ret = ''
if self.function:
ret += self.function
if self.x:
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,
self.format))
if self.i:
ret += 'I{0}'.format(write_gerber_value(self.i, self.zeros,
self.format))
if self.j:
ret += 'J{0}'.format(write_gerber_value(self.j, self.zeros,
self.format))
if self.op:
ret += self.op
return ret + '*'
def __str__(self):
coord_str = ''
if self.function:
coord_str += 'Fn: %s ' % self.function
if self.x:
coord_str += 'X: %f ' % self.x
if self.y:
coord_str += 'Y: %f ' % self.y
if self.i:
coord_str += 'I: %f ' % self.i
if self.j:
coord_str += 'J: %f ' % self.j
if self.op:
if self.op == 'D01':
op = 'Lights On'
elif self.op == 'D02':
op = 'Lights Off'
elif self.op == 'D03':
op = 'Flash'
else:
op = self.op
coord_str += 'Op: %s' % op
return '<Coordinate Statement: %s>' % coord_str
class ApertureStmt(Statement):
""" Aperture 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)
def __str__(self):
return '<Comment: %s>' % self.comment
class EofStmt(Statement):
""" EOF Statement
"""
def __init__(self):
Statement.__init__(self, "EOF")
def to_gerber(self):
return 'M02*'
def __str__(self):
return '<EOF Statement>'
class QuadrantModeStmt(Statement):
@classmethod
def from_gerber(cls, line):
if 'G74' not in line and 'G75' not in line:
raise ValueError('%s is not a valid quadrant mode statement'
% line)
return (cls('single-quadrant') if line[:3] == 'G74'
else cls('multi-quadrant'))
def __init__(self, mode):
super(QuadrantModeStmt, self).__init__('QuadrantMode')
mode = mode.lower()
if mode not in ['single-quadrant', 'multi-quadrant']:
raise ValueError('Quadrant mode must be "single-quadrant" \
or "multi-quadrant"')
self.mode = mode
def to_gerber(self):
return 'G74*' if self.mode == 'single-quadrant' else 'G75*'
class RegionModeStmt(Statement):
@classmethod
def from_gerber(cls, line):
if 'G36' not in line and 'G37' not in line:
raise ValueError('%s is not a valid region mode statement' % line)
return (cls('on') if line[:3] == 'G36' else cls('off'))
def __init__(self, mode):
super(RegionModeStmt, self).__init__('RegionMode')
mode = mode.lower()
if mode not in ['on', 'off']:
raise ValueError('Valid modes are "on" or "off"')
self.mode = mode
def to_gerber(self):
return 'G36*' if self.mode == 'on' else 'G37*'
class UnknownStmt(Statement):
""" Unknown Statement
"""
def __init__(self, line):
Statement.__init__(self, "UNKNOWN")
self.line = line
def to_gerber(self):
return self.line

54
gerber/layers.py Normal file
View file

@ -0,0 +1,54 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
top_copper_ext = ['gtl', 'cmp', 'top', ]
top_copper_name = ['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ]
bottom_copper_ext = ['gbl', 'sld', 'bot', 'sol', ]
bottom_coppper_name = ['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ]
internal_layer_ext = ['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1',
'g2', 'g3', 'g4', 'g5', 'g6', ]
internal_layer_name = ['art', 'internal']
power_plane_name = ['pgp', 'pwr', ]
ground_plane_name = ['gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6', 'gnd',
'ground', ]
top_silk_ext = ['gto', 'sst', 'plc', 'ts', 'skt', ]
top_silk_name = ['sst01', 'topsilk', 'silk', 'slk', 'sst', ]
bottom_silk_ext = ['gbo', 'ssb', 'pls', 'bs', 'skb', ]
bottom_silk_name = ['sst', 'bsilk', 'ssb', 'botsilk', ]
top_mask_ext = ['gts', 'stc', 'tmk', 'smt', 'tr', ]
top_mask_name = ['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
'mst', ]
bottom_mask_ext = ['gbs', 'sts', 'bmk', 'smb', 'br', ]
bottom_mask_name = ['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ]
top_paste_ext = ['gtp', 'tm']
top_paste_name = ['sp01', 'toppaste', 'pst']
bottom_paste_ext = ['gbp', 'bm']
bottom_paste_name = ['sp02', 'botpaste', 'psb']
board_outline_ext = ['gko']
board_outline_name = ['BDR', 'border', 'out', ]

View file

@ -1,370 +0,0 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2013-2014 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
# 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.
import re
import json
class Statement(object):
def __init__(self, type):
self.type = type
def __str__(self):
s = "<{0} ".format(self.__class__.__name__)
for key, value in self.__dict__.items():
s += "{0}={1} ".format(key, value)
s = s.rstrip() + ">"
return s
class ParamStmt(Statement):
def __init__(self, param):
Statement.__init__(self, "PARAM")
self.param = param
class FSParamStmt(ParamStmt):
def __init__(self, param, zero="L", notation="A", x="24", y="24"):
ParamStmt.__init__(self, param)
self.zero = zero
self.notation = notation
self.x = x
self.y = y
def to_gerber(self):
return '%FS{0}{1}X{2}Y{3}*%'.format(self.zero, self.notation,
self.x, self.y)
class MOParamStmt(ParamStmt):
def __init__(self, param, mo):
ParamStmt.__init__(self, param)
self.mo = mo
def to_gerber(self):
return '%MO{0}*%'.format(self.mo)
class IPParamStmt(ParamStmt):
def __init__(self, param, ip):
ParamStmt.__init__(self, param)
self.ip = ip
def to_gerber(self):
return '%IP{0}*%'.format(self.ip)
class OFParamStmt(ParamStmt):
def __init__(self, param, a, b):
ParamStmt.__init__(self, param)
self.a = a
self.b = b
def to_gerber(self):
ret = '%OF'
if self.a:
ret += 'A' + self.a
if self.b:
ret += 'B' + self.b
return ret + '*%'
class LPParamStmt(ParamStmt):
def __init__(self, param, lp):
ParamStmt.__init__(self, param)
self.lp = lp
def to_gerber(self):
return '%LP{0}*%'.format(self.lp)
class ADParamStmt(ParamStmt):
def __init__(self, param, d, shape, modifiers):
ParamStmt.__init__(self, param)
self.d = d
self.shape = shape
if modifiers is not None:
self.modifiers = [[x for x in m.split("X")] for m in modifiers.split(",")]
else:
self.modifiers = []
def to_gerber(self):
return '%ADD{0}{1},{2}*%'.format(self.d, self.shape,
','.join(['X'.join(e) for e in self.modifiers]))
class AMParamStmt(ParamStmt):
def __init__(self, param, name, macro):
ParamStmt.__init__(self, param)
self.name = name
self.macro = macro
def to_gerber(self):
#think this is right...
return '%AM{0}*{1}*%'.format(self.name, self.macro)
class INParamStmt(ParamStmt):
def __init__(self, param, name):
ParamStmt.__init__(self, param)
self.name = name
def to_gerber(self):
return '%IN{0}*%'.format(self.name)
class LNParamStmt(ParamStmt):
def __init__(self, param, name):
ParamStmt.__init__(self, param)
self.name = name
def to_gerber(self):
return '%LN{0}*%'.format(self.name)
class CoordStmt(Statement):
def __init__(self, function, x, y, i, j, op):
Statement.__init__(self, "COORD")
self.function = function
self.x = x
self.y = y
self.i = i
self.j = j
self.op = op
def to_gerber(self):
ret = ''
if self.function:
ret += self.function
if self.x:
ret += 'X{0}'.format(self.x)
if self.y:
ret += 'Y{0}'.format(self.y)
if self.i:
ret += 'I{0}'.format(self.i)
if self.j:
ret += 'J{0}'.format(self.j)
if self.op:
ret += self.op
return ret + '*'
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)
class CommentStmt(Statement):
def __init__(self, comment):
Statement.__init__(self, "COMMENT")
self.comment = comment
def to_gerber(self):
return 'G04{0}*'.format(self.comment)
class EofStmt(Statement):
def __init__(self):
Statement.__init__(self, "EOF")
def to_gerber(self):
return 'M02*'
class UnknownStmt(Statement):
def __init__(self, line):
Statement.__init__(self, "UNKNOWN")
self.line = line
class GerberParser(object):
NUMBER = r"[\+-]?\d+"
DECIMAL = r"[\+-]?\d+([.]?\d+)?"
STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+"
NAME = r"[a-zA-Z_$][a-zA-Z_$0-9]+"
FUNCTION = r"G\d{2}"
COORD_OP = r"D[0]?[123]"
FS = r"(?P<param>FS)(?P<zero>(L|T))?(?P<notation>(A|I))X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])"
MO = r"(?P<param>MO)(?P<mo>(MM|IN))"
IP = r"(?P<param>IP)(?P<ip>(POS|NEG))"
LP = r"(?P<param>LP)(?P<lp>(D|C))"
AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,](?P<modifiers>[^,]*)"
AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,]?(?P<modifiers>[^,]+)?"
AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,]*)"
AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,]*)"
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,]+)?".format(name=NAME)
AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>.*)".format(name=NAME)
# begin deprecated
OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL)
IN = r"(?P<param>IN)(?P<name>.*)"
LN = r"(?P<param>LN)(?P<name>.*)"
# end deprecated
PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, OF, IN, LN)
PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS]
COORD_STMT = re.compile((
r"(?P<function>{function})?"
r"(X(?P<x>{number}))?(Y(?P<y>{number}))?"
r"(I(?P<i>{number}))?(J(?P<j>{number}))?"
r"(?P<op>{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP)))
APERTURE_STMT = re.compile(r"(G54)?D(?P<d>\d+)\*")
#COMMENT_STMT = re.compile(r"G04(?P<comment>{string})(\*)?".format(string=STRING))
#spec is unclear on whether all chars allowed in comment string -
#seems reasonable to be more permissive.
COMMENT_STMT = re.compile(r"G04(?P<comment>[^*]*)(\*)?")
EOF_STMT = re.compile(r"(?P<eof>M02)\*")
def __init__(self, ctx=None):
self.statements = []
self.ctx = ctx
def parse(self, filename):
fp = open(filename, "r")
data = fp.readlines()
for stmt in self._parse(data):
self.statements.append(stmt)
if self.ctx:
self.ctx.evaluate(stmt)
def dump_json(self):
stmts = {"statements": [stmt.__dict__ for stmt in self.statements]}
return json.dumps(stmts)
def dump_str(self):
s = ""
for stmt in self.statements:
s += str(stmt) + "\n"
return s
def dump(self):
self.ctx.dump()
def _parse(self, data):
oldline = ''
for i, line in enumerate(data):
line = oldline + line.strip()
# skip empty lines
if not len(line):
continue
# deal with multi-line parameters
if line.startswith("%") and not line.endswith("%"):
oldline = line
continue
did_something = True # make sure we do at least one loop
while did_something and len(line) > 0:
did_something = False
# coord
(coord, r) = self._match_one(self.COORD_STMT, line)
if coord:
yield CoordStmt(**coord)
line = r
did_something = True
continue
# aperture selection
(aperture, r) = self._match_one(self.APERTURE_STMT, line)
if aperture:
yield ApertureStmt(**aperture)
did_something = True
line = r
continue
# comment
(comment, r) = self._match_one(self.COMMENT_STMT, line)
if comment:
yield CommentStmt(comment["comment"])
did_something = True
line = r
continue
# parameter
(param, r) = self._match_one_from_many(self.PARAM_STMT, line)
if param:
if param["param"] == "FS":
yield FSParamStmt(**param)
elif param["param"] == "MO":
yield MOParamStmt(**param)
elif param["param"] == "IP":
yield IPParamStmt(**param)
elif param["param"] == "LP":
yield LPParamStmt(**param)
elif param["param"] == "AD":
yield ADParamStmt(**param)
elif param["param"] == "AM":
yield AMParamStmt(**param)
elif param["param"] == "OF":
yield OFParamStmt(**param)
elif param["param"] == "IN":
yield INParamStmt(**param)
elif param["param"] == "LN":
yield LNParamStmt(**param)
else:
yield UnknownStmt(line)
did_something = True
line = r
continue
# eof
(eof, r) = self._match_one(self.EOF_STMT, line)
if eof:
yield EofStmt()
did_something = True
line = r
continue
if False:
print self.COORD_STMT.pattern
print self.APERTURE_STMT.pattern
print self.COMMENT_STMT.pattern
print self.EOF_STMT.pattern
for i in self.PARAM_STMT:
print i.pattern
if line.find('*') > 0:
yield UnknownStmt(line)
did_something = True
line = ""
continue
oldline = line
def _match_one(self, expr, data):
match = expr.match(data)
if match is None:
return ({}, None)
else:
return (match.groupdict(), data[match.end(0):])
def _match_one_from_many(self, exprs, data):
for expr in exprs:
match = expr.match(data)
if match:
return (match.groupdict(), data[match.end(0):])
return ({}, None)

228
gerber/primitives.py Normal file
View file

@ -0,0 +1,228 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
from operator import sub
class Primitive(object):
def __init__(self, level_polarity='dark'):
self.level_polarity = level_polarity
def bounding_box(self):
""" Calculate bounding box
will be helpful for sweep & prune during DRC clearance checks.
Return ((min x, max x), (min y, max y))
"""
pass
class Line(Primitive):
"""
"""
def __init__(self, start, end, width, level_polarity='dark'):
super(Line, self).__init__(level_polarity)
self.start = start
self.end = end
self.width = width
@property
def angle(self):
delta_x, delta_y = tuple(map(sub, end, start))
angle = degrees(math.tan(delta_y/delta_x))
return angle
@property
def bounding_box(self):
width_2 = self.width / 2.
min_x = min(self.start[0], self.end[0]) - width_2
max_x = max(self.start[0], self.end[0]) + width_2
min_y = min(self.start[1], self.end[1]) - width_2
max_y = max(self.start[1], self.end[1]) + width_2
return ((min_x, max_x), (min_y, max_y))
class Arc(Primitive):
"""
"""
def __init__(self, start, end, center, direction, width, level_polarity='dark'):
super(Arc, self).__init__(level_polarity)
self.start = start
self.end = end
self.center = center
self.direction = direction
self.width = width
@property
def start_angle(self):
dy, dx = map(sub, self.start, self.center)
return math.atan2(dy, dx)
@property
def end_angle(self):
dy, dx = map(sub, self.end, self.center)
return math.atan2(dy, dx)
@property
def bounding_box(self):
pass
class Circle(Primitive):
"""
"""
def __init__(self, position, diameter, level_polarity='dark'):
super(Circle, self).__init__(level_polarity)
self.position = position
self.diameter = diameter
@property
def radius(self):
return self.diameter / 2.
@property
def bounding_box(self):
min_x = self.position[0] - self.radius
max_x = self.position[0] + self.radius
min_y = self.position[1] - self.radius
max_y = self.position[1] + self.radius
return ((min_x, max_x), (min_y, max_y))
@property
def stroke_width(self):
return self.diameter
class Rectangle(Primitive):
"""
"""
def __init__(self, position, width, height, level_polarity='dark'):
super(Rectangle, self).__init__(level_polarity)
self.position = position
self.width = width
self.height = height
@property
def lower_left(self):
return (self.position[0] - (self.width / 2.),
self.position[1] - (self.height / 2.))
@property
def upper_right(self):
return (self.position[0] + (self.width / 2.),
self.position[1] + (self.height / 2.))
@property
def bounding_box(self):
min_x = self.lower_left[0]
max_x = self.upper_right[0]
min_y = self.lower_left[1]
max_y = self.upper_right[1]
return ((min_x, max_x), (min_y, max_y))
@property
def stroke_width(self):
return max((self.width, self.height))
class Obround(Primitive):
"""
"""
def __init__(self, position, width, height, level_polarity='dark'):
super(Obround, self).__init__(level_polarity)
self.position = position
self.width = width
self.height = height
@property
def orientation(self):
return 'vertical' if self.height > self.width else 'horizontal'
@property
def lower_left(self):
return (self.position[0] - (self.width / 2.),
self.position[1] - (self.height / 2.))
@property
def upper_right(self):
return (self.position[0] + (self.width / 2.),
self.position[1] + (self.height / 2.))
@property
def bounding_box(self):
min_x = self.lower_left[0]
max_x = self.upper_right[0]
min_y = self.lower_left[1]
max_y = self.upper_right[1]
return ((min_x, max_x), (min_y, max_y))
class Polygon(Primitive):
"""
"""
def __init__(self, position, sides, radius, level_polarity='dark'):
super(Polygon, self).__init__(level_polarity)
self.position = position
self.sides = sides
self.radius = radius
@property
def bounding_box(self):
min_x = self.position[0] - self.radius
max_x = self.position[0] + self.radius
min_y = self.position[1] - self.radius
max_y = self.position[1] + self.radius
return ((min_x, max_x), (min_y, max_y))
class Region(Primitive):
"""
"""
def __init__(self, points, level_polarity='dark'):
super(Region, self).__init__(level_polarity)
self.points = points
@property
def bounding_box(self):
x_list, y_list = zip(*self.points)
min_x = min(x_list)
max_x = max(x_list)
min_y = min(y_list)
max_y = max(y_list)
return ((min_x, max_x), (min_y, max_y))
class Drill(Primitive):
"""
"""
def __init__(self, position, diameter):
super(Drill, self).__init__('dark')
self.position = position
self.diameter = diameter
@property
def radius(self):
return self.diameter / 2.
@property
def bounding_box(self):
min_x = self.position[0] - self.radius
max_x = self.position[0] + self.radius
min_y = self.position[1] - self.radius
max_y = self.position[1] + self.radius
return ((min_x, max_x), (min_y, max_y))

View file

@ -1,188 +0,0 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2013-2014 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
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .parser import CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt
IMAGE_POLARITY_POSITIVE = 1
IMAGE_POLARITY_NEGATIVE = 2
LEVEL_POLARITY_DARK = 1
LEVEL_POLARITY_CLEAR = 2
NOTATION_ABSOLUTE = 1
NOTATION_INCREMENTAL = 2
UNIT_INCH = 1
UNIT_MM = 2
INTERPOLATION_LINEAR = 1
INTERPOLATION_ARC = 2
class GerberCoordFormat(object):
def __init__(self, zeroes, x, y):
self.omit_leading_zeroes = True if zeroes == "L" else False
self.omit_trailing_zeroes = True if zeroes == "T" else False
self.x_int_digits, self.x_dec_digits = [int(d) for d in x]
self.y_int_digits, self.y_dec_digits = [int(d) for d in y]
def resolve(self, x, y):
new_x = x.replace("+", "") if x else None
new_y = y.replace("+", "") if y else None
if new_x is not None:
negative = "-" in new_x
new_x = new_x.replace("-", "")
missing_zeroes = (self.x_int_digits + self.x_dec_digits) - len(new_x)
if missing_zeroes and self.omit_leading_zeroes:
new_x = (missing_zeroes * "0") + new_x
elif missing_zeroes and self.omit_trailing_zeroes:
new_x += missing_zeroes * "0"
new_x = float("{0}{1}.{2}".format("-" if negative else "",
new_x[:self.x_int_digits],
new_x[self.x_int_digits:]))
if new_y is not None:
negative = "-" in new_y
new_y = new_y.replace("-", "")
missing_zeroes = (self.y_int_digits + self.y_dec_digits) - len(new_y)
if missing_zeroes and self.omit_leading_zeroes:
new_y = (missing_zeroes * "0") + new_y
elif missing_zeroes and self.omit_trailing_zeroes:
new_y += missing_zeroes * "0"
new_y = float("{0}{1}.{2}".format("-" if negative else "",
new_y[:self.y_int_digits],
new_y[self.y_int_digits:]))
return new_x, new_y
class GerberContext(object):
coord_format = None
coord_notation = NOTATION_ABSOLUTE
coord_unit = None
x = 0
y = 0
aperture = 0
interpolation = INTERPOLATION_LINEAR
image_polarity = IMAGE_POLARITY_POSITIVE
level_polarity = LEVEL_POLARITY_DARK
def __init__(self):
pass
def set_coord_format(self, zeroes, x, y):
self.coord_format = GerberCoordFormat(zeroes, x, y)
def set_coord_notation(self, notation):
self.coord_notation = NOTATION_ABSOLUTE if notation == "A" else NOTATION_INCREMENTAL
def set_coord_unit(self, unit):
self.coord_unit = UNIT_INCH if unit == "IN" else UNIT_MM
def set_image_polarity(self, polarity):
self.image_polarity = IMAGE_POLARITY_POSITIVE if polarity == "POS" else IMAGE_POLARITY_NEGATIVE
def set_level_polarity(self, polarity):
self.level_polarity = LEVEL_POLARITY_DARK if polarity == "D" else LEVEL_POLARITY_CLEAR
def set_interpolation(self, interpolation):
self.interpolation = INTERPOLATION_LINEAR if interpolation in ("G01", "G1") else INTERPOLATION_ARC
def set_aperture(self, d):
self.aperture = d
def resolve(self, x, y):
x, y = self.coord_format.resolve(x, y)
return x if x is not None else self.x, y if y is not None else self.y
def define_aperture(self, d, shape, modifiers):
pass
def move(self, x, y, resolve=True):
if resolve:
self.x, self.y = self.resolve(x, y)
else:
self.x, self.y = x, y
def stroke(self, x, y):
pass
def line(self, x, y):
pass
def arc(self, x, y):
pass
def flash(self, x, y):
pass
def drill(self, x, y, diameter):
pass
def evaluate(self, stmt):
if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)):
return
elif isinstance(stmt, ParamStmt):
self._evaluate_param(stmt)
elif isinstance(stmt, CoordStmt):
self._evaluate_coord(stmt)
elif isinstance(stmt, ApertureStmt):
self._evaluate_aperture(stmt)
else:
raise Exception("Invalid statement to evaluate")
def _evaluate_param(self, stmt):
if stmt.param == "FS":
self.set_coord_format(stmt.zero, stmt.x, stmt.y)
self.set_coord_notation(stmt.notation)
elif stmt.param == "MO:":
self.set_coord_unit(stmt.mo)
elif stmt.param == "IP:":
self.set_image_polarity(stmt.ip)
elif stmt.param == "LP:":
self.set_level_polarity(stmt.lp)
elif stmt.param == "AD":
self.define_aperture(stmt.d, stmt.shape, stmt.modifiers)
def _evaluate_coord(self, stmt):
if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"):
self.set_interpolation(stmt.function)
if stmt.op == "D01":
self.stroke(stmt.x, stmt.y)
elif stmt.op == "D02":
self.move(stmt.x, stmt.y)
elif stmt.op == "D03":
self.flash(stmt.x, stmt.y)
def _evaluate_aperture(self, stmt):
self.set_aperture(stmt.d)

28
gerber/render/__init__.py Normal file
View file

@ -0,0 +1,28 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
gerber.render
============
**Gerber Renderers**
This module provides contexts for rendering images of gerber layers. Currently
SVG is the only supported format.
"""
from svgwrite_backend import GerberSvgContext

View file

@ -0,0 +1,76 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
gerber.render.apertures
============
**Gerber Aperture base classes**
This module provides base classes for gerber apertures. These are used by
the rendering engine to draw the gerber file.
"""
import math
class Aperture(object):
""" Gerber Aperture base class
"""
def draw(self, ctx, x, y):
raise NotImplementedError('The draw method must be implemented \
in an Aperture subclass.')
def flash(self, ctx, x, y):
raise NotImplementedError('The flash method must be implemented \
in an Aperture subclass.')
def _arc_params(self, startx, starty, x, y, i, j):
center = (startx + i, starty + j)
radius = math.sqrt(math.pow(center[0] - x, 2) +
math.pow(center[1] - y, 2))
delta_x0 = startx - center[0]
delta_y0 = center[1] - starty
delta_x1 = x - center[0]
delta_y1 = center[1] - y
start_angle = math.atan2(delta_y0, delta_x0)
end_angle = math.atan2(delta_y1, delta_x1)
return {'center': center, 'radius': radius,
'start_angle': start_angle, 'end_angle': end_angle}
class Circle(Aperture):
""" Circular Aperture base class
"""
def __init__(self, diameter=0.0):
self.diameter = diameter
class Rect(Aperture):
""" Rectangular Aperture base class
"""
def __init__(self, size=(0, 0)):
self.size = size
class Obround(Aperture):
""" Obround Aperture base class
"""
def __init__(self, size=(0, 0)):
self.size = size
class Polygon(Aperture):
""" Polygon Aperture base class
"""
pass

174
gerber/render/render.py Normal file
View file

@ -0,0 +1,174 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Modified from code 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
# 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.
"""
Rendering
============
**Gerber (RS-274X) and Excellon file rendering**
Render Gerber and Excellon files to a variety of formats. The render module
currently supports SVG rendering using the `svgwrite` library.
"""
from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt,
CoordStmt, ApertureStmt, RegionModeStmt,
QuadrantModeStmt,
)
from ..primitives import *
class GerberContext(object):
""" Gerber rendering context base class
Provides basic functionality and API for rendering gerber files. Medium-
specific renderers should subclass GerberContext and implement the drawing
functions. Colors are stored internally as 32-bit RGB and may need to be
converted to a native format in the rendering subclass.
Attributes
----------
units : string
Measurement units
color : tuple (<float>, <float>, <float>)
Color used for rendering as a tuple of normalized (red, green, blue) values.
drill_color : tuple (<float>, <float>, <float>)
Color used for rendering drill hits. Format is the same as for `color`.
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, units='inch'):
self.units = units
self.color = (0.7215, 0.451, 0.200)
self.drill_color = (0.25, 0.25, 0.25)
self.background_color = (0.0, 0.0, 0.0)
self.alpha = 1.0
def set_units(self, units):
""" Set context measurement units
Parameters
----------
unit : string
Measurement units. may be 'inch' or 'metric'
Raises
------
ValueError
If `unit` is not 'inch' or 'metric'
"""
if units not in ('inch', 'metric'):
raise ValueError('Units may be "inch" or "metric"')
self.units = units
def set_color(self, color):
""" Set rendering color.
Parameters
----------
color : tuple (<float>, <float>, <float>)
Color as a tuple of (red, green, blue) values. Each channel is
represented as a float value in (0, 1)
"""
self.color = color
def set_drill_color(self, color):
""" Set color used for rendering drill hits.
Parameters
----------
color : tuple (<float>, <float>, <float>)
Color as a tuple of (red, green, blue) values. Each channel is
represented as a float value in (0, 1)
"""
self.drill_color = color
def set_background_color(self, color):
""" Set rendering background color
Parameters
----------
color : tuple (<float>, <float>, <float>)
Color as a tuple of (red, green, blue) values. Each channel is
represented as a float value in (0, 1)
"""
self.background_color = color
def set_alpha(self, alpha):
""" Set layer rendering opacity
.. note::
Not all backends/rendering devices support this parameter.
Parameters
----------
alpha : float
Rendering opacity. must be between 0.0 (transparent) and 1.0 (opaque)
"""
self.alpha = alpha
def render(self, primitive):
color = (self.color if primitive.level_polarity == 'dark'
else self.background_color)
if isinstance(primitive, Line):
self._render_line(primitive, color)
elif isinstance(primitive, Arc):
self._render_arc(primitive, color)
elif isinstance(primitive, Region):
self._render_region(primitive, color)
elif isinstance(primitive, Circle):
self._render_circle(primitive, color)
elif isinstance(primitive, Rectangle):
self._render_rectangle(primitive, color)
elif isinstance(primitive, Obround):
self._render_obround(primitive, color)
elif isinstance(primitive, Polygon):
self._render_polygon(Polygon, color)
elif isinstance(primitive, Drill):
self._render_drill(primitive, self.drill_color)
else:
return
def _render_line(self, primitive, color):
pass
def _render_arc(self, primitive, color):
pass
def _render_region(self, primitive, color):
pass
def _render_circle(self, primitive, color):
pass
def _render_rectangle(self, primitive, color):
pass
def _render_obround(self, primitive, color):
pass
def _render_polygon(self, primitive, color):
pass
def _render_drill(self, primitive, color):
pass

View file

@ -0,0 +1,155 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Based on render_svg.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
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .render import GerberContext
from operator import mul
import svgwrite
SCALE = 300
def svg_color(color):
color = tuple([int(ch * 255) for ch in color])
return 'rgb(%d, %d, %d)' % color
class GerberSvgContext(GerberContext):
def __init__(self):
GerberContext.__init__(self)
self.scale = (SCALE, -SCALE)
self.dwg = svgwrite.Drawing()
self.background = False
def dump(self, filename):
self.dwg.saveas(filename)
def set_bounds(self, bounds):
xbounds, ybounds = bounds
size = (SCALE * (xbounds[1] - xbounds[0]),
SCALE * (ybounds[1] - ybounds[0]))
if not self.background:
vbox = '%f, %f, %f, %f' % (SCALE * xbounds[0], -SCALE * ybounds[1],
size[0], size[1])
self.dwg = svgwrite.Drawing(viewBox=vbox)
rect = self.dwg.rect(insert=(SCALE * xbounds[0],
-SCALE * ybounds[1]),
size=size,
fill=svg_color(self.background_color))
self.dwg.add(rect)
self.background = True
def _render_line(self, line, color):
start = map(mul, line.start, self.scale)
end = map(mul, line.end, self.scale)
aline = self.dwg.line(start=start, end=end,
stroke=svg_color(color),
stroke_width=SCALE * line.width,
stroke_linecap='round')
aline.stroke(opacity=self.alpha)
self.dwg.add(aline)
def _render_region(self, region, color):
points = [tuple(map(mul, point, self.scale)) for point in region.points]
region_path = self.dwg.path(d='M %f, %f' % points[0],
fill=svg_color(color),
stroke='none')
region_path.fill(opacity=self.alpha)
for point in points[1:]:
region_path.push('L %f, %f' % point)
self.dwg.add(region_path)
def _render_circle(self, circle, color):
center = map(mul, circle.position, self.scale)
acircle = self.dwg.circle(center=center,
r = SCALE * circle.radius,
fill=svg_color(color))
acircle.fill(opacity=self.alpha)
self.dwg.add(acircle)
def _render_rectangle(self, rectangle, color):
center = map(mul, rectangle.position, self.scale)
size = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale)))
insert = center[0] - size[0] / 2., center[1] - size[1] / 2.
arect = self.dwg.rect(insert=insert, size=size,
fill=svg_color(color))
arect.fill(opacity=self.alpha)
self.dwg.add(arect)
def _render_obround(self, obround, color):
x, y = tuple(map(mul, obround.position, self.scale))
xsize, ysize = tuple(map(mul, (obround.width, obround.height),
self.scale))
xscale, yscale = self.scale
# Corner case...
if xsize == ysize:
circle = self.dwg.circle(center=(x, y),
r = (xsize / 2.0),
fill=svg_color(color))
circle.fill(opacity=self.alpha)
self.dwg.add(circle)
# Horizontal obround
elif xsize > ysize:
rectx = xsize - ysize
recty = ysize
c1 = self.dwg.circle(center=(x - (rectx / 2.0), y),
r = (ysize / 2.0),
fill=svg_color(color))
c2 = self.dwg.circle(center=(x + (rectx / 2.0), y),
r = (ysize / 2.0),
fill=svg_color(color))
rect = self.dwg.rect(insert=(x, y),
size=(xsize, ysize),
fill=svg_color(color))
c1.fill(opacity=self.alpha)
c2.fill(opacity=self.alpha)
rect.fill(opacity=self.alpha)
self.dwg.add(c1)
self.dwg.add(c2)
self.dwg.add(rect)
# Vertical obround
else:
rectx = xsize
recty = ysize - xsize
c1 = self.dwg.circle(center=(x, y - (recty / 2.)),
r = (xsize / 2.),
fill=svg_color(color))
c2 = self.dwg.circle(center=(x, y + (recty / 2.)),
r = (xsize / 2.),
fill=svg_color(color))
rect = self.dwg.rect(insert=(x, y),
size=(xsize, ysize),
fill=svg_color(color))
c1.fill(opacity=self.alpha)
c2.fill(opacity=self.alpha)
rect.fill(opacity=self.alpha)
self.dwg.add(c1)
self.dwg.add(c2)
self.dwg.add(rect)
def _render_drill(self, primitive, color):
center = map(mul, primitive.position, self.scale)
hit = self.dwg.circle(center=center, r=SCALE * primitive.radius,
fill=svg_color(color))
self.dwg.add(hit)

View file

@ -1,114 +0,0 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2013-2014 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
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .render import GerberContext, INTERPOLATION_LINEAR, INTERPOLATION_ARC
import svgwrite
class Shape(object):
pass
class Circle(Shape):
def __init__(self, diameter=0.0):
self.diameter = diameter
def draw(self, ctx, x, y):
return ctx.dwg.line(start=(ctx.x*300, ctx.y*300), end=(x*300, y*300), stroke="rgb(184, 115, 51)",
stroke_width=2, stroke_linecap="round")
def flash(self, ctx, x, y):
return ctx.dwg.circle(center=(x*300, y*300), r=300*(self.diameter/2.0), fill="rgb(184, 115, 51)")
class Rect(Shape):
def __init__(self, size=(0, 0)):
self.size = size
def draw(self, ctx, x, y):
return ctx.dwg.line(start=(ctx.x*300, ctx.y*300), end=(x*300, y*300), stroke="rgb(184, 115, 51)",
stroke_width=2, stroke_linecap="butt")
def flash(self, ctx, x, y):
# Center the rectange on x,y
x -= (self.size[0] / 2.0)
y -= (self.size[0] / 2.0)
return ctx.dwg.rect(insert=(300*x, 300*y), size=(300*float(self.size[0]), 300*float(self.size[1])),
fill="rgb(184, 115, 51)")
class GerberSvgContext(GerberContext):
def __init__(self):
GerberContext.__init__(self)
self.apertures = {}
self.dwg = svgwrite.Drawing()
self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black"))
def define_aperture(self, d, shape, modifiers):
aperture = None
if shape == "C":
aperture = Circle(diameter=float(modifiers[0][0]))
elif shape == "R":
aperture = Rect(size=modifiers[0][0:2])
self.apertures[d] = aperture
def stroke(self, x, y):
super(GerberSvgContext, self).stroke(x, y)
if self.interpolation == INTERPOLATION_LINEAR:
self.line(x, y)
elif self.interpolation == INTERPOLATION_ARC:
self.arc(x, y)
def line(self, x, y):
super(GerberSvgContext, self).line(x, y)
x, y = self.resolve(x, y)
ap = self.apertures.get(str(self.aperture), None)
if ap is None:
return
self.dwg.add(ap.draw(self, x, y))
self.move(x, y, resolve=False)
def arc(self, x, y):
super(GerberSvgContext, self).arc(x, y)
def flash(self, x, y):
super(GerberSvgContext, self).flash(x, y)
x, y = self.resolve(x, y)
ap = self.apertures.get(str(self.aperture), None)
if ap is None:
return
self.dwg.add(ap.flash(self, x, y))
self.move(x, y, resolve=False)
def drill(self, x, y, diameter):
hit = self.dwg.circle(center=(x*300, y*300), r=300*(diameter/2.0), fill="gray")
self.dwg.add(hit)
def dump(self,filename='teste.svg'):
self.dwg.saveas(filename)

434
gerber/rs274x.py Normal file
View file

@ -0,0 +1,434 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# 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
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" This module provides an RS-274-X class and parser.
"""
import copy
import json
import re
from .gerber_statements import *
from .primitives import *
from .cam import CamFile, FileSettings
def read(filename):
""" Read data from filename and return a GerberFile
Parameters
----------
filename : string
Filename of file to parse
Returns
-------
file : :class:`gerber.rs274x.GerberFile`
A GerberFile created from the specified file.
"""
return GerberParser().parse(filename)
class GerberFile(CamFile):
""" A class representing a single gerber file
The GerberFile class represents a single gerber file.
Parameters
----------
statements : list
list of gerber file statements
settings : dict
Dictionary of gerber file settings
filename : string
Filename of the source gerber file
Attributes
----------
comments: list of strings
List of comments contained in the gerber file.
size : tuple, (<float>, <float>)
Size in [self.units] of the layer described by the gerber file.
bounds: tuple, ((<float>, <float>), (<float>, <float>))
boundaries of the layer described by the gerber file.
`bounds` is stored as ((min x, max x), (min y, max y))
"""
def __init__(self, statements, settings, primitives, filename=None):
super(GerberFile, self).__init__(statements, settings, primitives, filename)
@property
def comments(self):
return [comment.comment for comment in self.statements
if isinstance(comment, CommentStmt)]
@property
def size(self):
xbounds, ybounds = self.bounds
return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0])
@property
def bounds(self):
xbounds = [0.0, 0.0]
ybounds = [0.0, 0.0]
for stmt in [stmt for stmt in self.statements
if isinstance(stmt, CoordStmt)]:
if stmt.x is not None:
if stmt.x < xbounds[0]:
xbounds[0] = stmt.x
elif stmt.x > xbounds[1]:
xbounds[1] = stmt.x
if stmt.y is not None:
if stmt.y < ybounds[0]:
ybounds[0] = stmt.y
elif stmt.y > ybounds[1]:
ybounds[1] = stmt.y
return (xbounds, ybounds)
def write(self, filename):
""" Write data out to a gerber file
"""
with open(filename, 'w') as f:
for statement in self.statements:
f.write(statement.to_gerber())
class GerberParser(object):
""" GerberParser
"""
NUMBER = r"[\+-]?\d+"
DECIMAL = r"[\+-]?\d+([.]?\d+)?"
STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+"
NAME = r"[a-zA-Z_$][a-zA-Z_$0-9]+"
FUNCTION = r"G\d{2}"
COORD_OP = r"D[0]?[123]"
FS = r"(?P<param>FS)(?P<zero>(L|T))?(?P<notation>(A|I))X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])"
MO = r"(?P<param>MO)(?P<mo>(MM|IN))"
IP = r"(?P<param>IP)(?P<ip>(POS|NEG))"
LP = r"(?P<param>LP)(?P<lp>(D|C))"
AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,](?P<modifiers>[^,]*)"
AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,]?(?P<modifiers>[^,]+)?"
AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,]*)"
AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,]*)"
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,]+)?".format(name=NAME)
AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>.*)".format(name=NAME)
# begin deprecated
OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL)
IN = r"(?P<param>IN)(?P<name>.*)"
LN = r"(?P<param>LN)(?P<name>.*)"
# end deprecated
PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, OF, IN, LN)
PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS]
COORD_STMT = re.compile((
r"(?P<function>{function})?"
r"(X(?P<x>{number}))?(Y(?P<y>{number}))?"
r"(I(?P<i>{number}))?(J(?P<j>{number}))?"
r"(?P<op>{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP)))
APERTURE_STMT = re.compile(r"(G54)?D(?P<d>\d+)\*")
COMMENT_STMT = re.compile(r"G04(?P<comment>[^*]*)(\*)?")
EOF_STMT = re.compile(r"(?P<eof>M02)\*")
REGION_MODE_STMT = re.compile(r'(?P<mode>G3[67])\*')
QUAD_MODE_STMT = re.compile(r'(?P<mode>G7[45])\*')
def __init__(self):
self.settings = FileSettings()
self.statements = []
self.primitives = []
self.apertures = {}
self.current_region = None
self.x = 0
self.y = 0
self.aperture = 0
self.interpolation = 'linear'
self.direction = 'clockwise'
self.image_polarity = 'positive'
self.level_polarity = 'dark'
self.region_mode = 'off'
self.quadrant_mode = 'multi-quadrant'
self.step_and_repeat = (1, 1, 0, 0)
def parse(self, filename):
fp = open(filename, "r")
data = fp.readlines()
for stmt in self._parse(data):
self.evaluate(stmt)
self.statements.append(stmt)
return GerberFile(self.statements, self.settings, self.primitives, filename)
def dump_json(self):
stmts = {"statements": [stmt.__dict__ for stmt in self.statements]}
return json.dumps(stmts)
def dump_str(self):
s = ""
for stmt in self.statements:
s += str(stmt) + "\n"
return s
def _parse(self, data):
oldline = ''
for i, line in enumerate(data):
line = oldline + line.strip()
# skip empty lines
if not len(line):
continue
# deal with multi-line parameters
if line.startswith("%") and not line.endswith("%"):
oldline = line
continue
did_something = True # make sure we do at least one loop
while did_something and len(line) > 0:
did_something = False
# Region Mode
(mode, r) = _match_one(self.REGION_MODE_STMT, line)
if mode:
yield RegionModeStmt.from_gerber(line)
line = r
did_something = True
continue
# Quadrant Mode
(mode, r) = _match_one(self.QUAD_MODE_STMT, line)
if mode:
yield QuadrantModeStmt.from_gerber(line)
line = r
did_something = True
continue
# coord
(coord, r) = _match_one(self.COORD_STMT, line)
if coord:
yield CoordStmt.from_dict(coord, self.settings)
line = r
did_something = True
continue
# aperture selection
(aperture, r) = _match_one(self.APERTURE_STMT, line)
if aperture:
yield ApertureStmt(**aperture)
did_something = True
line = r
continue
# comment
(comment, r) = _match_one(self.COMMENT_STMT, line)
if comment:
yield CommentStmt(comment["comment"])
did_something = True
line = r
continue
# parameter
(param, r) = _match_one_from_many(self.PARAM_STMT, line)
if param:
if param["param"] == "FS":
stmt = FSParamStmt.from_dict(param)
self.settings.zero_suppression = stmt.zero_suppression
self.settings.format = stmt.format
self.settings.notation = stmt.notation
yield stmt
elif param["param"] == "MO":
stmt = MOParamStmt.from_dict(param)
self.settings.units = stmt.mode
yield stmt
elif param["param"] == "IP":
yield IPParamStmt.from_dict(param)
elif param["param"] == "LP":
yield LPParamStmt.from_dict(param)
elif param["param"] == "AD":
yield ADParamStmt.from_dict(param)
elif param["param"] == "AM":
yield AMParamStmt.from_dict(param)
elif param["param"] == "OF":
yield OFParamStmt.from_dict(param)
elif param["param"] == "IN":
yield INParamStmt.from_dict(param)
elif param["param"] == "LN":
yield LNParamStmt.from_dict(param)
else:
yield UnknownStmt(line)
did_something = True
line = r
continue
# eof
(eof, r) = _match_one(self.EOF_STMT, line)
if eof:
yield EofStmt()
did_something = True
line = r
continue
if False:
print self.COORD_STMT.pattern
print self.APERTURE_STMT.pattern
print self.COMMENT_STMT.pattern
print self.EOF_STMT.pattern
for i in self.PARAM_STMT:
print i.pattern
if line.find('*') > 0:
yield UnknownStmt(line)
oldline = line
def evaluate(self, stmt):
""" Evaluate Gerber statement and update image accordingly.
This method is called once for each statement in the file as it
is parsed.
Parameters
----------
statement : Statement
Gerber/Excellon statement to evaluate.
"""
if isinstance(stmt, CoordStmt):
self._evaluate_coord(stmt)
elif isinstance(stmt, ParamStmt):
self._evaluate_param(stmt)
elif isinstance(stmt, ApertureStmt):
self._evaluate_aperture(stmt)
elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)):
self._evaluate_mode(stmt)
elif isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)):
return
else:
raise Exception("Invalid statement to evaluate")
def _define_aperture(self, d, shape, modifiers):
aperture = None
if shape == 'C':
diameter = float(modifiers[0][0])
aperture = Circle(position=None, diameter=diameter)
elif shape == 'R':
width = float(modifiers[0][0])
height = float(modifiers[0][1])
aperture = Rectangle(position=None, width=width, height=height)
elif shape == 'O':
width = float(modifiers[0][0])
height = float(modifiers[0][1])
aperture = Obround(position=None, width=width, height=height)
self.apertures[d] = aperture
def _evaluate_mode(self, stmt):
if stmt.type == 'RegionMode':
if self.region_mode == 'on' and stmt.mode == 'off':
self.primitives.append(Region(self.current_region, self.level_polarity))
self.current_region = None
self.region_mode = stmt.mode
elif stmt.type == 'QuadrantMode':
self.quadrant_mode = stmt.mode
def _evaluate_param(self, stmt):
if stmt.param == "FS":
self.settings.zero_suppression = stmt.zero_suppression
self.settings.format = stmt.format
self.settings.notation = stmt.notation
elif stmt.param == "MO":
self.settings.units = stmt.mode
elif stmt.param == "IP":
self.image_polarity = stmt.ip
elif stmt.param == "LP":
self.level_polarity = stmt.lp
elif stmt.param == "AD":
self._define_aperture(stmt.d, stmt.shape, stmt.modifiers)
def _evaluate_coord(self, stmt):
x = self.x if stmt.x is None else stmt.x
y = self.y if stmt.y is None else stmt.y
if stmt.function in ("G01", "G1"):
self.interpolation = 'linear'
elif stmt.function in ('G02', 'G2', 'G03', 'G3'):
self.interpolation = 'arc'
self.direction = ('clockwise' if stmt.function in ('G02', 'G2')
else 'counterclockwise')
if stmt.op == "D01":
if self.region_mode == 'on':
if self.current_region is None:
self.current_region = [(self.x, self.y), ]
self.current_region.append((x, y,))
else:
start = (self.x, self.y)
end = (x, y)
width = self.apertures[self.aperture].stroke_width
if self.interpolation == 'linear':
self.primitives.append(Line(start, end, width, self.level_polarity))
else:
center = (start[0] + stmt.i, start[1] + stmt.j)
self.primitives.append(Arc(start, end, center, self.direction, width, self.level_polarity))
elif stmt.op == "D02":
pass
elif stmt.op == "D03":
primitive = copy.deepcopy(self.apertures[self.aperture])
primitive.position = (x, y)
primitive.level_polarity = self.level_polarity
self.primitives.append(primitive)
self.x, self.y = x, y
def _evaluate_aperture(self, stmt):
self.aperture = stmt.d
def _match_one(expr, data):
match = expr.match(data)
if match is None:
return ({}, None)
else:
return (match.groupdict(), data[match.end(0):])
def _match_one_from_many(exprs, data):
for expr in exprs:
match = expr.match(data)
if match:
return (match.groupdict(), data[match.end(0):])
return ({}, None)

0
gerber/tests/__init__.py Normal file
View file

View file

@ -0,0 +1,503 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.0000*%
%ADD11C,0.0004*%
D10*
X000300Y003064D02*
X000300Y018064D01*
X022800Y018064D01*
X022800Y003064D01*
X000300Y003064D01*
X001720Y005114D02*
X001722Y005164D01*
X001728Y005214D01*
X001738Y005263D01*
X001752Y005311D01*
X001769Y005358D01*
X001790Y005403D01*
X001815Y005447D01*
X001843Y005488D01*
X001875Y005527D01*
X001909Y005564D01*
X001946Y005598D01*
X001986Y005628D01*
X002028Y005655D01*
X002072Y005679D01*
X002118Y005700D01*
X002165Y005716D01*
X002213Y005729D01*
X002263Y005738D01*
X002312Y005743D01*
X002363Y005744D01*
X002413Y005741D01*
X002462Y005734D01*
X002511Y005723D01*
X002559Y005708D01*
X002605Y005690D01*
X002650Y005668D01*
X002693Y005642D01*
X002734Y005613D01*
X002773Y005581D01*
X002809Y005546D01*
X002841Y005508D01*
X002871Y005468D01*
X002898Y005425D01*
X002921Y005381D01*
X002940Y005335D01*
X002956Y005287D01*
X002968Y005238D01*
X002976Y005189D01*
X002980Y005139D01*
X002980Y005089D01*
X002976Y005039D01*
X002968Y004990D01*
X002956Y004941D01*
X002940Y004893D01*
X002921Y004847D01*
X002898Y004803D01*
X002871Y004760D01*
X002841Y004720D01*
X002809Y004682D01*
X002773Y004647D01*
X002734Y004615D01*
X002693Y004586D01*
X002650Y004560D01*
X002605Y004538D01*
X002559Y004520D01*
X002511Y004505D01*
X002462Y004494D01*
X002413Y004487D01*
X002363Y004484D01*
X002312Y004485D01*
X002263Y004490D01*
X002213Y004499D01*
X002165Y004512D01*
X002118Y004528D01*
X002072Y004549D01*
X002028Y004573D01*
X001986Y004600D01*
X001946Y004630D01*
X001909Y004664D01*
X001875Y004701D01*
X001843Y004740D01*
X001815Y004781D01*
X001790Y004825D01*
X001769Y004870D01*
X001752Y004917D01*
X001738Y004965D01*
X001728Y005014D01*
X001722Y005064D01*
X001720Y005114D01*
X001670Y016064D02*
X001672Y016114D01*
X001678Y016164D01*
X001688Y016213D01*
X001702Y016261D01*
X001719Y016308D01*
X001740Y016353D01*
X001765Y016397D01*
X001793Y016438D01*
X001825Y016477D01*
X001859Y016514D01*
X001896Y016548D01*
X001936Y016578D01*
X001978Y016605D01*
X002022Y016629D01*
X002068Y016650D01*
X002115Y016666D01*
X002163Y016679D01*
X002213Y016688D01*
X002262Y016693D01*
X002313Y016694D01*
X002363Y016691D01*
X002412Y016684D01*
X002461Y016673D01*
X002509Y016658D01*
X002555Y016640D01*
X002600Y016618D01*
X002643Y016592D01*
X002684Y016563D01*
X002723Y016531D01*
X002759Y016496D01*
X002791Y016458D01*
X002821Y016418D01*
X002848Y016375D01*
X002871Y016331D01*
X002890Y016285D01*
X002906Y016237D01*
X002918Y016188D01*
X002926Y016139D01*
X002930Y016089D01*
X002930Y016039D01*
X002926Y015989D01*
X002918Y015940D01*
X002906Y015891D01*
X002890Y015843D01*
X002871Y015797D01*
X002848Y015753D01*
X002821Y015710D01*
X002791Y015670D01*
X002759Y015632D01*
X002723Y015597D01*
X002684Y015565D01*
X002643Y015536D01*
X002600Y015510D01*
X002555Y015488D01*
X002509Y015470D01*
X002461Y015455D01*
X002412Y015444D01*
X002363Y015437D01*
X002313Y015434D01*
X002262Y015435D01*
X002213Y015440D01*
X002163Y015449D01*
X002115Y015462D01*
X002068Y015478D01*
X002022Y015499D01*
X001978Y015523D01*
X001936Y015550D01*
X001896Y015580D01*
X001859Y015614D01*
X001825Y015651D01*
X001793Y015690D01*
X001765Y015731D01*
X001740Y015775D01*
X001719Y015820D01*
X001702Y015867D01*
X001688Y015915D01*
X001678Y015964D01*
X001672Y016014D01*
X001670Y016064D01*
X020060Y012714D02*
X020062Y012764D01*
X020068Y012814D01*
X020078Y012863D01*
X020091Y012912D01*
X020109Y012959D01*
X020130Y013005D01*
X020154Y013048D01*
X020182Y013090D01*
X020213Y013130D01*
X020247Y013167D01*
X020284Y013201D01*
X020324Y013232D01*
X020366Y013260D01*
X020409Y013284D01*
X020455Y013305D01*
X020502Y013323D01*
X020551Y013336D01*
X020600Y013346D01*
X020650Y013352D01*
X020700Y013354D01*
X020750Y013352D01*
X020800Y013346D01*
X020849Y013336D01*
X020898Y013323D01*
X020945Y013305D01*
X020991Y013284D01*
X021034Y013260D01*
X021076Y013232D01*
X021116Y013201D01*
X021153Y013167D01*
X021187Y013130D01*
X021218Y013090D01*
X021246Y013048D01*
X021270Y013005D01*
X021291Y012959D01*
X021309Y012912D01*
X021322Y012863D01*
X021332Y012814D01*
X021338Y012764D01*
X021340Y012714D01*
X021338Y012664D01*
X021332Y012614D01*
X021322Y012565D01*
X021309Y012516D01*
X021291Y012469D01*
X021270Y012423D01*
X021246Y012380D01*
X021218Y012338D01*
X021187Y012298D01*
X021153Y012261D01*
X021116Y012227D01*
X021076Y012196D01*
X021034Y012168D01*
X020991Y012144D01*
X020945Y012123D01*
X020898Y012105D01*
X020849Y012092D01*
X020800Y012082D01*
X020750Y012076D01*
X020700Y012074D01*
X020650Y012076D01*
X020600Y012082D01*
X020551Y012092D01*
X020502Y012105D01*
X020455Y012123D01*
X020409Y012144D01*
X020366Y012168D01*
X020324Y012196D01*
X020284Y012227D01*
X020247Y012261D01*
X020213Y012298D01*
X020182Y012338D01*
X020154Y012380D01*
X020130Y012423D01*
X020109Y012469D01*
X020091Y012516D01*
X020078Y012565D01*
X020068Y012614D01*
X020062Y012664D01*
X020060Y012714D01*
X020170Y016064D02*
X020172Y016114D01*
X020178Y016164D01*
X020188Y016213D01*
X020202Y016261D01*
X020219Y016308D01*
X020240Y016353D01*
X020265Y016397D01*
X020293Y016438D01*
X020325Y016477D01*
X020359Y016514D01*
X020396Y016548D01*
X020436Y016578D01*
X020478Y016605D01*
X020522Y016629D01*
X020568Y016650D01*
X020615Y016666D01*
X020663Y016679D01*
X020713Y016688D01*
X020762Y016693D01*
X020813Y016694D01*
X020863Y016691D01*
X020912Y016684D01*
X020961Y016673D01*
X021009Y016658D01*
X021055Y016640D01*
X021100Y016618D01*
X021143Y016592D01*
X021184Y016563D01*
X021223Y016531D01*
X021259Y016496D01*
X021291Y016458D01*
X021321Y016418D01*
X021348Y016375D01*
X021371Y016331D01*
X021390Y016285D01*
X021406Y016237D01*
X021418Y016188D01*
X021426Y016139D01*
X021430Y016089D01*
X021430Y016039D01*
X021426Y015989D01*
X021418Y015940D01*
X021406Y015891D01*
X021390Y015843D01*
X021371Y015797D01*
X021348Y015753D01*
X021321Y015710D01*
X021291Y015670D01*
X021259Y015632D01*
X021223Y015597D01*
X021184Y015565D01*
X021143Y015536D01*
X021100Y015510D01*
X021055Y015488D01*
X021009Y015470D01*
X020961Y015455D01*
X020912Y015444D01*
X020863Y015437D01*
X020813Y015434D01*
X020762Y015435D01*
X020713Y015440D01*
X020663Y015449D01*
X020615Y015462D01*
X020568Y015478D01*
X020522Y015499D01*
X020478Y015523D01*
X020436Y015550D01*
X020396Y015580D01*
X020359Y015614D01*
X020325Y015651D01*
X020293Y015690D01*
X020265Y015731D01*
X020240Y015775D01*
X020219Y015820D01*
X020202Y015867D01*
X020188Y015915D01*
X020178Y015964D01*
X020172Y016014D01*
X020170Y016064D01*
X020060Y008714D02*
X020062Y008764D01*
X020068Y008814D01*
X020078Y008863D01*
X020091Y008912D01*
X020109Y008959D01*
X020130Y009005D01*
X020154Y009048D01*
X020182Y009090D01*
X020213Y009130D01*
X020247Y009167D01*
X020284Y009201D01*
X020324Y009232D01*
X020366Y009260D01*
X020409Y009284D01*
X020455Y009305D01*
X020502Y009323D01*
X020551Y009336D01*
X020600Y009346D01*
X020650Y009352D01*
X020700Y009354D01*
X020750Y009352D01*
X020800Y009346D01*
X020849Y009336D01*
X020898Y009323D01*
X020945Y009305D01*
X020991Y009284D01*
X021034Y009260D01*
X021076Y009232D01*
X021116Y009201D01*
X021153Y009167D01*
X021187Y009130D01*
X021218Y009090D01*
X021246Y009048D01*
X021270Y009005D01*
X021291Y008959D01*
X021309Y008912D01*
X021322Y008863D01*
X021332Y008814D01*
X021338Y008764D01*
X021340Y008714D01*
X021338Y008664D01*
X021332Y008614D01*
X021322Y008565D01*
X021309Y008516D01*
X021291Y008469D01*
X021270Y008423D01*
X021246Y008380D01*
X021218Y008338D01*
X021187Y008298D01*
X021153Y008261D01*
X021116Y008227D01*
X021076Y008196D01*
X021034Y008168D01*
X020991Y008144D01*
X020945Y008123D01*
X020898Y008105D01*
X020849Y008092D01*
X020800Y008082D01*
X020750Y008076D01*
X020700Y008074D01*
X020650Y008076D01*
X020600Y008082D01*
X020551Y008092D01*
X020502Y008105D01*
X020455Y008123D01*
X020409Y008144D01*
X020366Y008168D01*
X020324Y008196D01*
X020284Y008227D01*
X020247Y008261D01*
X020213Y008298D01*
X020182Y008338D01*
X020154Y008380D01*
X020130Y008423D01*
X020109Y008469D01*
X020091Y008516D01*
X020078Y008565D01*
X020068Y008614D01*
X020062Y008664D01*
X020060Y008714D01*
X020170Y005064D02*
X020172Y005114D01*
X020178Y005164D01*
X020188Y005213D01*
X020202Y005261D01*
X020219Y005308D01*
X020240Y005353D01*
X020265Y005397D01*
X020293Y005438D01*
X020325Y005477D01*
X020359Y005514D01*
X020396Y005548D01*
X020436Y005578D01*
X020478Y005605D01*
X020522Y005629D01*
X020568Y005650D01*
X020615Y005666D01*
X020663Y005679D01*
X020713Y005688D01*
X020762Y005693D01*
X020813Y005694D01*
X020863Y005691D01*
X020912Y005684D01*
X020961Y005673D01*
X021009Y005658D01*
X021055Y005640D01*
X021100Y005618D01*
X021143Y005592D01*
X021184Y005563D01*
X021223Y005531D01*
X021259Y005496D01*
X021291Y005458D01*
X021321Y005418D01*
X021348Y005375D01*
X021371Y005331D01*
X021390Y005285D01*
X021406Y005237D01*
X021418Y005188D01*
X021426Y005139D01*
X021430Y005089D01*
X021430Y005039D01*
X021426Y004989D01*
X021418Y004940D01*
X021406Y004891D01*
X021390Y004843D01*
X021371Y004797D01*
X021348Y004753D01*
X021321Y004710D01*
X021291Y004670D01*
X021259Y004632D01*
X021223Y004597D01*
X021184Y004565D01*
X021143Y004536D01*
X021100Y004510D01*
X021055Y004488D01*
X021009Y004470D01*
X020961Y004455D01*
X020912Y004444D01*
X020863Y004437D01*
X020813Y004434D01*
X020762Y004435D01*
X020713Y004440D01*
X020663Y004449D01*
X020615Y004462D01*
X020568Y004478D01*
X020522Y004499D01*
X020478Y004523D01*
X020436Y004550D01*
X020396Y004580D01*
X020359Y004614D01*
X020325Y004651D01*
X020293Y004690D01*
X020265Y004731D01*
X020240Y004775D01*
X020219Y004820D01*
X020202Y004867D01*
X020188Y004915D01*
X020178Y004964D01*
X020172Y005014D01*
X020170Y005064D01*
D11*
X022869Y007639D02*
X022869Y013789D01*
M02*

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,66 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.0634*%
%ADD11C,0.1360*%
%ADD12C,0.0680*%
%ADD13C,0.1340*%
%ADD14C,0.0476*%
D10*
X017200Y009464D03*
X018200Y009964D03*
X018200Y010964D03*
X017200Y010464D03*
X017200Y011464D03*
X018200Y011964D03*
D11*
X020700Y012714D03*
X020700Y008714D03*
D12*
X018350Y016514D02*
X018350Y017114D01*
X017350Y017114D02*
X017350Y016514D01*
X007350Y016664D02*
X007350Y017264D01*
X006350Y017264D02*
X006350Y016664D01*
X005350Y016664D02*
X005350Y017264D01*
X001800Y012564D02*
X001200Y012564D01*
X001200Y011564D02*
X001800Y011564D01*
X001800Y010564D02*
X001200Y010564D01*
X001200Y009564D02*
X001800Y009564D01*
X001800Y008564D02*
X001200Y008564D01*
D13*
X002350Y005114D03*
X002300Y016064D03*
X020800Y016064D03*
X020800Y005064D03*
D14*
X015650Y006264D03*
X013500Y006864D03*
X012100Y005314D03*
X009250Y004064D03*
X015200Y004514D03*
X013550Y008764D03*
X013350Y010114D03*
X013300Y011464D03*
X011650Y013164D03*
X010000Y015114D03*
X006500Y013714D03*
X004150Y011564D03*
X014250Y014964D03*
X015850Y009914D03*
M02*

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
%
M48
M72
T01C0.0236
T02C0.0354
T03C0.0400
T04C0.1260
T05C0.1280
%
T01
X9250Y4064
X12100Y5314
X13500Y6864
X15650Y6264
X15200Y4514
X13550Y8764
X13350Y10114
X13300Y11464
X11650Y13164
X10000Y15114
X6500Y13714
X4150Y11564
X14250Y14964
X15850Y9914
T02
X17200Y9464
X18200Y9964
X18200Y10964
X17200Y10464
X17200Y11464
X18200Y11964
T03
X18350Y16814
X17350Y16814
X7350Y16964
X6350Y16964
X5350Y16964
X1500Y12564
X1500Y11564
X1500Y10564
X1500Y9564
X1500Y8564
T04
X2350Y5114
X2300Y16064
X20800Y16064
X20800Y5064
T05
X20700Y8714
X20700Y12714
M30

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,162 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10R,0.0340X0.0880*%
%ADD11R,0.0671X0.0237*%
%ADD12R,0.4178X0.4332*%
%ADD13R,0.0930X0.0500*%
%ADD14R,0.0710X0.1655*%
%ADD15R,0.0671X0.0592*%
%ADD16R,0.0592X0.0671*%
%ADD17R,0.0710X0.1615*%
%ADD18R,0.1419X0.0828*%
%ADD19C,0.0634*%
%ADD20C,0.1360*%
%ADD21R,0.0474X0.0580*%
%ADD22C,0.0680*%
%ADD23R,0.0552X0.0552*%
%ADD24C,0.1340*%
%ADD25C,0.0476*%
D10*
X005000Y010604D03*
X005500Y010604D03*
X006000Y010604D03*
X006500Y010604D03*
X006500Y013024D03*
X006000Y013024D03*
X005500Y013024D03*
X005000Y013024D03*
D11*
X011423Y007128D03*
X011423Y006872D03*
X011423Y006616D03*
X011423Y006360D03*
X011423Y006104D03*
X011423Y005848D03*
X011423Y005592D03*
X011423Y005336D03*
X011423Y005080D03*
X011423Y004825D03*
X011423Y004569D03*
X011423Y004313D03*
X011423Y004057D03*
X011423Y003801D03*
X014277Y003801D03*
X014277Y004057D03*
X014277Y004313D03*
X014277Y004569D03*
X014277Y004825D03*
X014277Y005080D03*
X014277Y005336D03*
X014277Y005592D03*
X014277Y005848D03*
X014277Y006104D03*
X014277Y006360D03*
X014277Y006616D03*
X014277Y006872D03*
X014277Y007128D03*
D12*
X009350Y010114D03*
D13*
X012630Y010114D03*
X012630Y010784D03*
X012630Y011454D03*
X012630Y009444D03*
X012630Y008774D03*
D14*
X010000Y013467D03*
X010000Y016262D03*
D15*
X004150Y012988D03*
X004150Y012240D03*
X009900Y005688D03*
X009900Y004940D03*
X015000Y006240D03*
X015000Y006988D03*
D16*
X014676Y008364D03*
X015424Y008364D03*
X017526Y004514D03*
X018274Y004514D03*
X010674Y004064D03*
X009926Y004064D03*
X004174Y009564D03*
X003426Y009564D03*
X005376Y014564D03*
X006124Y014564D03*
D17*
X014250Y016088D03*
X014250Y012741D03*
D18*
X014250Y010982D03*
X014250Y009447D03*
D19*
X017200Y009464D03*
X018200Y009964D03*
X018200Y010964D03*
X017200Y010464D03*
X017200Y011464D03*
X018200Y011964D03*
D20*
X020700Y012714D03*
X020700Y008714D03*
D21*
X005004Y003814D03*
X005004Y004864D03*
X005004Y005864D03*
X005004Y006914D03*
X008696Y006914D03*
X008696Y005864D03*
X008696Y004864D03*
X008696Y003814D03*
D22*
X001800Y008564D02*
X001200Y008564D01*
X001200Y009564D02*
X001800Y009564D01*
X001800Y010564D02*
X001200Y010564D01*
X001200Y011564D02*
X001800Y011564D01*
X001800Y012564D02*
X001200Y012564D01*
X005350Y016664D02*
X005350Y017264D01*
X006350Y017264D02*
X006350Y016664D01*
X007350Y016664D02*
X007350Y017264D01*
X017350Y017114D02*
X017350Y016514D01*
X018350Y016514D02*
X018350Y017114D01*
D23*
X016613Y004514D03*
X015787Y004514D03*
D24*
X020800Y005064D03*
X020800Y016064D03*
X002300Y016064D03*
X002350Y005114D03*
D25*
X009250Y004064D03*
X012100Y005314D03*
X013500Y006864D03*
X015650Y006264D03*
X015200Y004514D03*
X013550Y008764D03*
X013350Y010114D03*
X013300Y011464D03*
X011650Y013164D03*
X010000Y015114D03*
X006500Y013714D03*
X004150Y011564D03*
X014250Y014964D03*
X015850Y009914D03*
M02*

File diff suppressed because it is too large Load diff

68
gerber/tests/test_cam.py Normal file
View file

@ -0,0 +1,68 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
from ..cam import CamFile, FileSettings
from tests import *
def test_filesettings_defaults():
""" Test FileSettings default values
"""
fs = FileSettings()
assert_equal(fs.format, (2, 5))
assert_equal(fs.notation, 'absolute')
assert_equal(fs.zero_suppression, 'trailing')
assert_equal(fs.units, 'inch')
def test_filesettings_dict():
""" Test FileSettings Dict
"""
fs = FileSettings()
assert_equal(fs['format'], (2, 5))
assert_equal(fs['notation'], 'absolute')
assert_equal(fs['zero_suppression'], 'trailing')
assert_equal(fs['units'], 'inch')
def test_filesettings_assign():
""" Test FileSettings attribute assignment
"""
fs = FileSettings()
fs.units = 'test1'
fs.notation = 'test2'
fs.zero_suppression = 'test3'
fs.format = 'test4'
assert_equal(fs.units, 'test1')
assert_equal(fs.notation, 'test2')
assert_equal(fs.zero_suppression, 'test3')
assert_equal(fs.format, 'test4')
def test_filesettings_dict_assign():
""" Test FileSettings dict-style attribute assignment
"""
fs = FileSettings()
fs['units'] = 'metric'
fs['notation'] = 'incremental'
fs['zero_suppression'] = 'leading'
fs['format'] = (1, 2)
assert_equal(fs.units, 'metric')
assert_equal(fs.notation, 'incremental')
assert_equal(fs.zero_suppression, 'leading')
assert_equal(fs.format, (1, 2))
def test_camfile_init():
""" Smoke test CamFile test
"""
cf = CamFile()
def test_camfile_settings():
""" Test CamFile Default Settings
"""
cf = CamFile()
assert_equal(cf.settings, FileSettings())

View file

@ -0,0 +1,24 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
from ..common import read
from ..excellon import ExcellonFile
from ..rs274x import GerberFile
from tests import *
import os
NCDRILL_FILE = os.path.join(os.path.dirname(__file__),
'resources/ncdrill.DRD')
TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__),
'resources/top_copper.GTL')
def test_file_type_detection():
""" Test file type detection
"""
ncdrill = read(NCDRILL_FILE)
top_copper = read(TOP_COPPER_FILE)
assert(isinstance(ncdrill, ExcellonFile))
assert(isinstance(top_copper, GerberFile))

View file

@ -0,0 +1,32 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
from ..excellon import read, detect_excellon_format, ExcellonFile
from tests import *
import os
NCDRILL_FILE = os.path.join(os.path.dirname(__file__),
'resources/ncdrill.DRD')
def test_format_detection():
""" Test file type detection
"""
settings = detect_excellon_format(NCDRILL_FILE)
assert_equal(settings['format'], (2, 4))
assert_equal(settings['zero_suppression'], 'leading')
def test_read():
ncdrill = read(NCDRILL_FILE)
assert(isinstance(ncdrill, ExcellonFile))
def test_read_settings():
ncdrill = read(NCDRILL_FILE)
assert_equal(ncdrill.settings.format, (2, 4))
assert_equal(ncdrill.settings.zero_suppression, 'leading')

View file

@ -0,0 +1,270 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
from .tests import assert_equal, assert_raises
from ..excellon_statements import *
from ..cam import FileSettings
def test_excellontool_factory():
""" Test ExcellonTool factory method
"""
exc_line = 'T8F00S00C0.12500'
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
units='inch', notation='absolute')
tool = ExcellonTool.from_excellon(exc_line, settings)
assert_equal(tool.diameter, 0.125)
assert_equal(tool.feed_rate, 0)
assert_equal(tool.rpm, 0)
def test_excellontool_dump():
""" Test ExcellonTool to_excellon()
"""
exc_lines = ['T1F00S00C0.01200', 'T2F00S00C0.01500', 'T3F00S00C0.01968',
'T4F00S00C0.02800', 'T5F00S00C0.03300', 'T6F00S00C0.03800',
'T7F00S00C0.04300', 'T8F00S00C0.12500', 'T9F00S00C0.13000', ]
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
units='inch', notation='absolute')
for line in exc_lines:
tool = ExcellonTool.from_excellon(line, settings)
assert_equal(tool.to_excellon(), line)
def test_excellontool_order():
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
units='inch', notation='absolute')
line = 'T8F00S00C0.12500'
tool1 = ExcellonTool.from_excellon(line, settings)
line = 'T8C0.12500F00S00'
tool2 = ExcellonTool.from_excellon(line, settings)
assert_equal(tool1.diameter, tool2.diameter)
assert_equal(tool1.feed_rate, tool2.feed_rate)
assert_equal(tool1.rpm, tool2.rpm)
def test_toolselection_factory():
""" Test ToolSelectionStmt factory method
"""
stmt = ToolSelectionStmt.from_excellon('T01')
assert_equal(stmt.tool, 1)
assert_equal(stmt.compensation_index, None)
stmt = ToolSelectionStmt.from_excellon('T0223')
assert_equal(stmt.tool, 2)
assert_equal(stmt.compensation_index, 23)
def test_toolselection_dump():
""" Test ToolSelectionStmt to_excellon()
"""
lines = ['T01', 'T0223', 'T10', 'T09', 'T0000']
for line in lines:
stmt = ToolSelectionStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
def test_coordinatestmt_factory():
""" Test CoordinateStmt factory method
"""
line = 'X0278207Y0065293'
stmt = CoordinateStmt.from_excellon(line)
assert_equal(stmt.x, 2.78207)
assert_equal(stmt.y, 0.65293)
line = 'X02945'
stmt = CoordinateStmt.from_excellon(line)
assert_equal(stmt.x, 2.945)
line = 'Y00575'
stmt = CoordinateStmt.from_excellon(line)
assert_equal(stmt.y, 0.575)
def test_coordinatestmt_dump():
""" Test CoordinateStmt to_excellon()
"""
lines = ['X0278207Y0065293', 'X0243795', 'Y0082528', 'Y0086028',
'X0251295Y0081528', 'X02525Y0078', 'X0255Y00575', 'Y0052',
'X02675', 'Y00575', 'X02425', 'Y0052', 'X023', ]
for line in lines:
stmt = CoordinateStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
def test_commentstmt_factory():
""" Test CommentStmt factory method
"""
line = ';Layer_Color=9474304'
stmt = CommentStmt.from_excellon(line)
assert_equal(stmt.comment, line[1:])
line = ';FILE_FORMAT=2:5'
stmt = CommentStmt.from_excellon(line)
assert_equal(stmt.comment, line[1:])
line = ';TYPE=PLATED'
stmt = CommentStmt.from_excellon(line)
assert_equal(stmt.comment, line[1:])
def test_commentstmt_dump():
""" Test CommentStmt to_excellon()
"""
lines = [';Layer_Color=9474304', ';FILE_FORMAT=2:5', ';TYPE=PLATED', ]
for line in lines:
stmt = CommentStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
def test_unitstmt_factory():
""" Test UnitStmt factory method
"""
line = 'INCH,LZ'
stmt = UnitStmt.from_excellon(line)
assert_equal(stmt.units, 'inch')
assert_equal(stmt.zero_suppression, 'trailing')
line = 'METRIC,TZ'
stmt = UnitStmt.from_excellon(line)
assert_equal(stmt.units, 'metric')
assert_equal(stmt.zero_suppression, 'leading')
def test_unitstmt_dump():
""" Test UnitStmt to_excellon()
"""
lines = ['INCH,LZ', 'INCH,TZ', 'METRIC,LZ', 'METRIC,TZ', ]
for line in lines:
stmt = UnitStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
def test_incrementalmode_factory():
""" Test IncrementalModeStmt factory method
"""
line = 'ICI,ON'
stmt = IncrementalModeStmt.from_excellon(line)
assert_equal(stmt.mode, 'on')
line = 'ICI,OFF'
stmt = IncrementalModeStmt.from_excellon(line)
assert_equal(stmt.mode, 'off')
def test_incrementalmode_dump():
""" Test IncrementalModeStmt to_excellon()
"""
lines = ['ICI,ON', 'ICI,OFF', ]
for line in lines:
stmt = IncrementalModeStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
def test_incrementalmode_validation():
""" Test IncrementalModeStmt input validation
"""
assert_raises(ValueError, IncrementalModeStmt, 'OFF-ISH')
def test_versionstmt_factory():
""" Test VersionStmt factory method
"""
line = 'VER,1'
stmt = VersionStmt.from_excellon(line)
assert_equal(stmt.version, 1)
line = 'VER,2'
stmt = VersionStmt.from_excellon(line)
assert_equal(stmt.version, 2)
def test_versionstmt_dump():
""" Test VersionStmt to_excellon()
"""
lines = ['VER,1', 'VER,2', ]
for line in lines:
stmt = VersionStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
def test_versionstmt_validation():
""" Test VersionStmt input validation
"""
assert_raises(ValueError, VersionStmt, 3)
def test_formatstmt_factory():
""" Test FormatStmt factory method
"""
line = 'FMAT,1'
stmt = FormatStmt.from_excellon(line)
assert_equal(stmt.format, 1)
line = 'FMAT,2'
stmt = FormatStmt.from_excellon(line)
assert_equal(stmt.format, 2)
def test_formatstmt_dump():
""" Test FormatStmt to_excellon()
"""
lines = ['FMAT,1', 'FMAT,2', ]
for line in lines:
stmt = FormatStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
def test_formatstmt_validation():
""" Test FormatStmt input validation
"""
assert_raises(ValueError, FormatStmt, 3)
def test_linktoolstmt_factory():
""" Test LinkToolStmt factory method
"""
line = '1/2/3/4'
stmt = LinkToolStmt.from_excellon(line)
assert_equal(stmt.linked_tools, [1, 2, 3, 4])
line = '01/02/03/04'
stmt = LinkToolStmt.from_excellon(line)
assert_equal(stmt.linked_tools, [1, 2, 3, 4])
def test_linktoolstmt_dump():
""" Test LinkToolStmt to_excellon()
"""
lines = ['1/2/3/4', '5/6/7', ]
for line in lines:
stmt = LinkToolStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
def test_measmodestmt_factory():
""" Test MeasuringModeStmt factory method
"""
line = 'M72'
stmt = MeasuringModeStmt.from_excellon(line)
assert_equal(stmt.units, 'inch')
line = 'M71'
stmt = MeasuringModeStmt.from_excellon(line)
assert_equal(stmt.units, 'metric')
def test_measmodestmt_dump():
""" Test MeasuringModeStmt to_excellon()
"""
lines = ['M71', 'M72', ]
for line in lines:
stmt = MeasuringModeStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
def test_measmodestmt_validation():
""" Test MeasuringModeStmt input validation
"""
assert_raises(ValueError, MeasuringModeStmt.from_excellon, 'M70')
assert_raises(ValueError, MeasuringModeStmt, 'millimeters')

View file

@ -0,0 +1,303 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
from .tests import *
from ..gerber_statements import *
def test_FSParamStmt_factory():
""" Test FSParamStruct factory
"""
stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'}
fs = FSParamStmt.from_dict(stmt)
assert_equal(fs.param, 'FS')
assert_equal(fs.zero_suppression, 'leading')
assert_equal(fs.notation, 'absolute')
assert_equal(fs.format, (2, 7))
stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '27'}
fs = FSParamStmt.from_dict(stmt)
assert_equal(fs.param, 'FS')
assert_equal(fs.zero_suppression, 'trailing')
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()
"""
stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'}
fs = FSParamStmt.from_dict(stmt)
assert_equal(fs.to_gerber(), '%FSLAX27Y27*%')
stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '25'}
fs = FSParamStmt.from_dict(stmt)
assert_equal(fs.to_gerber(), '%FSTIX25Y25*%')
def test_MOParamStmt_factory():
""" Test MOParamStruct factory
"""
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')
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():
""" Test MOParamStmt to_gerber()
"""
stmt = {'param': 'MO', 'mo': 'IN'}
mo = MOParamStmt.from_dict(stmt)
assert_equal(mo.to_gerber(), '%MOIN*%')
stmt = {'param': 'MO', 'mo': 'MM'}
mo = MOParamStmt.from_dict(stmt)
assert_equal(mo.to_gerber(), '%MOMM*%')
def test_IPParamStmt_factory():
""" Test IPParamStruct factory
"""
stmt = {'param': 'IP', 'ip': 'POS'}
ip = IPParamStmt.from_dict(stmt)
assert_equal(ip.ip, 'positive')
stmt = {'param': 'IP', 'ip': 'NEG'}
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()
"""
stmt = {'param': 'IP', 'ip': 'POS'}
ip = IPParamStmt.from_dict(stmt)
assert_equal(ip.to_gerber(), '%IPPOS*%')
stmt = {'param': 'IP', 'ip': 'NEG'}
ip = IPParamStmt.from_dict(stmt)
assert_equal(ip.to_gerber(), '%IPNEG*%')
def test_OFParamStmt_factory():
""" 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()
"""
stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'}
of = OFParamStmt.from_dict(stmt)
assert_equal(of.to_gerber(), '%OFA0.123456B0.123456*%')
def test_LPParamStmt_factory():
""" Test LPParamStmt factory
"""
stmt = {'param': 'LP', 'lp': 'C'}
lp = LPParamStmt.from_dict(stmt)
assert_equal(lp.lp, 'clear')
stmt = {'param': 'LP', 'lp': 'D'}
lp = LPParamStmt.from_dict(stmt)
assert_equal(lp.lp, 'dark')
def test_LPParamStmt_dump():
""" Test LPParamStmt to_gerber()
"""
stmt = {'param': 'LP', 'lp': 'C'}
lp = LPParamStmt.from_dict(stmt)
assert_equal(lp.to_gerber(), '%LPC*%')
stmt = {'param': 'LP', 'lp': 'D'}
lp = LPParamStmt.from_dict(stmt)
assert_equal(lp.to_gerber(), '%LPD*%')
def test_INParamStmt_factory():
""" Test INParamStmt factory
"""
stmt = {'param': 'IN', 'name': 'test'}
inp = INParamStmt.from_dict(stmt)
assert_equal(inp.name, 'test')
def test_INParamStmt_dump():
""" Test INParamStmt to_gerber()
"""
stmt = {'param': 'IN', 'name': 'test'}
inp = INParamStmt.from_dict(stmt)
assert_equal(inp.to_gerber(), '%INtest*%')
def test_LNParamStmt_factory():
""" Test LNParamStmt factory
"""
stmt = {'param': 'LN', 'name': 'test'}
lnp = LNParamStmt.from_dict(stmt)
assert_equal(lnp.name, 'test')
def test_LNParamStmt_dump():
""" Test LNParamStmt to_gerber()
"""
stmt = {'param': 'LN', 'name': 'test'}
lnp = LNParamStmt.from_dict(stmt)
assert_equal(lnp.to_gerber(), '%LNtest*%')
def test_comment_stmt():
""" Test comment statement
"""
stmt = CommentStmt('A comment')
assert_equal(stmt.type, 'COMMENT')
assert_equal(stmt.comment, 'A comment')
def test_comment_stmt_dump():
""" Test CommentStmt to_gerber()
"""
stmt = CommentStmt('A comment')
assert_equal(stmt.to_gerber(), 'G04A comment*')
def test_eofstmt():
""" Test EofStmt
"""
stmt = EofStmt()
assert_equal(stmt.type, 'EOF')
def test_eofstmt_dump():
""" Test EofStmt to_gerber()
"""
stmt = EofStmt()
assert_equal(stmt.to_gerber(), 'M02*')
def test_quadmodestmt_factory():
""" Test QuadrantModeStmt.from_gerber()
"""
line = 'G74*'
stmt = QuadrantModeStmt.from_gerber(line)
assert_equal(stmt.type, 'QuadrantMode')
assert_equal(stmt.mode, 'single-quadrant')
line = 'G75*'
stmt = QuadrantModeStmt.from_gerber(line)
assert_equal(stmt.mode, 'multi-quadrant')
def test_quadmodestmt_validation():
""" Test QuadrantModeStmt input validation
"""
line = 'G76*'
assert_raises(ValueError, QuadrantModeStmt.from_gerber, line)
assert_raises(ValueError, QuadrantModeStmt, 'quadrant-ful')
def test_quadmodestmt_dump():
""" Test QuadrantModeStmt.to_gerber()
"""
for line in ('G74*', 'G75*',):
stmt = QuadrantModeStmt.from_gerber(line)
assert_equal(stmt.to_gerber(), line)
def test_regionmodestmt_factory():
""" Test RegionModeStmt.from_gerber()
"""
line = 'G36*'
stmt = RegionModeStmt.from_gerber(line)
assert_equal(stmt.type, 'RegionMode')
assert_equal(stmt.mode, 'on')
line = 'G37*'
stmt = RegionModeStmt.from_gerber(line)
assert_equal(stmt.mode, 'off')
def test_regionmodestmt_validation():
""" Test RegionModeStmt input validation
"""
line = 'G38*'
assert_raises(ValueError, RegionModeStmt.from_gerber, line)
assert_raises(ValueError, RegionModeStmt, 'off-ish')
def test_regionmodestmt_dump():
""" Test RegionModeStmt.to_gerber()
"""
for line in ('G36*', 'G37*',):
stmt = RegionModeStmt.from_gerber(line)
assert_equal(stmt.to_gerber(), line)
def test_unknownstmt():
""" Test UnknownStmt
"""
line = 'G696969*'
stmt = UnknownStmt(line)
assert_equal(stmt.type, 'UNKNOWN')
assert_equal(stmt.line, line)
def test_unknownstmt_dump():
""" Test UnknownStmt.to_gerber()
"""
lines = ('G696969*', 'M03*',)
for line in lines:
stmt = UnknownStmt(line)
assert_equal(stmt.to_gerber(), line)

View file

@ -0,0 +1,16 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
from ..rs274x import read, GerberFile
from tests import *
import os
TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__),
'resources/top_copper.GTL')
def test_read():
top_copper = read(TOP_COPPER_FILE)
assert(isinstance(top_copper, GerberFile))

View file

@ -0,0 +1,83 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
from .tests import assert_equal
from ..utils import decimal_string, parse_gerber_value, write_gerber_value
def test_zero_suppression():
""" Test gerber value parser and writer handle zero suppression correctly.
"""
# Default format
fmt = (2, 5)
# Test leading zero suppression
zero_suppression = 'leading'
test_cases = [('1', 0.00001), ('10', 0.0001), ('100', 0.001),
('1000', 0.01), ('10000', 0.1), ('100000', 1.0),
('1000000', 10.0), ('-1', -0.00001), ('-10', -0.0001),
('-100', -0.001), ('-1000', -0.01), ('-10000', -0.1),
('-100000', -1.0), ('-1000000', -10.0), ]
for string, value in test_cases:
assert(value == parse_gerber_value(string, fmt, zero_suppression))
assert(string == write_gerber_value(value, fmt, zero_suppression))
# Test trailing zero suppression
zero_suppression = 'trailing'
test_cases = [('1', 10.0), ('01', 1.0), ('001', 0.1), ('0001', 0.01),
('00001', 0.001), ('000001', 0.0001),
('0000001', 0.00001), ('-1', -10.0), ('-01', -1.0),
('-001', -0.1), ('-0001', -0.01), ('-00001', -0.001),
('-000001', -0.0001), ('-0000001', -0.00001)]
for string, value in test_cases:
assert(value == parse_gerber_value(string, fmt, zero_suppression))
assert(string == write_gerber_value(value, fmt, zero_suppression))
def test_format():
""" Test gerber value parser and writer handle format correctly
"""
zero_suppression = 'leading'
test_cases = [((2, 7), '1', 0.0000001), ((2, 6), '1', 0.000001),
((2, 5), '1', 0.00001), ((2, 4), '1', 0.0001),
((2, 3), '1', 0.001), ((2, 2), '1', 0.01),
((2, 1), '1', 0.1), ((2, 7), '-1', -0.0000001),
((2, 6), '-1', -0.000001), ((2, 5), '-1', -0.00001),
((2, 4), '-1', -0.0001), ((2, 3), '-1', -0.001),
((2, 2), '-1', -0.01), ((2, 1), '-1', -0.1), ]
for fmt, string, value in test_cases:
assert(value == parse_gerber_value(string, fmt, zero_suppression))
assert(string == write_gerber_value(value, fmt, zero_suppression))
zero_suppression = 'trailing'
test_cases = [((6, 5), '1', 100000.0), ((5, 5), '1', 10000.0),
((4, 5), '1', 1000.0), ((3, 5), '1', 100.0),
((2, 5), '1', 10.0), ((1, 5), '1', 1.0),
((6, 5), '-1', -100000.0), ((5, 5), '-1', -10000.0),
((4, 5), '-1', -1000.0), ((3, 5), '-1', -100.0),
((2, 5), '-1', -10.0), ((1, 5), '-1', -1.0), ]
for fmt, string, value in test_cases:
assert(value == parse_gerber_value(string, fmt, zero_suppression))
assert(string == write_gerber_value(value, fmt, zero_suppression))
def test_decimal_truncation():
""" Test decimal_string truncates value to the correct precision
"""
value = 1.123456789
for x in range(10):
result = decimal_string(value, precision=x)
calculated = '1.' + ''.join(str(y) for y in range(1,x+1))
assert(result == calculated)
def test_decimal_padding():
""" Test decimal_string padding
"""
value = 1.123
assert_equal(decimal_string(value, precision=3, padding=True), '1.123')
assert_equal(decimal_string(value, precision=4, padding=True), '1.1230')
assert_equal(decimal_string(value, precision=5, padding=True), '1.12300')
assert_equal(decimal_string(value, precision=6, padding=True), '1.123000')

18
gerber/tests/tests.py Normal file
View file

@ -0,0 +1,18 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
from nose.tools import assert_in
from nose.tools import assert_not_in
from nose.tools import assert_equal
from nose.tools import assert_not_equal
from nose.tools import assert_true
from nose.tools import assert_false
from nose.tools import assert_raises
from nose.tools import raises
from nose import with_setup
__all__ = ['assert_in', 'assert_not_in', 'assert_equal', 'assert_not_equal',
'assert_true', 'assert_false', 'assert_raises', 'raises',
'with_setup' ]

View file

@ -23,27 +23,30 @@ This module provides utility functions for working with Gerber and Excellon
files.
"""
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
# 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
@ -53,12 +56,16 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
-------
value : float
The specified value as a floating-point number.
"""
# Handle excellon edge case with explicit decimal. "That was easy!"
if '.' in value:
return float(value)
# 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
@ -66,45 +73,41 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
raise ValueError('Parser only supports precision up to 6:7 format')
# Remove extraneous information
value = value.strip()
value = value.strip(' +')
#value = value.strip()
value = value.lstrip('+')
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]
value = value.lstrip('-')
digits = list('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
return -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
@ -118,61 +121,90 @@ 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')
# Edge case...
if value == 0:
return '00'
# 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
# Format string for padding out in both directions
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):
def decimal_string(value, precision=6, padding=False):
""" 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
floatstr = '%0.10g' % 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]
elif padding:
decimal = decimal + (precision - len(decimal)) * '0'
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'

12
requirements.txt Normal file
View file

@ -0,0 +1,12 @@
## The following requirements were added by pip --freeze:
Jinja2==2.7.3
MarkupSafe==0.23
Pygments==1.6
Sphinx==1.2.3
coverage==3.7.1
docutils==0.12
nose==1.3.4
numpydoc==0.5
pyparsing==2.0.2
svgwrite==1.1.6
wsgiref==0.1.2

View file

@ -1,23 +1,61 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2013-2014 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
# 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.
import os
from setuptools import setup
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
name = "gerber-tools",
version = "0.1",
author = "Paulo Henrique Silva",
author_email = "ph.silva@gmail.com",
description = ("Utilities to handle Gerber (RS-274X) files."),
license = "Apache",
keywords = "gerber tools",
url = "http://github.com/curtacircuitos/gerber-tools",
packages=['gerber'],
long_description=read('README.md'),
classifiers=[
METADATA = {
'name': 'gerber-tools',
'version': 0.1,
'author': 'Paulo Henrique Silva <ph.silva@gmail.com>, Hamilton Kibbe <ham@hamiltonkib.be>',
'author_email': "ph.silva@gmail.com, ham@hamiltonkib.be",
'description': ("Utilities to handle Gerber (RS-274X) files."),
'license': "Apache",
'keywords': "gerber tools",
'url': "http://github.com/curtacircuitos/gerber-tools",
'packages': ['gerber'],
'long_description': read('README.md'),
'classifiers':[
"Development Status :: 3 - Alpha",
"Topic :: Utilities",
"License :: OSI Approved :: Apple Public Source License",
],
)
}
SETUPTOOLS_METADATA = {
'install_requires': ['svgwrite'],
}
def install():
""" Install using setuptools, fallback to distutils
"""
try:
from setuptools import setup
METADATA.update(SETUPTOOLS_METADATA)
setup(**METADATA)
except ImportError:
from sys import stderr
stderr.write('Could not import setuptools, using distutils')
stderr.write('NOTE: You will need to install dependencies manualy')
from distutils.core import setup
setup(**METADATA)
if __name__ == '__main__':
install()