summaryrefslogtreecommitdiff
path: root/lib/lettering
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2025-02-05 18:50:31 +0100
committerGitHub <noreply@github.com>2025-02-05 18:50:31 +0100
commitaf6cdc442bbcc1323ac1d13adaa49982318c9cfe (patch)
treef71d2de5c2a89167cdb4dc9975b5f6e6d97f978e /lib/lettering
parent8f1f68a1db65b150a6429f334059bcae34a9b883 (diff)
Lettering typographic features (#3466)
* add svg font to layers extension which saves glyph annotations into the glyph name --------- Co-authored-by: Claudine
Diffstat (limited to 'lib/lettering')
-rw-r--r--lib/lettering/font.py81
-rw-r--r--lib/lettering/font_info.py43
-rw-r--r--lib/lettering/font_variant.py90
-rw-r--r--lib/lettering/glyph.py4
4 files changed, 187 insertions, 31 deletions
diff --git a/lib/lettering/font.py b/lib/lettering/font.py
index 0c02de84..a554183e 100644
--- a/lib/lettering/font.py
+++ b/lib/lettering/font.py
@@ -324,40 +324,69 @@ class Font(object):
group = inkex.Group()
group.label = line
+ if self.text_direction == 'rtl':
+ group.label = line[::-1]
group.set("inkstitch:letter-group", "line")
last_character = None
words = line.split(" ")
for word in words:
+
word_group = inkex.Group()
- word_group.label = word
+ label = word
+ if self.text_direction == 'rtl':
+ label = word[::-1]
+ word_group.label = label
word_group.set("inkstitch:letter-group", "word")
- for character in word:
- if self.letter_case == "upper":
- character = character.upper()
- elif self.letter_case == "lower":
- character = character.lower()
-
- glyph = glyph_set[character]
+ if self.text_direction == 'rtl':
+ glyphs = self._get_word_glyphs(glyph_set, word[::-1])
+ glyphs = glyphs[::-1]
+ else:
+ glyphs = self._get_word_glyphs(glyph_set, word)
- if glyph is None and self.default_glyph == " ":
+ last_character = None
+ for glyph in glyphs:
+ if glyph is None:
position.x += self.word_spacing
last_character = None
- else:
- if glyph is None:
- glyph = glyph_set[self.default_glyph]
+ continue
+ node = self._render_glyph(destination_group, glyph, position, glyph.name, last_character)
+ word_group.append(node)
+ last_character = glyph.name
+ group.append(word_group)
+ position.x += self.word_spacing
+ return group
- if glyph is not None:
- node = self._render_glyph(destination_group, glyph, position, character, last_character)
- word_group.append(node)
+ def _get_word_glyphs(self, glyph_set, word):
+ glyphs = []
+ skip = []
+ previous_is_binding = True
- last_character = character
- position.x += self.word_spacing
- last_character = None
- group.append(word_group)
+ for i, character in enumerate(word):
+ if i in skip:
+ continue
- return group
+ # forced letter case
+ if self.letter_case == "upper":
+ character = character.upper()
+ elif self.letter_case == "lower":
+ character = character.lower()
+
+ glyph, glyph_len, binding = glyph_set.get_next_glyph(word, i, previous_is_binding)
+ previous_is_binding = binding
+
+ skip = list(range(i, i+glyph_len))
+
+ if glyph is None and self.default_glyph == " ":
+ glyphs.append(None)
+ else:
+ if glyph is None:
+ glyphs.append(glyph_set[self.default_glyph])
+ if glyph is not None:
+ glyphs.append(glyph)
+
+ return glyphs
def _render_glyph(self, destination_group, glyph, position, character, last_character):
"""Render a single glyph.
@@ -383,9 +412,17 @@ class Font(object):
node = deepcopy(glyph.node)
if last_character is not None:
if self.text_direction != "rtl":
- position.x += glyph.min_x - self.kerning_pairs.get(last_character + character, 0)
+ kerning = self.kerning_pairs.get(f'{last_character} {character}', None)
+ if kerning is None:
+ # legacy kerning without space
+ kerning = self.kerning_pairs.get(last_character + character, 0)
+ position.x += glyph.min_x - kerning
else:
- position.x += glyph.min_x - self.kerning_pairs.get(character + last_character, 0)
+ kerning = self.kerning_pairs.get(f'{character} {last_character}', None)
+ if kerning is None:
+ # legacy kerning without space
+ kerning = self.kerning_pairs.get(character + last_character, 0)
+ position.x += glyph.min_x - kerning
transform = "translate(%s, %s)" % position.as_tuple()
node.set('transform', transform)
diff --git a/lib/lettering/font_info.py b/lib/lettering/font_info.py
index cb47feea..74cc0967 100644
--- a/lib/lettering/font_info.py
+++ b/lib/lettering/font_info.py
@@ -3,6 +3,8 @@
# 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
from inkex import NSS
from lxml import etree
@@ -21,11 +23,35 @@ class FontFileInfo(object):
# 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]/@*[name()='unicode' or name()='horiz-adv-x']"
- hax = self.svg.xpath(xpath, namespaces=NSS)
- if len(hax) == 0:
+ 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 {}
- return dict(zip(hax[0::2], [float(x) for x in hax[1::2]]))
+
+ 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:
+ 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):
@@ -54,7 +80,7 @@ class FontFileInfo(object):
for first, second, key in kern_list:
for f in first:
for s in second:
- hkern[f+s] = key
+ hkern[f'{f} {s}'] = key
return hkern
def split_glyph_list(self, glyph):
@@ -62,7 +88,7 @@ class FontFileInfo(object):
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 commas separated and each first letter needs to be combined with each next letter
+ # 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:
@@ -73,11 +99,12 @@ class FontFileInfo(object):
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("%s%s%s" % (toUnicode(glyph_with_separator[0]), separator, glyph_with_separator[1]))
+ glyphs.append(f"{toUnicode(glyph_with_separator[0])}{separator}{glyph_with_separator[1]}")
used_separator = True
- continue
# there is no extra separator
if not used_separator:
glyphs.append(toUnicode(glyph_name))
diff --git a/lib/lettering/font_variant.py b/lib/lettering/font_variant.py
index baf7c09f..5bedaa87 100644
--- a/lib/lettering/font_variant.py
+++ b/lib/lettering/font_variant.py
@@ -5,6 +5,7 @@
import os
from collections import defaultdict
+from unicodedata import normalize
import inkex
@@ -73,7 +74,7 @@ class FontVariant(object):
for layer in glyph_layers:
self._clean_group(layer)
layer.attrib[INKSCAPE_LABEL] = layer.attrib[INKSCAPE_LABEL].replace("GlyphLayer-", "", 1)
- glyph_name = layer.attrib[INKSCAPE_LABEL]
+ glyph_name = normalize('NFKC', layer.attrib[INKSCAPE_LABEL])
try:
self.glyphs[glyph_name] = Glyph(layer)
except (AttributeError, ValueError):
@@ -134,6 +135,93 @@ class FontVariant(object):
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]
diff --git a/lib/lettering/glyph.py b/lib/lettering/glyph.py
index a3a9df71..fd1e1985 100644
--- a/lib/lettering/glyph.py
+++ b/lib/lettering/glyph.py
@@ -5,6 +5,7 @@
from collections import defaultdict
from copy import copy
+from unicodedata import normalize
from inkex import paths, transforms, units
@@ -36,6 +37,9 @@ class Glyph(object):
this Glyph. Nested groups are allowed.
"""
+ self.name = group.label
+ if len(self.name) > 11:
+ self.name = normalize('NFKC', self.name[11:])
self._process_baseline(group.getroottree().getroot())
self.clips = self._process_clips(group)
self.node = self._process_group(group)