#!/usr/bin/env python3 """Compile an Altium **integrated library** (.IntLib) from a .SchLib + .PcbLib, in pure Python. An .IntLib is an OLE compound file with five streams: SchLib/0.schlib 0x02 + zlib() PCBLib/0.pcblib 0x02 + zlib() LibCrossRef.Txt symbol -> footprint cross-reference index (length-prefixed strings) Parameters .bin the Components-panel search index (parameter string) Version.Txt constant \x00 + uint32(2) We don't hand-roll a compound-file writer: we take a known-good single-component .IntLib as a **container template** and swap in this part's five stream contents, then rebuild the OLE with the same directory tree (reusing schlib_write.read_container / rebuild, already proven to emit Altium-valid OLEs). The symbol->footprint linkage lives inside the embedded .SchLib itself (RECORD=45 model link), so the compiled library resolves the footprint with no external PcbLib. """ import argparse, json, os, re, struct, sys, zlib import olefile sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)))) from schlib_write import read_container FREESECT = 0xFFFFFFFF; ENDOFCHAIN = 0xFFFFFFFE; FATSECT = 0xFFFFFFFD SEC = 512; MINI = 64; CUTOFF = 4096 def _pad(b, n): return b + b"\x00" * ((-len(b)) % n) def write_cfb(entries, content): """Serialise an OLE/CFB compound file from raw 128-byte directory entries + {sid: bytes}, reproducing Altium's on-disk SECTOR ORDER: [FAT][directory][miniFAT][mini-stream][big streams]. (schlib_write.rebuild is spec-valid but places the FAT last; Altium's IntLib reader is strict about big streams and expects the conventional FAT-first layout, so we match it here.) Streams < 4096 B live in the mini-stream; >= 4096 B get their own FAT sectors.""" minis = [sid for sid, c in content.items() if len(c) < CUTOFF] bigs = [sid for sid, c in content.items() if len(c) >= CUTOFF] # mini-stream + mini-FAT ministream = b""; mini_start = {} for sid in minis: mini_start[sid] = len(ministream) // MINI ministream += _pad(content[sid], MINI) n_mini = len(ministream) // MINI minifat = [ENDOFCHAIN] * n_mini for sid in minis: st = mini_start[sid]; cnt = (len(content[sid]) + MINI - 1) // MINI or 1 for k in range(cnt - 1): minifat[st + k] = st + k + 1 minifat_bytes = _pad(b"".join(struct.pack(" n_fat * SEC: n_fat += 1 total = n_fat + non_fat if n_fat > 109: sys.exit("too many FAT sectors for a header-only DIFAT") # sector assignment — FAT first, matching Altium fat_secs = list(range(0, n_fat)); cur = n_fat dir_secs = list(range(cur, cur + n_dir)); cur += n_dir mf_secs = list(range(cur, cur + n_mf)); cur += n_mf ms_secs = list(range(cur, cur + n_ms)); cur += n_ms big_secs = {} for sid in bigs: big_secs[sid] = list(range(cur, cur + big_k[sid])); cur += big_k[sid] # patch directory entries (start sector + 64-bit size) def setentry(e, start, size): e[116:120] = struct.pack(" 254: raise ValueError(f"string too long for single-byte length prefix: {s[:40]!r}...") return struct.pack("