Files
lightcraft/crates/engine/tests/preview_read.rs
T
goujichengandDeepSeek V3.2 ab438b05ca LR-IMP-PREVIEW-READ: a preview reads the raw's header, not the raw
A grid thumbnail and a loupe preview of an unedited raw show the camera's embedded JPEG, which
sits near the start of the file — for a 24 MP Fuji RAF, 5.6 MB of a 56 MB file (measured:
DSCF9492.RAF). The loader read the file whole and threw the sensor data away, so every preview
paid for 50 MB nobody looked at. On a local disk that is merely wasteful; on a network share,
where photographers keep libraries, it is the wait: the same preview took 10 s over a GVFS/SMB
Wi-Fi share against 0.96 s after this change.

- `lightcraft_raw::bounded_preview(bytes)` locates the preview from the header alone and reports
  its byte range; `embedded_preview_from_range` extracts it, byte for byte what `embedded_preview`
  returns for the whole file (the pointer is read from the RAF header's fixed u32s at 84/88).
- `engine::files::preview_window` reads a 4 KiB prefix, then — because the container reader clamps
  a slice to the buffer it was given, so a short prefix reports a plausible but truncated 3948
  bytes — re-reads exactly the extent the header declares, and takes the window out of the prefix
  rather than reading those bytes twice. `LIGHTCRAFT_PROFILE=1` prints what each preview cost.
- Formats whose preview only the whole file reveals (CR3 boxes, TIFF pointer tags past the prefix,
  CRW/MRW/X3F scans), a lying pointer, a short read and a file that changes underneath us all fall
  back to the whole-file path: a damaged header never costs the user their preview.
- Tests: the header range matches `embedded_preview` byte for byte, with a clamped prefix, a padded
  extent, a late pointer, a lying pointer and a non-JPEG window covered; on a synthetic RAF with a
  200 KB tail the quick path and the whole-file path produce identical pixels and the read stays
  under half the file; `crates/engine/tests/preview_read.rs` (opt-in, `#[ignore]`) measures a real
  file on a share. No raw fixture is committed.
- `docs/raw-preview-reads.md` records the mechanism and the numbers.

Co-Authored-By: DeepSeek V3.2 <noreply@deepseek.com>
2026-10-10 22:16:04 +08:00

43 lines
2.0 KiB
Rust

//! Opt-in test of the embedded-preview read path on a **real file the user has** — typically one on a
//! network share, which is where reading only the header instead of the whole raw matters:
//!
//! LC_PREVIEW_RAW="/run/user/1000/gvfs/smb-share:.../DSCF0001.RAF" \
//! cargo test --release -p lightcraft-engine --test preview_read -- --ignored --nocapture
//!
//! It reports what the preview cost and fails when the read is not the small part of the file the
//! header path promises. Skipped without `LC_PREVIEW_RAW` (there is no committed raw fixture: media
//! is never committed, see AGENTS.md).
use std::time::Instant;
#[test]
#[ignore = "needs LC_PREVIEW_RAW pointing at a real raw file"]
fn a_preview_reads_only_the_header_and_the_preview() {
let Ok(path) = std::env::var("LC_PREVIEW_RAW") else {
eprintln!("skipped: set LC_PREVIEW_RAW=<path to a raw file>");
return;
};
let file_len = std::fs::metadata(&path).expect("the file exists").len();
assert!(file_len > 0, "empty file");
let started = Instant::now();
let window =
lightcraft_engine::files::preview_window(&path).expect("the file reads").expect("it carries an embedded preview the header can point at");
let elapsed = started.elapsed();
let (_, preview, read) = window;
let frac = read.fraction();
println!(
"preview of {path}\n read {} of {} bytes ({:.1} %) in {:.2} s{}",
read.read,
read.total,
frac * 100.0,
elapsed.as_secs_f64(),
if read.header_path { " [header path]" } else { " [whole file]" }
);
assert!(read.header_path, "the header path must answer for a raw whose pointer is in its header");
assert_eq!(read.total, file_len, "the report covers the whole file");
assert!(frac < 0.5, "a preview must not cost most of the file: read {} of {} bytes", read.read, read.total);
assert!(preview.starts_with(&[0xff, 0xd8]), "the preview bytes are a JPEG: {:?}", preview.get(..4));
}