use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use cfb::CompoundFile;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Cursor, Read, Seek, Write};
use std::path::Path;
use crate::error::{AltiumError, Result};
use crate::format::SIZE_FLAG_MASK;
use crate::io::reader::{
decode_windows_1252, read_parameters_block, read_pascal_short_string, read_string_block,
};
use crate::io::writer::{
write_block, write_parameters, write_pascal_short_string, write_string_block,
};
use crate::records::sch::{
PinConglomerateFlags, PinElectricalType, PinSymbol, SchComponent, SchGraphicalBase, SchPin,
SchRecord, coord_to_dxp_frac, dxp_frac_to_coord,
};
use crate::types::ParameterCollection;
#[derive(Debug, Default)]
pub struct SchLib {
section_keys: HashMap<String, String>,
pub components: Vec<SchLibComponent>,
}
#[derive(Debug)]
pub struct SchLibComponent {
pub component: SchComponent,
pub primitives: Vec<SchRecord>,
}
impl SchLib {
pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
let mut schlib = SchLib::default();
let mut cf = CompoundFile::open(reader).map_err(|e| {
AltiumError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_string(),
))
})?;
schlib.read_section_keys(&mut cf)?;
let ref_names = schlib.read_file_header(&mut cf)?;
for ref_name in ref_names.iter() {
let section_key = schlib.get_section_key(ref_name);
if let Ok(component) = schlib.read_component(&mut cf, §ion_key) {
schlib.components.push(component);
}
}
Ok(schlib)
}
pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path)?;
Self::open(file)
}
pub fn save<W: Read + Write + Seek>(&self, writer: W) -> Result<()> {
let mut cf = CompoundFile::create(writer)
.map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
self.write_storage(&mut cf)?;
self.write_file_header(&mut cf)?;
self.write_section_keys(&mut cf)?;
for comp in &self.components {
self.write_component(&mut cf, comp)?;
}
cf.flush()
.map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
Ok(())
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let file = File::create(path)?;
self.save(file)
}
fn write_storage<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
let mut data = Vec::new();
let header = "|HEADER=Icon storage\0";
let header_bytes = header.as_bytes();
data.write_i32::<LittleEndian>(header_bytes.len() as i32)?;
data.write_all(header_bytes)?;
let stream = cf
.create_stream("/Storage")
.map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
let mut stream = stream;
stream.write_all(&data)?;
Ok(())
}
fn write_file_header<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
let mut data = Vec::new();
let mut header_params = ParameterCollection::new();
header_params.add(
"HEADER",
"Protel for Windows - Schematic Library Editor Binary File Version 5.0",
);
header_params.add_int("WEIGHT", self.components.len() as i32);
let mut header_block = Vec::new();
write_parameters(&mut header_block, &header_params)?;
write_block(&mut data, &header_block, 0)?;
data.write_i32::<LittleEndian>(self.components.len() as i32)?;
for comp in &self.components {
write_string_block(&mut data, &comp.component.lib_reference)?;
}
let stream = cf
.create_stream("/FileHeader")
.map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
let mut stream = stream;
stream.write_all(&data)?;
Ok(())
}
fn write_section_keys<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
let components_needing_keys: Vec<_> = self
.components
.iter()
.filter(|c| Self::needs_section_key(&c.component.lib_reference))
.collect();
if components_needing_keys.is_empty() {
return Ok(());
}
let mut data = Vec::new();
let mut params = ParameterCollection::new();
params.add_int("KEYCOUNT", components_needing_keys.len() as i32);
for (i, comp) in components_needing_keys.iter().enumerate() {
let lib_ref = &comp.component.lib_reference;
let section_key = Self::get_section_key_for(lib_ref);
params.add(&format!("LIBREF{}", i), lib_ref);
params.add(&format!("SECTIONKEY{}", i), §ion_key);
}
let mut block = Vec::new();
write_parameters(&mut block, ¶ms)?;
write_block(&mut data, &block, 0)?;
let stream = cf
.create_stream("/SectionKeys")
.map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
let mut stream = stream;
stream.write_all(&data)?;
Ok(())
}
fn needs_section_key(name: &str) -> bool {
name.len() > 31 || name.contains('/')
}
fn get_section_key_for(name: &str) -> String {
let mut key = name.replace('/', "_");
if key.len() > 31 {
key.truncate(31);
}
key
}
fn write_component<F: Read + Write + Seek>(
&self,
cf: &mut CompoundFile<F>,
comp: &SchLibComponent,
) -> Result<()> {
let section_key = if Self::needs_section_key(&comp.component.lib_reference) {
Self::get_section_key_for(&comp.component.lib_reference)
} else {
comp.component.lib_reference.clone()
};
let storage_path = format!("/{}", section_key);
cf.create_storage(&storage_path)
.map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
let mut data = Vec::new();
for record in &comp.primitives {
self.write_record(&mut data, record)?;
}
let data_path = format!("{}/Data", storage_path);
let stream = cf
.create_stream(&data_path)
.map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
let mut stream = stream;
stream.write_all(&data)?;
Ok(())
}
fn write_record<W: Write>(&self, writer: &mut W, record: &SchRecord) -> Result<()> {
match record {
SchRecord::Pin(pin) => {
self.write_binary_pin(writer, pin)
}
_ => {
let params = record.export_to_params();
let mut block = Vec::new();
write_parameters(&mut block, ¶ms)?;
write_block(writer, &block, 0)
}
}
}
fn write_binary_pin<W: Write>(&self, writer: &mut W, pin: &SchPin) -> Result<()> {
let mut data = Vec::new();
data.write_i32::<LittleEndian>(2)?;
data.write_u8(0)?;
data.write_i16::<LittleEndian>(pin.graphical.base.owner_part_id.unwrap_or(1) as i16)?;
data.write_u8(pin.graphical.base.owner_part_display_mode.unwrap_or(0) as u8)?;
data.write_u8(pin.symbol_inner_edge.to_int() as u8)?;
data.write_u8(pin.symbol_outer_edge.to_int() as u8)?;
data.write_u8(pin.symbol_inside.to_int() as u8)?;
data.write_u8(pin.symbol_outside.to_int() as u8)?;
write_pascal_short_string(&mut data, &pin.description)?;
data.write_u8(0)?;
data.write_u8(pin.electrical.to_int() as u8)?;
data.write_u8(pin.pin_conglomerate.bits())?;
let (length, _) = coord_to_dxp_frac(pin.pin_length);
let (loc_x, _) = coord_to_dxp_frac(pin.graphical.location_x);
let (loc_y, _) = coord_to_dxp_frac(pin.graphical.location_y);
data.write_i16::<LittleEndian>(length as i16)?;
data.write_i16::<LittleEndian>(loc_x as i16)?;
data.write_i16::<LittleEndian>(loc_y as i16)?;
data.write_i32::<LittleEndian>(pin.graphical.color)?;
write_pascal_short_string(&mut data, &pin.name)?;
write_pascal_short_string(&mut data, &pin.designator)?;
write_pascal_short_string(&mut data, &pin.swap_id_group)?;
let part_and_sequence = if pin.swap_id_part == 0 && pin.swap_id_sequence.is_empty() {
String::new()
} else if pin.swap_id_part != 0 {
format!("{}|&|{}", pin.swap_id_part, pin.swap_id_sequence)
} else {
format!("|&|{}", pin.swap_id_sequence)
};
write_pascal_short_string(&mut data, &part_and_sequence)?;
write_pascal_short_string(&mut data, &pin.default_value)?;
write_block(writer, &data, 0x01)
}
fn get_section_key(&self, ref_name: &str) -> String {
self.section_keys
.get(ref_name)
.cloned()
.unwrap_or_else(|| ref_name.to_string())
}
fn read_section_keys<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
let stream_path = "/SectionKeys";
if cf.entry(stream_path).is_err() {
return Ok(());
}
let mut stream = cf.open_stream(stream_path).map_err(|e| {
AltiumError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
e.to_string(),
))
})?;
let mut data = Vec::new();
stream.read_to_end(&mut data)?;
if data.is_empty() {
return Ok(());
}
let mut cursor = Cursor::new(data);
let params = read_parameters_block(&mut cursor)?;
let key_count = match params.get("KEYCOUNT") {
Some(v) => v.as_int_or(0),
None => 0,
};
for i in 0..key_count {
let lib_ref = match params.get(&format!("LIBREF{}", i)) {
Some(v) => v.as_str().to_string(),
None => String::new(),
};
let section_key = match params.get(&format!("SECTIONKEY{}", i)) {
Some(v) => v.as_str().to_string(),
None => String::new(),
};
if !lib_ref.is_empty() && !section_key.is_empty() {
self.section_keys.insert(lib_ref, section_key);
}
}
Ok(())
}
fn read_file_header<R: Read + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<Vec<String>> {
let stream_path = "/FileHeader";
let mut stream = cf.open_stream(stream_path).map_err(|e| {
AltiumError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
e.to_string(),
))
})?;
let mut data = Vec::new();
stream.read_to_end(&mut data)?;
if data.is_empty() {
return Ok(Vec::new());
}
let mut cursor = Cursor::new(&data);
let params = read_parameters_block(&mut cursor)?;
let mut ref_names = Vec::new();
if cursor.position() as usize >= data.len() {
let comp_count = match params.get("COMPCOUNT") {
Some(v) => v.as_int_or(0),
None => 0,
};
for i in 0..comp_count {
if let Some(name) = params.get(&format!("LIBREF{}", i)) {
ref_names.push(name.as_str().to_string());
}
}
} else {
use byteorder::{LittleEndian, ReadBytesExt};
let count = cursor.read_u32::<LittleEndian>()?;
for _ in 0..count {
let name = read_string_block(&mut cursor)?;
ref_names.push(name);
}
}
Ok(ref_names)
}
fn read_component<R: Read + Seek>(
&self,
cf: &mut CompoundFile<R>,
section_key: &str,
) -> Result<SchLibComponent> {
let stream_path = format!("/{}/Data", section_key);
let mut stream = cf.open_stream(&stream_path).map_err(|e| {
AltiumError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Component stream not found: {} - {}", stream_path, e),
))
})?;
let mut data = Vec::new();
stream.read_to_end(&mut data)?;
if data.is_empty() {
return Err(AltiumError::Parse("Empty component data".to_string()));
}
let mut cursor = Cursor::new(&data);
let mut primitives = Vec::new();
while (cursor.position() as usize) < data.len() {
match self.read_record(&mut cursor) {
Ok(record) => primitives.push(record),
Err(_) => break, }
}
let component = match primitives.first() {
Some(SchRecord::Component(c)) => c.clone(),
_ => {
return Err(AltiumError::Parse(
"First record is not a component".to_string(),
));
}
};
Ok(SchLibComponent {
component,
primitives,
})
}
fn read_record<R: Read>(&self, reader: &mut R) -> Result<SchRecord> {
use byteorder::{LittleEndian, ReadBytesExt};
let size = reader.read_i32::<LittleEndian>()?;
let is_binary = (size as u32 & !SIZE_FLAG_MASK) != 0;
let clean_size = (size & SIZE_FLAG_MASK as i32) as usize;
if clean_size == 0 {
return Err(AltiumError::Parse("Empty record".to_string()));
}
let mut buffer = vec![0u8; clean_size];
reader.read_exact(&mut buffer)?;
if is_binary {
let mut cursor = Cursor::new(buffer);
self.read_binary_pin(&mut cursor)
} else {
let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
let param_str = decode_windows_1252(&buffer[..end]);
let params = ParameterCollection::from_string(¶m_str);
SchRecord::from_params(¶ms)
}
}
fn read_binary_pin<R: Read>(&self, reader: &mut R) -> Result<SchRecord> {
let record_type = reader.read_i32::<LittleEndian>()?;
if record_type != 2 {
return Err(AltiumError::Parse(format!(
"Expected pin record type 2, got {}",
record_type
)));
}
let _unknown1 = reader.read_u8()?; let owner_part_id = reader.read_i16::<LittleEndian>()?;
let owner_part_display_mode = reader.read_u8()?;
let symbol_inner_edge = PinSymbol::from_int(reader.read_u8()? as i32);
let symbol_outer_edge = PinSymbol::from_int(reader.read_u8()? as i32);
let symbol_inside = PinSymbol::from_int(reader.read_u8()? as i32);
let symbol_outside = PinSymbol::from_int(reader.read_u8()? as i32);
let description = read_pascal_short_string(reader)?;
let _unknown2 = reader.read_u8()?; let electrical = PinElectricalType::from_int(reader.read_u8()? as i32);
let pin_conglomerate = PinConglomerateFlags::from_int(reader.read_u8()? as i32);
let pin_length_int = reader.read_i16::<LittleEndian>()? as i32;
let location_x_int = reader.read_i16::<LittleEndian>()? as i32;
let location_y_int = reader.read_i16::<LittleEndian>()? as i32;
let color = reader.read_i32::<LittleEndian>()?;
let name = read_pascal_short_string(reader)?;
let designator = read_pascal_short_string(reader)?;
let swap_id_group = read_pascal_short_string(reader)?;
let part_and_sequence = read_pascal_short_string(reader)?;
let default_value = read_pascal_short_string(reader)?;
let (swap_id_part, swap_id_sequence) = if !part_and_sequence.is_empty() {
let parts: Vec<&str> = part_and_sequence.split('|').collect();
if parts.len() == 3 {
(parts[0].parse().unwrap_or(0), parts[2].to_string())
} else {
(0, String::new())
}
} else {
(0, String::new())
};
let pin_length = dxp_frac_to_coord(pin_length_int, 0);
let location_x = dxp_frac_to_coord(location_x_int, 0);
let location_y = dxp_frac_to_coord(location_y_int, 0);
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(owner_part_id as i32);
graphical.base.owner_part_display_mode = Some(owner_part_display_mode as i32);
graphical.location_x = location_x;
graphical.location_y = location_y;
graphical.color = color;
let pin = SchPin {
graphical,
symbol_inner_edge,
symbol_outer_edge,
symbol_inside,
symbol_outside,
description,
electrical,
pin_conglomerate,
pin_length,
name,
designator,
swap_id_group,
swap_id_part,
swap_id_sequence,
default_value,
..Default::default()
};
Ok(SchRecord::Pin(pin))
}
pub fn component_count(&self) -> usize {
self.components.len()
}
pub fn iter(&self) -> impl Iterator<Item = &SchLibComponent> {
self.components.iter()
}
}
impl SchLibComponent {
pub fn name(&self) -> &str {
&self.component.lib_reference
}
pub fn description(&self) -> &str {
&self.component.component_description
}
pub fn pin_count(&self) -> usize {
self.primitives
.iter()
.filter(|r| matches!(r, SchRecord::Pin(_)))
.count()
}
pub fn primitive_count(&self) -> usize {
self.primitives.len()
}
}
use crate::dump::{DumpTree, TreeBuilder};
impl DumpTree for SchLib {
fn dump(&self, tree: &mut TreeBuilder) {
tree.root(&format!("SchLib ({} components)", self.components.len()));
for (i, comp) in self.components.iter().enumerate() {
tree.push(i < self.components.len() - 1);
comp.dump(tree);
tree.pop();
}
}
}
impl DumpTree for SchLibComponent {
fn dump(&self, tree: &mut TreeBuilder) {
tree.begin_node(&format!("Symbol: {}", self.component.lib_reference));
tree.push(true);
tree.push(self.primitives.len() > 1);
let mut meta_props = vec![];
if !self.component.component_description.is_empty() {
meta_props.push(("description", self.component.component_description.clone()));
}
meta_props.push(("parts", format!("{}", self.component.part_count)));
meta_props.push(("pins", format!("{}", self.pin_count())));
meta_props.push(("primitives", format!("{}", self.primitive_count())));
tree.add_leaf("Info", &meta_props);
tree.pop();
let child_primitives: Vec<_> = self.primitives.iter().skip(1).collect();
if !child_primitives.is_empty() {
tree.push(false);
tree.begin_node(&format!("Primitives ({})", child_primitives.len()));
for (i, prim) in child_primitives.iter().enumerate() {
tree.push(i < child_primitives.len() - 1);
prim.dump(tree);
tree.pop();
}
tree.pop();
}
tree.pop();
}
}