summaryrefslogtreecommitdiff
path: root/lib/extensions/lettering_fill_composed_glyphs.py
blob: edc1dd81c5789b094a95f8c34b704139cf92d0c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
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 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()