---
name: altium-pcblib
description: >
  Native Rust crate that parses Altium .PcbLib (and .IntLib) binary footprint
  libraries — OLE2 Compound File container with Altium's reverse-engineered
  binary record format inside. Emits KiCad-shaped pad JSON so chipsmith,
  concur, and any other Adom tool that needs to read Altium binary footprints
  pulls from one source of truth instead of re-implementing OLE2 walking +
  binary record decoding.
  Public Rust API: parse_altium_pcblib_path(&Path), parse_altium_pcblib_bytes(&[u8]),
  render_footprint_svg(&Value), render_all_footprints_svg(&Value).
  Bundled CLI: altium-pcblib-svg <path> [--all] [-o out.svg] — emits the
  authoritative parser-side SVG so a downstream renderer disagreeing with
  the SVG is provably the bug, not the parser.
  Source is private (github.com/adom-inc/altium-pcblib); depend via
  Cargo path in the same workspace, or git URL on a build container.
  Trigger words: altium pcblib, altium intlib, parse altium binary footprint,
  altium ole2, altium compound file, altium binary record, .PcbLib, .IntLib,
  altium pad parser, read altium footprint in rust, altium-pcblib, altium
  cross-source, footprint consensus altium, altium svg debug, altium-pcblib-svg,
  authoritative footprint render, debug altium footprint, why is my altium pad
  rotated wrong, altium rotation convention, altium fat overshoot, altium pcblib
  malformed fat, multi-variant altium footprint, MFG vs IPC variant, RGF0040E.
---

# altium-pcblib — Adom's shared Altium .PcbLib parser (Rust crate)

`altium-pcblib` is a private Adom Rust crate that reads Altium Designer's
binary footprint libraries (`.PcbLib`, and the PCB stream embedded in
`.IntLib`) and emits per-pad JSON shaped like KiCad's `parse_kicad_mod`
output. Used by `adom-chipsmith` (Tab 4 cross-source agreement) and
`concur` (cross-source consensus check) so neither has to re-implement
OLE2 / CFBF walking or Altium's reverse-engineered binary record format.

> **This is a skill, not a library page.** The wiki's `library` type means
> "EDA part bundle = symbol + footprint + 3D + variants + pad↔pin mapping".
> `altium-pcblib` is a Rust crate, so it lives as a skill that points at the
> private GH repo. See `adom-wiki` skill → "What each `<type>` means in Adom".

## Where the source lives

