diff --git a/Cargo.lock b/Cargo.lock index a3cde12..0b42c06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,49 +2,592 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "const-random", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrow" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05048a8932648b63f21c37d88b552ccc8a65afb6dfe9fc9f30ce79174c2e7a85" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-csv", + "arrow-data", + "arrow-ipc", + "arrow-json", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8a57966e43bfe9a3277984a14c24ec617ad874e4c0e1d2a1b083a39cfbf22c" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "num", +] + +[[package]] +name = "arrow-array" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f4a9468c882dc66862cef4e1fd8423d47e67972377d85d80e022786427768c" +dependencies = [ + "ahash", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c975484888fc95ec4a632cdc98be39c085b1bb518531b0c80c5d462063e5daa1" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-cast" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da26719e76b81d8bc3faad1d4dbdc1bcc10d14704e63dc17fc9f3e7e1e567c8e" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-csv" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13c36dc5ddf8c128df19bab27898eea64bf9da2b555ec1cd17a8ff57fba9ec2" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-schema", + "chrono", + "csv", + "csv-core", + "lazy_static", + "lexical-core", + "regex", +] + +[[package]] +name = "arrow-data" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd9d6f18c65ef7a2573ab498c374d8ae364b4a4edf67105357491c031f716ca5" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num", +] + +[[package]] +name = "arrow-ipc" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e786e1cdd952205d9a8afc69397b317cfbb6e0095e445c69cda7e8da5c1eeb0f" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-schema", + "flatbuffers", +] + +[[package]] +name = "arrow-json" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb22284c5a2a01d73cebfd88a33511a3234ab45d66086b2ca2d1228c3498e445" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "indexmap", + "lexical-core", + "num", + "serde", + "serde_json", +] + +[[package]] +name = "arrow-ord" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42745f86b1ab99ef96d1c0bcf49180848a64fe2c7a7a0d945bc64fa2b21ba9bc" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "half", + "num", +] + +[[package]] +name = "arrow-row" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd09a518c602a55bd406bcc291a967b284cfa7a63edfbf8b897ea4748aad23c" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e972cd1ff4a4ccd22f86d3e53e835c2ed92e0eea6a3e8eadb72b4f1ac802cf8" + +[[package]] +name = "arrow-select" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "600bae05d43483d216fb3494f8c32fdbefd8aa4e1de237e790dbb3d9f44690a3" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num", +] + +[[package]] +name = "arrow-string" +version = "52.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc1985b67cb45f6606a248ac2b4a288849f196bab8c657ea5589f47cdd55e6" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastformat" version = "0.1.0" dependencies = [ + "arrow", + "eyre", + "ndarray", "pyo3", ] +[[package]] +name = "flatbuffers" +version = "24.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8add37afff2d4ffa83bc748a70b4b1370984f6980768554182424ef71447c35f" +dependencies = [ + "bitflags 1.3.2", + "rustc_version", +] + +[[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 = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "indoc" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lexical-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +dependencies = [ + "lexical-util", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "lock_api" version = "0.4.12" @@ -55,6 +598,28 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "memoffset" version = "0.9.1" @@ -64,6 +629,93 @@ dependencies = [ "autocfg", ] +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[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-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[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-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "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", + "libm", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -180,27 +832,121 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "redox_syscall" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags", + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", ] +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "2.0.72" @@ -218,6 +964,15 @@ version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -230,6 +985,81 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -293,3 +1123,23 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "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", +] diff --git a/Cargo.toml b/Cargo.toml index f374e82..fbf00ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,13 +2,16 @@ name = "fastformat" version = "0.1.0" edition = "2021" -license = "MIT" +license = "Apache-2.0" description = "Fast world data converter" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "fastformat" -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] [dependencies] pyo3 = "0.21.1" +ndarray = "0.15.4" +arrow = "52.1.0" +eyre = "0.6.12" diff --git a/LICENSE b/LICENSE index 2e27333..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2024 dora-rs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e951c04 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +## Project Objective + +The goal is to create an efficient way to reconstruct and process data in real-time, in specific formats like NDarray, +Numpy, or Arrow, without unnecessary copies. + +- **Independent Library**: We are creating an independent library for our formats, which everyone could use even without + DORA. The current name is fastformat: + +- **Agnosticism**: We want our solution to be agnostic to Arrow, meaning it can work with Numpy and other data types + easily by implementing to_[format] functions. + +- **Rust and Python Code**: The main implementation will be in Rust for performance and portability reasons, with a + Python interface using PyO3. This will allow for fast vector manipulation and potentially GPU support in the future. + +- **Portability and Dependencies**: By using Rust, we aim to minimize dependencies and maximize portability. This will + allow us to achieve optimal performance across different platforms. + +- **Simplicity**: The goal is not to create yet another library for creating formats to store/process data. The aim here + is to create a simple interface to wrap data from one type and convert it to another. + +## DataTypes + +- **Image**: (Arrow representation is a **UnionArray**), + - Field "data': UintXArray (e.g [0, 255, 0, 255, 0, 255, ...]) + - Field "width": Uint32Array (e.g [1280]) + - Field "height": Uint32Array (e.g [720]) + - Field "encoding": StringArray (e.g ["RGB8"]) + - Field "name" (Optional): StringArray (e.g,["image.front_camera"] or [None]) diff --git a/examples/dummy-opencv-capture/main.rs b/examples/dummy-opencv-capture/main.rs new file mode 100644 index 0000000..97b6359 --- /dev/null +++ b/examples/dummy-opencv-capture/main.rs @@ -0,0 +1,68 @@ +extern crate fastformat; + +use fastformat::image::Image; + +fn camera_read() -> ndarray::Array { + // Dummy camera read + + let flat_image = vec![0; 27]; + println!( + "Generate a camera image at address: {:?}", + flat_image.as_ptr() + ); + + let image = Image::new_bgr8(flat_image, 3, 3, None).unwrap(); + + return image.bgr8_to_ndarray().unwrap(); +} + +fn image_show(_frame: ndarray::ArrayView) { + // Dummy image show + + println!("Showing an image."); +} + +fn send_output(arrow_array: arrow::array::UnionArray) { + // Dummy send output + + let image = Image::from_arrow(arrow_array).unwrap(); + + println!( + "Sending an image to dataflow. Image address is: {:?}", + image.as_ptr() + ); +} + +fn main() { + // Read OpenCV Camera, default is ndarray BGR8 + let frame = camera_read(); + + let image = Image::bgr8_from_ndarray(frame, Some("camera.left")).unwrap(); + + // Convert to RGB8, apply some filter (Black and White). + let mut frame = image.to_rgb8().unwrap().rgb8_to_ndarray().unwrap(); + + for i in 0..frame.shape()[0] { + for j in 0..frame.shape()[1] { + let mean = + (frame[[i, j, 0]] as f32 + frame[[i, j, 1]] as f32 + frame[[i, j, 2]] as f32) / 3.0; + + if mean > 128.0 { + frame[[i, j, 0]] = 255; + frame[[i, j, 1]] = 255; + frame[[i, j, 2]] = 255; + } else { + frame[[i, j, 0]] = 0; + frame[[i, j, 1]] = 0; + frame[[i, j, 2]] = 0; + } + } + } + + let image = Image::rgb8_from_ndarray(frame, Some("camera.left.baw")).unwrap(); + + // Plot the image, you may only need a ndarray_view + image_show(image.rgb8_to_ndarray_view().unwrap()); + + send_output(image.to_arrow().unwrap()); +} diff --git a/src/arrow.rs b/src/arrow.rs new file mode 100644 index 0000000..9f84456 --- /dev/null +++ b/src/arrow.rs @@ -0,0 +1,162 @@ +use arrow::datatypes::DataType; + +use std::{collections::HashMap, sync::Arc}; + +use eyre::{ContextCompat, Result}; + +/// Creates a lookup table (`HashMap`) from the fields of a union. +/// +/// This function takes a reference to `arrow::datatypes::UnionFields` and +/// creates a `HashMap` where the field names are the keys (as `String`) and +/// the associated values are the field identifiers (`i8`). +/// +/// # Arguments +/// +/// * `fields` - A reference to the fields of the Arrow union data structure (`arrow::datatypes::UnionFields`). +/// +/// # Returns +/// +/// A `HashMap` with the field names as keys and their identifiers (`i8`) as values. +/// +/// # Example +/// +/// ``` +/// use arrow::datatypes::{Field, DataType, UnionFields}; +/// use std::collections::HashMap; +/// +/// use fastformat::arrow::union_lookup_table; +/// +/// let fields = UnionFields::new( +/// vec![1, 2], +/// vec![ +/// Field::new("field1", DataType::Int32, false), +/// Field::new("field2", DataType::Float64, false), +/// ], +/// ); +/// +/// let lookup_table = union_lookup_table(&fields); +/// +/// assert_eq!(lookup_table.get("field1"), Some(&1)); +/// assert_eq!(lookup_table.get("field2"), Some(&2)); +/// ``` +pub fn union_lookup_table(fields: &arrow::datatypes::UnionFields) -> HashMap { + let mut result = HashMap::new(); + + for field in fields.iter() { + let (a, b) = field; + + result.insert(b.name().to_string(), a); + } + + result +} + +/// Retrieves a column from a `UnionArray` by its field name and downcasts it to the specified type. +/// +/// This function takes a reference to an `arrow::array::UnionArray`, a field name, +/// and a lookup table mapping field names to their identifiers. It retrieves the column +/// corresponding to the field name from the union array and attempts to downcast it to +/// the specified type `T`. +/// +/// # Arguments +/// +/// * `array` - A reference to the `UnionArray` from which to retrieve the column. +/// * `field` - The name of the field whose column is to be retrieved. +/// * `lookup_table` - A reference to a `HashMap` that maps field names (`String`) to their identifiers (`i8`). +/// +/// # Returns +/// +/// A `Result` containing a reference to the column cast to type `T` if successful, or an error otherwise. +/// +/// # Errors +/// +/// Returns an error if the field name is not found in the lookup table, or if the retrieved column +/// cannot be downcast to the specified type `T`. +/// +/// # Example +/// +/// ``` +/// use arrow::array::Array; +/// +/// use fastformat::image::Image; +/// use fastformat::arrow::union_lookup_table; +/// use fastformat::arrow::column_by_name; +/// +/// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel +/// let image = Image::new_bgr8(data, 3, 3, None).unwrap(); +/// let array = image.to_arrow().unwrap(); +/// +/// let union_fields = match array.data_type() { +/// arrow::datatypes::DataType::Union(fields, ..) => fields, +/// _ => panic!("Unexpected data type for image array") +/// }; +/// +/// let lookup_table = union_lookup_table(&union_fields); +/// +/// let int_column = column_by_name::(&array, "field1", &lookup_table); +/// ``` +pub fn column_by_name<'a, T: 'static>( + array: &'a arrow::array::UnionArray, + field: &'a str, + lookup_table: &'a HashMap, +) -> Result<&'a T> { + let index = lookup_table + .get(field) + .cloned() + .wrap_err(format!("Couldn't get field {} from look_up table", field))?; + + return array + .child(index) + .as_any() + .downcast_ref::() + .wrap_err(format!("Couldn't downcast field {} to type T", field)); +} + +/// Creates a tuple representing a union field with an index and an `Arc`-wrapped `Field`. +/// +/// This function constructs a tuple where the first element is the given index and the second element +/// is an `Arc`-wrapped `Field` constructed using the provided name, data type, and nullability. +/// +/// # Arguments +/// +/// * `index` - An identifier (`i8`) for the union field. +/// * `name` - A string slice representing the name of the field. +/// * `data_type` - The data type of the field (`arrow::datatypes::DataType`). +/// * `nullable` - A boolean indicating whether the field is nullable. +/// +/// # Returns +/// +/// A tuple where the first element is the given index (`i8`) and the second element is an `Arc`-wrapped +/// `Field` constructed from the provided name, data type, and nullability. +/// +/// # Example +/// +/// ``` +/// use arrow::datatypes::{DataType, Field}; +/// use std::sync::Arc; +/// use fastformat::arrow::union_field; +/// +/// let index = 1; +/// let name = "field1"; +/// let data_type = DataType::Int32; +/// let nullable = false; +/// +/// let union_field_tuple = union_field(index, name, data_type, nullable); +/// +/// assert_eq!(union_field_tuple.0, 1); +/// assert_eq!(union_field_tuple.1.name(), "field1"); +/// assert_eq!(union_field_tuple.1.data_type(), &DataType::Int32); +/// assert_eq!(union_field_tuple.1.is_nullable(), false); +/// ``` +/// +pub fn union_field( + index: i8, + name: &str, + data_type: DataType, + nullable: bool, +) -> (i8, Arc) { + ( + index, + Arc::new(arrow::datatypes::Field::new(name, data_type, nullable)), + ) +} diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 0000000..9af6e3d --- /dev/null +++ b/src/image.rs @@ -0,0 +1,116 @@ +use eyre::{Report, Result}; + +use container::DataContainer; +use encoding::Encoding; + +mod bgr8; +mod gray8; +mod rgb8; + +mod arrow; +mod container; +mod encoding; + +#[derive(Debug)] +pub struct Image { + data: DataContainer, + + width: u32, + height: u32, + + encoding: Encoding, + + name: Option, +} + +impl Image { + pub fn as_ptr(&self) -> *const u8 { + self.data.as_ptr() + } + + pub fn data(&self) -> &DataContainer { + &self.data + } + + pub fn to_rgb8(self) -> Result { + match self.encoding { + Encoding::BGR8 => { + let mut data = self.data.into_u8()?; + + for i in (0..data.len()).step_by(3) { + data.swap(i, i + 2); + } + + Ok(Image { + data: DataContainer::from_u8(data), + width: self.width, + height: self.height, + encoding: Encoding::RGB8, + name: self.name.clone(), + }) + } + Encoding::RGB8 => Ok(self), + _ => Err(Report::msg("Can't convert image to RGB8")), + } + } + + pub fn to_bgr8(self) -> Result { + match self.encoding { + Encoding::RGB8 => { + let mut data = self.data.into_u8()?; + + for i in (0..data.len()).step_by(3) { + data.swap(i, i + 2); + } + + Ok(Image { + data: DataContainer::from_u8(data), + width: self.width, + height: self.height, + encoding: Encoding::BGR8, + name: self.name.clone(), + }) + } + Encoding::BGR8 => Ok(self), + _ => Err(Report::msg("Can't convert image to BGR8")), + } + } +} + +mod tests { + #[test] + fn test_rgb8_to_bgr8() { + use crate::image::Image; + + let flat_image = (0..27).collect::>(); + let image = Image::new_rgb8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + let final_image = image.to_bgr8().unwrap(); + let final_image_data = final_image.data().as_u8().unwrap(); + + let expected_image = vec![ + 2, 1, 0, 5, 4, 3, 8, 7, 6, 11, 10, 9, 14, 13, 12, 17, 16, 15, 20, 19, 18, 23, 22, 21, + 26, 25, 24, + ]; + + assert_eq!(&expected_image, final_image_data); + } + + #[test] + fn test_bgr8_to_rgb8() { + use crate::image::Image; + + let flat_image = (0..27).collect::>(); + let image = Image::new_bgr8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + let final_image = image.to_rgb8().unwrap(); + let final_image_data = final_image.data().as_u8().unwrap(); + + let expected_image = vec![ + 2, 1, 0, 5, 4, 3, 8, 7, 6, 11, 10, 9, 14, 13, 12, 17, 16, 15, 20, 19, 18, 23, 22, 21, + 26, 25, 24, + ]; + + assert_eq!(&expected_image, final_image_data); + } +} diff --git a/src/image/arrow.rs b/src/image/arrow.rs new file mode 100644 index 0000000..3661588 --- /dev/null +++ b/src/image/arrow.rs @@ -0,0 +1,206 @@ +use crate::arrow::{column_by_name, union_field, union_lookup_table}; + +use super::{container::DataContainer, encoding::Encoding, Image}; +use eyre::{Context, Report, Result}; + +use std::{collections::HashMap, mem, sync::Arc}; + +impl Image { + unsafe fn arrow_data_to_vec( + array: &arrow::array::UnionArray, + lookup_table: &HashMap, + ) -> Result> { + let arrow = column_by_name::>(array, "data", lookup_table)?; + let ptr = arrow.values().as_ptr(); + let len = arrow.len(); + + Ok(Vec::from_raw_parts(ptr as *mut G, len, len)) + } + + /// Constructs an `Image` from an Arrow `UnionArray`. + /// + /// This function takes an Arrow `UnionArray` and extracts the necessary fields to construct + /// an `Image` object. It validates the data type of the `UnionArray`, builds a lookup table for + /// the fields, retrieves the image properties (width, height, encoding, name), and decodes the + /// pixel data based on the encoding. + /// + /// # Arguments + /// + /// * `array` - A reference to an `arrow::array::UnionArray` that contains the image data. + /// + /// # Returns + /// + /// A `Result` containing the constructed `Image` if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `UnionArray` has an invalid data type, if required fields are missing, + /// or if the pixel data cannot be downcasted to the expected type based on the encoding. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel + /// let image = Image::new_bgr8(data, 3, 3, None).unwrap(); + /// let array = image.to_arrow().unwrap(); + /// + /// let image = Image::from_arrow(array).unwrap(); + /// ``` + pub fn from_arrow(array: arrow::array::UnionArray) -> Result { + use arrow::array::Array; + + let union_fields = match array.data_type() { + arrow::datatypes::DataType::Union(fields, ..) => fields, + _ => { + return Err(Report::msg("UnionArray has invalid data type.")); + } + }; + + let lookup_table = union_lookup_table(union_fields); + + let width = + column_by_name::(&array, "width", &lookup_table)?.value(0); + let height = + column_by_name::(&array, "height", &lookup_table)?.value(0); + let encoding = Encoding::from_string( + column_by_name::(&array, "encoding", &lookup_table)? + .value(0) + .to_string(), + )?; + + let name = column_by_name::(&array, "name", &lookup_table)?; + + let name = if name.is_null(0) { + None + } else { + Some(name.value(0).to_string()) + }; + + unsafe { + let array = mem::ManuallyDrop::new(array); + + let data = match encoding { + Encoding::RGB8 => DataContainer::from_u8(Self::arrow_data_to_vec::< + arrow::datatypes::UInt8Type, + u8, + >(&array, &lookup_table)?), + Encoding::BGR8 => DataContainer::from_u8(Self::arrow_data_to_vec::< + arrow::datatypes::UInt8Type, + u8, + >(&array, &lookup_table)?), + Encoding::GRAY8 => DataContainer::from_u8(Self::arrow_data_to_vec::< + arrow::datatypes::UInt8Type, + u8, + >(&array, &lookup_table)?), + }; + + Ok(Image { + data, + width, + height, + encoding, + name, + }) + } + } + + fn convert_image_details_to_arrow(image: Image) -> Result>> { + let width = Arc::new(arrow::array::UInt32Array::from(vec![image.width; 1])); + let height = Arc::new(arrow::array::UInt32Array::from(vec![image.height; 1])); + + let encoding = Arc::new(arrow::array::StringArray::from(vec![ + image + .encoding + .to_string(); + 1 + ])); + + let name = Arc::new(arrow::array::StringArray::from(vec![image.name.clone(); 1])); + + let data: Arc = match image.encoding { + Encoding::RGB8 => Arc::new(arrow::array::UInt8Array::from(image.data.into_u8()?)), + Encoding::BGR8 => Arc::new(arrow::array::UInt8Array::from(image.data.into_u8()?)), + Encoding::GRAY8 => Arc::new(arrow::array::UInt8Array::from(image.data.into_u8()?)), + }; + + Ok(vec![data, width, height, encoding, name]) + } + + /// Converts an `Image` into an Arrow `UnionArray`. + /// + /// This function takes an `Image` object and converts it into an Arrow `UnionArray` + /// that contains the image properties and pixel data. The conversion handles different + /// image encodings (BGR8, RGB8, GRAY8) and ensures that the resulting `UnionArray` + /// contains all necessary fields. + /// + /// # Arguments + /// + /// * `self` - The `Image` object to be converted. + /// + /// # Returns + /// + /// A `Result` containing the constructed `arrow::array::UnionArray` if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `UnionArray` cannot be created due to issues with the provided data. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 640 * 480 * 3]; + /// let image = Image::new_bgr8(data, 640, 480, None).unwrap(); + /// + /// let arrow_array = image.to_arrow().unwrap(); + /// ``` + pub fn to_arrow(self) -> Result { + let type_ids = [].into_iter().collect::>(); + let offsets = [].into_iter().collect::>(); + + let datatype = match self.encoding { + Encoding::RGB8 => arrow::datatypes::DataType::UInt8, + Encoding::BGR8 => arrow::datatypes::DataType::UInt8, + Encoding::GRAY8 => arrow::datatypes::DataType::UInt8, + }; + + let union_fields = [ + union_field(0, "data", datatype, false), + union_field(1, "width", arrow::datatypes::DataType::UInt32, false), + union_field(2, "height", arrow::datatypes::DataType::UInt32, false), + union_field(3, "encoding", arrow::datatypes::DataType::Utf8, false), + union_field(4, "name", arrow::datatypes::DataType::Utf8, true), + ] + .into_iter() + .collect::(); + + let children = Self::convert_image_details_to_arrow(self)?; + + arrow::array::UnionArray::try_new(union_fields, type_ids, Some(offsets), children) + .wrap_err("Failed to create UnionArray width Image data.") + } +} + +mod tests { + #[test] + fn test_arrow_zero_copy_conversion() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + let original_buffer_address = flat_image.as_ptr(); + + let bgr8_image = Image::new_bgr8(flat_image, 3, 3, None).unwrap(); + let image_buffer_address = bgr8_image.as_ptr(); + + let arrow_image = bgr8_image.to_arrow().unwrap(); + + let new_image = Image::from_arrow(arrow_image).unwrap(); + let final_image_buffer = new_image.as_ptr(); + + assert_eq!(original_buffer_address, image_buffer_address); + assert_eq!(image_buffer_address, final_image_buffer); + } +} diff --git a/src/image/bgr8.rs b/src/image/bgr8.rs new file mode 100644 index 0000000..f9c1f08 --- /dev/null +++ b/src/image/bgr8.rs @@ -0,0 +1,282 @@ +use super::{container::DataContainer, encoding::Encoding, Image}; +use eyre::{Context, Report, Result}; + +impl Image { + /// Creates a new `Image` in BGR8 format. + /// + /// This function constructs a new `Image` object with the given pixel data, width, height, + /// and an optional name. It ensures that the pixel data length matches the expected size + /// for the given width, height, and BGR8 encoding (3 bytes per pixel). + /// + /// # Arguments + /// + /// * `data` - A `Vec` containing the pixel data in BGR8 format. + /// * `width` - The width of the image. + /// * `height` - The height of the image. + /// * `name` - An optional string slice representing the name of the image. + /// + /// # Returns + /// + /// A `Result` containing the constructed `Image` if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the length of the pixel data does not match the expected size + /// based on the width, height, and BGR8 encoding. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel + /// let image = Image::new_bgr8(data, 3, 3, Some("example")).unwrap(); + /// ``` + pub fn new_bgr8(data: Vec, width: u32, height: u32, name: Option<&str>) -> Result { + if width * height * 3 != data.len() as u32 { + return Err(Report::msg( + "Width, height and BGR8 encoding doesn't match data data length.", + )); + } + + Ok(Image { + data: DataContainer::from_u8(data), + width, + height, + encoding: Encoding::BGR8, + name: name.map(|s| s.to_string()), + }) + } + + /// Creates a new `Image` in BGR8 format from an ndarray. + /// + /// This function constructs a new `Image` object from an `ndarray::Array` with shape (height, width, 3). + /// It converts the ndarray into a raw vector and uses it to create the `Image`. + /// + /// # Arguments + /// + /// * `array` - An `ndarray::Array` containing the pixel data in BGR8 format. + /// * `name` - An optional string slice representing the name of the image. + /// + /// # Returns + /// + /// A `Result` containing the constructed `Image` if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the ndarray cannot be converted into a valid `Image`. + /// + /// # Example + /// + /// ``` + /// use ndarray::Array3; + /// use fastformat::image::Image; + /// + /// let array = Array3::::zeros((3, 3, 3)); // 3x3 image with 3 channels + /// let image = Image::bgr8_from_ndarray(array, Some("example")).unwrap(); + /// ``` + pub fn bgr8_from_ndarray( + array: ndarray::Array, + name: Option<&str>, + ) -> Result { + let width = array.shape()[1] as u32; + let height = array.shape()[0] as u32; + + let data = array.into_raw_vec(); + + Self::new_bgr8(data, width, height, name) + } + + /// Converts a BGR8 `Image` into an ndarray. + /// + /// This function takes a BGR8 `Image` and converts it into an `ndarray::Array`. + /// The resulting ndarray has shape (height, width, 3). + /// + /// # Arguments + /// + /// * `self` - The `Image` object to be converted. + /// + /// # Returns + /// + /// A `Result` containing the constructed ndarray if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `Image` is not in BGR8 format or if the pixel data cannot be reshaped + /// into the expected ndarray format. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel + /// let image = Image::new_bgr8(data, 3, 3, Some("example")).unwrap(); + /// + /// let ndarray = image.bgr8_to_ndarray().unwrap(); + /// ``` + pub fn bgr8_to_ndarray(self) -> Result> { + match self.encoding { + Encoding::BGR8 => ndarray::Array::from_shape_vec( + (self.height as usize, self.width as usize, 3), + self.data.into_u8()?, + ) + .wrap_err("Failed to reshape data into ndarray: width, height and BGR8 encoding doesn't match data data length."), + _ => Err(Report::msg("Image is not in BGR8 format")), + } + } + + /// Converts a BGR8 `Image` into an ndarray view. + /// + /// This function takes a reference to a BGR8 `Image` and creates an `ndarray::ArrayView` + /// over the pixel data. The resulting view has shape (height, width, 3). + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Image` object to be viewed. + /// + /// # Returns + /// + /// A `Result` containing the ndarray view if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `Image` is not in BGR8 format or if the pixel data cannot be reshaped + /// into the expected ndarray view format. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel + /// let image = Image::new_bgr8(data, 3, 3, Some("example")).unwrap(); + /// + /// let ndarray_view = image.bgr8_to_ndarray_view().unwrap(); + /// ``` + pub fn bgr8_to_ndarray_view(&self) -> Result> { + match self.encoding { + Encoding::BGR8 => ndarray::ArrayView::from_shape( + (self.height as usize, self.width as usize, 3), + self.data.as_u8()?, + ) + .wrap_err("Failed to reshape data into ndarray: width, height and BGR8 encoding doesn't match data data length."), + _ => Err(Report::msg("Image is not in BGR8 format")), + } + } + + /// Converts a mutable BGR8 `Image` into a mutable ndarray view. + /// + /// This function takes a mutable reference to a BGR8 `Image` and creates an `ndarray::ArrayViewMut` + /// over the pixel data. The resulting view has shape (height, width, 3). + /// + /// # Arguments + /// + /// * `&mut self` - A mutable reference to the `Image` object to be viewed. + /// + /// # Returns + /// + /// A `Result` containing the mutable ndarray view if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `Image` is not in BGR8 format or if the pixel data cannot be reshaped + /// into the expected mutable ndarray view format. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel + /// let mut image = Image::new_bgr8(data, 3, 3, Some("example")).unwrap(); + /// + /// let ndarray_view_mut = image.bgr8_to_ndarray_view_mut().unwrap(); + /// ``` + pub fn bgr8_to_ndarray_view_mut(&mut self) -> Result> { + match self.encoding { + Encoding::BGR8 => ndarray::ArrayViewMut::from_shape( + (self.height as usize, self.width as usize, 3), + self.data.as_mut_u8()?, + ) + .wrap_err("Failed to reshape data into ndarray: width, height and BGR8 encoding doesn't match data data length."), + _ => Err(Report::msg("Image is not in BGR8 format")), + } + } +} + +mod tests { + #[test] + fn test_bgr8_creation() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + + Image::new_bgr8(flat_image, 3, 3, Some("camera.test")).unwrap(); + } + + #[test] + fn test_bgr8_from_ndarray() { + use ndarray::Array3; + + use crate::image::Image; + + let array = Array3::::zeros((3, 3, 3)); + + Image::bgr8_from_ndarray(array, Some("camera.test")).unwrap(); + } + + #[test] + fn test_bgr8_to_ndarray() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + + let image = Image::new_bgr8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + image.bgr8_to_ndarray().unwrap(); + } + + #[test] + fn test_bgr8_to_ndarray_view() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + + let image = Image::new_bgr8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + image.bgr8_to_ndarray_view().unwrap(); + } + + #[test] + fn test_bgr8_to_ndarray_view_mut() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + + let mut image = Image::new_bgr8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + image.bgr8_to_ndarray_view_mut().unwrap(); + } + + #[test] + fn test_bgr8_ndarray_zero_copy_conversion() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + let original_buffer_address = flat_image.as_ptr(); + + let bgr8_image = Image::new_bgr8(flat_image, 3, 3, None).unwrap(); + let image_buffer_address = bgr8_image.as_ptr(); + + let bgr8_ndarray = bgr8_image.bgr8_to_ndarray().unwrap(); + let ndarray_buffer_address = bgr8_ndarray.as_ptr(); + + let final_image = Image::bgr8_from_ndarray(bgr8_ndarray, None).unwrap(); + let final_image_buffer_address = final_image.as_ptr(); + + assert_eq!(original_buffer_address, image_buffer_address); + assert_eq!(image_buffer_address, ndarray_buffer_address); + assert_eq!(ndarray_buffer_address, final_image_buffer_address); + } +} diff --git a/src/image/container.rs b/src/image/container.rs new file mode 100644 index 0000000..d05dd91 --- /dev/null +++ b/src/image/container.rs @@ -0,0 +1,36 @@ +use eyre::Result; + +#[derive(Debug)] +pub enum DataContainer { + U8Data(Vec), +} + +impl DataContainer { + pub fn as_ptr(&self) -> *const u8 { + match self { + Self::U8Data(data) => data.as_ptr(), + } + } + + pub fn into_u8(self) -> Result> { + match self { + Self::U8Data(data) => Ok(data), + } + } + + pub fn as_u8(&self) -> Result<&Vec> { + match self { + Self::U8Data(data) => Ok(data), + } + } + + pub fn as_mut_u8(&mut self) -> Result<&mut Vec> { + match self { + Self::U8Data(data) => Ok(data), + } + } + + pub fn from_u8(data: Vec) -> Self { + Self::U8Data(data) + } +} diff --git a/src/image/encoding.rs b/src/image/encoding.rs new file mode 100644 index 0000000..ff1c22c --- /dev/null +++ b/src/image/encoding.rs @@ -0,0 +1,31 @@ +use eyre::{Report, Result}; + +use std::fmt::Display; + +#[derive(Debug, Clone, Copy)] +pub enum Encoding { + RGB8, + BGR8, + GRAY8, +} + +impl Encoding { + pub fn from_string(encoding: String) -> Result { + match encoding.as_str() { + "RGB8" => Ok(Self::RGB8), + "BGR8" => Ok(Self::BGR8), + "GRAY8" => Ok(Self::GRAY8), + _ => Err(Report::msg(format!("Invalid String Encoding {}", encoding))), + } + } +} + +impl Display for Encoding { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + Self::RGB8 => write!(fmt, "RGB8"), + Self::BGR8 => write!(fmt, "BGR8"), + Self::GRAY8 => write!(fmt, "GRAY8"), + } + } +} diff --git a/src/image/gray8.rs b/src/image/gray8.rs new file mode 100644 index 0000000..9a02230 --- /dev/null +++ b/src/image/gray8.rs @@ -0,0 +1,280 @@ +use super::{container::DataContainer, encoding::Encoding, Image}; +use eyre::{Context, Report, Result}; + +impl Image { + /// Creates a new `Image` in Gray8 format. + /// + /// This function constructs a new `Image` object with the given pixel data, width, height, + /// and an optional name. It ensures that the pixel data length matches the expected size + /// for the given width and height. + /// + /// # Arguments + /// + /// * `data` - A `Vec` containing the pixel data in Gray8 format. + /// * `width` - The width of the image. + /// * `height` - The height of the image. + /// * `name` - An optional string slice representing the name of the image. + /// + /// # Returns + /// + /// A `Result` containing the constructed `Image` if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the length of the pixel data does not match the expected size + /// based on the width and height. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 9]; // 3x3 image with 1 byte per pixel + /// let image = Image::new_gray8(data, 3, 3, Some("example")).unwrap(); + /// ``` + pub fn new_gray8(data: Vec, width: u32, height: u32, name: Option<&str>) -> Result { + if data.len() != (width * height) as usize { + return Err(Report::msg("Invalid data data length.")); + } + + Ok(Image { + data: DataContainer::from_u8(data), + width, + height, + encoding: Encoding::GRAY8, + name: name.map(|s| s.to_string()), + }) + } + + /// Creates a new `Image` in Gray8 format from an ndarray. + /// + /// This function constructs a new `Image` object from an `ndarray::Array` with shape (height, width). + /// It converts the ndarray into a raw vector and uses it to create the `Image`. + /// + /// # Arguments + /// + /// * `array` - An `ndarray::Array` containing the pixel data in Gray8 format. + /// * `name` - An optional string slice representing the name of the image. + /// + /// # Returns + /// + /// A `Result` containing the constructed `Image` if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the ndarray cannot be converted into a valid `Image`. + /// + /// # Example + /// + /// ``` + /// use ndarray::Array2; + /// use fastformat::image::Image; + /// + /// let array = Array2::::zeros((3, 3)); // 3x3 image + /// let image = Image::gray8_from_ndarray(array, Some("example")).unwrap(); + /// ``` + pub fn gray8_from_ndarray( + array: ndarray::Array, + name: Option<&str>, + ) -> Result { + let width = array.shape()[1] as u32; + let height = array.shape()[0] as u32; + + let data = array.into_raw_vec(); + + Self::new_gray8(data, width, height, name) + } + + /// Converts a Gray8 `Image` into an ndarray. + /// + /// This function takes a Gray8 `Image` and converts it into an `ndarray::Array`. + /// The resulting ndarray has shape (height, width). + /// + /// # Arguments + /// + /// * `self` - The `Image` object to be converted. + /// + /// # Returns + /// + /// A `Result` containing the constructed ndarray if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `Image` is not in Gray8 format or if the pixel data cannot be reshaped + /// into the expected ndarray format. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 9]; // 3x3 image with 1 byte per pixel + /// let image = Image::new_gray8(data, 3, 3, Some("example")).unwrap(); + /// + /// let ndarray = image.gray8_to_ndarray().unwrap(); + /// ``` + pub fn gray8_to_ndarray(self) -> Result> { + match self.encoding { + Encoding::GRAY8 => ndarray::Array::from_shape_vec( + (self.height as usize, self.width as usize), + self.data.into_u8()?, + ) + .wrap_err("Failed to reshape data into ndarray: width, height and Gray8 encoding doesn't match data data length."), + _ => Err(Report::msg("Image is not in Gray8 format")), + } + } + + /// Converts a Gray8 `Image` into an ndarray view. + /// + /// This function takes a reference to a Gray8 `Image` and creates an `ndarray::ArrayView` + /// over the pixel data. The resulting view has shape (height, width). + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Image` object to be viewed. + /// + /// # Returns + /// + /// A `Result` containing the ndarray view if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `Image` is not in Gray8 format or if the pixel data cannot be reshaped + /// into the expected ndarray view format. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 9]; // 3x3 image with 1 byte per pixel + /// let image = Image::new_gray8(data, 3, 3, Some("example")).unwrap(); + /// + /// let ndarray_view = image.gray8_to_ndarray_view().unwrap(); + /// ``` + pub fn gray8_to_ndarray_view(&self) -> Result> { + match self.encoding { + Encoding::GRAY8 => ndarray::ArrayView::from_shape( + (self.height as usize, self.width as usize), + self.data.as_u8()?, + ) + .wrap_err("Failed to reshape data into ndarray: width, height and Gray8 encoding doesn't match data data length."), + _ => Err(Report::msg("Image is not in Gray8 format")), + } + } + + /// Converts a mutable Gray8 `Image` into a mutable ndarray view. + /// + /// This function takes a mutable reference to a Gray8 `Image` and creates an `ndarray::ArrayViewMut` + /// over the pixel data. The resulting view has shape (height, width). + /// + /// # Arguments + /// + /// * `&mut self` - A mutable reference to the `Image` object to be viewed. + /// + /// # Returns + /// + /// A `Result` containing the mutable ndarray view if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `Image` is not in Gray8 format or if the pixel data cannot be reshaped + /// into the expected mutable ndarray view format. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 9]; // 3x3 image with 1 byte per pixel + /// let mut image = Image::new_gray8(data, 3, 3, Some("example")).unwrap(); + /// + /// let ndarray_view_mut = image.gray8_to_ndarray_view_mut().unwrap(); + /// ``` + pub fn gray8_to_ndarray_view_mut(&mut self) -> Result> { + match self.encoding { + Encoding::GRAY8 => ndarray::ArrayViewMut::from_shape( + (self.height as usize, self.width as usize), + self.data.as_mut_u8()?, + ) + .wrap_err("Failed to reshape data into ndarray: width, height and Gray8 encoding doesn't match data data length."), + _ => Err(Report::msg("Image is not in Gray8 format")), + } + } +} + +mod test { + #[test] + fn test_gray8_creation() { + use crate::image::Image; + + let flat_image = (1..10).collect::>(); + + Image::new_gray8(flat_image, 3, 3, Some("camera.test")).unwrap(); + } + + #[test] + fn test_gray8_from_ndarray() { + use ndarray::Array2; + + use crate::image::Image; + + let array = Array2::::zeros((3, 3)); + + Image::gray8_from_ndarray(array, Some("camera.test")).unwrap(); + } + + #[test] + fn test_gray8_to_ndarray() { + use crate::image::Image; + + let flat_image = (1..10).collect::>(); + + let image = Image::new_gray8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + image.gray8_to_ndarray().unwrap(); + } + + #[test] + fn test_gray8_to_ndarray_view() { + use crate::image::Image; + + let flat_image = (1..10).collect::>(); + + let image = Image::new_gray8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + image.gray8_to_ndarray_view().unwrap(); + } + + #[test] + fn test_gray8_to_ndarray_view_mut() { + use crate::image::Image; + + let flat_image = (1..10).collect::>(); + + let mut image = Image::new_gray8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + image.gray8_to_ndarray_view_mut().unwrap(); + } + + #[test] + fn test_gray8_ndarray_zero_copy_conversion() { + use crate::image::Image; + + let flat_image = (1..10).collect::>(); + let original_buffer_address = flat_image.as_ptr(); + + let gray8_image = Image::new_gray8(flat_image, 3, 3, None).unwrap(); + let image_buffer_address = gray8_image.as_ptr(); + + let gray8_ndarray = gray8_image.gray8_to_ndarray().unwrap(); + let ndarray_buffer_address = gray8_ndarray.as_ptr(); + + let final_image = Image::gray8_from_ndarray(gray8_ndarray, None).unwrap(); + let final_image_buffer_address = final_image.as_ptr(); + + assert_eq!(original_buffer_address, image_buffer_address); + assert_eq!(image_buffer_address, ndarray_buffer_address); + assert_eq!(ndarray_buffer_address, final_image_buffer_address); + } +} diff --git a/src/image/rgb8.rs b/src/image/rgb8.rs new file mode 100644 index 0000000..8ebfda2 --- /dev/null +++ b/src/image/rgb8.rs @@ -0,0 +1,280 @@ +use super::{container::DataContainer, encoding::Encoding, Image}; +use eyre::{Context, Report, Result}; + +impl Image { + /// Creates a new `Image` in RGB8 format. + /// + /// This function constructs a new `Image` object with the given pixel data, width, height, + /// and an optional name. It ensures that the pixel data length matches the expected size + /// for the given width and height. + /// + /// # Arguments + /// + /// * `data` - A `Vec` containing the pixel data in RGB8 format. + /// * `width` - The width of the image. + /// * `height` - The height of the image. + /// * `name` - An optional string slice representing the name of the image. + /// + /// # Returns + /// + /// A `Result` containing the constructed `Image` if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the length of the pixel data does not match the expected size + /// based on the width and height. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel + /// let image = Image::new_rgb8(data, 3, 3, Some("example")).unwrap(); + /// ``` + pub fn new_rgb8(data: Vec, width: u32, height: u32, name: Option<&str>) -> Result { + if data.len() != (width * height * 3) as usize { + return Err(Report::msg("Invalid pixel data length.")); + } + + Ok(Image { + data: DataContainer::from_u8(data), + width, + height, + encoding: Encoding::RGB8, + name: name.map(|s| s.to_string()), + }) + } + + /// Creates a new `Image` in RGB8 format from an ndarray. + /// + /// This function constructs a new `Image` object from an `ndarray::Array` with shape (height, width, 3). + /// It converts the ndarray into a raw vector and uses it to create the `Image`. + /// + /// # Arguments + /// + /// * `array` - An `ndarray::Array` containing the pixel data in RGB8 format. + /// * `name` - An optional string slice representing the name of the image. + /// + /// # Returns + /// + /// A `Result` containing the constructed `Image` if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the ndarray cannot be converted into a valid `Image`. + /// + /// # Example + /// + /// ``` + /// use ndarray::Array3; + /// use fastformat::image::Image; + /// + /// let array = Array3::::zeros((3, 3, 3)); // 3x3 image with 3 bytes per pixel + /// let image = Image::rgb8_from_ndarray(array, Some("example")).unwrap(); + /// ``` + pub fn rgb8_from_ndarray( + array: ndarray::Array, + name: Option<&str>, + ) -> Result { + let width = array.shape()[1] as u32; + let height = array.shape()[0] as u32; + + let data = array.into_raw_vec(); + + Self::new_rgb8(data, width, height, name) + } + + /// Converts an RGB8 `Image` into an ndarray. + /// + /// This function takes an RGB8 `Image` and converts it into an `ndarray::Array`. + /// The resulting ndarray has shape (height, width, 3). + /// + /// # Arguments + /// + /// * `self` - The `Image` object to be converted. + /// + /// # Returns + /// + /// A `Result` containing the constructed ndarray if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `Image` is not in RGB8 format or if the pixel data cannot be reshaped + /// into the expected ndarray format. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel + /// let image = Image::new_rgb8(data, 3, 3, Some("example")).unwrap(); + /// + /// let ndarray = image.rgb8_to_ndarray().unwrap(); + /// ``` + pub fn rgb8_to_ndarray(self) -> Result> { + match self.encoding { + Encoding::RGB8 => ndarray::Array::from_shape_vec( + (self.height as usize, self.width as usize, 3), + self.data.into_u8()?, + ) + .wrap_err("Failed to reshape data into ndarray: width, height and RGB8 encoding doesn't match data data length."), + _ => Err(Report::msg("Image is not in RGB8 format")), + } + } + + /// Converts an RGB8 `Image` into an ndarray view. + /// + /// This function takes a reference to an RGB8 `Image` and creates an `ndarray::ArrayView` + /// over the pixel data. The resulting view has shape (height, width, 3). + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Image` object to be viewed. + /// + /// # Returns + /// + /// A `Result` containing the ndarray view if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `Image` is not in RGB8 format or if the pixel data cannot be reshaped + /// into the expected ndarray view format. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel + /// let image = Image::new_rgb8(data, 3, 3, Some("example")).unwrap(); + /// + /// let ndarray_view = image.rgb8_to_ndarray_view().unwrap(); + /// ``` + pub fn rgb8_to_ndarray_view(&self) -> Result> { + match self.encoding { + Encoding::RGB8 => ndarray::ArrayView::from_shape( + (self.height as usize, self.width as usize, 3), + self.data.as_u8()?, + ) + .wrap_err("Failed to reshape data into ndarray: width, height and RGB8 encoding doesn't match data data length."), + _ => Err(Report::msg("Image is not in RGB8 format")), + } + } + + /// Converts a mutable RGB8 `Image` into a mutable ndarray view. + /// + /// This function takes a mutable reference to an RGB8 `Image` and creates an `ndarray::ArrayViewMut` + /// over the pixel data. The resulting view has shape (height, width, 3). + /// + /// # Arguments + /// + /// * `&mut self` - A mutable reference to the `Image` object to be viewed. + /// + /// # Returns + /// + /// A `Result` containing the mutable ndarray view if successful, or an error otherwise. + /// + /// # Errors + /// + /// Returns an error if the `Image` is not in RGB8 format or if the pixel data cannot be reshaped + /// into the expected mutable ndarray view format. + /// + /// # Example + /// + /// ``` + /// use fastformat::image::Image; + /// + /// let data = vec![0; 27]; // 3x3 image with 3 bytes per pixel + /// let mut image = Image::new_rgb8(data, 3, 3, Some("example")).unwrap(); + /// + /// let ndarray_view_mut = image.rgb8_to_ndarray_view_mut().unwrap(); + /// ``` + pub fn rgb8_to_ndarray_view_mut(&mut self) -> Result> { + match self.encoding { + Encoding::RGB8 => ndarray::ArrayViewMut::from_shape( + (self.height as usize, self.width as usize, 3), + self.data.as_mut_u8()?, + ) + .wrap_err("Failed to reshape data into ndarray: width, height and RGB8 encoding doesn't match data data length."), + _ => Err(Report::msg("Image is not in RGB8 format")), + } + } +} + +mod tests { + #[test] + fn test_rgb8_creation() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + + Image::new_rgb8(flat_image, 3, 3, Some("camera.test")).unwrap(); + } + + #[test] + fn test_rgb8_from_ndarray() { + use ndarray::Array3; + + use crate::image::Image; + + let array = Array3::::zeros((3, 3, 3)); + + Image::rgb8_from_ndarray(array, Some("camera.test")).unwrap(); + } + + #[test] + fn test_rgb8_to_ndarray() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + + let image = Image::new_rgb8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + image.rgb8_to_ndarray().unwrap(); + } + + #[test] + fn test_rgb8_to_ndarray_view() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + + let image = Image::new_rgb8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + image.rgb8_to_ndarray_view().unwrap(); + } + + #[test] + fn test_rgb8_to_ndarray_view_mut() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + + let mut image = Image::new_rgb8(flat_image, 3, 3, Some("camera.test")).unwrap(); + + image.rgb8_to_ndarray_view_mut().unwrap(); + } + + #[test] + fn test_rgb8_ndarray_zero_copy_conversion() { + use crate::image::Image; + + let flat_image = vec![0; 27]; + let original_buffer_address = flat_image.as_ptr(); + + let rgb8_image = Image::new_rgb8(flat_image, 3, 3, None).unwrap(); + let image_buffer_address = rgb8_image.as_ptr(); + + let rgb8_ndarray = rgb8_image.rgb8_to_ndarray().unwrap(); + let ndarray_buffer_address = rgb8_ndarray.as_ptr(); + + let final_image = Image::rgb8_from_ndarray(rgb8_ndarray, None).unwrap(); + let final_image_buffer_address = final_image.as_ptr(); + + assert_eq!(original_buffer_address, image_buffer_address); + assert_eq!(image_buffer_address, ndarray_buffer_address); + assert_eq!(ndarray_buffer_address, final_image_buffer_address); + } +} diff --git a/src/lib.rs b/src/lib.rs index c343935..7419bd0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,9 @@ use pyo3::prelude::*; +pub mod arrow; + +pub mod image; + /// Formats the sum of two numbers as string. #[pyfunction] fn sum_as_string(a: usize, b: usize) -> PyResult {