use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use cfb::CompoundFile;
use std::fs::File;
use std::io::{Cursor, Read, Seek, Write};
use std::path::Path;
use crate::dump::{DumpTree, TreeBuilder};
use crate::error::{AltiumError, Result};
use crate::format::SIZE_FLAG_MASK;
use crate::io::reader::{decode_windows_1252, read_parameters_block};
use crate::io::writer::{write_block, write_parameters};
use crate::records::sch::{SchPrimitive, SchRecord, SchSheetHeader};
use crate::types::ParameterCollection;
#[derive(Debug, Default)]
pub struct SchDoc {
pub primitives: Vec<SchRecord>,
pub document_name: Option<String>,
}
impl SchDoc {
pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
let mut schdoc = SchDoc::default();
let mut cf = CompoundFile::open(reader).map_err(|e| {
AltiumError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_string(),
))
})?;
schdoc.read_file_header(&mut cf)?;
Ok(schdoc)
}
pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path_ref = path.as_ref();
let file = File::open(path_ref)?;
let mut doc = Self::open(file)?;
if let Some(file_stem) = path_ref.file_stem() {
if let Some(name) = file_stem.to_str() {
doc.document_name = Some(name.to_string());
}
}
Ok(doc)
}
fn read_file_header<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
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(());
}
let mut cursor = Cursor::new(&data);
let header_params = read_parameters_block(&mut cursor)?;
let _header = header_params.get("HEADER").map(|v| v.as_str().to_string());
let _weight = header_params
.get("WEIGHT")
.map(|v| v.as_int_or(0))
.unwrap_or(0);
while (cursor.position() as usize) < data.len() {
match self.read_record(&mut cursor) {
Ok(record) => self.primitives.push(record),
Err(e) => {
log::warn!(
"Failed to read record at position {}: {}, skipping remaining records",
cursor.position(),
e
);
break;
}
}
}
Ok(())
}
fn read_record<R: Read>(&self, reader: &mut R) -> Result<SchRecord> {
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 {
Err(AltiumError::Parse(
"Binary records not supported in SchDoc".to_string(),
))
} 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)
}
}
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_additional(&mut cf)?;
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 Capture Binary File Version 5.0",
);
header_params.add_int("WEIGHT", self.primitives.len() as i32);
let mut header_block = Vec::new();
write_parameters(&mut header_block, &header_params)?;
write_block(&mut data, &header_block, 0)?;
for record in &self.primitives {
self.write_record(&mut data, record)?;
}
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_additional<F: Read + Write + Seek>(&self, cf: &mut CompoundFile<F>) -> Result<()> {
let mut data = Vec::new();
let mut params = ParameterCollection::new();
params.add(
"HEADER",
"Protel for Windows - Schematic Capture Binary File Version 5.0",
);
let mut block = Vec::new();
write_parameters(&mut block, ¶ms)?;
write_block(&mut data, &block, 0)?;
let stream = cf
.create_stream("/Additional")
.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<()> {
let params = record.export_to_params();
let mut block = Vec::new();
write_parameters(&mut block, ¶ms)?;
write_block(writer, &block, 0)
}
pub fn sheet_header(&self) -> Option<&SchSheetHeader> {
self.primitives.iter().find_map(|r| {
if let SchRecord::SheetHeader(h) = r {
Some(h)
} else {
None
}
})
}
pub fn components(&self) -> impl Iterator<Item = &crate::records::sch::SchComponent> {
self.primitives.iter().filter_map(|r| {
if let SchRecord::Component(c) = r {
Some(c)
} else {
None
}
})
}
pub fn wires(&self) -> impl Iterator<Item = &crate::records::sch::SchWire> {
self.primitives.iter().filter_map(|r| {
if let SchRecord::Wire(w) = r {
Some(w)
} else {
None
}
})
}
pub fn primitive_count(&self) -> usize {
self.primitives.len()
}
}
impl SchRecord {
pub fn export_to_params(&self) -> ParameterCollection {
match self {
SchRecord::Component(r) => r.export_to_params(),
SchRecord::Pin(r) => r.export_to_params(),
SchRecord::Symbol(r) => r.export_to_params(),
SchRecord::Label(r) => r.export_to_params(),
SchRecord::Bezier(r) => r.export_to_params(),
SchRecord::Polyline(r) => r.export_to_params(),
SchRecord::Polygon(r) => r.export_to_params(),
SchRecord::Ellipse(r) => r.export_to_params(),
SchRecord::Pie(r) => r.export_to_params(),
SchRecord::EllipticalArc(r) => r.export_to_params(),
SchRecord::Arc(r) => r.export_to_params(),
SchRecord::Line(r) => r.export_to_params(),
SchRecord::Rectangle(r) => r.export_to_params(),
SchRecord::PowerObject(r) => r.export_to_params(),
SchRecord::Port(r) => r.export_to_params(),
SchRecord::NoErc(r) => r.export_to_params(),
SchRecord::NetLabel(r) => r.export_to_params(),
SchRecord::Bus(r) => r.export_to_params(),
SchRecord::Wire(r) => r.export_to_params(),
SchRecord::TextFrame(r) => r.export_to_params(),
SchRecord::TextFrameVariant(r) => r.export_to_params(),
SchRecord::Junction(r) => r.export_to_params(),
SchRecord::Image(r) => r.export_to_params(),
SchRecord::SheetHeader(r) => r.export_to_params(),
SchRecord::Designator(r) => r.export_to_params(),
SchRecord::BusEntry(r) => r.export_to_params(),
SchRecord::Parameter(r) => r.export_to_params(),
SchRecord::WarningSign(r) => r.export_to_params(),
SchRecord::ImplementationList(r) => r.export_to_params(),
SchRecord::Implementation(r) => r.export_to_params(),
SchRecord::MapDefinerList(r) => r.export_to_params(),
SchRecord::MapDefiner(r) => r.export_to_params(),
SchRecord::ImplementationParameters(r) => r.export_to_params(),
SchRecord::Unknown { record_id, params } => {
let mut p = params.clone();
p.add_int("RECORD", *record_id);
p
}
}
}
}
impl DumpTree for SchDoc {
fn dump(&self, tree: &mut TreeBuilder) {
tree.root(&format!("SchDoc ({} primitives)", self.primitives.len()));
let mut component_count = 0;
let mut wire_count = 0;
let mut other_count = 0;
for prim in &self.primitives {
match prim {
SchRecord::Component(_) => component_count += 1,
SchRecord::Wire(_) => wire_count += 1,
_ => other_count += 1,
}
}
tree.push(true);
tree.add_leaf(
"Summary",
&[
("components", format!("{}", component_count)),
("wires", format!("{}", wire_count)),
("other", format!("{}", other_count)),
],
);
tree.pop();
tree.push(false);
tree.begin_node(&format!("Primitives ({})", self.primitives.len()));
for (i, prim) in self.primitives.iter().enumerate() {
tree.push(i < self.primitives.len() - 1);
prim.dump(tree);
tree.pop();
}
tree.pop();
}
}