| 
									
										
										
										
											2021-03-12 04:17:19 +00:00
										 |  |  |  | # Authors: see git history | 
					
						
							|  |  |  |  | # | 
					
						
							|  |  |  |  | # Copyright (c) 2010 Authors | 
					
						
							|  |  |  |  | # Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details. | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | import json | 
					
						
							|  |  |  |  | import os | 
					
						
							| 
									
										
										
										
											2025-02-03 21:37:36 +00:00
										 |  |  |  | from collections import defaultdict | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  | from copy import deepcopy | 
					
						
							| 
									
										
										
										
											2021-10-09 16:25:29 +00:00
										 |  |  |  | from random import randint | 
					
						
							| 
									
										
										
										
											2025-06-25 08:30:24 +00:00
										 |  |  |  | import unicodedata | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-25 05:24:34 +00:00
										 |  |  |  | import inkex | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-26 06:52:31 +00:00
										 |  |  |  | from ..commands import add_commands, ensure_command_symbols | 
					
						
							| 
									
										
										
										
											2023-04-30 09:26:56 +00:00
										 |  |  |  | from ..elements import SatinColumn, Stroke, nodes_to_elements | 
					
						
							| 
									
										
										
										
											2019-03-25 23:40:37 +00:00
										 |  |  |  | from ..exceptions import InkstitchException | 
					
						
							| 
									
										
										
										
											2021-07-26 16:54:38 +00:00
										 |  |  |  | from ..extensions.lettering_custom_font_dir import get_custom_font_dir | 
					
						
							| 
									
										
										
										
											2019-04-03 03:07:38 +00:00
										 |  |  |  | from ..i18n import _, get_languages | 
					
						
							| 
									
										
										
										
											2025-04-27 19:20:55 +00:00
										 |  |  |  | from ..marker import ensure_marker_symbols, has_marker, is_grouped_with_marker | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | from ..stitches.auto_satin import auto_satin | 
					
						
							| 
									
										
										
										
											2025-02-03 21:37:36 +00:00
										 |  |  |  | from ..svg.clip import get_clips | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  | from ..svg.tags import (CONNECTION_END, CONNECTION_START, EMBROIDERABLE_TAGS, | 
					
						
							| 
									
										
										
										
											2023-04-30 09:26:56 +00:00
										 |  |  |  |                         INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_GROUP_TAG, | 
					
						
							| 
									
										
										
										
											2025-04-27 19:20:55 +00:00
										 |  |  |  |                         SVG_PATH_TAG) | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | from ..utils import Point | 
					
						
							| 
									
										
										
										
											2021-07-26 16:54:38 +00:00
										 |  |  |  | from .font_variant import FontVariant | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-25 23:40:37 +00:00
										 |  |  |  | class FontError(InkstitchException): | 
					
						
							|  |  |  |  |     pass | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | def font_metadata(name, default=None, multiplier=None): | 
					
						
							|  |  |  |  |     def getter(self): | 
					
						
							|  |  |  |  |         value = self.metadata.get(name, default) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         if multiplier is not None: | 
					
						
							|  |  |  |  |             value *= multiplier | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         return value | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     return property(getter) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-03 03:07:38 +00:00
										 |  |  |  | def localized_font_metadata(name, default=None): | 
					
						
							|  |  |  |  |     def getter(self): | 
					
						
							|  |  |  |  |         # If the font contains a localized version of the attribute, use it. | 
					
						
							|  |  |  |  |         for language in get_languages(): | 
					
						
							|  |  |  |  |             attr = "%s_%s" % (name, language) | 
					
						
							|  |  |  |  |             if attr in self.metadata: | 
					
						
							|  |  |  |  |                 return self.metadata.get(attr) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         if name in self.metadata: | 
					
						
							|  |  |  |  |             # This may be a font packaged with Ink/Stitch, in which case the | 
					
						
							|  |  |  |  |             # text will have been sent to CrowdIn for community translation. | 
					
						
							|  |  |  |  |             # Try to fetch the translated version. | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  |             original_metadata = self.metadata.get(name) | 
					
						
							|  |  |  |  |             localized_metadata = "" | 
					
						
							|  |  |  |  |             if original_metadata != "": | 
					
						
							|  |  |  |  |                 localized_metadata = _(original_metadata) | 
					
						
							|  |  |  |  |             return localized_metadata | 
					
						
							| 
									
										
										
										
											2019-04-03 03:07:38 +00:00
										 |  |  |  |         else: | 
					
						
							|  |  |  |  |             return default | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     return property(getter) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | class Font(object): | 
					
						
							|  |  |  |  |     """Represents a font with multiple variants.
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     Each font may have multiple FontVariants for left-to-right, right-to-left, | 
					
						
							|  |  |  |  |     etc.  Each variant has a set of Glyphs, one per character. | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     Properties: | 
					
						
							|  |  |  |  |       path     -- the path to the directory containing this font | 
					
						
							|  |  |  |  |       metadata -- A dict of information about the font. | 
					
						
							|  |  |  |  |       name     -- Shortcut property for metadata["name"] | 
					
						
							|  |  |  |  |       license  -- contents of the font's LICENSE file, or None if no LICENSE file exists. | 
					
						
							|  |  |  |  |       variants -- A dict of FontVariants, with keys in FontVariant.VARIANT_TYPES. | 
					
						
							|  |  |  |  |     """
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-01 05:03:43 +00:00
										 |  |  |  |     def __init__(self, font_path, show_font_path_warning=True): | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         self.path = font_path | 
					
						
							| 
									
										
										
										
											2019-05-01 00:15:58 +00:00
										 |  |  |  |         self.metadata = {} | 
					
						
							|  |  |  |  |         self.license = None | 
					
						
							|  |  |  |  |         self.variants = {} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-01 05:03:43 +00:00
										 |  |  |  |         self._load_metadata(show_font_path_warning) | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         self._load_license() | 
					
						
							| 
									
										
										
										
											2019-03-25 23:40:37 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-01 05:03:43 +00:00
										 |  |  |  |     def _load_metadata(self, show_font_path_warning=True): | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2022-04-24 06:27:42 +00:00
										 |  |  |  |             with open(os.path.join(self.path, "font.json"), encoding="utf-8-sig") as metadata_file: | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |                 self.metadata = json.load(metadata_file) | 
					
						
							|  |  |  |  |         except IOError: | 
					
						
							| 
									
										
										
										
											2025-06-01 05:03:43 +00:00
										 |  |  |  |             if not show_font_path_warning: | 
					
						
							|  |  |  |  |                 return | 
					
						
							| 
									
										
										
										
											2024-07-10 14:06:24 +00:00
										 |  |  |  |             path = os.path.join(self.path, "font.json") | 
					
						
							|  |  |  |  |             msg = _("JSON file missing. Expected a JSON file at the following location:") | 
					
						
							|  |  |  |  |             msg += f"\n{path}\n\n" | 
					
						
							|  |  |  |  |             msg += _("Generate the JSON file through:\nExtensions > Ink/Stitch > Font Management > Generate JSON...") | 
					
						
							|  |  |  |  |             msg += '\n\n' | 
					
						
							|  |  |  |  |             inkex.errormsg(msg) | 
					
						
							|  |  |  |  |         except json.decoder.JSONDecodeError as exception: | 
					
						
							| 
									
										
										
										
											2025-06-01 05:03:43 +00:00
										 |  |  |  |             if not show_font_path_warning: | 
					
						
							|  |  |  |  |                 return | 
					
						
							| 
									
										
										
										
											2024-07-10 14:06:24 +00:00
										 |  |  |  |             path = os.path.join(self.path, "font.json") | 
					
						
							|  |  |  |  |             msg = _("Corrupt JSON file") | 
					
						
							|  |  |  |  |             msg += f" ({exception}):\n{path}\n\n" | 
					
						
							|  |  |  |  |             msg += _("Regenerate the JSON file through:\nExtensions > Ink/Stitch > Font Management > Generate JSON...") | 
					
						
							|  |  |  |  |             msg += '\n\n' | 
					
						
							|  |  |  |  |             inkex.errormsg(msg) | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     def _load_license(self): | 
					
						
							|  |  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2022-04-24 06:27:42 +00:00
										 |  |  |  |             with open(os.path.join(self.path, "LICENSE"), encoding="utf-8-sig") as license_file: | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |                 self.license = license_file.read() | 
					
						
							|  |  |  |  |         except IOError: | 
					
						
							| 
									
										
										
										
											2019-05-01 00:15:58 +00:00
										 |  |  |  |             pass | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     def _load_variants(self): | 
					
						
							| 
									
										
										
										
											2019-05-01 00:15:58 +00:00
										 |  |  |  |         if not self.variants: | 
					
						
							|  |  |  |  |             for variant in FontVariant.VARIANT_TYPES: | 
					
						
							|  |  |  |  |                 try: | 
					
						
							|  |  |  |  |                     self.variants[variant] = FontVariant(self.path, variant, self.default_glyph) | 
					
						
							|  |  |  |  |                 except IOError: | 
					
						
							|  |  |  |  |                     # we'll deal with missing variants when we apply lettering | 
					
						
							|  |  |  |  |                     pass | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-12 18:27:31 +00:00
										 |  |  |  |     name = font_metadata('name', '') | 
					
						
							| 
									
										
										
										
											2019-04-03 03:07:38 +00:00
										 |  |  |  |     description = localized_font_metadata('description', '') | 
					
						
							| 
									
										
										
										
											2023-07-12 16:28:07 +00:00
										 |  |  |  |     keywords = font_metadata('keywords', '') | 
					
						
							| 
									
										
										
										
											2025-01-12 19:46:37 +00:00
										 |  |  |  |     json_default_variant = font_metadata('default_variant', FontVariant.LEFT_TO_RIGHT) | 
					
						
							|  |  |  |  |     text_direction = font_metadata('text_direction', 'ltr') | 
					
						
							| 
									
										
										
										
											2021-04-02 17:05:34 +00:00
										 |  |  |  |     letter_case = font_metadata('letter_case', '') | 
					
						
							| 
									
										
										
										
											2021-04-05 17:20:48 +00:00
										 |  |  |  |     default_glyph = font_metadata('default_glyph', "<EFBFBD>") | 
					
						
							| 
									
										
										
										
											2021-03-22 16:06:48 +00:00
										 |  |  |  |     leading = font_metadata('leading', 100) | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |     kerning_pairs = font_metadata('kerning_pairs', {}) | 
					
						
							|  |  |  |  |     auto_satin = font_metadata('auto_satin', True) | 
					
						
							| 
									
										
										
										
											2019-04-11 00:23:11 +00:00
										 |  |  |  |     min_scale = font_metadata('min_scale', 1.0) | 
					
						
							|  |  |  |  |     max_scale = font_metadata('max_scale', 1.0) | 
					
						
							| 
									
										
										
										
											2022-10-23 07:17:17 +00:00
										 |  |  |  |     size = font_metadata('size', 0) | 
					
						
							| 
									
										
										
										
											2023-07-12 16:28:07 +00:00
										 |  |  |  |     available_glyphs = font_metadata('glyphs', []) | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-29 18:52:44 +00:00
										 |  |  |  |     # use values from SVG Font, example: | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  |     # <font horiz-adv-x="45" ...  <glyph .... horiz-adv-x="49" glyph-name="A" /> ... <hkern ... k="3"g1="A" g2="B" /> .... /> | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     # Example font.json : "horiz_adv_x": {"A":49}, | 
					
						
							|  |  |  |  |     horiz_adv_x = font_metadata('horiz_adv_x', {}) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     # Example font.json : "horiz_adv_x_default" : 45, | 
					
						
							|  |  |  |  |     horiz_adv_x_default = font_metadata('horiz_adv_x_default') | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     # Define by <glyph glyph-name="space" unicode=" " horiz-adv-x="22" />, Example font.json : "horiz_adv_x_space":22, | 
					
						
							| 
									
										
										
										
											2021-03-22 16:06:48 +00:00
										 |  |  |  |     word_spacing = font_metadata('horiz_adv_x_space', 20) | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     reversible = font_metadata('reversible', True) | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |     sortable = font_metadata('sortable', False) | 
					
						
							|  |  |  |  |     combine_at_sort_indices = font_metadata('combine_at_sort_indices', []) | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-25 23:40:37 +00:00
										 |  |  |  |     @property | 
					
						
							|  |  |  |  |     def id(self): | 
					
						
							|  |  |  |  |         return os.path.basename(self.path) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  |     @property | 
					
						
							|  |  |  |  |     def default_variant(self): | 
					
						
							|  |  |  |  |         # Set default variant to any existing variant if default font file is missing | 
					
						
							| 
									
										
										
										
											2025-01-12 19:46:37 +00:00
										 |  |  |  |         default_variant = self.json_default_variant | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  |         font_variants = self.has_variants() | 
					
						
							|  |  |  |  |         if default_variant not in font_variants and len(font_variants) > 0: | 
					
						
							|  |  |  |  |             default_variant = font_variants[0] | 
					
						
							|  |  |  |  |         return default_variant | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     @property | 
					
						
							|  |  |  |  |     def preview_image(self): | 
					
						
							|  |  |  |  |         preview_image_path = os.path.join(self.path, "preview.png") | 
					
						
							|  |  |  |  |         if os.path.isfile(preview_image_path): | 
					
						
							|  |  |  |  |             return preview_image_path | 
					
						
							|  |  |  |  |         return None | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     def has_variants(self): | 
					
						
							| 
									
										
										
										
											2021-03-04 17:40:53 +00:00
										 |  |  |  |         # returns available variants | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  |         font_variants = [] | 
					
						
							|  |  |  |  |         for variant in FontVariant.VARIANT_TYPES: | 
					
						
							|  |  |  |  |             if os.path.isfile(os.path.join(self.path, "%s.svg" % variant)): | 
					
						
							|  |  |  |  |                 font_variants.append(variant) | 
					
						
							| 
									
										
										
										
											2023-02-19 09:38:25 +00:00
										 |  |  |  |             elif (os.path.isdir(os.path.join(self.path, "%s" % variant)) and | 
					
						
							|  |  |  |  |                     [svg for svg in os.listdir(os.path.join(self.path, "%s" % variant)) if svg.endswith('.svg')]): | 
					
						
							|  |  |  |  |                 font_variants.append(variant) | 
					
						
							| 
									
										
										
										
											2021-03-04 17:40:53 +00:00
										 |  |  |  |         if not font_variants: | 
					
						
							|  |  |  |  |             raise FontError(_("The font '%s' has no variants.") % self.name) | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  |         return font_variants | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-26 16:54:38 +00:00
										 |  |  |  |     @property | 
					
						
							|  |  |  |  |     def marked_custom_font_id(self): | 
					
						
							|  |  |  |  |         if not self.is_custom_font(): | 
					
						
							|  |  |  |  |             return self.id | 
					
						
							|  |  |  |  |         else: | 
					
						
							|  |  |  |  |             return self.id + '*' | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     @property | 
					
						
							|  |  |  |  |     def marked_custom_font_name(self): | 
					
						
							|  |  |  |  |         if not self.is_custom_font(): | 
					
						
							|  |  |  |  |             return self.name | 
					
						
							|  |  |  |  |         else: | 
					
						
							|  |  |  |  |             return self.name + '*' | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     def is_custom_font(self): | 
					
						
							| 
									
										
										
										
											2023-05-10 14:35:37 +00:00
										 |  |  |  |         custom_dir = get_custom_font_dir() | 
					
						
							|  |  |  |  |         if not custom_dir: | 
					
						
							|  |  |  |  |             return False | 
					
						
							| 
									
										
										
										
											2023-05-10 15:12:20 +00:00
										 |  |  |  |         return custom_dir in self.path | 
					
						
							| 
									
										
										
										
											2021-07-26 16:54:38 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-29 14:19:33 +00:00
										 |  |  |  |     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): | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-07 01:32:51 +00:00
										 |  |  |  |         """Render text into an SVG group element.""" | 
					
						
							| 
									
										
										
										
											2019-05-01 00:15:58 +00:00
										 |  |  |  |         self._load_variants() | 
					
						
							| 
									
										
										
										
											2019-03-07 01:32:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-25 08:30:24 +00:00
										 |  |  |  |         # Normalize the text in the same way that glyph names are normalized (NFC) | 
					
						
							|  |  |  |  |         text = unicodedata.normalize('NFC', text) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         if variant is None: | 
					
						
							|  |  |  |  |             variant = self.default_variant | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  |         if back_and_forth and self.reversible: | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |             glyph_sets = [self.get_variant(variant), self.get_variant(FontVariant.reversed_variant(variant))] | 
					
						
							|  |  |  |  |         else: | 
					
						
							|  |  |  |  |             glyph_sets = [self.get_variant(variant)] * 2 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-29 14:19:33 +00:00
										 |  |  |  |         max_line_width = 0 | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         position = Point(0, 0) | 
					
						
							|  |  |  |  |         for i, line in enumerate(text.splitlines()): | 
					
						
							|  |  |  |  |             glyph_set = glyph_sets[i % 2] | 
					
						
							|  |  |  |  |             line = line.strip() | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 19:46:37 +00:00
										 |  |  |  |             if self.text_direction == "rtl": | 
					
						
							|  |  |  |  |                 line = line[::-1] | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-18 17:47:12 +00:00
										 |  |  |  |             letter_group = self._render_line(destination_group, line, position, glyph_set, i) | 
					
						
							| 
									
										
										
										
											2025-01-12 19:46:37 +00:00
										 |  |  |  |             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))): | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |                 letter_group[:] = reversed(letter_group) | 
					
						
							| 
									
										
										
										
											2025-01-08 16:16:02 +00:00
										 |  |  |  |                 for group in letter_group: | 
					
						
							|  |  |  |  |                     group[:] = reversed(group) | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |             position.x = 0 | 
					
						
							|  |  |  |  |             position.y += self.leading | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-29 18:54:15 +00:00
										 |  |  |  |             # 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 | 
					
						
							|  |  |  |  |             destination_group.append(letter_group) | 
					
						
							|  |  |  |  |             bounding_box = None | 
					
						
							|  |  |  |  |             try: | 
					
						
							|  |  |  |  |                 bounding_box = letter_group.bounding_box() | 
					
						
							|  |  |  |  |             except AttributeError: | 
					
						
							|  |  |  |  |                 # letter group is None | 
					
						
							|  |  |  |  |                 continue | 
					
						
							|  |  |  |  |             # remove destination_group if it is empty | 
					
						
							| 
									
										
										
										
											2024-12-29 14:19:33 +00:00
										 |  |  |  |             if not bounding_box: | 
					
						
							| 
									
										
										
										
											2025-03-22 16:43:50 +00:00
										 |  |  |  |                 letter_group.delete() | 
					
						
							| 
									
										
										
										
											2024-12-29 14:19:33 +00:00
										 |  |  |  |                 continue | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             line_width = bounding_box.width | 
					
						
							|  |  |  |  |             max_line_width = max(max_line_width, line_width) | 
					
						
							|  |  |  |  |             if text_align == 1: | 
					
						
							|  |  |  |  |                 # align center | 
					
						
							|  |  |  |  |                 letter_group.transform = f'translate({-line_width/2}, 0)' | 
					
						
							|  |  |  |  |             if text_align == 2: | 
					
						
							|  |  |  |  |                 letter_group.transform = f'translate({-line_width}, 0)' | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         if text_align in [3, 4]: | 
					
						
							|  |  |  |  |             for line_group in destination_group.iterchildren(): | 
					
						
							|  |  |  |  |                 if text_align == 4 and len(line_group) == 1: | 
					
						
							|  |  |  |  |                     line_group = line_group[0] | 
					
						
							|  |  |  |  |                 if len(line_group) > 1: | 
					
						
							| 
									
										
										
										
											2025-05-01 06:58:41 +00:00
										 |  |  |  |                     try: | 
					
						
							|  |  |  |  |                         distance = max_line_width - line_group.bounding_box().width | 
					
						
							|  |  |  |  |                     except AttributeError: | 
					
						
							|  |  |  |  |                         # line group bounding_box is None | 
					
						
							|  |  |  |  |                         continue | 
					
						
							| 
									
										
										
										
											2024-12-29 14:19:33 +00:00
										 |  |  |  |                     distance_per_space = distance / (len(line_group) - 1) | 
					
						
							|  |  |  |  |                     for i, word in enumerate(line_group.getchildren()[1:]): | 
					
						
							|  |  |  |  |                         transform = word.transform | 
					
						
							|  |  |  |  |                         translate = distance_per_space * (i + 1) | 
					
						
							|  |  |  |  |                         transform.add_translate(translate, 0) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-07 01:32:51 +00:00
										 |  |  |  |         if self.auto_satin and len(destination_group) > 0: | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  |             self._apply_auto_satin(destination_group) | 
					
						
							| 
									
										
										
										
											2021-03-04 17:40:53 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |         self._set_style(destination_group) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         # add trims | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |         self._add_trims(destination_group, text, trim_option, use_trim_symbols, back_and_forth, color_sort) | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |         # make sure necessary marker and command symbols are in the defs section | 
					
						
							| 
									
										
										
										
											2025-04-26 06:52:31 +00:00
										 |  |  |  |         ensure_command_symbols(destination_group) | 
					
						
							|  |  |  |  |         ensure_marker_symbols(destination_group) | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  |         if color_sort != 0 and self.sortable: | 
					
						
							|  |  |  |  |             self.do_color_sort(destination_group, color_sort) | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |         return destination_group | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     def _set_style(self, destination_group): | 
					
						
							| 
									
										
										
										
											2021-03-04 17:40:53 +00:00
										 |  |  |  |         # make sure font stroke styles have always a similar look | 
					
						
							|  |  |  |  |         for element in destination_group.iterdescendants(SVG_PATH_TAG): | 
					
						
							| 
									
										
										
										
											2021-12-04 10:06:10 +00:00
										 |  |  |  |             style = inkex.Style(element.get('style')) | 
					
						
							| 
									
										
										
										
											2021-03-04 17:40:53 +00:00
										 |  |  |  |             if style.get('fill') == 'none': | 
					
						
							| 
									
										
										
										
											2021-12-04 10:06:10 +00:00
										 |  |  |  |                 style += inkex.Style("stroke-width:1px") | 
					
						
							| 
									
										
										
										
											2021-03-04 17:40:53 +00:00
										 |  |  |  |                 if style.get('stroke-dasharray') and style.get('stroke-dasharray') != 'none': | 
					
						
							| 
									
										
										
										
											2021-12-04 10:06:10 +00:00
										 |  |  |  |                     style += inkex.Style("stroke-dasharray:3, 1") | 
					
						
							|  |  |  |  |                     # Set a smaller width to auto-route running stitches | 
					
						
							|  |  |  |  |                     if self.auto_satin or element.get_id().startswith("autosatinrun"): | 
					
						
							|  |  |  |  |                         style += inkex.Style("stroke-width:0.5px") | 
					
						
							|  |  |  |  |                 element.set('style', '%s' % style.to_str()) | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     def get_variant(self, variant): | 
					
						
							|  |  |  |  |         return self.variants.get(variant, self.variants[self.default_variant]) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-18 17:47:12 +00:00
										 |  |  |  |     def _render_line(self, destination_group, line, position, glyph_set, line_number): | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         """Render a line of text.
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         An SVG XML node tree will be returned, with an svg:g at its root.  If | 
					
						
							|  |  |  |  |         the font metadata requests it, Auto-Satin will be applied. | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         Parameters: | 
					
						
							|  |  |  |  |             line -- the line of text to render. | 
					
						
							|  |  |  |  |             position -- Current position.  Will be updated to point to the spot | 
					
						
							|  |  |  |  |                         immediately after the last character. | 
					
						
							|  |  |  |  |             glyph_set -- a FontVariant instance. | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         Returns: | 
					
						
							|  |  |  |  |             An svg:g element containing the rendered text. | 
					
						
							|  |  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2021-03-04 17:40:53 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |         group = inkex.Group() | 
					
						
							|  |  |  |  |         group.label = line | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |         if self.text_direction == 'rtl': | 
					
						
							|  |  |  |  |             group.label = line[::-1] | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |         group.set("inkstitch:letter-group", "line") | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         last_character = None | 
					
						
							| 
									
										
										
										
											2021-04-02 17:05:34 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  |         words = line.split(" ") | 
					
						
							| 
									
										
										
										
											2025-04-18 17:47:12 +00:00
										 |  |  |  |         for i, word in enumerate(words): | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  |             word_group = inkex.Group() | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |             label = word | 
					
						
							|  |  |  |  |             if self.text_direction == 'rtl': | 
					
						
							|  |  |  |  |                 label = word[::-1] | 
					
						
							|  |  |  |  |             word_group.label = label | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |             word_group.set("inkstitch:letter-group", "word") | 
					
						
							| 
									
										
										
										
											2021-04-05 17:20:48 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |             if self.text_direction == 'rtl': | 
					
						
							|  |  |  |  |                 glyphs = self._get_word_glyphs(glyph_set, word[::-1]) | 
					
						
							|  |  |  |  |                 glyphs = glyphs[::-1] | 
					
						
							|  |  |  |  |             else: | 
					
						
							|  |  |  |  |                 glyphs = self._get_word_glyphs(glyph_set, word) | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |             last_character = None | 
					
						
							| 
									
										
										
										
											2025-04-18 17:47:12 +00:00
										 |  |  |  |             for j, glyph in enumerate(glyphs): | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |                 if glyph is None: | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  |                     position.x += self.word_spacing | 
					
						
							|  |  |  |  |                     last_character = None | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |                     continue | 
					
						
							| 
									
										
										
										
											2025-04-18 17:47:12 +00:00
										 |  |  |  |                 node = self._render_glyph(destination_group, glyph, position, glyph.name, last_character, f'{line_number}-{i}-{j}') | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |                 word_group.append(node) | 
					
						
							|  |  |  |  |                 last_character = glyph.name | 
					
						
							|  |  |  |  |             group.append(word_group) | 
					
						
							|  |  |  |  |             position.x += self.word_spacing | 
					
						
							|  |  |  |  |         return group | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |     def _get_word_glyphs(self, glyph_set, word): | 
					
						
							|  |  |  |  |         glyphs = [] | 
					
						
							|  |  |  |  |         skip = [] | 
					
						
							|  |  |  |  |         previous_is_binding = True | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-14 19:23:08 +00:00
										 |  |  |  |         # forced letter case | 
					
						
							|  |  |  |  |         if self.letter_case == "upper": | 
					
						
							|  |  |  |  |             word = word.upper() | 
					
						
							|  |  |  |  |         elif self.letter_case == "lower": | 
					
						
							|  |  |  |  |             word = word.lower() | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |         for i, character in enumerate(word): | 
					
						
							|  |  |  |  |             if i in skip: | 
					
						
							|  |  |  |  |                 continue | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |             glyph, glyph_len, binding = glyph_set.get_next_glyph(word, i, previous_is_binding) | 
					
						
							|  |  |  |  |             previous_is_binding = binding | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             skip = list(range(i, i+glyph_len)) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             if glyph is None and self.default_glyph == " ": | 
					
						
							|  |  |  |  |                 glyphs.append(None) | 
					
						
							|  |  |  |  |             else: | 
					
						
							|  |  |  |  |                 if glyph is None: | 
					
						
							|  |  |  |  |                     glyphs.append(glyph_set[self.default_glyph]) | 
					
						
							|  |  |  |  |                 if glyph is not None: | 
					
						
							|  |  |  |  |                     glyphs.append(glyph) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         return glyphs | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-18 17:47:12 +00:00
										 |  |  |  |     def _render_glyph(self, destination_group, glyph, position, character, last_character, id_extension): | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         """Render a single glyph.
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         An SVG XML node tree will be returned, with an svg:g at its root. | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         Parameters: | 
					
						
							|  |  |  |  |             glyph -- a Glyph instance | 
					
						
							|  |  |  |  |             position -- Current position.  Will be updated based on the width | 
					
						
							|  |  |  |  |                         of this character and the letter spacing. | 
					
						
							|  |  |  |  |             character -- the current Unicode character. | 
					
						
							|  |  |  |  |             last_character -- the previous character in the line, or None if | 
					
						
							|  |  |  |  |                               we're at the start of the line or a word. | 
					
						
							|  |  |  |  |         """
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  |         # Concerning min_x: I add it before moving the letter because it is to | 
					
						
							|  |  |  |  |         # take into account the margin in the drawing of the letter. With respect | 
					
						
							|  |  |  |  |         # to point 0 the letter can start at 5 or -5. The letters have a defined | 
					
						
							|  |  |  |  |         # place in the drawing that's important. | 
					
						
							|  |  |  |  |         # Then to calculate the position of x for the next letter I have to remove | 
					
						
							|  |  |  |  |         # the min_x margin because the horizontal adv is calculated from point 0 of the drawing. | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  |         node = deepcopy(glyph.node) | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         if last_character is not None: | 
					
						
							| 
									
										
										
										
											2025-01-12 19:46:37 +00:00
										 |  |  |  |             if self.text_direction != "rtl": | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |                 kerning = self.kerning_pairs.get(f'{last_character} {character}', None) | 
					
						
							|  |  |  |  |                 if kerning is None: | 
					
						
							|  |  |  |  |                     # legacy kerning without space | 
					
						
							|  |  |  |  |                     kerning = self.kerning_pairs.get(last_character + character, 0) | 
					
						
							|  |  |  |  |                 position.x += glyph.min_x - kerning | 
					
						
							| 
									
										
										
										
											2025-01-12 19:46:37 +00:00
										 |  |  |  |             else: | 
					
						
							| 
									
										
										
										
											2025-02-05 17:50:31 +00:00
										 |  |  |  |                 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 | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |         transform = "translate(%s, %s)" % position.as_tuple() | 
					
						
							|  |  |  |  |         node.set('transform', transform) | 
					
						
							| 
									
										
										
										
											2021-02-04 15:40:02 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |         horiz_adv_x_default = self.horiz_adv_x_default | 
					
						
							|  |  |  |  |         if horiz_adv_x_default is None: | 
					
						
							|  |  |  |  |             horiz_adv_x_default = glyph.width + glyph.min_x | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         position.x += self.horiz_adv_x.get(character, horiz_adv_x_default) - glyph.min_x | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-18 17:47:12 +00:00
										 |  |  |  |         self._update_commands(node, glyph, id_extension) | 
					
						
							| 
									
										
										
										
											2024-11-12 18:07:24 +00:00
										 |  |  |  |         self._update_clips(destination_group, node, glyph) | 
					
						
							| 
									
										
										
										
											2021-10-09 16:25:29 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  |         # this is used to recognize a glyph layer later in the process | 
					
						
							|  |  |  |  |         # because this is not unique it will be overwritten by inkscape when inserted into the document | 
					
						
							|  |  |  |  |         node.set("id", "glyph") | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |         node.set("inkstitch:letter-group", "glyph") | 
					
						
							| 
									
										
										
										
											2025-07-06 12:00:28 +00:00
										 |  |  |  |         # force inkscape to show a label when the glyph is only a non-spacing mark | 
					
						
							|  |  |  |  |         if len(node.label) == 1 and unicodedata.category(node.label) == 'Mn': | 
					
						
							|  |  |  |  |             # force inkscape to show a label when the glyph is only a non-spacing mark | 
					
						
							|  |  |  |  |             node.label = ' ' + node.label | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         return node | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-26 06:52:31 +00:00
										 |  |  |  |     def _update_commands(self, node, glyph, id_extension=""): | 
					
						
							| 
									
										
										
										
											2021-10-09 16:25:29 +00:00
										 |  |  |  |         for element, connectors in glyph.commands.items(): | 
					
						
							|  |  |  |  |             # update element | 
					
						
							|  |  |  |  |             el = node.find(".//*[@id='%s']" % element) | 
					
						
							|  |  |  |  |             # we cannot get a unique id from the document at this point | 
					
						
							|  |  |  |  |             # so let's create a random id which will most probably work as well | 
					
						
							| 
									
										
										
										
											2025-04-18 17:47:12 +00:00
										 |  |  |  |             new_element_id = f'{element}-{id_extension}-{randint(0, 9999)}' | 
					
						
							| 
									
										
										
										
											2021-10-09 16:25:29 +00:00
										 |  |  |  |             el.set_id(new_element_id) | 
					
						
							|  |  |  |  |             for connector, symbol in connectors: | 
					
						
							|  |  |  |  |                 # update symbol | 
					
						
							| 
									
										
										
										
											2025-04-18 17:47:12 +00:00
										 |  |  |  |                 new_symbol_id = f'{symbol}-{id_extension}-{randint(0, 9999)}' | 
					
						
							| 
									
										
										
										
											2021-10-09 16:25:29 +00:00
										 |  |  |  |                 s = node.find(".//*[@id='%s']" % symbol) | 
					
						
							|  |  |  |  |                 s.set_id(new_symbol_id) | 
					
						
							|  |  |  |  |                 # update connector | 
					
						
							|  |  |  |  |                 c = node.find(".//*[@id='%s']" % connector) | 
					
						
							|  |  |  |  |                 c.set(CONNECTION_END, "#%s" % new_element_id) | 
					
						
							|  |  |  |  |                 c.set(CONNECTION_START, "#%s" % new_symbol_id) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-12 18:07:24 +00:00
										 |  |  |  |     def _update_clips(self, destination_group, node, glyph): | 
					
						
							|  |  |  |  |         svg = destination_group.getroottree().getroot() | 
					
						
							|  |  |  |  |         for node_id, clip in glyph.clips.items(): | 
					
						
							|  |  |  |  |             if clip not in svg.defs: | 
					
						
							|  |  |  |  |                 svg.defs.append(clip) | 
					
						
							|  |  |  |  |             el = node.find(f".//*[@id='{node_id}']") | 
					
						
							|  |  |  |  |             el.clip = clip | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |     def _add_trims(self, destination_group, text, trim_option, use_trim_symbols, back_and_forth, color_sort): | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  |         """
 | 
					
						
							|  |  |  |  |         trim_option == 0  --> no trims | 
					
						
							|  |  |  |  |         trim_option == 1  --> trim at the end of each line | 
					
						
							|  |  |  |  |         trim_option == 2  --> trim after each word | 
					
						
							|  |  |  |  |         trim_option == 3  --> trim after each letter | 
					
						
							|  |  |  |  |         """
 | 
					
						
							|  |  |  |  |         if trim_option == 0: | 
					
						
							|  |  |  |  |             return | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         # reverse every second line of text if back and forth is true and strip spaces | 
					
						
							|  |  |  |  |         text = text.splitlines() | 
					
						
							|  |  |  |  |         text = [t[::-1].strip() if i % 2 != 0 and back_and_forth else t.strip() for i, t in enumerate(text)] | 
					
						
							|  |  |  |  |         text = "\n".join(text) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         i = -1 | 
					
						
							|  |  |  |  |         space_indices = [i for i, t in enumerate(text) if t == " "] | 
					
						
							|  |  |  |  |         line_break_indices = [i for i, t in enumerate(text) if t == "\n"] | 
					
						
							|  |  |  |  |         for group in destination_group.iterdescendants(SVG_GROUP_TAG): | 
					
						
							|  |  |  |  |             # make sure we are only looking at glyph groups | 
					
						
							| 
									
										
										
										
											2024-02-10 19:17:36 +00:00
										 |  |  |  |             if not group.get("id", "").startswith("glyph"): | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  |                 continue | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             i += 1 | 
					
						
							|  |  |  |  |             while i in space_indices + line_break_indices: | 
					
						
							|  |  |  |  |                 i += 1 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             # letter | 
					
						
							|  |  |  |  |             if trim_option == 3: | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |                 self._process_trim(group, use_trim_symbols, color_sort) | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  |             # word | 
					
						
							|  |  |  |  |             elif trim_option == 2 and i+1 in space_indices + line_break_indices: | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |                 self._process_trim(group, use_trim_symbols, color_sort) | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  |             # line | 
					
						
							|  |  |  |  |             elif trim_option == 1 and i+1 in line_break_indices: | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |                 self._process_trim(group, use_trim_symbols, color_sort) | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |     def _process_trim(self, group, use_trim_symbols, color_sort): | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  |         if color_sort != 0 and self.sortable: | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |             elements = defaultdict(list) | 
					
						
							|  |  |  |  |             for path_child in group.iterdescendants(EMBROIDERABLE_TAGS): | 
					
						
							|  |  |  |  |                 if not has_marker(path_child): | 
					
						
							|  |  |  |  |                     sort_index = path_child.get('inkstitch:color_sort_index', None) | 
					
						
							|  |  |  |  |                     if sort_index is not None: | 
					
						
							|  |  |  |  |                         elements[sort_index] = path_child | 
					
						
							|  |  |  |  |                     else: | 
					
						
							|  |  |  |  |                         elements[404] = path_child | 
					
						
							|  |  |  |  |             for value in elements.values(): | 
					
						
							|  |  |  |  |                 self._add_trim_to_element(Stroke(value), use_trim_symbols) | 
					
						
							|  |  |  |  |         else: | 
					
						
							| 
									
										
										
										
											2025-04-15 18:20:58 +00:00
										 |  |  |  |             nodes = list(group.iterdescendants(EMBROIDERABLE_TAGS))[::-1] | 
					
						
							| 
									
										
										
										
											2025-02-21 12:35:24 +00:00
										 |  |  |  |             # find the last path that does not carry a marker or belongs to a visual command and add a trim there | 
					
						
							| 
									
										
										
										
											2025-04-15 18:20:58 +00:00
										 |  |  |  |             for path_child in nodes: | 
					
						
							|  |  |  |  |                 if has_marker(path_child) or path_child.get_id().startswith('command_connector'): | 
					
						
							|  |  |  |  |                     continue | 
					
						
							|  |  |  |  |                 element = Stroke(path_child) | 
					
						
							|  |  |  |  |                 self._add_trim_to_element(element, use_trim_symbols) | 
					
						
							|  |  |  |  |                 break | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     def _add_trim_to_element(self, element, use_trim_symbols): | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  |         if element.shape: | 
					
						
							|  |  |  |  |             element_id = "%s_%s" % (element.node.get('id'), randint(0, 9999)) | 
					
						
							|  |  |  |  |             element.node.set("id", element_id) | 
					
						
							| 
									
										
										
										
											2023-03-07 19:08:21 +00:00
										 |  |  |  |             if use_trim_symbols is False: | 
					
						
							|  |  |  |  |                 element.node.set(INKSTITCH_ATTRIBS['trim_after'], 'true') | 
					
						
							|  |  |  |  |             else: | 
					
						
							|  |  |  |  |                 add_commands(element, ['trim']) | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     def _apply_auto_satin(self, group): | 
					
						
							| 
									
										
										
										
											2018-11-15 01:23:06 +00:00
										 |  |  |  |         """Apply Auto-Satin to an SVG XML node tree with an svg:g at its root.
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         The group's contents will be replaced with the results of the auto- | 
					
						
							|  |  |  |  |         satin operation.  Any nested svg:g elements will be removed. | 
					
						
							|  |  |  |  |         """
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-05 15:47:03 +00:00
										 |  |  |  |         elements = nodes_to_elements(group.iterdescendants(EMBROIDERABLE_TAGS)) | 
					
						
							| 
									
										
										
										
											2022-11-30 14:49:51 +00:00
										 |  |  |  |         elements = [element for element in elements if isinstance(element, SatinColumn) or isinstance(element, Stroke)] | 
					
						
							| 
									
										
										
										
											2022-02-28 15:24:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-16 18:01:21 +00:00
										 |  |  |  |         if elements and any(isinstance(element, SatinColumn) for element in elements): | 
					
						
							| 
									
										
										
										
											2022-11-22 09:46:44 +00:00
										 |  |  |  |             auto_satin(elements, preserve_order=True, trim=False) | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  |     def do_color_sort(self, group, color_sort): | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |         """Sort elements by their color sort index as defined by font author""" | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |         if color_sort == 1: | 
					
						
							|  |  |  |  |             # Whole text | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |             self._color_sort_group(group, 'line') | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  |         elif color_sort == 2: | 
					
						
							|  |  |  |  |             # per line | 
					
						
							|  |  |  |  |             groups = group.getchildren() | 
					
						
							|  |  |  |  |             for group in groups: | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |                 self._color_sort_group(group, 'word') | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  |         elif color_sort == 3: | 
					
						
							|  |  |  |  |             # per word | 
					
						
							|  |  |  |  |             line_groups = group.getchildren() | 
					
						
							|  |  |  |  |             for line_group in line_groups: | 
					
						
							|  |  |  |  |                 for group in line_group.iterchildren(): | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |                     self._color_sort_group(group, 'glyph') | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |     def _color_sort_group(self, group, transform_key): | 
					
						
							|  |  |  |  |         elements_by_color = self._get_color_sorted_elements(group, transform_key) | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |         # 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) | 
					
						
							| 
									
										
										
										
											2024-12-30 19:40:24 +00:00
										 |  |  |  |                 if grouped_elements[0][0].get("inkstitch:fill_method", False) in ['tartan_fill', 'linear_gradient_fill']: | 
					
						
							|  |  |  |  |                     grouped_elements[0][0].set('inkstitch:stop_at_ending_point', True) | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |                 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) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-05 11:52:02 +00:00
										 |  |  |  |     def _get_color_sorted_elements(self, group, transform_key):  # noqa: C901 | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |         elements_by_color = defaultdict(list) | 
					
						
							|  |  |  |  |         last_parent = None | 
					
						
							| 
									
										
										
										
											2024-12-29 10:38:59 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |         for element in group.iterdescendants(EMBROIDERABLE_TAGS, SVG_GROUP_TAG): | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |             sort_index = element.get('inkstitch:color_sort_index', None) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |             # Skip command connectors, we only aim for command groups | 
					
						
							|  |  |  |  |             # Skip command connectors as well, they will be included with the command group | 
					
						
							|  |  |  |  |             if (element.TAG == 'g' and not element.get_id().startswith('command_group') | 
					
						
							|  |  |  |  |                     or element.get_id().startswith('command_connector')): | 
					
						
							|  |  |  |  |                 continue | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 21:37:36 +00:00
										 |  |  |  |             clips = get_clips(element) | 
					
						
							|  |  |  |  |             if len(clips) > 1: | 
					
						
							|  |  |  |  |                 # multiple clips: wrap the element into clipped groups | 
					
						
							|  |  |  |  |                 parent = element.getparent() | 
					
						
							|  |  |  |  |                 index = parent.index(element) | 
					
						
							|  |  |  |  |                 for clip in clips: | 
					
						
							|  |  |  |  |                     new_group = inkex.Group() | 
					
						
							|  |  |  |  |                     new_group.clip = clip | 
					
						
							|  |  |  |  |                     parent.insert(index, new_group) | 
					
						
							|  |  |  |  |                     new_group.append(element) | 
					
						
							|  |  |  |  |                     element = new_group | 
					
						
							|  |  |  |  |             elif len(clips) == 1: | 
					
						
							|  |  |  |  |                 # only one clip: we can apply the clip directly to the element | 
					
						
							|  |  |  |  |                 element.clip = clips[0] | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |             # get glyph group to calculate transform | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |             glyph_group = None | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |             for ancestor in element.ancestors(group): | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |                 if ancestor.get("inkstitch:letter-group", '') == transform_key: | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |                     glyph_group = ancestor | 
					
						
							|  |  |  |  |                     break | 
					
						
							| 
									
										
										
										
											2024-12-31 16:14:39 +00:00
										 |  |  |  |             if glyph_group is None: | 
					
						
							|  |  |  |  |                 # this should never happen | 
					
						
							|  |  |  |  |                 continue | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |             element.transform = element.composed_transform(glyph_group.getparent()) | 
					
						
							| 
									
										
										
										
											2024-11-12 18:07:24 +00:00
										 |  |  |  |             if sort_index is not None and int(sort_index) in self.combine_at_sort_indices: | 
					
						
							|  |  |  |  |                 element.apply_transform() | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |             if not sort_index: | 
					
						
							|  |  |  |  |                 elements_by_color[404].append([element]) | 
					
						
							|  |  |  |  |                 continue | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-30 16:36:12 +00:00
										 |  |  |  |             if element.get_id().startswith('command_group'): | 
					
						
							|  |  |  |  |                 elements_by_color[int(sort_index)].append([element]) | 
					
						
							|  |  |  |  |                 continue | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |             parent = element.getparent() | 
					
						
							| 
									
										
										
										
											2024-11-12 18:07:24 +00:00
										 |  |  |  |             if element.clip is None and parent.clip is not None: | 
					
						
							|  |  |  |  |                 element.clip = parent.clip | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |             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) | 
					
						
							| 
									
										
										
										
											2024-11-12 18:07:24 +00:00
										 |  |  |  |             last_parent = parent | 
					
						
							| 
									
										
										
										
											2024-10-21 15:01:58 +00:00
										 |  |  |  |         return elements_by_color |