Implement fontconv in Python.

This fixes metrics of U+1F98 (ᾘ)

Fixes https://gitlab.com/kicad/code/kicad/-/issues/14398
This commit is contained in:
Kuba Sunderland-Ober 2023-07-24 22:23:11 +00:00 committed by Seth Hillbrand
parent 000998ccae
commit b8fc45b14d
33 changed files with 3068221 additions and 34 deletions

View File

@ -8273,7 +8273,7 @@ const char* const newstroke_font[] =
"I\\NMN[ RNOONQMTMVNWPWb RXEUH RMEMGNHOH RN`NcOdPd", "I\\NMN[ RNOONQMTMVNWPWb RXEUH RMEMGNHOH RN`NcOdPd",
"I\\NMN[ RNOONQMTMVNWPWb RQHRHSGSE RMAN@P?TAV@W? RN`NcOdPd", "I\\NMN[ RNOONQMTMVNWPWb RQHRHSGSE RMAN@P?TAV@W? RN`NcOdPd",
"I\\NMN[ RNOONQMTMVNWPWb RQEQGRHSH RMAN@P?TAV@W? RN`NcOdPd", "I\\NMN[ RNOONQMTMVNWPWb RQEQGRHSH RMAN@P?TAV@W? RN`NcOdPd",
"N]L[LF RLPXP RX[XF RR`RcSdTd", "G]L[LF RLPXP RX[XF RR`RcSdTd",
"A]L[LF RLPXP RX[XF RDEDGEHFH RR`RcSdTd", "A]L[LF RLPXP RX[XF RDEDGEHFH RR`RcSdTd",
"9]L[LF RLPXP RX[XF RCEFH R<H=H>G>E RR`RcSdTd", "9]L[LF RLPXP RX[XF RCEFH R<H=H>G>E RR`RcSdTd",
"9]L[LF RLPXP RX[XF RCEFH R<E<G=H>H RR`RcSdTd", "9]L[LF RLPXP RX[XF RCEFH R<E<G=H>H RR`RcSdTd",

View File

