flashraw-cli 0.2.0

The flashraw command-line tool: inspect, decode, convert and benchmark camera RAW files
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
//! `flashraw` command-line tool.

mod crops;
mod output;
mod probe;

use clap::{Args, Parser, Subcommand};
use flashraw::{
    Backend, CameraProfile, ColorSpace, CorruptionPolicy, DecodeOptions, Demosaic, FlashRaw, Highlights,
    NoiseReduction, OutputFormat, OutputProfile, Rect, RenderingIntent, WhiteBalanceMode,
};
use std::path::PathBuf;
use std::process::ExitCode;

#[derive(Parser)]
#[command(name = "flashraw", version, about = "Fast RAW image decoder", propagate_version = true)]
struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Args, Clone)]
struct DecodeArgs {
    /// Backend: auto, cpu, simd, gpu.
    #[arg(long, default_value = "auto")]
    backend: String,
    /// Output: raw-u16, raw-f32, normalized, linear-rgb, linear-rgb16, linear-rgb-f16, rgba8,
    /// rgb16.
    #[arg(long, default_value = "raw-u16")]
    output: String,
    /// Decode only a region: x,y,width,height (sensor coordinates).
    #[arg(long, value_parser = parse_rect)]
    region: Option<Rect>,
    /// Decode a single tile/strip by index.
    #[arg(long)]
    tile: Option<usize>,
    /// Worker threads (default: all cores).
    #[arg(long)]
    threads: Option<usize>,
    /// Demosaic for RGB outputs: bilinear, half, edge-aware, ahd, ppg, rcd, auto (the default: AHD on Bayer, edge-aware on X-Trans, else bilinear).
    #[arg(long, default_value = "auto")]
    demosaic: String,
    /// Clipped highlights of RGB outputs: clip, neutral, blend, reconstruct.
    #[arg(long, default_value = "clip")]
    highlights: String,
    /// White balance of RGB outputs: as-shot, daylight (D65 from the colour matrix), auto (refine the
    /// camera's over near-neutral areas).
    #[arg(long, default_value = "as-shot")]
    white_balance: String,
    /// Red,green,blue multipliers of the camera RGB, replacing --white-balance (e.g. 2.1,1,1.6).
    #[arg(long, value_name = "R,G,B", value_parser = parse_multipliers)]
    wb_multipliers: Option<[f32; 3]>,
    /// Smoothing of --demosaic edge-aware: auto (from the frame's noise), off, low, medium, high.
    #[arg(long, default_value = "auto")]
    noise_reduction: String,
    /// Correct the lateral chromatic aberration of Bayer frames (RGB outputs).
    #[arg(long)]
    chromatic_aberration: bool,
    /// Do not apply DNG opcode lists (bad pixels, lens shading, ...).
    #[arg(long)]
    no_opcodes: bool,
    /// RGB outputs: exposure in EV, applied to linear values before encoding.
    #[arg(long, default_value_t = 0.0, allow_negative_numbers = true)]
    exposure: f32,
    /// RGB outputs: linear value mapped to black (after exposure).
    #[arg(long, default_value_t = 0.0, allow_negative_numbers = true)]
    black_point: f32,
    /// RGB outputs: linear value mapped to white (after exposure).
    #[arg(long, default_value_t = 1.0)]
    white_point: f32,
    /// Fail on truncated or corrupt sensor data instead of zero-filling what is missing
    /// (with a warning, the default).
    #[arg(long)]
    strict: bool,
    /// RGB outputs of files whose sensor data cannot be decoded (unsupported compression):
    /// write the embedded full-size preview instead of failing (reported as a warning and
    /// as `"source": "preview"` with --json).
    #[arg(long)]
    preview_fallback: bool,
    /// RGB outputs: colour space: srgb (default), display-p3, adobe-rgb, prophoto, rec2020.
    /// TIFF and JPEG files carry the matching ICC profile.
    #[arg(long, default_value = "srgb")]
    color_space: String,
    /// RGB display outputs (rgba8, rgb16): convert to this ICC profile instead of
    /// --color-space and embed it in TIFF and JPEG files. Profiles with lookup tables use a
    /// second, slower pass.
    #[arg(long, value_name = "FILE.icc")]
    output_profile: Option<PathBuf>,
    /// Rendering intent for --output-profile: perceptual, relative, saturation, absolute.
    #[arg(long, default_value = "perceptual")]
    intent: String,
    /// Black point compensation for --output-profile.
    #[arg(long)]
    bpc: bool,
    /// RGB outputs: render with a DNG camera profile: a .dcp file, or `embedded` for the
    /// profile stored in the DNG being decoded (an error when it has none). Replaces the
    /// file's colour matrices and applies the profile's hue/sat map, look table and tone
    /// curve. CPU only.
    #[arg(long, value_name = "FILE.dcp|embedded")]
    camera_profile: Option<String>,
    /// Print per-stage timings.
    #[arg(long)]
    profile: bool,
    /// Machine-readable output.
    #[arg(long)]
    json: bool,
}

