mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 20:48:33 -06:00
+1
-1
@@ -23,7 +23,7 @@ duplicate Local entries #22, black GPU exports on an Intel iGPU #78).
|
||||
| Dimension | Estimate | What's true today | Biggest gaps |
|
||||
|---|---:|---|---|
|
||||
| **Feature checklist** | 79.7% | P0 core and P1 nearly complete: import (Add / Copy / Move, templates, devices), library, grid/loupe/compare/survey, every Edit slider, curves, colour grading, masking tools, crop/Upright, heal/clone, presets/profiles, versions/history, sync, export, menus, shortcuts | P0: full-resolution zoom detail; P1: lens-profile database, content-aware fill (patch synthesis), video playback/trim |
|
||||
| **RAW coverage** (formats people shoot) | ~55% | DNG (all kinds), CR2, CR3 lossless Bayer and version 0x100/0x200 C-RAW (M50/R100/R8 pixel-exact corpus), ARW, NEF (uncompressed + Huffman lossless/lossy), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans), uncompressed ORF, uncompressed Samsung SRW (NX5 to NX1100, EX1, WB2000), RW2 / RWL / Panasonic RAW (every raw format, checked on 178 files from 118 Panasonic and Leica bodies), PEF; every container's embedded preview (incl. CR3) | **CR3** (every Canon since ~2018), compressed ORF, compressed Samsung SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini), Nikon lossy-after-split, Canon sRAW, HEIC/AVIF. NEFs labelled compressed but stored uncompressed (Z 6 packed 14-bit, D850 12-bit uncompressed). Per-model verification is thin (~55 corpus files vs >1,000 models) |
|
||||
| **RAW coverage** (formats people shoot) | ~55% | DNG (all kinds), CR2, CR3 lossless Bayer and version 0x100/0x200 C-RAW (M50/R100/R8 pixel-exact corpus), ARW, NEF (uncompressed + Huffman lossless/lossy), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans), uncompressed ORF, uncompressed Samsung SRW (NX5 to NX1100, EX1, WB2000), RW2 / RWL / Panasonic RAW (every raw format, checked on 178 files from 118 Panasonic and Leica bodies), PEF; every container's embedded preview (incl. CR3) | **CR3** (every Canon since ~2018), compressed ORF, compressed Samsung SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini), Nikon lossy-after-split, Canon sRAW, HEIC/AVIF. NEFs labelled compressed but stored uncompressed (Z 6 packed 14-bit, D850 12-bit uncompressed). Nikon High Efficiency NEF (HE / HE★: Z 8, Z 9, Z 6III, Z f; JPEG XS-based, preview only, #193). Per-model verification is thin (~55 corpus files vs >1,000 models) |
|
||||
| **Colour & image quality** | ~55–65% | Pipeline is complete and fast; GPU path CPU-exact within 1/255 | **No measured camera calibration database**: ARW, NEF, RW2, RAF, CR3, CR2 and PEF have guarded camera-JPEG colour estimates, with bundled ILCE-7M4, X-H2S and X-T4 profiles (docs/camera-preview-colour.md); other non-DNG raws and rejected estimates use a neutral matrix. Colour fidelity remains incomplete. No lens-profile database. No measured fidelity against Lightroom (tone, highlights, texture/clarity, NR, sharpening are tuned by eye) |
|
||||
| **AI & computational** | ~15–20% | Assisted culling (focus, bursts), auto tone, HDR/panorama merge; subject/sky/background masks as classical heuristics, or the photo's own segmentation mattes when it ships them (DNG semantic masks, e.g. iPhone ProRAW's sky matte); non-destructive Bayer RAW denoise with opt-in weights on pure-Rust CPU/GPU; **faces (MVP, [docs/faces.md](docs/faces.md))**: a pure-Rust detector (a 232 KB opt-in download), opt-in recognition models (one-click download or add a file; they run on the shared pure-Rust CPU interpreter; pinned SFace/AuraFace embeddings agree with an independent reference runtime on 40 public crops; one-thread medians 34–36 / 233–252 ms (AuraFace remains expensive)), a background scan that finds and embeds faces, name suggestions, a person's page with look-alike faces to confirm, an Unnamed faces section (look-alikes together, select a group and name it at once) | Real segmentation masks (subject, sky, people, objects, landscape, depth), denoise model policy / linear RGB / quality verification, super resolution, lens blur, generative remove, natural-language search; for faces: clusters with separators between groups, small faces in large photos (the detector looks at 640 px), thresholds calibrated on live faces, writing face regions back to XMP. **Blocked on a model strategy** (licensable weights or our own training; pure-Rust inference is feasible) |
|
||||
| **Workflow & library** | ~85% (single machine) | Robust catalog (journal + snapshots, background compaction, crash-tested), 85k-photo libraries stay responsive, Local browsing with automatic cleanup, XMP interop, keywords, smart albums, Move import | Opening an 85k library takes 1.7–4.7 s; no cloud sync (out of scope), no tablet companion (#74, roadmap), shared albums, publish services, tethering |
|
||||
|
||||
Vendored
+34
-2
@@ -4,7 +4,9 @@
|
||||
//! layout, SubIFDs, maker note header) and the ExifTool Nikon tag-name documentation (`0x000c` WB_RBLevels,
|
||||
//! `0x003d` BlackLevel, `0x0096` NEFLinearizationTable). The Huffman-compressed data (compression 34713) is decoded
|
||||
//! by [`super::nefc`], which documents its clean-room sources; files it can't decode yet ("lossy after split")
|
||||
//! are reported as [`RawError::Unsupported`] and their embedded previews still work.
|
||||
//! are reported as [`RawError::Unsupported`] and their embedded previews still work. So are High Efficiency NEFs
|
||||
//! (HE / HE★, Z 8, Z 9, Z 6III, Z f): they keep compression 34713 but carry a wavelet codestream that starts with
|
||||
//! the JPEG XS markers SOC + CAP (ISO/IEC 21122-1, `FF10 FF50`) and have no `0x0096` table (issue #193).
|
||||
|
||||
use super::{nefc, white_from_data};
|
||||
use crate::tiffraw::{Packing, read_image};
|
||||
@@ -18,6 +20,11 @@ const WB_RB_LEVELS: u16 = 0x000c;
|
||||
const BLACK_LEVEL: u16 = 0x003d;
|
||||
const LINEARIZATION_TABLE: u16 = 0x0096;
|
||||
const CROP_AREA: u16 = 0x0045;
|
||||
/// JPEG XS start of codestream (SOC, `FF10`) followed by its mandatory capabilities marker (CAP, `FF50`).
|
||||
const JPEG_XS_START: [u8; 4] = [0xff, 0x10, 0xff, 0x50];
|
||||
/// Why a High Efficiency NEF shows its embedded preview: the UI shows the part before " (".
|
||||
pub(crate) const HIGH_EFFICIENCY: &str =
|
||||
"Nikon High Efficiency NEF (HE / HE★, a JPEG XS-based codec with no public specification; shoot RAW Lossless compressed to edit the raw data)";
|
||||
/// Maker note `0x0014`, the "NRW" data block of Coolpix raws: `"NRW "` + a 4-character version, then fields. In
|
||||
/// version `0104` the `u32` at byte `0x20` is the black level in sample units (200 in the four 12-bit files here
|
||||
/// that have 200 as their darkest samples, 0 in the one whose samples reach 0).
|
||||
@@ -166,6 +173,13 @@ fn uncompressed(bytes: &[u8], info: &ImageInfo, order: ByteOrder, mn: Option<&ma
|
||||
|
||||
/// Nikon Huffman-compressed strip (see [`super::nefc`]); the decode table is maker note `0x0096`.
|
||||
fn compressed(bytes: &[u8], info: &ImageInfo, mn: Option<&makernote::MakerNote>) -> Result<RawData> {
|
||||
let starts_with = |magic: &[u8]| {
|
||||
let start = info.chunks(bytes.len() as u64).first().and_then(|c| usize::try_from(c.offset).ok());
|
||||
start.and_then(|o| bytes.get(o..o.checked_add(magic.len())?)) == Some(magic)
|
||||
};
|
||||
if starts_with(&JPEG_XS_START) {
|
||||
return Err(RawError::Unsupported(HIGH_EFFICIENCY.into()));
|
||||
}
|
||||
// (some Z bodies label uncompressed, row-padded data 34713 without a table: not handled here yet)
|
||||
let table = mn
|
||||
.and_then(|m| Some((m.ifd.bytes(LINEARIZATION_TABLE)?, m.order)))
|
||||
@@ -354,6 +368,24 @@ mod tests {
|
||||
#[test]
|
||||
fn compressed_without_table_is_unsupported() {
|
||||
let bytes = nef(34713, 14, vec![vec![0; 64]], 8, 8, 8);
|
||||
assert!(matches!(crate::decode(&bytes), Err(RawError::Unsupported(_))));
|
||||
let Err(RawError::Unsupported(why)) = crate::decode(&bytes) else { panic!("expected unsupported") };
|
||||
assert!(why.contains("linearization table"), "{why}");
|
||||
}
|
||||
|
||||
/// Issue #193: a High Efficiency NEF (Z f, Z 8: compression 34713, no `0x0096` table, a JPEG XS codestream) is
|
||||
/// named as such instead of being blamed on the missing table, and the header probe reports the same reason.
|
||||
#[test]
|
||||
fn high_efficiency_nef_is_named() {
|
||||
let mut strip = vec![0xff, 0x10, 0xff, 0x50, 0x00, 0x22];
|
||||
strip.resize(64, 0);
|
||||
let bytes = nef(34713, 14, vec![strip], 8, 8, 8);
|
||||
for result in [crate::decode(&bytes).map(|_| ()), crate::probe_info(&bytes).map(|_| ())] {
|
||||
let Err(RawError::Unsupported(why)) = result else { panic!("expected unsupported") };
|
||||
assert_eq!(why, HIGH_EFFICIENCY);
|
||||
assert!(why.starts_with("Nikon High Efficiency NEF ("), "{why}");
|
||||
}
|
||||
// a strip too short to hold the markers is still the missing-table case, not a panic
|
||||
let short = nef(34713, 14, vec![vec![0xff, 0x10]], 1, 1, 1);
|
||||
assert!(matches!(crate::decode(&short), Err(RawError::Unsupported(w)) if w.contains("linearization table")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ fn corpus_root() -> PathBuf {
|
||||
const KNOWN_UNSUPPORTED: &[&str] = &[
|
||||
"cr3-", // Unverified CRX coding variants; exact supported cases live in cr3_corpus.rs.
|
||||
"orf-olympus-em", // Olympus compressed ORF
|
||||
"-he.nef", // Nikon High Efficiency (HE / HE★) NEF, issue #193
|
||||
"sraw", // Canon sRAW / mRAW
|
||||
];
|
||||
|
||||
@@ -224,6 +225,24 @@ fn corpus_nef_compressed_matches_uncompressed() {
|
||||
|
||||
/// raw.pixls.us has the same D7500 scene as 12- and 14-bit lossless compressed NEF. Both store black level 400 in
|
||||
/// maker note 0x003d (14-bit units): after subtracting the black level and scaling to white, the two must agree.
|
||||
/// Issue #193: the Z f's High Efficiency NEF (no `0x0096` table, a JPEG XS codestream) is named as such and keeps a
|
||||
/// full-size embedded JPEG to edit, while the same body's Lossless compressed NEF decodes.
|
||||
#[test]
|
||||
fn corpus_nef_high_efficiency_is_named() {
|
||||
let dir = corpus_root().join("raw");
|
||||
let (Ok(he), Ok(lossless)) = (std::fs::read(dir.join("nef-nikon-zf-he.nef")), std::fs::read(dir.join("nef-nikon-zf-lossless.nef"))) else {
|
||||
eprintln!("skip: Z f samples absent");
|
||||
return;
|
||||
};
|
||||
for result in [decode(&he).map(|_| ()), probe_info(&he).map(|_| ())] {
|
||||
let Err(RawError::Unsupported(why)) = result else { panic!("Z f HE: expected unsupported") };
|
||||
assert!(why.starts_with("Nikon High Efficiency NEF ("), "{why}");
|
||||
}
|
||||
assert!(embedded_preview(&he).is_some_and(|p| p.len() > 1 << 20), "Z f HE: no full-size preview");
|
||||
let img = decode(&lossless).unwrap();
|
||||
assert_eq!((img.width, img.height, img.bits), (6064, 4040, 14));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corpus_nef_12_bit_black_level_matches_14_bit() {
|
||||
let dir = corpus_root().join("raw");
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -503,6 +503,16 @@ const RAW_SAMPLES: &[(&str, &str, &str)] = &[
|
||||
"https://raw.pixls.us/getfile.php/1534/nice/Nikon%20-%20D7500%20-%2014bit%2014bit%20compressed%20%28Lossless%29%20%283:2%29.NEF",
|
||||
"430b4f1be4e53a011861b63294ad19fdc0353ec3ea62104f77d4c193e3dd3fc8",
|
||||
),
|
||||
(
|
||||
"nef-nikon-zf-he.nef",
|
||||
"https://raw.pixls.us/download/data/Nikon/Z%20f/DSC_0043.NEF",
|
||||
"98d6ca8e6c98048ca7ffed68ccaeda7b2b9f03807f0d320d97d5678db21748c2",
|
||||
),
|
||||
(
|
||||
"nef-nikon-zf-lossless.nef",
|
||||
"https://raw.pixls.us/download/data/Nikon/Z%20f/DSC_0040.NEF",
|
||||
"83c82be0be8865d796096dfbcc8ef2abf5af1bd37db44dfad6715070b0c99d15",
|
||||
),
|
||||
(
|
||||
"nrw-nikon-b700-uncompressed.nrw",
|
||||
"https://raw.pixls.us/getfile.php/1621/nice/Nikon%20-%20COOLPIX%20B700%20-%2012bit%2012bit%20uncompressed%20%284:3%29.NRW",
|
||||
|
||||
Reference in New Issue
Block a user