kicad/tools/newstroke/obsolete/lib2sym.py

434 lines
12 KiB
Python

#!/usr/bin/python
"""
Converts 5.1 font .lib files to .kicad_sym.
Deals with "rescaled" libraries that have non-integer coordinates,
such as CKJ_wide.lib.
This is not a general purpose converter - it's only meant to
deal with font files.
Usage: lib2sym.py
"""
import re
from dataclasses import dataclass, field
from typing import Any, ClassVar, TextIO
ROTATIONS = {'R': 0, 'U': 90, 'L': 180, 'D': 270}
VISIBILITIES = {'V': True, 'I': False}
ORIENTATIONS = {'H': 0, 'V': 90}
FILLTYPES = {'N': 'none'}
PINETYPES = {'I': 'input', 'O': 'output'}
PINSHAPES = {
'': 'line',
'I': 'inverted',
'C': 'clock',
'CI': 'inverted_clock',
'L': 'input_low',
'CL': 'clock_low',
'F': 'falling_clock',
'X': 'non_logic'}
PINHIDDEN = 'N'
def mil_to_mm(mil: float):
return round(mil * 0.0254, 6)
def npairwise(iterable):
args = (iter(iterable),) * 2
return list(zip(*args))
class SexprWriter:
def __init__(self, stream: TextIO):
self.indent = 0
self.stream = stream
self.space = ""
self.dedents: list[bool] = []
def _indent(self) -> None:
self.stream.write(" " * 2 * self.indent)
def _newline(self) -> None:
self.stream.write("\n")
def _space(self) -> None:
self.stream.write(self.space)
self.space = ""
def group(self, key: Any, /, items=None, newline=False, indent=False):
self.startgroup(key, items, newline, indent)
self.endgroup(newline=False)
def startgroup(self, key: Any, /, items=None, newline=False, indent=False):
self.dedents.append(indent)
if indent:
self.indent += 1
if newline:
self._newline()
self._indent()
else:
self._space()
self.stream.write("(")
if key:
self.stream.write(str(key))
self.space = " "
if items:
for item in items:
self.additem(item)
def endgroup(self, newline=True):
dedent = self.dedents.pop()
if newline:
self._newline()
self._indent()
self.space = ""
else:
self.space = " "
if dedent:
if self.indent > 0:
self.indent -= 1
self.stream.write(")")
def additem(self, item: Any):
self._space()
if item == 0:
self.stream.write("0")
else:
self.stream.write(str(item))
self.space = " "
@dataclass
class Point:
x: float
y: float
@classmethod
def new_mil(cls, x: float, y: float) -> 'Point':
return cls(mil_to_mm(x), mil_to_mm(y))
def write(self, to: SexprWriter):
to.group("xy", [self.x, self.y], newline=True, indent=True)
@dataclass
class TextEffect:
size: float
is_hidden: bool = False
@classmethod
def new_mil(cls, size: float) -> 'TextEffect':
return cls(mil_to_mm(size))
def write(self, to: SexprWriter, newline=True):
if self:
to.startgroup("effects", newline=newline, indent=True)
to.startgroup("font")
to.group("size", [self.size, self.size])
to.endgroup(newline=False)
if self.is_hidden:
to.additem("hide")
to.endgroup(newline=False)
@dataclass
class Field:
name: str
value: str
posx: float
posy: float
rotation: float
effects: TextEffect
_pattern: ClassVar[re.Pattern] = re.compile(
r'^\s*F(?P<n>\d+)\s+"(?P<value>[^"]*)"\s+(?P<rest>.+)$')
@staticmethod
def valid(line: str):
return Field._pattern.match(line) is not None
@classmethod
def new_v5(cls, line: str) -> 'Field':
match = Field._pattern.match(line)
assert match is not None
n = int(match.group("n"))
name = ['Reference', 'Value', 'Footprint', 'Datasheet'][n]
value = match.group("value")
rest = match.group("rest").split()
x = mil_to_mm(float(rest[0]))
y = mil_to_mm(float(rest[1]))
dimension = int(rest[2])
rotation = ORIENTATIONS[rest[3]]
visibility = VISIBILITIES[rest[4]]
effects = TextEffect.new_mil(dimension)
effects.is_hidden = not visibility
return cls(name, value, x, y, rotation, effects)
def write(self, to: SexprWriter):
to.startgroup(
"property", [
f'"{self.name}"', f'"{self.value}"'], newline=True, indent=True)
to.group("at", [self.posx, self.posy, self.rotation])
self.effects.write(to)
to.endgroup()
@dataclass
class Polyline:
points: list[Point]
stroke_width: float
fill_type: str
@staticmethod
def valid(line: str):
return line.startswith('P ')
@classmethod
def new_v5(cls, line: str) -> 'Polyline':
tokens = line.split()
points: list[Point] = []
_1, _2, _3, stroke_width = tokens[1:5]
fill_type = tokens[-1]
for x, y in npairwise(tokens[5:]):
points.append(Point.new_mil(float(x), float(y)))
return cls(points,
mil_to_mm(int(stroke_width)),
FILLTYPES[fill_type]
)
def write(self, to: SexprWriter):
to.startgroup("polyline", newline=True, indent=True)
to.startgroup("pts", newline=True, indent=True)
for point in self.points:
point.write(to)
to.endgroup()
to.startgroup("stroke", newline=True, indent=True)
to.group("width", [self.stroke_width])
to.group("type", ["solid"])
to.endgroup(newline=False)
to.startgroup("fill", newline=True, indent=True)
to.group("type", [self.fill_type])
to.endgroup(newline=False)
to.endgroup()
@dataclass
class Pin:
name: str
number: str
etype: str
name_effect: TextEffect
number_effect: TextEffect
posx: float = 0.0
posy: float = 0.0
rotation: int = 0
shape: str = "line"
length: float = 2.54
is_hidden: bool = False
@staticmethod
def valid(line: str):
return line.startswith('X ')
@classmethod
def new_v5(cls, line: str) -> 'Pin':
tokens = line.split()
name, number, x, y, length, orientation, numsize, namesize, _1, _2, etype = tokens[
1:12]
shape = tokens[12] if len(tokens) > 12 else ''
rotation = ROTATIONS[orientation]
etype = PINETYPES[etype]
if is_hidden := shape.startswith(PINHIDDEN):
shape = shape[len(PINHIDDEN):]
pin = cls(name, number, etype,
name_effect=TextEffect.new_mil(int(namesize)),
number_effect=TextEffect.new_mil(int(numsize))
)
pin.posx = mil_to_mm(float(x))
pin.posy = mil_to_mm(float(y))
pin.shape = PINSHAPES[shape]
pin.length = mil_to_mm(float(length))
pin.is_hidden = is_hidden
pin.rotation = rotation
return pin
def write(self, to: SexprWriter):
to.startgroup("pin", [self.etype, self.shape],
newline=True, indent=True)
to.group("at", [self.posx, self.posy, self.rotation])
to.group("length", [self.length])
to.startgroup("name", [f'"{self.name}"'], newline=True, indent=True)
self.name_effect.write(to, newline=False)
to.endgroup(newline=False)
to.startgroup("number", [f'"{self.number}"'],
newline=True, indent=True)
self.number_effect.write(to, newline=False)
to.endgroup(newline=False)
to.endgroup(newline=True)
@dataclass
class Symbol:
name: str
pin_names_offset: float
properties: list[Field] = field(default_factory=list)
pins: list[Pin] = field(default_factory=list)
polylines: list[Polyline] = field(default_factory=list)
@staticmethod
def valid(line: str):
return line.startswith("DEF ")
@staticmethod
def final(line: str):
return line.startswith("ENDDEF")
@classmethod
def new_v5(cls, line: str) -> 'Symbol':
tokens = line.split()
name = tokens[1]
offset = mil_to_mm(int(tokens[4]))
return cls(name, offset)
@property
def has_contents(self):
return bool(self.pins) | bool(self.polylines)
def write(self, to: SexprWriter):
to.startgroup("symbol", [f'"{self.name}"'], newline=True, indent=True)
to.startgroup("pin_names")
to.group("offset", [self.pin_names_offset])
to.endgroup(newline=False)
to.group("in_bom", ["yes"])
to.group("on_board", ["yes"])
for prop in self.properties:
prop.write(to)
if self.has_contents:
to.startgroup(
"symbol", [f'"{self.name}_0_1"'], newline=True, indent=True)
for poly in self.polylines:
poly.write(to)
for pin in self.pins:
pin.write(to)
to.endgroup()
to.endgroup()
def getheft(self):
"""A good heuristic for how much space this symbol's sexprs take"""
heft = 1 + 2*len(self.properties) + 3*len(self.pins)
for poly in self.polylines:
heft += 1 + len(poly.points)
return heft
def translate(libfilename: str, symfilebase: str, symfileext: str, split=False):
fo: Optional[TextIO] = None
to: Optional[SexprWriter] = None
part = ""
def closeOutput():
nonlocal fo, to
if fo:
to.endgroup()
fo.write('\n')
fo.close()
fo = None
def newOutput():
nonlocal fo, to, part
fo = open(f"{symfilebase}{part}{symfileext}", 'w', encoding="utf-8")
to = SexprWriter(fo)
to.startgroup("kicad_symbol_lib")
to.group("version", ["20220914"])
to.group("generator", ["font_lib2sym"])
with open(libfilename, encoding="utf-8") as fi:
linecount = 0
totalheft = 0
heftperfile = 175_000 # just shy of 10MB
sym: Symbol
while line := fi.readline():
linecount += 1
if Polyline.valid(line):
polyline = Polyline.new_v5(line)
sym.polylines.append(polyline)
elif Pin.valid(line):
pin = Pin.new_v5(line)
sym.pins.append(pin)
elif Field.valid(line):
prop = Field.new_v5(line)
sym.properties.append(prop)
elif line.startswith('#'):
pass
elif Symbol.valid(line):
sym = Symbol.new_v5(line)
elif Symbol.final(line):
if not fo or (split and totalheft > heftperfile):
if split:
part = f'_{sym.name}'
closeOutput()
newOutput()
totalheft = 0
sym.write(to)
totalheft += sym.getheft()
if linecount & 127 == 1:
print(sym.name)
elif line.startswith("DRAW") or line.startswith("ENDDRAW") or line.startswith("EESchema-LIBRARY"):
pass
elif line.strip():
raise RuntimeError(f"Unknown line contents: {line}")
closeOutput()
if __name__ == "__main__":
fonts = ['symbol', 'hiragana', 'katakana',
'half_full', 'font', 'CJK_symbol', 'CKJ_wide:split']
for font in fonts:
split = False
out = font.replace("CKJ", "CJK")
if font.endswith(":split"):
font = font[:-6]
out = out[:-6]
split = True
translate(f'{font}.lib', out, '.kicad_sym', split)