#[derive(Subcommand)]
enum Cmd {
    /// Show a summary of a RAW file.
    Info {
        files: Vec<PathBuf>,
        #[arg(long)]
        json: bool,
    },
    /// Print all metadata as JSON.
    DumpMetadata { file: PathBuf },
    /// Decode sensor data (or RGB) to a file. Format from --format or extension:
    /// .raw (u16 LE samples; half-float bits for linear-rgb-f16), .pgm/.ppm (16-bit Netpbm),
    /// .tif/.tiff (16-bit), .jpg (rgba8).
    Decode {
        file: PathBuf,
        out: PathBuf,
        #[arg(long)]
        format: Option<String>,
        #[command(flatten)]
        args: DecodeArgs,
    },
    /// Extract the largest embedded preview.
    Preview { file: PathBuf, out: PathBuf },
    /// Extract the smallest embedded preview (thumbnail).
    Thumbnail { file: PathBuf, out: PathBuf },
    /// Benchmark decoding.
    Benchmark {
        files: Vec<PathBuf>,
        #[arg(long, default_value_t = 5)]
        iterations: usize,
        #[command(flatten)]
        args: DecodeArgs,
    },
    /// Decode and print hashes and geometry of the decoded data (for regression testing).
    Verify {
        files: Vec<PathBuf>,
        #[arg(long)]
        json: bool,
        /// Also write raw u16 samples to `<file>.flashraw.u16` next to the output dir.
        #[arg(long)]
        dump_dir: Option<PathBuf>,
        /// Hash the samples with DNG opcode lists applied (the default decode). Without
        /// it, samples are hashed as stored (the payload decode alone).
        #[arg(long)]
        apply_opcodes: bool,
        /// Fail on truncated or corrupt sensor data instead of recovering it (recovered
        /// files report `incomplete` and `warnings`).
        #[arg(long)]
        strict: bool,
    },
    /// List supported formats and compression modes.
    Supported {
        #[arg(long)]
        json: bool,
    },
    /// Camera database tooling.
    #[command(subcommand)]
    CameraDb(CameraDbCmd),
}

#[derive(Subcommand)]
enum CameraDbCmd {
    /// Validate the database file.
    Validate {
        #[arg(default_value = "crates/flashraw-camera-db/data/cameras.json")]
        db: PathBuf,
    },
    /// Re-normalise (load, sort and rewrite) the database file.
    Generate {
        #[arg(default_value = "crates/flashraw-camera-db/data/cameras.json")]
        db: PathBuf,
    },
    /// Compare two database files.
    Compare { a: PathBuf, b: PathBuf },
    /// Show statistics of the compiled-in database.
    Stats,
    /// Coverage report: entries per make by the kinds of data they carry and by sample files.
    Coverage {
        #[arg(default_value = "crates/flashraw-camera-db/data/cameras.json")]
        db: PathBuf,
    },
    /// Measure the camera's own image area from files (JSON lines): active area, resolved
    /// default crop and its source, recorded image size, embedded full-size JPEG size.
    /// Aggregated into `image_crops` entries by tools/camera_db_crops.py.
    Crops {
        files: Vec<PathBuf>,
        /// Locate the embedded full-size JPEG inside the active area by matching it
        /// against the developed raw data.
        #[arg(long)]
        register: bool,
        /// Also decode to RGBA8 (half size) and report the output dimensions.
        #[arg(long)]
        rgba: bool,
    },
    /// Report what each file states (geometry, CFA, levels, colour matrices, masked areas),
    /// its sample statistics and its database entry (JSON lines). Used by
    /// tools/camera_db/rederive.py.
    Probe { files: Vec<PathBuf> },
}

