Skip to main content

altium_format/io/
schlib.rs

1//! SchLib reader/writer for Altium schematic library files.
2
3use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
4use cfb::CompoundFile;
5use std::collections::HashMap;
6use std::fs::File;
7use std::io::{Cursor, Read, Seek, Write};
8use std::path::Path;
9
10use crate::error::{AltiumError, Result};
11use crate::format::SIZE_FLAG_MASK;
12use crate::io::reader::{
13    decode_windows_1252, read_parameters_block, read_pascal_short_string, read_string_block,
14};
15use crate::io::writer::{
16    write_block, write_parameters, write_pascal_short_string, write_string_block,
17};
18use crate::records::sch::{
19    PinConglomerateFlags, PinElectricalType, PinSymbol, SchComponent, SchGraphicalBase, SchPin,
20    SchRecord, coord_to_dxp_frac, dxp_frac_to_coord,
21};
22use crate::types::ParameterCollection;
23
24/// A schematic library containing components.
25#[derive(Debug, Default)]
26pub struct SchLib {
27    /// Section keys mapping LIBREF to storage path.
28    section_keys: HashMap<String, String>,
29    /// Components in the library.
30    pub components: Vec<SchLibComponent>,
31}
32
33/// A component in the schematic library.
34#[derive(Debug)]
35pub struct SchLibComponent {
36    /// Component data record.
37    pub component: SchComponent,
38    /// All primitives belonging to this component.
39    pub primitives: Vec<SchRecord>,
40}
41
42impl SchLib {
43    /// Open and read a SchLib file.
44    pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
45        let mut schlib = SchLib::default();
46        let mut cf = CompoundFile::open(reader).map_err(|e| {
47            AltiumError::Io(std::io::Error::new(
48                std::io::ErrorKind::InvalidData,
49                e.to_string(),
50            ))
51        })?;
52
53        // Read section keys
54        schlib.read_section_keys(&mut cf)?;
55
56        // Read file header to get component list
57        let ref_names = schlib.read_file_header(&mut cf)?;
58
59        // Read each component
60        for ref_name in ref_names.iter() {
61            let section_key = schlib.get_section_key(ref_name);
62            if let Ok(component) = schlib.read_component(&mut cf, &section_key) {
63                schlib.components.push(component);
64            }
65        }
66
67        Ok(schlib)
68    }
69
70    /// Open and read a SchLib file from a path.
71    pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
72        let file = File::open(path)?;
73        Self::open(file)
74    }
75
76    /// Save the SchLib to a file.
77    pub fn save<W: Read + Write + Seek>(&self, writer: W) -> Result<()> {
78        let mut cf = CompoundFile::create(writer)
79            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
80
81        // Write Storage stream (icon storage header)
82        self.write_storage(&mut cf)?;
83
84        // Write FileHeader
85        self.write_file_header(&mut cf)?;
86
87        // Write SectionKeys if needed
88        self.write_section_keys(&mut cf)?;
89
90        // Write each component
91        for comp in &self.components {
92            self.write_component(&mut cf, comp)?;
93        }
94
95        cf.flush()
96            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
97
98        Ok(())
99    }
100
101    /// Save the SchLib to a file path.
102    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
103        let file = File::create(path)?;
104        self.save(file)
105    }
106
107    /// Write the Storage stream (icon storage header).
108    fn write_storage<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
109        let mut data = Vec::new();
110
111        // Write the icon storage header block
112        let header = "|HEADER=Icon storage\0";
113        let header_bytes = header.as_bytes();
114        data.write_i32::<LittleEndian>(header_bytes.len() as i32)?;
115        data.write_all(header_bytes)?;
116
117        let stream = cf
118            .create_stream("/Storage")
119            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
120
121        let mut stream = stream;
122        stream.write_all(&data)?;
123
124        Ok(())
125    }
126
127    /// Write the FileHeader stream.
128    fn write_file_header<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
129        let mut data = Vec::new();
130
131        // Write header parameters
132        let mut header_params = ParameterCollection::new();
133        header_params.add(
134            "HEADER",
135            "Protel for Windows - Schematic Library Editor Binary File Version 5.0",
136        );
137        header_params.add_int("WEIGHT", self.components.len() as i32);
138
139        let mut header_block = Vec::new();
140        write_parameters(&mut header_block, &header_params)?;
141        write_block(&mut data, &header_block, 0)?;
142
143        // Write component count
144        data.write_i32::<LittleEndian>(self.components.len() as i32)?;
145
146        // Write component names
147        for comp in &self.components {
148            write_string_block(&mut data, &comp.component.lib_reference)?;
149        }
150
151        let stream = cf
152            .create_stream("/FileHeader")
153            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
154
155        let mut stream = stream;
156        stream.write_all(&data)?;
157
158        Ok(())
159    }
160
161    /// Write the SectionKeys stream (for components with names that need aliasing).
162    fn write_section_keys<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
163        // Only write section keys for components that need them
164        let components_needing_keys: Vec<_> = self
165            .components
166            .iter()
167            .filter(|c| Self::needs_section_key(&c.component.lib_reference))
168            .collect();
169
170        if components_needing_keys.is_empty() {
171            return Ok(());
172        }
173
174        let mut data = Vec::new();
175
176        let mut params = ParameterCollection::new();
177        params.add_int("KEYCOUNT", components_needing_keys.len() as i32);
178
179        for (i, comp) in components_needing_keys.iter().enumerate() {
180            let lib_ref = &comp.component.lib_reference;
181            let section_key = Self::get_section_key_for(lib_ref);
182            params.add(&format!("LIBREF{}", i), lib_ref);
183            params.add(&format!("SECTIONKEY{}", i), &section_key);
184        }
185
186        let mut block = Vec::new();
187        write_parameters(&mut block, &params)?;
188        write_block(&mut data, &block, 0)?;
189
190        let stream = cf
191            .create_stream("/SectionKeys")
192            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
193
194        let mut stream = stream;
195        stream.write_all(&data)?;
196
197        Ok(())
198    }
199
200    /// Check if a component name needs a section key alias.
201    fn needs_section_key(name: &str) -> bool {
202        name.len() > 31 || name.contains('/')
203    }
204
205    /// Generate a section key for a component name.
206    fn get_section_key_for(name: &str) -> String {
207        let mut key = name.replace('/', "_");
208        if key.len() > 31 {
209            key.truncate(31);
210        }
211        key
212    }
213
214    /// Write a component to its storage.
215    fn write_component<F: Read + Write + Seek>(
216        &self,
217        cf: &mut CompoundFile<F>,
218        comp: &SchLibComponent,
219    ) -> Result<()> {
220        let section_key = if Self::needs_section_key(&comp.component.lib_reference) {
221            Self::get_section_key_for(&comp.component.lib_reference)
222        } else {
223            comp.component.lib_reference.clone()
224        };
225
226        // Create storage for component
227        let storage_path = format!("/{}", section_key);
228        cf.create_storage(&storage_path)
229            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
230
231        // Write Data stream
232        let mut data = Vec::new();
233        for record in &comp.primitives {
234            self.write_record(&mut data, record)?;
235        }
236
237        let data_path = format!("{}/Data", storage_path);
238        let stream = cf
239            .create_stream(&data_path)
240            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
241
242        let mut stream = stream;
243        stream.write_all(&data)?;
244
245        Ok(())
246    }
247
248    /// Write a single record to the stream.
249    fn write_record<W: Write>(&self, writer: &mut W, record: &SchRecord) -> Result<()> {
250        match record {
251            SchRecord::Pin(pin) => {
252                // Pins are written in binary format with flag 0x01
253                self.write_binary_pin(writer, pin)
254            }
255            _ => {
256                // Other records are ASCII parameter format
257                let params = record.export_to_params();
258                let mut block = Vec::new();
259                write_parameters(&mut block, &params)?;
260                write_block(writer, &block, 0)
261            }
262        }
263    }
264
265    /// Write a binary pin record.
266    fn write_binary_pin<W: Write>(&self, writer: &mut W, pin: &SchPin) -> Result<()> {
267        let mut data = Vec::new();
268
269        // Record type (2 = pin)
270        data.write_i32::<LittleEndian>(2)?;
271
272        // Unknown byte
273        data.write_u8(0)?;
274
275        // Owner part ID
276        data.write_i16::<LittleEndian>(pin.graphical.base.owner_part_id.unwrap_or(1) as i16)?;
277
278        // Owner part display mode
279        data.write_u8(pin.graphical.base.owner_part_display_mode.unwrap_or(0) as u8)?;
280
281        // Symbol edges
282        data.write_u8(pin.symbol_inner_edge.to_int() as u8)?;
283        data.write_u8(pin.symbol_outer_edge.to_int() as u8)?;
284        data.write_u8(pin.symbol_inside.to_int() as u8)?;
285        data.write_u8(pin.symbol_outside.to_int() as u8)?;
286
287        // Description
288        write_pascal_short_string(&mut data, &pin.description)?;
289
290        // Unknown byte
291        data.write_u8(0)?;
292
293        // Electrical type
294        data.write_u8(pin.electrical.to_int() as u8)?;
295
296        // Pin conglomerate flags
297        data.write_u8(pin.pin_conglomerate.bits())?;
298
299        // Pin length, location X, location Y (as integer mils)
300        let (length, _) = coord_to_dxp_frac(pin.pin_length);
301        let (loc_x, _) = coord_to_dxp_frac(pin.graphical.location_x);
302        let (loc_y, _) = coord_to_dxp_frac(pin.graphical.location_y);
303        data.write_i16::<LittleEndian>(length as i16)?;
304        data.write_i16::<LittleEndian>(loc_x as i16)?;
305        data.write_i16::<LittleEndian>(loc_y as i16)?;
306
307        // Color
308        data.write_i32::<LittleEndian>(pin.graphical.color)?;
309
310        // Name, Designator, SwapIdGroup
311        write_pascal_short_string(&mut data, &pin.name)?;
312        write_pascal_short_string(&mut data, &pin.designator)?;
313        write_pascal_short_string(&mut data, &pin.swap_id_group)?;
314
315        // Part and sequence (combined format: "part|&|sequence")
316        let part_and_sequence = if pin.swap_id_part == 0 && pin.swap_id_sequence.is_empty() {
317            String::new()
318        } else if pin.swap_id_part != 0 {
319            format!("{}|&|{}", pin.swap_id_part, pin.swap_id_sequence)
320        } else {
321            format!("|&|{}", pin.swap_id_sequence)
322        };
323        write_pascal_short_string(&mut data, &part_and_sequence)?;
324
325        // Default value
326        write_pascal_short_string(&mut data, &pin.default_value)?;
327
328        // Write block with flag 0x01 (binary record)
329        write_block(writer, &data, 0x01)
330    }
331
332    /// Get section key from reference name.
333    fn get_section_key(&self, ref_name: &str) -> String {
334        self.section_keys
335            .get(ref_name)
336            .cloned()
337            .unwrap_or_else(|| ref_name.to_string())
338    }
339
340    /// Read section keys stream.
341    fn read_section_keys<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
342        let stream_path = "/SectionKeys";
343        if cf.entry(stream_path).is_err() {
344            return Ok(());
345        }
346
347        let mut stream = cf.open_stream(stream_path).map_err(|e| {
348            AltiumError::Io(std::io::Error::new(
349                std::io::ErrorKind::NotFound,
350                e.to_string(),
351            ))
352        })?;
353
354        let mut data = Vec::new();
355        stream.read_to_end(&mut data)?;
356
357        if data.is_empty() {
358            return Ok(());
359        }
360
361        let mut cursor = Cursor::new(data);
362        let params = read_parameters_block(&mut cursor)?;
363
364        let key_count = match params.get("KEYCOUNT") {
365            Some(v) => v.as_int_or(0),
366            None => 0,
367        };
368
369        for i in 0..key_count {
370            let lib_ref = match params.get(&format!("LIBREF{}", i)) {
371                Some(v) => v.as_str().to_string(),
372                None => String::new(),
373            };
374            let section_key = match params.get(&format!("SECTIONKEY{}", i)) {
375                Some(v) => v.as_str().to_string(),
376                None => String::new(),
377            };
378
379            if !lib_ref.is_empty() && !section_key.is_empty() {
380                self.section_keys.insert(lib_ref, section_key);
381            }
382        }
383
384        Ok(())
385    }
386
387    /// Read file header to get component list.
388    fn read_file_header<R: Read + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<Vec<String>> {
389        let stream_path = "/FileHeader";
390        let mut stream = cf.open_stream(stream_path).map_err(|e| {
391            AltiumError::Io(std::io::Error::new(
392                std::io::ErrorKind::NotFound,
393                e.to_string(),
394            ))
395        })?;
396
397        let mut data = Vec::new();
398        stream.read_to_end(&mut data)?;
399
400        if data.is_empty() {
401            return Ok(Vec::new());
402        }
403
404        let mut cursor = Cursor::new(&data);
405        let params = read_parameters_block(&mut cursor)?;
406
407        let mut ref_names = Vec::new();
408
409        // Check if we're at end of stream (use params-based component list)
410        if cursor.position() as usize >= data.len() {
411            let comp_count = match params.get("COMPCOUNT") {
412                Some(v) => v.as_int_or(0),
413                None => 0,
414            };
415            for i in 0..comp_count {
416                if let Some(name) = params.get(&format!("LIBREF{}", i)) {
417                    ref_names.push(name.as_str().to_string());
418                }
419            }
420        } else {
421            // Read binary component list
422            use byteorder::{LittleEndian, ReadBytesExt};
423            let count = cursor.read_u32::<LittleEndian>()?;
424            for _ in 0..count {
425                let name = read_string_block(&mut cursor)?;
426                ref_names.push(name);
427            }
428        }
429
430        Ok(ref_names)
431    }
432
433    /// Read a component from its storage.
434    fn read_component<R: Read + Seek>(
435        &self,
436        cf: &mut CompoundFile<R>,
437        section_key: &str,
438    ) -> Result<SchLibComponent> {
439        let stream_path = format!("/{}/Data", section_key);
440        let mut stream = cf.open_stream(&stream_path).map_err(|e| {
441            AltiumError::Io(std::io::Error::new(
442                std::io::ErrorKind::NotFound,
443                format!("Component stream not found: {} - {}", stream_path, e),
444            ))
445        })?;
446
447        let mut data = Vec::new();
448        stream.read_to_end(&mut data)?;
449
450        if data.is_empty() {
451            return Err(AltiumError::Parse("Empty component data".to_string()));
452        }
453
454        let mut cursor = Cursor::new(&data);
455        let mut primitives = Vec::new();
456
457        // Read all primitives
458        while (cursor.position() as usize) < data.len() {
459            match self.read_record(&mut cursor) {
460                Ok(record) => primitives.push(record),
461                Err(_) => break, // Stop on error
462            }
463        }
464
465        // First primitive should be the component
466        let component = match primitives.first() {
467            Some(SchRecord::Component(c)) => c.clone(),
468            _ => {
469                return Err(AltiumError::Parse(
470                    "First record is not a component".to_string(),
471                ));
472            }
473        };
474
475        Ok(SchLibComponent {
476            component,
477            primitives,
478        })
479    }
480
481    /// Read a single record from the stream.
482    fn read_record<R: Read>(&self, reader: &mut R) -> Result<SchRecord> {
483        use byteorder::{LittleEndian, ReadBytesExt};
484
485        let size = reader.read_i32::<LittleEndian>()?;
486        let is_binary = (size as u32 & !SIZE_FLAG_MASK) != 0;
487        let clean_size = (size & SIZE_FLAG_MASK as i32) as usize;
488
489        if clean_size == 0 {
490            return Err(AltiumError::Parse("Empty record".to_string()));
491        }
492
493        let mut buffer = vec![0u8; clean_size];
494        reader.read_exact(&mut buffer)?;
495
496        if is_binary {
497            // Binary pin record
498            let mut cursor = Cursor::new(buffer);
499            self.read_binary_pin(&mut cursor)
500        } else {
501            // ASCII parameter record
502            let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
503            let param_str = decode_windows_1252(&buffer[..end]);
504            let params = ParameterCollection::from_string(&param_str);
505            SchRecord::from_params(&params)
506        }
507    }
508
509    /// Read a binary pin record.
510    ///
511    /// Pin records in SchLib files are stored in a compact binary format rather than
512    /// the typical ASCII parameter format used by other records.
513    fn read_binary_pin<R: Read>(&self, reader: &mut R) -> Result<SchRecord> {
514        let record_type = reader.read_i32::<LittleEndian>()?;
515        if record_type != 2 {
516            return Err(AltiumError::Parse(format!(
517                "Expected pin record type 2, got {}",
518                record_type
519            )));
520        }
521
522        let _unknown1 = reader.read_u8()?; // Unknown byte
523        let owner_part_id = reader.read_i16::<LittleEndian>()?;
524        let owner_part_display_mode = reader.read_u8()?;
525        let symbol_inner_edge = PinSymbol::from_int(reader.read_u8()? as i32);
526        let symbol_outer_edge = PinSymbol::from_int(reader.read_u8()? as i32);
527        let symbol_inside = PinSymbol::from_int(reader.read_u8()? as i32);
528        let symbol_outside = PinSymbol::from_int(reader.read_u8()? as i32);
529        let description = read_pascal_short_string(reader)?;
530        let _unknown2 = reader.read_u8()?; // Unknown byte
531        let electrical = PinElectricalType::from_int(reader.read_u8()? as i32);
532        let pin_conglomerate = PinConglomerateFlags::from_int(reader.read_u8()? as i32);
533        let pin_length_int = reader.read_i16::<LittleEndian>()? as i32;
534        let location_x_int = reader.read_i16::<LittleEndian>()? as i32;
535        let location_y_int = reader.read_i16::<LittleEndian>()? as i32;
536        let color = reader.read_i32::<LittleEndian>()?;
537        let name = read_pascal_short_string(reader)?;
538        let designator = read_pascal_short_string(reader)?;
539        let swap_id_group = read_pascal_short_string(reader)?;
540        let part_and_sequence = read_pascal_short_string(reader)?;
541        let default_value = read_pascal_short_string(reader)?;
542
543        // Parse the swap ID part and sequence from the combined string
544        let (swap_id_part, swap_id_sequence) = if !part_and_sequence.is_empty() {
545            let parts: Vec<&str> = part_and_sequence.split('|').collect();
546            if parts.len() == 3 {
547                (parts[0].parse().unwrap_or(0), parts[2].to_string())
548            } else {
549                (0, String::new())
550            }
551        } else {
552            (0, String::new())
553        };
554
555        // Convert integer coordinates to raw coord values (multiply by 10000)
556        // In binary format, coordinates are stored as integer mils without fractional part
557        let pin_length = dxp_frac_to_coord(pin_length_int, 0);
558        let location_x = dxp_frac_to_coord(location_x_int, 0);
559        let location_y = dxp_frac_to_coord(location_y_int, 0);
560
561        let mut graphical = SchGraphicalBase::default();
562        graphical.base.owner_part_id = Some(owner_part_id as i32);
563        graphical.base.owner_part_display_mode = Some(owner_part_display_mode as i32);
564        graphical.location_x = location_x;
565        graphical.location_y = location_y;
566        graphical.color = color;
567
568        let pin = SchPin {
569            graphical,
570            symbol_inner_edge,
571            symbol_outer_edge,
572            symbol_inside,
573            symbol_outside,
574            description,
575            electrical,
576            pin_conglomerate,
577            pin_length,
578            name,
579            designator,
580            swap_id_group,
581            swap_id_part,
582            swap_id_sequence,
583            default_value,
584            ..Default::default()
585        };
586
587        Ok(SchRecord::Pin(pin))
588    }
589
590    /// Get the number of components.
591    pub fn component_count(&self) -> usize {
592        self.components.len()
593    }
594
595    /// Iterate over components.
596    pub fn iter(&self) -> impl Iterator<Item = &SchLibComponent> {
597        self.components.iter()
598    }
599}
600
601impl SchLibComponent {
602    /// Get the component name (LIBREFERENCE).
603    pub fn name(&self) -> &str {
604        &self.component.lib_reference
605    }
606
607    /// Get the component description.
608    pub fn description(&self) -> &str {
609        &self.component.component_description
610    }
611
612    /// Get the number of pins.
613    pub fn pin_count(&self) -> usize {
614        self.primitives
615            .iter()
616            .filter(|r| matches!(r, SchRecord::Pin(_)))
617            .count()
618    }
619
620    /// Get total primitive count.
621    pub fn primitive_count(&self) -> usize {
622        self.primitives.len()
623    }
624}
625
626// DumpTree implementations
627use crate::dump::{DumpTree, TreeBuilder};
628
629impl DumpTree for SchLib {
630    fn dump(&self, tree: &mut TreeBuilder) {
631        tree.root(&format!("SchLib ({} components)", self.components.len()));
632
633        for (i, comp) in self.components.iter().enumerate() {
634            tree.push(i < self.components.len() - 1);
635            comp.dump(tree);
636            tree.pop();
637        }
638    }
639}
640
641impl DumpTree for SchLibComponent {
642    fn dump(&self, tree: &mut TreeBuilder) {
643        tree.begin_node(&format!("Symbol: {}", self.component.lib_reference));
644        tree.push(true);
645
646        // Metadata section
647        tree.push(self.primitives.len() > 1);
648        let mut meta_props = vec![];
649        if !self.component.component_description.is_empty() {
650            meta_props.push(("description", self.component.component_description.clone()));
651        }
652        meta_props.push(("parts", format!("{}", self.component.part_count)));
653        meta_props.push(("pins", format!("{}", self.pin_count())));
654        meta_props.push(("primitives", format!("{}", self.primitive_count())));
655        tree.add_leaf("Info", &meta_props);
656        tree.pop();
657
658        // Primitives section (skip first which is the component itself)
659        let child_primitives: Vec<_> = self.primitives.iter().skip(1).collect();
660        if !child_primitives.is_empty() {
661            tree.push(false);
662            tree.begin_node(&format!("Primitives ({})", child_primitives.len()));
663            for (i, prim) in child_primitives.iter().enumerate() {
664                tree.push(i < child_primitives.len() - 1);
665                prim.dump(tree);
666                tree.pop();
667            }
668            tree.pop();
669        }
670
671        tree.pop();
672    }
673}