Compare commits

...
Sign in to create a new pull request.

100 commits

Author SHA1 Message Date
jaseg
ada252ad6d svg-flatten: Bump to -O2 2021-02-27 20:11:24 +01:00
jaseg
3d2aefe4d9 Vendor pugixml 2021-02-27 20:08:48 +01:00
jaseg
901efc75c6 Bump version to 2.0.5 2021-02-14 12:15:19 +01:00
jaseg
3fc628beec Fix handling of stroke width under transformations 2021-02-11 00:18:54 +01:00
jaseg
e8d7ca1d6c Fix readme RST syntax 2021-02-08 22:33:57 +01:00
jaseg
a3443a459b README: fix intro 2021-02-08 22:27:20 +01:00
Nein Seg
0ffb9ece31 Update README.rst 2021-02-07 23:13:38 +00:00
Ricardo (XenGi) Band
29408cb2b0 fix $PATH 2021-02-07 23:57:24 +01:00
Ricardo (XenGi) Band
99d0479d3a no need for su 2021-02-07 22:40:26 +01:00
Ricardo (XenGi) Band
f560923a9c typo 2021-02-07 22:37:19 +01:00
Ricardo (XenGi) Band
100e865394 using own build containers now 2021-02-07 22:34:02 +01:00
Ricardo (XenGi) Band
d4c7e8d344 give ubuntu/debian it's own special thing 2021-02-07 20:47:01 +01:00
Ricardo (XenGi) Band
d52b0f20e5 ubuntu fixed 2021-02-07 20:41:53 +01:00
Ricardo (XenGi) Band
cb67de412c fixed various typos 2021-02-07 20:38:00 +01:00
Ricardo (XenGi) Band
f52dea8eac fixed bash string replacement 2021-02-07 20:36:46 +01:00
Ricardo (XenGi) Band
69a274189a put dependencies into variables 2021-02-07 20:32:29 +01:00
Ricardo (XenGi) Band
0f2e8bd5ee debian should work 2021-02-07 20:24:30 +01:00
Ricardo (XenGi) Band
bcae0f16c7 fedora almost works 2021-02-07 20:01:08 +01:00
Ricardo (XenGi) Band
729ec14b66 use help message instead of version 2021-02-07 19:28:21 +01:00
Ricardo (XenGi) Band
d3a83bd0cc Install python deps with pacman 2021-02-07 19:20:53 +01:00
Ricardo (XenGi) Band
0a02e01a96 Checkout submodules before build 2021-02-07 19:09:36 +01:00
Ricardo (XenGi) Band
7e6b07a491 Now actually building the source 2021-02-07 19:03:49 +01:00
Ricardo (XenGi) Band
2fd7045c82 using pip instead of pip3 2021-02-07 18:56:29 +01:00
Ricardo (XenGi) Band
e74a0cf204 base-devel added as dependency 2021-02-07 18:53:41 +01:00
Ricardo (XenGi) Band
89a09b60ab updating arch before build 2021-02-07 18:51:54 +01:00
Ricardo (XenGi) Band
ce9a7a3082 trying to build gerbolize in archlinux 2021-02-07 18:48:08 +01:00
Ricardo (XenGi) Band
c0c019aea6 trying to build gerbolize via ci 2021-02-07 18:29:44 +01:00
jaseg
57fad5d8c3 README: Add missing pkg-config to arch install instructions 2021-02-07 14:43:08 +01:00
jaseg
59ea38c9bd README: use pcb-tools-extension fork for now 2021-02-07 14:34:45 +01:00
jaseg
be6783e803 Update description in gerboweb 2021-02-07 14:08:23 +01:00
jaseg
a4b45196df Update gerboweb 2021-02-07 13:55:21 +01:00
jaseg
5e04765842 Bump version to v2.0.4 2021-02-07 13:19:45 +01:00
jaseg
56c6443301 README: fix syntax error 2021-02-07 13:19:16 +01:00
jaseg
fe4a4001ba Bump version to v2.0.3 2021-02-06 22:16:08 +01:00
jaseg
ac65590c85 README: add info on mfg 2021-02-07 13:12:31 +01:00
jaseg
d9d06bff63 Bump version to v2.0.2 2021-02-07 13:12:31 +01:00
jaseg
4a967682d4 Make inital test run on debian, ubuntu, arch, fedora 2021-02-07 12:47:49 +01:00
jaseg
79db262858 Make svg-flatten always clean up its temporary files 2021-02-06 19:24:45 +01:00
jaseg
88faecbdc2 README: set image width 2021-02-06 19:13:27 +01:00
jaseg
a250ee6295 Fix some rst syntax errors 2021-02-06 19:01:23 +01:00
jaseg
685dbb56b2 README: Update subtraction script blurb 2021-02-06 16:01:29 +01:00
jaseg
40a1c2bceb README: Update subtract example pic 2021-02-06 15:57:46 +01:00
jaseg
3f9cdce1cb Update README w/ subtraction script example 2021-02-06 15:51:37 +01:00
jaseg
50cc3ce27c Update README 2021-02-06 15:46:53 +01:00
jaseg
3bcbb29cf3 svg-flatten: Fix invalid export of negative gerber coordinates 2021-02-06 15:36:27 +01:00
jaseg
3e323c953c Add workaround to clipper zero-height path bug 2021-02-06 14:59:44 +01:00
jaseg
13cb49b218 Add support for direct PNG/JPG input and add legacy wrapper 2021-02-06 13:26:36 +01:00
jaseg
18d857668e Small fixes, find svg-flatten in user pip install 2021-02-06 12:30:27 +01:00
jaseg
1e9d0c62f9 Fix svg-flatten install location 2021-02-06 09:34:02 +01:00
jaseg
06c2d5295d Remove usvg auto-install from setup.py 2021-02-06 00:20:40 +01:00
jaseg
1d58b4d584 Update syntax of second note
github doesn't style rst attention blocks properly
2021-02-06 00:16:42 +01:00
jaseg
f95a24ebb9 Finish up initial draft of install doc 2021-02-06 00:14:01 +01:00
jaseg
8c494f7736 Work on fedora/debian compatibility 2021-02-06 00:05:27 +01:00
jaseg
bf428103d3 Setup doc WIP 2021-02-05 23:01:48 +01:00
jaseg
6dec0a6e17 Add svg-flatten source files to sdist 2021-02-04 23:20:03 +01:00
jaseg
62622813be setup.py: Integrate C++/usvg install 2021-02-04 23:11:55 +01:00
jaseg
6aab099baa svg-flatten: Be even smarter locating usvg 2021-02-04 22:56:53 +01:00
jaseg
bd2b012740 svg-flatten: Be a bit smarter locating usvg 2021-02-04 22:53:49 +01:00
jaseg
fb9c6ac32c svg-flatten: Add dependency check 2021-02-04 22:53:33 +01:00
jaseg
bdd79a8f2b Update README 2021-02-04 22:09:17 +01:00
jaseg
91c46a07c6 Update post 2021-02-04 12:38:18 +01:00
jaseg
52a35dd63a Add post 2021-02-01 23:36:00 +01:00
jaseg
fc495607dc Update README 2021-02-01 12:46:50 +01:00
jaseg
5f4f667e17 Work on packaging 2021-02-01 00:14:41 +01:00
jaseg
6c01126ce9 Finish command a bit 2021-01-31 23:58:55 +01:00
jaseg
a68e395cb6 Advanced svg/gerber composition working 2021-01-31 22:11:34 +01:00
jaseg
49a7c6df41 WIP 2021-01-31 20:49:23 +01:00
jaseg
b5d523741c Both raster and vector template generation work now 2021-01-31 14:27:37 +01:00
jaseg
04f1cab5fc Re-did all the render code 2021-01-31 01:21:54 +01:00
jaseg
133f5bb98d README: add blurb on AGPL 2021-01-30 20:26:34 +01:00
jaseg
c5f8416b63 Merge old gerbolyze history 2021-01-30 20:19:34 +01:00
jaseg
3f3b8487d4 Move gerbolyze python files to subdirectory 2021-01-30 20:16:19 +01:00
jaseg
2133867c8a Reorg: move svg-flatten files into subdir 2021-01-30 20:01:00 +01:00
jaseg
617a42a674 README: Update commandline usage 2021-01-30 15:19:10 +01:00
jaseg
f88134f9ca Add dilation option 2021-01-30 15:18:56 +01:00
jaseg
a6540b73da Multi-layer module export working 2021-01-30 14:17:42 +01:00
jaseg
eac89409b8 Add KiCAD sexp output 2021-01-30 00:27:43 +01:00
jaseg
52dcceb87f Add support for preserveAspectRatio for both SVG and bitmap input 2021-01-29 23:20:16 +01:00
jaseg
a34efc058a Add direct image export 2021-01-29 21:58:42 +01:00
jaseg
6d1a7750c5 Per-image vectorizer selection works 2021-01-28 23:15:36 +01:00
jaseg
f65cd52304 Binary vectorizer works 2021-01-28 22:34:38 +01:00
jaseg
9711fabab7 WIP render settings 2021-01-27 01:26:54 +01:00
jaseg
5285b6dce8 Add group id include/exclude matching 2021-01-27 00:49:31 +01:00
jaseg
70d0021df1 Remove randomization from flattener 2021-01-26 23:52:10 +01:00
jaseg
f9a871f9b2 Speed up flatterner through randomization 2021-01-26 23:44:17 +01:00
jaseg
3cee5d4f01 The cavalier flattener works!!1! 2021-01-26 23:21:31 +01:00
jaseg
bc0ef634cf Prototype flattener/compositor code 2021-01-26 00:01:53 +01:00
jaseg
538b8a32b9 Command-line format selection works 2021-01-25 15:15:36 +01:00
jaseg
33848ad43c Fix build, add missing include 2021-01-25 14:56:20 +01:00
jaseg
95b198a5aa Gerber and SVG export working 2021-01-25 14:55:13 +01:00
jaseg
f7b4cc602b Initial commit 2021-01-24 18:44:56 +01:00
jaseg
c6b1c2225d remove ansible scripts, they are now in their own "infra" repo 2020-12-30 13:12:06 +01:00
jaseg
e290ac758b Add README 2020-12-30 12:11:11 +01:00
jaseg
ee35c06119 Update cgit config with cosmetic changes 2020-12-30 11:52:05 +01:00
jaseg
6fbea50682 Tag git setup foo 2020-12-30 11:38:27 +01:00
jaseg
b47ca7bbdc Add dyndns secrets to gitignore 2020-12-29 13:08:39 +01:00
jaseg
e63a7e557d Add dns, dyndns services 2020-12-29 13:08:13 +01:00
jaseg
659290677b Move most domains from jaseg.net to jaseg.de 2020-12-29 13:07:30 +01:00
jaseg
9510936fd9 gitolite.rc: Enable cgit metadata config keys 2020-12-29 13:06:57 +01:00
jaseg
6635bc6d46 Update arch container keyrings before sysupdate 2020-12-29 13:06:44 +01:00
332 changed files with 37029 additions and 3427 deletions

38
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,38 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
stages:
- build
debian_10:
stage: build
image: "registry.gitlab.com/gerbolyze/build-containers/debian:10"
script:
- "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
- "python3 setup.py install --user"
- "gerbolyze --help"
ubuntu_2004:
stage: build
image: "registry.gitlab.com/gerbolyze/build-containers/ubuntu:20.04"
script:
- "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
- "python3 setup.py install --user"
- "gerbolyze --help"
fedora_33:
stage: build
image: "registry.gitlab.com/gerbolyze/build-containers/fedora:33"
script:
- "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
- "python3 setup.py install --user"
- "gerbolyze --help"
archlinux:
stage: build
image: "registry.gitlab.com/gerbolyze/build-containers/archlinux:latest"
script:
- "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
- "python setup.py install --user"
- "gerbolyze --help"

27
.gitmodules vendored
View file

@ -1,6 +1,21 @@
[submodule "gerboweb/deploy/checkouts/pogojig"]
path = gerboweb/deploy/checkouts/pogojig
url = https://github.com/jaseg/pogojig.git
[submodule "gerboweb/deploy/library/ansible-collection"]
path = gerboweb/deploy/library/inwx-collection
url = https://github.com/inwx/ansible-collection
[submodule "upstream/cpp-base64"]
path = upstream/cpp-base64
url = https://github.com/ReneNyffenegger/cpp-base64
[submodule "upstream/voronoi"]
path = upstream/voronoi
url = https://github.com/JCash/voronoi
[submodule "upstream/poisson-disk-sampling"]
path = upstream/poisson-disk-sampling
url = https://github.com/thinks/poisson-disk-sampling
[submodule "upstream/argagg"]
path = upstream/argagg
url = https://github.com/vietjtnguyen/argagg
[submodule "upstream/CavalierContours"]
path = upstream/CavalierContours
url = https://github.com/jbuckmccready/CavalierContours
[submodule "upstream/subprocess.h"]
path = upstream/subprocess.h
url = https://github.com/sheredom/subprocess.h
[submodule "upstream/pugixml"]
path = upstream/pugixml
url = https://github.com/zeux/pugixml

View file

