Lettering typographic features (#3466)

* add svg font to layers extension which saves glyph annotations into the glyph name
---------
Co-authored-by: Claudine
pull/3491/head
Kaalleen 2025-02-05 18:50:31 +01:00 zatwierdzone przez GitHub
rodzic 8f1f68a1db
commit af6cdc442b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
11 zmienionych plików z 476 dodań i 94 usunięć

Wyświetl plik

@ -40,6 +40,7 @@ from .lettering_force_lock_stitches import LetteringForceLockStitches
from .lettering_generate_json import LetteringGenerateJson
from .lettering_remove_kerning import LetteringRemoveKerning
from .lettering_set_color_sort_index import LetteringSetColorSortIndex
from .lettering_svg_font_to_layers import LetteringSvgFontToLayers
from .letters_to_font import LettersToFont
from .object_commands import ObjectCommands
from .object_commands_toggle_visibility import ObjectCommandsToggleVisibility
@ -112,6 +113,7 @@ __all__ = extensions = [About,
LetteringGenerateJson,
LetteringRemoveKerning,
LetteringSetColorSortIndex,
LetteringSvgFontToLayers,
LettersToFont,
ObjectCommands,
ObjectCommandsToggleVisibility,

Wyświetl plik

@ -0,0 +1,117 @@
#!/usr/bin/env python3
# coding=utf-8
#
# Copyright (C) 2011 Felipe Correa da Silva Sanches <juca@members.fsf.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
#
# Adapted for the inkstitch lettering module to allow glyph annotations for characters
# in specific positions or settings. Changes: see git history
"""Extension for converting svg fonts to layers"""
from inkex import Layer, PathElement, errormsg
from .base import InkstitchExtension
class LetteringSvgFontToLayers(InkstitchExtension):
"""Convert an svg font to layers"""
def add_arguments(self, pars):
pars.add_argument(
"--count",
type=int,
default=30,
help="Stop making layers after this number of glyphs.",
)
def flip_cordinate_system(self, elem, emsize, baseline):
"""Scale and translate the element's path, returns the path object"""
path = elem.path
path.scale(1, -1, inplace=True)
path.translate(0, int(emsize) - int(baseline), inplace=True)
return path
def effect(self):
# Current code only reads the first svgfont instance
font = self.svg.defs.findone("svg:font")
if font is None:
return errormsg("There are no svg fonts")
# setwidth = font.get("horiz-adv-x")
baseline = font.get("horiz-origin-y")
if baseline is None:
baseline = 0
guidebase = self.svg.viewbox_height - baseline
fontface = font.findone("svg:font-face")
emsize = fontface.get("units-per-em")
# TODO: should we guarantee that <svg:font horiz-adv-x> equals <svg:font-face units-per-em> ?
caps = int(fontface.get("cap-height", 0))
xheight = int(fontface.get("x-height", 0))
ascender = int(fontface.get("ascent", 0))
descender = int(fontface.get("descent", 0))
self.svg.set("width", emsize)
self.svg.namedview.add_guide(guidebase, True, "baseline")
self.svg.namedview.add_guide(guidebase - ascender, True, "ascender")
self.svg.namedview.add_guide(guidebase - caps, True, "caps")
self.svg.namedview.add_guide(guidebase - xheight, True, "xheight")
self.svg.namedview.add_guide(guidebase + descender, True, "decender")
count = 0
for glyph in font.findall("svg:glyph"):
hide_layer = count != 0
self.convert_glyph_to_layer(glyph, emsize, baseline, hide_layer=hide_layer)
count += 1
if count >= self.options.count:
break
def convert_glyph_to_layer(self, glyph, emsize, baseline, hide_layer):
unicode_char = glyph.get("unicode")
glyph_name = glyph.get("glyph-name").split('.')
if unicode_char is None:
if len(glyph_name) == 2:
unicode_char = glyph_name[0]
else:
return
typographic_feature = ''
if len(glyph_name) == 2:
typographic_feature = glyph_name[1]
else:
arabic_form = glyph.get('arabic-form', None)
if arabic_form is not None and len(arabic_form) > 4:
typographic_feature = arabic_form[:4]
if typographic_feature:
typographic_feature = f".{typographic_feature}"
layer = self.svg.add(Layer.new(f"GlyphLayer-{unicode_char}{typographic_feature}"))
# glyph layers (except the first one) are innitially hidden
if hide_layer:
layer.style["display"] = "none"
# Using curve description in d attribute of svg:glyph
path = layer.add(PathElement())
path.path = self.flip_cordinate_system(glyph, emsize, baseline)
if __name__ == "__main__":
LetteringSvgFontToLayers().run()

Wyświetl plik

@ -237,16 +237,29 @@ class LetteringEditJsonPanel(wx.Panel):
self.horiz_adv_x = self.font.horiz_adv_x
kerning_combinations = combinations_with_replacement(self.glyphs, 2)
self.kerning_combinations = [''.join(combination) for combination in kerning_combinations]
self.kerning_combinations.extend([combination[1] + combination[0] for combination in self.kerning_combinations])
self.kerning_combinations = [' '.join(combination) for combination in kerning_combinations]
self.kerning_combinations.extend([f'{combination[1]} {combination[0]}' for combination in kerning_combinations])
self.kerning_combinations = list(set(self.kerning_combinations))
self.kerning_combinations.sort()
self.update_legacy_kerning_pairs()
self.update_settings()
self.update_kerning_list()
self.update_glyph_list()
self.update_preview()
def update_legacy_kerning_pairs(self):
new_list = defaultdict(list)
for kerning_pair, value in self.kerning_pairs.items():
if " " in kerning_pair:
# legacy kerning pairs do not use a space
return
if len(kerning_pair) < 2:
continue
pair = f'{kerning_pair[0]} {kerning_pair[1]}'
new_list[pair] = value
self.kerning_pairs = new_list
def update_settings(self):
# reset font_meta
self.font_meta = defaultdict(list)
@ -305,7 +318,8 @@ class LetteringEditJsonPanel(wx.Panel):
kerning_list.AppendColumn("New kerning", width=wx.LIST_AUTOSIZE_USEHEADER)
for kerning_pair in self.kerning_combinations:
if self.font_meta['text_direction'] == 'rtl':
kerning_pair = kerning_pair[::-1]
pair = kerning_pair.split()
kerning_pair = ' '.join(pair[::-1])
index = kerning_list.InsertItem(kerning_list.GetItemCount(), kerning_pair)
# kerning_list.SetItem(index, 0, kerning_pair)
kerning_list.SetItem(index, 1, str(self.kerning_pairs.get(kerning_pair, 0.0)))
@ -373,43 +387,59 @@ class LetteringEditJsonPanel(wx.Panel):
if self.last_notebook_selection == 3:
text = self.get_active_glyph()
else:
text = self.get_active_kerning_pair()
kerning = self.get_active_kerning_pair()
kerning = kerning.split()
text = ''.join(kerning)
if self.font_meta['text_direction'] == 'rtl':
text = ''.join(kerning[::-1])
if not text:
return
text = self.text_before + text + self.text_after
if self.font_meta['text_direction'] == 'rtl':
text = text[::-1]
self._render_text(text)
position_x = self._render_text(self.text_before, 0, True)
position_x = self._render_text(text, position_x, False)
self._render_text(self.text_after, position_x, True)
if self.default_variant.variant == FontVariant.RIGHT_TO_LEFT:
self.layer[:] = reversed(self.layer)
for group in self.layer:
group[:] = reversed(group)
def _render_text(self, text):
last_character = None
position_x = 0
for character in text:
glyph = self.default_variant[character]
if character == " " or (glyph is None and self.font_meta['default_glyph'] == " "):
position_x += self.font_meta['horiz_adv_x_space']
last_character = None
else:
if glyph is None:
glyph = self.default_variant[self.font_meta['default_glyph']]
def _render_text(self, text, position_x, use_character_position):
words = text.split()
for i, word in enumerate(words):
glyphs = []
skip = []
previous_is_binding = False
for i, character in enumerate(word):
if i in skip:
continue
if use_character_position:
glyph, glyph_len, previous_is_binding = self.default_variant.get_next_glyph(word, i, previous_is_binding)
else:
glyph, glyph_len = self.default_variant.get_glyph(character, word[i:])
glyphs.append(glyph)
skip = list(range(i, i+glyph_len))
if glyph is not None:
position_x, last_character = self._render_glyph(glyph, position_x, character, last_character)
last_character = None
for glyph in glyphs:
if glyph is None:
position_x += self.font_meta['horiz_adv_x_space']
last_character = None
continue
position_x = self._render_glyph(glyph, position_x, glyph.name, last_character)
last_character = glyph.name
position_x += self.font_meta['horiz_adv_x_space']
position_x -= self.font_meta['horiz_adv_x_space']
return position_x
def _render_glyph(self, glyph, position_x, character, last_character):
node = deepcopy(glyph.node)
if last_character is not None:
if self.font_meta['text_direction'] != 'rtl':
position_x += glyph.min_x - self.kerning_pairs.get(last_character + character, 0)
position_x += glyph.min_x - self.kerning_pairs.get(f'{last_character} {character}', 0)
else:
position_x += glyph.min_x - self.kerning_pairs.get(character + last_character, 0)
position_x += glyph.min_x - self.kerning_pairs.get(f'{character} {last_character}', 0)
transform = f"translate({position_x}, 0)"
node.set('transform', transform)
@ -427,7 +457,7 @@ class LetteringEditJsonPanel(wx.Panel):
# because this is not unique it will be overwritten by inkscape when inserted into the document
node.set("id", "glyph")
self.layer.add(node)
return position_x, character
return position_x
def render_stitch_plan(self):
stitch_groups = []

Wyświetl plik

@ -97,7 +97,7 @@ class FontInfo(wx.Panel):
)
default_variant_label = wx.StaticText(self, label=_("Default Variant"))
self.default_variant = wx.Choice(self, choices=[_(""), _(""), _(""), ("")])
self.default_variant = wx.Choice(self, choices=["", "", "", ""])
self.default_variant.Bind(wx.EVT_CHOICE, self.parent.on_default_variant_change)
text_direction_label = wx.StaticText(self, label=_("Text direction"))

Wyświetl plik

@ -15,7 +15,7 @@ from ...i18n import _
from ...lettering import FontError, get_font_list
from ...lettering.categories import FONT_CATEGORIES
from ...stitch_plan import stitch_groups_to_stitch_plan
from ...svg.tags import INKSCAPE_LABEL, INKSTITCH_LETTERING
from ...svg.tags import INKSTITCH_LETTERING
from ...utils import DotDict, cache
from ...utils.threading import ExitThread, check_stop_flag
from .. import PresetsPanel, PreviewRenderer, info_dialog
@ -292,15 +292,11 @@ class LetteringPanel(wx.Panel):
del self.group[:]
destination_group = inkex.Group(attrib={
# L10N The user has chosen to scale the text by some percentage
# (50%, 200%, etc). If you need to use the percentage symbol,
# make sure to double it (%%).
INKSCAPE_LABEL: _("Text scale") + f' {self.settings.scale}%'
})
destination_group = inkex.Group()
self.group.append(destination_group)
font = self.fonts.get(self.options_panel.font_chooser.GetValue(), self.default_font)
destination_group.label = f"{font.name} {_('scale')} {self.settings.scale}%"
try:
font.render_text(
self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth,

Wyświetl plik

@ -3,9 +3,11 @@
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from copy import deepcopy
import wx
import wx.adv
from inkex import errormsg
from inkex import Group, errormsg
from ..i18n import _
from ..lettering import get_font_list
@ -20,6 +22,8 @@ class FontSampleFrame(wx.Frame):
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
self.fonts = None
self.font = None
self.font_variant = None
self.main_panel = wx.Panel(self, wx.ID_ANY)
@ -124,14 +128,14 @@ class FontSampleFrame(wx.Frame):
self.font_chooser.Append(font.marked_custom_font_name)
def on_font_changed(self, event=None):
font = self.fonts.get(self.font_chooser.GetValue(), list(self.fonts.values())[0].marked_custom_font_name)
self.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100))
self.font = self.fonts.get(self.font_chooser.GetValue(), list(self.fonts.values())[0].marked_custom_font_name)
self.scale_spinner.SetRange(int(self.font.min_scale * 100), int(self.font.max_scale * 100))
# font._load_variants()
self.direction.Clear()
for variant in font.has_variants():
for variant in self.font.has_variants():
self.direction.Append(variant)
self.direction.SetSelection(0)
if font.sortable:
if self.font.sortable:
self.color_sort_label.Enable()
self.color_sort_checkbox.Enable()
else:
@ -143,19 +147,16 @@ class FontSampleFrame(wx.Frame):
self.layer.transform.add_scale(self.scale_spinner.GetValue() / 100)
scale = self.layer.transform.a
# set font
font = self.fonts.get(self.font_chooser.GetValue())
if font is None:
if self.font is None:
self.GetTopLevelParent().Close()
return
# parameters
line_width = self.max_line_width.GetValue()
direction = self.direction.GetValue()
color_sort = self.sortable(font)
font._load_variants()
font_variant = font.variants[direction]
self.font._load_variants()
self.font_variant = self.font.variants[direction]
# setup lines of text
text = ''
@ -164,33 +165,32 @@ class FontSampleFrame(wx.Frame):
printed_warning = False
update_glyphlist_warning = _(
"The glyphlist for this font seems to be outdated.\n\n"
"Please update the glyph list for %s:\n"
"open Extensions > Ink/Stitch > Font Management > Edit JSON "
"select this font and apply. No other changes necessary."
% font.marked_custom_font_name
)
"Please update the glyph list for {font_name}:\n"
"* Open Extensions > Ink/Stitch > Font Management > Edit JSON\n"
"* Select this font and apply."
).format(font_name=self.font.marked_custom_font_name)
self.duplicate_warning(font)
self.duplicate_warning()
# font variant glyph list length falls short if a single quote sign is available
# let's add it in the length comparison
if len(set(font.available_glyphs)) != len(font_variant.glyphs):
if len(set(self.font.available_glyphs)) != len(self.font_variant.glyphs):
errormsg(update_glyphlist_warning)
printed_warning = True
for glyph in font.available_glyphs:
glyph_obj = font_variant[glyph]
for glyph in self.font.available_glyphs:
glyph_obj = self.font_variant[glyph]
if glyph_obj is None:
if not printed_warning:
errormsg(update_glyphlist_warning)
printed_warning = True
continue
if last_glyph is not None:
width_to_add = (glyph_obj.min_x - font.kerning_pairs.get(last_glyph + glyph, 0)) * scale
width_to_add = (glyph_obj.min_x - self.font.kerning_pairs.get(f'{last_glyph} {glyph}', 0)) * scale
width += width_to_add
try:
width_to_add = (font.horiz_adv_x.get(glyph, font.horiz_adv_x_default) - glyph_obj.min_x) * scale
width_to_add = (self.font.horiz_adv_x.get(glyph, self.font.horiz_adv_x_default) - glyph_obj.min_x) * scale
except TypeError:
width_to_add = glyph_obj.width
@ -203,24 +203,84 @@ class FontSampleFrame(wx.Frame):
text += glyph
width += width_to_add
# render text and close
font.render_text(text, self.layer, variant=direction, back_and_forth=False, color_sort=color_sort)
self._render_text(text)
self.GetTopLevelParent().Close()
def sortable(self, font):
def sortable(self):
color_sort = self.color_sort_checkbox.GetValue()
if color_sort and not font.sortable:
if color_sort and not self.font.sortable:
color_sort = False
return color_sort
def duplicate_warning(self, font):
def duplicate_warning(self):
# warn about duplicated glyphs
if len(set(font.available_glyphs)) != len(font.available_glyphs):
if len(set(self.font.available_glyphs)) != len(self.font.available_glyphs):
duplicated_glyphs = " ".join(
[glyph for glyph in set(font.available_glyphs) if font.available_glyphs.count(glyph) > 1]
[glyph for glyph in set(self.font.available_glyphs) if self.font.available_glyphs.count(glyph) > 1]
)
errormsg(_("Found duplicated glyphs in font file: {duplicated_glyphs}").format(duplicated_glyphs=duplicated_glyphs))
def _render_text(self, text):
lines = text.splitlines()
position = {'x': 0, 'y': 0}
for line in lines:
group = Group()
group.label = line
group.set("inkstitch:letter-group", "line")
glyphs = []
skip = []
for i, character in enumerate(line):
if i in skip:
continue
default_variant = self.font.variants[self.font.json_default_variant]
glyph, glyph_len = default_variant.get_glyph(character, line[i:])
glyphs.append(glyph)
skip = list(range(i, i+glyph_len))
last_character = None
for glyph in glyphs:
if glyph is None:
position['x'] += self.font.horiz_adv_x_space
last_character = None
continue
position = self._render_glyph(group, glyph, position, glyph.name, last_character)
last_character = glyph.name
self.layer.add(group)
position['x'] = 0
position['y'] += self.font.leading
if self.sortable():
self.font.do_color_sort(self.layer, 1)
def _render_glyph(self, group, glyph, position, character, last_character):
node = deepcopy(glyph.node)
if last_character is not None:
if self.font.text_direction != 'rtl':
position['x'] += glyph.min_x - self.font.kerning_pairs.get(f'{last_character} {character}', 0)
else:
position['x'] += glyph.min_x - self.font.kerning_pairs.get(f'{character} {last_character}', 0)
transform = f"translate({position['x']}, {position['y']})"
node.set('transform', transform)
horiz_adv_x_default = self.font.horiz_adv_x_default
if horiz_adv_x_default is None:
horiz_adv_x_default = glyph.width + glyph.min_x
position['x'] += self.font.horiz_adv_x.get(character, horiz_adv_x_default) - glyph.min_x
self.font._update_commands(node, glyph)
self.font._update_clips(group, node, glyph)
# this is used to recognize a glyph layer later in the process
# because this is not unique it will be overwritten by inkscape when inserted into the document
node.set("id", "glyph")
node.set("inkstitch:letter-group", "glyph")
group.add(node)
return position
def cancel(self, event):
self.GetTopLevelParent().Close()

