mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Squash of PR #426 by @vangel76. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
475a70e573
commit
139d98bf6f
8 files changed
+608
-18
No files matched your search
+104
-8
@@ -64,9 +64,11 @@ mod native {
|
||||
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 doc = std::panic::catch_unwind(|| acadrust::DwgReader::from_stream(Cursor::new(bytes)).read())
|
||||
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}"))?;
|
||||
table_merges(&mut doc);
|
||||
table_style_rows(&mut doc, false);
|
||||
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())?;
|
||||
@@ -82,6 +84,65 @@ mod native {
|
||||
Ok(add_header_vars(out.buf, &doc.header))
|
||||
}
|
||||
|
||||
/// Merged table cells onto the cells. acadrust 0.6.3 reads a 2010+ (AC1024+) DWG table's
|
||||
/// merged cells into the table's `merged_ranges` only, while its DXF writer writes each cell's
|
||||
/// own merge groups (173, 175, 176), so without this the merges would be lost. Tables whose
|
||||
/// cells already carry merges are left alone. Each range's first cell gets the span; the
|
||||
/// cells it covers are marked merged with no span of their own, as CADCraft's DXF writer does.
|
||||
fn table_merges(doc: &mut acadrust::CadDocument) {
|
||||
for e in doc.entities_mut() {
|
||||
let acadrust::entities::EntityType::Table(t) = e else { continue };
|
||||
if t.merged_ranges.is_empty() || t.rows.iter().flat_map(|r| &r.cells).any(|c| c.merge_width > 1 || c.merge_height > 1) {
|
||||
continue;
|
||||
}
|
||||
let (rows, cols) = (t.rows.len(), t.columns.len());
|
||||
// A real table has at most one merge per cell; more ranges than that come from a
|
||||
// damaged file and would only make this rows × cols × ranges.
|
||||
let max_ranges = rows.saturating_mul(cols).min(1 << 16);
|
||||
for r in t.merged_ranges.iter().take(max_ranges).cloned().collect::<Vec<_>>() {
|
||||
// Clamp to the table: the ranges come from the file.
|
||||
let (bottom, right) = (r.bottom_row.min(rows.saturating_sub(1)), r.right_col.min(cols.saturating_sub(1)));
|
||||
if r.top_row > bottom || r.left_col > right || rows == 0 || cols == 0 || (r.top_row, r.left_col) == (bottom, right) {
|
||||
continue;
|
||||
}
|
||||
let span = |n: usize| i32::try_from(n + 1).unwrap_or(i32::MAX);
|
||||
for (ri, row) in t.rows.iter_mut().enumerate().take(bottom + 1).skip(r.top_row) {
|
||||
for (ci, cell) in row.cells.iter_mut().enumerate().take(right + 1).skip(r.left_col) {
|
||||
cell.merged = 1;
|
||||
let first = (ri, ci) == (r.top_row, r.left_col);
|
||||
cell.merge_width = if first { span(right - r.left_col) } else { 0 };
|
||||
cell.merge_height = if first { span(bottom - r.top_row) } else { 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table styles' title and header suppression between the two places acadrust 0.6.3 keeps
|
||||
/// it. 2010+ (AC1024+) DWG stores it only in the style's flags (title 1, header 2), while
|
||||
/// acadrust's DXF reader and writer use the `title_suppressed`/`header_suppressed` fields
|
||||
/// (groups 280, 281). `to_flags` copies the fields into the flags before a DWG is written;
|
||||
/// otherwise the flags are copied into the fields after one is read. Older versions store
|
||||
/// both and are left alone.
|
||||
fn table_style_rows(doc: &mut acadrust::CadDocument, to_flags: bool) {
|
||||
use acadrust::objects::{ObjectType, TableStyleFlags};
|
||||
if doc.version < acadrust::DxfVersion::AC1024 {
|
||||
return;
|
||||
}
|
||||
for o in doc.objects.values_mut() {
|
||||
let ObjectType::TableStyle(s) = o else { continue };
|
||||
for (bit, suppressed) in
|
||||
[(TableStyleFlags::TITLE_SUPPRESSED, &mut s.title_suppressed), (TableStyleFlags::HEADER_SUPPRESSED, &mut s.header_suppressed)]
|
||||
{
|
||||
if to_flags {
|
||||
s.flags.set(bit, *suppressed);
|
||||
} else {
|
||||
*suppressed = s.flags.contains(bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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] {
|
||||
@@ -146,22 +207,53 @@ mod native {
|
||||
/// Convert DXF bytes into a DWG file of the DXF's version, and at least AutoCAD 2004
|
||||
/// (AC1018).
|
||||
pub fn dxf_to_dwg(dxf: &[u8]) -> Result<Vec<u8>, String> {
|
||||
convert(dxf, None)
|
||||
}
|
||||
|
||||
/// Convert DXF bytes into a DWG file of `version`, a code from [`super::VERSIONS`] ("AC1032"),
|
||||
/// whatever version the DXF declares.
|
||||
pub fn dxf_to_dwg_as(dxf: &[u8], version: &str) -> Result<Vec<u8>, String> {
|
||||
let v = match version.trim().to_ascii_uppercase().as_str() {
|
||||
"AC1015" => acadrust::DxfVersion::AC1015,
|
||||
"AC1018" => acadrust::DxfVersion::AC1018,
|
||||
"AC1021" => acadrust::DxfVersion::AC1021,
|
||||
"AC1024" => acadrust::DxfVersion::AC1024,
|
||||
"AC1027" => acadrust::DxfVersion::AC1027,
|
||||
"AC1032" => acadrust::DxfVersion::AC1032,
|
||||
_ => return Err(super::unsupported(version)),
|
||||
};
|
||||
convert(dxf, Some(v))
|
||||
}
|
||||
|
||||
fn convert(dxf: &[u8], version: Option<acadrust::DxfVersion>) -> Result<Vec<u8>, String> {
|
||||
let data = dxf.to_vec();
|
||||
let mut doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read()))
|
||||
.map_err(|_| "the DXF→DWG conversion failed".to_string())?
|
||||
.map_err(|e| format!("DXF: {e}"))?;
|
||||
// Write at least AutoCAD 2004 (AC1018) DWG: R2000 DWG has no true colours, transparency,
|
||||
// table styles or gradients, and readers can't tell its layouts apart reliably. The DXF
|
||||
// stays R2000; only the DWG is newer.
|
||||
if doc.version < MIN_DWG_VERSION {
|
||||
doc.version = MIN_DWG_VERSION;
|
||||
match version {
|
||||
Some(v) => doc.version = v,
|
||||
// Without a version asked for, write at least AutoCAD 2004 (AC1018) DWG: R2000 DWG
|
||||
// has no true colours, transparency, table styles or gradients. The DXF stays R2000;
|
||||
// only the DWG is newer.
|
||||
None if doc.version < MIN_DWG_VERSION => doc.version = MIN_DWG_VERSION,
|
||||
None => {}
|
||||
}
|
||||
acadrust::DwgWriter::write_to_vec(&doc).map_err(|e| format!("DWG write: {e}"))
|
||||
table_style_rows(&mut doc, true);
|
||||
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}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use native::{dwg_to_dxf, dwg_to_dxf_with, dxf_to_dwg};
|
||||
pub use native::{dwg_to_dxf, 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))]
|
||||
fn unsupported(version: &str) -> String {
|
||||
let list: Vec<&str> = VERSIONS.iter().map(|(_, year)| *year).collect();
|
||||
format!("DWG version `{version}` can't be written (CADCraft writes {})", list.join(", "))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn dwg_to_dxf(_bytes: &[u8]) -> Result<Vec<u8>, String> {
|
||||
@@ -175,6 +267,10 @@ pub fn dwg_to_dxf_with(_bytes: &[u8], _limits: Limits) -> Result<Vec<u8>, String
|
||||
pub fn dxf_to_dwg(_dxf: &[u8]) -> Result<Vec<u8>, String> {
|
||||
Err("DWG files can't be written in the web build yet; save as DXF".into())
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn dxf_to_dwg_as(_dxf: &[u8], _version: &str) -> Result<Vec<u8>, String> {
|
||||
Err("DWG files can't be written in the web build yet; save as DXF".into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
mod version;
|
||||
pub use version::Version;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DxfError {
|
||||
#[error("not a DXF file")]
|
||||
@@ -359,6 +362,9 @@ fn parse_binary_with_limit(b: &[u8], max_tags: usize) -> Result<Vec<Tag>> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if tags.is_empty() {
|
||||
return Err(DxfError::NotDxf);
|
||||
}
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
@@ -376,10 +382,18 @@ pub fn format_real(v: f64) -> String {
|
||||
/// The output is pure ASCII: non-ASCII characters in strings are written as `\U+XXXX` escapes,
|
||||
/// the pre-R2007 convention, so it is correct whatever `$DWGCODEPAGE` the header declares.
|
||||
pub fn write_ascii(tags: &[Tag]) -> String {
|
||||
write_ascii_as(tags, false)
|
||||
}
|
||||
|
||||
/// Write tags as ASCII DXF with text in UTF-8 (`utf8`, the R2007+ convention: non-ASCII
|
||||
/// characters as they are) or escaped as `\U+XXXX` (older versions, see [`write_ascii`]). Line
|
||||
/// breaks inside strings become spaces either way.
|
||||
pub fn write_ascii_as(tags: &[Tag], utf8: bool) -> String {
|
||||
let mut out = String::with_capacity(tags.len() * 16);
|
||||
for t in tags {
|
||||
let _ = write!(out, "{:>3}\r\n", t.code);
|
||||
match &t.value {
|
||||
Value::Str(s) if utf8 => out.extend(s.chars().map(|c| if matches!(c, '\r' | '\n') { ' ' } else { c })),
|
||||
Value::Str(s) => escape_unicode(s, &mut out),
|
||||
Value::Real(r) => out.push_str(&format_real(*r)),
|
||||
Value::Int(i) => out.push_str(&i.to_string()),
|
||||
@@ -391,6 +405,86 @@ pub fn write_ascii(tags: &[Tag]) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// boolean, or a binary chunk (one length byte, then up to 255 bytes; longer data is split into
|
||||
/// several groups of the same code). Strings are UTF-8 when `utf8` (R2007+), otherwise escaped as
|
||||
/// in [`write_ascii`]. A tag's value is converted to its code's kind, so a number written as text
|
||||
/// is stored as a number; a code outside the 16-bit range is skipped.
|
||||
pub fn write_binary(tags: &[Tag], utf8: bool) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(BINARY_SENTINEL.len() + tags.len() * 12);
|
||||
out.extend_from_slice(BINARY_SENTINEL);
|
||||
for t in tags {
|
||||
let Ok(code) = i16::try_from(t.code) else { continue };
|
||||
let code_bytes = code.to_le_bytes();
|
||||
match kind_of(t.code) {
|
||||
Kind::Str => {
|
||||
out.extend_from_slice(&code_bytes);
|
||||
let s = match &t.value {
|
||||
Value::Str(s) => s.clone(),
|
||||
v => v.as_str(),
|
||||
};
|
||||
let mut text = String::with_capacity(s.len());
|
||||
if utf8 {
|
||||
text.extend(s.chars().map(|c| if matches!(c, '\r' | '\n') { ' ' } else { c }));
|
||||
} else {
|
||||
escape_unicode(&s, &mut text);
|
||||
}
|
||||
out.extend(text.bytes().filter(|&b| b != 0));
|
||||
out.push(0);
|
||||
}
|
||||
Kind::Real => {
|
||||
out.extend_from_slice(&code_bytes);
|
||||
out.extend_from_slice(&t.value.as_f64().to_le_bytes());
|
||||
}
|
||||
Kind::Int16 => {
|
||||
out.extend_from_slice(&code_bytes);
|
||||
let v = t.value.as_i64().clamp(i64::from(i16::MIN), i64::from(i16::MAX));
|
||||
out.extend_from_slice(&i16::try_from(v).unwrap_or_default().to_le_bytes());
|
||||
}
|
||||
Kind::Int32 => {
|
||||
out.extend_from_slice(&code_bytes);
|
||||
let v = t.value.as_i64().clamp(i64::from(i32::MIN), i64::from(i32::MAX));
|
||||
out.extend_from_slice(&i32::try_from(v).unwrap_or_default().to_le_bytes());
|
||||
}
|
||||
Kind::Int64 => {
|
||||
out.extend_from_slice(&code_bytes);
|
||||
out.extend_from_slice(&t.value.as_i64().to_le_bytes());
|
||||
}
|
||||
Kind::Bool => {
|
||||
out.extend_from_slice(&code_bytes);
|
||||
out.push(u8::from(t.value.as_i64() != 0));
|
||||
}
|
||||
Kind::Hex => {
|
||||
let bytes = hex_bytes(&t.value.as_str());
|
||||
// An empty chunk is still one group, as in ASCII.
|
||||
let mut chunks: Vec<&[u8]> = bytes.chunks(255).collect();
|
||||
if chunks.is_empty() {
|
||||
chunks.push(&[]);
|
||||
}
|
||||
for chunk in chunks {
|
||||
out.extend_from_slice(&code_bytes);
|
||||
out.push(u8::try_from(chunk.len()).unwrap_or(u8::MAX));
|
||||
out.extend_from_slice(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The bytes of hex text, two digits each; anything that isn't a pair of hex digits is skipped.
|
||||
fn hex_bytes(s: &str) -> Vec<u8> {
|
||||
let digits: Vec<u8> = s.bytes().filter_map(|b| char::from(b).to_digit(16).and_then(|d| u8::try_from(d).ok())).collect();
|
||||
digits.as_chunks::<2>().0.iter().map(|[hi, lo]| (hi << 4) | lo).collect()
|
||||
}
|
||||
|
||||
/// Whether `bytes` are a binary DXF file (they start with the binary sentinel).
|
||||
pub fn is_binary(bytes: &[u8]) -> bool {
|
||||
bytes.starts_with(BINARY_SENTINEL)
|
||||
}
|
||||
|
||||
/// Escape a string for a pre-R2007 DXF, whose text is in the `$DWGCODEPAGE` code page: ASCII
|
||||
/// stays as it is and every other character becomes `\U+XXXX` (a surrogate pair of escapes
|
||||
/// above U+FFFF), so the bytes are valid in any code page. Line breaks become spaces.
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
//! The DXF/DWG file versions CADCraft writes (DXF Reference, HEADER `$ACADVER`).
|
||||
|
||||
/// A file version CADCraft can save: the AutoCAD release that introduced the format. Each format
|
||||
/// also serves the releases after it (2018 is still AutoCAD's current format).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Version {
|
||||
/// AutoCAD 2000/2002 (AC1015).
|
||||
R2000,
|
||||
/// AutoCAD 2004–2006 (AC1018).
|
||||
R2004,
|
||||
/// AutoCAD 2007–2009 (AC1021): the first version whose DXF text is UTF-8.
|
||||
R2007,
|
||||
/// AutoCAD 2010–2012 (AC1024).
|
||||
R2010,
|
||||
/// AutoCAD 2013–2017 (AC1027).
|
||||
R2013,
|
||||
/// AutoCAD 2018 and later (AC1032), the default.
|
||||
#[default]
|
||||
R2018,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
/// Every version, oldest first.
|
||||
pub const ALL: [Version; 6] = [Version::R2000, Version::R2004, Version::R2007, Version::R2010, Version::R2013, Version::R2018];
|
||||
|
||||
/// The `$ACADVER` code ("AC1032").
|
||||
pub fn code(self) -> &'static str {
|
||||
match self {
|
||||
Version::R2000 => "AC1015",
|
||||
Version::R2004 => "AC1018",
|
||||
Version::R2007 => "AC1021",
|
||||
Version::R2010 => "AC1024",
|
||||
Version::R2013 => "AC1027",
|
||||
Version::R2018 => "AC1032",
|
||||
}
|
||||
}
|
||||
|
||||
/// The release year that names the version ("2018").
|
||||
pub fn year(self) -> &'static str {
|
||||
match self {
|
||||
Version::R2000 => "2000",
|
||||
Version::R2004 => "2004",
|
||||
Version::R2007 => "2007",
|
||||
Version::R2010 => "2010",
|
||||
Version::R2013 => "2013",
|
||||
Version::R2018 => "2018",
|
||||
}
|
||||
}
|
||||
|
||||
/// A version named by its year ("2018", "R2018") or its `$ACADVER` code ("AC1032"), any case.
|
||||
pub fn parse(s: &str) -> Option<Version> {
|
||||
let s = s.trim();
|
||||
let year = s.strip_prefix(['R', 'r']).unwrap_or(s);
|
||||
Version::ALL.into_iter().find(|v| v.year() == year || v.code().eq_ignore_ascii_case(s))
|
||||
}
|
||||
|
||||
/// The version to save a file of `$ACADVER` `code` in: the same version when CADCraft writes
|
||||
/// it, else the nearest one it does (R2000 for older files, 2018 for newer ones); `None` when
|
||||
/// the code isn't a version code at all.
|
||||
pub fn for_acadver(code: &str) -> Option<Version> {
|
||||
let code = code.trim();
|
||||
if let Some(v) = Version::ALL.into_iter().find(|v| v.code().eq_ignore_ascii_case(code)) {
|
||||
return Some(v);
|
||||
}
|
||||
code.get(..2).filter(|p| p.eq_ignore_ascii_case("AC"))?;
|
||||
let n = code.get(2..).filter(|d| d.bytes().all(|b| b.is_ascii_digit())).and_then(|d| d.parse::<u32>().ok())?;
|
||||
Some(match n {
|
||||
..1018 => Version::R2000,
|
||||
1018..1021 => Version::R2004,
|
||||
1021..1024 => Version::R2007,
|
||||
1024..1027 => Version::R2010,
|
||||
1027..1032 => Version::R2013,
|
||||
_ => Version::R2018,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
pub fn utf8(self) -> bool {
|
||||
self >= Version::R2007
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.year())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_years_and_codes() {
|
||||
assert_eq!(Version::parse("2018"), Some(Version::R2018));
|
||||
assert_eq!(Version::parse(" r2004 "), Some(Version::R2004));
|
||||
assert_eq!(Version::parse("ac1021"), Some(Version::R2007));
|
||||
assert_eq!(Version::parse("2005"), None);
|
||||
assert_eq!(Version::parse(""), None);
|
||||
for v in Version::ALL {
|
||||
assert_eq!(Version::parse(v.code()), Some(v));
|
||||
assert_eq!(Version::parse(v.year()), Some(v));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_versions_map_to_the_nearest_writable_one() {
|
||||
assert_eq!(Version::for_acadver("AC1009"), Some(Version::R2000));
|
||||
assert_eq!(Version::for_acadver("AC1014"), Some(Version::R2000));
|
||||
assert_eq!(Version::for_acadver("AC1024"), Some(Version::R2010));
|
||||
assert_eq!(Version::for_acadver("AC1035"), Some(Version::R2018));
|
||||
assert_eq!(Version::for_acadver("MC0.0"), None);
|
||||
assert_eq!(Version::for_acadver("AC"), None);
|
||||
assert_eq!(Version::for_acadver("AC99999999999999999999"), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//! Binary DXF writing (#421): every value kind round-trips through `write_binary` and `parse`,
|
||||
//! and hostile binary input never panics.
|
||||
|
||||
use cadcraft_dxf::{Tag, Value, is_binary, parse, write_ascii_as, write_binary};
|
||||
|
||||
fn sample() -> Vec<Tag> {
|
||||
vec![
|
||||
Tag::s(0, "SECTION"),
|
||||
Tag::s(2, "ENTITIES"),
|
||||
Tag::s(0, "TEXT"),
|
||||
Tag::s(5, "1F"),
|
||||
Tag::f(10, -3.25),
|
||||
Tag::i(62, 5),
|
||||
Tag::i(90, 70_000),
|
||||
Tag::i(160, 1 << 40),
|
||||
Tag::new(290, Value::Bool(true)),
|
||||
Tag::new(310, Value::Hex("00FFA5".into())),
|
||||
Tag::s(1, "图号 Café 😀"),
|
||||
Tag::i(1071, -2),
|
||||
Tag::s(0, "ENDSEC"),
|
||||
Tag::s(0, "EOF"),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_roundtrips_every_value_kind() {
|
||||
for utf8 in [false, true] {
|
||||
let bytes = write_binary(&sample(), utf8);
|
||||
assert!(is_binary(&bytes));
|
||||
assert_eq!(parse(&bytes).unwrap(), sample(), "utf8 = {utf8}");
|
||||
}
|
||||
// Pre-2007 strings are escaped to ASCII, 2007+ ones are UTF-8.
|
||||
let old = write_binary(&sample(), false);
|
||||
assert!(old.windows(7).any(|w| w == b"\\U+56FE"));
|
||||
let new = write_binary(&sample(), true);
|
||||
assert!(new.windows("图号".len()).any(|w| w == "图号".as_bytes()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_converts_values_to_their_codes_kind() {
|
||||
// A number held as text at a numeric code, an out-of-range 16-bit value, a long chunk.
|
||||
let long = "AB".repeat(300);
|
||||
let tags = vec![Tag::s(70, "12"), Tag::i(62, 1 << 20), Tag::new(310, Value::Hex(long)), Tag::s(0, "EOF")];
|
||||
let back = parse(&write_binary(&tags, true)).unwrap();
|
||||
assert_eq!(back[0], Tag::i(70, 12));
|
||||
assert_eq!(back[1], Tag::i(62, i64::from(i16::MAX)));
|
||||
assert_eq!(back[2].code, 310);
|
||||
assert_eq!(back[3].code, 310, "300 bytes are split into chunks of at most 255");
|
||||
assert_eq!(back[2].str().len() + back[3].str().len(), 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_ascii_keeps_text_and_drops_line_breaks() {
|
||||
let text = write_ascii_as(&[Tag::s(1, "图号\r\nÄ"), Tag::s(0, "EOF")], true);
|
||||
assert!(text.contains("图号 Ä\r\n"));
|
||||
assert_eq!(parse(text.as_bytes()).unwrap()[0].str(), "图号 Ä");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_binary_input_never_panics() {
|
||||
let good = write_binary(&sample(), true);
|
||||
// Every truncation, and every single-byte corruption of the body.
|
||||
for n in 0..good.len() {
|
||||
let _ = parse(good.get(..n).unwrap());
|
||||
}
|
||||
for i in 22..good.len() {
|
||||
for b in [0u8, 0x7f, 0xff] {
|
||||
let mut bad = good.clone();
|
||||
bad[i] = b;
|
||||
let _ = parse(&bad);
|
||||
}
|
||||
}
|
||||
// A sentinel and nothing else is not a drawing.
|
||||
assert!(parse(b"AutoCAD Binary DXF\r\n\x1a\0").is_err());
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
//! Drawing → DXF (AC1015 / R2000 structure, readable by every DXF consumer since 2000).
|
||||
//! Drawing → DXF (AC1015 / R2000 structure, readable by every DXF consumer since 2000). Later
|
||||
//! versions keep that structure (the groups they add are optional, DXF Reference) and differ in
|
||||
//! `$ACADVER` and, from 2007, in UTF-8 text; the caller picks the text encoding.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::*;
|
||||
use cadcraft_dxf::Tag;
|
||||
use cadcraft_dxf::{Tag, Version};
|
||||
use cadcraft_geom::{Bounds2, Vec2, Vec3};
|
||||
|
||||
use crate::dxf_ext::{self, DimVal, K};
|
||||
@@ -1226,7 +1228,13 @@ fn record_head(w: &mut W, kind: &str, owner: &str, subclass: &str) -> String {
|
||||
}
|
||||
|
||||
/// Write a drawing as ASCII DXF.
|
||||
/// The drawing as R2000 ASCII DXF.
|
||||
pub fn write(d: &Drawing) -> String {
|
||||
cadcraft_dxf::write_ascii(&tags(d, Version::R2000))
|
||||
}
|
||||
|
||||
/// The drawing's DXF tags, declaring `version` in `$ACADVER`.
|
||||
pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
|
||||
// Structural handles start above every entity handle.
|
||||
let mut w = W { t: Vec::new(), next: d.handseed.max(0x100) + 0x1000, pstyles: HashMap::new() };
|
||||
// Pre-allocate handles for block records.
|
||||
@@ -1377,7 +1385,7 @@ pub fn write(d: &Drawing) -> String {
|
||||
w.s(0, "SECTION");
|
||||
w.s(2, "HEADER");
|
||||
w.s(9, "$ACADVER");
|
||||
w.s(1, "AC1015");
|
||||
w.s(1, version.code());
|
||||
w.s(9, "$DWGCODEPAGE");
|
||||
w.s(3, "ANSI_1252");
|
||||
let ext = d.extents(&Space::Model);
|
||||
@@ -2025,7 +2033,7 @@ pub fn write(d: &Drawing) -> String {
|
||||
*t = Tag::s(5, seed);
|
||||
}
|
||||
mark_xrefs(&mut w.t, d);
|
||||
cadcraft_dxf::write_ascii(&w.t)
|
||||
w.t
|
||||
}
|
||||
|
||||
/// Xref blocks (DXF Reference, BLOCK): bit 4 of group 70 and the external drawing's path in
|
||||
|
||||
+45
-3
@@ -12,8 +12,24 @@ pub mod svg;
|
||||
use cadcraft_doc::{Drawing, Space};
|
||||
use cadcraft_geom::Bounds2;
|
||||
|
||||
pub use cadcraft_dxf::Version;
|
||||
pub use pdf::{PdfOptions, pdf, plot};
|
||||
|
||||
/// How a drawing is saved as DXF or DWG.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct SaveOptions {
|
||||
/// The file version; `None` saves in the drawing's own version ([`save_version`]).
|
||||
pub version: Option<Version>,
|
||||
/// Binary instead of ASCII DXF (DXF only; DWG is always binary).
|
||||
pub binary: bool,
|
||||
}
|
||||
|
||||
/// The version a drawing is saved in when none is asked for: the version of the file it was
|
||||
/// read from (`$ACADVER`, the nearest one CADCraft writes), and 2018 for a new drawing.
|
||||
pub fn save_version(d: &Drawing) -> Version {
|
||||
d.header.get("ACADVER").and_then(cadcraft_doc::HVal::as_str).and_then(Version::for_acadver).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IoError {
|
||||
#[error("{0}")]
|
||||
@@ -42,11 +58,37 @@ pub fn read(bytes: &[u8], name: &str) -> Result<Drawing> {
|
||||
}
|
||||
|
||||
/// Write a drawing in the format chosen by the name's extension. Images (PNG, SVG, PDF) show
|
||||
/// model space fitted to its extents.
|
||||
/// model space fitted to its extents. DXF and DWG are written in the drawing's version
|
||||
/// ([`save_version`]), DXF as ASCII.
|
||||
pub fn write(d: &Drawing, name: &str) -> Result<Vec<u8>> {
|
||||
write_framed(d, name, None)
|
||||
}
|
||||
|
||||
/// Like [`write`], with the DXF/DWG version and binary DXF chosen by `opts`. Binary applies to
|
||||
/// DXF only; asking for it with another format is an error.
|
||||
pub fn write_with(d: &Drawing, name: &str, opts: &SaveOptions) -> Result<Vec<u8>> {
|
||||
let version = opts.version.unwrap_or_else(|| save_version(d));
|
||||
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()
|
||||
})
|
||||
}
|
||||
// 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
|
||||
// gradients, so a drawing from an R2000 DXF doesn't lose them by default.
|
||||
"dwg" if !opts.binary => {
|
||||
let version = opts.version.unwrap_or_else(|| version.max(Version::R2004));
|
||||
cadcraft_dwg::dxf_to_dwg_as(dxf_write::write(d).as_bytes(), version.code()).map_err(IoError::Format)
|
||||
}
|
||||
e if opts.binary => Err(IoError::Format(format!("binary output is for DXF files, not `{e}`"))),
|
||||
_ => write_framed(d, name, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`write`], but image formats (PNG, SVG, PDF) show `window` (a model-space rectangle in
|
||||
/// drawing units, centred and scaled to fit the image without distortion) instead of the
|
||||
/// extents. Formats that are not images (DXF, DWG) ignore the window. The window is checked with
|
||||
@@ -58,8 +100,7 @@ pub fn write_framed(d: &Drawing, name: &str, window: Option<Bounds2>) -> Result<
|
||||
check_window(w)?;
|
||||
}
|
||||
match ext(name).as_str() {
|
||||
"dxf" | "" => Ok(dxf_write::write(d).into_bytes()),
|
||||
"dwg" => cadcraft_dwg::dxf_to_dwg(dxf_write::write(d).as_bytes()).map_err(IoError::Format),
|
||||
"dxf" | "" | "dwg" => write_with(d, name, &SaveOptions::default()),
|
||||
"svg" => Ok(svg::export_window(d, &Space::Model, window).into_bytes()),
|
||||
"png" => png_framed(d, &Space::Model, 2400, 1600, window),
|
||||
"pdf" => pdf::pdf(d, &Space::Model, &PdfOptions { compress: true, window, ..PdfOptions::default() }),
|
||||
@@ -120,6 +161,7 @@ pub fn png_view(extents: &Bounds2, width: u32, height: u32, window: Option<Bound
|
||||
pub fn read_dxf(bytes: &[u8]) -> Result<Drawing> {
|
||||
dxf_read::read(bytes)
|
||||
}
|
||||
/// The drawing as R2000 ASCII DXF, whatever its own version ([`write_with`] chooses one).
|
||||
pub fn write_dxf(d: &Drawing) -> String {
|
||||
dxf_write::write(d)
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ fn viewport_ids(l: &Layout) -> Vec<u32> {
|
||||
l.entities.iter().filter_map(|e| if let EntityKind::Viewport(v) = &e.kind { Some(v.id) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// True colours (entity and layer) and entity transparency: the DWG is written as AutoCAD 2004
|
||||
/// (R2000 DWG has neither), while the DXF stays R2000.
|
||||
/// True colours (entity and layer) and entity transparency: a new drawing's DWG is written as
|
||||
/// AutoCAD 2018 (R2000 DWG has neither), while the DXF the bridge reads stays R2000.
|
||||
#[test]
|
||||
fn dwg_keeps_true_colours_and_transparency() {
|
||||
let mut d = Drawing::new_metric();
|
||||
@@ -69,7 +69,7 @@ fn dwg_keeps_true_colours_and_transparency() {
|
||||
);
|
||||
|
||||
let dwg = cadcraft_io::write(&d, "x.dwg").unwrap();
|
||||
assert_eq!(cadcraft_dwg::version(&dwg).as_deref(), Some("AC1018"));
|
||||
assert_eq!(cadcraft_dwg::version(&dwg).as_deref(), Some("AC1032"));
|
||||
let header = records(&cadcraft_io::write_dxf(&d), "SECTION");
|
||||
assert_eq!(header.first().and_then(|h| group(h, "1")), Some("AC1015"), "the DXF stays R2000");
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//! Saving a drawing as DXF or DWG at a chosen version (2000–2018) and as binary DXF (#421).
|
||||
|
||||
use cadcraft_color::{Color, Rgb};
|
||||
use cadcraft_doc::*;
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{SaveOptions, Version, read, save_version, write, write_dxf, write_with};
|
||||
|
||||
const INTL: &str = "图号 Café Ø 😀";
|
||||
|
||||
fn text(value: &str) -> EntityKind {
|
||||
EntityKind::Text(Text {
|
||||
insert: Vec3::new(1.0, 1.0, 0.0),
|
||||
align_pt: None,
|
||||
height: 2.5,
|
||||
value: value.into(),
|
||||
rotation: 0.0,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: "Standard".into(),
|
||||
halign: HAlign::Left,
|
||||
valign: VAlign::Baseline,
|
||||
})
|
||||
}
|
||||
|
||||
fn table() -> Table {
|
||||
let cell = |t: &str| TableCell { text: t.into(), merged: None };
|
||||
let mut cells = vec![vec![cell("Title"), cell(""), cell("")], vec![cell("A"), cell("B"), cell("C")], vec![cell("1"), cell("2"), cell("3")]];
|
||||
cells[0][0].merged = Some((1, 3));
|
||||
Table {
|
||||
insert: Vec3::new(5.0, 20.0, 0.0),
|
||||
col_widths: vec![1.5, 2.5, 4.0],
|
||||
row_heights: vec![0.5, 0.4, 0.4],
|
||||
cells,
|
||||
style: "Standard".into(),
|
||||
text_height: 0.2,
|
||||
title: true,
|
||||
header: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample() -> Drawing {
|
||||
let mut d = Drawing::new_metric();
|
||||
d.layers.push(Layer { name: "Fancy".into(), color: Color::True(Rgb(12, 200, 99)), ..Layer::default() });
|
||||
let fancy = Common { layer: "Fancy".into(), color: Color::True(Rgb(1, 2, 3)), transparency: Transparency::Percent(40), ..Common::default() };
|
||||
d.add(&Space::Model, fancy, EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(10.0, 5.0, 0.0) })).unwrap();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Circle(Circle { center: Vec3::new(3.0, 4.0, 0.0), radius: 2.0 })).unwrap();
|
||||
d.add(&Space::Model, Common::default(), text(INTL)).unwrap();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Table(table())).unwrap();
|
||||
d.table_styles.push(sched());
|
||||
d
|
||||
}
|
||||
|
||||
fn sched() -> TableStyle {
|
||||
TableStyle { name: "Sched".into(), text_height: 2.5, margin: 1.0, title: false, header: false }
|
||||
}
|
||||
|
||||
fn opts(version: Version, binary: bool) -> SaveOptions {
|
||||
SaveOptions { version: Some(version), binary }
|
||||
}
|
||||
|
||||
fn acadver(bytes: &[u8]) -> String {
|
||||
let tags = cadcraft_dxf::parse(bytes).unwrap();
|
||||
tags.windows(2).find(|w| w[0].code == 9 && w[0].str() == "$ACADVER").map(|w| w[1].str()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn first_text(d: &Drawing) -> Option<String> {
|
||||
d.model.iter().find_map(|e| if let EntityKind::Text(t) = &e.kind { Some(t.value.clone()) } else { None })
|
||||
}
|
||||
|
||||
fn first_table(d: &Drawing) -> Option<Table> {
|
||||
d.model.iter().find_map(|e| if let EntityKind::Table(t) = &e.kind { Some(t.clone()) } else { None })
|
||||
}
|
||||
|
||||
/// ASCII and binary DXF at every version declare it, encode text the version's way and read
|
||||
/// back to the same drawing as the R2000 file.
|
||||
#[test]
|
||||
fn dxf_at_every_version_reads_back_the_same() {
|
||||
let d = sample();
|
||||
let reference = write_dxf(&read(write_dxf(&d).as_bytes(), "x.dxf").unwrap());
|
||||
for v in Version::ALL {
|
||||
for binary in [false, true] {
|
||||
let bytes = write_with(&d, "x.dxf", &opts(v, binary)).unwrap();
|
||||
let what = format!("{v} binary={binary}");
|
||||
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");
|
||||
if !v.utf8() && !binary {
|
||||
assert!(bytes.is_ascii(), "{what}");
|
||||
}
|
||||
let back = read(&bytes, "x.dxf").unwrap();
|
||||
assert_eq!(first_text(&back).as_deref(), Some(INTL), "{what}");
|
||||
assert_eq!(save_version(&back), v, "{what}: the file's version is remembered");
|
||||
assert_eq!(write_dxf(&back), reference, "{what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// R2000 ASCII is exactly the R2000 DXF CADCraft always wrote.
|
||||
#[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());
|
||||
}
|
||||
|
||||
/// Without a version asked for: 2018 for a new drawing, otherwise the file's own version (the
|
||||
/// nearest one CADCraft writes for older or newer files).
|
||||
#[test]
|
||||
fn default_version_is_the_drawings_own() {
|
||||
let d = sample();
|
||||
assert_eq!(acadver(&write(&d, "x.dxf").unwrap()), "AC1032");
|
||||
for (file, saved) in [("AC1015", "AC1015"), ("AC1024", "AC1024"), ("AC1009", "AC1015"), ("AC1040", "AC1032")] {
|
||||
let mut d = sample();
|
||||
d.header.set_str("ACADVER", file);
|
||||
assert_eq!(acadver(&write(&d, "x.dxf").unwrap()), saved, "{file}");
|
||||
}
|
||||
let from_r2000 = read(write_dxf(&d).as_bytes(), "x.dxf").unwrap();
|
||||
assert_eq!(save_version(&from_r2000), Version::R2000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_is_for_dxf_only() {
|
||||
assert!(write_with(&sample(), "x.dwg", &opts(Version::R2018, true)).is_err());
|
||||
assert!(write_with(&sample(), "x.svg", &SaveOptions { version: None, binary: true }).is_err());
|
||||
}
|
||||
|
||||
/// DWG at every version is that version and keeps the drawing: merged table cells, and from
|
||||
/// 2004 true colours, transparency and table styles (R2000 DWG has none of those).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn dwg_at_every_version_reads_back() {
|
||||
let d = sample();
|
||||
for v in Version::ALL {
|
||||
let dwg = write_with(&d, "x.dwg", &opts(v, false)).unwrap();
|
||||
assert_eq!(cadcraft_dwg::version(&dwg).as_deref(), Some(v.code()), "{v}");
|
||||
let back = read(&dwg, "x.dwg").unwrap();
|
||||
assert_eq!(save_version(&back), v, "{v}");
|
||||
assert_eq!(back.model.len(), d.model.len(), "{v}");
|
||||
assert_eq!(first_text(&back).as_deref(), Some(INTL), "{v}");
|
||||
assert_eq!(first_table(&back).map(|t| t.cells), Some(table().cells), "{v}: merged cells kept");
|
||||
if v >= Version::R2004 {
|
||||
let line = back.model.iter().find(|e| matches!(e.kind, EntityKind::Line(_))).unwrap();
|
||||
assert_eq!(line.common.color, Color::True(Rgb(1, 2, 3)), "{v}");
|
||||
assert_eq!(line.common.transparency, Transparency::Percent(40), "{v}");
|
||||
assert_eq!(back.table_styles.iter().find(|s| s.name == "Sched"), Some(&sched()), "{v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A drawing from an R2000 file saves as 2004 DWG unless 2000 is asked for: R2000 DWG would
|
||||
/// lose its true colours.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn dwg_default_is_at_least_2004() {
|
||||
let from_r2000 = read(write_dxf(&sample()).as_bytes(), "x.dxf").unwrap();
|
||||
assert_eq!(cadcraft_dwg::version(&write(&from_r2000, "x.dwg").unwrap()).as_deref(), Some("AC1018"));
|
||||
assert_eq!(cadcraft_dwg::version(&write(&sample(), "x.dwg").unwrap()).as_deref(), Some("AC1032"));
|
||||
}
|
||||
Reference in new issue
Block a user