@ -1 +1,10 @@
include README.rst
recursive-include svg-flatten *
recursive-include upstream *
recursive-exclude upstream/voronoi/test *
recursive-exclude upstream/subprocess.h/test *
recursive-exclude upstream/poisson-disk-sampling/thinks/poisson_disk_sampling/examples *
recursive-exclude upstream/poisson-disk-sampling/images *
recursive-exclude upstream/clipper-6.4.2/Documentation *
recursive-exclude upstream/CavalierContours tests/* examples/*
recursive-exclude upstream/argagg doc/* examples/* tests/*
recursive-exclude svg-flatten/build *

View file

@ -1,87 +1,546 @@
Gerbolyze high-resolution image-to-PCB converter
================================================
Gerbolyze high-fidelity SVG/PNG/JPG to PCB converter
====================================================
Gerbolyze renders SVG vector and PNG/JPG raster images into existing gerber PCB manufacturing files.
Vector data from SVG files is rendered losslessly *without* an intermediate rasterization/revectorization step.
Still, gerbolyze supports (almost) the full SVG 1.1 spec including complex, self-intersecting paths with holes,
patterns, dashes and transformations
Raster images can either be vectorized through contour tracing (like gerbolyze v1.0 did) or they can be embedded using
high-resolution grayscale emulation while (mostly) guaranteeing trace/space design rules.
.. figure:: pics/pcbway_sample_02_small.jpg
:width: 800px
Drawing by `トーコ Toko <https://twitter.com/fluffy2038/status/1317231121269104640>`__ converted using Gerbolyze and printed at PCBWay.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample1.jpg
Tooling for PCB art is quite limited in both open source and closed source ecosystems. Something as simple as putting a
pretty picture on a PCB can be an extremely tedious task. Depending on the PCB tool used, various arcane incantations
may be necessary and even modestly complex images will slow down most PCB tools to a crawl.
Gerbolyze solves this problem in a toolchain-agnostic way by directly vectorizing bitmap files onto existing gerber
layers. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the industry-standard Altium
Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of thousands of primitives,
generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can finally be confident that
your PCB fab's toolchain will fall over before yours does if you overdo it with the high-poly anime silkscreen.
Gerbolyze solves this problem in a toolchain-agnostic way by directly vectorizing SVG vector and PNG or JPG bitmap files
onto existing gerber layers. Gerbolyze processes any spec-compliant SVG and "gerbolyzes" SVG vector data into a Gerber
spec-compliant form. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the
industry-standard Altium Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of
thousands of primitives, generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can
finally be confident that your PCB fab's toolchain will fall over before yours does if you overdo it with the high-poly
anime silkscreen.
.. image:: pics/process-overview.png
:width: 800px
.. contents::
Produce high-quality artistic PCBs in three easy steps!
-------------------------------------------------------
Tl;dr: Produce high-quality artistic PCBs in three easy steps!
--------------------------------------------------------------
Gerbolyze works in three steps.
1. Generate a scale-accurate preview of the finished PCB from your CAD tool's gerber output:
1. Generate a scale-accurate template of the finished PCB from your CAD tool's gerber output:
.. code::
$ gerbolyze render top my_gerber_dir preview.png
$ gerbolyze template --top template_top.svg [--bottom template_bottom.svg] my_gerber_dir
2. Load the resulting preview image into the GIMP or another image editing program. Use it as a guide to position scale
your artwork. Create a black-and-white image from your scaled artwork using GIMP's newsprint filter. Make sure most
details are larger than about 10px to ensure manufacturing goes smooth.
2. Load the resulting template image Inkscape_ or another SVG editing program. Put your artwork on the appropriate SVG
layer. Dark colors become filled gerber primitives, bright colors become unfilled primitives. You can directly put
raster images (PNG/JPG) into this SVG as well, just position and scale them like everything else. SVG clips work for
images, too. Masks are not supported.
3. Vectorize the resulting grayscale image drectly into the PCB's gerber files:
3. Vectorize the edited SVG template image drectly into the PCB's gerber files:
.. code::
$ gerbolyze vectorize top input_gerber_dir output_gerber_dir black_and_white_artwork.png
$ gerbolyze paste --top template_top_edited.svg [--bottom ...] my_gerber_dir output_gerber_dir
Image preprocessing guide
-------------------------
Quick Start Installation
------------------------
Nice black-and-white images can be generated from any grayscale image using the GIMP's newsprint filter. The
straight-forward pre-processing steps necessary for use by ``gerbolyze vectorize`` are as follows.
This will install gerbolyze and svg-flatten into a Python virtualenv and install usvg into your ``~/.cargo``.
1 Import a render of the board generated using ``gerbolyze render``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Note:
Right now (2020-02-07), ``pcb-tools-extension`` must be installed manually from the fork at:
``gerbolyze render`` will automatically scale the render such that ten pixels in the render correspond to 6mil on the
board, which is about the smallest detail most manufacturers can resolve on the silkscreen layer. You can control this
setting using the ``--fab-resolution`` and ``--oversampling`` options. Refer to ``gerbolyze --help`` for details.
``pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git``
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/01import01.png
This fork contains fixes for compatibility issues with KiCAD nightlies that are still in the process of being
upstreamed.
2 Import your desired artwork
Debian
~~~~~~
Note:
Right now, debian stable ships with a rust that is so stable it can't even build half of usvg's dependencies. That's
why we yolo-install our own rust here. Sorry about that. I guess it'll work with the packaged rust on sid.
.. code-block:: shell
sudo apt install libopencv-dev libpugixml-dev libpangocairo-1.0-0 libpango1.0-dev libcairo2-dev clang make python3 git python3-wheel curl python3-pip python3-venv
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustup install stable
rustup default stable
cargo install usvg
pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git
pip3 install --user gerbolyze --no-binary gerbolyze
Ubuntu
~~~~~~
.. code-block:: shell
sudo apt install libopencv-dev libpugixml-dev libpangocairo-1.0-0 libpango1.0-dev libcairo2-dev clang make python3 git python3-wheel curl python3-pip python3-venv cargo
cargo install usvg
pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git
pip3 install --user gerbolyze --no-binary gerbolyze
Fedora
~~~~~~
.. code-block:: shell
sudo dnf install python3 make clang opencv-devel pugixml-devel pango-devel cairo-devel rust cargo
cargo install usvg
pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git
pip3 install --user gerbolyze --no-binary gerbolyze
Arch
~~~~
.. code-block:: shell
sudo pacman -S pugixml opencv pango cairo git python make clang rustup cargo pkgconf
rustup install stable
rustup default stable
cargo install usvg
pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git
pip3 install --user gerbolyze --no-binary gerbolyze
Build from source (any distro)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
First, install prerequisites like shown above. Then,
.. code-block:: shell
git clone --recurse-submodules https://git.jaseg.de/gerbolyze.git
cd gerbolyze
pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git
python3 -m venv
source venv/bin/activate
python3 setup.py install
Features
--------
Input on the left, output on the right.
.. image:: pics/test_svg_readme_composited.png
:width: 800px
* Almost full SVG 1.1 static spec coverage (!)
* Paths with beziers, self-intersections and holes
* Strokes, even with dashes and markers
* Pattern fills and strokes
* Transformations and nested groups
* Proper text rendering with support for complex text layout (e.g. Arabic)
* <image> elements via either built-in vectorizer or built-in halftone processor
* (some) CSS
* Writes Gerber, SVG or KiCAD S-Expression (``.kicad_mod``) formats
* Can export from top/bottom SVGs to a whole gerber layer stack at once with filename autodetection
* Can export SVGs to ``.kicad_mod`` files like svg2mod (but with full SVG support)
* Beziers flattening with configurable tolerance using actual math!
* Polygon intersection removal
* Polygon hole removal (!)
* Optionally vector-compositing of output: convert black/white/transparent image to black/transparent image
* Renders SVG templates from input gerbers for accurate and easy scaling and positioning of artwork
* layer masking with offset (e.g. all silk within 1mm of soldermask)
* Can read gerbers from zip files
Gerbolyze is the end-to-end "paste this svg into these gerbers" command that handles all layers on both board sides at
once. The heavy-duty computer geometry logic of gerbolyze is handled by the svg-flatten utility (``svg-flatten``
directory). svg-flatten reads an SVG file and renders it into a variety of output formats. svg-flatten can be used like
a variant of the popular svg2mod that supports all of SVG and handles arbitrary input ``<path>`` elements.
Algorithm Overview
------------------
This is the algorithm gerbolyze uses to process a stack of gerbers.
* Map input files to semantic layers by their filenames
* For each layer:
* load input gerber
* Pass mask layers through ``gerbv`` for conversion to SVG
* Pass mask layers SVG through ``svg-flatten --dilate``
* Pass input SVG through ``svg-flatten --only-groups [layer]``
* Overlay input gerber, mask and input svg
* Write result to output gerber
This is the algorithm svg-flatten uses to process an SVG.
* pass input SVG through usvg_
* iterate depth-first through resulting SVG.
* for groups: apply transforms and clip and recurse
* for images: Vectorize using selected vectorizer
* for paths:
* flatten path using Cairo
* remove self-intersections using Clipper
* if stroke is set: process dash, then offset using Clipper
* apply pattern fills
* clip to clip-path
* remove holes using Clipper
* for KiCAD S-Expression export: vector-composite results using CavalierContours: subtract each clear output primitive
from all previous dark output primitives
Command-line usage
------------------
Generate SVG template from Gerber files:
.. code-block:: shell
gerbolyze template [options] [-t|--top top_side_output.svg] [-b|--bottom ...] input_dir_or.zip
Render design from an SVG made with the template above into a set of gerber files:
.. code-block:: shell
gerbolyze paste [options] [-t|--top top_side_design.svg] [-b|--bottom ...] input_dir_or.zip output_dir
Use svg-flatten to convert an SVG file into Gerber or flattened SVG:
.. code-block:: shell
svg-flatten [options] --format [gerber|svg] [input_file.svg] [output_file]
Use svg-flatten to convert an SVG file into the given layer of a KiCAD S-Expression (``.kicad_mod``) file:
.. code-block:: shell
svg-flatten [options] --format kicad --sexp-layer F.SilkS --sexp-mod-name My_Module [input_file.svg] [output_file]
Use svg-flatten to convert an SVG file into a ``.kicad_mod`` with SVG layers fed into separate KiCAD layers based on
their IDs like the popular ``svg2mod`` is doing:
Note:
Right now, the input SVG's layers must have *ids* that match up KiCAD's s-exp layer names. Note that when you name
a layer in Inkscape that only sets a ``name`` attribute, but does not change the ID. In order to change the ID in
Inkscape, you have to use Inkscape's "object properties" context menu function.
Also note that svg-flatten expects the layer names KiCAD uses in their S-Expression format. These are *different* to
the layer names KiCAD exposes in the UI (even though most of them match up!).
For your convenience, there is an SVG template with all the right layer names and IDs located next to this README.
.. code-block:: shell
svg-flatten [options] --format kicad --sexp-mod-name My_Module [input_file.svg] [output_file]
``gerbolyze template``
~~~~~~~~~~~~~~~~~~~~~~
Usage: ``gerbolyze template [OPTIONS] INPUT``
Generate SVG template for gerbolyze paste from gerber files.
INPUT may be a gerber file, directory of gerber files or zip file with gerber files
Options:
********
``-t, --top top_layer.svg``
Top layer output file.
``-b, --bottom bottom_layer.svg``
Bottom layer output file. --top or --bottom may be given at once. If neither is given, autogenerate filenames.
``--vector | --raster``
Embed preview renders into output file as SVG vector graphics instead of rendering them to PNG bitmaps. The
resulting preview may slow down your SVG editor.
``--raster-dpi FLOAT``
DPI for rastering preview
``--bbox TEXT``
Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR "x,y,w,h" to force [w] mm by [h]
mm output canvas with its bottom left corner at the given input gerber coördinates.
``gerbolyze paste``
~~~~~~~~~~~~~~~~~~~
(see `below <vectorization_>`__)
Usage: ``gerbolyze paste [OPTIONS] INPUT_GERBERS OUTPUT_GERBERS``
Render vector data and raster images from SVG file into gerbers.
Options:
********
``-t, --top TEXT``
Top side SVG overlay input file. At least one of this and ``--bottom`` should be given.
``-b, --bottom TEXT``
Bottom side SVG overlay input file. At least one of this and ``--top`` should be given.
``--layer-top``
Top side SVG or PNG target layer. Default: Map SVG layers to Gerber layers, map PNG to Silk.
``--layer-bottom``
Bottom side SVG or PNG target layer. See ``--layer-top``.
``--bbox TEXT``
Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR "x,y,w,h" to force [w] mm by [h]
mm output canvas with its bottom left corner at the given input gerber coördinates. This **must match the ``--bbox`` value given to
template**!
``--subtract TEXT``
Use user subtraction script from argument (see `below <subtraction_script_>`_)
``--no-subtract``
Disable subtraction (see `below <subtraction_script_>`_)
``--dilate FLOAT``
Default dilation for subtraction operations in mm (see `below <subtraction_script_>`_)
``--trace-space FLOAT``
Passed through to svg-flatten, see `below <svg_flatten_>`__.
``--vectorizer TEXT``
Passed through to svg-flatten, see `its description below <svg_flatten_>`__. Also have a look at `the examples below <vectorization_>`_.
``--vectorizer-map TEXT``
Passed through to svg-flatten, see `below <svg_flatten_>`__.
``--exclude-groups TEXT``
Passed through to svg-flatten, see `below <svg_flatten_>`__.
.. _subtraction_script:
Subtraction scripts
*******************
.. image:: pics/subtract_example.png
:width: 800px
Subtraction scripts tell ``gerbolyze paste`` to remove an area around certain input layers to from an overlay layer.
When a input layer is given in the subtraction script, gerbolyze will dilate (extend outwards) everything on this input
layer and remove it from the target overlay layer. By default, Gerbolyze subtracts the mask layer from the silk layer to
make sure there are no silk primitives that overlap bare copper, and subtracts each input layer from its corresponding
overlay to make sure the two do not overlap. In the picture above you can see both at work: The overlay contains
halftone primitives all over the place. The subtraction script has cut out an area around all pads (mask layer) and all
existing silkscreen. You can turn off this behavior by passing ``--no-subtract`` or pass your own "script".
The syntax of these scripts is:
.. code-block::
{target layer} -= {source layer} {dilation} [; ...]
The target layer must be ``out.{layer name}`` and the source layer ``in.{layer name}``. The layer names are gerbolyze's
internal layer names, i.e.: ``paste, silk, mask, copper, outline, drill``
The dilation value is optional, but can be a float with a leading ``+`` or ``-``. If given, before subtraction the
source layer's features will be extended by that many mm. If not given, the dilation defaults to the value given by
``--dilate`` if given or 0.1 mm otherwise. To disable dilation, simply pass ``+0`` here.
Multiple commands can be separated by semicolons ``;`` or line breaks.
The default subtraction script is:
.. code-block::
out.silk -= in.mask
out.silk -= in.silk+0.5
out.mask -= in.mask+0.5
out.copper -= in.copper+0.5
``gerbolyze vectorize``
~~~~~~~~~~~~~~~~~~~~~~~
``gerbolyze vectorize`` is a wrapper provided for compatibility with Gerbolyze version 1. It does nothing more than
internally call ``gerbolyze paste`` with some default arguments set.
.. _svg_flatten:
``svg-flatten``
~~~~~~~~~~~~~~~
Usage: ``svg-flatten [OPTIONS]... [INPUT_FILE] [OUTPUT_FILE]``
Specify ``-`` for stdin/stdout.
Options:
********
``-h, --help``
Print help and exit
``-v, --version``
Print version and exit
``-o, --format``
Output format. Supported: gerber, svg, s-exp (KiCAD S-Expression)
``-p, --precision``
Number of decimal places use for exported coordinates (gerber: 1-9, SVG: >=0). Note that not all gerber viewers are
happy with too many digits. 5 or 6 is a reasonable choice.
``--clear-color``
SVG color to use for "clear" areas (default: white)
``--dark-color``
SVG color to use for "dark" areas (default: black)
``-d, --trace-space``
Minimum feature size of elements in vectorized graphics (trace/space) in mm. Default: 0.1mm.
``--no-header``
Do not export output format header/footer, only export the primitives themselves
``--flatten``
Flatten output so it only consists of non-overlapping white polygons. This perform composition at the vector level.
Potentially slow. This defaults to on when using KiCAD S-Exp export because KiCAD does not know polarity or colors.
``--no-flatten``
Disable automatic flattening for KiCAD S-Exp export
``--dilate``
Dilate output gerber primitives by this amount in mm. Used for masking out other layers.
``-g, --only-groups``
Comma-separated list of group IDs to export.
``-b, --vectorizer``
Vectorizer to use for bitmap images. One of poisson-disc (default), hex-grid, square-grid, binary-contours,
dev-null. Have a look at `the examples below <vectorization_>`_.
``--vectorizer-map``
Map from image element id to vectorizer. Overrides --vectorizer. Format: id1=vectorizer,id2=vectorizer,...
You can use this to set a certain vectorizer for specific images, e.g. if you want to use both halftone
vectorization and contour tracing in the same SVG. Note that you can set an ``<image>`` element's SVG ID from within
Inkscape though the context menu's Object Properties tool.
``--force-svg``
Force SVG input irrespective of file name
``--force-png``
Force bitmap graphics input irrespective of file name
``-s, --size``
Bitmap mode only: Physical size of output image in mm. Format: 12.34x56.78
``--sexp-mod-name``
Module name for KiCAD S-Exp output. This is a mandatory argument if using S-Exp output.
``--sexp-layer``
Layer for KiCAD S-Exp output. Defaults to auto-detect layers from SVG layer/top-level group IDs. If given, SVG
groups and layers are completely ignored and everything is simply vectorized into this layer, though you cna still
use ``-g`` for group selection.
``-a, --preserve-aspect-ratio``
Bitmap mode only: Preserve aspect ratio of image. Allowed values are meet, slice. Can also parse full SVG
preserveAspectRatio syntax.
``--no-usvg``
Do not preprocess input using usvg (do not use unless you know *exactly* what you're doing)
``--usvg-dpi``
Passed through to usvg's --dpi, in case the input file has different ideas of DPI than usvg has.
``--scale``
Scale input svg lengths by this factor.
``-e, --exclude-groups``
Comma-separated list of group IDs to exclude from export. Takes precedence over --only-groups.
.. _vectorization:
Gerbolyze image vectorization
-----------------------------
Gerbolyze has two built-in strategies to translate pixel images into vector images. One is its built-in halftone
processor that tries to approximate grayscale. The other is its built-in binary vectorizer that traces contours in
black-and-white images. Below are examples for the four options.
The vectorizers can be used in isolation through ``svg-flatten`` with either an SVG input that contains an image or a
PNG/JPG input.
The vectorizer can be controlled globally using the ``--vectorizer`` flag in both ``gerbolyze`` and ``svg-flatten``. It
can also be set on a per-image basis in both using ``--vectorizer-map [image svg id]=[option]["," ...]``.
.. for f in vec_*.png; convert -background white -gravity center $f -resize 500x500 -extent 500x500 (basename -s .png $f)-square.png; end
.. for vec in hexgrid square poisson contours; convert vec_"$vec"_whole-square.png vec_"$vec"_detail-square.png -background transparent -splice 25x0+0+0 +append -chop 25x0+0+0 vec_"$vec"_composited.png; end
``--vectorizer poisson-disc`` (the default)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: pics/vec_poisson_composited.png
:width: 800px
``--vectorizer hex-grid``
~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: pics/vec_hexgrid_composited.png
:width: 800px
``--vectorizer square-grid``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: pics/vec_square_composited.png
:width: 800px
``--vectorizer binary-contours``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: pics/vec_contours_composited.png
:width: 800px
The binary contours vectorizer requires a black-and-white binary input image. As you can see, like every bitmap tracer
it will produce some artifacts. For artistic input this is usually not too bad as long as the input data is
high-resolution. Antialiased edges in the input image are not only OK, they may even help with an accurate
vectorization.
GIMP halftone preprocessing guide
---------------------------------
Gerbolyze has its own built-in halftone processor, but you can also use the high-quality "newsprint" filter built into
GIMP_ instead if you like. This section will guide you through this. The PNG you get out of this can then be fed into
gerbolyze using ``--vectorizer binary-contours``.
1 Import your desired artwork
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Though anime or manga pictures are highly recommended, you can use any image including photographs. Be careful to select
a picture with comparatively low detail that remains recognizable at very low resolution. While working on a screen this
is hard to vizualize, but the grain resulting from the low resolution of a PCB's silkscreen is quite coarse.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/02import02.png
.. image:: screenshots/02import02.png
:width: 800px
3 Paste the artwork onto the render as a new layer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/03paste.png
4 Scale, rotate and position the artwork to the desired size
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/04scale_cut.png
For alignment it may help to set the artwork layer's mode in the layers dialog to ``overlay``, which makes the PCB
render layer below shine through more. If you can't set the layer's mode, make sure you have actually made a new layer
from the floating selection you get when pasting one image into another in the GIMP.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/05position.png
5 Convert the image to grayscale
2 Convert the image to grayscale
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/06grayscale.png
.. image:: screenshots/06grayscale.png
:width: 800px
6 Fine-tune the image's contrast
3 Fine-tune the image's contrast
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To look well on the PCB, contrast is critical. If your source image is in color, you may have lost some contrast during
@ -92,9 +551,10 @@ dots that might be beyond your PCB manufacturer's maximum resolution. To control
of the grayscale value curve as shown (exaggerated) in the picture below. These steps saturate very bright grays to
white and very dark grays to black while preserving the values in the middle.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/08curve_cut.png
.. image:: screenshots/08curve_cut.png
:width: 800px
7 Retouch details
4 Retouch details
~~~~~~~~~~~~~~~~~
Therer might be small details that don't look right yet, such as the image's background color or small highlights that
@ -105,14 +565,16 @@ If you don't want the image's background to show up on the final PCB at all, jus
Particularly on low-resolution source images it may make sense to apply a blur with a radius similar to the following
newsprint filter's cell size (10px) to smooth out the dot pattern generated by the newsprint filter.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/09retouch.png
.. image:: screenshots/09retouch.png
:width: 800px
In the following example, I retouched the highlights in the hair of the character in the picture to make them completely
white instead of light-gray, so they still stand out nicely in the finished picture.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/10retouched.png
.. image:: screenshots/10retouched.png
:width: 800px
8 Run the newsprint filter
5 Run the newsprint filter
~~~~~~~~~~~~~~~~~~~~~~~~~~
Now, run the GIMP's newsprint filter, under filters, distorts, newsprint.
@ -123,32 +585,124 @@ with ``gerbolyze render`` default settings for good-quality silkscreen). In gene
The second important setting is oversampling, which should be set to four or slightly higher. This improves the result
of the edge reconstruction of ``gerbolyze vectorize``.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/11newsprint.png
.. image:: screenshots/11newsprint.png
:width: 800px
The following are examples on the detail resulting from the newsprint filter.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/12newsprint.png
.. image:: screenshots/12newsprint.png
:width: 800px
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/13newsprint.png
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/14newsprint.png
9 Export the image for use with ``gerbolyze vectorize``
6 Export the image for use with ``gerbolyze vectorize``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Simply export the image as a PNG file. Below are some pictures of the output ``gerbolyze vectorize`` produced for this
example.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/14result_cut.png
.. image:: screenshots/14result_cut.png
:width: 800px
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/15result_cut.png
.. image:: screenshots/15result_cut.png
:width: 800px
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/16result_cut.png
Manufacturing Considerations
----------------------------
The main consideration when designing artwork for PCB processes is the processes' trace/space design rule. The two
things you can do here is one, to be creative with graphical parts of the design and avoid extremely narrow lines,
wedges or other thin features that will not come out well. Number two is to keep detail in raster images several times
larger than the manufacturing processes native capability. For example, to target a trace/space design rule of 100 µm,
the smallest detail in embedded raster graphics should not be much below 1mm.
Gerbolyze's halftone vectorizers have built-in support for trace/space design rules. While they can still produce small
artifacts that violate these rules, their output should be close enough to satifsy board houses and close enough for the
result to look good. The way gerbolyze does this is to clip the halftone cell's values to zero whenevery they get too
small, and to forcefully split or merge two neighboring cells when they get too close. While this process introduces
slight steps at the top and bottom of grayscale response, for most inputs these are not noticeable.
On the other hand, for SVG vector elements as well as for traced raster images, Gerbolyze cannot help with these design
rules. There is no heuristic that would allow Gerbolyze to non-destructively "fix" a design here, so all that's on the
roadmap here is to eventually include a gerber-level design rule checker.
As far as board houses go, I have made good experiences with the popular Chinese board houses. In my experience, JLC
will just produce whatever you send them with little fucks being given about design rule adherence or validity of the
input gerbers. This is great if you just want artistic circuit boards without much of a hassle, and you don't care if
they come out exactly as you imagined. The worst I've had happen was when an older version of gerbolyze generated
polygons with holes assuming standard fill-rule processing. The in the board house's online gerber viewer things looked
fine, and neither did they complain during file review. However, the resulting boards looked completely wrong because
all the dark halftones were missing.
PCBWay on the other hand has a much more rigurous file review process. They <em>will</em> complain when you throw
illegal garbage gerbers at them, and they will helpfully guide you through your design rule violations. In this way you
get much more of a professional service from them and for designs that have to be functional their higher level of
scrutiny definitely is a good thing. For the design you saw in the first picture in this article, I ended up begging
them to just plot my files if it doesn't physically break their machines and to their credit, while they seemed unhappy
about it they did it and the result looks absolutely stunning.
PCBWay is a bit more expensive on their lowest-end offering than JLC, but I found that for anything else (large boards,
multi-layer, gold plating etc.) their prices match. PCBWay offers a much broader range of manufacturing options such as
flexible circuit boards, multi-layer boards, thick or thin substrates and high-temperature substrates.
When in doubt about how your design is going to come out on the board, do not hesitate to contact your board house. Most
of the end customer-facing online PCB services have a number of different factories that do a number of different
fabrication processes for them depending on order parameters. Places like PCBWay have exceptional quality control and
good customer service, but that is mostly focused on the technical aspects of the PCB. If you rely on visual aspects
like silkscreen uniformity or solder mask color that is a strong no concern to everyone else in the electronics
industry, you may find significant variations between manufacturers or even between orders with the same manufacturer
and you may encounter challenges communicating your requirements.
Limitations
-----------
SVG raster features
~~~~~~~~~~~~~~~~~~~
Currently, SVG masks and filters are not supported. Though SVG is marketed as a "vector graphics format", these two
features are really raster primitives that all SVG viewers perform at the pixel level after rasterization. Since
supporting these would likely not end up looking like what you want, it is not a planned feature. If you need masks or
filters, simply export the relevant parts of the SVG as a PNG then include that in your template.
Gerber pass-through
~~~~~~~~~~~~~~~~~~~
Since gerbolyze has to composite your input gerbers with its own output, it has to fully parse and re-serialize them.
gerbolyze uses pcb-tools_ and pcb-tools-extension_ for all its gerber parsing needs. Both seem well-written, but likely
not free of bugs. This means that in rare cases information may get lost during this round trip. Thus, *always* check
the output files for errors before submitting them to production.
Gerbolyze is provided without any warranty, but still please open an issue or `send me an email
<mailto:gerbolyze@jaseg.de>`__ if you find any errors or inconsistencies.
Trace/Space design rule adherence
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
While the grayscale halftone vectorizers do a reasonable job adhering to a given trace/space design rule, they can still
produce small parts of output that violate it. For the contour vectorizer as well as for all SVG primitives, you are
responsible for adhering to design rules yourself as there is no algorithm that gerboyze could use to "fix" its input.
A design rule checker is planned as a future addition to gerbolyze, but is not yet part of it. If in doubt, talk to your
fab and consider doing a test run of your design before ordering assembled boards ;)
Gallery
-------
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample2.jpg
.. image:: pics/sample3.jpg
:width: 400px
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample3.jpg
Licensing
---------
This tool is licensed under the rather radical AGPLv3 license. Briefly, this means that you have to provide users of a
webapp using this tool in the backend with this tool's source.
I get that some people have issues with the AGPL. In case this license prevents you from using this software, please
send me `an email <mailto:agpl.sucks@jaseg.de>`__ and I can grant you an exception. I want this software to be useful to as
many people as possible and I wouldn't want the license to be a hurdle to anyone. OTOH I see a danger of some cheap
board house just integrating a fork into their webpage without providing their changes back upstream, and I want to
avoid that so the default license is still AGPL.
.. _usvg: https://github.com/RazrFalcon/resvg
.. _Inkscape: https://inkscape.org/
.. _pcb-tools: https://github.com/curtacircuitos/pcb-tools
.. _pcb-tools-extension: https://github.com/opiopan/pcb-tools-extension
.. _GIMP: https://gimp.org/

8
TODO Normal file
View file

@ -0,0 +1,8 @@
[ ] Do not just return "error 255" if usvg is not installed
[ ] Straighten up svg-flatten input unit handling
[ ] split up python code into modules
[ ] Add backwards-compatible vectorize drop-in
[ ] Figure out handling of drill layers
[ ] Re-publish my own pcb-tools, pcb-tools-extension forks with actual maintenance
[ ] For pattern rendering: validate pattern origin aligns with what the svg spec expects
[ ] Invert SVG color interpretation (use saturation maybe? or sat * val?)

File diff suppressed because one or more lines are too long

View file

@ -1,372 +0,0 @@
#!/usr/bin/env python3
import tempfile
import os.path as path
import os
import sys
import time
import shutil
import math
import gerber
from gerber.render.cairo_backend import GerberCairoContext
import numpy as np
import cv2
import enum
import tqdm
def generate_mask(
outline,
target,
scale,
bounds,
debugimg,
status_print,
extend_overlay_r_mil,
subtract_gerber
):
# Render all gerber layers whose features are to be excluded from the target image, such as board outline, the
# original silk layer and the solder paste layer to binary images.
with tempfile.TemporaryDirectory() as tmpdir:
img_file = path.join(tmpdir, 'target.png')
status_print('Combining keepout composite')
fg, bg = gerber.render.RenderSettings((1, 1, 1)), gerber.render.RenderSettings((0, 0, 0))
ctx = GerberCairoContext(scale=scale)
status_print(' * outline')
ctx.render_layer(outline, settings=fg, bgsettings=bg, bounds=bounds)
status_print(' * target layer')
ctx.render_layer(target, settings=fg, bgsettings=bg, bounds=bounds)
for fn, sub in subtract_gerber:
status_print(' * extra layer', os.path.basename(fn))
layer = gerber.loads(sub)
ctx.render_layer(layer, settings=fg, bgsettings=bg, bounds=bounds)
status_print('Rendering keepout composite')
ctx.dump(img_file)
# Vertically flip exported image
original_img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)[::-1, :]
f = 1 if outline.units == 'inch' else 25.4
r = 1+2*max(1, int(extend_overlay_r_mil/1000 * f * scale))
status_print('Expanding keepout composite by', r)
# Extend image by a few pixels and flood-fill from (0, 0) to mask out the area outside the outermost outline
# This ensures no polygons are generated outside the board even for non-rectangular boards.
border = 10
outh, outw = original_img.shape
extended_img = np.zeros((outh + 2*border, outw + 2*border), dtype=np.uint8)
extended_img[border:outh+border, border:outw+border] = original_img
debugimg(extended_img, 'outline')
cv2.floodFill(extended_img, None, (0, 0), (255,))
original_img = extended_img[border:outh+border, border:outw+border]
debugimg(extended_img, 'flooded')
# Dilate the white areas of the image using gaussian blur and threshold. Use these instead of primitive dilation
# here for their non-directionality.
target_img = cv2.blur(original_img, (r, r))
_, target_img = cv2.threshold(target_img, 255//(1+r), 255, cv2.THRESH_BINARY)
return target_img
def render_gerbers_to_image(*gerbers, scale, bounds=None):
with tempfile.TemporaryDirectory() as tmpdir:
img_file = path.join(tmpdir, 'target.png')
fg, bg = gerber.render.RenderSettings((1, 1, 1)), gerber.render.RenderSettings((0, 0, 0))
ctx = GerberCairoContext(scale=scale)
for grb in gerbers:
ctx.render_layer(grb, settings=fg, bgsettings=bg, bounds=bounds)
ctx.dump(img_file)
# Vertically flip exported image to align coordinate systems
return cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)[::-1, :]
def pcb_area_mask(outline, scale, bounds):
# Merge layers to target mask
img = render_gerbers_to_image(outline, scale=scale, bounds=bounds)
# Extend
imgh, imgw = img.shape
img_ext = np.zeros(shape=(imgh+2, imgw+2), dtype=np.uint8)
img_ext[1:-1, 1:-1] = img
# Binarize
img_ext[img_ext < 128] = 0
img_ext[img_ext >= 128] = 255
# Flood-fill
cv2.floodFill(img_ext, None, (0, 0), (255,)) # Flood-fill with white from top left corner (0,0)
img_ext_snap = img_ext.copy()
cv2.floodFill(img_ext, None, (0, 0), (0,)) # Flood-fill with black
cv2.floodFill(img_ext, None, (0, 0), (255,)) # Flood-fill with white
return np.logical_xor(img_ext_snap, img_ext)[1:-1, 1:-1].astype(float)
def generate_template(
silk, mask, copper, outline, drill,
image,
process_resolution:float=6, # mil
resolution_oversampling:float=10, # times
status_print=lambda *args:None
):
silk, mask, copper, outline, *drill = map(gerber.load_layer_data, [silk, mask, copper, outline, *drill])
silk.layer_class = 'topsilk'
mask.layer_class = 'topmask'
copper.layer_class = 'top'
outline.layer_class = 'outline'
f = 1.0 if outline.cam_source.units == 'metric' else 25.4
scale = (1000/process_resolution) / 25.4 * resolution_oversampling * f # dpmm
bounds = outline.cam_source.bounding_box
# Create a new drawing context
ctx = GerberCairoContext(scale=scale)
ctx.render_layer(outline, bounds=bounds)
ctx.render_layer(copper, bounds=bounds)
ctx.render_layer(mask, bounds=bounds)
ctx.render_layer(silk, bounds=bounds)
for dr in drill:
ctx.render_layer(dr, bounds=bounds)
ctx.dump(image)
def paste_image(
target_gerber:str,
outline_gerber:str,
source_img:np.ndarray,
subtract_gerber:list=[],
extend_overlay_r_mil:float=6,
extend_picture_r_mil:float=2,
status_print=lambda *args:None,
debugdir:str=None):
debugctr = 0
def debugimg(img, name):
nonlocal debugctr
if debugdir:
cv2.imwrite(path.join(debugdir, '{:02d}{}.png'.format(debugctr, name)), img)
debugctr += 1
# Parse outline layer to get bounds of gerber file
status_print('Parsing outline gerber')
outline = gerber.loads(outline_gerber)
bounds = (minx, maxx), (miny, maxy) = outline.bounding_box
grbw, grbh = maxx - minx, maxy - miny
status_print(' * outline has offset {}, size {}'.format((minx, miny), (grbw, grbh)))
# Parse target layer
status_print('Parsing target gerber')
target = gerber.loads(target_gerber)
(tminx, tmaxx), (tminy, tmaxy) = target.bounding_box
status_print(' * target layer has offset {}, size {}'.format((tminx, tminy), (tmaxx-tminx, tmaxy-tminy)))
# Read source image
imgh, imgw = source_img.shape
scale = math.ceil(max(imgw/grbw, imgh/grbh)) # scale is in dpmm
status_print(' * source image has size {}, going for scale {}dpmm'.format((imgw, imgh), scale))
# Merge layers to target mask
target_img = generate_mask(outline, target, scale, bounds, debugimg, status_print, extend_overlay_r_mil, subtract_gerber)
# Threshold source image. Ideally, the source image is already binary but in case it's not, or in case it's not
# exactly binary (having a few very dark or very light grays e.g. due to JPEG compression) we're thresholding here.
status_print('Thresholding source image')
qr = 1+2*max(1, int(extend_picture_r_mil/1000 * scale))
source_img = source_img[::-1]
_, source_img = cv2.threshold(source_img, 127, 255, cv2.THRESH_BINARY)
debugimg(source_img, 'thresh')
# Pad image to size of target layer images generated above. After this, `scale` applies to the padded image as well
# as the gerber renders. For padding, zoom or shrink the image to completely fit the gerber's rectangular bounding
# box. Center the image vertically or horizontally if it has a different aspect ratio.
status_print('Padding source image')
tgth, tgtw = target_img.shape
padded_img = np.zeros(shape=target_img.shape, dtype=source_img.dtype)
offx = int((minx-tminx if tminx < minx else 0)*scale)
offy = int((miny-tminy if tminy < miny else 0)*scale)
offx += int(grbw*scale - imgw) // 2
offy += int(grbh*scale - imgh) // 2
endx, endy = min(offx+imgw, tgtw), min(offy+imgh, tgth)
print('off', (offx, offy), 'end', (endx, endy), 'img', (imgw, imgh), 'tgt', (tgtw, tgth))
padded_img[offy:endy, offx:endx] = source_img[:endy-offy, :endx-offx]
debugimg(padded_img, 'padded')
debugimg(target_img, 'target')
# Mask out excluded gerber features (source silk, holes, solder mask etc.) from the target image
status_print('Masking source image')
out_img = (np.multiply((padded_img/255.0), (target_img/255.0) * -1 + 1) * 255).astype(np.uint8)
debugimg(out_img, 'multiplied')
# Calculate contours from masked target image and plot them to the target gerber context
status_print('Calculating contour lines')
plot_contours(out_img,
target,
offx=(minx, miny),
scale=scale,
status_print=lambda *args: status_print(' ', *args))
# Write target gerber context to disk
status_print('Generating output gerber')
from gerber.render import rs274x_backend
ctx = rs274x_backend.Rs274xContext(target.settings)
target.render(ctx)
out = ctx.dump().getvalue()
status_print('Done.')
return out
def plot_contours(
img:np.ndarray,
layer:gerber.rs274x.GerberFile,
offx:tuple,
scale:float,
debug=lambda *args:None,
status_print=lambda *args:None):
from gerber.primitives import Line, Region, Circle
imgh, imgw = img.shape
# Extract contour hierarchy using OpenCV
status_print('Extracting contours')
# See https://stackoverflow.com/questions/48291581/how-to-use-cv2-findcontours-in-different-opencv-versions/48292371
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_TC89_KCOS)[-2:]
aperture = list(layer.apertures)[0] if layer.apertures else Circle(None, 0.10)
status_print('offx', offx, 'scale', scale)
xbias, ybias = offx
def map(coord):
x, y = coord
return (x/scale + xbias, y/scale + ybias)
def contour_lines(c):
return [ Line(map(start), map(end), aperture, units=layer.settings.units)
for start, end in zip(c, np.vstack((c[1:], c[:1]))) ]
done = []
process_stack = [-1]
next_process_stack = []
parents = [ (i, first_child != -1, parent) for i, (_1, _2, first_child, parent) in enumerate(hierarchy[0]) ]
is_dark = True
status_print('Converting contours to gerber primitives')
with tqdm.tqdm(total=len(contours)) as progress:
while len(done) != len(contours):
for i, has_children, parent in parents[:]:
if parent in process_stack:
contour = contours[i]
polarity = 'dark' if is_dark else 'clear'
debug('rendering {} with parent {} as {} with {} vertices'.format(i, parent, polarity, len(contour)))
debug('process_stack is', process_stack)
debug()
layer.primitives.append(Region(contour_lines(contour[:,0]), level_polarity=polarity, units=layer.settings.units))
if has_children:
next_process_stack.append(i)
done.append(i)
parents.remove((i, has_children, parent))
progress.update(1)
debug('skipping to next level')
process_stack, next_process_stack = next_process_stack, []
is_dark = not is_dark
debug('done', done)
# Utility foo
# ===========
def find_gerber_in_dir(dir_path, extensions, exclude=''):
contents = os.listdir(dir_path)
exts = extensions.split('|')
excs = exclude.split('|')
for entry in contents:
if any(entry.lower().endswith(ext.lower()) for ext in exts) and not any(entry.lower().endswith(ex) for ex in excs if exclude):
lname = path.join(dir_path, entry)
if not path.isfile(lname):
continue
with open(lname, 'r') as f:
return lname, f.read()
raise ValueError(f'Cannot find file with suffix {extensions} in dir {dir_path}')
# Gerber file name extensions for Altium/Protel | KiCAD | Eagle
LAYER_SPEC = {
'top': {
'paste': '.gtp|-F_Paste.gbr|-F.Paste.gbr|.pmc',
'silk': '.gto|-F_SilkS.gbr|-F.SilkS.gbr|.plc',
'mask': '.gts|-F_Mask.gbr|-F.Mask.gbr|.stc',
'copper': '.gtl|-F_Cu.gbr|-F.Cu.gbr|.cmp',
'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb',
},
'bottom': {
'paste': '.gbp|-B_Paste.gbr|-B.Paste.gbr|.pms',
'silk': '.gbo|-B_SilkS.gbr|-B.SilkS.gbr|.pls',
'mask': '.gbs|-B_Mask.gbr|-B.Mask.gbr|.sts',
'copper': '.gbl|-B_Cu.gbr|-B.Cu.gbr|.sol',
'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb'
},
}
# Command line interface
# ======================
def process_gerbers(source, target, image, side, layer, debugdir):
if not os.path.isdir(source):
raise ValueError(f'Given source "{source}" is not a directory.')
# Load input files
source_img = cv2.imread(image, cv2.IMREAD_GRAYSCALE)
if source_img is None:
print(f'"{image}" is not a valid image file', file=sys.stderr)
sys.exit(1)
tlayer, slayer = {
'silk': ('silk', 'mask'),
'mask': ('mask', 'silk'),
'copper': ('copper', None)
}[layer]
layers = LAYER_SPEC[side]
tname, tgrb = find_gerber_in_dir(source, layers[tlayer])
print('Target layer file {}'.format(os.path.basename(tname)))
oname, ogrb = find_gerber_in_dir(source, layers['outline'])
print('Outline layer file {}'.format(os.path.basename(oname)))
subtract = find_gerber_in_dir(source, layers[slayer]) if slayer else None
# Prepare output. Do this now to error out as early as possible if there's a problem.
if os.path.exists(target):
if os.path.isdir(target) and sorted(os.listdir(target)) == sorted(os.listdir(source)):
shutil.rmtree(target)
else:
print('Error: Target already exists and does not look like source. Please manually remove the target dir before proceeding.', file=sys.stderr)
sys.exit(1)
# Generate output
out = paste_image(tgrb, ogrb, source_img, [subtract], debugdir=debugdir, status_print=lambda *args: print(*args, flush=True))
shutil.copytree(source, target)
with open(os.path.join(target, os.path.basename(tname)), 'w') as f:
f.write(out)
def render_preview(source, image, side, process_resolution, resolution_oversampling):
def load_layer(layer):
name, grb = find_gerber_in_dir(source, LAYER_SPEC[side][layer])
print(f'{layer} layer file {os.path.basename(name)}')
return grb
outline = load_layer('outline')
silk = load_layer('silk')
mask = load_layer('mask')
copper = load_layer('copper')
try:
nm, npth = find_gerber_in_dir(source, '-npth.drl')
print(f'npth drill file {nm}')
except ValueError:
npth = None
nm, drill = find_gerber_in_dir(source, '.drl|.txt', exclude='-npth.drl')
print(f'drill file {nm}')
drill = ([npth] if npth else []) + [drill]
generate_template(
silk, mask, copper, outline, drill,
image,
process_resolution=process_resolution,
resolution_oversampling=resolution_oversampling,
)

667
gerbolyze/gerbolyze.py Executable file
View file

@ -0,0 +1,667 @@
import tempfile
import os.path as path
from pathlib import Path
import textwrap
import subprocess
import functools
import os
import base64
import re
import sys
import warnings
import shutil
from zipfile import ZipFile, is_zipfile
from lxml import etree
import gerber
from gerber.render.cairo_backend import GerberCairoContext
import gerberex
import gerberex.rs274x
import numpy as np
import click
from slugify import slugify
@click.group()
def cli():
pass
@cli.command()
@click.option('-l', '--layer', type=click.Choice(['silk', 'mask', 'copper']), default='silk')
@click.option('-x', '--exact', flag_value=True)
@click.option('--trace-space', type=float, default=0.1, help='passed through to svg-flatten')
@click.argument('side', type=click.Choice(['top', 'bottom']))
@click.argument('source')
@click.argument('target')
@click.argument('image')
@click.pass_context
def vectorize(ctx, side, layer, exact, source, target, image, trace_space):
""" Compatibility command for Gerbolyze version 1.
This command is deprecated, please update your code to call "gerbolyze paste" instead.
"""
ctx.invoke(paste,
input_gerbers=source,
output_gerbers=target,
**{side: image, f'layer_{side}': layer},
no_subtract=exact,
trace_space=trace_space,
vectorizer='binary-contours',
preserve_aspect_ratio='meet')
@cli.command()
@click.argument('input_gerbers')
@click.argument('output_gerbers')
@click.option('-t', '--top', help='Top side SVG or PNG overlay')
@click.option('-b', '--bottom', help='Bottom side SVG or PNG overlay')
@click.option('--layer-top', help='Top side SVG or PNG target layer. Default: Map SVG layers to Gerber layers, map PNG to Silk.')
@click.option('--layer-bottom', help='Bottom side SVG or PNG target layer. See --layer-top.')
@click.option('--bbox', help='Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR '
'"x,y,w,h" to force [w] mm by [h] mm output canvas with its bottom left corner at the given input gerber '
'coördinates. MUST MATCH --bbox GIVEN TO PREVIEW')
@click.option('--dilate', default=0.1, help='Default dilation for subtraction operations in mm')
@click.option('--no-subtract', 'no_subtract', flag_value=True, help='Disable subtraction')
@click.option('--subtract', help='Use user subtraction script from argument (see description above)')
@click.option('--trace-space', type=float, default=0.1, help='passed through to svg-flatten')
@click.option('--vectorizer', help='passed through to svg-flatten')
@click.option('--vectorizer-map', help='passed through to svg-flatten')
@click.option('--preserve-aspect-ratio', help='PNG/JPG files only: passed through to svg-flatten')
@click.option('--exclude-groups', help='passed through to svg-flatten')
def paste(input_gerbers, output_gerbers,
top, bottom, layer_top, layer_bottom,
bbox,
dilate, no_subtract, subtract,
preserve_aspect_ratio,
trace_space, vectorizer, vectorizer_map, exclude_groups):
""" Render vector data and raster images from SVG file into gerbers. """
if no_subtract:
subtract_map = {}
else:
subtract_map = parse_subtract_script(subtract, dilate)
if not top and not bottom:
raise click.UsageError('Either --top or --bottom must be given')
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
output_gerbers = Path(output_gerbers)
input_gerbers = Path(input_gerbers)
source = unpack_if_necessary(input_gerbers, tmpdir)
matches = match_gerbers_in_dir(source)
if input_gerbers.is_dir():
# Create output dir if it does not exist yet
output_gerbers.mkdir(exist_ok=True)
# In case output dir already existed, remove files we will overwrite
for in_file in source.iterdir():
out_cand = output_gerbers / in_file.name
out_cand.unlink(missing_ok=True)
for side, in_svg_or_png, target_layer in [
('top', top, layer_top),
('bottom', bottom, layer_bottom)]:
if not in_svg_or_png:
continue
if Path(in_svg_or_png).suffix.lower() in ['.png', '.jpg'] and target_layer is None:
target_layer = 'silk'
print()
print('#########################################')
print('processing side', side, 'infile', in_svg_or_png)
print('#########################################')
print()
if not matches[side]:
warnings.warn(f'No input gerber files found for {side} side')
continue
try:
units, layers = load_side(matches[side])
except SystemError as e:
raise click.UsageError(e.args)
print('loaded layers:', list(layers.keys()))
bounds = get_bounds(bbox, layers)
print('bounds:', bounds)
@functools.lru_cache()
def do_dilate(layer, amount):
print('dilating', layer, 'by', amount)
outfile = tmpdir / f'dilated-{layer}-{amount}.gbr'
dilate_gerber(layers, layer, amount, bbox, tmpdir, outfile, units)
gbr = gerberex.read(str(outfile))
gbr.offset(bounds[0][0], bounds[1][0])
return gbr
for layer, input_files in layers.items():
if layer == 'drill':
continue
if target_layer is not None:
if layer != target_layer:
continue
(in_grb_path, in_grb), = input_files
print()
print('-----------------------------------------')
print('processing side', side, 'layer', layer)
print('-----------------------------------------')
print()
print('rendering layer', layer)
overlay_file = tmpdir / f'overlay-{side}-{layer}.gbr'
layer_arg = layer if target_layer is None else None # slightly confusing but trust me :)
svg_to_gerber(in_svg_or_png, overlay_file, layer_arg,
trace_space, vectorizer, vectorizer_map, exclude_groups,
bounds_for_png=bounds, preserve_aspect_ratio=preserve_aspect_ratio)
overlay_grb = gerberex.read(str(overlay_file))
if not overlay_grb.primitives:
print(f'Overlay layer {layer} does not contain anything. Skipping.', file=sys.stderr)
continue
print('compositing')
comp = gerberex.GerberComposition()
# overlay on bottom
overlay_grb.offset(bounds[0][0], bounds[1][0])
comp.merge(overlay_grb)
# dilated subtract layers on top of overlay
dilations = subtract_map.get(layer, [])
for d_layer, amount in dilations:
print('processing dilation', d_layer, amount)
dilated = do_dilate(d_layer, amount)
comp.merge(dilated)
# input on top of everything
comp.merge(gerberex.rs274x.GerberFile.from_gerber_file(in_grb.cam_source))
if input_gerbers.is_dir():
this_out = output_gerbers / in_grb_path.name
else:
this_out = output_gerbers
print('dumping to', this_out)
comp.dump(this_out)
if input_gerbers.is_dir():
for in_file in source.iterdir():
out_cand = output_gerbers / in_file.name
if not out_cand.is_file():
print(f'Input file {in_file.name} remained unprocessed. Copying.', file=sys.stderr)
shutil.copy(in_file, out_cand)
@cli.command()
@click.argument('input')
@click.option('-t' ,'--top', help='Top layer output file.')
@click.option('-b' ,'--bottom', help='Bottom layer output file. --top or --bottom may be given at once. If neither is given, autogenerate filenames.')
@click.option('--vector/--raster', help='Embed preview renders into output file as SVG vector graphics instead of rendering them to PNG bitmaps. The resulting preview may slow down your SVG editor.')
@click.option('--raster-dpi', type=float, default=300.0, help='DPI for rastering preview')
@click.option('--bbox', help='Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR '
'"x,y,w,h" to force [w] mm by [h] mm output canvas with its bottom left corner at the given input gerber '
'coördinates.')
def template(input, top, bottom, bbox, vector, raster_dpi):
''' Generate SVG template for gerbolyze paste from gerber files.
INPUT may be a gerber file, directory of gerber files or zip file with gerber files
'''
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
source = Path(input)
if not top and not bottom: # autogenerate two file names if neither --top nor --bottom are given
# /path/to/gerber/dir -> /path/to/gerber/dir.preview-{top|bottom}.svg
# /path/to/gerbers.zip -> /path/to/gerbers.zip.preview-{top|bottom}.svg
# /path/to/single/file.grb -> /path/to/single/file.grb.preview-{top|bottom}.svg
outfiles = {
'top': source.parent / f'{source.name}.preview-top.svg',
'bottom': source.parent / f'{source.name}.preview-top.svg' }
else:
outfiles = {
'top': Path(top) if top else None,
'bottom': Path(bottom) if bottom else None }
source = unpack_if_necessary(source, tmpdir)
matches = match_gerbers_in_dir(source)
for side in ('top', 'bottom'):
if not outfiles[side]:
continue
if not matches[side]:
warnings.warn(f'No input gerber files found for {side} side')
continue
try:
units, layers = load_side(matches[side])
except SystemError as e:
raise click.UsageError(e.args)
# cairo-svg uses a hardcoded dpi value of 72. pcb-tools does something weird, so we have to scale things
# here.
scale = 1/25.4 if units == 'metric' else 1.0 # pcb-tools gerber scale
scale *= CAIRO_SVG_HARDCODED_DPI
if not vector: # adapt scale for png export
scale *= raster_dpi / CAIRO_SVG_HARDCODED_DPI
bounds = get_bounds(bbox, layers)
ctx = GerberCairoContext(scale=scale)
for layer_name in LAYER_RENDER_ORDER:
for _path, to_render in layers.get(layer_name, ()):
ctx.render_layer(to_render, bounds=bounds)
filetype = 'svg' if vector else 'png'
tmp_render = tmpdir / f'intermediate-{side}.{filetype}'
ctx.dump(str(tmp_render))
if vector:
with open(tmp_render, 'rb') as f:
svg_data = f.read()
with open(outfiles[side], 'wb') as f:
f.write(create_template_from_svg(bounds, svg_data))
else: # raster
with open(tmp_render, 'rb') as f:
png_data = f.read()
with open(outfiles[side], 'w') as f:
f.write(template_svg_for_png(bounds, png_data))
# Subtraction script handling
#============================
DEFAULT_SUB_SCRIPT = '''
out.silk -= in.mask
out.silk -= in.silk+0.5
out.mask -= in.mask+0.5
out.copper -= in.copper+0.5
'''
def parse_subtract_script(script, default_dilation=0.1):
if script is None:
script = DEFAULT_SUB_SCRIPT
subtract_script = {}
lines = script.replace(';', '\n').splitlines()
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
continue
line = line.lower()
line = re.sub('\s', '', line)
# out.copper -= in.copper+0.1
varname = r'([a-z]+\.[a-z]+)'
floatnum = r'([+-][.0-9]+)'
match = re.fullmatch(fr'{varname}-={varname}{floatnum}?', line)
if not match:
raise ValueError(f'Cannot parse line: {line}')
out_var, in_var, dilation = match.groups()
if not out_var.startswith('out.') or not in_var.startswith('in.'):
raise ValueError('All left-hand side values must be outputs, right-hand side values must be inputs.')
_out, _, out_layer = out_var.partition('.')
_in, _, in_layer = in_var.partition('.')
dilation = float(dilation) if dilation else default_dilation
subtract_script[out_layer] = subtract_script.get(out_layer, []) + [(in_layer, dilation)]
return subtract_script
# Parameter parsing foo
#======================
def parse_bbox(bbox):
if not bbox:
return None
elems = [ int(elem) for elem in re.split('[,/ ]', bbox) ]
if len(elems) not in (2, 4):
raise click.BadParameter(
'--bbox must be either two floating-point values like: w,h or four like: x,y,w,h')
elems = [ float(e) for e in elems ]
if len(elems) == 2:
bounds = [0, 0, *elems]
else:
bounds = elems
# now transform bounds to the format pcb-tools uses. Instead of (x, y, w, h) or even (x1, y1, x2, y2), that
# is ((x1, x2), (y1, y2)
x, y, w, h = bounds
return ((x, x+w), (y, y+h))
def bounds_from_outline(layers):
''' NOTE: When the user has not set explicit bounds, we automatically extract the design's bounding box from the
input gerber files. If a folder is used as input, we use the outline gerber and barf if we can't find one. If only a
single file is given, we simply use that file's bounding box
We have to do things this way since gerber files do not have explicit bounds listed.
Note that the bounding box extracted from the outline layer usually will be one outline layer stroke widht larger in
all directions than the finished board.
'''
if 'outline' in layers:
outline_files = layers['outline']
_path, grb = outline_files[0]
return calculate_apertureless_bounding_box(grb.cam_source)
elif len(layers) == 1:
first_layer, *rest = layers.values()
first_file, *rest = first_layer
_path, grb = first_file
return grb.cam_source.bounding_box
else:
raise click.UsageError('Cannot find an outline file and no --bbox given.')
def get_bounds(bbox, layers):
bounds = parse_bbox(bbox)
if bounds:
return bounds
return bounds_from_outline(layers)
# Utility foo
# ===========
# Gerber file name extensions for Altium/Protel | KiCAD | Eagle
# Note that in case of KiCAD these extensions occassionally change without notice. If you discover that this list is not
# up to date, please know that it's not my fault and submit an issue or send me an email.
LAYER_SPEC = {
'top': {
'paste': '.gtp|-F_Paste.gbr|-F.Paste.gbr|.pmc',
'silk': '.gto|-F_Silkscreen.gbr|-F_SilkS.gbr|-F.SilkS.gbr|.plc',
'mask': '.gts|-F_Mask.gbr|-F.Mask.gbr|.stc',
'copper': '.gtl|-F_Cu.gbr|-F.Cu.gbr|.cmp',
'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb',
'drill': '.drl|.txt|-npth.drl',
},
'bottom': {
'paste': '.gbp|-B_Paste.gbr|-B.Paste.gbr|.pms',
'silk': '.gbo|-B_Silkscreen.gbr|-B_SilkS.gbr|-B.SilkS.gbr|.pls',
'mask': '.gbs|-B_Mask.gbr|-B.Mask.gbr|.sts',
'copper': '.gbl|-B_Cu.gbr|-B.Cu.gbr|.sol',
'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb',
'drill': '.drl|.txt|-npth.drl',
},
}
# Maps keys from LAYER_SPEC to pcb-tools layer classes (see pcb-tools'es gerber/layers.py)
LAYER_CLASSES = {
'silk': 'topsilk',
'mask': 'topmask',
'paste': 'toppaste',
'copper': 'top',
'outline': 'outline',
'drill': 'drill',
}
LAYER_RENDER_ORDER = [ 'copper', 'mask', 'silk', 'paste', 'outline', 'drill' ]
def match_gerbers_in_dir(path):
out = {}
for side, layers in LAYER_SPEC.items():
out[side] = {}
for layer, match in layers.items():
l = list(find_gerber_in_dir(path, match))
if l:
out[side][layer] = l
return out
def find_gerber_in_dir(path, extensions):
exts = extensions.split('|')
for entry in path.iterdir():
if not entry.is_file():
continue
if any(entry.name.lower().endswith(suffix.lower()) for suffix in exts):
yield entry
def calculate_apertureless_bounding_box(cam):
''' pcb-tools'es default bounding box function returns the bounding box of the primitives including apertures (i.e.
line widths). For determining a board's size from the outline layer, we want the bounding box disregarding
apertures.
'''
min_x = min_y = 1000000
max_x = max_y = -1000000
for prim in cam.primitives:
bounds = prim.bounding_box_no_aperture
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 unpack_if_necessary(source, tmpdir, dirname='input'):
""" Handle command-line input paths. If path points to a directory, return unchanged. If path points to a zip file,
unpack to a directory inside tmpdir and return that. If path points to a file that is not a zip, copy that file into
a subdir of tmpdir and return that subdir. """
# If source is not a directory with gerber files (-> zip/single gerber), make it one
if not source.is_dir():
tmp_indir = tmpdir / dirname
tmp_indir.mkdir()
if source.suffix.lower() == '.zip' or is_zipfile(source):
with ZipFile(source) as f:
f.extractall(path=tmp_indir)
else: # single input file
shutil.copy(source, tmp_indir)
return tmp_indir
else:
return source
def load_side(side_matches):
""" Load all gerber files for one side returned by match_gerbers_in_dir. """
def load(layer, path):
print('loading', layer, 'layer from:', path)
grb = gerber.load_layer(str(path))
grb.layer_class = LAYER_CLASSES.get(layer, 'unknown')
return grb
layers = { layer: [ (path, load(layer, path)) for path in files ]
for layer, files in side_matches.items() }
for layer, elems in layers.items():
if len(elems) > 1 and layer != 'drill':
raise SystemError(f'Multiple files found for layer {layer}: {", ".join(str(x) for x in side_matches[layer]) }')
unitses = set(layer.cam_source.units for items in layers.values() for _path, layer in items)
if len(unitses) != 1:
# FIXME: we should ideally be able to deal with this. We'll have to figure out a way to update a
# GerberCairoContext's scale in between layers.
raise SystemError('Input gerber files mix metric and imperial units. Please fix your export.')
units, = unitses
return units, layers
# SVG export
#===========
DEFAULT_EXTRA_LAYERS = [ layer for layer in LAYER_RENDER_ORDER if layer != "drill" ]
def template_layer(name):
return f'<g id="g-{slugify(name)}" inkscape:label="{name}" inkscape:groupmode="layer"></g>'
def template_svg_for_png(bounds, png_data, extra_layers=DEFAULT_EXTRA_LAYERS):
(x1, x2), (y1, y2) = bounds
w_mm, h_mm = (x2 - x1), (y2 - y1)
extra_layers = "\n ".join(template_layer(name) for name in extra_layers)
# we set up the viewport such that document dimensions = document units = mm
template = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="{w_mm}mm" height="{h_mm}mm" viewBox="0 0 {w_mm} {h_mm}" >
<defs/>
<g inkscape:label="Preview" inkscape:groupmode="layer" id="g-preview" sodipodi:insensitive="true" style="opacity:0.5">
<image x="0" y="0" width="{w_mm}" height="{h_mm}"
xlink:href="data:image/jpeg;base64,{base64.b64encode(png_data).decode()}" />
</g>
{extra_layers}
</svg>
'''
return textwrap.dedent(template)
# this is fixed, we cannot tell cairo-svg to use some other value. we just have to work around it.
CAIRO_SVG_HARDCODED_DPI = 72.0
MM_PER_INCH = 25.4
def svg_pt_to_mm(pt_len, dpi=CAIRO_SVG_HARDCODED_DPI):
if pt_len.endswith('pt'):
pt_len = pt_len[:-2]
return f'{float(pt_len) / dpi * MM_PER_INCH}mm'
def create_template_from_svg(bounds, svg_data, extra_layers=DEFAULT_EXTRA_LAYERS):
svg = etree.fromstring(svg_data)
# add inkscape namespaces
SVG_NS = '{http://www.w3.org/2000/svg}'
INKSCAPE_NS = 'http://www.inkscape.org/namespaces/inkscape'
SODIPODI_NS = 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'
# glObAL stAtE YaY
etree.register_namespace('inkscape', INKSCAPE_NS)
etree.register_namespace('sodipodi', SODIPODI_NS)
INKSCAPE_NS = '{'+INKSCAPE_NS+'}'
SODIPODI_NS = '{'+SODIPODI_NS+'}'
# convert document units to mm
svg.set('width', svg_pt_to_mm(svg.get('width')))
svg.set('height', svg_pt_to_mm(svg.get('height')))
# make original group an inkscape layer
orig_g = svg.find(SVG_NS+'g')
orig_g.set('id', 'g-preview')
orig_g.set(INKSCAPE_NS+'label', 'Preview')
orig_g.set(SODIPODI_NS+'insensitive', 'true') # lock group
orig_g.set('style', 'opacity:0.5')
# add layers
for layer in extra_layers:
new_g = etree.SubElement(svg, SVG_NS+'g')
new_g.set('id', f'g-{slugify(layer)}')
new_g.set(INKSCAPE_NS+'label', layer)
new_g.set(INKSCAPE_NS+'groupmode', 'layer')
return etree.tostring(svg)
# SVG/gerber import
#==================
def dilate_gerber(layers, layer_name, dilation, bbox, tmpdir, outfile, units):
if layer_name not in layers:
raise ValueError(f'Cannot dilate layer {layer_name}: layer not found in input dir')
bounds = get_bounds(bbox, layers)
(x_min_mm, x_max_mm), (y_min_mm, y_max_mm) = bounds
origin_x = x_min_mm / MM_PER_INCH
origin_y = y_min_mm / MM_PER_INCH
width = (x_max_mm - x_min_mm) / MM_PER_INCH
height = (y_max_mm - y_min_mm) / MM_PER_INCH
tmpfile = tmpdir / 'dilate-tmp.svg'
path, _gbr = layers[layer_name][0]
# NOTE: gerbv has an undocumented maximum length of 20 chars for the arguments to --origin and --window_inch
cmd = ['gerbv', '-x', 'svg',
'--border=0',
f'--origin={origin_x:.6f}x{origin_y:.6f}', f'--window_inch={width:.6f}x{height:.6f}',
'--foreground=#ffffff',
'-o', str(tmpfile), str(path)]
print('dilation cmd:', ' '.join(cmd))
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# dilate & render back to gerber
# TODO: the scale parameter is a hack. ideally we would fix svg-flatten to handle input units correctly.
svg_to_gerber(tmpfile, outfile, dilate=-dilation*72.0/25.4, dpi=72, scale=25.4/72.0)
def svg_to_gerber(infile, outfile,
layer=None, trace_space:'mm'=0.1,
vectorizer=None, vectorizer_map=None,
exclude_groups=None,
dilate=None,
dpi=None, scale=None, bounds_for_png=None,
preserve_aspect_ratio=None,
force_png=False, force_svg=False):
infile = Path(infile)
if 'SVG_FLATTEN' in os.environ:
candidates = [os.environ['SVG_FLATTEN']]
else:
# By default, try three options:
candidates = [
# somewhere in $PATH
'svg-flatten',
# in user-local pip installation
Path.home() / '.local' / 'bin' / 'svg-flatten',
# next to our current python interpreter (e.g. in virtualenv
str(Path(sys.executable).parent / 'svg-flatten'),
# next to this python source file in the development repo
str(Path(__file__).parent.parent / 'svg-flatten' / 'build' / 'svg-flatten') ]
args = [ '--format', 'gerber',
'--precision', '6', # intermediate file, use higher than necessary precision
'--trace-space', str(trace_space) ]
if layer:
args += ['--only-groups', f'g-{slugify(layer)}']
if vectorizer:
args += ['--vectorizer', vectorizer]
if vectorizer_map:
args += ['--vectorizer-map', vectorizer_map]
if exclude_groups:
args += ['--exclude-groups', exclude_groups]
if dilate:
args += ['--dilate', str(dilate)]
if dpi:
args += ['--usvg-dpi', str(dpi)]
if scale:
args += ['--scale', str(scale)]
if force_png or (infile.suffix.lower() in ['.jpg', '.png'] and not force_svg):
(min_x, max_x), (min_y, max_y) = bounds_for_png
args += ['--size', f'{max_x - min_x}x{max_y - min_y}']
if force_svg and force_png:
raise ValueError('both force_svg and force_png given')
if force_svg:
args += ['--force-svg']
if force_png:
args += ['--force-png']
if preserve_aspect_ratio:
args += ['--preserve-aspect-ratio', preserve_aspect_ratio]
args += [str(infile), str(outfile)]
print('svg-flatten args:', ' '.join(args))
for candidate in candidates:
try:
res = subprocess.run([candidate, *args], check=True)
print('used svg-flatten:', candidate)
break
except FileNotFoundError:
continue
else:
raise SystemError('svg-flatten executable not found')
if __name__ == '__main__':
cli()

View file

@ -1,4 +0,0 @@
*_secret.txt
*_apikey.txt
playbook.retry
credentials.ini

View file

@ -1,60 +0,0 @@
---
- name: Set local path facts
set_fact:
image: "/var/lib/machines/{{ container }}.img"
root: "/var/lib/machines/{{ container }}"
"{{container}}_root": "/var/lib/machines/{{ container }}"
- name: Create container image file
command: truncate -s 4G "{{image}}"
args:
creates: "{{image}}"
register: create_container
- name: Download arch bootstrap image
get_url:
url: http://mirror.rackspace.com/archlinux/iso/2020.03.01/archlinux-bootstrap-2020.03.01-x86_64.tar.gz
dest: /tmp/arch-bootstrap.tar.xz
checksum: sha256:49c7aa8718e48f5a4ec570624520fa50616ed3e044af101ec3aa16c155136f82
when: create_container is changed
- name: Create container image filesystem
filesystem:
dev: "{{image}}"
fstype: btrfs
- name: Create container image fstab entry
mount:
src: "{{image}}"
path: "{{root}}"
state: mounted
fstype: btrfs
opts: loop
- name: Unpack bootstrap image
unarchive:
remote_src: yes
src: /tmp/arch-bootstrap.tar.xz
dest: "{{root}}"
extra_opts: --strip-components=1
creates: "{{root}}/etc"
- name: Copy mirrorlist into container
copy:
src: mirrorlist
dest: "{{root}}/etc/pacman.d/mirrorlist"
- name: Initialize container pacman keyring
shell: arch-chroot "{{root}}" pacman-key --init && arch-chroot "{{root}}" pacman-key --populate archlinux
args:
creates: "{{root}}/etc/pacman.d/gnupg"
- name: Fixup pacman.conf for pacman to work in chroot without its own root fs
lineinfile:
path: "{{root}}/etc/pacman.conf"
regexp: '^CheckSpace'
line: '#CheckSpace'
- name: Update container and install software
shell: arch-chroot "{{root}}" pacman -Syu --noconfirm

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View file

@ -1,20 +0,0 @@
css=/cgit.css
logo= /cgit.png
enable-http-clone=1
robots=noindex, nofollow
virtual-root=/
readme=:README.rst
about-filter=/usr/libexec/cgit/filters/about-formatting.sh
enable-index-links=1
enable-commit-grpah=1
enable-log-filecount=1
enable-log-linecount=1
enable-git-config=1
source-filter=/usr/libexec/cgit/filters/syntax-highlighting.py
project-list=/var/lib/gitolite3/projects.list
scan-path=/var/lib/gitolite3/repositories

@ -1 +0,0 @@
Subproject commit 13a57211f0d0feb34b452b3e19be83a095707ed6

View file

@ -1,36 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1+
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=Clippy container
PartOf=machines.target
Before=machines.target
After=network.target systemd-resolved.service
RequiresMountsFor=/var/lib/machines
[Service]
ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --ephemeral --boot -U --settings=override --machine=clippy
KillMode=mixed
Type=notify
RestartForceExitStatus=133
SuccessExitStatus=133
WatchdogSec=3min
Slice=machine.slice
Delegate=yes
TasksMax=512
# Enforce a strict device policy, similar to the one nspawn configures when it
# allocates its own scope unit. Make sure to keep these policies in sync if you
# change them!
DevicePolicy=closed
DeviceAllow=/dev/net/tun rwm
DeviceAllow=char-pts rw
[Install]
WantedBy=machines.target

View file

@ -1,2 +0,0 @@
[Network]
VirtualEthernet=no

View file

@ -1,9 +0,0 @@
[Unit]
Description=Clippy listener daemon
[Service]
WorkingDirectory=/var/lib/clippy.git
ExecStart=/usr/bin/python3 clippy.py -s -x 60x30 -e
[Install]
WantedBy=multi-user.target

View file

@ -1,3 +0,0 @@
[inwx]
user=...
pass=...

View file

@ -1,9 +0,0 @@
[Unit]
Description=Gerboweb gerber job processor
[Service]
WorkingDirectory=/var/lib/gerboweb
ExecStart=/usr/bin/python3 job_processor.py {{gerboweb_cache}}/job_queue.sqlite3
[Install]
WantedBy=uwsgi-app@gerboweb.service

View file

@ -1,4 +0,0 @@
MAX_CONTENT_LENGTH=10000000
SECRET_KEY="{{lookup('password', 'gerboweb_flask_secret.txt length=32')}}"
UPLOAD_PATH="{{gerboweb_cache}}/upload"
JOB_QUEUE_DB="{{gerboweb_cache}}/job_queue.sqlite3"

View file

@ -1,202 +0,0 @@
# configuration variables for gitolite
# This file is in perl syntax. But you do NOT need to know perl to edit it --
# just mind the commas, use single quotes unless you know what you're doing,
# and make sure the brackets and braces stay matched up!
# (Tip: perl allows a comma after the last item in a list also!)
# HELP for commands can be had by running the command with "-h".
# HELP for all the other FEATURES can be found in the documentation (look for
# "list of non-core programs shipped with gitolite" in the master index) or
# directly in the corresponding source file.
%RC = (
# ------------------------------------------------------------------
# default umask gives you perms of '0700'; see the rc file docs for
# how/why you might change this
UMASK => 0027,
# look for "git-config" in the documentation
GIT_CONFIG_KEYS => 'core\.sharedRepository',
# comment out if you don't need all the extra detail in the logfile
LOG_EXTRA => 1,
# logging options
# 1. leave this section as is for 'normal' gitolite logging (default)
# 2. uncomment this line to log ONLY to syslog:
# LOG_DEST => 'syslog',
# 3. uncomment this line to log to syslog and the normal gitolite log:
# LOG_DEST => 'syslog,normal',
# 4. prefixing "repo-log," to any of the above will **also** log just the
# update records to "gl-log" in the bare repo directory:
# LOG_DEST => 'repo-log,normal',
# LOG_DEST => 'repo-log,syslog',
# LOG_DEST => 'repo-log,syslog,normal',
# syslog 'facility': defaults to 'local0', uncomment if needed. For example:
# LOG_FACILITY => 'local4',
# roles. add more roles (like MANAGER, TESTER, ...) here.
# WARNING: if you make changes to this hash, you MUST run 'gitolite
# compile' afterward, and possibly also 'gitolite trigger POST_COMPILE'
ROLES => {
READERS => 1,
WRITERS => 1,
},
# enable caching (currently only Redis). PLEASE RTFM BEFORE USING!!!
# CACHE => 'Redis',
# ------------------------------------------------------------------
# rc variables used by various features
# the 'info' command prints this as additional info, if it is set
# SITE_INFO => 'Please see http://blahblah/gitolite for more help',
# the CpuTime feature uses these
# display user, system, and elapsed times to user after each git operation
# DISPLAY_CPU_TIME => 1,
# display a warning if total CPU times (u, s, cu, cs) crosses this limit
# CPU_TIME_WARN_LIMIT => 0.1,
# the Mirroring feature needs this
# HOSTNAME => "foo",
# TTL for redis cache; PLEASE SEE DOCUMENTATION BEFORE UNCOMMENTING!
# CACHE_TTL => 600,
# ------------------------------------------------------------------
# suggested locations for site-local gitolite code (see cust.html)
# this one is managed directly on the server
# LOCAL_CODE => "$ENV{HOME}/local",
# or you can use this, which lets you put everything in a subdirectory
# called "local" in your gitolite-admin repo. For a SECURITY WARNING
# on this, see http://gitolite.com/gitolite/non-core.html#pushcode
# LOCAL_CODE => "$rc{GL_ADMIN_BASE}/local",
# ------------------------------------------------------------------
# List of commands and features to enable
ENABLE => [
# COMMANDS
# These are the commands enabled by default
'help',
'desc',
'info',
'perms',
'writable',
# Uncomment or add new commands here.
# 'create',
# 'fork',
# 'mirror',
# 'readme',
# 'sskm',
'D',
# These FEATURES are enabled by default.
# essential (unless you're using smart-http mode)
'ssh-authkeys',
# creates git-config entries from gitolite.conf file entries like 'config foo.bar = baz'
'git-config',
# creates git-daemon-export-ok files; if you don't use git-daemon, comment this out
'daemon',
# creates projects.list file; if you don't use gitweb, comment this out
'gitweb',
# These FEATURES are disabled by default; uncomment to enable. If you
# need to add new ones, ask on the mailing list :-)
# user-visible behaviour
# prevent wild repos auto-create on fetch/clone
# 'no-create-on-read',
# no auto-create at all (don't forget to enable the 'create' command!)
# 'no-auto-create',
# access a repo by another (possibly legacy) name
# 'Alias',
# give some users direct shell access. See documentation in
# sts.html for details on the following two choices.
# "Shell $ENV{HOME}/.gitolite.shell-users",
# 'Shell alice bob',
# set default roles from lines like 'option default.roles-1 = ...', etc.
# 'set-default-roles',
# show more detailed messages on deny
# 'expand-deny-messages',
# show a message of the day
# 'Motd',
# system admin stuff
# enable mirroring (don't forget to set the HOSTNAME too!)
# 'Mirroring',
# allow people to submit pub files with more than one key in them
# 'ssh-authkeys-split',
# selective read control hack
# 'partial-copy',
# manage local, gitolite-controlled, copies of read-only upstream repos
# 'upstream',
# updates 'description' file instead of 'gitweb.description' config item
# 'cgit',
# allow repo-specific hooks to be added
# 'repo-specific-hooks',
# performance, logging, monitoring...
# be nice
# 'renice 10',
# log CPU times (user, system, cumulative user, cumulative system)
# 'CpuTime',
# syntactic_sugar for gitolite.conf and included files
# allow backslash-escaped continuation lines in gitolite.conf
# 'continuation-lines',
# create implicit user groups from directory names in keydir/
# 'keysubdirs-as-groups',
# allow simple line-oriented macros
# 'macros',
# Kindergarten mode
# disallow various things that sensible people shouldn't be doing anyway
# 'Kindergarten',
],
);
# ------------------------------------------------------------------------------
# per perl rules, this should be the last line in such a file:
1;
# Local variables:
# mode: perl
# End:
# vim: set syn=perl:

View file

@ -1,11 +0,0 @@
---
all:
hosts:
wendelstein:
ansible_host: wendelstein.jaseg.net
ansible_ssh_identity_file: ~/.ssh/id_ed25519
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
localhost:
ansible_connection: local
ansible_python_interpreter: "{{ansible_playbook_python}}"

View file

@ -1,27 +0,0 @@
# Generated by iptables-save v1.8.0 on Thu Apr 4 11:06:33 2019
*nat
:PREROUTING ACCEPT [13:648]
:INPUT ACCEPT [8:440]
:OUTPUT ACCEPT [18:1260]
:POSTROUTING ACCEPT [18:1260]
-A PREROUTING -i eth0 -p tcp -m tcp --dport 23 -j REDIRECT --to-ports 2342
COMMIT
# Completed on Thu Apr 4 11:06:33 2019
# Generated by iptables-save v1.8.0 on Thu Apr 4 11:06:33 2019
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [360:761646]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 2342 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 23 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT
-A INPUT -p udp --dport 53 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
COMMIT
# Completed on Thu Apr 4 11:06:33 2019

@ -1 +0,0 @@
Subproject commit 0ac040da14cc9d834098addc03cd8d4d26647df0

View file

@ -1,474 +0,0 @@
##
## Arch Linux repository mirrorlist
## Generated on 2017-06-06
##
## Worldwide
#Server = https://archlinux.surlyjake.com/archlinux/$repo/os/$arch
#Server = http://mirrors.evowise.com/archlinux/$repo/os/$arch
Server = http://mirror.rackspace.com/archlinux/$repo/os/$arch
## Australia
#Server = https://mirror.aarnet.edu.au/pub/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch
#Server = http://ftp.iinet.net.au/pub/archlinux/$repo/os/$arch
#Server = http://mirror.internode.on.net/pub/archlinux/$repo/os/$arch
#Server = http://ftp.swin.edu.au/archlinux/$repo/os/$arch
#Server = http://archlinux.uberglobalmirror.com/$repo/os/$arch
## Austria
#Server = http://mirror.digitalnova.at/archlinux/$repo/os/$arch
#Server = http://mirror.easyname.at/archlinux/$repo/os/$arch
#Server = http://mirror1.htu.tugraz.at/archlinux/$repo/os/$arch
## Belarus
#Server = http://ftp.byfly.by/pub/archlinux/$repo/os/$arch
#Server = http://mirror.datacenter.by/pub/archlinux/$repo/os/$arch
## Belgium
#Server = http://archlinux.cu.be/$repo/os/$arch
#Server = http://archlinux.mirror.kangaroot.net/$repo/os/$arch
## Bosnia and Herzegovina
#Server = http://burek.archlinux.ba/$repo/os/$arch
#Server = http://archlinux.mirror.ba/$repo/os/$arch
## Brazil
#Server = http://br.mirror.archlinux-br.org/$repo/os/$arch
#Server = http://archlinux.c3sl.ufpr.br/$repo/os/$arch
#Server = http://linorg.usp.br/archlinux/$repo/os/$arch
#Server = http://pet.inf.ufsc.br/mirrors/archlinux/$repo/os/$arch
#Server = http://archlinux.pop-es.rnp.br/$repo/os/$arch
## Bulgaria
#Server = http://mirror.host.ag/archlinux/$repo/os/$arch
#Server = http://mirrors.netix.net/archlinux/$repo/os/$arch
#Server = http://mirror.telepoint.bg/archlinux/$repo/os/$arch
#Server = http://mirrors.uni-plovdiv.net/archlinux/$repo/os/$arch
#Server = https://mirrors.uni-plovdiv.net/archlinux/$repo/os/$arch
## Canada
#Server = http://mirror.cedille.club/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.colo-serv.net/$repo/os/$arch
#Server = http://mirror.csclub.uwaterloo.ca/archlinux/$repo/os/$arch
#Server = https://mirror.csclub.uwaterloo.ca/archlinux/$repo/os/$arch
#Server = http://mirror.frgl.pw/archlinux/$repo/os/$arch
#Server = https://mirror.frgl.pw/archlinux/$repo/os/$arch
#Server = http://mirror.its.dal.ca/archlinux/$repo/os/$arch
#Server = http://muug.ca/mirror/archlinux/$repo/os/$arch
#Server = https://muug.ca/mirror/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.rafal.ca/$repo/os/$arch
## Chile
#Server = http://mirror.archlinux.cl/$repo/os/$arch
## China
#Server = http://mirrors.163.com/archlinux/$repo/os/$arch
#Server = http://mirror.lzu.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.neusoft.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.skyshe.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.xjtu.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.xjtu.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.zju.edu.cn/archlinux/$repo/os/$arch
## Colombia
#Server = http://mirror.edatel.net.co/archlinux/$repo/os/$arch
#Server = http://mirror.upb.edu.co/archlinux/$repo/os/$arch
## Croatia
#Server = http://archlinux.iskon.hr/$repo/os/$arch
## Czech Republic
#Server = http://mirror.dkm.cz/archlinux/$repo/os/$arch
#Server = https://mirror.dkm.cz/archlinux/$repo/os/$arch
#Server = http://ftp.fi.muni.cz/pub/linux/arch/$repo/os/$arch
#Server = http://ftp.linux.cz/pub/linux/arch/$repo/os/$arch
#Server = http://gluttony.sin.cvut.cz/arch/$repo/os/$arch
#Server = https://gluttony.sin.cvut.cz/arch/$repo/os/$arch
#Server = http://mirrors.nic.cz/archlinux/$repo/os/$arch
#Server = http://ftp.sh.cvut.cz/arch/$repo/os/$arch
#Server = https://ftp.sh.cvut.cz/arch/$repo/os/$arch
#Server = http://mirror.vpsfree.cz/archlinux/$repo/os/$arch
## Denmark
#Server = http://mirrors.dotsrc.org/archlinux/$repo/os/$arch
#Server = https://mirrors.dotsrc.org/archlinux/$repo/os/$arch
#Server = http://ftp.klid.dk/ftp/archlinux/$repo/os/$arch
#Server = http://mirror.one.com/archlinux/$repo/os/$arch
#Server = https://mirror.one.com/archlinux/$repo/os/$arch
## Ecuador
#Server = http://mirror.cedia.org.ec/archlinux/$repo/os/$arch
#Server = http://mirror.espoch.edu.ec/archlinux/$repo/os/$arch
#Server = http://mirror.uta.edu.ec/archlinux/$repo/os/$arch
## Finland
#Server = http://arch.mirror.far.fi/$repo/os/$arch
## France
#Server = http://archlinux.de-labrusse.fr/$repo/os/$arch
#Server = http://mirror.archlinux.ikoula.com/archlinux/$repo/os/$arch
#Server = http://archlinux.vi-di.fr/$repo/os/$arch
#Server = https://archlinux.vi-di.fr/$repo/os/$arch
#Server = http://mirror.armbrust.me/archlinux/$repo/os/$arch
#Server = https://mirror.armbrust.me/archlinux/$repo/os/$arch
#Server = https://archlinux.ec-tech.fr/$repo/os/$arch
#Server = http://fooo.biz/archlinux/$repo/os/$arch
#Server = https://fooo.biz/archlinux/$repo/os/$arch
#Server = http://mirror.gerhard.re/archlinux/$repo/os/$arch
#Server = http://mirror.ibcp.fr/pub/archlinux/$repo/os/$arch
#Server = http://mirror.lastmikoi.net/archlinux/$repo/os/$arch
#Server = http://archlinux.mailtunnel.eu/$repo/os/$arch
#Server = https://www.mailtunnel.eu/archlinux/$repo/os/$arch
#Server = http://mir.archlinux.fr/$repo/os/$arch
#Server = http://archlinux.mirrors.ovh.net/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.pkern.at/$repo/os/$arch
#Server = https://archlinux.mirror.pkern.at/$repo/os/$arch
#Server = http://archlinux.polymorf.fr/$repo/os/$arch
#Server = http://mirrors.standaloneinstaller.com/archlinux/$repo/os/$arch
#Server = http://arch.tamcore.eu/$repo/os/$arch
#Server = http://mirror.tyborek.pl/arch/$repo/os/$arch
#Server = https://mirror.tyborek.pl/arch/$repo/os/$arch
#Server = http://ftp.u-strasbg.fr/linux/distributions/archlinux/$repo/os/$arch
#Server = https://mirror.wormhole.eu/archlinux/$repo/os/$arch
#Server = http://arch.yourlabs.org/$repo/os/$arch
## Germany
#Server = http://mirror.23media.de/archlinux/$repo/os/$arch
#Server = https://arch.32g.eu/$repo/os/$arch
#Server = http://artfiles.org/archlinux.org/$repo/os/$arch
#Server = https://fabric-mirror.vps.hosteurope.de/archlinux/$repo/os/$arch
#Server = https://mirror.bethselamin.de/$repo/os/$arch
#Server = http://mirror.euserv.net/linux/archlinux/$repo/os/$arch
#Server = http://mirror.f4st.host/archlinux/$repo/os/$arch
#Server = https://mirror.f4st.host/archlinux/$repo/os/$arch
#Server = http://ftp.fau.de/archlinux/$repo/os/$arch
#Server = https://ftp.fau.de/archlinux/$repo/os/$arch
#Server = http://mirror.fluxent.de/archlinux/$repo/os/$arch
#Server = https://mirror.fluxent.de/archlinux/$repo/os/$arch
#Server = http://mirror.gnomus.de/$repo/os/$arch
#Server = http://www.gutscheindrache.com/mirror/archlinux/$repo/os/$arch
#Server = http://ftp.gwdg.de/pub/linux/archlinux/$repo/os/$arch
#Server = http://mirror.hactar.xyz/$repo/os/$arch
#Server = https://mirror.hactar.xyz/$repo/os/$arch
#Server = http://archlinux.honkgong.info/$repo/os/$arch
#Server = http://ftp.hosteurope.de/mirror/ftp.archlinux.org/$repo/os/$arch
#Server = http://ftp-stud.hs-esslingen.de/pub/Mirrors/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.iphh.net/$repo/os/$arch
#Server = http://repo.itmettke.de/archlinux/$repo/os/$arch
#Server = https://repo.itmettke.de/archlinux/$repo/os/$arch
#Server = https://mirror.jankoppe.de/archlinux/$repo/os/$arch
#Server = http://arch.jensgutermuth.de/$repo/os/$arch
#Server = https://arch.jensgutermuth.de/$repo/os/$arch
#Server = http://mirror.js-webcoding.de/pub/archlinux/$repo/os/$arch
#Server = https://mirror.js-webcoding.de/pub/archlinux/$repo/os/$arch
#Server = http://k42.ch/mirror/archlinux/$repo/os/$arch
#Server = https://k42.ch/mirror/archlinux/$repo/os/$arch
#Server = http://mirror.de.leaseweb.net/archlinux/$repo/os/$arch
Server = https://mirror.de.leaseweb.net/archlinux/$repo/os/$arch
#Server = http://mirror.loli.forsale/arch/$repo/os/$arch
#Server = https://mirror.loli.forsale/arch/$repo/os/$arch
#Server = http://mirror.metalgamer.eu/archlinux/$repo/os/$arch
#Server = https://mirror.metalgamer.eu/archlinux/$repo/os/$arch
#Server = http://mirror.michael-eckert.net/archlinux/$repo/os/$arch
#Server = https://mirror.michael-eckert.net/archlinux/$repo/os/$arch
#Server = http://mirrors.n-ix.net/archlinux/$repo/os/$arch
#Server = https://mirrors.n-ix.net/archlinux/$repo/os/$arch
#Server = http://mirror.netcologne.de/archlinux/$repo/os/$arch
Server = https://mirror.netcologne.de/archlinux/$repo/os/$arch
#Server = http://mirrors.niyawe.de/archlinux/$repo/os/$arch
#Server = https://mirrors.niyawe.de/archlinux/$repo/os/$arch
#Server = http://archlinux.nullpointer.io/$repo/os/$arch
#Server = https://archlinux.nullpointer.io/$repo/os/$arch
#Server = http://mirror.pseudoform.org/$repo/os/$arch
#Server = https://mirror.pseudoform.org/$repo/os/$arch
#Server = https://www.ratenzahlung.de/mirror/archlinux/$repo/os/$arch
#Server = http://ftp.halifax.rwth-aachen.de/archlinux/$repo/os/$arch
#Server = http://linux.rz.rub.de/archlinux/$repo/os/$arch
#Server = http://mirror.selfnet.de/archlinux/$repo/os/$arch
#Server = http://ftp.spline.inf.fu-berlin.de/mirrors/archlinux/$repo/os/$arch
#Server = https://ftp.spline.inf.fu-berlin.de/mirrors/archlinux/$repo/os/$arch
#Server = http://archlinux.thaller.ws/$repo/os/$arch
#Server = https://archlinux.thaller.ws/$repo/os/$arch
#Server = http://archlinux.thelinuxnetworx.rocks/$repo/os/$arch
#Server = https://archlinux.thelinuxnetworx.rocks/$repo/os/$arch
#Server = http://archmirror.tomforb.es/$repo/os/$arch
#Server = https://archmirror.tomforb.es/$repo/os/$arch
#Server = http://ftp.tu-chemnitz.de/pub/linux/archlinux/$repo/os/$arch
#Server = http://mirror.ubrco.de/archlinux/$repo/os/$arch
#Server = https://mirror.ubrco.de/archlinux/$repo/os/$arch
#Server = http://ftp.uni-bayreuth.de/linux/archlinux/$repo/os/$arch
#Server = http://ftp.uni-hannover.de/archlinux/$repo/os/$arch
#Server = http://ftp.uni-kl.de/pub/linux/archlinux/$repo/os/$arch
#Server = http://mirror.united-gameserver.de/archlinux/$repo/os/$arch
#Server = http://mirror.vfn-nrw.de/archlinux/$repo/os/$arch
#Server = https://mirror.vfn-nrw.de/archlinux/$repo/os/$arch
## Greece
#Server = http://ftp.cc.uoc.gr/mirrors/linux/archlinux/$repo/os/$arch
#Server = http://foss.aueb.gr/mirrors/linux/archlinux/$repo/os/$arch
#Server = https://foss.aueb.gr/mirrors/linux/archlinux/$repo/os/$arch
#Server = http://mirrors.myaegean.gr/linux/archlinux/$repo/os/$arch
#Server = http://ftp.ntua.gr/pub/linux/archlinux/$repo/os/$arch
#Server = http://ftp.otenet.gr/linux/archlinux/$repo/os/$arch
## Hong Kong
#Server = http://arch-mirror.wtako.net/$repo/os/$arch
#Server = https://arch-mirror.wtako.net/$repo/os/$arch
## Hungary
#Server = http://ftp.energia.mta.hu/pub/mirrors/ftp.archlinux.org/$repo/os/$arch
#Server = http://archmirror.hbit.sztaki.hu/archlinux/$repo/os/$arch
## Iceland
#Server = http://mirror.system.is/arch/$repo/os/$arch
#Server = https://mirror.system.is/arch/$repo/os/$arch
## India
#Server = http://mirror.cse.iitk.ac.in/archlinux/$repo/os/$arch
#Server = http://ftp.iitm.ac.in/archlinux/$repo/os/$arch
## Indonesia
#Server = http://mirror.devilzc0de.org/archlinux/$repo/os/$arch
#Server = http://mirror.poliwangi.ac.id/archlinux/$repo/os/$arch
#Server = http://suro.ubaya.ac.id/archlinux/$repo/os/$arch
## Iran
#Server = http://repo.sadjad.ac.ir/arch/$repo/os/$arch
#Server = https://repo.sadjad.ac.ir/arch/$repo/os/$arch
## Ireland
#Server = http://ftp.heanet.ie/mirrors/ftp.archlinux.org/$repo/os/$arch
#Server = https://ftp.heanet.ie/mirrors/ftp.archlinux.org/$repo/os/$arch
## Israel
#Server = http://mirror.isoc.org.il/pub/archlinux/$repo/os/$arch
## Italy
#Server = http://archlinux.prometeolibero.eu/archlinux/$repo/os/$arch
#Server = https://archlinux.prometeolibero.eu/archlinux/$repo/os/$arch
#Server = https://archlinux.beccacervello.it/archlinux/$repo/os/$arch
#Server = http://mi.mirror.garr.it/mirrors/archlinux/$repo/os/$arch
#Server = http://mirrors.prometeus.net/archlinux/$repo/os/$arch
#Server = http://archlinux.students.cs.unibo.it/$repo/os/$arch
## Japan
#Server = http://ftp.tsukuba.wide.ad.jp/Linux/archlinux/$repo/os/$arch
Server = http://ftp.jaist.ac.jp/pub/Linux/ArchLinux/$repo/os/$arch
## Kazakhstan
#Server = http://mirror.neolabs.kz/archlinux/$repo/os/$arch
## Latvia
#Server = http://archlinux.koyanet.lv/archlinux/$repo/os/$arch
## Lithuania
#Server = http://mirrors.atviras.lt/archlinux/$repo/os/$arch
#Server = https://mirrors.atviras.lt/archlinux/$repo/os/$arch
## Luxembourg
#Server = http://archlinux.mirror.root.lu/$repo/os/$arch
## Macedonia
#Server = http://arch.softver.org.mk/archlinux/$repo/os/$arch
#Server = http://mirror.t-home.mk/archlinux/$repo/os/$arch
#Server = https://mirror.t-home.mk/archlinux/$repo/os/$arch
## Netherlands
#Server = http://arch.apt-get.eu/$repo/os/$arch
#Server = http://mirror.i3d.net/pub/archlinux/$repo/os/$arch
#Server = https://mirror.i3d.net/pub/archlinux/$repo/os/$arch
#Server = http://mirror.nl.leaseweb.net/archlinux/$repo/os/$arch
#Server = https://mirror.nl.leaseweb.net/archlinux/$repo/os/$arch
#Server = http://mirror.netrouting.net/archlinux/$repo/os/$arch
#Server = http://ftp.nluug.nl/os/Linux/distr/archlinux/$repo/os/$arch
#Server = http://ftp.snt.utwente.nl/pub/os/linux/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.wearetriple.com/$repo/os/$arch
#Server = https://archlinux.mirror.wearetriple.com/$repo/os/$arch
## New Caledonia
#Server = http://mirror.lagoon.nc/pub/archlinux/$repo/os/$arch
#Server = http://archlinux.nautile.nc/archlinux/$repo/os/$arch
## New Zealand
#Server = https://mirror.smith.geek.nz/archlinux/$repo/os/$arch
## Norway
#Server = http://mirror.archlinux.no/$repo/os/$arch
#Server = http://archlinux.uib.no/$repo/os/$arch
#Server = http://mirror.neuf.no/archlinux/$repo/os/$arch
#Server = https://mirror.neuf.no/archlinux/$repo/os/$arch
## Philippines
#Server = http://mirror.rise.ph/archlinux/$repo/os/$arch
## Poland
#Server = http://mirror.chmuri.net/archmirror/$repo/os/$arch
#Server = http://arch.midov.pl/arch/$repo/os/$arch
#Server = http://mirror.onet.pl/pub/mirrors/archlinux/$repo/os/$arch
#Server = http://piotrkosoft.net/pub/mirrors/ftp.archlinux.org/$repo/os/$arch
#Server = http://ftp.vectranet.pl/archlinux/$repo/os/$arch
## Portugal
#Server = http://glua.ua.pt/pub/archlinux/$repo/os/$arch
#Server = https://glua.ua.pt/pub/archlinux/$repo/os/$arch
#Server = http://ftp.rnl.tecnico.ulisboa.pt/pub/archlinux/$repo/os/$arch
## Qatar
#Server = http://mirror.qnren.qa/archlinux/$repo/os/$arch
## Romania
#Server = http://mirror.archlinux.ro/archlinux/$repo/os/$arch
#Server = http://archlinux.mirrors.linux.ro/$repo/os/$arch
#Server = http://mirrors.m247.ro/archlinux/$repo/os/$arch
#Server = http://mirrors.pidginhost.com/arch/$repo/os/$arch
## Russia
#Server = http://mirror.aur.rocks/$repo/os/$arch
#Server = https://mirror.aur.rocks/$repo/os/$arch
#Server = http://mirror.rol.ru/archlinux/$repo/os/$arch
#Server = https://mirror.rol.ru/archlinux/$repo/os/$arch
#Server = http://mirror.yandex.ru/archlinux/$repo/os/$arch
#Server = https://mirror.yandex.ru/archlinux/$repo/os/$arch
## Serbia
#Server = http://mirror.pmf.kg.ac.rs/archlinux/$repo/os/$arch
## Singapore
#Server = http://mirror.0x.sg/archlinux/$repo/os/$arch
#Server = http://download.nus.edu.sg/mirror/arch/$repo/os/$arch
## Slovakia
#Server = http://mirror.lnx.sk/pub/linux/archlinux/$repo/os/$arch
#Server = https://mirror.lnx.sk/pub/linux/archlinux/$repo/os/$arch
#Server = http://tux.rainside.sk/archlinux/$repo/os/$arch
## Slovenia
#Server = http://archimonde.ts.si/archlinux/$repo/os/$arch
#Server = https://archimonde.ts.si/archlinux/$repo/os/$arch
## South Africa
#Server = http://za.mirror.archlinux-br.org/$repo/os/$arch
#Server = http://ftp.wa.co.za/pub/archlinux/$repo/os/$arch
#Server = http://mirror.is.co.za/mirror/archlinux.org/$repo/os/$arch
#Server = http://mirror.wbs.co.za/archlinux/$repo/os/$arch
## South Korea
#Server = http://ftp.kaist.ac.kr/ArchLinux/$repo/os/$arch
#Server = http://mirror.premi.st/archlinux/$repo/os/$arch
## Spain
#Server = http://osl.ugr.es/archlinux/$repo/os/$arch
#Server = http://sunsite.rediris.es/mirror/archlinux/$repo/os/$arch
## Sweden
#Server = http://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch
#Server = https://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch
#Server = http://archlinux.dynamict.se/$repo/os/$arch
#Server = https://archlinux.dynamict.se/$repo/os/$arch
#Server = http://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch
#Server = https://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch
#Server = http://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch
#Server = https://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch
#Server = https://mirror.osbeck.com/archlinux/$repo/os/$arch
#Server = http://ftp.portlane.com/pub/os/linux/archlinux/$repo/os/$arch
## Switzerland
#Server = http://pkg.adfinis-sygroup.ch/archlinux/$repo/os/$arch
#Server = https://pkg.adfinis-sygroup.ch/archlinux/$repo/os/$arch
#Server = http://archlinux.puzzle.ch/$repo/os/$arch
## Taiwan
#Server = http://archlinux.cs.nctu.edu.tw/$repo/os/$arch
#Server = http://shadow.ind.ntou.edu.tw/archlinux/$repo/os/$arch
#Server = http://ftp.tku.edu.tw/Linux/ArchLinux/$repo/os/$arch
#Server = http://ftp.yzu.edu.tw/Linux/archlinux/$repo/os/$arch
## Thailand
#Server = http://mirror.adminbannok.com/archlinux/$repo/os/$arch
#Server = http://mirror.kku.ac.th/archlinux/$repo/os/$arch
#Server = https://mirror.kku.ac.th/archlinux/$repo/os/$arch
## Turkey
#Server = http://ftp.linux.org.tr/archlinux/$repo/os/$arch
## Ukraine
#Server = http://archlinux.ip-connect.vn.ua/$repo/os/$arch
#Server = https://archlinux.ip-connect.vn.ua/$repo/os/$arch
#Server = http://mirrors.nix.org.ua/linux/archlinux/$repo/os/$arch
#Server = https://mirrors.nix.org.ua/linux/archlinux/$repo/os/$arch
## United Kingdom
#Server = http://mirror.bytemark.co.uk/archlinux/$repo/os/$arch
#Server = http://mirrors.manchester.m247.com/arch-linux/$repo/os/$arch
#Server = http://www.mirrorservice.org/sites/ftp.archlinux.org/$repo/os/$arch
#Server = http://arch.serverspace.co.uk/arch/$repo/os/$arch
#Server = http://archlinux.mirrors.uk2.net/$repo/os/$arch
## United States
#Server = http://mirrors.acm.wpi.edu/archlinux/$repo/os/$arch
#Server = http://mirrors.advancedhosters.com/archlinux/$repo/os/$arch
#Server = http://mirrors.aggregate.org/archlinux/$repo/os/$arch
#Server = http://ca.us.mirror.archlinux-br.org/$repo/os/$arch
#Server = http://il.us.mirror.archlinux-br.org/$repo/os/$arch
#Server = http://archlinux.surlyjake.com/archlinux/$repo/os/$arch
#Server = http://arlm.tyzoid.com/$repo/os/$arch
#Server = http://mirror.as65535.net/archlinux/$repo/os/$arch
#Server = http://mirrors.cat.pdx.edu/archlinux/$repo/os/$arch
#Server = http://mirror.cc.columbia.edu/pub/linux/archlinux/$repo/os/$arch
#Server = http://arch.mirror.constant.com/$repo/os/$arch
#Server = https://arch.mirror.constant.com/$repo/os/$arch
#Server = http://cosmos.cites.illinois.edu/pub/archlinux/$repo/os/$arch
#Server = http://mirror.cs.pitt.edu/archlinux/$repo/os/$arch
#Server = http://mirror.cs.vt.edu/pub/ArchLinux/$repo/os/$arch
#Server = http://mirror.epiphyte.network/archlinux/$repo/os/$arch
#Server = https://mirror.epiphyte.network/archlinux/$repo/os/$arch
#Server = http://mirror.es.its.nyu.edu/archlinux/$repo/os/$arch
#Server = http://mirrors.gigenet.com/archlinux/$repo/os/$arch
#Server = http://mirror.grig.io/archlinux/$repo/os/$arch
#Server = https://mirror.grig.io/archlinux/$repo/os/$arch
#Server = http://www.gtlib.gatech.edu/pub/archlinux/$repo/os/$arch
#Server = http://mirror1.hackingand.coffee/arch/$repo/os/$arch
#Server = http://mirror2.hackingand.coffee/arch/$repo/os/$arch
#Server = http://mirror3.hackingand.coffee/arch/$repo/os/$arch
#Server = http://mirror.htnshost.com/archlinux/$repo/os/$arch
#Server = http://mirror.jmu.edu/pub/archlinux/$repo/os/$arch
#Server = http://mirrors.kernel.org/archlinux/$repo/os/$arch
#Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch
#Server = http://mirror.us.leaseweb.net/archlinux/$repo/os/$arch
#Server = https://mirror.us.leaseweb.net/archlinux/$repo/os/$arch
#Server = http://il.mirrors.linaxe.net/archlinux/$repo/os/$arch
#Server = http://mirrors.liquidweb.com/archlinux/$repo/os/$arch
#Server = http://arch.localmsp.org/arch/$repo/os/$arch
#Server = https://arch.localmsp.org/arch/$repo/os/$arch
#Server = http://mirror.lty.me/archlinux/$repo/os/$arch
#Server = https://mirror.lty.me/archlinux/$repo/os/$arch
#Server = http://mirrors.lug.mtu.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.lug.mtu.edu/archlinux/$repo/os/$arch
#Server = http://mirror.math.princeton.edu/pub/archlinux/$repo/os/$arch
#Server = http://mirror.metrocast.net/archlinux/$repo/os/$arch
#Server = http://mirror.kaminski.io/archlinux/$repo/os/$arch
#Server = https://mirror.kaminski.io/archlinux/$repo/os/$arch
#Server = http://mirror.nexcess.net/archlinux/$repo/os/$arch
#Server = http://mirrors.ocf.berkeley.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.ocf.berkeley.edu/archlinux/$repo/os/$arch
#Server = http://ftp.osuosl.org/pub/archlinux/$repo/os/$arch
#Server = http://arch.mirrors.pair.com/$repo/os/$arch
#Server = http://mirrors.rit.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.rit.edu/archlinux/$repo/os/$arch
#Server = http://mirrors.rutgers.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.rutgers.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.tuxns.net/archlinux/$repo/os/$arch
#Server = http://mirror.umd.edu/archlinux/$repo/os/$arch
#Server = http://mirror.vtti.vt.edu/archlinux/$repo/os/$arch
#Server = http://mirrors.xmission.com/archlinux/$repo/os/$arch
#Server = http://mirror.yellowfiber.net/archlinux/$repo/os/$arch
## Vietnam
#Server = http://f.archlinuxvn.org/archlinux/$repo/os/$arch
#Server = http://mirror-fpt-telecom.fpt.net/archlinux/$repo/os/$arch

View file

@ -1,412 +0,0 @@
# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
server {
listen 80;
listen [::]:80;
server_name .jaseg.net;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name gerbolyze.jaseg.net;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/gerbolyze.jaseg.net/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/gerbolyze.jaseg.net/privkey.pem";
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_stapling on;
ssl_stapling_verify on;
resolver 67.207.67.2 67.207.67.3 valid=300s;
resolver_timeout 10s;
add_header Strict-Transport-Security "max-age=86400";
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location ^~ /static/ {
root /var/lib/gerboweb;
}
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/gerboweb.socket;
}
error_page 404 /404.html;
location = /40x.html {
root /usr/share/nginx/html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name blog.jaseg.net;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/blog.jaseg.net/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/blog.jaseg.net/privkey.pem";
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_stapling on;
ssl_stapling_verify on;
resolver 67.207.67.2 67.207.67.3 valid=300s;
resolver_timeout 10s;
add_header Strict-Transport-Security "max-age=86400";
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
root /var/www/blog.jaseg.net;
}
location /d/ {
access_log off;
log_not_found off;
rewrite ^/d/(.*)$ /$1 break;
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/secure-download.socket;
}
error_page 404 /404.html;
location = /40x.html {
root /usr/share/nginx/html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name automation.jaseg.de;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/automation.jaseg.de/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/automation.jaseg.de/privkey.pem";
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_stapling on;
ssl_stapling_verify on;
resolver 67.207.67.2 67.207.67.3 valid=300s;
resolver_timeout 10s;
add_header Strict-Transport-Security "max-age=86400";
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/notification-proxy.socket;
}
error_page 404 /404.html;
location = /40x.html {
root /usr/share/nginx/html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name kochbuch.jaseg.net;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/kochbuch.jaseg.net/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/kochbuch.jaseg.net/privkey.pem";
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_stapling on;
ssl_stapling_verify on;
resolver 67.207.67.2 67.207.67.3 valid=300s;
resolver_timeout 10s;
add_header Strict-Transport-Security "max-age=86400";
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
auth_basic "blubb";
auth_basic_user_file /etc/nginx/kochbuch.htpasswd;
root /var/www/kochbuch.jaseg.net;
}
error_page 404 /404.html;
location = /40x.html {
root /usr/share/nginx/html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name pogojig.jaseg.net;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/pogojig.jaseg.net/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/pogojig.jaseg.net/privkey.pem";
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_stapling on;
ssl_stapling_verify on;
resolver 67.207.67.2 67.207.67.3 valid=300s;
resolver_timeout 10s;
client_max_body_size 10M;
add_header Strict-Transport-Security "max-age=86400";
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location ^~ /pogospace/ {
root /var/lib/pogojig/pogospace;
}
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/pogojig.socket;
}
error_page 404 /404.html;
location = /40x.html {
root /usr/share/nginx/html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name tracespace.jaseg.net;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/tracespace.jaseg.net/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/tracespace.jaseg.net/privkey.pem";
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_stapling on;
ssl_stapling_verify on;
resolver 67.207.67.2 67.207.67.3 valid=300s;
resolver_timeout 10s;
add_header Strict-Transport-Security "max-age=86400";
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
root /var/www/tracespace.jaseg.net;
}
error_page 404 /404.html;
location = /40x.html {
root /usr/share/nginx/html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name openjscad.jaseg.net;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/openjscad.jaseg.net/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/openjscad.jaseg.net/privkey.pem";
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_stapling on;
ssl_stapling_verify on;
resolver 67.207.67.2 67.207.67.3 valid=300s;
resolver_timeout 10s;
add_header Strict-Transport-Security "max-age=86400";
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
root /var/www/openjscad.jaseg.net;
}
error_page 404 /404.html;
location = /40x.html {
root /usr/share/nginx/html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name git.jaseg.net;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/git.jaseg.net/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/git.jaseg.net/privkey.pem";
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_stapling on;
ssl_stapling_verify on;
resolver 67.207.67.2 67.207.67.3 valid=300s;
resolver_timeout 10s;
add_header Strict-Transport-Security "max-age=86400";
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location ~ ^/(cgit.css|robots.txt) {
root /usr/share/cgit;
expires 30d;
}
location ~ ^/(cgit.png|favicon.png) {
alias /var/www/git.jaseg.net/cgit.png;
}
location / {
include uwsgi_params;
uwsgi_modifier1 9;
uwsgi_pass unix:/run/uwsgi/cgit.socket;
}
error_page 404 /404.html;
location = /40x.html {
root /usr/share/nginx/html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name dyndns.jaseg.de;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/dyndns.jaseg.de/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/dyndns.jaseg.de/privkey.pem";
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_stapling on;
ssl_stapling_verify on;
resolver 67.207.67.2 67.207.67.3 valid=300s;
resolver_timeout 10s;
add_header Strict-Transport-Security "max-age=86400";
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/dyndns.socket;
}
error_page 404 /404.html;
location = /40x.html {
root /usr/share/nginx/html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}

View file

@ -1,52 +0,0 @@
# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name gerbolyze.jaseg.net;
return 301 https://$host$request_uri;
}
server {
listen 80;
listen [::]:80;
server_name blog.jaseg.net;
return 301 https://$host$request_uri;
}
}

View file

@ -1,179 +0,0 @@
import smtplib
import ssl
import email.utils
import hmac
from email.mime.text import MIMEText
from datetime import datetime
import time
import functools
import json
import binascii
import uwsgidecorators
import sqlite3
from flask import Flask, request, abort
app = Flask(__name__)
app.config.from_pyfile('config.py')
db = sqlite3.connect(app.config['SQLITE_DB'], check_same_thread=False)
with db as conn:
conn.execute('''CREATE TABLE IF NOT EXISTS seqs_seen
(route_name TEXT PRIMARY KEY,
seq INTEGER)''')
conn.execute('''CREATE TABLE IF NOT EXISTS time_seen
(route_name TEXT PRIMARY KEY)''')
conn.execute('''CREATE TABLE IF NOT EXISTS heartbeats_seen
(route_name TEXT PRIMARY KEY,
timestamp INTEGER,
notified INTEGER)''')
# Clear table on startup to avoid spurious notifications
conn.execute('''DELETE FROM heartbeats_seen''')
mail_routes = {}
def mail_route(name, receiver, secret):
def wrap(func):
global routes
mail_routes[name] = (receiver, func, secret)
return func
return wrap
def authenticate(route_name, secret, clock_delta_tolerance:'s'=120):
with db as conn:
if not request.is_json:
print('Rejecting notification: Incorrect content type')
abort(400)
if not 'auth' in request.json and 'payload' in request.json:
print('Rejecting notification: signature or payload not found')
abort(400)
if not isinstance(request.json['auth'], str):
print('Rejecting notification: signature is of incorrect type')
abort(400)
their_digest = binascii.unhexlify(request.json['auth'])
our_digest = hmac.digest(secret.encode('utf-8'), request.json['payload'].encode('utf-8'), 'sha256')
if not hmac.compare_digest(their_digest, our_digest):
print('Rejecting notification: Incorrect signature')
abort(403)
try:
payload = json.loads(request.json['payload'])
except:
print('Rejecting notification: Payload is not JSON')
abort(400)
last_seqnum = conn.execute('SELECT seq FROM seqs_seen WHERE route_name = ?', (route_name,)).fetchone() or 0
# We can check for seq here: Only an attacker with knowledge of the secret would be able to remove
# seq from a message. This means for a single key, only messages with or without seq may ever be used.
if 'seq' in payload:
seq = payload['seq']
if not isinstance(seq, int):
print('Rejecting notification: seq of wrong type')
abort(400)
if seq <= last_seqnum:
print('Rejecting notification: seq out of order')
abort(400)
conn.execute('INSERT OR REPLACE INTO seqs_seen VALUES (?, ?)', (route_name, seq))
elif last_seqnum:
print('Rejecting notification: seq not included but past messages included seq')
abort(400)
msg_time = None
if 'time' in payload:
msg_time = payload['time']
if not isinstance(msg_time, int):
print('Rejecting notification: time of wrong type')
abort(400)
if abs(msg_time - int(time.time())) > clock_delta_tolerance:
print('Rejecting notification: timestamp too far in the future or past')
abort(400)
conn.execute('INSERT OR REPLACE INTO time_seen VALUES (?)', (route_name,))
elif conn.execute('SELECT * FROM time_seen WHERE route_name = ?', (route_name,)).fetchone():
print('Rejecting notification: time not included but past messages included time')
abort(400)
if msg_time is None:
msg_time = int(time.time())
return msg_time, payload['scope'], payload['d']
@mail_route('klingel', 'computerstuff@jaseg.de', app.config['SECRET_KLINGEL'])
def klingel(classification='somewhere', rms=None, capture=None, **kwargs):
return (f'It rang {classification}!',
f'rms={rms}\ncapture={capture}\nextra_args={kwargs}')
def send_mail(route_name, receiver, subject, body):
try:
context = ssl.create_default_context()
smtp = smtplib.SMTP_SSL(app.config['SMTP_HOST'], app.config['SMTP_PORT'])
smtp.login('apikey', app.config['SENDGRID_APIKEY'])
sender = f'{route_name}@{app.config["DOMAIN"]}'
msg = MIMEText(body)
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = receiver
msg['Date'] = email.utils.formatdate()
smtp.sendmail(sender, receiver, msg.as_string())
finally:
smtp.quit()
@app.route('/v1/notify/<route_name>', methods=['POST'])
def notify(route_name):
receiver, func, secret = mail_routes[route_name]
msg_time, scope, kwargs = authenticate(route_name, secret)
if scope == 'default':
# Exceptions will yield a 500 error
subject, body = func(**kwargs)
send_mail(route_name, receiver, subject, body or 'empty message')
elif scope == 'info':
send_mail(route_name, receiver, f'System info: {kwargs["info_msg"]}', f'Logged data: {kwargs}')
elif scope == 'boot':
formatted = datetime.utcfromtimestamp(msg_time).isoformat()
send_mail(route_name, receiver, 'System startup', f'System powered up at {formatted}')
elif scope == 'heartbeat':
with db as conn:
conn.execute('INSERT OR REPLACE INTO heartbeats_seen VALUES (?, ?, 0)', (route_name, int(time.time())))
elif scope == 'error':
print(f'Device error: {kwargs}')
return 'success'
@uwsgidecorators.timer(60)
def heartbeat_timer(_uwsgi_signum):
threshold = int(time.time()) - app.config['HEARTBEAT_TIMEOUT']
with db as conn:
for route, ts in db.execute(
'SELECT route_name, timestamp FROM heartbeats_seen WHERE timestamp <= ? AND notified == 0',
(threshold,)).fetchall():
print(f'Heartbeat expired for {route}: {ts} < {threshold}')
receiver, *_ = mail_routes[route]
last = datetime.utcfromtimestamp(ts).isoformat()
send_mail(route, receiver, 'Heartbeat timeout', f'Last heartbeat at {last}')
db.execute('UPDATE heartbeats_seen SET notified = ? WHERE route_name = ?', (int(time.time()), route))
if __name__ == '__main__':
app.run()

View file

@ -1,9 +0,0 @@
SENDGRID_APIKEY = '{{lookup('file', 'notification_proxy_sendgrid_apikey.txt')}}'
DOMAIN = 'automation.jaseg.de'
SMTP_HOST = "smtp.sendgrid.net"
SMTP_PORT = 465
HEARTBEAT_TIMEOUT = 300
SQLITE_DB = '{{notification_proxy_sqlite_dbfile}}'
SECRET_KLINGEL = '{{lookup('password', 'notification_proxy_klingel_secret.txt length=32')}}'

View file

@ -1,111 +0,0 @@
- name: DNS setup
hosts: localhost
module_defaults:
inwx:
username: "{{lookup('ini', 'user section=inwx file=credentials.ini')}}"
password: "{{lookup('ini', 'pass section=inwx file=credentials.ini')}}"
vars:
subdomains:
- git.jaseg.net
- blog.jaseg.net
- kochbuch.jaseg.net
- gerbolyze.jaseg.net
- tracespace.jaseg.net
- openjscad.jaseg.net
- pogojig.jaseg.net
- automation.jaseg.de
- dyndns.jaseg.de
fastmail_domains:
- jaseg.net
- jaseg.de
tasks:
- name: Gather wendelstein facts
setup:
delegate_to: wendelstein
delegate_facts: True
- name: Setup DNS
include_tasks: dns.yml
- name: Wendelstein setup
hosts: wendelstein
tasks:
- name: Set hostname
hostname:
name: wendelstein.jaseg.net
- name: Install common admin tools
dnf:
name: htop,tmux,fish,mosh,neovim,sqlite
state: latest
- name: Install host requisites
dnf:
name: nginx,uwsgi,python3-flask,python3-flask-wtf,uwsgi-plugin-python3,certbot,python3-certbot-nginx,libselinux-python,git,iptables-services,python3-pycryptodomex,zip,python3-uwsgidecorators,nsd
state: latest
- name: Disable password-based root login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin without-password'
register: disable_root_pw_ssh
- name: Restart sshd
systemd:
name: sshd
state: restarted
when: disable_root_pw_ssh is changed
- name: Configure iptables firewall service
copy:
src: iptables.rules
dest: /etc/sysconfig/iptables
owner: root
group: root
mode: 0664
- name: Enable iptables firewall service
systemd:
name: iptables
enabled: yes
state: started
- name: Create containers
include_tasks: setup_containers.yml
vars:
containers:
- gerboweb
- clippy
- pogojig
- name: Setup web server
include_tasks: setup_webserver.yml
- name: Setup gerboweb
include_tasks: setup_gerboweb.yml
- name: Setup clippy
include_tasks: setup_clippy.yml
- name: Setup secure download
include_tasks: setup_secure_download.yml
- name: Setup tracespace
include_tasks: setup_tracespace.yml
- name: Setup openjscad
include_tasks: setup_openjscad.yml
- name: Setup pogojig
include_tasks: setup_pogojig.yml
- name: Setup notification proxy
include_tasks: setup_notification_proxy.yml
- name: Setup semi-public git server
include_tasks: setup_git.yml
- name: Setup private DynDNS service
include_tasks: setup_dyndns.yml

View file

@ -1,9 +0,0 @@
[Unit]
Description=Pogojig render job processor
[Service]
WorkingDirectory=/var/lib/pogojig
ExecStart=/usr/bin/python3 job_processor.py {{pogojig_cache}}/job_queue.sqlite3
[Install]
WantedBy=uwsgi-app@pogojig.service

View file

@ -1,4 +0,0 @@
MAX_CONTENT_LENGTH=10000000
SECRET_KEY="{{lookup('password', 'pogojig_flask_secret.txt length=32')}}"
UPLOAD_PATH="{{pogojig_cache}}/upload"
JOB_QUEUE_DB="{{pogojig_cache}}/job_queue.sqlite3"

View file

@ -1,25 +0,0 @@
#!/bin/sh
[ $# != 1 ] && exit 1
ID=$1
egrep -x -q '^[-0-9A-Za-z]{36}$'<<<"$ID" || exit 2
systemd-nspawn \
-D {{pogojig_root}} \
-x --bind={{pogojig_cache}}/upload/$ID:/mnt \
/bin/sh -c "set -euo pipefail
cd /mnt
date; echo 'Cleaning up previous output'
rm -rf pcb_shape.dxf jig.stl kicad kicad.zip sources.zip
date; echo 'Rendering'
cp -r /var/lib/pogojig_renderer sources
cp input.svg sources/
make -C sources
date; echo 'Packing source bundle'
cp -r sources/out/pcb_shape.dxf sources/out/jig.stl sources/out/kicad ./
zip -r sources.zip sources
zip -r kicad.zip kicad
rm -rf sources"

View file

@ -1,20 +0,0 @@
#!/bin/sh
[ $# != 1 ] && exit 1
ID=$1
egrep -x -q '^[-0-9A-Za-z]{36}$'<<<"$ID" || exit 2
systemd-nspawn \
-D {{gerboweb_root}} \
-x --bind={{gerboweb_cache}}/upload/$ID:/mnt \
/bin/sh -c "set -euo pipefail
unzip -j -d /tmp/gerber /mnt/gerber.zip
rm -f /mnt/render_top.png /mnt/render_bottom.png /mnt/render_top.small.png /mnt/render_bottom.small.png
date; echo 'Rendering bottom layer'
gerbolyze render top /tmp/gerber /mnt/render_top.png
date; echo 'Scaling down'
convert /mnt/render_top.png -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/render_top.small.png
date; echo 'Rendering top layer'
gerbolyze render bottom /tmp/gerber /mnt/render_bottom.png
date; echo 'Scaling down'
convert /mnt/render_bottom.png -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/render_bottom.small.png"

View file

@ -1 +0,0 @@
SERVE_PATH="{{secure_download_dir}}"

View file

@ -1,85 +0,0 @@
---
- name: Clone pixelterm git
git:
repo: https://github.com/jaseg/pixelterm
dest: "{{clippy_root}}/var/lib/pixelterm.git"
- name: Clone clippy git
git:
repo: https://github.com/jaseg/clippy
dest: "{{clippy_root}}/var/lib/clippy.git"
- name: Setup required packages for clippy
command: arch-chroot "{{clippy_root}}" pacman -Syu --noconfirm python3 python-pip python-numpy python-pillow
- name: Setup pixelterm
command: arch-chroot "{{clippy_root}}" sh -c "cd /var/lib/pixelterm.git && python3 setup.py install"
- name: Setup container clippy systemd service file
template:
src: clippy.service.j2
dest: "{{clippy_root}}/etc/systemd/system/clippy.service"
owner: root
group: root
mode: 0664
- name: Enable systemd machines target
systemd:
name: machines.target
enabled: yes
- name: Copy over clippy container auto boot service file
copy:
src: clippy-nspawn.service
dest: /etc/systemd/system/clippy-nspawn.service
owner: root
group: root
mode: 0664
- name: Create systemd-nspawn config dir
file:
path: /etc/systemd/nspawn
state: directory
owner: root
group: root
mode: 0775
- name: Copy over clippy container config
copy:
src: clippy.nspawn
dest: /etc/systemd/nspawn/clippy.nspawn
owner: root
group: root
mode: 0664
- name: Enable clippy container auto boot
systemd:
daemon-reload: yes
name: clippy-nspawn.service
enabled: yes
- name: Restart clippy container
shell: |
systemctl stop clippy-nspawn
sleep 1
systemctl start clippy-nspawn
for x in $(seq 0 30); do
systemctl -M clippy is-system-running && exit
sleep 1
done
- name: Enable clippy systemd service in container
command: systemctl enable -M clippy clippy.service
- name: Restart clippy systemd service in container
command: systemctl restart -M clippy clippy.service
#- name: Enable host networkd
# systemd:
# name: systemd-networkd
# enabled: yes
# state: started
#- name: Enable clippy container networkd
# command: systemctl enable -M clippy systemd-networkd

View file

@ -1,17 +0,0 @@
---
- name: Install host requisites
dnf:
name: btrfs-progs,arch-install-scripts,systemd-container,libselinux-python
state: latest
- name: Create individual containers
include_tasks: bootstrap_arch_container.yml
with_items: "{{ containers }}"
loop_control:
loop_var: container
- name: Cleanup bootstrap image
file:
path: /tmp/arch-bootstrap.tar.xz
state: absent

View file

@ -1,100 +0,0 @@
---
- name: Set local facts
set_fact:
gerboweb_cache: /var/cache/gerboweb
- name: Copy render script
template:
src: render.sh.j2
dest: /usr/local/sbin/gerbolyze_render.sh
mode: ug+x
- name: Copy vector script
template:
src: vector.sh.j2
dest: /usr/local/sbin/gerbolyze_vector.sh
mode: ug+x
- name: Install packages into gerbolyze container
shell: arch-chroot "{{gerboweb_root}}" pacman -Syu --noconfirm python3 opencv hdf5 gtk3 python-numpy python-pip imagemagick unzip zip
- name: Workaround for cairoffi problem
shell: arch-chroot "{{gerboweb_root}}" pip install -U --upgrade-strategy=eager wheel
# TODO maybe install directly from local git checkout?
- name: Install gerbolyze
shell: arch-chroot "{{gerboweb_root}}" pip install -U --upgrade-strategy=eager gerbolyze
- name: Copy webapp sources
synchronize:
# FIXME: make this path configurable
src: ~/gerbolyze/gerboweb/
dest: /var/lib/gerboweb/
rsync_opts:
- "--exclude=/deploy"
group: no
owner: no
- name: Create uwsgi worker user and group
user:
name: uwsgi-gerboweb
create_home: no
group: uwsgi
password: '!'
shell: /sbin/nologin
system: yes
- name: Template webapp config
template:
src: gerboweb.cfg.j2
dest: /var/lib/gerboweb/gerboweb_prod.cfg
owner: uwsgi-gerboweb
group: root
mode: 0660
- name: Copy uwsgi config
copy:
src: uwsgi-gerboweb.ini
dest: /etc/uwsgi.d/gerboweb.ini
owner: uwsgi-gerboweb
group: uwsgi
mode: 0440
- name: Copy job processor systemd service config
template:
src: gerboweb-job-processor.service.j2
dest: /etc/systemd/system/gerboweb-job-processor.service
- name: Enable uwsgi systemd socket
systemd:
daemon-reload: yes
name: uwsgi-app@gerboweb.socket
enabled: yes
- name: Copy gerboweb cache dir tmpfiles.d config
template:
src: tmpfiles-gerboweb.conf.j2
dest: /etc/tmpfiles.d/gerboweb.conf
owner: root
group: root
mode: 0644
register: tmpfiles_config
- name: Kick systemd tmpfiles service to create cache dir
command: systemd-tmpfiles --create
when: tmpfiles_config is changed
- name: Create job queue db
file:
path: "{{gerboweb_cache}}/job_queue.sqlite3"
owner: root
group: uwsgi
mode: 0660
state: touch
- name: Enable and launch job processor
systemd:
name: gerboweb-job-processor.service
enabled: yes
state: restarted

View file

@ -1,115 +0,0 @@
- name: Install host requisites
dnf:
name: cgit,gitolite3,python3-pygments,python3-docutils,nodejs-markdown
state: latest
- name: Copy cgit favicon
copy:
src: cgit-logo.png
dest: /var/www/git.jaseg.net/cgit.png
- name: Create cgit instance config dir
file:
path: /var/lib/cgit
state: directory
mode: 0755
- name: Copy cgit rc
copy:
src: cgitrc
dest: /var/lib/cgit/cgitrc-gitolite-public
mode: 0644
- name: Create uwsgi worker user and group
user:
name: uwsgi-cgit
create_home: no
group: uwsgi
password: '!'
shell: /sbin/nologin
system: yes
- name: Copy uwsgi config
copy:
src: uwsgi-cgit.ini
dest: /etc/uwsgi.d/cgit.ini
owner: uwsgi-cgit
group: uwsgi
mode: 0440
- name: Enable uwsgi systemd socket
systemd:
daemon-reload: yes
name: uwsgi-app@cgit.socket
enabled: yes
- name: Copy gitolite admin pubkey
copy:
src: ~/.ssh/id_ed25519.gitolite.pub
dest: /tmp/jaseg-gitolite.pub
owner: gitolite3
group: gitolite3
- name: Run gitolite initialization
command: gitolite setup -pk /tmp/jaseg-gitolite.pub
become: true
become_method: su
become_user: gitolite3
become_flags: '-s /bin/sh'
args:
creates: /var/lib/gitolite3/projects.list
- name: Remove leftover admin pubkey
file:
state: absent
path: /tmp/jaseg-gitolite.pub
- name: Allow uwsgi group to access gitolite repo dir
file:
path: /var/lib/gitolite3
state: directory
owner: gitolite3
group: uwsgi
- name: Add cgit uwsgi user to gitolite group
user:
name: uwsgi-cgit
groups: gitolite3
append: yes
- name: Allow cgit uwsgi user to access gitolite repos
file:
path: /var/lib/gitolite3/repositories
mode: 0750
- name: Allow cgit uwsgi user to gitolite repo list
file:
path: /var/lib/gitolite3/projects.list
mode: 0640
- name: Copy gitolite rc
copy:
src: gitolite.rc
dest: /var/lib/gitolite3/.gitolite.rc
owner: gitolite3
group: gitolite3
mode: 0600
- name: Query system user account info
getent:
database: passwd
key: gitolite3
- name: Create git alias user
user:
name: git
create_home: no
group: gitolite3
password: '!'
comment: Alias for gitolite3 user
shell: "{{ getent_passwd['gitolite3'][5] }}"
system: yes
non_unique: yes
home: "{{ getent_passwd['gitolite3'][4] }}"
uid: "{{ getent_passwd['gitolite3'][1] }}"

View file

@ -1,61 +0,0 @@
---
- name: Set local facts
set_fact:
notification_proxy_sqlite_dbfile: /var/lib/notification-proxy/db.sqlite3
- name: Create notification proxy worker user and group
user:
name: uwsgi-notification-proxy
create_home: no
group: uwsgi
password: '!'
shell: /sbin/nologin
system: yes
- name: Create webapp dir
file:
path: /var/lib/notification-proxy
state: directory
owner: uwsgi-notification-proxy
group: uwsgi
mode: 0750
- name: Copy webapp sources
copy:
src: notification_proxy.py
dest: /var/lib/notification-proxy/
owner: uwsgi-notification-proxy
group: uwsgi
mode: 0440
- name: Template webapp config
template:
src: notification_proxy_config.py.j2
dest: /var/lib/notification-proxy/config.py
owner: uwsgi-notification-proxy
group: root
mode: 0660
- name: Copy uwsgi config
copy:
src: uwsgi-notification-proxy.ini
dest: /etc/uwsgi.d/notification-proxy.ini
owner: uwsgi-notification-proxy
group: uwsgi
mode: 0440
- name: Enable uwsgi systemd socket
systemd:
daemon-reload: yes
name: uwsgi-app@notification-proxy.socket
enabled: yes
- name: Create sqlite db file
file:
path: "{{notification_proxy_sqlite_dbfile}}"
owner: uwsgi-notification-proxy
group: uwsgi
mode: 0660
state: touch

View file

@ -1,9 +0,0 @@
---
- name: Copy openjscad webapp sources
synchronize:
# FIXME: make this path configurable
src: ~/openjscad_dist/
dest: /var/www/openjscad.jaseg.net/
group: no
owner: no

View file

@ -1,125 +0,0 @@
---
- name: Set local facts
set_fact:
pogojig_cache: /var/cache/pogojig
- name: Copy render script
template:
src: pogojig_generate.sh.j2
dest: /usr/local/sbin/pogojig_generate.sh
mode: ug+x
- name: Install packages into pogojig container
shell: arch-chroot "{{pogojig_root}}" pacman -Syu --noconfirm python3 python-pip imagemagick unzip zip openscad inkscape make python-lxml xorg-server-xvfb
- name: Install python dependencies into pogojig container
shell: arch-chroot "{{pogojig_root}}" pip install -U --upgrade-strategy=eager ezdxf xvfbwrapper
- name: Install pogojig
synchronize:
# FIXME: make this path configurable
src: checkouts/pogojig/renderer/
dest: "{{pogojig_root}}/var/lib/pogojig_renderer"
group: no
- name: Copy webapp sources
synchronize:
# FIXME: make this path configurable
src: checkouts/pogojig/webapp/
dest: /var/lib/pogojig
delete: true
group: no
owner: no
- name: Pack makefile template zip
archive:
path: "{{pogojig_root}}/var/lib/pogojig_renderer"
dest: /var/lib/pogojig/static/pogojig_makefile_template.zip
format: zip
- name: Create web home for modified tracespace
file:
path: /var/lib/pogojig/pogospace
state: directory
owner: nginx
group: nginx
mode: 0550
- name: Unpack modified tracespace sources
unarchive:
src: resource/pogojig-tracespace.tar.gz
dest: /var/lib/pogojig/pogospace
extra_opts: [--strip-components=1]
owner: nginx
group: nginx
- name: Create uwsgi worker user and group
user:
name: uwsgi-pogojig
create_home: no
group: uwsgi
password: '!'
shell: /sbin/nologin
system: yes
- name: Template webapp config
template:
src: pogojig.cfg.j2
dest: /var/lib/pogojig/pogojig_prod.cfg
owner: uwsgi-pogojig
group: root
mode: 0660
- name: Copy uwsgi config
copy:
src: uwsgi-pogojig.ini
dest: /etc/uwsgi.d/pogojig.ini
owner: uwsgi-pogojig
group: uwsgi
mode: 440
- name: Copy job processor systemd service config
template:
src: pogojig-job-processor.service.j2
dest: /etc/systemd/system/pogojig-job-processor.service
- name: Enable uwsgi systemd socket
systemd:
daemon-reload: yes
name: uwsgi-app@pogojig.socket
enabled: yes
# FIXME the socket doesn't seem to work properly
- name: Enable uwsgi systemd service
systemd:
daemon-reload: yes
name: uwsgi-app@pogojig.service
enabled: yes
- name: Copy pogojig cache dir tmpfiles.d config
template:
src: tmpfiles-pogojig.conf.j2
dest: /etc/tmpfiles.d/pogojig.conf
owner: root
group: root
mode: 0644
register: pogojig_tmpfiles_config
- name: Kick systemd tmpfiles service to create cache dir
command: systemd-tmpfiles --create
when: pogojig_tmpfiles_config is changed
- name: Create job queue db
file:
path: "{{pogojig_cache}}/job_queue.sqlite3"
owner: root
group: uwsgi
mode: 0660
state: touch
- name: Enable and launch job processor
systemd:
name: pogojig-job-processor.service
enabled: yes
state: restarted

View file

@ -1,57 +0,0 @@
---
- name: Set local facts
set_fact:
secure_download_dir: /var/cache/secure_download
- name: Copy webapp sources
synchronize:
# FIXME: make this path configurable
src: ~/secure_download/
dest: /var/lib/secure_download/
group: no
owner: no
- name: Create secure download worker user and group
user:
name: uwsgi-secure-download
create_home: no
group: uwsgi
password: '!'
shell: /sbin/nologin
system: yes
- name: Template webapp config
template:
src: secure_download.cfg.j2
dest: /var/lib/secure_download/secure_download_prod.cfg
owner: uwsgi-secure-download
group: root
mode: 0660
- name: Copy uwsgi config
copy:
src: uwsgi-secure-download.ini
dest: /etc/uwsgi.d/secure-download.ini
owner: uwsgi-secure-download
group: uwsgi
mode: 440
- name: Enable uwsgi systemd socket
systemd:
daemon-reload: yes
name: uwsgi-app@secure-download.socket
enabled: yes
- name: Copy server dir tmpfiles.d config
template:
src: tmpfiles-secure-download.conf.j2
dest: /etc/tmpfiles.d/secure-download.conf
owner: root
group: root
mode: 0644
register: sec_dl_tmpfiles_config
- name: Kick systemd tmpfiles service to create serve dir
command: systemd-tmpfiles --create
when: sec_dl_tmpfiles_config is changed

View file

@ -1,9 +0,0 @@
---
- name: Copy tracespace webapp sources
synchronize:
# FIXME: make this path configurable
src: ~/tracespace_dist/
dest: /var/www/tracespace.jaseg.net/
group: no
owner: no

View file

@ -1,77 +0,0 @@
- name: Copy first stage nginx config
copy:
src: nginx_nossl.conf
dest: /etc/nginx/nginx.conf
- name: Add nginx user to uwsgi group for access to uwsgi socket
user:
name: nginx
groups: uwsgi
append: yes
- name: Create subdomain content dirs
file:
path: /var/www/{{item}}
state: directory
owner: nginx
group: nginx
mode: 0550
loop:
- git.jaseg.net
- blog.jaseg.net
- kochbuch.jaseg.net
- tracespace.jaseg.net
- openjscad.jaseg.net
- automation.jaseg.de
- name: Copy uwsgi systemd socket config
copy:
src: uwsgi-app@.socket
dest: /etc/systemd/system/
- name: Copy uwsgi systemd service config
copy:
src: uwsgi-app@.service
dest: /etc/systemd/system/
- name: Set SELinux to permissive mode # FIXME this is to let nginx talk to uwsgi
selinux:
state: permissive
policy: targeted
- name: Enable and launch nginx systemd service
systemd:
name: nginx.service
enabled: yes
state: restarted
- name: Create subdomain letsencrypt certificates
command: certbot --nginx certonly -d {{item}} -n --agree-tos --email {{item}}-letsencrypt@jaseg.net
args:
creates: /etc/letsencrypt/live/{{item}}/fullchain.pem
loop:
- git.jaseg.net
- blog.jaseg.net
- kochbuch.jaseg.net
- gerbolyze.jaseg.net
- tracespace.jaseg.net
- openjscad.jaseg.net
- pogojig.jaseg.net
- automation.jaseg.de
- dyndns.jaseg.de
- name: Copy final nginx config
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
- name: Restart nginx to load new cert
systemd:
name: nginx.service
state: restarted
- name: Enable certbot renewal timer
systemd:
name: certbot-renew.timer
enabled: yes

View file

@ -1 +0,0 @@
d {{gerboweb_cache}} 770 uwsgi-gerboweb uwsgi 2d

View file

@ -1 +0,0 @@
d {{pogojig_cache}} 770 uwsgi-pogojig uwsgi 2d

View file

@ -1 +0,0 @@
d {{secure_download_dir}} 770 uwsgi-download uwsgi 45d

View file

@ -1,16 +0,0 @@
[Unit]
Description=%i uWSGI app
After=syslog.target
[Service]
ExecStart=/usr/sbin/uwsgi \
--ini /etc/uwsgi.d/%i.ini \
--chmod-socket=660 \
--socket=/run/uwsgi/%i.socket
User=uwsgi-%i
Group=uwsgi
Restart=on-failure
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

View file

@ -1,11 +0,0 @@
[Unit]
Description=Socket for uWSGI app %i
[Socket]
ListenStream=/run/uwsgi/%i.socket
SocketUser=uwsgi-%i
SocketGroup=nginx
SocketMode=0660
[Install]
WantedBy=sockets.target

View file

@ -1,8 +0,0 @@
[uwsgi]
master = True
plugins = cgi
chdir = /var/lib/gitolite3
processes = 1
threads = 2
cgi = /var/www/cgi-bin/cgit
env = CGIT_CONFIG=/var/lib/cgit/cgitrc-gitolite-public

View file

@ -1,10 +0,0 @@
[uwsgi]
master = True
cheap = True
die-on-idle = False
manage-script-name = True
plugins = python3
chdir = /var/lib/gerboweb
mount = /=gerboweb:app
env = GERBOWEB_SETTINGS=gerboweb_prod.cfg

View file

@ -1,10 +0,0 @@
[uwsgi]
master = True
cheap = True
die-on-idle = False
manage-script-name = True
log-format = [pid: %(pid)|app: -|req: -/-] %(addr) (%(user)) {%(vars) vars in %(pktsize) bytes} [%(ctime)] %(method) [URI hidden] => generated %(rsize) bytes in %(msecs) msecs (%(proto) %(status)) %(headers) headers in %(hsize) bytes (%(switches) switches on core %(core))
plugins = python3
chdir = /var/lib/notification-proxy
mount = /=notification_proxy:app

View file

@ -1,10 +0,0 @@
[uwsgi]
master = True
cheap = True
die-on-idle = False
manage-script-name = True
plugins = python3
chdir = /var/lib/pogojig
mount = /=pogojig:app
env = POGOJIG_SETTINGS=pogojig_prod.cfg

View file

@ -1,11 +0,0 @@
[uwsgi]
master = True
cheap = True
die-on-idle = False
manage-script-name = True
log-format = [pid: %(pid)|app: -|req: -/-] %(addr) (%(user)) {%(vars) vars in %(pktsize) bytes} [%(ctime)] %(method) [URI hidden] => generated %(rsize) bytes in %(msecs) msecs (%(proto) %(status)) %(headers) headers in %(hsize) bytes (%(switches) switches on core %(core))
plugins = python3
chdir = /var/lib/secure_download
mount = /=server:app
env = SECURE_DOWNLOAD_SETTINGS=secure_download_prod.cfg

View file

@ -1,18 +0,0 @@
#!/bin/sh
[ $# != 2 ] && exit 1
ID=$1
egrep -x -q '^[-0-9A-Za-z]{36}$'<<<"$ID" || exit 2
LAYER=$2
egrep -x -q '^(top|bottom)$'<<<"$LAYER" || exit 2
systemd-nspawn \
-D {{gerboweb_root}} \
-x --bind={{gerboweb_cache}}/upload/$ID:/mnt \
/bin/sh -c "set -euo pipefail
cd /tmp
unzip -j -d gerber_in /mnt/gerber.zip
gerbolyze vectorize $LAYER gerber_in gerber /mnt/overlay.png
rm -f /mnt/gerber_out.zip
zip -r /mnt/gerber_out.zip gerber"

View file

@ -72,7 +72,7 @@ def index():
r = make_response(render_template('index.html',
has_renders = path.isfile(tempfile_path('gerber.zip')),
has_output = path.isfile(tempfile_path('overlay.png')),
has_output = path.isfile(tempfile_path('overlay.svg')),
**forms))
if 'vector_job' in session or 'render_job' in session:
r.headers.set('refresh', '10')
@ -108,7 +108,7 @@ def upload_gerber():
session['filename'] = secure_filename(f.filename) # Cache filename for later download
render()
if path.isfile(tempfile_path('overlay.png')): # Re-vectorize when gerbers change
if path.isfile(tempfile_path('overlay.svg')): # Re-vectorize when gerbers change
vectorize()
flash(f'Gerber file successfully uploaded.', 'success')
@ -121,7 +121,7 @@ def upload_overlay():
if upload_form.validate_on_submit():
# FIXME raise error when no side selected
f = upload_form.upload_file.data
f.save(tempfile_path('overlay.png'))
f.save(tempfile_path('overlay.svg'))
session['side_selected'] = upload_form.side.data
vectorize()
@ -133,7 +133,7 @@ def upload_overlay():
def render_preview(side):
if not side in ('top', 'bottom'):
return abort(400, 'side must be either "top" or "bottom"')
return send_file(tempfile_path(f'render_{side}.small.png'))
return send_file(tempfile_path(f'template_{side}.preview.png'))
@app.route('/render/download/<side>')
def render_download(side):
@ -141,10 +141,10 @@ def render_download(side):
return abort(400, 'side must be either "top" or "bottom"')
session['last_download'] = side
return send_file(tempfile_path(f'render_{side}.png'),
mimetype='image/png',
return send_file(tempfile_path(f'template_{side}.svg'),
mimetype='image/svg',
as_attachment=True,
attachment_filename=f'{path.splitext(session["filename"])[0]}_render_{side}.png')
attachment_filename=f'{path.splitext(session["filename"])[0]}_template_{side}.svg')
@app.route('/output/download')
def output_download():

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 509 KiB

Before After
Before After

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Gerbolyze Raster image to PCB renderer</title>
<title>Gerbolyze Image to PCB Toolchain</title>
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='style.css')}}">
<link rel="icon" type="image/png" href="{{url_for('static', filename='favicon-512.png')}}">
<link rel="apple-touch-icon" href="{{url_for('static', filename='favicon-512.png')}}">
@ -10,157 +10,159 @@
<body>
<div class="layout-container">
<div class="header">
<div class="desc">
<h1>Raster image to PCB converter</h1>
<p>
Gerbolyze is a tool for rendering black and white raster (PNG) images directly onto gerber layers. You can
use this to put art on a PCB's silkscreen, solder mask or copper layers. The input is a black-and-white PNG
image that is vectorized and rendered into an existing gerber file. Gerbolyze works with gerber files
produced with any EDA toolchain and has been tested to work with both Altium and KiCAD.
</p>
</div>
<div class="desc">
<h1>SVG/JPG/PNG to PCB converter</h1>
<p>
This is the toy web frontend to <a href="https://github.com/jaseg/gerbolyze">Gerbolyze</a>.
Gerbolyze is a tool for rendering arbitrary vector (SVG) and raster (PNG/JPG) images directly onto gerber layers.
You can use this to put art on a PCB's silkscreen, solder mask or copper layers. The input is an SVG file
generated from a template. This SVG file has one layer for each PCB layer and the layers are rendered one by one
into the existing gerber file. SVG primitives are converted as-is with (almost) full SVG support, and bitmap
images are vectorized using a vector halftone processor. Gerbolyze works with gerber files produced with any EDA
toolchain and has been tested to work with both Altium and KiCAD.
</p>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
<div class="flashes">
{% for category, message in messages %}
<div class="flash flash-{{category}}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% if messages %}
<div class="flashes">
{% for category, message in messages %}
<div class="flash flash-{{category}}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form id="reset-form" method="POST" action="{{url_for('session_reset')}}" class="reset-form">{{reset_form.csrf_token}}</form>
<div class="steps">
<div class="step" id="step1">
<div class="description">
<h2>Upload zipped gerber files</h2>
<p>
First, upload a zip file containing all your gerber files. The default file names used by KiCAD, Eagle
and Altium are supported.
</p>
</div>
<div class="step" id="step1">
<div class="description">
<h2>Upload zipped gerber files</h2>
<p>
First, upload a zip file containing all your gerber files. The default file names used by KiCAD, Eagle
and Altium are supported.
</p>
</div>
<div class="controls">
<form id="gerber-upload-form" method="POST" action="{{url_for('upload_gerber')}}" enctype="multipart/form-data">
{{gerber_form.csrf_token}}
</form>
<div class="form-controls">
<div class="upload-label">Upload Gerber file:</div>
<input class='upload-button' form="gerber-upload-form" name="upload_file" size="20" type="file">
</div>
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
<input class='submit-button' form="gerber-upload-form" type="submit" value="Submit">
</div>
</div>
</div>
<div class="controls">
<form id="gerber-upload-form" method="POST" action="{{url_for('upload_gerber')}}" enctype="multipart/form-data">
{{gerber_form.csrf_token}}
</form>
<div class="form-controls">
<div class="upload-label">Upload Gerber file:</div>
<input class='upload-button' form="gerber-upload-form" name="upload_file" size="20" type="file">
</div>
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
<input class='submit-button' form="gerber-upload-form" type="submit" value="Submit">
</div>
</div>
</div>
{% if 'render_job' in session or has_renders %}
<div class="step" id="step2">
<div class="description">
<h2>Download the target side's preview image</h2>
<p>
Second, download either the top or bottom preview image and use it to align and scale your own artwork
in an image editing program such as Gimp. Then upload your overlay image below.
{% if 'render_job' in session or has_renders %}
<div class="step" id="step2">
<div class="description">
<h2>Download the target side's preview image</h2>
<p>
Second, download either the top or bottom SVG template and place your own artwork in it on the appropriate
layers. The template is made to work well with the excellent open-source <a href="https://inkscape.org">Inkscape</a>
vector graphics editor. When you are done, upload your overlay below.
Note that you will have to convert grayscale images into binary images yourself. Gerbolyze can't do this
for you since there are lots of variables involved. Our <a href="https://github.com/jaseg/gerbolyze/blob/master/README.rst#image-preprocessing-guide">Guideline on image processing</a> gives an overview on
<i>one</i> way to produce agreeable binary images from grayscale source material.
</p>
</div>
<div class="controls">
{% if 'render_job' in session %}
<div class="loading-message">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div><strong>Processing...</strong></div>
<div>(this may take several minutes!)</div>
</div>
{% else %}
<div class="preview-images">
<a href="{{url_for('render_download', side='top')}}" onclick="document.querySelector('#side-0').checked=true" class="preview preview-top" style="background-image:url('{{url_for('render_preview', side='top')}}');">
<div class="overlay">top</div>
</a>
<a href="{{url_for('render_download', side='bottom')}}" onclick="document.querySelector('#side-1').checked=true" class="preview preview-bottom" style="background-image:url('{{url_for('render_preview', side='bottom')}}');">
<div class="overlay">bot<br/>tom</div>
</a>
</div>
{% endif %}
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
</div>
</div>
</div>
If you wish to put a bitmap image (PNG/JPG) on your board, simply place it into the SVG on the appropriate
layer. Make sure you select Inkscape's "embed image" option when importing it.
</p>
</div>
<div class="controls">
{% if 'render_job' in session %}
<div class="loading-message">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div><strong>Processing...</strong></div>
<div>(this may take several minutes!)</div>
</div>
{% else %}
<div class="preview-images">
<a href="{{url_for('render_download', side='top')}}" onclick="document.querySelector('#side-0').checked=true" class="preview preview-top" style="background-image:url('{{url_for('render_preview', side='top')}}');">
<div class="overlay">top</div>
</a>
<a href="{{url_for('render_download', side='bottom')}}" onclick="document.querySelector('#side-1').checked=true" class="preview preview-bottom" style="background-image:url('{{url_for('render_preview', side='bottom')}}');">
<div class="overlay">bot<br/>tom</div>
</a>
</div>
{% endif %}
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
</div>
</div>
</div>
<div class="step" id="step3">
<div class="description">
<h2>Upload overlay image</h2>
<p>
Now, upload your binary overlay image as a PNG and let gerbolyze render it onto the target layer. The PNG
file should be a black and white binary file with details generally above about 10px size. <b>Antialiased
edges are supported.</b>
</p>
</div>
<div class="controls">
<form id="overlay-upload-form" method="POST" action="{{url_for('upload_overlay')}}" enctype="multipart/form-data">
{{overlay_form.csrf_token}}
</form>
<div class="form-controls">
<div class="form-label upload-label">Upload Overlay PNG file:</div>
<input class='upload-button' form="overlay-upload-form" name="upload_file" size="20" type="file">
</div>
<div class="form-controls">
<div class="form-label target-label">Target layer:</div>
<input form="overlay-upload-form" name="side" id="side-0" type="radio" value="top">
<label for="side-0">Top</label>
<input form="overlay-upload-form" name="side" id="side-1" type="radio" value="top">
<label for="side-1">Bottom</label>
</div>
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
<input class='submit-button' form="overlay-upload-form" type="submit" value="Submit">
</div>
</div>
</div>
<div class="step" id="step3">
<div class="description">
<h2>Upload overlay SVG</h2>
<p>
Now, upload your binary overlay as an SVG and let gerbolyze paste it onto the target layers.
</p>
</div>
<div class="controls">
<form id="overlay-upload-form" method="POST" action="{{url_for('upload_overlay')}}" enctype="multipart/form-data">
{{overlay_form.csrf_token}}
</form>
<div class="form-controls">
<div class="form-label upload-label">Upload Overlay PNG file:</div>
<input class='upload-button' form="overlay-upload-form" name="upload_file" size="20" type="file">
</div>
<div class="form-controls">
<div class="form-label target-label">Target layer:</div>
<input form="overlay-upload-form" name="side" id="side-0" type="radio" value="top">
<label for="side-0">Top</label>
<input form="overlay-upload-form" name="side" id="side-1" type="radio" value="top">
<label for="side-1">Bottom</label>
</div>
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
<input class='submit-button' form="overlay-upload-form" type="submit" value="Submit">
</div>
</div>
</div>
{% if 'vector_job' in session or has_output %}
<div class="step" id="step4">
<div class="description">
<h2>Download the processed gerber files</h2>
</div>
<div class="controls">
{% if 'vector_job' in session %}
<div class="loading-message">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div><strong>Processing...</strong></div>
<div>(this may take several minutes!)</div>
</div>
{% else %}
<div class='download-controls'>
<a class='output-download' href="{{url_for('output_download')}}">Click to download</a>
</div>
{% endif %}
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
</div>
<!--4>Debug foo</h4>
<div class="loading-message">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div><strong>Processing...</strong></div>
<div>(this may take several minutes!)</div>
</div-->
</div>
</div>
{% endif %} {# vector job #}
{% endif %} {# render job #}
{% if 'vector_job' in session or has_output %}
<div class="step" id="step4">
<div class="description">
<h2>Download the processed gerber files</h2>
</div>
<div class="controls">
{% if 'vector_job' in session %}
<div class="loading-message">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div><strong>Processing...</strong></div>
<div>(this may take several minutes!)</div>
</div>
{% else %}
<div class='download-controls'>
<a class='output-download' href="{{url_for('output_download')}}">Click to download</a>
</div>
{% endif %}
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
</div>
<!--4>Debug foo</h4>
<div class="loading-message">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div><strong>Processing...</strong></div>
<div>(this may take several minutes!)</div>
</div-->
</div>
</div>
{% endif %} {# vector job #}
{% endif %} {# render job #}
</div>
<div class="sample-images">
<h1>Sample images</h1>
<img src="{{url_for('static', filename='sample1.jpg')}}">
<img src="{{url_for('static', filename='sample2.jpg')}}">
<img src="{{url_for('static', filename='sample3.jpg')}}">
<h1>Sample images</h1>
<img src="{{url_for('static', filename='sample1.jpg')}}">
<img src="{{url_for('static', filename='sample2.jpg')}}">
<img src="{{url_for('static', filename='sample3.jpg')}}">
</div>
</div>
</body>

95
kicad_mod_template.svg Normal file
View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="160mm"
height="100mm"
viewBox="0 0 160 100"
version="1.1"
id="svg8"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07, custom)"
sodipodi:docname="kicad_mod_template.svg">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1019"
id="namedview16"
showgrid="false"
inkscape:zoom="1.1197381"
inkscape:cx="293.88402"
inkscape:cy="186.11375"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="F.SilkS" />
<g
inkscape:groupmode="layer"
id="Eco2.User"
inkscape:label="Eco2.User" />
<g
inkscape:groupmode="layer"
id="Eco1.User"
inkscape:label="Eco1.User" />
<g
inkscape:groupmode="layer"
id="Margin"
inkscape:label="Margin" />
<g
inkscape:groupmode="layer"
id="F.CrtYd"
inkscape:label="F.CrtYd" />
<g
inkscape:groupmode="layer"
id="F.Fab"
inkscape:label="F.Fab" />
<g
inkscape:groupmode="layer"
id="Dwgs.User"
inkscape:label="Dwgs.User" />
<g
inkscape:groupmode="layer"
id="Cmts.User"
inkscape:label="Cmts.User" />
<g
inkscape:groupmode="layer"
id="Edge.Cuts"
inkscape:label="Edge.Cuts" />
<g
inkscape:groupmode="layer"
id="g5"
inkscape:label="F.Cu" />
<g
inkscape:groupmode="layer"
id="F.Mask"
inkscape:label="F.Mask" />
<g
inkscape:groupmode="layer"
id="F.SilkS"
inkscape:label="F.SilkS" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
pics/ex-flattening.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
pics/ex-intersections.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
pics/ex-strokes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
pics/ex-svg-joins.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
pics/ex-svg-strokes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
pics/ex-svg-winding.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
pics/fr4_comparison2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 KiB

BIN
pics/process-overview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

7230
pics/process-overview.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

Before

Width:  |  Height:  |  Size: 293 KiB

After

Width:  |  Height:  |  Size: 293 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Before After
Before After

BIN
pics/subtract_example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

515
pics/test_svg_readme.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

9
podman/arch-testenv Normal file
View file

@ -0,0 +1,9 @@
FROM docker.io/archlinux:latest
MAINTAINER gerbolyze@jaseg.de
RUN pacman --noconfirm -Syu
RUN pacman --noconfirm -Sy pugixml opencv pango cairo git python make clang rustup cargo python-pip base-devel
RUN rustup install stable
RUN rustup default stable
RUN cargo install usvg

11
podman/debian-testenv Normal file
View file

@ -0,0 +1,11 @@
FROM docker.io/debian:latest
MAINTAINER gerbolyze@jaseg.de
RUN env DEBIAN_FRONTEND=noninteractive apt update -y
RUN env DEBIAN_FRONTEND=noninteractive apt install -y libopencv-dev libpugixml-dev libpangocairo-1.0-0 libpango1.0-dev libcairo2-dev clang make python3 git python3-wheel curl python3-pip python3-venv
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
RUN sh -c '. ~/.cargo/env && rustup install stable'
RUN sh -c '. ~/.cargo/env && rustup default stable'
RUN sh -c '. ~/.cargo/env && cargo install usvg'

7
podman/fedora-testenv Normal file
View file

@ -0,0 +1,7 @@
FROM docker.io/fedora:latest
MAINTAINER gerbolyze@jaseg.de
RUN dnf update --refresh -y
RUN dnf install -y python3 make clang opencv-devel pugixml-devel pango-devel cairo-devel rust cargo
RUN cargo install usvg

BIN
podman/testdata/gerbolyze-2.0.0.tar.gz vendored Normal file

Binary file not shown.

515
podman/testdata/test_svg_readme.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 143 KiB

5
podman/testdata/testscript.sh vendored Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
pip3 install --user /data/gerbolyze-*.tar.gz --no-binary gerbolyze
/root/.local/bin/svg-flatten --clear-color black --dark-color white --format svg /data/test_svg_readme.svg /out/test_out.svg

7
podman/ubuntu-testenv Normal file
View file

@ -0,0 +1,7 @@
FROM docker.io/ubuntu:latest
MAINTAINER gerbolyze@jaseg.de
RUN env DEBIAN_FRONTEND=noninteractive apt update -y
RUN env DEBIAN_FRONTEND=noninteractive apt install -y libopencv-dev libpugixml-dev libpangocairo-1.0-0 libpango1.0-dev libcairo2-dev clang make python3 git python3-wheel curl python3-pip python3-venv cargo
RUN cargo install usvg

463
post.html Normal file
View file

@ -0,0 +1,463 @@
<!doctype html> <html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Create Beautiful Circuit Boards With Gerbolyze</title>
<style type="text/css">
@font-face {
font-family: "playfair display webfont";
font-weight: 600;
src: url("fonts/playfair-display-v22-latin-ext_latin-600.woff2") format("woff2");
}
@font-face {
font-family: "playfair display webfont";
font-weight: 400;
src: url("fonts/playfair-display-v22-latin-ext_latin-regular.woff2") format("woff2");
}
@font-face {
font-family: "playfair display webfont";
font-weight: 700;
src: url("fonts/playfair-display-v22-latin-ext_latin-700.woff2") format("woff2");
}
:root {
--textw: calc(min(600px, 100vw));
--mar: calc(50vw - 0.5*var(--textw));
}
html, body {
margin: 0;
padding: 0;
hyphens: auto;
overflow-wrap: break-word;
font-size: 14pt;
font-family: "Source Sans Pro";
}
.content {
padding-left: 10px;
padding-right: 10px;
width: 100%;
box-sizing: border-box;
}
/*
* { border: 1px solid red; }
*/
figure {
overflow: hidden;
padding-top: 20px;
padding-right: 0;
padding-bottom: 20px;
padding-left: var(--mar);
margin: 0;
box-sizing: border-box;
width: 100vw;
max-width: 100%;
}
img {
display: inline-block;
width: var(--textw);
margin-right: 2em;
}
figcaption {
display: inline-block;
margin: 0;
width: 20em;
vertical-align: top;
text-align: justify;
}
figcaption > strong {
display: block;
font-family: "playfair display webfont";
font-weight: 700;
color: #c00;
font-size: 16pt;
margin-bottom: .5em;
}
.content > p {
margin-left: var(--mar);
width: var(--textw);
box-sizing: border-box;
text-align: justify;
line-height: 1.5;
max-width: 100%;
}
h1, h2, h3, h4, h5, h6 {
font-family: "playfair display webfont";
font-weight: 600;
color: #c00;
text-align: right;
margin: 0;
padding-left: 1em;
padding-top: 0;
box-sizing: border-box;
width: calc(var(--mar) + var(--textw));
max-width: 100%;
}
h1 {
font-size: calc(24pt + 2vh + 2vw);
padding-bottom: 0.7em;
}
body::before {
content: "";
display: block;
margin-top: 100px;
border-top: 0.7mm dashed #c00;
width: 100vw;
max-width: 100%;
}
h2 {
font-size: calc(18pt + .2vh + .2vw);
margin-top: 1em;
margin-bottom: .5em;
}
</style>
</head>
<body lang="en">
<div class="content">
<h1>Create Beautiful Circuit Boards With Gerbolyze</h1>
<p>
Today, there is an increasingly large crowd of people who do artistic circuit board designs. People who fuse
the roles of engineer and artist. Unitl today, circuit board design tools mostly ignore this use case and
present a multitude of obstacles for such use. Gerbolyze finally solves this problem and presents an
integrated solution for artistic PCB design that is compatible with real designer's workflows on one side
and with real electronic design automation software on the other side.
</p>
<figure>
<a href="pics/pcbway_sample_02_small.jpg"><img src="pics/pcbway_sample_02_small.jpg" alt="A printed
circuit board showing a surrealist manga-style drawing of a woman sitting atop several traffic
lights. The woman's hair looks golden from the circuit board's gold copper finish. The
background is blue and white with scales of gray emulated through a halftone technique like it
is used in newspapers."/></a>
<figcaption>
<strong>An artistic PCB design</strong>
<p>
This design was created from a digital artwork in a raster image file after circuit board
manufacturer PCBWay offered me some free boards. The artwork was pre-processed using a raster
graphics tool: It was split into layers for the different colors. Then, its color components were
adjusted for brightness and contrast and finally passed through a raster-based halftone filter. The
resulting file was then converted into circuit board manufacturing files using Gerbolyze.
</p>
</figcaption>
</figure>
<p>
Thirty years ago, the world of printed circuit board design was revolutionized by the introduction of
computer aided design tools. These tools enabled extremely complex designs through automation features
like autorouting and through automatic design rule checking to weed out human error. While the first
such tools were still very limited, their capabilites quickly grew and a few years after their
introduction modern electronics design without computers was already unthinkable.
</p>
<p>
Today, circuit board design programs can look back on a rich history and have accumulated a healty
amount of expert knowledge in their design. Despite their difficult economic niche, even <em>free
software</em> design tools have grown to become usable for advanced designs. However, all modern
circuit board design tools are severely lacking in one area: That of <em>artistic</em> design. Many
design assumptions are hard-wired deep into their design: Traces should be at 45° angles. Silkscreen is
one opaque color. etc. These design assumptions lead to these tools being <em>in the way</em> more often
than not for the increasing crowd of designers who try to create art with them.
</p>
<p>
Gerbolyze solves this problem. Gerbolyze interfaces with circuit board design tools such as Altium or Kicad
on one side through standard Gerber files. It interfaces with vector graphics editors such as inkscape on
the other side through Scaleable Vector Graphics (SVG) files. By fusing both, it yields a powerful
environment for artistic circuit board design.
</p>
<h2>Gerbolyze Algorithm Overview</h2>
<p>
Gerbolyze has two major components. The first is the gerbolyze executable itself, which orchestrates the
process of fusing SVG and Gerber files. The second is svg-flatten, a tool encapsulating all of the
heavy-duty computer graphics code. The gerbolyze executable is a python script for readability, while the
geometry backend is a C++ binary for performance.
</p>
<p>
In the beginning of the fusing process, the orchestrator figures out what semantic layers such as silkscreen
or copper the tool's input files correspond to. The assigned layers are then processed one by one. For each
layer, the tool first checks the input SVG file for any content. If there is none on this layer, the layer
file is directly copied to the output. If ther is some, this SVG content is passed through the geometry
backend to convert it to Gerber code. The resulting Gerber code is then read and added on top of the input
file's gerber code. The result is written to the output.
</p>
<h2>The Computer Geometry of Scaleable Vector Graphics</h2>
<figure>
<a href="pics/test_svg_readme_composited.png"><img src="pics/test_svg_readme_composited.png"/></a>
<figcaption>
<strong>A complex SVG file...</strong>
...illustrates the range of features that Gerbolyze supports. The left side of this picture shows
the input to gerbolze, the right side the resulting output. As you can see, clips, bitmap images,
pattern fills and strokes all behave in a "visually intuitive" way.
</figcaption>
</figure>
<p>
The heavy lifting during this process is done by the geometry backend. While its job seems simple at first,
it is surprisingly stretching the state of the art in both academic research and technical implementations
of computer graphics. In its core the problem is that while SVG and Gerber are both essentially vector
graphics formats, both have very different conceptions of their drawing models: They differ significantly in
what happens when one of the input file's vectors is drawn.
</p>
<p>
SVG is little more than a highly standardized description of the basic operations provided by the modern 2D
graphics interfaces such as Qt, Cairo or Skia that are built-in to all operating systems. Drawing
<em>paths</em> that are described by the vector coördinates of points along them is the most basic of these
operations. SVG also includes support for a surprising number of decidedly <em>raster</em> operations such
as masking but, operating on grayscales, these are less relevant for the type of design one would create
when targeting circuit board production processes.
</p>
<p>
Similar to SVG, the Gerber file format also targets a type of graphics programming interface. Only instead
of that of operating systems 2D graphics APIs of the 90ies and 2000s, the Gerber format was created as a way
to encapsulate commands for photoplotters, computer-controlled machines that physically move a light source
across a photo-sensitive material. Gerber's concept of "aptertures" goes back to mechanical photoplotters
having magazines of stencils of different shapes and sizes that could be swapped into the path of light
during the plotting process.
</p>
<figure>
<a href="pics/ex-intersections.png"><img src="pics/ex-intersections.png"/></a>
<figcaption>
<strong>An SVG path with one self-intersection and one hole</strong>
On the left you see our example path illustrated with the locations of its <em>nodes</em>, that is
the points where one segment starts and another ends. Also illustrated are the <em>handles</em> that
define its curvature. Note that not every node has handles. This is because some of the path's
segments are straight lines instead of bezier curves.
</figcaption>
</figure>
<p>
Like SVG has its paths, the Gerber format has polygons. Polygons were added to the format to ease the
description of irregularly-shaped areas: Previously, these would have to be drawn by overlaying thousands of
thin lines, described one by one in the Gerber file. Using a Polygon, one only needs to describe the shape's
outline as a series of points and the photoplotter will fill in the rest.
</p>
<h2>Transforming Vectors into Vectors</h2>
<p>
The crux in converting from SVG to Gerber lies here, in the conversion of paths. While in both a path or
polygon is described by its outline, which is described by points, there are significant differences in the
limitations both place on these outlines. In SVG an outline can consist of several types of segments:
besides basic straight lines, multiple types of parametrized curves including cubic bezier curves are
possible. An SVG path's outline can self-intersect (cross over itself). The path can also have holes,
additional parts that are inside the outline.
</p>
<figure>
<a href="pics/ex-flattening.png"><img src="pics/ex-flattening.png"/></a>
<figcaption>
<strong>Bezier flattening...</strong>
...transforms a elegant bezier curve into a dumb list of points. The resulting image can still be
called a <em>vector</em> image since after all points are vectors, too, but they can only allude to
their previous mathematical glory. At least straight lines are easy to deal with, though!
</figcaption>
</figure>
<p>
Gerber, on the other hand, has a much more limited view of what a polygon is. In gerber, a polygon is
something bounded by straight line segments, that cannot touch except under very particular circumstances,
and holes are simply not supported. Converting from SVG's flexible model of a path to Gerber's very
limited model of a polygon while preserving the fidelity of the input data is the true challenge here. As an
aside, a fun complication one will encounter when embarking on this endeavour is that most programs that
display Gerber files use modern graphics libraries in the backend. In these programs, a valid SVG path
ineptly converted into an illegal gerber polygon may still end up looking alright since these programs
usually just pass through the gerber's input data to the underlying graphics layer without validation—and
that graphics layer is the one from SVG.
</p>
<h2>Styles and Strokes</h2>
<p>
Creating gerber-compatible polygons from SVG paths is not the only place where some heavy computer geometry
is necessary. SVG also allows a path to be drawn with its outline stroked with a set width. Though stroking
a path with a given width is very intuitive, it again happens to be surprisingly complex to do in
computer-geometrically.
</p>
<p>
At first one might think SVG stroke widths naturally map to gerber apertures, and all that is left is
flattening the path's bezier curves into straight line segments for it to be drawn. This approach would be
a passable approximation in many cases, but there is a large part of SVG's expressivity that will be lost
under this mapping. SVG allows designers to define join styles and end cap styles on a path. The join style
describes how two path segments that are at an angle will be joined. End caps describe how open ends of the
path will be rendered. Gerber does not have native support for either.
</p>
<figure>
<a href="pics/ex-strokes.png"><img src="pics/ex-strokes.png"/></a>
<figcaption>
<strong>Strokes can be quite complex</strong>
<p>
In this picture, on the left you see a circle that is stroked with a pattern. To draw this to screen
or gerber, you have to first convert the ring's path to its outline. Then, you have to render the
pattern tiles that overlap that outline, while clipping them against the outline. The right-hand
picture illustrates different end and join styles.
</figcaption>
</figure>
<p>
Regular 2D renderers perform stroking as a part of their rasterization routine. This means that a regular
renderer will likely never actually calculate the vector representation of the outline of the stroke at all,
instead bypassing that step and directly converting the path's vector representation into its pixel
representation given some stroke width. In our implementation we instead convert the outline of the path's
stroke into its vector polygon representation, which we then output as gerber code.
</p>
<p>
This transformation from path plus stroke width to vector outline can be done in several ways. The one we
chose was to leverage the excellent Clipper library that we are already using for clipping shapes. Clipper's
offsetting function is essentially a turn-key solution for this use case. A nice side-effect of this
approach is that we can directly use the resulting stroke polygons as clips when a user specifies a path
with a patterned stroke!
</p>
<h2>Gerbolyze Image Vectorization</h2>
<p>
To make Gerbolyze as user-friendly as possible I decided to include support for raster images as well. The
previous version of Gerbolyze in fact exclusively handled raster images, so keeping this support seemed
natural to me.
</p>
<p>
There are several ways to convert raster images into a vector representation. Roughly, they fall into two
categories: Tracing and halftone processing. Tracing tries to read contours of colored areas of a raster
image, and approximates these contours with vector shapes. Tracing is most useful for raster images that
contain text or graphics. On good-quality input, tracing can produce surprisingly accurate results. In
contrast to tracing, halftone processing tries to emulate the picture's tones (be they grayscale or color)
with thousands or even millions of tiny colored filled shapes. Halftone processing to this day is used in
all kinds of printing processes.
</p>
<p>
For bringing grayscale imagery into circuit board production, halftone processing is a very good fit. The
silkscreen processes that are commonly used for circuit boards have very high resolution and can reproduce
any input shape with micrometer precision, but they are limited in the smallest amount of ink they can put
on a board or the smallest gap between two blobs of ink that they can reliably produce. These limitations
are not due to the printers' or photoplotters' resolution or precision but simply due to the mechanics of
small droplets of liquid being squeegied onto or shot at a circuit board.
</p>
<!-- FIXME some microscope images of silkscreen's 3d structure would be great here! -->
<p>
The following pictures illustrate the different vectorization processes gerbolyze supports. In each row you
see the output of each vectorization process zoomed out on the left, and a zoomed-in detail view on the
right. For grayscale images, the poisson-disc-sampled halftone vectorizer works best. For tracing graphics,
the <a href="https://opencv.org/">OpenCV</a> contour tracer does a good job. This contour tracer is the
exact same one that was used in the previous version of Gerbolyze.
<figure>
<a href="pics/vec_square_composited.png"><img src="pics/vec_poisson_composited.png"/></a>
<figcaption>
<strong>Poisson disc sampling</strong>
Poisson disc sampling randomly distributes points on the plane. We then calculate these point's
voronoi tesselation, whose cells we then fill proportional to the image's brightness at that spot.
The resulting image looks very natural as it is devoid of distracting regular aliasing patterns.
</figcaption>
</figure>
<figure>
<a href="pics/vec_square_composited.png"><img src="pics/vec_hexgrid_composited.png"/></a>
<figcaption>
<strong>Hexagon grid sampling</strong>
The hexgrid sampler actually uses the same voronoi-based halftone code of the poisson-disc
sampling-based code, just with points generated on a hexgrid instead of randomly. The result is an
image that has a decidedly "space" look to it. At coarser resolutions, this sampling method has the
chance to shine with its attractive geometry.
</figcaption>
</figure>
<figure>
<a href="pics/vec_square_composited.png"><img src="pics/vec_square_composited.png"/></a>
<figcaption>
<strong>Square grid sampling</strong>
Though I think the square grid method looks the worst of the three halftone methods compared here,
it would have been ridiculous not to implement it given how little work it was using the
voronoi-cell halftone code.
</figcaption>
</figure>
<figure>
<a href="pics/vec-comparison-contour.png"><img src="pics/vec-comparison-contour.png"/></a>
<figcaption>
<strong>Binary contour tracing</strong>
This method calls into the same piece of the <a href="https://opencv.org/">OpenCV</a> image
processing library that the old gerbolyze used. As demonstrated here, this method lends itself well
to graphic inputs. It does also enable you to experiment with basically any rasterized halftone
processor such as the "Newsprint" filter built into GIMP.
</figcaption>
</figure>
<h2>Manufacturing Considerations</h2>
<!-- minimum trace/space guarantees in the vectorizer -->
<p>
When creating artistic designs for PCB manufacturing, you have to keep in mind the limitations of the PCB
manufacturing process at all times. PCB manufacturing processes only know filled and unfilled areas, and
fundamentally cannot do grayscale without hacks like halftone processing. More importantly, these
manufacturing processes have significant limitations in the smallest detail size that they can resolve.
For inexpensive processes, these trace/space design rules are commonly in the range of 50-150µm. While this
sounds great at first, it is <em>vastly</em> larger than what even a cheap home printer can accomplish. For
comparison, a regular inkjet printer for home use can print photos at 1200 dpi without breaking a sweat.
1200 dpi means that this printer can put dots of ink on paper that are only 20 µm in size! And it can do
these dots in millions of colors, too.
</p>
<p>
When tailoring a design for PCB manufacturing there's two things one can do. Number one is to be creative
with graphical parts of the design and avoid extremely narrow lines, wedges or other thin features that will
not come out during circuit board manufacturing. Number two is to keep detail in raster images several times
larger than the manufacturing processes native capability. For example, to target a trace/space design rule
of 100 µm, the smallest detail in embedded raster graphics should not be much below 1mm.
</p>
<p>
Gerbolyze's halftone vectorizers have built-in support for trace/space design rules. While they can still
produce small artifacts that violate these rules, their output should be close enough to satifsy board
houses and close enough for the result to look good. The way gerbolyze does this is to clip the halftone
cell's values to zero whenevery they get too small, and to forcefully split or merge two neighboring cells
when they get too close. While this process introduces slight steps at the top and bottom of grayscale
response, for most inputs these are not noticeable.
</p>
<p>
On the other hand, for SVG vector elements as well as for traced raster images, Gerbolyze cannot help with
these design rules. There is no heuristic that would allow Gerbolyze to non-destructively "fix" a design
here, so all that's on the roadmap here is to eventually include a gerber-level design rule checker.
</p>
<p>
As far as board houses go, I have made good experiences with the popular Chinese board houses. In my
experience, JLC will just produce whatever you send them with little fucks being given about design rule
adherence or validity of the input gerbers. This is great if you just want artistic circuit boards without
much of a hassle, and you don't care if they come out exactly as you imagined. The worst I've had happen was
when an older version of gerbolyze generated polygons with holes assuming standard fill-rule processing. The
in the board house's online gerber viewer things looked fine, and neither did they complain during file
review. However, the resulting boards looked completely wrong because all the dark halftones were missing.
</p>
<p>
PCBWay on the other hand has a much more rigurous file review process. They <em>will</em> complain when you
throw illegal garbage gerbers at them, and they will helpfully guide you through your design rule
violations. In this way you get much more of a professional service from them and for designs that have to
be functional their higher level of scrutiny definitely is a good thing. For the design you saw in the
first picture in this article, I ended up begging them to just plot my files if it doesn't physically break
their machines and to their credit, while they seemed unhappy about it they did it and the result looks
absolutely stunning.
</p>
<p>
PCBWay is a bit more expensive on their lowest-end offering than JLC, but I found that for anything else
(large boards, multi-layer, gold plating etc.) their prices match. PCBWay offers a much broader range of
manufacturing options such as flexible circuit boards, multi-layer boards, thick or thin substrates and
high-temperature substrates so they closer to an industry supplier like Eurocircuits than they are to a
hobbyist prototyping service like dirtypcbs.
</p>
<figure>
<a href="pics/fr4_comparison2.jpg"><img src="pics/fr4_comparison2.jpg"/></a>
<figcaption>
<strong>FR-4 substrate color differs significantly...</strong>
...between these two boards. These boards come from two batches PCBWay produced of the exact same
design. As you can see, it would not have been wise to rely too much on the substrate's color in
this design. Obviously, this is not a defect as the precise color of the FR-4 substrate used is
perfectly irrelevant for the market these manufacturers actually target.
</figcaption>
</figure>
<p>
When in doubt about how your design is going to come out on the board, do not hesitate to contact your board
house. Most of the customer-facing online PCB services have a number of different factories that do a number
of different fabrication processes for them depending on order parameters. Places like PCBWay have
exceptional quality control, but that is mostly focused on the technical aspects of the PCB. If you rely on
visual aspects like silkscreen uniformity or solder mask color, you may find significant variations between
manufacturers or even between orders with the same manufacturer.
</p>
<h2>Conclusion</h2>
<p>
I hope Gerbolyze will make you life a bit easier when it comes to artistic PCB design. I hope I have managed
to illustrate a bit the design choices I made in Gerbolyze in this article. If you have any comments or
suggestions, please feel free to write me <a href="mailto:gerbolyze.nospam@jaseg.de">an email</a> or open an
issue <a href="https://github.com/jaseg/gerbolyze">on Github</a>.
</p>
</div>
</body>
</html>

