diff --git a/crates/doc/src/tables.rs b/crates/doc/src/tables.rs
--- a/crates/doc/src/tables.rs
+++ b/crates/doc/src/tables.rs
@@ -787,6 +787,10 @@ pub struct PageSetup {
     /// The layout's paper units are millimetres (plot paper units 1 in DXF) whatever the
     /// drawing's MEASUREMENT says; otherwise paper units follow MEASUREMENT (#1058).
     pub paper_mm: bool,
+    /// Paper image origin (DXF plot settings groups 148/149) as read from a file; `None` for a
+    /// page setup CADCraft created (written as 0,0).
+    #[serde(default)]
+    pub paper_image_origin: Option<Vec2>,
 }
 
 impl Default for PageSetup {
@@ -808,6 +812,7 @@ impl Default for PageSetup {
             show_plot_styles: false,
             plot_transparency: false,
             paper_mm: false,
+            paper_image_origin: None,
         }
     }
 }
@@ -823,11 +828,15 @@ pub struct Layout {
     /// Saved paper-space view (centre, height).
     #[serde(default)]
     pub view: Option<(Vec2, f64)>,
+    /// The layout's limits (LIMMIN, LIMMAX; DXF AcDbLayout groups 10/11) as read from a file;
+    /// `None` for a layout CADCraft created (written as (0,0)-(12,9)).
+    #[serde(default)]
+    pub limits: Option<(Vec2, Vec2)>,
 }
 
 impl Layout {
     pub fn new(name: &str, tab_order: u32) -> Self {
-        Layout { name: name.into(), tab_order, page: PageSetup::default(), entities: EntityStore::new(), view: None }
+        Layout { name: name.into(), tab_order, page: PageSetup::default(), entities: EntityStore::new(), view: None, limits: None }
     }
 }
 
diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs
--- a/crates/engine/src/cmd/layout.rs
+++ b/crates/engine/src/cmd/layout.rs
@@ -309,7 +309,7 @@ fn run_copy(s: &mut Session, p: &Value) -> Result<Value> {
         let h = d.new_handle();
         store.push(Entity { handle: h, ..Entity::clone(e) });
     }
-    d.layouts.push(Layout { name: to.clone(), tab_order: order, page: src.page, entities: store, view: src.view });
+    d.layouts.push(Layout { name: to.clone(), tab_order: order, page: src.page, entities: store, view: src.view, limits: src.limits });
     Ok(json!({ "name": to }))
 }
 
diff --git a/crates/io/src/dxf_read.rs b/crates/io/src/dxf_read.rs
--- a/crates/io/src/dxf_read.rs
+++ b/crates/io/src/dxf_read.rs
@@ -1701,7 +1701,7 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
     d.header.set_i64("INSUNITS", 0);
     let mut rx = Rx { pstyles: plot_style_names(&secs), ..Rx::default() };
     let mut blocks: Vec<(String, Vec3, String, Vec<(Option<String>, bool, Entity)>, i64)> = Vec::new();
-    let mut layout_objs: Vec<(String, u32, String, PageSetup)> = Vec::new();
+    let mut layout_objs: Vec<(String, u32, String, PageSetup, Option<(Vec2, Vec2)>)> = Vec::new();
     let mut entities = Vec::new();
     let mut objs = Objects::default();
     // Every handle another program's file already uses is taken before anything gets a new one:
@@ -1774,7 +1774,17 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
                         // LAYOUT has two 330s: owner dict and the block record; take the last.
                         let br = tg.iter().rfind(|x| x.code == 330).map(Tag::str).unwrap_or_default();
                         let plot = T(tg.get(..start).unwrap_or(&[]));
-                        layout_objs.push((t.s(1).unwrap_or_default(), t.i(71).unwrap_or(0) as u32, br.to_ascii_uppercase(), page_setup(&plot)));
+                        // Limits (10/11); kept only when all four values are finite and differ
+                        // from what CADCraft writes for a layout it created.
+                        let pt = |c: i32| Some(Vec2::new(t.f(c)?, t.f(c + 10)?)).filter(|p| p.x.is_finite() && p.y.is_finite());
+                        let limits = pt(10).zip(pt(11)).filter(|l| *l != (Vec2::ZERO, Vec2::new(12.0, 9.0)));
+                        layout_objs.push((
+                            t.s(1).unwrap_or_default(),
+                            t.i(71).unwrap_or(0) as u32,
+                            br.to_ascii_uppercase(),
+                            page_setup(&plot),
+                            limits,
+                        ));
                     }
                 }
             }
