summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2024-10-21 17:01:58 +0200
committerGitHub <noreply@github.com>2024-10-21 17:01:58 +0200
commitc6fecfb0bc91d94f56da43e242b6e59b41058094 (patch)
tree7d33e5a67d97461089ffb6f0dbfeb1fefa95cbd0 /lib
parentdbfdb3e8d4fe3787927e8ab536473f9db6264e2b (diff)
Add color sort option for multicolor fonts (#3242)
Diffstat (limited to 'lib')
-rw-r--r--lib/extensions/__init__.py2
-rw-r--r--lib/extensions/lettering_generate_json.py12
-rw-r--r--lib/extensions/lettering_set_color_sort_index.py28
-rw-r--r--lib/gui/lettering/main_panel.py22
-rw-r--r--lib/gui/lettering/option_panel.py28
-rw-r--r--lib/gui/lettering_font_sample.py18
-rw-r--r--lib/lettering/font.py92
-rw-r--r--lib/marker.py7
8 files changed, 178 insertions, 31 deletions
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py
index 8bf0b021..d5cfc8f8 100644
--- a/lib/extensions/__init__.py
+++ b/lib/extensions/__init__.py
@@ -37,6 +37,7 @@ from .lettering_font_sample import LetteringFontSample
from .lettering_force_lock_stitches import LetteringForceLockStitches
from .lettering_generate_json import LetteringGenerateJson
from .lettering_remove_kerning import LetteringRemoveKerning
+from .lettering_set_color_sort_index import LetteringSetColorSortIndex
from .lettering_update_json_glyphlist import LetteringUpdateJsonGlyphlist
from .letters_to_font import LettersToFont
from .object_commands import ObjectCommands
@@ -105,6 +106,7 @@ __all__ = extensions = [About,
LetteringForceLockStitches,
LetteringGenerateJson,
LetteringRemoveKerning,
+ LetteringSetColorSortIndex,
LetteringUpdateJsonGlyphlist,
LettersToFont,
ObjectCommands,
diff --git a/lib/extensions/lettering_generate_json.py b/lib/extensions/lettering_generate_json.py
index 5ff64c41..e6f997c7 100644
--- a/lib/extensions/lettering_generate_json.py
+++ b/lib/extensions/lettering_generate_json.py
@@ -5,9 +5,8 @@
import json
import os
-import sys
-from inkex import Boolean
+from inkex import Boolean, errormsg
from ..i18n import _
from ..lettering.categories import FONT_CATEGORIES
@@ -27,6 +26,8 @@ class LetteringGenerateJson(InkstitchExtension):
self.arg_parser.add_argument("-d", "--font-description", type=str, default="Description", dest="font_description")
self.arg_parser.add_argument("-s", "--auto-satin", type=Boolean, default="true", dest="auto_satin")
self.arg_parser.add_argument("-r", "--reversible", type=Boolean, default="true", dest="reversible")
+ self.arg_parser.add_argument("-o", "--combine-at-sort-indices", type=str, default="", dest="combine_at_sort_indices")
+ self.arg_parser.add_argument("-t", "--sortable", type=Boolean, default="false", dest="sortable")
self.arg_parser.add_argument("-u", "--letter-case", type=str, default="", dest="letter_case")
self.arg_parser.add_argument("-g", "--default-glyph", type=str, default="", dest="default_glyph")
self.arg_parser.add_argument("-z", "--size", type=float, default=15, dest="size")
@@ -45,7 +46,7 @@ class LetteringGenerateJson(InkstitchExtension):
# file paths
path = self.options.path
if not os.path.isfile(path):
- print(_("Please specify a font file."), file=sys.stderr)
+ errormsg(_("Please specify a font file."))
return
output_path = os.path.join(os.path.dirname(path), 'font.json')
@@ -77,6 +78,9 @@ class LetteringGenerateJson(InkstitchExtension):
if getattr(self.options, category.id):
keywords.append(category.id)
+ combine_at_sort_indices = self.options.combine_at_sort_indices.split(',')
+ combine_at_sort_indices = set([index.strip() for index in combine_at_sort_indices if index.strip()])
+
# collect data
data = {'name': self.options.font_name,
'description': self.options.font_description,
@@ -84,6 +88,8 @@ class LetteringGenerateJson(InkstitchExtension):
'leading': leading,
'auto_satin': self.options.auto_satin,
'reversible': self.options.reversible,
+ 'sortable': self.options.sortable,
+ 'combine_at_sort_indices': list(combine_at_sort_indices),
'letter_case': self.options.letter_case,
'default_glyph': self.options.default_glyph,
'size': self.options.size,
diff --git a/lib/extensions/lettering_set_color_sort_index.py b/lib/extensions/lettering_set_color_sort_index.py
new file mode 100644
index 00000000..3fb33094
--- /dev/null
+++ b/lib/extensions/lettering_set_color_sort_index.py
@@ -0,0 +1,28 @@
+# Authors: see git history
+#
+# Copyright (c) 2024 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+from .base import InkstitchExtension
+
+
+class LetteringSetColorSortIndex(InkstitchExtension):
+ '''
+ This extension sets a color sort index to selected elements.
+ It enables font authors to define the order of elements in multicolor fonts when color sorted.
+ '''
+ def __init__(self, *args, **kwargs):
+ InkstitchExtension.__init__(self, *args, **kwargs)
+ self.arg_parser.add_argument("--notebook")
+ self.arg_parser.add_argument("-i", "--color-sort-index", type=int, default=0, dest="color_sort_index")
+
+ def effect(self):
+ selection = self.svg.selection
+ self.set_index(selection)
+
+ def set_index(self, element_list):
+ for element in element_list:
+ if element.TAG == "path":
+ element.set('inkstitch:color_sort_index', self.options.color_sort_index)
+ elif element.TAG == "g":
+ self.set_index(element.getchildren())
diff --git a/lib/gui/lettering/main_panel.py b/lib/gui/lettering/main_panel.py
index 5393fcf3..06388dd0 100644
--- a/lib/gui/lettering/main_panel.py
+++ b/lib/gui/lettering/main_panel.py
@@ -81,7 +81,8 @@ class LetteringPanel(wx.Panel):
"font": None,
"scale": 100,
"trim_option": 0,
- "use_trim_symbols": False
+ "use_trim_symbols": False,
+ "color_sort": False
})
if INKSTITCH_LETTERING in self.group.attrib:
@@ -98,6 +99,7 @@ class LetteringPanel(wx.Panel):
def apply_settings(self):
"""Make the settings in self.settings visible in the UI."""
+ self.options_panel.color_sort_checkbox.SetValue(bool(self.settings.color_sort))
self.options_panel.back_and_forth_checkbox.SetValue(bool(self.settings.back_and_forth))
self.options_panel.trim_option_choice.SetSelection(self.settings.trim_option)
self.options_panel.use_trim_symbols.SetValue(bool(self.settings.use_trim_symbols))
@@ -230,6 +232,14 @@ class LetteringPanel(wx.Panel):
self.options_panel.back_and_forth_checkbox.Disable()
self.options_panel.back_and_forth_checkbox.SetValue(False)
+ if font.sortable:
+ # The creator of the font allowed color sorting: "sortable": false
+ self.options_panel.color_sort_checkbox.Enable()
+ self.options_panel.color_sort_checkbox.SetValue(bool(self.settings.color_sort))
+ else:
+ self.options_panel.color_sort_checkbox.Disable()
+ self.options_panel.color_sort_checkbox.SetValue(False)
+
self.options_panel.Layout()
self.update_preview()
@@ -279,15 +289,17 @@ class LetteringPanel(wx.Panel):
# L10N The user has chosen to scale the text by some percentage
# (50%, 200%, etc). If you need to use the percentage symbol,
# make sure to double it (%%).
- INKSCAPE_LABEL: _("Text scale %s%%") % self.settings.scale
+ INKSCAPE_LABEL: _("Text scale") + f' {self.settings.scale}%'
})
self.group.append(destination_group)
font = self.fonts.get(self.options_panel.font_chooser.GetValue(), self.default_font)
try:
- 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)
-
+ 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
+ )
except FontError as e:
if raise_error:
inkex.errormsg(_("Error: Text cannot be applied to the document.\n%s") % e)
diff --git a/lib/gui/lettering/option_panel.py b/lib/gui/lettering/option_panel.py
index e0c4388b..aea281ba 100644
--- a/lib/gui/lettering/option_panel.py
+++ b/lib/gui/lettering/option_panel.py
@@ -57,14 +57,16 @@ class LetteringOptionsPanel(wx.Panel):
outer_sizer.Add(filter_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10)
# options
- self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options"))
-
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))
+ self.color_sort_checkbox = wx.CheckBox(self, label=_("Color sort"))
+ self.color_sort_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.panel.on_change("color_sort", event))
+ self.color_sort_checkbox.SetToolTip(_("Sort multicolor fonts. Unifies tartan patterns."))
+
self.trim_option_choice = wx.Choice(self, choices=[_("Never"), _("after each line"), _("after each word"), _("after each letter")],
name=_("Add trim command"))
self.trim_option_choice.Bind(wx.EVT_CHOICE, lambda event: self.panel.on_trim_option_change(event))
@@ -74,22 +76,26 @@ class LetteringOptionsPanel(wx.Panel):
self.use_trim_symbols.SetToolTip(_('Uses command symbols if enabled. When disabled inserts trim commands as params.'))
left_option_sizer = wx.BoxSizer(wx.VERTICAL)
- left_option_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 5)
-
- trim_option_sizer = wx.BoxSizer(wx.HORIZONTAL)
- trim_option_sizer.Add(wx.StaticText(self, wx.ID_ANY, _("Add trims")), 0, wx.LEFT | wx.ALIGN_TOP, 5)
- trim_option_sizer.Add(self.trim_option_choice, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5)
- trim_option_sizer.Add(self.use_trim_symbols, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5)
- left_option_sizer.Add(trim_option_sizer, 0, wx.ALIGN_LEFT, 5)
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.ALIGN_LEFT, 5)
+ left_option_sizer.Add(self.back_and_forth_checkbox, 1, wx.LEFT | wx.TOP | wx.RIGHT, 5)
+ left_option_sizer.Add(self.color_sort_checkbox, 1, wx.LEFT | wx.TOP | wx.RIGHT, 5)
+
+ right_option_sizer = wx.BoxSizer(wx.VERTICAL)
+
+ right_option_sizer.Add(wx.StaticText(self, wx.ID_ANY, _("Add trims")), 0, wx.LEFT | wx.ALIGN_TOP, 5)
+ right_option_sizer.Add(self.trim_option_choice, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5)
+ right_option_sizer.Add(self.use_trim_symbols, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5)
+
+ self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options"))
options_sizer = wx.StaticBoxSizer(self.options_box, wx.HORIZONTAL)
- options_sizer.Add(left_option_sizer, 1, wx.EXPAND, 10)
- options_sizer.Add(font_scale_sizer, 0, wx.RIGHT, 10)
+ options_sizer.Add(left_option_sizer, 1, wx.LEFT | wx.RIGHT, 10)
+ options_sizer.Add(right_option_sizer, 0, wx.RIGHT, 10)
outer_sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10)
# text input
diff --git a/lib/gui/lettering_font_sample.py b/lib/gui/lettering_font_sample.py
index e5312c7e..984f6356 100644
--- a/lib/gui/lettering_font_sample.py
+++ b/lib/gui/lettering_font_sample.py
@@ -36,7 +36,7 @@ class FontSampleFrame(wx.Frame):
self.font_chooser = wx.adv.BitmapComboBox(self.settings, wx.ID_ANY, style=wx.CB_READONLY | wx.CB_SORT, size=((800, 20)))
self.font_chooser.Bind(wx.EVT_COMBOBOX, self.on_font_changed)
- grid_settings_sizer = wx.FlexGridSizer(6, 2, 5, 5)
+ grid_settings_sizer = wx.FlexGridSizer(7, 2, 5, 5)
grid_settings_sizer.AddGrowableCol(1)
direction_label = wx.StaticText(self.settings, label=_("Stitch direction"))
@@ -45,6 +45,7 @@ class FontSampleFrame(wx.Frame):
self.scale_spinner = wx.SpinCtrl(self.settings, wx.ID_ANY, min=0, max=1000, initial=100)
max_line_width_label = wx.StaticText(self.settings, label=_("Max. line width"))
self.max_line_width = wx.SpinCtrl(self.settings, wx.ID_ANY, min=0, max=5000, initial=180)
+ self.color_sort_checkbox = wx.CheckBox(self.settings, label=_("Color sort"))
grid_settings_sizer.Add(direction_label, 0, wx.ALIGN_LEFT, 0)
grid_settings_sizer.Add(self.direction, 0, wx.EXPAND, 0)
@@ -52,6 +53,8 @@ class FontSampleFrame(wx.Frame):
grid_settings_sizer.Add(self.scale_spinner, 0, wx.EXPAND, 0)
grid_settings_sizer.Add(max_line_width_label, 0, wx.ALIGN_LEFT, 0)
grid_settings_sizer.Add(self.max_line_width, 0, wx.EXPAND, 0)
+ grid_settings_sizer.Add(wx.StaticText(), 0, wx.ALIGN_LEFT, 0)
+ grid_settings_sizer.Add(self.color_sort_checkbox, 0, wx.EXPAND, 0)
apply_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.cancel_button = wx.Button(self.settings, label=_("Cancel"))
@@ -127,6 +130,10 @@ class FontSampleFrame(wx.Frame):
for variant in font.has_variants():
self.direction.Append(variant)
self.direction.SetSelection(0)
+ if font.sortable:
+ self.color_sort_checkbox.Enable()
+ else:
+ self.color_sort_checkbox.Disable()
def apply(self, event):
# apply scale to layer and extract for later use
@@ -142,6 +149,7 @@ class FontSampleFrame(wx.Frame):
# parameters
line_width = self.max_line_width.GetValue()
direction = self.direction.GetValue()
+ color_sort = self.sortable(font)
font._load_variants()
font_variant = font.variants[direction]
@@ -192,9 +200,15 @@ class FontSampleFrame(wx.Frame):
width += width_to_add
# render text and close
- font.render_text(text, self.layer, variant=direction, back_and_forth=False)
+ font.render_text(text, self.layer, variant=direction, back_and_forth=False, color_sort=color_sort)
self.GetTopLevelParent().Close()
+ def sortable(self, font):
+ color_sort = self.color_sort_checkbox.GetValue()
+ if color_sort and not font.sortable:
+ color_sort = False
+ return color_sort
+
def duplicate_warning(self, font):
# warn about duplicated glyphs
if len(set(font.available_glyphs)) != len(font.available_glyphs):
diff --git a/lib/lettering/font.py b/lib/lettering/font.py
index 431dc8d4..e1f0962b 100644
--- a/lib/lettering/font.py
+++ b/lib/lettering/font.py
@@ -15,13 +15,14 @@ from ..elements import SatinColumn, Stroke, nodes_to_elements
from ..exceptions import InkstitchException
from ..extensions.lettering_custom_font_dir import get_custom_font_dir
from ..i18n import _, get_languages
-from ..marker import MARKER, ensure_marker, has_marker
+from ..marker import MARKER, ensure_marker, has_marker, is_grouped_with_marker
from ..stitches.auto_satin import auto_satin
from ..svg.tags import (CONNECTION_END, CONNECTION_START, EMBROIDERABLE_TAGS,
INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_GROUP_TAG,
SVG_PATH_TAG, SVG_USE_TAG, XLINK_HREF)
from ..utils import Point
from .font_variant import FontVariant
+from collections import defaultdict
class FontError(InkstitchException):
@@ -147,6 +148,8 @@ class Font(object):
word_spacing = font_metadata('horiz_adv_x_space', 20)
reversible = font_metadata('reversible', True)
+ sortable = font_metadata('sortable', False)
+ combine_at_sort_indices = font_metadata('combine_at_sort_indices', [])
@property
def id(self):
@@ -201,7 +204,7 @@ class Font(object):
return False
return custom_dir in self.path
- def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim_option=0, use_trim_symbols=False):
+ def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim_option=0, use_trim_symbols=False, color_sort=False):
"""Render text into an SVG group element."""
self._load_variants()
@@ -230,6 +233,20 @@ class Font(object):
if self.auto_satin and len(destination_group) > 0:
self._apply_auto_satin(destination_group)
+ self._set_style(destination_group)
+
+ # add trims
+ self._add_trims(destination_group, text, trim_option, use_trim_symbols, back_and_forth)
+ # make sure necessary marker and command symbols are in the defs section
+ self._ensure_command_symbols(destination_group)
+ self._ensure_marker_symbols(destination_group)
+
+ if color_sort and self.sortable:
+ self.do_color_sort(destination_group)
+
+ return destination_group
+
+ def _set_style(self, destination_group):
# make sure font stroke styles have always a similar look
for element in destination_group.iterdescendants(SVG_PATH_TAG):
style = inkex.Style(element.get('style'))
@@ -242,14 +259,6 @@ class Font(object):
style += inkex.Style("stroke-width:0.5px")
element.set('style', '%s' % style.to_str())
- # add trims
- self._add_trims(destination_group, text, trim_option, use_trim_symbols, back_and_forth)
- # make sure necessary marker and command symbols are in the defs section
- self._ensure_command_symbols(destination_group)
- self._ensure_marker_symbols(destination_group)
-
- return destination_group
-
def get_variant(self, variant):
return self.variants.get(variant, self.variants[self.default_variant])
@@ -441,3 +450,66 @@ class Font(object):
if elements:
auto_satin(elements, preserve_order=True, trim=False)
+
+ def do_color_sort(self, group):
+ """Sort elements by their color sort index as defined by font author"""
+ elements_by_color = self._get_color_sorted_elements(group)
+
+ # there are no sort indexes defined, abort color sorting and return to normal
+ if not elements_by_color:
+ return
+
+ group.remove_all()
+ for index, grouped_elements in sorted(elements_by_color.items()):
+ color_group = inkex.Group(attrib={
+ INKSCAPE_LABEL: _("Color Group") + f' {index}'
+ })
+
+ # combined indices
+ if index in self.combine_at_sort_indices:
+ path = ""
+ for element_list in grouped_elements:
+ for element in element_list:
+ path += element.get("d", "")
+ grouped_elements[0][0].set("d", path)
+ color_group.append(grouped_elements[0][0])
+ group.append(color_group)
+ continue
+
+ # everything else, create marker groups if applicable
+ for element_list in grouped_elements:
+ if len(element_list) == 1:
+ color_group.append(element_list[0])
+ continue
+ elements_group = inkex.Group()
+ for element in element_list:
+ elements_group.append(element)
+ color_group.append(elements_group)
+
+ group.append(color_group)
+
+ def _get_color_sorted_elements(self, group):
+ elements_by_color = defaultdict(list)
+ last_parent = None
+ for element in group.iterdescendants(SVG_PATH_TAG):
+ sort_index = element.get('inkstitch:color_sort_index', None)
+
+ # get glyph group to calculate transform
+ for ancestor in element.ancestors(group):
+ if ancestor.get_id().startswith("glyph"):
+ glyph_group = ancestor
+ break
+ element.transform = element.composed_transform(glyph_group.getparent())
+ element.apply_transform()
+
+ if not sort_index:
+ elements_by_color[404].append([element])
+ continue
+
+ parent = element.getparent()
+ if last_parent != parent or int(sort_index) not in elements_by_color or not is_grouped_with_marker(element):
+ elements_by_color[int(sort_index)].append([element])
+ else:
+ elements_by_color[int(sort_index)][-1].append(element)
+ last_parent = element.getparent()
+ return elements_by_color
diff --git a/lib/marker.py b/lib/marker.py
index 91a5862f..6a5f71e6 100644
--- a/lib/marker.py
+++ b/lib/marker.py
@@ -93,3 +93,10 @@ def has_marker(node, marker=list()):
if "marker-start:url(#inkstitch-%s-marker" % m in style:
return True
return False
+
+
+def is_grouped_with_marker(node):
+ for element in node.getparent().iterchildren():
+ if has_marker(element):
+ return True
+ return False