diff options
| author | Kaalleen <36401965+kaalleen@users.noreply.github.com> | 2025-02-05 18:50:31 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-02-05 18:50:31 +0100 |
| commit | af6cdc442bbcc1323ac1d13adaa49982318c9cfe (patch) | |
| tree | f71d2de5c2a89167cdb4dc9975b5f6e6d97f978e /lib | |
| parent | 8f1f68a1db65b150a6429f334059bcae34a9b883 (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')
| -rw-r--r-- | lib/extensions/__init__.py | 2 | ||||
| -rw-r--r-- | lib/extensions/lettering_svg_font_to_layers.py | 117 | ||||
| -rw-r--r-- | lib/gui/edit_json/main_panel.py | 78 | ||||
| -rw-r--r-- | lib/gui/edit_json/settings_panel.py | 2 | ||||
| -rw-r--r-- | lib/gui/lettering/main_panel.py | 10 | ||||
| -rw-r--r-- | lib/gui/lettering_font_sample.py | 118 | ||||
| -rw-r--r-- | lib/lettering/font.py | 81 | ||||
| -rw-r--r-- | lib/lettering/font_info.py | 43 | ||||
| -rw-r--r-- | lib/lettering/font_variant.py | 90 | ||||
| -rw-r--r-- | lib/lettering/glyph.py | 4 |
10 files changed, 453 insertions, 92 deletions
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 1ca475b9..91c20547 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -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, diff --git a/lib/extensions/lettering_svg_font_to_layers.py b/lib/extensions/lettering_svg_font_to_layers.py new file mode 100644 index 00000000..d5fc2219 --- /dev/null +++ b/lib/extensions/lettering_svg_font_to_layers.py @@ -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() diff --git a/lib/gui/edit_json/main_panel.py b/lib/gui/edit_json/main_panel.py index 26eb5793..b175f892 100644 --- a/lib/gui/edit_json/main_panel.py +++ b/lib/gui/edit_json/main_panel.py @@ -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: + 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)) + + last_character = None + for glyph in glyphs: if glyph is None: - glyph = self.default_variant[self.font_meta['default_glyph']] + position_x += self.font_meta['horiz_adv_x_space'] + last_character = None + continue - if glyph is not None: - position_x, last_character = self._render_glyph(glyph, position_x, character, last_character) + 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 = [] diff --git a/lib/gui/edit_json/settings_panel.py b/lib/gui/edit_json/settings_panel.py index 57ba5fdc..8c51a3d0 100644 --- a/lib/gui/edit_json/settings_panel.py +++ b/lib/gui/edit_json/settings_panel.py @@ -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")) diff --git a/lib/gui/lettering/main_panel.py b/lib/gui/lettering/main_panel.py index 630595f1..6bb7219e 100644 --- a/lib/gui/lettering/main_panel.py +++ b/lib/gui/lettering/main_panel.py @@ -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, diff --git a/lib/gui/lettering_font_sample.py b/lib/gui/lettering_font_sample.py index 1049747c..e5ce312d 100644 --- a/lib/gui/lettering_font_sample.py +++ b/lib/gui/lettering_font_sample.py @@ -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() 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) |
