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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
//! Altium `.SchLib` / `.PcbLib` / `.IntLib` → canonical [`AdomLbrPart`] (the
//! DECODE side — the inverse of `schlib`/`pcblib`/`intlib` encoders).
//!
//! Walks the OLE2/CFBF component streams directly so we control unit conversion:
//! symbol pins + graphics come back in mils, footprint pads + graphics in mm
//! (Y-down neutral), with silk/courtyard/fab layers preserved as [`FpLayer`]s.
use crate::model::{
AdomLbrPart, ConnectEntry, Footprint, FpGraphic, FpLayer, Graphic, Pad, Pin, Symbol,
};
use anyhow::{Context, Result};
use std::collections::BTreeMap;
use std::io::{Cursor, Read};
/// Altium base unit = 1/10000 mil = 0.00000254 mm (PCB coordinates).
const ALTIUM_UNIT_MM: f64 = 0.00000254;
/// Schematic binary coordinate units per mil (pin X).
const UNITS_PER_MIL: f64 = 25.6;
// ---------------------------------------------------------------------------
// Public entry points
// ---------------------------------------------------------------------------
/// Decode a `.SchLib` + `.PcbLib` pair into the canonical part. Either may be
/// `None` (e.g. footprint-only), but at least one is required.
pub fn decode_altium(schlib: Option<&[u8]>, pcblib: Option<&[u8]>, mpn: &str) -> Result<AdomLbrPart> {
let symbol = match schlib {
Some(b) => decode_schlib_symbol(b)?,
None => Symbol {
name: mpn.to_string(),
pins: vec![],
body: None,
graphics: vec![],
designator_prefix: Some("U".into()),
footprint: None,
parameters: vec![],
pin_numbers_hidden: None,
pin_names_hidden: None,
},
};
let footprint = match pcblib {
Some(b) => decode_pcblib_footprint(b)?,
None => Footprint { name: mpn.to_string(), pads: vec![], graphics: vec![] },
};
build_part(symbol, footprint, mpn)
}
/// Decode a `.IntLib` (bundles both) into the canonical part.
pub fn decode_intlib_part(bytes: &[u8], mpn: &str) -> Result<AdomLbrPart> {
let (sch, pcb) = crate::intlib::extract_intlib_libs(bytes)?;
decode_altium(Some(&sch), Some(&pcb), mpn)
}
fn build_part(mut symbol: Symbol, footprint: Footprint, mpn: &str) -> Result<AdomLbrPart> {
if symbol.footprint.is_none() && !footprint.name.is_empty() {
symbol.footprint = Some(footprint.name.clone());
}
// identity connect over shared designators
let pad_nums: std::collections::HashSet<&str> =
footprint.pads.iter().map(|p| p.number.as_str()).collect();
let connect = symbol
.pins
.iter()
.filter(|p| pad_nums.contains(p.number.as_str()))
.map(|p| ConnectEntry { pin: p.number.clone(), pad: p.number.clone() })
.collect();
// pull value/manufacturer/etc. out of the symbol's decoded parameters
let param = |name: &str| {
symbol
.parameters
.iter()
.find(|p| p.name.eq_ignore_ascii_case(name))
.map(|p| p.value.clone())
.unwrap_or_default()
};
Ok(AdomLbrPart {
schema_version: 2,
mpn: if mpn.is_empty() { symbol.name.clone() } else { mpn.to_string() },
manufacturer: param("Manufacturer"),
package: param("Package"),
value: param("Value"),
category: String::new(),
symbol,
footprint,
connect,
model_3d: None,
provenance: Some(serde_json::json!({"source": "altium_decode"})),
})
}
// ---------------------------------------------------------------------------
// CFBF helpers
// ---------------------------------------------------------------------------
fn read_streams(bytes: &[u8]) -> Result<BTreeMap<String, Vec<u8>>> {
let mut comp = cfb::CompoundFile::open(Cursor::new(bytes.to_vec())).context("open CFBF")?;
let paths: Vec<String> = comp
.walk()
.filter(|e| e.is_stream())
.map(|e| e.path().to_string_lossy().to_string())
.collect();
let mut map = BTreeMap::new();
for p in paths {
let mut buf = Vec::new();
comp.open_stream(&p)?.read_to_end(&mut buf)?;
map.insert(p, buf);
}
Ok(map)
}
fn le_u16(b: &[u8], o: usize) -> u16 {
u16::from_le_bytes([*b.get(o).unwrap_or(&0), *b.get(o + 1).unwrap_or(&0)])
}
fn le_u32(b: &[u8], o: usize) -> u32 {
u32::from_le_bytes([
*b.get(o).unwrap_or(&0),
*b.get(o + 1).unwrap_or(&0),
*b.get(o + 2).unwrap_or(&0),
*b.get(o + 3).unwrap_or(&0),
])
}
fn le_i32(b: &[u8], o: usize) -> i32 {
le_u32(b, o) as i32
}
fn le_i16(b: &[u8], o: usize) -> i16 {
le_u16(b, o) as i16
}
fn le_i24(b: &[u8], o: usize) -> i32 {
let v = (*b.get(o).unwrap_or(&0) as i32)
| ((*b.get(o + 1).unwrap_or(&0) as i32) << 8)
| ((*b.get(o + 2).unwrap_or(&0) as i32) << 16);
(v << 8) >> 8 // sign-extend 24-bit
}
fn le_f64(b: &[u8], o: usize) -> f64 {
let mut a = [0u8; 8];
for i in 0..8 {
a[i] = *b.get(o + i).unwrap_or(&0);
}
f64::from_le_bytes(a)
}
/// Read a `|KEY=VAL|...` field out of a text record.
fn field<'a>(txt: &'a str, key: &str) -> Option<&'a str> {
let pat = format!("|{key}=");
let i = txt.find(&pat)? + pat.len();
let rest = &txt[i..];
let end = rest.find('|').unwrap_or(rest.len());
Some(&rest[..end])
}
// ---------------------------------------------------------------------------
// SchLib → Symbol
// ---------------------------------------------------------------------------
fn decode_schlib_symbol(bytes: &[u8]) -> Result<Symbol> {
let streams = read_streams(bytes)?;
// first component storage's Data (skip housekeeping streams)
let (name, data) = streams
.iter()
.filter(|(p, _)| p.ends_with("/Data") && !p.contains("FileHeader") && !p.contains("Storage"))
.map(|(p, d)| {
let comp = p.trim_start_matches('/').split('/').next().unwrap_or("SYM").to_string();
(comp, d.clone())
})
.next()
.context("SchLib has no component Data stream")?;
let mut pins = Vec::new();
let mut graphics = Vec::new();
let mut parameters = Vec::new();
let mut designator_prefix = Some("U".to_string());
let mut footprint = None;
let mut lib_name = name.clone();
let mut cur = 0usize;
while cur + 4 <= data.len() {
let len = le_u16(&data, cur) as usize;
let flags = le_u16(&data, cur + 2);
let payload = &data[cur + 4..(cur + 4 + len).min(data.len())];
cur += 4 + len;
if flags == 0x0100 {
// binary pin record
if let Some(p) = decode_pin(payload) {
pins.push(p);
}
continue;
}
// text record
if payload.first() != Some(&b'|') {
continue;
}
let txt = String::from_utf8_lossy(payload);
let txt = txt.split('\0').next().unwrap_or("");
let rec = field(txt, "RECORD").unwrap_or("");
match rec {
"1" => {
if let Some(n) = field(txt, "LibReference") {
lib_name = n.to_string();
}
}
"6" | "11" | "12" | "14" => {
if let Some(g) = decode_sym_graphic(txt, rec) {
graphics.push(g);
}
}
"34" => {
if let Some(t) = field(txt, "Text") {
designator_prefix = Some(t.trim_end_matches('?').to_string());
}
}
"41" => {
if let (Some(name), Some(text)) = (field(txt, "Name"), field(txt, "Text")) {
if name != "Comment" && !text.is_empty() && text != "*" {
parameters.push(crate::model::Parameter {
name: name.to_string(),
value: text.to_string(),
visible: !txt.contains("|IsHidden=T"),
});
}
}
}
"45" => {
if let Some(m) = field(txt, "ModelName") {
footprint = Some(m.to_string());
}
}
_ => {}
}
}
Ok(Symbol {
name: lib_name,
pins,
body: None,
graphics,
designator_prefix,
footprint,
parameters,
pin_numbers_hidden: None,
pin_names_hidden: None,
})
}
/// Decode a binary pin record → neutral [`Pin`] (connection-point mils).
fn decode_pin(p: &[u8]) -> Option<Pin> {
if p.len() < 26 {
return None;
}
// orientation packed in low 2 bits of the length field [15:17]
let field15 = le_u16(p, 15);
let aorient = (field15 & 0x3) as u32;
let length_units = (field15 & 0xFFFC) as f64;
let len_mil = (length_units / UNITS_PER_MIL).round() as i32;
// body-end location: X = i24 @[17:20] (÷25.6/mil), Y = i16 @[20:22] (×10 mil)
let body_x = (le_i24(p, 17) as f64 / UNITS_PER_MIL).round() as i32;
let body_y = (le_i16(p, 20) as i32) * 10;
// kicad angle q from aorient = (q + 2) % 4
let q = ((aorient + 2) % 4) as i32;
let (dx, dy) = match q % 4 {
0 => (1, 0),
1 => (0, 1),
2 => (-1, 0),
_ => (0, -1),
};
// connection = body_end - length * dir
let x = body_x - len_mil * dx;
let y = body_y - len_mil * dy;
// strings: name pstr then designator pstr @ 26
let mut j = 26usize;
let name = read_pstr(p, &mut j);
let number = read_pstr(p, &mut j);
Some(Pin {
number: if number.is_empty() { "?".into() } else { number },
name,
x,
y,
length: Some(len_mil),
rotation: (q * 90) as u16,
})
}
fn read_pstr(p: &[u8], j: &mut usize) -> String {
if *j >= p.len() {
return String::new();
}
let n = p[*j] as usize;
let start = *j + 1;
let end = (start + n).min(p.len());
*j = end;
String::from_utf8_lossy(&p[start..end]).to_string()
}
/// Decode a symbol graphic text record (RECORD=6/11/12/14) → [`Graphic`] (mils).
fn decode_sym_graphic(txt: &str, rec: &str) -> Option<Graphic> {
// coordinate = base*10 + frac/10000 (mils), from `KEY` + optional `KEY_Frac`
let coord = |key: &str| -> f64 {
let base: f64 = field(txt, key).and_then(|v| v.parse().ok()).unwrap_or(0.0);
let frac: f64 = field(txt, &format!("{key}_Frac"))
.and_then(|v| v.parse().ok())
.unwrap_or(0.0);
base * 10.0 + frac / 10000.0
};
let width = field(txt, "LineWidth").and_then(|v| v.parse::<f64>().ok()).unwrap_or(1.0) * 10.0;
match rec {
"14" => Some(Graphic::Rect {
x1: coord("Location.X"),
y1: coord("Location.Y"),
x2: coord("Corner.X"),
y2: coord("Corner.Y"),
width,
filled: txt.contains("|IsSolid=T"),
}),
"11" => Some(Graphic::Circle {
cx: coord("Location.X"),
cy: coord("Location.Y"),
r: coord("Radius"),
width,
}),
"12" => Some(Graphic::Arc {
cx: coord("Location.X"),
cy: coord("Location.Y"),
r: coord("Radius"),
start: field(txt, "StartAngle").and_then(|v| v.parse().ok()).unwrap_or(0.0),
end: field(txt, "EndAngle").and_then(|v| v.parse().ok()).unwrap_or(360.0),
width,
}),
"6" => {
let n: usize = field(txt, "LocationCount").and_then(|v| v.parse().ok()).unwrap_or(0);
let mut points = Vec::new();
for i in 1..=n {
points.push([coord(&format!("X{i}")), coord(&format!("Y{i}"))]);
}
if points.len() >= 2 {
Some(Graphic::Polyline { points, width })
} else {
None
}
}
_ => None,
}
}
// ---------------------------------------------------------------------------
// PcbLib → Footprint (pads + silk/courtyard/fab graphics)
// ---------------------------------------------------------------------------
fn decode_pcblib_footprint(bytes: &[u8]) -> Result<Footprint> {
let streams = read_streams(bytes)?;
let (name, data) = streams
.iter()
.filter(|(p, _)| {
p.ends_with("/Data")
&& !p.contains("/Library")
&& !p.contains("FileVersionInfo")
&& !p.contains("FileHeader")
})
.map(|(p, d)| {
let comp = p.trim_start_matches('/').split('/').next().unwrap_or("FP").to_string();
(comp, d.clone())
})
.next()
.context("PcbLib has no component Data stream")?;
// component name block: <u32 len><name>
let name_len = le_u32(&data, 0) as usize;
let mut cur = 4 + name_len;
let fp_name = {
// the block holds a pstring: <u8 len><name>
let blk = &data[4..(4 + name_len).min(data.len())];
if !blk.is_empty() {
let n = blk[0] as usize;
String::from_utf8_lossy(&blk[1..(1 + n).min(blk.len())]).to_string()
} else {
name
}
};
let mut pads = Vec::new();
let mut graphics = Vec::new();
while cur < data.len() {
let rt = data[cur];
cur += 1;
let nblocks = match rt {
0x02 => 6, // Pad
0x05 => 2, // Text
0x01 | 0x03 | 0x04 | 0x06 | 0x09 | 0x0B | 0x0C => 1,
_ => 1,
};
let mut blocks: Vec<&[u8]> = Vec::new();
let mut ok = true;
for _ in 0..nblocks {
if cur + 4 > data.len() {
ok = false;
break;
}
let blen = le_u32(&data, cur) as usize;
cur += 4;
if cur + blen > data.len() {
ok = false;
break;
}
blocks.push(&data[cur..cur + blen]);
cur += blen;
}
if !ok {
break;
}
match rt {
0x02 => {
if let Some(pad) = decode_pad(&blocks) {
pads.push(pad);
}
}
0x04 => {
if let Some(g) = decode_track(blocks[0]) {
graphics.push(g);
}
}
0x01 => {
if let Some(g) = decode_arc(blocks[0]) {
graphics.push(g);
}
}
_ => {}
}
}
Ok(Footprint { name: fp_name, pads, graphics })
}
/// Altium layer byte → neutral role. Counterpart of `pcblib::layer_enc`.
/// NOTE: the house convention encodes BOTH Silk and Fab onto Mech13 (69), so
/// 69 canonically decodes to Fab — silk art merged there on encode stays Fab
/// after a round trip (deliberate; see the doc on `layer_enc`). Vendor libs
/// (UL/SamacSys) put silk on TopOverlay (33), which decodes to Silk correctly.
fn altium_layer(byte: u8) -> FpLayer {
match byte {
33 => FpLayer::Silk,
69 => FpLayer::Fab,
71 => FpLayer::Courtyard,
1 | 32 => FpLayer::Copper,
b => FpLayer::Other { altium: b },
}
}
fn decode_pad(blocks: &[&[u8]]) -> Option<Pad> {
let designator = blocks.first()?;
let n = *designator.first()? as usize;
let number = String::from_utf8_lossy(&designator[1..(1 + n).min(designator.len())]).to_string();
let pd = blocks.get(4)?; // pad data block
if pd.len() < 60 {
return None;
}
let x = le_i32(pd, 13) as f64 * ALTIUM_UNIT_MM;
let y = -(le_i32(pd, 17) as f64) * ALTIUM_UNIT_MM; // Altium Y-up → neutral Y-down
let w = le_u32(pd, 21) as f64 * ALTIUM_UNIT_MM;
let h = le_u32(pd, 25) as f64 * ALTIUM_UNIT_MM;
let hole = le_u32(pd, 45) as f64 * ALTIUM_UNIT_MM;
let shape = match pd[49] {
1 => "circle",
9 => "roundrect",
_ => "rect",
};
let rotation = if pd.len() >= 60 { le_f64(pd, 52) } else { 0.0 };
Some(Pad {
number,
x_mm: x,
y_mm: y,
w_mm: w,
h_mm: h,
drill_mm: hole,
rotation,
shape: shape.to_string(),
// The plated flag byte in the Altium pad record isn't calibrated yet —
// unknown, so leave None (treated as plated downstream).
plated: None,
})
}
/// Track (0x04): [0]layer, [13:17]x1, [17:21]y1, [21:25]x2, [25:29]y2, [29:33]width.
fn decode_track(p: &[u8]) -> Option<FpGraphic> {
if p.len() < 33 {
return None;
}
let layer = altium_layer(p[0]);
let c = |o: usize| le_i32(p, o) as f64 * ALTIUM_UNIT_MM;
Some(FpGraphic::Line {
layer,
x1: c(13),
y1: -c(17),
x2: c(21),
y2: -c(25),
width: le_u32(p, 29) as f64 * ALTIUM_UNIT_MM,
})
}
/// Arc (0x01): [0]layer, [13:17]cx, [17:21]cy, [21:25]r, [25:33]f64 start,
/// [33:41]f64 end, [41:45]width. A full 0..360 arc becomes a Circle.
fn decode_arc(p: &[u8]) -> Option<FpGraphic> {
if p.len() < 45 {
return None;
}
let layer = altium_layer(p[0]);
let c = |o: usize| le_i32(p, o) as f64 * ALTIUM_UNIT_MM;
let cx = c(13);
let cy = -c(17);
let r = c(21);
let start = le_f64(p, 25);
let end = le_f64(p, 33);
let width = le_u32(p, 41) as f64 * ALTIUM_UNIT_MM;
if (end - start).abs() >= 359.9 || (start == 0.0 && end == 360.0) {
Some(FpGraphic::Circle { layer, cx, cy, r, width })
} else {
// Y-flip mirrors angles: negate + swap so the visible sweep matches.
Some(FpGraphic::Arc { layer, cx, cy, r, start: -end, end: -start, width })
}
}