Skip to main content

altium_format/io/
schdoc.rs

1//! SchDoc reader/writer for Altium schematic document files.
2
3use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
4use cfb::CompoundFile;
5use std::fs::File;
6use std::io::{Cursor, Read, Seek, Write};
7use std::path::Path;
8
9use crate::dump::{DumpTree, TreeBuilder};
10use crate::error::{AltiumError, Result};
11use crate::format::SIZE_FLAG_MASK;
12use crate::io::reader::{decode_windows_1252, read_parameters_block};
13use crate::io::writer::{write_block, write_parameters};
14use crate::records::sch::{SchPrimitive, SchRecord, SchSheetHeader};
15use crate::types::ParameterCollection;
16
17/// A schematic document containing primitives.
18#[derive(Debug, Default)]
19pub struct SchDoc {
20    /// All primitives in the document.
21    pub primitives: Vec<SchRecord>,
22    /// Optional document name (typically the filename without extension).
23    /// This is used as the sheet name in queries, not the Title parameter.
24    pub document_name: Option<String>,
25}
26
27impl SchDoc {
28    /// Open and read a SchDoc file.
29    pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
30        let mut schdoc = SchDoc::default();
31        let mut cf = CompoundFile::open(reader).map_err(|e| {
32            AltiumError::Io(std::io::Error::new(
33                std::io::ErrorKind::InvalidData,
34                e.to_string(),
35            ))
36        })?;
37
38        // Read FileHeader stream (contains all primitives)
39        schdoc.read_file_header(&mut cf)?;
40
41        Ok(schdoc)
42    }
43
44    /// Open and read a SchDoc file from a path.
45    pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
46        let path_ref = path.as_ref();
47        let file = File::open(path_ref)?;
48        let mut doc = Self::open(file)?;
49
50        // Extract document name from filename (without extension)
51        if let Some(file_stem) = path_ref.file_stem() {
52            if let Some(name) = file_stem.to_str() {
53                doc.document_name = Some(name.to_string());
54            }
55        }
56
57        Ok(doc)
58    }
59
60    /// Read the FileHeader stream.
61    fn read_file_header<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
62        let stream_path = "/FileHeader";
63        let mut stream = cf.open_stream(stream_path).map_err(|e| {
64            AltiumError::Io(std::io::Error::new(
65                std::io::ErrorKind::NotFound,
66                e.to_string(),
67            ))
68        })?;
69
70        let mut data = Vec::new();
71        stream.read_to_end(&mut data)?;
72
73        if data.is_empty() {
74            return Ok(());
75        }
76
77        let mut cursor = Cursor::new(&data);
78
79        // Read header parameters
80        let header_params = read_parameters_block(&mut cursor)?;
81        let _header = header_params.get("HEADER").map(|v| v.as_str().to_string());
82        let _weight = header_params
83            .get("WEIGHT")
84            .map(|v| v.as_int_or(0))
85            .unwrap_or(0);
86
87        // Read all primitives
88        while (cursor.position() as usize) < data.len() {
89            match self.read_record(&mut cursor) {
90                Ok(record) => self.primitives.push(record),
91                Err(e) => {
92                    log::warn!(
93                        "Failed to read record at position {}: {}, skipping remaining records",
94                        cursor.position(),
95                        e
96                    );
97                    break;
98                }
99            }
100        }
101
102        Ok(())
103    }
104
105    /// Read a single record from the stream.
106    fn read_record<R: Read>(&self, reader: &mut R) -> Result<SchRecord> {
107        let size = reader.read_i32::<LittleEndian>()?;
108        let is_binary = (size as u32 & !SIZE_FLAG_MASK) != 0;
109        let clean_size = (size & SIZE_FLAG_MASK as i32) as usize;
110
111        if clean_size == 0 {
112            return Err(AltiumError::Parse("Empty record".to_string()));
113        }
114
115        let mut buffer = vec![0u8; clean_size];
116        reader.read_exact(&mut buffer)?;
117
118        if is_binary {
119            // Binary pin record (not common in SchDoc)
120            Err(AltiumError::Parse(
121                "Binary records not supported in SchDoc".to_string(),
122            ))
123        } else {
124            // ASCII parameter record
125            let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
126            let param_str = decode_windows_1252(&buffer[..end]);
127            let params = ParameterCollection::from_string(&param_str);
128            SchRecord::from_params(&params)
129        }
130    }
131
132    /// Save the SchDoc to a file.
133    pub fn save<W: Read + Write + Seek>(&self, writer: W) -> Result<()> {
134        let mut cf = CompoundFile::create(writer)
135            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
136
137        // Write Storage stream (icon storage header)
138        self.write_storage(&mut cf)?;
139
140        // Write FileHeader
141        self.write_file_header(&mut cf)?;
142
143        // Write Additional stream
144        self.write_additional(&mut cf)?;
145
146        cf.flush()
147            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
148
149        Ok(())
150    }
151
152    /// Save the SchDoc to a file path.
153    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
154        let file = File::create(path)?;
155        self.save(file)
156    }
157
158    /// Write the Storage stream (icon storage header).
159    fn write_storage<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
160        let mut data = Vec::new();
161
162        // Write the icon storage header block
163        let header = "|HEADER=Icon storage\0";
164        let header_bytes = header.as_bytes();
165        data.write_i32::<LittleEndian>(header_bytes.len() as i32)?;
166        data.write_all(header_bytes)?;
167
168        let stream = cf
169            .create_stream("/Storage")
170            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
171
172        let mut stream = stream;
173        stream.write_all(&data)?;
174
175        Ok(())
176    }
177
178    /// Write the FileHeader stream.
179    fn write_file_header<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
180        let mut data = Vec::new();
181
182        // Write header parameters
183        let mut header_params = ParameterCollection::new();
184        header_params.add(
185            "HEADER",
186            "Protel for Windows - Schematic Capture Binary File Version 5.0",
187        );
188        header_params.add_int("WEIGHT", self.primitives.len() as i32);
189
190        let mut header_block = Vec::new();
191        write_parameters(&mut header_block, &header_params)?;
192        write_block(&mut data, &header_block, 0)?;
193
194        // Write all primitives
195        for record in &self.primitives {
196            self.write_record(&mut data, record)?;
197        }
198
199        let stream = cf
200            .create_stream("/FileHeader")
201            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
202
203        let mut stream = stream;
204        stream.write_all(&data)?;
205
206        Ok(())
207    }
208
209    /// Write the Additional stream.
210    fn write_additional<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
211        let mut data = Vec::new();
212
213        let mut params = ParameterCollection::new();
214        params.add(
215            "HEADER",
216            "Protel for Windows - Schematic Capture Binary File Version 5.0",
217        );
218
219        let mut block = Vec::new();
220        write_parameters(&mut block, &params)?;
221        write_block(&mut data, &block, 0)?;
222
223        let stream = cf
224            .create_stream("/Additional")
225            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
226
227        let mut stream = stream;
228        stream.write_all(&data)?;
229
230        Ok(())
231    }
232
233    /// Write a single record to the stream.
234    fn write_record<W: Write>(&self, writer: &mut W, record: &SchRecord) -> Result<()> {
235        let params = record.export_to_params();
236        let mut block = Vec::new();
237        write_parameters(&mut block, &params)?;
238        write_block(writer, &block, 0)
239    }
240
241    /// Get the sheet header if present.
242    pub fn sheet_header(&self) -> Option<&SchSheetHeader> {
243        self.primitives.iter().find_map(|r| {
244            if let SchRecord::SheetHeader(h) = r {
245                Some(h)
246            } else {
247                None
248            }
249        })
250    }
251
252    /// Get all components in the document.
253    pub fn components(&self) -> impl Iterator<Item = &crate::records::sch::SchComponent> {
254        self.primitives.iter().filter_map(|r| {
255            if let SchRecord::Component(c) = r {
256                Some(c)
257            } else {
258                None
259            }
260        })
261    }
262
263    /// Get all wires in the document.
264    pub fn wires(&self) -> impl Iterator<Item = &crate::records::sch::SchWire> {
265        self.primitives.iter().filter_map(|r| {
266            if let SchRecord::Wire(w) = r {
267                Some(w)
268            } else {
269                None
270            }
271        })
272    }
273
274    /// Get the number of primitives.
275    pub fn primitive_count(&self) -> usize {
276        self.primitives.len()
277    }
278}
279
280impl SchRecord {
281    /// Export record to parameters.
282    pub fn export_to_params(&self) -> ParameterCollection {
283        match self {
284            SchRecord::Component(r) => r.export_to_params(),
285            SchRecord::Pin(r) => r.export_to_params(),
286            SchRecord::Symbol(r) => r.export_to_params(),
287            SchRecord::Label(r) => r.export_to_params(),
288            SchRecord::Bezier(r) => r.export_to_params(),
289            SchRecord::Polyline(r) => r.export_to_params(),
290            SchRecord::Polygon(r) => r.export_to_params(),
291            SchRecord::Ellipse(r) => r.export_to_params(),
292            SchRecord::Pie(r) => r.export_to_params(),
293            SchRecord::EllipticalArc(r) => r.export_to_params(),
294            SchRecord::Arc(r) => r.export_to_params(),
295            SchRecord::Line(r) => r.export_to_params(),
296            SchRecord::Rectangle(r) => r.export_to_params(),
297            SchRecord::PowerObject(r) => r.export_to_params(),
298            SchRecord::Port(r) => r.export_to_params(),
299            SchRecord::NoErc(r) => r.export_to_params(),
300            SchRecord::NetLabel(r) => r.export_to_params(),
301            SchRecord::Bus(r) => r.export_to_params(),
302            SchRecord::Wire(r) => r.export_to_params(),
303            SchRecord::TextFrame(r) => r.export_to_params(),
304            SchRecord::TextFrameVariant(r) => r.export_to_params(),
305            SchRecord::Junction(r) => r.export_to_params(),
306            SchRecord::Image(r) => r.export_to_params(),
307            SchRecord::SheetHeader(r) => r.export_to_params(),
308            SchRecord::Designator(r) => r.export_to_params(),
309            SchRecord::BusEntry(r) => r.export_to_params(),
310            SchRecord::Parameter(r) => r.export_to_params(),
311            SchRecord::WarningSign(r) => r.export_to_params(),
312            SchRecord::ImplementationList(r) => r.export_to_params(),
313            SchRecord::Implementation(r) => r.export_to_params(),
314            SchRecord::MapDefinerList(r) => r.export_to_params(),
315            SchRecord::MapDefiner(r) => r.export_to_params(),
316            SchRecord::ImplementationParameters(r) => r.export_to_params(),
317            SchRecord::Unknown { record_id, params } => {
318                let mut p = params.clone();
319                p.add_int("RECORD", *record_id);
320                p
321            }
322        }
323    }
324}
325
326impl DumpTree for SchDoc {
327    fn dump(&self, tree: &mut TreeBuilder) {
328        tree.root(&format!("SchDoc ({} primitives)", self.primitives.len()));
329
330        // Count by type
331        let mut component_count = 0;
332        let mut wire_count = 0;
333        let mut other_count = 0;
334
335        for prim in &self.primitives {
336            match prim {
337                SchRecord::Component(_) => component_count += 1,
338                SchRecord::Wire(_) => wire_count += 1,
339                _ => other_count += 1,
340            }
341        }
342
343        // Summary
344        tree.push(true);
345        tree.add_leaf(
346            "Summary",
347            &[
348                ("components", format!("{}", component_count)),
349                ("wires", format!("{}", wire_count)),
350                ("other", format!("{}", other_count)),
351            ],
352        );
353        tree.pop();
354
355        // Show primitives
356        tree.push(false);
357        tree.begin_node(&format!("Primitives ({})", self.primitives.len()));
358        for (i, prim) in self.primitives.iter().enumerate() {
359            tree.push(i < self.primitives.len() - 1);
360            prim.dump(tree);
361            tree.pop();
362        }
363        tree.pop();
364    }
365}