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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
|
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import os
from collections import defaultdict
from unicodedata import normalize
import inkex
from ..svg.tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SVG_GROUP_TAG,
SVG_PATH_TAG, SVG_USE_TAG)
from ..update import update_inkstitch_document
from .glyph import Glyph
class FontVariant(object):
"""Represents a single variant of a font.
Each font may have multiple variants for left-to-right, right-to-left,
etc. Each variant has a set of Glyphs, one per character.
A FontVariant instance can be accessed as a dict by using a unicode
character as a key.
Properties:
path -- the path to the directory containing this font
variant -- the font variant, specified using one of the constants below
glyphs -- a dict of Glyphs, with the glyphs' unicode characters as keys.
"""
# We use unicode characters rather than English strings for font file names
# in order to be more approachable for languages other than English.
LEFT_TO_RIGHT = "→"
RIGHT_TO_LEFT = "←"
TOP_TO_BOTTOM = "↓"
BOTTOM_TO_TOP = "↑"
VARIANT_TYPES = (LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP)
@classmethod
def reversed_variant(cls, variant):
if variant == cls.LEFT_TO_RIGHT:
return cls.RIGHT_TO_LEFT
elif variant == cls.RIGHT_TO_LEFT:
return cls.LEFT_TO_RIGHT
elif variant == cls.TOP_TO_BOTTOM:
return cls.BOTTOM_TO_TOP
elif variant == cls.BOTTOM_TO_TOP:
return cls.TOP_TO_BOTTOM
else:
return None
def __init__(self, font_path, variant, default_glyph=None):
# If the font variant file does not exist, this constructor will
# raise an exception. The caller should catch it and decide
# what to do.
self.path = font_path
self.variant = variant
self.default_glyph = default_glyph
self.glyphs = {}
self._load_glyphs()
def _load_glyphs(self):
variant_file_paths = self._get_variant_file_paths()
for svg_path in variant_file_paths:
document = inkex.load_svg(svg_path)
update_inkstitch_document(document, warn_unversioned=False)
svg = document.getroot()
svg = self._apply_transforms(svg)
glyph_layers = svg.xpath(".//svg:g[starts-with(@inkscape:label, 'GlyphLayer-')]", namespaces=inkex.NSS)
for layer in glyph_layers:
self._clean_group(layer)
layer.attrib[INKSCAPE_LABEL] = layer.attrib[INKSCAPE_LABEL].replace("GlyphLayer-", "", 1)
glyph_name = normalize('NFKC', layer.attrib[INKSCAPE_LABEL])
try:
self.glyphs[glyph_name] = Glyph(layer)
except (AttributeError, ValueError):
pass
def _get_variant_file_paths(self):
file_paths = []
direct_path = os.path.join(self.path, "%s.svg" % self.variant)
if os.path.isfile(direct_path):
file_paths.append(direct_path)
elif os.path.isdir(os.path.join(self.path, "%s" % self.variant)):
path = os.path.join(self.path, "%s" % self.variant)
file_paths.extend([os.path.join(path, svg) for svg in os.listdir(path) if svg.endswith('.svg')])
return file_paths
def _clean_group(self, group):
# We'll repurpose the layer as a container group labelled with the
# glyph.
del group.attrib[INKSCAPE_GROUPMODE]
# The layer may be marked invisible, so we'll clear the 'display'
# style and presentation attribute.
group.style.pop('display', None)
group.attrib.pop('display', None)
def _apply_transforms(self, svg):
self.clip_transforms = defaultdict(list)
# apply transforms to paths and use tags
for element in svg.iterdescendants((SVG_PATH_TAG, SVG_USE_TAG, SVG_GROUP_TAG)):
transform = element.composed_transform()
if element.clip is not None:
self.clip_transforms[element.clip] = element.composed_transform()
if element.tag == SVG_GROUP_TAG:
continue
if element.tag == SVG_PATH_TAG:
path = element.path.transform(transform)
element.set_path(path)
element.attrib.pop("transform", None)
elif element.tag == SVG_USE_TAG:
oldx = element.get('x', 0)
oldy = element.get('y', 0)
newx, newy = transform.apply_to_point((oldx, oldy))
element.set('x', newx)
element.set('y', newy)
element.attrib.pop("transform", None)
for clip, transform in self.clip_transforms.items():
for element in clip.iterdescendants():
if element.tag == SVG_PATH_TAG:
path = element.path.transform(transform)
element.set_path(path)
element.attrib.pop("transform", None)
# remove transforms after they have been applied
for group in svg.iterdescendants(SVG_GROUP_TAG):
group.attrib.pop('transform', None)
return svg
def glyphs_start_with(self, character):
glyph_selection = [glyph_name for glyph_name, glyph_layer in self.glyphs.items() if glyph_name.startswith(character)]
return sorted(glyph_selection, key=lambda glyph: (len(glyph.split('.')[0]), len(glyph)), reverse=True)
def isbinding(self, character):
# after a non binding letter a letter can only be in isol or fina shape.
# binding glyph only have two shapes, isol and fina
non_binding_char = ['ا', 'أ', 'ﺇ', 'آ', 'ٱ', 'د', 'ذ', 'ر', 'ز', 'و']
normalized_non_binding_char = [normalize('NFKC', letter) for letter in non_binding_char]
return not (character in normalized_non_binding_char)
def ispunctuation(self, character):
# punctuation sign are not considered as part of the word. They onnly have one shape
punctuation_signs = ['؟', '،', '.', ',', ';', '.', '!', ':', '؛']
normalized_punctuation_signs = [normalize('NFKC', letter) for letter in punctuation_signs]
return (character in normalized_punctuation_signs)
def get_glyph(self, character, word):
"""
Returns the glyph for the given character, searching for combined glyphs first
This expects glyph annotations to be within the given word, for example: a.init
Returns glyph node and length of the glyph name
"""
glyph_selection = self.glyphs_start_with(character)
for glyph in glyph_selection:
if word.startswith(glyph):
return self.glyphs[glyph], len(glyph)
return self.glyphs.get(self.default_glyph, None), 1
def get_next_glyph_shape(self, word, starting, ending, previous_is_binding):
# in arabic each letter (or ligature) may have up to 4 different shapes, hence 4 glyphs
# this computes the shape of the glyph that represents word[starting:ending+1]
# punctuation is not really part of the word
# they may appear at begining or end of words
# computes where the actual word begins and ends up
last_char_index = len(word)-1
first_char_index = 0
while self.ispunctuation(word[last_char_index]):
last_char_index = last_char_index - 1
while self.ispunctuation(word[first_char_index]):
first_char_index = first_char_index + 1
# first glyph is eithher isol or init depending wether it is also the last glyph of the actual word
if starting == first_char_index:
if not self.isbinding(word[ending]) or len(word) == 1:
shape = 'isol'
else:
shape = 'init'
# last glyph is final if previous is binding, isol otherwise
# a non binding glyph behaves like the last glyph
elif ending == last_char_index or not self.isbinding(word[ending]):
if previous_is_binding:
shape = 'fina'
else:
shape = 'isol'
# in the middle of the actual word, the shape of a glyph is medi if previous glyph is bendinng, init otherwise
elif previous_is_binding:
shape = 'medi'
else:
shape = 'init'
return shape
def get_next_glyph(self, word, i, previous_is_binding):
# search for the glyph of word that starts at i,taking into acount the previous glyph binding status
# find all the glyphs in tthe font that start with first letter of the glyph
glyph_selection = self.glyphs_start_with(word[i])
# find the longest glyph that match
for glyph in glyph_selection:
glyph_name = glyph.split('.')
if len(glyph_name) == 2 and glyph_name[1] in ['isol', 'init', 'medi', 'fina']:
is_binding = self.isbinding(glyph_name[0][-1])
if len(word) < i + len(glyph_name[0]):
continue
shape = self.get_next_glyph_shape(word, i, i + len(glyph_name[0]) - 1, previous_is_binding)
if glyph_name[1] == shape and word[i:].startswith(glyph_name[0]):
return self.glyphs[glyph], len(glyph_name[0]), is_binding
elif word[i:].startswith(glyph):
return self.glyphs[glyph], len(glyph), True
# nothing was found
return self.glyphs.get(self.default_glyph, None), 1, True
def __getitem__(self, character):
if character in self.glyphs:
return self.glyphs[character]
else:
return self.glyphs.get(self.default_glyph, None)
def __contains__(self, character):
return character in self.glyphs
|