imagepipe-0.5.0/.cargo_vcs_info.json0000644000000001360000000000100127760ustar { "git": { "sha1": "65ca96ce730d42da4f0a70331902f4094d7b83da" }, "path_in_vcs": "" }imagepipe-0.5.0/.gitignore000064400000000000000000000000470072674642500136070ustar 00000000000000target Cargo.lock rawloader multicache imagepipe-0.5.0/.travis.yml000064400000000000000000000007700072674642500137330ustar 00000000000000language: rust rust: - stable - beta - nightly matrix: allow_failures: - rust: nightly before_script: - git clone https://github.com/pedrocr/rawloader.git ../rawloader/ - git clone https://github.com/pedrocr/multicache.git ../multicache/ notifications: email: recipients: - pedro@pedrocr.net on_success: change on_failure: always irc: channels: - "irc.libera.chat#chimper" on_success: change on_failure: always skip_join: true if: tag IS blank imagepipe-0.5.0/Cargo.lock0000644000000517550000000000100107660ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "adler32" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "arrayref" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" [[package]] name = "arrayvec" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bincode" version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" dependencies = [ "serde", ] [[package]] name = "bit_field" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "blake3" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", "digest", ] [[package]] name = "block-buffer" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "bytemuck" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[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 = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "crc32fast" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "lazy_static", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-utils" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" dependencies = [ "cfg-if", "lazy_static", ] [[package]] name = "crypto-common" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ "generic-array", "typenum", ] [[package]] name = "deflate" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" dependencies = [ "adler32", ] [[package]] name = "digest" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "enumn" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e58b112d5099aa0857c5d05f0eacab86406dd8c0f85fe5d320a13256d29ecf4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "exr" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4badb9489a465cb2c555af1f00f0bfd8cecd6fc12ac11da9d5b40c5dd5f0200" dependencies = [ "bit_field", "deflate", "flume", "half", "inflate", "lebe", "smallvec", "threadpool", ] [[package]] name = "flate2" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" dependencies = [ "cfg-if", "crc32fast", "libc", "miniz_oxide", ] [[package]] name = "flume" version = "0.10.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843c03199d0c0ca54bc1ea90ac0d507274c28abcc4f691ae8b4eaa375087c76a" dependencies = [ "futures-core", "futures-sink", "nanorand", "pin-project", "spin", ] [[package]] name = "futures-core" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-sink" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "generic-array" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gif" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3a7187e78088aead22ceedeee99779455b23fc231fe13ec443f99bb71694e5b" dependencies = [ "color_quant", "weezl", ] [[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "half" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "image" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db207d030ae38f1eb6f240d5a1c1c88ff422aa005d10f8c6c6fc5e75286ab30e" dependencies = [ "bytemuck", "byteorder", "color_quant", "exr", "gif", "jpeg-decoder", "num-iter", "num-rational", "num-traits", "png", "scoped_threadpool", "tiff", ] [[package]] name = "imagepipe" version = "0.5.0" dependencies = [ "bincode", "blake3", "image", "lazy_static", "log", "multicache", "num-traits", "rawloader", "rayon", "serde", "serde_derive", "serde_yaml", ] [[package]] name = "indexmap" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ "autocfg", "hashbrown", ] [[package]] name = "inflate" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" dependencies = [ "adler32", ] [[package]] name = "jpeg-decoder" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744c24117572563a98a7e9168a5ac1ee4a1ca7f702211258797bbe0ed0346c3c" dependencies = [ "rayon", ] [[package]] name = "js-sys" version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lebe" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7efd1d698db0759e6ef11a7cd44407407399a910c774dd804c64c032da7826ff" [[package]] name = "libc" version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" [[package]] name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lock_api" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" dependencies = [ "cfg-if", ] [[package]] name = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] [[package]] name = "miniz_oxide" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" dependencies = [ "adler", ] [[package]] name = "multicache" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5086074c0a0812980aa88703d1bbcb4433e8423ecf4098a9849934f3dc09ba72" dependencies = [ "linked-hash-map", ] [[package]] name = "nanorand" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ "getrandom", ] [[package]] name = "num-integer" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-iter" version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "pin-project" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "png" version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" dependencies = [ "bitflags", "crc32fast", "deflate", "miniz_oxide", ] [[package]] name = "proc-macro2" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" dependencies = [ "unicode-xid", ] [[package]] name = "quote" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] [[package]] name = "rawloader" version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d8c6f168c492ffd326537b3aa5a8d5fe07f0d8a3970c5957f286bcd13f888aa" dependencies = [ "byteorder", "enumn", "glob", "lazy_static", "rayon", "rustc_version", "toml", ] [[package]] name = "rayon" version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221" dependencies = [ "autocfg", "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", "num_cpus", ] [[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.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "scoped_threadpool" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" [[package]] name = "serde" version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" [[package]] name = "serde_derive" version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_yaml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" dependencies = [ "indexmap", "ryu", "serde", "yaml-rust", ] [[package]] name = "smallvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "spin" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" dependencies = [ "lock_api", ] [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "threadpool" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" dependencies = [ "num_cpus", ] [[package]] name = "tiff" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cfada0986f446a770eca461e8c6566cb879682f7d687c8348aa0c857bd52286" dependencies = [ "flate2", "jpeg-decoder", "weezl", ] [[package]] name = "toml" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" dependencies = [ "bumpalo", "lazy_static", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" [[package]] name = "weezl" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] imagepipe-0.5.0/Cargo.toml0000644000000024710000000000100110000ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "imagepipe" version = "0.5.0" authors = ["Pedro Côrte-Real "] description = "An image processing pipeline" documentation = "https://docs.rs/imagepipe/" categories = ["multimedia::images"] license = "LGPL-3.0-only" repository = "https://github.com/pedrocr/imagepipe" [profile.dev] opt-level = 3 [[bin]] name = "converter" doc = false [dependencies.bincode] version = "1" [dependencies.blake3] version = "1" [dependencies.image] version = "0.24" [dependencies.lazy_static] version = "1" [dependencies.log] version = "0.4" [dependencies.multicache] version = "0.6.0" [dependencies.num-traits] version = "0.2" [dependencies.rawloader] version = "0.37" [dependencies.rayon] version = "1" [dependencies.serde] version = "1" [dependencies.serde_derive] version = "1" [dependencies.serde_yaml] version = "0.8" imagepipe-0.5.0/Cargo.toml.orig000064400000000000000000000013050072674642500145040ustar 00000000000000[package] name = "imagepipe" version = "0.5.0" authors = ["Pedro Côrte-Real "] description = "An image processing pipeline" documentation = "https://docs.rs/imagepipe/" #homepage = "..." repository = "https://github.com/pedrocr/imagepipe" license = "LGPL-3.0-only" categories = ["multimedia::images"] edition = "2018" [dependencies] lazy_static = "1" rayon = "1" serde = "1" serde_derive = "1" serde_yaml = "0.8" bincode = "1" blake3 = "1" log = "0.4" num-traits = "0.2" image = "0.24" [dependencies.rawloader] version = "0.37" path = "../rawloader" [dependencies.multicache] version = "0.6.0" path = "../multicache" [profile.dev] opt-level = 3 [[bin]] name = "converter" doc = false imagepipe-0.5.0/LICENSE000064400000000000000000000167440072674642500126370ustar 00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. imagepipe-0.5.0/README.md000064400000000000000000000012030072674642500130710ustar 00000000000000# imagepipe [![Build Status](https://travis-ci.com/pedrocr/imagepipe.svg?branch=master)](https://travis-ci.com/pedrocr/imagepipe) [![Crates.io](https://img.shields.io/crates/v/imagepipe.svg)](https://crates.io/crates/imagepipe) An image pipeline capable of taking any image format, including camera RAW files and transforming it into a final output. Contributing ------------ Bug reports and pull requests welcome at https://github.com/pedrocr/imagepipe Meet us at #chimper on irc.libera.chat if you need to discuss a feature or issue in detail or even just for general chat. To just start chatting go to https://web.libera.chat/#chimper imagepipe-0.5.0/src/bin/converter.rs000064400000000000000000000036730072674642500155430ustar 00000000000000use std::env; use std::fs::File; use std::io::BufWriter; //use std::time::Instant; use image::ColorType; extern crate imagepipe; extern crate rawloader; extern crate image; fn usage() { println!("converter [outfile]"); std::process::exit(1); } fn error(err: &str) -> ! { println!("ERROR: {}", err); std::process::exit(2); } fn main() { let args: Vec<_> = env::args().collect(); if args.len() < 2 { usage(); } let file = &args[1]; let filejpg = format!("{}.jpg",file); let outfile = if args.len() > 2 { &args[2] } else { &filejpg }; println!("Loading file \"{}\" and saving it as \"{}\"", file, outfile); /* let from_time = Instant::now(); let image = match rawloader::decode_file(file) { Ok(val) => val, Err(e) => {error(&e.to_string());unreachable!()}, }; let duration = from_time.elapsed(); println!("Decoded in {} ms", duration.as_millis()); println!("Found camera \"{}\" model \"{}\"", image.make, image.model); println!("Found clean named camera \"{}\" model \"{}\"", image.clean_make, image.clean_model); println!("Image size is {}x{}", image.width, image.height); println!("WB coeffs are {:?}", image.wb_coeffs); println!("black levels are {:?}", image.blacklevels); println!("white levels are {:?}", image.whitelevels); println!("xyz_to_cam is {:?}", image.xyz_to_cam); println!("CFA is {:?}", image.cfa); println!("crops are {:?}", image.crops); */ let decoded = match imagepipe::simple_decode_8bit(file, 0, 0) { Ok(img) => img, Err(e) => error(&e), }; let uf = match File::create(outfile) { Ok(val) => val, Err(e) => { error(format!("Error: {}", e).as_ref()) } }; let mut f = BufWriter::new(uf); let mut jpg_encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut f, 90); jpg_encoder .encode(&decoded.data, decoded.width as u32, decoded.height as u32, ColorType::Rgb8) .expect("Encoding image in JPEG format failed."); } imagepipe-0.5.0/src/buffer.rs000064400000000000000000000056120072674642500142300ustar 00000000000000extern crate rayon; use self::rayon::prelude::*; #[derive(Debug, Clone, PartialEq)] pub struct OpBuffer { pub width: usize, pub height: usize, pub colors: usize, pub monochrome: bool, pub data: Vec, } impl OpBuffer { pub fn default() -> OpBuffer { OpBuffer { width: 0, height: 0, colors: 0, monochrome: false, data: Vec::new(), } } pub fn new(width: usize, height: usize, colors: usize, monochrome: bool) -> OpBuffer { OpBuffer { width: width, height: height, colors: colors, monochrome, data: vec![0.0; width*height*(colors as usize)], } } pub fn mutate_lines(&mut self, closure: &F) where F : Fn(&mut [f32], usize)+Sync { self.data.par_chunks_mut(self.width*self.colors).enumerate().for_each(|(row, line)| { closure(line, row); }); } pub fn mutate_lines_copying(&self, closure: &F) -> OpBuffer where F : Fn(&mut [f32], usize)+Sync { let mut buf = self.clone(); buf.data.par_chunks_mut(self.width*self.colors).enumerate().for_each(|(row, line)| { closure(line, row); }); buf } pub fn process_into_new(&self, colors: usize, closure: &F) -> OpBuffer where F : Fn(&mut [f32], &[f32])+Sync { let mut out = OpBuffer::new(self.width, self.height, colors, self.monochrome); out.data.par_chunks_mut(out.width*out.colors).enumerate().for_each(|(row, line)| { closure(line, &self.data[self.width*self.colors*row..]); }); out } pub fn transform(&self, topleft: (isize, isize), topright: (isize, isize), bottomleft: (isize, isize), width: usize, height: usize) -> OpBuffer { let newdata = crate::scaling::transform_buffer(&self.data, self.width, self.height, topleft, topright, bottomleft, width, height, self.colors, None); Self { width, height, colors: self.colors, monochrome: self.monochrome, data: newdata, } } /// Helper function to allow human readable creation of `OpBuffer` instances pub fn from_rgb_str_vec(data: Vec<&str>) -> OpBuffer { let width = data.first().expect("Invalid data for rgb helper function").len(); let height = data.len(); let colors = 3; let mut pixel_data: Vec = Vec::with_capacity(width * height * colors); for row in data { for col in row.chars() { let (r, g, b) = match col { 'R' => (1.0, 0.0, 0.0), 'G' => (0.0, 1.0, 0.0), 'B' => (0.0, 0.0, 1.0), 'O' => (1.0, 1.0, 1.0), ' ' => (0.0, 0.0, 0.0), c @ _ => panic!( "Invalid color '{}' sent to rgb expected any of 'RGBO '", c), }; pixel_data.push(r); pixel_data.push(g); pixel_data.push(b); } } OpBuffer { width: width, height: height, colors: colors, monochrome: false, data: pixel_data, } } } imagepipe-0.5.0/src/color_conversions.rs000064400000000000000000000460100072674642500165220ustar 00000000000000lazy_static! { pub static ref SRGB_D65_33: [[f32;3];3] = [ [0.4124564, 0.3575761, 0.1804375], [0.2126729, 0.7151522, 0.0721750], [0.0193339, 0.1191920, 0.9503041], ]; pub static ref SRGB_D65_XYZ_WHITE: (f32,f32,f32) = (0.95047, 1.000, 1.08883); pub static ref XYZ_D65_33: [[f32;3];3] = inverse(*SRGB_D65_33); pub static ref XYZ_D65_34: [[f32;3];4] = [ XYZ_D65_33[0], XYZ_D65_33[1], XYZ_D65_33[2], [0.0, 0.0, 0.0] ]; pub static ref SRGB_D65_43: [[f32;4];3] = [ [SRGB_D65_33[0][0], SRGB_D65_33[0][1], SRGB_D65_33[0][2], 0.0], [SRGB_D65_33[1][0], SRGB_D65_33[1][1], SRGB_D65_33[1][2], 0.0], [SRGB_D65_33[2][0], SRGB_D65_33[2][1], SRGB_D65_33[2][2], 0.0], ]; } // FIXME: when float math is allowed in const fn get rid of lazy_static! fn inverse(inm: [[f32;3];3]) -> [[f32;3];3] { let invdet = 1.0 / ( inm[0][0] * (inm[1][1] * inm[2][2] - inm[2][1] * inm[1][2]) - inm[0][1] * (inm[1][0] * inm[2][2] - inm[1][2] * inm[2][0]) + inm[0][2] * (inm[1][0] * inm[2][1] - inm[1][1] * inm[2][0]) ); let mut out = [[0.0; 3];3]; out[0][0] = (inm[1][1]*inm[2][2] - inm[2][1]*inm[1][2]) * invdet; out[0][1] = -(inm[0][1]*inm[2][2] - inm[0][2]*inm[2][1]) * invdet; out[0][2] = (inm[0][1]*inm[1][2] - inm[0][2]*inm[1][1]) * invdet; out[1][0] = -(inm[1][0]*inm[2][2] - inm[1][2]*inm[2][0]) * invdet; out[1][1] = (inm[0][0]*inm[2][2] - inm[0][2]*inm[2][0]) * invdet; out[1][2] = -(inm[0][0]*inm[1][2] - inm[1][0]*inm[0][2]) * invdet; out[2][0] = (inm[1][0]*inm[2][1] - inm[2][0]*inm[1][1]) * invdet; out[2][1] = -(inm[0][0]*inm[2][1] - inm[2][0]*inm[0][1]) * invdet; out[2][2] = (inm[0][0]*inm[1][1] - inm[1][0]*inm[0][1]) * invdet; out } #[inline(always)] pub fn camera_to_lab(mul: [f32;4], cmatrix: [[f32;4];3], pixin: &[f32]) -> (f32, f32, f32) { // Multiply pixel by RGBE multipliers, clipping to 1.0 let r = (pixin[0] * mul[0]).min(1.0); let g = (pixin[1] * mul[1]).min(1.0); let b = (pixin[2] * mul[2]).min(1.0); let e = (pixin[3] * mul[3]).min(1.0); // Calculate XYZ by applying the camera matrix let x = r * cmatrix[0][0] + g * cmatrix[0][1] + b * cmatrix[0][2] + e * cmatrix[0][3]; let y = r * cmatrix[1][0] + g * cmatrix[1][1] + b * cmatrix[1][2] + e * cmatrix[1][3]; let z = r * cmatrix[2][0] + g * cmatrix[2][1] + b * cmatrix[2][2] + e * cmatrix[2][3]; xyz_to_lab(x,y,z) } #[inline(always)] pub fn lab_to_rgb(rgbmatrix: [[f32;3];3], pixin: &[f32]) -> (f32, f32, f32) { let (x,y,z) = lab_to_xyz(pixin[0], pixin[1], pixin[2]); let r = x * rgbmatrix[0][0] + y * rgbmatrix[0][1] + z * rgbmatrix[0][2]; let g = x * rgbmatrix[1][0] + y * rgbmatrix[1][1] + z * rgbmatrix[1][2]; let b = x * rgbmatrix[2][0] + y * rgbmatrix[2][1] + z * rgbmatrix[2][2]; (r, g, b) } #[inline(always)] pub fn temp_tint_to_rgb(temp: f32, tint: f32) -> (f32, f32, f32) { let xyz = temp_to_xyz(temp); let (x, y , z) = (xyz[0], xyz[1]/tint, xyz[2]); let rgbmatrix = *XYZ_D65_33; let r = x * rgbmatrix[0][0] + y * rgbmatrix[0][1] + z * rgbmatrix[0][2]; let g = x * rgbmatrix[1][0] + y * rgbmatrix[1][1] + z * rgbmatrix[1][2]; let b = x * rgbmatrix[2][0] + y * rgbmatrix[2][1] + z * rgbmatrix[2][2]; (r, g, b) } // Encapsulate a given transformation including a lookup table for speedup struct TransformLookup { max: f32, table: Vec, transform: Box f32+Sync>, } impl TransformLookup { fn new(maxbits: usize, transform: F) -> Self where F : Fn(f32) -> f32+Sync+'static { let max = (1 << maxbits) - 1; let mut table: Vec = Vec::with_capacity(max+2); for i in 0..=(max+1) { let v = (i as f32) / (max as f32); table.push(transform(v)); } Self { max: max as f32, table, transform: Box::new(transform), } } fn lookup(&self, val: f32) -> f32 { if val < 0.0 || val > 1.0 { (self.transform)(val) } else { let pos = val * self.max as f32; let key = pos as usize; let base = pos.trunc(); let a = pos - base; let v1 = self.table[key]; let v2 = self.table[key+1]; v1 + a * (v2 - v1) } } } // FIXME: In the future when floats and loops are stable in const fn get rid of // lazy_static and have the table be generated at compile time instead. lazy_static! { static ref XYZ_LAB_TRANSFORM: TransformLookup = TransformLookup::new(13, |v: f32| { let e = 216.0 / 24389.0; let k = 24389.0 / 27.0; if v > e {v.cbrt()} else {(k*v + 16.0) / 116.0} }); static ref SRGB_GAMMA_REVERSE: TransformLookup = TransformLookup::new(13, |v: f32| { if v < 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) } }); static ref SRGB_GAMMA_TRANSFORM: TransformLookup = TransformLookup::new(13, |v: f32| { if v < 0.0031308 { v * 12.92 } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 } }); } /// Remove sRGB gamma from a value #[inline(always)] pub fn expand_srgb_gamma(v: f32) -> f32 { SRGB_GAMMA_REVERSE.lookup(v) } /// Apply sRGB gamma to a value #[inline(always)] pub fn apply_srgb_gamma(v: f32) -> f32 { SRGB_GAMMA_TRANSFORM.lookup(v) } #[inline(always)] pub fn xyz_to_lab(x: f32, y: f32, z: f32) -> (f32,f32,f32) { let (xw, yw, zw) = *SRGB_D65_XYZ_WHITE; let (xr, yr, zr) = (x/xw, y/yw, z/zw); let fx = XYZ_LAB_TRANSFORM.lookup(xr); let fy = XYZ_LAB_TRANSFORM.lookup(yr); let fz = XYZ_LAB_TRANSFORM.lookup(zr); let l = 116.0 * fy - 16.0; let a = 500.0 * (fx - fy); let b = 200.0 * (fy - fz); (l/100.0,(a+127.0)/255.0,(b+127.0)/255.0) } #[inline(always)] pub fn lab_to_xyz(l: f32, a: f32, b: f32) -> (f32,f32,f32) { let cl = l * 100.0; let ca = (a * 255.0) - 127.0; let cb = (b * 255.0) - 127.0; let fy = (cl + 16.0) / 116.0; let fx = ca / 500.0 + fy; let fz = fy - (cb / 200.0); let e = 216.0 / 24389.0; let k = 24389.0 / 27.0; let fx3 = fx * fx * fx; let xr = if fx3 > e {fx3} else {(116.0 * fx - 16.0)/k}; let yr = if cl > k*e {fy*fy*fy} else {cl / k}; let fz3 = fz * fz * fz; let zr = if fz3 > e {fz3} else {(116.0 * fz - 16.0)/k}; let (xw, yw, zw) = *SRGB_D65_XYZ_WHITE; (xr*xw, yr*yw, zr*zw) } const CIE_OBSERVERS : [(u32, [f64;3]); 81] = [ ( 380, [ 0.001368, 0.000039, 0.006450 ] ), ( 385, [ 0.002236, 0.000064, 0.010550 ] ), ( 390, [ 0.004243, 0.000120, 0.020050 ] ), ( 395, [ 0.007650, 0.000217, 0.036210 ] ), ( 400, [ 0.014310, 0.000396, 0.067850 ] ), ( 405, [ 0.023190, 0.000640, 0.110200 ] ), ( 410, [ 0.043510, 0.001210, 0.207400 ] ), ( 415, [ 0.077630, 0.002180, 0.371300 ] ), ( 420, [ 0.134380, 0.004000, 0.645600 ] ), ( 425, [ 0.214770, 0.007300, 1.039050 ] ), ( 430, [ 0.283900, 0.011600, 1.385600 ] ), ( 435, [ 0.328500, 0.016840, 1.622960 ] ), ( 440, [ 0.348280, 0.023000, 1.747060 ] ), ( 445, [ 0.348060, 0.029800, 1.782600 ] ), ( 450, [ 0.336200, 0.038000, 1.772110 ] ), ( 455, [ 0.318700, 0.048000, 1.744100 ] ), ( 460, [ 0.290800, 0.060000, 1.669200 ] ), ( 465, [ 0.251100, 0.073900, 1.528100 ] ), ( 470, [ 0.195360, 0.090980, 1.287640 ] ), ( 475, [ 0.142100, 0.112600, 1.041900 ] ), ( 480, [ 0.095640, 0.139020, 0.812950 ] ), ( 485, [ 0.057950, 0.169300, 0.616200 ] ), ( 490, [ 0.032010, 0.208020, 0.465180 ] ), ( 495, [ 0.014700, 0.258600, 0.353300 ] ), ( 500, [ 0.004900, 0.323000, 0.272000 ] ), ( 505, [ 0.002400, 0.407300, 0.212300 ] ), ( 510, [ 0.009300, 0.503000, 0.158200 ] ), ( 515, [ 0.029100, 0.608200, 0.111700 ] ), ( 520, [ 0.063270, 0.710000, 0.078250 ] ), ( 525, [ 0.109600, 0.793200, 0.057250 ] ), ( 530, [ 0.165500, 0.862000, 0.042160 ] ), ( 535, [ 0.225750, 0.914850, 0.029840 ] ), ( 540, [ 0.290400, 0.954000, 0.020300 ] ), ( 545, [ 0.359700, 0.980300, 0.013400 ] ), ( 550, [ 0.433450, 0.994950, 0.008750 ] ), ( 555, [ 0.512050, 1.000000, 0.005750 ] ), ( 560, [ 0.594500, 0.995000, 0.003900 ] ), ( 565, [ 0.678400, 0.978600, 0.002750 ] ), ( 570, [ 0.762100, 0.952000, 0.002100 ] ), ( 575, [ 0.842500, 0.915400, 0.001800 ] ), ( 580, [ 0.916300, 0.870000, 0.001650 ] ), ( 585, [ 0.978600, 0.816300, 0.001400 ] ), ( 590, [ 1.026300, 0.757000, 0.001100 ] ), ( 595, [ 1.056700, 0.694900, 0.001000 ] ), ( 600, [ 1.062200, 0.631000, 0.000800 ] ), ( 605, [ 1.045600, 0.566800, 0.000600 ] ), ( 610, [ 1.002600, 0.503000, 0.000340 ] ), ( 615, [ 0.938400, 0.441200, 0.000240 ] ), ( 620, [ 0.854450, 0.381000, 0.000190 ] ), ( 625, [ 0.751400, 0.321000, 0.000100 ] ), ( 630, [ 0.642400, 0.265000, 0.000050 ] ), ( 635, [ 0.541900, 0.217000, 0.000030 ] ), ( 640, [ 0.447900, 0.175000, 0.000020 ] ), ( 645, [ 0.360800, 0.138200, 0.000010 ] ), ( 650, [ 0.283500, 0.107000, 0.000000 ] ), ( 655, [ 0.218700, 0.081600, 0.000000 ] ), ( 660, [ 0.164900, 0.061000, 0.000000 ] ), ( 665, [ 0.121200, 0.044580, 0.000000 ] ), ( 670, [ 0.087400, 0.032000, 0.000000 ] ), ( 675, [ 0.063600, 0.023200, 0.000000 ] ), ( 680, [ 0.046770, 0.017000, 0.000000 ] ), ( 685, [ 0.032900, 0.011920, 0.000000 ] ), ( 690, [ 0.022700, 0.008210, 0.000000 ] ), ( 695, [ 0.015840, 0.005723, 0.000000 ] ), ( 700, [ 0.011359, 0.004102, 0.000000 ] ), ( 705, [ 0.008111, 0.002929, 0.000000 ] ), ( 710, [ 0.005790, 0.002091, 0.000000 ] ), ( 715, [ 0.004109, 0.001484, 0.000000 ] ), ( 720, [ 0.002899, 0.001047, 0.000000 ] ), ( 725, [ 0.002049, 0.000740, 0.000000 ] ), ( 730, [ 0.001440, 0.000520, 0.000000 ] ), ( 735, [ 0.001000, 0.000361, 0.000000 ] ), ( 740, [ 0.000690, 0.000249, 0.000000 ] ), ( 745, [ 0.000476, 0.000172, 0.000000 ] ), ( 750, [ 0.000332, 0.000120, 0.000000 ] ), ( 755, [ 0.000235, 0.000085, 0.000000 ] ), ( 760, [ 0.000166, 0.000060, 0.000000 ] ), ( 765, [ 0.000117, 0.000042, 0.000000 ] ), ( 770, [ 0.000083, 0.000030, 0.000000 ] ), ( 775, [ 0.000059, 0.000021, 0.000000 ] ), ( 780, [ 0.000042, 0.000015, 0.000000 ] ), ]; pub fn temp_to_xyz(temp: f32) -> [f32; 3] { const C1: f64 = 3.7417717905326694e-16; const C2: f64 = 0.014387773457709927; let mut xyz = [0.0f64; 3]; for (wavelength, vals) in CIE_OBSERVERS.iter() { // Get the wavelength in meters let wavelength = (*wavelength as f64) / 1.0e9; let power = C1 / (wavelength.powi(5) * ((C2/((temp as f64)*wavelength)).exp()-1.0)); xyz[0] += power * vals[0]; xyz[1] += power * vals[1]; xyz[2] += power * vals[2]; } let max = xyz[0].max(xyz[1]).max(xyz[2]); [(xyz[0] / max) as f32, (xyz[1] / max) as f32, (xyz[2] / max) as f32] } pub fn xyz_to_temp(xyz: [f32; 3]) -> (f32, f32) { let (mut min, mut max) = (1000.0f32, 40000.0f32); let mut temp = 0.0; let mut new_xyz = [0.0; 3]; while (max - min) > 1.0 { temp = (max + min) / 2.0; new_xyz = temp_to_xyz(temp); if (new_xyz[2] / new_xyz[0]) > (xyz[2] / xyz[0]) { max = temp; } else { min = temp; } } let tint = (new_xyz[1]/new_xyz[0]) / (xyz[1]/xyz[0]); (temp, tint) } #[inline(always)] pub fn input8bit(v: u8) -> f32 { (v as f32) / 255.0 } #[inline(always)] pub fn input16bit(v: u16) -> f32 { (v as f32) / 65535.0 } #[inline(always)] pub fn output8bit(v: f32) -> u8 { (v * 256.0).max(0.0).min(255.0) as u8 } #[inline(always)] pub fn output16bit(v: f32) -> u16 { (v * 65535.0).round().max(0.0).min(65535.0) as u16 } #[cfg(test)] mod tests { use super::*; use image::{ImageBuffer, DynamicImage}; #[test] fn roundtrip_8bit() { for i in 0..u8::MAX { assert_eq!(i, output8bit(input8bit(i))); } } #[test] fn roundtrip_16bit() { for i in 0..u16::MAX { assert_eq!(i, output16bit(input16bit(i))); } } // Make sure all 8 bit values roundtrip when read as 16 bits from the image crate #[test] fn roundtrip_8bit_values_image_crate() { let mut image_data: Vec = vec![0; 10*10*3]; for v in 0..=u8::MAX { image_data.push(v); } let image = ImageBuffer::from_raw(10, 10, image_data.clone()).unwrap(); let out = DynamicImage::ImageRgb8(image).into_rgb16().into_raw(); for (vin, vout) in image_data.into_iter().zip(out.into_iter()) { let f = input16bit(vout); let out = output8bit(f); assert_eq!(out, vin); } } // Make sure all 16 bit values roundtrip when read as 16 bits from the image crate #[test] fn roundtrip_16bit_values_image_crate() { let mut image_data: Vec = vec![0; 148*148*3]; for v in 0..=u16::MAX { image_data.push(v); } let image = ImageBuffer::from_raw(10, 10, image_data.clone()).unwrap(); let out = DynamicImage::ImageRgb16(image).into_rgb16().into_raw(); for (vin, vout) in image_data.into_iter().zip(out.into_iter()) { let f = input16bit(vout); let out = output16bit(f); assert_eq!(out, vin); } } fn roundtrip_gamma(v: f32) -> f32 { let inter = expand_srgb_gamma(v); apply_srgb_gamma(inter) } #[test] fn roundtrip_8bit_gamma() { for i in 0..u8::MAX { assert_eq!(i, output8bit(roundtrip_gamma(input8bit(i)))); } } #[test] fn roundtrip_16bit_gamma() { for i in 0..u16::MAX { assert_eq!(i, output16bit(roundtrip_gamma(input16bit(i)))); } } use num_traits::ops::saturating::Saturating; use std::fmt::Debug; use std::cmp::PartialOrd; #[inline(always)] fn assert_offby(to: (T,T,T), from: (T,T,T), offdown: T, offup: T) where T: Saturating+Debug+PartialOrd+Copy { let condition = to.0 <= from.0.saturating_add(offup) && to.0 >= from.0.saturating_sub(offdown) && to.1 <= from.1.saturating_add(offup) && to.1 >= from.1.saturating_sub(offdown) && to.2 <= from.2.saturating_add(offup) && to.2 >= from.2.saturating_sub(offdown); if !condition { eprintln!("Got {:?} instead of {:?}", to, from); } assert!(condition) } #[test] fn roundtrip_8bit_lab_xyz() { for x in 0..u8::MAX { for y in 0..u8::MAX { for z in 0..u8::MAX { let xf = input8bit(x); let yf = input8bit(y); let zf = input8bit(z); let (l,a,b) = xyz_to_lab(xf,yf,zf); let (outxf,outyf,outzf) = lab_to_xyz(l,a,b); let outx = output8bit(outxf); let outy = output8bit(outyf); let outz = output8bit(outzf); assert_eq!((outx, outy, outz), (x, y, z)); } } } } #[test] fn roundtrip_8bit_lab_rgb() { for r in 0..u8::MAX { for g in 0..u8::MAX { for b in 0..u8::MAX { let pixel = [input8bit(r), input8bit(g), input8bit(b), 0.0]; let multipliers = [1.0,1.0,1.0,1.0]; let cmatrix = *SRGB_D65_43; let rgbmatrix = *XYZ_D65_33; let (ll,la,lb) = camera_to_lab(multipliers, cmatrix, &pixel); let (outrf,outgf,outbf) = lab_to_rgb(rgbmatrix, &[ll,la,lb]); let outr = output8bit(outrf); let outg = output8bit(outgf); let outb = output8bit(outbf); assert_eq!((outr, outg, outb), (r, g, b)); } } } } #[test] fn roundtrip_8bit_lab_rgb_gamma() { for r in 0..u8::MAX { for g in 0..u8::MAX { for b in 0..u8::MAX { let pixel = [ expand_srgb_gamma(input8bit(r)), expand_srgb_gamma(input8bit(g)), expand_srgb_gamma(input8bit(b)), 0.0 ]; let multipliers = [1.0,1.0,1.0,1.0]; let cmatrix = *SRGB_D65_43; let rgbmatrix = *XYZ_D65_33; let (ll,la,lb) = camera_to_lab(multipliers, cmatrix, &pixel); let (outrf,outgf,outbf) = lab_to_rgb(rgbmatrix, &[ll,la,lb]); let outrf = apply_srgb_gamma(outrf); let outgf = apply_srgb_gamma(outgf); let outbf = apply_srgb_gamma(outbf); let outr = output8bit(outrf); let outg = output8bit(outgf); let outb = output8bit(outbf); assert_eq!((outr, outg, outb), (r, g, b)); } } } } #[test] fn roundtrip_16bit_lab_xyz() { // step_by different primes to try and get coverage without being exaustive for x in (0..u16::MAX).step_by(89) { for y in (0..u16::MAX).step_by(97){ for z in (0..u16::MAX).step_by(101) { let xf = input16bit(x); let yf = input16bit(y); let zf = input16bit(z); let (l,a,b) = xyz_to_lab(xf,yf,zf); let (outxf,outyf,outzf) = lab_to_xyz(l,a,b); // test output 16 bit let outx = output16bit(outxf); let outy = output16bit(outyf); let outz = output16bit(outzf); assert_eq!((outx, outy, outz), (x, y, z)); // test output 8 bit let x = x >> 8; let y = y >> 8; let z = z >> 8; let outx = output8bit(outxf) as u16; let outy = output8bit(outyf) as u16; let outz = output8bit(outzf) as u16; assert_eq!((outx, outy, outz), (x, y, z)); } } } } #[test] fn roundtrip_16bit_lab_rgb() { for r in (0..u16::MAX).step_by(89) { for g in (0..u16::MAX).step_by(97){ for b in (0..u16::MAX).step_by(101) { let pixel = [input16bit(r), input16bit(g), input16bit(b), 0.0]; let multipliers = [1.0,1.0,1.0,1.0]; let cmatrix = *SRGB_D65_43; let rgbmatrix = *XYZ_D65_33; let (ll,la,lb) = camera_to_lab(multipliers, cmatrix, &pixel); let (outrf,outgf,outbf) = lab_to_rgb(rgbmatrix, &[ll,la,lb]); // test output 16 bit let outr = output16bit(outrf); let outg = output16bit(outgf); let outb = output16bit(outbf); assert_eq!((outr, outg, outb), (r, g, b)); // test output 8 bit let r = r >> 8; let g = g >> 8; let b = b >> 8; let outr = output8bit(outrf) as u16; let outg = output8bit(outgf) as u16; let outb = output8bit(outbf) as u16; assert_eq!((outr, outg, outb), (r, g, b)); } } } } #[test] fn roundtrip_16bit_lab_rgb_gamma() { for r in (0..u16::MAX).step_by(89) { for g in (0..u16::MAX).step_by(97){ for b in (0..u16::MAX).step_by(101) { let pixel = [ expand_srgb_gamma(input16bit(r)), expand_srgb_gamma(input16bit(g)), expand_srgb_gamma(input16bit(b)), 0.0 ]; let multipliers = [1.0,1.0,1.0,1.0]; let cmatrix = *SRGB_D65_43; let rgbmatrix = *XYZ_D65_33; let (ll,la,lb) = camera_to_lab(multipliers, cmatrix, &pixel); let ll = roundtrip_gamma(ll); let (outrf,outgf,outbf) = lab_to_rgb(rgbmatrix, &[ll,la,lb]); let outrf = apply_srgb_gamma(outrf); let outgf = apply_srgb_gamma(outgf); let outbf = apply_srgb_gamma(outbf); // test output 16 bit let outr = output16bit(outrf) as u16; let outg = output16bit(outgf) as u16; let outb = output16bit(outbf) as u16; // FIXME: 16bit full color conversion has off-by-one roundtrip only assert_offby((outr, outg, outb), (r, g, b), 1, 1); // test output 8 bit let r = r >> 8; let g = g >> 8; let b = b >> 8; let outr = output8bit(outrf) as u16; let outg = output8bit(outgf) as u16; let outb = output8bit(outbf) as u16; assert_eq!((outr, outg, outb), (r, g, b)); } } } } } imagepipe-0.5.0/src/hasher.rs000064400000000000000000000017260072674642500142330ustar 00000000000000extern crate blake3; extern crate bincode; extern crate serde; use self::serde::Serialize; use std; use std::io::Write; use std::fmt; use std::fmt::Debug; type HashType = self::blake3::Hasher; const HASHSIZE: usize = 32; pub type BufHash = [u8;HASHSIZE]; #[derive(Clone)] pub struct BufHasher { hash: HashType, } impl BufHasher { pub fn new() -> BufHasher { BufHasher { hash: HashType::new(), } } pub fn result(&self) -> BufHash { *self.hash.finalize().as_bytes() } } impl Debug for BufHasher { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "BufHasher {{ {:?} }}", self.result()) } } impl Write for BufHasher { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.hash.update(buf); Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> {Ok(())} } impl BufHasher { pub fn from_serialize(&mut self, obj: &T) where T: Serialize { self::bincode::serialize_into(self, obj).unwrap(); } } imagepipe-0.5.0/src/lib.rs000064400000000000000000000012750072674642500135260ustar 00000000000000#[macro_use] extern crate serde_derive; #[macro_use] extern crate lazy_static; #[macro_use] extern crate log; extern crate rawloader; extern crate image; mod buffer; mod hasher; mod ops; pub use ops::transform::Rotation; mod opbasics; mod pipeline; pub use self::pipeline::*; pub use self::ops::*; pub mod color_conversions; mod scaling; pub use self::ops::curves::SplineFunc; use std::path::Path; pub fn simple_decode_8bit>(img: P, maxwidth: usize, maxheight: usize) -> Result { let mut pipeline = Pipeline::new_from_file(&img)?; pipeline.globals.settings.maxwidth = maxwidth; pipeline.globals.settings.maxheight = maxheight; pipeline.output_8bit(None) } imagepipe-0.5.0/src/opbasics.rs000064400000000000000000000003360072674642500145600ustar 00000000000000pub use crate::buffer::*; pub use crate::pipeline::*; pub use crate::hasher::*; pub use crate::color_conversions::*; pub use rawloader::{RawImage, CFA, Orientation, RawImageData}; pub use std::sync::Arc; pub use std::cmp; imagepipe-0.5.0/src/ops/colorspaces.rs000064400000000000000000000066240072674642500161010ustar 00000000000000use crate::opbasics::*; use crate::color_conversions::*; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct OpToLab { pub cam_to_xyz: [[f32;4];3], pub cam_to_xyz_normalized: [[f32;4];3], pub xyz_to_cam: [[f32;3];4], pub wb_coeffs: [f32;4], } fn normalize_wbs(vals: [f32;4]) -> [f32;4] { // Set green multiplier as 1.0 let unity: f32 = vals[1]; macro_rules! norm { ($val:expr) => { if !$val.is_normal() { 1.0 } else { $val / unity } }; } [norm!(vals[0]), norm!(vals[1]), norm!(vals[2]), norm!(vals[3])] } impl OpToLab { pub fn new(img: &ImageSource) -> OpToLab { match img { ImageSource::Raw(img) => { let coeffs = if !img.wb_coeffs[0].is_normal() || !img.wb_coeffs[1].is_normal() || !img.wb_coeffs[2].is_normal() { normalize_wbs(img.neutralwb()) } else { normalize_wbs(img.wb_coeffs) }; OpToLab{ cam_to_xyz: img.cam_to_xyz(), cam_to_xyz_normalized: img.cam_to_xyz_normalized(), xyz_to_cam: img.xyz_to_cam, wb_coeffs: coeffs, } }, ImageSource::Other(_) => { OpToLab{ cam_to_xyz: *SRGB_D65_43, cam_to_xyz_normalized: *SRGB_D65_43, xyz_to_cam: *XYZ_D65_34, wb_coeffs: [1.0, 1.0, 1.0, 0.0], } } } } pub fn set_temp(&mut self, temp: f32, tint: f32) { let xyz = temp_to_xyz(temp); let xyz = [xyz[0], xyz[1]/tint, xyz[2]]; for i in 0..4 { self.wb_coeffs[i] = 0.0; for j in 0..3 { self.wb_coeffs[i] += self.xyz_to_cam[i][j] * xyz[j]; } self.wb_coeffs[i] = self.wb_coeffs[i].recip(); } self.wb_coeffs = normalize_wbs(self.wb_coeffs); } pub fn get_temp(&self) -> (f32, f32) { let mut xyz = [0.0; 3]; for i in 0..3 { for j in 0..4 { let mul = self.wb_coeffs[j]; if mul > 0.0 { xyz[i] += self.cam_to_xyz[i][j] / mul; } } } let (temp, tint) = xyz_to_temp(xyz); (temp, tint) } } impl<'a> ImageOp<'a> for OpToLab { fn name(&self) -> &str {"to_lab"} fn run(&self, _pipeline: &PipelineGlobals, buf: Arc) -> Arc { let cmatrix = if buf.monochrome { // Monochrome means we don't need color conversion so it's as if the camera is itself D65 SRGB *SRGB_D65_43 } else { self.cam_to_xyz_normalized }; let mul = if buf.monochrome { [1.0, 1.0, 1.0, 1.0] } else { normalize_wbs(self.wb_coeffs) }; Arc::new(buf.process_into_new(3, &(|outb: &mut [f32], inb: &[f32]| { for (pixin, pixout) in inb.chunks_exact(4).zip(outb.chunks_exact_mut(3)) { let (l,a,b) = camera_to_lab(mul, cmatrix, pixin); pixout[0] = l; pixout[1] = a; pixout[2] = b; } }))) } } #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct OpFromLab { } impl OpFromLab { pub fn new(_img: &ImageSource) -> OpFromLab { OpFromLab{} } } impl<'a> ImageOp<'a> for OpFromLab { fn name(&self) -> &str {"from_lab"} fn run(&self, _pipeline: &PipelineGlobals, buf: Arc) -> Arc { Arc::new(buf.mutate_lines_copying(&(|line: &mut [f32], _| { for pix in line.chunks_exact_mut(3) { let (r,g,b) = lab_to_rgb(*XYZ_D65_33, pix); pix[0] = r; pix[1] = g; pix[2] = b; } }))) } } imagepipe-0.5.0/src/ops/curves.rs000064400000000000000000000112340072674642500150640ustar 00000000000000use crate::opbasics::*; use std::cmp; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OpBaseCurve { pub exposure: f32, pub points: Vec<(f32, f32)>, } impl OpBaseCurve { pub fn new(img: &ImageSource) -> OpBaseCurve { match img { ImageSource::Raw(_) => { OpBaseCurve{ exposure: 0.0, // Slopes the curve to go from the linear raw to a more natural look points: vec![(0.50, 0.60)], } }, ImageSource::Other(_) => { OpBaseCurve{ exposure: 0.0, points: vec![], } } } } } impl<'a> ImageOp<'a> for OpBaseCurve { fn name(&self) -> &str {"basecurve"} fn run(&self, _pipeline: &PipelineGlobals, buf: Arc) -> Arc { if self.points.len() == 0 && self.exposure.abs() < 0.001 { return buf } let mut final_points = self.points.clone(); for (_, to) in final_points.iter_mut() { *to = *to * self.exposure.exp2(); } let func = SplineFunc::new(&final_points); Arc::new(buf.mutate_lines_copying(&(|line: &mut [f32], _| { for pix in line.chunks_exact_mut(3) { pix[0] = func.interpolate(pix[0]); } }))) } } impl OpBaseCurve { pub fn get_spline(&self) -> SplineFunc { SplineFunc::new(&self.points) } } pub struct SplineFunc { points: Vec<(f32,f32)>, c1s: Vec, c2s: Vec, c3s: Vec, } impl SplineFunc { // Monotone cubic interpolation code adapted from the Javascript example in Wikipedia pub fn new(p: &[(f32,f32)]) -> SplineFunc { let mut points = Vec::new(); if p.len() == 0 || (p[0].0 > 0.0 && p[0].1 > 0.0) { points.push((0.0, 0.0)); } points.extend_from_slice(p); if p.len() == 0 || (p[p.len()-1].0 < 1.0 && p[p.len()-1].1 < 1.0) { points.push((1.0, 1.0)); } // Get consecutive differences and slopes let mut dxs = Vec::new(); let mut dys = Vec::new(); let mut slopes = Vec::new(); for i in 0..(points.len()-1) { let dx = points[i+1].0 - points[i].0; let dy = points[i+1].1 - points[i].1; dxs.push(dx); dys.push(dy); slopes.push(dy/dx); } // Get degree-1 coefficients let mut c1s = vec![slopes[0]]; for i in 0..(dxs.len()-1) { let m = slopes[i]; let next = slopes[i+1]; if m*next <= 0.0 { c1s.push(0.0); } else { let dx = dxs[i]; let dxnext = dxs[i+1]; let common = dx + dxnext; c1s.push(3.0*common/((common+dxnext)/m + (common + dx)/next)); } } c1s.push(slopes[slopes.len()-1]); // Get degree-2 and degree-3 coefficients let mut c2s = Vec::new(); let mut c3s = Vec::new(); for i in 0..(c1s.len()-1) { let c1 = c1s[i]; let slope = slopes[i]; let invdx = 1.0 / dxs[i]; let common = c1+c1s[i+1] - slope - slope; c2s.push((slope-c1-common)*invdx); c3s.push(common*invdx*invdx); } SplineFunc { points: points, c1s: c1s, c2s: c2s, c3s: c3s, } } pub fn interpolate(&self, val: f32) -> f32 { // Anything at or beyond the last value returns the last value let end = self.points[self.points.len()-1].0; if val >= end { return self.points[self.points.len()-1].1; } // Anything at or under the first value returns the first value let first = self.points[0].0; if val <= first { return self.points[0].1; } // Search for the interval x is in, returning the corresponding y if x is one of the original xs let mut low: isize = 0; let mut mid: isize; let mut high: isize = (self.c3s.len() - 1) as isize; while low <= high { mid = (low+high)/2; let xhere = self.points[mid as usize].0; if xhere < val { low = mid + 1; } else if xhere > val { high = mid - 1; } else { return self.points[mid as usize].1 } } let i = cmp::max(0, high) as usize; // Interpolate let diff = val - self.points[i].0; self.points[i].1 + self.c1s[i]*diff + self.c2s[i]*diff*diff + self.c3s[i]*diff*diff*diff } } #[cfg(test)] mod tests { use super::*; #[test] fn extremes() { let spline = SplineFunc::new(&[]); assert_eq!(spline.interpolate(0.0), 0.0); assert_eq!(spline.interpolate(1.0), 1.0); } #[test] fn saturates() { let spline = SplineFunc::new(&[]); assert_eq!(spline.interpolate(1.5), 1.0); assert_eq!(spline.interpolate(-0.2), 0.0); } #[test] fn high_blackpoint() { let spline = SplineFunc::new(&[(0.0,0.2)]); assert_eq!(spline.interpolate(0.0), 0.2); } #[test] fn low_whitepoint() { let spline = SplineFunc::new(&[(1.0,0.8)]); assert_eq!(spline.interpolate(1.0), 0.8); } } imagepipe-0.5.0/src/ops/demosaic.rs000064400000000000000000000074110072674642500153430ustar 00000000000000use crate::opbasics::*; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OpDemosaic { pub cfa: String, } impl OpDemosaic { pub fn new(img: &ImageSource) -> OpDemosaic { match img { ImageSource::Raw(img) => { OpDemosaic{ cfa: img.cropped_cfa().to_string(), } }, ImageSource::Other(_) => { OpDemosaic{ cfa: "".to_string(), } } } } } impl<'a> ImageOp<'a> for OpDemosaic { fn name(&self) -> &str {"demosaic"} fn run(&self, pipeline: &PipelineGlobals, buf: Arc) -> Arc { let nwidth = pipeline.settings.demosaic_width; let nheight = pipeline.settings.demosaic_height; let scale = crate::scaling::calculate_scale(buf.width, buf.height, nwidth, nheight); let cfa = CFA::new(&self.cfa); let minscale = match cfa.width { 2 => 2.0, // RGGB/RGBE bayer 6 => 3.0, // x-trans is 6 wide but has all colors in every 3x3 block 8 => 2.0, // Canon pro 70 has a 8x2 patern that has all four colors every 2x2 block 12 => 12.0, // some crazy sensor I haven't actually encountered, use full block _ => 2.0, // default }; if scale <= 1.0 && buf.colors == 4 { // We want full size and the image is already 4 color, pass it through buf } else if buf.colors == 4 { // Scale down a 4 colour image Arc::new(crate::scaling::scale_down_opbuf(&buf, nwidth, nheight)) } else if scale >= minscale { // We're scaling down enough that each pixel has all four colors under it so do the // demosaic and scale down in one go Arc::new(crate::scaling::scaled_demosaic(cfa, &buf, nwidth, nheight)) } else { // We're in a close to full scale output that needs full demosaic and possibly // minimal scale down let fullsize = full(cfa, &buf); if scale > 1.0 { Arc::new(crate::scaling::scale_down_opbuf(&fullsize, nwidth, nheight)) } else { Arc::new(fullsize) } } } // We don't transform_reverse as image sizing is relative to the scaling done // at the demosaic step, so whatever scale down is needed can be achieved here } pub fn full(cfa: CFA, buf: &OpBuffer) -> OpBuffer { let mut out = OpBuffer::new(buf.width, buf.height, 4, buf.monochrome); let offsets3x3: [(isize,isize);9] = [ (-1,-1), (-1, 0), (-1, 1), ( 0,-1), ( 0, 0), ( 0, 1), ( 1,-1), ( 1, 0), ( 1, 1), ]; // Initialize a lookup table for the colors of each pixel in a 3x3 grid let mut lookups = [[[0;9];48];48]; for (row, line) in lookups.iter_mut().enumerate() { for (col, colors) in line.iter_mut().enumerate() { let pixcolor = cfa.color_at(row, col); for (i, o) in offsets3x3.iter().enumerate() { let (dy, dx) = *o; let row = (48+dy) as usize + row; let col = (48+dx) as usize + col; let ocolor = cfa.color_at(row, col); colors[i] = if ocolor != pixcolor || (dx == 0 && dy == 0) { ocolor } else { 4 }; } } } // Now calculate RGBE for each pixel based on the lookup table out.mutate_lines(&(|line: &mut [f32], row| { for (col, pix) in line.chunks_exact_mut(4).enumerate() { let ref colors = lookups[row%48][col%48]; let mut sums = [0f32;5]; let mut counts = [0f32;5]; for (i, o) in offsets3x3.iter().enumerate() { let (dy, dx) = *o; let row = row as isize + dy; let col = col as isize + dx; if row >= 0 && row < (buf.height as isize) && col >= 0 && col < (buf.width as isize) { sums[colors[i]] += buf.data[(row as usize)*buf.width+(col as usize)]; counts[colors[i]] += 1.0; } } for c in 0..4 { if counts[c] > 0.0 { pix[c] = sums[c] / counts[c]; } } } })); out } imagepipe-0.5.0/src/ops/gamma.rs000064400000000000000000000012000072674642500146270ustar 00000000000000use crate::opbasics::*; use crate::color_conversions::*; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct OpGamma { } impl<'a> OpGamma { pub fn new(_img: &ImageSource) -> OpGamma { OpGamma{} } } impl<'a> ImageOp<'a> for OpGamma { fn name(&self) -> &str {"gamma"} fn run(&self, pipeline: &PipelineGlobals, buf: Arc) -> Arc { if pipeline.settings.linear { buf } else { Arc::new(buf.mutate_lines_copying(&(|line: &mut [f32], _| { for pix in line.chunks_exact_mut(1) { pix[0] = apply_srgb_gamma(pix[0].max(0.0).min(1.0)); } }))) } } } imagepipe-0.5.0/src/ops/gofloat.rs000064400000000000000000000157730072674642500152240ustar 00000000000000use crate::opbasics::*; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct OpGoFloat { pub crop_top: usize, pub crop_right: usize, pub crop_bottom: usize, pub crop_left: usize, pub is_cfa: bool, pub blacklevels: [f32;4], pub whitelevels: [f32;4], } fn from_int4(arr: [u16;4]) -> [f32;4] { [arr[0] as f32, arr[1] as f32, arr[2] as f32, arr[3] as f32] } impl OpGoFloat { pub fn new(img: &ImageSource) -> OpGoFloat { match img { ImageSource::Raw(img) => { // Calculate the resulting width/height and top-left corner after crops OpGoFloat{ crop_top: img.crops[0], crop_right: img.crops[1], crop_bottom: img.crops[2], crop_left: img.crops[3], is_cfa: img.cfa.is_valid(), blacklevels: from_int4(img.blacklevels), whitelevels: from_int4(img.whitelevels), } }, ImageSource::Other(_) => { OpGoFloat{ crop_top: 0, crop_right: 0, crop_bottom: 0, crop_left: 0, is_cfa: false, blacklevels: [0.0, 0.0, 0.0, 0.0], // Unused whitelevels: [0.0, 0.0, 0.0, 0.0], // Unused } } } } } impl<'a> ImageOp<'a> for OpGoFloat { fn name(&self) -> &str {"gofloat"} fn run(&self, pipeline: &PipelineGlobals, _buf: Arc) -> Arc { // FIXME: Doing all the transforms with lookup tables instead of f32 calcs // on every pixel is much faster match &pipeline.image { ImageSource::Raw(img) => { self.run_raw(img) }, ImageSource::Other(img) => { self.run_other(img) } } } fn transform_forward(&mut self, width: usize, height: usize) -> (usize, usize) { let (_, _, width, height) = self.size_image(width, height); (width, height) } // We don't transform_reverse as image sizing is relative to the scaling done // at the demosaic step } impl OpGoFloat { fn size_image(&self, owidth: usize, oheight: usize) -> (usize, usize, usize, usize) { // Calculate x/y/width/height making sure we get at least a 10x10 "image" to not trip up // reasonable assumptions in later ops let x = cmp::min(self.crop_left, owidth-10); let y = cmp::min(self.crop_top, oheight-10); let width = owidth - cmp::min(self.crop_left + self.crop_right, owidth-10); let height = oheight - cmp::min(self.crop_top + self.crop_bottom, oheight-10); (x, y, width, height) } fn run_raw(&self, img: &RawImage) -> Arc { // Calculate the levels let mins = self.blacklevels; let ranges = self.whitelevels.iter().enumerate().map(|(i, &x)| { x - mins[i] }).collect::>(); let owidth = img.width; let oheight = img.height; let (x, y, width, height) = self.size_image(owidth, oheight); Arc::new(match img.data { RawImageData::Integer(ref data) => { if img.cpp == 1 && !self.is_cfa { // We're in a monochrome image so turn it into RGB let mut out = OpBuffer::new(width, height, 4, true); out.mutate_lines(&(|line: &mut [f32], row| { for (o, i) in line.chunks_exact_mut(4).zip(data[owidth*(row+y)+x..].chunks_exact(1)) { let val = ((i[0] as f32 - mins[0]) / ranges[0]).min(1.0); o[0] = val; o[1] = val; o[2] = val; o[3] = 0.0; } })); out } else if img.cpp == 3 { // We're in an RGB image, turn it into four channel let mut out = OpBuffer::new(width, height, 4, false); out.mutate_lines(&(|line: &mut [f32], row| { for (o, i) in line.chunks_exact_mut(4).zip(data[(owidth*(row+y)+x)*3..].chunks_exact(3)) { o[0] = ((i[0] as f32 - mins[0]) / ranges[0]).min(1.0); o[1] = ((i[1] as f32 - mins[1]) / ranges[1]).min(1.0); o[2] = ((i[2] as f32 - mins[2]) / ranges[2]).min(1.0); o[3] = 0.0; } })); out } else { let mut out = OpBuffer::new(width, height, img.cpp, false); out.mutate_lines(&(|line: &mut [f32], row| { for (o, i) in line.chunks_exact_mut(1).zip(data[owidth*(row+y)+x..].chunks_exact(1)) { o[0] = ((i[0] as f32 - mins[0]) / ranges[0]).min(1.0); } })); out } }, RawImageData::Float(ref data) => { if img.cpp == 1 && !self.is_cfa { // We're in a monochrome image so turn it into RGB let mut out = OpBuffer::new(width, height, 4, true); out.mutate_lines(&(|line: &mut [f32], row| { for (o, i) in line.chunks_exact_mut(4).zip(data[owidth*(row+y)+x..].chunks_exact(1)) { let val = ((i[0] as f32 - mins[0]) / ranges[0]).min(1.0); o[0] = val; o[1] = val; o[2] = val; o[3] = 0.0; } })); out } else if img.cpp == 3 { // We're in an RGB image, turn it into four channel let mut out = OpBuffer::new(width, height, 4, false); out.mutate_lines(&(|line: &mut [f32], row| { for (o, i) in line.chunks_exact_mut(4).zip(data[(owidth*(row+y)+x)*3..].chunks_exact(3)) { o[0] = ((i[0] as f32 - mins[0]) / ranges[0]).min(1.0); o[1] = ((i[1] as f32 - mins[1]) / ranges[1]).min(1.0); o[2] = ((i[2] as f32 - mins[2]) / ranges[2]).min(1.0); o[3] = 0.0; } })); out } else { let mut out = OpBuffer::new(width, height, img.cpp, false); out.mutate_lines(&(|line: &mut [f32], row| { for (o, i) in line.chunks_exact_mut(1).zip(data[owidth*(row+y)+x..].chunks_exact(1)) { o[0] = ((i[0] as f32 - mins[0]) / ranges[0]).min(1.0); } })); out } }, }) } fn run_other(&self, img: &OtherImage) -> Arc { let owidth = img.width() as usize; let oheight = img.height() as usize; let (x, y, width, height) = self.size_image(owidth, oheight); let mut out = OpBuffer::new(width, height, 4, false); let bits_per_channel = img.color().bits_per_pixel() / img.color().channel_count() as u16; if bits_per_channel == 8 { let data = img.to_rgb8().into_raw(); out.mutate_lines(&(|line: &mut [f32], row| { for (o, i) in line.chunks_exact_mut(4).zip(data[(owidth*(row+y)+x)*3..].chunks_exact(3)) { o[0] = expand_srgb_gamma(input8bit(i[0])); o[1] = expand_srgb_gamma(input8bit(i[1])); o[2] = expand_srgb_gamma(input8bit(i[2])); o[3] = 0.0; } })); } else { let data = img.to_rgb16().into_raw(); out.mutate_lines(&(|line: &mut [f32], row| { for (o, i) in line.chunks_exact_mut(4).zip(data[(owidth*(row+y)+x)*3..].chunks_exact(3)) { o[0] = input16bit(i[0]); o[1] = input16bit(i[1]); o[2] = input16bit(i[2]); o[3] = 0.0; } })); } Arc::new(out) } } imagepipe-0.5.0/src/ops/mod.rs000064400000000000000000000001760072674642500143370ustar 00000000000000pub mod gofloat; pub mod demosaic; pub mod colorspaces; pub mod curves; pub mod gamma; pub mod transform; pub mod rotatecrop; imagepipe-0.5.0/src/ops/rotatecrop.rs000064400000000000000000000231410072674642500157370ustar 00000000000000use crate::opbasics::*; use std::f32::consts::FRAC_PI_2; // Crops that are less than 1 pixel in a million are treated as no-ops // Transforms that need more than 1:million magnification are broken and are // thus also treated as no-ops static EPSILON: f32 = 1.0 / 1000000.0; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct OpRotateCrop { pub crop_top: f32, pub crop_right: f32, pub crop_bottom: f32, pub crop_left: f32, pub rotation: f32, input_ratio: f32, output_size: Option<(usize, usize)>, } impl OpRotateCrop { pub fn new(_img: &ImageSource) -> Self { Self::empty() } pub fn empty() -> Self { Self { crop_top: 0.0, crop_right: 0.0, crop_bottom: 0.0, crop_left: 0.0, rotation: 0.0, input_ratio: 1.0, output_size: None, } } } impl<'a> ImageOp<'a> for OpRotateCrop { fn name(&self) -> &str {"rotatecrop"} fn run(&self, _pipeline: &PipelineGlobals, buf: Arc) -> Arc { if self.noop() { return buf; } // Calculate source and destination sizes let (swidth, sheight) = (buf.width as f32, buf.height as f32); let (nwidth, nheight) = self.calc_size(buf.width, buf.height, false); let (fnwidth, fnheight) = (nwidth as f32, nheight as f32); // Figure out x and y let x = (swidth * self.crop_left).floor(); if x < 0.0 || x > swidth { log::error!("Trying to crop left outside image"); return buf; } let y = (sheight * self.crop_top).floor(); if y < 0.0 || y > sheight { log::error!("Trying to crop top outside image"); return buf; } let topleft = self.rotate_point_reverse(x, y, fnwidth, fnheight, swidth, sheight); let topright = self.rotate_point_reverse(x + fnwidth - 1.0, y, fnwidth, fnheight, swidth, sheight); let bottomleft = self.rotate_point_reverse(x, y + fnheight - 1.0, fnwidth, fnheight, swidth, sheight); let newbuffer = buf.transform(topleft, topright, bottomleft, nwidth, nheight); Arc::new(newbuffer) } fn transform_forward(&mut self, width: usize, height: usize) -> (usize, usize) { if let Some(size) = self.output_size { // We're going forward after going reverse so we're commited to an output size size } else { self.input_ratio = width as f32 / height as f32; self.calc_size(width, height, false) } } fn transform_reverse(&mut self, width: usize, height: usize) -> (usize, usize) { // Save the output size we're now commited to self.output_size = Some((width, height)); self.calc_size(width, height, true) } fn reset(&mut self) { self.input_ratio = 1.0; self.output_size = None; } } impl OpRotateCrop { fn noop(&self) -> bool { self.rotation.abs() < EPSILON && self.crop_top.abs() < EPSILON && self.crop_right.abs() < EPSILON && self.crop_bottom.abs() < EPSILON && self.crop_left.abs() < EPSILON } fn rotate_point_reverse(&self, x: f32, y: f32, width: f32, height: f32, swidth: f32, sheight: f32) -> (isize, isize) { if self.rotation < EPSILON { (x as isize, y as isize) } else { let angle = FRAC_PI_2 * if self.rotation > 1.0 {1.0} else {self.rotation}; let (sin, cos) = angle.sin_cos(); // Translate the coordinates to the center let (tx, ty) = (x - (width / 2.0), y - (height / 2.0)); let nx = tx*cos + ty*sin + (swidth / 2.0); let ny = - tx*sin + ty*cos + (sheight / 2.0); (nx as isize, ny as isize) } } fn calc_size(&self, owidth: usize, oheight: usize, reverse: bool) -> (usize, usize){ if self.noop() { return (owidth, oheight); } let (width, height) = (owidth as f32, oheight as f32); let (width, height) = if reverse || self.rotation < EPSILON { (width, height) } else { let angle = FRAC_PI_2 * if self.rotation > 1.0 {1.0} else {self.rotation}; let (sin, cos) = angle.sin_cos(); (width*cos + height*sin, width*sin + height*cos) }; let nwidth = { let ratio = 1.0 - self.crop_left - self.crop_right; let nwidth = if reverse { (width / ratio).round() } else { (width * ratio).round() }; if ratio < EPSILON || nwidth < 1.0 { log::error!("Trying to crop width beyond limits"); return (owidth, oheight); } nwidth }; let nheight = { let ratio = 1.0 - self.crop_top - self.crop_bottom; let nheight = if reverse { (height / ratio).round() } else { (height * ratio).round() }; if ratio < EPSILON || nheight < 1.0 { log::error!("Trying to crop height beyond limits"); return (owidth, oheight); } nheight }; let (nwidth, nheight) = if !reverse || self.rotation < EPSILON { (nwidth, nheight) } else { let angle = FRAC_PI_2 * if self.rotation > 1.0 {1.0} else {self.rotation}; let (sin, cos) = angle.sin_cos(); let width = (nheight / (sin + (cos/self.input_ratio))).round(); let height = (width / self.input_ratio).round(); (width, height) }; (nwidth as usize, nheight as usize) } } #[cfg(test)] mod tests { use crate::buffer::OpBuffer; use super::*; fn setup() -> (Arc, OpRotateCrop, PipelineGlobals) { let mut buffer = OpBuffer::new(100, 100, 3, false); for (i, o) in buffer.data.chunks_exact_mut(1).enumerate() { o[0] = i as f32; } let op = OpRotateCrop::empty(); (Arc::new(buffer), op, PipelineGlobals::mock(100, 100)) } #[test] fn crop_top() { let (buffer, mut op, globals) = setup(); op.crop_top = 0.1; let newbuf = op.run(&globals, buffer.clone()); assert_eq!(newbuf.height, 90); assert_eq!(newbuf.width, 100); assert_eq!(&newbuf.data[0], &buffer.data[100*10*3]); } #[test] fn crop_bottom() { let (buffer, mut op, globals) = setup(); op.crop_bottom = 0.1; let newbuf = op.run(&globals, buffer.clone()); assert_eq!(newbuf.height, 90); assert_eq!(newbuf.width, 100); assert_eq!(&newbuf.data[0], &buffer.data[0]); } #[test] fn crop_vertical() { let (buffer, mut op, globals) = setup(); op.crop_top = 0.1; op.crop_bottom = 0.1; let newbuf = op.run(&globals, buffer.clone()); assert_eq!(newbuf.height, 80); assert_eq!(newbuf.width, 100); assert_eq!(&newbuf.data[0], &buffer.data[100*10*3]); } #[test] fn crop_left() { let (buffer, mut op, globals) = setup(); op.crop_left = 0.1; let newbuf = op.run(&globals, buffer.clone()); assert_eq!(newbuf.height, 100); assert_eq!(newbuf.width, 90); assert_eq!(&newbuf.data[0], &buffer.data[10*3]); } #[test] fn crop_right() { let (buffer, mut op, globals) = setup(); op.crop_right = 0.1; let newbuf = op.run(&globals, buffer.clone()); assert_eq!(newbuf.height, 100); assert_eq!(newbuf.width, 90); assert_eq!(&newbuf.data[0], &buffer.data[0]); } #[test] fn crop_horizontal() { let (buffer, mut op, globals) = setup(); op.crop_left = 0.1; op.crop_right = 0.1; let newbuf = op.run(&globals, buffer.clone()); assert_eq!(newbuf.height, 100); assert_eq!(newbuf.width, 80); assert_eq!(&newbuf.data[0], &buffer.data[10*3]); } #[test] fn crop_horizontal_and_vertical() { let (buffer, mut op, globals) = setup(); op.crop_left = 0.1; op.crop_right = 0.1; op.crop_top = 0.1; op.crop_bottom = 0.1; let newbuf = op.run(&globals, buffer.clone()); assert_eq!(newbuf.height, 80); assert_eq!(newbuf.width, 80); assert_eq!(&newbuf.data[0], &buffer.data[100*10*3+10*3]); } #[test] fn rotate_45() { let (buffer, mut op, globals) = setup(); op.rotation = 0.5; let newbuf = op.run(&globals, buffer.clone()); assert_eq!(newbuf.height, 141); assert_eq!(newbuf.width, 141); } #[test] fn rotate_90() { let (buffer, mut op, globals) = setup(); op.rotation = 1.0; let newbuf = op.run(&globals, buffer.clone()); assert_eq!(newbuf.height, 100); assert_eq!(newbuf.width, 100); } #[test] fn roundtrip_transform() { let mut op = OpRotateCrop::empty(); for dim in (0..10000).step_by(89) { for crop1 in (0..u16::MAX).step_by(97) { for crop2 in (0..u16::MAX).step_by(101) { op.crop_top = input16bit(crop1); op.crop_right = input16bit(crop1); op.crop_bottom = input16bit(crop2); op.crop_left = input16bit(crop2); let (width, height) = (dim as usize, dim as usize); // First reverse and then direct to make sure that if we promised we could // make a given output from a given input we then follow through exactly let intermediate = op.transform_reverse(width, height); let result = op.transform_forward(intermediate.0, intermediate.1); let from = (width, height); assert_eq!(result, from, "Got {:?}->{:?}->{:?} crops ({:.3}/{:.3}/{:.3}/{:.3})", from, intermediate, result, op.crop_top, op.crop_right, op.crop_bottom, op.crop_left); } } } } #[test] fn roundtrip_transform_rotation() { let mut op = OpRotateCrop::empty(); for width in (0..10000).step_by(89) { for height in (0..10000).step_by(97) { for rotation in 0..u8::MAX { op.rotation = input8bit(rotation); let from = (width, height); let inter1 = op.transform_forward(from.0, from.1); let inter2 = op.transform_reverse(inter1.0, inter1.1); let result = op.transform_forward(inter2.0, inter2.1); assert_eq!(result, inter1, "Got {:?}->{:?}->{:?}->{:?} crops ({:.3}/{:.3}/{:.3}/{:.3})", from, inter1, inter2, result, op.crop_top, op.crop_right, op.crop_bottom, op.crop_left); } } } } } imagepipe-0.5.0/src/ops/transform.rs000064400000000000000000000161400072674642500155710ustar 00000000000000use crate::opbasics::*; use std::mem; use std::usize; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub enum Rotation { Normal, Rotate90, Rotate180, Rotate270, } #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct OpTransform { pub rotation: Rotation, pub fliph: bool, pub flipv: bool, } impl OpTransform { pub fn new(img: &ImageSource) -> OpTransform { match img { ImageSource::Raw(img) => { let (rotation, fliph, flipv) = match img.orientation { Orientation::Normal | Orientation::Unknown => (Rotation::Normal, false, false), Orientation::VerticalFlip => (Rotation::Normal, false, true), Orientation::HorizontalFlip => (Rotation::Normal, true, false), Orientation::Rotate180 => (Rotation::Rotate180, false, false), Orientation::Transpose => (Rotation::Rotate90, false, true), Orientation::Rotate90 => (Rotation::Rotate90, false, false), Orientation::Rotate270 => (Rotation::Rotate270, false, false), Orientation::Transverse => (Rotation::Rotate270, true, false), }; OpTransform{ rotation, fliph, flipv, } }, ImageSource::Other(_) => { OpTransform{ rotation: Rotation::Normal, fliph: false, flipv: false, } } } } } impl<'a> ImageOp<'a> for OpTransform { fn name(&self) -> &str {"transform"} fn run(&self, _pipeline: &PipelineGlobals, buf: Arc) -> Arc { // Grab back a base orientation let (f1, f2, f3) = match self.rotation { Rotation::Normal => Orientation::Normal, Rotation::Rotate90 => Orientation::Rotate90, Rotation::Rotate180 => Orientation::Rotate180, Rotation::Rotate270 => Orientation::Rotate270, }.to_flips(); // Adjust it with the vertical and horizontal flips if that applies let orientation = Orientation::from_flips((f1, f2 ^ self.fliph, f3 ^ self.flipv)); if orientation == Orientation::Normal || orientation == Orientation::Unknown { buf } else { Arc::new(rotate_buffer(&buf, &orientation)) } } fn transform_forward(&mut self, width: usize, height: usize) -> (usize, usize) { match self.rotation { Rotation::Rotate90 | Rotation::Rotate270 => (height, width), _ => (width, height), } } fn transform_reverse(&mut self, width: usize, height: usize) -> (usize, usize) { self.transform_forward(width, height) } } fn rotate_buffer(buf: &OpBuffer, orientation: &Orientation) -> OpBuffer { assert_eq!(buf.colors, 3); // When we're rotating we're always at 3 cpp // Don't rotate things we don't know how to rotate or don't need to if *orientation == Orientation::Normal || *orientation == Orientation::Unknown { return buf.clone(); } // Since we are using isize when calculating values for the rotation its // indices must be addressable by an isize as well if buf.data.len() >= usize::MAX / 2 { panic!("Buffer is too wide or high to rotate"); } // We extract buffers parameters early since all math is done with isize. // This avoids verbose casts later on let mut width = buf.width as isize; let mut height = buf.height as isize; let (transpose, flip_x, flip_y) = orientation.to_flips(); let mut base_offset: isize = 0; let mut x_step: isize = 3; let mut y_step: isize = width * 3; if flip_x { x_step = -x_step; base_offset += (width - 1) * 3; } if flip_y { y_step = -y_step; base_offset += width * (height - 1) * 3; } let mut out = if transpose { mem::swap(&mut width, &mut height); mem::swap(&mut x_step, &mut y_step); OpBuffer::new(buf.height, buf.width, 3 as usize, buf.monochrome) } else { OpBuffer::new(buf.width, buf.height, 3 as usize, buf.monochrome) }; out.mutate_lines(&(|line: &mut [f32], row| { // Calculate the current line's offset in original buffer. When transposing // this is the current column's offset in the original buffer let line_offset = base_offset + y_step * row as isize; for col in 0..width { // The current pixel's offset in original buffer let offset = line_offset + x_step * col; for c in 0..3 { line[(col * 3 + c) as usize] = buf.data[(offset + c) as usize]; } } })); out } #[cfg(test)] mod tests { use rawloader::Orientation; use crate::buffer::OpBuffer; use super::rotate_buffer; // Store a colorful capital F as a constant, since it is used in all tests lazy_static! { static ref F: OpBuffer = { OpBuffer::from_rgb_str_vec(vec![ " ", " RRRRRR ", " GG ", " BBBB ", " GG ", " GG ", " ", ]) }; } #[test] fn rotate_unknown() { assert_eq!(rotate_buffer(&F.clone(), &Orientation::Unknown), F.clone()); } #[test] fn rotate_normal() { assert_eq!(rotate_buffer(&F.clone(), &Orientation::Normal), F.clone()); } #[test] fn rotate_flip_x() { let output = OpBuffer::from_rgb_str_vec(vec![ " ", " RRRRRR ", " GG ", " BBBB ", " GG ", " GG ", " ", ]); assert_eq!(rotate_buffer(&F.clone(), &Orientation::HorizontalFlip), output); } #[test] fn rotate_flip_y() { let output = OpBuffer::from_rgb_str_vec(vec![ " ", " GG ", " GG ", " BBBB ", " GG ", " RRRRRR ", " ", ]); assert_eq!(rotate_buffer(&F.clone(), &Orientation::VerticalFlip), output); } #[test] fn rotate_rotate90_cw() { let output = OpBuffer::from_rgb_str_vec(vec![ " ", " GGBGR ", " GGBGR ", " B R ", " B R ", " R ", " R ", " ", ]); assert_eq!(rotate_buffer(&F.clone(), &Orientation::Rotate90), output); } #[test] fn rotate_rotate270_cw() { let output = OpBuffer::from_rgb_str_vec(vec![ " ", " R ", " R ", " R B ", " R B ", " RGBGG ", " RGBGG ", " ", ]); assert_eq!(rotate_buffer(&F.clone(), &Orientation::Rotate270), output); } #[test] fn rotate_rotate180() { let output = OpBuffer::from_rgb_str_vec(vec![ " ", " GG ", " GG ", " BBBB ", " GG ", " RRRRRR ", " ", ]); assert_eq!(rotate_buffer(&F.clone(), &Orientation::Rotate180), output); } #[test] fn rotate_transpose() { let output = OpBuffer::from_rgb_str_vec(vec![ " ", " RGBGG ", " RGBGG ", " R B ", " R B ", " R ", " R ", " ", ]); assert_eq!(rotate_buffer(&F.clone(), &Orientation::Transpose), output); } #[test] fn rotate_transverse() { let output = OpBuffer::from_rgb_str_vec(vec![ " ", " R ", " R ", " B R ", " B R ", " GGBGR ", " GGBGR ", " ", ]); assert_eq!(rotate_buffer(&F.clone(), &Orientation::Transverse), output); } } imagepipe-0.5.0/src/pipeline.rs000064400000000000000000000317110072674642500145630ustar 00000000000000use crate::ops::*; use crate::opbasics::*; extern crate rawloader; extern crate multicache; use self::multicache::MultiCache; extern crate serde; extern crate serde_yaml; use self::serde::{Serialize,Deserialize}; extern crate image; use image::{RgbImage, DynamicImage}; use std::fmt::Debug; use std::sync::Arc; use std::io::Write; use std::path::Path; use std::hash::{Hash, Hasher}; use std::time::Instant; /// A RawImage processed into a full 8bit sRGB image with levels and gamma /// /// The data is a Vec width width*height*3 elements, where each element is a value /// between 0 and 255 with the intensity of the color channel with gamma applied #[derive(Debug, Clone, PartialEq)] pub struct SRGBImage { pub width: usize, pub height: usize, pub data: Vec, } /// A RawImage processed into a full 16bit sRGB image with levels and gamma /// /// The data is a Vec width width*height*3 elements, where each element is a value /// between 0 and 65535 with the intensity of the color channel with gamma applied #[derive(Debug, Clone, PartialEq)] pub struct SRGBImage16 { pub width: usize, pub height: usize, pub data: Vec, } pub type PipelineCache = MultiCache; pub type OtherImage = DynamicImage; #[derive(Debug, Clone)] pub enum ImageSource { Raw(RawImage), Other(OtherImage), } impl ImageSource { fn width(&self) -> usize { match self { Self::Raw(raw) => raw.width, Self::Other(img) => img.width() as usize, } } fn height(&self) -> usize { match self { Self::Raw(raw) => raw.height, Self::Other(img) => img.height() as usize, } } } macro_rules! do_timing { ($name:expr, $body:expr) => { { let from_time = Instant::now(); let ret = { $body }; let duration = from_time.elapsed(); info!("timing: {:>7} ms for |{}", duration.as_millis(), $name); ret } } } pub trait ImageOp<'a>: Debug+Serialize+Deserialize<'a> { fn name(&self) -> &str; fn run(&self, pipeline: &PipelineGlobals, buf: Arc) -> Arc; fn to_settings(&self) -> String { serde_yaml::to_string(self).unwrap() } fn hash(&self, hasher: &mut BufHasher) { // Hash the name first as a zero sized struct doesn't actually do any hashing hasher.write(self.name().as_bytes()).unwrap(); hasher.from_serialize(self); } fn shash(&self) -> BufHash { let mut selfhasher = BufHasher::new(); selfhasher.from_serialize(self); selfhasher.result() } // What size is the output the operation creates given its input fn transform_forward(&mut self, width: usize, height: usize) -> (usize, usize) { (width, height) } // What size is the input the operation needs to create a given output fn transform_reverse(&mut self, width: usize, height: usize) -> (usize, usize) { (width, height) } // Reset any saved data so the pipeline runs again, for most ops this is noop fn reset(&mut self) {} } #[derive(Debug, Copy, Clone, Serialize)] pub struct PipelineSettings { pub maxwidth: usize, pub maxheight: usize, pub demosaic_width: usize, pub demosaic_height: usize, pub linear: bool, pub use_fastpath: bool, } impl PipelineSettings { fn default() -> Self { Self { maxwidth: 0, maxheight: 0, demosaic_width: 0, demosaic_height: 0, linear: false, use_fastpath: true, } } } impl PipelineSettings{ fn hash(&self, hasher: &mut BufHasher) { hasher.from_serialize(self); } } #[derive(Debug)] pub struct PipelineGlobals { pub image: ImageSource, pub settings: PipelineSettings, } impl PipelineGlobals { pub fn mock(width: u32, height: u32) -> Self { Self { image: ImageSource::Other(DynamicImage::ImageRgb8(RgbImage::new(width, height))), settings: PipelineSettings::default(), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PipelineOps { pub gofloat: gofloat::OpGoFloat, pub demosaic: demosaic::OpDemosaic, pub rotatecrop: rotatecrop::OpRotateCrop, pub tolab: colorspaces::OpToLab, pub basecurve: curves::OpBaseCurve, pub fromlab: colorspaces::OpFromLab, pub gamma: gamma::OpGamma, pub transform: transform::OpTransform, } impl PipelineOps { fn new(img: &ImageSource) -> Self { Self { gofloat: gofloat::OpGoFloat::new(&img), demosaic: demosaic::OpDemosaic::new(&img), rotatecrop: rotatecrop::OpRotateCrop::new(&img), tolab: colorspaces::OpToLab::new(&img), basecurve: curves::OpBaseCurve::new(&img), fromlab: colorspaces::OpFromLab::new(&img), gamma: gamma::OpGamma::new(&img), transform: transform::OpTransform::new(&img), } } } impl PartialEq for PipelineOps { fn eq(&self, other: &Self) -> bool { let mut selfhasher = BufHasher::new(); selfhasher.from_serialize(self); let mut otherhasher = BufHasher::new(); otherhasher.from_serialize(other); selfhasher.result() == otherhasher.result() } } impl Eq for PipelineOps {} impl Hash for PipelineOps { fn hash(&self, state: &mut H) { let mut selfhasher = BufHasher::new(); selfhasher.from_serialize(self); selfhasher.result().hash(state); } } macro_rules! for_vals { ([$($val:expr),*] |$x:pat, $i:ident| $body:expr) => { let mut pos = 0; $({ let $x = $val; pos += 1; let $i = pos-1; $body })* } } macro_rules! all_ops { ($ops:expr, |$x:pat, $i:ident| $body:expr) => { for_vals!([ $ops.gofloat, $ops.demosaic, $ops.rotatecrop, $ops.tolab, $ops.basecurve, $ops.fromlab, $ops.gamma, $ops.transform ] |$x, $i| { $body }); } } macro_rules! all_ops_reverse { ($ops:expr, |$x:pat, $i:ident| $body:expr) => { for_vals!([ $ops.transform, $ops.gamma, $ops.fromlab, $ops.basecurve, $ops.tolab, $ops.rotatecrop, $ops.demosaic, $ops.gofloat ] |$x, $i| { $body }); } } #[derive(Debug)] pub struct Pipeline { pub globals: PipelineGlobals, pub ops: PipelineOps, } #[derive(Debug, Serialize, Deserialize)] pub struct PipelineSerialization { pub version: u32, pub filehash: String, } impl Pipeline { pub fn new_cache(size: usize) -> PipelineCache { MultiCache::new(size) } pub fn new_from_file>(path: P) -> Result { do_timing!("total new_from_file()", { if let Ok(img) = do_timing!(" rawloader", rawloader::decode_file(&path)) { Self::new_from_source(ImageSource::Raw(img)) } else if let Ok(img) = do_timing!(" image::open", image::open(&path)) { Self::new_from_source(ImageSource::Other(img)) } else { Err("imagepipe: Don't know how to decode image".to_string()) } }) } pub fn new_from_source(img: ImageSource) -> Result { let ops = PipelineOps::new(&img); Ok(Pipeline { globals: PipelineGlobals { image: img, settings: PipelineSettings::default(), }, ops, }) } pub fn default_ops(&self) -> bool { self.ops == PipelineOps::new(&self.globals.image) } pub fn to_serial(&self) -> String { let serial = (PipelineSerialization { version: 0, filehash: "0".to_string(), }, &self.ops); serde_yaml::to_string(&serial).unwrap() } pub fn new_from_serial(img: ImageSource, serial: String) -> Pipeline { let serial: (PipelineSerialization, PipelineOps) = serde_yaml::from_str(&serial).unwrap(); Pipeline { globals: PipelineGlobals { image: img, settings: PipelineSettings::default(), }, ops: serial.1, } } pub fn run(&mut self, cache: Option<&PipelineCache>) -> Arc { do_timing!(" total pipeline", { // Reset all ops to make sure we're starting clean all_ops!(self.ops, |ref mut op, _i| { op.reset(); }); // Calculate what size of image we should scale down to at the demosaic stage let mut width = self.globals.image.width(); let mut height = self.globals.image.height(); all_ops!(self.ops, |ref mut op, _i| { let (w, h) = op.transform_forward(width, height); width = w; height = h; }); log::debug!("Maximum possible image size is {}x{}", width, height); let maxwidth = self.globals.settings.maxwidth; let maxheight = self.globals.settings.maxheight; let (mut width, mut height) = crate::scaling::scaling_size(width, height, maxwidth, maxheight); log::debug!("Final image size is {}x{}", width, height); all_ops_reverse!(self.ops, |ref mut op, _i| { let (w, h) = op.transform_reverse(width, height); width = w; height = h; }); log::debug!("Needed image size at demosaic {}x{}", width, height); self.globals.settings.demosaic_width = width; self.globals.settings.demosaic_height = height; // Generate all the hashes for the operations let mut hasher = BufHasher::new(); let mut ophashes = Vec::new(); let mut startpos = 0; // Hash the base settings that are potentially used by all operations self.globals.settings.hash(&mut hasher); // Start with a dummy buffer as gofloat doesn't use it let mut bufin = Arc::new(OpBuffer::default()); // Find the hashes of all ops all_ops!(self.ops, |ref op, i| { op.hash(&mut hasher); let result = hasher.result(); ophashes.push(result); // Set the latest op for which we already have the calculated buffer if let Some(cache) = cache { if let Some(buffer) = cache.get(&result) { bufin = buffer; startpos = i+1; } } }); // Do the operations, starting for the last we have a cached buffer for all_ops!(self.ops, |ref op, i| { if i >= startpos { let opstr = " ".to_string() + op.name(); bufin = do_timing!(&opstr, op.run(&self.globals, bufin.clone())); if let Some(cache) = cache { cache.put_arc(ophashes[i], bufin.clone(), bufin.width*bufin.height*bufin.colors*4); } } }); bufin }) } pub fn output_8bit(&mut self, cache: Option<&PipelineCache>) -> Result { // If the image is raster and we haven't changed it yet there's no need to go // through the whole pipeline. Just go straight to 8bit using the image // crate and resize if needed if let ImageSource::Other(ref image) = self.globals.image { if self.globals.settings.use_fastpath && self.default_ops() { return Ok(do_timing!("total output_8bit_fastpath()", { let rgb = image.to_rgb8(); let (width, height) = (rgb.width() as usize, rgb.height() as usize); let out = SRGBImage{ width, height, data: rgb.into_raw(), }; let (nwidth, nheight) = crate::scaling::scaling_size( out.width, out.height, self.globals.settings.maxwidth, self.globals.settings.maxheight ); if nwidth != out.width || nheight != out.height { crate::scaling::scale_down_srgb(&out, nwidth, nheight) } else { out } })) } } do_timing!("total output_8bit()", { self.globals.settings.linear = false; let buffer = self.run(cache); let image = do_timing!(" 8 bit conversion", { let mut image = vec![0 as u8; buffer.width*buffer.height*3]; for (o, i) in image.chunks_exact_mut(1).zip(buffer.data.iter()) { o[0] = output8bit(*i); } image }); Ok(SRGBImage{ width: buffer.width, height: buffer.height, data: image, }) }) } pub fn output_16bit(&mut self, cache: Option<&PipelineCache>) -> Result { // If the image is raster and we haven't changed it yet there's no need to go // through the whole pipeline. Just go straight to 16bit using the image // crate and resize if needed if let ImageSource::Other(ref image) = self.globals.image { if self.globals.settings.use_fastpath && self.default_ops() { return Ok(do_timing!("total output_16bit_fastpath()", { let rgb = image.to_rgb16(); let (width, height) = (rgb.width() as usize, rgb.height() as usize); let out = SRGBImage16{ width, height, data: rgb.into_raw(), }; let (nwidth, nheight) = crate::scaling::scaling_size( out.width, out.height, self.globals.settings.maxwidth, self.globals.settings.maxheight ); if nwidth != out.width || nheight != out.height { crate::scaling::scale_down_srgb16(&out, nwidth, nheight) } else { out } })) } } do_timing!("total output_16bit()", { self.globals.settings.linear = true; let buffer = self.run(cache); let image = do_timing!(" 8 bit conversion", { let mut image = vec![0 as u16; buffer.width*buffer.height*3]; for (o, i) in image.chunks_exact_mut(1).zip(buffer.data.iter()) { o[0] = output16bit(*i); } image }); Ok(SRGBImage16{ width: buffer.width, height: buffer.height, data: image, }) }) } } imagepipe-0.5.0/src/scaling.rs000064400000000000000000000164600072674642500144020ustar 00000000000000use crate::buffer::*; use crate::pipeline::{SRGBImage, SRGBImage16}; use rawloader::CFA; use num_traits::cast::AsPrimitive; use rayon::prelude::*; use std::cmp; fn calculate_scaling_total(width: usize, height: usize, maxwidth: usize, maxheight: usize) -> (f32, usize, usize) { if maxwidth == 0 && maxheight == 0 { (1.0, width, height) } else { // Do the calculations manually to avoid off-by-one errors from floating point rounding let xscale = if maxwidth == 0 {1.0} else {width as f32 / maxwidth as f32}; let yscale = if maxheight == 0 {1.0} else {height as f32 / maxheight as f32}; if yscale <= 1.0 && xscale <= 1.0 { (1.0, width, height) } else if yscale > xscale { (yscale, ((width as f32)/yscale) as usize, maxheight) } else { (xscale, maxwidth, ((height as f32)/xscale) as usize) } } } pub fn scaling_size(width: usize, height: usize, maxwidth: usize, maxheight: usize) -> (usize, usize) { let (_, width, height) = calculate_scaling_total(width, height, maxwidth, maxheight); (width, height) } pub fn calculate_scale(width: usize, height: usize, maxwidth: usize, maxheight: usize) -> f32 { calculate_scaling_total(width, height, maxwidth, maxheight).0 } #[inline(always)] fn scale_down_buffer( src: &[T], width: usize, height: usize, nwidth: usize, nheight: usize, components: usize, cfa: Option<&CFA>, ) -> Vec where f32: AsPrimitive, T: AsPrimitive, T: Sync+Send { transform_buffer(src, width, height, (0, 0), (width as isize - 1, 0), (0, height as isize - 1), nwidth, nheight, components, cfa) } #[inline(always)] pub fn transform_buffer( src: &[T], width: usize, height: usize, topleft: (isize, isize), topright: (isize, isize), bottomleft: (isize, isize), nwidth: usize, nheight: usize, components: usize, cfa: Option<&CFA>, ) -> Vec where f32: AsPrimitive, T: AsPrimitive, T: Sync+Send { let mut out = vec![(0 as f32).as_(); nwidth*nheight*components]; // This scales by using a rectangular window of the source image for each // destination pixel. The destination pixel is filled with a weighted average // of the source window, using the square of the distance as the weight. let skip_x_x = (topright.0 as f32- topleft.0 as f32) / ((nwidth-1) as f32); let skip_x_y = (topright.1 as f32 - topleft.1 as f32) / ((nwidth-1) as f32); let skip_y_x = (bottomleft.0 as f32 - topleft.0 as f32) / ((nheight-1) as f32); let skip_y_y = (bottomleft.1 as f32 - topleft.1 as f32) / ((nheight-1) as f32); // Using rayon to make this multithreaded is 10-15% faster on an i5-6200U which // is useful but not a great speedup for 2 cores 4 threads. It may even make // sense to give this up to not thrash caches. out.par_chunks_exact_mut(nwidth*components).enumerate().for_each(|(row, line)| { let from_x = topleft.0 as f32 + skip_y_x * row as f32; let to_x = topleft.0 as f32 + skip_y_x * (row+1) as f32; let from_y = topleft.1 as f32 + skip_y_y * row as f32; let to_y = topleft.1 as f32 + skip_y_y * (row+1) as f32; let center_x = (topleft.0 as f32) + (skip_y_x * row as f32) + (skip_y_x / 2.0) - 0.5; let center_y = (topleft.1 as f32) + (skip_y_y * row as f32) + (skip_y_y / 2.0) - 0.5; for col in 0..nwidth { let from_x = cmp::min(width-1, (from_x + (skip_x_x * col as f32)).floor() as usize); let to_x = cmp::min(width-1, (to_x + (skip_x_x * (col+1) as f32)).floor() as usize); let from_y = cmp::min(height-1, (from_y + (skip_x_y * col as f32)).floor() as usize); let to_y = cmp::min(height-1, (to_y + (skip_x_y * (col+1) as f32)).floor() as usize); let center_x = center_x + (skip_x_x * col as f32) + (skip_x_x / 2.0); let center_y = center_y + (skip_x_y * col as f32) + (skip_x_y / 2.0); let mut sums = [0.0 as f32; 4]; let mut counts = [0.0 as f32; 4]; for y in from_y..=to_y { for x in from_x..=to_x { // FIXME: Hopefully this is a reasonable low-pass filter that works for // most cases but something more sophisticated may be useful. // More specifically probably one of two things: // - A gaussian filter with parameters calculated based on how // much scale down we are doing so as to exactly remove the // high frequencies we can no longer represent // - A good windowed sinc function like Lanczos that should // preserve more detail but will always have some artifacts // in some cases let delta_x = (x as f32 - center_x) / skip_x_x; let delta_y = (y as f32 - center_y) / skip_y_y; let factor = 1.0 - (delta_x*delta_x) - (delta_y*delta_y); let factor = if factor < 0.0 {0.0} else {factor}; if let Some(cfa) = cfa { let c = cfa.color_at(y, x); sums[c] += src[y*width+x].as_() * factor; counts[c] += factor; } else { for c in 0..components { sums[c] += src[(y*width+x)*components+c].as_() * factor; counts[c] += factor; } } } } for c in 0..components { if counts[c] > 0.0 { line[col*components+c] = (sums[c] / counts[c]).as_(); } } } }); out } pub fn scaled_demosaic(cfa: CFA, buf: &OpBuffer, nwidth: usize, nheight: usize) -> OpBuffer { assert_eq!(buf.colors, 1); // When we're in demosaic we start with a 1 color buffer log::debug!("Doing a scaled demosaic from {}x{} to {}x{}", buf.width, buf.height, nwidth, nheight); let data = scale_down_buffer(&buf.data, buf.width, buf.height, nwidth, nheight, 4, Some(&cfa)); OpBuffer { width: nwidth, height: nheight, data, monochrome: buf.monochrome, colors: 4, } } pub fn scale_down_opbuf(buf: &OpBuffer, nwidth: usize, nheight: usize) -> OpBuffer { assert_eq!(buf.colors, 4); // When we're scaling down we're always at 4 cpp log::debug!("Scaling OpBuffer from {}x{} to {}x{}", buf.width, buf.height, nwidth, nheight); let data = scale_down_buffer(&buf.data, buf.width, buf.height, nwidth, nheight, 4, None); OpBuffer { width: nwidth, height: nheight, data, monochrome: buf.monochrome, colors: 4, } } pub fn scale_down_srgb(buf: &SRGBImage, nwidth: usize, nheight: usize) -> SRGBImage { log::debug!("Scaling SRGBImage from {}x{} to {}x{}", buf.width, buf.height, nwidth, nheight); let data = scale_down_buffer(&buf.data, buf.width, buf.height, nwidth, nheight, 3, None); SRGBImage { width: nwidth, height: nheight, data, } } pub fn scale_down_srgb16(buf: &SRGBImage16, nwidth: usize, nheight: usize) -> SRGBImage16 { log::debug!("Scaling SRGBImage from {}x{} to {}x{}", buf.width, buf.height, nwidth, nheight); let data = scale_down_buffer(&buf.data, buf.width, buf.height, nwidth, nheight, 3, None); SRGBImage16 { width: nwidth, height: nheight, data, } } #[cfg(test)] mod tests { use super::*; #[test] fn scaling_noop() { let width = 150; let height = 150; let mut data = vec![0 as u16; width*height*3]; for (i, o) in data.chunks_exact_mut(1).enumerate() { o[0] = i as u16; } let orig = SRGBImage16 { width, height, data, }; let new = scale_down_srgb16(&orig, width, height); assert_eq!(orig, new); } } imagepipe-0.5.0/tests/maxsize_test.rs000064400000000000000000000052240072674642500160500ustar 00000000000000use imagepipe::{Pipeline, ImageSource, Rotation}; use image::{RgbImage, DynamicImage}; fn create_pipeline() -> Pipeline { let source = ImageSource::Other(DynamicImage::ImageRgb8(RgbImage::new(128, 64))); Pipeline::new_from_source(source).unwrap() } fn assert_width(pipeline: &mut Pipeline, width: usize, height: usize) { let decoded = pipeline.output_8bit(None).unwrap(); pipeline.globals.settings.use_fastpath = true; assert_eq!(decoded.width, width); assert_eq!(decoded.height, height); let decoded = pipeline.output_8bit(None).unwrap(); pipeline.globals.settings.use_fastpath = false; assert_eq!(decoded.width, width); assert_eq!(decoded.height, height); let decoded = pipeline.output_16bit(None).unwrap(); pipeline.globals.settings.use_fastpath = true; assert_eq!(decoded.width, width); assert_eq!(decoded.height, height); let decoded = pipeline.output_16bit(None).unwrap(); pipeline.globals.settings.use_fastpath = false; assert_eq!(decoded.width, width); assert_eq!(decoded.height, height); } #[test] fn default_same_size() { let mut pipeline = create_pipeline(); assert_width(&mut pipeline, 128, 64); } #[test] fn no_upscaling() { let mut pipeline = create_pipeline(); pipeline.globals.settings.maxwidth = 256; pipeline.globals.settings.maxwidth = 128; assert_width(&mut pipeline, 128, 64); } #[test] fn downscale_keeps_ratio() { let mut pipeline = create_pipeline(); pipeline.globals.settings.maxwidth = 64; assert_width(&mut pipeline, 64, 32); } #[test] fn rotation() { let mut pipeline = create_pipeline(); pipeline.globals.settings.maxwidth = 64; pipeline.ops.transform.rotation = Rotation::Rotate90; assert_width(&mut pipeline, 64, 128); let mut pipeline = create_pipeline(); pipeline.globals.settings.maxwidth = 32; pipeline.ops.transform.rotation = Rotation::Rotate90; assert_width(&mut pipeline, 32, 64); let mut pipeline = create_pipeline(); pipeline.globals.settings.maxwidth = 256; pipeline.ops.transform.rotation = Rotation::Rotate90; assert_width(&mut pipeline, 64, 128); } #[test] fn crops() { let mut pipeline = create_pipeline(); pipeline.globals.settings.maxwidth = 64; pipeline.ops.gofloat.crop_top = 1; pipeline.ops.gofloat.crop_bottom = 1; pipeline.ops.gofloat.crop_left = 1; pipeline.ops.gofloat.crop_right = 1; assert_width(&mut pipeline, 64, 31); } #[test] fn rotatecrop() { let mut pipeline = create_pipeline(); pipeline.globals.settings.maxwidth = 64; pipeline.ops.rotatecrop.crop_top = 0.1; pipeline.ops.rotatecrop.crop_bottom = 0.1; pipeline.ops.rotatecrop.crop_left = 0.1; pipeline.ops.rotatecrop.crop_right = 0.1; assert_width(&mut pipeline, 64, 32); } imagepipe-0.5.0/tests/roundtrip_test.rs000064400000000000000000000046230072674642500164200ustar 00000000000000use imagepipe::{Pipeline, ImageSource}; use image::{ImageBuffer, DynamicImage}; fn roundtrip_8bit(fast: bool) { // Create a source with all possibilities of u8 (R,G,B) pixels let mut image_data: Vec = Vec::with_capacity(256 * 256 * 256 * 3); for r in 0..=u8::MAX { for g in 0..=u8::MAX { for b in 0..=u8::MAX { image_data.push(r); image_data.push(g); image_data.push(b); } } } let image = ImageBuffer::from_raw(4096, 4096, image_data.clone()).unwrap(); let source = ImageSource::Other(DynamicImage::ImageRgb8(image)); let mut pipeline = Pipeline::new_from_source(source).unwrap(); pipeline.globals.settings.use_fastpath = fast; let decoded = pipeline.output_8bit(None).unwrap(); for (pixin, pixout) in image_data.chunks_exact(3).zip(decoded.data.chunks_exact(3)) { assert_eq!(pixout, pixin); } } #[test] fn roundtrip_8bit_fastpath() { roundtrip_8bit(true); } #[test] fn roundtrip_8bit_slowpath() { roundtrip_8bit(false); } fn roundtrip_16bit(fast: bool) { let mut start = (0,0,0); loop { // Create a source with a bunch of possibilities of u16 (R,G,B) pixels // We need to do this in blocks or we'd end up allocating huge buffers let mut image_data: Vec = vec![0; 256 * 256 * 256 * 3]; let mut pos = 0; let mut newstart = None; 'outer: for r in (start.0..=u16::MAX).step_by(89) { for g in (start.1..=u16::MAX).step_by(97) { for b in (start.2..=u16::MAX).step_by(101) { if pos >= image_data.len() { newstart = Some((r,g,b)); break 'outer } image_data[pos+0] = r; image_data[pos+1] = g; image_data[pos+2] = b; pos += 3; } } } let image = ImageBuffer::from_raw(4096, 4096, image_data.clone()).unwrap(); let source = ImageSource::Other(DynamicImage::ImageRgb16(image)); let mut pipeline = Pipeline::new_from_source(source).unwrap(); pipeline.globals.settings.use_fastpath = fast; let decoded = pipeline.output_16bit(None).unwrap(); for (pixin, pixout) in image_data.chunks_exact(3).zip(decoded.data.chunks_exact(3)) { assert_eq!(pixout, pixin); } if let Some(newstart) = newstart { start = newstart; } else { break; } } } #[test] fn roundtrip_16bit_fastpath() { roundtrip_16bit(true); } #[test] fn roundtrip_16bit_slowpath() { roundtrip_16bit(false); }