mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 13:07:43 -06:00
@@ -159,9 +159,9 @@ pub fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
/// measure them from the samples).
|
||||
///
|
||||
/// A few uncompressed vendor formats derive part of this from the samples themselves (Nikon
|
||||
/// NEF: optically masked trailing columns; Olympus ORF: the CFA phase and bit depth; Pentax PEF
|
||||
/// without crop tags: dark borders); for those the samples are read (unpacked, nothing to
|
||||
/// decompress) and dropped.
|
||||
/// NEF: optically masked trailing columns; Olympus ORF: the bit depth of 16-bit files, and the CFA
|
||||
/// phase of files without an Exif `CFAPattern`; Pentax PEF without crop tags: dark borders); for
|
||||
/// those the samples are read (unpacked, nothing to decompress) and dropped.
|
||||
pub fn probe_info(bytes: &[u8]) -> Result<RawInfo> {
|
||||
decode_with(bytes, Mode::Header).map(RawImage::into_info)
|
||||
}
|
||||
@@ -185,7 +185,7 @@ fn decode_with(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
RawFormat::Raf => vendor::raf::decode(bytes, mode),
|
||||
RawFormat::Rw2 => vendor::rw2::decode(bytes, mode),
|
||||
RawFormat::Pef => vendor::pef::decode(bytes, mode),
|
||||
RawFormat::Orf => vendor::orf::decode(bytes),
|
||||
RawFormat::Orf => vendor::orf::decode(bytes, mode),
|
||||
other => Err(RawError::Unsupported(format!("{other:?} files are not decoded yet"))),
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+178
-24
@@ -1,10 +1,10 @@
|
||||
//! Olympus ORF — uncompressed variants.
|
||||
//!
|
||||
//! Sources: TIFF 6.0 (the `IIRO`/`MMOR` container is a TIFF with a different magic number), the ExifTool
|
||||
//! Olympus tag-name documentation (maker-note sub-directories `0x2020` CameraSettings — `0x0101/0x0102`
|
||||
//! PreviewImageStart/Length — and `0x2040` ImageProcessing — `0x0100` WB_RBLevels, `0x0600` BlackLevel2,
|
||||
//! `0x0612–0x0615` CropLeft/Top/Width/Height) and our own black-box analysis of CC0 samples from raw.pixls.us
|
||||
//! (E-1, E-400, XZ-2):
|
||||
//! Sources: TIFF 6.0 (the `IIRO`/`MMOR` container is a TIFF with a different magic number), Exif 2.3 (`CFAPattern`,
|
||||
//! tag `0xa302`), the ExifTool Olympus tag-name documentation (maker-note sub-directories `0x2020` CameraSettings —
|
||||
//! `0x0101/0x0102` PreviewImageStart/Length — and `0x2040` ImageProcessing — `0x0100` WB_RBLevels, `0x0600`
|
||||
//! BlackLevel2, `0x0612–0x0615` CropLeft/Top/Width/Height) and our own black-box analysis of CC0 samples from
|
||||
//! raw.pixls.us (E-1, E-400, XZ-2):
|
||||
//!
|
||||
//! - 16 bits per sample: little-endian words; some bodies (E-1, E-400) store 12-bit values in the top bits (the low
|
||||
//! four bits zero in more than 99% of samples), which we shift down.
|
||||
@@ -12,14 +12,16 @@
|
||||
//! candidate bit orders for the smoothest image).
|
||||
//! - Olympus's compressed ORF (most interchangeable-lens bodies since ~2008) is not decoded: no permissively
|
||||
//! licensed description exists. It reports [`RawError::Unsupported`]; the embedded preview still works.
|
||||
//! - The files carry no CFA tag. The green diagonal is found from the data (the diagonal whose two samples differ
|
||||
//! least); bodies with greens on the main diagonal (E-1, E-400) are GRBG, the others (XZ-2) RGGB — verified by
|
||||
//! colour renders.
|
||||
//! - The colour-filter layout is the file's Exif `CFAPattern`: GRBG on the E-1 and E-400, RGGB on the XZ-2, where
|
||||
//! the colours of the decoded mosaic follow the embedded JPEG best with exactly that layout
|
||||
//! (`corpus_orf_cfa_patterns`). The samples' active areas start at even offsets, so they can't tell whether the
|
||||
//! pattern is anchored at the sensor origin (assumed) or at the active area. Files without a usable tag fall back
|
||||
//! to the green diagonal found from the data, which can't tell red from blue (GRBG or RGGB is assumed).
|
||||
|
||||
use super::white_from_data;
|
||||
use crate::tiffraw::{Packing, read_image};
|
||||
use crate::unpack::unpack_msb;
|
||||
use crate::{BlackLevel, Cfa, ColorData, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use crate::{BlackLevel, Cfa, ColorData, Mode, OpcodeLists, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_tiff::image::chunk_bytes;
|
||||
use lightcraft_tiff::makernote::MakerNote;
|
||||
@@ -33,6 +35,8 @@ pub(crate) const PREVIEW_LENGTH: u16 = 0x0102;
|
||||
const WB_RB: u16 = 0x0100;
|
||||
const BLACK: u16 = 0x0600;
|
||||
const CROP: [u16; 4] = [0x0612, 0x0613, 0x0614, 0x0615];
|
||||
/// Exif `CFAPattern`.
|
||||
const EXIF_CFA_PATTERN: u16 = 0xa302;
|
||||
|
||||
/// The Olympus maker note.
|
||||
pub(crate) fn maker_note(bytes: &[u8], tiff: &Tiff) -> Option<MakerNote> {
|
||||
@@ -66,23 +70,52 @@ pub(crate) fn unpack_row_le32_msb(src: &[u8], bits: u32, out: &mut [u16]) {
|
||||
unpack_msb(&swapped, bits, out);
|
||||
}
|
||||
|
||||
/// GRBG when the greens sit on the main diagonal of the 2×2 cell at the sensor origin, else RGGB.
|
||||
/// The colour-filter layout from the Exif `CFAPattern` tag (`0xa302`: two 16-bit repeat counts, found in either
|
||||
/// byte order, then one byte per site: 0 = red, 1 = green, 2 = blue), when it describes a 2×2 Bayer cell.
|
||||
fn cfa_from_exif(tiff: &Tiff) -> Option<Cfa> {
|
||||
let &[c0, c1, r0, r1, s0, s1, s2, s3] = tiff.exif()?.bytes(EXIF_CFA_PATTERN)? else { return None };
|
||||
let two = |a: u8, b: u8| matches!((a, b), (2, 0) | (0, 2));
|
||||
let name = match [s0, s1, s2, s3] {
|
||||
[0, 1, 1, 2] => "RGGB",
|
||||
[2, 1, 1, 0] => "BGGR",
|
||||
[1, 0, 2, 1] => "GRBG",
|
||||
[1, 2, 0, 1] => "GBRG",
|
||||
_ => return None,
|
||||
};
|
||||
(two(c0, c1) && two(r0, r1)).then(|| Cfa::bayer_static(name))
|
||||
}
|
||||
|
||||
/// The layout found from the samples, for files without the Exif tag: GRBG when the greens sit on the main
|
||||
/// diagonal of the 2×2 cell at the sensor origin, else RGGB. Over 32×32-pixel blocks of `a` (every fourth in both
|
||||
/// directions) it compares the block totals of the two sites on each diagonal: the two greens of a block see the
|
||||
/// same light, red and blue rarely do. Totals rather than single pixels, so that fine texture, which makes
|
||||
/// neighbouring greens differ, doesn't outweigh a small difference between red and blue.
|
||||
pub(crate) fn cfa_from_data(d: &[u16], w: usize, a: Rect) -> Cfa {
|
||||
let (x0, y0) = (a.x + ((a.width / 8) & !1), a.y + ((a.height / 8) & !1));
|
||||
let (x1, y1) = (a.x + a.width * 7 / 8, a.y + a.height * 7 / 8);
|
||||
const BLOCK: usize = 32;
|
||||
let (x0, y0) = (a.x.saturating_add(1) & !1, a.y.saturating_add(1) & !1);
|
||||
let across = a.x.saturating_add(a.width).min(w).saturating_sub(x0) / BLOCK;
|
||||
let down = a.y.saturating_add(a.height).saturating_sub(y0) / BLOCK;
|
||||
let (mut main, mut anti) = (0u64, 0u64);
|
||||
for y in (y0..y1.saturating_sub(1)).step_by(16) {
|
||||
for x in (x0..x1.saturating_sub(1)).step_by(8) {
|
||||
let (x, y) = (x & !1, y & !1);
|
||||
let p = |dx: usize, dy: usize| d[(y + dy) * w + x + dx] as i64;
|
||||
main += (p(0, 0) - p(1, 1)).unsigned_abs();
|
||||
anti += (p(1, 0) - p(0, 1)).unsigned_abs();
|
||||
for by in (0..down).step_by(4) {
|
||||
for bx in (0..across).step_by(4) {
|
||||
let mut sums = [0i64; 4];
|
||||
for y in 0..BLOCK {
|
||||
let row = (y0 + by * BLOCK + y).checked_mul(w).and_then(|r| r.checked_add(x0 + bx * BLOCK));
|
||||
// rows past the end of the samples: nothing more to read
|
||||
let Some(row) = row.and_then(|start| d.get(start..start.checked_add(BLOCK)?)) else { break };
|
||||
for (x, &v) in row.iter().enumerate() {
|
||||
sums[(y & 1) * 2 + (x & 1)] += v as i64;
|
||||
}
|
||||
}
|
||||
let [s0, s1, s2, s3] = sums;
|
||||
main += (s0 - s3).unsigned_abs();
|
||||
anti += (s1 - s2).unsigned_abs();
|
||||
}
|
||||
}
|
||||
Cfa::bayer_static(if main < anti { "GRBG" } else { "RGGB" })
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let tiff = Tiff::parse(bytes)?;
|
||||
let ifd0 = &tiff.ifds[0];
|
||||
let info = ifd0.image()?;
|
||||
@@ -90,6 +123,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
let n = w.checked_mul(h).filter(|n| *n > 0 && *n <= crate::MAX_SAMPLES).ok_or(RawError::Limit("image too large"))?;
|
||||
let chunks = info.chunks(bytes.len() as u64);
|
||||
let total: u64 = chunks.iter().map(|c| c.len).sum();
|
||||
let stated = cfa_from_exif(&tiff);
|
||||
let (mut data, bits) = if info.compression != 1 {
|
||||
return Err(RawError::Unsupported(format!("ORF compression {}", info.compression)));
|
||||
} else if total >= (n as u64) * 2 {
|
||||
@@ -97,9 +131,16 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
(d, 16)
|
||||
} else if total * 8 >= (n as u64) * 12 && total * 8 < (n as u64) * 13 && chunks.len() == 1 {
|
||||
let src = chunk_bytes(bytes, &chunks[0]).ok_or_else(|| RawError::Corrupt("ORF strip outside file".into()))?;
|
||||
let stride = src.len() / h;
|
||||
let mut d = vec![0u16; n];
|
||||
d.par_chunks_mut(w).enumerate().for_each(|(y, row)| unpack_row_le32_msb(&src[y * stride..(y + 1) * stride], 12, row));
|
||||
// the depth is fixed here, so a header-only probe needs the samples only when the file doesn't state its
|
||||
// colour-filter layout
|
||||
let d = if mode == Mode::Full || stated.is_none() {
|
||||
let stride = src.len() / h;
|
||||
let mut d = vec![0u16; n];
|
||||
d.par_chunks_mut(w).enumerate().for_each(|(y, row)| unpack_row_le32_msb(&src[y * stride..(y + 1) * stride], 12, row));
|
||||
d
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
(RawData::U16(d), 12)
|
||||
} else {
|
||||
return Err(RawError::Unsupported("Olympus compressed ORF".into()));
|
||||
@@ -128,7 +169,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
Some([Some(x), Some(y), Some(cw), Some(ch)]) if cw > 0 && ch > 0 && x + cw <= w && y + ch <= h => Rect::new(x, y, cw, ch),
|
||||
_ => Rect::new(0, 0, w, h),
|
||||
};
|
||||
let cfa = cfa_from_data(samples, w, active);
|
||||
let cfa = stated.unwrap_or_else(|| cfa_from_data(samples, w, active));
|
||||
let black = match ip.as_ref().and_then(|i| i.f64s(BLACK)).as_deref() {
|
||||
Some(v @ [_, _, _, _]) => {
|
||||
let a = cfa.shifted(active.x, active.y);
|
||||
@@ -165,7 +206,7 @@ pub(crate) fn decode(bytes: &[u8]) -> Result<RawImage> {
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
};
|
||||
img.validate()?;
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
@@ -185,6 +226,11 @@ mod tests {
|
||||
use lightcraft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter};
|
||||
|
||||
fn orf(w: u32, h: u32, bits: u16, strip: Vec<u8>) -> Vec<u8> {
|
||||
orf_with(w, h, bits, strip, None)
|
||||
}
|
||||
|
||||
/// A minimal ORF with one strip in IFD0 and, optionally, an Exif `CFAPattern`.
|
||||
fn orf_with(w: u32, h: u32, bits: u16, strip: Vec<u8>, cfa_pattern: Option<&[u8]>) -> Vec<u8> {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
|
||||
@@ -193,12 +239,120 @@ mod tests {
|
||||
ifd.set(t::PHOTOMETRIC, Value::Short(vec![1]));
|
||||
ifd.set(t::MAKE, Value::Ascii("OLYMPUS IMAGING CORP.".into()));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![strip] });
|
||||
if let Some(p) = cfa_pattern {
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(EXIF_CFA_PATTERN, Value::Undefined(p.to_vec()));
|
||||
ifd.set_child(t::EXIF_IFD, exif);
|
||||
}
|
||||
let mut b = TiffWriter::new(ByteOrder::Little, false).write(&[ifd]).unwrap();
|
||||
b[2] = b'R';
|
||||
b[3] = b'O';
|
||||
b
|
||||
}
|
||||
|
||||
/// A 12-bit mosaic stored as 16-bit words: `site(cx, cy)` gives the four values of the 2×2 cell at (cx, cy).
|
||||
fn mosaic(w: usize, h: usize, site: impl Fn(usize, usize) -> [u16; 4]) -> Vec<u8> {
|
||||
(0..w * h).flat_map(|i| site(i % w / 2, i / w / 2)[((i / w) & 1) * 2 + ((i % w) & 1)].to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
/// Smooth scene texture shared by the four sites of a cell.
|
||||
fn shade(cx: usize, cy: usize) -> u16 {
|
||||
((cx * 7 + cy * 13) % 23) as u16
|
||||
}
|
||||
|
||||
const BGGR: &[u8] = &[2, 0, 2, 0, 2, 1, 1, 0];
|
||||
const GRBG: &[u8] = &[2, 0, 2, 0, 1, 0, 2, 1];
|
||||
// repeat counts in the other byte order
|
||||
const RGGB: &[u8] = &[0, 2, 0, 2, 0, 1, 1, 2];
|
||||
const GBRG: &[u8] = &[0, 2, 0, 2, 1, 2, 0, 1];
|
||||
|
||||
fn layout(bytes: &[u8]) -> String {
|
||||
let r = crate::decode(bytes).unwrap();
|
||||
assert_eq!(crate::probe_info(bytes).unwrap(), r.info());
|
||||
r.cfa.unwrap().name()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exif_cfa_pattern_states_the_layout() {
|
||||
let (w, h) = (160usize, 144usize);
|
||||
// samples whose green diagonal alone would say RGGB
|
||||
let px = mosaic(w, h, |cx, cy| [600, 1000, 1000, 300].map(|v| v + shade(cx, cy)));
|
||||
for (tag, name) in [(BGGR, "BGGR"), (GRBG, "GRBG"), (RGGB, "RGGB"), (GBRG, "GBRG")] {
|
||||
assert_eq!(layout(&orf_with(w as u32, h as u32, 16, px.clone(), Some(tag))), name);
|
||||
}
|
||||
// not a 2×2 Bayer cell (four greens, greens side by side, a fourth colour), other repeat counts, wrong
|
||||
// lengths: the samples decide
|
||||
let unusable: [&[u8]; 8] = [
|
||||
&[2, 0, 2, 0, 1, 1, 1, 1],
|
||||
&[2, 0, 2, 0, 1, 1, 0, 2],
|
||||
&[2, 0, 2, 0, 0, 1, 1, 3],
|
||||
&[3, 0, 3, 0, 0, 1, 1, 2],
|
||||
&[2, 2, 2, 0, 0, 1, 1, 2],
|
||||
&[2, 0, 2, 0, 1, 0],
|
||||
&[2, 0, 2, 0, 2, 1, 1, 0, 0],
|
||||
&[],
|
||||
];
|
||||
for (site, name) in [([600, 1000, 1000, 300], "RGGB"), ([1000, 600, 300, 1000], "GRBG")] {
|
||||
let px = mosaic(w, h, |cx, cy| site.map(|v| v + shade(cx, cy)));
|
||||
assert_eq!(layout(&orf(w as u32, h as u32, 16, px.clone())), name);
|
||||
for tag in unusable {
|
||||
assert_eq!(layout(&orf_with(w as u32, h as u32, 16, px.clone(), Some(tag))), name, "{tag:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packed_12_bit_probe_agrees_with_decode() {
|
||||
let (w, h) = (16usize, 4usize);
|
||||
let strip: Vec<u8> = (0..w * h * 3 / 2).map(|i| (i * 37 % 251) as u8).collect();
|
||||
for tag in [Some(BGGR), None] {
|
||||
let bytes = orf_with(w as u32, h as u32, 12, strip.clone(), tag);
|
||||
let r = crate::decode(&bytes).unwrap();
|
||||
assert_eq!((r.bits, r.data.len()), (12, w * h));
|
||||
assert_eq!(r.cfa.as_ref().unwrap().name(), if tag.is_some() { "BGGR" } else { "RGGB" });
|
||||
assert_eq!(crate::probe_info(&bytes).unwrap(), r.info());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_fallback_looks_past_fine_texture() {
|
||||
let (w, h) = (160usize, 144usize);
|
||||
// greens on the main diagonal, red and blue close; a checkerboard of detail makes the two greens of a cell
|
||||
// differ by far more than red differs from blue
|
||||
let detail = |cx: usize, cy: usize| if (cx + cy) & 1 == 0 { 150 } else { -150i32 };
|
||||
let site = |cx: usize, cy: usize| {
|
||||
let (g, d) = (1000 + shade(cx, cy) as i32, detail(cx, cy));
|
||||
[(g + d) as u16, 500, 520, (g - d) as u16]
|
||||
};
|
||||
assert_eq!(layout(&orf(w as u32, h as u32, 16, mosaic(w, h, site))), "GRBG");
|
||||
// the same scene with the greens on the other diagonal
|
||||
let px = mosaic(w, h, |cx, cy| {
|
||||
let [g0, r, b, g1] = site(cx, cy);
|
||||
[r, g0, g1, b]
|
||||
});
|
||||
assert_eq!(layout(&orf(w as u32, h as u32, 16, px)), "RGGB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_fallback_survives_any_area() {
|
||||
let d = vec![100u16; 64 * 64];
|
||||
let big = usize::MAX;
|
||||
for a in [
|
||||
Rect::new(0, 0, 64, 64),
|
||||
Rect::new(0, 0, 0, 0),
|
||||
Rect::new(0, 0, 31, 31),
|
||||
Rect::new(63, 63, 1, 1),
|
||||
Rect::new(0, 0, 4096, 4096),
|
||||
Rect::new(big, big, big, big),
|
||||
Rect::new(0, big - 40, 64, 40),
|
||||
] {
|
||||
for w in [64, 0, 1, big] {
|
||||
assert_eq!(cfa_from_data(&d, w, a).name(), "RGGB", "{a:?} width {w}");
|
||||
assert_eq!(cfa_from_data(&[], w, a).name(), "RGGB", "{a:?} width {w}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word16_shifted_and_packed12() {
|
||||
let (w, h) = (16usize, 4usize);
|
||||
|
||||
@@ -427,3 +427,32 @@ fn corpus_panasonic_encodings() {
|
||||
}
|
||||
eprintln!("Panasonic encodings checked on {seen} files");
|
||||
}
|
||||
|
||||
/// Olympus ORF (`crates/raw/src/vendor/orf.rs`): the colour-filter layout comes from the file's Exif `CFAPattern`.
|
||||
/// With it, the decoded mosaic's colours follow the camera's own JPEG better than with any other layout.
|
||||
#[test]
|
||||
fn corpus_orf_cfa_patterns() {
|
||||
let dir = corpus_root().join("raw");
|
||||
let cases = [("orf-olympus-e1.orf", "GRBG"), ("orf-olympus-e400.orf", "GRBG"), ("orf-olympus-xz2.orf", "RGGB")];
|
||||
let mut seen = 0;
|
||||
for (name, cfa) in cases {
|
||||
let Ok(bytes) = std::fs::read(dir.join(name)) else {
|
||||
eprintln!("skip: {name} absent");
|
||||
continue;
|
||||
};
|
||||
let img = decode(&bytes).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(img.cfa.as_ref().map(|c| c.name()).as_deref(), Some(cfa), "{name}: CFA layout");
|
||||
assert_eq!(green_on_main_diagonal(&img), cfa.starts_with('G'), "{name}: mosaic statistics disagree with {cfa}");
|
||||
let jpeg = embedded_preview(&bytes).unwrap_or_else(|| panic!("{name}: no embedded preview"));
|
||||
let (gw, gh) = (24, 18);
|
||||
let (sites, jpeg) = (raw_sites(&img, gw, gh), jpeg_cells(&jpeg, gw, gh));
|
||||
let agree = |l: &str| chroma_agreement(&sites, &jpeg, &lightcraft_raw::Cfa::bayer(l).unwrap().pattern);
|
||||
let best_other = ["RGGB", "GRBG", "GBRG", "BGGR"].into_iter().filter(|l| *l != cfa).map(agree).fold(f64::MIN, f64::max);
|
||||
let own = agree(cfa);
|
||||
let a = img.active_area;
|
||||
eprintln!("{name:32} {cfa}, active area at ({}, {}): colour agreement {own:.3} (others ≤ {best_other:.3})", a.x, a.y);
|
||||
assert!(own > 0.5 && own > best_other, "{name}: colours follow the JPEG better with another layout ({own} vs {best_other})");
|
||||
seen += 1;
|
||||
}
|
||||
eprintln!("ORF CFA layouts checked on {seen} files");
|
||||
}
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-IMP-PROFILES | Import profiles & presets | P1 | ✅ | `cmd:file.importPresets`, `cmd:preset.import`, `cmd:profile.import`, `crates/engine/src/preset_import.rs`, `crates/engine/src/preset_luminar.rs` | presets: .lcpreset, XMP, classic .lrtemplate, photos carrying edits ("DNG presets"), Luminar looks (.lmp, .mplumpack collections; sliders with a counterpart), .zip bundles, folders (folder → group), drag & drop; masks carried over; unmapped settings reported; profiles: .cube 3D LUTs; Adobe profile formats deliberately unsupported |
|
||||
| LR-IMP-LOCAL | Work on files in place | P0 | ✅ | `cmd:library.browse`, `cmd:photo.addToLibrary`, `cmd:library.import` (mode add), `cmd:local.addRoot`, `cmd:local.hide`, `cmd:local.restoreHidden`, `crates/engine/src/cmd/browse.rs`, `crates/ui-egui/src/panels/left.rs` (`local_section`), `crates/ui-egui/src/panels/grid.rs` (`folder_header`) | Local: browse Pictures / Desktop / Downloads / Home or any folder without adding it (breadcrumb, Include subfolders, Add N to My Photos); browsed photos stay out of All Photos, albums and counts; folders picked with Browse Folder… (or Keep in Local / Add to Local on a folder row) stay listed across restarts, and browsing a folder inside a listed one highlights it inside that tree (opened down to it, siblings reachable) instead of adding a root; edits go to XMP sidecars; importing promotes them; right-click a top-level location → Remove from Local hides the shortcut only (nothing on disk or in the catalog changes), saved with the UI state, and “Show N hidden locations” (or Browse Folder… on it) brings it back; a folder is one location however its path is spelled (`/` or `\`, trailing separator, `.`/`..`, drive-letter case; `folder_key` in `crates/catalog/src/query.rs`); browsed photos nobody changed are forgotten once their folder has not been browsed for 30 days (Settings → Performance → Local folders, 0 = never; checked when the library opens, or `cmd:library.forgetLocal` with `dryRun`; files and sidecars stay, browsing again brings them back; see `crates/catalog/src/local.rs` for what counts as a change) |
|
||||
| LR-IMP-SIDECAR-SPLIT | Separate XMP sidecar variants | P2 | ⬜ | `cmd:library.xmpPreferences` | sidecar naming option exists (stem/full), no split sidecars |
|
||||
| LR-IMP-FORMATS | Supported formats | P0 | 🟡 | `crates/codecs/src/lib.rs`, `crates/raw/src/lib.rs` | JPEG, PNG, TIFF (1- to 64-bit integer, 16- to 64-bit float, incl. bilevel and CCITT fax), WebP, JXL, PSD, GIF, BMP; DNG (incl. lossy / Smart Preview DNG), CR2 (colour-filter layout from the file's `CR2CFAPattern` tag, issue #85), ARW (pre-2017 bodies: white balance and black level from enciphered maker-note / `SR2SubIFD` data, issue #148; downsized Sony lossless YCbCr 4:2:0 / 4:2:2 tiles decoded to linear RGB, verified on private ILCE-7M4 3:2 & 4:3 examples including 16-bit full-resolution export), NEF (uncompressed, lossless and lossy compressed 12/14-bit, `crates/raw/src/vendor/nefc.rs`), RAF, RW2 / Leica RWL / Panasonic RAW in every raw format (compressed 4 and 6, the prefix-coded strips of format 8 from the GH6 on, packed 2/5/7, the 16-bit words of the 2005–2007 bodies; black levels, in-camera aspect crops and mapped-out defect pixels; 178 CC0 files from 118 bodies checked, `crates/raw/src/vendor/rw2.rs`), PEF, ORF. 12-bit NEF black level: maker note `0x003d` is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). CR3 opens from its full-size embedded JPEG with full metadata (Exif, GPS, XMP from the `CMT1`/`CMT2`/`CMT4` boxes, `crates/meta/src/cr3.rs`) but its raw data is not decoded yet. Missing: CR3 raw decoding (CRX), compressed RAF/ORF, NEFs labelled compressed (34713) but stored uncompressed without a `0x0096` table (Z 6 packed 14-bit, D850 12-bit uncompressed), NEF "lossy after split" (preview only: imported with `preview_only` = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, `previewOnly` in `catalog.query`; Reload clears it once the file decodes), HEIC/AVIF decode, palette (indexed-colour) TIFF (refused by the `tiff` crate) |
|
||||
| LR-IMP-FORMATS | Supported formats | P0 | 🟡 | `crates/codecs/src/lib.rs`, `crates/raw/src/lib.rs` | JPEG, PNG, TIFF (1- to 64-bit integer, 16- to 64-bit float, incl. bilevel and CCITT fax), WebP, JXL, PSD, GIF, BMP; DNG (incl. lossy / Smart Preview DNG), CR2 (colour-filter layout from the file's `CR2CFAPattern` tag, issue #85), ARW (pre-2017 bodies: white balance and black level from enciphered maker-note / `SR2SubIFD` data, issue #148; downsized Sony lossless YCbCr 4:2:0 / 4:2:2 tiles decoded to linear RGB, verified on private ILCE-7M4 3:2 & 4:3 examples including 16-bit full-resolution export), NEF (uncompressed, lossless and lossy compressed 12/14-bit, `crates/raw/src/vendor/nefc.rs`), RAF, RW2 / Leica RWL / Panasonic RAW in every raw format (compressed 4 and 6, the prefix-coded strips of format 8 from the GH6 on, packed 2/5/7, the 16-bit words of the 2005–2007 bodies; black levels, in-camera aspect crops and mapped-out defect pixels; 178 CC0 files from 118 bodies checked, `crates/raw/src/vendor/rw2.rs`), PEF, ORF (uncompressed; colour-filter layout from the file's Exif `CFAPattern`). 12-bit NEF black level: maker note `0x003d` is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). CR3 opens from its full-size embedded JPEG with full metadata (Exif, GPS, XMP from the `CMT1`/`CMT2`/`CMT4` boxes, `crates/meta/src/cr3.rs`) but its raw data is not decoded yet. Missing: CR3 raw decoding (CRX), compressed RAF/ORF, NEFs labelled compressed (34713) but stored uncompressed without a `0x0096` table (Z 6 packed 14-bit, D850 12-bit uncompressed), NEF "lossy after split" (preview only: imported with `preview_only` = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, `previewOnly` in `catalog.query`; Reload clears it once the file decodes), HEIC/AVIF decode, palette (indexed-colour) TIFF (refused by the `tiff` crate) |
|
||||
| LR-IMP-CAMERA-COVERAGE | Camera coverage, verified per model | P0 | 🟡 | `crates/raw/tests/corpus.rs`, `xtask/src/main.rs` | a supported container is not the same as every camera that writes it decoding correctly: ~55 CC0 corpus files (plus ~60 NEFs checked by hand and 178 Panasonic / Leica RW2, RWL and RAW files from 118 bodies, checked against each file's own JPEG) against the >1,000 models Lightroom lists; per-model differences still surface (e.g. the CR2 colour-filter layout differed by model until it was read from the file's own tag, issue #85, checked on 35 CR2 bodies; pre-2017 Sony ARWs opened green until their scrambled white balance / black level were read, issue #148, checked on 16 ARWs from 15 Sony bodies). Grow the CC0 corpus per model and test each sample decodes with plausible colour |
|
||||
| LR-IMP-CULL-AT-IMPORT | Culling analysis at import | P2 | 🟡 | `cmd:photo.analyze` | run Assisted Culling on the imported photos (they're selected after an import); not automatic |
|
||||
| LR-IMP-MOVE | Move on import [Classic] | P1 | ✅ | `cmd:library.import` (`mode` move), `crates/engine/src/import_move.rs`, `crates/engine/src/tests_import_move.rs`, `crates/ui-egui/src/import.rs` | Transfer ▸ Move with Copy's destination, folders (day / month / one folder / custom template), rename template and example destination; XMP sidecars (both namings) move along; each source is removed only after its destination is written (hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate, unchecked files keep their sources, taken names get -1, -2…, never overwritten; files already in the destination/library are added in place; sources that can't be removed (read-only card) are kept and reported (`kept`). Undo removes the photos from the library but leaves the moved files at the destination. No Copy as DNG while moving |
|
||||
|
||||
Reference in New Issue
Block a user