diff options
| author | Kaalleen <36401965+kaalleen@users.noreply.github.com> | 2025-03-21 19:31:25 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-21 19:31:25 +0100 |
| commit | 9f91470ac779dd13e9bbb2b3357c6d3a89c08ae5 (patch) | |
| tree | dc30e2ad96a4cbe37bae46c56be022bda23f27b7 /lib/extensions/batch_lettering.py | |
| parent | de7d86e526faa64e79e7c5fb5a0606fbd4c3e42f (diff) | |
Add batch lettering extension (#3589)
Diffstat (limited to 'lib/extensions/batch_lettering.py')
| -rw-r--r-- | lib/extensions/batch_lettering.py | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/lib/extensions/batch_lettering.py b/lib/extensions/batch_lettering.py new file mode 100644 index 00000000..44ae7569 --- /dev/null +++ b/lib/extensions/batch_lettering.py @@ -0,0 +1,231 @@ +# Authors: see git history +# +# Copyright (c) 2025 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import json +import os +import sys +import tempfile +from copy import deepcopy +from zipfile import ZipFile + +from inkex import Boolean, Group, errormsg +from lxml import etree + +import pyembroidery + +from ..extensions.lettering_along_path import TextAlongPath +from ..i18n import _ +from ..lettering import get_font_by_name +from ..output import write_embroidery_file +from ..stitch_plan import stitch_groups_to_stitch_plan +from ..svg import get_correction_transform +from ..threads import ThreadCatalog +from ..utils import DotDict +from .base import InkstitchExtension + + +class BatchLettering(InkstitchExtension): + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self) + + self.arg_parser.add_argument('--notebook') + + self.arg_parser.add_argument('--text', type=str, default='', dest='text') + self.arg_parser.add_argument('--separator', type=str, default='', dest='separator') + + self.arg_parser.add_argument('--font', type=str, default='', dest='font') + self.arg_parser.add_argument('--scale', type=int, default=100, dest='scale') + self.arg_parser.add_argument('--color-sort', type=str, default='off', dest='color_sort') + self.arg_parser.add_argument('--trim', type=str, default='off', dest='trim') + self.arg_parser.add_argument('--use-command-symbols', type=Boolean, default=False, dest='command_symbols') + self.arg_parser.add_argument('--text-align', type=str, default='left', dest='text_align') + + self.arg_parser.add_argument('--text-position', type=str, default='left', dest='text_position') + + self.arg_parser.add_argument('--file-formats', type=str, default='', dest='formats') + + def effect(self): + separator = self.options.separator + if not separator: + separator = '\n' + text_input = self.options.text + if not text_input: + errormsg(_("Please specify a text")) + return + texts = text_input.replace('\\n', '\n').split(separator) + + if not self.options.font: + errormsg(_("Please specify a font")) + return + self.font = get_font_by_name(self.options.font) + if self.font is None: + errormsg(_("Please specify a valid font name")) + return + + if not self.options.formats: + errormsg(_("Please specify at least one output file format")) + return + available_formats = [file_format['extension'] for file_format in pyembroidery.supported_formats()] + ['svg'] + file_formats = self.options.formats.split(',') + file_formats = [file_format.strip().lower() for file_format in file_formats if file_format.strip().lower() in available_formats] + if not file_formats: + errormsg(_("Please specify at least one file format supported by pyembroidery")) + return + + self.setup_trim() + self.setup_text_align() + self.setup_color_sort() + self.setup_scale() + + self.generate_output_files(texts, file_formats) + + # don't let inkex output the SVG! + sys.exit(0) + + def setup_trim(self): + self.trim = 0 + if self.options.trim == "line": + self.trim = 1 + elif self.options.trim == "word": + self.trim = 2 + elif self.options.trim == "glyph": + self.trim = 3 + + def setup_text_align(self): + self.text_align = 0 + if self.options.text_align == "center": + self.text_align = 1 + elif self.options.text_align == "right": + self.text_align = 2 + elif self.options.text_align == "block": + self.text_align = 3 + elif self.options.text_align == "letterspacing": + self.text_align = 4 + + def setup_color_sort(self): + self.color_sort = 0 + if self.options.color_sort == "all": + self.color_sort = 1 + elif self.options.color_sort == "line": + self.color_sort = 2 + elif self.options.color_sort == "word": + self.color_sort = 3 + + def setup_scale(self): + self.scale = self.options.scale / 100 + if self.scale < self.font.min_scale: + self.scale = self.font.min_scale + elif self.scale > self.font.max_scale: + self.scale = self.font.max_scale + + def generate_output_files(self, texts, file_formats): + self.metadata = self.get_inkstitch_metadata() + self.collapse_len = self.metadata['collapse_len_mm'] + self.min_stitch_len = self.metadata['min_stitch_len_mm'] + + # The user can specify a path which can be use for the text along path method. + # The path should be labeled as "batch lettering" + text_positioning_path = self.svg.findone(".//*[@inkscape:label='batch lettering']") + + path = tempfile.mkdtemp() + files = [] + for text in texts: + stitch_plan, lettering_group = self.generate_stitch_plan(text, text_positioning_path) + for file_format in file_formats: + files.append(self.generate_output_file(file_format, path, text, stitch_plan)) + + self.reset_document(lettering_group, text_positioning_path) + + temp_file = tempfile.NamedTemporaryFile(suffix=".zip", delete=False) + + # in windows, failure to close here will keep the file locked + temp_file.close() + + with ZipFile(temp_file.name, "w") as zip_file: + for output in files: + zip_file.write(output, os.path.basename(output)) + + # inkscape will read the file contents from stdout and copy + # to the destination file that the user chose + with open(temp_file.name, 'rb') as output_file: + sys.stdout.buffer.write(output_file.read()) + + os.remove(temp_file.name) + for output in files: + os.remove(output) + os.rmdir(path) + + def reset_document(self, lettering_group, text_positioning_path): + # reset document for the next iteration + parent = lettering_group.getparent() + index = parent.index(lettering_group) + if text_positioning_path is not None: + parent.insert(index, text_positioning_path) + parent.remove(lettering_group) + + def generate_output_file(self, file_format, path, text, stitch_plan): + text = text.replace('\n', '') + output_file = os.path.join(path, f"{text}.{file_format}") + + if file_format == 'svg': + document = deepcopy(self.document.getroot()) + with open(output_file, 'w', encoding='utf-8') as svg: + svg.write(etree.tostring(document).decode('utf-8')) + else: + write_embroidery_file(output_file, stitch_plan, self.document.getroot()) + + return output_file + + def generate_stitch_plan(self, text, text_positioning_path): + + self.settings = DotDict({ + "text": text, + "text_align": self.text_align, + "back_and_forth": True, + "font": self.font.marked_custom_font_id, + "scale": self.scale * 100, + "trim_option": self.trim, + "use_trim_symbols": self.options.command_symbols, + "color_sort": self.color_sort + }) + + lettering_group = Group() + lettering_group.label = _("Ink/Stitch Lettering") + lettering_group.set('inkstitch:lettering', json.dumps(self.settings)) + self.svg.append(lettering_group) + lettering_group.set("transform", get_correction_transform(lettering_group, child=True)) + + destination_group = Group() + destination_group.label = f"{self.font.name} {_('scale')} {self.scale * 100}%" + lettering_group.append(destination_group) + + text = self.font.render_text( + text, + destination_group, + trim_option=self.trim, + use_trim_symbols=self.options.command_symbols, + color_sort=self.color_sort, + text_align=self.text_align + ) + + destination_group.attrib['transform'] = f'scale({self.scale})' + + if text_positioning_path is not None: + parent = text_positioning_path.getparent() + index = parent.index(text_positioning_path) + parent.insert(index, lettering_group) + TextAlongPath(self.svg, lettering_group, text_positioning_path, self.options.text_position) + parent.remove(text_positioning_path) + + self.get_elements() + stitch_groups = self.elements_to_stitch_groups(self.elements) + stitch_plan = stitch_groups_to_stitch_plan(stitch_groups, collapse_len=self.collapse_len, min_stitch_len=self.min_stitch_len) + ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) + + return stitch_plan, lettering_group + + +if __name__ == '__main__': + BatchLettering().run() |
