diff --git a/crates/catalog/src/lib.rs b/crates/catalog/src/lib.rs
--- a/crates/catalog/src/lib.rs
+++ b/crates/catalog/src/lib.rs
@@ -928,7 +928,7 @@ impl Catalog {
             }
             Op::SetEmbeddedLens { id, lens } => {
                 let p = self.photo_mut(id)?;
-                Op::SetEmbeddedLens { id, lens: std::mem::replace(&mut p.embedded_lens, lens.map(|l| *l)).map(Box::new) }
+                Op::SetEmbeddedLens { id, lens: std::mem::replace(&mut p.embedded_lens, lens) }
             }
             Op::SetAsShotWb { id, wb } => {
                 if wb.is_some_and(|(t, tint)| !(t.is_finite() && tint.is_finite())) {
diff --git a/crates/catalog/src/model.rs b/crates/catalog/src/model.rs
--- a/crates/catalog/src/model.rs
+++ b/crates/catalog/src/model.rs
@@ -276,7 +276,7 @@ pub struct Photo {
     /// Lens corrections embedded in the file (DNG `WarpRectilinear` / `FixVignetteRadial`), applied when
     /// "Enable Profile Corrections" is on.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub embedded_lens: Option<lightcraft_develop::EmbeddedLens>,
+    pub embedded_lens: Option<Box<lightcraft_develop::EmbeddedLens>>,
     /// A virtual copy: the photo it was copied from (it shares that photo's file but has its own
     /// settings, metadata and history).
     #[serde(default, skip_serializing_if = "Option::is_none")]
diff --git a/crates/catalog/src/tests.rs b/crates/catalog/src/tests.rs
--- a/crates/catalog/src/tests.rs
+++ b/crates/catalog/src/tests.rs
@@ -568,7 +568,7 @@ fn set_embedded_lens_is_undoable_and_journaled() {
     let op = Op::SetEmbeddedLens { id: a, lens: Some(Box::new(lens)) };
     let op: Op = serde_json::from_str(&serde_json::to_string(&op).unwrap()).unwrap();
     let inv = c.apply(op).unwrap();
-    assert_eq!(c.photo(a).unwrap().embedded_lens, Some(lens));
+    assert_eq!(c.photo(a).unwrap().embedded_lens, Some(Box::new(lens)));
     let inv: Op = serde_json::from_str(&serde_json::to_string(&inv).unwrap()).unwrap();
     c.apply(inv).unwrap();
     assert_eq!(c.photo(a).unwrap().embedded_lens, None);
@@ -1046,3 +1046,18 @@ fn a_batch_that_cannot_be_rolled_back_whole_makes_earlier_changes_unknown() {
     assert_eq!(c.touched_since(start), None, "so nothing is said of what changed");
     assert_eq!(c.touched_since(c.revision), Some(vec![]), "from after it, it is known again");
 }
+
+/// `Photo::embedded_lens` is boxed to keep photo records small; the box doesn't change the serialized form:
+/// a photo written with the lens reads back with it, and its JSON is the bare lens, as before the box.
+#[test]
+fn boxed_embedded_lens_serializes_like_the_bare_lens() {
+    let lens = lightcraft_develop::EmbeddedLens { warp: Some(Default::default()), vignette: None, stages: Default::default() };
+    let mut p = Photo::new(PhotoId(1), Source::File { path: "/x.dng".into() }, "x.dng", "DNG", 4000, 3000, "2026-10-06T00:00:00");
+    p.embedded_lens = Some(Box::new(lens));
+    let json = serde_json::to_value(&p).unwrap();
+    assert_eq!(json["embedded_lens"], serde_json::to_value(lens).unwrap());
+    let back: Photo = serde_json::from_value(json).unwrap();
+    assert_eq!(back.embedded_lens.as_deref(), Some(&lens));
+    p.embedded_lens = None;
+    assert!(serde_json::to_value(&p).unwrap().get("embedded_lens").is_none(), "no lens, no field");
+}
diff --git a/crates/engine/src/cmd/convert.rs b/crates/engine/src/cmd/convert.rs
--- a/crates/engine/src/cmd/convert.rs
+++ b/crates/engine/src/cmd/convert.rs
@@ -260,7 +260,7 @@ fn fill_missing_meta(id: lightcraft_catalog::PhotoId, ph: &lightcraft_catalog::P
 /// what import gave it, the lens correction is turned on as import would; an edited photo keeps
 /// its settings.
 pub(crate) fn lens_ops(id: lightcraft_catalog::PhotoId, ph: &lightcraft_catalog::Photo, info: &crate::media::ProbeInfo) -> Vec<Op> {
-    if info.embedded_lens == ph.embedded_lens {
+    if info.embedded_lens.as_ref() == ph.embedded_lens.as_deref() {
         return Vec::new();
     }
     let mut ops = vec![Op::SetEmbeddedLens { id, lens: info.embedded_lens.map(Box::new) }];
@@ -486,7 +486,7 @@ mod tests {
             c.apply(op).unwrap();
         }
         let after = c.photo(lightcraft_catalog::PhotoId(1)).unwrap();
-        assert_eq!(after.embedded_lens, Some(lens()));
+        assert_eq!(after.embedded_lens, Some(Box::new(lens())));
         assert!(!after.is_edited());
         assert_eq!(*after.develop, after.camera_defaults());
         assert!(lens_ops(after.id, after, &info).is_empty(), "nothing more to do the second time");
@@ -507,7 +507,7 @@ mod tests {
     #[test]
     fn reload_leaves_a_photo_whose_user_switched_the_correction_off_alone() {
         let mut ph = old_rw2();
-        ph.embedded_lens = Some(lens());
+        ph.embedded_lens = Some(Box::new(lens()));
         let mut d = ph.camera_defaults();
         d.optics.lens_profile = false;
         ph.develop = std::sync::Arc::new(d);
diff --git a/crates/engine/src/files.rs b/crates/engine/src/files.rs
--- a/crates/engine/src/files.rs
+++ b/crates/engine/src/files.rs
@@ -2881,7 +2881,7 @@ pub(crate) mod tests {
                 let name = format!("x.{format}");
                 let mut p = Photo::new(PhotoId(1), Source::File { path: format!("/{name}") }, &name, format, 64, 48, "");
                 p.kind = MediaKind::Raw;
-                p.embedded_lens = embedded;
+                p.embedded_lens = embedded.map(Box::new);
                 crate::import::apply_import_defaults(&s, &mut p);
                 assert_eq!(p.develop.optics.lens_profile, embedded.is_some(), "{format} {embedded:?}");
             }
diff --git a/crates/engine/src/import.rs b/crates/engine/src/import.rs
--- a/crates/engine/src/import.rs
+++ b/crates/engine/src/import.rs
@@ -1470,7 +1470,7 @@ pub fn commit_prepared(s: &mut Session, opts: &ImportOptions, now: &str, prepare
                 p.meta = info.meta;
                 p.as_shot_wb = info.as_shot_wb;
                 p.content_hash = info.content_hash;
-                p.embedded_lens = info.embedded_lens;
+                p.embedded_lens = info.embedded_lens.map(Box::new);
                 p.set_preview_only(info.preview_only.clone());
                 apply_import_defaults(s, &mut p);
                 if let Some(sc) = &sidecar {
@@ -1593,7 +1593,7 @@ impl Session {
         p.kind = c.kind;
         if let Some(info) = self.import_probes.get(&c.path) {
             p.as_shot_wb = info.as_shot_wb;
-            p.embedded_lens = info.embedded_lens;
+            p.embedded_lens = info.embedded_lens.map(Box::new);
             p.preview_only = info.preview_only.clone();
         }
         p.develop = std::sync::Arc::new(p.import_defaults());
diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs
--- a/crates/engine/src/lib.rs
+++ b/crates/engine/src/lib.rs
@@ -960,7 +960,7 @@ impl Session {
             photo.height.max(1) as usize,
             &d,
             true,
-            photo.embedded_lens.as_ref(),
+            photo.embedded_lens.as_deref(),
         );
         d.crop.geometry.rect = frame.crop.rect;
         d
diff --git a/crates/engine/src/media.rs b/crates/engine/src/media.rs
--- a/crates/engine/src/media.rs
+++ b/crates/engine/src/media.rs
@@ -1238,7 +1238,7 @@ fn header_info(p: &Photo) -> SourceInfo {
             raw: true,
             as_shot_temp: temp,
             as_shot_tint: tint,
-            lens: p.embedded_lens,
+            lens: p.embedded_lens.as_deref().copied(),
             relative_wb: relative,
             // (a fitted-look raw edited in Kelvin: its camera's matrices give the scale, see `files::WbScaleOf`)
             legacy_relative_wb: p.fitted_look() && !relative,
@@ -1525,7 +1525,7 @@ impl crate::Session {
             p.height.max(1) as usize,
             &job.settings,
             apply_crop,
-            p.embedded_lens.as_ref(),
+            p.embedded_lens.as_deref(),
         );
         let ppl = frame.px_per_long(full_w);
         lightcraft_pipeline::spots::window_for_reads_checked(&job.settings, &frame, full_w, full_h, ppl, window.clamped(full_w, full_h))?;
diff --git a/crates/engine/src/slide.rs b/crates/engine/src/slide.rs
--- a/crates/engine/src/slide.rs
+++ b/crates/engine/src/slide.rs
@@ -87,7 +87,7 @@ impl crate::Session {
             p.height.max(1) as usize,
             &p.develop,
             true,
-            p.embedded_lens.as_ref(),
+            p.embedded_lens.as_deref(),
         );
         let a = frame.aspect();
         Some(if a.is_finite() && a > 0.0 { a } else { 1.5 })
diff --git a/crates/engine/src/tests_constrain_crop.rs b/crates/engine/src/tests_constrain_crop.rs
--- a/crates/engine/src/tests_constrain_crop.rs
+++ b/crates/engine/src/tests_constrain_crop.rs
@@ -17,7 +17,7 @@ fn fits(s: &Session) -> bool {
     let p = s.catalog.photo(s.active().unwrap()).unwrap();
     let mut d = (*p.develop).clone();
     d.geometry.constrain_crop = true;
-    let f = Frame::with_lens(p.width as usize, p.height as usize, &d, true, p.embedded_lens.as_ref());
+    let f = Frame::with_lens(p.width as usize, p.height as usize, &d, true, p.embedded_lens.as_deref());
     let (a, b) = (f.crop.rect, p.develop.crop.geometry.rect);
     [a.x0 - b.x0, a.y0 - b.y0, a.x1 - b.x1, a.y1 - b.y1].iter().all(|d| d.abs() < 1e-9)
 }
diff --git a/crates/engine/src/tests_merge.rs b/crates/engine/src/tests_merge.rs
--- a/crates/engine/src/tests_merge.rs
+++ b/crates/engine/src/tests_merge.rs
@@ -100,8 +100,8 @@ fn hdr_merge_keeps_the_reference_frames_lens_corrections() {
         let reference = s.catalog.photo(lightcraft_catalog::PhotoId(ids[1])).unwrap().clone();
         let expect = if orientation == Orientation::Normal { (452, 304) } else { (304, 452) };
         assert_eq!(((merged.width, merged.height), (reference.width, reference.height)), (expect, expect));
-        let a = reference.embedded_lens.expect("the source's correction");
-        let b = merged.embedded_lens.expect("the merged DNG carries the reference frame's correction");
+        let a = reference.embedded_lens.as_deref().copied().expect("the source's correction");
+        let b = merged.embedded_lens.as_deref().copied().expect("the merged DNG carries the reference frame's correction");
         let (wa, wb) = (a.warp.unwrap(), b.warp.unwrap());
         let (va, vb) = (a.vignette.unwrap(), b.vignette.unwrap());
         let (w, h) = (expect.0 as f64, expect.1 as f64);
diff --git a/crates/ui-egui/examples/thumb_request_bench.rs b/crates/ui-egui/examples/thumb_request_bench.rs
--- a/crates/ui-egui/examples/thumb_request_bench.rs
+++ b/crates/ui-egui/examples/thumb_request_bench.rs
@@ -195,7 +195,7 @@ fn real() {
         p.kind = probe.kind;
         p.meta = probe.meta;
         p.as_shot_wb = probe.as_shot_wb;
-        p.embedded_lens = probe.embedded_lens;
+        p.embedded_lens = probe.embedded_lens.map(Box::new);
         p.content_hash = probe.content_hash;
         p.preview_only = probe.preview_only;
         lightcraft_engine::import::apply_import_defaults(&session, &mut p);
diff --git a/crates/ui-egui/src/panels/compare.rs b/crates/ui-egui/src/panels/compare.rs
--- a/crates/ui-egui/src/panels/compare.rs
+++ b/crates/ui-egui/src/panels/compare.rs
@@ -212,7 +212,7 @@ fn photo_tile(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, slot: Slo
         photo.height.max(1) as usize,
         &photo.develop,
         true,
-        photo.embedded_lens.as_ref(),
+        photo.embedded_lens.as_deref(),
     );
     let aspect = frame.aspect() as f32;
     let native = super::detail::output_px(&frame);
diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs
--- a/crates/ui-egui/src/panels/detail.rs
+++ b/crates/ui-egui/src/panels/detail.rs
@@ -296,7 +296,7 @@ pub(crate) fn output_px(frame: &Frame) -> [usize; 2] {
 pub(crate) fn focus_box(photo: &lightcraft_catalog::Photo) -> Option<lightcraft_geom::Rect> {
     let f = photo.meta.focus.as_ref()?;
     let d = &photo.develop;
-    let frame = Frame::with_lens(photo.width.max(1) as usize, photo.height.max(1) as usize, d, true, photo.embedded_lens.as_ref());
+    let frame = Frame::with_lens(photo.width.max(1) as usize, photo.height.max(1) as usize, d, true, photo.embedded_lens.as_deref());
     let [w, h] = output_px(&frame);
     let to_out = frame.out_to_norm(w, h).inverse()?;
     // stored on the upright photo: the user's Rotate / Flip on top, then the crop
@@ -527,7 +527,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
     // the full-screen preview shows the photo only: no tool overlays
     let right = if fullscreen { RightPanel::None } else { app.ui.right };
     let crop_tool = right == RightPanel::Crop;
-    let frame = Frame::with_lens(photo.width.max(1) as usize, photo.height.max(1) as usize, &d, !crop_tool, photo.embedded_lens.as_ref());
+    let frame = Frame::with_lens(photo.width.max(1) as usize, photo.height.max(1) as usize, &d, !crop_tool, photo.embedded_lens.as_deref());
     let aspect = frame.aspect() as f32;
     let ppp = ui.ctx().pixels_per_point();
     let area = canvas.shrink(if fullscreen {
@@ -701,7 +701,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
             for (n, nid) in [next, prev].into_iter().enumerate() {
                 let Some(nid) = nid else { continue };
                 let Some(np) = app.session.catalog.photo(nid).cloned() else { continue };
-                let nf = Frame::with_lens(np.width.max(1) as usize, np.height.max(1) as usize, &np.develop, !crop_tool, np.embedded_lens.as_ref());
+                let nf = Frame::with_lens(np.width.max(1) as usize, np.height.max(1) as usize, &np.develop, !crop_tool, np.embedded_lens.as_deref());
                 let na = nf.aspect() as f32;
                 let nr = fit_rect(main_area, na, app.ui.zoom, output_px(&nf), ppp, app.ui.pan);
                 let nw =
diff --git a/crates/ui-egui/src/panels/slideshow.rs b/crates/ui-egui/src/panels/slideshow.rs
--- a/crates/ui-egui/src/panels/slideshow.rs
+++ b/crates/ui-egui/src/panels/slideshow.rs
@@ -194,7 +194,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context, now: f64) {
 /// [`SlideSettings::is_plain`]: lightcraft_engine::slide::SlideSettings::is_plain
 fn slide_job(app: &mut LightcraftApp, id: PhotoId, area: Rect, ppp: f32, texture_side: usize) -> Option<lightcraft_engine::RenderJob> {
     let photo = app.session.catalog.photo(id)?.clone();
-    let frame = Frame::with_lens(photo.width.max(1) as usize, photo.height.max(1) as usize, &photo.develop, true, photo.embedded_lens.as_ref());
+    let frame = Frame::with_lens(photo.width.max(1) as usize, photo.height.max(1) as usize, &photo.develop, true, photo.embedded_lens.as_deref());
     let native = super::detail::output_px(&frame);
     if !app.session.slides_plain() {
         let (w, h) = slide_px(area, ppp, texture_side);
