diff options
| -rw-r--r-- | lib/extensions/__init__.py | 6 | ||||
| -rw-r--r-- | lib/extensions/png_realistic.py | 32 | ||||
| -rw-r--r-- | lib/extensions/png_simple.py | 72 | ||||
| -rw-r--r-- | lib/extensions/thread_list.py | 78 | ||||
| -rw-r--r-- | lib/extensions/zip.py | 93 | ||||
| -rw-r--r-- | lib/inx/outputs.py | 3 | ||||
| -rw-r--r-- | lib/output.py | 3 | ||||
| -rw-r--r-- | lib/svg/rendering.py | 8 | ||||
| -rw-r--r-- | templates/png_realistic.xml | 16 | ||||
| -rw-r--r-- | templates/png_simple.xml | 30 | ||||
| -rw-r--r-- | templates/thread_list.xml | 16 | ||||
| -rw-r--r-- | templates/zip.xml | 12 |
12 files changed, 311 insertions, 58 deletions
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index e38eeb43..901fe02c 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -46,6 +46,8 @@ from .output import Output from .palette_split_text import PaletteSplitText from .palette_to_text import PaletteToText from .params import Params +from .png_realistic import PngRealistic +from .png_simple import PngSimple from .preferences import Preferences from .print_pdf import Print from .redwork import Redwork @@ -61,6 +63,7 @@ from .stitch_plan_preview_undo import StitchPlanPreviewUndo from .stroke_to_lpe_satin import StrokeToLpeSatin from .tartan import Tartan from .test_swatches import TestSwatches +from .thread_list import ThreadList from .troubleshoot import Troubleshoot from .unlink_clone import UnlinkClone from .update_svg import UpdateSvg @@ -110,6 +113,8 @@ __all__ = extensions = [About, PaletteSplitText, PaletteToText, Params, + PngRealistic, + PngSimple, Preferences, Print, Redwork, @@ -125,6 +130,7 @@ __all__ = extensions = [About, StrokeToLpeSatin, Tartan, TestSwatches, + ThreadList, Troubleshoot, UnlinkClone, UpdateSvg, diff --git a/lib/extensions/png_realistic.py b/lib/extensions/png_realistic.py new file mode 100644 index 00000000..24ebce55 --- /dev/null +++ b/lib/extensions/png_realistic.py @@ -0,0 +1,32 @@ +# Authors: see git history +# +# Copyright (c) 2024 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import sys + +from ..stitch_plan import stitch_groups_to_stitch_plan +from ..svg import render_stitch_plan +from ..threads import ThreadCatalog +from .base import InkstitchExtension +from .png_simple import write_png_output + + +class PngRealistic(InkstitchExtension): + def effect(self): + if not self.get_elements(): + return + + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] + min_stitch_len = self.metadata['min_stitch_len_mm'] + stitch_groups = self.elements_to_stitch_groups(self.elements) + stitch_plan = stitch_groups_to_stitch_plan(stitch_groups, collapse_len=collapse_len, min_stitch_len=min_stitch_len) + ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) + + layer = render_stitch_plan(self.svg, stitch_plan, True, visual_commands=False, render_jumps=False) + + write_png_output(self.svg, layer) + + # don't let inkex output the SVG! + sys.exit(0) diff --git a/lib/extensions/png_simple.py b/lib/extensions/png_simple.py new file mode 100644 index 00000000..7902b43b --- /dev/null +++ b/lib/extensions/png_simple.py @@ -0,0 +1,72 @@ +# Authors: see git history +# +# Copyright (c) 2024 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import sys +from tempfile import TemporaryDirectory + +from inkex.units import convert_unit + +from ..stitch_plan import stitch_groups_to_stitch_plan +from ..svg import render_stitch_plan +from ..threads import ThreadCatalog +from ..utils.svg_data import get_pagecolor +from .base import InkstitchExtension +from .utils.inkex_command import inkscape + + +class PngSimple(InkstitchExtension): + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self) + + self.arg_parser.add_argument('--notebook', type=str, default='') + self.arg_parser.add_argument('--line_width', type=str, default='', dest='line_width') + + def effect(self): + if not self.get_elements(): + return + + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] + min_stitch_len = self.metadata['min_stitch_len_mm'] + stitch_groups = self.elements_to_stitch_groups(self.elements) + stitch_plan = stitch_groups_to_stitch_plan(stitch_groups, collapse_len=collapse_len, min_stitch_len=min_stitch_len) + ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) + + line_width = convert_unit(f"{self.options.line_width}mm", self.svg.document_unit) + layer = render_stitch_plan(self.svg, stitch_plan, False, visual_commands=False, + render_jumps=False, line_width=line_width) + + write_png_output(self.svg, layer) + + # don't let inkex output the SVG! + sys.exit(0) + + +def write_png_output(svg, layer): + with TemporaryDirectory() as tempdir: + # Inkex's command functionality also writes files to temp directories like this. + temp_svg_path = f"{tempdir}/temp.svg" + temp_png_path = f"{tempdir}/temp.png" + with open(temp_svg_path, "wb") as f: + f.write(svg.tostring()) + + generate_png(svg, layer, temp_svg_path, temp_png_path) + + # inkscape will read the file contents from stdout and copy + # to the destination file that the user chose + with open(temp_png_path, 'rb') as output_file: + sys.stdout.buffer.write(output_file.read()) + + +def generate_png(svg, layer, input_path, output_path): + inkscape(input_path, actions="; ".join([ + f"export-id:{layer.get_id()}", + "export-id-only", + "export-type:png", + f"export-dpi:{96*8}", + f"export-filename:{output_path}", + f"export-background:{get_pagecolor(svg.namedview)}", + "export-do" # Inkscape docs say this should be implicit at the end, but it doesn't seem to be. + ])) diff --git a/lib/extensions/thread_list.py b/lib/extensions/thread_list.py new file mode 100644 index 00000000..5603107a --- /dev/null +++ b/lib/extensions/thread_list.py @@ -0,0 +1,78 @@ +# Authors: see git history +# +# Copyright (c) 2024 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import sys + +from ..i18n import _ +from ..stitch_plan import stitch_groups_to_stitch_plan +from ..threads import ThreadCatalog +from .base import InkstitchExtension + + +class ThreadList(InkstitchExtension): + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self) + + def effect(self): + if not self.get_elements(): + return + + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] + min_stitch_len = self.metadata['min_stitch_len_mm'] + stitch_groups = self.elements_to_stitch_groups(self.elements) + stitch_plan = stitch_groups_to_stitch_plan(stitch_groups, collapse_len=collapse_len, min_stitch_len=min_stitch_len) + ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) + + thread_list = get_threadlist(stitch_plan, self.get_base_file_name()) + + # inkscape will read the file contents from stdout and copy + # to the destination file that the user chose + sys.stdout.write(thread_list) + + # don't let inkex output the SVG! + sys.exit(0) + + +def get_threadlist(stitch_plan, design_name): + width = round(stitch_plan.dimensions_mm[0], 2) + height = round(stitch_plan.dimensions_mm[1], 2) + + thread_used = [] + + thread_output = "%s\n" % _("Design Details") + thread_output += "==============================\n\n" + + thread_output += _("Title") + thread_output += f": {design_name}\n" + + thread_output += _("Size") + thread_output += f" (mm): {width}, {height}" + + thread_output += _("Stitches") + thread_output += f": {stitch_plan.num_stitches}\n" + + thread_output += _("Colors") + thread_output += f": {stitch_plan.num_colors}\n\n" + + thread_output += _("Thread Order") + thread_output += "\n===========================\n\n" + + for i, color_block in enumerate(stitch_plan): + thread = color_block.color + + thread_output += str(i + 1) + " " + string = f"{thread.name} #{thread.number} - {thread.manufacturer} (#{thread.hex_digits.lower()})" + thread_output += string + "\n" + thread_used.append(string) + + thread_output += "\n" + thread_output += _("Thread Used") + "\n" + thread_output += "===========================" + "\n\n" + + for thread in set(thread_used): + thread_output += thread + "\n" + + return thread_output diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py index 9a13ca2d..1c33c080 100644 --- a/lib/extensions/zip.py +++ b/lib/extensions/zip.py @@ -10,6 +10,7 @@ from copy import deepcopy from zipfile import ZipFile from inkex import Boolean, errormsg +from inkex.units import convert_unit from lxml import etree import pyembroidery @@ -17,10 +18,12 @@ import pyembroidery from ..i18n import _ from ..output import write_embroidery_file from ..stitch_plan import stitch_groups_to_stitch_plan -from ..svg import PIXELS_PER_MM +from ..svg import PIXELS_PER_MM, render_stitch_plan from ..threads import ThreadCatalog from ..utils.geometry import Point from .base import InkstitchExtension +from .png_simple import generate_png +from .thread_list import get_threadlist class Zip(InkstitchExtension): @@ -41,6 +44,11 @@ class Zip(InkstitchExtension): self.formats.append('svg') self.arg_parser.add_argument('--format-threadlist', type=Boolean, default=False, dest='threadlist') self.formats.append('threadlist') + self.arg_parser.add_argument('--format-png_realistic', type=Boolean, default=False, dest='png_realistic') + self.formats.append('png_realistic') + self.arg_parser.add_argument('--format-png_simple', type=Boolean, default=False, dest='png_simple') + self.arg_parser.add_argument('--png_simple_line_width', type=float, default=0.3, dest='line_width') + self.formats.append('png_simple') self.arg_parser.add_argument('--x-repeats', type=int, default=1, dest='x_repeats', ) self.arg_parser.add_argument('--y-repeats', type=int, default=1, dest='y_repeats',) @@ -64,23 +72,7 @@ class Zip(InkstitchExtension): base_file_name = self._get_file_name() path = tempfile.mkdtemp() - files = [] - - for format in self.formats: - if getattr(self.options, format): - output_file = os.path.join(path, "%s.%s" % (base_file_name, format)) - if 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')) - elif format == 'threadlist': - output_file = os.path.join(path, "%s_%s.txt" % (base_file_name, _("threadlist"))) - output = open(output_file, 'w', encoding='utf-8') - output.write(self.get_threadlist(stitch_plan, base_file_name)) - output.close() - else: - write_embroidery_file(output_file, stitch_plan, self.document.getroot()) - files.append(output_file) + files = self.generate_output_files(stitch_plan, path, base_file_name) if not files: errormsg(_("No embroidery file formats selected.")) @@ -123,34 +115,37 @@ class Zip(InkstitchExtension): offsets.append(Point(x * dx, y * dy)) return stitch_plan.make_offsets(offsets) - def get_threadlist(self, stitch_plan, design_name): - thread_used = [] - - thread_output = "%s\n" % _("Design Details") - thread_output += "==============\n\n" - - thread_output += "%s: %s\n" % (_("Title"), design_name) - thread_output += "%s (mm): %.2f x %.2f\n" % (_("Size"), stitch_plan.dimensions_mm[0], stitch_plan.dimensions_mm[1]) - thread_output += "%s: %s\n" % (_("Stitches"), stitch_plan.num_stitches) - thread_output += "%s: %s\n\n" % (_("Colors"), stitch_plan.num_colors) - - thread_output += "%s\n" % _("Thread Order") - thread_output += "============\n\n" - - for i, color_block in enumerate(stitch_plan): - thread = color_block.color - - thread_output += str(i + 1) + " " - string = "%s #%s - %s (#%s)" % (thread.name, thread.number, thread.manufacturer, thread.hex_digits.lower()) - thread_output += string + "\n" - - thread_used.append(string) - - thread_output += "\n" - thread_output += _("Thread Used") + "\n" - thread_output += "============" + "\n\n" - - for thread in set(thread_used): - thread_output += thread + "\n" - - return "%s" % thread_output + def generate_output_files(self, stitch_plan, path, base_file_name): + files = [] + for format in self.formats: + if getattr(self.options, format): + output_file = os.path.join(path, "%s.%s" % (base_file_name, format)) + if 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')) + elif format == 'threadlist': + output_file = os.path.join(path, "%s_%s.txt" % (base_file_name, _("threadlist"))) + with open(output_file, 'w', encoding='utf-8') as output: + output.write(get_threadlist(stitch_plan, base_file_name)) + elif format == 'png_realistic': + output_file = os.path.join(path, f"{base_file_name}_realistic.png") + layer = render_stitch_plan(self.svg, stitch_plan, True, visual_commands=False, render_jumps=False) + self.generate_png_output(output_file, layer) + elif format == 'png_simple': + output_file = os.path.join(path, f"{base_file_name}_simple.png") + line_width = convert_unit(f"{self.options.line_width}mm", self.svg.document_unit) + layer = render_stitch_plan(self.svg, stitch_plan, False, visual_commands=False, + render_jumps=False, line_width=line_width) + self.generate_png_output(output_file, layer) + else: + write_embroidery_file(output_file, stitch_plan, self.document.getroot()) + files.append(output_file) + return files + + def generate_png_output(self, output_file, layer): + with tempfile.TemporaryDirectory() as tempdir: + temp_svg_path = f"{tempdir}/temp.svg" + with open(temp_svg_path, "wb") as f: + f.write(self.svg.tostring()) + generate_png(self.svg, layer, temp_svg_path, output_file) diff --git a/lib/inx/outputs.py b/lib/inx/outputs.py index 21014744..af2766ea 100644 --- a/lib/inx/outputs.py +++ b/lib/inx/outputs.py @@ -20,7 +20,8 @@ def pyembroidery_output_formats(): description = "%s [STITCH]" % description elif format['category'] != "embroidery": description = "%s [DEBUG]" % description - yield format['extension'], description, format['mimetype'], format['category'] + if not format['extension'] == 'png': + yield format['extension'], description, format['mimetype'], format['category'] def generate_output_inx_files(alter_data): diff --git a/lib/output.py b/lib/output.py index 1f932a0b..4559ca2b 100644 --- a/lib/output.py +++ b/lib/output.py @@ -104,9 +104,6 @@ def write_embroidery_file(file_path, stitch_plan, svg, settings={}): settings['max_stitch'] = float('inf') settings['max_jump'] = float('inf') settings['explicit_trim'] = False - elif file_path.endswith('.png'): - settings['linewidth'] = 1 - settings['background'] = 'white' try: pyembroidery.write(pattern, file_path, settings) diff --git a/lib/svg/rendering.py b/lib/svg/rendering.py index ee0b5d0c..4f0ec154 100644 --- a/lib/svg/rendering.py +++ b/lib/svg/rendering.py @@ -179,7 +179,7 @@ def color_block_to_realistic_stitches(color_block, svg, destination, render_jump start = point -def color_block_to_paths(color_block, svg, destination, visual_commands, render_jumps=True): +def color_block_to_paths(color_block, svg, destination, visual_commands, line_width, render_jumps=True): # If we try to import these above, we get into a mess of circular # imports. from ..commands import add_commands @@ -200,7 +200,7 @@ def color_block_to_paths(color_block, svg, destination, visual_commands, render_ color = color_block.color.visible_on_white.to_hex_str() path = inkex.PathElement(attrib={ 'id': svg.get_unique_id("object"), - 'style': "stroke: %s; stroke-width: 0.4; fill: none;" % color, + 'style': f"stroke: {color}; stroke-width: {line_width}; fill: none;stroke-linejoin: round;stroke-linecap: round;", 'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list), 'transform': get_correction_transform(svg), INKSTITCH_ATTRIBS['stroke_method']: 'manual_stitch' @@ -220,7 +220,7 @@ def color_block_to_paths(color_block, svg, destination, visual_commands, render_ path.set(INKSTITCH_ATTRIBS['stop_after'], 'true') -def render_stitch_plan(svg, stitch_plan, realistic=False, visual_commands=True, render_jumps=True) -> inkex.Group: +def render_stitch_plan(svg, stitch_plan, realistic=False, visual_commands=True, render_jumps=True, line_width=0.4) -> inkex.Group: layer_or_image = svg.findone(".//*[@id='__inkstitch_stitch_plan__']") if layer_or_image is not None: layer_or_image.getparent().remove(layer_or_image) @@ -241,7 +241,7 @@ def render_stitch_plan(svg, stitch_plan, realistic=False, visual_commands=True, if realistic: color_block_to_realistic_stitches(color_block, svg, group, render_jumps) else: - color_block_to_paths(color_block, svg, group, visual_commands, render_jumps) + color_block_to_paths(color_block, svg, group, visual_commands, line_width, render_jumps) if realistic: # Remove filter from defs, if any diff --git a/templates/png_realistic.xml b/templates/png_realistic.xml new file mode 100644 index 00000000..93f0ee2b --- /dev/null +++ b/templates/png_realistic.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension"> + <name>PNG file output (realistic)</name> + <id>org.{{ id_inkstitch }}.png_realistic</id> + <output is_exported="true"> + <extension>.png</extension> + <mimetype>image/png</mimetype> + <filetypename>{{ menu_inkstitch }}: Portable Network Graphics (Realistic) [IMAGE] (.png)</filetypename> + <filetypetooltip>Create a PNG file with a realistic embroidery representation using Ink/Stitch</filetypetooltip> + <dataloss>true</dataloss> + </output> + <param name="extension" type="string" gui-hidden="true">png_realistic</param> + <script> + {{ command_tag | safe }} + </script> +</inkscape-extension> diff --git a/templates/png_simple.xml b/templates/png_simple.xml new file mode 100644 index 00000000..be12e784 --- /dev/null +++ b/templates/png_simple.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension"> + <name>embroidery PNG file output</name> + <id>org.{{ id_inkstitch }}.png_simple</id> + <output is_exported="true"> + <extension>.png</extension> + <mimetype>image/png</mimetype> + <filetypename>{{ menu_inkstitch }}: Portable Network Graphics (Simple) [IMAGE] (.png)</filetypename> + <filetypetooltip>Create a PNG file with a simple line embroidery representation using Ink/Stitch</filetypetooltip> + <dataloss>true</dataloss> + </output> + <param name="extension" type="string" gui-hidden="true">png_simple</param> + <param name="notebook" type="notebook"> + <page name="settings" gui-text="Settings"> + <param name="line_width" type="float" precision="2" min="0.01" max="5" gui-text="Line width (mm)">0.3</param> + </page> + <page name="info" gui-text="Help"> + <label appearance="header">PNG file export</label> + <label>Export embroidery design to PNG</label> + <spacer /> + <separator /> + <spacer /> + <label>Read more on our webiste</label> + <label appearance="url">https://inkstitch.org/docs/import-export/</label> + </page> + </param> + <script> + {{ command_tag | safe }} + </script> +</inkscape-extension> diff --git a/templates/thread_list.xml b/templates/thread_list.xml new file mode 100644 index 00000000..3da67ae1 --- /dev/null +++ b/templates/thread_list.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension"> + <name>embroidery TXT file output</name> + <id>org.{{ id_inkstitch }}.thread_list</id> + <output> + <extension>.txt</extension> + <mimetype>text/plain</mimetype> + <filetypename>{{ menu_inkstitch }}: Threadlist [COLOR] (.txt)</filetypename> + <filetypetooltip>A list of thread colors</filetypetooltip> + <dataloss>true</dataloss> + </output> + <param name="extension" type="string" gui-hidden="true">thread_list</param> + <script> + {{ command_tag | safe }} + </script> +</inkscape-extension> diff --git a/templates/zip.xml b/templates/zip.xml index ddaefe5b..448819bc 100644 --- a/templates/zip.xml +++ b/templates/zip.xml @@ -17,10 +17,13 @@ <label>Output formats:</label> {%- for format, description, mimetype, category in formats %} {%- if category != "vector" and category != "debug" %} - <param name="format-{{ format }}" type="boolean" _gui-text=".{{ format | upper }}: {{ description }}">false</param> + <param name="format-{{ format }}" type="boolean" gui-text=".{{ format | upper }}: {{ description }}">false</param> {%- endif %} {%- endfor %} <param name="format-threadlist" type="boolean" gui-text=".TXT: Threadlist [COLOR]">false</param> + <param name="format-png_realistic" type="boolean" gui-text=".PNG: Portable Network Graphics (Realistic) [COLOR]">false</param> + <param name="format-png_simple" type="boolean" gui-text=".PNG: Portable Network Graphics (Simple) [COLOR]">false</param> + <param name="png_simple_line_width" type="float" precision="2" min="0.01" max="5" gui-text="Line width (mm)" indent="4">0.3</param> <param name="format-svg" type="boolean" gui-text=".SVG: Scalable Vector Graphic">false</param> <param name="extension" type="string" gui-hidden="true">zip</param> </page> @@ -32,6 +35,13 @@ <param name="y-repeats" type="int" min="1" max="20" gui-text="Vertical repeats">1</param> <param name="y-spacing" type="float" min="-1000" max="1000" gui-text="Vertical spacing (mm)">100</param> </page> + <page name="info" gui-text="Help"> + <label appearance="header">Zip File Export</label> + <label>Export multiple embroidery file formats at once.</label> + <separator /> + <label>Read more on our webiste:</label> + <label appearance="url">https://inkstitch.org/docs/import-export/#batch-export</label> + </page> </param> <script> {{ command_tag | safe }} |
