#!/usr/bin/env python3 """Write Vecmocon's mandatory parameters directly INTO an Altium schematic-library (.SchLib) file, in pure Python — no Altium needed. Reading a value out of a .SchLib is a plain stream read (see altium_refs.py). Writing is harder: parameters live as text records inside the component's ``Data`` stream, and adding/removing them changes that stream's size, which means rebuilding the whole OLE compound container around it. This script does exactly that while **preserving every original byte except the Data stream(s) it must change** — all other streams (FileHeader, Storage, …), the directory tree, the Altium CLSIDs and timestamps are copied through untouched, and only the size/offset fields that must move are recomputed. What it does to the target component's Data stream: - removes the Ultra-Librarian default params ``Manufacturer_Name`` / ``Manufacturer_Part_Number`` (they duplicate the SOP ``Manufacturer`` / ``Manufacturer Part`` — override with --keep or a "remove" list in params.json), - adds/updates the SOP parameters from params.json (see references/schlib_parameters.md), - leaves pins, graphics, the Comment and all other records exactly as they were. Usage: python schlib_write.py --schlib IN.SchLib --params params.json --out OUT.SchLib params.json (same shape altium_params.py uses): {"component":"JMK105BJ105KV-F", # LibRef / component-storage name; omit -> all comps "parameters":{"Value":"1u","Manufacturer":"Taiyo Yuden", ...}, "remove":["Manufacturer_Name","Manufacturer_Part_Number"]} # optional; this is the default IMPORTANT: this writes Altium's own binary format from outside Altium. It is validated to re-open as a well-formed OLE with every other stream byte-identical, but ALWAYS open the result in Altium once to confirm it loads before relying on it. """ import argparse, json, os, struct, sys, hashlib import olefile FREESECT=0xFFFFFFFF; ENDOFCHAIN=0xFFFFFFFE; FATSECT=0xFFFFFFFD SEC=512; MINI=64; CUTOFF=4096 DEFAULT_REMOVE=["Manufacturer_Name", "Manufacturer_Part_Number"] def le16(b,o): return struct.unpack('path, sid->content-bytes).""" raw=open(path,'rb').read() dir_start=le32(raw,48) difat=[le32(raw,76+4*i) for i in range(109)] fat=[] for s in difat: if s>=0xFFFFFFFC: continue sec=raw[512+s*SEC:512+(s+1)*SEC] fat+=[struct.unpack('=2 else '' paths={} def walk(sid, prefix): if sid>=0xFFFFFFFC or sid>=len(entries): return e=entries[sid] walk(le32(e,68), prefix) # left nm=name_of(e); paths[sid]=prefix+[nm] if e[66]==1: # storage -> recurse children walk(le32(e,76), prefix+[nm]) walk(le32(e,72), prefix) # right walk(le32(entries[0],76), []) # from root's child ole=olefile.OleFileIO(path) bypath={'/'.join(e):ole.openstream(e).read() for e in ole.listdir(streams=True)} ole.close() content={sid:bypath['/'.join(p)] for sid,p in paths.items() if '/'.join(p) in bypath} return entries, paths, content # ----------------------------------------------------------------- edit a Data stream def _uid(name): h=''.join(c for c in hashlib.md5(name.encode()).hexdigest().upper() if c.isalpha()) return (h+"ABCDEFGH")[:8] def _param_record(idx, name, value): s=(f"|RECORD=41|IndexInSheet={idx}|OwnerPartId=1|Justification=4|FontID=2|IsHidden=T" f"|Text={value}|Name={name}|UniqueID={_uid(name)}") payload=s.encode('utf-8')+b'\x00' # utf-8: handles Ω, µ, °, ± in names/values return struct.pack('len(data): break payload=data[i+4:i+4+L] if payload[-1:]!=b'\x00' or not payload[:8].startswith(b'|RECORD='): break recs.append(data[i:i+4+L]); i+=4+L return recs, data[i:] def _rec_name(block): t=block[4:-1].decode('utf-8','replace') return t.split('|Name=')[1].split('|')[0] if '|Name=' in t else None def _patch_field(block, field, value): """Replace |field=...| inside a length-prefixed text record, re-framing its 4-byte length. Used to set the component's ComponentDescription in the RECORD=1 header.""" import re text = block[4:-1].decode('utf-8', 'replace') if f"|{field}=" in text: text = re.sub(rf"\|{re.escape(field)}=[^|]*", f"|{field}={value}", text, count=1) elif text.startswith("|RECORD="): text = text + f"|{field}={value}" payload = text.encode('utf-8') + b'\x00' return struct.pack('=CUTOFF] 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 fat_secs=list(range(cur,cur+n_fat)); total=cur+n_fat if n_fat>109: sys.exit("too many FAT sectors for a header-only DIFAT (file unexpectedly large)") # patch directory entries: start-sector + size for sid in bigs: _set(entries[sid],116,big_secs[sid][0][0],4); entries[sid][120:128]=struct.pack('=2 and p[-1].lower()=="data" and sid in content: if component is None or p[-2]==component: targets.append(sid) if not targets: sys.exit(f"no component Data stream found" + (f" for '{component}'" if component else "")) for sid in targets: content[sid]=edit_data(content[sid], fields, remove) blob=rebuild(entries, content) open(out,'wb').write(blob) # self-check: re-open and confirm it's a valid OLE with the params present if not olefile.isOleFile(out): sys.exit("ERROR: rebuilt file is not a valid OLE — aborting") ole=olefile.OleFileIO(out) ok=True for e in ole.listdir(streams=True): if e[-1].lower()=="data": t=ole.openstream(e).read().decode('utf-8','ignore') for nm in fields: if f"|Name={nm}|" not in t and f"|Name={nm}\x00" not in t and f"Name={nm}" not in t: ok=False ole.close() n_comp=len(targets) print(f"wrote {out} ({len(fields)} params into {n_comp} component(s); removed {remove}) " f"{'[self-check OK]' if ok else '[WARN: verify params]'}") def main(): ap=argparse.ArgumentParser() ap.add_argument("--schlib", required=True) ap.add_argument("--params", required=True) ap.add_argument("--out", required=True) ap.add_argument("--typeid", help="component typeid; with --template, writes that typeid's " "full template parameter set (blank where not provided)") ap.add_argument("--template", help="path to template.xlsx (defaults to the skill's)") a=ap.parse_args() template = a.template if a.typeid and not template: template = os.path.join(os.path.dirname(__file__), "..", "assets", "template", "template.xlsx") write_params(a.schlib, json.load(open(a.params, encoding="utf-8")), a.out, typeid=a.typeid, template=template) if __name__ == "__main__": main()