skill
Adom Altium SchLib
Public Made by Adomby adom
Native Rust parser for Altium .SchLib binary symbol libraries — pins + body to KiCad-shaped JSON, native SVG, .kicad_sym export. The symbol counterpart to altium-pcblib.
← Commit history
Publish 1.0.5
18 files changed
+869
.gitignore+1Cargo.lock+262Cargo.toml+25LICENSE+21SKILL.md+66adom-altium-schlib-svginstall.sh+7package.json+33screenshots/ads1263-symbol.pngscreenshots/hero-v2.pngscreenshots/hero.pngscreenshots/validation.pngskills/adom-altium-schlib/SKILL.md+66src/bin/json.rs+6src/bin/kicad.rs+10src/bin/svg.rs+7src/lib.rs+362uninstall.sh+3.gitignoreadded+1@@ -0,0 +1 @@+/target
Cargo.lockadded+262@@ -0,0 +1,262 @@+# This file is automatically @generated by Cargo.+# It is not intended for manual editing.+version = 4++[[package]]+name = "altium-schlib"+version = "0.1.0"+dependencies = [+ "anyhow",+ "cfb",+ "serde_json",+]++[[package]]+name = "anyhow"+version = "1.0.102"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"++[[package]]+name = "bumpalo"+version = "3.20.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"++[[package]]+name = "byteorder"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"++[[package]]+name = "cfb"+version = "0.10.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d8a4f8e55be323b378facfcf1f06aa97f6ec17cf4ac84fb17325093aaf62da41"+dependencies = [+ "byteorder",+ "fnv",+ "uuid",+]++[[package]]+name = "cfg-if"+version = "1.0.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"++[[package]]+name = "fnv"+version = "1.0.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"++[[package]]+name = "futures-core"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"++[[package]]+name = "futures-task"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"++[[package]]+name = "futures-util"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"+dependencies = [+ "futures-core",+ "futures-task",+ "pin-project-lite",+ "slab",+]++[[package]]+name = "itoa"+version = "1.0.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"++[[package]]+name = "js-sys"+version = "0.3.102"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"+dependencies = [+ "cfg-if",+ "futures-util",+ "wasm-bindgen",+]++[[package]]+name = "memchr"+version = "2.8.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"++[[package]]+name = "once_cell"+version = "1.21.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"++[[package]]+name = "pin-project-lite"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"++[[package]]+name = "proc-macro2"+version = "1.0.106"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "quote"+version = "1.0.46"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"+dependencies = [+ "proc-macro2",+]++[[package]]+name = "rustversion"+version = "1.0.22"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"++[[package]]+name = "serde"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"+dependencies = [+ "serde_core",+]++[[package]]+name = "serde_core"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"+dependencies = [+ "serde_derive",+]++[[package]]+name = "serde_derive"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "serde_json"+version = "1.0.150"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"+dependencies = [+ "itoa",+ "memchr",+ "serde",+ "serde_core",+ "zmij",+]++[[package]]+name = "slab"+version = "0.4.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"++[[package]]+name = "syn"+version = "2.0.118"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "unicode-ident"+version = "1.0.24"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"++[[package]]+name = "uuid"+version = "1.23.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"+dependencies = [+ "js-sys",+ "wasm-bindgen",+]++[[package]]+name = "wasm-bindgen"+version = "0.2.125"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"+dependencies = [+ "cfg-if",+ "once_cell",+ "rustversion",+ "wasm-bindgen-macro",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-macro"+version = "0.2.125"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"+dependencies = [+ "quote",+ "wasm-bindgen-macro-support",+]++[[package]]+name = "wasm-bindgen-macro-support"+version = "0.2.125"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"+dependencies = [+ "bumpalo",+ "proc-macro2",+ "quote",+ "syn",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-shared"+version = "0.2.125"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "zmij"+version = "1.0.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
Cargo.tomladded+25@@ -0,0 +1,25 @@+[package]+name = "altium-schlib"+version = "0.1.0"+edition = "2021"+description = "Native Rust parser for Altium .SchLib (and .IntLib) symbol libraries — extracts pins (number/name/xy/rotation) + body as KiCad-shaped JSON, renders SVG, emits .kicad_sym. Sibling to altium-pcblib."+authors = ["Adom <[email protected]>"]+license = "MIT"+repository = "https://github.com/adom-inc/altium-schlib"++[dependencies]+anyhow = "1"+serde_json = "1"+cfb = "0.10"++[[bin]]+name = "adom-altium-schlib-svg"+path = "src/bin/svg.rs"++[[bin]]+name = "altium-schlib-kicad"+path = "src/bin/kicad.rs"++[[bin]]+name = "altium-schlib-json"+path = "src/bin/json.rs"
LICENSEadded+21@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2026 Adom Industries Inc.++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
SKILL.mdadded+66@@ -0,0 +1,66 @@+---+name: adom-altium-schlib+description: >+ Native Rust crate that parses Altium .SchLib (and .IntLib) binary schematic-+ symbol libraries — an OLE2 Compound File container holding a reverse-engineered+ binary pin-record format. Emits KiCad-shaped pin JSON, renders a native symbol+ SVG, and emits .kicad_sym for export. The symbol counterpart to altium-pcblib+ (footprints); together they read both halves of an Altium library natively, with+ no Altium install and no KiCad/service round-trip. Used by adom-sfconvert ++ chip-fetcher so each format renders its OWN geometry for cross-source comparison.+ Public Rust API: parse_altium_schlib_path(&Path), parse_altium_schlib_bytes(&[u8]),+ render_symbol_svg(&Value), to_kicad_sym(&Value, name).+ Validated 28/28 pins (number + name, overbars included) against KiCad 10's+ Altium importer on ADS1263IPWR.+---++# altium-schlib++Read Altium schematic-symbol libraries (`.SchLib` / `.IntLib`) in pure Rust.++## When to reach for this++- A tool needs the **pins** (numbers, names, sides, rotation) out of an Altium+ `.SchLib` without installing Altium or converting through KiCad.+- You want a **native symbol thumbnail** (SVG) that shows the Altium library's+ own geometry, for side-by-side comparison against other sources.+- You need to **export** an Altium symbol to `.kicad_sym`.++## API++| Call | Returns |+|---|---|+| `parse_altium_schlib_path(&Path)` | symbol JSON (`module_name`, `pin_count`, `pins[]`, `body`, `part_count`) |+| `parse_altium_schlib_bytes(&[u8])` | same, from raw bytes |+| `render_symbol_svg(&Value)` | standalone SVG, drawn natively |+| `to_kicad_sym(&Value, name)` | `.kicad_sym` text (export only) |++Each pin: `{ number, name, x, y, length, rotation, conglomerate }`.++## The binary pin record (the hard-won part)++Inside `/<Component>/Data`, text records are `<u16 len><u16 flags>|KEY=VAL|…`,+but **pins are binary** records whose payload starts with `0x02`:++```+[14] PinConglomerate orientation in bits 0..1 → 0/90/180/270°+[15..17] PinLength i16+[17..19] Location.X i16+[19..21] Location.Y i16+[26..] length-prefixed strings: Name, then Designator+```++Open the OLE2 container after padding the buffer to ≥1 MiB with `0xFF` (Altium's+FAT overshoots the real sector count, which `cfb` otherwise rejects).++## Don't re-implement this++If another Adom tool needs Altium symbols, depend on this crate — do **not**+re-walk OLE2 + re-decode the pin format. One source of truth, like+`altium-pcblib` is for footprints.++## Pairing++- Footprints → [`altium-pcblib`](https://wiki.adom.inc/skill/altium-pcblib)+- Converter that uses both → `adom-sfconvert`+- Consumer that shows the result → `chip-fetcher`
adom-altium-schlib-svgadded⋯ 1 unchanged line ⋯
install.shadded+7@@ -0,0 +1,7 @@+#!/usr/bin/env bash+set -e+HERE="$(cd "$(dirname "$0")" && pwd)"+DEST="$HOME/.claude/skills/adom-altium-schlib"+mkdir -p "$DEST"+cp -f "$HERE/skills/adom-altium-schlib/SKILL.md" "$DEST/SKILL.md"+echo "Installed adom-altium-schlib skill → $DEST"
package.jsonadded+33@@ -0,0 +1,33 @@+{+ "slug": "adom-altium-schlib",+ "version": "1.0.5",+ "type": "skill",+ "description": "Native Rust parser for Altium .SchLib binary symbol libraries \u2014 pins + body to KiCad-shaped JSON, native SVG, .kicad_sym export. The symbol counterpart to altium-pcblib.",+ "dependencies": {},+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "tags": [+ "altium",+ "schlib",+ "kicad",+ "rust",+ "eda",+ "symbol",+ "parser"+ ],+ "hero": {+ "path": "screenshots/hero-v2.png"+ },+ "license": "MIT",+ "discovery_triggers": [+ "altium schlib",+ "parse altium symbol",+ "read altium .schlib",+ "altium symbol to kicad",+ "altium-schlib",+ "native altium symbol parser",+ "altium ole2 symbol"+ ]+}
screenshots/ads1263-symbol.pngadded⋯ 1 unchanged line ⋯
screenshots/hero-v2.pngadded⋯ 1 unchanged line ⋯
screenshots/hero.pngadded⋯ 1 unchanged line ⋯
screenshots/validation.pngadded⋯ 1 unchanged line ⋯
skills/adom-altium-schlib/SKILL.mdadded+66@@ -0,0 +1,66 @@+---+name: adom-altium-schlib+description: >+ Native Rust crate that parses Altium .SchLib (and .IntLib) binary schematic-+ symbol libraries — an OLE2 Compound File container holding a reverse-engineered+ binary pin-record format. Emits KiCad-shaped pin JSON, renders a native symbol+ SVG, and emits .kicad_sym for export. The symbol counterpart to altium-pcblib+ (footprints); together they read both halves of an Altium library natively, with+ no Altium install and no KiCad/service round-trip. Used by adom-sfconvert ++ chip-fetcher so each format renders its OWN geometry for cross-source comparison.+ Public Rust API: parse_altium_schlib_path(&Path), parse_altium_schlib_bytes(&[u8]),+ render_symbol_svg(&Value), to_kicad_sym(&Value, name).+ Validated 28/28 pins (number + name, overbars included) against KiCad 10's+ Altium importer on ADS1263IPWR.+---++# altium-schlib++Read Altium schematic-symbol libraries (`.SchLib` / `.IntLib`) in pure Rust.++## When to reach for this++- A tool needs the **pins** (numbers, names, sides, rotation) out of an Altium+ `.SchLib` without installing Altium or converting through KiCad.+- You want a **native symbol thumbnail** (SVG) that shows the Altium library's+ own geometry, for side-by-side comparison against other sources.+- You need to **export** an Altium symbol to `.kicad_sym`.++## API++| Call | Returns |+|---|---|+| `parse_altium_schlib_path(&Path)` | symbol JSON (`module_name`, `pin_count`, `pins[]`, `body`, `part_count`) |+| `parse_altium_schlib_bytes(&[u8])` | same, from raw bytes |+| `render_symbol_svg(&Value)` | standalone SVG, drawn natively |+| `to_kicad_sym(&Value, name)` | `.kicad_sym` text (export only) |++Each pin: `{ number, name, x, y, length, rotation, conglomerate }`.++## The binary pin record (the hard-won part)++Inside `/<Component>/Data`, text records are `<u16 len><u16 flags>|KEY=VAL|…`,+but **pins are binary** records whose payload starts with `0x02`:++```+[14] PinConglomerate orientation in bits 0..1 → 0/90/180/270°+[15..17] PinLength i16+[17..19] Location.X i16+[19..21] Location.Y i16+[26..] length-prefixed strings: Name, then Designator+```++Open the OLE2 container after padding the buffer to ≥1 MiB with `0xFF` (Altium's+FAT overshoots the real sector count, which `cfb` otherwise rejects).++## Don't re-implement this++If another Adom tool needs Altium symbols, depend on this crate — do **not**+re-walk OLE2 + re-decode the pin format. One source of truth, like+`altium-pcblib` is for footprints.++## Pairing++- Footprints → [`altium-pcblib`](https://wiki.adom.inc/skill/altium-pcblib)+- Converter that uses both → `adom-sfconvert`+- Consumer that shows the result → `chip-fetcher`
src/bin/json.rsadded+6@@ -0,0 +1,6 @@+fn main() -> anyhow::Result<()> {+ let path = std::env::args().nth(1).unwrap();+ let sym = altium_schlib::parse_altium_schlib_path(std::path::Path::new(&path))?;+ println!("{}", serde_json::to_string_pretty(&sym)?);+ Ok(())+}
src/bin/kicad.rsadded+10@@ -0,0 +1,10 @@+//! altium-schlib-kicad <file.SchLib> [name] — emit a .kicad_sym (export path).+fn main() -> anyhow::Result<()> {+ let path = std::env::args().nth(1).ok_or_else(|| anyhow::anyhow!("usage: altium-schlib-kicad <file.SchLib> [name]"))?;+ let sym = altium_schlib::parse_altium_schlib_path(std::path::Path::new(&path))?;+ let name = std::env::args().nth(2).unwrap_or_else(|| {+ sym.get("module_name").and_then(|v| v.as_str()).unwrap_or("SYM").to_string()+ });+ print!("{}", altium_schlib::to_kicad_sym(&sym, &name));+ Ok(())+}
src/bin/svg.rsadded+7@@ -0,0 +1,7 @@+//! adom-altium-schlib-svg <file.SchLib> — print the native symbol SVG to stdout.+fn main() -> anyhow::Result<()> {+ let path = std::env::args().nth(1).ok_or_else(|| anyhow::anyhow!("usage: adom-altium-schlib-svg <file.SchLib>"))?;+ let sym = altium_schlib::parse_altium_schlib_path(std::path::Path::new(&path))?;+ print!("{}", altium_schlib::render_symbol_svg(&sym));+ Ok(())+}
src/lib.rsadded+362@@ -0,0 +1,362 @@+//! Native Rust parser for Altium `.SchLib` (and `.IntLib`'s SchLib stream)+//! schematic-symbol libraries — the symbol counterpart to `altium-pcblib`.+//!+//! Altium `.SchLib` is a Microsoft OLE2 / Compound File Binary container. Each+//! component is a storage holding a `Data` stream of records:+//! * **text records** — `<u16 len><u16 flags>` then `|KEY=VAL|KEY=VAL` ASCII.+//! RECORD=1 is the component header, RECORD=14 a rectangle (body), RECORD=41+//! a parameter.+//! * **binary pin records** — `<u16 len><u16 flags>` then a payload whose first+//! byte is `0x02`. Layout (offsets into the payload), reverse-engineered and+//! validated pin-for-pin against KiCad 10's Altium importer:+//! [14] PinConglomerate (u8) — orientation in bits 0..1+//! [15..17] PinLength (i16)+//! [17..19] Location.X (i16)+//! [19..21] Location.Y (i16)+//! [26..] length-prefixed strings: Name, then Designator+//!+//! Output JSON mirrors `altium-pcblib`'s KiCad-shaped style so the rest of the+//! Adom EDA family consumes both the same way.++use anyhow::{anyhow, bail, Result};+use std::io::{Cursor, Read};+use std::path::Path;++/// Parse an Altium `.SchLib` from a file path → symbol JSON.+pub fn parse_altium_schlib_path(path: &Path) -> Result<serde_json::Value> {+ let bytes = std::fs::read(path)+ .map_err(|e| anyhow!("read .SchLib {}: {e}", path.display()))?;+ parse_altium_schlib_bytes(&bytes)+}++/// Parse an Altium `.SchLib` from raw bytes → symbol JSON.+pub fn parse_altium_schlib_bytes(bytes: &[u8]) -> Result<serde_json::Value> {+ if bytes.len() < 8 || bytes[..8] != [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1] {+ bail!("not an OLE2 compound document (bad magic)");+ }+ // Altium writes FATs that overshoot the file's real sector count; pad with+ // the CFBF free-sector sentinel (0xFF) and re-open. Same fix as altium-pcblib.+ let owned;+ let cursor = if bytes.len() < 1024 * 1024 {+ owned = {+ let mut v = bytes.to_vec();+ v.resize(1024 * 1024, 0xFF);+ v+ };+ Cursor::new(owned.as_slice())+ } else {+ Cursor::new(bytes)+ };+ let mut comp = cfb::CompoundFile::open(cursor).map_err(|e| anyhow!("open OLE2: {e}"))?;++ // Each non-housekeeping root storage is a component.+ let comps: Vec<String> = comp+ .read_root_storage()+ .filter_map(|e| {+ if !e.is_storage() {+ return None;+ }+ let n = e.name().to_string();+ if n.starts_with('\u{1}') || n == "Library" || n == "FileVersionInfo" {+ return None;+ }+ Some(n)+ })+ .collect();+ if comps.is_empty() {+ bail!("no component storages found in SchLib");+ }++ // Single-part library is the common chip-fetcher case: take the first.+ let name = &comps[0];+ let mut data = Vec::new();+ comp.open_stream(format!("/{name}/Data"))+ .map_err(|e| anyhow!("open /{name}/Data: {e}"))?+ .read_to_end(&mut data)?;+ parse_component_stream(name, &data)+}++fn parse_component_stream(comp_name: &str, d: &[u8]) -> Result<serde_json::Value> {+ let mut pins: Vec<serde_json::Value> = Vec::new();+ let mut description = String::new();+ let mut part_count = 1i64;+ let mut body: Option<serde_json::Value> = None;++ let mut i = 0usize;+ while i + 4 <= d.len() {+ let plen = u16::from_le_bytes([d[i], d[i + 1]]) as usize;+ let is_text = i + 4 < d.len() && d[i + 4] == b'|';+ i += 4;+ if plen == 0 || i + plen > d.len() {+ break;+ }+ let payload = &d[i..i + plen];+ i += plen;++ if is_text {+ let txt = String::from_utf8_lossy(payload);+ let rec = field(&txt, "RECORD").unwrap_or_default();+ match rec.as_str() {+ "1" => {+ description = field(&txt, "ComponentDescription").unwrap_or_default();+ part_count = field(&txt, "PartCount").and_then(|v| v.parse().ok()).unwrap_or(1);+ }+ "14" => {+ // Rectangle = the symbol body. Coords are |Location.X|.Y|Corner.X|.Y.+ let lx = field(&txt, "Location.X").and_then(|v| v.parse::<f64>().ok());+ let ly = field(&txt, "Location.Y").and_then(|v| v.parse::<f64>().ok());+ let cx = field(&txt, "Corner.X").and_then(|v| v.parse::<f64>().ok());+ let cy = field(&txt, "Corner.Y").and_then(|v| v.parse::<f64>().ok());+ if let (Some(lx), Some(ly), Some(cx), Some(cy)) = (lx, ly, cx, cy) {+ body = Some(serde_json::json!({+ "x1": lx, "y1": ly, "x2": cx, "y2": cy+ }));+ }+ }+ _ => {}+ }+ continue;+ }++ if payload.first() == Some(&0x02) {+ pins.push(parse_pin(payload));+ }+ }++ if pins.is_empty() {+ bail!("no pins parsed from component {comp_name}");+ }++ Ok(serde_json::json!({+ "module_name": comp_name,+ "description": description,+ "part_count": part_count,+ "pin_count": pins.len(),+ "pins": pins,+ "body": body,+ "_source": "altium_schlib",+ }))+}++/// Decode one binary pin record payload → pin JSON.+fn parse_pin(p: &[u8]) -> serde_json::Value {+ let i16at = |o: usize| p.get(o..o + 2).map(|b| i16::from_le_bytes([b[0], b[1]]) as i64).unwrap_or(0);+ let conglom = p.get(14).copied().unwrap_or(0);+ let length = i16at(15);+ let x = i16at(17);+ let y = i16at(19);+ // strings: Name then Designator, starting at offset 26+ let mut j = 26usize;+ let name = read_pstr(p, &mut j);+ let designator = read_pstr(p, &mut j);+ // Orientation: PinConglomerate bits 0..1 → 0/90/180/270°.+ let rotation = (conglom & 0x03) as i64 * 90;+ serde_json::json!({+ "number": designator,+ "name": name,+ "x": x,+ "y": y,+ "length": length,+ "rotation": rotation,+ "conglomerate": conglom,+ })+}++/// `<u8 len><bytes>` length-prefixed string.+fn read_pstr(d: &[u8], i: &mut usize) -> String {+ if *i >= d.len() {+ return String::new();+ }+ let l = d[*i] as usize;+ *i += 1;+ if *i + l > d.len() {+ return String::new();+ }+ let s = String::from_utf8_lossy(&d[*i..*i + l]).to_string();+ *i += l;+ s+}++/// Pull `KEY=VALUE` out of a `|KEY=VAL|KEY=VAL` Altium text record.+fn field(rec: &str, key: &str) -> Option<String> {+ let needle = format!("{key}=");+ for part in rec.split('|') {+ if let Some(v) = part.strip_prefix(&needle) {+ return Some(v.trim().to_string());+ }+ }+ None+}++// ------------------------------------------------------------------------+// Native SVG renderer — draws the symbol directly from the parsed pins, the+// same way altium-pcblib renders footprints. No KiCad, no service round-trip.+// ------------------------------------------------------------------------++/// Altium SCH coordinate → mm. Pin pitch is 2560 internal units = 100 mil =+/// 2.54 mm (the schematic-grid standard), so one unit = 2.54/2560 mm.+const MM_PER_UNIT: f64 = 2.54 / 2560.0;++/// Render the parsed symbol to a standalone SVG. Pins are grouped into a left+/// and right bank by X sign and stacked in designator order at a uniform pitch+/// — a clean, readable schematic symbol that preserves which side each pin is+/// on and its name/number (the data that actually matters for comparison).+pub fn render_symbol_svg(sym: &serde_json::Value) -> String {+ let module = sym.get("module_name").and_then(|v| v.as_str()).unwrap_or("?");+ let empty = vec![];+ let pins = sym.get("pins").and_then(|v| v.as_array()).unwrap_or(&empty);++ // Split into banks by X sign; preserve native vertical order (top→bottom).+ let mut left: Vec<&serde_json::Value> = Vec::new();+ let mut right: Vec<&serde_json::Value> = Vec::new();+ for p in pins {+ let x = p.get("x").and_then(|v| v.as_i64()).unwrap_or(0);+ if x < 0 { left.push(p); } else { right.push(p); }+ }+ let key = |p: &&serde_json::Value| -> i64 { -p.get("y").and_then(|v| v.as_i64()).unwrap_or(0) };+ left.sort_by_key(key);+ right.sort_by_key(key);++ let pitch = 2.54_f64; // mm between pins+ let rows = left.len().max(right.len()).max(1) as f64;+ let body_w = 38.0_f64;+ let body_h = rows * pitch + pitch; // one row of padding top+bottom+ let pin_len = 7.62_f64;+ let name_pad = 1.4_f64;++ let total_w = body_w + 2.0 * pin_len + 44.0; // room for pin names outside? names go inside+ let total_h = body_h + 8.0;+ let ox = total_w / 2.0; // body centered horizontally+ let bx1 = ox - body_w / 2.0;+ let bx2 = ox + body_w / 2.0;+ let by1 = 4.0;+ let by2 = by1 + body_h;++ let mut s = String::new();+ s.push_str(&format!(+ r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {total_w:.1} {total_h:.1}" width="{:.0}" height="{:.0}" font-family="monospace">"#,+ total_w * 14.0, total_h * 14.0+ ));+ s.push('\n');+ s.push_str(&format!(r##"<rect x="0" y="0" width="{total_w:.1}" height="{total_h:.1}" fill="#0d1117"/>"##));+ // body+ s.push_str(&format!(+ r##"<rect x="{bx1:.2}" y="{by1:.2}" width="{:.2}" height="{:.2}" fill="#10212a" stroke="#00b8b1" stroke-width="0.25"/>"##,+ bx2 - bx1, by2 - by1+ ));+ s.push('\n');++ let draw = |bank: &Vec<&serde_json::Value>, is_left: bool, out: &mut String| {+ for (idx, p) in bank.iter().enumerate() {+ let num = p.get("number").and_then(|v| v.as_str()).unwrap_or("");+ let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");+ let cy = by1 + pitch + idx as f64 * pitch;+ let (x_body, x_tip, anchor, nx) = if is_left {+ (bx1, bx1 - pin_len, "start", bx1 + name_pad)+ } else {+ (bx2, bx2 + pin_len, "end", bx2 - name_pad)+ };+ // pin line+ out.push_str(&format!(+ r##"<line x1="{x_body:.2}" y1="{cy:.2}" x2="{x_tip:.2}" y2="{cy:.2}" stroke="#7cb6ff" stroke-width="0.18"/>"##+ ));+ // pin number above the line, near the tip+ let numx = if is_left { x_tip + 0.6 } else { x_tip - 0.6 };+ let numanchor = if is_left { "start" } else { "end" };+ out.push_str(&format!(+ r##"<text x="{numx:.2}" y="{:.2}" font-size="1.0" fill="#8b949e" text-anchor="{numanchor}">{}</text>"##,+ cy - 0.5, esc(num)+ ));+ // pin name inside the body+ let display = altium_name_to_plain(name);+ out.push_str(&format!(+ r##"<text x="{nx:.2}" y="{:.2}" font-size="1.25" fill="#e6edf3" text-anchor="{anchor}">{}</text>"##,+ cy + 0.45, esc(&display)+ ));+ out.push('\n');+ }+ };+ draw(&left, true, &mut s);+ draw(&right, false, &mut s);++ // header+ s.push_str(&format!(+ r##"<text x="{ox:.2}" y="{:.2}" font-size="1.5" fill="#00e6dc" text-anchor="middle">{} ({} pins)</text>"##,+ by1 - 1.2, esc(module), pins.len()+ ));+ let _ = MM_PER_UNIT;+ s.push_str("\n</svg>\n");+ s+}++/// Altium overbar/special markup → plain readable text. Altium marks an active-+/// low signal by following each char with a backslash (`C\S\` = C̄S̄); collapse+/// those so a name reads as "CS".+fn altium_name_to_plain(s: &str) -> String {+ s.replace('\\', "")+}++fn esc(s: &str) -> String {+ s.replace('&', "&").replace('<', "<").replace('>', ">")+}++// ------------------------------------------------------------------------+// .kicad_sym emitter — for EXPORT only (when the user wants KiCad output).+// Rendering uses the native SVG above; this is the convert-on-export path.+// ------------------------------------------------------------------------++/// Emit a `.kicad_sym` from the parsed symbol. Banks pins left/right like the+/// SVG so the exported KiCad symbol is laid out sensibly.+pub fn to_kicad_sym(sym: &serde_json::Value, lib_name: &str) -> String {+ let empty = vec![];+ let pins = sym.get("pins").and_then(|v| v.as_array()).unwrap_or(&empty);+ let mut left: Vec<&serde_json::Value> = Vec::new();+ let mut right: Vec<&serde_json::Value> = Vec::new();+ for p in pins {+ let x = p.get("x").and_then(|v| v.as_i64()).unwrap_or(0);+ if x < 0 { left.push(p); } else { right.push(p); }+ }+ let key = |p: &&serde_json::Value| -> i64 { -p.get("y").and_then(|v| v.as_i64()).unwrap_or(0) };+ left.sort_by_key(key);+ right.sort_by_key(key);++ let pitch = 2.54_f64;+ let rows = left.len().max(right.len()).max(1) as f64;+ let half_h = (rows * pitch) / 2.0 + pitch;+ let half_w = 19.05_f64;+ let pin_len = 5.08_f64;++ let mut body = String::new();+ let mut emit = |bank: &Vec<&serde_json::Value>, is_left: bool, out: &mut String| {+ for (idx, p) in bank.iter().enumerate() {+ let num = p.get("number").and_then(|v| v.as_str()).unwrap_or("");+ let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");+ let cy = half_h - pitch - idx as f64 * pitch;+ let (px, rot) = if is_left { (-half_w - pin_len, 0) } else { (half_w + pin_len, 180) };+ out.push_str(&format!(+ " (pin bidirectional line (at {px:.2} {cy:.2} {rot}) (length {pin_len:.2})\n (name \"{}\" (effects (font (size 1.27 1.27)))) (number \"{}\" (effects (font (size 1.27 1.27)))))\n",+ kicad_name(name), kesc(num)+ ));+ }+ };+ emit(&left, true, &mut body);+ emit(&right, false, &mut body);++ format!(+ "(kicad_symbol_lib (version 20211014) (generator altium-schlib)\n (symbol \"{lib_name}\" (in_bom yes) (on_board yes)\n (property \"Reference\" \"U\" (at 0 {:.2} 0) (effects (font (size 1.27 1.27))))\n (property \"Value\" \"{lib_name}\" (at 0 {:.2} 0) (effects (font (size 1.27 1.27))))\n (symbol \"{lib_name}_1_1\"\n (rectangle (start {:.2} {:.2}) (end {:.2} {:.2}) (stroke (width 0.254) (type default)) (fill (type background)))\n{body} )\n )\n)\n",+ half_h + 1.27, -half_h - 1.27, -half_w, half_h, half_w, -half_h,+ )+}++/// KiCad uses `~{X}` for an overbar; Altium's `X\` per-char → wrap the whole+/// name in one overbar group when it's marked active-low.+fn kicad_name(s: &str) -> String {+ if s.contains('\\') {+ format!("~{{{}}}", s.replace('\\', ""))+ } else {+ kesc(s)+ }+}+fn kesc(s: &str) -> String {+ s.replace('"', "'")+}
uninstall.shadded+3@@ -0,0 +1,3 @@+#!/usr/bin/env bash+rm -rf "$HOME/.claude/skills/adom-altium-schlib"+echo "Removed adom-altium-schlib skill"