From 1f50ed799a8d1dd580c023ad2c75473159bf3e67 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 15 Jun 2023 02:34:27 -0400 Subject: [PATCH] Implement sprite support Serve image assets sprites for MapLibre rendering, given a directory with images. ### API Per [MapLibre sprites API](https://maplibre.org/maplibre-style-spec/sprite/), we need to support the following: * `/sprite/.json` metadata about the sprite file - all coming from a single directory * `/sprite/.png` all images combined into a single PNG * `/sprite/@2x.json` same but for high DPI devices * `/sprite/@2x.png` Multiple sprite_id values can be combined into one sprite with the same pattern as for tile joining: `/sprite/,,...,[.json|.png|@2x.json|@2x.png]`. No ID renaming is done, so identical names will override one another. ### Configuration [Config file](https://maplibre.org/martin/config-file.html) and possibly CLI should have a simple option to serve sprites. The configuration may look similar to how mbtiles and pmtiles are configured: ```yaml # Publish sprite images sprites: paths: # scan this whole dir, matching all image files, and publishing it as "my_images" sprite source - /path/to/my_images sources: # named source matching source name to a directory my_sprites: /path/to/some_dir ``` Implement #705 --- Cargo.lock | 708 +++++++++++++++++- Cargo.toml | 1 + README.md | 1 + justfile | 1 + martin/Cargo.toml | 3 + martin/src/config.rs | 19 + martin/src/lib.rs | 1 + martin/src/sprites/mod.rs | 226 ++++++ martin/src/srv/mod.rs | 2 + martin/src/srv/server.rs | 45 +- martin/src/utils/error.rs | 4 + tests/config.yaml | 5 + tests/expected/configured/spr_cmp.json | 30 + tests/expected/configured/spr_cmp.png | Bin 0 -> 829 bytes tests/expected/configured/spr_cmp.png.txt | 1 + tests/expected/configured/spr_cmp_2x.json | 30 + tests/expected/configured/spr_cmp_2x.png | Bin 0 -> 1540 bytes tests/expected/configured/spr_cmp_2x.png.txt | 1 + tests/expected/configured/spr_mysrc.json | 9 + tests/expected/configured/spr_mysrc.png | Bin 0 -> 265 bytes tests/expected/configured/spr_mysrc.png.txt | 1 + tests/expected/configured/spr_mysrc_2x.json | 9 + tests/expected/configured/spr_mysrc_2x.png | Bin 0 -> 399 bytes .../expected/configured/spr_mysrc_2x.png.txt | 1 + tests/expected/configured/spr_src1.json | 23 + tests/expected/configured/spr_src1.png | Bin 0 -> 805 bytes tests/expected/configured/spr_src1.png.txt | 1 + tests/expected/configured/spr_src1_2x.json | 23 + tests/expected/configured/spr_src1_2x.png | Bin 0 -> 1486 bytes tests/expected/configured/spr_src1_2x.png.txt | 1 + tests/expected/given_config.yaml | 1 + tests/fixtures/sprites/expected/all_1.json | 30 + tests/fixtures/sprites/expected/all_1.png | Bin 0 -> 829 bytes tests/fixtures/sprites/expected/all_2.json | 30 + tests/fixtures/sprites/expected/all_2.png | Bin 0 -> 1540 bytes tests/fixtures/sprites/expected/src1_1.json | 23 + tests/fixtures/sprites/expected/src1_1.png | Bin 0 -> 805 bytes tests/fixtures/sprites/expected/src1_2.json | 23 + tests/fixtures/sprites/expected/src1_2.png | Bin 0 -> 1486 bytes tests/fixtures/sprites/expected/src2_1.json | 9 + tests/fixtures/sprites/expected/src2_1.png | Bin 0 -> 265 bytes tests/fixtures/sprites/expected/src2_2.json | 9 + tests/fixtures/sprites/expected/src2_2.png | Bin 0 -> 399 bytes .../fixtures/sprites/src1/another_bicycle.svg | 3 + tests/fixtures/sprites/src1/bear.svg | 12 + tests/fixtures/sprites/src1/sub/circle.svg | 3 + tests/fixtures/sprites/src2/bicycle.svg | 12 + tests/test.sh | 13 + 48 files changed, 1307 insertions(+), 7 deletions(-) create mode 100644 martin/src/sprites/mod.rs create mode 100644 tests/expected/configured/spr_cmp.json create mode 100644 tests/expected/configured/spr_cmp.png create mode 100644 tests/expected/configured/spr_cmp.png.txt create mode 100644 tests/expected/configured/spr_cmp_2x.json create mode 100644 tests/expected/configured/spr_cmp_2x.png create mode 100644 tests/expected/configured/spr_cmp_2x.png.txt create mode 100644 tests/expected/configured/spr_mysrc.json create mode 100644 tests/expected/configured/spr_mysrc.png create mode 100644 tests/expected/configured/spr_mysrc.png.txt create mode 100644 tests/expected/configured/spr_mysrc_2x.json create mode 100644 tests/expected/configured/spr_mysrc_2x.png create mode 100644 tests/expected/configured/spr_mysrc_2x.png.txt create mode 100644 tests/expected/configured/spr_src1.json create mode 100644 tests/expected/configured/spr_src1.png create mode 100644 tests/expected/configured/spr_src1.png.txt create mode 100644 tests/expected/configured/spr_src1_2x.json create mode 100644 tests/expected/configured/spr_src1_2x.png create mode 100644 tests/expected/configured/spr_src1_2x.png.txt create mode 100644 tests/fixtures/sprites/expected/all_1.json create mode 100644 tests/fixtures/sprites/expected/all_1.png create mode 100644 tests/fixtures/sprites/expected/all_2.json create mode 100644 tests/fixtures/sprites/expected/all_2.png create mode 100644 tests/fixtures/sprites/expected/src1_1.json create mode 100644 tests/fixtures/sprites/expected/src1_1.png create mode 100644 tests/fixtures/sprites/expected/src1_2.json create mode 100644 tests/fixtures/sprites/expected/src1_2.png create mode 100644 tests/fixtures/sprites/expected/src2_1.json create mode 100644 tests/fixtures/sprites/expected/src2_1.png create mode 100644 tests/fixtures/sprites/expected/src2_2.json create mode 100644 tests/fixtures/sprites/expected/src2_2.png create mode 100644 tests/fixtures/sprites/src1/another_bicycle.svg create mode 100644 tests/fixtures/sprites/src1/bear.svg create mode 100644 tests/fixtures/sprites/src1/sub/circle.svg create mode 100644 tests/fixtures/sprites/src2/bicycle.svg diff --git a/Cargo.lock b/Cargo.lock index 7f9eadc8d..ce9ed5c64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,6 +263,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "aho-corasick" version = "1.0.2" @@ -354,6 +363,33 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8868f09ff8cea88b079da74ae569d9b8c62a23c68c746240b704ee6f7525c89c" + +[[package]] +name = "assert_fs" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" +dependencies = [ + "anstyle", + "doc-comment", + "globwalk", + "predicates", + "predicates-core", + "predicates-tree", + "tempfile", +] + [[package]] name = "async-compression" version = "0.3.15" @@ -419,6 +455,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -449,12 +497,28 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + [[package]] name = "byteorder" version = "1.4.3" @@ -572,6 +636,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.0" @@ -613,9 +683,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] @@ -735,6 +805,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunch" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc013e70da3bfe5b552de26a1f34ecf67d61ea811251d2bf75c1324a1ecb425" + [[package]] name = "crypto-common" version = "0.1.6" @@ -755,6 +831,12 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "data-url" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" + [[package]] name = "deadpool" version = "0.9.5" @@ -802,6 +884,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -813,6 +901,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dotenvy" version = "0.15.7" @@ -900,6 +994,12 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -915,6 +1015,27 @@ dependencies = [ "instant", ] +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", +] + [[package]] name = "flate2" version = "1.0.26" @@ -925,6 +1046,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "flume" version = "0.10.14" @@ -960,6 +1087,29 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fontconfig-parser" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ab2e12762761366dcb876ab8b6e0cfa4797ddcd890575919f008b5ba655672a" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af8d8cbea8f21307d7e84bca254772981296f058a1d36b461bf4d83a7499fc9e" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.19.0", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -996,6 +1146,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.28" @@ -1117,6 +1273,40 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick 0.7.20", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + [[package]] name = "h2" version = "0.3.19" @@ -1251,6 +1441,43 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + [[package]] name = "indexmap" version = "1.9.3" @@ -1259,6 +1486,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "rayon", ] [[package]] @@ -1329,6 +1557,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + [[package]] name = "js-sys" version = "0.3.64" @@ -1338,6 +1572,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1356,6 +1599,24 @@ version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +[[package]] +name = "libdeflate-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6784b6b84b67d71b4307963d456a9c7c29f9b47c658f533e598de369e34277" +dependencies = [ + "cc", +] + +[[package]] +name = "libdeflater" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e285aa6a046fd338b2592c16bee148b2b00789138ed6b7bb56bb13d585050d" +dependencies = [ + "libdeflate-sys", +] + [[package]] name = "libsqlite3-sys" version = "0.24.2" @@ -1449,9 +1710,11 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "spreet", "subst", "thiserror", "tilejson", + "tokio", ] [[package]] @@ -1490,6 +1753,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + [[package]] name = "memmapix" version = "0.6.3" @@ -1527,6 +1799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -1541,6 +1814,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multimap" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70db9248a93dc36a36d9a47898caa007a32755c7ad140ec64eeeb50d5a730631" +dependencies = [ + "serde", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -1569,6 +1851,27 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1654,6 +1957,27 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "oxipng" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630638e107fb436644c300e781d3f17e1b04656138ba0d40564be4be3b06db32" +dependencies = [ + "bitvec", + "crossbeam-channel", + "filetime", + "image", + "indexmap", + "itertools", + "libdeflater", + "log", + "rayon", + "rgb", + "rustc-hash", + "rustc_version", + "zopfli", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1758,6 +2082,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.0" @@ -1843,6 +2173,19 @@ dependencies = [ "varint-rs", ] +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "postgis" version = "0.9.0" @@ -1920,6 +2263,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" +dependencies = [ + "anstyle", + "difflib", + "itertools", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.60" @@ -1938,6 +2309,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1990,6 +2367,12 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -2014,7 +2397,7 @@ version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.2", "memchr", "regex-syntax 0.7.2", ] @@ -2031,12 +2414,53 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "resvg" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e3d65cea36eefb28a020edb6e66341764e00cd4b426e0c1f0599b1adaa78f5" +dependencies = [ + "gif", + "jpeg-decoder", + "log", + "pico-args", + "png", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + [[package]] name = "retain_mut" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f595a457b6b8c6cda66a48503e92ee8d19342f905948f29c383200ec9eb1d8" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2074,6 +2498,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustybuzz" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162bdf42e261bee271b3957691018634488084ef577dddeb6420a9684cab2a6a" +dependencies = [ + "bitflags", + "bytemuck", + "smallvec", + "ttf-parser 0.18.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-general-category", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.13" @@ -2224,9 +2664,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", @@ -2242,6 +2682,21 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + [[package]] name = "siphasher" version = "0.3.10" @@ -2257,6 +2712,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -2292,6 +2756,25 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spreet" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357c86676a23af570a68dbc1f59d2d6442d12ac7c94c41b8317f30706f5ca05d" +dependencies = [ + "assert_fs", + "clap", + "crunch", + "exitcode", + "multimap", + "oxipng", + "png", + "rayon", + "resvg", + "serde", + "serde_json", +] + [[package]] name = "sqlformat" version = "0.2.1" @@ -2391,6 +2874,15 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + [[package]] name = "stringprep" version = "0.1.2" @@ -2448,6 +2940,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "svgtypes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed4b0611e7f3277f68c0fa18e385d9e2d26923691379690039548f867cef02a7" +dependencies = [ + "kurbo", + "siphasher", +] + [[package]] name = "syn" version = "1.0.109" @@ -2470,6 +2972,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.6.0" @@ -2493,6 +3001,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.40" @@ -2513,6 +3027,16 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tilejson" version = "0.3.2" @@ -2551,6 +3075,32 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db11798945fa5c3e5490c794ccca7c6de86d3afdd54b4eb324109939c6f37bc" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f60aa35c89ac2687ace1a2556eaaea68e8c0d47408a2e3e7f5c98a489e7281c" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2698,6 +3248,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "ttf-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633" + +[[package]] +name = "ttf-parser" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dcf002ae3b32cd25400d6df128c5babec3927cd1eb7ce813cfff20eb6c3746" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.16.0" @@ -2710,6 +3278,24 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + +[[package]] +name = "unicode-general-category" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" + [[package]] name = "unicode-ident" version = "1.0.9" @@ -2725,12 +3311,24 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-script" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" + [[package]] name = "unicode-segmentation" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.10" @@ -2760,6 +3358,67 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "usvg" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2304b933107198a910c1f3219acb65246f2b148f862703cffd51c6e62156abe" +dependencies = [ + "base64", + "log", + "pico-args", + "usvg-parser", + "usvg-text-layout", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b940fea80394e3b14cb21c83fa1b8f8a41023c25929bba68bb84a76193ebed" +dependencies = [ + "data-url", + "flate2", + "imagesize", + "kurbo", + "log", + "roxmltree", + "simplecss", + "siphasher", + "svgtypes", + "usvg-tree", +] + +[[package]] +name = "usvg-text-layout" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69dfd6119f431aa7e969b4a69f9cc8b9ae37b8ae85bb26780ccfa3beaf8b71eb" +dependencies = [ + "fontdb", + "kurbo", + "log", + "rustybuzz", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "usvg-tree", +] + +[[package]] +name = "usvg-tree" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3185eb13b6e3d3cf1817d29612251cc308d5a7e5e6235362e67efe832435c6d9" +dependencies = [ + "rctree", + "strict-num", + "svgtypes", + "tiny-skia-path", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -2870,6 +3529,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "winapi" version = "0.3.9" @@ -3024,6 +3689,39 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xmlparser" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "zopfli" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0650ae6a051326d798eb099b632f1afb0d323d25ee4ec82ffb0779512084d5" +dependencies = [ + "crc32fast", + "log", + "simd-adler32", + "typed-arena", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index aab866b6d..7774aa69f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ semver = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" +spreet = { version = "0.8", default-features = false } sqlx = { version = "0.6", features = ["offline", "sqlite", "runtime-actix-native-tls"] } subst = { version = "0.2", features = ["yaml"] } thiserror = "1" diff --git a/README.md b/README.md index c015b1de9..9eec46c95 100755 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Martin data is available via the HTTP `GET` endpoints: | `/{sourceID}/{z}/{x}/{y}` | Map Tiles | | `/{source1},...,{sourceN}` | Composite Source TileJSON | | `/{source1},...,{sourceN}/{z}/{x}/{y}` | Composite Source Tiles | +| `/sprite/{spriteID}[@2x].{json,png}` | Sprites (low and high DPI, index/png) | | `/health` | Martin server health check: returns 200 `OK` | ## Documentation diff --git a/justfile b/justfile index 48776bce8..9972283b9 100644 --- a/justfile +++ b/justfile @@ -106,6 +106,7 @@ test-int: clean-test # Run integration tests and save its output as the new expected output bless: start clean-test + cargo test --features bless-tests tests/test.sh rm -rf tests/expected mv tests/output tests/expected diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 69ea5e640..bce076731 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -25,6 +25,7 @@ path = "src/bin/main.rs" default = [] ssl = ["openssl", "postgres-openssl"] vendored-openssl = ["ssl", "openssl/vendored"] +bless-tests = [] [dependencies] actix-cors.workspace = true @@ -53,9 +54,11 @@ semver.workspace = true serde.workspace = true serde_json = { workspace = true, features = ["preserve_order"] } serde_yaml.workspace = true +spreet.workspace = true subst.workspace = true thiserror.workspace = true tilejson.workspace = true +tokio = { workspace = true, features = ["io-std"] } # Optional dependencies for openssl support openssl = { workspace = true, optional = true } diff --git a/martin/src/config.rs b/martin/src/config.rs index 33168a33f..d1f578e9a 100644 --- a/martin/src/config.rs +++ b/martin/src/config.rs @@ -15,12 +15,14 @@ use crate::mbtiles::MbtSource; use crate::pg::PgConfig; use crate::pmtiles::PmtSource; use crate::source::Sources; +use crate::sprites::{resolve_sprites, SpriteSources}; use crate::srv::SrvConfig; use crate::utils::{IdResolver, OneOrMany, Result}; use crate::Error::{ConfigLoadError, ConfigParseError, NoSources}; pub struct AllSources { pub sources: Sources, + pub sprites: SpriteSources, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -37,6 +39,9 @@ pub struct Config { #[serde(skip_serializing_if = "Option::is_none")] pub mbtiles: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sprites: Option, + #[serde(flatten)] pub unrecognized: HashMap, } @@ -70,6 +75,13 @@ impl Config { false }; + any |= if let Some(cfg) = &mut self.sprites { + res.extend(cfg.finalize("sprites.")?); + !cfg.is_empty() + } else { + false + }; + if any { Ok(res) } else { @@ -97,6 +109,12 @@ impl Config { sources.push(Box::pin(val)); } + let sprites = if let Some(v) = self.sprites.as_mut() { + resolve_sprites(v)? + } else { + SpriteSources::default() + }; + Ok(AllSources { sources: try_join_all(sources).await?.into_iter().fold( Sources::default(), @@ -105,6 +123,7 @@ impl Config { acc }, ), + sprites, }) } } diff --git a/martin/src/lib.rs b/martin/src/lib.rs index a5b1cf648..9c560b75b 100644 --- a/martin/src/lib.rs +++ b/martin/src/lib.rs @@ -14,6 +14,7 @@ pub mod mbtiles; pub mod pg; pub mod pmtiles; mod source; +pub mod sprites; pub mod srv; mod utils; diff --git a/martin/src/sprites/mod.rs b/martin/src/sprites/mod.rs new file mode 100644 index 000000000..8d0cf1842 --- /dev/null +++ b/martin/src/sprites/mod.rs @@ -0,0 +1,226 @@ +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::fmt::Debug; +use std::path::PathBuf; + +use actix_web::error::ErrorNotFound; +use futures::future::try_join_all; +use log::{info, warn}; +use spreet::fs::get_svg_input_paths; +use spreet::resvg::tiny_skia::Pixmap; +use spreet::resvg::usvg::{Error as ResvgError, Options, Tree, TreeParsing}; +use spreet::sprite::{generate_pixmap_from_svg, sprite_name, Spritesheet, SpritesheetBuilder}; +use tokio::io::AsyncReadExt; + +use crate::file_config::{FileConfigEnum, FileError}; + +#[derive(thiserror::Error, Debug)] +pub enum SpriteError { + #[error("IO error {0}: {}", .1.display())] + IoError(std::io::Error, PathBuf), + + #[error("Sprite path is not a file: {}", .0.display())] + InvalidFilePath(PathBuf), + + #[error("Sprite {0} uses bad file {}", .1.display())] + InvalidSpriteFilePath(String, PathBuf), + + #[error("No sprite files found in {}", .0.display())] + NoSpriteFilesFound(PathBuf), + + #[error("Sprite {} could not be loaded", .0.display())] + UnableToReadSprite(PathBuf), + + #[error("{0} in file {}", .1.display())] + SpriteProcessingError(spreet::error::Error, PathBuf), + + #[error("{0} in file {}", .1.display())] + SpriteParsingError(ResvgError, PathBuf), + + #[error("Unable to generate spritesheet")] + UnableToGenerateSpritesheet, +} + +pub fn resolve_sprites(config: &mut FileConfigEnum) -> Result { + let cfg = config.extract_file_config(); + let mut results = SpriteSources::default(); + + if let Some(sources) = cfg.sources { + for (id, source) in sources { + add_source(id, source.abs_path()?, &mut results); + } + }; + + if let Some(paths) = cfg.paths { + for path in paths { + let Some(name) = path.file_name() else { + warn!("Ignoring sprite source with no name from {}", path.display()); + continue; + }; + add_source(name.to_string_lossy().to_string(), path, &mut results); + } + } + + Ok(results) +} + +fn add_source(id: String, path: PathBuf, results: &mut SpriteSources) { + let disp_path = path.display(); + if path.is_file() { + warn!("Ignoring non-directory sprite source {id} from {disp_path}"); + } else { + match results.0.entry(id) { + Entry::Occupied(v) => { + warn!("Ignoring duplicate sprite source {} from {disp_path} because it was already configured for {}", + v.key(), v.get().path.display()); + } + Entry::Vacant(v) => { + info!("Configured sprite source {} from {disp_path}", v.key()); + v.insert(SpriteSource { path }); + } + } + }; +} + +#[derive(Debug, Clone, Default)] +pub struct SpriteSources(HashMap); + +impl SpriteSources { + pub fn get_sprite_source(&self, id: &str) -> actix_web::Result<&SpriteSource> { + self.0 + .get(id) + .ok_or_else(|| ErrorNotFound(format!("Sprite {id} does not exist"))) + } +} + +#[derive(Clone, Debug)] +pub struct SpriteSource { + path: PathBuf, +} + +async fn parse_sprite( + name: String, + path: PathBuf, + pixel_ratio: u8, +) -> Result<(String, Pixmap), SpriteError> { + let on_err = |e| SpriteError::IoError(e, path.clone()); + + let mut file = tokio::fs::File::open(&path).await.map_err(on_err)?; + + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).await.map_err(on_err)?; + + let tree = Tree::from_data(&buffer, &Options::default()) + .map_err(|e| SpriteError::SpriteParsingError(e, path.clone()))?; + + let pixmap = generate_pixmap_from_svg(&tree, pixel_ratio) + .ok_or_else(|| SpriteError::UnableToReadSprite(path.clone()))?; + + Ok((name, pixmap)) +} + +pub async fn get_spritesheet( + sources: impl Iterator, + pixel_ratio: u8, +) -> Result { + // Asynchronously load all SVG files from the given sources + let sprites = try_join_all(sources.flat_map(|source| { + get_svg_input_paths(&source.path, true) + .into_iter() + .map(|svg_path| { + let name = sprite_name(&svg_path, &source.path); + parse_sprite(name, svg_path, pixel_ratio) + }) + .collect::>() + })) + .await?; + + let mut builder = SpritesheetBuilder::new(); + builder + .sprites(sprites.into_iter().collect()) + .pixel_ratio(pixel_ratio); + + // TODO: decide if this is needed and/or configurable + // builder.make_unique(); + + builder + .generate() + .ok_or(SpriteError::UnableToGenerateSpritesheet) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + use crate::file_config::FileConfig; + use crate::OneOrMany::Many; + + #[actix_rt::test] + async fn test_sprites() { + let config = FileConfig { + paths: Some(Many(vec![ + PathBuf::from("../tests/fixtures/sprites/src1"), + PathBuf::from("../tests/fixtures/sprites/src2"), + ])), + ..FileConfig::default() + }; + + let sprites = resolve_sprites(&mut FileConfigEnum::Config(config)) + .unwrap() + .0; + assert_eq!(sprites.len(), 2); + + test_src(sprites.values(), 1, "all_1").await; + test_src(sprites.values(), 2, "all_2").await; + + test_src(sprites.get("src1").into_iter(), 1, "src1_1").await; + test_src(sprites.get("src1").into_iter(), 2, "src1_2").await; + + test_src(sprites.get("src2").into_iter(), 1, "src2_1").await; + test_src(sprites.get("src2").into_iter(), 2, "src2_2").await; + } + + async fn test_src( + sources: impl Iterator, + pixel_ratio: u8, + filename: &str, + ) { + let path = PathBuf::from(format!("../tests/fixtures/sprites/expected/{filename}")); + + let sprites = get_spritesheet(sources, pixel_ratio).await.unwrap(); + let mut json = serde_json::to_string_pretty(sprites.get_index()).unwrap(); + json.push('\n'); + let png = sprites.encode_png().unwrap(); + + #[cfg(feature = "bless-tests")] + { + use std::io::Write as _; + let mut file = std::fs::File::create(path.with_extension("json")).unwrap(); + file.write_all(json.as_bytes()).unwrap(); + + let mut file = std::fs::File::create(path.with_extension("png")).unwrap(); + file.write_all(&png).unwrap(); + } + + #[cfg(not(feature = "bless-tests"))] + { + let expected = std::fs::read_to_string(path.with_extension("json")) + .expect("Unable to open expected JSON file, make sure to bless tests with\n cargo test --features bless-tests\n"); + + assert_eq!( + serde_json::from_str::(&json).unwrap(), + serde_json::from_str::(&expected).unwrap(), + "Make sure to run bless if needed:\n cargo test --features bless-tests\n\n{json}", + ); + + let expected = std::fs::read(path.with_extension("png")) + .expect("Unable to open expected PNG file, make sure to bless tests with\n cargo test --features bless-tests\n"); + + assert_eq!( + png, expected, + "Make sure to run bless if needed:\n cargo test --features bless-tests\n\n{json}", + ); + } + } +} diff --git a/martin/src/srv/mod.rs b/martin/src/srv/mod.rs index 6496a9230..bad4343b9 100644 --- a/martin/src/srv/mod.rs +++ b/martin/src/srv/mod.rs @@ -3,3 +3,5 @@ mod server; pub use config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; pub use server::{new_server, router, RESERVED_KEYWORDS}; + +pub use crate::source::IndexEntry; diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 0219ea3ea..3c0864285 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -6,7 +6,8 @@ use actix_http::ContentEncoding; use actix_web::dev::Server; use actix_web::error::ErrorBadRequest; use actix_web::http::header::{ - AcceptEncoding, Encoding as HeaderEnc, HeaderValue, Preference, CACHE_CONTROL, CONTENT_ENCODING, + AcceptEncoding, ContentType, Encoding as HeaderEnc, HeaderValue, Preference, CACHE_CONTROL, + CONTENT_ENCODING, }; use actix_web::http::Uri; use actix_web::middleware::TrailingSlash; @@ -19,10 +20,12 @@ use futures::future::try_join_all; use log::error; use martin_tile_utils::{Encoding, Format, TileInfo}; use serde::Deserialize; +use spreet::sprite::Spritesheet; use tilejson::{tilejson, TileJSON}; use crate::config::AllSources; use crate::source::{Source, Sources, UrlQuery, Xyz}; +use crate::sprites::{get_spritesheet, SpriteSources}; use crate::srv::config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; use crate::utils::{decode_brotli, decode_gzip, encode_brotli, encode_gzip}; use crate::Error::BindingError; @@ -87,6 +90,41 @@ async fn get_catalog(sources: Data) -> impl Responder { HttpResponse::Ok().json(sources.get_catalog()) } +#[route("/sprite/{source_ids}.png", method = "GET", method = "HEAD")] +async fn get_sprite_png( + path: Path, + sprites: Data, +) -> Result { + let ss = get_sprite_int(&path.source_ids, &sprites).await?; + Ok(HttpResponse::Ok() + .content_type(ContentType::png()) + .body(ss.encode_png().map_err(map_internal_error)?)) +} + +#[route("/sprite/{source_ids}.json", method = "GET", method = "HEAD")] +async fn get_sprite_json( + path: Path, + sprites: Data, +) -> Result { + let ss = get_sprite_int(&path.source_ids, &sprites).await?; + Ok(HttpResponse::Ok().json(ss.get_index())) +} + +async fn get_sprite_int(ids: &str, sprites: &SpriteSources) -> Result { + let (ids, dpi) = if let Some(ids) = ids.strip_suffix("@2x") { + (ids, 2) + } else { + (ids, 1) + }; + let sprite_ids = ids + .split(',') + .map(|id| sprites.get_sprite_source(id)) + .collect::>>()?; + get_spritesheet(sprite_ids.into_iter(), dpi) + .await + .map_err(map_internal_error) +} + #[route( "/{source_ids}", method = "GET", @@ -364,7 +402,9 @@ pub fn router(cfg: &mut web::ServiceConfig) { .service(get_index) .service(get_catalog) .service(git_source_info) - .service(get_tile); + .service(get_tile) + .service(get_sprite_json) + .service(get_sprite_png); } /// Create a new initialized Actix `App` instance together with the listening address. @@ -382,6 +422,7 @@ pub fn new_server(config: SrvConfig, all_sources: AllSources) -> crate::Result<( App::new() .app_data(Data::new(all_sources.sources.clone())) + .app_data(Data::new(all_sources.sprites.clone())) .wrap(cors_middleware) .wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly)) .wrap(middleware::Logger::default()) diff --git a/martin/src/utils/error.rs b/martin/src/utils/error.rs index b9a945d1f..44829c34e 100644 --- a/martin/src/utils/error.rs +++ b/martin/src/utils/error.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use crate::file_config::FileError; use crate::pg::PgError; +use crate::sprites::SpriteError; pub type Result = std::result::Result; @@ -34,4 +35,7 @@ pub enum Error { #[error("{0}")] FileError(#[from] FileError), + + #[error("{0}")] + SpriteError(#[from] SpriteError), } diff --git a/tests/config.yaml b/tests/config.yaml index 0019cf255..15e39c09f 100644 --- a/tests/config.yaml +++ b/tests/config.yaml @@ -155,3 +155,8 @@ postgres: pmtiles: sources: pmt: tests/fixtures/files/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles + +sprites: + paths: tests/fixtures/sprites/src1 + sources: + mysrc: tests/fixtures/sprites/src2 diff --git a/tests/expected/configured/spr_cmp.json b/tests/expected/configured/spr_cmp.json new file mode 100644 index 000000000..7828c798e --- /dev/null +++ b/tests/expected/configured/spr_cmp.json @@ -0,0 +1,30 @@ +{ + "another_bicycle": { + "height": 15, + "pixelRatio": 1, + "width": 15, + "x": 20, + "y": 16 + }, + "bear": { + "height": 16, + "pixelRatio": 1, + "width": 16, + "x": 20, + "y": 0 + }, + "bicycle": { + "height": 15, + "pixelRatio": 1, + "width": 15, + "x": 35, + "y": 16 + }, + "sub/circle": { + "height": 20, + "pixelRatio": 1, + "width": 20, + "x": 0, + "y": 0 + } +} diff --git a/tests/expected/configured/spr_cmp.png b/tests/expected/configured/spr_cmp.png new file mode 100644 index 0000000000000000000000000000000000000000..fdd2c95dc8ca98b6d7884f207536009067c42ba5 GIT binary patch literal 829 zcmeAS@N?(olHy`uVBq!ia0vp^MnEjj!3-oPUCzs9U|>`T@Ck7Ra_1)){0CAD3`eJA zPmj=;8mbLsPYczU9HIeaObO9~$U?|HUFkpuggqrxb84{Wln_mjW(EeJdbmoUIFLOh z7$gEAA!>l+rpi!=Vj#J?&>OA=$lXvGx}h>;Nv89z_Eco?ovq31%7PZ9JFKq^frtYY zAD>ZqY+Bi-nuzJ)x>G~7ULITjc>l6nJD1$tFzxi(nI~u0o}68CWJ<}t-W;IzEp^eG z>!K!B=dR56YR*rZpJX{R%CIfo1?aX$Pi3H)F(#5;S|TQzO4_Q5s-j$cJlwq8oIqPL zIx=*DlygatUoeA!fPlP$fq}h)gTKFjKtOnWLPBPKK|y^(Lqq?B3G?SKSg>IIhW+~w z95{Xc{DliwuU@@=|Nesq51&7O`~Llhk3WC@`RlA4EddPrPEQxdkcwM-FJ%V5GLT?> zP^=%bKFl}dj<`#~LarSl1Hi&;#NKxQ7SeHewYTDtn=C8ru6rMwZ~8Hjb=xAPE`wj?E{Dt5bIueB`*>B>#;u)q zeW^@?u4nyeF%{3nqHBKaZ2s6$I8#}efDIR9UqF!cb$$ap2VU4F8hAApH%&EZN78+H|+SZI1(7g44$rjF6*2U FngHO!OPBxv literal 0 HcmV?d00001 diff --git a/tests/expected/configured/spr_cmp.png.txt b/tests/expected/configured/spr_cmp.png.txt new file mode 100644 index 000000000..85e2609b4 --- /dev/null +++ b/tests/expected/configured/spr_cmp.png.txt @@ -0,0 +1 @@ +tests/output/configured/spr_cmp.png: PNG image data, 50 x 31, 8-bit colormap, non-interlaced diff --git a/tests/expected/configured/spr_cmp_2x.json b/tests/expected/configured/spr_cmp_2x.json new file mode 100644 index 000000000..b95ec54b2 --- /dev/null +++ b/tests/expected/configured/spr_cmp_2x.json @@ -0,0 +1,30 @@ +{ + "another_bicycle": { + "height": 30, + "pixelRatio": 2, + "width": 30, + "x": 40, + "y": 32 + }, + "bear": { + "height": 32, + "pixelRatio": 2, + "width": 32, + "x": 40, + "y": 0 + }, + "bicycle": { + "height": 30, + "pixelRatio": 2, + "width": 30, + "x": 70, + "y": 32 + }, + "sub/circle": { + "height": 40, + "pixelRatio": 2, + "width": 40, + "x": 0, + "y": 0 + } +} diff --git a/tests/expected/configured/spr_cmp_2x.png b/tests/expected/configured/spr_cmp_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..31167b4f31899f01631d1fe20b6998864a528288 GIT binary patch literal 1540 zcmYjRc|6o<7#}N37^&8o5JMSa$d#O9e3&7I89UfaANkXx|UFEay=kq?#_j#W0_j#Z9{o_qT+ggGJr3FDC5EyBNZ~**8 zU>ghY0(M@-b^`?B;h=4>7C^Y%UsK`%+XWB^^oZ(T;B;ya(;QXz04LKg-|-Ys&BJ~# zv4fC}u`X0BG#{g~$7xOXreaRf{-XhzmK4JGCO;(mS}OLtg!V&wHy(eNga_n+WKUHW zi)&1}u!r~TjX=vi!vDwEZ?#`4a@S>toZk=kK^4)8`s29$WcRhGm(_V?7Y)lV8I@l$ z8D^7<-1UH4kJnu*bb%E*!#+H&dCRGs<=hx=W==H~Pc;?~*VD&p^M|XlhpMvr%9Gp5 zvY*mpA5z1*^1|A)0@^eEo0B~o;(n`-bE%HRK z^76{c%IezM=gsZy?XNpKi;?GNKpa_1|Zo z=8Y9{6DA4d^TqTWhdvuwYx$XBC3{!HqAAwvjRPSr_)sIgTl@oart2vS&C12R-IQtpD7Z7S>NQknmD^R}-REbQ zJw`?xyI?)RzGLCbKjwo%dJBmN7jhom;}((fnb+STB(it2~DI-tnJTo!qu zSK;B>MkhouOS!aF8P2$hw9$G95>7|MqTqgWZ6@k)@Pkr_)Ep1vm$Gd6ao$`d9)?8E zXE~FM{E0@3q2SpOf|M`qt0^S5G4=8)%8jBn)}p$(Ch>ZrJ=B|ybI4UYa|@ExjL`aa zQYmFu<1L7;$}Ek=VjhBbOw#TgEc_Qr*&f?21R>jK%?Sd)<0k%WO}0 z@I5)1(K>CXd!_3|hl`&-=Eq^XV%FDBuNfupYUE6>mN)rZb0rra@%5ITtV_{|v2z+r z6%lIFy3q7%n`Xmbt9Wo(g<+InM;wOTMbHvApeysaGVeS~2&ku|h3{lW`5;oXZ3vKa z>=OI29K?I?XJ(38%t`B^GXrlBDGf2f(1{gcYMPAsc$+VZ$Lg>vlW}6v4HdYELD+St z1zhg&CX1zh^hEGtn8rcb>Y`$g144TWpm{YmG#+{9&nLU7A6cm($XD40m5=l(5OU&0 z+i%)Td_P8SvrOl%g>jF9zpiUJ_G%QI?f6P_)Bs%fMUaL-Th< zC__XF6CNh0uHeejqxI}?2a+#|7cJE@oiZ1uBj*5|z_vnu$;Ary4Sq1zjT3A)WF>|I z{1&*1k0Pyv RR|5D8Ku8N)L^a$a;U6mBw2%M* literal 0 HcmV?d00001 diff --git a/tests/expected/configured/spr_cmp_2x.png.txt b/tests/expected/configured/spr_cmp_2x.png.txt new file mode 100644 index 000000000..d8b7a3380 --- /dev/null +++ b/tests/expected/configured/spr_cmp_2x.png.txt @@ -0,0 +1 @@ +tests/output/configured/spr_cmp_2x.png: PNG image data, 100 x 62, 8-bit colormap, non-interlaced diff --git a/tests/expected/configured/spr_mysrc.json b/tests/expected/configured/spr_mysrc.json new file mode 100644 index 000000000..33c5a847b --- /dev/null +++ b/tests/expected/configured/spr_mysrc.json @@ -0,0 +1,9 @@ +{ + "bicycle": { + "height": 15, + "pixelRatio": 1, + "width": 15, + "x": 0, + "y": 0 + } +} diff --git a/tests/expected/configured/spr_mysrc.png b/tests/expected/configured/spr_mysrc.png new file mode 100644 index 0000000000000000000000000000000000000000..490e533575d7ca1e9a1d80119835d0ec18ca91c8 GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^{2R(9&C|E}ZL-!()&ku6((b=948%XJp9e{)_Y&S&yf+&?Lb|Lg*`?Gv@% yTNr6c-0Cx2*__F^LMJ;XM|oOcA*bH|KOY$%PhttUwBSev$U~m4elF{r5}E)>6iu`M literal 0 HcmV?d00001 diff --git a/tests/expected/configured/spr_mysrc.png.txt b/tests/expected/configured/spr_mysrc.png.txt new file mode 100644 index 000000000..9be03516b --- /dev/null +++ b/tests/expected/configured/spr_mysrc.png.txt @@ -0,0 +1 @@ +tests/output/configured/spr_mysrc.png: PNG image data, 15 x 15, 8-bit colormap, non-interlaced diff --git a/tests/expected/configured/spr_mysrc_2x.json b/tests/expected/configured/spr_mysrc_2x.json new file mode 100644 index 000000000..b7ec269c2 --- /dev/null +++ b/tests/expected/configured/spr_mysrc_2x.json @@ -0,0 +1,9 @@ +{ + "bicycle": { + "height": 30, + "pixelRatio": 2, + "width": 30, + "x": 0, + "y": 0 + } +} diff --git a/tests/expected/configured/spr_mysrc_2x.png b/tests/expected/configured/spr_mysrc_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c5269e3201467167b7d5b1d8efdd0e539dbccdab GIT binary patch literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX3?zBp#Z3TG-T^)#u0Wbv;8}uI3D7Kwk|4ie z1_1>FdxwC8g8GL32@BS5*ni;sh5HZQfB60C(l4Nz+nz3tAr-gIP6*69tRTQDZ}DKp zwqtE9=l}XgH3o)Se%U>fKjGV3FYPaOISiil8-peNbF&UqX!Skvd*ID$-J^T-O10L5 z_UN;d(kg_$o;ssedtf>1;`kY=2e%#Aa!AKjZi3k5<6LtO8G5cO`K;UeBIOTXinb+# zzW8g+;2BqMZ#etD@dnq?7d6|K&bxE<&NjEVEUI%gTe~DWM4f@0pBy literal 0 HcmV?d00001 diff --git a/tests/expected/configured/spr_mysrc_2x.png.txt b/tests/expected/configured/spr_mysrc_2x.png.txt new file mode 100644 index 000000000..826bb7405 --- /dev/null +++ b/tests/expected/configured/spr_mysrc_2x.png.txt @@ -0,0 +1 @@ +tests/output/configured/spr_mysrc_2x.png: PNG image data, 30 x 30, 8-bit colormap, non-interlaced diff --git a/tests/expected/configured/spr_src1.json b/tests/expected/configured/spr_src1.json new file mode 100644 index 000000000..578634f50 --- /dev/null +++ b/tests/expected/configured/spr_src1.json @@ -0,0 +1,23 @@ +{ + "another_bicycle": { + "height": 15, + "pixelRatio": 1, + "width": 15, + "x": 20, + "y": 16 + }, + "bear": { + "height": 16, + "pixelRatio": 1, + "width": 16, + "x": 20, + "y": 0 + }, + "sub/circle": { + "height": 20, + "pixelRatio": 1, + "width": 20, + "x": 0, + "y": 0 + } +} diff --git a/tests/expected/configured/spr_src1.png b/tests/expected/configured/spr_src1.png new file mode 100644 index 0000000000000000000000000000000000000000..8880b9cb4e6303926df8de0614301a97278ef4cd GIT binary patch literal 805 zcmeAS@N?(olHy`uVBq!ia0vp^DnKmH!3-pKB+oy>z`&>w;1l8s-WK zo*tnwHB=kOo))SxIYa}CFyaUgq2 zFh~SKLev1sO_iY##Xxd(p*LI$kh`HWbVFsxl1%4a?WxG(J6n_2l?5$IcUWH;0ucu) zK0c%J*tD`uH4)Rpb*F}Ey*#%5@&0AEb}qTOVcO}nGf&R0JvqDP$drp2Y-M6fPnD$goMoef`a;nhKBwL6XwrfuwcRZ4g2>W zIB@#>`3o1WUcGw#{{06J9zK8m_Wk=0AAkP*^VeB9S^^mKg`O^sAr-gQUc4D}$Uvg) zq5r|PUd;<0v2D@_h;$KjVHIyGWfVCY5L5iWJdw+9#g;E8(wBWMS9`xDvuokgjEe?F z7gc$FXwPjDXTIn3{0#qOt*(WOxE5YoP*qe`S6EnQw({~Z(Uuj78+jKvXKp^nw(h;g z+Y@eG4D40ip5K3I3K$$)@!{R(iN6+#Uw-~LP>{p8+ce?whQNtwVulMc`X8lzxt4xt z`Ho8siPIY+UVWW$Vfw`0r=@yY761FEk!RL0XYa|x#?r61_0QR_xR+||a3}Mz zhkZcR=RYo*xvh*WbNU#9)xTU9^jZ3G8B5~RNqjLn2bNgba9H>3-7@=Z+4PN}Gn5|N z-v~UicH)&uclx^~8m<3#_K{_wUEx)sDU1a>yjdC*z8?@hnY4FV>zyu<^PleSN$>tF zGV{t64vzbuC)7kLnzHt7;(E5KQ>Slg0Fz6zGT)Qt2M<0>QLjsiV{|aFl%2SWf8Q#l j<;IH&eJ6gF|I1J@X~Qo0{f<9?amnE6>gTe~DWM4f!2eFU literal 0 HcmV?d00001 diff --git a/tests/expected/configured/spr_src1.png.txt b/tests/expected/configured/spr_src1.png.txt new file mode 100644 index 000000000..c4191fec8 --- /dev/null +++ b/tests/expected/configured/spr_src1.png.txt @@ -0,0 +1 @@ +tests/output/configured/spr_src1.png: PNG image data, 36 x 31, 8-bit colormap, non-interlaced diff --git a/tests/expected/configured/spr_src1_2x.json b/tests/expected/configured/spr_src1_2x.json new file mode 100644 index 000000000..7ac8efa9c --- /dev/null +++ b/tests/expected/configured/spr_src1_2x.json @@ -0,0 +1,23 @@ +{ + "another_bicycle": { + "height": 30, + "pixelRatio": 2, + "width": 30, + "x": 40, + "y": 32 + }, + "bear": { + "height": 32, + "pixelRatio": 2, + "width": 32, + "x": 40, + "y": 0 + }, + "sub/circle": { + "height": 40, + "pixelRatio": 2, + "width": 40, + "x": 0, + "y": 0 + } +} diff --git a/tests/expected/configured/spr_src1_2x.png b/tests/expected/configured/spr_src1_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f39bce52f42d6ee6f7e57755548a4acfb453716d GIT binary patch literal 1486 zcmYjQ3p5jY7@y--BJ#{*g_JOECMH52acv{7VKvHQr95Y~@<>h1<9cn(Ht)wI8Mh7d zCftP6Jj?W$LU|=I*Q-3cJ6AdPf6o8=e&6Hw{lD*j&X?fiU@I=FBnkik#9>ft7lDQf z?pYCGfoz37VE_O^txoo?HUg91&noZ+{}3Pmpo0>aaY1tjQ{A+8Kx=XU%}rAfO?Ukn zDi4L#MWHCJAR0nzC#N~_8U>+A{Vz@6X^QjxS>zXGmrHT|NumDY)O;5`L&0cD3( z6@{#e@!7#UvLx7cC*l8X?3UUM<#`#F-#EW(?g9n|O8NENuCpsM;teyMe#NxN&#cJL zoLf%H^Exez_Sr4F%?A`_BZ9Cu6j&Pq!XE>wM zuy&7}^suUQ0`yU)1vdZ^^fxL6F776VGAlyNZn>D@EA-hiszHW5i7H!nIT4E!cl&bL zCYDXlAC6Byj+Go7??SvCUsf;EJ>01rFaKq^)4v)^>`%Bf<#3{+TL%IRJUvUezI0)0 z3iS}i4%U;ad{auG>En7cLbwNgGS004TW~4)Hr>(5`^yis)pM7ra*AcjX1u^ zIqjBLQ8Jw{DIZy(fbX1rUavwhc6#A|%NOsf|*j$RSm!Xn5fGlndAh^s&1m{w?dQl~Vcpb)y13VQbJa5O@8Ml*UNmMRN|abEMyoxAf) zWR2*KepgH7md*4P>#{KX-^_IXny52bAHR00c7EgoZ-1GH_8>?zQ9_!w%`TkxNRq=* zk}>$ToJXTAblM&Co)vQK+DlSU^S2i#v{)9|Qto~L`|ROq6N$~5fRIK+>f-BJTYTy` zYg=+6>a*P`|H^$>W#|WT!>suR^~Yww5RzGzr=>NFwEl8w^Zh}({GGyHF|nnA_0#Q^ zNl!&4bXZbzAL~}9noHbOrhWC5!ylVXuP6rr*%%R0bJQ#v-$k9pG39^nBfsq;3qP6G zf#vjmW0g|EfU|Wy8%N)59m&d-zn((`#|0nEAk;*Ci|6I1^U33?#7aUzx59-l^(-Nr z>lKD9wprr*WDyb4A*y!##!@gJdGcjLx|)HXwVn9nwHSkR6}Zi9mp?@kHM$+o*(rlH zU%1J;cv0WATiRpyTEwlX$%jfRLcVHb6~Iq5eMi1elfH?KX}~;i^VrkU=BbpBM`Khu zPoh*(nc1AAtaeXm`wCaa$$@j2W?C$!7eDWAJSaL*%p_f1sn-u6tv*nh(es)>Tjp`o zGC%2jr5Zh1cR{a)!?$?CqG!~4)!(9DV&^8PO#Hx~cR>b~l}O znWo__YYZ%#Yk>4w06%5}%g_s%8bp9?V$bDLVn=zU`;yfy`;gim&%Q?bI{x;y(8X<- V{n}^jRl!yRU^WicOe^mP{{kWcos9qh literal 0 HcmV?d00001 diff --git a/tests/expected/configured/spr_src1_2x.png.txt b/tests/expected/configured/spr_src1_2x.png.txt new file mode 100644 index 000000000..309919452 --- /dev/null +++ b/tests/expected/configured/spr_src1_2x.png.txt @@ -0,0 +1 @@ +tests/output/configured/spr_src1_2x.png: PNG image data, 72 x 62, 8-bit colormap, non-interlaced diff --git a/tests/expected/given_config.yaml b/tests/expected/given_config.yaml index 30b3f053e..89ceb0d16 100644 --- a/tests/expected/given_config.yaml +++ b/tests/expected/given_config.yaml @@ -117,3 +117,4 @@ postgres: pmtiles: sources: pmt: tests/fixtures/files/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles +sprites: {} diff --git a/tests/fixtures/sprites/expected/all_1.json b/tests/fixtures/sprites/expected/all_1.json new file mode 100644 index 000000000..7828c798e --- /dev/null +++ b/tests/fixtures/sprites/expected/all_1.json @@ -0,0 +1,30 @@ +{ + "another_bicycle": { + "height": 15, + "pixelRatio": 1, + "width": 15, + "x": 20, + "y": 16 + }, + "bear": { + "height": 16, + "pixelRatio": 1, + "width": 16, + "x": 20, + "y": 0 + }, + "bicycle": { + "height": 15, + "pixelRatio": 1, + "width": 15, + "x": 35, + "y": 16 + }, + "sub/circle": { + "height": 20, + "pixelRatio": 1, + "width": 20, + "x": 0, + "y": 0 + } +} diff --git a/tests/fixtures/sprites/expected/all_1.png b/tests/fixtures/sprites/expected/all_1.png new file mode 100644 index 0000000000000000000000000000000000000000..fdd2c95dc8ca98b6d7884f207536009067c42ba5 GIT binary patch literal 829 zcmeAS@N?(olHy`uVBq!ia0vp^MnEjj!3-oPUCzs9U|>`T@Ck7Ra_1)){0CAD3`eJA zPmj=;8mbLsPYczU9HIeaObO9~$U?|HUFkpuggqrxb84{Wln_mjW(EeJdbmoUIFLOh z7$gEAA!>l+rpi!=Vj#J?&>OA=$lXvGx}h>;Nv89z_Eco?ovq31%7PZ9JFKq^frtYY zAD>ZqY+Bi-nuzJ)x>G~7ULITjc>l6nJD1$tFzxi(nI~u0o}68CWJ<}t-W;IzEp^eG z>!K!B=dR56YR*rZpJX{R%CIfo1?aX$Pi3H)F(#5;S|TQzO4_Q5s-j$cJlwq8oIqPL zIx=*DlygatUoeA!fPlP$fq}h)gTKFjKtOnWLPBPKK|y^(Lqq?B3G?SKSg>IIhW+~w z95{Xc{DliwuU@@=|Nesq51&7O`~Llhk3WC@`RlA4EddPrPEQxdkcwM-FJ%V5GLT?> zP^=%bKFl}dj<`#~LarSl1Hi&;#NKxQ7SeHewYTDtn=C8ru6rMwZ~8Hjb=xAPE`wj?E{Dt5bIueB`*>B>#;u)q zeW^@?u4nyeF%{3nqHBKaZ2s6$I8#}efDIR9UqF!cb$$ap2VU4F8hAApH%&EZN78+H|+SZI1(7g44$rjF6*2U FngHO!OPBxv literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/expected/all_2.json b/tests/fixtures/sprites/expected/all_2.json new file mode 100644 index 000000000..b95ec54b2 --- /dev/null +++ b/tests/fixtures/sprites/expected/all_2.json @@ -0,0 +1,30 @@ +{ + "another_bicycle": { + "height": 30, + "pixelRatio": 2, + "width": 30, + "x": 40, + "y": 32 + }, + "bear": { + "height": 32, + "pixelRatio": 2, + "width": 32, + "x": 40, + "y": 0 + }, + "bicycle": { + "height": 30, + "pixelRatio": 2, + "width": 30, + "x": 70, + "y": 32 + }, + "sub/circle": { + "height": 40, + "pixelRatio": 2, + "width": 40, + "x": 0, + "y": 0 + } +} diff --git a/tests/fixtures/sprites/expected/all_2.png b/tests/fixtures/sprites/expected/all_2.png new file mode 100644 index 0000000000000000000000000000000000000000..31167b4f31899f01631d1fe20b6998864a528288 GIT binary patch literal 1540 zcmYjRc|6o<7#}N37^&8o5JMSa$d#O9e3&7I89UfaANkXx|UFEay=kq?#_j#W0_j#Z9{o_qT+ggGJr3FDC5EyBNZ~**8 zU>ghY0(M@-b^`?B;h=4>7C^Y%UsK`%+XWB^^oZ(T;B;ya(;QXz04LKg-|-Ys&BJ~# zv4fC}u`X0BG#{g~$7xOXreaRf{-XhzmK4JGCO;(mS}OLtg!V&wHy(eNga_n+WKUHW zi)&1}u!r~TjX=vi!vDwEZ?#`4a@S>toZk=kK^4)8`s29$WcRhGm(_V?7Y)lV8I@l$ z8D^7<-1UH4kJnu*bb%E*!#+H&dCRGs<=hx=W==H~Pc;?~*VD&p^M|XlhpMvr%9Gp5 zvY*mpA5z1*^1|A)0@^eEo0B~o;(n`-bE%HRK z^76{c%IezM=gsZy?XNpKi;?GNKpa_1|Zo z=8Y9{6DA4d^TqTWhdvuwYx$XBC3{!HqAAwvjRPSr_)sIgTl@oart2vS&C12R-IQtpD7Z7S>NQknmD^R}-REbQ zJw`?xyI?)RzGLCbKjwo%dJBmN7jhom;}((fnb+STB(it2~DI-tnJTo!qu zSK;B>MkhouOS!aF8P2$hw9$G95>7|MqTqgWZ6@k)@Pkr_)Ep1vm$Gd6ao$`d9)?8E zXE~FM{E0@3q2SpOf|M`qt0^S5G4=8)%8jBn)}p$(Ch>ZrJ=B|ybI4UYa|@ExjL`aa zQYmFu<1L7;$}Ek=VjhBbOw#TgEc_Qr*&f?21R>jK%?Sd)<0k%WO}0 z@I5)1(K>CXd!_3|hl`&-=Eq^XV%FDBuNfupYUE6>mN)rZb0rra@%5ITtV_{|v2z+r z6%lIFy3q7%n`Xmbt9Wo(g<+InM;wOTMbHvApeysaGVeS~2&ku|h3{lW`5;oXZ3vKa z>=OI29K?I?XJ(38%t`B^GXrlBDGf2f(1{gcYMPAsc$+VZ$Lg>vlW}6v4HdYELD+St z1zhg&CX1zh^hEGtn8rcb>Y`$g144TWpm{YmG#+{9&nLU7A6cm($XD40m5=l(5OU&0 z+i%)Td_P8SvrOl%g>jF9zpiUJ_G%QI?f6P_)Bs%fMUaL-Th< zC__XF6CNh0uHeejqxI}?2a+#|7cJE@oiZ1uBj*5|z_vnu$;Ary4Sq1zjT3A)WF>|I z{1&*1k0Pyv RR|5D8Ku8N)L^a$a;U6mBw2%M* literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/expected/src1_1.json b/tests/fixtures/sprites/expected/src1_1.json new file mode 100644 index 000000000..578634f50 --- /dev/null +++ b/tests/fixtures/sprites/expected/src1_1.json @@ -0,0 +1,23 @@ +{ + "another_bicycle": { + "height": 15, + "pixelRatio": 1, + "width": 15, + "x": 20, + "y": 16 + }, + "bear": { + "height": 16, + "pixelRatio": 1, + "width": 16, + "x": 20, + "y": 0 + }, + "sub/circle": { + "height": 20, + "pixelRatio": 1, + "width": 20, + "x": 0, + "y": 0 + } +} diff --git a/tests/fixtures/sprites/expected/src1_1.png b/tests/fixtures/sprites/expected/src1_1.png new file mode 100644 index 0000000000000000000000000000000000000000..8880b9cb4e6303926df8de0614301a97278ef4cd GIT binary patch literal 805 zcmeAS@N?(olHy`uVBq!ia0vp^DnKmH!3-pKB+oy>z`&>w;1l8s-WK zo*tnwHB=kOo))SxIYa}CFyaUgq2 zFh~SKLev1sO_iY##Xxd(p*LI$kh`HWbVFsxl1%4a?WxG(J6n_2l?5$IcUWH;0ucu) zK0c%J*tD`uH4)Rpb*F}Ey*#%5@&0AEb}qTOVcO}nGf&R0JvqDP$drp2Y-M6fPnD$goMoef`a;nhKBwL6XwrfuwcRZ4g2>W zIB@#>`3o1WUcGw#{{06J9zK8m_Wk=0AAkP*^VeB9S^^mKg`O^sAr-gQUc4D}$Uvg) zq5r|PUd;<0v2D@_h;$KjVHIyGWfVCY5L5iWJdw+9#g;E8(wBWMS9`xDvuokgjEe?F z7gc$FXwPjDXTIn3{0#qOt*(WOxE5YoP*qe`S6EnQw({~Z(Uuj78+jKvXKp^nw(h;g z+Y@eG4D40ip5K3I3K$$)@!{R(iN6+#Uw-~LP>{p8+ce?whQNtwVulMc`X8lzxt4xt z`Ho8siPIY+UVWW$Vfw`0r=@yY761FEk!RL0XYa|x#?r61_0QR_xR+||a3}Mz zhkZcR=RYo*xvh*WbNU#9)xTU9^jZ3G8B5~RNqjLn2bNgba9H>3-7@=Z+4PN}Gn5|N z-v~UicH)&uclx^~8m<3#_K{_wUEx)sDU1a>yjdC*z8?@hnY4FV>zyu<^PleSN$>tF zGV{t64vzbuC)7kLnzHt7;(E5KQ>Slg0Fz6zGT)Qt2M<0>QLjsiV{|aFl%2SWf8Q#l j<;IH&eJ6gF|I1J@X~Qo0{f<9?amnE6>gTe~DWM4f!2eFU literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/expected/src1_2.json b/tests/fixtures/sprites/expected/src1_2.json new file mode 100644 index 000000000..7ac8efa9c --- /dev/null +++ b/tests/fixtures/sprites/expected/src1_2.json @@ -0,0 +1,23 @@ +{ + "another_bicycle": { + "height": 30, + "pixelRatio": 2, + "width": 30, + "x": 40, + "y": 32 + }, + "bear": { + "height": 32, + "pixelRatio": 2, + "width": 32, + "x": 40, + "y": 0 + }, + "sub/circle": { + "height": 40, + "pixelRatio": 2, + "width": 40, + "x": 0, + "y": 0 + } +} diff --git a/tests/fixtures/sprites/expected/src1_2.png b/tests/fixtures/sprites/expected/src1_2.png new file mode 100644 index 0000000000000000000000000000000000000000..f39bce52f42d6ee6f7e57755548a4acfb453716d GIT binary patch literal 1486 zcmYjQ3p5jY7@y--BJ#{*g_JOECMH52acv{7VKvHQr95Y~@<>h1<9cn(Ht)wI8Mh7d zCftP6Jj?W$LU|=I*Q-3cJ6AdPf6o8=e&6Hw{lD*j&X?fiU@I=FBnkik#9>ft7lDQf z?pYCGfoz37VE_O^txoo?HUg91&noZ+{}3Pmpo0>aaY1tjQ{A+8Kx=XU%}rAfO?Ukn zDi4L#MWHCJAR0nzC#N~_8U>+A{Vz@6X^QjxS>zXGmrHT|NumDY)O;5`L&0cD3( z6@{#e@!7#UvLx7cC*l8X?3UUM<#`#F-#EW(?g9n|O8NENuCpsM;teyMe#NxN&#cJL zoLf%H^Exez_Sr4F%?A`_BZ9Cu6j&Pq!XE>wM zuy&7}^suUQ0`yU)1vdZ^^fxL6F776VGAlyNZn>D@EA-hiszHW5i7H!nIT4E!cl&bL zCYDXlAC6Byj+Go7??SvCUsf;EJ>01rFaKq^)4v)^>`%Bf<#3{+TL%IRJUvUezI0)0 z3iS}i4%U;ad{auG>En7cLbwNgGS004TW~4)Hr>(5`^yis)pM7ra*AcjX1u^ zIqjBLQ8Jw{DIZy(fbX1rUavwhc6#A|%NOsf|*j$RSm!Xn5fGlndAh^s&1m{w?dQl~Vcpb)y13VQbJa5O@8Ml*UNmMRN|abEMyoxAf) zWR2*KepgH7md*4P>#{KX-^_IXny52bAHR00c7EgoZ-1GH_8>?zQ9_!w%`TkxNRq=* zk}>$ToJXTAblM&Co)vQK+DlSU^S2i#v{)9|Qto~L`|ROq6N$~5fRIK+>f-BJTYTy` zYg=+6>a*P`|H^$>W#|WT!>suR^~Yww5RzGzr=>NFwEl8w^Zh}({GGyHF|nnA_0#Q^ zNl!&4bXZbzAL~}9noHbOrhWC5!ylVXuP6rr*%%R0bJQ#v-$k9pG39^nBfsq;3qP6G zf#vjmW0g|EfU|Wy8%N)59m&d-zn((`#|0nEAk;*Ci|6I1^U33?#7aUzx59-l^(-Nr z>lKD9wprr*WDyb4A*y!##!@gJdGcjLx|)HXwVn9nwHSkR6}Zi9mp?@kHM$+o*(rlH zU%1J;cv0WATiRpyTEwlX$%jfRLcVHb6~Iq5eMi1elfH?KX}~;i^VrkU=BbpBM`Khu zPoh*(nc1AAtaeXm`wCaa$$@j2W?C$!7eDWAJSaL*%p_f1sn-u6tv*nh(es)>Tjp`o zGC%2jr5Zh1cR{a)!?$?CqG!~4)!(9DV&^8PO#Hx~cR>b~l}O znWo__YYZ%#Yk>4w06%5}%g_s%8bp9?V$bDLVn=zU`;yfy`;gim&%Q?bI{x;y(8X<- V{n}^jRl!yRU^WicOe^mP{{kWcos9qh literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/expected/src2_1.json b/tests/fixtures/sprites/expected/src2_1.json new file mode 100644 index 000000000..33c5a847b --- /dev/null +++ b/tests/fixtures/sprites/expected/src2_1.json @@ -0,0 +1,9 @@ +{ + "bicycle": { + "height": 15, + "pixelRatio": 1, + "width": 15, + "x": 0, + "y": 0 + } +} diff --git a/tests/fixtures/sprites/expected/src2_1.png b/tests/fixtures/sprites/expected/src2_1.png new file mode 100644 index 0000000000000000000000000000000000000000..490e533575d7ca1e9a1d80119835d0ec18ca91c8 GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^{2R(9&C|E}ZL-!()&ku6((b=948%XJp9e{)_Y&S&yf+&?Lb|Lg*`?Gv@% yTNr6c-0Cx2*__F^LMJ;XM|oOcA*bH|KOY$%PhttUwBSev$U~m4elF{r5}E)>6iu`M literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/expected/src2_2.json b/tests/fixtures/sprites/expected/src2_2.json new file mode 100644 index 000000000..b7ec269c2 --- /dev/null +++ b/tests/fixtures/sprites/expected/src2_2.json @@ -0,0 +1,9 @@ +{ + "bicycle": { + "height": 30, + "pixelRatio": 2, + "width": 30, + "x": 0, + "y": 0 + } +} diff --git a/tests/fixtures/sprites/expected/src2_2.png b/tests/fixtures/sprites/expected/src2_2.png new file mode 100644 index 0000000000000000000000000000000000000000..c5269e3201467167b7d5b1d8efdd0e539dbccdab GIT binary patch literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX3?zBp#Z3TG-T^)#u0Wbv;8}uI3D7Kwk|4ie z1_1>FdxwC8g8GL32@BS5*ni;sh5HZQfB60C(l4Nz+nz3tAr-gIP6*69tRTQDZ}DKp zwqtE9=l}XgH3o)Se%U>fKjGV3FYPaOISiil8-peNbF&UqX!Skvd*ID$-J^T-O10L5 z_UN;d(kg_$o;ssedtf>1;`kY=2e%#Aa!AKjZi3k5<6LtO8G5cO`K;UeBIOTXinb+# zzW8g+;2BqMZ#etD@dnq?7d6|K&bxE<&NjEVEUI%gTe~DWM4f@0pBy literal 0 HcmV?d00001 diff --git a/tests/fixtures/sprites/src1/another_bicycle.svg b/tests/fixtures/sprites/src1/another_bicycle.svg new file mode 100644 index 000000000..cfb8f9b3a --- /dev/null +++ b/tests/fixtures/sprites/src1/another_bicycle.svg @@ -0,0 +1,3 @@ + + + diff --git a/tests/fixtures/sprites/src1/bear.svg b/tests/fixtures/sprites/src1/bear.svg new file mode 100644 index 000000000..084a1f857 --- /dev/null +++ b/tests/fixtures/sprites/src1/bear.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/fixtures/sprites/src1/sub/circle.svg b/tests/fixtures/sprites/src1/sub/circle.svg new file mode 100644 index 000000000..2f35d511a --- /dev/null +++ b/tests/fixtures/sprites/src1/sub/circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/tests/fixtures/sprites/src2/bicycle.svg b/tests/fixtures/sprites/src2/bicycle.svg new file mode 100644 index 000000000..484d5745a --- /dev/null +++ b/tests/fixtures/sprites/src2/bicycle.svg @@ -0,0 +1,12 @@ + + + + diff --git a/tests/test.sh b/tests/test.sh index 31b77df8f..ce1ffcac6 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -251,6 +251,19 @@ test_pbf fnc_0_0_0 function_zxy_query/0/0/0 test_pbf fnc2_0_0_0 function_zxy_query_test/0/0/0?token=martin test_png pmt_0_0_0 pmt/0/0/0 +test_jsn spr_src1 sprite/src1.json +test_png spr_src1 sprite/src1.png +test_jsn spr_src1_2x sprite/src1@2x.json +test_png spr_src1_2x sprite/src1@2x.png +test_jsn spr_mysrc sprite/mysrc.json +test_png spr_mysrc sprite/mysrc.png +test_jsn spr_mysrc_2x sprite/mysrc@2x.json +test_png spr_mysrc_2x sprite/mysrc@2x.png +test_jsn spr_cmp sprite/src1,mysrc.json +test_png spr_cmp sprite/src1,mysrc.png +test_jsn spr_cmp_2x sprite/src1,mysrc@2x.json +test_png spr_cmp_2x sprite/src1,mysrc@2x.png + kill_process $PROCESS_ID validate_log test_log_2.txt