1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from collections import defaultdict
from fontTools.agl import toUnicode # type:ignore[import-untyped]
from inkex import NSS
from lxml import etree
from ..svg.tags import INKSCAPE_LABEL
class FontFileInfo(object):
"""
This class reads kerning information from an SVG file
"""
def __init__(self, path):
with open(path, 'r', encoding="utf-8") as svg:
self.svg = etree.parse(svg)
# horiz_adv_x defines the width of specific letters (distance to next letter)
def horiz_adv_x(self):
# In XPath 2.0 we could use ".//svg:glyph/(@unicode|@horiz-adv-x)"
xpath = ".//svg:glyph" # [@unicode and @horiz-adv-x and @glyph-name]/@*[name()='unicode' or name()='horiz-adv-x' or name()='glyph-name']"
glyph_definitions = self.svg.xpath(xpath, namespaces=NSS)
if len(glyph_definitions) == 0:
return {}
horiz_adv_x_dict = defaultdict(list)
for glyph in glyph_definitions:
unicode_char = glyph.get('unicode', None)
if unicode_char is None:
continue
hax = glyph.get('horiz-adv-x', None)
if hax is None:
continue
else:
hax = float(hax)
glyph_name = glyph.get('glyph-name', None)
if glyph_name is not None:
glyph_name = glyph_name.split('.')
if len(glyph_name) == 2:
if ord(unicode_char[0]) >= 57344 :
unicode_char = glyph_name[0]
typographic_feature = glyph_name[1]
unicode_char += f'.{typographic_feature}'
else:
arabic_form = glyph.get('arabic-form', None)
if arabic_form is not None and len(arabic_form) > 4:
typographic_feature = arabic_form[:4]
unicode_char += f'.{typographic_feature}'
horiz_adv_x_dict[unicode_char] = hax
return horiz_adv_x_dict
# kerning (specific distances of two specified letters)
def hkern(self):
xpath = ".//svg:hkern[(@u1 or @g1) and (@u1 or @g1) and @k]/@*[contains(name(), '1') or contains(name(), '2') or name()='k']"
hkern = self.svg.xpath(xpath, namespaces=NSS)
# the kerning list now contains the kerning values as a list where every first value contains the first letter(s),
# every second value contains the second letter(s) and every third value contains the kerning
u_first = [k for k in hkern[0::3]]
u_second = [k for k in hkern[1::3]]
k = [float(x) for x in hkern[2::3]]
# sometimes a font file contains conflicting kerning value for a letter pair
# in this case the value which is specified as a single pair overrules the one specified in a list of letters
# therefore we want to sort our list by length of the letter values
kern_list = list(zip(u_first, u_second, k))
kern_list.sort(key=lambda x: len(x[0] + x[1]), reverse=True)
for index, kerning in enumerate(kern_list):
first, second, key = kerning
first = self.split_glyph_list(first)
second = self.split_glyph_list(second)
kern_list[index] = (first, second, key)
hkern = {}
for first, second, key in kern_list:
for f in first:
for s in second:
hkern[f'{f} {s}'] = key
return hkern
def split_glyph_list(self, glyph):
glyphs = []
if len(glyph) > 1:
# glyph names need to be converted to unicode
# we need to take into account, that there can be more than one first/second letter in the very same hkern element
# in this case they will be comma separated and each first letter needs to be combined with each next letter
# e.g. <hkern g1="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Amacron,Abreve,Aogonek" g2="T,Tcaron" k="5" />
glyph_names = glyph.split(",")
for glyph_name in glyph_names:
# each glyph can have additional special markers, e.g. o.cmp
# toUnicode will not respect those and convert them to a simple letter
# this behaviour will generate a wrong spacing for this letter.
# Let's make sure to also transfer the separators and extensions to our json file
separators = [".", "_"]
used_separator = False
for separator in separators:
if used_separator:
continue
glyph_with_separator = glyph_name.split(separator)
if len(glyph_with_separator) == 2:
glyphs.append(f"{toUnicode(glyph_with_separator[0])}{separator}{glyph_with_separator[1]}")
used_separator = True
# there is no extra separator
if not used_separator:
glyphs.append(toUnicode(glyph_name))
else:
glyphs.append(glyph)
return glyphs
# the space character
def word_spacing(self):
xpath = "string(.//svg:glyph[@glyph-name='space'][1]/@*[name()='horiz-adv-x'])"
word_spacing = self.svg.xpath(xpath, namespaces=NSS)
try:
return float(word_spacing)
except ValueError:
return None
# default letter spacing
def letter_spacing(self):
xpath = "string(.//svg:font[@horiz-adv-x][1]/@*[name()='horiz-adv-x'])"
letter_spacing = self.svg.xpath(xpath, namespaces=NSS)
try:
return float(letter_spacing)
except ValueError:
return None
# this value will be saved into the json file to preserve it for later font edits
# additionally it serves to automatically define the line height (leading)
def units_per_em(self):
xpath = "string(.//svg:font-face[@units-per-em][1]/@*[name()='units-per-em'])"
units_per_em = self.svg.xpath(xpath, namespaces=NSS)
try:
return float(units_per_em)
except ValueError:
return None
"""
def missing_glyph_spacing(self):
xpath = "string(.//svg:missing-glyph/@*[name()='horiz-adv-x'])"
return float(self.svg.xpath(xpath, namespaces=NSS))
"""
def glyph_list(self):
"""
Returns a list of available glyphs in the font file
"""
glyphs = []
glyph_layers = self.svg.xpath(".//svg:g[starts-with(@inkscape:label, 'GlyphLayer-')]", namespaces=NSS)
for layer in glyph_layers:
glyph_name = layer.attrib[INKSCAPE_LABEL].replace("GlyphLayer-", "", 1)
glyphs.append(glyph_name)
return glyphs
|