diff options
| author | Kaalleen <36401965+kaalleen@users.noreply.github.com> | 2025-10-27 18:09:21 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-27 18:09:21 +0100 |
| commit | 6f282829537b69b8628b901cc717f18222ff33a0 (patch) | |
| tree | d703e389a395f021836816ef1e0e5fe509474fc8 /lib | |
| parent | e8859c4e14984bcdb3dd6d3c9bfed05cd233251c (diff) | |
Lettering: add spacing options (#4020)
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/extensions/batch_lettering.py | 17 | ||||
| -rw-r--r-- | lib/extensions/lettering_along_path.py | 25 | ||||
| -rw-r--r-- | lib/gui/lettering/main_panel.py | 11 | ||||
| -rw-r--r-- | lib/gui/lettering/option_panel.py | 90 | ||||
| -rw-r--r-- | lib/lettering/font.py | 27 | ||||
| -rw-r--r-- | lib/lettering/utils.py | 4 |
6 files changed, 123 insertions, 51 deletions
diff --git a/lib/extensions/batch_lettering.py b/lib/extensions/batch_lettering.py index c0581e76..045a1223 100644 --- a/lib/extensions/batch_lettering.py +++ b/lib/extensions/batch_lettering.py @@ -42,6 +42,9 @@ class BatchLettering(InkstitchExtension): self.arg_parser.add_argument('--trim', type=str, default='off', dest='trim') self.arg_parser.add_argument('--use-command-symbols', type=Boolean, default=False, dest='command_symbols') self.arg_parser.add_argument('--text-align', type=str, default='left', dest='text_align') + self.arg_parser.add_argument('--letter_spacing', type=float, default=0, dest='letter_spacing') + self.arg_parser.add_argument('--word_spacing', type=float, default=0, dest='word_spacing') + self.arg_parser.add_argument('--line_height', type=float, default=0, dest='line_height') self.arg_parser.add_argument('--text-position', type=str, default='left', dest='text_position') @@ -60,7 +63,7 @@ class BatchLettering(InkstitchExtension): if not self.options.font: errormsg(_("Please specify a font")) return - self.font = get_font_by_name(self.options.font) + self.font = get_font_by_name(self.options.font, False) if self.font is None: errormsg(_("Please specify a valid font name.")) errormsg(_("You can find a list with all font names on our website: https://inkstitch.org/fonts/font-library/")) @@ -194,10 +197,13 @@ class BatchLettering(InkstitchExtension): "text_align": self.text_align, "back_and_forth": True, "font": self.font.marked_custom_font_id, - "scale": self.scale * 100, + "scale": int(self.scale * 100), "trim_option": self.trim, "use_trim_symbols": self.options.command_symbols, - "color_sort": self.color_sort + "color_sort": self.color_sort, + "letter_spacing": self.options.letter_spacing, + "word_spacing": self.options.word_spacing, + "line_height": self.options.line_height }) lettering_group = Group() @@ -216,7 +222,10 @@ class BatchLettering(InkstitchExtension): trim_option=self.trim, use_trim_symbols=self.options.command_symbols, color_sort=self.color_sort, - text_align=self.text_align + text_align=self.text_align, + letter_spacing=self.options.letter_spacing, + word_spacing=self.options.word_spacing, + line_height=self.options.line_height ) destination_group.attrib['transform'] = f'scale({self.scale})' diff --git a/lib/extensions/lettering_along_path.py b/lib/extensions/lettering_along_path.py index 6f1ca7a6..4afc4416 100644 --- a/lib/extensions/lettering_along_path.py +++ b/lib/extensions/lettering_along_path.py @@ -72,11 +72,7 @@ class TextAlongPath: self.text = text self.path = Stroke(path).as_multi_line_string().geoms[0] self.text_position = text_position - - self.glyphs = [glyph for glyph in self.text.iterdescendants(SVG_GROUP_TAG) if glyph.get('inkstitch:letter-group', '') == 'glyph'] - if not self.glyphs: - errormsg(_("The text doesn't contain any glyphs.")) - return + self.glyphs = [] self.load_settings() self.font = get_font_by_id(self.settings.font, False) @@ -87,6 +83,10 @@ class TextAlongPath: self.font_scale = self.settings.scale / 100 self._reset_glyph_transforms() + if not self.glyphs: + errormsg(_("The text doesn't contain any glyphs.")) + return + hidden_commands = self.hide_commands() self.glyphs_along_path() self.restore_commands(hidden_commands) @@ -109,8 +109,14 @@ class TextAlongPath: None, # we don't know the font variant (?) self.settings.back_and_forth, self.settings.trim_option, - self.settings.use_trim_symbols + self.settings.use_trim_symbols, + 0, # color sort breaks the glyph structure needed for this method + self.settings.text_align, + self.settings.letter_spacing, + self.settings.word_spacing, + self.settings.line_height ) + self.glyphs = [glyph for glyph in rendered_text.iterdescendants(SVG_GROUP_TAG) if glyph.get('inkstitch:letter-group', '') == 'glyph'] self.bake_transforms_recursively(text_group) @@ -230,7 +236,12 @@ class TextAlongPath: "font": None, "scale": 100, "trim_option": 0, - "use_trim_symbols": False + "use_trim_symbols": False, + "color_sort": 0, + "text_align": 0, + "letter_spacing": 0, + "word_spacing": 0, + "line_height": 0 }) if INKSTITCH_LETTERING in self.text.attrib: diff --git a/lib/gui/lettering/main_panel.py b/lib/gui/lettering/main_panel.py index 1766516e..af5af494 100644 --- a/lib/gui/lettering/main_panel.py +++ b/lib/gui/lettering/main_panel.py @@ -82,7 +82,10 @@ class LetteringPanel(wx.Panel): "scale": 100, "trim_option": global_settings['lettering_trim_option'], "use_trim_symbols": global_settings['lettering_use_command_symbols'], - "color_sort": 0 + "color_sort": 0, + "letter_spacing": 0, + "word_spacing": 0, + "line_height": 0 }) if INKSTITCH_LETTERING in self.group.attrib: @@ -106,6 +109,9 @@ class LetteringPanel(wx.Panel): self.options_panel.use_trim_symbols.SetValue(bool(self.settings.use_trim_symbols)) self.options_panel.text_editor.SetValue(self.settings.text) self.options_panel.scale_spinner.SetValue(self.settings.scale) + self.options_panel.letter_spacing.SetValue(self.settings.letter_spacing) + self.options_panel.word_spacing.SetValue(self.settings.word_spacing) + self.options_panel.line_height.SetValue(self.settings.line_height) self.set_initial_font(self.settings.font) def save_settings(self): @@ -313,7 +319,8 @@ class LetteringPanel(wx.Panel): font.render_text( self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, trim_option=self.settings.trim_option, use_trim_symbols=self.settings.use_trim_symbols, - color_sort=self.settings.color_sort, text_align=self.settings.text_align + color_sort=self.settings.color_sort, text_align=self.settings.text_align, + letter_spacing=self.settings.letter_spacing, word_spacing=self.settings.word_spacing, line_height=self.settings.line_height ) except FontError as e: if raise_error: diff --git a/lib/gui/lettering/option_panel.py b/lib/gui/lettering/option_panel.py index e768a29b..f0bd7846 100644 --- a/lib/gui/lettering/option_panel.py +++ b/lib/gui/lettering/option_panel.py @@ -34,11 +34,11 @@ class LetteringOptionsPanel(ScrolledPanel): self.filter_box = wx.StaticBox(self, wx.ID_ANY, label=_("Font Filter")) filter_sizer = wx.StaticBoxSizer(self.filter_box, wx.HORIZONTAL) filter_size_label = wx.StaticText(self, wx.ID_ANY, _("Size")) - filter_sizer.Add(filter_size_label, 0, wx.LEFT | wx.TOP | wx.BOTTOM, 10) + filter_sizer.Add(filter_size_label, 0, wx.LEFT | wx.TOP | wx.BOTTOM | wx.ALIGN_CENTER_VERTICAL, 10) filter_sizer.AddSpacer(5) filter_sizer.Add(self.font_size_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) filter_sizer.AddSpacer(5) - filter_sizer.Add(self.font_glyph_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) + filter_sizer.Add(self.font_glyph_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM | wx.ALIGN_CENTER_VERTICAL, 10) filter_sizer.Add(self.font_category_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) outer_sizer.Add(filter_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) @@ -77,19 +77,71 @@ class LetteringOptionsPanel(ScrolledPanel): font_selector_sizer.Add(font_description_sizer, 0, wx.EXPAND | wx.ALL, 10) outer_sizer.Add(font_selector_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) - # options + # sizing and alignment + scale_spinner_label = wx.StaticText(self, wx.ID_ANY, _("Scale (%)")) self.scale_spinner = wx.SpinCtrl(self, wx.ID_ANY, min=0, max=1000, initial=100) - self.scale_spinner.Bind(wx.EVT_SPINCTRL, lambda event: self.panel.on_change("scale", event)) - - self.back_and_forth_checkbox = wx.CheckBox(self, label=_("Stitch lines of text back and forth")) - self.back_and_forth_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.panel.on_change("back_and_forth", event)) - align_text_label = wx.StaticText(self, wx.ID_ANY, _("Align Text")) self.align_text_choice = wx.Choice( self, choices=[_("Left"), _("Center"), _("Right"), _("Block (default)"), _("Block (letterspacing)")] ) + + self.spacing_box = wx.StaticBox(self, wx.ID_ANY, label=_("Sizing and alignment")) + letter_spacing_label = wx.StaticText(self, wx.ID_ANY, _("Letter spacing (mm)")) + letter_spacing_label.SetToolTip(_("Additional letter spacing in mm.")) + self.letter_spacing = wx.SpinCtrlDouble(self, min=-500, max=500, inc=0.01, initial=0, style=wx.SP_WRAP) + word_spacing_label = wx.StaticText(self, wx.ID_ANY, _("Word spacing (mm)")) + word_spacing_label.SetToolTip(_("Additional word spacing in mm.")) + self.word_spacing = wx.SpinCtrlDouble(self, min=-500, max=500, inc=0.01, initial=0, style=wx.SP_WRAP) + line_height_label = wx.StaticText(self, wx.ID_ANY, _("Line height (mm)")) + line_height_label.SetToolTip(_("Additional line height in mm.")) + self.line_height = wx.SpinCtrlDouble(self, min=-500, max=500, inc=0.01, initial=0, style=wx.SP_WRAP) + + # alignment events + self.scale_spinner.Bind(wx.EVT_SPINCTRL, lambda event: self.panel.on_change("scale", event)) self.align_text_choice.Bind(wx.EVT_CHOICE, lambda event: self.panel.on_choice_change("text_align", event)) + self.letter_spacing.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.panel.on_change("letter_spacing", event)) + self.word_spacing.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.panel.on_change("word_spacing", event)) + self.line_height.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.panel.on_change("line_height", event)) + + # alignment sizers + alignment_sizer = wx.StaticBoxSizer(self.spacing_box, wx.VERTICAL) + top_align_sizer = wx.BoxSizer(wx.HORIZONTAL) + alignment_sizer.Add(top_align_sizer, 0, wx.TOP | wx.LEFT | wx.RIGHT, 5) + spacing_sizer = wx.BoxSizer(wx.HORIZONTAL) + alignment_sizer.Add(spacing_sizer, 0, wx.ALL, 5) + outer_sizer.Add(alignment_sizer, 0, wx.ALL | wx.EXPAND, 10) + + font_scale_sizer = wx.BoxSizer(wx.VERTICAL) + font_scale_sizer.Add(scale_spinner_label, 0, wx.LEFT, 0) + font_scale_sizer.Add(self.scale_spinner, 0, wx.TOP, 6) + top_align_sizer.Add(font_scale_sizer, 1, wx.ALL, 5) + + text_align_sizer = wx.BoxSizer(wx.VERTICAL) + text_align_sizer.Add(align_text_label, 0, wx.ALL, 0) + text_align_spinner_sizer = wx.BoxSizer(wx.HORIZONTAL) + text_align_spinner_sizer.Add(self.align_text_choice, 0, wx.ALL, 0) + text_align_sizer.Add(text_align_spinner_sizer, 0, wx.TOP, 5) + top_align_sizer.Add(text_align_sizer, 0, wx.ALL, 5) + + letter_spacing_sizer = wx.BoxSizer(wx.VERTICAL) + letter_spacing_sizer.Add(letter_spacing_label, 0, wx.BOTTOM, 5) + letter_spacing_sizer.Add(self.letter_spacing, 0, wx.ALL, 0) + spacing_sizer.Add(letter_spacing_sizer, 0, wx.ALL, 5) + + word_spacing_sizer = wx.BoxSizer(wx.VERTICAL) + word_spacing_sizer.Add(word_spacing_label, 0, wx.BOTTOM, 5) + word_spacing_sizer.Add(self.word_spacing, 0, wx.ALL, 0) + spacing_sizer.Add(word_spacing_sizer, 0, wx.ALL, 5) + + line_height_sizer = wx.BoxSizer(wx.VERTICAL) + line_height_sizer.Add(line_height_label, 0, wx.BOTTOM, 5) + line_height_sizer.Add(self.line_height, 0, wx.ALL, 0) + spacing_sizer.Add(line_height_sizer, 0, wx.ALL, 5) + + # options + self.back_and_forth_checkbox = wx.CheckBox(self, label=_("Stitch lines of text back and forth")) + self.back_and_forth_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.panel.on_change("back_and_forth", event)) color_sort_label = wx.StaticText(self, wx.ID_ANY, _("Color sort")) color_sort_label.SetToolTip(_("Sort multicolor fonts. Unifies tartan patterns.")) @@ -107,26 +159,14 @@ class LetteringOptionsPanel(ScrolledPanel): self.use_trim_symbols.SetToolTip(_('Uses command symbols if enabled. When disabled inserts trim commands as params.')) left_option_sizer = wx.BoxSizer(wx.VERTICAL) - - font_scale_sizer = wx.BoxSizer(wx.HORIZONTAL) - font_scale_sizer.Add(wx.StaticText(self, wx.ID_ANY, _("Scale")), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 0) - font_scale_sizer.Add(self.scale_spinner, 0, wx.LEFT, 10) - font_scale_sizer.Add(wx.StaticText(self, wx.ID_ANY, "%"), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 3) - left_option_sizer.Add(font_scale_sizer, 0, wx.ALL, 5) - - left_option_sizer.Add(self.back_and_forth_checkbox, 1, wx.LEFT | wx.TOP | wx.RIGHT, 5) - - align_sizer = wx.BoxSizer(wx.HORIZONTAL) - align_sizer.Add(align_text_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) - align_sizer.Add(self.align_text_choice, 0, wx.ALL, 5) - left_option_sizer.Add(align_sizer, 0, wx.ALL, 5) - - right_option_sizer = wx.BoxSizer(wx.VERTICAL) + left_option_sizer.Add(self.back_and_forth_checkbox, 1, wx.TOP | wx.RIGHT, 5) color_sort_sizer = wx.BoxSizer(wx.HORIZONTAL) color_sort_sizer.Add(color_sort_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) color_sort_sizer.Add(self.color_sort_choice, 1, wx.ALL, 5) - right_option_sizer.Add(color_sort_sizer, 0, wx.ALIGN_LEFT, 5) + left_option_sizer.Add(color_sort_sizer, 0, wx.ALIGN_LEFT, 0) + + right_option_sizer = wx.BoxSizer(wx.VERTICAL) trim_sizer = wx.BoxSizer(wx.HORIZONTAL) trim_sizer.Add(trim_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) @@ -135,7 +175,7 @@ class LetteringOptionsPanel(ScrolledPanel): right_option_sizer.Add(self.use_trim_symbols, 0, wx.LEFT | wx.BOTTOM | wx.RIGHT, 5) - self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options")) + self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Embroidery settings")) options_sizer = wx.StaticBoxSizer(self.options_box, wx.HORIZONTAL) options_sizer.Add(left_option_sizer, 1, wx.ALL, 10) options_sizer.Add(right_option_sizer, 0, wx.ALL, 10) diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 1d9f8b40..2fc66c40 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -5,10 +5,10 @@ import json import os +import unicodedata from collections import defaultdict from copy import deepcopy from random import randint -import unicodedata import inkex @@ -19,6 +19,7 @@ from ..extensions.lettering_custom_font_dir import get_custom_font_dir from ..i18n import _, get_languages from ..marker import ensure_marker_symbols, has_marker, is_grouped_with_marker from ..stitches.auto_satin import auto_satin +from ..svg import PIXELS_PER_MM from ..svg.clip import get_clips from ..svg.tags import (CONNECTION_END, CONNECTION_START, EMBROIDERABLE_TAGS, INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_GROUP_TAG, @@ -213,7 +214,8 @@ class Font(object): return custom_dir in self.path def render_text(self, text, destination_group, variant=None, back_and_forth=True, # noqa: C901 - trim_option=0, use_trim_symbols=False, color_sort=0, text_align=0): + trim_option=0, use_trim_symbols=False, color_sort=0, text_align=0, + letter_spacing=0, word_spacing=0, line_height=0): """Render text into an SVG group element.""" self._load_variants() @@ -238,7 +240,7 @@ class Font(object): if self.text_direction == "rtl": line = line[::-1] - letter_group = self._render_line(destination_group, line, position, glyph_set, i) + letter_group = self._render_line(destination_group, line, position, glyph_set, i, letter_spacing, word_spacing) if ((variant == '→' and back_and_forth and self.reversible and i % 2 == 1) or (variant == '←' and not (back_and_forth and self.reversible and i % 2 == 1))): letter_group[:] = reversed(letter_group) @@ -246,7 +248,7 @@ class Font(object): group[:] = reversed(group) position.x = 0 - position.y += self.leading + position.y += self.leading + line_height * PIXELS_PER_MM # We need to insert the destination_group now, even though it is possibly empty # otherwise we could run into a FragmentError in case a glyph contains commands @@ -264,13 +266,16 @@ class Font(object): line_width = bounding_box.width max_line_width = max(max_line_width, line_width) + # text_align 0: left (default) if text_align == 1: - # align center + # 1: align center letter_group.transform = f'translate({-line_width/2}, 0)' if text_align == 2: + # 2: align right letter_group.transform = f'translate({-line_width}, 0)' if text_align in [3, 4]: + # 3: Block (default) 4: Block (letterspacing) for line_group in destination_group.iterchildren(): if text_align == 4 and len(line_group) == 1: line_group = line_group[0] @@ -318,7 +323,7 @@ class Font(object): def get_variant(self, variant): return self.variants.get(variant, self.variants[self.default_variant]) - def _render_line(self, destination_group, line, position, glyph_set, line_number): + def _render_line(self, destination_group, line, position, glyph_set, line_number, letter_spacing=0, word_spacing=0): """Render a line of text. An SVG XML node tree will be returned, with an svg:g at its root. If @@ -363,11 +368,11 @@ class Font(object): position.x += self.word_spacing last_character = None continue - node = self._render_glyph(destination_group, glyph, position, glyph.name, last_character, f'{line_number}-{i}-{j}') + node = self._render_glyph(destination_group, glyph, position, glyph.name, last_character, f'{line_number}-{i}-{j}', letter_spacing) word_group.append(node) last_character = glyph.name group.append(word_group) - position.x += self.word_spacing + position.x += self.word_spacing + word_spacing * PIXELS_PER_MM return group def _get_word_glyphs(self, glyph_set, word): @@ -400,7 +405,7 @@ class Font(object): return glyphs - def _render_glyph(self, destination_group, glyph, position, character, last_character, id_extension): + def _render_glyph(self, destination_group, glyph, position, character, last_character, id_extension, letter_spacing=0): """Render a single glyph. An SVG XML node tree will be returned, with an svg:g at its root. @@ -428,13 +433,13 @@ class Font(object): if kerning is None: # legacy kerning without space kerning = self.kerning_pairs.get(last_character + character, 0) - position.x += glyph.min_x - kerning + position.x += glyph.min_x - kerning + letter_spacing * PIXELS_PER_MM else: 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 + position.x += glyph.min_x - kerning + letter_spacing * PIXELS_PER_MM transform = "translate(%s, %s)" % position.as_tuple() node.set('transform', transform) diff --git a/lib/lettering/utils.py b/lib/lettering/utils.py index 4c9822ea..6be49ffe 100644 --- a/lib/lettering/utils.py +++ b/lib/lettering/utils.py @@ -52,7 +52,7 @@ def get_font_by_id(font_id, show_font_path_warning=True): return None -def get_font_by_name(font_name): +def get_font_by_name(font_name, show_font_path_warning=True): font_paths = get_font_paths() for font_path in font_paths: try: @@ -60,7 +60,7 @@ def get_font_by_name(font_name): except OSError: continue for font_dir in font_dirs: - font = _get_font_from_path(font_path, font_dir) + font = _get_font_from_path(font_path, font_dir, show_font_path_warning) if font and font_name in [font.name, font.marked_custom_font_name]: return font return None |
