1use 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#[derive(Debug, Default)]
19pub struct SchDoc {
20 pub primitives: Vec<SchRecord>,
22 pub document_name: Option<String>,
25}
26
27impl SchDoc {
28 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 schdoc.read_file_header(&mut cf)?;
40
41 Ok(schdoc)
42 }
43
44 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 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 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 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 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 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 Err(AltiumError::Parse(
121 "Binary records not supported in SchDoc".to_string(),
122 ))
123 } else {
124 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(¶m_str);
128 SchRecord::from_params(¶ms)
129 }
130 }
131
132 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 self.write_storage(&mut cf)?;
139
140 self.write_file_header(&mut cf)?;
142
143 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 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 fn write_storage<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
160 let mut data = Vec::new();
161
162 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 fn write_file_header<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
180 let mut data = Vec::new();
181
182 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 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 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, ¶ms)?;
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 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, ¶ms)?;
238 write_block(writer, &block, 0)
239 }
240
241 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 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 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 pub fn primitive_count(&self) -> usize {
276 self.primitives.len()
277 }
278}
279
280impl SchRecord {
281 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 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 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 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}