14
run-tests.sh Executable file
View file

@ -0,0 +1,14 @@
#!/bin/sh
set -e
python setup.py sdist build
cp dist/*.tar.gz podman/testdata
for distro in arch fedora debian ubuntu
do
podman build -t gerbolyze-$distro-testenv -f podman/$distro-testenv
mkdir -p /tmp/gerbolyze-test-out
podman run --mount type=bind,src=podman/testdata,dst=/data,ro --mount type=bind,src=/tmp/gerbolyze-test-out,dst=/out gerbolyze-$distro-testenv /data/testscript.sh
done

View file

@ -1,25 +1,57 @@
#!/usr/bin/env python3
import os
import sys
from setuptools import setup
from setuptools.command.install import install
import subprocess
from multiprocessing import cpu_count
from pathlib import Path
def readme():
with open('README.rst') as f:
return f.read()
def compile_and_install_svgflatten(target_dir):
src_path = 'svg-flatten'
try:
subprocess.run(['make', 'check-deps'], cwd=src_path, check=True)
subprocess.run(['make', '-j', str(cpu_count()), 'all'], cwd=src_path, check=True)
bin_dir = target_dir / ".."
bin_dir.mkdir(parents=True, exist_ok=True)
subprocess.run(['make', 'install', f'PREFIX={bin_dir.resolve()}'], cwd=src_path, check=True)
except subprocess.CalledProcessError:
print('Error building svg-flatten C++ binary. Please see log above for details.', file=sys.stderr)
sys.exit(1)
class CustomInstall(install):
"""Custom handler for the 'install' command."""
def run(self):
compile_and_install_svgflatten(Path(self.install_scripts))
super().run()
setup(
cmdclass={'install': CustomInstall},
name = 'gerbolyze',
version = '0.1.10',
version = '2.0.5',
py_modules = ['gerbolyze'],
scripts = ['gerbolyze'],
description = ('A high-resolution image-to-PCB converter. Gerbolyze reads and vectorizes black-and-white raster '
'images, then plots the vectorized image into an existing gerber file while avoiding existing features such as '
'text or holes.'),
package_dir = {'': 'gerbolyze'},
entry_points = '''
[console_scripts]
gerbolyze=gerbolyze:cli
''',
description = ('A high-resolution image-to-PCB converter. Gerbolyze plots SVG, PNG and JPG onto existing gerber '
'files. It handles almost the full SVG spec and deals with text, path outlines, patterns, arbitrary paths with '
'self-intersections and holes, etc. fully automatically. It can vectorize raster images both by contour '
'tracing and by grayscale dithering. All processing is done at the vector level without intermediate '
'conversions to raster images accurately preserving the input.'),
long_description=readme(),
long_description_content_type='text/x-rst',
url = 'https://github.com/jaseg/gerbolyze',
url = 'https://git.jaseg.de/gerbolyze',
author = 'jaseg',
author_email = 'github@jaseg.net',
install_requires = ['pcb-tools', 'tqdm', 'numpy', 'opencv-python'],
author_email = 'github@jaseg.de',
install_requires = ['pcb-tools', 'numpy', 'python-slugify', 'lxml', 'click', 'pcb-tools-extension'],
license = 'AGPLv3',
classifiers = [
'Development Status :: 5 - Production/Stable',
@ -34,3 +66,4 @@ setup(
'Topic :: Utilities'
]
)

85
svg-flatten/Makefile Normal file
View file

@ -0,0 +1,85 @@
CXX := clang++
LD ?= ld
INSTALL := install
PKG_CONFIG ?= pkg-config
BUILDDIR ?= build
PREFIX ?= /usr/local
UPSTREAM_DIR ?= ../upstream
SOURCES := src/svg_color.cpp \
src/svg_doc.cpp \
src/svg_geom.cpp \
src/svg_import_util.cpp \
src/svg_path.cpp \
src/svg_pattern.cpp \
src/vec_core.cpp \
src/vec_grid.cpp \
src/main.cpp \
src/out_svg.cpp \
src/out_gerber.cpp \
src/out_sexp.cpp \
src/out_flattener.cpp \
src/out_dilater.cpp \
src/lambda_sink.cpp \
$(UPSTREAM_DIR)/cpp-base64/base64.cpp
CLIPPER_SOURCES ?= $(UPSTREAM_DIR)/clipper-6.4.2/cpp/clipper.cpp $(UPSTREAM_DIR)/clipper-6.4.2/cpp/cpp_cairo/cairo_clipper.cpp
PUGIXML_SOURCES ?= $(UPSTREAM_DIR)/pugixml/src/pugixml.cpp
CLIPPER_INCLUDES ?= -I$(UPSTREAM_DIR)/clipper-6.4.2/cpp -I$(UPSTREAM_DIR)/clipper-6.4.2/cpp/cpp_cairo/
VORONOI_INCLUDES ?= -I$(UPSTREAM_DIR)/voronoi/src
POISSON_INCLUDES ?= -I$(UPSTREAM_DIR)/poisson-disk-sampling/thinks/poisson_disk_sampling/
BASE64_INCLUDES ?= -I$(UPSTREAM_DIR)/cpp-base64
ARGAGG_INCLUDES ?= -I$(UPSTREAM_DIR)/argagg/include/argagg
CAVC_INCLUDES ?= -I$(UPSTREAM_DIR)/CavalierContours/include/cavc/
# the folder name is subprocess.h
SUBPROCESS_INCLUDES ?= -I$(UPSTREAM_DIR)/subprocess.h
# make sure this appears after -Isvg-flatten/src on the compiler cmdline so pugixml loads the correct config
PUGIXML_INCLUDES ?= -I$(UPSTREAM_DIR)/pugixml/src
DEP_INCLUDES := $(CLIPPER_INCLUDES) $(VORONOI_INCLUDES) $(POISSON_INCLUDES) $(BASE64_INCLUDES) $(ARGAGG_INCLUDES)\
$(CAVC_INCLUDES) $(SUBPROCESS_INCLUDES) $(PUGIXML_INCLUDES)
SOURCES += $(CLIPPER_SOURCES)
SOURCES += $(PUGIXML_SOURCES)
INCLUDES := -Iinclude -Isrc $(DEP_INCLUDES)
PKG_CONFIG_DEPS := cairo
CXXFLAGS := -std=c++2a -g -Wall -Wextra -O2
CXXFLAGS += $(shell $(PKG_CONFIG) --cflags $(PKG_CONFIG_DEPS))
# hack for stone age opencv in debian stable
CXXFLAGS += $(shell $(PKG_CONFIG) --cflags opencv4 2> /dev/null || $(PKG_CONFIG) --cflags opencv 2>/dev/null)
LDFLAGS := -lm -lc -lstdc++
LDFLAGS += $(shell $(PKG_CONFIG) --libs $(PKG_CONFIG_DEPS))
# debian hack. see above.
OPENCV_LDFLAGS := $(shell $(PKG_CONFIG) --libs opencv4 2> /dev/null || $(PKG_CONFIG) --libs opencv 2>/dev/null)
LDFLAGS += $(shell echo $(OPENCV_LDFLAGS) | sed 's/-l\S\+ //g') -lopencv_core -lopencv_imgproc -lopencv_imgcodecs
TARGET := svg-flatten
all: $(BUILDDIR)/$(TARGET)
.PHONY: check-deps
check-deps:
@echo
@$(PKG_CONFIG) --cflags --libs cairo >/dev/null
# debian hack. see above.
@$(PKG_CONFIG) --cflags --libs opencv4 >/dev/null ||$(PKG_CONFIG) --cflags --libs opencv >/dev/null
$(BUILDDIR)/%.o: %.cpp
@mkdir -p $(dir $@)
$(CXX) -c $(CXXFLAGS) $(CXXFLAGS) $(INCLUDES) -o $@ $^
$(BUILDDIR)/$(TARGET): $(SOURCES:%.cpp=$(BUILDDIR)/%.o)
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ -Wl,--start-group $^ -lstdc++fs -Wl,--end-group
.PHONY: install
install:
$(INSTALL) $(BUILDDIR)/$(TARGET) $(PREFIX)/bin
.PHONY: clean
clean:
rm -rf $(BUILDDIR)

View file

@ -0,0 +1,272 @@
/*
* This file is part of gerbolyze, a vector image preprocessing toolchain
* Copyright (C) 2021 Jan Sebastian Götte <gerbolyze@jaseg.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <map>
#include <iostream>
#include <string>
#include <pugixml.hpp>
#include "svg_pattern.h"
namespace gerbolyze {
constexpr char lib_version[] = "2.0";
typedef std::array<double, 2> d2p;
typedef std::function<std::vector<d2p> *(double, double, double)> sampling_fun;
typedef std::vector<d2p> Polygon;
enum GerberPolarityToken {
GRB_POL_CLEAR,
GRB_POL_DARK
};
class LayerNameToken {
public:
std::string m_name;
};
class PolygonSink {
public:
virtual ~PolygonSink() {}
virtual void header(d2p origin, d2p size) {(void) origin; (void) size;}
virtual PolygonSink &operator<<(const Polygon &poly) = 0;
virtual PolygonSink &operator<<(const LayerNameToken &) { return *this; };
virtual PolygonSink &operator<<(GerberPolarityToken pol) = 0;
virtual void footer() {}
};
class Flattener_D;
class Flattener : public PolygonSink {
public:
Flattener(PolygonSink &sink);
virtual ~Flattener();
virtual void header(d2p origin, d2p size);
virtual Flattener &operator<<(const Polygon &poly);
virtual Flattener &operator<<(const LayerNameToken &layer_name);
virtual Flattener &operator<<(GerberPolarityToken pol);
virtual void footer();
private:
void render_out_clear_polys();
void flush_polys_to_sink();
PolygonSink &m_sink;
GerberPolarityToken m_current_polarity = GRB_POL_DARK;
Flattener_D *d;
};
class Dilater : public PolygonSink {
public:
Dilater(PolygonSink &sink, double dilation) : m_sink(sink), m_dilation(dilation) {}
virtual void header(d2p origin, d2p size);
virtual Dilater &operator<<(const Polygon &poly);
virtual Dilater &operator<<(const LayerNameToken &layer_name);
virtual Dilater &operator<<(GerberPolarityToken pol);
virtual void footer();
private:
PolygonSink &m_sink;
double m_dilation;
GerberPolarityToken m_current_polarity = GRB_POL_DARK;
};
class StreamPolygonSink : public PolygonSink {
public:
StreamPolygonSink(std::ostream &out, bool only_polys=false) : m_only_polys(only_polys), m_out(out) {}
virtual ~StreamPolygonSink() {}
virtual void header(d2p origin, d2p size) { if (!m_only_polys) header_impl(origin, size); }
virtual void footer() { if (!m_only_polys) { footer_impl(); } m_out.flush(); }
protected:
virtual void header_impl(d2p origin, d2p size) = 0;
virtual void footer_impl() = 0;
bool m_only_polys = false;
std::ostream &m_out;
};
extern const std::vector<std::string> kicad_default_layers;
class ElementSelector {
public:
virtual bool match(const pugi::xml_node &node, bool included, bool is_root) const = 0;
};
class IDElementSelector : public ElementSelector {
public:
virtual bool match(const pugi::xml_node &node, bool included, bool is_root) const;
std::vector<std::string> include;
std::vector<std::string> exclude;
const std::vector<std::string> *layers = nullptr;
};
class ImageVectorizer {
public:
virtual ~ImageVectorizer() {};
virtual void vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px) = 0;
};
ImageVectorizer *makeVectorizer(const std::string &name);
class VectorizerSelectorizer {
public:
VectorizerSelectorizer(const std::string default_vectorizer="dev-null", const std::string defs="");
ImageVectorizer *select(const pugi::xml_node &img);
private:
std::string m_default;
std::map<std::string, std::string> m_map;
};
class RenderSettings {
public:
double m_minimum_feature_size_mm = 0.1;
VectorizerSelectorizer &m_vec_sel;
};
class SVGDocument {
public:
SVGDocument() : _valid(false) {}
~SVGDocument();
/* true -> load successful */
bool load(std::istream &in, std::string debug_out_filename="/tmp/kicad_svg_debug.svg");
bool load(std::string filename, std::string debug_out_filename="/tmp/kicad_svg_debug.svg");
/* true -> load successful */
bool valid() const { return _valid; }
operator bool() const { return valid(); }
double mm_to_doc_units(double) const;
double doc_units_to_mm(double) const;
double width() const { return page_w_mm; }
double height() const { return page_h_mm; }
void render(const RenderSettings &rset, PolygonSink &sink, const ElementSelector *sel=nullptr);
void render_to_list(const RenderSettings &rset, std::vector<std::pair<Polygon, GerberPolarityToken>> &out, const ElementSelector *sel=nullptr);
private:
friend class Pattern;
cairo_t *cairo() { return cr; }
const ClipperLib::Paths *lookup_clip_path(const pugi::xml_node &node);
Pattern *lookup_pattern(const std::string id);
void export_svg_group(const RenderSettings &rset, const pugi::xml_node &group, ClipperLib::Paths &parent_clip_path, const ElementSelector *sel=nullptr, bool included=true, bool is_root=false);
void export_svg_path(const RenderSettings &rset, const pugi::xml_node &node, ClipperLib::Paths &clip_path);
void setup_debug_output(std::string filename="");
void setup_viewport_clip();
void load_clips();
void load_patterns();
bool _valid;
pugi::xml_document svg_doc;
pugi::xml_node root_elem;
pugi::xml_node defs_node;
double vb_x, vb_y, vb_w, vb_h;
double page_w, page_h;
double page_w_mm, page_h_mm;
std::map<std::string, Pattern> pattern_map;
std::map<std::string, ClipperLib::Paths> clip_path_map;
cairo_matrix_t viewport_matrix;
ClipperLib::Paths vb_paths; /* viewport clip rect */
cairo_t *cr = nullptr;
cairo_surface_t *surface = nullptr;
PolygonSink *polygon_sink = nullptr;
static constexpr double dbg_fill_alpha = 0.8;
static constexpr double dbg_stroke_alpha = 1.0;
static constexpr double assumed_usvg_dpi = 96.0;
};
typedef std::function<void (const Polygon &, GerberPolarityToken)> lambda_sink_fun;
class LambdaPolygonSink : public PolygonSink {
public:
LambdaPolygonSink(lambda_sink_fun lambda) : m_lambda(lambda) {}
virtual LambdaPolygonSink &operator<<(const Polygon &poly);
virtual LambdaPolygonSink &operator<<(GerberPolarityToken pol);
private:
GerberPolarityToken m_currentPolarity = GRB_POL_DARK;
lambda_sink_fun m_lambda;
};
class SimpleGerberOutput : public StreamPolygonSink {
public:
SimpleGerberOutput(std::ostream &out, bool only_polys=false, int digits_int=4, int digits_frac=6, double scale=1.0, d2p offset={0,0});
virtual ~SimpleGerberOutput() {}
virtual SimpleGerberOutput &operator<<(const Polygon &poly);
virtual SimpleGerberOutput &operator<<(GerberPolarityToken pol);
virtual void header_impl(d2p origin, d2p size);
virtual void footer_impl();
private:
int m_digits_int;
int m_digits_frac;
double m_width;
double m_height;
long long int m_gerber_scale;
d2p m_offset;
double m_scale;
};
class SimpleSVGOutput : public StreamPolygonSink {
public:
SimpleSVGOutput(std::ostream &out, bool only_polys=false, int digits_frac=6, std::string dark_color="#000000", std::string clear_color="#ffffff");
virtual ~SimpleSVGOutput() {}
virtual SimpleSVGOutput &operator<<(const Polygon &poly);
virtual SimpleSVGOutput &operator<<(GerberPolarityToken pol);
virtual void header_impl(d2p origin, d2p size);
virtual void footer_impl();
private:
int m_digits_frac;
std::string m_dark_color;
std::string m_clear_color;
std::string m_current_color;
d2p m_offset;
};
class KicadSexpOutput : public StreamPolygonSink {
public:
KicadSexpOutput(std::ostream &out, std::string mod_name, std::string layer, bool only_polys=false, std::string m_ref_text="", std::string m_val_text="G*****", d2p ref_pos={0,10}, d2p val_pos={0,-10});
virtual ~KicadSexpOutput() {}
virtual KicadSexpOutput &operator<<(const Polygon &poly);
virtual KicadSexpOutput &operator<<(const LayerNameToken &layer_name);
virtual KicadSexpOutput &operator<<(GerberPolarityToken pol);
virtual void header_impl(d2p origin, d2p size);
virtual void footer_impl();
void set_export_layers(const std::vector<std::string> &layers) { m_export_layers = &layers; }
private:
const std::vector<std::string> *m_export_layers = &kicad_default_layers;
std::string m_mod_name;
std::string m_layer;
bool m_auto_layer;
std::string m_ref_text;
std::string m_val_text;
d2p m_ref_pos;
d2p m_val_pos;
};
}

