-linux-aarch64.rpm` | |
@@ -602,8 +645,6 @@ ArtCraft Team and are not covered by this license. They may be used only unmodif
part of this repository and LightCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
Forks and modified versions must remove them.
-Adobe, Photoshop, Illustrator, Premiere Pro, Lightroom, Acrobat, After Effects and InDesign are trademarks or registered trademarks of Adobe Inc. in the United States and/or other countries. LightCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Adobe Inc.; these names are used only to describe the workflows it is compatible with.
-
Made by the ArtCraft team and community.
diff --git a/README.zh-hans.md b/README.zh-hans.md
index 0a7feb32c..58e1d627f 100644
--- a/README.zh-hans.md
+++ b/README.zh-hans.md
@@ -18,7 +18,7 @@
- 照片图库与 raw 显影;Adobe Lightroom 的开源净室实现,用纯 Rust 重建。
+ 照片图库与 raw 显影;开源、非破坏性的照片图库与 raw 显影工具,用纯 Rust 构建。
在 macOS、Windows 和 Linux 上原生运行,也可通过 WebAssembly 在浏览器中使用,并能由 AI 智能体经 MCP 端到端驱动。
@@ -299,12 +299,12 @@ lightcraft --control 7980 ~/Pictures/trip
LightCraft 很年轻,而且进展很快。**我们如实所处的位置**(细节见[路线图](ROADMAP.md#where-we-stand)):
-- **按功能数量算,我们达到了 Lightroom 的约 79%**(核心功能 98%),逐行记录在
+- **按功能数量算,我们达到了约 79%**(核心功能 98%),逐行记录在
[docs/parity.md](docs/parity.md)。
-- **作为日常替代 Lightroom 的工具,我们更接近 60–70%。** 在单台机器上处理 JPEG/DNG 以及大多数尼康/索尼/
+- **作为日常使用的工具,我们更接近 60–70%。** 在单台机器上处理 JPEG/DNG 以及大多数尼康/索尼/
较老佳能的 raw 都很出色。
- **最大的缺口:**
- - **相机色彩校准:** 索尼、尼康、松下、富士和佳能 CR3 的 raw 会从各自的相机 JPEG 得到受保护的估计值,内置 ILCE-7CR、ILCE-7M4、X-H2S 和 X-T4 配置文件;实测校准仍然缺失,其他 raw 或被拒绝的拟合会保留中性矩阵;
+ - **相机色彩校准:** 索尼、尼康、松下、富士和佳能 CR3 的 raw 会从各自的相机 JPEG 得到受保护的估计值,内置 ILCA-99M2、ILCE-7CR、ILCE-7M4、X-H2S、X-T4、尼康 Z 6II、Z 8 和 D5300 配置文件;实测校准仍然缺失,其他 raw 或被拒绝的拟合会保留中性矩阵;
- **压缩的奥林巴斯 raw 和不受支持的 CR3 变体:** 这些在有内嵌 JPEG 预览时使用预览。富士无损/有损压缩 RAF 现在能解码传感器数据;[验证与已有图库重新加载说明](docs/raf-compression.md);
- **AI 蒙版与降噪:** 主体和天空选择目前是经典启发式算法;
- **屏幕上的 HDR 显示、视频,以及经典版式中的打印/画册/地图模块。**
@@ -453,9 +453,9 @@ egui 前端是一个可替换的 crate;它下面没有任何东西知道 UI
人和智能体遵循同样的规则,所以请先读 [AGENTS.md](AGENTS.md)。简短版本:
-- **净室。** 绝不阅读 Adobe 的二进制文件或 GPL 的 raw/照片代码(darktable、RawTherapee、LibRaw、rawspeed、dcraw……);
+- **净室。** 绝不阅读专有软件的二进制文件或 GPL 的 raw/照片代码(darktable、RawTherapee、LibRaw、rawspeed、dcraw……);
只依据公开规范和黑盒观察来工作。
-- **绝不使用 Adobe 资源:** 不使用 Adobe 产品的图标、截图、预设、配置文件、LUT 或字体。仓库中的每一个
+- **绝不使用专有资源:** 不使用商业产品的图标、截图、预设、配置文件、LUT 或字体。仓库中的每一个
图像、图标和字体都是原创、公有领域、知识共享、OFL 或宽松许可的,并在同一提交中
于 [assets/ATTRIBUTION.md](assets/ATTRIBUTION.md) 添加条目。新字体提交到
[storytold/craft-fonts](https://github.com/storytold/craft-fonts),而不是这里。
@@ -579,8 +579,6 @@ ArtCraft 团队的商标,不在本许可范围内。它们只能在未修改
本仓库和 LightCraft 的一部分,按 [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt) 使用。
分支和修改版本必须移除它们。
-Adobe、Photoshop、Illustrator、Premiere Pro、Lightroom、Acrobat、After Effects 和 InDesign 是 Adobe Inc. 在美国和/或其他国家的商标或注册商标。LightCraft 是一个独立的开源项目,与 Adobe Inc. 无隶属关系,也未获其赞助或认可;这些名称仅用于说明它所兼容的工作流。
-
由 ArtCraft 团队和社区制作。
diff --git a/ROADMAP.md b/ROADMAP.md
index eefb39b45..f142cd226 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~14 points and ~340–600 h away
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (full-resolution zoom done) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (RAW: 83% of Adobe's models decode, 38% verified after the camera-coverage sweep #1327) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
One-page summary of how close LightCraft is to Adobe Lightroom. Agents: read this, then pick work
from [docs/gaps.md](docs/gaps.md). The full assessment, method and evidence are in
@@ -12,11 +12,11 @@ from [docs/gaps.md](docs/gaps.md). The full assessment, method and evidence are
| | Value | Kind |
|---|---:|---|
-| **Feature breadth** (checklist) | **81.1%** of 517 rows (P0 98.0%, P1 95.1%, P2 46.9%) | measured: [parity-checklist.md](docs/parity-checklist.md) recount |
+| **Feature breadth** (checklist) | **81.6%** of 532 rows (P0 98.0%, P1 95.2%, P2 50.0%) | measured: [parity-checklist.md](docs/parity-checklist.md) recount |
| **Feature breadth** (weighted by use) | **~77%** | estimated |
| **Ready for real work** (full target) | **~61%** (56–66%) | estimated: weighted sum over the dimensions |
-| **Mainstream practitioner** | **~49%** (44–54%) | estimated |
-| **Essentials user** | **~64%** (59–69%) | estimated |
+| **Mainstream practitioner** | **~51%** (46–56%) | estimated |
+| **Essentials user** | **~65%** (60–70%) | estimated |
| Remaining to **beta** | **~340–600 h** (Opus 5.5 agent hours, ~60% parallel) | estimated |
| Remaining to **full parity** | **~1,000–1,800 h** (~70% parallel) | estimated |
@@ -25,15 +25,15 @@ Each audience with its own hours (Opus 5.5 agent wall-clock hours to ~95%):
| Audience | Ready | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates |
|---|---:|---:|---|
| Full target (ready for real work) | **~61%** | **1,000–1,800 h** (~70% parallelizes) | raw colour, lens and camera data; AI models; Classic output modules; localization; plug-in ecosystem |
-| Mainstream practitioner | **~49%** | **450–840 h** (~60% parallelizes) | camera colour and render fidelity; CR3 / ORF / HE NEF; lens database; subject / sky masks; Lightroom XMP exchange; stability |
-| Essentials user | **~64%** | **160–300 h** (~50% parallelizes) | default raw look for popular cameras; launch stability; basic-slider feel; opening CR3 / HEIC files people send |
+| Mainstream practitioner | **~51%** | **450–840 h** (~60% parallelizes) | camera colour and render fidelity; CR3 / ORF / HE NEF; lens database; subject / sky masks; Lightroom XMP exchange; stability |
+| Essentials user | **~65%** | **160–300 h** (~50% parallelizes) | default raw look for popular cameras; launch stability; basic-slider feel; opening CR3 / HEIC files people send |
The gap between breadth and readiness is quality and coverage, not missing buttons: camera colour is
-estimated rather than measured, CR3 decodes on four verified Canon bodies, compressed ORF and HE NEF
+estimated rather than measured, CR3 decodes on 26 verified Canon bodies (rarer CRX variants still open as previews), compressed ORF and HE NEF
open as previews, rendering is tuned by eye, AI masks are heuristics, there is no lens database, and
tests run in CI on FreeBSD only. **Why alpha:** core workflows run end to end, but readiness is below
-the ~75% beta bar (it is above the ~40% alpha bar) and Lightroom's main inputs, raw files, still have blocking gaps (Canon, ~30% of
-raw users, decodes CR3 on four verified bodies). It passes the alpha gate: all six core workflows
+the ~75% beta bar (it is above the ~40% alpha bar) and Lightroom's main inputs, raw files, still have blocking gaps (compressed ORF and HE NEF open as previews; Canon,
+~30% of raw users, decodes CR3 on 26 verified bodies). It passes the alpha gate: all six core workflows
(import, cull, develop, save and reopen, batch looks, export) work end to end on macOS
([docs/roadmap.md](docs/roadmap.md#alpha-gate)). The three readiness numbers and their weights and discounts are in
[target-app-parity.md](docs/target-app-parity.md#ready-for-real-work-mainstream-practitioner-and-essentials-user).
@@ -45,11 +45,11 @@ Hours are calibrated on this repo's PR history; see
| Dimension | Ready | Breadth | Remaining | Doc |
|---|---:|---:|---:|---|
| Features | ~61% | ~77% (checklist 81.1%) | 600–1,100 h | [target-app-parity](docs/target-app-parity.md#features) |
-| ↳ RAW cameras, colour, lens profiles | ~56% | 81% of 1,446 Adobe models decode, 16% verified | 255–480 h | [raw-parity](docs/raw-parity.md) |
-| UI/UX fidelity | ~70% | ~85% | 34–58 h | [ui-parity](docs/ui-parity.md) |
+| ↳ RAW cameras, colour, lens profiles | ~60% | 83% of 1,446 Adobe models decode, 38% verified | 255–480 h | [raw-parity](docs/raw-parity.md) |
+| UI/UX fidelity | ~70% | ~85% | 32–54 h | [ui-parity](docs/ui-parity.md) |
| File formats (non-raw) | ~70% | ~85% | 30–55 h | [file-format-parity](docs/file-format-parity.md) |
| Hardware | ~40% | ~50% | 40–80 h | [hardware-parity](docs/hardware-parity.md) |
-| Localization | ~52% of the 12 key languages | 7 of 12 present | 130–210 h (+145–255 h new scripts) | [localization-parity](docs/localization-parity.md) |
+| Localization | ~55% of the 12 key languages | 7 of 12 present | 130–210 h (+145–255 h new scripts) | [localization-parity](docs/localization-parity.md) |
| Performance | ~70% | ~80% | 30–50 h | [target-app-parity](docs/target-app-parity.md#by-dimension) |
| Stability | ~60% | — | 30–60 h | [target-app-parity](docs/target-app-parity.md#by-dimension) |
| Platforms | ~60% | macOS, Windows, Linux, FreeBSD, web | 25–45 h | [target-app-parity](docs/target-app-parity.md#by-dimension) |
@@ -61,7 +61,7 @@ Hours are calibrated on this repo's PR history; see
| Area | Ready | Breadth | Remaining |
|---|---:|---:|---:|
| Import and library | 80% | 90% | 25–45 h |
-| RAW decoding and camera coverage | 77% | 81% | 120–220 h |
+| RAW decoding and camera coverage | 77% | 82% | 120–220 h |
| Colour, camera profiles, render fidelity | 35% | 65% | 80–150 h |
| Global develop adjustments | 70% | 93% | 30–60 h |
| Optics, lens profiles, crop and geometry | 50% | 80% | 40–80 h |
@@ -96,7 +96,7 @@ while lower-layer errors stay English. Detail: [localization-parity.md](docs/loc
| Korean | ko | 0% | none (no Hangul face) |
| Vietnamese | vi | 0% | none |
-Also shipped: Traditional Chinese (87%), Russian (74%), Ukrainian (99%).
+Also shipped: Traditional Chinese (87%), Russian (74%), Ukrainian (99%), Italian (92%), Georgian (100%).
## Upcoming
@@ -114,8 +114,26 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
## Progress log
+- 2026-10-14: Camera-coverage sweep verifies 30 more models on 40 CC0 files, with file and sensor
+ hashes pinned (`docs/raw/coverage.py`: 515 → 545 verified). Eleven candidates excluded after rechecking
+ identity, provenance and camera-image agreement. Phone and drone DNG tone differences remain tracked in #1277.
+- 2026-10-14: 97 Canon CR2, Pentax and Ricoh models verified, one CC0 raw.pixls.us file each pinned and
+ checked against the camera JPEG (`crates/raw/tests/canon_pentax_corpus.rs`). 334 models verified, ~50% of
+ photographers on a verified body; headline numbers unchanged.
+- 2026-10-12: Mamiya MEF (ZD) decodes from sensor data and is imported; verified on a CC0 file. 241 models
+ verified, 35 unsupported; headline numbers unchanged.
+- 2026-10-12: Epson ERF (R-D1, R-D1s, R-D1x) decodes from sensor data, white balance measured from each
+ file's preview; 3 bodies verified on CC0 files. 240 models verified; headline numbers unchanged.
- 2026-10-11: Leica Q3 verified by a recorded check of 38 private DNGs (all decode and develop at full
size; mean ΔE to the camera JPEG 6.3–13.9). 236 models verified; headline numbers unchanged.
+- 2026-10-12: 22 more Canon CR3 bodies verified (EOS R, RP, R3, R5, R6, R6 Mark II / III, R7, R10, R50,
+ R50 V, 90D, 250D, 850D, M200, M50 Mark II, M6 Mark II, 1D X Mark III, PowerShot G5 X II, G7 X III, SX70,
+ V1): one CC0 file each pinned and checked against the camera JPEG; all 110 CC0 CR3s on raw.pixls.us
+ decode. The EOS R6 Mark III gets its as-shot white balance (`ColorData` 66). 259 models verified; RAW
+ decoding 77% → 83%; mainstream ~49% → ~51%, essentials ~64% → ~65%, full ~61%.
+- 2026-10-12: 117 Nikon and Fujifilm bodies verified, one CC0 raw.pixls.us file each pinned and checked
+ against the camera JPEG (`crates/raw/tests/body_corpus.rs`); the sweep found COOLPIX 5700 / 8400 / 8800
+ decoding wrongly. 354 models verified, ~58% of photographers on a verified body; headline numbers unchanged.
- 2026-10-11: Canon EOS R5 Mark II C-RAW decodes from sensor data (57% of its files opened as a
preview): the CRX QP map's first-symbol literal and limited-length escape; four CC0 R5 Mark II files
pinned, the QP maps of 33,665 private C-RAW files decode exactly ([cr3.md](docs/cr3.md)). 235 models
@@ -132,6 +150,8 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
/ localization parity); `docs/parity.md` renamed `docs/parity-checklist.md`. Landed today: Nikon
lossy-after-split NEF (#633), ORF 12.8 bpp (#651), HDR gain map JPEG / PQ AVIF / float TIFF export
(#646), Lightroom-like Auto (#628), activity stack.
+- 2026-10-11: Sony ILCA-99M2 (α99 II) camera colour: bundled profile from 144 photos; on 30 withheld photos
+ mean ΔE vs the camera JPEG 5.11 → 4.87, 25 of them closer.
- 2026-10-09: Sony ILCE-7CR camera colour: bundled profile from 125 photos; on 26 withheld photos mean
ΔE vs the camera JPEG 4.07 → 3.29. HEIC in release builds (#609), Samsung uncompressed SRW,
contact-sheet PDF export, Ukrainian and French UI, release v0.5.0.
@@ -163,8 +183,20 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
| Date | Change | Summary |
|---|---|---|
+| 2026-10-12 | minor | Italian interface (`it`, #1192): 2,201 of 2,398 messages, locale, menu command and tests; headline numbers unchanged |
+| 2026-10-13 | minor | Sony ILCA-99M2 camera colour profile in the log; numbers unchanged |
+| 2026-10-14 | minor | Localization ~52% → ~55% (mean of the 12 key languages): de, es, fr, ja and uk catalogs filled |
+| 2026-10-14 | minor | 21 Sony and Leica models verified (258 models, 18%); headline numbers unchanged |
+| 2026-10-12 | minor | UI/UX remaining 34–58 h → 32–54 h (canvas tools by keyboard); headline numbers unchanged |
+| 2026-10-14 | minor | 17 uncompressed Olympus and Samsung models verified on one CC0 file each (254 models verified, 18%); headline numbers unchanged |
+| 2026-10-12 | minor | Mamiya MEF decoded and imported: ZD verified (241); headline numbers unchanged |
+| 2026-10-12 | minor | Epson ERF decoded: 3 more verified models (240); headline numbers unchanged |
+| 2026-10-12 | minor | Georgian (`ka`) interface language added (every catalog message) |
| 2026-10-14 | minor | Full-resolution zoom (gap 4) closed: soft proofing gets zoom windows; headline numbers unchanged |
| 2026-10-12 | minor | UI/UX remaining 40–70 h → 34–58 h (accessibility audit landed); headline numbers unchanged |
+| 2026-10-14 | minor | Camera-coverage sweep (#1327): 30 models verified on 40 CC0 files (coverage.py 515 → 545 verified); sensor verification does not establish colour parity (#1277); headline numbers unchanged |
+| 2026-10-12 | minor | 22 more CR3 bodies verified, EOS R6 Mark III white balance: RAW decoding 77% → 83%, RAW ~56% → ~60%; mainstream 49% → 51%, essentials 64% → 65%; full unchanged at ~61% |
+| 2026-10-12 | minor | 117 Nikon and Fujifilm bodies verified (354 models, 24%); headline numbers unchanged |
| 2026-10-11 | minor | EOS R5 Mark II CR3 verified: CR3 on four verified bodies, RAW decoding 76% → 77%; headline numbers unchanged |
| 2026-10-10 | minor | RAW measured model by model; RAW decode and colour as explicit rows: full 59% → 61%, mainstream 51% → 49%, essentials 63% → 64%; beta ~14 points |
| 2026-10-10 | minor | Full ready restored to the additive weighted sum (~59%; method aligned with the standard, no new evidence); hours per audience; beta ~16 points away |
@@ -174,3 +206,5 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
| 2026-10-08 | minor | *Where we stand* by dimension and by kind of user |
| 2026-10-02 | major | Parity estimate and effort table |
| 2026-09-30 | major | Created |
+| 2026-10-14 | minor | 97 Canon CR2, Pentax and Ricoh models verified (334 models, 23%); headline numbers unchanged |
+| 2026-10-14 | minor | RAW: 82% of Adobe's models decode, 36% verified (integration batch 36's CC0 per-model checks) |
diff --git a/apps/lightcraft-cli/src/main.rs b/apps/lightcraft-cli/src/main.rs
index b6cad57e0..01dae3055 100644
--- a/apps/lightcraft-cli/src/main.rs
+++ b/apps/lightcraft-cli/src/main.rs
@@ -3,9 +3,9 @@
//! ```text
//! lightcraft-cli run [--demo | --library DIR | --connect [ADDR]] [--import PATH]… CMD [key=value…]…
//! lightcraft-cli mcp [--connect [ADDR]] [--demo] [--compact] [FILES/FOLDERS…]
-//! lightcraft-cli render -o [--set control=value]… [--settings FILE.json] [--preset ID] [--size N] [--quality Q]
+//! lightcraft-cli render -o [--set control=value]… [--settings FILE.json|FILE.lcpreset] [--preset ID] [--size N] [--quality Q]
//! lightcraft-cli snapshot [--library DIR | --demo] [--script FILE.jsonl] [-o OUT.png] [--size WxH] [--scale S] [FILES…]
-//! lightcraft-cli merge hdr|panorama|hdr-panorama [OPTIONS] FILES…
+//! lightcraft-cli merge hdr|panorama|hdr-panorama|hdr-batch [OPTIONS] FILES…
//! lightcraft-cli synth-merge hdr|panorama -o DIR
//! lightcraft-cli denoise-serve [--model ID] [--models DIR] [--listen ADDR] [--token-env VAR] [--run-on auto|gpu|cpu]
//! lightcraft-cli commands [--json]
@@ -17,7 +17,7 @@
mod alloc_release;
-use std::io::{BufReader, Write};
+use std::io::Write;
use std::path::Path;
use std::process::ExitCode;
@@ -55,7 +55,8 @@ USAGE:
Develop one file and export it (.jpg, .png, .tif, .webp, .avif, .jxl, .psd or .dng by extension) with the
same encoder as the app's Export dialog. Options:
--set CONTROL=VALUE set a develop slider, repeatable (e.g. --set light.exposure=0.5)
- --settings FILE merge a partial develop-settings JSON file
+ --settings FILE merge a partial develop-settings JSON file, or apply the one preset of a
+ .lcpreset (preset.export) or XMP preset
--preset ID apply a preset (see `commands`/presets.list)
--size N long edge in pixels (default: full size, cropped)
--quality Q JPEG/AVIF/JPEG XL quality 1..100 (default 92)
@@ -79,9 +80,10 @@ USAGE:
-o, --output OUT PNG path (a final screenshot is written here if the script took none)
--size WxH window size in points (default 1600x1000)
--scale S pixels per point (default 1)
- lightcraft-cli merge hdr|panorama|hdr-panorama [OPTIONS] FILES…
+ lightcraft-cli merge hdr|panorama|hdr-panorama|hdr-batch [OPTIONS] FILES…
Photo Merge: writes -HDR.dng / -Pano.dng / -HDR-Pano.dng next to the first file and
- prints the result as JSON. Options:
+ prints the result as JSON. hdr-batch merges each bracket set among the files (photos taken
+ less than a second apart at different exposures), e.g. a whole folder. Options:
--deghost none|low|medium|high --no-align --bracket N (HDR panorama)
--projection auto|spherical|cylindrical|perspective --boundary-warp 0..100
--auto-crop --fill-edges --no-auto-settings
@@ -364,8 +366,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
};
eprintln!("lightcraft-cli mcp: serving MCP on stdio ({})", backend.describe());
let mut server = Server::new(backend).with_command_tools(!compact);
- let stdin = std::io::stdin();
- server.serve(BufReader::new(stdin), std::io::stdout()).map_err(|e| e.to_string())
+ server.serve_stdio(std::io::stdout()).map_err(|e| e.to_string())
}
fn merge(args: &[String]) -> Result<(), String> {
@@ -373,7 +374,8 @@ fn merge(args: &[String]) -> Result<(), String> {
Some("hdr") => "merge.hdr",
Some("panorama" | "pano") => "merge.panorama",
Some("hdr-panorama") => "merge.hdrPanorama",
- _ => return Err("merge: expected hdr, panorama or hdr-panorama".into()),
+ Some("hdr-batch") => "merge.hdrBatch",
+ _ => return Err("merge: expected hdr, panorama, hdr-panorama or hdr-batch".into()),
};
let mut p = serde_json::Map::new();
let mut files = Vec::new();
@@ -635,11 +637,41 @@ fn connect_port(addr: &str) -> &str {
addr.rsplit_once(':').map_or(addr, |(_, port)| port)
}
+/// What `render --settings FILE` applies: a partial develop-settings object, or the one preset of
+/// a preset file (`.lcpreset` from `preset.export`, XMP), applied as the app applies it. Merging a
+/// `.lcpreset` as settings dropped its wrapper and applied nothing (#1730).
+enum SettingsFile {
+ Partial(Value),
+ Preset(lightcraft_engine::develop::Preset),
+}
+
+fn read_settings(p: &str) -> Result {
+ use lightcraft_engine::presets::{LCPRESET_EXT, parse_preset_file};
+ let bytes = std::fs::read(p).map_err(|e| format!("{p}: {e}"))?;
+ let ext = Path::new(p).extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default();
+ let json = serde_json::from_slice::(&bytes);
+ // a settings object never has a top-level `presets` or `settings` key; a preset file always does
+ let preset_file =
+ ext == LCPRESET_EXT || ext == "xmp" || json.as_ref().is_ok_and(|v| v.is_array() || v.get("presets").is_some() || v.get("settings").is_some());
+ if preset_file {
+ let mut list = parse_preset_file(p, &bytes).map_err(|e| format!("{p}: {e}"))?;
+ return match (list.pop(), list.len()) {
+ (Some(preset), 0) => Ok(SettingsFile::Preset(preset)),
+ (None, _) => Err(format!("{p}: no presets in this file")),
+ (Some(_), n) => Err(format!("{p}: {} presets in this file; --settings applies one, export it on its own", n + 1)),
+ };
+ }
+ match json.map_err(|e| format!("{p}: {e}"))? {
+ v @ Value::Object(_) => Ok(SettingsFile::Partial(v)),
+ _ => Err(format!("{p}: not a develop-settings object or a preset file")),
+ }
+}
+
fn render(args: &[String]) -> Result<(), String> {
let mut input = None;
let mut output = None;
let mut values = serde_json::Map::new();
- let mut settings: Option = None;
+ let mut settings = None;
let mut preset = None;
let mut size: Option = None;
let mut quality = 92u8;
@@ -654,11 +686,7 @@ fn render(args: &[String]) -> Result<(), String> {
let v: f64 = v.trim().parse().map_err(|_| format!("--set {k}: `{v}` is not a number"))?;
values.insert(k.trim().to_string(), json!(v));
}
- "--settings" => {
- let p = take_value(args, &mut i, "--settings")?;
- let text = std::fs::read_to_string(p).map_err(|e| format!("{p}: {e}"))?;
- settings = Some(serde_json::from_str(&text).map_err(|e| format!("{p}: {e}"))?);
- }
+ "--settings" => settings = Some(read_settings(take_value(args, &mut i, "--settings")?)?),
"--preset" => preset = Some(take_value(args, &mut i, "--preset")?.to_string()),
"--size" => size = Some(take_value(args, &mut i, "--size")?.parse().map_err(|_| "--size expects a number")?),
"--quality" => quality = take_value(args, &mut i, "--quality")?.parse().map_err(|_| "--quality expects 1..100")?,
@@ -684,8 +712,14 @@ fn render(args: &[String]) -> Result<(), String> {
if let Some(p) = preset {
run(&mut s, "preset.apply", json!({"id": p}))?;
}
- if let Some(st) = settings {
- run(&mut s, "develop.merge", json!({"settings": st}))?;
+ match settings {
+ Some(SettingsFile::Partial(st)) => run(&mut s, "develop.merge", json!({"settings": st}))?,
+ Some(SettingsFile::Preset(mut p)) => {
+ p.id = "cli.settings".into();
+ s.presets.push(p);
+ run(&mut s, "preset.apply", json!({"id": "cli.settings"}))?;
+ }
+ None => {}
}
if !values.is_empty() {
run(&mut s, "develop.set", json!({"values": values}))?;
@@ -763,12 +797,15 @@ fn snapshot(args: &[String]) -> Result<(), String> {
let t0 = Instant::now();
let mut session = match &library {
Some(dir) => {
- let mut s = Session::new().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models();
+ let mut s =
+ Session::new().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models().with_default_slide_templates();
s.open_library(dir, false).map_err(|e| library_error(dir, e))?;
s
}
- None if files.is_empty() => Session::with_demo().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models(),
- None => Session::new().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models(),
+ None if files.is_empty() => {
+ Session::with_demo().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models().with_default_slide_templates()
+ }
+ None => Session::new().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models().with_default_slide_templates(),
};
// Apply compute policy before imports or the first scripted query can discover an adapter.
session.execute("app.gpu", &json!({"enabled": false})).map_err(|e| e.to_string())?;
@@ -791,6 +828,8 @@ fn snapshot(args: &[String]) -> Result<(), String> {
// before the first frame can start background adapter discovery.
// This UI state is session-local: the CLI neither loads nor saves desktop ui.json.
app.ui.settings.gpu = false;
+ // as the desktop app opens it: an empty library on the grid, where Import is offered
+ lightcraft_ui_egui::panels::empty_library::open_on_empty_library(&mut app);
let mut h = Headless::new(app, size, scale);
let timeout = Duration::from_secs(60);
let mut shots = 0usize;
@@ -832,7 +871,17 @@ fn snapshot(args: &[String]) -> Result<(), String> {
match next_path(&mut shots) {
Some(p) => {
wrote_output = true;
- params["path"] = json!(p);
+ // `params` is untrusted script JSON: `params["path"] = …` panics on
+ // an array or a string. Only an object (or null, like an absent
+ // `params`) may take the path.
+ if params.is_null() {
+ params = json!({});
+ }
+ if let Some(o) = params.as_object_mut() {
+ o.insert("path".into(), json!(p));
+ } else {
+ return Err(format!("{script}:{}: ui.screenshot: `params` must be a JSON object", n + 1));
+ }
}
None => return Err(format!("{script}:{}: ui.screenshot needs a `path` (or give -o)", n + 1)),
}
diff --git a/apps/lightcraft-cli/tests/cli.rs b/apps/lightcraft-cli/tests/cli.rs
index bf56b02ab..8f56bafe6 100644
--- a/apps/lightcraft-cli/tests/cli.rs
+++ b/apps/lightcraft-cli/tests/cli.rs
@@ -47,6 +47,48 @@ fn contact_sheet_exports_pdf_and_protects_originals() {
assert_eq!(std::fs::read(&out).unwrap(), prior, "failed export changed prior output");
}
+#[test]
+fn print_commands_chain_into_a_picture_package_pdf() {
+ let input = tmp("print-input.png");
+ gradient_png(&input);
+ let out = tmp("print.pdf");
+ let (ok, lines, err) = run_cli(
+ &[
+ "--import",
+ input.to_str().unwrap(),
+ "print.set",
+ r#"layout={"style":"picturePackage","page":{"paper":"4x6","orientation":"landscape"}}"#,
+ r#"image={"zoomToFill":true,"borderEnabled":true,"borderWidth":0.05}"#,
+ "job={\"ppi\":72}",
+ "print.cell.add",
+ "width=1.5",
+ "height=1",
+ "print.cell.add",
+ "width=1.5",
+ "height=1",
+ "print.layout.autoLayout",
+ "print.exportPdf",
+ &format!("path={}", out.display()),
+ ],
+ None,
+ );
+ assert!(ok, "{err}: {lines:?}");
+ let export = lines.last().unwrap();
+ assert_eq!((export["result"]["pages"].as_u64(), export["result"]["photos"].as_u64()), (Some(1), Some(1)));
+ let bytes = std::fs::read(&out).unwrap();
+ assert!(bytes.starts_with(b"%PDF-"));
+ let text = String::from_utf8_lossy(&bytes);
+ let at = text.find("/MediaBox [").unwrap() + "/MediaBox [".len();
+ let nums: Vec = text[at..].split(']').next().unwrap().split_whitespace().map(|v| v.parse().unwrap()).collect();
+ assert_eq!((nums[2], nums[3]), (432.0, 288.0), "6 x 4 in");
+ // a bad setting fails the run and writes nothing new
+ let before = std::fs::read(&out).unwrap();
+ let (ok, _, _) =
+ run_cli(&["--import", input.to_str().unwrap(), "print.set", "job={\"ppi\":5}", "print.exportPdf", &format!("path={}", out.display())], None);
+ assert!(!ok);
+ assert_eq!(std::fs::read(&out).unwrap(), before);
+}
+
fn tmp(name: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("lightcraft-cli-test-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
@@ -139,6 +181,51 @@ fn render_subcommand() {
assert!(String::from_utf8_lossy(&o.stderr).contains("unknown control"));
}
+/// Issue #1730: `render --settings` took a `.lcpreset` from `preset.export`, ignored its wrapper
+/// and rendered the photo unedited.
+#[test]
+fn render_settings_applies_an_lcpreset_and_refuses_unknown_files() {
+ let input = tmp("lcpreset-in.png");
+ gradient_png(&input);
+ let preset = tmp("bright.lcpreset");
+ let (ok, lines, err) = run_cli(
+ &[
+ "--import",
+ input.to_str().unwrap(),
+ "develop.set",
+ "control=light.exposure",
+ "value=1",
+ "preset.create",
+ "name=Bright",
+ "preset.export",
+ &format!("path={}", preset.display()),
+ ],
+ None,
+ );
+ assert!(ok, "{err}: {lines:?}");
+ let plain = tmp("lcpreset-plain.png");
+ let st = Command::new(BIN).args(["render", input.to_str().unwrap(), "-o", plain.to_str().unwrap()]).status().unwrap();
+ assert!(st.success());
+ let bright = tmp("lcpreset-bright.png");
+ let o = Command::new(BIN)
+ .args(["render", input.to_str().unwrap(), "-o", bright.to_str().unwrap(), "--settings", preset.to_str().unwrap()])
+ .output()
+ .unwrap();
+ assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
+ assert!(mean(&bright) > mean(&plain) + 10.0, "the preset's exposure was not applied");
+
+ let other = tmp("other.lcpreset");
+ std::fs::write(&other, r#"{"format": "other", "version": 1, "presets": []}"#).unwrap();
+ let out = tmp("lcpreset-other.png");
+ let o = Command::new(BIN)
+ .args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap(), "--settings", other.to_str().unwrap()])
+ .output()
+ .unwrap();
+ assert!(!o.status.success(), "an unreadable settings file was accepted");
+ assert!(String::from_utf8_lossy(&o.stderr).contains("other.lcpreset"), "{}", String::from_utf8_lossy(&o.stderr));
+ assert!(!out.exists(), "nothing is written when the settings can't be read");
+}
+
/// Issue #93: `render IMG -o IMG` replaced its own input with the render.
#[test]
fn render_refuses_to_overwrite_its_input() {
@@ -273,6 +360,24 @@ fn snapshot_script_failure_exits_non_zero() {
assert_eq!((d.width, d.height), (320, 200));
}
+/// A script line whose `params` is not an object must be an error, never a panic: the
+/// `ui.screenshot`-without-a-path branch used to index (`params["path"] = …`) an array and abort
+/// the whole script.
+#[test]
+fn snapshot_script_with_non_object_params_is_an_error() {
+ let script = tmp("snap-bad-params.jsonl");
+ let out = tmp("snap-bad-params.png");
+ std::fs::write(&script, format!("{}\n", json!({"method": "ui.screenshot", "params": [1]}))).unwrap();
+ let o = Command::new(BIN)
+ .args(["snapshot", "--demo", "--script", script.to_str().unwrap(), "-o", out.to_str().unwrap(), "--size", "320x200"])
+ .output()
+ .unwrap();
+ let stderr = String::from_utf8_lossy(&o.stderr);
+ assert!(!o.status.success(), "{stderr}");
+ assert!(!stderr.contains("panicked"), "a bad `params` must be an error, not a panic: {stderr}");
+ assert!(stderr.contains("`params` must be a JSON object"), "{stderr}");
+}
+
#[test]
fn snapshot_ui_zoom_keeps_requested_pixel_dimensions() {
let script = tmp("snap-ui-zoom.jsonl");
diff --git a/apps/lightcraft-cli/tests/mcp_shutdown.rs b/apps/lightcraft-cli/tests/mcp_shutdown.rs
new file mode 100644
index 000000000..29d1ace93
--- /dev/null
+++ b/apps/lightcraft-cli/tests/mcp_shutdown.rs
@@ -0,0 +1,38 @@
+//! A broken protocol output must not require the client to close its input pipe.
+
+use std::io::{Read, Write};
+use std::process::{Command, Stdio};
+use std::time::{Duration, Instant};
+
+#[test]
+fn broken_stdout_exits_with_stdin_still_open() {
+ let mut child = Command::new(env!("CARGO_BIN_EXE_lightcraft-cli"))
+ .arg("mcp")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()
+ .unwrap();
+ let mut input = child.stdin.take().unwrap();
+ drop(child.stdout.take());
+ input.write_all(b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"}\n{\"jsonrpc\":").unwrap();
+ input.flush().unwrap();
+ let deadline = Instant::now() + Duration::from_secs(10);
+ let status = loop {
+ if let Some(status) = child.try_wait().unwrap() {
+ break status;
+ }
+ if Instant::now() >= deadline {
+ child.kill().unwrap();
+ child.wait().unwrap();
+ panic!("MCP did not exit after stdout failed while stdin stayed open");
+ }
+ std::thread::sleep(Duration::from_millis(10));
+ };
+ let mut stderr = String::new();
+ child.stderr.take().unwrap().read_to_string(&mut stderr).unwrap();
+ assert!(stderr.contains("serving MCP on stdio"), "CLI must reach the MCP transport: {stderr}");
+ assert!(!status.success(), "broken protocol output must be reported: {stderr}");
+ assert!(stderr.contains("lightcraft-cli:"), "CLI must report the transport error: {stderr}");
+ drop(input);
+}
diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs
index 18c896367..6a4487d09 100644
--- a/apps/lightcraft-web/src/web.rs
+++ b/apps/lightcraft-web/src/web.rs
@@ -219,6 +219,8 @@ fn services(originals: Originals, backend: Option, files: Files, frozen
save_keyword_list: None,
pick_curve_preset_files: None,
save_curve_preset_file: None,
+ pick_slideshow_templates: None,
+ save_slideshow_template: None,
write: Some(Box::new(download)),
// downloads happen on the main thread: exports run in the foreground on the web
write_shared: None,
diff --git a/apps/lightcraft-web/src/wire.rs b/apps/lightcraft-web/src/wire.rs
index b3d685f5f..08f9e898d 100644
--- a/apps/lightcraft-web/src/wire.rs
+++ b/apps/lightcraft-web/src/wire.rs
@@ -41,6 +41,9 @@ pub struct WireJob {
pub as_shot_temp: f64,
pub as_shot_tint: f64,
pub lens: Option,
+ /// The original's long edge (`SourceInfo::native_long`).
+ #[serde(default)]
+ pub native_long: Option,
pub settings: Arc,
pub max_w: usize,
pub max_h: usize,
@@ -80,6 +83,7 @@ impl WireJob {
as_shot_temp: job.info.as_shot_temp,
as_shot_tint: job.info.as_shot_tint,
lens: job.info.lens,
+ native_long: job.info.native_long,
settings: job.settings.clone(),
max_w: job.request.max_w,
max_h: job.request.max_h,
@@ -101,6 +105,7 @@ impl WireJob {
lens: self.lens,
relative_wb: self.relative_wb,
legacy_relative_wb: self.legacy_relative_wb,
+ native_long: self.native_long,
..Default::default()
}
}
@@ -469,6 +474,7 @@ mod tests {
as_shot_temp: 6500.0,
as_shot_tint: 0.0,
lens: None,
+ native_long: None,
settings: Arc::new(DevelopSettings::default()),
max_w: 32,
max_h: 32,
diff --git a/apps/lightcraft-web/src/workers.rs b/apps/lightcraft-web/src/workers.rs
index f1523c377..a5e64886e 100644
--- a/apps/lightcraft-web/src/workers.rs
+++ b/apps/lightcraft-web/src/workers.rs
@@ -161,6 +161,8 @@ struct Busy {
level: SourceLevel,
key: u64,
cache: Option<(Arc, Hash128)>,
+ /// A slideshow slide: the worker renders the photo, it is composed here when it comes back.
+ slide: Option>,
}
struct W {
@@ -370,6 +372,13 @@ impl Workers {
});
}
g.remote_done += 1;
+ let rendered = match (rendered, &b.slide) {
+ (Ok(mut r), Some(slide)) => {
+ r.image = slide.compose(&r.image);
+ Ok(r)
+ }
+ (r, _) => r,
+ };
g.done.push((
b.slot,
RenderResult {
@@ -493,6 +502,7 @@ impl RenderOffload for Workers {
level: job.level,
key: job.key,
cache: job.cache,
+ slide: job.slide,
});
None
}
diff --git a/apps/lightcraft/Cargo.toml b/apps/lightcraft/Cargo.toml
index bbe495c7d..26122c3e6 100644
--- a/apps/lightcraft/Cargo.toml
+++ b/apps/lightcraft/Cargo.toml
@@ -48,3 +48,7 @@ muda = { workspace = true }
objc2 = { workspace = true }
objc2-app-kit = { workspace = true }
objc2-foundation = { workspace = true }
+
+[dev-dependencies]
+# tests record why they skip (`cargo xtask ci` lists the skips)
+lightcraft-testkit = { workspace = true }
diff --git a/apps/lightcraft/src/appearance.rs b/apps/lightcraft/src/appearance.rs
index 74333a301..223dbc560 100644
--- a/apps/lightcraft/src/appearance.rs
+++ b/apps/lightcraft/src/appearance.rs
@@ -51,7 +51,7 @@ fn decode(code: u32) -> Option {
/// absolute path, and is never repeated.
#[cfg(target_os = "linux")]
fn gtk_theme() -> Option {
- let exe = ["/usr/bin/gsettings", "/bin/gsettings", "/usr/local/bin/gsettings"].into_iter().find(|p| std::path::Path::new(p).is_file())?;
+ let exe = gsettings_exe(std::env::var_os("PATH").as_deref())?;
let output = std::process::Command::new(exe).args(["get", "org.gnome.desktop.interface", "color-scheme"]).output().ok()?;
if !output.status.success() {
return None;
@@ -59,6 +59,15 @@ fn gtk_theme() -> Option {
parse_gtk_scheme(std::str::from_utf8(&output.stdout).ok()?)
}
+/// `gsettings` by absolute path: the absolute `PATH` entries first (NixOS has no /usr/bin/gsettings),
+/// then the usual places for a sparse `PATH`. Relative entries are skipped, so nothing runs from
+/// the working folder.
+#[cfg(target_os = "linux")]
+fn gsettings_exe(path: Option<&std::ffi::OsStr>) -> Option {
+ let listed = path.map(std::env::split_paths).into_iter().flatten().filter(|dir| dir.is_absolute());
+ listed.chain(["/usr/bin", "/bin", "/usr/local/bin"].map(std::path::PathBuf::from)).map(|dir| dir.join("gsettings")).find(|exe| exe.is_file())
+}
+
#[cfg(target_os = "linux")]
fn parse_gtk_scheme(s: &str) -> Option {
match s.trim().trim_matches('\'') {
@@ -174,6 +183,24 @@ mod tests {
assert_eq!(calls.load(Ordering::Relaxed), after_settle);
}
+ /// NixOS keeps `gsettings` in /run/current-system/sw/bin or the per-user profile: it is found
+ /// on `PATH`, but never through a relative entry, even one that leads to a real file.
+ #[cfg(target_os = "linux")]
+ #[test]
+ fn gsettings_is_found_on_absolute_path_entries_only() {
+ let dir = std::env::temp_dir().join(format!("lightcraft-gsettings-{}", std::process::id()));
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(dir.join("gsettings"), "").unwrap();
+ let path = std::env::join_paths([std::path::Path::new("relative/bin"), &dir]).unwrap();
+ assert_eq!(super::gsettings_exe(Some(&path)), Some(dir.join("gsettings")));
+ let cwd = std::env::current_dir().unwrap();
+ let relative = std::path::PathBuf::from("../".repeat(cwd.components().count() - 1)).join(dir.strip_prefix("/").unwrap());
+ assert!(relative.join("gsettings").is_file());
+ let found = super::gsettings_exe(Some(relative.as_os_str()));
+ assert!(found.as_ref().is_none_or(|p| p.is_absolute()), "{found:?}");
+ let _ = std::fs::remove_dir_all(&dir);
+ }
+
#[cfg(target_os = "linux")]
#[test]
fn portal_values_and_gsettings_output_decode() {
diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs
index 1c70e19ea..95739b3e9 100644
--- a/apps/lightcraft/src/main.rs
+++ b/apps/lightcraft/src/main.rs
@@ -76,10 +76,13 @@ impl eframe::App for App {
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
self.0.raw_input_hook(raw);
}
- fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
+ fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
+ self.0.perf.host_frame_ms = frame.info().cpu_usage.map(|s| f64::from(s) * 1000.0);
self.0.ui(ui);
}
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
+ // (a size the window was just given is remembered too)
+ self.0.ui.window = self.1.window_at_exit(self.0.ui.window);
if let Err(e) = self.1.save(&self.0) {
log::error!("{e}");
}
@@ -162,6 +165,46 @@ fn gpu_crash_notice(what: &str) -> String {
)
}
+/// Written when the window stopped drawing after the GPU device was lost (issue #1337): the next
+/// launch renders on the CPU and says why.
+fn gpu_lost_marker_path() -> Option {
+ gpu_marker_path().map(|p| p.with_file_name("gpu-lost.marker"))
+}
+
+/// Why the GPU was lost, when the GPU pipeline stopped after a device failure in this process (a
+/// driver reset, a hang, the Windows watchdog): `None` for other reasons it is off, or when it
+/// never failed.
+fn gpu_lost_reason() -> Option {
+ lightcraft_engine::gpu::unavailable_reason().and_then(|r| r.strip_prefix("stopped after a GPU failure: ").map(str::to_string))
+}
+
+/// Record that LightCraft stopped after the GPU was lost, for the next launch.
+fn record_gpu_lost(marker: Option<&std::path::Path>, reason: &str) {
+ let Some(m) = marker else { return };
+ if let Some(d) = m.parent() {
+ let _ = std::fs::create_dir_all(d);
+ }
+ if let Err(e) = std::fs::write(m, format!("{reason}\n")) {
+ log::warn!("could not write {}: {e}", m.display());
+ }
+}
+
+/// A GPU loss the last launch stopped on: its notice, once (the marker is removed, except by a
+/// `--memory` session, which leaves the disk as it found it).
+fn gpu_lost_check_at(marker: Option<&std::path::Path>, in_memory: bool) -> Option {
+ let m = marker?;
+ let reason = std::fs::read_to_string(m).ok()?;
+ if !in_memory {
+ let _ = std::fs::remove_file(m);
+ }
+ Some(format!(
+ "LightCraft stopped last time after the graphics driver reset the GPU ({}) while photos rendered on it, so GPU rendering \
+ is now off and photos render on the CPU. To try the GPU again, turn on Settings ▸ Performance ▸ Use the GPU for rendering; \
+ an up-to-date graphics driver may help.",
+ reason.trim()
+ ))
+}
+
/// Where the system keeps monitor profiles, for the Choose Profile dialog to start in: the user's
/// own profiles first (Linux: colord / DisplayCAL; macOS: ColorSync), then the system's.
fn display_profile_dir() -> Option {
@@ -183,6 +226,16 @@ fn config_dir() -> Option {
lightcraft_engine::config::config_dir()
}
+/// The window at the size it was last used at, zoomed if it was (`window`), or at the usual size
+/// when nothing is remembered. A zoomed window is given the size it goes back to. Where it
+/// opens is the system's to say.
+fn as_remembered(builder: egui::ViewportBuilder, window: Option) -> egui::ViewportBuilder {
+ let builder = builder.with_inner_size(window.map_or(lightcraft_ui_egui::window_place::DEFAULT_SIZE, |w| w.size));
+ // (only ever asked to zoom: asked not to, macOS un-zooms a window whose size fills the screen
+ // to a size of its own choosing)
+ if window.is_some_and(|w| w.zoomed) { builder.with_maximized(true) } else { builder }
+}
+
/// `/ui.json`, the saved UI state and app settings — `None` when nothing is read or
/// written: `LIGHTCRAFT_NO_PREFS` (tests, scripts) or no home folder.
fn prefs_path() -> Option {
@@ -301,6 +354,11 @@ struct PrefsWriter {
failing: bool,
/// `ui.json` couldn't be read at launch: never overwrite it this session.
keep_file: bool,
+ /// Follows the window, to remember the size it rests at (`UiState::window`).
+ window: lightcraft_ui_egui::window_place::WindowWatch,
+ /// When the window was first looked at: its watch goes by the clock from then on, not by
+ /// the interface's time, which stands still while the window is hidden.
+ window_since: Option,
}
impl PrefsWriter {
@@ -331,8 +389,33 @@ impl PrefsWriter {
Ok(())
}
+ /// What to remember of the window as the app quits ([`WindowWatch::at_exit`]).
+ ///
+ /// [`WindowWatch::at_exit`]: lightcraft_ui_egui::window_place::WindowWatch::at_exit
+ fn window_at_exit(
+ &mut self,
+ remembered: Option,
+ ) -> Option {
+ match self.window_since {
+ Some(since) => self.window.at_exit(remembered, since.elapsed().as_secs_f64()),
+ None => remembered,
+ }
+ }
+
fn tick(&mut self, app: &mut LightcraftApp, ctx: &egui::Context) {
- let now = ctx.input(|i| i.time);
+ // (asked for before the input is read: the context is not asked from inside its own lock)
+ let zoom = ctx.zoom_factor();
+ let (now, seen) = ctx.input(|i| (i.time, lightcraft_ui_egui::window_place::Seen::of(i.viewport(), i.content_rect(), zoom)));
+ // how the window is, to open like that next time (written with the settings below). What
+ // the settings hold is checked first: a script may have set it (`ui.set`)
+ let remembered = app.ui.window.and_then(lightcraft_ui_egui::window_place::WindowPlace::checked);
+ let at = self.window_since.get_or_insert_with(std::time::Instant::now).elapsed().as_secs_f64();
+ app.ui.window = self.window.note(remembered, &seen, at);
+ if let Some(wait) = self.window.waits(at) {
+ // (a size is only remembered once the window has rested at it: look again then, though
+ // nothing else asks for a frame)
+ ctx.request_repaint_after(wait);
+ }
let moved = app.ui.settings.library_path != self.library;
if !moved && now - self.checked < 3.0 {
return;
@@ -603,6 +686,24 @@ fn services(cc: &eframe::CreationContext<'_>, log_file: Option<&std::path::Path>
.save_file()
.map(|p| p.to_string_lossy().to_string())
})),
+ pick_slideshow_templates: Some(Box::new(|| {
+ rfd::FileDialog::new()
+ .set_title(lightcraft_ui_egui::i18n::tr("Import Slideshow Templates"))
+ .add_filter_nocase(lightcraft_ui_egui::i18n::tr("Slideshow Templates"), &["json"])
+ .pick_files()
+ .unwrap_or_default()
+ .into_iter()
+ .map(|p| p.to_string_lossy().to_string())
+ .collect()
+ })),
+ save_slideshow_template: Some(Box::new(|name: &str| {
+ rfd::FileDialog::new()
+ .set_title(lightcraft_ui_egui::i18n::tr("Export Slideshow Template"))
+ .add_filter_nocase(lightcraft_ui_egui::i18n::tr("Slideshow Templates"), &["json"])
+ .set_file_name(name)
+ .save_file()
+ .map(|p| p.to_string_lossy().to_string())
+ })),
// atomic (temp file + sync + rename): a failed write never leaves a truncated file
write_shared: Some(std::sync::Arc::new(lightcraft_engine::export::write_file)),
write: Some(Box::new(lightcraft_engine::export::write_file)),
@@ -616,10 +717,12 @@ fn services(cc: &eframe::CreationContext<'_>, log_file: Option<&std::path::Path>
}
}
-/// The library session with the folder face models are kept in (`/models`).
+/// The library session with the folders face models (`/models`) and slideshow templates
+/// (`/slideshow-templates`) are kept in.
fn open_session(in_memory: bool, dir: Option, seed_demo: bool) -> (Session, Option) {
let (mut s, problem) = open_library_session(in_memory, dir, seed_demo);
s.face_models_dir = lightcraft_engine::config::default_face_models_dir();
+ s.slide_templates_dir = lightcraft_engine::config::config_dir().map(|d| d.join(lightcraft_engine::slide_template::DIR_NAME));
// Control requests are answered on the UI thread: Add to Apple Photos must not wait there
s.apple_photos_background = true;
(s, problem)
@@ -808,7 +911,7 @@ fn main() -> eframe::Result {
let library_dir = library_dir.or_else(lightcraft_engine::library::default_dir);
// GPU compute: off if the preference says so, or if the last launch died creating the device
// (issue #136) — before anything can create it
- let gpu_crash = gpu_crash_check(in_memory);
+ let gpu_crash = gpu_crash_check(in_memory).or_else(|| gpu_lost_check_at(gpu_lost_marker_path().as_deref(), in_memory));
let gpu_on = prefs.as_ref().is_none_or(|u| u.settings.gpu) && gpu_crash.is_none();
lightcraft_engine::gpu::set_enabled(gpu_on);
// the window's renderer: probed (Windows) and with a fallback chain, so a driver that can't
@@ -817,12 +920,15 @@ fn main() -> eframe::Result {
let window_gpu::Chosen { renderer, wgpu_options, which: window_renderer, notice: window_notice } = window_gpu::choose(&win_files);
win_files.arm(window_renderer);
let win_files_app = win_files.clone();
+ // the window came up: a failure after this is LightCraft stopping, not a start that failed
+ let came_up = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
+ let came_up_app = came_up.clone();
+ // how the window was when the app was last used (checked: the settings file is a file)
+ let opens_as = prefs.as_ref().and_then(|ui| ui.window).and_then(lightcraft_ui_egui::window_place::WindowPlace::checked);
let options = eframe::NativeOptions {
renderer,
- viewport: egui::ViewportBuilder::default()
- .with_title("LightCraft")
- .with_inner_size([1600.0, 1000.0])
- .with_min_inner_size([900.0, 560.0])
+ viewport: as_remembered(egui::ViewportBuilder::default().with_title("LightCraft"), opens_as)
+ .with_min_inner_size(lightcraft_ui_egui::window_place::MIN_SIZE)
.with_drag_and_drop(true)
.with_fullsize_content_view(true)
.with_titlebar_shown(false)
@@ -854,6 +960,7 @@ fn main() -> eframe::Result {
Box::new(move |cc| {
// the window's renderer is up (eframe calls this after creating it)
win_files_app.started(window_renderer);
+ came_up_app.store(true, std::sync::atomic::Ordering::Relaxed);
let (mut session, problem) = open_session(in_memory, library_dir, seed_demo && files.is_empty());
// AI masks: the SAM 3 checkpoint (facebook/sam3) in /models/sam3, or LIGHTCRAFT_SAM3_DIR
// (never required: without it, AI masks offer to download it; see docs/ai-masks.md)
@@ -871,6 +978,11 @@ fn main() -> eframe::Result {
app.notices.extend(launch_notices);
app.notices.extend(window_notice);
app.gpu_notice = true;
+ // an empty library opens where Import is offered (a grid, with the sidebar); before
+ // the snapshot below, so this alone isn't written to the settings file
+ if problem.is_none() && files.is_empty() {
+ lightcraft_ui_egui::panels::empty_library::open_on_empty_library(&mut app);
+ }
// what's on disk now: only changes are written
let writer = PrefsWriter {
path: if in_memory { None } else { prefs_path() },
@@ -915,6 +1027,31 @@ fn main() -> eframe::Result {
}),
)
}));
+ if came_up.load(std::sync::atomic::Ordering::Relaxed) {
+ // the window was up: a panic that escaped it now is LightCraft stopping, typically the
+ // window's renderer after the graphics driver reset the GPU (issue #1337: egui-wgpu panics
+ // on the lost device). Not a failed start: no other renderer is tried, and the message
+ // says what happened.
+ let stopped = match started {
+ Ok(r) => r,
+ Err(_) => Err(eframe::Error::AppCreation("its window stopped drawing (the renderer panicked)".into())),
+ };
+ if let Err(e) = &stopped {
+ let lost = gpu_lost_reason();
+ if let Some(reason) = &lost
+ && !in_memory
+ {
+ record_gpu_lost(gpu_lost_marker_path().as_deref(), reason);
+ }
+ // (not "app creation error: …": the app had been created)
+ let what = match e {
+ eframe::Error::AppCreation(m) => m.to_string(),
+ e => e.to_string(),
+ };
+ stopped_while_running(&stopped_text(&what, lost.as_deref()), log_file.as_deref());
+ }
+ return stopped;
+ }
// a panic that still escaped the renderer's start (the probe and error handler should
// prevent it): an error like any other failed start
let started = started.unwrap_or_else(|_| Err(eframe::Error::AppCreation("the window's renderer panicked while starting".into())));
@@ -929,6 +1066,34 @@ fn main() -> eframe::Result {
started
}
+/// What to tell the user when LightCraft stopped while running (`error`), and why when the GPU
+/// was lost (`lost`): GPU rendering is off from the next start.
+fn stopped_text(error: &str, lost: Option<&str>) -> String {
+ match lost {
+ Some(reason) => format!(
+ "{error}. The graphics driver reset the GPU ({reason}). LightCraft renders on the CPU from its next start; \
+ Settings ▸ Performance ▸ Use the GPU for rendering turns the GPU back on"
+ ),
+ None => error.to_string(),
+ }
+}
+
+/// LightCraft stopped while its window was up: say so in the log and in a native message box
+/// naming the log file (as [`startup_failed`] does for a start that failed).
+fn stopped_while_running(text: &str, log_file: Option<&std::path::Path>) {
+ log::error!("LightCraft stopped: {text}");
+ if std::env::var_os("LIGHTCRAFT_NO_PREFS").is_some() {
+ return; // tests and scripts: no dialog to click away
+ }
+ let (title, text) = lightcraft_ui_egui::i18n::stopped_message(text, log_file.map(|p| p.display().to_string()).as_deref());
+ let _ = rfd::MessageDialog::new()
+ .set_level(rfd::MessageLevel::Error)
+ .set_title(title)
+ .set_description(text)
+ .set_buttons(rfd::MessageButtons::Ok)
+ .show();
+}
+
/// The window could not start (e.g. no usable graphics device): say so in the log and in a native
/// message box naming the log file, instead of exiting without a trace — the release builds have
/// no console on Windows, and an app started from Finder shows no standard error (issue #260).
@@ -1131,6 +1296,37 @@ mod tests {
let _ = std::fs::remove_dir_all(&d);
}
+ /// Issue #1337: the window stopped drawing after the graphics driver reset the GPU during an
+ /// export, and the dialog said the window "could not open". The stop is recorded; the next
+ /// launch says why once and renders on the CPU (a `--memory` session leaves the record).
+ #[test]
+ fn a_gpu_lost_while_running_turns_the_gpu_off_next_time() {
+ let d = dir("gpu-lost");
+ let m = d.join("gpu-lost.marker");
+ assert_eq!(gpu_lost_check_at(Some(&m), false), None, "nothing recorded");
+ assert_eq!(gpu_lost_check_at(None, false), None, "no settings folder");
+ record_gpu_lost(Some(&m), "device lost (Unknown): Device is lost");
+ let notice = gpu_lost_check_at(Some(&m), true).unwrap();
+ assert!(
+ notice.contains("(device lost (Unknown): Device is lost)")
+ && notice.contains("GPU rendering is now off")
+ && notice.contains("Use the GPU for rendering"),
+ "{notice}"
+ );
+ assert!(m.exists(), "a --memory session leaves it");
+ assert!(gpu_lost_check_at(Some(&m), false).is_some());
+ assert!(!m.exists());
+ assert_eq!(gpu_lost_check_at(Some(&m), false), None, "said once");
+ // the message box: LightCraft stopped (not "could not open its window"), and why
+ let text = stopped_text("its window stopped drawing (the renderer panicked)", Some("device lost (Unknown): Device is lost"));
+ assert!(text.contains("reset the GPU (device lost (Unknown)") && text.contains("renders on the CPU from its next start"), "{text}");
+ assert_eq!(stopped_text("its window stopped drawing", None), "its window stopped drawing");
+ let (title, body) = lightcraft_ui_egui::i18n::stopped_message(&text, Some("/logs/lightcraft.log"));
+ assert_eq!(title, "LightCraft stopped");
+ assert!(body.starts_with("LightCraft stopped unexpectedly: its window stopped drawing") && body.ends_with("/logs/lightcraft.log"), "{body}");
+ let _ = std::fs::remove_dir_all(&d);
+ }
+
/// Issue #136: a GPU init marker left by a crashed launch is reported once and removed.
#[test]
fn gpu_crash_marker_is_reported_once() {
@@ -1237,6 +1433,96 @@ mod tests {
let _ = std::fs::remove_dir_all(&d);
}
+ /// Given what the settings remember of the window, when the app's window is built, then it
+ /// opens at that size, zoomed if it was, where the system puts it; with nothing remembered it
+ /// opens at the usual size.
+ #[test]
+ fn the_window_opens_as_it_was() {
+ use lightcraft_ui_egui::window_place::{DEFAULT_SIZE, WindowPlace};
+ let fresh = as_remembered(egui::ViewportBuilder::default(), None);
+ assert_eq!((fresh.inner_size, fresh.position, fresh.maximized), (Some(egui::Vec2::from(DEFAULT_SIZE)), None, None));
+ let b = as_remembered(egui::ViewportBuilder::default(), Some(WindowPlace { size: [1300.0, 800.0], zoomed: true }));
+ assert_eq!((b.inner_size, b.position, b.maximized), (Some(egui::vec2(1300.0, 800.0)), None, Some(true)));
+ // (not asked to un-zoom: macOS would then resize a window that fills its screen)
+ let b = as_remembered(egui::ViewportBuilder::default(), Some(WindowPlace { size: [1300.0, 800.0], zoomed: false }));
+ assert_eq!((b.inner_size, b.position, b.maximized), (Some(egui::vec2(1300.0, 800.0)), None, None));
+ }
+
+ /// Given a window that rests at a size, when the settings are written and read again, then
+ /// they hold how it was. A window in the file that is not one (a hand edit, another version's)
+ /// reads as nothing remembered and costs no other setting; and a window resized just before
+ /// the app quits is remembered at that size.
+ #[test]
+ fn the_settings_keep_how_the_window_was() {
+ use lightcraft_ui_egui::window_place::{Seen, WindowPlace};
+ let d = dir("window");
+ let path = d.join("ui.json");
+ let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services::default());
+ app.ui.settings.library_path = "/pictures/library".into();
+ let mut writer = PrefsWriter { path: Some(path.clone()), ..Default::default() };
+ writer.save(&app).unwrap();
+ assert!(!std::fs::read_to_string(&path).unwrap().contains("\"window\""), "nothing to remember yet");
+ let window = WindowPlace { size: [1300.0, 800.0], zoomed: true };
+ app.ui.window = Some(window);
+ writer.save(&app).unwrap();
+ let (restored, warning, _) = load_prefs_at(&path, false);
+ assert_eq!((restored.unwrap().window, warning), (Some(window), None));
+
+ // a window that is no window: the file is read all the same, with its other settings
+ let text = std::fs::read_to_string(&path).unwrap();
+ for odd in [r#""window": {}"#, r#""window": "wide""#, r#""window": {"size": [1, 2, 3]}"#, r#""window": {"size": [null, 800.0]}"#] {
+ let start = text.find("\"window\"").unwrap();
+ let end = start + text[start..].find('}').unwrap() + 1;
+ std::fs::write(&path, format!("{}{odd}{}", &text[..start], &text[end..])).unwrap();
+ let (restored, warning, keep) = load_prefs_at(&path, true);
+ let restored = restored.unwrap_or_else(|| panic!("{odd}: {warning:?}"));
+ assert_eq!((restored.window, restored.settings.library_path.as_str(), warning, keep), (None, "/pictures/library", None, false), "{odd}");
+ }
+
+ // resized, and the app quits a moment later: the new size is what is written
+ let mut writer = PrefsWriter { path: Some(path.clone()), ..Default::default() };
+ let rest = Some(WindowPlace { size: [1300.0, 800.0], zoomed: false });
+ writer.window.note(rest, &Seen { size: Some([1500.0, 900.0]), ..Default::default() }, 0.0);
+ writer.window_since = std::time::Instant::now().checked_sub(std::time::Duration::from_millis(400));
+ assert!(writer.window_since.is_some());
+ assert_eq!(writer.window_at_exit(rest), Some(WindowPlace { size: [1500.0, 900.0], zoomed: false }));
+ assert_eq!(PrefsWriter::default().window_at_exit(rest), rest, "a window that was never seen");
+ let _ = std::fs::remove_dir_all(&d);
+ }
+
+ /// Given the app's window at a size, when the settings writer looks at it frame after frame,
+ /// then it asks to be woken while the size has still to rest, remembers the size once it has,
+ /// and asks for nothing more. A window in the settings that is not one (a script set it) is
+ /// put right before it is gone by.
+ #[test]
+ fn the_writer_follows_the_window_from_frame_to_frame() {
+ use lightcraft_ui_egui::window_place::WindowPlace;
+ let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services::default());
+ let mut writer = PrefsWriter::default();
+ let ctx = egui::Context::default();
+ let pass = |look: &mut dyn FnMut(&egui::Context)| {
+ let raw =
+ egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1300.0, 800.0))), ..Default::default() };
+ let mut out = ctx.run_ui(raw, |ui| look(ui.ctx()));
+ out.textures_delta.clear();
+ out.viewport_output.get(&egui::ViewportId::ROOT).map(|v| v.repaint_delay)
+ };
+ // (egui's own first frames ask for another: let them pass)
+ for _ in 0..3 {
+ pass(&mut |_| {});
+ }
+ app.ui.window = Some(WindowPlace { size: [f32::NAN, 800.0], zoomed: true });
+ let asked = pass(&mut |ctx| writer.tick(&mut app, ctx));
+ assert_eq!(app.ui.window, None, "no window, and not yet at rest");
+ assert!(asked.is_some_and(|wait| wait <= std::time::Duration::from_secs(1)), "{asked:?}");
+ // a second later
+ writer.window_since = writer.window_since.and_then(|since| since.checked_sub(std::time::Duration::from_secs(1)));
+ pass(&mut |ctx| writer.tick(&mut app, ctx));
+ assert_eq!(app.ui.window, Some(WindowPlace { size: [1300.0, 800.0], zoomed: false }));
+ let asked = pass(&mut |ctx| writer.tick(&mut app, ctx));
+ assert!(asked.is_none_or(|wait| wait > std::time::Duration::from_secs(3600)), "at rest and remembered: {asked:?}");
+ }
+
/// #1331: on macOS the process ends inside `on_exit`, before eframe would tear the window down.
/// The check runs in a child (this test binary again), since on macOS it ends the process.
#[test]
diff --git a/apps/lightcraft/src/native_menu.rs b/apps/lightcraft/src/native_menu.rs
index 256faa1f8..4ac40bcac 100644
--- a/apps/lightcraft/src/native_menu.rs
+++ b/apps/lightcraft/src/native_menu.rs
@@ -26,8 +26,9 @@ const QUIT: &str = "app.quit";
const SETTINGS: &str = "app.settings";
const SETTINGS_KEY: &str = "Cmd+,";
/// Keys whose meaning depends on the panel (X = reject or swap crop aspect, ⌫ = delete the photo
-/// or the active mask): left to the egui handler, which knows the context.
-const CONTEXTUAL: &[&str] = &["X", "Delete"];
+/// or the active mask; ⌘↑/⌘↓ flag status follows Compare/Survey's active photo): left to the egui
+/// handler, which knows the context.
+const CONTEXTUAL: &[&str] = &["X", "Delete", "Cmd+Up", "Cmd+Down"];
/// Shortcuts text fields need while they have focus.
const TEXT_EDIT: &[&str] = &["Cmd+A", "Cmd+C", "Cmd+V", "Cmd+X", "Cmd+Z", "Cmd+Shift+Z"];
@@ -126,6 +127,7 @@ fn accelerator(sc: &str) -> Option {
"=" => Code::Equal,
"-" => Code::Minus,
"'" => Code::Quote,
+ "`" => Code::Backquote,
"," => Code::Comma,
"Delete" => Code::Backspace,
// with modifiers only: a bare Enter belongs to text fields and tools
diff --git a/apps/lightcraft/src/window_gpu.rs b/apps/lightcraft/src/window_gpu.rs
index 66e5a78d7..1f84ec477 100644
--- a/apps/lightcraft/src/window_gpu.rs
+++ b/apps/lightcraft/src/window_gpu.rs
@@ -278,11 +278,11 @@ mod tests {
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let Ok(adapter) = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())) else {
- eprintln!("no GPU adapter: skipped");
+ lightcraft_testkit::skip!(Gpu, "no GPU adapter");
return;
};
let Ok((device, _queue)) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())) else {
- eprintln!("no GPU device: skipped");
+ lightcraft_testkit::skip!(Gpu, "no GPU device");
return;
};
install_error_handler(&device);
diff --git a/assets/ATTRIBUTION.md b/assets/ATTRIBUTION.md
index 7ade9dac2..7f8a8a719 100644
--- a/assets/ATTRIBUTION.md
+++ b/assets/ATTRIBUTION.md
@@ -12,20 +12,28 @@ covers a whole directory), or if a licence file referenced in the Licence column
| `assets/app-icon/` (`lightcraft.svg` and the PNG/ICNS/ICO/SVG files derived from it) | LightCraft app icon (lynx) | LightCraft project owner (drawn in ArtCraft) | original work (vectorised from the owner's drawing; see `assets/app-icon/README.md`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | 2026-10-02 | vectorised to SVG; rendered to PNG/ICNS/ICO by `packaging/icons.sh` |
| `assets/camera-profiles/*.json` | Camera colour profiles (matrix + hue/saturation/value table), built into LightCraft | ILCE-7M4: Harald Hoyer | original work: fitted by `lightcraft-cli calibrate` to the contributor's own raw photos and their in-camera JPEGs (597 ILCE-7M4 photos, 2026); aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-07 | none |
| `packaging/macos/dmg/` (`background.svg`, the `background.tiff` rendered from it by `generate.py`, and the `dmg-layout.DS_Store` layout file) | LightCraft macOS DMG window background | @XusBadia | original work for this repository, derived from the LightCraft app icon (`assets/app-icon/lightcraft.svg`, referenced via ``, not copied); text is outlined Inter / JetBrains Mono glyph paths | MIT OR Apache-2.0 | 2026-10-08 | none (original); used for the Finder window of the macOS DMG, `packaging/macos/dmg/generate.py` renders it to `background.tiff` |
+| `assets/camera-profiles/ILCA-99M2.json` | Sony ILCA-99M2 camera colour profile (matrix + hue/saturation/value table), built into LightCraft | LightCraft contributors; source photographs supplied by @PiMinusMax | original work: fitted by `lightcraft-cli calibrate` to 144 usable ILCA-99M2 ARWs (of 150, seven shooting days, Standard creative style) 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-11 | none |
| `assets/camera-profiles/ILCE-7CR.json` | Sony ILCE-7CR camera colour profile (matrix + hue/saturation/value table), built into LightCraft | LightCraft contributors; source photographs supplied by @AlexChambers | original work: fitted by `lightcraft-cli calibrate` to 125 usable ILCE-7CR ARWs (of 131, September 2025 to September 2026, Standard creative style) 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-09 | none |
| `assets/camera-profiles/X-H2S.json`, `assets/camera-profiles/X-T4.json` | Fujifilm camera colour profiles (matrix + hue/saturation/value table), built into LightCraft | LightCraft contributors; source photographs supplied by Sebastian Pfluegelmeier | original work: fitted by `lightcraft-cli calibrate` to 201 X-H2S and 545 X-T4 RAFs 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-08 | none |
+| `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 |
| `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 |
| `crates/ui-egui/src/icons.rs` | UI icons drawn as vector paths in code | LightCraft contributors | original work | MIT OR Apache-2.0 | 2026-09-30 | n/a |
+| `crates/ui-egui/src/panels/person.rs` | Up arrow on the People page bulk-confirm control, drawn in code | LightCraft contributors | original work | MIT OR Apache-2.0 | 2026-10-13 | n/a |
| `docs/images/*-tetons.jpg`, `docs/images/grid-pd.jpg` (screenshots containing it) | "The Tetons and the Snake River" | Ansel Adams (1942), U.S. National Archives | https://commons.wikimedia.org/wiki/File:Adams_The_Tetons_and_the_Snake_River.jpg | Public domain (U.S. federal government work) | 2026-09-30 | developed in LightCraft; shown inside app screenshots |
| `docs/images/*-migrant-mother.jpg`, `docs/images/grid-pd.jpg` | "Migrant Mother" | Dorothea Lange (1936), Farm Security Administration / Library of Congress | https://commons.wikimedia.org/wiki/File:Lange-MigrantMother02.jpg | Public domain (U.S. federal government work) | 2026-09-30 | developed in LightCraft; shown inside app screenshots |
| `docs/images/*-earthrise.jpg`, `docs/images/grid-pd.jpg` | "Earthrise" (AS08-14-2383) | NASA / Bill Anders, Apollo 8 (1968) | https://commons.wikimedia.org/wiki/File:NASA-Apollo8-Dec24-Earthrise.jpg | Public domain (NASA) | 2026-09-30 | developed in LightCraft; shown inside app screenshots |
| `docs/images/*-blue-marble.jpg`, `docs/images/grid-pd.jpg` | "The Blue Marble" (AS17-148-22727) | NASA, Apollo 17 crew (1972) | https://commons.wikimedia.org/wiki/File:The_Earth_seen_from_Apollo_17.jpg | Public domain (NASA) | 2026-09-30 | developed in LightCraft; shown inside app screenshots |
| `docs/images/*.jpg` (UI) | LightCraft application screenshots | LightCraft contributors | original work (captured with `docs/showcase/`) | MIT OR Apache-2.0 | 2026-09-30 | n/a |
| `docs/brand/` (artcraft-logo and artcraft-mark, SVG and PNG) | ArtCraft wordmark and mark | ArtCraft Team | original work (https://getartcraft.com/) | ArtCraft trademark, see `docs/brand/LICENSE-brand.txt` (not open source) | 2026-10-01 | none |
-| craft-fonts: optional build input, not files in this repository | BIZ UDPGothic (Regular, Bold), BIZ UDMincho Regular, Shippori Mincho Regular, Noto Sans CJK SC Regular | Morisawa Inc. / The BIZ UDGothic and BIZ UDMincho Project Authors; The Shippori Mincho Project Authors; Google and Adobe (Noto Sans CJK, maintainer-approved exception; see AGENTS.md) (per craft-fonts' attribution) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | SIL Open Font License 1.1 (each font's OFL.txt in craft-fonts; release packages ship it as OFL-.txt) | 2026-10-06 | none; native builds made with `CRAFT_FONTS_DIR` embed the manifest fonts (all official releases); the web build embeds BIZ UDPGothic Regular and ships Noto Sans CJK SC Regular as a separate font file |
+| craft-fonts: optional build input, not files in this repository | BIZ UDPGothic (Regular, Bold), BIZ UDMincho Regular, Shippori Mincho Regular, Noto Sans CJK SC Regular, Noto Sans Arabic Regular, Noto Sans Thai Regular | The Noto Project Authors (Arabic and Thai); Morisawa Inc. / The BIZ UDGothic and BIZ UDMincho Project Authors; The Shippori Mincho Project Authors; Google and Adobe (Noto Sans CJK, maintainer-approved exception; see AGENTS.md) (per craft-fonts' attribution) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | SIL Open Font License 1.1 (each font's OFL.txt in craft-fonts; release packages ship it as OFL-.txt) | 2026-10-06 | none; native builds made with `CRAFT_FONTS_DIR` embed the manifest fonts (all official releases); the web build embeds BIZ UDPGothic Regular and Noto Sans Thai Regular and ships Noto Sans CJK SC Regular as a separate font file |
| `crates/ui-egui/locales/uk.json`, `crates/ui-egui/locales/uk-formats.json` | Ukrainian interface catalogs | @dmatviichuk with Codex; PhotoCraft, FilmCraft and VectorCraft translation contributors for reused labels | original translations from English source; 337 initial English-key matches reviewed from [PhotoCraft 69c41f3](https://github.com/storytold/photocraft/blob/69c41f3523e147eb215da8c7b4c29b18b35d1c63/crates/ui-egui/src/i18n/uk.tsv), [FilmCraft 4854c27](https://github.com/storytold/filmcraft/blob/4854c277536fe8118257362304b6a7d82d8e704b/crates/ui-egui/src/i18n/uk.tsv), [VectorCraft 4b44265](https://github.com/storytold/vectorcraft/blob/4b44265965fdd6ffedfd6182a892c1a6c360d344/crates/ui-egui/src/i18n/uk.tsv) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`, also the source repositories' licences) | 2026-10-09 | adapted to LightCraft photo-development context, completed catalogs, reviewed spelling and placeholders |
+| `crates/ui-egui/locales/cs.json`, `crates/ui-egui/locales/cs-formats.json` | Czech interface catalogs | @Losminek with AI assistance (Codex) | original work: translations of English source phrases, reviewed against the app UI conventions; no external translation material | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | 2026-10-11 | catalog coverage, Czech dates and count-neutral wording; independent scope/terminology review; native-speaker proofreading pending |
+| `crates/ui-egui/locales/it.json`, `crates/ui-egui/locales/it-formats.json` | Italian interface catalogs | @GabrielePecchioli with Claude Code; PhotoCraft, VectorCraft, PdfCraft and WordCraft translation contributors for reused labels | original translations from English source; 410 initial English-key matches reviewed from [PhotoCraft da3da09](https://github.com/storytold/photocraft/blob/da3da0942c14c1159f40c652afcf33584332c733/crates/ui-egui/src/i18n/it.tsv), [VectorCraft 5432821](https://github.com/storytold/vectorcraft/blob/543282148007bcb5633c493a5374cd26b0339c8d/crates/ui-egui/src/i18n/it.tsv), [PdfCraft e8e275c](https://github.com/storytold/pdfcraft/blob/e8e275cbd40ccd0dc6b56f82b16a575571043702/crates/ui-egui/src/i18n/it.tsv) and the Italian WordCraft catalog ([storytold/wordcraft#598](https://github.com/storytold/wordcraft/pull/598)); 46 of them retranslated where the sibling's meaning didn't fit a photo app | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`, also the source repositories' licences) | 2026-10-12 | adapted to LightCraft photo-development context, count-neutral labels for Italian plural agreement, reviewed placeholders |
+| `crates/ui-egui/locales/ka.json`, `crates/ui-egui/locales/ka-formats.json` | Georgian interface catalogs | Davit Matchakhelidze with Claude; WordCraft, GridCraft, PhotoCraft, FilmCraft and VectorCraft Georgian catalogs for reused labels | original translations from the English source (meaning-based, no vendor localization used); 450 English-key matches reused from the sibling Craft apps' Georgian catalogs | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | 2026-10-12 | adapted to LightCraft photo-library and raw-development context; count-neutral numerals; placeholders checked |
Notes
- Fonts authored or published by Adobe (Source Sans/Serif/Code, Source Han, …) are not used, even though some are
diff --git a/assets/camera-profiles/ILCA-99M2.json b/assets/camera-profiles/ILCA-99M2.json
new file mode 100644
index 000000000..ff18a63c1
--- /dev/null
+++ b/assets/camera-profiles/ILCA-99M2.json
@@ -0,0 +1,1819 @@
+{
+ "version": 1,
+ "model": "ILCA-99M2",
+ "files": 144,
+ "samples": 501511,
+ "matrix": [
+ [1.1432775892083846, -0.27864150084892714, 0.13786480817829982],
+ [-0.05589204732762987, 1.1699605558717394, -0.11694884297799035],
+ [0.004313413220749851, -0.7088386949439993, 1.7263782534882033]
+ ],
+ "hue_sat": {
+ "hue_divisions": 72,
+ "sat_divisions": 5,
+ "val_divisions": 5,
+ "data": [
+ [0.0, 1.0, 1.0],
+ [2.804254, 1.2235245, 1.0],
+ [5.0115886, 1.2228547, 1.0],
+ [5.1745224, 1.1897815, 1.0],
+ [3.4398973, 1.0871476, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.1009562, 1.231483, 1.0],
+ [3.2371128, 1.1828748, 1.0],
+ [3.6905267, 1.1272595, 1.0],
+ [3.2581418, 1.0763286, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3609977, 1.2428942, 1.0],
+ [1.1722605, 1.1633604, 1.0],
+ [1.2722441, 1.0881792, 1.0],
+ [0.99433255, 1.026622, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.12375347, 1.2171124, 1.0],
+ [-0.6004144, 1.1662252, 1.0],
+ [-1.2418907, 1.1001159, 1.0],
+ [-1.3929275, 1.017488, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.4741973, 1.1985689, 1.0],
+ [-2.3110185, 1.1587616, 1.0],
+ [-3.2606716, 1.1098144, 1.0],
+ [-3.9140587, 1.0290464, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.748514, 1.1680661, 1.0],
+ [-3.8273644, 1.1325561, 1.0],
+ [-4.7215014, 1.0942773, 1.0],
+ [-5.6006517, 1.0109093, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.6385696, 1.1548748, 1.0],
+ [-4.8762474, 1.106731, 1.0],
+ [-5.845225, 1.0598869, 1.0],
+ [-6.7829485, 0.9773247, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.4043136, 1.1618558, 1.0],
+ [-5.663378, 1.1124074, 1.0],
+ [-6.676991, 1.0622271, 1.0],
+ [-7.2388797, 0.9792954, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.5636735, 1.1730139, 1.0],
+ [-5.5787325, 1.131743, 1.0],
+ [-6.434246, 1.0818648, 1.0],
+ [-7.713659, 0.977632, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.1642222, 1.1961858, 1.0],
+ [-5.135349, 1.1508327, 1.0],
+ [-5.9561267, 1.1025041, 1.0],
+ [-7.8126693, 0.9789329, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.3540795, 1.2313552, 1.0],
+ [-4.0874486, 1.1859667, 1.0],
+ [-5.090505, 1.1065466, 1.0],
+ [-6.980868, 0.94789493, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.2277284, 1.2495877, 1.0],
+ [-3.6526108, 1.1937634, 1.0],
+ [-3.5733712, 1.0832378, 1.0],
+ [-5.550715, 0.9253031, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.2758887, 1.2372755, 1.0],
+ [-2.8500462, 1.1516474, 1.0],
+ [-1.8038367, 1.0081928, 1.0],
+ [-2.1356683, 0.93026036, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.842822, 1.1995343, 1.0],
+ [-1.5608833, 1.0880193, 1.0],
+ [-0.6414115, 0.96593964, 1.0],
+ [-0.8170321, 0.9045082, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.8589126, 1.1368302, 1.0],
+ [-0.5850774, 1.0399925, 1.0],
+ [-0.4096108, 0.94941473, 1.0],
+ [-0.67716926, 0.89125115, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.50222045, 1.0790738, 1.0],
+ [0.13412993, 1.0091311, 1.0],
+ [0.04161173, 0.94268256, 1.0],
+ [0.21809827, 0.89096487, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.5395973, 1.0523564, 1.0],
+ [0.68198884, 0.9942523, 1.0],
+ [0.8921194, 0.93612367, 1.0],
+ [1.5682732, 0.8875503, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4230179, 1.0399433, 1.0],
+ [1.7522293, 0.98858124, 1.0],
+ [2.120758, 0.93895113, 1.0],
+ [2.7880068, 0.8917508, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.485209, 1.0362785, 1.0],
+ [3.2553241, 0.9826427, 1.0],
+ [3.6980822, 0.9420671, 1.0],
+ [3.6589043, 0.9088622, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.8520021, 1.0283107, 1.0],
+ [4.4029727, 0.97790647, 1.0],
+ [4.6192904, 0.941655, 1.0],
+ [3.3324108, 0.9242873, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.579471, 1.0396363, 1.0],
+ [5.1236196, 0.99359185, 1.0],
+ [4.4844737, 0.9637591, 1.0],
+ [0.93364024, 0.9805732, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.6343756, 1.0391579, 1.0],
+ [5.9764285, 0.9963135, 1.0],
+ [4.2140217, 0.97128063, 1.0],
+ [0.31541073, 0.99498296, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.3622503, 1.0628132, 1.0],
+ [5.916998, 1.0203458, 1.0],
+ [1.3126858, 0.9978764, 1.0],
+ [-0.040236503, 0.99987936, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.1092844, 1.1148107, 1.0],
+ [5.2341003, 1.0724294, 1.0],
+ [0.57583106, 1.0097842, 1.0],
+ [-0.004625028, 1.0000128, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.6949396, 1.1773212, 1.0],
+ [3.8311253, 1.1038187, 1.0],
+ [-0.11847352, 1.0086719, 1.0],
+ [-0.0105433175, 1.000084, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.546873, 1.1809658, 1.0],
+ [0.6606819, 1.0607728, 1.0],
+ [-1.8319762, 0.98954993, 1.0],
+ [-0.11198713, 0.9991004, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.975616, 1.171921, 1.0],
+ [-2.6058009, 1.0177505, 1.0],
+ [-5.031273, 0.9486056, 1.0],
+ [-0.71531844, 0.99169016, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.3296447, 1.1538781, 1.0],
+ [-4.7061315, 0.9983956, 1.0],
+ [-7.1538033, 0.9327583, 1.0],
+ [-1.329432, 0.9859865, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.33782, 1.1317362, 1.0],
+ [-5.738668, 0.9846136, 1.0],
+ [-8.032541, 0.93113464, 1.0],
+ [-1.414409, 0.9868347, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1102281, 1.1464978, 1.0],
+ [-4.347359, 1.0098535, 1.0],
+ [-5.8042417, 0.9558565, 1.0],
+ [-0.65224177, 0.99453866, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3978106, 1.1282454, 1.0],
+ [-2.5084882, 1.0191262, 1.0],
+ [-2.5297656, 0.9832681, 1.0],
+ [-0.15988652, 0.99876547, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.6285329, 1.1082805, 1.0],
+ [-1.8376639, 1.0320127, 1.0],
+ [-1.5057008, 0.99416333, 1.0],
+ [-0.07763386, 0.99956375, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.47474164, 1.1118255, 1.0],
+ [-2.2814848, 1.0398426, 1.0],
+ [-0.96154535, 0.9967108, 1.0],
+ [-0.038478848, 0.99972326, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.28353742, 1.1260961, 1.0],
+ [-2.6729789, 1.0359904, 1.0],
+ [-0.76547706, 0.9970432, 1.0],
+ [-0.02355436, 0.99970925, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.65706646, 1.1279969, 1.0],
+ [-2.8462512, 1.0391037, 1.0],
+ [-0.5868237, 1.0005401, 1.0],
+ [-0.010716562, 0.9999053, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.12108033, 1.1502995, 1.0],
+ [-1.9704113, 1.0370626, 1.0],
+ [-0.63179135, 1.0001434, 1.0],
+ [-0.015800219, 0.99995714, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.0309358, 1.1655595, 1.0],
+ [0.13250555, 1.0445937, 1.0],
+ [-0.07715364, 1.0009365, 1.0],
+ [-0.0021050358, 0.99999684, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.2827152, 1.1324676, 1.0],
+ [0.014774211, 1.0343506, 1.0],
+ [-0.019916466, 1.0006523, 1.0],
+ [-0.00040274212, 1.0000006, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.2018666, 1.1418259, 1.0],
+ [-1.4933577, 1.0408006, 1.0],
+ [-0.3667708, 1.0002662, 1.0],
+ [-0.007263095, 0.9999904, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.64397, 1.1194679, 1.0],
+ [-1.6423713, 1.0442007, 1.0],
+ [-0.20484583, 1.0007143, 1.0],
+ [-0.0025689714, 0.99998134, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.8773065, 1.110005, 1.0],
+ [-2.6916754, 1.0417831, 1.0],
+ [-0.33602908, 1.0002605, 1.0],
+ [-0.0021417958, 0.9999703, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.714574, 1.0977588, 1.0],
+ [-3.5595996, 1.0386114, 1.0],
+ [-1.5827396, 1.0060165, 1.0],
+ [-0.028114408, 1.0000618, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.2299962, 1.0955467, 1.0],
+ [-3.3268821, 1.0478108, 1.0],
+ [-1.9395999, 1.0131217, 1.0],
+ [-0.075512886, 1.0002799, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.336435, 1.0960432, 1.0],
+ [-0.8794556, 1.0795935, 1.0],
+ [-0.29321554, 1.0271318, 1.0],
+ [0.00032340753, 1.0008185, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.09420732, 1.1007514, 1.0],
+ [0.45092306, 1.0983796, 1.0],
+ [1.0769465, 1.0420003, 1.0],
+ [0.22761619, 1.0022283, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.71772265, 1.1304002, 1.0],
+ [1.5783058, 1.1197016, 1.0],
+ [2.8271558, 1.0629567, 1.0],
+ [1.0452774, 1.0075884, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.7700194, 1.1402828, 1.0],
+ [2.267522, 1.1383678, 1.0],
+ [2.3750477, 1.0799156, 1.0],
+ [0.46060455, 0.99580747, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.9440982, 1.1368023, 1.0],
+ [3.6898718, 1.1418967, 1.0],
+ [3.8923314, 1.1259586, 1.0],
+ [1.2024251, 1.0325387, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.2370567, 1.1504492, 1.0],
+ [4.6764765, 1.1490715, 1.0],
+ [3.910422, 1.1321807, 1.0],
+ [0.89606607, 1.0308274, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.9599085, 1.134546, 1.0],
+ [5.1401796, 1.1103181, 1.0],
+ [3.7372687, 1.0921954, 1.0],
+ [0.55078083, 1.021753, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.7153788, 1.1273357, 1.0],
+ [5.978767, 1.1330243, 1.0],
+ [3.9994862, 1.1207391, 1.0],
+ [0.36665595, 1.0121281, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.2697597, 1.1095688, 1.0],
+ [5.6365104, 1.1205149, 1.0],
+ [3.4179032, 1.1214768, 1.0],
+ [0.26656342, 1.0103098, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.1428804, 1.0995386, 1.0],
+ [3.956236, 1.1270815, 1.0],
+ [1.9768763, 1.1226226, 1.0],
+ [0.11463216, 1.0099398, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.929935, 1.1041147, 1.0],
+ [3.3182452, 1.1096137, 1.0],
+ [1.2072796, 1.0818946, 1.0],
+ [-0.01571625, 1.005663, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.265804, 1.1154059, 1.0],
+ [3.7104347, 1.0477185, 1.0],
+ [1.056846, 0.9974326, 1.0],
+ [0.03417005, 0.9998331, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.849703, 1.1269237, 1.0],
+ [4.3993697, 1.0325269, 1.0],
+ [1.3606595, 0.99489033, 1.0],
+ [0.047941446, 0.99993676, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.439311, 1.1298556, 1.0],
+ [3.303848, 1.0538055, 1.0],
+ [0.59302783, 1.0085934, 1.0],
+ [0.025294233, 1.0004109, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.608997, 1.1400498, 1.0],
+ [3.3591669, 1.0461109, 1.0],
+ [0.40004572, 1.0048742, 1.0],
+ [0.01262109, 1.0001965, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.761172, 1.1502578, 1.0],
+ [2.1108382, 1.0369791, 1.0],
+ [0.11530293, 1.0012623, 1.0],
+ [0.001693561, 1.0000176, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.5255957, 1.1716747, 1.0],
+ [1.0459789, 1.037805, 1.0],
+ [0.09180857, 1.001637, 1.0],
+ [0.003279748, 1.0000362, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.1823792, 1.1748279, 1.0],
+ [1.723147, 1.0416057, 1.0],
+ [0.2180456, 1.0030037, 1.0],
+ [0.005272273, 1.0000591, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.7087555, 1.1780843, 1.0],
+ [2.619079, 1.0446409, 1.0],
+ [0.35640785, 1.0032725, 1.0],
+ [0.010388461, 1.0001107, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.8262243, 1.1712428, 1.0],
+ [3.972946, 1.0097475, 1.0],
+ [0.6286015, 0.9970032, 1.0],
+ [0.017907226, 1.0001063, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.524165, 1.182861, 1.0],
+ [3.200896, 1.0579331, 1.0],
+ [0.28860277, 1.0027157, 1.0],
+ [0.0036479658, 1.0000447, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.831539, 1.1791148, 1.0],
+ [2.774682, 1.0702703, 1.0],
+ [0.35563686, 1.0059416, 1.0],
+ [0.0069925115, 1.0001086, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.6593747, 1.1839095, 1.0],
+ [2.9386632, 1.0513804, 1.0],
+ [0.23482445, 0.999349, 1.0],
+ [0.0063177217, 1.0000361, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.268381, 1.1659219, 1.0],
+ [2.5044994, 1.0421095, 1.0],
+ [0.20415363, 0.99924254, 1.0],
+ [0.0059488467, 1.0000399, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.236816, 1.1485709, 1.0],
+ [4.5540986, 1.06799, 1.0],
+ [1.4449328, 1.0170963, 1.0],
+ [0.0639702, 1.0006945, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.453048, 1.1589189, 1.0],
+ [6.3102107, 1.1038384, 1.0],
+ [3.1707234, 1.032528, 1.0],
+ [0.19751447, 1.00155, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.372379, 1.202752, 1.0],
+ [7.4304466, 1.1593263, 1.0],
+ [5.6761837, 1.0836742, 1.0],
+ [0.6670318, 1.0070612, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.3508396, 1.1789645, 1.0],
+ [7.0611863, 1.1524564, 1.0],
+ [7.2240868, 1.1157393, 1.0],
+ [2.2516217, 1.0274429, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.5282054, 1.1895584, 1.0],
+ [6.615412, 1.2008698, 1.0],
+ [7.1562805, 1.1773871, 1.0],
+ [3.693685, 1.0640011, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.989149, 1.1666261, 1.0],
+ [5.0449367, 1.163257, 1.0],
+ [5.3947783, 1.1525968, 1.0],
+ [4.8617563, 1.1140602, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.4345527, 1.1707076, 1.0],
+ [4.030044, 1.1360824, 1.0],
+ [4.400234, 1.1141994, 1.0],
+ [3.9981327, 1.0895209, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.6249478, 1.1449721, 1.0],
+ [2.3648932, 1.0950501, 1.0],
+ [2.5787578, 1.0738068, 1.0],
+ [1.557968, 1.048907, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1732466, 1.1023636, 1.0],
+ [1.5519327, 1.0694385, 1.0],
+ [0.8835925, 1.0562818, 1.0],
+ [-0.95382243, 1.0234519, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.54519135, 1.0728073, 1.0],
+ [0.7171778, 1.047036, 1.0],
+ [-0.4340524, 1.0440289, 1.0],
+ [-3.1891012, 1.0054655, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.81599987, 1.0807294, 1.0],
+ [-1.1246754, 1.0611808, 1.0],
+ [-2.2497585, 1.0415246, 1.0],
+ [-4.411367, 0.973935, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7861705, 1.0760409, 1.0],
+ [-2.835779, 1.0522164, 1.0],
+ [-3.9953759, 1.0148282, 1.0],
+ [-4.4476047, 0.9319428, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.4466016, 1.0813212, 1.0],
+ [-3.5611944, 1.0451691, 1.0],
+ [-4.5130477, 1.011627, 1.0],
+ [-4.224515, 0.92788553, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.003385, 1.1208798, 1.0],
+ [-4.1261635, 1.0943259, 1.0],
+ [-5.189995, 1.0589299, 1.0],
+ [-6.5665016, 0.95204496, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.5737576, 1.1543715, 1.0],
+ [-3.583895, 1.1235094, 1.0],
+ [-4.758327, 1.0856714, 1.0],
+ [-6.4036903, 0.95784587, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.783091, 1.1985654, 1.0],
+ [-2.344906, 1.1536651, 1.0],
+ [-3.4222968, 1.0852684, 1.0],
+ [-5.1206603, 0.9148598, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.8261324, 1.2225533, 1.0],
+ [-2.3288019, 1.1683235, 1.0],
+ [-1.7856505, 1.0476049, 1.0],
+ [-4.6820045, 0.88435495, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9707233, 1.2195381, 1.0],
+ [-0.81782085, 1.122641, 1.0],
+ [0.9111474, 0.9622587, 1.0],
+ [-1.1497722, 0.88565606, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.09956, 1.2140585, 1.0],
+ [0.056621242, 1.0911676, 1.0],
+ [1.0300432, 0.9330744, 1.0],
+ [-0.18233444, 0.8674556, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.7154876, 1.144233, 1.0],
+ [1.4851263, 1.0231369, 1.0],
+ [0.555909, 0.9251492, 1.0],
+ [-0.18147656, 0.8636336, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3398509, 1.0599889, 1.0],
+ [1.6690949, 0.9969751, 1.0],
+ [0.37584245, 0.92953223, 1.0],
+ [0.07826649, 0.87178516, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.508692, 1.0410949, 1.0],
+ [1.8833909, 0.99179685, 1.0],
+ [1.2507737, 0.9335844, 1.0],
+ [1.6161711, 0.8835863, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.1758716, 1.0253911, 1.0],
+ [2.4910228, 0.9809797, 1.0],
+ [2.236299, 0.9303089, 1.0],
+ [2.6537087, 0.8825234, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.6103837, 1.0229514, 1.0],
+ [3.1707804, 0.97286725, 1.0],
+ [3.3409152, 0.9399243, 1.0],
+ [3.5829213, 0.9052561, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.3082023, 1.0349113, 1.0],
+ [4.284679, 0.9839242, 1.0],
+ [4.3494177, 0.9484854, 1.0],
+ [3.8492424, 0.9255855, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.793538, 1.0573108, 1.0],
+ [5.6492386, 0.98787755, 1.0],
+ [6.214179, 0.91949385, 1.0],
+ [3.9530585, 0.92670876, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.384946, 1.070319, 1.0],
+ [6.3971586, 0.98128706, 1.0],
+ [6.9482284, 0.89684695, 1.0],
+ [4.2139463, 0.9239156, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.509941, 1.109065, 1.0],
+ [4.31208, 1.0552181, 1.0],
+ [-1.2347931, 0.9985642, 1.0],
+ [-0.86304265, 0.9993037, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.1984997, 1.162435, 1.0],
+ [3.29675, 1.1169462, 1.0],
+ [-2.0880382, 1.0190989, 1.0],
+ [-0.5265214, 0.99782705, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.2004547, 1.2332976, 1.0],
+ [2.4406395, 1.1688915, 1.0],
+ [-0.54659694, 1.020866, 1.0],
+ [-0.039971404, 0.99981636, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.9126167, 1.2407047, 1.0],
+ [-0.152128, 1.1377726, 1.0],
+ [-2.4389071, 0.9923629, 1.0],
+ [-0.18998405, 0.99835, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.832136, 1.2248999, 1.0],
+ [-4.439477, 1.0757953, 1.0],
+ [-6.5581284, 0.94083136, 1.0],
+ [-1.3619823, 0.98418033, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.8086253, 1.2119304, 1.0],
+ [-6.9719944, 1.0509604, 1.0],
+ [-8.899021, 0.9207292, 1.0],
+ [-2.5299528, 0.97328454, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.2520319, 1.1828909, 1.0],
+ [-7.6938405, 1.0309292, 1.0],
+ [-9.733912, 0.9186155, 1.0],
+ [-2.4881692, 0.97676903, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.6646547, 1.1841325, 1.0],
+ [-6.6125364, 1.0378011, 1.0],
+ [-7.26035, 0.942406, 1.0],
+ [-1.0572171, 0.99080205, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.598152, 1.1774639, 1.0],
+ [-4.97536, 1.0513587, 1.0],
+ [-3.3090515, 0.9811577, 1.0],
+ [-0.20941074, 0.9982669, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.010846901, 1.1766325, 1.0],
+ [-5.117737, 1.0593848, 1.0],
+ [-2.631352, 0.99419326, 1.0],
+ [-0.11850837, 0.9993512, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.5248753, 1.1829028, 1.0],
+ [-6.0299883, 1.0577121, 1.0],
+ [-2.215714, 0.99519426, 1.0],
+ [-0.067606665, 0.9995176, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.1450118, 1.1907917, 1.0],
+ [-5.886881, 1.0472964, 1.0],
+ [-2.5883603, 0.98812264, 1.0],
+ [-0.085475355, 0.9989431, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.39547902, 1.2007595, 1.0],
+ [-4.610833, 1.0562892, 1.0],
+ [-1.7052215, 0.99749553, 1.0],
+ [-0.037703, 0.9996055, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.01975878, 1.2410866, 1.0],
+ [-1.2528956, 1.080659, 1.0],
+ [-0.68467104, 1.0020955, 1.0],
+ [-0.019976415, 0.99990904, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.2921033, 1.2522528, 1.0],
+ [0.5268386, 1.0997523, 1.0],
+ [0.03676964, 1.0044748, 1.0],
+ [-0.0015876454, 1.0000012, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.6588416, 1.1782904, 1.0],
+ [-0.8304472, 1.0511743, 1.0],
+ [-0.09593859, 1.0008658, 1.0],
+ [-0.0012349889, 0.9999909, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.5003, 1.1401259, 1.0],
+ [-2.4005525, 1.002213, 1.0],
+ [-0.7121907, 0.98226595, 1.0],
+ [-0.017461486, 0.999597, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.994106, 1.0949768, 1.0],
+ [-2.85081, 1.0083107, 1.0],
+ [-0.30721804, 0.9763625, 1.0],
+ [-0.0013189864, 0.9991703, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.196782, 1.1068348, 1.0],
+ [-3.2161317, 1.0495526, 1.0],
+ [-1.099006, 0.99147415, 1.0],
+ [-0.024919514, 0.9993193, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.84915, 1.05568, 1.0],
+ [-3.9007847, 1.03011, 1.0],
+ [-2.599225, 1.0099269, 1.0],
+ [-0.11461259, 0.99996513, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.721844, 1.055208, 1.0],
+ [-3.9854348, 1.0393229, 1.0],
+ [-2.5744975, 1.0209645, 1.0],
+ [-0.293057, 1.001174, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.059898, 1.0536826, 1.0],
+ [-2.5768554, 1.0555562, 1.0],
+ [-1.39879, 1.0136101, 1.0],
+ [-0.50610787, 0.9911003, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.9069445, 1.0556097, 1.0],
+ [-1.5781095, 1.0518695, 1.0],
+ [-0.8278498, 1.001524, 1.0],
+ [-0.32421526, 0.98277986, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.4363952, 1.0812076, 1.0],
+ [0.5487326, 1.0899016, 1.0],
+ [2.4110415, 1.0404066, 1.0],
+ [2.9115984, 1.0175672, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.008678005, 1.1017222, 1.0],
+ [1.2356163, 1.1221837, 1.0],
+ [2.8750088, 1.0839373, 1.0],
+ [3.1348855, 1.032868, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.88217217, 1.091444, 1.0],
+ [2.0927439, 1.1262348, 1.0],
+ [3.240285, 1.1331868, 1.0],
+ [2.67113, 1.0834583, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.428248, 1.095952, 1.0],
+ [3.390544, 1.1449028, 1.0],
+ [3.5510561, 1.1565588, 1.0],
+ [2.31717, 1.0987086, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.1580663, 1.0814137, 1.0],
+ [4.4028983, 1.1175768, 1.0],
+ [3.174666, 1.1427947, 1.0],
+ [2.0205822, 1.1122999, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.2129626, 1.0714386, 1.0],
+ [6.590155, 1.0948668, 1.0],
+ [4.619345, 1.1303223, 1.0],
+ [1.1349491, 1.0543766, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.7700667, 1.0417291, 1.0],
+ [6.674038, 1.0694815, 1.0],
+ [3.9405415, 1.1325624, 1.0],
+ [0.40215716, 1.0195748, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.373421, 1.0177879, 1.0],
+ [5.4952292, 1.081058, 1.0],
+ [2.1721451, 1.1446264, 1.0],
+ [0.14383824, 1.0215358, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.597993, 1.0280006, 1.0],
+ [4.9888263, 1.084475, 1.0],
+ [1.9800822, 1.1122874, 1.0],
+ [-0.23990147, 1.0166304, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.2620325, 1.0467869, 1.0],
+ [6.4645605, 1.0722194, 1.0],
+ [2.8058643, 1.069191, 1.0],
+ [0.13071911, 1.005048, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.7541, 1.079291, 1.0],
+ [7.8130198, 1.0582371, 1.0],
+ [3.0989928, 1.0383492, 1.0],
+ [0.14017454, 1.0023274, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.04703, 1.1110121, 1.0],
+ [6.10701, 1.0926124, 1.0],
+ [2.378649, 1.0378062, 1.0],
+ [0.111506626, 1.001809, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.323562, 1.1182009, 1.0],
+ [7.539196, 1.0773741, 1.0],
+ [2.3685758, 1.0214494, 1.0],
+ [0.07044682, 1.0008357, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.691661, 1.143956, 1.0],
+ [7.088831, 1.0745311, 1.0],
+ [1.8496133, 1.0144777, 1.0],
+ [0.05676793, 1.0005342, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.4111695, 1.2071966, 1.0],
+ [4.6049232, 1.1096628, 1.0],
+ [1.9098912, 1.0257343, 1.0],
+ [0.07917395, 1.0008571, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.0088325, 1.2360815, 1.0],
+ [5.181472, 1.1226107, 1.0],
+ [2.5577261, 1.0353444, 1.0],
+ [0.112243205, 1.0013376, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.824198, 1.2390603, 1.0],
+ [7.472616, 1.1490262, 1.0],
+ [3.6435897, 1.0531034, 1.0],
+ [0.21466114, 1.00276, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.237296, 1.2194712, 1.0],
+ [7.4227743, 1.131169, 1.0],
+ [3.8712175, 1.049929, 1.0],
+ [0.27166268, 1.0031942, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.786514, 1.1667371, 1.0],
+ [6.6639266, 1.1076087, 1.0],
+ [2.2446363, 1.0388349, 1.0],
+ [0.102016255, 1.0017875, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.9329085, 1.1376165, 1.0],
+ [6.6409307, 1.1026622, 1.0],
+ [3.2540584, 1.0458134, 1.0],
+ [0.23406105, 1.0032705, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.150625, 1.1396258, 1.0],
+ [6.7835217, 1.1053689, 1.0],
+ [3.3122694, 1.0424184, 1.0],
+ [0.22282204, 1.0027751, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.179195, 1.1203728, 1.0],
+ [6.1122427, 1.0689247, 1.0],
+ [2.574998, 1.0149101, 1.0],
+ [0.17865337, 1.001089, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.981582, 1.1198573, 1.0],
+ [6.162663, 1.0758042, 1.0],
+ [3.49672, 1.0282632, 1.0],
+ [0.5296274, 1.0024091, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.104293, 1.1063842, 1.0],
+ [6.60601, 1.0895218, 1.0],
+ [6.026096, 1.0566804, 1.0],
+ [1.4251214, 1.0060695, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.400258, 1.118055, 1.0],
+ [8.1627245, 1.1168847, 1.0],
+ [8.838523, 1.1060567, 1.0],
+ [3.2699623, 1.0280458, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.0916386, 1.0858877, 1.0],
+ [7.020515, 1.0801034, 1.0],
+ [8.994645, 1.1054409, 1.0],
+ [6.3794594, 1.070257, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.8969727, 1.0963963, 1.0],
+ [5.3678546, 1.111281, 1.0],
+ [7.492073, 1.145503, 1.0],
+ [6.806583, 1.107631, 1.0],
+ [0.0, 1.0, 1.0],
+ [11.097742, 0.7495534, 1.0],
+ [4.302149, 1.0255402, 1.0],
+ [4.3949976, 1.078089, 1.0],
+ [3.0446813, 1.0549035, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.670632, 0.7986941, 1.0],
+ [4.084819, 1.0083629, 1.0],
+ [4.0668464, 1.0512977, 1.0],
+ [3.5106099, 1.0471594, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.4860106, 0.8172128, 1.0],
+ [3.818937, 0.93780375, 1.0],
+ [3.2241101, 1.0080932, 1.0],
+ [2.3703701, 1.0238551, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.833141, 0.8377232, 1.0],
+ [4.498794, 0.8643455, 1.0],
+ [3.6683908, 0.9057964, 1.0],
+ [1.7416582, 0.96007955, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.6399913, 0.80761904, 1.0],
+ [4.839282, 0.8098061, 1.0],
+ [4.2472334, 0.8426727, 1.0],
+ [2.3996296, 0.87981254, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.0833488, 0.78954774, 1.0],
+ [4.703831, 0.781375, 1.0],
+ [4.1265993, 0.8191039, 1.0],
+ [3.244208, 0.82794446, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.2156487, 0.8334128, 1.0],
+ [2.5546548, 0.82133436, 1.0],
+ [2.4130013, 0.82352406, 1.0],
+ [2.1992364, 0.81915104, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.719304, 0.8250702, 1.0],
+ [1.4982271, 0.8221971, 1.0],
+ [1.3897603, 0.840134, 1.0],
+ [1.7225292, 0.8336163, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.0925283, 0.85481983, 1.0],
+ [0.7631879, 0.85975116, 1.0],
+ [0.027124116, 0.87992114, 1.0],
+ [0.60234094, 0.84217334, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.077557, 0.872631, 1.0],
+ [1.0173641, 0.8593242, 1.0],
+ [0.46943918, 0.87838364, 1.0],
+ [2.6384852, 0.79771537, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7770302, 0.9353661, 1.0],
+ [1.0596069, 0.8860215, 1.0],
+ [1.2889328, 0.8582148, 1.0],
+ [4.9600816, 0.74249977, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.34926006, 0.98829997, 1.0],
+ [0.18424937, 0.9600296, 1.0],
+ [1.1720486, 0.8844167, 1.0],
+ [2.579216, 0.7862993, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.30250847, 0.9603346, 1.0],
+ [1.107201, 0.928601, 1.0],
+ [1.9961823, 0.8556583, 1.0],
+ [-0.3471245, 0.82866865, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.7629411, 1.0180708, 1.0],
+ [0.610268, 0.974038, 1.0],
+ [1.429175, 0.8520721, 1.0],
+ [0.48019677, 0.8095613, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.3008022, 0.9407405, 1.0],
+ [1.6219236, 0.9161009, 1.0],
+ [0.49270624, 0.8484004, 1.0],
+ [-0.12723985, 0.791222, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.25840467, 0.85001826, 1.0],
+ [1.2374158, 0.87433594, 1.0],
+ [-0.47591653, 0.8426007, 1.0],
+ [-0.9091751, 0.79707766, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1872596, 0.8374762, 1.0],
+ [1.1351508, 0.8393883, 1.0],
+ [-0.09448363, 0.82199603, 1.0],
+ [-0.22021097, 0.80633336, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.99161613, 0.82922834, 1.0],
+ [0.5485594, 0.83473396, 1.0],
+ [0.39220774, 0.826323, 1.0],
+ [0.58646864, 0.81625366, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.25968486, 0.823715, 1.0],
+ [-0.11242166, 0.8438889, 1.0],
+ [0.41922796, 0.8506193, 1.0],
+ [1.028925, 0.8475653, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.0410757, 0.82169753, 1.0],
+ [1.1827178, 0.83744586, 1.0],
+ [1.5406011, 0.8407932, 1.0],
+ [1.5877901, 0.88981646, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.7911158, 0.83056414, 1.0],
+ [4.6160154, 0.8464025, 1.0],
+ [5.6431475, 0.8491971, 1.0],
+ [2.9428084, 0.91466653, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.4177535, 0.8561214, 1.0],
+ [6.020544, 0.8638199, 1.0],
+ [6.9461846, 0.8605439, 1.0],
+ [3.500521, 0.92217606, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.851688, 0.89191425, 1.0],
+ [1.421044, 0.9390341, 1.0],
+ [-1.1085083, 0.9700407, 1.0],
+ [-0.36786354, 0.9936673, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.3683667, 0.9257322, 1.0],
+ [-0.26451036, 0.95317423, 1.0],
+ [-2.130682, 0.9368306, 1.0],
+ [-0.4093729, 0.986178, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.5891942, 0.99163103, 1.0],
+ [0.045109738, 0.966062, 1.0],
+ [-0.53449297, 0.92987776, 1.0],
+ [-0.033470135, 0.9932046, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.27134877, 0.99536645, 1.0],
+ [0.0898188, 0.98402214, 1.0],
+ [0.27262244, 0.96843773, 1.0],
+ [0.009789813, 0.9986736, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.7730278, 1.0203122, 1.0],
+ [-2.9028432, 1.0235066, 1.0],
+ [-1.0447694, 0.9868708, 1.0],
+ [-0.04624136, 0.99908286, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3631408, 1.0156548, 1.0],
+ [-3.9609292, 1.0409077, 1.0],
+ [-1.4876856, 0.9928084, 1.0],
+ [-0.095956564, 0.998979, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7949396, 0.9771055, 1.0],
+ [-3.3644142, 1.0089597, 1.0],
+ [-0.6663962, 0.98575324, 1.0],
+ [-0.060724735, 0.9990037, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.6404126, 0.9683573, 1.0],
+ [-3.7188525, 0.9884891, 1.0],
+ [-0.33517227, 0.9891578, 1.0],
+ [-0.01776004, 0.99959093, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.5392375, 0.9850047, 1.0],
+ [-1.9149097, 0.9972289, 1.0],
+ [0.076658234, 0.99591476, 1.0],
+ [0.0015469929, 0.99989164, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.5000389, 0.9821583, 1.0],
+ [-2.8312242, 0.9990609, 1.0],
+ [-0.28693938, 0.9993521, 1.0],
+ [-0.003116089, 0.9999806, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.261437, 0.9956475, 1.0],
+ [-4.1936493, 0.99516, 1.0],
+ [-0.5251263, 0.99886537, 1.0],
+ [-0.0044811945, 0.99998087, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.1491625, 1.0218415, 1.0],
+ [-4.5739403, 1.0102247, 1.0],
+ [-0.68651015, 0.99985665, 1.0],
+ [-0.0075566974, 0.99995035, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.2349107, 0.9898649, 1.0],
+ [-1.9306059, 1.0036258, 1.0],
+ [-0.31590265, 0.99970144, 1.0],
+ [-0.0033171063, 0.9999775, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.0307009, 0.99630994, 1.0],
+ [0.30616513, 1.0119231, 1.0],
+ [0.06396701, 1.0005364, 1.0],
+ [0.00027660295, 0.9999968, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.346574, 0.9880379, 1.0],
+ [-0.8791935, 1.0093088, 1.0],
+ [-0.016242307, 1.0002874, 1.0],
+ [0.00025250288, 0.99999845, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.0779517, 0.97170013, 1.0],
+ [-1.4663464, 0.99664164, 1.0],
+ [-0.10389895, 0.9996869, 1.0],
+ [-0.00044551404, 0.9999951, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.567538, 0.9528182, 1.0],
+ [-1.9175193, 0.96272826, 1.0],
+ [-0.24028678, 0.988517, 1.0],
+ [-0.0031621323, 0.9997933, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.301078, 0.9462786, 1.0],
+ [-2.0612185, 0.9623441, 1.0],
+ [-0.22937998, 0.98045844, 1.0],
+ [-0.0035447287, 0.99954635, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.17369, 0.9347734, 1.0],
+ [-2.6256938, 0.9702652, 1.0],
+ [-0.9779252, 0.98185503, 1.0],
+ [-0.025275169, 0.999532, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.6190476, 0.87789106, 1.0],
+ [-4.1578727, 0.9216868, 1.0],
+ [-2.5072808, 0.9612179, 1.0],
+ [-0.089652225, 0.9986668, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.76994, 0.87418616, 1.0],
+ [-6.514886, 0.907811, 1.0],
+ [-4.9078856, 0.9322168, 1.0],
+ [-0.41067597, 0.99400187, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.1820183, 0.9030464, 1.0],
+ [-4.7838874, 0.9592428, 1.0],
+ [-3.901864, 0.9493777, 1.0],
+ [-2.8153791, 0.9527283, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.788914, 0.91208506, 1.0],
+ [-3.994215, 0.97003484, 1.0],
+ [-3.385323, 0.9577372, 1.0],
+ [-2.9413586, 0.9510517, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.824406, 0.83820003, 1.0],
+ [-3.1810389, 0.9660887, 1.0],
+ [-1.5774505, 0.9790124, 1.0],
+ [-0.8217753, 0.9740097, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.3065467, 0.80589384, 1.0],
+ [-2.6072268, 0.93019086, 1.0],
+ [-0.21090473, 0.9885001, 1.0],
+ [0.43375567, 0.9852358, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.482041, 0.80521226, 1.0],
+ [-1.9464252, 0.89081764, 1.0],
+ [-1.3067281, 0.97063005, 1.0],
+ [-0.08558672, 1.0085713, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.005971689, 0.79016274, 1.0],
+ [0.35732928, 0.85779244, 1.0],
+ [0.1754206, 0.99188244, 1.0],
+ [0.22379632, 1.037684, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.7745373, 0.8123556, 1.0],
+ [2.660167, 0.8928497, 1.0],
+ [1.9056137, 1.0349722, 1.0],
+ [1.3743136, 1.0741935, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.84149, 0.94729656, 1.0],
+ [5.9234858, 0.99772394, 1.0],
+ [3.5638256, 1.0254989, 1.0],
+ [0.789219, 1.0284225, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.216669, 0.94323486, 1.0],
+ [6.1197004, 0.97384584, 1.0],
+ [3.111261, 1.0015063, 1.0],
+ [0.13389735, 1.0010753, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.8003964, 0.8915141, 1.0],
+ [6.054007, 0.933874, 1.0],
+ [2.107739, 1.0027483, 1.0],
+ [0.0493525, 1.0011154, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.9880567, 0.84407914, 1.0],
+ [5.6231203, 0.93609124, 1.0],
+ [2.2157097, 1.0120378, 1.0],
+ [0.032817453, 1.0015986, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.595032, 0.8414821, 1.0],
+ [5.738505, 0.9392406, 1.0],
+ [1.8269083, 1.0084609, 1.0],
+ [0.04897936, 1.0008367, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.220856, 0.87541443, 1.0],
+ [5.0150123, 0.95354825, 1.0],
+ [1.1460975, 0.9991748, 1.0],
+ [0.023244264, 1.0001849, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.352977, 0.90786576, 1.0],
+ [4.568452, 0.9657601, 1.0],
+ [1.6529517, 1.000971, 1.0],
+ [0.03857504, 1.0002371, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.984532, 0.9082033, 1.0],
+ [4.602232, 0.9495377, 1.0],
+ [1.6734874, 0.9877272, 1.0],
+ [0.03744144, 0.9998395, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.9091544, 0.93606, 1.0],
+ [5.298362, 0.9841254, 1.0],
+ [1.5838195, 1.0008063, 1.0],
+ [0.046534117, 1.0002071, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.066354, 0.97590166, 1.0],
+ [4.5295296, 1.0173208, 1.0],
+ [1.7539304, 1.013229, 1.0],
+ [0.053133972, 1.0004649, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.9509256, 0.96985763, 1.0],
+ [2.8036354, 1.0145437, 1.0],
+ [1.5774267, 1.01402, 1.0],
+ [0.066340216, 1.0006825, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.067796, 0.9293189, 1.0],
+ [4.159626, 1.0254444, 1.0],
+ [2.7054405, 1.0338923, 1.0],
+ [0.16508698, 1.0020102, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.1854787, 0.88944936, 1.0],
+ [4.127846, 0.9825014, 1.0],
+ [2.468719, 1.0214276, 1.0],
+ [0.17890404, 1.0017699, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.5697684, 0.868272, 1.0],
+ [3.8859425, 0.95566565, 1.0],
+ [1.9449633, 1.0140244, 1.0],
+ [0.15235585, 1.0016147, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.9004993, 0.8488444, 1.0],
+ [3.3493254, 0.9401458, 1.0],
+ [1.7450691, 1.0157793, 1.0],
+ [0.2219146, 1.0026209, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.1511345, 0.8408119, 1.0],
+ [2.344393, 0.9062895, 1.0],
+ [-0.006539325, 0.989216, 1.0],
+ [-0.08388412, 0.9982721, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.9436674, 0.83059716, 1.0],
+ [2.7960584, 0.8703427, 1.0],
+ [-0.36575812, 0.97218806, 1.0],
+ [-0.23437832, 0.9956032, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.1871066, 0.7945113, 1.0],
+ [3.3608866, 0.8636077, 1.0],
+ [0.7976455, 0.97853297, 1.0],
+ [0.016725179, 0.99557835, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.6724114, 0.8086163, 1.0],
+ [4.0700526, 0.9031084, 1.0],
+ [2.1568875, 0.99063873, 1.0],
+ [0.28391585, 0.99492174, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.5037627, 0.8231193, 1.0],
+ [5.7297187, 0.9803322, 1.0],
+ [4.538334, 1.0247983, 1.0],
+ [0.78753865, 0.9985357, 1.0],
+ [0.0, 1.0, 1.0],
+ [10.182478, 0.7484477, 1.0],
+ [4.6068373, 0.93632764, 1.0],
+ [5.901852, 0.9994183, 1.0],
+ [1.9283233, 1.0053344, 1.0],
+ [0.0, 1.0, 1.0],
+ [11.154881, 0.70661646, 1.0],
+ [2.7159872, 0.93115985, 1.0],
+ [4.0488343, 1.0193834, 1.0],
+ [1.9382095, 1.0209278, 1.0],
+ [0.0, 1.0, 1.0],
+ [21.429089, 0.534035, 1.0],
+ [10.9904585, 0.6681652, 1.0],
+ [0.0045763263, 0.9355186, 1.0],
+ [0.13676341, 0.9799069, 1.0],
+ [0.0, 1.0, 1.0],
+ [19.529167, 0.5656457, 1.0],
+ [9.061795, 0.7158103, 1.0],
+ [1.5038702, 0.90302753, 1.0],
+ [0.78457785, 0.9086724, 1.0],
+ [0.0, 1.0, 1.0],
+ [15.508226, 0.5985953, 1.0],
+ [7.4871893, 0.7188201, 1.0],
+ [2.3094695, 0.89489657, 1.0],
+ [0.73899657, 0.9671641, 1.0],
+ [0.0, 1.0, 1.0],
+ [10.39154, 0.65087306, 1.0],
+ [6.622824, 0.6944927, 1.0],
+ [5.501718, 0.7407023, 1.0],
+ [1.7589878, 0.92959625, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.076021, 0.62203556, 1.0],
+ [7.467209, 0.6181576, 1.0],
+ [7.245981, 0.6409904, 1.0],
+ [5.4263334, 0.7738036, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.469799, 0.5848823, 1.0],
+ [8.423419, 0.5817134, 1.0],
+ [8.497577, 0.60529923, 1.0],
+ [9.111917, 0.69250333, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.762021, 0.6242976, 1.0],
+ [7.8041244, 0.61129224, 1.0],
+ [8.319156, 0.64303446, 1.0],
+ [9.280437, 0.71576697, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.9923735, 0.63731086, 1.0],
+ [4.8158092, 0.65566516, 1.0],
+ [6.311018, 0.6723989, 1.0],
+ [8.32342, 0.7326516, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.2170925, 0.60791266, 1.0],
+ [5.0867386, 0.6423507, 1.0],
+ [6.415117, 0.66422945, 1.0],
+ [8.737183, 0.70679927, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.2006907, 0.60027266, 1.0],
+ [4.8628798, 0.61094993, 1.0],
+ [6.024442, 0.6481432, 1.0],
+ [9.87192, 0.66732514, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.6975617, 0.6112319, 1.0],
+ [4.514887, 0.6216314, 1.0],
+ [6.3849893, 0.63863033, 1.0],
+ [10.629338, 0.64065015, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.5348377, 0.62149215, 1.0],
+ [3.3658662, 0.67045367, 1.0],
+ [5.1989746, 0.71183586, 1.0],
+ [8.826136, 0.6767556, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.2873642, 0.6042733, 1.0],
+ [2.2218688, 0.65636855, 1.0],
+ [0.86957604, 0.82655025, 1.0],
+ [0.053957075, 0.88620996, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.4001925, 0.6340274, 1.0],
+ [1.1141202, 0.70201296, 1.0],
+ [0.50959253, 0.8092494, 1.0],
+ [0.2311227, 0.8478709, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.2452888, 0.66887766, 1.0],
+ [-0.07834225, 0.74380815, 1.0],
+ [-0.13215514, 0.7847734, 1.0],
+ [-0.38037032, 0.7592258, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.1262196, 0.6688091, 1.0],
+ [-1.8940061, 0.6305882, 1.0],
+ [-1.4030197, 0.7027344, 1.0],
+ [-1.2132488, 0.7527478, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7841728, 0.6271489, 1.0],
+ [-2.0797975, 0.61307365, 1.0],
+ [-1.7369123, 0.6998982, 1.0],
+ [-1.4816345, 0.78196275, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.5889271, 0.6240376, 1.0],
+ [-1.8163804, 0.6369125, 1.0],
+ [-0.99799794, 0.7531041, 1.0],
+ [-0.5732026, 0.82648337, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.0947924, 0.62250054, 1.0],
+ [-2.1894937, 0.6211645, 1.0],
+ [-1.2518405, 0.701402, 1.0],
+ [-0.14743061, 0.8963739, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3260301, 0.622353, 1.0],
+ [-1.4357653, 0.6204895, 1.0],
+ [-0.9074722, 0.7065318, 1.0],
+ [-0.00092270726, 0.94407517, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.53253675, 0.609965, 1.0],
+ [0.7909907, 0.63592654, 1.0],
+ [0.8281013, 0.861355, 1.0],
+ [0.09484549, 0.99166083, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.1939044, 0.6141987, 1.0],
+ [2.304398, 0.68116254, 1.0],
+ [1.0291927, 0.94084936, 1.0],
+ [0.07020744, 0.99777913, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.544789, 0.6641931, 1.0],
+ [1.7962058, 0.7804687, 1.0],
+ [0.024910655, 0.9779748, 1.0],
+ [-0.006917878, 0.9988935, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.512072, 0.6963922, 1.0],
+ [1.0768867, 0.8578466, 1.0],
+ [-0.15686627, 0.95935565, 1.0],
+ [-0.018332472, 0.9965962, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.2941554, 0.7241867, 1.0],
+ [0.49197897, 0.8924251, 1.0],
+ [-0.07057831, 0.97556025, 1.0],
+ [0.0007015772, 0.9984997, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.14841639, 0.75324446, 1.0],
+ [0.3125728, 0.9184302, 1.0],
+ [0.12940219, 0.99002546, 1.0],
+ [0.004882609, 0.9996811, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.0759145, 0.8147672, 1.0],
+ [0.36428088, 0.95350146, 1.0],
+ [0.044762604, 0.9945495, 1.0],
+ [0.0020008544, 0.99985397, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1844163, 0.82202315, 1.0],
+ [0.079763375, 0.9658192, 1.0],
+ [-0.040133536, 0.99962455, 1.0],
+ [-0.0008112378, 0.9999972, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3717666, 0.79799265, 1.0],
+ [0.041872248, 0.9601765, 1.0],
+ [-0.031498604, 0.9993519, 1.0],
+ [-0.00014435564, 0.99999434, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4442534, 0.8161425, 1.0],
+ [-0.063920654, 0.9636011, 1.0],
+ [-0.05635313, 0.9993309, 1.0],
+ [-0.00048342353, 0.99999565, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.52015144, 0.8598735, 1.0],
+ [0.19060682, 0.97779083, 1.0],
+ [0.0042832624, 0.9997414, 1.0],
+ [4.660216e-05, 0.999999, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.7497536, 0.84093064, 1.0],
+ [-0.81031346, 0.97599655, 1.0],
+ [-0.09859322, 0.99943244, 1.0],
+ [-0.0006943415, 0.9999969, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3677365, 0.8722283, 1.0],
+ [-1.8130565, 0.97921914, 1.0],
+ [-0.21724299, 0.9991554, 1.0],
+ [-0.0015388737, 0.99999464, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.91486025, 0.8624227, 1.0],
+ [-1.8185471, 0.98354757, 1.0],
+ [-0.18334843, 1.00006, 1.0],
+ [-0.0010572135, 1.0000012, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.7346696, 0.8424903, 1.0],
+ [-0.63329214, 0.9792088, 1.0],
+ [-0.059044026, 0.9998297, 1.0],
+ [-0.00033109298, 0.99999976, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.6148448, 0.8629954, 1.0],
+ [-0.1529285, 0.9853164, 1.0],
+ [-0.0012836644, 0.99992263, 1.0],
+ [2.836192e-07, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.1700978, 0.8661239, 1.0],
+ [-0.63642955, 0.98434937, 1.0],
+ [-0.031005092, 0.99981445, 1.0],
+ [-0.000120288605, 0.99999934, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.4762814, 0.86192167, 1.0],
+ [-0.56582534, 0.98256135, 1.0],
+ [-0.020217514, 0.9998631, 1.0],
+ [-7.2207775e-05, 0.9999997, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.8507402, 0.82345366, 1.0],
+ [-0.7367012, 0.96947515, 1.0],
+ [-0.014731503, 0.9995469, 1.0],
+ [-6.203018e-05, 0.9999977, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.3435655, 0.7789662, 1.0],
+ [-1.2070022, 0.9441542, 1.0],
+ [-0.06403829, 0.99689096, 1.0],
+ [-0.0005338597, 0.999972, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.598504, 0.7135656, 1.0],
+ [-2.8674366, 0.8796157, 1.0],
+ [-0.2061764, 0.98925316, 1.0],
+ [-0.0019789634, 0.9999003, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.5659275, 0.68000054, 1.0],
+ [-6.2886615, 0.7959308, 1.0],
+ [-1.6636302, 0.9520329, 1.0],
+ [-0.02707145, 0.99932, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.858409, 0.65844935, 1.0],
+ [-10.290696, 0.7347493, 1.0],
+ [-7.691491, 0.82455605, 1.0],
+ [-0.43717942, 0.9903527, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.475192, 0.660726, 1.0],
+ [-8.357337, 0.8211995, 1.0],
+ [-6.577057, 0.9004506, 1.0],
+ [-2.6555653, 0.960382, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.107314, 0.6639968, 1.0],
+ [-6.700647, 0.8769558, 1.0],
+ [-5.7258368, 0.9253376, 1.0],
+ [-4.3847528, 0.93834984, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.857171, 0.6214388, 1.0],
+ [-7.408514, 0.77258706, 1.0],
+ [-5.1308303, 0.92324936, 1.0],
+ [-3.1422675, 0.95364594, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.9207935, 0.5845634, 1.0],
+ [-7.1896048, 0.6665354, 1.0],
+ [-4.057381, 0.85848784, 1.0],
+ [-0.73692507, 0.97766274, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.263444, 0.5832066, 1.0],
+ [-5.9315505, 0.6382356, 1.0],
+ [-6.1455026, 0.72098804, 1.0],
+ [-1.0433422, 0.96975046, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.4575808, 0.57827437, 1.0],
+ [-2.964139, 0.62386805, 1.0],
+ [-4.5475383, 0.7487994, 1.0],
+ [-1.4768721, 0.9732885, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.157196, 0.57085544, 1.0],
+ [-0.23392056, 0.64966595, 1.0],
+ [-1.4028531, 0.88008684, 1.0],
+ [-1.9659572, 0.9766486, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.5584664, 0.6198235, 1.0],
+ [4.1219063, 0.74054, 1.0],
+ [0.9924724, 0.93495196, 1.0],
+ [-0.7218012, 0.988608, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.719361, 0.6867312, 1.0],
+ [3.9891121, 0.7887134, 1.0],
+ [1.6480545, 0.94551146, 1.0],
+ [0.059591822, 0.99834126, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.201495, 0.7431256, 1.0],
+ [4.526226, 0.8292865, 1.0],
+ [1.1760861, 0.9666664, 1.0],
+ [0.023769544, 0.9994029, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.170543, 0.76272196, 1.0],
+ [2.6068974, 0.8673706, 1.0],
+ [0.39172325, 0.9858963, 1.0],
+ [0.004990869, 0.9999019, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.301262, 0.77241415, 1.0],
+ [2.272072, 0.8702671, 1.0],
+ [0.20433316, 0.98867095, 1.0],
+ [0.0015561674, 0.9999392, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.9231374, 0.8245692, 1.0],
+ [1.1306319, 0.9499702, 1.0],
+ [0.12337404, 0.9968917, 1.0],
+ [0.0012145389, 0.9999798, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7749719, 0.84335756, 1.0],
+ [0.5761641, 0.95042235, 1.0],
+ [0.15160465, 0.9963909, 1.0],
+ [0.0022156653, 0.9999764, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.24714066, 0.83536404, 1.0],
+ [0.69087815, 0.93358576, 1.0],
+ [0.13375169, 0.9901696, 1.0],
+ [0.00097174913, 0.9998683, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.9913264, 0.84575355, 1.0],
+ [0.6101601, 0.9683786, 1.0],
+ [0.03825838, 0.9969422, 1.0],
+ [-0.00015123004, 0.9999521, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.36478883, 0.8678444, 1.0],
+ [0.0008414984, 0.9753911, 1.0],
+ [0.040139902, 0.9994379, 1.0],
+ [0.0008033622, 0.999998, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.5517159, 0.860102, 1.0],
+ [-0.14287986, 0.9731134, 1.0],
+ [-0.0241953, 0.9983607, 1.0],
+ [0.00026539815, 0.9999833, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.771025, 0.8242807, 1.0],
+ [0.39131552, 0.9689884, 1.0],
+ [0.10286286, 1.0004354, 1.0],
+ [0.0041896314, 1.0000376, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.0717576, 0.7639471, 1.0],
+ [1.06725, 0.9274218, 1.0],
+ [0.16506667, 0.99773335, 1.0],
+ [0.0063294014, 0.99998826, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.2291703, 0.7308612, 1.0],
+ [0.49462095, 0.8725961, 1.0],
+ [-0.10180267, 0.97933596, 1.0],
+ [-0.0027677065, 0.99942666, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.6348059, 0.6752963, 1.0],
+ [-0.1813347, 0.81271625, 1.0],
+ [-0.452213, 0.9789637, 1.0],
+ [-0.02926826, 0.99930775, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.977978, 0.63618356, 1.0],
+ [-0.62426865, 0.7659978, 1.0],
+ [-1.2976239, 0.96756303, 1.0],
+ [-0.16725117, 0.9974616, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.5557356, 0.63749415, 1.0],
+ [-0.46793, 0.74725634, 1.0],
+ [-1.4595929, 0.9653957, 1.0],
+ [-0.23743904, 0.99673766, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.1136274, 0.6158691, 1.0],
+ [1.4451386, 0.7140542, 1.0],
+ [-0.7944747, 0.9716655, 1.0],
+ [-0.17677365, 0.9974942, 1.0],
+ [0.0, 1.0, 1.0],
+ [12.495926, 0.6081697, 1.0],
+ [2.6334763, 0.75307316, 1.0],
+ [-0.12727743, 0.9822432, 1.0],
+ [-0.019888313, 0.9994333, 1.0],
+ [0.0, 1.0, 1.0],
+ [20.359663, 0.5505554, 1.0],
+ [5.360571, 0.7478232, 1.0],
+ [-0.15579183, 0.9834688, 1.0],
+ [0.0031478892, 0.9996698, 1.0],
+ [0.0, 1.0, 1.0],
+ [22.983652, 0.51872075, 1.0],
+ [11.201727, 0.65178657, 1.0],
+ [0.02786072, 0.96850365, 1.0],
+ [0.050246127, 0.9990089, 1.0],
+ [0.0, 1.0, 1.0],
+ [22.102345, 0.5159302, 1.0],
+ [13.516024, 0.6239164, 1.0],
+ [-0.2264105, 0.9579077, 1.0],
+ [0.011741261, 0.9989512, 1.0],
+ [0.0, 1.0, 1.0],
+ [16.458723, 0.6381099, 1.0],
+ [2.070874, 0.8461226, 1.0],
+ [-0.38940287, 0.94793105, 1.0],
+ [-0.004449404, 0.9940882, 1.0],
+ [0.0, 1.0, 1.0],
+ [14.319419, 0.6865272, 1.0],
+ [2.107183, 0.90325254, 1.0],
+ [0.08382885, 0.95650256, 1.0],
+ [0.08427957, 0.95735323, 1.0],
+ [0.0, 1.0, 1.0],
+ [11.907165, 0.7272812, 1.0],
+ [2.3358235, 0.9138314, 1.0],
+ [0.061927434, 0.9853146, 1.0],
+ [0.02880717, 0.98940504, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.328565, 0.7501317, 1.0],
+ [3.736033, 0.8165723, 1.0],
+ [0.886498, 0.9493906, 1.0],
+ [0.53428864, 0.9778165, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.8924174, 0.6558145, 1.0],
+ [8.357237, 0.58703214, 1.0],
+ [5.640144, 0.71630454, 1.0],
+ [2.3299222, 0.9120247, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.797466, 0.5970874, 1.0],
+ [10.189405, 0.5419211, 1.0],
+ [9.585496, 0.602155, 1.0],
+ [8.76234, 0.7506968, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.77069, 0.73146415, 1.0],
+ [8.458371, 0.6105788, 1.0],
+ [9.997233, 0.66665345, 1.0],
+ [14.185366, 0.72296506, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.832553, 0.7476512, 1.0],
+ [6.631264, 0.64727205, 1.0],
+ [9.165473, 0.66433716, 1.0],
+ [12.874936, 0.71633804, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.3384695, 0.7561362, 1.0],
+ [5.5324025, 0.6862774, 1.0],
+ [6.1772375, 0.7093537, 1.0],
+ [5.11874, 0.82211673, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.3927083, 0.68014836, 1.0],
+ [5.3236537, 0.617722, 1.0],
+ [5.153285, 0.7291801, 1.0],
+ [6.883996, 0.7625799, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.9320729, 0.6744753, 1.0],
+ [4.8258367, 0.61940265, 1.0],
+ [5.4672303, 0.7232466, 1.0],
+ [8.657131, 0.7020352, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.7061602, 0.8554576, 1.0],
+ [1.1815847, 0.8894174, 1.0],
+ [1.0903149, 0.93655825, 1.0],
+ [2.8275063, 0.8868961, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.90848404, 0.85196733, 1.0],
+ [0.4342817, 0.9318938, 1.0],
+ [0.04572374, 0.9942397, 1.0],
+ [0.0133670345, 0.9983886, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.4749884, 0.8968156, 1.0],
+ [0.13983242, 0.95840764, 1.0],
+ [0.012114237, 0.9945305, 1.0],
+ [0.004142619, 0.9975343, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.2421307, 0.94426733, 1.0],
+ [0.025308255, 0.97187287, 1.0],
+ [-0.013660883, 0.9893051, 1.0],
+ [-0.014684059, 0.9894599, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.15639894, 0.9373672, 1.0],
+ [-0.3664362, 0.8935984, 1.0],
+ [-0.15253535, 0.9533504, 1.0],
+ [-0.039207056, 0.98815924, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.43275413, 0.8943086, 1.0],
+ [-0.70259815, 0.840497, 1.0],
+ [-0.252648, 0.937427, 1.0],
+ [-0.03330618, 0.9936832, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.06659569, 0.87807643, 1.0],
+ [-0.1700009, 0.8497151, 1.0],
+ [-0.12636127, 0.963959, 1.0],
+ [-0.013172978, 0.9975136, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.64482135, 0.87148684, 1.0],
+ [-0.74943525, 0.83117217, 1.0],
+ [-0.27330378, 0.93200976, 1.0],
+ [-0.021869717, 0.9946897, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.0401838, 0.8728109, 1.0],
+ [-0.73823136, 0.8744596, 1.0],
+ [-0.18287456, 0.95961267, 1.0],
+ [-0.009951859, 0.9977242, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.22627981, 0.88137406, 1.0],
+ [0.007839352, 0.9245987, 1.0],
+ [0.0014544588, 0.99349904, 1.0],
+ [-2.9291497e-05, 0.99989325, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.18447767, 0.8809199, 1.0],
+ [0.008496483, 0.9478462, 1.0],
+ [0.0067973994, 0.9979893, 1.0],
+ [5.0736588e-05, 0.9999927, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.29172453, 0.92011845, 1.0],
+ [0.15676522, 0.9734236, 1.0],
+ [0.004252948, 0.99931306, 1.0],
+ [-1.0232604e-05, 0.9999954, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.17869076, 0.9557188, 1.0],
+ [0.08755076, 0.99127334, 1.0],
+ [0.0010010353, 0.99973154, 1.0],
+ [-5.539131e-05, 0.9999892, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.22946571, 0.9652117, 1.0],
+ [0.05355996, 0.9938835, 1.0],
+ [0.0004149077, 0.9998538, 1.0],
+ [8.350738e-06, 0.9999956, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.037329286, 0.9731236, 1.0],
+ [0.006478223, 0.9956182, 1.0],
+ [0.00065915973, 0.9999197, 1.0],
+ [2.1461079e-05, 0.9999988, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.038507767, 0.98602444, 1.0],
+ [0.009278379, 0.9980656, 1.0],
+ [0.00043359466, 0.9999601, 1.0],
+ [1.2074154e-05, 0.99999934, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.024321932, 0.99058217, 1.0],
+ [0.0005276345, 0.99881136, 1.0],
+ [-0.000108261214, 0.99998933, 1.0],
+ [-2.0593393e-06, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.08471028, 0.98001426, 1.0],
+ [0.007567339, 0.9977277, 1.0],
+ [-0.000107173975, 0.9999823, 1.0],
+ [-1.4792153e-06, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.21450363, 0.97752666, 1.0],
+ [0.027476575, 0.9972575, 1.0],
+ [1.0573019e-05, 0.99997705, 1.0],
+ [-2.127626e-06, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.13710256, 0.98749036, 1.0],
+ [0.021214448, 0.9985921, 1.0],
+ [0.00022974156, 0.99998766, 1.0],
+ [6.995456e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.034353305, 0.9869362, 1.0],
+ [-0.0033373374, 0.99891263, 1.0],
+ [-0.0008769339, 0.99998987, 1.0],
+ [-6.4116425e-06, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.029380038, 0.9895625, 1.0],
+ [-0.010520089, 0.99891555, 1.0],
+ [-0.0019082854, 0.9999861, 1.0],
+ [-1.3866463e-05, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.085345775, 0.9883013, 1.0],
+ [-0.0024072328, 0.99874765, 1.0],
+ [-0.001102076, 0.99999017, 1.0],
+ [-6.650834e-06, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.09333435, 0.9841484, 1.0],
+ [0.007872991, 0.99843925, 1.0],
+ [-0.00031172566, 0.9999886, 1.0],
+ [-2.2500953e-06, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.1495411, 0.9844692, 1.0],
+ [-0.009347158, 0.9987978, 1.0],
+ [-4.104763e-05, 0.9999937, 1.0],
+ [-4.1019636e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.13588922, 0.99047935, 1.0],
+ [-0.011877171, 0.9992225, 1.0],
+ [-0.00016244329, 0.99999523, 1.0],
+ [-4.7618443e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.18492526, 0.98800945, 1.0],
+ [-0.01919961, 0.9989189, 1.0],
+ [-0.00018170738, 0.9999931, 1.0],
+ [-2.8776438e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.5042778, 0.9758248, 1.0],
+ [-0.05973959, 0.99744993, 1.0],
+ [-0.00048369938, 0.99998105, 1.0],
+ [-3.4633175e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.1252605, 0.9543784, 1.0],
+ [-0.18540663, 0.99284697, 1.0],
+ [-0.0028871074, 0.99987376, 1.0],
+ [-9.758851e-06, 0.99999946, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.6847491, 0.9297688, 1.0],
+ [-0.30172828, 0.9871863, 1.0],
+ [-0.006863096, 0.9996188, 1.0],
+ [-3.727347e-05, 0.99999744, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.370997, 0.89369446, 1.0],
+ [-1.2190995, 0.9472215, 1.0],
+ [-0.12854093, 0.99495983, 1.0],
+ [-0.0013108054, 0.999956, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.9047966, 0.7847389, 1.0],
+ [-5.3030334, 0.8162339, 1.0],
+ [-1.4172684, 0.9577376, 1.0],
+ [-0.026107062, 0.999312, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.9655056, 0.73402, 1.0],
+ [-5.2766113, 0.82234603, 1.0],
+ [-1.8166275, 0.95730686, 1.0],
+ [-0.073980905, 0.99863815, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.649344, 0.66660523, 1.0],
+ [-6.673369, 0.77574635, 1.0],
+ [-3.8847363, 0.9271979, 1.0],
+ [-0.35958254, 0.9939365, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.770011, 0.6152831, 1.0],
+ [-8.663781, 0.6730373, 1.0],
+ [-4.5796795, 0.8964258, 1.0],
+ [-0.38371933, 0.9933861, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.064724, 0.563459, 1.0],
+ [-8.883273, 0.5998541, 1.0],
+ [-3.3382602, 0.8610413, 1.0],
+ [-0.06820223, 0.9978487, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.522412, 0.5443333, 1.0],
+ [-10.088787, 0.5584405, 1.0],
+ [-8.74115, 0.69652474, 1.0],
+ [-0.46212718, 0.9844496, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.3416877, 0.5573365, 1.0],
+ [-10.180397, 0.54992926, 1.0],
+ [-10.34654, 0.65771925, 1.0],
+ [-2.239636, 0.95445883, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.17010239, 0.6200763, 1.0],
+ [-3.7829561, 0.64616305, 1.0],
+ [-7.5884185, 0.8539007, 1.0],
+ [-7.036888, 0.9037384, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.296787, 0.8043461, 1.0],
+ [1.2797425, 0.88472384, 1.0],
+ [-2.9147022, 0.94889057, 1.0],
+ [-2.639398, 0.96286654, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1746049, 0.87596625, 1.0],
+ [2.4819775, 0.8980377, 1.0],
+ [0.43790334, 0.9830677, 1.0],
+ [-0.006361744, 0.9996459, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4755538, 0.92148954, 1.0],
+ [3.057467, 0.9028049, 1.0],
+ [0.5373922, 0.9829736, 1.0],
+ [0.005417032, 0.99982744, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.19876552, 0.97760195, 1.0],
+ [0.26070496, 0.99011695, 1.0],
+ [0.033617545, 0.9989797, 1.0],
+ [0.0002978694, 0.99999154, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.07401737, 0.9809595, 1.0],
+ [0.0669684, 0.9947014, 1.0],
+ [0.0036516946, 0.99973106, 1.0],
+ [1.5824207e-05, 0.99999887, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.24908549, 0.9819961, 1.0],
+ [-0.027645566, 0.9976866, 1.0],
+ [-0.00042068172, 0.9999474, 1.0],
+ [1.7829321e-06, 0.9999998, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.45939854, 0.97829735, 1.0],
+ [-0.06557831, 0.99736834, 1.0],
+ [-0.0014225548, 0.99993753, 1.0],
+ [7.041205e-07, 0.99999976, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.97001255, 0.94978815, 1.0],
+ [-0.06016365, 0.9956392, 1.0],
+ [0.0005834204, 0.9998938, 1.0],
+ [1.0331767e-05, 0.99999905, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.8007346, 0.9450692, 1.0],
+ [0.061064422, 0.9958661, 1.0],
+ [0.0004179476, 0.99996316, 1.0],
+ [-5.38278e-07, 0.99999976, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7554829, 0.97007614, 1.0],
+ [0.038412966, 0.99764246, 1.0],
+ [-1.2250363e-05, 0.99998575, 1.0],
+ [-4.3817803e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.4214841, 0.98421025, 1.0],
+ [0.018357918, 0.9986999, 1.0],
+ [-0.0001539443, 0.99998885, 1.0],
+ [-2.1144313e-06, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.26302785, 0.9877291, 1.0],
+ [0.016418263, 0.99897736, 1.0],
+ [0.00017517614, 0.999994, 1.0],
+ [3.7461887e-06, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.2549705, 0.9871261, 1.0],
+ [0.019887371, 0.99840975, 1.0],
+ [-3.7766345e-05, 0.99995524, 1.0],
+ [-7.270728e-06, 0.9999991, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.57600856, 0.9830809, 1.0],
+ [0.028567612, 0.99657667, 1.0],
+ [-0.00476558, 0.99963075, 1.0],
+ [-0.00016184518, 0.9999903, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.023581166, 0.9658987, 1.0],
+ [-0.12978074, 0.9894591, 1.0],
+ [-0.010897471, 0.999496, 1.0],
+ [-0.0003179809, 0.9999912, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.48003206, 0.947442, 1.0],
+ [-0.28566536, 0.98114216, 1.0],
+ [-0.02372709, 0.99919075, 1.0],
+ [-0.0012802386, 0.9999792, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.30012533, 0.9344908, 1.0],
+ [-0.415363, 0.9781973, 1.0],
+ [-0.04023375, 0.99892974, 1.0],
+ [-0.002275092, 0.9999678, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.2335646, 0.91651773, 1.0],
+ [-0.35089287, 0.9740984, 1.0],
+ [-0.03576802, 0.9988748, 1.0],
+ [-0.0019647575, 0.99997276, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.4362745, 0.8536556, 1.0],
+ [-0.5057072, 0.96044815, 1.0],
+ [-0.05471109, 0.99809283, 1.0],
+ [-0.00035249075, 0.99999046, 1.0],
+ [0.0, 1.0, 1.0],
+ [12.081045, 0.7132523, 1.0],
+ [0.5422661, 0.9341002, 1.0],
+ [-0.06538023, 0.99676913, 1.0],
+ [-0.00047848828, 0.9999808, 1.0],
+ [0.0, 1.0, 1.0],
+ [17.529392, 0.60886335, 1.0],
+ [1.5537803, 0.8626087, 1.0],
+ [-0.3528088, 0.98406494, 1.0],
+ [-0.0052189324, 0.99973387, 1.0],
+ [0.0, 1.0, 1.0],
+ [18.210768, 0.5879146, 1.0],
+ [1.4481329, 0.77672154, 1.0],
+ [-0.9064065, 0.9183754, 1.0],
+ [-0.024934258, 0.99743956, 1.0]
+ ],
+ "srgb_value": true
+ }
+}
diff --git a/assets/camera-profiles/NIKON_D5300.json b/assets/camera-profiles/NIKON_D5300.json
new file mode 100644
index 000000000..5ab1f062e
--- /dev/null
+++ b/assets/camera-profiles/NIKON_D5300.json
@@ -0,0 +1,1819 @@
+{
+ "version": 1,
+ "model": "NIKON D5300",
+ "files": 264,
+ "samples": 964687,
+ "matrix": [
+ [1.1916187747366802, -0.24231291824981582, 0.06983947565237258],
+ [-0.07690142035770187, 1.1730235959883188, -0.10538788116290476],
+ [0.03036949208278017, -0.9047958593485248, 1.8955506437912675]
+ ],
+ "hue_sat": {
+ "hue_divisions": 72,
+ "sat_divisions": 5,
+ "val_divisions": 5,
+ "data": [
+ [0.0, 1.0, 1.0],
+ [-0.27515984, 1.1516873, 1.0],
+ [1.6639282, 1.1351492, 1.0],
+ [3.788241, 1.0706222, 1.0],
+ [2.7755148, 1.0334021, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.14424162, 1.1334472, 1.0],
+ [1.3097183, 1.1570547, 1.0],
+ [2.2948222, 1.0653765, 1.0],
+ [2.48696, 1.0126696, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.39059362, 1.108461, 1.0],
+ [0.91269153, 1.1615747, 1.0],
+ [1.2193394, 1.1302495, 1.0],
+ [1.564095, 1.0433257, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.37582052, 1.0874242, 1.0],
+ [0.37180975, 1.12975, 1.0],
+ [0.08854759, 1.119602, 1.0],
+ [0.12284424, 1.0544983, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.41708267, 1.0888891, 1.0],
+ [0.06426376, 1.1006688, 1.0],
+ [-0.8100427, 1.0653179, 1.0],
+ [-1.3206435, 1.0191233, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.13243157, 1.080272, 1.0],
+ [-0.96863246, 1.0961237, 1.0],
+ [-2.4892373, 1.0579139, 1.0],
+ [-3.6368277, 1.0045699, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.5086707, 1.0768719, 1.0],
+ [-2.4455884, 1.090032, 1.0],
+ [-4.392341, 1.0430846, 1.0],
+ [-5.70884, 0.984795, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.9826923, 1.0976027, 1.0],
+ [-3.1522026, 1.0984454, 1.0],
+ [-5.0928793, 1.0361301, 1.0],
+ [-6.190662, 0.9671035, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.83517295, 1.1328652, 1.0],
+ [-2.3040426, 1.1309464, 1.0],
+ [-3.782865, 1.0633757, 1.0],
+ [-5.345632, 0.9701844, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.5699928, 1.1778497, 1.0],
+ [-0.5092877, 1.1737088, 1.0],
+ [-1.6260748, 1.0975113, 1.0],
+ [-3.6590147, 0.97431755, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.965812, 1.2052466, 1.0],
+ [1.2205318, 1.1937522, 1.0],
+ [0.32865468, 1.1070911, 1.0],
+ [-0.5527478, 0.9777932, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.1100607, 1.198459, 1.0],
+ [2.1056213, 1.1429148, 1.0],
+ [1.3356481, 1.0316085, 1.0],
+ [0.5096412, 0.93477476, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4636977, 1.2118869, 1.0],
+ [1.7222096, 1.1241792, 1.0],
+ [0.98221314, 1.0024475, 1.0],
+ [0.21372011, 0.9131384, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.5342778, 1.1797847, 1.0],
+ [0.957812, 1.0956764, 1.0],
+ [0.90817916, 1.0003859, 1.0],
+ [0.27506226, 0.91949815, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.18069139, 1.1459695, 1.0],
+ [1.0440427, 1.0779425, 1.0],
+ [1.4630631, 0.9941303, 1.0],
+ [1.2396606, 0.87395155, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.18410955, 1.0979248, 1.0],
+ [1.3498847, 1.0526143, 1.0],
+ [2.2518015, 0.97271824, 1.0],
+ [2.5790663, 0.85005623, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.86806947, 1.0538092, 1.0],
+ [2.255343, 1.0105029, 1.0],
+ [3.711173, 0.92738235, 1.0],
+ [4.5281615, 0.85241896, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1754395, 1.0296917, 1.0],
+ [2.5134487, 0.98583174, 1.0],
+ [3.7772336, 0.90998256, 1.0],
+ [4.45152, 0.85479385, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1445321, 1.0435532, 1.0],
+ [2.0396032, 0.9995336, 1.0],
+ [3.0725362, 0.9361685, 1.0],
+ [3.631579, 0.8885794, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7827189, 1.0589092, 1.0],
+ [1.0505862, 1.0227683, 1.0],
+ [1.7957299, 0.98822904, 1.0],
+ [1.967721, 0.9724715, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.29322907, 1.06687, 1.0],
+ [-0.08487194, 1.0363688, 1.0],
+ [0.55795294, 1.0033514, 1.0],
+ [0.38443246, 0.9958227, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.20798732, 1.0558138, 1.0],
+ [-0.22798188, 1.0087736, 1.0],
+ [-0.27102852, 0.97902, 1.0],
+ [-0.030924844, 0.9963477, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.6630197, 1.0661411, 1.0],
+ [-1.1126446, 1.0020812, 1.0],
+ [-0.8216712, 0.96949637, 1.0],
+ [-0.05875481, 0.99697804, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.306994, 1.0804613, 1.0],
+ [-1.9421868, 1.0075339, 1.0],
+ [-0.4435876, 0.9711658, 1.0],
+ [-0.009687971, 0.9980309, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.1269542, 1.0775944, 1.0],
+ [-1.3976283, 1.0217266, 1.0],
+ [-0.24913609, 0.98865783, 1.0],
+ [-0.0031247279, 0.9994072, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.375694, 1.0512048, 1.0],
+ [-0.75242716, 1.0082058, 1.0],
+ [-0.06595289, 0.99760807, 1.0],
+ [-0.0031345324, 0.99984926, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7031127, 1.0458598, 1.0],
+ [-1.1684496, 1.0118368, 1.0],
+ [-0.1479137, 0.9961412, 1.0],
+ [-0.0055244397, 0.9997797, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7284406, 1.0431077, 1.0],
+ [-0.73593587, 1.002617, 1.0],
+ [0.02080173, 0.9911072, 1.0],
+ [0.00041606076, 0.9996808, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.2268276, 1.0388371, 1.0],
+ [-1.0188804, 1.0028651, 1.0],
+ [-0.10369495, 0.9951023, 1.0],
+ [-0.00074021873, 0.99985033, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.4187577, 1.0261325, 1.0],
+ [-1.5957482, 0.9980721, 1.0],
+ [-0.1547406, 0.99747145, 1.0],
+ [0.006172743, 0.99982816, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.17795917, 1.0130396, 1.0],
+ [-0.6837107, 0.98732287, 1.0],
+ [-0.06094904, 0.9973522, 1.0],
+ [0.004337359, 0.99988484, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.0028615, 1.0350925, 1.0],
+ [0.68680644, 1.000726, 1.0],
+ [0.019622738, 0.99780047, 1.0],
+ [0.0016065001, 0.999882, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.0603294, 1.019478, 1.0],
+ [0.38180757, 0.983154, 1.0],
+ [-0.23340763, 0.98130405, 1.0],
+ [-0.0110910535, 0.9989576, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.5435147, 1.0271243, 1.0],
+ [0.99150276, 0.989704, 1.0],
+ [-0.28383088, 0.98094606, 1.0],
+ [-0.020072438, 0.9989133, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.08294, 1.0426484, 1.0],
+ [1.818317, 1.0104465, 1.0],
+ [0.061887734, 0.99672973, 1.0],
+ [-0.003478195, 0.99980235, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.420556, 1.0468477, 1.0],
+ [1.3910332, 0.9856526, 1.0],
+ [0.014045477, 0.992967, 1.0],
+ [-0.0012650526, 0.99988115, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.0608708, 1.0270871, 1.0],
+ [-0.91338277, 0.92517686, 1.0],
+ [-0.4462344, 0.9653749, 1.0],
+ [-0.00868847, 0.99921674, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.41799447, 1.0205152, 1.0],
+ [-1.6170602, 0.9293957, 1.0],
+ [-0.6444141, 0.96566117, 1.0],
+ [-0.013291724, 0.99908036, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.30684704, 1.0309951, 1.0],
+ [-0.32407844, 0.9751206, 1.0],
+ [-0.2714902, 0.9828883, 1.0],
+ [-0.00732241, 0.9995556, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.23834689, 1.0267385, 1.0],
+ [0.17927068, 0.98710644, 1.0],
+ [0.18505995, 0.988422, 1.0],
+ [0.007564359, 0.99952435, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.9417192, 1.0186211, 1.0],
+ [0.4331768, 0.98655975, 1.0],
+ [0.5120104, 0.973346, 1.0],
+ [0.046577834, 0.9975678, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.37069434, 1.015258, 1.0],
+ [-0.33970982, 1.0002911, 1.0],
+ [0.21809609, 0.9738671, 1.0],
+ [0.05885403, 0.996412, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.3410674, 1.0226996, 1.0],
+ [-0.46905938, 1.0094384, 1.0],
+ [1.1913799, 0.9839222, 1.0],
+ [0.27729467, 0.9954679, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.8399196, 1.0310917, 1.0],
+ [1.3589114, 1.0043445, 1.0],
+ [3.2229822, 0.9866625, 1.0],
+ [0.79395044, 0.9953453, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.6512873, 1.0852343, 1.0],
+ [3.6366863, 1.0575762, 1.0],
+ [4.409545, 1.0124124, 1.0],
+ [1.2255954, 0.9980612, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.5640118, 1.103434, 1.0],
+ [3.857803, 1.0989252, 1.0],
+ [5.013495, 1.0628691, 1.0],
+ [2.1981053, 1.0128543, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.750485, 1.1216393, 1.0],
+ [4.4771504, 1.1242076, 1.0],
+ [5.554026, 1.098979, 1.0],
+ [4.0766697, 1.0487705, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.716854, 1.1420878, 1.0],
+ [3.9434807, 1.149656, 1.0],
+ [4.698269, 1.1275396, 1.0],
+ [3.355159, 1.0637993, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.1019642, 1.152162, 1.0],
+ [3.018406, 1.1701936, 1.0],
+ [3.5190942, 1.1440179, 1.0],
+ [1.6282337, 1.0440116, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.0533535, 1.1430386, 1.0],
+ [2.6682806, 1.1609282, 1.0],
+ [1.6297065, 1.1209588, 1.0],
+ [0.3274113, 1.0186641, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.7143104, 1.1457591, 1.0],
+ [2.010889, 1.1753439, 1.0],
+ [1.1698594, 1.1809243, 1.0],
+ [0.26954132, 1.0447038, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.9450912, 1.1625692, 1.0],
+ [1.4925617, 1.1576607, 1.0],
+ [0.73646855, 1.1099164, 1.0],
+ [0.041240156, 1.0114015, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4923553, 1.1628035, 1.0],
+ [0.9318059, 1.1155237, 1.0],
+ [-0.08695841, 1.0332942, 1.0],
+ [-0.048176043, 1.0041637, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.084317, 1.1418072, 1.0],
+ [0.077919334, 1.1205045, 1.0],
+ [-0.37862262, 1.0519394, 1.0],
+ [-0.020748373, 1.0024971, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.70962113, 1.1346613, 1.0],
+ [0.040982615, 1.1304044, 1.0],
+ [-0.59360516, 1.0895824, 1.0],
+ [-0.044442892, 1.0063348, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.044502545, 1.1331831, 1.0],
+ [-0.31845695, 1.1328877, 1.0],
+ [-0.70814407, 1.0888221, 1.0],
+ [-0.053025763, 1.0063571, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.102819525, 1.0983768, 1.0],
+ [-0.41320392, 1.0724556, 1.0],
+ [-0.34162834, 1.0306761, 1.0],
+ [-0.015480887, 1.0013198, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.45333293, 1.1152055, 1.0],
+ [-0.36607388, 1.0825105, 1.0],
+ [-0.061023094, 1.0260044, 1.0],
+ [-0.00076291023, 1.0008935, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.5181267, 1.1252426, 1.0],
+ [-0.7772274, 1.0954831, 1.0],
+ [0.07648249, 1.0305958, 1.0],
+ [0.007827844, 1.0011276, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.7173598, 1.1436788, 1.0],
+ [-1.1496428, 1.0798888, 1.0],
+ [0.13708575, 1.0121282, 1.0],
+ [0.0056166523, 1.0002311, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.8031726, 1.1485914, 1.0],
+ [-1.1996536, 1.065685, 1.0],
+ [-0.12146866, 1.0105418, 1.0],
+ [-0.001331464, 1.000203, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.7381613, 1.1670295, 1.0],
+ [-2.4761944, 1.0762824, 1.0],
+ [-0.30495355, 1.010074, 1.0],
+ [-0.003095148, 1.0001698, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.7574484, 1.1367188, 1.0],
+ [-1.0683595, 1.0591327, 1.0],
+ [0.3032825, 1.0109986, 1.0],
+ [0.0240107, 1.0003924, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.4444375, 1.1478848, 1.0],
+ [-2.10396, 1.077343, 1.0],
+ [0.39878115, 1.0108056, 1.0],
+ [0.04858238, 1.0006601, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.6460211, 1.1262141, 1.0],
+ [-0.94227576, 1.0764561, 1.0],
+ [0.46847248, 1.0121009, 1.0],
+ [0.056671463, 1.0005428, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.941849, 1.1401461, 1.0],
+ [-1.4121957, 1.0967879, 1.0],
+ [0.3250404, 1.0172433, 1.0],
+ [0.05624255, 1.0007424, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.3003352, 1.1373972, 1.0],
+ [-1.2239021, 1.0964172, 1.0],
+ [0.4203376, 1.0171689, 1.0],
+ [0.097768836, 1.0010349, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3678734, 1.1021602, 1.0],
+ [0.18997674, 1.0791068, 1.0],
+ [1.9053582, 1.0272077, 1.0],
+ [0.36325264, 1.0027457, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.888127, 1.1030134, 1.0],
+ [0.9222494, 1.0793839, 1.0],
+ [4.407062, 1.0317812, 1.0],
+ [1.2517463, 1.0053726, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.87627596, 1.1100941, 1.0],
+ [2.1658633, 1.0810852, 1.0],
+ [6.2091784, 1.0344337, 1.0],
+ [2.7591643, 1.0083429, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.58831537, 1.1441597, 1.0],
+ [3.26025, 1.1017427, 1.0],
+ [5.9591794, 1.0396717, 1.0],
+ [2.5558283, 1.009373, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.6157835, 1.1522069, 1.0],
+ [2.7376304, 1.1033771, 1.0],
+ [5.0446105, 1.0609956, 1.0],
+ [2.545954, 1.0267637, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.567323, 1.050905, 1.0],
+ [3.219447, 1.086401, 1.0],
+ [5.309476, 1.0623287, 1.0],
+ [5.3261275, 1.0507418, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.4884005, 1.039593, 1.0],
+ [2.2780461, 1.0956311, 1.0],
+ [3.2725804, 1.0421693, 1.0],
+ [3.57421, 1.0101761, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.709007, 1.016824, 1.0],
+ [1.9036032, 1.1006229, 1.0],
+ [1.8250041, 1.09863, 1.0],
+ [2.2753541, 1.0252498, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.8476596, 1.0087867, 1.0],
+ [1.692206, 1.0733916, 1.0],
+ [0.98378277, 1.0820075, 1.0],
+ [0.71567744, 1.0277224, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.7584476, 1.0095731, 1.0],
+ [1.4141955, 1.0379248, 1.0],
+ [0.30707088, 1.0278721, 1.0],
+ [-0.48991957, 0.99570155, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.3251288, 1.0099978, 1.0],
+ [0.40751106, 1.034089, 1.0],
+ [-1.266682, 1.0152951, 1.0],
+ [-2.8792267, 0.97016555, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.676751, 1.0196159, 1.0],
+ [-1.0043784, 1.0388186, 1.0],
+ [-3.3858006, 1.0105125, 1.0],
+ [-5.2836957, 0.95619494, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4230912, 1.0371325, 1.0],
+ [-1.6468483, 1.0507761, 1.0],
+ [-4.205406, 1.0050639, 1.0],
+ [-5.696899, 0.9382988, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.215277, 1.0757174, 1.0],
+ [-0.13039589, 1.0800881, 1.0],
+ [-2.5378978, 1.0185709, 1.0],
+ [-4.5910883, 0.941271, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.7552316, 1.1195593, 1.0],
+ [1.9935799, 1.1178633, 1.0],
+ [-0.1950342, 1.0336235, 1.0],
+ [-2.9394403, 0.9406914, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.6019835, 1.1451467, 1.0],
+ [3.4129214, 1.1365347, 1.0],
+ [2.2300456, 1.0304257, 1.0],
+ [0.6699211, 0.8971019, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.556115, 1.1341692, 1.0],
+ [3.8200648, 1.0857233, 1.0],
+ [3.1856856, 0.96298456, 1.0],
+ [2.2841954, 0.8578174, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.4975002, 1.1417533, 1.0],
+ [2.8865607, 1.0605808, 1.0],
+ [2.4713562, 0.93972397, 1.0],
+ [1.982328, 0.83509517, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.0893435, 1.1080577, 1.0],
+ [1.7293227, 1.0274619, 1.0],
+ [1.6507962, 0.9354451, 1.0],
+ [1.3905607, 0.8366557, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.6415445, 1.0806029, 1.0],
+ [1.506959, 1.0187534, 1.0],
+ [1.6388204, 0.9282385, 1.0],
+ [1.8973097, 0.82975906, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.7991877, 1.0542749, 1.0],
+ [2.0936208, 1.0012176, 1.0],
+ [2.6603212, 0.9100169, 1.0],
+ [3.5249443, 0.80724543, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.1576753, 1.0242636, 1.0],
+ [2.7907417, 0.9638929, 1.0],
+ [3.652071, 0.87338793, 1.0],
+ [4.5204115, 0.78994405, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.1243021, 1.008548, 1.0],
+ [2.7552125, 0.9425203, 1.0],
+ [3.522211, 0.8644462, 1.0],
+ [4.1264706, 0.8124369, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.8834304, 1.0225105, 1.0],
+ [2.3353438, 0.9708129, 1.0],
+ [3.0901859, 0.9099647, 1.0],
+ [3.7211735, 0.86348975, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4581822, 1.0330464, 1.0],
+ [1.5594033, 0.99513304, 1.0],
+ [2.0912547, 0.96644175, 1.0],
+ [2.5819116, 0.94535977, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.60412645, 1.0405735, 1.0],
+ [0.5539686, 1.0038283, 1.0],
+ [0.8588483, 0.9796771, 1.0],
+ [0.7651864, 0.98082924, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.19108707, 1.0352545, 1.0],
+ [-0.16049986, 0.99251735, 1.0],
+ [-0.5080386, 0.96194357, 1.0],
+ [-0.33787724, 0.98708415, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.24877733, 1.0316597, 1.0],
+ [-0.70576507, 0.97597045, 1.0],
+ [-0.8016234, 0.9442628, 1.0],
+ [-0.11087948, 0.9908073, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.8790754, 1.0514538, 1.0],
+ [-1.4108989, 0.9916971, 1.0],
+ [-1.0544907, 0.9481936, 1.0],
+ [-0.10507189, 0.99393654, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3867714, 1.0623717, 1.0],
+ [-2.1656637, 1.0158321, 1.0],
+ [-1.1665387, 0.9772894, 1.0],
+ [-0.049957793, 0.99827534, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.4962778, 1.047387, 1.0],
+ [-3.1633992, 1.0073104, 1.0],
+ [-1.4553053, 0.95946413, 1.0],
+ [-0.13852488, 0.99207044, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.1582537, 1.039661, 1.0],
+ [-3.3594494, 0.99911463, 1.0],
+ [-1.4694877, 0.9412946, 1.0],
+ [-0.15061137, 0.9906891, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.7993233, 1.040663, 1.0],
+ [-3.003692, 0.9947964, 1.0],
+ [-0.8132878, 0.94909513, 1.0],
+ [-0.09164102, 0.99424165, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.1300888, 1.0289713, 1.0],
+ [-2.916435, 0.98529565, 1.0],
+ [-0.3949024, 0.9686243, 1.0],
+ [0.057513624, 0.99605733, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.870443, 1.0128809, 1.0],
+ [-3.22342, 0.97333175, 1.0],
+ [1.0390259, 0.9529574, 1.0],
+ [0.3107253, 0.99131966, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9573697, 1.0006245, 1.0],
+ [-1.8365934, 0.9674953, 1.0],
+ [0.7572013, 0.9682651, 1.0],
+ [0.17691553, 0.9942757, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.28490174, 1.0081447, 1.0],
+ [-0.29309836, 0.97951233, 1.0],
+ [0.18393767, 0.98060596, 1.0],
+ [0.007778787, 0.997274, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.2726132, 0.99976957, 1.0],
+ [-0.4088234, 0.95960116, 1.0],
+ [0.0077163163, 0.9512176, 1.0],
+ [0.019507347, 0.9959901, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7798813, 0.9985067, 1.0],
+ [0.76253676, 0.9651048, 1.0],
+ [-0.1965283, 0.95622796, 1.0],
+ [-0.024494128, 0.99734217, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.2395806, 1.0156081, 1.0],
+ [1.9043974, 0.98719865, 1.0],
+ [0.2547382, 0.9865172, 1.0],
+ [-0.0017040195, 0.99939626, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.3556528, 1.0269231, 1.0],
+ [1.6541926, 0.94798446, 1.0],
+ [0.19142385, 0.96310043, 1.0],
+ [0.00077345694, 0.99907255, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.8628551, 1.0025055, 1.0],
+ [-0.8925193, 0.86057365, 1.0],
+ [-0.89507776, 0.8785938, 1.0],
+ [-0.02676659, 0.9942501, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.69604295, 0.9841022, 1.0],
+ [-1.5614027, 0.86573595, 1.0],
+ [-1.0682615, 0.8759204, 1.0],
+ [-0.053552028, 0.992165, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3843973, 0.9732926, 1.0],
+ [-1.6747307, 0.9024153, 1.0],
+ [-1.1190381, 0.90966296, 1.0],
+ [-0.0430858, 0.995535, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.4726397, 0.9728779, 1.0],
+ [-0.48264512, 0.9410372, 1.0],
+ [0.31483912, 0.93758917, 1.0],
+ [0.029700607, 0.99565494, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.5699832, 0.9680764, 1.0],
+ [-0.5216335, 0.9524717, 1.0],
+ [1.1201954, 0.9122498, 1.0],
+ [0.2576845, 0.9783837, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9922228, 0.9569479, 1.0],
+ [-1.1839337, 0.9620515, 1.0],
+ [0.49700478, 0.90824383, 1.0],
+ [0.2757639, 0.9656649, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.8267179, 0.97236925, 1.0],
+ [-1.3503194, 0.98332787, 1.0],
+ [-0.21534178, 0.9398797, 1.0],
+ [0.3592183, 0.9531863, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3845943, 0.9576693, 1.0],
+ [-0.88324434, 0.95311546, 1.0],
+ [1.0021961, 0.92629516, 1.0],
+ [1.4048795, 0.9467104, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.21868584, 0.9868099, 1.0],
+ [1.2409965, 0.98529285, 1.0],
+ [3.478748, 0.96618646, 1.0],
+ [3.547536, 0.9636579, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.72944003, 1.011865, 1.0],
+ [2.2030432, 1.0459692, 1.0],
+ [4.3416624, 1.0307989, 1.0],
+ [4.5565796, 1.0013582, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7054098, 1.0304452, 1.0],
+ [2.7193584, 1.0797284, 1.0],
+ [4.6252937, 1.0782566, 1.0],
+ [5.346714, 1.0554028, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.8206882, 1.0563322, 1.0],
+ [2.66612, 1.105401, 1.0],
+ [4.257176, 1.1014127, 1.0],
+ [5.333192, 1.0896508, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.6858709, 1.0573044, 1.0],
+ [2.1161485, 1.1302112, 1.0],
+ [3.9404676, 1.1377732, 1.0],
+ [4.928983, 1.1188648, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.4658943, 1.0244572, 1.0],
+ [1.4814435, 1.1276039, 1.0],
+ [2.0604527, 1.1611818, 1.0],
+ [1.7115017, 1.0927407, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.66739404, 1.0136315, 1.0],
+ [1.41378, 1.1620725, 1.0],
+ [1.2858282, 1.2084693, 1.0],
+ [0.8847436, 1.1387153, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.22088455, 1.0020697, 1.0],
+ [0.8503165, 1.1110431, 1.0],
+ [0.8370134, 1.1636014, 1.0],
+ [0.16764726, 1.0484313, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.032334913, 0.99697274, 1.0],
+ [-0.12844399, 1.0419574, 1.0],
+ [-0.9318103, 1.0669361, 1.0],
+ [-0.2286045, 1.0165362, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.1669066, 0.99338514, 1.0],
+ [-0.8307293, 1.0627798, 1.0],
+ [-1.2913208, 1.0905485, 1.0],
+ [-0.08585524, 1.0079323, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.017506301, 0.9974502, 1.0],
+ [-0.4714752, 1.0923178, 1.0],
+ [-1.2507538, 1.1427293, 1.0],
+ [-0.15650667, 1.0198009, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.5337322, 0.98601866, 1.0],
+ [0.4784788, 1.0887345, 1.0],
+ [-0.95893174, 1.1386591, 1.0],
+ [-0.15236506, 1.0194373, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.78718185, 0.9737713, 1.0],
+ [0.8975688, 1.024816, 1.0],
+ [-0.2945547, 1.0546055, 1.0],
+ [-0.03156037, 1.0033555, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.24685815, 0.98656416, 1.0],
+ [0.55160105, 1.0425034, 1.0],
+ [-0.11143197, 1.0422429, 1.0],
+ [-0.003670774, 1.0018142, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.77873033, 1.0003996, 1.0],
+ [0.19153872, 1.0427284, 1.0],
+ [0.37716967, 1.041646, 1.0],
+ [0.016879126, 1.0023743, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7304914, 1.0388753, 1.0],
+ [-0.7251529, 1.0577725, 1.0],
+ [0.34579277, 1.0164489, 1.0],
+ [0.009596101, 1.0004158, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.6364738, 1.067764, 1.0],
+ [-0.9167525, 1.0778942, 1.0],
+ [0.103218675, 1.0217229, 1.0],
+ [0.0034776113, 1.0004051, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7374398, 1.0553571, 1.0],
+ [-0.8997327, 1.072175, 1.0],
+ [0.018124623, 1.0228853, 1.0],
+ [0.0031013442, 1.0004023, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9461782, 1.0329045, 1.0],
+ [-0.9284798, 1.0640512, 1.0],
+ [0.5232761, 1.0302181, 1.0],
+ [0.0713816, 1.0012505, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7830942, 1.0167092, 1.0],
+ [-1.0461223, 1.0536364, 1.0],
+ [0.71468955, 1.031544, 1.0],
+ [0.1528099, 1.0023446, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.901455, 1.0192658, 1.0],
+ [0.3789207, 1.0608308, 1.0],
+ [1.4712181, 1.0287849, 1.0],
+ [0.17150511, 1.0013422, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.4228317, 1.0136043, 1.0],
+ [-0.4306419, 1.0521969, 1.0],
+ [0.79203665, 1.0235791, 1.0],
+ [0.17930758, 1.0008605, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.89692557, 1.0078237, 1.0],
+ [0.22381781, 1.0376579, 1.0],
+ [1.2225298, 1.0229918, 1.0],
+ [0.30199468, 1.0024537, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.96971756, 0.998565, 1.0],
+ [2.4854388, 1.0386328, 1.0],
+ [5.093825, 1.0409368, 1.0],
+ [1.5859087, 1.0088303, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.0574582, 0.99701333, 1.0],
+ [3.9885697, 1.0335468, 1.0],
+ [7.3134866, 1.02023, 1.0],
+ [4.876025, 1.0077791, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.9773273, 1.0016919, 1.0],
+ [4.8199377, 1.0256822, 1.0],
+ [7.568321, 1.0072774, 1.0],
+ [6.5177317, 0.99610305, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.3437927, 1.0140332, 1.0],
+ [4.7947435, 1.036412, 1.0],
+ [7.550965, 1.0280272, 1.0],
+ [6.326854, 1.0115026, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.9055889, 1.0224042, 1.0],
+ [3.8671165, 1.0454612, 1.0],
+ [6.3752146, 1.0593462, 1.0],
+ [5.871097, 1.0545919, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.4711437, 0.89240575, 1.0],
+ [3.8707209, 0.975318, 1.0],
+ [5.54684, 1.0295455, 1.0],
+ [5.4792643, 1.0330564, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.5822687, 0.86561245, 1.0],
+ [3.369101, 0.9498307, 1.0],
+ [4.0309362, 0.99107826, 1.0],
+ [4.1218996, 0.97818476, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.7380633, 0.856774, 1.0],
+ [3.2887201, 0.9503168, 1.0],
+ [2.9449494, 1.0068572, 1.0],
+ [3.3715508, 0.97625136, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.9660296, 0.8707871, 1.0],
+ [3.211986, 0.9548858, 1.0],
+ [1.9611124, 1.0027668, 1.0],
+ [1.4650759, 0.9794454, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.919991, 0.8776082, 1.0],
+ [2.96986, 0.92894495, 1.0],
+ [1.43205, 0.9510039, 1.0],
+ [0.48539585, 0.9417541, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.378387, 0.883634, 1.0],
+ [2.1083007, 0.919261, 1.0],
+ [0.5318521, 0.9173686, 1.0],
+ [-0.7582033, 0.896065, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.8246558, 0.8962875, 1.0],
+ [0.9034894, 0.928397, 1.0],
+ [-1.1259035, 0.92528087, 1.0],
+ [-3.3147051, 0.8988185, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.153484, 0.91500145, 1.0],
+ [0.48695508, 0.940206, 1.0],
+ [-2.1672661, 0.9259778, 1.0],
+ [-3.7963305, 0.88363916, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.7889557, 0.9511915, 1.0],
+ [2.4093935, 0.9637889, 1.0],
+ [-0.28017357, 0.9176229, 1.0],
+ [-1.7246062, 0.86704445, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.9804792, 1.0035709, 1.0],
+ [4.4882298, 0.9964915, 1.0],
+ [2.0359013, 0.9096808, 1.0],
+ [0.44025534, 0.8560936, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.1492724, 1.0114399, 1.0],
+ [5.4943357, 1.0019382, 1.0],
+ [4.05172, 0.8888965, 1.0],
+ [2.2645717, 0.7902095, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.758939, 0.99153984, 1.0],
+ [5.431977, 0.95972574, 1.0],
+ [4.6545696, 0.84344786, 1.0],
+ [3.4948368, 0.77209806, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.388168, 0.986505, 1.0],
+ [4.070344, 0.92283, 1.0],
+ [3.5232341, 0.83252263, 1.0],
+ [3.0174634, 0.7662801, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.7689843, 0.96089333, 1.0],
+ [2.5116751, 0.8785204, 1.0],
+ [2.3376532, 0.8223458, 1.0],
+ [2.414712, 0.7608956, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.2908578, 0.9508312, 1.0],
+ [2.2224586, 0.88289684, 1.0],
+ [2.228365, 0.8166788, 1.0],
+ [2.6414301, 0.7484262, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.3916478, 0.93619686, 1.0],
+ [2.823667, 0.87227386, 1.0],
+ [3.0609424, 0.79408795, 1.0],
+ [3.626718, 0.72579795, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.1597862, 0.9192608, 1.0],
+ [3.008894, 0.85837466, 1.0],
+ [3.3871465, 0.79258764, 1.0],
+ [3.9049165, 0.7342933, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.9172287, 0.9121524, 1.0],
+ [2.6648562, 0.85023427, 1.0],
+ [2.947294, 0.8032925, 1.0],
+ [3.3702457, 0.76370627, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.5077455, 0.9239658, 1.0],
+ [2.4199598, 0.87722534, 1.0],
+ [2.618067, 0.84640473, 1.0],
+ [2.995166, 0.8136889, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.9437534, 0.9129008, 1.0],
+ [1.9712167, 0.87672514, 1.0],
+ [2.1036656, 0.8706977, 1.0],
+ [2.0733418, 0.87078476, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4246463, 0.91300803, 1.0],
+ [1.1752675, 0.8862555, 1.0],
+ [0.9968104, 0.90140474, 1.0],
+ [0.23840506, 0.97830564, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.67706054, 0.9196341, 1.0],
+ [0.109584466, 0.9024811, 1.0],
+ [-0.27386528, 0.9419742, 1.0],
+ [-0.059076753, 0.9978306, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.07224172, 0.93229127, 1.0],
+ [-0.34362468, 0.914439, 1.0],
+ [-0.17629111, 0.96382296, 1.0],
+ [-0.011366743, 0.9988268, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.81656486, 0.955494, 1.0],
+ [-1.0913409, 0.94884336, 1.0],
+ [-0.44080573, 0.982739, 1.0],
+ [-0.030075505, 0.9989954, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3378361, 0.9686959, 1.0],
+ [-1.5182222, 0.96549654, 1.0],
+ [-0.4606443, 0.9918533, 1.0],
+ [-0.019811057, 0.9994661, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.243623, 0.95517683, 1.0],
+ [-2.0646274, 0.93799746, 1.0],
+ [-0.90104544, 0.95197743, 1.0],
+ [-0.10999995, 0.99141395, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.8076265, 0.9409817, 1.0],
+ [-2.058747, 0.9154531, 1.0],
+ [-1.5804225, 0.88738924, 1.0],
+ [-0.23936853, 0.9801497, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.9064386, 0.95459986, 1.0],
+ [-2.3988228, 0.896239, 1.0],
+ [-1.7098186, 0.8685506, 1.0],
+ [-0.25361952, 0.9790636, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.2093053, 0.92354167, 1.0],
+ [-2.2733746, 0.9118433, 1.0],
+ [0.16433056, 0.9224389, 1.0],
+ [0.32686478, 0.98159236, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.749688, 0.9219642, 1.0],
+ [-1.7706362, 0.91556644, 1.0],
+ [1.4995979, 0.9179761, 1.0],
+ [0.58829814, 0.9714674, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.9969661, 0.9133036, 1.0],
+ [-1.1233542, 0.9314691, 1.0],
+ [0.91808987, 0.9250387, 1.0],
+ [0.36256704, 0.96524554, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.4866922, 0.91120887, 1.0],
+ [-0.59065604, 0.9415378, 1.0],
+ [-0.78037834, 0.9368204, 1.0],
+ [-0.3913863, 0.9704481, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.3026705, 0.90438557, 1.0],
+ [-0.8718288, 0.94404954, 1.0],
+ [-0.15336794, 0.977961, 1.0],
+ [-0.09629151, 0.9925991, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9070312, 0.90280366, 1.0],
+ [-0.55553955, 0.935856, 1.0],
+ [0.25413927, 0.988254, 1.0],
+ [0.009861473, 0.9995575, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9049702, 0.9080516, 1.0],
+ [-0.266718, 0.9349882, 1.0],
+ [0.13215524, 0.9933759, 1.0],
+ [0.0041504498, 0.99984777, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.8701549, 0.90498465, 1.0],
+ [1.0092131, 0.8918811, 1.0],
+ [0.3660149, 0.9696158, 1.0],
+ [0.0074352697, 0.9991519, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.0172404, 0.8942851, 1.0],
+ [0.6829563, 0.8349779, 1.0],
+ [0.48213816, 0.91718537, 1.0],
+ [0.016518895, 0.99731606, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7463626, 0.8773616, 1.0],
+ [-0.36728278, 0.80656576, 1.0],
+ [-0.05267751, 0.8861593, 1.0],
+ [-0.0062499205, 0.99655586, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.645922, 0.85724914, 1.0],
+ [-1.6585516, 0.8101157, 1.0],
+ [-0.77995527, 0.8782376, 1.0],
+ [-0.014658492, 0.99659693, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.6578364, 0.8668074, 1.0],
+ [-2.0196826, 0.8535868, 1.0],
+ [-0.37624604, 0.921359, 1.0],
+ [0.00017735251, 0.99736357, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.0841975, 0.8596372, 1.0],
+ [-2.4710217, 0.8740145, 1.0],
+ [-0.1209378, 0.920483, 1.0],
+ [0.045910586, 0.99135536, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.430148, 0.85118824, 1.0],
+ [-2.7032092, 0.8731887, 1.0],
+ [-0.7946189, 0.8594394, 1.0],
+ [-0.09909849, 0.9567725, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.8029838, 0.8495463, 1.0],
+ [-2.6373093, 0.8836435, 1.0],
+ [-1.0274059, 0.83784634, 1.0],
+ [-0.34142062, 0.8863734, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.0252833, 0.8179336, 1.0],
+ [-3.0253406, 0.8344459, 1.0],
+ [-1.3676816, 0.83761764, 1.0],
+ [0.08377362, 0.8903176, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.5889053, 0.8333059, 1.0],
+ [-1.7982966, 0.8581034, 1.0],
+ [-0.067006715, 0.87093455, 1.0],
+ [1.3765932, 0.9137696, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.6394725, 0.8462031, 1.0],
+ [-0.5374585, 0.88883096, 1.0],
+ [1.3434483, 0.91526973, 1.0],
+ [2.196534, 0.948764, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.8830673, 0.91449887, 1.0],
+ [0.3436676, 0.948412, 1.0],
+ [2.4071665, 0.9718761, 1.0],
+ [3.2197835, 0.9905023, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.1540444, 0.9479222, 1.0],
+ [0.77035385, 0.9906655, 1.0],
+ [3.5416546, 1.0201747, 1.0],
+ [4.4329867, 1.036617, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.6539636, 0.8664964, 1.0],
+ [0.31695533, 0.9838804, 1.0],
+ [3.5114965, 1.0515474, 1.0],
+ [4.3682404, 1.0739245, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.889564, 0.7832875, 1.0],
+ [-0.47233972, 0.94827306, 1.0],
+ [1.9158661, 1.0925105, 1.0],
+ [1.7773509, 1.0825669, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.359935, 0.7418968, 1.0],
+ [0.13793384, 0.9809765, 1.0],
+ [1.3180828, 1.1471351, 1.0],
+ [0.7645996, 1.0599663, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.2418065, 0.7301813, 1.0],
+ [0.35360616, 0.91816384, 1.0],
+ [0.9692918, 1.0641508, 1.0],
+ [0.07664488, 1.006114, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.0775743, 0.7516581, 1.0],
+ [-0.124465264, 0.8922322, 1.0],
+ [0.04122935, 0.9999263, 1.0],
+ [-0.016318215, 1.0011346, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3793565, 0.7586109, 1.0],
+ [0.04257845, 0.8988754, 1.0],
+ [-0.082526416, 0.98730445, 1.0],
+ [-0.0054138596, 0.9999747, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.505196, 0.7803437, 1.0],
+ [0.40779415, 0.90821487, 1.0],
+ [-0.1287433, 1.0026283, 1.0],
+ [-0.011499701, 1.0009819, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.058046483, 0.78796697, 1.0],
+ [0.93765765, 0.894442, 1.0],
+ [0.043037504, 1.004575, 1.0],
+ [-0.008396565, 1.0010221, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.043535788, 0.8018015, 1.0],
+ [0.80214524, 0.9042569, 1.0],
+ [0.15616187, 0.9971092, 1.0],
+ [-0.00031897958, 1.0001391, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.13094118, 0.81996274, 1.0],
+ [-0.057957847, 0.9010969, 1.0],
+ [-0.15578985, 0.9838379, 1.0],
+ [-0.0028997594, 0.9998916, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.24140343, 0.8421498, 1.0],
+ [-0.33087662, 0.8944223, 1.0],
+ [-0.11538279, 0.96010375, 1.0],
+ [-0.0023777992, 0.99934727, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.6291469, 0.8663977, 1.0],
+ [-0.93443865, 0.8860551, 1.0],
+ [-0.52833366, 0.9360109, 1.0],
+ [-0.013429848, 0.99847513, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.90612704, 0.8846548, 1.0],
+ [-1.0758501, 0.9156333, 1.0],
+ [-0.31288663, 0.95635, 1.0],
+ [-0.0065308507, 0.9988607, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.8505977, 0.8680036, 1.0],
+ [-0.6228383, 0.9419045, 1.0],
+ [0.008568136, 0.99068564, 1.0],
+ [0.00062457524, 0.9997916, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3649141, 0.8661397, 1.0],
+ [-1.1351817, 0.9570879, 1.0],
+ [-0.4343645, 0.99677765, 1.0],
+ [-0.007229383, 0.9998938, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.3357523, 0.86801755, 1.0],
+ [-0.53919494, 0.9580416, 1.0],
+ [-0.3332547, 1.0017875, 1.0],
+ [0.0020524773, 1.0001712, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.0389299, 0.87367016, 1.0],
+ [1.5910575, 0.95255834, 1.0],
+ [1.0409293, 0.9986148, 1.0],
+ [0.06169785, 0.99989563, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.69167626, 0.864867, 1.0],
+ [0.517416, 0.9371735, 1.0],
+ [0.7297484, 0.9740146, 1.0],
+ [0.14100057, 0.9978946, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1966501, 0.8564259, 1.0],
+ [0.7971627, 0.9295377, 1.0],
+ [0.98976326, 0.9878759, 1.0],
+ [0.10663256, 0.9997394, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.9381518, 0.84076357, 1.0],
+ [2.7677243, 0.9219802, 1.0],
+ [4.2827673, 0.9931431, 1.0],
+ [0.7215513, 1.0000048, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.0477877, 0.82670283, 1.0],
+ [3.4655888, 0.91149926, 1.0],
+ [5.5720844, 0.97649837, 1.0],
+ [2.4715145, 0.9948263, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.8419666, 0.8251362, 1.0],
+ [4.0095496, 0.90738267, 1.0],
+ [5.8746896, 0.9661354, 1.0],
+ [4.1081214, 0.98024684, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.5143023, 0.835298, 1.0],
+ [3.7496395, 0.9078515, 1.0],
+ [5.115886, 0.9541289, 1.0],
+ [3.3494966, 0.97551304, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.479901, 0.8648893, 1.0],
+ [3.820371, 0.9325536, 1.0],
+ [5.326134, 0.97972375, 1.0],
+ [4.717001, 1.0019616, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.5529094, 0.68653643, 1.0],
+ [4.249971, 0.8358243, 1.0],
+ [3.886182, 0.9036481, 1.0],
+ [3.1052408, 0.93986005, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.0453534, 0.69864804, 1.0],
+ [4.088292, 0.8079499, 1.0],
+ [4.4767594, 0.8848239, 1.0],
+ [4.920347, 0.8965313, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.8496194, 0.71566397, 1.0],
+ [4.111261, 0.7927633, 1.0],
+ [4.182716, 0.8936798, 1.0],
+ [4.9267282, 0.9027427, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.4675403, 0.7260139, 1.0],
+ [4.864559, 0.7873533, 1.0],
+ [3.662903, 0.8703417, 1.0],
+ [3.33834, 0.89759904, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.627538, 0.73187256, 1.0],
+ [4.7362914, 0.7925029, 1.0],
+ [2.762565, 0.85528374, 1.0],
+ [1.6968315, 0.8718531, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.1378064, 0.74603015, 1.0],
+ [3.7333937, 0.8095638, 1.0],
+ [1.7157373, 0.84111804, 1.0],
+ [0.7313545, 0.84846157, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.8278027, 0.73261493, 1.0],
+ [3.218722, 0.7893107, 1.0],
+ [1.7261162, 0.8081886, 1.0],
+ [1.3808451, 0.80620646, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.5486107, 0.735842, 1.0],
+ [3.34487, 0.7877779, 1.0],
+ [1.9852514, 0.7963513, 1.0],
+ [3.2250917, 0.7770536, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.3918, 0.7492594, 1.0],
+ [5.8002057, 0.76848954, 1.0],
+ [3.2627735, 0.7929065, 1.0],
+ [4.1515646, 0.7706826, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.339295, 0.7832515, 1.0],
+ [7.126709, 0.778491, 1.0],
+ [3.6896281, 0.8016613, 1.0],
+ [3.1497123, 0.78488094, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.593153, 0.7601979, 1.0],
+ [7.8867235, 0.7450819, 1.0],
+ [5.9123063, 0.7422206, 1.0],
+ [3.8759134, 0.74508727, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.585278, 0.7479115, 1.0],
+ [7.529606, 0.7175019, 1.0],
+ [6.162009, 0.7064114, 1.0],
+ [4.4188056, 0.7255969, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.858168, 0.7368539, 1.0],
+ [5.492259, 0.7144793, 1.0],
+ [4.347751, 0.70099527, 1.0],
+ [3.5681868, 0.7093534, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.2282605, 0.73686594, 1.0],
+ [3.2710242, 0.70864916, 1.0],
+ [2.9356287, 0.6760259, 1.0],
+ [3.0854452, 0.64669275, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.4789567, 0.74254066, 1.0],
+ [2.5880811, 0.7186022, 1.0],
+ [2.387628, 0.66940564, 1.0],
+ [2.4265919, 0.6265305, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.528232, 0.7247535, 1.0],
+ [2.7541575, 0.7074213, 1.0],
+ [2.5300722, 0.6434275, 1.0],
+ [2.7180758, 0.5980046, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.102856, 0.728164, 1.0],
+ [2.6525943, 0.71301305, 1.0],
+ [2.7899814, 0.67262053, 1.0],
+ [3.1614673, 0.6270317, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.6348686, 0.73153895, 1.0],
+ [2.726432, 0.6877879, 1.0],
+ [2.54031, 0.7097242, 1.0],
+ [2.7431397, 0.7150978, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.9502785, 0.69625, 1.0],
+ [2.6242392, 0.6294526, 1.0],
+ [2.2081497, 0.66211057, 1.0],
+ [1.3193871, 0.82756114, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.0414326, 0.6955794, 1.0],
+ [2.0578754, 0.6632495, 1.0],
+ [1.7014283, 0.7117629, 1.0],
+ [0.3407154, 0.9488139, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1289076, 0.7163192, 1.0],
+ [1.151557, 0.7172398, 1.0],
+ [0.52380514, 0.86909467, 1.0],
+ [0.019359209, 0.9961916, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.67299646, 0.7355766, 1.0],
+ [0.51002353, 0.7648463, 1.0],
+ [0.06944082, 0.96125567, 1.0],
+ [0.000119109, 0.9998196, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.031164411, 0.77996194, 1.0],
+ [0.014347508, 0.86290824, 1.0],
+ [-0.007677018, 0.99158084, 1.0],
+ [-6.689423e-05, 0.9999727, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.6832901, 0.8222584, 1.0],
+ [-0.3419607, 0.92960227, 1.0],
+ [-0.022380346, 0.9983181, 1.0],
+ [-0.00018521023, 0.9999936, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.0638311, 0.89357316, 1.0],
+ [-0.2623169, 0.97307545, 1.0],
+ [-0.008422446, 0.99948406, 1.0],
+ [-0.00014290158, 0.99999386, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.1319717, 0.89954185, 1.0],
+ [-0.28591886, 0.9751361, 1.0],
+ [-0.01649866, 0.9978075, 1.0],
+ [-0.0016126642, 0.99978495, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.0644026, 0.86974543, 1.0],
+ [-0.26208007, 0.9613298, 1.0],
+ [-0.054121625, 0.9739053, 1.0],
+ [-0.018682964, 0.996442, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9141648, 0.88052386, 1.0],
+ [-0.27626938, 0.93415976, 1.0],
+ [-0.24882263, 0.9263471, 1.0],
+ [-0.06974276, 0.9880197, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.0521605, 0.8621233, 1.0],
+ [-0.4830994, 0.962234, 1.0],
+ [-0.02020153, 0.9847871, 1.0],
+ [0.019950174, 0.99770623, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.3087368, 0.84192723, 1.0],
+ [-0.65065837, 0.9700698, 1.0],
+ [0.11342574, 0.99164987, 1.0],
+ [0.050568797, 0.99678665, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.7450078, 0.8366232, 1.0],
+ [-0.49405074, 0.9702762, 1.0],
+ [0.107314065, 0.9858985, 1.0],
+ [0.053864308, 0.99334574, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.5509698, 0.8247367, 1.0],
+ [-0.20142983, 0.96617097, 1.0],
+ [-0.2560394, 0.981325, 1.0],
+ [-0.1159319, 0.9916455, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.5854938, 0.81745756, 1.0],
+ [-0.14268312, 0.97136134, 1.0],
+ [-0.06509467, 0.9953569, 1.0],
+ [-0.029058143, 0.9980467, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.8367958, 0.8402703, 1.0],
+ [-0.10091935, 0.97176695, 1.0],
+ [-0.0010153912, 0.9995565, 1.0],
+ [-3.0446154e-05, 0.9999878, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.1198392, 0.84143376, 1.0],
+ [-0.03249274, 0.952964, 1.0],
+ [0.012029766, 0.99843806, 1.0],
+ [8.835511e-05, 0.9999943, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9262636, 0.86367226, 1.0],
+ [0.5532922, 0.93086356, 1.0],
+ [0.09160686, 0.99354273, 1.0],
+ [0.00081093685, 0.9999354, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3739588, 0.8016463, 1.0],
+ [1.2970295, 0.7846802, 1.0],
+ [0.45966804, 0.9381906, 1.0],
+ [0.0061520343, 0.99921715, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.4077954, 0.7208454, 1.0],
+ [0.4733461, 0.6787829, 1.0],
+ [0.47692603, 0.80988336, 1.0],
+ [0.012340703, 0.9957405, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.542054, 0.69510156, 1.0],
+ [-1.0206543, 0.66685224, 1.0],
+ [-0.3861955, 0.77343684, 1.0],
+ [-0.0016055064, 0.9925781, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.949766, 0.7236445, 1.0],
+ [-2.2197113, 0.72735757, 1.0],
+ [-0.622677, 0.88092417, 1.0],
+ [-0.015985137, 0.996581, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.6489305, 0.68735474, 1.0],
+ [-3.5802984, 0.78218496, 1.0],
+ [-0.5320879, 0.92443025, 1.0],
+ [-0.022485942, 0.9953605, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.177222, 0.67137307, 1.0],
+ [-3.1258667, 0.780959, 1.0],
+ [-1.202351, 0.83195996, 1.0],
+ [-0.23152742, 0.9466141, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.1036143, 0.67803013, 1.0],
+ [-3.811027, 0.75741714, 1.0],
+ [-1.5931911, 0.8057145, 1.0],
+ [-0.4153525, 0.8942517, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.7684326, 0.6596717, 1.0],
+ [-5.32512, 0.69404334, 1.0],
+ [-3.1082761, 0.7684906, 1.0],
+ [-1.2460512, 0.894522, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.8683157, 0.6405401, 1.0],
+ [-4.9210896, 0.68566376, 1.0],
+ [-6.001439, 0.7500565, 1.0],
+ [-7.3015804, 0.812211, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.0999904, 0.6463381, 1.0],
+ [-3.7537494, 0.71320873, 1.0],
+ [-3.3960533, 0.7802192, 1.0],
+ [-3.351788, 0.85274297, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.22911, 0.68255895, 1.0],
+ [-2.943262, 0.75258833, 1.0],
+ [-2.539806, 0.79581076, 1.0],
+ [-1.4436098, 0.85693127, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.421207, 0.6897405, 1.0],
+ [-2.546169, 0.7558628, 1.0],
+ [-0.93006057, 0.8137628, 1.0],
+ [0.48019657, 0.90172833, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.955394, 0.63199955, 1.0],
+ [-3.2294798, 0.7054303, 1.0],
+ [-1.6279404, 0.7905365, 1.0],
+ [0.42406368, 0.95621544, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.406013, 0.61143196, 1.0],
+ [-2.4224486, 0.6885289, 1.0],
+ [0.15226217, 0.94315904, 1.0],
+ [0.3682204, 1.0079356, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.6070106, 0.59719855, 1.0],
+ [-0.5420019, 0.6935155, 1.0],
+ [0.49049014, 0.98263836, 1.0],
+ [0.093591675, 1.0030893, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.4122118, 0.62467295, 1.0],
+ [0.61009103, 0.7372408, 1.0],
+ [0.48575926, 0.9820844, 1.0],
+ [0.025345104, 1.0000371, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.9076017, 0.68767226, 1.0],
+ [0.656146, 0.8025899, 1.0],
+ [0.37165537, 0.9719272, 1.0],
+ [0.009354364, 0.9996975, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.519386, 0.7165023, 1.0],
+ [0.67302865, 0.8780051, 1.0],
+ [0.5275675, 0.94928, 1.0],
+ [0.022246271, 0.9979151, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.09680573, 0.738186, 1.0],
+ [0.793014, 0.8750658, 1.0],
+ [0.381764, 0.9613324, 1.0],
+ [0.009429865, 0.99904037, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.8834604, 0.77035594, 1.0],
+ [1.7601224, 0.8644663, 1.0],
+ [0.14496735, 0.98939514, 1.0],
+ [0.00014458288, 0.999937, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.8881173, 0.79435354, 1.0],
+ [2.1193182, 0.87771165, 1.0],
+ [0.18743913, 0.9930873, 1.0],
+ [0.00059748656, 0.999982, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.8984065, 0.7908348, 1.0],
+ [0.8870811, 0.86748314, 1.0],
+ [0.060655933, 0.9830568, 1.0],
+ [-0.00023914597, 0.9998897, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.052016236, 0.79481345, 1.0],
+ [-0.2686036, 0.8499417, 1.0],
+ [-0.24796923, 0.94703746, 1.0],
+ [-0.00565591, 0.99915326, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.2217782, 0.81121093, 1.0],
+ [-1.5196391, 0.8464336, 1.0],
+ [-0.6995659, 0.93161744, 1.0],
+ [-0.015096771, 0.99854493, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9182163, 0.7860046, 1.0],
+ [-1.7838149, 0.8694839, 1.0],
+ [-0.3889935, 0.966303, 1.0],
+ [-0.0060358397, 0.99937606, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.4767361, 0.76479936, 1.0],
+ [-0.9574383, 0.89968514, 1.0],
+ [-0.093257755, 0.9917394, 1.0],
+ [-0.0010219052, 0.99989015, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.1596841, 0.762179, 1.0],
+ [-1.0841764, 0.8807078, 1.0],
+ [-0.13100374, 0.99240035, 1.0],
+ [-0.0010621684, 0.9999584, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.6849198, 0.7800176, 1.0],
+ [-1.0038462, 0.8779829, 1.0],
+ [-0.09007008, 0.9921065, 1.0],
+ [-0.0005381258, 0.9999664, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.28321254, 0.7977584, 1.0],
+ [-0.16388126, 0.890086, 1.0],
+ [0.03192479, 0.9929511, 1.0],
+ [0.001241174, 0.9999684, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.86829543, 0.7936024, 1.0],
+ [-0.37591735, 0.89833724, 1.0],
+ [-0.14845619, 0.9868604, 1.0],
+ [0.00082641013, 0.99980897, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.0176198, 0.7752524, 1.0],
+ [1.6908506, 0.8934624, 1.0],
+ [0.44986808, 0.9873634, 1.0],
+ [0.008859725, 0.99985033, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.6487887, 0.748154, 1.0],
+ [2.6644409, 0.8853934, 1.0],
+ [0.99475205, 0.98551035, 1.0],
+ [0.050193034, 0.9997457, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.480023, 0.7250523, 1.0],
+ [1.926729, 0.8424514, 1.0],
+ [0.040736295, 0.92375875, 1.0],
+ [0.051179513, 0.99580204, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.1756477, 0.71742564, 1.0],
+ [1.3003006, 0.80706257, 1.0],
+ [-1.707145, 0.8457335, 1.0],
+ [-0.49415466, 0.96840185, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.975246, 0.7181208, 1.0],
+ [2.2769587, 0.81511086, 1.0],
+ [-0.6684549, 0.8240774, 1.0],
+ [-1.1093187, 0.9021424, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.6670837, 0.7181022, 1.0],
+ [3.5531816, 0.83718723, 1.0],
+ [2.3386168, 0.85608834, 1.0],
+ [1.0339524, 0.91459143, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.161144, 0.74439704, 1.0],
+ [1.2685431, 0.90642077, 1.0],
+ [1.2386372, 0.91409504, 1.0],
+ [0.9295507, 0.9615406, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.1777663, 0.7336535, 1.0],
+ [1.6435673, 0.89623827, 1.0],
+ [4.6341896, 0.8448458, 1.0],
+ [5.328556, 0.8743724, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.8456101, 0.74945605, 1.0],
+ [1.7118593, 0.8730719, 1.0],
+ [4.7893443, 0.8741666, 1.0],
+ [5.7356167, 0.8831293, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.541483, 0.68340665, 1.0],
+ [3.1775188, 0.750212, 1.0],
+ [2.7961404, 0.89943826, 1.0],
+ [2.240247, 0.94229096, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.315433, 0.6658273, 1.0],
+ [4.4101477, 0.6992003, 1.0],
+ [2.1726768, 0.87961835, 1.0],
+ [0.8891901, 0.96283895, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.594221, 0.6638776, 1.0],
+ [5.8431273, 0.65800124, 1.0],
+ [5.264457, 0.7205041, 1.0],
+ [2.3550959, 0.89265925, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.089242, 0.6326394, 1.0],
+ [7.3321614, 0.59986347, 1.0],
+ [8.751942, 0.6218272, 1.0],
+ [7.835094, 0.73740655, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.8648624, 0.6189883, 1.0],
+ [7.854025, 0.5970248, 1.0],
+ [9.556352, 0.6305344, 1.0],
+ [9.962623, 0.70967585, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.106201, 0.6309747, 1.0],
+ [8.538228, 0.6175027, 1.0],
+ [9.568188, 0.6525294, 1.0],
+ [9.985585, 0.7052805, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.281834, 0.64497524, 1.0],
+ [7.932338, 0.6421112, 1.0],
+ [6.0639887, 0.72733206, 1.0],
+ [5.3430767, 0.77500343, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.019873, 0.6824282, 1.0],
+ [7.110651, 0.6669911, 1.0],
+ [5.331789, 0.7284526, 1.0],
+ [3.3326175, 0.8244816, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.284438, 0.6928456, 1.0],
+ [7.2835546, 0.6664346, 1.0],
+ [4.883378, 0.72301525, 1.0],
+ [1.6401588, 0.8878237, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.3979216, 0.7423805, 1.0],
+ [5.322938, 0.73012125, 1.0],
+ [2.8791666, 0.7680131, 1.0],
+ [1.2293904, 0.8762204, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.9055657, 0.8100785, 1.0],
+ [3.3159246, 0.7462504, 1.0],
+ [2.3902402, 0.6859098, 1.0],
+ [1.833428, 0.74496704, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.3105671, 0.8353257, 1.0],
+ [1.5842186, 0.76920885, 1.0],
+ [1.6503354, 0.6779311, 1.0],
+ [1.5094707, 0.6925992, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.6453714, 0.83882755, 1.0],
+ [1.2651672, 0.7836908, 1.0],
+ [1.7122288, 0.64048153, 1.0],
+ [1.8864015, 0.6182712, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4681439, 0.84673464, 1.0],
+ [1.016748, 0.7926556, 1.0],
+ [1.5301808, 0.6895232, 1.0],
+ [1.7087327, 0.6774361, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.2655282, 0.8380442, 1.0],
+ [1.400092, 0.7222438, 1.0],
+ [0.9317295, 0.751648, 1.0],
+ [0.38670647, 0.9075669, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.99011, 0.8104178, 1.0],
+ [1.5600725, 0.6673882, 1.0],
+ [0.8698622, 0.75667554, 1.0],
+ [0.05312782, 0.9771482, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.57059133, 0.84510267, 1.0],
+ [0.82562035, 0.7819899, 1.0],
+ [0.26795024, 0.91805077, 1.0],
+ [0.007893546, 0.9974623, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.025850505, 0.885807, 1.0],
+ [0.16424465, 0.907354, 1.0],
+ [0.02480482, 0.9894822, 1.0],
+ [0.0002904646, 0.9998891, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.0051441705, 0.905744, 1.0],
+ [0.052886356, 0.94857466, 1.0],
+ [0.003892918, 0.99737555, 1.0],
+ [1.5296924e-05, 0.9999911, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.13219802, 0.9432007, 1.0],
+ [0.0067049917, 0.9855628, 1.0],
+ [0.00048594674, 0.99958736, 1.0],
+ [1.3410622e-06, 0.99999905, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.10593759, 0.96154904, 1.0],
+ [-0.007101243, 0.9942198, 1.0],
+ [-5.9125785e-05, 0.99992687, 1.0],
+ [-1.7132953e-07, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.1380907, 0.98662883, 1.0],
+ [-0.0150175765, 0.99846256, 1.0],
+ [-0.00010678602, 0.9999852, 1.0],
+ [-7.602713e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.13843897, 0.99187297, 1.0],
+ [-0.014456171, 0.9993438, 1.0],
+ [-0.00010089983, 0.9999929, 1.0],
+ [-1.5361991e-06, 0.99999964, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.25314486, 0.9803601, 1.0],
+ [-0.015702732, 0.9983346, 1.0],
+ [-0.0008912772, 0.99966294, 1.0],
+ [-0.00028706418, 0.9999485, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.13319662, 0.9884858, 1.0],
+ [-0.008045408, 0.9985032, 1.0],
+ [-0.00546511, 0.9987218, 1.0],
+ [-0.0013116907, 0.9997901, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.14743957, 0.98442274, 1.0],
+ [-0.010547765, 0.9988189, 1.0],
+ [-0.00075808156, 0.99986076, 1.0],
+ [-8.594952e-05, 0.99998194, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.37524244, 0.9815919, 1.0],
+ [-0.030857285, 0.9987118, 1.0],
+ [-7.657946e-06, 0.9999798, 1.0],
+ [7.531104e-05, 0.9999943, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.30199113, 0.97909504, 1.0],
+ [-0.019221604, 0.99835676, 1.0],
+ [0.000142119, 0.99995494, 1.0],
+ [0.000114501025, 0.99998343, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.07426342, 0.9718809, 1.0],
+ [0.006918636, 0.99745023, 1.0],
+ [-0.00086405553, 0.99991477, 1.0],
+ [-0.0004274567, 0.99996984, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.1214249, 0.97155046, 1.0],
+ [0.020241432, 0.9978329, 1.0],
+ [-6.842438e-05, 0.9999722, 1.0],
+ [-0.000104643506, 0.99999297, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.00912883, 0.98002875, 1.0],
+ [0.009914126, 0.998089, 1.0],
+ [0.00014658475, 0.99998343, 1.0],
+ [-3.147069e-07, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.34022465, 0.97413856, 1.0],
+ [-0.001044419, 0.99583006, 1.0],
+ [0.0007858895, 0.9998906, 1.0],
+ [2.2824117e-06, 0.99999976, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.234471, 0.98301566, 1.0],
+ [0.012073258, 0.9957116, 1.0],
+ [0.0027358236, 0.9997176, 1.0],
+ [1.8471012e-05, 0.9999981, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.37233362, 0.96034855, 1.0],
+ [0.20355557, 0.9555334, 1.0],
+ [0.040644884, 0.99291646, 1.0],
+ [0.00042774918, 0.99992293, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.26744676, 0.88313514, 1.0],
+ [0.25718036, 0.8152884, 1.0],
+ [0.09190484, 0.9529967, 1.0],
+ [0.0019344173, 0.999235, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.92890036, 0.8132875, 1.0],
+ [-0.62145543, 0.75051594, 1.0],
+ [-0.07648181, 0.9260143, 1.0],
+ [0.0015313957, 0.9985089, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.4036704, 0.87832355, 1.0],
+ [-0.62560636, 0.879744, 1.0],
+ [-0.07835409, 0.97920424, 1.0],
+ [-0.001020268, 0.9996515, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.5662296, 0.83067703, 1.0],
+ [-0.6488212, 0.94305354, 1.0],
+ [-0.052740045, 0.9922627, 1.0],
+ [-0.0016239518, 0.9996923, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.7540584, 0.71597546, 1.0],
+ [-2.4036474, 0.8318091, 1.0],
+ [-0.7855407, 0.9302143, 1.0],
+ [-0.030755855, 0.9963339, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.401625, 0.6375166, 1.0],
+ [-4.2524567, 0.73438454, 1.0],
+ [-1.7984731, 0.8661054, 1.0],
+ [-0.076227136, 0.9925712, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.093013, 0.57054, 1.0],
+ [-7.138467, 0.61704755, 1.0],
+ [-10.662155, 0.7250832, 1.0],
+ [-4.8741217, 0.8936352, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.7872076, 0.543695, 1.0],
+ [-9.125988, 0.5788258, 1.0],
+ [-17.282015, 0.6463619, 1.0],
+ [-15.361004, 0.7047228, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.5472207, 0.5460788, 1.0],
+ [-7.3952637, 0.5873735, 1.0],
+ [-9.985295, 0.6460461, 1.0],
+ [-9.715605, 0.75660294, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.143126, 0.5428474, 1.0],
+ [-6.979643, 0.59978616, 1.0],
+ [-7.3462987, 0.654454, 1.0],
+ [-4.6714454, 0.7781302, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.6845136, 0.5430165, 1.0],
+ [-7.2012515, 0.5718516, 1.0],
+ [-8.748415, 0.59380674, 1.0],
+ [-4.6859508, 0.76675576, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.1071506, 0.54315025, 1.0],
+ [-5.8883495, 0.56850153, 1.0],
+ [-7.7368093, 0.6292215, 1.0],
+ [-1.4560128, 0.9221482, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.574775, 0.5935165, 1.0],
+ [-1.5976609, 0.6629436, 1.0],
+ [-0.42182118, 0.9376009, 1.0],
+ [0.0032954898, 0.99905676, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.5040222, 0.66888565, 1.0],
+ [0.22192046, 0.765007, 1.0],
+ [0.039829105, 0.97707367, 1.0],
+ [0.0013656623, 0.99990016, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.13229428, 0.750091, 1.0],
+ [0.26277184, 0.88919055, 1.0],
+ [0.020233026, 0.99522144, 1.0],
+ [0.00035138454, 0.99998504, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.24901535, 0.8024303, 1.0],
+ [0.3177278, 0.91495854, 1.0],
+ [0.051968362, 0.9940321, 1.0],
+ [0.0007878256, 0.9999375, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.40787914, 0.8393718, 1.0],
+ [0.6778435, 0.91640764, 1.0],
+ [0.2564198, 0.97801465, 1.0],
+ [0.0053125066, 0.99956983, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.9363007, 0.86411923, 1.0],
+ [0.7308819, 0.92368954, 1.0],
+ [0.13012709, 0.9888115, 1.0],
+ [0.0020543067, 0.99983764, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.792059, 0.9070631, 1.0],
+ [0.8328082, 0.961077, 1.0],
+ [0.03201244, 0.9983668, 1.0],
+ [0.00010289253, 0.99999255, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.61971796, 0.93544394, 1.0],
+ [0.28986993, 0.98064864, 1.0],
+ [0.011807639, 0.99949574, 1.0],
+ [3.4211585e-05, 0.99999887, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.36694813, 0.9191711, 1.0],
+ [0.15008779, 0.9748141, 1.0],
+ [0.004682179, 0.9991603, 1.0],
+ [5.24191e-06, 0.9999966, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.04590827, 0.94109243, 1.0],
+ [-0.035157125, 0.9765446, 1.0],
+ [-0.007650317, 0.99820185, 1.0],
+ [-0.00011332178, 0.99998254, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.15313512, 0.97539425, 1.0],
+ [-0.13065065, 0.9864903, 1.0],
+ [-0.017957566, 0.9982736, 1.0],
+ [-0.00023828977, 0.99997765, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.32719183, 0.960239, 1.0],
+ [-0.13606764, 0.9877723, 1.0],
+ [-0.007543968, 0.99940944, 1.0],
+ [-6.793774e-05, 0.9999944, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.33494267, 0.960886, 1.0],
+ [-0.12677892, 0.9896274, 1.0],
+ [-0.00804112, 0.9995762, 1.0],
+ [-5.0966377e-05, 0.99999756, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.56956315, 0.94241995, 1.0],
+ [-0.83334297, 0.9581462, 1.0],
+ [-0.09486739, 0.9961979, 1.0],
+ [-0.00065249624, 0.99997574, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.39292914, 0.943953, 1.0],
+ [-0.43260995, 0.9667326, 1.0],
+ [-0.036218695, 0.99800736, 1.0],
+ [-0.00021563526, 0.99999046, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.32144308, 0.95278865, 1.0],
+ [-0.25995246, 0.9713513, 1.0],
+ [-0.016442856, 0.9985246, 1.0],
+ [-9.176571e-05, 0.9999942, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.2966282, 0.9744063, 1.0],
+ [-0.86246514, 0.9668585, 1.0],
+ [-0.17140412, 0.9942151, 1.0],
+ [-0.0021118673, 0.9999324, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.08769655, 0.97262937, 1.0],
+ [-0.25923535, 0.9713818, 1.0],
+ [-0.06498976, 0.99482614, 1.0],
+ [-0.00078246964, 0.99993557, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.5253878, 0.9505631, 1.0],
+ [-0.16767718, 0.9614315, 1.0],
+ [-0.24291219, 0.98733723, 1.0],
+ [-0.009154632, 0.99959785, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.57957804, 0.9173889, 1.0],
+ [-3.944535, 0.831082, 1.0],
+ [-2.9686522, 0.8881784, 1.0],
+ [-0.14177988, 0.9944771, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.42895818, 0.90997833, 1.0],
+ [-5.41419, 0.7882363, 1.0],
+ [-5.786541, 0.80714077, 1.0],
+ [-0.8622991, 0.97064185, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3849322, 0.89100575, 1.0],
+ [-3.418151, 0.8283688, 1.0],
+ [-7.0915565, 0.766481, 1.0],
+ [-2.9420247, 0.89867216, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.6696517, 0.8438243, 1.0],
+ [-0.2037993, 0.8984032, 1.0],
+ [-3.0600338, 0.83977926, 1.0],
+ [-1.4190359, 0.93247384, 1.0]
+ ],
+ "srgb_value": true
+ }
+}
diff --git a/assets/camera-profiles/NIKON_Z_6_2.json b/assets/camera-profiles/NIKON_Z_6_2.json
new file mode 100644
index 000000000..b00e045c1
--- /dev/null
+++ b/assets/camera-profiles/NIKON_Z_6_2.json
@@ -0,0 +1,1819 @@
+{
+ "version": 1,
+ "model": "NIKON Z 6_2",
+ "files": 296,
+ "samples": 1099207,
+ "matrix": [
+ [1.182910144251291, -0.29205257488744785, 0.10076600427689364],
+ [-0.06715969465291372, 1.185204734495203, -0.11541627485450512],
+ [-0.04243207284624334, -0.8237200432609093, 1.8732041320081247]
+ ],
+ "hue_sat": {
+ "hue_divisions": 72,
+ "sat_divisions": 5,
+ "val_divisions": 5,
+ "data": [
+ [0.0, 1.0, 1.0],
+ [6.2229915, 1.153762, 1.0],
+ [3.9428349, 1.1697679, 1.0],
+ [2.9512422, 1.1150318, 1.0],
+ [1.4521159, 1.0443524, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.067863, 1.1530151, 1.0],
+ [2.674163, 1.1492246, 1.0],
+ [1.6906267, 1.0755203, 1.0],
+ [1.0559876, 1.0193381, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.406609, 1.1438, 1.0],
+ [2.0468237, 1.126483, 1.0],
+ [0.6318166, 1.0328915, 1.0],
+ [0.38942862, 0.9951044, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.6572795, 1.1365504, 1.0],
+ [1.088494, 1.1316147, 1.0],
+ [-0.7465848, 1.038763, 1.0],
+ [-1.7078602, 0.9750448, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.6649947, 1.1077542, 1.0],
+ [0.8204385, 1.1192347, 1.0],
+ [-1.5888842, 1.0491564, 1.0],
+ [-3.0090246, 0.96298206, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.256699, 1.0831563, 1.0],
+ [0.6007586, 1.0977919, 1.0],
+ [-2.570202, 1.0551977, 1.0],
+ [-5.4068832, 0.9654456, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.539097, 1.0591519, 1.0],
+ [-0.10774251, 1.0788635, 1.0],
+ [-4.641281, 1.0305448, 1.0],
+ [-8.283271, 0.93709576, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.142809, 1.0373175, 1.0],
+ [-0.8308222, 1.0613977, 1.0],
+ [-5.655344, 1.0214658, 1.0],
+ [-9.607277, 0.93629843, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.8500762, 1.0362741, 1.0],
+ [-1.0345856, 1.0639124, 1.0],
+ [-5.4233723, 1.0284288, 1.0],
+ [-9.0873, 0.9531294, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.0558853, 1.0385702, 1.0],
+ [-0.52777964, 1.0754012, 1.0],
+ [-4.6981053, 1.0426258, 1.0],
+ [-7.390084, 0.97722876, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.439531, 1.0652789, 1.0],
+ [0.86604506, 1.0861824, 1.0],
+ [-2.8576968, 1.0505832, 1.0],
+ [-5.8478637, 0.9746698, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.3485656, 1.0967733, 1.0],
+ [1.9174945, 1.1038177, 1.0],
+ [0.058721844, 1.0518842, 1.0],
+ [-1.5929213, 0.9253342, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.6591005, 1.1211772, 1.0],
+ [2.2590137, 1.1025198, 1.0],
+ [1.1305307, 1.0139761, 1.0],
+ [-0.1372095, 0.87818533, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.6356182, 1.1266937, 1.0],
+ [1.7848063, 1.0862155, 1.0],
+ [1.2136673, 0.99129325, 1.0],
+ [0.49023673, 0.8714401, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4648303, 1.118881, 1.0],
+ [0.9687011, 1.0753107, 1.0],
+ [0.7960804, 0.99398947, 1.0],
+ [0.7395835, 0.8858062, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.44883335, 1.1299157, 1.0],
+ [0.61828953, 1.0732124, 1.0],
+ [0.9684744, 0.99130034, 1.0],
+ [1.4130936, 0.9186135, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.40853867, 1.140463, 1.0],
+ [0.99736977, 1.0603918, 1.0],
+ [1.5362628, 0.9824557, 1.0],
+ [2.16631, 0.9292637, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.20395991, 1.1486486, 1.0],
+ [0.8272985, 1.0560744, 1.0],
+ [1.4990557, 0.9908201, 1.0],
+ [2.295185, 0.93907094, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.57153416, 1.1716017, 1.0],
+ [0.24325375, 1.0848265, 1.0],
+ [1.1403633, 1.0200775, 1.0],
+ [2.1166058, 0.95587236, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7980459, 1.2015567, 1.0],
+ [-0.9087, 1.1328977, 1.0],
+ [0.12065623, 1.0612612, 1.0],
+ [1.1421617, 0.9739673, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.3530312, 1.227411, 1.0],
+ [-2.557493, 1.1682405, 1.0],
+ [-1.6862918, 1.0950687, 1.0],
+ [-0.6257788, 0.9961956, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.1431966, 1.2525787, 1.0],
+ [-4.3500066, 1.1942875, 1.0],
+ [-3.3942263, 1.1264504, 1.0],
+ [-0.860235, 1.0129987, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.172523, 1.2782786, 1.0],
+ [-6.2652287, 1.2184532, 1.0],
+ [-5.2175336, 1.1654094, 1.0],
+ [-0.90600926, 1.02157, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.301277, 1.3098416, 1.0],
+ [-8.319625, 1.254189, 1.0],
+ [-6.29361, 1.1807519, 1.0],
+ [-0.2820965, 1.0070943, 1.0],
+ [0.0, 1.0, 1.0],
+ [-11.833535, 1.3462554, 1.0],
+ [-10.534334, 1.2848289, 1.0],
+ [-5.5140915, 1.1255784, 1.0],
+ [-0.11413354, 1.0005571, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.837044, 1.3168585, 1.0],
+ [-11.592036, 1.241745, 1.0],
+ [-3.8057983, 1.0537387, 1.0],
+ [-0.12250334, 0.9994377, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.302334, 1.2547938, 1.0],
+ [-12.304744, 1.171094, 1.0],
+ [-2.6331367, 1.0254496, 1.0],
+ [-0.02462376, 1.0000972, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.3649, 1.2033144, 1.0],
+ [-13.840853, 1.121017, 1.0],
+ [-4.7440042, 1.016522, 1.0],
+ [-0.09164992, 1.0001389, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.23536, 1.1622376, 1.0],
+ [-14.924817, 1.0792886, 1.0],
+ [-6.874241, 1.0002234, 1.0],
+ [-0.19945428, 0.9995216, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.24167, 1.1120819, 1.0],
+ [-13.978684, 1.0264746, 1.0],
+ [-5.8763328, 0.98856574, 1.0],
+ [-0.13876384, 0.999489, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.172258, 1.0878747, 1.0],
+ [-15.457481, 1.0080547, 1.0],
+ [-6.565444, 0.9856226, 1.0],
+ [-0.13294418, 0.9996134, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.83038, 1.0693645, 1.0],
+ [-15.625549, 0.9914287, 1.0],
+ [-7.72421, 0.9804178, 1.0],
+ [-0.15811352, 0.9995089, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.11028, 1.0584325, 1.0],
+ [-14.346652, 0.99943095, 1.0],
+ [-6.310089, 0.9858199, 1.0],
+ [-0.109850876, 0.9996651, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.929288, 1.0361999, 1.0],
+ [-12.50551, 0.9834405, 1.0],
+ [-5.2874904, 0.98251873, 1.0],
+ [-0.0921156, 0.99959683, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.727734, 1.0360364, 1.0],
+ [-12.03307, 0.9717191, 1.0],
+ [-5.2304645, 0.97766334, 1.0],
+ [-0.07621693, 0.9996011, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.998532, 1.0374138, 1.0],
+ [-13.555987, 0.9710816, 1.0],
+ [-6.071474, 0.97758615, 1.0],
+ [-0.088387944, 0.9996019, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.750172, 1.027443, 1.0],
+ [-14.493484, 0.9749124, 1.0],
+ [-5.9644837, 0.9815559, 1.0],
+ [-0.09535428, 0.9994739, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.231142, 0.9994733, 1.0],
+ [-14.346948, 0.95981574, 1.0],
+ [-6.037715, 0.9591468, 1.0],
+ [-0.38386485, 0.9952976, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.424784, 0.9555782, 1.0],
+ [-14.198873, 0.9357824, 1.0],
+ [-6.6123714, 0.94408447, 1.0],
+ [-0.4532846, 0.9941679, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.279493, 0.90192854, 1.0],
+ [-13.564478, 0.90127003, 1.0],
+ [-6.147515, 0.93834627, 1.0],
+ [-0.2323653, 0.9971496, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.093803, 0.85399765, 1.0],
+ [-12.756323, 0.87821823, 1.0],
+ [-6.1378803, 0.92416245, 1.0],
+ [-0.26953444, 0.9962813, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.701775, 0.8148734, 1.0],
+ [-11.737119, 0.86213005, 1.0],
+ [-4.978892, 0.925974, 1.0],
+ [-0.2315494, 0.9961441, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.116898, 0.79390544, 1.0],
+ [-11.100497, 0.8544334, 1.0],
+ [-3.051835, 0.9431366, 1.0],
+ [-0.095544234, 0.99742615, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.288991, 0.78051233, 1.0],
+ [-9.944976, 0.8618912, 1.0],
+ [-2.3829138, 0.9570897, 1.0],
+ [-0.033786688, 0.9981832, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.088607, 0.799739, 1.0],
+ [-6.9067097, 0.9043021, 1.0],
+ [-2.2045915, 0.98223805, 1.0],
+ [-0.04008953, 0.9986963, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.372293, 0.8433635, 1.0],
+ [-3.43054, 0.96173584, 1.0],
+ [-0.43066922, 1.0183202, 1.0],
+ [0.19982383, 1.0013425, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.0979204, 0.87468654, 1.0],
+ [-2.605678, 0.9790638, 1.0],
+ [0.7438621, 1.0240322, 1.0],
+ [0.90940267, 1.007664, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.001, 0.881765, 1.0],
+ [-1.34574, 1.0225254, 1.0],
+ [1.5278748, 1.0623524, 1.0],
+ [1.7761368, 1.0346187, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.164434, 0.8987546, 1.0],
+ [-0.2458219, 1.0717543, 1.0],
+ [1.9966408, 1.1075406, 1.0],
+ [2.03719, 1.0744351, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.634933, 0.86535674, 1.0],
+ [-1.0753672, 1.0728083, 1.0],
+ [1.0631428, 1.1154276, 1.0],
+ [0.6964589, 1.0430381, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.4912696, 0.92214453, 1.0],
+ [-0.93249446, 1.0711535, 1.0],
+ [-0.41212073, 1.0973893, 1.0],
+ [-0.1942435, 1.0235159, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.8377413, 1.0034848, 1.0],
+ [-0.45683056, 1.0996051, 1.0],
+ [-0.7544672, 1.0989084, 1.0],
+ [-0.3452393, 1.0226208, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.1365077, 1.0087785, 1.0],
+ [0.0327875, 1.0931748, 1.0],
+ [0.03409552, 1.0605692, 1.0],
+ [-0.08136579, 1.0068369, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.215217, 0.95414764, 1.0],
+ [1.6130612, 1.0335056, 1.0],
+ [0.07605914, 1.0273331, 1.0],
+ [-0.20885852, 1.003798, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.3572938, 0.92316496, 1.0],
+ [2.6421683, 1.0025958, 1.0],
+ [0.9645612, 1.0110464, 1.0],
+ [0.10840102, 1.0010912, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.770162, 0.91984165, 1.0],
+ [3.5076504, 0.99966085, 1.0],
+ [1.6117474, 1.0087022, 1.0],
+ [0.47452623, 0.99906003, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.279187, 0.9259939, 1.0],
+ [2.1051095, 1.0003723, 1.0],
+ [0.7558728, 1.0085037, 1.0],
+ [0.4204648, 0.999157, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.1316261, 0.9648295, 1.0],
+ [1.2332041, 1.0157939, 1.0],
+ [0.23060545, 1.0078584, 1.0],
+ [0.014259016, 1.0006881, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.3989656, 0.99194896, 1.0],
+ [2.2900262, 1.03556, 1.0],
+ [1.1375499, 1.013088, 1.0],
+ [0.113812156, 1.0003917, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.0414777, 1.0378546, 1.0],
+ [2.3057628, 1.1105787, 1.0],
+ [1.6360228, 1.0446037, 1.0],
+ [0.24664895, 1.0011342, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.806446, 1.0598252, 1.0],
+ [2.9564526, 1.1106843, 1.0],
+ [0.9111372, 1.0506293, 1.0],
+ [-0.017489238, 1.0016158, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.3138623, 1.0589161, 1.0],
+ [2.0492713, 1.1007633, 1.0],
+ [1.2071747, 1.0373629, 1.0],
+ [0.13990282, 1.0014288, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.150446, 1.0142206, 1.0],
+ [2.3013182, 1.0552701, 1.0],
+ [1.7414474, 1.0204091, 1.0],
+ [0.2009622, 1.0014528, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.0740504, 0.9837976, 1.0],
+ [2.0655751, 1.0382036, 1.0],
+ [0.7234968, 1.018726, 1.0],
+ [-0.1094017, 1.0020746, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.1731286, 0.99559367, 1.0],
+ [2.5818305, 1.019053, 1.0],
+ [1.2261839, 1.0031092, 1.0],
+ [0.17011318, 1.0003996, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.2959146, 1.0069213, 1.0],
+ [1.4468756, 1.0213524, 1.0],
+ [0.6366564, 1.0068634, 1.0],
+ [0.06898734, 1.0005128, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.0099535, 1.005978, 1.0],
+ [2.4273057, 1.0169591, 1.0],
+ [-0.13818303, 1.0067599, 1.0],
+ [-0.0033633148, 1.0006784, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.5230107, 1.0331137, 1.0],
+ [4.7096148, 1.0529838, 1.0],
+ [1.6526409, 1.0215665, 1.0],
+ [0.09062616, 1.0016879, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.177528, 1.0500778, 1.0],
+ [5.78432, 1.0727233, 1.0],
+ [4.1867056, 1.0556285, 1.0],
+ [0.946918, 1.0117143, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.787752, 1.0514582, 1.0],
+ [6.581295, 1.0929804, 1.0],
+ [5.1642475, 1.0660969, 1.0],
+ [1.3973913, 1.0133713, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.352549, 1.0832101, 1.0],
+ [6.488305, 1.1071424, 1.0],
+ [4.7150044, 1.0691316, 1.0],
+ [0.9782577, 1.0079756, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.44746, 1.1423436, 1.0],
+ [5.2333174, 1.1424847, 1.0],
+ [4.2587075, 1.0928718, 1.0],
+ [1.1760778, 1.0190315, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.7819715, 1.0929513, 1.0],
+ [3.038743, 1.1225566, 1.0],
+ [3.0403032, 1.0947937, 1.0],
+ [2.7168245, 1.0584737, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.2442985, 1.0740716, 1.0],
+ [1.8085717, 1.092445, 1.0],
+ [1.6521245, 1.0550505, 1.0],
+ [1.2793007, 1.015843, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.8161194, 1.048953, 1.0],
+ [1.0271565, 1.0645669, 1.0],
+ [0.37108362, 1.0149966, 1.0],
+ [0.16207772, 0.98473, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.9961936, 1.0367951, 1.0],
+ [0.3735622, 1.0575669, 1.0],
+ [-1.3138187, 0.9924495, 1.0],
+ [-2.2027087, 0.9511259, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.2774894, 1.0240017, 1.0],
+ [0.48561475, 1.0381898, 1.0],
+ [-1.8494679, 0.98786247, 1.0],
+ [-3.2887905, 0.9338637, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.4378223, 1.0108881, 1.0],
+ [0.52244306, 1.0189863, 1.0],
+ [-1.8643115, 0.99323344, 1.0],
+ [-5.1015615, 0.92938703, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.1067812, 0.9985392, 1.0],
+ [-0.12131784, 1.0145019, 1.0],
+ [-4.1442766, 0.986398, 1.0],
+ [-8.339385, 0.9153509, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.4298422, 0.96142745, 1.0],
+ [-1.1118385, 1.0061582, 1.0],
+ [-5.695069, 0.9836191, 1.0],
+ [-9.741408, 0.91440827, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.01661, 0.9458497, 1.0],
+ [-1.662954, 0.999966, 1.0],
+ [-5.8223696, 0.9850643, 1.0],
+ [-9.232703, 0.9267809, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.2962167, 0.9605364, 1.0],
+ [-1.1373878, 1.0126402, 1.0],
+ [-4.8479958, 0.99816114, 1.0],
+ [-7.4450336, 0.9406932, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.036456, 0.99115914, 1.0],
+ [0.17963961, 1.0285513, 1.0],
+ [-3.3088403, 1.0056642, 1.0],
+ [-6.273956, 0.93629354, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.0664618, 1.026951, 1.0],
+ [1.2165159, 1.0476551, 1.0],
+ [-0.42700893, 0.9974062, 1.0],
+ [-1.4580182, 0.8679089, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.7013292, 1.0606134, 1.0],
+ [1.5602528, 1.0500852, 1.0],
+ [0.58767015, 0.96606606, 1.0],
+ [-0.12461443, 0.83817023, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.75421, 1.0628642, 1.0],
+ [1.077012, 1.0321119, 1.0],
+ [0.81344783, 0.9464788, 1.0],
+ [0.4725721, 0.8391105, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.6471407, 1.0532293, 1.0],
+ [0.40457213, 1.0171094, 1.0],
+ [0.61659175, 0.9494374, 1.0],
+ [0.7804061, 0.8600364, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.15095921, 1.0584109, 1.0],
+ [0.24909112, 0.999217, 1.0],
+ [0.7718789, 0.9383637, 1.0],
+ [1.2283664, 0.8916653, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.23281652, 1.0636709, 1.0],
+ [0.5478948, 0.98645896, 1.0],
+ [1.1050255, 0.9356548, 1.0],
+ [1.721518, 0.89987594, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.45712063, 1.0652657, 1.0],
+ [0.3815263, 0.98859835, 1.0],
+ [1.0404433, 0.94592875, 1.0],
+ [1.8172855, 0.90899503, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.1051946, 1.0864792, 1.0],
+ [-0.06119209, 1.0147051, 1.0],
+ [0.7248705, 0.9713246, 1.0],
+ [1.5626872, 0.92918205, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.237749, 1.127092, 1.0],
+ [-1.070423, 1.0662243, 1.0],
+ [-0.104194425, 1.0130713, 1.0],
+ [0.804536, 0.9474472, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.6953864, 1.1598293, 1.0],
+ [-2.686826, 1.106682, 1.0],
+ [-1.7835299, 1.0456588, 1.0],
+ [-0.8053292, 0.9597566, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.2643313, 1.1904461, 1.0],
+ [-4.4016128, 1.1447512, 1.0],
+ [-3.6761749, 1.0970792, 1.0],
+ [-2.1196303, 0.9963984, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.199227, 1.2223662, 1.0],
+ [-6.308516, 1.1835574, 1.0],
+ [-5.5572577, 1.15013, 1.0],
+ [-2.1184824, 1.0233642, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.357493, 1.2634833, 1.0],
+ [-8.469821, 1.2300229, 1.0],
+ [-7.221032, 1.1844004, 1.0],
+ [-0.7896582, 1.0104114, 1.0],
+ [0.0, 1.0, 1.0],
+ [-11.875645, 1.3014976, 1.0],
+ [-10.859666, 1.2672151, 1.0],
+ [-7.7761583, 1.1516356, 1.0],
+ [-0.47165853, 0.9992604, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.108668, 1.2699456, 1.0],
+ [-12.638117, 1.2220073, 1.0],
+ [-7.2701273, 1.0625613, 1.0],
+ [-0.6548264, 0.99727935, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.724136, 1.204452, 1.0],
+ [-13.639737, 1.1411229, 1.0],
+ [-5.4015455, 1.0294274, 1.0],
+ [-0.093046986, 0.99997, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.673388, 1.1604823, 1.0],
+ [-15.056745, 1.0991567, 1.0],
+ [-6.999325, 1.0093077, 1.0],
+ [-0.15357149, 0.99979734, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.081741, 1.1213652, 1.0],
+ [-15.57136, 1.0569661, 1.0],
+ [-8.844844, 0.9832494, 1.0],
+ [-0.32678962, 0.9983225, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.33926, 1.0855446, 1.0],
+ [-15.313985, 1.0192273, 1.0],
+ [-8.119184, 0.9764423, 1.0],
+ [-0.23862769, 0.9987188, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.433193, 1.0620961, 1.0],
+ [-16.90102, 0.9942711, 1.0],
+ [-9.617896, 0.9711072, 1.0],
+ [-0.2552796, 0.99904865, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.276731, 1.0457346, 1.0],
+ [-16.739477, 0.9782363, 1.0],
+ [-11.156491, 0.96119857, 1.0],
+ [-0.34167433, 0.9984216, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.084091, 1.0438286, 1.0],
+ [-15.816032, 0.984013, 1.0],
+ [-9.323119, 0.9641214, 1.0],
+ [-0.2387637, 0.9986463, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.596954, 1.0198183, 1.0],
+ [-13.655594, 0.9686297, 1.0],
+ [-7.797318, 0.955375, 1.0],
+ [-0.22518365, 0.9980864, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.301703, 1.0141279, 1.0],
+ [-12.989882, 0.93955874, 1.0],
+ [-7.9458184, 0.9391862, 1.0],
+ [-0.2038498, 0.9980101, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.792349, 1.0134387, 1.0],
+ [-14.75936, 0.9455252, 1.0],
+ [-8.760609, 0.9422921, 1.0],
+ [-0.30783197, 0.9958334, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.005386, 1.0050753, 1.0],
+ [-15.469527, 0.9674358, 1.0],
+ [-8.49741, 0.95576, 1.0],
+ [-0.41260514, 0.9953345, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.273808, 0.9787281, 1.0],
+ [-15.69969, 0.942322, 1.0],
+ [-12.234762, 0.87553686, 1.0],
+ [-2.3212836, 0.9701262, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.132599, 0.9134147, 1.0],
+ [-15.41619, 0.8991917, 1.0],
+ [-12.281813, 0.8682994, 1.0],
+ [-2.1571953, 0.970933, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.799068, 0.85166913, 1.0],
+ [-14.772821, 0.8631338, 1.0],
+ [-9.500277, 0.8921043, 1.0],
+ [-0.7092388, 0.98975027, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.442728, 0.8058451, 1.0],
+ [-13.375286, 0.8466388, 1.0],
+ [-8.537738, 0.8891218, 1.0],
+ [-0.5375653, 0.99202126, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.213923, 0.78132874, 1.0],
+ [-11.185756, 0.8387474, 1.0],
+ [-7.113949, 0.8830028, 1.0],
+ [-0.51550055, 0.98982215, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.168373, 0.7644356, 1.0],
+ [-10.289349, 0.830659, 1.0],
+ [-4.6552444, 0.8868689, 1.0],
+ [-0.362867, 0.9811054, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.229682, 0.75506926, 1.0],
+ [-8.255229, 0.8390729, 1.0],
+ [-3.1724656, 0.88942176, 1.0],
+ [-0.36211884, 0.96008885, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.780003, 0.78203535, 1.0],
+ [-4.8128314, 0.88308793, 1.0],
+ [-1.7214035, 0.9332362, 1.0],
+ [0.07604427, 0.9700093, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.1902246, 0.84770036, 1.0],
+ [-2.0365455, 0.94785786, 1.0],
+ [0.075043306, 0.98688334, 1.0],
+ [1.348575, 0.9891258, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.9187067, 0.8880616, 1.0],
+ [-0.8643225, 0.98719597, 1.0],
+ [1.677873, 1.0176314, 1.0],
+ [3.0216281, 1.0148586, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.5886059, 0.8924353, 1.0],
+ [0.30670318, 1.0308015, 1.0],
+ [2.7104156, 1.0678495, 1.0],
+ [3.735997, 1.0629262, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.084609, 0.91431457, 1.0],
+ [1.0545353, 1.082462, 1.0],
+ [2.8006148, 1.1123718, 1.0],
+ [3.2798169, 1.1044176, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.0868382, 0.88934803, 1.0],
+ [0.24395227, 1.0737534, 1.0],
+ [1.8516512, 1.1117501, 1.0],
+ [1.8256167, 1.0846347, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.303041, 0.91892815, 1.0],
+ [-0.3339426, 1.0717436, 1.0],
+ [-0.5027457, 1.1226003, 1.0],
+ [-0.55369043, 1.0714196, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.009014121, 0.99058026, 1.0],
+ [-0.30335096, 1.1036884, 1.0],
+ [-1.4451755, 1.1425995, 1.0],
+ [-1.269934, 1.0913438, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.61342883, 0.9911708, 1.0],
+ [-0.14296348, 1.1055524, 1.0],
+ [-1.336663, 1.1335871, 1.0],
+ [-1.1314107, 1.0589578, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.7491869, 0.94608873, 1.0],
+ [1.4192203, 1.0620985, 1.0],
+ [-0.24427313, 1.079302, 1.0],
+ [-1.2224454, 1.0283191, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.2000449, 0.9136502, 1.0],
+ [3.1475732, 1.0253253, 1.0],
+ [2.1547606, 1.0377923, 1.0],
+ [0.30383298, 1.0079229, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.7772436, 0.9144093, 1.0],
+ [4.162533, 1.047513, 1.0],
+ [4.2758074, 1.0457861, 1.0],
+ [3.105151, 0.9908633, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.676512, 0.9309974, 1.0],
+ [2.7957842, 1.0558958, 1.0],
+ [3.4883666, 1.0355694, 1.0],
+ [3.137294, 0.9896551, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.122498, 0.9589433, 1.0],
+ [1.543551, 1.0560359, 1.0],
+ [1.3497539, 1.0383536, 1.0],
+ [0.11099989, 1.0035237, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.580221, 0.9780591, 1.0],
+ [1.9945487, 1.0655055, 1.0],
+ [2.4221385, 1.0402309, 1.0],
+ [0.4563281, 1.0015888, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.7698708, 1.0119603, 1.0],
+ [2.4278777, 1.1139587, 1.0],
+ [3.0077763, 1.075531, 1.0],
+ [0.89499116, 1.0035012, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.788685, 1.0530056, 1.0],
+ [2.6037643, 1.1413025, 1.0],
+ [1.4215078, 1.0968238, 1.0],
+ [0.20484339, 1.0055182, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.1022477, 1.0453085, 1.0],
+ [2.2648408, 1.1255327, 1.0],
+ [3.5084808, 1.0749761, 1.0],
+ [0.8860151, 1.0050836, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.840788, 0.99767727, 1.0],
+ [2.867805, 1.0597396, 1.0],
+ [5.0557113, 1.041923, 1.0],
+ [1.229561, 1.0051966, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.0307097, 0.97708684, 1.0],
+ [4.806399, 1.047505, 1.0],
+ [1.4926684, 1.0392994, 1.0],
+ [-0.316868, 1.0058483, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.4268966, 0.97928965, 1.0],
+ [4.44971, 1.0286714, 1.0],
+ [3.9176347, 1.0229295, 1.0],
+ [0.89289904, 1.0034719, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.0285277, 0.9728169, 1.0],
+ [2.574475, 1.0172137, 1.0],
+ [3.1116145, 1.0253292, 1.0],
+ [0.8804246, 1.0046561, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.9326806, 0.9673203, 1.0],
+ [3.0191274, 0.9986364, 1.0],
+ [2.0088856, 1.0064441, 1.0],
+ [0.8739533, 1.0035771, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.88084, 0.9650284, 1.0],
+ [3.5343342, 0.9909499, 1.0],
+ [2.52684, 0.99866265, 1.0],
+ [1.0010276, 1.0015429, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.6674595, 0.9901648, 1.0],
+ [3.5274854, 1.0219513, 1.0],
+ [4.0995092, 1.0413644, 1.0],
+ [2.9521806, 1.0259401, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.1322083, 1.0172622, 1.0],
+ [4.796819, 1.0498525, 1.0],
+ [4.7010384, 1.0518665, 1.0],
+ [3.3101153, 1.0278659, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.23181, 1.0341, 1.0],
+ [4.575992, 1.0661216, 1.0],
+ [4.1811743, 1.0555745, 1.0],
+ [2.9168804, 1.0209843, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.773051, 1.0802647, 1.0],
+ [3.930077, 1.1002141, 1.0],
+ [3.9242337, 1.0825543, 1.0],
+ [3.3296824, 1.0521122, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.4754193, 0.95264477, 1.0],
+ [1.6237425, 0.9976076, 1.0],
+ [1.6940635, 1.0124187, 1.0],
+ [1.4032439, 1.0027901, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.8328916, 0.9358476, 1.0],
+ [0.5054257, 0.9516976, 1.0],
+ [0.82207125, 0.96641874, 1.0],
+ [0.867466, 0.96557885, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.107456, 0.9178314, 1.0],
+ [-0.11612688, 0.9198909, 1.0],
+ [-0.13824676, 0.9118656, 1.0],
+ [-0.0028314982, 0.9240737, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.2662044, 0.9126919, 1.0],
+ [0.20858723, 0.91308266, 1.0],
+ [-0.74156207, 0.8892692, 1.0],
+ [-0.96805483, 0.8813321, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.5846848, 0.8996693, 1.0],
+ [0.3940221, 0.9022535, 1.0],
+ [-1.0160103, 0.8844732, 1.0],
+ [-1.9547969, 0.869879, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.8889635, 0.8861786, 1.0],
+ [0.46730384, 0.8902896, 1.0],
+ [-0.8314566, 0.8740593, 1.0],
+ [-3.0556507, 0.85314214, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.786228, 0.87517726, 1.0],
+ [-0.25766176, 0.8843216, 1.0],
+ [-2.6286218, 0.87064177, 1.0],
+ [-6.709825, 0.8475933, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1630952, 0.8485678, 1.0],
+ [-1.315835, 0.88361984, 1.0],
+ [-4.564759, 0.8761655, 1.0],
+ [-7.6052637, 0.84420526, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.65808207, 0.8359365, 1.0],
+ [-1.8502634, 0.871772, 1.0],
+ [-4.9014773, 0.88665104, 1.0],
+ [-6.9419703, 0.85948473, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.98892176, 0.85553217, 1.0],
+ [-1.2973236, 0.88800645, 1.0],
+ [-3.9453318, 0.8810778, 1.0],
+ [-5.0194716, 0.83465356, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4077076, 0.8883648, 1.0],
+ [-0.20262001, 0.89855146, 1.0],
+ [-2.0743167, 0.87382066, 1.0],
+ [-2.846246, 0.8157335, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.6395962, 0.92741174, 1.0],
+ [0.7942348, 0.92847836, 1.0],
+ [-0.10936606, 0.878342, 1.0],
+ [-0.28163967, 0.78697026, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.9163837, 0.9670593, 1.0],
+ [1.1606812, 0.94956505, 1.0],
+ [0.48494318, 0.86900467, 1.0],
+ [0.48800853, 0.77839893, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.9139721, 0.9643932, 1.0],
+ [0.70910484, 0.9468291, 1.0],
+ [0.70515186, 0.87377256, 1.0],
+ [0.7435633, 0.7895271, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.021806521, 0.9712936, 1.0],
+ [0.09517821, 0.94683015, 1.0],
+ [0.48132607, 0.8953816, 1.0],
+ [0.7116937, 0.82242763, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.79750377, 0.9793866, 1.0],
+ [-0.08199019, 0.933896, 1.0],
+ [0.54560524, 0.8938053, 1.0],
+ [0.91736573, 0.8542098, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.93426365, 0.9845968, 1.0],
+ [0.15814182, 0.92910755, 1.0],
+ [0.72163683, 0.89656097, 1.0],
+ [1.2182864, 0.86374855, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.0705345, 0.98736614, 1.0],
+ [-0.011547462, 0.9383716, 1.0],
+ [0.6337991, 0.90952563, 1.0],
+ [1.2993928, 0.87798977, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.6194162, 1.0018165, 1.0],
+ [-0.49097732, 0.9606046, 1.0],
+ [0.2521988, 0.9307922, 1.0],
+ [0.9645536, 0.8885482, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.80128, 1.0367984, 1.0],
+ [-1.5576428, 0.9923525, 1.0],
+ [-0.66330856, 0.9425935, 1.0],
+ [0.1666689, 0.83448035, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.1835513, 1.0688453, 1.0],
+ [-3.0496554, 1.0094827, 1.0],
+ [-2.0194643, 0.9350747, 1.0],
+ [-0.84885937, 0.81810623, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.472717, 1.096382, 1.0],
+ [-4.410626, 1.0342624, 1.0],
+ [-3.3867917, 0.9620032, 1.0],
+ [-1.1987106, 0.92667925, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.2029138, 1.1229113, 1.0],
+ [-5.953019, 1.0709114, 1.0],
+ [-3.932608, 1.0097591, 1.0],
+ [-0.7465921, 0.9776096, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.683279, 1.1355549, 1.0],
+ [-7.128312, 1.0855685, 1.0],
+ [-3.1864233, 1.005719, 1.0],
+ [-0.26363567, 0.99284154, 1.0],
+ [0.0, 1.0, 1.0],
+ [-10.163454, 1.1477863, 1.0],
+ [-7.8144236, 1.0853112, 1.0],
+ [-1.8801234, 1.0063028, 1.0],
+ [-0.08024162, 0.9992318, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.115101, 1.1146804, 1.0],
+ [-8.96621, 1.0459248, 1.0],
+ [-1.6161847, 0.9987317, 1.0],
+ [-0.071704574, 0.99979985, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.706088, 1.0729254, 1.0],
+ [-8.66254, 1.008506, 1.0],
+ [-1.0726118, 0.99519026, 1.0],
+ [-0.013879563, 0.999924, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.639081, 1.0313063, 1.0],
+ [-8.5867815, 0.9870387, 1.0],
+ [-1.1318198, 0.9935133, 1.0],
+ [-0.014718597, 0.99989676, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.590664, 1.0171928, 1.0],
+ [-7.5184603, 0.98305726, 1.0],
+ [-0.9713564, 0.9937742, 1.0],
+ [-0.017022038, 0.9998613, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.854863, 1.0014843, 1.0],
+ [-7.721969, 0.9806506, 1.0],
+ [-0.77561605, 0.99587494, 1.0],
+ [-0.009547515, 0.9999292, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.060616, 0.98336315, 1.0],
+ [-8.691673, 0.94924486, 1.0],
+ [-1.2617085, 0.9861989, 1.0],
+ [-0.017596014, 0.99978256, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.874084, 0.9567539, 1.0],
+ [-9.545912, 0.9260754, 1.0],
+ [-2.5305426, 0.97063804, 1.0],
+ [-0.05151436, 0.99930036, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.672559, 0.9840259, 1.0],
+ [-8.942783, 0.9562326, 1.0],
+ [-1.7533314, 0.983495, 1.0],
+ [-0.031896364, 0.99963516, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.389929, 0.9684237, 1.0],
+ [-7.423202, 0.9658616, 1.0],
+ [-1.1646813, 0.9892395, 1.0],
+ [-0.019982396, 0.9997543, 1.0],
+ [0.0, 1.0, 1.0],
+ [-11.921755, 0.9305603, 1.0],
+ [-8.221304, 0.9161024, 1.0],
+ [-2.065746, 0.9699598, 1.0],
+ [-0.05210479, 0.999025, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.236582, 0.92396444, 1.0],
+ [-8.276435, 0.9221221, 1.0],
+ [-3.3240492, 0.9396533, 1.0],
+ [-0.477805, 0.9889453, 1.0],
+ [0.0, 1.0, 1.0],
+ [-11.884224, 0.9080763, 1.0],
+ [-6.9323835, 0.9483917, 1.0],
+ [-2.8353944, 0.9600339, 1.0],
+ [-0.33724767, 0.9937283, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.285868, 0.8697298, 1.0],
+ [-7.293664, 0.9262688, 1.0],
+ [-2.6733654, 0.9643648, 1.0],
+ [-0.293086, 0.9961857, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.295206, 0.8048045, 1.0],
+ [-9.883155, 0.8632042, 1.0],
+ [-3.227856, 0.95656085, 1.0],
+ [-0.23363292, 0.9969017, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.586395, 0.75223583, 1.0],
+ [-11.311147, 0.81547827, 1.0],
+ [-2.238798, 0.96711767, 1.0],
+ [-0.07408935, 0.998945, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.474079, 0.7245708, 1.0],
+ [-11.533241, 0.7828741, 1.0],
+ [-2.736401, 0.9435411, 1.0],
+ [-0.044651125, 0.9990903, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.674288, 0.71236974, 1.0],
+ [-9.81334, 0.7972127, 1.0],
+ [-3.45957, 0.9197005, 1.0],
+ [-0.07574147, 0.9975253, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.7195, 0.69227666, 1.0],
+ [-8.596455, 0.7779914, 1.0],
+ [-2.3802116, 0.8463873, 1.0],
+ [-0.35825908, 0.95749724, 1.0],
+ [0.0, 1.0, 1.0],
+ [-10.18446, 0.6873084, 1.0],
+ [-6.0542045, 0.7841001, 1.0],
+ [-2.1212237, 0.8281544, 1.0],
+ [-0.7646297, 0.9053229, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.1229663, 0.74606425, 1.0],
+ [-3.4445412, 0.8351193, 1.0],
+ [-1.6135985, 0.87599313, 1.0],
+ [-0.2056776, 0.9368623, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.7523043, 0.8158876, 1.0],
+ [-1.8315514, 0.894766, 1.0],
+ [-0.27596512, 0.93012476, 1.0],
+ [1.0396978, 0.9575369, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.7688127, 0.8445321, 1.0],
+ [-0.50951934, 0.9420834, 1.0],
+ [1.4284167, 0.9780485, 1.0],
+ [2.5512972, 0.9817148, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.1246834, 0.8524671, 1.0],
+ [0.29334772, 0.96627426, 1.0],
+ [2.560665, 1.0186502, 1.0],
+ [3.3895814, 1.0229484, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3665168, 0.8436846, 1.0],
+ [1.2660046, 1.012508, 1.0],
+ [2.7319925, 1.0737249, 1.0],
+ [3.0757403, 1.0803311, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.5887472, 0.81296784, 1.0],
+ [0.699834, 0.98561674, 1.0],
+ [1.4461241, 1.0658891, 1.0],
+ [1.3923068, 1.0570195, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.096663, 0.8274317, 1.0],
+ [0.40805075, 0.98812795, 1.0],
+ [-0.045402966, 1.0472325, 1.0],
+ [-0.06040587, 1.0157863, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.9790232, 0.87913215, 1.0],
+ [0.37345043, 1.0132964, 1.0],
+ [-0.79287934, 1.0823095, 1.0],
+ [-0.34175304, 1.0432074, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.5627797, 0.87573963, 1.0],
+ [0.6304921, 1.0118407, 1.0],
+ [-0.9373828, 1.0851029, 1.0],
+ [-0.46959534, 1.031097, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.7361374, 0.8472772, 1.0],
+ [1.9630458, 0.98144615, 1.0],
+ [-0.21733184, 1.0450604, 1.0],
+ [-0.49014735, 1.0128133, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.3971963, 0.8324688, 1.0],
+ [2.660751, 0.9585303, 1.0],
+ [0.16532291, 1.0232745, 1.0],
+ [-0.14636081, 1.0036846, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.0258904, 0.8367818, 1.0],
+ [2.8924615, 0.9749669, 1.0],
+ [1.7615348, 1.0238429, 1.0],
+ [0.81233835, 0.9984574, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.2452407, 0.85815537, 1.0],
+ [2.6645274, 0.9761913, 1.0],
+ [1.4570304, 1.0139804, 1.0],
+ [1.0984119, 0.997529, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.93689, 0.8569606, 1.0],
+ [3.1328692, 0.9457437, 1.0],
+ [0.033365987, 1.0082009, 1.0],
+ [-0.05564574, 1.0016743, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.0655975, 0.8770879, 1.0],
+ [2.174103, 0.9875497, 1.0],
+ [0.2545323, 1.0086097, 1.0],
+ [0.022092488, 1.000235, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.4488134, 0.9094009, 1.0],
+ [1.7526729, 1.0061622, 1.0],
+ [0.42274538, 1.0103294, 1.0],
+ [0.051791485, 1.0002624, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.1548405, 0.95060486, 1.0],
+ [1.8016412, 1.0232372, 1.0],
+ [0.20820145, 1.0119417, 1.0],
+ [0.027733212, 1.0003631, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.4379053, 0.94287974, 1.0],
+ [1.6865833, 1.0055557, 1.0],
+ [0.5884739, 1.0089288, 1.0],
+ [0.10115034, 1.0003462, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.8398051, 0.90459824, 1.0],
+ [1.1598605, 0.9777082, 1.0],
+ [0.77112013, 1.0046481, 1.0],
+ [0.15015621, 1.0003301, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.6665335, 0.91051537, 1.0],
+ [3.33516, 0.98394734, 1.0],
+ [0.32077682, 1.0035998, 1.0],
+ [-0.005901165, 1.00032, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.3764243, 0.9138666, 1.0],
+ [1.7997692, 0.97097856, 1.0],
+ [0.87327296, 0.9996552, 1.0],
+ [0.14200021, 1.0003701, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.8900363, 0.8881567, 1.0],
+ [-0.016681926, 0.9412745, 1.0],
+ [1.2008874, 0.9925353, 1.0],
+ [0.2819057, 1.0004404, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.0338037, 0.8888064, 1.0],
+ [0.49603212, 0.92353123, 1.0],
+ [0.344872, 0.9546143, 1.0],
+ [0.23514952, 0.99573493, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.2889633, 0.8941952, 1.0],
+ [0.12481757, 0.90704453, 1.0],
+ [-0.89585435, 0.91424674, 1.0],
+ [-0.24134572, 0.9783999, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.7199328, 0.9107902, 1.0],
+ [0.096942216, 0.9292534, 1.0],
+ [-0.3109405, 0.9336825, 1.0],
+ [-0.33917436, 0.96199673, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.5014708, 0.9283932, 1.0],
+ [1.2909425, 0.95882636, 1.0],
+ [0.26298514, 0.95131576, 1.0],
+ [-0.69164455, 0.9573067, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.5789986, 0.93125904, 1.0],
+ [2.0657613, 0.9823736, 1.0],
+ [1.2339572, 0.9823017, 1.0],
+ [0.035981927, 0.9774156, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.2489352, 0.947897, 1.0],
+ [2.3317747, 1.0011578, 1.0],
+ [1.9685928, 1.009779, 1.0],
+ [1.1930741, 1.0035145, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.41370514, 0.86071473, 1.0],
+ [-0.63482434, 0.8487081, 1.0],
+ [-0.96838796, 0.8932299, 1.0],
+ [-0.16537139, 0.98455006, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7274317, 0.8435787, 1.0],
+ [-0.7413707, 0.8246945, 1.0],
+ [-0.6780908, 0.84962696, 1.0],
+ [-0.03611998, 0.9278284, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.025755988, 0.7956887, 1.0],
+ [-0.7080098, 0.7706736, 1.0],
+ [-0.00667306, 0.80417067, 1.0],
+ [1.2645549, 0.8267063, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.844674, 0.77885765, 1.0],
+ [1.358191, 0.74993426, 1.0],
+ [1.8842225, 0.771222, 1.0],
+ [3.6828177, 0.75069827, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.147642, 0.7875774, 1.0],
+ [1.6485577, 0.7667687, 1.0],
+ [2.232393, 0.75781333, 1.0],
+ [3.8045251, 0.73986065, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.1638684, 0.78218484, 1.0],
+ [1.5419188, 0.7789207, 1.0],
+ [1.5175326, 0.76315284, 1.0],
+ [2.9210594, 0.7496939, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.6404829, 0.76603454, 1.0],
+ [0.3848709, 0.76939934, 1.0],
+ [-0.5066958, 0.7645504, 1.0],
+ [0.97468865, 0.73737776, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.65317655, 0.7593511, 1.0],
+ [-0.66478103, 0.7523295, 1.0],
+ [0.07674445, 0.7192325, 1.0],
+ [0.11743816, 0.7180049, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.07553896, 0.75588036, 1.0],
+ [-0.7599607, 0.7307774, 1.0],
+ [0.7937273, 0.702063, 1.0],
+ [1.210313, 0.71042067, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.6146166, 0.7519787, 1.0],
+ [-0.11158234, 0.7340125, 1.0],
+ [1.8024358, 0.6884776, 1.0],
+ [2.2258494, 0.6841371, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7284711, 0.81019914, 1.0],
+ [0.0031727236, 0.7846696, 1.0],
+ [2.4180965, 0.6961533, 1.0],
+ [3.2767165, 0.68659955, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7535696, 0.863907, 1.0],
+ [0.36523274, 0.8374792, 1.0],
+ [1.2830526, 0.791074, 1.0],
+ [1.2069559, 0.8067631, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3411092, 0.88778365, 1.0],
+ [0.7838614, 0.86403745, 1.0],
+ [1.0240868, 0.81532073, 1.0],
+ [0.9583133, 0.7903267, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.12238092, 0.85272485, 1.0],
+ [0.07477853, 0.8615265, 1.0],
+ [0.6215121, 0.80922437, 1.0],
+ [0.8200295, 0.7752551, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.82122755, 0.87185943, 1.0],
+ [-0.2457037, 0.8873393, 1.0],
+ [0.26640493, 0.85581523, 1.0],
+ [0.45366037, 0.81068766, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3295536, 0.9104388, 1.0],
+ [-0.5824124, 0.87757504, 1.0],
+ [0.006645972, 0.8408192, 1.0],
+ [0.28378436, 0.8216209, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7040519, 0.9045666, 1.0],
+ [-0.47549373, 0.86758405, 1.0],
+ [0.053788297, 0.83981687, 1.0],
+ [0.40476194, 0.81218266, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.5469458, 0.9090261, 1.0],
+ [-0.5149614, 0.88305897, 1.0],
+ [0.04461902, 0.8536314, 1.0],
+ [0.47533098, 0.8176997, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9928781, 0.9177581, 1.0],
+ [-1.1592609, 0.8999771, 1.0],
+ [-0.47069812, 0.8662861, 1.0],
+ [0.24946758, 0.8074272, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.8105757, 0.9419143, 1.0],
+ [-1.9706584, 0.9134377, 1.0],
+ [-1.0593511, 0.8243537, 1.0],
+ [-0.37524036, 0.77119154, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.1471803, 0.9753915, 1.0],
+ [-2.3176653, 0.91906554, 1.0],
+ [-1.3088802, 0.86219937, 1.0],
+ [-0.58739233, 0.86786795, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.94802, 0.97012186, 1.0],
+ [-2.1835663, 0.95009077, 1.0],
+ [-1.0128062, 0.9544606, 1.0],
+ [-0.15606245, 0.98380494, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.345666, 0.98280185, 1.0],
+ [-1.4991432, 0.95262855, 1.0],
+ [-0.4551724, 0.9883049, 1.0],
+ [-0.03270482, 0.9987078, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.8461266, 0.9927924, 1.0],
+ [-1.276193, 0.98380685, 1.0],
+ [-0.1276723, 0.99596465, 1.0],
+ [-0.006702845, 0.9997529, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.713865, 0.98152876, 1.0],
+ [-0.75546634, 0.9879916, 1.0],
+ [-0.049746733, 0.9991844, 1.0],
+ [-0.0007843447, 0.9999856, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.2784758, 0.98056763, 1.0],
+ [-1.0423217, 0.98997813, 1.0],
+ [-0.050068997, 0.9994019, 1.0],
+ [-0.00034191704, 0.9999971, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.9297116, 0.96755266, 1.0],
+ [-0.5981317, 0.9923545, 1.0],
+ [-0.017654736, 0.999798, 1.0],
+ [-9.46762e-05, 0.9999992, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.1329167, 0.9680987, 1.0],
+ [-0.42209783, 0.9959619, 1.0],
+ [-0.012668612, 0.9998711, 1.0],
+ [-8.0989346e-05, 0.9999992, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.681753, 0.96301955, 1.0],
+ [-0.42579386, 0.9962166, 1.0],
+ [-0.00814088, 0.9999096, 1.0],
+ [-4.4159344e-05, 0.9999995, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.2585168, 0.9649381, 1.0],
+ [-0.30428812, 0.9962269, 1.0],
+ [-0.005310869, 0.999927, 1.0],
+ [-2.5505675e-05, 0.99999964, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.2021565, 0.9749354, 1.0],
+ [-0.3944129, 0.9904369, 1.0],
+ [-0.025527034, 0.9992587, 1.0],
+ [-0.00025198492, 0.9999939, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.929304, 0.971067, 1.0],
+ [-0.45261085, 0.99142736, 1.0],
+ [-0.05011053, 0.9990594, 1.0],
+ [-0.0008144145, 0.99998575, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.7514565, 0.9707775, 1.0],
+ [-0.364937, 0.9964542, 1.0],
+ [-0.020018931, 0.9997449, 1.0],
+ [-0.00028382998, 0.9999959, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.7956183, 0.9632996, 1.0],
+ [-0.42287976, 0.9969505, 1.0],
+ [-0.013087578, 0.9999144, 1.0],
+ [-8.082379e-05, 0.99999905, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.4113169, 0.9547617, 1.0],
+ [-0.33804947, 0.9945204, 1.0],
+ [-0.026301317, 0.9995303, 1.0],
+ [-0.0012762488, 0.99997115, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.6816075, 0.95678437, 1.0],
+ [-0.44680336, 0.99332184, 1.0],
+ [-0.13794503, 0.99693584, 1.0],
+ [-0.024087088, 0.9994248, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.1641667, 0.9557707, 1.0],
+ [-0.37171954, 0.99482054, 1.0],
+ [-0.076554835, 0.99849296, 1.0],
+ [-0.012065996, 0.9997304, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.1045394, 0.91970074, 1.0],
+ [-0.6387288, 0.9893497, 1.0],
+ [-0.016694926, 0.9997221, 1.0],
+ [-0.0008751128, 0.99998766, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.9304733, 0.85944414, 1.0],
+ [-1.2397609, 0.9733297, 1.0],
+ [-0.039723754, 0.99928564, 1.0],
+ [-0.0009279736, 0.9999874, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.17317, 0.7962485, 1.0],
+ [-2.5968554, 0.9279736, 1.0],
+ [-0.06445126, 0.9980414, 1.0],
+ [-0.00037242688, 0.99999243, 1.0],
+ [0.0, 1.0, 1.0],
+ [-10.704245, 0.7364805, 1.0],
+ [-5.6246877, 0.8459256, 1.0],
+ [-0.26169717, 0.99058115, 1.0],
+ [-0.0014548219, 0.9999466, 1.0],
+ [0.0, 1.0, 1.0],
+ [-10.675443, 0.68334174, 1.0],
+ [-4.872382, 0.8478102, 1.0],
+ [-0.4881562, 0.98677355, 1.0],
+ [-0.009578875, 0.99971837, 1.0],
+ [0.0, 1.0, 1.0],
+ [-10.985089, 0.61614233, 1.0],
+ [-6.222056, 0.75582373, 1.0],
+ [-1.2719929, 0.92699444, 1.0],
+ [-0.07923371, 0.99300826, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.835739, 0.6035176, 1.0],
+ [-6.378802, 0.7114187, 1.0],
+ [-2.099836, 0.85796636, 1.0],
+ [-0.28848997, 0.9751244, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.6503634, 0.67487407, 1.0],
+ [-5.500912, 0.7487064, 1.0],
+ [-4.2971406, 0.8258655, 1.0],
+ [-0.6571109, 0.96886605, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.7830634, 0.73914504, 1.0],
+ [-4.141989, 0.79455334, 1.0],
+ [-5.006245, 0.8116664, 1.0],
+ [-1.3780211, 0.94300467, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.9277482, 0.75783825, 1.0],
+ [-2.659795, 0.83125865, 1.0],
+ [-2.5174627, 0.85422933, 1.0],
+ [-1.3893734, 0.9027223, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.7880778, 0.79374677, 1.0],
+ [-1.5678242, 0.8652207, 1.0],
+ [0.2077058, 0.92604816, 1.0],
+ [0.5667539, 0.94809216, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7917917, 0.7941517, 1.0],
+ [-0.65741813, 0.895106, 1.0],
+ [0.75534123, 0.9829742, 1.0],
+ [0.4768409, 0.98621637, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.1052668, 0.73317593, 1.0],
+ [-0.6049394, 0.8575412, 1.0],
+ [-0.15478949, 0.99293196, 1.0],
+ [-0.25519162, 0.99671155, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3859553, 0.7583256, 1.0],
+ [0.3529042, 0.9013461, 1.0],
+ [-0.09280269, 0.99014723, 1.0],
+ [-0.19439623, 0.9756913, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.662148, 0.78581506, 1.0],
+ [1.3001701, 0.8985178, 1.0],
+ [0.46722722, 0.9815502, 1.0],
+ [0.23440441, 0.98131305, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.7134244, 0.7862562, 1.0],
+ [1.8330814, 0.8773475, 1.0],
+ [0.75808954, 0.9856246, 1.0],
+ [0.15871423, 0.9977693, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.707434, 0.78545547, 1.0],
+ [2.0999856, 0.88736033, 1.0],
+ [0.21350783, 0.9968301, 1.0],
+ [0.0005532017, 1.0001154, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.2265773, 0.7976756, 1.0],
+ [2.41513, 0.9033128, 1.0],
+ [0.098965846, 0.99737674, 1.0],
+ [-0.0019273323, 1.0000342, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.586104, 0.8242546, 1.0],
+ [1.1944336, 0.94521993, 1.0],
+ [0.018398829, 0.999946, 1.0],
+ [0.0061938893, 1.00001, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.3312273, 0.85412824, 1.0],
+ [0.8018384, 0.96504784, 1.0],
+ [-0.038536284, 1.0002738, 1.0],
+ [0.0041213674, 1.0000454, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.5602465, 0.86023563, 1.0],
+ [0.8536984, 0.97106135, 1.0],
+ [-0.06325424, 1.0002065, 1.0],
+ [-0.009787895, 1.0001173, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.319309, 0.88970965, 1.0],
+ [0.4574435, 0.98576313, 1.0],
+ [0.0016855623, 0.9999831, 1.0],
+ [-0.000406475, 1.0000062, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.0391257, 0.92719066, 1.0],
+ [-0.11533654, 0.9924606, 1.0],
+ [-0.022894483, 1.0000801, 1.0],
+ [-3.8161572e-05, 1.0000013, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.221341, 0.9522132, 1.0],
+ [0.16617404, 0.99605083, 1.0],
+ [-0.0031928865, 0.999991, 1.0],
+ [1.2969534e-05, 1.0000004, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.2095737, 0.94623184, 1.0],
+ [0.2084119, 0.9924171, 1.0],
+ [-0.0017147859, 0.999864, 1.0],
+ [0.00015271064, 1.0000001, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.7305284, 0.92747825, 1.0],
+ [0.26084626, 0.99035066, 1.0],
+ [0.002197185, 0.9998796, 1.0],
+ [0.0002519184, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.83777, 0.9303522, 1.0],
+ [0.49865678, 0.99259496, 1.0],
+ [0.010380797, 0.99993306, 1.0],
+ [1.543077e-05, 1.0000001, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.6540057, 0.93718725, 1.0],
+ [-0.056169994, 0.98687667, 1.0],
+ [-0.009431662, 0.9991736, 1.0],
+ [0.00055399764, 0.9999875, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.219968, 0.9170335, 1.0],
+ [-1.04431, 0.9552193, 1.0],
+ [-0.7334884, 0.9844318, 1.0],
+ [-0.067632034, 0.9988193, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3329756, 0.9103736, 1.0],
+ [-4.453365, 0.87807673, 1.0],
+ [-7.3110423, 0.86146367, 1.0],
+ [-1.0087034, 0.9803414, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.96455824, 0.8981043, 1.0],
+ [-5.200631, 0.8460088, 1.0],
+ [-11.297041, 0.7854038, 1.0],
+ [-2.6635964, 0.9481161, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.67527205, 0.9010675, 1.0],
+ [-6.0442433, 0.8281746, 1.0],
+ [-12.542688, 0.7473031, 1.0],
+ [-5.324665, 0.8921913, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.2026789, 0.89397043, 1.0],
+ [-4.445598, 0.84913737, 1.0],
+ [-11.919539, 0.74464583, 1.0],
+ [-6.795066, 0.85686606, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.6662235, 0.8867006, 1.0],
+ [-3.937991, 0.83842826, 1.0],
+ [-13.422844, 0.71948135, 1.0],
+ [-8.835538, 0.8229743, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7172698, 0.8660123, 1.0],
+ [-0.94286054, 0.8551379, 1.0],
+ [-5.286918, 0.83392274, 1.0],
+ [-2.4993267, 0.9417325, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.0017951994, 0.9914302, 1.0],
+ [-0.06927332, 0.9921463, 1.0],
+ [-0.057490297, 0.9959994, 1.0],
+ [-0.008928111, 0.99963653, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.041254885, 0.98150927, 1.0],
+ [-0.1306019, 0.97009796, 1.0],
+ [-0.048590906, 0.9860053, 1.0],
+ [0.0030063766, 0.99852145, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.112703465, 0.95625734, 1.0],
+ [-0.2643623, 0.9157722, 1.0],
+ [0.17177379, 0.9519122, 1.0],
+ [0.89937246, 0.95642823, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.29111728, 0.94092214, 1.0],
+ [0.3868302, 0.90032035, 1.0],
+ [3.1862643, 0.84815085, 1.0],
+ [6.1715746, 0.7603379, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.538952, 0.9446369, 1.0],
+ [0.83218, 0.90219474, 1.0],
+ [3.4132833, 0.844821, 1.0],
+ [6.4263897, 0.7552901, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.90486366, 0.9366256, 1.0],
+ [1.3550385, 0.9041044, 1.0],
+ [4.7555075, 0.83666027, 1.0],
+ [9.834919, 0.7355414, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.749637, 0.92996854, 1.0],
+ [1.2060188, 0.9036133, 1.0],
+ [10.243389, 0.71855605, 1.0],
+ [15.236945, 0.6319401, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.38263044, 0.9178865, 1.0],
+ [1.6851017, 0.8404756, 1.0],
+ [6.062546, 0.71273714, 1.0],
+ [7.5150943, 0.7055973, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.2073275, 0.8944352, 1.0],
+ [1.694259, 0.7952898, 1.0],
+ [4.963602, 0.71330947, 1.0],
+ [4.4856033, 0.76623136, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.42934892, 0.91428727, 1.0],
+ [1.7023742, 0.81033534, 1.0],
+ [4.653338, 0.67880553, 1.0],
+ [4.1607633, 0.7217924, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7662586, 0.92551315, 1.0],
+ [1.281609, 0.8594126, 1.0],
+ [4.2112913, 0.7075525, 1.0],
+ [3.2996652, 0.7721644, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.36315376, 0.96427274, 1.0],
+ [0.17142291, 0.9671226, 1.0],
+ [0.3704626, 0.96229166, 1.0],
+ [0.16843563, 0.98521745, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.14203277, 0.980844, 1.0],
+ [0.08888949, 0.98635364, 1.0],
+ [0.08142525, 0.99080217, 1.0],
+ [0.04689812, 0.99329925, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.041995242, 0.97444767, 1.0],
+ [-0.015486327, 0.9832361, 1.0],
+ [0.025345497, 0.9886648, 1.0],
+ [0.061056096, 0.9832061, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.06349996, 0.9847081, 1.0],
+ [-0.010158759, 0.993606, 1.0],
+ [0.0070079323, 0.9965746, 1.0],
+ [0.012292187, 0.9953988, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.02714656, 0.99495846, 1.0],
+ [-0.0047707465, 0.995372, 1.0],
+ [-0.011022978, 0.9921308, 1.0],
+ [-0.0004924384, 0.99648327, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.065880194, 0.99450415, 1.0],
+ [-0.041692242, 0.99075663, 1.0],
+ [-0.022810183, 0.9813757, 1.0],
+ [0.0066275946, 0.9910071, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.051800754, 0.9933403, 1.0],
+ [-0.031969808, 0.9909193, 1.0],
+ [-0.0138383275, 0.98399764, 1.0],
+ [0.0023257341, 0.99193186, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.064492755, 0.99565995, 1.0],
+ [-0.056850635, 0.993179, 1.0],
+ [-0.03568528, 0.9925555, 1.0],
+ [-0.006500367, 0.9941837, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.076604635, 0.9975212, 1.0],
+ [-0.04835303, 0.99697053, 1.0],
+ [-0.05276575, 0.9912819, 1.0],
+ [-0.048627503, 0.98763776, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.07185075, 0.99944353, 1.0],
+ [-0.049465895, 0.99844307, 1.0],
+ [-0.03390795, 0.9961937, 1.0],
+ [-0.01572451, 0.99718726, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.06696204, 0.99883705, 1.0],
+ [-0.059485506, 0.99909484, 1.0],
+ [-0.020033833, 0.99961084, 1.0],
+ [-0.001215999, 0.9999084, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.06329192, 0.9984661, 1.0],
+ [-0.030843416, 0.99811184, 1.0],
+ [-0.010421576, 0.99985075, 1.0],
+ [-0.000277232, 0.99999887, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.09967728, 0.9996282, 1.0],
+ [-0.024422867, 0.9996509, 1.0],
+ [-0.0013948235, 0.9999416, 1.0],
+ [-1.7647946e-05, 0.99999917, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.061887965, 0.9996889, 1.0],
+ [-0.0055153067, 0.9999299, 1.0],
+ [-0.00014356381, 0.9999968, 1.0],
+ [-7.3728444e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.06313205, 0.9991679, 1.0],
+ [-0.0105344625, 0.99976623, 1.0],
+ [-0.00022694842, 0.99999404, 1.0],
+ [-6.2425016e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.07309622, 0.998452, 1.0],
+ [-0.010137941, 0.99966383, 1.0],
+ [-0.00015650428, 0.999994, 1.0],
+ [-2.0530616e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.12139271, 0.99886787, 1.0],
+ [-0.006217619, 0.9999299, 1.0],
+ [-2.767876e-05, 0.9999996, 1.0],
+ [-2.8060315e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.11297469, 0.9986093, 1.0],
+ [-0.005666369, 0.99993217, 1.0],
+ [-2.3441991e-05, 0.99999976, 1.0],
+ [-1.6374731e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.11892667, 0.9984859, 1.0],
+ [-0.0054892004, 0.9999281, 1.0],
+ [-1.8891958e-05, 0.9999997, 1.0],
+ [-1.42802365e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.063609995, 0.9991952, 1.0],
+ [-0.003612312, 0.9999425, 1.0],
+ [-5.6865858e-05, 0.99999845, 1.0],
+ [-2.8125933e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.03687637, 0.99945694, 1.0],
+ [-0.0022629017, 0.99996173, 1.0],
+ [-4.7338475e-05, 0.9999989, 1.0],
+ [-3.8873702e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.066277154, 0.9989287, 1.0],
+ [-0.0041808677, 0.9999447, 1.0],
+ [-2.6149986e-05, 0.9999997, 1.0],
+ [-7.8076916e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.18022063, 0.99584043, 1.0],
+ [-0.009360226, 0.99979895, 1.0],
+ [-4.573966e-05, 0.99999934, 1.0],
+ [-4.968432e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.057497833, 0.9985912, 1.0],
+ [-0.0030033966, 0.99992764, 1.0],
+ [-2.1283115e-05, 0.9999996, 1.0],
+ [-7.399287e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.02698668, 0.99940234, 1.0],
+ [-0.0018560152, 0.9999619, 1.0],
+ [-0.00011015628, 0.9999977, 1.0],
+ [-1.6312291e-05, 0.9999996, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.020353582, 0.99962854, 1.0],
+ [-0.0012829889, 0.99997663, 1.0],
+ [-4.823795e-05, 0.999999, 1.0],
+ [-7.425042e-06, 0.9999998, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.046696484, 0.9990762, 1.0],
+ [-0.0027994127, 0.999945, 1.0],
+ [-1.47203e-05, 0.9999997, 1.0],
+ [-1.4775334e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.11455002, 0.9970088, 1.0],
+ [-0.008082145, 0.9997811, 1.0],
+ [-5.997455e-05, 0.9999983, 1.0],
+ [-2.153083e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.20616151, 0.9932033, 1.0],
+ [-0.026683627, 0.9989487, 1.0],
+ [-0.0006210741, 0.9999699, 1.0],
+ [-1.9223974e-06, 0.9999999, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.42821977, 0.98413795, 1.0],
+ [-0.09130513, 0.9955624, 1.0],
+ [-0.0039294222, 0.99976504, 1.0],
+ [-1.938189e-05, 0.9999988, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.737228, 0.9656451, 1.0],
+ [-0.11107813, 0.99402, 1.0],
+ [-0.005023841, 0.9997979, 1.0],
+ [-7.540838e-05, 0.9999977, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.0423653, 0.89530706, 1.0],
+ [-0.39499432, 0.9775829, 1.0],
+ [-0.021451676, 0.9990429, 1.0],
+ [-0.00062327104, 0.9999673, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.7882614, 0.7621558, 1.0],
+ [-1.4943185, 0.919121, 1.0],
+ [-0.26310703, 0.9902079, 1.0],
+ [-0.026021129, 0.9991246, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.967163, 0.70525557, 1.0],
+ [-6.2708335, 0.7825963, 1.0],
+ [-3.0123074, 0.9135249, 1.0],
+ [-0.16591884, 0.99503696, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.747682, 0.693125, 1.0],
+ [-11.015906, 0.6835952, 1.0],
+ [-10.588246, 0.7426725, 1.0],
+ [-1.4011235, 0.96407425, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.8046284, 0.6869158, 1.0],
+ [-12.226663, 0.67317563, 1.0],
+ [-14.401511, 0.68768495, 1.0],
+ [-5.656612, 0.8701306, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.2980156, 0.7554591, 1.0],
+ [-8.451796, 0.7457393, 1.0],
+ [-10.891963, 0.76741225, 1.0],
+ [-4.9737005, 0.8988167, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.7862368, 0.7663066, 1.0],
+ [-6.968383, 0.74598277, 1.0],
+ [-7.1698184, 0.82501894, 1.0],
+ [-3.5572019, 0.9294954, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.0710254, 0.80158305, 1.0],
+ [-2.644557, 0.82318443, 1.0],
+ [-2.829198, 0.9057661, 1.0],
+ [-3.1149986, 0.90927655, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.5783251, 0.8887635, 1.0],
+ [0.14086528, 0.92088306, 1.0],
+ [-1.3454764, 0.91390127, 1.0],
+ [-2.1663988, 0.89477795, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.90967906, 0.9097722, 1.0],
+ [0.40966874, 0.96211725, 1.0],
+ [0.012563121, 0.9758351, 1.0],
+ [-0.12836997, 0.9668173, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7635332, 0.9177408, 1.0],
+ [0.26205018, 0.9728322, 1.0],
+ [0.017903045, 0.99900806, 1.0],
+ [0.0027794365, 0.9998005, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.70256805, 0.9528801, 1.0],
+ [0.12026947, 0.989671, 1.0],
+ [0.0014969448, 0.9997942, 1.0],
+ [4.7565347e-05, 0.99999905, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.43385658, 0.97707945, 1.0],
+ [0.08405786, 0.99513173, 1.0],
+ [0.001577933, 0.99989617, 1.0],
+ [2.1989558e-06, 0.99999976, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.17016695, 0.990982, 1.0],
+ [0.020933557, 0.99893415, 1.0],
+ [0.00020579397, 0.99998814, 1.0],
+ [1.8093911e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.118497156, 0.9939799, 1.0],
+ [0.009560065, 0.99949676, 1.0],
+ [-6.1264276e-05, 0.99999744, 1.0],
+ [-7.5626076e-06, 1.0000001, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.18632784, 0.9930626, 1.0],
+ [0.016471203, 0.9994041, 1.0],
+ [1.850781e-05, 0.99999624, 1.0],
+ [-1.3992527e-05, 1.0000001, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.12490073, 0.99491405, 1.0],
+ [0.009683974, 0.9996138, 1.0],
+ [5.5344124e-05, 0.9999977, 1.0],
+ [-6.122279e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.046315443, 0.9974161, 1.0],
+ [0.0011095641, 0.99975616, 1.0],
+ [-9.514568e-05, 0.9999985, 1.0],
+ [-4.2969023e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.06989265, 0.99835086, 1.0],
+ [0.0046474864, 0.9999081, 1.0],
+ [8.086032e-06, 0.99999964, 1.0],
+ [-6.4283086e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.05445273, 0.9984405, 1.0],
+ [0.0056540556, 0.9999072, 1.0],
+ [3.1231557e-05, 0.99999934, 1.0],
+ [-1.4891012e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.049770087, 0.9989236, 1.0],
+ [0.0066248314, 0.9999419, 1.0],
+ [5.813844e-05, 0.99999964, 1.0],
+ [3.4580864e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.011926558, 0.9986787, 1.0],
+ [0.0030024045, 0.99992526, 1.0],
+ [5.3523254e-05, 0.9999996, 1.0],
+ [7.105971e-08, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.0051469407, 0.99917525, 1.0],
+ [-0.0015877569, 0.99988997, 1.0],
+ [-0.0019601185, 0.99996835, 1.0],
+ [-0.00019134225, 0.9999972, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.0026346056, 0.9979655, 1.0],
+ [-0.6399264, 0.9890465, 1.0],
+ [-1.0591722, 0.9829664, 1.0],
+ [-0.1191338, 0.9980818, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.33427265, 0.9909866, 1.0],
+ [-6.1553082, 0.8892826, 1.0],
+ [-8.770668, 0.85096085, 1.0],
+ [-1.2606072, 0.9774506, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.47517955, 0.9840748, 1.0],
+ [-8.44668, 0.82929236, 1.0],
+ [-13.877107, 0.7396451, 1.0],
+ [-3.6484365, 0.9244635, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.47713858, 0.9821221, 1.0],
+ [-11.744733, 0.7621532, 1.0],
+ [-19.645943, 0.6432617, 1.0],
+ [-8.809313, 0.82319057, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.22001219, 0.98877585, 1.0],
+ [-9.559413, 0.79853284, 1.0],
+ [-19.126335, 0.6495684, 1.0],
+ [-10.234527, 0.7984638, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.15887243, 0.9931774, 1.0],
+ [-6.930633, 0.85245496, 1.0],
+ [-16.76443, 0.69196665, 1.0],
+ [-9.0214, 0.82428676, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.016224792, 0.9942058, 1.0],
+ [-0.8251402, 0.9754959, 1.0],
+ [-3.2789445, 0.9271614, 1.0],
+ [-1.1487821, 0.97528994, 1.0]
+ ],
+ "srgb_value": true
+ }
+}
diff --git a/assets/camera-profiles/NIKON_Z_8.json b/assets/camera-profiles/NIKON_Z_8.json
new file mode 100644
index 000000000..0c63c0d84
--- /dev/null
+++ b/assets/camera-profiles/NIKON_Z_8.json
@@ -0,0 +1,1819 @@
+{
+ "version": 1,
+ "model": "NIKON Z 8",
+ "files": 261,
+ "samples": 969860,
+ "matrix": [
+ [1.0990362785046823, -0.20725005493106963, 0.10268209021537977],
+ [-0.029117321941279695, 1.1458550830734442, -0.11855469779814065],
+ [-0.10582269960146817, -0.749496743582382, 1.900598651063195]
+ ],
+ "hue_sat": {
+ "hue_divisions": 72,
+ "sat_divisions": 5,
+ "val_divisions": 5,
+ "data": [
+ [0.0, 1.0, 1.0],
+ [7.111473, 1.181401, 1.0],
+ [2.9113195, 1.1633477, 1.0],
+ [1.8994527, 1.137069, 1.0],
+ [0.53534466, 1.0345078, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.794022, 1.1620317, 1.0],
+ [1.6603756, 1.1454334, 1.0],
+ [0.65508085, 1.131233, 1.0],
+ [0.18744454, 1.0536848, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.2740474, 1.1378076, 1.0],
+ [0.39785913, 1.126624, 1.0],
+ [-1.5408971, 1.0759563, 1.0],
+ [-1.7140917, 1.0247983, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.2532353, 1.1126455, 1.0],
+ [-0.7449979, 1.1205007, 1.0],
+ [-3.0170422, 1.0559602, 1.0],
+ [-3.354577, 0.9959453, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.8965535, 1.0955226, 1.0],
+ [-0.70895463, 1.1094975, 1.0],
+ [-3.1364403, 1.0476824, 1.0],
+ [-4.1989164, 0.96736634, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.7045963, 1.0627788, 1.0],
+ [-1.3298069, 1.0956368, 1.0],
+ [-2.823391, 1.0708525, 1.0],
+ [-2.8166869, 1.0153031, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.7337725, 1.037307, 1.0],
+ [-1.8260294, 1.0655074, 1.0],
+ [-3.5580583, 1.0488383, 1.0],
+ [-4.437229, 1.0113343, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.3106112, 1.0036124, 1.0],
+ [-2.1256907, 1.0205824, 1.0],
+ [-4.961178, 1.0156583, 1.0],
+ [-6.710935, 0.9911362, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3536092, 0.9934402, 1.0],
+ [-2.5290065, 0.99898964, 1.0],
+ [-5.4174004, 0.9998236, 1.0],
+ [-6.597085, 0.9757822, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1366612, 1.002107, 1.0],
+ [-2.4894423, 0.9997597, 1.0],
+ [-5.4894304, 0.9861238, 1.0],
+ [-7.5021935, 0.92478454, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.5145179, 1.021503, 1.0],
+ [-1.5998987, 1.0070803, 1.0],
+ [-4.508984, 0.9658881, 1.0],
+ [-7.158313, 0.89098585, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.5639887, 1.0445297, 1.0],
+ [-0.915344, 1.0133737, 1.0],
+ [-3.1283567, 0.9622828, 1.0],
+ [-5.1986914, 0.8836733, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1355672, 1.0603209, 1.0],
+ [-0.56755894, 1.0115352, 1.0],
+ [-1.8851622, 0.95658684, 1.0],
+ [-3.4127562, 0.8781371, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.5467292, 1.0580485, 1.0],
+ [-0.41302148, 1.0018384, 1.0],
+ [-1.0522548, 0.94771504, 1.0],
+ [-1.7539524, 0.876335, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.0725913, 1.0581536, 1.0],
+ [-0.4528266, 0.9990066, 1.0],
+ [-0.57961255, 0.9448505, 1.0],
+ [-0.5526036, 0.8833151, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.46772903, 1.0652338, 1.0],
+ [-0.02375252, 1.0044563, 1.0],
+ [0.43251258, 0.94911736, 1.0],
+ [0.87126744, 0.89478797, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.16140896, 1.0854058, 1.0],
+ [1.07174, 1.0152693, 1.0],
+ [1.7586585, 0.95322454, 1.0],
+ [2.3524222, 0.8983894, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.2469127, 1.1031706, 1.0],
+ [1.1097546, 1.0245277, 1.0],
+ [1.8333874, 0.96286947, 1.0],
+ [2.6193652, 0.9074495, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.6874714, 1.1182649, 1.0],
+ [0.16355518, 1.0515357, 1.0],
+ [1.1668121, 0.98603916, 1.0],
+ [2.3031707, 0.9206025, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.0503242, 1.1499335, 1.0],
+ [-1.1552397, 1.0879966, 1.0],
+ [-0.10152792, 1.0231872, 1.0],
+ [1.0718579, 0.95476115, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.8791447, 1.188533, 1.0],
+ [-2.9817088, 1.1327138, 1.0],
+ [-1.7755328, 1.0618997, 1.0],
+ [-0.21666291, 0.94862247, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.8841834, 1.226612, 1.0],
+ [-4.98382, 1.1740679, 1.0],
+ [-3.1025357, 1.0698453, 1.0],
+ [-0.9934275, 0.9242727, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.169295, 1.2657825, 1.0],
+ [-7.0660734, 1.2132481, 1.0],
+ [-3.1967385, 1.0559258, 1.0],
+ [-0.6672474, 0.97608167, 1.0],
+ [0.0, 1.0, 1.0],
+ [-10.515467, 1.3097212, 1.0],
+ [-8.952165, 1.2460706, 1.0],
+ [-2.6956723, 1.0394313, 1.0],
+ [-0.24548055, 0.9968061, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.161258, 1.348111, 1.0],
+ [-10.267992, 1.2503926, 1.0],
+ [-1.3459312, 1.0174122, 1.0],
+ [-0.040196028, 1.0000353, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.790122, 1.339094, 1.0],
+ [-11.393451, 1.2008783, 1.0],
+ [-2.688318, 1.0172836, 1.0],
+ [-0.20984244, 1.0009907, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.627531, 1.308051, 1.0],
+ [-12.985201, 1.160609, 1.0],
+ [-2.6413164, 1.014484, 1.0],
+ [-0.1162438, 1.0004718, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.562479, 1.2632611, 1.0],
+ [-12.712911, 1.108189, 1.0],
+ [-2.095739, 1.0048033, 1.0],
+ [-0.04567434, 0.99998564, 1.0],
+ [0.0, 1.0, 1.0],
+ [-20.291159, 1.2343935, 1.0],
+ [-12.380699, 1.0716544, 1.0],
+ [-1.7035112, 0.9987354, 1.0],
+ [-0.026139155, 0.9999211, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.989433, 1.200086, 1.0],
+ [-11.1118, 1.0480033, 1.0],
+ [-1.400926, 0.996738, 1.0],
+ [-0.024835525, 0.9998831, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.566994, 1.17641, 1.0],
+ [-10.257852, 1.0363005, 1.0],
+ [-1.2149851, 0.99804825, 1.0],
+ [-0.015627315, 0.9999487, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.913862, 1.134827, 1.0],
+ [-10.370755, 1.0167582, 1.0],
+ [-1.6505148, 0.99528724, 1.0],
+ [-0.026268613, 0.9998695, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.999414, 1.1098753, 1.0],
+ [-9.642386, 1.0115949, 1.0],
+ [-1.2675271, 0.9941188, 1.0],
+ [-0.018203475, 0.99986815, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.586956, 1.091192, 1.0],
+ [-8.722237, 1.0048285, 1.0],
+ [-1.1602849, 0.9960165, 1.0],
+ [-0.013571129, 0.9999434, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.729242, 1.0722094, 1.0],
+ [-9.583627, 0.9960505, 1.0],
+ [-2.039792, 0.9939141, 1.0],
+ [-0.04050307, 0.99985063, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.906395, 1.075484, 1.0],
+ [-8.840361, 1.0053103, 1.0],
+ [-1.4046353, 0.9954329, 1.0],
+ [-0.031679757, 0.99983394, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.531858, 1.0629116, 1.0],
+ [-8.608148, 0.9959869, 1.0],
+ [-1.1416123, 0.99636537, 1.0],
+ [-0.013378057, 0.99994534, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.99668, 1.0072999, 1.0],
+ [-10.570478, 0.9696376, 1.0],
+ [-1.9599729, 0.9910149, 1.0],
+ [-0.022619616, 0.99988747, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.86213, 0.96442, 1.0],
+ [-11.836861, 0.9444823, 1.0],
+ [-2.469638, 0.984263, 1.0],
+ [-0.030691203, 0.99978447, 1.0],
+ [0.0, 1.0, 1.0],
+ [-20.236654, 0.90864915, 1.0],
+ [-13.107022, 0.9121311, 1.0],
+ [-2.9203506, 0.97508854, 1.0],
+ [-0.041130785, 0.9996122, 1.0],
+ [0.0, 1.0, 1.0],
+ [-20.714214, 0.8536292, 1.0],
+ [-14.74853, 0.8750625, 1.0],
+ [-3.946734, 0.9621585, 1.0],
+ [-0.05774192, 0.9994195, 1.0],
+ [0.0, 1.0, 1.0],
+ [-20.098251, 0.80836505, 1.0],
+ [-14.476766, 0.851234, 1.0],
+ [-5.6719704, 0.9378101, 1.0],
+ [-0.12513989, 0.9985625, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.542671, 0.76297987, 1.0],
+ [-13.628136, 0.83560556, 1.0],
+ [-7.0074353, 0.91483426, 1.0],
+ [-0.28871387, 0.99629575, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.084225, 0.74823433, 1.0],
+ [-12.162158, 0.84476614, 1.0],
+ [-6.7999973, 0.9135009, 1.0],
+ [-0.40371415, 0.9944375, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.452961, 0.72636646, 1.0],
+ [-9.328942, 0.86091775, 1.0],
+ [-4.9608183, 0.9375208, 1.0],
+ [-0.27671492, 0.9964092, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.168701, 0.74453783, 1.0],
+ [-6.218363, 0.90167975, 1.0],
+ [-3.7400405, 0.95979875, 1.0],
+ [-0.46412376, 0.995171, 1.0],
+ [0.0, 1.0, 1.0],
+ [-10.4688425, 0.77134514, 1.0],
+ [-5.7574325, 0.8950614, 1.0],
+ [-3.034646, 0.9650316, 1.0],
+ [-0.36110362, 0.9969887, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.514723, 0.7996464, 1.0],
+ [-3.7689729, 0.9172442, 1.0],
+ [-0.9782931, 1.0084524, 1.0],
+ [0.31402144, 1.0211289, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.6537147, 0.8334752, 1.0],
+ [-2.504266, 0.96418613, 1.0],
+ [-0.07010336, 1.052774, 1.0],
+ [0.894411, 1.0569643, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.493737, 0.7882688, 1.0],
+ [-2.3410633, 0.936153, 1.0],
+ [-0.32673165, 1.0513396, 1.0],
+ [0.49912313, 1.0484643, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.7034019, 0.73160046, 1.0],
+ [-0.831635, 0.8589306, 1.0],
+ [-0.3882015, 1.0134665, 1.0],
+ [-0.18704136, 1.0146226, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.61953825, 0.7258855, 1.0],
+ [0.81030744, 0.8431724, 1.0],
+ [0.00040164185, 1.0047435, 1.0],
+ [-0.10240378, 1.0050768, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.0624804, 0.7425685, 1.0],
+ [1.5886667, 0.8645959, 1.0],
+ [0.4176404, 1.0037674, 1.0],
+ [-0.00398716, 1.0012411, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.623457, 0.7510747, 1.0],
+ [1.7844758, 0.885587, 1.0],
+ [-0.036421247, 1.0089039, 1.0],
+ [-0.020441405, 1.001009, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.7174945, 0.7708686, 1.0],
+ [3.2143273, 0.90923154, 1.0],
+ [0.25317824, 1.0065747, 1.0],
+ [-0.016773207, 1.0006565, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.9824314, 0.7961609, 1.0],
+ [3.939417, 0.94614726, 1.0],
+ [0.7240807, 1.0062363, 1.0],
+ [0.030703958, 1.0001671, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.3545732, 0.82526684, 1.0],
+ [4.982609, 0.9743153, 1.0],
+ [1.4535582, 1.0078721, 1.0],
+ [0.062866114, 1.0002259, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.4568152, 0.8617716, 1.0],
+ [3.6539178, 1.0011972, 1.0],
+ [0.9039327, 1.0089482, 1.0],
+ [0.03915892, 1.000176, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.3634186, 0.91159344, 1.0],
+ [3.4343126, 1.0100657, 1.0],
+ [0.695478, 1.010742, 1.0],
+ [0.03255169, 1.0004065, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.238165, 0.9486031, 1.0],
+ [4.089919, 1.0266953, 1.0],
+ [1.1045972, 1.0186851, 1.0],
+ [0.057033073, 1.0010694, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.0543814, 0.9770822, 1.0],
+ [3.9128, 1.0336262, 1.0],
+ [1.1944752, 1.0113511, 1.0],
+ [0.07083795, 1.0004443, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.143442, 0.99413896, 1.0],
+ [4.040843, 1.0496417, 1.0],
+ [0.8080747, 1.0116086, 1.0],
+ [0.042427447, 0.99993575, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.18577, 0.9783746, 1.0],
+ [3.2767346, 1.0421677, 1.0],
+ [1.2492384, 1.016086, 1.0],
+ [0.15854485, 1.0005085, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.194208, 1.0016203, 1.0],
+ [3.0112083, 1.0349026, 1.0],
+ [0.91562295, 1.0119883, 1.0],
+ [0.0654759, 1.0005552, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.414966, 0.998843, 1.0],
+ [3.1521475, 1.0262057, 1.0],
+ [0.9972541, 1.0102346, 1.0],
+ [0.060677312, 1.0006106, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.276159, 0.9916557, 1.0],
+ [4.375442, 1.0329365, 1.0],
+ [1.5409418, 1.015073, 1.0],
+ [0.13645935, 1.0012853, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.8098955, 1.0221618, 1.0],
+ [5.381683, 1.0610034, 1.0],
+ [2.1655529, 1.0283315, 1.0],
+ [0.15032019, 1.0015818, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.995072, 1.0311706, 1.0],
+ [5.2788897, 1.0783035, 1.0],
+ [2.6601613, 1.0430328, 1.0],
+ [0.23225841, 1.0026511, 1.0],
+ [0.0, 1.0, 1.0],
+ [10.116063, 1.0506214, 1.0],
+ [6.2658486, 1.0895245, 1.0],
+ [3.640211, 1.0535048, 1.0],
+ [0.39292678, 1.0040704, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.800368, 1.0582323, 1.0],
+ [6.5737424, 1.0822098, 1.0],
+ [6.3034444, 1.0650462, 1.0],
+ [1.6771196, 1.0111806, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.205544, 1.097704, 1.0],
+ [5.412633, 1.1020687, 1.0],
+ [4.3975163, 1.0531521, 1.0],
+ [1.2703937, 1.0038551, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.4540615, 1.1362739, 1.0],
+ [4.478308, 1.1312087, 1.0],
+ [2.8681872, 1.0796018, 1.0],
+ [0.52417415, 1.009483, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.872955, 1.1356013, 1.0],
+ [2.2753267, 1.1355586, 1.0],
+ [1.7234284, 1.1281133, 1.0],
+ [1.1223476, 1.0683283, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.124278, 1.1170305, 1.0],
+ [0.4689261, 1.1054591, 1.0],
+ [-0.22648379, 1.088597, 1.0],
+ [-0.41844296, 1.0501652, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.5311556, 1.0830601, 1.0],
+ [-0.7712272, 1.0794301, 1.0],
+ [-2.3635144, 1.0375766, 1.0],
+ [-2.6961873, 1.0000627, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.776837, 1.051424, 1.0],
+ [-1.30482, 1.0655525, 1.0],
+ [-3.4497104, 1.0115541, 1.0],
+ [-4.008624, 0.9658105, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.325843, 1.0289863, 1.0],
+ [-0.89701647, 1.0507549, 1.0],
+ [-3.5267189, 0.9840365, 1.0],
+ [-4.8896995, 0.92570627, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.2383246, 0.9979077, 1.0],
+ [-1.0782118, 1.0382204, 1.0],
+ [-2.7714362, 1.0106578, 1.0],
+ [-3.8654947, 0.9470026, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.3593056, 0.97518253, 1.0],
+ [-1.4679712, 1.0127448, 1.0],
+ [-3.3288295, 1.0004958, 1.0],
+ [-4.8622665, 0.95781606, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.5416691, 0.94487643, 1.0],
+ [-1.9971544, 0.97270143, 1.0],
+ [-4.8065176, 0.9733487, 1.0],
+ [-6.8176403, 0.9401679, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.57058907, 0.9429605, 1.0],
+ [-2.4935536, 0.96301734, 1.0],
+ [-5.3679204, 0.9637666, 1.0],
+ [-6.831026, 0.91412973, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.26053378, 0.9610666, 1.0],
+ [-2.7039409, 0.9701933, 1.0],
+ [-5.4002733, 0.9458906, 1.0],
+ [-6.9784875, 0.86947393, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.53928864, 0.9811304, 1.0],
+ [-2.0121186, 0.97265023, 1.0],
+ [-4.3233175, 0.9172091, 1.0],
+ [-6.005866, 0.8446555, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.6283378, 1.0006269, 1.0],
+ [-1.4216532, 0.9777993, 1.0],
+ [-3.27393, 0.91542476, 1.0],
+ [-4.512793, 0.8313424, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.36547175, 1.0127599, 1.0],
+ [-1.0703696, 0.9762654, 1.0],
+ [-2.3095455, 0.9178097, 1.0],
+ [-3.2788665, 0.82293683, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.073588796, 1.014126, 1.0],
+ [-0.83646154, 0.96881944, 1.0],
+ [-1.3723681, 0.9147329, 1.0],
+ [-1.7665399, 0.82023007, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.63139224, 1.017959, 1.0],
+ [-0.81763905, 0.96828437, 1.0],
+ [-0.8057263, 0.91791266, 1.0],
+ [-0.65346074, 0.85073775, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.9540565, 1.0314627, 1.0],
+ [-0.4009139, 0.97503597, 1.0],
+ [0.16406323, 0.91989255, 1.0],
+ [0.69900894, 0.8609631, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.29338956, 1.051337, 1.0],
+ [0.6638694, 0.9824637, 1.0],
+ [1.4182094, 0.92527664, 1.0],
+ [2.0502326, 0.8743307, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.19034386, 1.060521, 1.0],
+ [0.7418265, 0.9920945, 1.0],
+ [1.5311259, 0.94175714, 1.0],
+ [2.3645382, 0.89062786, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.1072253, 1.0738078, 1.0],
+ [-0.17039186, 1.0250928, 1.0],
+ [1.0041893, 0.9713254, 1.0],
+ [2.2262986, 0.9069038, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.3444607, 1.0938511, 1.0],
+ [-1.396141, 1.0545899, 1.0],
+ [-0.19473651, 1.0042597, 1.0],
+ [1.0580148, 0.93644226, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.9988203, 1.1291953, 1.0],
+ [-3.121686, 1.0951587, 1.0],
+ [-1.7795982, 1.0358233, 1.0],
+ [-0.33727086, 0.92730236, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.7153583, 1.1640722, 1.0],
+ [-4.88902, 1.1327654, 1.0],
+ [-3.3953416, 1.0511376, 1.0],
+ [-1.5109448, 0.885162, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.723858, 1.1939173, 1.0],
+ [-6.834126, 1.162886, 1.0],
+ [-4.4360557, 1.0393157, 1.0],
+ [-2.0289664, 0.9277147, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.929747, 1.233579, 1.0],
+ [-8.989848, 1.1980727, 1.0],
+ [-5.8884463, 1.0467408, 1.0],
+ [-1.4371129, 0.9823743, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.79711, 1.2787904, 1.0],
+ [-11.43725, 1.2281201, 1.0],
+ [-5.0934134, 1.0372151, 1.0],
+ [-0.3870912, 0.99834675, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.6093645, 1.2775221, 1.0],
+ [-13.393321, 1.1939902, 1.0],
+ [-7.004254, 1.02318, 1.0],
+ [-0.8632698, 1.0002364, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.6125, 1.2465408, 1.0],
+ [-15.71152, 1.1482728, 1.0],
+ [-7.1894984, 1.0131606, 1.0],
+ [-0.6019608, 0.9991237, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.882221, 1.2043245, 1.0],
+ [-16.663143, 1.0912375, 1.0],
+ [-6.9870176, 0.9956438, 1.0],
+ [-0.2885382, 0.9992374, 1.0],
+ [0.0, 1.0, 1.0],
+ [-20.625488, 1.1759886, 1.0],
+ [-16.814833, 1.0537983, 1.0],
+ [-6.23361, 0.9808524, 1.0],
+ [-0.16323197, 0.99922365, 1.0],
+ [0.0, 1.0, 1.0],
+ [-21.191574, 1.1593459, 1.0],
+ [-16.181896, 1.0420355, 1.0],
+ [-4.434142, 0.979971, 1.0],
+ [-0.098916985, 0.9993104, 1.0],
+ [0.0, 1.0, 1.0],
+ [-21.118694, 1.1469342, 1.0],
+ [-15.308236, 1.0306582, 1.0],
+ [-3.593454, 0.9800867, 1.0],
+ [-0.11090971, 0.99904144, 1.0],
+ [0.0, 1.0, 1.0],
+ [-20.06453, 1.1023701, 1.0],
+ [-16.392384, 0.9912434, 1.0],
+ [-6.5470276, 0.95821047, 1.0],
+ [-0.29561612, 0.99735236, 1.0],
+ [0.0, 1.0, 1.0],
+ [-20.86482, 1.0911682, 1.0],
+ [-16.905964, 0.9835901, 1.0],
+ [-6.5230694, 0.95751834, 1.0],
+ [-0.18875386, 0.99822915, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.563591, 1.0770866, 1.0],
+ [-15.724344, 0.9894494, 1.0],
+ [-5.0314937, 0.9782653, 1.0],
+ [-0.08916562, 0.99951047, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.648596, 1.066223, 1.0],
+ [-15.772977, 0.97731113, 1.0],
+ [-6.1778703, 0.97096556, 1.0],
+ [-0.17319372, 0.999023, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.83075, 1.0626674, 1.0],
+ [-15.684646, 0.9758881, 1.0],
+ [-5.8669987, 0.96583444, 1.0],
+ [-0.21658812, 0.99854726, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.55086, 1.04659, 1.0],
+ [-14.666198, 0.9744223, 1.0],
+ [-4.026313, 0.98011285, 1.0],
+ [-0.07394151, 0.9995447, 1.0],
+ [0.0, 1.0, 1.0],
+ [-20.196869, 0.988159, 1.0],
+ [-17.390198, 0.93337435, 1.0],
+ [-7.4060826, 0.95599365, 1.0],
+ [-0.14739579, 0.99894, 1.0],
+ [0.0, 1.0, 1.0],
+ [-21.26852, 0.9264197, 1.0],
+ [-18.344461, 0.8974489, 1.0],
+ [-8.743559, 0.936538, 1.0],
+ [-0.19807786, 0.99823266, 1.0],
+ [0.0, 1.0, 1.0],
+ [-21.510828, 0.87103164, 1.0],
+ [-18.518517, 0.86176705, 1.0],
+ [-10.065859, 0.9085589, 1.0],
+ [-0.2821744, 0.99699414, 1.0],
+ [0.0, 1.0, 1.0],
+ [-21.584885, 0.8148765, 1.0],
+ [-18.512503, 0.82746816, 1.0],
+ [-11.73013, 0.8824068, 1.0],
+ [-0.3848725, 0.9958189, 1.0],
+ [0.0, 1.0, 1.0],
+ [-20.356161, 0.77016896, 1.0],
+ [-16.736427, 0.8110233, 1.0],
+ [-13.46877, 0.8463329, 1.0],
+ [-1.0992389, 0.9864028, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.00107, 0.7378436, 1.0],
+ [-14.466894, 0.8157218, 1.0],
+ [-12.32627, 0.84496945, 1.0],
+ [-2.4299142, 0.9672269, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.491095, 0.72382104, 1.0],
+ [-11.5228195, 0.84021336, 1.0],
+ [-9.1774845, 0.87830716, 1.0],
+ [-2.8254447, 0.9605242, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.435101, 0.7124373, 1.0],
+ [-8.480166, 0.8606328, 1.0],
+ [-5.882448, 0.91346925, 1.0],
+ [-1.6596885, 0.9736952, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.965368, 0.78840566, 1.0],
+ [-5.0230317, 0.92467666, 1.0],
+ [-3.841451, 0.95225155, 1.0],
+ [-2.0082777, 0.97166634, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.585785, 0.8164634, 1.0],
+ [-4.2593484, 0.9317818, 1.0],
+ [-2.753388, 0.96636844, 1.0],
+ [-0.8575842, 0.98519945, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.9080076, 0.82606196, 1.0],
+ [-2.4876971, 0.95708805, 1.0],
+ [-0.017117301, 1.0234948, 1.0],
+ [1.1949928, 1.0367807, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.3217926, 0.8621694, 1.0],
+ [-1.1579821, 1.013248, 1.0],
+ [0.829161, 1.0717134, 1.0],
+ [1.6019802, 1.078209, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.4365811, 0.801541, 1.0],
+ [-1.142285, 0.9963089, 1.0],
+ [0.6441237, 1.0807948, 1.0],
+ [1.3309326, 1.0855218, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.0976561, 0.7120078, 1.0],
+ [-0.8466615, 0.89612305, 1.0],
+ [-0.7710759, 1.0564098, 1.0],
+ [-0.6550111, 1.07382, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.8495644, 0.71387726, 1.0],
+ [0.49764365, 0.8776602, 1.0],
+ [-0.9067246, 1.0398972, 1.0],
+ [-1.0517284, 1.0432695, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.4473016, 0.72682, 1.0],
+ [1.7983775, 0.88795257, 1.0],
+ [0.20548964, 1.0286621, 1.0],
+ [-0.35707185, 1.0189359, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.0796447, 0.7306628, 1.0],
+ [2.1504579, 0.89834225, 1.0],
+ [0.34639126, 1.0416607, 1.0],
+ [-0.23788506, 1.019259, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.6701546, 0.74359995, 1.0],
+ [4.0726633, 0.91694146, 1.0],
+ [1.3216503, 1.0316267, 1.0],
+ [-0.045850165, 1.0076267, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.0043573, 0.7694095, 1.0],
+ [5.4557076, 0.9445862, 1.0],
+ [4.0584545, 1.0185257, 1.0],
+ [0.56355107, 1.0008852, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.084995, 0.78970784, 1.0],
+ [5.8458114, 0.970168, 1.0],
+ [4.5538306, 1.024938, 1.0],
+ [0.49345702, 1.0022416, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.05806, 0.817034, 1.0],
+ [4.4210987, 1.0048858, 1.0],
+ [3.0152867, 1.0326685, 1.0],
+ [0.38135135, 1.0021529, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.005357, 0.85885465, 1.0],
+ [4.763144, 1.0251466, 1.0],
+ [2.17603, 1.0425395, 1.0],
+ [0.36619714, 1.0030634, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.993758, 0.90082896, 1.0],
+ [5.2438936, 1.0453547, 1.0],
+ [2.549889, 1.0553246, 1.0],
+ [0.32793412, 1.0068744, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.829749, 0.9506837, 1.0],
+ [6.0920825, 1.0672028, 1.0],
+ [3.7592368, 1.0513718, 1.0],
+ [0.4075813, 1.0041977, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.575895, 0.9809986, 1.0],
+ [5.147536, 1.0732907, 1.0],
+ [2.6697485, 1.0479143, 1.0],
+ [0.23983598, 1.0018369, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.565204, 0.9654144, 1.0],
+ [4.159953, 1.0592482, 1.0],
+ [2.982862, 1.046513, 1.0],
+ [0.51444083, 1.0031579, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.271386, 0.9671414, 1.0],
+ [3.9021678, 1.0480341, 1.0],
+ [2.3847647, 1.0395492, 1.0],
+ [0.338095, 1.0032356, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.753154, 0.9583014, 1.0],
+ [4.58982, 1.0358695, 1.0],
+ [3.3719125, 1.0326496, 1.0],
+ [0.62065965, 1.0025914, 1.0],
+ [0.0, 1.0, 1.0],
+ [10.777158, 0.9585278, 1.0],
+ [5.382428, 1.024311, 1.0],
+ [3.983915, 1.028954, 1.0],
+ [0.95064604, 1.0067729, 1.0],
+ [0.0, 1.0, 1.0],
+ [10.107686, 0.98531634, 1.0],
+ [5.2336845, 1.0421758, 1.0],
+ [3.7427158, 1.0352445, 1.0],
+ [0.91338927, 1.0056044, 1.0],
+ [0.0, 1.0, 1.0],
+ [10.010471, 1.002975, 1.0],
+ [5.0806794, 1.0562643, 1.0],
+ [3.9300086, 1.0432692, 1.0],
+ [1.2768706, 1.0071726, 1.0],
+ [0.0, 1.0, 1.0],
+ [10.721367, 1.020363, 1.0],
+ [5.9179363, 1.0760281, 1.0],
+ [4.653525, 1.0701501, 1.0],
+ [1.5535151, 1.0154992, 1.0],
+ [0.0, 1.0, 1.0],
+ [10.558615, 1.0318329, 1.0],
+ [6.080362, 1.067487, 1.0],
+ [6.671132, 1.0704038, 1.0],
+ [4.238108, 1.0257412, 1.0],
+ [0.0, 1.0, 1.0],
+ [9.48343, 1.0585111, 1.0],
+ [4.4426146, 1.0725648, 1.0],
+ [4.5216165, 1.0536869, 1.0],
+ [3.2611907, 1.0090235, 1.0],
+ [0.0, 1.0, 1.0],
+ [8.163124, 1.0967343, 1.0],
+ [3.4163864, 1.1021494, 1.0],
+ [3.0036535, 1.0894843, 1.0],
+ [1.6298203, 1.0331198, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.7200303, 0.99990386, 1.0],
+ [-0.18434145, 0.9829197, 1.0],
+ [-1.7782454, 0.9463851, 1.0],
+ [-0.8780288, 0.9674484, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.3225217, 0.9776916, 1.0],
+ [-1.491963, 0.9430486, 1.0],
+ [-2.2781556, 0.91853374, 1.0],
+ [-1.9229406, 0.92325467, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.4281845, 0.93801886, 1.0],
+ [-2.0302162, 0.92144394, 1.0],
+ [-2.984083, 0.90740937, 1.0],
+ [-2.8202295, 0.9010818, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.21699, 0.9216663, 1.0],
+ [-1.478845, 0.9148489, 1.0],
+ [-3.1692345, 0.9026742, 1.0],
+ [-3.4822135, 0.8847937, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.6095815, 0.91278374, 1.0],
+ [-0.50396746, 0.90550876, 1.0],
+ [-2.5619872, 0.8661648, 1.0],
+ [-3.3487625, 0.8425053, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.2401028, 0.9033001, 1.0],
+ [-0.25377062, 0.8960398, 1.0],
+ [-2.032842, 0.87459487, 1.0],
+ [-2.7365766, 0.83498585, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.695987, 0.8888191, 1.0],
+ [-0.67016447, 0.88870287, 1.0],
+ [-2.36245, 0.87585235, 1.0],
+ [-3.2576814, 0.8303152, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.9194321, 0.8731023, 1.0],
+ [-1.3481888, 0.8760105, 1.0],
+ [-3.32636, 0.8514322, 1.0],
+ [-4.274623, 0.8039877, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.23748975, 0.8785199, 1.0],
+ [-1.6813989, 0.8869438, 1.0],
+ [-4.1970234, 0.867815, 1.0],
+ [-4.993503, 0.8058187, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.0443294, 0.89978844, 1.0],
+ [-1.9890785, 0.90232074, 1.0],
+ [-3.950284, 0.85917366, 1.0],
+ [-4.1894603, 0.7918877, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.29374087, 0.92003465, 1.0],
+ [-1.5607008, 0.90463936, 1.0],
+ [-2.7787287, 0.8298095, 1.0],
+ [-2.9107876, 0.7702355, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.2690241, 0.9420806, 1.0],
+ [-1.3042494, 0.91745335, 1.0],
+ [-2.3432212, 0.8269896, 1.0],
+ [-2.5380266, 0.7619623, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.00078135007, 0.9576381, 1.0],
+ [-1.0908285, 0.9226896, 1.0],
+ [-1.7685524, 0.83202, 1.0],
+ [-1.8833431, 0.7369599, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.3298981, 0.9611947, 1.0],
+ [-0.9030142, 0.9218993, 1.0],
+ [-1.1803176, 0.84522974, 1.0],
+ [-1.2364491, 0.7347839, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.90065134, 0.96639967, 1.0],
+ [-0.92589504, 0.9220938, 1.0],
+ [-0.7533085, 0.86246485, 1.0],
+ [-0.58836806, 0.7862185, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.2350203, 0.982918, 1.0],
+ [-0.63451254, 0.92791486, 1.0],
+ [-0.05854761, 0.8623331, 1.0],
+ [0.30643168, 0.8013406, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.6747929, 0.99744356, 1.0],
+ [0.28179812, 0.93459874, 1.0],
+ [0.98867965, 0.88537693, 1.0],
+ [1.5234042, 0.83812547, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.5743862, 1.0066322, 1.0],
+ [0.4175759, 0.9530498, 1.0],
+ [1.1614387, 0.9119914, 1.0],
+ [1.933506, 0.86427605, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3801366, 1.0321106, 1.0],
+ [-0.38676104, 0.9921655, 1.0],
+ [0.70755553, 0.94378203, 1.0],
+ [1.802139, 0.8818721, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.5278633, 1.0482858, 1.0],
+ [-1.6080537, 1.0186095, 1.0],
+ [-0.4270131, 0.96137357, 1.0],
+ [0.52042204, 0.89067566, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.9787476, 1.078391, 1.0],
+ [-3.1643727, 1.050093, 1.0],
+ [-1.7115612, 0.9687348, 1.0],
+ [-0.4047129, 0.8798787, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.596516, 1.1130706, 1.0],
+ [-4.6923046, 1.0734227, 1.0],
+ [-2.8240192, 0.92865837, 1.0],
+ [-1.3897139, 0.8587299, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.4136, 1.1421592, 1.0],
+ [-6.2911453, 1.0910819, 1.0],
+ [-3.5203412, 0.91654646, 1.0],
+ [-1.5387671, 0.8988556, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.345446, 1.175205, 1.0],
+ [-7.8788543, 1.1139026, 1.0],
+ [-3.9887538, 0.95732474, 1.0],
+ [-0.7494897, 0.9770353, 1.0],
+ [0.0, 1.0, 1.0],
+ [-11.726303, 1.2018178, 1.0],
+ [-9.235658, 1.1204212, 1.0],
+ [-2.6193218, 0.99971265, 1.0],
+ [-0.17413116, 0.9981184, 1.0],
+ [0.0, 1.0, 1.0],
+ [-13.852155, 1.1776309, 1.0],
+ [-9.851412, 1.0741005, 1.0],
+ [-3.3592322, 0.99141484, 1.0],
+ [-0.30808955, 0.99881417, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.295475, 1.1342069, 1.0],
+ [-10.099276, 1.0167769, 1.0],
+ [-3.3911893, 0.9816512, 1.0],
+ [-0.22844037, 0.99877155, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.857363, 1.0911952, 1.0],
+ [-10.737116, 0.98477983, 1.0],
+ [-3.0231202, 0.9772009, 1.0],
+ [-0.08856626, 0.99933064, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.511763, 1.0658559, 1.0],
+ [-10.546561, 0.97217023, 1.0],
+ [-2.431429, 0.97843724, 1.0],
+ [-0.04837607, 0.9995322, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.32334, 1.046019, 1.0],
+ [-9.642773, 0.9659112, 1.0],
+ [-1.8824904, 0.9784988, 1.0],
+ [-0.039536994, 0.9995053, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.230003, 1.0588695, 1.0],
+ [-7.7154303, 0.98527706, 1.0],
+ [-1.1871514, 0.9866838, 1.0],
+ [-0.05562747, 0.99940675, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.798325, 1.0390636, 1.0],
+ [-9.137856, 0.95683104, 1.0],
+ [-3.1458273, 0.96849316, 1.0],
+ [-0.17147331, 0.99812067, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.813182, 1.0053109, 1.0],
+ [-10.048948, 0.94771963, 1.0],
+ [-3.4650283, 0.9650353, 1.0],
+ [-0.12416336, 0.9983432, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.047022, 0.98889244, 1.0],
+ [-8.209296, 0.96882516, 1.0],
+ [-2.0700836, 0.98336583, 1.0],
+ [-0.05653161, 0.9994855, 1.0],
+ [0.0, 1.0, 1.0],
+ [-16.9603, 0.97119784, 1.0],
+ [-8.66278, 0.9401887, 1.0],
+ [-2.2208483, 0.9735744, 1.0],
+ [-0.05832822, 0.9992916, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.93582, 0.9818394, 1.0],
+ [-9.735115, 0.91714615, 1.0],
+ [-2.8493056, 0.9654458, 1.0],
+ [-0.06305568, 0.99927205, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.386252, 0.98472387, 1.0],
+ [-8.998497, 0.9461114, 1.0],
+ [-2.1540806, 0.9810613, 1.0],
+ [-0.04269774, 0.99961257, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.852745, 0.9424422, 1.0],
+ [-13.541525, 0.9004132, 1.0],
+ [-5.0671234, 0.95313597, 1.0],
+ [-0.12552416, 0.99874544, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.605848, 0.8931836, 1.0],
+ [-11.864264, 0.9022595, 1.0],
+ [-3.5728223, 0.95726645, 1.0],
+ [-0.09980284, 0.9986085, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.33637, 0.8163921, 1.0],
+ [-12.252236, 0.8543749, 1.0],
+ [-3.4134076, 0.94611895, 1.0],
+ [-0.07950901, 0.9985124, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.060442, 0.7734047, 1.0],
+ [-15.23906, 0.8017895, 1.0],
+ [-5.954306, 0.91588944, 1.0],
+ [-0.12067752, 0.9982148, 1.0],
+ [0.0, 1.0, 1.0],
+ [-19.100754, 0.72602576, 1.0],
+ [-15.591848, 0.7819147, 1.0],
+ [-9.629234, 0.86622393, 1.0],
+ [-0.3522098, 0.99482995, 1.0],
+ [0.0, 1.0, 1.0],
+ [-18.568441, 0.6896306, 1.0],
+ [-14.766998, 0.7783395, 1.0],
+ [-11.441, 0.83353055, 1.0],
+ [-0.95255774, 0.98503613, 1.0],
+ [0.0, 1.0, 1.0],
+ [-17.873262, 0.66292804, 1.0],
+ [-11.700582, 0.7895474, 1.0],
+ [-7.4423504, 0.87738365, 1.0],
+ [-0.7649209, 0.9871464, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.81971, 0.65139574, 1.0],
+ [-9.3584585, 0.78892857, 1.0],
+ [-5.956205, 0.8730324, 1.0],
+ [-0.8720731, 0.9798331, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.644368, 0.7343603, 1.0],
+ [-5.3256245, 0.8688635, 1.0],
+ [-4.0904546, 0.905134, 1.0],
+ [-1.9802942, 0.942991, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.834648, 0.768598, 1.0],
+ [-3.7342772, 0.8917114, 1.0],
+ [-2.275454, 0.9256257, 1.0],
+ [-0.91388834, 0.94262, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.958538, 0.79897887, 1.0],
+ [-1.7752197, 0.92934436, 1.0],
+ [-0.052028436, 0.9775266, 1.0],
+ [1.0069743, 0.99931246, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.5435648, 0.83892524, 1.0],
+ [-0.45535767, 0.99585503, 1.0],
+ [1.1220992, 1.0467564, 1.0],
+ [1.688386, 1.0540297, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.584903, 0.7611296, 1.0],
+ [-0.29603556, 0.99536204, 1.0],
+ [1.0826626, 1.0670043, 1.0],
+ [1.5285076, 1.0699658, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.24621776, 0.6534611, 1.0],
+ [-0.5641967, 0.88916516, 1.0],
+ [-0.5208639, 1.0147351, 1.0],
+ [-0.17287369, 1.0311226, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.6623358, 0.67162013, 1.0],
+ [-0.014613975, 0.90694416, 1.0],
+ [-0.7910066, 0.9813947, 1.0],
+ [-0.7020828, 0.9796464, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.2134006, 0.70449984, 1.0],
+ [0.69320697, 0.9216747, 1.0],
+ [-0.55953246, 0.98736876, 1.0],
+ [-0.60563326, 0.9877621, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.672629, 0.72619575, 1.0],
+ [2.541607, 0.90983087, 1.0],
+ [1.9858508, 0.9788352, 1.0],
+ [0.5622153, 1.0007318, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.4891405, 0.7448458, 1.0],
+ [4.03939, 0.9308739, 1.0],
+ [3.3720996, 0.97321606, 1.0],
+ [1.2299676, 0.9903418, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.157809, 0.77485734, 1.0],
+ [4.3740115, 0.9462917, 1.0],
+ [3.2057083, 0.98681796, 1.0],
+ [0.7071888, 0.997923, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.228337, 0.79360247, 1.0],
+ [2.7674537, 0.9669471, 1.0],
+ [1.3489184, 1.003461, 1.0],
+ [0.16082476, 1.0005132, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.9538918, 0.81647366, 1.0],
+ [3.1088212, 0.9820907, 1.0],
+ [1.4730394, 1.0125114, 1.0],
+ [0.15418816, 1.0018836, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.6807427, 0.8534504, 1.0],
+ [3.718583, 0.99722385, 1.0],
+ [1.4714887, 1.0191232, 1.0],
+ [0.16044605, 1.0014175, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.902218, 0.8794101, 1.0],
+ [2.855522, 0.99446875, 1.0],
+ [1.4610727, 1.0151895, 1.0],
+ [0.11758561, 1.0017471, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.0196548, 0.9160186, 1.0],
+ [1.6999657, 1.0058115, 1.0],
+ [1.0062673, 1.0163534, 1.0],
+ [0.08308377, 1.0012561, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.268169, 0.93658304, 1.0],
+ [1.035491, 0.99574167, 1.0],
+ [0.41364503, 1.0079995, 1.0],
+ [0.040540703, 1.0006597, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.259043, 0.92028683, 1.0],
+ [0.23776771, 0.9797173, 1.0],
+ [0.07063755, 0.9973912, 1.0],
+ [0.066322915, 1.0000901, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.786019, 0.91943693, 1.0],
+ [-0.12373619, 0.97899324, 1.0],
+ [-1.2265637, 0.9915, 1.0],
+ [-0.047353376, 0.9991457, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.188807, 0.89457834, 1.0],
+ [-0.7516189, 0.9425688, 1.0],
+ [-1.1165174, 0.962824, 1.0],
+ [0.15711758, 0.99334264, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.1094146, 0.89115375, 1.0],
+ [-0.14875633, 0.9282804, 1.0],
+ [-0.99777395, 0.94288254, 1.0],
+ [-0.13148743, 0.9870557, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.441057, 0.90025866, 1.0],
+ [-0.48353556, 0.9186272, 1.0],
+ [-0.82087964, 0.9264453, 1.0],
+ [0.08318026, 0.9731093, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.7111974, 0.91075236, 1.0],
+ [-0.36840874, 0.92250305, 1.0],
+ [-0.4315016, 0.9269158, 1.0],
+ [0.2359939, 0.96514857, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.3763676, 0.92495906, 1.0],
+ [0.36888885, 0.9462593, 1.0],
+ [-1.5960634, 0.9405903, 1.0],
+ [-0.54044867, 0.9806632, 1.0],
+ [0.0, 1.0, 1.0],
+ [7.855471, 0.9369563, 1.0],
+ [1.1207947, 0.95559967, 1.0],
+ [-0.42048588, 0.9605789, 1.0],
+ [0.09410473, 0.9890609, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.971675, 0.9428375, 1.0],
+ [1.4723355, 0.95907015, 1.0],
+ [-0.40449092, 0.9526683, 1.0],
+ [-0.034233358, 0.98458976, 1.0],
+ [0.0, 1.0, 1.0],
+ [6.2168164, 0.96651506, 1.0],
+ [0.45716965, 0.969795, 1.0],
+ [-1.276185, 0.95871323, 1.0],
+ [-0.46196827, 0.98386484, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.5696334, 0.88579595, 1.0],
+ [-4.8797174, 0.8235008, 1.0],
+ [-5.7836246, 0.81873804, 1.0],
+ [-1.2262843, 0.9544453, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.3305974, 0.86768496, 1.0],
+ [-4.0607495, 0.7667695, 1.0],
+ [-5.33198, 0.72661614, 1.0],
+ [-2.8205767, 0.83346885, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.10735662, 0.7961712, 1.0],
+ [-2.5164855, 0.746526, 1.0],
+ [-3.905786, 0.70082253, 1.0],
+ [-2.8722813, 0.7402977, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.0955659, 0.7785365, 1.0],
+ [-0.020220347, 0.7376785, 1.0],
+ [-1.4722649, 0.72488576, 1.0],
+ [-1.4273844, 0.7533604, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.1442819, 0.74805653, 1.0],
+ [2.5943675, 0.7122985, 1.0],
+ [1.4479681, 0.70843244, 1.0],
+ [-0.06824569, 0.74796575, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.0809562, 0.7545235, 1.0],
+ [2.7876127, 0.7176543, 1.0],
+ [3.0311806, 0.6859141, 1.0],
+ [2.3895133, 0.7123276, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.9816447, 0.78059196, 1.0],
+ [2.157276, 0.7259703, 1.0],
+ [2.2583017, 0.69609505, 1.0],
+ [2.6869667, 0.69099945, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1339029, 0.8082483, 1.0],
+ [0.9635001, 0.7438238, 1.0],
+ [1.7119975, 0.69262445, 1.0],
+ [2.8751085, 0.67264295, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.7148004, 0.8236315, 1.0],
+ [0.15944664, 0.7895265, 1.0],
+ [0.8037349, 0.73090047, 1.0],
+ [2.2817705, 0.700999, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.1721781, 0.823198, 1.0],
+ [0.42141455, 0.79766697, 1.0],
+ [1.731092, 0.72331715, 1.0],
+ [1.9070219, 0.70068294, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4472144, 0.82568, 1.0],
+ [0.7194572, 0.78889567, 1.0],
+ [1.2272654, 0.7160339, 1.0],
+ [1.3908615, 0.6934153, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.9659478, 0.8547631, 1.0],
+ [0.23514794, 0.8048065, 1.0],
+ [0.6698278, 0.720812, 1.0],
+ [1.0716225, 0.6872872, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.3646527, 0.8849565, 1.0],
+ [-0.07247575, 0.8116052, 1.0],
+ [0.0033967285, 0.72046185, 1.0],
+ [-0.12690859, 0.66787803, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.14792149, 0.90996736, 1.0],
+ [-0.41231233, 0.8411673, 1.0],
+ [-0.4463926, 0.7333069, 1.0],
+ [-0.42133877, 0.6662905, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.5877023, 0.92537576, 1.0],
+ [-0.7669311, 0.85629356, 1.0],
+ [-0.6382856, 0.7815932, 1.0],
+ [-0.5130515, 0.72520864, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.8632642, 0.935545, 1.0],
+ [-0.6218721, 0.8610767, 1.0],
+ [-0.2218913, 0.7949077, 1.0],
+ [-0.026698561, 0.76374143, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.6670934, 0.95232236, 1.0],
+ [-0.013086532, 0.8753517, 1.0],
+ [0.3907465, 0.82373375, 1.0],
+ [0.6438301, 0.7755369, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.8246577, 0.96011007, 1.0],
+ [-0.022377672, 0.9073638, 1.0],
+ [0.5644851, 0.86494243, 1.0],
+ [1.0286213, 0.8235526, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.3516122, 0.97745264, 1.0],
+ [-0.5598576, 0.9293583, 1.0],
+ [0.04228649, 0.8763748, 1.0],
+ [-0.31584078, 0.8592634, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9975094, 0.9925349, 1.0],
+ [-1.3282233, 0.9415561, 1.0],
+ [-0.52022403, 0.85210764, 1.0],
+ [-0.5763454, 0.8441975, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.0044324, 0.993117, 1.0],
+ [-2.32693, 0.9048576, 1.0],
+ [-1.0574396, 0.8305523, 1.0],
+ [-0.25919157, 0.86520964, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.8471406, 0.9966514, 1.0],
+ [-2.3174558, 0.8910772, 1.0],
+ [-1.6867507, 0.8372742, 1.0],
+ [-0.6674814, 0.9019048, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.7654781, 1.0002338, 1.0],
+ [-2.2775855, 0.92181945, 1.0],
+ [-1.6766036, 0.8592095, 1.0],
+ [-0.6071752, 0.91706604, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.2180955, 1.0019648, 1.0],
+ [-2.4868007, 0.9530428, 1.0],
+ [-0.8147237, 0.9659948, 1.0],
+ [-0.08990055, 0.99272156, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.6169603, 1.0069607, 1.0],
+ [-1.80826, 0.9822141, 1.0],
+ [-0.27318954, 0.99557036, 1.0],
+ [-0.0078226, 0.9997451, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.620088, 1.0105745, 1.0],
+ [-1.6558734, 0.99095327, 1.0],
+ [-0.34937736, 0.99785286, 1.0],
+ [-0.009377529, 0.9999541, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.9161261, 1.0054843, 1.0],
+ [-0.7893424, 0.9937573, 1.0],
+ [-0.21549447, 0.9983831, 1.0],
+ [-0.006033252, 0.9999624, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.6860873, 1.000248, 1.0],
+ [-0.8517966, 0.99048835, 1.0],
+ [-0.1809431, 0.99742275, 1.0],
+ [-0.0035661473, 0.99995184, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.009457, 0.9998743, 1.0],
+ [-1.0168654, 0.9889097, 1.0],
+ [-0.14624618, 0.99738944, 1.0],
+ [-0.002028748, 0.99996185, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.343951, 0.9905693, 1.0],
+ [-0.7694108, 0.988736, 1.0],
+ [-0.13042787, 0.9973714, 1.0],
+ [-0.0019758502, 0.9999585, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.5010524, 0.9900646, 1.0],
+ [-0.6511199, 0.99298286, 1.0],
+ [-0.05297016, 0.9991895, 1.0],
+ [-0.0008825612, 0.99998635, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.6744328, 0.9952045, 1.0],
+ [-0.56495047, 0.9939041, 1.0],
+ [-0.072879314, 0.9991123, 1.0],
+ [-0.0022557385, 0.9999713, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.817171, 0.98676676, 1.0],
+ [-0.59550554, 0.9955451, 1.0],
+ [-0.10011909, 0.99888563, 1.0],
+ [-0.002785918, 0.9999608, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.859452, 0.9669093, 1.0],
+ [-0.5635375, 0.9964578, 1.0],
+ [-0.061867803, 0.99939847, 1.0],
+ [-0.001733142, 0.9999818, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.3632984, 0.9476491, 1.0],
+ [-0.9620065, 0.9895864, 1.0],
+ [-0.1275121, 0.9982268, 1.0],
+ [-0.0027327028, 0.9999624, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.8316839, 0.9652471, 1.0],
+ [-1.5537338, 0.9825404, 1.0],
+ [-0.27127823, 0.99667627, 1.0],
+ [-0.0039444612, 0.9999499, 1.0],
+ [0.0, 1.0, 1.0],
+ [-2.9406507, 0.9717847, 1.0],
+ [-1.1054865, 0.98933583, 1.0],
+ [-0.15259248, 0.9985821, 1.0],
+ [-0.0023587716, 0.99997854, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.1605053, 0.9520869, 1.0],
+ [-2.1564174, 0.9791306, 1.0],
+ [-0.33962235, 0.9967707, 1.0],
+ [-0.005252303, 0.9999497, 1.0],
+ [0.0, 1.0, 1.0],
+ [-4.6772366, 0.9365187, 1.0],
+ [-0.94710606, 0.98695683, 1.0],
+ [-0.11579239, 0.9980016, 1.0],
+ [-0.002743927, 0.9999484, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.730075, 0.9172761, 1.0],
+ [-2.2775342, 0.96206695, 1.0],
+ [-0.38358852, 0.9927294, 1.0],
+ [-0.0075762877, 0.9998489, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.438858, 0.8943925, 1.0],
+ [-5.09395, 0.91621476, 1.0],
+ [-1.3278813, 0.9786671, 1.0],
+ [-0.026160287, 0.99958307, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.272365, 0.8478823, 1.0],
+ [-3.386477, 0.9320077, 1.0],
+ [-0.5339506, 0.9900594, 1.0],
+ [-0.008624411, 0.9998474, 1.0],
+ [0.0, 1.0, 1.0],
+ [-11.802094, 0.75678855, 1.0],
+ [-6.5117326, 0.85963863, 1.0],
+ [-1.7044662, 0.96734977, 1.0],
+ [-0.04367834, 0.9992392, 1.0],
+ [0.0, 1.0, 1.0],
+ [-15.306793, 0.6631, 1.0],
+ [-9.06385, 0.7773203, 1.0],
+ [-2.7547972, 0.9315435, 1.0],
+ [-0.10010718, 0.99756825, 1.0],
+ [0.0, 1.0, 1.0],
+ [-14.635688, 0.62598675, 1.0],
+ [-11.576207, 0.6989409, 1.0],
+ [-7.9337134, 0.8109356, 1.0],
+ [-0.6527108, 0.98375565, 1.0],
+ [0.0, 1.0, 1.0],
+ [-9.552264, 0.68059266, 1.0],
+ [-8.43511, 0.7298391, 1.0],
+ [-8.363759, 0.7697578, 1.0],
+ [-3.363505, 0.9041366, 1.0],
+ [0.0, 1.0, 1.0],
+ [-7.9862127, 0.70823747, 1.0],
+ [-6.549984, 0.7803926, 1.0],
+ [-6.411847, 0.8035285, 1.0],
+ [-5.4986544, 0.83412504, 1.0],
+ [0.0, 1.0, 1.0],
+ [-6.795965, 0.7171654, 1.0],
+ [-4.852691, 0.821311, 1.0],
+ [-4.071751, 0.8528317, 1.0],
+ [-2.6617806, 0.89110637, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.3123918, 0.72757715, 1.0],
+ [-3.218556, 0.8593564, 1.0],
+ [-1.1645839, 0.93264043, 1.0],
+ [0.039813682, 0.9652358, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.6778567, 0.6624857, 1.0],
+ [-1.7741417, 0.8541198, 1.0],
+ [-0.0029635795, 0.9721248, 1.0],
+ [0.40605798, 0.98661965, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.8761976, 0.6101054, 1.0],
+ [-0.49433082, 0.8107166, 1.0],
+ [-0.104019575, 0.86723596, 1.0],
+ [0.20899503, 0.890897, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.861542, 0.67296326, 1.0],
+ [0.71851164, 0.8735976, 1.0],
+ [0.8827842, 0.8889713, 1.0],
+ [1.2020328, 0.8868588, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.1083093, 0.72884905, 1.0],
+ [1.4813685, 0.8916017, 1.0],
+ [0.98923063, 0.90779537, 1.0],
+ [1.0078008, 0.9088085, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.061599, 0.7781577, 1.0],
+ [6.068938, 0.85700047, 1.0],
+ [7.827683, 0.85164875, 1.0],
+ [3.5587494, 0.9304464, 1.0],
+ [0.0, 1.0, 1.0],
+ [5.0541573, 0.80283755, 1.0],
+ [5.1185517, 0.9080198, 1.0],
+ [6.060745, 0.91160166, 1.0],
+ [2.6937423, 0.9596598, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.3760943, 0.8454239, 1.0],
+ [2.5560715, 0.9567565, 1.0],
+ [2.1207442, 0.9821871, 1.0],
+ [0.37868842, 0.99643296, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.3878245, 0.8679573, 1.0],
+ [0.71156067, 0.96918815, 1.0],
+ [0.12126909, 0.99451196, 1.0],
+ [0.001029497, 0.9995172, 1.0],
+ [0.0, 1.0, 1.0],
+ [3.1282337, 0.8924943, 1.0],
+ [0.5896895, 0.9759098, 1.0],
+ [-0.11373672, 0.9960285, 1.0],
+ [-0.01838675, 0.9997154, 1.0],
+ [0.0, 1.0, 1.0],
+ [4.3400493, 0.90476185, 1.0],
+ [1.3310212, 0.9786638, 1.0],
+ [0.17455858, 0.99855655, 1.0],
+ [0.005317117, 0.9999772, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.8491004, 0.93242216, 1.0],
+ [1.6193472, 0.9691039, 1.0],
+ [0.80294114, 0.99088013, 1.0],
+ [0.029686294, 0.9997005, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4401128, 0.9450957, 1.0],
+ [-0.16860908, 0.97805417, 1.0],
+ [0.08968142, 0.9966332, 1.0],
+ [0.0058796364, 0.9999138, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.9945528, 0.91562444, 1.0],
+ [-1.0134145, 0.97015774, 1.0],
+ [-0.3692183, 0.9947299, 1.0],
+ [-0.007456935, 0.9999008, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.055624425, 0.9145302, 1.0],
+ [-2.5425062, 0.9648608, 1.0],
+ [-1.0486484, 0.98947704, 1.0],
+ [-0.02597243, 0.99974424, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.5285127, 0.9355288, 1.0],
+ [-2.3404818, 0.95899427, 1.0],
+ [-1.5345422, 0.98380965, 1.0],
+ [-0.061189, 0.9995191, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.72061044, 0.92197156, 1.0],
+ [-5.383721, 0.90637255, 1.0],
+ [-3.6513355, 0.9478104, 1.0],
+ [-0.16841647, 0.9974061, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.028428238, 0.89694136, 1.0],
+ [-7.034104, 0.86039716, 1.0],
+ [-6.6405563, 0.88451445, 1.0],
+ [-0.60662454, 0.98856294, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.0993353, 0.8637165, 1.0],
+ [-7.684664, 0.8347192, 1.0],
+ [-6.927881, 0.8624654, 1.0],
+ [-0.7485936, 0.98060685, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.6461796, 0.892773, 1.0],
+ [-7.967004, 0.8294017, 1.0],
+ [-6.8060684, 0.8550583, 1.0],
+ [-0.8756036, 0.9734578, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.017491894, 0.8894634, 1.0],
+ [-13.43903, 0.7625003, 1.0],
+ [-16.398203, 0.7407526, 1.0],
+ [-4.292457, 0.92504275, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.3580081, 0.8969149, 1.0],
+ [-12.114263, 0.7745682, 1.0],
+ [-15.785243, 0.74346626, 1.0],
+ [-3.6444035, 0.9348789, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.7327255, 0.87856543, 1.0],
+ [-6.4373803, 0.8177134, 1.0],
+ [-10.325781, 0.78358805, 1.0],
+ [-2.567262, 0.9422052, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.7113199, 0.8661993, 1.0],
+ [-6.172294, 0.80224586, 1.0],
+ [-8.819174, 0.79800993, 1.0],
+ [-2.020759, 0.95133585, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.265015, 0.9794298, 1.0],
+ [-2.8535793, 0.9150202, 1.0],
+ [-2.305115, 0.93263686, 1.0],
+ [-0.16463748, 0.9945547, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.040856205, 0.9830346, 1.0],
+ [-1.0551267, 0.93554235, 1.0],
+ [-1.7591288, 0.8933596, 1.0],
+ [-0.34586504, 0.97479594, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.04883148, 0.94569665, 1.0],
+ [-0.56813735, 0.9045709, 1.0],
+ [-1.5370053, 0.81085134, 1.0],
+ [-0.67263573, 0.8751091, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.3425455, 0.9003105, 1.0],
+ [0.7451464, 0.8549796, 1.0],
+ [0.20236868, 0.8441968, 1.0],
+ [0.07073531, 0.86383015, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.8022106, 0.80535203, 1.0],
+ [4.376988, 0.70769286, 1.0],
+ [4.8166633, 0.707951, 1.0],
+ [2.8132818, 0.8092864, 1.0],
+ [0.0, 1.0, 1.0],
+ [2.3843334, 0.85017675, 1.0],
+ [5.65875, 0.6995908, 1.0],
+ [8.184944, 0.62843776, 1.0],
+ [5.3796787, 0.7383771, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.9017272, 0.9307022, 1.0],
+ [3.9063222, 0.765117, 1.0],
+ [6.974823, 0.69017345, 1.0],
+ [8.185112, 0.6926818, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.34200728, 0.96810126, 1.0],
+ [2.172312, 0.84994847, 1.0],
+ [8.207728, 0.66688955, 1.0],
+ [10.739886, 0.62480503, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.2027068, 0.9796112, 1.0],
+ [0.44695723, 0.9623203, 1.0],
+ [2.7991126, 0.8646282, 1.0],
+ [5.550562, 0.77532023, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.27983877, 0.96956384, 1.0],
+ [0.464609, 0.9587286, 1.0],
+ [2.1685085, 0.8807419, 1.0],
+ [3.9939811, 0.8027935, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.32165113, 0.96697015, 1.0],
+ [0.6435883, 0.93713695, 1.0],
+ [2.400229, 0.8268258, 1.0],
+ [3.7155714, 0.7548948, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.13139115, 0.98465353, 1.0],
+ [0.26204187, 0.9602435, 1.0],
+ [1.2602012, 0.87366205, 1.0],
+ [2.4989963, 0.7911792, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.04954044, 0.990862, 1.0],
+ [0.15550658, 0.9640937, 1.0],
+ [0.25354654, 0.9320879, 1.0],
+ [0.37589833, 0.8832426, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.045748647, 0.99577636, 1.0],
+ [0.049044535, 0.9851228, 1.0],
+ [0.006159843, 0.9524085, 1.0],
+ [0.02588024, 0.8944849, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.0030355332, 0.99710935, 1.0],
+ [-0.011863776, 0.9914036, 1.0],
+ [-0.054379225, 0.972176, 1.0],
+ [-0.039397053, 0.9642003, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.008150871, 0.9936041, 1.0],
+ [-0.037190154, 0.9887421, 1.0],
+ [-0.06706805, 0.96539193, 1.0],
+ [-0.0317106, 0.9736436, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.0109262755, 0.9985575, 1.0],
+ [-0.024314428, 0.99118114, 1.0],
+ [-0.047992434, 0.9725764, 1.0],
+ [-0.012975532, 0.9771226, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.011895402, 0.99871415, 1.0],
+ [-0.05900639, 0.9952044, 1.0],
+ [-0.07541064, 0.9911421, 1.0],
+ [-0.010232872, 0.99422073, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.014660832, 0.9972283, 1.0],
+ [-0.028494045, 0.9946739, 1.0],
+ [-0.04470135, 0.9915807, 1.0],
+ [-0.08405579, 0.9945791, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.033616997, 0.9969934, 1.0],
+ [-0.045168266, 0.993965, 1.0],
+ [-0.059918057, 0.98309493, 1.0],
+ [-0.062201336, 0.98883307, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.2381658, 0.99073696, 1.0],
+ [-0.47134387, 0.97534996, 1.0],
+ [-0.15076974, 0.978513, 1.0],
+ [-0.027898252, 0.9923376, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.094118536, 0.99575883, 1.0],
+ [-0.22728153, 0.9851976, 1.0],
+ [-0.14446811, 0.98079014, 1.0],
+ [-0.026381236, 0.9947049, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.027151769, 0.9987402, 1.0],
+ [-0.055821914, 0.9954352, 1.0],
+ [-0.07118323, 0.9875545, 1.0],
+ [-0.024681333, 0.9936937, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.057406988, 0.9985795, 1.0],
+ [-0.075127326, 0.99687934, 1.0],
+ [-0.012029578, 0.99905646, 1.0],
+ [-0.0011061644, 0.99975455, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.02290934, 0.99975824, 1.0],
+ [-0.025390672, 0.9995626, 1.0],
+ [-0.0026628294, 0.99994296, 1.0],
+ [-3.556243e-05, 0.9999988, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.017105369, 0.99992365, 1.0],
+ [-0.01626235, 0.9998567, 1.0],
+ [-0.0023494267, 0.9999825, 1.0],
+ [-3.9371847e-05, 0.9999998, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.01157836, 0.999962, 1.0],
+ [-0.002812421, 0.99997854, 1.0],
+ [-0.0007163898, 0.9999961, 1.0],
+ [-1.7496699e-05, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.014018655, 0.99996704, 1.0],
+ [-0.002332822, 0.9999732, 1.0],
+ [-0.0003527423, 0.99999386, 1.0],
+ [-5.771106e-06, 0.9999999, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.010408213, 0.99997354, 1.0],
+ [-0.0040600216, 0.99995077, 1.0],
+ [-0.0005498676, 0.999989, 1.0],
+ [-7.1796394e-06, 0.9999998, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.0023898028, 0.9999025, 1.0],
+ [-0.0021971867, 0.99995613, 1.0],
+ [-0.00040569165, 0.99999076, 1.0],
+ [-5.7337793e-06, 0.9999999, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.006176083, 0.99988633, 1.0],
+ [-0.0033466935, 0.99995404, 1.0],
+ [-0.00018771198, 0.99999684, 1.0],
+ [-1.5582656e-06, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.0078279935, 0.99997926, 1.0],
+ [-0.0021727988, 0.99997437, 1.0],
+ [-8.390261e-05, 0.9999989, 1.0],
+ [-7.6212564e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.011409035, 0.9999373, 1.0],
+ [-0.0012485707, 0.99999255, 1.0],
+ [-5.873547e-05, 0.99999946, 1.0],
+ [-1.1412731e-06, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.0320137, 0.9996883, 1.0],
+ [-0.0022234167, 0.99998236, 1.0],
+ [-4.2978718e-05, 0.99999964, 1.0],
+ [-8.9091026e-07, 1.0, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.060883675, 0.99925774, 1.0],
+ [-0.0041872207, 0.9999481, 1.0],
+ [-0.00016332162, 0.99999785, 1.0],
+ [-2.8002216e-06, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.026953986, 0.999662, 1.0],
+ [-0.00503322, 0.9999406, 1.0],
+ [-0.0005006954, 0.9999944, 1.0],
+ [-6.1892397e-06, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.019095117, 0.9997323, 1.0],
+ [-0.0048538097, 0.9999398, 1.0],
+ [-0.00037457192, 0.9999961, 1.0],
+ [-3.6742229e-06, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.03751212, 0.9993693, 1.0],
+ [-0.010522507, 0.99987054, 1.0],
+ [-0.0009128494, 0.99999034, 1.0],
+ [-7.887352e-06, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.05509656, 0.99895823, 1.0],
+ [-0.005975121, 0.99990004, 1.0],
+ [-0.00018607976, 0.9999967, 1.0],
+ [-2.4801245e-06, 0.99999994, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.06763289, 0.9990122, 1.0],
+ [-0.026763257, 0.99962497, 1.0],
+ [-0.0033132737, 0.9999459, 1.0],
+ [-5.9110884e-05, 0.999999, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.06290056, 0.9987752, 1.0],
+ [-0.06590056, 0.9988833, 1.0],
+ [-0.016112784, 0.9997368, 1.0],
+ [-0.00030407612, 0.9999951, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.06050088, 0.9985156, 1.0],
+ [-0.017029036, 0.99951345, 1.0],
+ [-0.0026492565, 0.9999436, 1.0],
+ [-4.364142e-05, 0.9999992, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.21668494, 0.99283016, 1.0],
+ [-0.19338678, 0.9937941, 1.0],
+ [-0.029703936, 0.99916244, 1.0],
+ [-0.00055253773, 0.9999868, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.0463094, 0.95176995, 1.0],
+ [-1.3973116, 0.9536363, 1.0],
+ [-0.50282496, 0.98618114, 1.0],
+ [-0.019256245, 0.9995079, 1.0],
+ [0.0, 1.0, 1.0],
+ [-3.5435355, 0.84623945, 1.0],
+ [-5.1535788, 0.84832674, 1.0],
+ [-2.824056, 0.9316529, 1.0],
+ [-0.15195742, 0.9963698, 1.0],
+ [0.0, 1.0, 1.0],
+ [-8.673897, 0.7131587, 1.0],
+ [-12.270074, 0.68430734, 1.0],
+ [-11.376253, 0.73719627, 1.0],
+ [-1.8281991, 0.95513356, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.039047, 0.6432662, 1.0],
+ [-13.724231, 0.6535759, 1.0],
+ [-14.0326, 0.6799867, 1.0],
+ [-9.946063, 0.780004, 1.0],
+ [0.0, 1.0, 1.0],
+ [-12.528839, 0.6221673, 1.0],
+ [-13.425968, 0.65002877, 1.0],
+ [-13.075199, 0.67871696, 1.0],
+ [-10.942493, 0.7442244, 1.0],
+ [0.0, 1.0, 1.0],
+ [-10.306666, 0.6389583, 1.0],
+ [-12.0551195, 0.66896164, 1.0],
+ [-9.823601, 0.7308576, 1.0],
+ [-5.281826, 0.83818936, 1.0],
+ [0.0, 1.0, 1.0],
+ [-5.3867707, 0.65209216, 1.0],
+ [-5.8347864, 0.71964204, 1.0],
+ [-3.59081, 0.8329669, 1.0],
+ [-1.4847257, 0.9156909, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.754784, 0.74886227, 1.0],
+ [-0.24797672, 0.74751395, 1.0],
+ [0.6299296, 0.7613501, 1.0],
+ [0.82416815, 0.8632212, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.4033656, 0.9101428, 1.0],
+ [2.4547875, 0.81642723, 1.0],
+ [3.6370852, 0.787311, 1.0],
+ [3.6531272, 0.8031764, 1.0],
+ [0.0, 1.0, 1.0],
+ [1.9459922, 0.951957, 1.0],
+ [2.5553524, 0.8894862, 1.0],
+ [3.6345732, 0.81872904, 1.0],
+ [2.785436, 0.8552696, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.679637, 0.9787555, 1.0],
+ [3.8709927, 0.9172711, 1.0],
+ [5.7068896, 0.8849159, 1.0],
+ [1.1118813, 0.9758798, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.3420551, 0.98117673, 1.0],
+ [1.5517482, 0.967525, 1.0],
+ [2.653233, 0.95270234, 1.0],
+ [0.44305915, 0.9921006, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.23155965, 0.9924181, 1.0],
+ [0.11397146, 0.99741286, 1.0],
+ [0.13187227, 0.9980809, 1.0],
+ [0.020392587, 0.9996969, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.13352406, 0.99550617, 1.0],
+ [0.025518132, 0.99892914, 1.0],
+ [-0.0005152045, 0.99990207, 1.0],
+ [-0.000118601536, 0.99999446, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.15706417, 0.99707514, 1.0],
+ [0.02578307, 0.9993154, 1.0],
+ [-0.00202729, 0.9999331, 1.0],
+ [-0.00021436776, 0.9999964, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.3615966, 0.99366856, 1.0],
+ [0.087646805, 0.99867254, 1.0],
+ [0.002637883, 0.99996406, 1.0],
+ [3.6361645e-05, 0.99999946, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.1291499, 0.9977887, 1.0],
+ [0.046300918, 0.99929285, 1.0],
+ [0.012817254, 0.9998544, 1.0],
+ [0.00041612334, 0.9999955, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.011200566, 0.9983723, 1.0],
+ [-0.008214572, 0.99942863, 1.0],
+ [0.0021838315, 0.9999416, 1.0],
+ [0.000100621306, 0.9999986, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.060381968, 0.9930565, 1.0],
+ [-0.06464544, 0.9983384, 1.0],
+ [-0.007711393, 0.99990016, 1.0],
+ [-0.00011417703, 0.9999988, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.11693042, 0.99224305, 1.0],
+ [-0.11525121, 0.9982498, 1.0],
+ [-0.027416473, 0.99979055, 1.0],
+ [-0.00054621015, 0.99999607, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.014975694, 0.99728876, 1.0],
+ [-0.056130975, 0.9989796, 1.0],
+ [-0.0222607, 0.9997437, 1.0],
+ [-0.00058228534, 0.9999938, 1.0],
+ [0.0, 1.0, 1.0],
+ [0.05060913, 0.99668014, 1.0],
+ [-0.2700596, 0.99520206, 1.0],
+ [-0.14083444, 0.9978208, 1.0],
+ [-0.005215654, 0.9999189, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.05793456, 0.99310505, 1.0],
+ [-1.3339288, 0.97664934, 1.0],
+ [-1.0737243, 0.98243177, 1.0],
+ [-0.06070526, 0.9990094, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.22500885, 0.9885798, 1.0],
+ [-2.394239, 0.9580366, 1.0],
+ [-1.8970352, 0.9690168, 1.0],
+ [-0.109246016, 0.99819875, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.39491686, 0.98909, 1.0],
+ [-2.9813097, 0.9401541, 1.0],
+ [-2.3468347, 0.9552055, 1.0],
+ [-0.15643519, 0.99701655, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.8292645, 0.9594883, 1.0],
+ [-13.511781, 0.763661, 1.0],
+ [-14.536935, 0.7543218, 1.0],
+ [-2.2238204, 0.95809615, 1.0],
+ [0.0, 1.0, 1.0],
+ [-1.0053811, 0.97643167, 1.0],
+ [-12.578439, 0.77343625, 1.0],
+ [-15.1597395, 0.73498416, 1.0],
+ [-2.625323, 0.9481359, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.55154705, 0.9794314, 1.0],
+ [-6.9208384, 0.85034823, 1.0],
+ [-10.053682, 0.79368883, 1.0],
+ [-1.6742866, 0.96229243, 1.0],
+ [0.0, 1.0, 1.0],
+ [-0.69403476, 0.96975285, 1.0],
+ [-3.2779868, 0.9127137, 1.0],
+ [-4.176942, 0.9027185, 1.0],
+ [-0.5743538, 0.98639596, 1.0]
+ ],
+ "srgb_value": true
+ }
+}
diff --git a/contributors/people.toml b/contributors/people.toml
index 27a8c8d9c..08d002e8b 100644
--- a/contributors/people.toml
+++ b/contributors/people.toml
@@ -25,9 +25,35 @@ sync_github_name = false
[people.rock-meister]
sync_github_name = true
+[people.Th4nat0s]
+display_name = "Thanat0s"
+
[people.sanyamsingh001]
display_name = "sanyamsingh001"
[people.ArekatlaNishanthchowdary]
real_name = "Arekatla Nishanth Chowdary"
sync_github_name = true
+
+[people.Losminek]
+sync_github_name = true
+
+[people.FooVoo]
+real_name = "Aleksandr Batyrev"
+sync_github_name = true
+
+[people.Silvertree2010]
+display_name = "Netter"
+sync_github_name = false
+
+[people.nledford]
+real_name = "Nathaniel Ledford"
+
+[people.marcoarnulfo]
+real_name = "Marco Arnulfo"
+display_name = "Marco Arnulfo"
+sync_github_name = false
+
+[people.egegl]
+real_name = "Ege Gursel"
+sync_github_name = true
diff --git a/crates/catalog/proptest-regressions/tests_index.txt b/crates/catalog/proptest-regressions/tests_index.txt
new file mode 100644
index 000000000..9c827f0c0
--- /dev/null
+++ b/crates/catalog/proptest-regressions/tests_index.txt
@@ -0,0 +1,8 @@
+# Seeds for failure cases proptest has generated in the past. It is
+# automatically read and these particular cases re-run before any
+# novel cases are generated.
+#
+# It is recommended to check this file in to source control so that
+# everyone who runs the test benefits from these saved cases.
+cc b91df6f4f23ec90cae3c0fb934b04b806181b61570f00ba25826e2232b7e1932 # shrinks to steps = [(3, 0, 0, false)]
+cc b11415bdf5e4133b65e00645d4051129c71bf927747d6384aa50107440e92e07 # shrinks to steps = [(0, 10, 166, false), (7, 1, 0, false), (1, 14, 0, false)]
diff --git a/crates/catalog/src/dates.rs b/crates/catalog/src/dates.rs
index d180d013d..4d16c2f4d 100644
--- a/crates/catalog/src/dates.rs
+++ b/crates/catalog/src/dates.rs
@@ -49,7 +49,9 @@ pub fn shift_iso(iso: &str, secs: i64) -> Option {
if !(first..=last).contains(&t) {
return None;
}
- let tail = iso.trim().get(19..).unwrap_or("");
+ let iso = iso.trim();
+ // the seconds are optional (`10:00+02:00`): without them the tail starts right after the minutes
+ let tail = iso.get(if iso.get(16..17) == Some(":") { 19 } else { 16 }..).unwrap_or("");
Some(format!("{}{tail}", civil(t)))
}
@@ -222,6 +224,15 @@ mod tests {
assert_eq!(normalize_iso("2026-02-30"), None);
}
+ #[test]
+ fn shift_keeps_the_zone_of_a_time_without_seconds() {
+ assert_eq!(shift_iso("2026-01-01T10:00+02:00", 3600).as_deref(), Some("2026-01-01T11:00:00+02:00"));
+ assert_eq!(shift_iso("2026-01-01T10:00Z", 60).as_deref(), Some("2026-01-01T10:01:00Z"));
+ assert_eq!(shift_iso("2026-01-01T10:00", 3600).as_deref(), Some("2026-01-01T11:00:00"));
+ assert_eq!(shift_iso("2026-01-01T10:00:00.5-05:00", 3600).as_deref(), Some("2026-01-01T11:00:00.5-05:00"));
+ assert_eq!(iso_seconds("2026-01-01T10:00+02:00"), iso_seconds("2026-01-01T10:00"));
+ }
+
#[test]
fn runs_by_day_month_year_keep_stacks_together() {
let mut c = Catalog::new();
diff --git a/crates/catalog/src/index.rs b/crates/catalog/src/index.rs
new file mode 100644
index 000000000..2c347473b
--- /dev/null
+++ b/crates/catalog/src/index.rs
@@ -0,0 +1,101 @@
+//! Lookups derived from the catalog's own data, so questions asked once per photo of a big library
+//! don't scan a list each time: which stack a photo is in, whether a regular album holds it.
+//!
+//! They are never saved (the on-disk format is unchanged). Each is built on first use from what
+//! the catalog holds, so a catalog read from a snapshot, replayed from the journal or cloned starts
+//! without them and builds them when asked. Once built, [`crate::Catalog::apply`] keeps them up to
+//! date: the ops that change stacks or album photos (`AddStack`, `RemoveStack`, `SetStack`,
+//! `AddAlbum`, `RemoveAlbum`, `SetAlbumPhotos`) update them as they apply, inverses and the
+//! rollback of a failed batch included, as those are ops too.
+
+use std::collections::{HashMap, HashSet};
+use std::sync::OnceLock;
+
+use crate::{AlbumId, Catalog, PhotoId, StackId};
+
+#[derive(Default)]
+pub(crate) struct Indexes {
+ /// Photo → the stack it is in; `None` when the catalog was read with a photo in two stacks
+ /// (only a hand-edited file can hold that: ops refuse it), where a scan says which one counts.
+ stacks: OnceLock>>,
+ /// Album → its photos (regular albums; a smart album's list is empty).
+ albums: OnceLock>>,
+}
+
+/// A copy builds its own when asked (copies are made to be saved, not queried).
+impl Clone for Indexes {
+ fn clone(&self) -> Self {
+ Indexes::default()
+ }
+}
+
+/// Derived from the rest of the catalog: two catalogs with the same data are equal whatever was
+/// built.
+impl PartialEq for Indexes {
+ fn eq(&self, _: &Self) -> bool {
+ true
+ }
+}
+
+impl std::fmt::Debug for Indexes {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let stacks = self.stacks.get().map(|m| m.as_ref().map(HashMap::len));
+ f.debug_struct("Indexes").field("stacks", &stacks).field("albums", &self.albums.get().map(HashMap::len)).finish()
+ }
+}
+
+impl Indexes {
+ /// Stack `id` held `old` and holds `new` now (empty: it was added, or is gone).
+ pub(crate) fn stack_changed(&mut self, id: StackId, old: &[PhotoId], new: &[PhotoId]) {
+ let Some(Some(m)) = self.stacks.get_mut() else { return };
+ for p in old {
+ if m.get(p) == Some(&id) {
+ m.remove(p);
+ }
+ }
+ for p in new {
+ m.insert(*p, id);
+ }
+ }
+
+ /// Album `id` holds `photos` now (`None`: it is gone).
+ pub(crate) fn album_changed(&mut self, id: AlbumId, photos: Option<&[PhotoId]>) {
+ let Some(m) = self.albums.get_mut() else { return };
+ match photos {
+ Some(photos) => {
+ m.insert(id, photos.iter().copied().collect());
+ }
+ None => {
+ m.remove(&id);
+ }
+ }
+ }
+}
+
+impl Catalog {
+ /// The stack photo `photo` is in, by the index (built on first use).
+ pub(crate) fn stack_id_of(&self, photo: PhotoId) -> Option {
+ let m = self.index.stacks.get_or_init(|| {
+ let mut m = HashMap::new();
+ for s in self.stacks.values() {
+ for p in &s.photos {
+ if m.insert(*p, s.id).is_some_and(|other| other != s.id) {
+ return None;
+ }
+ }
+ }
+ Some(m)
+ });
+ match m {
+ Some(m) => m.get(&photo).copied(),
+ // in id order, the first stack listing the photo has it
+ None => self.stacks.values().find(|s| s.photos.contains(&photo)).map(|s| s.id),
+ }
+ }
+
+ /// Whether album `album`'s own list holds `photo`, by the index (built on first use).
+ pub(crate) fn album_lists(&self, album: AlbumId, photo: PhotoId) -> bool {
+ let m = self.index.albums.get_or_init(|| self.albums.values().map(|a| (a.id, a.photos.iter().copied().collect())).collect());
+ m.get(&album).is_some_and(|s| s.contains(&photo))
+ }
+}
diff --git a/crates/catalog/src/journal.rs b/crates/catalog/src/journal.rs
index 98fd616a3..edcfae22a 100644
--- a/crates/catalog/src/journal.rs
+++ b/crates/catalog/src/journal.rs
@@ -51,6 +51,7 @@
//! | 6 | folder labels | `Op::SetFolderRecord`, `Catalog.folder_records` |
//! | 7 | Kelvin white balance for raws whose camera has matrices (#730) | `Op::SetAsShotWb`, `WhiteBalance.scale` |
//! | 8 | History cursor: Go to History Step adds no step | `Op::SetHistoryCursor`, `Photo.history_cursor` |
+//! | 9 | preview-only recheck | `Photo.preview_checked`, `Op::SetContent.preview_checked` |
//!
//! Rules:
//! - **Bump [`VERSION`]** (and add a row above) in the change that adds an [`Op`] variant or a
@@ -75,7 +76,7 @@ pub const LOG: &str = "catalog.log";
const FORMAT: &str = "lightcraft-catalog";
/// The catalog format this build writes (and the newest it reads). See the module docs →
/// *Format versions*; bump it whenever an [`Op`] variant or a serialized field is added.
-pub const VERSION: u32 = 8;
+pub const VERSION: u32 = 9;
/// When [`Journal::wants_snapshot`] says it's time to compact the log.
#[derive(Clone, Copy, Debug)]
@@ -270,7 +271,7 @@ fn resync(line: &[u8], last: u64) -> Option<(usize, (u64, Op))> {
const START: &[u8] = b"{\"seq\":";
(1..line.len().saturating_sub(START.len() - 1)).filter(|&i| line[i..].starts_with(START)).find_map(|i| {
let rec = std::str::from_utf8(&line[i..]).ok().map(|l| l.trim_end_matches('\r')).and_then(decode_record)?;
- (rec.0 <= last + 1).then_some((i, rec))
+ (rec.0 <= last.saturating_add(1)).then_some((i, rec))
})
}
@@ -297,6 +298,9 @@ fn write_snapshot(store: &mut dyn Store, seq: u64, catalog: &Catalog) -> std::io
Ok(SnapshotTiming { serialize_ms, write_sync_ms: (ms_since(t0) - serialize_ms).max(0.0), bytes, ..Default::default() })
}
+/// The highest `seq` a snapshot may hold; beyond it the counter is treated as corrupt, far from overflow.
+const MAX_SEQ: u64 = u64::MAX / 2;
+
/// Just the identification of `catalog.snap` (read before the catalog itself).
#[derive(serde::Deserialize)]
struct SnapHeader {
@@ -330,6 +334,10 @@ impl Journal {
return Err(newer(format!("{SNAPSHOT} is catalog format v{}", h.version)));
}
let s: SnapFile = serde_json::from_slice(&bytes).map_err(|e| CatalogError::Corrupt(format!("{SNAPSHOT}: {e}")))?;
+ // a seq this high can't come from real use; the next record's seq would wrap
+ if s.seq > MAX_SEQ {
+ return Err(CatalogError::Corrupt(format!("{SNAPSHOT}: seq {} is out of range", s.seq)));
+ }
let mut catalog = s.catalog;
report.repaired = catalog.repair();
for r in &report.repaired {
@@ -391,7 +399,7 @@ impl Journal {
let _ = op;
report.stale += 1;
}
- Some((seq, op)) if seq == j.seq + 1 => {
+ Some((seq, op)) if j.seq.checked_add(1) == Some(seq) => {
if catalog.apply(op).is_err() {
report.failed += 1;
} else {
@@ -475,7 +483,7 @@ impl Journal {
let mut buf = String::new();
let mut seq = self.seq;
for op in ops {
- seq += 1;
+ seq = seq.checked_add(1).ok_or_else(|| CatalogError::Corrupt(format!("{LOG}: op counter overflow")))?;
buf.push_str(&encode_record(seq, op));
buf.push('\n');
}
@@ -526,7 +534,7 @@ impl Journal {
log::warn!("catalog: background snapshot failed ({e}); writing one now");
}
let t0 = web_time::Instant::now();
- let seq = self.seq + unlogged;
+ let seq = self.seq.checked_add(unlogged).ok_or_else(|| CatalogError::Corrupt(format!("{SNAPSHOT}: op counter overflow")))?;
let mut timing = match write_snapshot(self.store.as_mut(), seq, catalog) {
Ok(t) => t,
Err(e) => {
diff --git a/crates/catalog/src/lib.rs b/crates/catalog/src/lib.rs
index ed4cb75b4..1776c895c 100644
--- a/crates/catalog/src/lib.rs
+++ b/crates/catalog/src/lib.rs
@@ -15,6 +15,7 @@
pub mod dates;
pub mod folders;
+mod index;
pub mod journal;
pub mod keywords;
pub mod local;
@@ -25,6 +26,7 @@ pub mod rules;
pub mod safe_file;
pub mod stacks;
pub mod store;
+pub mod tally;
use std::collections::BTreeMap;
use std::sync::Arc;
@@ -214,6 +216,10 @@ pub enum Op {
/// `None` = its raw data decodes (or it isn't a raw).
#[serde(default, skip_serializing_if = "Option::is_none")]
preview_only: Option,
+ /// The app version that decided `preview_only` (see [`Photo::preview_checked`]); empty
+ /// when it is `None`.
+ #[serde(default, skip_serializing_if = "String::is_empty")]
+ preview_checked: String,
},
/// The lens data a photo's file carries (what Reload finds when it was read after import).
SetEmbeddedLens {
@@ -281,6 +287,74 @@ pub struct Catalog {
/// Increments on every applied op.
#[serde(skip)]
pub revision: u64,
+ /// Which photos the latest ops touched ([`Catalog::touched_since`]).
+ #[serde(skip)]
+ changes: Changes,
+ /// Lookups derived from the above (never saved), see [`index`].
+ #[serde(skip)]
+ index: index::Indexes,
+}
+
+/// What of a photo an op changed: enough to tell which tallies of the library an edit leaves as they were (a
+/// rating changes no folder's count; a slider changes none at all).
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub struct Aspects(u16);
+
+impl Aspects {
+ pub const NONE: Aspects = Aspects(0);
+ pub const RATING: Aspects = Aspects(1);
+ pub const FLAG: Aspects = Aspects(1 << 1);
+ pub const LABEL: Aspects = Aspects(1 << 2);
+ /// Develop settings, versions and history.
+ pub const DEVELOP: Aspects = Aspects(1 << 3);
+ /// Title, caption, keywords, people, place and the rest of the metadata.
+ pub const META: Aspects = Aspects(1 << 4);
+ pub const CAPTURED: Aspects = Aspects(1 << 5);
+ /// Where its file is.
+ pub const FILE: Aspects = Aspects(1 << 6);
+ pub const ANALYSIS: Aspects = Aspects(1 << 7);
+ /// Whether it is in the library at all: added, removed, deleted, restored, taken in from Local.
+ pub const PRESENCE: Aspects = Aspects(1 << 8);
+ /// Everything: the photo may count differently for any tally.
+ pub const ALL: Aspects = Aspects(u16::MAX);
+
+ pub const fn with(self, other: Aspects) -> Aspects {
+ Aspects(self.0 | other.0)
+ }
+
+ /// Whether any of `other` is among these.
+ pub const fn any_of(self, other: Aspects) -> bool {
+ self.0 & other.0 != 0
+ }
+}
+
+/// The most photos remembered as touched; older changes are then answered with "anything may have changed".
+const MAX_TOUCHED: usize = 8192;
+
+/// A short memory of what the latest ops changed, so that what is derived from the catalog (the grid's list) can
+/// follow a small edit without being worked out again from every photo.
+#[derive(Clone, Debug, Default)]
+struct Changes {
+ /// The photos ops changed, each with the revision the op made and what of the photo it changed. Oldest first.
+ touched: std::collections::VecDeque<(u64, PhotoId, Aspects)>,
+ /// What the op being applied changes of its photo.
+ now: Aspects,
+ /// The latest revision made by an op that changed more than photos of its own (an album, a stack, a keyword of
+ /// the list…): what it changed for which photos is not known.
+ broad: u64,
+ /// Changes up to this revision are no longer all in `touched`.
+ floor: u64,
+ /// The revision the last op recorded here made. When the catalog's revision is another, it was set from
+ /// outside (a library just loaded takes a range of its own, so that nothing kept from another library is
+ /// taken for this one's): nothing is known of how the catalog got to where it is.
+ seen: u64,
+}
+
+impl PartialEq for Changes {
+ /// Two catalogs are the same catalog whatever each remembers of how it got there.
+ fn eq(&self, _: &Self) -> bool {
+ true
+ }
}
impl Catalog {
@@ -365,13 +439,18 @@ impl Catalog {
/// no smart album).
pub fn album_contains(&self, id: AlbumId, p: &Photo) -> bool {
match self.albums.get(&id) {
- // guarded: a smart album testing smart albums can't loop or recurse without end
- Some(Album { smart: Some(rules), .. }) => !p.deleted && rules::smart_album_holds(id, p.id, || rules.matches(p, self)),
- Some(a) => a.photos.contains(&p.id),
+ Some(Album { smart: Some(rules), .. }) => self.smart_album_has(id, rules, &rules.prepare_one(), p),
+ Some(a) => self.album_lists(a.id, p.id),
None => false,
}
}
+ /// Whether smart album `id` with `rules` (as [`Filter::prepare`]d) holds `p`.
+ fn smart_album_has(&self, id: AlbumId, rules: &Filter, prepared: &query::Prepared, p: &Photo) -> bool {
+ // guarded: a smart album testing smart albums can't loop or recurse without end
+ !p.deleted && rules::smart_album_holds(id, p.id, || rules.matches_in(p, self, prepared))
+ }
+
/// Whether smart album `from` tests `to`, directly or through the smart albums it tests (the
/// rules of `from` would then change when those of `to` do). Loops in saved rules end the
/// search, they don't repeat it.
@@ -419,7 +498,10 @@ impl Catalog {
/// The photos of an album: the stored list, or a smart album's current matches (id order).
pub fn album_photos(&self, id: AlbumId) -> Vec {
match self.albums.get(&id) {
- Some(Album { smart: Some(_), .. }) => self.photos.values().filter(|p| self.album_contains(id, p)).map(|p| p.id).collect(),
+ Some(Album { smart: Some(rules), .. }) => {
+ let prepared = rules.prepare();
+ self.photos.values().filter(|p| self.smart_album_has(id, rules, &prepared, p)).map(|p| p.id).collect()
+ }
Some(a) => a.photos.clone(),
None => Vec::new(),
}
@@ -429,7 +511,10 @@ impl Catalog {
pub fn album_count(&self, id: AlbumId) -> usize {
match self.albums.get(&id) {
// counted in place: no id list is built just for its length
- Some(Album { smart: Some(_), .. }) => self.photos.values().filter(|p| self.album_contains(id, p)).count(),
+ Some(Album { smart: Some(rules), .. }) => {
+ let prepared = rules.prepare();
+ self.photos.values().filter(|p| self.smart_album_has(id, rules, &prepared, p)).count()
+ }
Some(a) => a.photos.iter().filter(|p| self.photos.get(p).is_some_and(|p| !p.deleted)).count(),
None => 0,
}
@@ -479,20 +564,110 @@ impl Catalog {
// ---- writes
fn photo_mut(&mut self, id: PhotoId) -> Result<&mut Photo> {
+ self.touch(id);
self.photos.get_mut(&id).map(Arc::make_mut).ok_or(CatalogError::NoPhoto(id))
}
+
+ /// The op being applied changes photo `id` (or adds or removes it).
+ fn touch(&mut self, id: PhotoId) {
+ // (the revision the op will make: `apply` counts up once it is done)
+ self.changes.touched.push_back((self.revision.saturating_add(1), id, self.changes.now));
+ while self.changes.touched.len() > MAX_TOUCHED {
+ if let Some((revision, _, _)) = self.changes.touched.pop_front() {
+ self.changes.floor = revision;
+ }
+ }
+ }
+
+ /// What of its photos the ops since `revision` changed, taken together ([`Aspects::NONE`] when there were
+ /// none). `None` when that is not known ([`Catalog::touched_since`]). A tally of the library that reads only
+ /// other things of a photo is still right.
+ pub fn aspects_since(&self, revision: u64) -> Option {
+ if !self.remembers(revision) {
+ return None;
+ }
+ Some(self.changes.touched.iter().filter(|(r, _, _)| *r > revision).fold(Aspects::NONE, |all, (_, _, a)| all.with(*a)))
+ }
+
+ /// Whether every change since the catalog was at `revision` is remembered photo by photo.
+ fn remembers(&self, revision: u64) -> bool {
+ self.changes.seen == self.revision && revision <= self.revision && revision >= self.changes.floor && self.changes.broad <= revision
+ }
+
+ /// The photos changed, added or removed since the catalog was at `revision`, each once. `None` when that is not
+ /// known: an op since then changed more than photos of its own, or it was too long ago. Then anything may
+ /// have changed.
+ pub fn touched_since(&self, revision: u64) -> Option> {
+ if !self.remembers(revision) {
+ return None;
+ }
+ let mut ids: Vec = self.changes.touched.iter().filter(|(r, _, _)| *r > revision).map(|(_, id, _)| *id).collect();
+ ids.sort_unstable();
+ ids.dedup();
+ Some(ids)
+ }
fn album_mut(&mut self, id: AlbumId) -> Result<&mut Album> {
self.albums.get_mut(&id).ok_or(CatalogError::NoAlbum(id))
}
/// Apply an op; returns its inverse. On error nothing changes.
pub fn apply(&mut self, op: Op) -> Result {
+ if self.changes.seen != self.revision {
+ // the revision was set from outside: what is remembered is of another past
+ self.changes = Changes { floor: self.revision, seen: self.revision, ..Default::default() };
+ }
let inv = self.apply_inner(op)?;
self.revision += 1;
+ self.changes.seen = self.revision;
Ok(inv)
}
fn apply_inner(&mut self, op: Op) -> Result {
+ // An op that changes one photo says which (`photo_mut`, and adding or removing it below). Every other kind
+ // can change what any photo matches or where it sorts: an album's photos or rules, a stack, a keyword's
+ // name. Listed kind by kind, so a new one has to be put on one side.
+ match &op {
+ // (a photo that comes, goes, or changes what it is to the library counts for every tally; so do the
+ // rare kinds whose reach nobody has to think about)
+ Op::AddPhoto { photo } => {
+ self.changes.now = Aspects::ALL;
+ self.touch(photo.id);
+ }
+ Op::RemovePhoto { id } => {
+ self.changes.now = Aspects::ALL;
+ self.touch(*id);
+ }
+ Op::SetDeleted { .. } | Op::SetLocal { .. } | Op::SetContent { .. } | Op::SetEmbeddedLens { .. } | Op::SetAsShotWb { .. } => {
+ self.changes.now = Aspects::ALL
+ }
+ Op::SetRating { .. } => self.changes.now = Aspects::RATING,
+ Op::SetFlag { .. } => self.changes.now = Aspects::FLAG,
+ Op::SetLabel { .. } => self.changes.now = Aspects::LABEL,
+ Op::SetMeta { .. } => self.changes.now = Aspects::META,
+ Op::SetCaptured { .. } => self.changes.now = Aspects::CAPTURED,
+ Op::SetFile { .. } | Op::Relink { .. } => self.changes.now = Aspects::FILE,
+ Op::SetAnalysis { .. } => self.changes.now = Aspects::ANALYSIS,
+ Op::SetDevelop { .. } | Op::SetVersions { .. } | Op::SetHistory { .. } | Op::PushHistory { .. } | Op::SetHistoryCursor { .. } => {
+ self.changes.now = Aspects::DEVELOP
+ }
+ // (its ops say for themselves, one by one)
+ Op::Batch { .. } => {}
+ Op::AddAlbum { .. }
+ | Op::RemoveAlbum { .. }
+ | Op::RenameAlbum { .. }
+ | Op::MoveAlbum { .. }
+ | Op::SetAlbumOrder { .. }
+ | Op::SetAlbumPhotos { .. }
+ | Op::SetAlbumCover { .. }
+ | Op::SetAlbumRules { .. }
+ | Op::AddStack { .. }
+ | Op::RemoveStack { .. }
+ | Op::SetStack { .. }
+ | Op::SetLabelName { .. }
+ | Op::SetBrowsed { .. }
+ | Op::SetKeyword { .. }
+ | Op::SetFolderRecord { .. } => self.changes.broad = self.revision.saturating_add(1),
+ }
Ok(match op {
Op::AddPhoto { photo } => {
if self.photos.contains_key(&photo.id) {
@@ -590,6 +765,7 @@ impl Catalog {
}
let id = album.id;
self.next_album = self.next_album.max(id.0.saturating_add(1));
+ self.index.album_changed(id, Some(&album.photos));
self.albums.insert(id, album);
Op::RemoveAlbum { id }
}
@@ -598,6 +774,7 @@ impl Catalog {
return Err(CatalogError::Invalid("folder is not empty".into()));
}
let a = self.albums.remove(&id).ok_or(CatalogError::NoAlbum(id))?;
+ self.index.album_changed(id, None);
Op::AddAlbum { album: a }
}
Op::RenameAlbum { id, name } => {
@@ -637,13 +814,16 @@ impl Catalog {
Op::SetAlbumOrder { id, order: std::mem::replace(&mut a.order, order) }
}
Op::SetAlbumPhotos { id, photos } => {
- let a = self.album_mut(id)?;
+ // the field, not `album_mut`: the index is updated while the album is borrowed
+ let a = self.albums.get_mut(&id).ok_or(CatalogError::NoAlbum(id))?;
if (a.folder || a.smart.is_some()) && !photos.is_empty() {
return Err(CatalogError::Invalid(
if a.folder { "folders can't hold photos" } else { "smart albums update automatically" }.into(),
));
}
- Op::SetAlbumPhotos { id, photos: std::mem::replace(&mut a.photos, photos) }
+ let old = std::mem::replace(&mut a.photos, photos);
+ self.index.album_changed(id, Some(&a.photos));
+ Op::SetAlbumPhotos { id, photos: old }
}
Op::SetAlbumCover { id, cover } => {
let a = self.album_mut(id)?;
@@ -664,11 +844,13 @@ impl Catalog {
self.validate_stack(stack.id, &stack.photos)?;
let id = stack.id;
self.next_stack = self.next_stack.max(id.0.saturating_add(1));
+ self.index.stack_changed(id, &[], &stack.photos);
self.stacks.insert(id, stack);
Op::RemoveStack { id }
}
Op::RemoveStack { id } => {
let s = self.stacks.remove(&id).ok_or(CatalogError::NoStack(id))?;
+ self.index.stack_changed(id, &s.photos, &[]);
Op::AddStack { stack: s }
}
Op::SetStack { id, photos, collapsed } => {
@@ -677,7 +859,9 @@ impl Catalog {
}
self.validate_stack(id, &photos)?;
let s = self.stacks.get_mut(&id).ok_or(CatalogError::NoStack(id))?;
- let old = Op::SetStack { id, photos: std::mem::replace(&mut s.photos, photos), collapsed: s.collapsed };
+ let old_photos = std::mem::replace(&mut s.photos, photos);
+ self.index.stack_changed(id, &old_photos, &s.photos);
+ let old = Op::SetStack { id, photos: old_photos, collapsed: s.collapsed };
s.collapsed = collapsed;
old
}
@@ -700,7 +884,7 @@ impl Catalog {
let old_format = format.map(|f| std::mem::replace(&mut p.format, f));
Op::Relink { id, file_name: old_name, source: std::mem::replace(&mut p.source, source), format: old_format }
}
- Op::SetContent { id, width, height, file_size, content_hash, preview_only } => {
+ Op::SetContent { id, width, height, file_size, content_hash, preview_only, preview_checked } => {
let p = self.photo_mut(id)?;
Op::SetContent {
id,
@@ -709,6 +893,7 @@ impl Catalog {
file_size: std::mem::replace(&mut p.file_size, file_size),
content_hash: std::mem::replace(&mut p.content_hash, content_hash),
preview_only: std::mem::replace(&mut p.preview_only, preview_only),
+ preview_checked: std::mem::replace(&mut p.preview_checked, preview_checked),
}
}
Op::SetEmbeddedLens { id, lens } => {
@@ -914,6 +1099,10 @@ impl Catalog {
self.next_album = self.next_album.max(album);
self.next_stack = self.next_stack.max(stack);
}
+ if !changed.is_empty() {
+ // stacks and albums changed outside `apply`: the lookups are built again when asked
+ self.index = index::Indexes::default();
+ }
changed
}
@@ -963,6 +1152,8 @@ mod tests_folders;
#[cfg(test)]
mod tests_format_version;
#[cfg(test)]
+mod tests_index;
+#[cfg(test)]
mod tests_journal;
#[cfg(test)]
mod tests_local;
@@ -971,4 +1162,6 @@ mod tests_lock;
#[cfg(test)]
mod tests_repair;
#[cfg(test)]
+mod tests_scale;
+#[cfg(test)]
mod tests_torn_append;
diff --git a/crates/catalog/src/local.rs b/crates/catalog/src/local.rs
index 9cfc14f35..093c22427 100644
--- a/crates/catalog/src/local.rs
+++ b/crates/catalog/src/local.rs
@@ -183,15 +183,7 @@ impl Catalog {
let mut plan = ForgetPlan { now: now.to_string(), ..Default::default() };
let now_s = crate::stacks::iso_seconds(now);
let max_age = i64::from(days) * 86_400;
- let mut referenced: HashSet = HashSet::new();
- for a in self.albums.values() {
- referenced.extend(a.photos.iter().copied());
- referenced.extend(a.cover);
- }
- for s in self.stacks.values() {
- referenced.extend(s.photos.iter().copied());
- }
- referenced.extend(self.photos.values().filter_map(|p| p.copy_of));
+ let referenced = self.referenced();
// the fingerprints are the costly part: work them out for the candidates in parallel
let candidates: Vec<&Photo> = self
.photos
@@ -237,6 +229,30 @@ impl Catalog {
plan.unstamp = self.browsed.keys().filter(|f| !remaining.contains(*f)).cloned().collect();
plan
}
+
+ /// Of `ids`, the untouched Local records nothing refers to (no album, stack or virtual copy):
+ /// the ones [`Catalog::forget_local_plan`] could forget, whatever their folder's time.
+ pub fn forgettable_local(&self, ids: &[PhotoId]) -> Vec {
+ let referenced = self.referenced();
+ let candidates: Vec<&Photo> =
+ ids.iter().filter(|id| !referenced.contains(id)).filter_map(|id| self.photos.get(id)).filter(|p| p.local).map(|p| &**p).collect();
+ let untouched = untouched_of(&candidates);
+ ids.iter().copied().filter(|id| untouched.contains(id)).collect()
+ }
+
+ /// Photos an album (as a member or its cover), a stack or a virtual copy refers to.
+ fn referenced(&self) -> HashSet {
+ let mut referenced: HashSet = HashSet::new();
+ for a in self.albums.values() {
+ referenced.extend(a.photos.iter().copied());
+ referenced.extend(a.cover);
+ }
+ for s in self.stacks.values() {
+ referenced.extend(s.photos.iter().copied());
+ }
+ referenced.extend(self.photos.values().filter_map(|p| p.copy_of));
+ referenced
+ }
}
/// A folder browsed at `at` is past the period (never when `days` is 0 or a time is unreadable).
diff --git a/crates/catalog/src/model.rs b/crates/catalog/src/model.rs
index 434ea3a1e..6ac3544ee 100644
--- a/crates/catalog/src/model.rs
+++ b/crates/catalog/src/model.rs
@@ -199,6 +199,11 @@ pub struct Meta {
/// Left out of the catalog JSON when empty (most photos), so older catalogs read unchanged.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub regions: Vec,
+ /// Where the camera focused (read from the file's maker notes at import or Reload from Disk; Sony
+ /// bodies whose files have been checked), on the upright (EXIF-oriented) photo. Read-only: never
+ /// written to XMP. Left out of the catalog JSON when unknown, so older catalogs read unchanged.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub focus: Option,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -293,6 +298,12 @@ pub struct Photo {
/// picture style baked in — so it is treated as a rendered (non-raw) source.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preview_only: Option,
+ /// The app version (`CARGO_PKG_VERSION`) whose decoder set [`Photo::preview_only`]; empty when
+ /// it isn't set, and in catalogs from before format v9. A preview-only photo whose stamp
+ /// differs from the running version is rechecked when its library opens (issue #662): newer
+ /// builds may decode the file, older ones may decode less.
+ #[serde(default, skip_serializing_if = "String::is_empty")]
+ pub preview_checked: String,
/// A Local record: the fingerprint of its state when the browse catalogued it, to tell
/// whether the user changed anything since (see [`crate::local`]).
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -318,7 +329,7 @@ pub struct Analysis {
/// file's white balance is edited relative to its as-shot look or in Kelvin depends on the camera:
/// [`relative_wb_camera`].
pub fn relative_wb_format(format: &str) -> bool {
- ["ARW", "NEF", "NRW", "RW2", "RWL", "RAW", "RAF", "CR3", "CR2", "PEF", "SRW", "ORF"].iter().any(|f| format.eq_ignore_ascii_case(f))
+ ["ARW", "NEF", "NRW", "RW2", "RWL", "RAW", "RAF", "CR3", "CR2", "PEF", "SRW", "ORF", "ERF"].iter().any(|f| format.eq_ignore_ascii_case(f))
}
/// Whether a raw of `format` shot on `camera` (Exif make and model, as [`Meta::camera`] keeps them) is developed
@@ -332,6 +343,16 @@ pub fn relative_wb_camera(format: &str, camera: &str) -> bool {
relative_wb_format(format) && lightcraft_raw::spectral::find_camera(camera).is_none() && !lightcraft_raw::kelvin_presets::covers_camera(camera)
}
+/// This build's version: what a photo's [`Photo::preview_checked`] stamp is compared with. Every
+/// workspace crate shares one version, so it is the app's.
+pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+/// The [`Photo::preview_checked`] stamp for a `preview_only` decided by this build: its version
+/// when the photo is preview-only, empty otherwise.
+pub fn preview_stamp(preview_only: &Option) -> String {
+ if preview_only.is_some() { APP_VERSION.to_string() } else { String::new() }
+}
+
impl Photo {
/// The History step the settings are at (the cursor, clamped to the list; `None` = no history).
pub fn history_index(&self) -> Option {
@@ -371,9 +392,20 @@ impl Photo {
import_look: None,
analysis: None,
preview_only: None,
+ preview_checked: String::new(),
local_baseline: None,
}
}
+ /// Sets [`Photo::preview_only`] as this build decided it, with its stamp.
+ pub fn set_preview_only(&mut self, preview_only: Option) {
+ self.preview_checked = preview_stamp(&preview_only);
+ self.preview_only = preview_only;
+ }
+ /// A preview-only photo whose flag was set by another app version (or before stamps existed),
+ /// so this build should probe its file again (issue #662).
+ pub fn preview_only_stale(&self) -> bool {
+ self.preview_only.is_some() && self.preview_checked != APP_VERSION
+ }
/// A raw file developed from its sensor data: not a rendered image, and not a raw shown from
/// its embedded preview ([`Photo::preview_only`]).
pub fn develops_raw(&self) -> bool {
@@ -577,6 +609,7 @@ mod edited_tests {
("a.cr2", "CR2", true),
("a.pef", "pef", true),
("a.orf", "ORF", true),
+ ("a.erf", "ERF", true),
("a.dng", "DNG", false),
] {
let mut p = Photo::new(PhotoId(1), Source::Demo { scene: 0 }, name, format, 10, 10, "2026-10-01T00:00:00");
@@ -609,6 +642,9 @@ mod edited_tests {
("SONY ILCE-7RM4A", "ARW", false),
("NIKON CORPORATION NIKON D850", "NEF", false),
("Canon Canon EOS 5D Mark III", "CR2", false),
+ ("Canon Canon EOS R6m2", "CR3", false),
+ ("Canon Canon EOS R6 Mark II", "CR3", false),
+ ("Canon Canon EOS R7", "CR3", true),
("FUJIFILM X-T4", "RAF", false),
("ILCE-7M3", "ARW", false),
("SONY ILCE-7M2", "ARW", true),
@@ -624,7 +660,11 @@ mod edited_tests {
p.as_shot_wb = Some((4986.0, -2.0));
assert_eq!(p.relative_wb(), relative, "{camera} {format}");
assert_eq!(relative_wb_camera(format, camera), relative, "{camera} {format}");
- assert_eq!(p.fitted_look(), format == "ARW" || format == "NEF" || format == "CR2" || format == "RAF", "{camera} {format}");
+ assert_eq!(
+ p.fitted_look(),
+ format == "ARW" || format == "NEF" || format == "CR2" || format == "CR3" || format == "RAF",
+ "{camera} {format}"
+ );
let wb = p.camera_defaults().wb;
assert_eq!((wb.temp, wb.tint), if relative { (6500.0, 0.0) } else { (4986.0, -2.0) }, "{camera} {format}");
assert_eq!(wb.scale, if relative { WbScale::Legacy } else { WbScale::Kelvin }, "{camera} {format}");
diff --git a/crates/catalog/src/query.rs b/crates/catalog/src/query.rs
index a9956703d..7625c9ab6 100644
--- a/crates/catalog/src/query.rs
+++ b/crates/catalog/src/query.rs
@@ -94,6 +94,27 @@ pub struct Sort {
pub seed: u64,
}
+impl Sort {
+ /// Which of two photos comes first in this order (never `Equal` for two photos: the id decides last).
+ pub fn order(&self, a: &Photo, b: &Photo) -> std::cmp::Ordering {
+ let o = match self.key {
+ SortKey::CaptureDate => a.captured.cmp(&b.captured).then_with(|| a.imported.cmp(&b.imported)),
+ SortKey::ImportDate => a.imported.cmp(&b.imported),
+ SortKey::EditDate => a.edited.cmp(&b.edited),
+ SortKey::FileName => cmp_name(&a.file_name, &b.file_name),
+ SortKey::Rating => a.rating.cmp(&b.rating),
+ SortKey::FileSize => a.file_size.cmp(&b.file_size),
+ SortKey::Random => shuffle_rank(self.seed, a.id).cmp(&shuffle_rank(self.seed, b.id)),
+ }
+ // Ties (one import batch, a burst within a second, equal ratings) sort by file name, the
+ // order cameras number frames, so the order doesn't depend on how ids were allocated;
+ // the id only keeps it stable.
+ .then_with(|| if self.key == SortKey::FileName { Equal } else { cmp_name(&a.file_name, &b.file_name) })
+ .then(a.id.cmp(&b.id));
+ if self.ascending { o } else { o.reverse() }
+ }
+}
+
impl Default for Sort {
fn default() -> Self {
Sort { key: SortKey::CaptureDate, ascending: false, group: crate::GroupBy::Auto, seed: 0 }
@@ -114,6 +135,12 @@ fn shuffle_rank(seed: u64, id: PhotoId) -> u64 {
mix64(mix64(seed) ^ id.0)
}
+/// What [`Filter::prepare`] works out once for a pass over many photos.
+pub(crate) struct Prepared {
+ root: Option,
+ only: Option>,
+}
+
fn token_matches(p: &Photo, tok: &str) -> bool {
let t = tok.to_lowercase();
if let Some((field, val)) = t.split_once(':') {
@@ -130,13 +157,20 @@ fn token_matches(p: &Photo, tok: &str) -> bool {
Some((_, v)) => (x - v).abs() < 1e-9,
None => false,
};
+ // Fields stored as `f32` compare at f32 precision: 2.8f32 widened to f64 is not 2.8.
+ let cmp32 = |x: f32, s: &str| match num(s) {
+ Some(('>', v)) => x > v as f32,
+ Some(('<', v)) => x < v as f32,
+ Some((_, v)) => x == v as f32,
+ None => false,
+ };
return match field {
"rating" | "stars" => cmp(p.rating as f64, val),
"flag" => Flag::parse(val) == Some(p.flag),
"label" | "color" => p.label.is_some_and(|l| format!("{l:?}").eq_ignore_ascii_case(val)),
"iso" => p.meta.iso.is_some_and(|i| cmp(i as f64, val)),
- "f" | "aperture" => p.meta.aperture.is_some_and(|a| cmp(a as f64, val)),
- "focal" => p.meta.focal_mm.is_some_and(|a| cmp(a as f64, val)),
+ "f" | "aperture" => p.meta.aperture.is_some_and(|a| cmp32(a, val)),
+ "focal" => p.meta.focal_mm.is_some_and(|a| cmp32(a, val)),
"camera" => p.meta.camera.to_lowercase().contains(val),
"lens" => p.meta.lens.to_lowercase().contains(val),
"keyword" | "kw" => p.meta.keywords.iter().any(|k| crate::keywords::is_under(k, val)),
@@ -146,11 +180,18 @@ fn token_matches(p: &Photo, tok: &str) -> bool {
"date" => p.captured.as_deref().is_some_and(|c| c.starts_with(val)),
"copy" | "virtual" => (val == "true" || val == "yes") == p.copy_of.is_some(),
"name" | "file" => p.file_name.to_lowercase().contains(val),
- _ => false,
+ // Not a known field: the colon is part of a plain word ("10:30", "Re:").
+ _ => text_matches(p, &t),
};
}
+ text_matches(p, &t)
+}
+
+/// Whether the lower-cased word `t` is in the file name, title, caption, camera, lens, location,
+/// format or a keyword of `p`.
+fn text_matches(p: &Photo, t: &str) -> bool {
let hay = [&p.file_name, &p.meta.title, &p.meta.caption, &p.meta.camera, &p.meta.lens, &p.meta.location, &p.format];
- hay.iter().any(|h| h.to_lowercase().contains(&t)) || p.meta.keywords.iter().any(|k| k.to_lowercase().contains(&t))
+ hay.iter().any(|h| h.to_lowercase().contains(t)) || p.meta.keywords.iter().any(|k| k.to_lowercase().contains(t))
}
/// Whether `p` has a named face region called `name` (case-insensitive).
@@ -236,7 +277,18 @@ impl Filter {
}
pub fn matches(&self, p: &Photo, cat: &Catalog) -> bool {
- self.matches_in(p, cat, self.library_root().as_deref())
+ self.matches_in(p, cat, &self.prepare_one())
+ }
+
+ /// [`Filter::prepare`] for testing one photo (the `only` ids are looked through, not hashed).
+ pub(crate) fn prepare_one(&self) -> Prepared {
+ Prepared { root: self.library_root(), only: None }
+ }
+
+ /// What [`Filter::matches_in`] needs worked out once for a pass over many photos rather than
+ /// once per photo: the library folder's identity and the `only` ids as a set.
+ pub(crate) fn prepare(&self) -> Prepared {
+ Prepared { root: self.library_root(), only: (!self.only.is_empty()).then(|| self.only.iter().copied().collect()) }
}
/// [`Filter::library_folder`] as a folder identity ([`folder_key`]); `None` without a choice.
@@ -246,9 +298,9 @@ impl Filter {
self.library_folder.as_deref().filter(|d| !d.trim().is_empty()).map(folder_key)
}
- /// [`Filter::matches`] with the library folder already turned into its identity, so a query
- /// over a big library does that once, not once per photo.
- fn matches_in(&self, p: &Photo, cat: &Catalog, root: Option<&str>) -> bool {
+ /// [`Filter::matches`] with what [`Filter::prepare`] works out done already, so a query over a
+ /// big library does that once, not once per photo.
+ pub(crate) fn matches_in(&self, p: &Photo, cat: &Catalog, prepared: &Prepared) -> bool {
if p.deleted != self.deleted {
return false;
}
@@ -287,7 +339,7 @@ impl Filter {
if !self.labels.is_empty() && !p.label.is_some_and(|l| self.labels.contains(&l)) {
return false;
}
- if !self.only.is_empty() && !self.only.contains(&p.id) {
+ if !self.only.is_empty() && !prepared.only.as_ref().map_or_else(|| self.only.contains(&p.id), |only| only.contains(&p.id)) {
return false;
}
if let Some(want) = &self.merged {
@@ -327,7 +379,7 @@ impl Filter {
{
return false;
}
- if let Some(root) = root
+ if let Some(root) = prepared.root.as_deref()
&& !photo_in_root(p, root)
{
return false;
@@ -369,38 +421,66 @@ pub fn cmp_name(a: &str, b: &str) -> std::cmp::Ordering {
impl Catalog {
/// Photos matching `filter`, in `sort` order (ties broken by file name, then id).
pub fn query(&self, filter: &Filter, sort: &Sort) -> Vec {
- let root = filter.library_root();
- let mut v: Vec<&Photo> = self.photos().map(|p| p.as_ref()).filter(|p| filter.matches_in(p, self, root.as_deref())).collect();
- v.sort_by(|a, b| {
- let o = match sort.key {
- SortKey::CaptureDate => a.captured.cmp(&b.captured).then_with(|| a.imported.cmp(&b.imported)),
- SortKey::ImportDate => a.imported.cmp(&b.imported),
- SortKey::EditDate => a.edited.cmp(&b.edited),
- SortKey::FileName => cmp_name(&a.file_name, &b.file_name),
- SortKey::Rating => a.rating.cmp(&b.rating),
- SortKey::FileSize => a.file_size.cmp(&b.file_size),
- SortKey::Random => shuffle_rank(sort.seed, a.id).cmp(&shuffle_rank(sort.seed, b.id)),
- }
- // Ties (one import batch, a burst within a second, equal ratings) sort by file name, the
- // order cameras number frames, so the order doesn't depend on how ids were allocated;
- // the id only keeps it stable.
- .then_with(|| if sort.key == SortKey::FileName { Equal } else { cmp_name(&a.file_name, &b.file_name) })
- .then(a.id.cmp(&b.id));
- if sort.ascending { o } else { o.reverse() }
- });
+ self.query_keeping(filter, sort, &std::collections::HashSet::new(), filter)
+ }
+
+ /// [`Catalog::query`], and with them the photos of `keep` that still match `within`, each at its place in `sort`
+ /// order: a view that holds the photos it showed when an edit makes them stop matching `filter` (a smart
+ /// album's rules, the filter bar), as long as they are still where the view looks (`within`: in the library,
+ /// the album, the folder; not deleted).
+ pub fn query_keeping(&self, filter: &Filter, sort: &Sort, keep: &std::collections::HashSet, within: &Filter) -> Vec {
+ let prepared = filter.prepare();
+ // `within` only decides for the few kept photos: worked out per photo, not hashed
+ let within_prepared = within.prepare_one();
+ let mut v: Vec<&Photo> = self
+ .photos()
+ .map(|p| p.as_ref())
+ .filter(|p| filter.matches_in(p, self, &prepared) || (keep.contains(&p.id) && within.matches_in(p, self, &within_prepared)))
+ .collect();
+ v.sort_by(|a, b| sort.order(a, b));
v.into_iter().map(|p| p.id).collect()
}
+ /// The list `was` (a [`Catalog::query`] of `filter` in `sort` order, or a [`Catalog::query_keeping`]), after ops
+ /// that touched only the photos `touched` (sorted by id, each once: [`Catalog::touched_since`]): those photos
+ /// taken out, and the ones that match `filter` now put back at their place in the order. With `within`, a
+ /// touched photo that was in the list and still matches `within` stays, as `query_keeping` would keep it. What
+ /// the query would give, without asking every photo again.
+ pub fn follow(&self, was: &[PhotoId], touched: &[PhotoId], filter: &Filter, sort: &Sort, within: Option<&Filter>) -> Vec {
+ let listed: std::collections::HashSet = was.iter().copied().filter(|id| touched.binary_search(id).is_ok()).collect();
+ let mut ids: Vec = was.iter().copied().filter(|id| !listed.contains(id)).collect();
+ let (prepared, within_prepared) = (filter.prepare_one(), within.map(Filter::prepare_one));
+ for id in touched {
+ let Some(p) = self.photo(*id) else { continue };
+ let kept = within.zip(within_prepared.as_ref()).is_some_and(|(w, wp)| listed.contains(id) && w.matches_in(p, self, wp));
+ if filter.matches_in(p, self, &prepared) || kept {
+ // (the list is in order without the touched photos, and stays so as each is put back)
+ let at = ids.partition_point(|other| self.photo(*other).is_some_and(|o| sort.order(o, p).is_lt()));
+ ids.insert(at, *id);
+ }
+ }
+ ids
+ }
+
+ /// How many photos match `filter`: [`Catalog::query`]'s length without its sorting.
+ pub fn count(&self, filter: &Filter) -> usize {
+ let prepared = filter.prepare();
+ self.photos().filter(|p| filter.matches_in(p, self, &prepared)).count()
+ }
+
/// Year → month → day counts for the "By Date" section (newest first).
pub fn date_groups(&self) -> Vec {
use std::collections::BTreeMap;
let mut years: BTreeMap<&str, (BTreeMap<&str, usize>, BTreeMap<&str, usize>)> = BTreeMap::new();
for p in self.photos().filter(|p| p.in_library()) {
let Some(d) = p.captured.as_deref() else { continue };
- if d.len() >= 10 {
- let (months, days) = years.entry(&d[..4]).or_default();
- *months.entry(&d[..7]).or_default() += 1;
- *days.entry(&d[..10]).or_default() += 1;
+ // `get`, not `[..]`: a corrupt or foreign `captured` (a Lightroom catalog's
+ // captureTime is copied verbatim) may not be ASCII, and slicing it on a byte index
+ // would panic — the same rule as `dates::group_label`.
+ if let (Some(year), Some(month), Some(day)) = (d.get(..4), d.get(..7), d.get(..10)) {
+ let (months, days) = years.entry(year).or_default();
+ *months.entry(month).or_default() += 1;
+ *days.entry(day).or_default() += 1;
}
}
let newest_first = |m: BTreeMap<&str, usize>| m.into_iter().rev().map(|(k, n)| (k.to_string(), n)).collect::>();
@@ -441,8 +521,8 @@ impl Catalog {
pub fn people_in(&self, filter: &Filter) -> Vec {
let filter = Filter { person: None, ..filter.clone() };
let mut m: std::collections::HashMap = Default::default();
- let root = filter.library_root();
- for p in self.photos().filter(|p| filter.matches_in(p, self, root.as_deref())) {
+ let prepared = filter.prepare();
+ for p in self.photos().filter(|p| filter.matches_in(p, self, &prepared)) {
let mut seen: Vec = Vec::new();
for r in p.meta.regions.iter().filter(|r| r.kind == lightcraft_meta::RegionKind::Face) {
let Some(name) = r.name.as_deref().map(str::trim).filter(|n| !n.is_empty()) else { continue };
diff --git a/crates/catalog/src/rules.rs b/crates/catalog/src/rules.rs
index f17f12e01..a87c9effc 100644
--- a/crates/catalog/src/rules.rs
+++ b/crates/catalog/src/rules.rs
@@ -598,6 +598,35 @@ pub fn now() -> String {
}
}
+/// A flag as rules name it (its variant, lower case), without building a string per photo.
+fn flag_name(f: crate::Flag) -> &'static str {
+ match f {
+ crate::Flag::None => "none",
+ crate::Flag::Pick => "pick",
+ crate::Flag::Reject => "reject",
+ }
+}
+
+/// A colour label's colour as rules name it (lower case).
+fn label_colour(l: crate::ColorLabel) -> &'static str {
+ match l {
+ crate::ColorLabel::Red => "red",
+ crate::ColorLabel::Yellow => "yellow",
+ crate::ColorLabel::Green => "green",
+ crate::ColorLabel::Blue => "blue",
+ crate::ColorLabel::Purple => "purple",
+ }
+}
+
+/// A media kind as rules name it (lower case).
+fn kind_name(k: crate::MediaKind) -> &'static str {
+ match k {
+ crate::MediaKind::Image => "image",
+ crate::MediaKind::Raw => "raw",
+ crate::MediaKind::Video => "video",
+ }
+}
+
fn lower(v: &Value) -> String {
match v {
Value::String(s) => s.trim().to_lowercase(),
@@ -778,16 +807,16 @@ impl Rule {
match field {
"rating" => num_op(op, Some(p.rating as f64), value),
"flag" => {
- let f = format!("{:?}", p.flag).to_lowercase();
+ let f = flag_name(p.flag);
(f == want || (want == "picked" && f == "pick") || (want == "rejected" && f == "reject")) == (op == "is")
}
"label" => {
- let l = p.label.map(|l| format!("{l:?}").to_lowercase()).unwrap_or_else(|| "none".into());
+ let l = p.label.map_or("none", label_colour);
// custom label names count too
- let named = p.label.is_some_and(|l| cat.label_name(l).to_lowercase() == want);
+ let named = p.label.is_some_and(|l| cat.custom_label_name(l).map_or(label_colour(l) == want, |n| n.to_lowercase() == want));
(l == want || named) == (op == "is")
}
- "kind" => (format!("{:?}", p.kind).to_lowercase() == want) == (op == "is"),
+ "kind" => (kind_name(p.kind) == want) == (op == "is"),
"edited" => yes(p.is_edited()),
"hasGps" => yes(m.gps.is_some()),
"virtualCopy" => yes(p.copy_of.is_some()),
diff --git a/crates/catalog/src/stacks.rs b/crates/catalog/src/stacks.rs
index 6ffb11f52..eb7e89ef4 100644
--- a/crates/catalog/src/stacks.rs
+++ b/crates/catalog/src/stacks.rs
@@ -21,7 +21,7 @@ impl Catalog {
}
/// The stack a photo belongs to.
pub fn stack_of(&self, photo: PhotoId) -> Option<&Stack> {
- self.stacks.values().find(|s| s.photos.contains(&photo))
+ self.stack_id_of(photo).and_then(|id| self.stacks.get(&id))
}
/// Photo → (stack, position in the stack), for every stacked photo.
pub fn stack_index(&self) -> HashMap {
@@ -41,7 +41,7 @@ impl Catalog {
return Err(CatalogError::NoPhoto(*p));
}
}
- if self.stacks.values().any(|s| s.id != id && s.photos.iter().any(|p| seen.contains(p))) {
+ if photos.iter().any(|p| self.stack_id_of(*p).is_some_and(|s| s != id)) {
return Err(CatalogError::Invalid("a photo can be in only one stack".into()));
}
Ok(())
@@ -231,6 +231,60 @@ impl Catalog {
}
}
+impl Catalog {
+ /// The bracket sets among `ids`, an HDR merge each: every stack one of them is in (its photos, less earlier merge
+ /// results), then the unstacked ones in capture order, a set running while each starts within about a second of the
+ /// previous one's end (its shutter time; capture times are whole seconds). A run is a set when its exposures span at
+ /// least ½ EV: a burst at one exposure is no bracket. Photos without a capture time or shutter speed are left out.
+ pub fn bracket_sets(&self, ids: &[PhotoId]) -> Vec> {
+ let merged = |p: &crate::Photo| crate::query::merged_kind(&p.file_name).is_some();
+ let mut sets: Vec> = Vec::new();
+ let mut stacks = HashSet::new();
+ let mut loose = Vec::new();
+ for id in ids {
+ match self.stack_of(*id) {
+ Some(s) if stacks.insert(s.id) => {
+ let set: Vec = s.photos.iter().copied().filter(|p| self.photos.get(p).is_some_and(|p| !merged(p))).collect();
+ if set.len() >= 2 {
+ sets.push(set);
+ }
+ }
+ Some(_) => {}
+ None => loose.push(*id),
+ }
+ }
+ // (capture second, file name, id, shutter seconds, EV)
+ let mut timed: Vec<(i64, &str, PhotoId, f64, f64)> = loose
+ .iter()
+ .filter_map(|id| self.photos.get(id))
+ .filter(|p| !p.deleted && !merged(p))
+ .filter_map(|p| {
+ let t = iso_seconds(p.captured.as_deref()?)?;
+ let s = crate::model::parse_shutter_seconds(&p.meta.shutter)?;
+ let n = p.meta.aperture.filter(|n| n.is_finite() && *n > 0.0).unwrap_or(1.0) as f64;
+ let iso = p.meta.iso.filter(|i| *i > 0).unwrap_or(100) as f64;
+ Some((t, p.file_name.as_str(), p.id, s, (s * iso / (n * n)).log2()))
+ })
+ .collect();
+ timed.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| cmp_name(a.1, b.1)).then(a.2.cmp(&b.2)));
+ timed.dedup_by_key(|x| x.2);
+ let mut runs: Vec> = Vec::new();
+ for x in timed {
+ match runs.last_mut() {
+ Some(r) if r.last().is_some_and(|l| (x.0 - l.0) as f64 <= l.3.ceil() + 1.0) => r.push(x),
+ _ => runs.push(vec![x]),
+ }
+ }
+ for r in runs {
+ let (lo, hi) = r.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), x| (lo.min(x.4), hi.max(x.4)));
+ if r.len() >= 2 && hi - lo >= 0.5 {
+ sets.push(r.iter().map(|x| x.2).collect());
+ }
+ }
+ sets
+ }
+}
+
/// Seconds since 1970-01-01 for an ISO 8601 local time (`2026-04-01T10:00:00`, `2026-04-01 10:00`,
/// `2026-04-01`; fractions and zone suffixes are ignored). `None` if it doesn't parse.
pub fn iso_seconds(s: &str) -> Option {
@@ -240,7 +294,8 @@ pub fn iso_seconds(s: &str) -> Option {
if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
return None;
}
- let (hh, mm, ss) = if s.len() >= 16 { (num(11, 13)?, num(14, 16)?, num(17, 19).unwrap_or(0)) } else { (0, 0, 0) };
+ let (hh, mm, ss) =
+ if s.len() >= 16 { (num(11, 13)?, num(14, 16)?, if s.get(16..17) == Some(":") { num(17, 19).unwrap_or(0) } else { 0 }) } else { (0, 0, 0) };
// days from civil (proleptic Gregorian)
let y2 = if m <= 2 { y - 1 } else { y };
let era = y2.div_euclid(400);
@@ -358,6 +413,46 @@ mod tests {
assert_eq!(back.clone().alloc_stack_id(), c.clone().alloc_stack_id());
}
+ #[test]
+ fn bracket_sets_by_time_and_exposure() {
+ // (time, shutter, ISO, file name)
+ let shots = [
+ ("2026-08-01T09:20:36", "1/125", 640, "A1.ARW"), // a handheld bracket
+ ("2026-08-01T09:20:36", "1/125", 200, "A2.ARW"),
+ ("2026-08-01T09:20:37", "1/125", 2000, "A3.ARW"),
+ ("2026-08-01T09:20:37", "", 0, "A3-HDR.dng"), // an earlier merge of it
+ ("2026-08-01T09:20:44", "1/125", 800, "B1.ARW"), // 7 s later: the next bracket
+ ("2026-08-01T09:20:44", "1/125", 250, "B2.ARW"),
+ ("2026-08-01T09:20:50", "1/1000", 160, "C1.ARW"), // a burst at one exposure
+ ("2026-08-01T09:20:50", "1/1000", 160, "C2.ARW"),
+ ("2026-06-27T23:34:59", "2", 320, "N1.ARW"), // 2 s night frames, 2.1 s apart
+ ("2026-06-27T23:35:01", "2", 100, "N2.ARW"),
+ ("2026-06-27T23:35:04", "2", 800, "N3.ARW"),
+ ("2026-06-27T23:36:00", "1/40", 250, "S1.ARW"), // stacked by hand: merged whatever its times
+ ("2026-06-27T23:39:00", "1/40", 250, "S2.ARW"),
+ ];
+ let mut c = Catalog::new();
+ let mut ids = Vec::new();
+ for (t, sh, iso, name) in shots {
+ let id = c.alloc_photo_id();
+ let mut p = Photo::new(id, Source::Demo { scene: 1 }, name, "ARW", 60, 40, "2026-01-01T00:00:00");
+ p.captured = Some(t.into());
+ p.meta.shutter = sh.into();
+ p.meta.iso = Some(iso);
+ p.meta.aperture = Some(5.6);
+ c.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap();
+ ids.push(id);
+ }
+ let op = c.group_ops(ids[11], &[ids[12]], true).unwrap();
+ c.apply(op).unwrap();
+ let sets = c.bracket_sets(&ids);
+ assert_eq!(sets, vec![vec![ids[11], ids[12]], vec![ids[8], ids[9], ids[10]], vec![ids[0], ids[1], ids[2]], vec![ids[4], ids[5]]]);
+ // a stack's merge result on top is left out; a stack selected twice is merged once
+ let op = c.stack_with_ops(ids[3], &[ids[0], ids[1], ids[2]]).unwrap();
+ c.apply(op).unwrap();
+ assert_eq!(c.bracket_sets(&[ids[3], ids[0]]), vec![vec![ids[0], ids[1], ids[2]]]);
+ }
+
#[test]
fn auto_stack_by_capture_time() {
let times = ["2026-04-01T10:00:00", "2026-04-01T10:00:02", "2026-04-01T10:00:09", "2026-04-01T11:00:00", "2026-04-01T11:00:01"];
diff --git a/crates/catalog/src/store.rs b/crates/catalog/src/store.rs
index a672d00e7..2f3dc4912 100644
--- a/crates/catalog/src/store.rs
+++ b/crates/catalog/src/store.rs
@@ -73,6 +73,16 @@ impl FsStore {
}
}
+/// A new staging file at `tmp`: whatever is already there (a leftover from a crashed save, or a link) is removed first
+/// (a link itself, never what it points to), then the file is created exclusively, so a write never follows a link
+/// planted at that name into another file (issue #1502).
+fn create_staging(tmp: &std::path::Path) -> io::Result {
+ if std::fs::symlink_metadata(tmp).is_ok() {
+ std::fs::remove_file(tmp)?;
+ }
+ std::fs::OpenOptions::new().write(true).create_new(true).open(tmp)
+}
+
impl Store for FsStore {
fn read(&mut self, name: &str) -> io::Result>> {
match std::fs::read(self.path(name)) {
@@ -89,7 +99,7 @@ impl Store for FsStore {
}
let tmp = self.path(&format!("{name}.tmp"));
{
- let mut f = std::fs::File::create(&tmp)?;
+ let mut f = create_staging(&tmp)?;
f.write_all(data)?;
f.sync_all()?;
}
@@ -105,7 +115,7 @@ impl Store for FsStore {
}
let tmp = self.path(&format!("{name}.tmp"));
let written = (|| {
- let mut w = Counter { inner: io::BufWriter::with_capacity(1 << 20, std::fs::File::create(&tmp)?), n: 0 };
+ let mut w = Counter { inner: io::BufWriter::with_capacity(1 << 20, create_staging(&tmp)?), n: 0 };
fill(&mut w)?;
w.flush()?;
let f = w.inner.into_inner().map_err(|e| e.into_error())?;
@@ -231,3 +241,29 @@ impl Store for MemStore {
self.background.then(|| Box::new(self.clone()) as Box)
}
}
+
+#[cfg(test)]
+mod tests_staging {
+ use super::*;
+
+ /// A link planted at a staging name (`.tmp`) is removed, never followed: the file it points to keeps its
+ /// bytes and the save still lands (issue #1502).
+ #[cfg(unix)]
+ #[test]
+ fn a_link_at_the_staging_name_is_never_written_through() {
+ let dir = std::env::temp_dir().join(format!("lc-staging-link-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&dir);
+ let mut store = FsStore::open(&dir).unwrap();
+ let victim = dir.join("victim.txt");
+ std::fs::write(&victim, b"keep").unwrap();
+ std::os::unix::fs::symlink(&victim, dir.join("catalog.snap.tmp")).unwrap();
+ store.write_atomic("catalog.snap", b"{}").unwrap();
+ assert_eq!(std::fs::read(&victim).unwrap(), b"keep");
+ assert_eq!(std::fs::read(dir.join("catalog.snap")).unwrap(), b"{}");
+ std::os::unix::fs::symlink(&victim, dir.join("catalog.log.tmp")).unwrap();
+ store.write_atomic_with("catalog.log", &mut |w| w.write_all(b"[1]")).unwrap();
+ assert_eq!(std::fs::read(&victim).unwrap(), b"keep");
+ assert_eq!(std::fs::read(dir.join("catalog.log")).unwrap(), b"[1]");
+ let _ = std::fs::remove_dir_all(&dir);
+ }
+}
diff --git a/crates/catalog/src/tally.rs b/crates/catalog/src/tally.rs
new file mode 100644
index 000000000..8f15e5ec4
--- /dev/null
+++ b/crates/catalog/src/tally.rs
@@ -0,0 +1,234 @@
+//! How many photos each album shows, kept up to date from what the latest ops touched.
+//!
+//! A smart album's count asks every photo whether its rules hold. Asked again after every edit, that is a pass over
+//! the library for each smart album each time a keyword is given. [`AlbumTally`] remembers which photos each smart
+//! album holds, and after a small edit asks only the photos the edit touched ([`Catalog::touched_since`]). An op
+//! that changes more than photos of its own (an album's rules or photos, a stack, the keyword list) has it counted
+//! whole again, as before.
+
+use std::collections::{HashMap, HashSet};
+
+use crate::{AlbumId, Aspects, Catalog, PhotoId};
+
+/// The most photos an edit may touch for the counts to follow it photo by photo.
+const MAX_FOLLOWED: usize = 256;
+/// Bits are kept while they cost no more than this many words a photo held, and these to start with. Past that
+/// (photo ids far apart: a library whose ids don't count up from 1) a set is kept instead, so what an album
+/// remembers is always in proportion to the photos it holds, never to the largest id in the library.
+const WORDS_A_MEMBER: usize = 4;
+const WORDS_TO_START: usize = 1024;
+
+/// The photos a smart album holds.
+#[derive(Clone, Debug)]
+enum Members {
+ /// One bit a photo id, and how many are set.
+ Bits {
+ words: Vec,
+ len: usize,
+ },
+ Set(HashSet),
+}
+
+impl Members {
+ fn none() -> Members {
+ Members::Bits { words: Vec::new(), len: 0 }
+ }
+
+ fn len(&self) -> usize {
+ match self {
+ Members::Bits { len, .. } => *len,
+ Members::Set(set) => set.len(),
+ }
+ }
+
+ #[cfg(test)]
+ fn holds(&self, id: PhotoId) -> bool {
+ match self {
+ Members::Bits { words, .. } => {
+ usize::try_from(id.0 / 64).ok().and_then(|at| words.get(at)).is_some_and(|word| word & (1u64 << (id.0 % 64)) != 0)
+ }
+ Members::Set(set) => set.contains(&id),
+ }
+ }
+
+ /// Put `id` in or take it out.
+ fn set(&mut self, id: PhotoId, held: bool) {
+ if let Members::Bits { words, len } = self {
+ let bit = 1u64 << (id.0 % 64);
+ match usize::try_from(id.0 / 64) {
+ Ok(at) if at < words.len() => {
+ if let Some(word) = words.get_mut(at) {
+ match (*word & bit != 0, held) {
+ (false, true) => {
+ *word |= bit;
+ *len = len.saturating_add(1);
+ }
+ (true, false) => {
+ *word &= !bit;
+ *len = len.saturating_sub(1);
+ }
+ _ => {}
+ }
+ }
+ return;
+ }
+ // (past the end: not held, so there is nothing to take out)
+ _ if !held => return,
+ // room for it, as long as the bits stay in proportion to the photos held
+ Ok(at) if at < len.saturating_mul(WORDS_A_MEMBER).saturating_add(WORDS_TO_START) => {
+ words.resize(at + 1, 0);
+ if let Some(word) = words.get_mut(at) {
+ *word |= bit;
+ *len = len.saturating_add(1);
+ }
+ return;
+ }
+ // ids too far apart for bits: a set from here on
+ _ => {
+ let ids = words
+ .iter()
+ .enumerate()
+ .flat_map(|(at, word)| (0..64u64).filter(move |b| word & (1u64 << b) != 0).map(move |b| PhotoId(at as u64 * 64 + b)));
+ *self = Members::Set(ids.collect());
+ }
+ }
+ }
+ if let Members::Set(set) = self {
+ if held {
+ set.insert(id);
+ } else {
+ set.remove(&id);
+ }
+ }
+ }
+}
+
+/// [`Catalog::album_count`] of every album, as of one revision of the catalog.
+#[derive(Clone, Debug)]
+pub struct AlbumTally {
+ revision: u64,
+ counts: HashMap,
+ /// The smart albums' photos.
+ members: HashMap,
+}
+
+impl AlbumTally {
+ /// Counted from every photo.
+ pub fn of(cat: &Catalog) -> AlbumTally {
+ let mut counts = HashMap::new();
+ let mut members = HashMap::new();
+ for album in cat.albums() {
+ if album.is_smart() {
+ let mut held = Members::none();
+ for p in cat.photos().filter(|p| cat.album_contains(album.id, p)) {
+ held.set(p.id, true);
+ }
+ counts.insert(album.id, held.len());
+ members.insert(album.id, held);
+ } else {
+ counts.insert(album.id, cat.album_count(album.id));
+ }
+ }
+ AlbumTally { revision: cat.revision, counts, members }
+ }
+
+ /// The count of each album.
+ pub fn counts(&self) -> &HashMap {
+ &self.counts
+ }
+
+ /// The catalog's revision the counts are of.
+ pub fn revision(&self) -> u64 {
+ self.revision
+ }
+
+ /// Bring the counts up to `cat` as it is now. `true` when that was done from the photos the ops since touched
+ /// (or nothing had changed), `false` when every photo was asked again.
+ ///
+ /// `cat` has to be the catalog the counts were made of, further along. Another catalog is told apart only by
+ /// its revisions: a library that is loaded takes a range of its own (and then everything is counted again), but
+ /// two catalogs both counting from 0 look like one.
+ pub fn update(&mut self, cat: &Catalog) -> bool {
+ if self.revision == cat.revision {
+ return true;
+ }
+ let followed = match (cat.touched_since(self.revision), cat.aspects_since(self.revision)) {
+ (Some(touched), Some(changed)) if touched.len() <= MAX_FOLLOWED => {
+ self.follow(cat, &touched, changed);
+ true
+ }
+ _ => false,
+ };
+ if !followed {
+ *self = AlbumTally::of(cat);
+ }
+ followed
+ }
+
+ /// The counts after ops that touched only `touched`: each smart album asks those photos again; an album of
+ /// chosen photos counts again only when a photo came, went or was deleted (who is in it did not change: that
+ /// is an op on the album, and not followed).
+ fn follow(&mut self, cat: &Catalog, touched: &[PhotoId], changed: Aspects) {
+ for (album, held) in &mut self.members {
+ for id in touched {
+ held.set(*id, cat.photo(*id).is_some_and(|p| cat.album_contains(*album, p)));
+ }
+ self.counts.insert(*album, held.len());
+ }
+ if changed.any_of(Aspects::PRESENCE) {
+ for album in cat.albums().filter(|a| !a.is_smart()) {
+ self.counts.insert(album.id, cat.album_count(album.id));
+ }
+ }
+ self.revision = cat.revision;
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn ids(m: &Members, of: &[u64]) -> Vec {
+ of.iter().copied().filter(|id| m.holds(PhotoId(*id))).collect()
+ }
+
+ #[test]
+ fn members_are_kept_as_bits_at_the_edges_of_a_word_and_past_the_end() {
+ let mut m = Members::none();
+ let probe = [0, 1, 63, 64, 65, 127, 128, 5000];
+ assert!(ids(&m, &probe).is_empty() && m.len() == 0);
+ for id in [0, 63, 64, 5000] {
+ m.set(PhotoId(id), true);
+ }
+ assert_eq!((ids(&m, &probe), m.len()), (vec![0, 63, 64, 5000], 4));
+ // put in twice, taken out twice, taken out where nothing is, far past the end: counted once each way
+ m.set(PhotoId(63), true);
+ m.set(PhotoId(64), false);
+ m.set(PhotoId(64), false);
+ m.set(PhotoId(1), false);
+ m.set(PhotoId(900_000), false);
+ assert_eq!((ids(&m, &probe), m.len()), (vec![0, 63, 5000], 3));
+ assert!(matches!(m, Members::Bits { .. }));
+ }
+
+ #[test]
+ fn photo_ids_far_apart_are_kept_as_a_set_in_proportion_to_the_photos_held() {
+ // one photo with an id in the thousands of millions: no bit for every id up to it
+ let mut m = Members::none();
+ for id in [3, 70, 9_000_000_000] {
+ m.set(PhotoId(id), true);
+ }
+ assert!(matches!(m, Members::Set(_)), "a set once the ids are too far apart for bits");
+ assert_eq!((ids(&m, &[3, 70, 71, 9_000_000_000, u64::MAX]), m.len()), (vec![3, 70, 9_000_000_000], 3));
+ m.set(PhotoId(u64::MAX), true);
+ m.set(PhotoId(3), false);
+ assert_eq!((ids(&m, &[3, 70, 9_000_000_000, u64::MAX]), m.len()), (vec![70, 9_000_000_000, u64::MAX], 3));
+ // an album of many photos with ids counting up stays bits, however many
+ let mut dense = Members::none();
+ for id in 1..=200_000u64 {
+ dense.set(PhotoId(id), true);
+ }
+ assert!(matches!(&dense, Members::Bits { words, .. } if words.len() <= 200_000 / 64 + 1));
+ assert_eq!(dense.len(), 200_000);
+ }
+}
diff --git a/crates/catalog/src/tests.rs b/crates/catalog/src/tests.rs
index acfbb1bb1..a32b5acf5 100644
--- a/crates/catalog/src/tests.rs
+++ b/crates/catalog/src/tests.rs
@@ -123,6 +123,65 @@ fn filter_search_sort() {
assert_eq!(c.keywords(), vec![("ocean".to_string(), 1), ("summer".to_string(), 1)]);
}
+/// A search word with a colon whose left part is not a known field ("10:30", "Re:") is plain
+/// text, not a fielded token that matches nothing.
+#[test]
+fn free_text_with_a_colon_searches_the_text_fields() {
+ let mut c = Catalog::new();
+ let a = photo(&mut c, "meeting.jpg", "2026-05-01T10:00:00");
+ let _b = photo(&mut c, "other.jpg", "2026-04-01T10:00:00");
+ let mut m = c.photo(a).unwrap().meta.clone();
+ m.caption = "Meeting at 10:30 with Re: notes".into();
+ m.iso = Some(1600);
+ c.apply(Op::SetMeta { id: a, meta: Box::new(m) }).unwrap();
+ let q = |text: &str| c.query(&Filter { text: text.into(), ..Default::default() }, &Sort::default());
+ assert_eq!(q("meeting"), vec![a]);
+ assert_eq!(q("10:30"), vec![a], "a free-text token with a colon");
+ assert_eq!(q("re:"), vec![a]);
+ assert_eq!(q("RE: notes"), vec![a], "case-insensitive, one token per word");
+ assert_eq!(q("iso:>800"), vec![a], "known fields stay fielded");
+ assert_eq!(q("iso:>2000"), Vec::::new());
+ assert_eq!(q("11:45"), Vec::::new(), "an unknown field matching no text finds nothing");
+}
+
+/// `f:`/`aperture:` and `focal:` are stored as `f32`; a typed value that isn't exact in binary
+/// (2.8, 5.6) must still match, and `<`/`>` must not count the photo's own value.
+#[test]
+fn search_tokens_match_f32_aperture_and_focal() {
+ let mut c = Catalog::new();
+ let a = photo(&mut c, "a.jpg", "2026-05-01T10:00:00");
+ let mut m = c.photo(a).unwrap().meta.clone();
+ m.aperture = Some(2.8);
+ m.focal_mm = Some(5.6);
+ c.apply(Op::SetMeta { id: a, meta: Box::new(m) }).unwrap();
+ let q = |t: &str| c.query(&Filter { text: t.into(), ..Default::default() }, &Sort::default());
+ assert_eq!(q("f:2.8"), vec![a], "f:2.8");
+ assert_eq!(q("aperture:=2.8"), vec![a], "aperture:=2.8");
+ assert_eq!(q("focal:5.6"), vec![a], "focal:5.6");
+ assert!(q("f:2.9").is_empty(), "f:2.9");
+ assert!(q("f:<2.8").is_empty(), "f:<2.8");
+ assert!(q("f:>2.8").is_empty(), "f:>2.8");
+ assert!(q("focal:<5.6").is_empty(), "focal:<5.6");
+ assert_eq!(q("f:>2.7"), vec![a], "f:>2.7");
+}
+
+/// A `captured` string that is not ASCII (the Lightroom importer copies `captureTime` into an
+/// `AddPhoto` op verbatim, and a JSON snapshot is valid UTF-8 whatever it holds) must not be
+/// sliced on a byte index in `date_groups`: grouping counts the well-formed dates and ignores
+/// the rest instead of panicking on a char boundary.
+#[test]
+fn date_groups_ignores_a_non_ascii_captured() {
+ let mut c = Catalog::new();
+ let ok = photo(&mut c, "ok.jpg", "2026-05-01T10:00:00");
+ // `AddPhoto` takes the field as it is, like `library.importLightroom` building the op
+ let bad = photo(&mut c, "bad.jpg", "日本語日本語");
+ assert_ne!(ok, bad);
+ let g = c.date_groups();
+ assert_eq!(g.len(), 1, "the one well-formed date still groups");
+ assert_eq!((g[0].year.as_str(), g[0].count), ("2026", 1));
+ assert_eq!(g[0].days.iter().map(|d| d.1).sum::(), 1);
+}
+
#[test]
fn snapshot_and_log_replay() {
let mut c = Catalog::new();
@@ -265,13 +324,30 @@ fn preview_only_survives_serde_and_set_content_undo() {
let old: Catalog = serde_json::from_value(v).unwrap();
assert_eq!(old.photo(a).unwrap().preview_only, None);
assert!(old.photo(a).unwrap().develops_raw());
- // Reload: the file decodes now → cleared; undo (through the journal's JSON) restores it
- let op = Op::SetContent { id: a, width: 6016, height: 4000, file_size: 1, content_hash: None, preview_only: None };
+ // a newer build still can't decode it: the reason and the stamp of the version that decided
+ // it are recorded together (#662)
+ let op = Op::SetContent {
+ id: a,
+ width: 6000,
+ height: 4000,
+ file_size: 1,
+ content_hash: None,
+ preview_only: Some("Nikon Huffman-compressed NEF".into()),
+ preview_checked: "0.6.0".into(),
+ };
+ c.apply(op).unwrap();
+ assert_eq!(c.photo(a).unwrap().preview_checked, "0.6.0");
+ // Reload: the file decodes now → cleared, with its stamp; undo (through the journal's JSON)
+ // restores both
+ let op =
+ Op::SetContent { id: a, width: 6016, height: 4000, file_size: 1, content_hash: None, preview_only: None, preview_checked: String::new() };
let inv = c.apply(op).unwrap();
assert!(c.photo(a).unwrap().develops_raw());
+ assert_eq!(c.photo(a).unwrap().preview_checked, "");
let inv: Op = serde_json::from_str(&serde_json::to_string(&inv).unwrap()).unwrap();
c.apply(inv).unwrap();
assert_eq!(c.photo(a).unwrap().preview_only.as_deref(), Some("Nikon Huffman-compressed NEF"));
+ assert_eq!(c.photo(a).unwrap().preview_checked, "0.6.0");
assert_eq!(c.photo(a).unwrap().width, 6000);
}
@@ -391,6 +467,28 @@ fn empty_regions_are_not_serialized_and_default_when_missing() {
assert_eq!(back, with);
}
+/// The camera's focus point is optional camera data: absent from the catalog JSON when unknown (so a
+/// catalog from before it existed reads with none, and writes the same), kept exactly when known.
+#[test]
+fn focus_point_is_not_serialized_when_unknown_and_round_trips() {
+ let m = Meta { camera: "SONY ILCE-1".into(), ..Default::default() };
+ let v = serde_json::to_value(&m).unwrap();
+ assert!(v.get("focus").is_none(), "{v}");
+ let back: Meta = serde_json::from_value(v).unwrap();
+ assert!(back.focus.is_none());
+ let mut with = m.clone();
+ with.focus = Some(lightcraft_meta::FocusPoint {
+ rect: lightcraft_meta::Rect { x0: 0.36, y0: 0.33, x1: 0.38, y1: 0.36 },
+ mode: Some("AF-C".into()),
+ area: Some("Wide".into()),
+ tracking: None,
+ });
+ let json = serde_json::to_string(&with).unwrap();
+ assert!(json.contains("\"focus\"") && !json.contains("tracking"), "{json}");
+ let back: Meta = serde_json::from_str(&json).unwrap();
+ assert_eq!(back, with);
+}
+
// ---- random sort: Given a seed, the order is a stable, reproducible shuffle of the matching photos
fn many(c: &mut Catalog, n: usize) -> Vec {
@@ -737,3 +835,81 @@ fn cmp_name_ignores_case_without_allocating() {
assert_eq!(cmp_name("DSC_0001", "DSC_0002"), Less);
assert_eq!(cmp_name("a", "ab"), Less);
}
+
+/// What the catalog remembers of its latest ops: which photos each changed and what of them, as long as that can be
+/// said photo by photo.
+#[test]
+fn the_catalog_remembers_which_photos_its_latest_ops_changed_and_what_of_them() {
+ let mut c = Catalog::default();
+ let photo = |c: &mut Catalog, name: &str| {
+ let id = c.alloc_photo_id();
+ c.apply(Op::AddPhoto {
+ photo: Box::new(Photo::new(id, Source::File { path: format!("/p/{name}") }, name, "JPEG", 60, 40, "2026-01-01T10:00:00")),
+ })
+ .unwrap();
+ id
+ };
+ let (a, b) = (photo(&mut c, "a.jpg"), photo(&mut c, "b.jpg"));
+ let start = c.revision;
+ assert_eq!(c.touched_since(start), Some(vec![]));
+ assert_eq!(c.aspects_since(start), Some(Aspects::NONE));
+ // a rating, then a flag on another photo, as one batch
+ c.apply(Op::SetRating { id: a, rating: 3 }).unwrap();
+ assert_eq!(c.touched_since(start), Some(vec![a]));
+ assert_eq!(c.aspects_since(start), Some(Aspects::RATING));
+ let after_rating = c.revision;
+ c.apply(Op::Batch { ops: vec![Op::SetFlag { id: b, flag: Flag::Pick }, Op::SetRating { id: a, rating: 4 }] }).unwrap();
+ assert_eq!(c.touched_since(start), Some(vec![a, b]), "each once");
+ assert_eq!(c.touched_since(after_rating), Some(vec![a, b]));
+ assert_eq!(c.aspects_since(after_rating), Some(Aspects::FLAG.with(Aspects::RATING)));
+ assert_eq!(c.touched_since(c.revision), Some(vec![]));
+ // a photo that goes counts for everything
+ let before = c.revision;
+ c.apply(Op::RemovePhoto { id: b }).unwrap();
+ assert_eq!(c.touched_since(before), Some(vec![b]));
+ assert!(c.aspects_since(before).is_some_and(|x| x.any_of(Aspects::PRESENCE) && x.any_of(Aspects::FILE)));
+ // an op that changes more than photos of its own: nothing is said of what came before it
+ let before = c.revision;
+ let album = c.alloc_album_id();
+ c.apply(Op::AddAlbum { album: Album::new(album, "Trip") }).unwrap();
+ assert_eq!(c.touched_since(before), None);
+ assert_eq!(c.aspects_since(start), None);
+ assert_eq!(c.touched_since(c.revision), Some(vec![]), "from after it, it is known again");
+ // a revision that is not this catalog's past
+ assert_eq!(c.touched_since(c.revision + 5), None);
+ // a revision set from outside (a library just loaded): nothing is known of how the catalog got there, and after
+ // the next op nothing of before it
+ let old = c.revision;
+ c.revision = 7 << 32;
+ assert_eq!(c.touched_since(old), None);
+ assert_eq!(c.aspects_since(c.revision), None);
+ let loaded = c.revision;
+ c.apply(Op::SetRating { id: a, rating: 1 }).unwrap();
+ assert_eq!(c.touched_since(loaded), Some(vec![a]));
+ assert_eq!(c.touched_since(old), None);
+ // an op that fails changes nothing, and what is remembered never says less than what happened
+ let before = c.revision;
+ assert!(c.apply(Op::SetRating { id: PhotoId(9_999), rating: 1 }).is_err());
+ assert_eq!(c.revision, before);
+ c.apply(Op::SetFlag { id: a, flag: Flag::Pick }).unwrap();
+ assert!(c.touched_since(before).is_some_and(|ids| ids.contains(&a)));
+ assert!(c.aspects_since(before).is_some_and(|x| x.any_of(Aspects::FLAG)));
+ // a batch that fails midway is undone as a whole; a batch in a batch is remembered photo by photo; a batch
+ // with an op that changes more than photos is not
+ let before = c.revision;
+ assert!(c.apply(Op::Batch { ops: vec![Op::SetRating { id: a, rating: 5 }, Op::SetRating { id: PhotoId(9_999), rating: 1 }] }).is_err());
+ assert_eq!((c.revision, c.photo(a).map(|p| p.rating)), (before, Some(1)));
+ c.apply(Op::Batch { ops: vec![Op::Batch { ops: vec![Op::SetRating { id: a, rating: 2 }] }] }).unwrap();
+ // (the photo the failed batch asked for and never found may be named too: more is said, never less)
+ assert!(c.touched_since(before).is_some_and(|ids| ids.contains(&a)));
+ let before = c.revision;
+ c.apply(Op::Batch { ops: vec![Op::SetRating { id: a, rating: 3 }, Op::RenameAlbum { id: album, name: "Journey".into() }] }).unwrap();
+ assert_eq!(c.touched_since(before), None);
+ // more ops than are remembered: the oldest are answered with "anything"
+ let long_ago = c.revision;
+ for i in 0..9000u32 {
+ c.apply(Op::SetRating { id: a, rating: (i % 5) as u8 }).unwrap();
+ }
+ assert_eq!(c.touched_since(long_ago), None);
+ assert_eq!(c.touched_since(c.revision - 10), Some(vec![a]));
+}
diff --git a/crates/catalog/src/tests_format_version.rs b/crates/catalog/src/tests_format_version.rs
index e2461781a..e31d7fe0c 100644
--- a/crates/catalog/src/tests_format_version.rs
+++ b/crates/catalog/src/tests_format_version.rs
@@ -74,6 +74,29 @@ fn v3_library_loads_and_is_upgraded() {
assert_eq!(snapshot_version(&m), u64::from(VERSION));
}
+/// A v6 library (before preview-only photos recorded which version decided it, #662) opens with
+/// an empty stamp on its preview-only photos, so they are rechecked once, and is rewritten in the
+/// current format.
+#[test]
+fn v6_preview_only_photo_loads_unstamped_and_is_upgraded() {
+ let mut c = Catalog::new();
+ let id = c.alloc_photo_id();
+ let mut p = Photo::new(id, Source::File { path: "/p/a.cr3".into() }, "a.cr3", "CR3", 160, 120, "2026-01-01");
+ p.kind = MediaKind::Raw;
+ p.preview_only = Some("Cr3 files are not decoded yet".into());
+ c.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap();
+ let snap = c.to_snapshot();
+ assert!(!snap.contains("preview_checked"), "an empty stamp isn't written");
+ let m = MemStore::new();
+ m.set(SNAPSHOT, format!("{{\"format\":\"lightcraft-catalog\",\"version\":6,\"seq\":1,\"catalog\":{snap}}}\n").into_bytes());
+ let (_, loaded, r) = Journal::open(Box::new(m.clone())).unwrap();
+ let p = loaded.photo(id).unwrap();
+ assert_eq!(p.preview_only.as_deref(), Some("Cr3 files are not decoded yet"));
+ assert_eq!(p.preview_checked, "");
+ assert_eq!(r.upgraded_from, Some(6));
+ assert_eq!(snapshot_version(&m), u64::from(VERSION));
+}
+
#[test]
fn versionless_log_only_library_is_upgraded() {
let mut c = Catalog::new();
@@ -171,7 +194,8 @@ fn bad_crc_is_still_a_torn_tail() {
/// Guard for the version rule (see `journal` → *Format versions*): adding an [`Op`] variant breaks
/// this match. When it does, bump `journal::VERSION`, add a row to the format table, and add the
-/// variant here under the new version.
+/// variant here under the new version. A version that only adds serialized fields raises
+/// `NEWEST_FIELDS` instead.
#[test]
fn op_variants_are_versioned() {
fn since(op: &Op) -> u32 {
@@ -214,6 +238,8 @@ fn op_variants_are_versioned() {
Op::SetHistoryCursor { .. } => 8,
}
}
+ /// v9: `Photo.preview_checked` and `Op::SetContent.preview_checked`.
+ const NEWEST_FIELDS: u32 = 9;
let newest = since(&Op::SetHistoryCursor { id: PhotoId(1), cursor: None });
- assert_eq!(newest, VERSION, "the newest op's version must be the current format version");
+ assert_eq!(newest.max(NEWEST_FIELDS), VERSION, "the newest op's or fields' version must be the current format version");
}
diff --git a/crates/catalog/src/tests_index.rs b/crates/catalog/src/tests_index.rs
new file mode 100644
index 000000000..f1f41501b
--- /dev/null
+++ b/crates/catalog/src/tests_index.rs
@@ -0,0 +1,211 @@
+//! The derived indexes ([`crate::index`]) answer as a scan of the catalog would, through every op,
+//! inverse, failed batch, snapshot, replay and clone; and the faster query paths give the same
+//! results as before.
+
+use proptest::prelude::*;
+
+use super::*;
+
+const PHOTOS: u64 = 8;
+
+fn library() -> Catalog {
+ let mut c = Catalog::new();
+ for i in 0..PHOTOS {
+ let p = Photo::new(PhotoId(i + 1), Source::Demo { scene: 1 }, &format!("p{i}.jpg"), "JPEG", 60, 40, "2026-01-01T00:00:00");
+ c.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap();
+ }
+ c
+}
+
+/// What the indexes must say, worked out by scanning (as the catalog did before it had them).
+fn scanned(c: &Catalog) -> (Vec>, Vec>) {
+ let photos: Vec = (1..=PHOTOS + 1).map(PhotoId).collect();
+ let stacks = photos.iter().map(|p| c.stacks().find(|s| s.photos.contains(p)).map(|s| s.id)).collect();
+ let albums = (1..=4)
+ .map(|a| {
+ photos
+ .iter()
+ .map(|p| c.photo(*p).is_some() && c.album(AlbumId(a)).is_some_and(|al| al.smart.is_none() && al.photos.contains(p)))
+ .collect()
+ })
+ .collect();
+ (stacks, albums)
+}
+
+/// What the indexes say.
+fn indexed(c: &Catalog) -> (Vec>, Vec>) {
+ let photos: Vec = (1..=PHOTOS + 1).map(PhotoId).collect();
+ let stacks = photos.iter().map(|p| c.stack_of(*p).map(|s| s.id)).collect();
+ let albums = (1..=4)
+ .map(|a| {
+ photos
+ .iter()
+ .map(|p| c.album(AlbumId(a)).is_some_and(|al| al.smart.is_none()) && c.photo(*p).is_some_and(|ph| c.album_contains(AlbumId(a), ph)))
+ .collect()
+ })
+ .collect();
+ (stacks, albums)
+}
+
+/// Photo ids from bits (the extra id names a photo that isn't there: such ops fail).
+fn ids(bits: u16) -> Vec {
+ (0..=PHOTOS).filter(|i| bits & (1 << i) != 0).map(|i| PhotoId(i + 1)).collect()
+}
+
+fn op(c: &Catalog, kind: u8, n: u64, bits: u16, collapsed: bool) -> Op {
+ match kind % 8 {
+ 0 => Op::AddStack { stack: Stack { id: StackId(n % 4 + 1), photos: ids(bits), collapsed } },
+ 1 => Op::RemoveStack { id: StackId(n % 4 + 1) },
+ 2 => Op::SetStack { id: StackId(n % 4 + 1), photos: ids(bits), collapsed },
+ 3 => {
+ let mut a = Album::new(AlbumId(n % 4 + 1), "A");
+ a.photos = ids(bits);
+ Op::AddAlbum { album: a }
+ }
+ 4 => Op::RemoveAlbum { id: AlbumId(n % 4 + 1) },
+ 5 => Op::SetAlbumPhotos { id: AlbumId(n % 4 + 1), photos: ids(bits) },
+ // a batch whose last op fails half the time: what it did is rolled back
+ 6 => Op::Batch {
+ ops: vec![
+ Op::SetAlbumPhotos { id: AlbumId(n % 4 + 1), photos: ids(bits) },
+ Op::SetStack { id: StackId(n % 4 + 1), photos: ids(bits.rotate_left(3)), collapsed },
+ Op::AddStack { stack: Stack { id: StackId(n % 4 + 1), photos: ids(!bits), collapsed } },
+ ],
+ },
+ // as the engine deletes a photo: out of its albums and stacks too, in one batch
+ _ => c.delete_permanently_ops(PhotoId(n % (PHOTOS + 1) + 1)),
+ }
+}
+
+proptest! {
+ #![proptest_config(ProptestConfig::with_cases(256))]
+
+ #[test]
+ fn indexes_agree_with_a_scan(steps in proptest::collection::vec((0u8..8, 0u64..16, 0u16..512, any::()), 1..40)) {
+ let mut c = library();
+ let start = c.to_snapshot();
+ // built before the ops, so every op has to keep them up to date
+ prop_assert_eq!(indexed(&c), scanned(&c));
+ let mut log = String::new();
+ let mut inverses = Vec::new();
+ for (kind, n, bits, collapsed) in steps {
+ let op = op(&c, kind, n, bits, collapsed);
+ if let Ok(inv) = c.apply(op.clone()) {
+ log.push_str(&Catalog::op_to_log_line(&op));
+ inverses.push(inv);
+ }
+ prop_assert_eq!(indexed(&c), scanned(&c));
+ }
+ // a snapshot, a replayed journal and a clone build their own
+ let snap = Catalog::from_snapshot(&c.to_snapshot()).unwrap();
+ prop_assert_eq!(indexed(&snap), scanned(&c));
+ let mut replayed = Catalog::from_snapshot(&start).unwrap();
+ prop_assert_eq!(indexed(&replayed), scanned(&replayed));
+ replayed.replay(&log).unwrap();
+ prop_assert_eq!(indexed(&replayed), scanned(&c));
+ prop_assert_eq!(indexed(&c.clone()), scanned(&c));
+ prop_assert!(c.clone() == c, "the indexes don't take part in equality");
+ // undone step by step
+ for inv in inverses.into_iter().rev() {
+ c.apply(inv).unwrap();
+ prop_assert_eq!(indexed(&c), scanned(&c));
+ }
+ prop_assert!(c.stacks().next().is_none() && c.albums().next().is_none(), "all undone");
+ }
+}
+
+#[test]
+fn a_photo_in_another_stack_is_refused_by_the_index() {
+ let mut c = library();
+ c.apply(Op::AddStack { stack: Stack { id: StackId(1), photos: vec![PhotoId(1), PhotoId(2)], collapsed: false } }).unwrap();
+ let e = c.apply(Op::AddStack { stack: Stack { id: StackId(2), photos: vec![PhotoId(2), PhotoId(3)], collapsed: false } }).unwrap_err();
+ assert_eq!(e, CatalogError::Invalid("a photo can be in only one stack".into()));
+ // its own stack may keep it
+ c.apply(Op::SetStack { id: StackId(1), photos: vec![PhotoId(2), PhotoId(1), PhotoId(3)], collapsed: true }).unwrap();
+ assert_eq!(c.stack_of(PhotoId(3)).map(|s| s.id), Some(StackId(1)));
+}
+
+/// The sort keys are worked out once per photo now; the order is the one the comparisons gave.
+#[test]
+fn sorted_queries_keep_their_order() {
+ let mut c = Catalog::new();
+ let names = ["b.jpg", "A.jpg", "a.jpg", "Été.jpg", "été.jpg", "C.JPG", "b.jpg"];
+ for (i, n) in names.iter().enumerate() {
+ let mut p = Photo::new(PhotoId(i as u64 + 1), Source::Demo { scene: 1 }, n, "JPEG", 60, 40, &format!("2026-01-0{}T00:00:00", 7 - i % 3));
+ p.captured = (i % 2 == 0).then(|| format!("2025-0{}-01T00:00:00", 1 + i % 4));
+ p.rating = (i % 3) as u8;
+ c.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap();
+ }
+ let all = Filter::default();
+ for key in [SortKey::CaptureDate, SortKey::ImportDate, SortKey::EditDate, SortKey::FileName, SortKey::Rating, SortKey::FileSize, SortKey::Random]
+ {
+ for ascending in [true, false] {
+ let sort = Sort { key, ascending, seed: 7, ..Default::default() };
+ // the comparison the query sorts by
+ let mut want: Vec<&Photo> = c.photos().map(|p| p.as_ref()).collect();
+ want.sort_by(|a, b| sort.order(a, b));
+ let want: Vec = want.iter().map(|p| p.id).collect();
+ assert_eq!(c.query(&all, &sort), want, "{key:?} ascending {ascending}");
+ }
+ }
+ // `only` as a set gives the photos it names
+ let only = Filter { only: vec![PhotoId(3), PhotoId(1), PhotoId(3)], ..Default::default() };
+ assert_eq!(c.query(&only, &Sort { key: SortKey::FileName, ascending: true, ..Default::default() }), vec![PhotoId(3), PhotoId(1)]);
+}
+
+/// Rules name flags, labels and kinds as their variants in lower case (what `{:?}` gave).
+#[test]
+fn rule_names_are_the_variants_in_lower_case() {
+ let mut c = Catalog::new();
+ let mut n = 0;
+ for flag in [Flag::None, Flag::Pick, Flag::Reject] {
+ for label in
+ [None, Some(ColorLabel::Red), Some(ColorLabel::Yellow), Some(ColorLabel::Green), Some(ColorLabel::Blue), Some(ColorLabel::Purple)]
+ {
+ for kind in [MediaKind::Image, MediaKind::Raw, MediaKind::Video] {
+ n += 1;
+ let mut p = Photo::new(PhotoId(n), Source::Demo { scene: 1 }, "x.jpg", "JPEG", 60, 40, "2026-01-01T00:00:00");
+ (p.flag, p.label, p.kind) = (flag, label, kind);
+ c.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap();
+ }
+ }
+ }
+ c.apply(Op::SetLabelName { label: ColorLabel::Blue, name: Some("Sky".into()) }).unwrap();
+ let count = |field: &str, value: String| {
+ let rules: RuleSet =
+ serde_json::from_value(serde_json::json!({"match": "all", "rules": [{"field": field, "op": "is", "value": value}]})).unwrap();
+ c.query(&Filter { rule_set: Some(rules), ..Default::default() }, &Sort::default()).len()
+ };
+ for flag in [Flag::None, Flag::Pick, Flag::Reject] {
+ assert_eq!(count("flag", format!("{flag:?}").to_lowercase()), 18, "{flag:?}");
+ }
+ assert_eq!(count("flag", "picked".into()), 18);
+ for label in ColorLabel::ALL {
+ assert_eq!(count("label", format!("{label:?}").to_uppercase()), 9, "{label:?}");
+ }
+ assert_eq!(count("label", "sky".into()), 9, "a custom name");
+ assert_eq!(count("label", "none".into()), 9);
+ for kind in [MediaKind::Image, MediaKind::Raw, MediaKind::Video] {
+ assert_eq!(count("kind", format!("{kind:?}")), 18, "{kind:?}");
+ }
+}
+
+/// A hand-edited file can put a photo in two stacks (ops refuse it): loading repairs it (the photo
+/// stays in the first) and the stack index gives the answers a scan of the stacks gives, before
+/// and after either stack goes.
+#[test]
+fn a_photo_in_two_stacks_read_from_a_file_answers_as_a_scan() {
+ let mut c = library();
+ c.apply(Op::AddStack { stack: Stack { id: StackId(1), photos: vec![PhotoId(1), PhotoId(2)], collapsed: false } }).unwrap();
+ c.apply(Op::AddStack { stack: Stack { id: StackId(2), photos: vec![PhotoId(3), PhotoId(4)], collapsed: false } }).unwrap();
+ let mut v: serde_json::Value = serde_json::from_str(&c.to_snapshot()).unwrap();
+ v["stacks"]["2"]["photos"] = serde_json::json!([2, 3, 4]);
+ let mut c = Catalog::from_snapshot(&v.to_string()).unwrap();
+ assert_eq!(indexed(&c), scanned(&c));
+ assert_eq!(c.stack_of(PhotoId(2)).map(|s| s.id), Some(StackId(1)));
+ // the second stack can't be changed to keep it, as before
+ assert!(c.apply(Op::SetStack { id: StackId(2), photos: vec![PhotoId(2), PhotoId(3)], collapsed: false }).is_err());
+ c.apply(Op::RemoveStack { id: StackId(1) }).unwrap();
+ assert_eq!(indexed(&c), scanned(&c));
+ assert_eq!(c.stack_of(PhotoId(2)).map(|s| s.id), None, "the repair took it out of the second stack");
+}
diff --git a/crates/catalog/src/tests_journal.rs b/crates/catalog/src/tests_journal.rs
index 8570bd077..26b4e902a 100644
--- a/crates/catalog/src/tests_journal.rs
+++ b/crates/catalog/src/tests_journal.rs
@@ -376,3 +376,18 @@ fn a_damaged_process_value_loads_as_v1_with_the_edits() {
let d = &loaded.photo(id).unwrap().develop;
assert_eq!((d.process, d.light.exposure), (ProcessVersion::V1, 0.8));
}
+
+/// A snapshot whose `seq` is near `u64::MAX` is refused as corrupt: the next record's seq would
+/// wrap (and every later edit would be skipped as stale on reopen) or overflow-panic in debug.
+#[test]
+fn snapshot_with_seq_near_u64_max_is_corrupt() {
+ for seq in [u64::MAX, u64::MAX - 1, u64::MAX / 2 + 1] {
+ let m = MemStore::new();
+ m.set(
+ SNAPSHOT,
+ format!("{{\"format\":\"lightcraft-catalog\",\"version\":1,\"seq\":{seq},\"catalog\":{}}}\n", Catalog::new().to_snapshot()).into_bytes(),
+ );
+ let e = Journal::open(Box::new(m)).err();
+ assert!(matches!(e, Some(CatalogError::Corrupt(_))), "seq {seq}: {e:?}");
+ }
+}
diff --git a/crates/catalog/src/tests_lock.rs b/crates/catalog/src/tests_lock.rs
index d464d5c25..baa8da659 100644
--- a/crates/catalog/src/tests_lock.rs
+++ b/crates/catalog/src/tests_lock.rs
@@ -126,33 +126,40 @@ fn the_library_is_free_once_dropped_even_if_a_child_holds_a_copy() {
/// Issue #680, as it happens in practice: one thread spawns cheap children while another acquires,
/// drops and re-acquires the lock on its own directory. Every acquire must succeed (before the fix
-/// a few per cent were refused with `InUse(None)`). Bounded: about two seconds of cycles, pass or fail.
+/// a few per cent were refused with `InUse(None)`). Bounded: about two seconds of cycles, and on a
+/// slow or loaded machine (the FreeBSD CI VM, where a spawn can take longer than that) until a few
+/// children have been spawned during them, at most a minute.
#[cfg(unix)]
#[test]
fn dropping_the_lock_frees_the_library_while_another_thread_spawns_processes() {
use std::sync::Arc;
- use std::sync::atomic::{AtomicBool, Ordering};
+ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
+ use std::time::{Duration, Instant};
+
+ /// Children spawned while the lock cycles, at least, before the test may stop.
+ const CHILDREN: usize = 5;
let dir = temp_dir("spawn");
let stop = Arc::new(AtomicBool::new(false));
+ let count = Arc::new(AtomicUsize::new(0));
let spawner = {
- let stop = Arc::clone(&stop);
+ let (stop, count) = (Arc::clone(&stop), Arc::clone(&count));
std::thread::spawn(move || {
- let mut spawned = 0usize;
while !stop.load(Ordering::Relaxed) {
if let Ok(mut c) = std::process::Command::new("true").stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn()
{
let _ = c.wait();
- spawned += 1;
+ count.fetch_add(1, Ordering::Relaxed);
}
}
- spawned
+ count.load(Ordering::Relaxed)
})
};
- let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
+ let t0 = Instant::now();
+ let enough = |cycles: usize| (cycles >= 200_000 || t0.elapsed() >= Duration::from_secs(2)) && count.load(Ordering::Relaxed) >= CHILDREN;
let mut cycles = 0usize;
let mut refused = Vec::new();
- while cycles < 200_000 && std::time::Instant::now() < deadline {
+ while !enough(cycles) && t0.elapsed() < Duration::from_secs(60) {
match LibraryLock::acquire(&dir, "LightCraft") {
Ok(l) => assert!(l.held()), // dropped here: the next cycle must get it back
Err(e) => refused.push(format!("cycle {cycles}: {e:?}")),
diff --git a/crates/catalog/src/tests_scale.rs b/crates/catalog/src/tests_scale.rs
new file mode 100644
index 000000000..81ddccc41
--- /dev/null
+++ b/crates/catalog/src/tests_scale.rs
@@ -0,0 +1,92 @@
+//! What library queries cost at 100k photos (ignored:
+//! `cargo test --release -p lightcraft-catalog --lib scale_100k -- --ignored --nocapture`).
+
+use std::time::Instant;
+
+use serde_json::json;
+
+use super::*;
+
+/// 100k photos, 10k two-photo stacks, a 20k-photo album, keywords and flags.
+fn library(n: u64) -> (Catalog, AlbumId) {
+ let mut c = Catalog::new();
+ let mut ops: Vec = (0..n)
+ .map(|i| {
+ let mut p = Photo::new(
+ PhotoId(i + 1),
+ Source::Demo { scene: (i % 20) as u32 },
+ &format!("IMG_{:06}.jpg", mix64(i) % 1_000_000),
+ "JPEG",
+ 6000,
+ 4000,
+ "2026-01-01T00:00:00",
+ );
+ p.captured = Some(format!("20{:02}-{:02}-{:02}T{:02}:00:00", 10 + i % 16, 1 + i % 12, 1 + i % 28, i % 24));
+ p.rating = (i % 6) as u8;
+ p.flag = if i % 7 == 0 { Flag::Pick } else { Flag::None };
+ p.meta.keywords = vec![format!("kw{}", i % 500), "travel|italy".into()];
+ Op::AddPhoto { photo: Box::new(p) }
+ })
+ .collect();
+ ops.extend(
+ (0..n / 10)
+ .map(|s| Op::AddStack { stack: Stack { id: StackId(s + 1), photos: vec![PhotoId(s * 10 + 1), PhotoId(s * 10 + 2)], collapsed: false } }),
+ );
+ let album = AlbumId(1);
+ let mut a = Album::new(album, "Big");
+ a.photos = (0..n).step_by(5).map(|i| PhotoId(i + 1)).collect();
+ ops.push(Op::AddAlbum { album: a });
+ let t = Instant::now();
+ c.apply(Op::Batch { ops }).unwrap();
+ eprintln!("build {n} photos, {} stacks: {:.0} ms", n / 10, t.elapsed().as_secs_f64() * 1e3);
+ (c, album)
+}
+
+fn smart(c: &mut Catalog, id: u64, rules: serde_json::Value) -> AlbumId {
+ let mut a = Album::new(AlbumId(id), format!("Smart {id}"));
+ a.smart = Some(Box::new(Filter { rule_set: Some(serde_json::from_value(rules).unwrap()), ..Default::default() }));
+ c.apply(Op::AddAlbum { album: a }).unwrap();
+ AlbumId(id)
+}
+
+/// Best of five runs (the machine may be busy with other work).
+fn time(what: &str, f: impl Fn() -> T) -> T {
+ let mut best = f64::MAX;
+ let mut r = None;
+ for _ in 0..5 {
+ let t = Instant::now();
+ r = Some(f());
+ best = best.min(t.elapsed().as_secs_f64() * 1e3);
+ }
+ eprintln!("{what}: {best:.1} ms");
+ r.unwrap()
+}
+
+#[test]
+#[ignore]
+fn scale_100k() {
+ let (mut c, album) = library(100_000);
+ let all = Filter::default();
+ time("query, capture date", || c.query(&all, &Sort::default()));
+ time("query, file name", || c.query(&all, &Sort { key: SortKey::FileName, ..Default::default() }));
+ time("query, text", || c.query(&Filter { text: "kw42".into(), ..Default::default() }, &Sort::default()));
+ time("query, album of 20k", || c.query(&Filter { album: Some(album), ..Default::default() }, &Sort::default()));
+ let only: Vec = (0..5000).map(|i| PhotoId(i * 7 + 1)).collect();
+ time("query, only 5k ids", || c.query(&Filter { only: only.clone(), ..Default::default() }, &Sort::default()));
+ let stacked = smart(&mut c, 10, json!({"match": "all", "rules": [{"field": "stacked", "op": "is", "value": true}]}));
+ let flagged = smart(
+ &mut c,
+ 11,
+ json!({"match": "all", "rules": [{"field": "flag", "op": "is", "value": "pick"}, {"field": "kind", "op": "is", "value": "image"}]}),
+ );
+ let keyword = smart(&mut c, 12, json!({"match": "all", "rules": [{"field": "keywords", "op": "contains", "value": "kw42"}]}));
+ let in_album = smart(&mut c, 13, json!({"match": "all", "rules": [{"field": "album", "op": "is", "value": album.0}]}));
+ let recent = smart(&mut c, 14, json!({"match": "all", "rules": [{"field": "captureDate", "op": "inLast", "value": {"n": 2, "unit": "years"}}]}));
+ for (what, a) in [("stacked", stacked), ("flag + kind", flagged), ("keyword", keyword), ("in album", in_album), ("in the last 2 years", recent)] {
+ let n = time(&format!("smart album count, {what}"), || c.album_count(a));
+ eprintln!(" = {n}");
+ }
+ time("albums_of", || c.albums_of(PhotoId(6)));
+ time("arrange stacks", || c.arrange_stacks(&c.query(&all, &Sort::default())));
+ time("stack_of × 1000", || (0..1000u64).filter(|i| c.stack_of(PhotoId(i * 97 + 1)).is_some()).count());
+}
diff --git a/crates/codecs/Cargo.toml b/crates/codecs/Cargo.toml
index 166fbf4dc..589d285fa 100644
--- a/crates/codecs/Cargo.toml
+++ b/crates/codecs/Cargo.toml
@@ -48,8 +48,13 @@ heif = ["dep:lightcraft-heif"]
avif = ["dep:ravif", "dep:rav1e", "dep:avif-serialize"]
[dev-dependencies]
+# tests record why they skip (`cargo xtask ci` lists the skips)
+lightcraft-testkit = { workspace = true }
# JPEG XL tests read encoded samples without our colour conversion
jxl-oxide = { workspace = true }
proptest = { workspace = true }
# Lossless synthetic HEIC files for the `heif` feature's tests.
lightcraft-heif = { workspace = true, features = ["testdata"] }
+
+[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
+dhat = { workspace = true }
diff --git a/crates/codecs/README.md b/crates/codecs/README.md
index 6ea5866b7..5902243fc 100644
--- a/crates/codecs/README.md
+++ b/crates/codecs/README.md
@@ -73,6 +73,10 @@ ICC profiles are parsed with `moxcms` (v2/v4, `curv`/`para` TRCs, `chad`); sRGB,
Adobe RGB (1998), ProPhoto (ROMM) and Rec.2020 are recognised by colorants. `icc::write_matrix_trc`
emits v4 display profiles (D50 PCS, Bradford `chad`) for export embedding.
+Before colour conversion, positive-infinity float samples become 1.0; NaN and negative infinity
+become 0.0. This also applies to alpha. Finite samples keep their existing range: matrix/TRC paths
+preserve values above 1.0 and below 0.0, while CMS inputs and alpha remain bounded to 0..1.
+
## Dependencies and licences
All pure Rust, verified with `cargo info` / `cargo tree`:
diff --git a/crates/codecs/src/convert.rs b/crates/codecs/src/convert.rs
index 02652db5a..836aed548 100644
--- a/crates/codecs/src/convert.rs
+++ b/crates/codecs/src/convert.rs
@@ -44,11 +44,23 @@ impl Buf {
match self {
Buf::U8(v) => v[i] as f32 / 255.0,
Buf::U16(v) => v[i] as f32 / 65535.0,
- Buf::F32(v) => v[i],
+ Buf::F32(v) => normalize_float(v[i]),
}
}
}
+/// Clip non-finite device samples before colour conversion, preserving finite extended range.
+#[inline]
+fn normalize_float(value: f32) -> f32 {
+ if value == f32::INFINITY {
+ 1.0
+ } else if value.is_finite() {
+ value
+ } else {
+ 0.0
+ }
+}
+
/// Interleaved device samples as produced by a format decoder.
pub(crate) struct Raw {
pub width: usize,
@@ -218,10 +230,7 @@ fn linearize(raw: &Raw, ch: usize, trc: &[Trc; 3]) -> Rgb32f {
par_rows(&mut img.data, w, |y, row| {
for (x, o) in row.iter_mut().enumerate() {
let i = (y * w + x) * ch;
- let s = |c: usize| {
- let val = v[i + c];
- if val.is_finite() { val } else { 0.0 }
- };
+ let s = |c: usize| normalize_float(v[i + c]);
*o = if gray {
let l = if lin { s(0) } else { trc[1].to_linear(s(0)) };
[l, l, l]
diff --git a/crates/codecs/src/jpeg.rs b/crates/codecs/src/jpeg.rs
index e40cf7e73..b8de12bdd 100644
--- a/crates/codecs/src/jpeg.rs
+++ b/crates/codecs/src/jpeg.rs
@@ -75,14 +75,12 @@ pub(crate) fn parse_markers(b: &[u8]) -> Option {
let end = (p + 2 + len).min(b.len());
let seg = &b[start..end];
match marker {
- 0xC0..=0xCF if marker != 0xC4 && marker != 0xC8 && marker != 0xCC => {
- if seg.len() >= 6 && m.components == 0 {
- m.precision = seg[0];
- m.sof = marker;
- m.height = u16::from_be_bytes([seg[1], seg[2]]) as u32;
- m.width = u16::from_be_bytes([seg[3], seg[4]]) as u32;
- m.components = seg[5];
- }
+ 0xC0..=0xCF if marker != 0xC4 && marker != 0xC8 && marker != 0xCC && seg.len() >= 6 && m.components == 0 => {
+ m.precision = seg[0];
+ m.sof = marker;
+ m.height = u16::from_be_bytes([seg[1], seg[2]]) as u32;
+ m.width = u16::from_be_bytes([seg[3], seg[4]]) as u32;
+ m.components = seg[5];
}
0xE1 => {
if seg.starts_with(b"Exif\0\0") && m.exif.is_none() {
@@ -182,9 +180,11 @@ pub(crate) fn decode_with_fallback(bytes: &[u8], opts: &DecodeOptions, fallback:
check_size(F, m.width as u64, m.height as u64, opts)?;
let scale_to = opts.max_size.filter(|&(mw, mh)| mw > 0 && mh > 0 && (m.width >= mw.saturating_mul(2) || m.height >= mh.saturating_mul(2)));
// zune-jpeg handles the common cases fastest; jpeg-decoder covers DCT scaling, CMYK/YCCK,
- // 12-bit and non-interleaved sequential scans (which zune-jpeg 0.5 mis-decodes with subsampling).
+ // 12-bit, non-interleaved sequential scans (which zune-jpeg 0.5 mis-decodes with subsampling)
+ // and every lossless (SOF3) file, which zune-jpeg refuses outright.
+ let lossless = m.sof == 0xC3;
let non_interleaved = matches!(m.sof, 0xC0 | 0xC1) && m.first_scan_components < m.components;
- let raw = if m.components == 4 || scale_to.is_some() || m.precision > 8 || non_interleaved {
+ let raw = if m.components == 4 || scale_to.is_some() || m.precision > 8 || non_interleaved || lossless {
decode_jpeg_decoder(bytes, &m, scale_to)?
} else {
decode_zune(bytes, &m)?
@@ -221,7 +221,9 @@ fn decode_jpeg_decoder(bytes: &[u8], m: &Markers, scale_to: Option<(u32, u32)>)
use jpeg_decoder::PixelFormat;
let mut d = jpeg_decoder::Decoder::new(bytes);
d.read_info().map_err(|e| Error::Malformed(F, e.to_string()))?;
- if let Some((mw, mh)) = scale_to {
+ // `scale` is the DCT size, not a lossless resample: on a lossless frame it would cut the
+ // output to the top-left of each plane. Those decode at full size; `finish` scales after.
+ if let Some((mw, mh)) = scale_to.filter(|_| m.sof != 0xC3) {
// Ask for the smallest DCT scale still covering the target box.
let (sw, sh) = scaled_request(m.width, m.height, mw, mh);
d.scale(sw, sh).map_err(|e| Error::Malformed(F, e.to_string()))?;
@@ -231,16 +233,25 @@ fn decode_jpeg_decoder(bytes: &[u8], m: &Markers, scale_to: Option<(u32, u32)>)
let (w, h) = (info.width as usize, info.height as usize);
let (model, buf, depth) = match info.pixel_format {
PixelFormat::L8 => (Model::Gray, Buf::U8(px), 8),
- PixelFormat::L16 => {
- let v = px.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
- (Model::Gray, Buf::U16(v), 16)
- }
+ PixelFormat::L16 => (Model::Gray, Buf::U16(lossless_samples(&px, m.precision)), 16),
+ // A lossless frame over 8 bits per sample still reports `RGB24` (or `CMYK32`), but its
+ // buffer holds two native-endian bytes per sample like L16.
+ PixelFormat::RGB24 if m.precision > 8 => (Model::Rgb, Buf::U16(lossless_samples(&px, m.precision)), 16),
PixelFormat::RGB24 => (Model::Rgb, Buf::U8(px), 8),
+ PixelFormat::CMYK32 if m.precision > 8 => (Model::Cmyk, Buf::U16(lossless_samples(&px, m.precision)), 16),
PixelFormat::CMYK32 => (Model::Cmyk, Buf::U8(px), 8),
};
Ok(Raw { width: w, height: h, model, alpha: false, premultiplied: false, buf, bit_depth: depth })
}
+/// jpeg-decoder's lossless samples: one native-endian `u16` per sample holding the raw
+/// `precision`-bit value. Left-justify it into the 16-bit range the rest of the pipeline reads,
+/// as every 16-bit decoder feeding [`Buf::U16`] does.
+fn lossless_samples(px: &[u8], precision: u8) -> Vec {
+ let shift = 16u8.saturating_sub(precision);
+ px.as_chunks::<2>().0.iter().map(|c| u16::from_ne_bytes([c[0], c[1]]) << shift).collect()
+}
+
/// The first `0xFF` byte, eight bytes at a time (scan data has one every few hundred bytes: this
/// is most of [`header`]'s time).
fn find_ff(b: &[u8]) -> Option {
@@ -450,4 +461,94 @@ mod tests {
let m = parse_markers(&[0xFF, 0xD8, 0xFF, 0xE1, 0, 200, 1, 2]).unwrap();
assert!(m.exif.is_none());
}
+
+ // --- lossless (SOF3) synthetic files: one DHT table with a single 1-bit category-0 code, so
+ // every pixel is a zero difference and decodes to the predictor's initial sample. No such
+ // encoder exists here; the bytes are the smallest a decoder accepts.
+
+ fn lossless_header(out: &mut Vec, components: u8, w: u16, precision: u8) {
+ // DHT: one DC table (id 0) with a single 1-bit code for the category-0 symbol.
+ out.extend_from_slice(&[0xFF, 0xC4]);
+ out.extend_from_slice(&(2u16 + 1 + 16 + 1).to_be_bytes());
+ out.push(0x00);
+ let mut counts = [0u8; 16];
+ counts[0] = 1;
+ out.extend_from_slice(&counts);
+ out.push(0x00);
+ // SOF3
+ out.extend_from_slice(&[0xFF, 0xC3]);
+ out.extend_from_slice(&(8u16 + 3 * components as u16).to_be_bytes());
+ out.push(precision);
+ out.extend_from_slice(&1u16.to_be_bytes());
+ out.extend_from_slice(&w.to_be_bytes());
+ out.push(components);
+ for c in 1..=components {
+ out.extend_from_slice(&[c, 0x11, 0]);
+ }
+ // SOS: predictor Ra, Se = 0, Ah/Al = 0
+ out.extend_from_slice(&[0xFF, 0xDA]);
+ out.extend_from_slice(&(6u16 + 2 * components as u16).to_be_bytes());
+ out.push(components);
+ for c in 1..=components {
+ out.extend_from_slice(&[c, 0]);
+ }
+ out.extend_from_slice(&[1, 0, 0]);
+ }
+
+ /// A minimal lossless JPEG, `w` x 1, `components` samples per pixel, every sample the initial
+ /// prediction `1 << (precision - 1)`.
+ fn lossless_jpeg(components: u8, w: u16, precision: u8) -> Vec {
+ let mut out = vec![0xFF, 0xD8];
+ lossless_header(&mut out, components, w, precision);
+ // entropy: one zero bit per sample (a category-0 code), padded to whole bytes
+ out.extend_from_slice(&vec![0u8; (components as usize * w as usize).div_ceil(8)]);
+ out.extend_from_slice(&[0xFF, 0xD9]);
+ out
+ }
+
+ /// jpeg-decoder emits lossless samples native-endian (its `to_ne_bytes`) holding the raw
+ /// `precision`-bit value, so the old `from_be_bytes` swapped every sample (2048 -> 8) and the
+ /// raw value was never left-justified into the 16-bit buffer.
+ #[test]
+ fn lossless_grayscale_samples_are_native_endian_and_left_justified() {
+ let bytes = lossless_jpeg(1, 8, 12);
+ let m = parse_markers(&bytes).unwrap();
+ let raw = decode_jpeg_decoder(&bytes, &m, None).unwrap();
+ let Buf::U16(v) = &raw.buf else { panic!("expected 16-bit samples") };
+ assert_eq!(v, &vec![2048u16 << 4; 8], "0x0800 native-endian, left-justified");
+ assert_eq!((raw.model, raw.bit_depth), (Model::Gray, 16));
+ }
+
+ /// A lossless frame over 8 bits per sample reports `RGB24`, but its buffer is two bytes per
+ /// sample: it must become `Buf::U16`, not `Buf::U8` over double-length rows.
+ #[test]
+ fn lossless_high_precision_rgb_is_16_bit() {
+ let bytes = lossless_jpeg(3, 2, 12);
+ let m = parse_markers(&bytes).unwrap();
+ let raw = decode_jpeg_decoder(&bytes, &m, None).unwrap();
+ let Buf::U16(v) = &raw.buf else { panic!("expected 16-bit samples") };
+ assert_eq!(v, &vec![2048u16 << 4; 6]);
+ assert_eq!(raw.model, Model::Rgb);
+ }
+
+ /// The same 16-bit wrapping for four components: `CMYK32` over 8 bits is two bytes a sample.
+ #[test]
+ fn lossless_high_precision_cmyk_is_16_bit() {
+ let bytes = lossless_jpeg(4, 2, 12);
+ let m = parse_markers(&bytes).unwrap();
+ let raw = decode_jpeg_decoder(&bytes, &m, None).unwrap();
+ let Buf::U16(v) = &raw.buf else { panic!("expected 16-bit samples") };
+ assert_eq!(v, &vec![2048u16 << 4; 8]);
+ assert_eq!((raw.model, raw.bit_depth), (Model::Cmyk, 16));
+ }
+
+ /// zune-jpeg refuses SOF3 ("Lossless Huffman"), so an 8-bit lossless file must route to
+ /// jpeg-decoder; `header` accepts it, so `decode` used to fail a file we listed as readable.
+ #[test]
+ fn eight_bit_lossless_jpeg_decodes() {
+ let bytes = lossless_jpeg(1, 8, 8);
+ let d = crate::decode(&bytes, DecodeOptions::default()).unwrap();
+ assert_eq!((d.width, d.height), (8, 1));
+ assert_eq!(d.bit_depth, 8);
+ }
}
diff --git a/crates/codecs/src/jpeg_par.rs b/crates/codecs/src/jpeg_par.rs
index 6ec9b6a80..c8d9e5023 100644
--- a/crates/codecs/src/jpeg_par.rs
+++ b/crates/codecs/src/jpeg_par.rs
@@ -3,8 +3,11 @@
//! The image is cut into bands of MCU rows separated by restart markers (`DRI` + `RSTn`): DC
//! prediction restarts at every marker, so bands are entropy-coded independently on all cores and
//! concatenated. Quantization uses the example tables of T.81 Annex K scaled by the usual 1–100
-//! quality convention; Huffman coding uses the Annex K example tables (interleaved baseline, readable
-//! by every decoder). Supports grayscale and YCbCr 4:4:4 / 4:2:0.
+//! quality convention. Huffman tables are made for each image from its own symbol counts (T.81
+//! Annex K.2): a first pass over the bands counts the symbols, a second codes them. The file stays
+//! interleaved baseline with the tables in its `DHT` segments, readable by every decoder, and is
+//! smaller than with the Annex K example tables at the same pixels. Supports grayscale and YCbCr
+//! 4:4:4 / 4:2:0.
use crate::encode::ChromaSubsampling;
@@ -61,16 +64,103 @@ impl Huff {
let (mut c, mut k) = (0u16, 0usize);
for (len, &n) in bits.iter().enumerate() {
for _ in 0..n {
- code[vals[k] as usize] = (c, len as u8 + 1);
- c += 1;
+ if let Some(&v) = vals.get(k) {
+ code[v as usize] = (c, len as u8 + 1);
+ }
+ c = c.wrapping_add(1);
k += 1;
}
- c <<= 1;
+ c = c.wrapping_shl(1);
}
Huff { code }
}
}
+/// Symbol counts of one Huffman table: index = symbol (DC size category, or AC run/size byte).
+type Freq = [u64; 256];
+
+/// The code lengths (`BITS`, counts per length 1..16) and symbols (`HUFFVAL`) of an optimal
+/// Huffman table for `freq`, limited to 16-bit codes and never assigning the all-ones code
+/// (T.81 Annex K.2, figures K.1–K.4). Symbols with a zero count get no code. At least one symbol
+/// is always present (a table nothing used gets symbol 0), so the result is a valid `DHT`.
+fn optimal_table(freq: &Freq) -> ([u8; 16], Vec) {
+ // 256 is the reserved pseudo-symbol: counted once, it takes the longest code, which is
+ // then dropped so no real code is all ones (K.2)
+ let mut f = [0u64; 257];
+ f[..256].copy_from_slice(freq);
+ if f[..256].iter().all(|&c| c == 0) {
+ f[0] = 1;
+ }
+ f[256] = 1;
+ let mut size = [0usize; 257];
+ let mut next: [Option; 257] = [None; 257];
+ loop {
+ // the two smallest non-zero counts; on equal counts the larger symbol goes first (K.1)
+ let mut c1: Option = None;
+ for i in 0..257 {
+ if f[i] > 0 && c1.is_none_or(|c| f[i] <= f[c]) {
+ c1 = Some(i);
+ }
+ }
+ let Some(c1) = c1 else { break };
+ let mut c2: Option = None;
+ for i in 0..257 {
+ if i != c1 && f[i] > 0 && c2.is_none_or(|c| f[i] <= f[c]) {
+ c2 = Some(i);
+ }
+ }
+ let Some(c2) = c2 else { break };
+ f[c1] = f[c1].saturating_add(f[c2]);
+ f[c2] = 0;
+ // every symbol in both trees goes one level deeper; c2's chain is appended to c1's
+ let mut k = c1;
+ size[k] += 1;
+ while let Some(n) = next[k] {
+ k = n;
+ size[k] += 1;
+ }
+ next[k] = Some(c2);
+ let mut k = c2;
+ size[k] += 1;
+ while let Some(n) = next[k] {
+ k = n;
+ size[k] += 1;
+ }
+ }
+ // counts per code length; 257 symbols need at most 256 levels
+ let mut bits = [0usize; 257];
+ for &s in &size {
+ if s > 0 {
+ bits[s.min(256)] += 1;
+ }
+ }
+ // limit to 16 bits (K.3): move pairs of the longest codes up, one level at a time (the
+ // deepest level of a full tree always holds an even number of codes)
+ for i in (17..257).rev() {
+ while bits[i] >= 2 {
+ let Some(j) = (1..i - 1).rev().find(|&j| bits[j] > 0) else { break };
+ bits[i] -= 2;
+ bits[i - 1] += 1;
+ bits[j + 1] += 2;
+ bits[j] -= 1;
+ }
+ }
+ // drop the reserved symbol's code: it is one of the longest
+ if let Some(i) = (1..=16).rev().find(|&i| bits[i] > 0) {
+ bits[i] -= 1;
+ }
+ // symbols by code length, then by value (K.4); the reserved symbol is not listed
+ let mut vals = Vec::new();
+ for len in 1..257 {
+ vals.extend((0..256).filter(|&s| size[s] == len).map(|s| s as u8));
+ }
+ let mut out = [0u8; 16];
+ for (o, &b) in out.iter_mut().zip(&bits[1..=16]) {
+ *o = b.min(255) as u8;
+ }
+ (out, vals)
+}
+
fn scaled_quant(base: &[u16; 64], quality: u8) -> [u16; 64] {
let q = quality.clamp(1, 100) as u32;
let scale = if q < 50 { 5000 / q } else { 200 - 2 * q };
@@ -177,13 +267,15 @@ fn magnitude(v: i32) -> (u32, u32) {
(size, bits & ((1u32 << size) - 1))
}
-fn encode_block(w: &mut Bits, q: &[i16; 64], pred: &mut i32, dc: &Huff, ac: &Huff) {
+/// The Huffman symbols of one quantized block in coding order: `sym(ac, symbol, bits, n)` for the
+/// DC size category (`ac` false) and each AC run/size, ZRL and EOB (`ac` true), each followed by
+/// its `n` extra bits `bits`.
+#[inline(always)]
+fn block_symbols(q: &[i16; 64], pred: &mut i32, mut sym: impl FnMut(bool, usize, u32, u32)) {
let d = q[0] as i32 - *pred;
*pred = q[0] as i32;
let (s, b) = magnitude(d);
- let (c, l) = dc.code[s as usize];
- w.put(c as u32, l as u32);
- w.put(b, s);
+ sym(false, s as usize, b, s);
let mut run = 0u32;
for &zi in &ZIGZAG[1..] {
let v = q[zi] as i32;
@@ -192,22 +284,45 @@ fn encode_block(w: &mut Bits, q: &[i16; 64], pred: &mut i32, dc: &Huff, ac: &Huf
continue;
}
while run >= 16 {
- let (c, l) = ac.code[0xF0];
- w.put(c as u32, l as u32);
+ sym(true, 0xF0, 0, 0);
run -= 16;
}
let (s, b) = magnitude(v);
- let (c, l) = ac.code[((run << 4) | s) as usize];
- w.put(c as u32, l as u32);
- w.put(b, s);
+ sym(true, ((run << 4) | s) as usize, b, s);
run = 0;
}
if run > 0 {
- let (c, l) = ac.code[0x00];
- w.put(c as u32, l as u32);
+ sym(true, 0x00, 0, 0);
}
}
+/// One coded symbol of the entropy-coded data: bits 0–7 the symbol, 8–9 its table (0 DC luma,
+/// 1 AC luma, 2 DC chroma, 3 AC chroma), 10–13 the number of extra bits, 14–29 those bits (a
+/// baseline coefficient needs at most 11).
+type Token = u32;
+
+/// Append one block's symbols to `out` and count them in `freq` (by table, as in [`Token`]).
+fn tokenize_block(q: &[i16; 64], pred: &mut i32, chroma: bool, out: &mut Vec, freq: &mut [Freq; 4]) {
+ let base = if chroma { 2 } else { 0 };
+ block_symbols(q, pred, |is_ac, s, bits, n| {
+ let table = base + is_ac as usize;
+ freq[table][s & 0xFF] += 1;
+ out.push((s as u32 & 0xFF) | (table as u32) << 8 | (n & 0xF) << 10 | (bits & 0xFFFF) << 14);
+ });
+}
+
+/// Code a band's symbols with `tables` (indexed as in [`Token`]).
+fn write_tokens(tokens: &[Token], tables: &[Huff; 4]) -> Vec {
+ let mut w = Bits::new();
+ for &t in tokens {
+ let (s, table, n, bits) = (t & 0xFF, (t >> 8) & 3, (t >> 10) & 0xF, t >> 14);
+ let (c, l) = tables[table as usize].code[s as usize];
+ w.put(c as u32, l as u32);
+ w.put(bits, n);
+ }
+ w.finish()
+}
+
/// Planar component samples (level-shifted to −128..127) with edge replication on read.
struct Plane {
w: usize,
@@ -249,7 +364,8 @@ fn dht(out: &mut Vec, class_id: u8, bits: &[u8; 16], vals: &[u8]) {
segment(out, 0xC4, &p);
}
-/// Encode interleaved 8-bit samples (`channels` 1, 3 or 4; alpha is dropped).
+/// Encode interleaved 8-bit samples (`channels` 1, 3 or 4; alpha is dropped), with Huffman
+/// tables made for the image.
pub fn encode(
data: &[u8],
width: usize,
@@ -258,6 +374,21 @@ pub fn encode(
quality: u8,
sub: ChromaSubsampling,
app_segments: &[(u8, Vec)],
+) -> Vec {
+ encode_with(data, width, height, channels, quality, sub, app_segments, true)
+}
+
+/// [`encode`], with optimized Huffman tables or (`optimize` false) the Annex K example tables.
+#[allow(clippy::too_many_arguments)]
+fn encode_with(
+ data: &[u8],
+ width: usize,
+ height: usize,
+ channels: usize,
+ quality: u8,
+ sub: ChromaSubsampling,
+ app_segments: &[(u8, Vec)],
+ optimize: bool,
) -> Vec {
let gray = channels == 1;
let (hs, vs) = match (gray, sub) {
@@ -331,42 +462,67 @@ pub fn encode(
let ql = scaled_quant(&Q_LUMA, quality);
let qc = scaled_quant(&Q_CHROMA, quality);
let (rl, rc) = (recip_table(&ql), recip_table(&qc));
- let (dcl, acl) = (Huff::new(&DC_LUMA_BITS, &DC_VALS), Huff::new(&AC_LUMA_BITS, &AC_LUMA_VALS));
- let (dcc, acc) = (Huff::new(&DC_CHROMA_BITS, &DC_VALS), Huff::new(&AC_CHROMA_BITS, &AC_CHROMA_VALS));
-
let (mcu_w, mcu_h) = (8 * hs, 8 * vs);
let (mx, my) = (width.div_ceil(mcu_w), height.div_ceil(mcu_h));
// restart interval: whole MCU rows per band, ≤ 65535 MCUs, enough bands to keep all cores busy
let rows_per_band = (my / 64).clamp(1, (65_535 / mx.max(1)).max(1));
let bands: Vec = (0..my.div_ceil(rows_per_band)).collect();
- let encode_band = |band: &usize| -> Vec {
- let mut w = Bits::new();
- let mut pred = [0i32; 3];
+ // first pass: each band's quantized blocks as symbols, with their counts per table (DC
+ // prediction restarts per band, as the restart markers say)
+ let tokenize_band = |band: &usize| -> (Vec, [Freq; 4]) {
let (mut blk, mut q) = ([0f32; 64], [0i16; 64]);
+ let (mut tokens, mut freq) = (Vec::new(), [[0u64; 256]; 4]);
+ let mut pred = [0i32; 3];
for mcu_y in band * rows_per_band..((band + 1) * rows_per_band).min(my) {
for mcu_x in 0..mx {
for vy in 0..vs {
for hx in 0..hs {
planes[0].block(mcu_x * hs + hx, mcu_y * vs + vy, &mut blk);
fdct_quant(&blk, &rl, &mut q);
- encode_block(&mut w, &q, &mut pred[0], &dcl, &acl);
+ tokenize_block(&q, &mut pred[0], false, &mut tokens, &mut freq);
}
}
if !gray {
- for k in 1..3 {
- planes[k].block(mcu_x, mcu_y, &mut blk);
+ for (plane, pred) in planes.iter().zip(pred.iter_mut()).take(3).skip(1) {
+ plane.block(mcu_x, mcu_y, &mut blk);
fdct_quant(&blk, &rc, &mut q);
- encode_block(&mut w, &q, &mut pred[k], &dcc, &acc);
+ tokenize_block(&q, pred, true, &mut tokens, &mut freq);
}
}
}
}
- w.finish()
+ (tokens, freq)
};
#[cfg(feature = "parallel")]
- let coded: Vec> = bands.par_iter().map(encode_band).collect();
+ let tokenized: Vec<(Vec, [Freq; 4])> = bands.par_iter().map(tokenize_band).collect();
#[cfg(not(feature = "parallel"))]
- let coded: Vec> = bands.iter().map(encode_band).collect();
+ let tokenized: Vec<(Vec, [Freq; 4])> = bands.iter().map(tokenize_band).collect();
+ drop(planes);
+ // the tables: DC luma, AC luma, DC chroma, AC chroma
+ let tables: [([u8; 16], Vec); 4] = if optimize {
+ let mut total = [[0u64; 256]; 4];
+ for (_, f) in &tokenized {
+ for (t, f) in total.iter_mut().zip(f) {
+ for (t, f) in t.iter_mut().zip(f) {
+ *t = t.saturating_add(*f);
+ }
+ }
+ }
+ total.each_ref().map(optimal_table)
+ } else {
+ [
+ (DC_LUMA_BITS, DC_VALS.to_vec()),
+ (AC_LUMA_BITS, AC_LUMA_VALS.to_vec()),
+ (DC_CHROMA_BITS, DC_VALS.to_vec()),
+ (AC_CHROMA_BITS, AC_CHROMA_VALS.to_vec()),
+ ]
+ };
+ let huff = tables.each_ref().map(|(bits, vals)| Huff::new(bits, vals));
+ // second pass: code the symbols
+ #[cfg(feature = "parallel")]
+ let coded: Vec> = tokenized.into_par_iter().map(|(t, _)| write_tokens(&t, &huff)).collect();
+ #[cfg(not(feature = "parallel"))]
+ let coded: Vec> = tokenized.into_iter().map(|(t, _)| write_tokens(&t, &huff)).collect();
// headers
let mut out = Vec::with_capacity(coded.iter().map(Vec::len).sum::() + 4096);
@@ -396,11 +552,12 @@ pub fn encode(
sof.extend_from_slice(&[3, 1, ((hs as u8) << 4) | vs as u8, 0, 2, 0x11, 1, 3, 0x11, 1]);
}
segment(&mut out, 0xC0, &sof);
- dht(&mut out, 0x00, &DC_LUMA_BITS, &DC_VALS);
- dht(&mut out, 0x10, &AC_LUMA_BITS, &AC_LUMA_VALS);
+ let [(dlb, dlv), (alb, alv), (dcb, dcv), (acb, acv)] = &tables;
+ dht(&mut out, 0x00, dlb, dlv);
+ dht(&mut out, 0x10, alb, alv);
if !gray {
- dht(&mut out, 0x01, &DC_CHROMA_BITS, &DC_VALS);
- dht(&mut out, 0x11, &AC_CHROMA_BITS, &AC_CHROMA_VALS);
+ dht(&mut out, 0x01, dcb, dcv);
+ dht(&mut out, 0x11, acb, acv);
}
let interval = (rows_per_band * mx) as u16;
if coded.len() > 1 {
@@ -495,6 +652,100 @@ mod tests {
}
}
+ /// The code lengths of `(bits, vals)`, checked: every listed symbol once, at most 16 bits,
+ /// and room left in the code space (the all-ones code is never assigned, Kraft sum < 1).
+ fn check_table(bits: &[u8; 16], vals: &[u8]) {
+ assert_eq!(bits.iter().map(|&b| b as usize).sum::(), vals.len());
+ let mut seen = [false; 256];
+ for &v in vals {
+ assert!(!seen[v as usize], "symbol {v} listed twice");
+ seen[v as usize] = true;
+ }
+ let kraft: f64 = bits.iter().enumerate().map(|(i, &n)| n as f64 / 2f64.powi(i as i32 + 1)).sum();
+ assert!(kraft < 1.0, "code space full: {kraft}");
+ let h = Huff::new(bits, vals);
+ for &v in vals {
+ let (c, l) = h.code[v as usize];
+ assert!((1..=16).contains(&l));
+ assert_ne!(c as u32, (1u32 << l) - 1, "symbol {v} has the all-ones code");
+ }
+ }
+
+ #[test]
+ fn optimal_tables_are_valid_and_bounded() {
+ // one symbol, two, a typical spread and every symbol
+ let mut one = [0u64; 256];
+ one[7] = 5;
+ let mut two = [0u64; 256];
+ two[0] = 1;
+ two[0xF0] = 1_000_000;
+ let typical: Freq = std::array::from_fn(|s| if s % 3 == 0 { (s as u64 * 7919) % 1000 + 1 } else { 0 });
+ let all: Freq = std::array::from_fn(|s| s as u64 + 1);
+ // Fibonacci counts make an unbounded Huffman tree ~40 levels deep: the 16-bit limit applies
+ let mut fib = [0u64; 256];
+ let (mut a, mut b) = (1u64, 1u64);
+ for f in fib.iter_mut().take(40) {
+ *f = a;
+ (a, b) = (b, a + b);
+ }
+ for (name, f) in [("one", one), ("two", two), ("typical", typical), ("all", all), ("fib", fib), ("none", [0; 256])] {
+ let (bits, vals) = optimal_table(&f);
+ check_table(&bits, &vals);
+ for s in 0..256 {
+ assert_eq!(vals.contains(&(s as u8)), f[s] > 0 || (name == "none" && s == 0), "{name}: symbol {s}");
+ }
+ }
+ // more frequent symbols never get longer codes
+ let (bits, vals) = optimal_table(&typical);
+ let h = Huff::new(&bits, &vals);
+ for &x in &vals {
+ for &y in &vals {
+ if typical[x as usize] > typical[y as usize] {
+ assert!(h.code[x as usize].1 <= h.code[y as usize].1, "{x} vs {y}");
+ }
+ }
+ }
+ // the Annex K example tables pass the same checks
+ check_table(&AC_LUMA_BITS, &AC_LUMA_VALS);
+ check_table(&DC_CHROMA_BITS, &DC_VALS);
+ }
+
+ /// Issue #1373: tables made for the image give a smaller file with exactly the same pixels as
+ /// the Annex K example tables (the quantized coefficients are the same), at every quality and
+ /// subsampling, and both decoders read it.
+ #[test]
+ fn optimized_tables_shrink_files_without_changing_pixels() {
+ let (w, h) = (517, 333);
+ // gradients plus deterministic noise, like a photo's grain
+ let mut seed = 0x2545_f491_u32;
+ let src: Vec = test_rgb(w, h)
+ .into_iter()
+ .map(|v| {
+ seed ^= seed << 13;
+ seed ^= seed >> 17;
+ seed ^= seed << 5;
+ v.saturating_add((seed % 23) as u8)
+ })
+ .collect();
+ for (q, sub) in [(100, ChromaSubsampling::S444), (92, ChromaSubsampling::S444), (75, ChromaSubsampling::S420), (30, ChromaSubsampling::S420)]
+ {
+ let std = encode_with(&src, w, h, 3, q, sub, &[], false);
+ let opt = encode(&src, w, h, 3, q, sub, &[]);
+ assert!(opt.len() < std.len(), "q{q} {sub:?}: {} ≥ {}", opt.len(), std.len());
+ let a = jpeg_decoder::Decoder::new(&std[..]).decode().expect("standard tables");
+ let b = jpeg_decoder::Decoder::new(&opt[..]).decode().expect("optimized tables");
+ assert_eq!(a, b, "q{q} {sub:?}: pixels differ");
+ let z = zune_jpeg::JpegDecoder::new(zune_core::bytestream::ZCursor::new(&opt[..])).decode().expect("zune-jpeg");
+ assert_eq!(z.len(), b.len());
+ }
+ // grayscale too
+ let gray: Vec = src.iter().step_by(3).copied().collect();
+ let std = encode_with(&gray, w, h, 1, 90, ChromaSubsampling::S444, &[], false);
+ let opt = encode(&gray, w, h, 1, 90, ChromaSubsampling::S444, &[]);
+ assert!(opt.len() < std.len());
+ assert_eq!(jpeg_decoder::Decoder::new(&std[..]).decode().unwrap(), jpeg_decoder::Decoder::new(&opt[..]).decode().unwrap());
+ }
+
#[test]
fn grayscale_and_quality_monotone() {
let (w, h) = (300, 200);
diff --git a/crates/codecs/src/jxl.rs b/crates/codecs/src/jxl.rs
index 26e144aed..adf59c0aa 100644
--- a/crates/codecs/src/jxl.rs
+++ b/crates/codecs/src/jxl.rs
@@ -8,7 +8,7 @@ use crate::convert::{Buf, Meta, Model, Raw, check_size, finish};
use crate::space::{NamedSpace, SourceSpace, SpaceOrigin, Trc};
use crate::{DecodeOptions, Decoded, Error, Format, Result};
use jxl_oxide::color::{ColourSpace, EnumColourEncoding, Primaries, RenderingIntent, TransferFunction, WhitePoint};
-use jxl_oxide::{AuxBoxData, JxlImage};
+use jxl_oxide::{AllocTracker, AuxBoxData, InitializeResult, JxlImage};
const F: Format = Format::Jxl;
@@ -16,10 +16,60 @@ fn err(e: impl std::fmt::Display) -> Error {
Error::Malformed(F, e.to_string())
}
+/// Tracked allocation budget per allowed pixel for frames, extra channels and modular trees.
+/// This bounds allocations covered by `AllocTracker`, not every allocation made by the decoder (#1517).
+const TRACKED_BYTES_PER_PIXEL: u64 = 4 * 4 * 8;
+
+/// The allocation budget never drops below this, so small images keep the decoder's fixed
+/// overhead (group buffers, entropy tables) whatever `max_pixels` is.
+const MIN_TRACKED_BYTES: u64 = 64 << 20;
+
+/// Bytes handed to the decoder at a time (the size `JxlImageBuilder::read` reads in).
+const INPUT_CHUNK: usize = 4096;
+
+/// Initializes from bounded input, checks dimensions before rendering, then loads remaining frames
+/// within a tracked allocation budget.
+fn open(bytes: &[u8], opts: &DecodeOptions) -> Result {
+ let budget = opts.max_pixels.saturating_mul(TRACKED_BYTES_PER_PIXEL).max(MIN_TRACKED_BYTES);
+ let tracker = AllocTracker::with_limit(usize::try_from(budget).unwrap_or(usize::MAX));
+ let mut uninit = JxlImage::builder().alloc_tracker(tracker).build_uninit();
+ // Initialization can load already-buffered frame data, so small feeds limit work before
+ // dimension checking.
+ let mut rest = bytes;
+ let mut window = INPUT_CHUNK;
+ let mut img = loop {
+ let consumed = uninit.feed_bytes(rest.get(..window).unwrap_or(rest)).map_err(err)?;
+ rest = rest.get(consumed..).unwrap_or_default();
+ match uninit.try_init().map_err(err)? {
+ InitializeResult::Initialized(img) => break img,
+ InitializeResult::NeedMoreData(more) => uninit = more,
+ }
+ if consumed == 0 {
+ // the parser wants more than the window holds at once
+ if window >= rest.len() {
+ return Err(err("truncated JPEG XL header"));
+ }
+ window = window.saturating_mul(2);
+ }
+ };
+ check_size(F, img.width() as u64, img.height() as u64, opts)?;
+ // Match `JxlImageBuilder::read`: bounded feeds until the final frame, not merely the first
+ // keyframe. `feed_bytes` copies codestream data into an untracked Vec, including any tail
+ // after the last frame; feeding the remaining file at once bypasses the allocation budget.
+ while !img.is_loading_done() && !rest.is_empty() {
+ let consumed = img.feed_bytes(rest.get(..INPUT_CHUNK).unwrap_or(rest)).map_err(err)?;
+ if consumed == 0 {
+ break;
+ }
+ rest = rest.get(consumed..).unwrap_or_default();
+ }
+ img.finalize().map_err(err)?;
+ Ok(img)
+}
+
pub(crate) fn decode(bytes: &[u8], opts: &DecodeOptions) -> Result {
- let mut img = JxlImage::builder().read(std::io::Cursor::new(bytes)).map_err(err)?;
+ let mut img = open(bytes, opts)?;
let (w, h) = (img.width(), img.height());
- check_size(F, w as u64, h as u64, opts)?;
let header = img.image_header();
let bits = header.metadata.bit_depth.bits_per_sample() as u8;
let float = matches!(header.metadata.bit_depth, jxl_oxide::image::BitDepth::FloatSample { .. });
diff --git a/crates/codecs/src/lib.rs b/crates/codecs/src/lib.rs
index e524cffe8..d14b1812f 100644
--- a/crates/codecs/src/lib.rs
+++ b/crates/codecs/src/lib.rs
@@ -12,6 +12,7 @@
//! - [`encode`] writes JPEG, PNG, TIFF, lossless WebP and, on native targets, AVIF (feature `avif`) and
//! JPEG XL ([`encode_jxl`], feature `jxl`), embedding
//! ICC/EXIF/XMP. [`icc::write_matrix_trc`] builds profiles for export.
+//! - [`video`] encodes AV1 video (native, feature `avif`) and writes it as MP4 or WebM.
//!
//! No decoder panics on malformed input (property-tested).
#![forbid(unsafe_code)]
@@ -31,11 +32,13 @@ mod jxl;
mod jxl_encode;
mod other;
mod png_codec;
+pub mod print;
pub mod proof;
mod psd;
mod sniff;
pub mod space;
mod tiff_codec;
+pub mod video;
mod webp;
pub use encode::{
diff --git a/crates/codecs/src/print.rs b/crates/codecs/src/print.rs
new file mode 100644
index 000000000..560e2710d
--- /dev/null
+++ b/crates/codecs/src/print.rs
@@ -0,0 +1,375 @@
+//! Print colour: a composed print page (16-bit RGB encoded with the BT.2020 / BT.709 curve, as the
+//! `Rec2020` output space renders it) to the samples a print-ready file carries, in the colour space
+//! of the chosen printer / paper profile.
+//!
+//! - **Target**: a printer / paper ICC profile (RGB, CMYK or grey; matrix or LUT based) converts the
+//! page to the device's own values. When the printer manages colour instead, the page is sent in a
+//! well-known RGB space (sRGB, Adobe RGB) and that space's profile travels with it.
+//! - **Rendering intent**: perceptual or relative colorimetric, as for soft proofing ([`ProofIntent`]).
+//! - **Black point compensation** (relative colorimetric only): the darkest colour the device holds is
+//! measured by a round trip of black through the profile, and the page's linear values are scaled
+//! so its black lands there instead of every shadow below it clipping to the same ink. Linear
+//! Rec.2020 is linear in XYZ, so with source black 0 and white 1 this is the published BPC mapping
+//! `Y' = Y·(1 − Yb) + Yb` for a neutral destination black. Perceptual tables carry their own black
+//! mapping and are left alone.
+//! - **Print adjustment**: brightness and contrast (−100..100) for prints that come out darker or
+//! flatter than the screen, on the sRGB-encoded values with black and white held. It is not shown
+//! on screen (the print is what it corrects).
+//!
+//! Everything before the profile conversion is per channel, so it folds into one 65,536-entry table
+//! indexed by the 16-bit sample; the conversion itself is moxcms's.
+//!
+//! Sources: ICC.1:2022 (rendering intents, profile classes); Adobe Systems, "Adobe Black Point
+//! Compensation" (2006, public white paper) for the black point mapping. Own design otherwise.
+
+use std::sync::Arc;
+
+use moxcms::{ColorProfile, DataColorSpace, Layout, RenderingIntent, TransformExecutor, TransformOptions};
+
+use crate::display::MAX_PROFILE_BYTES;
+use crate::proof::ProofIntent;
+use crate::space::{NamedSpace, Trc};
+
+/// Where a print's colours go.
+#[derive(Clone, Debug, PartialEq)]
+pub enum PrintTarget {
+ /// The printer manages colour: the page is sent in this RGB space, with its profile embedded.
+ Managed(NamedSpace),
+ /// A printer / paper ICC profile: the page is converted to the device's values.
+ Profile(Arc>),
+}
+
+/// How the page is rendered into the target.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct PrintColor {
+ pub intent: ProofIntent,
+ /// Map the page's black to the device's darkest (relative colorimetric only).
+ pub black_point_compensation: bool,
+ /// −100..100; 0 leaves the page as rendered.
+ pub brightness: f32,
+ /// −100..100; 0 leaves the page as rendered.
+ pub contrast: f32,
+}
+
+impl Default for PrintColor {
+ fn default() -> Self {
+ PrintColor { intent: ProofIntent::Perceptual, black_point_compensation: true, brightness: 0.0, contrast: 0.0 }
+ }
+}
+
+/// Pixels per conversion batch: bounds the temporary float buffers whatever the page size.
+const BATCH: usize = 1 << 16;
+
+/// The darkest black point compensation will lift the page's black to. A profile whose round trip
+/// says more is broken or not a print profile; leave the page alone rather than wash it out.
+const MAX_BLACK: f32 = 0.5;
+
+type Executor = Arc + Send + Sync>;
+
+/// A page conversion, built once per print job and applied to every page.
+pub struct PrintTransform {
+ forward: Executor,
+ layout: Layout,
+ channels: usize,
+ icc: Arc>,
+ /// 16-bit sample → linear Rec.2020 after the print adjustment and black point compensation.
+ pre: Vec,
+ black: f32,
+ description: Option,
+}
+
+impl std::fmt::Debug for PrintTransform {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("PrintTransform")
+ .field("channels", &self.channels)
+ .field("black", &self.black)
+ .field("description", &self.description)
+ .finish_non_exhaustive()
+ }
+}
+
+fn guarded(f: impl FnOnce() -> Option) -> Option {
+ std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).ok().flatten()
+}
+
+fn rendering_intent(intent: ProofIntent) -> RenderingIntent {
+ match intent {
+ ProofIntent::Perceptual => RenderingIntent::Perceptual,
+ ProofIntent::Relative => RenderingIntent::RelativeColorimetric,
+ }
+}
+
+/// A slider value as a finite fraction in −1..1 (NaN and infinities count as 0).
+fn amount(v: f32) -> f32 {
+ if v.is_finite() { (v / 100.0).clamp(-1.0, 1.0) } else { 0.0 }
+}
+
+/// The print adjustment on one sRGB-encoded value (0..1): brightness bends the mid-tones with a
+/// power curve, contrast steepens (or flattens) them with `e − k·sin(2πe)/2π`, which keeps black,
+/// mid grey and white where they are and stays monotonic for |k| < 1.
+pub fn print_adjust(e: f32, brightness: f32, contrast: f32) -> f32 {
+ let e = if e.is_finite() { e.clamp(0.0, 1.0) } else { 0.0 };
+ let (b, c) = (amount(brightness), amount(contrast));
+ let e = if b != 0.0 { e.powf(2f32.powf(-0.5 * b)) } else { e };
+ let k = 0.6 * c;
+ let tau = std::f32::consts::TAU;
+ (e - k * (tau * e).sin() / tau).clamp(0.0, 1.0)
+}
+
+impl PrintTransform {
+ /// Build the conversion for a target. Errors say what's wrong with the profile; nothing here
+ /// panics on a hostile one.
+ pub fn new(target: &PrintTarget, color: &PrintColor) -> Result {
+ let icc = match target {
+ PrintTarget::Managed(n) => Arc::new(crate::icc::write_named(*n)),
+ PrintTarget::Profile(bytes) => Arc::clone(bytes),
+ };
+ if icc.len() > MAX_PROFILE_BYTES {
+ return Err("the profile is too large".into());
+ }
+ let dev = guarded(|| ColorProfile::new_from_slice(&icc).ok()).ok_or("not a valid ICC profile")?;
+ let (layout, channels) = match dev.color_space {
+ DataColorSpace::Rgb => (Layout::Rgb, 3),
+ DataColorSpace::Cmyk => (Layout::Rgba, 4),
+ DataColorSpace::Gray => (Layout::Gray, 1),
+ _ => return Err("only RGB, CMYK and grey printer profiles can be printed to".into()),
+ };
+ let lin = crate::icc::linear_rec2020_profile();
+ let opts = |i| TransformOptions { rendering_intent: i, ..TransformOptions::default() };
+ let forward: Executor = guarded(|| lin.create_transform_f32(Layout::Rgb, &dev, layout, opts(rendering_intent(color.intent))).ok())
+ .ok_or("can't convert to this profile")?;
+
+ let black = if color.black_point_compensation && color.intent == ProofIntent::Relative {
+ let back: Executor = guarded(|| dev.create_transform_f32(layout, &lin, Layout::Rgb, opts(RenderingIntent::RelativeColorimetric)).ok())
+ .ok_or("can't convert from this profile")?;
+ device_black(&forward, &back, channels)
+ } else {
+ 0.0
+ };
+
+ let decode = Trc::Rec709.lut16();
+ let (b, c) = (color.brightness, color.contrast);
+ let adjust = amount(b) != 0.0 || amount(c) != 0.0;
+ let pre = decode
+ .iter()
+ .map(|&lin| {
+ let lin = if adjust {
+ let e = lightcraft_color::transfer::linear_to_srgb(lin.clamp(0.0, 1.0));
+ lightcraft_color::transfer::srgb_to_linear(print_adjust(e, b, c))
+ } else {
+ lin.clamp(0.0, 1.0)
+ };
+ lin * (1.0 - black) + black
+ })
+ .collect();
+ let description = crate::icc::parse(&icc).and_then(|i| i.description);
+ Ok(PrintTransform { forward, layout, channels, icc, pre, black, description })
+ }
+
+ /// Samples per pixel of the converted page: 3 (RGB), 4 (CMYK) or 1 (grey).
+ pub fn channels(&self) -> usize {
+ self.channels
+ }
+
+ /// The profile the converted samples are in: embed it with them.
+ pub fn icc(&self) -> &Arc> {
+ &self.icc
+ }
+
+ /// Whether the samples are CMYK (PDF readers show CMYK images inverted unless told otherwise,
+ /// so writers need to know).
+ pub fn is_cmyk(&self) -> bool {
+ self.layout == Layout::Rgba
+ }
+
+ /// The linear black black point compensation lifts the page's black to (0 when off).
+ pub fn black(&self) -> f32 {
+ self.black
+ }
+
+ /// The profile's description, for the job's settings and error messages.
+ pub fn description(&self) -> Option<&str> {
+ self.description.as_deref()
+ }
+
+ /// Convert a page: `rgb` is interleaved 16-bit RGB in the BT.2020 / BT.709 encoding; the result
+ /// is interleaved 16-bit device samples ([`PrintTransform::channels`] per pixel).
+ pub fn apply(&self, rgb: &[u16]) -> Result, String> {
+ if !rgb.len().is_multiple_of(3) {
+ return Err("the page isn't whole RGB pixels".into());
+ }
+ let pixels = rgb.len() / 3;
+ let ch = self.channels;
+ let out_len = pixels.checked_mul(ch).ok_or("the page is too large")?;
+ let mut out = vec![0u16; out_len];
+ let run = |src: &[u16], dst: &mut [u16]| -> Option<()> {
+ let lin: Vec = src.iter().map(|&v| self.pre.get(usize::from(v)).copied().unwrap_or(0.0)).collect();
+ let mut dev = vec![0f32; dst.len()];
+ self.forward.transform(&lin, &mut dev).ok()?;
+ for (d, v) in dst.iter_mut().zip(dev) {
+ let v = if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 };
+ *d = (v * 65535.0 + 0.5) as u16;
+ }
+ Some(())
+ };
+ let src = rgb.chunks(BATCH * 3);
+ let dst = out.chunks_mut(BATCH * ch);
+ #[cfg(feature = "parallel")]
+ let ok = {
+ use rayon::prelude::*;
+ let pairs: Vec<_> = src.zip(dst).collect();
+ pairs.into_par_iter().all(|(s, d)| guarded(|| run(s, d)).is_some())
+ };
+ #[cfg(not(feature = "parallel"))]
+ let ok = src.zip(dst).all(|(s, d)| guarded(|| run(s, d)).is_some());
+ if ok { Ok(out) } else { Err("the profile's tables can't be applied".into()) }
+ }
+}
+
+/// The darkest the device holds, as linear Rec.2020 luminance: black there and back, relative
+/// colorimetric both ways (for a device that reaches true black, 0).
+fn device_black(forward: &Executor, back: &Executor, channels: usize) -> f32 {
+ let mut device = vec![0f32; channels];
+ let mut lin = [0f32; 3];
+ let ok = guarded(|| {
+ forward.transform(&[0.0; 3], &mut device).ok()?;
+ back.transform(&device, &mut lin).ok()
+ });
+ if ok.is_none() {
+ return 0.0;
+ }
+ // BT.2020 luminance weights
+ let y = 0.2627 * lin[0] + 0.6780 * lin[1] + 0.0593 * lin[2];
+ if y.is_finite() { y.clamp(0.0, MAX_BLACK) } else { 0.0 }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use moxcms::ToneReprCurve;
+
+ /// A page of one 16-bit pixel per linear grey level.
+ fn greys(levels: &[f32]) -> Vec {
+ levels.iter().flat_map(|&l| [(Trc::Rec709.from_linear(l) * 65535.0).round() as u16; 3]).collect()
+ }
+
+ fn convert(target: &PrintTarget, color: PrintColor, levels: &[f32]) -> Vec {
+ PrintTransform::new(target, &color).unwrap().apply(&greys(levels)).unwrap()
+ }
+
+ /// An RGB print profile whose ink can't get darker than linear 0.03: device 0 is 3 % grey.
+ fn raised_black_profile() -> Arc> {
+ let mut p = ColorProfile::new_from_slice(&crate::icc::write_named(NamedSpace::Srgb)).unwrap();
+ let (g, e) = (2.2f32, 0.03f32);
+ let a = (1.0 - e).powf(1.0 / g);
+ // ICC parametric type 4: Y = (aX + b)^g + e for X >= d, Y = cX + f below
+ let trc = ToneReprCurve::Parametric(vec![g, a, 0.0, 0.0, 0.0, e, e]);
+ p.red_trc = Some(trc.clone());
+ p.green_trc = Some(trc.clone());
+ p.blue_trc = Some(trc);
+ Arc::new(p.encode().unwrap())
+ }
+
+ #[test]
+ fn managed_srgb_keeps_black_grey_and_white() {
+ let t = PrintTransform::new(&PrintTarget::Managed(NamedSpace::Srgb), &PrintColor::default()).unwrap();
+ assert_eq!(t.channels(), 3);
+ assert_eq!(crate::icc::parse(t.icc()).and_then(|i| i.named), Some(NamedSpace::Srgb));
+ let out = t.apply(&greys(&[0.0, 0.18, 1.0])).unwrap();
+ let mid = (lightcraft_color::transfer::linear_to_srgb(0.18) * 65535.0) as i32;
+ assert!(out[..3].iter().all(|&v| v < 64), "{out:?}");
+ assert!(out[3..6].iter().all(|&v| (i32::from(v) - mid).abs() < 300), "{out:?} vs {mid}");
+ assert!(out[6..].iter().all(|&v| v > 65400), "{out:?}");
+ }
+
+ #[test]
+ fn adobe_rgb_target_embeds_its_profile() {
+ let t = PrintTransform::new(&PrintTarget::Managed(NamedSpace::AdobeRgb), &PrintColor::default()).unwrap();
+ assert_eq!(crate::icc::parse(t.icc()).and_then(|i| i.named), Some(NamedSpace::AdobeRgb));
+ // a saturated Rec.2020 green is outside Adobe RGB: it comes out at the space's edge, not wrapped
+ let px = [0u16, 65535, 0];
+ let out = t.apply(&px).unwrap();
+ assert!(out[1] > 60000 && out[0] < 8000 && out[2] < 8000, "{out:?}");
+ }
+
+ #[test]
+ fn brightness_lifts_the_mid_tones_and_holds_the_ends() {
+ let target = PrintTarget::Managed(NamedSpace::Srgb);
+ let levels = [0.0, 0.18, 1.0];
+ let plain = convert(&target, PrintColor::default(), &levels);
+ let bright = convert(&target, PrintColor { brightness: 50.0, ..PrintColor::default() }, &levels);
+ let dark = convert(&target, PrintColor { brightness: -50.0, ..PrintColor::default() }, &levels);
+ assert!(bright[3] > plain[3] + 2000 && dark[3] + 2000 < plain[3], "{dark:?} {plain:?} {bright:?}");
+ for v in [&plain, &bright, &dark] {
+ assert!(v[0] < 64 && v[6] > 65400, "{v:?}");
+ }
+ }
+
+ #[test]
+ fn contrast_spreads_shadows_and_highlights_and_stays_monotonic() {
+ let target = PrintTarget::Managed(NamedSpace::Srgb);
+ let levels = [0.03, 0.6];
+ let plain = convert(&target, PrintColor::default(), &levels);
+ let more = convert(&target, PrintColor { contrast: 60.0, ..PrintColor::default() }, &levels);
+ assert!(more[0] < plain[0] && more[3] > plain[3], "{plain:?} {more:?}");
+ for c in [-100.0, 100.0] {
+ for b in [-100.0, 100.0] {
+ let ramp: Vec = (0..=64).map(|i| i as f32 / 64.0).map(|e| print_adjust(e, b, c)).collect();
+ assert!(ramp.windows(2).all(|w| w[1] >= w[0]), "b {b} c {c}: {ramp:?}");
+ assert!(ramp[0] == 0.0 && (ramp[64] - 1.0).abs() < 1e-6);
+ }
+ }
+ }
+
+ #[test]
+ fn black_point_compensation_keeps_shadow_detail_above_the_inks_black() {
+ let target = PrintTarget::Profile(raised_black_profile());
+ let shadows = [0.0, 0.006, 0.012, 0.018, 0.024, 0.5, 1.0];
+ let relative = PrintColor { intent: ProofIntent::Relative, black_point_compensation: false, ..PrintColor::default() };
+ let clipped = convert(&target, relative, &shadows);
+ let kept = convert(&target, PrintColor { black_point_compensation: true, ..relative }, &shadows);
+ let reds = |v: &[u16]| v.chunks(3).map(|p| p[0]).collect::>();
+ let (clipped, kept) = (reds(&clipped), reds(&kept));
+ // everything below the ink's black prints as (within rounding) the same device value without
+ // compensation …
+ assert!(clipped[..5].iter().all(|&v| v <= clipped[0] + 16), "{clipped:?}");
+ // … and as distinct, rising values with it; white still prints as white
+ assert!(kept[..5].windows(2).all(|w| w[1] > w[0]) && kept[4] > kept[0] + 1000, "{kept:?}");
+ assert!(kept[6] > 65400 && clipped[6] > 65400, "{kept:?} {clipped:?}");
+ let t = PrintTransform::new(&target, &PrintColor { black_point_compensation: true, ..relative }).unwrap();
+ assert!((t.black() - 0.03).abs() < 0.01, "{}", t.black());
+ }
+
+ #[test]
+ fn black_point_compensation_is_relative_colorimetric_only() {
+ let target = PrintTarget::Profile(raised_black_profile());
+ let levels = [0.0, 0.01, 0.2];
+ let with = convert(&target, PrintColor { intent: ProofIntent::Perceptual, black_point_compensation: true, ..PrintColor::default() }, &levels);
+ let without =
+ convert(&target, PrintColor { intent: ProofIntent::Perceptual, black_point_compensation: false, ..PrintColor::default() }, &levels);
+ assert_eq!(with, without);
+ let t = PrintTransform::new(&target, &PrintColor { intent: ProofIntent::Perceptual, ..PrintColor::default() }).unwrap();
+ assert_eq!(t.black(), 0.0);
+ }
+
+ #[test]
+ fn hostile_input_is_an_error_not_a_panic() {
+ let color = PrintColor::default();
+ assert!(PrintTransform::new(&PrintTarget::Profile(Arc::new(vec![])), &color).is_err());
+ assert!(PrintTransform::new(&PrintTarget::Profile(Arc::new(vec![0u8; 400])), &color).is_err());
+ assert!(PrintTransform::new(&PrintTarget::Profile(Arc::new(vec![0u8; MAX_PROFILE_BYTES + 1])), &color).is_err());
+ let good = crate::icc::write_named(NamedSpace::DisplayP3);
+ for i in (0..good.len()).step_by(7) {
+ let mut b = good.clone();
+ b[i] ^= 0x5a;
+ if let Ok(t) = PrintTransform::new(&PrintTarget::Profile(Arc::new(b)), &PrintColor { intent: ProofIntent::Relative, ..color }) {
+ let _ = t.apply(&greys(&[0.0, 0.5, 1.0]));
+ }
+ }
+ let t = PrintTransform::new(&PrintTarget::Managed(NamedSpace::Srgb), &PrintColor { brightness: f32::NAN, contrast: f32::INFINITY, ..color })
+ .unwrap();
+ assert!(t.apply(&[1, 2]).is_err(), "half a pixel");
+ assert_eq!(t.apply(&greys(&[0.18])).unwrap(), convert(&PrintTarget::Managed(NamedSpace::Srgb), color, &[0.18]));
+ assert!(print_adjust(f32::NAN, 50.0, 50.0).is_finite());
+ }
+}
diff --git a/crates/codecs/src/tiff_codec.rs b/crates/codecs/src/tiff_codec.rs
index b077c214a..1bbfa8e6c 100644
--- a/crates/codecs/src/tiff_codec.rs
+++ b/crates/codecs/src/tiff_codec.rs
@@ -95,6 +95,11 @@ pub(crate) fn decode(bytes: &[u8], opts: &DecodeOptions) -> Result {
fn limits(opts: &DecodeOptions) -> Limits {
let mut limits = Limits::default();
limits.decoding_buffer_size = opts.max_pixels.saturating_mul(4 * 8).min(usize::MAX as u64) as usize;
+ // A strip or tile whose declared byte count exceeds this is refused: the default 128 MiB refuses a
+ // single-strip uncompressed 16-bit export of ~22 MP (#1983). Raised to the decoded image's cap
+ // (32 bytes per allowed pixel), never lowered: a strip or tile can outgrow the pixels it shows
+ // (JPEG headers, padding to the tile size).
+ limits.intermediate_buffer_size = limits.intermediate_buffer_size.max(limits.decoding_buffer_size);
limits.ifd_value_size = 32 << 20;
limits
}
diff --git a/crates/codecs/src/video.rs b/crates/codecs/src/video.rs
new file mode 100644
index 000000000..3991269d8
--- /dev/null
+++ b/crates/codecs/src/video.rs
@@ -0,0 +1,1158 @@
+//! Video: AV1 encoding of RGBA frames (`rav1e`, native targets with the `avif` feature) and two
+//! small muxers of our own, written from the public specifications only:
+//!
+//! - **MP4** (ISO/IEC 14496-12 ISO base media file format + the AOM "AV1 Codec ISO Media File
+//! Format Binding"): `ftyp`, one `mdat` (64-bit size, patched at the end) holding the samples,
+//! then `moov` with one video track whose sample entry is `av01` + `av1C` + `colr` (`nclx`).
+//! - **WebM** (Matroska / EBML as restricted by WebM, codec mapping `V_AV1`): EBML header,
+//! Segment (size patched at the end) with SeekHead, Info, Tracks, Clusters of SimpleBlocks (the
+//! last frame a BlockGroup with its duration) and Cues.
+//!
+//! Frames may last several frame periods: a picture held on screen is one sample with a long
+//! duration (variable frame rate), so a still slide costs one encoded frame however long it
+//! stays up. Durations are counted in frame periods of the nominal rate.
+//!
+//! Samples are AV1 temporal units without temporal delimiter OBUs (both bindings ask for that);
+//! the configuration record carries the sequence header OBU. No audio track.
+
+use std::io::{Seek, SeekFrom, Write};
+
+use crate::{Error, Result};
+
+/// The container a video is written in.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Container {
+ Mp4,
+ WebM,
+}
+
+impl Container {
+ pub fn id(self) -> &'static str {
+ match self {
+ Container::Mp4 => "mp4",
+ Container::WebM => "webm",
+ }
+ }
+
+ pub fn parse(s: &str) -> Option {
+ match s.trim().to_ascii_lowercase().as_str() {
+ "mp4" => Some(Container::Mp4),
+ "webm" => Some(Container::WebM),
+ _ => None,
+ }
+ }
+}
+
+/// One encoded picture: an AV1 temporal unit (no temporal delimiter), whether it is a key frame
+/// (a sync sample), and how many frame periods it stays on screen (≥ 1).
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Sample {
+ pub data: Vec,
+ pub key: bool,
+ pub duration: u64,
+}
+
+/// The longest video the muxers write, in frame periods (keeps every timestamp in range).
+pub const MAX_PERIODS: u64 = 60 * 60 * 24 * 120;
+
+fn io(e: std::io::Error) -> Error {
+ Error::Encode(format!("video: {e}"))
+}
+
+fn bad(msg: impl Into) -> Error {
+ Error::Encode(format!("video: {}", msg.into()))
+}
+
+// ---------------------------------------------------------------- AV1 OBUs
+
+/// OBU types (AV1 specification §6.2.2).
+pub const OBU_SEQUENCE_HEADER: u8 = 1;
+pub const OBU_TEMPORAL_DELIMITER: u8 = 2;
+pub const OBU_PADDING: u8 = 15;
+
+/// Read a LEB128 value at `pos` (at most 8 bytes); returns (value, bytes read).
+fn leb128(data: &[u8], pos: usize) -> Result<(u64, usize)> {
+ let mut v = 0u64;
+ for i in 0..8 {
+ let b = *data.get(pos + i).ok_or_else(|| bad("truncated OBU size"))?;
+ v |= u64::from(b & 0x7f) << (7 * i);
+ if b & 0x80 == 0 {
+ return Ok((v, i + 1));
+ }
+ }
+ Err(bad("OBU size longer than 8 bytes"))
+}
+
+/// The OBUs of a temporal unit: (type, the whole OBU's byte range). Every OBU but the last must
+/// carry its size (as the low-overhead bitstream format requires).
+pub fn obus(data: &[u8]) -> Result)>> {
+ let mut out = Vec::new();
+ let mut pos = 0usize;
+ while let Some(&header) = data.get(pos) {
+ if header & 0x80 != 0 {
+ return Err(bad("OBU forbidden bit set"));
+ }
+ let kind = (header >> 3) & 0x0f;
+ let ext = header & 0x04 != 0;
+ let has_size = header & 0x02 != 0;
+ let mut p = pos + 1 + usize::from(ext);
+ let end = if has_size {
+ let (size, n) = leb128(data, p)?;
+ p += n;
+ let size = usize::try_from(size).map_err(|_| bad("OBU too large"))?;
+ p.checked_add(size).filter(|e| *e <= data.len()).ok_or_else(|| bad("OBU runs past the end of the packet"))?
+ } else {
+ data.len()
+ };
+ if p > end {
+ return Err(bad("truncated OBU header"));
+ }
+ out.push((kind, pos..end));
+ pos = end;
+ }
+ Ok(out)
+}
+
+/// A temporal unit as a sample: temporal delimiters and padding dropped. Also returns its
+/// sequence header OBU, if it has one.
+pub fn to_sample_data(tu: &[u8]) -> Result<(Vec, Option>)> {
+ let mut data = Vec::with_capacity(tu.len());
+ let mut seq = None;
+ for (kind, r) in obus(tu)? {
+ let Some(bytes) = tu.get(r) else { continue };
+ match kind {
+ OBU_TEMPORAL_DELIMITER | OBU_PADDING => {}
+ _ => {
+ if kind == OBU_SEQUENCE_HEADER && seq.is_none() {
+ seq = Some(bytes.to_vec());
+ }
+ data.extend_from_slice(bytes);
+ }
+ }
+ }
+ Ok((data, seq))
+}
+
+/// The `av1C` configuration record (AV1-ISOBMFF §2.3) for 8-bit 4:2:0 main profile, followed by
+/// `seq_header` (the sequence header OBU, may be empty). Matroska's `CodecPrivate` is the same.
+pub fn av1_config(seq_header: &[u8]) -> Vec {
+ // marker 1, version 1 | profile 0, level 31 (unconstrained) | tier 0, 8-bit, not mono,
+ // subsampling x and y, chroma position 0 | no initial presentation delay
+ let mut v = vec![0x81, 31, 0b0000_1100, 0];
+ v.extend_from_slice(seq_header);
+ v
+}
+
+// ---------------------------------------------------------------- MP4
+
+fn mp4_box(kind: &[u8; 4], body: &[u8]) -> Result> {
+ let size = u32::try_from(body.len().checked_add(8).ok_or_else(|| bad("box too large"))?).map_err(|_| bad("box too large"))?;
+ let mut v = Vec::with_capacity(body.len() + 8);
+ v.extend_from_slice(&size.to_be_bytes());
+ v.extend_from_slice(kind);
+ v.extend_from_slice(body);
+ Ok(v)
+}
+
+fn full_box(kind: &[u8; 4], version: u8, flags: u32, body: &[u8]) -> Result> {
+ let mut b = Vec::with_capacity(body.len() + 4);
+ b.push(version);
+ b.extend_from_slice(&flags.to_be_bytes()[1..]);
+ b.extend_from_slice(body);
+ mp4_box(kind, &b)
+}
+
+/// The unity transformation matrix of `mvhd` / `tkhd`.
+fn matrix() -> Vec {
+ let mut m = Vec::with_capacity(36);
+ for v in [0x0001_0000u32, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000] {
+ m.extend_from_slice(&v.to_be_bytes());
+ }
+ m
+}
+
+/// An MP4 writer: [`Mp4Writer::add`] streams each sample into `mdat`; [`Mp4Writer::finish`]
+/// patches its size and appends `moov`.
+pub struct Mp4Writer {
+ w: W,
+ width: u16,
+ height: u16,
+ fps: u32,
+ /// Where `mdat`'s 64-bit size is, and where its data starts.
+ mdat_size_at: u64,
+ data_start: u64,
+ written: u64,
+ sizes: Vec,
+ durations: Vec,
+ keys: Vec,
+ config: Option>,
+}
+
+/// Track ticks per frame period (ffmpeg's habit: 512 per frame).
+const TICKS: u64 = 512;
+
+impl Mp4Writer {
+ pub fn new(mut w: W, width: u32, height: u32, fps: u32) -> Result {
+ let (width, height) = (u16::try_from(width).map_err(|_| bad("too wide"))?, u16::try_from(height).map_err(|_| bad("too tall"))?);
+ if width == 0 || height == 0 || !(1..=240).contains(&fps) {
+ return Err(bad("bad size or frame rate"));
+ }
+ let mut ftyp = Vec::new();
+ ftyp.extend_from_slice(b"isom");
+ ftyp.extend_from_slice(&0x200u32.to_be_bytes());
+ for brand in [b"isom", b"iso6", b"av01", b"mp41"] {
+ ftyp.extend_from_slice(brand);
+ }
+ w.write_all(&mp4_box(b"ftyp", &ftyp)?).map_err(io)?;
+ let start = w.stream_position().map_err(io)?;
+ // size 1 = a 64-bit size follows (patched in finish)
+ w.write_all(&1u32.to_be_bytes()).map_err(io)?;
+ w.write_all(b"mdat").map_err(io)?;
+ w.write_all(&0u64.to_be_bytes()).map_err(io)?;
+ Ok(Mp4Writer {
+ w,
+ width,
+ height,
+ fps,
+ mdat_size_at: start + 8,
+ data_start: start + 16,
+ written: 0,
+ sizes: Vec::new(),
+ durations: Vec::new(),
+ keys: Vec::new(),
+ config: None,
+ })
+ }
+
+ /// The `av1C` record ([`av1_config`]); needed before [`Self::finish`].
+ pub fn set_config(&mut self, config: Vec) {
+ self.config = Some(config);
+ }
+
+ pub fn add(&mut self, s: &Sample) -> Result<()> {
+ let size = u32::try_from(s.data.len()).map_err(|_| bad("sample too large"))?;
+ if s.duration == 0 {
+ return Err(bad("a sample lasts at least one frame"));
+ }
+ if self.sizes.is_empty() && !s.key {
+ return Err(bad("the first sample must be a key frame"));
+ }
+ self.w.write_all(&s.data).map_err(io)?;
+ self.written = self.written.saturating_add(u64::from(size));
+ self.sizes.push(size);
+ self.durations.push(s.duration);
+ if s.key {
+ let n = u32::try_from(self.sizes.len()).map_err(|_| bad("too many samples"))?;
+ self.keys.push(n);
+ }
+ Ok(())
+ }
+
+ /// Frame periods written so far.
+ pub fn periods(&self) -> u64 {
+ self.durations.iter().sum()
+ }
+
+ pub fn finish(mut self) -> Result {
+ if self.sizes.is_empty() {
+ return Err(bad("no frames"));
+ }
+ let config = self.config.take().ok_or_else(|| bad("no AV1 configuration"))?;
+ let periods = self.periods();
+ if periods > MAX_PERIODS {
+ return Err(bad("video too long"));
+ }
+ let end = self.w.stream_position().map_err(io)?;
+ self.w.seek(SeekFrom::Start(self.mdat_size_at)).map_err(io)?;
+ self.w.write_all(&(end - (self.mdat_size_at - 8)).to_be_bytes()).map_err(io)?;
+ self.w.seek(SeekFrom::Start(end)).map_err(io)?;
+ let moov = self.moov(&config, periods)?;
+ self.w.write_all(&moov).map_err(io)?;
+ self.w.flush().map_err(io)?;
+ Ok(self.w)
+ }
+
+ fn moov(&self, config: &[u8], periods: u64) -> Result> {
+ let n = u32::try_from(self.sizes.len()).map_err(|_| bad("too many samples"))?;
+ let timescale = u32::try_from(u64::from(self.fps) * TICKS).map_err(|_| bad("bad frame rate"))?;
+ let ticks = periods.checked_mul(TICKS).ok_or_else(|| bad("video too long"))?;
+ // the movie's own timescale: milliseconds
+ let movie_ms = periods.saturating_mul(1000) / u64::from(self.fps);
+ let u32_of = |v: u64| u32::try_from(v).map_err(|_| bad("video too long"));
+ let (track_ticks, movie_ms) = (u32_of(ticks)?, u32_of(movie_ms)?);
+
+ let mut mvhd = Vec::new();
+ mvhd.extend_from_slice(&[0; 8]); // creation, modification
+ mvhd.extend_from_slice(&1000u32.to_be_bytes());
+ mvhd.extend_from_slice(&movie_ms.to_be_bytes());
+ mvhd.extend_from_slice(&0x0001_0000u32.to_be_bytes()); // rate 1.0
+ mvhd.extend_from_slice(&0x0100u16.to_be_bytes()); // volume 1.0
+ mvhd.extend_from_slice(&[0; 10]);
+ mvhd.extend_from_slice(&matrix());
+ mvhd.extend_from_slice(&[0; 24]); // pre_defined
+ mvhd.extend_from_slice(&2u32.to_be_bytes()); // next track id
+
+ let mut tkhd = Vec::new();
+ tkhd.extend_from_slice(&[0; 8]);
+ tkhd.extend_from_slice(&1u32.to_be_bytes()); // track id
+ tkhd.extend_from_slice(&[0; 4]);
+ tkhd.extend_from_slice(&movie_ms.to_be_bytes());
+ tkhd.extend_from_slice(&[0; 8]);
+ tkhd.extend_from_slice(&[0; 2]); // layer
+ tkhd.extend_from_slice(&[0; 2]); // alternate group
+ tkhd.extend_from_slice(&[0; 2]); // volume (video: 0)
+ tkhd.extend_from_slice(&[0; 2]);
+ tkhd.extend_from_slice(&matrix());
+ tkhd.extend_from_slice(&(u32::from(self.width) << 16).to_be_bytes());
+ tkhd.extend_from_slice(&(u32::from(self.height) << 16).to_be_bytes());
+
+ let mut mdhd = Vec::new();
+ mdhd.extend_from_slice(&[0; 8]);
+ mdhd.extend_from_slice(×cale.to_be_bytes());
+ mdhd.extend_from_slice(&track_ticks.to_be_bytes());
+ mdhd.extend_from_slice(&0x55c4u16.to_be_bytes()); // language "und"
+ mdhd.extend_from_slice(&[0; 2]);
+
+ let mut hdlr = Vec::new();
+ hdlr.extend_from_slice(&[0; 4]);
+ hdlr.extend_from_slice(b"vide");
+ hdlr.extend_from_slice(&[0; 12]);
+ hdlr.extend_from_slice(b"LightCraft video\0");
+
+ let mut vmhd = Vec::new();
+ vmhd.extend_from_slice(&[0; 8]); // graphics mode, opcolor
+ let url = full_box(b"url ", 0, 1, &[])?;
+ let mut dref = 1u32.to_be_bytes().to_vec();
+ dref.extend_from_slice(&url);
+ let dinf = mp4_box(b"dinf", &full_box(b"dref", 0, 0, &dref)?)?;
+
+ // av01 visual sample entry
+ let mut av01 = Vec::new();
+ av01.extend_from_slice(&[0; 6]);
+ av01.extend_from_slice(&1u16.to_be_bytes()); // data reference index
+ av01.extend_from_slice(&[0; 16]); // pre_defined, reserved
+ av01.extend_from_slice(&self.width.to_be_bytes());
+ av01.extend_from_slice(&self.height.to_be_bytes());
+ av01.extend_from_slice(&0x0048_0000u32.to_be_bytes()); // 72 dpi
+ av01.extend_from_slice(&0x0048_0000u32.to_be_bytes());
+ av01.extend_from_slice(&[0; 4]);
+ av01.extend_from_slice(&1u16.to_be_bytes()); // frame count
+ let mut name = [0u8; 32];
+ let label = b"AOM Coding";
+ name[0] = label.len() as u8;
+ name[1..=label.len()].copy_from_slice(label);
+ av01.extend_from_slice(&name);
+ av01.extend_from_slice(&0x0018u16.to_be_bytes()); // depth
+ av01.extend_from_slice(&(-1i16).to_be_bytes());
+ av01.extend_from_slice(&mp4_box(b"av1C", config)?);
+ // BT.709 primaries, transfer and matrix, limited range
+ let mut colr = b"nclx".to_vec();
+ colr.extend_from_slice(&[0, 1, 0, 1, 0, 1, 0]);
+ av01.extend_from_slice(&mp4_box(b"colr", &colr)?);
+ let mut pasp = 1u32.to_be_bytes().to_vec();
+ pasp.extend_from_slice(&1u32.to_be_bytes());
+ av01.extend_from_slice(&mp4_box(b"pasp", &pasp)?);
+ let mut stsd = 1u32.to_be_bytes().to_vec();
+ stsd.extend_from_slice(&mp4_box(b"av01", &av01)?);
+
+ // decoding times: runs of equal durations
+ let mut runs: Vec<(u32, u64)> = Vec::new();
+ for d in &self.durations {
+ match runs.last_mut() {
+ Some((count, delta)) if *delta == *d => *count += 1,
+ _ => runs.push((1, *d)),
+ }
+ }
+ let mut stts = u32::try_from(runs.len()).map_err(|_| bad("too many samples"))?.to_be_bytes().to_vec();
+ for (count, delta) in runs {
+ stts.extend_from_slice(&count.to_be_bytes());
+ stts.extend_from_slice(&u32_of(delta.saturating_mul(TICKS))?.to_be_bytes());
+ }
+ let mut stsc = 1u32.to_be_bytes().to_vec();
+ for v in [1u32, n, 1] {
+ stsc.extend_from_slice(&v.to_be_bytes());
+ }
+ let mut stsz = 0u32.to_be_bytes().to_vec();
+ stsz.extend_from_slice(&n.to_be_bytes());
+ for s in &self.sizes {
+ stsz.extend_from_slice(&s.to_be_bytes());
+ }
+ // one chunk holding every sample, at the start of mdat's data
+ let chunk_offset = match u32::try_from(self.data_start) {
+ Ok(o) => full_box(b"stco", 0, 0, &[1u32.to_be_bytes(), o.to_be_bytes()].concat())?,
+ Err(_) => {
+ let mut b = 1u32.to_be_bytes().to_vec();
+ b.extend_from_slice(&self.data_start.to_be_bytes());
+ full_box(b"co64", 0, 0, &b)?
+ }
+ };
+ let mut stbl = Vec::new();
+ stbl.extend_from_slice(&full_box(b"stsd", 0, 0, &stsd)?);
+ stbl.extend_from_slice(&full_box(b"stts", 0, 0, &stts)?);
+ if self.keys.len() < self.sizes.len() {
+ let mut stss = u32::try_from(self.keys.len()).map_err(|_| bad("too many samples"))?.to_be_bytes().to_vec();
+ for k in &self.keys {
+ stss.extend_from_slice(&k.to_be_bytes());
+ }
+ stbl.extend_from_slice(&full_box(b"stss", 0, 0, &stss)?);
+ }
+ stbl.extend_from_slice(&full_box(b"stsc", 0, 0, &stsc)?);
+ stbl.extend_from_slice(&full_box(b"stsz", 0, 0, &stsz)?);
+ stbl.extend_from_slice(&chunk_offset);
+
+ let mut minf = full_box(b"vmhd", 0, 1, &vmhd)?;
+ minf.extend_from_slice(&dinf);
+ minf.extend_from_slice(&mp4_box(b"stbl", &stbl)?);
+ let mut mdia = full_box(b"mdhd", 0, 0, &mdhd)?;
+ mdia.extend_from_slice(&full_box(b"hdlr", 0, 0, &hdlr)?);
+ mdia.extend_from_slice(&mp4_box(b"minf", &minf)?);
+ let mut trak = full_box(b"tkhd", 0, 3, &tkhd)?;
+ trak.extend_from_slice(&mp4_box(b"mdia", &mdia)?);
+ let mut moov = full_box(b"mvhd", 0, 0, &mvhd)?;
+ moov.extend_from_slice(&mp4_box(b"trak", &trak)?);
+ mp4_box(b"moov", &moov)
+ }
+}
+
+// ---------------------------------------------------------------- WebM
+
+/// An EBML element id with its length marker, as written (e.g. `0x1A45DFA3`).
+fn ebml_id(id: u32) -> Vec {
+ let b = id.to_be_bytes();
+ let skip = b.iter().take_while(|x| **x == 0).count().min(3);
+ b[skip..].to_vec()
+}
+
+/// An EBML size, in the fewest bytes (all-ones values are reserved for "unknown").
+fn ebml_size(n: u64) -> Vec {
+ for len in 1..=8u32 {
+ if n < (1u64 << (7 * len)) - 1 {
+ let mut v = Vec::with_capacity(len as usize);
+ let marked = n | (1u64 << (7 * len));
+ let bytes = marked.to_be_bytes();
+ v.extend_from_slice(&bytes[(8 - len as usize)..]);
+ return v;
+ }
+ }
+ vec![0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe]
+}
+
+/// An 8-byte EBML size (patchable in place).
+fn ebml_size8(n: u64) -> [u8; 8] {
+ let mut b = (n & 0x00ff_ffff_ffff_ffff).to_be_bytes();
+ b[0] = 0x01;
+ b
+}
+
+fn el(id: u32, body: &[u8]) -> Vec {
+ let mut v = ebml_id(id);
+ v.extend_from_slice(&ebml_size(body.len() as u64));
+ v.extend_from_slice(body);
+ v
+}
+
+fn el_uint(id: u32, n: u64) -> Vec {
+ let b = n.to_be_bytes();
+ let skip = b.iter().take_while(|x| **x == 0).count().min(7);
+ el(id, &b[skip..])
+}
+
+fn el_float(id: u32, f: f64) -> Vec {
+ el(id, &f.to_be_bytes())
+}
+
+const EBML: u32 = 0x1A45_DFA3;
+const SEGMENT: u32 = 0x1853_8067;
+const SEEK_HEAD: u32 = 0x114D_9B74;
+const SEEK: u32 = 0x4DBB;
+const SEEK_ID: u32 = 0x53AB;
+const SEEK_POSITION: u32 = 0x53AC;
+const VOID: u32 = 0xEC;
+const INFO: u32 = 0x1549_A966;
+const TIMESTAMP_SCALE: u32 = 0x2A_D7B1;
+const DURATION: u32 = 0x4489;
+const TRACKS: u32 = 0x1654_AE6B;
+const TRACK_ENTRY: u32 = 0xAE;
+const CLUSTER: u32 = 0x1F43_B675;
+const TIMESTAMP: u32 = 0xE7;
+const SIMPLE_BLOCK: u32 = 0xA3;
+const BLOCK_GROUP: u32 = 0xA0;
+const BLOCK: u32 = 0xA1;
+const BLOCK_DURATION: u32 = 0x9B;
+const REFERENCE_BLOCK: u32 = 0xFB;
+const CUES: u32 = 0x1C53_BB6B;
+
+/// Room kept after the Segment header for the SeekHead written at the end.
+const SEEK_ROOM: usize = 96;
+
+/// A WebM writer (see the module docs). Timestamps are in milliseconds.
+pub struct WebmWriter {
+ w: W,
+ width: u32,
+ height: u32,
+ fps: u32,
+ segment_size_at: u64,
+ segment_start: u64,
+ duration_at: u64,
+ info_at: u64,
+ tracks_at: Option,
+ config: Option>,
+ /// Frame periods before the pending sample.
+ periods: u64,
+ /// The sample not written yet (the last one becomes a BlockGroup with its duration).
+ pending: Option<(Sample, u64)>,
+ cluster: Vec,
+ cluster_ts: u64,
+ last_block_ts: u64,
+ cues: Vec<(u64, u64)>,
+}
+
+impl WebmWriter {
+ pub fn new(mut w: W, width: u32, height: u32, fps: u32) -> Result {
+ if width == 0 || height == 0 || width > 65_535 || height > 65_535 || !(1..=240).contains(&fps) {
+ return Err(bad("bad size or frame rate"));
+ }
+ let mut header = Vec::new();
+ header.extend_from_slice(&el_uint(0x4286, 1)); // EBMLVersion
+ header.extend_from_slice(&el_uint(0x42F7, 1)); // EBMLReadVersion
+ header.extend_from_slice(&el_uint(0x42F2, 4)); // EBMLMaxIDLength
+ header.extend_from_slice(&el_uint(0x42F3, 8)); // EBMLMaxSizeLength
+ header.extend_from_slice(&el(0x4282, b"webm")); // DocType
+ header.extend_from_slice(&el_uint(0x4287, 4)); // DocTypeVersion
+ header.extend_from_slice(&el_uint(0x4285, 2)); // DocTypeReadVersion
+ w.write_all(&el(EBML, &header)).map_err(io)?;
+ w.write_all(&ebml_id(SEGMENT)).map_err(io)?;
+ let segment_size_at = w.stream_position().map_err(io)?;
+ w.write_all(&ebml_size8(0)).map_err(io)?;
+ let segment_start = w.stream_position().map_err(io)?;
+ // a Void the SeekHead replaces at the end
+ w.write_all(&el(VOID, &[0; SEEK_ROOM - 2])).map_err(io)?;
+ let info_at = w.stream_position().map_err(io)? - segment_start;
+ let mut info = el_uint(TIMESTAMP_SCALE, 1_000_000);
+ let duration_offset = info.len() + 3; // after Duration's id (2 bytes) and size (1 byte)
+ info.extend_from_slice(&el_float(DURATION, 0.0));
+ info.extend_from_slice(&el(0x4D80, b"LightCraft")); // MuxingApp
+ info.extend_from_slice(&el(0x5741, b"LightCraft")); // WritingApp
+ let info_el = el(INFO, &info);
+ let duration_at = segment_start + info_at + (info_el.len() - info.len()) as u64 + duration_offset as u64;
+ w.write_all(&info_el).map_err(io)?;
+ Ok(WebmWriter {
+ w,
+ width,
+ height,
+ fps,
+ segment_size_at,
+ segment_start,
+ duration_at,
+ info_at,
+ tracks_at: None,
+ config: None,
+ periods: 0,
+ pending: None,
+ cluster: Vec::new(),
+ cluster_ts: 0,
+ last_block_ts: 0,
+ cues: Vec::new(),
+ })
+ }
+
+ /// The `CodecPrivate` (the `av1C` record, [`av1_config`]); needed before the first sample.
+ pub fn set_config(&mut self, config: Vec) {
+ self.config = Some(config);
+ }
+
+ fn ms(&self, periods: u64) -> u64 {
+ periods.saturating_mul(1000) / u64::from(self.fps)
+ }
+
+ fn write_tracks(&mut self) -> Result<()> {
+ let config = self.config.clone().ok_or_else(|| bad("no AV1 configuration"))?;
+ let mut video = el_uint(0xB0, u64::from(self.width)); // PixelWidth
+ video.extend_from_slice(&el_uint(0xBA, u64::from(self.height))); // PixelHeight
+ let mut entry = el_uint(0xD7, 1); // TrackNumber
+ entry.extend_from_slice(&el_uint(0x73C5, 1)); // TrackUID
+ entry.extend_from_slice(&el_uint(0x83, 1)); // TrackType video
+ entry.extend_from_slice(&el_uint(0x9C, 0)); // FlagLacing
+ entry.extend_from_slice(&el(0x86, b"V_AV1")); // CodecID
+ entry.extend_from_slice(&el(0x63A2, &config)); // CodecPrivate
+ entry.extend_from_slice(&el_uint(0x23E383, 1_000_000_000 / u64::from(self.fps))); // DefaultDuration (ns)
+ entry.extend_from_slice(&el(0xE0, &video));
+ let at = self.w.stream_position().map_err(io)? - self.segment_start;
+ self.w.write_all(&el(TRACKS, &el(TRACK_ENTRY, &entry))).map_err(io)?;
+ self.tracks_at = Some(at);
+ Ok(())
+ }
+
+ fn flush_cluster(&mut self) -> Result<()> {
+ if self.cluster.is_empty() {
+ return Ok(());
+ }
+ let mut body = el_uint(TIMESTAMP, self.cluster_ts);
+ body.extend_from_slice(&self.cluster);
+ let at = self.w.stream_position().map_err(io)? - self.segment_start;
+ self.w.write_all(&el(CLUSTER, &body)).map_err(io)?;
+ self.cues.push((self.cluster_ts, at));
+ self.cluster.clear();
+ Ok(())
+ }
+
+ /// The block header: track 1, timestamp relative to the cluster, flags.
+ fn block(&self, data: &[u8], ts: u64, flags: u8) -> Result> {
+ let rel = i16::try_from(ts.saturating_sub(self.cluster_ts)).map_err(|_| bad("block timestamp out of range"))?;
+ let mut b = Vec::with_capacity(data.len() + 4);
+ b.push(0x81);
+ b.extend_from_slice(&rel.to_be_bytes());
+ b.push(flags);
+ b.extend_from_slice(data);
+ Ok(b)
+ }
+
+ /// Start a new cluster at a key frame or before the relative timestamp could overflow.
+ fn place(&mut self, key: bool, ts: u64) -> Result<()> {
+ if !self.cluster.is_empty() && (key || ts.saturating_sub(self.cluster_ts) > 30_000) {
+ self.flush_cluster()?;
+ }
+ if self.cluster.is_empty() {
+ self.cluster_ts = ts;
+ }
+ Ok(())
+ }
+
+ fn write_simple(&mut self, s: &Sample, ts: u64) -> Result<()> {
+ self.place(s.key, ts)?;
+ let b = self.block(&s.data, ts, if s.key { 0x80 } else { 0 })?;
+ self.cluster.extend_from_slice(&el(SIMPLE_BLOCK, &b));
+ self.last_block_ts = ts;
+ Ok(())
+ }
+
+ pub fn add(&mut self, s: &Sample) -> Result<()> {
+ if s.duration == 0 {
+ return Err(bad("a sample lasts at least one frame"));
+ }
+ if self.tracks_at.is_none() {
+ if !s.key {
+ return Err(bad("the first sample must be a key frame"));
+ }
+ self.write_tracks()?;
+ }
+ if let Some((p, start)) = self.pending.take() {
+ let ts = self.ms(start);
+ self.write_simple(&p, ts)?;
+ }
+ let start = self.periods;
+ self.periods = self.periods.saturating_add(s.duration);
+ if self.periods > MAX_PERIODS {
+ return Err(bad("video too long"));
+ }
+ self.pending = Some((s.clone(), start));
+ Ok(())
+ }
+
+ pub fn periods(&self) -> u64 {
+ self.periods
+ }
+
+ pub fn finish(mut self) -> Result {
+ let (last, start) = self.pending.take().ok_or_else(|| bad("no frames"))?;
+ let ts = self.ms(start);
+ self.place(last.key, ts)?;
+ let mut group = el(BLOCK, &self.block(&last.data, ts, 0)?);
+ group.extend_from_slice(&el_uint(BLOCK_DURATION, self.ms(self.periods).saturating_sub(ts).max(1)));
+ if !last.key {
+ // the previous block, as a (negative) offset from this one
+ let back = i64::try_from(ts.saturating_sub(self.last_block_ts)).unwrap_or(i64::MAX).max(1);
+ el_int_into(&mut group, REFERENCE_BLOCK, -back);
+ }
+ self.cluster.extend_from_slice(&el(BLOCK_GROUP, &group));
+ self.flush_cluster()?;
+ // cues: one per cluster (each starts with a key frame or carries on from one)
+ let cues_at = self.w.stream_position().map_err(io)? - self.segment_start;
+ let mut cues = Vec::new();
+ for (t, pos) in &self.cues {
+ let mut positions = el_uint(0xF7, 1); // CueTrack
+ positions.extend_from_slice(&el_uint(0xF1, *pos)); // CueClusterPosition
+ let mut point = el_uint(0xB3, *t); // CueTime
+ point.extend_from_slice(&el(0xB7, &positions)); // CueTrackPositions
+ cues.extend_from_slice(&el(0xBB, &point)); // CuePoint
+ }
+ self.w.write_all(&el(CUES, &cues)).map_err(io)?;
+ let end = self.w.stream_position().map_err(io)?;
+ // SeekHead in the room kept for it, the rest stays a Void
+ let tracks_at = self.tracks_at.ok_or_else(|| bad("no tracks"))?;
+ let mut seeks = Vec::new();
+ for (id, at) in [(INFO, self.info_at), (TRACKS, tracks_at), (CUES, cues_at)] {
+ let mut seek = el(SEEK_ID, &ebml_id(id));
+ seek.extend_from_slice(&el(SEEK_POSITION, &at.to_be_bytes()));
+ seeks.extend_from_slice(&el(SEEK, &seek));
+ }
+ let head = el(SEEK_HEAD, &seeks);
+ let rest = SEEK_ROOM.checked_sub(head.len()).filter(|r| *r >= 2).ok_or_else(|| bad("seek head too large"))?;
+ let mut room = head;
+ room.extend_from_slice(&el(VOID, &vec![0; rest - 2]));
+ self.w.seek(SeekFrom::Start(self.segment_start)).map_err(io)?;
+ self.w.write_all(&room).map_err(io)?;
+ self.w.seek(SeekFrom::Start(self.duration_at)).map_err(io)?;
+ self.w.write_all(&(self.ms(self.periods) as f64).to_be_bytes()).map_err(io)?;
+ self.w.seek(SeekFrom::Start(self.segment_size_at)).map_err(io)?;
+ self.w.write_all(&ebml_size8(end - self.segment_start)).map_err(io)?;
+ self.w.seek(SeekFrom::Start(end)).map_err(io)?;
+ self.w.flush().map_err(io)?;
+ Ok(self.w)
+ }
+}
+
+fn el_int_into(out: &mut Vec, id: u32, v: i64) {
+ let b = v.to_be_bytes();
+ // the fewest bytes that keep the sign
+ let mut skip = 0;
+ while skip < 7 && ((b[skip] == 0 && b[skip + 1] & 0x80 == 0) || (b[skip] == 0xff && b[skip + 1] & 0x80 != 0)) {
+ skip += 1;
+ }
+ out.extend_from_slice(&el(id, &b[skip..]));
+}
+
+// ---------------------------------------------------------------- either container
+
+/// An MP4 or WebM writer.
+pub enum VideoWriter {
+ Mp4(Mp4Writer),
+ WebM(WebmWriter),
+}
+
+impl VideoWriter {
+ pub fn new(w: W, container: Container, width: u32, height: u32, fps: u32) -> Result {
+ Ok(match container {
+ Container::Mp4 => VideoWriter::Mp4(Mp4Writer::new(w, width, height, fps)?),
+ Container::WebM => VideoWriter::WebM(WebmWriter::new(w, width, height, fps)?),
+ })
+ }
+
+ pub fn set_config(&mut self, config: Vec) {
+ match self {
+ VideoWriter::Mp4(m) => m.set_config(config),
+ VideoWriter::WebM(m) => m.set_config(config),
+ }
+ }
+
+ pub fn add(&mut self, s: &Sample) -> Result<()> {
+ match self {
+ VideoWriter::Mp4(m) => m.add(s),
+ VideoWriter::WebM(m) => m.add(s),
+ }
+ }
+
+ pub fn periods(&self) -> u64 {
+ match self {
+ VideoWriter::Mp4(m) => m.periods(),
+ VideoWriter::WebM(m) => m.periods(),
+ }
+ }
+
+ pub fn finish(self) -> Result {
+ match self {
+ VideoWriter::Mp4(m) => m.finish(),
+ VideoWriter::WebM(m) => m.finish(),
+ }
+ }
+}
+
+// ---------------------------------------------------------------- AV1 encoder
+
+/// How to encode: frame size (even numbers; odd ones are rounded down), nominal frame rate,
+/// quality 1–100 and rav1e speed 1 (slow) – 10 (fast).
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct VideoConfig {
+ pub width: u32,
+ pub height: u32,
+ pub fps: u32,
+ pub quality: u8,
+ pub speed: u8,
+}
+
+/// Is AV1 video encoding in this build (native, `avif` feature)?
+pub const AV1_AVAILABLE: bool = cfg!(all(feature = "avif", not(target_arch = "wasm32")));
+
+/// BT.709 limited-range 4:2:0 planes of an sRGB RGBA image (alpha ignored), `w × h` (even).
+pub fn yuv420(rgba: &[[u8; 4]], w: usize, h: usize) -> Result<(Vec, Vec, Vec)> {
+ if w == 0 || h == 0 || !w.is_multiple_of(2) || !h.is_multiple_of(2) || w.checked_mul(h).is_none_or(|n| rgba.len() < n) {
+ return Err(bad("frame size"));
+ }
+ const KR: f32 = 0.2126;
+ const KB: f32 = 0.0722;
+ const KG: f32 = 1.0 - KR - KB;
+ let mut y = vec![0u8; w * h];
+ let (cw, ch) = (w / 2, h / 2);
+ let mut u = vec![0u8; cw * ch];
+ let mut v = vec![0u8; cw * ch];
+ let luma = |p: [u8; 4]| KR * f32::from(p[0]) + KG * f32::from(p[1]) + KB * f32::from(p[2]);
+ for (i, out) in y.iter_mut().enumerate() {
+ let p = rgba.get(i).copied().unwrap_or([0; 4]);
+ *out = (16.0 + luma(p) * (219.0 / 255.0)).round().clamp(0.0, 255.0) as u8;
+ }
+ for cy in 0..ch {
+ for cx in 0..cw {
+ let mut acc = [0f32; 3];
+ for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
+ let p = rgba.get((cy * 2 + dy) * w + cx * 2 + dx).copied().unwrap_or([0; 4]);
+ for (a, c) in acc.iter_mut().zip(p) {
+ *a += f32::from(c) / 4.0;
+ }
+ }
+ let l = KR * acc[0] + KG * acc[1] + KB * acc[2];
+ let cb = (acc[2] - l) / (2.0 * (1.0 - KB));
+ let cr = (acc[0] - l) / (2.0 * (1.0 - KR));
+ let i = cy * cw + cx;
+ if let Some(o) = u.get_mut(i) {
+ *o = (128.0 + cb * (224.0 / 255.0)).round().clamp(0.0, 255.0) as u8;
+ }
+ if let Some(o) = v.get_mut(i) {
+ *o = (128.0 + cr * (224.0 / 255.0)).round().clamp(0.0, 255.0) as u8;
+ }
+ }
+ }
+ Ok((y, u, v))
+}
+
+/// The quantizer for `quality` (ravif's curve, so qualities match the AVIF export).
+pub fn quantizer(quality: u8) -> usize {
+ let q = f32::from(quality.clamp(1, 100)) / 100.0;
+ let x = if q >= 0.82 {
+ (1.0 - q) * 2.6
+ } else if q > 0.25 {
+ 1.0 - 0.125 - q * 0.5
+ } else {
+ 1.0 - q
+ };
+ (x * 255.0).round().clamp(0.0, 255.0) as usize
+}
+
+/// An AV1 encoder fed RGBA frames, each with how many frame periods it stays up; it hands back
+/// the samples as they come out (in order: no frame reordering).
+pub struct Av1Encoder {
+ #[cfg(all(feature = "avif", not(target_arch = "wasm32")))]
+ ctx: rav1e::Context,
+ /// Each frame sent so far: its duration (by input frame number).
+ #[cfg_attr(not(all(feature = "avif", not(target_arch = "wasm32"))), allow(dead_code))]
+ durations: Vec,
+ width: usize,
+ height: usize,
+ seq_header: Option>,
+}
+
+impl Av1Encoder {
+ pub fn new(c: &VideoConfig) -> Result {
+ let (w, h) = ((c.width & !1) as usize, (c.height & !1) as usize);
+ if w < 16 || h < 16 || w > 8192 || h > 8192 || !(1..=240).contains(&c.fps) {
+ return Err(bad("unsupported video size or frame rate"));
+ }
+ #[cfg(all(feature = "avif", not(target_arch = "wasm32")))]
+ {
+ use rav1e::prelude::*;
+ let mut ec = EncoderConfig::with_speed_preset(c.speed.clamp(1, 10));
+ ec.width = w;
+ ec.height = h;
+ ec.time_base = Rational::new(1, u64::from(c.fps));
+ ec.bit_depth = 8;
+ ec.chroma_sampling = ChromaSampling::Cs420;
+ ec.pixel_range = PixelRange::Limited;
+ ec.color_description = Some(ColorDescription {
+ color_primaries: ColorPrimaries::BT709,
+ transfer_characteristics: TransferCharacteristics::BT709,
+ matrix_coefficients: MatrixCoefficients::BT709,
+ });
+ // in order, one shown frame per packet: samples map 1:1 to the frames sent
+ ec.low_latency = true;
+ let q = quantizer(c.quality);
+ ec.quantizer = q;
+ ec.min_quantizer = q.min(255) as u8;
+ ec.min_key_frame_interval = 1;
+ ec.max_key_frame_interval = 120;
+ // tiles are what rav1e encodes in parallel (16 measured best for 1080p on 32 threads)
+ ec.tiles = std::thread::available_parallelism().map_or(4, |n| n.get()).clamp(2, 16);
+ let ctx: Context = Config::new().with_encoder_config(ec).new_context().map_err(|e| bad(format!("AV1: {e}")))?;
+ Ok(Av1Encoder { ctx, durations: Vec::new(), width: w, height: h, seq_header: None })
+ }
+ #[cfg(not(all(feature = "avif", not(target_arch = "wasm32"))))]
+ {
+ let _ = (w, h);
+ Err(bad("AV1 video encoding is not available in this build"))
+ }
+ }
+
+ /// The frame size actually encoded (even numbers).
+ pub fn size(&self) -> (usize, usize) {
+ (self.width, self.height)
+ }
+
+ /// The configuration record for the container ([`av1_config`]), once the first sample (with
+ /// the sequence header) has come out.
+ pub fn config_record(&self) -> Option> {
+ self.seq_header.as_ref().map(|s| av1_config(s))
+ }
+
+ /// Encode `rgba` (`width × height` of [`Self::size`]) shown for `periods` frame periods;
+ /// `key` forces a key frame (a picture that doesn't lean on earlier ones: a held slide is
+ /// then clean of the slide before it, and seeking lands on it).
+ pub fn push(&mut self, rgba: &[[u8; 4]], periods: u64, key: bool) -> Result> {
+ let (y, u, v) = yuv420(rgba, self.width, self.height)?;
+ #[cfg(all(feature = "avif", not(target_arch = "wasm32")))]
+ {
+ let mut frame = self.ctx.new_frame();
+ for (plane, (data, w)) in frame.planes.iter_mut().zip([(&y, self.width), (&u, self.width / 2), (&v, self.width / 2)]) {
+ plane.copy_from_raw_u8(data, w, 1);
+ }
+ let params = rav1e::prelude::FrameParameters {
+ frame_type_override: if key { rav1e::prelude::FrameTypeOverride::Key } else { rav1e::prelude::FrameTypeOverride::No },
+ ..Default::default()
+ };
+ self.ctx.send_frame((frame, params)).map_err(|e| bad(format!("AV1: {e}")))?;
+ self.durations.push(periods.max(1));
+ self.drain()
+ }
+ #[cfg(not(all(feature = "avif", not(target_arch = "wasm32"))))]
+ {
+ let _ = (y, u, v, periods, key);
+ Err(bad("AV1 video encoding is not available in this build"))
+ }
+ }
+
+ /// Flush the encoder: the samples still inside.
+ pub fn finish(&mut self) -> Result> {
+ #[cfg(all(feature = "avif", not(target_arch = "wasm32")))]
+ {
+ self.ctx.flush();
+ self.drain()
+ }
+ #[cfg(not(all(feature = "avif", not(target_arch = "wasm32"))))]
+ Err(bad("AV1 video encoding is not available in this build"))
+ }
+
+ #[cfg(all(feature = "avif", not(target_arch = "wasm32")))]
+ fn drain(&mut self) -> Result> {
+ use rav1e::prelude::*;
+ let mut out = Vec::new();
+ loop {
+ match self.ctx.receive_packet() {
+ Ok(p) => {
+ let (data, seq) = to_sample_data(&p.data)?;
+ if self.seq_header.is_none() {
+ self.seq_header = seq;
+ }
+ let duration = usize::try_from(p.input_frameno).ok().and_then(|i| self.durations.get(i)).copied().unwrap_or(1);
+ out.push(Sample { data, key: p.frame_type == FrameType::KEY, duration });
+ }
+ Err(EncoderStatus::Encoded) => {}
+ Err(EncoderStatus::NeedMoreData | EncoderStatus::LimitReached) => break,
+ Err(e) => return Err(bad(format!("AV1: {e}"))),
+ }
+ }
+ Ok(out)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::io::Cursor;
+
+ /// The boxes of an MP4 byte range: (type, body range).
+ pub fn boxes(d: &[u8], range: std::ops::Range) -> Vec<([u8; 4], std::ops::Range)> {
+ let mut out = Vec::new();
+ let mut p = range.start;
+ while p + 8 <= range.end {
+ let size32 = u32::from_be_bytes(d[p..p + 4].try_into().unwrap()) as usize;
+ let kind: [u8; 4] = d[p + 4..p + 8].try_into().unwrap();
+ let (hdr, size) = if size32 == 1 { (16, u64::from_be_bytes(d[p + 8..p + 16].try_into().unwrap()) as usize) } else { (8, size32) };
+ assert!(size >= hdr && p + size <= range.end, "box {} overruns", String::from_utf8_lossy(&kind));
+ out.push((kind, p + hdr..p + size));
+ p += size;
+ }
+ assert_eq!(p, range.end, "trailing bytes");
+ out
+ }
+
+ fn find(d: &[u8], r: std::ops::Range, path: &[&[u8; 4]]) -> std::ops::Range {
+ let mut r = r;
+ for k in path {
+ r = boxes(d, r).into_iter().find(|(t, _)| t == *k).unwrap_or_else(|| panic!("no {}", String::from_utf8_lossy(*k))).1;
+ }
+ r
+ }
+
+ fn samples() -> Vec {
+ // fake AV1 data: an OBU_FRAME (type 6) with a size field
+ let frame = |b: u8| vec![0x32, 2, b, b];
+ vec![
+ Sample { data: frame(1), key: true, duration: 30 },
+ Sample { data: frame(2), key: false, duration: 1 },
+ Sample { data: frame(3), key: false, duration: 1 },
+ Sample { data: frame(4), key: true, duration: 58 },
+ ]
+ }
+
+ #[test]
+ fn obus_split_and_temporal_delimiters_go() {
+ // TD, sequence header (3 bytes), frame (2 bytes)
+ let tu = [0x12, 0, 0x0a, 3, 1, 2, 3, 0x32, 2, 9, 9];
+ let o = obus(&tu).unwrap();
+ assert_eq!(o.iter().map(|(k, _)| *k).collect::>(), vec![2, 1, 6]);
+ let (data, seq) = to_sample_data(&tu).unwrap();
+ assert_eq!(data, &tu[2..]);
+ assert_eq!(seq.unwrap(), &tu[2..7]);
+ // hostile: sizes past the end, forbidden bit, endless LEB128
+ assert!(obus(&[0x32, 9, 1]).is_err());
+ assert!(obus(&[0x80]).is_err());
+ assert!(obus(&[0x32, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]).is_err());
+ assert!(obus(&[0x36]).is_err(), "extension byte missing");
+ assert_eq!(av1_config(&[1, 2])[..], [0x81, 31, 0x0c, 0, 1, 2]);
+ }
+
+ #[test]
+ fn mp4_box_tree_samples_and_durations() {
+ let mut m = Mp4Writer::new(Cursor::new(Vec::new()), 64, 36, 30).unwrap();
+ assert!(m.add(&Sample { data: vec![1], key: false, duration: 1 }).is_err(), "must start on a key frame");
+ for s in samples() {
+ m.add(&s).unwrap();
+ }
+ assert!(Mp4Writer::new(Cursor::new(Vec::new()), 64, 36, 30).unwrap().finish().is_err(), "no frames");
+ m.set_config(av1_config(&[0x0a, 1, 0]));
+ let d = m.finish().unwrap().into_inner();
+ let top = boxes(&d, 0..d.len());
+ assert_eq!(top.iter().map(|(k, _)| *k).collect::>(), vec![*b"ftyp", *b"mdat", *b"moov"]);
+ assert_eq!(&d[top[1].1.clone()], &[0x32, 2, 1, 1, 0x32, 2, 2, 2, 0x32, 2, 3, 3, 0x32, 2, 4, 4]);
+ let stbl = find(&d, 0..d.len(), &[b"moov", b"trak", b"mdia", b"minf", b"stbl"]);
+ let kinds: Vec<_> = boxes(&d, stbl.clone()).into_iter().map(|(k, _)| k).collect();
+ assert_eq!(kinds, vec![*b"stsd", *b"stts", *b"stss", *b"stsc", *b"stsz", *b"stco"]);
+ let be = |r: usize| u32::from_be_bytes(d[r..r + 4].try_into().unwrap());
+ let stsz = find(&d, stbl.clone(), &[b"stsz"]);
+ assert_eq!(be(stsz.start + 8), 4, "sample count");
+ let stts = find(&d, stbl.clone(), &[b"stts"]);
+ assert_eq!(be(stts.start + 4), 3, "runs: 30, 1×2, 58");
+ let total: u32 = (0..3).map(|i| be(stts.start + 8 + i * 8) * be(stts.start + 12 + i * 8)).sum();
+ assert_eq!(total, 90 * 512);
+ let stss = find(&d, stbl.clone(), &[b"stss"]);
+ assert_eq!((be(stss.start + 4), be(stss.start + 8), be(stss.start + 12)), (2, 1, 4));
+ let stco = find(&d, stbl.clone(), &[b"stco"]);
+ assert_eq!(be(stco.start + 8) as usize, top[1].1.start, "chunk offset = mdat data");
+ let stsd = find(&d, stbl, &[b"stsd"]);
+ let av01 = boxes(&d, stsd.start + 8..stsd.end);
+ assert_eq!(av01[0].0, *b"av01");
+ let entry = av01[0].1.clone();
+ assert_eq!(u16::from_be_bytes([d[entry.start + 24], d[entry.start + 25]]), 64);
+ let inner: Vec<_> = boxes(&d, entry.start + 78..entry.end).into_iter().map(|(k, _)| k).collect();
+ assert_eq!(inner, vec![*b"av1C", *b"colr", *b"pasp"]);
+ let mdhd = find(&d, 0..d.len(), &[b"moov", b"trak", b"mdia", b"mdhd"]);
+ assert_eq!((be(mdhd.start + 12), be(mdhd.start + 16)), (30 * 512, 90 * 512));
+ let mvhd = find(&d, 0..d.len(), &[b"moov", b"mvhd"]);
+ assert_eq!(be(mvhd.start + 16), 3000, "3 s in ms");
+ }
+
+ /// The EBML elements of a byte range: (id, body range).
+ fn elements(d: &[u8], range: std::ops::Range) -> Vec<(u32, std::ops::Range)> {
+ let vint = |p: usize| -> (u64, usize) {
+ let len = d[p].leading_zeros() as usize + 1;
+ let mut v = u64::from(d[p]) & (0xff >> len);
+ for i in 1..len {
+ v = (v << 8) | u64::from(d[p + i]);
+ }
+ (v, len)
+ };
+ let mut out = Vec::new();
+ let mut p = range.start;
+ while p < range.end {
+ let idlen = d[p].leading_zeros() as usize + 1;
+ let id = d[p..p + idlen].iter().fold(0u32, |a, b| (a << 8) | u32::from(*b));
+ let (size, n) = vint(p + idlen);
+ let start = p + idlen + n;
+ let end = start + size as usize;
+ assert!(end <= range.end, "element {id:x} overruns");
+ out.push((id, start..end));
+ p = end;
+ }
+ out
+ }
+
+ #[test]
+ fn webm_element_tree_blocks_and_duration() {
+ let mut m = WebmWriter::new(Cursor::new(Vec::new()), 64, 36, 30).unwrap();
+ assert!(m.add(&Sample { data: vec![0x32, 0], key: true, duration: 1 }).is_err(), "configuration first");
+ let mut m = WebmWriter::new(Cursor::new(Vec::new()), 64, 36, 30).unwrap();
+ m.set_config(av1_config(&[0x0a, 1, 0]));
+ for s in samples() {
+ m.add(&s).unwrap();
+ }
+ let d = m.finish().unwrap().into_inner();
+ let top = elements(&d, 0..d.len());
+ assert_eq!(top.iter().map(|(i, _)| *i).collect::>(), vec![EBML, SEGMENT]);
+ let seg = elements(&d, top[1].1.clone());
+ let ids: Vec<_> = seg.iter().map(|(i, _)| *i).collect();
+ assert_eq!(ids, vec![SEEK_HEAD, VOID, INFO, TRACKS, CLUSTER, CLUSTER, CUES]);
+ let info = elements(&d, seg[2].1.clone());
+ let dur = info.iter().find(|(i, _)| *i == DURATION).unwrap().1.clone();
+ assert_eq!(f64::from_be_bytes(d[dur].try_into().unwrap()), 3000.0);
+ let track = elements(&d, elements(&d, seg[3].1.clone())[0].1.clone());
+ let codec = track.iter().find(|(i, _)| *i == 0x86).unwrap().1.clone();
+ assert_eq!(&d[codec], b"V_AV1");
+ let first: Vec<_> = elements(&d, seg[4].1.clone()).into_iter().map(|(i, _)| i).collect();
+ assert_eq!(first, vec![TIMESTAMP, SIMPLE_BLOCK, SIMPLE_BLOCK, SIMPLE_BLOCK]);
+ let second = elements(&d, seg[5].1.clone());
+ assert_eq!(d[second[0].1.clone()], [0x04, 0x2a], "the second cluster starts at 1066 ms (32 periods)");
+ let group = elements(&d, second[1].1.clone());
+ assert_eq!(group.iter().map(|(i, _)| *i).collect::>(), vec![BLOCK, BLOCK_DURATION]);
+ // the SeekHead points at Info, Tracks and Cues
+ let seg_start = top[1].1.start;
+ for seek in elements(&d, seg[0].1.clone()) {
+ let e = elements(&d, seek.1);
+ let pos = d[e[1].1.clone()].iter().fold(0usize, |a, b| (a << 8) | usize::from(*b));
+ let id = d[e[0].1.clone()].iter().fold(0u32, |a, b| (a << 8) | u32::from(*b));
+ assert!(seg.iter().any(|(i, r)| *i == id && r.start > seg_start + pos), "{id:x}");
+ }
+ let mut neg = Vec::new();
+ el_int_into(&mut neg, REFERENCE_BLOCK, -33);
+ assert_eq!(neg, vec![0xfb, 0x81, 0xdf]);
+ assert_eq!(ebml_size(126), vec![0xfe]);
+ assert_eq!(ebml_size(127), vec![0x40, 0x7f]);
+ }
+
+ #[test]
+ fn hostile_sizes_are_refused() {
+ assert!(Mp4Writer::new(Cursor::new(Vec::new()), 0, 10, 30).is_err());
+ assert!(Mp4Writer::new(Cursor::new(Vec::new()), 70_000, 10, 30).is_err());
+ assert!(WebmWriter::new(Cursor::new(Vec::new()), 10, 10, 0).is_err());
+ assert!(yuv420(&[[0; 4]; 3], 2, 2).is_err());
+ assert!(yuv420(&[[0; 4]; 9], 3, 3).is_err());
+ let (y, u, v) = yuv420(&[[255; 4]; 4], 2, 2).unwrap();
+ assert_eq!((y, u, v), (vec![235; 4], vec![128], vec![128]));
+ assert!(quantizer(100) < quantizer(50) && quantizer(50) < quantizer(1));
+ assert!(Av1Encoder::new(&VideoConfig { width: 8, height: 8, fps: 30, quality: 50, speed: 10 }).is_err());
+ }
+
+ #[cfg(all(feature = "avif", not(target_arch = "wasm32")))]
+ #[test]
+ fn encodes_av1_into_a_playable_mp4() {
+ let c = VideoConfig { width: 64, height: 48, fps: 24, quality: 60, speed: 10 };
+ let mut e = Av1Encoder::new(&c).unwrap();
+ let mut out = Vec::new();
+ for (i, periods) in [(0u8, 24u64), (80, 1), (160, 1), (240, 30)] {
+ let img = vec![[i, 255 - i, 128, 255]; 64 * 48];
+ out.extend(e.push(&img, periods, periods > 1).unwrap());
+ }
+ out.extend(e.finish().unwrap());
+ assert_eq!(out.len(), 4, "one sample per frame sent");
+ assert!(out[0].key);
+ assert_eq!(out.iter().map(|s| s.duration).sum::(), 56);
+ for s in &out {
+ assert!(obus(&s.data).unwrap().iter().all(|(k, _)| *k != OBU_TEMPORAL_DELIMITER));
+ }
+ let config = e.config_record().unwrap();
+ assert_eq!(obus(&config[4..]).unwrap()[0].0, OBU_SEQUENCE_HEADER);
+ let mut m = VideoWriter::new(Cursor::new(Vec::new()), Container::Mp4, 64, 48, 24).unwrap();
+ m.set_config(config);
+ for s in &out {
+ m.add(s).unwrap();
+ }
+ let d = m.finish().unwrap().into_inner();
+ assert_eq!(boxes(&d, 0..d.len()).len(), 3);
+ }
+}
diff --git a/crates/codecs/tests/corpus.rs b/crates/codecs/tests/corpus.rs
index d3b2ceba3..9a86bc5e2 100644
--- a/crates/codecs/tests/corpus.rs
+++ b/crates/codecs/tests/corpus.rs
@@ -31,7 +31,7 @@ fn files(dir: &Path) -> Vec {
fn corpus_images_decode() {
let dir = corpus_root().join("images");
if !dir.is_dir() {
- eprintln!("skip: {} absent", dir.display());
+ lightcraft_testkit::skip!(Corpus, "{} absent", dir.display());
return;
}
let (mut ok, mut unsupported) = (0, 0);
@@ -57,7 +57,7 @@ fn corpus_images_decode() {
fn corpus_pngsuite() {
let dir = corpus_root().join("pngsuite");
if !dir.is_dir() {
- eprintln!("skip: {} absent", dir.display());
+ lightcraft_testkit::skip!(Corpus, "{} absent", dir.display());
return;
}
let mut n = 0;
@@ -83,7 +83,7 @@ fn corpus_pngsuite() {
fn corpus_raw_is_routed() {
let dir = corpus_root().join("raw");
if !dir.is_dir() {
- eprintln!("skip: {} absent", dir.display());
+ lightcraft_testkit::skip!(Corpus, "{} absent", dir.display());
return;
}
for p in files(&dir) {
@@ -103,7 +103,7 @@ fn system_icc_profiles() {
use lightcraft_codecs::icc::{IccColorModel, IccKind, parse};
let dir = Path::new("/System/Library/ColorSync/Profiles");
if !dir.is_dir() {
- eprintln!("skip: no system ICC profiles");
+ lightcraft_testkit::skip!(System, "no system ICC profiles");
return;
}
let expect = [
diff --git a/crates/codecs/tests/float_tiff.rs b/crates/codecs/tests/float_tiff.rs
new file mode 100644
index 000000000..fe1cd2411
--- /dev/null
+++ b/crates/codecs/tests/float_tiff.rs
@@ -0,0 +1,128 @@
+//! Non-finite samples in procedural 32-bit float TIFFs (issue #1952, item 6).
+
+use lightcraft_codecs::{icc, *};
+
+fn tiff(samples: &[f32], channels: u8, profile: Option<&[u8]>) -> Vec {
+ encode_tiff(
+ &EncodeImage::new((samples.len() / channels as usize) as u32, 1, channels, Samples::F32(samples)),
+ TiffCompression::None,
+ &EncodeMeta { icc: profile, ..Default::default() },
+ )
+ .unwrap()
+}
+
+#[test]
+fn float_tiff_nonfinite_rgb_clips_without_losing_finite_hdr() {
+ let samples =
+ [[f32::INFINITY; 3], [f32::NAN; 3], [f32::NEG_INFINITY; 3], [-0.25; 3], [0.25, 0.5, 0.75], [2.0, 4.0, 8.0], [f32::INFINITY, 0.5, f32::NAN]];
+ let bytes = tiff(samples.as_flattened(), 3, None);
+ let decoded = decode(&bytes, DecodeOptions::default()).unwrap();
+ assert!(decoded.float);
+ assert_eq!(decoded.bit_depth, 32);
+ assert_eq!(decoded.image.data, [[1.0; 3], [0.0; 3], [0.0; 3], [-0.25; 3], [0.25, 0.5, 0.75], [2.0, 4.0, 8.0], [1.0, 0.5, 0.0]]);
+ let display = decoded.to_srgb8();
+ assert_eq!(display.data[0], [255; 4]);
+ for pixel in &display.data[1..4] {
+ assert_eq!(*pixel, [0, 0, 0, 255]);
+ }
+ assert_eq!(display.data[6], [255, 188, 0, 255]);
+ assert!(decoded.to_working().data.iter().flatten().all(|v| v.is_finite()));
+
+ // A box-filtered pair must average clipped white with its ordinary neighbor.
+ let pair = tiff(&[f32::INFINITY, f32::INFINITY, f32::INFINITY, 0.25, 0.25, 0.25], 3, None);
+ let small = decode(&pair, DecodeOptions::fit(1, 1)).unwrap();
+ assert_eq!(small.image.data, [[0.625; 3]]);
+}
+
+#[test]
+fn float_tiff_nonfinite_gray_and_icc_curves_match_clipped_samples() {
+ let samples = [f32::INFINITY, f32::NAN, f32::NEG_INFINITY, 0.25, 1.0, 2.0, -0.25];
+ let clipped = [1.0, 0.0, 0.0, 0.25, 1.0, 2.0, -0.25];
+ let profiles = [
+ None,
+ Some(icc::write_matrix_trc(&lightcraft_color::SRGB, &Trc::Linear)),
+ Some(icc::write_named(NamedSpace::Srgb)),
+ Some(icc::write_matrix_trc(&lightcraft_color::SRGB, &Trc::Gamma(2.0))),
+ Some(icc::write_matrix_trc(&lightcraft_color::SRGB, &Trc::Table(vec![0, 16384, 65535]))),
+ Some(moxcms::ColorProfile::new_gray_with_gamma(2.0).encode().unwrap()),
+ Some(b"invalid ICC profile".to_vec()),
+ ];
+ for channels in [1, 3] {
+ let samples: Vec<_> = samples.iter().flat_map(|v| std::iter::repeat_n(*v, channels as usize)).collect();
+ let clipped: Vec<_> = clipped.iter().flat_map(|v| std::iter::repeat_n(*v, channels as usize)).collect();
+ for profile in &profiles {
+ let profile = profile.as_deref();
+ let decoded = decode(&tiff(&samples, channels, profile), DecodeOptions::default()).unwrap();
+ let reference = decode(&tiff(&clipped, channels, profile), DecodeOptions::default()).unwrap();
+ assert_eq!(decoded.grayscale, channels == 1);
+ assert_eq!(decoded.image, reference.image, "channels={channels}, space={:?}", decoded.space);
+ assert_eq!(decoded.to_srgb8(), reference.to_srgb8());
+ assert!(decoded.image.data[5].iter().all(|v| *v > 1.0), "finite HDR survives the curve");
+ assert!(decoded.image.data[6].iter().all(|v| *v < 0.0), "finite negative samples survive the curve");
+ }
+ }
+}
+
+#[test]
+fn float_tiff_nonfinite_alpha_is_finite_and_bounded() {
+ let samples = [
+ [f32::INFINITY, f32::INFINITY, f32::INFINITY, f32::INFINITY],
+ [f32::NAN, f32::NEG_INFINITY, 0.5, f32::NAN],
+ [0.25, 0.5, 2.0, f32::NEG_INFINITY],
+ [2.0, 3.0, 4.0, 0.5],
+ ];
+ let decoded = decode(&tiff(samples.as_flattened(), 4, None), DecodeOptions::default()).unwrap();
+ assert_eq!(decoded.alpha.as_ref().unwrap().data, [1.0, 0.0, 0.0, 0.5]);
+ assert_eq!(decoded.image.data, [[1.0; 3], [0.0, 0.0, 0.5], [0.25, 0.5, 2.0], [2.0, 3.0, 4.0]]);
+ assert!(decoded.to_working().data.iter().flatten().all(|v| v.is_finite()));
+
+ // Associated alpha takes the same path before unpremultiplication.
+ let mut cursor = std::io::Cursor::new(Vec::new());
+ {
+ use tiff::encoder::{TiffEncoder, colortype};
+ let mut encoder = TiffEncoder::new(&mut cursor).unwrap();
+ let mut image = encoder.new_image::(2, 1).unwrap();
+ image.encoder().write_tag(tiff::tags::Tag::ExtraSamples, &[1u16][..]).unwrap();
+ image.write_data(&[f32::INFINITY, f32::NAN, f32::NEG_INFINITY, 0.5, 1.0, 2.0, 3.0, 0.5]).unwrap();
+ }
+ let decoded = decode(&cursor.into_inner(), DecodeOptions::default()).unwrap();
+ assert_eq!(decoded.alpha.as_ref().unwrap().data, [0.5, 0.5]);
+ assert_eq!(decoded.image.data, [[2.0, 0.0, 0.0], [2.0, 4.0, 6.0]]);
+}
+
+fn lut_profile() -> Vec {
+ use moxcms::{ColorProfile, LutMultidimensionalType, LutWarehouse, Matrix3d, ToneReprCurve, Vector3d, Xyzd};
+ let mut profile = ColorProfile::new_srgb();
+ profile.cicp = None;
+ profile.red_colorant = Xyzd::default();
+ profile.green_colorant = Xyzd::default();
+ profile.blue_colorant = Xyzd::default();
+ profile.red_trc = None;
+ profile.green_trc = None;
+ profile.blue_trc = None;
+ // A procedural mAB profile without a CLUT: linear RGB to D50 XYZ, then identity curves.
+ profile.lut_a_to_b_colorimetric = Some(LutWarehouse::Multidimensional(LutMultidimensionalType {
+ num_input_channels: 3,
+ num_output_channels: 3,
+ grid_points: [0; 16],
+ clut: None,
+ a_curves: vec![],
+ m_curves: vec![ToneReprCurve::Parametric(vec![1.0]); 3],
+ matrix: Matrix3d { v: NamedSpace::Srgb.to_xyz_d50().0.map(|r| r.map(|v| v * 32768.0 / 65535.0)) },
+ b_curves: vec![ToneReprCurve::Parametric(vec![1.0]); 3],
+ bias: Vector3d::default(),
+ }));
+ profile.encode().unwrap()
+}
+
+#[test]
+fn float_tiff_nonfinite_cms_samples_match_clipped_samples() {
+ let profile = lut_profile();
+ let samples = [f32::INFINITY, f32::NAN, f32::NEG_INFINITY, 0.25, 0.5, 0.75];
+ let clipped = [1.0, 0.0, 0.0, 0.25, 0.5, 0.75];
+ let decoded = decode(&tiff(&samples, 3, Some(&profile)), DecodeOptions::default()).unwrap();
+ let reference = decode(&tiff(&clipped, 3, Some(&profile)), DecodeOptions::default()).unwrap();
+ assert_eq!(decoded.space.origin, SpaceOrigin::IccCms);
+ assert_eq!(decoded.image, reference.image);
+ assert!(decoded.to_working().data.iter().flatten().all(|v| v.is_finite()));
+}
diff --git a/crates/codecs/tests/jxl_streaming.rs b/crates/codecs/tests/jxl_streaming.rs
new file mode 100644
index 000000000..298034656
--- /dev/null
+++ b/crates/codecs/tests/jxl_streaming.rs
@@ -0,0 +1,98 @@
+#![cfg(all(feature = "jxl", not(target_arch = "wasm32")))]
+
+use lightcraft_codecs::exif::minimal_exif;
+use lightcraft_codecs::*;
+use lightcraft_raster::Rgba8;
+
+fn codestream(bytes: &[u8]) -> Vec {
+ if bytes.starts_with(b"\xff\x0a") {
+ return bytes.to_vec();
+ }
+ let mut raw = Vec::new();
+ let mut at = 12;
+ while at + 8 <= bytes.len() {
+ let len = u32::from_be_bytes(bytes[at..at + 4].try_into().unwrap()) as usize;
+ let end = if len == 0 { bytes.len() } else { at + len };
+ match &bytes[at + 4..at + 8] {
+ b"jxlc" => raw.extend_from_slice(&bytes[at + 8..end]),
+ b"jxlp" => raw.extend_from_slice(&bytes[at + 12..end]),
+ _ => {}
+ }
+ at = end;
+ }
+ assert!(raw.starts_with(b"\xff\x0a"));
+ raw
+}
+
+fn box_bytes(out: &mut Vec, kind: &[u8; 4], data: &[u8]) {
+ out.extend_from_slice(&u32::try_from(data.len() + 8).unwrap().to_be_bytes());
+ out.extend_from_slice(kind);
+ out.extend_from_slice(data);
+}
+
+#[test]
+fn metadata_in_final_feed_matches_builder() {
+ let pixels = Rgba8::from_fn(8, 8, |x, y| [(x * 20) as u8, (y * 20) as u8, 80, 255]);
+ let encoded = encode_jxl(&EncodeImage::rgba8(&pixels), &JxlOptions { lossless: true, ..Default::default() }, &EncodeMeta::default()).unwrap();
+ let raw = codestream(&encoded);
+ let exif = minimal_exif(1);
+ let mut exif_box = vec![0; 4]; // TIFF header offset, relative to the end of this field
+ exif_box.extend_from_slice(&exif);
+ // Split the codestream across two feeds, with Exif and XMP following its last frame.
+ let mut bytes = b"\0\0\0\x0cJXL \x0d\x0a\x87\x0a".to_vec();
+ box_bytes(&mut bytes, b"ftyp", b"jxl \0\0\0\0jxl ");
+ let split = raw.len() / 2;
+ let mut part = 0u32.to_be_bytes().to_vec();
+ part.extend_from_slice(&raw[..split]);
+ box_bytes(&mut bytes, b"jxlp", &part);
+ let padding = vec![0; 4096 - bytes.len() - 8];
+ box_bytes(&mut bytes, b"free", &padding);
+ let mut part = 0x8000_0001u32.to_be_bytes().to_vec();
+ part.extend_from_slice(&raw[split..]);
+ box_bytes(&mut bytes, b"jxlp", &part);
+ box_bytes(&mut bytes, b"Exif", &exif_box);
+ box_bytes(&mut bytes, b"xml ", b" ");
+ assert!(bytes.len() < 8192, "metadata must fit in the completing input chunk");
+ let old = jxl_oxide::JxlImage::builder().read(bytes.as_slice()).unwrap();
+ let old_exif = match old.aux_boxes().first_exif().unwrap() {
+ jxl_oxide::AuxBoxData::Data(ex) => ex.payload().to_vec(),
+ _ => panic!("builder must preserve Exif in its final feed"),
+ };
+ let old_xml = match old.aux_boxes().first_xml() {
+ jxl_oxide::AuxBoxData::Data(xml) => xml.to_vec(),
+ _ => panic!("builder must preserve XMP in its final feed"),
+ };
+ let decoded = decode(&bytes, DecodeOptions::default()).unwrap();
+ assert_eq!(decoded.exif.as_deref(), Some(old_exif.as_slice()));
+ assert_eq!(decoded.xmp.as_deref(), Some(std::str::from_utf8(&old_xml).unwrap()));
+ assert_eq!(decoded.to_srgb8(), pixels);
+}
+
+#[test]
+fn progressive_truncation_matches_builder() {
+ let pixels = Rgba8::from_fn(96, 64, |x, y| {
+ let mut seed = (y * 96 + x + 1) as u32;
+ let mut rgb = [0; 4];
+ for channel in &mut rgb[..3] {
+ seed ^= seed << 13;
+ seed ^= seed >> 17;
+ seed ^= seed << 5;
+ *channel = seed as u8;
+ }
+ rgb[3] = 255;
+ rgb
+ });
+ for lossless in [false, true] {
+ let encoded =
+ encode_jxl(&EncodeImage::rgba8(&pixels), &JxlOptions { lossless, progressive: true, ..Default::default() }, &EncodeMeta::default())
+ .unwrap();
+ let raw = codestream(&encoded);
+ assert!(raw.len() > 4096, "fixture must exercise post-header feeds");
+ for cut in [0, 1, 2, 12, 4095, 4096, 4097, raw.len() / 2, raw.len() - 1, raw.len()] {
+ let bytes = &raw[..cut];
+ // `finalize` permits partial images: rendering determines whether frame 0 is usable.
+ let old_ok = jxl_oxide::JxlImage::builder().read(bytes).is_ok_and(|image| image.render_frame(0).is_ok());
+ assert_eq!(decode(bytes, DecodeOptions::default()).is_ok(), old_ok, "lossless={lossless}, cut={cut}/{}", raw.len());
+ }
+ }
+}
diff --git a/crates/codecs/tests/jxl_tail_heap.rs b/crates/codecs/tests/jxl_tail_heap.rs
new file mode 100644
index 000000000..f2200e862
--- /dev/null
+++ b/crates/codecs/tests/jxl_tail_heap.rs
@@ -0,0 +1,43 @@
+#![cfg(all(feature = "jxl", not(target_arch = "wasm32")))]
+
+use lightcraft_codecs::*;
+use lightcraft_raster::Rgba8;
+
+#[global_allocator]
+static ALLOC: dhat::Alloc = dhat::Alloc;
+
+fn codestream(bytes: &[u8]) -> Vec {
+ if bytes.starts_with(b"\xff\x0a") {
+ return bytes.to_vec();
+ }
+ let mut raw = Vec::new();
+ let mut at = 12;
+ while at + 8 <= bytes.len() {
+ let len = u32::from_be_bytes(bytes[at..at + 4].try_into().unwrap()) as usize;
+ let end = if len == 0 { bytes.len() } else { at + len };
+ match &bytes[at + 4..at + 8] {
+ b"jxlc" => raw.extend_from_slice(&bytes[at + 8..end]),
+ b"jxlp" => raw.extend_from_slice(&bytes[at + 12..end]),
+ _ => {}
+ }
+ at = end;
+ }
+ assert!(raw.starts_with(b"\xff\x0a"));
+ raw
+}
+
+#[test]
+fn tiny_jxl_tail_does_not_allocate_a_tail_sized_buffer() {
+ let image = Rgba8::from_fn(8, 8, |x, y| [(x * 20) as u8, (y * 20) as u8, 80, 255]);
+ let encoded = encode_jxl(&EncodeImage::rgba8(&image), &JxlOptions { lossless: true, ..Default::default() }, &EncodeMeta::default()).unwrap();
+ let mut raw = codestream(&encoded);
+ let opts = DecodeOptions { max_pixels: 64, ..Default::default() };
+ decode(&raw, opts).unwrap();
+ raw.resize(raw.len() + (1 << 20), 0);
+ let _profiler = dhat::Profiler::builder().testing().build();
+ let decoded = decode(&raw, opts).unwrap();
+ assert_eq!((decoded.width, decoded.height), (8, 8));
+ let peak = dhat::HeapStats::get().max_bytes;
+ eprintln!("peak heap bytes: {peak}");
+ assert!(peak < (128 << 10), "tiny image used {peak} heap bytes");
+}
diff --git a/crates/codecs/tests/roundtrip.rs b/crates/codecs/tests/roundtrip.rs
index af689ae5d..d802366c3 100644
--- a/crates/codecs/tests/roundtrip.rs
+++ b/crates/codecs/tests/roundtrip.rs
@@ -263,6 +263,90 @@ fn tiff_cmyk_and_orientation() {
assert!(red[0] > 0.99 && red[1] < 0.01 && red[2] < 0.01, "{red:?}");
}
+/// An uncompressed 16-bit RGB export in a single strip, the shape of the file in #1983 (3923 × 5877,
+/// 138 MB). Its one strip is larger than the `tiff` crate's default 128 MiB intermediate buffer, so
+/// the thumbnail (and the full decode) failed with "decoder limits exceeded".
+#[test]
+fn tiff_single_strip_larger_than_128_mib() {
+ use tiff::encoder::{TiffEncoder, colortype};
+ let (w, h) = (3923u32, 5877u32);
+ let mut cur = std::io::Cursor::new(Vec::new());
+ {
+ let mut enc = TiffEncoder::new(&mut cur).unwrap();
+ let mut im = enc.new_image::(w, h).unwrap();
+ im.rows_per_strip(h).unwrap();
+ im.write_data(&vec![0x8000u16; (w * h * 3) as usize]).unwrap();
+ }
+ let bytes = cur.into_inner();
+ assert!(bytes.len() > 128 << 20);
+ let t = decode_thumbnail(&bytes, 256).unwrap();
+ assert_eq!((t.source_width, t.source_height), (w, h));
+ let px = t.image.get(10, 10);
+ assert!(px[0].abs_diff(128) <= 2 && px[0] == px[1] && px[1] == px[2], "{px:?}");
+}
+
+/// A little-endian TIFF with one IFD of single-value `(tag, type, value)` entries, followed by `data`.
+fn ifd_tiff(entries: &[(u16, u16, u32)], data: &[u8]) -> Vec {
+ let mut b = b"II*\0".to_vec();
+ b.extend(8u32.to_le_bytes());
+ b.extend((entries.len() as u16).to_le_bytes());
+ for &(tag, ty, value) in entries {
+ b.extend(tag.to_le_bytes());
+ b.extend(ty.to_le_bytes());
+ b.extend(1u32.to_le_bytes());
+ b.extend(value.to_le_bytes());
+ }
+ b.extend(0u32.to_le_bytes());
+ b.extend_from_slice(data);
+ b
+}
+
+/// A strip or tile can be larger than the pixels it shows (a JPEG header, padding to the tile size), so
+/// a small `max_pixels` must not shrink the intermediate buffer below the `tiff` crate's 128 MiB
+/// default. Both files below were refused with "decoder limits exceeded" when the buffer was capped at
+/// 32 bytes per allowed pixel. Fixtures from AlexChambers' review of #1989.
+#[test]
+fn tiff_small_pixel_budget_reads_headers_and_padded_tiles() {
+ let opts = |max_pixels| DecodeOptions { max_size: None, max_pixels };
+
+ // 1 × 1 gray, one JPEG-compressed strip (~400 bytes for one pixel), with a one-pixel budget.
+ let mut jpeg = Vec::new();
+ jpeg_encoder::Encoder::new(&mut jpeg, 90).encode(&[128], 1, 1, jpeg_encoder::ColorType::Luma).unwrap();
+ let at = 8 + 2 + 10 * 12 + 4;
+ let entries = [
+ (256, 4, 1),
+ (257, 4, 1),
+ (258, 3, 8),
+ (259, 3, 7),
+ (262, 3, 1),
+ (273, 4, at),
+ (277, 3, 1),
+ (278, 4, 1),
+ (279, 4, jpeg.len() as u32),
+ (284, 3, 1),
+ ];
+ let d = decode(&ifd_tiff(&entries, &jpeg), opts(1)).unwrap();
+ assert_eq!((d.image.width, d.image.height), (1, 1));
+
+ // 16 × 16 gray stored in one 256 × 256 tile (64 KiB), with a budget of exactly 256 pixels.
+ let at = 8 + 2 + 11 * 12 + 4;
+ let entries = [
+ (256, 4, 16),
+ (257, 4, 16),
+ (258, 3, 8),
+ (259, 3, 1),
+ (262, 3, 1),
+ (277, 3, 1),
+ (284, 3, 1),
+ (322, 4, 256),
+ (323, 4, 256),
+ (324, 4, at),
+ (325, 4, 65536),
+ ];
+ let d = decode(&ifd_tiff(&entries, &[128; 65536]), opts(256)).unwrap();
+ assert_eq!((d.image.width, d.image.height), (16, 16));
+}
+
/// A one-strip, uncompressed, single-sample TIFF (little-endian) whose samples are `bits` wide and
/// already packed into `strip`, one padded row after another.
fn packed_tiff(width: u16, height: u16, bits: u16, photometric: u16, strip: &[u8]) -> Vec {
@@ -734,6 +818,21 @@ fn jxl_lossless_exact_with_metadata() {
assert!(max_diff(&d.image, &want.image) < 1e-5, "16-bit lossless differs by {}", max_diff(&d.image, &want.image));
}
+/// #1517: the pixel limit rejects an oversized image; selected truncations never panic.
+#[test]
+#[cfg(all(feature = "jxl", not(target_arch = "wasm32")))]
+fn jxl_size_limit_and_truncation() {
+ let b = encode_jxl(&EncodeImage::rgba8(&gradient(97, 61)), &JxlOptions { lossless: true, ..Default::default() }, &EncodeMeta::default()).unwrap();
+ let r = decode(&b, DecodeOptions { max_pixels: 97 * 61 - 1, ..Default::default() });
+ assert!(matches!(r, Err(Error::TooLarge(97, 61))), "{r:?}");
+ let d = decode(&b, DecodeOptions { max_pixels: 97 * 61, ..Default::default() }).unwrap();
+ assert_eq!((d.width, d.height), (97, 61));
+ for cut in [0, 1, 2, 5, 12, 40, b.len() / 2, b.len() - 1] {
+ let _ = decode(&b[..cut], DecodeOptions::default());
+ }
+ assert!(decode(&b[..12.min(b.len())], DecodeOptions::default()).is_err());
+}
+
#[test]
#[cfg(feature = "jxl")]
fn jxl_lossy_quality_and_effort() {
diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs
index 855c3d049..89fe93c9f 100644
--- a/crates/color/src/lib.rs
+++ b/crates/color/src/lib.rs
@@ -180,8 +180,8 @@ pub struct Rgb8(pub u8, pub u8, pub u8);
impl Rgb8 {
pub fn from_hex(s: &str) -> Option {
- let s = s.trim_start_matches('#');
- if s.len() != 6 {
+ let s = s.strip_prefix('#').unwrap_or(s);
+ if s.len() != 6 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let v = u32::from_str_radix(s, 16).ok()?;
@@ -196,6 +196,15 @@ impl Rgb8 {
mod tests {
use super::*;
+ #[test]
+ fn rgb8_from_hex_rejects_signs_and_extra_hashes() {
+ assert_eq!(Rgb8::from_hex("#0f0f0f"), Some(Rgb8(15, 15, 15)));
+ assert_eq!(Rgb8::from_hex("ff8000"), Some(Rgb8(255, 128, 0)));
+ for bad in ["+12345", "#+12345", "#+f+f+f", "-12345", "##123456", "#12345", ""] {
+ assert_eq!(Rgb8::from_hex(bad), None, "{bad}");
+ }
+ }
+
#[test]
fn srgb_matrix_matches_standard() {
let m = SRGB.to_xyz();
diff --git a/crates/denoise-core/src/run.rs b/crates/denoise-core/src/run.rs
index 987a859b9..4314ed8c3 100644
--- a/crates/denoise-core/src/run.rs
+++ b/crates/denoise-core/src/run.rs
@@ -14,7 +14,7 @@ use crate::manifest::Gain;
use crate::tiles::{starts, weight};
/// Most pixels one picture may have (guards the allocations that follow from its size).
-pub const MAX_PIXELS: usize = 100_000_000;
+pub const MAX_PIXELS: usize = 110_000_000;
/// A tile whose mean signal is below this (white = 1) is too dark to measure its scale from.
const MIN_SIGNAL: f32 = 0.003;
@@ -642,4 +642,19 @@ mod tests {
assert!(matches!(denoise_bayer(&[0.5; 16], 4, 4, layout, &Bad(vec![0.0; 5]), &p, &Control::default()), Err(Error::Model(_))));
assert!(matches!(denoise_bayer(&[0.5; 16], 4, 4, layout, &Bad(vec![f32::NAN; 3 * 64 * 64]), &p, &Control::default()), Err(Error::Model(_))));
}
+
+ #[test]
+ fn accepts_102_mp_inputs_and_rejects_inputs_above_the_limit() {
+ let layout = Layout { dx: 0, dy: 0 };
+ let model = Mock { tile: 32, scale: 1.0 };
+ let p = params(32, 8, Gain::None);
+ assert!(matches!(
+ denoise_bayer(&[], 11_648, 8_736, layout, &model, &p, &Control::default()),
+ Err(Error::Input(message)) if message == "the mosaic does not have width × height samples"
+ ));
+ assert!(matches!(
+ denoise_bayer(&[], MAX_PIXELS + 1, 1, layout, &model, &p, &Control::default()),
+ Err(Error::Input(message)) if message == "the picture has no size, or is too large"
+ ));
+ }
}
diff --git a/crates/develop/src/lib.rs b/crates/develop/src/lib.rs
index f376b3c67..4631e5e49 100644
--- a/crates/develop/src/lib.rs
+++ b/crates/develop/src/lib.rs
@@ -41,7 +41,7 @@ impl DevelopSettings {
/// Merge a partial JSON object into these settings (fields not mentioned are kept). A white balance that
/// names no [`WbScale`] keeps the meaning it always had (`Legacy`), whatever scale these settings were on: a
/// partial from before the scale existed, or one that never said, must not become Kelvin by landing on
- /// Kelvin settings.
+ /// Kelvin settings. Out-of-range values are brought back into range ([`DevelopSettings::keep_in_range`]).
pub fn merged(&self, partial: &Value) -> Result {
let mut v = self.to_json();
presets::deep_merge(&mut v, partial);
@@ -52,9 +52,34 @@ impl DevelopSettings {
{
out.wb.scale = WbScale::Legacy;
}
+ out.keep_in_range(self);
Ok(out)
}
+ /// What crs XMP import does to the values it reads, for settings that arrive as JSON (presets, `develop.merge`,
+ /// `render --settings`; issue #1946): every slider clamped through its control spec, point curves in 0..1, and a
+ /// crop with no area left as it was in `before`. Unrotated, the crop is clipped to the photo first; a turned crop
+ /// may reach past 0..1 and is left to the crop tools, which know the photo's size.
+ fn keep_in_range(&mut self, before: &DevelopSettings) {
+ for c in controls::CONTROLS {
+ if let Some(v) = controls::get(self, c.id) {
+ controls::set(self, c.id, v);
+ }
+ }
+ let c = &mut self.curve;
+ for p in [&mut c.master, &mut c.red, &mut c.green, &mut c.blue].into_iter().flatten() {
+ *p = lightcraft_geom::Point::new(p.x.clamp(0.0, 1.0), p.y.clamp(0.0, 1.0));
+ }
+ let g = &mut self.crop.geometry;
+ if g.angle == 0.0 {
+ g.rect = g.rect.intersect(&lightcraft_geom::Rect::UNIT);
+ }
+ if g.rect.is_empty() {
+ let old = before.crop.geometry.rect;
+ g.rect = if old.is_empty() { lightcraft_geom::Rect::UNIT } else { old };
+ }
+ }
+
/// Stable 64-bit hash of the settings (FNV-1a over canonical JSON), for preview cache keys.
pub fn hash64(&self) -> u64 {
let s = serde_json::to_string(self).unwrap_or_default();
@@ -255,6 +280,30 @@ mod tests {
assert_eq!(m.light.exposure, 0.0);
}
+ #[test]
+ fn merged_settings_are_kept_in_range() {
+ // issue #1946: `develop.merge`, `render --settings` and presets stored whatever the JSON said
+ let mut base = DevelopSettings::default();
+ base.crop.geometry.rect = lightcraft_geom::Rect::new(0.1, 0.2, 0.8, 0.9);
+ let m = base
+ .merged(&json!({"light": {"exposure": 1e308}, "vignette": {"amount": -1e308}, "geometry": {"scale": 0}, "optics": {"distortion": 1e308}}))
+ .unwrap();
+ assert_eq!((m.light.exposure, m.vignette.amount, m.geometry.scale, m.optics.distortion), (5.0, -100.0, 50.0, 100.0));
+ // point curves stay in 0..1, as an imported XMP curve does
+ let m = base.merged(&json!({"curve": {"master": [{"x": -3, "y": 0}, {"x": 1, "y": 1e308}]}})).unwrap();
+ assert_eq!(m.curve.master, vec![lightcraft_geom::Point::new(0.0, 0.0), lightcraft_geom::Point::new(1.0, 1.0)]);
+ // a crop with no area is left out, as XMP import leaves out one whose edges are inverted
+ for rect in
+ [json!({"x0": 0.9, "x1": 0.1, "y0": 0, "y1": 1}), json!({"x0": 1e308, "x1": -1e308, "y0": 0, "y1": 1}), json!({"x0": 0.5, "x1": 0.5})]
+ {
+ let m = base.merged(&json!({"crop": {"geometry": {"rect": rect}}})).unwrap();
+ assert_eq!(m.crop.geometry.rect, base.crop.geometry.rect, "{rect}");
+ }
+ // unrotated, the crop stays inside the photo
+ let m = base.merged(&json!({"crop": {"geometry": {"rect": {"x0": -1e308, "y0": 0.25, "x1": 1e308, "y1": 0.75}}}})).unwrap();
+ assert_eq!(m.crop.geometry.rect, lightcraft_geom::Rect::new(0.0, 0.25, 1.0, 0.75));
+ }
+
#[test]
fn unedited_and_reset() {
let mut s = DevelopSettings::default();
diff --git a/crates/develop/src/presets.rs b/crates/develop/src/presets.rs
index acaa303fc..b3edc801c 100644
--- a/crates/develop/src/presets.rs
+++ b/crates/develop/src/presets.rs
@@ -10,6 +10,7 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
+use std::borrow::Cow;
use crate::DevelopSettings;
@@ -183,18 +184,11 @@ fn scale_patch(base: &Value, patch: &Value, t: f64) -> Value {
}
/// Apply a partial settings object with an amount (1.0 = 100 %). Out-of-range values are clamped
-/// through the control specs.
+/// through the control specs, as every merge does ([`DevelopSettings::merged`]).
pub fn apply_partial(s: &DevelopSettings, partial: &Value, amount: f64) -> DevelopSettings {
- let base = s.to_json();
- let patch = if (amount - 1.0).abs() < 1e-9 { partial.clone() } else { scale_patch(&base, partial, amount) };
+ let patch = if (amount - 1.0).abs() < 1e-9 { Cow::Borrowed(partial) } else { Cow::Owned(scale_patch(&s.to_json(), partial, amount)) };
// (a white balance without a scale keeps its old meaning: `DevelopSettings::merged`)
- let mut out = s.merged(&patch).unwrap_or_else(|_| s.clone());
- for c in crate::controls::CONTROLS {
- if let Some(v) = crate::controls::get(&out, c.id) {
- crate::controls::set(&mut out, c.id, v);
- }
- }
- out
+ s.merged(&patch).unwrap_or_else(|_| s.clone())
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
diff --git a/crates/develop/src/settings.rs b/crates/develop/src/settings.rs
index 1895776b3..a40f094c2 100644
--- a/crates/develop/src/settings.rs
+++ b/crates/develop/src/settings.rs
@@ -900,11 +900,24 @@ pub struct BrushStroke {
pub density: f64,
pub erase: bool,
pub auto_mask: bool,
+ /// Pen pressure (0..1) at each of `points`, recorded when the brush's pressure switch is on: it scales each dab's
+ /// radius and flow. Empty (the default, a mouse or the switch off) is full pressure everywhere; a point past the
+ /// end of a shorter list takes the last value.
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub pressure: Vec