fn parse_rect(s: &str) -> Result<Rect, String> {
    let v: Vec<u32> = s
        .split(',')
        .map(|x| x.trim().parse().map_err(|_| format!("invalid number in {s:?}")))
        .collect::<Result<_, _>>()?;
    if v.len() != 4 {
        return Err("expected x,y,width,height".into());
    }
    Ok(Rect::new(v[0], v[1], v[2], v[3]))
}

fn policy(strict: bool) -> CorruptionPolicy {
    if strict { CorruptionPolicy::Strict } else { CorruptionPolicy::Recover }
}

fn decode_options(a: &DecodeArgs, raw: Option<&FlashRaw<'_>>) -> Result<DecodeOptions, String> {
    let camera_profile = match a.camera_profile.as_deref() {
        None => None,
        Some("embedded") => match raw {
            Some(r) => Some(
                r.embedded_camera_profile()
                    .ok_or("--camera-profile embedded: the file has no embedded camera profile")?,
            ),
            None => return Err("--camera-profile embedded is not supported here; pass a .dcp file".into()),
        },
        Some(p) => {
            let bytes = std::fs::read(p).map_err(|e| format!("{p}: {e}"))?;
            Some(CameraProfile::from_dcp(&bytes).map_err(|e| format!("{p}: {e}"))?)
        }
    };
    let output_profile = match &a.output_profile {
        Some(p) => {
            let bytes = std::fs::read(p).map_err(|e| format!("{}: {e}", p.display()))?;
            let intent = RenderingIntent::parse(&a.intent).ok_or_else(|| format!("unknown intent {:?}", a.intent))?;
            Some(OutputProfile::new(bytes).with_intent(intent).with_black_point_compensation(a.bpc))
        }
        None => None,
    };
    Ok(DecodeOptions {
        color_space: ColorSpace::parse(&a.color_space)
            .ok_or_else(|| format!("unknown color space {:?}", a.color_space))?,
        output_profile,
        backend: Backend::parse(&a.backend).ok_or_else(|| format!("unknown backend {:?}", a.backend))?,
        output: OutputFormat::parse(&a.output).ok_or_else(|| format!("unknown output {:?}", a.output))?,
        threads: a.threads,
        region: a.region,
        demosaic: Demosaic::parse(&a.demosaic).ok_or_else(|| format!("unknown demosaic {:?}", a.demosaic))?,
        highlights: Highlights::parse(&a.highlights).ok_or_else(|| format!("unknown highlights {:?}", a.highlights))?,
        white_balance: WhiteBalanceMode::parse(&a.white_balance)
            .ok_or_else(|| format!("unknown white balance {:?}", a.white_balance))?,
        white_balance_multipliers: a.wb_multipliers,
        noise_reduction: NoiseReduction::parse(&a.noise_reduction)
            .ok_or_else(|| format!("unknown noise reduction {:?}", a.noise_reduction))?,
        chromatic_aberration: a.chromatic_aberration,
        apply_opcodes: !a.no_opcodes,
        corruption_policy: policy(a.strict),
        exposure: a.exposure,
        black_point: a.black_point,
        white_point: a.white_point,
        preview_fallback: a.preview_fallback,
        camera_profile,
        ..Default::default()
    })
}

