Skip to main content

altium_format/io/
pcblib.rs

1//! PcbLib reader/writer for Altium PCB footprint 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::io::reader::{
12    read_block, read_parameters_block, read_pascal_short_string, read_pascal_string,
13    read_string_block,
14};
15use crate::io::writer::{
16    write_block, write_parameters, write_pascal_short_string, write_string_block,
17};
18use crate::records::pcb::{
19    PcbArc, PcbComponent, PcbComponentBody, PcbFill, PcbObjectId, PcbPad, PcbRecord, PcbRegion,
20    PcbText, PcbTrack, PcbVia,
21};
22use crate::traits::{FromBinary, ToBinary};
23use crate::types::ParameterCollection;
24
25/// A PCB footprint library containing components.
26#[derive(Debug, Default)]
27pub struct PcbLib {
28    /// Section keys mapping pattern names to storage paths.
29    section_keys: HashMap<String, String>,
30    /// Unique ID of the library.
31    pub unique_id: String,
32    /// Components (footprints) in the library.
33    pub components: Vec<PcbComponent>,
34}
35
36impl PcbLib {
37    /// Open and read a PcbLib file.
38    pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
39        let mut pcblib = PcbLib::default();
40        let mut cf = CompoundFile::open(reader).map_err(|e| {
41            AltiumError::Io(std::io::Error::new(
42                std::io::ErrorKind::InvalidData,
43                e.to_string(),
44            ))
45        })?;
46        // Read file header
47        pcblib.read_file_header(&mut cf)?;
48        // Read section keys
49        pcblib.read_section_keys(&mut cf)?;
50        // Read library data (component list)
51        pcblib.read_library(&mut cf)?;
52        Ok(pcblib)
53    }
54
55    /// Open and read a PcbLib file from a path.
56    pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
57        let file = File::open(path)?;
58        Self::open(file)
59    }
60
61    /// Save the PcbLib to a file.
62    pub fn save<W: Read + Write + Seek>(&self, writer: W) -> Result<()> {
63        let mut cf = CompoundFile::create(writer)
64            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
65
66        // Write FileHeader
67        self.write_file_header(&mut cf)?;
68
69        // Write SectionKeys if needed
70        self.write_section_keys(&mut cf)?;
71
72        // Write Library storage
73        self.write_library(&mut cf)?;
74
75        cf.flush()
76            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
77
78        Ok(())
79    }
80
81    /// Save the PcbLib to a file path.
82    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
83        let file = File::create(path)?;
84        self.save(file)
85    }
86
87    /// Write the FileHeader stream.
88    fn write_file_header<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
89        let mut data = Vec::new();
90
91        // Version text block
92        let version = "PCB 6.0 Binary Library File";
93        data.write_i32::<LittleEndian>(version.len() as i32)?;
94        write_pascal_short_string(&mut data, version)?;
95
96        // Additional required fields (observed in Altium files)
97        // Float value (appears to be version-related)
98        data.write_f64::<LittleEndian>(5.0)?;
99
100        // Token string "DVLTOKCO" (required marker)
101        write_pascal_short_string(&mut data, "DVLTOKCO")?;
102
103        let stream = cf
104            .create_stream("/FileHeader")
105            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
106
107        let mut stream = stream;
108        stream.write_all(&data)?;
109
110        Ok(())
111    }
112
113    /// Write the SectionKeys stream.
114    /// Maps pattern names to PCBComponent_N storage names.
115    fn write_section_keys<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
116        // Always write section keys to map pattern names to PCBComponent_N
117        if self.components.is_empty() {
118            return Ok(());
119        }
120
121        let mut data = Vec::new();
122        data.write_i32::<LittleEndian>(self.components.len() as i32)?;
123
124        for (i, comp) in self.components.iter().enumerate() {
125            let storage_name = format!("PCBComponent_{}", i + 1);
126            crate::io::writer::write_pascal_string(&mut data, &comp.pattern)?;
127            write_string_block(&mut data, &storage_name)?;
128        }
129
130        let stream = cf
131            .create_stream("/SectionKeys")
132            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
133
134        let mut stream = stream;
135        stream.write_all(&data)?;
136
137        Ok(())
138    }
139
140    /// Write the Library storage with all required sub-storages.
141    fn write_library<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
142        // Create Library storage
143        cf.create_storage("/Library")
144            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
145
146        // Write Library/Header
147        self.write_library_header(cf)?;
148
149        // Write Library/Data with proper parameters
150        self.write_library_data(cf)?;
151
152        // Write required sub-storages
153        self.write_library_substorages(cf)?;
154
155        // Write FileVersionInfo
156        self.write_file_version_info(cf)?;
157
158        // Write each footprint using PCBComponent_N naming
159        for (i, comp) in self.components.iter().enumerate() {
160            let storage_name = format!("PCBComponent_{}", i + 1);
161            self.write_footprint(cf, comp, &storage_name)?;
162        }
163
164        Ok(())
165    }
166
167    /// Write Library/Header stream.
168    fn write_library_header<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
169        let mut header_data = Vec::new();
170        header_data.write_u32::<LittleEndian>(1)?; // Record count = 1
171
172        let stream = cf
173            .create_stream("/Library/Header")
174            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
175        let mut stream = stream;
176        stream.write_all(&header_data)?;
177        Ok(())
178    }
179
180    /// Write Library/Data stream with required parameters.
181    fn write_library_data<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
182        let mut data = Vec::new();
183
184        // Build minimal required library parameters
185        let params = Self::build_library_parameters();
186        let mut params_block = Vec::new();
187        write_parameters(&mut params_block, &params)?;
188        write_block(&mut data, &params_block, 0)?;
189
190        // Write footprint count
191        data.write_u32::<LittleEndian>(self.components.len() as u32)?;
192
193        // Write footprint storage names (PCBComponent_N format)
194        for i in 0..self.components.len() {
195            let storage_name = format!("PCBComponent_{}", i + 1);
196            write_string_block(&mut data, &storage_name)?;
197        }
198
199        let stream = cf
200            .create_stream("/Library/Data")
201            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
202        let mut stream = stream;
203        stream.write_all(&data)?;
204        Ok(())
205    }
206
207    /// Build minimal required library parameters for Library/Data.
208    fn build_library_parameters() -> ParameterCollection {
209        let mut params = ParameterCollection::new();
210
211        // Essential parameters that Altium requires
212        params.add("KIND", "Protel_Advanced_PCB_Library");
213        params.add("VERSION", "1.0");
214
215        // Board configuration (minimal required)
216        params.add("BOARDVERSION", "5.01");
217        params.add("VISIBLEGRIDMULTFACTOR", "1.000");
218        params.add("BIGVISIBLEGRIDMULTFACTOR", "5.000");
219        params.add("CURRENT2D3DVIEWSTATE", "2D");
220
221        // Layer settings (minimal)
222        params.add("CFG2D.CURRENTLAYER", "TOP");
223        params.add("CFG2D.SHOWPADNETS", "TRUE");
224        params.add("CFG2D.SHOWPADNUMBERS", "TRUE");
225        params.add("CFG2D.SHOWVIANETS", "TRUE");
226        params.add("CFG2D.SHOWORIGINMARKER", "TRUE");
227        params.add("CFG2D.DISPLAYSPECIALSTRINGS", "FALSE");
228        params.add("CFG2D.SHOWTESTPOINTS", "FALSE");
229        params.add("CFG2D.SHOWSTATUSINFO", "TRUE");
230        params.add("CFG2D.USETRANSPARENTLAYERS", "FALSE");
231        params.add("CFG2D.PLANEDRAWMODE", "2");
232        params.add("CFG2D.DISPLAYNETNAMESONTRACKS", "1");
233        params.add("CFG2D.SINGLELAYERMODESTATE", "3");
234        params.add("CFG2D.ORIGINMARKERCOLOR", "16777215");
235
236        // Toggle layers (all enabled)
237        params.add(
238            "CFG2D.TOGGLELAYERS",
239            "1111111111111111111111111111111111111111111111111111111111111111",
240        );
241
242        // Grid settings
243        params.add("EGENABLED", "TRUE");
244        params.add("EGRANGE", "8mil");
245        params.add("OGSNAPENABLED", "TRUE");
246        params.add("GRIDSNAPENABLED", "TRUE");
247
248        params
249    }
250
251    /// Write required Library sub-storages.
252    fn write_library_substorages<F: Read + Write + Seek>(
253        &self,
254        cf: &mut CompoundFile<F>,
255    ) -> Result<()> {
256        // Create empty storages for Models, Textures, ModelsNoEmbed
257        for storage in &[
258            "/Library/Models",
259            "/Library/Textures",
260            "/Library/ModelsNoEmbed",
261        ] {
262            cf.create_storage(storage)
263                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
264
265            // Each needs Header and Data streams (empty)
266            let header_path = format!("{}/Header", storage);
267            let data_path = format!("{}/Data", storage);
268
269            let mut stream = cf
270                .create_stream(&header_path)
271                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
272            stream.write_u32::<LittleEndian>(0)?;
273
274            let mut stream = cf
275                .create_stream(&data_path)
276                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
277            stream.write_all(&[])?;
278        }
279
280        // EmbeddedFonts stream
281        {
282            let mut stream = cf
283                .create_stream("/Library/EmbeddedFonts")
284                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
285            stream.write_u32::<LittleEndian>(0)?;
286        }
287
288        // PadViaLibrary storage
289        {
290            cf.create_storage("/Library/PadViaLibrary")
291                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
292
293            let mut header = cf
294                .create_stream("/Library/PadViaLibrary/Header")
295                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
296            header.write_u32::<LittleEndian>(1)?;
297
298            let mut params = ParameterCollection::new();
299            params.add(
300                "PADVIALIBRARY.LIBRARYID",
301                "{00000000-0000-0000-0000-000000000000}",
302            );
303            params.add("PADVIALIBRARY.LIBRARYNAME", "<Local>");
304            params.add("PADVIALIBRARY.DISPLAYUNITS", "1");
305            let mut block = Vec::new();
306            write_parameters(&mut block, &params)?;
307            let mut data_buf = Vec::new();
308            write_block(&mut data_buf, &block, 0)?;
309
310            let mut data = cf
311                .create_stream("/Library/PadViaLibrary/Data")
312                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
313            data.write_all(&data_buf)?;
314        }
315
316        // LayerKindMapping storage
317        {
318            cf.create_storage("/Library/LayerKindMapping")
319                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
320
321            let mut header = cf
322                .create_stream("/Library/LayerKindMapping/Header")
323                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
324            header.write_u32::<LittleEndian>(1)?;
325
326            // LayerKindMapping data: version string "1.0" as wide string + padding
327            let mut data_buf = Vec::new();
328            // Block size
329            data_buf.write_u32::<LittleEndian>(8)?;
330            // Wide string "1.0" (UTF-16LE)
331            for c in "1.0\0".encode_utf16() {
332                data_buf.write_u16::<LittleEndian>(c)?;
333            }
334
335            let mut data = cf
336                .create_stream("/Library/LayerKindMapping/Data")
337                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
338            data.write_all(&data_buf)?;
339        }
340
341        // ComponentParamsTOC storage
342        {
343            cf.create_storage("/Library/ComponentParamsTOC")
344                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
345
346            let mut header = cf
347                .create_stream("/Library/ComponentParamsTOC/Header")
348                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
349            header.write_u32::<LittleEndian>(self.components.len() as u32)?;
350
351            // Build TOC entries for each component
352            let mut toc_data = Vec::new();
353            for (i, comp) in self.components.iter().enumerate() {
354                let storage_name = format!("PCBComponent_{}", i + 1);
355                let pad_count = comp
356                    .primitives
357                    .iter()
358                    .filter(|p| matches!(p, PcbRecord::Pad(_)))
359                    .count();
360
361                let mut params = ParameterCollection::new();
362                params.add("Name", &storage_name);
363                params.add("Pad Count", &pad_count.to_string());
364                params.add("Height", "0");
365                params.add("Description", &comp.description);
366
367                let mut block = Vec::new();
368                write_parameters(&mut block, &params)?;
369                write_block(&mut toc_data, &block, 0)?;
370            }
371
372            let mut data = cf
373                .create_stream("/Library/ComponentParamsTOC/Data")
374                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
375            data.write_all(&toc_data)?;
376        }
377
378        Ok(())
379    }
380
381    /// Write FileVersionInfo storage.
382    fn write_file_version_info<F: Read + Write + Seek>(
383        &self,
384        cf: &mut CompoundFile<F>,
385    ) -> Result<()> {
386        cf.create_storage("/FileVersionInfo")
387            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
388
389        let mut header = cf
390            .create_stream("/FileVersionInfo/Header")
391            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
392        header.write_u32::<LittleEndian>(1)?;
393
394        // FileVersionInfo data - minimal version info
395        let mut params = ParameterCollection::new();
396        params.add("VERSIONNUMBER", "1.0");
397        params.add("REVISIONDATE", "2024-01-01");
398
399        let mut block = Vec::new();
400        write_parameters(&mut block, &params)?;
401        let mut data_buf = Vec::new();
402        write_block(&mut data_buf, &block, 0)?;
403
404        let mut data = cf
405            .create_stream("/FileVersionInfo/Data")
406            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
407        data.write_all(&data_buf)?;
408
409        Ok(())
410    }
411
412    /// Write a footprint to its storage using the provided storage name.
413    fn write_footprint<F: Read + Write + Seek>(
414        &self,
415        cf: &mut CompoundFile<F>,
416        comp: &PcbComponent,
417        storage_name: &str,
418    ) -> Result<()> {
419        // Create storage for footprint using PCBComponent_N naming
420        let storage_path = format!("/{}", storage_name);
421        cf.create_storage(&storage_path)
422            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
423
424        // Write Header
425        {
426            let mut header_data = Vec::new();
427            header_data.write_u32::<LittleEndian>(comp.primitives.len() as u32)?;
428
429            let header_path = format!("{}/Header", storage_path);
430            let stream = cf
431                .create_stream(&header_path)
432                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
433            let mut stream = stream;
434            stream.write_all(&header_data)?;
435        }
436
437        // Write Parameters
438        {
439            let mut params_data = Vec::new();
440            let params = comp.export_to_parameters();
441            let mut block = Vec::new();
442            write_parameters(&mut block, &params)?;
443            write_block(&mut params_data, &block, 0)?;
444
445            let params_path = format!("{}/Parameters", storage_path);
446            let stream = cf
447                .create_stream(&params_path)
448                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
449            let mut stream = stream;
450            stream.write_all(&params_data)?;
451        }
452
453        // Write Data
454        {
455            let mut data = Vec::new();
456
457            // Pattern name (the actual footprint name, not storage name)
458            write_string_block(&mut data, &comp.pattern)?;
459
460            // Primitives
461            for record in &comp.primitives {
462                self.write_primitive(&mut data, record)?;
463            }
464
465            let data_path = format!("{}/Data", storage_path);
466            let stream = cf
467                .create_stream(&data_path)
468                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
469            let mut stream = stream;
470            stream.write_all(&data)?;
471        }
472
473        // Write WideStrings stream (required, even if empty)
474        {
475            let mut params = ParameterCollection::new();
476            params.add("COUNT", "0");
477            let mut block = Vec::new();
478            write_parameters(&mut block, &params)?;
479            let mut wide_data = Vec::new();
480            write_block(&mut wide_data, &block, 0)?;
481
482            let wide_path = format!("{}/WideStrings", storage_path);
483            let stream = cf
484                .create_stream(&wide_path)
485                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
486            let mut stream = stream;
487            stream.write_all(&wide_data)?;
488        }
489
490        // Write PrimitiveGuids storage (required)
491        {
492            let guids_path = format!("{}/PrimitiveGuids", storage_path);
493            cf.create_storage(&guids_path)
494                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
495
496            // Header with primitive count
497            let header_path = format!("{}/Header", guids_path);
498            let mut header = cf
499                .create_stream(&header_path)
500                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
501            header.write_u32::<LittleEndian>(comp.primitives.len() as u32)?;
502
503            // Data with empty GUIDs (zeros) for each primitive
504            let data_path = format!("{}/Data", guids_path);
505            let mut data = cf
506                .create_stream(&data_path)
507                .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
508            // Each primitive gets a 16-byte zero GUID
509            for _ in 0..comp.primitives.len() {
510                data.write_all(&[0u8; 16])?;
511            }
512        }
513
514        Ok(())
515    }
516
517    /// Write a single PCB primitive.
518    fn write_primitive<W: Write>(&self, writer: &mut W, record: &PcbRecord) -> Result<()> {
519        // Write object ID byte
520        let object_id = match record {
521            PcbRecord::Arc(_) => 1,
522            PcbRecord::Pad(_) => 2,
523            PcbRecord::Via(_) => 3,
524            PcbRecord::Track(_) => 4,
525            PcbRecord::Text(_) => 5,
526            PcbRecord::Fill(_) => 6,
527            PcbRecord::Region(_) => 11,
528            PcbRecord::ComponentBody(_) => 12,
529            PcbRecord::Polygon(_) => {
530                return Err(AltiumError::Parse(
531                    "Polygons are not supported in PcbLib footprints".to_string(),
532                ));
533            }
534            PcbRecord::Unknown { object_id, .. } => *object_id as u8,
535        };
536        writer.write_u8(object_id)?;
537
538        // Write primitive data based on type
539        match record {
540            PcbRecord::Arc(arc) => {
541                let mut data = Vec::new();
542                arc.write_to(&mut data)?;
543                write_block(writer, &data, 0)?;
544            }
545            PcbRecord::Pad(pad) => {
546                // Pad has a special multi-block format, writes directly
547                pad.write_to(writer)?;
548            }
549            PcbRecord::Via(via) => {
550                let mut data = Vec::new();
551                via.write_to(&mut data)?;
552                write_block(writer, &data, 0)?;
553            }
554            PcbRecord::Track(track) => {
555                let mut data = Vec::new();
556                track.write_to(&mut data)?;
557                write_block(writer, &data, 0)?;
558            }
559            PcbRecord::Text(text) => {
560                // Text has special format: block + ASCII text block
561                let mut data = Vec::new();
562                text.write_to(&mut data)?;
563                write_block(writer, &data, 0)?;
564                write_string_block(writer, &text.text)?;
565            }
566            PcbRecord::Fill(fill) => {
567                let mut data = Vec::new();
568                fill.write_to(&mut data)?;
569                write_block(writer, &data, 0)?;
570            }
571            PcbRecord::Region(region) => {
572                let mut data = Vec::new();
573                region.write_to(&mut data)?;
574                write_block(writer, &data, 0)?;
575            }
576            PcbRecord::ComponentBody(body) => {
577                let mut data = Vec::new();
578                body.write_to(&mut data)?;
579                write_block(writer, &data, 0)?;
580            }
581            PcbRecord::Polygon(_) => {
582                // Already handled above with an error, unreachable
583                unreachable!("Polygons should have errored in object_id match")
584            }
585            PcbRecord::Unknown { raw_data, .. } => {
586                write_block(writer, raw_data, 0)?;
587            }
588        }
589
590        Ok(())
591    }
592
593    /// Get section key from reference name.
594    fn get_section_key(&self, ref_name: &str) -> String {
595        self.section_keys
596            .get(ref_name)
597            .cloned()
598            .unwrap_or_else(|| ref_name.to_string())
599    }
600
601    /// Read file header stream.
602    fn read_file_header<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
603        let stream_path = "/FileHeader";
604        if cf.entry(stream_path).is_err() {
605            return Ok(());
606        }
607
608        let mut stream = cf.open_stream(stream_path).map_err(|e| {
609            AltiumError::Io(std::io::Error::new(
610                std::io::ErrorKind::NotFound,
611                e.to_string(),
612            ))
613        })?;
614
615        let mut data = Vec::new();
616        stream.read_to_end(&mut data)?;
617
618        if data.is_empty() {
619            return Ok(());
620        }
621
622        let mut cursor = Cursor::new(&data);
623
624        // Read version text block (length prefix then Pascal string)
625        let _version_len = cursor.read_i32::<LittleEndian>()?;
626        let _version_text = read_pascal_short_string(&mut cursor)?;
627
628        // Try to read additional fields (optional, vary by Altium version).
629        // These fields may not exist in older file versions, so EOF is acceptable.
630        if (cursor.position() as usize) < data.len() {
631            // First optional field: appears to be a version-related float value as string.
632            // EOF here indicates older file format without extended header.
633            let field1 = read_pascal_short_string(&mut cursor).unwrap_or_default();
634            if !field1.is_empty() {
635                log::trace!("FileHeader optional field 1: {:?}", field1);
636            }
637
638            // Second optional field: appears to be a token/marker string (e.g., "DVLTOKCO").
639            // EOF here indicates file format without this marker.
640            let field2 = read_pascal_short_string(&mut cursor).unwrap_or_default();
641            if !field2.is_empty() {
642                log::trace!("FileHeader optional field 2: {:?}", field2);
643            }
644
645            // Third optional field: unique ID string for the library.
646            // Empty is valid for libraries without assigned unique ID.
647            if let Ok(uid) = read_pascal_short_string(&mut cursor) {
648                self.unique_id = uid;
649            }
650        }
651
652        Ok(())
653    }
654
655    /// Read section keys stream.
656    fn read_section_keys<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
657        let stream_path = "/SectionKeys";
658        if cf.entry(stream_path).is_err() {
659            return Ok(());
660        }
661
662        let mut stream = cf.open_stream(stream_path).map_err(|e| {
663            AltiumError::Io(std::io::Error::new(
664                std::io::ErrorKind::NotFound,
665                e.to_string(),
666            ))
667        })?;
668
669        let mut data = Vec::new();
670        stream.read_to_end(&mut data)?;
671
672        if data.is_empty() {
673            return Ok(());
674        }
675
676        let mut cursor = Cursor::new(&data);
677        let key_count = cursor.read_i32::<LittleEndian>()?;
678
679        for _ in 0..key_count {
680            let lib_ref = read_pascal_string(&mut cursor)?;
681            let section_key = read_string_block(&mut cursor)?;
682
683            if !lib_ref.is_empty() && !section_key.is_empty() {
684                self.section_keys.insert(lib_ref, section_key);
685            }
686        }
687
688        Ok(())
689    }
690
691    /// Read the library storage.
692    fn read_library<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
693        let storage_path = "/Library";
694
695        // Read header to get record count
696        let header_path = format!("{}/Header", storage_path);
697        if cf.entry(&header_path).is_ok() {
698            let mut stream = cf.open_stream(&header_path).map_err(|e| {
699                AltiumError::Io(std::io::Error::new(
700                    std::io::ErrorKind::NotFound,
701                    e.to_string(),
702                ))
703            })?;
704            let _record_count = stream.read_u32::<LittleEndian>()?;
705        }
706
707        // Read data stream
708        let data_path = format!("{}/Data", storage_path);
709        let mut stream = cf.open_stream(&data_path).map_err(|e| {
710            AltiumError::Io(std::io::Error::new(
711                std::io::ErrorKind::NotFound,
712                e.to_string(),
713            ))
714        })?;
715
716        let mut data = Vec::new();
717        stream.read_to_end(&mut data)?;
718
719        if data.is_empty() {
720            return Ok(());
721        }
722
723        let mut cursor = Cursor::new(&data);
724
725        // Read library header parameters
726        let _header_params = read_parameters_block(&mut cursor)?;
727
728        // Read footprint count and list
729        let footprint_count = cursor.read_u32::<LittleEndian>()?;
730        let mut ref_names = Vec::with_capacity(footprint_count as usize);
731
732        for _ in 0..footprint_count {
733            let ref_name = read_string_block(&mut cursor)?;
734            ref_names.push(ref_name);
735        }
736
737        // Read each footprint
738        for ref_name in ref_names {
739            let section_key = self.get_section_key(&ref_name);
740            match self.read_footprint(cf, &section_key) {
741                Ok(component) => {
742                    self.components.push(component);
743                }
744                Err(e) => {
745                    eprintln!("Warning: Failed to read footprint {:?}: {}", ref_name, e);
746                    continue;
747                }
748            }
749        }
750
751        Ok(())
752    }
753
754    /// Read a footprint from its storage.
755    fn read_footprint<R: Read + Seek>(
756        &self,
757        cf: &mut CompoundFile<R>,
758        section_key: &str,
759    ) -> Result<PcbComponent> {
760        // The section_key is the actual storage name and may contain forward slashes
761        // as part of the name (e.g., "C 0805 / 2012"). These are NOT path separators.
762        // However, Altium may convert forward slashes to underscores in CFB storage names.
763        let mut storage_path = format!("/{}", section_key);
764
765        // If the storage doesn't exist, try replacing forward slashes with underscores
766        if cf.entry(&storage_path).is_err() {
767            let section_key_alt = section_key.replace('/', "_");
768            let alt_path = format!("/{}", section_key_alt);
769            if cf.entry(&alt_path).is_ok() {
770                storage_path = alt_path;
771            }
772        }
773
774        // Read header
775        let header_path = format!("{}/Header", storage_path);
776        let _record_count = if cf.entry(&header_path).is_ok() {
777            let mut stream = cf.open_stream(&header_path).map_err(|e| {
778                AltiumError::Io(std::io::Error::new(
779                    std::io::ErrorKind::NotFound,
780                    e.to_string(),
781                ))
782            })?;
783            stream.read_u32::<LittleEndian>()?
784        } else {
785            0
786        };
787
788        let mut component = PcbComponent::default();
789
790        // Read parameters
791        let params_path = format!("{}/Parameters", storage_path);
792        if cf.entry(&params_path).is_ok() {
793            let mut stream = cf.open_stream(&params_path).map_err(|e| {
794                AltiumError::Io(std::io::Error::new(
795                    std::io::ErrorKind::NotFound,
796                    e.to_string(),
797                ))
798            })?;
799            let mut data = Vec::new();
800            stream.read_to_end(&mut data)?;
801
802            if !data.is_empty() {
803                let mut cursor = Cursor::new(&data);
804                let params = read_parameters_block(&mut cursor)?;
805                component.import_from_parameters(&params);
806            }
807        }
808
809        // Read wide strings (for Unicode text)
810        let wide_strings = self.read_wide_strings(cf, &storage_path)?;
811
812        // Read data stream
813        let data_path = format!("{}/Data", storage_path);
814        let mut stream = cf.open_stream(&data_path).map_err(|e| {
815            AltiumError::Io(std::io::Error::new(
816                std::io::ErrorKind::NotFound,
817                format!("Footprint data not found: {} - {}", data_path, e),
818            ))
819        })?;
820
821        let mut data = Vec::new();
822        stream.read_to_end(&mut data)?;
823
824        if data.is_empty() {
825            return Err(AltiumError::Parse("Empty footprint data".to_string()));
826        }
827
828        let mut cursor = Cursor::new(&data);
829
830        // First block is the pattern name (should match component.pattern)
831        let pattern = read_string_block(&mut cursor)?;
832        if component.pattern.is_empty() {
833            component.pattern = pattern;
834        }
835
836        // Read primitives
837        while (cursor.position() as usize) < data.len() {
838            match self.read_primitive(&mut cursor, &wide_strings) {
839                Ok(record) => component.primitives.push(record),
840                Err(_) => break,
841            }
842        }
843
844        Ok(component)
845    }
846
847    /// Read wide strings storage for Unicode text support.
848    fn read_wide_strings<R: Read + Seek>(
849        &self,
850        cf: &mut CompoundFile<R>,
851        storage_path: &str,
852    ) -> Result<Vec<String>> {
853        let wide_path = format!("{}/WideStrings", storage_path);
854        if cf.entry(&wide_path).is_err() {
855            return Ok(Vec::new());
856        }
857
858        let mut stream = cf.open_stream(&wide_path).map_err(|e| {
859            AltiumError::Io(std::io::Error::new(
860                std::io::ErrorKind::NotFound,
861                e.to_string(),
862            ))
863        })?;
864
865        let mut data = Vec::new();
866        stream.read_to_end(&mut data)?;
867
868        if data.is_empty() {
869            return Ok(Vec::new());
870        }
871
872        let mut cursor = Cursor::new(&data);
873        let params = read_parameters_block(&mut cursor)?;
874
875        let mut strings = Vec::new();
876        let count = params.get("COUNT").map(|v| v.as_int_or(0)).unwrap_or(0) as usize;
877
878        for i in 0..count {
879            let key = format!("WIDESTRING{}", i);
880            if let Some(val) = params.get(&key) {
881                strings.push(val.as_str().to_string());
882            } else {
883                strings.push(String::new());
884            }
885        }
886
887        Ok(strings)
888    }
889
890    /// Read a single primitive from the stream.
891    fn read_primitive(
892        &self,
893        cursor: &mut Cursor<&Vec<u8>>,
894        wide_strings: &[String],
895    ) -> Result<PcbRecord> {
896        let object_id = PcbObjectId::from_byte(cursor.read_u8()?);
897
898        match object_id {
899            PcbObjectId::Arc => {
900                let block = read_block(cursor)?;
901                let mut block_cursor = Cursor::new(&block);
902                let arc = <PcbArc as FromBinary>::read_from(&mut block_cursor)?;
903                Ok(PcbRecord::Arc(arc))
904            }
905            PcbObjectId::Pad => {
906                let pad = PcbPad::read_from(cursor)?;
907                Ok(PcbRecord::Pad(Box::new(pad)))
908            }
909            PcbObjectId::Via => {
910                let block = read_block(cursor)?;
911                let mut block_cursor = Cursor::new(&block);
912                let via = <PcbVia as FromBinary>::read_from(&mut block_cursor)?;
913                Ok(PcbRecord::Via(via))
914            }
915            PcbObjectId::Track => {
916                let block = read_block(cursor)?;
917                let mut block_cursor = Cursor::new(&block);
918                let track = <PcbTrack as FromBinary>::read_from(&mut block_cursor)?;
919                Ok(PcbRecord::Track(track))
920            }
921            PcbObjectId::Text => {
922                let block = read_block(cursor)?;
923                let mut block_cursor = Cursor::new(&block);
924                let mut text = <PcbText as FromBinary>::read_from(&mut block_cursor)?;
925
926                // Read ASCII text from separate block
927                let ascii_text = read_string_block(cursor)?;
928
929                // Use wide string if available
930                if text.wide_strings_index >= 0
931                    && (text.wide_strings_index as usize) < wide_strings.len()
932                {
933                    text.text = wide_strings[text.wide_strings_index as usize].clone();
934                } else {
935                    text.text = ascii_text;
936                }
937
938                Ok(PcbRecord::Text(text))
939            }
940            PcbObjectId::Fill => {
941                let block = read_block(cursor)?;
942                let mut block_cursor = Cursor::new(&block);
943                let fill = <PcbFill as FromBinary>::read_from(&mut block_cursor)?;
944                Ok(PcbRecord::Fill(fill))
945            }
946            PcbObjectId::Region => {
947                let block = read_block(cursor)?;
948                let mut block_cursor = Cursor::new(&block);
949                let region = <PcbRegion as FromBinary>::read_from(&mut block_cursor)?;
950                Ok(PcbRecord::Region(region))
951            }
952            PcbObjectId::ComponentBody => {
953                let block = read_block(cursor)?;
954                let mut block_cursor = Cursor::new(&block);
955                let body = <PcbComponentBody as FromBinary>::read_from(&mut block_cursor)?;
956                Ok(PcbRecord::ComponentBody(Box::new(body)))
957            }
958            _ => {
959                // Unknown - skip the block
960                let block = read_block(cursor)?;
961                Ok(PcbRecord::Unknown {
962                    object_id,
963                    raw_data: block,
964                })
965            }
966        }
967    }
968
969    /// Get the number of components.
970    pub fn component_count(&self) -> usize {
971        self.components.len()
972    }
973
974    /// Iterate over components.
975    pub fn iter(&self) -> impl Iterator<Item = &PcbComponent> {
976        self.components.iter()
977    }
978}
979
980// DumpTree implementation
981use crate::dump::{DumpTree, TreeBuilder};
982
983impl DumpTree for PcbLib {
984    fn dump(&self, tree: &mut TreeBuilder) {
985        tree.root(&format!("PcbLib ({} footprints)", self.components.len()));
986
987        for (i, comp) in self.components.iter().enumerate() {
988            tree.push(i < self.components.len() - 1);
989            comp.dump(tree);
990            tree.pop();
991        }
992    }
993}