From 0b3e0e962dd238297a0c05896b592837c0480015 Mon Sep 17 00:00:00 2001
From: Claudine Peyrat <88194877+claudinepeyrat06@users.noreply.github.com>
Date: Sun, 24 Aug 2025 20:31:43 +0200
Subject: rename extension (#3938)
rename lettering_fill_composed_glyphs lettering_organize_glyphs
---
lib/extensions/__init__.py | 4 +-
lib/extensions/lettering_fill_composed_glyphs.py | 573 -----------------------
lib/extensions/lettering_organize_glyphs.py | 573 +++++++++++++++++++++++
templates/lettering_fill_composed_glyphs.xml | 49 --
templates/lettering_organize_glyphs.xml | 49 ++
5 files changed, 624 insertions(+), 624 deletions(-)
delete mode 100644 lib/extensions/lettering_fill_composed_glyphs.py
create mode 100644 lib/extensions/lettering_organize_glyphs.py
delete mode 100644 templates/lettering_fill_composed_glyphs.xml
create mode 100644 templates/lettering_organize_glyphs.xml
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py
index 4308d316..f6694a90 100644
--- a/lib/extensions/__init__.py
+++ b/lib/extensions/__init__.py
@@ -35,7 +35,7 @@ from .lettering import Lettering
from .lettering_along_path import LetteringAlongPath
from .lettering_custom_font_dir import LetteringCustomFontDir
from .lettering_edit_json import LetteringEditJson
-from .lettering_fill_composed_glyphs import LetteringFillComposedGlyphs
+from .lettering_organize_glyphs import LetteringOrganizeGlyphs
from .lettering_font_sample import LetteringFontSample
from .lettering_force_lock_stitches import LetteringForceLockStitches
from .lettering_generate_json import LetteringGenerateJson
@@ -113,7 +113,7 @@ extensions = [
LetteringAlongPath,
LetteringCustomFontDir,
LetteringEditJson,
- LetteringFillComposedGlyphs,
+ LetteringOrganizeGlyphs,
LetteringFontSample,
LetteringForceLockStitches,
LetteringGenerateJson,
diff --git a/lib/extensions/lettering_fill_composed_glyphs.py b/lib/extensions/lettering_fill_composed_glyphs.py
deleted file mode 100644
index edc1dd81..00000000
--- a/lib/extensions/lettering_fill_composed_glyphs.py
+++ /dev/null
@@ -1,573 +0,0 @@
-# Authors: see git history
-#
-# Copyright (c) 2025 Authors
-# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-import unicodedata
-from inkex import NSS, Group, Layer, errormsg
-from copy import deepcopy
-from ..svg.tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SODIPODI_INSENSITIVE, SVG_GROUP_TAG)
-from .base import InkstitchExtension
-from ..i18n import _
-
-
-class LetteringFillComposedGlyphs(InkstitchExtension):
- """_summary_
- The goal of this extension is to help the font digitizer with steps to organize its work.
- At each step a group of glyphs is brought to the top of the object stack, and the font
- digitizer should digitize these glyphs before going to the next step.
- Steps are organized in such a way as to break the work into smaller chunks and maximize reuse of already digitized letters.
-
- unicodedata is used to decomposed letters into pieces.
- Furthermore the extension use some additional information, such as "i" and "j" usually reuse
- the digitalization of "."
- """
-
- def __init__(self, *args, **kwargs):
- InkstitchExtension.__init__(self, *args, **kwargs)
- self.arg_parser.add_argument("--tabs")
- self.arg_parser.add_argument("-c", "--action", dest="action", type=str, default="none")
- self._glyphs_layers = [] # list of all Layers ()
- self._all_glyphs = [] # list of chr, (one per layer)
- # constructed using unicodedata.decomposition
- self._decomposition = {} # keys are glyphs, value are the list of pieces in the decomposition
- self._used_in_decompositions = {} # reverse dictionary
- self._normalization = {} # constructed using unicodedata.normalize('NFD', glyph)
-
- self._pieces = [] # all the pieces for all decomposable glyphs
- self._missing = [] # pieces not already among _all_glyphs
-
- self._category_name = self._category_name()
-
- def _remove_empty_groups(self):
- for group in self.svg.iterdescendants(SVG_GROUP_TAG):
- if len(group.getchildren()) == 0:
- group.delete()
-
- def _update_glyphs_layers(self):
- self._glyphs_layers = self.svg.xpath('.//svg:g[starts-with(@inkscape:label, "GlyphLayer-")]', namespaces=NSS)
-
- def _category_name(self):
- category_name = {}
- category_name["Lu"] = _("Upper Case Letters")
- category_name["Ll"] = _('Lower Case Letters')
- category_name["Lo"] = _('Other Letters')
- category_name["Nd"] = _("Digits")
- category_name["Sc"] = _('Symbols')
- category_name["Pc"] = _('Punctuation')
- category_name["Pe"] = _('Closing Punctuation')
- category_name["Mn"] = _('Diacritics')
- category_name["Co"] = _('Special')
-
- return category_name
-
- def _update_all_glyphs(self):
- # only consider GlyphLayer we know the unicode they represent, that means their name is a single letter
- self._update_glyphs_layers()
- for layer in self._glyphs_layers:
- name = layer.attrib[INKSCAPE_LABEL]
- name = name.replace("GlyphLayer-", "", 1)
- if len(name) == 1:
- self._all_glyphs.append(name)
-
- def _fill_decompose_lists(self):
- # NFD normalization decomposes 'Ṓ' into three characters, letter O, macron accent and acute accent,
- # unicodedata.decomposition(Ṓ) splits it into two entry points, one for 'Ō' and one for the acute accent
- # NFD normalization of 'a' is simply 'a', while unicodedata.decomposition('a') is an empty string.
- # unicodedata.decomposition also splits a subscript letter into a keyword for subscript and the entry point
- # of the corresponding letter, this is just an example, the normalization of the subscript letter being
- # the subscript letter itself. We do need both!
-
- for glyph in self._all_glyphs:
- normalization = [char for char in unicodedata.normalize('NFD', glyph)]
- decomposition = []
- for code in unicodedata.decomposition(glyph).split(' '):
- try:
- piece = chr(int(code, 16)) # convert entry point into unicode character
- # we don't need the space separator nor the other separators as pieces, as there
- # is nothing to render for them
- if unicodedata.category(piece)[0] != 'Z':
- decomposition.append(piece)
- except ValueError: # this will eliminate keywords as they do not convert to int
- pass
- if decomposition != []:
- self._decomposition[glyph] = decomposition
- if unicodedata.decomposition(glyph) == "":
- decomposition = [glyph]
- self._normalization[glyph] = normalization
- for piece in decomposition:
- if piece not in self._used_in_decompositions:
- self._used_in_decompositions[piece] = [glyph]
- else:
- self._used_in_decompositions[piece].append(glyph)
- self._pieces = [piece for piece in self._used_in_decompositions]
- self._missing = [piece for piece in self._pieces
- if piece not in self._all_glyphs and len(self._used_in_decompositions[piece]) > 1]
-
- def _find_layer(self, char):
- for layer in self._glyphs_layers:
- label = layer.attrib[INKSCAPE_LABEL]
- label = label.replace("GlyphLayer-", "", 1)
- if SODIPODI_INSENSITIVE in layer.attrib:
- layer.attrib.pop(SODIPODI_INSENSITIVE)
- if len(label) == 1 and label == char:
- return layer
- return None
-
- def _remove(self, char):
- char_layer = self._find_layer(char)
- if char_layer is not None:
- char_layer.delete()
-
- def _create_empty_glyph(self, char):
- new_layer = self.svg.add(Layer.new("GlyphLayer-" + char))
- new_layer.set("style", "display:none")
- return new_layer
-
- # Step 0: check for duplicate and remove unwanted layer
-
- def _look_for_duplicate(self, verbose=False):
- if len(self._all_glyphs) != len(set(self._all_glyphs)):
- duplicated_glyphs = " ".join(
- [glyph for glyph in set(self._all_glyphs) if self._all_glyphs.count(glyph) > 1]
- )
- errormsg(_("Found duplicated glyphs in font file: {duplicated_glyphs}").format(duplicated_glyphs=duplicated_glyphs))
-
- for letter in duplicated_glyphs:
- errormsg((unicodedata.name(letter)))
- else:
- if verbose:
- errormsg(_("No duplicated glyph found"))
-
- def _is_valid(self, char):
- # sometimes one grabs a non rendering char in the ttf file, and it results in
- # an invalid glyph (d='')
- if char == "":
- return True
- category = unicodedata.category(char)
- if category[0] in ['Z', 'C'] and category != 'Co':
- return False
- return True
-
- def _remove_invalid_glyphs(self):
- for layer in self._glyphs_layers:
- name = layer.attrib[INKSCAPE_LABEL]
- name = name.replace("GlyphLayer-", "", 1)
- if name == "" or name == ".null" or (len(name) == 1 and not self._is_valid(name)):
- layer.delete()
-
- # Step1 time to digitize comma, hyphen, and period:
- # move comma, hyphen and period on top
- # Lock all other glyphs
-
- def _lock_and_hide_all_layers(self):
- self._update_glyphs_layers()
- for layer in self._glyphs_layers:
- layer.set(SODIPODI_INSENSITIVE, True)
- layer.set("style", "display:none")
-
- def _move_on_top(self, layer):
- if SODIPODI_INSENSITIVE in layer.attrib:
- layer.pop(SODIPODI_INSENSITIVE)
- copy_layer = deepcopy(layer)
- layer.delete()
- self.svg.append(copy_layer)
-
- def _move_char_on_top(self, char):
- warning = False
- layer_char = self._find_layer(char)
- if layer_char is None:
- warning = True
- self._create_empty_glyph(char)
- else:
- self._move_on_top(layer_char)
- return warning
-
- def _add_chars(self, char_list):
- self._lock_and_hide_all_layers()
- added = ""
- for char in char_list:
- if self._move_char_on_top(char):
- added = added + char + " "
- if added != "":
- added_char__warning = _(
- "This or these glyphs have been added:\n"
- "{added_char}\n"
- "Either fill them or delete them").format(added_char=added)
- errormsg(added_char__warning)
-
- # Step 2
- # Find all non-composed letters (no use of diacritic allowed)
- # Group them by category, all upper cases, lower cases and other
-
- def _create_empty_group(self, group_name):
- new_group = Group()
- new_group.label = group_name
- return new_group
-
- def _do_in_first_steps(self, glyph):
- # check if a glyph can be digitalize without waiting for some of its piece to be digitalized first
- if unicodedata.decomposition(glyph) == "":
- return True
- # There is a decomposition, but the decomposition is in only one piece, and this piece
- # is not used for anything else.
- if len(self._decomposition[glyph]) == 1:
- piece = self._decomposition[glyph][0]
- if len(self._used_in_decompositions[piece]) == 1:
- return True
- return False
-
- def _create_and_fill_group(self, unicode_categories, excepting=[], adding=[], also_composed=False):
-
- group_name = self._category_name[unicode_categories[0]]
- new_group = self._create_empty_group(group_name)
- glyphs = self._all_glyphs
- for glyph in glyphs:
- if glyph not in excepting:
- if unicodedata.category(glyph) in unicode_categories:
- if self._do_in_first_steps(glyph) or also_composed:
- glyph_layer = self._find_layer(glyph)
- if glyph_layer is not None:
- new_group.add(glyph_layer)
- for glyph in adding:
- if self._do_in_first_steps(glyph) or also_composed:
- glyph_layer = self._find_layer(glyph)
- if glyph_layer is not None:
- new_group.add(glyph_layer)
- if len(new_group) > 0:
- self.svg.append(new_group)
-
- def _add_first_in_second(self, glyph_one, glyph_two):
- layer_one = self._find_layer(glyph_one)
- layer_two = self._find_layer(glyph_two)
- if layer_one is not None and layer_two is not None:
- layer_to_insert = deepcopy(layer_one)
- layer_to_insert.attrib[INKSCAPE_LABEL] = ' ' + glyph_one
- layer_to_insert.pop(INKSCAPE_GROUPMODE)
- layer_to_insert.set("style", "display:inline")
- layer_two.append(layer_to_insert)
-
- def _all_non_composed_letters_by_category(self):
- self._create_and_fill_group(['Lo', 'Lt', 'Lm'])
- self._create_and_fill_group(['Ll'])
- self._create_and_fill_group(['Lu'])
-
- # Step 3
- # Find all non-composed digits and symbols, also find some punctuation signs
-
- def _add_usually_used(self, usually_use):
- for B in usually_use:
- for A in usually_use[B]:
- self._add_first_in_second(A, B)
-
- def _digit_symbols_non_closing_punctuation(self):
-
- usually_use = {}
- usually_use[";"] = [",", "."]
- usually_use[":"] = [".", "."]
- usually_use["!"] = ["."]
- usually_use["?"] = ["."]
- usually_use["!"] = ["."]
- usually_use["_"] = ["-"]
- usually_use["¨"] = [".", "."]
- usually_use["÷"] = [".", "."]
- usually_use["%"] = [".", "."]
- usually_use['0'] = ['O']
- usually_use['1'] = ['l', 'I']
- usually_use['÷'] = ['.', '.']
- usually_use['='] = ['-', '-']
- usually_use['±'] = ['-']
- usually_use['$'] = ['S']
- usually_use["'"] = [',']
- usually_use["·"] = ["."]
- usually_use['"'] = [",", ","]
-
- self._add_usually_used(usually_use)
- self._create_and_fill_group(['Nd', 'Nl', 'No'])
- self._create_and_fill_group(['Sc', 'Sm', 'Sk', 'So'], excepting=[">"])
- self._create_and_fill_group(['Pc', 'Pd', 'Ps', 'Pi', 'Po'], excepting=["¿", "¡", "/"])
-
- # Step 4
- # Punctuation
-
- def _closing_punctuation(self):
- usually_use = {}
- usually_use["¿"] = ["?"]
- usually_use["¡"] = ["!"]
- usually_use[">"] = ["<"]
- usually_use[")"] = ["("]
- usually_use["}"] = ["{"]
- usually_use["]"] = ["["]
- usually_use["»"] = ["«"]
- usually_use['”'] = ['“']
- usually_use["’"] = ["‘"]
- usually_use["/"] = ["\\"]
-
- self._add_usually_used(usually_use)
- self._create_and_fill_group(['Pe', 'Pf'], excepting=[], adding=["¿", "¡", ">", "/"])
- # Step 5
- # There are several sorts of apostrophes and quotes depending on the used language.
- # If there is at least one, let us make sure that we have all those in ["'","’", "ʼ"]
- # Same for quotes
-
- def _find_representative(self, equivalence):
- use_to_represent = None
- for item in equivalence:
- if item in self._all_glyphs:
- use_to_represent = item
- break
- return use_to_represent
-
- def _deal_with_equivalences(self):
- apostrophes = ["'", "’", "ʼ"]
- quotes_opening = ['"', "«", '“']
- quotes_closing = ['"', "»", '”']
- equivalences = [apostrophes, quotes_opening, quotes_closing]
- use_A_in_B = {}
- group_name = _("Additional Punctuation")
- new_group = self._create_empty_group(group_name)
- for equivalence in equivalences:
- use_to_represent = self._find_representative(equivalence)
- if use_to_represent is not None:
- for item in equivalence:
- if item not in self._all_glyphs:
- item_layer = self._create_empty_glyph(item)
- new_group.add(item_layer)
- if use_to_represent not in use_A_in_B:
- use_A_in_B[use_to_represent] = [item]
- else:
- use_A_in_B[use_to_represent].append(item)
-
- if len(new_group) > 0:
- self.svg.append(new_group)
- self._update_glyphs_layers()
- for use_to_represent in use_A_in_B:
- for char in use_A_in_B[use_to_represent]:
- self._add_first_in_second(use_to_represent, char)
-
- # To fill the composed glyphs we need diacritics (COMBINING ACCENT mostly)
- # We may already have some of them already digitized, as a COMBINING ACCENT (Mark category)
- # has sometimes an homoglyph MODIFIER LETTER ACCENT in the letter category and or an homoglyph ACCENT in the
- # symmbol category.
- # At this step we want only diacritics without positioning or doubling info. For instance, we want the font digitizer
- # to create COMBINING ACUTE ACCENT, but to wait till next step for COMBININIG ACUTE ACCENT BELOW
- # COMBINIG ACCENT ABOVE and COMBINING DOUBLE ACUTE ACCENT, not to do the same work several times.
- # create the missing diacritics. If the same drawing letter is here, we will fill the diacritic
- # with it. Many diacritics are the same, except for the positioning. For instance, for COMBINING ACUTE ACCENT
- # has a corresponding letter MODIFIER LETTER ACUTE ACCENT
- # If (for instance) COMBINING ACUTE ACCENT is in the glyphs,we simply brinng it to the new group
- # of letters to be digitized.
- # If it is not here but we have the corresponding MODIFIER LETTER, we create an empty glyph that
- # contains the already digitized letter. If there is no such corresponding LETTER or SYMBOL, we fill
- # the empty glyph with a letter that uses the accent, so that the font digitizer knows what this
- # diacritics is supposed to look like
- def _simplify_name(self, glyph):
- name = unicodedata.name(glyph)
- words = ["DOUBLE", "BELOW", "ABOVE", "INVERTED", "TURNED", "REVERSED"]
- simplified_name = name
- for word in words:
- simplified_name = simplified_name.replace(word, "")
-
- return simplified_name
-
- def _has_simple_name(self, glyph):
-
- words = ["DOUBLE", "BELOW", "ABOVE", "INVERTED", "TURNED", "REVERSED"]
- for word in words:
- if word in unicodedata.name(glyph):
- return False
- return True
-
- def _use_modifier_letter_instead(self, missing_char):
- substitute = None
- if unicodedata.name(missing_char).startswith("COMBINING"):
- letter_name = unicodedata.name(missing_char).replace("COMBINING", "MODIFIER LETTER")
- symbol_name = unicodedata.name(missing_char).replace("COMBINING ", "")
- for glyph in self._all_glyphs:
- if unicodedata.name(glyph) == letter_name or unicodedata.name(glyph) == symbol_name:
- substitute = glyph
- break
- return substitute
-
- def _add_letter_using_piece(self, piece):
- try:
- for char in self._used_in_decompositions[piece]:
- if unicodedata.category(char)[0] == 'L':
- self._add_first_in_second(char, piece)
- break
- except KeyError:
- pass
-
- def _add_simple_diacritics(self):
- missing_group_name = _("Simple Diacritics")
- new_group = self._create_empty_group(missing_group_name)
- fill_now = []
- for glyph in self._all_glyphs:
- if unicodedata.category(glyph) == 'Mn':
- fill_now.append(glyph)
- for glyph in fill_now:
- glyph_layer = self._find_layer(glyph)
- new_group.add(glyph_layer)
-
- for glyph in self._missing:
- if self._has_simple_name(glyph):
- glyph_layer = self._create_empty_glyph(glyph)
- new_group.add(glyph_layer)
- self.svg.append(new_group)
- self._update_glyphs_layers()
- for glyph in self._missing:
- if self._has_simple_name(glyph):
- substitute = self._use_modifier_letter_instead(glyph)
- if substitute is not None:
- self._add_first_in_second(substitute, glyph)
- else:
- self._add_letter_using_piece(glyph)
- # Step 6
- # at this step we deal with other diacritics.
- # if the diacritic is not present, we prefill the created layer with one copy or two of the
- # corresponding simple diacritic, and additionally one letter that does use the diacritics so that the font
- # digitizer can move the simple diacritics to its right position (and then delete the additional letter)
-
- def _find_substitute(self, glyph):
- simplified_name = self._simplify_name(glyph)
- substitute = None
- for candidate in self._all_glyphs:
- if simplified_name.replace(" ", "") == unicodedata.name(candidate).replace(" ", ""):
- substitute = candidate
- break
- if substitute is None:
- if "COMMA" in unicodedata.name(glyph):
- substitute = ','
- if "DOT" in unicodedata.name(glyph):
- substitute = "."
- return substitute
-
- def _add_other_diacritics(self):
- if self._missing == []:
- errormsg(_("nothing to do, you are ready for next step"))
- else:
- missing_group_name = _("Other Diacritics")
- new_group = self._create_empty_group(missing_group_name)
- for glyph in self._missing:
- glyph_layer = self._create_empty_glyph(glyph)
- new_group.add(glyph_layer)
- self.svg.append(new_group)
- self._update_glyphs_layers()
- for glyph in self._missing:
- self._add_letter_using_piece(glyph)
- substitute = self._find_substitute(glyph)
- if substitute is not None:
- self._add_first_in_second(substitute, glyph)
- if "DOUBLE" in unicodedata.name(glyph):
- self._add_first_in_second(substitute, glyph)
-
- # Step 7
- # Proceed with letters with decomposition of length 2
-
- def _fill_two_pieces_letters(self):
- glyphs_to_add = [glyph for glyph in self._all_glyphs if len(self._normalization[glyph]) == 2]
- also_take = [glyph for glyph in self._decomposition
- if len(self._normalization[glyph]) == 1]
-
- if glyphs_to_add == [] and also_take == []:
- errormsg(_("nothing to do, you are ready for next step"))
- else:
- group_name = _("Two pieces letters")
- new_group = self._create_empty_group(group_name)
- for glyph in glyphs_to_add:
- glyph_layer = self._find_layer(glyph)
- new_group.add(glyph_layer)
- for piece in self._normalization[glyph][::-1]:
- self._add_first_in_second(piece, glyph)
-
- for glyph in also_take:
- glyph_layer = self._find_layer(glyph)
- new_group.add(glyph_layer)
- for piece in self._decomposition[glyph][::-1]:
- self._add_first_in_second(piece, glyph)
- self.svg.append(new_group)
-
- # Step 8
- # Proceed with letters with decomposition of length 3
- def _fill_other_letters(self):
- glyphs_to_add = [glyph for glyph in self._all_glyphs if len(self._normalization[glyph]) == 3]
- also_take = [glyph for glyph in self._all_glyphs
- if len(self._normalization[glyph]) > 3]
-
- if glyphs_to_add == [] and also_take == []:
- errormsg(_("nothing to do, you are ready for next step"))
- else:
- group_name = _("Other composed letters")
- new_group = self._create_empty_group(group_name)
- for glyph in glyphs_to_add:
- glyph_layer = self._find_layer(glyph)
- new_group.add(glyph_layer)
- for piece in self._decomposition[glyph]:
- self._add_first_in_second(piece, glyph)
- for glyph in also_take:
- glyph_layer = self._find_layer(glyph)
- new_group.add(glyph_layer)
- for piece in self._normalization[glyph]:
- self._add_first_in_second(piece, glyph)
- self.svg.append(new_group)
-
- def _sort_by_category(self):
- self._create_and_fill_group(['Co', 'Cf', 'Cc', 'Cs'], [], [], also_composed=True)
- self._create_and_fill_group(['Mn', 'Mc', 'Me'], [], [], also_composed=True)
- self._create_and_fill_group(['Nd', 'Nl', 'No'], [], [], also_composed=True)
- self._create_and_fill_group(['Sc', 'Sm', 'Sk', 'So'], [], [], also_composed=True)
- self._create_and_fill_group(['Pc', 'Pd', 'Ps', 'Pi', 'Po', 'Pe', 'Pf'], [], [], also_composed=True)
- self._create_and_fill_group(['Lo', 'Lt', 'Lm'], [], [], also_composed=True)
- self._create_and_fill_group(['Ll'], [], [], also_composed=True)
- self._create_and_fill_group(['Lu'], [], [], also_composed=True)
-
- def _additional_actions(self):
- # These last actions may be used any time on any font file
-
- if self.options.action == 'duplicate':
- self._look_for_duplicate(verbose=True)
-
- if self.options.action == 'sort':
- self._sort_by_category()
- self._remove_empty_groups()
-
- def effect(self):
- self.svg = self.document.getroot()
- self._update_glyphs_layers()
- self._update_all_glyphs()
- self._fill_decompose_lists()
-
- if self.options.action == 'step1':
- self._remove_invalid_glyphs()
- self._look_for_duplicate()
- self._add_chars([',', '.', '-'])
-
- if self.options.action == 'step2':
- self._all_non_composed_letters_by_category()
- self._remove_empty_groups()
-
- if self.options.action == 'step3':
- self._digit_symbols_non_closing_punctuation()
- self._remove_empty_groups()
-
- if self.options.action == 'step4':
- self._closing_punctuation()
- self._remove_empty_groups()
-
- if self.options.action == 'step5':
- self._deal_with_equivalences()
- self._add_simple_diacritics()
- self._remove_empty_groups()
-
- if self.options.action == 'step6':
- self._add_other_diacritics()
- self._remove_empty_groups()
-
- if self.options.action == 'step7':
- self._fill_two_pieces_letters()
- self._remove_empty_groups()
-
- if self.options.action == 'step8':
- self._fill_other_letters()
- self._remove_empty_groups()
-
- self._additional_actions()
diff --git a/lib/extensions/lettering_organize_glyphs.py b/lib/extensions/lettering_organize_glyphs.py
new file mode 100644
index 00000000..067cc6db
--- /dev/null
+++ b/lib/extensions/lettering_organize_glyphs.py
@@ -0,0 +1,573 @@
+# Authors: see git history
+#
+# Copyright (c) 2025 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+import unicodedata
+from inkex import NSS, Group, Layer, errormsg
+from copy import deepcopy
+from ..svg.tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SODIPODI_INSENSITIVE, SVG_GROUP_TAG)
+from .base import InkstitchExtension
+from ..i18n import _
+
+
+class LetteringOrganizeGlyphs(InkstitchExtension):
+ """_summary_
+ The goal of this extension is to help the font digitizer with steps to organize its work.
+ At each step a group of glyphs is brought to the top of the object stack, and the font
+ digitizer should digitize these glyphs before going to the next step.
+ Steps are organized in such a way as to break the work into smaller chunks and maximize reuse of already digitized letters.
+
+ unicodedata is used to decomposed letters into pieces.
+ Furthermore the extension use some additional information, such as "i" and "j" usually reuse
+ the digitalization of "."
+ """
+
+ def __init__(self, *args, **kwargs):
+ InkstitchExtension.__init__(self, *args, **kwargs)
+ self.arg_parser.add_argument("--tabs")
+ self.arg_parser.add_argument("-c", "--action", dest="action", type=str, default="none")
+ self._glyphs_layers = [] # list of all Layers ()
+ self._all_glyphs = [] # list of chr, (one per layer)
+ # constructed using unicodedata.decomposition
+ self._decomposition = {} # keys are glyphs, value are the list of pieces in the decomposition
+ self._used_in_decompositions = {} # reverse dictionary
+ self._normalization = {} # constructed using unicodedata.normalize('NFD', glyph)
+
+ self._pieces = [] # all the pieces for all decomposable glyphs
+ self._missing = [] # pieces not already among _all_glyphs
+
+ self._category_name = self._category_name()
+
+ def _remove_empty_groups(self):
+ for group in self.svg.iterdescendants(SVG_GROUP_TAG):
+ if len(group.getchildren()) == 0:
+ group.delete()
+
+ def _update_glyphs_layers(self):
+ self._glyphs_layers = self.svg.xpath('.//svg:g[starts-with(@inkscape:label, "GlyphLayer-")]', namespaces=NSS)
+
+ def _category_name(self):
+ category_name = {}
+ category_name["Lu"] = _("Upper Case Letters")
+ category_name["Ll"] = _('Lower Case Letters')
+ category_name["Lo"] = _('Other Letters')
+ category_name["Nd"] = _("Digits")
+ category_name["Sc"] = _('Symbols')
+ category_name["Pc"] = _('Punctuation')
+ category_name["Pe"] = _('Closing Punctuation')
+ category_name["Mn"] = _('Diacritics')
+ category_name["Co"] = _('Special')
+
+ return category_name
+
+ def _update_all_glyphs(self):
+ # only consider GlyphLayer we know the unicode they represent, that means their name is a single letter
+ self._update_glyphs_layers()
+ for layer in self._glyphs_layers:
+ name = layer.attrib[INKSCAPE_LABEL]
+ name = name.replace("GlyphLayer-", "", 1)
+ if len(name) == 1:
+ self._all_glyphs.append(name)
+
+ def _fill_decompose_lists(self):
+ # NFD normalization decomposes 'Ṓ' into three characters, letter O, macron accent and acute accent,
+ # unicodedata.decomposition(Ṓ) splits it into two entry points, one for 'Ō' and one for the acute accent
+ # NFD normalization of 'a' is simply 'a', while unicodedata.decomposition('a') is an empty string.
+ # unicodedata.decomposition also splits a subscript letter into a keyword for subscript and the entry point
+ # of the corresponding letter, this is just an example, the normalization of the subscript letter being
+ # the subscript letter itself. We do need both!
+
+ for glyph in self._all_glyphs:
+ normalization = [char for char in unicodedata.normalize('NFD', glyph)]
+ decomposition = []
+ for code in unicodedata.decomposition(glyph).split(' '):
+ try:
+ piece = chr(int(code, 16)) # convert entry point into unicode character
+ # we don't need the space separator nor the other separators as pieces, as there
+ # is nothing to render for them
+ if unicodedata.category(piece)[0] != 'Z':
+ decomposition.append(piece)
+ except ValueError: # this will eliminate keywords as they do not convert to int
+ pass
+ if decomposition != []:
+ self._decomposition[glyph] = decomposition
+ if unicodedata.decomposition(glyph) == "":
+ decomposition = [glyph]
+ self._normalization[glyph] = normalization
+ for piece in decomposition:
+ if piece not in self._used_in_decompositions:
+ self._used_in_decompositions[piece] = [glyph]
+ else:
+ self._used_in_decompositions[piece].append(glyph)
+ self._pieces = [piece for piece in self._used_in_decompositions]
+ self._missing = [piece for piece in self._pieces
+ if piece not in self._all_glyphs and len(self._used_in_decompositions[piece]) > 1]
+
+ def _find_layer(self, char):
+ for layer in self._glyphs_layers:
+ label = layer.attrib[INKSCAPE_LABEL]
+ label = label.replace("GlyphLayer-", "", 1)
+ if SODIPODI_INSENSITIVE in layer.attrib:
+ layer.attrib.pop(SODIPODI_INSENSITIVE)
+ if len(label) == 1 and label == char:
+ return layer
+ return None
+
+ def _remove(self, char):
+ char_layer = self._find_layer(char)
+ if char_layer is not None:
+ char_layer.delete()
+
+ def _create_empty_glyph(self, char):
+ new_layer = self.svg.add(Layer.new("GlyphLayer-" + char))
+ new_layer.set("style", "display:none")
+ return new_layer
+
+ # Step 0: check for duplicate and remove unwanted layer
+
+ def _look_for_duplicate(self, verbose=False):
+ if len(self._all_glyphs) != len(set(self._all_glyphs)):
+ duplicated_glyphs = " ".join(
+ [glyph for glyph in set(self._all_glyphs) if self._all_glyphs.count(glyph) > 1]
+ )
+ errormsg(_("Found duplicated glyphs in font file: {duplicated_glyphs}").format(duplicated_glyphs=duplicated_glyphs))
+
+ for letter in duplicated_glyphs:
+ errormsg((unicodedata.name(letter)))
+ else:
+ if verbose:
+ errormsg(_("No duplicated glyph found"))
+
+ def _is_valid(self, char):
+ # sometimes one grabs a non rendering char in the ttf file, and it results in
+ # an invalid glyph (d='')
+ if char == "":
+ return True
+ category = unicodedata.category(char)
+ if category[0] in ['Z', 'C'] and category != 'Co':
+ return False
+ return True
+
+ def _remove_invalid_glyphs(self):
+ for layer in self._glyphs_layers:
+ name = layer.attrib[INKSCAPE_LABEL]
+ name = name.replace("GlyphLayer-", "", 1)
+ if name == "" or name == ".null" or (len(name) == 1 and not self._is_valid(name)):
+ layer.delete()
+
+ # Step1 time to digitize comma, hyphen, and period:
+ # move comma, hyphen and period on top
+ # Lock all other glyphs
+
+ def _lock_and_hide_all_layers(self):
+ self._update_glyphs_layers()
+ for layer in self._glyphs_layers:
+ layer.set(SODIPODI_INSENSITIVE, True)
+ layer.set("style", "display:none")
+
+ def _move_on_top(self, layer):
+ if SODIPODI_INSENSITIVE in layer.attrib:
+ layer.pop(SODIPODI_INSENSITIVE)
+ copy_layer = deepcopy(layer)
+ layer.delete()
+ self.svg.append(copy_layer)
+
+ def _move_char_on_top(self, char):
+ warning = False
+ layer_char = self._find_layer(char)
+ if layer_char is None:
+ warning = True
+ self._create_empty_glyph(char)
+ else:
+ self._move_on_top(layer_char)
+ return warning
+
+ def _add_chars(self, char_list):
+ self._lock_and_hide_all_layers()
+ added = ""
+ for char in char_list:
+ if self._move_char_on_top(char):
+ added = added + char + " "
+ if added != "":
+ added_char__warning = _(
+ "This or these glyphs have been added:\n"
+ "{added_char}\n"
+ "Either fill them or delete them").format(added_char=added)
+ errormsg(added_char__warning)
+
+ # Step 2
+ # Find all non-composed letters (no use of diacritic allowed)
+ # Group them by category, all upper cases, lower cases and other
+
+ def _create_empty_group(self, group_name):
+ new_group = Group()
+ new_group.label = group_name
+ return new_group
+
+ def _do_in_first_steps(self, glyph):
+ # check if a glyph can be digitalize without waiting for some of its piece to be digitalized first
+ if unicodedata.decomposition(glyph) == "":
+ return True
+ # There is a decomposition, but the decomposition is in only one piece, and this piece
+ # is not used for anything else.
+ if len(self._decomposition[glyph]) == 1:
+ piece = self._decomposition[glyph][0]
+ if len(self._used_in_decompositions[piece]) == 1:
+ return True
+ return False
+
+ def _create_and_fill_group(self, unicode_categories, excepting=[], adding=[], also_composed=False):
+
+ group_name = self._category_name[unicode_categories[0]]
+ new_group = self._create_empty_group(group_name)
+ glyphs = self._all_glyphs
+ for glyph in glyphs:
+ if glyph not in excepting:
+ if unicodedata.category(glyph) in unicode_categories:
+ if self._do_in_first_steps(glyph) or also_composed:
+ glyph_layer = self._find_layer(glyph)
+ if glyph_layer is not None:
+ new_group.add(glyph_layer)
+ for glyph in adding:
+ if self._do_in_first_steps(glyph) or also_composed:
+ glyph_layer = self._find_layer(glyph)
+ if glyph_layer is not None:
+ new_group.add(glyph_layer)
+ if len(new_group) > 0:
+ self.svg.append(new_group)
+
+ def _add_first_in_second(self, glyph_one, glyph_two):
+ layer_one = self._find_layer(glyph_one)
+ layer_two = self._find_layer(glyph_two)
+ if layer_one is not None and layer_two is not None:
+ layer_to_insert = deepcopy(layer_one)
+ layer_to_insert.attrib[INKSCAPE_LABEL] = ' ' + glyph_one
+ layer_to_insert.pop(INKSCAPE_GROUPMODE)
+ layer_to_insert.set("style", "display:inline")
+ layer_two.append(layer_to_insert)
+
+ def _all_non_composed_letters_by_category(self):
+ self._create_and_fill_group(['Lo', 'Lt', 'Lm'])
+ self._create_and_fill_group(['Ll'])
+ self._create_and_fill_group(['Lu'])
+
+ # Step 3
+ # Find all non-composed digits and symbols, also find some punctuation signs
+
+ def _add_usually_used(self, usually_use):
+ for B in usually_use:
+ for A in usually_use[B]:
+ self._add_first_in_second(A, B)
+
+ def _digit_symbols_non_closing_punctuation(self):
+
+ usually_use = {}
+ usually_use[";"] = [",", "."]
+ usually_use[":"] = [".", "."]
+ usually_use["!"] = ["."]
+ usually_use["?"] = ["."]
+ usually_use["!"] = ["."]
+ usually_use["_"] = ["-"]
+ usually_use["¨"] = [".", "."]
+ usually_use["÷"] = [".", "."]
+ usually_use["%"] = [".", "."]
+ usually_use['0'] = ['O']
+ usually_use['1'] = ['l', 'I']
+ usually_use['÷'] = ['.', '.']
+ usually_use['='] = ['-', '-']
+ usually_use['±'] = ['-']
+ usually_use['$'] = ['S']
+ usually_use["'"] = [',']
+ usually_use["·"] = ["."]
+ usually_use['"'] = [",", ","]
+
+ self._add_usually_used(usually_use)
+ self._create_and_fill_group(['Nd', 'Nl', 'No'])
+ self._create_and_fill_group(['Sc', 'Sm', 'Sk', 'So'], excepting=[">"])
+ self._create_and_fill_group(['Pc', 'Pd', 'Ps', 'Pi', 'Po'], excepting=["¿", "¡", "/"])
+
+ # Step 4
+ # Punctuation
+
+ def _closing_punctuation(self):
+ usually_use = {}
+ usually_use["¿"] = ["?"]
+ usually_use["¡"] = ["!"]
+ usually_use[">"] = ["<"]
+ usually_use[")"] = ["("]
+ usually_use["}"] = ["{"]
+ usually_use["]"] = ["["]
+ usually_use["»"] = ["«"]
+ usually_use['”'] = ['“']
+ usually_use["’"] = ["‘"]
+ usually_use["/"] = ["\\"]
+
+ self._add_usually_used(usually_use)
+ self._create_and_fill_group(['Pe', 'Pf'], excepting=[], adding=["¿", "¡", ">", "/"])
+ # Step 5
+ # There are several sorts of apostrophes and quotes depending on the used language.
+ # If there is at least one, let us make sure that we have all those in ["'","’", "ʼ"]
+ # Same for quotes
+
+ def _find_representative(self, equivalence):
+ use_to_represent = None
+ for item in equivalence:
+ if item in self._all_glyphs:
+ use_to_represent = item
+ break
+ return use_to_represent
+
+ def _deal_with_equivalences(self):
+ apostrophes = ["'", "’", "ʼ"]
+ quotes_opening = ['"', "«", '“']
+ quotes_closing = ['"', "»", '”']
+ equivalences = [apostrophes, quotes_opening, quotes_closing]
+ use_A_in_B = {}
+ group_name = _("Additional Punctuation")
+ new_group = self._create_empty_group(group_name)
+ for equivalence in equivalences:
+ use_to_represent = self._find_representative(equivalence)
+ if use_to_represent is not None:
+ for item in equivalence:
+ if item not in self._all_glyphs:
+ item_layer = self._create_empty_glyph(item)
+ new_group.add(item_layer)
+ if use_to_represent not in use_A_in_B:
+ use_A_in_B[use_to_represent] = [item]
+ else:
+ use_A_in_B[use_to_represent].append(item)
+
+ if len(new_group) > 0:
+ self.svg.append(new_group)
+ self._update_glyphs_layers()
+ for use_to_represent in use_A_in_B:
+ for char in use_A_in_B[use_to_represent]:
+ self._add_first_in_second(use_to_represent, char)
+
+ # To fill the composed glyphs we need diacritics (COMBINING ACCENT mostly)
+ # We may already have some of them already digitized, as a COMBINING ACCENT (Mark category)
+ # has sometimes an homoglyph MODIFIER LETTER ACCENT in the letter category and or an homoglyph ACCENT in the
+ # symmbol category.
+ # At this step we want only diacritics without positioning or doubling info. For instance, we want the font digitizer
+ # to create COMBINING ACUTE ACCENT, but to wait till next step for COMBININIG ACUTE ACCENT BELOW
+ # COMBINIG ACCENT ABOVE and COMBINING DOUBLE ACUTE ACCENT, not to do the same work several times.
+ # create the missing diacritics. If the same drawing letter is here, we will fill the diacritic
+ # with it. Many diacritics are the same, except for the positioning. For instance, for COMBINING ACUTE ACCENT
+ # has a corresponding letter MODIFIER LETTER ACUTE ACCENT
+ # If (for instance) COMBINING ACUTE ACCENT is in the glyphs,we simply brinng it to the new group
+ # of letters to be digitized.
+ # If it is not here but we have the corresponding MODIFIER LETTER, we create an empty glyph that
+ # contains the already digitized letter. If there is no such corresponding LETTER or SYMBOL, we fill
+ # the empty glyph with a letter that uses the accent, so that the font digitizer knows what this
+ # diacritics is supposed to look like
+ def _simplify_name(self, glyph):
+ name = unicodedata.name(glyph)
+ words = ["DOUBLE", "BELOW", "ABOVE", "INVERTED", "TURNED", "REVERSED"]
+ simplified_name = name
+ for word in words:
+ simplified_name = simplified_name.replace(word, "")
+
+ return simplified_name
+
+ def _has_simple_name(self, glyph):
+
+ words = ["DOUBLE", "BELOW", "ABOVE", "INVERTED", "TURNED", "REVERSED"]
+ for word in words:
+ if word in unicodedata.name(glyph):
+ return False
+ return True
+
+ def _use_modifier_letter_instead(self, missing_char):
+ substitute = None
+ if unicodedata.name(missing_char).startswith("COMBINING"):
+ letter_name = unicodedata.name(missing_char).replace("COMBINING", "MODIFIER LETTER")
+ symbol_name = unicodedata.name(missing_char).replace("COMBINING ", "")
+ for glyph in self._all_glyphs:
+ if unicodedata.name(glyph) == letter_name or unicodedata.name(glyph) == symbol_name:
+ substitute = glyph
+ break
+ return substitute
+
+ def _add_letter_using_piece(self, piece):
+ try:
+ for char in self._used_in_decompositions[piece]:
+ if unicodedata.category(char)[0] == 'L':
+ self._add_first_in_second(char, piece)
+ break
+ except KeyError:
+ pass
+
+ def _add_simple_diacritics(self):
+ missing_group_name = _("Simple Diacritics")
+ new_group = self._create_empty_group(missing_group_name)
+ fill_now = []
+ for glyph in self._all_glyphs:
+ if unicodedata.category(glyph) == 'Mn':
+ fill_now.append(glyph)
+ for glyph in fill_now:
+ glyph_layer = self._find_layer(glyph)
+ new_group.add(glyph_layer)
+
+ for glyph in self._missing:
+ if self._has_simple_name(glyph):
+ glyph_layer = self._create_empty_glyph(glyph)
+ new_group.add(glyph_layer)
+ self.svg.append(new_group)
+ self._update_glyphs_layers()
+ for glyph in self._missing:
+ if self._has_simple_name(glyph):
+ substitute = self._use_modifier_letter_instead(glyph)
+ if substitute is not None:
+ self._add_first_in_second(substitute, glyph)
+ else:
+ self._add_letter_using_piece(glyph)
+ # Step 6
+ # at this step we deal with other diacritics.
+ # if the diacritic is not present, we prefill the created layer with one copy or two of the
+ # corresponding simple diacritic, and additionally one letter that does use the diacritics so that the font
+ # digitizer can move the simple diacritics to its right position (and then delete the additional letter)
+
+ def _find_substitute(self, glyph):
+ simplified_name = self._simplify_name(glyph)
+ substitute = None
+ for candidate in self._all_glyphs:
+ if simplified_name.replace(" ", "") == unicodedata.name(candidate).replace(" ", ""):
+ substitute = candidate
+ break
+ if substitute is None:
+ if "COMMA" in unicodedata.name(glyph):
+ substitute = ','
+ if "DOT" in unicodedata.name(glyph):
+ substitute = "."
+ return substitute
+
+ def _add_other_diacritics(self):
+ if self._missing == []:
+ errormsg(_("nothing to do, you are ready for next step"))
+ else:
+ missing_group_name = _("Other Diacritics")
+ new_group = self._create_empty_group(missing_group_name)
+ for glyph in self._missing:
+ glyph_layer = self._create_empty_glyph(glyph)
+ new_group.add(glyph_layer)
+ self.svg.append(new_group)
+ self._update_glyphs_layers()
+ for glyph in self._missing:
+ self._add_letter_using_piece(glyph)
+ substitute = self._find_substitute(glyph)
+ if substitute is not None:
+ self._add_first_in_second(substitute, glyph)
+ if "DOUBLE" in unicodedata.name(glyph):
+ self._add_first_in_second(substitute, glyph)
+
+ # Step 7
+ # Proceed with letters with decomposition of length 2
+
+ def _fill_two_pieces_letters(self):
+ glyphs_to_add = [glyph for glyph in self._all_glyphs if len(self._normalization[glyph]) == 2]
+ also_take = [glyph for glyph in self._decomposition
+ if len(self._normalization[glyph]) == 1]
+
+ if glyphs_to_add == [] and also_take == []:
+ errormsg(_("nothing to do, you are ready for next step"))
+ else:
+ group_name = _("Two pieces letters")
+ new_group = self._create_empty_group(group_name)
+ for glyph in glyphs_to_add:
+ glyph_layer = self._find_layer(glyph)
+ new_group.add(glyph_layer)
+ for piece in self._normalization[glyph][::-1]:
+ self._add_first_in_second(piece, glyph)
+
+ for glyph in also_take:
+ glyph_layer = self._find_layer(glyph)
+ new_group.add(glyph_layer)
+ for piece in self._decomposition[glyph][::-1]:
+ self._add_first_in_second(piece, glyph)
+ self.svg.append(new_group)
+
+ # Step 8
+ # Proceed with letters with decomposition of length 3
+ def _fill_other_letters(self):
+ glyphs_to_add = [glyph for glyph in self._all_glyphs if len(self._normalization[glyph]) == 3]
+ also_take = [glyph for glyph in self._all_glyphs
+ if len(self._normalization[glyph]) > 3]
+
+ if glyphs_to_add == [] and also_take == []:
+ errormsg(_("nothing to do, you are ready for next step"))
+ else:
+ group_name = _("Other composed letters")
+ new_group = self._create_empty_group(group_name)
+ for glyph in glyphs_to_add:
+ glyph_layer = self._find_layer(glyph)
+ new_group.add(glyph_layer)
+ for piece in self._decomposition[glyph]:
+ self._add_first_in_second(piece, glyph)
+ for glyph in also_take:
+ glyph_layer = self._find_layer(glyph)
+ new_group.add(glyph_layer)
+ for piece in self._normalization[glyph]:
+ self._add_first_in_second(piece, glyph)
+ self.svg.append(new_group)
+
+ def _sort_by_category(self):
+ self._create_and_fill_group(['Co', 'Cf', 'Cc', 'Cs'], [], [], also_composed=True)
+ self._create_and_fill_group(['Mn', 'Mc', 'Me'], [], [], also_composed=True)
+ self._create_and_fill_group(['Nd', 'Nl', 'No'], [], [], also_composed=True)
+ self._create_and_fill_group(['Sc', 'Sm', 'Sk', 'So'], [], [], also_composed=True)
+ self._create_and_fill_group(['Pc', 'Pd', 'Ps', 'Pi', 'Po', 'Pe', 'Pf'], [], [], also_composed=True)
+ self._create_and_fill_group(['Lo', 'Lt', 'Lm'], [], [], also_composed=True)
+ self._create_and_fill_group(['Ll'], [], [], also_composed=True)
+ self._create_and_fill_group(['Lu'], [], [], also_composed=True)
+
+ def _additional_actions(self):
+ # These last actions may be used any time on any font file
+
+ if self.options.action == 'duplicate':
+ self._look_for_duplicate(verbose=True)
+
+ if self.options.action == 'sort':
+ self._sort_by_category()
+ self._remove_empty_groups()
+
+ def effect(self):
+ self.svg = self.document.getroot()
+ self._update_glyphs_layers()
+ self._update_all_glyphs()
+ self._fill_decompose_lists()
+
+ if self.options.action == 'step1':
+ self._remove_invalid_glyphs()
+ self._look_for_duplicate()
+ self._add_chars([',', '.', '-'])
+
+ if self.options.action == 'step2':
+ self._all_non_composed_letters_by_category()
+ self._remove_empty_groups()
+
+ if self.options.action == 'step3':
+ self._digit_symbols_non_closing_punctuation()
+ self._remove_empty_groups()
+
+ if self.options.action == 'step4':
+ self._closing_punctuation()
+ self._remove_empty_groups()
+
+ if self.options.action == 'step5':
+ self._deal_with_equivalences()
+ self._add_simple_diacritics()
+ self._remove_empty_groups()
+
+ if self.options.action == 'step6':
+ self._add_other_diacritics()
+ self._remove_empty_groups()
+
+ if self.options.action == 'step7':
+ self._fill_two_pieces_letters()
+ self._remove_empty_groups()
+
+ if self.options.action == 'step8':
+ self._fill_other_letters()
+ self._remove_empty_groups()
+
+ self._additional_actions()
diff --git a/templates/lettering_fill_composed_glyphs.xml b/templates/lettering_fill_composed_glyphs.xml
deleted file mode 100644
index 9ebf72c0..00000000
--- a/templates/lettering_fill_composed_glyphs.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
- Fill Composed Glyphs
- org.{{ id_inkstitch }}.lettering_fill_composed_glyphs
- lettering_fill_composed_glyphs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
- {{ icon_path }}inx/font_management.svg
- Help with composed glyphs like é,ç or ï
-
-
-
-
-
-
-
-
diff --git a/templates/lettering_organize_glyphs.xml b/templates/lettering_organize_glyphs.xml
new file mode 100644
index 00000000..b89ffa76
--- /dev/null
+++ b/templates/lettering_organize_glyphs.xml
@@ -0,0 +1,49 @@
+
+
+ Organize Glyphs
+ org.{{ id_inkstitch }}.organize_glyphs
+ lettering_organize_glyphs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ {{ icon_path }}inx/font_management.svg
+ Help with composed glyphs like é,ç or ï
+
+
+
+
+
+
+
+
--
cgit v1.2.3