app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footp
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
//! Altium `.IntLib` (Integrated Library) **encoder** + decoder.
//!
//! An IntLib is a CFBF container that *bundles* a symbol library and a footprint
//! library so the symbol's footprint model always resolves (no external library
//! search path needed). Structure:
//! ```text
//! /SchLib/0.schlib 0x02 marker + zlib(a complete standard .SchLib)
//! /PCBLib/0.pcblib 0x02 marker + zlib(a complete standard .PcbLib)
//! /LibCrossRef.Txt symbol↔footprint cross-reference (framed strings)
//! /Parameters .bin |KEY=VAL parameters (note: 3 spaces in the name)
//! /Version.Txt 00 02 00 00 00
//! ```
//! Because we already encode the inner `.SchLib`/`.PcbLib`, the IntLib writer is
//! just: encode both, zlib-wrap, and assemble the container + crossref.
use crate::model::{Footprint, Symbol};
use crate::{encode_pcblib, encode_schlib};
use anyhow::{Context, Result};
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::io::{Cursor, Read, Write};
/// `0x02` marker byte + zlib-DEFLATE of the library bytes (Altium's scheme).
fn wrap(bytes: &[u8]) -> Result<Vec<u8>> {
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(bytes)?;
let z = enc.finish()?;
let mut out = vec![0x02u8];
out.extend_from_slice(&z);
Ok(out)
}
/// `[u32 len+1][u8 len][bytes]` — a LibCrossRef string block.
fn sblock(s: &str) -> Vec<u8> {
let b = s.as_bytes();
let mut out = ((b.len() + 1) as u32).to_le_bytes().to_vec();
out.push(b.len() as u8);
out.extend_from_slice(b);
out
}
fn lib_cross_ref(comp: &str, descr: &str, fp: &str) -> Vec<u8> {
let one = 1u32.to_le_bytes();
let mut o = vec![0u8];
o.extend_from_slice(&one);
o.extend(sblock(comp));
o.extend(sblock(r":\SchLib\0.schlib"));
o.extend_from_slice(&one);
o.extend(sblock(descr));
o.extend(sblock(&format!(r"C:\adom\{comp}.SchLib")));
o.extend_from_slice(&one);
o.extend(sblock(fp));
o.extend(sblock("PCBLIB"));
o.extend_from_slice(&one);
o.extend(sblock(r":\PCBLib\0.pcblib"));
o.extend(sblock(&format!(r"C:\adom\{fp}.PcbLib")));
o
}
/// Encode a symbol + footprint into a self-contained `.IntLib`. The symbol must
/// carry `footprint = Some(footprint.name)` so its PCBLIB model resolves against
/// the bundled footprint.
pub fn encode_intlib(symbol: &Symbol, footprint: &Footprint, descr: &str) -> Result<Vec<u8>> {
let schlib = encode_schlib(std::slice::from_ref(symbol))?;
let pcblib = encode_pcblib(footprint)?;
let cursor = Cursor::new(Vec::<u8>::new());
let mut comp = cfb::CompoundFile::create(cursor).context("create IntLib CFBF")?;
comp.create_storage("/SchLib")?;
comp.create_storage("/PCBLib")?;
write_stream(&mut comp, "/SchLib/0.schlib", &wrap(&schlib)?)?;
write_stream(&mut comp, "/PCBLib/0.pcblib", &wrap(&pcblib)?)?;
write_stream(
&mut comp,
"/LibCrossRef.Txt",
&lib_cross_ref(&symbol.name, descr, &footprint.name),
)?;
write_stream(&mut comp, "/Version.Txt", &[0x00, 0x02, 0x00, 0x00, 0x00])?;
// Parameters .bin — [u8 0x00] then [u32 len][text\0] records:
// one for the symbol's parameters, then one for the footprint's. (No leading
// pipe in the text, unlike the |RECORD records.)
let mut params = vec![0x00u8];
let push_rec = |buf: &mut Vec<u8>, text: &str| {
let mut p = text.as_bytes().to_vec();
p.push(0);
buf.extend_from_slice(&(p.len() as u32).to_le_bytes());
buf.extend_from_slice(&p);
};
push_rec(
&mut params,
&format!(
"Comment=*|Component Kind=Standard|Description={descr}|Footprint={}",
footprint.name
),
);
push_rec(
&mut params,
&format!("Height=0mil|Pad Count={}", footprint.pads.len()),
);
write_stream(&mut comp, "/Parameters .bin", ¶ms)?;
comp.flush()?;
Ok(comp.into_inner().into_inner())
}
fn write_stream<F: Read + Write + std::io::Seek>(
comp: &mut cfb::CompoundFile<F>,
path: &str,
bytes: &[u8],
) -> Result<()> {
let mut s = comp
.create_stream(path)
.with_context(|| format!("create {path}"))?;
s.write_all(bytes)?;
s.flush()?;
Ok(())
}
/// Decode an `.IntLib`: inflate the embedded libs and parse each with the
/// existing readers. Returns `(symbol_json, footprint_json)`.
pub fn decode_intlib(bytes: &[u8]) -> Result<(serde_json::Value, serde_json::Value)> {
let (sch, pcb) = extract_intlib_libs(bytes)?;
Ok((crate::decode_schlib_first(&sch)?, crate::decode_pcblib(&pcb)?))
}
/// Extract the raw (inflated) embedded `.SchLib` + `.PcbLib` byte images from an
/// IntLib — for callers that want to re-decode them into the neutral model.
pub fn extract_intlib_libs(bytes: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
use flate2::read::ZlibDecoder;
let cursor = Cursor::new(bytes.to_vec());
let mut comp = cfb::CompoundFile::open(cursor).context("open IntLib")?;
let inflate = |comp: &mut cfb::CompoundFile<Cursor<Vec<u8>>>, path: &str| -> Result<Vec<u8>> {
let mut raw = Vec::new();
comp.open_stream(path)?.read_to_end(&mut raw)?;
// Payload is a 0x02 marker byte + zlib stream — verify, don't assume.
match raw.first() {
None => anyhow::bail!("{path}: empty embedded stream"),
Some(0x02) => {}
Some(b) => anyhow::bail!("{path}: unexpected marker byte {b:#04x} (expected 0x02)"),
}
let mut out = Vec::new();
ZlibDecoder::new(&raw[1..]).read_to_end(&mut out).with_context(|| format!("{path}: inflate"))?;
Ok(out)
};
let sch = inflate(&mut comp, "/SchLib/0.schlib")?;
let pcb = inflate(&mut comp, "/PCBLib/0.pcblib")?;
Ok((sch, pcb))
}