@@ -1787,11 +1797,11 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
     // Layouts.
     let mut paper_block_for: HashMap<String, String> = HashMap::new();
     layout_objs.sort_by_key(|l| l.1);
-    for (name, order, br, page) in &layout_objs {
+    for (name, order, br, page, limits) in &layout_objs {
         if name.eq_ignore_ascii_case("Model") {
             continue;
         }
-        d.layouts.push(Layout { page: page.clone(), ..Layout::new(name, *order) });
+        d.layouts.push(Layout { page: page.clone(), limits: *limits, ..Layout::new(name, *order) });
         if let Some(bname) = rx.brs.get(br) {
             paper_block_for.insert(bname.to_ascii_uppercase(), name.clone());
         }
@@ -2366,6 +2376,9 @@ fn page_setup(t: &T) -> PageSetup {
     .into();
     p.plot_window = [t.fd(48, 0.0), t.fd(49, 0.0), t.fd(140, 0.0), t.fd(141, 0.0)].map(|v| if v.is_finite() { v } else { 0.0 });
     p.plot_style_table = t.s(7).unwrap_or_default();
+    // Paper image origin (148/149); kept only when both values are finite and not 0,0 (what
+    // CADCraft writes for a page setup it created).
+    p.paper_image_origin = t.f(148).zip(t.f(149)).map(|(x, y)| Vec2::new(x, y)).filter(|o| o.x.is_finite() && o.y.is_finite() && *o != Vec2::ZERO);
     p
 }
 
diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs
--- a/crates/io/src/dxf_write.rs
+++ b/crates/io/src/dxf_write.rs
@@ -2612,7 +2612,15 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
         w.s(3, &l.name);
         w.s(350, layout_handles.get(i).cloned().unwrap_or_default());
     }
-    let layout_obj = |w: &mut W, h: &str, name: &str, tab: i64, brh: &str, flags: i64, page: &PageSetup, view: Option<(Vec2, f64)>| {
+    let layout_obj = |w: &mut W,
+                      h: &str,
+                      name: &str,
+                      tab: i64,
+                      brh: &str,
+                      flags: i64,
+                      page: &PageSetup,
+                      view: Option<(Vec2, f64)>,
+                      limits: Option<(Vec2, Vec2)>| {
         // Plot scale: a standard scale when fitting or 1:1, otherwise custom (142 paper units
         // per 143 drawing units).
         let scale = if page.scale.is_finite() && page.scale > 0.0 { page.scale } else { 1.0 };
@@ -2665,14 +2673,19 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
         w.s(7, &page.plot_style_table);
         w.i(75, if page.scale_to_fit { 0 } else { 16 });
         w.f(147, scale);
-        w.f(148, 0.0);
-        w.f(149, 0.0);
+        // Paper image origin and limits: the file's own values, else the defaults (a layout
+        // CADCraft created, or a non-finite value).
+        let finite = |p: &Vec2| p.x.is_finite() && p.y.is_finite();
+        let origin = page.paper_image_origin.filter(finite).unwrap_or(Vec2::ZERO);
+        w.f(148, origin.x);
+        w.f(149, origin.y);
         w.s(100, "AcDbLayout");
         w.s(1, name);
         w.i(70, flags);
         w.i(71, tab);
-        w.p2(10, Vec2::ZERO);
-        w.p2(11, Vec2::new(12.0, 9.0));
+        let (lim_min, lim_max) = limits.filter(|(a, b)| finite(a) && finite(b)).unwrap_or((Vec2::ZERO, Vec2::new(12.0, 9.0)));
+        w.p2(10, lim_min);
+        w.p2(11, lim_max);
         w.p(12, Vec3::ZERO);
         w.p(14, Vec3::ZERO);
         w.p(15, Vec3::ZERO);
@@ -2697,10 +2710,10 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
     // plot type 5, "Layout information" (DXF Reference, PLOTSETTINGS group 74), which means
     // nothing for model space: another program's Plot dialog then shows no plot area or scale.
     let model_page = PageSetup { plot_area: "extents".into(), scale_to_fit: true, ..PageSetup::default() };
-    layout_obj(&mut w, &model_layout, "Model", 0, &ms_br, 1, &model_page, None);
+    layout_obj(&mut w, &model_layout, "Model", 0, &ms_br, 1, &model_page, None, None);
     for (i, (_, brh, l)) in ps_brs.iter().enumerate() {
         let h = layout_handles.get(i).cloned().unwrap_or_default();
-        layout_obj(&mut w, &h, &l.name, i64::from(l.tab_order.max(1)), brh, 1, &l.page, l.view);
+        layout_obj(&mut w, &h, &l.name, i64::from(l.tab_order.max(1)), brh, 1, &l.page, l.view, l.limits);
     }
     // Raster image definitions.
     w.t.extend(crate::dxf_image::objects(&cx.images, &root_dict));
diff --git a/crates/io/tests/it/layout_limits_roundtrip.rs b/crates/io/tests/it/layout_limits_roundtrip.rs
new file mode 100644
--- /dev/null
+++ b/crates/io/tests/it/layout_limits_roundtrip.rs
@@ -0,0 +1,125 @@
+//! A layout's limits (AcDbLayout groups 10/11) and paper image origin (AcDbPlotSettings groups
+//! 148/149) survive a DXF round trip (#2109). They place the sheet in paper space; with fixed
+//! values another program drew the sheet away from the entities and shifted a "Layout" plot.
+//! Layouts CADCraft creates keep (0,0)-(12,9) and origin 0,0.
+
+use cadcraft_doc::*;
+use cadcraft_geom::Vec2;
+
+/// DXF text with groups of the "Layout1" LAYOUT record set to new values: (code, value,
+/// whether the group comes after the record's `AcDbLayout` marker).
+fn set_paper_layout(text: &str, values: &[(&str, &str, bool)]) -> String {
+    let lines: Vec<&str> = text.lines().collect();
+    let mut out: Vec<String> = Vec::with_capacity(lines.len());
+    let mut i = 0;
+    while i + 1 < lines.len() {
+        let (code, value) = (lines[i].trim(), lines[i + 1].trim());
+        if code == "0" && value == "LAYOUT" {
+            // Collect the record.
+            let mut j = i + 2;
+            while j + 1 < lines.len() && lines[j].trim() != "0" {
+                j += 2;
+            }
+            let mut rec: Vec<(String, String)> = (i..j).step_by(2).map(|k| (lines[k].to_string(), lines[k + 1].to_string())).collect();
+            let at = rec.iter().position(|(c, v)| c.trim() == "100" && v.trim() == "AcDbLayout").unwrap();
+            let name = rec.iter().skip(at).find(|(c, _)| c.trim() == "1").map(|(_, v)| v.trim().to_string()).unwrap();
+            if name == "Layout1" {
+                for (c, v, after) in values {
+                    let k = rec.iter().enumerate().position(|(k, (rc, _))| rc.trim() == *c && (k > at) == *after).unwrap();
+                    rec[k].1 = v.to_string();
+                }
+            }
+            for (c, v) in rec {
+                out.push(c);
+                out.push(v);
+            }
+            i = j;
+        } else {
+            out.push(lines[i].to_string());
+            out.push(lines[i + 1].to_string());
+            i += 2;
+        }
+    }
+    out.extend(lines.iter().skip(i).map(|l| l.to_string()));
+    out.join("\n") + "\n"
+}
+
+/// Limits (10, 20, 11, 21) and paper image origin (148, 149) of every paper-space LAYOUT.
+fn limits(text: &str) -> Vec<[f64; 6]> {
+    let lines: Vec<&str> = text.lines().map(str::trim).collect();
+    let mut recs: Vec<Vec<(String, String)>> = Vec::new();
+    let mut inside = false;
+    for pair in lines.chunks(2) {
+        let [code, value] = pair else { break };
+        if *code == "0" {
+            inside = *value == "LAYOUT";
+            if inside {
+                recs.push(Vec::new());
+            }
+        } else if inside && let Some(rec) = recs.last_mut() {
+            rec.push((code.to_string(), value.to_string()));
+        }
+    }
+    recs.iter()
+        .filter_map(|rec| {
+            let at = rec.iter().position(|(c, v)| c == "100" && v == "AcDbLayout")?;
+            let name = rec.iter().skip(at).find(|(c, _)| c == "1").map(|(_, v)| v.as_str())?;
+            if name == "Model" {
+                return None;
+            }
+            let after = |code: &str| rec.iter().skip(at).find(|(c, _)| c == code).and_then(|(_, v)| v.parse().ok()).unwrap_or(f64::NAN);
+            let before = |code: &str| rec.iter().take(at).find(|(c, _)| c == code).and_then(|(_, v)| v.parse().ok()).unwrap_or(f64::NAN);
+            Some([after("10"), after("20"), after("11"), after("21"), before("148"), before("149")])
+        })
+        .collect()
+}
+
+/// What CADCraft writes for a layout it created.
+const DEFAULT: [f64; 6] = [0.0, 0.0, 12.0, 9.0, 0.0, 0.0];
+
+#[test]
+fn layout_limits_and_paper_image_origin_survive_a_round_trip() {
+    let src = cadcraft_io::write_dxf(&Drawing::new_metric());
+    let edited = set_paper_layout(
+        &src,
+        &[
+            ("10", "1.038", true),
+            ("20", "0.694", true),
+            ("11", "9.506", true),
+            ("21", "6.681", true),
+            ("148", "-1.054", false),
+            ("149", "-0.71", false),
+        ],
+    );
+    let ours = [1.038, 0.694, 9.506, 6.681, -1.054, -0.71];
+    assert_eq!(limits(&edited), vec![ours, DEFAULT]);
+    let d = cadcraft_io::read_dxf(edited.as_bytes()).unwrap();
+    let out = cadcraft_io::write_dxf(&d);
+    // Layout1 keeps the file's values; Layout2 had the defaults and keeps them.
+    assert_eq!(limits(&out), vec![ours, DEFAULT]);
+}
+
+#[test]
+fn new_layouts_keep_the_default_limits_and_origin() {
+    let out = cadcraft_io::write_dxf(&Drawing::new_metric());
+    assert_eq!(limits(&out), vec![DEFAULT, DEFAULT]);
+}
+
+#[test]
+fn non_finite_limits_and_origin_fall_back_to_the_defaults() {
+    let src = cadcraft_io::write_dxf(&Drawing::new_metric());
+    let edited = set_paper_layout(&src, &[("10", "NaN", true), ("148", "NaN", false)]);
+    let d = cadcraft_io::read_dxf(edited.as_bytes()).unwrap();
+    let out = cadcraft_io::write_dxf(&d);
+    assert_eq!(limits(&out), vec![DEFAULT, DEFAULT]);
+}
+
+#[test]
+fn non_finite_limits_and_origin_in_memory_write_the_defaults() {
+    let mut d = Drawing::new_metric();
+    for l in &mut d.layouts {
+        l.limits = Some((Vec2::new(f64::NAN, 0.0), Vec2::new(12.0, f64::INFINITY)));
+        l.page.paper_image_origin = Some(Vec2::new(f64::NAN, 1.0));
+    }
+    assert_eq!(limits(&cadcraft_io::write_dxf(&d)), vec![DEFAULT, DEFAULT]);
+}
diff --git a/crates/io/tests/it/main.rs b/crates/io/tests/it/main.rs
--- a/crates/io/tests/it/main.rs
+++ b/crates/io/tests/it/main.rs
@@ -29,6 +29,7 @@ mod groups_roundtrip;
 mod hatch_assoc_roundtrip;
 mod hatch_pattern_def;
 mod helix_roundtrip;
+mod layout_limits_roundtrip;
 mod layout_paper_units;
 mod layout_plot_window;
 #[cfg(not(target_arch = "wasm32"))]