View file

@ -0,0 +1,77 @@
/**
* pugixml parser - version 1.11
* --------------------------------------------------------
* Copyright (C) 2006-2020, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
* Report bugs and download new versions at https://pugixml.org/
*
* This library is distributed under the MIT License. See notice at the end
* of this file.
*
* This work is based on the pugxml parser, which is:
* Copyright (C) 2003, by Kristen Wegner (kristen@tima.net)
*/
#ifndef HEADER_PUGICONFIG_HPP
#define HEADER_PUGICONFIG_HPP
// Uncomment this to enable wchar_t mode
// #define PUGIXML_WCHAR_MODE
// Uncomment this to enable compact mode
// #define PUGIXML_COMPACT
// Uncomment this to disable XPath
#define PUGIXML_NO_XPATH
// Uncomment this to disable STL
#define PUGIXML_NO_STL
// Uncomment this to disable exceptions
// #define PUGIXML_NO_EXCEPTIONS
// Set this to control attributes for public classes/functions, i.e.:
// #define PUGIXML_API __declspec(dllexport) // to export all public symbols from DLL
// #define PUGIXML_CLASS __declspec(dllimport) // to import all classes from DLL
// #define PUGIXML_FUNCTION __fastcall // to set calling conventions to all public functions to fastcall
// In absence of PUGIXML_CLASS/PUGIXML_FUNCTION definitions PUGIXML_API is used instead
// Tune these constants to adjust memory-related behavior
// #define PUGIXML_MEMORY_PAGE_SIZE 32768
// #define PUGIXML_MEMORY_OUTPUT_STACK 10240
// #define PUGIXML_MEMORY_XPATH_PAGE_SIZE 4096
// Tune this constant to adjust max nesting for XPath queries
// #define PUGIXML_XPATH_DEPTH_LIMIT 1024
// Uncomment this to switch to header-only version
// #define PUGIXML_HEADER_ONLY
// Uncomment this to enable long long support
// #define PUGIXML_HAS_LONG_LONG
#endif
/**
* Copyright (c) 2006-2020 Arseny Kapoulkine
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/

View file

@ -0,0 +1,36 @@
/*
* This file is part of gerbolyze, a vector image preprocessing toolchain
* Copyright (C) 2021 Jan Sebastian Götte <gerbolyze@jaseg.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <cmath>
#include <algorithm>
#include <string>
#include <gerbolyze.hpp>
#include <svg_import_defs.h>
using namespace gerbolyze;
using namespace std;
LambdaPolygonSink& LambdaPolygonSink::operator<<(const Polygon &poly) {
m_lambda(poly, m_currentPolarity);
return *this;
}
LambdaPolygonSink& LambdaPolygonSink::operator<<(GerberPolarityToken pol) {
m_currentPolarity = pol;
return *this;
}

441
svg-flatten/src/main.cpp Normal file
View file

@ -0,0 +1,441 @@
#include <cstdlib>
#include <cstdio>
#include <sys/types.h>
#include <pwd.h>
#include <filesystem>
#include <iostream>
#include <fstream>
#include <algorithm>
#include <string>
#include <argagg.hpp>
#include <subprocess.h>
#include <gerbolyze.hpp>
#include "vec_core.h"
#include <base64.h>
using argagg::parser_results;
using argagg::parser;
using namespace std;
using namespace gerbolyze;
int main(int argc, char **argv) {
parser argparser {{
{"help", {"-h", "--help"},
"Print help and exit",
0},
{"version", {"-v", "--version"},
"Print version and exit",
0},
{"ofmt", {"-o", "--format"},
"Output format. Supported: gerber, svg, s-exp (KiCAD S-Expression)",
1},
{"precision", {"-p", "--precision"},
"Number of decimal places use for exported coordinates (gerber: 1-9, SVG: 0-*)",
1},
{"svg_clear_color", {"--clear-color"},
"SVG color to use for \"clear\" areas (default: white)",
1},
{"svg_dark_color", {"--dark-color"},
"SVG color to use for \"dark\" areas (default: black)",
1},
{"min_feature_size", {"-d", "--trace-space"},
"Minimum feature size of elements in vectorized graphics (trace/space) in mm. Default: 0.1mm.",
1},
{"no_header", {"--no-header"},
"Do not export output format header/footer, only export the primitives themselves",
0},
{"flatten", {"--flatten"},
"Flatten output so it only consists of non-overlapping white polygons. This perform composition at the vector level. Potentially slow.",
0},
{"no_flatten", {"--no-flatten"},
"Disable automatic flattening for KiCAD S-Exp export",
0},
{"dilate", {"--dilate"},
"Dilate output gerber primitives by this amount in mm. Used for masking out other layers.",
1},
{"only_groups", {"-g", "--only-groups"},
"Comma-separated list of group IDs to export.",
1},
{"vectorizer", {"-b", "--vectorizer"},
"Vectorizer to use for bitmap images. One of poisson-disc (default), hex-grid, square-grid, binary-contours, dev-null.",
1},
{"vectorizer_map", {"--vectorizer-map"},
"Map from image element id to vectorizer. Overrides --vectorizer. Format: id1=vectorizer,id2=vectorizer,...",
1},
{"force_svg", {"--force-svg"},
"Force SVG input irrespective of file name",
0},
{"force_png", {"--force-png"},
"Force bitmap graphics input irrespective of file name",
0},
{"size", {"-s", "--size"},
"Bitmap mode only: Physical size of output image in mm. Format: 12.34x56.78",
1},
{"sexp_mod_name", {"--sexp-mod-name"},
"Module name for KiCAD S-Exp output",
1},
{"sexp_layer", {"--sexp-layer"},
"Layer for KiCAD S-Exp output. Defaults to auto-detect layers from SVG layer/top-level group names",
1},
{"preserve_aspect_ratio", {"-a", "--preserve-aspect-ratio"},
"Bitmap mode only: Preserve aspect ratio of image. Allowed values are meet, slice. Can also parse full SVG preserveAspectRatio syntax.",
1},
{"skip_usvg", {"--no-usvg"},
"Do not preprocess input using usvg (do not use unless you know *exactly* what you're doing)",
0},
{"usvg_dpi", {"--usvg-dpi"},
"Passed through to usvg's --dpi, in case the input file has different ideas of DPI than usvg has.",
1},
{"scale", {"--scale"},
"Scale input svg lengths by this factor.",
1},
{"exclude_groups", {"-e", "--exclude-groups"},
"Comma-separated list of group IDs to exclude from export. Takes precedence over --only-groups.",
1},
}};
ostringstream usage;
usage
<< argv[0] << " " << lib_version << endl
<< endl
<< "Usage: " << argv[0] << " [options]... [input_file] [output_file]" << endl
<< endl
<< "Specify \"-\" for stdin/stdout." << endl
<< endl;
argagg::parser_results args;
try {
args = argparser.parse(argc, argv);
} catch (const std::exception& e) {
argagg::fmt_ostream fmt(cerr);
fmt << usage.str() << argparser << '\n'
<< "Encountered exception while parsing arguments: " << e.what()
<< '\n';
return EXIT_FAILURE;
}
if (args["help"]) {
argagg::fmt_ostream fmt(cerr);
fmt << usage.str() << argparser;
return EXIT_SUCCESS;
}
if (args["version"]) {
cerr << lib_version << endl;
return EXIT_SUCCESS;
}
string in_f_name;
istream *in_f = &cin;
ifstream in_f_file;
string out_f_name;
ostream *out_f = &cout;
ofstream out_f_file;
if (args.pos.size() >= 1) {
in_f_name = args.pos[0];
if (args.pos.size() >= 2) {
out_f_name = args.pos[1];
}
}
if (!in_f_name.empty() && in_f_name != "-") {
in_f_file.open(in_f_name);
if (!in_f_file) {
cerr << "Cannot open input file \"" << in_f_name << "\"" << endl;
return EXIT_FAILURE;
}
in_f = &in_f_file;
}
if (!out_f_name.empty() && out_f_name != "-") {
out_f_file.open(out_f_name);
if (!out_f_file) {
cerr << "Cannot open output file \"" << out_f_name << "\"" << endl;
return EXIT_FAILURE;
}
out_f = &out_f_file;
}
bool only_polys = args["no_header"];
int precision = 6;
if (args["precision"]) {
precision = atoi(args["precision"]);
}
string fmt = args["ofmt"] ? args["ofmt"].as<string>() : "gerber";
transform(fmt.begin(), fmt.end(), fmt.begin(), [](unsigned char c){ return std::tolower(c); }); /* c++ yeah */
string sexp_layer = args["sexp_layer"] ? args["sexp_layer"].as<string>() : "auto";
bool force_flatten = false;
bool is_sexp = false;
PolygonSink *sink = nullptr;
PolygonSink *flattener = nullptr;
PolygonSink *dilater = nullptr;
if (fmt == "svg") {
string dark_color = args["svg_dark_color"] ? args["svg_dark_color"].as<string>() : "#000000";
string clear_color = args["svg_clear_color"] ? args["svg_clear_color"].as<string>() : "#ffffff";
sink = new SimpleSVGOutput(*out_f, only_polys, precision, dark_color, clear_color);
} else if (fmt == "gbr" || fmt == "grb" || fmt == "gerber") {
double scale = args["scale"].as<double>(1.0);
cerr << "loading @scale=" << scale << endl;
sink = new SimpleGerberOutput(*out_f, only_polys, 4, precision, scale);
} else if (fmt == "s-exp" || fmt == "sexp" || fmt == "kicad") {
if (!args["sexp_mod_name"]) {
cerr << "Error: --sexp-mod-name must be given for sexp export" << endl;
return EXIT_FAILURE;
}
sink = new KicadSexpOutput(*out_f, args["sexp_mod_name"], sexp_layer, only_polys);
force_flatten = true;
is_sexp = true;
} else {
cerr << "Error: Unknown output format \"" << fmt << "\"" << endl;
return EXIT_FAILURE;
}
PolygonSink *top_sink = sink;
if (args["dilate"]) {
dilater = new Dilater(*top_sink, args["dilate"].as<double>());
top_sink = dilater;
}
if (args["flatten"] || (force_flatten && !args["no_flatten"])) {
flattener = new Flattener(*top_sink);
top_sink = flattener;
}
/* Because the C++ stdlib is bullshit */
auto id_match = [](string in, vector<string> &out) {
stringstream ss(in);
while (getline(ss, out.emplace_back(), ',')) {
}
out.pop_back();
};
IDElementSelector sel;
if (args["only_groups"])
id_match(args["only_groups"], sel.include);
if (args["exclude_groups"])
id_match(args["exclude_groups"], sel.exclude);
if (is_sexp && sexp_layer == "auto") {
sel.layers = &gerbolyze::kicad_default_layers;
}
string vectorizer = args["vectorizer"] ? args["vectorizer"].as<string>() : "poisson-disc";
/* Check argument */
ImageVectorizer *vec = makeVectorizer(vectorizer);
if (!vec) {
cerr << "Unknown vectorizer \"" << vectorizer << "\"." << endl;
argagg::fmt_ostream fmt(cerr);
fmt << usage.str() << argparser;
return EXIT_FAILURE;
}
delete vec;
double min_feature_size = 0.1; /* mm */
if (args["min_feature_size"]) {
min_feature_size = args["min_feature_size"].as<double>();
}
string ending = "";
auto idx = in_f_name.rfind(".");
if (idx != string::npos) {
ending = in_f_name.substr(idx);
transform(ending.begin(), ending.end(), ending.begin(), [](unsigned char c){ return std::tolower(c); }); /* c++ yeah */
}
filesystem::path barf = { filesystem::temp_directory_path() /= (std::tmpnam(nullptr) + string(".svg")) };
filesystem::path frob = { filesystem::temp_directory_path() /= (std::tmpnam(nullptr) + string(".svg")) };
bool is_svg = args["force_svg"] || (ending == ".svg" && !args["force_png"]);
if (!is_svg) {
cerr << "writing bitmap into svg" << endl;
if (!args["size"]) {
cerr << "Error: --size must be given when using bitmap input." << endl;
return EXIT_FAILURE;
}
string sz = args["size"].as<string>();
auto pos = sz.find_first_of("x*,");
if (pos == string::npos) {
cerr << "Error: --size must be of form 12.34x56.78" << endl;
return EXIT_FAILURE;
}
string x_str = sz.substr(0, pos);
string y_str = sz.substr(pos+1);
double width = std::strtod(x_str.c_str(), nullptr);
double height = std::strtod(y_str.c_str(), nullptr);
if (width < 1 || height < 1) {
cerr << "Error: --size must be of form 12.34x56.78 and values must be positive floating-point numbers in mm" << endl;
return EXIT_FAILURE;
}
ofstream svg(barf.c_str());
svg << "<svg width=\"" << width << "mm\" height=\"" << height << "mm\" viewBox=\"0 0 "
<< width << " " << height << "\" "
<< "xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">" << endl;
string par_attr = "none";
if (args["preserve_aspect_ratio"]) {
string aspect_ratio = args["preserve_aspect_ratio"].as<string>();
if (aspect_ratio == "meet") {
par_attr = "xMidYMid meet";
} else if (aspect_ratio == "slice") {
par_attr = "xMidYMid slice";
} else {
par_attr = aspect_ratio;
}
}
svg << "<image width=\"" << width << "\" height=\"" << height << "\" x=\"0\" y=\"0\" preserveAspectRatio=\""
<< par_attr << "\" xlink:href=\"data:image/png;base64,";
/* c++ has the best hacks */
std::ostringstream sstr;
sstr << in_f->rdbuf();
string le_data = sstr.str();
svg << base64_encode(le_data);
svg << "\"/>" << endl;
svg << "</svg>" << endl;
svg.close();
} else { /* svg file */
cerr << "copying svg input into temp svg" << endl;
/* c++ has the best hacks */
std::ostringstream sstr;
sstr << in_f->rdbuf();
ofstream tmp_out(barf.c_str());
tmp_out << sstr.str();
tmp_out.close();
}
if (args["skip_usvg"]) {
cerr << "skipping usvg" << endl;
frob = barf;
} else {
cerr << "calling usvg on " << barf << " and " << frob << endl;
int dpi = 96;
if (args["usvg_dpi"]) {
dpi = args["usvg_dpi"].as<int>();
}
string dpi_str = to_string(dpi);
const char *homedir;
if ((homedir = getenv("HOME")) == NULL) {
homedir = getpwuid(getuid())->pw_dir;
}
string homedir_s(homedir);
string loc_in_home = homedir_s + "/.cargo/bin/usvg";
const char *command_line[] = {nullptr, "--keep-named-groups", "--dpi", dpi_str.c_str(), barf.c_str(), frob.c_str(), NULL};
bool found_usvg = false;
int usvg_rc=-1;
for (int i=0; i<3; i++) {
const char *usvg_envvar;
switch (i) {
case 0:
if ((usvg_envvar = getenv("USVG")) == NULL) {
continue;
} else {
command_line[0] = "usvg";
}
break;
case 1:
command_line[0] = "usvg";
break;
case 2:
command_line[0] = loc_in_home.c_str();
break;
}
struct subprocess_s subprocess;
int rc = subprocess_create(command_line, subprocess_option_inherit_environment, &subprocess);
if (rc) {
cerr << "Error calling usvg!" << endl;
return EXIT_FAILURE;
}
usvg_rc = -1;
rc = subprocess_join(&subprocess, &usvg_rc);
if (rc) {
cerr << "Error calling usvg!" << endl;
return EXIT_FAILURE;
}
rc = subprocess_destroy(&subprocess);
if (rc) {
cerr << "Error calling usvg!" << endl;
return EXIT_FAILURE;
}
if (usvg_rc == 255) {
continue;
}
found_usvg = true;
break;
}
if (!found_usvg) {
cerr << "Error: Cannot find usvg. Is it installed and in $PATH?" << endl;
return EXIT_FAILURE;
}
if (usvg_rc) {
cerr << "usvg returned an error code: " << usvg_rc << endl;
return EXIT_FAILURE;
}
}
VectorizerSelectorizer vec_sel(vectorizer, args["vectorizer_map"] ? args["vectorizer_map"].as<string>() : "");
RenderSettings rset {
min_feature_size,
vec_sel,
};
SVGDocument doc;
cerr << "Loading temporary file " << frob << endl;
ifstream load_f(frob);
if (!doc.load(load_f)) {
cerr << "Error loading input file \"" << in_f_name << "\", exiting." << endl;
return EXIT_FAILURE;
}
doc.render(rset, *top_sink, &sel);
remove(frob.c_str());
remove(barf.c_str());
if (flattener) {
delete flattener;
}
if (dilater) {
delete dilater;
}
if (sink) {
delete sink;
}
return EXIT_SUCCESS;
}

