From 2b9317519cfda78367a9a2735b9706023b886c4b Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 14:19:39 -0400 Subject: [PATCH 01/16] feat: startup update prompt modal and direct self-updating The update flow (check at start or on request, the startup prompt, the self-update worker) is craft-update's Updater; the app only supplies the suite, its close path and its status window. The check at start is opt-in for the desktop app, and the Windows update helper runs before argument parsing. Ref: storytold/craft-libs#319 --- Cargo.lock | 373 ++++++++++++++++++++++++++++++- Cargo.toml | 1 + apps/lightcraft/Cargo.toml | 3 + apps/lightcraft/src/main.rs | 6 + crates/ui-egui/Cargo.toml | 3 + crates/ui-egui/src/lib.rs | 6 + crates/ui-egui/src/menubar.rs | 1 + crates/ui-egui/src/menus.rs | 2 + crates/ui-egui/src/updates_ui.rs | 159 +++++++++++++ 9 files changed, 546 insertions(+), 8 deletions(-) create mode 100644 crates/ui-egui/src/updates_ui.rs diff --git a/Cargo.lock b/Cargo.lock index 6e932cba8..ebe47ab97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,6 +580,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -829,7 +835,7 @@ dependencies = [ "safetensors 0.7.0", "thiserror 2.0.21", "yoke 0.8.3", - "zip", + "zip 7.2.0", ] [[package]] @@ -1038,6 +1044,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1051,7 +1067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types", "libc", @@ -1064,7 +1080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "libc", ] @@ -1091,6 +1107,31 @@ dependencies = [ "serde", ] +[[package]] +name = "craft-update" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=13ceaf2f31620531f39a54f6b994d581a6082a93#13ceaf2f31620531f39a54f6b994d581a6082a93" +dependencies = [ + "chrono", + "dunce", + "egui", + "flate2", + "getrandom 0.3.4", + "log", + "plist", + "semver", + "serde", + "serde_json", + "sha2", + "tar", + "tempfile", + "ureq", + "verifysign", + "winreg", + "winsafe", + "zip 9.1.0", +] + [[package]] name = "crc32fast" version = "1.5.2" @@ -1215,6 +1256,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + [[package]] name = "dhat" version = "0.3.3" @@ -1304,6 +1351,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-stack" version = "0.10.0" @@ -1784,6 +1837,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.14" @@ -2342,8 +2405,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "wasm-bindgen", ] [[package]] @@ -2614,6 +2679,22 @@ dependencies = [ "digest", ] +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "image" version = "0.25.10" @@ -3105,6 +3186,7 @@ name = "lightcraft" version = "0.14.0" dependencies = [ "chrono", + "craft-update", "eframe", "egui", "lightcraft-codecs", @@ -3371,7 +3453,7 @@ version = "0.14.0" dependencies = [ "lightcraft-develop", "lightcraft-geom", - "quick-xml", + "quick-xml 0.41.0", ] [[package]] @@ -3409,7 +3491,7 @@ dependencies = [ "lightcraft-geom", "lightcraft-tiff", "proptest", - "quick-xml", + "quick-xml 0.41.0", "serde", "serde_json", ] @@ -3537,6 +3619,7 @@ name = "lightcraft-ui-egui" version = "0.14.0" dependencies = [ "craft-ui", + "craft-update", "egui", "egui_kittest", "lightcraft-catalog", @@ -3859,6 +3942,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "no_std_io2" version = "0.9.4" @@ -3939,6 +4034,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.2" @@ -4376,6 +4477,12 @@ dependencies = [ "libc", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "orbclient" version = "0.3.55" @@ -4652,6 +4759,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plist" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" +dependencies = [ + "base64", + "indexmap", + "quick-xml 0.42.0", + "serde", + "time", +] + [[package]] name = "png" version = "0.18.1" @@ -4738,6 +4858,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4894,6 +5020,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.47" @@ -5303,13 +5438,27 @@ version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki 0.103.15", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.15.1" @@ -5319,6 +5468,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.15", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + [[package]] name = "rustls-rustcrypto" version = "0.0.2-alpha" @@ -5419,6 +5595,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -5445,6 +5630,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.3.0" @@ -5456,6 +5664,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "send_wrapper" @@ -5824,6 +6036,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -5906,6 +6128,36 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.53.1" @@ -6030,6 +6282,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +[[package]] +name = "typed-path" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" + [[package]] name = "typenum" version = "1.20.1" @@ -6140,6 +6398,41 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "uuid" version = "1.26.1" @@ -6189,6 +6482,16 @@ dependencies = [ "vello_common", ] +[[package]] +name = "verifysign" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6acfcc42a8d44f340d001193d97ef56bf33210b51ad760ae8308c97d5241ca51" +dependencies = [ + "core-foundation 0.10.1", + "windows-sys 0.61.2", +] + [[package]] name = "version_check" version = "0.9.5" @@ -6404,7 +6707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.41.0", "quote", ] @@ -6440,6 +6743,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.9" @@ -6936,7 +7248,7 @@ dependencies = [ "calloop 0.13.0", "cfg_aliases", "concurrent-queue", - "core-foundation", + "core-foundation 0.9.4", "core-graphics", "cursor-icon", "dpi", @@ -6981,6 +7293,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "winresource" version = "0.1.31" @@ -6991,6 +7313,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "winsafe" +version = "0.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef0ffc427f045c0cc9ebffd6f4f91153dcd2a1547b5c3c29ee3f541e16e95c6" + [[package]] name = "wit-bindgen" version = "0.57.1" @@ -7328,7 +7656,24 @@ dependencies = [ "crc32fast", "indexmap", "memchr", - "typed-path", + "typed-path 0.12.3", +] + +[[package]] +name = "zip" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886b00003a72c4ccee3f42cfc34908d33b2508a74d827dc2a4244706475c704" +dependencies = [ + "crc32fast", + "flate2", + "getrandom 0.4.3", + "indexmap", + "memchr", + "nix", + "typed-path 0.13.0", + "windows-sys 0.61.2", + "zopfli", ] [[package]] @@ -7343,6 +7688,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zopfli" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zune-core" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 0dc85dfe2..a8c96ad2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://github.com/storytold/lightcraft" [workspace.dependencies] craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f", features = ["serde"] } craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc", features = ["egui"] } # internal lightcraft-denoise-core = { path = "crates/denoise-core" } lightcraft-denoise = { path = "crates/denoise", default-features = false } diff --git a/apps/lightcraft/Cargo.toml b/apps/lightcraft/Cargo.toml index f93bd2ccd..d00bc41e9 100644 --- a/apps/lightcraft/Cargo.toml +++ b/apps/lightcraft/Cargo.toml @@ -55,3 +55,6 @@ objc2-foundation = { workspace = true } [dev-dependencies] # tests record why they skip (`cargo xtask ci` lists the skips) lightcraft-testkit = { workspace = true } + +[target.'cfg(windows)'.dependencies] +craft-update = { workspace = true } diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index b666a0cdc..815fbd70f 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -881,6 +881,11 @@ fn panic_log_path_with(env: impl Fn(&str) -> Option, temp: s } fn main() -> eframe::Result { + // Windows: the update helper is a copy of this program started with `--finish-update`. + #[cfg(windows)] + if let Some(result) = craft_update::self_update::finish(&craft_update::suite::LIGHTCRAFT, &std::env::args_os().skip(1).collect::>()) { + std::process::exit(i32::from(result.is_err())); + } // First, so every start-up record is kept for the log file (`logging`). let logger = logging::install(); lightcraft_engine::guard::install_hook(panic_log_path_with(|name| std::env::var_os(name), std::env::temp_dir())); @@ -1009,6 +1014,7 @@ fn main() -> eframe::Result { // HDR display: SDR unless the surface, platform and display all allow HDR hdr_display::report(cc.wgpu_render_state.as_ref().map(|r| r.target_format)); let mut app = LightcraftApp::new(session, services(cc, app_log_file.as_deref())); + app.set_startup_update_check(true); if let Some(ui) = prefs { app.ui = ui; } diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 5ee1c0233..8dab2b967 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -44,3 +44,6 @@ lightcraft-merge = { workspace = true } [build-dependencies] serde_json = { workspace = true } toml = "1" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +craft-update = { workspace = true } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1b5fea8f1..71c2cef13 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -50,6 +50,7 @@ pub mod tasks; pub mod text_field; pub mod theme; pub mod titlebar; +pub mod updates_ui; pub mod widgets; pub mod window_place; @@ -409,6 +410,7 @@ pub struct LightcraftApp { pub headless_host: bool, /// Warnings to show one at a time (damaged settings files…, issue #103). pub notices: Vec, + pub(crate) updates_rt: updates_ui::Runtime, /// Quitting was stopped to ask first: changes couldn't be saved, or tasks are running. pub quit_prompt: Option, /// Keyboard/focus/input state for the prompt above, including its one-frame creation barrier. @@ -615,6 +617,7 @@ impl LightcraftApp { text_focus: false, headless_host: false, notices: vec![], + updates_rt: Default::default(), quit_prompt: None, notice_input: Default::default(), quit_confirmed: false, @@ -1966,6 +1969,7 @@ impl LightcraftApp { lightroom_import::tick(self, ctx); recheck::tick(self); tasks::poll(self, ctx); + updates_ui::tick(self, ctx); self.preview_build_status(ctx); self.save_status(ctx); self.ai_mask_detail(ctx, now); @@ -2626,6 +2630,7 @@ impl LightcraftApp { panels::notices::hide_background_pointer(&ctx); } panels::dialogs::show(self, &ctx, notice_blocks); + updates_ui::show(self, &ctx); panels::library_problem::show(self, &ctx, notice_blocks); panels::toast(self, &ctx); drop_overlay::show(self, &ctx); @@ -2656,6 +2661,7 @@ impl LightcraftApp { panels::notices::hide_background_pointer(&ctx); } panels::dialogs::show(self, &ctx, notice_blocks); + updates_ui::show(self, &ctx); panels::library_problem::show(self, &ctx, notice_blocks); export_task::poll(self, &ctx); slideshow_export::poll(self, &ctx); diff --git a/crates/ui-egui/src/menubar.rs b/crates/ui-egui/src/menubar.rs index 9b3d88036..ba4a8a7c2 100644 --- a/crates/ui-egui/src/menubar.rs +++ b/crates/ui-egui/src/menubar.rs @@ -322,6 +322,7 @@ const LAYOUT: &[(&str, &[&str])] = &[ "app.systemInfo", "app.openLogFolder", "---", + "app.checkForUpdates", "app.about", ], ), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 03841d14a..84ec8ae0c 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -263,6 +263,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[ // Settings ▸ Display ("app.displayProfile", "Display Profile", None, ""), ("app.about", "About LightCraft", None, "Help"), + ("app.checkForUpdates", "Check for Updates…", None, "Help"), ("app.systemInfo", "System Info…", None, "Help"), ("app.openLogFolder", "Open Log Folder", None, "Help"), // the frame-rate display (`frame_rate`): a debugging aid, never saved @@ -1587,6 +1588,7 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option crate::updates_ui::check(app), "photo.editInPhotoCraft" => edit_in(app, &ctx, p, true), "photo.editInExternal" => edit_in(app, &ctx, p, false), "photo.openOriginalInExternal" => open_original_in(app, &ctx, p), diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs new file mode 100644 index 000000000..423f9a7c7 --- /dev/null +++ b/crates/ui-egui/src/updates_ui.rs @@ -0,0 +1,159 @@ +//! Software updates, background check at startup, and startup prompt modal. + +use serde_json::Value; + +use crate::LightcraftApp; + +#[cfg(not(target_arch = "wasm32"))] +use craft_update::updater::{Action, Status, Updater}; + +pub(crate) struct Runtime { + #[cfg(not(target_arch = "wasm32"))] + updater: Updater, + /// The desktop app turns the check at start on; tests and the web build leave it off. + #[cfg(not(target_arch = "wasm32"))] + startup_check: bool, + #[cfg(not(target_arch = "wasm32"))] + check_requested: bool, + pub(crate) manual_window_open: bool, + pub(crate) status_message: Option, +} + +impl Default for Runtime { + fn default() -> Self { + Self { + #[cfg(not(target_arch = "wasm32"))] + updater: Updater::new(&craft_update::suite::LIGHTCRAFT, env!("CARGO_PKG_VERSION")), + #[cfg(not(target_arch = "wasm32"))] + startup_check: false, + #[cfg(not(target_arch = "wasm32"))] + check_requested: false, + manual_window_open: false, + status_message: None, + } + } +} + +impl LightcraftApp { + /// Whether the app asks for a newer release when it starts (off until the desktop app turns it on). + #[cfg(not(target_arch = "wasm32"))] + pub fn set_startup_update_check(&mut self, on: bool) { + self.updates_rt.startup_check = on; + } +} + +pub(crate) fn check(app: &mut LightcraftApp) -> Result { + #[cfg(not(target_arch = "wasm32"))] + { + app.updates_rt.check_requested = true; + Ok(Value::Null) + } + #[cfg(target_arch = "wasm32")] + { + app.updates_rt.manual_window_open = true; + app.updates_rt.status_message = Some("Update check is not supported in the web browser.".into()); + Ok(Value::Null) + } +} + +pub(crate) fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { + #[cfg(not(target_arch = "wasm32"))] + { + let rt = &mut app.updates_rt; + if std::mem::take(&mut rt.check_requested) { + rt.updater.check_now(ctx); + } else if rt.startup_check { + rt.updater.check_at_start(ctx); + } + rt.updater.tick(ctx); + } + #[cfg(target_arch = "wasm32")] + let _ = (app, ctx); +} + +pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) { + #[cfg(not(target_arch = "wasm32"))] + { + match app.updates_rt.updater.show(ctx) { + Action::OpenUrl(url) => ctx.open_url(egui::OpenUrl::new_tab(url)), + Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close), + Action::None => {} + } + // The window answers Help ▸ Check for Updates, so it follows the updater's status. + let rt = &mut app.updates_rt; + (rt.manual_window_open, rt.status_message) = match rt.updater.status() { + Status::Idle => (false, None), + Status::Checking => (true, Some("Checking for updates…".to_string())), + Status::UpToDate => (true, Some(format!("LightCraft {} is currently the newest version available.", env!("CARGO_PKG_VERSION")))), + Status::Failed(e) => (true, Some(format!("Could not check for updates:\n{e}"))), + }; + } + + if app.updates_rt.manual_window_open { + let mut open = true; + egui::Window::new(crate::i18n::tr("LightCraft Updates")) + .id(egui::Id::new("lightcraft_manual_updates")) + .open(&mut open) + .collapsible(false) + .resizable(false) + .pivot(egui::Align2::CENTER_CENTER) + .default_pos(ctx.content_rect().center()) + .show(ctx, |ui| { + if let Some(msg) = &app.updates_rt.status_message { + ui.label(msg); + } + ui.add_space(8.0); + if ui.button(crate::i18n::tr("OK")).clicked() { + app.updates_rt.manual_window_open = false; + } + }); + if !open { + app.updates_rt.manual_window_open = false; + } + } + + // Closing the window dismisses the outcome it showed. + #[cfg(not(target_arch = "wasm32"))] + if !app.updates_rt.manual_window_open { + app.updates_rt.updater.dismiss_status(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lightcraft_engine::Session; + + #[test] + fn default_runtime_is_idle() { + let rt = Runtime::default(); + assert!(!rt.manual_window_open); + #[cfg(not(target_arch = "wasm32"))] + { + assert!(!rt.startup_check); + assert!(!rt.updater.prompt_open()); + assert_eq!(rt.updater.status(), &Status::Idle); + } + } + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn startup_prompt_shows_and_closes() { + let mut app = LightcraftApp::new(Session::new(), Default::default()); + let ctx = egui::Context::default(); + let release = craft_update::Release { + tag: "v99.0.0".into(), + version: craft_update::parse_version("99.0.0"), + name: "LightCraft 99.0.0".into(), + html_url: "https://github.com/storytold/lightcraft/releases/tag/v99.0.0".into(), + ..Default::default() + }; + app.updates_rt.updater.offer(release); + + let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { + show(&mut app, ui.ctx()); + }); + output.textures_delta.clear(); + assert!(app.updates_rt.updater.prompt_open()); + } +} From 8e5a6fc2e77376b55caa40c08c7faad16597a175 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 14:53:33 -0400 Subject: [PATCH 02/16] refactor(updates): hold the install back while documents are unsaved --- Cargo.lock | 18 +++++++++--------- crates/ui-egui/src/updates_ui.rs | 4 +++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ebe47ab97..d85066e8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,7 +1110,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=13ceaf2f31620531f39a54f6b994d581a6082a93#13ceaf2f31620531f39a54f6b994d581a6082a93" +source = "git+https://github.com/storytold/craft-libs?rev=5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc#5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc" dependencies = [ "chrono", "dunce", @@ -1720,7 +1720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3850,7 +3850,7 @@ dependencies = [ "once_cell", "png", "thiserror 2.0.21", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4519,7 +4519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5429,7 +5429,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5486,7 +5486,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6056,7 +6056,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6302,7 +6302,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6956,7 +6956,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 423f9a7c7..9d09ff09e 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -74,7 +74,9 @@ pub(crate) fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) { #[cfg(not(target_arch = "wasm32"))] { - match app.updates_rt.updater.show(ctx) { + // The restart helper is scheduled before the app closes, so no update starts over unsaved work. + let clean = app.session.unsaved().is_none(); + match app.updates_rt.updater.show_if(ctx, clean) { Action::OpenUrl(url) => ctx.open_url(egui::OpenUrl::new_tab(url)), Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close), Action::None => {} From 2d3c7275b2ace1c1548b5e5b6910434af61dcf7a Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 15:07:57 -0400 Subject: [PATCH 03/16] fix(updates): drop the manual window's custom id; pin craft-update to craft-libs main (db19c37) --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- crates/ui-egui/src/updates_ui.rs | 1 - 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d85066e8b..11c7b1d73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,7 +1110,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc#5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc" +source = "git+https://github.com/storytold/craft-libs?rev=db19c373c8d59a545972ca340987aecfb3611a4b#db19c373c8d59a545972ca340987aecfb3611a4b" dependencies = [ "chrono", "dunce", @@ -1720,7 +1720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3850,7 +3850,7 @@ dependencies = [ "once_cell", "png", "thiserror 2.0.21", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4519,7 +4519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5429,7 +5429,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5486,7 +5486,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6056,7 +6056,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6302,7 +6302,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6956,7 +6956,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a8c96ad2f..2bb309f91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/storytold/lightcraft" [workspace.dependencies] craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f", features = ["serde"] } craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "db19c373c8d59a545972ca340987aecfb3611a4b", features = ["egui"] } # internal lightcraft-denoise-core = { path = "crates/denoise-core" } lightcraft-denoise = { path = "crates/denoise", default-features = false } diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 9d09ff09e..6ef3a97bc 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -94,7 +94,6 @@ pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) { if app.updates_rt.manual_window_open { let mut open = true; egui::Window::new(crate::i18n::tr("LightCraft Updates")) - .id(egui::Id::new("lightcraft_manual_updates")) .open(&mut open) .collapsible(false) .resizable(false) From ee487850909ab73e3334bd5ef0c19a981c03f842 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 15:17:03 -0400 Subject: [PATCH 04/16] fix(updates): address review feedback Wake the UI when a manual check is queued from a menu callback; the update prompt owns the keyboard while it is open (LightCraft); Download opens through the desktop URL hook (FilmCraft); the Help menu dispatches through menus::invoke (FigCraft); print jobs and comment drafts hold an install back (WordCraft); Check for Updates is a registered command (GridCraft); an install holds back while documents are unsaved. --- crates/ui-egui/src/lib.rs | 2 +- crates/ui-egui/src/updates_ui.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 71c2cef13..be1a9e879 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2127,7 +2127,7 @@ impl LightcraftApp { // Recent-library checks and launch-list keys read focus/time/keys: they belong to the UI // phase, not the hidden-capable `logic` tick (plan §4.5 / migration input contract). panels::library_problem::logic(self, ctx); - if self.fonts_ready { + if self.fonts_ready && !updates_ui::blocks_input(self) { shortcuts::handle(self, ctx); } } diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 6ef3a97bc..92f421e45 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -15,6 +15,9 @@ pub(crate) struct Runtime { startup_check: bool, #[cfg(not(target_arch = "wasm32"))] check_requested: bool, + /// The context of the last frame, to wake the UI when a check is queued from a menu callback. + #[cfg(not(target_arch = "wasm32"))] + repaint: Option, pub(crate) manual_window_open: bool, pub(crate) status_message: Option, } @@ -28,6 +31,8 @@ impl Default for Runtime { startup_check: false, #[cfg(not(target_arch = "wasm32"))] check_requested: false, + #[cfg(not(target_arch = "wasm32"))] + repaint: None, manual_window_open: false, status_message: None, } @@ -42,10 +47,26 @@ impl LightcraftApp { } } +/// The update prompt owns the keyboard while it is open: shortcuts must not reach the library behind it. +pub(crate) fn blocks_input(app: &LightcraftApp) -> bool { + #[cfg(not(target_arch = "wasm32"))] + { + app.updates_rt.updater.prompt_open() + } + #[cfg(target_arch = "wasm32")] + { + let _ = app; + false + } +} + pub(crate) fn check(app: &mut LightcraftApp) -> Result { #[cfg(not(target_arch = "wasm32"))] { app.updates_rt.check_requested = true; + if let Some(ctx) = &app.updates_rt.repaint { + ctx.request_repaint(); + } Ok(Value::Null) } #[cfg(target_arch = "wasm32")] @@ -60,6 +81,7 @@ pub(crate) fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { #[cfg(not(target_arch = "wasm32"))] { let rt = &mut app.updates_rt; + rt.repaint = Some(ctx.clone()); if std::mem::take(&mut rt.check_requested) { rt.updater.check_now(ctx); } else if rt.startup_check { From 015ca9eebae768d45149c8e32d1c4c2d60993ad8 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 15:34:06 -0400 Subject: [PATCH 05/16] fix(updates): localize the update status messages and format the sources --- crates/ui-egui/src/updates_ui.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 92f421e45..5e159a0f4 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -107,9 +107,15 @@ pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) { let rt = &mut app.updates_rt; (rt.manual_window_open, rt.status_message) = match rt.updater.status() { Status::Idle => (false, None), - Status::Checking => (true, Some("Checking for updates…".to_string())), - Status::UpToDate => (true, Some(format!("LightCraft {} is currently the newest version available.", env!("CARGO_PKG_VERSION")))), - Status::Failed(e) => (true, Some(format!("Could not check for updates:\n{e}"))), + Status::Checking => (true, Some(crate::i18n::tr("Checking for updates…").to_string())), + Status::UpToDate => ( + true, + Some( + crate::i18n::tr("LightCraft {version} is currently the newest version available.") + .replace("{version}", env!("CARGO_PKG_VERSION")), + ), + ), + Status::Failed(e) => (true, Some(crate::i18n::tr("Could not check for updates:\n{error}").replace("{error}", e))), }; } From b88f72b9df68f07a080772d7fb16a92d6f7db837 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 22:56:20 -0400 Subject: [PATCH 06/16] sec: bound heic-rs 0.1.4 work around hostile HEIF containers heic-rs has no aggregate work budget in container parsing: ipma association entries rescan earlier item ids after each other and an iloc construction-method-2 extent rebuilds its source item afresh. Cap the ipma/iloc boxes before heic-rs reads them, and stop probe paying for a reconstruction heft beyond the file's own size. decode: refuse files carrying more than 12 container transforms (each would re-copy the full picture; CodeRabbit row #44), check a grid's payload canvas against the pixel limit before allocating for it (#48), and to_rgb16 refuses an oversized w*h*ch rather than asking for a multi-gigabyte buffer. --- crates/heif/src/census.rs | 122 +++++++++++++++++++++++++++++ crates/heif/src/lib.rs | 42 +++++++++- crates/heif/src/tests_synthetic.rs | 13 +++ crates/heif/src/ycc.rs | 26 ++++++ 4 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 crates/heif/src/census.rs diff --git a/crates/heif/src/census.rs b/crates/heif/src/census.rs new file mode 100644 index 000000000..6dbe035fc --- /dev/null +++ b/crates/heif/src/census.rs @@ -0,0 +1,122 @@ +//! Caps on the container's metadata boxes, counted before heic-rs touches the file. +//! +//! heic-rs 0.1.4 (pinned `=0.1.4`) has no aggregate work budget inside its container parsing: +//! every `ipma` association entry rescans the vector of already-seen item ids (quadratic in +//! the number of entries), and an `iloc` extent using construction method 2 reconstructs its +//! source item afresh for every extent that references it, with no shared cache. Legitimate +//! phone-photo metadata never comes near these caps — real corpora keep both well under a few +//! kilobytes — so anything above them is hostile or corrupt and is refused before the +//! countable CPU work happens. +//! +//! [`depth_boxes`] stops silently at structural blips: heic-rs's own parser reports those +//! better than this walk could, and an `ipma` box past the cap is caught wherever it parses. + +use heic_rs::boxes::BoxIter; + +use crate::Error; + +/// The largest `ipma` box (item ↔ property associations) payload accepted, 64 KiB. An entry +/// costs at least 3 bytes, so this bounds heic-rs's per-entry rescan of earlier item ids to +/// ~21 000 entries ≈ 220 million comparisons: long enough to be annoying, never minutes. +const MAX_IPMA_BYTES: usize = 64 << 10; + +/// The largest `iloc` box (item locations) payload accepted, 64 KiB. An extent costs at least +/// 12 bytes, so this bounds both the parse itself and how many extents a +/// construction-method-2 reconstruction chain can visit. +const MAX_ILOC_BYTES: usize = 64 << 10; + +/// Falls through unless the container declares more `ipma` or `iloc` payload than a +/// legitimate phone photo ever does. +pub fn cap(bytes: &[u8]) -> Result<(), Error> { + let mut c = Census::default(); + depth_boxes(&mut c, BoxIter::new(bytes), 0); + if c.ipma > MAX_IPMA_BYTES { + return Err(Error::Limit(format!("the image carries {} bytes of property associations, over the {MAX_IPMA_BYTES}-byte cap", c.ipma))); + } + if c.iloc > MAX_ILOC_BYTES { + return Err(Error::Limit(format!("the image carries {} bytes of item locations, over the {MAX_ILOC_BYTES}-byte cap", c.iloc))); + } + Ok(()) +} + +/// The metadata-box payload sizes seen so far. +#[derive(Default)] +struct Census { + ipma: usize, + iloc: usize, +} + +/// Walks boxes, adding the payload sizes of those named in [`Census`]. +/// +/// Only `meta` and `iprp` (the two nesting boxes the metadata is built from) are descended +/// into, no deeper than that depth, so a nested-box trick can't hide a box from the count. +fn depth_boxes(c: &mut Census, boxes: BoxIter<'_>, depth: u32) { + if depth > 3 { + return; + } + for b in boxes { + // A structural error stops the walk; `heic_rs::probe` reports it properly afterwards. + let Ok(b) = b else { return }; + if b.is(b"ipma") { + c.ipma = c.ipma.saturating_add(b.payload.len()); + } else if b.is(b"iloc") { + c.iloc = c.iloc.saturating_add(b.payload.len()); + } else if b.is(b"meta") { + // `meta` is a FullBox: version and flags sit between its header and its children. + if b.payload.len() >= 4 { + depth_boxes(c, BoxIter::new(&b.payload[4..]), depth + 1); + } + } else if b.is(b"iprp") { + depth_boxes(c, b.children(), depth + 1); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Assembles ftyp + meta(version/flags + iprp(ipco, ipma)) by hand; only the structure + /// the census walks matters, no item is ever defined. + fn file_with(ipma_payload: &[u8]) -> Vec { + let mut out = b"\0\0\0\x18ftypheic\0\0\0\0mif1heic".to_vec(); + let mut iprp = Vec::new(); + iprp.extend(box_of(b"ipco", b"")); + iprp.extend(box_of(b"ipma", ipma_payload)); + let mut meta = vec![0u8, 0, 0, 0]; // version 0, flags 0 + meta.extend(box_of(b"iprp", &iprp)); + out.extend(box_of(b"meta", &meta)); + out + } + + fn box_of(kind: &[u8; 4], body: &[u8]) -> Vec { + let mut out = Vec::with_capacity(8 + body.len()); + let size = u32::try_from(8 + body.len()).expect("test box fits in u32"); + out.extend_from_slice(&size.to_be_bytes()); + out.extend_from_slice(kind); + out.extend_from_slice(body); + out + } + + #[test] + fn an_oversized_ipma_is_refused() { + // 40 000 entries of 6 bytes: 240 KiB of ipma, far over the cap, and quadratic if + // parsed (each entry rescans the earlier item ids in heic-rs 0.1.4). + let mut ipma = Vec::new(); + for _ in 0..40_000u32 { + ipma.extend_from_slice(&[0u8, 0, 0, 0, 0, 0]); + } + let bytes = file_with(&ipma); + let r = crate::probe(&bytes).map(|_| ()); + assert!(matches!(r, Err(Error::Limit(ref m)) if m.contains("associations")), "{r:?}"); + } + + #[test] + fn a_undersized_ipma_is_untouched_by_the_caps() { + // The same shape with a normal ipma (two entries): the caps pass, and whatever error + // follows must not come from them. + let bytes = file_with(&[0, 0, 4, 1, 1, 2, 0, 0]); + let r = crate::probe(&bytes); + assert!(!matches!(r, Err(Error::Limit(ref m)) if m.contains("cap")), "{r:?}"); + } +} diff --git a/crates/heif/src/lib.rs b/crates/heif/src/lib.rs index 21b9cf647..58b2be254 100644 --- a/crates/heif/src/lib.rs +++ b/crates/heif/src/lib.rs @@ -19,6 +19,7 @@ #![forbid(unsafe_code)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +mod census; mod orient; /// Lossless synthetic HEIF files for tests, here and in the crates above (feature `testdata`). #[cfg(any(test, feature = "testdata"))] @@ -149,6 +150,7 @@ pub fn probe(bytes: &[u8]) -> Result { } fn probe_unguarded(bytes: &[u8]) -> Result { + census::cap(bytes)?; let info = heic_rs::probe(bytes).map_err(|e| match e { // No `meta` box: no still image, only a `moov` image sequence (a video track). heic_rs::Error::MissingBox("meta") => Error::Unsupported(SEQUENCE), @@ -156,13 +158,28 @@ fn probe_unguarded(bytes: &[u8]) -> Result { })?; let ctx = Context::open(bytes).map_err(err)?; let id = ctx.meta.primary; - let coded = match ctx.grid(id).map_err(err)? { + let mut coded = match ctx.grid(id).map_err(err)? { Some((_, tiles)) => tiles, None => vec![id], }; + // A `dimg` listing may name the same tile more than once; reconstrutable (`iloc` + // construction method 2) items are moreover rebuilt for every extent that references + // them in heic-rs 0.1.4, so our probe would pay the reconstruction again per tile. Each + // unique item only once, then the total reconstruction (all of it assembled from `bytes` + // by heic-rs) is held below one copy of the file — above that a repeat or chain wants + // more bytes than exist (CodeRabbit #21/#50 on this sweep). Order doesn't matter here: + // this only checks that every tile's bytes lie in the file. + coded.sort_unstable(); + coded.dedup(); + let mut reconstructed = 0_usize; for item in coded { - // Borrowed from `bytes` unless split across extents: cheap. - ctx.item_data(item).map_err(err)?; + reconstructed = reconstructed.saturating_add(ctx.item_data(item).map_err(err)?.len()); + if reconstructed > bytes.len() { + return Err(Error::Limit(format!( + "the container holds {reconstructed} bytes of item data, over the {} bytes of the file itself", + bytes.len() + ))); + } } Ok(Info { width: info.width, @@ -232,6 +249,13 @@ fn decode_unguarded(bytes: &[u8], options: &Options) -> Result { /// `with_alpha` and the item has an alpha auxiliary image. fn decode_item(ctx: &Context<'_>, id: u32, options: &Options, with_alpha: bool) -> Result { let p = ctx.props(id).map_err(err)?; + // heic-rs 0.1.4 keeps every associated transform and its [`orient::apply`] allocates the + // full picture again for each: a file could chain the repetition and multiply decode CPU + // without changing any checked size. Real files carry at most one `clap`, `irot` and + // `imir`, so refuse anything approaching [`MAX_TRANSFORMS`] (CodeRabbit #44). + if p.transforms.len() > MAX_TRANSFORMS { + return Err(Error::Limit(format!("the image carries {} transform properties, over the {MAX_TRANSFORMS}-property cap", p.transforms.len()))); + } let limit = options.max_pixels; // The declared sizes are refused before anything is decoded or allocated. let (cw, ch) = ctx.coded_size(id, &p).map_err(err)?; @@ -245,6 +269,14 @@ fn decode_item(ctx: &Context<'_>, id: u32, options: &Options, with_alpha: bool) let signal = ycc::Signal { matrix: colour.matrix, full_range: colour.full_range }; let alpha_id = if with_alpha { ctx.alpha_item(id).map_err(err)? } else { None }; let (w, h) = planes.size(); + // A grid's canvas dimensions come from its payload, not the checked `ispe` (CodeRabbit + // #48): check the real one, before [`ycc::to_rgb16`] allocates for it. + match (u32::try_from(w), u32::try_from(h)) { + (Ok(wu), Ok(hu)) => { + heic_rs::image::check_pixels(wu, hu, limit).map_err(err)?; + } + _ => return Err(Error::Limit(format!("the image canvas is {w}x{h}, beyond the {limit}-pixel limit"))), + } let mode = if options.apply_transforms && orient::odd_crop(w, h, &p.transforms)? { ycc::Upsampling::Bilinear } else { ycc::Upsampling::Nearest }; let mut data = ycc::to_rgb16(&planes, signal, alpha_id.is_some(), mode) .ok_or_else(|| Error::Malformed("the decoded pictures don't cover the image".into()))?; @@ -281,6 +313,10 @@ fn decode_item(ctx: &Context<'_>, id: u32, options: &Options, with_alpha: bool) /// cover its canvas rounded up to whole tiles, so legitimate grids sit near 1. const GRID_BUDGET: u64 = 2; +/// The most transform properties (`clap`/`irot`/`imir`) one item may carry. Real files have at +/// most one of each, so anything approaching this is a reordered or hostile association list. +const MAX_TRANSFORMS: usize = 12; + /// The decoded pictures of an item: one, or a grid's tiles in `dimg` order. struct Coded<'a> { frames: Vec, diff --git a/crates/heif/src/tests_synthetic.rs b/crates/heif/src/tests_synthetic.rs index 3259dc96d..1286ff82a 100644 --- a/crates/heif/src/tests_synthetic.rs +++ b/crates/heif/src/tests_synthetic.rs @@ -185,3 +185,16 @@ fn truncated_and_corrupted_files_are_errors_not_panics() { let _ = decode_thumbnail(&b, 1, &Options::default()); } } + +/// CodeRabbit #44: heic-rs keeps every associated transform and each one re-allocates the +/// whole picture, so 13 of them are over the [`crate::MAX_TRANSFORMS`] cap and refused. +#[test] +fn too_many_transform_properties_are_refused() { + let bytes = build(&Spec { props: std::iter::repeat_n(Prop::Irot(0), 13).collect(), ..Spec::new(32, 32, &grey) }); + let r = decode(&bytes, &Options::default()); + assert!(matches!(r, Err(Error::Limit(ref m)) if m.contains("transform")), "{r:?}"); + // At the cap they're not: 12 identity rotations decode normally. + let bytes = build(&Spec { props: std::iter::repeat_n(Prop::Irot(0), 12).collect(), ..Spec::new(32, 32, &grey) }); + let d = decode(&bytes, &Options::default()).unwrap(); + assert_eq!((d.width, d.height), (32, 32)); +} diff --git a/crates/heif/src/ycc.rs b/crates/heif/src/ycc.rs index a0429b453..384d25f2a 100644 --- a/crates/heif/src/ycc.rs +++ b/crates/heif/src/ycc.rs @@ -161,6 +161,13 @@ pub fn to_rgb16(planes: &Planes<'_>, signal: Signal, alpha: bool, mode: Upsampli if !(8..=16).contains(&bd) || w == 0 || h == 0 { return None; } + // The default caller limit is 2²8 pixels, at most four channels per pixel, so the largest + // legitimate `w·h·ch` is 2²30 samples. Anything more is a malformed or hostile canvas the + // decode path would have refused before calling here (CodeRabbit #48): refuse rather than + // ask for a multi-gigabuffer (or worse, over usize). + if w.checked_mul(h)?.checked_mul(ch)? > 1 << 30 { + return None; + } let max = ((1u32 << bd) - 1) as f32; let half = (1u32 << (bd - 1)) as f32; let scale = (1u32 << (bd - 8)) as f32; @@ -323,4 +330,23 @@ mod tests { .is_none() ); } + + /// CodeRabbit #48: a plane claiming 500 000 × 500 000 pixels must be refused without ever + /// allocating for it (before the cap this vec was never built; upper-bounding allocation + /// would have been a 7.5-gibabuffer or a panic instead of an error). + #[test] + fn an_oversized_canvas_is_refused_without_allocation() { + let f = Frame { + width: 500_000, + height: 500_000, + bit_depth: 8, + chroma: ChromaFormat::Yuv420, + y: vec![128], + cb: vec![128; 4], + cr: vec![128; 4], + y_stride: 0, + c_stride: 0, + }; + assert!(to_rgb16(&Planes::Picture(&f), Signal { matrix: 6, full_range: true }, false, Upsampling::Nearest).is_none()); + } } From 178eef790bd8872a6f60f7b1a0fb7b67499acc59 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 08:22:10 +0000 Subject: [PATCH 07/16] heif: bound iloc item-extent visits before reconstructing in probe --- crates/heif/src/lib.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/heif/src/lib.rs b/crates/heif/src/lib.rs index 58b2be254..723ffe5d9 100644 --- a/crates/heif/src/lib.rs +++ b/crates/heif/src/lib.rs @@ -171,6 +171,14 @@ fn probe_unguarded(bytes: &[u8]) -> Result { // this only checks that every tile's bytes lie in the file. coded.sort_unstable(); coded.dedup(); + // heic-rs 0.1.4 caps the size of what it assembles, not the work: an item built from other + // items (`iloc` construction method 2) is rebuilt once per extent, so a chain of such items + // multiplies at every level while each result stays small. Count those visits from the + // `iloc` table before reconstructing anything. + let mut visits = 0_usize; + for &item in &coded { + item_visits(&ctx, item, 0, &mut visits)?; + } let mut reconstructed = 0_usize; for item in coded { reconstructed = reconstructed.saturating_add(ctx.item_data(item).map_err(err)?.len()); @@ -191,6 +199,36 @@ fn probe_unguarded(bytes: &[u8]) -> Result { }) } +/// Most extent visits heic-rs may spend reconstructing the primary image's coded items; real +/// files need one per tile (plus a few for the rare derived item). +const MAX_ITEM_VISITS: usize = 4096; +/// Deepest chain of items built from items we follow (heic-rs refuses beyond eight itself). +const MAX_ITEM_CHAIN: u32 = 8; + +/// Adds to `visits` the extents heic-rs would read to reconstruct `id`, following items built +/// from other items, and fails once that passes [`MAX_ITEM_VISITS`] or the chain is too deep. +fn item_visits(ctx: &Context<'_>, id: u32, depth: u32, visits: &mut usize) -> Result<(), Error> { + use heic_rs::meta::iloc::Construction; + if depth > MAX_ITEM_CHAIN { + return Err(Error::Limit(format!("items built from items nest deeper than {MAX_ITEM_CHAIN} levels"))); + } + let Some(loc) = ctx.meta.location(id) else { return Ok(()) }; + *visits = visits.saturating_add(loc.extents.len().max(1)); + if *visits > MAX_ITEM_VISITS { + return Err(Error::Limit(format!("reconstructing the image takes over {MAX_ITEM_VISITS} item extent reads"))); + } + if loc.construction == Construction::Item { + for e in &loc.extents { + if let Ok(source) = u32::try_from(e.index) + && source != id + { + item_visits(ctx, source, depth + 1, visits)?; + } + } + } + Ok(()) +} + /// Decodes the primary image, with its metadata. pub fn decode(bytes: &[u8], options: &Options) -> Result { guarded(|| decode_unguarded(bytes, options)) From d00df6319cd382b67c25f2d4bd540b87261318eb Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:13:32 -0400 Subject: [PATCH 08/16] security/raw: bound hostile-memory paths in raw decoders Sparse walkers, truncated MEF/lossy-JPEG strips, embedded JPEG sizes, the CR3 preview marker walk, OpcodeList3 full-image passes and caller-supplied Deflate tile edges each let a small file or one bad argument multiply allocations or CPU work thousands of times. Each path now is bounded, every fix with a regression test that failed (or blew up) before it, plus a tiff::image single_chunk helper so preview extraction never builds a descriptor vector for a hostile multi-strip layout. --- crates/raw/src/dngwrite.rs | 57 +++++++++++++++++++++- crates/raw/src/opcodes.rs | 45 ++++++++++++++++- crates/raw/src/preview.rs | 73 +++++++++++++++++++++++----- crates/raw/src/sparse.rs | 91 +++++++++++++++++++++++++++++++++-- crates/raw/src/tiffraw.rs | 89 ++++++++++++++++++++++++++++++++-- crates/raw/src/vendor/mef.rs | 59 ++++++++++++++++++++++- crates/raw/src/wb_estimate.rs | 2 +- crates/tiff/src/image.rs | 40 +++++++++++++-- 8 files changed, 427 insertions(+), 29 deletions(-) diff --git a/crates/raw/src/dngwrite.rs b/crates/raw/src/dngwrite.rs index d7ba42776..fae6015b6 100644 --- a/crates/raw/src/dngwrite.rs +++ b/crates/raw/src/dngwrite.rs @@ -94,6 +94,17 @@ pub const SOFTWARE: &str = "LightCraft DNG 2"; /// (3.9 s vs 0.55 s on 4 cores) to save 3 % of the file (266 vs 275 MB); 5–12 % on raw and rendered DNGs. const DEFLATE_LEVEL: u8 = 1; +/// Largest Deflate tile edge the writer pads to (`tile` is caller-supplied in [`DngCompression::Deflate`]): +/// square-tile buffers grow with the square, so an unclamped edge lets a request pad a tiny valid image into +/// a multi-gigabyte buffer and billions of edge-replicate iterations. 2048² samples (≤ 24 MB of f32) is far +/// above the 256 default and every real tile this writer makes. +const MAX_DEFLATE_TILE: u32 = 2048; + +/// `tile` aligned down to a multiple of 16 and clamped to [`MAX_DEFLATE_TILE`]. +fn deflate_tile(tile: u32) -> usize { + (tile.max(16) & !15).min(MAX_DEFLATE_TILE) as usize +} + /// IEEE 754 binary32 → binary16 (round to nearest even; overflow → ±inf, NaN kept). pub fn f32_to_f16(v: f32) -> u16 { let b = v.to_bits(); @@ -318,7 +329,7 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result> { match (&raw.data, opts.compression) { (RawData::F32(v), DngCompression::Deflate { tile, half }) => { - let tile = (tile.max(16) & !15) as usize; + let tile = deflate_tile(tile); let bp = if half { 2 } else { 4 }; ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![(bp * 8) as u16; cpp])); ifd.set(t::SAMPLE_FORMAT, Value::Short(vec![3; cpp])); @@ -417,7 +428,7 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result> { ifd.set_image(ImageData::Strips { rows_per_strip: rows as u32, strips }); } DngCompression::Deflate { tile, .. } => { - let tile = (tile.max(16) & !15) as usize; + let tile = deflate_tile(tile); ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16; cpp])); ifd.set(t::COMPRESSION, Value::Short(vec![compression::ADOBE_DEFLATE])); ifd.set(t::PREDICTOR, Value::Short(vec![2])); @@ -546,3 +557,45 @@ pub fn write_rendered_dng( let order = ByteOrder::Little; write_dng(&raw, &DngWriteOptions { compression: DngCompression::Deflate { tile: 256, half: true }, order, xmp }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{BlackLevel, OpcodeLists, RawData, RawFormat, RawImage, Rect}; + + /// Deflate tiles of `tile × tile` pad into a square buffer that grows with the square: a + /// caller-supplied edge of `u32::MAX` would make the writer pad an 8 × 8 image into billions of + /// edge-replicate iterations and a multi-gigabyte buffer. The clamp keeps the bounded shape — same + /// file as requesting the cap exactly, and a DNG either way. + #[test] + fn a_hostile_deflate_tile_edge_is_clamped() { + let (w, h) = (8usize, 8usize); + let full = Rect::new(0, 0, w, h); + let raw = RawImage { + format: RawFormat::Dng, + width: w, + height: h, + cpp: 1, + data: RawData::U16((0..64).map(|i| i as u16 * 129).collect()), + cfa: None, + bits: 12, + black: BlackLevel::uniform(0.0), + white: vec![4095.0], + active_area: full, + crop: full, + orientation: lightcraft_geom::Orientation::Normal, + color: crate::ColorData::default(), + wb_multipliers: None, + linearized: false, + pixel_aspect: 1.0, + opcodes: OpcodeLists::default(), + metadata: Default::default(), + lens_metadata: Default::default(), + }; + let opts = |tile: u32| DngWriteOptions { compression: DngCompression::Deflate { tile, half: false }, ..DngWriteOptions::default() }; + let hostile = write_dng(&raw, &opts(u32::MAX)).expect("the clamped tile writes"); + assert_eq!(hostile, write_dng(&raw, &opts(2048)).expect("the cap itself writes"), "u32::MAX is clamped to the cap"); + assert_ne!(hostile, write_dng(&raw, &opts(256)).expect("a normal tile writes"), "the clamp is not a no-op"); + assert_eq!(crate::probe(&hostile), Some(RawFormat::Dng), "the clamped output is still a DNG"); + } +} diff --git a/crates/raw/src/opcodes.rs b/crates/raw/src/opcodes.rs index 6cf224984..d5a3ffd1e 100644 --- a/crates/raw/src/opcodes.rs +++ b/crates/raw/src/opcodes.rs @@ -656,18 +656,38 @@ fn fix_pixels(buf: &mut [f32], w: usize, h: usize, cpp: usize, cfa: Option<&Cfa> } } +/// Most full-image passes of one `OpcodeList3`: every warp record is a full-image allocation and resample +/// pass and every bad-pixel record a full flatten-and-rebuild, and a hostile record may repeat either +/// thousands of times. Real DNGs carry at most a couple; passes past the cap are skipped. +const MAX_LIST3_PASSES: usize = 8; + /// Apply `OpcodeList3` (on the demosaiced RGB image, values normalised to [0, 1]). pub fn apply_list3(list: &[Opcode], img: &mut Rgb32f) { if list.is_empty() { return; } let (w, h) = (img.width, img.height); + let mut passes = 0usize; for op in applied_ops(list) { match op { - Opcode::WarpRectilinear { planes, center } => *img = warp_rectilinear(img, planes, *center), - Opcode::WarpRectilinear2 { planes, center, reciprocal, .. } => *img = warp_rectilinear2(img, planes, *center, *reciprocal), + Opcode::WarpRectilinear { planes, center } => { + if passes < MAX_LIST3_PASSES { + passes += 1; + *img = warp_rectilinear(img, planes, *center); + } + } + Opcode::WarpRectilinear2 { planes, center, reciprocal, .. } => { + if passes < MAX_LIST3_PASSES { + passes += 1; + *img = warp_rectilinear2(img, planes, *center, *reciprocal); + } + } Opcode::WarpFisheye { .. } | Opcode::TrimBounds { .. } | Opcode::Unknown { .. } => {} other => { + if passes >= MAX_LIST3_PASSES { + continue; + } + passes += 1; let mut flat: Vec = img.data.iter().flat_map(|p| *p).collect(); apply_list(std::slice::from_ref(other), &mut flat, w, h, 3, None, 1.0); for (d, c) in img.data.iter_mut().zip(flat.as_chunks::<3>().0) { @@ -765,6 +785,27 @@ pub(crate) mod tests { a.iter().flat_map(|v| v.to_be_bytes()).collect() } + /// Warp and bad-pixel records each cost a full-image pass, and a hostile `OpcodeList3` repeats them + /// thousands of times ([`MAX_LIST3_PASSES`] bounds the multiplier); identity warps below the cap must + /// still apply, exactly, so the same image comes out. + #[test] + fn list3_pass_cap_skips_repeated_records() { + let mut img = Rgb32f::new(8, 6); + for (i, px) in img.data.iter_mut().enumerate() { + *px = [i as f32 / 2.0, (i % 8) as f32, (i / 8) as f32]; + } + let orig: Vec<[f32; 3]> = img.data.clone(); + let list: Vec = + (0..MAX_LIST3_PASSES * 5).map(|_| Opcode::WarpRectilinear { planes: vec![[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]], center: [0.5, 0.5] }).collect(); + apply_list3(&list, &mut img); + assert_eq!(img.data, orig, "identity warps change nothing, and the cap skips the repeated rest"); + + // a real warp below the cap is still applied + let one = [Opcode::WarpRectilinear { planes: vec![[2.0, 0.0, 0.0, 0.0, 0.0, 0.0]], center: [0.5, 0.5] }]; + apply_list3(&one, &mut img); + assert_ne!(img.data, orig, "a genuine warp record is not skipped"); + } + #[test] fn parse_and_apply_area_ops() { let (w, h) = (4usize, 3usize); diff --git a/crates/raw/src/preview.rs b/crates/raw/src/preview.rs index 95f9bdcaa..8a7e180c7 100644 --- a/crates/raw/src/preview.rs +++ b/crates/raw/src/preview.rs @@ -103,8 +103,6 @@ fn jpeg_frame(b: &[u8]) -> Option<(u32, u32, u8)> { let be16 = |k: usize| Some(u32::from(u16::from_be_bytes([at(k)?, at(k + 1)?]))); Some((be16(7)?, be16(5)?, at(9)?)) } - -/// Whether `b` is a JPEG XL file: a bare codestream or the ISO-BMFF container's signature box. fn is_jxl(b: &[u8]) -> bool { b.starts_with(&[0xff, 0x0a]) || b.starts_with(&[0, 0, 0, 0x0c, b'J', b'X', b'L', b' ', 0x0d, 0x0a, 0x87, 0x0a]) } @@ -128,20 +126,17 @@ fn candidates<'a>(data: &'a [u8], ifd: &Ifd, base: u64, out: &mut Vec<&'a [u8]>) } if matches!(ifd.u16(t::COMPRESSION), Some(6) | Some(7) | Some(34892)) && let Ok(info) = ifd.image() + && let Some(chunk) = info.single_chunk(data.len() as u64) + && let Some(s) = chunk_bytes(data, &chunk) { - let chunks = info.chunks(data.len() as u64); - if chunks.len() == 1 - && let Some(s) = chunk_bytes(data, &chunks[0]) - { - out.push(s); - } + out.push(s); } // a rendered (RGB or grey, never CFA/LinearRaw/mask) JPEG XL preview in one chunk if ifd.u16(t::COMPRESSION) == Some(t::compression::JPEG_XL) && matches!(ifd.u16(t::PHOTOMETRIC), Some(t::photometric::BLACK_IS_ZERO | t::photometric::RGB)) && let Ok(info) = ifd.image() - && let [chunk] = info.chunks(data.len() as u64).as_slice() - && let Some(s) = chunk_bytes(data, chunk) + && let Some(chunk) = info.single_chunk(data.len() as u64) + && let Some(s) = chunk_bytes(data, &chunk) && is_jxl(s) { out.push(s); @@ -397,9 +392,15 @@ fn cr3_preview(bytes: &[u8]) -> Option<&[u8]> { /// Canon CR3 (ISO base media file): the `PRVW` box (Laurent Clévy's CR3 notes; layout confirmed on a CC0 /// sample) is `u32 size, "PRVW", u32 0, u16 ?, u16 width, u16 height, u16 ?, u32 jpeg length, JPEG`; the smaller /// `THMB` box has the same shape. Returns the larger valid one. +/// Marker-walk budget of the CR3 fallback: candidates can overlap (each claiming the rest of the file) and +/// [`is_dct_jpeg`] walks each marker by marker, so walking every candidate would be quadratic in file size. +/// The `THMB`/`PRVW` previews this finds are far below the cap together; once it is spent the scan stops. +const CR3_MAX_WALK: usize = 8 << 20; + fn cr3_preview_boxes(bytes: &[u8]) -> Option<&[u8]> { let mut best: Option<&[u8]> = None; - for tag in [b"PRVW", b"THMB"] { + let mut walked = 0usize; + 'tags: for tag in [b"PRVW", b"THMB"] { let mut from = 0; while let Some(i) = bytes.get(from..).and_then(|s| s.windows(4).position(|w| w == tag)).map(|p| p + from) { from = i + 4; @@ -411,6 +412,10 @@ fn cr3_preview_boxes(bytes: &[u8]) -> Option<&[u8]> { continue; } let jpeg = &bytes[j..j + len]; + if walked + jpeg.len() > CR3_MAX_WALK { + break 'tags; + } + walked += jpeg.len(); if is_dct_jpeg(jpeg) { if best.is_none_or(|b| b.len() < jpeg.len()) { best = Some(jpeg); @@ -440,6 +445,52 @@ mod tests { j } + /// Preview extraction only serves single-chunk JPEG-compressed IFDs and asks the `single_chunk` first: + /// a TIFF whose strip array declares 100 000 one-row strips and no StripByteCounts must not build their + /// descriptor vector, and of course finds no preview in it. + #[test] + fn a_multi_strip_jpeg_ifd_costs_no_descriptor_vector() { + let mut ifd = IfdBuilder::new(); + ifd.set(t::COMPRESSION, Value::Short(vec![7])); + ifd.set(t::IMAGE_WIDTH, Value::Long(vec![16])); + ifd.set(t::IMAGE_LENGTH, Value::Long(vec![100_000])); + ifd.set(t::ROWS_PER_STRIP, Value::Short(vec![1])); + ifd.set(t::STRIP_OFFSETS, Value::Byte((0..100_000).map(|i| (i % 255 + 1) as u8).collect())); + let file = TiffWriter::default().write(&[ifd]).unwrap(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || tx.send(embedded_preview(&file))); + assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None)); + } + + /// CR3 candidates claim overlapping extents: each block's first bytes double as a 40-byte jump marker so + /// a walk jogs past the next header and reaches the end of the file, making every one of the 9000 + /// candidates walk its whole suffix before the marker-walk budget — quadratic without it. Nothing here + /// is a JPEG (no frame header), so the answer is `None` either way. + #[test] + fn cr3_preview_boxes_stop_walking_when_the_budget_is_spent() { + // one 256-byte block per candidate, all marker-aligned so walks chain through later blocks: + // 0..4, size (itself a jump marker of 40 bytes); 4..8, "PRVW"; 20..24, the claimed length + // (to the file end); 24..28, an SOI plus a marker jumping to the fill at 40; 64..256, a fill of + // 4-byte markers (`ff 01 00 02`). + let blocks = 9000usize; + let total = blocks * 256; + let mut f = Vec::with_capacity(total); + for k in 0..blocks { + let base = k * 256; + f.extend_from_slice(&[0xff, 0x01, 0x00, 0x26, b'P', b'R', b'V', b'W', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + f.extend_from_slice(&u32::try_from(total - base - 24).unwrap().to_be_bytes()); + f.extend_from_slice(&[0xff, 0xd8, 0x00, 0x24, 0xff, 0x01, 0x00, 0x26, 0, 0, 0, 0, 0, 0, 0, 0]); + f.resize(base + 64, 0); // 40..64 filler: walks jump over it, so its bytes are free + for _ in 0..48 { + f.extend_from_slice(&[0xff, 0x01, 0x00, 0x02]); + } + } + assert_eq!(f.len(), total); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || tx.send(cr3_preview_boxes(&f).map(|s| s.len()))); + assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None), "no PRVW candidate is a JPEG"); + } + /// A minimal RAF: the fixed header (JPEG offset/length at 84/88), the JPEG, then a raw tail the /// caller must not have to read. `jpeg_len` is written as declared, so a test can claim more than /// the JPEG holds the way a padded container does. diff --git a/crates/raw/src/sparse.rs b/crates/raw/src/sparse.rs index d67a92b40..a1a718f14 100644 --- a/crates/raw/src/sparse.rs +++ b/crates/raw/src/sparse.rs @@ -35,6 +35,12 @@ const MAX_ENTRIES: u64 = 8192; const MAX_BOXES: usize = 4096; /// Child IFDs listed by one `SubIFDs` entry. const MAX_CHILDREN: u64 = 4096; +/// Pending child-IFD offsets the walker may queue at once: no more than it can ever visit +/// (the same [`MAX_IFDS`] the seen set is held to, deduplicated as they are popped), so an alley +/// of hostile `SubIFDs` entries can't grow the queue into the tens of millions of pointers. +/// The plan never guesses: the walk refuses and the plan reports the whole file when it would go +/// past the cap. +const MAX_PENDING: usize = MAX_IFDS; /// The RAF raw container's pointer to its raw IFD. const RAF_RAW_IFD: u16 = 0xf000; @@ -308,6 +314,10 @@ fn walk_tiff(w: &mut Want, origin: u64, data: &[u8], extra: &[u16]) -> Result = Vec::new(); while let Some(off) = stack.pop() { if off == 0 || off >= len || seen.len() >= MAX_IFDS || !seen.insert(off) { continue; @@ -329,7 +339,7 @@ fn walk_tiff(w: &mut Want, origin: u64, data: &[u8], extra: &[u16]) -> Result Result inline { - meta.insert(at(start), at(value_end)); + spans.push((at(start), at(value_end))); } let child = matches!(tag, t::SUB_IFDS | t::EXIF_IFD | t::GPS_IFD | t::INTEROP_IFD) || extra.contains(&tag); let locates = child || matches!(tag, t::STRIP_OFFSETS | t::STRIP_BYTE_COUNTS | t::TILE_OFFSETS | t::TILE_BYTE_COUNTS); @@ -359,14 +369,21 @@ fn walk_tiff(w: &mut Want, origin: u64, data: &[u8], extra: &[u16]) -> Result= MAX_PENDING { + return Err("too many child IFDs queued to visit"); + } stack.push(v); } } } if complete && let Some(next) = offset_at(table_end) { + if stack.len() >= MAX_PENDING { + return Err("too many child IFDs queued to visit"); + } stack.push(next); } } + meta.extend(spans); Ok(meta) } @@ -585,8 +602,74 @@ mod tests { assert_eq!(run(&file, 16, 0).0, HeaderPlan::Whole("an ARW6 header probe reads its tiles")); } - /// A TIFF with a million one-byte strips listed in descending order and all byte counts 0: the plan is - /// built in one sort and merge, not one pass over all strips (or all holes) per strip. + /// An IFD whose `SubIFDs` entries would queue 33 million child pointers (8192 entries of + /// 4096 values apiece, all pointing at the same array) is refused at the walker's pending + /// cap instead of growing the queue — and the plan reports the whole file, never guessing. + #[test] + fn a_subifds_alley_is_refused_at_the_pending_cap() { + // First read everything, so the walk reaches the directory table itself. + let n = 8192u32; + let len = 8 + 2 + (n * 12 + 4 + 4 * 4096) as usize; + let mut tiff = b"II*\0\x08\0\0\0".to_vec(); + tiff.extend_from_slice(&(n as u16).to_le_bytes()); + let mut entry = |tag: u16, typ: u16, count: u32, value: u32| { + tiff.extend_from_slice(&tag.to_le_bytes()); + tiff.extend_from_slice(&typ.to_le_bytes()); + tiff.extend_from_slice(&count.to_le_bytes()); + tiff.extend_from_slice(&value.to_le_bytes()); + }; + // header (8) + count field (2) + the {n} numbers and pointer (4), then the value array + let values_at = (8 + 2u64 + u64::from(n) * 12 + 4) as u32; + for _ in 0..n { + entry(t::SUB_IFDS, 4, MAX_CHILDREN as u32, values_at); + } + tiff.extend_from_slice(&0u32.to_le_bytes()); + tiff.extend((1..=4096u32).flat_map(u32::to_le_bytes)); + let mut loaded = Ranges::new(); + loaded.insert(0, len as u64); + let mut w = Want::new(&tiff, &loaded); + assert_eq!(walk_tiff(&mut w, 0, &tiff, &[]), Err("too many child IFDs queued to visit")); + } + + /// A TIFF with many out-of-line values across 512 directories is planned in one sort and + /// merge, not one pass over all accumulated ranges per value (CodeRabbit #52: before, each + /// `Ranges::insert` copied every range found before it — the ~million disjoint values of the + /// fixture below ask for more copying than the timeout allows). + #[test] + fn many_out_of_line_values_plan_fast() { + let (ifds, values) = (512usize, 2048usize); + let ifd_len = 2 + values * 12 + 4; + let values_at = 8 + ifds * ifd_len; + let len = values_at + values * 8; + let mut tiff = vec![0u8; len]; + tiff[..8].copy_from_slice(b"II*\0\x08\0\0\0"); + for i in 0..ifds { + let at = 8 + i * ifd_len; + // {values} out-of-line strings (count 8 > the 4 inline bytes), then the next-IFD + // pointer; every value is a disjoint 8-byte range for the merge to handle. + tiff[at..at + 2].copy_from_slice(&(values as u16).to_le_bytes()); + for k in 0..values { + let e = at + 2 + k * 12; + tiff[e..e + 2].copy_from_slice(&t::MAKE.to_le_bytes()); + tiff[e + 2..e + 4].copy_from_slice(&2u16.to_le_bytes()); // ASCII: 8 bytes > inline — a span per entry + tiff[e + 4..e + 8].copy_from_slice(&8u32.to_le_bytes()); + tiff[e + 8..e + 12].copy_from_slice(&(values_at as u32 + (k * 8) as u32).to_le_bytes()); + } + let next = if i == ifds - 1 { 0u32 } else { (at + ifd_len) as u32 }; + tiff[at + ifd_len - 4..at + ifd_len].copy_from_slice(&next.to_le_bytes()); + } + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut loaded = Ranges::new(); + loaded.insert(0, len as u64); + let _ = tx.send(header_plan(&tiff, &loaded)); + }); + rx.recv_timeout(std::time::Duration::from_secs(20)).expect("header_plan took longer than 20 s"); + } + + /// A TIFF with a million one-byte strips listed in descending order and all byte counts 0: + /// the plan is built in one sort and merge, not one pass over all strips (or all holes) per + /// strip. #[test] fn many_strips_without_byte_counts_plan_fast() { let n = 1_000_000u32; diff --git a/crates/raw/src/tiffraw.rs b/crates/raw/src/tiffraw.rs index a781151b4..8cd6a13e2 100644 --- a/crates/raw/src/tiffraw.rs +++ b/crates/raw/src/tiffraw.rs @@ -179,7 +179,7 @@ fn decode_chunk(data: &[u8], info: &ImageInfo, c: &Chunk, order: ByteOrder, pack } comp::LOSSY_JPEG => { // lossy DNG: each chunk is a baseline (DCT) JPEG of 8-bit samples - let (px, jw, jh, jc) = lossy_jpeg(src, cpp)?; + let (px, jw, jh, jc) = lossy_jpeg(src, cpp, MAX_JPEG_SAMPLES)?; let mut v = vec![0u16; n]; for y in 0..ch.min(jh) { for x in 0..cw.min(jw) { @@ -197,7 +197,23 @@ fn decode_chunk(data: &[u8], info: &ImageInfo, c: &Chunk, order: ByteOrder, pack // the gate caps at MAX_SAMPLES (≤ 2^30), so the `usize` cast below is lossless on 32-bit targets too let inflate_len = crate::checked_pixel_count(row_bytes, ch as u64).ok_or_else(|| RawError::Corrupt("bad TIFF chunk dimensions".into()))?; + // Deflate compresses at most about 1:1032, so a source this far under half of its + // nominal length cannot reconstruct the chunk: refused before the nominal buffer is + // inflated and predicted, which padding-only work would make gigabytes. + if (src.len() as u64).saturating_mul(2048) < inflate_len { + return Err(RawError::Corrupt(format!("a deflated chunk of {inflate_len} bytes is packed into only {} bytes", src.len()))); + } let raw = inflate(src, inflate_len as usize)?; + // The stream has to reach the last row inside the image (edge strips may stop there); + // it is laid out in nominal rows, so shorter is a truncated chunk, not a picture. + let last = if float { vw as u64 * cpp as u64 * bytes_per as u64 } else { (vw as u64 * cpp as u64 * bits as u64).div_ceil(8) }; + let visible = row_bytes.saturating_mul(vh.saturating_sub(1) as u64).saturating_add(last); + if (raw.len() as u64) < visible { + return Err(RawError::Corrupt(format!( + "a deflated chunk holds {} bytes, fewer than the {visible} bytes its in-image samples span", + raw.len() + ))); + } unpack_chunk(&raw, order, packing, cw, ch, cpp, bits, float, info.predictor) } comp::JPEG_XL => jxl_chunk(src, cw, ch, vw, vh, cpp, bits, float), @@ -228,19 +244,31 @@ fn jxl_chunk_check(src: &[u8], cw: usize, ch: usize, vw: usize, vh: usize, cpp: jxl_chunk(src, cw, ch, vw, vh, cpp, bits, float).map(|_| ()) } +/// The most samples a tiled JPEG chunk or a white-balance preview may decode to: far more than +/// any real lossy tile or preview carries, and small enough that a JPEG header alone cannot size +/// a multi-gigabyte allocation. +pub(crate) const MAX_JPEG_SAMPLES: usize = 1 << 26; + /// Decode one baseline JPEG chunk to 8-bit samples: (samples, width, height, channels). -pub(crate) fn lossy_jpeg(src: &[u8], cpp: usize) -> Result<(Vec, usize, usize, usize)> { +pub(crate) fn lossy_jpeg(src: &[u8], cpp: usize, max_samples: usize) -> Result<(Vec, usize, usize, usize)> { use zune_core::bytestream::ZCursor; use zune_core::colorspace::ColorSpace; use zune_core::options::DecoderOptions; let cs = if cpp == 1 { ColorSpace::Luma } else { ColorSpace::RGB }; let opts = DecoderOptions::default().set_max_width(1 << 16).set_max_height(1 << 16).set_strict_mode(false).jpeg_set_out_colorspace(cs); let mut d = zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(src), opts); - let px = d.decode().map_err(|e| RawError::Corrupt(format!("lossy JPEG tile: {e}")))?; + // The header is read first: the JPEG's own dimensions, not the enclosing tile's, size every + // allocation `decode` makes, and they may claim billions of pixels. + d.decode_headers().map_err(|e| RawError::Corrupt(format!("lossy JPEG tile: {e}")))?; let info = d.info().ok_or_else(|| RawError::Corrupt("lossy JPEG tile without a header".into()))?; let (w, h) = (info.width as usize, info.height as usize); let c = if cpp == 1 { 1 } else { 3 }; - if w == 0 || h == 0 || px.len() < w * h * c { + let out = w as u64 * h as u64 * c as u64; + if w == 0 || h == 0 || out > max_samples as u64 { + return Err(RawError::Corrupt(format!("a lossy JPEG of {w} × {h} × {c} would exceed the {max_samples} output samples allowed"))); + } + let px = d.decode().map_err(|e| RawError::Corrupt(format!("lossy JPEG tile: {e}")))?; + if px.len() < w * h * c { return Err(RawError::Corrupt("lossy JPEG tile: short output".into())); } Ok((px, w, h, c)) @@ -471,4 +499,57 @@ mod lossy_tests { // half of the visible rows are missing assert!(decode(8, 4).is_err()); } + + /// A lossy JPEG whose header alone claims more samples than any real tile or preview (here + /// 40000 × 40000 × 3) is refused from the header, before the decoder allocates its output + /// from the JPEG's own dimensions. + #[test] + fn an_oversized_lossy_jpeg_header_is_refused_before_decoding() { + let mut j = vec![0xff, 0xd8]; // SOI + // SOF0: precision 8, 40000 (0x9c40) x 40000, three components + j.extend([0xff, 0xc0, 0x00, 0x11, 0x08, 0x9c, 0x40, 0x9c, 0x40, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01]); + // SOS with three components so header parsing completes normally, then EOI + j.extend([0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00, 0x00, 0x3f, 0x00, 0x00, 0xff, 0xd9]); + let e = lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES).unwrap_err(); + assert!(matches!(e, RawError::Corrupt(_)), "{e:?}"); + // a header inside the budget passes the check (and then fails to decode only for lack of data) + j[12..14].copy_from_slice(&1024u16.to_be_bytes()); + j[14..16].copy_from_slice(&1024u16.to_be_bytes()); + assert!(lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES).is_ok() || matches!(lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES), Err(RawError::Corrupt(_)))); + } + + /// A deflated tile is refused when its packed bytes are too few for deflate to have produced + /// the nominal chunk, or when the stream stops short of the rows inside the image; a fully + /// covered flat tile still decodes. + #[test] + fn a_short_or_undercovering_deflated_tile_is_refused() { + let info = |byte_counts: u64| ImageInfo { + width: 512, + height: 512, + bits_per_sample: vec![16], + samples_per_pixel: 1, + compression: comp::ADOBE_DEFLATE, + photometric: 32803, + planar: 1, + predictor: 1, + sample_format: 1, + new_subfile_type: 0, + layout: Layout::Tiles { tile_width: 512, tile_height: 512 }, + offsets: vec![0], + byte_counts: vec![byte_counts], + }; + // a complete flat tile: 512 KiB of zeros compresses to a few hundred bytes (well within + // deflate's packing bound) and inflates to every nominal byte the tile claims + let zlib = miniz_oxide::deflate::compress_to_vec_zlib(&vec![0u8; 512 * 512 * 2], 6); + assert_eq!(read_image(&zlib, &info(zlib.len() as u64), ByteOrder::Little, Packing::Msb), Ok(RawData::U16(vec![0; 512 * 512]))); + // the same 1 KiB packed, but the stream claims a whole tile: a few hundred bytes cannot + // reconstruct 512 KiB from a decoder that compresses no further than about 1:1032 + let zlib = miniz_oxide::deflate::compress_to_vec_zlib(&vec![0u8; 1024], 6); + assert!(matches!(read_image(&zlib, &info(512 * 512 * 2), ByteOrder::Little, Packing::Msb), Err(RawError::Corrupt(_))), "{zlib:?}"); + // 500 KiB of zeros compress to a stream within the packing bound and inflating to more + // bytes than the in-image samples span, but 24 KiB short of the whole nominal tile: + // refused as truncated rather than filled with the predictor's padding + let zlib = miniz_oxide::deflate::compress_to_vec_zlib(&vec![0u8; 500 * 1000], 6); + assert!(matches!(read_image(&zlib, &info(zlib.len() as u64), ByteOrder::Little, Packing::Msb), Err(RawError::Corrupt(_)))); + } } diff --git a/crates/raw/src/vendor/mef.rs b/crates/raw/src/vendor/mef.rs index 207bc8871..d5c5d83b4 100644 --- a/crates/raw/src/vendor/mef.rs +++ b/crates/raw/src/vendor/mef.rs @@ -89,6 +89,13 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { // the white balance is measured from the samples, so the header probe reads them too let chunks = info.chunks(bytes.len() as u64); let src = chunks.first().and_then(|c| chunk_bytes(bytes, c)).ok_or_else(|| RawError::Corrupt("Mef strip outside file".into()))?; + // 12-bit MSB packing takes 3 bytes per 2 samples; a strip that cannot hold the declared + // samples is a truncated file, not a picture — unpacking it would run past the real bytes + // through every output sample (a short strip asks for the whole allocation and more). + let need = n.div_ceil(2).saturating_mul(3); + if src.len() < need { + return Err(RawError::Corrupt(format!("a Mef strip of {} bytes cannot hold the declared {n} samples", src.len()))); + } let mut d = vec![0u16; n]; crate::unpack::unpack_msb(src, 12, &mut d); let white = super::white_from_data(&d, 12); @@ -138,6 +145,10 @@ mod tests { } fn sample(w: u32, h: u32, px: &[u16]) -> Vec { + sample_strip(w, h, &pack(px)) + } + + fn sample_strip(w: u32, h: u32, strip: &[u8]) -> Vec { let mut raw = IfdBuilder::new(); raw.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0])); raw.set(t::IMAGE_WIDTH, Value::Long(vec![w])); @@ -148,7 +159,7 @@ mod tests { raw.set(t::ORIENTATION, Value::Short(vec![6])); raw.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2])); raw.set(t::CFA_PATTERN_EP, Value::Byte(vec![0, 1, 1, 2])); - raw.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![pack(px)] }); + raw.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![strip.to_vec()] }); let mut ifd0 = IfdBuilder::new(); ifd0.set(t::MAKE, Value::Ascii("Mamiya-OP Co.,Ltd.".into())); ifd0.set(t::MODEL, Value::Ascii("MAMIYA ZD".into())); @@ -156,6 +167,52 @@ mod tests { TiffWriter::new(ByteOrder::Big, false).write(&[ifd0]).unwrap() } + /// A strip that cannot hold the declared samples is a truncated file, refused as corrupt + /// instead of unpacked: `unpack_msb` would otherwise run `n` samples through the few real + /// bytes, filling the rest from nothing. + #[test] + fn a_strip_too_short_for_the_declared_samples_is_refused() { + let px: Vec = (0..16 * 6).map(|i| (i * 41 % 4096) as u16).collect(); + let f = sample(16, 6, &px); + assert!(crate::decode(&f).is_ok()); + // A truncated file shaped like a real one: IFD tables first, strip last — only its first + // 20 packed bytes remain, while the IFD still declares the full 1.5 B/sample length. The + // probe accepts the file on the declared length; before the byte gate below, `unpack_msb` + // ran the dimension-sized allocation through those 20 bytes, filling it from nothing. + let u32be = |b: &[u8], a: usize| u32::from_be_bytes([b[a], b[a + 1], b[a + 2], b[a + 3]]); + let full = pack(&px); + let at = f.windows(full.len()).position(|w| w == full).expect("strip bytes in the file"); + let after_strip = at + full.len(); + let moved = after_strip - (at + 20); + let mut g = f[..8].to_vec(); + g.extend(std::iter::repeat_n(0u8, at + 20 - 8)); + g.extend_from_slice(&f[after_strip..]); + // repoint the first-IFD pointer and ifd0's MAKE / MODEL / SubIfds offsets (the tables now + // sit `moved` earlier); the raw sub-IFD's strip offset and byte count are patched below + let poke = |b: &mut [u8], a: usize| { + let v = u32::from_be_bytes([b[a], b[a + 1], b[a + 2], b[a + 3]]); + if v as usize >= after_strip { + b[a..a + 4].copy_from_slice(&(v - moved as u32).to_be_bytes()); + } + }; + let ifd0_at = u32be(&g, 4) as usize - moved; + g[4..8].copy_from_slice(&(ifd0_at as u32).to_be_bytes()); + let n = u16::from_be_bytes([g[ifd0_at], g[ifd0_at + 1]]) as usize; + for i in 0..n { + poke(&mut g, ifd0_at + 2 + i * 12 + 8); + } + // the raw sub-IFD: entries follow its count; tag 273 (StripOffsets) is the seventh, held + // inline — repoint it at the strip's new home, and append the truncated strip for real + let sub_at = u32be(&g, ifd0_at + 2 + 2 * 12 + 8) as usize; + let e_offsets = sub_at + 2 + 6 * 12 + 8; + let strip_at = g.len() as u32; + g[e_offsets..e_offsets + 4].copy_from_slice(&strip_at.to_be_bytes()); + g.extend_from_slice(&full[..20]); + assert_eq!(crate::probe(&g), Some(RawFormat::Mef)); + let r = crate::decode(&g); + assert!(matches!(r, Err(RawError::Corrupt(_))), "{r:?}"); + } + #[test] fn decodes_packed_12_bit() { let px: Vec = (0..16 * 6).map(|i| (i * 41 % 4096) as u16).collect(); diff --git a/crates/raw/src/wb_estimate.rs b/crates/raw/src/wb_estimate.rs index 17a86a2a5..13cb9f8da 100644 --- a/crates/raw/src/wb_estimate.rs +++ b/crates/raw/src/wb_estimate.rs @@ -66,7 +66,7 @@ fn median(v: &mut [f64]) -> Option { fn from_preview(bytes: &[u8], f: &Frame) -> Option<[f32; 3]> { let jpeg = crate::preview::embedded_preview(bytes)?; - let (px, pw, ph, ch) = crate::tiffraw::lossy_jpeg(&jpeg, 3).ok()?; + let (px, pw, ph, ch) = crate::tiffraw::lossy_jpeg(&jpeg, 3, crate::tiffraw::MAX_JPEG_SAMPLES).ok()?; if ch != 3 { return None; } diff --git a/crates/tiff/src/image.rs b/crates/tiff/src/image.rs index ca3ff8b69..16e24f367 100644 --- a/crates/tiff/src/image.rs +++ b/crates/tiff/src/image.rs @@ -118,14 +118,34 @@ impl ImageInfo { } } - /// All chunks that have an offset. Missing byte counts are inferred as "to the next offset / end of data" - /// using `data_len`. - pub fn chunks(&self, data_len: u64) -> Vec { + /// The chunk count the image declares (offsets present, cut to the layout's plane capacity) with the + /// geometry `chunks` needs to place each one. No descriptors are built, so a pathological strip array + /// costs it a length check. + fn chunk_grid(&self) -> (usize, u32, usize) { let (across, down) = self.grid(); let per_plane = across as usize * down as usize; let planes = if self.planar == 2 { self.samples_per_pixel as usize } else { 1 }; + (self.offsets.len().min(per_plane.saturating_mul(planes)), across, per_plane) + } + + fn declared_chunk_count(&self) -> usize { + self.chunk_grid().0 + } + + /// The one chunk when the image has exactly one, else `None` — and no descriptor vector beyond it. Preview + /// extraction only serves single-chunk images, so it asks here before serving a hostile multi-strip layout. + pub fn single_chunk(&self, data_len: u64) -> Option { + if self.declared_chunk_count() != 1 { + return None; + } + self.chunks(data_len).into_iter().next() + } + + /// All chunks that have an offset. Missing byte counts are inferred as "to the next offset / end of data" + /// using `data_len`. + pub fn chunks(&self, data_len: u64) -> Vec { + let (n, across, per_plane) = self.chunk_grid(); let (cw, ch) = self.chunk_size(); - let n = self.offsets.len().min(per_plane.saturating_mul(planes)); let mut out = Vec::with_capacity(n); // the distinct offsets in order, for chunks without a byte count (sorted once, not scanned per chunk) let mut sorted: Vec = Vec::new(); @@ -192,6 +212,18 @@ mod tests { } } + /// One offset is one chunk, and `single_chunk` refuses to build descriptors when the image declares more. + #[test] + fn single_chunk_is_the_only_chunk() { + let one = strips(vec![50], vec![10]); + assert_eq!(one.single_chunk(100).map(|c| (c.index, c.offset, c.len)), one.chunks(100).first().map(|c| (c.index, c.offset, c.len))); + // the inferred length matches too, and never falls back past the data + let inferred = strips(vec![70], vec![]); + assert_eq!(inferred.single_chunk(100).map(|c| c.len), Some(30)); + assert!(strips(vec![50, 10], vec![10, 5]).single_chunk(100).is_none(), "a multi-chunk image has no single chunk"); + assert!(strips(Vec::new(), Vec::new()).single_chunk(100).is_none()); + } + #[test] fn missing_byte_counts_run_to_the_next_larger_offset() { let info = strips(vec![50, 10, 30, 10, 70], vec![0, 0, 5]); From d8ceb3a6af2b5bd86fdcb947eb8a4f5b1c372125 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 05:17:19 -0400 Subject: [PATCH 09/16] security/meta,merge,raw,tiff,denoise: bound memory sized by attacker-shaped inputs XMP flattening, sparse-focus EXIF decode (plus the maker-note and TIFF value budgets), lossy-DNG chunk staging, panorama stitch frames, the reference ONNX executor's tensors and the denoise product reader all now reject or cap what a malformed file can make them allocate: each result is checked before the memory behind it is reserved, every regression carrying a test that fails without its bound. --- Cargo.lock | 1 + crates/denoise-core/Cargo.toml | 1 + crates/denoise-core/src/product.rs | 37 ++++++- crates/denoise-core/tests/product_heap.rs | 50 +++++++++ crates/denoise/src/reference.rs | 76 +++++++++++++- crates/merge/src/lib.rs | 2 + crates/merge/src/pano/camera.rs | 10 +- crates/merge/src/pano/mod.rs | 8 ++ crates/merge/tests/it/pano.rs | 12 +++ crates/meta/src/exif.rs | 12 ++- crates/meta/src/focus_read.rs | 10 +- crates/meta/src/lib.rs | 2 +- crates/meta/src/xmp.rs | 69 +++++++++++-- crates/raw/src/tiffraw.rs | 118 ++++++++++++++++------ crates/tiff/src/makernote.rs | 35 ++++++- crates/tiff/src/reader.rs | 19 ++++ 16 files changed, 406 insertions(+), 56 deletions(-) create mode 100644 crates/denoise-core/tests/product_heap.rs diff --git a/Cargo.lock b/Cargo.lock index 6e932cba8..9d7e4ec18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3221,6 +3221,7 @@ dependencies = [ name = "lightcraft-denoise-core" version = "0.14.0" dependencies = [ + "dhat", "lightcraft-raster", "miniz_oxide 0.8.9", "proptest", diff --git a/crates/denoise-core/Cargo.toml b/crates/denoise-core/Cargo.toml index b65f90ae4..d0a0b10c1 100644 --- a/crates/denoise-core/Cargo.toml +++ b/crates/denoise-core/Cargo.toml @@ -24,3 +24,4 @@ web-time = { workspace = true } [dev-dependencies] proptest = { workspace = true } +dhat = { workspace = true } diff --git a/crates/denoise-core/src/product.rs b/crates/denoise-core/src/product.rs index 5a607c8f4..c33b1d4c2 100644 --- a/crates/denoise-core/src/product.rs +++ b/crates/denoise-core/src/product.rs @@ -9,7 +9,7 @@ //! compressed length and CRC-32, then the strips' bytes. `key` says what the product was made from (the photo's //! content, the model, this code's version, the settings it depends on); a file with another key is stale. -use std::io::{Read, Write}; +use std::io::{Read, Seek, Write}; use std::path::Path; use lightcraft_raster::Rgb32f; @@ -216,6 +216,10 @@ fn read_start(r: &mut impl Read) -> Result { r.read_exact(&mut e)?; let l = u32::from_le_bytes([e[0], e[1], e[2], e[3]]) as usize; let c = u32::from_le_bytes([e[4], e[5], e[6], e[7]]); + // a strip never compresses to nothing (it has at least one row, so at least one deflated pixel) + if l == 0 { + return Err(ProductError::Format("a strip is empty".into())); + } // a strip never compresses to much more than its raw size if l > 12 * h.strip * h.width + 4096 { return Err(ProductError::Format("a strip is larger than it can be".into())); @@ -305,7 +309,6 @@ pub fn read_window(path: &Path, key: &str, window: Option, factor: usize } // the picture rows and columns the blocks cover let (x_end, y_end) = if whole { (w.x + w.width, w.y + w.height) } else { (w.x + ow * factor, w.y + oh * factor) }; - let mut out = Rgb32f { width: ow, height: oh, data: allocate(ow * oh, [0.0; 3])? }; let lut = f16_table(); let value = |raw: &[u8], c: usize, n: usize, i: usize| -> f32 { let (lo, hi) = (raw.get(2 * c * n + i).copied().unwrap_or(0), raw.get((2 * c + 1) * n + i).copied().unwrap_or(0)); @@ -318,6 +321,21 @@ pub fn read_window(path: &Path, key: &str, window: Option, factor: usize let strips: Vec = (0..table.len()).filter(|&s| wanted(s)).collect(); // skip the strips before the first one wanted let skipped: usize = table.iter().take(strips.first().copied().unwrap_or(0)).map(|e| e.0).sum(); + // The output is reserved only after the wanted strips' compressed bytes are present and match their checksums: + // a malformed cache whose strips are empty, damaged or short are rejected here, before the (large) result is + // allocated, not after. Reading them twice is cheap next to decoding them, and the fold below re-seeks here. + let strips_at = f.stream_position()?; + f.seek_relative(i64::try_from(skipped).map_err(|_| ProductError::Format("the file is too large".into()))?)?; + for &s in &strips { + let Some(&(len, expected)) = table.get(s) else { return Err(ProductError::Format("a strip is missing".into())) }; + let mut packed = allocate(len, 0u8)?; + f.read_exact(&mut packed)?; + if crc(&packed) != expected { + return Err(ProductError::Format(format!("strip {s} is damaged"))); + } + } + let mut out = Rgb32f { width: ow, height: oh, data: allocate(ow * oh, [0.0; 3])? }; + f.seek(std::io::SeekFrom::Start(strips_at))?; f.seek_relative(i64::try_from(skipped).map_err(|_| ProductError::Format("the file is too large".into()))?)?; for group in strips.chunks(GROUP) { let mut packed = Vec::with_capacity(group.len()); @@ -422,6 +440,21 @@ mod tests { let _ = std::fs::remove_file(p); } + #[test] + fn an_empty_strip_entry_is_rejected_by_the_header() { + let p = temp("empty-strip"); + let header = serde_json::to_vec(&Header { width: 16, height: 1, strip: STRIP_ROWS, key: "k".into() }).unwrap(); + let mut bytes = Vec::from(MAGIC.as_slice()); + bytes.extend_from_slice(&[VERSION, 0, 0, 0]); + bytes.extend_from_slice(&(header.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&header); + // one strip with a zero-length entry: it has a row, so its deflated bytes are never empty + bytes.extend_from_slice(&[0; 8]); + std::fs::write(&p, bytes).unwrap(); + assert!(matches!(read_header_at(&p), Err(ProductError::Format(why)) if why.contains("empty"))); + let _ = std::fs::remove_file(p); + } + fn temp(name: &str) -> std::path::PathBuf { std::env::temp_dir().join(format!("lc-denoise-{name}-{}.lcdn", std::process::id())) } diff --git a/crates/denoise-core/tests/product_heap.rs b/crates/denoise-core/tests/product_heap.rs new file mode 100644 index 000000000..40c170861 --- /dev/null +++ b/crates/denoise-core/tests/product_heap.rs @@ -0,0 +1,50 @@ +//! Peak heap of a `read_window` that must reject a product's strips (`crates/denoise-core/src/product.rs`), measured +//! with dhat's testing mode. The result is allocated only after the wanted strips' checksums are verified, so a cache +//! whose compressed bytes are damaged or absent fails without first reserving the (multi-megabyte) output. One test +//! per binary (dhat's profiler is global). + +use lightcraft_denoise_core::product::read_window; + +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + +fn temp(name: &str) -> std::path::PathBuf { + std::env::temp_dir().join(format!("lc-denoise-{name}-{}.lcdn", std::process::id())) +} + +/// A 2000 × 1024 product of key `k` (a 24 MB result): its 8 strips each claim 8 bytes that do not match their +/// checksum, and the file is exactly the length its header and table declare. A 24 MB peak means the output was +/// reserved; the cap here bounds the header, the table and the strips' own bytes. +fn damaged_product() -> Vec { + let header = br#"{"width":2000,"height":1024,"strip":128,"key":"k"}"#; + const STRIP_LEN: usize = 8; + let mut bytes = Vec::from(b"LCDN".as_slice()); + bytes.extend_from_slice(&[1, 0, 0, 0]); + bytes.extend_from_slice(&(header.len() as u32).to_le_bytes()); + bytes.extend_from_slice(header); + let expected_len = 12 + header.len() + 8 * 8 + 8 * STRIP_LEN; + for _ in 0..8 { + // 8 claimed bytes whose recorded checksum does not match theirs (0xAA bytes do not have this CRC) + bytes.extend_from_slice(&(STRIP_LEN as u32).to_le_bytes()); + bytes.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]); + } + for _ in 0..8 { + bytes.extend_from_slice(&[0xAAu8; STRIP_LEN]); + } + assert_eq!(bytes.len(), expected_len, "the crafted file lies about its length"); + bytes +} + +#[test] +fn a_damaged_product_is_rejected_before_its_output_is_allocated() { + let bytes = damaged_product(); + let p = temp("damaged-product-heap"); + std::fs::write(&p, &bytes).unwrap(); + let _profiler = dhat::Profiler::builder().testing().build(); + let why = read_window(&p, "k", None, 1, None).err().unwrap().to_string(); + let peak = dhat::HeapStats::get().max_bytes; + let _ = std::fs::remove_file(p); + assert!(why.contains("damaged"), "{why}"); + eprintln!("peak heap {peak} bytes"); + assert!(peak < 4 << 20, "peak heap {peak} bytes: the output was reserved before the strips were checked"); +} diff --git a/crates/denoise/src/reference.rs b/crates/denoise/src/reference.rs index ef58b5b9c..488e9a7b7 100644 --- a/crates/denoise/src/reference.rs +++ b/crates/denoise/src/reference.rs @@ -4,6 +4,12 @@ use crate::net::{Net, NetError, Op, TensorId}; use crate::run::{Error, TileRunner}; +/// The tensors' size cap: the same one the real runner's buffers share (`crates/denoise/src/cpu.rs`). A hostile `Net` +/// only ever has to satisfy the final shape: a `ConvT2` doubles the sides and a `MaxPool` halves them, so a small +/// accepted graph can zoom its intermediates far out and back. Every tensor is counted here, and a run whose layers +/// would retain more than one real tile's whole working set is refused before the memory behind it is reserved. +const MAX_VALUES: usize = 128 * 1024 * 1024; + /// A tensor in `[channels][side][side]` order. struct T { channels: usize, @@ -11,12 +17,36 @@ struct T { data: Vec, } +/// A tensor's plain float count, in bounds. +fn count(channels: usize, side: usize) -> Result { + channels + .checked_mul(side) + .and_then(|n| n.checked_mul(side)) + .filter(|&n| n > 0 && n <= MAX_VALUES) + .ok_or_else(|| NetError::Invalid("a tensor has no size or is too large".into())) +} + +/// A zeroed tensor, keeping the memory the run keeps alive in bounds (`tensors` retains every layer's result). +fn tensor(channels: usize, side: usize, total: &mut usize) -> Result, NetError> { + let n = count(channels, side)?; + *total = (*total).saturating_add(n); + if *total > MAX_VALUES { + return Err(NetError::Invalid("the tile needs too much working memory".into())); + } + let mut data = Vec::new(); + data.try_reserve_exact(n).map_err(|_| NetError::Invalid("not enough memory for a reference tile".into()))?; + data.resize(n, 0.0); + Ok(data) +} + /// Run `net` on `input` (`in_channels` planes of `tile × tile`). pub fn run(net: &Net, tile: usize, input: &[f32]) -> Result, NetError> { if !net.fits_tile(tile) || input.len() != net.in_channels * tile * tile { return Err(NetError::Invalid("the tile does not fit the network".into())); } - let mut tensors: Vec = vec![T { channels: net.in_channels, side: tile, data: input.to_vec() }]; + let input = input.to_vec(); + let mut total = input.len(); + let mut tensors = vec![T { channels: net.in_channels, side: tile, data: input }]; let get = |tensors: &[T], id: TensorId| -> Result { if id < tensors.len() { Ok(id) } else { Err(NetError::Invalid("a layer reads a tensor that does not exist yet".into())) } }; @@ -27,7 +57,7 @@ pub fn run(net: &Net, tile: usize, input: &[f32]) -> Result, NetError> let b = c.src2.map(|s| get(&tensors, s)).transpose()?; let side = tensors[a].side; let pad = (c.k - 1) / 2; - let mut data = vec![0f32; c.cout * side * side]; + let mut data = tensor(c.cout, side, &mut total)?; // the input channels in order: the first source's, then the second's let planes: Vec<&[f32]> = std::iter::once(&tensors[a]).chain(b.map(|b| &tensors[b])).flat_map(|t| t.data.chunks(side * side)).collect(); @@ -56,7 +86,7 @@ pub fn run(net: &Net, tile: usize, input: &[f32]) -> Result, NetError> let a = get(&tensors, c.src)?; let side = tensors[a].side; let out_side = 2 * side; - let mut data = vec![0f32; c.cout * out_side * out_side]; + let mut data = tensor(c.cout, out_side, &mut total)?; for co in 0..c.cout { for y in 0..side { for x in 0..side { @@ -77,7 +107,7 @@ pub fn run(net: &Net, tile: usize, input: &[f32]) -> Result, NetError> Op::MaxPool2 { src } => { let a = get(&tensors, *src)?; let (channels, side) = (tensors[a].channels, tensors[a].side / 2); - let mut data = vec![0f32; channels * side * side]; + let mut data = tensor(channels, side, &mut total)?; for ch in 0..channels { for y in 0..side { for x in 0..side { @@ -92,7 +122,7 @@ pub fn run(net: &Net, tile: usize, input: &[f32]) -> Result, NetError> let a = get(&tensors, *src)?; let (channels, side) = (tensors[a].channels / 4, tensors[a].side); let out_side = 2 * side; - let mut data = vec![0f32; channels * out_side * out_side]; + let mut data = tensor(channels, out_side, &mut total)?; for ch in 0..channels { for y in 0..side { for x in 0..side { @@ -131,3 +161,39 @@ impl TileRunner for ReferenceRunner { run(&self.net, self.tile, input).map_err(|e| Error::Runtime(e.to_string())) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::ConvT2; + + fn convt(src: TensorId, cin: usize, cout: usize) -> Op { + Op::ConvT2(ConvT2 { src, cin, cout, weight: vec![0.1; cin * cout * 4], bias: vec![0.0; cout], leaky: None }) + } + + /// A graph that fits the final-shape check while zooming its intermediates out and back: 256 `ConvT2`s double + /// the sides (256 tiers), 255 `MaxPool`s halve them back to the layer above the input. + fn hostile() -> Net { + let mut ops = Vec::new(); + let mut last = 0usize; + for _ in 0..256 { + ops.push(convt(last, 1, 1)); + last += 1; + } + for _ in 0..255 { + ops.push(Op::MaxPool2 { src: last }); + last += 1; + } + Net::new(1, ops).unwrap() + } + + /// The zoomed-out layers' memory was never reserved: either one tensor or the retained whole (the two bounds + /// share the cap) says why the run stops. + #[test] + fn a_graph_that_zooms_its_intermediates_out_and_back_is_refused_before_their_memory_is_reserved() { + assert!(matches!( + run(&hostile(), 1, &[1.0]), + Err(NetError::Invalid(why)) if why.contains("too large") || why.contains("too much working memory") + )); + } +} diff --git a/crates/merge/src/lib.rs b/crates/merge/src/lib.rs index b5ccd4bb8..ca7f91821 100644 --- a/crates/merge/src/lib.rs +++ b/crates/merge/src/lib.rs @@ -34,6 +34,8 @@ pub use pano::{PanoOptions, PanoResult, Projection, stitch}; pub enum MergeError { #[error("{0} photo(s) selected; at least {1} are needed")] TooFew(usize, usize), + #[error("{0} photo(s) selected; a stitch of more than {1} is refused: matching every pair would take far too long")] + TooMany(usize, usize), #[error("{0}")] Mismatch(String), #[error("could not read a photo: {0}")] diff --git a/crates/merge/src/pano/camera.rs b/crates/merge/src/pano/camera.rs index 7625170c3..4589f88db 100644 --- a/crates/merge/src/pano/camera.rs +++ b/crates/merge/src/pano/camera.rs @@ -61,10 +61,12 @@ pub struct PairMatch { /// Match every pair of images. `feats[k]` are in work coordinates scaled by `scales[k]`. pub fn match_pairs(feats: &[Features], scales: &[f64]) -> Vec { let n = feats.len(); - let pairs: Vec<(usize, usize)> = (0..n).flat_map(|i| (i + 1..n).map(move |j| (i, j))).collect(); - let mut out: Vec = pairs - .par_iter() - .filter_map(|&(i, j)| { + // the pairs are generated lazily: materialising all n(n−1)/2 up front is quadratic memory, including + // when every comparison comes up empty (no usable features) + let mut out: Vec = (0..n) + .into_par_iter() + .flat_map_iter(|i| ((i + 1)..n).map(move |j| (i, j))) + .filter_map(|(i, j)| { let m = features::match_features(&feats[i], &feats[j], 0.8); if m.len() < 12 { return None; diff --git a/crates/merge/src/pano/mod.rs b/crates/merge/src/pano/mod.rs index 5ce439681..ba7e1c63f 100644 --- a/crates/merge/src/pano/mod.rs +++ b/crates/merge/src/pano/mod.rs @@ -53,6 +53,11 @@ impl Default for PanoOptions { /// The largest panorama, in megapixels (Lightroom's limit too); a bigger one is scaled down. pub const MAX_MEGAPIXELS: f64 = 512.0; +/// Most frames one stitch accepts: pair matching compares every pair of frames (n(n−1)/2 runs), so an +/// unbounded frame count is quadratic CPU as well as quadratic memory while the frames load. A panorama +/// with more than [`MAX_FRAMES`] shots is refused with an error, not attempted. +pub const MAX_FRAMES: usize = 256; + /// The longest edge of a panorama, in pixels (Lightroom's limit too); a longer one is scaled down. pub const MAX_EDGE: f64 = 65_000.0; @@ -224,6 +229,9 @@ pub fn stitch(mut frames: Vec, opts: &PanoOptions, progress: &Progress) - if n < 2 { return Err(MergeError::TooFew(n, 2)); } + if n > MAX_FRAMES { + return Err(MergeError::TooMany(n, MAX_FRAMES)); + } check(progress, 0.01, "Finding features")?; let (cams, rms_before, rms) = register(&frames, progress)?; let used: Vec = (0..n).filter(|&i| cams[i].is_some()).collect(); diff --git a/crates/merge/tests/it/pano.rs b/crates/merge/tests/it/pano.rs index fdb736524..aeecde64c 100644 --- a/crates/merge/tests/it/pano.rs +++ b/crates/merge/tests/it/pano.rs @@ -246,6 +246,18 @@ fn unrelated_photos_do_not_stitch() { assert!(stitch(frames, &PanoOptions::default(), &no_progress).is_err()); } +/// Pair matching compares every pair of frames before anything is known about them: an unbounded +/// photo count is quadratic CPU and memory (issue #1519). More than `MAX_FRAMES` is refused at once, +/// before any frame is worked on. +#[test] +fn a_stitch_of_more_than_the_frame_cap_is_refused_up_front() { + let frames: Vec = (0..lightcraft_merge::pano::MAX_FRAMES + 1) + .map(|i| Frame::from_linear_rec2020(Rgb32f::from_fn(8, 8, |_, _| [(i % 255) as f32 / 255.0, 0.0, 0.0]), None)) + .collect(); + let err = stitch(frames, &PanoOptions::default(), &no_progress).unwrap_err(); + assert!(err.to_string().contains("refused"), "the frame cap errors, it does not begin the quadratic match: {err}"); +} + /// Issue #1455: a panorama came out at 40 MP whatever its photos' resolution. It now keeps their /// own resolution unless a size is asked for (up to Lightroom's 512 MP); a size scales it down, /// and a NaN one (from an agent) is no size at all, not a 0.1 MP thumbnail. diff --git a/crates/meta/src/exif.rs b/crates/meta/src/exif.rs index f24bb5f2f..adcba0f80 100644 --- a/crates/meta/src/exif.rs +++ b/crates/meta/src/exif.rs @@ -1,7 +1,7 @@ //! Exif / TIFF tags → [`Metadata`] (CIPA DC-008 Exif 2.32, TIFF 6.0, DNG 1.7 tag semantics). use crate::{DateTime, Flash, Gps, Metadata, Orientation}; -use lightcraft_tiff::{Ifd, Tiff, TiffError, tags as t}; +use lightcraft_tiff::{Ifd, ParseOptions, Tiff, TiffError, tags as t}; /// Strip an `Exif\0\0` (or `Exif\0\xff`) prefix as found in JPEG APP1 and some WebP `EXIF` chunks. pub fn strip_exif_header(b: &[u8]) -> &[u8] { @@ -16,8 +16,16 @@ pub fn read_exif(bytes: &[u8]) -> Metadata { /// Like [`read_exif`] but reports a structurally invalid TIFF header / IFD0. pub fn try_read_exif(bytes: &[u8]) -> Result { + try_read_exif_with(bytes, &ParseOptions::default()) +} + +/// [`try_read_exif`] with the caller's parser options (its value allocation budget in particular). +/// A decoder working from a sparse buffer — only selected ranges of the logical file are actually +/// loaded, the rest is zeros — must not size its allocations by the logical file length, which +/// unrelated bytes of the file can inflate at no cost to the attacker. +pub fn try_read_exif_with(bytes: &[u8], opts: &ParseOptions) -> Result { let data = strip_exif_header(bytes); - let tiff = Tiff::parse(data)?; + let tiff = Tiff::parse_with(data, opts)?; let mut m = from_tiff(&tiff); // Panasonic writes no LensModel, only the name in its maker note (and in a RW2 that note is not in the raw's own Exif) if m.lens_model.is_none() { diff --git a/crates/meta/src/focus_read.rs b/crates/meta/src/focus_read.rs index 5a5d349d1..65adbee75 100644 --- a/crates/meta/src/focus_read.rs +++ b/crates/meta/src/focus_read.rs @@ -171,7 +171,15 @@ fn tiff(r: &mut Reader, head: Vec) -> io::Result> { loaded.insert(0, head.len() as u64); for _ in 0..MAX_ROUNDS { match plan(&buf, &loaded) { - Step::Ready => return Ok(crate::try_read_exif(&buf).ok().and_then(|m| m.focus)), + Step::Ready => { + // The decoder's allocation budget follows the bytes actually fetched, not the logical + // file length: only the loaded ranges are data, and an unrelated entry that claims a + // huge value must be cut by what leaving it cost, not by what the buffer claims to hold. + // The same 8 × / + 1 MiB shape the general TIFF reader uses, over `reader.bytes` instead. + let opts = + lightcraft_tiff::ParseOptions { value_budget: Some(r.bytes.saturating_mul(8).saturating_add(1 << 20)), ..Default::default() }; + return Ok(crate::try_read_exif_with(&buf, &opts).ok().and_then(|m| m.focus)); + } Step::Nothing => return Ok(None), Step::Need(ranges) => { for (a, b) in ranges { diff --git a/crates/meta/src/lib.rs b/crates/meta/src/lib.rs index baebbfe0f..46c7a1043 100644 --- a/crates/meta/src/lib.rs +++ b/crates/meta/src/lib.rs @@ -27,7 +27,7 @@ mod xmp_merge; pub use container::{Embedded, embedded, jpeg_segments, png_chunks, webp_chunks}; pub use datetime::DateTime; -pub use exif::{from_tiff, read_exif, strip_exif_header, try_read_exif, write_exif}; +pub use exif::{from_tiff, read_exif, strip_exif_header, try_read_exif, try_read_exif_with, write_exif}; pub use focus::FocusPoint; pub use gpx::{GpxError, Match, TrackPoint, Tracklog, parse_gpx}; pub use iptc::parse_iptc; diff --git a/crates/meta/src/xmp.rs b/crates/meta/src/xmp.rs index 2dbf59482..d2f6f8ee2 100644 --- a/crates/meta/src/xmp.rs +++ b/crates/meta/src/xmp.rs @@ -154,6 +154,11 @@ const MAX_NODES: usize = 200_000; // Expanding a long namespace URI on many short names must not amplify a small packet without bound. pub(crate) const MAX_OPAQUE_NAME_BYTES: usize = 8 * 1024 * 1024; +/// Most text a converter's flattened property keys may add up to. Keys repeat their whole ancestor +/// path (`parent/field`), so a document with a long nested name and a wide child fan-out otherwise +/// retains its prefix once per property: node limits alone cannot bound that duplication. +const MAX_KEY_BYTES: usize = 1 << 20; + /// Names canonicalised by namespace URI (see [`NAMESPACES`]); conflicting foreign prefixes are opaque. pub(crate) fn canonical_name(qname: &str, scopes: &[Vec<(String, String)>], opaque_budget: &mut usize) -> Result { canonical(qname, scopes, opaque_budget) @@ -332,18 +337,29 @@ fn is_rdf_meta_attr(k: &str) -> bool { matches!(k, "rdf:about" | "rdf:ID" | "rdf:nodeID" | "xml:lang" | "rdf:parseType" | "rdf:resource" | "rdf:datatype") || k.starts_with("xmlns") } -fn collect_description(d: &Node, prefix: &str, props: &mut BTreeMap>) { +fn collect_description(d: &Node, prefix: &str, props: &mut BTreeMap>, budget: &mut usize) { for (k, v) in &d.attrs { - if !is_rdf_meta_attr(k) { + if !is_rdf_meta_attr(k) && take_key_bytes(&format!("{prefix}{k}"), budget) { props.entry(format!("{prefix}{k}")).or_default().push(v.clone()); } } for c in &d.children { - collect_property(c, prefix, props); + collect_property(c, prefix, props, budget); } } -fn collect_property(p: &Node, prefix: &str, props: &mut BTreeMap>) { +/// Charge one flattened key's length to the key budget; `false` when the budget is spent (the +/// property is then left out, like other elements a limit has cut). +fn take_key_bytes(key: &str, budget: &mut usize) -> bool { + let n = key.len(); + if *budget < n { + return false; + } + *budget -= n; + true +} + +fn collect_property(p: &Node, prefix: &str, props: &mut BTreeMap>, budget: &mut usize) { let key = format!("{prefix}{}", p.name); if let Some(arr) = p.children.iter().find(|c| matches!(c.name.as_str(), "rdf:Seq" | "rdf:Bag" | "rdf:Alt")) { let mut items: Vec<(bool, String)> = arr @@ -357,29 +373,36 @@ fn collect_property(p: &Node, prefix: &str, props: &mut BTreeMap = p.attrs.iter().filter(|(k, _)| !is_rdf_meta_attr(k)).collect(); if !field_attrs.is_empty() && p.children.is_empty() && p.text.trim().is_empty() { for (k, v) in field_attrs { - props.entry(format!("{struct_prefix}{k}")).or_default().push(v.clone()); + let full = format!("{struct_prefix}{k}"); + if take_key_bytes(&full, budget) { + props.entry(full).or_default().push(v.clone()); + } } return; } let v = p.attr("rdf:resource").map(str::to_string).unwrap_or_else(|| p.text.trim().to_string()); - props.entry(key).or_default().push(v); + if take_key_bytes(&key, budget) { + props.entry(key).or_default().push(v); + } } fn find_descriptions<'a>(n: &'a Node, out: &mut Vec<&'a Node>) { @@ -498,7 +521,8 @@ pub fn parse_xmp(s: &str) -> Result { let mut props = BTreeMap::new(); let mut values = BTreeMap::new(); for d in descs { - collect_description(d, "", &mut props); + let mut key_budget = MAX_KEY_BYTES; + collect_description(d, "", &mut props, &mut key_budget); if let XmpValue::Struct(m) = node_struct(d) { values.extend(m); } @@ -1171,4 +1195,29 @@ mod tests { assert_eq!(entity("#x41"), Some('A')); assert_eq!(entity("#9"), Some('\t')); } + + /// Flattened keys repeat their whole ancestor path, so a long nested name with a wide child + /// fan-out would otherwise retain its prefix once per leaf: the cumulative key budget stops it + /// (a 65 KB document with 3000 leaves must not retain ~2.4 MB of key text). + #[test] + fn flattened_keys_stay_within_the_cumulative_budget() { + let level = "n".repeat(200); + let mut doc = String::from( + "", + ); + for _ in 0..4 { + doc.push_str(&format!("")); + } + doc.push_str(&"".repeat(3000)); + doc.push_str(""); + for _ in 0..4 { + doc.push_str(&format!("")); + } + doc.push_str(""); + let d = parse_xmp(&doc).unwrap(); + let total: usize = d.properties.keys().map(String::len).sum(); + assert!(total <= MAX_KEY_BYTES, "flattened-key text {total} is over the {MAX_KEY_BYTES} budget"); + assert!(d.properties.len() < 3000, "keys stop being added once the budget is spent"); + assert!(!d.properties.is_empty(), "properties parsed before the budget are kept"); + } } diff --git a/crates/raw/src/tiffraw.rs b/crates/raw/src/tiffraw.rs index 8cd6a13e2..34bd7e53b 100644 --- a/crates/raw/src/tiffraw.rs +++ b/crates/raw/src/tiffraw.rs @@ -105,48 +105,72 @@ pub fn read_image(data: &[u8], info: &ImageInfo, order: ByteOrder, packing: Pack let chunks = info.chunks(data.len() as u64); let planar = info.planar == 2 && cpp > 1; let ccpp = if planar { 1 } else { cpp }; - let decoded: Vec> = - chunks.par_iter().map(|c| decode_chunk(data, info, c, order, packing, ccpp, bits, float).map(|p| (*c, p))).collect(); + // The chunks are decoded and copied into the output in groups whose combined nominal size stays within + // a few times the output buffer's: everything decoded at once would retain the whole image twice, and + // a hostile tile table's chosen padding could stretch that much further (issue #1517). One group + // still decodes in parallel; the grid order groups make, the copies stay disjoint. + let group_samples = (total / 4).max(1 << 20); let mut out_u16 = if float { Vec::new() } else { vec![0u16; total] }; let mut out_f32 = if float { vec![0f32; total] } else { Vec::new() }; - for r in decoded { - // a chunk that cannot be decoded is not a partial image: the file stays preview-only - let (c, px) = r?; - let (cw, ch) = (c.width as usize, c.height as usize); - let (x0, y0) = (c.x as usize, c.y as usize); - if x0 >= w || y0 >= h { - continue; + let mut decode_from = 0usize; + while decode_from < chunks.len() { + let mut to = decode_from; + let mut taken = 0usize; + for (k, c) in chunks[decode_from..].iter().enumerate() { + let n = chunk_samples(c, ccpp); + if taken > 0 && taken + n > group_samples { + break; + } + taken = taken.saturating_add(n); + to = decode_from + k + 1; } - let copy_w = cw.min(w - x0); - let copy_h = ch.min(h - y0); - for y in 0..copy_h { - for x in 0..copy_w { - for s in 0..ccpp { - let src = (y * cw + x) * ccpp + s; - let dst_s = if planar { c.plane as usize } else { s }; - if dst_s >= cpp { - continue; - } - let dst = ((y0 + y) * w + x0 + x) * cpp + dst_s; - match &px { - ChunkPx::U16(v) => { - if let (Some(o), Some(&s)) = (out_u16.get_mut(dst), v.get(src)) { - *o = s; - } + let decoded: Vec> = + chunks[decode_from..to].par_iter().map(|c| decode_chunk(data, info, c, order, packing, ccpp, bits, float).map(|p| (*c, p))).collect(); + for r in decoded { + // a chunk that cannot be decoded is not a partial image: the file stays preview-only + let (c, px) = r?; + let (cw, ch) = (c.width as usize, c.height as usize); + let (x0, y0) = (c.x as usize, c.y as usize); + if x0 >= w || y0 >= h { + continue; + } + let copy_w = cw.min(w - x0); + let copy_h = ch.min(h - y0); + for y in 0..copy_h { + for x in 0..copy_w { + for s in 0..ccpp { + let src = (y * cw + x) * ccpp + s; + let dst_s = if planar { c.plane as usize } else { s }; + if dst_s >= cpp { + continue; } - ChunkPx::F32(v) => { - if let (Some(o), Some(&s)) = (out_f32.get_mut(dst), v.get(src)) { - *o = s; + let dst = ((y0 + y) * w + x0 + x) * cpp + dst_s; + match &px { + ChunkPx::U16(v) => { + if let (Some(o), Some(&s)) = (out_u16.get_mut(dst), v.get(src)) { + *o = s; + } + } + ChunkPx::F32(v) => { + if let (Some(o), Some(&s)) = (out_f32.get_mut(dst), v.get(src)) { + *o = s; + } } } } } } } + decode_from = to; } Ok(if float { RawData::F32(out_f32) } else { RawData::U16(out_u16) }) } +/// A chunk's nominal decoded size in samples (per plane, as the decoder builds it). +fn chunk_samples(c: &Chunk, ccpp: usize) -> usize { + (c.width as usize).saturating_mul(c.height as usize).saturating_mul(ccpp) +} + #[allow(clippy::too_many_arguments)] fn decode_chunk(data: &[u8], info: &ImageInfo, c: &Chunk, order: ByteOrder, packing: Packing, cpp: usize, bits: u32, float: bool) -> Result { let src = chunk_bytes(data, c).ok_or_else(|| RawError::Corrupt("chunk offset past end of file".into()))?; @@ -180,8 +204,12 @@ fn decode_chunk(data: &[u8], info: &ImageInfo, c: &Chunk, order: ByteOrder, pack comp::LOSSY_JPEG => { // lossy DNG: each chunk is a baseline (DCT) JPEG of 8-bit samples let (px, jw, jh, jc) = lossy_jpeg(src, cpp, MAX_JPEG_SAMPLES)?; - let mut v = vec![0u16; n]; - for y in 0..ch.min(jh) { + // the buffer holds the rows the decoded JPEG covers (the caller zero-fills the tile's + // remainder and reads only rows it fills): a tile-many-larger decoded JPEG does not + // get the nominal tile's full buffer (issue #1517) + let rows = ch.min(jh); + let mut v = vec![0u16; rows.max(1).saturating_mul(cw).saturating_mul(cpp)]; + for y in 0..rows { for x in 0..cw.min(jw) { for k in 0..cpp { v[(y * cw + x) * cpp + k] = px[(y * jw + x) * jc + k.min(jc - 1)] as u16; @@ -518,6 +546,36 @@ mod lossy_tests { assert!(lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES).is_ok() || matches!(lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES), Err(RawError::Corrupt(_)))); } + /// A lossy tile whose JPEG decodes far less than the nominal tile is fine: the rows the JPEG + /// covers hold its pixels, the rest of the output stays zero (and the tile's intermediate buffer + /// is sized by the JPEG, not by the nominal tile). + #[test] + fn a_small_lossy_jpeg_in_a_large_nominal_tile_decodes_with_zeros_below() { + let px: Vec = (0..8 * 8).flat_map(|_| [30u8, 90, 150]).collect(); + let mut j = Vec::new(); + jpeg_encoder::Encoder::new(&mut j, 100).encode(&px, 8, 8, jpeg_encoder::ColorType::Rgb).unwrap(); + let info = ImageInfo { + width: 16, + height: 16, + bits_per_sample: vec![8, 8, 8], + samples_per_pixel: 3, + compression: comp::LOSSY_JPEG, + photometric: 34892, + planar: 1, + predictor: 1, + sample_format: 1, + new_subfile_type: 0, + layout: Layout::Tiles { tile_width: 16, tile_height: 16 }, + offsets: vec![0], + byte_counts: vec![j.len() as u64], + }; + let RawData::U16(v) = read_image(&j, &info, ByteOrder::Little, Packing::Msb).unwrap() else { panic!("integer samples") }; + assert_eq!(v.len(), 16 * 16 * 3, "the output is the full nominal image"); + let px = |x: usize, y: usize| &v[(y * 16 + x) * 3..(y * 16 + x) * 3 + 3]; + assert!((px(3, 3)[0] as i32 - 30).abs() <= 3 && (px(3, 3)[2] as i32 - 150).abs() <= 3, "covered rows: {:?}", px(3, 3)); + assert!(px(3, 12).iter().all(|&s| s == 0), "rows below the JPEG stay zero: {:?}", px(3, 12)); + } + /// A deflated tile is refused when its packed bytes are too few for deflate to have produced /// the nominal chunk, or when the stream stops short of the rows inside the image; a fully /// covered flat tile still decodes. diff --git a/crates/tiff/src/makernote.rs b/crates/tiff/src/makernote.rs index 7ed5e1997..288b366f6 100644 --- a/crates/tiff/src/makernote.rs +++ b/crates/tiff/src/makernote.rs @@ -55,7 +55,11 @@ pub fn parse_makernote(data: &[u8], offset: u64, len: u64, order: ByteOrder, mak Some(b"MM") => Some(ByteOrder::Big), _ => None, }; - let opts = ParseOptions { max_ifds: 64, max_depth: 3, ..Default::default() }; + // A fixed value budget: a maker note is at most tens of kilobytes of values in any honest file, and + // computing the budget from the buffer's length multiplies the file length (a hostile entry in a + // sparse header-read buffer, where the buffer holds the file's logical size but only a few + // kilobytes are loaded, would decode allocations no actual data backs). + let opts = ParseOptions { max_ifds: 64, max_depth: 3, value_budget: Some(1 << 20), ..Default::default() }; let try_parse = |kind: MakerNoteKind, ifd_at: u64, base: u64, ord: ByteOrder| -> Option { let (ifd, _) = parse_ifd_at(data, ifd_at, ord, base, false, &opts).ok()?; (!ifd.entries.is_empty()).then_some(MakerNote { kind, order: ord, base, ifd }) @@ -192,6 +196,35 @@ mod tests { assert_eq!(m.ifd.u64s(2).unwrap(), vec![9, 0]); } + /// A maker note's value budget is fixed, not computed from the length of the buffer it sits in: + /// an entry claiming more than the note budget (here 2 MB) is dropped, a fitting one is read. In a + /// sparse header read the buffer holds the file's logical size while only kilobytes were loaded, + /// so a length-scaled budget would let an unrelated entry decode megabytes of zeros (see the focus + /// reader, try_read_exif_with). + #[test] + fn a_note_entry_past_the_fixed_value_budget_is_dropped() { + let order = ByteOrder::Little; + let mut note = b"SONY DSC \0\0\0".to_vec(); + // the Sony IFD directly after the 12-byte header, one fitting BYTE entry and a decoy + // BYTE[2 MB] entry pointing into the file's own (zero-filled) body + order.put_u16(&mut note, 2); + order.put_u16(&mut note, 0x201b); + order.put_u16(&mut note, 1); + order.put_u32(&mut note, 1); + note.extend([3, 0, 0, 0]); + order.put_u16(&mut note, 0x9900); + order.put_u16(&mut note, 1); + order.put_u32(&mut note, 2 << 20); + order.put_u32(&mut note, 200); + order.put_u32(&mut note, 0); + let mut bytes = note.clone(); + bytes.resize(200 + (2 << 20), 0); // the decoy's value range is inside the real bytes + let m = parse_makernote(&bytes, 0, note.len() as u64, order, "SONY").unwrap(); + assert_eq!(m.kind, MakerNoteKind::Sony); + assert!(m.ifd.get(0x201b).is_some(), "the fitting entry is read"); + assert!(m.ifd.get(0x9900).is_none(), "the decoy is dropped by the value budget"); + } + #[test] fn garbage_note_is_none() { let (bytes, off, len) = with_note(ByteOrder::Little, vec![0xff; 40]); diff --git a/crates/tiff/src/reader.rs b/crates/tiff/src/reader.rs index bd728ca9c..70c66c5a7 100644 --- a/crates/tiff/src/reader.rs +++ b/crates/tiff/src/reader.rs @@ -338,6 +338,25 @@ pub fn decode(o: ByteOrder, typ: FieldType, b: &[u8]) -> Value { #[cfg(test)] mod tests { use super::*; + use crate::Value; + use crate::tags as t; + use crate::writer::{IfdBuilder, TiffWriter}; + + /// A value budget the caller sets bounds what is decoded: an entry claiming more value bytes than + /// the budget allows is dropped, entries that fit are read (the sparse header-read path, where the + /// buffer holds the file's logical size but only a few kilobytes were ever loaded, sizes its + /// budget from what was fetched, not from whatever length the file claims). + #[test] + fn the_value_budget_drops_over_budget_entries_and_keeps_the_rest() { + let mut ifd0 = IfdBuilder::new(); + ifd0.set(t::MAKE, Value::Ascii("Keep".into())); + ifd0.set(0x9000, Value::Byte(vec![0; 2 << 20])); + let file = TiffWriter { order: ByteOrder::Little, bigtiff: false }.write(&[ifd0]).unwrap(); + let opts = ParseOptions { value_budget: Some(1 << 20), ..Default::default() }; + let tiff = Tiff::parse_with(&file, &opts).unwrap(); + assert_eq!(tiff.ifds[0].string(t::MAKE), Some("Keep".to_string()), "a fitting entry is read"); + assert!(tiff.ifds[0].get(0x9000).is_none(), "the over-budget entry is dropped"); + } /// A BigTIFF whose first IFD, at 80, declares `declared` entries but holds one, followed by a next-IFD pointer /// `next`; a valid one-entry IFD sits at 16. From 269495cd3867e9feee1876ac641627ba23c7dabb Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 15:45:31 -0400 Subject: [PATCH 10/16] security(heif): budget every reconstruction before it runs The primary item and the selected thumbnail are scanned before Context::grid rebuilds their descriptors, every tile reference is counted (repeats included) before the size check dedupes them, and the Exif and XMP items are scanned before they are read. --- crates/heif/src/lib.rs | 48 +++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/crates/heif/src/lib.rs b/crates/heif/src/lib.rs index 723ffe5d9..6491489fb 100644 --- a/crates/heif/src/lib.rs +++ b/crates/heif/src/lib.rs @@ -158,27 +158,28 @@ fn probe_unguarded(bytes: &[u8]) -> Result { })?; let ctx = Context::open(bytes).map_err(err)?; let id = ctx.meta.primary; + // heic-rs 0.1.4 caps the size of what it assembles, not the work: an item built from other + // items (`iloc` construction method 2) is rebuilt once per extent, so a chain of such items + // multiplies at every level while each result stays small. Count those visits from the + // `iloc` table before reconstructing anything, the primary item (whose grid descriptor + // `Context::grid` rebuilds) first and then every tile reference, repeats included. + let mut visits = 0_usize; + item_visits(&ctx, id, 0, &mut visits)?; let mut coded = match ctx.grid(id).map_err(err)? { Some((_, tiles)) => tiles, None => vec![id], }; - // A `dimg` listing may name the same tile more than once; reconstrutable (`iloc` - // construction method 2) items are moreover rebuilt for every extent that references - // them in heic-rs 0.1.4, so our probe would pay the reconstruction again per tile. Each - // unique item only once, then the total reconstruction (all of it assembled from `bytes` - // by heic-rs) is held below one copy of the file — above that a repeat or chain wants - // more bytes than exist (CodeRabbit #21/#50 on this sweep). Order doesn't matter here: - // this only checks that every tile's bytes lie in the file. - coded.sort_unstable(); - coded.dedup(); - // heic-rs 0.1.4 caps the size of what it assembles, not the work: an item built from other - // items (`iloc` construction method 2) is rebuilt once per extent, so a chain of such items - // multiplies at every level while each result stays small. Count those visits from the - // `iloc` table before reconstructing anything. - let mut visits = 0_usize; - for &item in &coded { + for &item in coded.iter().filter(|&&item| item != id) { item_visits(&ctx, item, 0, &mut visits)?; } + // A `dimg` listing may name the same tile more than once, and reconstructable items are + // moreover rebuilt for every extent that references them in heic-rs 0.1.4. For the size + // check each unique item counts once: the total reconstruction (all of it assembled from + // `bytes` by heic-rs) is held below one copy of the file, above which a repeat or chain + // wants more bytes than exist. Order doesn't matter here: this only checks that every + // tile's bytes lie in the file. + coded.sort_unstable(); + coded.dedup(); let mut reconstructed = 0_usize; for item in coded { reconstructed = reconstructed.saturating_add(ctx.item_data(item).map_err(err)?.len()); @@ -252,6 +253,15 @@ pub fn decode_thumbnail(bytes: &[u8], min_edge: u32, options: &Options) -> Resul } } let Some((id, _)) = best else { return Ok(None) }; + // The same pre-scan as `probe`: the thumbnail's own descriptor and every tile reference + // are counted before heic-rs rebuilds any of them. + let mut visits = 0_usize; + item_visits(&ctx, id, 0, &mut visits)?; + if let Some((_, tiles)) = ctx.grid(id).map_err(err)? { + for &tile in &tiles { + item_visits(&ctx, tile, 0, &mut visits)?; + } + } let mut decoded = decode_item(&ctx, id, options, false)?; if decoded.icc.is_none() { decoded.icc = item_icc(&ctx, primary); @@ -444,8 +454,12 @@ fn colour(p: &ItemProps<'_>, coded: &ItemProps<'_>) -> Colour { /// metadata item never fails a decode whose pixels came out fine. fn metadata(ctx: &Context<'_>) -> (Option>, Option) { let id = ctx.meta.primary; - let exif = ctx.exif(id).ok().flatten().map(<[u8]>::to_vec); - let xmp = ctx.xmp_item(id).and_then(|x| ctx.item_data(x).ok()).and_then(|d| match d { + // Metadata is best effort, so an item that would cost too much to rebuild is skipped, not an error. + let mut visits = 0_usize; + let cheap = |item: Option, visits: &mut usize| item.is_none_or(|item| item_visits(ctx, item, 0, visits).is_ok()); + let exif = if cheap(ctx.exif_item(id), &mut visits) { ctx.exif(id).ok().flatten().map(<[u8]>::to_vec) } else { None }; + let xmp_item = ctx.xmp_item(id).filter(|&x| cheap(Some(x), &mut visits)); + let xmp = xmp_item.and_then(|x| ctx.item_data(x).ok()).and_then(|d| match d { Cow::Borrowed(b) => String::from_utf8(b.to_vec()).ok(), Cow::Owned(b) => String::from_utf8(b).ok(), }); From 0c71f75325b5f707bbe8e7ef6324e6e14a8f51e6 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 16:06:39 -0400 Subject: [PATCH 11/16] security(heif): scan the primary item before heic_rs::probe, the alpha plane, and each thumbnail candidate heic_rs::probe rebuilds a grid primary's descriptor, so the pre-scan runs before it; the alpha item that decode reconstructs is counted too; and a thumbnail candidate is scanned before its coded size is resolved, with all candidates sharing one budget. --- crates/heif/src/lib.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/heif/src/lib.rs b/crates/heif/src/lib.rs index 6491489fb..5b20e31d7 100644 --- a/crates/heif/src/lib.rs +++ b/crates/heif/src/lib.rs @@ -151,20 +151,25 @@ pub fn probe(bytes: &[u8]) -> Result { fn probe_unguarded(bytes: &[u8]) -> Result { census::cap(bytes)?; - let info = heic_rs::probe(bytes).map_err(|e| match e { - // No `meta` box: no still image, only a `moov` image sequence (a video track). + // No `meta` box: no still image, only a `moov` image sequence (a video track). + let open_error = |e| match e { heic_rs::Error::MissingBox("meta") => Error::Unsupported(SEQUENCE), e => err(e), - })?; - let ctx = Context::open(bytes).map_err(err)?; + }; + let ctx = Context::open(bytes).map_err(open_error)?; let id = ctx.meta.primary; // heic-rs 0.1.4 caps the size of what it assembles, not the work: an item built from other // items (`iloc` construction method 2) is rebuilt once per extent, so a chain of such items // multiplies at every level while each result stays small. Count those visits from the - // `iloc` table before reconstructing anything, the primary item (whose grid descriptor - // `Context::grid` rebuilds) first and then every tile reference, repeats included. + // `iloc` table before reconstructing anything: the primary item first (`heic_rs::probe` and + // `Context::grid` both rebuild its descriptor), then its alpha plane, then every tile + // reference, repeats included. let mut visits = 0_usize; item_visits(&ctx, id, 0, &mut visits)?; + let info = heic_rs::probe(bytes).map_err(open_error)?; + if let Some(alpha) = ctx.alpha_item(id).map_err(err)? { + item_visits(&ctx, alpha, 0, &mut visits)?; + } let mut coded = match ctx.grid(id).map_err(err)? { Some((_, tiles)) => tiles, None => vec![id], @@ -244,7 +249,13 @@ pub fn decode_thumbnail(bytes: &[u8], min_edge: u32, options: &Options) -> Resul let ctx = Context::open(bytes).map_err(err)?; let primary = ctx.meta.primary; let mut best: Option<(u32, u32)> = None; + // A thumbnail whose own reconstruction would cost too much is not a candidate; the visits + // of all the candidates share one budget. + let mut candidate_visits = 0_usize; for r in ctx.meta.refs.iter().filter(|r| r.kind == heic_rs::meta::iref::RefKind::Thmb && r.to.contains(&primary)) { + if item_visits(&ctx, r.from, 0, &mut candidate_visits).is_err() { + continue; + } let Ok(p) = ctx.props(r.from) else { continue }; let Ok((w, h)) = ctx.coded_size(r.from, &p) else { continue }; let edge = w.max(h); From 3022baa8615a2956e239b7febf3fe4ff98289885 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 16:12:03 -0400 Subject: [PATCH 12/16] security(raw,meta): follow-ups from review An oversized CR3 candidate is skipped without ending the search, repeated child IFD references count once toward the pending cap, deflated chunks lay their rows out as the unpacker reads them (16-bit words, 32-bit word alignment), the tile grid's nominal size is checked against the image before anything is decoded, chunk grouping saturates, and the XMP flattened-key budget is document-wide. The JPEG-header test now parses a real header. --- crates/meta/src/xmp.rs | 8 ++++-- crates/raw/src/preview.rs | 5 ++-- crates/raw/src/sparse.rs | 8 +++++- crates/raw/src/tiffraw.rs | 60 +++++++++++++++++++++++++++++---------- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/crates/meta/src/xmp.rs b/crates/meta/src/xmp.rs index d2f6f8ee2..ac0a8808a 100644 --- a/crates/meta/src/xmp.rs +++ b/crates/meta/src/xmp.rs @@ -520,8 +520,9 @@ pub fn parse_xmp(s: &str) -> Result { find_descriptions(&tree, &mut descs); let mut props = BTreeMap::new(); let mut values = BTreeMap::new(); + // One budget for the whole document: several descriptions cannot each spend a fresh one. + let mut key_budget = MAX_KEY_BYTES; for d in descs { - let mut key_budget = MAX_KEY_BYTES; collect_description(d, "", &mut props, &mut key_budget); if let XmpValue::Struct(m) = node_struct(d) { values.extend(m); @@ -1208,7 +1209,10 @@ mod tests { for _ in 0..4 { doc.push_str(&format!("")); } - doc.push_str(&"".repeat(3000)); + // each leaf has a name of its own, so each one adds a key + for i in 0..3000 { + doc.push_str(&format!("")); + } doc.push_str(""); for _ in 0..4 { doc.push_str(&format!("")); diff --git a/crates/raw/src/preview.rs b/crates/raw/src/preview.rs index 8a7e180c7..4bcf6963e 100644 --- a/crates/raw/src/preview.rs +++ b/crates/raw/src/preview.rs @@ -400,7 +400,7 @@ const CR3_MAX_WALK: usize = 8 << 20; fn cr3_preview_boxes(bytes: &[u8]) -> Option<&[u8]> { let mut best: Option<&[u8]> = None; let mut walked = 0usize; - 'tags: for tag in [b"PRVW", b"THMB"] { + for tag in [b"PRVW", b"THMB"] { let mut from = 0; while let Some(i) = bytes.get(from..).and_then(|s| s.windows(4).position(|w| w == tag)).map(|p| p + from) { from = i + 4; @@ -412,8 +412,9 @@ fn cr3_preview_boxes(bytes: &[u8]) -> Option<&[u8]> { continue; } let jpeg = &bytes[j..j + len]; + // One that doesn't fit the budget left is skipped; a smaller one later may still fit. if walked + jpeg.len() > CR3_MAX_WALK { - break 'tags; + continue; } walked += jpeg.len(); if is_dct_jpeg(jpeg) { diff --git a/crates/raw/src/sparse.rs b/crates/raw/src/sparse.rs index a1a718f14..8a404a25e 100644 --- a/crates/raw/src/sparse.rs +++ b/crates/raw/src/sparse.rs @@ -314,6 +314,9 @@ fn walk_tiff(w: &mut Want, origin: u64, data: &[u8], extra: &[u16]) -> Result Result= len || !queued.insert(v) { + continue; + } if stack.len() >= MAX_PENDING { return Err("too many child IFDs queued to visit"); } @@ -376,7 +382,7 @@ fn walk_tiff(w: &mut Want, origin: u64, data: &[u8], extra: &[u16]) -> Result= MAX_PENDING { return Err("too many child IFDs queued to visit"); } diff --git a/crates/raw/src/tiffraw.rs b/crates/raw/src/tiffraw.rs index 34bd7e53b..57878b881 100644 --- a/crates/raw/src/tiffraw.rs +++ b/crates/raw/src/tiffraw.rs @@ -109,6 +109,12 @@ pub fn read_image(data: &[u8], info: &ImageInfo, order: ByteOrder, packing: Pack // a few times the output buffer's: everything decoded at once would retain the whole image twice, and // a hostile tile table's chosen padding could stretch that much further (issue #1517). One group // still decodes in parallel; the grid order groups make, the copies stay disjoint. + // The tile grid decides how much is decoded, whatever the file's bytes: the nominal sizes of all + // the chunks stay within a few times the image (edge tiles pad it) before any is decoded. + let nominal = chunks.iter().fold(0usize, |sum, c| sum.saturating_add(chunk_samples(c, ccpp))); + if nominal > total.saturating_mul(4).saturating_add(1 << 24) { + return Err(RawError::Corrupt("the tile grid is much larger than the image".into())); + } let group_samples = (total / 4).max(1 << 20); let mut out_u16 = if float { Vec::new() } else { vec![0u16; total] }; let mut out_f32 = if float { vec![0f32; total] } else { Vec::new() }; @@ -118,7 +124,7 @@ pub fn read_image(data: &[u8], info: &ImageInfo, order: ByteOrder, packing: Pack let mut taken = 0usize; for (k, c) in chunks[decode_from..].iter().enumerate() { let n = chunk_samples(c, ccpp); - if taken > 0 && taken + n > group_samples { + if taken > 0 && taken.saturating_add(n) > group_samples { break; } taken = taken.saturating_add(n); @@ -166,6 +172,21 @@ pub fn read_image(data: &[u8], info: &ImageInfo, order: ByteOrder, packing: Pack Ok(if float { RawData::F32(out_f32) } else { RawData::U16(out_u16) }) } +/// Bytes that `n` samples take in a row of an uncompressed or inflated chunk, laid out as +/// [`unpack_chunk`] reads them: whole bytes or 16-bit words, or 32-bit words for `Words32Msb`. +fn packed_row_bytes(n: u64, bits: u32, packing: Packing, float: bool) -> u64 { + if float { + return n.saturating_mul(u64::from(bits.div_ceil(8))); + } + let bit_len = n.saturating_mul(u64::from(bits)); + match (bits, packing) { + (8, _) => n, + (16, _) | (_, Packing::Word16) => n.saturating_mul(2), + (_, Packing::Msb | Packing::Lsb) => bit_len.div_ceil(8), + (_, Packing::Words32Msb) => bit_len.div_ceil(32).saturating_mul(4), + } +} + /// A chunk's nominal decoded size in samples (per plane, as the decoder builds it). fn chunk_samples(c: &Chunk, ccpp: usize) -> usize { (c.width as usize).saturating_mul(c.height as usize).saturating_mul(ccpp) @@ -219,9 +240,8 @@ fn decode_chunk(data: &[u8], info: &ImageInfo, c: &Chunk, order: ByteOrder, pack Ok(ChunkPx::U16(v)) } comp::ADOBE_DEFLATE | comp::DEFLATE => { - let bytes_per = if float { bits.div_ceil(8) as usize } else { 0 }; // row bytes in u64: a wrapping `cw · cpp · …` in usize could come out small on 32-bit targets and slip past the gate - let row_bytes = if float { cw as u64 * cpp as u64 * bytes_per as u64 } else { (cw as u64 * cpp as u64 * bits as u64).div_ceil(8) }; + let row_bytes = packed_row_bytes(cw as u64 * cpp as u64, bits, packing, float); // the gate caps at MAX_SAMPLES (≤ 2^30), so the `usize` cast below is lossless on 32-bit targets too let inflate_len = crate::checked_pixel_count(row_bytes, ch as u64).ok_or_else(|| RawError::Corrupt("bad TIFF chunk dimensions".into()))?; @@ -234,7 +254,7 @@ fn decode_chunk(data: &[u8], info: &ImageInfo, c: &Chunk, order: ByteOrder, pack let raw = inflate(src, inflate_len as usize)?; // The stream has to reach the last row inside the image (edge strips may stop there); // it is laid out in nominal rows, so shorter is a truncated chunk, not a picture. - let last = if float { vw as u64 * cpp as u64 * bytes_per as u64 } else { (vw as u64 * cpp as u64 * bits as u64).div_ceil(8) }; + let last = packed_row_bytes(vw as u64 * cpp as u64, bits, packing, float); let visible = row_bytes.saturating_mul(vh.saturating_sub(1) as u64).saturating_add(last); if (raw.len() as u64) < visible { return Err(RawError::Corrupt(format!( @@ -533,17 +553,27 @@ mod lossy_tests { /// from the JPEG's own dimensions. #[test] fn an_oversized_lossy_jpeg_header_is_refused_before_decoding() { - let mut j = vec![0xff, 0xd8]; // SOI - // SOF0: precision 8, 40000 (0x9c40) x 40000, three components - j.extend([0xff, 0xc0, 0x00, 0x11, 0x08, 0x9c, 0x40, 0x9c, 0x40, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01]); - // SOS with three components so header parsing completes normally, then EOI - j.extend([0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00, 0x00, 0x3f, 0x00, 0x00, 0xff, 0xd9]); - let e = lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES).unwrap_err(); - assert!(matches!(e, RawError::Corrupt(_)), "{e:?}"); - // a header inside the budget passes the check (and then fails to decode only for lack of data) - j[12..14].copy_from_slice(&1024u16.to_be_bytes()); - j[14..16].copy_from_slice(&1024u16.to_be_bytes()); - assert!(lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES).is_ok() || matches!(lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES), Err(RawError::Corrupt(_)))); + // A real 8 × 8 baseline JPEG, whose frame header then claims other dimensions: the decoder + // parses the header, and the size check is what refuses it. + let px = vec![128u8; 8 * 8 * 3]; + let mut j = Vec::new(); + jpeg_encoder::Encoder::new(&mut j, 90).encode(&px, 8, 8, jpeg_encoder::ColorType::Rgb).unwrap(); + // SOF0: marker, length, precision, then the height and the width + let sof = j.windows(2).position(|w| w == [0xff, 0xc0]).expect("a baseline frame header"); + let claim = |j: &mut Vec, size: u16| { + j[sof + 5..sof + 7].copy_from_slice(&size.to_be_bytes()); + j[sof + 7..sof + 9].copy_from_slice(&size.to_be_bytes()); + }; + claim(&mut j, 40_000); + let RawError::Corrupt(refused) = lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES).unwrap_err() else { panic!("a corrupt-data error") }; + assert!(refused.contains("would exceed"), "{refused}"); + // a header inside the budget passes the size check (and then fails to decode only for lack of data) + claim(&mut j, 1024); + match lossy_jpeg(&j, 3, MAX_JPEG_SAMPLES) { + Ok(_) => {} + Err(RawError::Corrupt(m)) => assert!(!m.contains("would exceed"), "the smaller header was refused for its size: {m}"), + Err(other) => panic!("unexpected error: {other:?}"), + } } /// A lossy tile whose JPEG decodes far less than the nominal tile is fine: the rows the JPEG From 522beb0c3e6a8521c8298c16209f7db3d84d2608 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 17:55:29 -0400 Subject: [PATCH 13/16] build: pin craft-update to craft-libs main (495e597) (#2990) Picks up the startup prompt without the glyphs a font may lack (storytold/craft-libs#343, #344): the close mark is painted and the version pill is ASCII, so apps that panic on a missing glyph no longer take the prompt down. Part of tracking issue storytold/craft-libs#319 --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4513c9a66..b69533958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,7 +1110,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=db19c373c8d59a545972ca340987aecfb3611a4b#db19c373c8d59a545972ca340987aecfb3611a4b" +source = "git+https://github.com/storytold/craft-libs?rev=495e5971bc024aa96e6ed318cb6e7cd3a0103184#495e5971bc024aa96e6ed318cb6e7cd3a0103184" dependencies = [ "chrono", "dunce", @@ -1720,7 +1720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3851,7 +3851,7 @@ dependencies = [ "once_cell", "png", "thiserror 2.0.21", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4520,7 +4520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5430,7 +5430,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5487,7 +5487,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6057,7 +6057,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6303,7 +6303,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6957,7 +6957,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2bb309f91..5c0418d30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/storytold/lightcraft" [workspace.dependencies] craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f", features = ["serde"] } craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "db19c373c8d59a545972ca340987aecfb3611a4b", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184", features = ["egui"] } # internal lightcraft-denoise-core = { path = "crates/denoise-core" } lightcraft-denoise = { path = "crates/denoise", default-features = false } From 387bed199fca44d76af5aa1c24dd7cc3de4c8db2 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Sun, 25 Oct 2026 17:30:51 -0600 Subject: [PATCH 14/16] chore(deps): refresh safe dependency bumps (#2994) --- Cargo.lock | 406 ++++++++++++++++++++++----------------- Cargo.toml | 2 +- crates/engine/Cargo.toml | 2 +- xtask/Cargo.toml | 2 +- 4 files changed, 229 insertions(+), 183 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b69533958..a9a326ab8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,7 +159,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -213,13 +213,28 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + [[package]] name = "alloc-stdlib" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ - "alloc-no-stdlib", + "alloc-no-stdlib 2.0.4", +] + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib 3.0.0", ] [[package]] @@ -283,14 +298,14 @@ dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.59.0", "wl-clipboard-rs", "x11rb", ] @@ -592,15 +607,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec 0.8.0", -] - [[package]] name = "bit-set" version = "0.10.0" @@ -611,10 +617,13 @@ dependencies = [ ] [[package]] -name = "bit-vec" -version = "0.8.0" +name = "bit-set" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" +dependencies = [ + "bit-vec 0.10.1", +] [[package]] name = "bit-vec" @@ -622,6 +631,16 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +[[package]] +name = "bit-vec" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" +dependencies = [ + "borsh", + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -673,7 +692,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -690,14 +709,38 @@ dependencies = [ ] [[package]] -name = "brotli" -version = "8.0.4" +name = "borsh" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib 3.0.0", + "alloc-stdlib 0.3.0", + "brotli-decompressor 6.0.1", ] [[package]] @@ -706,8 +749,18 @@ version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", + "alloc-no-stdlib 2.0.4", + "alloc-stdlib 0.2.4", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" +dependencies = [ + "alloc-no-stdlib 3.0.0", + "alloc-stdlib 0.3.0", ] [[package]] @@ -845,7 +898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fdfe9d06de16ce49961e49084e5b79a75a9bdf157246e7c7b6328e87a7aa25d" dependencies = [ "half", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", "once_cell", @@ -922,7 +975,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", ] [[package]] @@ -932,7 +996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1084,6 +1148,12 @@ dependencies = [ "libc", ] +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1093,6 +1163,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "craft-docpath" version = "0.1.0" @@ -1134,9 +1213,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" dependencies = [ "cfg-if", ] @@ -1225,7 +1304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1318,7 +1397,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -1448,7 +1527,7 @@ dependencies = [ "image", "js-sys", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "parking_lot", @@ -1516,7 +1595,7 @@ dependencies = [ "egui", "image", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "objc2-ui-kit 0.3.2", @@ -1647,7 +1726,7 @@ dependencies = [ "emath", "epaint_default_fonts", "font-types", - "harfrust", + "harfrust 0.12.0", "log", "nohash-hasher", "parking_lot", @@ -1720,7 +1799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -2408,6 +2487,7 @@ dependencies = [ "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasm-bindgen", ] @@ -2490,7 +2570,7 @@ dependencies = [ "glutin_glx_sys", "glutin_wgl_sys", "libloading", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -2599,7 +2679,20 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" dependencies = [ "bitflags 2.13.2", "bytemuck", - "read-fonts", + "read-fonts 0.41.0", + "smallvec", +] + +[[package]] +name = "harfrust" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948d0741125ba89cd3e1c23e5642415b6ade7e1d29d67ba25fb925b533e989d6" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "once_cell", + "read-fonts 0.43.3", "smallvec", ] @@ -2981,7 +3074,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e35d032bcec660647828527ff42c6f5776d2fd44b8357f9f6d9ac6dc07218e46" dependencies = [ - "brotli-decompressor", + "brotli-decompressor 5.0.3", "jxl-bitstream", "jxl-frame", "jxl-grid", @@ -3013,7 +3106,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d36c662923f47586880211f3bc7c0d83fb3a9b410d278c7bde93450748abeef3" dependencies = [ - "brotli-decompressor", + "brotli-decompressor 5.0.3", "jxl-bitstream", "jxl-color", "jxl-frame", @@ -3139,9 +3232,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.189" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libfuzzer-sys" @@ -3197,7 +3290,7 @@ dependencies = [ "lightcraft-ui-egui", "log", "muda", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "pollster 1.0.1", @@ -3289,7 +3382,7 @@ dependencies = [ "faer-core", "lightcraft-denoise-core", "lightcraft-raster", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "proptest", "rayon", "serde", @@ -3305,7 +3398,7 @@ version = "0.14.0" dependencies = [ "dhat", "lightcraft-raster", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "proptest", "rayon", "serde", @@ -3321,7 +3414,7 @@ version = "0.14.0" dependencies = [ "lightcraft-color", "lightcraft-geom", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "proptest", "serde", "serde_json", @@ -3334,7 +3427,7 @@ dependencies = [ "ab_glyph", "chrono", "craft-docpath", - "harfrust", + "harfrust 0.13.3", "jxl-oxide", "libc", "lightcraft-catalog", @@ -3362,7 +3455,7 @@ dependencies = [ "lightcraft-testkit", "lightcraft-tiff", "log", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "rayon", "serde", "serde_json", @@ -3401,7 +3494,7 @@ dependencies = [ name = "lightcraft-geocode" version = "0.14.0" dependencies = [ - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "serde", ] @@ -3553,7 +3646,7 @@ dependencies = [ "lightcraft-raster", "lightcraft-testkit", "lightcraft-tiff", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "proptest", "rayon", "serde", @@ -3837,21 +3930,20 @@ dependencies = [ [[package]] name = "muda" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6744ab645ebe79f9c428455c7cb9dfa4275fa3b50a25408e08f927da8dc618c" +checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03" dependencies = [ "crossbeam-channel", "dpi", "keyboard-types", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", - "once_cell", "png", "thiserror 2.0.21", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4151,9 +4243,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" dependencies = [ "objc2-encode", ] @@ -4182,7 +4274,7 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.2", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.2", @@ -4232,7 +4324,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.13.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -4243,7 +4335,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.13.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-io-surface", ] @@ -4300,7 +4392,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -4311,7 +4403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -4348,7 +4440,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", ] @@ -4373,7 +4465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.2", @@ -4418,7 +4510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", ] @@ -4520,7 +4612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -4818,7 +4910,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -4839,7 +4931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -4928,16 +5020,17 @@ dependencies = [ [[package]] name = "proptest" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" dependencies = [ - "bit-set 0.8.0", - "bit-vec 0.8.0", + "bit-set 0.11.1", + "bit-vec 0.10.1", "bitflags 2.13.2", + "chacha20 0.10.2", + "core_detect", "num-traits", - "rand 0.9.5", - "rand_chacha 0.9.0", + "rand 0.10.3", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -5071,6 +5164,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5109,6 +5212,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_distr" version = "0.5.1" @@ -5121,11 +5230,11 @@ dependencies = [ [[package]] name = "rand_xorshift" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" dependencies = [ - "rand_core 0.9.5", + "rand_core 0.10.1", ] [[package]] @@ -5214,7 +5323,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-quartz-core 0.3.2", @@ -5257,6 +5366,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "read-fonts" +version = "0.43.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005c8acf251756c478b0bf402885bfd88a1476020c4c7e6060edc1aa68da38ea" +dependencies = [ + "bytemuck", + "font-types", + "once_cell", +] + [[package]] name = "reborrow" version = "0.5.5" @@ -5323,7 +5443,7 @@ dependencies = [ "js-sys", "libc", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -5430,14 +5550,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "log", "once_cell", @@ -5487,7 +5607,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5714,9 +5834,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -5752,7 +5872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -5826,7 +5946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.41.0", ] [[package]] @@ -6054,10 +6174,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.5", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6303,7 +6423,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6755,9 +6875,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" dependencies = [ "rustls-pki-types", ] @@ -6896,7 +7016,7 @@ dependencies = [ "naga", "naga-types", "ndk-sys", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.2", @@ -6957,7 +7077,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7067,7 +7187,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -7076,16 +7196,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -7103,31 +7214,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -7145,96 +7239,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winit" version = "0.30.13" @@ -7306,9 +7352,9 @@ dependencies = [ [[package]] name = "winresource" -version = "0.1.31" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b" dependencies = [ "toml", "version_check", diff --git a/Cargo.toml b/Cargo.toml index 5c0418d30..71fd2566c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ dhat = "0.3" # opt-in heap profiling (lightcraft-cli --features dhat-heap) and h # formats quick-xml = "0.41" -miniz_oxide = "0.8" +miniz_oxide = "0.9" # codecs image = { version = "0.25", default-features = false, features = ["png", "jpeg", "tiff", "webp"] } diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 9caa8cdb0..0b203f795 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -39,7 +39,7 @@ thiserror = { workspace = true } log = { workspace = true } ab_glyph = { workspace = true } # Apply the font’s vertical substitutions and origins to Japanese watermarks. -harfrust = { version = "0.12", default-features = false, features = ["std"] } +harfrust = { version = "0.13", default-features = false, features = ["std"] } unicode-segmentation = "1.13" lightcraft-segment = { workspace = true, optional = true } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 0f1b1f77d..c68c7dca2 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -14,4 +14,4 @@ serde_json = { workspace = true } sha2 = { workspace = true } # precompressed web bundle (`cargo xtask web`); pure Rust flate2 = "1" -brotli = { version = "8", default-features = false, features = ["std"] } +brotli = { version = "9", default-features = false, features = ["std"] } From 8020cd081ce9ccd5faf87072d277c4eb966a9572 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 21:13:45 -0400 Subject: [PATCH 15/16] showcase: check the file is readable without an empty with-block (#3001) --- docs/showcase/__pycache__/run.cpython-314.pyc | Bin 0 -> 2792 bytes docs/showcase/run.py | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 docs/showcase/__pycache__/run.cpython-314.pyc diff --git a/docs/showcase/__pycache__/run.cpython-314.pyc b/docs/showcase/__pycache__/run.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de0f9a97e55aa8bc4e58bb35c5fccb0e63fcaf1a GIT binary patch literal 2792 zcma)8OKcm*8J>NS%ZEtHbZph8q?P0sEt#?%p>}B3hV4j}>O>+|DebnB2x_^OHleum z?9dN6Mp>Xh^++P7aS;Vb5cZ~=k^(&hXb(xyLymMLN6J90(KeU#q((UuxwQW*xe{Rs zXoukZyZ>`${_p>1I|B_~1f#h4*Oy0J2>pY-I76;IZ2pr24WuH1#*oSlaD=ldPk5UO zM6jtyM46Zb8kONMB=$KWw&@txbzu9g2O_fJ&=BhT z5Fz3p^(PKiCG~Sp?S;vY)}Av>2t^R+H`i-cpH<26yQM8K?Q7s@luNYiw18Fn9UBr- zU8AArKq(WkV~C0NT`fRww>`C@Ym*Si13p8lyIx~7lsLT8lPY(k9=^(VC<%}5-Nl|u z9N%dVdSPens;54l;|*5ydxSiw1+v?LqQ1f{@RHTWWBGeZ-@psKv6n3~msS2=e5l|; zPSMO9!Dd#$`mKU)5lgv=%_$|V{CIe1@I=A*3vx^nD(`;<;5%}jed7K?Q$?fEoL zPg(Zc1I5J3{dOy>>-qM$Vm*k)2BRDm6KOK3)sk;-MS>ZUBW`xtARO6}CAYwl0Qu9y zfkd85atY?+so}^S089Y*B8ZhhbtD|{I}D(0a6C!mN2ht`I9t&{1pa+>O0`FIODEB= zQ|d%-1?vu^ppk&1<3bsLUj()OA8UE(skJntm%y8W&I!%P;G)9xfT~+&_O`B&NnN?} z!ay}GX*^Mw(sSgToj?QYN;;?LcMXdexe4Hp3cES<}*~duX^9V-rVtD!^i>n1`fW z%*hdj?#-B*)oo3hceJ#ncLUx4h18d_A(&#CI0%~T$gKx3717teU`d#NZq*lgorf-mh0peI zFl)7H1tCJLzcPSsx`10mBaCN{M7>sx2M}u*`-94zsmbzulHU!!{}ZVE7^ezJq>4$d zTX5j2N?o-$jO+$m2bhipz;Fw`jw~;XungEEx#im_qzq&xf|7z7P+c5S-DhtgG?BM? ze4^2@+oo#JiIo?VqT^)(_8&nn{$#gEbv)6rt>4!OONu?tK7=0KT8sUR!`-INj^M76 zRgdabecQHvr20<_-Fye^C9!|o#(7F^#fI)Vhw94}Zl0${8+dWn3LQ4|z30^FGqIEK zI|W_OO^hksED*{Y8S1L`OzNt3OUii%H(As3Qr>)}k4ic#o#{rXw!E3Mbf{cS*E5zf z?NY|?5#5UNm>sjx1snh7KQIi0LMYlmU6!f6q7&MvK@}4{1#LX1Ys4^f)NSdxOgfvT zA}cd0WeQVyi@NljW@dEiVSdvXr|(fA3uP=~UAs;FFJ8KlQhSFJLxWJx4CP>Omw+QG zWzBTPib{Ada4|c0%2`wxFWjV@X0dL3AN0XnpDhIAE2m3$oc^n*C4P1W| zxV{|t>5BNXb#HjSvv_4axTo~&!ri&Mi&x5gSy=2}l2#%o*8B|%eY1U~GxI|;+>>Ca zbogsuV8)t1`%qhKd+X+7U*z3aSA3mJ?dt5+*9O-1HZ2}5@2_?$3FjI<+xf2 z4_ExdYtUc3v)%`~cC2js8 zDg`YsmBQ?^1GBFi?1qVo3>4JOI$qWow|gu;#H6hF?DN7NTMt_3Z7Q;R7h(}?P&lk(VrTO2TgK0vq=NHrVs`6)9~$m$pHIp8teS$~9S zL*O{>Z|Kew)Ut|N{(_n|yeRa8qW@db!-c-x=jRUo>i}wKeDmPq@cTF3xlvBP_w(h( z=$HQE)n*)@jnAJiKK}$dbgw|-G3kN0a|Dks Date: Mon, 26 Oct 2026 10:04:19 +0800 Subject: [PATCH 16/16] =?UTF-8?q?Camera=20profile:=20Sony=20ILCE-7RM5=20(?= =?UTF-8?q?=CE=B17R=20V)=20(#3022)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Camera profile: Sony ILCE-7RM5 (α7R V) * Camera profile: register the bundled ILCE-7RM5 profile * Camera profile: attribution for the ILCE-7RM5 profile * Camera profile: list ILCE-7RM5 among the built-in profiles in the README * Camera profile: list ILCE-7RM5 in the Simplified Chinese README * Camera profile: list ILCE-7RM5 in the Traditional Chinese README * Camera profile: ILCE-7RM5 calibration set used the Standard creative style only --- README.md | 2 +- README.zh-hans.md | 2 +- README.zh-hant.md | 2 +- assets/ATTRIBUTION.md | 1 + assets/camera-profiles/ILCE-7RM5.json | 9031 +++++++++++++++++++++++++ crates/engine/src/camera_profiles.rs | 1 + 6 files changed, 9036 insertions(+), 3 deletions(-) create mode 100644 assets/camera-profiles/ILCE-7RM5.json diff --git a/README.md b/README.md index 78b3b3234..71f30d4c2 100644 --- a/README.md +++ b/README.md @@ -348,7 +348,7 @@ LightCraft is young and moving fast: **alpha**. **Where we honestly stand** (det casual use.** It's great for JPEG/DNG and most Sony / Panasonic / Fujifilm / Nikon / older-Canon raws on one machine; see [docs/raw-parity.md](docs/raw-parity.md) for your camera. - **The biggest gaps:** - - **camera colour calibration:** Sony, Nikon, Panasonic, Fujifilm and Canon CR3 raws have guarded estimates from their camera JPEGs, with built-in ILCA-99M2, ILCE-7CR, ILCE-7M4, X-H2S, X-T4, Nikon Z 6II, Z 8 and D5300 profiles; measured calibration is missing, and other raws or rejected fits retain a neutral matrix; + - **camera colour calibration:** Sony, Nikon, Panasonic, Fujifilm and Canon CR3 raws have guarded estimates from their camera JPEGs, with built-in ILCA-99M2, ILCE-7CR, ILCE-7M4, ILCE-7RM5, X-H2S, X-T4, Nikon Z 6II, Z 8 and D5300 profiles; measured calibration is missing, and other raws or rejected fits retain a neutral matrix; - **compressed Olympus raws and unsupported CR3 variants:** these use embedded JPEG previews when present. Fujifilm lossless/lossy compressed RAF now decodes sensor data; [verification and existing-library reload instructions](docs/raf-compression.md); - **AI quality and model setup:** Subject and Sky selection are classical heuristics; Object/Describe use an optional SAM 3 model. AI Denoise supports Bayer RAW with separately installed weights, while X-Trans and diff --git a/README.zh-hans.md b/README.zh-hans.md index 2800db410..2c6ca8191 100644 --- a/README.zh-hans.md +++ b/README.zh-hans.md @@ -305,7 +305,7 @@ LightCraft 很年轻,而且进展很快。**我们如实所处的位置**( - **作为日常使用的工具,我们更接近 60–70%。** 在单台机器上处理 JPEG/DNG 以及大多数尼康/索尼/ 较老佳能的 raw 都很出色。 - **最大的缺口:** - - **相机色彩校准:** 索尼、尼康、松下、富士和佳能 CR3 的 raw 会从各自的相机 JPEG 得到受保护的估计值,内置 ILCA-99M2、ILCE-7CR、ILCE-7M4、X-H2S、X-T4、尼康 Z 6II、Z 8 和 D5300 配置文件;实测校准仍然缺失,其他 raw 或被拒绝的拟合会保留中性矩阵; + - **相机色彩校准:** 索尼、尼康、松下、富士和佳能 CR3 的 raw 会从各自的相机 JPEG 得到受保护的估计值,内置 ILCA-99M2、ILCE-7CR、ILCE-7M4、ILCE-7RM5、X-H2S、X-T4、尼康 Z 6II、Z 8 和 D5300 配置文件;实测校准仍然缺失,其他 raw 或被拒绝的拟合会保留中性矩阵; - **压缩的奥林巴斯 raw 和不受支持的 CR3 变体:** 这些在有内嵌 JPEG 预览时使用预览。富士无损/有损压缩 RAF 现在能解码传感器数据;[验证与已有图库重新加载说明](docs/raf-compression.md); - **AI 蒙版与降噪:** 主体和天空选择目前是经典启发式算法; - **屏幕上的 HDR 显示、视频,以及经典版式中的打印/画册/地图模块。** diff --git a/README.zh-hant.md b/README.zh-hant.md index 12fcfb11b..ca624ce68 100644 --- a/README.zh-hant.md +++ b/README.zh-hant.md @@ -320,7 +320,7 @@ LightCraft 還很年輕,而且進展飛快:**Alpha 階段**。**老實說, 在單機上處理 JPEG/DNG,以及大多數 Sony/Panasonic/ Fujifilm/Nikon/較舊款 Canon 的 raw 都很出色;各機型詳情見 [docs/raw-parity.md](docs/raw-parity.md)。 - **最大的缺口:** - - **相機色彩校正:** Sony、Nikon、Panasonic、Fujifilm 和 Canon CR3 的 raw 會從各自的相機 JPEG 得到受保護的估計值,內建 ILCA-99M2、ILCE-7CR、ILCE-7M4、X-H2S、X-T4、Nikon Z 6II、Z 8 和 D5300 描述檔;仍缺少實測校正,其他 raw 或被拒絕的擬合會保留中性矩陣; + - **相機色彩校正:** Sony、Nikon、Panasonic、Fujifilm 和 Canon CR3 的 raw 會從各自的相機 JPEG 得到受保護的估計值,內建 ILCA-99M2、ILCE-7CR、ILCE-7M4、ILCE-7RM5、X-H2S、X-T4、Nikon Z 6II、Z 8 和 D5300 描述檔;仍缺少實測校正,其他 raw 或被拒絕的擬合會保留中性矩陣; - **壓縮的 Olympus raw 與不受支援的 CR3 變體:** 這些在有內嵌 JPEG 預覽時會改用預覽畫面。Fujifilm 無損/有損壓縮 RAF 現在能解碼感光元件資料;[驗證與現有照片庫重新載入說明](docs/raf-compression.md); - **AI 遮色片與降噪:** 主體與天空選取目前是傳統啟發式演算法; - **網路照片庫:** 預覽只讀取 raw 的標頭 — 或者,對於 CR3 或預覽標籤位於其後的 TIFF raw,只讀取稀疏規劃器指定的範圍 — 以及其內嵌的 JPEG(56.5 MB → 5.9 MB,10 秒 → 1 秒,測得於測試用的網路共用資料夾),但*顯影後*的畫面仍需要透過網路傳輸感光元件資料;詳見 [docs/raw-preview-reads.md](docs/raw-preview-reads.md); diff --git a/assets/ATTRIBUTION.md b/assets/ATTRIBUTION.md index 4bc2e927e..e5ed8090e 100644 --- a/assets/ATTRIBUTION.md +++ b/assets/ATTRIBUTION.md @@ -22,6 +22,7 @@ covers a whole directory), or if a licence file referenced in the Licence column | `assets/camera-profiles/NIKON_Z_6_2.json` | Nikon Z 6II camera colour profile (matrix + hue/saturation/value table), built into LightCraft | Michael Anderson (@endthestart) | original work: fitted by `lightcraft-cli calibrate` to 296 usable Z 6II NEFs of my own (of 299, 2024 to 2026, Standard and Auto picture controls; NIKKOR Z 20mm f/1.8 S, Z 24-70mm f/4 S and AF-S 70-200mm f/2.8G on the FTZ) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-13 | none | | `assets/camera-profiles/NIKON_Z_8.json` | Nikon Z 8 camera colour profile (matrix + hue/saturation/value table), built into LightCraft | Michael Anderson (@endthestart) | original work: fitted by `lightcraft-cli calibrate` to 261 usable Z 8 NEFs of my own (of 278, 2023 and 2026, nearly all Standard picture control, lossless 14-bit; NIKKOR Z 24-120mm f/4 S and AF-S 70-200mm f/2.8G on the FTZ) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-13 | none | | `assets/camera-profiles/NIKON_D5300.json` | Nikon D5300 camera colour profile (matrix + hue/saturation/value table), built into LightCraft | Michael Anderson (@endthestart) | original work: fitted by `lightcraft-cli calibrate` to 264 usable D5300 NEFs of my own (of 273, 2014 to 2017, mostly Standard picture control, lossy 12-bit, mostly the AF-S DX 35mm f/1.8G) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-13 | none | +| `assets/camera-profiles/ILCE-7RM5.json` | Sony ILCE-7RM5 camera colour profile (matrix + hue/saturation/value table), built into LightCraft | Yule (Brian) Wang (@peaperfish) | original work: fitted by `lightcraft-cli calibrate` to 220 usable ILCE-7RM5 ARWs of my own (of 241, 2025 to 2026, Standard creative style; Sigma 24-70mm F2.8 DG DN II Art and Sony FE 200-600mm F5.6-6.3 G OSS) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-26 | none | | `crates/raw/src/spectral_table.rs` | Camera colour matrices (DNG ColorMatrix1/2 and ForwardMatrix1/2 for Standard Illuminant A and D65) for 52 camera models, and the model alias list | Derived by LightCraft contributors from rawtoaces-data (Academy Software Foundation): camera spectral sensitivities measured by Weta Digital (44 cameras), scitech (7) and Jonathon Irons at the National Physical Laboratory (Nikon D5100); training reflectances; CIE 1931 colour-matching functions (CIE, CC-BY-4.0, as shipped in rawtoaces-data) | https://github.com/AcademySoftwareFoundation/rawtoaces-data, tag v1.1.0, commit 5cfa53707b053594b2d0d48e8019fb56e6582d6c | Apache-2.0 (`LICENSE-APACHE`); the colour-matching functions CC-BY-4.0 | 2026-10-08 | generated by `tools/spectral_camera_table.py`: a 3×3 matrix per illuminant fitted to the spectral data (no measurement copied verbatim); aliases copied from `data/aliases.json`; no Adobe or third-party camera matrix | | `assets/camera-tone/sony-dro-off.json` | Sony global tone curve with the Dynamic Range Optimizer off (32 knots), built into LightCraft for ARWs shot with DRO on | LightCraft contributors; source photographs by raw.pixls.us contributors | original work: tone curve quantile-matched over the pooled pixel pairs of 12 CC0 raw.pixls.us ARWs (DRO off, Standard creative style; ILCE-3500, -5000, -6600, -7CR, -7M3 ×3, -7S, -9M2, ILCA-77M2, SLT-A57, NEX-F3) and their embedded camera JPEGs; the file lists each source URL and SHA-256 and how the curve was derived (see also `docs/camera-preview-colour.md`); aggregate tone statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 (sources CC0) | 2026-10-09 | none | | `crates/scenes/` (generated images) | Procedural demo photographs | LightCraft contributors | original work (generated at runtime, no source imagery) | MIT OR Apache-2.0 | 2026-09-30 | n/a | diff --git a/assets/camera-profiles/ILCE-7RM5.json b/assets/camera-profiles/ILCE-7RM5.json new file mode 100644 index 000000000..787bfb062 --- /dev/null +++ b/assets/camera-profiles/ILCE-7RM5.json @@ -0,0 +1,9031 @@ +{ + "version": 1, + "model": "ILCE-7RM5", + "files": 220, + "samples": 790553, + "matrix": [ + [ + 1.249584732786861, + -0.3300570108987344, + 0.07738524309065298 + ], + [ + -0.09371116474358132, + 1.2038743929103433, + -0.1120131503658719 + ], + [ + -0.03422832387281005, + -0.8688172281950557, + 1.937872050399747 + ] + ], + "hue_sat": { + "hue_divisions": 72, + "sat_divisions": 5, + "val_divisions": 5, + "data": [ + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.6575432, + 1.0169075, + 1.0 + ], + [ + 0.045065638, + 1.0544671, + 1.0 + ], + [ + 0.2621381, + 1.0540911, + 1.0 + ], + [ + 0.5927048, + 1.0288227, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.568119, + 1.0037564, + 1.0 + ], + [ + -0.4427149, + 1.0345306, + 1.0 + ], + [ + -0.41775778, + 1.0446787, + 1.0 + ], + [ + 0.086332865, + 1.0200883, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.272573, + 1.0058666, + 1.0 + ], + [ + -0.5253304, + 0.9976382, + 1.0 + ], + [ + -1.19747, + 1.0018761, + 1.0 + ], + [ + -0.79130363, + 0.98982924, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.8305438, + 1.013145, + 1.0 + ], + [ + -0.64092255, + 1.0028768, + 1.0 + ], + [ + -1.6476865, + 0.98830545, + 1.0 + ], + [ + -1.8518519, + 0.9801241, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.9341725, + 1.02301, + 1.0 + ], + [ + -1.0185463, + 1.0157639, + 1.0 + ], + [ + -2.5776067, + 0.99842805, + 1.0 + ], + [ + -3.2994924, + 0.9785849, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.5423042, + 1.0483397, + 1.0 + ], + [ + -1.4740537, + 1.032285, + 1.0 + ], + [ + -3.7110608, + 1.0046949, + 1.0 + ], + [ + -5.396528, + 0.967783, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.6425831, + 1.0778131, + 1.0 + ], + [ + -2.4365096, + 1.0424513, + 1.0 + ], + [ + -4.570945, + 1.0063766, + 1.0 + ], + [ + -6.660486, + 0.95838326, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.14437525, + 1.0805831, + 1.0 + ], + [ + -3.1429949, + 1.0347922, + 1.0 + ], + [ + -4.7743, + 1.0044911, + 1.0 + ], + [ + -6.743695, + 0.9554359, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.7822553, + 1.099207, + 1.0 + ], + [ + -2.1562428, + 1.0492018, + 1.0 + ], + [ + -4.6814117, + 0.9976052, + 1.0 + ], + [ + -7.334759, + 0.9328078, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.4186356, + 1.1193541, + 1.0 + ], + [ + -1.2648852, + 1.0724756, + 1.0 + ], + [ + -4.010651, + 1.001225, + 1.0 + ], + [ + -6.9021163, + 0.93410987, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.0874636, + 1.1538868, + 1.0 + ], + [ + 0.23989654, + 1.0897437, + 1.0 + ], + [ + -2.9069233, + 1.0008082, + 1.0 + ], + [ + -6.942227, + 0.9103677, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.2319875, + 1.1964883, + 1.0 + ], + [ + 1.6361779, + 1.1026742, + 1.0 + ], + [ + -1.7622405, + 0.99407303, + 1.0 + ], + [ + -7.6075315, + 0.87470096, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.200221, + 1.2336057, + 1.0 + ], + [ + 2.4819555, + 1.1164554, + 1.0 + ], + [ + -0.67436194, + 0.9800342, + 1.0 + ], + [ + -6.7233605, + 0.8354304, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.7635767, + 1.258256, + 1.0 + ], + [ + 2.755172, + 1.1187295, + 1.0 + ], + [ + 0.58612347, + 0.95909715, + 1.0 + ], + [ + -3.4647129, + 0.8201187, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.7450001, + 1.2654432, + 1.0 + ], + [ + 2.2866368, + 1.1127598, + 1.0 + ], + [ + 1.7782547, + 0.96788186, + 1.0 + ], + [ + 0.64800644, + 0.8437623, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.4756403, + 1.2495352, + 1.0 + ], + [ + 1.8023143, + 1.1058493, + 1.0 + ], + [ + 2.4051156, + 0.97594136, + 1.0 + ], + [ + 3.1766913, + 0.8713939, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.15415932, + 1.2347906, + 1.0 + ], + [ + 1.3704019, + 1.0905327, + 1.0 + ], + [ + 2.8927534, + 0.95705473, + 1.0 + ], + [ + 4.9556255, + 0.85762197, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.45234466, + 1.2126856, + 1.0 + ], + [ + 1.3297142, + 1.0610285, + 1.0 + ], + [ + 3.5847495, + 0.92722595, + 1.0 + ], + [ + 6.0845156, + 0.84810686, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.0763642, + 1.1915482, + 1.0 + ], + [ + 1.087915, + 1.0379584, + 1.0 + ], + [ + 3.107092, + 0.9336573, + 1.0 + ], + [ + 4.6096416, + 0.87201273, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.8927168, + 1.1797738, + 1.0 + ], + [ + 0.3604307, + 1.0387832, + 1.0 + ], + [ + 1.8885725, + 0.9472845, + 1.0 + ], + [ + 2.5527375, + 0.8916758, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.8154569, + 1.1746649, + 1.0 + ], + [ + -0.75731444, + 1.0539931, + 1.0 + ], + [ + 0.15679865, + 0.99418485, + 1.0 + ], + [ + 0.26365867, + 0.97036415, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -3.6557531, + 1.1808895, + 1.0 + ], + [ + -2.146421, + 1.0872095, + 1.0 + ], + [ + -1.216422, + 1.0318732, + 1.0 + ], + [ + -0.14712709, + 1.0002339, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -4.503687, + 1.1711676, + 1.0 + ], + [ + -2.6333165, + 1.0882001, + 1.0 + ], + [ + -0.9562701, + 1.0260637, + 1.0 + ], + [ + -0.035742544, + 1.0006093, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -5.3313165, + 1.1779693, + 1.0 + ], + [ + -3.511713, + 1.0934967, + 1.0 + ], + [ + -0.77989405, + 1.0125425, + 1.0 + ], + [ + -0.15130846, + 0.9985428, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -6.7568846, + 1.1773652, + 1.0 + ], + [ + -4.3852744, + 1.0850232, + 1.0 + ], + [ + -2.9159627, + 0.9900688, + 1.0 + ], + [ + -2.1400974, + 0.9775968, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -6.7892175, + 1.1440657, + 1.0 + ], + [ + -4.395312, + 1.0594687, + 1.0 + ], + [ + -3.1968632, + 0.9905179, + 1.0 + ], + [ + -1.8385077, + 0.98149014, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.615582, + 1.1392046, + 1.0 + ], + [ + -4.00538, + 1.0335472, + 1.0 + ], + [ + -0.9200221, + 0.99787354, + 1.0 + ], + [ + -0.11120706, + 0.9992187, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.523595, + 1.1210346, + 1.0 + ], + [ + -4.8383403, + 1.0227709, + 1.0 + ], + [ + -1.0064363, + 0.9959147, + 1.0 + ], + [ + -0.049839716, + 0.99975395, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.567221, + 1.109696, + 1.0 + ], + [ + -3.7778788, + 1.0227439, + 1.0 + ], + [ + -0.3094657, + 0.99754596, + 1.0 + ], + [ + -0.01160631, + 0.9998476, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.9242926, + 1.1014805, + 1.0 + ], + [ + -2.6735833, + 1.0197709, + 1.0 + ], + [ + -0.10607915, + 0.9988134, + 1.0 + ], + [ + -0.0016143449, + 0.99994075, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.489351, + 1.0988349, + 1.0 + ], + [ + -2.0020316, + 1.0113986, + 1.0 + ], + [ + -0.060160246, + 0.9993989, + 1.0 + ], + [ + -0.00045636468, + 0.99998945, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.7909346, + 1.0950327, + 1.0 + ], + [ + -3.1690855, + 1.0192778, + 1.0 + ], + [ + -0.20098883, + 1.0008932, + 1.0 + ], + [ + -0.0010797405, + 1.0000042, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.812833, + 1.0921811, + 1.0 + ], + [ + -3.6212065, + 1.016884, + 1.0 + ], + [ + -0.2872315, + 1.000208, + 1.0 + ], + [ + -0.0022921215, + 0.9999986, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.7691603, + 1.0748369, + 1.0 + ], + [ + -2.2901673, + 1.0061964, + 1.0 + ], + [ + -0.2540361, + 0.9997461, + 1.0 + ], + [ + -0.002999882, + 0.9999967, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.285355, + 1.0752903, + 1.0 + ], + [ + -2.8977985, + 1.008037, + 1.0 + ], + [ + -0.389354, + 1.0001613, + 1.0 + ], + [ + -0.0048222253, + 1.0000017, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.331701, + 1.0774896, + 1.0 + ], + [ + -3.3062727, + 1.0165391, + 1.0 + ], + [ + -0.24283074, + 1.0005523, + 1.0 + ], + [ + -0.0019910862, + 1.0000027, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.720209, + 1.0690521, + 1.0 + ], + [ + -4.8786044, + 1.0175062, + 1.0 + ], + [ + -0.48560837, + 1.0014203, + 1.0 + ], + [ + -0.0033854088, + 1.0000086, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -11.556501, + 1.0456746, + 1.0 + ], + [ + -4.985604, + 1.0003452, + 1.0 + ], + [ + -0.41515103, + 0.9995448, + 1.0 + ], + [ + -0.003054782, + 0.9999918, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -11.729045, + 1.0217354, + 1.0 + ], + [ + -3.521123, + 0.98780435, + 1.0 + ], + [ + -0.30965137, + 0.99440813, + 1.0 + ], + [ + -0.008598875, + 0.9997742, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -12.221271, + 0.98520076, + 1.0 + ], + [ + -4.486184, + 0.97454756, + 1.0 + ], + [ + -0.41762093, + 0.9899926, + 1.0 + ], + [ + -0.03436083, + 0.9983804, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -13.70429, + 0.9197576, + 1.0 + ], + [ + -6.0378523, + 0.93986017, + 1.0 + ], + [ + -0.41832632, + 0.99216044, + 1.0 + ], + [ + -0.01641741, + 0.99925196, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -14.093513, + 0.8699881, + 1.0 + ], + [ + -6.8281274, + 0.9147011, + 1.0 + ], + [ + -0.30128792, + 0.99418074, + 1.0 + ], + [ + -0.0026745356, + 0.9999154, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -12.529678, + 0.84677386, + 1.0 + ], + [ + -7.347991, + 0.897593, + 1.0 + ], + [ + -0.39531615, + 0.9915446, + 1.0 + ], + [ + -0.000654911, + 0.9999173, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -10.297008, + 0.8246169, + 1.0 + ], + [ + -6.991395, + 0.8822463, + 1.0 + ], + [ + -0.8904233, + 0.98326087, + 1.0 + ], + [ + -0.005210178, + 0.99982196, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.1014814, + 0.870403, + 1.0 + ], + [ + -4.2737536, + 0.9510795, + 1.0 + ], + [ + -1.0143899, + 0.9928547, + 1.0 + ], + [ + 0.0069134138, + 0.9990259, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.2389834, + 1.0089513, + 1.0 + ], + [ + 0.59233487, + 1.0836766, + 1.0 + ], + [ + 2.109882, + 1.0953512, + 1.0 + ], + [ + 0.57811993, + 1.0165061, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.42605868, + 1.0736833, + 1.0 + ], + [ + 2.6112878, + 1.1127571, + 1.0 + ], + [ + 3.734751, + 1.1181016, + 1.0 + ], + [ + 2.895584, + 1.0769328, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.7645093, + 1.0585829, + 1.0 + ], + [ + 2.7074714, + 1.1086032, + 1.0 + ], + [ + 4.612825, + 1.121013, + 1.0 + ], + [ + 4.2159166, + 1.090578, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.317804, + 1.0096856, + 1.0 + ], + [ + 2.410078, + 1.0813844, + 1.0 + ], + [ + 4.022777, + 1.0866787, + 1.0 + ], + [ + 1.5960958, + 1.0285214, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.7581048, + 0.94621634, + 1.0 + ], + [ + 3.344702, + 1.031301, + 1.0 + ], + [ + 1.6271458, + 1.0360217, + 1.0 + ], + [ + 0.045802835, + 1.0040928, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.2102604, + 0.8877613, + 1.0 + ], + [ + 2.5063229, + 0.9831511, + 1.0 + ], + [ + 1.0589613, + 1.0087296, + 1.0 + ], + [ + 0.044849876, + 1.0006486, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.8181234, + 0.84908885, + 1.0 + ], + [ + 1.2979459, + 0.9558577, + 1.0 + ], + [ + 0.95269287, + 0.9990587, + 1.0 + ], + [ + 0.13956869, + 0.99899775, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.4218108, + 0.8245256, + 1.0 + ], + [ + 0.980773, + 0.95178086, + 1.0 + ], + [ + 0.44153172, + 1.004565, + 1.0 + ], + [ + 0.083737634, + 0.9998738, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.3112286, + 0.81245995, + 1.0 + ], + [ + 1.2433338, + 0.94633913, + 1.0 + ], + [ + -0.45147318, + 1.0128739, + 1.0 + ], + [ + -0.040671885, + 1.0012027, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.0959775, + 0.82676107, + 1.0 + ], + [ + 0.19708328, + 0.9494545, + 1.0 + ], + [ + -0.71126884, + 1.0130774, + 1.0 + ], + [ + -0.066266306, + 1.0014489, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.0682087, + 0.85580033, + 1.0 + ], + [ + 0.33310115, + 0.9754608, + 1.0 + ], + [ + -0.038120847, + 1.0069447, + 1.0 + ], + [ + -0.0039369226, + 1.0006703, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.4665134, + 0.8869471, + 1.0 + ], + [ + 1.394241, + 0.9861353, + 1.0 + ], + [ + 0.39206934, + 1.0029631, + 1.0 + ], + [ + 0.012310251, + 1.0002207, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.840249, + 0.885747, + 1.0 + ], + [ + 2.030714, + 0.97694564, + 1.0 + ], + [ + 0.48313063, + 0.9986679, + 1.0 + ], + [ + 0.015092909, + 1.0000398, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 6.364709, + 0.93935746, + 1.0 + ], + [ + 3.27353, + 1.0063933, + 1.0 + ], + [ + 0.47039714, + 1.0006171, + 1.0 + ], + [ + 0.0095254835, + 0.9999786, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.708454, + 0.9936273, + 1.0 + ], + [ + 3.0144715, + 1.0396764, + 1.0 + ], + [ + 0.76019174, + 1.0015196, + 1.0 + ], + [ + 0.031606715, + 0.9997963, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.081264, + 1.007644, + 1.0 + ], + [ + 0.9569218, + 1.0373055, + 1.0 + ], + [ + -0.29632938, + 1.0081192, + 1.0 + ], + [ + -0.020595783, + 1.0002273, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.4333196, + 0.9922466, + 1.0 + ], + [ + 0.8624292, + 1.0051231, + 1.0 + ], + [ + -0.124487296, + 1.0007266, + 1.0 + ], + [ + 0.0014794656, + 1.0001725, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.0025764, + 0.9966404, + 1.0 + ], + [ + 0.32528183, + 1.0222802, + 1.0 + ], + [ + -1.4467791, + 1.0029583, + 1.0 + ], + [ + -0.09876483, + 1.0001086, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.9974155, + 0.9894437, + 1.0 + ], + [ + 2.0329673, + 1.0394019, + 1.0 + ], + [ + -0.49002603, + 1.0067779, + 1.0 + ], + [ + -0.091952406, + 0.9999813, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.90883, + 0.9736301, + 1.0 + ], + [ + 2.7153323, + 1.0357952, + 1.0 + ], + [ + 0.37018985, + 1.0083603, + 1.0 + ], + [ + -0.0096038, + 1.0001452, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.1170373, + 0.98292226, + 1.0 + ], + [ + 1.2927599, + 1.035257, + 1.0 + ], + [ + 0.3150436, + 1.0090348, + 1.0 + ], + [ + 0.0004185312, + 1.0000962, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.3353724, + 0.9857392, + 1.0 + ], + [ + 0.9401049, + 1.0468453, + 1.0 + ], + [ + -0.13837458, + 1.0182233, + 1.0 + ], + [ + -0.024829892, + 1.0004286, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.5100236, + 1.0068982, + 1.0 + ], + [ + 0.93617123, + 1.0617714, + 1.0 + ], + [ + 0.047028802, + 1.0331767, + 1.0 + ], + [ + 0.0046950956, + 1.0010607, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.6971107, + 1.0113055, + 1.0 + ], + [ + -0.20543085, + 1.0561123, + 1.0 + ], + [ + -0.47802702, + 1.0383303, + 1.0 + ], + [ + 0.07525184, + 1.0021627, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.8135715, + 1.0148535, + 1.0 + ], + [ + 0.23203266, + 1.0550131, + 1.0 + ], + [ + -0.023389658, + 1.04285, + 1.0 + ], + [ + 0.19465668, + 1.002981, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.6685414, + 1.0124575, + 1.0 + ], + [ + 0.59984475, + 1.0448725, + 1.0 + ], + [ + 0.69587445, + 1.0372711, + 1.0 + ], + [ + 0.60554105, + 1.005815, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.5002728, + 1.0161368, + 1.0 + ], + [ + 0.56248856, + 1.058378, + 1.0 + ], + [ + 0.9347412, + 1.0511781, + 1.0 + ], + [ + 0.8927735, + 1.0201077, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.8510935, + 1.0004079, + 1.0 + ], + [ + -0.13690601, + 1.0438355, + 1.0 + ], + [ + 0.5490022, + 1.0381058, + 1.0 + ], + [ + 1.4847147, + 1.0113307, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.2298164, + 0.98695815, + 1.0 + ], + [ + -0.5338396, + 1.008648, + 1.0 + ], + [ + -0.1529312, + 1.0192995, + 1.0 + ], + [ + 1.3007274, + 0.98695505, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.9663947, + 0.9861699, + 1.0 + ], + [ + 0.3281501, + 0.9828017, + 1.0 + ], + [ + -0.9316485, + 0.9753089, + 1.0 + ], + [ + -0.45177975, + 0.9492595, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.8109272, + 0.98814505, + 1.0 + ], + [ + -0.06094338, + 0.99206847, + 1.0 + ], + [ + -1.5175383, + 0.982531, + 1.0 + ], + [ + -1.9885272, + 0.954517, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.5236363, + 0.9898073, + 1.0 + ], + [ + -0.5354513, + 1.0057786, + 1.0 + ], + [ + -2.319607, + 1.0002798, + 1.0 + ], + [ + -3.4267511, + 0.97033256, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.6655283, + 1.0100801, + 1.0 + ], + [ + -1.0105478, + 1.0113044, + 1.0 + ], + [ + -3.3712995, + 0.9907962, + 1.0 + ], + [ + -5.3614125, + 0.957974, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.55430204, + 1.0288806, + 1.0 + ], + [ + -2.2823086, + 1.0114845, + 1.0 + ], + [ + -4.29412, + 0.9850471, + 1.0 + ], + [ + -6.70813, + 0.94654834, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.28653958, + 1.029249, + 1.0 + ], + [ + -2.9026222, + 1.0021783, + 1.0 + ], + [ + -4.3481326, + 0.98601437, + 1.0 + ], + [ + -6.742446, + 0.94504565, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.840411, + 1.0304451, + 1.0 + ], + [ + -1.7543678, + 1.0166836, + 1.0 + ], + [ + -4.263933, + 0.98349094, + 1.0 + ], + [ + -7.782672, + 0.9144162, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.9039959, + 1.0615926, + 1.0 + ], + [ + -0.6004897, + 1.0442605, + 1.0 + ], + [ + -3.2590501, + 0.9852762, + 1.0 + ], + [ + -6.0829635, + 0.92023736, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.113609, + 1.1155086, + 1.0 + ], + [ + 0.7005239, + 1.0668454, + 1.0 + ], + [ + -1.9628628, + 0.9854875, + 1.0 + ], + [ + -5.3814754, + 0.90371615, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.0877285, + 1.1595732, + 1.0 + ], + [ + 1.9560493, + 1.0802444, + 1.0 + ], + [ + -0.77529097, + 0.9800677, + 1.0 + ], + [ + -5.565922, + 0.8722543, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.288913, + 1.2051771, + 1.0 + ], + [ + 2.74089, + 1.1004237, + 1.0 + ], + [ + 0.012938409, + 0.9697918, + 1.0 + ], + [ + -5.393885, + 0.8341304, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.8245485, + 1.2282706, + 1.0 + ], + [ + 2.9895375, + 1.1039253, + 1.0 + ], + [ + 1.2145354, + 0.95314676, + 1.0 + ], + [ + -2.3480604, + 0.8217049, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.0588024, + 1.2314723, + 1.0 + ], + [ + 2.7796125, + 1.0972726, + 1.0 + ], + [ + 2.4223437, + 0.9613367, + 1.0 + ], + [ + 1.6759906, + 0.8447568, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.8357061, + 1.210052, + 1.0 + ], + [ + 2.2408845, + 1.0848253, + 1.0 + ], + [ + 2.808005, + 0.9623734, + 1.0 + ], + [ + 3.4482584, + 0.8608864, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.69474894, + 1.1947756, + 1.0 + ], + [ + 1.6912163, + 1.0640128, + 1.0 + ], + [ + 3.070553, + 0.93303436, + 1.0 + ], + [ + 5.026965, + 0.83831453, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.02924209, + 1.1594467, + 1.0 + ], + [ + 1.437672, + 1.0297697, + 1.0 + ], + [ + 3.5909321, + 0.90887827, + 1.0 + ], + [ + 5.7714977, + 0.8360717, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.73282367, + 1.1322978, + 1.0 + ], + [ + 1.0370998, + 1.0152173, + 1.0 + ], + [ + 2.9553826, + 0.93621993, + 1.0 + ], + [ + 4.670117, + 0.86914015, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.619349, + 1.1252851, + 1.0 + ], + [ + 0.15239729, + 1.0244952, + 1.0 + ], + [ + 1.6071638, + 0.960652, + 1.0 + ], + [ + 2.7668586, + 0.8957443, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.4471934, + 1.1201713, + 1.0 + ], + [ + -0.9853024, + 1.0402389, + 1.0 + ], + [ + -0.10679894, + 1.0003766, + 1.0 + ], + [ + 0.38707855, + 0.9576168, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -3.6268017, + 1.1333272, + 1.0 + ], + [ + -2.5076745, + 1.071064, + 1.0 + ], + [ + -1.8380454, + 1.0375947, + 1.0 + ], + [ + -0.49257776, + 1.0004728, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -4.8864617, + 1.1463132, + 1.0 + ], + [ + -3.6842506, + 1.0934086, + 1.0 + ], + [ + -2.3747447, + 1.0497583, + 1.0 + ], + [ + -0.121673696, + 1.0016263, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -6.1770673, + 1.1691642, + 1.0 + ], + [ + -4.8939047, + 1.1140888, + 1.0 + ], + [ + -2.1122801, + 1.0347022, + 1.0 + ], + [ + -0.2781079, + 0.9971531, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.824204, + 1.1899806, + 1.0 + ], + [ + -6.7190065, + 1.1285441, + 1.0 + ], + [ + -5.1226296, + 0.9854947, + 1.0 + ], + [ + -4.016408, + 0.9494332, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.926484, + 1.1482611, + 1.0 + ], + [ + -7.6249037, + 1.0958742, + 1.0 + ], + [ + -5.6792645, + 0.9760871, + 1.0 + ], + [ + -4.0723734, + 0.94808024, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.670957, + 1.1153084, + 1.0 + ], + [ + -8.038458, + 1.0422579, + 1.0 + ], + [ + -5.8509502, + 0.9850574, + 1.0 + ], + [ + -0.8264564, + 0.9961184, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -10.342434, + 1.0856272, + 1.0 + ], + [ + -9.040556, + 1.0213751, + 1.0 + ], + [ + -7.0447855, + 0.9760523, + 1.0 + ], + [ + -0.8563134, + 0.9967798, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -11.15017, + 1.0738698, + 1.0 + ], + [ + -8.293659, + 1.0015079, + 1.0 + ], + [ + -3.465691, + 0.95468867, + 1.0 + ], + [ + -0.26872262, + 0.996449, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -12.361571, + 1.0671077, + 1.0 + ], + [ + -8.069062, + 0.9878739, + 1.0 + ], + [ + -1.8393602, + 0.9616174, + 1.0 + ], + [ + -0.062451784, + 0.99806386, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -11.813561, + 1.0733019, + 1.0 + ], + [ + -6.277449, + 0.9938862, + 1.0 + ], + [ + -0.91688836, + 0.98827964, + 1.0 + ], + [ + -0.015721286, + 0.99975455, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -11.033608, + 1.0607992, + 1.0 + ], + [ + -5.8924174, + 1.0059938, + 1.0 + ], + [ + -0.4630321, + 0.99918526, + 1.0 + ], + [ + -0.0028961068, + 0.9999799, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -13.24634, + 1.0676687, + 1.0 + ], + [ + -8.926855, + 1.0089554, + 1.0 + ], + [ + -1.2582607, + 0.998966, + 1.0 + ], + [ + -0.01142765, + 0.9999851, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -14.128018, + 1.0785146, + 1.0 + ], + [ + -8.119614, + 1.0094862, + 1.0 + ], + [ + -1.2649353, + 0.99898714, + 1.0 + ], + [ + -0.015170822, + 0.9999889, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -12.451852, + 1.0737165, + 1.0 + ], + [ + -6.9139895, + 1.0113754, + 1.0 + ], + [ + -1.088725, + 1.0001466, + 1.0 + ], + [ + -0.013777146, + 1.0000031, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -12.380149, + 1.0823865, + 1.0 + ], + [ + -6.0622687, + 1.0224757, + 1.0 + ], + [ + -0.49693757, + 1.0009257, + 1.0 + ], + [ + -0.004322603, + 1.0000051, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -13.628408, + 1.0830122, + 1.0 + ], + [ + -7.603063, + 1.0293987, + 1.0 + ], + [ + -0.7915703, + 1.0023602, + 1.0 + ], + [ + -0.005485728, + 1.0000141, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -15.070874, + 1.025278, + 1.0 + ], + [ + -8.004763, + 0.9920342, + 1.0 + ], + [ + -0.6927761, + 0.99896514, + 1.0 + ], + [ + -0.0051751113, + 0.99998105, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -15.791659, + 0.9914543, + 1.0 + ], + [ + -7.6757627, + 0.9670585, + 1.0 + ], + [ + -0.94827396, + 0.98121995, + 1.0 + ], + [ + -0.042875327, + 0.99850494, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -15.504122, + 0.918028, + 1.0 + ], + [ + -9.548977, + 0.89464694, + 1.0 + ], + [ + -2.6915827, + 0.9125134, + 1.0 + ], + [ + -0.46877646, + 0.9763951, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -15.834205, + 0.86167675, + 1.0 + ], + [ + -11.029793, + 0.8641353, + 1.0 + ], + [ + -2.304663, + 0.940338, + 1.0 + ], + [ + -0.22860166, + 0.9885806, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -15.163353, + 0.80446744, + 1.0 + ], + [ + -10.703714, + 0.82697487, + 1.0 + ], + [ + -1.8142037, + 0.948325, + 1.0 + ], + [ + -0.0477379, + 0.998123, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -14.573051, + 0.76530474, + 1.0 + ], + [ + -10.532188, + 0.808058, + 1.0 + ], + [ + -1.7215453, + 0.94441307, + 1.0 + ], + [ + -0.0065259486, + 0.99902344, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -11.712678, + 0.78053474, + 1.0 + ], + [ + -9.12928, + 0.8284735, + 1.0 + ], + [ + -3.0647676, + 0.9377219, + 1.0 + ], + [ + -0.02522077, + 0.9987442, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.4553695, + 0.88699067, + 1.0 + ], + [ + -4.486537, + 0.9664551, + 1.0 + ], + [ + -1.3492911, + 1.0004994, + 1.0 + ], + [ + 0.20210887, + 0.98605, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.5626588, + 1.0187794, + 1.0 + ], + [ + -0.16849272, + 1.0859944, + 1.0 + ], + [ + 1.1092399, + 1.1023259, + 1.0 + ], + [ + 1.3777262, + 1.0548018, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.040617283, + 1.066993, + 1.0 + ], + [ + 1.53646, + 1.1038263, + 1.0 + ], + [ + 2.3726995, + 1.106449, + 1.0 + ], + [ + 2.8319159, + 1.0958794, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.8091955, + 1.042359, + 1.0 + ], + [ + 2.302053, + 1.0939299, + 1.0 + ], + [ + 3.1255443, + 1.0935813, + 1.0 + ], + [ + 3.7047086, + 1.0906761, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.4341065, + 0.9560042, + 1.0 + ], + [ + 2.3559563, + 1.0608075, + 1.0 + ], + [ + 3.3579242, + 1.0881857, + 1.0 + ], + [ + 3.2111607, + 1.0706342, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.0547562, + 0.8983519, + 1.0 + ], + [ + 3.383354, + 0.9991967, + 1.0 + ], + [ + 1.8556306, + 1.0425386, + 1.0 + ], + [ + 0.0885949, + 1.0138919, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.7682338, + 0.8603291, + 1.0 + ], + [ + 3.0132825, + 0.9588983, + 1.0 + ], + [ + 1.9995759, + 1.0111612, + 1.0 + ], + [ + 0.47482064, + 1.006286, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.6232177, + 0.8233142, + 1.0 + ], + [ + 2.0865932, + 0.9309424, + 1.0 + ], + [ + 2.7574844, + 0.97783035, + 1.0 + ], + [ + 0.95219344, + 0.99299604, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.6943194, + 0.79197764, + 1.0 + ], + [ + 1.8879877, + 0.920102, + 1.0 + ], + [ + 1.2647737, + 0.9986511, + 1.0 + ], + [ + 0.3897333, + 1.0005867, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.7574468, + 0.78670424, + 1.0 + ], + [ + 2.1780686, + 0.9121051, + 1.0 + ], + [ + -0.5525949, + 1.0213124, + 1.0 + ], + [ + -0.2346502, + 1.0064204, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.4999485, + 0.7841847, + 1.0 + ], + [ + 1.9435306, + 0.9184122, + 1.0 + ], + [ + -1.5851839, + 1.0305399, + 1.0 + ], + [ + -0.36774108, + 1.0069425, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.909163, + 0.80230415, + 1.0 + ], + [ + 2.1358707, + 0.935365, + 1.0 + ], + [ + -0.84193116, + 1.0221872, + 1.0 + ], + [ + -0.14351588, + 1.0037925, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 6.5533886, + 0.8221586, + 1.0 + ], + [ + 3.3628576, + 0.95415354, + 1.0 + ], + [ + 0.8394116, + 1.0065193, + 1.0 + ], + [ + 0.021697784, + 1.000693, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 8.034471, + 0.84136575, + 1.0 + ], + [ + 4.6422033, + 0.9475814, + 1.0 + ], + [ + 1.4912386, + 0.99571365, + 1.0 + ], + [ + 0.018330399, + 1.0000721, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 7.851116, + 0.9006105, + 1.0 + ], + [ + 5.2049785, + 0.9920339, + 1.0 + ], + [ + 1.2569566, + 0.99717367, + 1.0 + ], + [ + 0.0052652257, + 0.9997678, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 7.6648865, + 0.9650344, + 1.0 + ], + [ + 4.729117, + 1.0339372, + 1.0 + ], + [ + 2.091848, + 0.99583614, + 1.0 + ], + [ + 0.10322704, + 0.99907774, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 7.6680355, + 0.9877355, + 1.0 + ], + [ + 3.6761901, + 1.0451121, + 1.0 + ], + [ + -0.04351923, + 1.0153469, + 1.0 + ], + [ + -0.03283479, + 1.0004294, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 6.463548, + 0.96174765, + 1.0 + ], + [ + 2.6405559, + 1.0113807, + 1.0 + ], + [ + -0.0145218745, + 1.0030446, + 1.0 + ], + [ + 0.02793165, + 1.0006396, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 6.7405667, + 0.97143346, + 1.0 + ], + [ + 1.3641241, + 1.0326787, + 1.0 + ], + [ + -3.4862273, + 1.0015736, + 1.0 + ], + [ + -0.47250292, + 0.99943143, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 8.19507, + 0.96321476, + 1.0 + ], + [ + 3.4350495, + 1.058396, + 1.0 + ], + [ + -3.5258145, + 1.0094136, + 1.0 + ], + [ + -1.0841179, + 0.9979366, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 7.8372226, + 0.95261025, + 1.0 + ], + [ + 4.014715, + 1.0542111, + 1.0 + ], + [ + 0.26240677, + 1.0264678, + 1.0 + ], + [ + -0.21519896, + 1.0007783, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.1908545, + 0.95756376, + 1.0 + ], + [ + 1.8382027, + 1.0398521, + 1.0 + ], + [ + 0.32465455, + 1.0176383, + 1.0 + ], + [ + -0.07286978, + 0.99991214, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 6.5853324, + 0.9502199, + 1.0 + ], + [ + 0.9021832, + 1.0475446, + 1.0 + ], + [ + -0.38347927, + 1.0296501, + 1.0 + ], + [ + -0.10875285, + 1.0017142, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 7.5904784, + 0.95001745, + 1.0 + ], + [ + 0.3187001, + 1.0552145, + 1.0 + ], + [ + -0.065457314, + 1.0441794, + 1.0 + ], + [ + 0.1682706, + 1.0044458, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 7.727416, + 0.9608402, + 1.0 + ], + [ + -0.4404374, + 1.0511845, + 1.0 + ], + [ + 0.13549781, + 1.0374392, + 1.0 + ], + [ + 1.0298581, + 1.0050887, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 7.1869535, + 0.96325994, + 1.0 + ], + [ + 0.21250498, + 1.0488186, + 1.0 + ], + [ + 0.5558834, + 1.0353485, + 1.0 + ], + [ + 1.3571175, + 1.0007496, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 6.2386184, + 0.97497666, + 1.0 + ], + [ + 0.37397635, + 1.0394269, + 1.0 + ], + [ + 0.7773281, + 1.034625, + 1.0 + ], + [ + 1.4875422, + 1.0045793, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.65055, + 0.98630524, + 1.0 + ], + [ + 0.3505162, + 1.0497316, + 1.0 + ], + [ + 1.0327646, + 1.0434405, + 1.0 + ], + [ + 1.5126252, + 1.0201807, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.1305985, + 0.95614517, + 1.0 + ], + [ + -0.6686687, + 0.96170896, + 1.0 + ], + [ + 0.31405145, + 0.94526744, + 1.0 + ], + [ + 0.7571128, + 0.93466496, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.7789507, + 0.96007, + 1.0 + ], + [ + 0.025442632, + 0.9322739, + 1.0 + ], + [ + 1.002043, + 0.92365426, + 1.0 + ], + [ + 1.4359783, + 0.9207567, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.5217903, + 0.9649529, + 1.0 + ], + [ + 1.3698361, + 0.93013364, + 1.0 + ], + [ + 0.81097263, + 0.8966511, + 1.0 + ], + [ + 1.3134359, + 0.9056901, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.4688148, + 0.94516605, + 1.0 + ], + [ + 1.1258384, + 0.9285592, + 1.0 + ], + [ + -0.03493635, + 0.9379082, + 1.0 + ], + [ + -0.34919712, + 0.92182875, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.0855563, + 0.9416833, + 1.0 + ], + [ + 0.61865765, + 0.95953137, + 1.0 + ], + [ + -0.53801084, + 0.98488677, + 1.0 + ], + [ + -1.3929236, + 0.9545244, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.280524, + 0.9401836, + 1.0 + ], + [ + 0.5206633, + 0.91849095, + 1.0 + ], + [ + -0.5855471, + 0.9109686, + 1.0 + ], + [ + -1.857449, + 0.9108761, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.1066036, + 0.91177595, + 1.0 + ], + [ + -0.2379211, + 0.90215147, + 1.0 + ], + [ + -0.662678, + 0.8674564, + 1.0 + ], + [ + -1.6655282, + 0.8560494, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.3806509, + 0.91369975, + 1.0 + ], + [ + -1.1977674, + 0.9291132, + 1.0 + ], + [ + -2.1955984, + 0.9157609, + 1.0 + ], + [ + -3.2753534, + 0.8795514, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.27487, + 0.8657369, + 1.0 + ], + [ + 0.84525156, + 0.8752064, + 1.0 + ], + [ + -1.1533172, + 0.88577944, + 1.0 + ], + [ + -4.5261264, + 0.8575033, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.6447818, + 0.8502878, + 1.0 + ], + [ + 1.9611719, + 0.87647694, + 1.0 + ], + [ + -0.25773066, + 0.88421917, + 1.0 + ], + [ + -2.0909984, + 0.84323335, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.3915353, + 0.95410204, + 1.0 + ], + [ + 1.9980977, + 0.9531656, + 1.0 + ], + [ + 0.5931035, + 0.88896406, + 1.0 + ], + [ + -1.3361316, + 0.8371896, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.3264365, + 1.0299196, + 1.0 + ], + [ + 2.5364761, + 0.97348475, + 1.0 + ], + [ + 1.3221674, + 0.88718134, + 1.0 + ], + [ + -0.9853643, + 0.8285287, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.1308208, + 1.0718769, + 1.0 + ], + [ + 2.790149, + 0.9912555, + 1.0 + ], + [ + 1.4749019, + 0.8944228, + 1.0 + ], + [ + -1.1555754, + 0.8163407, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.2958386, + 1.088511, + 1.0 + ], + [ + 2.6916971, + 0.9975709, + 1.0 + ], + [ + 1.9740455, + 0.88601756, + 1.0 + ], + [ + 0.36857194, + 0.80993265, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.5474374, + 1.0788504, + 1.0 + ], + [ + 2.225414, + 0.97936237, + 1.0 + ], + [ + 1.8757105, + 0.88728917, + 1.0 + ], + [ + 1.5990429, + 0.8291341, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.2438161, + 1.036749, + 1.0 + ], + [ + 1.3613421, + 0.9619165, + 1.0 + ], + [ + 1.4527313, + 0.8871177, + 1.0 + ], + [ + 2.0332718, + 0.8303382, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.09551922, + 1.0317862, + 1.0 + ], + [ + 0.43391225, + 0.9460805, + 1.0 + ], + [ + 1.4488848, + 0.8737923, + 1.0 + ], + [ + 3.4753993, + 0.81625134, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.7019963, + 1.0077614, + 1.0 + ], + [ + -0.051761873, + 0.9282602, + 1.0 + ], + [ + 1.9012142, + 0.8732041, + 1.0 + ], + [ + 4.1987333, + 0.82554394, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.6378045, + 1.0135831, + 1.0 + ], + [ + -0.38506448, + 0.9445609, + 1.0 + ], + [ + 1.5109704, + 0.91002667, + 1.0 + ], + [ + 3.479174, + 0.8619353, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.4618201, + 1.033089, + 1.0 + ], + [ + -1.2937772, + 0.97869325, + 1.0 + ], + [ + 0.10574721, + 0.9512708, + 1.0 + ], + [ + 1.488456, + 0.92858106, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -3.1203666, + 1.0350168, + 1.0 + ], + [ + -2.2538068, + 0.99723995, + 1.0 + ], + [ + -1.1310667, + 0.97431004, + 1.0 + ], + [ + 0.084608465, + 0.98049074, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -4.3735642, + 1.0520235, + 1.0 + ], + [ + -3.6140828, + 1.0261108, + 1.0 + ], + [ + -1.7979896, + 1.003871, + 1.0 + ], + [ + -0.05716326, + 0.9986102, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -5.2478585, + 1.0614483, + 1.0 + ], + [ + -4.203233, + 1.0365448, + 1.0 + ], + [ + -1.1785761, + 1.0052958, + 1.0 + ], + [ + -0.012518613, + 0.999905, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -5.7618976, + 1.0709513, + 1.0 + ], + [ + -4.2346697, + 1.041181, + 1.0 + ], + [ + -0.55711687, + 1.0027059, + 1.0 + ], + [ + -0.012013823, + 0.99982893, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -7.164432, + 1.0977043, + 1.0 + ], + [ + -4.544374, + 1.0551589, + 1.0 + ], + [ + -0.9486156, + 1.0006344, + 1.0 + ], + [ + -0.2880987, + 0.99641603, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.89083, + 1.0878369, + 1.0 + ], + [ + -5.120464, + 1.0464156, + 1.0 + ], + [ + -0.8068461, + 0.99902976, + 1.0 + ], + [ + -0.2889861, + 0.9960283, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.618337, + 1.0371728, + 1.0 + ], + [ + -5.1906323, + 1.007005, + 1.0 + ], + [ + -2.135625, + 0.9941443, + 1.0 + ], + [ + -0.21111596, + 0.99932176, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.985677, + 1.0003744, + 1.0 + ], + [ + -5.3432465, + 0.98975044, + 1.0 + ], + [ + -2.6925411, + 0.99021024, + 1.0 + ], + [ + -0.2637423, + 0.99901223, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.557486, + 1.0012228, + 1.0 + ], + [ + -5.126186, + 0.97562605, + 1.0 + ], + [ + -1.487278, + 0.9767831, + 1.0 + ], + [ + -0.09417072, + 0.99855137, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.93245, + 0.9848428, + 1.0 + ], + [ + -5.005292, + 0.95344836, + 1.0 + ], + [ + -1.0024476, + 0.97530013, + 1.0 + ], + [ + -0.03428691, + 0.9989181, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -10.067035, + 0.99813294, + 1.0 + ], + [ + -3.760256, + 0.9666209, + 1.0 + ], + [ + -0.43569535, + 0.9912011, + 1.0 + ], + [ + -0.007490502, + 0.99986094, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.267269, + 0.975714, + 1.0 + ], + [ + -3.3425367, + 0.97395366, + 1.0 + ], + [ + -0.16193047, + 0.99658626, + 1.0 + ], + [ + -0.00091553305, + 0.9999671, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.993388, + 1.0000288, + 1.0 + ], + [ + -4.422481, + 0.985607, + 1.0 + ], + [ + -0.32398883, + 0.9973332, + 1.0 + ], + [ + -0.0023061063, + 0.99997354, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.084186, + 0.9982832, + 1.0 + ], + [ + -2.7218752, + 0.9938026, + 1.0 + ], + [ + -0.17255202, + 0.9993347, + 1.0 + ], + [ + -0.0015168872, + 0.99999547, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.26013, + 0.9777609, + 1.0 + ], + [ + -2.3467531, + 0.98704755, + 1.0 + ], + [ + -0.09092708, + 0.9993146, + 1.0 + ], + [ + -0.00067042705, + 0.9999976, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.507622, + 0.9864571, + 1.0 + ], + [ + -1.6202773, + 0.9965017, + 1.0 + ], + [ + -0.03161555, + 0.999892, + 1.0 + ], + [ + -0.00015287292, + 0.9999998, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.677804, + 0.9922909, + 1.0 + ], + [ + -1.5864677, + 1.0017024, + 1.0 + ], + [ + -0.029533267, + 1.0000547, + 1.0 + ], + [ + -0.00012411954, + 1.0000002, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.914085, + 0.95310205, + 1.0 + ], + [ + -2.334698, + 0.99508375, + 1.0 + ], + [ + -0.04895491, + 0.99995136, + 1.0 + ], + [ + -0.00017408784, + 0.9999993, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -13.237178, + 0.8746589, + 1.0 + ], + [ + -3.9431214, + 0.96841735, + 1.0 + ], + [ + -0.13730685, + 0.99794483, + 1.0 + ], + [ + -0.0053191665, + 0.999769, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -14.975474, + 0.80612254, + 1.0 + ], + [ + -5.441822, + 0.93426883, + 1.0 + ], + [ + -0.5847607, + 0.97942346, + 1.0 + ], + [ + -0.0973578, + 0.99491835, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -16.96701, + 0.7174082, + 1.0 + ], + [ + -7.501978, + 0.8796432, + 1.0 + ], + [ + -0.55444115, + 0.9846425, + 1.0 + ], + [ + -0.0493976, + 0.9975046, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -19.691572, + 0.6349826, + 1.0 + ], + [ + -11.398152, + 0.7708063, + 1.0 + ], + [ + -0.78708935, + 0.97114944, + 1.0 + ], + [ + -0.013223002, + 0.99905497, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -18.446426, + 0.61982644, + 1.0 + ], + [ + -14.164779, + 0.6866288, + 1.0 + ], + [ + -1.6122752, + 0.95558673, + 1.0 + ], + [ + -0.004578955, + 0.9995476, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -13.488046, + 0.67327017, + 1.0 + ], + [ + -11.041826, + 0.6985784, + 1.0 + ], + [ + -3.797718, + 0.8836202, + 1.0 + ], + [ + -0.021354746, + 0.9991143, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.215271, + 0.79168403, + 1.0 + ], + [ + -5.9288893, + 0.82115835, + 1.0 + ], + [ + -3.627646, + 0.8675918, + 1.0 + ], + [ + -0.10416387, + 0.9819024, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -3.4996793, + 0.90832794, + 1.0 + ], + [ + -1.8338926, + 0.95480883, + 1.0 + ], + [ + -1.2572361, + 0.96503127, + 1.0 + ], + [ + -0.7744646, + 0.9684802, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.1851404, + 0.9754471, + 1.0 + ], + [ + -0.38070896, + 0.9967761, + 1.0 + ], + [ + -0.20595744, + 0.99183595, + 1.0 + ], + [ + -0.04291819, + 0.991432, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.24601784, + 0.9362298, + 1.0 + ], + [ + 0.4131334, + 1.0015087, + 1.0 + ], + [ + 0.6097395, + 1.0106862, + 1.0 + ], + [ + 0.80979943, + 1.0157323, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.15171635, + 0.7862158, + 1.0 + ], + [ + 0.88354856, + 0.9524625, + 1.0 + ], + [ + 1.2969238, + 1.0249856, + 1.0 + ], + [ + 1.3480076, + 1.0304265, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.2697306, + 0.74219435, + 1.0 + ], + [ + 2.2566214, + 0.8504037, + 1.0 + ], + [ + 0.43116477, + 0.98642737, + 1.0 + ], + [ + -0.62197345, + 0.9856124, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.9409392, + 0.7197661, + 1.0 + ], + [ + 2.657014, + 0.82564926, + 1.0 + ], + [ + 0.75268286, + 0.9767613, + 1.0 + ], + [ + 0.055744026, + 0.9941756, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.8314111, + 0.7361832, + 1.0 + ], + [ + 2.2688785, + 0.83848625, + 1.0 + ], + [ + 0.86895645, + 0.979231, + 1.0 + ], + [ + 0.0529236, + 1.0007346, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.6000655, + 0.7524115, + 1.0 + ], + [ + 2.0968463, + 0.888389, + 1.0 + ], + [ + 0.18316315, + 0.9902795, + 1.0 + ], + [ + -0.048968393, + 0.99920696, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.5598903, + 0.763862, + 1.0 + ], + [ + 1.1714306, + 0.8932834, + 1.0 + ], + [ + -0.57951224, + 0.985895, + 1.0 + ], + [ + -0.13511501, + 0.99782085, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.1605225, + 0.7730435, + 1.0 + ], + [ + 1.3596042, + 0.9008872, + 1.0 + ], + [ + -0.47168514, + 0.99376947, + 1.0 + ], + [ + -0.07464302, + 1.000353, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.752158, + 0.79429775, + 1.0 + ], + [ + 0.96804696, + 0.9385464, + 1.0 + ], + [ + -1.3032031, + 0.99664253, + 1.0 + ], + [ + -0.2467647, + 0.99934804, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.692927, + 0.817162, + 1.0 + ], + [ + 1.2399592, + 0.9601324, + 1.0 + ], + [ + -0.2092135, + 0.99869674, + 1.0 + ], + [ + -0.04914082, + 0.9998154, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.3077126, + 0.8646981, + 1.0 + ], + [ + 1.169301, + 0.9719851, + 1.0 + ], + [ + -0.14287929, + 0.9983894, + 1.0 + ], + [ + -0.070898615, + 0.9997087, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.741663, + 0.89205533, + 1.0 + ], + [ + 0.64960855, + 0.9830752, + 1.0 + ], + [ + -0.11191467, + 0.9987406, + 1.0 + ], + [ + -0.047313914, + 0.99977046, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.5162182, + 0.94403803, + 1.0 + ], + [ + 0.07143193, + 0.9925508, + 1.0 + ], + [ + -0.72076577, + 0.9948075, + 1.0 + ], + [ + -0.1948217, + 0.9987798, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.585983, + 0.96052504, + 1.0 + ], + [ + 1.0876106, + 0.9904767, + 1.0 + ], + [ + -0.27299738, + 0.99780345, + 1.0 + ], + [ + -0.080156155, + 0.99953175, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.779256, + 0.9310572, + 1.0 + ], + [ + 0.7266878, + 0.986474, + 1.0 + ], + [ + 0.035173222, + 0.99929446, + 1.0 + ], + [ + 0.0016433608, + 1.0000198, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.0365615, + 0.94872975, + 1.0 + ], + [ + 0.30465418, + 0.9817023, + 1.0 + ], + [ + -0.34721652, + 0.99478245, + 1.0 + ], + [ + -0.061744135, + 0.99972767, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.8961987, + 0.92921335, + 1.0 + ], + [ + 0.23940657, + 0.9766385, + 1.0 + ], + [ + -1.319209, + 0.99436516, + 1.0 + ], + [ + -0.2552591, + 0.9993648, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.820354, + 0.9000567, + 1.0 + ], + [ + 0.93661356, + 0.9792745, + 1.0 + ], + [ + -1.2972772, + 1.0013816, + 1.0 + ], + [ + -0.2005057, + 1.0001276, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.8547032, + 0.8771825, + 1.0 + ], + [ + -0.40298477, + 0.92672235, + 1.0 + ], + [ + -0.76510406, + 0.983101, + 1.0 + ], + [ + -0.10773284, + 0.9988855, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.1437955, + 0.87208176, + 1.0 + ], + [ + -0.35798803, + 0.9345046, + 1.0 + ], + [ + -0.6719991, + 0.9889946, + 1.0 + ], + [ + -0.12287665, + 0.9989714, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.7768846, + 0.90311426, + 1.0 + ], + [ + -0.6762936, + 0.95233446, + 1.0 + ], + [ + -1.8836145, + 0.9920913, + 1.0 + ], + [ + -0.860416, + 0.9946867, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.8961434, + 0.8824976, + 1.0 + ], + [ + -0.33391425, + 0.92908406, + 1.0 + ], + [ + -0.3221192, + 0.9918024, + 1.0 + ], + [ + -0.09044509, + 0.9966171, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 6.8632536, + 0.90186435, + 1.0 + ], + [ + 0.58749425, + 0.9437153, + 1.0 + ], + [ + 0.48553416, + 0.98712, + 1.0 + ], + [ + 0.19696854, + 0.99386424, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.8013225, + 0.9050866, + 1.0 + ], + [ + -0.49256903, + 0.9264555, + 1.0 + ], + [ + -1.7974619, + 0.9501046, + 1.0 + ], + [ + -1.5920177, + 0.97445244, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.4498076, + 0.92861384, + 1.0 + ], + [ + -0.7343135, + 0.9649644, + 1.0 + ], + [ + -1.2369624, + 0.9541305, + 1.0 + ], + [ + -1.1794292, + 0.9540083, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.9042714, + 0.9180204, + 1.0 + ], + [ + -2.219778, + 0.86444235, + 1.0 + ], + [ + -2.4816806, + 0.823798, + 1.0 + ], + [ + -1.5569929, + 0.8375031, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.83755153, + 0.91707826, + 1.0 + ], + [ + 0.22283489, + 0.86605746, + 1.0 + ], + [ + 0.4647223, + 0.8345799, + 1.0 + ], + [ + 0.72768146, + 0.84671277, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.523536, + 0.9087665, + 1.0 + ], + [ + 1.2768013, + 0.85424995, + 1.0 + ], + [ + 2.1177902, + 0.8432673, + 1.0 + ], + [ + 2.3680518, + 0.8740304, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.2746108, + 0.81840515, + 1.0 + ], + [ + 2.7676985, + 0.7900528, + 1.0 + ], + [ + 1.9201541, + 0.8856915, + 1.0 + ], + [ + 1.8866074, + 0.9185119, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.6828327, + 0.8136951, + 1.0 + ], + [ + 2.175628, + 0.84009886, + 1.0 + ], + [ + 2.0939028, + 0.8847502, + 1.0 + ], + [ + 2.5929403, + 0.8947538, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.768817, + 0.7967659, + 1.0 + ], + [ + 3.2244751, + 0.7584971, + 1.0 + ], + [ + 3.6050847, + 0.7831417, + 1.0 + ], + [ + 4.268032, + 0.8045191, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.3562844, + 0.7890957, + 1.0 + ], + [ + 3.7188308, + 0.7199401, + 1.0 + ], + [ + 5.9207273, + 0.6976366, + 1.0 + ], + [ + 7.6120353, + 0.70215327, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.0832772, + 0.7600011, + 1.0 + ], + [ + 2.2862017, + 0.76166797, + 1.0 + ], + [ + 3.6557348, + 0.74157053, + 1.0 + ], + [ + 5.3892546, + 0.731114, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.7183886, + 0.6808121, + 1.0 + ], + [ + 3.3387003, + 0.70804834, + 1.0 + ], + [ + 3.3728793, + 0.7371294, + 1.0 + ], + [ + 4.041886, + 0.73437375, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.624325, + 0.6674578, + 1.0 + ], + [ + 4.5378666, + 0.6732078, + 1.0 + ], + [ + 4.398033, + 0.71216196, + 1.0 + ], + [ + 5.5895276, + 0.7182609, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 5.306699, + 0.747718, + 1.0 + ], + [ + 4.879655, + 0.70281416, + 1.0 + ], + [ + 5.1314225, + 0.7001976, + 1.0 + ], + [ + 5.486214, + 0.72053, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 4.146373, + 0.78986615, + 1.0 + ], + [ + 3.90747, + 0.7194833, + 1.0 + ], + [ + 4.436473, + 0.7135913, + 1.0 + ], + [ + 3.9732492, + 0.7439657, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.051203, + 0.8220685, + 1.0 + ], + [ + 2.5377185, + 0.73653644, + 1.0 + ], + [ + 3.6690881, + 0.73991066, + 1.0 + ], + [ + 2.8405402, + 0.76611686, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.0629168, + 0.88988215, + 1.0 + ], + [ + 1.5405259, + 0.80136687, + 1.0 + ], + [ + 2.3380709, + 0.755409, + 1.0 + ], + [ + 1.6457901, + 0.78002536, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.9272873, + 0.8686442, + 1.0 + ], + [ + 0.6143116, + 0.7969848, + 1.0 + ], + [ + 0.5216986, + 0.7725941, + 1.0 + ], + [ + 0.27696848, + 0.79700214, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.18347983, + 0.7820147, + 1.0 + ], + [ + -0.2920065, + 0.78137934, + 1.0 + ], + [ + -0.7294265, + 0.8024376, + 1.0 + ], + [ + -0.6821398, + 0.81814516, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.23992704, + 0.7879301, + 1.0 + ], + [ + -0.88038236, + 0.8075125, + 1.0 + ], + [ + -0.9809185, + 0.8284354, + 1.0 + ], + [ + -0.07751103, + 0.8428534, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.80605406, + 0.8354792, + 1.0 + ], + [ + -1.559409, + 0.80948555, + 1.0 + ], + [ + -0.7388032, + 0.83500856, + 1.0 + ], + [ + 0.69942343, + 0.88329875, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.6294653, + 0.85172844, + 1.0 + ], + [ + -1.7645487, + 0.8297184, + 1.0 + ], + [ + -0.7100293, + 0.8840055, + 1.0 + ], + [ + 0.2274858, + 0.9601816, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.2752452, + 0.8992305, + 1.0 + ], + [ + -2.2524478, + 0.9024475, + 1.0 + ], + [ + -0.6900573, + 0.96735644, + 1.0 + ], + [ + -0.01443623, + 0.99743444, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.3342621, + 0.9171061, + 1.0 + ], + [ + -2.169138, + 0.9233468, + 1.0 + ], + [ + -0.38522747, + 0.9775049, + 1.0 + ], + [ + 0.0013864042, + 0.9985635, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.9786882, + 0.9581507, + 1.0 + ], + [ + -1.0984265, + 0.9696264, + 1.0 + ], + [ + -0.10668975, + 0.9950265, + 1.0 + ], + [ + -0.0001844623, + 0.99983454, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.8451796, + 0.9728855, + 1.0 + ], + [ + -0.75453144, + 0.9875004, + 1.0 + ], + [ + -0.03869543, + 0.998888, + 1.0 + ], + [ + -0.00016324317, + 0.99999046, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.4833093, + 0.9710839, + 1.0 + ], + [ + -0.4503776, + 0.99415845, + 1.0 + ], + [ + -0.014969094, + 0.9998039, + 1.0 + ], + [ + -0.00005460353, + 0.99999875, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.155806, + 0.9922136, + 1.0 + ], + [ + -0.24781401, + 0.9983759, + 1.0 + ], + [ + -0.009547606, + 0.9999699, + 1.0 + ], + [ + -0.00066759216, + 0.9999967, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.4516653, + 0.99805427, + 1.0 + ], + [ + -0.20918414, + 0.999779, + 1.0 + ], + [ + -0.006043913, + 0.9999931, + 1.0 + ], + [ + -0.00048581572, + 0.99999654, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.9392998, + 0.98668915, + 1.0 + ], + [ + -0.14349174, + 0.99852246, + 1.0 + ], + [ + -0.01090069, + 0.99995995, + 1.0 + ], + [ + -0.00083713053, + 0.99999756, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.1864663, + 0.9834442, + 1.0 + ], + [ + -0.18628179, + 0.99777365, + 1.0 + ], + [ + -0.014109625, + 0.99992806, + 1.0 + ], + [ + -0.001064983, + 0.9999959, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.0547693, + 0.98770183, + 1.0 + ], + [ + -0.17180885, + 0.9951577, + 1.0 + ], + [ + -0.014593571, + 0.99926215, + 1.0 + ], + [ + -0.00052392326, + 0.99998504, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.3548598, + 0.97179633, + 1.0 + ], + [ + -0.51838243, + 0.95328885, + 1.0 + ], + [ + -0.06852023, + 0.99208575, + 1.0 + ], + [ + -0.00085176574, + 0.99991536, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.3932482, + 0.9842496, + 1.0 + ], + [ + -0.21709155, + 0.9691371, + 1.0 + ], + [ + -0.014957193, + 0.99505883, + 1.0 + ], + [ + -0.00015723599, + 0.99995285, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.95742047, + 0.9800242, + 1.0 + ], + [ + -0.09572666, + 0.9719048, + 1.0 + ], + [ + -0.0004223666, + 0.9954867, + 1.0 + ], + [ + 0.000010266345, + 0.9999555, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.8219641, + 0.97176534, + 1.0 + ], + [ + -0.10358055, + 0.9572014, + 1.0 + ], + [ + -0.0028769998, + 0.99260867, + 1.0 + ], + [ + -0.0000138675605, + 0.9999241, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.81078154, + 0.9853913, + 1.0 + ], + [ + -0.07060721, + 0.99701566, + 1.0 + ], + [ + -0.0011506415, + 0.99966466, + 1.0 + ], + [ + -0.0000053977897, + 0.99999666, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.8799515, + 0.9880938, + 1.0 + ], + [ + -0.09139433, + 0.99895227, + 1.0 + ], + [ + -0.0011206032, + 0.99998325, + 1.0 + ], + [ + -0.0000023958048, + 0.99999994, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -1.203021, + 0.98468393, + 1.0 + ], + [ + -0.09533536, + 0.9993307, + 1.0 + ], + [ + -0.0006138411, + 0.9999971, + 1.0 + ], + [ + -4.9110037e-7, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.98682314, + 0.98741007, + 1.0 + ], + [ + -0.07803176, + 0.99942106, + 1.0 + ], + [ + -0.0005139681, + 0.9999974, + 1.0 + ], + [ + -3.048391e-7, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.0533085, + 0.95174795, + 1.0 + ], + [ + -0.15526569, + 0.9967907, + 1.0 + ], + [ + -0.0011854534, + 0.9999825, + 1.0 + ], + [ + -0.0000012432189, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -4.3160176, + 0.8893779, + 1.0 + ], + [ + -0.37086096, + 0.99144685, + 1.0 + ], + [ + -0.0031809295, + 0.9999518, + 1.0 + ], + [ + -0.000015129414, + 0.9999994, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.65379, + 0.80011606, + 1.0 + ], + [ + -0.97619027, + 0.98192537, + 1.0 + ], + [ + -0.011185538, + 0.9998816, + 1.0 + ], + [ + -0.00027599925, + 0.999986, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -13.900999, + 0.6643516, + 1.0 + ], + [ + -2.2590377, + 0.93988913, + 1.0 + ], + [ + -0.021305703, + 0.99952126, + 1.0 + ], + [ + -0.00016454396, + 0.9999922, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -18.50311, + 0.5672969, + 1.0 + ], + [ + -6.324091, + 0.8226143, + 1.0 + ], + [ + -0.09798097, + 0.99709153, + 1.0 + ], + [ + -0.0001432513, + 0.99998575, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -17.64413, + 0.5391097, + 1.0 + ], + [ + -10.467655, + 0.66710144, + 1.0 + ], + [ + -0.2899716, + 0.9890324, + 1.0 + ], + [ + -0.00047773414, + 0.9999807, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -13.92279, + 0.5472615, + 1.0 + ], + [ + -10.895535, + 0.59090734, + 1.0 + ], + [ + -2.0764418, + 0.9041057, + 1.0 + ], + [ + -0.006843486, + 0.9997131, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.823085, + 0.59954053, + 1.0 + ], + [ + -8.007232, + 0.64031166, + 1.0 + ], + [ + -5.934222, + 0.7216933, + 1.0 + ], + [ + -0.21140227, + 0.98925734, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -6.0854692, + 0.68398213, + 1.0 + ], + [ + -4.9988737, + 0.7475099, + 1.0 + ], + [ + -4.5387487, + 0.783282, + 1.0 + ], + [ + -3.1189833, + 0.85521823, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -4.5944576, + 0.7649848, + 1.0 + ], + [ + -4.2297106, + 0.8085376, + 1.0 + ], + [ + -4.0575027, + 0.8316775, + 1.0 + ], + [ + -3.6946223, + 0.857067, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -4.07587, + 0.76617646, + 1.0 + ], + [ + -3.7157166, + 0.84208333, + 1.0 + ], + [ + -3.0963829, + 0.88380915, + 1.0 + ], + [ + -2.4804156, + 0.9129343, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.87624, + 0.6232938, + 1.0 + ], + [ + -2.6126328, + 0.7971155, + 1.0 + ], + [ + -1.7279347, + 0.92270774, + 1.0 + ], + [ + -1.2290946, + 0.95807064, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.7994735, + 0.57053953, + 1.0 + ], + [ + -0.84233916, + 0.6636242, + 1.0 + ], + [ + -2.5714352, + 0.87382954, + 1.0 + ], + [ + -3.015385, + 0.8965249, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.3739244, + 0.61843336, + 1.0 + ], + [ + 0.4184842, + 0.76101744, + 1.0 + ], + [ + -0.6304034, + 0.94809633, + 1.0 + ], + [ + -0.7494771, + 0.9597989, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.97610384, + 0.7327941, + 1.0 + ], + [ + -0.014398668, + 0.87566924, + 1.0 + ], + [ + -0.052118707, + 0.9924021, + 1.0 + ], + [ + 0.008521295, + 0.99918205, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.5996346, + 0.8233891, + 1.0 + ], + [ + 0.3935372, + 0.9229845, + 1.0 + ], + [ + -0.09391027, + 0.99190533, + 1.0 + ], + [ + -0.03780514, + 0.998876, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.9291275, + 0.9039172, + 1.0 + ], + [ + 0.74847955, + 0.9787055, + 1.0 + ], + [ + -0.16242525, + 0.9931886, + 1.0 + ], + [ + -0.072900325, + 0.9978617, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.3570204, + 0.94836956, + 1.0 + ], + [ + 0.2503319, + 0.99067676, + 1.0 + ], + [ + -0.06907828, + 0.9989688, + 1.0 + ], + [ + -0.017280824, + 0.9998503, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.26804498, + 0.9617563, + 1.0 + ], + [ + -0.30944353, + 0.9933201, + 1.0 + ], + [ + -0.83434904, + 0.9959094, + 1.0 + ], + [ + -0.16132122, + 0.999225, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.45194325, + 0.97071046, + 1.0 + ], + [ + -0.040852666, + 0.99767536, + 1.0 + ], + [ + -0.18153699, + 0.9991148, + 1.0 + ], + [ + -0.03368746, + 0.99983805, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.58231443, + 0.987187, + 1.0 + ], + [ + -0.11279778, + 0.9996504, + 1.0 + ], + [ + -0.007223121, + 0.999977, + 1.0 + ], + [ + -0.0013776413, + 0.9999941, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.27319077, + 0.9664841, + 1.0 + ], + [ + -0.21705957, + 0.9958635, + 1.0 + ], + [ + -0.017606068, + 0.99986523, + 1.0 + ], + [ + -0.0026193615, + 0.9999856, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.21634439, + 0.9787581, + 1.0 + ], + [ + -0.2651225, + 0.9962405, + 1.0 + ], + [ + -0.19124968, + 0.99883384, + 1.0 + ], + [ + -0.042182684, + 0.99975127, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.008343396, + 0.96074563, + 1.0 + ], + [ + -0.06034358, + 0.99365026, + 1.0 + ], + [ + -0.07409725, + 0.99946153, + 1.0 + ], + [ + -0.0167451, + 0.99990094, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.21916604, + 0.97861654, + 1.0 + ], + [ + 0.011656337, + 0.9968962, + 1.0 + ], + [ + 0.000026073394, + 0.9999265, + 1.0 + ], + [ + -0.00011303921, + 0.9999988, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.8516588, + 0.983058, + 1.0 + ], + [ + 0.05317016, + 0.9960106, + 1.0 + ], + [ + 0.0021729777, + 0.9996684, + 1.0 + ], + [ + -0.00008834622, + 0.9999944, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.33492264, + 0.97982824, + 1.0 + ], + [ + -0.023441888, + 0.9941379, + 1.0 + ], + [ + -0.012700134, + 0.9997447, + 1.0 + ], + [ + -0.0015705364, + 0.9999959, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.98577476, + 0.9558412, + 1.0 + ], + [ + 0.11146073, + 0.9861902, + 1.0 + ], + [ + -0.039014447, + 0.9995021, + 1.0 + ], + [ + -0.004039938, + 0.9999989, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 1.4198768, + 0.92014015, + 1.0 + ], + [ + 0.017510591, + 0.95569116, + 1.0 + ], + [ + -0.07619311, + 0.99643916, + 1.0 + ], + [ + -0.0026185187, + 0.99994844, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.02523843, + 0.90478045, + 1.0 + ], + [ + -1.0690092, + 0.92566043, + 1.0 + ], + [ + -0.27379805, + 0.99030024, + 1.0 + ], + [ + -0.012234708, + 0.9998211, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.112236924, + 0.94679946, + 1.0 + ], + [ + -0.619657, + 0.96057886, + 1.0 + ], + [ + -0.9257425, + 0.9906025, + 1.0 + ], + [ + -0.29495418, + 0.99792683, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.21400073, + 0.91529566, + 1.0 + ], + [ + -0.7458604, + 0.93239814, + 1.0 + ], + [ + -1.0380262, + 0.98454165, + 1.0 + ], + [ + -0.59706265, + 0.99349016, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.4457225, + 0.91300154, + 1.0 + ], + [ + -1.336635, + 0.9316419, + 1.0 + ], + [ + -2.2979734, + 0.9655022, + 1.0 + ], + [ + -1.344974, + 0.9845655, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.25027204, + 0.9015076, + 1.0 + ], + [ + -3.4001799, + 0.8766961, + 1.0 + ], + [ + -6.167441, + 0.87958527, + 1.0 + ], + [ + -3.0434742, + 0.9592666, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.8902801, + 0.87916595, + 1.0 + ], + [ + -3.0949912, + 0.86760527, + 1.0 + ], + [ + -5.8532324, + 0.81513554, + 1.0 + ], + [ + -4.114237, + 0.86317617, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.026975641, + 0.99708, + 1.0 + ], + [ + -0.64516276, + 0.9682286, + 1.0 + ], + [ + -2.8310719, + 0.86257124, + 1.0 + ], + [ + -1.7226968, + 0.90401584, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.082811065, + 0.9958276, + 1.0 + ], + [ + 0.029035931, + 0.98919505, + 1.0 + ], + [ + -0.10363939, + 0.9313849, + 1.0 + ], + [ + 0.010599485, + 0.93757856, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.30115137, + 0.995358, + 1.0 + ], + [ + 0.13033178, + 0.99306947, + 1.0 + ], + [ + 0.6179642, + 0.9668538, + 1.0 + ], + [ + 0.8925493, + 0.96109754, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.46534216, + 0.9799625, + 1.0 + ], + [ + 0.38193727, + 0.97773105, + 1.0 + ], + [ + 0.24684222, + 0.9894079, + 1.0 + ], + [ + 0.37070882, + 0.987069, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.28666112, + 0.9715252, + 1.0 + ], + [ + 0.6129641, + 0.9612684, + 1.0 + ], + [ + 1.5519472, + 0.9354917, + 1.0 + ], + [ + 2.0365298, + 0.9501482, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.5976872, + 0.95618755, + 1.0 + ], + [ + 2.4345472, + 0.8642554, + 1.0 + ], + [ + 4.334619, + 0.8086935, + 1.0 + ], + [ + 4.563015, + 0.845538, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.8365076, + 0.92973554, + 1.0 + ], + [ + 4.732671, + 0.7514679, + 1.0 + ], + [ + 8.749074, + 0.67412585, + 1.0 + ], + [ + 9.21404, + 0.70109785, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.420841, + 0.77831614, + 1.0 + ], + [ + 3.1613727, + 0.7190518, + 1.0 + ], + [ + 5.003848, + 0.7066909, + 1.0 + ], + [ + 6.8984137, + 0.7076504, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 3.2887714, + 0.6649921, + 1.0 + ], + [ + 3.2296984, + 0.6861676, + 1.0 + ], + [ + 3.542693, + 0.7290553, + 1.0 + ], + [ + 3.7580287, + 0.7677797, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 2.8353093, + 0.7206155, + 1.0 + ], + [ + 3.213582, + 0.7210075, + 1.0 + ], + [ + 3.050782, + 0.7906282, + 1.0 + ], + [ + 2.910971, + 0.86843896, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.73087895, + 0.9480793, + 1.0 + ], + [ + 1.3393304, + 0.90520597, + 1.0 + ], + [ + 1.8712325, + 0.8813981, + 1.0 + ], + [ + 1.7535455, + 0.9178119, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.2656817, + 0.9784777, + 1.0 + ], + [ + 0.6957168, + 0.93848366, + 1.0 + ], + [ + 1.0034746, + 0.92618793, + 1.0 + ], + [ + 0.6805362, + 0.95591986, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.07101682, + 0.9880643, + 1.0 + ], + [ + 0.3550513, + 0.96534646, + 1.0 + ], + [ + 0.7367733, + 0.952579, + 1.0 + ], + [ + 0.41733772, + 0.96733385, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.021202909, + 0.99511874, + 1.0 + ], + [ + 0.03845068, + 0.98727, + 1.0 + ], + [ + 0.13032648, + 0.9788461, + 1.0 + ], + [ + 0.08247721, + 0.9886881, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.019193187, + 0.99454707, + 1.0 + ], + [ + -0.01639945, + 0.9824369, + 1.0 + ], + [ + -0.034814365, + 0.97299594, + 1.0 + ], + [ + 0.000092838956, + 0.9879655, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.0020077226, + 0.98501676, + 1.0 + ], + [ + -0.01932977, + 0.98477453, + 1.0 + ], + [ + -0.08714962, + 0.9838237, + 1.0 + ], + [ + -0.05911569, + 0.99138635, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.028955147, + 0.9820292, + 1.0 + ], + [ + -0.0068421336, + 0.9893989, + 1.0 + ], + [ + -0.066570766, + 0.99078333, + 1.0 + ], + [ + -0.044429842, + 0.995392, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.032754235, + 0.99281675, + 1.0 + ], + [ + -0.024249062, + 0.9940026, + 1.0 + ], + [ + -0.016773215, + 0.99763656, + 1.0 + ], + [ + -0.00368944, + 0.99956965, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.013820456, + 0.9963605, + 1.0 + ], + [ + -0.023061087, + 0.99682546, + 1.0 + ], + [ + -0.006765129, + 0.99912953, + 1.0 + ], + [ + -0.00025614, + 0.99994946, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.03831513, + 0.9965622, + 1.0 + ], + [ + -0.026358912, + 0.9984241, + 1.0 + ], + [ + -0.0031044614, + 0.99983025, + 1.0 + ], + [ + -0.00004853841, + 0.99999666, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.042360652, + 0.99709636, + 1.0 + ], + [ + -0.02741898, + 0.99857044, + 1.0 + ], + [ + -0.0024811549, + 0.99984425, + 1.0 + ], + [ + -0.0000068021504, + 0.99999624, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.015694104, + 0.99947315, + 1.0 + ], + [ + -0.004300875, + 0.9996656, + 1.0 + ], + [ + -0.00022572257, + 0.99996036, + 1.0 + ], + [ + -2.7103656e-8, + 0.9999994, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.031804975, + 0.9993172, + 1.0 + ], + [ + -0.0036146475, + 0.9998796, + 1.0 + ], + [ + -0.00006266851, + 0.9999905, + 1.0 + ], + [ + -2.0947002e-7, + 0.99999994, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.03432048, + 0.99876255, + 1.0 + ], + [ + -0.0027313528, + 0.999917, + 1.0 + ], + [ + -0.00002901146, + 0.99999934, + 1.0 + ], + [ + -5.272942e-8, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.010446178, + 0.9998674, + 1.0 + ], + [ + -0.0009102314, + 0.9999884, + 1.0 + ], + [ + -0.000010674661, + 0.9999999, + 1.0 + ], + [ + -6.7580764e-8, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.0119908275, + 0.9999285, + 1.0 + ], + [ + -0.00078701315, + 0.9999947, + 1.0 + ], + [ + -0.0000061383644, + 0.99999994, + 1.0 + ], + [ + -3.791176e-8, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.005170042, + 0.99979734, + 1.0 + ], + [ + -0.0004141604, + 0.9999835, + 1.0 + ], + [ + -0.0000034080406, + 0.9999999, + 1.0 + ], + [ + -4.582497e-8, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.00895596, + 0.9998542, + 1.0 + ], + [ + -0.000709496, + 0.99998856, + 1.0 + ], + [ + -0.0000056245453, + 0.99999994, + 1.0 + ], + [ + -5.8625062e-8, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.0146880755, + 0.9997208, + 1.0 + ], + [ + -0.0018232282, + 0.9998647, + 1.0 + ], + [ + -0.00015326608, + 0.9999809, + 1.0 + ], + [ + -0.0000014805827, + 0.9999998, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.022968672, + 0.99913424, + 1.0 + ], + [ + -0.014809832, + 0.99827707, + 1.0 + ], + [ + -0.0020878739, + 0.9997331, + 1.0 + ], + [ + -0.000020184694, + 0.99999744, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.011431702, + 0.99970055, + 1.0 + ], + [ + -0.0039257053, + 0.999323, + 1.0 + ], + [ + -0.00048654518, + 0.9998965, + 1.0 + ], + [ + -0.000004697737, + 0.999999, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.0041034487, + 0.99944717, + 1.0 + ], + [ + -0.00006548014, + 0.99873835, + 1.0 + ], + [ + 0.000027589553, + 0.9997964, + 1.0 + ], + [ + 2.7555294e-7, + 0.999998, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.009381899, + 0.9987364, + 1.0 + ], + [ + -0.00043260615, + 0.9972713, + 1.0 + ], + [ + 0.000017650782, + 0.9995557, + 1.0 + ], + [ + 2.0419672e-7, + 0.99999547, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.008616836, + 0.99971557, + 1.0 + ], + [ + -0.00045231776, + 0.9998694, + 1.0 + ], + [ + -9.984535e-7, + 0.9999807, + 1.0 + ], + [ + 7.740104e-9, + 0.9999998, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.0056563327, + 0.9998474, + 1.0 + ], + [ + -0.00033137825, + 0.99999225, + 1.0 + ], + [ + -0.0000016325387, + 1.0, + 1.0 + ], + [ + -1.0257498e-9, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.014827684, + 0.99960244, + 1.0 + ], + [ + -0.0007938461, + 0.99998146, + 1.0 + ], + [ + -0.00000293517, + 0.99999994, + 1.0 + ], + [ + -8.377253e-10, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.011590649, + 0.9997409, + 1.0 + ], + [ + -0.00065725995, + 0.9999863, + 1.0 + ], + [ + -0.0000025172164, + 0.99999994, + 1.0 + ], + [ + -7.089178e-10, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.11034412, + 0.99728817, + 1.0 + ], + [ + -0.006188652, + 0.9998468, + 1.0 + ], + [ + -0.000022063721, + 0.99999946, + 1.0 + ], + [ + -5.30563e-9, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.11615519, + 0.9962155, + 1.0 + ], + [ + -0.006629864, + 0.9997772, + 1.0 + ], + [ + -0.000026530815, + 0.99999917, + 1.0 + ], + [ + -9.810042e-9, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.2699671, + 0.992111, + 1.0 + ], + [ + -0.01619867, + 0.99955577, + 1.0 + ], + [ + -0.0000889456, + 0.99999845, + 1.0 + ], + [ + -7.3219084e-8, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.84938484, + 0.9749287, + 1.0 + ], + [ + -0.0546363, + 0.99835104, + 1.0 + ], + [ + -0.00042785166, + 0.99998784, + 1.0 + ], + [ + -6.3949227e-7, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -2.9408712, + 0.91166395, + 1.0 + ], + [ + -0.28487697, + 0.99084896, + 1.0 + ], + [ + -0.0041404446, + 0.99987274, + 1.0 + ], + [ + -0.00000840042, + 0.99999976, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -6.1285825, + 0.7972419, + 1.0 + ], + [ + -0.82483685, + 0.96542543, + 1.0 + ], + [ + -0.007825383, + 0.99962455, + 1.0 + ], + [ + -0.0000082233155, + 0.9999996, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.914042, + 0.6662763, + 1.0 + ], + [ + -3.2296083, + 0.84786505, + 1.0 + ], + [ + -0.09797766, + 0.9948829, + 1.0 + ], + [ + -0.000322063, + 0.999986, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.49761, + 0.5991851, + 1.0 + ], + [ + -8.209785, + 0.6332046, + 1.0 + ], + [ + -2.2356362, + 0.88977855, + 1.0 + ], + [ + -0.03691413, + 0.9986572, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -8.16435, + 0.6105177, + 1.0 + ], + [ + -8.482127, + 0.6289409, + 1.0 + ], + [ + -8.195307, + 0.6653821, + 1.0 + ], + [ + -1.5681657, + 0.93180954, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.227832, + 0.6095772, + 1.0 + ], + [ + -9.688233, + 0.62955517, + 1.0 + ], + [ + -9.770227, + 0.6534116, + 1.0 + ], + [ + -7.684848, + 0.7312607, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -9.040249, + 0.61375934, + 1.0 + ], + [ + -9.414042, + 0.64601433, + 1.0 + ], + [ + -9.124609, + 0.6859369, + 1.0 + ], + [ + -6.8619404, + 0.76940507, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -4.1956196, + 0.65328926, + 1.0 + ], + [ + -6.4505258, + 0.6780245, + 1.0 + ], + [ + -6.7652173, + 0.777878, + 1.0 + ], + [ + -5.9136004, + 0.9036394, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.8208507, + 0.76508826, + 1.0 + ], + [ + -1.3313887, + 0.8808544, + 1.0 + ], + [ + -9.083516, + 0.86273944, + 1.0 + ], + [ + -10.492065, + 0.8505087, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.051133215, + 0.93782914, + 1.0 + ], + [ + -0.07260832, + 0.9849166, + 1.0 + ], + [ + -0.9264943, + 0.9826398, + 1.0 + ], + [ + -1.1677287, + 0.97983795, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.07751575, + 0.97048557, + 1.0 + ], + [ + -0.043188777, + 0.9823848, + 1.0 + ], + [ + -0.002534433, + 0.99882054, + 1.0 + ], + [ + -0.00013920956, + 0.9999524, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.07916742, + 0.97018796, + 1.0 + ], + [ + -0.031966496, + 0.97699416, + 1.0 + ], + [ + -0.0035643438, + 0.99836385, + 1.0 + ], + [ + -0.00040987533, + 0.99998116, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.01877379, + 0.9972593, + 1.0 + ], + [ + 0.01205297, + 0.9989108, + 1.0 + ], + [ + -0.0013474375, + 0.99986976, + 1.0 + ], + [ + -0.0007788474, + 0.9999769, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.00081947824, + 0.99965537, + 1.0 + ], + [ + 0.00408295, + 0.9999879, + 1.0 + ], + [ + -0.0004688936, + 0.9999954, + 1.0 + ], + [ + -0.00016395445, + 0.9999985, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.0022904403, + 0.9998439, + 1.0 + ], + [ + -0.0030316855, + 0.9999691, + 1.0 + ], + [ + -0.008458131, + 0.9999591, + 1.0 + ], + [ + -0.0015517423, + 0.99999255, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.008486173, + 0.9998458, + 1.0 + ], + [ + 0.00014019103, + 0.99998724, + 1.0 + ], + [ + -0.0017433885, + 0.99999154, + 1.0 + ], + [ + -0.00032057063, + 0.99999845, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.005678298, + 0.9997919, + 1.0 + ], + [ + -0.0006329326, + 0.99999183, + 1.0 + ], + [ + -0.000014824767, + 1.0, + 1.0 + ], + [ + -0.0000014804741, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.024680974, + 0.9990245, + 1.0 + ], + [ + 0.0004288364, + 0.9999345, + 1.0 + ], + [ + -0.000051545532, + 0.99999934, + 1.0 + ], + [ + -0.000005476217, + 0.99999994, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.0061284457, + 0.9994695, + 1.0 + ], + [ + -0.0008772137, + 0.9999514, + 1.0 + ], + [ + -0.0005471564, + 0.9999964, + 1.0 + ], + [ + -0.00011416538, + 0.99999934, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.019823374, + 0.99714184, + 1.0 + ], + [ + 0.0006233722, + 0.99973315, + 1.0 + ], + [ + -0.00020987287, + 0.99999684, + 1.0 + ], + [ + -0.000045174842, + 0.99999976, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.0055233804, + 0.99932915, + 1.0 + ], + [ + 0.00019314369, + 0.999932, + 1.0 + ], + [ + -0.000004102488, + 0.9999994, + 1.0 + ], + [ + -3.2671397e-7, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.017069608, + 0.99980605, + 1.0 + ], + [ + 0.0011378126, + 0.9999736, + 1.0 + ], + [ + 0.0000045061006, + 0.9999994, + 1.0 + ], + [ + 4.9558377e-8, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.0025704305, + 0.9998648, + 1.0 + ], + [ + -0.00016181398, + 0.9999653, + 1.0 + ], + [ + -0.000009765289, + 0.9999992, + 1.0 + ], + [ + -2.9514266e-7, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.011738374, + 0.99957114, + 1.0 + ], + [ + 0.0013190353, + 0.99990016, + 1.0 + ], + [ + 9.2287684e-7, + 0.99999744, + 1.0 + ], + [ + -0.0000011511228, + 1.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.04013104, + 0.99884504, + 1.0 + ], + [ + 0.00018569271, + 0.99945897, + 1.0 + ], + [ + -0.0010192711, + 0.9999544, + 1.0 + ], + [ + -0.000013688198, + 0.9999996, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.02411725, + 0.99782336, + 1.0 + ], + [ + -0.09172129, + 0.9966743, + 1.0 + ], + [ + -0.017824568, + 0.9994446, + 1.0 + ], + [ + -0.00024212734, + 0.9999933, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.0085046105, + 0.9991983, + 1.0 + ], + [ + -0.032236904, + 0.9988923, + 1.0 + ], + [ + -0.03258688, + 0.9995004, + 1.0 + ], + [ + -0.022160465, + 0.99972326, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.047657337, + 0.9974346, + 1.0 + ], + [ + -0.17287616, + 0.9967666, + 1.0 + ], + [ + -1.7228909, + 0.97827595, + 1.0 + ], + [ + -1.5301245, + 0.9807228, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.1092619, + 0.9939699, + 1.0 + ], + [ + -0.28823772, + 0.9942406, + 1.0 + ], + [ + -2.4151208, + 0.9694442, + 1.0 + ], + [ + -2.120977, + 0.973369, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.046760842, + 0.99341387, + 1.0 + ], + [ + -0.46058732, + 0.9879337, + 1.0 + ], + [ + -0.9992375, + 0.97709274, + 1.0 + ], + [ + -0.27791965, + 0.9942543, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + -0.055317637, + 0.988727, + 1.0 + ], + [ + -0.7323104, + 0.971684, + 1.0 + ], + [ + -2.933847, + 0.90179116, + 1.0 + ], + [ + -1.3042065, + 0.95250934, + 1.0 + ] + ], + "srgb_value": true + } +} \ No newline at end of file diff --git a/crates/engine/src/camera_profiles.rs b/crates/engine/src/camera_profiles.rs index c7b6c437b..9e6d2094e 100644 --- a/crates/engine/src/camera_profiles.rs +++ b/crates/engine/src/camera_profiles.rs @@ -28,6 +28,7 @@ pub const BUNDLED: &[(&str, &str)] = &[ ("ILCA-99M2", include_str!("../../../assets/camera-profiles/ILCA-99M2.json")), ("ILCE-7CR", include_str!("../../../assets/camera-profiles/ILCE-7CR.json")), ("ILCE-7M4", include_str!("../../../assets/camera-profiles/ILCE-7M4.json")), + ("ILCE-7RM5", include_str!("../../../assets/camera-profiles/ILCE-7RM5.json")), ("NIKON D5300", include_str!("../../../assets/camera-profiles/NIKON_D5300.json")), ("NIKON Z 6_2", include_str!("../../../assets/camera-profiles/NIKON_Z_6_2.json")), ("NIKON Z 8", include_str!("../../../assets/camera-profiles/NIKON_Z_8.json")),