# Conflicts:
#	crates/engine/src/media.rs
This commit is contained in:
Claude committed 2026-10-11 11:47:51 +00:00
commit edb95894b4
5 files changed
+78 -39

No files matched your search

+1 -1
View File
@@ -62,7 +62,7 @@ impl SettingsHashes {
}
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
pub const RENDER_CACHE_VERSION: u64 = 53;
pub const RENDER_CACHE_VERSION: u64 = 54;
/// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];
+3 -1
View File
@@ -694,7 +694,9 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
// validated against Sony's exports with that centre. The opcode's centre is in active-area coordinates, so
// it stays on the optical centre whichever crop frames the image.
opcodes: OpcodeLists {
list3: if linear_rgb {
// DefaultCrop can retain native geometry while Sony's tags select an aspect crop.
// Both the image framing and the table geometry must stay within the validated 3:2 scope.
list3: if linear_rgb || crop.height == 0 || (crop.width as f64 / crop.height as f64 - 1.5).abs() > 0.005 {
Vec::new()
} else {
let geometry = dng_default_crop(raw, w, h).unwrap_or(crop);
+32 -29
View File
@@ -194,36 +194,39 @@ mod tests {
fn header_and_full_decode_preserve_correction_through_dng() {
use lightcraft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter, tags as t};
for (model, expected) in [("ILCE-7RM4A", 1), ("ILCE-7RM4", 0), ("ILCE-7M3", 0), ("", 0)] {
for order in [ByteOrder::Little, ByteOrder::Big] {
let mut raw = IfdBuilder::new();
raw.set(t::MAKE, Value::Ascii("SONY".into()));
raw.set(t::MODEL, Value::Ascii(model.into()));
raw.set(t::IMAGE_WIDTH, Value::Long(vec![32]));
raw.set(t::IMAGE_LENGTH, Value::Long(vec![24]));
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
raw.set(t::COMPRESSION, Value::Short(vec![1]));
raw.set(t::WHITE_LEVEL, Value::Long(vec![16383]));
raw.set(0x74c7, Value::Long(vec![0, 2]));
raw.set(t::DEFAULT_CROP_ORIGIN, Value::Long(vec![2, 2]));
raw.set(t::DEFAULT_CROP_SIZE, Value::Long(vec![30, 20]));
raw.set(0x74c8, Value::Long(vec![30, 20]));
raw.set(DISTORTION, Value::SShort(std::iter::once(16).chain(TELE).collect()));
// Zero pixels have the same byte representation in both orders.
raw.set_image(ImageData::Strips { rows_per_strip: 24, strips: vec![vec![0; 32 * 24 * 2]] });
let file = TiffWriter { order, ..Default::default() }.write(&[raw]).unwrap();
let header = super::super::arw::decode(&file, crate::Mode::Header).unwrap();
let full = super::super::arw::decode(&file, crate::Mode::Full).unwrap();
assert_eq!(header.info(), full.info());
assert_eq!(full.opcodes.list3.len(), expected, "{model} {order:?}");
// the image keeps Sony's crop tags; the warp is centred on the DNG default crop (x 2..32 of 32)
assert_eq!(full.crop, Rect::new(0, 2, 30, 20));
if let Some(Opcode::WarpRectilinear { center, .. }) = full.opcodes.list3.first() {
assert!((center[0] - 16.5 / 31.0).abs() < 1e-12 && (center[1] - 11.5 / 23.0).abs() < 1e-12, "{center:?}");
for (height, expected) in [(20, expected), (16, 0)] {
for order in [ByteOrder::Little, ByteOrder::Big] {
let mut raw = IfdBuilder::new();
raw.set(t::MAKE, Value::Ascii("SONY".into()));
raw.set(t::MODEL, Value::Ascii(model.into()));
raw.set(t::IMAGE_WIDTH, Value::Long(vec![32]));
raw.set(t::IMAGE_LENGTH, Value::Long(vec![24]));
raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
raw.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![1]));
raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
raw.set(t::COMPRESSION, Value::Short(vec![1]));
raw.set(t::WHITE_LEVEL, Value::Long(vec![16383]));
raw.set(0x74c7, Value::Long(vec![0, 2]));
raw.set(t::DEFAULT_CROP_ORIGIN, Value::Long(vec![2, 2]));
raw.set(t::DEFAULT_CROP_SIZE, Value::Long(vec![30, 20]));
// An aspect crop must stay excluded even when DefaultCrop still describes 3:2.
raw.set(0x74c8, Value::Long(vec![30, height]));
raw.set(DISTORTION, Value::SShort(std::iter::once(16).chain(TELE).collect()));
// Zero pixels have the same byte representation in both orders.
raw.set_image(ImageData::Strips { rows_per_strip: 24, strips: vec![vec![0; 32 * 24 * 2]] });
let file = TiffWriter { order, ..Default::default() }.write(&[raw]).unwrap();
let header = super::super::arw::decode(&file, crate::Mode::Header).unwrap();
let full = super::super::arw::decode(&file, crate::Mode::Full).unwrap();
assert_eq!(header.info(), full.info());
assert_eq!(full.opcodes.list3.len(), expected, "{model} {order:?} height={height}");
// the image keeps Sony's crop tags; the warp is centred on the DNG default crop (x 2..32 of 32)
assert_eq!(full.crop, Rect::new(0, 2, 30, height as usize));
if let Some(Opcode::WarpRectilinear { center, .. }) = full.opcodes.list3.first() {
assert!((center[0] - 16.5 / 31.0).abs() < 1e-12 && (center[1] - 11.5 / 23.0).abs() < 1e-12, "{center:?}");
}
let dng = crate::write_dng(&full, &Default::default()).unwrap();
assert_eq!(crate::decode(&dng).unwrap().opcodes.list3, full.opcodes.list3);
}
let dng = crate::write_dng(&full, &Default::default()).unwrap();
assert_eq!(crate::decode(&dng).unwrap().opcodes.list3, full.opcodes.list3);
}
}
}
+12
View File
@@ -956,6 +956,18 @@ fn sony_embedded_distortion_is_limited_to_validated_models() {
("arw-sony-a7rm2-12bit-uncompressed.arw", "ILCE-7RM2", 0),
("arw-sony-rx100.arw", "DSC-RX100", 0),
("arw-sony-rx100m3.arw", "DSC-RX100M3", 0),
("arw-sony-a7cr-lossless-l.arw", "ILCE-7CR", 0),
("arw-sony-a7cr-lossless-m.arw", "ILCE-7CR", 0),
("arw-sony-a7cr-compressed.arw", "ILCE-7CR", 0),
("arw-sony-a7rm4-14bit-compressed.arw", "ILCE-7RM4", 0),
("arw-sony-a500.arw", "DSLR-A500", 0),
("arw-sony-a700.arw", "DSLR-A700", 0),
("arw-sony-a33.arw", "SLT-A33", 0),
("arw-sony-a3500-5600k.arw", "ILCE-3500", 0),
("arw-sony-a7s-shade.arw", "ILCE-7S", 0),
("arw-sony-a7sm2-16x9.arw", "ILCE-7SM2", 0),
("arw-sony-a580-16x9.arw", "DSLR-A580", 0),
("arw-sony-a900-packed12.arw", "DSLR-A900", 0),
] {
let path = root.join(name);
if !path.exists() {
+30 -8
View File
@@ -64,7 +64,8 @@ from the interpolated file table was below 0.12 px at 1440 px. That measures app
not agreement with an independent renderer. Camera JPEG and Apple Core Image comparisons support
the wide-lens interpretation but are weaker evidence than same-renderer On/Off pairs.
Actual LightCraft exports after crop normalization were also compared directly with Sony's corrected
Historical validation before restoring main's image crop precedence: LightCraft exports with the
standard default crop framing were compared directly with Sony's corrected
TIFFs. At 2376 px wide, median / 95th-percentile feature residuals were 0.35 / 1.04 px at 24 mm,
0.15 / 0.40 px at 200 mm, and 0.13 / 0.56 px at 600 mm (median translation below 0.024 px).
The uncorrected medians at 200 and 600 mm were 8.63 and 10.82 px respectively. Different colour,
@@ -73,6 +74,20 @@ An isolated desktop run on Apple M1 Metal detected the profile, rendered without
warnings, and exposed the existing Optics controls. Existing library edits are not reset: once the
source has loaded, enable lens corrections if they were previously disabled.
After restoring main's crop precedence, fresh 2376×1584 exports on 2026-10-10
(main `b70ae174` plus the aspect-framing guard) measured:
| Focal length | Corrected median / p95 residual | Uncorrected median | Removed translation x / y | Matches / maximum radius |
|---|---|---|---|---|
| 24 mm | 0.36 / 1.12 px | 5.62 px | 7.98 / −0.01 px | 1,442 / 0.95 |
| 200 mm | 0.13 / 0.35 px | 8.65 px | 8.01 / 0.02 px | 11,606 / 0.98 |
| 600 mm | 0.10 / 0.37 px | 10.33 px | 8.02 / 0.00 px | 12,268 / 0.97 |
Residuals subtract only the median feature translation; no scale or lens coefficients are fitted to
these reference pairs. The roughly eight-pixel horizontal translation is the retained crop-origin
difference described above, not a claim of exact Sony framing. Maximum radius is in half diagonals.
The distortion geometry remains aligned while every body's image framing follows main's crop order.
Tests cover signedness/length rejection, zero and malformed tables, independent 600 mm reference
geometry, barrel framing, offset crop normalization, both TIFF byte orders, header/full consistency,
and preservation through DNG export. No private photographs or reference TIFFs are committed.
@@ -84,10 +99,9 @@ model names, and every other model remain unsupported by this distortion decoder
numbers and table lengths do not establish the scale, interpolation or crop normalization on another
body. Support can expand after independent geometry validation; do not add model-name prefix matching.
The original nine Sony samples listed by `cargo xtask corpus --download` were downloaded from its
CC0 URLs. The manifest now also includes CC0 samples 4822 (A7R IVA / 24–105 mm) and 3989
(A9 II / 200–600 mm), with their SHA-256 hashes recorded alongside the URLs. All eleven files
are checked with `sony_embedded_distortion_is_limited_to_validated_models`:
The current `cargo xtask corpus --download` manifest contains 23 Sony CC0 samples across 17 bodies,
including samples 4822 (A7R IVA / 24–105 mm) and 3989 (A9 II / 200–600 mm). All 23 files have
pinned SHA-256 hashes and are checked with `sony_embedded_distortion_is_limited_to_validated_models`:
| Camera | Corpus samples | Raw-IFD table observed | Expected correction |
|---|---|---|---|
@@ -98,15 +112,23 @@ are checked with `sony_embedded_distortion_is_limited_to_validated_models`:
| ILCE-7RM2 | 12-bit uncompressed | 16 samples | None |
| DSC-RX100 | One | No raw-IFD table | None |
| DSC-RX100M3 | One | 11 samples | None |
| ILCE-7CR | Compressed, lossless L/M | Not used; model excluded | None |
| ILCE-7RM4 | 14-bit compressed | Not used; model excluded | None |
| DSLR-A500, DSLR-A700, SLT-A33 | One each | Not used; model excluded | None |
| ILCE-3500, ILCE-7S | 5600 K, Shade respectively | Not used; model excluded | None |
| ILCE-7SM2, DSLR-A580 | 16:9 framing, one each | Not used; model excluded | None |
| DSLR-A900 | Packed 12-bit | Not used; model excluded | None |
These are **model-boundary regression checks, not new geometric validation of those models**. In particular,
refusing the A7 III tables resolves the unverified corner warp identified in review. The lossless M/S
files are linear YCbCr and remain excluded independently of the model check. Header and full decode
must agree for every sample. Synthetic tests also verify both TIFF byte orders, exact model matching,
that the warp is centred on the standard default crop while the image keeps Sony's crop, and that DNG export preserves an accepted warp without adding one to rejected
models. The existing independent Sony geometry tests continue to cover the enabled A7R IVA path.
Re-exporting the 24, 200 and 600 mm reference files after the model restriction produced byte-identical
corrected PNGs to the earlier validated exports. Isolated headless app checks showed the A7R IVA profile
models. A non-3:2 Sony image crop is also rejected when the standard default crop still describes
3:2 geometry; the regression fails without the image-framing guard. The existing independent Sony
geometry tests continue to cover the enabled A7R IVA path.
Before restoring main's crop precedence, re-exporting the 24, 200 and 600 mm reference files after
the model restriction produced byte-identical corrected PNGs to the earlier validated exports. Isolated headless app checks showed the A7R IVA profile
controls present and the A7 III profile controls absent, with no notices and completed RAW renders.
Before enabling another body, compare corrected and uncorrected renders against independent references