Compare commits
2 commits
static-com
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfcf4c5395 | ||
|
|
9a6ab822f2 |
|
|
@ -1,38 +0,0 @@
|
||||||
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
|
|
@ -1,21 +1,6 @@
|
||||||
[submodule "upstream/cpp-base64"]
|
[submodule "gerboweb/deploy/checkouts/pogojig"]
|
||||||
path = upstream/cpp-base64
|
path = gerboweb/deploy/checkouts/pogojig
|
||||||
url = https://github.com/ReneNyffenegger/cpp-base64
|
url = https://github.com/jaseg/pogojig.git
|
||||||
[submodule "upstream/voronoi"]
|
[submodule "gerboweb/deploy/library/ansible-collection"]
|
||||||
path = upstream/voronoi
|
path = gerboweb/deploy/library/inwx-collection
|
||||||
url = https://github.com/JCash/voronoi
|
url = https://github.com/inwx/ansible-collection
|
||||||
[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
|
|
||||||
|
|
|
||||||
11
MANIFEST.in
|
|
@ -1,10 +1 @@
|
||||||
recursive-include svg-flatten *
|
include README.rst
|
||||||
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 *
|
|
||||||
|
|
|
||||||
684
README.rst
|
|
@ -1,546 +1,87 @@
|
||||||
Gerbolyze high-fidelity SVG/PNG/JPG to PCB converter
|
Gerbolyze high-resolution image-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
|
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
|
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.
|
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 SVG vector and PNG or JPG bitmap files
|
Gerbolyze solves this problem in a toolchain-agnostic way by directly vectorizing bitmap files onto existing gerber
|
||||||
onto existing gerber layers. Gerbolyze processes any spec-compliant SVG and "gerbolyzes" SVG vector data into a Gerber
|
layers. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the industry-standard Altium
|
||||||
spec-compliant form. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the
|
Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of thousands of primitives,
|
||||||
industry-standard Altium Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of
|
generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can finally be confident that
|
||||||
thousands of primitives, generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can
|
your PCB fab's toolchain will fall over before yours does if you overdo it with the high-poly anime silkscreen.
|
||||||
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::
|
.. contents::
|
||||||
|
|
||||||
Tl;dr: Produce high-quality artistic PCBs in three easy steps!
|
Produce high-quality artistic PCBs in three easy steps!
|
||||||
--------------------------------------------------------------
|
-------------------------------------------------------
|
||||||
|
|
||||||
Gerbolyze works in three steps.
|
Gerbolyze works in three steps.
|
||||||
|
|
||||||
1. Generate a scale-accurate template of the finished PCB from your CAD tool's gerber output:
|
1. Generate a scale-accurate preview of the finished PCB from your CAD tool's gerber output:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
$ gerbolyze template --top template_top.svg [--bottom template_bottom.svg] my_gerber_dir
|
$ gerbolyze render top my_gerber_dir preview.png
|
||||||
|
|
||||||
2. Load the resulting template image Inkscape_ or another SVG editing program. Put your artwork on the appropriate SVG
|
2. Load the resulting preview image into the GIMP or another image editing program. Use it as a guide to position scale
|
||||||
layer. Dark colors become filled gerber primitives, bright colors become unfilled primitives. You can directly put
|
your artwork. Create a black-and-white image from your scaled artwork using GIMP's newsprint filter. Make sure most
|
||||||
raster images (PNG/JPG) into this SVG as well, just position and scale them like everything else. SVG clips work for
|
details are larger than about 10px to ensure manufacturing goes smooth.
|
||||||
images, too. Masks are not supported.
|
|
||||||
|
|
||||||
3. Vectorize the edited SVG template image drectly into the PCB's gerber files:
|
3. Vectorize the resulting grayscale image drectly into the PCB's gerber files:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
$ gerbolyze paste --top template_top_edited.svg [--bottom ...] my_gerber_dir output_gerber_dir
|
$ gerbolyze vectorize top input_gerber_dir output_gerber_dir black_and_white_artwork.png
|
||||||
|
|
||||||
Quick Start Installation
|
Image preprocessing guide
|
||||||
------------------------
|
-------------------------
|
||||||
|
|
||||||
This will install gerbolyze and svg-flatten into a Python virtualenv and install usvg into your ``~/.cargo``.
|
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.
|
||||||
|
|
||||||
Note:
|
1 Import a render of the board generated using ``gerbolyze render``
|
||||||
Right now (2020-02-07), ``pcb-tools-extension`` must be installed manually from the fork at:
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
``pip3 install --user git+https://git.jaseg.de/pcb-tools-extension.git``
|
``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.
|
||||||
|
|
||||||
This fork contains fixes for compatibility issues with KiCAD nightlies that are still in the process of being
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/01import01.png
|
||||||
upstreamed.
|
|
||||||
|
|
||||||
Debian
|
2 Import your desired artwork
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
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
|
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
|
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.
|
is hard to vizualize, but the grain resulting from the low resolution of a PCB's silkscreen is quite coarse.
|
||||||
|
|
||||||
.. image:: screenshots/02import02.png
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/02import02.png
|
||||||
:width: 800px
|
|
||||||
|
|
||||||
2 Convert the image to grayscale
|
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
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. image:: screenshots/06grayscale.png
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/06grayscale.png
|
||||||
:width: 800px
|
|
||||||
|
|
||||||
3 Fine-tune the image's contrast
|
6 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
|
To look well on the PCB, contrast is critical. If your source image is in color, you may have lost some contrast during
|
||||||
|
|
@ -551,10 +92,9 @@ 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
|
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.
|
white and very dark grays to black while preserving the values in the middle.
|
||||||
|
|
||||||
.. image:: screenshots/08curve_cut.png
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/08curve_cut.png
|
||||||
:width: 800px
|
|
||||||
|
|
||||||
4 Retouch details
|
7 Retouch details
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Therer might be small details that don't look right yet, such as the image's background color or small highlights that
|
Therer might be small details that don't look right yet, such as the image's background color or small highlights that
|
||||||
|
|
@ -565,16 +105,14 @@ 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
|
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.
|
newsprint filter's cell size (10px) to smooth out the dot pattern generated by the newsprint filter.
|
||||||
|
|
||||||
.. image:: screenshots/09retouch.png
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/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
|
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.
|
white instead of light-gray, so they still stand out nicely in the finished picture.
|
||||||
|
|
||||||
.. image:: screenshots/10retouched.png
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/10retouched.png
|
||||||
:width: 800px
|
|
||||||
|
|
||||||
5 Run the newsprint filter
|
8 Run the newsprint filter
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Now, run the GIMP's newsprint filter, under filters, distorts, newsprint.
|
Now, run the GIMP's newsprint filter, under filters, distorts, newsprint.
|
||||||
|
|
@ -585,124 +123,32 @@ 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
|
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``.
|
of the edge reconstruction of ``gerbolyze vectorize``.
|
||||||
|
|
||||||
.. image:: screenshots/11newsprint.png
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/11newsprint.png
|
||||||
:width: 800px
|
|
||||||
|
|
||||||
The following are examples on the detail resulting from the newsprint filter.
|
The following are examples on the detail resulting from the newsprint filter.
|
||||||
|
|
||||||
.. image:: screenshots/12newsprint.png
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/12newsprint.png
|
||||||
:width: 800px
|
|
||||||
|
|
||||||
6 Export the image for use with ``gerbolyze vectorize``
|
.. 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``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Simply export the image as a PNG file. Below are some pictures of the output ``gerbolyze vectorize`` produced for this
|
Simply export the image as a PNG file. Below are some pictures of the output ``gerbolyze vectorize`` produced for this
|
||||||
example.
|
example.
|
||||||
|
|
||||||
.. image:: screenshots/14result_cut.png
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/14result_cut.png
|
||||||
:width: 800px
|
|
||||||
|
|
||||||
.. image:: screenshots/15result_cut.png
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/15result_cut.png
|
||||||
:width: 800px
|
|
||||||
|
|
||||||
Manufacturing Considerations
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/16result_cut.png
|
||||||
----------------------------
|
|
||||||
|
|
||||||
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
|
Gallery
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. image:: pics/sample3.jpg
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample2.jpg
|
||||||
:width: 400px
|
|
||||||
|
|
||||||
Licensing
|
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample3.jpg
|
||||||
---------
|
|
||||||
|
|
||||||
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
|
|
@ -1,8 +0,0 @@
|
||||||
[ ] 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?)
|
|
||||||
394
Untitled_17-11-19_18-00-15.ipynb
Normal file
|
|
@ -16,6 +16,7 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
vectorize_parser.add_argument('side', choices=['top', 'bottom'], help='Target board side')
|
vectorize_parser.add_argument('side', choices=['top', 'bottom'], help='Target board side')
|
||||||
vectorize_parser.add_argument('--layer', '-l', choices=['silk', 'mask', 'copper'], default='silk', help='Target layer on given side')
|
vectorize_parser.add_argument('--layer', '-l', choices=['silk', 'mask', 'copper'], default='silk', help='Target layer on given side')
|
||||||
|
vectorize_parser.add_argument('--exact', '-x', action='store_true', default=False, help='Do not subtract existing features on other layers from overlay')
|
||||||
|
|
||||||
vectorize_parser.add_argument('source', help='Source gerber directory')
|
vectorize_parser.add_argument('source', help='Source gerber directory')
|
||||||
vectorize_parser.add_argument('target', help='Target gerber directory')
|
vectorize_parser.add_argument('target', help='Target gerber directory')
|
||||||
|
|
@ -29,7 +30,7 @@ if __name__ == '__main__':
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.command == 'vectorize':
|
if args.command == 'vectorize':
|
||||||
gerbolyze.process_gerbers(args.source, args.target, args.image, args.side, args.layer, args.debugdir)
|
gerbolyze.process_gerbers(args.source, args.target, args.image, args.side, args.layer, args.exact, args.debugdir)
|
||||||
else: # command == render
|
else: # command == render
|
||||||
gerbolyze.render_preview(args.source, args.image, args.side, args.fab_resolution, args.oversampling)
|
gerbolyze.render_preview(args.source, args.image, args.side, args.fab_resolution, args.oversampling)
|
||||||
|
|
||||||
372
gerbolyze.py
Executable file
|
|
@ -0,0 +1,372 @@
|
||||||
|
#!/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|-F_Silkscreen.gbr',
|
||||||
|
'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|-B_Silkscreen.gbr',
|
||||||
|
'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, exact, 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 and not exact else []
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -1,667 +0,0 @@
|
||||||
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()
|
|
||||||
4
gerboweb/deploy/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
*_secret.txt
|
||||||
|
*_apikey.txt
|
||||||
|
playbook.retry
|
||||||
|
credentials.ini
|
||||||
60
gerboweb/deploy/bootstrap_arch_container.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
- 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
|
||||||
|
|
||||||
BIN
gerboweb/deploy/cgit-logo.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
20
gerboweb/deploy/cgitrc
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
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
gerboweb/deploy/checkouts/pogojig
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 13a57211f0d0feb34b452b3e19be83a095707ed6
|
||||||
36
gerboweb/deploy/clippy-nspawn.service
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# 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
|
||||||
2
gerboweb/deploy/clippy.nspawn
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[Network]
|
||||||
|
VirtualEthernet=no
|
||||||
9
gerboweb/deploy/clippy.service.j2
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[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
|
||||||
3
gerboweb/deploy/credentials.ini.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[inwx]
|
||||||
|
user=...
|
||||||
|
pass=...
|
||||||
9
gerboweb/deploy/gerboweb-job-processor.service.j2
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[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
|
||||||
4
gerboweb/deploy/gerboweb.cfg.j2
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
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"
|
||||||
202
gerboweb/deploy/gitolite.rc
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
# 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:
|
||||||
11
gerboweb/deploy/inventory.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
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}}"
|
||||||
27
gerboweb/deploy/iptables.rules
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# 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
gerboweb/deploy/library/inwx-collection
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 0ac040da14cc9d834098addc03cd8d4d26647df0
|
||||||
474
gerboweb/deploy/mirrorlist
Normal file
|
|
@ -0,0 +1,474 @@
|
||||||
|
##
|
||||||
|
## 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
|
||||||
|
|
||||||
412
gerboweb/deploy/nginx.conf
Normal file
|
|
@ -0,0 +1,412 @@
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
52
gerboweb/deploy/nginx_nossl.conf
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
179
gerboweb/deploy/notification_proxy.py
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
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()
|
||||||
|
|
||||||
9
gerboweb/deploy/notification_proxy_config.py.j2
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
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')}}'
|
||||||
111
gerboweb/deploy/playbook.yml
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
- 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
|
||||||
9
gerboweb/deploy/pogojig-job-processor.service.j2
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[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
|
||||||
4
gerboweb/deploy/pogojig.cfg.j2
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
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"
|
||||||
25
gerboweb/deploy/pogojig_generate.sh.j2
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/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"
|
||||||
20
gerboweb/deploy/render.sh.j2
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/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"
|
||||||
1
gerboweb/deploy/secure_download.cfg.j2
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
SERVE_PATH="{{secure_download_dir}}"
|
||||||
85
gerboweb/deploy/setup_clippy.yml
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
---
|
||||||
|
- 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
|
||||||
|
|
||||||
17
gerboweb/deploy/setup_containers.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
- 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
|
||||||
|
|
||||||
100
gerboweb/deploy/setup_gerboweb.yml
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
- 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
|
||||||
|
|
||||||
115
gerboweb/deploy/setup_git.yml
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
- 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] }}"
|
||||||
|
|
||||||
61
gerboweb/deploy/setup_notification_proxy.yml
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
- 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
|
||||||
|
|
||||||
|
|
||||||
9
gerboweb/deploy/setup_openjscad.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
- name: Copy openjscad webapp sources
|
||||||
|
synchronize:
|
||||||
|
# FIXME: make this path configurable
|
||||||
|
src: ~/openjscad_dist/
|
||||||
|
dest: /var/www/openjscad.jaseg.net/
|
||||||
|
group: no
|
||||||
|
owner: no
|
||||||
|
|
||||||
125
gerboweb/deploy/setup_pogojig.yml
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
---
|
||||||
|
- 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
|
||||||
|
|
||||||
57
gerboweb/deploy/setup_secure_download.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
- 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
|
||||||
|
|
||||||
9
gerboweb/deploy/setup_tracespace.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
- name: Copy tracespace webapp sources
|
||||||
|
synchronize:
|
||||||
|
# FIXME: make this path configurable
|
||||||
|
src: ~/tracespace_dist/
|
||||||
|
dest: /var/www/tracespace.jaseg.net/
|
||||||
|
group: no
|
||||||
|
owner: no
|
||||||
|
|
||||||
77
gerboweb/deploy/setup_webserver.yml
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
- 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
|
||||||
|
|
||||||
1
gerboweb/deploy/tmpfiles-gerboweb.conf.j2
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
d {{gerboweb_cache}} 770 uwsgi-gerboweb uwsgi 2d
|
||||||
1
gerboweb/deploy/tmpfiles-pogojig.conf.j2
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
d {{pogojig_cache}} 770 uwsgi-pogojig uwsgi 2d
|
||||||
1
gerboweb/deploy/tmpfiles-secure-download.conf.j2
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
d {{secure_download_dir}} 770 uwsgi-download uwsgi 45d
|
||||||
16
gerboweb/deploy/uwsgi-app@.service
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[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
|
||||||
11
gerboweb/deploy/uwsgi-app@.socket
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Socket for uWSGI app %i
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=/run/uwsgi/%i.socket
|
||||||
|
SocketUser=uwsgi-%i
|
||||||
|
SocketGroup=nginx
|
||||||
|
SocketMode=0660
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
8
gerboweb/deploy/uwsgi-cgit.ini
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[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
|
||||||
10
gerboweb/deploy/uwsgi-gerboweb.ini
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[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
|
||||||
|
|
||||||
10
gerboweb/deploy/uwsgi-notification-proxy.ini
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[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
|
||||||
|
|
||||||
10
gerboweb/deploy/uwsgi-pogojig.ini
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[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
|
||||||
|
|
||||||
11
gerboweb/deploy/uwsgi-secure-download.ini
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[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
|
||||||
|
|
||||||
18
gerboweb/deploy/vector.sh.j2
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/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"
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ def index():
|
||||||
|
|
||||||
r = make_response(render_template('index.html',
|
r = make_response(render_template('index.html',
|
||||||
has_renders = path.isfile(tempfile_path('gerber.zip')),
|
has_renders = path.isfile(tempfile_path('gerber.zip')),
|
||||||
has_output = path.isfile(tempfile_path('overlay.svg')),
|
has_output = path.isfile(tempfile_path('overlay.png')),
|
||||||
**forms))
|
**forms))
|
||||||
if 'vector_job' in session or 'render_job' in session:
|
if 'vector_job' in session or 'render_job' in session:
|
||||||
r.headers.set('refresh', '10')
|
r.headers.set('refresh', '10')
|
||||||
|
|
@ -108,7 +108,7 @@ def upload_gerber():
|
||||||
session['filename'] = secure_filename(f.filename) # Cache filename for later download
|
session['filename'] = secure_filename(f.filename) # Cache filename for later download
|
||||||
|
|
||||||
render()
|
render()
|
||||||
if path.isfile(tempfile_path('overlay.svg')): # Re-vectorize when gerbers change
|
if path.isfile(tempfile_path('overlay.png')): # Re-vectorize when gerbers change
|
||||||
vectorize()
|
vectorize()
|
||||||
|
|
||||||
flash(f'Gerber file successfully uploaded.', 'success')
|
flash(f'Gerber file successfully uploaded.', 'success')
|
||||||
|
|
@ -121,7 +121,7 @@ def upload_overlay():
|
||||||
if upload_form.validate_on_submit():
|
if upload_form.validate_on_submit():
|
||||||
# FIXME raise error when no side selected
|
# FIXME raise error when no side selected
|
||||||
f = upload_form.upload_file.data
|
f = upload_form.upload_file.data
|
||||||
f.save(tempfile_path('overlay.svg'))
|
f.save(tempfile_path('overlay.png'))
|
||||||
session['side_selected'] = upload_form.side.data
|
session['side_selected'] = upload_form.side.data
|
||||||
|
|
||||||
vectorize()
|
vectorize()
|
||||||
|
|
@ -133,7 +133,7 @@ def upload_overlay():
|
||||||
def render_preview(side):
|
def render_preview(side):
|
||||||
if not side in ('top', 'bottom'):
|
if not side in ('top', 'bottom'):
|
||||||
return abort(400, 'side must be either "top" or "bottom"')
|
return abort(400, 'side must be either "top" or "bottom"')
|
||||||
return send_file(tempfile_path(f'template_{side}.preview.png'))
|
return send_file(tempfile_path(f'render_{side}.small.png'))
|
||||||
|
|
||||||
@app.route('/render/download/<side>')
|
@app.route('/render/download/<side>')
|
||||||
def 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"')
|
return abort(400, 'side must be either "top" or "bottom"')
|
||||||
|
|
||||||
session['last_download'] = side
|
session['last_download'] = side
|
||||||
return send_file(tempfile_path(f'template_{side}.svg'),
|
return send_file(tempfile_path(f'render_{side}.png'),
|
||||||
mimetype='image/svg',
|
mimetype='image/png',
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename=f'{path.splitext(session["filename"])[0]}_template_{side}.svg')
|
attachment_filename=f'{path.splitext(session["filename"])[0]}_render_{side}.png')
|
||||||
|
|
||||||
@app.route('/output/download')
|
@app.route('/output/download')
|
||||||
def output_download():
|
def output_download():
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 509 KiB After Width: | Height: | Size: 246 KiB |
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Gerbolyze Image to PCB Toolchain</title>
|
<title>Gerbolyze Raster image to PCB renderer</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='style.css')}}">
|
<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="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')}}">
|
<link rel="apple-touch-icon" href="{{url_for('static', filename='favicon-512.png')}}">
|
||||||
|
|
@ -10,159 +10,157 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="layout-container">
|
<div class="layout-container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
<h1>SVG/JPG/PNG to PCB converter</h1>
|
<h1>Raster image to PCB converter</h1>
|
||||||
<p>
|
<p>
|
||||||
This is the toy web frontend to <a href="https://github.com/jaseg/gerbolyze">Gerbolyze</a>.
|
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
|
||||||
Gerbolyze is a tool for rendering arbitrary vector (SVG) and raster (PNG/JPG) images directly onto gerber layers.
|
image that is vectorized and rendered into an existing gerber file. Gerbolyze works with gerber files
|
||||||
You can use this to put art on a PCB's silkscreen, solder mask or copper layers. The input is an SVG file
|
produced with any EDA toolchain and has been tested to work with both Altium and KiCAD.
|
||||||
generated from a template. This SVG file has one layer for each PCB layer and the layers are rendered one by one
|
</p>
|
||||||
into the existing gerber file. SVG primitives are converted as-is with (almost) full SVG support, and bitmap
|
</div>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="flashes">
|
<div class="flashes">
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<div class="flash flash-{{category}}">{{ message }}</div>
|
<div class="flash flash-{{category}}">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<form id="reset-form" method="POST" action="{{url_for('session_reset')}}" class="reset-form">{{reset_form.csrf_token}}</form>
|
<form id="reset-form" method="POST" action="{{url_for('session_reset')}}" class="reset-form">{{reset_form.csrf_token}}</form>
|
||||||
|
|
||||||
<div class="steps">
|
<div class="steps">
|
||||||
<div class="step" id="step1">
|
<div class="step" id="step1">
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<h2>Upload zipped gerber files</h2>
|
<h2>Upload zipped gerber files</h2>
|
||||||
<p>
|
<p>
|
||||||
First, upload a zip file containing all your gerber files. The default file names used by KiCAD, Eagle
|
First, upload a zip file containing all your gerber files. The default file names used by KiCAD, Eagle
|
||||||
and Altium are supported.
|
and Altium are supported.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<form id="gerber-upload-form" method="POST" action="{{url_for('upload_gerber')}}" enctype="multipart/form-data">
|
<form id="gerber-upload-form" method="POST" action="{{url_for('upload_gerber')}}" enctype="multipart/form-data">
|
||||||
{{gerber_form.csrf_token}}
|
{{gerber_form.csrf_token}}
|
||||||
</form>
|
</form>
|
||||||
<div class="form-controls">
|
<div class="form-controls">
|
||||||
<div class="upload-label">Upload Gerber file:</div>
|
<div class="upload-label">Upload Gerber file:</div>
|
||||||
<input class='upload-button' form="gerber-upload-form" name="upload_file" size="20" type="file">
|
<input class='upload-button' form="gerber-upload-form" name="upload_file" size="20" type="file">
|
||||||
</div>
|
</div>
|
||||||
<div class="submit-buttons">
|
<div class="submit-buttons">
|
||||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||||
<input class='submit-button' form="gerber-upload-form" type="submit" value="Submit">
|
<input class='submit-button' form="gerber-upload-form" type="submit" value="Submit">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if 'render_job' in session or has_renders %}
|
{% if 'render_job' in session or has_renders %}
|
||||||
<div class="step" id="step2">
|
<div class="step" id="step2">
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<h2>Download the target side's preview image</h2>
|
<h2>Download the target side's preview image</h2>
|
||||||
<p>
|
<p>
|
||||||
Second, download either the top or bottom SVG template and place your own artwork in it on the appropriate
|
Second, download either the top or bottom preview image and use it to align and scale your own artwork
|
||||||
layers. The template is made to work well with the excellent open-source <a href="https://inkscape.org">Inkscape</a>
|
in an image editing program such as Gimp. Then upload your overlay image below.
|
||||||
vector graphics editor. When you are done, upload your overlay below.
|
|
||||||
|
|
||||||
If you wish to put a bitmap image (PNG/JPG) on your board, simply place it into the SVG on the appropriate
|
Note that you will have to convert grayscale images into binary images yourself. Gerbolyze can't do this
|
||||||
layer. Make sure you select Inkscape's "embed image" option when importing it.
|
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
|
||||||
</p>
|
<i>one</i> way to produce agreeable binary images from grayscale source material.
|
||||||
</div>
|
</p>
|
||||||
<div class="controls">
|
</div>
|
||||||
{% if 'render_job' in session %}
|
<div class="controls">
|
||||||
<div class="loading-message">
|
{% if 'render_job' in session %}
|
||||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
<div class="loading-message">
|
||||||
<div><strong>Processing...</strong></div>
|
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||||
<div>(this may take several minutes!)</div>
|
<div><strong>Processing...</strong></div>
|
||||||
</div>
|
<div>(this may take several minutes!)</div>
|
||||||
{% else %}
|
</div>
|
||||||
<div class="preview-images">
|
{% else %}
|
||||||
<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="preview-images">
|
||||||
<div class="overlay">top</div>
|
<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')}}');">
|
||||||
</a>
|
<div class="overlay">top</div>
|
||||||
<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')}}');">
|
</a>
|
||||||
<div class="overlay">bot<br/>tom</div>
|
<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')}}');">
|
||||||
</a>
|
<div class="overlay">bot<br/>tom</div>
|
||||||
</div>
|
</a>
|
||||||
{% endif %}
|
</div>
|
||||||
<div class="submit-buttons">
|
{% endif %}
|
||||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
<div class="submit-buttons">
|
||||||
</div>
|
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="step" id="step3">
|
<div class="step" id="step3">
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<h2>Upload overlay SVG</h2>
|
<h2>Upload overlay image</h2>
|
||||||
<p>
|
<p>
|
||||||
Now, upload your binary overlay as an SVG and let gerbolyze paste it onto the target layers.
|
Now, upload your binary overlay image as a PNG and let gerbolyze render it onto the target layer. The PNG
|
||||||
</p>
|
file should be a black and white binary file with details generally above about 10px size. <b>Antialiased
|
||||||
</div>
|
edges are supported.</b>
|
||||||
<div class="controls">
|
</p>
|
||||||
<form id="overlay-upload-form" method="POST" action="{{url_for('upload_overlay')}}" enctype="multipart/form-data">
|
</div>
|
||||||
{{overlay_form.csrf_token}}
|
<div class="controls">
|
||||||
</form>
|
<form id="overlay-upload-form" method="POST" action="{{url_for('upload_overlay')}}" enctype="multipart/form-data">
|
||||||
<div class="form-controls">
|
{{overlay_form.csrf_token}}
|
||||||
<div class="form-label upload-label">Upload Overlay PNG file:</div>
|
</form>
|
||||||
<input class='upload-button' form="overlay-upload-form" name="upload_file" size="20" type="file">
|
<div class="form-controls">
|
||||||
</div>
|
<div class="form-label upload-label">Upload Overlay PNG file:</div>
|
||||||
<div class="form-controls">
|
<input class='upload-button' form="overlay-upload-form" name="upload_file" size="20" type="file">
|
||||||
<div class="form-label target-label">Target layer:</div>
|
</div>
|
||||||
<input form="overlay-upload-form" name="side" id="side-0" type="radio" value="top">
|
<div class="form-controls">
|
||||||
<label for="side-0">Top</label>
|
<div class="form-label target-label">Target layer:</div>
|
||||||
<input form="overlay-upload-form" name="side" id="side-1" type="radio" value="top">
|
<input form="overlay-upload-form" name="side" id="side-0" type="radio" value="top">
|
||||||
<label for="side-1">Bottom</label>
|
<label for="side-0">Top</label>
|
||||||
</div>
|
<input form="overlay-upload-form" name="side" id="side-1" type="radio" value="top">
|
||||||
<div class="submit-buttons">
|
<label for="side-1">Bottom</label>
|
||||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
</div>
|
||||||
<input class='submit-button' form="overlay-upload-form" type="submit" value="Submit">
|
<div class="submit-buttons">
|
||||||
</div>
|
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||||
</div>
|
<input class='submit-button' form="overlay-upload-form" type="submit" value="Submit">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if 'vector_job' in session or has_output %}
|
{% if 'vector_job' in session or has_output %}
|
||||||
<div class="step" id="step4">
|
<div class="step" id="step4">
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<h2>Download the processed gerber files</h2>
|
<h2>Download the processed gerber files</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
{% if 'vector_job' in session %}
|
{% if 'vector_job' in session %}
|
||||||
<div class="loading-message">
|
<div class="loading-message">
|
||||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||||
<div><strong>Processing...</strong></div>
|
<div><strong>Processing...</strong></div>
|
||||||
<div>(this may take several minutes!)</div>
|
<div>(this may take several minutes!)</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class='download-controls'>
|
<div class='download-controls'>
|
||||||
<a class='output-download' href="{{url_for('output_download')}}">Click to download</a>
|
<a class='output-download' href="{{url_for('output_download')}}">Click to download</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="submit-buttons">
|
<div class="submit-buttons">
|
||||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||||
</div>
|
</div>
|
||||||
<!--4>Debug foo</h4>
|
<!--4>Debug foo</h4>
|
||||||
<div class="loading-message">
|
<div class="loading-message">
|
||||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||||
<div><strong>Processing...</strong></div>
|
<div><strong>Processing...</strong></div>
|
||||||
<div>(this may take several minutes!)</div>
|
<div>(this may take several minutes!)</div>
|
||||||
</div-->
|
</div-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %} {# vector job #}
|
{% endif %} {# vector job #}
|
||||||
{% endif %} {# render job #}
|
{% endif %} {# render job #}
|
||||||
</div>
|
</div>
|
||||||
<div class="sample-images">
|
<div class="sample-images">
|
||||||
<h1>Sample images</h1>
|
<h1>Sample images</h1>
|
||||||
<img src="{{url_for('static', filename='sample1.jpg')}}">
|
<img src="{{url_for('static', filename='sample1.jpg')}}">
|
||||||
<img src="{{url_for('static', filename='sample2.jpg')}}">
|
<img src="{{url_for('static', filename='sample2.jpg')}}">
|
||||||
<img src="{{url_for('static', filename='sample3.jpg')}}">
|
<img src="{{url_for('static', filename='sample3.jpg')}}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 442 KiB |
|
Before Width: | Height: | Size: 509 KiB |
|
Before Width: | Height: | Size: 729 KiB |
|
Before Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
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
515
podman/testdata/test_svg_readme.svg
vendored
|
Before Width: | Height: | Size: 143 KiB |
5
podman/testdata/testscript.sh
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
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
|
|
@ -1,463 +0,0 @@
|
||||||
<!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
|
|
@ -1,14 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 293 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
49
setup.py
|
|
@ -1,57 +1,25 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
from setuptools.command.install import install
|
|
||||||
import subprocess
|
|
||||||
from multiprocessing import cpu_count
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def readme():
|
def readme():
|
||||||
with open('README.rst') as f:
|
with open('README.rst') as f:
|
||||||
return f.read()
|
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(
|
setup(
|
||||||
cmdclass={'install': CustomInstall},
|
|
||||||
name = 'gerbolyze',
|
name = 'gerbolyze',
|
||||||
version = '2.0.5',
|
version = '0.1.10',
|
||||||
py_modules = ['gerbolyze'],
|
py_modules = ['gerbolyze'],
|
||||||
package_dir = {'': 'gerbolyze'},
|
scripts = ['gerbolyze'],
|
||||||
entry_points = '''
|
description = ('A high-resolution image-to-PCB converter. Gerbolyze reads and vectorizes black-and-white raster '
|
||||||
[console_scripts]
|
'images, then plots the vectorized image into an existing gerber file while avoiding existing features such as '
|
||||||
gerbolyze=gerbolyze:cli
|
'text or holes.'),
|
||||||
''',
|
|
||||||
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=readme(),
|
||||||
long_description_content_type='text/x-rst',
|
long_description_content_type='text/x-rst',
|
||||||
url = 'https://git.jaseg.de/gerbolyze',
|
url = 'https://github.com/jaseg/gerbolyze',
|
||||||
author = 'jaseg',
|
author = 'jaseg',
|
||||||
author_email = 'github@jaseg.de',
|
author_email = 'github@jaseg.net',
|
||||||
install_requires = ['pcb-tools', 'numpy', 'python-slugify', 'lxml', 'click', 'pcb-tools-extension'],
|
install_requires = ['pcb-tools', 'tqdm', 'numpy', 'opencv-python'],
|
||||||
license = 'AGPLv3',
|
license = 'AGPLv3',
|
||||||
classifiers = [
|
classifiers = [
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
|
|
@ -66,4 +34,3 @@ setup(
|
||||||
'Topic :: Utilities'
|
'Topic :: Utilities'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
@ -1,272 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,441 +0,0 @@
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
/*
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||