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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
//! Altium `.PcbLib` (footprint library) **encoder** + decoder.
//!
//! A PcbLib carries ~94 KB of `Library/*` boilerplate (master layer stack,
//! version info, pad/via library …) that is near-identical for every library,
//! plus per-component storages. So the encoder **rebuilds fresh from a real
//! Altium template**: every boilerplate stream is copied verbatim, the two
//! name-bearing library streams are patched, and one clean component is written
//! with the requested name + pad count. The template's own components are simply
//! not copied — so there's no stray default component.
//!
//! Pad record (type `0x02`) = six `[u32 len][bytes]` sub-blocks: designator
//! pstring, `00`, `|&|0` const, `00`, the 202-byte pad data block, empty. Pad
//! data offsets (from `altium-pcblib`): [0] layer, [13..17] i32 x,
//! [17..21] i32 y (Y-up), [21..25] u32 xsize, [25..29] u32 ysize,
//! [45..49] u32 hole, [49] shape, [52..60] f64 rotation.
//!
//! Component names + pad counts also live in `Library/ComponentParamsTOC/Data`
//! (one `Name=..|Pad Count=N|..` record), in a length-prefixed name list at the
//! **tail** of `Library/Data`, and in each `<comp>/Parameters`. All are rebuilt.
use crate::model::{Footprint, FpGraphic, FpLayer, Pad};
use anyhow::{bail, Context, Result};
use std::collections::BTreeMap;
use std::io::{Cursor, Read, Write};
const TEMPLATE: &[u8] = include_bytes!("../templates/pcblib_template.bin");
const TRACK_TPL: &[u8] = include_bytes!("../templates/track_tpl.bin");
const ARC_TPL: &[u8] = include_bytes!("../templates/arc_tpl.bin");
const ALTIUM_UNIT_TO_MM: f64 = 0.00000254;
const MM_TO_UNIT: f64 = 1.0 / ALTIUM_UNIT_TO_MM;
/// Component storage names in the template that we never copy (the real
/// components live in the model, not the skeleton).
const TEMPLATE_COMPONENTS: [&str; 2] = ["REFFP", "PCBComponent_1"];
/// Full layer encoding for a graphic record: `(layer_id [0], class_index [41],
/// class [43])`. Altium honours the tail pair `[41]/[43]` (not just `[0]`), so
/// both must be set or the primitive lands on whatever the template layer was.
///
/// Mapping (barrett's house convention, from his C0402): the part outline
/// lives on Mechanical 13 ("Assembly Top") and the courtyard on Mechanical 15
/// ("Courtyard Top") — nothing on Top Overlay.
///
/// ⚠ DELIBERATELY LOSSY: Silk and Fab both encode onto Mech13 (one layer byte
/// can't carry two roles), and the decoder ([`altium_decode::altium_layer`])
/// canonically maps 69 back to Fab. So a KiCad→Altium→KiCad round trip moves
/// F.SilkS art onto F.Fab. That is the house convention, not drift — the
/// `layer_roundtrip_house_convention` test pins it. Change both sites together.
fn layer_enc(l: &FpLayer) -> (u8, u8, u8) {
match l {
FpLayer::Silk => (69, 13, 2), // Mechanical 13 (Assembly Top)
FpLayer::Fab => (69, 13, 2), // Mechanical 13 (Assembly Top)
FpLayer::Courtyard => (71, 15, 2), // Mechanical 15 (Courtyard Top)
FpLayer::Copper => (1, 1, 1), // Top Layer
FpLayer::Other { altium } => {
let a = *altium;
match a {
33 | 34 => (a, 6, 3), // overlays
57..=72 => (a, a - 56, 2), // Mechanical 1..16
_ => (a, 6, 3),
}
}
}
}
fn ux(mm: f64) -> i32 {
(mm * MM_TO_UNIT).round() as i32
}
/// Y flips: neutral Y-down → Altium Y-up.
fn uy(mm: f64) -> i32 {
(-mm * MM_TO_UNIT).round() as i32
}
/// Patch the layer id `[0]` and the tail class pair — Track: `[41]/[43]`,
/// Arc: `[len-8]/[len-6]` — from the neutral layer role.
fn patch_layer(p: &mut [u8], l: &FpLayer, tail_b41: usize) {
let (b0, b41, b43) = layer_enc(l);
p[0] = b0;
p[tail_b41] = b41;
p[tail_b41 + 2] = b43;
}
/// A Track record (type 0x04, 1 sub-block): layer + endpoints + width.
fn emit_track(layer: &FpLayer, x1: f64, y1: f64, x2: f64, y2: f64, w: f64) -> Vec<u8> {
let mut p = TRACK_TPL.to_vec();
let tail = p.len() - 8; // class pair sits 8 bytes before the end
patch_layer(&mut p, layer, tail);
p[13..17].copy_from_slice(&ux(x1).to_le_bytes());
p[17..21].copy_from_slice(&uy(y1).to_le_bytes());
p[21..25].copy_from_slice(&ux(x2).to_le_bytes());
p[25..29].copy_from_slice(&uy(y2).to_le_bytes());
p[29..33].copy_from_slice(&((w * MM_TO_UNIT).round() as u32).to_le_bytes());
frame_pcb_record(0x04, &p)
}
/// An Arc record (type 0x01, 1 sub-block): layer + centre + radius + angles + width.
fn emit_arc(layer: &FpLayer, cx: f64, cy: f64, r: f64, start: f64, end: f64, w: f64) -> Vec<u8> {
let mut p = ARC_TPL.to_vec();
let tail = p.len() - 8;
patch_layer(&mut p, layer, tail);
p[13..17].copy_from_slice(&ux(cx).to_le_bytes());
p[17..21].copy_from_slice(&uy(cy).to_le_bytes());
p[21..25].copy_from_slice(&((r * MM_TO_UNIT).round() as i32).to_le_bytes());
p[25..33].copy_from_slice(&start.to_le_bytes());
p[33..41].copy_from_slice(&end.to_le_bytes());
p[41..45].copy_from_slice(&((w * MM_TO_UNIT).round() as u32).to_le_bytes());
frame_pcb_record(0x01, &p)
}
/// `<u8 type><u32 len><payload>` — one single-block PCB record.
fn frame_pcb_record(rt: u8, payload: &[u8]) -> Vec<u8> {
let mut rec = vec![rt];
rec.extend_from_slice(&(payload.len() as u32).to_le_bytes());
rec.extend_from_slice(payload);
rec
}
/// Expand a footprint's graphics into Altium primitive records, each tagged with
/// its `(objtype, objid_name)` for the GUID/UID sidecars (Track=4, Arc=1). A Rect
/// becomes 4 tracks; a Circle becomes a full 0–360 arc; an Arc's angles get the
/// Y-flip (neutral Y-down → Altium Y-up) applied as `-end .. -start`.
fn expand_fp_graphics(fp: &Footprint) -> Vec<(u32, &'static str, Vec<u8>)> {
let mut out = Vec::new();
for g in &fp.graphics {
match g {
FpGraphic::Line { layer, x1, y1, x2, y2, width } => {
out.push((4, "Track", emit_track(layer, *x1, *y1, *x2, *y2, *width)));
}
FpGraphic::Rect { layer, x1, y1, x2, y2, width } => {
for (a, b, c, d) in [
(*x1, *y1, *x2, *y1),
(*x2, *y1, *x2, *y2),
(*x2, *y2, *x1, *y2),
(*x1, *y2, *x1, *y1),
] {
out.push((4, "Track", emit_track(layer, a, b, c, d, *width)));
}
}
FpGraphic::Circle { layer, cx, cy, r, width } => {
out.push((1, "Arc", emit_arc(layer, *cx, *cy, *r, 0.0, 360.0, *width)));
}
FpGraphic::Arc { layer, cx, cy, r, start, end, width } => {
out.push((1, "Arc", emit_arc(layer, *cx, *cy, *r, -end, -start, *width)));
}
}
}
out
}
// ---------------------------------------------------------------------------
// Encode
// ---------------------------------------------------------------------------
/// Encode one footprint into a clean `.PcbLib` byte image.
pub fn encode_pcblib(fp: &Footprint) -> Result<Vec<u8>> {
encode_pcblib_many(std::slice::from_ref(fp))
}
/// Encode several footprints into one `.PcbLib`.
pub fn encode_pcblib_many(fps: &[Footprint]) -> Result<Vec<u8>> {
if fps.is_empty() {
bail!("no footprints to encode");
}
let tpl = read_all_streams(TEMPLATE)?;
let pad_data_tpl = harvest_pad_data_block(&tpl)?;
// Fresh CFBF.
let cursor = Cursor::new(Vec::<u8>::new());
let mut comp = cfb::CompoundFile::create(cursor).context("create CFBF")?;
let mut made_storages: std::collections::BTreeSet<String> = Default::default();
// 1) Copy every boilerplate stream verbatim, except the template's own
// component storages. Patch the two name-bearing library streams.
for (path, bytes) in &tpl {
if is_template_component_stream(path) {
continue;
}
let patched;
let data: &[u8] = if path == "/Library/Data" {
patched = patch_library_data_tail(bytes, fps);
&patched
} else if path == "/Library/ComponentParamsTOC/Data" {
patched = build_component_toc(fps);
&patched
} else if path == "/Library/ComponentParamsTOC/Header" {
patched = 1u32.to_le_bytes().to_vec();
&patched
} else {
bytes
};
write_stream(&mut comp, path, data, &mut made_storages)?;
}
// 2) Write each component's storages.
for fp in fps {
write_component(&mut comp, fp, &pad_data_tpl, &mut made_storages)?;
}
comp.flush()?;
Ok(comp.into_inner().into_inner())
}
/// Write the full set of streams for one footprint component.
fn write_component<F: Read + Write + std::io::Seek>(
comp: &mut cfb::CompoundFile<F>,
fp: &Footprint,
pad_data_tpl: &[u8],
made: &mut std::collections::BTreeSet<String>,
) -> Result<()> {
let n = fp.pads.len();
let base = format!("/{}", &fp.name);
// <comp>/Data — name block + pad records.
let mut data = Vec::new();
let mut name_block = vec![fp.name.len() as u8];
name_block.extend_from_slice(fp.name.as_bytes());
data.extend_from_slice(&(name_block.len() as u32).to_le_bytes());
data.extend_from_slice(&name_block);
for pad in &fp.pads {
data.extend(encode_pad_record(pad, pad_data_tpl));
}
// Silk / courtyard / fab graphics as Track/Arc records after the pads.
let graphics = expand_fp_graphics(fp);
for (_, _, rec) in &graphics {
data.extend_from_slice(rec);
}
let total = n + graphics.len(); // every record is one primitive
write_stream(comp, &format!("{base}/Data"), &data, made)?;
write_stream(comp, &format!("{base}/Header"), &(total as u32).to_le_bytes(), made)?;
// Parameters.
let params = format!(
"|PATTERN={}|HEIGHT=0mil|DESCRIPTION=|ITEMGUID=|REVISIONGUID=",
fp.name
);
write_stream(comp, &format!("{base}/Parameters"), &u32_text(¶ms), made)?;
// WideStrings — minimal constant.
write_stream(comp, &format!("{base}/WideStrings"), &[0x01, 0, 0, 0, 0], made)?;
// PrimitiveGuids/Data — [u32 hdr=0x55][objtype 0 + comp GUID][objtype 2 + idx + pad GUID]*
let mut pg = 0x55u32.to_le_bytes().to_vec();
pg.extend_from_slice(&0u32.to_le_bytes()); // component entry: objtype 0
pg.extend_from_slice(&guid16(&fp.name, 0));
for (i, _) in fp.pads.iter().enumerate() {
pg.extend_from_slice(&2u32.to_le_bytes()); // objtype 2 = pad
pg.extend_from_slice(&(i as u32).to_le_bytes());
pg.extend_from_slice(&guid16(&fp.name, (i as u32) + 1));
}
for (gi, (objtype, _, _)) in graphics.iter().enumerate() {
let idx = (n + gi) as u32;
pg.extend_from_slice(&objtype.to_le_bytes());
pg.extend_from_slice(&idx.to_le_bytes());
pg.extend_from_slice(&guid16(&fp.name, idx + 1));
}
write_stream(comp, &format!("{base}/PrimitiveGuids/Data"), &pg, made)?;
write_stream(
comp,
&format!("{base}/PrimitiveGuids/Header"),
&((total as u32) + 1).to_le_bytes(),
made,
)?;
// UniqueIDPrimitiveInformation/Data — one text record per primitive.
let mut uid = Vec::new();
for i in 0..n {
let rec = format!("|PRIMITIVEINDEX={i}|PRIMITIVEOBJECTID=Pad|UNIQUEID=");
uid.extend(u32_text(&rec));
}
for (gi, (_, objid, _)) in graphics.iter().enumerate() {
let rec = format!("|PRIMITIVEINDEX={}|PRIMITIVEOBJECTID={objid}|UNIQUEID=", n + gi);
uid.extend(u32_text(&rec));
}
write_stream(
comp,
&format!("{base}/UniqueIDPrimitiveInformation/Data"),
&uid,
made,
)?;
write_stream(
comp,
&format!("{base}/UniqueIDPrimitiveInformation/Header"),
&(total as u32).to_le_bytes(),
made,
)?;
Ok(())
}
/// `<u32 len>` + text + `\0`.
fn u32_text(text: &str) -> Vec<u8> {
let mut payload = text.as_bytes().to_vec();
payload.push(0);
let mut out = (payload.len() as u32).to_le_bytes().to_vec();
out.extend_from_slice(&payload);
out
}
/// Build `Library/ComponentParamsTOC/Data`: one length-prefixed text record
/// holding a `Name=..|Pad Count=N|Height=0|Description=\r\n` line per component.
fn build_component_toc(fps: &[Footprint]) -> Vec<u8> {
let mut text = String::new();
for fp in fps {
text.push_str(&format!(
"Name={}|Pad Count={}|Height=0|Description=\r\n",
fp.name,
fp.pads.len()
));
}
u32_text(&text)
}
/// Rebuild the trailing component-name list of `Library/Data`:
/// `<u32 count>` then per component `<u32 namelen+1><u8 namelen><name>`.
/// The list sits at the very end after the last `...GUID=\0`; we locate it by
/// reading the existing trailing `<u32 count>` and slicing it off.
fn patch_library_data_tail(orig: &[u8], fps: &[Footprint]) -> Vec<u8> {
// The original tail = [u32 count] + count*(name block). Walk backwards: the
// template has exactly 2 names (REFFP, PCBComponent_1). Find the count u32 by
// scanning from the known structure — simplest: reconstruct the prefix by
// dropping the original list and appending ours.
//
// The list start = position of the u32 count. We find it by parsing from a
// candidate: the byte right after the final "...GUID=\0" key. Robust approach:
// try every position from the end that parses as a valid [count][names...]
// consuming exactly to end-of-stream.
if let Some(list_start) = find_name_list_start(orig) {
let mut out = orig[..list_start].to_vec();
out.extend_from_slice(&(fps.len() as u32).to_le_bytes());
for fp in fps {
let nb_len = fp.name.len() as u32 + 1;
out.extend_from_slice(&nb_len.to_le_bytes());
out.push(fp.name.len() as u8);
out.extend_from_slice(fp.name.as_bytes());
}
out
} else {
// Couldn't locate — leave Library/Data unchanged (component still works
// via storage enumeration; TOC + storages carry the truth).
orig.to_vec()
}
}
/// Scan from the end for a position P such that `orig[P..]` parses exactly as
/// `<u32 count>` followed by `count` blocks of `<u32 len><len bytes>` ending at
/// end-of-stream. Returns P.
fn find_name_list_start(orig: &[u8]) -> Option<usize> {
let n = orig.len();
// The list is short; only scan the last 256 bytes.
let lo = n.saturating_sub(256);
for p in (lo..=n.saturating_sub(4)).rev() {
if parses_as_name_list(&orig[p..]) {
return Some(p);
}
}
None
}
fn parses_as_name_list(b: &[u8]) -> bool {
if b.len() < 4 {
return false;
}
let count = u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize;
if count == 0 || count > 64 {
return false;
}
let mut cur = 4;
for _ in 0..count {
if cur + 4 > b.len() {
return false;
}
let blen = u32::from_le_bytes([b[cur], b[cur + 1], b[cur + 2], b[cur + 3]]) as usize;
cur += 4;
if blen == 0 || blen > 256 || cur + blen > b.len() {
return false;
}
// first byte is the pstring length and must equal blen-1
if b[cur] as usize != blen - 1 {
return false;
}
cur += blen;
}
cur == b.len()
}
/// Build one pad record (type 0x02 + 6 sub-blocks), patching the data block.
fn encode_pad_record(pad: &Pad, pad_tpl: &[u8]) -> Vec<u8> {
let mut designator = vec![pad.number.len() as u8];
designator.extend_from_slice(pad.number.as_bytes());
let mut pd = pad_tpl.to_vec();
let x_units = (pad.x_mm * MM_TO_UNIT).round() as i32;
let y_units = (-pad.y_mm * MM_TO_UNIT).round() as i32; // Y-down → Altium Y-up
let xs = (pad.w_mm * MM_TO_UNIT).round() as u32;
let ys = (pad.h_mm * MM_TO_UNIT).round() as u32;
let hole = (pad.drill_mm * MM_TO_UNIT).round() as u32;
let shape = shape_byte(&pad.shape);
pd[0] = if hole > 0 { 0x21 } else { 1 }; // multilayer (thru) vs top
pd[13..17].copy_from_slice(&x_units.to_le_bytes());
pd[17..21].copy_from_slice(&y_units.to_le_bytes());
pd[21..25].copy_from_slice(&xs.to_le_bytes());
pd[25..29].copy_from_slice(&ys.to_le_bytes());
pd[29..33].copy_from_slice(&xs.to_le_bytes());
pd[33..37].copy_from_slice(&ys.to_le_bytes());
pd[37..41].copy_from_slice(&xs.to_le_bytes());
pd[41..45].copy_from_slice(&ys.to_le_bytes());
pd[45..49].copy_from_slice(&hole.to_le_bytes());
pd[49] = shape;
pd[50] = shape;
pd[51] = shape;
if pd.len() >= 60 {
pd[52..60].copy_from_slice(&pad.rotation.to_le_bytes());
}
let blocks: [&[u8]; 6] = [
&designator,
&[0x00],
&[0x04, 0x7c, 0x26, 0x7c, 0x30],
&[0x00],
&pd,
&[],
];
let mut rec = vec![0x02u8];
for b in blocks {
rec.extend_from_slice(&(b.len() as u32).to_le_bytes());
rec.extend_from_slice(b);
}
rec
}
fn shape_byte(shape: &str) -> u8 {
match shape {
"circle" | "oval" => 1,
"roundrect" => 9,
_ => 2,
}
}
/// Deterministic 16-byte GUID from a name + salt (no RNG; reproducible builds).
fn guid16(name: &str, salt: u32) -> [u8; 16] {
let mut out = [0u8; 16];
let mut h: u64 = 0x9e3779b97f4a7c15 ^ salt as u64;
for (i, b) in name.bytes().chain(salt.to_le_bytes()).enumerate() {
h ^= (b as u64).wrapping_shl(((i % 8) * 8) as u32);
h = h.wrapping_mul(0xff51afd7ed558ccd);
h ^= h >> 33;
}
for i in 0..16 {
h = h.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
out[i] = (h >> 56) as u8;
}
out
}
// ---------------------------------------------------------------------------
// Template I/O
// ---------------------------------------------------------------------------
fn read_all_streams(bytes: &[u8]) -> Result<BTreeMap<String, Vec<u8>>> {
let cursor = Cursor::new(bytes.to_vec());
let mut comp = cfb::CompoundFile::open(cursor).context("open template")?;
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 is_template_component_stream(path: &str) -> bool {
TEMPLATE_COMPONENTS
.iter()
.any(|c| path == format!("/{c}") || path.starts_with(&format!("/{c}/")))
}
fn harvest_pad_data_block(tpl: &BTreeMap<String, Vec<u8>>) -> Result<Vec<u8>> {
let data = tpl
.get("/REFFP/Data")
.context("template missing /REFFP/Data")?;
let name_block_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut cur = 4 + name_block_len;
if data.get(cur) != Some(&0x02) {
bail!("template first record is not a pad");
}
cur += 1;
let mut blocks = Vec::new();
for _ in 0..6 {
let blen =
u32::from_le_bytes([data[cur], data[cur + 1], data[cur + 2], data[cur + 3]]) as usize;
cur += 4;
blocks.push(data[cur..cur + blen].to_vec());
cur += blen;
}
Ok(blocks[4].clone())
}
/// Create any missing parent storages, then write `path` as a stream.
fn write_stream<F: Read + Write + std::io::Seek>(
comp: &mut cfb::CompoundFile<F>,
path: &str,
bytes: &[u8],
made: &mut std::collections::BTreeSet<String>,
) -> Result<()> {
// ensure parent storages
let parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
let mut acc = String::new();
for seg in &parts[..parts.len().saturating_sub(1)] {
acc.push('/');
acc.push_str(seg);
if made.insert(acc.clone()) {
comp.create_storage(&acc)
.with_context(|| format!("create storage {acc}"))?;
}
}
let mut s = comp
.create_stream(path)
.with_context(|| format!("create stream {path}"))?;
s.write_all(bytes)?;
s.flush()?;
Ok(())
}
// ---------------------------------------------------------------------------
// Decode
// ---------------------------------------------------------------------------
/// Decode a `.PcbLib` byte image to footprint JSON via `altium-pcblib`.
pub fn decode_pcblib(bytes: &[u8]) -> Result<serde_json::Value> {
altium_pcblib::parse_altium_pcblib_bytes(bytes).context("altium-pcblib decode")
}