diff --git a/crates/dxf/src/codepage.rs b/crates/dxf/src/codepage.rs
--- a/crates/dxf/src/codepage.rs
+++ b/crates/dxf/src/codepage.rs
@@ -174,6 +174,48 @@ impl BinaryText {
     }
 }
 
+/// How text becomes the bytes of a pre-2007 file's code page (the code pages [`decoder`] knows).
+pub(crate) enum Encoder {
+    Encoding(&'static Encoding),
+    Table(&'static [char; 128]),
+}
+
+/// The encoder for a `$DWGCODEPAGE` value, or `None` for UTF-8 and code pages CADCraft doesn't
+/// know.
+pub(crate) fn encoder(code_page: &str) -> Option<Encoder> {
+    match decoder(code_page)? {
+        Decoder::Utf8 => None,
+        Decoder::Encoding(e) => Some(Encoder::Encoding(e)),
+        Decoder::Table(t) => Some(Encoder::Table(t)),
+    }
+}
+
+impl Encoder {
+    /// Append `c` in this code page to `out`; false, with nothing appended, when the code page
+    /// has no such character, or its bytes read back as another one (Shift-JIS writes `¥` as
+    /// 0x5C, which reads back as a backslash).
+    pub(crate) fn push(&self, c: char, out: &mut Vec<u8>) -> bool {
+        match self {
+            Encoder::Table(t) => match t.iter().position(|&x| x == c).and_then(|i| u8::try_from(i).ok()) {
+                Some(i) => {
+                    out.push(0x80 | i);
+                    true
+                }
+                None => false,
+            },
+            Encoder::Encoding(e) => {
+                let mut buf = [0u8; 4];
+                let (bytes, _, unmappable) = e.encode(c.encode_utf8(&mut buf));
+                if unmappable || e.decode_without_bom_handling(&bytes).0.chars().ne(std::iter::once(c)) {
+                    return false;
+                }
+                out.extend_from_slice(&bytes);
+                true
+            }
+        }
+    }
+}
+
 /// True for an `$ACADVER` of R2007 (AC1021) or later, whose text is UTF-8.
 fn is_utf8_version(acadver: &str) -> bool {
     acadver.trim().strip_prefix("AC").and_then(|n| n.parse::<u32>().ok()).is_some_and(|n| n >= 1021)
diff --git a/crates/dxf/src/lib.rs b/crates/dxf/src/lib.rs
--- a/crates/dxf/src/lib.rs
+++ b/crates/dxf/src/lib.rs
@@ -424,6 +424,70 @@ pub fn write_ascii_as(tags: &[Tag], utf8: bool) -> String {
     out
 }
 
+/// Write tags as ASCII DXF bytes, with text as AutoCAD writes it: UTF-8 from 2007 (`utf8`), and
+/// before 2007 in the code page the tags' `$DWGCODEPAGE` names, characters outside it as
+/// `\U+XXXX`. Older readers that don't decode the escapes (the Italian cadastre's DOCFA, CNC and
+/// plotter front ends) then show `è`, not `\U+00E8`. Without a code page CADCraft knows, the text
+/// is pure ASCII with escapes, as [`write_ascii`] writes it.
+pub fn write_ascii_bytes(tags: &[Tag], utf8: bool) -> Vec<u8> {
+    if utf8 {
+        return write_ascii_as(tags, true).into_bytes();
+    }
+    let Some(enc) = header_code_page(tags).and_then(codepage::encoder) else { return write_ascii_as(tags, false).into_bytes() };
+    let mut out = Vec::with_capacity(tags.len() * 16);
+    for t in tags {
+        match &t.value {
+            Value::Str(s) if !s.is_ascii() => {
+                out.extend_from_slice(format!("{:>3}\r\n", t.code).as_bytes());
+                encode_string(s, &enc, &mut out);
+                out.extend_from_slice(b"\r\n");
+            }
+            _ => out.extend_from_slice(write_ascii_as(std::slice::from_ref(t), false).as_bytes()),
+        }
+    }
+    out
+}
+
+/// The `$DWGCODEPAGE` value among the tags (the group 3 after the group 9 that names it).
+fn header_code_page(tags: &[Tag]) -> Option<&str> {
+    let i = tags.iter().position(|t| t.code == 9 && matches!(&t.value, Value::Str(s) if s == "$DWGCODEPAGE"))?;
+    match tags.get(i + 1) {
+        Some(Tag { code: 3, value: Value::Str(s) }) => Some(s.as_str()),
+        _ => None,
+    }
+}
+
+/// A string in a pre-2007 file's code page: as [`escape_unicode`] writes it, but with the
+/// characters the code page has as its bytes. A string whose bytes would also be valid UTF-8 is
+/// escaped instead: the reader takes a file that is valid UTF-8 as UTF-8, so every line with a
+/// code page byte must not be.
+fn encode_string(s: &str, enc: &codepage::Encoder, out: &mut Vec<u8>) {
+    let chars: Vec<char> = s.chars().collect();
+    let mut bytes = Vec::with_capacity(s.len());
+    for (i, &c) in chars.iter().enumerate() {
+        match c {
+            '\r' | '\n' => bytes.push(b' '),
+            '\\' if matches!(chars.get(i + 1), Some('U' | 'M')) && chars.get(i + 2) == Some(&'+') => bytes.extend_from_slice(b"\\U+005C"),
+            c if c.is_ascii() => bytes.extend_from_slice(c.encode_utf8(&mut [0u8; 4]).as_bytes()),
+            c => {
+                if !enc.push(c, &mut bytes) {
+                    let mut units = [0u16; 2];
+                    for u in c.encode_utf16(&mut units) {
+                        bytes.extend_from_slice(format!("\\U+{u:04X}").as_bytes());
+                    }
+                }
+            }
+        }
+    }
+    if !bytes.is_ascii() && std::str::from_utf8(&bytes).is_ok() {
+        let mut escaped = String::new();
+        escape_unicode(s, false, &mut escaped);
+        out.extend_from_slice(escaped.as_bytes());
+    } else {
+        out.extend_from_slice(&bytes);
+    }
+}
+
 /// Write tags as binary DXF (DXF Reference, "Binary DXF Files", R13+ layout): the sentinel,
 /// then per tag a 2-byte little-endian group code and the value in the binary form of the code's
 /// [`Kind`]: a zero-terminated string, an 8-byte real, a 2-, 4- or 8-byte integer, a 1-byte
@@ -568,6 +632,54 @@ pub fn records(tags: &[Tag]) -> Vec<(String, Vec<Tag>)> {
 mod tests {
     use super::*;
 
+    /// Pre-2007 text is written in the header's code page, as AutoCAD writes it, and reads back.
+    #[test]
+    fn pre_2007_text_is_written_in_the_code_page() {
+        let file = |code_page: &str, text: &str| {
+            vec![
+                Tag::s(0, "SECTION"),
+                Tag::s(2, "HEADER"),
+                Tag::s(9, "$ACADVER"),
+                Tag::s(1, "AC1015"),
+                Tag::s(9, "$DWGCODEPAGE"),
+                Tag::s(3, code_page),
+                Tag::s(0, "ENDSEC"),
+                Tag::s(0, "SECTION"),
+                Tag::s(2, "ENTITIES"),
+                Tag::s(0, "TEXT"),
+                Tag::s(1, text),
+                Tag::s(0, "ENDSEC"),
+                Tag::s(0, "EOF"),
+            ]
+        };
+        let cases: [(&str, &str, &[u8]); 5] = [
+            ("ANSI_1252", "Caffè 20° – ±1", b"Caff\xe8 20\xb0 \x96 \xb11"),
+            ("ANSI_1252", "图 è", b"\\U+56FE \xe8"),
+            ("ANSI_1251", "Київ", b"\xca\xe8\xbf\xe2"),
+            ("DOS850", "è", b"\x8a"),
+            // Bytes that would read as UTF-8 ("Ã©" in Windows-1252 is UTF-8 for "é"): escaped.
+            ("ANSI_1252", "Ã©", b"\\U+00C3\\U+00A9"),
+        ];
+        for (code_page, text, expected) in cases {
+            let tags = file(code_page, text);
+            let bytes = write_ascii_bytes(&tags, false);
+            let line = bytes.split(|&b| b == b'\n').map(<[u8]>::trim_ascii).find(|l| l.starts_with(&expected[..1]) && l.len() == expected.len());
+            assert_eq!(line, Some(expected), "{code_page} {text}");
+            assert_eq!(parse(&bytes).unwrap(), tags, "{code_page} {text} reads back");
+        }
+        // Shift-JIS encodes ¥ as 0x5C, which reads back as a backslash: a character whose bytes
+        // don't decode to it again is escaped.
+        let tags = file("ANSI_932", "¥図");
+        assert_eq!(parse(&write_ascii_bytes(&tags, false)).unwrap(), tags, "¥ survives Shift-JIS");
+        // A literal escape stays literal; no code page CADCraft knows: pure ASCII; 2007+: UTF-8.
+        let tags = file("ANSI_1252", "\\U+00E9 è");
+        assert_eq!(parse(&write_ascii_bytes(&tags, false)).unwrap(), tags);
+        assert_eq!(write_ascii_bytes(&file("NOPE", "è"), false), write_ascii(&file("NOPE", "è")).into_bytes());
+        assert_eq!(write_ascii_bytes(&file("ANSI_1252", "è"), true), write_ascii_as(&file("ANSI_1252", "è"), true).into_bytes());
+        // ASCII text is written exactly as before.
+        assert_eq!(write_ascii_bytes(&file("ANSI_1252", "plain"), false), write_ascii(&file("ANSI_1252", "plain")).into_bytes());
+    }
+
     #[test]
     fn literal_escape_text_roundtrips() {
         for utf8 in [false, true] {
diff --git a/crates/dxf/src/version.rs b/crates/dxf/src/version.rs
--- a/crates/dxf/src/version.rs
+++ b/crates/dxf/src/version.rs
@@ -75,7 +75,7 @@ impl Version {
     }
 
     /// Whether text in this version's DXF is UTF-8 (2007 and later). Older versions keep text in
-    /// the `$DWGCODEPAGE` code page, which CADCraft avoids by escaping non-ASCII as `\U+XXXX`.
+    /// the `$DWGCODEPAGE` code page, characters outside it as `\U+XXXX` escapes.
     pub fn utf8(self) -> bool {
         self >= Version::R2007
     }
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
@@ -1234,9 +1234,9 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
     }
 }
 
-/// `$DWGCODEPAGE`: before 2007 the code page read from the file when CADCraft knows it (the text
-/// is ASCII with `\U+XXXX` escapes, right in any code page, but the file keeps its declared one);
-/// otherwise, and from 2007 (UTF-8 text), `ANSI_1252`.
+/// `$DWGCODEPAGE`: before 2007 the code page read from the file when CADCraft knows it (the
+/// text is written in it, characters outside it as `\U+XXXX` escapes); otherwise, and from 2007
+/// (UTF-8 text), `ANSI_1252`.
 fn code_page(d: &Drawing, version: Version) -> String {
     let read = d.header.get("DWGCODEPAGE").and_then(HVal::as_str).map(str::trim).unwrap_or_default();
     let known = read.is_ascii() && read.len() <= 32 && cadcraft_dxf::decode_code_page(b"", read).is_some();
diff --git a/crates/io/src/lib.rs b/crates/io/src/lib.rs
--- a/crates/io/src/lib.rs
+++ b/crates/io/src/lib.rs
@@ -88,11 +88,7 @@ pub fn write_with(d: &Drawing, name: &str, opts: &SaveOptions) -> Result<Vec<u8>
     match ext(name).as_str() {
         "dxf" | "" => {
             let tags = dxf_write::tags(d, version);
-            Ok(if opts.binary {
-                cadcraft_dxf::write_binary(&tags, version.utf8())
-            } else {
-                cadcraft_dxf::write_ascii_as(&tags, version.utf8()).into_bytes()
-            })
+            Ok(if opts.binary { cadcraft_dxf::write_binary(&tags, version.utf8()) } else { cadcraft_dxf::write_ascii_bytes(&tags, version.utf8()) })
         }
         // The DWG bridge reads our R2000 DXF and writes the DWG at the version asked for. Without
         // one, at least 2004: R2000 DWG has no true colours, transparency, table styles or
diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs
--- a/crates/io/src/tests.rs
+++ b/crates/io/src/tests.rs
@@ -1551,6 +1551,25 @@ fn non_ascii_text_is_escaped_for_r2000_and_roundtrips() {
     assert_eq!(write_dxf(&back).matches("\\U+").count(), text.matches("\\U+").count());
 }
 
+/// A DXF saved at 2000 or 2004 has its text in its code page, as AutoCAD writes it: the Latin-1
+/// characters of `ANSI_1252` as single bytes (older readers such as the Italian cadastre's DOCFA
+/// show `\U+00C4` literally), the others as `\U+XXXX`. It reads back the same.
+#[test]
+fn a_2000_save_writes_text_in_its_code_page_and_roundtrips() {
+    let d = intl_sample();
+    for version in [crate::Version::R2000, crate::Version::R2004] {
+        let bytes = crate::write_with(&d, "intl.dxf", &crate::SaveOptions { version: Some(version), binary: false }).unwrap();
+        assert!(std::str::from_utf8(&bytes).is_err(), "{version}: code page bytes, not UTF-8");
+        let has = |needle: &[u8]| bytes.windows(needle.len()).any(|w| w == needle);
+        assert!(has(b"\\U+56FE\\U+53F7 \xc4\xb0\xf8 \xd8 \\U+D83D\\U+DE00"), "{version}: CJK and astral escaped, Latin-1 as bytes");
+        assert!(!has(b"\\U+00C4"));
+        assert_intl(&read_dxf(&bytes).unwrap());
+    }
+    // 2007 and later stay UTF-8.
+    let bytes = crate::write_with(&d, "intl.dxf", &crate::SaveOptions { version: Some(crate::Version::R2018), binary: false }).unwrap();
+    assert!(std::str::from_utf8(&bytes).is_ok_and(|t| t.contains("图号 Ä°ø Ø")));
+}
+
 /// Real R2007+ files are UTF-8 and older ones are in their code page; both still read.
 #[test]
 fn reads_utf8_and_code_page_text() {
diff --git a/crates/io/tests/it/dxf_versions.rs b/crates/io/tests/it/dxf_versions.rs
--- a/crates/io/tests/it/dxf_versions.rs
+++ b/crates/io/tests/it/dxf_versions.rs
@@ -87,9 +87,9 @@ fn dxf_at_every_version_reads_back_the_same() {
             assert_eq!(acadver(&bytes), v.code(), "{what}");
             assert_eq!(cadcraft_dxf::is_binary(&bytes), binary, "{what}");
             let raw = bytes.windows("图号".len()).any(|w| w == "图号".as_bytes());
-            assert_eq!(raw, v.utf8(), "{what}: UTF-8 text from 2007, \\U+ escapes before");
+            assert_eq!(raw, v.utf8(), "{what}: UTF-8 text from 2007, the code page (CJK as \\U+ escapes) before");
             if !v.utf8() && !binary {
-                assert!(bytes.is_ascii(), "{what}");
+                assert!(std::str::from_utf8(&bytes).is_err(), "{what}: Latin-1 text as ANSI_1252 bytes, as AutoCAD writes it");
             }
             let back = read(&bytes, "x.dxf").unwrap();
             assert_eq!(first_text(&back).as_deref(), Some(INTL), "{what}");
@@ -99,11 +99,16 @@ fn dxf_at_every_version_reads_back_the_same() {
     }
 }
 
-/// R2000 ASCII is exactly the R2000 DXF CADCraft always wrote.
+/// R2000 ASCII holds exactly the tags of the R2000 DXF CADCraft always wrote; only its non-ASCII
+/// text is now in the code page instead of escaped.
 #[test]
 fn r2000_ascii_is_unchanged() {
     let d = sample();
-    assert_eq!(write_with(&d, "x.dxf", &opts(Version::R2000, false)).unwrap(), write_dxf(&d).into_bytes());
+    let saved = write_with(&d, "x.dxf", &opts(Version::R2000, false)).unwrap();
+    assert_eq!(cadcraft_dxf::parse(&saved).unwrap(), cadcraft_dxf::parse(write_dxf(&d).as_bytes()).unwrap());
+    let mut ascii = Drawing::new_metric();
+    ascii.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(1.0, 0.0, 0.0) })).unwrap();
+    assert_eq!(write_with(&ascii, "x.dxf", &opts(Version::R2000, false)).unwrap(), write_dxf(&ascii).into_bytes(), "ASCII text: byte for byte");
 }
 
 /// Without a version asked for: 2018 for a new drawing, otherwise the file's own version (the
diff --git a/crates/io/tests/it/followups_626.rs b/crates/io/tests/it/followups_626.rs
--- a/crates/io/tests/it/followups_626.rs
+++ b/crates/io/tests/it/followups_626.rs
@@ -177,10 +177,9 @@ fn code_page_is_kept_before_2007() {
     .unwrap();
     for v in [Version::R2000, Version::R2004] {
         let bytes = save(&d, "x.dxf", v);
-        assert!(bytes.is_ascii(), "{v:?}: ASCII with escapes");
-        let text = String::from_utf8(bytes).unwrap();
-        assert_eq!(code_page(&text), "ANSI_1251", "{v:?}");
-        let back = cadcraft_io::read_dxf(text.as_bytes()).unwrap();
+        assert!(bytes.windows(6).any(|w| w == b"\xcf\xf0\xe8\xe2\xe5\xf2"), "{v:?}: the text in ANSI_1251, as AutoCAD writes it");
+        assert_eq!(code_page(&String::from_utf8_lossy(&bytes)), "ANSI_1251", "{v:?}");
+        let back = cadcraft_io::read_dxf(&bytes).unwrap();
         assert_eq!(back.header.str("DWGCODEPAGE", ""), "ANSI_1251");
         assert!(back.model.iter().any(|e| matches!(&e.kind, EntityKind::Text(t) if t.value == "Привет")));
     }
diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md
--- a/docs/file-format-parity.md
+++ b/docs/file-format-parity.md
@@ -1,6 +1,6 @@
 # File-format parity
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (DIMASSOC objects for angular, diametric and radial dimensions) · **Target:** Autodesk AutoCAD 2027
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (2000/2004 DXF text in the file's code page, as AutoCAD writes it) · **Target:** Autodesk AutoCAD 2027
 
 Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work
 items are in [gaps.md](gaps.md#file-formats); the weighted score feeds
@@ -140,6 +140,7 @@ our output in AutoCAD black-box.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-25 | minor | DXF save at 2000 and 2004: text is written in the file's `$DWGCODEPAGE` (`è`, `°` as `ANSI_1252` bytes), as AutoCAD writes it, with `\U+XXXX` only for characters outside the code page; older readers that don't decode the escapes, such as the Italian cadastre's DOCFA, showed `\U+00E8`. DWG 2000/2004 already stored the code page text |
 | 2026-10-22 | minor | DXF/DWG save: angular, diametric and radial dimensions get standard DIMASSOC objects (one centre reference to the circle for radial/diametric, the definition points for angular), so AutoCAD follows their geometry; the standard objects of those kinds are read back too, without CADCraft's xdata (#1651) |
 | 2026-10-19 | minor | DXF/DWG save: angular dimension block text honors DIMDSEP, including per-dimension overrides (#1646) |
 | 2026-10-19 | minor | A file's own arrowhead blocks with standard names (AutoCAD's `_Dot`, `_DotSmall`), named only by a dimension style, are kept and saved as they were instead of being replaced by CADCraft's solids; PURGE keeps arrowhead blocks dimension styles name |
