mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-15 06:46:45 -04:00
Bump acadrust to 0.6.4 and read DWG in its failsafe mode: an object record the reader can't decode is skipped instead of failing the whole file. The drawing's new read_notes (never saved) say how many records were left out and of which types; OPEN echoes them, returns them as `warnings`, and the app shows the first in the status bar. A file whose objects stream can't be read at all is still an error. acadrust 0.6.4 writes a merged table cell's anchor with 173=0 and the span, so the DXF reader now takes a merge from the span alone. Unknown records the DWG writer can't write back are dropped before writing. Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
11 files changed
+285
-22
No files matched your search
Generated
+34
-6
@@ -4,9 +4,9 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.6.3"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df99ae4f5a6d0147907f9976fe1dd166d01e72ab1a93820b50242937c5797e6"
|
||||
checksum = "94862189cab6e282ed0433a801a216cd1a712953813284a48fa81c67223e38f6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.2",
|
||||
@@ -18,6 +18,7 @@ dependencies = [
|
||||
"itoa",
|
||||
"memmap2",
|
||||
"nom",
|
||||
"oem_cp",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"ryu",
|
||||
@@ -45,7 +46,7 @@ dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer 0.36.0",
|
||||
"atspi-common",
|
||||
"phf",
|
||||
"phf 0.13.1",
|
||||
"serde",
|
||||
"zvariant",
|
||||
]
|
||||
@@ -2708,6 +2709,15 @@ dependencies = [
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oem_cp"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c7959d3349f484aec36462f98226983578bf7d33f9b034c32967489c7000e1f"
|
||||
dependencies = [
|
||||
"phf 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -2801,6 +2811,15 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_shared 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
@@ -2808,7 +2827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -2819,7 +2838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2829,12 +2848,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.13.1"
|
||||
|
||||
@@ -117,6 +117,9 @@ pub struct Drawing {
|
||||
/// Objects hidden for this session by ISOLATEOBJECTS / HIDEOBJECTS. Never written to a file:
|
||||
/// a drawing opens with everything shown again.
|
||||
pub hidden_objects: BTreeSet<Handle>,
|
||||
/// What reading the file left out (a DWG's object records the reader couldn't decode), told
|
||||
/// to the user when the drawing opens. Never written to a file.
|
||||
pub read_notes: Vec<String>,
|
||||
/// The in-place block reference edit in progress (REFEDIT). Never written to a file; files are
|
||||
/// written from [`Drawing::committed`].
|
||||
pub refedit: Option<RefEdit>,
|
||||
@@ -173,6 +176,7 @@ impl Drawing {
|
||||
scales: imperial_scales(),
|
||||
handseed: 0x100,
|
||||
hidden_objects: BTreeSet::new(),
|
||||
read_notes: Vec::new(),
|
||||
refedit: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,4 @@ workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# MPL-2.0: used as an unmodified dependency only (see plan/adr/0001 and NOTICE).
|
||||
acadrust = "0.6.3"
|
||||
acadrust = "0.6.4"
|
||||
+170
-5
@@ -61,12 +61,30 @@ mod native {
|
||||
/// guard: the input size before the DWG reader runs, the DXF size while it is written, so a
|
||||
/// runaway conversion stops at the limit instead of filling memory first.
|
||||
pub fn dwg_to_dxf_with(bytes: &[u8], limits: Limits) -> Result<Vec<u8>, String> {
|
||||
dwg_to_dxf_noted(bytes, limits).map(|(dxf, _)| dxf)
|
||||
}
|
||||
|
||||
/// [`dwg_to_dxf_with`], also returning notes on what the read left out. The reader runs in
|
||||
/// failsafe mode: an object record it can't decode is skipped instead of failing the whole
|
||||
/// file, and the notes say how many were skipped and of which types, so a partial read never
|
||||
/// passes for a complete one.
|
||||
pub fn dwg_to_dxf_noted(bytes: &[u8], limits: Limits) -> Result<(Vec<u8>, Vec<String>), String> {
|
||||
if bytes.len() > limits.max_dwg_bytes {
|
||||
return Err(format!("DWG: the file is {}, larger than the {} CADCraft opens", mb(bytes.len() as u64), mb(limits.max_dwg_bytes as u64)));
|
||||
}
|
||||
let mut doc = std::panic::catch_unwind(|| acadrust::DwgReader::from_stream(Cursor::new(bytes)).read())
|
||||
.map_err(|_| "the DWG reader failed on this file".to_string())?
|
||||
.map_err(|e| format!("DWG: {e}"))?;
|
||||
let outcome = std::panic::catch_unwind(|| {
|
||||
acadrust::DwgReader::from_stream_with_options(Cursor::new(bytes), acadrust::DwgReadOptions::failsafe()).read_with_stats()
|
||||
})
|
||||
.map_err(|_| "the DWG reader failed on this file".to_string())?
|
||||
.map_err(|e| format!("DWG: {e}"))?;
|
||||
// Failsafe skips records, not whole files: when the objects stream itself couldn't be
|
||||
// read (a damaged header or section map, or not a DWG at all), nothing was read.
|
||||
if !outcome.stats.record_stream_read {
|
||||
let why = outcome.stats.diagnostics.first().map(|d| d.message.trim_start_matches("Failsafe: ").to_string());
|
||||
return Err(format!("DWG: {}", why.unwrap_or_else(|| "the file's objects couldn't be read".to_string())));
|
||||
}
|
||||
let notes = read_notes(&outcome.stats);
|
||||
let mut doc = outcome.document;
|
||||
table_merges(&mut doc);
|
||||
table_style_rows(&mut doc, false);
|
||||
let mut out = CappedWriter { buf: Vec::new(), limit: limits.max_dxf_bytes, exceeded: false };
|
||||
@@ -81,7 +99,111 @@ mod native {
|
||||
));
|
||||
}
|
||||
written.map_err(|e| format!("DWG→DXF: {e}"))?;
|
||||
Ok(add_header_vars(out.buf, &doc.header))
|
||||
Ok((add_header_vars(out.buf, &doc.header), notes))
|
||||
}
|
||||
|
||||
/// Most record types named in the skipped-records note.
|
||||
const NOTE_TYPES: usize = 6;
|
||||
|
||||
/// The note on object records a failsafe read skipped: how many, and how many of each type
|
||||
/// (most first), or nothing when the read was complete.
|
||||
pub(crate) fn read_notes(stats: &acadrust::ReadStats) -> Vec<String> {
|
||||
let skipped = stats.skipped_source_records;
|
||||
if skipped == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let label = |t: Option<&str>| match t.map(str::parse::<i16>) {
|
||||
Some(Ok(code)) => type_name(code).map_or_else(|| format!("type {code}"), str::to_string),
|
||||
Some(Err(_)) => t.unwrap_or_default().to_string(),
|
||||
None => "unknown".to_string(),
|
||||
};
|
||||
let mut by_type: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
|
||||
for d in stats.diagnostics.iter().filter(|d| d.record_handle.is_some()) {
|
||||
*by_type.entry(label(d.record_type.as_deref())).or_default() += 1;
|
||||
}
|
||||
let mut types: Vec<(String, usize)> = by_type.into_iter().collect();
|
||||
types.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
|
||||
let mut list: Vec<String> = types.iter().take(NOTE_TYPES).map(|(t, n)| format!("{t} × {n}")).collect();
|
||||
if types.len() > NOTE_TYPES {
|
||||
list.push(format!("{} more types", types.len() - NOTE_TYPES));
|
||||
}
|
||||
let of = if stats.source_records > 0 { format!(" of {}", stats.source_records) } else { String::new() };
|
||||
let records = if skipped == 1 && of.is_empty() { "object record" } else { "object records" };
|
||||
let (were, them) = if skipped == 1 { ("was", "it") } else { ("were", "them") };
|
||||
let which = if list.is_empty() { String::new() } else { format!(" ({})", list.join(", ")) };
|
||||
vec![format!(
|
||||
"DWG: {skipped}{of} {records} couldn't be read and {were} left out{which}; \
|
||||
saving over the original file would lose {them}"
|
||||
)]
|
||||
}
|
||||
|
||||
/// The DXF name of a DWG object type code with a fixed number (classes, 500 and up, are
|
||||
/// numbered per file).
|
||||
fn type_name(code: i16) -> Option<&'static str> {
|
||||
Some(match code {
|
||||
1 => "TEXT",
|
||||
2 => "ATTRIB",
|
||||
3 => "ATTDEF",
|
||||
4 => "BLOCK",
|
||||
5 => "ENDBLK",
|
||||
6 => "SEQEND",
|
||||
7 => "INSERT",
|
||||
8 => "MINSERT",
|
||||
10..=14 => "VERTEX",
|
||||
15 | 16 | 29 | 30 => "POLYLINE",
|
||||
17 => "ARC",
|
||||
18 => "CIRCLE",
|
||||
19 => "LINE",
|
||||
20..=26 => "DIMENSION",
|
||||
27 => "POINT",
|
||||
28 => "3DFACE",
|
||||
31 => "SOLID",
|
||||
32 => "TRACE",
|
||||
33 => "SHAPE",
|
||||
34 => "VIEWPORT",
|
||||
35 => "ELLIPSE",
|
||||
36 => "SPLINE",
|
||||
37 => "REGION",
|
||||
38 => "3DSOLID",
|
||||
39 => "BODY",
|
||||
40 => "RAY",
|
||||
41 => "XLINE",
|
||||
42 => "DICTIONARY",
|
||||
43 => "OLEFRAME",
|
||||
44 => "MTEXT",
|
||||
45 => "LEADER",
|
||||
46 => "TOLERANCE",
|
||||
47 => "MLINE",
|
||||
48 => "BLOCK_CONTROL",
|
||||
49 => "BLOCK_RECORD",
|
||||
50 => "LAYER_CONTROL",
|
||||
51 => "LAYER",
|
||||
52 => "STYLE_CONTROL",
|
||||
53 => "STYLE",
|
||||
56 => "LTYPE_CONTROL",
|
||||
57 => "LTYPE",
|
||||
60 => "VIEW_CONTROL",
|
||||
61 => "VIEW",
|
||||
62 => "UCS_CONTROL",
|
||||
63 => "UCS",
|
||||
64 => "VPORT_CONTROL",
|
||||
65 => "VPORT",
|
||||
66 => "APPID_CONTROL",
|
||||
67 => "APPID",
|
||||
68 => "DIMSTYLE_CONTROL",
|
||||
69 => "DIMSTYLE",
|
||||
70 => "VX_CONTROL",
|
||||
71 => "VX_TABLE_RECORD",
|
||||
72 => "GROUP",
|
||||
73 => "MLINESTYLE",
|
||||
74 => "OLE2FRAME",
|
||||
77 => "LWPOLYLINE",
|
||||
78 => "HATCH",
|
||||
79 => "XRECORD",
|
||||
80 => "ACDBPLACEHOLDER",
|
||||
82 => "LAYOUT",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Merged table cells onto the cells. acadrust 0.6.3 reads a 2010+ (AC1024+) DWG table's
|
||||
@@ -143,6 +265,44 @@ mod native {
|
||||
}
|
||||
}
|
||||
|
||||
/// Leave out what acadrust can't write as this DWG version: other programs' custom entities
|
||||
/// and objects without raw DWG data of that version. acadrust 0.6.4 refuses the whole file
|
||||
/// for one of them (0.6.3 left them out); CADCraft already tells the user which entity types
|
||||
/// a DWG doesn't keep (`not_saved`), so the rest of the drawing is written. Returns how many.
|
||||
fn drop_unwritable(doc: &mut acadrust::CadDocument) -> usize {
|
||||
let version = doc.version;
|
||||
let unwritable: Vec<acadrust::Handle> = doc
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
acadrust::entities::EntityType::Unknown(u)
|
||||
if u.raw_dwg_data.is_none() || u.dwg_source_version.is_some_and(|v| v != version) || version == acadrust::DxfVersion::AC1009 =>
|
||||
{
|
||||
Some(u.common.handle)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for h in &unwritable {
|
||||
doc.remove_entity(*h);
|
||||
}
|
||||
let objects: Vec<acadrust::Handle> = doc
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(h, o)| match o {
|
||||
acadrust::objects::ObjectType::Unknown { raw_dwg_data, raw_dwg_version, .. }
|
||||
if raw_dwg_data.is_none() || raw_dwg_version.is_some_and(|v| v != version) || version == acadrust::DxfVersion::AC1009 =>
|
||||
{
|
||||
Some(*h)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for h in &objects {
|
||||
doc.objects.remove(h);
|
||||
}
|
||||
unwritable.len() + objects.len()
|
||||
}
|
||||
|
||||
/// Header variables the DWG reader fills in but acadrust's DXF writer (0.6.3) leaves out of
|
||||
/// the HEADER section: (name, group code, value).
|
||||
fn unwritten_header_vars(h: &acadrust::document::HeaderVariables) -> [(&'static str, i32, String); 7] {
|
||||
@@ -239,6 +399,7 @@ mod native {
|
||||
None => {}
|
||||
}
|
||||
table_style_rows(&mut doc, true);
|
||||
drop_unwritable(&mut doc);
|
||||
std::panic::catch_unwind(AssertUnwindSafe(|| acadrust::DwgWriter::write_to_vec(&doc)))
|
||||
.map_err(|_| "the DWG writer failed on this drawing".to_string())?
|
||||
.map_err(|e| format!("DWG write: {e}"))
|
||||
@@ -246,7 +407,7 @@ mod native {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use native::{dwg_to_dxf, dwg_to_dxf_with, dxf_to_dwg, dxf_to_dwg_as};
|
||||
pub use native::{dwg_to_dxf, dwg_to_dxf_noted, dwg_to_dxf_with, dxf_to_dwg, dxf_to_dwg_as};
|
||||
|
||||
/// The error for a DWG version CADCraft can't write.
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
@@ -260,6 +421,10 @@ pub fn dwg_to_dxf(_bytes: &[u8]) -> Result<Vec<u8>, String> {
|
||||
Err("DWG files can't be opened in the web build yet; save as DXF".into())
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn dwg_to_dxf_noted(_bytes: &[u8], _limits: Limits) -> Result<(Vec<u8>, Vec<String>), String> {
|
||||
Err("DWG files can't be opened in the web build yet; save as DXF".into())
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn dwg_to_dxf_with(_bytes: &[u8], _limits: Limits) -> Result<Vec<u8>, String> {
|
||||
Err("DWG files can't be opened in the web build yet; save as DXF".into())
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
CommandSpec::new("open", "Open...", run_open)
|
||||
.menu(&["File", "Open..."])
|
||||
.key("Cmd+O")
|
||||
.params("{path} | {data: base64, name}")
|
||||
.params("{path} | {data: base64, name} → {index, entities?, warnings: what reading left out}")
|
||||
.enabled(always)
|
||||
.noundo(),
|
||||
CommandSpec::new("close", "Close", run_close).menu(&["File", "Close"]).key("Cmd+W").params("{index?}").noundo(),
|
||||
@@ -263,7 +263,7 @@ pub fn finish_open(s: &mut Session, f: ReadFile) -> Result<Value> {
|
||||
let ReadFile { drawing, binary_dxf, name, path } = f;
|
||||
let i = add_read(s, drawing, binary_dxf, &name, path);
|
||||
super::blocks::xref::after_open(s, i);
|
||||
Ok(json!({ "index": i, "entities": s.docs.get(i).map(|d| d.doc.entity_count()) }))
|
||||
Ok(opened(s, i, true))
|
||||
}
|
||||
|
||||
fn read_bytes(bytes: &[u8], name: &str, path: Option<String>) -> Result<ReadFile> {
|
||||
@@ -276,11 +276,15 @@ fn read_bytes(bytes: &[u8], name: &str, path: Option<String>) -> Result<ReadFile
|
||||
}
|
||||
|
||||
fn add_read(s: &mut Session, drawing: Drawing, binary_dxf: bool, name: &str, path: Option<String>) -> usize {
|
||||
let notes = drawing.read_notes.clone();
|
||||
let i = s.open_drawing(drawing, &file_name(name), path);
|
||||
if let Some(st) = s.docs.get_mut(i) {
|
||||
// QSAVE keeps a binary DXF binary.
|
||||
st.binary_dxf = binary_dxf;
|
||||
}
|
||||
for note in notes {
|
||||
s.echo(note);
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
@@ -289,6 +293,17 @@ pub(crate) fn open_bytes(s: &mut Session, bytes: &[u8], name: &str, path: Option
|
||||
Ok(add_read(s, drawing, binary_dxf, &name, path))
|
||||
}
|
||||
|
||||
/// OPEN's reply: the drawing's index and what reading it left out.
|
||||
fn opened(s: &Session, i: usize, entities: bool) -> Value {
|
||||
let d = s.docs.get(i).map(|st| &st.doc);
|
||||
let warnings = d.map(|d| d.read_notes.clone()).unwrap_or_default();
|
||||
if entities {
|
||||
json!({ "index": i, "entities": d.map(|d| d.entity_count()), "warnings": warnings })
|
||||
} else {
|
||||
json!({ "index": i, "warnings": warnings })
|
||||
}
|
||||
}
|
||||
|
||||
fn run_open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(_path) = str_param(p, "path") {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -298,7 +313,7 @@ fn run_open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let bytes = std::fs::read(path).map_err(|e| bad("open", format!("{path}: {e}")))?;
|
||||
let i = open_bytes(s, &bytes, path, Some(path.to_string()))?;
|
||||
super::blocks::xref::after_open(s, i);
|
||||
return Ok(json!({ "index": i, "entities": s.docs.get(i).map(|d| d.doc.entity_count()) }));
|
||||
return Ok(opened(s, i, true));
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Err(bad("open", "paths are not available on the web; pass `data`"));
|
||||
@@ -308,7 +323,7 @@ fn run_open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").unwrap_or("Drawing.dxf");
|
||||
let i = open_bytes(s, &bytes, name, None)?;
|
||||
super::blocks::xref::after_open(s, i);
|
||||
Ok(json!({ "index": i }))
|
||||
Ok(opened(s, i, false))
|
||||
}
|
||||
|
||||
fn run_close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -58,7 +58,7 @@ pub struct Prompt {
|
||||
/// Parse `@distance<angle` (relative polar), returning `(distance, angle in radians)`.
|
||||
/// Returns `None` for any other form.
|
||||
pub fn parse_polar_text(text: &str, angles: &crate::units::AngleSettings) -> Option<(f64, f64)> {
|
||||
let body = text.trim().strip_prefix('@')?; // relative form only
|
||||
let body = text.trim().strip_prefix('@')?; // relative form only
|
||||
let (d, a) = body.split_once('<')?;
|
||||
let dist = d.trim().parse::<f64>().ok()?;
|
||||
let angle = angles.amount(a.trim())?;
|
||||
|
||||
@@ -523,8 +523,11 @@ fn acad_table(tags: &[Tag]) -> Table {
|
||||
let mut text: String = cell.iter().filter(|x| x.code == 2 || x.code == 3).map(Tag::str).collect();
|
||||
text.push_str(&c.s(1).unwrap_or_default());
|
||||
let span = |code: i32| c.i(code).and_then(|v| u32::try_from(v).ok());
|
||||
let merged = match (c.i(173).unwrap_or(0) != 0, span(176), span(175)) {
|
||||
(true, Some(rs), Some(cs)) if rs >= 1 && cs >= 1 && (rs, cs) != (1, 1) => Some((rs, cs)),
|
||||
// A merge starts at the cell whose span (rows 176, columns 175) is more than one cell.
|
||||
// Its own merged flag (173) varies by writer: CADCraft sets it, while AutoCAD-style
|
||||
// writers (acadrust 0.6.4) set it only on the cells the merge covers, whose span is 0.
|
||||
let merged = match (span(176), span(175)) {
|
||||
(Some(rs), Some(cs)) if rs >= 1 && cs >= 1 && (rs, cs) != (1, 1) => Some((rs, cs)),
|
||||
_ => None,
|
||||
};
|
||||
// The cell's own text height: 140 outside the value block (301 … 304), where it is a number.
|
||||
|
||||
@@ -1514,7 +1514,13 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
|
||||
let committed = d.committed();
|
||||
let d: &Drawing = &committed;
|
||||
// Structural handles start above every entity handle.
|
||||
let mut w = W { t: Vec::new(), next: d.handseed.max(0x100).saturating_add(0x1000), pstyles: HashMap::new(), layers: HashMap::new(), reactors: hatch_reactors(d) };
|
||||
let mut w = W {
|
||||
t: Vec::new(),
|
||||
next: d.handseed.max(0x100).saturating_add(0x1000),
|
||||
pstyles: HashMap::new(),
|
||||
layers: HashMap::new(),
|
||||
reactors: hatch_reactors(d),
|
||||
};
|
||||
// Pre-allocate handles for block records.
|
||||
let mut paper_layouts: Vec<&Layout> = d.layouts.iter().collect();
|
||||
paper_layouts.sort_by_key(|l| l.tab_order);
|
||||
|
||||
@@ -59,8 +59,10 @@ fn ext(name: &str) -> String {
|
||||
/// (SVG and gzip-compressed SVGZ).
|
||||
pub fn read(bytes: &[u8], name: &str) -> Result<Drawing> {
|
||||
if cadcraft_dwg::is_dwg(bytes) {
|
||||
let dxf = cadcraft_dwg::dwg_to_dxf(bytes).map_err(IoError::Format)?;
|
||||
return dxf_read::read(&dxf).map_err(|e| IoError::Format(format!("{e} (in the DXF converted from this DWG)")));
|
||||
let (dxf, notes) = cadcraft_dwg::dwg_to_dxf_noted(bytes, cadcraft_dwg::Limits::DEFAULT).map_err(IoError::Format)?;
|
||||
let mut d = dxf_read::read(&dxf).map_err(|e| IoError::Format(format!("{e} (in the DXF converted from this DWG)")))?;
|
||||
d.read_notes = notes;
|
||||
return Ok(d);
|
||||
}
|
||||
match ext(name).as_str() {
|
||||
"svg" | "svgz" => svg_read::read(bytes),
|
||||
|
||||
@@ -980,6 +980,41 @@ fn oversized_dwg_and_dxf_are_rejected_early() {
|
||||
assert_eq!(e.to_string(), "drawing too large: 60 DXF group codes, more than the 50 CADCraft reads");
|
||||
}
|
||||
|
||||
/// Issue #800: one unreadable object record doesn't fail the whole DWG. The reader skips it, the
|
||||
/// rest opens, and the drawing's read notes say how many records were left out and of which
|
||||
/// types, so a partial read never passes for a complete one. A complete read has no notes.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn damaged_dwg_opens_partially_and_says_what_it_left_out() {
|
||||
let mut dxf = String::from("0\nSECTION\n2\nENTITIES\n");
|
||||
for i in 0..200 {
|
||||
dxf += &format!("0\nLINE\n8\n0\n10\n{i}\n20\n0\n11\n{i}\n21\n10\n");
|
||||
}
|
||||
dxf += "0\nENDSEC\n0\nEOF\n";
|
||||
let d = read(dxf.as_bytes(), "lines.dxf").unwrap();
|
||||
let dwg = crate::write_with(&d, "lines.dwg", &crate::SaveOptions { version: Some(crate::Version::R2000), binary: false }).unwrap();
|
||||
let whole = read(&dwg, "lines.dwg").unwrap();
|
||||
assert_eq!((whole.entity_count(), whole.read_notes.len()), (200, 0));
|
||||
|
||||
// Damage a few bytes at a time across the objects area: every damaged copy that still reads
|
||||
// as a DWG either reads whole or notes what it left out.
|
||||
let mut partial = 0;
|
||||
for at in (dwg.len() / 4..dwg.len() * 3 / 5).step_by(97) {
|
||||
let mut b = dwg.clone();
|
||||
b[at..at + 6].iter_mut().for_each(|x| *x ^= 0xA5);
|
||||
let Ok(d) = read(&b, "damaged.dwg") else { continue };
|
||||
if d.entity_count() < 200 {
|
||||
assert_eq!(d.read_notes.len(), 1, "{} lines read, no note", d.entity_count());
|
||||
}
|
||||
if let [note] = d.read_notes.as_slice() {
|
||||
assert!(note.starts_with("DWG: ") && note.contains(" couldn't be read and "), "{note}");
|
||||
assert!(note.ends_with("saving over the original file would lose it") || note.ends_with("would lose them"), "{note}");
|
||||
partial += 1;
|
||||
}
|
||||
}
|
||||
assert!(partial > 0, "no damaged copy read partially");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimension_associativity_roundtrips() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
|
||||
@@ -456,6 +456,11 @@ impl CadApp {
|
||||
if let Some(path) = doc.and_then(|d| d.path.clone()) {
|
||||
self.remember_recent(path);
|
||||
}
|
||||
// A partial read (DWG records left out) shows in the status bar too, not only on
|
||||
// the command line.
|
||||
if let Some(w) = v.get("warnings").and_then(Value::as_array).and_then(|w| w.first()).and_then(Value::as_str) {
|
||||
self.set_status(w.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user