mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-03 00:42:46 -06:00
* fix(dwg): preserve literal spaces after MTEXT carets (#2250) * test(dwg): fix MTEXT fixture ownership and boxed entity --------- Co-authored-by: Zaxaerith <189452125+Zaxaerith@users.noreply.github.com>
This commit is contained in:
+93
-2
@@ -110,8 +110,11 @@ mod native {
|
||||
notes.push(format!("DWG: {orphans} DGN line style component{s} that no line style definition uses {were} left out"));
|
||||
}
|
||||
let mut out = CappedWriter { buf: Vec::new(), limit: limits.max_dxf_bytes, exceeded: false };
|
||||
let written = std::panic::catch_unwind(AssertUnwindSafe(|| acadrust::DxfWriter::new(&doc).write_to_writer(&mut out)))
|
||||
.map_err(|_| "the DWG→DXF conversion failed on this file".to_string())?;
|
||||
let written = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
acadrust::DxfWriter::new(&doc).write_to_writer(&mut out)?;
|
||||
mtext_contents(&doc, &mut out)
|
||||
}))
|
||||
.map_err(|_| "the DWG→DXF conversion failed on this file".to_string())?;
|
||||
if out.exceeded {
|
||||
return Err(format!(
|
||||
"DWG: converting this {} file produced more than {} of drawing data (the limit); \
|
||||
@@ -124,6 +127,94 @@ mod native {
|
||||
Ok((add_header_vars(out.buf, &doc.header), notes))
|
||||
}
|
||||
|
||||
/// acadrust 0.6.6's ASCII writer treats a caret followed by a literal space as already
|
||||
/// escaped, and turns line feeds into paragraph codes. Serialize affected MTEXT contents
|
||||
/// from the native values instead, using its lossless string writer. All other groups,
|
||||
/// including embedded-object data, remain byte-for-byte as the DXF writer emitted them.
|
||||
fn mtext_contents(doc: &acadrust::CadDocument, out: &mut CappedWriter) -> acadrust::Result<()> {
|
||||
use acadrust::io::dxf::{DxfStreamWriter, DxfTextWriter};
|
||||
let contents: std::collections::HashMap<_, _> = doc
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
acadrust::EntityType::MText(t) if t.value.contains(['^', '\t', '\n']) => Some((t.common.handle.value(), t.value.as_str())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if contents.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let dxf = std::mem::take(&mut out.buf);
|
||||
let mut lines = dxf.split_inclusive(|b| *b == b'\n');
|
||||
let (mut mtext, mut text, mut written) = (false, None, false);
|
||||
while let Some(code) = lines.next() {
|
||||
let value = lines.next().ok_or_else(|| std::io::Error::other("incomplete DWG bridge DXF pair"))?;
|
||||
match code.trim_ascii() {
|
||||
b"0" => {
|
||||
mtext = value.trim_ascii() == b"MTEXT";
|
||||
text = None;
|
||||
written = false;
|
||||
}
|
||||
b"5" if mtext => {
|
||||
text = std::str::from_utf8(value.trim_ascii()).ok().and_then(|s| u64::from_str_radix(s, 16).ok()).and_then(|h| contents.get(&h));
|
||||
}
|
||||
b"101" => mtext = false,
|
||||
b"1" | b"3" if mtext && text.is_some() => {
|
||||
if let Some(&text) = text
|
||||
&& !written
|
||||
{
|
||||
let mut writer = DxfTextWriter::new(&mut *out);
|
||||
if doc.version < acadrust::DxfVersion::AC1021
|
||||
&& let Some(encoding) = acadrust::io::dxf::code_page::encoding_from_code_page(&doc.header.code_page)
|
||||
{
|
||||
writer.set_encoding(encoding);
|
||||
}
|
||||
// Each character can expand to two caret-escape characters. Keep
|
||||
// those pairs together and each group within 250 characters.
|
||||
let mut rest = text;
|
||||
while let Some((end, _)) = rest.char_indices().nth(125) {
|
||||
let (chunk, tail) = rest.split_at(end);
|
||||
writer.write_xrecord_string(3, chunk)?;
|
||||
rest = tail;
|
||||
}
|
||||
writer.write_xrecord_string(1, rest)?;
|
||||
written = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
out.write_all(code)?;
|
||||
out.write_all(value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn mtext_contents_keeps_other_groups_and_the_output_limit() -> acadrust::Result<()> {
|
||||
for version in [acadrust::DxfVersion::AC1015, acadrust::DxfVersion::AC1032] {
|
||||
let mut doc = acadrust::CadDocument::new();
|
||||
doc.version = version;
|
||||
doc.header.code_page = "ANSI_1252".into();
|
||||
let mut t = acadrust::MText::with_value("é ^ C", acadrust::Vector3::ZERO);
|
||||
t.common.handle = acadrust::Handle::new(0x10);
|
||||
doc.add_entity(acadrust::EntityType::MText(Box::new(t)))?;
|
||||
// Only the MTEXT's own 1/3 groups change. The embedded object's groups and the
|
||||
// following TEXT record must survive byte-for-byte, including their spaces.
|
||||
let prefix = b" 0\r\nMTEXT\r\n 5\r\n10\r\n 8\r\n0\r\n";
|
||||
let suffix = b"101\r\nEmbedded Object\r\n 1\r\n^ embedded\r\n 3\r\n^ tail\r\n 0\r\nTEXT\r\n 1\r\n^ plain\r\n";
|
||||
let raw = [prefix.as_slice(), b" 3\r\nold\r\n 1\r\ntext\r\n", suffix.as_slice()].concat();
|
||||
let mut out = CappedWriter { buf: raw.clone(), limit: 4096, exceeded: false };
|
||||
mtext_contents(&doc, &mut out)?;
|
||||
let value: &[u8] = if version == acadrust::DxfVersion::AC1015 { b"\xe9 ^ C" } else { "é ^ C".as_bytes() };
|
||||
assert_eq!(out.buf, [prefix.as_slice(), b" 1\r\n", value, b"\r\n", suffix.as_slice()].concat());
|
||||
let mut limited = CappedWriter { buf: raw, limit: 1, exceeded: false };
|
||||
assert!(mtext_contents(&doc, &mut limited).is_err());
|
||||
assert!(limited.exceeded);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Most record types named in the skipped-records note.
|
||||
const NOTE_TYPES: usize = 6;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ mod mleader_roundtrip;
|
||||
mod mline_roundtrip;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod model_layout_plot_type;
|
||||
mod mtext_caret_spaces;
|
||||
mod mtext_columns_roundtrip;
|
||||
mod mtext_embedded_object;
|
||||
mod mtext_line_spacing;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Literal MTEXT carets and their adjacent whitespace survive DXF and DWG saves (#2250).
|
||||
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, Handle, MText, Space};
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{SaveOptions, Version, read, write_with};
|
||||
|
||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
fn drawing() -> Result<(Drawing, Vec<(Handle, String)>), Box<dyn std::error::Error>> {
|
||||
let mut d = Drawing::new_metric();
|
||||
let mut expected = Vec::new();
|
||||
let mut cases: Vec<String> = [
|
||||
"ordinary text",
|
||||
"^ C",
|
||||
"A ^ C B",
|
||||
"^ C",
|
||||
"A ^ C B",
|
||||
"^ ",
|
||||
"^^ C ^ ^ D^",
|
||||
"^I ^J ^ C",
|
||||
"\t^ C\nD\tE",
|
||||
"^\t ^\n C",
|
||||
r"A\P^ C\P^I ^J",
|
||||
r"\S+0.1^-0.1; ^ C",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
// Exercise multiple group-3 chunks and a caret at the output chunk boundary.
|
||||
cases.push(format!("{}^ C", "A".repeat(124)));
|
||||
cases.push(format!("{}^ C B", "A".repeat(260)));
|
||||
for contents in cases {
|
||||
let kind = EntityKind::MText(MText {
|
||||
insert: Vec3::new(1.0, 2.0, 0.0),
|
||||
height: 0.5,
|
||||
width: 12.5,
|
||||
attach: 1,
|
||||
rotation: 0.0,
|
||||
style: "Standard".into(),
|
||||
contents: contents.clone(),
|
||||
line_spacing: 1.0,
|
||||
line_spacing_exact: false,
|
||||
columns: None,
|
||||
});
|
||||
let handle = d.add(&Space::Model, Common::default(), kind)?;
|
||||
expected.push((handle, contents));
|
||||
}
|
||||
Ok((d, expected))
|
||||
}
|
||||
|
||||
fn assert_contents(d: &Drawing, expected: &[(Handle, String)], context: &str) -> TestResult {
|
||||
assert_eq!(d.model.len(), expected.len(), "{context}: entity count");
|
||||
for (handle, contents) in expected {
|
||||
let e = d.entity(*handle).ok_or("MTEXT handle lost")?;
|
||||
let EntityKind::MText(t) = &e.kind else { return Err("MTEXT entity type changed".into()) };
|
||||
assert_eq!(&t.contents, contents, "{context}: {handle:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dxf_mtext_caret_spaces_round_trip() -> TestResult {
|
||||
let (original, expected) = drawing()?;
|
||||
for version in Version::ALL {
|
||||
for binary in [false, true] {
|
||||
let mut d = original.clone();
|
||||
for pass in 1..=2 {
|
||||
let bytes = write_with(&d, "carets.dxf", &SaveOptions { version: Some(version), binary })?;
|
||||
d = read(&bytes, "carets.dxf")?;
|
||||
assert_contents(&d, &expected, &format!("{version:?}, binary={binary}, pass={pass}"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn dwg_mtext_caret_spaces_round_trip() -> TestResult {
|
||||
let (original, expected) = drawing()?;
|
||||
for version in Version::ALL {
|
||||
let mut d = original.clone();
|
||||
for pass in 1..=2 {
|
||||
let bytes = write_with(&d, "carets.dwg", &SaveOptions { version: Some(version), binary: false })?;
|
||||
assert_eq!(cadcraft_dwg::version(&bytes).as_deref(), Some(version.code()));
|
||||
// Inspect the stored DWG text before its DXF rendition: a compensating reader
|
||||
// must not hide corruption on save. This uses the same codec, not an external oracle.
|
||||
let native = acadrust::DwgReader::from_stream(std::io::Cursor::new(&bytes)).read()?;
|
||||
for (handle, contents) in &expected {
|
||||
let text = native.entities().find_map(|e| match e {
|
||||
acadrust::EntityType::MText(t) if t.common.handle.value() == handle.0 => Some(&t.value),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(text, Some(contents), "native DWG {version:?}, pass={pass}, {handle:?}");
|
||||
}
|
||||
let dxf = cadcraft_dwg::dwg_to_dxf(&bytes)?;
|
||||
let converted = read(&dxf, "bridge.dxf")?;
|
||||
assert_contents(&converted, &expected, &format!("DWG bridge DXF {version:?}, pass={pass}"))?;
|
||||
d = read(&bytes, "carets.dwg")?;
|
||||
assert_contents(&d, &expected, &format!("DWG {version:?}, pass={pass}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn bridge_caret_space_is_escaped_once_in_each_direction() -> TestResult {
|
||||
let (d, _) = drawing()?;
|
||||
let dxf = cadcraft_io::write_dxf(&d);
|
||||
assert!(dxf.contains(" 1\r\n^ C\r\n"), "literal caret plus authored space needs two wire spaces");
|
||||
let native = acadrust::DxfReader::from_reader(std::io::Cursor::new(dxf.into_bytes()))?.read()?;
|
||||
assert!(native.entities().any(|e| matches!(e, acadrust::EntityType::MText(t) if t.value == "^ C")));
|
||||
let dwg = write_with(&d, "carets.dwg", &SaveOptions { version: Some(Version::R2018), binary: false })?;
|
||||
let returned = String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg)?)?;
|
||||
assert!(returned.contains(" 1\r\n^ C\r\n"), "the return bridge must encode the separator independently of authored whitespace");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user