mirror of
https://gitlab.com/kicad/code/kicad.git
synced 2025-09-14 02:03:12 +02:00
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 RQHRHSGSE 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",
|
||||
"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",
|
||||
|
@ -1,4 +0,0 @@
|
||||
Author:
|
||||
vladimir uryvaev (vovanius@bk.ru)
|
||||
Web site:
|
||||
http://vovanium.ru/_media/sledy/newstroke
|
2830
tools/newstroke/CJK_symbol.kicad_sym
Normal file
2830
tools/newstroke/CJK_symbol.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
397299
tools/newstroke/CJK_wide_U+4E00.kicad_sym
Normal file
397299
tools/newstroke/CJK_wide_U+4E00.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
402848
tools/newstroke/CJK_wide_U+5AE6.kicad_sym
Normal file
402848
tools/newstroke/CJK_wide_U+5AE6.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
405793
tools/newstroke/CJK_wide_U+66B9.kicad_sym
Normal file
405793
tools/newstroke/CJK_wide_U+66B9.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
405830
tools/newstroke/CJK_wide_U+7212.kicad_sym
Normal file
405830
tools/newstroke/CJK_wide_U+7212.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
405847
tools/newstroke/CJK_wide_U+7D2A.kicad_sym
Normal file
405847
tools/newstroke/CJK_wide_U+7D2A.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
407045
tools/newstroke/CJK_wide_U+8814.kicad_sym
Normal file
407045
tools/newstroke/CJK_wide_U+8814.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
411963
tools/newstroke/CJK_wide_U+92B4.kicad_sym
Normal file
411963
tools/newstroke/CJK_wide_U+92B4.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
164587
tools/newstroke/CJK_wide_U+9C60.kicad_sym
Normal file
164587
tools/newstroke/CJK_wide_U+9C60.kicad_sym
Normal file
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.
|
||||
|
||||
Font author: Vladimir Uryvaev (vovanius@bk.ru)
|
||||
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
|
||||
font.kicad_sym - main glyph library in KiCAD library format
|
||||
symbol.kicad_sym - glyph library for most math, tech and other symbols
|
||||
CJK_symbol.kicad_sym - CJK symbols
|
||||
CJK_wide.kicad_sym - CKJ characters
|
||||
katakana.kicad_sym - Japanese script
|
||||
hiragana.kicad_sym - Japanese script
|
||||
half_full.kicad_sym - U+FF00 half- and full-width forms
|
||||
charlist.txt - unicode glyph map list
|
||||
fontconv.py - 'compiling' .kicad_sym files to C source used by KiCAD
|
||||
../../common/newstroke_font.cpp
|
||||
- C source with the font, generated by fontconv.awk
|
||||
- C source with the font, generated by fontconv.py
|
||||
|
||||
Other Files
|
||||
-----------
|
||||
font_draft1.lib - old draft glyph library with the metrics from Hersheys Simplex
|
||||
font.pro - KiCAD project
|
||||
old/*.lib - the fonts that were originally used to generate .kicad_sym files
|
||||
old/font_draft1.lib - old draft glyph library with the metrics from Hersheys Simplex
|
||||
old/font.pro - KiCAD project
|
||||
|
||||
Requirements
|
||||
------------
|
||||
KiCAD (http://kicad.sourceforge.net/) - for glyph editing
|
||||
AWK - for font generating
|
||||
KiCAD 6 or newer (https://www.kicad.org/download/) - for glyph editing
|
||||
Python 3.10 or newer - for font generation
|
||||
|
||||
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.
|
||||
* Edit glyps with KiCAD 6 or newer EESchema library editor.
|
||||
* Add/modify 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
|
||||
|
||||
python fontconv.py
|
||||
|
||||
License
|
||||
-------
|
||||
Released under CC0 licence.
|
||||
|
27103
tools/newstroke/font.kicad_sym
Normal file
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
541
tools/newstroke/fontconv.py
Normal 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")
|
7420
tools/newstroke/half_full.kicad_sym
Normal file
7420
tools/newstroke/half_full.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
5016
tools/newstroke/hiragana.kicad_sym
Normal file
5016
tools/newstroke/hiragana.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
4832
tools/newstroke/katakana.kicad_sym
Normal file
4832
tools/newstroke/katakana.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
52
tools/newstroke/obsolete/README-old.txt
Normal file
52
tools/newstroke/obsolete/README-old.txt
Normal 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.
|
433
tools/newstroke/obsolete/lib2sym.py
Normal file
433
tools/newstroke/obsolete/lib2sym.py
Normal 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)
|
18760
tools/newstroke/symbol.kicad_sym
Normal file
18760
tools/newstroke/symbol.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user