From 0cb0ed337676e80fd0968f8291e3b0baa98156d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Thu, 21 Nov 2024 22:00:43 +0100 Subject: [PATCH] Add rendering tests with reference assets --- poetry.lock | 357 +++++++++++++++++- pyproject.toml | 3 + test/assets/assetlib.py | 56 ++- .../gerberx3/A64_OLinuXino_rev_G/__init__.py | 152 +++++--- .../a64_olinuxino_rev_g_bottom_copper.py | 2 +- test/e2e/test_gerber/test_pillow.py | 87 ++++- test/e2e/test_gerber/test_shapely.py | 108 +++++- test/examples/gerberx3/api/test_examples.py | 4 +- test/tags.py | 1 + 9 files changed, 672 insertions(+), 98 deletions(-) diff --git a/poetry.lock b/poetry.lock index 762ddaf3..b44a1da7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -296,6 +296,17 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + [[package]] name = "charset-normalizer" version = "3.4.0" @@ -571,6 +582,25 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "cssselect2" +version = "0.7.0" +description = "CSS selectors for Python ElementTree" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cssselect2-0.7.0-py3-none-any.whl", hash = "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969"}, + {file = "cssselect2-0.7.0.tar.gz", hash = "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a"}, +] + +[package.dependencies] +tinycss2 = "*" +webencodings = "*" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["flake8", "isort", "pytest"] + [[package]] name = "distlib" version = "0.3.9" @@ -724,6 +754,22 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2. testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] typing = ["typing-extensions (>=4.12.2)"] +[[package]] +name = "freetype-py" +version = "2.3.0" +description = "Freetype python bindings" +optional = false +python-versions = ">=3.7" +files = [ + {file = "freetype-py-2.3.0.zip", hash = "sha256:f9b64ce3272a5c358dcee824800a32d70997fb872a0965a557adca20fce7a5d0"}, + {file = "freetype_py-2.3.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:ca7155de937af6f26bfd9f9089a6e9b01fa8f9d3040a3ddc0aeb3a53cf88f428"}, + {file = "freetype_py-2.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccdb1616794a8ad48beaa9e29d3494e6643d24d8e925cc39263de21c062ea5a7"}, + {file = "freetype_py-2.3.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c8f17c3ac35dc7cc9571ac37a00a6daa428a1a6d0fe6926a77d16066865ed5ef"}, + {file = "freetype_py-2.3.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:89cee8f4e7cf0a37b73a43a08c88703d84e3b9f9243fc665d8dc0b72a5d206a8"}, + {file = "freetype_py-2.3.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b95ccd52ff7e9bef34505f8af724cee114a3c3cc9cf13e0fd406fa0cc92b988a"}, + {file = "freetype_py-2.3.0-py3-none-win_amd64.whl", hash = "sha256:3a552265b06c2cb3fa54f86ed6fcbf045d8dc8176f9475bedddf9a1b31f5402f"}, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -1071,6 +1117,160 @@ files = [ attrs = ">=21.3.0" cattrs = "!=23.2.1" +[[package]] +name = "lxml" +version = "5.3.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.6" +files = [ + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, + {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, + {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, + {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, + {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, + {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, + {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, + {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, + {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, + {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, + {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, + {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, + {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, + {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, + {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, + {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, + {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, + {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, + {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, + {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, + {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, + {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml-html-clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.11)"] + [[package]] name = "markdown" version = "3.7" @@ -2109,6 +2309,30 @@ files = [ dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] test = ["pytest", "pytest-xdist", "setuptools"] +[[package]] +name = "pycairo" +version = "1.26.1" +description = "Python interface for cairo" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycairo-1.26.1-cp310-cp310-win32.whl", hash = "sha256:b93b9e3072826a346f1f79cb1becc403d1ba4a3971cad61d144db0fe6dcb6be8"}, + {file = "pycairo-1.26.1-cp310-cp310-win_amd64.whl", hash = "sha256:acfc76924ed668d8fea50f6cc6097b9a57ef6cd3dc3f2fa20814380d639a6dd2"}, + {file = "pycairo-1.26.1-cp310-cp310-win_arm64.whl", hash = "sha256:067191315c3b4d09cad1ec57cdb8fc1d72e2574e89389c268a94f22d4fa98b5f"}, + {file = "pycairo-1.26.1-cp311-cp311-win32.whl", hash = "sha256:56a29623aa7b4adbde5024c61ff001455b5a3def79e512742ea45ab36c3fe24b"}, + {file = "pycairo-1.26.1-cp311-cp311-win_amd64.whl", hash = "sha256:8d2889e03a095de5da9e68a589b691a3ada09d60ef18b5fc1b1b99f2a7794297"}, + {file = "pycairo-1.26.1-cp311-cp311-win_arm64.whl", hash = "sha256:7a307111de345304ed8eadd7f81ebd7fb1fc711224aa314a4e8e33af7dfa3d27"}, + {file = "pycairo-1.26.1-cp312-cp312-win32.whl", hash = "sha256:5cc1808e9e30ccd0f4d84ba7700db5aab5673f8b6b901760369ebb88a0823436"}, + {file = "pycairo-1.26.1-cp312-cp312-win_amd64.whl", hash = "sha256:36131a726f568b2dbc5e78ff50fbaa379e69db00614d46d66b1e4289caf9b1ce"}, + {file = "pycairo-1.26.1-cp312-cp312-win_arm64.whl", hash = "sha256:5577b51543ea4c283c15f436d891e9eaf6fd43fe74882adb032fba2c271f3fe9"}, + {file = "pycairo-1.26.1-cp38-cp38-win32.whl", hash = "sha256:27ec7b42c58af35dc11352881262dce4254378b0f11be0959d1c13edb4539d2c"}, + {file = "pycairo-1.26.1-cp38-cp38-win_amd64.whl", hash = "sha256:27357994d277b3fd10a45e9ef58f80a4cb5e3291fe76c5edd58d2d06335eb8e7"}, + {file = "pycairo-1.26.1-cp39-cp39-win32.whl", hash = "sha256:e68300d1c2196d1d34de3432885ae9ff78e10426fa16f765742a11c6f8fe0a71"}, + {file = "pycairo-1.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:ce049930e294c29b53c68dcaab3df97cc5de7eb1d3d8e8a9f5c77e7164cd6e85"}, + {file = "pycairo-1.26.1-cp39-cp39-win_arm64.whl", hash = "sha256:22e1db531d4ed3167a98f0ea165bfa2a30df9d6eb22361c38158c031065999a4"}, + {file = "pycairo-1.26.1.tar.gz", hash = "sha256:a11b999ce55b798dbf13516ab038e0ce8b6ec299b208d7c4e767a6f7e68e8430"}, +] + [[package]] name = "pycparser" version = "2.22" @@ -2718,6 +2942,28 @@ files = [ {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, ] +[[package]] +name = "reportlab" +version = "4.2.5" +description = "The Reportlab Toolkit" +optional = false +python-versions = "<4,>=3.7" +files = [ + {file = "reportlab-4.2.5-py3-none-any.whl", hash = "sha256:eb2745525a982d9880babb991619e97ac3f661fae30571b7d50387026ca765ee"}, + {file = "reportlab-4.2.5.tar.gz", hash = "sha256:5cf35b8fd609b68080ac7bbb0ae1e376104f7d5f7b2d3914c7adc63f2593941f"}, +] + +[package.dependencies] +chardet = "*" +freetype-py = {version = ">=2.3.0,<2.4", optional = true, markers = "extra == \"pycairo\""} +pillow = ">=9.0.0" +rlPyCairo = {version = ">=0.2.0,<1", optional = true, markers = "extra == \"pycairo\""} + +[package.extras] +accel = ["rl-accel (>=0.9.0,<1.1)"] +pycairo = ["freetype-py (>=2.3.0,<2.4)", "rlPyCairo (>=0.2.0,<1)"] +renderpm = ["rl-renderPM (>=4.0.3,<4.1)"] + [[package]] name = "requests" version = "2.32.3" @@ -2786,6 +3032,20 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.1 [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "rlpycairo" +version = "0.3.0" +description = "Plugin backend renderer for reportlab.graphics.renderPM" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rlPyCairo-0.3.0-py3-none-any.whl", hash = "sha256:f6ec712a76914f78c1491351e1d79eeb1a40d072accf5d36075e399963c17b17"}, +] + +[package.dependencies] +freetype-py = ">=2.3" +pycairo = ">=1.20.0" + [[package]] name = "ruff" version = "0.7.3" @@ -2991,6 +3251,56 @@ dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pyde doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "scipy" +version = "1.14.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"}, + {file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"}, + {file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"}, + {file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"}, + {file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"}, + {file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"}, + {file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"}, + {file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"}, + {file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"}, + {file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + [[package]] name = "secretstorage" version = "3.3.3" @@ -3092,6 +3402,22 @@ hjson = "*" [package.extras] test = ["pytest (>=7.0)"] +[[package]] +name = "svglib" +version = "1.5.1" +description = "A pure-Python library for reading and converting SVG" +optional = false +python-versions = ">=3.7" +files = [ + {file = "svglib-1.5.1.tar.gz", hash = "sha256:3ae765d3a9409ee60c0fb4d24c2deb6a80617aa927054f5bcd7fc98f0695e587"}, +] + +[package.dependencies] +cssselect2 = ">=0.2.0" +lxml = "*" +reportlab = "*" +tinycss2 = ">=0.6.0" + [[package]] name = "termcolor" version = "2.4.0" @@ -3145,6 +3471,24 @@ test = ["cmapfile", "czifile", "dask", "defusedxml", "fsspec", "imagecodecs", "l xml = ["defusedxml", "lxml"] zarr = ["fsspec", "zarr"] +[[package]] +name = "tinycss2" +version = "1.4.0" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"}, + {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["pytest", "ruff"] + [[package]] name = "tomli" version = "2.0.2" @@ -3341,6 +3685,17 @@ files = [ [package.extras] watchmedo = ["PyYAML (>=3.10)"] +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + [[package]] name = "wheel" version = "0.45.0" @@ -3384,4 +3739,4 @@ svg = ["drawsvg"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "b594b3faa5f94402981bebddc71120ef29a7b8b8a60f6e41b988f589e821764b" +content-hash = "c9cf7f0ca225d430a8ead362218d7798d43012246dd987797dea9ee1fc56fea3" diff --git a/pyproject.toml b/pyproject.toml index ef91c692..a5861954 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,10 @@ scikit-image = [ scipy = [ { version = ">=1.10.0,<1.11.0", python = ">=3.8,<3.9" }, # Pillow dropped support for 3.8 in 8.0 { version = ">=1.11.0,<2.0.0", python = ">=3.9" }, + { version = ">=1.14.1,<2.0.0", python = ">=3.13,<4.0" }, ] +svglib = "^1.5.1" +reportlab = {extras = ["pycairo"], version = "^4.2.5"} [tool.poetry.group.style] optional = true diff --git a/test/assets/assetlib.py b/test/assets/assetlib.py index 62800856..6def64f2 100644 --- a/test/assets/assetlib.py +++ b/test/assets/assetlib.py @@ -21,7 +21,7 @@ from attr import dataclass from filelock import FileLock from PIL import Image -from pydantic import BaseModel, Field, FilePath, HttpUrl +from pydantic import BaseModel, DirectoryPath, Field, HttpUrl from pydantic_core import Url if TYPE_CHECKING: @@ -181,17 +181,50 @@ def update(self, content: bytes) -> None: ) -class File(Source): - """Asset source representing a file.""" +class Directory(BaseModel): + """Asset source representing a file system location.""" - file_path: FilePath + directory: DirectoryPath @classmethod - def new(cls, file_path: Path | str) -> Self: - """Create a new FilePath instance.""" - if isinstance(file_path, str): - file_path = Path(file_path) - return cls(file_path=file_path) + def new(cls, directory: Path | str) -> Self: + """Create a new Directory instance.""" + if isinstance(directory, str): + directory = Path(directory) + return cls(directory=directory.resolve()) + + def file(self, path: str | Path) -> File: + """Get a File instance for the directory.""" + return File( + directory=self, file_path=Path(path) if isinstance(path, str) else path + ) + + +class File(Source): + """Asset source representing a file system location.""" + + directory: Directory + """Directory information.""" + + file_path: Path + """Path to the file within the directory, relative to directory root.""" + + def load(self) -> bytes: + """Load the asset from the source.""" + src = self.absolute_path + return src.read_bytes() + + def update(self, content: bytes) -> None: + """Update the asset from the source.""" + dest = self.absolute_path + dest.parent.mkdir(0o777, parents=True, exist_ok=True) + dest.touch(0o777, exist_ok=True) + dest.write_bytes(content) + + @property + def absolute_path(self) -> Path: + """Get the absolute path to the file.""" + return self.directory.directory / self.file_path SourceT = TypeVar("SourceT", bound=Source) @@ -217,6 +250,11 @@ def update(self, content: str) -> None: """Update the asset from the source.""" self.src.update(content.encode(encoding=self.encoding)) + @classmethod + def new(cls, src: SourceT) -> TextAsset[SourceT]: + """Create a new TextAsset instance.""" + return cls(src=src) + class ImageFormat(Enum): """Enumeration of image formats.""" diff --git a/test/assets/gerberx3/A64_OLinuXino_rev_G/__init__.py b/test/assets/gerberx3/A64_OLinuXino_rev_G/__init__.py index 51b4d55b..f30b21f8 100644 --- a/test/assets/gerberx3/A64_OLinuXino_rev_G/__init__.py +++ b/test/assets/gerberx3/A64_OLinuXino_rev_G/__init__.py @@ -2,57 +2,111 @@ from pathlib import Path -from pygerber.common.namespace import Namespace -from test.assets.asset import ExcellonAsset, GerberX3Asset +from test.assets.assetlib import ( + Directory, + File, + GitFile, + ImageAsset, + ImageFormat, + TextAsset, +) +from test.assets.reference import REFERENCE_REPOSITORY THIS_FILE = Path(__file__) THIS_DIRECTORY = THIS_FILE.parent +THIS_DIRECTORY_SRC = Directory.new(THIS_DIRECTORY) +NAMESPACE = "gerber/A64_OLinuXino_rev_G" -class A64_OlinuXino_Rev_G(Namespace): # noqa: N801 - A64_OlinuXino_Rev_G_B_Cu = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-B_Cu.gbr" - ) - A64_OlinuXino_Rev_G_B_Mask = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-B_Mask.gbr" - ) - A64_OlinuXino_Rev_G_B_Paste = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-B_Paste.gbr" - ) - A64_OlinuXino_Rev_G_B_SilkS = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-B_SilkS.gbr" - ) - A64_OlinuXino_Rev_G_Edge_Cuts = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-Edge_Cuts.gbr" - ) - A64_OlinuXino_Rev_G_F_Cu = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-F_Cu.gbr" - ) - A64_OlinuXino_Rev_G_F_Mask = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-F_Mask.gbr" - ) - A64_OlinuXino_Rev_G_F_Paste = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-F_Paste.gbr" - ) - A64_OlinuXino_Rev_G_F_SilkS = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-F_SilkS.gbr" - ) - A64_OlinuXino_Rev_G_In1_Cu = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-In1_Cu.gbr" - ) - A64_OlinuXino_Rev_G_In2_Cu = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-In2_Cu.gbr" - ) - A64_OlinuXino_Rev_G_In3_Cu = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-In3_Cu.gbr" - ) - A64_OlinuXino_Rev_G_In4_Cu = GerberX3Asset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-In4_Cu.gbr" - ) - A64_OlinuXino_Rev_G_NPTH = ExcellonAsset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-NPTH.drl" - ) - A64_OlinuXino_Rev_G_PTH = ExcellonAsset( - THIS_DIRECTORY / "A64-OlinuXino_Rev_G-PTH.drl" - ) - gbrjob = GerberX3Asset(THIS_DIRECTORY / "A64-OlinuXino_Rev_G-job.gbrjob") + +A64_OlinuXino_Rev_G_B_Cu = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-B_Cu.gbr") +) +A64_OlinuXino_Rev_G_B_Cu_Formatted = TextAsset[GitFile].new( + REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Cu.formatted.gbr") +) +A64_OlinuXino_Rev_G_B_Cu_png = ImageAsset[GitFile].new( + REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Cu.png"), + ImageFormat.PNG, +) +A64_OlinuXino_Rev_G_B_Cu_png_shapely = ImageAsset[GitFile].new( + REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Cu.shapely.png"), + ImageFormat.PNG, +) + +A64_OlinuXino_Rev_G_B_Mask = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-B_Mask.gbr") +) +A64_OlinuXino_Rev_G_B_Mask_Formatted = TextAsset[GitFile].new( + REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Mask.formatted.gbr") +) +A64_OlinuXino_Rev_G_B_Mask_png = ImageAsset[GitFile].new( + REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-B_Mask.png"), + ImageFormat.PNG, +) + + +A64_OlinuXino_Rev_G_B_Paste = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-B_Paste.gbr") +) + +A64_OlinuXino_Rev_G_B_SilkS = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-B_SilkS.gbr") +) + +A64_OlinuXino_Rev_G_Edge_Cuts = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-Edge_Cuts.gbr") +) + +A64_OlinuXino_Rev_G_F_Cu = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-F_Cu.gbr") +) +A64_OlinuXino_Rev_G_F_Cu_Formatted = TextAsset[GitFile].new( + REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-F_Cu.formatted.gbr") +) +A64_OlinuXino_Rev_G_F_Cu_png = ImageAsset[GitFile].new( + REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-F_Cu.png"), + ImageFormat.PNG, +) +A64_OlinuXino_Rev_G_F_Cu_png_shapely = ImageAsset[GitFile].new( + REFERENCE_REPOSITORY.file(f"{NAMESPACE}/A64-OlinuXino_Rev_G-F_Cu.shapely.png"), + ImageFormat.PNG, +) + +A64_OlinuXino_Rev_G_F_Mask = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-F_Mask.gbr") +) + +A64_OlinuXino_Rev_G_F_Paste = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-F_Paste.gbr") +) + +A64_OlinuXino_Rev_G_F_SilkS = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-F_SilkS.gbr") +) + +A64_OlinuXino_Rev_G_In1_Cu = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-In1_Cu.gbr") +) + +A64_OlinuXino_Rev_G_In2_Cu = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-In2_Cu.gbr") +) + +A64_OlinuXino_Rev_G_In3_Cu = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-In3_Cu.gbr") +) + +A64_OlinuXino_Rev_G_In4_Cu = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-In4_Cu.gbr") +) + +A64_OlinuXino_Rev_G_NPTH = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-NPTH.drl") +) + +A64_OlinuXino_Rev_G_PTH = TextAsset[File].new( + THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-PTH.drl") +) + +gbrjob = TextAsset[File].new(THIS_DIRECTORY_SRC.file("A64-OlinuXino_Rev_G-job.gbrjob")) diff --git a/test/benchmark/a64_olinuxino_rev_g_bottom_copper.py b/test/benchmark/a64_olinuxino_rev_g_bottom_copper.py index cf055b4e..64f2488c 100644 --- a/test/benchmark/a64_olinuxino_rev_g_bottom_copper.py +++ b/test/benchmark/a64_olinuxino_rev_g_bottom_copper.py @@ -4,11 +4,11 @@ from pathlib import Path from typing import cast +import test.assets.gerberx3.A64_OLinuXino_rev_G as A64_OlinuXino_Rev_G from pygerber.gerber.compiler import compile from pygerber.gerber.parser import parse from pygerber.vm import render from pygerber.vm.pillow.vm import PillowResult -from test.assets.gerberx3.A64_OLinuXino_rev_G import A64_OlinuXino_Rev_G THIS_FILE = Path(__file__) THIS_DIRECTORY = THIS_FILE.parent diff --git a/test/e2e/test_gerber/test_pillow.py b/test/e2e/test_gerber/test_pillow.py index 4c3930f6..efaecb5a 100644 --- a/test/e2e/test_gerber/test_pillow.py +++ b/test/e2e/test_gerber/test_pillow.py @@ -1,20 +1,27 @@ from __future__ import annotations import inspect +from io import BytesIO from pathlib import Path -from typing import ClassVar, Type +from typing import ClassVar, Literal, Type +import pytest +from PIL import Image + +import test.assets.gerberx3.A64_OLinuXino_rev_G as A64_OlinuXino_Rev_G from pygerber.gerber.ast.nodes import File -from pygerber.gerber.compiler import Compiler +from pygerber.gerber.compiler import compile +from pygerber.gerber.parser import parse from pygerber.gerber.parser.pyparsing.parser import Parser +from pygerber.vm import render from pygerber.vm.pillow.vm import PillowResult, PillowVirtualMachine from test.assets.asset import GerberX3Asset +from test.assets.assetlib import ImageAnalyzer, ImageAsset, TextAsset from test.assets.generated.macro import ( get_custom_circle_local_2_0, get_custom_circle_local_2_0_ring_rot_30, get_custom_circle_local_2_0_rot_30, ) -from test.assets.gerberx3.A64_OLinuXino_rev_G import A64_OlinuXino_Rev_G from test.assets.gerberx3.arc.clockwise import ClockwiseArcAssets from test.assets.gerberx3.arc.counterclockwise import CounterClockwiseArcAssets from test.assets.gerberx3.ATMEGA328 import ATMEGA328Assets @@ -41,7 +48,7 @@ def _render(self, source: GerberX3Asset, dpmm: int = 10) -> PillowResult: return self._render_ast(ast, dpmm=dpmm) def _render_ast(self, ast: File, dpmm: int = 10) -> PillowResult: - rvmc = Compiler().compile(ast) + rvmc = compile(ast) return PillowVirtualMachine(dpmm=dpmm).run(rvmc) def _save(self, result: PillowResult) -> None: @@ -58,21 +65,67 @@ def _save(self, result: PillowResult) -> None: result.get_image_no_style().save(dump_directory / f"{caller_function_name}.png") -class TestOLinuXinoRevG(PillowRenderE2E): - @tag(Tag.PILLOW) - def test_bottom_copper(self) -> None: - result = self._render(A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Cu, dpmm=100) - self._save(result) +class PillowRender: + parser: Literal["pyparsing"] = "pyparsing" + dpmm: int = 20 - @tag(Tag.PILLOW) - def test_bottom_mask(self) -> None: - result = self._render(A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Mask, dpmm=100) - self._save(result) + def create_image(self, source: str) -> Image.Image: + ast = parse(source, parser=self.parser) + rvmc = compile(ast) + result = render(rvmc, dpmm=self.dpmm) - @tag(Tag.PILLOW) - def test_bottom_paste(self) -> None: - result = self._render(A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Paste, dpmm=100) - self._save(result) + buffer = BytesIO() + result.save(buffer, file_format="PNG") + + buffer.seek(0) + return Image.open(buffer, formats=["png"]) + + def compare_with_reference( + self, reference: ImageAsset, ssim_threshold: float, image: Image.Image + ) -> None: + ia = ImageAnalyzer(reference.load()) + ia.assert_same_size(image) + ( + ia.histogram_compare_color(image) + .assert_channel_count(4) + .assert_greater_or_equal_values(0.99) + ) + assert ia.structural_similarity(image) > ssim_threshold + + +class TestOLinuXinoRevG(PillowRender): + dpmm: int = 40 + + @tag(Tag.PILLOW, Tag.OPENCV, Tag.SKIMAGE) + @pytest.mark.parametrize( + ("asset", "reference", "ssim_threshold"), + [ + ( + A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Cu, + A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Cu_png, + 0.99, + ), + ( + A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_F_Cu, + A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_F_Cu_png, + 0.99, + ), + ], + ids=["B_Cu", "F_Cu"], + ) + def test_render_pillow( + self, + asset: TextAsset, + reference: ImageAsset, + ssim_threshold: float, + *, + is_regeneration_enabled: bool, + ) -> None: + image = self.create_image(asset.load()) + if is_regeneration_enabled: + reference.update(image) + else: + self.compare_with_reference(reference, ssim_threshold, image) class TestFcPolyTest(PillowRenderE2E): diff --git a/test/e2e/test_gerber/test_shapely.py b/test/e2e/test_gerber/test_shapely.py index fc790851..f1fdb008 100644 --- a/test/e2e/test_gerber/test_shapely.py +++ b/test/e2e/test_gerber/test_shapely.py @@ -2,19 +2,27 @@ import inspect from pathlib import Path -from typing import Type +from tempfile import TemporaryDirectory +from typing import Literal, Type + +import pytest +from PIL import Image +from reportlab.graphics import renderPM +from svglib.svglib import svg2rlg from pygerber.gerber.ast.nodes.file import File from pygerber.gerber.compiler import compile from pygerber.gerber.parser import parse +from pygerber.vm import render from pygerber.vm.shapely.vm import ShapelyResult, ShapelyVirtualMachine from test.assets.asset import GerberX3Asset +from test.assets.assetlib import ImageAnalyzer, ImageAsset, TextAsset from test.assets.generated.macro import ( get_custom_circle_local_2_0, get_custom_circle_local_2_0_ring_rot_30, get_custom_circle_local_2_0_rot_30, ) -from test.assets.gerberx3.A64_OLinuXino_rev_G import A64_OlinuXino_Rev_G +from test.assets.gerberx3 import A64_OLinuXino_rev_G from test.assets.gerberx3.arc.clockwise import ClockwiseArcAssets from test.assets.gerberx3.arc.counterclockwise import CounterClockwiseArcAssets from test.assets.gerberx3.ATMEGA328 import ATMEGA328Assets @@ -35,6 +43,85 @@ OUTPUT_DUMP_DIRECTORY.mkdir(exist_ok=True) +class ShapelyRender: + parser: Literal["pyparsing"] = "pyparsing" + dpmm: int = 20 + + def create_image(self, source: str) -> Image.Image: + ast = parse(source, parser=self.parser) + rvmc = compile(ast) + result = render(rvmc, backend="shapely") + + with TemporaryDirectory() as tempdir: + svg_path = Path(tempdir) / "tmp.svg" + png_path = Path(tempdir) / "tmp.png" + + result.save(svg_path, file_format="SVG") + + drawing = svg2rlg(svg_path.as_posix()) + assert drawing is not None + + renderPM.drawToFile( + drawing, + png_path.as_posix(), + fmt="PNG", + dpi=(self.dpmm * 25.4), + ) + img = Image.open(png_path.as_posix(), formats=["png"]) + # Pillow images are lazy-loaded and since we are using temporary directory + # we have to load image before temp dir is deleted. + img.load() + + return img + + def compare_with_reference( + self, reference: ImageAsset, ssim_threshold: float, image: Image.Image + ) -> None: + ia = ImageAnalyzer(reference.load()) + ia.assert_same_size(image) + ( + ia.histogram_compare_color(image) + .assert_channel_count(3) + .assert_greater_or_equal_values(0.99) + ) + assert ia.structural_similarity(image) > ssim_threshold + + +class TestOLinuXinoRevG(ShapelyRender): + dpmm: int = 40 + + @tag(Tag.SHAPELY, Tag.EXTRAS, Tag.OPENCV, Tag.SKIMAGE, Tag.SVGLIB) + @pytest.mark.parametrize( + ("asset", "reference", "ssim_threshold"), + [ + ( + A64_OLinuXino_rev_G.A64_OlinuXino_Rev_G_B_Cu, + A64_OLinuXino_rev_G.A64_OlinuXino_Rev_G_B_Cu_png_shapely, + 0.99, + ), + ( + A64_OLinuXino_rev_G.A64_OlinuXino_Rev_G_F_Cu, + A64_OLinuXino_rev_G.A64_OlinuXino_Rev_G_F_Cu_png_shapely, + 0.99, + ), + ], + ids=["B_Cu", "F_Cu"], + ) + def test_render_shapely( + self, + asset: TextAsset, + reference: ImageAsset, + ssim_threshold: float, + *, + is_regeneration_enabled: bool, + ) -> None: + image = self.create_image(asset.load()) + if is_regeneration_enabled: + reference.update(image) + else: + self.compare_with_reference(reference, ssim_threshold, image) + + class ShapelyRenderE2E: def _render(self, source: GerberX3Asset) -> ShapelyResult: ast = parse(source.load()) @@ -58,23 +145,6 @@ def _save(self, result: ShapelyResult) -> None: result.save_svg((dump_directory / caller_function_name).with_suffix(".svg")) -class TestOLinuXinoRevG(ShapelyRenderE2E): - @tag(Tag.SHAPELY, Tag.EXTRAS) - def test_bottom_copper(self) -> None: - result = self._render(A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Cu) - self._save(result) - - @tag(Tag.SHAPELY, Tag.EXTRAS) - def test_bottom_mask(self) -> None: - result = self._render(A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Mask) - self._save(result) - - @tag(Tag.SHAPELY, Tag.EXTRAS) - def test_bottom_paste(self) -> None: - result = self._render(A64_OlinuXino_Rev_G.A64_OlinuXino_Rev_G_B_Paste) - self._save(result) - - class TestFcPolyTest(ShapelyRenderE2E): @tag(Tag.SHAPELY, Tag.EXTRAS) def test_bottom(self) -> None: diff --git a/test/examples/gerberx3/api/test_examples.py b/test/examples/gerberx3/api/test_examples.py index 6b04975d..df92c123 100644 --- a/test/examples/gerberx3/api/test_examples.py +++ b/test/examples/gerberx3/api/test_examples.py @@ -81,9 +81,9 @@ def test_no_output_examples(example_path: Path) -> None: def test_multi_layer_gerber_job() -> None: from pygerber.gerber.api._gerber_job_file import GerberJobFile - from test.assets.gerberx3.A64_OLinuXino_rev_G import A64_OlinuXino_Rev_G + import test.assets.gerberx3.A64_OLinuXino_rev_G as A64_OlinuXino_Rev_G - gerber_job = GerberJobFile.from_file(A64_OlinuXino_Rev_G.gbrjob.path) + gerber_job = GerberJobFile.from_file(A64_OlinuXino_Rev_G.gbrjob.src.absolute_path) project = gerber_job.to_project() assert len(project.top) != 0 diff --git a/test/tags.py b/test/tags.py index a81452a8..70168f3d 100644 --- a/test/tags.py +++ b/test/tags.py @@ -15,6 +15,7 @@ class Tag(Enum): LSP = "lsp" OPENCV = "opencv" SKIMAGE = "skimage" + SVGLIB = "svglib" @classmethod def _missing_(cls, value: object) -> Any: