commit d052cb8152e1adec84e385da03caa4c389875eb2 Author: kp <kp@sys@patchii.net> Date: Wed Feb 19 14:28:12 2025 -0600 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7dacfe3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +insert_final_newline = true + +[*.{rs}] +indent_style = tab +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c2a1100 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1039 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bzip2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b89e7c29231c673a61a46e722602bcd138298f6b9e81e71119693534585f5c" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.12+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ebc2f1a417f01e1da30ef264ee86ae31d2dcd2d603ea283d3c244a883ca2a9" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "image" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "l2decodeV5" +version = "0.1.0" +dependencies = [ + "byteorder", + "bzip2", + "image", +] + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" + +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "winnow" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..239a32e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "l2decodeV5" +version = "0.1.0" +edition = "2021" + +[dependencies] +byteorder = "1.4" +bzip2 = "0.5.1" +image = "0.25.5" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ebc553d --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# l2decodeV5 + +This is a prototype WSR-88D Level-II (NEXRAD weather radar) data decoder and image generator written in Rust. I've never really used Rust before, so I decided to rewrite one of my old C routines to get a feel for how things work. This code probably isn't the best, but it's a start. I've come to like Rust a lot in the process of writing this over the past couple of days! + +Notes: +- Only processes reflectivity data for the first elevation scan. The code is structured such that it's possible to get other products out of it though. +- Expected input is an Archive-II file assuming RDA build 19 and later. It probably wouldn't take much to get it to support data from sites running older builds of the RDA software. + +## Output Example + + diff --git a/output.png b/output.png new file mode 100644 index 0000000..c195a6e Binary files /dev/null and b/output.png differ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..77946ce --- /dev/null +++ b/src/main.rs @@ -0,0 +1,841 @@ +// +// NEXRAD Level-II data decoder and image generator with WSR-88D Build +// 10 Message 31 support +// +use std::io::{self, Read, Seek}; +use std::fs::File; +use std::mem; +use std::str; +use std::cmp; +use byteorder::{BigEndian, ReadBytesExt}; +use bzip2::read::{BzDecoder}; +use image::{ImageBuffer, Rgba}; + +const MSG_LENGTH: usize = 2432; +const MOMENT_LENGTH: usize = 2400; + +#[repr(u8)] +enum RadialStatus { + StartElevationScan, + InterRadial, + EndElevationScan, + StartVolumeScan, + EndVolumeScan +} + +#[repr(u8)] +enum DataBlockType { + Volume, + Elevation, + Radial, + Reflectivity, + Velocity, + SpectrumWidth, + Zdr, // Differential reflectivity + Phi, // Differential phase + Rho, // Correlation coefficient +} + +// -------- +// ARCHIVE II STRUCTS +// These are implemented based on what's written in the ICD from NOAA: +// https://www.roc.noaa.gov/public-documents/icds/2620002Y.pdf +// -------- + +// At the start of every Archive II file is a 24-byte record +// describing certain attributes of the volume scan. +#[repr(C, packed)] +struct VolumeHeader { + ver: [u8; 2], + period: [u8; 1], + ext: [u8; 3], + modified_julian_date: i32, + modified_time_millis: i32, + icao_id: [u8; 4] +} + +// Level II data packet -- used for all message types +#[repr(C, packed)] +struct Packet { + ctm: [i16; 6], + size: u16, + id: u8, + message_type: u8, + seq: u16, + gen_julian_date: u16, + gen_time_millis: u32, + num_message_segments: u16, + message_segment: u16, + data: [u8; 2404] +} + +// Header data for message 31 (digital radar data) +#[repr(C, packed)] +#[derive(Copy, Clone)] +struct Message31Header { + site_id: [u8; 4], + collection_time: u32, + modified_julian_date: u16, + radial_idx: u16, + radial_azimuth: f32, + compression_indicator: u8, + spare: u8, + radial_length: u16, + azimuth_res_spacing_code: u8, + radial_status: u8, + elevation_number: u8, + cut_sector_number: u8, + elevation_angle: f32, + radial_spot_blanking_status: u8, + azimuth_indexing_mode: u8, + data_block_count: u16, + data_block_pointers: [u32; 9] +} + +// Data block (descriptor of generic data moment type) +#[repr(C, packed)] +#[derive(Copy, Clone)] +struct DataBlock { + block_type: u8, + moment_name: [u8; 3] +} + +impl Default for DataBlock { + fn default() -> Self { + Self { + block_type: 0, + moment_name: [0; 3] + } + } +} + +#[repr(C, packed)] +#[derive(Default, Copy, Clone)] +struct VolumeData { + data_block: DataBlock, + size: u16, + ver_major: u8, + ver_minor: u8, + lat: f32, + lon: f32, + site_height: i16, + feedhorn_height: u16, + calibration_constant: f32, + hor_tx_power: f32, + ver_tx_power: f32, + system_diff_refl: f32, + initial_system_diff_refl: f32, + vcp_num: u16, + processing_status: u16 +} + +#[repr(C, packed)] +#[derive(Default, Copy, Clone)] +struct ElevationData { + data_block: DataBlock, + size: u16, + atmos: [u8; 2], + calibration_constant: f32 +} + +#[repr(C, packed)] +#[derive(Default, Copy, Clone)] +struct RadialData { + data_block: DataBlock, + size: u16, + unambiguous_range: u16, + noise_level_hor: f32, + noise_level_ver: f32, + nyquist_velocity: u16, + spare: [u8; 2], + calibration_constant_channel_hor: f32, + calibration_constant_channel_ver: f32 +} + +#[repr(C, packed)] +#[derive(Default, Copy, Clone)] +struct GenericDataMoment { + data_block: DataBlock, + reserved: u32, + num_gates: u16, + range: u16, + range_sample_interval: u16, + threshold: u16, + snr_threshold: u16, + control_flags: i8, + word_size: u8, + scale: f32, + offset: f32 +} + +#[repr(C, packed)] +#[derive(Copy, Clone)] +struct DataMoment { + generic_data_moment: GenericDataMoment, + data: [u16; MOMENT_LENGTH] +} + +impl Default for DataMoment { + fn default() -> Self { + Self { + generic_data_moment: GenericDataMoment::default(), + data: [69; MOMENT_LENGTH] + } + } +} + +// -------- +// PROCESSED STRUCTS +// These aren't set to 1-byte alignment as these are *separate* from +// structs meant to be written to from raw data +// -------- + +#[derive(Copy, Clone)] +struct Message31 { + header: Message31Header, + volume_data: VolumeData, + elevation_data: ElevationData, + radial_data: RadialData, + reflectivity_data: DataMoment, + velocity_data: DataMoment, + spectrum_width_data: DataMoment, + zdr_data: DataMoment, + phi_data: DataMoment, + rho_data: DataMoment, + cfp_data: DataMoment +} + +impl Message31 { + fn new(header: Message31Header) -> Self { + Self { + header, + volume_data: VolumeData::default(), + elevation_data: ElevationData::default(), + radial_data: RadialData::default(), + reflectivity_data: DataMoment::default(), + velocity_data: DataMoment::default(), + spectrum_width_data: DataMoment::default(), + zdr_data: DataMoment::default(), + phi_data: DataMoment::default(), + rho_data: DataMoment::default(), + cfp_data: DataMoment::default() + } + } +} + +// Structure to hold radial data (radials), which contains momentary data (bins) +struct ElevationScan { + radials: Vec<Message31>, + azimuth: [f32; 800], // Expecting 720 radials, but + // overscan/underscan can occur... + first_radial_idx: u16, + first_radial_az: f32, + num_radials: i32, + num_gates: i32, + mean_elev_angle: f32, + accum_elev_angle: f32, + gate_size: f32, + first_gate: f32 +} + +impl ElevationScan { + fn new() -> Self { + Self { + radials: Vec::new(), + azimuth: [0.0; 800], + first_radial_idx: 0, + first_radial_az: 0.0, + num_radials: 0, + num_gates: 0, + mean_elev_angle: 0.0, + accum_elev_angle: 0.0, + gate_size: 0.0, + first_gate: 0.0 + } + } +} + +// Aggregate structure for any and all data coming from message packets +struct VolumeScan { + volume_header: VolumeHeader, + elevation_scans: Vec<ElevationScan> +} + +impl VolumeScan { + fn new(volume_header: VolumeHeader) -> Self { + Self { volume_header, elevation_scans: Vec::new() } + } +} + +fn check_for_volume_scan<R: Read + Seek>(mut stream: R) -> Option<VolumeScan> { + let magic_str = b"AR2V00"; + let mut magic_pos = 0; + let mut buf = [0u8; 1]; + + while magic_pos < magic_str.len() { + if stream.read_exact(&mut buf).is_err() { + println!("Stream error; EOF?"); + return None; + } + if buf[0] == magic_str[magic_pos] { + magic_pos += 1; + } else { + magic_pos = 0; + } + } + + println!("Found AR2V00xx identifier"); + + Some(process_volume_scan(stream)) +} + +fn process_volume_scan<R: Read>(mut stream: R) -> VolumeScan { + // Read volume header + // ------------------ + let mut header_buffer = [0u8; mem::size_of::<VolumeHeader>()]; + stream.read_exact(&mut header_buffer); + let volume_header: VolumeHeader = unsafe { + std::ptr::read(header_buffer.as_ptr() as *const _) + }; + // Test the ICAO ID from the volume header real quick... + // TODO: We should include some kind of check against an expected + // ICAO ID via command line arguments + let icao = str::from_utf8(&volume_header.icao_id).unwrap(); + println!("Detected ICAO ID: {}", icao); + + // Read rest of the data + // --------------------- + let mut volume_scan = VolumeScan::new(volume_header); + let mut control_word: i32 = 0; + let mut verify_control_word: i32 = 0; + + let mut bail = false; + while !bail { + let mut raw_buffer = Vec::new(); + let mut raw_buffer_size = 0; + let mut decompressed_data = Vec::new(); + // Read in the control word (size) of the LDM record. There's + // also a chance this is a new volume scan, so we need to + // check for the "magic string" we encountered earlier. + let mut control_word: i32 = 0; + match stream.read_i32::<BigEndian>() { + Ok(res) => { + control_word = res; + }, + Err(e) => { + bail = true; + } + } + if control_word == 0x56325241 /* "AR2V" but big endian */ { + // We've got another volume scan. We're bailing here, but + // really we ought to just start the decoding process over + // again from this byte. + println!("Encountered magic string twice!"); + bail = true; + continue; + } + + // Decompress the data + // ------------------- + // TODO: check if it's actually bzip2-compressed first...? + if control_word >= raw_buffer_size { + raw_buffer.resize(control_word as usize, 0); + raw_buffer_size = control_word; + } + stream.read_exact(&mut raw_buffer); + let mut decoder = BzDecoder::new(&raw_buffer[..]); + let mut decoding = true; + let mut buffer = [0u8; 8192]; + while decoding { + match decoder.read(&mut buffer) { + Ok(bytes_read) => { + if bytes_read == 0 { + decoding = false; + break; + } + decompressed_data.extend_from_slice(&buffer[..bytes_read]); + }, + Err(error) => match error.kind() { + DataMagic => { + // If we've made it here then the bzip2 magic + // string is missing. In this case, we'll + // assume it's uncompressed data, although I + // feel like it'd probably be better to check + // for the string ahead of time prior to + // decompressing it. Doing it this way allows + // us to deal with Archive II files with data + // blocks that may or may not be + // bzip2-compressed though... + decompressed_data.extend_from_slice(&raw_buffer[..raw_buffer_size as usize]); + }, + _ => panic!("Problem decompressing bzip2 data: {error:?}") + } + } + // If we have no data left, we can assume we're done. + if decompressed_data.len() == 0 { + decoding = false; + bail = true; + } + } + + // Process decompressed data + // ------------------------- + let mut msg_length: usize = MSG_LENGTH; + let mut i: usize = 0; + while i < decompressed_data.len() { + let slice = &decompressed_data[i..i+mem::size_of::<Packet>()]; + let packet: Packet = unsafe { + std::ptr::read(slice.as_ptr() as *const _) + }; + + // Message 31 packets are variable length; we must + // readjust. If this isn't a message 31 packet though, + // then return to standard length. + if packet.message_type == 31 { + msg_length = u16::from_be(packet.size) as usize * 2 + 12; + } else { + msg_length = MSG_LENGTH; + } + + if packet.message_type != 0 { + // Handle messages accordingly. We'll break out + // message processing into separate routines since + // some of them can be pretty involved (e.g. 1, 31) + match packet.message_type { + 31 => { + // println!("Processing message 31... length: {}", msg_length); + let raw_data = &decompressed_data[i + mem::size_of::<Packet>() + mem::size_of::<Message31Header>() - packet.data.len()..]; + bail |= process_message_31(&mut volume_scan, packet, raw_data); + if bail { + // Bail as soon as the elevation (not + // volume!) scan is done. + // This works for my purposes, but isn't + // isn't ideal for loading a whole archive + // at once. + render_image(&mut volume_scan); + } + }, + _ => { + println!("Message {} functionality unimplemented...", packet.message_type); + } + } + } + + i += msg_length; + } + } + + return volume_scan; +} + +fn convert_float(float: f32) -> f32 { + return f32::from_bits(float.to_bits().swap_bytes()); +} + +fn process_message_31(volume_scan: &mut VolumeScan, packet: Packet, raw_data: &[u8]) -> bool { + let slice = &packet.data[..mem::size_of::<Message31Header>()]; + let header: Message31Header = unsafe { + std::ptr::read(slice.as_ptr() as *const _) + }; + + // Set up our new elevation scan (really not doing this the best + // way...) + let elevation_number: usize = header.elevation_number as usize; + if volume_scan.elevation_scans.len() < elevation_number { + volume_scan.elevation_scans.push(ElevationScan::new()); + } + let elevation_scan: &mut ElevationScan = &mut volume_scan.elevation_scans[elevation_number - 1]; + + // Bump up the radial counter. This should contain the # of + // radials when we're done. + elevation_scan.num_radials += 1; + + // Add the elevation angle of this radial (it varies) to the + // elevation scan structure + elevation_scan.accum_elev_angle += convert_float(header.elevation_angle); + elevation_scan.mean_elev_angle = elevation_scan.accum_elev_angle / elevation_scan.num_radials as f32; + + // Note that NEXRAD radials are numbered starting at 1, but for + // our purposes we will number them starting at zero. + let radial: usize = u16::from_be(header.radial_idx) as usize - 1; + + // Record the azimuth of the start of this radial. 'angle' is + // actually the center of the radial so to get the start we will + // subtract off 1/2 of the radial spacing (which will currently + // either be 1 or 0.5 degrees). + elevation_scan.azimuth[radial] = match header.azimuth_res_spacing_code { + 1 => convert_float(header.radial_azimuth) - 0.25, + 2 => convert_float(header.radial_azimuth) - 0.5, + _ => convert_float(header.radial_azimuth) + }; + + // If we go negative from the above operation, then correct + if elevation_scan.azimuth[radial] < 0.0 { + elevation_scan.azimuth[radial] = 360.0 + elevation_scan.azimuth[radial]; + } + + // Set our azimuth and index of the northernmost radial here. + // We'll also check if the azimuth of this radial is further + // clockwise than the ones we've already recorded. It's nice to + // know where the first radial is. + let wrap_around = elevation_scan.azimuth[radial] < elevation_scan.first_radial_az; + if radial == 0 || wrap_around { + elevation_scan.first_radial_az = elevation_scan.azimuth[radial]; + elevation_scan.first_radial_idx = radial as u16; + } + + // If we've wrapped around (i.e. crossed 0deg north) and the + // azimuth of this radial is further clockwise than the first one + // we've processed, then this indicates an overscan condition. In + // other words, we have scanned past 360 degrees. In this case, + // we'll effectively nullify this and all subsequent radials by + // decrementing the number of radials, making these not count. + if wrap_around && elevation_scan.azimuth[radial] > elevation_scan.azimuth[0] { + elevation_scan.num_radials -= 1; + } + + // Build 19 added CFP (clutter filter power removed) data, an + // ABI-breaking change. Since our Message31Header structure is + // still set up to have only 9 data blocks, we'll need to check + // the RDA software version and bump up our byte index should this + // data be present. For now though, we'll hardcode it (cool) + let cfp_present_hack: usize = 4; + + // Build 20 introduced another ABI-breaking change with the "RPG + // weighted mean ZDR bias estimate" field. It's 2 bytes w/ 6 + // leftover as spare memory. We'll address it here too. + let rpg_weighted_mean_zdr_bias_present_hack = 0; + + // Copy over the data to our block structure + let mut byte_idx = cfp_present_hack; + for i in 0..u16::from_be(header.data_block_count) { + let slice = &raw_data[byte_idx..byte_idx + mem::size_of::<DataBlock>()]; + let data_block: DataBlock = unsafe { std::ptr::read(slice.as_ptr() as *const _) }; + let name = str::from_utf8(&data_block.moment_name).unwrap(); + if radial >= elevation_scan.radials.len() { + // This will be where the read data goes. We'll insert this into + // the elevation scan's radial data vector once we're done. + elevation_scan.radials.push(Message31::new(header)); + } + match name { + "VOL" => { + byte_idx += mem::size_of::<VolumeData>(); + }, + "ELV" => { + byte_idx += mem::size_of::<ElevationData>(); + }, + "RAD" => { + byte_idx += mem::size_of::<RadialData>(); + }, + "REF"|"VEL"|"SW "|"ZDR"|"PHI"|"RHO"|"CFP" => { + let slice = &raw_data[byte_idx..byte_idx + mem::size_of::<GenericDataMoment>()]; + let generic_moment: GenericDataMoment = unsafe { + std::ptr::read(slice.as_ptr() as *const _) + }; + byte_idx += mem::size_of::<GenericDataMoment>(); + + // Per the ICD: + // + // LDM is the amount of space in bytes required for a + // data moment array and equals ((NG * DWS) / 8) where + // NG is the number of gates at the gate spacing + // resolution specified and DWS is the number of bits + // stored for each gate (DWS is always a multiple of + // 8). + // + // Means "Length of Data Moment"; no relation to + // UCAR's related LDM software :P + let num_gates = u16::from_be(generic_moment.num_gates); + let ldm = {num_gates * generic_moment.word_size as u16 / 8} as usize; + + // Record adjusted gate information + elevation_scan.num_gates = cmp::max(elevation_scan.num_gates, num_gates as i32); + elevation_scan.gate_size = u16::from_be(generic_moment.range_sample_interval) as f32; + elevation_scan.first_gate = u16::from_be(generic_moment.range) as f32; + + // Record radial data (gross nested match here but whatever) + let mut radial_block: &mut Message31 = &mut elevation_scan.radials[radial]; + let mut moment: DataMoment = DataMoment::default(); + let radar = &raw_data[byte_idx..]; + for (i, chunk) in radar.chunks_exact(1).enumerate().take(MOMENT_LENGTH) { + moment.data[i] = chunk[0] as u16; + // For 16-bit data types (e.g. ZDR) we'd probably want this instead: + // moment.data[i] = u16::from_le_bytes([chunk[0], chunk[1]]); + } + match name { + "REF" => { radial_block.reflectivity_data = moment; }, + "VEL" => { radial_block.velocity_data = moment; }, + "SW " => { radial_block.spectrum_width_data = moment; }, + "ZDR" => { radial_block.zdr_data = moment; }, + "PHI" => { radial_block.phi_data = moment; }, + "RHO" => { radial_block.rho_data = moment; }, + "CFP" => { radial_block.cfp_data = moment; } + _ => {} + } + byte_idx += ldm; + + } + _ => { + // panic!("Unexpected block type!"); + } + } + + // Debug: + // println!("Block name: {}", name); + } + + // For our purposes we only want to process one elevation scan for + // now... if we're at the end of the elevation (not volume) scan, + // we're done. + if header.radial_status == RadialStatus::EndElevationScan as u8 { + return true; + } + + return false; +} + +fn get_dbz(r: f32, theta: f32, elevation_scan: &ElevationScan, smooth: bool) -> f32 { + let num_radials: i32 = elevation_scan.num_radials as i32; + let last_radial: i32 = num_radials - 1; + + let native_scale_r = elevation_scan.num_gates as f32 / 458.0; // Gates per km + let native_scale_theta = elevation_scan.num_radials as f32 / 360.0; // Radials per deg + + // Determine the range to the first gate that contains data (this + // is in the metadata from the radar). There is no data for the + // first km or two close to the radar site. It's the so-called + // "cone of silence". If we're asked for data in this region we'll + // return 0 dBZ. + let range_to_first_gate_km = elevation_scan.first_gate / 1000.0 - elevation_scan.gate_size / 1000.0 / 2.0; + if r <= range_to_first_gate_km { + return 0.0; + } + + // Translate r (which is in km) to a gate number by subtracting + // off the range to the first gate data (which is also in km), + // then multiplying by the native_scale_r value (which is in + // gates/km). We need two gate numbers, one 'below' the actual r + // (i.e. closer to the radar site) and one 'above' the actual r + // (i.e. further from the radar site). These top and bottom gates + // will be used for interpolation. + let bottom = ((r - range_to_first_gate_km) * native_scale_r) as usize; + let top = (bottom + 1) as usize; + + // Not only do we need the gate numbers for the top and bottom, + // but we also need the relative distance of r from the top and + // bottom gates. This distance is used as an interpolation weight, + // or to determine the nearest neighbor. For now we're only doing + // nearest neighbor interpolation. + let dist_bottom: f32 = (r - range_to_first_gate_km) * native_scale_r - bottom as f32; + let dist_top: f32 = 1.0 - dist_bottom; + + // Identify the radial immediately preceeding the northernmost radial. It crosses the 0 degree threshold + let mut trans_radial = elevation_scan.first_radial_idx as i32; + if trans_radial < 0 { + trans_radial = last_radial; + } + + // Here's a cool trick. Estimate the proper radial num (0 - + // lastradial) by subtracting the azimuth of the northernmost + // radial then taking the integer portion of the remainder. This + // will provide an index, relative to the northernmost radial, of + // the radial that we need. Once we have an index, we have to + // resolve it to an actual radial number by adding it to the + // radial number of the northernmost radial. If we exceed + // lastradial, then we have simply wrapped around. + let mut radial_idx: i32 = ((theta - elevation_scan.first_radial_az) * native_scale_theta + elevation_scan.first_radial_idx as f32) as i32; + if radial_idx > last_radial { + radial_idx -= num_radials; + } + + // Check to see if our estimated radial index was correct. Usually + // it will be, however in some cases it might not. We check for + // overestimates and underestimates and starting at the estimate + // we work back and forth trying to find the true value. This was + // done to improve interpolation accuracy for some sites that have + // very uneven spacing between radial azimuths. A safety check is + // provided to prevent hangs. Also, the transition radial (the one + // right before 0 degrees) is exempted from this check for obvious + // reasons. This could be made better, but meh. + let mut safety = 10; + while theta < elevation_scan.azimuth[radial_idx as usize] && radial_idx != trans_radial && safety != 0 { + safety -= 1; + if radial_idx > 0 { + radial_idx -= 1; + } + if radial_idx < 0 { + radial_idx = last_radial; + } + } + safety = 10; + while theta > elevation_scan.azimuth[radial_idx as usize + 1] && radial_idx != trans_radial && safety != 0 { + safety -= 1; + radial_idx += 1; + if radial_idx > last_radial { + radial_idx -= num_radials; + } + } + if safety == 0 { + return 0.0; + } + + // Determine the left and right radial numbers to use for interpolation. + let mut left: usize = 0; + let mut right: usize = 0; + let mut dist_left: f32 = 0.0; + let mut dist_right: f32 = 0.0; + if radial_idx == trans_radial { + // We're processing the radial immediately before the northernmost one + left = trans_radial as usize; + right = elevation_scan.first_radial_idx as usize; + if theta < 180.0 { + dist_left = theta + 360.0 - elevation_scan.azimuth[left]; + dist_right = elevation_scan.azimuth[right] - theta; + } else { + dist_left = theta - elevation_scan.azimuth[left]; + dist_right = elevation_scan.azimuth[right] + 360.0 - theta; + } + } else { + left = radial_idx as usize; + right = left + 1; + if right > last_radial as usize { + right = 0; + } + dist_left = theta - elevation_scan.azimuth[left]; + dist_right = elevation_scan.azimuth[right] - theta; + } + + // Grab the dBZ values for all 4 neighboring points + let mut dbz_ll = elevation_scan.radials[left].reflectivity_data.data[bottom] as f32; + let mut dbz_ul = elevation_scan.radials[left].reflectivity_data.data[top] as f32; + let mut dbz_lr = elevation_scan.radials[right].reflectivity_data.data[bottom] as f32; + let mut dbz_ur = elevation_scan.radials[right].reflectivity_data.data[top] as f32; + + // Apply a dBZ threshold (don't want anything below 0 for reasons) + dbz_ll = f32::max(dbz_ll, 66.0); + dbz_ul = f32::max(dbz_ul, 66.0); + dbz_lr = f32::max(dbz_lr, 66.0); + dbz_ur = f32::max(dbz_ur, 66.0); + + // Perform interpolation + let mut dbz_top: f32 = 0.0; + let mut dbz_bottom: f32 = 0.0; + let mut dbz: f32 = 0.0; + if smooth { + dbz_top = ((dbz_ul * dist_right) + (dbz_ur * dist_left)) / (dist_left + dist_right); + dbz_bottom = ((dbz_ll * dist_right) + (dbz_lr * dist_left)) / (dist_left + dist_right); + dbz = ((dbz_top * dist_bottom) + (dbz_bottom * dist_top)) / (dist_top + dist_bottom); + } else { + if dist_left < dist_right { + dbz_top = dbz_ul; + dbz_bottom = dbz_ll; + } else { + dbz_top = dbz_ur; + dbz_bottom = dbz_lr; + } + if dist_top < dist_bottom { + dbz = dbz_top; + } else { + dbz = dbz_bottom; + } + } + + return (dbz - 2.0) / 2.0 - 32.0; +} + +const NUMINTERVALS: usize = 8; + +fn render_image(volume_scan: &mut VolumeScan) { + let elevation_scan: &ElevationScan = &volume_scan.elevation_scans[0]; + + // We *could* use a 16-color scale, but this 128-color interpolated one is cooler + let mut color_scale: [[u8; 3]; 16 * NUMINTERVALS] = [[0; 3]; 16 * NUMINTERVALS]; + build_smooth_color_scale(&mut color_scale); + + // Render :D + let width = 1920; + let height = 1920; + let mut img: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::new(width, height); + let mut x1: f32 = 0.0; + let mut y1: f32 = 0.0; + for (x, y, pixel) in img.enumerate_pixels_mut() { + x1 = x as f32 - width as f32 / 2.0; + y1 = height as f32 / 2.0 - y as f32; + let r = f32::sqrt(x1 * x1 + y1 * y1) * 0.25; + let mut theta = (1.570795 - f32::atan2(y1, x1)) * 57.29583; // (90deg-atan2(y1,x1))*180/pi) + if theta < 0.0 { + theta = 360.0 + theta; + } + let mut dbz = get_dbz(r, theta, elevation_scan, false); + let color_idx: usize = (dbz * NUMINTERVALS as f32 / 5.0 + 0.5) as usize; + if color_idx >= 16 * NUMINTERVALS || dbz <= 0.0 { + continue; + } + let out_r = color_scale[color_idx][0]; + let out_g = color_scale[color_idx][1]; + let out_b = color_scale[color_idx][2]; + *pixel = Rgba([out_r, out_g, out_b, 0xFF]); + } + img.save("output.png").expect("Failed to save image"); +} + +fn build_smooth_color_scale(scolorscale: &mut [[u8; 3]; 16 * NUMINTERVALS]) { + let colorscale: [[u8; 3]; 16] = [ + [0, 0, 0], + [0, 72, 144], + [0, 96, 240], + [128, 224, 80], + [100, 184, 64], + [72, 144, 48], + [44, 104, 32], + [16, 64, 16], + [240, 160, 16], + [240, 118, 32], + [240, 16, 32], + [144, 0, 0], + [176, 32, 128], + [202, 64, 160], + [229, 96, 192], + [225, 128, 224], + ]; + let numintervals = NUMINTERVALS as f32; + let mut step = 0; + while step < 15 { + let (curr, curg, curb) = (colorscale[step][0], colorscale[step][1], colorscale[step][2]); + let (nextr, nextg, nextb) = (colorscale[step + 1][0], colorscale[step + 1][1], colorscale[step + 1][2]); + + // Compute the deltas between the colors + let deltar = (nextr as f32 - curr as f32) / numintervals; + let deltag = (nextg as f32 - curg as f32) / numintervals; + let deltab = (nextb as f32 - curb as f32) / numintervals; + + for t in 0..numintervals as usize { + scolorscale[step * numintervals as usize + t][0] = (curr as f32 + deltar * t as f32).round() as u8; + scolorscale[step * numintervals as usize + t][1] = (curg as f32 + deltag * t as f32).round() as u8; + scolorscale[step * numintervals as usize + t][2] = (curb as f32 + deltab * t as f32).round() as u8; + } + + step += 1; + } + scolorscale[step * numintervals as usize][0] = colorscale[step][0]; + scolorscale[step * numintervals as usize][1] = colorscale[step][1]; + scolorscale[step * numintervals as usize][2] = colorscale[step][2]; + + // Last bit of color scale is all white + for t in 1..numintervals as usize { + scolorscale[step * numintervals as usize + t][0] = (248 + t) as u8; + scolorscale[step * numintervals as usize + t][1] = (248 + t) as u8; + scolorscale[step * numintervals as usize + t][2] = (248 + t) as u8; + } +} + +fn main() -> io::Result<()> { + println!("Processing file..."); + let file = File::open("test/KRAX.dat")?; + if let Some(scan) = check_for_volume_scan(file) { + println!("Successfully processed volume scan"); + } else { + println!("Failed to find volume scan data"); + } + Ok(()) +} diff --git a/test/KRAX.dat b/test/KRAX.dat new file mode 100644 index 0000000..f6a08d8 Binary files /dev/null and b/test/KRAX.dat differ