View file

@ -0,0 +1,86 @@
/*
* This file is part of gerbolyze, a vector image preprocessing toolchain
* Copyright (C) 2021 Jan Sebastian Götte <gerbolyze@jaseg.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <cmath>
#include <algorithm>
#include <string>
#include <iostream>
#include <iomanip>
#include <gerbolyze.hpp>
#include <clipper.hpp>
#include <svg_import_defs.h>
#include <svg_geom.h>
#include "polylinecombine.hpp"
using namespace gerbolyze;
using namespace std;
void Dilater::header(d2p origin, d2p size) {
m_sink.header(origin, size);
}
void Dilater::footer() {
m_sink.footer();
}
Dilater &Dilater::operator<<(const LayerNameToken &layer_name) {
m_sink << layer_name;
return *this;
}
Dilater &Dilater::operator<<(GerberPolarityToken pol) {
m_current_polarity = pol;
m_sink << pol;
return *this;
}
Dilater &Dilater::operator<<(const Polygon &poly) {
ClipperLib::Path poly_c;
for (auto &p : poly) {
poly_c.push_back({(ClipperLib::cInt)round(p[0] * clipper_scale), (ClipperLib::cInt)round(p[1] * clipper_scale)});
}
ClipperLib::ClipperOffset offx;
offx.ArcTolerance = 0.05 * clipper_scale; /* 10µm; TODO: Make this configurable */
offx.AddPath(poly_c, ClipperLib::jtRound, ClipperLib::etClosedPolygon);
double dilation = m_dilation;
if (m_current_polarity == GRB_POL_CLEAR) {
dilation = -dilation;
}
ClipperLib::PolyTree solution;
offx.Execute(solution, dilation * clipper_scale);
ClipperLib::Paths c_nice_polys;
dehole_polytree(solution, c_nice_polys);
for (auto &nice_poly : c_nice_polys) {
Polygon new_poly;
for (auto &p : nice_poly) {
new_poly.push_back({
(double)p.X / clipper_scale,
(double)p.Y / clipper_scale });
}
m_sink << new_poly;
}
return *this;
}

