# 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.")) errormsg(_("You can find a list with all font names on our website: https://inkstitch.org/fonts/font-library/")) 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 Ink/Stitch")) errormsg(_("You can find a list with all supported file formats our website: https://inkstitch.org/docs/file-formats/#writing")) 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) lettering_group.delete() 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) text_positioning_path.delete() 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()