mod binary_reader;
mod section_reader;
mod stream_reader;
mod text_reader;
pub use binary_reader::DxfBinaryReader;
pub use stream_reader::DxfStreamReader;
use stream_reader::XDataRecorder;
pub use text_reader::DxfTextReader;
use section_reader::SectionReader;
use crate::document::CadDocument;
use crate::entities::solid3d::AcisVersion;
use crate::entities::EntityType;
use crate::error::{DxfError, Result};
use crate::io::read::{push_read_diagnostic, ReadDiagnostic, ReadStage, SourceFormat};
use crate::tables::TableEntry;
use crate::types::Handle;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read, Seek};
use std::path::Path;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DxfReaderConfiguration {
pub failsafe: bool,
pub default_encoding: Option<String>,
}
impl Default for DxfReaderConfiguration {
fn default() -> Self {
Self {
failsafe: false,
default_encoding: None,
}
}
}
pub struct DxfReader {
reader: Box<dyn DxfStreamReader>,
config: DxfReaderConfiguration,
estimated_entities: usize,
source_path: Option<String>,
}
impl DxfReader {
pub fn from_reader<R: Read + Seek + 'static>(reader: R) -> Result<Self> {
let mut buf_reader = BufReader::with_capacity(64 * 1024, reader);
let stream_size = buf_reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0);
buf_reader.seek(std::io::SeekFrom::Start(0))?;
let estimated_entities = (stream_size as usize / 300).max(16);
let is_binary = Self::is_binary(&mut buf_reader)?;
let reader: Box<dyn DxfStreamReader> = if is_binary {
Box::new(XDataRecorder::new(Box::new(DxfBinaryReader::new(
buf_reader,
)?)))
} else {
buf_reader.seek(std::io::SeekFrom::Start(0))?;
Box::new(XDataRecorder::new(Box::new(DxfTextReader::new(
buf_reader,
)?)))
};
Ok(Self {
reader,
config: DxfReaderConfiguration::default(),
estimated_entities,
source_path: None,
})
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let source_path = Some(path.as_ref().to_string_lossy().into_owned());
let file = File::open(path)?;
let mut buf_reader = BufReader::with_capacity(64 * 1024, file);
let stream_size = buf_reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0);
buf_reader.seek(std::io::SeekFrom::Start(0))?;
let estimated_entities = (stream_size as usize / 300).max(16);
let is_binary = Self::is_binary(&mut buf_reader)?;
let reader: Box<dyn DxfStreamReader> = if is_binary {
Box::new(XDataRecorder::new(Box::new(DxfBinaryReader::new(
buf_reader,
)?)))
} else {
buf_reader.seek(std::io::SeekFrom::Start(0))?;
Box::new(XDataRecorder::new(Box::new(DxfTextReader::new(
buf_reader,
)?)))
};
Ok(Self {
reader,
config: DxfReaderConfiguration::default(),
estimated_entities,
source_path,
})
}
fn is_binary<R: Read + Seek>(reader: &mut R) -> Result<bool> {
const SENTINEL: &[u8] = b"AutoCAD Binary DXF";
let mut buffer = vec![0u8; SENTINEL.len()];
let bytes_read = reader.read(&mut buffer)?;
reader.seek(std::io::SeekFrom::Start(0))?;
if bytes_read < SENTINEL.len() {
return Ok(false);
}
Ok(buffer == SENTINEL)
}
pub fn with_configuration(mut self, config: DxfReaderConfiguration) -> Self {
self.config = config;
self
}
fn read_diagnostic(
&self,
code: impl Into<String>,
stage: ReadStage,
message: impl Into<String>,
) -> ReadDiagnostic {
let context = self.reader.diagnostic_context();
let mut diagnostic = ReadDiagnostic::new(code, stage, message);
diagnostic.source_offset = context.source_offset;
diagnostic.source_offset_basis = Some("file-byte".to_string());
diagnostic.source_line = context.source_line;
diagnostic.record_handle = context.record_handle;
diagnostic.record_type = context.record_type;
diagnostic
}
pub fn read(self) -> Result<CadDocument> {
self.read_with_stats().map(|outcome| outcome.document)
}
pub fn recover(mut self) -> Result<crate::io::recover::RecoverOutcome> {
self.config.failsafe = true;
crate::io::recover::RecoverOutcome::from_read_outcome(self.read_with_stats()?)
}
pub fn read_with_stats(mut self) -> Result<crate::io::read::ReadOutcome> {
if let Some(ref encoding_name) = self.config.default_encoding {
if let Some(enc) = crate::io::dxf::code_page::encoding_from_code_page(encoding_name) {
self.reader.set_encoding(enc);
}
}
let mut document = CadDocument::new();
document.entities.reserve(self.estimated_entities);
document.entity_index.reserve(self.estimated_entities);
let default_entry_handles = snapshot_default_entry_handles(&document);
let failsafe = self.config.failsafe;
let mut source_sections = 0usize;
let mut source_records = 0usize;
let mut decoded_source_records = 0usize;
let mut skipped_source_records = 0usize;
let mut record_stream_read = false;
let mut stream_completed = false;
let mut diagnostics = Vec::new();
loop {
let pair = match self.reader.read_pair() {
Ok(Some(pair)) => pair,
Ok(None) if failsafe => {
let message = "Drawing stream ended before the EOF marker".to_string();
document.notifications.notify(
crate::notification::NotificationType::Error,
message.clone(),
);
push_read_diagnostic(
&mut diagnostics,
self.read_diagnostic(
"stream-ended-early",
ReadStage::RecordStream,
message,
),
);
break;
}
Ok(None) => {
return Err(DxfError::Parse(
"drawing stream ended before the EOF marker".to_string(),
));
}
Err(error) if failsafe => {
let message = format!("Error reading drawing stream: {}", error);
document.notifications.notify(
crate::notification::NotificationType::Error,
message.clone(),
);
push_read_diagnostic(
&mut diagnostics,
self.read_diagnostic(
"stream-read-failed",
ReadStage::RecordStream,
message,
),
);
break;
}
Err(error) => return Err(error),
};
if pair.code == 0 && pair.value_string == "SECTION" {
let section_pair = match self.reader.read_pair() {
Ok(Some(section_pair)) => Some(section_pair),
Ok(None) if failsafe => {
let message = "Drawing stream ended before the section name".to_string();
document.notifications.notify(
crate::notification::NotificationType::Error,
message.clone(),
);
push_read_diagnostic(
&mut diagnostics,
self.read_diagnostic(
"section-name-missing",
ReadStage::Section,
message,
),
);
break;
}
Ok(None) => {
return Err(DxfError::Parse(
"drawing stream ended before the section name".to_string(),
));
}
Err(error) if failsafe => {
let message = format!("Error reading section name: {}", error);
document.notifications.notify(
crate::notification::NotificationType::Error,
message.clone(),
);
push_read_diagnostic(
&mut diagnostics,
self.read_diagnostic(
"section-name-read-failed",
ReadStage::Section,
message,
),
);
break;
}
Err(error) => return Err(error),
};
if let Some(section_pair) = section_pair {
if section_pair.code == 2 {
let section_name = section_pair.value_string.clone();
let carries_records = matches!(
section_name.as_str(),
"TABLES" | "BLOCKS" | "ENTITIES" | "OBJECTS" | "ACDSDATA"
);
let decoded_before = decoded_source_records;
let result = match section_name.as_str() {
"HEADER" => self.read_header_section(&mut document),
"CLASSES" => self.read_classes_section(&mut document),
"TABLES" => self.read_tables_section(
&mut document,
&mut decoded_source_records,
&mut skipped_source_records,
&mut diagnostics,
),
"BLOCKS" => self.read_blocks_section(
&mut document,
&mut decoded_source_records,
&mut skipped_source_records,
&mut diagnostics,
),
"ENTITIES" => self.read_entities_section(
&mut document,
&mut decoded_source_records,
&mut skipped_source_records,
&mut diagnostics,
),
"OBJECTS" => self.read_objects_section(
&mut document,
&mut decoded_source_records,
&mut skipped_source_records,
&mut diagnostics,
),
"ACDSDATA" => self.read_acdsdata_section(&mut document),
"THUMBNAILIMAGE" => {
document.notifications.notify(
crate::notification::NotificationType::NotImplemented,
"THUMBNAILIMAGE section skipped",
);
self.skip_section()
}
_ => {
self.skip_section()
}
};
if carries_records {
source_sections = source_sections.saturating_add(1);
record_stream_read = true;
let decoded = decoded_source_records.saturating_sub(decoded_before);
source_records = source_records.saturating_add(decoded);
}
if let Err(e) = result {
if failsafe {
let message =
format!("Error reading {} section: {}", section_name, e);
document.notifications.notify(
crate::notification::NotificationType::Error,
message.clone(),
);
let mut diagnostic = self.read_diagnostic(
"section-read-failed",
ReadStage::Section,
message,
);
diagnostic.section = Some(section_name.clone());
push_read_diagnostic(&mut diagnostics, diagnostic);
skipped_source_records = skipped_source_records.saturating_add(1);
let _ = self.skip_section();
} else {
return Err(e);
}
}
}
}
} else if pair.code == 0 && pair.value_string == "EOF" {
stream_completed = true;
break;
}
}
document.synchronize_handle_allocator();
rehandle_colliding_default_entries(&mut document, &default_entry_handles);
document.resolve_references();
rewire_stale_text_style_references(&mut document);
rewire_dangling_layout_block_records(&mut document);
document.ensure_model_layout();
if document.version == crate::types::DxfVersion::AC1009 {
let layers: HashMap<_, _> = document
.layers
.iter()
.map(|layer| (layer.name.to_uppercase(), layer.handle))
.collect();
for entity in document.entities_mut_keep_raw() {
if let EntityType::Viewport(viewport) = entity {
crate::io::dwg::legacy_viewport::restore_r12(viewport, |name| {
layers.get(&name.to_uppercase()).copied()
});
}
}
}
if document.version < crate::types::DxfVersion::AC1018 {
crate::io::dwg::dwg_reader::recover_roundtrip_gradients(&mut document);
crate::io::dwg::dwg_reader::recover_mtext_bg_roundtrip(&mut document);
}
document.restore_constraint_vertex_ids();
source_records = source_records.saturating_add(skipped_source_records);
let stats = crate::io::read::ReadStats::from_document(
&document,
SourceFormat::Dxf,
source_sections,
source_records,
decoded_source_records,
skipped_source_records,
record_stream_read,
failsafe,
stream_completed,
diagnostics,
);
crate::document::sync_dimension_arrow_header(&mut document);
document.source_path = self.source_path.take();
crate::objects::restore_visual_style_roundtrip(&mut document);
crate::objects::restore_table_style_roundtrip(&mut document);
Ok(crate::io::read::ReadOutcome::new(document, stats))
}
fn read_header_section(&mut self, document: &mut CadDocument) -> Result<()> {
let mut section_reader = SectionReader::new(&mut self.reader);
section_reader.read_header(document)
}
fn read_classes_section(&mut self, document: &mut CadDocument) -> Result<()> {
let mut section_reader = SectionReader::new(&mut self.reader);
section_reader.read_classes(document)
}
fn read_tables_section(
&mut self,
document: &mut CadDocument,
decoded_records: &mut usize,
skipped_records: &mut usize,
diagnostics: &mut Vec<ReadDiagnostic>,
) -> Result<()> {
let mut section_reader =
SectionReader::new(&mut self.reader).with_failsafe(self.config.failsafe);
let result = section_reader.read_tables(document);
*decoded_records = decoded_records.saturating_add(section_reader.decoded_records());
Self::collect_section_recovery(
&mut section_reader,
document,
"TABLES",
skipped_records,
diagnostics,
);
result
}
fn read_blocks_section(
&mut self,
document: &mut CadDocument,
decoded_records: &mut usize,
skipped_records: &mut usize,
diagnostics: &mut Vec<ReadDiagnostic>,
) -> Result<()> {
let mut section_reader =
SectionReader::new(&mut self.reader).with_failsafe(self.config.failsafe);
let result = section_reader.read_blocks(document);
*decoded_records = decoded_records.saturating_add(section_reader.decoded_records());
Self::collect_section_recovery(
&mut section_reader,
document,
"BLOCKS",
skipped_records,
diagnostics,
);
result
}
fn read_entities_section(
&mut self,
document: &mut CadDocument,
decoded_records: &mut usize,
skipped_records: &mut usize,
diagnostics: &mut Vec<ReadDiagnostic>,
) -> Result<()> {
let mut section_reader =
SectionReader::new(&mut self.reader).with_failsafe(self.config.failsafe);
let result = section_reader.read_entities(document);
*decoded_records = decoded_records.saturating_add(section_reader.decoded_records());
Self::collect_section_recovery(
&mut section_reader,
document,
"ENTITIES",
skipped_records,
diagnostics,
);
result
}
fn read_objects_section(
&mut self,
document: &mut CadDocument,
decoded_records: &mut usize,
skipped_records: &mut usize,
diagnostics: &mut Vec<ReadDiagnostic>,
) -> Result<()> {
let mut section_reader =
SectionReader::new(&mut self.reader).with_failsafe(self.config.failsafe);
let result = section_reader.read_objects(document);
*decoded_records = decoded_records.saturating_add(section_reader.decoded_records());
Self::collect_section_recovery(
&mut section_reader,
document,
"OBJECTS",
skipped_records,
diagnostics,
);
result
}
fn collect_section_recovery(
section_reader: &mut SectionReader<'_>,
document: &mut CadDocument,
section: &str,
skipped_records: &mut usize,
diagnostics: &mut Vec<ReadDiagnostic>,
) {
*skipped_records = skipped_records.saturating_add(section_reader.skipped_records());
for recovery in section_reader.take_recovery_errors() {
let message = format!("Error reading {} record: {}", section, recovery.message);
document.notifications.notify(
crate::notification::NotificationType::Error,
message.clone(),
);
let mut diagnostic =
ReadDiagnostic::new("record-read-failed", ReadStage::RecordStream, message);
diagnostic.section = Some(section.to_string());
diagnostic.source_offset = recovery.context.source_offset;
diagnostic.source_offset_basis = Some("file-byte".to_string());
diagnostic.source_line = recovery.context.source_line;
diagnostic.record_handle = recovery.context.record_handle;
diagnostic.record_type = recovery.context.record_type;
push_read_diagnostic(diagnostics, diagnostic);
}
}
fn read_acdsdata_section(&mut self, document: &mut CadDocument) -> Result<()> {
let mut blobs: Vec<(u64, Vec<u8>)> = Vec::new();
let mut cur_handle: Option<u64> = None;
let mut is_asm = false;
let mut chunks: Vec<u8> = Vec::new();
while let Some(pair) = self.reader.read_pair()? {
if pair.code == 0 {
if is_asm && !chunks.is_empty() {
if let Some(h) = cur_handle {
blobs.push((h, std::mem::take(&mut chunks)));
}
}
cur_handle = None;
is_asm = false;
chunks.clear();
if pair.value_string == "ENDSEC" {
break;
}
continue;
}
match pair.code {
320 => {
if let Ok(h) = u64::from_str_radix(pair.value_string.trim(), 16) {
cur_handle = Some(h);
}
}
2 => is_asm = pair.value_string == "ASM_Data",
310 if is_asm => {
let hex = pair.value_string.trim().as_bytes();
let mut i = 0;
while i + 1 < hex.len() {
let hi = (hex[i] as char).to_digit(16);
let lo = (hex[i + 1] as char).to_digit(16);
if let (Some(hi), Some(lo)) = (hi, lo) {
chunks.push((hi * 16 + lo) as u8);
}
i += 2;
}
}
_ => {}
}
}
for (handle, sab) in blobs {
let Some(entity) = document.get_entity_mut(Handle::new(handle)) else {
continue;
};
match entity {
EntityType::Solid3D(e) => {
e.acis_data.sab_data = sab;
e.acis_data.is_binary = true;
e.acis_data.version = AcisVersion::Version2;
e.point_of_reference = e
.acis_data
.geometry_centre()
.or_else(|| e.acis_data.placement_origin())
.unwrap_or(e.point_of_reference);
}
EntityType::Region(e) => {
e.acis_data.sab_data = sab;
e.acis_data.is_binary = true;
e.acis_data.version = AcisVersion::Version2;
e.point_of_reference = e
.acis_data
.geometry_centre()
.or_else(|| e.acis_data.placement_origin())
.unwrap_or(e.point_of_reference);
}
EntityType::Body(e) => {
e.acis_data.sab_data = sab;
e.acis_data.is_binary = true;
e.acis_data.version = AcisVersion::Version2;
e.point_of_reference = e
.acis_data
.geometry_centre()
.or_else(|| e.acis_data.placement_origin())
.unwrap_or(e.point_of_reference);
}
EntityType::Surface(e) => {
e.acis_data.sab_data = sab;
e.acis_data.is_binary = true;
e.acis_data.version = AcisVersion::Version2;
}
_ => {}
}
}
Ok(())
}
fn skip_section(&mut self) -> Result<()> {
while let Some(pair) = self.reader.read_pair()? {
if pair.code == 0 && pair.value_string == "ENDSEC" {
break;
}
}
Ok(())
}
}
fn snapshot_default_entry_handles(document: &CadDocument) -> HashMap<(&'static str, String), u64> {
let mut map: HashMap<(&'static str, String), u64> = HashMap::new();
macro_rules! snapshot {
($tag:literal, $table:expr) => {
for entry in $table.iter() {
let handle = entry.handle().value();
if handle != 0 {
map.insert(($tag, entry.name().to_string()), handle);
}
}
};
}
snapshot!("layers", document.layers);
snapshot!("line_types", document.line_types);
snapshot!("text_styles", document.text_styles);
snapshot!("dim_styles", document.dim_styles);
snapshot!("app_ids", document.app_ids);
snapshot!("views", document.views);
snapshot!("vports", document.vports);
snapshot!("ucss", document.ucss);
snapshot!("vx_table", document.vx_table);
snapshot!("block_records", document.block_records);
map
}
fn rehandle_colliding_default_entries(
document: &mut CadDocument,
defaults: &HashMap<(&'static str, String), u64>,
) {
if defaults.is_empty() {
return;
}
let mut usage: HashMap<u64, usize> = HashMap::new();
macro_rules! count {
($handle:expr) => {{
let handle: u64 = $handle;
if handle != 0 {
*usage.entry(handle).or_insert(0) += 1;
}
}};
}
macro_rules! count_table {
($table:expr) => {
for entry in $table.iter() {
count!(entry.handle().value());
}
};
}
count_table!(document.layers);
count_table!(document.line_types);
count_table!(document.text_styles);
count_table!(document.dim_styles);
count_table!(document.app_ids);
count_table!(document.views);
count_table!(document.vports);
count_table!(document.ucss);
count_table!(document.vx_table);
count_table!(document.block_records);
for br in document.block_records.iter() {
count!(br.block_entity_handle.value());
count!(br.block_end_handle.value());
}
for entity in document.entities() {
count!(entity.common().handle.value());
}
for handle in document.objects.keys() {
count!(handle.value());
}
let collides = |tag: &'static str, name: &str, handle: u64| -> bool {
handle != 0
&& defaults.get(&(tag, name.to_string())) == Some(&handle)
&& usage.get(&handle).copied().unwrap_or(0) > 1
};
let mut moves: Vec<(&'static str, String, u64)> = Vec::new();
macro_rules! collect {
($tag:literal, $table:expr) => {
for entry in $table.iter() {
if collides($tag, entry.name(), entry.handle().value()) {
moves.push(($tag, entry.name().to_string(), entry.handle().value()));
}
}
};
}
collect!("layers", document.layers);
collect!("line_types", document.line_types);
collect!("text_styles", document.text_styles);
collect!("dim_styles", document.dim_styles);
collect!("app_ids", document.app_ids);
collect!("views", document.views);
collect!("vports", document.vports);
collect!("ucss", document.ucss);
collect!("vx_table", document.vx_table);
collect!("block_records", document.block_records);
for (tag, name, old) in moves {
let new = document.allocate_handle();
macro_rules! apply {
($t:literal, $table:expr) => {
if tag == $t {
if let Some(entry) = $table.get_mut(&name) {
entry.set_handle(new);
}
}
};
}
apply!("layers", document.layers);
apply!("line_types", document.line_types);
apply!("text_styles", document.text_styles);
apply!("dim_styles", document.dim_styles);
apply!("app_ids", document.app_ids);
apply!("views", document.views);
apply!("vports", document.vports);
apply!("ucss", document.ucss);
apply!("vx_table", document.vx_table);
apply!("block_records", document.block_records);
let header = &mut document.header;
macro_rules! follow {
($field:ident, $t:literal) => {
if tag == $t && header.$field.value() == old {
header.$field = new;
}
};
}
follow!(current_layer_handle, "layers");
follow!(continuous_linetype_handle, "line_types");
follow!(bylayer_linetype_handle, "line_types");
follow!(current_linetype_handle, "line_types");
follow!(byblock_linetype_handle, "line_types");
follow!(current_text_style_handle, "text_styles");
follow!(dim_text_style_handle, "text_styles");
follow!(current_dimstyle_handle, "dim_styles");
follow!(model_space_block_handle, "block_records");
follow!(paper_space_block_handle, "block_records");
follow!(dim_linetype_handle, "line_types");
follow!(dim_linetype1_handle, "line_types");
follow!(dim_linetype2_handle, "line_types");
if tag == "text_styles" {
for ds in document.dim_styles.iter_mut() {
if ds.dimtxsty_handle.value() == old {
ds.dimtxsty_handle = new;
}
}
}
}
}
fn rewire_dangling_layout_block_records(document: &mut CadDocument) {
use crate::objects::ObjectType;
let records: HashMap<u64, String> = document
.block_records
.iter()
.map(|record| (record.handle.value(), record.name.to_ascii_lowercase()))
.collect();
let mut dangling = Vec::new();
let mut owned = std::collections::HashSet::new();
for (handle, object) in &document.objects {
if let ObjectType::Layout(layout) = object {
if records.contains_key(&layout.block_record.value()) {
owned.insert(layout.block_record.value());
} else {
dangling.push((
layout.tab_order,
*handle,
layout.name.eq_ignore_ascii_case("Model"),
));
}
}
}
if dangling.is_empty() {
return;
}
dangling.sort();
let mut free_paper: Vec<(Option<u64>, u64)> = records
.iter()
.filter(|(handle, _)| !owned.contains(*handle))
.filter_map(|(handle, name)| {
let suffix = name.strip_prefix("*paper_space")?;
let order = if suffix.is_empty() {
None
} else {
Some(suffix.parse().ok()?)
};
Some((order, *handle))
})
.collect();
free_paper.sort();
let mut free_paper = free_paper.into_iter().map(|(_, handle)| handle);
let model = document.header.model_space_block_handle.value();
for (_, layout_handle, is_model) in dangling {
let target = if is_model && records.contains_key(&model) && !owned.contains(&model) {
model
} else if is_model {
continue;
} else {
let Some(handle) = free_paper.next() else {
continue;
};
handle
};
owned.insert(target);
if let Some(ObjectType::Layout(layout)) = document.objects.get_mut(&layout_handle) {
layout.block_record = Handle::new(target);
}
if let Some(record) = document
.block_records
.iter_mut()
.find(|record| record.handle.value() == target)
{
record.layout = layout_handle;
}
}
}
fn rewire_stale_text_style_references(document: &mut CadDocument) {
let text_style_handles: std::collections::HashSet<u64> = document
.text_styles
.iter()
.map(|style| style.handle.value())
.collect();
let is_stale =
|handle: Handle| !handle.is_null() && !text_style_handles.contains(&handle.value());
let resolve = |document: &CadDocument, name: &str| -> Option<Handle> {
document.text_styles.get(name).map(|s| s.handle)
};
let mut stale_names: Vec<String> = Vec::new();
for style in document.dim_styles.iter() {
if is_stale(style.dimtxsty_handle) {
stale_names.push(style.name().to_string());
}
}
for name in stale_names {
if let Some(new) = resolve(document, &name) {
if let Some(style) = document.dim_styles.get_mut(&name) {
style.dimtxsty_handle = new;
}
}
}
let mut stale_styles: Vec<(Handle, String)> = Vec::new();
for (handle, object) in document.objects.iter() {
if let crate::objects::ObjectType::MultiLeaderStyle(style) = object {
if style.text_style_handle.is_some_and(is_stale) {
stale_styles.push((*handle, style.name.clone()));
}
}
}
for (handle, name) in stale_styles {
let Some(new) = resolve(document, &name) else {
continue;
};
if let Some(crate::objects::ObjectType::MultiLeaderStyle(style)) =
document.objects.get_mut(&handle)
{
style.text_style_handle = Some(new);
}
}
}