Compare commits

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

295 commits
master ... temp

Author SHA1 Message Date
jaseg
3a4b96d105 foobar 2024-07-23 15:44:50 +02:00
jaseg
ce75189693 foobar 2024-07-23 15:10:35 +02:00
jaseg
bf972ae6cb Make tox tests run 2024-05-23 10:35:12 +02:00
jaseg
419fa73865 Bump version to v3.1.7 2023-11-14 22:06:18 +01:00
jaseg
357c49016d Bump required gerbonara version to v1.2.0
gerbonara v1.2.0 lands a fix for extremely broken aperture macro parsing
at certain cheap PCB fab houses.
2023-11-14 22:06:18 +01:00
jaseg
2384f4fc94 Fix failing unit test 2023-11-14 20:51:05 +01:00
jaseg
8e8bcee209 Fix infinite loop bug in dehole_polytree
Closes #43. Thanks to github users @Altomare for reporting this one, and
@fstark for suggesting a fix.
2023-11-14 20:50:52 +01:00
jaseg
1442601f7b Bump version to v3.1.6 2023-10-27 15:12:01 +02:00
jaseg
7e6b8abc85 ci: Disable tests for now 2023-10-27 15:12:01 +02:00
jaseg
92f78e09de Work around pip now needing a new feature switch for no good reason
...except to break every CI pipeline in existence, that is.
2023-10-27 00:19:53 +02:00
jaseg
42a4665268 Bump required gerbonara and svg-flatten-wasi versions 2023-10-27 00:19:48 +02:00
jaseg
4d711a2fe8 Update all svg-flatten dependencies 2023-10-26 00:32:02 +02:00
jaseg
00eb9594d6 svg-flatten: Add cubic bezier support for newer usvg versions 2023-10-26 00:04:17 +02:00
jaseg
8ab0c9fa01 Make warning messages in color handling code easier to understand 2023-06-21 15:39:46 +02:00
jaseg
2fbde62df9 Fix build on newer gcc 2023-06-21 15:39:32 +02:00
jaseg
86e5c531f5 README: Add note on resvg-wasi 2023-04-19 11:03:34 +02:00
jaseg
8d2f1b830d Add bs4 dependency 2023-04-19 10:59:52 +02:00
jaseg
17c186ea6b Bump version to v3.1.5 2023-04-16 20:37:54 +02:00
jaseg
8c2fc45601 ci: Use system twine in svg-flatten 2023-04-16 20:37:33 +02:00
jaseg
bcfd27bd53 Bump version to v3.1.4 2023-04-16 20:23:59 +02:00
jaseg
f80f341a72 svg-flatten: Remove trace of legacy usvg command line 2023-04-16 20:23:31 +02:00
jaseg
c5640b4db3 svg-flatten: Improve error handling in Makefile 2023-04-16 20:13:51 +02:00
jaseg
288e36764c svg-flatten: Fix broken math in transform inversion 2023-04-16 20:13:24 +02:00
jaseg
4aa448a5a3 svg-flatten: Fix pattern clipping for rotated/skewed transforms 2023-04-16 20:12:59 +02:00
jaseg
384c917307 svg-flatten: Fix extraneous fill in SVG stroke output 2023-04-16 20:11:43 +02:00
jaseg
ac52ecdafc Move CI to the new container names 2023-04-16 17:54:10 +02:00
jaseg
5dbeb6800b Bump version to v3.1.3 2023-04-16 14:46:40 +02:00
jaseg
e7e9da5a0c CI: Disable failing debian builds 2023-04-16 14:46:21 +02:00
jaseg
8d0a4da313 Bump version to v3.1.2 2023-04-16 14:19:28 +02:00
jaseg
348409a29e svg-flatten: Bump WASI SDK to v20 and fix build 2023-04-16 14:19:12 +02:00
jaseg
0ae1da08cc Bump version to v3.1.1 2023-04-16 14:05:35 +02:00
jaseg
3fbb56c3ae Fix clang build and warnings 2023-04-16 14:04:53 +02:00
jaseg
da315879cc Bump version to v3.1.0 2023-04-11 00:55:23 +02:00
jaseg
c3f51bfb97 Remove resvg-wasi dependency for now 2023-04-11 00:55:23 +02:00
jaseg
9d7fd8b3b4 Fix failing tests 2023-04-11 00:40:53 +02:00
jaseg
a02ff5fc87 Update TODO 2023-04-10 23:56:39 +02:00
jaseg
b26c28e58b Remove debug prints 2023-04-02 23:19:19 +02:00
jaseg
be24d0368f Don't apply dilation scripts in convert, add paste test 2023-04-02 23:18:03 +02:00
jaseg
e1c40e8c80 Fix failing tests 2023-03-31 22:31:58 +02:00
jaseg
4a3a9f1582 Fix remaining commands for layer rework 2023-03-31 16:35:41 +02:00
jaseg
4127a18e89 Fix drill layer handling for latest changes in gerbonara 2023-03-31 14:12:45 +02:00
jaseg
0d4172901b Fix scale of drill mapping test 2023-03-30 15:05:09 +02:00
jaseg
bc63166a40 svg-flatten: Make drill size mapping more precise, add tests 2023-03-30 14:54:46 +02:00
jaseg
2df63318a2 svg-flatten: Test transform decomposition under mirroring 2023-03-30 12:12:38 +02:00
jaseg
5f008f623a svg-flatten: Add transform decomposition unit tests 2023-03-29 23:53:01 +02:00
jaseg
10669301a1 svg-flatten: Add stroke mapping test 2023-03-29 22:42:49 +02:00
jaseg
25628f1d24 svg-flatten: Add stroked SVG output 2023-03-29 22:21:15 +02:00
jaseg
65a426c645 svg-flatten: Add another transform uniformity test pattern 2023-03-29 18:35:52 +02:00
jaseg
eb7107a8c6 svg-flatten: Fix incorrect polarity for outlined strokes 2023-03-29 18:23:56 +02:00
jaseg
454d587d79 svg-flatten: Optimize output aperture count 2023-03-29 15:54:00 +02:00
jaseg
602e51ca10 svg_doc: Fix gerber mapping of strokes with skewed or non-uniform transforms 2023-03-29 15:45:14 +02:00
jaseg
b4753e66e2 WIP 2023-03-26 00:40:43 +01:00
jaseg
6b0382ab77 WIP 2023-03-25 22:05:03 +01:00
jaseg
a6adfe4d1d svg-flatten: Add input scaling 2023-03-22 23:27:12 +01:00
jaseg
70a7a0aa95 Add scaled footprint generator script 2023-03-22 23:24:25 +01:00
jaseg
b1324e9a53 protoboard: small formatting fix 2023-03-22 12:12:52 +01:00
jaseg
89fa6fbf83 Fix unit tests 2023-03-05 23:28:09 +01:00
jaseg
7e7f4b1aa6 README: Update demo web interface link 2022-07-11 12:34:40 +02:00
jaseg
ea0a6d83f8 README: Add link to demo web interface 2022-07-11 12:33:05 +02:00
jaseg
869fd09ad9 gerboweb: fix deployment for new gerbolyze 2022-07-11 10:21:07 +00:00
jaseg
00de98d5e3 gerboweb: Add prod container build 2022-07-05 22:31:09 +02:00
jaseg
3ba932209f gerboweb: add deployment stuff 2022-07-04 23:57:28 +02:00
jaseg
1da5919d91 svg-flatten: fix dilation of new aperture tokens 2022-07-03 23:13:23 +02:00
jaseg
dd0c1cb632 gerboweb: remove obsolete paste layer selection 2022-07-03 22:51:25 +02:00
jaseg
9632509060 Bring gerboweb roughly back into shape 2022-07-03 22:50:42 +02:00
jaseg
58eabf59fe gerbolyze paste: fix zip logic 2022-07-03 22:50:34 +02:00
jaseg
85460a7c55 protoboard: add more patterns 2022-07-03 22:49:43 +02:00
jaseg
575e24cca7 Update protoboard index 2022-06-28 13:46:43 +02:00
jaseg
68ce1505f1 protoboards: WIP 2022-06-26 17:38:55 +02:00
jaseg
25ebdbe625 protoboard: add tight layout option 2022-06-24 13:21:44 +02:00
jaseg
e845888580 svg-flatten: Fix pattern aperture macro export 2022-06-24 13:04:25 +02:00
jaseg
e76d257220 svg-flatten: fix failing tests 2022-06-21 16:23:09 +02:00
jaseg
29c5c0c03d upstream: move submodules to correct commits 2022-06-21 15:46:42 +02:00
jaseg
0cf02e9c1d fix local tests 2022-06-21 15:34:14 +02:00
jaseg
e73b577178 add pytest tests 2022-06-21 13:55:31 +02:00
jaseg
bb8b28f86a update local tests 2022-06-21 13:54:29 +02:00
jaseg
a01e44b142 protoboard: remove match syntax for python 3.8 2022-06-21 13:54:05 +02:00
jaseg
0ecb612d59 svg-flatten: remove debug prints 2022-06-20 19:40:24 +02:00
jaseg
d3204b1ede svg-flatten: Finish direct interpolation optimization 2022-06-20 19:37:33 +02:00
jaseg
2fc5d1d929 svg-flatten: outline mode WIP 2022-06-20 18:48:17 +02:00
jaseg
e4a0c1ba4a Fix outline mode clip & flash handling 2022-06-20 18:30:34 +02:00
jaseg
9e9cc2bc01 svg-flatten: add export of patterns via aperture macros 2022-06-20 16:19:53 +02:00
jaseg
0e1c8507bb protoboard: fix autogenerator 2022-06-20 14:38:34 +02:00
jaseg
8a1f9d1832 protoboard: handle hole keepouts on small pcbs 2022-06-20 14:05:15 +02:00
jaseg
f09c436e56 protoboard: Add generator for a number of varieties 2022-06-20 14:04:04 +02:00
jaseg
4636383ffc protoboard: Add support for two-layer SMD layouts 2022-06-20 11:38:12 +02:00
jaseg
c1cda48a4c protoboard: Add SMD patterns 2022-06-20 11:21:42 +02:00
jaseg
d09cf6ef3b svg-flatten: Add 'complete pattern tiles only' switch
This is not part of the SVG spec, but it is useful for generating proto
boards using SVG patterns.
2022-06-20 10:24:26 +02:00
jaseg
f58cca0ba6 svg-flatten: remove debug prints 2022-06-19 21:26:35 +02:00
jaseg
446c5e5901 svg-flatten: fix patterns with offset clip path 2022-06-19 21:25:10 +02:00
jaseg
5f33356f33 protoboard: finish hole support 2022-06-19 21:24:45 +02:00
jaseg
e3d8c3a063 protoboard: add mounting holes 2022-06-19 21:02:47 +02:00
jaseg
4b83ec29de protoboard: initial version works 2022-06-19 19:50:18 +02:00
jaseg
ee4ad9d602 protoboard: initial commit 2022-06-19 18:42:02 +02:00
jaseg
676eff0a30 svg-flatten: Fix drills in patterns 2022-06-19 13:42:58 +02:00
jaseg
31b5a77c8a svg-flatten: Fix drill export 2022-06-19 00:53:55 +02:00
jaseg
13b92b0947 svg: Fix template layer IDs 2022-06-19 00:52:57 +02:00
jaseg
d8c20e6311 convert: generate better file names for weird layers 2022-06-19 00:51:52 +02:00
jaseg
14e9d7fbc2 svg-flatten: add drill handling to outline mode 2022-06-18 17:29:27 +02:00
jaseg
1622e9c943 svg-flatten-wasi: fix env var override 2022-06-17 20:37:07 +02:00
jaseg
48f78dd391 cli: fix sub script handling for convert 2022-06-17 20:36:34 +02:00
jaseg
826f414f9d cli: add direct conversion functions 2022-06-17 20:17:04 +02:00
jaseg
6b6f13d2ab cli: Straighten out zip handling 2022-06-17 20:16:28 +02:00
jaseg
13ae96092f cli: Allow direct calling of the module via python -m gerbolyze 2022-06-17 20:14:56 +02:00
jaseg
44fe22b6d4 Add uninstall/update instructions to README
Fixes #15
2022-06-17 10:45:30 +02:00
jaseg
2c9abc7e84 pkg: Fix README for pypi 2022-06-13 23:43:13 +02:00
jaseg
32b6aa650a pkg: Fix MANIFEST, unb0rk package 2022-06-13 22:56:08 +02:00
jaseg
23945b4cc6 pkg: fix gerbolyze script
this broke for no apparent reason. python packaging is terrible. so much
enterprise-grade code for what really should be a very simple job.
2022-06-13 22:19:48 +02:00
jaseg
d61d642c39 ci: Also build sdists 2022-06-12 22:08:00 +02:00
jaseg
8ccbc98706 README: Add link to gerbonara 2022-06-12 16:59:48 +02:00
jaseg
ca43fe715b Update README 2022-06-12 16:54:55 +02:00
jaseg
c339f4cec1 pkg: require minimum resvg-wasi version 2022-06-12 15:41:52 +02:00
jaseg
dddced97aa pkg: Format README for PyPI, make wasi default 2022-06-12 14:42:50 +02:00
jaseg
4d9381b84a ci: disable debian tests b/c of too old python 2022-06-12 02:04:47 +02:00
jaseg
1c75a9eebe ci: force install newer pillow version 2022-06-12 02:03:31 +02:00
jaseg
aa7d626569 ci: update testing distros 2022-06-12 00:08:54 +02:00
jaseg
952ddf4a20 tests: remove use of deprecated Pillow API 2022-06-11 22:57:09 +02:00
jaseg
527fbca7f5 ci: build gerbolyze python package 2022-06-11 22:22:29 +02:00
jaseg
225e9b3631 ci: work around more git safe.directory issues 2022-06-10 23:15:32 +02:00
jaseg
0e5b1cb999 ci: work around new git safe.directory check 2022-06-10 23:07:03 +02:00
jaseg
f55cfb4b35 svg-flatten: Make view box log output more readable 2022-06-10 22:42:31 +02:00
jaseg
2e2a7e6a41 Fix dilation script processing 2022-06-10 00:25:28 +02:00
jaseg
b5b16535c9 Basic compositing works using gerbonara 2022-05-23 00:20:22 +02:00
jaseg
973aee30b6 Templating works with gerbonara now 2022-05-21 15:34:59 +02:00
jaseg
b3807b6530 WIP 2022-05-20 16:55:28 +02:00
jaseg
2616cf46ff svg-flatten: Add forwarding logic for usvg font options 2021-09-30 20:37:49 +02:00
jaseg
bc54e8233f pypi/gerbolyze: add extra dep on svg-flatten-wasi pkg 2021-09-29 21:41:10 +02:00
jaseg
29c8245d0a Auto-discover usvg and svg-flatten properly 2021-09-29 18:44:09 +02:00
jaseg
a1e25a0cfb svg-flatten: pypi: Add optional resvg-wasi dependency 2021-09-29 17:57:18 +02:00
jaseg
7dccfc8e11 svg-flatten/tests: Only print output on error 2021-09-29 17:44:08 +02:00
jaseg
2380dcb222 svg-flatten: Remove unnecessary debug prints 2021-09-29 17:43:39 +02:00
jaseg
2184f3b1f5 Fix svg-flatten versioning logic 2021-09-29 17:35:56 +02:00
jaseg
a3cf9e6845 svg-flatten: Remove extraneous debug print 2021-09-29 17:20:59 +02:00
jaseg
a6acc8dc69 CI: do not clone recursively for PyPI publish 2021-09-29 17:06:33 +02:00
jaseg
a511e7dc82 svg-flatten: Remove leftover debug print in svg_color.c 2021-09-29 16:39:52 +02:00
jaseg
d406b1f1d0 Add rgba color support for newer resvg/usvg 2021-09-29 16:38:51 +02:00
jaseg
8a64621e8c CI/svg-flatten: add wasm builds 2021-09-29 16:38:13 +02:00
jaseg
7eb0b9d7e4 svg-flatten: Add wasmtime support 2021-09-29 16:37:59 +02:00
jaseg
c58b6573f2 Butcher upstream subprojects to remove C++ exceptions for WASI support 2021-09-29 16:37:59 +02:00
jaseg
956538a32c svg-flatten: Add multi-exclude/include tests 2021-08-18 21:39:54 +02:00
jaseg
fae8532b05 svg-flatten: Fix include/exclude logic 2021-08-18 21:28:58 +02:00
jaseg
aaade1b168 Collect various rendering params into new RenderContext object 2021-08-18 17:29:57 +02:00
jaseg
aad0ae0215 svg-flatten: zeroize polygon_sink pointer after use 2021-08-17 23:05:59 +02:00
jaseg
a084be8849 svg-flatten: Fix doc on --scale 2021-08-17 23:02:55 +02:00
jaseg
236c4a4485 svg-flatten: Fix case where document unit is not mm 2021-08-17 22:55:47 +02:00
jaseg
4ffb4c6582 Make svg-flatten auto-search for cargo dependencies in all call sites 2021-06-05 23:33:44 +02:00
jaseg
f2f0ac2416 CI: Run tests 2021-06-05 22:53:56 +02:00
jaseg
b2873329d4 Hopefully fix debian CI build 2021-06-05 22:39:02 +02:00
jaseg
831993cdc3 Fix build for stone-age debian compiler 2021-06-05 22:14:48 +02:00
jaseg
6a69f8c1d3 Add -lstdc++ for debian builds 2021-06-05 22:00:53 +02:00
jaseg
1b02cdd2e7 Add missing pugixml submodule 2021-06-05 21:53:50 +02:00
jaseg
b7cdcd396b Fix compiler issue on debian CI build 2021-06-05 21:51:40 +02:00
jaseg
094aca80b8 Remove dependency check. We don't have system dependencies anymore. 2021-06-05 21:46:26 +02:00
jaseg
cc882a4195 Throw out a bunch of test stuff, hopefully fix CI build 2021-06-05 21:43:20 +02:00
jaseg
6685b7587a Fix binary contours vectorizer
Replace teh-chin with ramer-douglas-peucker
2021-06-05 21:22:01 +02:00
jaseg
3ef3f0223e Enable (shitty) contour simplification
I think my implementation of this algorithm is broken. It does
something, but the output looks bad.
2021-06-04 23:35:26 +02:00
jaseg
a26c04873e Add vectorizer test cases 2021-06-04 23:31:00 +02:00
jaseg
2e38e66f14 Update comment on join discrepancies 2021-06-04 23:29:38 +02:00
jaseg
61887e9ee1 Add & fix vectorizer tests 2021-06-04 23:28:36 +02:00
jaseg
6193fa151e Add larger contour tracing test image 2021-06-04 21:43:14 +02:00
jaseg
018748aa23 Fix handling of dashes and joins, all tests run through now 2021-06-04 16:39:05 +02:00
jaseg
6dd7bbc38c Add additional test cases 2021-06-04 16:38:50 +02:00
jaseg
2d03b014f9 Add missing gaussian blur implementation 2021-06-04 16:38:32 +02:00
jaseg
9230d678af Add support for stroke-dashoffset 2021-06-04 00:44:38 +02:00
jaseg
f1bf25b51f Fix transform math 2021-06-04 00:34:39 +02:00
jaseg
564ab243cc Add svg-flatten SVG feature tests 2021-06-03 23:45:11 +02:00
jaseg
de0f851645 svg-flatten: build against submodule pugixml 2021-06-03 00:10:26 +02:00
jaseg
6cca4a3278 Port svg-flatten to nopencv 2021-06-02 15:05:36 +02:00
jaseg
3e58a4228b Fix small logical error in tests 2021-06-02 12:13:52 +02:00
jaseg
7b58f2efc5 Fix tests 2021-06-02 12:12:32 +02:00
jaseg
0530c365ca Add polygon area function & tests. These tests catch a single-px bug. 2021-06-02 11:57:00 +02:00
jaseg
536a34cd59 Prettify test cases 2021-06-02 11:31:38 +02:00
jaseg
d18b8a1d80 Finish chain approximation tests 2021-06-02 11:00:35 +02:00
jaseg
3b553b3a1d Chain approximation approximately working 2021-06-02 00:02:36 +02:00
jaseg
3386e586ac Work on chain approx 2021-06-01 23:36:32 +02:00
jaseg
bbf1c02e79 Contour finding tests run through 2021-05-30 20:22:59 +02:00
jaseg
d175570177 Add beginnings of minimalist contour tracing code 2021-05-30 19:39:45 +02:00
jaseg
e06bbdbe9b Make templates default to silk layer when opened in inkscape 2021-05-24 21:11:46 +02:00
jaseg
d23d148660 Bump version to v2.1.1 2021-05-24 20:54:57 +02:00
jaseg
79e8da1f46 Fix phys2doc/doc2phys for image export 2021-05-24 20:50:53 +02:00
jaseg
ca6e6abfdc Bump version to v2.1.0 2021-04-25 18:44:09 +02:00
jaseg
5bb733e559 Fix pattern rendering and some xform bugs 2021-04-25 18:42:43 +02:00
jaseg
6eb2c967a0 svg-flatten: Add support for patterned strokes on outline layers 2021-04-25 15:41:50 +02:00
jaseg
1790ef9137 README: Document outline layer handling 2021-04-25 15:41:33 +02:00
jaseg
1d6d4e4f14 svg-flatten: Add support for line thickness to board outline exporter 2021-04-25 14:41:06 +02:00
jaseg
046e827be1 README: add doc on new board outline feature 2021-04-25 14:24:31 +02:00
jaseg
a0fe2d83f7 gerbolyze: Add support for board outline layer 2021-04-25 14:16:48 +02:00
jaseg
f2c891533f svg-flatten: Add outline/edge layer mode 2021-04-25 14:03:16 +02:00
jaseg
1180ebdc1f Remove cairo dependency
We initially used Cairo for its bezier flattening algorithm. That
algorithm turned out to be a bit too imprecise at the scales we're
working at here (#17), so I ended up porting over some code from
Antigrain Graphics. The only other thing we used Cairo for was debug
output and coordinate transforms, so I just wrote the relevant vector
math in a small header file, deleted all debug output code and thus
eliminated the cairo dependency. This is a step towards Windows builds.
2021-04-25 00:20:51 +02:00
jaseg
776e0bd206 Replace cairo curve flattener from Anitgrain Graphics
This also fixes an issue where non-closed curves were not dilated
properly.
2021-04-24 20:17:42 +02:00
jaseg
89da2b3664 Fix README rst 2021-04-05 14:02:37 +02:00
jaseg
ee580d1642 Bump version to v2.0.8 2021-04-05 14:01:28 +02:00
jaseg
5d1bcd7fc0 Add MacOS support 2021-04-05 13:57:40 +02:00
jaseg
3288fb8345 Add -f/--flip-gerber-polarity option 2021-04-05 13:57:40 +02:00
jaseg
0be9f4b3ba Bump version to v2.0.6 2021-02-17 18:59:56 +01:00
jaseg
da9d7280d5 svg-flatten: add curve flattening tolerance command line param 2021-02-17 18:58:11 +01:00
jaseg
f9c5c00f51 svg-flatten: Fix fill-rule handling for filled open paths 2021-02-17 18:33:31 +01:00
jaseg
901efc75c6 Bump version to 2.0.5 2021-02-14 12:15:19 +01:00
jaseg
3fc628beec Fix handling of stroke width under transformations 2021-02-11 00:18:54 +01:00
jaseg
e8d7ca1d6c Fix readme RST syntax 2021-02-08 22:33:57 +01:00
jaseg
a3443a459b README: fix intro 2021-02-08 22:27:20 +01:00
Nein Seg
0ffb9ece31 Update README.rst 2021-02-07 23:13:38 +00:00
Ricardo (XenGi) Band
29408cb2b0 fix $PATH 2021-02-07 23:57:24 +01:00
Ricardo (XenGi) Band
99d0479d3a no need for su 2021-02-07 22:40:26 +01:00
Ricardo (XenGi) Band
f560923a9c typo 2021-02-07 22:37:19 +01:00
Ricardo (XenGi) Band
100e865394 using own build containers now 2021-02-07 22:34:02 +01:00
Ricardo (XenGi) Band
d4c7e8d344 give ubuntu/debian it's own special thing 2021-02-07 20:47:01 +01:00
Ricardo (XenGi) Band
d52b0f20e5 ubuntu fixed 2021-02-07 20:41:53 +01:00
Ricardo (XenGi) Band
cb67de412c fixed various typos 2021-02-07 20:38:00 +01:00
Ricardo (XenGi) Band
f52dea8eac fixed bash string replacement 2021-02-07 20:36:46 +01:00
Ricardo (XenGi) Band
69a274189a put dependencies into variables 2021-02-07 20:32:29 +01:00
Ricardo (XenGi) Band
0f2e8bd5ee debian should work 2021-02-07 20:24:30 +01:00
Ricardo (XenGi) Band
bcae0f16c7 fedora almost works 2021-02-07 20:01:08 +01:00
Ricardo (XenGi) Band
729ec14b66 use help message instead of version 2021-02-07 19:28:21 +01:00
Ricardo (XenGi) Band
d3a83bd0cc Install python deps with pacman 2021-02-07 19:20:53 +01:00
Ricardo (XenGi) Band
0a02e01a96 Checkout submodules before build 2021-02-07 19:09:36 +01:00
Ricardo (XenGi) Band
7e6b07a491 Now actually building the source 2021-02-07 19:03:49 +01:00
Ricardo (XenGi) Band
2fd7045c82 using pip instead of pip3 2021-02-07 18:56:29 +01:00
Ricardo (XenGi) Band
e74a0cf204 base-devel added as dependency 2021-02-07 18:53:41 +01:00
Ricardo (XenGi) Band
89a09b60ab updating arch before build 2021-02-07 18:51:54 +01:00
Ricardo (XenGi) Band
ce9a7a3082 trying to build gerbolize in archlinux 2021-02-07 18:48:08 +01:00
Ricardo (XenGi) Band
c0c019aea6 trying to build gerbolize via ci 2021-02-07 18:29:44 +01:00
jaseg
57fad5d8c3 README: Add missing pkg-config to arch install instructions 2021-02-07 14:43:08 +01:00
jaseg
59ea38c9bd README: use pcb-tools-extension fork for now 2021-02-07 14:34:45 +01:00
jaseg
be6783e803 Update description in gerboweb 2021-02-07 14:08:23 +01:00
jaseg
a4b45196df Update gerboweb 2021-02-07 13:55:21 +01:00
jaseg
5e04765842 Bump version to v2.0.4 2021-02-07 13:19:45 +01:00
jaseg
56c6443301 README: fix syntax error 2021-02-07 13:19:16 +01:00
jaseg
fe4a4001ba Bump version to v2.0.3 2021-02-06 22:16:08 +01:00
jaseg
ac65590c85 README: add info on mfg 2021-02-07 13:12:31 +01:00
jaseg
d9d06bff63 Bump version to v2.0.2 2021-02-07 13:12:31 +01:00
jaseg
4a967682d4 Make inital test run on debian, ubuntu, arch, fedora 2021-02-07 12:47:49 +01:00
jaseg
79db262858 Make svg-flatten always clean up its temporary files 2021-02-06 19:24:45 +01:00
jaseg
88faecbdc2 README: set image width 2021-02-06 19:13:27 +01:00
jaseg
a250ee6295 Fix some rst syntax errors 2021-02-06 19:01:23 +01:00
jaseg
685dbb56b2 README: Update subtraction script blurb 2021-02-06 16:01:29 +01:00
jaseg
40a1c2bceb README: Update subtract example pic 2021-02-06 15:57:46 +01:00
jaseg
3f9cdce1cb Update README w/ subtraction script example 2021-02-06 15:51:37 +01:00
jaseg
50cc3ce27c Update README 2021-02-06 15:46:53 +01:00
jaseg
3bcbb29cf3 svg-flatten: Fix invalid export of negative gerber coordinates 2021-02-06 15:36:27 +01:00
jaseg
3e323c953c Add workaround to clipper zero-height path bug 2021-02-06 14:59:44 +01:00
jaseg
13cb49b218 Add support for direct PNG/JPG input and add legacy wrapper 2021-02-06 13:26:36 +01:00
jaseg
18d857668e Small fixes, find svg-flatten in user pip install 2021-02-06 12:30:27 +01:00
jaseg
1e9d0c62f9 Fix svg-flatten install location 2021-02-06 09:34:02 +01:00
jaseg
06c2d5295d Remove usvg auto-install from setup.py 2021-02-06 00:20:40 +01:00
jaseg
1d58b4d584 Update syntax of second note
github doesn't style rst attention blocks properly
2021-02-06 00:16:42 +01:00
jaseg
f95a24ebb9 Finish up initial draft of install doc 2021-02-06 00:14:01 +01:00
jaseg
8c494f7736 Work on fedora/debian compatibility 2021-02-06 00:05:27 +01:00
jaseg
bf428103d3 Setup doc WIP 2021-02-05 23:01:48 +01:00
jaseg
6dec0a6e17 Add svg-flatten source files to sdist 2021-02-04 23:20:03 +01:00
jaseg
62622813be setup.py: Integrate C++/usvg install 2021-02-04 23:11:55 +01:00
jaseg
6aab099baa svg-flatten: Be even smarter locating usvg 2021-02-04 22:56:53 +01:00
jaseg
bd2b012740 svg-flatten: Be a bit smarter locating usvg 2021-02-04 22:53:49 +01:00
jaseg
fb9c6ac32c svg-flatten: Add dependency check 2021-02-04 22:53:33 +01:00
jaseg
bdd79a8f2b Update README 2021-02-04 22:09:17 +01:00
jaseg
91c46a07c6 Update post 2021-02-04 12:38:18 +01:00
jaseg
52a35dd63a Add post 2021-02-01 23:36:00 +01:00
jaseg
fc495607dc Update README 2021-02-01 12:46:50 +01:00
jaseg
5f4f667e17 Work on packaging 2021-02-01 00:14:41 +01:00
jaseg
6c01126ce9 Finish command a bit 2021-01-31 23:58:55 +01:00
jaseg
a68e395cb6 Advanced svg/gerber composition working 2021-01-31 22:11:34 +01:00
jaseg
49a7c6df41 WIP 2021-01-31 20:49:23 +01:00
jaseg
b5d523741c Both raster and vector template generation work now 2021-01-31 14:27:37 +01:00
jaseg
04f1cab5fc Re-did all the render code 2021-01-31 01:21:54 +01:00
jaseg
133f5bb98d README: add blurb on AGPL 2021-01-30 20:26:34 +01:00
jaseg
c5f8416b63 Merge old gerbolyze history 2021-01-30 20:19:34 +01:00
jaseg
3f3b8487d4 Move gerbolyze python files to subdirectory 2021-01-30 20:16:19 +01:00
jaseg
2133867c8a Reorg: move svg-flatten files into subdir 2021-01-30 20:01:00 +01:00
jaseg
617a42a674 README: Update commandline usage 2021-01-30 15:19:10 +01:00
jaseg
f88134f9ca Add dilation option 2021-01-30 15:18:56 +01:00
jaseg
a6540b73da Multi-layer module export working 2021-01-30 14:17:42 +01:00
jaseg
eac89409b8 Add KiCAD sexp output 2021-01-30 00:27:43 +01:00
jaseg
52dcceb87f Add support for preserveAspectRatio for both SVG and bitmap input 2021-01-29 23:20:16 +01:00
jaseg
a34efc058a Add direct image export 2021-01-29 21:58:42 +01:00
jaseg
6d1a7750c5 Per-image vectorizer selection works 2021-01-28 23:15:36 +01:00
jaseg
f65cd52304 Binary vectorizer works 2021-01-28 22:34:38 +01:00
jaseg
9711fabab7 WIP render settings 2021-01-27 01:26:54 +01:00
jaseg
5285b6dce8 Add group id include/exclude matching 2021-01-27 00:49:31 +01:00
jaseg
70d0021df1 Remove randomization from flattener 2021-01-26 23:52:10 +01:00
jaseg
f9a871f9b2 Speed up flatterner through randomization 2021-01-26 23:44:17 +01:00
jaseg
3cee5d4f01 The cavalier flattener works!!1! 2021-01-26 23:21:31 +01:00
jaseg
bc0ef634cf Prototype flattener/compositor code 2021-01-26 00:01:53 +01:00
jaseg
538b8a32b9 Command-line format selection works 2021-01-25 15:15:36 +01:00
jaseg
33848ad43c Fix build, add missing include 2021-01-25 14:56:20 +01:00
jaseg
95b198a5aa Gerber and SVG export working 2021-01-25 14:55:13 +01:00
jaseg
f7b4cc602b Initial commit 2021-01-24 18:44:56 +01:00
jaseg
c6b1c2225d remove ansible scripts, they are now in their own "infra" repo 2020-12-30 13:12:06 +01:00
jaseg
e290ac758b Add README 2020-12-30 12:11:11 +01:00
jaseg
ee35c06119 Update cgit config with cosmetic changes 2020-12-30 11:52:05 +01:00
jaseg
6fbea50682 Tag git setup foo 2020-12-30 11:38:27 +01:00
jaseg
b47ca7bbdc Add dyndns secrets to gitignore 2020-12-29 13:08:39 +01:00
jaseg
e63a7e557d Add dns, dyndns services 2020-12-29 13:08:13 +01:00
jaseg
659290677b Move most domains from jaseg.net to jaseg.de 2020-12-29 13:07:30 +01:00
jaseg
9510936fd9 gitolite.rc: Enable cgit metadata config keys 2020-12-29 13:06:57 +01:00
jaseg
6635bc6d46 Update arch container keyrings before sysupdate 2020-12-29 13:06:44 +01:00
445 changed files with 100887 additions and 3457 deletions

176
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,176 @@
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"
# --break-system-packages to work around the latest way some galaxy brain messed up python's packaging stuff even
# further just to break this CI pipeline for no reason :/
- pip3 install --user --break-system-packages 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.*$/
# Builds for debian are disabled until they get a C++20-capable compiler.
#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_rolling:
stage: build
image: "registry.gitlab.com/gerbolyze/build-containers/ubuntu:rolling"
script:
- "export CXX=clang++"
- "make -C svg-flatten"
artifacts:
name: "gerbolyze-$CI_COMMIT_REF_NAME-svg-flatten-uburl"
paths:
- svg-flatten/build/svg-flatten
- svg-flatten/build/nopencv-test
# test:svg-flatten-ubuntu_rolling:
# stage: test
# variables:
# GIT_SUBMODULE_STRATEGY: none
# image: "registry.gitlab.com/gerbolyze/build-containers/ubuntu:rolling"
# 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 --break-system-packages 'pillow>=9.1.1'
# - "python3 setup.py install --user"
# - "gerbolyze --help"
# - "make -C svg-flatten tests"
# dependencies:
# - build:svg-flatten-ubuntu_rolling
# artifacts:
# name: "gerbolyze-$CI_COMMIT_REF_NAME-test-uburl"
# when: on_failure
# paths:
# - svg-flatten/testcase-fails/*.png
# - svg-flatten/testcase-fails/*.svg
build:svg-flatten-fedora_latest:
stage: build
image: "registry.gitlab.com/gerbolyze/build-containers/fedora:latest"
script:
- "export CXX=clang++"
- "make -C svg-flatten"
artifacts:
name: "gerbolyze-$CI_COMMIT_REF_NAME-svg-flatten-fedlt"
paths:
- svg-flatten/build/svg-flatten
- svg-flatten/build/nopencv-test
#test:svg-flatten-fedora_latest:
# stage: test
# variables:
# GIT_SUBMODULE_STRATEGY: none
# image: "registry.gitlab.com/gerbolyze/build-containers/fedora: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"
# - pip3 install --user 'pillow>=9.1.1'
# - "python3 setup.py install --user"
# - "gerbolyze --help"
# - "make -C svg-flatten tests"
# dependencies:
# - build:svg-flatten-fedora_latest
# artifacts:
# name: "gerbolyze-$CI_COMMIT_REF_NAME-test-fedlt"
# 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
View file

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

View file

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

View file

@ -1,87 +1,541 @@
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 resvg-wasi
``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.
If you already have resvg installed, you can omit the resvg-wasi package.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/01import01.png
To uninstall, run
2 Import your desired artwork
.. code-block:: shell
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 +546,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 +560,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 +580,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
View 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?)

File diff suppressed because one or more lines are too long

5
bin/gerbolyze Normal file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env python3
if __name__ == '__main__':
import gerbolyze
gerbolyze.cli()

26
export_previews.py Normal file
View 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
View 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
View 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()

View 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()

BIN
gerber.zip Normal file

Binary file not shown.

View file

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

View file

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

596
gerbolyze/__init__.py Executable file
View file

@ -0,0 +1,596 @@
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
__version__ = '3.1.7'
@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('g', 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
View file

@ -0,0 +1,2 @@
import gerbolyze
gerbolyze.cli()

636
gerbolyze/protoboard.py Normal file
View 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))

View file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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

View 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

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 188 KiB

View 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

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 521 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 192 KiB

View file

@ -0,0 +1,118 @@
#s -*- 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
View 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
View 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

View 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

View file

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

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

View file

@ -7,4 +7,5 @@ plugins = python3
chdir = /var/lib/gerboweb
mount = /=gerboweb:app
env = GERBOWEB_SETTINGS=gerboweb_prod.cfg
mule = job_processor.py

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

22
gerboweb/develop-startup.sh Executable file
View 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

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 509 KiB

Before After
Before After

View file

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

View file

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

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

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

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
pics/ex-flattening.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

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