fn parse_multipliers(s: &str) -> Result<[f32; 3], String> {
    let v: Vec<f32> =
        s.split(',').map(|t| t.trim().parse::<f32>().map_err(|e| format!("{t:?}: {e}"))).collect::<Result<_, _>>()?;
    match v[..] {
        [r, g, b] if [r, g, b].iter().all(|x| x.is_finite() && *x > 0.0) => Ok([r, g, b]),
        _ => Err("expected three positive numbers R,G,B".into()),
    }
}

fn main() -> ExitCode {
    // Behave like a normal Unix tool when piped into `head` etc.: exit quietly on
    // EPIPE instead of panicking in `println!`.
    #[cfg(unix)]
    // SAFETY: restoring the default SIGPIPE disposition at startup, before any threads exist.
    unsafe {
        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
    }
    let cli = Cli::parse();
    match run(cli) {
        Ok(code) => code,
        Err(e) => {
            eprintln!("error: {e}");
            ExitCode::from(1)
        }
    }
}

fn open(path: &PathBuf) -> Result<FlashRaw<'static>, String> {
    FlashRaw::open_path(path).map_err(|e| format!("{}: {e}", path.display()))
}

fn run(cli: Cli) -> Result<ExitCode, String> {
    match cli.cmd {
        Cmd::Info { files, json } => {
            let mut failed = false;
            for f in &files {
                match open(f) {
                    Ok(raw) => {
                        if json {
                            println!("{}", output::info_json(f, &raw));
                        } else {
                            output::print_info(f, &raw);
                        }
                    }
                    Err(e) => {
                        failed = true;
                        if json {
                            println!("{}", serde_json::json!({"file": f, "error": e}));
                        } else {
                            eprintln!("{e}");
                        }
                    }
                }
            }
            Ok(if failed { ExitCode::from(2) } else { ExitCode::SUCCESS })
        }
        Cmd::DumpMetadata { file } => {
            let raw = open(&file)?;
            let v = serde_json::json!({
                "metadata": raw.metadata(),
                "frame": output::frame_info_json(raw.frame_info()),
                "capabilities": raw.capabilities(),
                "units": raw.units().len(),
            });
            println!("{}", serde_json::to_string_pretty(&v).unwrap());
            Ok(ExitCode::SUCCESS)
        }
        Cmd::Decode { file, out, format, args } => {
            let raw = open(&file)?;
            let opts = decode_options(&args, Some(&raw))?;
            let frame = match args.tile {
                Some(t) => raw.decode_tile(t, opts),
                None => raw.decode(opts),
            }
            .map_err(|e| format!("{}: {e}", file.display()))?;
            for w in frame.warnings() {
                eprintln!("warning: {}: {w}", file.display());
            }
            let fmt = format
                .or_else(|| out.extension().map(|e| e.to_string_lossy().to_lowercase()))
                .unwrap_or_else(|| "raw".into());
            output::write_frame(&frame, &out, &fmt)?;
            if args.profile {
                output::print_profile(&frame);
            }
            if args.json {
                println!(
                    "{}",
                    serde_json::json!({"width": frame.width, "height": frame.height, "cpp": frame.cpp, "stats": frame.stats,
                        "color_space": frame.color_space.name(), "icc_profile_bytes": frame.icc_profile().map(<[u8]>::len),
                        "white_balance": frame.white_balance, "white_balance_applied": frame.white_balance_applied,
                        "source": frame.source.name(), "incomplete": frame.is_incomplete(), "warnings": output::warnings_json(frame.warnings())})
                );
            } else {
                eprintln!("wrote {} ({}x{}x{})", out.display(), frame.width, frame.height, frame.cpp);
            }
            Ok(ExitCode::SUCCESS)
        }
        Cmd::Preview { file, out } => {
            let raw = open(&file)?;
            let p = raw.preview().cloned().ok_or("file has no embedded preview")?;
            output::write_preview(&raw, &p, &out)?;
            eprintln!("wrote {} ({}x{} {:?})", out.display(), p.width, p.height, p.encoding);
            Ok(ExitCode::SUCCESS)
        }
        Cmd::Thumbnail { file, out } => {
            let raw = open(&file)?;
            let p = raw.thumbnail().cloned().ok_or("file has no embedded thumbnail")?;
            output::write_preview(&raw, &p, &out)?;
            eprintln!("wrote {} ({}x{} {:?})", out.display(), p.width, p.height, p.encoding);
            Ok(ExitCode::SUCCESS)
        }
        Cmd::Benchmark { files, iterations, args } => {
            let opts = decode_options(&args, None)?;
            for f in &files {
                output::benchmark(f, &opts, iterations.max(1), args.json)?;
            }
            Ok(ExitCode::SUCCESS)
        }
        Cmd::Verify { files, json, dump_dir, apply_opcodes, strict } => {
            let mut failed = false;
            for f in &files {
                let r = output::verify(f, dump_dir.as_deref(), apply_opcodes, policy(strict));
                if r.get("error").is_some() {
                    failed = true;
                }
                if json {
                    println!("{r}");
                } else {
                    println!("{}", serde_json::to_string_pretty(&r).unwrap());
                }
            }
            Ok(if failed { ExitCode::from(2) } else { ExitCode::SUCCESS })
        }
        Cmd::Supported { json } => {
            output::supported(json);
            Ok(ExitCode::SUCCESS)
        }
        Cmd::CameraDb(c) => camera_db(c),
    }
}

