diff options
Diffstat (limited to 'lib/extensions')
27 files changed, 473 insertions, 329 deletions
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index a5388f19..1758772e 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -1,28 +1,32 @@ -from auto_satin import AutoSatin -from break_apart import BreakApart -from cleanup import Cleanup -from convert_to_satin import ConvertToSatin -from cut_satin import CutSatin -from embroider import Embroider -from flip import Flip -from global_commands import GlobalCommands -from import_threadlist import ImportThreadlist -from input import Input -from install import Install -from layer_commands import LayerCommands -from lettering import Lettering from lib.extensions.troubleshoot import Troubleshoot -from object_commands import ObjectCommands -from output import Output -from params import Params -from print_pdf import Print -from remove_embroidery_settings import RemoveEmbroiderySettings -from simulator import Simulator -from stitch_plan_preview import StitchPlanPreview -from zip import Zip -__all__ = extensions = [Embroider, - StitchPlanPreview, +from .auto_satin import AutoSatin +from .break_apart import BreakApart +from .cleanup import Cleanup +from .convert_to_satin import ConvertToSatin +from .cut_satin import CutSatin +from .flip import Flip +from .global_commands import GlobalCommands +from .import_threadlist import ImportThreadlist +from .input import Input +from .install import Install +from .layer_commands import LayerCommands +from .lettering import Lettering +from .object_commands import ObjectCommands +from .output import Output +from .params import Params +from .print_pdf import Print +from .remove_embroidery_settings import RemoveEmbroiderySettings +from .reorder import Reorder +from .simulator import Simulator +from .stitch_plan_preview import StitchPlanPreview +from .zip import Zip +from .lettering_generate_json import LetteringGenerateJson +from .lettering_remove_kerning import LetteringRemoveKerning +from .lettering_custom_font_dir import LetteringCustomFontDir +from .embroider_settings import EmbroiderSettings + +__all__ = extensions = [StitchPlanPreview, Install, Params, Print, @@ -37,9 +41,14 @@ __all__ = extensions = [Embroider, CutSatin, AutoSatin, Lettering, + LetteringGenerateJson, + LetteringRemoveKerning, + LetteringCustomFontDir, Troubleshoot, RemoveEmbroiderySettings, Cleanup, BreakApart, ImportThreadlist, - Simulator] + Simulator, + Reorder, + EmbroiderSettings] diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index a447a493..fce4a1fd 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -14,7 +14,7 @@ class AutoSatin(CommandsExtension): def __init__(self, *args, **kwargs): CommandsExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-p", "--preserve_order", dest="preserve_order", type="inkbool", default=False) + self.arg_parser.add_argument("-p", "--preserve_order", dest="preserve_order", type=inkex.Boolean, default=False) def get_starting_point(self): return self.get_point("satin_start") @@ -39,7 +39,7 @@ class AutoSatin(CommandsExtension): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: # L10N auto-route satin columns extension inkex.errormsg(_("Please select one or more satin columns.")) return False diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 9f6dc5f6..1a38973f 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -1,21 +1,19 @@ import json import os import re -from collections import MutableMapping -from copy import deepcopy - -from stringcase import snakecase +from collections.abc import MutableMapping import inkex +from lxml import etree +from stringcase import snakecase from ..commands import is_command, layer_commands from ..elements import EmbroideryElement, nodes_to_elements -from ..elements.clone import is_clone, is_embroiderable_clone +from ..elements.clone import is_clone from ..i18n import _ from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, - NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG, - SVG_PATH_TAG) + NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -71,7 +69,7 @@ class InkStitchMetadata(MutableMapping): tag = inkex.addNS(name, "inkstitch") item = self.metadata.find(tag) if item is None and create: - item = inkex.etree.SubElement(self.metadata, tag) + item = etree.SubElement(self.metadata, tag) return item @@ -117,7 +115,7 @@ class InkstitchExtension(inkex.Effect): def ensure_current_layer(self): # if no layer is selected, inkex defaults to the root, which isn't # particularly useful - if self.current_layer is self.document.getroot(): + if self.svg.get_current_layer() is self.document.getroot(): try: self.current_layer = self.document.xpath(".//svg:g[@inkscape:groupmode='layer']", namespaces=inkex.NSS)[0] except IndexError: @@ -125,7 +123,7 @@ class InkstitchExtension(inkex.Effect): pass def no_elements_error(self): - if self.selected: + if self.svg.selected: # l10n This was previously: "No embroiderable paths selected." inkex.errormsg(_("Ink/Stitch doesn't know how to work with any of the objects you've selected.") + "\n") else: @@ -154,8 +152,8 @@ class InkstitchExtension(inkex.Effect): if is_command(node) or node.get(CONNECTOR_TYPE): return[] - if self.selected: - if node.get("id") in self.selected: + if self.svg.selected: + if node.get("id") in self.svg.selected: selected = True else: # if the user didn't select anything that means we process everything @@ -165,7 +163,7 @@ class InkstitchExtension(inkex.Effect): nodes.extend(self.descendants(child, selected, troubleshoot)) if selected: - if (node.tag in EMBROIDERABLE_TAGS or is_embroiderable_clone(node)) and not (node.tag == SVG_PATH_TAG and not node.get('d', '')): + if getattr(node, "get_path", None): nodes.append(node) elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or node.tag in EMBROIDERABLE_TAGS or is_clone(node)): nodes.append(node) @@ -206,24 +204,3 @@ class InkstitchExtension(inkex.Effect): def uniqueId(self, prefix, make_new_id=True): """Override inkex.Effect.uniqueId with a nicer naming scheme.""" return generate_unique_id(self.document, prefix) - - def parse(self): - """Override inkex.Effect.parse to add Ink/Stitch xml namespace""" - - # SVG parsers don't actually look for anything at this URL. They just - # care that it's unique. That defines a "namespace" of element and - # attribute names to disambiguate conflicts with element and - # attribute names other XML namespaces. - - # call the superclass's method first - inkex.Effect.parse(self) - - # Add the inkstitch namespace to the SVG. The inkstitch namespace is - # added to inkex.NSS in ../svg/tags.py at import time. - - # The below is the only way I could find to add a namespace to an - # existing element tree at the top without getting ugly prefixes like "ns0". - inkex.etree.cleanup_namespaces(self.document, - top_nsmap=inkex.NSS, - keep_ns_prefixes=inkex.NSS.keys()) - self.original_document = deepcopy(self.document) diff --git a/lib/extensions/break_apart.py b/lib/extensions/break_apart.py index 0b17d3d7..d0ab2619 100644 --- a/lib/extensions/break_apart.py +++ b/lib/extensions/break_apart.py @@ -1,11 +1,10 @@ import logging from copy import copy +import inkex from shapely.geometry import LineString, MultiPolygon, Polygon from shapely.ops import polygonize, unary_union -import inkex - from ..elements import EmbroideryElement from ..i18n import _ from ..svg import get_correction_transform @@ -19,10 +18,11 @@ class BreakApart(InkstitchExtension): ''' def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-m", "--method", type="int", default=1, dest="method") + self.arg_parser.add_argument("-m", "--method", type=int, default=1, dest="method") + self.minimum_size = 5 def effect(self): # noqa: C901 - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select one or more fill areas to break apart.")) return @@ -41,13 +41,12 @@ class BreakApart(InkstitchExtension): try: paths.sort(key=lambda point_list: Polygon(point_list).area, reverse=True) polygon = MultiPolygon([(paths[0], paths[1:])]) - if self.geom_is_valid(polygon): + if self.geom_is_valid(polygon) and Polygon(paths[-1]).area > self.minimum_size: continue except ValueError: pass polygons = self.break_apart_paths(paths) - polygons = self.ensure_minimum_size(polygons, 5) if self.options.method == 1: polygons = self.combine_overlapping_polygons(polygons) polygons = self.recombine_polygons(polygons) @@ -106,6 +105,7 @@ class BreakApart(InkstitchExtension): polygons.sort(key=lambda polygon: polygon.area, reverse=True) multipolygons = [] holes = [] + self.ensure_minimum_size(polygons, self.minimum_size) for polygon in polygons: if polygon in holes: continue diff --git a/lib/extensions/cleanup.py b/lib/extensions/cleanup.py index e06b4bea..f1965aba 100644 --- a/lib/extensions/cleanup.py +++ b/lib/extensions/cleanup.py @@ -1,6 +1,4 @@ -import sys - -from inkex import NSS +from inkex import NSS, Boolean, errormsg from ..elements import Fill, Stroke from ..i18n import _ @@ -10,10 +8,10 @@ from .base import InkstitchExtension class Cleanup(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-f", "--rm_fill", dest="rm_fill", type="inkbool", default=True) - self.OptionParser.add_option("-s", "--rm_stroke", dest="rm_stroke", type="inkbool", default=True) - self.OptionParser.add_option("-a", "--fill_threshold", dest="fill_threshold", type="int", default=20) - self.OptionParser.add_option("-l", "--stroke_threshold", dest="stroke_threshold", type="int", default=5) + self.arg_parser.add_argument("-f", "--rm_fill", dest="rm_fill", type=Boolean, default=True) + self.arg_parser.add_argument("-s", "--rm_stroke", dest="rm_stroke", type=Boolean, default=True) + self.arg_parser.add_argument("-a", "--fill_threshold", dest="fill_threshold", type=int, default=20) + self.arg_parser.add_argument("-l", "--stroke_threshold", dest="stroke_threshold", type=int, default=5) def effect(self): self.rm_fill = self.options.rm_fill @@ -21,8 +19,7 @@ class Cleanup(InkstitchExtension): self.fill_threshold = self.options.fill_threshold self.stroke_threshold = self.options.stroke_threshold - # Remove selection, we want every element in the document - self.selected = {} + self.svg.selected.clear() count = 0 svg = self.document.getroot() @@ -32,7 +29,7 @@ class Cleanup(InkstitchExtension): count += 1 if not self.get_elements(): - print >> sys.stderr, _("%s elements removed" % count) + errormsg(_("%s elements removed" % count)) return for element in self.elements: @@ -44,4 +41,4 @@ class Cleanup(InkstitchExtension): element.node.getparent().remove(element.node) count += 1 - print >> sys.stderr, _("%s elements removed" % count) + errormsg(_("%s elements removed" % count)) diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index 86e291fd..19b85e6d 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -1,3 +1,5 @@ +from inkex import Boolean + from .base import InkstitchExtension @@ -7,4 +9,4 @@ class CommandsExtension(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) for command in self.COMMANDS: - self.OptionParser.add_option("--%s" % command, type="inkbool") + self.arg_parser.add_argument("--%s" % command, type=Boolean) diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py index e2b287dd..048c08da 100644 --- a/lib/extensions/convert_to_satin.py +++ b/lib/extensions/convert_to_satin.py @@ -1,12 +1,13 @@ import math +import sys from itertools import chain, groupby +import inkex import numpy +from lxml import etree from numpy import diff, setdiff1d, sign from shapely import geometry as shgeo -import inkex - from ..elements import Stroke from ..i18n import _ from ..svg import PIXELS_PER_MM, get_correction_transform @@ -26,7 +27,7 @@ class ConvertToSatin(InkstitchExtension): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select at least one line to convert to a satin column.")) return @@ -120,8 +121,15 @@ class ConvertToSatin(InkstitchExtension): path = shgeo.LineString(path) - left_rail = path.parallel_offset(stroke_width / 2.0, 'left', **style_args) - right_rail = path.parallel_offset(stroke_width / 2.0, 'right', **style_args) + try: + left_rail = path.parallel_offset(stroke_width / 2.0, 'left', **style_args) + right_rail = path.parallel_offset(stroke_width / 2.0, 'right', **style_args) + except ValueError: + # TODO: fix this error automatically + # Error reference: https://github.com/inkstitch/inkstitch/issues/964 + inkex.errormsg(_("Ink/Stitch cannot convert your stroke into a satin column. " + "Please break up your path and try again.") + '\n') + sys.exit(1) if not isinstance(left_rail, shgeo.LineString) or \ not isinstance(right_rail, shgeo.LineString): @@ -304,12 +312,11 @@ class ConvertToSatin(InkstitchExtension): d += "%s,%s " % (x, y) d += " " - return inkex.etree.Element(SVG_PATH_TAG, - { - "id": self.uniqueId("path"), - "style": path_style, - "transform": correction_transform, - "d": d, - INKSTITCH_ATTRIBS['satin_column']: "true", - } - ) + return etree.Element(SVG_PATH_TAG, + { + "id": self.uniqueId("path"), + "style": path_style, + "transform": correction_transform, + "d": d, + INKSTITCH_ATTRIBS['satin_column']: "true", + }) diff --git a/lib/extensions/cut_satin.py b/lib/extensions/cut_satin.py index b776a68c..7cc80295 100644 --- a/lib/extensions/cut_satin.py +++ b/lib/extensions/cut_satin.py @@ -1,9 +1,9 @@ import inkex -from .base import InkstitchExtension -from ..i18n import _ from ..elements import SatinColumn +from ..i18n import _ from ..svg import get_correction_transform +from .base import InkstitchExtension class CutSatin(InkstitchExtension): @@ -11,7 +11,7 @@ class CutSatin(InkstitchExtension): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select one or more satin columns to cut.")) return diff --git a/lib/extensions/embroider.py b/lib/extensions/embroider.py deleted file mode 100644 index b9089612..00000000 --- a/lib/extensions/embroider.py +++ /dev/null @@ -1,87 +0,0 @@ -import os - -from ..i18n import _ -from ..output import write_embroidery_file -from ..stitch_plan import patches_to_stitch_plan -from ..svg import render_stitch_plan, PIXELS_PER_MM -from .base import InkstitchExtension - - -class Embroider(InkstitchExtension): - def __init__(self, *args, **kwargs): - InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-c", "--collapse_len_mm", - action="store", type="float", - dest="collapse_length_mm", default=3.0, - help="max collapse length (mm)") - self.OptionParser.add_option("--hide_layers", - action="store", type="choice", - choices=["true", "false"], - dest="hide_layers", default="true", - help="Hide all other layers when the embroidery layer is generated") - self.OptionParser.add_option("-O", "--output_format", - action="store", type="string", - dest="output_format", default="csv", - help="Output file extenstion (default: csv)") - self.OptionParser.add_option("-P", "--path", - action="store", type="string", - dest="path", default=".", - help="Directory in which to store output file") - self.OptionParser.add_option("-F", "--output-file", - action="store", type="string", - dest="output_file", - help="Output filename.") - self.OptionParser.add_option("-b", "--max-backups", - action="store", type="int", - dest="max_backups", default=5, - help="Max number of backups of output files to keep.") - self.OptionParser.usage += _("\n\nSeeing a 'no such option' message? Please restart Inkscape to fix.") - - def get_output_path(self): - if self.options.output_file: - # This is helpful for folks that run the embroider extension - # manually from the command line (without Inkscape) for - # debugging purposes. - output_path = os.path.join(os.path.expanduser(os.path.expandvars(self.options.path.decode("UTF-8"))), - self.options.output_file.decode("UTF-8")) - else: - csv_filename = '%s.%s' % (self.get_base_file_name(), self.options.output_format) - output_path = os.path.join(self.options.path.decode("UTF-8"), csv_filename) - - def add_suffix(path, suffix): - if suffix > 0: - path = "%s.%s" % (path, suffix) - - return path - - def move_if_exists(path, suffix=0): - source = add_suffix(path, suffix) - - if suffix >= self.options.max_backups: - return - - dest = add_suffix(path, suffix + 1) - - if os.path.exists(source): - move_if_exists(path, suffix + 1) - - if os.path.exists(dest): - os.remove(dest) - - os.rename(source, dest) - - move_if_exists(output_path) - - return output_path - - def effect(self): - if not self.get_elements(): - return - - if self.options.hide_layers: - self.hide_all_layers() - - patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) - write_embroidery_file(self.get_output_path(), stitch_plan, self.document.getroot()) - render_stitch_plan(self.document.getroot(), stitch_plan) diff --git a/lib/extensions/embroider_settings.py b/lib/extensions/embroider_settings.py new file mode 100644 index 00000000..88e2ba9b --- /dev/null +++ b/lib/extensions/embroider_settings.py @@ -0,0 +1,17 @@ +from .base import InkstitchExtension + + +class EmbroiderSettings(InkstitchExtension): + ''' + This saves embroider settings into the metadata of the file + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-c", "--collapse_len_mm", + action="store", type=float, + dest="collapse_length_mm", default=3.0, + help="max collapse length (mm)") + + def effect(self): + self.metadata = self.get_inkstitch_metadata() + self.metadata['collapse_len_mm'] = self.options.collapse_length_mm diff --git a/lib/extensions/flip.py b/lib/extensions/flip.py index 0864da85..87b8b3f0 100644 --- a/lib/extensions/flip.py +++ b/lib/extensions/flip.py @@ -1,9 +1,8 @@ import inkex -import cubicsuperpath -from .base import InkstitchExtension -from ..i18n import _ from ..elements import SatinColumn +from ..i18n import _ +from .base import InkstitchExtension class Flip(InkstitchExtension): @@ -14,13 +13,13 @@ class Flip(InkstitchExtension): first, second = satin.rail_indices csp[first], csp[second] = csp[second], csp[first] - satin.node.set("d", cubicsuperpath.formatPath(csp)) + satin.node.set("d", inkex.paths.CubicSuperPath.to_path(csp)) def effect(self): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select one or more satin columns to flip.")) return diff --git a/lib/extensions/import_threadlist.py b/lib/extensions/import_threadlist.py index d31c0d69..029043c2 100644 --- a/lib/extensions/import_threadlist.py +++ b/lib/extensions/import_threadlist.py @@ -12,20 +12,23 @@ from .base import InkstitchExtension class ImportThreadlist(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-f", "--filepath", type="str", default="", dest="filepath") - self.OptionParser.add_option("-m", "--method", type="int", default=1, dest="method") - self.OptionParser.add_option("-t", "--palette", type="str", default=None, dest="palette") + self.arg_parser.add_argument("-f", "--filepath", type=str, default="", dest="filepath") + self.arg_parser.add_argument("-m", "--method", type=int, default=1, dest="method") + self.arg_parser.add_argument("-t", "--palette", type=str, default=None, dest="palette") def effect(self): # Remove selection, we want all the elements in the document - self.selected = {} + self.svg.selected.clear() if not self.get_elements(): return path = self.options.filepath if not os.path.exists(path): - print >> sys.stderr, _("File not found.") + inkex.errormsg(_("File not found.")) + sys.exit(1) + if os.path.isdir(path): + inkex.errormsg(_("The filepath specified is not a file but a dictionary.\nPlease choose a threadlist file to import.")) sys.exit(1) method = self.options.method @@ -35,11 +38,11 @@ class ImportThreadlist(InkstitchExtension): colors = self.parse_threadlist_by_catalog_number(path) if all(c is None for c in colors): - print >>sys.stderr, _("Couldn't find any matching colors in the file.") + inkex.errormsg(_("Couldn't find any matching colors in the file.")) if method == 1: - print >>sys.stderr, _('Please try to import as "other threadlist" and specify a color palette below.') + inkex.errormsg(_('Please try to import as "other threadlist" and specify a color palette below.')) else: - print >>sys.stderr, _("Please chose an other color palette for your design.") + inkex.errormsg(_("Please chose an other color palette for your design.")) sys.exit(1) # Iterate through the color blocks to apply colors diff --git a/lib/extensions/input.py b/lib/extensions/input.py index 957d355c..c6dcb698 100644 --- a/lib/extensions/input.py +++ b/lib/extensions/input.py @@ -1,8 +1,9 @@ import os -import pyembroidery -from inkex import etree import inkex +from lxml import etree + +import pyembroidery from ..stitch_plan import StitchPlan from ..svg import PIXELS_PER_MM, render_stitch_plan @@ -10,7 +11,7 @@ from ..svg.tags import INKSCAPE_LABEL class Input(object): - def affect(self, args): + def run(self, args): embroidery_file = args[0] pattern = pyembroidery.read(embroidery_file) @@ -47,11 +48,11 @@ class Input(object): # rename the Stitch Plan layer so that it doesn't get overwritten by Embroider layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") - layer.set(INKSCAPE_LABEL, os.path.basename(embroidery_file.decode("UTF-8"))) + layer.set(INKSCAPE_LABEL, os.path.basename(embroidery_file)) layer.attrib.pop('id') # Shift the design so that its origin is at the center of the canvas # Note: this is NOT the same as centering the design in the canvas! layer.set('transform', 'translate(%s,%s)' % (extents[0], extents[1])) - print etree.tostring(svg) + print(etree.tostring(svg).decode('utf-8')) diff --git a/lib/extensions/layer_commands.py b/lib/extensions/layer_commands.py index e710e351..89726510 100644 --- a/lib/extensions/layer_commands.py +++ b/lib/extensions/layer_commands.py @@ -1,9 +1,10 @@ import inkex +from lxml import etree -from ..commands import LAYER_COMMANDS, get_command_description, ensure_symbol +from ..commands import LAYER_COMMANDS, ensure_symbol, get_command_description from ..i18n import _ from ..svg import get_correction_transform -from ..svg.tags import SVG_USE_TAG, INKSCAPE_LABEL, XLINK_HREF +from ..svg.tags import INKSCAPE_LABEL, SVG_USE_TAG, XLINK_HREF from .commands import CommandsExtension @@ -17,20 +18,19 @@ class LayerCommands(CommandsExtension): inkex.errormsg(_("Please choose one or more commands to add.")) return - self.ensure_current_layer() - correction_transform = get_correction_transform(self.current_layer, child=True) + correction_transform = get_correction_transform(self.svg.get_current_layer(), child=True) for i, command in enumerate(commands): ensure_symbol(self.document, command) - inkex.etree.SubElement(self.current_layer, SVG_USE_TAG, - { - "id": self.uniqueId("use"), - INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), - XLINK_HREF: "#inkstitch_%s" % command, - "height": "100%", - "width": "100%", - "x": str(i * 20), - "y": "-10", - "transform": correction_transform - }) + etree.SubElement(self.svg.get_current_layer(), SVG_USE_TAG, + { + "id": self.uniqueId("use"), + INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": str(i * 20), + "y": "-10", + "transform": correction_transform + }) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index d988778d..ee0dd9a0 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -1,14 +1,12 @@ -# -*- coding: UTF-8 -*- - import json import os import sys -from base64 import b64decode, b64encode import appdirs import inkex import wx import wx.adv +from lxml import etree from ..elements import nodes_to_elements from ..gui import PresetsPanel, SimulatorPreview, info_dialog @@ -19,6 +17,7 @@ from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_LETTERING, SVG_GROUP_TAG, SVG_PATH_TAG) from ..utils import DotDict, cache, get_bundled_dir from .commands import CommandsExtension +from .lettering_custom_font_dir import get_custom_font_dir class LetteringFrame(wx.Frame): @@ -45,6 +44,7 @@ class LetteringFrame(wx.Frame): # font details self.font_description = wx.StaticText(self, wx.ID_ANY) + self.Bind(wx.EVT_SIZE, self.resize) # options self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options")) @@ -80,7 +80,7 @@ class LetteringFrame(wx.Frame): """Load the settings saved into the SVG group element""" self.settings = DotDict({ - "text": u"", + "text": "", "back_and_forth": False, "font": None, "scale": 100 @@ -88,7 +88,7 @@ class LetteringFrame(wx.Frame): try: if INKSTITCH_LETTERING in self.group.attrib: - self.settings.update(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) + self.settings.update(json.loads(self.group.get(INKSTITCH_LETTERING))) return except (TypeError, ValueError): pass @@ -103,22 +103,14 @@ class LetteringFrame(wx.Frame): def save_settings(self): """Save the settings into the SVG group element.""" - - # We base64 encode the string before storing it in an XML attribute. - # In theory, lxml should properly html-encode the string, using HTML - # entities like as necessary. However, we've found that Inkscape - # incorrectly interpolates the HTML entities upon reading the - # extension's output, rather than leaving them as is. - # - # Details: - # https://bugs.launchpad.net/inkscape/+bug/1804346 - self.group.set(INKSTITCH_LETTERING, b64encode(json.dumps(self.settings))) + self.group.set(INKSTITCH_LETTERING, json.dumps(self.settings)) def update_font_list(self): font_paths = { get_bundled_dir("fonts"), os.path.expanduser("~/.inkstitch/fonts"), os.path.join(appdirs.user_config_dir('inkstitch'), 'fonts'), + get_custom_font_dir() } self.fonts = {} @@ -130,13 +122,12 @@ class LetteringFrame(wx.Frame): except OSError: continue - try: - for font_dir in font_dirs: - font = Font(os.path.join(font_path, font_dir)) - self.fonts[font.name] = font - self.fonts_by_id[font.id] = font - except FontError: - pass + for font_dir in font_dirs: + font = Font(os.path.join(font_path, font_dir)) + if font.name == "" or font.id == "": + continue + self.fonts[font.name] = font + self.fonts_by_id[font.id] = font if len(self.fonts) == 0: info_dialog(self, _("Unable to find any fonts! Please try reinstalling Ink/Stitch.")) @@ -165,13 +156,13 @@ class LetteringFrame(wx.Frame): self.font_chooser.Append(font.name) def get_font_names(self): - font_names = [font.name for font in self.fonts.itervalues()] + font_names = [font.name for font in self.fonts.values()] font_names.sort() return font_names def get_font_descriptions(self): - return {font.name: font.description for font in self.fonts.itervalues()} + return {font.name: font.description for font in self.fonts.values()} def set_initial_font(self, font_id): if font_id: @@ -191,7 +182,7 @@ class LetteringFrame(wx.Frame): try: return self.fonts_by_id[self.DEFAULT_FONT] except KeyError: - return self.fonts.values()[0] + return list(self.fonts.values())[0] def on_change(self, attribute, event): self.settings[attribute] = event.GetEventObject().GetValue() @@ -202,7 +193,11 @@ class LetteringFrame(wx.Frame): self.settings.font = font.id self.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100)) - font_variants = font.has_variants() + font_variants = [] + try: + font_variants = font.has_variants() + except FontError: + pass # Update font description color = (0, 0, 0) @@ -212,7 +207,7 @@ class LetteringFrame(wx.Frame): description = _('This font has no available font variant. Please update or remove the font.') self.font_description.SetLabel(description) self.font_description.SetForegroundColour(color) - self.font_description.Wrap(self.GetSize().width - 20) + self.font_description.Wrap(self.GetSize().width - 35) if font.reversible: self.back_and_forth_checkbox.Enable() @@ -230,18 +225,24 @@ class LetteringFrame(wx.Frame): self.trim_checkbox.SetValue(False) self.update_preview() - self.GetSizer().Layout() + self.Layout() + + def resize(self, event=None): + description = self.font_description.GetLabel().replace("\n", " ") + self.font_description.SetLabel(description) + self.font_description.Wrap(self.GetSize().width - 35) + self.Layout() def update_preview(self, event=None): self.preview.update() - def update_lettering(self): + def update_lettering(self, raise_error=False): del self.group[:] if self.settings.scale == 100: destination_group = self.group else: - destination_group = inkex.etree.SubElement(self.group, SVG_GROUP_TAG, { + destination_group = etree.SubElement(self.group, SVG_GROUP_TAG, { # L10N The user has chosen to scale the text by some percentage # (50%, 200%, etc). If you need to use the percentage symbol, # make sure to double it (%%). @@ -249,7 +250,14 @@ class LetteringFrame(wx.Frame): }) font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) - font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + try: + font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + except FontError as e: + if raise_error: + inkex.errormsg("Error: Text cannot be applied to the document.\n%s" % e) + return + else: + pass if self.settings.scale != 100: destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) @@ -295,7 +303,7 @@ class LetteringFrame(wx.Frame): def apply(self, event): self.preview.disable() - self.update_lettering() + self.update_lettering(True) self.save_settings() self.close() @@ -369,10 +377,10 @@ class Lettering(CommandsExtension): self.cancelled = True def get_or_create_group(self): - if self.selected: + if self.svg.selected: groups = set() - for node in self.selected.itervalues(): + for node in self.svg.selected.values(): if node.tag == SVG_GROUP_TAG and INKSTITCH_LETTERING in node.attrib: groups.add(node) @@ -391,9 +399,9 @@ class Lettering(CommandsExtension): return list(groups)[0] else: self.ensure_current_layer() - return inkex.etree.SubElement(self.current_layer, SVG_GROUP_TAG, { + return etree.SubElement(self.svg.get_current_layer(), SVG_GROUP_TAG, { INKSCAPE_LABEL: _("Ink/Stitch Lettering"), - "transform": get_correction_transform(self.current_layer, child=True) + "transform": get_correction_transform(self.svg.get_current_layer(), child=True) }) def effect(self): @@ -405,7 +413,7 @@ class Lettering(CommandsExtension): display = wx.Display(current_screen) display_size = display.GetClientArea() frame_size = frame.GetSize() - frame.SetPosition((display_size[0], display_size[3] / 2 - frame_size[1] / 2)) + frame.SetPosition((int(display_size[0]), int(display_size[3] / 2 - frame_size[1] / 2))) frame.Show() app.MainLoop() diff --git a/lib/extensions/lettering_custom_font_dir.py b/lib/extensions/lettering_custom_font_dir.py new file mode 100644 index 00000000..0103c7d6 --- /dev/null +++ b/lib/extensions/lettering_custom_font_dir.py @@ -0,0 +1,48 @@ +import json +import os + +import appdirs +from inkex import errormsg + +from ..i18n import _ +from .base import InkstitchExtension + + +class LetteringCustomFontDir(InkstitchExtension): + ''' + This extension will create a json file to store a custom directory path for additional user fonts + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-d", "--path", type=str, default="", dest="path") + + def effect(self): + path = self.options.path + if not os.path.isdir(path): + errormsg(_("Please specify the directory of your custom fonts.")) + return + + data = {'custom_font_dir': '%s' % path} + + try: + config_path = appdirs.user_config_dir('inkstitch') + except ImportError: + config_path = os.path.expanduser('~/.inkstitch') + config_path = os.path.join(config_path, 'custom_dirs.json') + + with open(config_path, 'w', encoding="utf8") as font_data: + json.dump(data, font_data, indent=4, ensure_ascii=False) + + +def get_custom_font_dir(): + custom_font_dir_path = os.path.join(appdirs.user_config_dir('inkstitch'), 'custom_dirs.json') + try: + with open(custom_font_dir_path, 'r') as custom_dirs: + custom_dir = json.load(custom_dirs) + except (IOError, ValueError): + return "" + try: + return custom_dir['custom_font_dir'] + except KeyError: + pass + return "" diff --git a/lib/extensions/lettering_generate_json.py b/lib/extensions/lettering_generate_json.py new file mode 100644 index 00000000..9b44c367 --- /dev/null +++ b/lib/extensions/lettering_generate_json.py @@ -0,0 +1,76 @@ +import json +import os +import sys + +from inkex import Boolean + +from ..i18n import _ +from ..lettering.kerning import FontKerning +from .base import InkstitchExtension + + +class LetteringGenerateJson(InkstitchExtension): + ''' + This extension helps font creators to generate the json file for the lettering tool + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-n", "--font-name", type=str, default="Font", dest="font_name") + self.arg_parser.add_argument("-d", "--font-description", type=str, default="Description", dest="font_description") + self.arg_parser.add_argument("-s", "--auto-satin", type=Boolean, default="true", dest="auto_satin") + self.arg_parser.add_argument("-r", "--reversible", type=Boolean, default="true", dest="reversible") + self.arg_parser.add_argument("-g", "--default-glyph", type=str, default="", dest="default_glyph") + self.arg_parser.add_argument("-i", "--min-scale", type=float, default=1, dest="min_scale") + self.arg_parser.add_argument("-a", "--max-scale", type=float, default=1, dest="max_scale") + self.arg_parser.add_argument("-l", "--leading", type=int, default=0, dest="leading") + self.arg_parser.add_argument("-p", "--font-file", type=str, default="", dest="path") + + def effect(self): + # file paths + path = self.options.path + if not os.path.isfile(path): + print(_("Please specify a font file."), file=sys.stderr) + return + output_path = os.path.join(os.path.dirname(path), 'font.json') + + # kerning + kerning = FontKerning(path) + + horiz_adv_x = kerning.horiz_adv_x() + hkern = kerning.hkern() + word_spacing = kerning.word_spacing() + letter_spacing = kerning.letter_spacing() + units_per_em = kerning.units_per_em() + # missing_glyph_spacing = kerning.missing_glyph_spacing() + + # if letter spacing returns 0, it hasn't been specified in the font file + # Ink/Stitch will calculate the width of each letter automatically + if letter_spacing == 0: + letter_spacing = None + + # if leading (line height) is set to 0, the font author wants Ink/Stitch to use units_per_em + # if units_per_em is not defined in the font file a default value will be returned + if self.options.leading == 0: + leading = units_per_em + else: + leading = self.options.leading + + # collect data + data = {'name': self.options.font_name, + 'description': self.options.font_description, + 'leading': leading, + 'auto_satin': self.options.auto_satin, + 'reversible': self.options.reversible, + 'default_glyph': self.options.default_glyph, + 'min_scale': self.options.min_scale, + 'max_scale': self.options.max_scale, + 'horiz_adv_x_default': letter_spacing, + 'horiz_adv_x_space': word_spacing, + 'units_per_em': units_per_em, + 'horiz_adv_x': horiz_adv_x, + 'kerning_pairs': hkern + } + + # write data to font.json into the same directory as the font file + with open(output_path, 'w', encoding="utf8") as font_data: + json.dump(data, font_data, indent=4, ensure_ascii=False) diff --git a/lib/extensions/lettering_remove_kerning.py b/lib/extensions/lettering_remove_kerning.py new file mode 100644 index 00000000..aec8717e --- /dev/null +++ b/lib/extensions/lettering_remove_kerning.py @@ -0,0 +1,30 @@ +import os + +from inkex import NSS +from lxml import etree + +from .base import InkstitchExtension + + +class LetteringRemoveKerning(InkstitchExtension): + ''' + This extension helps font creators to generate the json file for the lettering tool + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-p", "--font-files", type=str, default="", dest="paths") + + def effect(self): + # file paths + paths = self.options.paths.split("|") + for path in paths: + if not os.path.isfile(path): + continue + with open(path, 'r+', encoding="utf-8") as fontfile: + svg = etree.parse(fontfile) + xpath = ".//svg:font[1]" + kerning = svg.xpath(xpath, namespaces=NSS)[0] + kerning.getparent().remove(kerning) + fontfile.seek(0) + fontfile.write(etree.tostring(svg).decode('utf-8')) + fontfile.truncate() diff --git a/lib/extensions/object_commands.py b/lib/extensions/object_commands.py index d33ab2ba..f1c2fb46 100644 --- a/lib/extensions/object_commands.py +++ b/lib/extensions/object_commands.py @@ -12,7 +12,7 @@ class ObjectCommands(CommandsExtension): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select one or more objects to which to attach commands.")) return diff --git a/lib/extensions/output.py b/lib/extensions/output.py index ccf4d7cb..52e9d3a9 100644 --- a/lib/extensions/output.py +++ b/lib/extensions/output.py @@ -11,7 +11,7 @@ class Output(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - def getoptions(self, args=sys.argv[1:]): + def parse_arguments(self, args=sys.argv[1:]): # inkex's option parsing can't handle arbitrary command line arguments # that may be passed for a given output format, so we'll just parse the # args ourselves. :P @@ -39,14 +39,16 @@ class Output(InkstitchExtension): self.file_extension = self.settings.pop('format') del sys.argv[1:] - InkstitchExtension.getoptions(self, extra_args) + InkstitchExtension.parse_arguments(self, extra_args) def effect(self): if not self.get_elements(): return + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches, disable_ties=self.settings.get('laser_mode', False)) + stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len, disable_ties=self.settings.get('laser_mode', False)) temp_file = tempfile.NamedTemporaryFile(suffix=".%s" % self.file_extension, delete=False) @@ -62,7 +64,7 @@ class Output(InkstitchExtension): # 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.write(output_file.read()) + sys.stdout.buffer.write(output_file.read()) sys.stdout.flush() # clean up the temp file diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 600a4669..910941fe 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -151,7 +151,7 @@ class ParamsTab(ScrolledPanel): # because they're grayed out anyway. return values - for name, input in self.param_inputs.iteritems(): + for name, input in self.param_inputs.items(): if input in self.changed_inputs and input != self.toggle_checkbox: values[name] = input.GetValue() @@ -161,7 +161,7 @@ class ParamsTab(ScrolledPanel): values = self.get_values() for node in self.nodes: # print >> sys.stderr, "apply: ", self.name, node.id, values - for name, value in values.iteritems(): + for name, value in values.items(): node.set_param(name, value) def on_change(self, callable): @@ -181,7 +181,7 @@ class ParamsTab(ScrolledPanel): def load_preset(self, preset): preset_data = preset.get(self.name, {}) - for name, value in preset_data.iteritems(): + for name, value in preset_data.items(): if name in self.param_inputs: self.param_inputs[name].SetValue(value) self.changed_inputs.add(self.param_inputs[name]) @@ -198,16 +198,16 @@ class ParamsTab(ScrolledPanel): description = _("These settings will be applied to %d objects.") % len(self.nodes) if any(len(param.values) > 1 for param in self.params): - description += u"\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") + description += "\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") if self.dependent_tabs: if len(self.dependent_tabs) == 1: - description += u"\n • " + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs) + description += "\n • " + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs) else: - description += u"\n • " + _("Disabling this tab will disable the following tab.") + description += "\n • " + _("Disabling this tab will disable the following tab.") if self.paired_tab: - description += u"\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name + description += "\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name self.description_text = description @@ -285,7 +285,7 @@ class ParamsTab(ScrolledPanel): self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) self.settings_grid.Add(wx.StaticText(self, label=param.unit or ""), proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) - self.inputs_to_params = {v: k for k, v in self.param_inputs.iteritems()} + self.inputs_to_params = {v: k for k, v in self.param_inputs.items()} box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) self.SetSizer(box) @@ -443,9 +443,9 @@ class SettingsFrame(wx.Frame): self.notebook.AddPage(tab, tab.name) sizer_1.Add(self.notebook, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) sizer_1.Add(self.presets_panel, 0, flag=wx.EXPAND | wx.ALL, border=10) - sizer_3.Add(self.cancel_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 5) - sizer_3.Add(self.use_last_button, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, 5) - sizer_3.Add(self.apply_button, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, 5) + sizer_3.Add(self.cancel_button, 0, wx.RIGHT, 5) + sizer_3.Add(self.use_last_button, 0, wx.RIGHT | wx.BOTTOM, 5) + sizer_3.Add(self.apply_button, 0, wx.RIGHT | wx.BOTTOM, 5) sizer_1.Add(sizer_3, 0, wx.ALIGN_RIGHT, 0) self.SetSizer(sizer_1) sizer_1.Fit(self) @@ -491,7 +491,7 @@ class Params(InkstitchExtension): element.order = z nodes_by_class[cls].append(element) - return sorted(nodes_by_class.items(), key=lambda (cls, nodes): cls.__name__) + return sorted(list(nodes_by_class.items()), key=lambda cls_nodes: cls_nodes[0].__name__) def get_values(self, param, nodes): getter = 'get_param' @@ -501,17 +501,16 @@ class Params(InkstitchExtension): else: getter = 'get_param' - values = filter(lambda item: item is not None, - (getattr(node, getter)(param.name, param.default) for node in nodes)) + values = [item for item in (getattr(node, getter)(param.name, param.default) for node in nodes) if item is not None] return values def group_params(self, params): def by_group_and_sort_index(param): - return param.group, param.sort_index + return param.group or "", param.sort_index def by_group(param): - return param.group + return param.group or "" return groupby(sorted(params, key=by_group_and_sort_index), by_group) @@ -526,7 +525,7 @@ class Params(InkstitchExtension): # If multiple tabs are enabled, make sure dependent # tabs are grouped with the parent. - parent, + parent and parent.name, # Within parent/dependents, put the parent first. tab == parent @@ -565,12 +564,13 @@ class Params(InkstitchExtension): parent_tab = None new_tabs = [] + for group, params in self.group_params(params): tab_name = group or cls.element_name tab = ParamsTab(parent, id=wx.ID_ANY, name=tab_name, params=list(params), nodes=nodes) new_tabs.append(tab) - if group is None: + if group == "": parent_tab = tab self.assign_parents(new_tabs, parent_tab) @@ -594,7 +594,7 @@ class Params(InkstitchExtension): display = wx.Display(current_screen) display_size = display.GetClientArea() frame_size = frame.GetSize() - frame.SetPosition((display_size[0], display_size[3] / 2 - frame_size[1] / 2)) + frame.SetPosition((int(display_size[0]), int(display_size[3]/2 - frame_size[1]/2))) frame.Show() app.MainLoop() diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py index 8af07cf7..1218b5e9 100644 --- a/lib/extensions/print_pdf.py +++ b/lib/extensions/print_pdf.py @@ -1,19 +1,19 @@ -from copy import deepcopy -from datetime import date import errno import json import logging import os import socket import sys -from threading import Thread import time +from copy import deepcopy +from datetime import date +from threading import Thread import appdirs -from flask import Flask, request, Response, send_from_directory, jsonify -import inkex -from jinja2 import Environment, FileSystemLoader, select_autoescape import requests +from flask import Flask, Response, jsonify, request, send_from_directory +from jinja2 import Environment, FileSystemLoader, select_autoescape +from lxml import etree from ..gui import open_url from ..i18n import translation as inkstitch_translation @@ -72,6 +72,11 @@ class PrintPreviewServer(Thread): def __setup_app(self): # noqa: C901 self.__set_resources_path() + + # Disable warning about using a development server in a production environment + cli = sys.modules['flask.cli'] + cli.show_server_banner = lambda *x: None + self.app = Flask(__name__) @self.app.route('/') @@ -211,13 +216,21 @@ class Print(InkstitchExtension): layers = svg.findall("./g[@%s='layer']" % INKSCAPE_GROUPMODE) stitch_plan_layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + # Make sure there is no leftover translation from stitch plan preview + stitch_plan_layer.pop('transform') + + # objects outside of the viewbox are invisible + # TODO: if we want them to be seen, we need to redefine document size to fit the design + # this is just a quick fix and doesn't work on realistic view + svg.set('style', 'overflow:visible;') + # First, delete all of the other layers. We don't need them and they'll # just bulk up the SVG. for layer in layers: if layer is not stitch_plan_layer: svg.remove(layer) - overview_svg = inkex.etree.tostring(svg) + overview_svg = etree.tostring(svg).decode('utf-8') color_block_groups = stitch_plan_layer.getchildren() color_block_svgs = [] @@ -229,7 +242,7 @@ class Print(InkstitchExtension): stitch_plan_layer.append(group) # save an SVG preview - color_block_svgs.append(inkex.etree.tostring(svg)) + color_block_svgs.append(etree.tostring(svg).decode('utf-8')) return overview_svg, color_block_svgs @@ -269,13 +282,15 @@ class Print(InkstitchExtension): # objects. It's almost certain they meant to print the whole design. # If they really wanted to print just a few objects, they could set # the rest invisible temporarily. - self.selected = {} + self.svg.selected.clear() if not self.get_elements(): return + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches) + stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len) palette = ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) overview_svg, color_block_svgs = self.render_svgs(stitch_plan, realistic=False) diff --git a/lib/extensions/remove_embroidery_settings.py b/lib/extensions/remove_embroidery_settings.py index 2a4d06dd..6ccdb703 100644 --- a/lib/extensions/remove_embroidery_settings.py +++ b/lib/extensions/remove_embroidery_settings.py @@ -1,4 +1,4 @@ -import inkex +from inkex import NSS, Boolean from ..commands import find_commands from ..svg.svg import find_elements @@ -8,9 +8,9 @@ from .base import InkstitchExtension class RemoveEmbroiderySettings(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-p", "--del_params", dest="del_params", type="inkbool", default=True) - self.OptionParser.add_option("-c", "--del_commands", dest="del_commands", type="inkbool", default=False) - self.OptionParser.add_option("-d", "--del_print", dest="del_print", type="inkbool", default=False) + self.arg_parser.add_argument("-p", "--del_params", dest="del_params", type=Boolean, default=True) + self.arg_parser.add_argument("-c", "--del_commands", dest="del_commands", type=Boolean, default=False) + self.arg_parser.add_argument("-d", "--del_print", dest="del_print", type=Boolean, default=False) def effect(self): self.svg = self.document.getroot() @@ -30,24 +30,24 @@ class RemoveEmbroiderySettings(InkstitchExtension): self.remove_element(print_setting) def remove_params(self): - if not self.selected: + if not self.svg.selected: xpath = ".//svg:path" elements = find_elements(self.svg, xpath) self.remove_inkstitch_attributes(elements) else: - for node in self.selected: + for node in self.svg.selected: elements = self.get_selected_elements(node) self.remove_inkstitch_attributes(elements) def remove_commands(self): - if not self.selected: + if not self.svg.selected: # we are not able to grab commands by a specific id # so let's move through every object instead and see if it has a command xpath = ".//svg:path|.//svg:circle|.//svg:rect|.//svg:ellipse" elements = find_elements(self.svg, xpath) else: elements = [] - for node in self.selected: + for node in self.svg.selected: elements.extend(self.get_selected_elements(node)) if elements: @@ -56,7 +56,7 @@ class RemoveEmbroiderySettings(InkstitchExtension): group = command.connector.getparent() group.getparent().remove(group) - if not self.selected: + if not self.svg.selected: # remove standalone commands standalone_commands = ".//svg:use[starts-with(@xlink:href, '#inkstitch_')]" self.remove_elements(standalone_commands) @@ -84,5 +84,5 @@ class RemoveEmbroiderySettings(InkstitchExtension): def remove_inkstitch_attributes(self, elements): for element in elements: for attrib in element.attrib: - if attrib.startswith(inkex.NSS['inkstitch'], 1): + if attrib.startswith(NSS['inkstitch'], 1): del element.attrib[attrib] diff --git a/lib/extensions/reorder.py b/lib/extensions/reorder.py new file mode 100644 index 00000000..4db02760 --- /dev/null +++ b/lib/extensions/reorder.py @@ -0,0 +1,36 @@ +import inkex + +from .base import InkstitchExtension + + +class Reorder(InkstitchExtension): + # Remove selected objects from the document and readd them in the order they + # were selected. + + def get_selected_in_order(self): + selected = [] + + for i in self.options.ids: + path = '//*[@id="%s"]' % i + for node in self.document.xpath(path, namespaces=inkex.NSS): + selected.append(node) + + return selected + + def effect(self): + objects = self.get_selected_in_order() + + for obj in objects[1:]: + obj.getparent().remove(obj) + + insert_parent = objects[0].getparent() + insert_pos = insert_parent.index(objects[0]) + + insert_parent.remove(objects[0]) + + insert_parent[insert_pos:insert_pos] = objects + + +if __name__ == '__main__': + e = Reorder() + e.affect() diff --git a/lib/extensions/stitch_plan_preview.py b/lib/extensions/stitch_plan_preview.py index b89e24a7..e1c398b5 100644 --- a/lib/extensions/stitch_plan_preview.py +++ b/lib/extensions/stitch_plan_preview.py @@ -4,7 +4,6 @@ from .base import InkstitchExtension class StitchPlanPreview(InkstitchExtension): - def effect(self): # delete old stitch plan svg = self.document.getroot() @@ -15,9 +14,13 @@ class StitchPlanPreview(InkstitchExtension): # create new stitch plan if not self.get_elements(): return + + realistic = False + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches) - render_stitch_plan(svg, stitch_plan) + stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len) + render_stitch_plan(svg, stitch_plan, realistic) # translate stitch plan to the right side of the canvas layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") diff --git a/lib/extensions/troubleshoot.py b/lib/extensions/troubleshoot.py index 6b63390a..bd352d8f 100644 --- a/lib/extensions/troubleshoot.py +++ b/lib/extensions/troubleshoot.py @@ -1,12 +1,13 @@ import textwrap -import inkex +from inkex import errormsg +from lxml import etree from ..commands import add_layer_commands from ..elements.validation import (ObjectTypeWarning, ValidationError, ValidationWarning) from ..i18n import _ -from ..svg import get_correction_transform +from ..svg.path import get_correction_transform from ..svg.tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SODIPODI_ROLE, SVG_GROUP_TAG, SVG_PATH_TAG, SVG_TEXT_TAG, SVG_TSPAN_TAG) @@ -43,7 +44,7 @@ class Troubleshoot(InkstitchExtension): message += "\n\n" message += _("If you are still having trouble with a shape not being embroidered, " "check if it is in a layer with an ignore command.") - inkex.errormsg(message) + errormsg(message) def insert_pointer(self, problem): correction_transform = get_correction_transform(self.troubleshoot_layer) @@ -61,7 +62,7 @@ class Troubleshoot(InkstitchExtension): pointer_style = "stroke:#000000;stroke-width:0.2;fill:%s;" % (fill_color) text_style = "fill:%s;stroke:#000000;stroke-width:0.2;font-size:8px;text-align:center;text-anchor:middle" % (fill_color) - path = inkex.etree.Element( + path = etree.Element( SVG_PATH_TAG, { "id": self.uniqueId("inkstitch__invalid_pointer__"), @@ -73,7 +74,7 @@ class Troubleshoot(InkstitchExtension): ) layer.insert(0, path) - text = inkex.etree.Element( + text = etree.Element( SVG_TEXT_TAG, { INKSCAPE_LABEL: _('Description'), @@ -85,7 +86,7 @@ class Troubleshoot(InkstitchExtension): ) layer.append(text) - tspan = inkex.etree.Element(SVG_TSPAN_TAG) + tspan = etree.Element(SVG_TSPAN_TAG) tspan.text = problem.name text.append(tspan) @@ -94,7 +95,7 @@ class Troubleshoot(InkstitchExtension): layer = svg.find(".//*[@id='__validation_layer__']") if layer is None: - layer = inkex.etree.Element( + layer = etree.Element( SVG_GROUP_TAG, { 'id': '__validation_layer__', @@ -109,7 +110,7 @@ class Troubleshoot(InkstitchExtension): add_layer_commands(layer, ["ignore_layer"]) - error_group = inkex.etree.SubElement( + error_group = etree.SubElement( layer, SVG_GROUP_TAG, { @@ -118,7 +119,7 @@ class Troubleshoot(InkstitchExtension): }) layer.append(error_group) - warning_group = inkex.etree.SubElement( + warning_group = etree.SubElement( layer, SVG_GROUP_TAG, { @@ -127,7 +128,7 @@ class Troubleshoot(InkstitchExtension): }) layer.append(warning_group) - type_warning_group = inkex.etree.SubElement( + type_warning_group = etree.SubElement( layer, SVG_GROUP_TAG, { @@ -145,7 +146,7 @@ class Troubleshoot(InkstitchExtension): svg = self.document.getroot() text_x = str(float(svg.get('viewBox', '0 0 800 0').split(' ')[2]) + 5.0) - text_container = inkex.etree.Element( + text_container = etree.Element( SVG_TEXT_TAG, { "x": text_x, @@ -160,7 +161,7 @@ class Troubleshoot(InkstitchExtension): ["", ""] ] - for problem_type, problems in problem_types.items(): + for problem_type, problems in list(problem_types.items()): if problem_type == "error": text_color = "#ff0000" problem_type_header = _("Errors") @@ -202,7 +203,7 @@ class Troubleshoot(InkstitchExtension): text = self.split_text(text) for text_line in text: - tspan = inkex.etree.Element( + tspan = etree.Element( SVG_TSPAN_TAG, { SODIPODI_ROLE: "line", diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py index aebff331..fedf8c65 100644 --- a/lib/extensions/zip.py +++ b/lib/extensions/zip.py @@ -1,36 +1,34 @@ import os import sys import tempfile +from copy import deepcopy from zipfile import ZipFile -import inkex +from inkex import Boolean +from lxml import etree + import pyembroidery from ..i18n import _ from ..output import write_embroidery_file from ..stitch_plan import patches_to_stitch_plan -from ..svg import PIXELS_PER_MM -from .base import InkstitchExtension from ..threads import ThreadCatalog +from .base import InkstitchExtension class Zip(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self) - self.OptionParser.add_option("-c", "--collapse_len_mm", - action="store", type="float", - dest="collapse_length_mm", default=3.0, - help="max collapse length (mm)") # it's kind of obnoxious that I have to do this... self.formats = [] for format in pyembroidery.supported_formats(): if 'writer' in format and format['category'] == 'embroidery': extension = format['extension'] - self.OptionParser.add_option('--format-%s' % extension, type="inkbool", dest=extension) + self.arg_parser.add_argument('--format-%s' % extension, type=Boolean, dest=extension) self.formats.append(extension) - self.OptionParser.add_option('--format-svg', type="inkbool", dest='svg') - self.OptionParser.add_option('--format-threadlist', type="inkbool", dest='threadlist') + self.arg_parser.add_argument('--format-svg', type=Boolean, dest='svg') + self.arg_parser.add_argument('--format-threadlist', type=Boolean, dest='threadlist') self.formats.append('svg') self.formats.append('threadlist') @@ -38,8 +36,10 @@ class Zip(InkstitchExtension): if not self.get_elements(): return + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) + stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len) base_file_name = self.get_base_file_name() path = tempfile.mkdtemp() @@ -50,10 +50,10 @@ class Zip(InkstitchExtension): if getattr(self.options, format): output_file = os.path.join(path, "%s.%s" % (base_file_name, format)) if format == 'svg': - output = open(output_file, 'w') - output.write(inkex.etree.tostring(self.document.getroot())) - output.close() - if format == 'threadlist': + 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') output.write(self.get_threadlist(stitch_plan, base_file_name)) @@ -76,8 +76,8 @@ class Zip(InkstitchExtension): # inkscape will read the file contents from stdout and copy # to the destination file that the user chose - with open(temp_file.name) as output_file: - sys.stdout.write(output_file.read()) + with open(temp_file.name, 'rb') as output_file: + sys.stdout.buffer.write(output_file.read()) os.remove(temp_file.name) for file in files: |
