Implement fontconv in Python.
This fixes metrics of U+1F98 (ᾘ) Fixes https://gitlab.com/kicad/code/kicad/-/issues/14398
This commit is contained in:
parent
000998ccae
commit
b8fc45b14d
|
@ -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",
|
||||||
|
|
|
@ -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
|
@ -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.
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
@ -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.
|
|
@ -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
Loading…
Reference in New Issue