inkstitch/lib/extensions/batch_lettering.py

234 wiersze
9.0 KiB
Python

# 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()