This document describes the binary format of Altium Designer .PcbLib (PCB footprint library) files
as implemented by this crate. Every offset below is verified against the byte-level
reverse-engineering campaign (goldens + the pyaltiumlib oracle) and the current reader/writer in
src/altium/pcblib/; where a field exists on disk but is not surfaced by our model it is explicitly
marked unmodelled rather than omitted.
Note: Cross-referenced against AltiumSharp, pyAltiumLib and Altium-authored sample libraries. See References for links.
PcbLib files are OLE Compound Documents (CFB format, OLE v3 with 512-byte sectors) containing:
/
├── FileHeader # Binary version string (53 bytes)
├── Library/
│ ├── Header # u32: record count (always 1)
│ ├── Data # Params block + component count + names
│ └── Models/
│ ├── Header # u32: embedded model count
│ ├── Data # Model parameter records
│ ├── 0 # First embedded model (zlib-compressed STEP)
│ ├── 1 # Second embedded model
│ └── ...
└── {ComponentName}/ # One storage per footprint
├── Header # u32: exact primitive count
├── Parameters # Block-prefixed pipe-delimited params
├── Data # Name block + primitives (NO end marker)
├── WideStrings # Block-prefixed ENCODEDTEXT params
└── UniqueIdPrimitiveInformation/
├── Header # u32: record count
└── Data # Block-prefixed UID records
Note: OLE version MUST be V3 (512-byte sectors). Altium Designer rejects V4 (4096-byte) files.
Real Altium libraries also carry
FileVersionInfo,EmbeddedFonts,LayerKindMapping,PadViaLibrary,ComponentParamsTOC,Textures,ModelsNoEmbed,PrimitiveGuids,UniqueIdPrimitiveInformationand — when a component name exceeds the 31-unit storage cap — a rootSectionKeysstream mapping each real name to its plain-truncated storage name. All of these are read and written back (SectionKeysformat: seeSCHLIB_FORMAT.md, identical here).
All Altium streams use these common encoding patterns:
| Pattern | Format |
|---|---|
WriteBlock(data) |
[block_len:4 LE][data] |
WriteStringBlock(str) |
[block_len:4][str_len:1][raw_string] |
WriteCStringParameterBlock(str) |
[block_len:4]["|KEY=VAL|..." + \x00] (length includes the null) |
| Pascal short string | [str_len:1][raw_string] (max 255 bytes) |
Critical rules:
- Parameter strings start with a leading
|pipe character (exceptions: the Region / ComponentBody nested param blocks and theModels/Datarecords, which have no leading pipe) - Parameter blocks ALWAYS end with
\x00, and the block length INCLUDES the null terminator - There is NO trailing pipe after the last
KEY=VALUE - All strings use Windows-1252 encoding, NOT UTF-8
The FileHeader is a 53-byte binary record with three fields (matching AltiumSharp's
PcbLibWriter.WriteFileHeader). It is NOT pipe-delimited key=value pairs. Altium Designer
rejects the library if the version double or the UniqueId block are missing.
[string_length:4 LE u32] = 27
[string_length:1 byte] = 27
[string_data:27 bytes] = "PCB 6.0 Binary Library File" # version-string block (32 bytes)
[version_double:8 LE f64] = 5.01 # raw 8 bytes, NO length prefix
[uid_length:4 LE u32] = 8
[uid_length:1 byte] = 8
[uid_data:8 bytes] = "XXXXXXXX" # 8-char UniqueId block (13 bytes)
Within each string block the 4-byte and 1-byte lengths are the SAME value (redundant). Total: 32 + 8 + 13 = 53 bytes.
Note: Component metadata (names, descriptions) is stored in
/Library/Data, not inFileHeader.
Contains library-level parameters and the component directory:
[block_len:4]["|KIND=Protel_Advanced_PCB_Library|VERSION=3.00|..." + \x00]
[component_count:4 LE u32]
// For each component:
[block_len:4][str_len:1][component_name] // WriteStringBlock
The parameter block uses the standard WriteCStringParameterBlock encoding. VERSION=3.00
requires the V9_MASTERSTACK + V9_STACK_LAYER layer-stack entries in the same block.
Beyond those, the block is the library's whole board configuration: LAYER_V8_{n}NAME,
…MECHKIND and …MECHENABLED per mechanical layer, V9_CACHE_LAYER{n}_* and
V9_STACK_LAYER{n}_* for the stack, LAYERSET{n}LAYERS for the layer sets, plus the view
state. Almost none of it is modelled, and the names in it are a designer's choice
(Mechanical 15 = Assembly Top), so a library read from disk has its block replayed
byte-for-byte on write — only FILENAME, DATE and TIME are rewritten. A library built
in memory has no block to replay and gets a template stack captured from a real
Altium-authored library; a synthesised one is rejected with "Catastrophic failure whilst
loading section Library".
[primitive_count:4 LE u32]
This is the exact primitive count. NOT count + 1.
[block_len:4]["|PATTERN=MyComponent|DESCRIPTION=My Desc" + \x00]
Standard WriteCStringParameterBlock encoding. Altium-authored libraries also carry HEIGHT,
ITEMGUID and REVISIONGUID keys here.
[block_len:4]["|ENCODEDTEXT0=72,101,108,108,111|..." + \x00]
One |ENCODEDTEXT{n}= entry per text primitive with real (non-special, non-empty) content, in
primitive order — a leading pipe per entry, NO trailing pipe. The value is the text as
comma-separated UTF-16 code units in decimal (10µF → 49,48,181,70, Ω → 937), which is
how the stream carries text the Windows-1252 Data block cannot; a text with an entry here is
read from it in preference to the Data block.
When empty (no text content): [block_len:4][\x00] (block_len = 1 — just the null terminator,
no pipe).
Header: [record_count:4 LE u32]
Data: Block-prefixed records, one per primitive that carries a unique id:
[block_len:4]["|PRIMITIVEINDEX=0|PRIMITIVEOBJECTID=Pad|UNIQUEID=ABCD1234" + \x00]
| Field | Description |
|---|---|
PRIMITIVEINDEX |
Single global 0-based ordinal over ALL primitives, in Data-stream order |
PRIMITIVEOBJECTID |
Primitive type name (Pad, Via, Track, Arc, Text, Region, Fill, ComponentBody) |
UNIQUEID |
8-character alphanumeric identifier, often empty |
Note:
PRIMITIVEINDEXis NOT a per-type index. Every primitive consumes an ordinal in the order the Data stream stores them, whether or not it has a record here, so the ordinals of the records present have gaps:LOCKFLAGS_PCBin the golden runs 0–5, 7, 8, because ordinal 6 is a track. On read the entry'sPRIMITIVEOBJECTIDmust match the primitive found at that ordinal; a mismatch (e.g. a foreign file with a different index base) is skipped rather than mis-attached.An empty
UNIQUEIDis normal — every record in the golden has one. The record marks the primitive as tracked; the value is filled in by a board, not a library. Treating an empty value as "no record" discards the stream.
Header: [record_count:4 LE u32]
Data: record_count fixed-width 24-byte records, no framing of their own:
[object_kind:4 LE u32][ordinal:4 LE u32][guid:16 bytes]
object_kind is Altium's object id — 1 arc, 2 pad, 3 via, 4 track, 5 text, 6 fill, 85 the
footprint record itself, 89 region, 90 component body. The GUID's first three fields are
little-endian (the Windows GUID struct layout).
ordinal is the same global ordinal UniqueIdPrimitiveInformation calls PRIMITIVEINDEX,
not a per-kind index: across the 22 golden footprints the ordinals are a permutation of 0..n-1
once the kind-85 record (always ordinal 0) is set aside, and PRIMPROPS has its regions at 0, 4,
5 and 6 with a pad at 1 and texts at 2 and 3. Both streams are therefore meaningless unless the
Data stream keeps the primitive order the file was written with.
Each component's Data stream contains the footprint primitives:
[name_block_len:4][str_len:1][name:str_len] # Component name block (WriteStringBlock)
[record_type:1][blocks...] # First primitive
[record_type:1][blocks...] # Second primitive
...
There is NO end-of-stream marker. Altium reads exactly the primitive count declared in the
component Header stream; a trailing 0x00 byte is mis-read as a record with object-id 0 and
breaks the stream (issue #68). Never emit one.
| ID | Type | Description |
|---|---|---|
0x01 |
Arc | Arc or circle (single 60-byte block) |
0x02 |
Pad | SMD or through-hole pad (6 blocks) |
0x03 |
Via | Via (single 321-byte block) |
0x04 |
Track | Line segment (single 49-byte block) |
0x05 |
Text | Text string (2 blocks) |
0x06 |
Fill | Filled rectangle (single 50-byte block) |
0x0B |
Region | Filled polygon (single variable-length block) |
0x0C |
ComponentBody | 3D body (single variable-length block) |
Every sub-block is length-prefixed:
[block_length:4 LE][block_data:block_length]
Block size limits (this crate):
| Block Type | Maximum Size |
|---|---|
| Standard block | 100,000 bytes |
| UniqueID record | 10,000 bytes |
| String field | 255 bytes |
Block sizes (reader minimum / writer output):
| Primitive | Reader minimum | Writer output | Notes |
|---|---|---|---|
| Pad geometry (Block 4) | 52 bytes | 202 bytes | Altium rejects pads with a shorter main block |
| Pad size/shape (Block 5) | — | 0, 651 or 320/576 bytes | See Pad |
| Track | 33 bytes | 49 bytes | Extended tail from offset 33 |
| Arc | 45 bytes | 60 bytes | Extended tail from offset 45 |
| Text geometry | 25 bytes | 252 bytes | Fixed TEXT_SR1_TEMPLATE layout |
| Via | 31 bytes | 321 bytes | Fixed VIA_SR1_TEMPLATE layout |
| Fill | 37 bytes | 50 bytes | Tail from offset 37 |
| Region | 22 bytes + params | variable | Single block incl. vertices |
| ComponentBody | — | variable | Single block incl. outline |
- Altium uses internal units: 10000 units = 1 mil = 0.0254 mm
- Conversion:
mm = internal_units / 10000.0 * 0.0254 - Reverse:
internal_units = mm / 0.0254 * 10000.0
Conversion constants:
| Constant | Value | Formula |
|---|---|---|
| MM_TO_INTERNAL | 393700.787... | 10000.0 / 0.0254 |
| INTERNAL_TO_MM | 2.54e-6 | 0.0254 / 10000.0 |
Note: Results are typically rounded to 6 decimal places (1nm resolution) to avoid floating-point noise.
| ID | Layer |
|---|---|
| 0 | No Layer |
| 1 | Top Layer |
| 2-31 | Mid Layer 1-30 (internal copper) |
| 32 | Bottom Layer |
| 74 | Multi-Layer |
| ID | Layer |
|---|---|
| 33 | Top Overlay (Silkscreen) |
| 34 | Bottom Overlay |
| 35 | Top Paste |
| 36 | Bottom Paste |
| 37 | Top Solder Mask |
| 38 | Bottom Solder Mask |
| ID | Layer |
|---|---|
| 39-54 | Internal Plane 1-16 |
| ID | Layer |
|---|---|
| 55 | Drill Guide |
| 56 | Keep-Out Layer |
| 57-72 | Mechanical 1-16 |
| 73 | Drill Drawing |
| 186-201 | Mechanical 17-32 — read only; see below |
Mechanical 17-32 have no header byte of their own. Altium stores a primitive on one of them
under byte 72 (Mechanical 16, the last the byte can hold) and names the real layer in the
V7 layer id — 72 + 0x01020014 for Mechanical 20 — or, for a region or
body, in the V7_LAYER token (MECHANICAL20). The MECH20 golden shows the pair on every
kind: pad, track, arc, text, fill, region and body. The reader lets a V7 id or token past
sixteen decide for a mechanical byte, and the writer stores such a layer the same way. Bytes
186-201 are accepted on read for files earlier versions of this crate wrote; they are never
written.
A header byte the table does not map reads as Multi-Layer and is carried as the primitive's
raw_layer_id, so a rewrite stores the byte as read rather than 74; moving the primitive to a
layer the model can name discards it.
These mechanical layers are typically configured as component layer pairs:
| ID | Mechanical | Purpose |
|---|---|---|
| 58 | M2 | Top Assembly |
| 59 | M3 | Bottom Assembly |
| 60 | M4 | Top Courtyard |
| 61 | M5 | Bottom Courtyard |
| 62 | M6 | Top 3D Body |
| 63 | M7 | Bottom 3D Body |
AI assistants should prefer these dedicated layers over generic mechanical layers.
| ID | Layer |
|---|---|
| 75 | Connect Layer |
| 76 | Background Layer |
| 77 | DRC Error Layer |
| 78 | Highlight Layer |
| 79 | Grid Color 1 |
| 80 | Grid Color 10 |
| 81 | Pad Hole Layer |
| 82 | Via Hole Layer |
| 83 | Top Pad Master |
| 84 | Bottom Pad Master |
| 85 | DRC Detail Layer |
The layer field is a plain
u8. There is no255 = Unknownlayer — any out-of-range id is simply unrecognised (an unknown layer name maps to0 = NoLayer).
Several primitives carry a derived 32-bit "v7 saved layer id" alongside the layer byte
(v7_layer_id in the writer, ported from AltiumSharp V7LayerId):
| Layer byte | V7 id |
|---|---|
| 1-31 (signal top/mid) | 0x01000000 + id |
| 32 (bottom) | 0x0100FFFF |
| 39-54 (internal plane n) | 0x01010000 + n |
| 57-72 (mechanical n) | 0x01020000 + n |
| Mechanical 17-32 (byte 72) | 0x01020011-0x01020020 |
| 33 / 34 (overlay) | 0x01030006 / 0x01030007 |
| 35 / 36 (paste) | 0x01030008 / 0x01030009 |
| 37 / 38 (solder) | 0x0103000A / 0x0103000B |
| 55 / 56 / 73 | 0x0103000C / 0x0103000D / 0x0103000E |
| 74 (multi-layer) + fallback | 0x0103000F |
All primitives start with a common header:
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | Layer ID (byte 72 for Mechanical 17-32 — see Layer IDs) |
| 1-2 | 2 | Flags word (u16, see below) |
| 3-4 | 2 | NetIndex (u16; 0xFFFF when unconnected) |
| 5-6 | 2 | PolygonIndex (u16; 0xFFFF = none) |
| 7-8 | 2 | ComponentIndex (u16; 0xFFFF = free primitive, modelled as -1) |
| 9-12 | 4 | Reserved (0xFFFFFFFF) |
For a footprint-library primitive the index fields are unused (all 0xFF) — but in a board file
they carry the net / polygon / component links. The reader and writer model all three indices; the
from-scratch defaults (net = 0xFFFF, polygon = 0xFFFF, component = -1) reproduce the 0xFF
fill byte-identically.
Flags word (bytes 1-2, u16) — on-wire bits (src/altium/pcblib/flags.rs):
| Bit | Flag | Meaning |
|---|---|---|
0x0004 |
Unlocked | Inverted — set = unlocked, clear = locked |
0x0008 |
Saved | Set on a saved primitive |
0x0020 |
TentingTop | Solder-mask tented on top |
0x0040 |
TentingBottom | Solder-mask tented on bottom |
0x0080 |
TestPointTop | Fabrication test point, top |
0x0100 |
TestPointBottom | Fabrication test point, bottom |
0x0200 |
Keepout | Keep-out primitive |
0x0001, 0x0002, 0x0010, 0x0400–0x8000 |
— | Not modelled (AltiumSharp reads them as selection / display state). Hand-authored libraries do set them — every track of a pin-header footprint carries 0x001C — so they are carried verbatim through a read-modify-write as PcbFlags::DISK_BIT_n |
A normal saved primitive therefore carries 0x000C (Saved + Unlocked), not 0x0000; a keepout
primitive carries 0x020C.
Altium clears the Unlocked bit on a primitive it marks as a fabrication test point, so a
test-point pad carries 0x0088 or 0x0108 and reads as locked as well. The writer reproduces
that rather than emitting a combination Altium never writes. This is Altium's behaviour, not a
decode artefact — see issue #334, and the golden's LOCKFLAGS_PCB pads 5-6.
The assembly test-point flags have no bit here: IsAssyTestPoint_Top / _Bottom author
without error in AD24 but a saved PcbLib pad comes back as a plain 0x000C, so they are not
stored per-primitive in a library.
Note: These are the literal bits on disk. The crate also exposes an internal abstract
PcbFlagsenum (LOCKED/KEEPOUT/TENTING_TOP/TENTING_BOTTOM/TESTPOINT_TOP/TESTPOINT_BOTTOM, with its own distinct values) for the public API; the reader/writer translate between the two. Do not confuse the abstract enum's values with the on-wire bits above.
Pads have 6 blocks:
| Block | Content |
|---|---|
| 0 | Designator string (WriteStringBlock) |
| 1 | Reserved — a 1-byte block containing 0x00 |
| 2 | Marker string |&|0 (WriteStringBlock) |
| 3 | Reserved — a 1-byte block containing 0x00 |
| 4 | Main geometry block (202 bytes from scratch; AD24 writes 194 — see below) |
| 5 | Size/shape block (empty, 651 bytes, or the legacy 320/576-byte per-layer form) |
Block 4 — main geometry. Offsets 0-60 are typed geometry; the bytes from 61 on are the
extended tail. A pad built from scratch gets the 141-byte tail of PAD_EXTENDED_TAIL_TEMPLATE
(captured from a standard Altium pad; 202 bytes in all); AD24 itself writes a 133-byte tail (194
bytes in all) with two bytes differing, so a read pad's tail is carried as raw_tail and
replayed as the base — length included — with the modelled fields overlaid:
| Offset | Size | Field | Modelled |
|---|---|---|---|
| 0-12 | 13 | Common header (layer, flags, net/poly/comp indices) | yes |
| 13-16 | 4 | X position (i32, internal units) | yes |
| 17-20 | 4 | Y position (i32) | yes |
| 21-24 | 4 | Width, top layer (i32) | yes |
| 25-28 | 4 | Height, top layer (i32) | yes |
| 29-32 | 4 | Width, middle layers (i32) | yes (TopMiddleBottom mode) |
| 33-36 | 4 | Height, middle layers (i32) | yes (TopMiddleBottom mode) |
| 37-40 | 4 | Width, bottom layer (i32) | yes (TopMiddleBottom mode) |
| 41-44 | 4 | Height, bottom layer (i32) | yes (TopMiddleBottom mode) |
| 45-48 | 4 | Hole size (i32; 0 for SMD) | yes |
| 49 | 1 | Shape, top | yes |
| 50 | 1 | Shape, middle | yes (TopMiddleBottom mode) |
| 51 | 1 | Shape, bottom | yes (TopMiddleBottom mode) |
| 52-59 | 8 | Rotation (f64, degrees) | yes |
| 60 | 1 | Is plated (0/1; an absent byte reads as plated) | yes |
| 61 | 1 | Reserved (0x00) — NOT the hole shape (that lives in Block 5 @262) |
— |
| 62 | 1 | Stack mode (0=Simple, 1=TopMiddleBottom, 2=FullStack) | yes |
| 67 | 1 | Power-plane connection style (0=Relief, 1=Direct, 2=NoConnect) | yes |
| 68-71 | 4 | Thermal-relief conductor (spoke) width (i32; default 0.254 mm) | yes |
| 72-73 | 2 | Thermal-relief spoke count (i16; default 4) | yes |
| 74-77 | 4 | Thermal-relief air gap (i32; default 0.254 mm) | yes |
| 78-81 | 4 | Power-plane relief expansion (i32; default 0.508 mm) | yes |
| 82-85 | 4 | Power-plane (anti-pad) clearance (i32; default 0.508 mm) | yes |
| 86-89 | 4 | Paste mask expansion (i32) | yes |
| 90-93 | 4 | Solder mask expansion (i32) | yes |
| 101 | 1 | Paste mask expansion cache state (TCacheState, see below) |
yes |
| 102 | 1 | Solder mask expansion cache state (TCacheState, see below) |
yes |
| 110-111 | 2 | Jumper ID (i16; 0 = not in a jumper group) |
yes |
| 114-117 | 4 | V7 layer id (derived from the pad's layer) | yes (write) |
| 121-124 | 4 | Solder-mask cache (mirrors @90) | unmodelled (template) |
| 125 | 1 | Solder-mask expansion measured from the hole edge (bool) | yes |
| 126-141 | 16 | Identity GUID A (identity_guid; replayed as read, fresh when absent) |
yes |
| 142-157 | 16 | Identity GUID B (identity_guid_b; as GUID A) |
yes |
| 162-165 | 4 | Hole positive tolerance (i32; 0x7FFFFFFF = unset) |
yes |
| 166-169 | 4 | Hole negative tolerance (i32; 0x7FFFFFFF = unset) |
yes |
| 172 | 1 | Format marker (0x1A PcbLib / 0x12 PcbDoc) |
template |
| 185 | 1 | Reserved marker (0x03 in the from-scratch template; AD24 writes 0x01, kept as read) |
template / replay |
All remaining bytes of the tail are reserved / cache values replayed verbatim — from raw_tail
for a read pad, from the template for one built from scratch.
Parsing thresholds:
| Field | Threshold | Behaviour |
|---|---|---|
| Hole size | ≤ 0.001 mm | Treated as zero (SMD pad) |
| Mask expansion | ≤ 0.0001 mm | The value reads back as absent (None), independently of the cache-state byte below |
Mask expansion cache state (TCacheState, bytes @101 / @102; via @66):
The mask-expansion byte is not an on/off switch. It is Altium's
TCacheState = (eCacheInvalid, eCacheValid, eCacheManual)(ordinals 0/1/2, read from the Advpcb.dll RTTI), and it says whether the expansion value
stored alongside it is trustworthy:
| Byte | TCacheState |
Meaning |
|---|---|---|
| 0 | eCacheInvalid |
The stored value is stale. A pad or via authored by Altium and saved without being opened again carries this. |
| 1 | eCacheValid |
The stored value is a rule result Altium has already computed. Altium leaves this behind after opening a library and resolving the expansion itself. |
| 2 | eCacheManual |
The stored value was specified by hand. |
Only eCacheManual survives a round trip through Altium. Opening a library recomputes
every rule-driven expansion from the applicable design rule, so bytes 0 and 1 are
indistinguishable afterwards — a pad written as eCacheInvalid with 0.0 and one written
as eCacheValid with 0.0 both come back as eCacheValid with the rule's 4 mil. The
value paired with byte 1 is therefore never authoritative in practice, and a wrong state
here is a fidelity difference rather than a change to the fabricated mask.
Two consequences worth knowing:
- The writer emits byte 0 for a from-scratch pad or via, matching a factory Altium primitive rather than one Altium has re-saved.
- A library re-saved by Altium is not byte-comparable with the one handed to it even when nothing was edited, because opening it resolves these caches.
Measured by scripts/Verify-MaskCacheState.ps1,
which writes all three states and reports what Altium makes of each.
Pad shapes (bytes @49-51 and in Block 5):
| ID | Shape | Notes |
|---|---|---|
| 1 | Round / Oval | An oval pad is a Round pad with width ≠ height |
| 2 | Rectangular | Sharp corners |
| 3 | Octagonal | 8-sided |
| 9 | RoundedRectangle | Written directly as id 9; requires the 651-byte Block 5 for the corner radius |
Note: For backwards compatibility with older files the reader also promotes a shape-1 pad whose Block 5 corner radius is 1-99% to RoundedRectangle.
Stack modes (@62):
| ID | Mode | Description |
|---|---|---|
| 0 | Simple | All layers mirror top layer settings; Block 5 empty |
| 1 | TopMiddleBottom | Independent top / middle / bottom sizes+shapes, carried in the MAIN block (@29-44, @50-51); Block 5 empty |
| 2 | FullStack | Complete per-layer customisation, carried in Block 5 |
Block 5 — size/shape block. Three forms are written:
-
Empty block (
block_len = 0) — plain Simple / TopMiddleBottom pads. -
Canonical 651-byte size/shape block — a Simple pad with a non-round hole or an explicit corner radius. Layout:
Offset Size Field 0-115 116 29 × i32 internal-layer X sizes 116-231 116 29 × i32 internal-layer Y sizes 232-260 29 29 × u8 internal-layer shape ids 261 1 Reserved 262 1 Hole type (0=Round, 1=Square, 2=Slot) 263-266 4 Hole slot length (i32) 267-274 8 Hole rotation (f64, degrees) 275-402 128 32 × i32 per-layer X offsets from hole centre 403-530 128 32 × i32 per-layer Y offsets from hole centre 531 1 Has-rounded-rect flag (u8 bool) 532-563 32 32 × u8 per-layer shape ids 564-595 32 32 × u8 per-layer corner radii (%) 596-627 32 Reserved (zeros) — full-stack tail starts here 628-631 4 Tail entry count (i32; we write 1) 632-635 4 Tail entry stride (i32 = 15) 636-650 15 One 15-byte entry: layer code (4=top signal), 3 flag bytes, shape id, size X (i32), size Y (i32), corner radius % (fixed 50), trailing 0 Altium NEVER emits a bare 596-byte block — every non-empty size/shape block in a golden
.PcbLibis 651 (one tail entry) or 696 (four) bytes; an under-length block makes Altium reject the pad. Multi-entry tails (count > 1) are currently unmodelled. -
Legacy per-layer block (320 or 576 bytes) — written for FullStack pads and still accepted on read:
Offset Size Field 0-255 256 32 size entries (width/height i32 pairs) 256-287 32 32 shape IDs (1 byte each) 288-319 32 32 corner radius percentages (0-100) 320-575 256 32 offset entries (x/y i32 pairs) — optional Layer index mapping: 0 = Top, 1 = Bottom, 2-31 = Mid Layers 1-30.
Note: Corner radius is stored as a percentage (0-100) of the smaller pad dimension. Default corner radius for RoundedRectangle is 50% if not specified.
Single 49-byte block:
| Offset | Size | Field | Modelled |
|---|---|---|---|
| 0-12 | 13 | Common header | yes |
| 13-16 | 4 | Start X (i32) | yes |
| 17-20 | 4 | Start Y (i32) | yes |
| 21-24 | 4 | End X (i32) | yes |
| 25-28 | 4 | End Y (i32) | yes |
| 29-32 | 4 | Width (i32) | yes |
| 33-34 | 2 | Sub-polygon index (i16; 0 in libraries) | write-only (0) |
| 35-38 | 4 | Solder mask expansion (i32) | yes |
| 39-40 | 2 | Paste mask expansion (i16 — 2 bytes, cf. the Arc's 1-byte field) | write-only (0) |
| 41-44 | 4 | V7 layer id (u32, derived from layer) | yes (write) |
| 45 | 1 | Keepout restrictions (u8) | yes |
| 46-48 | 3 | Reserved (zeros) | — |
Every Altium-authored track carries the extended tail (offsets 33-48); the reader accepts a 33-byte legacy block and defaults the tail fields.
Single 60-byte block:
| Offset | Size | Field | Modelled |
|---|---|---|---|
| 0-12 | 13 | Common header | yes |
| 13-16 | 4 | Centre X (i32) | yes |
| 17-20 | 4 | Centre Y (i32) | yes |
| 21-24 | 4 | Radius (i32) | yes |
| 25-32 | 8 | Start angle (f64, degrees) | yes |
| 33-40 | 8 | End angle (f64, degrees) | yes |
| 41-44 | 4 | Width (i32) | yes |
| 45-46 | 2 | Sub-polygon index (i16; 0 in libraries) | write-only (0) |
| 47-50 | 4 | Solder mask expansion (i32) | yes |
| 51 | 1 | Paste mask expansion (u8 — 1 byte, cf. the Track's 2-byte field) | write-only (0) |
| 52-55 | 4 | V7 layer id (u32, derived from layer) | yes (write) |
| 56 | 1 | Keepout restrictions (u8) | yes |
| 57-59 | 3 | Reserved (zeros) | — |
The reader accepts a 45-byte legacy block and defaults the tail fields.
Text has 2 blocks:
- Block 0: the fixed 252-byte geometry record (
TEXT_SR1_TEMPLATE) — the writer overlays the modelled fields onto the record as read (raw_geometry) or, from scratch, onto a canonical template captured from a real Altium text primitive, and replays every reserved byte verbatim. - Block 1: the text content as a
WriteStringBlock([u32 len][u8 str_len][Windows-1252 text]).
Block 0 — 252-byte geometry record:
| Offset | Size | Field | Modelled |
|---|---|---|---|
| 0-12 | 13 | Common header (layer, flags, net/poly/comp indices) | yes |
| 13-16 | 4 | X position (i32) | yes |
| 17-20 | 4 | Y position (i32) | yes |
| 21-24 | 4 | Height (i32) | yes |
| 25-26 | 2 | Stroke font id (u16; 1=Default, 2=Sans Serif, 3=Serif) | yes |
| 27-34 | 8 | Rotation (f64, degrees) | yes |
| 35 | 1 | Mirrored (u8 bool) | yes |
| 36-39 | 4 | Stroke line width (i32; template default 4 mil) | yes |
| 40 | 1 | IsComment flag (u8 bool; is_comment) |
yes |
| 41 | 1 | IsDesignator flag (u8 bool; is_designator) |
yes |
| 42 | 1 | Character set (u8) | unmodelled (template) |
| 43 | 1 | Base font type (0=stroke, 1=TrueType) | derived from kind @160 |
| 44 | 1 | Bold (u8 bool) | yes |
| 45 | 1 | Italic (u8 bool) | yes |
| 46-109 | 64 | Font name (UTF-16LE, null-terminated; default "Arial") | yes |
| 110 | 1 | IsInverted (u8 bool) | yes |
| 111-114 | 4 | Inverted border width (i32) | yes |
| 115-118 | 4 | WideStrings index (i32; -1 = none) | partially (reader consults the low u16 @115) |
| 119-122 | 4 | Union index (i32) | unmodelled (template) |
| 123 | 1 | UseInvertedRectangle (u8 bool) | yes |
| 124-127 | 4 | Inverted rect width (i32) | yes |
| 128-131 | 4 | Inverted rect height (i32) | yes |
| 132 | 1 | Text-box justification (u8, see below) | yes |
| 133-136 | 4 | Inverted rect text offset (i32) | yes |
| 137-140 | 4 | Barcode full width (i32) | yes (barcode only) |
| 141-144 | 4 | Barcode full height (i32) | yes (barcode only) |
| 145-148 | 4 | Barcode X margin (i32) | yes (barcode only) |
| 149-152 | 4 | Barcode Y margin (i32) | yes (barcode only) |
| 153-156 | 4 | Barcode render fields | unmodelled (template / replay) |
| 157 | 1 | Barcode symbology (barcode_kind: 0=Code 39, 1=Code 128, 2=QR, 3=Data Matrix) |
yes |
| 158 | 1 | Barcode min width | unmodelled (template / replay) |
| 159 | 1 | Barcode inverted (u8 bool) | yes (barcode only) |
| 160 | 1 | Text kind (authoritative): 0=Stroke, 1=TrueType, 2=BarCode | yes |
| 161-224 | 64 | Barcode font name (UTF-16LE, null-terminated) | yes (barcode only) |
| 225 | 1 | Barcode show-text flag (u8 bool) | yes (barcode only) |
| 226-229 | 4 | V7 layer id (u32, derived from layer) | yes (write) |
| 230-251 | 22 | Frame / snapping / snap-point fields | unmodelled (template) |
Text-box justification (@132) — Altium encodes this column-major, 1-based (0 = manual):
| ID | Position | ID | Position | ID | Position |
|---|---|---|---|---|---|
| 1 | Top Left | 4 | Top Centre | 7 | Top Right |
| 2 | Middle Left | 5 | Middle Centre | 8 | Middle Right |
| 3 | Bottom Left | 6 | Bottom Centre | 9 | Bottom Right |
The from-scratch default BottomLeft encodes to 0x03 (the template byte); ids 0 and 3 both
decode to BottomLeft.
Text kinds (@160):
| ID | Kind | Description |
|---|---|---|
| 0 | Stroke | Vector-based outline text |
| 1 | TrueType | Font-based rendering (base font type @43 = 1) |
| 2 | BarCode | Barcode (TEXT_SPECIAL golden: Code 128 with the sizing block above) |
Block 1 (Content): a length-prefixed string with the text content, a special string, or a numeric WideStrings index reference.
Special text values (inline):
| Value | Meaning |
|---|---|
.Designator |
Pad/component designator |
.Comment |
Component comment |
Note: When Block 1 is empty the reader falls back to scanning the geometry block for the special strings and to the WideStrings index at offset 115.
Filled rectangle, single 50-byte block:
| Offset | Size | Field | Modelled |
|---|---|---|---|
| 0-12 | 13 | Common header | yes |
| 13-16 | 4 | X1 (first corner, i32) | yes |
| 17-20 | 4 | Y1 (i32) | yes |
| 21-24 | 4 | X2 (second corner, i32) | yes |
| 25-28 | 4 | Y2 (i32) | yes |
| 29-36 | 8 | Rotation (f64, degrees) | yes |
| 37-40 | 4 | Solder mask expansion (i32) | yes |
| 41 | 1 | Paste mask expansion (u8) | write-only (0) |
| 42-45 | 4 | V7 layer id (u32, derived from layer) | yes (write) |
| 46 | 1 | Keepout restrictions (u8) | yes |
| 47-49 | 3 | Reserved (zeros) | — |
Filled polygon — a single size-prefixed block; an empty second block would be read by Altium as an invalid record type, silently dropping every primitive after the region.
| Offset | Size | Field |
|---|---|---|
| 0-12 | 13 | Common header (layer, flags, net/poly/comp indices) |
| 13 | 1 | Reserved (0x00) |
| 14-15 | 2 | Hole contour count (u16) |
| 16-17 | 2 | Reserved (0x00 0x00) |
| 18-21 | 4 | Parameter string length (u32, INCLUDES the null terminator) |
| 22.. | var | Parameter string (Windows-1252 C-string, no leading pipe) |
| +0 | 4 | Outline vertex count (u32) |
| +4 | 16×N | Outline vertices: N × (f64 X, f64 Y) in internal units |
| ... | var | Hole contours: hole_count × [u32 count][count × (f64 X, f64 Y)] |
Parameter string — canonical key order (no leading pipe, |-separated):
V7_LAYER={token}|NAME={name}|KIND={kind}|SUBPOLYINDEX={spi}|UNIONINDEX={uix}|ARCRESOLUTION={mil}|ISSHAPEBASED={bool}|CAVITYHEIGHT={mil}
| Key | Description | Default |
|---|---|---|
V7_LAYER |
Canonical layer token from Altium's own vocabulary — TOP, MECHANICAL4, PLANE3, … (see below) |
matches the layer byte, except on a board cutout |
NAME |
Region name | empty |
KIND |
Region kind: 0 copper, 1 polygon cutout, 2 named region, 4 cavity (a board cutout keeps KIND=0 and carries ISBOARDCUTOUT=TRUE instead — PRIMPROPS golden) |
0 |
SUBPOLYINDEX |
Sub-polygon index | -1 |
UNIONINDEX |
Union index | 0 |
ARCRESOLUTION |
Arc resolution, mil-suffixed (0mil, 0.5mil) |
0mil |
ISSHAPEBASED |
TRUE / FALSE |
FALSE |
CAVITYHEIGHT |
Cavity height, mil-suffixed | 0mil |
NET |
Numeric net index (read-side override of the header index) | absent |
Board-region keys the model does not consume (LAYER, KEEPOUT, ISBOARDCUTOUT, ...) are
captured verbatim on read and re-emitted after the canonical set, so a read-modify-write does not
drop them.
Warning: Altium resolves a Region's layer from the
V7_LAYERtoken, NOT the header layer byte, and the token is a fixed vocabulary rather than the display name with the spaces taken out.TOPLAYERandBOTTOMLAYERresolve to nothing, and an unresolved token leaves the region on Top Layer — so the bottom-side case is a silent side swap.
The vocabulary, held in Advpcb.dll as UTF-16LE strings. MECHANICAL, MID and PLANE take
a number; the rest are literals.
| Layer id | Token | Layer id | Token | |
|---|---|---|---|---|
| 1 | TOP |
39-54 | PLANE1-PLANE16 |
|
| 2-31 | MID1-MID30 |
55 | DRILLGUIDE |
|
| 32 | BOTTOM |
56 | KEEPOUT |
|
| 33 | TOPOVERLAY |
57-72 | MECHANICAL1-MECHANICAL16 |
|
| 34 | BOTTOMOVERLAY |
73 | DRILLDRAWING |
|
| 35 | TOPPASTE |
74 | MULTILAYER |
|
| 36 | BOTTOMPASTE |
75 | CONNECT |
|
| 37 | TOPSOLDER |
Mechanical 17-32 (byte 72) | MECHANICAL17-MECHANICAL32 |
|
| 38 | BOTTOMSOLDER |
Board cutouts are the one case where the token and the layer byte disagree. AD24 saves a
cutout authored on the top layer with layer byte 56 (keep-out) and
LAYER=KEEPOUT|KEEPOUT=TRUE|ISBOARDCUTOUT=TRUE, but leaves V7_LAYER=TOP — the layer it was
drawn on, recorded nowhere else in the record. The golden's REGION_CUTOUT and PRIMPROPS
cutouts both show this. Deriving the token from the layer byte would write KEEPOUT and lose it,
so a divergent token is kept as read.
Vertex format: each vertex is two IEEE 754 doubles (X then Y) in internal units. Values are whole internal units in real files; the reader rounds to integers before converting to mm.
3D body — a single size-prefixed block (verified against AltiumSharp and the BODY_3D /
BODY_3D_STEP golden libraries). The outline lives inside the block; there are NO separate
snap-point or reserved blocks, and there is no MODEL.SNAPCOUNT parameter.
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | Layer ID (e.g. 62 for Top 3D Body) |
| 1-2 | 2 | Record-type marker (0x0C 0x00) |
| 3-12 | 10 | Net/polygon/component indices + reserved (0xFF fill for a free body) |
| 13-17 | 5 | Zeros |
| 18-21 | 4 | Parameter string length (u32, INCLUDES the null) |
| 22.. | var | Parameter string (Windows-1252 C-string, no leading pipe, starts V7_LAYER=) |
| +0 | 4 | Outline vertex count (u32) |
| +4 | 16×N | Outline vertices: N × (f64 X, f64 Y) in internal units |
Warning: Outline coordinates MUST be whole internal units. Altium silently drops a body whose outline has fractional internal coordinates.
Parameter keys (writer canonical order; values from the typed model):
| Key | Description | Example |
|---|---|---|
V7_LAYER |
Canonical layer token; names the header layer byte, except that Mechanical 17-32 sit under byte 72 with the token naming the layer (MECH20 golden) |
MECHANICAL6 |
NAME |
Body name | (single space) |
KIND |
Body kind | 0 |
SUBPOLYINDEX |
Sub-polygon index | -1 |
UNIONINDEX |
Union index | 0 |
ARCRESOLUTION |
Arc resolution (emitted TWICE — repeated after BODYPROJECTION, matching Altium) |
0.5mil |
ISSHAPEBASED |
Shape-based flag | FALSE |
CAVITYHEIGHT |
Cavity height | 0mil |
STANDOFFHEIGHT |
Standoff height (mil-suffixed, trimmed) | 0mil |
OVERALLHEIGHT |
Overall height | 15.748mil |
BODYPROJECTION |
Body projection | 0 |
BODYCOLOR3D |
3D body colour | 8421504 |
BODYOPACITY3D |
3D body opacity | 1.000 |
IDENTIFIER |
Body name as comma-separated decimal Unicode code points (µΩ电 = 181,937,30005; empty stays empty) |
`` |
TEXTURE, TEXTURECENTERX/Y, TEXTURESIZEX/Y, TEXTUREROTATION |
Texture fields, round-tripped verbatim (a UI-authored body can carry a rotation of 9.00000000000000E+0001, leading space included); from-scratch defaults 0mil and a zero rotation |
|
MODELID |
Model GUID. A STEP reference the library does not embed takes one of two forms: UI-authored, empty (MODELID= followed by MODEL.CHECKSUM=0, MODEL.EMBED=FALSE, MODEL.NAME=test_0805.step, no /Library/Models entry); script-authored (IPCB_Model.Embed := False, the STEP_REF golden), a GUID with a /Library/Models/Data entry of EMBED=FALSE whose bytes are stored all the same. Both read; the writer reproduces the form it read |
{GUID} |
MODEL.CHECKSUM |
Model integrity checksum (round-tripped verbatim, see below) | 0 |
MODEL.EMBED |
TRUE / FALSE |
|
MODEL.NAME |
Model filename | RESC1005X04L.step |
MODEL.2D.X, MODEL.2D.Y |
2D placement (0mil from scratch; a read value is replayed verbatim) |
|
MODEL.2D.ROTATION |
2D rotation (degrees, 3 decimals) | 0.000 |
MODEL.3D.ROTX/Y/Z |
3D rotations (degrees, 3 decimals) | 0.000 |
MODEL.3D.DZ |
Z offset (mil-suffixed) | 15.748mil |
MODEL.MODELTYPE |
1 = model-backed (STEP) |
|
MODEL.MODELSOURCE |
Model source | Undefined |
Extruded vs model-backed — and the MODEL group's presence: the group is present exactly
when the body carries a MODELID or names a model file, which depends on the authoring route. A script-authored
extruded body (the golden's BODY3D, PRIMPROPS) has none and ends at TEXTUREROTATION; a
UI-authored extruded body (manual/identifier.PcbLib) carries a stable MODELID and the
full group with MODEL.MODELTYPE=0, MODEL.EXTRUDED.MINZ/MAXZ (standoff..overall), real
MODEL.2D.X/Y placement, and no MODELSOURCE — plus TEXTURESIZEX/Y=0.0001mil where the
scripted route writes 0mil (both round-trip verbatim). A model-backed body (EMBSTEP) uses
MODEL.MODELTYPE=1 plus MODEL.MODELSOURCE=Undefined and no EXTRUDED range. ISSHAPEBASED
stays FALSE throughout.
Unmodelled keys captured on read are re-emitted verbatim at their read position — the key
order is carried as param_key_order, as for regions, because Altium interleaves them
(BODYOVERRIDECOLOR=TRUE sits right after BODYOPACITY3D in a UI-authored body) and writes
ARCRESOLUTION twice; a from-scratch body emits the canonical set and appends any extras.
Note: Height values can be in "mil" or "mm" units on read. The tool parses both formats; mil values are converted using 1 mil = 0.0254 mm.
V7_LAYER mapping (any MECHANICAL1-MECHANICAL32 token is valid; the common pairs):
| V7_LAYER | Layer |
|---|---|
| MECHANICAL2 | Top Assembly |
| MECHANICAL3 | Bottom Assembly |
| MECHANICAL4 | Top Courtyard |
| MECHANICAL5 | Bottom Courtyard |
| MECHANICAL6 | Top 3D Body |
| MECHANICAL7 | Bottom 3D Body |
The body's layer on read is the header layer byte at offset 0, except that a mechanical byte
defers to a V7_LAYER token past the sixteen it can hold (MECHANICAL20 under byte 72 is
Mechanical 20). An unmapped byte reads as Multi-Layer and is carried as raw_layer_id together
with its token, so the pair round-trips. There is no token fallback for an absent byte — a block
without the byte has no parameter string to fall back to either.
Altium writes a via as ONE block: a fixed 321-byte record (VIA_SR1_TEMPLATE) whose first
13 bytes are the common header. (A 6-block pad-style layout is wrong and was misread by Altium —
issue #113.) The writer overlays the modelled fields onto the canonical template; undecoded
constant regions are replayed verbatim.
| Offset | Size | Field | Modelled |
|---|---|---|---|
| 0-12 | 13 | Common header (layer byte = 74 Multi-Layer; flags; net/poly/comp indices) | yes |
| 13-16 | 4 | X position (i32) | yes |
| 17-20 | 4 | Y position (i32) | yes |
| 21-24 | 4 | Diameter (i32) | yes |
| 25-28 | 4 | Hole size (i32) | yes |
| 29 | 1 | From (start) layer ID | yes |
| 30 | 1 | To (end) layer ID | yes |
| 31 | 1 | Power-plane connection style (0=Relief, 1=Direct, 2=NoConnect) | yes |
| 32-35 | 4 | Thermal relief air gap (i32; default 0.254 mm) | yes |
| 36 | 1 | Thermal relief conductor count (u8; default 4) | yes |
| 38-41 | 4 | Thermal relief conductor width (i32; default 0.254 mm) | yes |
| 42-45 | 4 | Power-plane relief expansion (i32; default 0.508 mm) | yes |
| 46-49 | 4 | Power-plane clearance (i32; default 0.508 mm) | yes |
| 50-53 | 4 | Paste mask expansion (i32) | yes |
| 54-57 | 4 | Solder mask expansion, front (i32) | yes |
| 61-64 | 4 | Cache-valid word | unmodelled (template) |
| 66 | 1 | Solder mask expansion cache state (TCacheState, see Pad) |
yes |
| 74 | 1 | Diameter stack mode (0=Simple, 1=TopMiddleBottom, 2=FullStack) | yes |
| 75-202 | 128 | Per-layer diameters (32 × i32; a Simple via repeats its diameter) | yes |
| 242-245 | 4 | Solder mask expansion, back/bottom face (i32; mirrors the front when unset) | yes |
| 258 | 1 | Solder-mask expansion measured from the hole edge (bool) | yes |
| 259-274 | 16 | Identity GUID A — zeros in every AD-authored library via; replayed from the read block, zeroed from scratch | replay |
| 275-290 | 16 | Identity GUID B — as GUID A | replay |
| 291-294 | 4 | Hole positive tolerance (i32; 0x7FFFFFFF = unset) |
yes |
| 295-298 | 4 | Hole negative tolerance (i32; 0x7FFFFFFF = unset) |
yes |
| 312 | 1 | Drill-pair classification (0=Through, 1=BlindBuriedStart, 2=Mid, 3=End) | yes |
| 320 | 1 | Trailing constant (0x01) |
template |
The reader accepts any block ≥ 31 bytes and defaults every absent field. The whole block as
read is the via's raw_block and the write base, length included: an older library stores
351-byte vias, and the thirty bytes past the template go back verbatim. A read block shorter
than the template cannot take the overlays, so the template is the base instead.
Altium embeds 3D models in the library file:
/Library/Models/
├── Header # 4-byte LE u32 model count
├── Data # Length-prefixed model records
├── 0 # First embedded model (zlib-compressed STEP)
├── 1 # Second embedded model
└── ...
Header stream format: a single 4-byte little-endian unsigned integer containing the number of embedded models.
Data stream format: a sequence of C-string parameter blocks — NO leading pipe (the record
starts at EMBED=, matching AltiumSharp's string.Join and every BODY_3D golden):
[block_len:4 LE]["EMBED=TRUE|MODELSOURCE=Undefined|ID={GUID}|ROTX=0.000|ROTY=0.000|ROTZ=0.000|DZ=0|CHECKSUM=0|NAME=model.step" + \x00]
...
Block length includes the null terminator.
| Field | Description |
|---|---|
EMBED |
TRUE if model is embedded |
MODELSOURCE |
Model source (usually Undefined) |
ID |
GUID matching MODELID in ComponentBody |
ROTX, ROTY, ROTZ |
Rotation values (degrees) |
DZ |
Z offset |
CHECKSUM |
Model checksum (see below) |
NAME |
Model filename (e.g. model.step) |
The record's position (0, 1, 2, ...) corresponds to the numbered model stream index. The
numbered streams (/Library/Models/0, ...) are the raw model bytes with a standard zlib
wrapper (RFC 1950: 78 9C header + Adler-32; matches flate2 ZlibEncoder / .NET ZLibStream).
Checksum algorithm (PcbModel.ComputeChecksum in AltiumSharp): a position-weighted byte sum
over the uncompressed model bytes — weight 1 for byte 0, weight i for byte i — modulo
2^32, stored as a signed i32. CHECKSUM=0 is explicitly tolerated by Altium, and this crate
writes 0 rather than recomputing (the value is round-tripped verbatim on read-modify-write; a
recomputed body checksum that disagreed with the Models/Data record would be worse than 0/0).
Altium stores primitives in authoring order, interleaving the kinds:
LOCKFLAGS_PCB in the golden runs pad x6, track, pad x2, track, arc x2, fill x2. That order is
the ordinal space both identity streams key off, so a footprint read from a file is written back
in the order it came in.
A footprint with no order of its own — one built in memory — is written kind by kind:
- Arcs (0x01)
- Pads (0x02)
- Vias (0x03)
- Tracks (0x04)
- Text (0x05)
- Regions (0x0B)
- Fills (0x06)
- ComponentBodies (0x0C)
WideStrings is indexed over the text primitives alone, in their own relative order, so
interleaving does not disturb the ENCODEDTEXT{n} numbering.
There is no end marker after the last primitive (issue #68).
- OLE version: MUST be V3 (512-byte sectors); Altium rejects V4
- No end markers: neither the Data stream nor any primitive carries a trailing
0x00 - WideStrings stream: per-component, block-prefixed; inline
.Designatorand.Commentare detected - 3D model embedding: zlib-compressed STEP files, referenced by GUID
- Pad hole shapes: Round (0), Square (1), Slot (2) — stored at offset 262 of the 651-byte size/shape block, NOT in the main geometry block
- Net information: modelled via the common-header indices; used in board files, not library files
- Unique IDs: 8-character alphanumeric or empty, keyed by a global 0-based
PRIMITIVEINDEXover all primitives in Data order — the ordinalPrimitiveGuidsalso uses - Default layer mapping: an unmapped layer byte reads as Multi-Layer (74) and is carried as
raw_layer_id, so a rewrite stores the byte as read - Default stack mode: unknown stack mode IDs default to Simple (0)
- Internal OLE entries filtered on read: FileHeader, Library, Models, Textures, ModelsNoEmbed, PadViaLibrary, LayerKindMapping, ComponentParamsTOC, FileVersionInfo, PrimitiveGuids
- AltiumSharp - C# library for Altium files (MIT)
- pyAltiumLib - Python library for reading Altium files
- python-altium - Altium format documentation