summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2025-10-27 18:09:21 +0100
committerGitHub <noreply@github.com>2025-10-27 18:09:21 +0100
commit6f282829537b69b8628b901cc717f18222ff33a0 (patch)
treed703e389a395f021836816ef1e0e5fe509474fc8
parente8859c4e14984bcdb3dd6d3c9bfed05cd233251c (diff)
Lettering: add spacing options (#4020)
-rw-r--r--lib/extensions/batch_lettering.py17
-rw-r--r--lib/extensions/lettering_along_path.py25
-rw-r--r--lib/gui/lettering/main_panel.py11
-rw-r--r--lib/gui/lettering/option_panel.py90
-rw-r--r--lib/lettering/font.py27
-rw-r--r--lib/lettering/utils.py4
-rw-r--r--templates/batch_lettering.xml10
7 files changed, 133 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
diff --git a/templates/batch_lettering.xml b/templates/batch_lettering.xml
index 39d0fe57..bfd93eb0 100644
--- a/templates/batch_lettering.xml
+++ b/templates/batch_lettering.xml
@@ -39,6 +39,16 @@
<option value="block">Block (default)</option>
<option value="letterspacing">Block (letterspacing)</option>
</param>
+
+ <hbox>
+ <param name="letter_spacing" type="float" precision="2" min="-500" max="500"
+ gui-text="Letter spacing">0</param>
+ <param name="word_spacing" type="float" precision="2" min="-500" max="500"
+ gui-text="Word spacing">0</param>
+ <param name="line_height" type="float" precision="2" min="-500" max="500"
+ gui-text="Line height">0</param>
+ </hbox>
+
</vbox>
<spacer />
<separator />