Graft pcb-tools upstream onto gerbonara tree

refactor
jaseg 2021-06-06 13:25:45 +02:00
commit 5a5ba2b709
161 zmienionych plików z 37263 dodań i 21 usunięć

8
.coveragerc 100644
Wyświetl plik

@ -0,0 +1,8 @@
[run]
branch = True
source = gerber
[report]
ignore_errors = True
omit =
gerber/tests/*

45
.github/workflows/pcb-tools.yml vendored 100644
Wyświetl plik

@ -0,0 +1,45 @@
name: pcb-tools
on: [push, pull_request]
jobs:
test:
strategy:
fail-fast: false
matrix:
python-version: [3.5, 3.6, 3.7, 3.8]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -r requirements-dev.txt
- name: Test with pytest
run: |
pytest
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: |
pip install -r requirements-dev.txt
- name: Run coverage
run: |
make test-coverage
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: unittest

Wyświetl plik

@ -178,7 +178,7 @@
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 pygerber
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

Wyświetl plik

@ -1,16 +1,27 @@
PROJECT = "desec_dns_api"
.PHONY: all clean sdist bdist_wheel test coverage html upload testupload
PYTHON ?= python
PYTEST ?= pytest
all: sdist bdist_wheel
.PHONY: clean
clean: doc-clean
find . -name '*.pyc' -delete
rm -rf *.egg-info
rm -f .coverage
rm -f coverage.xml
clean:
rm -rf .pytest_cache/
rm -rf build/
rm -rf ${PROJECT}.egg-info/
rm -rf dist/
rm -rf htmlcov/
rm -rf .coverage
.PHONY: test
test:
$(PYTEST)
.PHONY: test-coverage
test-coverage:
rm -f .coverage
rm -f coverage.xml
$(PYTEST) --cov=./ --cov-report=xml
.PHONY: install
install:
PYTHONPATH=. $(PYTHON) setup.py install
sdist:
python3 setup.py sdist
@ -18,17 +29,9 @@ sdist:
bdist_wheel:
python3 setup.py bdist_wheel
test:
pytest --flake8 tests/
coverage:
pytest --cov=${PROJECT} tests/
html:
pytest --cov-report html:htmlcov --cov=${PROJECT} tests/
upload: sdist bdist_wheel
twine upload -s -i contact@gerbonara.io --config-file ~/.pypirc --skip-existing --repository pypi dist/*
testupload: sdist bdist_wheel
twine upload --config-file ~/.pypirc --skip-existing --repository testpypi dist/*

Wyświetl plik

@ -7,6 +7,11 @@
Tools to handle Gerber and Excellon files in Python.
This repository is a friendly fork of [phsilva's pcb-tools](https://github.com/curtacircuitos/pcb-tools) with
[extensions from opiopan](https://github.com/opiopan/pcb-tools-extension) integrated. We decided to fork pcb-tools since
we need it as a dependency for [gerbolyze](https://gitlab.com/gerbolyze/gerbolyze) and pcb-tools was sometimes very
behind on bug fixes.
# Installation
Arch Linux:
@ -44,3 +49,5 @@ nc_drill.render(ctx, 'composite.svg')
---
Made with ❤️ and 🐍.
=======
pcb-tools

177
doc/Makefile 100644
Wyświetl plik

@ -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 100644
Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,40 @@
About PCB Tools
===============
PCB Tools provides a set of utilities for visualizing and working with PCB
design files in a variety of formats. The design files are generally referred
to as 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. These
file formats are used by the CNC equipment used to manufacutre PCBs.
PCB Tools currently supports the following file formats:
- Gerber (RS-274X)
- Excellon
with planned support for IPC-2581, ODB++ and more.
Image Rendering
~~~~~~~~~~~~~~~
.. image:: ../../examples/cairo_example.png
:alt: Rendering Example
The PCB Tools module provides tools to visualize PCBs and export images in a
variety of formats, including SVG and PNG.
Future Plans
~~~~~~~~~~~~
We are working on adding the following features to PCB Tools:
- Design Rules Checking
- Editing
- Panelization

262
doc/source/conf.py 100644
Wyświetl plik

@ -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'PCB Tools'
copyright = u'2014 Paulo Henrique Silva <ph.silva@gmail.com>, Hamilton Kibbe <ham@hamiltonkib.be>'
# 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 = False
# 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 = 'PCBToolsdoc'
# -- 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', 'PCBTools.tex', u'PCB 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', 'pcbtools', u'PCB 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', 'PCBTools', u'PCB Tools Documentation',
u'Hamilton Kibbe', 'PCBTools', 'Tools for working with PCB CAM files.',
'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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,24 @@
:mod:`operations` --- Cam File operations
=========================================
.. module:: operations
:synopsis: Functions for modifying CAM files
.. sectionauthor:: Hamilton Kibbe <ham@hamiltonkib.be>
The :mod:`operations` module provides functions which modify
:class:`gerber.cam.CamFile` objects. All of the functions in this module
return a modified copy of the supplied file.
.. _operations-contents:
Functions
---------
The :mod:`operations` module defines the following functions:
.. autofunction:: gerber.operations.to_inch
.. autofunction:: gerber.operations.to_metric
.. autofunction:: gerber.operations.offset

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,14 @@
Feature Suppport
================
Currently supported features are as follows:
============ ======== =========== ================ ====== ======= =======
File Format Parsing Rendering Unit Conversion Scale Offset Rotate
============ ======== =========== ================ ====== ======= =======
RS274-X Yes Yes Yes No Yes No
Excellon Yes Yes Yes No Yes No
ODB++ No No No No No No
============ ======== =========== ================ ====== ======= =======

Wyświetl plik

@ -0,0 +1,24 @@
.. PCB-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.
PCB-Tools
========================================
Contents:
.. toctree::
:maxdepth: 1
about
features
documentation/index
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 41 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 98 KiB

Wyświetl plik

@ -0,0 +1,78 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2015 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.
"""
This example demonstrates the use of pcb-tools with cairo to render a composite
image from a set of gerber files. Each layer is loaded and drawn using a
GerberCairoContext. The color and opacity of each layer can be set individually.
Once all thedesired layers are drawn on the context, the context is written to
a .png file.
"""
import os
from gerber import load_layer
from gerber.render import RenderSettings, theme
from gerber.render.cairo_backend import GerberCairoContext
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
# Open the gerber files
copper = load_layer(os.path.join(GERBER_FOLDER, 'copper.GTL'))
mask = load_layer(os.path.join(GERBER_FOLDER, 'soldermask.GTS'))
silk = load_layer(os.path.join(GERBER_FOLDER, 'silkscreen.GTO'))
drill = load_layer(os.path.join(GERBER_FOLDER, 'ncdrill.DRD'))
# Create a new drawing context
ctx = GerberCairoContext()
# Draw the copper layer. render_layer() uses the default color scheme for the
# layer, based on the layer type. Copper layers are rendered as
ctx.render_layer(copper)
# Draw the soldermask layer
ctx.render_layer(mask)
# The default style can be overridden by passing a RenderSettings instance to
# render_layer().
# First, create a settings object:
our_settings = RenderSettings(color=theme.COLORS['white'], alpha=0.85)
# Draw the silkscreen layer, and specify the rendering settings to use
ctx.render_layer(silk, settings=our_settings)
# Draw the drill layer
ctx.render_layer(drill)
# Write output to png file
ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_example.png'))
# Load the bottom layers
copper = load_layer(os.path.join(GERBER_FOLDER, 'bottom_copper.GBL'))
mask = load_layer(os.path.join(GERBER_FOLDER, 'bottom_mask.GBS'))
# Clear the drawing
ctx.clear()
# Render bottom layers
ctx.render_layer(copper)
ctx.render_layer(mask)
ctx.render_layer(drill)
# Write png file
ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_bottom.png'))

Wyświetl plik

@ -0,0 +1,90 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Example using pcb-tools with tsp-solver (github.com/dmishin/tsp-solver) to
# optimize tool paths in an Excellon file.
#
#
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
# Based on a script by https://github.com/koppi
#
# 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 sys
import math
import gerber
from operator import sub
from gerber.excellon import DrillHit
try:
from tsp_solver.greedy import solve_tsp
except ImportError:
print('\n=================================================================\n'
'This example requires tsp-solver be installed in order to run.\n\n'
'tsp-solver can be downloaded from:\n'
' http://github.com/dmishin/tsp-solver.\n'
'=================================================================')
sys.exit(0)
if __name__ == '__main__':
# Get file name to open
if len(sys.argv) < 2:
fname = 'gerbers/shld.drd'
else:
fname = sys.argv[1]
# Read the excellon file
f = gerber.read(fname)
positions = {}
tools = {}
hit_counts = f.hit_count()
oldpath = sum(f.path_length().values())
#Get hit positions
for hit in f.hits:
tool_num = hit.tool.number
if tool_num not in positions.keys():
positions[tool_num] = []
positions[tool_num].append(hit.position)
hits = []
# Optimize tool path for each tool
for tool, count in iter(hit_counts.items()):
# Calculate distance matrix
distance_matrix = [[math.hypot(*tuple(map(sub,
positions[tool][i],
positions[tool][j])))
for j in iter(range(count))]
for i in iter(range(count))]
# Calculate new path
path = solve_tsp(distance_matrix, 50)
# Create new hits list
hits += [DrillHit(f.tools[tool], positions[tool][p]) for p in path]
# Update the file
f.hits = hits
f.filename = f.filename + '.optimized'
f.write()
# Print drill report
print(f.report())
print('Original path length: %1.4f' % oldpath)
print('Optimized path length: %1.4f' % sum(f.path_length().values()))

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 33 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 88 KiB

Plik diff jest za duży Load Diff

Wyświetl plik

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

Plik diff jest za duży Load Diff

Wyświetl plik

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

Plik diff jest za duży Load Diff

Wyświetl plik

@ -0,0 +1,162 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239,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*

Wyświetl plik

@ -0,0 +1,7 @@
X-1000Y0D02*
G54D10*
X1000Y0D01*
X0Y-1000D02*
G54D10*
X0Y1000D01*

Wyświetl plik

@ -0,0 +1,38 @@
G04 Test drawing with circular apertures*
G04 Hand coded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10C,0.050*%
G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
G04 drawing with it (only should show in flashes)*
%ADD11C,0.075X0.050*%
G04 No hole, centered at 0,0 *
G54D10*
G04 Recenter to 0,0
G01X0Y0D02*
G04 Draw a line segment*
X00100Y0D01*
G04 Turn off for a segment*
X00200Y0D02*
G04 Draw another line at angle*
G54D11*
X00300Y00100D01*
G04 Turn off for a segment*
X0Y00100D02*
G54D10*
G04 Turn on circular interpolation*
G75*
G03X0Y00300I0J00100D01*
G04 Turn off for a segment*
X00500Y00D02*
G04 Draw a larger radius arc*
G03X00350Y00150I-00250J-00050D01*
G04 Turn off for a segment*
X00250Y00200D02*
G04 Draw a larger clockwise radius arc*
G02X00350Y00350I00250J-00050D01*
M02*

Wyświetl plik

@ -0,0 +1,27 @@
G04 Test flashing of circular apertures*
G04 Four groups of circular apertures are arranged in a square*
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10C,0.050*%
%ADD11C,0.050X0.025*%
%ADD12C,0.050X0.025X0.030*%
G04 No hole, centered at 0,0 *
G54D10*
X0Y0D03*
G04 Round hole, centered at 0.1,0 *
G54D11*
X00100Y0D03*
G04 Square hole, centered at 0,0.1 *
G54D12*
X0Y00100D03*
G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
G54D11*
X00100Y00090D03*
X00100Y00110D03*
M02*

Wyświetl plik

@ -0,0 +1,38 @@
G04 Test drawing with rectangular apertures*
G04 Hand coded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10O,0.050X0.025*%
G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
G04 drawing with it (only should show in flashes)*
%ADD11O,0.075X0.050X0.025*%
G04 No hole, centered at 0,0 *
G54D10*
G04 Recenter to 0,0
G01X0Y0D02*
G04 Draw a line segment*
X00100Y0D01*
G04 Turn off for a segment*
X00200Y0D02*
G04 Draw another line at angle*
G54D11*
X00300Y00100D01*
G04 Turn off for a segment*
X0Y00100D02*
G54D10*
G04 Turn on circular interpolation*
G75*
G03X0Y00300I0J00100D01*
G04 Turn off for a segment*
X00500Y00D02*
G04 Draw a larger radius arc*
G03X00350Y00150I-00250J-00050D01*
G04 Turn off for a segment*
X00250Y00200D02*
G04 Draw a larger clockwise radius arc*
G02X00350Y00350I00250J-00050D01*
M02*

Wyświetl plik

@ -0,0 +1,27 @@
G04 Test flashing of obround apertures*
G04 Four groups of obround apertures are arranged in a square*
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10O,0.050X0.080*%
%ADD11O,0.080X0.050X0.025*%
%ADD12O,0.050X0.025X0.025X0.0150*%
G04 No hole, centered at 0,0 *
G54D10*
X0Y0D03*
G04 Round hole, centered at 0.1,0 *
G54D11*
X00100Y0D03*
G04 Square hole, centered at 0,0.1 *
G54D12*
X0Y00100D03*
G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
G54D11*
X00100Y00090D03*
X00100Y00110D03*
M02*

Wyświetl plik

@ -0,0 +1,38 @@
G04 Test drawing with polygon apertures*
G04 Hand coded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10O,0.050X0.025*%
G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
G04 drawing with it (only should show in flashes)*
%ADD11O,0.075X0.050X0.025*%
G04 No hole, centered at 0,0 *
G54D10*
G04 Recenter to 0,0
G01X0Y0D02*
G04 Draw a line segment*
X00100Y0D01*
G04 Turn off for a segment*
X00200Y0D02*
G04 Draw another line at angle*
G54D11*
X00300Y00100D01*
G04 Turn off for a segment*
X0Y00100D02*
G54D10*
G04 Turn on circular interpolation*
G75*
G03X0Y00300I0J00100D01*
G04 Turn off for a segment*
X00500Y00D02*
G04 Draw a larger radius arc*
G03X00350Y00150I-00250J-00050D01*
G04 Turn off for a segment*
X00250Y00200D02*
G04 Draw a larger clockwise radius arc*
G02X00350Y00350I00250J-00050D01*
M02*

Wyświetl plik

@ -0,0 +1,27 @@
G04 Test flashing of polygon apertures*
G04 Four groups of polygon apertures are arranged in a square*
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10P,0.050X3*%
%ADD11P,0.050X6X-45X0.035*%
%ADD12P,0.040X10X25X0.025X0.025X0.0150*%
G04 Triangle, centered at 0,0 *
G54D10*
X0Y0D03*
G04 Hexagon with round hole rotate 45 degreed ccwise, centered at 0.1,0 *
G54D11*
X00100Y0D03*
G04 10-sided with square hole rotated 25 degrees, centered at 0,0.1 *
G54D12*
X0Y00100D03*
G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
G54D11*
X00100Y00090D03*
X00100Y00110D03*
M02*

Wyświetl plik

@ -0,0 +1,38 @@
G04 Test drawing with rectangular apertures*
G04 Hand coded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10R,0.050X0.025*%
G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
G04 drawing with it (only should show in flashes)*
%ADD11R,0.075X0.050X0.025*%
G04 No hole, centered at 0,0 *
G54D10*
G04 Recenter to 0,0
G01X0Y0D02*
G04 Draw a line segment*
X00100Y0D01*
G04 Turn off for a segment*
X00200Y0D02*
G04 Draw another line at angle*
G54D11*
X00300Y00100D01*
G04 Turn off for a segment*
X0Y00100D02*
G54D10*
G04 Turn on circular interpolation*
G75*
G03X0Y00300I0J00100D01*
G04 Turn off for a segment*
X00500Y00D02*
G04 Draw a larger radius arc*
G03X00350Y00150I-00250J-00050D01*
G04 Turn off for a segment*
X00250Y00200D02*
G04 Draw a larger clockwise radius arc*
G02X00350Y00350I00250J-00050D01*
M02*

Wyświetl plik

@ -0,0 +1,27 @@
G04 Test flashing of rectangular apertures*
G04 Four groups of rectangular apertures are arranged in a square*
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10R,0.050X0.080*%
%ADD11R,0.080X0.050X0.025*%
%ADD12R,0.050X0.025X0.025X0.0150*%
G04 No hole, centered at 0,0 *
G54D10*
X0Y0D03*
G04 Round hole, centered at 0.1,0 *
G54D11*
X00100Y0D03*
G04 Square hole, centered at 0,0.1 *
G54D12*
X0Y00100D03*
G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
G54D11*
X00100Y00090D03*
X00100Y00110D03*
M02*

Wyświetl plik

@ -0,0 +1,35 @@
G04 Test circular interpolation*
G04 Hand coded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10C,0.050*%
G54D10*
G04 Recenter to 0,0*
G01X0Y0D02*
G04 Turn on multi-quadrant mode*
G75*
G03X0Y00200I0J00100D01*
G04 Switch to quadrant mode, draw ccwise*
G74*
G01X00400Y0D02*
G03X00470Y00080I0J00100D01*
G04 Draw things clockwise on the top two objects*
G04 Turn on multi-quadrant mode*
G75*
G01X00100Y00300D02*
G02X00100Y00500I0J00100D01*
G04 Switch to quadrant mode, draw clockwise*
G04 Note: since this is single quadrant mode, I and J must be*
G04 positive, and the parser should automatically negate the J value*
G04 to make the curve travel in the clockwise direction*
G74*
G01X00400Y00300D02*
G02X00500Y00300I00150J00300D01*
M02*

Wyświetl plik

@ -0,0 +1,8 @@
M48
INCH,LZ
T13C0.05
%
T13
X-001000Y030000
X00000Y03000
X001Y03 M30

Wyświetl plik

@ -0,0 +1,19 @@
M48
INCH,TZ
T01C0.050
%
T01
X0000Y0000
X10000Y10000
R5X1000
X20000Y10000
R5Y1000
X30000Y10000
R5X1000Y1500
X10000Y00000
R5X-1000
X20000Y00000
R5Y-1000
X30000Y00000
R5X-1000Y-1500
M30

Wyświetl plik

@ -0,0 +1,8 @@
M48
INCH,TZ
T13C0.05
%
T13
X-001000Y030000
X0Y030000
X01000Y30000 M30

Wyświetl plik

@ -0,0 +1,19 @@
G04 Test image justify 1*
G04 Crosshairs should be justified to the X axis *
G04 and 0.5 inches offset from Y axis *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%IJB.5*%
%ADD10C,0.050*%
G04 Crosshairs *
X-1000Y0D02*
G54D10*
X1000Y0D01*
X0Y-1000D02*
G54D10*
X0Y1000D01*
M02*

Wyświetl plik

@ -0,0 +1,19 @@
G04 Test image justify 2*
G04 Crosshairs should be centered in X and Y (platen size *
G04 is assumed to be 2x the overall size of the image) *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%IJACBC*%
%ADD10C,0.050*%
G04 Crosshairs *
X-1000Y0D02*
G54D10*
X1000Y0D01*
X0Y-1000D02*
G54D10*
X0Y1000D01*
M02*

Wyświetl plik

@ -0,0 +1,18 @@
G04 Test image polarity *
G04 Crosshairs should be centered on 0,0 in final rendering*
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%IOA-2.0B-1.0*%
%ADD10C,0.050*%
G04 Crosshairs to be on 0,0 *
X1000Y1000D02*
G54D10*
X3000Y1000D01*
X2000Y0D02*
G54D10*
X2000Y2000D01*
M02*

Wyświetl plik

@ -0,0 +1,19 @@
G04 Test image offset uses current units *
G04 Crosshairs should be centered on 0,0 in final rendering*
G04 Handcoded by Julian Lamb *
%MOMM*%
%FSLAX23Y23*%
%IOB-25.4*%
%MOIN*%
%ADD10C,0.050*%
G04 Crosshairs to be on 0,0 *
X-1000Y1000D02*
G54D10*
X1000Y1000D01*
X0Y0D02*
G54D10*
X0Y2000D01*
M02*

Wyświetl plik

@ -0,0 +1,17 @@
G04 Test image polarity *
G04 Crosshairs should be cut out of a positive background*
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%IPNEG*%
%ADD10C,0.050*%
G04 Draw crosshairs *
X-1000Y0D02*
G54D10*
X1000Y0D01*
X0Y-1000D02*
G54D10*
X0Y1000D01*
M02*

Wyświetl plik

@ -0,0 +1,21 @@
G04 Test image rotation *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%IR270*%
%ADD10C,0.050*%
G04 Quarter star *
X1000Y0D02*
G54D10*
X2000Y0D01*
X1000Y0D02*
G54D10*
X2000Y1000D01*
X1000Y0D02*
G54D10*
X1000Y1000D01*
M02*

Wyświetl plik

@ -0,0 +1,11 @@
G04 Test include file 1 *
G04 Crosshairs should be drawn at 0,0 in final rendering*
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%IOA-2.0B-1.0*%
%ADD10C,0.050*%
G04 Crosshairs to be on 0,0 *
%IFinclude-file-1.gbx*%
M02*

Wyświetl plik

@ -0,0 +1,15 @@
G04 Test layer axis select *
G04 Line is drawn along A axis, then axis select switches it and renders *
G04 line along y axis *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ASAYBX*%
%ADD10C,0.050*%
G04 Draw line *
X-1000Y0D02*
G54D10*
X1000Y0D01*
M02*

Wyświetl plik

@ -0,0 +1,28 @@
G04 Test layer knockout 1*
G04 A cleared 3x3 square should surround the crosshairs *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10C,0.050*%
G04 Create a large dark area *
G36*
X-2000Y-2000D02*
X2000Y-2000D01*
X2000Y2000D01*
X-2000Y2000D01*
X-2000Y-2000D01*
G37*
G04 Create the knockout region *
%KOCX-1.5Y-1.5I3J3*%
G04 Draw crosshairs *
X-1000Y0D02*
G54D10*
X1000Y0D01*
X0Y-1000D02*
G54D10*
X0Y1000D01*
M02*

Wyświetl plik

@ -0,0 +1,28 @@
G04 Test layer knockout 2*
G04 A cleared 0.5 inch border should surround the crosshairs *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10C,0.050*%
G04 Create a large dark area *
G36*
X-2000Y-2000D02*
X2000Y-2000D01*
X2000Y2000D01*
X-2000Y2000D01*
X-2000Y-2000D01*
G37*
G04 Create the knockout region *
%KOCK0.5*%
G04 Draw crosshairs *
X-1000Y0D02*
G54D10*
X1000Y0D01*
X0Y-1000D02*
G54D10*
X0Y1000D01*
M02*

Wyświetl plik

@ -0,0 +1,23 @@
G04 Test layer mirror image 1 *
G04 Quarter star is drawn pointing towards +X, +Y. Mirror
G04 flips around the Y axis and the star should point towards -X, -Y *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%MIA1B1*%
%ADD10C,0.050*%
G04 Draw quarter star *
X0Y0D02*
G54D10*
X1000Y0D01*
X0Y0D02*
G54D10*
X1000Y1000D01*
X0Y0D02*
G54D10*
X0Y1000D01*
M02*

Wyświetl plik

@ -0,0 +1,68 @@
G04 Test handling of unit changes within a RS274X file *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
G04 Aperture 10 should be in Inches *
%ADD10C,0.050*%
%MOMM*%
G04 Aperture 11 should be in MMs *
%ADD11C,1.250*%
G04 Aperture 12 should be in MMs *
%AMTHERMAL*
7,0,0,25.4,12.7,2.54,0*%
%MOIN*%
G04 Aperture 13 is in inches *
%AMTHERMALTWO*
7,0,0,1,0.5,0.1,0*%
%MOMM*%
%ADD12THERMAL*%
%MOIN*%
%ADD13THERMALTWO*%
%MOIN*%
G04 Box 1, using aperture 10*
X0Y0D02*
G54D10*
X0Y0D01*
X1000D01*
Y1000D01*
X0D01*
Y0D01*
G04 Box 2, using aperture 11*
X2000Y0D02*
G54D11*
X2000Y0D01*
X3000D01*
Y1000D01*
X2000D01*
Y0D01*
%MOMM*%
G04 Box 3, using aperture 10*
X100000Y0D02*
G54D10*
X100000Y0D01*
X125000D01*
Y25000D01*
X100000D01*
Y0D01*
G04 Draw Thermal in box 1*
G54D12*
Y12000X12700D03*
G04 Draw Thermal in box 2*
G04 ..switch to inches for coordinates*
G70*
Y500X2500D02*
G54D12*
Y500X2500D03*
G04 ..switch to mms for coordinates*
G71*
G04 Draw Thermal in box 3*
G54D13*
Y12000X112000D03*
M02*

Wyświetl plik

@ -0,0 +1,18 @@
G04 Test layer offset 1 *
G04 Crosshairs should be centered on 0,0*
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%OFA-2.0B-1.0*%
%ADD10C,0.050*%
G04 Crosshairs to be on 0,0 *
X1000Y1000D02*
G54D10*
X3000Y1000D01*
X2000Y0D02*
G54D10*
X2000Y2000D01*
M02*

Wyświetl plik

@ -0,0 +1,23 @@
G04 Test layer rotation 1 *
G04 Quarter star should be rotated 45 degrees counterclockwise, pointing*
G04 the center line straight up *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%RO45*%
%ADD10C,0.025*%
G04 Quarter star *
X1000Y0D02*
G54D10*
X2000Y0D01*
X1000Y0D02*
G54D10*
X2000Y1000D01*
X1000Y0D02*
G54D10*
X1000Y1000D01*
M02*

Wyświetl plik

@ -0,0 +1,17 @@
G04 Test layer scale factor 1 *
G04 Crosshairs should be centered on 0,0 and 2 inches wide and 1 inch tall*
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%SFA2B1*%
%ADD10C,0.025*%
G04 Crosshairs to be on 0,0 *
X-500Y0D02*
G54D10*
X500Y0D01*
X0Y-500D02*
G54D10*
X0Y500D01*
M02*

Wyświetl plik

@ -0,0 +1,17 @@
G04 Test step and repeat 1*
G04 Repeat a crosshair 3 times in the x direction and 2 times in the Y *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%SRX3Y2I5.0J2*%
%ADD10C,0.050*%
G04 Draw crosshairs *
X-1000Y0D02*
G54D10*
X1000Y0D01*
X0Y-1000D02*
G54D10*
X0Y1000D01*
M02*

Wyświetl plik

@ -0,0 +1,18 @@
G04 Test step and repeat 1*
G04 Repeat a crosshair 3 times in the x direction and 2 times in the Y *
G04 Handcoded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%SRX3Y2I1J1*%
%ADD10C,0.050*%
G04 Draw a simple square*
G36*
G01X00400Y0D02*
X00600Y0D01*
X00600Y00200D01*
X00400Y00200D01*
X00400Y0D01*
G37*
M02*

Wyświetl plik

@ -0,0 +1,45 @@
G04 Test drawing with polygon apertures*
G04 Four small polygon fills aranged in a square
G04 Hand coded by Julian Lamb *
%MOIN*%
%FSLAX23Y23*%
%ADD10C,0.050*%
G04 Draw a rectangle with a rounded right side*
G36*
G01X0Y0D02*
X00200Y0D01*
G75*
G03X00200Y00200I0J00100D01*
X0Y00200D01*
G04 Do not close with a final line, so let gerbv automatically close*
G37*
G04 Draw a simple square*
G36*
G01X00400Y0D02*
X00600Y0D01*
X00600Y00200D01*
X00400Y00200D01*
X00400Y0D01*
G37*
G04 Draw a small diamond*
G36*
G01X00100Y00300D02*
X00200Y00400D01*
X00100Y00500D01*
X0Y00400D01*
X00100Y00300D01*
G37*
G04 Draw a very-narrow slit*
G36*
G01X00500Y00300D02*
X00510Y00300D01*
X00510Y00500D01*
X00500Y00500D01*
X00500Y00300D01*
G37*
M02*

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 49 KiB

Wyświetl plik

@ -0,0 +1,53 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2016 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.
"""
This example demonstrates the use of pcb-tools with cairo to render composite
images using the PCB interface
"""
import os
from gerber import PCB
from gerber.render import theme
from gerber.render.cairo_backend import GerberCairoContext
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
# Create a new drawing context
ctx = GerberCairoContext()
# Create a new PCB instance
pcb = PCB.from_directory(GERBER_FOLDER)
# Render PCB top view
ctx.render_layers(pcb.top_layers,
os.path.join(os.path.dirname(__file__), 'pcb_top.png',),
theme.THEMES['OSH Park'], max_width=800, max_height=600)
# Render PCB bottom view
ctx.render_layers(pcb.bottom_layers,
os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'),
theme.THEMES['OSH Park'], max_width=800, max_height=600)
# Render copper layers only
ctx.render_layers(pcb.copper_layers + pcb.drill_layers,
os.path.join(os.path.dirname(__file__),
'pcb_transparent_copper.png'),
theme.THEMES['Transparent Copper'], max_width=800, max_height=600)

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 102 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 108 KiB

Wyświetl plik

@ -0,0 +1,59 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2015 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.
"""
This example renders the gerber files from the gerbv test suite
"""
import os
from gerber.rs274x import read as gerber_read
from gerber.excellon import read as excellon_read
from gerber.render import GerberCairoContext
from gerber.utils import listdir
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbv_test_files'))
if not os.path.isdir(os.path.join(os.path.dirname(__file__), 'outputs')):
os.mkdir(os.path.join(os.path.dirname(__file__), 'outputs'))
for infile in listdir(GERBER_FOLDER):
if infile.startswith('test'):
try:
outfile = os.path.splitext(infile)[0] + '.png'
if infile.endswith('gbx'):
layer = gerber_read(os.path.join(GERBER_FOLDER, infile))
print("Loaded Gerber file: {}".format(infile))
elif infile.endswith('exc'):
layer = excellon_read(os.path.join(GERBER_FOLDER, infile))
print("Loaded Excellon file: {}".format(infile))
else:
continue
# Create a new drawing context
ctx = GerberCairoContext(1200)
ctx.color = (80./255, 80/255., 154/255.)
ctx.drill_color = ctx.color
# Draw the layer, and specify the rendering settings to use
layer.render(ctx)
# Write output to png file
print("Writing output to: {}".format(outfile))
ctx.dump(os.path.join(os.path.dirname(__file__), 'outputs', outfile))
except Exception as exc:
import traceback
traceback.print_exc()

28
gerber/__init__.py 100644
Wyświetl plik

@ -0,0 +1,28 @@
#! /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.
"""
Gerber Tools
============
**Gerber Tools**
gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon
files in python.
"""
from .common import read, loads
from .layers import load_layer, load_layer_data
from .pcb import PCB

122
gerber/__main__.py 100644
Wyświetl plik

@ -0,0 +1,122 @@
#! /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
import argparse
from .render import available_renderers
from .render import theme
from .pcb import PCB
from . import load_layer
def main():
parser = argparse.ArgumentParser(
description='Render gerber files to image',
prog='gerber-render'
)
parser.add_argument(
'filenames', metavar='FILENAME', type=str, nargs='+',
help='Gerber files to render. If a directory is provided, it should '
'be provided alone and should contain the gerber files for a '
'single PCB.'
)
parser.add_argument(
'--outfile', '-o', type=str, nargs='?', default='out',
help="Output Filename (extension will be added automatically)"
)
parser.add_argument(
'--backend', '-b', choices=available_renderers.keys(), default='cairo',
help='Choose the backend to use to generate the output.'
)
parser.add_argument(
'--theme', '-t', choices=theme.THEMES.keys(), default='default',
help='Select render theme.'
)
parser.add_argument(
'--width', type=int, default=1920, help='Maximum width.'
)
parser.add_argument(
'--height', type=int, default=1080, help='Maximum height.'
)
parser.add_argument(
'--verbose', '-v', action='store_true', default=False,
help='Increase verbosity of the output.'
)
# parser.add_argument(
# '--quick', '-q', action='store_true', default=False,
# help='Skip longer running rendering steps to produce lower quality'
# ' output faster. This only has an effect for the freecad backend.'
# )
# parser.add_argument(
# '--nox', action='store_true', default=False,
# help='Run without using any GUI elements. This may produce suboptimal'
# 'output. For the freecad backend, colors, transparancy, and '
# 'visibility cannot be set without a GUI instance.'
# )
args = parser.parse_args()
renderer = available_renderers[args.backend]()
if args.backend in ['cairo', ]:
outext = 'png'
else:
outext = None
if os.path.exists(args.filenames[0]) and os.path.isdir(args.filenames[0]):
directory = args.filenames[0]
pcb = PCB.from_directory(directory)
if args.backend in ['cairo', ]:
top = pcb.top_layers
bottom = pcb.bottom_layers
copper = pcb.copper_layers
outline = pcb.outline_layer
if outline:
top = [outline] + top
bottom = [outline] + bottom
copper = [outline] + copper + pcb.drill_layers
renderer.render_layers(
layers=top, theme=theme.THEMES[args.theme],
max_height=args.height, max_width=args.width,
filename='{0}.top.{1}'.format(args.outfile, outext)
)
renderer.render_layers(
layers=bottom, theme=theme.THEMES[args.theme],
max_height=args.height, max_width=args.width,
filename='{0}.bottom.{1}'.format(args.outfile, outext)
)
renderer.render_layers(
layers=copper, theme=theme.THEMES['Transparent Multilayer'],
max_height=args.height, max_width=args.width,
filename='{0}.copper.{1}'.format(args.outfile, outext))
else:
pass
else:
filenames = args.filenames
for filename in filenames:
layer = load_layer(filename)
settings = theme.THEMES[args.theme].get(layer.layer_class, None)
renderer.render_layer(layer, settings=settings)
renderer.dump(filename='{0}.{1}'.format(args.outfile, outext))
if __name__ == '__main__':
main()

109
gerber/am_eval.py 100644
Wyświetl plik

@ -0,0 +1,109 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# copyright 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.
""" This module provides RS-274-X AM macro evaluation.
"""
class OpCode:
PUSH = 1
LOAD = 2
STORE = 3
ADD = 4
SUB = 5
MUL = 6
DIV = 7
PRIM = 8
@staticmethod
def str(opcode):
if opcode == OpCode.PUSH:
return "OPCODE_PUSH"
elif opcode == OpCode.LOAD:
return "OPCODE_LOAD"
elif opcode == OpCode.STORE:
return "OPCODE_STORE"
elif opcode == OpCode.ADD:
return "OPCODE_ADD"
elif opcode == OpCode.SUB:
return "OPCODE_SUB"
elif opcode == OpCode.MUL:
return "OPCODE_MUL"
elif opcode == OpCode.DIV:
return "OPCODE_DIV"
elif opcode == OpCode.PRIM:
return "OPCODE_PRIM"
else:
return "UNKNOWN"
def eval_macro(instructions, parameters={}):
if not isinstance(parameters, type({})):
p = {}
for i, val in enumerate(parameters):
p[i + 1] = val
parameters = p
stack = []
def pop():
return stack.pop()
def push(op):
stack.append(op)
def top():
return stack[-1]
def empty():
return len(stack) == 0
for opcode, argument in instructions:
if opcode == OpCode.PUSH:
push(argument)
elif opcode == OpCode.LOAD:
push(parameters.get(argument, 0))
elif opcode == OpCode.STORE:
parameters[argument] = pop()
elif opcode == OpCode.ADD:
op1 = pop()
op2 = pop()
push(op2 + op1)
elif opcode == OpCode.SUB:
op1 = pop()
op2 = pop()
push(op2 - op2)
elif opcode == OpCode.MUL:
op1 = pop()
op2 = pop()
push(op2 * op1)
elif opcode == OpCode.DIV:
op1 = pop()
op2 = pop()
push(op2 / op1)
elif opcode == OpCode.PRIM:
yield "%d,%s" % (argument, ",".join([str(x) for x in stack]))
stack = []

255
gerber/am_read.py 100644
Wyświetl plik

@ -0,0 +1,255 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# copyright 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.
""" This module provides RS-274-X AM macro modifiers parsing.
"""
from .am_eval import OpCode, eval_macro
import string
class Token:
ADD = "+"
SUB = "-"
# compatibility as many gerber writes do use non compliant X
MULT = ("x", "X")
DIV = "/"
OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV)
LEFT_PARENS = "("
RIGHT_PARENS = ")"
EQUALS = "="
EOF = "EOF"
def token_to_opcode(token):
if token == Token.ADD:
return OpCode.ADD
elif token == Token.SUB:
return OpCode.SUB
elif token in Token.MULT:
return OpCode.MUL
elif token == Token.DIV:
return OpCode.DIV
else:
return None
def precedence(token):
if token == Token.ADD or token == Token.SUB:
return 1
elif token in Token.MULT or token == Token.DIV:
return 2
else:
return 0
def is_op(token):
return token in Token.OPERATORS
class Scanner:
def __init__(self, s):
self.buff = s
self.n = 0
def eof(self):
return self.n == len(self.buff)
def peek(self):
if not self.eof():
return self.buff[self.n]
return Token.EOF
def ungetc(self):
if self.n > 0:
self.n -= 1
def getc(self):
if self.eof():
return ""
c = self.buff[self.n]
self.n += 1
return c
def readint(self):
n = ""
while not self.eof() and (self.peek() in string.digits):
n += self.getc()
return int(n)
def readfloat(self):
n = ""
while not self.eof() and (self.peek() in string.digits or self.peek() == "."):
n += self.getc()
# weird case where zero is ommited inthe last modifider, like in ',0.'
if n == ".":
return 0
return float(n)
def readstr(self, end="*"):
s = ""
while not self.eof() and self.peek() != end:
s += self.getc()
return s.strip()
def print_instructions(instructions):
for opcode, argument in instructions:
print("%s %s" % (OpCode.str(opcode),
str(argument) if argument is not None else ""))
def read_macro(macro):
instructions = []
for block in macro.split("*"):
is_primitive = False
is_equation = False
found_equation_left_side = False
found_primitive_code = False
equation_left_side = 0
primitive_code = 0
unary_minus_allowed = False
unary_minus = False
if Token.EQUALS in block:
is_equation = True
else:
is_primitive = True
scanner = Scanner(block)
# inlined here for compactness and convenience
op_stack = []
def pop():
return op_stack.pop()
def push(op):
op_stack.append(op)
def top():
return op_stack[-1]
def empty():
return len(op_stack) == 0
while not scanner.eof():
c = scanner.getc()
if c == ",":
found_primitive_code = True
# add all instructions on the stack to finish last modifier
while not empty():
instructions.append((token_to_opcode(pop()), None))
unary_minus_allowed = True
elif c in Token.OPERATORS:
if c == Token.SUB and unary_minus_allowed:
unary_minus = True
unary_minus_allowed = False
continue
while not empty() and is_op(top()) and precedence(top()) >= precedence(c):
instructions.append((token_to_opcode(pop()), None))
push(c)
elif c == Token.LEFT_PARENS:
push(c)
elif c == Token.RIGHT_PARENS:
while not empty() and top() != Token.LEFT_PARENS:
instructions.append((token_to_opcode(pop()), None))
if empty():
raise ValueError("unbalanced parentheses")
# discard "("
pop()
elif c.startswith("$"):
n = scanner.readint()
if is_equation and not found_equation_left_side:
equation_left_side = n
else:
instructions.append((OpCode.LOAD, n))
elif c == Token.EQUALS:
found_equation_left_side = True
elif c == "0":
if is_primitive and not found_primitive_code:
instructions.append((OpCode.PUSH, scanner.readstr("*")))
found_primitive_code = True
else:
# decimal or integer disambiguation
if scanner.peek() not in '.' or scanner.peek() == Token.EOF:
instructions.append((OpCode.PUSH, 0))
elif c in "123456789.":
scanner.ungetc()
if is_primitive and not found_primitive_code:
primitive_code = scanner.readint()
else:
n = scanner.readfloat()
if unary_minus:
unary_minus = False
n *= -1
instructions.append((OpCode.PUSH, n))
else:
# whitespace or unknown char
pass
# add all instructions on the stack to finish last modifier (if any)
while not empty():
instructions.append((token_to_opcode(pop()), None))
# at end, we either have a primitive or a equation
if is_primitive and found_primitive_code:
instructions.append((OpCode.PRIM, primitive_code))
if is_equation:
instructions.append((OpCode.STORE, equation_left_side))
return instructions
if __name__ == '__main__':
import sys
instructions = read_macro(sys.argv[1])
print("insructions:")
print_instructions(instructions)
print("eval:")
for primitive in eval_macro(instructions):
print(primitive)

Plik diff jest za duży Load Diff

286
gerber/cam.py 100644
Wyświetl plik

@ -0,0 +1,286 @@
#! /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
Parameters
----------
notation: string
notation format. either 'absolute' or 'incremental'
units : string
Measurement units. 'inch' or 'metric'
zero_suppression: string
'leading' to suppress leading zeros, 'trailing' to suppress trailing zeros.
This is the convention used in Gerber files.
format : tuple (int, int)
Decimal format
zeros : string
'leading' to include leading zeros, 'trailing to include trailing zeros.
This is the convention used in Excellon files
Notes
-----
Either `zeros` or `zero_suppression` should be specified, there is no need to
specify both. `zero_suppression` will take on the opposite value of `zeros`
and vice versa
"""
def __init__(self, notation='absolute', units='inch',
zero_suppression=None, format=(2, 5), zeros=None,
angle_units='degrees'):
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 is None and zeros is None:
self.zero_suppression = 'trailing'
elif zero_suppression == zeros:
raise ValueError('Zeros and Zero Suppression must be different. \
Best practice is to specify only one.')
elif zero_suppression is not None:
if zero_suppression not in ['leading', 'trailing']:
# This is a common problem in Eagle files, so just suppress it
self.zero_suppression = 'leading'
else:
self.zero_suppression = zero_suppression
elif zeros is not None:
if zeros not in ['leading', 'trailing']:
raise ValueError('Zeros must be either leading or trailling')
self.zeros = zeros
if len(format) != 2:
raise ValueError('Format must be a tuple(n=2) of integers')
self.format = format
if angle_units not in ('degrees', 'radians'):
raise ValueError('Angle units may be degrees or radians')
self.angle_units = angle_units
@property
def zero_suppression(self):
return self._zero_suppression
@zero_suppression.setter
def zero_suppression(self, value):
self._zero_suppression = value
self._zeros = 'leading' if value == 'trailing' else 'trailing'
@property
def zeros(self):
return self._zeros
@zeros.setter
def zeros(self, value):
self._zeros = value
self._zero_suppression = 'leading' if value == 'trailing' else 'trailing'
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 == 'zeros':
return self.zeros
elif key == 'format':
return self.format
elif key == 'angle_units':
return self.angle_units
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 == 'zeros':
if value not in ['leading', 'trailing']:
raise ValueError('Zeros must be either leading or trailling')
self.zeros = value
elif key == 'format':
if len(value) != 2:
raise ValueError('Format must be a tuple(n=2) of integers')
self.format = value
elif key == 'angle_units':
if value not in ('degrees', 'radians'):
raise ValueError('Angle units may be degrees or radians')
self.angle_units = value
else:
raise KeyError('%s is not a valid key' % key)
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 and
self.angle_units == other.angle_units)
def __str__(self):
return ('<Settings: %s %s %s %s %s>' %
(self.units, self.notation, self.zero_suppression, self.format, self.angle_units))
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.zeros = settings['zeros']
self.format = settings['format']
else:
self.notation = 'absolute'
self.units = 'inch'
self.zero_suppression = 'trailing'
self.zeros = 'leading'
self.format = (2, 5)
self.statements = statements if statements is not None else []
if primitives is not None:
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 boundaries
"""
pass
@property
def bounding_box(self):
pass
def to_inch(self):
pass
def to_metric(self):
pass
def render(self, ctx=None, invert=False, 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`
"""
if ctx is None:
from .render import GerberCairoContext
ctx = GerberCairoContext()
ctx.set_bounds(self.bounding_box)
ctx.paint_background()
ctx.invert = invert
ctx.new_render_layer()
for p in self.primitives:
ctx.render(p)
ctx.flatten()
if filename is not None:
ctx.dump(filename)

71
gerber/common.py 100644
Wyświetl plik

@ -0,0 +1,71 @@
#! /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.
from . import rs274x
from . import excellon
from . import ipc356
from .exceptions import ParseError
from .utils import detect_file_format
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, ExcellonFile,
or IPCNetlist. Returns None if file is not of the proper type.
"""
with open(filename, 'rU') as f:
data = f.read()
return loads(data, filename)
def loads(data, filename=None):
""" Read gerber or excellon file contents from a string and return a
representative object.
Parameters
----------
data : string
Source file contents as a string.
filename : string, optional
String containing the filename of the data source.
Returns
-------
file : CncFile subclass
CncFile object representing the data, either GerberFile, ExcellonFile,
or IPCNetlist. Returns None if data is not of the proper type.
"""
fmt = detect_file_format(data)
if fmt == 'rs274x':
return rs274x.loads(data, filename=filename)
elif fmt == 'excellon':
return excellon.loads(data, filename=filename)
elif fmt == 'ipc_d_356':
return ipc356.loads(data, filename=filename)
else:
raise ParseError('Unable to detect file format')

904
gerber/excellon.py 100755
Wyświetl plik

@ -0,0 +1,904 @@
#!/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 File module
====================
**Excellon file classes**
This module provides Excellon file classes and parsing utilities
"""
import math
import operator
from .cam import CamFile, FileSettings
from .excellon_statements import *
from .excellon_tool import ExcellonToolDefinitionParser
from .primitives import Drill, Slot
from .utils import inch, metric
try:
from cStringIO import StringIO
except(ImportError):
from io import StringIO
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.
"""
# File object should use settings from source file by default.
with open(filename, 'rU') as f:
data = f.read()
settings = FileSettings(**detect_excellon_format(data))
return ExcellonParser(settings).parse(filename)
def loads(data, filename=None, settings=None, tools=None):
""" Read data from string and return an ExcellonFile
Parameters
----------
data : string
string containing Excellon file contents
filename : string, optional
string containing the filename of the data source
tools: dict (optional)
externally defined tools
Returns
-------
file : :class:`gerber.excellon.ExcellonFile`
An ExcellonFile created from the specified file.
"""
# File object should use settings from source file by default.
if not settings:
settings = FileSettings(**detect_excellon_format(data))
return ExcellonParser(settings, tools).parse_raw(data, filename)
class DrillHit(object):
"""Drill feature that is a single drill hole.
Attributes
----------
tool : ExcellonTool
Tool to drill the hole. Defines the size of the hole that is generated.
position : tuple(float, float)
Center position of the drill.
"""
def __init__(self, tool, position):
self.tool = tool
self.position = position
def to_inch(self):
if self.tool.settings.units == 'metric':
self.tool.to_inch()
self.position = tuple(map(inch, self.position))
def to_metric(self):
if self.tool.settings.units == 'inch':
self.tool.to_metric()
self.position = tuple(map(metric, self.position))
@property
def bounding_box(self):
position = self.position
radius = self.tool.diameter / 2.
min_x = position[0] - radius
max_x = position[0] + radius
min_y = position[1] - radius
max_y = position[1] + radius
return ((min_x, max_x), (min_y, max_y))
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(operator.add, self.position, (x_offset, y_offset)))
def __str__(self):
return 'Hit (%f, %f) {%s}' % (self.position[0], self.position[1], self.tool)
class DrillSlot(object):
"""
A slot is created between two points. The way the slot is created depends on the statement used to create it
"""
TYPE_ROUT = 1
TYPE_G85 = 2
def __init__(self, tool, start, end, slot_type):
self.tool = tool
self.start = start
self.end = end
self.slot_type = slot_type
def to_inch(self):
if self.tool.settings.units == 'metric':
self.tool.to_inch()
self.start = tuple(map(inch, self.start))
self.end = tuple(map(inch, self.end))
def to_metric(self):
if self.tool.settings.units == 'inch':
self.tool.to_metric()
self.start = tuple(map(metric, self.start))
self.end = tuple(map(metric, self.end))
@property
def bounding_box(self):
start = self.start
end = self.end
radius = self.tool.diameter / 2.
min_x = min(start[0], end[0]) - radius
max_x = max(start[0], end[0]) + radius
min_y = min(start[1], end[1]) - radius
max_y = max(start[1], end[1]) + radius
return ((min_x, max_x), (min_y, max_y))
def offset(self, x_offset=0, y_offset=0):
self.start = tuple(map(operator.add, self.start, (x_offset, y_offset)))
self.end = tuple(map(operator.add, self.end, (x_offset, y_offset)))
class ExcellonFile(CamFile):
""" A class representing a single excellon file
The ExcellonFile class represents a single excellon file.
http://www.excellon.com/manuals/program.htm
(archived version at https://web.archive.org/web/20150920001043/http://www.excellon.com/manuals/program.htm)
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
@property
def primitives(self):
"""
Gets the primitives. Note that unlike Gerber, this generates new objects
"""
primitives = []
for hit in self.hits:
if isinstance(hit, DrillHit):
primitives.append(Drill(hit.position, hit.tool.diameter,
units=self.settings.units))
elif isinstance(hit, DrillSlot):
primitives.append(Slot(hit.start, hit.end, hit.tool.diameter,
units=self.settings.units))
else:
raise ValueError('Unknown hit type')
return primitives
@property
def bounding_box(self):
xmin = ymin = 100000000000
xmax = ymax = -100000000000
for hit in self.hits:
bbox = hit.bounding_box
xmin = min(bbox[0][0], xmin)
xmax = max(bbox[0][1], xmax)
ymin = min(bbox[1][0], ymin)
ymax = max(bbox[1][1], ymax)
return ((xmin, xmax), (ymin, ymax))
def report(self, filename=None):
""" Print or save drill report
"""
if self.settings.units == 'inch':
toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}in.\n' % self.settings.format
else:
toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}mm\n' % self.settings.format
rprt = '=====================\nExcellon Drill Report\n=====================\n'
if self.filename is not None:
rprt += 'NC Drill File: %s\n\n' % self.filename
rprt += 'Drill File Info:\n----------------\n'
rprt += (' Data Mode %s\n' % 'Absolute'
if self.settings.notation == 'absolute' else 'Incremental')
rprt += (' Units %s\n' % 'Inches'
if self.settings.units == 'inch' else 'Millimeters')
rprt += '\nTool List:\n----------\n\n'
rprt += ' Code Size Hits Path Length\n'
rprt += ' --------------------------------------\n'
for tool in iter(self.tools.values()):
rprt += toolfmt.format(tool.number, tool.diameter,
tool.hit_count, self.path_length(tool.number))
if filename is not None:
with open(filename, 'w') as f:
f.write(rprt)
return rprt
def write(self, filename=None):
filename = filename if filename is not None else self.filename
with open(filename, 'w') as f:
# Copy the header verbatim
for statement in self.statements:
if not isinstance(statement, ToolSelectionStmt):
f.write(statement.to_excellon(self.settings) + '\n')
else:
break
# Write out coordinates for drill hits by tool
for tool in iter(self.tools.values()):
f.write(ToolSelectionStmt(tool.number).to_excellon(self.settings) + '\n')
for hit in self.hits:
if hit.tool.number == tool.number:
f.write(CoordinateStmt(
*hit.position).to_excellon(self.settings) + '\n')
f.write(EndOfProgramStmt().to_excellon() + '\n')
def to_inch(self):
"""
Convert units to inches
"""
if self.units != 'inch':
for statement in self.statements:
statement.to_inch()
for tool in iter(self.tools.values()):
tool.to_inch()
#for primitive in self.primitives:
# primitive.to_inch()
#for hit in self.hits:
# hit.to_inch()
self.units = 'inch'
def to_metric(self):
""" Convert units to metric
"""
if self.units != 'metric':
for statement in self.statements:
statement.to_metric()
for tool in iter(self.tools.values()):
tool.to_metric()
#for primitive in self.primitives:
# print("Converting to metric: {}".format(primitive))
# primitive.to_metric()
# print(primitive)
for hit in self.hits:
hit.to_metric()
self.units = 'metric'
def offset(self, x_offset=0, y_offset=0):
for statement in self.statements:
statement.offset(x_offset, y_offset)
for primitive in self.primitives:
primitive.offset(x_offset, y_offset)
for hit in self. hits:
hit.offset(x_offset, y_offset)
def path_length(self, tool_number=None):
""" Return the path length for a given tool
"""
lengths = {}
positions = {}
for hit in self.hits:
tool = hit.tool
num = tool.number
positions[num] = ((0, 0) if positions.get(num) is None
else positions[num])
lengths[num] = 0.0 if lengths.get(num) is None else lengths[num]
lengths[num] = lengths[
num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position)))
positions[num] = hit.position
if tool_number is None:
return lengths
else:
return lengths.get(tool_number)
def hit_count(self, tool_number=None):
counts = {}
for tool in iter(self.tools.values()):
counts[tool.number] = tool.hit_count
if tool_number is None:
return counts
else:
return counts.get(tool_number)
def update_tool(self, tool_number, **kwargs):
""" Change parameters of a tool
"""
if kwargs.get('feed_rate') is not None:
self.tools[tool_number].feed_rate = kwargs.get('feed_rate')
if kwargs.get('retract_rate') is not None:
self.tools[tool_number].retract_rate = kwargs.get('retract_rate')
if kwargs.get('rpm') is not None:
self.tools[tool_number].rpm = kwargs.get('rpm')
if kwargs.get('diameter') is not None:
self.tools[tool_number].diameter = kwargs.get('diameter')
if kwargs.get('max_hit_count') is not None:
self.tools[tool_number].max_hit_count = kwargs.get('max_hit_count')
if kwargs.get('depth_offset') is not None:
self.tools[tool_number].depth_offset = kwargs.get('depth_offset')
# Update drill hits
newtool = self.tools[tool_number]
for hit in self.hits:
if hit.tool.number == newtool.number:
hit.tool = newtool
class ExcellonParser(object):
""" Excellon File Parser
Parameters
----------
settings : FileSettings or dict-like
Excellon file settings to use when interpreting the excellon file.
"""
def __init__(self, settings=None, ext_tools=None):
self.notation = 'absolute'
self.units = 'inch'
self.zeros = 'leading'
self.format = (2, 4)
self.state = 'INIT'
self.statements = []
self.tools = {}
self.ext_tools = ext_tools or {}
self.comment_tools = {}
self.hits = []
self.active_tool = None
self.pos = [0., 0.]
self.drill_down = False
self._previous_line = ''
# Default for plated is None, which means we don't know
self.plated = ExcellonTool.PLATED_UNKNOWN
if settings is not None:
self.units = settings.units
self.zeros = settings.zeros
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, 'rU') as f:
data = f.read()
return self.parse_raw(data, filename)
def parse_raw(self, data, filename=None):
for line in StringIO(data):
self._parse_line(line.strip())
for stmt in self.statements:
stmt.units = self.units
return ExcellonFile(self.statements, self.tools, self.hits,
self._settings(), filename)
def _parse_line(self, line):
# skip empty lines
# Prepend previous line's data...
line = '{}{}'.format(self._previous_line, line)
self._previous_line = ''
# Skip empty lines
if not line.strip():
return
if line[0] == ';':
comment_stmt = CommentStmt.from_excellon(line)
self.statements.append(comment_stmt)
# get format from altium comment
if "FILE_FORMAT" in comment_stmt.comment:
detected_format = tuple(
[int(x) for x in comment_stmt.comment.split('=')[1].split(":")])
if detected_format:
self.format = detected_format
if "TYPE=PLATED" in comment_stmt.comment:
self.plated = ExcellonTool.PLATED_YES
if "TYPE=NON_PLATED" in comment_stmt.comment:
self.plated = ExcellonTool.PLATED_NO
if "HEADER:" in comment_stmt.comment:
self.state = "HEADER"
if " Holesize " in comment_stmt.comment:
self.state = "HEADER"
# Parse this as a hole definition
tools = ExcellonToolDefinitionParser(self._settings()).parse_raw(comment_stmt.comment)
if len(tools) == 1:
tool = tools[tools.keys()[0]]
self._add_comment_tool(tool)
elif line[:3] == 'M48':
self.statements.append(HeaderBeginStmt())
self.state = 'HEADER'
elif line[0] == '%':
self.statements.append(RewindStopStmt())
if self.state == 'HEADER':
self.state = 'DRILL'
elif self.state == 'INIT':
self.state = 'HEADER'
elif line[:3] == 'M00' and self.state == 'DRILL':
if self.active_tool:
cur_tool_number = self.active_tool.number
next_tool = self._get_tool(cur_tool_number + 1)
self.statements.append(NextToolSelectionStmt(self.active_tool, next_tool))
self.active_tool = next_tool
else:
raise Exception('Invalid state exception')
elif line[:3] == 'M95':
self.statements.append(HeaderEndStmt())
if self.state == 'HEADER':
self.state = 'DRILL'
elif line[:3] == 'M15':
self.statements.append(ZAxisRoutPositionStmt())
self.drill_down = True
elif line[:3] == 'M16':
self.statements.append(RetractWithClampingStmt())
self.drill_down = False
elif line[:3] == 'M17':
self.statements.append(RetractWithoutClampingStmt())
self.drill_down = False
elif line[:3] == 'M30':
stmt = EndOfProgramStmt.from_excellon(line, self._settings())
self.statements.append(stmt)
elif line[:3] == 'G00':
# Coordinates may be on the next line
if line.strip() == 'G00':
self._previous_line = line
return
self.statements.append(RouteModeStmt())
self.state = 'ROUT'
stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
stmt.mode = self.state
x = stmt.x
y = stmt.y
self.statements.append(stmt)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
if y is not None:
self.pos[1] = y
else:
if x is not None:
self.pos[0] += x
if y is not None:
self.pos[1] += y
elif line[:3] == 'G01':
# Coordinates might be on the next line...
if line.strip() == 'G01':
self._previous_line = line
return
self.statements.append(RouteModeStmt())
self.state = 'LINEAR'
stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
stmt.mode = self.state
# The start position is where we were before the rout command
start = (self.pos[0], self.pos[1])
x = stmt.x
y = stmt.y
self.statements.append(stmt)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
if y is not None:
self.pos[1] = y
else:
if x is not None:
self.pos[0] += x
if y is not None:
self.pos[1] += y
# Our ending position
end = (self.pos[0], self.pos[1])
if self.drill_down:
if not self.active_tool:
self.active_tool = self._get_tool(1)
self.hits.append(DrillSlot(self.active_tool, start, end, DrillSlot.TYPE_ROUT))
self.active_tool._hit()
elif line[:3] == 'G05':
self.statements.append(DrillModeStmt())
self.drill_down = False
self.state = 'DRILL'
elif 'INCH' in line or 'METRIC' in line:
stmt = UnitStmt.from_excellon(line)
self.units = stmt.units
self.zeros = stmt.zeros
if stmt.format:
self.format = stmt.format
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)
self.format = stmt.format_tuple
elif line[:3] == 'G40':
self.statements.append(CutterCompensationOffStmt())
elif line[:3] == 'G41':
self.statements.append(CutterCompensationLeftStmt())
elif line[:3] == 'G42':
self.statements.append(CutterCompensationRightStmt())
elif line[:3] == 'G90':
self.statements.append(AbsoluteModeStmt())
self.notation = 'absolute'
elif line[0] == 'F':
infeed_rate_stmt = ZAxisInfeedRateStmt.from_excellon(line)
self.statements.append(infeed_rate_stmt)
elif line[0] == 'T' and self.state == 'HEADER':
if not ',OFF' in line and not ',ON' in line:
tool = ExcellonTool.from_excellon(line, self._settings(), None, self.plated)
self._merge_properties(tool)
self.tools[tool.number] = tool
self.statements.append(tool)
else:
self.statements.append(UnknownStmt.from_excellon(line))
elif line[0] == 'T' and self.state != 'HEADER':
stmt = ToolSelectionStmt.from_excellon(line)
self.statements.append(stmt)
# T0 is used as END marker, just ignore
if stmt.tool != 0:
tool = self._get_tool(stmt.tool)
if not tool:
# FIXME: for weird files with no tools defined, original calc from gerb
if self._settings().units == "inch":
diameter = (16 + 8 * stmt.tool) / 1000.0
else:
diameter = metric((16 + 8 * stmt.tool) / 1000.0)
tool = ExcellonTool(
self._settings(), number=stmt.tool, diameter=diameter)
self.tools[tool.number] = tool
# FIXME: need to add this tool definition inside header to
# make sure it is properly written
for i, s in enumerate(self.statements):
if isinstance(s, ToolSelectionStmt) or isinstance(s, ExcellonTool):
self.statements.insert(i, tool)
break
self.active_tool = tool
elif line[0] == 'R' and self.state != 'HEADER':
stmt = RepeatHoleStmt.from_excellon(line, self._settings())
self.statements.append(stmt)
for i in range(stmt.count):
self.pos[0] += stmt.xdelta if stmt.xdelta is not None else 0
self.pos[1] += stmt.ydelta if stmt.ydelta is not None else 0
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
self.active_tool._hit()
elif line[0] in ['X', 'Y']:
if 'G85' in line:
stmt = SlotStmt.from_excellon(line, self._settings())
# I don't know if this is actually correct, but it makes sense
# that this is where the tool would end
x = stmt.x_end
y = stmt.y_end
self.statements.append(stmt)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
if y is not None:
self.pos[1] = y
else:
if x is not None:
self.pos[0] += x
if y is not None:
self.pos[1] += y
if self.state == 'DRILL' or self.state == 'HEADER':
if not self.active_tool:
self.active_tool = self._get_tool(1)
self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end), DrillSlot.TYPE_G85))
self.active_tool._hit()
else:
stmt = CoordinateStmt.from_excellon(line, self._settings())
# We need this in case we are in rout mode
start = (self.pos[0], self.pos[1])
x = stmt.x
y = stmt.y
self.statements.append(stmt)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
if y is not None:
self.pos[1] = y
else:
if x is not None:
self.pos[0] += x
if y is not None:
self.pos[1] += y
if self.state == 'LINEAR' and self.drill_down:
if not self.active_tool:
self.active_tool = self._get_tool(1)
self.hits.append(DrillSlot(self.active_tool, start, tuple(self.pos), DrillSlot.TYPE_ROUT))
elif self.state == 'DRILL' or self.state == 'HEADER':
# Yes, drills in the header doesn't follow the specification, but it there are many
# files like this
if not self.active_tool:
self.active_tool = self._get_tool(1)
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
self.active_tool._hit()
else:
self.statements.append(UnknownStmt.from_excellon(line))
def _settings(self):
return FileSettings(units=self.units, format=self.format,
zeros=self.zeros, notation=self.notation)
def _add_comment_tool(self, tool):
"""
Add a tool that was defined in the comments to this file.
If we have already found this tool, then we will merge this comment tool definition into
the information for the tool
"""
existing = self.tools.get(tool.number)
if existing and existing.plated == None:
existing.plated = tool.plated
self.comment_tools[tool.number] = tool
def _merge_properties(self, tool):
"""
When we have externally defined tools, merge the properties of that tool into this one
For now, this is only plated
"""
if tool.plated == ExcellonTool.PLATED_UNKNOWN:
ext_tool = self.ext_tools.get(tool.number)
if ext_tool:
tool.plated = ext_tool.plated
def _get_tool(self, toolid):
tool = self.tools.get(toolid)
if not tool:
tool = self.comment_tools.get(toolid)
if tool:
tool.settings = self._settings()
self.tools[toolid] = tool
if not tool:
tool = self.ext_tools.get(toolid)
if tool:
tool.settings = self._settings()
self.tools[toolid] = tool
return tool
def detect_excellon_format(data=None, filename=None):
""" Detect excellon file decimal format and zero-suppression settings.
Parameters
----------
data : string
String containing contents of Excellon file.
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
zeros_options = ('leading', 'trailing', )
format_options = ((2, 4), (2, 5), (3, 3),)
if data is None and filename is None:
raise ValueError('Either data or filename arguments must be provided')
if data is None:
with open(filename, 'rU') as f:
data = f.read()
# Check for obvious clues:
p = ExcellonParser()
p.parse_raw(data)
# Get zero_suppression from a unit statement
zero_statements = [stmt.zeros 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, 'zeros': detected_zeros}
# Only look at remaining options
if detected_format is not None:
format_options = (detected_format,)
if detected_zeros is not None:
zeros_options = (detected_zeros,)
# Brute force all remaining options, and pick the best looking one...
for zeros in zeros_options:
for fmt in format_options:
key = (fmt, zeros)
settings = FileSettings(zeros=zeros, format=fmt)
try:
p = ExcellonParser(settings)
ef = p.parse_raw(data)
size = tuple([t[0] - t[1] for t in ef.bounding_box])
hole_area = 0.0
for hit in p.hits:
tool = hit.tool
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 iter(results.keys()))
zeros = set(key[1] for key in iter(results.keys()))
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, 'zeros': 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 iter(scores.keys()):
if scores[key] == minscore:
return {'format': key[0], 'zeros': 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]
if board_area == 0:
return 0
hole_percentage = hole_area / board_area
hole_score = (hole_percentage - 0.25) ** 2
size_score = (board_area - 8) ** 2
return hole_score * size_score

Wyświetl plik

@ -0,0 +1,25 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2015 Garret Fick <garret@ficksworkshop.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.
"""
Excellon DRR File module
====================
**Excellon file classes**
Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
"""

Wyświetl plik

@ -0,0 +1,105 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from argparse import PARSER
# Copyright 2015 Garret Fick <garret@ficksworkshop.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.
"""
Excellon Settings Definition File module
====================
**Excellon file classes**
This module provides Excellon file classes and parsing utilities
"""
import re
try:
from cStringIO import StringIO
except(ImportError):
from io import StringIO
from .cam import FileSettings
def loads(data):
""" Read settings file information and return an FileSettings
Parameters
----------
data : string
string containing Excellon settings file contents
Returns
-------
file settings: FileSettings
"""
return ExcellonSettingsParser().parse_raw(data)
def map_coordinates(value):
if value == 'ABSOLUTE':
return 'absolute'
return 'relative'
def map_units(value):
if value == 'ENGLISH':
return 'inch'
return 'metric'
def map_boolean(value):
return value == 'YES'
SETTINGS_KEYS = {
'INTEGER-PLACES': (int, 'format-int'),
'DECIMAL-PLACES': (int, 'format-dec'),
'COORDINATES': (map_coordinates, 'notation'),
'OUTPUT-UNITS': (map_units, 'units'),
}
class ExcellonSettingsParser(object):
"""Excellon Settings PARSER
Parameters
----------
None
"""
def __init__(self):
self.values = {}
self.settings = None
def parse_raw(self, data):
for line in StringIO(data):
self._parse(line.strip())
# Create the FileSettings object
self.settings = FileSettings(
notation=self.values['notation'],
units=self.values['units'],
format=(self.values['format-int'], self.values['format-dec'])
)
return self.settings
def _parse(self, line):
line_items = line.split()
if len(line_items) == 2:
item_type_info = SETTINGS_KEYS.get(line_items[0])
if item_type_info:
# Convert the value to the expected type
item_value = item_type_info[0](line_items[1])
self.values[item_type_info[1]] = item_value

Wyświetl plik

@ -0,0 +1,979 @@
#!/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**
"""
import re
import uuid
import itertools
from .utils import (parse_gerber_value, write_gerber_value, decimal_string,
inch, metric)
__all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt',
'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt',
'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt',
'MeasuringModeStmt', 'RouteModeStmt', 'LinearModeStmt', 'DrillModeStmt',
'AbsoluteModeStmt', 'RepeatHoleStmt', 'UnknownStmt',
'ExcellonStatement', 'ZAxisRoutPositionStmt',
'RetractWithClampingStmt', 'RetractWithoutClampingStmt',
'CutterCompensationOffStmt', 'CutterCompensationLeftStmt',
'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt',
'NextToolSelectionStmt', 'SlotStmt']
class ExcellonStatement(object):
""" Excellon Statement abstract base class
"""
@classmethod
def from_excellon(cls, line):
raise NotImplementedError('from_excellon must be implemented in a '
'subclass')
def __init__(self, unit='inch', id=None):
self.units = unit
self.id = uuid.uuid4().int if id is None else id
def to_excellon(self, settings=None):
raise NotImplementedError('to_excellon must be implemented in a '
'subclass')
def to_inch(self):
self.units = 'inch'
def to_metric(self):
self.units = 'metric'
def offset(self, x_offset=0, y_offset=0):
pass
def __eq__(self, other):
return self.__dict__ == other.__dict__
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.
"""
PLATED_UNKNOWN = None
PLATED_YES = 'plated'
PLATED_NO = 'nonplated'
PLATED_OPTIONAL = 'optional'
@classmethod
def from_tool(cls, tool):
args = {}
args['depth_offset'] = tool.depth_offset
args['diameter'] = tool.diameter
args['feed_rate'] = tool.feed_rate
args['max_hit_count'] = tool.max_hit_count
args['number'] = tool.number
args['plated'] = tool.plated
args['retract_rate'] = tool.retract_rate
args['rpm'] = tool.rpm
return cls(None, **args)
@classmethod
def from_excellon(cls, line, settings, id=None, plated=None):
""" 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 = pairwise(re.split('([BCFHSTZ])', line)[1:])
args = {}
args['id'] = id
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)
if plated != ExcellonTool.PLATED_UNKNOWN:
# Sometimees we can can parse the plating status
args['plated'] = plated
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):
if kwargs.get('id') is not None:
super(ExcellonTool, self).__init__(id=kwargs.get('id'))
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.plated = kwargs.get('plated')
self.hit_count = 0
def to_excellon(self, settings=None):
if self.settings and not settings:
settings = self.settings
fmt = settings.format
zs = settings.zero_suppression
stmt = 'T%02d' % 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 to_inch(self):
if self.settings.units != 'inch':
self.settings.units = 'inch'
if self.diameter is not None:
self.diameter = inch(self.diameter)
def to_metric(self):
if self.settings.units != 'metric':
self.settings.units = 'metric'
if self.diameter is not None:
self.diameter = metric(self.diameter)
def _hit(self):
self.hit_count += 1
def equivalent(self, other):
"""
Is the other tool equal to this, ignoring the tool number, and other file specified properties
"""
if type(self) != type(other):
return False
return (self.diameter == other.diameter
and self.feed_rate == other.feed_rate
and self.retract_rate == other.retract_rate
and self.rpm == other.rpm
and self.depth_offset == other.depth_offset
and self.max_hit_count == other.max_hit_count
and self.plated == other.plated
and self.settings.units == other.settings.units)
def __repr__(self):
unit = 'in.' if self.settings.units == 'inch' else 'mm'
fmtstr = '<ExcellonTool %%02d: %%%d.%dg%%s dia.>' % self.settings.format
return fmtstr % (self.number, self.diameter, unit)
class ToolSelectionStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, **kwargs):
""" 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
# up to 3 characters for tool number (Frizting uses that)
if len(line) <= 3:
tool = int(line)
else:
tool = int(line[:2])
compensation_index = int(line[2:])
return cls(tool, compensation_index, **kwargs)
def __init__(self, tool, compensation_index=None, **kwargs):
super(ToolSelectionStmt, self).__init__(**kwargs)
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, settings=None):
stmt = 'T%02d' % self.tool
if self.compensation_index is not None:
stmt += '%02d' % self.compensation_index
return stmt
class NextToolSelectionStmt(ExcellonStatement):
# TODO the statement exists outside of the context of the file,
# so it is imposible to know that it is really the next tool
def __init__(self, cur_tool, next_tool, **kwargs):
"""
Select the next tool in the wheel.
Parameters
----------
cur_tool : the tool that is currently selected
next_tool : the that that is now selected
"""
super(NextToolSelectionStmt, self).__init__(**kwargs)
self.cur_tool = cur_tool
self.next_tool = next_tool
def to_excellon(self, settings=None):
stmt = 'M00'
return stmt
class ZAxisInfeedRateStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, **kwargs):
""" Create a ZAxisInfeedRate from an excellon file line.
Parameters
----------
line : string
Line from an Excellon file
Returns
-------
z_axis_infeed_rate : ToolSelectionStmt
ToolSelectionStmt representation of `line.`
"""
rate = int(line[1:])
return cls(rate, **kwargs)
def __init__(self, rate, **kwargs):
super(ZAxisInfeedRateStmt, self).__init__(**kwargs)
self.rate = rate
def to_excellon(self, settings=None):
return 'F%02d' % self.rate
class CoordinateStmt(ExcellonStatement):
@classmethod
def from_point(cls, point, mode=None):
stmt = cls(point[0], point[1])
if mode:
stmt.mode = mode
return stmt
@classmethod
def from_excellon(cls, line, settings, **kwargs):
x_coord = None
y_coord = None
if line[0] == 'X':
splitline = line.strip('X').split('Y')
x_coord = parse_gerber_value(splitline[0], settings.format,
settings.zero_suppression)
if len(splitline) == 2:
y_coord = parse_gerber_value(splitline[1], settings.format,
settings.zero_suppression)
else:
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
settings.zero_suppression)
c = cls(x_coord, y_coord, **kwargs)
c.units = settings.units
return c
def __init__(self, x=None, y=None, **kwargs):
super(CoordinateStmt, self).__init__(**kwargs)
self.x = x
self.y = y
self.mode = None
def to_excellon(self, settings):
stmt = ''
if self.mode == "ROUT":
stmt += "G00"
if self.mode == "LINEAR":
stmt += "G01"
if self.x is not None:
stmt += 'X%s' % write_gerber_value(self.x, settings.format,
settings.zero_suppression)
if self.y is not None:
stmt += 'Y%s' % write_gerber_value(self.y, settings.format,
settings.zero_suppression)
return stmt
def to_inch(self):
if self.units == 'metric':
self.units = 'inch'
if self.x is not None:
self.x = inch(self.x)
if self.y is not None:
self.y = inch(self.y)
def to_metric(self):
if self.units == 'inch':
self.units = 'metric'
if self.x is not None:
self.x = metric(self.x)
if self.y is not None:
self.y = metric(self.y)
def offset(self, x_offset=0, y_offset=0):
if self.x is not None:
self.x += x_offset
if self.y is not None:
self.y += y_offset
def __str__(self):
coord_str = ''
if self.x is not None:
coord_str += 'X: %g ' % self.x
if self.y is not None:
coord_str += 'Y: %g ' % self.y
return '<Coordinate Statement: %s>' % coord_str
class RepeatHoleStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, settings, **kwargs):
match = re.compile(r'R(?P<rcount>[0-9]*)X?(?P<xdelta>[+\-]?\d*\.?\d*)?Y?'
'(?P<ydelta>[+\-]?\d*\.?\d*)?').match(line)
stmt = match.groupdict()
count = int(stmt['rcount'])
xdelta = (parse_gerber_value(stmt['xdelta'], settings.format,
settings.zero_suppression)
if stmt['xdelta'] is not '' else None)
ydelta = (parse_gerber_value(stmt['ydelta'], settings.format,
settings.zero_suppression)
if stmt['ydelta'] is not '' else None)
c = cls(count, xdelta, ydelta, **kwargs)
c.units = settings.units
return c
def __init__(self, count, xdelta=0.0, ydelta=0.0, **kwargs):
super(RepeatHoleStmt, self).__init__(**kwargs)
self.count = count
self.xdelta = xdelta
self.ydelta = ydelta
def to_excellon(self, settings):
stmt = 'R%d' % self.count
if self.xdelta is not None and self.xdelta != 0.0:
stmt += 'X%s' % write_gerber_value(self.xdelta, settings.format,
settings.zero_suppression)
if self.ydelta is not None and self.ydelta != 0.0:
stmt += 'Y%s' % write_gerber_value(self.ydelta, settings.format,
settings.zero_suppression)
return stmt
def to_inch(self):
if self.units == 'metric':
self.units = 'inch'
if self.xdelta is not None:
self.xdelta = inch(self.xdelta)
if self.ydelta is not None:
self.ydelta = inch(self.ydelta)
def to_metric(self):
if self.units == 'inch':
self.units = 'metric'
if self.xdelta is not None:
self.xdelta = metric(self.xdelta)
if self.ydelta is not None:
self.ydelta = metric(self.ydelta)
def __str__(self):
return '<Repeat Hole: %d times, offset X: %g Y: %g>' % (
self.count,
self.xdelta if self.xdelta is not None else 0,
self.ydelta if self.ydelta is not None else 0)
class CommentStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, **kwargs):
return cls(line.lstrip(';'))
def __init__(self, comment, **kwargs):
super(CommentStmt, self).__init__(**kwargs)
self.comment = comment
def to_excellon(self, settings=None):
return ';%s' % self.comment
class HeaderBeginStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(HeaderBeginStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'M48'
class HeaderEndStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(HeaderEndStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'M95'
class RewindStopStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(RewindStopStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return '%'
class ZAxisRoutPositionStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(ZAxisRoutPositionStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'M15'
class RetractWithClampingStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(RetractWithClampingStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'M16'
class RetractWithoutClampingStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(RetractWithoutClampingStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'M17'
class CutterCompensationOffStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(CutterCompensationOffStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G40'
class CutterCompensationLeftStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(CutterCompensationLeftStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G41'
class CutterCompensationRightStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(CutterCompensationRightStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G42'
class EndOfProgramStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, settings, **kwargs):
match = re.compile(r'M30X?(?P<x>\d*\.?\d*)?Y?'
'(?P<y>\d*\.?\d*)?').match(line)
stmt = match.groupdict()
x = (parse_gerber_value(stmt['x'], settings.format,
settings.zero_suppression)
if stmt['x'] is not '' else None)
y = (parse_gerber_value(stmt['y'], settings.format,
settings.zero_suppression)
if stmt['y'] is not '' else None)
c = cls(x, y, **kwargs)
c.units = settings.units
return c
def __init__(self, x=None, y=None, **kwargs):
super(EndOfProgramStmt, self).__init__(**kwargs)
self.x = x
self.y = y
def to_excellon(self, settings=None):
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
def to_inch(self):
if self.units == 'metric':
self.units = 'inch'
if self.x is not None:
self.x = inch(self.x)
if self.y is not None:
self.y = inch(self.y)
def to_metric(self):
if self.units == 'inch':
self.units = 'metric'
if self.x is not None:
self.x = metric(self.x)
if self.y is not None:
self.y = metric(self.y)
def offset(self, x_offset=0, y_offset=0):
if self.x is not None:
self.x += x_offset
if self.y is not None:
self.y += y_offset
class UnitStmt(ExcellonStatement):
@classmethod
def from_settings(cls, settings):
"""Create the unit statement from the FileSettings"""
return cls(settings.units, settings.zeros)
@classmethod
def from_excellon(cls, line, **kwargs):
units = 'inch' if 'INCH' in line else 'metric'
zeros = 'leading' if 'LZ' in line else 'trailing'
if '0000.00' in line:
format = (4, 2)
elif '000.000' in line:
format = (3, 3)
elif '00.0000' in line:
format = (2, 4)
else:
format = None
return cls(units, zeros, format, **kwargs)
def __init__(self, units='inch', zeros='leading', format=None, **kwargs):
super(UnitStmt, self).__init__(**kwargs)
self.units = units.lower()
self.zeros = zeros
self.format = format
def to_excellon(self, settings=None):
# TODO This won't export the invalid format statement if it exists
stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC',
'LZ' if self.zeros == 'leading'
else 'TZ')
return stmt
def to_inch(self):
self.units = 'inch'
def to_metric(self):
self.units = 'metric'
class IncrementalModeStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, **kwargs):
return cls('off', **kwargs) if 'OFF' in line else cls('on', **kwargs)
def __init__(self, mode='off', **kwargs):
super(IncrementalModeStmt, self).__init__(**kwargs)
if mode.lower() not in ['on', 'off']:
raise ValueError('Mode may be "on" or "off"')
self.mode = mode
def to_excellon(self, settings=None):
return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON')
class VersionStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, **kwargs):
version = int(line.split(',')[1])
return cls(version, **kwargs)
def __init__(self, version=1, **kwargs):
super(VersionStmt, self).__init__(**kwargs)
version = int(version)
if version not in [1, 2]:
raise ValueError('Valid versions are 1 or 2')
self.version = version
def to_excellon(self, settings=None):
return 'VER,%d' % self.version
class FormatStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, **kwargs):
fmt = int(line.split(',')[1])
return cls(fmt, **kwargs)
def __init__(self, format=1, **kwargs):
super(FormatStmt, self).__init__(**kwargs)
format = int(format)
if format not in [1, 2]:
raise ValueError('Valid formats are 1 or 2')
self.format = format
def to_excellon(self, settings=None):
return 'FMAT,%d' % self.format
@property
def format_tuple(self):
return (self.format, 6 - self.format)
class LinkToolStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, **kwargs):
linked = [int(tool) for tool in line.split('/')]
return cls(linked, **kwargs)
def __init__(self, linked_tools, **kwargs):
super(LinkToolStmt, self).__init__(**kwargs)
self.linked_tools = [int(x) for x in linked_tools]
def to_excellon(self, settings=None):
return '/'.join([str(x) for x in self.linked_tools])
class MeasuringModeStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, **kwargs):
if not ('M71' in line or 'M72' in line):
raise ValueError('Not a measuring mode statement')
return cls('inch', **kwargs) if 'M72' in line else cls('metric', **kwargs)
def __init__(self, units='inch', **kwargs):
super(MeasuringModeStmt, self).__init__(**kwargs)
units = units.lower()
if units not in ['inch', 'metric']:
raise ValueError('units must be "inch" or "metric"')
self.units = units
def to_excellon(self, settings=None):
return 'M72' if self.units == 'inch' else 'M71'
def to_inch(self):
self.units = 'inch'
def to_metric(self):
self.units = 'metric'
class RouteModeStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(RouteModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G00'
class LinearModeStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(LinearModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G01'
class DrillModeStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(DrillModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G05'
class AbsoluteModeStmt(ExcellonStatement):
def __init__(self, **kwargs):
super(AbsoluteModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G90'
class UnknownStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, **kwargs):
return cls(line, **kwargs)
def __init__(self, stmt, **kwargs):
super(UnknownStmt, self).__init__(**kwargs)
self.stmt = stmt
def to_excellon(self, settings=None):
return self.stmt
def __str__(self):
return "<Unknown Statement: %s>" % self.stmt
class SlotStmt(ExcellonStatement):
"""
G85 statement. Defines a slot created by multiple drills between two specified points.
Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn
"""
@classmethod
def from_points(cls, start, end):
return cls(start[0], start[1], end[0], end[1])
@classmethod
def from_excellon(cls, line, settings, **kwargs):
# Split the line based on the G85 separator
sub_coords = line.split('G85')
(x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings)
(x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings)
# Some files seem to specify only one of the coordinates
if x_end_coord == None:
x_end_coord = x_start_coord
if y_end_coord == None:
y_end_coord = y_start_coord
c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs)
c.units = settings.units
return c
@staticmethod
def parse_sub_coords(line, settings):
x_coord = None
y_coord = None
if line[0] == 'X':
splitline = line.strip('X').split('Y')
x_coord = parse_gerber_value(splitline[0], settings.format,
settings.zero_suppression)
if len(splitline) == 2:
y_coord = parse_gerber_value(splitline[1], settings.format,
settings.zero_suppression)
else:
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
settings.zero_suppression)
return (x_coord, y_coord)
def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs):
super(SlotStmt, self).__init__(**kwargs)
self.x_start = x_start
self.y_start = y_start
self.x_end = x_end
self.y_end = y_end
self.mode = None
def to_excellon(self, settings):
stmt = ''
if self.x_start is not None:
stmt += 'X%s' % write_gerber_value(self.x_start, settings.format,
settings.zero_suppression)
if self.y_start is not None:
stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format,
settings.zero_suppression)
stmt += 'G85'
if self.x_end is not None:
stmt += 'X%s' % write_gerber_value(self.x_end, settings.format,
settings.zero_suppression)
if self.y_end is not None:
stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format,
settings.zero_suppression)
return stmt
def to_inch(self):
if self.units == 'metric':
self.units = 'inch'
if self.x_start is not None:
self.x_start = inch(self.x_start)
if self.y_start is not None:
self.y_start = inch(self.y_start)
if self.x_end is not None:
self.x_end = inch(self.x_end)
if self.y_end is not None:
self.y_end = inch(self.y_end)
def to_metric(self):
if self.units == 'inch':
self.units = 'metric'
if self.x_start is not None:
self.x_start = metric(self.x_start)
if self.y_start is not None:
self.y_start = metric(self.y_start)
if self.x_end is not None:
self.x_end = metric(self.x_end)
if self.y_end is not None:
self.y_end = metric(self.y_end)
def offset(self, x_offset=0, y_offset=0):
if self.x_start is not None:
self.x_start += x_offset
if self.y_start is not None:
self.y_start += y_offset
if self.x_end is not None:
self.x_end += x_offset
if self.y_end is not None:
self.y_end += y_offset
def __str__(self):
start_str = ''
if self.x_start is not None:
start_str += 'X: %g ' % self.x_start
if self.y_start is not None:
start_str += 'Y: %g ' % self.y_start
end_str = ''
if self.x_end is not None:
end_str += 'X: %g ' % self.x_end
if self.y_end is not None:
end_str += 'Y: %g ' % self.y_end
return '<Slot Statement: %s to %s>' % (start_str, end_str)
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)]
"""
a, b = itertools.tee(iterator)
itr = zip(itertools.islice(a, 0, None, 2), itertools.islice(b, 1, None, 2))
for elem in itr:
yield elem

Wyświetl plik

@ -0,0 +1,190 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2015 Garret Fick <garret@ficksworkshop.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.
"""
Excellon Tool Definition File module
====================
**Excellon file classes**
This module provides Excellon file classes and parsing utilities
"""
import re
try:
from cStringIO import StringIO
except(ImportError):
from io import StringIO
from .excellon_statements import ExcellonTool
def loads(data, settings=None):
""" Read tool file information and return a map of tools
Parameters
----------
data : string
string containing Excellon Tool Definition file contents
Returns
-------
dict tool name: ExcellonTool
"""
return ExcellonToolDefinitionParser(settings).parse_raw(data)
class ExcellonToolDefinitionParser(object):
""" Excellon File Parser
Parameters
----------
None
"""
allegro_tool = re.compile(r'(?P<size>[0-9/.]+)\s+(?P<plated>P|N)\s+T(?P<toolid>[0-9]{2})\s+(?P<xtol>[0-9/.]+)\s+(?P<ytol>[0-9/.]+)')
allegro_comment_mils = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
allegro2_comment_mils = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
allegro_comment_mm = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
allegro2_comment_mm = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
matchers = [
(allegro_tool, 'mils'),
(allegro_comment_mils, 'mils'),
(allegro2_comment_mils, 'mils'),
(allegro_comment_mm, 'mm'),
(allegro2_comment_mm, 'mm'),
]
def __init__(self, settings=None):
self.tools = {}
self.settings = settings
def parse_raw(self, data):
for line in StringIO(data):
self._parse(line.strip())
return self.tools
def _parse(self, line):
for matcher in ExcellonToolDefinitionParser.matchers:
m = matcher[0].match(line)
if m:
unit = matcher[1]
size = float(m.group('size'))
platedstr = m.group('plated')
toolid = int(m.group('toolid'))
xtol = float(m.group('xtol'))
ytol = float(m.group('ytol'))
size = self._convert_length(size, unit)
xtol = self._convert_length(xtol, unit)
ytol = self._convert_length(ytol, unit)
if platedstr == 'PLATED':
plated = ExcellonTool.PLATED_YES
elif platedstr == 'NON_PLATED':
plated = ExcellonTool.PLATED_NO
elif platedstr == 'OPTIONAL':
plated = ExcellonTool.PLATED_OPTIONAL
else:
plated = ExcellonTool.PLATED_UNKNOWN
tool = ExcellonTool(None, number=toolid, diameter=size,
plated=plated)
self.tools[tool.number] = tool
break
def _convert_length(self, value, unit):
# Convert the value to mm
if unit == 'mils':
value /= 39.3700787402
# Now convert to the settings unit
if self.settings.units == 'inch':
return value / 25.4
else:
# Already in mm
return value
def loads_rep(data, settings=None):
""" Read tool report information generated by PADS and return a map of tools
Parameters
----------
data : string
string containing Excellon Report file contents
Returns
-------
dict tool name: ExcellonTool
"""
return ExcellonReportParser(settings).parse_raw(data)
class ExcellonReportParser(object):
# We sometimes get files with different encoding, so we can't actually
# match the text - the best we can do it detect the table header
header = re.compile(r'====\s+====\s+====\s+====\s+=====\s+===')
def __init__(self, settings=None):
self.tools = {}
self.settings = settings
self.found_header = False
def parse_raw(self, data):
for line in StringIO(data):
self._parse(line.strip())
return self.tools
def _parse(self, line):
# skip empty lines and "comments"
if not line.strip():
return
if not self.found_header:
# Try to find the heaader, since we need that to be sure we
# understand the contents correctly.
if ExcellonReportParser.header.match(line):
self.found_header = True
elif line[0] != '=':
# Already found the header, so we know to to map the contents
parts = line.split()
if len(parts) == 6:
toolid = int(parts[0])
size = float(parts[1])
if parts[2] == 'x':
plated = ExcellonTool.PLATED_YES
elif parts[2] == '-':
plated = ExcellonTool.PLATED_NO
else:
plated = ExcellonTool.PLATED_UNKNOWN
feedrate = int(parts[3])
speed = int(parts[4])
qty = int(parts[5])
tool = ExcellonTool(None, number=toolid, diameter=size,
plated=plated, feed_rate=feedrate,
rpm=speed)
self.tools[tool.number] = tool

Wyświetl plik

@ -0,0 +1,36 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2015 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.
class ParseError(Exception):
pass
class GerberParseError(ParseError):
pass
class ExcellonParseError(ParseError):
pass
class ExcellonFileError(IOError):
pass
class GerberFileError(IOError):
pass

Plik diff jest za duży Load Diff

485
gerber/ipc356.py 100644
Wyświetl plik

@ -0,0 +1,485 @@
#! /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.
import math
import re
from .cam import CamFile, FileSettings
from .primitives import TestRecord
# Net Name Variables
_NNAME = re.compile(r'^NNAME\d+$')
# Board Edge Coordinates
_COORD = re.compile(r'X?(?P<x>[\d\s]*)?Y?(?P<y>[\d\s]*)?')
_SM_FIELD = {
'0': 'none',
'1': 'primary side',
'2': 'secondary side',
'3': 'both'}
def read(filename):
""" Read data from filename and return an IPCNetlist
Parameters
----------
filename : string
Filename of file to parse
Returns
-------
file : :class:`gerber.ipc356.IPCNetlist`
An IPCNetlist object created from the specified file.
"""
# File object should use settings from source file by default.
return IPCNetlist.from_file(filename)
def loads(data, filename=None):
""" Generate an IPCNetlist object from IPC-D-356 data in memory
Parameters
----------
data : string
string containing netlist file contents
filename : string, optional
string containing the filename of the data source
Returns
-------
file : :class:`gerber.ipc356.IPCNetlist`
An IPCNetlist created from the specified file.
"""
return IPCNetlistParser().parse_raw(data, filename)
class IPCNetlist(CamFile):
@classmethod
def from_file(cls, filename):
parser = IPCNetlistParser()
return parser.parse(filename)
def __init__(self, statements, settings, primitives=None, filename=None):
self.statements = statements
self.units = settings.units
self.angle_units = settings.angle_units
self.primitives = [TestRecord((rec.x_coord, rec.y_coord), rec.net_name,
rec.access) for rec in self.test_records]
self.filename = filename
@property
def settings(self):
return FileSettings(units=self.units, angle_units=self.angle_units)
@property
def comments(self):
return [record for record in self.statements
if isinstance(record, IPC356_Comment)]
@property
def parameters(self):
return [record for record in self.statements
if isinstance(record, IPC356_Parameter)]
@property
def test_records(self):
return [record for record in self.statements
if isinstance(record, IPC356_TestRecord)]
@property
def nets(self):
nets = []
for net in list(set([rec.net_name for rec in self.test_records
if rec.net_name is not None])):
adjacent_nets = set()
for record in self.adjacency_records:
if record.net == net:
adjacent_nets = adjacent_nets.update(record.adjacent_nets)
elif net in record.adjacent_nets:
adjacent_nets.add(record.net)
nets.append(IPC356_Net(net, adjacent_nets))
return nets
@property
def components(self):
return list(set([rec.id for rec in self.test_records
if rec.id is not None and rec.id != 'VIA']))
@property
def vias(self):
return [rec.id for rec in self.test_records if rec.id == 'VIA']
@property
def outlines(self):
return [stmt for stmt in self.statements
if isinstance(stmt, IPC356_Outline)]
@property
def adjacency_records(self):
return [record for record in self.statements
if isinstance(record, IPC356_Adjacency)]
def render(self, ctx, layer='both', filename=None):
for p in self.primitives:
if layer == 'both' and p.layer in ('top', 'bottom', 'both'):
ctx.render(p)
elif layer == 'top' and p.layer in ('top', 'both'):
ctx.render(p)
elif layer == 'bottom' and p.layer in ('bottom', 'both'):
ctx.render(p)
if filename is not None:
ctx.dump(filename)
class IPCNetlistParser(object):
# TODO: Allow multi-line statements (e.g. Altium board edge)
def __init__(self):
self.units = 'inch'
self.angle_units = 'degrees'
self.statements = []
self.nnames = {}
@property
def settings(self):
return FileSettings(units=self.units, angle_units=self.angle_units)
def parse(self, filename):
with open(filename, 'rU') as f:
data = f.read()
return self.parse_raw(data, filename)
def parse_raw(self, data, filename=None):
oldline = ''
for line in data.splitlines():
# Check for existing multiline data...
if oldline != '':
if len(line) and line[0] == '0':
oldline = oldline.rstrip('\r\n') + line[3:].rstrip()
else:
self._parse_line(oldline)
oldline = line
else:
oldline = line
self._parse_line(oldline)
return IPCNetlist(self.statements, self.settings, filename=filename)
def _parse_line(self, line):
if not len(line):
return
if line[0] == 'C':
# Comment
self.statements.append(IPC356_Comment.from_line(line))
elif line[0] == 'P':
# Parameter
p = IPC356_Parameter.from_line(line)
if p.parameter == 'UNITS':
if p.value in ('CUST', 'CUST 0'):
self.units = 'inch'
self.angle_units = 'degrees'
elif p.value == 'CUST 1':
self.units = 'metric'
self.angle_units = 'degrees'
elif p.value == 'CUST 2':
self.units = 'inch'
self.angle_units = 'radians'
self.statements.append(p)
if _NNAME.match(p.parameter):
# Add to list of net name variables
self.nnames[p.parameter] = p.value
elif line[0] == '9':
self.statements.append(IPC356_EndOfFile())
elif line[0:3] in ('317', '327', '367'):
# Test Record
record = IPC356_TestRecord.from_line(line, self.settings)
# Substitute net name variables
net = record.net_name
if (_NNAME.match(net) and net in self.nnames.keys()):
record.net_name = self.nnames[record.net_name]
self.statements.append(record)
elif line[0:3] == '378':
# Conductor
self.statements.append(
IPC356_Conductor.from_line(
line, self.settings))
elif line[0:3] == '379':
# Net Adjacency
self.statements.append(IPC356_Adjacency.from_line(line))
elif line[0:3] == '389':
# Outline
self.statements.append(
IPC356_Outline.from_line(
line, self.settings))
class IPC356_Comment(object):
@classmethod
def from_line(cls, line):
if line[0] != 'C':
raise ValueError('Not a valid comment statment')
comment = line[2:].strip()
return cls(comment)
def __init__(self, comment):
self.comment = comment
def __repr__(self):
return '<IPC-D-356 Comment: %s>' % self.comment
class IPC356_Parameter(object):
@classmethod
def from_line(cls, line):
if line[0] != 'P':
raise ValueError('Not a valid parameter statment')
splitline = line[2:].split()
parameter = splitline[0].strip()
value = ' '.join(splitline[1:]).strip()
return cls(parameter, value)
def __init__(self, parameter, value):
self.parameter = parameter
self.value = value
def __repr__(self):
return '<IPC-D-356 Parameter: %s=%s>' % (self.parameter, self.value)
class IPC356_TestRecord(object):
@classmethod
def from_line(cls, line, settings):
offset = 0
units = settings.units
angle = settings.angle_units
feature_types = {'1': 'through-hole', '2': 'smt',
'3': 'tooling-feature', '4': 'tooling-hole',
'6': 'non-plated-tooling-hole'}
access = ['both', 'top', 'layer2', 'layer3', 'layer4', 'layer5',
'layer6', 'layer7', 'bottom']
record = {}
line = line.strip()
if line[0] != '3':
raise ValueError('Not a valid test record statment')
record['feature_type'] = feature_types[line[1]]
end = len(line) - 1 if len(line) < 18 else 17
record['net_name'] = line[3:end].strip()
if len(line) >= 27 and line[26] != '-':
offset = line[26:].find('-')
offset = 0 if offset == -1 else offset
end = len(line) - 1 if len(line) < (27 + offset) else (26 + offset)
record['id'] = line[20:end].strip()
end = len(line) - 1 if len(line) < (32 + offset) else (31 + offset)
record['pin'] = (line[27 + offset:end].strip() if line[27 + offset:end].strip() != ''
else None)
record['location'] = 'middle' if line[31 + offset] == 'M' else 'end'
if line[32 + offset] == 'D':
end = len(line) - 1 if len(line) < (38 + offset) else (37 + offset)
dia = int(line[33 + offset:end].strip())
record['hole_diameter'] = (dia * 0.0001 if units == 'inch'
else dia * 0.001)
if len(line) >= (38 + offset):
record['plated'] = (line[37 + offset] == 'P')
if len(line) >= (40 + offset):
end = len(line) - 1 if len(line) < (42 + offset) else (41 + offset)
record['access'] = access[int(line[39 + offset:end])]
if len(line) >= (43 + offset):
end = len(line) - 1 if len(line) < (50 + offset) else (49 + offset)
coord = int(line[42 + offset:end].strip())
record['x_coord'] = (coord * 0.0001 if units == 'inch'
else coord * 0.001)
if len(line) >= (51 + offset):
end = len(line) - 1 if len(line) < (58 + offset) else (57 + offset)
coord = int(line[50 + offset:end].strip())
record['y_coord'] = (coord * 0.0001 if units == 'inch'
else coord * 0.001)
if len(line) >= (59 + offset):
end = len(line) - 1 if len(line) < (63 + offset) else (62 + offset)
dim = line[58 + offset:end].strip()
if dim != '':
record['rect_x'] = (int(dim) * 0.0001 if units == 'inch'
else int(dim) * 0.001)
if len(line) >= (64 + offset):
end = len(line) - 1 if len(line) < (68 + offset) else (67 + offset)
dim = line[63 + offset:end].strip()
if dim != '':
record['rect_y'] = (int(dim) * 0.0001 if units == 'inch'
else int(dim) * 0.001)
if len(line) >= (69 + offset):
end = len(line) - 1 if len(line) < (72 + offset) else (71 + offset)
rot = line[68 + offset:end].strip()
if rot != '':
record['rect_rotation'] = (int(rot) if angle == 'degrees'
else math.degrees(rot))
if len(line) >= (74 + offset):
end = 74 + offset
sm_info = line[73 + offset:end].strip()
record['soldermask_info'] = _SM_FIELD.get(sm_info)
if len(line) >= (76 + offset):
end = len(line) - 1 if len(line) < (80 + offset) else 79 + offset
record['optional_info'] = line[75 + offset:end]
return cls(**record)
def __init__(self, **kwargs):
for key in kwargs:
setattr(self, key, kwargs[key])
def __repr__(self):
return '<IPC-D-356 %s Test Record: %s>' % (self.net_name,
self.feature_type)
class IPC356_Outline(object):
@classmethod
def from_line(cls, line, settings):
type = line[3:17].strip()
scale = 0.0001 if settings.units == 'inch' else 0.001
points = []
x = 0
y = 0
coord_strings = line.strip().split()[1:]
for coord in coord_strings:
coord_dict = _COORD.match(coord).groupdict()
x = int(coord_dict['x']) if coord_dict['x'] is not '' else x
y = int(coord_dict['y']) if coord_dict['y'] is not '' else y
points.append((x * scale, y * scale))
return cls(type, points)
def __init__(self, type, points):
self.type = type
self.points = points
def __repr__(self):
return '<IPC-D-356 %s Outline Definition>' % self.type
class IPC356_Conductor(object):
@classmethod
def from_line(cls, line, settings):
if line[0:3] != '378':
raise ValueError('Not a valid IPC-D-356 Conductor statement')
scale = 0.0001 if settings.units == 'inch' else 0.001
net_name = line[3:17].strip()
layer = int(line[19:21])
# Parse out aperture definiting
raw_aperture = line[22:].split()[0]
aperture_dict = _COORD.match(raw_aperture).groupdict()
x = 0
y = 0
x = int(aperture_dict['x']) * \
scale if aperture_dict['x'] is not '' else None
y = int(aperture_dict['y']) * \
scale if aperture_dict['y'] is not '' else None
aperture = (x, y)
# Parse out conductor shapes
shapes = []
coord_list = ' '.join(line[22:].split()[1:])
raw_shapes = coord_list.split('*')
for rshape in raw_shapes:
x = 0
y = 0
shape = []
coords = rshape.split()
for coord in coords:
coord_dict = _COORD.match(coord).groupdict()
x = int(coord_dict['x']) if coord_dict['x'] is not '' else x
y = int(coord_dict['y']) if coord_dict['y'] is not '' else y
shape.append((x * scale, y * scale))
shapes.append(tuple(shape))
return cls(net_name, layer, aperture, tuple(shapes))
def __init__(self, net_name, layer, aperture, shapes):
self.net_name = net_name
self.layer = layer
self.aperture = aperture
self.shapes = shapes
def __repr__(self):
return '<IPC-D-356 %s Conductor Record>' % self.net_name
class IPC356_Adjacency(object):
@classmethod
def from_line(cls, line):
if line[0:3] != '379':
raise ValueError('Not a valid IPC-D-356 Conductor statement')
nets = line[3:].strip().split()
return cls(nets[0], nets[1:])
def __init__(self, net, adjacent_nets):
self.net = net
self.adjacent_nets = adjacent_nets
def __repr__(self):
return '<IPC-D-356 %s Adjacency Record>' % self.net
class IPC356_EndOfFile(object):
def __init__(self):
pass
def to_netlist(self):
return '999'
def __repr__(self):
return '<IPC-D-356 EOF>'
class IPC356_Net(object):
def __init__(self, name, adjacent_nets):
self.name = name
self.adjacent_nets = set(
adjacent_nets) if adjacent_nets is not None else set()
def __repr__(self):
return '<IPC-D-356 Net %s>' % self.name

295
gerber/layers.py 100644
Wyświetl plik

@ -0,0 +1,295 @@
#! /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 os
import re
from collections import namedtuple
from . import common
from .excellon import ExcellonFile
from .ipc356 import IPCNetlist
Hint = namedtuple('Hint', 'layer ext name regex content')
hints = [
Hint(layer='top',
ext=['gtl', 'cmp', 'top', ],
name=['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', 'F.Cu', ],
regex='',
content=[]
),
Hint(layer='bottom',
ext=['gbl', 'sld', 'bot', 'sol', 'bottom', ],
name=['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', 'B.Cu', ],
regex='',
content=[]
),
Hint(layer='internal',
ext=['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', ],
name=['art', 'internal', 'pgp', 'pwr', 'gnd', 'ground',
'gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6',
'In1.Cu', 'In2.Cu', 'In3.Cu', 'In4.Cu',
'group3', 'group4', 'group5', 'group6', 'group7', 'group8', ],
regex='',
content=[]
),
Hint(layer='topsilk',
ext=['gto', 'sst', 'plc', 'ts', 'skt', 'topsilk', ],
name=['sst01', 'topsilk', 'silk', 'slk', 'sst', 'F.SilkS'],
regex='',
content=[]
),
Hint(layer='bottomsilk',
ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk', ],
name=['bsilk', 'ssb', 'botsilk', 'bottomsilk', 'B.SilkS'],
regex='',
content=[]
),
Hint(layer='topmask',
ext=['gts', 'stc', 'tmk', 'smt', 'tr', 'topmask', ],
name=['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
'mst', 'F.Mask', ],
regex='',
content=[]
),
Hint(layer='bottommask',
ext=['gbs', 'sts', 'bmk', 'smb', 'br', 'bottommask', ],
name=['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'bottommask',
'msb', 'B.Mask', ],
regex='',
content=[]
),
Hint(layer='toppaste',
ext=['gtp', 'tm', 'toppaste', ],
name=['sp01', 'toppaste', 'pst', 'F.Paste'],
regex='',
content=[]
),
Hint(layer='bottompaste',
ext=['gbp', 'bm', 'bottompaste', ],
name=['sp02', 'botpaste', 'bottompaste', 'psb', 'B.Paste', ],
regex='',
content=[]
),
Hint(layer='outline',
ext=['gko', 'outline', ],
name=['BDR', 'border', 'out', 'outline', 'Edge.Cuts', ],
regex='',
content=[]
),
Hint(layer='ipc_netlist',
ext=['ipc'],
name=[],
regex='',
content=[]
),
Hint(layer='drawing',
ext=['fab'],
name=['assembly drawing', 'assembly', 'fabrication',
'fab drawing', 'fab'],
regex='',
content=[]
),
]
def layer_signatures(layer_class):
for hint in hints:
if hint.layer == layer_class:
return hint.ext + hint.name
return []
def load_layer(filename):
return PCBLayer.from_cam(common.read(filename))
def load_layer_data(data, filename=None):
return PCBLayer.from_cam(common.loads(data, filename))
def guess_layer_class(filename):
try:
layer = guess_layer_class_by_content(filename)
if layer:
return layer
except:
pass
try:
directory, filename = os.path.split(filename)
name, ext = os.path.splitext(filename.lower())
for hint in hints:
if hint.regex:
if re.findall(hint.regex, filename, re.IGNORECASE):
return hint.layer
patterns = [r'^(\w*[.-])*{}([.-]\w*)?$'.format(x) for x in hint.name]
if ext[1:] in hint.ext or any(re.findall(p, name, re.IGNORECASE) for p in patterns):
return hint.layer
except:
pass
return 'unknown'
def guess_layer_class_by_content(filename):
try:
file = open(filename, 'r')
for line in file:
for hint in hints:
if len(hint.content) > 0:
patterns = [r'^(.*){}(.*)$'.format(x) for x in hint.content]
if any(re.findall(p, line, re.IGNORECASE) for p in patterns):
return hint.layer
except:
pass
return False
def sort_layers(layers, from_top=True):
layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top',
'internal', 'bottom', 'bottommask', 'bottomsilk',
'bottompaste']
append_after = ['drill', 'drawing']
output = []
drill_layers = [layer for layer in layers if layer.layer_class == 'drill']
internal_layers = list(sorted([layer for layer in layers
if layer.layer_class == 'internal']))
for layer_class in layer_order:
if layer_class == 'internal':
output += internal_layers
elif layer_class == 'drill':
output += drill_layers
else:
for layer in layers:
if layer.layer_class == layer_class:
output.append(layer)
if not from_top:
output = list(reversed(output))
for layer_class in append_after:
for layer in layers:
if layer.layer_class == layer_class:
output.append(layer)
return output
class PCBLayer(object):
""" Base class for PCB Layers
Parameters
----------
source : CAMFile
CAMFile representing the layer
Attributes
----------
filename : string
Source Filename
"""
@classmethod
def from_cam(cls, camfile):
filename = camfile.filename
layer_class = guess_layer_class(filename)
if isinstance(camfile, ExcellonFile) or (layer_class == 'drill'):
return DrillLayer.from_cam(camfile)
elif layer_class == 'internal':
return InternalLayer.from_cam(camfile)
if isinstance(camfile, IPCNetlist):
layer_class = 'ipc_netlist'
return cls(filename, layer_class, camfile)
def __init__(self, filename=None, layer_class=None, cam_source=None, **kwargs):
super(PCBLayer, self).__init__(**kwargs)
self.filename = filename
self.layer_class = layer_class
self.cam_source = cam_source
self.surface = None
self.primitives = cam_source.primitives if cam_source is not None else []
@property
def bounds(self):
if self.cam_source is not None:
return self.cam_source.bounds
else:
return None
def __repr__(self):
return '<PCBLayer: {}>'.format(self.layer_class)
class DrillLayer(PCBLayer):
@classmethod
def from_cam(cls, camfile):
return cls(camfile.filename, camfile)
def __init__(self, filename=None, cam_source=None, layers=None, **kwargs):
super(DrillLayer, self).__init__(filename, 'drill', cam_source, **kwargs)
self.layers = layers if layers is not None else ['top', 'bottom']
class InternalLayer(PCBLayer):
@classmethod
def from_cam(cls, camfile):
filename = camfile.filename
try:
order = int(re.search(r'\d+', filename).group())
except AttributeError:
order = 0
return cls(filename, camfile, order)
def __init__(self, filename=None, cam_source=None, order=0, **kwargs):
super(InternalLayer, self).__init__(filename, 'internal', cam_source, **kwargs)
self.order = order
def __eq__(self, other):
if not hasattr(other, 'order'):
raise TypeError()
return (self.order == other.order)
def __ne__(self, other):
if not hasattr(other, 'order'):
raise TypeError()
return (self.order != other.order)
def __gt__(self, other):
if not hasattr(other, 'order'):
raise TypeError()
return (self.order > other.order)
def __lt__(self, other):
if not hasattr(other, 'order'):
raise TypeError()
return (self.order < other.order)
def __ge__(self, other):
if not hasattr(other, 'order'):
raise TypeError()
return (self.order >= other.order)
def __le__(self, other):
if not hasattr(other, 'order'):
raise TypeError()
return (self.order <= other.order)

Wyświetl plik

@ -0,0 +1,25 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2015 Garret Fick <garret@ficksworkshop.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.
"""
Allegro File module
====================
**Excellon file classes**
Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
"""

Wyświetl plik

@ -0,0 +1,126 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2015 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 Operations
===================
**Transformations and other operations performed on Gerber and Excellon files**
"""
import copy
def to_inch(cam_file):
""" Convert Gerber or Excellon file units to imperial
Parameters
----------
cam_file : :class:`gerber.cam.CamFile` subclass
Gerber or Excellon file to convert
Returns
-------
cam_file : :class:`gerber.cam.CamFile` subclass
A deep copy of the source file with units converted to imperial.
"""
cam_file = copy.deepcopy(cam_file)
cam_file.to_inch()
return cam_file
def to_metric(cam_file):
""" Convert Gerber or Excellon file units to metric
Parameters
----------
cam_file : :class:`gerber.cam.CamFile` subclass
Gerber or Excellon file to convert
Returns
-------
cam_file : :class:`gerber.cam.CamFile` subclass
A deep copy of the source file with units converted to metric.
"""
cam_file = copy.deepcopy(cam_file)
cam_file.to_metric()
return cam_file
def offset(cam_file, x_offset, y_offset):
""" Offset a Cam file by a specified amount in the X and Y directions.
Parameters
----------
cam_file : :class:`gerber.cam.CamFile` subclass
Gerber or Excellon file to offset
x_offset : float
Amount to offset the file in the X direction
y_offset : float
Amount to offset the file in the Y direction
Returns
-------
cam_file : :class:`gerber.cam.CamFile` subclass
An offset deep copy of the source file.
"""
cam_file = copy.deepcopy(cam_file)
cam_file.offset(x_offset, y_offset)
return cam_file
def scale(cam_file, x_scale, y_scale):
""" Scale a Cam file by a specified amount in the X and Y directions.
Parameters
----------
cam_file : :class:`gerber.cam.CamFile` subclass
Gerber or Excellon file to scale
x_scale : float
X-axis scale factor
y_scale : float
Y-axis scale factor
Returns
-------
cam_file : :class:`gerber.cam.CamFile` subclass
An scaled deep copy of the source file.
"""
# TODO
pass
def rotate(cam_file, angle):
""" Rotate a Cam file a specified amount about the origin.
Parameters
----------
cam_file : :class:`gerber.cam.CamFile` subclass
Gerber or Excellon file to rotate
angle : float
Angle to rotate the file in degrees.
Returns
-------
cam_file : :class:`gerber.cam.CamFile` subclass
An rotated deep copy of the source file.
"""
# TODO
pass

124
gerber/pcb.py 100644
Wyświetl plik

@ -0,0 +1,124 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2015 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 os
from .exceptions import ParseError
from .layers import PCBLayer, sort_layers, layer_signatures
from .common import read as gerber_read
from .utils import listdir
class PCB(object):
@classmethod
def from_directory(cls, directory, board_name=None, verbose=False):
layers = []
names = set()
# Validate
directory = os.path.abspath(directory)
if not os.path.isdir(directory):
raise TypeError('{} is not a directory.'.format(directory))
# Load gerber files
for filename in listdir(directory, True, True):
try:
camfile = gerber_read(os.path.join(directory, filename))
layer = PCBLayer.from_cam(camfile)
layers.append(layer)
name = os.path.splitext(filename)[0]
if len(os.path.splitext(filename)) > 1:
_name, ext = os.path.splitext(name)
if ext[1:] in layer_signatures(layer.layer_class):
name = _name
if layer.layer_class == 'drill' and 'drill' in ext:
name = _name
names.add(name)
if verbose:
print('[PCB]: Added {} layer <{}>'.format(layer.layer_class,
filename))
except ParseError:
if verbose:
print('[PCB]: Skipping file {}'.format(filename))
except IOError:
if verbose:
print('[PCB]: Skipping file {}'.format(filename))
# Try to guess board name
if board_name is None:
if len(names) == 1:
board_name = names.pop()
else:
board_name = os.path.basename(directory)
# Return PCB
return cls(layers, board_name)
def __init__(self, layers, name=None):
self.layers = sort_layers(layers)
self.name = name
def __len__(self):
return len(self.layers)
@property
def top_layers(self):
board_layers = [l for l in reversed(self.layers) if l.layer_class in
('topsilk', 'topmask', 'top')]
drill_layers = [l for l in self.drill_layers if 'top' in l.layers]
# Drill layer goes under soldermask for proper rendering of tented vias
return [board_layers[0]] + drill_layers + board_layers[1:]
@property
def bottom_layers(self):
board_layers = [l for l in self.layers if l.layer_class in
('bottomsilk', 'bottommask', 'bottom')]
drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers]
# Drill layer goes under soldermask for proper rendering of tented vias
return [board_layers[0]] + drill_layers + board_layers[1:]
@property
def drill_layers(self):
return [l for l in self.layers if l.layer_class == 'drill']
@property
def copper_layers(self):
return list(reversed([layer for layer in self.layers if
layer.layer_class in
('top', 'bottom', 'internal')]))
@property
def outline_layer(self):
for layer in self.layers:
if layer.layer_class == 'outline':
return layer
@property
def layer_count(self):
""" Number of *COPPER* layers
"""
return len([l for l in self.layers if l.layer_class in
('top', 'bottom', 'internal')])
@property
def board_bounds(self):
for layer in self.layers:
if layer.layer_class == 'outline':
return layer.bounds
for layer in self.layers:
if layer.layer_class == 'top':
return layer.bounds

1697
gerber/primitives.py 100644

Plik diff jest za duży Load Diff

Wyświetl plik

@ -0,0 +1,31 @@
#! /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 .render import RenderSettings
from .cairo_backend import GerberCairoContext
available_renderers = {
'cairo': GerberCairoContext,
}

Wyświetl plik

@ -0,0 +1,616 @@
#!/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.
try:
import cairo
except ImportError:
import cairocffi as cairo
from operator import mul
import tempfile
import copy
import os
from .render import GerberContext, RenderSettings
from .theme import THEMES
from ..primitives import *
from ..utils import rotate_point
from io import BytesIO
class GerberCairoContext(GerberContext):
def __init__(self, scale=300):
super(GerberCairoContext, self).__init__()
self.scale = (scale, scale)
self.surface = None
self.surface_buffer = None
self.ctx = None
self.active_layer = None
self.active_matrix = None
self.output_ctx = None
self.has_bg = False
self.origin_in_inch = None
self.size_in_inch = None
self._xform_matrix = None
self._render_count = 0
@property
def origin_in_pixels(self):
return (self.scale_point(self.origin_in_inch)
if self.origin_in_inch is not None else (0.0, 0.0))
@property
def size_in_pixels(self):
return (self.scale_point(self.size_in_inch)
if self.size_in_inch is not None else (0.0, 0.0))
def set_bounds(self, bounds, new_surface=False):
origin_in_inch = (bounds[0][0], bounds[1][0])
size_in_inch = (abs(bounds[0][1] - bounds[0][0]),
abs(bounds[1][1] - bounds[1][0]))
size_in_pixels = self.scale_point(size_in_inch)
self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch
self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch
self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0,
x0=-self.origin_in_pixels[0],
y0=self.size_in_pixels[1])
if (self.surface is None) or new_surface:
self.surface_buffer = tempfile.NamedTemporaryFile()
self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
self.output_ctx = cairo.Context(self.surface)
def render_layer(self, layer, filename=None, settings=None, bgsettings=None,
verbose=False, bounds=None):
if settings is None:
settings = THEMES['default'].get(layer.layer_class, RenderSettings())
if bgsettings is None:
bgsettings = THEMES['default'].get('background', RenderSettings())
if self._render_count == 0:
if verbose:
print('[Render]: Rendering Background.')
self.clear()
if bounds is not None:
self.set_bounds(bounds)
else:
self.set_bounds(layer.bounds)
self.paint_background(bgsettings)
if verbose:
print('[Render]: Rendering {} Layer.'.format(layer.layer_class))
self._render_count += 1
self._render_layer(layer, settings)
if filename is not None:
self.dump(filename, verbose)
def render_layers(self, layers, filename, theme=THEMES['default'],
verbose=False, max_width=800, max_height=600):
""" Render a set of layers
"""
# Calculate scale parameter
x_range = [10000, -10000]
y_range = [10000, -10000]
for layer in layers:
bounds = layer.bounds
if bounds is not None:
layer_x, layer_y = bounds
x_range[0] = min(x_range[0], layer_x[0])
x_range[1] = max(x_range[1], layer_x[1])
y_range[0] = min(y_range[0], layer_y[0])
y_range[1] = max(y_range[1], layer_y[1])
width = x_range[1] - x_range[0]
height = y_range[1] - y_range[0]
scale = math.floor(min(float(max_width)/width, float(max_height)/height))
self.scale = (scale, scale)
self.clear()
# Render layers
bgsettings = theme['background']
for layer in layers:
settings = theme.get(layer.layer_class, RenderSettings())
self.render_layer(layer, settings=settings, bgsettings=bgsettings,
verbose=verbose)
self.dump(filename, verbose)
def dump(self, filename=None, verbose=False):
""" Save image as `filename`
"""
try:
is_svg = os.path.splitext(filename.lower())[1] == '.svg'
except:
is_svg = False
if verbose:
print('[Render]: Writing image to {}'.format(filename))
if is_svg:
self.surface.finish()
self.surface_buffer.flush()
with open(filename, "wb") as f:
self.surface_buffer.seek(0)
f.write(self.surface_buffer.read())
f.flush()
else:
return self.surface.write_to_png(filename)
def dump_str(self):
""" Return a byte-string containing the rendered image.
"""
fobj = BytesIO()
self.surface.write_to_png(fobj)
return fobj.getvalue()
def dump_svg_str(self):
""" Return a string containg the rendered SVG.
"""
self.surface.finish()
self.surface_buffer.flush()
return self.surface_buffer.read()
def clear(self):
self.surface = None
self.output_ctx = None
self.has_bg = False
self.origin_in_inch = None
self.size_in_inch = None
self._xform_matrix = None
self._render_count = 0
self.surface_buffer = None
def _new_mask(self):
class Mask:
def __enter__(msk):
size_in_pixels = self.size_in_pixels
msk.surface = cairo.SVGSurface(None, size_in_pixels[0],
size_in_pixels[1])
msk.ctx = cairo.Context(msk.surface)
msk.ctx.translate(-self.origin_in_pixels[0], -self.origin_in_pixels[1])
return msk
def __exit__(msk, exc_type, exc_val, traceback):
if hasattr(msk.surface, 'finish'):
msk.surface.finish()
return Mask()
def _render_layer(self, layer, settings):
self.invert = settings.invert
# Get a new clean layer to render on
self.new_render_layer(mirror=settings.mirror)
for prim in layer.primitives:
self.render(prim)
# Add layer to image
self.flatten(settings.color, settings.alpha)
def _render_line(self, line, color):
start = self.scale_point(line.start)
end = self.scale_point(line.end)
self.ctx.set_operator(cairo.OPERATOR_OVER
if (not self.invert)
and line.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
with self._clip_primitive(line):
with self._new_mask() as mask:
if isinstance(line.aperture, Circle):
width = line.aperture.diameter
mask.ctx.set_line_width(width * self.scale[0])
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
mask.ctx.move_to(*start)
mask.ctx.line_to(*end)
mask.ctx.stroke()
elif hasattr(line, 'vertices') and line.vertices is not None:
points = [self.scale_point(x) for x in line.vertices]
mask.ctx.set_line_width(0)
mask.ctx.move_to(*points[-1])
for point in points:
mask.ctx.line_to(*point)
mask.ctx.fill()
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_arc(self, arc, color):
center = self.scale_point(arc.center)
start = self.scale_point(arc.start)
end = self.scale_point(arc.end)
radius = self.scale[0] * arc.radius
two_pi = 2 * math.pi
angle1 = (arc.start_angle + two_pi) % two_pi
angle2 = (arc.end_angle + two_pi) % two_pi
if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant':
# Make the angles slightly different otherwise Cario will draw nothing
angle2 -= 0.000000001
if isinstance(arc.aperture, Circle):
width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001
else:
width = max(arc.aperture.width, arc.aperture.height, 0.001)
self.ctx.set_operator(cairo.OPERATOR_OVER
if (not self.invert)
and arc.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
with self._clip_primitive(arc):
with self._new_mask() as mask:
mask.ctx.set_line_width(width * self.scale[0])
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND if isinstance(arc.aperture, Circle) else cairo.LINE_CAP_SQUARE)
mask.ctx.move_to(*start) # You actually have to do this...
if arc.direction == 'counterclockwise':
mask.ctx.arc(center[0], center[1], radius, angle1, angle2)
else:
mask.ctx.arc_negative(center[0], center[1], radius,
angle1, angle2)
mask.ctx.move_to(*end) # ...lame
mask.ctx.stroke()
#if isinstance(arc.aperture, Rectangle):
# print("Flash Rectangle Ends")
# print(arc.aperture.rotation * 180/math.pi)
# rect = arc.aperture
# width = self.scale[0] * rect.width
# height = self.scale[1] * rect.height
# for point, angle in zip((start, end), (angle1, angle2)):
# print("{} w {} h{}".format(point, rect.width, rect.height))
# mask.ctx.rectangle(point[0] - width/2.0,
# point[1] - height/2.0, width, height)
# mask.ctx.fill()
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_region(self, region, color):
self.ctx.set_operator(cairo.OPERATOR_OVER
if (not self.invert) and region.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
with self._clip_primitive(region):
with self._new_mask() as mask:
mask.ctx.set_line_width(0)
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
mask.ctx.move_to(*self.scale_point(region.primitives[0].start))
for prim in region.primitives:
if isinstance(prim, Line):
mask.ctx.line_to(*self.scale_point(prim.end))
else:
center = self.scale_point(prim.center)
radius = self.scale[0] * prim.radius
angle1 = prim.start_angle
angle2 = prim.end_angle
if prim.direction == 'counterclockwise':
mask.ctx.arc(center[0], center[1], radius,
angle1, angle2)
else:
mask.ctx.arc_negative(center[0], center[1], radius,
angle1, angle2)
mask.ctx.fill()
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_circle(self, circle, color):
center = self.scale_point(circle.position)
self.ctx.set_operator(cairo.OPERATOR_OVER
if (not self.invert)
and circle.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
with self._clip_primitive(circle):
with self._new_mask() as mask:
mask.ctx.set_line_width(0)
mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi))
mask.ctx.fill()
if hasattr(circle, 'hole_diameter') and circle.hole_diameter is not None and circle.hole_diameter > 0:
mask.ctx.set_operator(cairo.OPERATOR_CLEAR)
mask.ctx.arc(center[0], center[1], circle.hole_radius * self.scale[0], 0, 2 * math.pi)
mask.ctx.fill()
if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height')
and circle.hole_width is not None and circle.hole_height is not None
and circle.hole_width > 0 and circle.hole_height > 0):
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
if circle.level_polarity == 'dark'
and (not self.invert)
else cairo.OPERATOR_OVER)
width, height = self.scale_point((circle.hole_width, circle.hole_height))
lower_left = rotate_point(
(center[0] - width / 2.0, center[1] - height / 2.0),
circle.rotation, center)
lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0),
circle.rotation, center)
upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0),
circle.rotation, center)
upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0),
circle.rotation, center)
points = (lower_left, lower_right, upper_right, upper_left)
mask.ctx.move_to(*points[-1])
for point in points:
mask.ctx.line_to(*point)
mask.ctx.fill()
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_rectangle(self, rectangle, color):
lower_left = self.scale_point(rectangle.lower_left)
width, height = tuple([abs(coord) for coord in
self.scale_point((rectangle.width,
rectangle.height))])
self.ctx.set_operator(cairo.OPERATOR_OVER
if (not self.invert)
and rectangle.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
with self._clip_primitive(rectangle):
with self._new_mask() as mask:
mask.ctx.set_line_width(0)
mask.ctx.rectangle(lower_left[0], lower_left[1], width, height)
mask.ctx.fill()
center = self.scale_point(rectangle.position)
if rectangle.hole_diameter > 0:
# Render the center clear
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
if rectangle.level_polarity == 'dark'
and (not self.invert)
else cairo.OPERATOR_OVER)
mask.ctx.arc(center[0], center[1], rectangle.hole_radius * self.scale[0], 0, 2 * math.pi)
mask.ctx.fill()
if rectangle.hole_width > 0 and rectangle.hole_height > 0:
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
if rectangle.level_polarity == 'dark'
and (not self.invert)
else cairo.OPERATOR_OVER)
width, height = self.scale_point((rectangle.hole_width, rectangle.hole_height))
lower_left = rotate_point((center[0] - width/2.0, center[1] - height/2.0), rectangle.rotation, center)
lower_right = rotate_point((center[0] + width/2.0, center[1] - height/2.0), rectangle.rotation, center)
upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), rectangle.rotation, center)
upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), rectangle.rotation, center)
points = (lower_left, lower_right, upper_right, upper_left)
mask.ctx.move_to(*points[-1])
for point in points:
mask.ctx.line_to(*point)
mask.ctx.fill()
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_obround(self, obround, color):
self.ctx.set_operator(cairo.OPERATOR_OVER
if (not self.invert)
and obround.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
with self._clip_primitive(obround):
with self._new_mask() as mask:
mask.ctx.set_line_width(0)
# Render circles
for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']):
center = self.scale_point(circle.position)
mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi))
mask.ctx.fill()
# Render Rectangle
rectangle = obround.subshapes['rectangle']
lower_left = self.scale_point(rectangle.lower_left)
width, height = tuple([abs(coord) for coord in
self.scale_point((rectangle.width,
rectangle.height))])
mask.ctx.rectangle(lower_left[0], lower_left[1], width, height)
mask.ctx.fill()
center = self.scale_point(obround.position)
if obround.hole_diameter > 0:
# Render the center clear
mask.ctx.set_operator(cairo.OPERATOR_CLEAR)
mask.ctx.arc(center[0], center[1], obround.hole_radius * self.scale[0], 0, 2 * math.pi)
mask.ctx.fill()
if obround.hole_width > 0 and obround.hole_height > 0:
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
if rectangle.level_polarity == 'dark'
and (not self.invert)
else cairo.OPERATOR_OVER)
width, height =self.scale_point((obround.hole_width, obround.hole_height))
lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0),
obround.rotation, center)
lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0),
obround.rotation, center)
upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0),
obround.rotation, center)
upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0),
obround.rotation, center)
points = (lower_left, lower_right, upper_right, upper_left)
mask.ctx.move_to(*points[-1])
for point in points:
mask.ctx.line_to(*point)
mask.ctx.fill()
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_polygon(self, polygon, color):
self.ctx.set_operator(cairo.OPERATOR_OVER
if (not self.invert)
and polygon.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
with self._clip_primitive(polygon):
with self._new_mask() as mask:
vertices = polygon.vertices
mask.ctx.set_line_width(0)
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
# Start from before the end so it is easy to iterate and make sure
# it is closed
mask.ctx.move_to(*self.scale_point(vertices[-1]))
for v in vertices:
mask.ctx.line_to(*self.scale_point(v))
mask.ctx.fill()
center = self.scale_point(polygon.position)
if polygon.hole_radius > 0:
# Render the center clear
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
if polygon.level_polarity == 'dark'
and (not self.invert)
else cairo.OPERATOR_OVER)
mask.ctx.set_line_width(0)
mask.ctx.arc(center[0],
center[1],
polygon.hole_radius * self.scale[0], 0, 2 * math.pi)
mask.ctx.fill()
if polygon.hole_width > 0 and polygon.hole_height > 0:
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
if polygon.level_polarity == 'dark'
and (not self.invert)
else cairo.OPERATOR_OVER)
width, height = self.scale_point((polygon.hole_width, polygon.hole_height))
lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0),
polygon.rotation, center)
lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0),
polygon.rotation, center)
upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0),
polygon.rotation, center)
upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0),
polygon.rotation, center)
points = (lower_left, lower_right, upper_right, upper_left)
mask.ctx.move_to(*points[-1])
for point in points:
mask.ctx.line_to(*point)
mask.ctx.fill()
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_drill(self, circle, color=None):
color = color if color is not None else self.drill_color
self._render_circle(circle, color)
def _render_slot(self, slot, color):
start = map(mul, slot.start, self.scale)
end = map(mul, slot.end, self.scale)
width = slot.diameter
self.ctx.set_operator(cairo.OPERATOR_OVER
if slot.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
with self._clip_primitive(slot):
with self._new_mask() as mask:
mask.ctx.set_line_width(width * self.scale[0])
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
mask.ctx.move_to(*start)
mask.ctx.line_to(*end)
mask.ctx.stroke()
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_amgroup(self, amgroup, color):
for primitive in amgroup.primitives:
self.render(primitive)
def _render_test_record(self, primitive, color):
position = [pos + origin for pos, origin in
zip(primitive.position, self.origin_in_inch)]
self.ctx.select_font_face(
'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
self.ctx.set_font_size(13)
self._render_circle(Circle(position, 0.015), color)
self.ctx.set_operator(cairo.OPERATOR_OVER
if primitive.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position])
self.ctx.scale(1, -1)
self.ctx.show_text(primitive.net_name)
self.ctx.scale(1, -1)
def new_render_layer(self, color=None, mirror=False):
size_in_pixels = self.scale_point(self.size_in_inch)
matrix = copy.copy(self._xform_matrix)
layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1])
ctx = cairo.Context(layer)
if self.invert:
ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
ctx.set_operator(cairo.OPERATOR_OVER)
ctx.paint()
if mirror:
matrix.xx = -1.0
matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0]
self.ctx = ctx
self.ctx.set_matrix(matrix)
self.active_layer = layer
self.active_matrix = matrix
def flatten(self, color=None, alpha=None):
color = color if color is not None else self.color
alpha = alpha if alpha is not None else self.alpha
self.output_ctx.set_source_rgba(color[0], color[1], color[2], alpha)
self.output_ctx.mask_surface(self.active_layer)
self.ctx = None
self.active_layer = None
self.active_matrix = None
def paint_background(self, settings=None):
color = settings.color if settings is not None else self.background_color
alpha = settings.alpha if settings is not None else 1.0
if not self.has_bg:
self.has_bg = True
self.output_ctx.set_source_rgba(color[0], color[1], color[2], alpha)
self.output_ctx.paint()
def _clip_primitive(self, primitive):
""" Clip rendering context to pixel-aligned bounding box
Calculates pixel- and axis- aligned bounding box, and clips current
context to that region. Improves rendering speed significantly. This
returns a context manager, use as follows:
with self._clip_primitive(some_primitive):
do_rendering_stuff()
do_more_rendering stuff(with, arguments)
The context manager will reset the context's clipping region when it
goes out of scope.
"""
class Clip:
def __init__(clp, primitive):
x_range, y_range = primitive.bounding_box
xmin, xmax = x_range
ymin, ymax = y_range
# Round bounds to the nearest pixel outside of the primitive
clp.xmin = math.floor(self.scale[0] * xmin)
clp.xmax = math.ceil(self.scale[0] * xmax)
# We need to offset Y to take care of the difference in y-pos
# caused by flipping the axis.
clp.ymin = math.floor(
(self.scale[1] * ymin) - math.ceil(self.origin_in_pixels[1]))
clp.ymax = math.floor(
(self.scale[1] * ymax) - math.floor(self.origin_in_pixels[1]))
# Calculate width and height, rounded to the nearest pixel
clp.width = abs(clp.xmax - clp.xmin)
clp.height = abs(clp.ymax - clp.ymin)
def __enter__(clp):
# Clip current context to primitive's bounding box
self.ctx.rectangle(clp.xmin, clp.ymin, clp.width, clp.height)
self.ctx.clip()
def __exit__(clp, exc_type, exc_val, traceback):
# Reset context clip region
self.ctx.reset_clip()
return Clip(primitive)
def scale_point(self, point):
return tuple([coord * scale for coord, scale in zip(point, self.scale)])

Wyświetl plik

@ -0,0 +1,188 @@
from .render import GerberContext
from ..excellon import DrillSlot
from ..excellon_statements import *
class ExcellonContext(GerberContext):
MODE_DRILL = 1
MODE_SLOT =2
def __init__(self, settings):
GerberContext.__init__(self)
# Statements that we write
self.comments = []
self.header = []
self.tool_def = []
self.body_start = [RewindStopStmt()]
self.body = []
self.start = [HeaderBeginStmt()]
# Current tool and position
self.handled_tools = set()
self.cur_tool = None
self.drill_mode = ExcellonContext.MODE_DRILL
self.drill_down = False
self._pos = (None, None)
self.settings = settings
self._start_header()
self._start_comments()
def _start_header(self):
"""Create the header from the settings"""
self.header.append(UnitStmt.from_settings(self.settings))
if self.settings.notation == 'incremental':
raise NotImplementedError('Incremental mode is not implemented')
else:
self.body.append(AbsoluteModeStmt())
def _start_comments(self):
# Write the digits used - this isn't valid Excellon statement, so we write as a comment
self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1])))
def _get_end(self):
"""How we end depends on our mode"""
end = []
if self.drill_down:
end.append(RetractWithClampingStmt())
end.append(RetractWithoutClampingStmt())
end.append(EndOfProgramStmt())
return end
@property
def statements(self):
return self.start + self.comments + self.header + self.body_start + self.body + self._get_end()
def set_bounds(self, bounds, *args, **kwargs):
pass
def paint_background(self):
pass
def _render_line(self, line, color):
raise ValueError('Invalid Excellon object')
def _render_arc(self, arc, color):
raise ValueError('Invalid Excellon object')
def _render_region(self, region, color):
raise ValueError('Invalid Excellon object')
def _render_level_polarity(self, region):
raise ValueError('Invalid Excellon object')
def _render_circle(self, circle, color):
raise ValueError('Invalid Excellon object')
def _render_rectangle(self, rectangle, color):
raise ValueError('Invalid Excellon object')
def _render_obround(self, obround, color):
raise ValueError('Invalid Excellon object')
def _render_polygon(self, polygon, color):
raise ValueError('Invalid Excellon object')
def _simplify_point(self, point):
return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None)
def _render_drill(self, drill, color):
if self.drill_mode != ExcellonContext.MODE_DRILL:
self._start_drill_mode()
tool = drill.hit.tool
if not tool in self.handled_tools:
self.handled_tools.add(tool)
self.header.append(ExcellonTool.from_tool(tool))
if tool != self.cur_tool:
self.body.append(ToolSelectionStmt(tool.number))
self.cur_tool = tool
point = self._simplify_point(drill.position)
self._pos = drill.position
self.body.append(CoordinateStmt.from_point(point))
def _start_drill_mode(self):
"""
If we are not in drill mode, then end the ROUT so we can do basic drilling
"""
if self.drill_mode == ExcellonContext.MODE_SLOT:
# Make sure we are retracted before changing modes
last_cmd = self.body[-1]
if self.drill_down:
self.body.append(RetractWithClampingStmt())
self.body.append(RetractWithoutClampingStmt())
self.drill_down = False
# Switch to drill mode
self.body.append(DrillModeStmt())
self.drill_mode = ExcellonContext.MODE_DRILL
else:
raise ValueError('Should be in slot mode')
def _render_slot(self, slot, color):
# Set the tool first, before we might go into drill mode
tool = slot.hit.tool
if not tool in self.handled_tools:
self.handled_tools.add(tool)
self.header.append(ExcellonTool.from_tool(tool))
if tool != self.cur_tool:
self.body.append(ToolSelectionStmt(tool.number))
self.cur_tool = tool
# Two types of drilling - normal drill and slots
if slot.hit.slot_type == DrillSlot.TYPE_ROUT:
# For ROUT, setting the mode is part of the actual command.
# Are we in the right position?
if slot.start != self._pos:
if self.drill_down:
# We need to move into the right position, so retract
self.body.append(RetractWithClampingStmt())
self.drill_down = False
# Move to the right spot
point = self._simplify_point(slot.start)
self._pos = slot.start
self.body.append(CoordinateStmt.from_point(point, mode="ROUT"))
# Now we are in the right spot, so drill down
if not self.drill_down:
self.body.append(ZAxisRoutPositionStmt())
self.drill_down = True
# Do a linear move from our current position to the end position
point = self._simplify_point(slot.end)
self._pos = slot.end
self.body.append(CoordinateStmt.from_point(point, mode="LINEAR"))
self.drill_mode = ExcellonContext.MODE_SLOT
else:
# This is a G85 slot, so do this in normally drilling mode
if self.drill_mode != ExcellonContext.MODE_DRILL:
self._start_drill_mode()
# Slots don't use simplified points
self._pos = slot.end
self.body.append(SlotStmt.from_points(slot.start, slot.end))
def _render_inverted_layer(self):
pass

Wyświetl plik

@ -0,0 +1,246 @@
#! /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 ..primitives import *
from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt,
CoordStmt, ApertureStmt, RegionModeStmt,
QuadrantModeStmt,)
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. 'inch' or 'metric'
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._background_color = (0.0, 0.0, 0.0)
self._drill_color = (0.0, 0.0, 0.0)
self._alpha = 1.0
self._invert = False
self.ctx = None
@property
def units(self):
return self._units
@units.setter
def units(self, units):
if units not in ('inch', 'metric'):
raise ValueError('Units may be "inch" or "metric"')
self._units = units
@property
def color(self):
return self._color
@color.setter
def color(self, color):
if len(color) != 3:
raise TypeError('Color must be a tuple of R, G, and B values')
for c in color:
if c < 0 or c > 1:
raise ValueError('Channel values must be between 0.0 and 1.0')
self._color = color
@property
def drill_color(self):
return self._drill_color
@drill_color.setter
def drill_color(self, color):
if len(color) != 3:
raise TypeError('Drill color must be a tuple of R, G, and B values')
for c in color:
if c < 0 or c > 1:
raise ValueError('Channel values must be between 0.0 and 1.0')
self._drill_color = color
@property
def background_color(self):
return self._background_color
@background_color.setter
def background_color(self, color):
if len(color) != 3:
raise TypeError('Background color must be a tuple of R, G, and B values')
for c in color:
if c < 0 or c > 1:
raise ValueError('Channel values must be between 0.0 and 1.0')
self._background_color = color
@property
def alpha(self):
return self._alpha
@alpha.setter
def alpha(self, alpha):
if alpha < 0 or alpha > 1:
raise ValueError('Alpha must be between 0.0 and 1.0')
self._alpha = alpha
@property
def invert(self):
return self._invert
@invert.setter
def invert(self, invert):
self._invert = invert
def render(self, primitive):
if not primitive:
return
self.pre_render_primitive(primitive)
color = self.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(primitive, color)
elif isinstance(primitive, Drill):
self._render_drill(primitive, self.color)
elif isinstance(primitive, Slot):
self._render_slot(primitive, self.color)
elif isinstance(primitive, AMGroup):
self._render_amgroup(primitive, color)
elif isinstance(primitive, Outline):
self._render_region(primitive, color)
elif isinstance(primitive, TestRecord):
self._render_test_record(primitive, color)
self.post_render_primitive(primitive)
def set_bounds(self, bounds, *args, **kwargs):
"""Called by the renderer to set the extents of the file to render.
Parameters
----------
bounds: Tuple[Tuple[float, float], Tuple[float, float]]
( (x_min, x_max), (y_min, y_max)
"""
pass
def paint_background(self):
pass
def new_render_layer(self):
pass
def flatten(self):
pass
def pre_render_primitive(self, primitive):
"""
Called before rendering a primitive. Use the callback to perform some action before rendering
a primitive, for example adding a comment.
"""
return
def post_render_primitive(self, primitive):
"""
Called after rendering a primitive. Use the callback to perform some action after rendering
a primitive
"""
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
def _render_slot(self, primitive, color):
pass
def _render_amgroup(self, primitive, color):
pass
def _render_test_record(self, primitive, color):
pass
class RenderSettings(object):
def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False,
mirror=False):
self.color = color
self.alpha = alpha
self.invert = invert
self.mirror = mirror

Wyświetl plik

@ -0,0 +1,510 @@
"""Renders an in-memory Gerber file to statements which can be written to a string
"""
from copy import deepcopy
try:
from cStringIO import StringIO
except(ImportError):
from io import StringIO
from .render import GerberContext
from ..am_statements import *
from ..gerber_statements import *
from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon, Rectangle
class AMGroupContext(object):
'''A special renderer to generate aperature macros from an AMGroup'''
def __init__(self):
self.statements = []
def render(self, amgroup, name):
if amgroup.stmt:
# We know the statement it was generated from, so use that to create the AMParamStmt
# It will give a much better result
stmt = deepcopy(amgroup.stmt)
stmt.name = name
return stmt
else:
# Clone ourselves, then offset by the psotion so that
# our render doesn't have to consider offset. Just makes things simpler
nooffset_group = deepcopy(amgroup)
nooffset_group.position = (0, 0)
# Now draw the shapes
for primitive in nooffset_group.primitives:
if isinstance(primitive, Outline):
self._render_outline(primitive)
elif isinstance(primitive, Circle):
self._render_circle(primitive)
elif isinstance(primitive, Rectangle):
self._render_rectangle(primitive)
elif isinstance(primitive, Line):
self._render_line(primitive)
elif isinstance(primitive, Polygon):
self._render_polygon(primitive)
else:
raise ValueError('amgroup')
statement = AMParamStmt('AM', name, self._statements_to_string())
return statement
def _statements_to_string(self):
macro = ''
for statement in self.statements:
macro += statement.to_gerber()
return macro
def _render_circle(self, circle):
self.statements.append(AMCirclePrimitive.from_primitive(circle))
def _render_rectangle(self, rectangle):
self.statements.append(AMCenterLinePrimitive.from_primitive(rectangle))
def _render_line(self, line):
self.statements.append(AMVectorLinePrimitive.from_primitive(line))
def _render_outline(self, outline):
self.statements.append(AMOutlinePrimitive.from_primitive(outline))
def _render_polygon(self, polygon):
self.statements.append(AMPolygonPrimitive.from_primitive(polygon))
def _render_thermal(self, thermal):
pass
class Rs274xContext(GerberContext):
def __init__(self, settings):
GerberContext.__init__(self)
self.comments = []
self.header = []
self.body = []
self.end = [EofStmt()]
# Current values so we know if we have to execute
# moves, levey changes before anything else
self._level_polarity = None
self._pos = (None, None)
self._func = None
self._quadrant_mode = None
self._dcode = None
# Primarily for testing and comarison to files, should we write
# flashes as a single statement or a move plus flash? Set to true
# to do in a single statement. Normally this can be false
self.condensed_flash = True
# When closing a region, force a D02 staement to close a region.
# This is normally not necessary because regions are closed with a G37
# staement, but this will add an extra statement for doubly close
# the region
self.explicit_region_move_end = False
self._next_dcode = 10
self._rects = {}
self._circles = {}
self._obrounds = {}
self._polygons = {}
self._macros = {}
self._i_none = 0
self._j_none = 0
self.settings = settings
self._start_header(settings)
def _start_header(self, settings):
self.header.append(FSParamStmt.from_settings(settings))
self.header.append(MOParamStmt.from_units(settings.units))
def _simplify_point(self, point):
return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None)
def _simplify_offset(self, point, offset):
if point[0] != offset[0]:
xoffset = point[0] - offset[0]
else:
xoffset = self._i_none
if point[1] != offset[1]:
yoffset = point[1] - offset[1]
else:
yoffset = self._j_none
return (xoffset, yoffset)
@property
def statements(self):
return self.comments + self.header + self.body + self.end
def set_bounds(self, bounds, *args, **kwargs):
pass
def paint_background(self):
pass
def _select_aperture(self, aperture):
# Select the right aperture if not already selected
if aperture:
if isinstance(aperture, Circle):
aper = self._get_circle(aperture.diameter, aperture.hole_diameter, aperture.hole_width, aperture.hole_height)
elif isinstance(aperture, Rectangle):
aper = self._get_rectangle(aperture.width, aperture.height)
elif isinstance(aperture, Obround):
aper = self._get_obround(aperture.width, aperture.height)
elif isinstance(aperture, AMGroup):
aper = self._get_amacro(aperture)
else:
raise NotImplementedError('Line with invalid aperture type')
if aper.d != self._dcode:
self.body.append(ApertureStmt(aper.d))
self._dcode = aper.d
def pre_render_primitive(self, primitive):
if hasattr(primitive, 'comment'):
self.body.append(CommentStmt(primitive.comment))
def _render_line(self, line, color, default_polarity='dark'):
self._select_aperture(line.aperture)
self._render_level_polarity(line, default_polarity)
# Get the right function
if self._func != CoordStmt.FUNC_LINEAR:
func = CoordStmt.FUNC_LINEAR
else:
func = None
self._func = CoordStmt.FUNC_LINEAR
if self._pos != line.start:
self.body.append(CoordStmt.move(func, self._simplify_point(line.start)))
self._pos = line.start
# We already set the function, so the next command doesn't require that
func = None
point = self._simplify_point(line.end)
# In some files, we see a lot of duplicated ponts, so omit those
if point[0] != None or point[1] != None:
self.body.append(CoordStmt.line(func, self._simplify_point(line.end)))
self._pos = line.end
elif func:
self.body.append(CoordStmt.mode(func))
def _render_arc(self, arc, color, default_polarity='dark'):
# Optionally set the quadrant mode if it has changed:
if arc.quadrant_mode != self._quadrant_mode:
if arc.quadrant_mode != 'multi-quadrant':
self.body.append(QuadrantModeStmt.single())
else:
self.body.append(QuadrantModeStmt.multi())
self._quadrant_mode = arc.quadrant_mode
# Select the right aperture if not already selected
self._select_aperture(arc.aperture)
self._render_level_polarity(arc, default_polarity)
# Find the right movement mode. Always set to be sure it is really right
dir = arc.direction
if dir == 'clockwise':
func = CoordStmt.FUNC_ARC_CW
self._func = CoordStmt.FUNC_ARC_CW
elif dir == 'counterclockwise':
func = CoordStmt.FUNC_ARC_CCW
self._func = CoordStmt.FUNC_ARC_CCW
else:
raise ValueError('Invalid circular interpolation mode')
if self._pos != arc.start:
# TODO I'm not sure if this is right
self.body.append(CoordStmt.move(CoordStmt.FUNC_LINEAR, self._simplify_point(arc.start)))
self._pos = arc.start
center = self._simplify_offset(arc.center, arc.start)
end = self._simplify_point(arc.end)
self.body.append(CoordStmt.arc(func, end, center))
self._pos = arc.end
def _render_region(self, region, color):
self._render_level_polarity(region)
self.body.append(RegionModeStmt.on())
for p in region.primitives:
# Make programmatically generated primitives within a region with
# unset level polarity inherit the region's level polarity
if isinstance(p, Line):
self._render_line(p, color, default_polarity=region.level_polarity)
else:
self._render_arc(p, color, default_polarity=region.level_polarity)
if self.explicit_region_move_end:
self.body.append(CoordStmt.move(None, None))
self.body.append(RegionModeStmt.off())
def _render_level_polarity(self, obj, default='dark'):
obj_polarity = obj.level_polarity if obj.level_polarity is not None else default
if obj_polarity != self._level_polarity:
self._level_polarity = obj_polarity
self.body.append(LPParamStmt('LP', obj_polarity))
def _render_flash(self, primitive, aperture):
self._render_level_polarity(primitive)
if aperture.d != self._dcode:
self.body.append(ApertureStmt(aperture.d))
self._dcode = aperture.d
if self.condensed_flash:
self.body.append(CoordStmt.flash(self._simplify_point(primitive.position)))
else:
self.body.append(CoordStmt.move(None, self._simplify_point(primitive.position)))
self.body.append(CoordStmt.flash(None))
self._pos = primitive.position
def _get_circle(self, diameter, hole_diameter=None, hole_width=None,
hole_height=None, dcode = None):
'''Define a circlar aperture'''
key = (diameter, hole_diameter, hole_width, hole_height)
aper = self._circles.get(key, None)
if not aper:
if not dcode:
dcode = self._next_dcode
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
aper = ADParamStmt.circle(dcode, diameter, hole_diameter, hole_width, hole_height)
self._circles[(diameter, hole_diameter, hole_width, hole_height)] = aper
self.header.append(aper)
return aper
def _render_circle(self, circle, color):
aper = self._get_circle(circle.diameter, circle.hole_diameter, circle.hole_width, circle.hole_height)
self._render_flash(circle, aper)
def _get_rectangle(self, width, height, hole_diameter=None, hole_width=None,
hole_height=None, dcode = None):
'''Get a rectanglar aperture. If it isn't defined, create it'''
key = (width, height, hole_diameter, hole_width, hole_height)
aper = self._rects.get(key, None)
if not aper:
if not dcode:
dcode = self._next_dcode
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
aper = ADParamStmt.rect(dcode, width, height, hole_diameter, hole_width, hole_height)
self._rects[(width, height, hole_diameter, hole_width, hole_height)] = aper
self.header.append(aper)
return aper
def _render_rectangle(self, rectangle, color):
aper = self._get_rectangle(rectangle.width, rectangle.height,
rectangle.hole_diameter,
rectangle.hole_width, rectangle.hole_height)
self._render_flash(rectangle, aper)
def _get_obround(self, width, height, hole_diameter=None, hole_width=None,
hole_height=None, dcode = None):
key = (width, height, hole_diameter, hole_width, hole_height)
aper = self._obrounds.get(key, None)
if not aper:
if not dcode:
dcode = self._next_dcode
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
aper = ADParamStmt.obround(dcode, width, height, hole_diameter, hole_width, hole_height)
self._obrounds[key] = aper
self.header.append(aper)
return aper
def _render_obround(self, obround, color):
aper = self._get_obround(obround.width, obround.height,
obround.hole_diameter, obround.hole_width,
obround.hole_height)
self._render_flash(obround, aper)
def _render_polygon(self, polygon, color):
aper = self._get_polygon(polygon.radius, polygon.sides,
polygon.rotation, polygon.hole_diameter,
polygon.hole_width, polygon.hole_height)
self._render_flash(polygon, aper)
def _get_polygon(self, radius, num_vertices, rotation, hole_diameter=None,
hole_width=None, hole_height=None, dcode = None):
key = (radius, num_vertices, rotation, hole_diameter, hole_width, hole_height)
aper = self._polygons.get(key, None)
if not aper:
if not dcode:
dcode = self._next_dcode
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices,
rotation, hole_diameter, hole_width,
hole_height)
self._polygons[key] = aper
self.header.append(aper)
return aper
def _render_drill(self, drill, color):
raise ValueError('Drills are not valid in RS274X files')
def _hash_amacro(self, amgroup):
'''Calculate a very quick hash code for deciding if we should even check AM groups for comparision'''
# We always start with an X because this forms part of the name
# Basically, in some cases, the name might start with a C, R, etc. That can appear
# to conflict with normal aperture definitions. Technically, it shouldn't because normal
# aperture definitions should have a comma, but in some cases the commit is omitted
hash = 'X'
for primitive in amgroup.primitives:
hash += primitive.__class__.__name__[0]
bbox = primitive.bounding_box
hash += str((bbox[0][1] - bbox[0][0]) * 100000)[0:2]
hash += str((bbox[1][1] - bbox[1][0]) * 100000)[0:2]
if hasattr(primitive, 'primitives'):
hash += str(len(primitive.primitives))
if isinstance(primitive, Rectangle):
hash += str(primitive.width * 1000000)[0:2]
hash += str(primitive.height * 1000000)[0:2]
elif isinstance(primitive, Circle):
hash += str(primitive.diameter * 1000000)[0:2]
if len(hash) > 20:
# The hash might actually get quite complex, so stop before
# it gets too long
break
return hash
def _get_amacro(self, amgroup, dcode = None):
# Macros are a little special since we don't have a good way to compare them quickly
# but in most cases, this should work
hash = self._hash_amacro(amgroup)
macro = None
macroinfo = self._macros.get(hash, None)
if macroinfo:
# We have a definition, but check that the groups actually are the same
for macro in macroinfo:
# Macros should have positions, right? But if the macro is selected for non-flashes
# then it won't have a position. This is of course a bad gerber, but they do exist
if amgroup.position:
position = amgroup.position
else:
position = (0, 0)
offset = (position[0] - macro[1].position[0], position[1] - macro[1].position[1])
if amgroup.equivalent(macro[1], offset):
break
macro = None
# Did we find one in the group0
if not macro:
# This is a new macro, so define it
if not dcode:
dcode = self._next_dcode
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
# Create the statements
# TODO
amrenderer = AMGroupContext()
statement = amrenderer.render(amgroup, hash)
self.header.append(statement)
aperdef = ADParamStmt.macro(dcode, hash)
self.header.append(aperdef)
# Store the dcode and the original so we can check if it really is the same
# If it didn't have a postition, set it to 0, 0
if amgroup.position == None:
amgroup.position = (0, 0)
macro = (aperdef, amgroup)
if macroinfo:
macroinfo.append(macro)
else:
self._macros[hash] = [macro]
return macro[0]
def _render_amgroup(self, amgroup, color):
aper = self._get_amacro(amgroup)
self._render_flash(amgroup, aper)
def _render_inverted_layer(self):
pass
def new_render_layer(self):
# TODO Might need to implement this
pass
def flatten(self):
# TODO Might need to implement this
pass
def dump(self):
"""Write the rendered file to a StringIO steam"""
statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements)
stream = StringIO()
for statement in statements:
stream.write(statement + '\n')
return stream

Wyświetl plik

@ -0,0 +1,112 @@
#! /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 RenderSettings
COLORS = {
'black': (0.0, 0.0, 0.0),
'white': (1.0, 1.0, 1.0),
'red': (1.0, 0.0, 0.0),
'green': (0.0, 1.0, 0.0),
'yellow': (1.0, 1.0, 0),
'blue': (0.0, 0.0, 1.0),
'fr-4': (0.290, 0.345, 0.0),
'green soldermask': (0.0, 0.412, 0.278),
'blue soldermask': (0.059, 0.478, 0.651),
'red soldermask': (0.968, 0.169, 0.165),
'black soldermask': (0.298, 0.275, 0.282),
'purple soldermask': (0.2, 0.0, 0.334),
'enig copper': (0.694, 0.533, 0.514),
'hasl copper': (0.871, 0.851, 0.839)
}
SPECTRUM = [
(0.804, 0.216, 0),
(0.78, 0.776, 0.251),
(0.545, 0.451, 0.333),
(0.545, 0.137, 0.137),
(0.329, 0.545, 0.329),
(0.133, 0.545, 0.133),
(0, 0.525, 0.545),
(0.227, 0.373, 0.804),
]
class Theme(object):
def __init__(self, name=None, **kwargs):
self.name = 'Default' if name is None else name
self.background = kwargs.get('background', RenderSettings(COLORS['fr-4']))
self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white']))
self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=True))
self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True))
self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True, mirror=True))
self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper']))
self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True))
self.drill = kwargs.get('drill', RenderSettings(COLORS['black']))
self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red']))
self._internal = kwargs.get('internal', [RenderSettings(x) for x in SPECTRUM])
self._internal_gen = None
def __getitem__(self, key):
return getattr(self, key)
@property
def internal(self):
if not self._internal_gen:
self._internal_gen = self._internal_gen_func()
return next(self._internal_gen)
def _internal_gen_func(self):
for setting in self._internal:
yield setting
def get(self, key, noneval=None):
val = getattr(self, key, None)
return val if val is not None else noneval
THEMES = {
'default': Theme(),
'OSH Park': Theme(name='OSH Park',
background=RenderSettings(COLORS['purple soldermask']),
top=RenderSettings(COLORS['enig copper']),
bottom=RenderSettings(COLORS['enig copper'], mirror=True),
topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True),
bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True, mirror=True),
topsilk=RenderSettings(COLORS['white'], alpha=0.8),
bottomsilk=RenderSettings(COLORS['white'], alpha=0.8, mirror=True)),
'Blue': Theme(name='Blue',
topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True),
bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)),
'Transparent Copper': Theme(name='Transparent',
background=RenderSettings((0.9, 0.9, 0.9)),
top=RenderSettings(COLORS['red'], alpha=0.5),
bottom=RenderSettings(COLORS['blue'], alpha=0.5),
drill=RenderSettings((0.3, 0.3, 0.3))),
'Transparent Multilayer': Theme(name='Transparent Multilayer',
background=RenderSettings((0, 0, 0)),
top=RenderSettings(SPECTRUM[0], alpha=0.8),
bottom=RenderSettings(SPECTRUM[-1], alpha=0.8),
drill=RenderSettings((0.3, 0.3, 0.3)),
internal=[RenderSettings(x, alpha=0.5) for x in SPECTRUM[1:-1]]),
}

800
gerber/rs274x.py 100644
Wyświetl plik

@ -0,0 +1,800 @@
#! /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 os
import re
import sys
try:
from cStringIO import StringIO
except(ImportError):
from io import StringIO
from .gerber_statements import *
from .primitives import *
from .cam import CamFile, FileSettings
from .utils import sq_distance
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)
def loads(data, filename=None):
""" Generate a GerberFile object from rs274x data in memory
Parameters
----------
data : string
string containing gerber file contents
filename : string, optional
string containing the filename of the data source
Returns
-------
file : :class:`gerber.rs274x.GerberFile`
A GerberFile created from the specified file.
"""
return GerberParser().parse_raw(data, 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, apertures, filename=None):
super(GerberFile, self).__init__(statements, settings, primitives, filename)
self.apertures = apertures
@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):
min_x = min_y = 1000000
max_x = max_y = -1000000
for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]:
if stmt.x is not None:
min_x = min(stmt.x, min_x)
max_x = max(stmt.x, max_x)
if stmt.y is not None:
min_y = min(stmt.y, min_y)
max_y = max(stmt.y, max_y)
return ((min_x, max_x), (min_y, max_y))
@property
def bounding_box(self):
min_x = min_y = 1000000
max_x = max_y = -1000000
for prim in self.primitives:
bounds = prim.bounding_box
min_x = min(bounds[0][0], min_x)
max_x = max(bounds[0][1], max_x)
min_y = min(bounds[1][0], min_y)
max_y = max(bounds[1][1], max_y)
return ((min_x, max_x), (min_y, max_y))
def write(self, filename, settings=None):
""" Write data out to a gerber file.
"""
with open(filename, 'w') as f:
for statement in self.statements:
f.write(statement.to_gerber(settings or self.settings))
f.write("\n")
def to_inch(self):
if self.units != 'inch':
self.units = 'inch'
for statement in self.statements:
statement.to_inch()
for primitive in self.primitives:
primitive.to_inch()
def to_metric(self):
if self.units != 'metric':
self.units = 'metric'
for statement in self.statements:
statement.to_metric()
for primitive in self.primitives:
primitive.to_metric()
def offset(self, x_offset=0, y_offset=0):
for statement in self.statements:
statement.offset(x_offset, y_offset)
for primitive in self.primitives:
primitive.offset(x_offset, y_offset)
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+\-]+"
FS = r"(?P<param>FS)(?P<zero>(L|T|D))?(?P<notation>(A|I))[NG0-9]*X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])[DM0-9]*"
MO = r"(?P<param>MO)(?P<mo>(MM|IN))"
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)
# Include File
IF = r"(?P<param>IF)(?P<filename>.*)"
# begin deprecated
AS = r"(?P<param>AS)(?P<mode>(AXBY)|(AYBX))"
IN = r"(?P<param>IN)(?P<name>.*)"
IP = r"(?P<param>IP)(?P<ip>(POS|NEG))"
IR = r"(?P<param>IR)(?P<angle>{number})".format(number=NUMBER)
MI = r"(?P<param>MI)(A(?P<a>0|1))?(B(?P<b>0|1))?"
OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL)
SF = r"(?P<param>SF)(?P<discarded>.*)"
LN = r"(?P<param>LN)(?P<name>.*)"
DEPRECATED_UNIT = re.compile(r'(?P<mode>G7[01])\*')
DEPRECATED_FORMAT = re.compile(r'(?P<format>G9[01])\*')
# end deprecated
PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY,
AD_MACRO, AM, AS, IF, IN, IP, IR, MI, OF, SF, LN)
PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS]
COORD_FUNCTION = r"G0?[123]"
COORD_OP = r"D0?[123]"
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=COORD_FUNCTION, op=COORD_OP)))
APERTURE_STMT = re.compile(r"(?P<deprecated>(G54)|(G55))?D(?P<d>\d+)\*")
COMMENT_STMT = re.compile(r"G0?4(?P<comment>[^*]*)(\*)?")
EOF_STMT = re.compile(r"(?P<eof>M[0]?[012])\*")
REGION_MODE_STMT = re.compile(r'(?P<mode>G3[67])\*')
QUAD_MODE_STMT = re.compile(r'(?P<mode>G7[45])\*')
# Keep include loop from crashing us
INCLUDE_FILE_RECURSION_LIMIT = 10
def __init__(self):
self.filename = None
self.settings = FileSettings()
self.statements = []
self.primitives = []
self.apertures = {}
self.macros = {}
self.current_region = None
self.x = 0
self.y = 0
self.op = "D02"
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)
self._recursion_depth = 0
def parse(self, filename):
self.filename = filename
with open(filename, "rU") as fp:
data = fp.read()
return self.parse_raw(data, filename)
def parse_raw(self, data, filename=None):
self.filename = filename
for stmt in self._parse(self._split_commands(data)):
self.evaluate(stmt)
self.statements.append(stmt)
# Initialize statement units
for stmt in self.statements:
stmt.units = self.settings.units
return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename)
def _split_commands(self, data):
"""
Split the data into commands. Commands end with * (and also newline to help with some badly formatted files)
"""
length = len(data)
start = 0
in_header = True
for cur in range(0, length):
val = data[cur]
if val == '%' and start == cur:
in_header = True
continue
if val == '\r' or val == '\n':
if start != cur:
yield data[start:cur]
start = cur + 1
elif not in_header and val == '*':
yield data[start:cur + 1]
start = cur + 1
elif in_header and val == '%':
yield data[start:cur + 1]
start = cur + 1
in_header = False
def dump_json(self):
stmts = {"statements": [stmt.__dict__ for stmt in self.statements]}
return json.dumps(stmts)
def dump_str(self):
string = ""
for stmt in self.statements:
string += str(stmt) + "\n"
return string
def _parse(self, data):
oldline = ''
for line in 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("%") and not "%" in line[1:]:
oldline = line
continue
did_something = True # make sure we do at least one loop
while did_something and len(line) > 0:
did_something = False
# consume empty data blocks
if line[0] == '*':
line = line[1:]
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
# 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"] == "LP":
yield LPParamStmt.from_dict(param)
elif param["param"] == "AD":
yield ADParamStmt.from_dict(param)
elif param["param"] == "AM":
stmt = AMParamStmt.from_dict(param)
stmt.units = self.settings.units
yield stmt
elif param["param"] == "OF":
yield OFParamStmt.from_dict(param)
elif param["param"] == "IF":
# Don't crash on include loop
if self._recursion_depth < self.INCLUDE_FILE_RECURSION_LIMIT:
self._recursion_depth += 1
with open(os.path.join(os.path.dirname(self.filename), param["filename"]), 'r') as f:
inc_data = f.read()
for stmt in self._parse(self._split_commands(inc_data)):
yield stmt
self._recursion_depth -= 1
else:
raise IOError("Include file nesting depth limit exceeded.")
elif param["param"] == "IN":
yield INParamStmt.from_dict(param)
elif param["param"] == "LN":
yield LNParamStmt.from_dict(param)
# deprecated commands AS, IN, IP, IR, MI, OF, SF, LN
elif param["param"] == "AS":
yield ASParamStmt.from_dict(param)
elif param["param"] == "IN":
yield INParamStmt.from_dict(param)
elif param["param"] == "IP":
yield IPParamStmt.from_dict(param)
elif param["param"] == "IR":
yield IRParamStmt.from_dict(param)
elif param["param"] == "MI":
yield MIParamStmt.from_dict(param)
elif param["param"] == "OF":
yield OFParamStmt.from_dict(param)
elif param["param"] == "SF":
yield SFParamStmt.from_dict(param)
elif param["param"] == "LN":
yield LNParamStmt.from_dict(param)
else:
yield UnknownStmt(line)
did_something = True
line = r
continue
# 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
# comment
(comment, r) = _match_one(self.COMMENT_STMT, line)
if comment:
yield CommentStmt(comment["comment"])
did_something = True
line = r
continue
# deprecated codes
(deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line)
if deprecated_unit:
stmt = MOParamStmt(param="MO", mo="inch" if "G70" in
deprecated_unit["mode"] else "metric")
self.settings.units = stmt.mode
yield stmt
line = r
did_something = True
continue
(deprecated_format, r) = _match_one(self.DEPRECATED_FORMAT, line)
if deprecated_format:
yield DeprecatedStmt.from_gerber(line)
line = r
did_something = True
continue
# eof
(eof, r) = _match_one(self.EOF_STMT, line)
if eof:
yield EofStmt()
did_something = True
line = r
continue
if line.find('*') > 0:
yield UnknownStmt(line)
did_something = True
line = ""
continue
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, DeprecatedStmt, EofStmt)):
return
else:
raise Exception("Invalid statement to evaluate")
def _define_aperture(self, d, shape, modifiers):
aperture = None
if shape == 'C':
diameter = modifiers[0][0]
hole_diameter = 0
rectangular_hole = (0, 0)
if len(modifiers[0]) == 2:
hole_diameter = modifiers[0][1]
elif len(modifiers[0]) == 3:
rectangular_hole = modifiers[0][1:3]
aperture = Circle(position=None, diameter=diameter,
hole_diameter=hole_diameter,
hole_width=rectangular_hole[0],
hole_height=rectangular_hole[1],
units=self.settings.units)
elif shape == 'R':
width = modifiers[0][0]
height = modifiers[0][1]
hole_diameter = 0
rectangular_hole = (0, 0)
if len(modifiers[0]) == 3:
hole_diameter = modifiers[0][2]
elif len(modifiers[0]) == 4:
rectangular_hole = modifiers[0][2:4]
aperture = Rectangle(position=None, width=width, height=height,
hole_diameter=hole_diameter,
hole_width=rectangular_hole[0],
hole_height=rectangular_hole[1],
units=self.settings.units)
elif shape == 'O':
width = modifiers[0][0]
height = modifiers[0][1]
hole_diameter = 0
rectangular_hole = (0, 0)
if len(modifiers[0]) == 3:
hole_diameter = modifiers[0][2]
elif len(modifiers[0]) == 4:
rectangular_hole = modifiers[0][2:4]
aperture = Obround(position=None, width=width, height=height,
hole_diameter=hole_diameter,
hole_width=rectangular_hole[0],
hole_height=rectangular_hole[1],
units=self.settings.units)
elif shape == 'P':
outer_diameter = modifiers[0][0]
number_vertices = int(modifiers[0][1])
if len(modifiers[0]) > 2:
rotation = modifiers[0][2]
else:
rotation = 0
hole_diameter = 0
rectangular_hole = (0, 0)
if len(modifiers[0]) == 4:
hole_diameter = modifiers[0][3]
elif len(modifiers[0]) >= 5:
rectangular_hole = modifiers[0][3:5]
aperture = Polygon(position=None, sides=number_vertices,
radius=outer_diameter/2.0,
hole_diameter=hole_diameter,
hole_width=rectangular_hole[0],
hole_height=rectangular_hole[1],
rotation=rotation)
else:
aperture = self.macros[shape].build(modifiers)
aperture.units = self.settings.units
self.apertures[d] = aperture
def _evaluate_mode(self, stmt):
if stmt.type == 'RegionMode':
if self.region_mode == 'on' and stmt.mode == 'off':
# Sometimes we have regions that have no points. Skip those
if self.current_region:
self.primitives.append(Region(self.current_region,
level_polarity=self.level_polarity, units=self.settings.units))
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 == "AM":
self.macros[stmt.name] = stmt
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.only_function:
# Sometimes we get a coordinate statement
# that only sets the function. If so, don't
# try futher otherwise that might draw/flash something
return
if stmt.op:
self.op = stmt.op
else:
# no implicit op allowed, force here if coord block doesn't have it
stmt.op = self.op
if self.op == "D01" or self.op == "D1":
start = (self.x, self.y)
end = (x, y)
if self.interpolation == 'linear':
if self.region_mode == 'off':
self.primitives.append(Line(start, end,
self.apertures[self.aperture],
level_polarity=self.level_polarity,
units=self.settings.units))
else:
# from gerber spec revision J3, Section 4.5, page 55:
# The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness.
# The current aperture is associated with the region.
# This has no graphical effect, but allows all its attributes to
# be applied to the region.
if self.current_region is None:
self.current_region = [Line(start, end,
self.apertures.get(self.aperture,
Circle((0, 0), 0)),
level_polarity=self.level_polarity,
units=self.settings.units), ]
else:
self.current_region.append(Line(start, end,
self.apertures.get(self.aperture,
Circle((0, 0), 0)),
level_polarity=self.level_polarity,
units=self.settings.units))
else:
i = 0 if stmt.i is None else stmt.i
j = 0 if stmt.j is None else stmt.j
center = self._find_center(start, end, (i, j))
if self.region_mode == 'off':
self.primitives.append(Arc(start, end, center, self.direction,
self.apertures[self.aperture],
quadrant_mode=self.quadrant_mode,
level_polarity=self.level_polarity,
units=self.settings.units))
else:
if self.current_region is None:
self.current_region = [Arc(start, end, center, self.direction,
self.apertures.get(self.aperture, Circle((0,0), 0)),
quadrant_mode=self.quadrant_mode,
level_polarity=self.level_polarity,
units=self.settings.units),]
else:
self.current_region.append(Arc(start, end, center, self.direction,
self.apertures.get(self.aperture, Circle((0,0), 0)),
quadrant_mode=self.quadrant_mode,
level_polarity=self.level_polarity,
units=self.settings.units))
# Gerbv seems to reset interpolation mode in regions..
# TODO: Make sure this is right.
self.interpolation = 'linear'
elif self.op == "D02" or self.op == "D2":
if self.region_mode == "on":
# D02 in the middle of a region finishes that region and starts a new one
if self.current_region and len(self.current_region) > 1:
self.primitives.append(Region(self.current_region,
level_polarity=self.level_polarity,
units=self.settings.units))
self.current_region = None
elif self.op == "D03" or self.op == "D3":
primitive = copy.deepcopy(self.apertures[self.aperture])
if primitive is not None:
if not isinstance(primitive, AMParamStmt):
primitive.position = (x, y)
primitive.level_polarity = self.level_polarity
primitive.units = self.settings.units
self.primitives.append(primitive)
else:
# Aperture Macro
for am_prim in primitive.primitives:
renderable = am_prim.to_primitive((x, y),
self.level_polarity,
self.settings.units)
if renderable is not None:
self.primitives.append(renderable)
self.x, self.y = x, y
def _find_center(self, start, end, offsets):
"""
In single quadrant mode, the offsets are always positive, which means
there are 4 possible centers. The correct center is the only one that
results in an arc with sweep angle of less than or equal to 90 degrees
in the specified direction
"""
two_pi = 2 * math.pi
if self.quadrant_mode == 'single-quadrant':
# The Gerber spec says single quadrant only has one possible center,
# and you can detect it based on the angle. But for real files, this
# seems to work better - there is usually only one option that makes
# sense for the center (since the distance should be the same
# from start and end). We select the center with the least error in
# radius from all the options with a valid sweep angle.
sqdist_diff_min = sys.maxsize
center = None
for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]:
test_center = (start[0] + offsets[0] * factors[0],
start[1] + offsets[1] * factors[1])
# Find angle from center to start and end points
start_angle = math.atan2(*reversed([_start - _center for _start, _center in zip(start, test_center)]))
end_angle = math.atan2(*reversed([_end - _center for _end, _center in zip(end, test_center)]))
# Clamp angles to 0, 2pi
theta0 = (start_angle + two_pi) % two_pi
theta1 = (end_angle + two_pi) % two_pi
# Determine sweep angle in the current arc direction
if self.direction == 'counterclockwise':
sweep_angle = abs(theta1 - theta0)
else:
theta0 += two_pi
sweep_angle = abs(theta0 - theta1) % two_pi
# Calculate the radius error
sqdist_start = sq_distance(start, test_center)
sqdist_end = sq_distance(end, test_center)
sqdist_diff = abs(sqdist_start - sqdist_end)
# Take the option with the lowest radius error from the set of
# options with a valid sweep angle
# In some rare cases, the sweep angle is numerically (10**-14) above pi/2
# So it is safer to compare the angles with some tolerance
is_lowest_radius_error = sqdist_diff < sqdist_diff_min
is_valid_sweep_angle = sweep_angle >= 0 and sweep_angle <= math.pi / 2.0 + 1e-6
if is_lowest_radius_error and is_valid_sweep_angle:
center = test_center
sqdist_diff_min = sqdist_diff
return center
else:
return (start[0] + offsets[0], start[1] + offsets[1])
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)

Wyświetl plik

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 9.9 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 46 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.3 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.8 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.0 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 70 KiB

Some files were not shown because too many files have changed in this diff Show More