View file

@ -0,0 +1,187 @@
/*
* This file is part of gerbolyze, a vector image preprocessing toolchain
* Copyright (C) 2021 Jan Sebastian Götte <gerbolyze@jaseg.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <cmath>
#include <algorithm>
#include <string>
#include <iostream>
#include <iomanip>
#include <gerbolyze.hpp>
#include <svg_import_defs.h>
#include <svg_geom.h>
#include "polylinecombine.hpp"
using namespace gerbolyze;
using namespace std;
static void polygon_to_cavc (const Polygon &in, cavc::Polyline<double> &out) {
for (auto &p : in) {
out.addVertex(p[0], p[1], 0);
}
out.isClosed() = true; /* sic! */
}
static void cavc_to_polygon (const cavc::Polyline<double> &in, Polygon &out) {
for (auto &p : in.vertexes()) {
out.emplace_back(d2p{p.x(), p.y()});
}
}
namespace gerbolyze {
class Flattener_D {
public:
vector<cavc::Polyline<double>> dark_polys;
vector<cavc::Polyline<double>> clear_polys;
void add_dark_polygon(const Polygon &in) {
polygon_to_cavc(in, dark_polys.emplace_back());
}
void add_clear_polygon(const Polygon &in) {
polygon_to_cavc(in, clear_polys.emplace_back());
}
};
}
Flattener::Flattener(PolygonSink &sink) : m_sink(sink) {
d = new Flattener_D();
}
Flattener::~Flattener() {
delete d;
}
void Flattener::header(d2p origin, d2p size) {
m_sink.header(origin, size);
}
void Flattener::render_out_clear_polys() {
for (auto &sub : d->clear_polys) {
vector<cavc::Polyline<double>> new_dark_polys;
new_dark_polys.reserve(d->dark_polys.size());
for (cavc::Polyline<double> cavc_in : d->dark_polys) {
auto res = cavc::combinePolylines(cavc_in, sub, cavc::PlineCombineMode::Exclude);
if (res.subtracted.size() == 0) {
for (auto &rem : res.remaining) {
new_dark_polys.push_back(std::move(rem));
}
} else { /* custom one-hole deholing code */
assert (res.remaining.size() == 1);
assert (res.subtracted.size() == 1);
auto &rem = res.remaining[0];
auto &sub = res.subtracted[0];
auto bbox = getExtents(rem);
cavc::Polyline<double> quad;
quad.addVertex(bbox.xMin, bbox.yMin, 0);
if (sub.vertexes()[0].x() < sub.vertexes()[1].x()) {
quad.addVertex(sub.vertexes()[0]);
quad.addVertex(sub.vertexes()[1]);
} else {
quad.addVertex(sub.vertexes()[1]);
quad.addVertex(sub.vertexes()[0]);
}
quad.addVertex(bbox.xMax, bbox.yMin, 0);
quad.isClosed() = true; /* sic! */
auto res2 = cavc::combinePolylines(rem, quad, cavc::PlineCombineMode::Exclude);
assert (res2.subtracted.size() == 0);
for (auto &rem : res2.remaining) {
auto res3 = cavc::combinePolylines(rem, sub, cavc::PlineCombineMode::Exclude);
assert (res3.subtracted.size() == 0);
for (auto &p : res3.remaining) {
new_dark_polys.push_back(std::move(p));
}
}
auto res4 = cavc::combinePolylines(rem, quad, cavc::PlineCombineMode::Intersect);
assert (res4.subtracted.size() == 0);
for (auto &rem : res4.remaining) {
auto res5 = cavc::combinePolylines(rem, sub, cavc::PlineCombineMode::Exclude);
assert (res5.subtracted.size() == 0);
for (auto &p : res5.remaining) {
new_dark_polys.push_back(std::move(p));
}
}
}
}
d->dark_polys = std::move(new_dark_polys);
}
d->clear_polys.clear();
}
Flattener &Flattener::operator<<(GerberPolarityToken pol) {
if (m_current_polarity != pol) {
m_current_polarity = pol;
if (pol == GRB_POL_DARK) {
render_out_clear_polys();
}
}
return *this;
}
Flattener &Flattener::operator<<(const LayerNameToken &layer_name) {
flush_polys_to_sink();
m_sink << layer_name;
cerr << "Flattener forwarding layer name to sink: \"" << layer_name.m_name << "\"" << endl;
return *this;
}
Flattener &Flattener::operator<<(const Polygon &poly) {
if (m_current_polarity == GRB_POL_DARK) {
d->add_dark_polygon(poly);
} else { /* clear */
d->add_clear_polygon(poly);
render_out_clear_polys();
}
return *this;
}
void Flattener::flush_polys_to_sink() {
*this << GRB_POL_DARK; /* force render */
m_sink << GRB_POL_DARK;
for (auto &poly : d->dark_polys) {
Polygon poly_out;
for (auto &p : poly.vertexes()) {
poly_out.emplace_back(d2p{p.x(), p.y()});
}
m_sink << poly_out;
}
d->clear_polys.clear();
d->dark_polys.clear();
}
void Flattener::footer() {
flush_polys_to_sink();
m_sink.footer();
}