fn camera_db(c: CameraDbCmd) -> Result<ExitCode, String> {
    use flashraw_camera_db::tooling::*;
    match c {
        CameraDbCmd::Validate { db } => {
            let d = load_db(&db)?;
            let problems = validate(&d);
            for p in &problems {
                println!("{p}");
            }
            println!(
                "{} cameras, {} fallback matrices, {} alias groups, {} size rules, {} problems",
                d.cameras.len(),
                d.matrices.len(),
                d.model_aliases.len(),
                d.size_rules.len(),
                problems.len()
            );
            Ok(if problems.is_empty() { ExitCode::SUCCESS } else { ExitCode::from(1) })
        }
        CameraDbCmd::Generate { db } => {
            let d = load_db(&db)?;
            write_db(&db, &d)?;
            println!(
                "rewrote {} ({} cameras); rebuild to regenerate the compiled tables",
                db.display(),
                d.cameras.len()
            );
            Ok(ExitCode::SUCCESS)
        }
        CameraDbCmd::Compare { a, b } => {
            let diffs = compare(&load_db(&a)?, &load_db(&b)?);
            for d in &diffs {
                println!("{d}");
            }
            println!("{} differences", diffs.len());
            Ok(ExitCode::SUCCESS)
        }
        CameraDbCmd::Crops { files, register, rgba } => {
            for f in &files {
                println!("{}", crops::measure(f, register, rgba));
            }
            Ok(ExitCode::SUCCESS)
        }
        CameraDbCmd::Coverage { db } => {
            for l in coverage(&load_db(&db)?) {
                println!("{l}");
            }
            Ok(ExitCode::SUCCESS)
        }
        CameraDbCmd::Probe { files } => {
            for f in &files {
                println!("{}", probe::probe(f));
            }
            Ok(ExitCode::SUCCESS)
        }
        CameraDbCmd::Stats => {
            let all = flashraw_camera_db::all();
            let mut makes = std::collections::BTreeMap::<&str, usize>::new();
            for c in all {
                *makes.entry(c.canonical_make).or_default() += 1;
            }
            println!(
                "{} entries, {} fallback matrices, {} alias groups, {} size rules",
                all.len(),
                flashraw_camera_db::MATRICES.len(),
                flashraw_camera_db::ALIAS_GROUPS.len(),
                flashraw_camera_db::SIZE_RULES.len()
            );
            for (m, n) in makes {
                println!("  {m:24} {n}");
            }
            Ok(ExitCode::SUCCESS)
        }
    }
}