- Private GitHub repo: <https://github.com/adom-inc/altium-pcblib>
- Local workspace path (Adom container): `/home/adom/project/altium-pcblib`
- Cargo description (matches what's in `Cargo.toml`):
  > "Native Rust parser for Altium .PcbLib (and .IntLib) footprint libraries — extracts pads as KiCad-shaped JSON"

## Depending on it from another Adom Rust tool

In the workspace (chipsmith, concur, anything else under `/home/adom/project/`):

```toml
[dependencies]
altium-pcblib = { path = "../altium-pcblib" }
```

On a build container that doesn't have the workspace checkout:

```toml
[dependencies]
altium-pcblib = { git = "https://github.com/adom-inc/altium-pcblib", branch = "main" }
```

Single direct dep (`cfb`, the OLE2 reader) plus `anyhow` and `serde_json`.

## Public Rust API

```rust
// Parse from a file on disk.
pub fn parse_altium_pcblib_path(path: &Path) -> anyhow::Result<serde_json::Value>;

// Parse from raw bytes (handy for include_bytes! tests and HTTP bodies).
pub fn parse_altium_pcblib_bytes(bytes: &[u8]) -> anyhow::Result<serde_json::Value>;

// Render the parser's view of the footprint as standalone SVG. THIS IS THE
// AUTHORITATIVE GROUND TRUTH — if a downstream tool draws something different,
// the downstream tool is the bug.
pub fn render_footprint_svg(footprint: &serde_json::Value) -> String;

// Render primary + every alternate variant, stacked vertically in one SVG.
pub fn render_all_footprints_svg(parsed: &serde_json::Value) -> String;
```

The returned `Value` is shaped exactly like
`adom-chipsmith::view_library::parse_kicad_mod_text` output:

```json
{
  "module_name": "D8",
  "pad_count": 8,
  "pads": [{
    "number": "1", "type": "smd", "shape": "rect",
    "at": [-2.4638, -1.905, 0.0],
    "size": [1.9812, 0.5588],
    "layers": ["F.Cu", "F.Paste", "F.Mask"],
    "drill": null
  }],
  "silk_markers": [],
  "pin1_silk": { "found": false },
  "_source": "altium_pcblib",
  "_alternates": [ /* other footprints in the lib, sorted by pad_count desc */ ]
}
```

## CLI bin: `altium-pcblib-svg`

Single binary that ships with the crate. Use it any time you suspect a
downstream renderer is drawing pads wrong:

```bash
# Primary footprint, SVG to stdout.
altium-pcblib-svg /path/to/Foo.PcbLib > foo.svg

# Primary + every alternate variant stacked vertically.
altium-pcblib-svg /path/to/Foo.PcbLib --all -o foo-all.svg
```

Conventions baked into the SVG:

- 1 SVG unit = 1 mm. Open it in any browser at 50 px/mm by default.
- Y-down (matches both KiCad and our emitted JSON). Altium's Y-up is flipped at parse time.
- Rotation applied via `transform="rotate(deg cx cy)"` clockwise on screen — same direction KiCad displays.
- Pad numbers stay un-rotated for readability regardless of pad rotation.
- F.Cu pads = orange. B.Cu pads = blue. Thru-hole = green. Drill holes = white circle on top.
- Origin crosshair at (0, 0) with 1 mm arms — sanity-check pad placement.
- Footprint name + pad count in the top-left corner.

## Integration patterns

### chipsmith Tab 4 — cross-source agreement matrix

`adom-chipsmith/src/cli/view_library.rs` calls into the parser when it sees a
`<MPN>.PcbLib` sibling next to the `.kicad_mod`:

```rust
let altium_pcblib = args.dir.join(format!("{mpn}.PcbLib"));
if altium_pcblib.exists() && !sources.contains_key("altium") {
    match altium_pcblib::parse_altium_pcblib_path(&altium_pcblib) {
        Ok(parsed) => sources.insert("altium".to_string(), parsed),
        Err(e) => err(format!("altium parse failed: {e}")),
    }
}
```

The result drops straight into the same map that holds the KiCad / Fusion /
Datasheet entries, so the Tab 4 matrix lights up a fourth column with no
adapter needed. Alternates ride along under `_alternates` so the UI can
offer a variant picker.

### concur — adapter to lean Part shape

`concur/src/parsers/altium.rs` adapts the rich pad JSON down to concur's
lean `Part` shape (concur cares about pad designators + module name only):

```rust
pub fn parse(path: &Path) -> Result<Part> {
    let parsed = altium_pcblib::parse_altium_pcblib_path(path)?;
    let module_name = parsed.get("module_name").and_then(|v| v.as_str()).map(String::from);
    let pads: Vec<Pad> = parsed.get("pads").and_then(|v| v.as_array()).map(|arr| {
        arr.iter()
            .filter_map(|p| p.get("number").and_then(|v| v.as_str()).map(String::from))
            .map(|number| Pad { number })
            .collect()
    }).unwrap_or_default();
    Ok(Part {
        source: "altium".into(),
        source_part_name: module_name.clone(),
        pads,
        pins: Vec::new(),
        package: module_name.map(|raw_name| PackageInfo { raw_name, body_x_mm: None, body_y_mm: None }),
    })
}
```

For multi-variant `.PcbLib`s (DRV8316RRGFR has IPC_A / IPC_B / IPC_C / MFG),
concur's `parse_with_consensus_hint` walks primary + every alternate and picks
the variant whose pad count is closest to what the other parsed sources agree
on, then tiebreaks against names containing `MFG` / `DEBUG` / `TEST` / `FIDUCIAL`
(manufacturing layouts with extra non-pinout pads).

## Format quirks worth knowing

These are the things that bit us during reverse-engineering — they're
encoded in the parser, but if you extend it or debug a weird chip, here's
what's going on.

### 1. Some Altium .PcbLibs have FAT overshoot

INA226AIDGSR's PcbLib declares 256 FAT entries but only has 213 sectors.
The `cfb` crate refuses with `Malformed FAT … FAT has N entries, but file
has only M sectors`. The fix the parser uses: pad the input bytes to 1 MiB
with `0xFF` (CFBF "free sector" sentinel) before opening. The unreferenced
trailing sectors don't hold real data, so this is safe.

### 2. Per-record-type sub-block count

Altium records aren't uniform `[type][len][payload]`. Different record
types carry different numbers of `[u32 len][bytes]` sub-blocks:

- Pad (0x02): **6** sub-blocks (designator, layer-name pstring, four metadata blocks).
- Text (0x05): **2** sub-blocks (main + style trailer).
- Track / Arc / Via / Fill / Region / ComponentBody: **1** sub-block.

If you hit an unknown type byte mid-stream, the parser stops walking
gracefully — pads always come first, so by then we already have what we need.

### 3. Pad block-5 layout (the 194-byte v3 / 202-byte v4 pad data)

```
[0]      u8 layer (1 = TOP_CU, 32 = BOTTOM_CU, 21 = TOP_OVERLAY, ...)
[1]      u8 net/flag (0x08 in v3, 0x0c in v4 — version probe)
[2]      u8 reserved
[3..13]  10 bytes of 0xFF (component / net id sentinels)
[13..17] i32 LE x position (units = 1 / 10000 mil = 2.54e-6 mm)
[17..21] i32 LE y position (Altium Y-up — flipped to KiCad Y-down at parse)
[21..29] u32 LE x size, y size — top layer
[29..37] u32 LE x size, y size — mid layer (multi-layer thru-hole pads)
[37..45] u32 LE x size, y size — bottom layer
[45..49] u32 LE hole size (0 → SMD; > 0 → thru-hole)
[49..52] u8 × 3 shape codes (1 round, 2 rect, 3 octagonal, 9 rounded-rect)
[52..60] f64 LE rotation in degrees — ONLY in 202-byte v4 layout
```

Past offset 60 the layout drifts between Altium releases (paste / mask
offsets, plated bool, GUIDs, primitive index). The parser stops at 60.

### 4. Two equally valid rotation conventions in the wild

This is the one that *looks* like a parser bug but isn't. Altium gives
library authors freedom in how to encode a rotated pad:

- **Convention A (matches KiCad):** store the pad as a "vertical stick"
  (xsize < ysize), set rotation = 90° on the side pads. DRV8316's IPC_A
  variant uses this.
- **Convention B (post-rotation):** store the physical post-rotation
  dimensions (xsize > ysize for a horizontal pad), set rotation = 0.
  DRV8316's MFG variant + INA226's v3 layout both use this.

Both render to the same physical pad on the PCB. A downstream tool that
compares `size[]` element-wise without applying rotation will see a 90°
mismatch in convention B → KiCad. The fix lives in the consumer (compute
the post-rotation bbox before comparing), **not** in this parser.

When in doubt about whether your downstream is the bug, run
`altium-pcblib-svg` on the file — that's the parser's authoritative view.

### 5. Y-axis flip

Altium Y-up vs KiCad Y-down. The parser flips Y at parse time so emitted
`at[1]` is in KiCad convention. **Do not flip again downstream.**

### 6. Round shape with unequal w/h → "oval"

KiCad's `circle` implies w == h. Altium's "round" (shape byte 1) covers
both true circles and stadium-shaped SMD pads with rounded ends. The
parser specializes: shape 1 with `(xsize - ysize).abs() < 1e-3` → `circle`,
otherwise → `oval` (KiCad's stadium).

## Visual proof — what "right" looks like

The crate ships three reference SVGs from the test corpus, generated by
`altium-pcblib-svg` (and reproducible any time):

- `playbooks/lm358d-soic8.svg` — SOIC-8 with 8 horizontal pads, two
  columns, rot=0 throughout. Sanity baseline.
- `playbooks/ina226-vssop10.svg` — VSSOP-10 v3 layout. xsize=1.4 mm,
  ysize=0.3 mm, rot=0 (Convention B above). Same physical pad as KiCad's
  rot=90 + size (0.3, 1.4) encoding.
- `playbooks/drv8316-all-variants.svg` — DRV8316RRGFR rendered with
  `--all`, showing the 41-pad IPC_A / IPC_B / IPC_C variants and the
  57-pad MFG variant stacked. The IPC variants match KiCad pin-for-pin
  (Convention A); MFG has extra fiducials, test points, and EP heatsink
  vias that don't appear in the chip's electrical pinout.

### chipsmith integration screenshot — DRV8316 in the 3D viewer

When the parser feeds chipsmith Tab 4, the Altium footprint appears as a
first-class Outline citizen alongside the KiCad and Fusion footprints. The
chipsmith view of `DRV8316RRGFR.step` shows:

- **Outline panel:** `KiCad footprint (41 pads)`, `Fusion (.lbr) footprint
  (41 pads)`, `Altium (.PcbLib) footprint (57)` — selected, drawing
  the MFG variant — plus `Heatsink (15 vias + EP pour)` and `Faux PCB
  (1.6 mm FR4)`.
- **3D canvas:** the full QFN-40 chip body with all 40 perimeter pads in
  their correct positions, the EP centred under the body, and the heatsink
  via grid drilled through the 1.6 mm FR4 plate. Tooltip on the FR4 plate
  shows `size 15.40 × 1.60 × 17.05 mm`.

The visual confirms three things at once: pads land in the right spots,
the multi-variant picker surfaces all four package options, and the
overlay layers stack correctly in the chipsmith Outline.

## Trouble-shooting checklist when a downstream renders pads wrong

1. **Run the authoritative SVG first.** `altium-pcblib-svg <path>.PcbLib > /tmp/auth.svg`. If pads are right in the SVG, the bug is downstream — not in this crate.
2. **Check which variant got picked.** `parsed.module_name` plus `_alternates[*].module_name`. Manufacturing variants often have extra pads vs the IPC pinout variant; concur uses `parse_with_consensus_hint` to pick the right one.
3. **Check rotation convention.** If pad 1 looks correct but pad 13 looks rotated 90° wrong, you're probably hitting Convention B vs A (see Format quirks #4) — your downstream is comparing or rendering size literally without applying rotation.
4. **Check Y direction.** If pads are mirrored top-to-bottom, your downstream double-flipped Y. Parser already emits KiCad Y-down.
5. **Scale.** SVG units are mm. If the rendered size is off by 1000×, the consumer is treating mm as µm or similar.

## Adding a new chip to the test corpus

When a new Altium PcbLib comes through chip-fetcher and you want to add
it as a regression test:

1. Drop the `.PcbLib` next to the existing test inputs (or use
   `chip-fetcher/library/<MPN>/`).
2. Run `altium-pcblib-svg <path> -o /tmp/new-chip.svg` and eyeball it.
3. Compare to the chip's `.kicad_mod` (or vendor-published IPC drawing).
4. If correct, add an `include_bytes!` test in `src/lib.rs::tests` like
   `lm358d_decodes_eight_pads_pad1_at_top_left` — assert pad count + a
   couple of representative positions.