View file

@ -0,0 +1,100 @@
/*
* This file is part of gerbolyze, a vector image preprocessing toolchain
* Copyright (C) 2021 Jan Sebastian Götte <gerbolyze@jaseg.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <cmath>
#include <algorithm>
#include <string>
#include <iostream>
#include <iomanip>
#include <gerbolyze.hpp>
#include <svg_import_defs.h>
using namespace gerbolyze;
using namespace std;
SimpleGerberOutput::SimpleGerberOutput(ostream &out, bool only_polys, int digits_int, int digits_frac, double scale, d2p offset)
: StreamPolygonSink(out, only_polys),
m_digits_int(digits_int),
m_digits_frac(digits_frac),
m_offset(offset),
m_scale(scale)
{
assert(1 <= digits_int && digits_int <= 9);
assert(0 <= digits_frac && digits_frac <= 9);
m_gerber_scale = round(pow(10, m_digits_frac));
}
void SimpleGerberOutput::header_impl(d2p origin, d2p size) {
m_offset[0] += origin[0] * m_scale;
m_offset[1] += origin[1] * m_scale;
m_width = (size[0] - origin[0]) * m_scale;
m_height = (size[1] - origin[1]) * m_scale;
if (pow(10, m_digits_int-1) < max(m_width, m_height)) {
cerr << "Warning: Input has bounding box too large for " << m_digits_int << "." << m_digits_frac << " gerber resolution!" << endl;
}
m_out << "%FSLAX" << m_digits_int << m_digits_frac << "Y" << m_digits_int << m_digits_frac << "*%" << endl;
m_out << "%MOMM*%" << endl;
m_out << "%LPD*%" << endl;
m_out << "G01*" << endl;
m_out << "%ADD10C,0.050000*%" << endl;
m_out << "D10*" << endl;
}
SimpleGerberOutput& SimpleGerberOutput::operator<<(GerberPolarityToken pol) {
if (pol == GRB_POL_DARK) {
m_out << "%LPD*%" << endl;
} else if (pol == GRB_POL_CLEAR) {
m_out << "%LPC*%" << endl;
} else {
assert(false);
}
return *this;
}
SimpleGerberOutput& SimpleGerberOutput::operator<<(const Polygon &poly) {
if (poly.size() < 3) {
cerr << "Warning: " << poly.size() << "-element polygon passed to SimpleGerberOutput" << endl;
return *this;
}
/* NOTE: Clipper and gerber both have different fixed-point scales. We get points in double mm. */
double x = round((poly[0][0] * m_scale + m_offset[0]) * m_gerber_scale);
double y = round((m_height - poly[0][1] * m_scale + m_offset[1]) * m_gerber_scale);
m_out << "G36*" << endl;
m_out << "X" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal /* isn't C++ a marvel of engineering? */ << (long long int)x
<< "Y" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal << (long long int)y
<< "D02*" << endl;
m_out << "G01*" << endl;
for (size_t i=1; i<poly.size(); i++) {
double x = round((poly[i][0] * m_scale + m_offset[0]) * m_gerber_scale);
double y = round((m_height - poly[i][1] * m_scale + m_offset[1]) * m_gerber_scale);
m_out << "X" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal << (long long int)x
<< "Y" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal << (long long int)y
<< "D01*" << endl;
}
m_out << "G37*" << endl;
return *this;
}
void SimpleGerberOutput::footer_impl() {
m_out << "M02*" << endl;
}

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