Wyświetl plik

@ -324,41 +324,70 @@ 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()
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)
glyph = glyph_set[character]
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]
if glyph is not None:
node = self._render_glyph(destination_group, glyph, position, character, last_character)
word_group.append(node)
last_character = character
position.x += self.word_spacing
last_character = None
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
def _get_word_glyphs(self, glyph_set, word):
glyphs = []
skip = []
previous_is_binding = True
for i, character in enumerate(word):
if i in skip:
continue
# 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)

Wyświetl plik

@ -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))

Wyświetl plik

@ -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]

Wyświetl plik

@ -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)

Wyświetl plik

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Convert SVG Font to Glyph Layers</name>
<id>org.{{ id_inkstitch }}.lettering_svg_font_to_layers</id>
<param name="extension" type="string" gui-hidden="true">lettering_svg_font_to_layers</param>
<param name="count" type="int" min="1" max="65535" gui-text="Stop after">100</param>
<effect needs-live-preview="false">
<object-type>all</object-type>
<icon>{{ icon_path }}inx/font_management.svg</icon>
<menu-tip>Converts a svg font to glyph layers</menu-tip>
<effects-menu>
<submenu name="{{ menu_inkstitch }}" translatable="no">
<submenu name="Font Management" />
</submenu>
</effects-menu>
</effect>
<script>
{{ command_tag | safe }}
</script>
</inkscape-extension>