mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 04:57:22 -06:00
LR-IMP-FORMATS: import and decode Mamiya MEF (uncompressed 12-bit; WB from the RGB preview)
Sources: TIFF 6.0 and own black-box analysis of the CC0 ZD sample. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a6d048be8d
commit
9181aa0b6c
+4
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~14 points and ~340–600 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Epson ERF decoded, 3 bodies verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Mamiya MEF and Epson ERF decoded) · **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
|
||||
@@ -114,6 +114,8 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
|
||||
|
||||
## Progress log
|
||||
|
||||
- 2026-10-12: Mamiya MEF (ZD) decodes from sensor data and is imported; verified on a CC0 file. 239 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. 238 models verified; headline numbers unchanged.
|
||||
- 2026-10-11: Canon EOS R5 Mark II C-RAW decodes from sensor data (57% of its files opened as a
|
||||
@@ -163,6 +165,7 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Mamiya MEF decoded and imported: ZD verified (239); headline numbers unchanged |
|
||||
| 2026-10-12 | minor | Epson ERF decoded: 3 more verified models (238); 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 |
|
||||
|
||||
@@ -39,7 +39,7 @@ pub const EXTENSIONS: &[&str] = &[
|
||||
// Decoded when built with the codecs' `heif` feature; otherwise the import reports them as
|
||||
// failed with the reason ("HEIC/HEIF support isn't included in this build").
|
||||
"heic", "heif", "avif", // no AV1 decoder yet: reported as failed, like a HEIC without the feature
|
||||
"iiq", "crw", "mrw", "x3f", "kdc", "mos", "erf", "3fr", "fff",
|
||||
"iiq", "crw", "mrw", "x3f", "kdc", "mos", "erf", "3fr", "fff", "mef",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::backend::{Backend, ProgressHook};
|
||||
pub const PHOTO_EXTENSIONS: &[&str] = &[
|
||||
"jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "cr3", "nef", "arw", "raf", "orf", "rw2", "rwl", "raw", "pef", "srw", "psd", "jxl",
|
||||
"gif", "bmp", "avif", // containers LightCraft cannot decode but imports as preview only (their embedded JPEG)
|
||||
"iiq", "crw", "mrw", "x3f", "kdc", "mos", "erf", "3fr", "fff",
|
||||
"iiq", "crw", "mrw", "x3f", "kdc", "mos", "erf", "3fr", "fff", "mef",
|
||||
];
|
||||
|
||||
/// Headless backend: a [`Session`] with filesystem hooks.
|
||||
|
||||
@@ -113,6 +113,8 @@ pub enum RawFormat {
|
||||
KodakDcs,
|
||||
/// Epson ERF: a CFA sub-IFD of 12-bit samples in 16-byte blocks (`vendor::erf`).
|
||||
Erf,
|
||||
/// Mamiya MEF: an uncompressed 12-bit packed CFA sub-IFD (`vendor::mef`).
|
||||
Mef,
|
||||
/// Another TIFF-based raw (3FR, IIQ, ERF, KDC, DCR, MOS, …).
|
||||
OtherTiff,
|
||||
}
|
||||
@@ -135,6 +137,7 @@ impl RawFormat {
|
||||
| RawFormat::Srw
|
||||
| RawFormat::KodakDcs
|
||||
| RawFormat::Erf
|
||||
| RawFormat::Mef
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -209,6 +212,9 @@ pub fn probe(bytes: &[u8]) -> Option<RawFormat> {
|
||||
if vendor::erf::raw_ifd(&t, bytes.len()).is_some() {
|
||||
return Some(RawFormat::Erf);
|
||||
}
|
||||
if vendor::mef::raw_ifd(&t, bytes.len()).is_some() {
|
||||
return Some(RawFormat::Mef);
|
||||
}
|
||||
if vendor::kodak::raw_ifd(&t, bytes).is_some() {
|
||||
return Some(RawFormat::KodakDcs);
|
||||
}
|
||||
@@ -378,6 +384,7 @@ fn decode_with(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
RawFormat::Srw => vendor::srw::decode(bytes, mode),
|
||||
RawFormat::KodakDcs => vendor::kodak::decode(bytes, mode),
|
||||
RawFormat::Erf => vendor::erf::decode(bytes, mode),
|
||||
RawFormat::Mef => vendor::mef::decode(bytes, mode),
|
||||
RawFormat::OtherTiff => Err(RawError::Unsupported(other_tiff_reason(bytes))),
|
||||
other => Err(RawError::Unsupported(format!("{other:?} files are not decoded yet"))),
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ fn tiff_plan(bytes: &[u8], loaded: &Ranges, preview: bool) -> HeaderPlan {
|
||||
return plan;
|
||||
}
|
||||
// (the Make is usually in the first kilobytes: a NEF, PEF or ORF is known before the rest is read)
|
||||
if matches!(crate::probe(bytes), Some(RawFormat::Nef | RawFormat::Nrw | RawFormat::Orf | RawFormat::Pef | RawFormat::Erf)) {
|
||||
if matches!(crate::probe(bytes), Some(RawFormat::Nef | RawFormat::Nrw | RawFormat::Orf | RawFormat::Pef | RawFormat::Erf | RawFormat::Mef)) {
|
||||
return HeaderPlan::Whole("its header probe reads samples");
|
||||
}
|
||||
let Ok(tiff) = Tiff::parse(bytes) else { return HeaderPlan::Whole("TIFF structure") };
|
||||
@@ -260,7 +260,7 @@ fn tiff_plan(bytes: &[u8], loaded: &Ranges, preview: bool) -> HeaderPlan {
|
||||
holes.insert(off.saturating_add(HEAD), len);
|
||||
}
|
||||
finish(len, loaded, &holes, &meta, || match crate::probe(bytes) {
|
||||
Some(RawFormat::Nef | RawFormat::Nrw | RawFormat::Orf | RawFormat::Pef | RawFormat::Erf) => {
|
||||
Some(RawFormat::Nef | RawFormat::Nrw | RawFormat::Orf | RawFormat::Pef | RawFormat::Erf | RawFormat::Mef) => {
|
||||
HeaderPlan::Whole("its header probe reads samples")
|
||||
}
|
||||
Some(f) if f.is_supported() => HeaderPlan::Ready,
|
||||
|
||||
Vendored
+185
@@ -0,0 +1,185 @@
|
||||
//! Mamiya MEF (ZD): a big-endian TIFF whose first `SubIFDs` child is an uncompressed 12-bit packed CFA image.
|
||||
//!
|
||||
//! Sources: TIFF 6.0 (container, MSB-first bit packing, `CFAPattern`) and our own black-box analysis of the one CC0
|
||||
//! MEF sample on raw.pixls.us (Mamiya ZD); every number below was measured on it.
|
||||
//!
|
||||
//! - IFD0 is a 144 x 192 RGB thumbnail with three `SubIFDs`: the raw (4016 x 5344, 12 bits, `Compression` 1,
|
||||
//! photometric CFA, one strip of exactly 1.5 bytes per pixel, two samples in three bytes most significant bits
|
||||
//! first, as TIFF packs them), a 992 x 1328 and a 240 x 320 RGB preview, all uncompressed and 8 bits. No JPEG.
|
||||
//! - `CFAPattern` `00 01 01 02`: RGGB; the two green sites agree (means 254 and 256 against 235 and 165).
|
||||
//! - No masked border and no stored black level: the darkest 0.01% of each site's samples sit at 3..6 and the
|
||||
//! minimum is 0, so black is 0. White: the saturation plateau, else 4095.
|
||||
//! - The 992 x 1328 preview shows a 3968 x 5312 window (4 sensor pixels per preview pixel) that starts at column 24,
|
||||
//! row 12: that offset gives the best correlation of the preview's luminance with the box-filtered mosaic (0.965,
|
||||
//! against 0.83 for the full frame). It is the default crop.
|
||||
//! - Orientation is the raw IFD's own `Orientation` (6 on the sample, a portrait shot).
|
||||
//! - No white-balance tag was found (the maker note holds an ICC-like profile), so the as-shot gains are measured
|
||||
//! from that preview's neutral pixels ([`crate::wb_estimate`]).
|
||||
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::{Ifd, Tiff, tags as t};
|
||||
|
||||
/// Where the preview's window starts in the sensor frame, and how many sensor pixels one preview pixel covers.
|
||||
const CROP_ORIGIN: (usize, usize) = (24, 12);
|
||||
const PREVIEW_SCALE: usize = 4;
|
||||
|
||||
fn is_mamiya(tiff: &Tiff) -> bool {
|
||||
tiff.find(t::MAKE).and_then(|e| e.value.as_str()).is_some_and(|m| m.to_ascii_uppercase().starts_with("MAMIYA"))
|
||||
}
|
||||
|
||||
/// The raw IFD of a Mamiya MEF: CFA, 12 bits, uncompressed, one strip of 1.5 bytes per pixel.
|
||||
pub(crate) fn raw_ifd(tiff: &Tiff, len: usize) -> Option<&Ifd> {
|
||||
if !is_mamiya(tiff) {
|
||||
return None;
|
||||
}
|
||||
tiff.all_ifds().into_iter().find(|i| {
|
||||
let Ok(info) = i.image() else { return false };
|
||||
let (w, h) = (info.width as u64, info.height as u64);
|
||||
i.u16(t::PHOTOMETRIC) == Some(t::photometric::CFA)
|
||||
&& i.u16(t::COMPRESSION).unwrap_or(1) == 1
|
||||
&& i.u16(t::BITS_PER_SAMPLE) == Some(12)
|
||||
&& w > 0
|
||||
&& w % 2 == 0
|
||||
&& matches!(info.chunks(len as u64).as_slice(), [c] if Some(c.len) == (w * 3 / 2).checked_mul(h))
|
||||
})
|
||||
}
|
||||
|
||||
/// The largest uncompressed 8-bit RGB sub-IFD (the preview): its pixels, width and height.
|
||||
fn rgb_preview<'a>(tiff: &Tiff, bytes: &'a [u8]) -> Option<(&'a [u8], usize, usize)> {
|
||||
tiff.all_ifds()
|
||||
.into_iter()
|
||||
.filter_map(|i| {
|
||||
let info = i.image().ok()?;
|
||||
let ok = i.u16(t::PHOTOMETRIC) == Some(2)
|
||||
&& i.u16(t::COMPRESSION).unwrap_or(1) == 1
|
||||
&& i.u16(t::SAMPLES_PER_PIXEL) == Some(3)
|
||||
&& i.u64s(t::BITS_PER_SAMPLE).is_some_and(|b| b.iter().all(|&v| v == 8));
|
||||
let (w, h) = (info.width as usize, info.height as usize);
|
||||
let chunks = info.chunks(bytes.len() as u64);
|
||||
let [c] = chunks.as_slice() else { return None };
|
||||
let px = chunk_bytes(bytes, c)?;
|
||||
(ok && px.len() >= w.checked_mul(h)?.checked_mul(3)?).then_some((px, w, h))
|
||||
})
|
||||
.max_by_key(|(_, w, h)| w * h)
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
let tiff = Tiff::parse(bytes)?;
|
||||
let raw = raw_ifd(&tiff, bytes.len()).ok_or_else(|| RawError::Unsupported("Mef raw image not found".into()))?;
|
||||
let info = raw.image()?;
|
||||
let (w, h) = (info.width as usize, info.height as usize);
|
||||
let n = w.checked_mul(h).filter(|n| *n > 0 && *n <= crate::MAX_SAMPLES).ok_or(RawError::Limit("image too large"))?;
|
||||
let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) {
|
||||
(Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() },
|
||||
_ => return Err(RawError::Unsupported("Mef raw without a 2x2 CFAPattern".into())),
|
||||
};
|
||||
let preview = rgb_preview(&tiff, bytes);
|
||||
let crop = match preview {
|
||||
Some((_, pw, ph)) => {
|
||||
let (cw, ch) = (pw.saturating_mul(PREVIEW_SCALE), ph.saturating_mul(PREVIEW_SCALE));
|
||||
if cw > 0 && ch > 0 && CROP_ORIGIN.0 + cw <= w && CROP_ORIGIN.1 + ch <= h {
|
||||
Rect::new(CROP_ORIGIN.0, CROP_ORIGIN.1, cw, ch)
|
||||
} else {
|
||||
Rect::new(0, 0, w, h)
|
||||
}
|
||||
}
|
||||
None => Rect::new(0, 0, w, h),
|
||||
};
|
||||
// the white balance is measured from the samples, so the header probe reads them too
|
||||
let chunks = info.chunks(bytes.len() as u64);
|
||||
let src = chunks.first().and_then(|c| chunk_bytes(bytes, c)).ok_or_else(|| RawError::Corrupt("Mef strip outside file".into()))?;
|
||||
let mut d = vec![0u16; n];
|
||||
crate::unpack::unpack_msb(src, 12, &mut d);
|
||||
let white = super::white_from_data(&d, 12);
|
||||
let black = BlackLevel::uniform(0.0);
|
||||
let frame = crate::wb_estimate::Frame { data: &d, width: w, crop, cfa: &cfa, black: &black, white };
|
||||
let wb = match preview {
|
||||
Some((px, pw, ph)) => crate::wb_estimate::as_shot_rgb(px, pw, ph, &frame),
|
||||
None => crate::wb_estimate::as_shot(bytes, &frame),
|
||||
};
|
||||
let data = if mode == Mode::Full { RawData::U16(d) } else { RawData::U16(Vec::new()) };
|
||||
let mut metadata = lightcraft_meta::from_tiff(&tiff);
|
||||
metadata.width = Some(crop.width as u32);
|
||||
metadata.height = Some(crop.height as u32);
|
||||
let orientation = raw.u16(t::ORIENTATION).or_else(|| tiff.ifds.first().and_then(|i| i.u16(t::ORIENTATION))).unwrap_or(1);
|
||||
let img = RawImage {
|
||||
format: RawFormat::Mef,
|
||||
width: w,
|
||||
height: h,
|
||||
cpp: 1,
|
||||
data,
|
||||
cfa: Some(cfa),
|
||||
bits: 12,
|
||||
black,
|
||||
white: vec![white],
|
||||
active_area: Rect::new(0, 0, w, h),
|
||||
crop,
|
||||
orientation: Orientation::from_exif(orientation),
|
||||
color: ColorData::default(),
|
||||
wb_multipliers: wb,
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lightcraft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter, Value};
|
||||
|
||||
fn pack(px: &[u16]) -> Vec<u8> {
|
||||
px.chunks(2).flat_map(|p| [(p[0] >> 4) as u8, ((p[0] & 15) << 4) as u8 | (p[1] >> 8) as u8, p[1] as u8]).collect()
|
||||
}
|
||||
|
||||
fn sample(w: u32, h: u32, px: &[u16]) -> Vec<u8> {
|
||||
let mut raw = IfdBuilder::new();
|
||||
raw.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0]));
|
||||
raw.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
|
||||
raw.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
|
||||
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![12]));
|
||||
raw.set(t::COMPRESSION, Value::Short(vec![1]));
|
||||
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
|
||||
raw.set(t::ORIENTATION, Value::Short(vec![6]));
|
||||
raw.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2]));
|
||||
raw.set(t::CFA_PATTERN_EP, Value::Byte(vec![0, 1, 1, 2]));
|
||||
raw.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![pack(px)] });
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(t::MAKE, Value::Ascii("Mamiya-OP Co.,Ltd.".into()));
|
||||
ifd0.set(t::MODEL, Value::Ascii("MAMIYA ZD".into()));
|
||||
ifd0.add_sub_ifd(raw);
|
||||
TiffWriter::new(ByteOrder::Big, false).write(&[ifd0]).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_packed_12_bit() {
|
||||
let px: Vec<u16> = (0..16 * 6).map(|i| (i * 41 % 4096) as u16).collect();
|
||||
let f = sample(16, 6, &px);
|
||||
assert_eq!(crate::probe(&f), Some(RawFormat::Mef));
|
||||
let r = crate::decode(&f).unwrap();
|
||||
assert_eq!(r.data, RawData::U16(px));
|
||||
assert_eq!(r.orientation, Orientation::from_exif(6));
|
||||
assert_eq!(r.crop, Rect::new(0, 0, 16, 6));
|
||||
assert_eq!(crate::probe_info(&f).unwrap(), r.info());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_files_never_panic() {
|
||||
let px: Vec<u16> = (0..16 * 6).map(|i| (i * 7 % 4096) as u16).collect();
|
||||
let f = sample(16, 6, &px);
|
||||
for n in 0..f.len() {
|
||||
let _ = crate::decode(&f[..n]);
|
||||
}
|
||||
for i in 0..f.len() {
|
||||
for v in [0u8, 0xff] {
|
||||
let mut g = f.clone();
|
||||
g[i] = v;
|
||||
let _ = crate::decode(&g);
|
||||
let _ = crate::probe_info(&g);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -12,6 +12,7 @@ pub mod crx;
|
||||
pub(crate) mod crx_wavelet;
|
||||
pub mod erf;
|
||||
pub mod kodak;
|
||||
pub mod mef;
|
||||
pub mod nef;
|
||||
pub mod nefc;
|
||||
pub mod orf;
|
||||
|
||||
@@ -67,7 +67,20 @@ fn median(v: &mut [f64]) -> Option<f64> {
|
||||
fn from_preview(bytes: &[u8], f: &Frame) -> Option<[f32; 3]> {
|
||||
let jpeg = crate::preview::embedded_preview(bytes)?;
|
||||
let (px, pw, ph, ch) = crate::tiffraw::lossy_jpeg(&jpeg, 3).ok()?;
|
||||
if ch != 3 || pw.checked_mul(ph)? > MAX_PREVIEW_PIXELS {
|
||||
if ch != 3 {
|
||||
return None;
|
||||
}
|
||||
from_rgb(&px, pw, ph, f)
|
||||
}
|
||||
|
||||
/// Gains from an 8-bit RGB preview `pw` x `ph` (rows of RGB triplets) showing the crop: its neutral pixels, else
|
||||
/// grey world. For raws whose preview is an uncompressed RGB image rather than a JPEG.
|
||||
pub(crate) fn as_shot_rgb(px: &[u8], pw: usize, ph: usize, f: &Frame) -> Option<[f32; 3]> {
|
||||
from_rgb(px, pw, ph, f).or_else(|| grey_world(f))
|
||||
}
|
||||
|
||||
fn from_rgb(px: &[u8], pw: usize, ph: usize, f: &Frame) -> Option<[f32; 3]> {
|
||||
if pw.checked_mul(ph)? > MAX_PREVIEW_PIXELS || px.len() < pw * ph * 3 {
|
||||
return None;
|
||||
}
|
||||
// the preview must show the crop: aspect ratios within 3%
|
||||
|
||||
@@ -42,7 +42,7 @@ fn corpus_raw_decodes() {
|
||||
let preview = embedded_preview(&bytes);
|
||||
let tp = t0.elapsed().as_secs_f64() * 1e3;
|
||||
// DNG previews are optional (and some carry only an uncompressed RGB thumbnail); vendor raws embed a JPEG,
|
||||
// except the Panasonic `.RAW` files of 2005–2007 and Canon's explicit HEVC preview tracks.
|
||||
// except the Panasonic `.RAW` files of 2005–2007, Canon's explicit HEVC preview tracks and Mamiya MEF.
|
||||
if let Some(p) = &preview {
|
||||
let jpeg = p.starts_with(&[0xff, 0xd8]) && p.ends_with(&[0xff, 0xd9]);
|
||||
let jxl = p.starts_with(&[0xff, 0x0a]) || p.starts_with(b"\0\0\0\x0cJXL ");
|
||||
@@ -51,7 +51,11 @@ fn corpus_raw_decodes() {
|
||||
let hevc_preview = fmt == RawFormat::Cr3
|
||||
&& lightcraft_meta::cr3::parse_cr3(&bytes)
|
||||
.is_some_and(|c| c.tracks.iter().any(|t| t.kind == lightcraft_meta::cr3::Cr3TrackKind::Other(*b"HEVC") && t.data.is_some()));
|
||||
assert!(fmt == RawFormat::Dng || name.starts_with("raw-panasonic-") || hevc_preview, "{name}: no embedded preview");
|
||||
// the Mamiya ZD's previews are uncompressed RGB sub-IFDs, not JPEGs
|
||||
assert!(
|
||||
fmt == RawFormat::Dng || fmt == RawFormat::Mef || name.starts_with("raw-panasonic-") || hevc_preview,
|
||||
"{name}: no embedded preview"
|
||||
);
|
||||
}
|
||||
let preview_kb = preview.as_ref().map_or(0, |p| p.len() / 1024);
|
||||
let t1 = Instant::now();
|
||||
@@ -1166,3 +1170,25 @@ fn corpus_epson_erf() {
|
||||
}
|
||||
eprintln!("ERF: {checked} corpus files checked");
|
||||
}
|
||||
|
||||
/// Mamiya MEF (`crates/raw/src/vendor/mef.rs`): the uncompressed 12-bit strip decodes to a smooth 4016 x 5344 RGGB
|
||||
/// mosaic, the crop is the preview's window and the white balance comes from the RGB preview. Skips when absent.
|
||||
#[test]
|
||||
fn corpus_mamiya_mef() {
|
||||
let Ok(bytes) = std::fs::read(corpus_root().join("raw").join("mef-mamiya-zd.mef")) else {
|
||||
eprintln!("skip: mef-mamiya-zd.mef absent");
|
||||
return;
|
||||
};
|
||||
assert_eq!(probe(&bytes), Some(RawFormat::Mef));
|
||||
let img = decode(&bytes).unwrap_or_else(|e| panic!("{e}"));
|
||||
assert_eq!((img.width, img.height, img.bits), (4016, 5344, 12));
|
||||
assert_eq!((img.crop.x, img.crop.y, img.crop.width, img.crop.height), (24, 12, 3968, 5312));
|
||||
assert_eq!(img.cfa.as_ref().map(|c| c.name()).as_deref(), Some("RGGB"));
|
||||
assert_eq!(img.orientation, lightcraft_geom::Orientation::from_exif(6));
|
||||
let wb = img.wb_multipliers.expect("white balance");
|
||||
eprintln!("MEF wb {wb:?}");
|
||||
assert!(wb.iter().all(|g| (0.5..=4.0).contains(g)), "{wb:?}");
|
||||
let lightcraft_raw::RawData::U16(d) = &img.data else { panic!("float data") };
|
||||
let step: f64 = d.chunks(4016).take(400).flat_map(|r| r.windows(3).map(|w| f64::from(w[0].abs_diff(w[2])))).sum::<f64>() / (400.0 * 4014.0);
|
||||
assert!(step < 40.0, "rough {step}");
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
# Gaps: where LightCraft falls short of Lightroom
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (gap 20: Epson ERF decodes; CR3 entry names the EOS R7 C-RAW fix and the refused CRX variants; CI and Windows / Linux entries note the 0.6.0 riscv64 build; camera colour: tone curve from black-and-white camera JPEGs; Compare and Reference get sharp zoom windows; gap 4 shrinks; gap 14: the HDR switch is read from crs XMP; gaps 2 and 10: EOS R5 Mark II CR3 verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (gap 20: Mamiya MEF and Epson ERF decode; CR3 entry names the EOS R7 C-RAW fix and the refused CRX variants; CI and Windows / Linux entries note the 0.6.0 riscv64 build; camera colour: tone curve from black-and-white camera JPEGs; Compare and Reference get sharp zoom windows; gap 4 shrinks; gap 14: the HDR switch is read from crs XMP; gaps 2 and 10: EOS R5 Mark II CR3 verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
|
||||
Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and
|
||||
how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is
|
||||
@@ -24,7 +24,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
|
||||
| 7 | **Nikon High Efficiency NEF** (HE / HE★ on Z 8, Z 9, Z 6III, Z f, Z50II, Z5II when set to HE) | named and preview only (#193); needs a pure-Rust JPEG XS decoder | +1.1 points (only shots taken in HE; lossless works on the same bodies); low gain per hour (~0.04), so not a beta blocker | 20–40 | [raw-parity](raw-parity.md) |
|
||||
| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion and Sony ILCE-7RM4A distortion | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) |
|
||||
| 9 B | **Tests don't run in CI on macOS, Windows or Linux x64**: only FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU | regressions reach users on the main platforms; `cargo xtask ci` runs only locally | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) |
|
||||
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 238 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12. Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
|
||||
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 239 of 1,446 Adobe models verified (17%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12. Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
|
||||
| 11 | **Enhance: Raw Details, Super Resolution; AI denoise without shippable weights or X-Trans** | ENH rows 0%; denoise Bayer only, RawNIND weights GPL and opt-in | Fujifilm users get no AI denoise; nobody gets it out of the box | 30–60 + model decision | [raw-parity](raw-parity.md#demosaic-levels-crops-and-lens-metadata) |
|
||||
| 12 | **Content-aware / generative remove, detect distractions, reflections removal** | REM rows 75%; patch synthesis missing | cleanup work goes back to Photoshop | 25–45 (+ generative model) | [target-app-parity](target-app-parity.md#ai) |
|
||||
| 13 | **Lightroom catalog migration is one-way and approximate**: unsupported Adobe profiles / AI settings archived, history kept as data | lightroom-catalog-import.md | switching users lose some looks | 8–12 | [file-format-parity](file-format-parity.md) |
|
||||
@@ -34,7 +34,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
|
||||
| 17 | **Tethered capture** | ⬜ | studio photographers | 25–45 + cameras | [hardware-parity](hardware-parity.md) |
|
||||
| 18 | **Video**: playback, trim, global edits, export | VID rows 0% | users who mix stills and clips | 25–45 | [target-app-parity](target-app-parity.md#features) |
|
||||
| 19 | **Plug-in ecosystem**: no Lightroom SDK equivalent (export / publish plug-ins, metadata plug-ins) | ⬜ | pro workflows built on plug-ins (LR/Enfuse, Jeffrey's exporters, Negative Lab Pro) | 40–80 + owner decision | [target-app-parity](target-app-parity.md#by-dimension) |
|
||||
| 20 | **Long-tail raw formats**: compressed SRW, ARQ pixel shift, IIQ, Hasselblad compression, X3F, CRW, MRW, KDC, DCR, MEF, GPR, old FinePix (ERF decodes since 2026-10-12) | raw-parity.md | small user share each | 43–83 | [raw-parity](raw-parity.md) |
|
||||
| 20 | **Long-tail raw formats**: compressed SRW, ARQ pixel shift, IIQ, Hasselblad compression, X3F, CRW, MRW, KDC, DCR, GPR, old FinePix (ERF and MEF decode since 2026-10-12) | raw-parity.md | small user share each | 42–82 | [raw-parity](raw-parity.md) |
|
||||
| 21 | **Localization**: 9 of 15 shipped languages partial (74–99% of catalog messages), lower-layer errors English; Hindi, Arabic, Indonesian, Korean, Vietnamese absent; no RTL or complex shaping | measured from catalogs | non-English users see mixed English | 55–90 (shipped to full); 145–255 (five new) | [localization-parity](localization-parity.md) |
|
||||
| 22 | **Pen pressure**, OS monitor profiles, follow-window colour | hardware-parity.md | brush users with tablets; multi-monitor setups | 11–22 | [hardware-parity](hardware-parity.md) |
|
||||
| 23 | **Accessibility** audit with screen readers; canvas tools keyboard-reachable | LR-BEHAV-ACCESS 🟡 | blind and low-vision users | 10–20 | [ui-parity](ui-parity.md) |
|
||||
@@ -72,6 +72,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Gap 20 shrinks: Mamiya MEF decodes (`vendor/mef.rs`) and is imported; gap 10 count 238 → 239 |
|
||||
| 2026-10-12 | minor | Gap 20 shrinks: Epson ERF decodes (`vendor/erf.rs`), 3 bodies verified; gap 10 count 235 → 238 |
|
||||
| 2026-10-11 | minor | Reviewed against main after 0.6.0: CR3 entry names the CRX variants `crx.rs` still refuses and the R7 / R6 Mark II C-RAW state (decoding, not verified, so the measured numbers don't move); CI and Windows / Linux entries note the riscv64 release build (#844); R7 fix added to Closed recently. Panel docking (UI.9, #869) closes no listed gap |
|
||||
| 2026-10-11 | minor | Camera colour entry: raws with a black-and-white camera JPEG take its tone curve (#577); gap not closed |
|
||||
|
||||
File diff suppressed because one or more lines are too long
+11
-9
@@ -1,6 +1,6 @@
|
||||
# RAW parity: cameras, formats, demosaic, colour and lens profiles
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Epson ERF decoded: 3 bodies verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Mamiya MEF decoded: ZD verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
|
||||
|
||||
Raw processing is the deepest single dimension of Lightroom parity: a photographer whose camera
|
||||
does not decode, or decodes with the wrong colour, cannot switch. This document measures it model by
|
||||
@@ -14,10 +14,10 @@ ranked in [gaps.md](gaps.md). Details per format: [cr3.md](cr3.md),
|
||||
| Measure | Value | Kind |
|
||||
|---|---:|---|
|
||||
| Adobe-listed camera models | **1,446** | measured: [raw/adobe-cameras.tsv](raw/adobe-cameras.tsv) |
|
||||
| Decoded from sensor data and **verified** on a real file | **238** (16%) | measured: [raw/evidence.tsv](raw/evidence.tsv) |
|
||||
| Decoded from sensor data and **verified** on a real file | **239** (17%) | measured: [raw/evidence.tsv](raw/evidence.tsv) |
|
||||
| Decoder path should handle it, **unverified** | **942** (65%) | measured from the format rules (632 of them DNG, mostly phones) |
|
||||
| **Preview-only** (embedded JPEG) | **230** (16%) | measured |
|
||||
| **Unsupported** (not imported, refused or decodes wrongly) | **36** (2%) | measured |
|
||||
| **Unsupported** (not imported, refused or decodes wrongly) | **35** (2%) | measured |
|
||||
| Models added since 2017 (Camera Raw 10+): verified / unverified / preview / unsupported | 82 / 640 / 24 / 8 of 754 | measured |
|
||||
| **Share of photographers whose camera fully works** (decodes from sensor data), use-weighted | **~85%** | estimated: measured classes × written usage weights × per-format confidence |
|
||||
| …of which on a body we have verified | **~42%** | estimated, same weights |
|
||||
@@ -41,7 +41,7 @@ committed table is stale.
|
||||
`lightcraft_engine::import::EXTENSIONS`, and `KNOWN_UNSUPPORTED` in `crates/raw/tests/corpus.rs`. Doc
|
||||
claims were not used.
|
||||
- **Verified** means a real file from that body decoded from sensor data in: the corpus test over the
|
||||
`RAW_SAMPLES` manifest (`xtask/src/main.rs`, 104 CC0 files; bodies whose sample is in
|
||||
`RAW_SAMPLES` manifest (`xtask/src/main.rs`, 105 CC0 files; bodies whose sample is in
|
||||
`KNOWN_UNSUPPORTED` excluded), `crates/raw/tests/cr3_corpus.rs` (3 bodies exact against an external decoder;
|
||||
the EOS R5 Mark II pinned, with a QP-map check of 33,665 private files), the RAF reference
|
||||
check ([raf-compression.md](raf-compression.md)), and the raw.pixls.us bulk checks recorded in issue
|
||||
@@ -76,12 +76,12 @@ committed table is stale.
|
||||
| Xiaomi, Oppo, LG, Huawei, OnePlus, Motorola, Nokia | 139 | 0 | 139 | 0 | 0 | — (DNG) |
|
||||
| Pentax / Ricoh | 60 | 4 | 56 | 0 | 0 | — |
|
||||
| Hasselblad | 34 | 0 | 2 | 32 | 0 | Hasselblad-compressed 3FR / FFF |
|
||||
| Phase One / Leaf / Mamiya | 59 | 0 | 0 | 43 | 16 | IIQ, MOS preview; TIFF raws and MEF not imported |
|
||||
| Phase One / Leaf / Mamiya | 59 | 1 | 0 | 43 | 15 | IIQ, MOS preview; TIFF raws not imported (MEF decodes) |
|
||||
| Casio | 26 | 0 | 26 | 0 | 0 | — (DNG) |
|
||||
| DJI | 21 | 0 | 21 | 0 | 0 | — (DNG) |
|
||||
| OM Digital Solutions | 7 | 0 | 0 | 7 | 0 | **compressed ORF** |
|
||||
| Kodak, Konica Minolta, Epson, Sigma, GoPro, Contax, others | 56 | 3 | 14 | 24 | 15 | DCR, GPR not imported; KDC, MRW, X3F preview (ERF decodes) |
|
||||
| **All** | **1,446** | **238 (16%)** | **942 (65%)** | **230 (16%)** | **36 (2%)** | |
|
||||
| **All** | **1,446** | **239 (17%)** | **942 (65%)** | **230 (16%)** | **35 (2%)** | |
|
||||
|
||||
## By use (estimated)
|
||||
|
||||
@@ -97,7 +97,7 @@ committed table is stale.
|
||||
| Pentax / Ricoh | 1.5% | 94% | 14% | |
|
||||
| Leica | 1% | 95% | 5% | |
|
||||
| DJI | 1% | 95% | 0% | |
|
||||
| Medium format and others | 0.5% | 24% | 2% | IIQ, 3FR, X3F |
|
||||
| Medium format and others | 0.5% | 25% | 2% | IIQ, 3FR, X3F |
|
||||
| **Weighted** | 100% | **85%** | **42%** | |
|
||||
|
||||
About 5 in 6 raw photographers' cameras decode, but only about 2 in 5 use a body we have actually
|
||||
@@ -146,7 +146,8 @@ checked, and Canon, the largest bucket, is the weakest of the big four.
|
||||
| 3FR / FFF | Hasselblad compression | ⬜ | preview |
|
||||
| ERF | 12-bit in 16-byte blocks (R-D1, R-D1s, R-D1x) | ✅ | 3 CC0 corpus files; crop registered and WB measured against the embedded preview (no WB tag found); `vendor/erf.rs` |
|
||||
| IIQ, MOS, MRW, KDC, X3F | all | ⬜ | preview |
|
||||
| DCR, MEF, MFW, DXO, GPR, TIFF raws | all | ⬜ | not imported |
|
||||
| MEF | uncompressed 12-bit (Mamiya ZD) | ✅ | 1 CC0 corpus file; crop and WB from its uncompressed RGB preview (no JPEG, no WB tag); `vendor/mef.rs` |
|
||||
| DCR, MFW, DXO, GPR, TIFF raws | all | ⬜ | not imported |
|
||||
|
||||
### Compressed ORF: a clean-room starting point
|
||||
|
||||
@@ -252,7 +253,7 @@ works, from [raw/usage-weights.tsv](raw/usage-weights.tsv).
|
||||
| 7 | Sony A100 and DSC-F828 regression (#702) | +0.1 | 1–2 | ~0.07 | samples on raw.pixls.us |
|
||||
| 8 | Canon CRW (2000–2004 bodies) | +1.2 by the uniform tail weight, far less by real use | 6–12 | ≤ 0.1 | samples |
|
||||
| 9 | Nikon HE / HE★ NEF (Z 8, Z 9, Z 6III, Z f, Z50II, Z5II set to HE) | +1.1 | 20–40 | ~0.04 | a pure-Rust JPEG XS decoder |
|
||||
| 10 | Compressed SRW, Hasselblad 3FR, IIQ, X3F, MRW, KDC, DCR, MEF, GPR (ERF done) | +0.6 together | 45–85 | ~0.01 | samples; low use |
|
||||
| 10 | Compressed SRW, Hasselblad 3FR, IIQ, X3F, MRW, KDC, DCR, GPR (ERF, MEF done) | +0.6 together | 45–85 | ~0.01 | samples; low use |
|
||||
|
||||
Beyond decoding:
|
||||
|
||||
@@ -267,6 +268,7 @@ Beyond decoding:
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Mamiya MEF decoded (`vendor/mef.rs`): ZD verified; 239 verified, 35 unsupported |
|
||||
| 2026-10-12 | minor | Epson ERF decoded (`vendor/erf.rs`): R-D1, R-D1s, R-D1x verified; 238 verified, 230 preview |
|
||||
| 2026-10-10 | minor | Compressed ORF: pointer to the two Olympus patents that describe the method, as a clean-room starting point |
|
||||
| 2026-10-11 | minor | EOS R5 Mark II verified (CRX QP-map first literal and limited-length escape, odd-height Bayer planes; 4 CC0 files pinned, QP maps of 33,665 private files): 235 verified, 942 unverified; use-weighted 85% (42% on verified bodies); decoding 77%, RAW readiness unchanged at ~56% |
|
||||
|
||||
@@ -83,6 +83,8 @@ def classify(maker, camera, ext_field):
|
||||
return "preview", "SRW", "compressed SRW is not decoded"
|
||||
return "unverified", "SRW", "uncompressed / packed SRW"
|
||||
e = es[0] if es else ""
|
||||
if e == "MEF":
|
||||
return "unverified", "MEF", "MEF decoder (uncompressed 12-bit)"
|
||||
if e == "ERF":
|
||||
return "unverified", "ERF", "ERF decoder (12-bit 16-byte blocks)"
|
||||
if e in ("CRW", "3FR", "FFF", "IIQ", "MOS", "KDC", "MRW", "X3F", "RAW", "ORI"):
|
||||
|
||||
@@ -673,7 +673,7 @@ Mamiya Mamiya M18 TIF 2008 unsupported TIF is not imported
|
||||
Mamiya Mamiya M22 TIF 2008 unsupported TIF is not imported
|
||||
Mamiya Mamiya M31 TIF 2008 unsupported TIF is not imported
|
||||
Mamiya Mamiya DM40 MOS 2010 preview MOS is opened from its embedded JPEG only
|
||||
Mamiya ZD MEF 2005 unsupported MEF is not imported
|
||||
Mamiya ZD MEF 2005 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Motorola Moto G Power (2021) Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw)
|
||||
Motorola Moto G Power (2021) Rear Macro Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw)
|
||||
Motorola Moto G Power (2021) Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 86 and column 19.
|
@@ -251,3 +251,4 @@ Sony ZV-E10 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies
|
||||
Epson R-D1 verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Epson R-D1s verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Epson R-D1x verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
Mamiya ZD verified corpus: crates/raw/tests/corpus.rs (xtask RAW_SAMPLES)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 3.
|
@@ -1,6 +1,6 @@
|
||||
# Target-app parity: LightCraft vs Adobe Lightroom
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Epson ERF decoded: 3 models verified; headline numbers unchanged) · **Target:** Adobe Lightroom Classic 15.5 (August 2026) and Lightroom 9.6 (desktop)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Mamiya MEF and Epson ERF decoded; headline numbers unchanged) · **Target:** Adobe Lightroom Classic 15.5 (August 2026) and Lightroom 9.6 (desktop)
|
||||
|
||||
The authoritative assessment of how close LightCraft is to Lightroom. The row-by-row checklist is
|
||||
[parity-checklist.md](parity-checklist.md); the ranked work list is [gaps.md](gaps.md); deep areas
|
||||
@@ -297,6 +297,7 @@ P2 items 12–25 h; look tuning, performance, packaging, hardening 25–45 h.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Mamiya MEF decoded (raw-parity.md): 239 verified, 35 unsupported; rounded numbers unchanged |
|
||||
| 2026-10-12 | minor | Epson ERF decoded (raw-parity.md): 238 models verified, 230 preview-only; shares and rounded numbers unchanged |
|
||||
| 2026-10-11 | minor | EOS R5 Mark II CR3 verified (raw-parity.md): use-weighted share whose camera decodes 84% → 85%, RAW decoding 76% → 77%; full 60.6% → 60.7%, mainstream depth 71.8% → 72.1%, essentials depth 77.4% → 77.5%; the rounded numbers (~61%, ~49%, ~64%) are unchanged |
|
||||
| 2026-10-10 | minor | RAW measured model by model (raw-parity.md): RAW decoding (76%) and colour (35%) are explicit rows of the full sum (59% → 61%); mainstream uses the use-weighted 84% plus a ×0.85 colour discount (51% → 49%); essentials uses 84% × colour 0.75 (63% → 64%); beta ~14 points |
|
||||
|
||||
@@ -888,6 +888,11 @@ const RAW_SAMPLES: &[(&str, &str, &str)] = &[
|
||||
("erf-epson-rd1.erf", "https://raw.pixls.us/data/Epson/R-D1/_EPS0672.ERF", "09d8e533d93116294a9f3e161ed868e929454c7b946e083b644bd6bb75bcb5e4"),
|
||||
("erf-epson-rd1s.erf", "https://raw.pixls.us/data/Epson/R-D1s/EPSN3575.ERF", "45bb0081baf1c813c7cd9e05b3e6ae734b90aba408dd1195118dc94dc6abbbb9"),
|
||||
("erf-epson-rd1x.erf", "https://raw.pixls.us/data/Epson/R-D1x/_EPS1919.ERF", "fa1bff3e0b9b9a14f83b8db9fae022c2aca8d455896bf4f25f2c1a0034e295e6"),
|
||||
(
|
||||
"mef-mamiya-zd.mef",
|
||||
"https://raw.pixls.us/data/Mamiya/ZD/RAW_MAMIYA_ZD.MEF",
|
||||
"bcd63507c3c4cc3ea1bad2945e3f88cb4677a1a3c762e1acc38df4445714eb7c",
|
||||
),
|
||||
];
|
||||
|
||||
fn cmd_corpus(download: bool) -> Result<(), String> {
|
||||
|
||||
Reference in New Issue
Block a user