Compare commits
262 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c6e8c5a2b | ||
|
|
9d7fd8b3b4 | ||
|
|
a02ff5fc87 | ||
|
|
b26c28e58b | ||
|
|
be24d0368f | ||
|
|
e1c40e8c80 | ||
|
|
4a3a9f1582 | ||
|
|
4127a18e89 | ||
|
|
0d4172901b | ||
|
|
bc63166a40 | ||
|
|
2df63318a2 | ||
|
|
5f008f623a | ||
|
|
10669301a1 | ||
|
|
25628f1d24 | ||
|
|
65a426c645 | ||
|
|
eb7107a8c6 | ||
|
|
454d587d79 | ||
|
|
602e51ca10 | ||
|
|
b4753e66e2 | ||
|
|
6b0382ab77 | ||
|
|
a6adfe4d1d | ||
|
|
70a7a0aa95 | ||
|
|
b1324e9a53 | ||
|
|
89fa6fbf83 | ||
|
|
7e7f4b1aa6 | ||
|
|
ea0a6d83f8 | ||
|
|
869fd09ad9 | ||
|
|
00de98d5e3 | ||
|
|
3ba932209f | ||
|
|
1da5919d91 | ||
|
|
dd0c1cb632 | ||
|
|
9632509060 | ||
|
|
58eabf59fe | ||
|
|
85460a7c55 | ||
|
|
575e24cca7 | ||
|
|
68ce1505f1 | ||
|
|
25ebdbe625 | ||
|
|
e845888580 | ||
|
|
e76d257220 | ||
|
|
29c5c0c03d | ||
|
|
0cf02e9c1d | ||
|
|
e73b577178 | ||
|
|
bb8b28f86a | ||
|
|
a01e44b142 | ||
|
|
0ecb612d59 | ||
|
|
d3204b1ede | ||
|
|
2fc5d1d929 | ||
|
|
e4a0c1ba4a | ||
|
|
9e9cc2bc01 | ||
|
|
0e1c8507bb | ||
|
|
8a1f9d1832 | ||
|
|
f09c436e56 | ||
|
|
4636383ffc | ||
|
|
c1cda48a4c | ||
|
|
d09cf6ef3b | ||
|
|
f58cca0ba6 | ||
|
|
446c5e5901 | ||
|
|
5f33356f33 | ||
|
|
e3d8c3a063 | ||
|
|
4b83ec29de | ||
|
|
ee4ad9d602 | ||
|
|
676eff0a30 | ||
|
|
31b5a77c8a | ||
|
|
13b92b0947 | ||
|
|
d8c20e6311 | ||
|
|
14e9d7fbc2 | ||
|
|
1622e9c943 | ||
|
|
48f78dd391 | ||
|
|
826f414f9d | ||
|
|
6b6f13d2ab | ||
|
|
13ae96092f | ||
|
|
44fe22b6d4 | ||
|
|
2c9abc7e84 | ||
|
|
32b6aa650a | ||
|
|
23945b4cc6 | ||
|
|
d61d642c39 | ||
|
|
8ccbc98706 | ||
|
|
ca43fe715b | ||
|
|
c339f4cec1 | ||
|
|
dddced97aa | ||
|
|
4d9381b84a | ||
|
|
1c75a9eebe | ||
|
|
aa7d626569 | ||
|
|
952ddf4a20 | ||
|
|
527fbca7f5 | ||
|
|
225e9b3631 | ||
|
|
0e5b1cb999 | ||
|
|
f55cfb4b35 | ||
|
|
2e2a7e6a41 | ||
|
|
b5b16535c9 | ||
|
|
973aee30b6 | ||
|
|
b3807b6530 | ||
|
|
2616cf46ff | ||
|
|
bc54e8233f | ||
|
|
29c8245d0a | ||
|
|
a1e25a0cfb | ||
|
|
7dccfc8e11 | ||
|
|
2380dcb222 | ||
|
|
2184f3b1f5 | ||
|
|
a3cf9e6845 | ||
|
|
a6acc8dc69 | ||
|
|
a511e7dc82 | ||
|
|
d406b1f1d0 | ||
|
|
8a64621e8c | ||
|
|
7eb0b9d7e4 | ||
|
|
c58b6573f2 | ||
|
|
956538a32c | ||
|
|
fae8532b05 | ||
|
|
aaade1b168 | ||
|
|
aad0ae0215 | ||
|
|
a084be8849 | ||
|
|
236c4a4485 | ||
|
|
4ffb4c6582 | ||
|
|
f2f0ac2416 | ||
|
|
b2873329d4 | ||
|
|
831993cdc3 | ||
|
|
6a69f8c1d3 | ||
|
|
1b02cdd2e7 | ||
|
|
b7cdcd396b | ||
|
|
094aca80b8 | ||
|
|
cc882a4195 | ||
|
|
6685b7587a | ||
|
|
3ef3f0223e | ||
|
|
a26c04873e | ||
|
|
2e38e66f14 | ||
|
|
61887e9ee1 | ||
|
|
6193fa151e | ||
|
|
018748aa23 | ||
|
|
6dd7bbc38c | ||
|
|
2d03b014f9 | ||
|
|
9230d678af | ||
|
|
f1bf25b51f | ||
|
|
564ab243cc | ||
|
|
de0f851645 | ||
|
|
6cca4a3278 | ||
|
|
3e58a4228b | ||
|
|
7b58f2efc5 | ||
|
|
0530c365ca | ||
|
|
536a34cd59 | ||
|
|
d18b8a1d80 | ||
|
|
3b553b3a1d | ||
|
|
3386e586ac | ||
|
|
bbf1c02e79 | ||
|
|
d175570177 | ||
|
|
e06bbdbe9b | ||
|
|
d23d148660 | ||
|
|
79e8da1f46 | ||
|
|
ca6e6abfdc | ||
|
|
5bb733e559 | ||
|
|
6eb2c967a0 | ||
|
|
1790ef9137 | ||
|
|
1d6d4e4f14 | ||
|
|
046e827be1 | ||
|
|
a0fe2d83f7 | ||
|
|
f2c891533f | ||
|
|
1180ebdc1f | ||
|
|
776e0bd206 | ||
|
|
89da2b3664 | ||
|
|
ee580d1642 | ||
|
|
5d1bcd7fc0 | ||
|
|
3288fb8345 | ||
|
|
0be9f4b3ba | ||
|
|
da9d7280d5 | ||
|
|
f9c5c00f51 | ||
|
|
901efc75c6 | ||
|
|
3fc628beec | ||
|
|
e8d7ca1d6c | ||
|
|
a3443a459b | ||
|
|
0ffb9ece31 | ||
|
|
29408cb2b0 | ||
|
|
99d0479d3a | ||
|
|
f560923a9c | ||
|
|
100e865394 | ||
|
|
d4c7e8d344 | ||
|
|
d52b0f20e5 | ||
|
|
cb67de412c | ||
|
|
f52dea8eac | ||
|
|
69a274189a | ||
|
|
0f2e8bd5ee | ||
|
|
bcae0f16c7 | ||
|
|
729ec14b66 | ||
|
|
d3a83bd0cc | ||
|
|
0a02e01a96 | ||
|
|
7e6b07a491 | ||
|
|
2fd7045c82 | ||
|
|
e74a0cf204 | ||
|
|
89a09b60ab | ||
|
|
ce9a7a3082 | ||
|
|
c0c019aea6 | ||
|
|
57fad5d8c3 | ||
|
|
59ea38c9bd | ||
|
|
be6783e803 | ||
|
|
a4b45196df | ||
|
|
5e04765842 | ||
|
|
56c6443301 | ||
|
|
fe4a4001ba | ||
|
|
ac65590c85 | ||
|
|
d9d06bff63 | ||
|
|
4a967682d4 | ||
|
|
79db262858 | ||
|
|
88faecbdc2 | ||
|
|
a250ee6295 | ||
|
|
685dbb56b2 | ||
|
|
40a1c2bceb | ||
|
|
3f9cdce1cb | ||
|
|
50cc3ce27c | ||
|
|
3bcbb29cf3 | ||
|
|
3e323c953c | ||
|
|
13cb49b218 | ||
|
|
18d857668e | ||
|
|
1e9d0c62f9 | ||
|
|
06c2d5295d | ||
|
|
1d58b4d584 | ||
|
|
f95a24ebb9 | ||
|
|
8c494f7736 | ||
|
|
bf428103d3 | ||
|
|
6dec0a6e17 | ||
|
|
62622813be | ||
|
|
6aab099baa | ||
|
|
bd2b012740 | ||
|
|
fb9c6ac32c | ||
|
|
bdd79a8f2b | ||
|
|
91c46a07c6 | ||
|
|
52a35dd63a | ||
|
|
fc495607dc | ||
|
|
5f4f667e17 | ||
|
|
6c01126ce9 | ||
|
|
a68e395cb6 | ||
|
|
49a7c6df41 | ||
|
|
b5d523741c | ||
|
|
04f1cab5fc | ||
|
|
133f5bb98d | ||
|
|
c5f8416b63 | ||
|
|
3f3b8487d4 | ||
|
|
2133867c8a | ||
|
|
617a42a674 | ||
|
|
f88134f9ca | ||
|
|
a6540b73da | ||
|
|
eac89409b8 | ||
|
|
52dcceb87f | ||
|
|
a34efc058a | ||
|
|
6d1a7750c5 | ||
|
|
f65cd52304 | ||
|
|
9711fabab7 | ||
|
|
5285b6dce8 | ||
|
|
70d0021df1 | ||
|
|
f9a871f9b2 | ||
|
|
3cee5d4f01 | ||
|
|
bc0ef634cf | ||
|
|
538b8a32b9 | ||
|
|
33848ad43c | ||
|
|
95b198a5aa | ||
|
|
f7b4cc602b | ||
|
|
c6b1c2225d | ||
|
|
e290ac758b | ||
|
|
ee35c06119 | ||
|
|
6fbea50682 | ||
|
|
b47ca7bbdc | ||
|
|
e63a7e557d | ||
|
|
659290677b | ||
|
|
9510936fd9 | ||
|
|
6635bc6d46 |
173
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- publish
|
||||
|
||||
include:
|
||||
- local: "/svg-flatten/svg-flatten-wasi-ci.yml"
|
||||
|
||||
build:gerbolyze:
|
||||
stage: build
|
||||
image: "registry.gitlab.com/gerbolyze/build-containers/archlinux:latest"
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: none
|
||||
script:
|
||||
- git config --global --add safe.directory "$CI_PROJECT_DIR"
|
||||
- pip3 install --user wheel
|
||||
- python3 setup.py sdist bdist_wheel
|
||||
artifacts:
|
||||
name: "gerbolyze-$CI_COMMIT_REF_NAME-gerbolyze"
|
||||
paths:
|
||||
- dist/*
|
||||
|
||||
publish:gerbolyze:
|
||||
stage: publish
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: none
|
||||
image: "registry.gitlab.com/gerbolyze/build-containers/archlinux:latest"
|
||||
cache: {}
|
||||
script:
|
||||
- export TWINE_USERNAME TWINE_PASSWORD
|
||||
- twine upload dist/*
|
||||
dependencies:
|
||||
- build:gerbolyze
|
||||
only:
|
||||
- /^v.*$/
|
||||
|
||||
build:svg-flatten-debian_11:
|
||||
stage: build
|
||||
image: "registry.gitlab.com/gerbolyze/build-containers/debian:11"
|
||||
script:
|
||||
- "export CXX=clang++"
|
||||
- "make -C svg-flatten"
|
||||
artifacts:
|
||||
name: "gerbolyze-$CI_COMMIT_REF_NAME-svg-flatten-deb11"
|
||||
paths:
|
||||
- svg-flatten/build/svg-flatten
|
||||
- svg-flatten/build/nopencv-test
|
||||
|
||||
# Tests for debian are disabled until they get python 3.10.
|
||||
# test:svg-flatten-debian_11:
|
||||
# stage: test
|
||||
# variables:
|
||||
# GIT_SUBMODULE_STRATEGY: none
|
||||
# image: "registry.gitlab.com/gerbolyze/build-containers/debian:11"
|
||||
# script:
|
||||
# - git config --global --add safe.directory "$CI_PROJECT_DIR"
|
||||
# - "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||||
# - "touch svg-flatten/build/svg-flatten svg-flatten/build/nopencv-test"
|
||||
# - "python3 setup.py install --user"
|
||||
# - "gerbolyze --help"
|
||||
# - "make -C svg-flatten tests"
|
||||
# dependencies:
|
||||
# - build:svg-flatten-debian_11
|
||||
# artifacts:
|
||||
# name: "gerbolyze-$CI_COMMIT_REF_NAME-test-deb11"
|
||||
# when: on_failure
|
||||
# paths:
|
||||
# - svg-flatten/testcase-fails/*.png
|
||||
# - svg-flatten/testcase-fails/*.svg
|
||||
|
||||
build:svg-flatten-ubuntu_2204:
|
||||
stage: build
|
||||
image: "registry.gitlab.com/gerbolyze/build-containers/ubuntu:22.04"
|
||||
script:
|
||||
- "export CXX=clang++"
|
||||
- "make -C svg-flatten"
|
||||
artifacts:
|
||||
name: "gerbolyze-$CI_COMMIT_REF_NAME-svg-flatten-ubu22"
|
||||
paths:
|
||||
- svg-flatten/build/svg-flatten
|
||||
- svg-flatten/build/nopencv-test
|
||||
|
||||
test:svg-flatten-ubuntu_2204:
|
||||
stage: test
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: none
|
||||
image: "registry.gitlab.com/gerbolyze/build-containers/ubuntu:22.04"
|
||||
script:
|
||||
- git config --global --add safe.directory "$CI_PROJECT_DIR"
|
||||
- "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||||
- "touch svg-flatten/build/svg-flatten svg-flatten/build/nopencv-test"
|
||||
- pip3 install --user 'pillow>=9.1.1'
|
||||
- "python3 setup.py install --user"
|
||||
- "gerbolyze --help"
|
||||
- "make -C svg-flatten tests"
|
||||
dependencies:
|
||||
- build:svg-flatten-ubuntu_2204
|
||||
artifacts:
|
||||
name: "gerbolyze-$CI_COMMIT_REF_NAME-test-ubu22"
|
||||
when: on_failure
|
||||
paths:
|
||||
- svg-flatten/testcase-fails/*.png
|
||||
- svg-flatten/testcase-fails/*.svg
|
||||
|
||||
build:svg-flatten-fedora_36:
|
||||
stage: build
|
||||
image: "registry.gitlab.com/gerbolyze/build-containers/fedora:36"
|
||||
script:
|
||||
- "export CXX=clang++"
|
||||
- "make -C svg-flatten"
|
||||
artifacts:
|
||||
name: "gerbolyze-$CI_COMMIT_REF_NAME-svg-flatten-fed36"
|
||||
paths:
|
||||
- svg-flatten/build/svg-flatten
|
||||
- svg-flatten/build/nopencv-test
|
||||
|
||||
test:svg-flatten-fedora_36:
|
||||
stage: test
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: none
|
||||
image: "registry.gitlab.com/gerbolyze/build-containers/fedora:36"
|
||||
script:
|
||||
- git config --global --add safe.directory "$CI_PROJECT_DIR"
|
||||
- "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||||
- "touch svg-flatten/build/svg-flatten svg-flatten/build/nopencv-test"
|
||||
- pip3 install --user 'pillow>=9.1.1'
|
||||
- "python3 setup.py install --user"
|
||||
- "gerbolyze --help"
|
||||
- "make -C svg-flatten tests"
|
||||
dependencies:
|
||||
- build:svg-flatten-fedora_36
|
||||
artifacts:
|
||||
name: "gerbolyze-$CI_COMMIT_REF_NAME-test-fed36"
|
||||
when: on_failure
|
||||
paths:
|
||||
- svg-flatten/testcase-fails/*.png
|
||||
- svg-flatten/testcase-fails/*.svg
|
||||
|
||||
build:svg-flatten-archlinux:
|
||||
stage: build
|
||||
image: "registry.gitlab.com/gerbolyze/build-containers/archlinux:latest"
|
||||
script:
|
||||
- "make -C svg-flatten"
|
||||
artifacts:
|
||||
name: "gerbolyze-$CI_COMMIT_REF_NAME-svg-flatten-arch"
|
||||
paths:
|
||||
- svg-flatten/build/svg-flatten
|
||||
- svg-flatten/build/nopencv-test
|
||||
|
||||
test:svg-flatten-archlinux:
|
||||
stage: test
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: none
|
||||
image: "registry.gitlab.com/gerbolyze/build-containers/archlinux:latest"
|
||||
script:
|
||||
- git config --global --add safe.directory "$CI_PROJECT_DIR"
|
||||
- "export PATH=$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||||
- "touch svg-flatten/build/svg-flatten svg-flatten/build/nopencv-test"
|
||||
- "python setup.py install --user"
|
||||
- "gerbolyze --help"
|
||||
- "make -C svg-flatten tests"
|
||||
dependencies:
|
||||
- build:svg-flatten-archlinux
|
||||
artifacts:
|
||||
name: "gerbolyze-$CI_COMMIT_REF_NAME-test-arch"
|
||||
when: on_failure
|
||||
paths:
|
||||
- svg-flatten/testcase-fails/*.png
|
||||
- svg-flatten/testcase-fails/*.svg
|
||||
|
||||
36
.gitmodules
vendored
|
|
@ -1,6 +1,30 @@
|
|||
[submodule "gerboweb/deploy/checkouts/pogojig"]
|
||||
path = gerboweb/deploy/checkouts/pogojig
|
||||
url = https://github.com/jaseg/pogojig.git
|
||||
[submodule "gerboweb/deploy/library/ansible-collection"]
|
||||
path = gerboweb/deploy/library/inwx-collection
|
||||
url = https://github.com/inwx/ansible-collection
|
||||
[submodule "upstream/cpp-base64"]
|
||||
path = upstream/cpp-base64
|
||||
url = https://gitlab.com/gerbolyze/gerbolyze-cpp-base64.git
|
||||
[submodule "upstream/voronoi"]
|
||||
path = upstream/voronoi
|
||||
url = https://github.com/JCash/voronoi
|
||||
[submodule "upstream/poisson-disk-sampling"]
|
||||
path = upstream/poisson-disk-sampling
|
||||
url = https://github.com/thinks/poisson-disk-sampling
|
||||
[submodule "upstream/argagg"]
|
||||
path = upstream/argagg
|
||||
url = https://gitlab.com/gerbolyze/gerbolyze-argagg.git
|
||||
[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 "svg-flatten/upstream/minunit"]
|
||||
path = upstream/minunit
|
||||
url = https://github.com/siu/minunit
|
||||
[submodule "upstream/stb"]
|
||||
path = upstream/stb
|
||||
url = https://github.com/nothings/stb
|
||||
[submodule "upstream/filesystem"]
|
||||
path = upstream/filesystem
|
||||
url = https://github.com/gulrak/filesystem
|
||||
[submodule "upstream/pugixml"]
|
||||
path = upstream/pugixml
|
||||
url = https://github.com/zeux/pugixml
|
||||
|
|
|
|||
11
MANIFEST.in
|
|
@ -1 +1,12 @@
|
|||
# setuptools'es default strategy of packing everything into the archive that is in git is really the opposite of smart
|
||||
# here, especially because AFAICT after hours of googling there is no way to turn it off. Meh. Why is python packaging
|
||||
# like this? what is so hard about putting a few files in a zip?!
|
||||
global-exclude *
|
||||
|
||||
include README.rst
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include setup.py
|
||||
recursive-include gerbolyze *.py
|
||||
recursive-include bin *
|
||||
recursive-include gerbolyze.egg-info *
|
||||
|
|
|
|||
684
README.rst
|
|
@ -1,87 +1,539 @@
|
|||
Gerbolyze high-resolution image-to-PCB converter
|
||||
================================================
|
||||
Gerbolyze high-fidelity SVG/PNG/JPG to PCB converter
|
||||
====================================================
|
||||
|
||||
.. note::
|
||||
|
||||
The command-line usage and SVG template format of gerbolyze changed between v2.0 and v3.0. You can find details on
|
||||
the new format below under command_line_usage_
|
||||
|
||||
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.
|
||||
|
||||
Try gerbolyze online at https://dyna.kokoroyukuma.de/gerboweb
|
||||
|
||||
.. figure:: pics/pcbway_sample_02_small.jpg
|
||||
:width: 800px
|
||||
|
||||
Drawing by `トーコ Toko <https://twitter.com/fluffy2038/status/1317231121269104640>`__ converted using Gerbolyze and printed at PCBWay.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample1.jpg
|
||||
|
||||
Tooling for PCB art is quite limited in both open source and closed source ecosystems. Something as simple as putting a
|
||||
pretty picture on a PCB can be an extremely tedious task. Depending on the PCB tool used, various arcane incantations
|
||||
may be necessary and even modestly complex images will slow down most PCB tools to a crawl.
|
||||
|
||||
Gerbolyze solves this problem in a toolchain-agnostic way by directly vectorizing bitmap files onto existing gerber
|
||||
layers. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the industry-standard Altium
|
||||
Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of thousands of primitives,
|
||||
generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can finally be confident that
|
||||
your PCB fab's toolchain will fall over before yours does if you overdo it with the high-poly anime silkscreen.
|
||||
Gerbolyze solves this problem in a toolchain-agnostic way by directly vectorizing SVG vector and PNG or JPG bitmap files
|
||||
onto existing gerber layers. Gerbolyze processes any spec-compliant SVG and "gerbolyzes" SVG vector data into a Gerber
|
||||
spec-compliant form. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the
|
||||
industry-standard Altium Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of
|
||||
thousands of primitives, generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can
|
||||
finally be confident that your PCB fab's toolchain will fall over before yours does if you overdo it with the high-poly
|
||||
anime silkscreen.
|
||||
|
||||
Gerbolyze is based on gerbonara_.
|
||||
|
||||
.. image:: pics/process-overview.png
|
||||
:width: 800px
|
||||
|
||||
.. contents::
|
||||
|
||||
Produce high-quality artistic PCBs in three easy steps!
|
||||
-------------------------------------------------------
|
||||
Tl;dr: Produce high-quality artistic PCBs in three easy steps!
|
||||
--------------------------------------------------------------
|
||||
|
||||
Gerbolyze works in three steps.
|
||||
|
||||
1. Generate a scale-accurate preview of the finished PCB from your CAD tool's gerber output:
|
||||
1. Generate a scale-accurate template of the finished PCB from your CAD tool's gerber output:
|
||||
|
||||
.. code::
|
||||
|
||||
$ gerbolyze render top my_gerber_dir preview.png
|
||||
$ gerbolyze template --top template_top.svg [--bottom template_bottom.svg] my_gerber_dir
|
||||
|
||||
2. Load the resulting preview image into the GIMP or another image editing program. Use it as a guide to position scale
|
||||
your artwork. Create a black-and-white image from your scaled artwork using GIMP's newsprint filter. Make sure most
|
||||
details are larger than about 10px to ensure manufacturing goes smooth.
|
||||
2. Load the resulting template image Inkscape_ or another SVG editing program. Put your artwork on the appropriate SVG
|
||||
layer. Dark colors become filled gerber primitives, bright colors become unfilled primitives. You can directly put
|
||||
raster images (PNG/JPG) into this SVG as well, just position and scale them like everything else. SVG clips work for
|
||||
images, too. Masks are not supported.
|
||||
|
||||
3. Vectorize the resulting grayscale image drectly into the PCB's gerber files:
|
||||
3. Vectorize the edited SVG template image drectly into the PCB's gerber files:
|
||||
|
||||
.. code::
|
||||
|
||||
$ gerbolyze vectorize top input_gerber_dir output_gerber_dir black_and_white_artwork.png
|
||||
$ gerbolyze paste --top template_top_edited.svg [--bottom ...] my_gerber_dir output_gerber_dir
|
||||
|
||||
Image preprocessing guide
|
||||
-------------------------
|
||||
Quick Start Installation (Any Platform)
|
||||
---------------------------------------
|
||||
|
||||
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.
|
||||
.. code-block:: shell
|
||||
|
||||
1 Import a render of the board generated using ``gerbolyze render``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
python -m pip install --user gerbolyze
|
||||
|
||||
``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.
|
||||
To uninstall, run
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/01import01.png
|
||||
.. code-block:: shell
|
||||
|
||||
2 Import your desired artwork
|
||||
python -m pip uninstall gerbolyze gerbonara resvg-wasi svg-flatten-wasi
|
||||
|
||||
To update, run
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
python -m pip install --user --upgrade --upgrade-strategy eager gerbolyze
|
||||
|
||||
Speeding up gerbolyze using natively-built binaries
|
||||
---------------------------------------------------
|
||||
|
||||
This will install gerbolyze's binary dependency resvg and gerbolyze's svg-flatten utility as pre-built cross-platform
|
||||
WASM binaries. When you first run gerbolyze, it will take some time (~30s) to link these binaries for your system. The
|
||||
output is cached, so any future run is going to be fast.
|
||||
|
||||
WASM is slower than natively-built binaries. To speed up gerbolyze, you can natively build its two binary dependencies:
|
||||
|
||||
1. Install resvg natively using rust's cargo package manager: ``cargo install resvg``
|
||||
2. Install gerbolyze's svg-flatten utility natively. You can get pre-built binaries from gerbolyze's gitlab CI jobs `at
|
||||
this link <https://gitlab.com/gerbolyze/gerbolyze/-/pipelines?scope=tags&page=1>`__ by clicking the three dots on the
|
||||
right next to the version you want. These pre-built binaries should work on any x86_64 linux since they are
|
||||
statically linked. You can also build svg-flatten yourself by running ``make`` inside the ``svg-flatten`` folder from
|
||||
a gerbolyze checkout.
|
||||
|
||||
Gerbolyze will pick up these binaries when installed in your ``$PATH``. resvg is also picked up when it is installed by
|
||||
cargo in your home's ``~/.cargo``, even if it's not in your ``$PATH``. You can override the resvg, usvg or svg-flatten
|
||||
binary that gerbolyze uses by giving it the absoulute path to a binary in the ``$RESVG``, ``$USVG`` and ``$SVG_FLATTEN``
|
||||
environment variables.
|
||||
|
||||
|
||||
Build from source (any distro)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git clone --recurse-submodules https://git.jaseg.de/gerbolyze.git
|
||||
cd gerbolyze
|
||||
|
||||
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
|
||||
* Limited SVG support for board outline layers (no fill/region support)
|
||||
* Dashed lines supported on board outline layers
|
||||
|
||||
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
|
||||
|
||||
Web interface
|
||||
-------------
|
||||
|
||||
You can try gerbolyze online at https://dyna.kokoroyukuma.de/gerboweb
|
||||
|
||||
The web interface does not expose all of gerbolyze's bells and whistles, but it allows you to simply paste a single SVG
|
||||
file on a board to try out gerbolyze. Upload your design on the web interface, then download the template for either the
|
||||
top or bottom side, and put your artwork on the appropriate layer of that template using Inkscape_. Finally, upload the
|
||||
modified template and let gerbolyze process your design.
|
||||
|
||||
Command-line usage
|
||||
------------------
|
||||
.. _command_line_usage:
|
||||
|
||||
Generate SVG template from Gerber files:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
gerbolyze template [options] [--top|--bottom] input_dir_or.zip output.svg
|
||||
|
||||
Render design from an SVG made with the template above into a set of gerber files:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
gerbolyze paste [options] artwork.svg input_dir_or.zip output_dir_or.zip
|
||||
|
||||
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. The output file contains a preview
|
||||
image of the input gerbers to allow you to position your artwork, as well as prepared Inkscape layers corresponding to
|
||||
each gerber layer. Simply place your artwork in this SVG template using Inkscape. Starting in v3.0, gerbolyze
|
||||
automatically keeps track of which board side (top or bottom) is contained in an SVG template.
|
||||
|
||||
Options:
|
||||
********
|
||||
``--top | --bottom``
|
||||
Output top or bottom side template. This affects both the preview image and the prepared Inkscape layers.
|
||||
|
||||
``--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 OVERLAY_SVG OUTPUT_GERBERS``
|
||||
|
||||
Render vector data and raster images from SVG file into gerbers. The SVG input file can be generated using ``gerbolyze
|
||||
template`` and contains the name and board side of each layer. Note that for board outline layers, handling slightly
|
||||
differs from other layers as PCB fabs do not support filled Gerber regions on these layers.
|
||||
|
||||
Options:
|
||||
********
|
||||
|
||||
``--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_>`__.
|
||||
|
||||
|
||||
.. _outline_layers:
|
||||
|
||||
Outline layers
|
||||
**************
|
||||
|
||||
Outline layers require special handling since PCB fabs do not support filled G36/G37 polygons on these layers. The main
|
||||
difference between normal layers and outline layers is how strokes are handled. On outline layers, strokes are
|
||||
translated to normal Gerber draw commands (D01, D02 etc.) with an aperture set to the stroke's width instead of tracing
|
||||
them to G36/G37 filled regions. This means that on outline layers, SVG end caps and line join types do not work: All
|
||||
lines are redered with round joins and end caps.
|
||||
|
||||
One exception from this are patterns, which work as expected for both fills and strokes with full support for joins and
|
||||
end caps.
|
||||
|
||||
Dashed strokes are supported on outline layers and can be used to make easy mouse bites.
|
||||
|
||||
.. _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
|
||||
|
||||
.. _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, gerber-outline (for board outline layers), 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 in SVG output for "clear" areas (default: white)
|
||||
|
||||
``--dark-color``
|
||||
SVG color to use in SVG output for "dark" areas (default: black)
|
||||
|
||||
``-f, --flip-gerber-polarity``
|
||||
Flip polarity of all output gerber primitives for --format gerber.
|
||||
|
||||
``-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 (-o gerber only).
|
||||
|
||||
``-e, --exclude-groups``
|
||||
Comma-separated list of group IDs to exclude from export. Takes precedence over --only-groups.
|
||||
|
||||
.. _vectorization:
|
||||
|
||||
Gerbolyze image vectorization
|
||||
-----------------------------
|
||||
|
||||
Gerbolyze has two built-in strategies to translate pixel images into vector images. One is its built-in halftone
|
||||
processor that tries to approximate grayscale. The other is its built-in binary vectorizer that traces contours in
|
||||
black-and-white images. Below are examples for the four options.
|
||||
|
||||
The vectorizers can be used in isolation through ``svg-flatten`` with either an SVG input that contains an image or a
|
||||
PNG/JPG input.
|
||||
|
||||
The vectorizer can be controlled globally using the ``--vectorizer`` flag in both ``gerbolyze`` and ``svg-flatten``. It
|
||||
can also be set on a per-image basis in both using ``--vectorizer-map [image svg id]=[option]["," ...]``.
|
||||
|
||||
.. for f in vec_*.png; convert -background white -gravity center $f -resize 500x500 -extent 500x500 (basename -s .png $f)-square.png; end
|
||||
.. for vec in hexgrid square poisson contours; convert vec_"$vec"_whole-square.png vec_"$vec"_detail-square.png -background transparent -splice 25x0+0+0 +append -chop 25x0+0+0 vec_"$vec"_composited.png; end
|
||||
|
||||
``--vectorizer poisson-disc`` (the default)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: pics/vec_poisson_composited.png
|
||||
:width: 800px
|
||||
|
||||
``--vectorizer hex-grid``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: pics/vec_hexgrid_composited.png
|
||||
:width: 800px
|
||||
|
||||
``--vectorizer square-grid``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: pics/vec_square_composited.png
|
||||
:width: 800px
|
||||
|
||||
``--vectorizer binary-contours``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: pics/vec_contours_composited.png
|
||||
:width: 800px
|
||||
|
||||
The binary contours vectorizer requires a black-and-white binary input image. As you can see, like every bitmap tracer
|
||||
it will produce some artifacts. For artistic input this is usually not too bad as long as the input data is
|
||||
high-resolution. Antialiased edges in the input image are not only OK, they may even help with an accurate
|
||||
vectorization.
|
||||
|
||||
GIMP halftone preprocessing guide
|
||||
---------------------------------
|
||||
|
||||
Gerbolyze has its own built-in halftone processor, but you can also use the high-quality "newsprint" filter built into
|
||||
GIMP_ instead if you like. This section will guide you through this. The PNG you get out of this can then be fed into
|
||||
gerbolyze using ``--vectorizer binary-contours``.
|
||||
|
||||
1 Import your desired artwork
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Though anime or manga pictures are highly recommended, you can use any image including photographs. Be careful to select
|
||||
a picture with comparatively low detail that remains recognizable at very low resolution. While working on a screen this
|
||||
is hard to vizualize, but the grain resulting from the low resolution of a PCB's silkscreen is quite coarse.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/02import02.png
|
||||
.. image:: screenshots/02import02.png
|
||||
:width: 800px
|
||||
|
||||
3 Paste the artwork onto the render as a new layer
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/03paste.png
|
||||
|
||||
4 Scale, rotate and position the artwork to the desired size
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/04scale_cut.png
|
||||
|
||||
For alignment it may help to set the artwork layer's mode in the layers dialog to ``overlay``, which makes the PCB
|
||||
render layer below shine through more. If you can't set the layer's mode, make sure you have actually made a new layer
|
||||
from the floating selection you get when pasting one image into another in the GIMP.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/05position.png
|
||||
|
||||
5 Convert the image to grayscale
|
||||
2 Convert the image to grayscale
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/06grayscale.png
|
||||
.. image:: screenshots/06grayscale.png
|
||||
:width: 800px
|
||||
|
||||
6 Fine-tune the image's contrast
|
||||
3 Fine-tune the image's contrast
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To look well on the PCB, contrast is critical. If your source image is in color, you may have lost some contrast during
|
||||
|
|
@ -92,9 +544,10 @@ dots that might be beyond your PCB manufacturer's maximum resolution. To control
|
|||
of the grayscale value curve as shown (exaggerated) in the picture below. These steps saturate very bright grays to
|
||||
white and very dark grays to black while preserving the values in the middle.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/08curve_cut.png
|
||||
.. image:: screenshots/08curve_cut.png
|
||||
:width: 800px
|
||||
|
||||
7 Retouch details
|
||||
4 Retouch details
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Therer might be small details that don't look right yet, such as the image's background color or small highlights that
|
||||
|
|
@ -105,14 +558,16 @@ If you don't want the image's background to show up on the final PCB at all, jus
|
|||
Particularly on low-resolution source images it may make sense to apply a blur with a radius similar to the following
|
||||
newsprint filter's cell size (10px) to smooth out the dot pattern generated by the newsprint filter.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/09retouch.png
|
||||
.. image:: screenshots/09retouch.png
|
||||
:width: 800px
|
||||
|
||||
In the following example, I retouched the highlights in the hair of the character in the picture to make them completely
|
||||
white instead of light-gray, so they still stand out nicely in the finished picture.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/10retouched.png
|
||||
.. image:: screenshots/10retouched.png
|
||||
:width: 800px
|
||||
|
||||
8 Run the newsprint filter
|
||||
5 Run the newsprint filter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Now, run the GIMP's newsprint filter, under filters, distorts, newsprint.
|
||||
|
|
@ -123,32 +578,131 @@ with ``gerbolyze render`` default settings for good-quality silkscreen). In gene
|
|||
The second important setting is oversampling, which should be set to four or slightly higher. This improves the result
|
||||
of the edge reconstruction of ``gerbolyze vectorize``.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/11newsprint.png
|
||||
.. image:: screenshots/11newsprint.png
|
||||
:width: 800px
|
||||
|
||||
The following are examples on the detail resulting from the newsprint filter.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/12newsprint.png
|
||||
.. image:: screenshots/12newsprint.png
|
||||
:width: 800px
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/13newsprint.png
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/14newsprint.png
|
||||
|
||||
9 Export the image for use with ``gerbolyze vectorize``
|
||||
6 Export the image for use with ``gerbolyze vectorize``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Simply export the image as a PNG file. Below are some pictures of the output ``gerbolyze vectorize`` produced for this
|
||||
example.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/14result_cut.png
|
||||
.. image:: screenshots/14result_cut.png
|
||||
:width: 800px
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/15result_cut.png
|
||||
.. image:: screenshots/15result_cut.png
|
||||
:width: 800px
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/16result_cut.png
|
||||
Manufacturing Considerations
|
||||
----------------------------
|
||||
|
||||
The main consideration when designing artwork for PCB processes is the processes' trace/space design rule. The two
|
||||
things you can do here is one, to be creative with graphical parts of the design and avoid extremely narrow lines,
|
||||
wedges or other thin features that will not come out well. Number two is to keep detail in raster images several times
|
||||
larger than the manufacturing processes native capability. For example, to target a trace/space design rule of 100 µm,
|
||||
the smallest detail in embedded raster graphics should not be much below 1mm.
|
||||
|
||||
Gerbolyze's halftone vectorizers have built-in support for trace/space design rules. While they can still produce small
|
||||
artifacts that violate these rules, their output should be close enough to satifsy board houses and close enough for the
|
||||
result to look good. The way gerbolyze does this is to clip the halftone cell's values to zero whenevery they get too
|
||||
small, and to forcefully split or merge two neighboring cells when they get too close. While this process introduces
|
||||
slight steps at the top and bottom of grayscale response, for most inputs these are not noticeable.
|
||||
|
||||
On the other hand, for SVG vector elements as well as for traced raster images, Gerbolyze cannot help with these design
|
||||
rules. There is no heuristic that would allow Gerbolyze to non-destructively "fix" a design here, so all that's on the
|
||||
roadmap here is to eventually include a gerber-level design rule checker.
|
||||
|
||||
As far as board houses go, I have made good experiences with the popular Chinese board houses. In my experience, JLC
|
||||
will just produce whatever you send them with little fucks being given about design rule adherence or validity of the
|
||||
input gerbers. This is great if you just want artistic circuit boards without much of a hassle, and you don't care if
|
||||
they come out exactly as you imagined. The worst I've had happen was when an older version of gerbolyze generated
|
||||
polygons with holes assuming standard fill-rule processing. The in the board house's online gerber viewer things looked
|
||||
fine, and neither did they complain during file review. However, the resulting boards looked completely wrong because
|
||||
all the dark halftones were missing.
|
||||
|
||||
PCBWay on the other hand has a much more rigurous file review process. They <em>will</em> complain when you throw
|
||||
illegal garbage gerbers at them, and they will helpfully guide you through your design rule violations. In this way you
|
||||
get much more of a professional service from them and for designs that have to be functional their higher level of
|
||||
scrutiny definitely is a good thing. For the design you saw in the first picture in this article, I ended up begging
|
||||
them to just plot my files if it doesn't physically break their machines and to their credit, while they seemed unhappy
|
||||
about it they did it and the result looks absolutely stunning.
|
||||
|
||||
PCBWay is a bit more expensive on their lowest-end offering than JLC, but I found that for anything else (large boards,
|
||||
multi-layer, gold plating etc.) their prices match. PCBWay offers a much broader range of manufacturing options such as
|
||||
flexible circuit boards, multi-layer boards, thick or thin substrates and high-temperature substrates.
|
||||
|
||||
When in doubt about how your design is going to come out on the board, do not hesitate to contact your board house. Most
|
||||
of the end customer-facing online PCB services have a number of different factories that do a number of different
|
||||
fabrication processes for them depending on order parameters. Places like PCBWay have exceptional quality control and
|
||||
good customer service, but that is mostly focused on the technical aspects of the PCB. If you rely on visual aspects
|
||||
like silkscreen uniformity or solder mask color that is a strong no concern to everyone else in the electronics
|
||||
industry, you may find significant variations between manufacturers or even between orders with the same manufacturer
|
||||
and you may encounter challenges communicating your requirements.
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
SVG raster features
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Currently, SVG masks and filters are not supported. Though SVG is marketed as a "vector graphics format", these two
|
||||
features are really raster primitives that all SVG viewers perform at the pixel level after rasterization. Since
|
||||
supporting these would likely not end up looking like what you want, it is not a planned feature. If you need masks or
|
||||
filters, simply export the relevant parts of the SVG as a PNG then include that in your template.
|
||||
|
||||
Gerber pass-through
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Since gerbolyze has to composite your input gerbers with its own output, it has to fully parse and re-serialize them.
|
||||
gerbolyze gerbonara_ for all its gerber parsing needs. Thus, gerbonara will interpret your gerbers and output will be in
|
||||
gerbonara's gerber "dialect". If you find a corner case where this does not work and the output looks wrong, please file
|
||||
a bug report with an example file on the gerbonara_ bug tracker. *Always* check the output files for errors before
|
||||
submitting them to production.
|
||||
|
||||
Gerbolyze is provided without any warranty, but still please open an issue or `send me an email
|
||||
<mailto:gerbolyze@jaseg.de>`__ if you find any errors or inconsistencies.
|
||||
|
||||
Trace/Space design rule adherence
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
While the grayscale halftone vectorizers do a reasonable job adhering to a given trace/space design rule, they can still
|
||||
produce small parts of output that violate it. For the contour vectorizer as well as for all SVG primitives, you are
|
||||
responsible for adhering to design rules yourself as there is no algorithm that gerboyze could use to "fix" its input.
|
||||
|
||||
A design rule checker is planned as a future addition to gerbolyze, but is not yet part of it. If in doubt, talk to your
|
||||
fab and consider doing a test run of your design before ordering assembled boards ;)
|
||||
|
||||
Gallery
|
||||
-------
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample2.jpg
|
||||
.. image:: pics/sample3.jpg
|
||||
:width: 400px
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample3.jpg
|
||||
For a demonstration of ``gerbolyze convert``, check out the `Gerbolyze Protoboard Index`_, where you can download gerber
|
||||
files for over 7.000 SMD and THT protoboard layouts.
|
||||
|
||||
Licensing
|
||||
---------
|
||||
|
||||
This tool is licensed under the rather radical AGPLv3 license. Briefly, this means that you have to provide users of a
|
||||
webapp using this tool in the backend with this tool's source.
|
||||
|
||||
I get that some people have issues with the AGPL. In case this license prevents you from using this software, please
|
||||
send me `an email <mailto:agpl.sucks@jaseg.de>`__ and I can grant you an exception. I want this software to be useful to as
|
||||
many people as possible and I wouldn't want the license to be a hurdle to anyone. OTOH I see a danger of some cheap
|
||||
board house just integrating a fork into their webpage without providing their changes back upstream, and I want to
|
||||
avoid that so the default license is still AGPL.
|
||||
|
||||
.. _usvg: https://github.com/RazrFalcon/resvg
|
||||
.. _Inkscape: https://inkscape.org/
|
||||
.. _pcb-tools: https://github.com/curtacircuitos/pcb-tools
|
||||
.. _pcb-tools-extension: https://github.com/opiopan/pcb-tools-extension
|
||||
.. _GIMP: https://gimp.org/
|
||||
.. _gerbonara: https://gitlab.com/gerbolyze/gerbonara
|
||||
.. _`Gerbolyze Protoboard Index`: https://dyna.kokoroyukuma.de/protos/
|
||||
|
||||
|
|
|
|||
8
TODO
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[ ] Do not just return "error 255" if usvg is not installed
|
||||
[ ] Straighten up svg-flatten input unit handling
|
||||
[x] split up python code into modules
|
||||
[x] Add backwards-compatible vectorize drop-in
|
||||
[x] Figure out handling of drill layers
|
||||
[x] 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?)
|
||||
5
bin/gerbolyze
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
if __name__ == '__main__':
|
||||
import gerbolyze
|
||||
gerbolyze.cli()
|
||||
26
export_previews.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import multiprocessing as mp
|
||||
import subprocess
|
||||
import pathlib
|
||||
|
||||
import click
|
||||
from tqdm import tqdm
|
||||
|
||||
def process_file(indir, outdir, inpath):
|
||||
outpath = outdir / inpath.relative_to(indir).with_suffix('.png')
|
||||
outpath.parent.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(['resvg', '--export-id', 'g-top-copper', '--width', '1000', inpath, outpath],
|
||||
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
@click.command()
|
||||
@click.argument('indir', type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=pathlib.Path))
|
||||
def export(indir):
|
||||
jobs = list(indir.glob('svg/**/*.svg'))
|
||||
with tqdm(total = len(jobs)) as tq:
|
||||
with mp.Pool() as pool:
|
||||
results = [ pool.apply_async(process_file, (indir / 'svg', indir / 'png', path), callback=lambda _res: tq.update(1)) for path in jobs ]
|
||||
results = [ res.get() for res in results ]
|
||||
|
||||
if __name__ == '__main__':
|
||||
export()
|
||||
26
export_protoboards.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import multiprocessing as mp
|
||||
import subprocess
|
||||
import pathlib
|
||||
|
||||
import click
|
||||
from tqdm import tqdm
|
||||
|
||||
def process_file(indir, outdir, inpath):
|
||||
outpath = outdir / inpath.relative_to(indir).with_suffix('.zip')
|
||||
outpath.parent.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run('python3 -m gerbolyze convert --zip --pattern-complete-tiles-only --use-apertures-for-patterns'.split() + [inpath, outpath],
|
||||
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
@click.command()
|
||||
@click.argument('indir', type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=pathlib.Path))
|
||||
def export(indir):
|
||||
jobs = list(indir.glob('svg/**/*.svg'))
|
||||
with tqdm(total = len(jobs)) as tq:
|
||||
with mp.Pool() as pool:
|
||||
results = [ pool.apply_async(process_file, (indir / 'svg', indir / 'gerber', path), callback=lambda _res: tq.update(1)) for path in jobs ]
|
||||
results = [ res.get() for res in results ]
|
||||
|
||||
if __name__ == '__main__':
|
||||
export()
|
||||
550
generate_protoboards.py
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import itertools
|
||||
import pathlib
|
||||
import textwrap
|
||||
|
||||
import click
|
||||
|
||||
from gerbolyze.protoboard import ProtoBoard, EmptyProtoArea, THTProtoArea, SMDProtoAreaRectangles, ManhattanProtoArea
|
||||
|
||||
common_defs = '''
|
||||
empty = Empty(copper=False);
|
||||
ground = Empty(copper=True);
|
||||
|
||||
tht = THTPads();
|
||||
thtsq = THTPads(pad_shape="square");
|
||||
thtl = THTPads(drill=1.2);
|
||||
thtxl = THTPads(drill=1.6, pad_size=2.1, pad_shape="square");
|
||||
tht50 = THTPads(pad_size=1.0, drill=0.6, pitch=1.27);
|
||||
tht50sq = THTPads(pad_size=1.0, drill=0.6, pitch=1.27, pad_shape="square");
|
||||
manhattan = Manhattan();
|
||||
|
||||
conn125 = THTPads(drill=0.6, pad_size=1.0, pitch=1.25);
|
||||
conn250 = THTPads(drill=1.0, pad_size=1.6, pitch=2.00);
|
||||
conn200 = THTPads(drill=1.2, pad_size=2.0, pitch=2.50);
|
||||
conn350 = THTPads(drill=1.6, pad_size=2.8, pitch=3.50);
|
||||
conn396 = THTPads(drill=1.6, pad_size=2.8, pitch=3.96);
|
||||
|
||||
smd100 = SMDPads(1.27, 2.54);
|
||||
smd100r = SMDPads(2.54, 1.27);
|
||||
smd950 = SMDPads(0.95, 2.5);
|
||||
smd950r = SMDPads(2.5, 0.95);
|
||||
smd800 = SMDPads(0.80, 2.0);
|
||||
smd800r = SMDPads(2.0, 0.80);
|
||||
smd650 = SMDPads(0.65, 2.0);
|
||||
smd650r = SMDPads(2.0, 0.65);
|
||||
smd500 = SMDPads(0.5, 2.0);
|
||||
smd500r = SMDPads(2.0, 0.5);
|
||||
'''
|
||||
|
||||
|
||||
smd_basic = {
|
||||
'smd100': 'smd_soic_100mil',
|
||||
'smd950': 'smd_sot_950um',
|
||||
'smd800': 'smd_sop_800um',
|
||||
'smd650': 'smd_sot_650um',
|
||||
'smd500': 'smd_sop_500um',
|
||||
'manhattan': 'manhattan_400mil'}
|
||||
|
||||
connector_pitches = {
|
||||
'tht50': '50mil',
|
||||
'conn125': '1.25mm',
|
||||
'conn200': '2.00mm',
|
||||
'conn250': '2.50mm',
|
||||
'conn350': '3.50mm',
|
||||
'conn396': '3.96mm',
|
||||
}
|
||||
|
||||
#lengths_large = [15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100, 120, 150, 160, 180, 200, 250, 300]
|
||||
lengths_large = [30, 40, 50, 60, 80, 100, 120, 150, 160]
|
||||
sizes_large = list(itertools.combinations(lengths_large, 2))
|
||||
|
||||
lengths_small = [15, 20, 25, 30, 40, 50, 60, 80, 100]
|
||||
sizes_small = list(itertools.combinations(lengths_small, 2))
|
||||
|
||||
lengths_medium = lengths_large
|
||||
sizes_medium = list(itertools.combinations(lengths_medium, 2))
|
||||
|
||||
def min_dim(sizes, dim):
|
||||
return [(w, h) for w, h in sizes if w > dim and h > dim]
|
||||
|
||||
def write_index(index, outdir):
|
||||
tht_pitches = lambda patterns: [ p.pitch for p in patterns if isinstance(p, THTProtoArea) ]
|
||||
smd_pitches = lambda patterns: [ min(p.pitch_x, p.pitch_y) for p in patterns if isinstance(p, SMDProtoAreaRectangles) ]
|
||||
has_ground_plane = lambda patterns: any(isinstance(p, EmptyProtoArea) and p.copper for p in patterns)
|
||||
has_manhattan_area = lambda patterns: any(isinstance(p, ManhattanProtoArea) for p in patterns)
|
||||
has_square_pads = lambda patterns: any(isinstance(p, THTProtoArea) and p.pad_shape == 'square' for p in patterns)
|
||||
has_large_holes = lambda patterns: any(isinstance(p, THTProtoArea) and abs(p.pitch_x - 2.54) < 0.01 and p.drill > 1.1 for p in patterns)
|
||||
format_pitches = lambda pitches: ', '.join(f'{p:.2f}' for p in sorted(pitches))
|
||||
format_length = lambda length_or_none, default='': default if length_or_none is None else f'{length_or_none:.2f} mm'
|
||||
area_count = lambda patterns: len(set(p for p in patterns if not isinstance(p, EmptyProtoArea)))
|
||||
|
||||
table_rows = [
|
||||
('<tr>'
|
||||
f'<td><a href="gerber/{path.relative_to(outdir / "svg").with_suffix(".zip")}" download>Gerber</a></td>'
|
||||
f'<td><a href="png/{path.relative_to(outdir / "svg").with_suffix(".png")}">Preview</a></td>'
|
||||
f'<td><a href="{path.relative_to(outdir)}" download>SVG</a></td>'
|
||||
f'<td>{w:.2f}</td>'
|
||||
f'<td>{h:.2f}</td>'
|
||||
f'<td>{"Yes" if hole_dia is not None else "No"}</td>'
|
||||
f'<td>{f"{hole_dia:.2f}" if hole_dia is not None else ""}</td>'
|
||||
f'<td>{area_count(patterns)}</td>'
|
||||
f'<td>{"Yes" if symmetric else "No"}</td>'
|
||||
f'<td>{"Yes" if has_ground_plane(patterns) else "No"}</td>'
|
||||
f'<td>{"Yes" if has_manhattan_area(patterns) else "No"}</td>'
|
||||
f'<td>{"Yes" if has_square_pads(patterns) else "No"}</td>'
|
||||
f'<td>{"Yes" if has_large_holes(patterns) else "No"}</td>'
|
||||
f'<td>{format_pitches(tht_pitches(patterns))}</td>'
|
||||
f'<td>{format_pitches(smd_pitches(patterns))}</td>'
|
||||
'</tr>')
|
||||
for path, (w, h, hole_dia, symmetric, patterns) in index.items()
|
||||
]
|
||||
table_content = '\n'.join(table_rows)
|
||||
length_sort = lambda length: float(length.partition(' ')[0])
|
||||
filter_cols = {
|
||||
'Width': sorted(set(w for w, h, *rest in index.values())),
|
||||
'Height': sorted(set(h for w, h, *rest in index.values())),
|
||||
'Mounting Hole Diameter': sorted(set(dia for w, h, dia, *rest in index.values() if dia)) + ['None'],
|
||||
'Number of Areas': sorted(set(area_count(patterns) for *_rest, patterns in index.values())),
|
||||
'Symmetric Top and Bottom?': ['Yes', 'No'],
|
||||
'Ground Plane?': ['Yes', 'No'],
|
||||
'Manhattan Area?': ['Yes', 'No'],
|
||||
'Square Pads?': ['Yes', 'No'],
|
||||
'Large Holes?': ['Yes', 'No'],
|
||||
'THT Pitches': sorted(set(p for *_rest, patterns in index.values() for p in tht_pitches(patterns))) + ['None'],
|
||||
'SMD Pitches': sorted(set(p for *_rest, patterns in index.values() for p in smd_pitches(patterns))) + ['None'],
|
||||
}
|
||||
filter_headers = '\n'.join(f'<th>{key}</th>' for key in filter_cols)
|
||||
key_id = lambda key: key.lower().replace("?", "").replace(" ", "_")
|
||||
val_id = lambda value: str(value).replace(".", "_")
|
||||
|
||||
def format_value(value):
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
elif isinstance(value, int):
|
||||
return str(value)
|
||||
elif isinstance(value, bool):
|
||||
return value and 'Yes' or 'No'
|
||||
else:
|
||||
return format_length(value)
|
||||
|
||||
filter_cols = {
|
||||
key: '\n'.join(f'<div class="filter-check"><input type="checkbox" id="check-{key_id(key)}-{val_id(value)}"><label for="check-{key_id(key)}-{val_id(value)}">{format_value(value)}</label></div>' for value in values)
|
||||
for key, values in filter_cols.items() }
|
||||
filter_cols = [f'<td id="filter-{key_id(key)}">{values}</td>' for key, values in filter_cols.items()]
|
||||
filter_content = '\n'.join(filter_cols)
|
||||
|
||||
filter_js = textwrap.dedent('''
|
||||
function get_filters(){
|
||||
let filters = {};
|
||||
table = document.querySelector('#filter');
|
||||
for (let filter of table.querySelectorAll('td')) {
|
||||
selected = [];
|
||||
for (let checkbox of filter.querySelectorAll('input')) {
|
||||
if (checkbox.checked) {
|
||||
selected.push(checkbox.nextElementSibling.textContent.replace(/ mm$/, ''));
|
||||
}
|
||||
}
|
||||
filters[filter.id.replace(/^filter-/, '')] = selected;
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
filter_indices = {
|
||||
};
|
||||
for (const [i, header] of document.querySelectorAll("#listing th").entries()) {
|
||||
if (header.hasAttribute('data-filter-key')) {
|
||||
filter_indices[header.attributes['data-filter-key'].value] = i;
|
||||
}
|
||||
}
|
||||
|
||||
function filter_row(filters, row) {
|
||||
cols = row.querySelectorAll('td');
|
||||
|
||||
for (const [filter_id, values] of Object.entries(filters)) {
|
||||
if (values.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const row_value = cols[filter_indices[filter_id]].textContent;
|
||||
|
||||
if (values.includes("None") && !row_value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (values.includes(row_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
let timeout = undefined;
|
||||
function apply_filters() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
}
|
||||
const filters = get_filters();
|
||||
for (let row of document.querySelectorAll("#listing tbody tr")) {
|
||||
if (filter_row(filters, row)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refresh_filters() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(apply_filters, 2000);
|
||||
}
|
||||
|
||||
function reset_filters() {
|
||||
for (let checkbox of document.querySelectorAll("#filter input")) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
refresh_filters();
|
||||
}
|
||||
|
||||
document.querySelector("#apply").onclick = apply_filters;
|
||||
document.querySelector("#reset-filters").onclick = reset_filters;
|
||||
for (let checkbox of document.querySelectorAll("#filter input")) {
|
||||
checkbox.onchange = refresh_filters;
|
||||
}
|
||||
|
||||
apply_filters();
|
||||
'''.strip())
|
||||
|
||||
style = textwrap.dedent('''
|
||||
:root {
|
||||
--gray1: #d0d0d0;
|
||||
--gray2: #eeeeee;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
box-shadow: 0 0 3px gray;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid var(--gray1);
|
||||
padding: .1em .5em;
|
||||
}
|
||||
|
||||
th {
|
||||
border: 1px solid var(--gray1);
|
||||
padding: .5em;
|
||||
background: linear-gradient(0deg, #e0e0e0, #eeeeee);
|
||||
}
|
||||
|
||||
#listing tr:hover {
|
||||
background-color: #ffff80;
|
||||
}
|
||||
|
||||
#listing tr td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#listing tr td:nth-child(4), #listing tr td:nth-child(5) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#filter {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 2em 0.2em;
|
||||
padding: .5em 1em;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 80em;
|
||||
margin: 3em auto;
|
||||
}
|
||||
|
||||
body > div {
|
||||
width: 100%;
|
||||
}
|
||||
'''.strip())
|
||||
html = textwrap.dedent(f'''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Gerbolyze Protoboard Index</title></head>
|
||||
<script src="tablesort.min.js"></script>
|
||||
<script src="tablesort.number.min.js"></script>
|
||||
<style>
|
||||
{style}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Gerbolyze Protoboard Index</h1>
|
||||
<p>
|
||||
This page contains gerbers for many different types of prototype circuit boards. Everything from different pitches
|
||||
of THT hole patterns to SMD pad patterns is included in many different sizes and with several mounting hole options.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
All downloads on this page are licensed under the <a href="https://unlicense.org">Unlicense</a>. This means you can
|
||||
download what you like and do with it whatever you want. Just note that everything here is provided without any
|
||||
warranty, so if you send files you find here to a pcb board house and what you get back from them is all wrong,
|
||||
that's your problem.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
All files on this page have been generated automatically from a number of templates using
|
||||
<a href="https://gitlab.com/gerbolyze/gerbolyze/">gerbolyze</a>
|
||||
(<a href="https://github.com/jaseg/gerbolyze">github mirror</a>). If you have any suggestions for additional layouts
|
||||
or layout options, please feel free to file an issue on
|
||||
<a href="https://github.com/jaseg/gerbolyze/issues">Gerbolyze's issue tracker</a> on github.
|
||||
</p>
|
||||
<div id="filters-container">
|
||||
<table id="filter">
|
||||
<tr>
|
||||
{filter_headers}
|
||||
</tr>
|
||||
<tr>
|
||||
{filter_content}
|
||||
</tr>
|
||||
</table>
|
||||
<button type="button" id="apply">Apply</button>
|
||||
<button type="button" id="reset-filters">Reset filters</button>
|
||||
</div>
|
||||
<div id="listing-container">
|
||||
<table id="listing">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sort-method="none" width="6em">Download</th>
|
||||
<th data-sort-method="none" width="6em">Preview</th>
|
||||
<th data-sort-method="none" width="3em">Source SVG</th>
|
||||
<th data-filter-key="width" width="3.5em">Width [mm]</th>
|
||||
<th data-filter-key="height" width="3.5em">Height [mm]</th>
|
||||
<th width="3em">Has Mounting Holes?</th>
|
||||
<th data-filter-key="mounting_hole_diameter" width="3em">Mounting Hole Diameter [mm]</th>
|
||||
<th data-filter-key="number_of_areas" width="3em">Number of Areas</th>
|
||||
<th data-filter-key="symmetric_top_and_bottom" width="3em">Symmetric Top and Bottom?</th>
|
||||
<th data-filter-key="ground_plane" width="3em">Ground Plane?</th>
|
||||
<th data-filter-key="manhattan_area" width="3em">Manhattan Area?</th>
|
||||
<th data-filter-key="square_pads" width="3em">Square Pads?</th>
|
||||
<th data-filter-key="large_holes" width="3em">Large Holes?</th>
|
||||
<th data-filter-key="tht_pitches">THT Pitches [mm]</th>
|
||||
<th data-filter-key="smd_pitches">SMD Pitches [mm]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{table_content}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
new Tablesort(document.getElementById('listing'));
|
||||
|
||||
{filter_js}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''.strip())
|
||||
(outdir / 'index.html').write_text(html)
|
||||
|
||||
|
||||
def generate(outdir, fun, sizes=sizes_large, name=None, generate_svg=True):
|
||||
name = name or fun.__name__
|
||||
outdir = outdir / f'{name}'
|
||||
plain_dir = outdir / 'no_mounting_holes'
|
||||
plain_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for w, h in sizes:
|
||||
outfile = plain_dir / f'{name}_{w}x{h}.svg'
|
||||
board = fun((w, h))
|
||||
yield outfile, (float(w), float(h), None, board.symmetric_sides, board.used_patterns)
|
||||
if generate_svg:
|
||||
outfile.write_text(board.generate(w, h))
|
||||
|
||||
for dia in (2, 2.5, 3, 4):
|
||||
hole_dir = outdir / f'mounting_holes_M{dia:.1f}'
|
||||
hole_dir.mkdir(exist_ok=True)
|
||||
|
||||
for w, h in sizes:
|
||||
if w < 25 or h < 25:
|
||||
continue
|
||||
outfile = hole_dir / f'{name}_{w}x{h}_holes_M{dia:.1f}.svg'
|
||||
try:
|
||||
# Add 0.2 mm tolerance to mounting holes for easier insertion of screw
|
||||
board = fun((w, h), (dia+0.2, dia+2))
|
||||
yield outfile, (float(w), float(h), float(dia), board.symmetric_sides, board.used_patterns)
|
||||
if generate_svg:
|
||||
outfile.write_text(board.generate(w, h))
|
||||
except ValueError: # mounting hole keepout too large for small board, ignore.
|
||||
pass
|
||||
|
||||
@click.command()
|
||||
@click.argument('outdir', type=click.Path(file_okay=False, dir_okay=True, path_type=pathlib.Path))
|
||||
@click.option('--generate-svg/--no-generate-svg')
|
||||
def generate_all(outdir, generate_svg):
|
||||
index_d = {}
|
||||
def index(sizes=sizes_large, name=None):
|
||||
def deco(fun):
|
||||
nonlocal index_d
|
||||
index_d.update(generate(outdir / 'svg', fun, sizes=sizes, name=name, generate_svg=generate_svg))
|
||||
return fun
|
||||
return deco
|
||||
|
||||
@index()
|
||||
def tht_normal_pitch100mil(size, mounting_holes=None):
|
||||
return ProtoBoard(common_defs, 'tht', mounting_holes, border=2)
|
||||
|
||||
@index()
|
||||
def tht_normal_pitch100mil_large_holes(size, mounting_holes=None):
|
||||
return ProtoBoard(common_defs, 'thtl', mounting_holes, border=2)
|
||||
|
||||
@index()
|
||||
def tht_normal_pitch100mil_xl_holes(size, mounting_holes=None):
|
||||
return ProtoBoard(common_defs, 'thtl', mounting_holes, border=2)
|
||||
|
||||
@index()
|
||||
def tht_normal_pitch100mil_square_pads(size, mounting_holes=None):
|
||||
return ProtoBoard(common_defs, 'thtl', mounting_holes, border=2)
|
||||
|
||||
@index()
|
||||
def tht_pitch_50mil(size, mounting_holes=None):
|
||||
return ProtoBoard(common_defs, 'tht50', mounting_holes, border=2)
|
||||
|
||||
@index()
|
||||
def tht_pitch_50mil_square_pads(size, mounting_holes=None):
|
||||
return ProtoBoard(common_defs, 'tht50', mounting_holes, border=2)
|
||||
|
||||
@index()
|
||||
def tht_mixed_pitch(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
return ProtoBoard(common_defs, f'tht50@{f}mm / tht', mounting_holes, border=2, tight_layout=True)
|
||||
|
||||
@index()
|
||||
def tht_mixed_pitch_square_pads(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
return ProtoBoard(common_defs, f'tht50@{f}mm / tht', mounting_holes, border=2, tight_layout=True)
|
||||
|
||||
for pattern, name in connector_pitches.items():
|
||||
@index(name=f'tht_and_connector_area_{name}')
|
||||
def tht_and_connector_area(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(3.96*2.1, min(15, h*0.1))
|
||||
return ProtoBoard(common_defs, f'{pattern}@{f}mm / tht', border=2, tight_layout=True)
|
||||
|
||||
@index()
|
||||
def tht_and_connector_areas(size, mounting_holes=None):
|
||||
w, h = size
|
||||
fh = max(3.96*2.1, min(15, h*0.1))
|
||||
fw = max(3.96*2.1, min(15, w*0.1))
|
||||
return ProtoBoard(common_defs, f'conn396@{fw}mm | ((tht50 | conn200)@{fh}mm / tht / (conn125|conn250)@{fh}mm) | conn350@{fw}mm', border=2, tight_layout=True)
|
||||
|
||||
for pattern, name in smd_basic.items():
|
||||
pattern_sizes = sizes_small if pattern not in ['manhattan'] else sizes_medium
|
||||
# Default to ground plane on back for manhattan proto boards
|
||||
pattern_back = pattern if pattern not in ['manhattan'] else 'ground'
|
||||
|
||||
@index(sizes=pattern_sizes, name=f'{name}_ground_plane')
|
||||
def gen(size, mounting_holes=None):
|
||||
return ProtoBoard(common_defs, f'{pattern} + ground', mounting_holes, border=1)
|
||||
|
||||
@index(sizes=pattern_sizes, name=f'{name}_single_side')
|
||||
def gen(size, mounting_holes=None):
|
||||
return ProtoBoard(common_defs, f'{pattern} + empty', mounting_holes, border=1)
|
||||
|
||||
@index(sizes=pattern_sizes, name=f'{name}_double_side')
|
||||
def gen(size, mounting_holes=None):
|
||||
return ProtoBoard(common_defs, f'{pattern} + {pattern}', mounting_holes, border=1)
|
||||
|
||||
@index(sizes=pattern_sizes, name=f'tht_and_{name}_large_holes')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
return ProtoBoard(common_defs, f'({pattern} + {pattern_back})@{f}mm / thtl', mounting_holes, border=1, tight_layout=True)
|
||||
|
||||
@index(sizes=pattern_sizes, name=f'{name}_and_tht_large_holes')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
return ProtoBoard(common_defs, f'({pattern} + {pattern_back}) / thtl@{f}mm', mounting_holes, border=1, tight_layout=True)
|
||||
|
||||
@index(sizes=pattern_sizes, name=f'tht_and_{name}')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
return ProtoBoard(common_defs, f'({pattern} + {pattern_back})@{f}mm / tht', mounting_holes, border=1, tight_layout=True)
|
||||
|
||||
@index(sizes=pattern_sizes, name=f'{name}_and_tht')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
return ProtoBoard(common_defs, f'({pattern} + {pattern_back}) / tht@{f}mm', mounting_holes, border=1, tight_layout=True)
|
||||
|
||||
@index(sizes=min_dim(pattern_sizes, 20), name=f'{name}_and_connector_areas')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
fh = max(3.96*2.1, min(15, h*0.1))
|
||||
fw = max(3.96*2.1, min(15, w*0.1))
|
||||
return ProtoBoard(common_defs, f'conn396@{fw}mm | ((tht50 | conn200)@{fh}mm / ({pattern} + {pattern_back}) / (conn125|conn250)@{fh}mm) | conn350@{fw}mm', border=2, tight_layout=True)
|
||||
|
||||
*_, suffix = name.split('_')
|
||||
if suffix not in ('100mil', '950um'):
|
||||
@index(sizes=sizes_medium, name=f'tht_and_three_smd_100mil_950um_{suffix}')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(50, h*0.3))
|
||||
f2 = max(1.27*5, min(30, w*0.2))
|
||||
pattern_rot = f'{pattern}r' if pattern not in ['manhattan'] else pattern
|
||||
pattern_back_rot = f'{pattern_back}r' if pattern not in ['manhattan'] else 'ground'
|
||||
return ProtoBoard(common_defs, f'((smd100 + smd100) | (smd950 + smd950) | ({pattern_rot} + {pattern_back_rot})@{f2}mm)@{f}mm / tht', mounting_holes, border=1, tight_layout=True)
|
||||
|
||||
for (pattern1, name1), (pattern2, name2) in itertools.combinations(smd_basic.items(), 2):
|
||||
*_, name1 = name1.split('_')
|
||||
*_, name2 = name2.split('_')
|
||||
|
||||
@index(sizes=sizes_small, name=f'tht_and_two_smd_{name1}_{name2}')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
return ProtoBoard(common_defs, f'(({pattern1} + {pattern1}) | ({pattern2} + {pattern2}))@{f}mm / tht', mounting_holes, border=1, tight_layout=True)
|
||||
|
||||
@index(sizes=sizes_small, name=f'tht_and_two_sided_smd_{name1}_{name2}')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
return ProtoBoard(common_defs, f'({pattern1} + {pattern2})@{f}mm / tht', mounting_holes, border=1, tight_layout=True)
|
||||
|
||||
@index(sizes=sizes_small, name=f'two_sided_smd_{name1}_{name2}')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
return ProtoBoard(common_defs, f'{pattern1} + {pattern2}', mounting_holes, border=1)
|
||||
|
||||
@index(sizes_medium, name=f'tht_and_50mil_and_two_smd_100mil_950um')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(50, h*0.3))
|
||||
f2 = max(1.27*5, min(30, w*0.2))
|
||||
return ProtoBoard(common_defs, f'((smd100 + smd100) | (smd950 + smd950) | tht50@{f2}mm)@{f}mm / tht', mounting_holes, border=1, tight_layout=True)
|
||||
|
||||
@index(sizes=min_dim(sizes_medium, 60), name=f'all_tht_and_smd')
|
||||
def gen(size, mounting_holes=None):
|
||||
w, h = size
|
||||
f = max(1.27*5, min(30, h*0.3))
|
||||
f2 = max(1.27*5, min(25, w*0.1))
|
||||
return ProtoBoard(common_defs, f'tht50@10mm | tht | ((smd100r + smd100r) / (smd950r + smd950r) / (smd800 + smd800)@{f2}mm / (smd650 + smd650)@{f2}mm / (smd500 + smd500)@{f2}mm)@{f}mm', mounting_holes, border=1, tight_layout=True)
|
||||
|
||||
write_index(index_d, outdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate_all()
|
||||
|
||||
115
generate_scaled_footprints.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import tempfile
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import click
|
||||
|
||||
default_widths = '3mm,5mm,8mm,10mm,12mm,15mm,18mm,20mm,25mm,30mm,35mm,40mm,45mm,50mm,60mm,70mm,80mm,90mm,100mm,120mm,150mm'
|
||||
|
||||
|
||||
# Mostly from https://www.w3.org/TR/css-values/#absolute-lengths
|
||||
UNIT_FACTORS = {
|
||||
'm': 1000,
|
||||
'cm': 10,
|
||||
'mm': 1,
|
||||
'Q': 1/4,
|
||||
'in': 25.4,
|
||||
'mil': 25.4/1000,
|
||||
'pc': 25.4/6,
|
||||
'pt': 25.4/72,
|
||||
'px': 25.4/96,
|
||||
}
|
||||
|
||||
def parse_length(foo, default_unit=None):
|
||||
''' Parse given physical length, and return result converted to mm. '''
|
||||
|
||||
match = re.fullmatch(r'(.*?)(m|cm|mm|Q|in|mil|pc|pt|px|)', foo.strip().lower())
|
||||
if not match:
|
||||
raise ValueError(f'Invalid length "{foo}"')
|
||||
num, unit = match.groups()
|
||||
|
||||
if not unit:
|
||||
if default_unit:
|
||||
unit = default_unit
|
||||
else:
|
||||
raise ValueError(f'Unit missing from length "{foo}"')
|
||||
|
||||
return float(num) * UNIT_FACTORS[unit]
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--width')
|
||||
@click.option('--height')
|
||||
@click.option('--sexp-layer', default='F.SilkS')
|
||||
@click.option('--basename', help='Base name for generated symbols and library')
|
||||
@click.argument('input_svg')
|
||||
def export(width, height, basename, sexp_layer, input_svg):
|
||||
svg_flatten = str(Path(os.environ.get('SVG_FLATTEN', 'svg-flatten')).expanduser())
|
||||
usvg = str(Path(os.environ.get('USVG', 'usvg')).expanduser())
|
||||
|
||||
if not basename:
|
||||
match = re.fullmatch(r'(.*?)(([-_.][0-9.,]+)(m|cm|mm|Q|in|mil|pc|pt|px|))?', Path(input_svg).stem)
|
||||
basename, *rest = match.groups()
|
||||
print(f'No --basename given. Using "{basename}"')
|
||||
|
||||
export_width, export_height = width, height
|
||||
if not export_width or export_height:
|
||||
export_width = default_widths
|
||||
|
||||
elif export_width and export_height:
|
||||
raise click.ClickException('Only one of --width or --height must be given.')
|
||||
|
||||
if export_width:
|
||||
targets = export_width
|
||||
axis = 'width'
|
||||
else:
|
||||
targets = export_height
|
||||
axis = 'height'
|
||||
|
||||
# Determine input document size
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
try:
|
||||
subprocess.run([usvg, input_svg, f.name], check=True)
|
||||
except FileNotFoundError:
|
||||
raise click.ClickException('Cannot find usvg binary in PATH. You can give a custom path to the usvg binary by setting the USVG environment variable.')
|
||||
|
||||
soup = BeautifulSoup(f.read(), features='xml')
|
||||
svg = soup.find('svg')
|
||||
doc_w_mm, doc_h_mm = parse_length(svg['width'], default_unit='px'), parse_length(svg['height'], default_unit='px')
|
||||
|
||||
print(f'Input file has dimensions width {doc_w_mm:.1f} mm by height {doc_h_mm:.1f} mm')
|
||||
|
||||
outdir = Path(f'{basename}.pretty')
|
||||
outdir.mkdir(exist_ok=True)
|
||||
|
||||
for target_length in targets.split(','):
|
||||
target_length = parse_length(target_length, default_unit='mm')
|
||||
|
||||
if axis == 'width':
|
||||
scaling_factor = target_length / doc_w_mm
|
||||
else:
|
||||
scaling_factor = target_length / doc_h_mm
|
||||
|
||||
instance_name = f'{basename}_{target_length:.1f}mm'
|
||||
outfile = outdir / f'{instance_name}.kicad_mod'
|
||||
print(f'{outfile}: Scaling to target {axis} {target_length:.1f} mm using scaling factor {scaling_factor:.3f}')
|
||||
|
||||
try:
|
||||
proc = subprocess.run([svg_flatten,
|
||||
'-o', 'sexp',
|
||||
'--sexp-layer', sexp_layer,
|
||||
'--sexp-mod-name', instance_name,
|
||||
'--scale', str(scaling_factor),
|
||||
input_svg], check=True, capture_output=True)
|
||||
outfile.write_bytes(proc.stdout)
|
||||
except FileNotFoundError:
|
||||
raise click.ClickException('Cannot find svg-flatten binary in PATH. You can give a custom path to the svg-flatten binary by setting the SVG_FLATTEN environment variable.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
export()
|
||||
35
gerbolyze
|
|
@ -1,35 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import gerbolyze
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Parse command line arguments
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
subcommand = parser.add_subparsers(help='Sub-commands')
|
||||
subcommand.required, subcommand.dest = True, 'command'
|
||||
vectorize_parser = subcommand.add_parser('vectorize', help='Vectorize bitmap image onto gerber layer')
|
||||
render_parser = subcommand.add_parser('render', help='Render bitmap preview of board suitable as a template for positioning and scaling the input image')
|
||||
|
||||
parser.add_argument('-d', '--debugdir', type=str, default=None, help='Directory to place intermediate images into for debuggin')
|
||||
|
||||
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('source', help='Source gerber directory')
|
||||
vectorize_parser.add_argument('target', help='Target gerber directory')
|
||||
vectorize_parser.add_argument('image', help='Image to render')
|
||||
|
||||
render_parser.add_argument('--fab-resolution', '-r', type=float, nargs='?', default=6.0, help='Smallest feature size supported by PCB manufacturer, in mil. On silkscreen layers, this is the minimum font stroke width.')
|
||||
render_parser.add_argument('--oversampling', '-o', type=float, nargs='?', default=10, help='Oversampling factor for the image. If set to say, 10 pixels, one minimum feature size (see --fab-resolution) will be 10 pixels long. The input image for vectorization should not contain any detail of smaller pixel size than this number in order to be manufacturable.')
|
||||
render_parser.add_argument('side', choices=['top', 'bottom'], help='Target board side')
|
||||
render_parser.add_argument('source', help='Source gerber directory')
|
||||
render_parser.add_argument('image', help='Output image filename')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'vectorize':
|
||||
gerbolyze.process_gerbers(args.source, args.target, args.image, args.side, args.layer, args.debugdir)
|
||||
else: # command == render
|
||||
gerbolyze.render_preview(args.source, args.image, args.side, args.fab_resolution, args.oversampling)
|
||||
|
||||
372
gerbolyze.py
|
|
@ -1,372 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
import os.path as path
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
import math
|
||||
|
||||
import gerber
|
||||
from gerber.render.cairo_backend import GerberCairoContext
|
||||
import numpy as np
|
||||
import cv2
|
||||
import enum
|
||||
import tqdm
|
||||
|
||||
def generate_mask(
|
||||
outline,
|
||||
target,
|
||||
scale,
|
||||
bounds,
|
||||
debugimg,
|
||||
status_print,
|
||||
extend_overlay_r_mil,
|
||||
subtract_gerber
|
||||
):
|
||||
# Render all gerber layers whose features are to be excluded from the target image, such as board outline, the
|
||||
# original silk layer and the solder paste layer to binary images.
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
img_file = path.join(tmpdir, 'target.png')
|
||||
|
||||
status_print('Combining keepout composite')
|
||||
fg, bg = gerber.render.RenderSettings((1, 1, 1)), gerber.render.RenderSettings((0, 0, 0))
|
||||
ctx = GerberCairoContext(scale=scale)
|
||||
status_print(' * outline')
|
||||
ctx.render_layer(outline, settings=fg, bgsettings=bg, bounds=bounds)
|
||||
status_print(' * target layer')
|
||||
ctx.render_layer(target, settings=fg, bgsettings=bg, bounds=bounds)
|
||||
for fn, sub in subtract_gerber:
|
||||
status_print(' * extra layer', os.path.basename(fn))
|
||||
layer = gerber.loads(sub)
|
||||
ctx.render_layer(layer, settings=fg, bgsettings=bg, bounds=bounds)
|
||||
status_print('Rendering keepout composite')
|
||||
ctx.dump(img_file)
|
||||
|
||||
# Vertically flip exported image
|
||||
original_img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)[::-1, :]
|
||||
|
||||
f = 1 if outline.units == 'inch' else 25.4
|
||||
r = 1+2*max(1, int(extend_overlay_r_mil/1000 * f * scale))
|
||||
status_print('Expanding keepout composite by', r)
|
||||
|
||||
# Extend image by a few pixels and flood-fill from (0, 0) to mask out the area outside the outermost outline
|
||||
# This ensures no polygons are generated outside the board even for non-rectangular boards.
|
||||
border = 10
|
||||
outh, outw = original_img.shape
|
||||
extended_img = np.zeros((outh + 2*border, outw + 2*border), dtype=np.uint8)
|
||||
extended_img[border:outh+border, border:outw+border] = original_img
|
||||
debugimg(extended_img, 'outline')
|
||||
cv2.floodFill(extended_img, None, (0, 0), (255,))
|
||||
original_img = extended_img[border:outh+border, border:outw+border]
|
||||
debugimg(extended_img, 'flooded')
|
||||
|
||||
# Dilate the white areas of the image using gaussian blur and threshold. Use these instead of primitive dilation
|
||||
# here for their non-directionality.
|
||||
target_img = cv2.blur(original_img, (r, r))
|
||||
_, target_img = cv2.threshold(target_img, 255//(1+r), 255, cv2.THRESH_BINARY)
|
||||
return target_img
|
||||
|
||||
def render_gerbers_to_image(*gerbers, scale, bounds=None):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
img_file = path.join(tmpdir, 'target.png')
|
||||
fg, bg = gerber.render.RenderSettings((1, 1, 1)), gerber.render.RenderSettings((0, 0, 0))
|
||||
ctx = GerberCairoContext(scale=scale)
|
||||
|
||||
for grb in gerbers:
|
||||
ctx.render_layer(grb, settings=fg, bgsettings=bg, bounds=bounds)
|
||||
|
||||
ctx.dump(img_file)
|
||||
# Vertically flip exported image to align coordinate systems
|
||||
return cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)[::-1, :]
|
||||
|
||||
def pcb_area_mask(outline, scale, bounds):
|
||||
# Merge layers to target mask
|
||||
img = render_gerbers_to_image(outline, scale=scale, bounds=bounds)
|
||||
# Extend
|
||||
imgh, imgw = img.shape
|
||||
img_ext = np.zeros(shape=(imgh+2, imgw+2), dtype=np.uint8)
|
||||
img_ext[1:-1, 1:-1] = img
|
||||
# Binarize
|
||||
img_ext[img_ext < 128] = 0
|
||||
img_ext[img_ext >= 128] = 255
|
||||
# Flood-fill
|
||||
cv2.floodFill(img_ext, None, (0, 0), (255,)) # Flood-fill with white from top left corner (0,0)
|
||||
img_ext_snap = img_ext.copy()
|
||||
cv2.floodFill(img_ext, None, (0, 0), (0,)) # Flood-fill with black
|
||||
cv2.floodFill(img_ext, None, (0, 0), (255,)) # Flood-fill with white
|
||||
return np.logical_xor(img_ext_snap, img_ext)[1:-1, 1:-1].astype(float)
|
||||
|
||||
def generate_template(
|
||||
silk, mask, copper, outline, drill,
|
||||
image,
|
||||
process_resolution:float=6, # mil
|
||||
resolution_oversampling:float=10, # times
|
||||
status_print=lambda *args:None
|
||||
):
|
||||
|
||||
silk, mask, copper, outline, *drill = map(gerber.load_layer_data, [silk, mask, copper, outline, *drill])
|
||||
silk.layer_class = 'topsilk'
|
||||
mask.layer_class = 'topmask'
|
||||
copper.layer_class = 'top'
|
||||
outline.layer_class = 'outline'
|
||||
|
||||
|
||||
f = 1.0 if outline.cam_source.units == 'metric' else 25.4
|
||||
scale = (1000/process_resolution) / 25.4 * resolution_oversampling * f # dpmm
|
||||
bounds = outline.cam_source.bounding_box
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext(scale=scale)
|
||||
|
||||
ctx.render_layer(outline, bounds=bounds)
|
||||
ctx.render_layer(copper, bounds=bounds)
|
||||
ctx.render_layer(mask, bounds=bounds)
|
||||
ctx.render_layer(silk, bounds=bounds)
|
||||
for dr in drill:
|
||||
ctx.render_layer(dr, bounds=bounds)
|
||||
ctx.dump(image)
|
||||
|
||||
def paste_image(
|
||||
target_gerber:str,
|
||||
outline_gerber:str,
|
||||
source_img:np.ndarray,
|
||||
subtract_gerber:list=[],
|
||||
extend_overlay_r_mil:float=6,
|
||||
extend_picture_r_mil:float=2,
|
||||
status_print=lambda *args:None,
|
||||
debugdir:str=None):
|
||||
|
||||
debugctr = 0
|
||||
def debugimg(img, name):
|
||||
nonlocal debugctr
|
||||
if debugdir:
|
||||
cv2.imwrite(path.join(debugdir, '{:02d}{}.png'.format(debugctr, name)), img)
|
||||
debugctr += 1
|
||||
|
||||
# Parse outline layer to get bounds of gerber file
|
||||
status_print('Parsing outline gerber')
|
||||
outline = gerber.loads(outline_gerber)
|
||||
bounds = (minx, maxx), (miny, maxy) = outline.bounding_box
|
||||
grbw, grbh = maxx - minx, maxy - miny
|
||||
status_print(' * outline has offset {}, size {}'.format((minx, miny), (grbw, grbh)))
|
||||
|
||||
# Parse target layer
|
||||
status_print('Parsing target gerber')
|
||||
target = gerber.loads(target_gerber)
|
||||
(tminx, tmaxx), (tminy, tmaxy) = target.bounding_box
|
||||
status_print(' * target layer has offset {}, size {}'.format((tminx, tminy), (tmaxx-tminx, tmaxy-tminy)))
|
||||
|
||||
# Read source image
|
||||
imgh, imgw = source_img.shape
|
||||
scale = math.ceil(max(imgw/grbw, imgh/grbh)) # scale is in dpmm
|
||||
status_print(' * source image has size {}, going for scale {}dpmm'.format((imgw, imgh), scale))
|
||||
|
||||
# Merge layers to target mask
|
||||
target_img = generate_mask(outline, target, scale, bounds, debugimg, status_print, extend_overlay_r_mil, subtract_gerber)
|
||||
|
||||
# Threshold source image. Ideally, the source image is already binary but in case it's not, or in case it's not
|
||||
# exactly binary (having a few very dark or very light grays e.g. due to JPEG compression) we're thresholding here.
|
||||
status_print('Thresholding source image')
|
||||
qr = 1+2*max(1, int(extend_picture_r_mil/1000 * scale))
|
||||
source_img = source_img[::-1]
|
||||
_, source_img = cv2.threshold(source_img, 127, 255, cv2.THRESH_BINARY)
|
||||
debugimg(source_img, 'thresh')
|
||||
|
||||
# Pad image to size of target layer images generated above. After this, `scale` applies to the padded image as well
|
||||
# as the gerber renders. For padding, zoom or shrink the image to completely fit the gerber's rectangular bounding
|
||||
# box. Center the image vertically or horizontally if it has a different aspect ratio.
|
||||
status_print('Padding source image')
|
||||
tgth, tgtw = target_img.shape
|
||||
padded_img = np.zeros(shape=target_img.shape, dtype=source_img.dtype)
|
||||
offx = int((minx-tminx if tminx < minx else 0)*scale)
|
||||
offy = int((miny-tminy if tminy < miny else 0)*scale)
|
||||
offx += int(grbw*scale - imgw) // 2
|
||||
offy += int(grbh*scale - imgh) // 2
|
||||
endx, endy = min(offx+imgw, tgtw), min(offy+imgh, tgth)
|
||||
print('off', (offx, offy), 'end', (endx, endy), 'img', (imgw, imgh), 'tgt', (tgtw, tgth))
|
||||
padded_img[offy:endy, offx:endx] = source_img[:endy-offy, :endx-offx]
|
||||
debugimg(padded_img, 'padded')
|
||||
debugimg(target_img, 'target')
|
||||
|
||||
# Mask out excluded gerber features (source silk, holes, solder mask etc.) from the target image
|
||||
status_print('Masking source image')
|
||||
out_img = (np.multiply((padded_img/255.0), (target_img/255.0) * -1 + 1) * 255).astype(np.uint8)
|
||||
|
||||
debugimg(out_img, 'multiplied')
|
||||
|
||||
# Calculate contours from masked target image and plot them to the target gerber context
|
||||
status_print('Calculating contour lines')
|
||||
plot_contours(out_img,
|
||||
target,
|
||||
offx=(minx, miny),
|
||||
scale=scale,
|
||||
status_print=lambda *args: status_print(' ', *args))
|
||||
|
||||
# Write target gerber context to disk
|
||||
status_print('Generating output gerber')
|
||||
from gerber.render import rs274x_backend
|
||||
ctx = rs274x_backend.Rs274xContext(target.settings)
|
||||
target.render(ctx)
|
||||
out = ctx.dump().getvalue()
|
||||
status_print('Done.')
|
||||
return out
|
||||
|
||||
|
||||
def plot_contours(
|
||||
img:np.ndarray,
|
||||
layer:gerber.rs274x.GerberFile,
|
||||
offx:tuple,
|
||||
scale:float,
|
||||
debug=lambda *args:None,
|
||||
status_print=lambda *args:None):
|
||||
from gerber.primitives import Line, Region, Circle
|
||||
imgh, imgw = img.shape
|
||||
|
||||
# Extract contour hierarchy using OpenCV
|
||||
status_print('Extracting contours')
|
||||
# See https://stackoverflow.com/questions/48291581/how-to-use-cv2-findcontours-in-different-opencv-versions/48292371
|
||||
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_TC89_KCOS)[-2:]
|
||||
|
||||
aperture = list(layer.apertures)[0] if layer.apertures else Circle(None, 0.10)
|
||||
|
||||
status_print('offx', offx, 'scale', scale)
|
||||
|
||||
xbias, ybias = offx
|
||||
def map(coord):
|
||||
x, y = coord
|
||||
return (x/scale + xbias, y/scale + ybias)
|
||||
def contour_lines(c):
|
||||
return [ Line(map(start), map(end), aperture, units=layer.settings.units)
|
||||
for start, end in zip(c, np.vstack((c[1:], c[:1]))) ]
|
||||
|
||||
done = []
|
||||
process_stack = [-1]
|
||||
next_process_stack = []
|
||||
parents = [ (i, first_child != -1, parent) for i, (_1, _2, first_child, parent) in enumerate(hierarchy[0]) ]
|
||||
is_dark = True
|
||||
status_print('Converting contours to gerber primitives')
|
||||
with tqdm.tqdm(total=len(contours)) as progress:
|
||||
while len(done) != len(contours):
|
||||
for i, has_children, parent in parents[:]:
|
||||
if parent in process_stack:
|
||||
contour = contours[i]
|
||||
polarity = 'dark' if is_dark else 'clear'
|
||||
debug('rendering {} with parent {} as {} with {} vertices'.format(i, parent, polarity, len(contour)))
|
||||
debug('process_stack is', process_stack)
|
||||
debug()
|
||||
layer.primitives.append(Region(contour_lines(contour[:,0]), level_polarity=polarity, units=layer.settings.units))
|
||||
if has_children:
|
||||
next_process_stack.append(i)
|
||||
done.append(i)
|
||||
parents.remove((i, has_children, parent))
|
||||
progress.update(1)
|
||||
debug('skipping to next level')
|
||||
process_stack, next_process_stack = next_process_stack, []
|
||||
is_dark = not is_dark
|
||||
debug('done', done)
|
||||
|
||||
# Utility foo
|
||||
# ===========
|
||||
|
||||
def find_gerber_in_dir(dir_path, extensions, exclude=''):
|
||||
contents = os.listdir(dir_path)
|
||||
exts = extensions.split('|')
|
||||
excs = exclude.split('|')
|
||||
for entry in contents:
|
||||
if any(entry.lower().endswith(ext.lower()) for ext in exts) and not any(entry.lower().endswith(ex) for ex in excs if exclude):
|
||||
lname = path.join(dir_path, entry)
|
||||
if not path.isfile(lname):
|
||||
continue
|
||||
with open(lname, 'r') as f:
|
||||
return lname, f.read()
|
||||
|
||||
raise ValueError(f'Cannot find file with suffix {extensions} in dir {dir_path}')
|
||||
|
||||
# Gerber file name extensions for Altium/Protel | KiCAD | Eagle
|
||||
LAYER_SPEC = {
|
||||
'top': {
|
||||
'paste': '.gtp|-F_Paste.gbr|-F.Paste.gbr|.pmc',
|
||||
'silk': '.gto|-F_SilkS.gbr|-F.SilkS.gbr|.plc',
|
||||
'mask': '.gts|-F_Mask.gbr|-F.Mask.gbr|.stc',
|
||||
'copper': '.gtl|-F_Cu.gbr|-F.Cu.gbr|.cmp',
|
||||
'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb',
|
||||
},
|
||||
'bottom': {
|
||||
'paste': '.gbp|-B_Paste.gbr|-B.Paste.gbr|.pms',
|
||||
'silk': '.gbo|-B_SilkS.gbr|-B.SilkS.gbr|.pls',
|
||||
'mask': '.gbs|-B_Mask.gbr|-B.Mask.gbr|.sts',
|
||||
'copper': '.gbl|-B_Cu.gbr|-B.Cu.gbr|.sol',
|
||||
'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb'
|
||||
},
|
||||
}
|
||||
|
||||
# Command line interface
|
||||
# ======================
|
||||
|
||||
def process_gerbers(source, target, image, side, layer, debugdir):
|
||||
if not os.path.isdir(source):
|
||||
raise ValueError(f'Given source "{source}" is not a directory.')
|
||||
|
||||
# Load input files
|
||||
source_img = cv2.imread(image, cv2.IMREAD_GRAYSCALE)
|
||||
if source_img is None:
|
||||
print(f'"{image}" is not a valid image file', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
tlayer, slayer = {
|
||||
'silk': ('silk', 'mask'),
|
||||
'mask': ('mask', 'silk'),
|
||||
'copper': ('copper', None)
|
||||
}[layer]
|
||||
|
||||
layers = LAYER_SPEC[side]
|
||||
tname, tgrb = find_gerber_in_dir(source, layers[tlayer])
|
||||
print('Target layer file {}'.format(os.path.basename(tname)))
|
||||
oname, ogrb = find_gerber_in_dir(source, layers['outline'])
|
||||
print('Outline layer file {}'.format(os.path.basename(oname)))
|
||||
subtract = find_gerber_in_dir(source, layers[slayer]) if slayer else None
|
||||
|
||||
# Prepare output. Do this now to error out as early as possible if there's a problem.
|
||||
if os.path.exists(target):
|
||||
if os.path.isdir(target) and sorted(os.listdir(target)) == sorted(os.listdir(source)):
|
||||
shutil.rmtree(target)
|
||||
else:
|
||||
print('Error: Target already exists and does not look like source. Please manually remove the target dir before proceeding.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Generate output
|
||||
out = paste_image(tgrb, ogrb, source_img, [subtract], debugdir=debugdir, status_print=lambda *args: print(*args, flush=True))
|
||||
|
||||
shutil.copytree(source, target)
|
||||
with open(os.path.join(target, os.path.basename(tname)), 'w') as f:
|
||||
f.write(out)
|
||||
|
||||
def render_preview(source, image, side, process_resolution, resolution_oversampling):
|
||||
def load_layer(layer):
|
||||
name, grb = find_gerber_in_dir(source, LAYER_SPEC[side][layer])
|
||||
print(f'{layer} layer file {os.path.basename(name)}')
|
||||
return grb
|
||||
|
||||
outline = load_layer('outline')
|
||||
silk = load_layer('silk')
|
||||
mask = load_layer('mask')
|
||||
copper = load_layer('copper')
|
||||
|
||||
try:
|
||||
nm, npth = find_gerber_in_dir(source, '-npth.drl')
|
||||
print(f'npth drill file {nm}')
|
||||
except ValueError:
|
||||
npth = None
|
||||
nm, drill = find_gerber_in_dir(source, '.drl|.txt', exclude='-npth.drl')
|
||||
print(f'drill file {nm}')
|
||||
drill = ([npth] if npth else []) + [drill]
|
||||
|
||||
generate_template(
|
||||
silk, mask, copper, outline, drill,
|
||||
image,
|
||||
process_resolution=process_resolution,
|
||||
resolution_oversampling=resolution_oversampling,
|
||||
)
|
||||
|
||||
594
gerbolyze/__init__.py
Executable file
|
|
@ -0,0 +1,594 @@
|
|||
import tempfile
|
||||
import logging
|
||||
import os.path as path
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
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 pathlib import Path
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import numpy as np
|
||||
import click
|
||||
|
||||
import gerbonara as gn
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
pass
|
||||
|
||||
@cli.command()
|
||||
@click.argument('input_gerbers', type=click.Path(exists=True, path_type=Path))
|
||||
@click.argument('input_svg', type=click.Path(exists=True, dir_okay=False, file_okay=True, allow_dash=True, path_type=Path))
|
||||
@click.argument('output_gerbers', type=click.Path(allow_dash=True, path_type=Path))
|
||||
@click.option('--dilate', default=0.1, type=float, help='Default dilation for subtraction operations in mm')
|
||||
@click.option('--zip/--no-zip', 'is_zip', default=None, help='zip output files. Default: zip if output path ends with ".zip" or when outputting to stdout.')
|
||||
@click.option('--curve-tolerance', type=float, help='Tolerance for curve flattening in mm')
|
||||
@click.option('--no-subtract', 'no_subtract', flag_value=True, help='Disable subtraction')
|
||||
@click.option('--subtract', help='Use user subtraction script from argument')
|
||||
@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('--excellon-conversion-errors', type=click.Choice(['raise', 'warn', 'ignore']), default='raise', help='Method of error handling during SVG to Excellon conversion')
|
||||
@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')
|
||||
@click.option('--circle-test-tolerance', help='passed through to svg-flatten')
|
||||
@click.option('--log-level', default='info', type=click.Choice(['debug', 'info', 'warning', 'error', 'critical']), help='log level')
|
||||
def paste(input_gerbers, input_svg, output_gerbers, is_zip,
|
||||
dilate, curve_tolerance, no_subtract, subtract,
|
||||
preserve_aspect_ratio, circle_test_tolerance,
|
||||
trace_space, vectorizer, vectorizer_map, exclude_groups,
|
||||
excellon_conversion_errors, log_level):
|
||||
""" Render vector data and raster images from SVG file into gerbers. """
|
||||
|
||||
logging.basicConfig(level=getattr(logging, log_level.upper()))
|
||||
|
||||
subtract_map = parse_subtract_script('' if no_subtract else subtract, dilate)
|
||||
|
||||
stack = gn.LayerStack.open(input_gerbers, lazy=True)
|
||||
(bb_min_x, bb_min_y), (bb_max_x, bb_max_y) = bounds = stack.board_bounds()
|
||||
|
||||
output_is_zip = output_gerbers.name.lower().endswith('.zip') if is_zip is None else is_zip
|
||||
|
||||
# Create output dir if it does not exist yet. Do this now so we fail early
|
||||
if not output_is_zip:
|
||||
output_gerbers.mkdir(exist_ok=True)
|
||||
|
||||
@functools.lru_cache()
|
||||
def do_dilate(layer, amount):
|
||||
return dilate_gerber(layer, bounds, amount, curve_tolerance)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as processed_svg:
|
||||
run_cargo_command('usvg', *shlex.split(os.environ.get('USVG_OPTIONS', '')), input_svg, processed_svg.name)
|
||||
|
||||
with open(processed_svg.name) as f:
|
||||
soup = BeautifulSoup(f.read(), features='xml')
|
||||
|
||||
for (side, use), layer in [
|
||||
*stack.graphic_layers.items(),
|
||||
(('drill', 'plated'), stack.drill_pth),
|
||||
(('drill', 'nonplated'), stack.drill_npth)]:
|
||||
logging.info(f'Layer {side} {use}')
|
||||
if (soup_layer := soup.find(id=f'g-{side}-{use}')):
|
||||
if not soup_layer.contents:
|
||||
logging.info(f' Corresponding overlay layer is empty. Skipping.')
|
||||
else:
|
||||
logging.info(f' Corresponding overlay layer not found. Skipping.')
|
||||
continue
|
||||
|
||||
if layer is None:
|
||||
loggin.error(f' Corresponding overlay layer is non-empty, but the corresponding layer could not be found in the input gerbers. Skipping.')
|
||||
continue
|
||||
|
||||
# only open lazily loaded layer if we need it. Replace lazy wrapper in stack with loaded layer.
|
||||
layer = layer.instance
|
||||
logging.info(f' Loaded layer: {layer}')
|
||||
|
||||
overlay_grb = svg_to_gerber(processed_svg.name, no_usvg=True,
|
||||
trace_space=trace_space, vectorizer=vectorizer, vectorizer_map=vectorizer_map,
|
||||
exclude_groups=exclude_groups, curve_tolerance=curve_tolerance,
|
||||
preserve_aspect_ratio=preserve_aspect_ratio, circle_test_tolerance=circle_test_tolerance,
|
||||
outline_mode=(use == 'outline' or side == 'drill'),
|
||||
only_groups=f'g-{side}-{use}')
|
||||
|
||||
logging.info(f' Converted overlay: {overlay_grb}')
|
||||
|
||||
# move overlay from svg origin to gerber origin
|
||||
overlay_grb.offset(bb_min_x, bb_min_y)
|
||||
|
||||
# dilated subtract layers on top of overlay
|
||||
if side in ('top', 'bottom'): # do not process subtraction scripts for inner layers, outline, and drill files
|
||||
dilations = subtract_map.get(use, [])
|
||||
for d_layer, amount in dilations:
|
||||
dilated = do_dilate(stack[(side, d_layer)], amount)
|
||||
layer.merge(dilated, mode='below', keep_settings=True)
|
||||
|
||||
if side == 'drill':
|
||||
try:
|
||||
overlay_grb = overlay_grb.to_excellon(plated=layer.is_plated_tristate,
|
||||
errors=excellon_conversion_errors)
|
||||
except ValueError as e:
|
||||
raise click.ClickException(f'Some objects on the {use} drill layer could not be converted from SVG to Excellon. This may be because they are not sufficiently circular to be matched. You can either increase the --circle-test-tolerance parameter from its default value of 0.1, or you can convert this error into a warning by passing --excellon-conversion-errors "warn" or "ignore".') from e
|
||||
|
||||
# overlay on bottom
|
||||
layer.merge(overlay_grb, mode='below', keep_settings=True)
|
||||
logging.info(f' Merged layer and overlay: {layer}')
|
||||
|
||||
if output_is_zip:
|
||||
stack.save_to_zipfile(output_gerbers)
|
||||
else:
|
||||
stack.save_to_directory(output_gerbers)
|
||||
|
||||
@cli.command()
|
||||
@click.argument('input_gerbers', type=click.Path(exists=True))
|
||||
@click.argument('output_svg', required=False)
|
||||
@click.option('-t' ,'--top', help='Render board top side.', is_flag=True)
|
||||
@click.option('-b' ,'--bottom', help='Render board bottom side.', is_flag=True)
|
||||
@click.option('-f' ,'--force', help='Overwrite existing output file when autogenerating file name.', is_flag=True)
|
||||
@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')
|
||||
def template(input_gerbers, output_svg, top, bottom, force, 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
|
||||
'''
|
||||
source = Path(input_gerbers)
|
||||
ttype = 'top' if top else 'bottom'
|
||||
|
||||
if (bool(top) + bool(bottom)) != 1:
|
||||
raise click.UsageError('Excactly one of --top or --bottom must be given.')
|
||||
|
||||
if output_svg is None:
|
||||
# autogenerate output file name if none is 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
|
||||
|
||||
output_svg = source.parent / f'{source.name}.template-{ttype}.svg'
|
||||
click.echo(f'Writing output to {output_svg}')
|
||||
|
||||
if output_svg.exists() and not force:
|
||||
raise UsageError(f'Autogenerated output file already exists. Please remote first, or use --force, or '
|
||||
'explicitly give an output path.')
|
||||
|
||||
else:
|
||||
output_svg = Path(output_svg)
|
||||
|
||||
stack = gn.LayerStack.open(source, lazy=True)
|
||||
svg = stack.to_pretty_svg(side=('top' if top else 'bottom'), inkscape=True)
|
||||
|
||||
template_layers = [f'{ttype}-copper', f'{ttype}-mask', f'{ttype}-silk', f'{ttype}-paste',
|
||||
'mechanical outline', 'drill plated', 'drill nonplated']
|
||||
silk = template_layers[-2]
|
||||
|
||||
if vector:
|
||||
output_svg.write_text(create_template_from_svg(svg, template_layers, current_layer=silk))
|
||||
|
||||
else:
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as temp_svg, \
|
||||
tempfile.NamedTemporaryFile(suffix='.png') as temp_png:
|
||||
Path(temp_svg.name).write_text(str(svg))
|
||||
run_cargo_command('resvg', temp_svg.name, temp_png.name, dpi=f'{raster_dpi:.0f}')
|
||||
output_svg.write_text(template_svg_for_png(stack.board_bounds(), Path(temp_png.name).read_bytes(),
|
||||
template_layers, current_layer=silk))
|
||||
|
||||
|
||||
class ClickSizeParam(click.ParamType):
|
||||
name = 'Size'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if isinstance(value, tuple):
|
||||
return value
|
||||
|
||||
if not (m := re.match(r'([0-9]+\.?[0-9]*)(mm|cm|in)?[xX*/,×]([0-9]+\.?[0-9]*)(mm|cm|in)?', value)):
|
||||
self.fail('Size must have format [width]x[height][unit]. The unit can be mm, cm or in. The unit is optional and defaults to mm.', param=param, ctx=ctx)
|
||||
|
||||
w, unit1, h, unit2 = m.groups()
|
||||
if unit1 and unit2 and unit1 != unit2:
|
||||
self.fail('Width and height must use the same unit. Two different units given for width and height: width is in {unit1}, and height is in {unit2}.', param=param, ctx=ctx)
|
||||
|
||||
unit = (unit1 or unit2) or 'mm'
|
||||
return float(w), float(h), unit
|
||||
|
||||
@cli.command()
|
||||
@click.argument('output_svg', type=click.Path(dir_okay=False, writable=True, allow_dash=True))
|
||||
@click.option('--size', type=ClickSizeParam(), default='100x100mm', help='PCB size in [width]x[height][unit] format. Units can be cm, mm or in, when no unit is given, defaults to mm. When no size is given, defaults to 100x100mm.')
|
||||
@click.option('--force', is_flag=True, help='Overwrite output file without asking if it exists.')
|
||||
@click.option('-n', '--copper-layers', default=2, type=int, help='Number of copper layers to generate.')
|
||||
@click.option('--no-default-layers', is_flag=True, help='Do not generate default layers.')
|
||||
@click.option('-l', '--layer', multiple=True, help='Add given layer to the top of the output layer stack. Can be given multiple times.')
|
||||
def empty_template(output_svg, size, force, copper_layers, no_default_layers, layer):
|
||||
if output_svg == '-':
|
||||
out = sys.stdout
|
||||
else:
|
||||
out = Path(output_svg)
|
||||
if out.exists():
|
||||
if not force and not click.confirm(f'Output file "{out}" already exists. Do you want to overwrite it?'):
|
||||
raise click.ClickException(f'Output file "{out}" already exists, exiting.')
|
||||
out = out.open('w')
|
||||
|
||||
layers = layer or []
|
||||
current_layer = None
|
||||
if not no_default_layers:
|
||||
layers += ['top paste', 'top silk', 'top mask']
|
||||
|
||||
if copper_layers > 0:
|
||||
current_layer = 'top copper'
|
||||
inner = [ 'inner{i} copper' for i in range(max(0, copper_layers-2)) ]
|
||||
layers += ['top copper', *inner, 'bottom copper'][:copper_layers]
|
||||
|
||||
layers += ['bottom mask', 'bottom silk', 'bottom paste']
|
||||
layers += ['mechanical outline', 'drill plated', 'drill nonplated', 'other comments']
|
||||
if layers and current_layer is None:
|
||||
current_layer = layers[0]
|
||||
|
||||
out.write(empty_pcb_template(size, layers, current_layer))
|
||||
out.flush()
|
||||
if output_svg != '-':
|
||||
out.close()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('input_svg', type=click.Path(exists=True, path_type=Path))
|
||||
@click.argument('output_gerbers', type=click.Path(path_type=Path))
|
||||
@click.option('-n', '--naming-scheme', default='kicad', type=click.Choice(['kicad', 'altium']), help='Naming scheme for gerber output file names.')
|
||||
@click.option('--zip/--no-zip', 'is_zip', default=None, help='zip output files. Default: zip if output path ends with ".zip" or when outputting to stdout.')
|
||||
@click.option('--composite-drill-file/--separate-drill-file', 'composite_drill', help='Use Altium composite Excellon drill file format (default)')
|
||||
@click.option('--dilate', default=0.1, type=float, help='Default dilation for subtraction operations in mm')
|
||||
@click.option('--curve-tolerance', type=float, help='Tolerance for curve flattening in mm')
|
||||
@click.option('--subtract', help='Use user subtraction script from argument (default for "convert": none)')
|
||||
@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('--exclude-groups', help='passed through to svg-flatten')
|
||||
@click.option('--circle-test-tolerance', help='passed through to svg-flatten')
|
||||
@click.option('--pattern-complete-tiles-only', is_flag=True, help='passed through to svg-flatten')
|
||||
@click.option('--use-apertures-for-patterns', is_flag=True, help='passed through to svg-flatten')
|
||||
@click.option('--log-level', default='info', type=click.Choice(['debug', 'info', 'warning', 'error', 'critical']), help='log level')
|
||||
def convert(input_svg, output_gerbers, is_zip, dilate, curve_tolerance, subtract, trace_space, vectorizer,
|
||||
vectorizer_map, exclude_groups, composite_drill, naming_scheme, circle_test_tolerance,
|
||||
pattern_complete_tiles_only, use_apertures_for_patterns, log_level):
|
||||
''' Convert SVG file directly to gerbers.
|
||||
|
||||
Unlike `gerbolyze paste`, this does not add the SVG's contents to existing gerbers. It allows you to directly create
|
||||
PCBs using Inkscape similar to PCBModE.
|
||||
'''
|
||||
logging.basicConfig(level=getattr(logging, log_level.upper()))
|
||||
|
||||
subtract_map = parse_subtract_script(subtract, dilate, default_script='')
|
||||
output_is_zip = output_gerbers.name.lower().endswith('.zip') if is_zip is None else is_zip
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as processed_svg:
|
||||
run_cargo_command('usvg', *shlex.split(os.environ.get('USVG_OPTIONS', '')), input_svg, processed_svg.name)
|
||||
|
||||
soup = BeautifulSoup(input_svg.read_text(), features='xml')
|
||||
layers = {e.get('id'): e.get('inkscape:label') for e in soup.find_all('g', recursive=True)}
|
||||
|
||||
stack = gn.LayerStack({}, None, None, [], board_name=input_svg.stem, original_path=input_svg)
|
||||
|
||||
for group_id, label in layers.items():
|
||||
label = label or ''
|
||||
if not group_id or 'no export' in label:
|
||||
continue
|
||||
|
||||
if not group_id.startswith('g-'):
|
||||
continue
|
||||
group_id = group_id[2:]
|
||||
|
||||
if group_id == 'outline':
|
||||
side, use = 'mechanical', 'outline'
|
||||
elif group_id == 'comments':
|
||||
side, use = 'other', 'comments'
|
||||
elif len(group_id.split('-')) != 2:
|
||||
warnings.warn(f'Unknown layer {group_id}')
|
||||
continue
|
||||
else:
|
||||
side, use = group_id.split('-')
|
||||
|
||||
grb = svg_to_gerber(processed_svg.name, no_usvg=True,
|
||||
trace_space=trace_space, vectorizer=vectorizer, vectorizer_map=vectorizer_map,
|
||||
exclude_groups=exclude_groups, curve_tolerance=curve_tolerance, only_groups=f'g-{group_id}',
|
||||
circle_test_tolerance=circle_test_tolerance, pattern_complete_tiles_only=pattern_complete_tiles_only,
|
||||
use_apertures_for_patterns=(use_apertures_for_patterns and use not in ('outline', 'drill')),
|
||||
outline_mode=(use == 'outline' or side == 'drill'))
|
||||
grb.original_path = Path()
|
||||
|
||||
if side == 'drill':
|
||||
if use == 'plated':
|
||||
stack.drill_pth = grb.to_excellon(plated=True)
|
||||
elif use == 'nonplated':
|
||||
stack.drill_npth = grb.to_excellon(plated=False)
|
||||
else:
|
||||
warnings.warn(f'Invalid drill layer type "{side}". Must be one of "plated" or "nonplated"')
|
||||
|
||||
else:
|
||||
stack.graphic_layers[(side, use)] = grb
|
||||
|
||||
bounds = stack.board_bounds()
|
||||
@functools.lru_cache()
|
||||
def do_dilate(layer, amount):
|
||||
return dilate_gerber(layer, bounds, amount, curve_tolerance)
|
||||
|
||||
for (side, use), layer in stack.graphic_layers.items():
|
||||
# dilated subtract layers on top of overlay
|
||||
if side in ('top', 'bottom'): # do not process subtraction scripts for inner layers
|
||||
dilations = subtract_map.get(use, [])
|
||||
for d_layer, amount in dilations:
|
||||
d_layer = stack.graphic_layers[(side, d_layer)]
|
||||
dilated = do_dilate(d_layer, amount)
|
||||
layer.merge(dilated, mode='above', keep_settings=True)
|
||||
|
||||
if composite_drill:
|
||||
logging.info('Merging drill layers...')
|
||||
stack.merge_drill_layers()
|
||||
|
||||
naming_scheme = getattr(gn.layers.NamingScheme, naming_scheme)
|
||||
if output_is_zip:
|
||||
stack.save_to_zipfile(output_gerbers, naming_scheme=naming_scheme)
|
||||
else:
|
||||
stack.save_to_directory(output_gerbers, naming_scheme=naming_scheme)
|
||||
|
||||
|
||||
# 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
|
||||
'''
|
||||
|
||||
DEFAULT_CONVERT_SUB_SCRIPT = '''
|
||||
out.silk -= in.mask
|
||||
'''
|
||||
|
||||
def parse_subtract_script(script, default_dilation=0.1, default_script=DEFAULT_SUB_SCRIPT):
|
||||
if script is None:
|
||||
script = default_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(r'\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
|
||||
|
||||
# Utility foo
|
||||
# ===========
|
||||
|
||||
def run_cargo_command(binary, *args, **kwargs):
|
||||
cmd_args = []
|
||||
for key, value in kwargs.items():
|
||||
if value is not None:
|
||||
if value is False:
|
||||
continue
|
||||
|
||||
cmd_args.append(f'--{key.replace("_", "-")}')
|
||||
|
||||
if value is not True:
|
||||
cmd_args.append(value)
|
||||
cmd_args.extend(map(str, args))
|
||||
|
||||
# By default, try a number of options:
|
||||
candidates = [
|
||||
# somewhere in $PATH
|
||||
binary,
|
||||
# wasi-wrapper in $PATH
|
||||
f'wasi-{binary}',
|
||||
# in user-local cargo installation
|
||||
Path.home() / '.cargo' / 'bin' / binary,
|
||||
# wasi-wrapper in user-local pip installation
|
||||
Path.home() / '.local' / 'bin' / f'wasi-{binary}',
|
||||
# next to our current python interpreter (e.g. in virtualenv)
|
||||
str(Path(sys.executable).parent / f'wasi-{binary}')
|
||||
]
|
||||
|
||||
# if envvar is set, try that first.
|
||||
if (env_var := os.environ.get(binary.upper())):
|
||||
candidates = [env_var, *candidates]
|
||||
|
||||
for cand in candidates:
|
||||
try:
|
||||
logging.debug(f'trying {binary}: {cand}')
|
||||
logging.debug(f'with args: {" ".join(cmd_args)}')
|
||||
res = subprocess.run([cand, *cmd_args], check=True)
|
||||
break
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
else:
|
||||
raise SystemError(f'{binary} executable not found')
|
||||
|
||||
|
||||
|
||||
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))
|
||||
|
||||
# SVG export
|
||||
#===========
|
||||
|
||||
def template_layer(name):
|
||||
return f'<g id="g-{name.lower().replace(" ", "-")}" inkscape:label="{name}" inkscape:groupmode="layer"></g>'
|
||||
|
||||
def template_svg_for_png(bounds, png_data, extra_layers, current_layer):
|
||||
(x1, y1), (x2, 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/>
|
||||
<sodipodi:namedview inkscape:current-layer="g-{current_layer.lower().replace(" ", "-")}" />
|
||||
<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)
|
||||
|
||||
def empty_pcb_template(size, extra_layers, current_layer):
|
||||
w, h, unit = size
|
||||
|
||||
extra_layers = "\n ".join(template_layer(name) for name in extra_layers)
|
||||
current_layer = f'<sodipodi:namedview inkscape:current-layer="g-{current_layer.lower().replace(" ", "-")}" />' if current_layer else ''
|
||||
|
||||
# we set up the viewport such that document dimensions = document units = [unit]
|
||||
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}{unit}" height="{h}{unit}" viewBox="0 0 {w} {h}" >
|
||||
<defs/>
|
||||
{current_layer}
|
||||
{extra_layers}
|
||||
</svg>
|
||||
'''
|
||||
return textwrap.dedent(template)
|
||||
|
||||
|
||||
MM_PER_INCH = 25.4
|
||||
|
||||
def create_template_from_svg(svg, extra_layers, current_layer):
|
||||
view, *layers = svg.children
|
||||
view.attrs['inkscape__current_layer'] = f'g-{current_layer.lower().replace(" ", "-")}'
|
||||
|
||||
extra_layers = [ template_layer(name) for name in extra_layers ]
|
||||
svg.children = [ view, *extra_layers, gn.utils.Tag('g', layers, inkscape__label='Preview', sodipodi__insensitive='true',
|
||||
inkscape__groupmode='layer', style='opacity:0.5') ]
|
||||
|
||||
return str(svg)
|
||||
|
||||
# SVG/gerber import
|
||||
#==================
|
||||
|
||||
def dilate_gerber(layer, bounds, dilation, curve_tolerance):
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as temp_in_svg,\
|
||||
tempfile.NamedTemporaryFile(suffix='.svg') as temp_out_svg:
|
||||
Path(temp_in_svg.name).write_text(str(layer.instance.to_svg(force_bounds=bounds, fg='white')))
|
||||
run_cargo_command('usvg', temp_in_svg.name, temp_out_svg.name)
|
||||
|
||||
# dilate & render back to gerber
|
||||
# NOTE: Maybe reconsider or nicely document dilation semantics ; It is weird that negative dilations affect
|
||||
# clear color and positive affects dark colors
|
||||
out = svg_to_gerber(temp_out_svg.name, no_usvg=True, dilate=-dilation, curve_tolerance=curve_tolerance)
|
||||
return out
|
||||
|
||||
def svg_to_gerber(infile, outline_mode=False, **kwargs):
|
||||
infile = Path(infile)
|
||||
|
||||
args = [ '--format', ('gerber-outline' if outline_mode else 'gerber'),
|
||||
'--precision', '6', # intermediate file, use higher than necessary precision
|
||||
]
|
||||
|
||||
for k, v in kwargs.items():
|
||||
if v:
|
||||
args.append('--' + k.replace('_', '-'))
|
||||
if not isinstance(v, bool):
|
||||
args.append(str(v))
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.gbr') as temp_gbr:
|
||||
args += [str(infile), str(temp_gbr.name)]
|
||||
|
||||
logging.debug(f'svg-flatten args: {" ".join(args)}')
|
||||
|
||||
if 'SVG_FLATTEN' in os.environ:
|
||||
logging.debug('using svg-flatten at $SVG_FLATTEN')
|
||||
subprocess.run([os.environ['SVG_FLATTEN'], *args], check=True)
|
||||
|
||||
else:
|
||||
# By default, try four options:
|
||||
for candidate in [
|
||||
# somewhere in $PATH
|
||||
'svg-flatten',
|
||||
None, # direct WASI import
|
||||
'wasi-svg-flatten',
|
||||
|
||||
# in user-local pip installation
|
||||
Path.home() / '.local' / 'bin' / 'svg-flatten',
|
||||
Path.home() / '.local' / 'bin' / 'wasi-svg-flatten',
|
||||
|
||||
# next to our current python interpreter (e.g. in virtualenv)
|
||||
str(Path(sys.executable).parent / 'svg-flatten'),
|
||||
str(Path(sys.executable).parent / 'wasi-svg-flatten'),
|
||||
|
||||
# next to this python source file in the development repo
|
||||
str(Path(__file__).parent.parent / 'svg-flatten' / 'build' / 'svg-flatten') ]:
|
||||
|
||||
try:
|
||||
if candidate is None:
|
||||
import svg_flatten_wasi
|
||||
svg_flatten_wasi.run_svg_flatten.callback(args[-2], args[-1], args[:-2], no_usvg=False)
|
||||
logging.debug('using svg_flatten_wasi python package')
|
||||
|
||||
else:
|
||||
subprocess.run([candidate, *args], check=True)
|
||||
logging.debug('using svg-flatten at', candidate)
|
||||
|
||||
break
|
||||
except (FileNotFoundError, ModuleNotFoundError):
|
||||
continue
|
||||
|
||||
else:
|
||||
raise SystemError('svg-flatten executable not found')
|
||||
|
||||
return gn.rs274x.GerberFile.open(temp_gbr.name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
2
gerbolyze/__main__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import gerbolyze
|
||||
gerbolyze.cli()
|
||||
636
gerbolyze/protoboard.py
Normal file
|
|
@ -0,0 +1,636 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import textwrap
|
||||
import ast
|
||||
import uuid
|
||||
|
||||
svg_str = lambda content: content if isinstance(content, str) else '\n'.join(str(c) for c in content)
|
||||
|
||||
class Pattern:
|
||||
def __init__(self, w, h=None):
|
||||
self.vb_w = self.w = w
|
||||
self.vb_h = self.h = h or w
|
||||
|
||||
def svg_def(self, svg_id, off_x, off_y):
|
||||
return textwrap.dedent(f'''
|
||||
<pattern id="{svg_id}" x="{off_x}" y="{off_y}" viewBox="0,0,{self.vb_w},{self.vb_h}" width="{self.w}" height="{self.h}" patternUnits="userSpaceOnUse">
|
||||
{svg_str(self.content)}
|
||||
</pattern>''')
|
||||
|
||||
def make_rect(svg_id, x, y, w, h, clip=''):
|
||||
#import random
|
||||
#c = random.randint(0, 2**24)
|
||||
#return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="#{c:06x}"/>'
|
||||
return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" {clip} fill="url(#{svg_id})"/>'
|
||||
|
||||
class CirclePattern(Pattern):
|
||||
def __init__(self, d, w, h=None):
|
||||
super().__init__(w, h)
|
||||
self.d = d
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
return f'<circle cx="{self.w/2}" cy="{self.h/2}" r="{self.d/2}"/>'
|
||||
|
||||
class RectPattern(Pattern):
|
||||
def __init__(self, rw, rh, w, h):
|
||||
super().__init__(w, h)
|
||||
self.rw, self.rh = rw, rh
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
x = (self.w - self.rw) / 2
|
||||
y = (self.h - self.rh) / 2
|
||||
return f'<rect x="{x}" y="{y}" width="{self.rw}" height="{self.rh}"/>'
|
||||
|
||||
class ManhattanPattern(Pattern):
|
||||
def __init__(self, pitch=2.54*4, gap=0.2):
|
||||
super().__init__(pitch)
|
||||
self.vb_w, self.vb_h = 1, 1
|
||||
self.gap = gap
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
return textwrap.dedent('''
|
||||
<rect x="0" y="0" width="0.5" height="0.5" style="fill: black; stroke: white; stroke-width: 0.01mm"/>
|
||||
<rect x="0" y="0.5" width="0.5" height="0.5" style="fill: black; stroke: white; stroke-width: 0.01mm"/>
|
||||
<rect x="0.5" y="0" width="0.5" height="0.5" style="fill: black; stroke: white; stroke-width: 0.01mm"/>
|
||||
<rect x="0.5" y="0.5" width="0.5" height="0.5" style="fill: black; stroke: white; stroke-width: 0.01mm"/>
|
||||
<rect x="0.3" y="0.3" width="0.4" height="0.4" style="fill: black; stroke: white; stroke-width: 0.01mm" transform="rotate(45 0.5 0.5)"/>
|
||||
'''.strip())
|
||||
|
||||
make_layer = lambda layer_name, content: \
|
||||
f'<g id="g-{layer_name.replace(" ", "-")}" inkscape:label="{layer_name}" inkscape:groupmode="layer">{svg_str(content)}</g>'
|
||||
|
||||
svg_template = textwrap.dedent('''
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg version="1.1" width="{w}mm" height="{h}mm" viewBox="0 0 {w} {h}" id="svg18" sodipodi:docname="proto.svg"
|
||||
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs id="defs2">
|
||||
{defs}
|
||||
</defs>
|
||||
<sodipodi:namedview inkscape:current-layer="g-top-copper" id="namedview4" pagecolor="#ffffff" bordercolor="#666666"
|
||||
borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" inkscape:zoom="2.8291492"
|
||||
inkscape:cx="157.29111" inkscape:cy="80.943063" inkscape:window-width="1920" inkscape:window-height="1011"
|
||||
inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" />
|
||||
{layers}
|
||||
</svg>
|
||||
''').strip()
|
||||
|
||||
class PatternProtoArea:
|
||||
def __init__(self, pitch_x, pitch_y=None, border=None):
|
||||
self.pitch_x = pitch_x
|
||||
self.pitch_y = pitch_y or pitch_x
|
||||
|
||||
if border is None:
|
||||
self.border = (0, 0, 0, 0)
|
||||
elif hasattr(border, '__iter__'):
|
||||
if len(border == 4):
|
||||
self.border = border
|
||||
else:
|
||||
raise TypeError('border must be None, int, or a 4-tuple of floats (top, right, bottom, left)')
|
||||
else:
|
||||
self.border = (border, border, border, border)
|
||||
|
||||
@property
|
||||
def pitch(self):
|
||||
if self.pitch_x != self.pitch_y:
|
||||
raise ValueError('Pattern has different X and Y pitches')
|
||||
return self.pitch_x
|
||||
|
||||
def fit_size(self, w, h):
|
||||
x, y, w, h = self.fit_rect(0, 0, w, h, False)
|
||||
t, r, b, l = self.border
|
||||
return (w+l+r), (h+t+b)
|
||||
|
||||
def fit_rect(self, x, y, w, h, center=True):
|
||||
t, r, b, l = self.border
|
||||
x, y, w, h = (x+l), (y+t), (w-l-r), (h-t-b)
|
||||
|
||||
w_mod, h_mod = round((w + 5e-7) % self.pitch_x, 6), round((h + 5e-7) % self.pitch_y, 6)
|
||||
w_fit, h_fit = round(w - w_mod, 6), round(h - h_mod, 6)
|
||||
|
||||
if center:
|
||||
x = x + (w-w_fit)/2
|
||||
y = y + (h-h_fit)/2
|
||||
return x, y, w_fit, h_fit
|
||||
|
||||
else:
|
||||
return x, y, w_fit, h_fit
|
||||
|
||||
def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
|
||||
yield {}
|
||||
|
||||
def symmetric_sides(self):
|
||||
return False
|
||||
|
||||
def used_patterns(self):
|
||||
yield self
|
||||
|
||||
|
||||
class EmptyProtoArea:
|
||||
def __init__(self, copper=False, border=None):
|
||||
self.copper = copper
|
||||
|
||||
if border is None:
|
||||
self.border = (0, 0, 0, 0)
|
||||
elif hasattr(border, '__iter__'):
|
||||
if len(border == 4):
|
||||
self.border = border
|
||||
else:
|
||||
raise TypeError('border must be None, int, or a 4-tuple of floats (top, right, bottom, left)')
|
||||
else:
|
||||
self.border = (border, border, border, border)
|
||||
|
||||
def fit_size(self, w, h):
|
||||
return w, h
|
||||
|
||||
def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
|
||||
if self.copper:
|
||||
t, r, b, l = self.border
|
||||
x, y, w, h = x+l, y+t, w-l-r, h-t-b
|
||||
yield { 'top copper': f'<rect x="{x}" y="{y}" width="{w}" height="{h}" {clip} fill="black"/>' }
|
||||
else:
|
||||
yield {}
|
||||
|
||||
def used_patterns(self):
|
||||
yield self
|
||||
|
||||
|
||||
class THTProtoArea(PatternProtoArea):
|
||||
def __init__(self, pad_size=2.0, drill=1.0, pitch=2.54, sides='both', plated=True, border=None, pad_shape='circle'):
|
||||
super().__init__(pitch, border=border)
|
||||
self.pad_size = pad_size
|
||||
self.pad_shape = pad_shape.lower().rstrip('s')
|
||||
self.drill = drill
|
||||
self.drill_pattern = CirclePattern(self.drill, self.pitch)
|
||||
if self.pad_shape == 'circle':
|
||||
self.pad_pattern = CirclePattern(self.pad_size, self.pitch)
|
||||
elif self.pad_shape == 'square':
|
||||
self.pad_pattern = RectPattern(self.pad_size, self.pad_size, self.pitch, self.pitch)
|
||||
self.patterns = [self.drill_pattern, self.pad_pattern]
|
||||
self.plated = plated
|
||||
self.sides = sides
|
||||
|
||||
def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
|
||||
x, y, w, h = self.fit_rect(x, y, w, h, center)
|
||||
drill = 'plated drill' if self.plated else 'nonplated drill'
|
||||
|
||||
pad_id = str(uuid.uuid4())
|
||||
drill_id = str(uuid.uuid4())
|
||||
|
||||
d = { drill: make_rect(drill_id, x, y, w, h, clip),
|
||||
'defs': [
|
||||
self.pad_pattern.svg_def(pad_id, x, y),
|
||||
self.drill_pattern.svg_def(drill_id, x, y)]}
|
||||
|
||||
if self.sides in ('top', 'both'):
|
||||
d['top copper'] = make_rect(pad_id, x, y, w, h, clip)
|
||||
d['top mask'] = make_rect(pad_id, x, y, w, h, clip)
|
||||
if self.sides in ('bottom', 'both'):
|
||||
d['bottom copper'] = make_rect(pad_id, x, y, w, h, clip)
|
||||
d['bottom mask'] = make_rect(pad_id, x, y, w, h, clip)
|
||||
|
||||
yield d
|
||||
|
||||
def __repr__(self):
|
||||
return f'THTPads(size={self.pad_size}, h={self.drill}, p={self.pitch}, sides={self.sides}, plated={self.plated}, pad_shape="{self.pad_shape}")'
|
||||
|
||||
def symmetric_sides(self):
|
||||
return True
|
||||
|
||||
|
||||
class SMDProtoAreaRectangles(PatternProtoArea):
|
||||
def __init__(self, pitch_x, pitch_y, w=None, h=None, border=None):
|
||||
super().__init__(pitch_x, pitch_y, border=border)
|
||||
w = w or pitch_x - 0.15
|
||||
h = h or pitch_y - 0.15
|
||||
self.w, self.h = w, h
|
||||
self.pad_pattern = RectPattern(w, h, pitch_x, pitch_y)
|
||||
self.patterns = [self.pad_pattern]
|
||||
|
||||
def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
|
||||
x, y, w, h = self.fit_rect(x, y, w, h, center)
|
||||
pad_id = str(uuid.uuid4())
|
||||
yield {'defs': [self.pad_pattern.svg_def(pad_id, x, y)],
|
||||
'top copper': make_rect(pad_id, x, y, w, h, clip),
|
||||
'top mask': make_rect(pad_id, x, y, w, h, clip)}
|
||||
|
||||
def symmetric_sides(self):
|
||||
return False
|
||||
|
||||
class ManhattanProtoArea(PatternProtoArea):
|
||||
def __init__(self, pitch=2.54*4, gap=0.25, border=None):
|
||||
super().__init__(pitch, pitch, border=border)
|
||||
self.gap = gap
|
||||
self.pad_pattern = ManhattanPattern(pitch, gap)
|
||||
self.patterns = [self.pad_pattern]
|
||||
|
||||
def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
|
||||
x, y, w, h = self.fit_rect(x, y, w, h, center)
|
||||
pad_id = str(uuid.uuid4())
|
||||
yield {'defs': [self.pad_pattern.svg_def(pad_id, x, y)],
|
||||
'top copper': make_rect(pad_id, x, y, w, h, clip),
|
||||
'top mask': make_rect(pad_id, x, y, w, h, clip)}
|
||||
|
||||
def symmetric_sides(self):
|
||||
return False
|
||||
|
||||
LAYERS = [
|
||||
'top paste',
|
||||
'top silk',
|
||||
'top mask',
|
||||
'top copper',
|
||||
'bottom copper',
|
||||
'bottom mask',
|
||||
'bottom silk',
|
||||
'bottom paste',
|
||||
'outline',
|
||||
'nonplated drill',
|
||||
'plated drill'
|
||||
]
|
||||
|
||||
class ProtoBoard:
|
||||
def __init__(self, defs, expr, mounting_holes=None, border=None, center=True, tight_layout=False):
|
||||
self.defs = eval_defs(defs)
|
||||
self.layout = parse_layout(expr, self.defs)
|
||||
self.mounting_holes = mounting_holes
|
||||
self.center = center
|
||||
self.tight_layout = tight_layout
|
||||
|
||||
if border is None:
|
||||
self.border = (0, 0, 0, 0)
|
||||
elif hasattr(border, '__iter__'):
|
||||
if len(border == 4):
|
||||
self.border = border
|
||||
else:
|
||||
raise TypeError('border must be None, int, or a 4-tuple of floats (top, right, bottom, left)')
|
||||
else:
|
||||
self.border = (border, border, border, border)
|
||||
|
||||
@property
|
||||
def symmetric_sides(self):
|
||||
return self.layout.symmetric_sides()
|
||||
|
||||
@property
|
||||
def used_patterns(self):
|
||||
return set(self.layout.used_patterns())
|
||||
|
||||
def generate(self, w, h):
|
||||
out = {l: [] for l in LAYERS}
|
||||
svg_defs = []
|
||||
clip = ''
|
||||
|
||||
if self.mounting_holes:
|
||||
d, o, *k = self.mounting_holes # diameter, offset from edge, keepout to proto area
|
||||
k = k[0] if k else o
|
||||
q = o + k
|
||||
if 2*q < w:
|
||||
if 2*q < h:
|
||||
clip_d = f'M 0 {q} L {q} {q} L {q} 0 L {w-q} 0 L {w-q} {q} L {w} {q} L {w} {h-q} L {w-q} {h-q} L {w-q} {h} L {q} {h} L {q} {h-q} L 0 {h-q} Z'
|
||||
else:
|
||||
clip_d = f'M {q} 0 L {w-q} 0 L {w-q} {h} L 0 {h} Z'
|
||||
else:
|
||||
if 2*q < h:
|
||||
clip_d = f'M 0 {q} L 0 {h-q} L {w} {h-q} L {w} {q} Z'
|
||||
else:
|
||||
raise ValueError(f'Hole keepout areas are so large that no board area is left. Available size is {w}x{h} mm, keepout areas are {q}x{q} mm in all four corners.')
|
||||
|
||||
svg_defs.append(f'<clipPath id="hole-clip"><path d="{clip_d}"/></clipPath>')
|
||||
clip = 'clip-path="url(#hole-clip)"'
|
||||
|
||||
out['nonplated drill'].append([
|
||||
f'<circle cx="{o}" cy="{o}" r="{d/2}"/>',
|
||||
f'<circle cx="{w-o}" cy="{o}" r="{d/2}"/>',
|
||||
f'<circle cx="{w-o}" cy="{h-o}" r="{d/2}"/>',
|
||||
f'<circle cx="{o}" cy="{h-o}" r="{d/2}"/>' ])
|
||||
|
||||
t, r, b, l = self.border
|
||||
for layer_dict in self.layout.generate(l, t, w-l-r, h-t-b, self.center, clip, self.tight_layout):
|
||||
for l in LAYERS:
|
||||
if l in layer_dict:
|
||||
out[l].append(layer_dict[l])
|
||||
svg_defs += layer_dict.get('defs', [])
|
||||
|
||||
out['outline'] = f'<rect x="0" y="0" width="{w}" height="{h}" fill="none" stroke="black" stroke-width="0.1mm"/>'
|
||||
|
||||
layers = [ make_layer(l, out[l]) for l in LAYERS ]
|
||||
return svg_template.format(w=w, h=h, defs='\n'.join(svg_defs), layers='\n'.join(layers))
|
||||
|
||||
|
||||
def convert_to_mm(value, unit):
|
||||
unitl = unit.lower()
|
||||
if unitl == 'mm':
|
||||
return value
|
||||
elif unitl == 'cm':
|
||||
return value*10
|
||||
elif unitl == 'in':
|
||||
return value*25.4
|
||||
elif unitl == 'mil':
|
||||
return value/1000*25.4
|
||||
else:
|
||||
raise ValueError(f'Invalid unit {unit}, allowed units are mm, cm, in, and mil.')
|
||||
|
||||
value_re = re.compile('([0-9]*\.?[0-9]+)(cm|mm|in|mil|%)')
|
||||
def eval_value(value, total_length=None):
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
|
||||
m = value_re.match(value.lower())
|
||||
number, unit = m.groups()
|
||||
if unit == '%':
|
||||
if total_length is None:
|
||||
raise ValueError('Percentages are not allowed for this value')
|
||||
return total_length * float(number) / 100
|
||||
return convert_to_mm(float(number), unit)
|
||||
|
||||
class PropLayout:
|
||||
def __init__(self, content, direction, proportions):
|
||||
self.content = content
|
||||
self.direction = direction
|
||||
self.proportions = proportions
|
||||
if len(content) != len(proportions):
|
||||
raise ValueError('proportions and content must have same length')
|
||||
|
||||
def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
|
||||
for (c_x, c_y, c_w, c_h), child in self.layout_2d(x, y, w, h, tight_layout):
|
||||
yield from child.generate(c_x, c_y, c_w, c_h, center, clip, tight_layout)
|
||||
|
||||
def fit_size(self, w, h):
|
||||
widths = []
|
||||
heights = []
|
||||
for (_x, _y, w, h), child in self.layout_2d(0, 0, w, h, True):
|
||||
if not isinstance(child, EmptyProtoArea):
|
||||
widths.append(w)
|
||||
heights.append(h)
|
||||
if self.direction == 'h':
|
||||
return sum(widths), max(heights)
|
||||
else:
|
||||
return max(widths), sum(heights)
|
||||
|
||||
def layout_2d(self, x, y, w, h, tight_layout=False):
|
||||
actual_l = 0
|
||||
target_l = 0
|
||||
for l, child in zip(self.layout(w if self.direction == 'h' else h), self.content):
|
||||
this_x, this_y = x, y
|
||||
this_w, this_h = w, h
|
||||
target_l += l
|
||||
|
||||
if self.direction == 'h':
|
||||
this_w = target_l - actual_l
|
||||
else:
|
||||
this_h = target_l - actual_l
|
||||
|
||||
if tight_layout:
|
||||
this_w, this_h = child.fit_size(this_w, this_h)
|
||||
|
||||
if self.direction == 'h':
|
||||
x += this_w
|
||||
actual_l += this_w
|
||||
this_h = h
|
||||
else:
|
||||
y += this_h
|
||||
actual_l += this_h
|
||||
this_w = w
|
||||
|
||||
yield (this_x, this_y, this_w, this_h), child
|
||||
|
||||
def layout(self, length):
|
||||
out = [ eval_value(value, length) for value in self.proportions ]
|
||||
total_length = sum(value for value in out if value is not None)
|
||||
if length - total_length < -1e-6:
|
||||
raise ValueError(f'Proportions sum to {total_length} mm, which is greater than the available space of {length} mm.')
|
||||
|
||||
leftover = length - total_length
|
||||
sum_props = sum( (value or 1.0) for value in self.proportions if not isinstance(value, str) )
|
||||
return [ (leftover * (value or 1.0) / sum_props if not isinstance(value, str) else calculated)
|
||||
for value, calculated in zip(self.proportions, out) ]
|
||||
|
||||
def __str__(self):
|
||||
children = ', '.join( f'{elem}:{width}' for elem, width in zip(self.content, self.proportions))
|
||||
return f'PropLayout[{self.direction.upper()}]({children})'
|
||||
|
||||
def symmetric_sides(self):
|
||||
return all(child.symmetric_sides() for child in self.content)
|
||||
|
||||
def used_patterns(self):
|
||||
for child in self.content:
|
||||
yield from child.used_patterns()
|
||||
|
||||
|
||||
class TwoSideLayout:
|
||||
def __init__(self, top, bottom):
|
||||
self.top, self.bottom = top, bottom
|
||||
|
||||
def flip(self, defs):
|
||||
out = dict(defs)
|
||||
for layer in ('copper', 'mask', 'silk', 'paste'):
|
||||
top, bottom = f'top {layer}', f'bottom {layer}'
|
||||
tval, bval = defs.get(top), defs.get(bottom)
|
||||
|
||||
if tval:
|
||||
defs[bottom] = tval
|
||||
elif bottom in defs:
|
||||
del defs[bottom]
|
||||
|
||||
if bval:
|
||||
defs[top] = bval
|
||||
elif top in defs:
|
||||
del defs[top]
|
||||
|
||||
return defs
|
||||
|
||||
def fit_size(self, w, h):
|
||||
top, bottom = self.top, self.bottom
|
||||
w1, h1 = top.fit_size(w, h)
|
||||
w2, h2 = bottom.fit_size(w, h)
|
||||
if isinstance(top, EmptyProtoArea):
|
||||
if isinstance(bottom, EmptyProtoArea):
|
||||
return w1, h1
|
||||
return w2, h2
|
||||
if isinstance(bottom, EmptyProtoArea):
|
||||
return w1, h1
|
||||
return max(w1, w2), max(h1, h2)
|
||||
|
||||
def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
|
||||
yield from self.top.generate(x, y, w, h, center, clip, tight_layout)
|
||||
yield from map(self.flip, self.bottom.generate(x, y, w, h, center, clip, tight_layout))
|
||||
|
||||
def symmetric_sides(self):
|
||||
return self.top == self.bottom
|
||||
|
||||
def used_patterns(self):
|
||||
yield from self.top.used_patterns()
|
||||
yield from self.bottom.used_patterns()
|
||||
|
||||
|
||||
def _map_expression(node, defs):
|
||||
if isinstance(node, ast.Name):
|
||||
return defs[node.id]
|
||||
|
||||
elif isinstance(node, ast.Constant):
|
||||
return node.value
|
||||
|
||||
|
||||
elif isinstance(node, ast.BinOp) and isinstance(node.op, (ast.BitOr, ast.BitAnd, ast.Add)):
|
||||
left_prop = right_prop = None
|
||||
|
||||
left, right = node.left, node.right
|
||||
|
||||
if isinstance(left, ast.BinOp) and isinstance(left.op, ast.MatMult):
|
||||
left_prop = _map_expression(left.right, defs)
|
||||
left = left.left
|
||||
|
||||
if isinstance(right, ast.BinOp) and isinstance(right.op, ast.MatMult):
|
||||
right_prop = _map_expression(right.right, defs)
|
||||
right = right.left
|
||||
|
||||
left, right = _map_expression(left, defs), _map_expression(right, defs)
|
||||
|
||||
direction = 'h' if isinstance(node.op, ast.BitOr) else 'v'
|
||||
if isinstance(left, PropLayout) and left.direction == direction and left_prop is None:
|
||||
left.content.append(right)
|
||||
left.proportions.append(right_prop)
|
||||
return left
|
||||
|
||||
elif isinstance(right, PropLayout) and right.direction == direction and right_prop is None:
|
||||
right.content.insert(0, left)
|
||||
right.proportions.insert(0, left_prop)
|
||||
return right
|
||||
|
||||
elif isinstance(node.op, ast.Add):
|
||||
if left_prop or right_prop:
|
||||
raise SyntaxError(f'Proportions ("@") not supported for two-side layout ("+")')
|
||||
|
||||
return TwoSideLayout(left, right)
|
||||
|
||||
else:
|
||||
return PropLayout([left, right], direction, [left_prop, right_prop])
|
||||
|
||||
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.MatMult):
|
||||
raise SyntaxError(f'Unexpected width specification "{ast.unparse(node.right)}"')
|
||||
|
||||
else:
|
||||
raise SyntaxError(f'Invalid layout expression "{ast.unparse(node)}"')
|
||||
|
||||
def parse_layout(expr, defs):
|
||||
''' Example layout:
|
||||
|
||||
( tht @ 2in | smd ) @ 50% / tht
|
||||
'''
|
||||
|
||||
expr = re.sub(r'\s', '', expr)
|
||||
expr = re.sub(r'([0-9]*\.?[0-9]+)([Mm][Mm]|[Cc][Mm]|[Ii][Nn]|[Mm][Ii][Ll]|%)', r'"\1\2"', expr)
|
||||
expr = expr.replace('/', '&')
|
||||
try:
|
||||
expr = ast.parse(expr, mode='eval').body
|
||||
match expr:
|
||||
case ast.Name():
|
||||
return PropLayout([defs[expr.id]], 'h', [None])
|
||||
|
||||
case ast.BinOp(op=ast.MatMult()):
|
||||
assert isinstance(expr.right, ast.Constant)
|
||||
return PropLayout([_map_expression(expr.left, defs)], 'h', [expr.right.value])
|
||||
|
||||
case _:
|
||||
return _map_expression(expr, defs)
|
||||
except SyntaxError as e:
|
||||
raise SyntaxError('Invalid layout expression') from e
|
||||
|
||||
PROTO_AREA_TYPES = {
|
||||
'THTPads': THTProtoArea,
|
||||
'SMDPads': SMDProtoAreaRectangles,
|
||||
'Manhattan': ManhattanProtoArea,
|
||||
'Empty': EmptyProtoArea,
|
||||
}
|
||||
|
||||
def eval_defs(defs):
|
||||
defs = defs.replace('\n', ';')
|
||||
defs = re.sub(r'\s', '', defs)
|
||||
|
||||
out = {}
|
||||
for elem in defs.split(';'):
|
||||
if not elem:
|
||||
continue
|
||||
|
||||
if not (m := re.match('([a-zA-Z_][a-zA-Z0-9_]*)=([a-zA-Z_][a-zA-Z0-9_]*)\((.*)\)', elem)):
|
||||
raise SyntaxError(f'Invalid pattern definition "{elem}"')
|
||||
|
||||
key, pattern, params = m.groups()
|
||||
args, kws = [], {}
|
||||
for elem in params.split(','):
|
||||
if not elem:
|
||||
continue
|
||||
if (m := re.match('([a-zA-Z_][a-zA-Z0-9_]*)=(.*)', elem)):
|
||||
param_name, param_value = m.groups()
|
||||
kws[param_name] = ast.literal_eval(param_value)
|
||||
|
||||
else:
|
||||
args.append(ast.literal_eval(elem))
|
||||
|
||||
out[key] = PROTO_AREA_TYPES[pattern](*args, **kws)
|
||||
return out
|
||||
|
||||
COMMON_DEFS = '''
|
||||
empty = Empty(copper=False);
|
||||
ground = Empty(copper=True);
|
||||
|
||||
tht = THTPads();
|
||||
manhattan = Manhattan();
|
||||
tht50 = THTPads(pad_size=1.0, drill=0.6, pitch=1.27);
|
||||
|
||||
smd100 = SMDPads(1.27, 2.54);
|
||||
smd100r = SMDPads(2.54, 1.27);
|
||||
smd950 = SMDPads(0.95, 2.5);
|
||||
smd950r = SMDPads(2.5, 0.95);
|
||||
smd800 = SMDPads(0.80, 2.0);
|
||||
smd800r = SMDPads(2.0, 0.80);
|
||||
smd650 = SMDPads(0.65, 2.0);
|
||||
smd650r = SMDPads(2.0, 0.65);
|
||||
smd500 = SMDPads(0.5, 2.0);
|
||||
smd500r = SMDPads(2.0, 0.5);
|
||||
'''
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# import sys
|
||||
# print('===== Layout expressions =====')
|
||||
# for line in [
|
||||
# 'tht',
|
||||
# 'tht@1mm',
|
||||
# 'tht|tht',
|
||||
# 'tht@1mm|tht',
|
||||
# 'tht|tht|tht',
|
||||
# 'tht@1mm|tht@2mm|tht@3mm',
|
||||
# '(tht@1mm|tht@2mm)|tht@3mm',
|
||||
# 'tht@1mm|(tht@2mm|tht@3mm)',
|
||||
# 'tht@2|tht|tht',
|
||||
# '(tht@1mm|tht|tht@3mm) / tht',
|
||||
# ]:
|
||||
# layout = parse_layout(line)
|
||||
# print(line, '->', layout)
|
||||
# print(' ', layout.layout(100))
|
||||
# print()
|
||||
# print('===== Pattern definitions =====')
|
||||
# for line in [
|
||||
# 'tht = THTCircles()',
|
||||
# 'tht = THTCircles(10)',
|
||||
# 'tht = THTCircles(10, 20)',
|
||||
# 'tht = THTCircles(plated=False)',
|
||||
# 'tht = THTCircles(10, plated=False)',
|
||||
# ]:
|
||||
# print(line, '->', eval_defs(line))
|
||||
# print()
|
||||
# print('===== Proto board =====')
|
||||
#b = ProtoBoard('tht = THTCircles(); tht_small = THTCircles(pad_size=1.0, drill=0.6, pitch=1.27)',
|
||||
# 'tht@1in|(tht_small@2/tht@1)', mounting_holes=(3.2, 5.0, 5.0), border=2, center=False)
|
||||
#b = ProtoBoard('tht = THTCircles(); smd1 = SMDPads(2.0, 2.0); smd2 = SMDPads(0.95, 1.895); plane=Empty(copper=True)', 'tht@25mm | (smd1 + plane)', mounting_holes=(3.2, 5.0, 5.0), border=2, tight_layout=True)
|
||||
#b = ProtoBoard(COMMON_DEFS, f'((smd100 + smd100) | (smd950 + smd950) | tht50@20mm)@20mm / tht', mounting_holes=(3.2,5,5), border=1, tight_layout=True, center=True)
|
||||
b = ProtoBoard(COMMON_DEFS, f'manhattan', mounting_holes=(3.2,5,5), border=1, tight_layout=True, center=True)
|
||||
print(b.generate(80, 60))
|
||||
0
gerbolyze/tests/__init__.py
Normal file
4940
gerbolyze/tests/resources/layers-gerber/layers-B.Cu.gbr
Normal file
4242
gerbolyze/tests/resources/layers-gerber/layers-B.Mask.gbr
Normal file
4470
gerbolyze/tests/resources/layers-gerber/layers-B.Paste.gbr
Normal file
4043
gerbolyze/tests/resources/layers-gerber/layers-B.SilkS.gbr
Normal file
29
gerbolyze/tests/resources/layers-gerber/layers-Cmts.User.gbr
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
G04 Gerber file generated by Gerbonara*
|
||||
%MOMM*%
|
||||
%FSLAX46Y46*%
|
||||
%IPPOS*%
|
||||
G75
|
||||
%LPD*%
|
||||
%AMGNC*
|
||||
1,1,$1,0,0,-57.29578X$4*
|
||||
1,0,$2,0,0,0*
|
||||
21,0,$2,$3,0,0,-57.29578X$4*
|
||||
%
|
||||
%AMGNR*
|
||||
21,1,$1,$2,0,0,$5X-57.29578*
|
||||
1,0,$3,0,0,0*
|
||||
21,0,$3,$4,0,0,$5X-57.29578*
|
||||
%
|
||||
%AMGNO*
|
||||
21,1,$1,$2,0,0,$5X-57.29578*
|
||||
1,1,$2,$1/2,0,$5X-57.29578*
|
||||
1,1,$2,(0-$1)/2,0,$5X-57.29578*
|
||||
1,0,$3,0,0,0*
|
||||
21,0,$3,$4,0,0,$5X-57.29578*
|
||||
%
|
||||
%AMGNP*
|
||||
5,1,$2,0,0,$1,$3X-57.29578*
|
||||
1,0,$4,0,0,0*
|
||||
%
|
||||
%ADD10C,0.05*%
|
||||
M02*
|
||||
2710
gerbolyze/tests/resources/layers-gerber/layers-Edge.Cuts.gbr
Normal file
3683
gerbolyze/tests/resources/layers-gerber/layers-F.Cu.gbr
Normal file
2983
gerbolyze/tests/resources/layers-gerber/layers-F.Mask.gbr
Normal file
3211
gerbolyze/tests/resources/layers-gerber/layers-F.Paste.gbr
Normal file
2788
gerbolyze/tests/resources/layers-gerber/layers-F.SilkS.gbr
Normal file
154
gerbolyze/tests/resources/layers-gerber/layers-NPTH.drl
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
; XNC file generated by gerbonara
|
||||
M48
|
||||
METRIC
|
||||
T01C0000.50000
|
||||
%
|
||||
T01
|
||||
G05
|
||||
X0006.40777Y0046.75991
|
||||
X0006.40777Y0046.00921
|
||||
X0006.40777Y0045.25852
|
||||
X0006.40777Y0044.50783
|
||||
X0006.40777Y0043.75713
|
||||
X0006.40777Y0043.00644
|
||||
X0006.40777Y0042.25575
|
||||
X0007.15847Y0046.00921
|
||||
X0007.90916Y0045.25852
|
||||
X0007.90916Y0044.50783
|
||||
X0007.90916Y0043.75713
|
||||
X0008.65985Y0043.00644
|
||||
X0009.41055Y0046.75991
|
||||
X0009.41055Y0046.00921
|
||||
X0009.41055Y0045.25852
|
||||
X0009.41055Y0044.50783
|
||||
X0009.41055Y0043.75713
|
||||
X0009.41055Y0043.00644
|
||||
X0009.41055Y0042.25575
|
||||
X0011.65588Y0046.00921
|
||||
X0011.65588Y0045.25852
|
||||
X0011.65588Y0044.50783
|
||||
X0011.65588Y0043.75713
|
||||
X0011.65588Y0043.00644
|
||||
X0012.40657Y0046.75991
|
||||
X0012.40657Y0042.25575
|
||||
X0013.15726Y0046.75991
|
||||
X0013.15726Y0042.25575
|
||||
X0013.90796Y0046.75991
|
||||
X0013.90796Y0042.25575
|
||||
X0014.65865Y0046.00921
|
||||
X0014.65865Y0045.25852
|
||||
X0014.65865Y0044.50783
|
||||
X0014.65865Y0043.75713
|
||||
X0014.65865Y0043.00644
|
||||
X0016.91003Y0046.75991
|
||||
X0016.91003Y0046.00921
|
||||
X0016.91003Y0045.25852
|
||||
X0016.91003Y0044.50783
|
||||
X0016.91003Y0043.75713
|
||||
X0016.91003Y0043.00644
|
||||
X0016.91003Y0042.25575
|
||||
X0017.66073Y0046.00921
|
||||
X0018.41142Y0045.25852
|
||||
X0018.41142Y0044.50783
|
||||
X0018.41142Y0043.75713
|
||||
X0019.16211Y0043.00644
|
||||
X0019.91281Y0046.75991
|
||||
X0019.91281Y0046.00921
|
||||
X0019.91281Y0045.25852
|
||||
X0019.91281Y0044.50783
|
||||
X0019.91281Y0043.75713
|
||||
X0019.91281Y0043.00644
|
||||
X0019.91281Y0042.25575
|
||||
X0022.37703Y0046.75991
|
||||
X0022.37703Y0046.00921
|
||||
X0022.37703Y0045.25852
|
||||
X0022.37703Y0044.50783
|
||||
X0022.37703Y0043.75713
|
||||
X0022.37703Y0043.00644
|
||||
X0022.37703Y0042.25575
|
||||
X0023.12773Y0046.75991
|
||||
X0023.12772Y0044.50783
|
||||
X0023.87842Y0046.75991
|
||||
X0023.87842Y0044.50783
|
||||
X0024.62911Y0046.75991
|
||||
X0024.62911Y0044.50783
|
||||
X0025.37981Y0046.00921
|
||||
X0025.37981Y0045.25852
|
||||
X0027.62514Y0046.75991
|
||||
X0027.62514Y0046.00921
|
||||
X0027.62514Y0045.25852
|
||||
X0027.62514Y0044.50783
|
||||
X0027.62514Y0043.75713
|
||||
X0027.62514Y0043.00644
|
||||
X0027.62514Y0042.25575
|
||||
X0028.37583Y0042.25575
|
||||
X0029.12652Y0042.25575
|
||||
X0029.87721Y0042.25575
|
||||
X0030.62791Y0042.25575
|
||||
X0032.87324Y0046.00921
|
||||
X0032.87324Y0045.25852
|
||||
X0032.87324Y0044.50783
|
||||
X0032.87324Y0043.75713
|
||||
X0032.87324Y0043.00644
|
||||
X0032.87324Y0042.25575
|
||||
X0033.62393Y0046.75991
|
||||
X0033.62393Y0043.75713
|
||||
X0034.37463Y0046.75991
|
||||
X0034.37463Y0043.75713
|
||||
X0035.12532Y0046.75991
|
||||
X0035.12532Y0043.75713
|
||||
X0035.87601Y0046.00921
|
||||
X0035.87601Y0045.25852
|
||||
X0035.87601Y0044.50783
|
||||
X0035.87601Y0043.75713
|
||||
X0035.87601Y0043.00644
|
||||
X0035.87601Y0042.25575
|
||||
X0038.12134Y0046.75991
|
||||
X0038.87204Y0046.75991
|
||||
X0039.62273Y0046.75991
|
||||
X0039.62273Y0046.00921
|
||||
X0039.62273Y0045.25852
|
||||
X0039.62273Y0044.50783
|
||||
X0039.62273Y0043.75713
|
||||
X0039.62273Y0043.00644
|
||||
X0039.62273Y0042.25575
|
||||
X0040.37342Y0046.75991
|
||||
X0041.12412Y0046.75991
|
||||
X0043.36945Y0046.75991
|
||||
X0043.36945Y0046.00921
|
||||
X0043.36945Y0045.25852
|
||||
X0043.36945Y0044.50783
|
||||
X0043.36945Y0043.75713
|
||||
X0043.36945Y0043.00644
|
||||
X0043.36945Y0042.25575
|
||||
X0044.12014Y0046.75991
|
||||
X0044.12014Y0044.50783
|
||||
X0044.12014Y0042.25575
|
||||
X0044.87083Y0046.75991
|
||||
X0044.87083Y0044.50783
|
||||
X0044.87083Y0042.25575
|
||||
X0045.62153Y0046.75991
|
||||
X0045.62153Y0044.50783
|
||||
X0045.62153Y0042.25575
|
||||
X0046.37222Y0046.75991
|
||||
X0046.37222Y0044.50783
|
||||
X0046.37222Y0042.25575
|
||||
X0048.61755Y0046.75991
|
||||
X0048.61755Y0046.00921
|
||||
X0048.61755Y0045.25852
|
||||
X0048.61755Y0044.50783
|
||||
X0048.61755Y0043.75713
|
||||
X0048.61755Y0043.00644
|
||||
X0048.61755Y0042.25575
|
||||
X0049.36825Y0046.75991
|
||||
X0049.36825Y0042.25575
|
||||
X0050.11894Y0046.75991
|
||||
X0050.11894Y0042.25575
|
||||
X0050.86963Y0046.75991
|
||||
X0050.86963Y0042.25575
|
||||
X0051.62033Y0046.00921
|
||||
X0051.62033Y0045.25852
|
||||
X0051.62033Y0044.50783
|
||||
X0051.62033Y0043.75713
|
||||
X0051.62033Y0043.00644
|
||||
M30
|
||||
136
gerbolyze/tests/resources/layers-gerber/layers-PTH.drl
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
; XNC file generated by gerbonara
|
||||
M48
|
||||
METRIC
|
||||
T01C0000.70000
|
||||
%
|
||||
T01
|
||||
G05
|
||||
X0006.50749Y0058.18246
|
||||
X0007.85409Y0058.18246
|
||||
X0007.85409Y0057.13262
|
||||
X0007.85409Y0056.08279
|
||||
X0007.85409Y0055.03295
|
||||
X0007.85409Y0053.98311
|
||||
X0007.85409Y0052.93327
|
||||
X0007.85409Y0051.88343
|
||||
X0008.90393Y0058.18246
|
||||
X0008.90393Y0057.13262
|
||||
X0008.90393Y0056.08279
|
||||
X0008.90393Y0055.03295
|
||||
X0008.90393Y0053.98311
|
||||
X0008.90393Y0052.93327
|
||||
X0008.90393Y0051.88343
|
||||
X0009.95377Y0058.18246
|
||||
X0009.95377Y0057.13262
|
||||
X0009.95377Y0056.08279
|
||||
X0009.95377Y0055.03295
|
||||
X0009.95377Y0053.98311
|
||||
X0009.95377Y0052.93327
|
||||
X0009.95377Y0051.88343
|
||||
X0011.00360Y0058.18246
|
||||
X0011.00360Y0057.13262
|
||||
X0011.00360Y0056.08279
|
||||
X0011.00360Y0055.03295
|
||||
X0011.00360Y0053.98311
|
||||
X0011.00360Y0052.93327
|
||||
X0011.00360Y0051.88343
|
||||
X0012.05344Y0058.18246
|
||||
X0012.05344Y0057.13262
|
||||
X0012.05344Y0056.08279
|
||||
X0012.05344Y0055.03295
|
||||
X0012.05344Y0053.98311
|
||||
X0012.05344Y0052.93327
|
||||
X0012.05344Y0051.88343
|
||||
X0014.85905Y0058.18246
|
||||
X0014.85905Y0057.13262
|
||||
X0014.85905Y0056.08279
|
||||
X0014.85905Y0055.03295
|
||||
X0014.85905Y0053.98311
|
||||
X0014.85905Y0052.93327
|
||||
X0014.85905Y0051.88343
|
||||
X0015.90889Y0058.18246
|
||||
X0015.90889Y0055.03295
|
||||
X0016.95873Y0058.18246
|
||||
X0016.95873Y0055.03295
|
||||
X0018.00857Y0058.18246
|
||||
X0018.00857Y0055.03295
|
||||
X0019.05841Y0057.13262
|
||||
X0019.05841Y0056.08279
|
||||
X0022.19849Y0058.18246
|
||||
X0022.19849Y0057.13262
|
||||
X0022.19849Y0056.08279
|
||||
X0022.19849Y0055.03295
|
||||
X0022.19849Y0053.98311
|
||||
X0022.19849Y0052.93327
|
||||
X0022.19849Y0051.88343
|
||||
X0023.24833Y0051.88343
|
||||
X0024.29816Y0051.88343
|
||||
X0025.34800Y0051.88343
|
||||
X0026.39784Y0051.88343
|
||||
X0029.53792Y0057.13262
|
||||
X0029.53792Y0056.08279
|
||||
X0029.53792Y0055.03295
|
||||
X0029.53792Y0053.98311
|
||||
X0029.53792Y0052.93327
|
||||
X0029.53792Y0051.88343
|
||||
X0030.58776Y0058.18246
|
||||
X0030.58776Y0053.98311
|
||||
X0031.63760Y0058.18246
|
||||
X0031.63760Y0053.98311
|
||||
X0032.68744Y0058.18246
|
||||
X0032.68744Y0053.98311
|
||||
X0033.73728Y0057.13262
|
||||
X0033.73728Y0056.08279
|
||||
X0033.73728Y0055.03295
|
||||
X0033.73728Y0053.98311
|
||||
X0033.73728Y0052.93327
|
||||
X0033.73728Y0051.88343
|
||||
X0036.87735Y0058.18246
|
||||
X0037.92719Y0058.18246
|
||||
X0038.97703Y0058.18246
|
||||
X0038.97703Y0057.13262
|
||||
X0038.97703Y0056.08279
|
||||
X0038.97703Y0055.03295
|
||||
X0038.97703Y0053.98311
|
||||
X0038.97703Y0052.93327
|
||||
X0038.97703Y0051.88343
|
||||
X0040.02687Y0058.18246
|
||||
X0041.07671Y0058.18246
|
||||
X0044.21679Y0058.18246
|
||||
X0044.21679Y0057.13262
|
||||
X0044.21679Y0056.08279
|
||||
X0044.21679Y0055.03295
|
||||
X0044.21679Y0053.98311
|
||||
X0044.21679Y0052.93327
|
||||
X0044.21679Y0051.88343
|
||||
X0045.26663Y0058.18246
|
||||
X0045.26663Y0055.03295
|
||||
X0045.26663Y0051.88343
|
||||
X0046.31647Y0058.18246
|
||||
X0046.31647Y0055.03295
|
||||
X0046.31647Y0051.88343
|
||||
X0047.36631Y0058.18246
|
||||
X0047.36631Y0055.03295
|
||||
X0047.36631Y0051.88343
|
||||
X0048.41615Y0058.18246
|
||||
X0048.41615Y0055.03295
|
||||
X0048.41615Y0051.88343
|
||||
X0051.55622Y0058.18246
|
||||
X0051.55622Y0057.13262
|
||||
X0051.55622Y0056.08279
|
||||
X0051.55622Y0055.03295
|
||||
X0051.55622Y0053.98311
|
||||
X0051.55622Y0052.93327
|
||||
X0051.55622Y0051.88343
|
||||
X0052.60606Y0058.18246
|
||||
X0052.60606Y0051.88343
|
||||
X0053.65590Y0058.18246
|
||||
X0053.65590Y0051.88343
|
||||
X0054.70574Y0058.18246
|
||||
X0054.70574Y0051.88343
|
||||
X0055.75558Y0057.13262
|
||||
X0055.75558Y0056.08279
|
||||
X0055.75558Y0055.03295
|
||||
X0055.75558Y0053.98311
|
||||
X0055.75558Y0052.93327
|
||||
M30
|
||||
2866
gerbolyze/tests/resources/layers.svg
Normal file
|
After Width: | Height: | Size: 188 KiB |
918
gerbolyze/tests/resources/svg_feature_test.svg
Normal file
|
|
@ -0,0 +1,918 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
width="100.0mm"
|
||||
height="100.0mm"
|
||||
viewBox="0 0 100.0 100.0"
|
||||
id="svg166"
|
||||
sodipodi:docname="svg_feature_test.svg"
|
||||
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs150">
|
||||
<pattern
|
||||
inkscape:collect="always"
|
||||
xlink:href="#Polkadots-large"
|
||||
id="pattern6704"
|
||||
patternTransform="matrix(0.72748034,0,0,0.72748034,24.377412,83.823381)" />
|
||||
<pattern
|
||||
inkscape:collect="always"
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="10"
|
||||
height="10"
|
||||
patternTransform="translate(0,0) scale(10,10)"
|
||||
id="Polkadots-large"
|
||||
inkscape:stockid="Polka dots, large">
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="2.567"
|
||||
cy="0.810"
|
||||
r="0.45"
|
||||
id="circle5968" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="3.048"
|
||||
cy="2.33"
|
||||
r="0.45"
|
||||
id="circle5970" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="4.418"
|
||||
cy="2.415"
|
||||
r="0.45"
|
||||
id="circle5972" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="1.844"
|
||||
cy="3.029"
|
||||
r="0.45"
|
||||
id="circle5974" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="6.08"
|
||||
cy="1.363"
|
||||
r="0.45"
|
||||
id="circle5976" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="5.819"
|
||||
cy="4.413"
|
||||
r="0.45"
|
||||
id="circle5978" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="4.305"
|
||||
cy="4.048"
|
||||
r="0.45"
|
||||
id="circle5980" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="5.541"
|
||||
cy="3.045"
|
||||
r="0.45"
|
||||
id="circle5982" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="4.785"
|
||||
cy="5.527"
|
||||
r="0.45"
|
||||
id="circle5984" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="2.667"
|
||||
cy="5.184"
|
||||
r="0.45"
|
||||
id="circle5986" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="7.965"
|
||||
cy="1.448"
|
||||
r="0.45"
|
||||
id="circle5988" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="7.047"
|
||||
cy="5.049"
|
||||
r="0.45"
|
||||
id="circle5990" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="4.340"
|
||||
cy="0.895"
|
||||
r="0.45"
|
||||
id="circle5992" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="7.125"
|
||||
cy="0.340"
|
||||
r="0.45"
|
||||
id="circle5994" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="7.125"
|
||||
cy="10.340"
|
||||
r="0.45"
|
||||
id="circle5996" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="9.550"
|
||||
cy="1.049"
|
||||
r="0.45"
|
||||
id="circle5998" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="7.006"
|
||||
cy="2.689"
|
||||
r="0.45"
|
||||
id="circle6000" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="8.909"
|
||||
cy="2.689"
|
||||
r="0.45"
|
||||
id="circle6002" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="9.315"
|
||||
cy="4.407"
|
||||
r="0.45"
|
||||
id="circle6004" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="7.820"
|
||||
cy="3.870"
|
||||
r="0.45"
|
||||
id="circle6006" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="8.270"
|
||||
cy="5.948"
|
||||
r="0.45"
|
||||
id="circle6008" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="7.973"
|
||||
cy="7.428"
|
||||
r="0.45"
|
||||
id="circle6010" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="9.342"
|
||||
cy="8.072"
|
||||
r="0.45"
|
||||
id="circle6012" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="8.206"
|
||||
cy="9.315"
|
||||
r="0.45"
|
||||
id="circle6014" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="9.682"
|
||||
cy="9.475"
|
||||
r="0.45"
|
||||
id="circle6016" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="-0.318"
|
||||
cy="9.475"
|
||||
r="0.45"
|
||||
id="circle6018" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="9.688"
|
||||
cy="6.186"
|
||||
r="0.45"
|
||||
id="circle6020" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="-0.312"
|
||||
cy="6.186"
|
||||
r="0.45"
|
||||
id="circle6022" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="3.379"
|
||||
cy="6.296"
|
||||
r="0.45"
|
||||
id="circle6024" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="2.871"
|
||||
cy="8.204"
|
||||
r="0.45"
|
||||
id="circle6026" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="4.59"
|
||||
cy="8.719"
|
||||
r="0.45"
|
||||
id="circle6028" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="3.181"
|
||||
cy="9.671"
|
||||
r="0.45"
|
||||
id="circle6030" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="3.181"
|
||||
cy="-0.329"
|
||||
r="0.45"
|
||||
id="circle6032" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="5.734"
|
||||
cy="7.315"
|
||||
r="0.45"
|
||||
id="circle6034" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="6.707"
|
||||
cy="6.513"
|
||||
r="0.45"
|
||||
id="circle6036" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="5.730"
|
||||
cy="9.670"
|
||||
r="0.45"
|
||||
id="circle6038" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="5.730"
|
||||
cy="-0.330"
|
||||
r="0.45"
|
||||
id="circle6040" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="6.535"
|
||||
cy="8.373"
|
||||
r="0.45"
|
||||
id="circle6042" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="4.37"
|
||||
cy="7.154"
|
||||
r="0.45"
|
||||
id="circle6044" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="0.622"
|
||||
cy="7.25"
|
||||
r="0.45"
|
||||
id="circle6046" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="0.831"
|
||||
cy="5.679"
|
||||
r="0.45"
|
||||
id="circle6048" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="1.257"
|
||||
cy="8.519"
|
||||
r="0.45"
|
||||
id="circle6050" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="1.989"
|
||||
cy="6.877"
|
||||
r="0.45"
|
||||
id="circle6052" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="0.374"
|
||||
cy="3.181"
|
||||
r="0.45"
|
||||
id="circle6054" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="10.374"
|
||||
cy="3.181"
|
||||
r="0.45"
|
||||
id="circle6056" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="1.166"
|
||||
cy="1.664"
|
||||
r="0.45"
|
||||
id="circle6058" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="1.151"
|
||||
cy="0.093"
|
||||
r="0.45"
|
||||
id="circle6060" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="1.151"
|
||||
cy="10.093"
|
||||
r="0.45"
|
||||
id="circle6062" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="1.302"
|
||||
cy="4.451"
|
||||
r="0.45"
|
||||
id="circle6064" />
|
||||
<circle
|
||||
style="fill:black;stroke:none"
|
||||
cx="3.047"
|
||||
cy="3.763"
|
||||
r="0.45"
|
||||
id="circle6066" />
|
||||
</pattern>
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="247.74823"
|
||||
height="77.94172"
|
||||
id="rect935" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="247.74823"
|
||||
height="77.94172"
|
||||
id="rect1051" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="247.74823"
|
||||
height="77.94172"
|
||||
id="rect1205" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="270.88816"
|
||||
height="79.870047"
|
||||
id="rect1935" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="270.88816"
|
||||
height="79.870047"
|
||||
id="rect1987" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="270.88816"
|
||||
height="79.870047"
|
||||
id="rect1995" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="270.88816"
|
||||
height="79.870047"
|
||||
id="rect2003" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect2794" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect3050" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3113">
|
||||
<circle
|
||||
style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:0.647597;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="circle3115"
|
||||
cx="24.514038"
|
||||
cy="50.877434"
|
||||
r="3.8201945" />
|
||||
</clipPath>
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect3119" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3206">
|
||||
<circle
|
||||
style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="circle3208"
|
||||
cx="27.645788"
|
||||
cy="54.697628"
|
||||
r="2.6457884" />
|
||||
</clipPath>
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect3295" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect3386" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3627">
|
||||
<circle
|
||||
style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:0.550532;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="circle3629"
|
||||
cx="28.023756"
|
||||
cy="61.331257"
|
||||
r="2.7942765" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3835">
|
||||
<circle
|
||||
style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="circle3837"
|
||||
cx="29.400648"
|
||||
cy="67.710587"
|
||||
r="1.8898487" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3845">
|
||||
<rect
|
||||
style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="rect3847"
|
||||
width="9.8002157"
|
||||
height="2.5377996"
|
||||
x="22.570196"
|
||||
y="66.441681" />
|
||||
</clipPath>
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect3952" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect3956" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect3960" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect4082" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297806"
|
||||
id="rect3956-2" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297806"
|
||||
id="rect3960-4" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect4528" />
|
||||
<rect
|
||||
x="16.595081"
|
||||
y="14.2862"
|
||||
width="240.6777"
|
||||
height="86.297804"
|
||||
id="rect6890" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="g-outline"
|
||||
id="namedview152"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.4500417"
|
||||
inkscape:cx="105.30433"
|
||||
inkscape:cy="192.24163"
|
||||
inkscape:window-width="1953"
|
||||
inkscape:window-height="1647"
|
||||
inkscape:window-x="1371"
|
||||
inkscape:window-y="441"
|
||||
inkscape:window-maximized="0" />
|
||||
<g
|
||||
id="g-top-paste"
|
||||
inkscape:label="top paste"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-top-silk"
|
||||
inkscape:label="top silk"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-top-mask"
|
||||
inkscape:label="top mask"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-top-copper"
|
||||
inkscape:label="top copper"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-bottom-copper"
|
||||
inkscape:label="bottom copper"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-bottom-mask"
|
||||
inkscape:label="bottom mask"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-bottom-silk"
|
||||
inkscape:label="bottom silk"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-bottom-paste"
|
||||
inkscape:label="bottom paste"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-outline"
|
||||
inkscape:label="outline"
|
||||
inkscape:groupmode="layer">
|
||||
<rect
|
||||
style="fill:none;fill-rule:evenodd;stroke-width:0.1;paint-order:fill markers stroke;stop-color:#000000;stroke:#000000;stroke-dasharray:none"
|
||||
id="rect270"
|
||||
width="100"
|
||||
height="100"
|
||||
x="-4.4408921e-16"
|
||||
y="0" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,6.8667425,2.8656611)"
|
||||
id="text933"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect935);display:inline"><tspan
|
||||
x="58.45161"
|
||||
y="41.306035"
|
||||
id="tspan7049">simple path</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 22.705533,4.6733961 H 33.167047"
|
||||
id="path1047" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,6.8808531,7.3328044)"
|
||||
id="text1049"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect1051);display:inline"><tspan
|
||||
x="58.689154"
|
||||
y="41.306035"
|
||||
id="tspan7051">corner path</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 22.705839,10.005595 5.192577,-2.0617595 5.268937,2.0617595"
|
||||
id="path1103"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,6.7882496,12.44902)"
|
||||
id="text1201"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect1205);display:inline"><tspan
|
||||
x="57.130163"
|
||||
y="41.306035"
|
||||
id="tspan7053">curved path</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 22.692461,14.581853 c 0,0 4.638508,2.548328 5.230758,-0.763615 0.55918,-3.127016 3.617687,2.256085 5.230756,0.763615"
|
||||
id="path1203"
|
||||
sodipodi:nodetypes="csc" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,2.9425817,17.16331)"
|
||||
id="text1933"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect1935);display:inline"
|
||||
x="124.18531"
|
||||
y="0"><tspan
|
||||
x="187.29086"
|
||||
y="41.306035"
|
||||
id="tspan7055">group;
|
||||
</tspan><tspan
|
||||
x="36.142285"
|
||||
y="78.425034"
|
||||
id="tspan7057">endcaps and joins</tspan></text>
|
||||
<g
|
||||
id="g2734"
|
||||
transform="translate(5.2953574,-1.8358531)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.9;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 18.574916,20.541223 5.192577,-2.061759 c -0.599962,4.290659 3.333486,-0.418399 5.268937,2.061759"
|
||||
id="path1937"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.9;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 17.505856,22.469349 5.192577,-2.061759 c -0.599962,4.290659 3.333486,-0.418399 5.268937,2.061759"
|
||||
id="path1939"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.9;stroke-linecap:square;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 16.474977,24.512017 5.192577,-2.061759 c -0.599962,4.290659 3.333486,-0.418399 5.268937,2.061759"
|
||||
id="path1945"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,7.8058302,29.501323)"
|
||||
id="text1985"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect1987);display:inline"><tspan
|
||||
x="97.281146"
|
||||
y="41.306035"
|
||||
id="tspan7059">plain fill</tspan></text>
|
||||
<path
|
||||
style="fill:#000000;stroke:none;stroke-width:0.176389px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 24.037881,30.609551 7.37041,-0.37197 0.426408,0.953658 -4.746062,1.638134 z"
|
||||
id="path1991"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.3822489,33.308018)"
|
||||
id="text1993"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect1995);display:inline"><tspan
|
||||
x="57.489574"
|
||||
y="41.306035"
|
||||
id="tspan7061">fill and stroke</tspan></text>
|
||||
<path
|
||||
style="fill:#000000;stroke:#000000;stroke-width:0.176389px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 24.104798,34.389249 7.37041,-0.37197 0.426408,0.953658 -4.746062,1.638134 z"
|
||||
id="path1997"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,6.2747768,25.397651)"
|
||||
id="text1999"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect2003);display:inline"><tspan
|
||||
x="71.624484"
|
||||
y="41.306035"
|
||||
id="tspan7063">closed path</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 24.037862,26.478882 7.37041,-0.37197 0.426408,0.953658 -4.746062,1.638134 z"
|
||||
id="path2001"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.3822489,38.462414)"
|
||||
id="text2792"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect2794);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="104.34328"
|
||||
y="41.306035"
|
||||
id="tspan7065">predefined </tspan><tspan
|
||||
x="158.80415"
|
||||
y="78.425034"
|
||||
id="tspan7067">shapes</tspan></text>
|
||||
<g
|
||||
id="g2916"
|
||||
transform="translate(5.4287604,-1.5315419)">
|
||||
<circle
|
||||
style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="path2910"
|
||||
cx="20.102144"
|
||||
cy="43.086933"
|
||||
r="1.2790539" />
|
||||
<rect
|
||||
style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="rect2912"
|
||||
width="2.7871919"
|
||||
height="2.7871919"
|
||||
x="23.404778"
|
||||
y="41.693336" />
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.3822489,44.647688)"
|
||||
id="text3048"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect3050);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="130.38594"
|
||||
y="41.306035"
|
||||
id="tspan7069">open clip</tspan></text>
|
||||
<g
|
||||
id="g3111"
|
||||
clip-path="url(#clipPath3113)"
|
||||
transform="translate(3.4222519,-4.1579965)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 29.622354,51.276401 c 0,0 -1.750305,1.946645 -3.375581,1.93653 -1.321482,-0.0082 -1.448632,-1.370721 -3.058728,-1.420138 -2.153629,-0.0661 -3.782176,1.136027 -3.782176,1.136027"
|
||||
id="path3052"
|
||||
sodipodi:nodetypes="cssc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 29.622354,50.439468 c 0,0 -1.750305,1.946645 -3.375581,1.93653 -1.321482,-0.0082 -1.448632,-1.370721 -3.058728,-1.420138 -2.153629,-0.0661 -3.782176,1.136027 -3.782176,1.136027"
|
||||
id="path3054"
|
||||
sodipodi:nodetypes="cssc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 29.622354,49.508043 c 0,0 -1.750305,1.946645 -3.375581,1.93653 -1.321482,-0.0082 -1.448632,-1.370721 -3.058728,-1.420138 -2.153629,-0.0661 -3.782176,1.136027 -3.782176,1.136027"
|
||||
id="path3056"
|
||||
sodipodi:nodetypes="cssc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 29.622354,48.54962 c 0,0 -1.750305,1.946645 -3.375581,1.93653 -1.321482,-0.0082 -1.448632,-1.370721 -3.058728,-1.420138 -2.153629,-0.0661 -3.782176,1.136027 -3.782176,1.136027"
|
||||
id="path3058"
|
||||
sodipodi:nodetypes="cssc" />
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.3822489,50.641538)"
|
||||
id="text3117"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect3119);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="112.59854"
|
||||
y="41.306035"
|
||||
id="tspan7071">closed clip</tspan></text>
|
||||
<path
|
||||
id="rect3201"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.349326;stroke-linecap:square;stroke-linejoin:round;paint-order:fill markers stroke;stop-color:#000000"
|
||||
d="m 25.410625,52.462463 h 4.470328 v 4.470328 h -4.470328 z"
|
||||
clip-path="url(#clipPath3206)"
|
||||
transform="translate(0.29050101,-2.0611323)" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.3822489,55.98711)"
|
||||
id="text3293"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect3295);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="133.50398"
|
||||
y="41.306035"
|
||||
id="tspan7073">filled clip</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.3822489,64.518427)"
|
||||
id="text3384"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect3386);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="106.65948"
|
||||
y="41.306035"
|
||||
id="tspan7075">nested clip</tspan></text>
|
||||
<path
|
||||
id="path3622"
|
||||
style="fill-rule:evenodd;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;paint-order:fill markers stroke;stop-color:#000000"
|
||||
inkscape:transform-center-y="-1.0053078"
|
||||
d="m 31.506243,64.125533 h -3.482486 -3.482485 l 1.741242,-3.015921 1.741243,-3.015921 1.741243,3.015921 z"
|
||||
clip-path="url(#clipPath3627)"
|
||||
transform="translate(-0.08746602,-2.5107991)" />
|
||||
<g
|
||||
id="g3843"
|
||||
clip-path="url(#clipPath3845)"
|
||||
transform="translate(0.8149011,-1.2958963)">
|
||||
<path
|
||||
id="path3819"
|
||||
style="fill-rule:evenodd;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;paint-order:fill markers stroke;stop-color:#000000"
|
||||
inkscape:transform-center-x="0.70766436"
|
||||
d="m 27.198265,70.162005 -2.122992,-1.22571 -2.122992,-1.225711 2.122992,-1.22571 2.122992,-1.22571 v 2.45142 z" />
|
||||
<path
|
||||
id="path3823"
|
||||
style="fill-rule:evenodd;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;paint-order:fill markers stroke;stop-color:#000000"
|
||||
inkscape:transform-center-x="-0.70766454"
|
||||
d="m 28.039316,70.162005 2.122992,-1.22571 2.122992,-1.225711 -2.122992,-1.22571 -2.122992,-1.22571 v 2.45142 z"
|
||||
clip-path="url(#clipPath3835)" />
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.3646103,72.230931)"
|
||||
id="text3950"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect3952);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="185.7971"
|
||||
y="41.306035"
|
||||
id="tspan7077">fonts</tspan></text>
|
||||
<g
|
||||
id="g3964"
|
||||
transform="translate(0.26725891,-1.2958963)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.19209956,0,0,0.19209956,-22.574531,69.324577)"
|
||||
id="text3954"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect3956);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="238.29821"
|
||||
y="41.306035"
|
||||
id="tspan7079">A</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.19209956,0,0,0.19209956,-17.286502,69.324577)"
|
||||
id="text3958"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:29.6952px;line-height:1.25;font-family:serif;-inkscape-font-specification:serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect3960);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="236.33832"
|
||||
y="41.318475"
|
||||
id="tspan7081">A</tspan></text>
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.3646103,78.568929)"
|
||||
id="text4080"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect4082);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="103.21475"
|
||||
y="41.306035"
|
||||
id="tspan7083">transforms</tspan></text>
|
||||
<g
|
||||
aria-label="A"
|
||||
transform="matrix(0.19209956,0,0,0.19209956,-21.792044,75.044579)"
|
||||
id="text3958-5"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:serif;-inkscape-font-specification:serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect3960-4);display:inline">
|
||||
<g
|
||||
id="g4399"
|
||||
transform="translate(-4.1636239)">
|
||||
<path
|
||||
d="m 239.50049,14.635007 v 1.251332 h 0.32749 q 0.49984,0 0.74115,0.446904 0.25853,0.446904 0.56878,1.876998 l 3.82637,17.69741 h 1.3444 l 3.89531,-18.44225 q 0.1896,-0.923602 0.44814,-1.251333 0.27577,-0.327729 0.77561,-0.327729 h 0.22407 v -1.251332 h -4.73987 v 1.251332 h 0.39642 q 1.03416,0 1.03416,1.430094 0,0.238348 -0.0345,0.506491 -0.0345,0.268142 -0.10342,0.595873 l -0.68943,3.336885 h -4.53305 l -0.65496,-3.128329 q -0.15512,-0.715048 -0.15512,-1.281127 0,-1.459887 1.13756,-1.459887 h 0.39643 v -1.251332 z m 3.80914,8.610356 h 3.89532 l -1.06863,5.213884 q -0.27578,1.310918 -0.49984,2.472869 -0.22407,1.161951 -0.36195,2.234522 -0.12066,-1.072571 -0.31025,-2.05576 -0.1896,-0.983189 -0.4826,-2.38349 z"
|
||||
id="path4264"
|
||||
style="stroke-width:0.763121" />
|
||||
<path
|
||||
d="m 274.98158,14.564737 -0.71924,1.247198 h -0.56421 q -0.86116,0 -1.53377,0.445428 -0.70229,0.445428 -2.0588,1.870798 L 253.34114,35.76711 h -2.31623 l 3.8891,-18.381329 q 0.20422,-0.920551 -0.0528,-1.247199 -0.28675,-0.326647 -1.14791,-0.326647 h -0.38604 l 0.71924,-1.247198 h 8.16618 l -0.71924,1.247198 h -0.68299 q -1.78171,0 -2.6037,1.42537 -0.13699,0.237561 -0.23172,0.504818 -0.0947,0.267257 -0.16432,0.593904 l -0.73016,3.325863 h 7.80984 l 2.9265,-3.117996 q 0.67826,-0.712685 1.00363,-1.276894 0.83911,-1.455065 -1.12077,-1.455065 h -0.68299 l 0.71924,-1.247198 z m -11.51169,8.581913 h -6.71112 l -1.15573,5.19666 q -0.27835,1.306588 -0.56018,2.464701 -0.28183,1.158113 -0.66076,2.22714 0.82436,-1.069027 1.71612,-2.048969 0.89177,-0.979941 2.20144,-2.375616 z"
|
||||
id="path4267" />
|
||||
<path
|
||||
d="m 286.56177,36.228458 -1.14338,0.4982 -0.22537,-0.517242 q -0.344,-0.789473 -0.91841,-0.992676 -0.58628,-0.230409 -2.1065,-0.151063 l -18.8039,1.002412 -0.92523,-2.123412 14.17037,-13.494927 q 0.71343,-0.667176 0.83496,-1.206008 0.10966,-0.566047 -0.23433,-1.355519 l -0.1542,-0.353901 1.14337,-0.4982 3.26201,7.486371 -1.14337,0.498199 -0.27282,-0.626133 q -0.71172,-1.633389 -2.01843,-1.064019 -0.21779,0.09489 -0.43907,0.256106 -0.22127,0.161214 -0.47329,0.400575 l -2.57452,2.417459 3.11967,7.159695 3.30919,-0.211021 q 0.76011,-0.03968 1.27735,-0.26505 1.33394,-0.581231 0.55106,-2.377957 l -0.27283,-0.626137 1.14338,-0.498194 z m -10.48897,-2.588239 -2.68079,-6.152441 -4.02862,3.76366 q -1.00802,0.957502 -1.91553,1.774016 -0.9075,0.816507 -1.79263,1.461322 1.06307,-0.236463 2.09191,-0.328449 1.02885,-0.09198 2.50998,-0.186707 z"
|
||||
id="path4392" />
|
||||
</g>
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.4281104,84.658753)"
|
||||
id="text4526"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect4528);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="153.5481"
|
||||
y="41.306035"
|
||||
id="tspan7085">pattern</tspan></text>
|
||||
<path
|
||||
id="path4530"
|
||||
style="fill:url(#pattern6704);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
d="m 25.800794,83.999673 a 2.7012854,2.7012854 0 0 0 -2.701644,2.701644 2.7012854,2.7012854 0 0 0 2.701644,2.701127 2.7012854,2.7012854 0 0 0 2.138371,-1.052649 2.7012854,2.7012854 0 0 0 2.137854,1.052649 2.7012854,2.7012854 0 0 0 2.701127,-2.701127 2.7012854,2.7012854 0 0 0 -2.701127,-2.701644 2.7012854,2.7012854 0 0 0 -2.137854,1.051615 2.7012854,2.7012854 0 0 0 -2.138371,-1.051615 z" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.05939974,0,0,0.05939974,5.4281104,91.454919)"
|
||||
id="text6888"
|
||||
style="font-size:29.6952px;line-height:1.25;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect6890);display:inline"
|
||||
x="76.212669"
|
||||
y="0"><tspan
|
||||
x="126.58495"
|
||||
y="41.306035"
|
||||
id="tspan7087">occlusion</tspan></text>
|
||||
<g
|
||||
id="g6898"
|
||||
transform="translate(0.18127085,-1.2958963)">
|
||||
<rect
|
||||
style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="rect6892"
|
||||
width="10.614238"
|
||||
height="4.3144202"
|
||||
x="22.450258"
|
||||
y="92.206421" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.499999;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||
id="path6894"
|
||||
cx="27.757378"
|
||||
cy="94.363632"
|
||||
r="3.1689992" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g-drill-plated"
|
||||
inkscape:label="drill plated"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-drill-nonplated"
|
||||
inkscape:label="drill nonplated"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
id="g-comments"
|
||||
inkscape:label="comments"
|
||||
inkscape:groupmode="layer" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 33 KiB |
BIN
gerbolyze/tests/resources/test_gerber_8seg.zip
Normal file
255
gerbolyze/tests/resources/tpl-bottom.svg
Normal file
|
After Width: | Height: | Size: 521 KiB |
135
gerbolyze/tests/resources/tpl-top.svg
Normal file
|
After Width: | Height: | Size: 192 KiB |
118
gerbolyze/tests/test_integration.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2022 Jan Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import sys
|
||||
import math
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import gerbonara
|
||||
import pytest
|
||||
|
||||
|
||||
REFERENCE_GERBERS = ['test_gerber_8seg.zip']
|
||||
REFERENCE_SVGS = ['svg_feature_test.svg']
|
||||
|
||||
reference_path = lambda reference: Path(__file__).parent / 'resources' / str(reference)
|
||||
|
||||
|
||||
def run_command(*args):
|
||||
try:
|
||||
proc = subprocess.run(args, check=True, capture_output=True)
|
||||
print(proc.stdout.decode())
|
||||
print(proc.stderr.decode(), file=sys.stderr)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.stdout.decode())
|
||||
print(e.stderr.decode(), file=sys.stderr)
|
||||
raise
|
||||
|
||||
def test_template_round_trip():
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as out_svg,\
|
||||
tempfile.TemporaryDirectory() as out_dir:
|
||||
run_command('python3', '-m', 'gerbolyze', 'empty-template', '--force', out_svg.name)
|
||||
run_command('python3', '-m', 'gerbolyze', 'convert', out_svg.name, out_dir)
|
||||
|
||||
def test_zip_write():
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as out_svg,\
|
||||
tempfile.NamedTemporaryFile(suffix='.zip') as out_zip:
|
||||
run_command('python3', '-m', 'gerbolyze', 'empty-template', '--force', out_svg.name)
|
||||
run_command('python3', '-m', 'gerbolyze', 'convert', out_svg.name, out_zip.name)
|
||||
|
||||
@pytest.mark.parametrize('reference', REFERENCE_SVGS)
|
||||
def test_complex_conversion(reference):
|
||||
infile = reference_path(reference)
|
||||
with tempfile.NamedTemporaryFile(suffix='.zip') as out_zip:
|
||||
run_command('python3', '-m', 'gerbolyze', 'convert', infile, out_zip.name)
|
||||
run_command('python3', '-m', 'gerbolyze', 'convert', '--pattern-complete-tiles-only', '--use-apertures-for-patterns', infile, out_zip.name)
|
||||
|
||||
@pytest.mark.parametrize('reference', REFERENCE_GERBERS)
|
||||
def test_template(reference):
|
||||
with tempfile.NamedTemporaryFile(suffix='.zip') as out_svg:
|
||||
infile = reference_path(reference)
|
||||
run_command('python3', '-m', 'gerbolyze', 'template', '--top', '--force', infile, out_svg.name)
|
||||
run_command('python3', '-m', 'gerbolyze', 'template', '--bottom', '--force', '--vector', infile, out_svg.name)
|
||||
|
||||
def test_paste():
|
||||
in_gerbers = reference_path('layers-gerber')
|
||||
top_overlay = reference_path('tpl-top.svg')
|
||||
bottom_overlay = reference_path('tpl-bottom.svg')
|
||||
with tempfile.TemporaryDirectory() as intermediate_gerbers,\
|
||||
tempfile.TemporaryDirectory() as output_gerbers:
|
||||
run_command('python3', '-m', 'gerbolyze', 'paste', '--log-level', 'debug', '--no-subtract', in_gerbers, top_overlay, intermediate_gerbers)
|
||||
run_command('python3', '-m', 'gerbolyze', 'paste', '--log-level', 'debug', '--no-subtract', intermediate_gerbers, bottom_overlay, output_gerbers)
|
||||
|
||||
stack_old = gerbonara.layers.LayerStack.open(in_gerbers)
|
||||
stack_new = gerbonara.layers.LayerStack.open(output_gerbers)
|
||||
|
||||
for (side, use), layer_old in stack_old.graphic_layers.items():
|
||||
if use == 'outline':
|
||||
continue
|
||||
layer_new = stack_new[side, use]
|
||||
|
||||
bbox_old = layer_old.bounding_box(gerbonara.utils.MM)
|
||||
bbox_new = layer_new.bounding_box(gerbonara.utils.MM)
|
||||
|
||||
e = 0.8
|
||||
assert math.isclose(bbox_new[0][0], bbox_old[0][0]-e, abs_tol=0.1)
|
||||
assert math.isclose(bbox_new[0][1], bbox_old[0][1]-e, abs_tol=0.1)
|
||||
assert math.isclose(bbox_new[1][0], bbox_old[1][0]+e, abs_tol=0.1)
|
||||
assert math.isclose(bbox_new[1][1], bbox_old[1][1]+e, abs_tol=0.1)
|
||||
|
||||
def test_convert_layers():
|
||||
infile = reference_path('layers.svg')
|
||||
with tempfile.TemporaryDirectory() as out_dir:
|
||||
run_command('python3', '-m', 'gerbolyze', 'convert', infile, out_dir)
|
||||
stack = gerbonara.layers.LayerStack.open(out_dir)
|
||||
|
||||
for layer, dia in {
|
||||
'top paste': 0.100,
|
||||
'top silk': 0.110,
|
||||
'top mask': 0.120,
|
||||
'top copper': 0.130,
|
||||
'bottom copper': 0.140,
|
||||
'bottom mask': 0.150,
|
||||
'bottom silk': 0.160,
|
||||
'bottom paste': 0.170,
|
||||
'mechanical outline': 0.09}.items():
|
||||
assert set(round(ap.diameter, 4) for ap in stack[layer].apertures) == {dia}
|
||||
|
||||
# Note: svg-flatten rounds these diameters to the geometric tolerance given on the command line (0.01mm by
|
||||
# default).
|
||||
assert stack.drill_pth.drill_sizes() == [0.7]
|
||||
assert stack.drill_npth.drill_sizes() == [0.5]
|
||||
|
||||
17
gerboweb.service
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=Gerboweb server service
|
||||
Documentation=https://gitlab.com/gerbolyze/gerbolyze
|
||||
Wants=network-online.target
|
||||
Wants=nginx.service
|
||||
After=nginx.service
|
||||
RequiresMountsFor=/var/run/container/storage
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/podman run --secret=gerboweb --conmon-pidfile=%t/container-gerboweb.pid --volume=${RUNTIME_DIRECTORY}:/run/uwsgi --detach gerboweb
|
||||
ExecStop=/usr/bin/podman stop --time 2 gerboweb
|
||||
Type=forking
|
||||
PIDFile=%t/container-gerboweb.pid
|
||||
RuntimeDirectory=gerboweb-uwsgi
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
19
gerboweb/Containerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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 cargo python-pip base-devel gerbv rsync tmux uwsgi uwsgi-plugin-python
|
||||
RUN cargo install usvg resvg
|
||||
RUN python3 -m pip install pip==21.3.1
|
||||
RUN python3 -m pip install flask numpy lxml wasmtime svg_flatten_wasi resvg_wasi flask_wtf
|
||||
RUN --mount=type=bind,rw,destination=/git \
|
||||
cd /git/gerbonara && \
|
||||
python3 -m pip --disable-pip-version-check install . && \
|
||||
cd /git && \
|
||||
python3 -m pip --disable-pip-version-check install .
|
||||
RUN mkdir /gerboweb
|
||||
ADD ["gerboweb/uwsgi-gerboweb.ini","gerboweb/gerboweb.py","gerboweb/job_processor.py","gerboweb/job_queue.py","/gerboweb/"]
|
||||
ADD ["gerboweb/static","/gerboweb/static"]
|
||||
ADD ["gerboweb/templates","/gerboweb/templates"]
|
||||
ADD gerboweb/gerboweb_prod.cfg /gerboweb/gerboweb.cfg
|
||||
RUN mkdir /var/cache/gerboweb
|
||||
ENTRYPOINT uwsgi --ini /gerboweb/uwsgi-gerboweb.ini --chmod-socket=660 --socket=/run/uwsgi/socket
|
||||
12
gerboweb/Containerfile.develop
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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 gerbv rsync tmux
|
||||
RUN rustup install stable
|
||||
RUN rustup default stable
|
||||
RUN cargo install usvg resvg
|
||||
RUN mkdir /app /gerbolyze
|
||||
RUN python3 -m pip install pip==21.3.1
|
||||
RUN python3 -m pip install flask numpy lxml wasmtime svg_flatten_wasi resvg_wasi flask_wtf
|
||||
COPY develop-startup.sh /app/
|
||||
ENTRYPOINT /app/develop-startup.sh
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
MAX_CONTENT_LENGTH=10000000
|
||||
MAX_CONTENT_LENGTH=50000000
|
||||
SECRET_KEY="{{lookup('password', 'gerboweb_flask_secret.txt length=32')}}"
|
||||
UPLOAD_PATH="{{gerboweb_cache}}/upload"
|
||||
JOB_QUEUE_DB="{{gerboweb_cache}}/job_queue.sqlite3"
|
||||
86
gerboweb/ansible/gerboweb.yml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
- name: Setup gerboweb
|
||||
hosts: all
|
||||
tasks:
|
||||
- name: Set local facts
|
||||
set_fact:
|
||||
gerboweb_cache: /var/cache/gerboweb
|
||||
|
||||
- name: Install packages into gerbolyze container
|
||||
pacman:
|
||||
name: python3,python-numpy,python-pip,imagemagick,pugixml,git,python,make,clang,rustup,cargo
|
||||
state: present
|
||||
|
||||
- name: Setup usvg
|
||||
shell: cargo install usvg resvg
|
||||
args:
|
||||
creates: /usr/bin/usvg
|
||||
|
||||
- name: Copy webapp sources
|
||||
git:
|
||||
repo: ../..
|
||||
dest: /var/lib/gerboweb
|
||||
|
||||
- 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
|
||||
|
||||
17
gerboweb/ansible/render.sh.j2
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/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/template_top.svg /mnt/template_bottom.svg /mnt/template_top.preview.png /mnt/template_bottom.preview.png
|
||||
date; echo 'Rendering'
|
||||
gerbolyze template --top /mnt/template_top.svg --bottom /mnt/template_bottom.svg /tmp/gerber
|
||||
date; echo 'Scaling down'
|
||||
convert /mnt/template_top.svg -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/template_top.preview.png
|
||||
convert /mnt/template_bottom.svg -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/template_bottom.preview.png
|
||||
|
|
@ -7,4 +7,5 @@ plugins = python3
|
|||
chdir = /var/lib/gerboweb
|
||||
mount = /=gerboweb:app
|
||||
env = GERBOWEB_SETTINGS=gerboweb_prod.cfg
|
||||
mule = job_processor.py
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ systemd-nspawn \
|
|||
/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
|
||||
gerbolyze paste "--"$LAYER /mnt/overlay.svg gerber_in gerber
|
||||
rm -f /mnt/gerber_out.zip
|
||||
zip -r /mnt/gerber_out.zip gerber"
|
||||
|
||||
4
gerboweb/deploy/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
|||
*_secret.txt
|
||||
*_apikey.txt
|
||||
playbook.retry
|
||||
credentials.ini
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
---
|
||||
- name: Set local path facts
|
||||
set_fact:
|
||||
image: "/var/lib/machines/{{ container }}.img"
|
||||
root: "/var/lib/machines/{{ container }}"
|
||||
"{{container}}_root": "/var/lib/machines/{{ container }}"
|
||||
|
||||
- name: Create container image file
|
||||
command: truncate -s 4G "{{image}}"
|
||||
args:
|
||||
creates: "{{image}}"
|
||||
register: create_container
|
||||
|
||||
- name: Download arch bootstrap image
|
||||
get_url:
|
||||
url: http://mirror.rackspace.com/archlinux/iso/2020.03.01/archlinux-bootstrap-2020.03.01-x86_64.tar.gz
|
||||
dest: /tmp/arch-bootstrap.tar.xz
|
||||
checksum: sha256:49c7aa8718e48f5a4ec570624520fa50616ed3e044af101ec3aa16c155136f82
|
||||
when: create_container is changed
|
||||
|
||||
- name: Create container image filesystem
|
||||
filesystem:
|
||||
dev: "{{image}}"
|
||||
fstype: btrfs
|
||||
|
||||
- name: Create container image fstab entry
|
||||
mount:
|
||||
src: "{{image}}"
|
||||
path: "{{root}}"
|
||||
state: mounted
|
||||
fstype: btrfs
|
||||
opts: loop
|
||||
|
||||
- name: Unpack bootstrap image
|
||||
unarchive:
|
||||
remote_src: yes
|
||||
src: /tmp/arch-bootstrap.tar.xz
|
||||
dest: "{{root}}"
|
||||
extra_opts: --strip-components=1
|
||||
creates: "{{root}}/etc"
|
||||
|
||||
- name: Copy mirrorlist into container
|
||||
copy:
|
||||
src: mirrorlist
|
||||
dest: "{{root}}/etc/pacman.d/mirrorlist"
|
||||
|
||||
- name: Initialize container pacman keyring
|
||||
shell: arch-chroot "{{root}}" pacman-key --init && arch-chroot "{{root}}" pacman-key --populate archlinux
|
||||
args:
|
||||
creates: "{{root}}/etc/pacman.d/gnupg"
|
||||
|
||||
- name: Fixup pacman.conf for pacman to work in chroot without its own root fs
|
||||
lineinfile:
|
||||
path: "{{root}}/etc/pacman.conf"
|
||||
regexp: '^CheckSpace'
|
||||
line: '#CheckSpace'
|
||||
|
||||
- name: Update container and install software
|
||||
shell: arch-chroot "{{root}}" pacman -Syu --noconfirm
|
||||
|
||||
|
Before Width: | Height: | Size: 102 KiB |
|
|
@ -1,20 +0,0 @@
|
|||
css=/cgit.css
|
||||
logo= /cgit.png
|
||||
|
||||
enable-http-clone=1
|
||||
robots=noindex, nofollow
|
||||
virtual-root=/
|
||||
|
||||
readme=:README.rst
|
||||
about-filter=/usr/libexec/cgit/filters/about-formatting.sh
|
||||
|
||||
enable-index-links=1
|
||||
enable-commit-grpah=1
|
||||
enable-log-filecount=1
|
||||
enable-log-linecount=1
|
||||
enable-git-config=1
|
||||
|
||||
source-filter=/usr/libexec/cgit/filters/syntax-highlighting.py
|
||||
|
||||
project-list=/var/lib/gitolite3/projects.list
|
||||
scan-path=/var/lib/gitolite3/repositories
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 13a57211f0d0feb34b452b3e19be83a095707ed6
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# SPDX-License-Identifier: LGPL-2.1+
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Clippy container
|
||||
PartOf=machines.target
|
||||
Before=machines.target
|
||||
After=network.target systemd-resolved.service
|
||||
RequiresMountsFor=/var/lib/machines
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --ephemeral --boot -U --settings=override --machine=clippy
|
||||
KillMode=mixed
|
||||
Type=notify
|
||||
RestartForceExitStatus=133
|
||||
SuccessExitStatus=133
|
||||
WatchdogSec=3min
|
||||
Slice=machine.slice
|
||||
Delegate=yes
|
||||
TasksMax=512
|
||||
|
||||
# Enforce a strict device policy, similar to the one nspawn configures when it
|
||||
# allocates its own scope unit. Make sure to keep these policies in sync if you
|
||||
# change them!
|
||||
DevicePolicy=closed
|
||||
DeviceAllow=/dev/net/tun rwm
|
||||
DeviceAllow=char-pts rw
|
||||
|
||||
[Install]
|
||||
WantedBy=machines.target
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
[Network]
|
||||
VirtualEthernet=no
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
[Unit]
|
||||
Description=Clippy listener daemon
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/var/lib/clippy.git
|
||||
ExecStart=/usr/bin/python3 clippy.py -s -x 60x30 -e
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
[inwx]
|
||||
user=...
|
||||
pass=...
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
# configuration variables for gitolite
|
||||
|
||||
# This file is in perl syntax. But you do NOT need to know perl to edit it --
|
||||
# just mind the commas, use single quotes unless you know what you're doing,
|
||||
# and make sure the brackets and braces stay matched up!
|
||||
|
||||
# (Tip: perl allows a comma after the last item in a list also!)
|
||||
|
||||
# HELP for commands can be had by running the command with "-h".
|
||||
|
||||
# HELP for all the other FEATURES can be found in the documentation (look for
|
||||
# "list of non-core programs shipped with gitolite" in the master index) or
|
||||
# directly in the corresponding source file.
|
||||
|
||||
%RC = (
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# default umask gives you perms of '0700'; see the rc file docs for
|
||||
# how/why you might change this
|
||||
UMASK => 0027,
|
||||
|
||||
# look for "git-config" in the documentation
|
||||
GIT_CONFIG_KEYS => 'core\.sharedRepository',
|
||||
|
||||
# comment out if you don't need all the extra detail in the logfile
|
||||
LOG_EXTRA => 1,
|
||||
# logging options
|
||||
# 1. leave this section as is for 'normal' gitolite logging (default)
|
||||
# 2. uncomment this line to log ONLY to syslog:
|
||||
# LOG_DEST => 'syslog',
|
||||
# 3. uncomment this line to log to syslog and the normal gitolite log:
|
||||
# LOG_DEST => 'syslog,normal',
|
||||
# 4. prefixing "repo-log," to any of the above will **also** log just the
|
||||
# update records to "gl-log" in the bare repo directory:
|
||||
# LOG_DEST => 'repo-log,normal',
|
||||
# LOG_DEST => 'repo-log,syslog',
|
||||
# LOG_DEST => 'repo-log,syslog,normal',
|
||||
# syslog 'facility': defaults to 'local0', uncomment if needed. For example:
|
||||
# LOG_FACILITY => 'local4',
|
||||
|
||||
# roles. add more roles (like MANAGER, TESTER, ...) here.
|
||||
# WARNING: if you make changes to this hash, you MUST run 'gitolite
|
||||
# compile' afterward, and possibly also 'gitolite trigger POST_COMPILE'
|
||||
ROLES => {
|
||||
READERS => 1,
|
||||
WRITERS => 1,
|
||||
},
|
||||
|
||||
# enable caching (currently only Redis). PLEASE RTFM BEFORE USING!!!
|
||||
# CACHE => 'Redis',
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# rc variables used by various features
|
||||
|
||||
# the 'info' command prints this as additional info, if it is set
|
||||
# SITE_INFO => 'Please see http://blahblah/gitolite for more help',
|
||||
|
||||
# the CpuTime feature uses these
|
||||
# display user, system, and elapsed times to user after each git operation
|
||||
# DISPLAY_CPU_TIME => 1,
|
||||
# display a warning if total CPU times (u, s, cu, cs) crosses this limit
|
||||
# CPU_TIME_WARN_LIMIT => 0.1,
|
||||
|
||||
# the Mirroring feature needs this
|
||||
# HOSTNAME => "foo",
|
||||
|
||||
# TTL for redis cache; PLEASE SEE DOCUMENTATION BEFORE UNCOMMENTING!
|
||||
# CACHE_TTL => 600,
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# suggested locations for site-local gitolite code (see cust.html)
|
||||
|
||||
# this one is managed directly on the server
|
||||
# LOCAL_CODE => "$ENV{HOME}/local",
|
||||
|
||||
# or you can use this, which lets you put everything in a subdirectory
|
||||
# called "local" in your gitolite-admin repo. For a SECURITY WARNING
|
||||
# on this, see http://gitolite.com/gitolite/non-core.html#pushcode
|
||||
# LOCAL_CODE => "$rc{GL_ADMIN_BASE}/local",
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# List of commands and features to enable
|
||||
|
||||
ENABLE => [
|
||||
|
||||
# COMMANDS
|
||||
|
||||
# These are the commands enabled by default
|
||||
'help',
|
||||
'desc',
|
||||
'info',
|
||||
'perms',
|
||||
'writable',
|
||||
|
||||
# Uncomment or add new commands here.
|
||||
# 'create',
|
||||
# 'fork',
|
||||
# 'mirror',
|
||||
# 'readme',
|
||||
# 'sskm',
|
||||
'D',
|
||||
|
||||
# These FEATURES are enabled by default.
|
||||
|
||||
# essential (unless you're using smart-http mode)
|
||||
'ssh-authkeys',
|
||||
|
||||
# creates git-config entries from gitolite.conf file entries like 'config foo.bar = baz'
|
||||
'git-config',
|
||||
|
||||
# creates git-daemon-export-ok files; if you don't use git-daemon, comment this out
|
||||
'daemon',
|
||||
|
||||
# creates projects.list file; if you don't use gitweb, comment this out
|
||||
'gitweb',
|
||||
|
||||
# These FEATURES are disabled by default; uncomment to enable. If you
|
||||
# need to add new ones, ask on the mailing list :-)
|
||||
|
||||
# user-visible behaviour
|
||||
|
||||
# prevent wild repos auto-create on fetch/clone
|
||||
# 'no-create-on-read',
|
||||
# no auto-create at all (don't forget to enable the 'create' command!)
|
||||
# 'no-auto-create',
|
||||
|
||||
# access a repo by another (possibly legacy) name
|
||||
# 'Alias',
|
||||
|
||||
# give some users direct shell access. See documentation in
|
||||
# sts.html for details on the following two choices.
|
||||
# "Shell $ENV{HOME}/.gitolite.shell-users",
|
||||
# 'Shell alice bob',
|
||||
|
||||
# set default roles from lines like 'option default.roles-1 = ...', etc.
|
||||
# 'set-default-roles',
|
||||
|
||||
# show more detailed messages on deny
|
||||
# 'expand-deny-messages',
|
||||
|
||||
# show a message of the day
|
||||
# 'Motd',
|
||||
|
||||
# system admin stuff
|
||||
|
||||
# enable mirroring (don't forget to set the HOSTNAME too!)
|
||||
# 'Mirroring',
|
||||
|
||||
# allow people to submit pub files with more than one key in them
|
||||
# 'ssh-authkeys-split',
|
||||
|
||||
# selective read control hack
|
||||
# 'partial-copy',
|
||||
|
||||
# manage local, gitolite-controlled, copies of read-only upstream repos
|
||||
# 'upstream',
|
||||
|
||||
# updates 'description' file instead of 'gitweb.description' config item
|
||||
# 'cgit',
|
||||
|
||||
# allow repo-specific hooks to be added
|
||||
# 'repo-specific-hooks',
|
||||
|
||||
# performance, logging, monitoring...
|
||||
|
||||
# be nice
|
||||
# 'renice 10',
|
||||
|
||||
# log CPU times (user, system, cumulative user, cumulative system)
|
||||
# 'CpuTime',
|
||||
|
||||
# syntactic_sugar for gitolite.conf and included files
|
||||
|
||||
# allow backslash-escaped continuation lines in gitolite.conf
|
||||
# 'continuation-lines',
|
||||
|
||||
# create implicit user groups from directory names in keydir/
|
||||
# 'keysubdirs-as-groups',
|
||||
|
||||
# allow simple line-oriented macros
|
||||
# 'macros',
|
||||
|
||||
# Kindergarten mode
|
||||
|
||||
# disallow various things that sensible people shouldn't be doing anyway
|
||||
# 'Kindergarten',
|
||||
],
|
||||
|
||||
);
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# per perl rules, this should be the last line in such a file:
|
||||
1;
|
||||
|
||||
# Local variables:
|
||||
# mode: perl
|
||||
# End:
|
||||
# vim: set syn=perl:
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
all:
|
||||
hosts:
|
||||
wendelstein:
|
||||
ansible_host: wendelstein.jaseg.net
|
||||
ansible_ssh_identity_file: ~/.ssh/id_ed25519
|
||||
ansible_user: root
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
localhost:
|
||||
ansible_connection: local
|
||||
ansible_python_interpreter: "{{ansible_playbook_python}}"
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# Generated by iptables-save v1.8.0 on Thu Apr 4 11:06:33 2019
|
||||
*nat
|
||||
:PREROUTING ACCEPT [13:648]
|
||||
:INPUT ACCEPT [8:440]
|
||||
:OUTPUT ACCEPT [18:1260]
|
||||
:POSTROUTING ACCEPT [18:1260]
|
||||
-A PREROUTING -i eth0 -p tcp -m tcp --dport 23 -j REDIRECT --to-ports 2342
|
||||
COMMIT
|
||||
# Completed on Thu Apr 4 11:06:33 2019
|
||||
# Generated by iptables-save v1.8.0 on Thu Apr 4 11:06:33 2019
|
||||
*filter
|
||||
:INPUT ACCEPT [0:0]
|
||||
:FORWARD ACCEPT [0:0]
|
||||
:OUTPUT ACCEPT [360:761646]
|
||||
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
-A INPUT -p icmp -j ACCEPT
|
||||
-A INPUT -i lo -j ACCEPT
|
||||
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
|
||||
-A INPUT -p tcp -m state --state NEW -m tcp --dport 2342 -j ACCEPT
|
||||
-A INPUT -p tcp -m state --state NEW -m tcp --dport 23 -j ACCEPT
|
||||
-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT
|
||||
-A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT
|
||||
-A INPUT -p udp --dport 53 -j ACCEPT
|
||||
-A INPUT -j REJECT --reject-with icmp-host-prohibited
|
||||
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
|
||||
COMMIT
|
||||
# Completed on Thu Apr 4 11:06:33 2019
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 0ac040da14cc9d834098addc03cd8d4d26647df0
|
||||
|
|
@ -1,474 +0,0 @@
|
|||
##
|
||||
## Arch Linux repository mirrorlist
|
||||
## Generated on 2017-06-06
|
||||
##
|
||||
|
||||
## Worldwide
|
||||
#Server = https://archlinux.surlyjake.com/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.evowise.com/archlinux/$repo/os/$arch
|
||||
Server = http://mirror.rackspace.com/archlinux/$repo/os/$arch
|
||||
|
||||
## Australia
|
||||
#Server = https://mirror.aarnet.edu.au/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch
|
||||
#Server = http://ftp.iinet.net.au/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.internode.on.net/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.swin.edu.au/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.uberglobalmirror.com/$repo/os/$arch
|
||||
|
||||
## Austria
|
||||
#Server = http://mirror.digitalnova.at/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.easyname.at/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror1.htu.tugraz.at/archlinux/$repo/os/$arch
|
||||
|
||||
## Belarus
|
||||
#Server = http://ftp.byfly.by/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.datacenter.by/pub/archlinux/$repo/os/$arch
|
||||
|
||||
## Belgium
|
||||
#Server = http://archlinux.cu.be/$repo/os/$arch
|
||||
#Server = http://archlinux.mirror.kangaroot.net/$repo/os/$arch
|
||||
|
||||
## Bosnia and Herzegovina
|
||||
#Server = http://burek.archlinux.ba/$repo/os/$arch
|
||||
#Server = http://archlinux.mirror.ba/$repo/os/$arch
|
||||
|
||||
## Brazil
|
||||
#Server = http://br.mirror.archlinux-br.org/$repo/os/$arch
|
||||
#Server = http://archlinux.c3sl.ufpr.br/$repo/os/$arch
|
||||
#Server = http://linorg.usp.br/archlinux/$repo/os/$arch
|
||||
#Server = http://pet.inf.ufsc.br/mirrors/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.pop-es.rnp.br/$repo/os/$arch
|
||||
|
||||
## Bulgaria
|
||||
#Server = http://mirror.host.ag/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.netix.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.telepoint.bg/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.uni-plovdiv.net/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.uni-plovdiv.net/archlinux/$repo/os/$arch
|
||||
|
||||
## Canada
|
||||
#Server = http://mirror.cedille.club/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.mirror.colo-serv.net/$repo/os/$arch
|
||||
#Server = http://mirror.csclub.uwaterloo.ca/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.csclub.uwaterloo.ca/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.frgl.pw/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.frgl.pw/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.its.dal.ca/archlinux/$repo/os/$arch
|
||||
#Server = http://muug.ca/mirror/archlinux/$repo/os/$arch
|
||||
#Server = https://muug.ca/mirror/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.mirror.rafal.ca/$repo/os/$arch
|
||||
|
||||
## Chile
|
||||
#Server = http://mirror.archlinux.cl/$repo/os/$arch
|
||||
|
||||
## China
|
||||
#Server = http://mirrors.163.com/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.lzu.edu.cn/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.neusoft.edu.cn/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.skyshe.cn/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.xjtu.edu.cn/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.xjtu.edu.cn/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.zju.edu.cn/archlinux/$repo/os/$arch
|
||||
|
||||
## Colombia
|
||||
#Server = http://mirror.edatel.net.co/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.upb.edu.co/archlinux/$repo/os/$arch
|
||||
|
||||
## Croatia
|
||||
#Server = http://archlinux.iskon.hr/$repo/os/$arch
|
||||
|
||||
## Czech Republic
|
||||
#Server = http://mirror.dkm.cz/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.dkm.cz/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.fi.muni.cz/pub/linux/arch/$repo/os/$arch
|
||||
#Server = http://ftp.linux.cz/pub/linux/arch/$repo/os/$arch
|
||||
#Server = http://gluttony.sin.cvut.cz/arch/$repo/os/$arch
|
||||
#Server = https://gluttony.sin.cvut.cz/arch/$repo/os/$arch
|
||||
#Server = http://mirrors.nic.cz/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.sh.cvut.cz/arch/$repo/os/$arch
|
||||
#Server = https://ftp.sh.cvut.cz/arch/$repo/os/$arch
|
||||
#Server = http://mirror.vpsfree.cz/archlinux/$repo/os/$arch
|
||||
|
||||
## Denmark
|
||||
#Server = http://mirrors.dotsrc.org/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.dotsrc.org/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.klid.dk/ftp/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.one.com/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.one.com/archlinux/$repo/os/$arch
|
||||
|
||||
## Ecuador
|
||||
#Server = http://mirror.cedia.org.ec/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.espoch.edu.ec/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.uta.edu.ec/archlinux/$repo/os/$arch
|
||||
|
||||
## Finland
|
||||
#Server = http://arch.mirror.far.fi/$repo/os/$arch
|
||||
|
||||
## France
|
||||
#Server = http://archlinux.de-labrusse.fr/$repo/os/$arch
|
||||
#Server = http://mirror.archlinux.ikoula.com/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.vi-di.fr/$repo/os/$arch
|
||||
#Server = https://archlinux.vi-di.fr/$repo/os/$arch
|
||||
#Server = http://mirror.armbrust.me/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.armbrust.me/archlinux/$repo/os/$arch
|
||||
#Server = https://archlinux.ec-tech.fr/$repo/os/$arch
|
||||
#Server = http://fooo.biz/archlinux/$repo/os/$arch
|
||||
#Server = https://fooo.biz/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.gerhard.re/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.ibcp.fr/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.lastmikoi.net/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.mailtunnel.eu/$repo/os/$arch
|
||||
#Server = https://www.mailtunnel.eu/archlinux/$repo/os/$arch
|
||||
#Server = http://mir.archlinux.fr/$repo/os/$arch
|
||||
#Server = http://archlinux.mirrors.ovh.net/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.mirror.pkern.at/$repo/os/$arch
|
||||
#Server = https://archlinux.mirror.pkern.at/$repo/os/$arch
|
||||
#Server = http://archlinux.polymorf.fr/$repo/os/$arch
|
||||
#Server = http://mirrors.standaloneinstaller.com/archlinux/$repo/os/$arch
|
||||
#Server = http://arch.tamcore.eu/$repo/os/$arch
|
||||
#Server = http://mirror.tyborek.pl/arch/$repo/os/$arch
|
||||
#Server = https://mirror.tyborek.pl/arch/$repo/os/$arch
|
||||
#Server = http://ftp.u-strasbg.fr/linux/distributions/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.wormhole.eu/archlinux/$repo/os/$arch
|
||||
#Server = http://arch.yourlabs.org/$repo/os/$arch
|
||||
|
||||
## Germany
|
||||
#Server = http://mirror.23media.de/archlinux/$repo/os/$arch
|
||||
#Server = https://arch.32g.eu/$repo/os/$arch
|
||||
#Server = http://artfiles.org/archlinux.org/$repo/os/$arch
|
||||
#Server = https://fabric-mirror.vps.hosteurope.de/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.bethselamin.de/$repo/os/$arch
|
||||
#Server = http://mirror.euserv.net/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.f4st.host/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.f4st.host/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.fau.de/archlinux/$repo/os/$arch
|
||||
#Server = https://ftp.fau.de/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.fluxent.de/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.fluxent.de/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.gnomus.de/$repo/os/$arch
|
||||
#Server = http://www.gutscheindrache.com/mirror/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.gwdg.de/pub/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.hactar.xyz/$repo/os/$arch
|
||||
#Server = https://mirror.hactar.xyz/$repo/os/$arch
|
||||
#Server = http://archlinux.honkgong.info/$repo/os/$arch
|
||||
#Server = http://ftp.hosteurope.de/mirror/ftp.archlinux.org/$repo/os/$arch
|
||||
#Server = http://ftp-stud.hs-esslingen.de/pub/Mirrors/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.mirror.iphh.net/$repo/os/$arch
|
||||
#Server = http://repo.itmettke.de/archlinux/$repo/os/$arch
|
||||
#Server = https://repo.itmettke.de/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.jankoppe.de/archlinux/$repo/os/$arch
|
||||
#Server = http://arch.jensgutermuth.de/$repo/os/$arch
|
||||
#Server = https://arch.jensgutermuth.de/$repo/os/$arch
|
||||
#Server = http://mirror.js-webcoding.de/pub/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.js-webcoding.de/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://k42.ch/mirror/archlinux/$repo/os/$arch
|
||||
#Server = https://k42.ch/mirror/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.de.leaseweb.net/archlinux/$repo/os/$arch
|
||||
Server = https://mirror.de.leaseweb.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.loli.forsale/arch/$repo/os/$arch
|
||||
#Server = https://mirror.loli.forsale/arch/$repo/os/$arch
|
||||
#Server = http://mirror.metalgamer.eu/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.metalgamer.eu/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.michael-eckert.net/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.michael-eckert.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.n-ix.net/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.n-ix.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.netcologne.de/archlinux/$repo/os/$arch
|
||||
Server = https://mirror.netcologne.de/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.niyawe.de/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.niyawe.de/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.nullpointer.io/$repo/os/$arch
|
||||
#Server = https://archlinux.nullpointer.io/$repo/os/$arch
|
||||
#Server = http://mirror.pseudoform.org/$repo/os/$arch
|
||||
#Server = https://mirror.pseudoform.org/$repo/os/$arch
|
||||
#Server = https://www.ratenzahlung.de/mirror/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.halifax.rwth-aachen.de/archlinux/$repo/os/$arch
|
||||
#Server = http://linux.rz.rub.de/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.selfnet.de/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.spline.inf.fu-berlin.de/mirrors/archlinux/$repo/os/$arch
|
||||
#Server = https://ftp.spline.inf.fu-berlin.de/mirrors/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.thaller.ws/$repo/os/$arch
|
||||
#Server = https://archlinux.thaller.ws/$repo/os/$arch
|
||||
#Server = http://archlinux.thelinuxnetworx.rocks/$repo/os/$arch
|
||||
#Server = https://archlinux.thelinuxnetworx.rocks/$repo/os/$arch
|
||||
#Server = http://archmirror.tomforb.es/$repo/os/$arch
|
||||
#Server = https://archmirror.tomforb.es/$repo/os/$arch
|
||||
#Server = http://ftp.tu-chemnitz.de/pub/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.ubrco.de/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.ubrco.de/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.uni-bayreuth.de/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.uni-hannover.de/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.uni-kl.de/pub/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.united-gameserver.de/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.vfn-nrw.de/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.vfn-nrw.de/archlinux/$repo/os/$arch
|
||||
|
||||
## Greece
|
||||
#Server = http://ftp.cc.uoc.gr/mirrors/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://foss.aueb.gr/mirrors/linux/archlinux/$repo/os/$arch
|
||||
#Server = https://foss.aueb.gr/mirrors/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.myaegean.gr/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.ntua.gr/pub/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.otenet.gr/linux/archlinux/$repo/os/$arch
|
||||
|
||||
## Hong Kong
|
||||
#Server = http://arch-mirror.wtako.net/$repo/os/$arch
|
||||
#Server = https://arch-mirror.wtako.net/$repo/os/$arch
|
||||
|
||||
## Hungary
|
||||
#Server = http://ftp.energia.mta.hu/pub/mirrors/ftp.archlinux.org/$repo/os/$arch
|
||||
#Server = http://archmirror.hbit.sztaki.hu/archlinux/$repo/os/$arch
|
||||
|
||||
## Iceland
|
||||
#Server = http://mirror.system.is/arch/$repo/os/$arch
|
||||
#Server = https://mirror.system.is/arch/$repo/os/$arch
|
||||
|
||||
## India
|
||||
#Server = http://mirror.cse.iitk.ac.in/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.iitm.ac.in/archlinux/$repo/os/$arch
|
||||
|
||||
## Indonesia
|
||||
#Server = http://mirror.devilzc0de.org/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.poliwangi.ac.id/archlinux/$repo/os/$arch
|
||||
#Server = http://suro.ubaya.ac.id/archlinux/$repo/os/$arch
|
||||
|
||||
## Iran
|
||||
#Server = http://repo.sadjad.ac.ir/arch/$repo/os/$arch
|
||||
#Server = https://repo.sadjad.ac.ir/arch/$repo/os/$arch
|
||||
|
||||
## Ireland
|
||||
#Server = http://ftp.heanet.ie/mirrors/ftp.archlinux.org/$repo/os/$arch
|
||||
#Server = https://ftp.heanet.ie/mirrors/ftp.archlinux.org/$repo/os/$arch
|
||||
|
||||
## Israel
|
||||
#Server = http://mirror.isoc.org.il/pub/archlinux/$repo/os/$arch
|
||||
|
||||
## Italy
|
||||
#Server = http://archlinux.prometeolibero.eu/archlinux/$repo/os/$arch
|
||||
#Server = https://archlinux.prometeolibero.eu/archlinux/$repo/os/$arch
|
||||
#Server = https://archlinux.beccacervello.it/archlinux/$repo/os/$arch
|
||||
#Server = http://mi.mirror.garr.it/mirrors/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.prometeus.net/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.students.cs.unibo.it/$repo/os/$arch
|
||||
|
||||
## Japan
|
||||
#Server = http://ftp.tsukuba.wide.ad.jp/Linux/archlinux/$repo/os/$arch
|
||||
Server = http://ftp.jaist.ac.jp/pub/Linux/ArchLinux/$repo/os/$arch
|
||||
|
||||
## Kazakhstan
|
||||
#Server = http://mirror.neolabs.kz/archlinux/$repo/os/$arch
|
||||
|
||||
## Latvia
|
||||
#Server = http://archlinux.koyanet.lv/archlinux/$repo/os/$arch
|
||||
|
||||
## Lithuania
|
||||
#Server = http://mirrors.atviras.lt/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.atviras.lt/archlinux/$repo/os/$arch
|
||||
|
||||
## Luxembourg
|
||||
#Server = http://archlinux.mirror.root.lu/$repo/os/$arch
|
||||
|
||||
## Macedonia
|
||||
#Server = http://arch.softver.org.mk/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.t-home.mk/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.t-home.mk/archlinux/$repo/os/$arch
|
||||
|
||||
## Netherlands
|
||||
#Server = http://arch.apt-get.eu/$repo/os/$arch
|
||||
#Server = http://mirror.i3d.net/pub/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.i3d.net/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.nl.leaseweb.net/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.nl.leaseweb.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.netrouting.net/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.nluug.nl/os/Linux/distr/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.snt.utwente.nl/pub/os/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.mirror.wearetriple.com/$repo/os/$arch
|
||||
#Server = https://archlinux.mirror.wearetriple.com/$repo/os/$arch
|
||||
|
||||
## New Caledonia
|
||||
#Server = http://mirror.lagoon.nc/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.nautile.nc/archlinux/$repo/os/$arch
|
||||
|
||||
## New Zealand
|
||||
#Server = https://mirror.smith.geek.nz/archlinux/$repo/os/$arch
|
||||
|
||||
## Norway
|
||||
#Server = http://mirror.archlinux.no/$repo/os/$arch
|
||||
#Server = http://archlinux.uib.no/$repo/os/$arch
|
||||
#Server = http://mirror.neuf.no/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.neuf.no/archlinux/$repo/os/$arch
|
||||
|
||||
## Philippines
|
||||
#Server = http://mirror.rise.ph/archlinux/$repo/os/$arch
|
||||
|
||||
## Poland
|
||||
#Server = http://mirror.chmuri.net/archmirror/$repo/os/$arch
|
||||
#Server = http://arch.midov.pl/arch/$repo/os/$arch
|
||||
#Server = http://mirror.onet.pl/pub/mirrors/archlinux/$repo/os/$arch
|
||||
#Server = http://piotrkosoft.net/pub/mirrors/ftp.archlinux.org/$repo/os/$arch
|
||||
#Server = http://ftp.vectranet.pl/archlinux/$repo/os/$arch
|
||||
|
||||
## Portugal
|
||||
#Server = http://glua.ua.pt/pub/archlinux/$repo/os/$arch
|
||||
#Server = https://glua.ua.pt/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.rnl.tecnico.ulisboa.pt/pub/archlinux/$repo/os/$arch
|
||||
|
||||
## Qatar
|
||||
#Server = http://mirror.qnren.qa/archlinux/$repo/os/$arch
|
||||
|
||||
## Romania
|
||||
#Server = http://mirror.archlinux.ro/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.mirrors.linux.ro/$repo/os/$arch
|
||||
#Server = http://mirrors.m247.ro/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.pidginhost.com/arch/$repo/os/$arch
|
||||
|
||||
## Russia
|
||||
#Server = http://mirror.aur.rocks/$repo/os/$arch
|
||||
#Server = https://mirror.aur.rocks/$repo/os/$arch
|
||||
#Server = http://mirror.rol.ru/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.rol.ru/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.yandex.ru/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.yandex.ru/archlinux/$repo/os/$arch
|
||||
|
||||
## Serbia
|
||||
#Server = http://mirror.pmf.kg.ac.rs/archlinux/$repo/os/$arch
|
||||
|
||||
## Singapore
|
||||
#Server = http://mirror.0x.sg/archlinux/$repo/os/$arch
|
||||
#Server = http://download.nus.edu.sg/mirror/arch/$repo/os/$arch
|
||||
|
||||
## Slovakia
|
||||
#Server = http://mirror.lnx.sk/pub/linux/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.lnx.sk/pub/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://tux.rainside.sk/archlinux/$repo/os/$arch
|
||||
|
||||
## Slovenia
|
||||
#Server = http://archimonde.ts.si/archlinux/$repo/os/$arch
|
||||
#Server = https://archimonde.ts.si/archlinux/$repo/os/$arch
|
||||
|
||||
## South Africa
|
||||
#Server = http://za.mirror.archlinux-br.org/$repo/os/$arch
|
||||
#Server = http://ftp.wa.co.za/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.is.co.za/mirror/archlinux.org/$repo/os/$arch
|
||||
#Server = http://mirror.wbs.co.za/archlinux/$repo/os/$arch
|
||||
|
||||
## South Korea
|
||||
#Server = http://ftp.kaist.ac.kr/ArchLinux/$repo/os/$arch
|
||||
#Server = http://mirror.premi.st/archlinux/$repo/os/$arch
|
||||
|
||||
## Spain
|
||||
#Server = http://osl.ugr.es/archlinux/$repo/os/$arch
|
||||
#Server = http://sunsite.rediris.es/mirror/archlinux/$repo/os/$arch
|
||||
|
||||
## Sweden
|
||||
#Server = http://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch
|
||||
#Server = https://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.dynamict.se/$repo/os/$arch
|
||||
#Server = https://archlinux.dynamict.se/$repo/os/$arch
|
||||
#Server = http://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch
|
||||
#Server = https://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch
|
||||
#Server = https://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.osbeck.com/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.portlane.com/pub/os/linux/archlinux/$repo/os/$arch
|
||||
|
||||
## Switzerland
|
||||
#Server = http://pkg.adfinis-sygroup.ch/archlinux/$repo/os/$arch
|
||||
#Server = https://pkg.adfinis-sygroup.ch/archlinux/$repo/os/$arch
|
||||
#Server = http://archlinux.puzzle.ch/$repo/os/$arch
|
||||
|
||||
## Taiwan
|
||||
#Server = http://archlinux.cs.nctu.edu.tw/$repo/os/$arch
|
||||
#Server = http://shadow.ind.ntou.edu.tw/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.tku.edu.tw/Linux/ArchLinux/$repo/os/$arch
|
||||
#Server = http://ftp.yzu.edu.tw/Linux/archlinux/$repo/os/$arch
|
||||
|
||||
## Thailand
|
||||
#Server = http://mirror.adminbannok.com/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.kku.ac.th/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.kku.ac.th/archlinux/$repo/os/$arch
|
||||
|
||||
## Turkey
|
||||
#Server = http://ftp.linux.org.tr/archlinux/$repo/os/$arch
|
||||
|
||||
## Ukraine
|
||||
#Server = http://archlinux.ip-connect.vn.ua/$repo/os/$arch
|
||||
#Server = https://archlinux.ip-connect.vn.ua/$repo/os/$arch
|
||||
#Server = http://mirrors.nix.org.ua/linux/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.nix.org.ua/linux/archlinux/$repo/os/$arch
|
||||
|
||||
## United Kingdom
|
||||
#Server = http://mirror.bytemark.co.uk/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.manchester.m247.com/arch-linux/$repo/os/$arch
|
||||
#Server = http://www.mirrorservice.org/sites/ftp.archlinux.org/$repo/os/$arch
|
||||
#Server = http://arch.serverspace.co.uk/arch/$repo/os/$arch
|
||||
#Server = http://archlinux.mirrors.uk2.net/$repo/os/$arch
|
||||
|
||||
## United States
|
||||
#Server = http://mirrors.acm.wpi.edu/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.advancedhosters.com/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.aggregate.org/archlinux/$repo/os/$arch
|
||||
#Server = http://ca.us.mirror.archlinux-br.org/$repo/os/$arch
|
||||
#Server = http://il.us.mirror.archlinux-br.org/$repo/os/$arch
|
||||
#Server = http://archlinux.surlyjake.com/archlinux/$repo/os/$arch
|
||||
#Server = http://arlm.tyzoid.com/$repo/os/$arch
|
||||
#Server = http://mirror.as65535.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.cat.pdx.edu/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.cc.columbia.edu/pub/linux/archlinux/$repo/os/$arch
|
||||
#Server = http://arch.mirror.constant.com/$repo/os/$arch
|
||||
#Server = https://arch.mirror.constant.com/$repo/os/$arch
|
||||
#Server = http://cosmos.cites.illinois.edu/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.cs.pitt.edu/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.cs.vt.edu/pub/ArchLinux/$repo/os/$arch
|
||||
#Server = http://mirror.epiphyte.network/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.epiphyte.network/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.es.its.nyu.edu/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.gigenet.com/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.grig.io/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.grig.io/archlinux/$repo/os/$arch
|
||||
#Server = http://www.gtlib.gatech.edu/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror1.hackingand.coffee/arch/$repo/os/$arch
|
||||
#Server = http://mirror2.hackingand.coffee/arch/$repo/os/$arch
|
||||
#Server = http://mirror3.hackingand.coffee/arch/$repo/os/$arch
|
||||
#Server = http://mirror.htnshost.com/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.jmu.edu/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.kernel.org/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.us.leaseweb.net/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.us.leaseweb.net/archlinux/$repo/os/$arch
|
||||
#Server = http://il.mirrors.linaxe.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.liquidweb.com/archlinux/$repo/os/$arch
|
||||
#Server = http://arch.localmsp.org/arch/$repo/os/$arch
|
||||
#Server = https://arch.localmsp.org/arch/$repo/os/$arch
|
||||
#Server = http://mirror.lty.me/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.lty.me/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.lug.mtu.edu/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.lug.mtu.edu/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.math.princeton.edu/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.metrocast.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.kaminski.io/archlinux/$repo/os/$arch
|
||||
#Server = https://mirror.kaminski.io/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.nexcess.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.ocf.berkeley.edu/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.ocf.berkeley.edu/archlinux/$repo/os/$arch
|
||||
#Server = http://ftp.osuosl.org/pub/archlinux/$repo/os/$arch
|
||||
#Server = http://arch.mirrors.pair.com/$repo/os/$arch
|
||||
#Server = http://mirrors.rit.edu/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.rit.edu/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.rutgers.edu/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.rutgers.edu/archlinux/$repo/os/$arch
|
||||
#Server = https://mirrors.tuxns.net/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.umd.edu/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.vtti.vt.edu/archlinux/$repo/os/$arch
|
||||
#Server = http://mirrors.xmission.com/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror.yellowfiber.net/archlinux/$repo/os/$arch
|
||||
|
||||
## Vietnam
|
||||
#Server = http://f.archlinuxvn.org/archlinux/$repo/os/$arch
|
||||
#Server = http://mirror-fpt-telecom.fpt.net/archlinux/$repo/os/$arch
|
||||
|
||||
|
|
@ -1,412 +0,0 @@
|
|||
# For more information on configuration, see:
|
||||
# * Official English Documentation: http://nginx.org/en/docs/
|
||||
# * Official Russian Documentation: http://nginx.org/ru/docs/
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
|
||||
include /usr/share/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 4096;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Load modular configuration files from the /etc/nginx/conf.d directory.
|
||||
# See http://nginx.org/en/docs/ngx_core_module.html#include
|
||||
# for more information.
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name .jaseg.net;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2 default_server;
|
||||
listen [::]:443 ssl http2 default_server;
|
||||
server_name gerbolyze.jaseg.net;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
ssl_certificate "/etc/letsencrypt/live/gerbolyze.jaseg.net/fullchain.pem";
|
||||
ssl_certificate_key "/etc/letsencrypt/live/gerbolyze.jaseg.net/privkey.pem";
|
||||
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
resolver 67.207.67.2 67.207.67.3 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=86400";
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location ^~ /static/ {
|
||||
root /var/lib/gerboweb;
|
||||
}
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/run/uwsgi/gerboweb.socket;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name blog.jaseg.net;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
ssl_certificate "/etc/letsencrypt/live/blog.jaseg.net/fullchain.pem";
|
||||
ssl_certificate_key "/etc/letsencrypt/live/blog.jaseg.net/privkey.pem";
|
||||
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
resolver 67.207.67.2 67.207.67.3 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=86400";
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location / {
|
||||
root /var/www/blog.jaseg.net;
|
||||
}
|
||||
|
||||
location /d/ {
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
rewrite ^/d/(.*)$ /$1 break;
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/run/uwsgi/secure-download.socket;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name automation.jaseg.de;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
ssl_certificate "/etc/letsencrypt/live/automation.jaseg.de/fullchain.pem";
|
||||
ssl_certificate_key "/etc/letsencrypt/live/automation.jaseg.de/privkey.pem";
|
||||
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
resolver 67.207.67.2 67.207.67.3 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=86400";
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/run/uwsgi/notification-proxy.socket;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name kochbuch.jaseg.net;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
ssl_certificate "/etc/letsencrypt/live/kochbuch.jaseg.net/fullchain.pem";
|
||||
ssl_certificate_key "/etc/letsencrypt/live/kochbuch.jaseg.net/privkey.pem";
|
||||
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
resolver 67.207.67.2 67.207.67.3 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=86400";
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location / {
|
||||
auth_basic "blubb";
|
||||
auth_basic_user_file /etc/nginx/kochbuch.htpasswd;
|
||||
root /var/www/kochbuch.jaseg.net;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name pogojig.jaseg.net;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
ssl_certificate "/etc/letsencrypt/live/pogojig.jaseg.net/fullchain.pem";
|
||||
ssl_certificate_key "/etc/letsencrypt/live/pogojig.jaseg.net/privkey.pem";
|
||||
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
resolver 67.207.67.2 67.207.67.3 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
client_max_body_size 10M;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=86400";
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location ^~ /pogospace/ {
|
||||
root /var/lib/pogojig/pogospace;
|
||||
}
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/run/uwsgi/pogojig.socket;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name tracespace.jaseg.net;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
ssl_certificate "/etc/letsencrypt/live/tracespace.jaseg.net/fullchain.pem";
|
||||
ssl_certificate_key "/etc/letsencrypt/live/tracespace.jaseg.net/privkey.pem";
|
||||
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
resolver 67.207.67.2 67.207.67.3 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=86400";
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location / {
|
||||
root /var/www/tracespace.jaseg.net;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name openjscad.jaseg.net;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
ssl_certificate "/etc/letsencrypt/live/openjscad.jaseg.net/fullchain.pem";
|
||||
ssl_certificate_key "/etc/letsencrypt/live/openjscad.jaseg.net/privkey.pem";
|
||||
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
resolver 67.207.67.2 67.207.67.3 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=86400";
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location / {
|
||||
root /var/www/openjscad.jaseg.net;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name git.jaseg.net;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
ssl_certificate "/etc/letsencrypt/live/git.jaseg.net/fullchain.pem";
|
||||
ssl_certificate_key "/etc/letsencrypt/live/git.jaseg.net/privkey.pem";
|
||||
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
resolver 67.207.67.2 67.207.67.3 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=86400";
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location ~ ^/(cgit.css|robots.txt) {
|
||||
root /usr/share/cgit;
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
location ~ ^/(cgit.png|favicon.png) {
|
||||
alias /var/www/git.jaseg.net/cgit.png;
|
||||
}
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
uwsgi_modifier1 9;
|
||||
uwsgi_pass unix:/run/uwsgi/cgit.socket;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name dyndns.jaseg.de;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
ssl_certificate "/etc/letsencrypt/live/dyndns.jaseg.de/fullchain.pem";
|
||||
ssl_certificate_key "/etc/letsencrypt/live/dyndns.jaseg.de/privkey.pem";
|
||||
ssl_dhparam "/etc/letsencrypt/ssl-dhparams.pem";
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
resolver 67.207.67.2 67.207.67.3 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=86400";
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/run/uwsgi/dyndns.socket;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# For more information on configuration, see:
|
||||
# * Official English Documentation: http://nginx.org/en/docs/
|
||||
# * Official Russian Documentation: http://nginx.org/ru/docs/
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
|
||||
include /usr/share/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 4096;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Load modular configuration files from the /etc/nginx/conf.d directory.
|
||||
# See http://nginx.org/en/docs/ngx_core_module.html#include
|
||||
# for more information.
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name gerbolyze.jaseg.net;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name blog.jaseg.net;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
import smtplib
|
||||
import ssl
|
||||
import email.utils
|
||||
import hmac
|
||||
from email.mime.text import MIMEText
|
||||
from datetime import datetime
|
||||
import time
|
||||
import functools
|
||||
import json
|
||||
import binascii
|
||||
import uwsgidecorators
|
||||
|
||||
import sqlite3
|
||||
|
||||
from flask import Flask, request, abort
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_pyfile('config.py')
|
||||
|
||||
db = sqlite3.connect(app.config['SQLITE_DB'], check_same_thread=False)
|
||||
with db as conn:
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS seqs_seen
|
||||
(route_name TEXT PRIMARY KEY,
|
||||
seq INTEGER)''')
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS time_seen
|
||||
(route_name TEXT PRIMARY KEY)''')
|
||||
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS heartbeats_seen
|
||||
(route_name TEXT PRIMARY KEY,
|
||||
timestamp INTEGER,
|
||||
notified INTEGER)''')
|
||||
# Clear table on startup to avoid spurious notifications
|
||||
conn.execute('''DELETE FROM heartbeats_seen''')
|
||||
|
||||
mail_routes = {}
|
||||
|
||||
def mail_route(name, receiver, secret):
|
||||
def wrap(func):
|
||||
global routes
|
||||
mail_routes[name] = (receiver, func, secret)
|
||||
return func
|
||||
return wrap
|
||||
|
||||
|
||||
def authenticate(route_name, secret, clock_delta_tolerance:'s'=120):
|
||||
with db as conn:
|
||||
if not request.is_json:
|
||||
print('Rejecting notification: Incorrect content type')
|
||||
abort(400)
|
||||
|
||||
if not 'auth' in request.json and 'payload' in request.json:
|
||||
print('Rejecting notification: signature or payload not found')
|
||||
abort(400)
|
||||
|
||||
if not isinstance(request.json['auth'], str):
|
||||
print('Rejecting notification: signature is of incorrect type')
|
||||
abort(400)
|
||||
their_digest = binascii.unhexlify(request.json['auth'])
|
||||
|
||||
our_digest = hmac.digest(secret.encode('utf-8'), request.json['payload'].encode('utf-8'), 'sha256')
|
||||
if not hmac.compare_digest(their_digest, our_digest):
|
||||
print('Rejecting notification: Incorrect signature')
|
||||
abort(403)
|
||||
|
||||
try:
|
||||
payload = json.loads(request.json['payload'])
|
||||
except:
|
||||
print('Rejecting notification: Payload is not JSON')
|
||||
abort(400)
|
||||
|
||||
last_seqnum = conn.execute('SELECT seq FROM seqs_seen WHERE route_name = ?', (route_name,)).fetchone() or 0
|
||||
# We can check for seq here: Only an attacker with knowledge of the secret would be able to remove
|
||||
# seq from a message. This means for a single key, only messages with or without seq may ever be used.
|
||||
if 'seq' in payload:
|
||||
seq = payload['seq']
|
||||
if not isinstance(seq, int):
|
||||
print('Rejecting notification: seq of wrong type')
|
||||
abort(400)
|
||||
|
||||
if seq <= last_seqnum:
|
||||
print('Rejecting notification: seq out of order')
|
||||
abort(400)
|
||||
|
||||
conn.execute('INSERT OR REPLACE INTO seqs_seen VALUES (?, ?)', (route_name, seq))
|
||||
|
||||
elif last_seqnum:
|
||||
print('Rejecting notification: seq not included but past messages included seq')
|
||||
abort(400)
|
||||
|
||||
msg_time = None
|
||||
if 'time' in payload:
|
||||
msg_time = payload['time']
|
||||
if not isinstance(msg_time, int):
|
||||
print('Rejecting notification: time of wrong type')
|
||||
abort(400)
|
||||
|
||||
if abs(msg_time - int(time.time())) > clock_delta_tolerance:
|
||||
print('Rejecting notification: timestamp too far in the future or past')
|
||||
abort(400)
|
||||
|
||||
conn.execute('INSERT OR REPLACE INTO time_seen VALUES (?)', (route_name,))
|
||||
|
||||
elif conn.execute('SELECT * FROM time_seen WHERE route_name = ?', (route_name,)).fetchone():
|
||||
print('Rejecting notification: time not included but past messages included time')
|
||||
abort(400)
|
||||
|
||||
if msg_time is None:
|
||||
msg_time = int(time.time())
|
||||
|
||||
return msg_time, payload['scope'], payload['d']
|
||||
|
||||
@mail_route('klingel', 'computerstuff@jaseg.de', app.config['SECRET_KLINGEL'])
|
||||
def klingel(classification='somewhere', rms=None, capture=None, **kwargs):
|
||||
return (f'It rang {classification}!',
|
||||
f'rms={rms}\ncapture={capture}\nextra_args={kwargs}')
|
||||
|
||||
|
||||
def send_mail(route_name, receiver, subject, body):
|
||||
try:
|
||||
context = ssl.create_default_context()
|
||||
smtp = smtplib.SMTP_SSL(app.config['SMTP_HOST'], app.config['SMTP_PORT'])
|
||||
smtp.login('apikey', app.config['SENDGRID_APIKEY'])
|
||||
|
||||
sender = f'{route_name}@{app.config["DOMAIN"]}'
|
||||
|
||||
msg = MIMEText(body)
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = sender
|
||||
msg['To'] = receiver
|
||||
msg['Date'] = email.utils.formatdate()
|
||||
|
||||
smtp.sendmail(sender, receiver, msg.as_string())
|
||||
finally:
|
||||
smtp.quit()
|
||||
|
||||
@app.route('/v1/notify/<route_name>', methods=['POST'])
|
||||
def notify(route_name):
|
||||
receiver, func, secret = mail_routes[route_name]
|
||||
msg_time, scope, kwargs = authenticate(route_name, secret)
|
||||
|
||||
if scope == 'default':
|
||||
# Exceptions will yield a 500 error
|
||||
subject, body = func(**kwargs)
|
||||
send_mail(route_name, receiver, subject, body or 'empty message')
|
||||
|
||||
elif scope == 'info':
|
||||
send_mail(route_name, receiver, f'System info: {kwargs["info_msg"]}', f'Logged data: {kwargs}')
|
||||
|
||||
elif scope == 'boot':
|
||||
formatted = datetime.utcfromtimestamp(msg_time).isoformat()
|
||||
send_mail(route_name, receiver, 'System startup', f'System powered up at {formatted}')
|
||||
|
||||
elif scope == 'heartbeat':
|
||||
with db as conn:
|
||||
conn.execute('INSERT OR REPLACE INTO heartbeats_seen VALUES (?, ?, 0)', (route_name, int(time.time())))
|
||||
|
||||
elif scope == 'error':
|
||||
print(f'Device error: {kwargs}')
|
||||
|
||||
return 'success'
|
||||
|
||||
@uwsgidecorators.timer(60)
|
||||
def heartbeat_timer(_uwsgi_signum):
|
||||
threshold = int(time.time()) - app.config['HEARTBEAT_TIMEOUT']
|
||||
with db as conn:
|
||||
for route, ts in db.execute(
|
||||
'SELECT route_name, timestamp FROM heartbeats_seen WHERE timestamp <= ? AND notified == 0',
|
||||
(threshold,)).fetchall():
|
||||
print(f'Heartbeat expired for {route}: {ts} < {threshold}')
|
||||
|
||||
receiver, *_ = mail_routes[route]
|
||||
last = datetime.utcfromtimestamp(ts).isoformat()
|
||||
|
||||
send_mail(route, receiver, 'Heartbeat timeout', f'Last heartbeat at {last}')
|
||||
db.execute('UPDATE heartbeats_seen SET notified = ? WHERE route_name = ?', (int(time.time()), route))
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
SENDGRID_APIKEY = '{{lookup('file', 'notification_proxy_sendgrid_apikey.txt')}}'
|
||||
DOMAIN = 'automation.jaseg.de'
|
||||
SMTP_HOST = "smtp.sendgrid.net"
|
||||
SMTP_PORT = 465
|
||||
HEARTBEAT_TIMEOUT = 300
|
||||
SQLITE_DB = '{{notification_proxy_sqlite_dbfile}}'
|
||||
|
||||
SECRET_KLINGEL = '{{lookup('password', 'notification_proxy_klingel_secret.txt length=32')}}'
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
- name: DNS setup
|
||||
hosts: localhost
|
||||
module_defaults:
|
||||
inwx:
|
||||
username: "{{lookup('ini', 'user section=inwx file=credentials.ini')}}"
|
||||
password: "{{lookup('ini', 'pass section=inwx file=credentials.ini')}}"
|
||||
vars:
|
||||
subdomains:
|
||||
- git.jaseg.net
|
||||
- blog.jaseg.net
|
||||
- kochbuch.jaseg.net
|
||||
- gerbolyze.jaseg.net
|
||||
- tracespace.jaseg.net
|
||||
- openjscad.jaseg.net
|
||||
- pogojig.jaseg.net
|
||||
- automation.jaseg.de
|
||||
- dyndns.jaseg.de
|
||||
fastmail_domains:
|
||||
- jaseg.net
|
||||
- jaseg.de
|
||||
tasks:
|
||||
- name: Gather wendelstein facts
|
||||
setup:
|
||||
delegate_to: wendelstein
|
||||
delegate_facts: True
|
||||
|
||||
- name: Setup DNS
|
||||
include_tasks: dns.yml
|
||||
|
||||
|
||||
- name: Wendelstein setup
|
||||
hosts: wendelstein
|
||||
tasks:
|
||||
- name: Set hostname
|
||||
hostname:
|
||||
name: wendelstein.jaseg.net
|
||||
|
||||
- name: Install common admin tools
|
||||
dnf:
|
||||
name: htop,tmux,fish,mosh,neovim,sqlite
|
||||
state: latest
|
||||
|
||||
- name: Install host requisites
|
||||
dnf:
|
||||
name: nginx,uwsgi,python3-flask,python3-flask-wtf,uwsgi-plugin-python3,certbot,python3-certbot-nginx,libselinux-python,git,iptables-services,python3-pycryptodomex,zip,python3-uwsgidecorators,nsd
|
||||
state: latest
|
||||
|
||||
- name: Disable password-based root login
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: '^PermitRootLogin'
|
||||
line: 'PermitRootLogin without-password'
|
||||
register: disable_root_pw_ssh
|
||||
|
||||
- name: Restart sshd
|
||||
systemd:
|
||||
name: sshd
|
||||
state: restarted
|
||||
when: disable_root_pw_ssh is changed
|
||||
|
||||
- name: Configure iptables firewall service
|
||||
copy:
|
||||
src: iptables.rules
|
||||
dest: /etc/sysconfig/iptables
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0664
|
||||
|
||||
- name: Enable iptables firewall service
|
||||
systemd:
|
||||
name: iptables
|
||||
enabled: yes
|
||||
state: started
|
||||
|
||||
- name: Create containers
|
||||
include_tasks: setup_containers.yml
|
||||
vars:
|
||||
containers:
|
||||
- gerboweb
|
||||
- clippy
|
||||
- pogojig
|
||||
|
||||
- name: Setup web server
|
||||
include_tasks: setup_webserver.yml
|
||||
|
||||
- name: Setup gerboweb
|
||||
include_tasks: setup_gerboweb.yml
|
||||
|
||||
- name: Setup clippy
|
||||
include_tasks: setup_clippy.yml
|
||||
|
||||
- name: Setup secure download
|
||||
include_tasks: setup_secure_download.yml
|
||||
|
||||
- name: Setup tracespace
|
||||
include_tasks: setup_tracespace.yml
|
||||
|
||||
- name: Setup openjscad
|
||||
include_tasks: setup_openjscad.yml
|
||||
|
||||
- name: Setup pogojig
|
||||
include_tasks: setup_pogojig.yml
|
||||
|
||||
- name: Setup notification proxy
|
||||
include_tasks: setup_notification_proxy.yml
|
||||
|
||||
- name: Setup semi-public git server
|
||||
include_tasks: setup_git.yml
|
||||
|
||||
- name: Setup private DynDNS service
|
||||
include_tasks: setup_dyndns.yml
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
[Unit]
|
||||
Description=Pogojig render job processor
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/var/lib/pogojig
|
||||
ExecStart=/usr/bin/python3 job_processor.py {{pogojig_cache}}/job_queue.sqlite3
|
||||
|
||||
[Install]
|
||||
WantedBy=uwsgi-app@pogojig.service
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
MAX_CONTENT_LENGTH=10000000
|
||||
SECRET_KEY="{{lookup('password', 'pogojig_flask_secret.txt length=32')}}"
|
||||
UPLOAD_PATH="{{pogojig_cache}}/upload"
|
||||
JOB_QUEUE_DB="{{pogojig_cache}}/job_queue.sqlite3"
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
[ $# != 1 ] && exit 1
|
||||
ID=$1
|
||||
egrep -x -q '^[-0-9A-Za-z]{36}$'<<<"$ID" || exit 2
|
||||
|
||||
systemd-nspawn \
|
||||
-D {{pogojig_root}} \
|
||||
-x --bind={{pogojig_cache}}/upload/$ID:/mnt \
|
||||
/bin/sh -c "set -euo pipefail
|
||||
cd /mnt
|
||||
|
||||
date; echo 'Cleaning up previous output'
|
||||
rm -rf pcb_shape.dxf jig.stl kicad kicad.zip sources.zip
|
||||
|
||||
date; echo 'Rendering'
|
||||
cp -r /var/lib/pogojig_renderer sources
|
||||
cp input.svg sources/
|
||||
make -C sources
|
||||
|
||||
date; echo 'Packing source bundle'
|
||||
cp -r sources/out/pcb_shape.dxf sources/out/jig.stl sources/out/kicad ./
|
||||
zip -r sources.zip sources
|
||||
zip -r kicad.zip kicad
|
||||
rm -rf sources"
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
[ $# != 1 ] && exit 1
|
||||
ID=$1
|
||||
egrep -x -q '^[-0-9A-Za-z]{36}$'<<<"$ID" || exit 2
|
||||
|
||||
systemd-nspawn \
|
||||
-D {{gerboweb_root}} \
|
||||
-x --bind={{gerboweb_cache}}/upload/$ID:/mnt \
|
||||
/bin/sh -c "set -euo pipefail
|
||||
unzip -j -d /tmp/gerber /mnt/gerber.zip
|
||||
rm -f /mnt/render_top.png /mnt/render_bottom.png /mnt/render_top.small.png /mnt/render_bottom.small.png
|
||||
date; echo 'Rendering bottom layer'
|
||||
gerbolyze render top /tmp/gerber /mnt/render_top.png
|
||||
date; echo 'Scaling down'
|
||||
convert /mnt/render_top.png -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/render_top.small.png
|
||||
date; echo 'Rendering top layer'
|
||||
gerbolyze render bottom /tmp/gerber /mnt/render_bottom.png
|
||||
date; echo 'Scaling down'
|
||||
convert /mnt/render_bottom.png -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/render_bottom.small.png"
|
||||
|
|
@ -1 +0,0 @@
|
|||
SERVE_PATH="{{secure_download_dir}}"
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
---
|
||||
- name: Clone pixelterm git
|
||||
git:
|
||||
repo: https://github.com/jaseg/pixelterm
|
||||
dest: "{{clippy_root}}/var/lib/pixelterm.git"
|
||||
|
||||
- name: Clone clippy git
|
||||
git:
|
||||
repo: https://github.com/jaseg/clippy
|
||||
dest: "{{clippy_root}}/var/lib/clippy.git"
|
||||
|
||||
- name: Setup required packages for clippy
|
||||
command: arch-chroot "{{clippy_root}}" pacman -Syu --noconfirm python3 python-pip python-numpy python-pillow
|
||||
|
||||
- name: Setup pixelterm
|
||||
command: arch-chroot "{{clippy_root}}" sh -c "cd /var/lib/pixelterm.git && python3 setup.py install"
|
||||
|
||||
- name: Setup container clippy systemd service file
|
||||
template:
|
||||
src: clippy.service.j2
|
||||
dest: "{{clippy_root}}/etc/systemd/system/clippy.service"
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0664
|
||||
|
||||
- name: Enable systemd machines target
|
||||
systemd:
|
||||
name: machines.target
|
||||
enabled: yes
|
||||
|
||||
- name: Copy over clippy container auto boot service file
|
||||
copy:
|
||||
src: clippy-nspawn.service
|
||||
dest: /etc/systemd/system/clippy-nspawn.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0664
|
||||
|
||||
- name: Create systemd-nspawn config dir
|
||||
file:
|
||||
path: /etc/systemd/nspawn
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0775
|
||||
|
||||
- name: Copy over clippy container config
|
||||
copy:
|
||||
src: clippy.nspawn
|
||||
dest: /etc/systemd/nspawn/clippy.nspawn
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0664
|
||||
|
||||
- name: Enable clippy container auto boot
|
||||
systemd:
|
||||
daemon-reload: yes
|
||||
name: clippy-nspawn.service
|
||||
enabled: yes
|
||||
|
||||
- name: Restart clippy container
|
||||
shell: |
|
||||
systemctl stop clippy-nspawn
|
||||
sleep 1
|
||||
systemctl start clippy-nspawn
|
||||
for x in $(seq 0 30); do
|
||||
systemctl -M clippy is-system-running && exit
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Enable clippy systemd service in container
|
||||
command: systemctl enable -M clippy clippy.service
|
||||
|
||||
- name: Restart clippy systemd service in container
|
||||
command: systemctl restart -M clippy clippy.service
|
||||
|
||||
#- name: Enable host networkd
|
||||
# systemd:
|
||||
# name: systemd-networkd
|
||||
# enabled: yes
|
||||
# state: started
|
||||
|
||||
#- name: Enable clippy container networkd
|
||||
# command: systemctl enable -M clippy systemd-networkd
|
||||
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
- name: Install host requisites
|
||||
dnf:
|
||||
name: btrfs-progs,arch-install-scripts,systemd-container,libselinux-python
|
||||
state: latest
|
||||
|
||||
- name: Create individual containers
|
||||
include_tasks: bootstrap_arch_container.yml
|
||||
with_items: "{{ containers }}"
|
||||
loop_control:
|
||||
loop_var: container
|
||||
|
||||
- name: Cleanup bootstrap image
|
||||
file:
|
||||
path: /tmp/arch-bootstrap.tar.xz
|
||||
state: absent
|
||||
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
---
|
||||
- name: Set local facts
|
||||
set_fact:
|
||||
gerboweb_cache: /var/cache/gerboweb
|
||||
|
||||
- name: Copy render script
|
||||
template:
|
||||
src: render.sh.j2
|
||||
dest: /usr/local/sbin/gerbolyze_render.sh
|
||||
mode: ug+x
|
||||
|
||||
- name: Copy vector script
|
||||
template:
|
||||
src: vector.sh.j2
|
||||
dest: /usr/local/sbin/gerbolyze_vector.sh
|
||||
mode: ug+x
|
||||
|
||||
- name: Install packages into gerbolyze container
|
||||
shell: arch-chroot "{{gerboweb_root}}" pacman -Syu --noconfirm python3 opencv hdf5 gtk3 python-numpy python-pip imagemagick unzip zip
|
||||
|
||||
- name: Workaround for cairoffi problem
|
||||
shell: arch-chroot "{{gerboweb_root}}" pip install -U --upgrade-strategy=eager wheel
|
||||
|
||||
# TODO maybe install directly from local git checkout?
|
||||
- name: Install gerbolyze
|
||||
shell: arch-chroot "{{gerboweb_root}}" pip install -U --upgrade-strategy=eager gerbolyze
|
||||
|
||||
- name: Copy webapp sources
|
||||
synchronize:
|
||||
# FIXME: make this path configurable
|
||||
src: ~/gerbolyze/gerboweb/
|
||||
dest: /var/lib/gerboweb/
|
||||
rsync_opts:
|
||||
- "--exclude=/deploy"
|
||||
group: no
|
||||
owner: no
|
||||
|
||||
- name: Create uwsgi worker user and group
|
||||
user:
|
||||
name: uwsgi-gerboweb
|
||||
create_home: no
|
||||
group: uwsgi
|
||||
password: '!'
|
||||
shell: /sbin/nologin
|
||||
system: yes
|
||||
|
||||
- name: Template webapp config
|
||||
template:
|
||||
src: gerboweb.cfg.j2
|
||||
dest: /var/lib/gerboweb/gerboweb_prod.cfg
|
||||
owner: uwsgi-gerboweb
|
||||
group: root
|
||||
mode: 0660
|
||||
|
||||
- name: Copy uwsgi config
|
||||
copy:
|
||||
src: uwsgi-gerboweb.ini
|
||||
dest: /etc/uwsgi.d/gerboweb.ini
|
||||
owner: uwsgi-gerboweb
|
||||
group: uwsgi
|
||||
mode: 0440
|
||||
|
||||
- name: Copy job processor systemd service config
|
||||
template:
|
||||
src: gerboweb-job-processor.service.j2
|
||||
dest: /etc/systemd/system/gerboweb-job-processor.service
|
||||
|
||||
- name: Enable uwsgi systemd socket
|
||||
systemd:
|
||||
daemon-reload: yes
|
||||
name: uwsgi-app@gerboweb.socket
|
||||
enabled: yes
|
||||
|
||||
- name: Copy gerboweb cache dir tmpfiles.d config
|
||||
template:
|
||||
src: tmpfiles-gerboweb.conf.j2
|
||||
dest: /etc/tmpfiles.d/gerboweb.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0644
|
||||
register: tmpfiles_config
|
||||
|
||||
- name: Kick systemd tmpfiles service to create cache dir
|
||||
command: systemd-tmpfiles --create
|
||||
when: tmpfiles_config is changed
|
||||
|
||||
- name: Create job queue db
|
||||
file:
|
||||
path: "{{gerboweb_cache}}/job_queue.sqlite3"
|
||||
owner: root
|
||||
group: uwsgi
|
||||
mode: 0660
|
||||
state: touch
|
||||
|
||||
- name: Enable and launch job processor
|
||||
systemd:
|
||||
name: gerboweb-job-processor.service
|
||||
enabled: yes
|
||||
state: restarted
|
||||
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
- name: Install host requisites
|
||||
dnf:
|
||||
name: cgit,gitolite3,python3-pygments,python3-docutils,nodejs-markdown
|
||||
state: latest
|
||||
|
||||
- name: Copy cgit favicon
|
||||
copy:
|
||||
src: cgit-logo.png
|
||||
dest: /var/www/git.jaseg.net/cgit.png
|
||||
|
||||
- name: Create cgit instance config dir
|
||||
file:
|
||||
path: /var/lib/cgit
|
||||
state: directory
|
||||
mode: 0755
|
||||
|
||||
- name: Copy cgit rc
|
||||
copy:
|
||||
src: cgitrc
|
||||
dest: /var/lib/cgit/cgitrc-gitolite-public
|
||||
mode: 0644
|
||||
|
||||
- name: Create uwsgi worker user and group
|
||||
user:
|
||||
name: uwsgi-cgit
|
||||
create_home: no
|
||||
group: uwsgi
|
||||
password: '!'
|
||||
shell: /sbin/nologin
|
||||
system: yes
|
||||
|
||||
- name: Copy uwsgi config
|
||||
copy:
|
||||
src: uwsgi-cgit.ini
|
||||
dest: /etc/uwsgi.d/cgit.ini
|
||||
owner: uwsgi-cgit
|
||||
group: uwsgi
|
||||
mode: 0440
|
||||
|
||||
- name: Enable uwsgi systemd socket
|
||||
systemd:
|
||||
daemon-reload: yes
|
||||
name: uwsgi-app@cgit.socket
|
||||
enabled: yes
|
||||
|
||||
- name: Copy gitolite admin pubkey
|
||||
copy:
|
||||
src: ~/.ssh/id_ed25519.gitolite.pub
|
||||
dest: /tmp/jaseg-gitolite.pub
|
||||
owner: gitolite3
|
||||
group: gitolite3
|
||||
|
||||
- name: Run gitolite initialization
|
||||
command: gitolite setup -pk /tmp/jaseg-gitolite.pub
|
||||
become: true
|
||||
become_method: su
|
||||
become_user: gitolite3
|
||||
become_flags: '-s /bin/sh'
|
||||
args:
|
||||
creates: /var/lib/gitolite3/projects.list
|
||||
|
||||
- name: Remove leftover admin pubkey
|
||||
file:
|
||||
state: absent
|
||||
path: /tmp/jaseg-gitolite.pub
|
||||
|
||||
- name: Allow uwsgi group to access gitolite repo dir
|
||||
file:
|
||||
path: /var/lib/gitolite3
|
||||
state: directory
|
||||
owner: gitolite3
|
||||
group: uwsgi
|
||||
|
||||
- name: Add cgit uwsgi user to gitolite group
|
||||
user:
|
||||
name: uwsgi-cgit
|
||||
groups: gitolite3
|
||||
append: yes
|
||||
|
||||
- name: Allow cgit uwsgi user to access gitolite repos
|
||||
file:
|
||||
path: /var/lib/gitolite3/repositories
|
||||
mode: 0750
|
||||
|
||||
- name: Allow cgit uwsgi user to gitolite repo list
|
||||
file:
|
||||
path: /var/lib/gitolite3/projects.list
|
||||
mode: 0640
|
||||
|
||||
- name: Copy gitolite rc
|
||||
copy:
|
||||
src: gitolite.rc
|
||||
dest: /var/lib/gitolite3/.gitolite.rc
|
||||
owner: gitolite3
|
||||
group: gitolite3
|
||||
mode: 0600
|
||||
|
||||
- name: Query system user account info
|
||||
getent:
|
||||
database: passwd
|
||||
key: gitolite3
|
||||
|
||||
- name: Create git alias user
|
||||
user:
|
||||
name: git
|
||||
create_home: no
|
||||
group: gitolite3
|
||||
password: '!'
|
||||
comment: Alias for gitolite3 user
|
||||
shell: "{{ getent_passwd['gitolite3'][5] }}"
|
||||
system: yes
|
||||
non_unique: yes
|
||||
home: "{{ getent_passwd['gitolite3'][4] }}"
|
||||
uid: "{{ getent_passwd['gitolite3'][1] }}"
|
||||
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
---
|
||||
- name: Set local facts
|
||||
set_fact:
|
||||
notification_proxy_sqlite_dbfile: /var/lib/notification-proxy/db.sqlite3
|
||||
|
||||
- name: Create notification proxy worker user and group
|
||||
user:
|
||||
name: uwsgi-notification-proxy
|
||||
create_home: no
|
||||
group: uwsgi
|
||||
password: '!'
|
||||
shell: /sbin/nologin
|
||||
system: yes
|
||||
|
||||
- name: Create webapp dir
|
||||
file:
|
||||
path: /var/lib/notification-proxy
|
||||
state: directory
|
||||
owner: uwsgi-notification-proxy
|
||||
group: uwsgi
|
||||
mode: 0750
|
||||
|
||||
- name: Copy webapp sources
|
||||
copy:
|
||||
src: notification_proxy.py
|
||||
dest: /var/lib/notification-proxy/
|
||||
owner: uwsgi-notification-proxy
|
||||
group: uwsgi
|
||||
mode: 0440
|
||||
|
||||
- name: Template webapp config
|
||||
template:
|
||||
src: notification_proxy_config.py.j2
|
||||
dest: /var/lib/notification-proxy/config.py
|
||||
owner: uwsgi-notification-proxy
|
||||
group: root
|
||||
mode: 0660
|
||||
|
||||
- name: Copy uwsgi config
|
||||
copy:
|
||||
src: uwsgi-notification-proxy.ini
|
||||
dest: /etc/uwsgi.d/notification-proxy.ini
|
||||
owner: uwsgi-notification-proxy
|
||||
group: uwsgi
|
||||
mode: 0440
|
||||
|
||||
- name: Enable uwsgi systemd socket
|
||||
systemd:
|
||||
daemon-reload: yes
|
||||
name: uwsgi-app@notification-proxy.socket
|
||||
enabled: yes
|
||||
|
||||
- name: Create sqlite db file
|
||||
file:
|
||||
path: "{{notification_proxy_sqlite_dbfile}}"
|
||||
owner: uwsgi-notification-proxy
|
||||
group: uwsgi
|
||||
mode: 0660
|
||||
state: touch
|
||||
|
||||
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
- name: Copy openjscad webapp sources
|
||||
synchronize:
|
||||
# FIXME: make this path configurable
|
||||
src: ~/openjscad_dist/
|
||||
dest: /var/www/openjscad.jaseg.net/
|
||||
group: no
|
||||
owner: no
|
||||
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
---
|
||||
- name: Set local facts
|
||||
set_fact:
|
||||
pogojig_cache: /var/cache/pogojig
|
||||
|
||||
- name: Copy render script
|
||||
template:
|
||||
src: pogojig_generate.sh.j2
|
||||
dest: /usr/local/sbin/pogojig_generate.sh
|
||||
mode: ug+x
|
||||
|
||||
- name: Install packages into pogojig container
|
||||
shell: arch-chroot "{{pogojig_root}}" pacman -Syu --noconfirm python3 python-pip imagemagick unzip zip openscad inkscape make python-lxml xorg-server-xvfb
|
||||
|
||||
- name: Install python dependencies into pogojig container
|
||||
shell: arch-chroot "{{pogojig_root}}" pip install -U --upgrade-strategy=eager ezdxf xvfbwrapper
|
||||
|
||||
- name: Install pogojig
|
||||
synchronize:
|
||||
# FIXME: make this path configurable
|
||||
src: checkouts/pogojig/renderer/
|
||||
dest: "{{pogojig_root}}/var/lib/pogojig_renderer"
|
||||
group: no
|
||||
|
||||
- name: Copy webapp sources
|
||||
synchronize:
|
||||
# FIXME: make this path configurable
|
||||
src: checkouts/pogojig/webapp/
|
||||
dest: /var/lib/pogojig
|
||||
delete: true
|
||||
group: no
|
||||
owner: no
|
||||
|
||||
- name: Pack makefile template zip
|
||||
archive:
|
||||
path: "{{pogojig_root}}/var/lib/pogojig_renderer"
|
||||
dest: /var/lib/pogojig/static/pogojig_makefile_template.zip
|
||||
format: zip
|
||||
|
||||
- name: Create web home for modified tracespace
|
||||
file:
|
||||
path: /var/lib/pogojig/pogospace
|
||||
state: directory
|
||||
owner: nginx
|
||||
group: nginx
|
||||
mode: 0550
|
||||
|
||||
- name: Unpack modified tracespace sources
|
||||
unarchive:
|
||||
src: resource/pogojig-tracespace.tar.gz
|
||||
dest: /var/lib/pogojig/pogospace
|
||||
extra_opts: [--strip-components=1]
|
||||
owner: nginx
|
||||
group: nginx
|
||||
|
||||
- name: Create uwsgi worker user and group
|
||||
user:
|
||||
name: uwsgi-pogojig
|
||||
create_home: no
|
||||
group: uwsgi
|
||||
password: '!'
|
||||
shell: /sbin/nologin
|
||||
system: yes
|
||||
|
||||
- name: Template webapp config
|
||||
template:
|
||||
src: pogojig.cfg.j2
|
||||
dest: /var/lib/pogojig/pogojig_prod.cfg
|
||||
owner: uwsgi-pogojig
|
||||
group: root
|
||||
mode: 0660
|
||||
|
||||
- name: Copy uwsgi config
|
||||
copy:
|
||||
src: uwsgi-pogojig.ini
|
||||
dest: /etc/uwsgi.d/pogojig.ini
|
||||
owner: uwsgi-pogojig
|
||||
group: uwsgi
|
||||
mode: 440
|
||||
|
||||
- name: Copy job processor systemd service config
|
||||
template:
|
||||
src: pogojig-job-processor.service.j2
|
||||
dest: /etc/systemd/system/pogojig-job-processor.service
|
||||
|
||||
- name: Enable uwsgi systemd socket
|
||||
systemd:
|
||||
daemon-reload: yes
|
||||
name: uwsgi-app@pogojig.socket
|
||||
enabled: yes
|
||||
|
||||
# FIXME the socket doesn't seem to work properly
|
||||
- name: Enable uwsgi systemd service
|
||||
systemd:
|
||||
daemon-reload: yes
|
||||
name: uwsgi-app@pogojig.service
|
||||
enabled: yes
|
||||
|
||||
- name: Copy pogojig cache dir tmpfiles.d config
|
||||
template:
|
||||
src: tmpfiles-pogojig.conf.j2
|
||||
dest: /etc/tmpfiles.d/pogojig.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0644
|
||||
register: pogojig_tmpfiles_config
|
||||
|
||||
- name: Kick systemd tmpfiles service to create cache dir
|
||||
command: systemd-tmpfiles --create
|
||||
when: pogojig_tmpfiles_config is changed
|
||||
|
||||
- name: Create job queue db
|
||||
file:
|
||||
path: "{{pogojig_cache}}/job_queue.sqlite3"
|
||||
owner: root
|
||||
group: uwsgi
|
||||
mode: 0660
|
||||
state: touch
|
||||
|
||||
- name: Enable and launch job processor
|
||||
systemd:
|
||||
name: pogojig-job-processor.service
|
||||
enabled: yes
|
||||
state: restarted
|
||||
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
---
|
||||
- name: Set local facts
|
||||
set_fact:
|
||||
secure_download_dir: /var/cache/secure_download
|
||||
|
||||
- name: Copy webapp sources
|
||||
synchronize:
|
||||
# FIXME: make this path configurable
|
||||
src: ~/secure_download/
|
||||
dest: /var/lib/secure_download/
|
||||
group: no
|
||||
owner: no
|
||||
|
||||
- name: Create secure download worker user and group
|
||||
user:
|
||||
name: uwsgi-secure-download
|
||||
create_home: no
|
||||
group: uwsgi
|
||||
password: '!'
|
||||
shell: /sbin/nologin
|
||||
system: yes
|
||||
|
||||
- name: Template webapp config
|
||||
template:
|
||||
src: secure_download.cfg.j2
|
||||
dest: /var/lib/secure_download/secure_download_prod.cfg
|
||||
owner: uwsgi-secure-download
|
||||
group: root
|
||||
mode: 0660
|
||||
|
||||
- name: Copy uwsgi config
|
||||
copy:
|
||||
src: uwsgi-secure-download.ini
|
||||
dest: /etc/uwsgi.d/secure-download.ini
|
||||
owner: uwsgi-secure-download
|
||||
group: uwsgi
|
||||
mode: 440
|
||||
|
||||
- name: Enable uwsgi systemd socket
|
||||
systemd:
|
||||
daemon-reload: yes
|
||||
name: uwsgi-app@secure-download.socket
|
||||
enabled: yes
|
||||
|
||||
- name: Copy server dir tmpfiles.d config
|
||||
template:
|
||||
src: tmpfiles-secure-download.conf.j2
|
||||
dest: /etc/tmpfiles.d/secure-download.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0644
|
||||
register: sec_dl_tmpfiles_config
|
||||
|
||||
- name: Kick systemd tmpfiles service to create serve dir
|
||||
command: systemd-tmpfiles --create
|
||||
when: sec_dl_tmpfiles_config is changed
|
||||
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
- name: Copy tracespace webapp sources
|
||||
synchronize:
|
||||
# FIXME: make this path configurable
|
||||
src: ~/tracespace_dist/
|
||||
dest: /var/www/tracespace.jaseg.net/
|
||||
group: no
|
||||
owner: no
|
||||
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
- name: Copy first stage nginx config
|
||||
copy:
|
||||
src: nginx_nossl.conf
|
||||
dest: /etc/nginx/nginx.conf
|
||||
|
||||
- name: Add nginx user to uwsgi group for access to uwsgi socket
|
||||
user:
|
||||
name: nginx
|
||||
groups: uwsgi
|
||||
append: yes
|
||||
|
||||
- name: Create subdomain content dirs
|
||||
file:
|
||||
path: /var/www/{{item}}
|
||||
state: directory
|
||||
owner: nginx
|
||||
group: nginx
|
||||
mode: 0550
|
||||
loop:
|
||||
- git.jaseg.net
|
||||
- blog.jaseg.net
|
||||
- kochbuch.jaseg.net
|
||||
- tracespace.jaseg.net
|
||||
- openjscad.jaseg.net
|
||||
- automation.jaseg.de
|
||||
|
||||
- name: Copy uwsgi systemd socket config
|
||||
copy:
|
||||
src: uwsgi-app@.socket
|
||||
dest: /etc/systemd/system/
|
||||
|
||||
- name: Copy uwsgi systemd service config
|
||||
copy:
|
||||
src: uwsgi-app@.service
|
||||
dest: /etc/systemd/system/
|
||||
|
||||
- name: Set SELinux to permissive mode # FIXME this is to let nginx talk to uwsgi
|
||||
selinux:
|
||||
state: permissive
|
||||
policy: targeted
|
||||
|
||||
- name: Enable and launch nginx systemd service
|
||||
systemd:
|
||||
name: nginx.service
|
||||
enabled: yes
|
||||
state: restarted
|
||||
|
||||
- name: Create subdomain letsencrypt certificates
|
||||
command: certbot --nginx certonly -d {{item}} -n --agree-tos --email {{item}}-letsencrypt@jaseg.net
|
||||
args:
|
||||
creates: /etc/letsencrypt/live/{{item}}/fullchain.pem
|
||||
loop:
|
||||
- git.jaseg.net
|
||||
- blog.jaseg.net
|
||||
- kochbuch.jaseg.net
|
||||
- gerbolyze.jaseg.net
|
||||
- tracespace.jaseg.net
|
||||
- openjscad.jaseg.net
|
||||
- pogojig.jaseg.net
|
||||
- automation.jaseg.de
|
||||
- dyndns.jaseg.de
|
||||
|
||||
- name: Copy final nginx config
|
||||
copy:
|
||||
src: nginx.conf
|
||||
dest: /etc/nginx/nginx.conf
|
||||
|
||||
- name: Restart nginx to load new cert
|
||||
systemd:
|
||||
name: nginx.service
|
||||
state: restarted
|
||||
|
||||
- name: Enable certbot renewal timer
|
||||
systemd:
|
||||
name: certbot-renew.timer
|
||||
enabled: yes
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
d {{pogojig_cache}} 770 uwsgi-pogojig uwsgi 2d
|
||||
|
|
@ -1 +0,0 @@
|
|||
d {{secure_download_dir}} 770 uwsgi-download uwsgi 45d
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
[Unit]
|
||||
Description=%i uWSGI app
|
||||
After=syslog.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/sbin/uwsgi \
|
||||
--ini /etc/uwsgi.d/%i.ini \
|
||||
--chmod-socket=660 \
|
||||
--socket=/run/uwsgi/%i.socket
|
||||
User=uwsgi-%i
|
||||
Group=uwsgi
|
||||
Restart=on-failure
|
||||
KillSignal=SIGQUIT
|
||||
Type=notify
|
||||
StandardError=syslog
|
||||
NotifyAccess=all
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
[Unit]
|
||||
Description=Socket for uWSGI app %i
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/uwsgi/%i.socket
|
||||
SocketUser=uwsgi-%i
|
||||
SocketGroup=nginx
|
||||
SocketMode=0660
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[uwsgi]
|
||||
master = True
|
||||
plugins = cgi
|
||||
chdir = /var/lib/gitolite3
|
||||
processes = 1
|
||||
threads = 2
|
||||
cgi = /var/www/cgi-bin/cgit
|
||||
env = CGIT_CONFIG=/var/lib/cgit/cgitrc-gitolite-public
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
[uwsgi]
|
||||
master = True
|
||||
cheap = True
|
||||
die-on-idle = False
|
||||
manage-script-name = True
|
||||
log-format = [pid: %(pid)|app: -|req: -/-] %(addr) (%(user)) {%(vars) vars in %(pktsize) bytes} [%(ctime)] %(method) [URI hidden] => generated %(rsize) bytes in %(msecs) msecs (%(proto) %(status)) %(headers) headers in %(hsize) bytes (%(switches) switches on core %(core))
|
||||
plugins = python3
|
||||
chdir = /var/lib/notification-proxy
|
||||
mount = /=notification_proxy:app
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
[uwsgi]
|
||||
master = True
|
||||
cheap = True
|
||||
die-on-idle = False
|
||||
manage-script-name = True
|
||||
plugins = python3
|
||||
chdir = /var/lib/pogojig
|
||||
mount = /=pogojig:app
|
||||
env = POGOJIG_SETTINGS=pogojig_prod.cfg
|
||||
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
[uwsgi]
|
||||
master = True
|
||||
cheap = True
|
||||
die-on-idle = False
|
||||
manage-script-name = True
|
||||
log-format = [pid: %(pid)|app: -|req: -/-] %(addr) (%(user)) {%(vars) vars in %(pktsize) bytes} [%(ctime)] %(method) [URI hidden] => generated %(rsize) bytes in %(msecs) msecs (%(proto) %(status)) %(headers) headers in %(hsize) bytes (%(switches) switches on core %(core))
|
||||
plugins = python3
|
||||
chdir = /var/lib/secure_download
|
||||
mount = /=server:app
|
||||
env = SECURE_DOWNLOAD_SETTINGS=secure_download_prod.cfg
|
||||
|
||||
22
gerboweb/develop-startup.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
cd /gerbolyze/gerbonara
|
||||
echo "### Setting up gerbonara ###"
|
||||
# newer pip is buggy and just crashes so we pinned an old version.
|
||||
# python packaging infrastructure is such an incoherent, buggy mess
|
||||
# also ignore the running pip as root warning, it's dumb and here we actually want to do just that.
|
||||
python3 -m pip --disable-pip-version-check install .
|
||||
cd /gerbolyze
|
||||
echo "### Setting up gerbolyze ###"
|
||||
python3 -m pip --disable-pip-version-check install .
|
||||
|
||||
export PATH=$PATH:$HOME/.cargo/bin
|
||||
cd /gerbolyze/gerboweb
|
||||
echo "### Launching app ###"
|
||||
tmux new-session -d -s dev env GERBOWEB_SETTINGS=gerboweb-develop.cfg FLASK_APP=gerboweb.py flask run -h 0.0.0.0
|
||||
tmux bind -n C-q kill-session
|
||||
tmux rename-window gerboweb
|
||||
tmux split-window -t 0 -v python3 job_processor.py /var/cache/job_queue.sqlite3
|
||||
tmux attach
|
||||
4
gerboweb/gerboweb-develop.cfg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
MAX_CONTENT_LENGTH=50000000
|
||||
SECRET_KEY="development mode"
|
||||
UPLOAD_PATH="/var/cache/upload"
|
||||
JOB_QUEUE_DB="/var/cache/job_queue.sqlite3"
|
||||
|
|
@ -6,6 +6,7 @@ import tempfile
|
|||
import uuid
|
||||
from functools import wraps
|
||||
from os import path
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
|
|
@ -15,25 +16,34 @@ from flask_wtf.file import FileField, FileRequired
|
|||
from wtforms.fields import RadioField
|
||||
from wtforms.validators import DataRequired
|
||||
from werkzeug.utils import secure_filename
|
||||
import uwsgidecorators
|
||||
|
||||
from job_queue import JobQueue
|
||||
import job_processor
|
||||
|
||||
app = Flask(__name__, static_url_path='/static')
|
||||
app = Flask(__name__, static_url_path='/gerboweb/static')
|
||||
app.config.from_envvar('GERBOWEB_SETTINGS')
|
||||
if app.config['SECRET_KEY'] is None:
|
||||
if (p := Path('/run/secrets/gerboweb')).is_file():
|
||||
app.config['SECRET_KEY'] = p.read_bytes()
|
||||
else:
|
||||
app.config['SECRET_KEY'] = os.urandom(32)
|
||||
|
||||
class UploadForm(FlaskForm):
|
||||
upload_file = FileField(validators=[DataRequired()])
|
||||
|
||||
class OverlayForm(UploadForm):
|
||||
upload_file = FileField(validators=[FileRequired()])
|
||||
side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')],
|
||||
default=lambda: session.get('side_selected', session.get('last_download')))
|
||||
|
||||
class ResetForm(FlaskForm):
|
||||
pass
|
||||
|
||||
job_queue = JobQueue(app.config['JOB_QUEUE_DB'])
|
||||
|
||||
@uwsgidecorators.timer(1)
|
||||
def job_processor_timer(_num):
|
||||
job_processor.process_job(job_queue)
|
||||
|
||||
def tempfile_path(namespace):
|
||||
""" Return a path for a per-session temporary file identified by the given namespace. Create the session tempfile
|
||||
dir if necessary. The application tempfile dir is controlled via the upload_path config value and not managed by
|
||||
|
|
@ -64,15 +74,18 @@ def index():
|
|||
|
||||
for job_type in ('vector_job', 'render_job'):
|
||||
if job_type in session:
|
||||
job = job_queue[session[job_type]]
|
||||
if job.finished:
|
||||
if job.result != 0:
|
||||
flash(f'Error processing gerber files', 'success') # FIXME make this an error, add CSS
|
||||
del session[job_type]
|
||||
try:
|
||||
job = job_queue[session[job_type]]
|
||||
if job.finished:
|
||||
if not job.result:
|
||||
flash(f'Error processing gerber files', 'success') # FIXME make this an error, add CSS
|
||||
del session[job_type]
|
||||
except:
|
||||
session.clear()
|
||||
|
||||
r = make_response(render_template('index.html',
|
||||
has_renders = path.isfile(tempfile_path('gerber.zip')),
|
||||
has_output = path.isfile(tempfile_path('overlay.png')),
|
||||
has_output = path.isfile(tempfile_path('overlay.svg')),
|
||||
**forms))
|
||||
if 'vector_job' in session or 'render_job' in session:
|
||||
r.headers.set('refresh', '10')
|
||||
|
|
@ -85,17 +98,30 @@ def index():
|
|||
|
||||
def vectorize():
|
||||
if 'vector_job' in session:
|
||||
job_queue[session['vector_job']].abort()
|
||||
try:
|
||||
job_queue[session['vector_job']].abort()
|
||||
except:
|
||||
pass
|
||||
session['vector_job'] = job_queue.enqueue('vector',
|
||||
client=request.remote_addr,
|
||||
session_id=session['session_id'],
|
||||
side=session['side_selected'])
|
||||
gerber_in=tempfile_path('gerber.zip'),
|
||||
overlay=tempfile_path('overlay.svg'),
|
||||
gerber_out=tempfile_path('gerber_out.zip'))
|
||||
|
||||
def render():
|
||||
if 'render_job' in session:
|
||||
job_queue[session['render_job']].abort()
|
||||
try:
|
||||
job_queue[session['render_job']].abort()
|
||||
except:
|
||||
pass
|
||||
session['render_job'] = job_queue.enqueue('render',
|
||||
session_id=session['session_id'],
|
||||
infile=tempfile_path('gerber.zip'),
|
||||
preview_top_out=tempfile_path('preview_top.png'),
|
||||
preview_bottom_out=tempfile_path('preview_bottom.png'),
|
||||
template_top_out=tempfile_path('template_top.svg'),
|
||||
template_bottom_out=tempfile_path('template_bottom.svg'),
|
||||
client=request.remote_addr)
|
||||
|
||||
@app.route('/upload/gerber', methods=['POST'])
|
||||
|
|
@ -108,7 +134,7 @@ def upload_gerber():
|
|||
session['filename'] = secure_filename(f.filename) # Cache filename for later download
|
||||
|
||||
render()
|
||||
if path.isfile(tempfile_path('overlay.png')): # Re-vectorize when gerbers change
|
||||
if path.isfile(tempfile_path('overlay.svg')): # Re-vectorize when gerbers change
|
||||
vectorize()
|
||||
|
||||
flash(f'Gerber file successfully uploaded.', 'success')
|
||||
|
|
@ -119,11 +145,8 @@ def upload_gerber():
|
|||
def upload_overlay():
|
||||
upload_form = OverlayForm()
|
||||
if upload_form.validate_on_submit():
|
||||
# FIXME raise error when no side selected
|
||||
f = upload_form.upload_file.data
|
||||
f.save(tempfile_path('overlay.png'))
|
||||
session['side_selected'] = upload_form.side.data
|
||||
|
||||
f.save(tempfile_path('overlay.svg'))
|
||||
vectorize()
|
||||
|
||||
flash(f'Overlay file successfully uploaded.', 'success')
|
||||
|
|
@ -133,7 +156,7 @@ def upload_overlay():
|
|||
def render_preview(side):
|
||||
if not side in ('top', 'bottom'):
|
||||
return abort(400, 'side must be either "top" or "bottom"')
|
||||
return send_file(tempfile_path(f'render_{side}.small.png'))
|
||||
return send_file(tempfile_path(f'preview_{side}.png'))
|
||||
|
||||
@app.route('/render/download/<side>')
|
||||
def render_download(side):
|
||||
|
|
@ -141,10 +164,10 @@ def render_download(side):
|
|||
return abort(400, 'side must be either "top" or "bottom"')
|
||||
|
||||
session['last_download'] = side
|
||||
return send_file(tempfile_path(f'render_{side}.png'),
|
||||
mimetype='image/png',
|
||||
return send_file(tempfile_path(f'template_{side}.svg'),
|
||||
mimetype='image/svg',
|
||||
as_attachment=True,
|
||||
attachment_filename=f'{path.splitext(session["filename"])[0]}_render_{side}.png')
|
||||
attachment_filename=f'{path.splitext(session["filename"])[0]}_template_{side}.svg')
|
||||
|
||||
@app.route('/output/download')
|
||||
def output_download():
|
||||
|
|
@ -157,9 +180,15 @@ def output_download():
|
|||
@require_session_id
|
||||
def session_reset():
|
||||
if 'render_job' in session:
|
||||
job_queue[session['render_job']].abort()
|
||||
try:
|
||||
job_queue[session['render_job']].abort()
|
||||
except:
|
||||
pass
|
||||
if 'vector_job' in session:
|
||||
job_queue[session['vector_job']].abort()
|
||||
try:
|
||||
job_queue[session['vector_job']].abort()
|
||||
except:
|
||||
pass
|
||||
session.clear()
|
||||
flash('Session reset', 'success');
|
||||
return redirect(url_for('index'))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
MAX_CONTENT_LENGTH=10000000
|
||||
SECRET_KEY="FIXME: CHANGE THIS KEY"
|
||||
UPLOAD_PATH="/var/cache/gerboweb/upload"
|
||||
JOB_QUEUE_DB="/var/cache/gerboweb/job_queue.sqlite3"
|
||||
APPLICATION_ROOT="/gerboweb/"
|
||||
|
|
@ -1,11 +1,79 @@
|
|||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import subprocess
|
||||
import logging
|
||||
import itertools
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from job_queue import JobQueue
|
||||
|
||||
def run_resvg(*args):
|
||||
if 'RESVG' in os.environ:
|
||||
subprocess.run([os.environ['RESVG'], *args], check=True)
|
||||
|
||||
else:
|
||||
# By default, try four options:
|
||||
for candidate in [
|
||||
# somewhere in $PATH
|
||||
'resvg',
|
||||
|
||||
# in user-local cargo installation
|
||||
Path.home() / '.cargo' / 'bin' / 'resvg',
|
||||
|
||||
# somewhere in $PATH
|
||||
'wasi-resvg',
|
||||
|
||||
# in user-local pip installation
|
||||
Path.home() / '.local' / 'bin' / 'wasi-resvg',
|
||||
|
||||
# next to our current python interpreter (e.g. in virtualenv)
|
||||
str(Path(sys.executable).parent / 'resvg'),
|
||||
str(Path(sys.executable).parent / 'wasi-resvg') ]:
|
||||
|
||||
try:
|
||||
subprocess.run([candidate, *args], check=True)
|
||||
print('used svg-flatten at', candidate)
|
||||
break
|
||||
|
||||
except (FileNotFoundError, ModuleNotFoundError):
|
||||
continue
|
||||
|
||||
else:
|
||||
raise SystemError('svg-flatten executable not found')
|
||||
|
||||
def process_job(job_queue):
|
||||
logging.debug('Checking for jobs')
|
||||
for job in job_queue.job_iter('render'):
|
||||
logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}')
|
||||
with job:
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as svg:
|
||||
subprocess.run(['python3', '-m', 'gerbonara', '--top', job['infile'], svg.name], check=True)
|
||||
run_resvg('--dpi', '300', svg.name, job['preview_top_out'])
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as svg:
|
||||
subprocess.run(['python3', '-m', 'gerbonara', '--bottom', job['infile'], svg.name], check=True)
|
||||
run_resvg('--dpi', '300', svg.name, job['preview_bottom_out'])
|
||||
subprocess.run(['python3', '-m', 'gerbolyze', 'template', '--top', job['infile'], job['template_top_out']], check=True)
|
||||
subprocess.run(['python3', '-m', 'gerbolyze', 'template', '--bottom', job['infile'], job['template_bottom_out']], check=True)
|
||||
logging.info(f'Finishied processing {job.type} job {job.id}')
|
||||
job.result = True
|
||||
except:
|
||||
logging.exception('Error during job processing')
|
||||
job.result = False
|
||||
|
||||
for job in job_queue.job_iter('vector'):
|
||||
logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}')
|
||||
with job:
|
||||
try:
|
||||
subprocess.run(['python3', '-m', 'gerbolyze', 'paste', job['gerber_in'], job['overlay'], job['gerber_out']], check=True)
|
||||
logging.info(f'Finishied processing {job.type} job {job.id}')
|
||||
job.result = True
|
||||
except:
|
||||
logging.exception('Error during job processing')
|
||||
job.result = False
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
|
@ -20,21 +88,11 @@ if __name__ == '__main__':
|
|||
logging.basicConfig(level=numeric_level)
|
||||
|
||||
job_queue = JobQueue(args.queue)
|
||||
print('Job processor online')
|
||||
|
||||
signal.signal(signal.SIGALRM, lambda *args: None) # Ignore incoming alarm signals while processing jobs
|
||||
signal.setitimer(signal.ITIMER_REAL, 0.001, 1)
|
||||
while signal.sigwait([signal.SIGALRM, signal.SIGINT]) == signal.SIGALRM:
|
||||
logging.debug('Checking for jobs')
|
||||
for job in job_queue.job_iter('render'):
|
||||
logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}')
|
||||
with job:
|
||||
job.result = subprocess.call(['sudo', '/usr/local/sbin/gerbolyze_render.sh', job['session_id']])
|
||||
logging.info(f'Finishied processing {job.type} job {job.id}')
|
||||
|
||||
for job in job_queue.job_iter('vector'):
|
||||
logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}')
|
||||
with job:
|
||||
job.result = subprocess.call(['sudo', '/usr/local/sbin/gerbolyze_vector.sh', job['session_id'], job['side']])
|
||||
logging.info(f'Finishied processing {job.type} job {job.id}')
|
||||
process_job(job_queue)
|
||||
logging.info('Caught SIGINT. Exiting.')
|
||||
|
||||
|
|
|
|||
7
gerboweb/run-develop.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname $0)"
|
||||
podman build -f Containerfile.develop --tag gerbolyze-develop
|
||||
podman run -p 127.0.0.1:5000:5000 -v ..:/gerbolyze -ti gerbolyze-develop
|
||||
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 509 KiB |
|
|
@ -73,7 +73,6 @@ body {
|
|||
}
|
||||
|
||||
div.header {
|
||||
background-image: url("/static/bg10.jpg");
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
|
|
@ -283,7 +282,6 @@ a.preview:link, a.preview:hover, a.preview:visited, a.preview:active {
|
|||
line-height: 70px;
|
||||
/* background-image: linear-gradient(to top right, var(--cg5), var(--cg6)); */
|
||||
|
||||
background-image: url("/static/bg10.jpg");
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
|
|
|
|||
|
|
@ -1,166 +1,172 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Gerbolyze Raster image to PCB renderer</title>
|
||||
<title>Gerbolyze Image to PCB Toolchain</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='style.css')}}">
|
||||
<link rel="icon" type="image/png" href="{{url_for('static', filename='favicon-512.png')}}">
|
||||
<link rel="apple-touch-icon" href="{{url_for('static', filename='favicon-512.png')}}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
|
||||
div.header {
|
||||
background-image: url("{{url_for('static', filename='bg10.jpg')}}");
|
||||
}
|
||||
|
||||
.sample-images > h1 {
|
||||
background-image: url("{{url_for('static', filename='bg10.jpg')}}");
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout-container">
|
||||
<div class="header">
|
||||
<div class="desc">
|
||||
<h1>Raster image to PCB converter</h1>
|
||||
<p>
|
||||
Gerbolyze is a tool for rendering black and white raster (PNG) images directly onto gerber layers. You can
|
||||
use this to put art on a PCB's silkscreen, solder mask or copper layers. The input is a black-and-white PNG
|
||||
image that is vectorized and rendered into an existing gerber file. Gerbolyze works with gerber files
|
||||
produced with any EDA toolchain and has been tested to work with both Altium and KiCAD.
|
||||
</p>
|
||||
</div>
|
||||
<div class="desc">
|
||||
<h1>SVG/JPG/PNG to PCB converter</h1>
|
||||
<p>
|
||||
This is the toy web frontend to <a href="https://github.com/jaseg/gerbolyze">Gerbolyze</a>.
|
||||
|
||||
Gerbolyze is a tool for rendering arbitrary vector (SVG) and raster (PNG/JPG) images directly onto gerber layers.
|
||||
You can use this to put art on a PCB's silkscreen, solder mask or copper layers. The input is an SVG file
|
||||
generated from a template. This SVG file has one layer for each PCB layer and the layers are rendered one by one
|
||||
into the existing gerber file. SVG primitives are converted as-is with (almost) full SVG support, and bitmap
|
||||
images are vectorized using a vector halftone processor. Gerbolyze works with gerber files produced with any EDA
|
||||
toolchain and has been tested to work with both Altium and KiCAD.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
<div class="flashes">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash flash-{{category}}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if messages %}
|
||||
<div class="flashes">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash flash-{{category}}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form id="reset-form" method="POST" action="{{url_for('session_reset')}}" class="reset-form">{{reset_form.csrf_token}}</form>
|
||||
|
||||
<div class="steps">
|
||||
<div class="step" id="step1">
|
||||
<div class="description">
|
||||
<h2>Upload zipped gerber files</h2>
|
||||
<p>
|
||||
First, upload a zip file containing all your gerber files. The default file names used by KiCAD, Eagle
|
||||
and Altium are supported.
|
||||
</p>
|
||||
</div>
|
||||
<div class="step" id="step1">
|
||||
<div class="description">
|
||||
<h2>Upload zipped gerber files</h2>
|
||||
<p>
|
||||
First, upload a zip file containing all your gerber files. The default file names used by KiCAD, Eagle
|
||||
and Altium are supported.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<form id="gerber-upload-form" method="POST" action="{{url_for('upload_gerber')}}" enctype="multipart/form-data">
|
||||
{{gerber_form.csrf_token}}
|
||||
</form>
|
||||
<div class="form-controls">
|
||||
<div class="upload-label">Upload Gerber file:</div>
|
||||
<input class='upload-button' form="gerber-upload-form" name="upload_file" size="20" type="file">
|
||||
</div>
|
||||
<div class="submit-buttons">
|
||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||
<input class='submit-button' form="gerber-upload-form" type="submit" value="Submit">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<form id="gerber-upload-form" method="POST" action="{{url_for('upload_gerber')}}" enctype="multipart/form-data">
|
||||
{{gerber_form.csrf_token}}
|
||||
</form>
|
||||
<div class="form-controls">
|
||||
<div class="upload-label">Upload Gerber file:</div>
|
||||
<input class='upload-button' form="gerber-upload-form" name="upload_file" size="20" type="file">
|
||||
</div>
|
||||
<div class="submit-buttons">
|
||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||
<input class='submit-button' form="gerber-upload-form" type="submit" value="Submit">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if 'render_job' in session or has_renders %}
|
||||
<div class="step" id="step2">
|
||||
<div class="description">
|
||||
<h2>Download the target side's preview image</h2>
|
||||
<p>
|
||||
Second, download either the top or bottom preview image and use it to align and scale your own artwork
|
||||
in an image editing program such as Gimp. Then upload your overlay image below.
|
||||
{% if 'render_job' in session or has_renders %}
|
||||
<div class="step" id="step2">
|
||||
<div class="description">
|
||||
<h2>Download the target side's preview image</h2>
|
||||
<p>
|
||||
Second, download either the top or bottom SVG template and place your own artwork in it on the appropriate
|
||||
layers. The template is made to work well with the excellent open-source <a href="https://inkscape.org">Inkscape</a>
|
||||
vector graphics editor. When you are done, upload your overlay below.
|
||||
|
||||
Note that you will have to convert grayscale images into binary images yourself. Gerbolyze can't do this
|
||||
for you since there are lots of variables involved. Our <a href="https://github.com/jaseg/gerbolyze/blob/master/README.rst#image-preprocessing-guide">Guideline on image processing</a> gives an overview on
|
||||
<i>one</i> way to produce agreeable binary images from grayscale source material.
|
||||
</p>
|
||||
</div>
|
||||
<div class="controls">
|
||||
{% if 'render_job' in session %}
|
||||
<div class="loading-message">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div><strong>Processing...</strong></div>
|
||||
<div>(this may take several minutes!)</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="preview-images">
|
||||
<a href="{{url_for('render_download', side='top')}}" onclick="document.querySelector('#side-0').checked=true" class="preview preview-top" style="background-image:url('{{url_for('render_preview', side='top')}}');">
|
||||
<div class="overlay">top</div>
|
||||
</a>
|
||||
<a href="{{url_for('render_download', side='bottom')}}" onclick="document.querySelector('#side-1').checked=true" class="preview preview-bottom" style="background-image:url('{{url_for('render_preview', side='bottom')}}');">
|
||||
<div class="overlay">bot<br/>tom</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="submit-buttons">
|
||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
If you wish to put a bitmap image (PNG/JPG) on your board, simply place it into the SVG on the appropriate
|
||||
layer. Make sure you select Inkscape's "embed image" option when importing it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="controls">
|
||||
{% if 'render_job' in session %}
|
||||
<div class="loading-message">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div><strong>Processing...</strong></div>
|
||||
<div>(this may take several minutes!)</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="preview-images">
|
||||
<a href="{{url_for('render_download', side='top')}}" onclick="document.querySelector('#side-0').checked=true" class="preview preview-top" style="background-image:url('{{url_for('render_preview', side='top')}}');">
|
||||
<div class="overlay">top</div>
|
||||
</a>
|
||||
<a href="{{url_for('render_download', side='bottom')}}" onclick="document.querySelector('#side-1').checked=true" class="preview preview-bottom" style="background-image:url('{{url_for('render_preview', side='bottom')}}');">
|
||||
<div class="overlay">bot<br/>tom</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="submit-buttons">
|
||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" id="step3">
|
||||
<div class="description">
|
||||
<h2>Upload overlay image</h2>
|
||||
<p>
|
||||
Now, upload your binary overlay image as a PNG and let gerbolyze render it onto the target layer. The PNG
|
||||
file should be a black and white binary file with details generally above about 10px size. <b>Antialiased
|
||||
edges are supported.</b>
|
||||
</p>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<form id="overlay-upload-form" method="POST" action="{{url_for('upload_overlay')}}" enctype="multipart/form-data">
|
||||
{{overlay_form.csrf_token}}
|
||||
</form>
|
||||
<div class="form-controls">
|
||||
<div class="form-label upload-label">Upload Overlay PNG file:</div>
|
||||
<input class='upload-button' form="overlay-upload-form" name="upload_file" size="20" type="file">
|
||||
</div>
|
||||
<div class="form-controls">
|
||||
<div class="form-label target-label">Target layer:</div>
|
||||
<input form="overlay-upload-form" name="side" id="side-0" type="radio" value="top">
|
||||
<label for="side-0">Top</label>
|
||||
<input form="overlay-upload-form" name="side" id="side-1" type="radio" value="top">
|
||||
<label for="side-1">Bottom</label>
|
||||
</div>
|
||||
<div class="submit-buttons">
|
||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||
<input class='submit-button' form="overlay-upload-form" type="submit" value="Submit">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step" id="step3">
|
||||
<div class="description">
|
||||
<h2>Upload overlay SVG</h2>
|
||||
<p>
|
||||
Now, upload your binary overlay as an SVG and let gerbolyze paste it onto the target layers.
|
||||
</p>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<form id="overlay-upload-form" method="POST" action="{{url_for('upload_overlay')}}" enctype="multipart/form-data">
|
||||
{{overlay_form.csrf_token}}
|
||||
</form>
|
||||
<div class="form-controls">
|
||||
<div class="form-label upload-label">Upload Overlay SVG file:</div>
|
||||
<input class='upload-button' form="overlay-upload-form" name="upload_file" size="20" type="file">
|
||||
</div>
|
||||
<div class="submit-buttons">
|
||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||
<input class='submit-button' form="overlay-upload-form" type="submit" value="Submit">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if 'vector_job' in session or has_output %}
|
||||
<div class="step" id="step4">
|
||||
<div class="description">
|
||||
<h2>Download the processed gerber files</h2>
|
||||
</div>
|
||||
<div class="controls">
|
||||
{% if 'vector_job' in session %}
|
||||
<div class="loading-message">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div><strong>Processing...</strong></div>
|
||||
<div>(this may take several minutes!)</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='download-controls'>
|
||||
<a class='output-download' href="{{url_for('output_download')}}">Click to download</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="submit-buttons">
|
||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||
</div>
|
||||
<!--4>Debug foo</h4>
|
||||
<div class="loading-message">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div><strong>Processing...</strong></div>
|
||||
<div>(this may take several minutes!)</div>
|
||||
</div-->
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {# vector job #}
|
||||
{% endif %} {# render job #}
|
||||
{% if 'vector_job' in session or has_output %}
|
||||
<div class="step" id="step4">
|
||||
<div class="description">
|
||||
<h2>Download the processed gerber files</h2>
|
||||
</div>
|
||||
<div class="controls">
|
||||
{% if 'vector_job' in session %}
|
||||
<div class="loading-message">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div><strong>Processing...</strong></div>
|
||||
<div>(this may take several minutes!)</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='download-controls'>
|
||||
<a class='output-download' href="{{url_for('output_download')}}">Click to download</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="submit-buttons">
|
||||
<input class='reset-button' form="reset-form" type="submit" value="Start over">
|
||||
</div>
|
||||
<!--4>Debug foo</h4>
|
||||
<div class="loading-message">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div><strong>Processing...</strong></div>
|
||||
<div>(this may take several minutes!)</div>
|
||||
</div-->
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {# vector job #}
|
||||
{% endif %} {# render job #}
|
||||
</div>
|
||||
<div class="sample-images">
|
||||
<h1>Sample images</h1>
|
||||
<img src="{{url_for('static', filename='sample1.jpg')}}">
|
||||
<img src="{{url_for('static', filename='sample2.jpg')}}">
|
||||
<img src="{{url_for('static', filename='sample3.jpg')}}">
|
||||
<h1>Sample images</h1>
|
||||
<img src="{{url_for('static', filename='sample1.jpg')}}">
|
||||
<img src="{{url_for('static', filename='sample2.jpg')}}">
|
||||
<img src="{{url_for('static', filename='sample3.jpg')}}">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
|||
12
gerboweb/uwsgi-gerboweb.ini
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[uwsgi]
|
||||
master = True
|
||||
cheap = True
|
||||
die-on-idle = False
|
||||
manage-script-name = True
|
||||
plugins = python
|
||||
chdir = /gerboweb
|
||||
chmod-socket=666
|
||||
mount = /gerboweb=gerboweb:app
|
||||
env = GERBOWEB_SETTINGS=gerboweb.cfg
|
||||
mule = job_processor.py /var/cache/gerboweb/job_queue.sqlite3
|
||||
static-map = /static=/gerboweb/static
|
||||
95
kicad_mod_template.svg
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="160mm"
|
||||
height="100mm"
|
||||
viewBox="0 0 160 100"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07, custom)"
|
||||
sodipodi:docname="kicad_mod_template.svg">
|
||||
<metadata
|
||||
id="metadata20">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs18" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1019"
|
||||
id="namedview16"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.1197381"
|
||||
inkscape:cx="293.88402"
|
||||
inkscape:cy="186.11375"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="F.SilkS" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="Eco2.User"
|
||||
inkscape:label="Eco2.User" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="Eco1.User"
|
||||
inkscape:label="Eco1.User" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="Margin"
|
||||
inkscape:label="Margin" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="F.CrtYd"
|
||||
inkscape:label="F.CrtYd" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="F.Fab"
|
||||
inkscape:label="F.Fab" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="Dwgs.User"
|
||||
inkscape:label="Dwgs.User" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="Cmts.User"
|
||||
inkscape:label="Cmts.User" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="Edge.Cuts"
|
||||
inkscape:label="Edge.Cuts" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="g5"
|
||||
inkscape:label="F.Cu" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="F.Mask"
|
||||
inkscape:label="F.Mask" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="F.SilkS"
|
||||
inkscape:label="F.SilkS" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
BIN
pics/ex-flattening.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
pics/ex-intersections.png
Normal file
|
After Width: | Height: | Size: 47 KiB |