@ -1,4 +0,0 @@
Author:
vladimir uryvaev (vovanius@bk.ru)
Web site:
http://vovanium.ru/_media/sledy/newstroke

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,50 +3,42 @@ Newstroke Font Readme
Newstroke is a stroke (plotter) font originally designed for KiCAD. Newstroke is a stroke (plotter) font originally designed for KiCAD.
Font author: Vladimir Uryvaev (vovanius@bk.ru)
Project homepage: http://vovanium.ru/sledy/newstroke Project homepage: http://vovanium.ru/sledy/newstroke
Files Files
----- -----
font.lib - main glyph library in KiCAD library format font.kicad_sym - main glyph library in KiCAD library format
symbol.lib - glyph library for most math, tech and other symbols symbol.kicad_sym - glyph library for most math, tech and other symbols
CJK_symbol.lib - CJK symbols CJK_symbol.kicad_sym - CJK symbols
CKJ_wide.lib - CJK characters, widened by a factor of 1.5 CJK_wide.kicad_sym - CKJ characters
katakana.lib - Japanese script katakana.kicad_sym - Japanese script
hiragana.lib - Japanese script hiragana.kicad_sym - Japanese script
half_full.lib - U+FF00 half- and full-width forms half_full.kicad_sym - U+FF00 half- and full-width forms
charlist.txt - unicode glyph map list charlist.txt - unicode glyph map list
fontconv.awk - AWK script for 'compiling' project to c-source used by KiCAD fontconv.py - 'compiling' .kicad_sym files to C source used by KiCAD
../../common/newstroke_font.cpp ../../common/newstroke_font.cpp
- C source with the font, generated by fontconv.awk - C source with the font, generated by fontconv.py
Other Files Other Files
----------- -----------
font_draft1.lib - old draft glyph library with the metrics from Hersheys Simplex old/*.lib - the fonts that were originally used to generate .kicad_sym files
font.pro - KiCAD project old/font_draft1.lib - old draft glyph library with the metrics from Hersheys Simplex
old/font.pro - KiCAD project
Requirements Requirements
------------ ------------
KiCAD (http://kicad.sourceforge.net/) - for glyph editing KiCAD 6 or newer (https://www.kicad.org/download/) - for glyph editing
AWK - for font generating Python 3.10 or newer - for font generation
Usage Usage
----- -----
* Edit glyps with KiCAD 5.1 EESchema library editor. * Edit glyps with KiCAD 6 or newer EESchema library editor.
See the note below for special handling of CJK_wide.lib * Add/modify Unicode positions to charlist.
* Add Unicode positions to charlist.
* Generate font using following command line: * Generate font using following command line:
awk -f fontconv.awk symbol.lib font.lib CJK_symbol.lib CKJ_wide.lib \ python fontconv.py
hiragana.lib katakana.lib half_full.lib charlist.txt \
>../../common/newstroke_font.cpp
Note
----
The CKJ_wide.lib file is not editable using EESchema editor directly.
To edit:
* Unscale the file back to CKJ_lib.lib using unscale.py
* Edit glyphs using KiCAD 5.1 EESchema library editor.
* Rescale the file from CKJ_lib.lib to CKJ_wide.lib using scale.py
License
-------
Released under CC0 licence. Released under CC0 licence.

27103
tools/newstroke/font.kicad_sym Normal file

File diff suppressed because it is too large Load Diff

541
tools/newstroke/fontconv.py Normal file
View File

@ -0,0 +1,541 @@
#!/usr/bin/python
"""
Generates newstroke_font.cpp from .kicad_sym font libraries.
Usage: fontconv.py
"""
from io import TextIOBase
from typing import Any, NamedTuple
import re
import sys
# fontconv.awk only performed duplicate removal within a source glyph
global_duplicate_point_removal = False
input_fonts = ['symbol', 'font', 'hiragana',
'katakana', 'half_full', 'CJK_symbol',
'CJK_wide_U+4E00',
'CJK_wide_U+5AE6',
'CJK_wide_U+66B9',
'CJK_wide_U+7212',
'CJK_wide_U+7D2A',
'CJK_wide_U+8814',
'CJK_wide_U+92B4',
'CJK_wide_U+9C60']
input_charlist = 'charlist.txt'
input_header = 'font_header.cpp'
output_cpp = '../../common/newstroke_font.cpp'
FONT_BASE = 9
FONT_SCALE = 50
FONT_C_BIAS = ord("R")
FONT_NEWSTROKE = " R"
C_ESC_TRANS = str.maketrans({'"': '\\"', '\\': '\\\\'})
REMOVE_REDUNDANT_STROKES = re.compile(r"(?P<point>\S\S) R(?P=point)")
REMOVE_POINT_PAIRS = re.compile(r"^(?P<prefix>(..)*)(?P<point>\S\S)(?P=point)")
def mm_to_mil_scaled(mm: float) -> int:
return round(mm / 0.0254 - 0.1) // FONT_SCALE
def c_encode(a: int, b: int) -> str:
return chr(a + FONT_C_BIAS) + chr(b + FONT_C_BIAS)
def remove_duplicate_points(data: str) -> str:
data = REMOVE_REDUNDANT_STROKES.sub(r"\g<point>", data)
return REMOVE_POINT_PAIRS.sub(r"\g<prefix>\g<point>", data)
def cesc(s: str):
return s.translate(C_ESC_TRANS)
###
# S-Expressions
###
# Sexpr code extracted from: http://rosettacode.org/wiki/S-Expressions
term_regex = r"""(?mx)
\s*(?:
(\()|
(\))|
([+-]?\d+\.\d+(?=[\ \)\n]))|
(\-?\d+(?=[\ \)\n]))|
"((?:[^"]|(?<=\\)")*)"|
([^(^)\s]+)
)"""
class SexprError(ValueError):
pass
def parse_sexp(sexp: str) -> Any:
re_iter = re.finditer(term_regex, sexp)
rv = list(_parse_sexp_internal(re_iter))
for leftover in re_iter:
lparen, rparen, *rest = leftover.groups()
if lparen or any(rest):
raise SexprError(f'Leftover garbage after end of expression at position {leftover.start()}') # noqa: E501
elif rparen:
raise SexprError(
f'Unbalanced closing parenthesis at position {leftover.start()}')
if len(rv) == 0:
raise SexprError('No or empty expression')
if len(rv) > 1:
raise SexprError('Missing initial opening parenthesis')
return rv[0]
def _parse_sexp_internal(re_iter) -> Any:
for match in re_iter:
lparen, rparen, float_num, integer_num, quoted_str, bare_str = match.groups()
if lparen:
yield list(_parse_sexp_internal(re_iter))
elif rparen:
break
elif bare_str is not None:
yield bare_str
elif quoted_str is not None:
yield quoted_str.replace('\\"', '"')
elif float_num:
yield float(float_num)
elif integer_num:
yield int(integer_num)
###
# Primitives
###
class Transform(NamedTuple):
SX: int = +1
SY: int = +1
OY: int = 0
class Point(NamedTuple):
x: int
y: int
@staticmethod
def from_mm(sx: str, sy: str) -> 'Point':
x = mm_to_mil_scaled(float(sx))
y = mm_to_mil_scaled(-float(sy))
return Point(x, y)
def transformed(self, tr: Transform, ofs: 'Point') -> 'Point':
return Point(self.x * tr.SX + ofs.x, self.y * tr.SY + tr.OY + ofs.y)
def as_data(self) -> str:
return c_encode(self.x, self.y + FONT_BASE)
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Point(self.x - other.x, self.y - other.y)
POINT0 = Point(0, 0)
DEFAULT_TRANSFORM = Transform()
class Metrics(NamedTuple):
l: int
r: int
def transformed(self, tr: Transform, ofs: Point) -> 'Metrics':
a = self.l * tr.SX + ofs.x
b = self.r * tr.SX + ofs.x
return Metrics(min(a, b), max(a, b))
def as_data(self):
return c_encode(self.l, self.r)
def __and__(self, other):
if self.l == self.r:
return other
elif other.l == other.r:
return self
left = min(self.l, self.r, other.l, other.r)
right = max(self.l, self.r, other.l, other.r)
return Metrics(left, right)
###
# Glyph Input
###
class KicadSymError(ValueError):
pass
class Glyph(NamedTuple):
"""The immutable shape and metrics of a single glyph.
Carries the shape of a single glyph.
Can parse the data from the .kicad_sym symbol sexp.
The `data` of the glyph eventually ends up in the glyph array
newstroke_font.cpp`
Attributes:
name: The name of the symbol library component this glyph came from
metrics: Left & right extents
anchors: Named anchor points
strokes: Strokes in this glyph
width: Computed width of the glyph
"""
name: str
metrics: Metrics
anchors: dict[str, Point]
strokes: list[tuple[Point, ...]]
def as_data(self, tr: Transform, ofs: Point) -> str:
def stroke_gen(s):
return "".join(map(lambda p: p.transformed(tr, ofs).as_data(), s))
data = FONT_NEWSTROKE.join(map(stroke_gen, self.strokes))
if global_duplicate_point_removal:
return data
else:
return remove_duplicate_points(data)
@property
def width(self):
return self.metrics.r - self.metrics.l
@classmethod
def from_sexpr(cls, sexp: Any) -> 'Glyph':
if sexp[0] != "symbol":
raise KicadSymError(f"Expected a symbol sexpr: {sexp}")
name = sexp[1]
if name[0] in Compositions.transforms:
raise KicadSymError(f"Invalid glyph name {name}")
anchors = {'-': POINT0}
strokes: list[tuple[Point, ...]] = []
for s1 in sexp[2:]:
if s1[0] == "symbol":
for s2 in s1[1:]:
if s2[0] == "polyline":
strokes.append(Glyph._parse_polyline(s2))
elif s2[0] == "pin":
anchor, point = Glyph._parse_pin(s2)
anchors[anchor] = point
P, S = anchors.get("P", POINT0), anchors.get("S", POINT0)
if P.x > S.x:
raise KicadSymError(
f"P/S anchors are right-to-left: P={P.x}, S={S.x}")
if P is POINT0:
print(f" Warning: missing P anchor in glyph {name}")
if S is POINT0:
print(f" Warning: missing S anchor in glyph {name}")
return Glyph(name, Metrics(P.x, S.x), anchors, strokes)
@staticmethod
def _parse_polyline(sexp: Any) -> tuple[Point, ...]:
if sexp[0] != "polyline":
raise KicadSymError(f"Expected a polyline sexpr: {sexp}")
for s in sexp[1:]:
if s[0] == "pts":
points = s[1:]
break
stroke: list[Point] = []
for key, x, y in points:
if key != "xy":
raise KicadSymError(f"Expected a point sexpr: {points}")
stroke.append(Point.from_mm(x, y))
return tuple(stroke)
@staticmethod
def _parse_pin(sexp: Any) -> tuple[str, Point]:
# pins are used for metrics and anchors
if sexp[0] != "pin":
raise KicadSymError(f"Expected a pin sexpr: {sexp}")
for s in sexp[1:]:
if s[0] == "at":
point = Point.from_mm(s[1], s[2])
elif s[0] == "name":
pname = s[1]
if pname == "~":
pname = "P" if point.x <= 0 else "S"
return pname, point
def parse_symfile(filename: str) -> dict[str, Glyph]:
with open(filename, 'r', encoding='utf-8') as f:
sexp = parse_sexp(f.read())
glyphs: dict[str, Glyph] = {}
for s in sexp:
if s[0] == 'symbol':
glyph = Glyph.from_sexpr(s)
glyphs[glyph.name] = glyph
return glyphs
###
# Compositor
###
def _make_transforms() -> dict[str, Transform]:
cap_height = -21
x_height = -14
sym_height = -16
sup_offset = -13
sub_offset = 6
# transformation prefixes used in charlist.txt
# SX SY OY
return {
"!": Transform(-1, +1, 0), # revert
"-": Transform(+1, -1, x_height), # invert small
"=": Transform(+1, -1, cap_height), # invert cap
"~": Transform(+1, -1, sym_height), # invert symbol
"+": Transform(-1, -1, x_height), # rotate small
"%": Transform(-1, -1, cap_height), # rotate cap
"*": Transform(-1, -1, sym_height), # rotate symbol
"^": Transform(+1, +1, sup_offset), # superscript
"`": Transform(-1, +1, sup_offset), # superscript reversed
".": Transform(+1, +1, sub_offset), # subscript
",": Transform(-1, +1, sub_offset), # subscript reversed
}
class SubGlyph(NamedTuple):
glyph: Glyph
tname: str
transform: Transform = DEFAULT_TRANSFORM
offset: Point = POINT0
def as_data(self):
return self.glyph.as_data(self.transform, self.offset)
class Composition(list[SubGlyph]):
__slots__ = ["metrics"]
def __init__(self, *args):
super().__init__(self, *args)
self.metrics = Metrics(0, 0)
def append(self, sg: SubGlyph, in_metrics=True):
if in_metrics:
self.metrics &= sg.glyph.metrics.transformed(
sg.transform, sg.offset)
super().append(sg)
def as_data(self) -> str:
mdata = self.metrics.as_data()
gdata = FONT_NEWSTROKE.join(map(SubGlyph.as_data, self))
if global_duplicate_point_removal:
return mdata + remove_duplicate_points(gdata)
else:
return mdata + gdata
class Compositions:
"""Compositions of glyphs for every code point described in the character list file"""
transforms = _make_transforms()
def __init__(self, glyphs: dict[str, Glyph]):
self.glyphs = glyphs
self.default_subglyph = SubGlyph(glyphs["DEL"], "")
self.empty_subglyph = SubGlyph(glyphs["0"], "")
self.missed: set[str] = set()
self.used: set[str] = set()
self._skip = POINT0
self.font_name = "default_font"
self.codepoints: list[Composition] = []
self.comments: dict[int, str] = {}
@classmethod
def from_read(cls, sin: TextIOBase, glyphs: dict[str, Glyph]) -> 'Compositions':
charlist = Compositions(glyphs)
try:
for line in sin:
line = line[:line.find("#")] # remove comments
charlist._parse_command(line)
except BaseException:
print(f"Error parsing line '{line}'", file=sys.stderr)
raise
return charlist
@property
def _codepoint(self) -> int:
return len(self.codepoints)-1
def _new_composition(self) -> None:
self.codepoints.append(Composition())
self._skip = POINT0
def _gl_tr(self, glyphname: str) -> tuple[Glyph, Transform]:
transform = Compositions.transforms.get(
glyphname[0], DEFAULT_TRANSFORM)
if transform is not DEFAULT_TRANSFORM:
glyphname = glyphname[1:]
glyph = self.glyphs.get(glyphname, None)
if glyph:
self.used.add(glyphname)
else:
self.missed.add(glyphname)
glyph = self.default_subglyph.glyph
return glyph, transform
def _parse_command(self, line) -> None:
tokens = line.split()
tokens.reverse()
if not tokens:
return
cmd = tokens.pop()
if cmd in {"+", "+w", "+p", "+(", "+|", "+)"}:
if cmd != "+|" and cmd != "+)":
self._new_composition()
self._parse_entry(tokens, cmd == "+w" or cmd == "+p")
elif cmd.startswith("//"):
self.comments[self._codepoint] = line
elif cmd == "skipcodes":
numskip = int(tokens.pop())
subglyph = self.default_subglyph if self._codepoint < 0x9000 else self.empty_subglyph
for _ in range(numskip):
self._new_composition()
self.codepoints[-1].append(subglyph)
elif cmd == "startchar":
codepoint = int(tokens.pop())
self.codepoints = [Composition()] * codepoint
elif cmd == "font":
self.font_name = tokens.pop()
else:
raise ValueError(f"Invalid charlist command '{cmd}'")
def _parse_entry(self, tokens, sub_metrics) -> None:
composition = self.codepoints[-1]
bname = tokens.pop()
base, btr = self._gl_tr(bname)
if self._skip is not POINT0:
self._skip += Point(-base.metrics.l, 0)
composition.append(SubGlyph(base, bname, btr, self._skip))
while tokens:
name = tokens.pop()
sub, tr = self._gl_tr(name)
offset = self._skip
if tokens:
parts = tokens.pop().split("=")
if len(parts) == 2:
n_from, n_to = parts
a_from = base.anchors[n_from].transformed(btr, POINT0)
a_to = sub.anchors[n_to].transformed(tr, POINT0)
offset += (a_from - a_to)
composition.append(SubGlyph(sub, name, tr, offset), sub_metrics)
self._skip += Point(base.metrics.r, 0)
class CFontWriter:
"""Processes the compositions to generate a C file with the font."""
def __init__(self, comps: Compositions):
self.comps = comps
def print_stats(self, sout: TextIOBase):
glyphs = set(self.comps.glyphs.keys())
unused_glyphs = list(glyphs - self.comps.used)
if self.comps.missed:
print(
f"/* --- {len(self.comps.missed)} missed glyphs --- */", file=sout)
for m in self.comps.missed:
print(f"/* {m} */", file=sout)
if unused_glyphs:
print("/* --- unused glyphs --- */", file=sout)
unused_glyphs.sort()
for u in unused_glyphs:
print(f"/* {u} */", file=sout)
def generate_c_output(self, start_cp: int, sout: TextIOBase) -> None:
fname = self.comps.font_name
print(f"\n\n\nconst char* const {fname}[] =\n{{", file=sout)
cmt_iter = iter(self.comps.comments)
while (cmt_point := next(cmt_iter, start_cp)) < start_cp:
self._print_comment(cmt_point, sout)
for cp, composition in enumerate(self.comps.codepoints[start_cp:], start_cp):
CFontWriter._print_row(cp, composition, sout)
if cmt_point == cp:
self._print_comment(cmt_point, sout)
cmt_point = next(cmt_iter, 0)
end = f"}};\nconst int {fname}_bufsize = sizeof({fname})/sizeof({fname}[0]);\n"
print(end, file=sout)
def _print_comment(self, codepoint, sout):
print(f" /* {self.comps.comments[codepoint]} */", file=sout)
@staticmethod
def _print_row(codepoint, composition: Composition, sout: TextIOBase):
data = cesc(composition.as_data())
if codepoint % 16:
print(f' "{data}",', file=sout)
else:
name1 = composition[0].tname
name2 = composition[1].tname if len(composition) > 1 else ""
if name1:
print(
f' "{data}", /* U+{codepoint:X} {name1} {name2} */', file=sout)
else:
print(f' "{data}", /* U+{codepoint:X} */', file=sout)
if __name__ == "__main__":
print('** Reading glyphs from fonts:')
all_glyphs: dict[str, Glyph] = {}
for basename in input_fonts:
print(f' - Reading {basename}.kicad_sym...')
all_glyphs |= parse_symfile(f'{basename}.kicad_sym')
print(f"** Reading {input_header}")
header = open(input_header, encoding='utf-8').read()
print(f"** Reading {input_charlist}")
with open(input_charlist, encoding='utf-8') as src:
compositions = Compositions.from_read(src, all_glyphs)
print(f"** Writing {output_cpp}")
font = CFontWriter(compositions)
with open(output_cpp, "w", encoding='utf-8') as dst:
dst.write(header)
font.generate_c_output(0x20, dst)
font.print_stats(dst)
print("** Done")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
Newstroke Font Readme
=====================
Newstroke is a stroke (plotter) font originally designed for KiCAD.
Project homepage: http://vovanium.ru/sledy/newstroke
Files
-----
font.lib - main glyph library in KiCAD library format
symbol.lib - glyph library for most math, tech and other symbols
CJK_symbol.lib - CJK symbols
CKJ_wide.lib - CJK characters, widened by a factor of 1.5
katakana.lib - Japanese script
hiragana.lib - Japanese script
half_full.lib - U+FF00 half- and full-width forms
charlist.txt - unicode glyph map list
fontconv.awk - AWK script for 'compiling' project to c-source used by KiCAD
../../common/newstroke_font.cpp
- C source with the font, generated by fontconv.awk
Other Files
-----------
font_draft1.lib - old draft glyph library with the metrics from Hersheys Simplex
font.pro - KiCAD project
Requirements
------------
KiCAD (http://kicad.sourceforge.net/) - for glyph editing
AWK - for font generating
Usage
-----
* Edit glyps with KiCAD 5.1 EESchema library editor.
See the note below for special handling of CJK_wide.lib
* Add Unicode positions to charlist.
* Generate font using following command line:
awk -f fontconv.awk symbol.lib font.lib CJK_symbol.lib CKJ_wide.lib \
hiragana.lib katakana.lib half_full.lib charlist.txt \
>../../common/newstroke_font.cpp
Note
----
The CKJ_wide.lib file is not editable using EESchema editor directly.
To edit:
* Unscale the file back to CKJ_lib.lib using unscale.py
* Edit glyphs using KiCAD 5.1 EESchema library editor.
* Rescale the file from CKJ_lib.lib to CKJ_wide.lib using scale.py
Released under CC0 licence.

View File

@ -0,0 +1,433 @@
#!/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)

File diff suppressed because it is too large Load Diff