summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2021-03-04 18:40:53 +0100
committerGitHub <noreply@github.com>2021-03-04 18:40:53 +0100
commite84a86d4ac0caf29d6074728376ff0a594243fec (patch)
tree888c79ed0094ba2916a1d329861a85515959913c /lib
parentb39575a50191307b3b56eab6455626398eec6397 (diff)
Update for Inkscape 1.0 (#880)
* update for inkscape 1.0 * add about extension * Build improvements for the inkscape1.0 branch (#985) * zip: export real svg not stitch plan * #411 and #726 * Tools for Font Creators (#1018) * ignore very small holes in fills * remove embroider (#1026) * auto_fill: ignore shrink_or_grow if result is empty (#589) * break apart: do not ignore small fills Co-authored-by: Hagen Fritsch <rumpeltux-github@irgendwo.org> Co-authored-by: Lex Neva <github.com@lexneva.name>
Diffstat (limited to 'lib')
-rw-r--r--lib/api/install.py2
-rw-r--r--lib/api/server.py9
-rw-r--r--lib/api/stitch_plan.py4
-rw-r--r--lib/commands.py88
-rw-r--r--lib/debug.py37
-rw-r--r--lib/elements/__init__.py22
-rw-r--r--lib/elements/auto_fill.py28
-rw-r--r--lib/elements/clone.py75
-rw-r--r--lib/elements/element.py56
-rw-r--r--lib/elements/fill.py6
-rw-r--r--lib/elements/image.py14
-rw-r--r--lib/elements/polyline.py7
-rw-r--r--lib/elements/satin_column.py47
-rw-r--r--lib/elements/stroke.py10
-rw-r--r--lib/elements/svg_objects.py71
-rw-r--r--lib/elements/text.py14
-rw-r--r--lib/extensions/__init__.py57
-rw-r--r--lib/extensions/auto_satin.py4
-rw-r--r--lib/extensions/base.py45
-rw-r--r--lib/extensions/break_apart.py12
-rw-r--r--lib/extensions/cleanup.py19
-rw-r--r--lib/extensions/commands.py4
-rw-r--r--lib/extensions/convert_to_satin.py35
-rw-r--r--lib/extensions/cut_satin.py6
-rw-r--r--lib/extensions/embroider.py87
-rw-r--r--lib/extensions/embroider_settings.py17
-rw-r--r--lib/extensions/flip.py9
-rw-r--r--lib/extensions/import_threadlist.py19
-rw-r--r--lib/extensions/input.py11
-rw-r--r--lib/extensions/layer_commands.py30
-rw-r--r--lib/extensions/lettering.py82
-rw-r--r--lib/extensions/lettering_custom_font_dir.py48
-rw-r--r--lib/extensions/lettering_generate_json.py76
-rw-r--r--lib/extensions/lettering_remove_kerning.py30
-rw-r--r--lib/extensions/object_commands.py2
-rw-r--r--lib/extensions/output.py10
-rw-r--r--lib/extensions/params.py38
-rw-r--r--lib/extensions/print_pdf.py35
-rw-r--r--lib/extensions/remove_embroidery_settings.py20
-rw-r--r--lib/extensions/reorder.py36
-rw-r--r--lib/extensions/stitch_plan_preview.py9
-rw-r--r--lib/extensions/troubleshoot.py27
-rw-r--r--lib/extensions/zip.py34
-rw-r--r--lib/gui/electron.py4
-rw-r--r--lib/gui/presets.py4
-rw-r--r--lib/gui/simulator.py26
-rw-r--r--lib/i18n.py4
-rw-r--r--lib/inx/__init__.py2
-rwxr-xr-xlib/inx/about.py7
-rwxr-xr-xlib/inx/extensions.py11
-rw-r--r--lib/inx/generate.py4
-rwxr-xr-xlib/inx/info.py9
-rwxr-xr-xlib/inx/inputs.py2
-rw-r--r--lib/inx/outputs.py13
-rw-r--r--lib/inx/utils.py31
-rw-r--r--lib/lettering/__init__.py2
-rw-r--r--lib/lettering/font.py44
-rw-r--r--lib/lettering/font_variant.py22
-rw-r--r--lib/lettering/glyph.py31
-rw-r--r--lib/lettering/kerning.py69
-rw-r--r--lib/output.py7
-rw-r--r--lib/stitch_plan/stitch_plan.py4
-rw-r--r--lib/stitches/__init__.py6
-rw-r--r--lib/stitches/auto_fill.py15
-rw-r--r--lib/stitches/auto_satin.py29
-rw-r--r--lib/stitches/fill.py5
-rw-r--r--lib/stitches/running_stitch.py2
-rw-r--r--lib/svg/guides.py8
-rw-r--r--lib/svg/path.py29
-rw-r--r--lib/svg/rendering.py59
-rw-r--r--lib/svg/svg.py3
-rw-r--r--lib/svg/tags.py5
-rw-r--r--lib/svg/units.py25
-rw-r--r--lib/threads/__init__.py6
-rw-r--r--lib/threads/catalog.py2
-rw-r--r--lib/threads/color.py9
-rw-r--r--lib/threads/palette.py5
-rw-r--r--lib/utils/dotdict.py2
-rw-r--r--lib/utils/geometry.py6
-rw-r--r--lib/utils/inkscape.py4
-rw-r--r--lib/utils/io.py22
-rw-r--r--lib/utils/version.py17
82 files changed, 1017 insertions, 830 deletions
diff --git a/lib/api/install.py b/lib/api/install.py
index 20138973..f52233fb 100644
--- a/lib/api/install.py
+++ b/lib/api/install.py
@@ -16,7 +16,7 @@ def palettes():
path = os.path.join(base_path, 'palettes')
src_dir = get_bundled_dir('palettes')
copy_files(glob(os.path.join(src_dir, "*")), path)
- except Exception, exc:
+ except Exception as exc:
return jsonify({"error": str(exc)}), 500
return jsonify({"status": "success"})
diff --git a/lib/api/server.py b/lib/api/server.py
index bdfa4573..0db253c6 100644
--- a/lib/api/server.py
+++ b/lib/api/server.py
@@ -1,16 +1,17 @@
import errno
import logging
import socket
+import sys
import time
from threading import Thread
import requests
from flask import Flask, g, request
+from ..utils.json import InkStitchJSONEncoder
from .install import install
from .simulator import simulator
from .stitch_plan import stitch_plan
-from ..utils.json import InkStitchJSONEncoder
class APIServer(Thread):
@@ -27,6 +28,10 @@ class APIServer(Thread):
self.__setup_app()
def __setup_app(self): # noqa: C901
+ # 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.json_encoder = InkStitchJSONEncoder
@@ -89,7 +94,7 @@ class APIServer(Thread):
response = requests.get("http://%s:%s/ping" % (self.host, self.port))
if response.status_code == 200:
break
- except socket.error, e:
+ except socket.error as e:
if e.errno == errno.ECONNREFUSED:
pass
else:
diff --git a/lib/api/stitch_plan.py b/lib/api/stitch_plan.py
index fd6bf9c9..95cdc7d8 100644
--- a/lib/api/stitch_plan.py
+++ b/lib/api/stitch_plan.py
@@ -11,7 +11,9 @@ def get_stitch_plan():
if not g.extension.get_elements():
return dict(colors=[], stitch_blocks=[], commands=[])
+ metadata = g.extension.get_inkstitch_metadata()
+ collapse_len = metadata['collapse_len_mm']
patches = g.extension.elements_to_patches(g.extension.elements)
- stitch_plan = patches_to_stitch_plan(patches)
+ stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len)
return jsonify(stitch_plan)
diff --git a/lib/commands.py b/lib/commands.py
index 9d0b243c..ba307487 100644
--- a/lib/commands.py
+++ b/lib/commands.py
@@ -3,11 +3,9 @@ import sys
from copy import deepcopy
from random import random
-from shapely import geometry as shgeo
-
-import cubicsuperpath
import inkex
-import simpletransform
+from lxml import etree
+from shapely import geometry as shgeo
from .i18n import N_, _
from .svg import (apply_transforms, generate_unique_id,
@@ -104,7 +102,7 @@ class Command(BaseCommand):
self.parse_command()
def parse_connector_path(self):
- path = cubicsuperpath.parsePath(self.connector.get('d'))
+ path = inkex.paths.Path(self.connector.get('d')).to_superpath()
return apply_transforms(path, self.connector)
def parse_command(self):
@@ -153,7 +151,7 @@ class StandaloneCommand(BaseCommand):
def point(self):
pos = [float(self.node.get("x", 0)), float(self.node.get("y", 0))]
transform = get_node_transform(self.node)
- simpletransform.applyTransformToPoint(transform, pos)
+ pos = inkex.transforms.Transform(transform).apply_to_point(pos)
return Point(*pos)
@@ -209,14 +207,14 @@ def global_command(svg, command):
if len(commands) == 1:
return commands[0]
elif len(commands) > 1:
- print >> sys.stderr, _("Error: there is more than one %(command)s command in the document, but there can only be one. "
- "Please remove all but one.") % dict(command=command)
+ print(_("Error: there is more than one %(command)s command in the document, but there can only be one. "
+ "Please remove all but one.") % dict(command=command), file=sys.stderr)
# L10N This is a continuation of the previous error message, letting the user know
# what command we're talking about since we don't normally expose the actual
# command name to them. Contents of %(description)s are in a separate translation
# string.
- print >> sys.stderr, _("%(command)s: %(description)s") % dict(command=command, description=_(get_command_description(command)))
+ print(_("%(command)s: %(description)s") % dict(command=command, description=_(get_command_description(command))), file=sys.stderr)
sys.exit(1)
else:
@@ -256,7 +254,7 @@ def symbols_path():
@cache
def symbols_svg():
with open(symbols_path()) as symbols_file:
- return inkex.etree.parse(symbols_file)
+ return etree.parse(symbols_file)
@cache
@@ -269,7 +267,7 @@ def get_defs(document):
defs = document.find(SVG_DEFS_TAG)
if defs is None:
- defs = inkex.etree.SubElement(document, SVG_DEFS_TAG)
+ defs = etree.SubElement(document, SVG_DEFS_TAG)
return defs
@@ -284,7 +282,7 @@ def ensure_symbol(document, command):
def add_group(document, node, command):
- return inkex.etree.SubElement(
+ return etree.SubElement(
node.getparent(),
SVG_GROUP_TAG,
{
@@ -304,35 +302,35 @@ def add_connector(document, symbol, element):
if element.node.get('id') is None:
element.node.set('id', generate_unique_id(document, "object"))
- path = inkex.etree.Element(SVG_PATH_TAG,
- {
- "id": generate_unique_id(document, "command_connector"),
- "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y),
- "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;",
- CONNECTION_START: "#%s" % symbol.get('id'),
- CONNECTION_END: "#%s" % element.node.get('id'),
- CONNECTOR_TYPE: "polyline",
+ path = etree.Element(SVG_PATH_TAG,
+ {
+ "id": generate_unique_id(document, "command_connector"),
+ "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y),
+ "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;",
+ CONNECTION_START: "#%s" % symbol.get('id'),
+ CONNECTION_END: "#%s" % element.node.get('id'),
+ CONNECTOR_TYPE: "polyline",
- # l10n: the name of the line that connects a command to the object it applies to
- INKSCAPE_LABEL: _("connector")
- })
+ # l10n: the name of the line that connects a command to the object it applies to
+ INKSCAPE_LABEL: _("connector")
+ })
symbol.getparent().insert(0, path)
def add_symbol(document, group, command, pos):
- return inkex.etree.SubElement(group, SVG_USE_TAG,
- {
- "id": generate_unique_id(document, "command_use"),
- XLINK_HREF: "#inkstitch_%s" % command,
- "height": "100%",
- "width": "100%",
- "x": str(pos.x),
- "y": str(pos.y),
+ return etree.SubElement(group, SVG_USE_TAG,
+ {
+ "id": generate_unique_id(document, "command_use"),
+ XLINK_HREF: "#inkstitch_%s" % command,
+ "height": "100%",
+ "width": "100%",
+ "x": str(pos.x),
+ "y": str(pos.y),
- # l10n: the name of a command symbol (example: scissors icon for trim command)
- INKSCAPE_LABEL: _("command marker"),
- })
+ # l10n: the name of a command symbol (example: scissors icon for trim command)
+ INKSCAPE_LABEL: _("command marker"),
+ })
def get_command_pos(element, index, total):
@@ -397,14 +395,14 @@ def add_layer_commands(layer, commands):
for command in commands:
ensure_symbol(document, command)
- inkex.etree.SubElement(layer, SVG_USE_TAG,
- {
- "id": generate_unique_id(document, "use"),
- INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
- XLINK_HREF: "#inkstitch_%s" % command,
- "height": "100%",
- "width": "100%",
- "x": "0",
- "y": "-10",
- "transform": correction_transform
- })
+ etree.SubElement(layer, SVG_USE_TAG,
+ {
+ "id": generate_unique_id(document, "use"),
+ INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
+ XLINK_HREF: "#inkstitch_%s" % command,
+ "height": "100%",
+ "width": "100%",
+ "x": "0",
+ "y": "-10",
+ "transform": correction_transform
+ })
diff --git a/lib/debug.py b/lib/debug.py
index 6ce67697..5d022e63 100644
--- a/lib/debug.py
+++ b/lib/debug.py
@@ -1,17 +1,16 @@
import atexit
-from contextlib import contextmanager
-from datetime import datetime
import os
import socket
import sys
import time
+from contextlib import contextmanager
+from datetime import datetime
-from inkex import etree
import inkex
-from simplestyle import formatStyle
+from lxml import etree
-from svg import line_strings_to_path
-from svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL
+from .svg import line_strings_to_path
+from .svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL
def check_enabled(func):
@@ -36,7 +35,10 @@ class Debug(object):
self.init_svg()
def init_log(self):
- self.log_file = open(os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.log"), "w")
+ self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.log")
+ # delete old content
+ with open(self.log_file, "w"):
+ pass
self.log("Debug logging enabled.")
def init_debugger(self):
@@ -60,7 +62,7 @@ class Debug(object):
try:
pydevd.settrace()
- except socket.error, error:
+ except socket.error as error:
self.log("Debugging: connection to pydevd failed: %s", error)
self.log("Be sure to run 'Start debugging server' in PyDev to enable debugging.")
else:
@@ -74,8 +76,8 @@ class Debug(object):
def save_svg(self):
tree = etree.ElementTree(self.svg)
- with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.svg"), "w") as debug_svg:
- tree.write(debug_svg)
+ debug_svg = os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.svg")
+ tree.write(debug_svg)
@check_enabled
def add_layer(self, name="Debug"):
@@ -113,20 +115,21 @@ class Debug(object):
timestamp = now.isoformat()
self.last_log_time = now
- print >> self.log_file, timestamp, message % args
- self.log_file.flush()
+ with open(self.log_file, "a") as logfile:
+ print(timestamp, message % args, file=logfile)
+ logfile.flush()
def time(self, func):
def decorated(*args, **kwargs):
if self.enabled:
- self.raw_log("entering %s()", func.func_name)
+ self.raw_log("entering %s()", func.__name__)
start = time.time()
result = func(*args, **kwargs)
if self.enabled:
end = time.time()
- self.raw_log("leaving %s(), duration = %s", func.func_name, round(end - start, 6))
+ self.raw_log("leaving %s(), duration = %s", func.__name__, round(end - start, 6))
return result
@@ -150,7 +153,7 @@ class Debug(object):
@check_enabled
def log_line_strings(self, line_strings, name=None, color=None):
path = line_strings_to_path(line_strings)
- path.set('style', formatStyle({"stroke": color or "#000000", "stroke-width": "0.3"}))
+ path.set('style', str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3"})))
if name is not None:
path.set(INKSCAPE_LABEL, name)
@@ -161,7 +164,7 @@ class Debug(object):
def log_line(self, start, end, name="line", color=None):
self.log_svg_element(etree.Element("path", {
"d": "M%s,%s %s,%s" % (start + end),
- "style": formatStyle({"stroke": color or "#000000", "stroke-width": "0.3"}),
+ "style": str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3"})),
INKSCAPE_LABEL: name
}))
@@ -174,7 +177,7 @@ class Debug(object):
self.log_svg_element(etree.Element("path", {
"d": d,
- "style": formatStyle({"stroke": color or "#000000", "stroke-width": "0.3"}),
+ "style": str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3"})),
INKSCAPE_LABEL: name
}))
diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py
index 92ef94a3..d53b2314 100644
--- a/lib/elements/__init__.py
+++ b/lib/elements/__init__.py
@@ -1,11 +1,11 @@
-from auto_fill import AutoFill
-from clone import Clone
-from element import EmbroideryElement
-from empty_d_object import EmptyDObject
-from fill import Fill
-from image import ImageObject
-from polyline import Polyline
-from satin_column import SatinColumn
-from stroke import Stroke
-from text import TextObject
-from utils import node_to_elements, nodes_to_elements
+from .auto_fill import AutoFill
+from .clone import Clone
+from .element import EmbroideryElement
+from .empty_d_object import EmptyDObject
+from .fill import Fill
+from .image import ImageObject
+from .polyline import Polyline
+from .satin_column import SatinColumn
+from .stroke import Stroke
+from .text import TextObject
+from .utils import node_to_elements, nodes_to_elements
diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py
index b574c8bf..31da7e63 100644
--- a/lib/elements/auto_fill.py
+++ b/lib/elements/auto_fill.py
@@ -6,10 +6,9 @@ from shapely import geometry as shgeo
from ..i18n import _
from ..stitches import auto_fill
-from ..utils import cache
+from ..utils import cache, version
from .element import Patch, param
from .fill import Fill
-
from .validation import ValidationWarning
@@ -20,6 +19,18 @@ class SmallShapeWarning(ValidationWarning):
"the outline instead.")
+class ExpandWarning(ValidationWarning):
+ name = _("Expand")
+ description = _("The expand parameter for this fill object cannot be applied. "
+ "Ink/Stitch will ignore it and will use original size instead.")
+
+
+class UnderlayInsetWarning(ValidationWarning):
+ name = _("Inset")
+ description = _("The underlay inset parameter for this fill object cannot be applied. "
+ "Ink/Stitch will ignore it and will use the original size instead.")
+
+
class AutoFill(Fill):
element_name = _("AutoFill")
@@ -157,9 +168,13 @@ class AutoFill(Fill):
def underlay_underpath(self):
return self.get_boolean_param('underlay_underpath', True)
- def shrink_or_grow_shape(self, amount):
+ def shrink_or_grow_shape(self, amount, validate=False):
if amount:
shape = self.shape.buffer(amount)
+ # changing the size can empty the shape
+ # in this case we want to use the original shape rather than returning an error
+ if shape.is_empty and not validate:
+ return self.shape
if not isinstance(shape, shgeo.MultiPolygon):
shape = shgeo.MultiPolygon([shape])
return shape
@@ -235,6 +250,7 @@ class AutoFill(Fill):
# L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new
message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ")
message += "https://github.com/inkstitch/inkstitch/issues/new\n\n"
+ message += version.get_inkstitch_version() + "\n\n"
message += traceback.format_exc()
self.fatal(message)
@@ -245,5 +261,11 @@ class AutoFill(Fill):
if self.shape.area < 20:
yield SmallShapeWarning(self.shape.centroid)
+ if self.shrink_or_grow_shape(self.expand, True).is_empty:
+ yield ExpandWarning(self.shape.centroid)
+
+ if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty:
+ yield UnderlayInsetWarning(self.shape.centroid)
+
for warning in super(AutoFill, self).validation_warnings():
yield warning
diff --git a/lib/elements/clone.py b/lib/elements/clone.py
index b8046d2d..fd770bd7 100644
--- a/lib/elements/clone.py
+++ b/lib/elements/clone.py
@@ -1,16 +1,13 @@
-from copy import copy
from math import atan, degrees
-from simpletransform import (applyTransformToNode, applyTransformToPoint,
- computeBBox, parseTransform)
+import inkex
from ..commands import is_command, is_command_symbol
from ..i18n import _
from ..svg.path import get_node_transform
from ..svg.svg import find_elements
-from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_GROUP_TAG,
- SVG_LINK_TAG, SVG_POLYLINE_TAG, SVG_USE_TAG,
- XLINK_HREF)
+from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS,
+ SVG_POLYLINE_TAG, SVG_USE_TAG, XLINK_HREF)
from ..utils import cache
from .auto_fill import AutoFill
from .element import EmbroideryElement, param
@@ -74,16 +71,16 @@ class Clone(EmbroideryElement):
if node.tag == SVG_POLYLINE_TAG:
return [Polyline(node)]
- elif element.get_boolean_param("satin_column") and element.get_style("stroke"):
+ elif element.get_boolean_param("satin_column") and self.get_clone_style("stroke", self.node):
return [SatinColumn(node)]
else:
elements = []
- if element.get_style("fill", "black") and not element.get_style("fill-opacity", 1) == "0":
+ if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0":
if element.get_boolean_param("auto_fill", True):
elements.append(AutoFill(node))
else:
elements.append(Fill(node))
- if element.get_style("stroke"):
+ if element.get_style("stroke", self.node) is not None:
if not is_command(element.node):
elements.append(Stroke(node))
if element.get_boolean_param("stroke_first", False):
@@ -98,32 +95,8 @@ class Clone(EmbroideryElement):
if source_node.tag not in EMBROIDERABLE_TAGS:
return []
- clone = copy(source_node)
-
- # set id
- clone_id = 'clone__%s__%s' % (self.node.get('id', ''), clone.get('id', ''))
- clone.set('id', clone_id)
-
- # apply transform
- transform = get_node_transform(self.node)
- applyTransformToNode(transform, clone)
-
- # apply style
- stroke_style = self.get_clone_style('stroke', self.node)
- if not stroke_style:
- stroke_style = self.get_clone_style('stroke', source_node)
- fill_style = self.node.get('fill')
- if not fill_style:
- fill_style = self.get_clone_style('fill', source_node, "#000000")
- fill_opacity = self.node.get('fill-opacity')
- if not fill_opacity:
- fill_opacity = self.get_clone_style('fill-opacity', source_node, "1")
- style = "fill:%s;fill-opacity:%s;" % (fill_style, fill_opacity)
- if stroke_style:
- style += "stroke:%s;" % stroke_style
- clone.set('style', style)
-
- # set fill angle. Use either
+ self.node.style = source_node.composed_style()
+
# a. a custom set fill angle
# b. calculated rotation for the cloned fill element to look exactly as it's source
param = INKSTITCH_ATTRIBS['angle']
@@ -131,48 +104,32 @@ class Clone(EmbroideryElement):
angle = self.clone_fill_angle
else:
# clone angle
- clone_mat = parseTransform(clone.get('transform', ''))
+ clone_mat = self.node.composed_transform()
clone_angle = degrees(atan(-clone_mat[1][0]/clone_mat[1][1]))
# source node angle
- source_mat = parseTransform(source_node.get('transform', ''))
+ source_mat = source_node.composed_transform()
source_angle = degrees(atan(-source_mat[1][0]/source_mat[1][1]))
# source node fill angle
source_fill_angle = source_node.get(param, 0)
angle = clone_angle + float(source_fill_angle) - source_angle
- clone.set(param, str(angle))
+ self.node.set(param, str(angle))
- elements = self.clone_to_element(clone)
+ elements = self.clone_to_element(self.node)
for element in elements:
patches.extend(element.to_patches(last_patch))
return patches
- def _get_clone_style_raw(self, style_name, node):
- style = self.parse_style()
- style = style.get(style_name) or self.node.get(style_name)
- parent = self.node.getparent()
- # style not found, get inherited style elements
- while not style and parent is not None:
- if parent.tag not in [SVG_GROUP_TAG, SVG_LINK_TAG]:
- parent = parent.getparent()
- continue
- style = self.parse_style(parent)
- style = style.get(style_name) or parent.get(style_name)
- parent = parent.getparent()
- return style
-
def get_clone_style(self, style_name, node, default=None):
- style = self._get_clone_style_raw(style_name, node) or default
+ style = inkex.styles.AttrFallbackStyle(node).get(style_name) or default
return style
def center(self, source_node):
- xmin, xmax, ymin, ymax = computeBBox([source_node])
- point = [(xmax-((xmax-xmin)/2)), (ymax-((ymax-ymin)/2))]
- transform = get_node_transform(self.node)
- applyTransformToPoint(transform, point)
- return point
+ transform = get_node_transform(self.node.getparent())
+ center = self.node.bounding_box(transform).center
+ return center
def validation_warnings(self):
source_node = get_clone_source(self.node)
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 5d2934cd..2ced143b 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -1,18 +1,16 @@
import sys
from copy import deepcopy
-import cubicsuperpath
-import simpletransform
+import inkex
import tinycss2
-from cspsubdiv import cspsubdiv
+from inkex import bezier
-from .svg_objects import circle_to_path, ellipse_to_path, rect_to_path
from ..commands import find_commands
from ..i18n import _
from ..svg import (PIXELS_PER_MM, apply_transforms, convert_length,
get_node_transform)
-from ..svg.tags import (EMBROIDERABLE_TAGS, INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_CIRCLE_TAG, SVG_ELLIPSE_TAG, SVG_GROUP_TAG, SVG_LINK_TAG,
- SVG_OBJECT_TAGS, SVG_RECT_TAG)
+from ..svg.tags import (EMBROIDERABLE_TAGS, INKSCAPE_LABEL, INKSTITCH_ATTRIBS,
+ SVG_GROUP_TAG, SVG_LINK_TAG, SVG_USE_TAG)
from ..utils import Point, cache
@@ -155,22 +153,29 @@ class EmbroideryElement(object):
def parse_style(self, node=None):
if node is None:
node = self.node
+ element_style = node.get("style", "")
+ if element_style is None:
+ return None
declarations = tinycss2.parse_declaration_list(node.get("style", ""))
style = {declaration.lower_name: declaration.value[0].serialize() for declaration in declarations}
return style
@cache
def _get_style_raw(self, style_name):
- if self.node.tag not in [SVG_GROUP_TAG, SVG_LINK_TAG] and self.node.tag not in EMBROIDERABLE_TAGS:
+ if self.node is None:
+ return None
+ if self.node.tag not in [SVG_GROUP_TAG, SVG_LINK_TAG, SVG_USE_TAG] and self.node.tag not in EMBROIDERABLE_TAGS:
return None
style = self.parse_style()
- style = style.get(style_name) or self.node.get(style_name)
+ if style:
+ style = style.get(style_name) or self.node.get(style_name)
parent = self.node.getparent()
# style not found, get inherited style elements
while not style and parent is not None:
style = self.parse_style(parent)
- style = style.get(style_name) or parent.get(style_name)
+ if style:
+ style = style.get(style_name) or parent.get(style_name)
parent = parent.getparent()
return style
@@ -196,23 +201,23 @@ class EmbroideryElement(object):
# Of course, transforms may also involve rotation, skewing, and translation.
# All except translation can affect how wide the stroke appears on the screen.
- node_transform = get_node_transform(self.node)
+ node_transform = inkex.transforms.Transform(get_node_transform(self.node))
# First, figure out the translation component of the transform. Using a zero
# vector completely cancels out the rotation, scale, and skew components.
zero = [0, 0]
- simpletransform.applyTransformToPoint(node_transform, zero)
+ zero = inkex.Transform.apply_to_point(node_transform, zero)
translate = Point(*zero)
# Next, see how the transform affects unit vectors in the X and Y axes. We
# need to subtract off the translation or it will affect the magnitude of
# the resulting vector, which we don't want.
unit_x = [1, 0]
- simpletransform.applyTransformToPoint(node_transform, unit_x)
+ unit_x = inkex.Transform.apply_to_point(node_transform, unit_x)
sx = (Point(*unit_x) - translate).length()
unit_y = [0, 1]
- simpletransform.applyTransformToPoint(node_transform, unit_y)
+ unit_y = inkex.Transform.apply_to_point(node_transform, unit_y)
sy = (Point(*unit_y) - translate).length()
# Take the average as a best guess.
@@ -223,11 +228,7 @@ class EmbroideryElement(object):
@property
@cache
def stroke_width(self):
- width = self.get_style("stroke-width", None)
-
- if width is None:
- return 1.0
-
+ width = self.get_style("stroke-width", "1.0")
width = convert_length(width)
return width * self.stroke_scale
@@ -271,20 +272,15 @@ class EmbroideryElement(object):
# In a path, each element in the 3-tuple is itself a tuple of (x, y).
# Tuples all the way down. Hasn't anyone heard of using classes?
- if self.node.tag in SVG_OBJECT_TAGS:
- if self.node.tag == SVG_RECT_TAG:
- d = rect_to_path(self.node)
- elif self.node.tag == SVG_ELLIPSE_TAG:
- d = ellipse_to_path(self.node)
- elif self.node.tag == SVG_CIRCLE_TAG:
- d = circle_to_path(self.node)
+ if getattr(self.node, "get_path", None):
+ d = self.node.get_path()
else:
d = self.node.get("d", "")
if not d:
self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict(id=self.node.get("id")))
- return cubicsuperpath.parsePath(d)
+ return inkex.paths.Path(d).to_superpath()
@cache
def parse_path(self):
@@ -315,7 +311,7 @@ class EmbroideryElement(object):
return commands[0]
elif len(commands) > 1:
raise ValueError(_("%(id)s has more than one command of type '%(command)s' linked to it") %
- dict(id=self.node.get(id), command=command))
+ dict(id=self.node.get('id'), command=command))
else:
return None
@@ -326,13 +322,13 @@ class EmbroideryElement(object):
"""approximate a path containing beziers with a series of points"""
path = deepcopy(path)
- cspsubdiv(path, 0.1)
+ bezier.cspsubdiv(path, 0.1)
return [self.strip_control_points(subpath) for subpath in path]
def flatten_subpath(self, subpath):
path = [deepcopy(subpath)]
- cspsubdiv(path, 0.1)
+ bezier.cspsubdiv(path, 0.1)
return self.strip_control_points(path[0])
@@ -373,7 +369,7 @@ class EmbroideryElement(object):
# L10N used when showing an error message to the user such as
# "Some Path (path1234): error: satin column: One or more of the rungs doesn't intersect both rails."
error_msg = "%s: %s %s" % (name, _("error:"), message)
- print >> sys.stderr, "%s" % (error_msg.encode("UTF-8"))
+ inkex.errormsg(error_msg)
sys.exit(1)
def validation_errors(self):
diff --git a/lib/elements/fill.py b/lib/elements/fill.py
index 2e94847a..1f4c7b1e 100644
--- a/lib/elements/fill.py
+++ b/lib/elements/fill.py
@@ -136,6 +136,12 @@ class Fill(EmbroideryElement):
# biggest path.
paths = self.paths
paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True)
+ # Very small holes will cause a shape to be rendered as an outline only
+ # they are too small to be rendered and only confuse the auto_fill algorithm.
+ # So let's ignore them
+ if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5:
+ paths = [path for path in paths if shgeo.Polygon(path).area > 3]
+
polygon = shgeo.MultiPolygon([(paths[0], paths[1:])])
# There is a great number of "crossing border" errors on fill shapes
diff --git a/lib/elements/image.py b/lib/elements/image.py
index ec8d1765..160898a5 100644
--- a/lib/elements/image.py
+++ b/lib/elements/image.py
@@ -1,7 +1,5 @@
-from simpletransform import applyTransformToPoint
-
from ..i18n import _
-from ..svg import get_node_transform
+from ..svg.path import get_node_transform
from .element import EmbroideryElement
from .validation import ObjectTypeWarning
@@ -19,13 +17,9 @@ class ImageTypeWarning(ObjectTypeWarning):
class ImageObject(EmbroideryElement):
def center(self):
- point = [float(self.node.get('x', 0)), float(self.node.get('y', 0))]
- point = [(point[0]+(float(self.node.get('width', 0))/2)), (point[1]+(float(self.node.get('height', 0))/2))]
-
- transform = get_node_transform(self.node)
- applyTransformToPoint(transform, point)
-
- return point
+ transform = get_node_transform(self.node.getparent())
+ center = self.node.bounding_box(transform).center
+ return center
def validation_warnings(self):
yield ImageTypeWarning(self.center())
diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py
index 2d008d35..da1e807d 100644
--- a/lib/elements/polyline.py
+++ b/lib/elements/polyline.py
@@ -1,3 +1,4 @@
+from inkex import Path
from shapely import geometry as shgeo
from ..i18n import _
@@ -36,7 +37,7 @@ class Polyline(EmbroideryElement):
@property
@param('polyline', _('Manual stitch along path'), type='toggle', inverse=True)
- def satin_column(self):
+ def polyline(self):
return self.get_boolean_param("polyline")
@property
@@ -61,8 +62,8 @@ class Polyline(EmbroideryElement):
# svg transforms that is in our superclass, we'll convert the polyline
# to a degenerate cubic superpath in which the bezier handles are on
# the segment endpoints.
-
- path = [[[point[:], point[:], point[:]] for point in self.points]]
+ path = self.node.get_path()
+ path = Path(path).to_superpath()
return path
diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py
index d0a3f8ff..fbadd92f 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -1,14 +1,15 @@
from copy import deepcopy
-from itertools import chain, izip
+from itertools import chain
-import cubicsuperpath
-from shapely import affinity as shaffinity, geometry as shgeo
+from inkex import paths
+from shapely import affinity as shaffinity
+from shapely import geometry as shgeo
-from .element import EmbroideryElement, Patch, param
-from .validation import ValidationError
from ..i18n import _
from ..svg import line_strings_to_csp, point_lists_to_csp
from ..utils import Point, cache, collapse_duplicate_point, cut
+from .element import EmbroideryElement, Patch, param
+from .validation import ValidationError
class SatinHasFillError(ValidationError):
@@ -234,7 +235,7 @@ class SatinColumn(EmbroideryElement):
rung_endpoints.append(points)
rungs = []
- for start, end in izip(*rung_endpoints):
+ for start, end in zip(*rung_endpoints):
# Expand the points just a bit to ensure that shapely thinks they
# intersect with the rails even with floating point inaccuracy.
start = Point(*start)
@@ -266,12 +267,12 @@ class SatinColumn(EmbroideryElement):
if num_paths <= 2:
# old-style satin column with no rungs
- return range(num_paths)
+ return list(range(num_paths))
# This takes advantage of the fact that sum() counts True as 1
- intersection_counts = [sum(paths[i].intersects(paths[j]) for j in xrange(num_paths) if i != j)
- for i in xrange(num_paths)]
- paths_not_intersecting_two = [i for i in xrange(num_paths) if intersection_counts[i] != 2]
+ intersection_counts = [sum(paths[i].intersects(paths[j]) for j in range(num_paths) if i != j)
+ for i in range(num_paths)]
+ paths_not_intersecting_two = [i for i in range(num_paths) if intersection_counts[i] != 2]
num_not_intersecting_two = len(paths_not_intersecting_two)
if num_not_intersecting_two == 2:
@@ -292,7 +293,7 @@ class SatinColumn(EmbroideryElement):
# kind of weird thing. Maybe one of the rungs crosses a rail more
# than once. Treat it like the previous case and we'll sort out
# the intersection issues later.
- indices_by_length = sorted(range(num_paths), key=lambda index: paths[index].length, reverse=True)
+ indices_by_length = sorted(list(range(num_paths)), key=lambda index: paths[index].length, reverse=True)
return indices_by_length[:2]
def _cut_rail(self, rail, rung):
@@ -330,7 +331,7 @@ class SatinColumn(EmbroideryElement):
self._cut_rail(rail, rung)
for rail in rails:
- for i in xrange(len(rail)):
+ for i in range(len(rail)):
if rail[i] is not None:
rail[i] = [Point(*coord) for coord in rail[i].coords]
@@ -345,7 +346,7 @@ class SatinColumn(EmbroideryElement):
# zero-length bezier at the star. The user's goal here is to ignore the
# horizontal section of the right rail.
- sections = zip(*rails)
+ sections = list(zip(*rails))
sections = [s for s in sections if s[0] is not None and s[1] is not None]
return sections
@@ -438,13 +439,13 @@ class SatinColumn(EmbroideryElement):
"""
# like in do_satin()
- points = list(chain.from_iterable(izip(*self.plot_points_on_rails(self.zigzag_spacing, 0))))
+ points = list(chain.from_iterable(zip(*self.plot_points_on_rails(self.zigzag_spacing, 0))))
if isinstance(split_point, float):
index_of_closest_stitch = int(round(len(points) * split_point))
else:
split_point = Point(*split_point)
- index_of_closest_stitch = min(range(len(points)), key=lambda index: split_point.distance(points[index]))
+ index_of_closest_stitch = min(list(range(len(points))), key=lambda index: split_point.distance(points[index]))
if index_of_closest_stitch % 2 == 0:
# split point is on the first rail
@@ -517,7 +518,7 @@ class SatinColumn(EmbroideryElement):
def _csp_to_satin(self, csp):
node = deepcopy(self.node)
- d = cubicsuperpath.formatPath(csp)
+ d = paths.CubicSuperPath(csp).to_path()
node.set("d", d)
# we've already applied the transform, so get rid of it
@@ -626,7 +627,9 @@ class SatinColumn(EmbroideryElement):
# Each iteration of this outer loop is one stitch. Keep going
# until we fall off the end of the section.
- old_center = (pos0 + pos1) / 2.0
+ # TODO: is there an other way?
+ # old_center = (pos0 + pos1) / 2.0
+ old_center = shgeo.Point(x/2 for x in (pos0 + pos1))
while to_travel > 0 and index0 < last_index0 and index1 < last_index1:
# In this loop, we inch along each rail a tiny bit per
@@ -653,7 +656,9 @@ class SatinColumn(EmbroideryElement):
pos0, index0 = self.walk(section0, pos0, index0, 0.05)
pos1, index1 = self.walk(section1, pos1, index1, 0.05 * ratio)
- new_center = (pos0 + pos1) / 2.0
+ # TODO: is there a better way?
+ # new_center = (pos0 + pos1) / 2.0
+ new_center = shgeo.Point(x/2 for x in (pos0 + pos1))
to_travel -= new_center.distance(old_center)
old_center = new_center
@@ -705,7 +710,7 @@ class SatinColumn(EmbroideryElement):
# This fancy bit of iterable magic just repeatedly takes a point
# from each side in turn.
- for point in chain.from_iterable(izip(*sides)):
+ for point in chain.from_iterable(zip(*sides)):
patch.add_stitch(point)
return patch
@@ -724,7 +729,7 @@ class SatinColumn(EmbroideryElement):
sides = self.plot_points_on_rails(self.zigzag_spacing, self.pull_compensation)
# Like in zigzag_underlay(): take a point from each side in turn.
- for point in chain.from_iterable(izip(*sides)):
+ for point in chain.from_iterable(zip(*sides)):
patch.add_stitch(point)
return patch
@@ -743,7 +748,7 @@ class SatinColumn(EmbroideryElement):
# "left" and "right" here are kind of arbitrary designations meaning
# a point from the first and second rail repectively
- for left, right in izip(*sides):
+ for left, right in zip(*sides):
patch.add_stitch(left)
patch.add_stitch(right)
patch.add_stitch(left)
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 36d1048e..d63a21a9 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -134,9 +134,9 @@ class Stroke(EmbroideryElement):
global warned_about_legacy_running_stitch
if not warned_about_legacy_running_stitch:
warned_about_legacy_running_stitch = True
- print >> sys.stderr, _("Legacy running stitch setting detected!\n\nIt looks like you're using a stroke " +
- "smaller than 0.5 units to indicate a running stitch, which is deprecated. Instead, please set " +
- "your stroke to be dashed to indicate running stitch. Any kind of dash will work.")
+ print(_("Legacy running stitch setting detected!\n\nIt looks like you're using a stroke " +
+ "smaller than 0.5 units to indicate a running stitch, which is deprecated. Instead, please set " +
+ "your stroke to be dashed to indicate running stitch. Any kind of dash will work."), file=sys.stderr)
# still allow the deprecated setting to work in order to support old files
return True
@@ -157,7 +157,7 @@ class Stroke(EmbroideryElement):
offset = stroke_width / 2.0
- for i in xrange(len(patch) - 1):
+ for i in range(len(patch) - 1):
start = patch.stitches[i]
end = patch.stitches[i + 1]
segment_direction = (end - start).unit()
@@ -174,7 +174,7 @@ class Stroke(EmbroideryElement):
repeated_path = []
# go back and forth along the path as specified by self.repeats
- for i in xrange(self.repeats):
+ for i in range(self.repeats):
if i % 2 == 1:
# reverse every other pass
this_path = path[::-1]
diff --git a/lib/elements/svg_objects.py b/lib/elements/svg_objects.py
deleted file mode 100644
index 4760af5f..00000000
--- a/lib/elements/svg_objects.py
+++ /dev/null
@@ -1,71 +0,0 @@
-def rect_to_path(node):
- x = float(node.get('x', '0'))
- y = float(node.get('y', '0'))
- width = float(node.get('width', '0'))
- height = float(node.get('height', '0'))
- rx = 0
- ry = 0
-
- # rounded corners
- # the following rules apply for radius calculations:
- # * if rx or ry is missing it has to take the value of the other one
- # * the radius cannot be bigger than half of the corresponding side
- # (otherwise we receive an invalid path)
- if node.get('rx') or node.get('ry'):
- if node.get('rx'):
- rx = float(node.get('rx', '0'))
- ry = rx
- if node.get('ry'):
- ry = float(node.get('ry', '0'))
- if not ry:
- ry = rx
-
- rx = min(width/2, rx)
- ry = min(height/2, ry)
-
- path = 'M %(startx)f,%(y)f ' \
- 'h %(width)f ' \
- 'q %(rx)f,0 %(rx)f,%(ry)f ' \
- 'v %(height)f ' \
- 'q 0,%(ry)f -%(rx)f,%(ry)f ' \
- 'h -%(width)f ' \
- 'q -%(rx)f,0 -%(rx)f,-%(ry)f ' \
- 'v -%(height)f ' \
- 'q 0,-%(ry)f %(rx)f,-%(ry)f ' \
- 'Z' \
- % dict(startx=x+rx, x=x, y=y, width=width-(2*rx), height=height-(2*ry), rx=rx, ry=ry)
-
- else:
- path = "M %f,%f H %f V %f H %f Z" % (x, y, width+x, height+y, x)
-
- return path
-
-
-def ellipse_to_path(node):
- rx = float(node.get('rx', "0")) or float(node.get('r', "0"))
- ry = float(node.get('ry', "0")) or float(node.get('r', "0"))
- cx = float(node.get('cx'))
- cy = float(node.get('cy'))
-
- path = 'M %(cx_r)f,%(cy)f' \
- 'C %(cx_r)f,%(cy_r)f %(cx)f,%(cy_r)f %(cx)f,%(cy_r)f ' \
- '%(cxr)f,%(cy_r)f %(cxr)f,%(cy)f %(cxr)f,%(cy)f ' \
- '%(cxr)f,%(cyr)f %(cx)f,%(cyr)f %(cx)f,%(cyr)f ' \
- '%(cx_r)f,%(cyr)f %(cx_r)f,%(cy)f %(cx_r)f,%(cy)f ' \
- 'Z' \
- % dict(cx=cx, cx_r=cx-rx, cxr=cx+rx, cy=cy, cyr=cy+ry, cy_r=cy-ry)
-
- return path
-
-
-def circle_to_path(node):
- cx = float(node.get('cx'))
- cy = float(node.get('cy'))
- r = float(node.get('r'))
-
- path = 'M %(xstart)f, %(cy)f ' \
- 'a %(r)f,%(r)f 0 1,0 %(rr)f,0 ' \
- 'a %(r)f,%(r)f 0 1,0 -%(rr)f,0 ' \
- % dict(xstart=(cx-r), cy=cy, r=r, rr=(r*2))
-
- return path
diff --git a/lib/elements/text.py b/lib/elements/text.py
index 2d066bb0..838be96a 100644
--- a/lib/elements/text.py
+++ b/lib/elements/text.py
@@ -1,9 +1,7 @@
-from simpletransform import applyTransformToPoint
-
from ..i18n import _
-from ..svg import get_node_transform
from .element import EmbroideryElement
from .validation import ObjectTypeWarning
+from ..svg.path import get_node_transform
class TextTypeWarning(ObjectTypeWarning):
@@ -17,16 +15,14 @@ class TextTypeWarning(ObjectTypeWarning):
class TextObject(EmbroideryElement):
- def center(self):
- point = [float(self.node.get('x', 0)), float(self.node.get('y', 0))]
-
- transform = get_node_transform(self.node)
- applyTransformToPoint(transform, point)
+ def pointer(self):
+ transform = get_node_transform(self.node.getparent())
+ point = self.node.bounding_box(transform).center
return point
def validation_warnings(self):
- yield TextTypeWarning(self.center())
+ yield TextTypeWarning(self.pointer())
def to_patches(self, last_patch):
return []
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 &#10; 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:
diff --git a/lib/gui/electron.py b/lib/gui/electron.py
index 83486f78..ef215fb5 100644
--- a/lib/gui/electron.py
+++ b/lib/gui/electron.py
@@ -27,5 +27,5 @@ def open_url(url):
cwd = get_bundled_dir("electron")
# Any output on stdout will crash inkscape.
- null = open(os.devnull, 'w')
- return subprocess.Popen(command, cwd=cwd, stdout=null)
+ with open(os.devnull, 'w') as null:
+ return subprocess.Popen(command, cwd=cwd, stdout=null)
diff --git a/lib/gui/presets.py b/lib/gui/presets.py
index b3312f0e..2c0d0481 100644
--- a/lib/gui/presets.py
+++ b/lib/gui/presets.py
@@ -64,7 +64,7 @@ class PresetsPanel(wx.Panel):
presets_sizer = wx.StaticBoxSizer(self.presets_box, wx.HORIZONTAL)
self.preset_chooser.SetMinSize((200, -1))
- presets_sizer.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.BOTTOM | wx.EXPAND, 10)
+ presets_sizer.Add(self.preset_chooser, 1, wx.LEFT | wx.BOTTOM | wx.EXPAND, 10)
presets_sizer.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10)
presets_sizer.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10)
presets_sizer.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10)
@@ -106,7 +106,7 @@ class PresetsPanel(wx.Panel):
json.dump(presets, presets_file)
def update_preset_list(self):
- preset_names = self._load_presets().keys()
+ preset_names = list(self._load_presets().keys())
preset_names = [preset for preset in preset_names if not self.is_hidden(preset)]
self.preset_chooser.SetItems(sorted(preset_names))
diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py
index 48709cb8..996bc8f9 100644
--- a/lib/gui/simulator.py
+++ b/lib/gui/simulator.py
@@ -1,21 +1,16 @@
-from itertools import izip
import sys
-from threading import Thread, Event
import time
import traceback
+from threading import Event, Thread
import wx
from wx.lib.intctrl import IntCtrl
from ..i18n import _
-from ..stitch_plan import stitch_plan_from_file, patches_to_stitch_plan
-
+from ..stitch_plan import patches_to_stitch_plan, stitch_plan_from_file
from ..svg import PIXELS_PER_MM
-
-
from .dialogs import info_dialog
-
# L10N command label at bottom of simulator window
COMMAND_NAMES = [_("STITCH"), _("JUMP"), _("TRIM"), _("STOP"), _("COLOR CHANGE")]
@@ -132,7 +127,7 @@ class ControlPanel(wx.Panel):
self.accel_entries = []
for shortcut_key in shortcut_keys:
- eventId = wx.NewId()
+ eventId = wx.NewIdRef()
self.accel_entries.append((shortcut_key[0], shortcut_key[1], eventId))
self.Bind(wx.EVT_MENU, shortcut_key[2], id=eventId)
@@ -378,7 +373,7 @@ class DrawingPanel(wx.Panel):
last_stitch = None
start = time.time()
- for pen, stitches in izip(self.pens, self.stitch_blocks):
+ for pen, stitches in zip(self.pens, self.stitch_blocks):
canvas.SetPen(pen)
if stitch + len(stitches) < self.current_stitch:
stitch += len(stitches)
@@ -423,7 +418,7 @@ class DrawingPanel(wx.Panel):
while scale_width < 50:
scale_width += one_mm
- scale_width_mm = scale_width / self.zoom / PIXELS_PER_MM
+ scale_width_mm = int(scale_width / self.zoom / PIXELS_PER_MM)
# The scale bar looks like this:
#
@@ -431,7 +426,7 @@ class DrawingPanel(wx.Panel):
# |_____|_____|
scale_lower_left_x = 20
- scale_lower_left_y = canvas_height - 20
+ scale_lower_left_y = canvas_height - 30
canvas.DrawLines(((scale_lower_left_x, scale_lower_left_y - 6),
(scale_lower_left_x, scale_lower_left_y),
@@ -504,7 +499,7 @@ class DrawingPanel(wx.Panel):
# We draw the thread with a thickness of 0.1mm. Real thread has a
# thickness of ~0.4mm, but if we did that, we wouldn't be able to
# see the individual stitches.
- return wx.Pen(color.visible_on_white.rgb, width=int(0.1 * PIXELS_PER_MM * self.PIXEL_DENSITY))
+ return wx.Pen(list(map(int, color.visible_on_white.rgb)), int(0.1 * PIXELS_PER_MM * self.PIXEL_DENSITY))
def parse_stitch_plan(self, stitch_plan):
self.pens = []
@@ -698,12 +693,7 @@ class EmbroiderySimulator(wx.Frame):
stitches_per_second=stitches_per_second)
sizer.Add(self.simulator_panel, 1, wx.EXPAND)
- # self.SetSizerAndFit() sets the minimum size so that the buttons don't
- # get squished. But it then also shrinks the window down to that size.
- self.SetSizerAndFit(sizer)
-
- # Therefore we have to reapply the size that the caller asked for.
- self.SetSize(size)
+ self.SetSizeHints(sizer.CalcMin())
self.Bind(wx.EVT_CLOSE, self.on_close)
diff --git a/lib/i18n.py b/lib/i18n.py
index f57bbf9c..50e71c92 100644
--- a/lib/i18n.py
+++ b/lib/i18n.py
@@ -1,7 +1,7 @@
import gettext
import os
-from os.path import dirname, realpath
import sys
+from os.path import dirname, realpath
from .utils import cache
@@ -32,7 +32,7 @@ def localize(languages=None):
global translation, _
translation = gettext.translation("inkstitch", locale_dir, fallback=True)
- _ = translation.ugettext
+ _ = translation.gettext
@cache
diff --git a/lib/inx/__init__.py b/lib/inx/__init__.py
index 32b8bfae..cc2b039d 100644
--- a/lib/inx/__init__.py
+++ b/lib/inx/__init__.py
@@ -1 +1 @@
-from generate import generate_inx_files
+from .generate import generate_inx_files
diff --git a/lib/inx/about.py b/lib/inx/about.py
new file mode 100755
index 00000000..6db13865
--- /dev/null
+++ b/lib/inx/about.py
@@ -0,0 +1,7 @@
+from .utils import build_environment, write_inx_file
+
+
+def generate_about_inx_file():
+ env = build_environment()
+ template = env.get_template('about.xml')
+ write_inx_file("about", template.render())
diff --git a/lib/inx/extensions.py b/lib/inx/extensions.py
index 030e8aa6..379e98cd 100755
--- a/lib/inx/extensions.py
+++ b/lib/inx/extensions.py
@@ -1,10 +1,11 @@
import pyembroidery
-from .utils import build_environment, write_inx_file
-from .outputs import pyembroidery_output_formats
-from ..extensions import extensions, Input, Output
-from ..commands import LAYER_COMMANDS, OBJECT_COMMANDS, GLOBAL_COMMANDS, COMMANDS
+from ..commands import (COMMANDS, GLOBAL_COMMANDS, LAYER_COMMANDS,
+ OBJECT_COMMANDS)
+from ..extensions import Input, Output, extensions
from ..threads import ThreadCatalog
+from .outputs import pyembroidery_output_formats
+from .utils import build_environment, write_inx_file
def layer_commands():
@@ -41,7 +42,7 @@ def generate_extension_inx_files():
continue
name = extension.name()
- template = env.get_template('%s.inx' % name)
+ template = env.get_template('%s.xml' % name)
write_inx_file(name, template.render(formats=pyembroidery_output_formats(),
debug_formats=pyembroidery_debug_formats(),
threadcatalog=threadcatalog(),
diff --git a/lib/inx/generate.py b/lib/inx/generate.py
index 941596de..8a5b9569 100644
--- a/lib/inx/generate.py
+++ b/lib/inx/generate.py
@@ -1,6 +1,7 @@
+from .info import generate_info_inx_files
+from .extensions import generate_extension_inx_files
from .inputs import generate_input_inx_files
from .outputs import generate_output_inx_files
-from .extensions import generate_extension_inx_files
from .utils import iterate_inx_locales
@@ -9,3 +10,4 @@ def generate_inx_files():
generate_input_inx_files()
generate_output_inx_files()
generate_extension_inx_files()
+ generate_info_inx_files()
diff --git a/lib/inx/info.py b/lib/inx/info.py
new file mode 100755
index 00000000..f391b546
--- /dev/null
+++ b/lib/inx/info.py
@@ -0,0 +1,9 @@
+from .utils import build_environment, write_inx_file
+
+
+def generate_info_inx_files():
+ env = build_environment()
+ info_inx_files = ['about', 'embroider']
+ for info in info_inx_files:
+ template = env.get_template('%s.xml' % info)
+ write_inx_file(info, template.render())
diff --git a/lib/inx/inputs.py b/lib/inx/inputs.py
index d40ffeaf..b50ec9f9 100755
--- a/lib/inx/inputs.py
+++ b/lib/inx/inputs.py
@@ -11,7 +11,7 @@ def pyembroidery_input_formats():
def generate_input_inx_files():
env = build_environment()
- template = env.get_template('input.inx')
+ template = env.get_template('input.xml')
for format, description in pyembroidery_input_formats():
name = "input_%s" % format.upper()
diff --git a/lib/inx/outputs.py b/lib/inx/outputs.py
index aef0c8b5..ccb323c7 100644
--- a/lib/inx/outputs.py
+++ b/lib/inx/outputs.py
@@ -5,14 +5,17 @@ from .utils import build_environment, write_inx_file
def pyembroidery_output_formats():
for format in pyembroidery.supported_formats():
- if 'writer' in format and format['category'] == 'embroidery':
- yield format['extension'], format['description']
+ if 'writer' in format:
+ description = format['description']
+ if format['category'] != "embroidery":
+ description = "%s [DEBUG]" % description
+ yield format['extension'], description, format['mimetype'], format['category']
def generate_output_inx_files():
env = build_environment()
- template = env.get_template('output.inx')
+ template = env.get_template('output.xml')
- for format, description in pyembroidery_output_formats():
+ for format, description, mimetype, category in pyembroidery_output_formats():
name = "output_%s" % format.upper()
- write_inx_file(name, template.render(format=format, description=description))
+ write_inx_file(name, template.render(format=format, mimetype=mimetype, description=description))
diff --git a/lib/inx/utils.py b/lib/inx/utils.py
index a7c98a60..2fb6b21b 100644
--- a/lib/inx/utils.py
+++ b/lib/inx/utils.py
@@ -6,11 +6,13 @@ from os.path import dirname
from jinja2 import Environment, FileSystemLoader
-from ..i18n import N_, locale_dir, translation as default_translation
+from ..i18n import N_, locale_dir
+from ..i18n import translation as default_translation
_top_path = dirname(dirname(dirname(os.path.realpath(__file__))))
inx_path = os.path.join(_top_path, "inx")
template_path = os.path.join(_top_path, "templates")
+version_path = _top_path
current_translation = default_translation
current_locale = "en_US"
@@ -26,16 +28,29 @@ def build_environment():
env.install_gettext_translations(current_translation)
env.globals["locale"] = current_locale
+ with open(os.path.join(version_path, 'LICENSE'), 'r') as license:
+ env.globals["inkstitch_license"] = "".join(license.readlines())
+
if "BUILD" in os.environ:
# building a ZIP release, with inkstitch packaged as a binary
+ # About extension: add version information
+ with open(os.path.join(version_path, 'VERSION'), 'r') as version:
+ env.globals["inkstitch_version"] = "%s %s" % (version.readline(), current_locale)
+ # Command tag and icons path
if sys.platform == "win32":
- env.globals["command_tag"] = '<command reldir="extensions">inkstitch/bin/inkstitch.exe</command>'
+ env.globals["command_tag"] = '<command location="inx">inkstitch/bin/inkstitch.exe</command>'
+ env.globals["image_path"] = 'inkstitch/bin/icons/'
+ elif sys.platform == "darwin":
+ env.globals["command_tag"] = '<command location="inx">inkstitch.app/Contents/MacOS/inkstitch</command>'
+ env.globals["image_path"] = 'inkstitch.app/Contents/MacOS/icons/'
else:
- env.globals["command_tag"] = '<command reldir="extensions">inkstitch/bin/inkstitch</command>'
+ env.globals["command_tag"] = '<command location="inx">inkstitch/bin/inkstitch</command>'
+ env.globals["image_path"] = 'inkstitch/bin/icons/'
else:
# user is running inkstitch.py directly as a developer
- env.globals["command_tag"] = '<command reldir="extensions" interpreter="python">inkstitch.py</command>'
-
+ env.globals["command_tag"] = '<command location="inx" interpreter="python">../../inkstitch.py</command>'
+ env.globals["image_path"] = '../../icons/'
+ env.globals["inkstitch_version"] = "Manual Install"
return env
@@ -49,8 +64,8 @@ def write_inx_file(name, contents):
raise
inx_file_name = "inkstitch_%s.inx" % name
- with open(os.path.join(inx_locale_dir, inx_file_name), 'w') as inx_file:
- print >> inx_file, contents.encode("utf-8")
+ with open(os.path.join(inx_locale_dir, inx_file_name), 'w', encoding="utf-8") as inx_file:
+ print(contents, file=inx_file)
def iterate_inx_locales():
@@ -64,7 +79,7 @@ def iterate_inx_locales():
# generate menu items for this language in Inkscape's "Extensions"
# menu.
magic_string = N_("Generate INX files")
- translated_magic_string = translation.ugettext(magic_string)
+ translated_magic_string = translation.gettext(magic_string)
if translated_magic_string != magic_string or locale == "en_US":
current_translation = translation
diff --git a/lib/lettering/__init__.py b/lib/lettering/__init__.py
index 5d20d683..5a9d345c 100644
--- a/lib/lettering/__init__.py
+++ b/lib/lettering/__init__.py
@@ -1 +1 @@
-from font import Font, FontError \ No newline at end of file
+from .font import Font, FontError
diff --git a/lib/lettering/font.py b/lib/lettering/font.py
index 0974a1cf..3ef99d47 100644
--- a/lib/lettering/font.py
+++ b/lib/lettering/font.py
@@ -1,10 +1,9 @@
-# -*- coding: UTF-8 -*-
-
import json
import os
from copy import deepcopy
-import inkex
+from inkex import styles
+from lxml import etree
from ..elements import nodes_to_elements
from ..exceptions import InkstitchException
@@ -80,14 +79,14 @@ class Font(object):
def _load_metadata(self):
try:
- with open(os.path.join(self.path, "font.json")) as metadata_file:
+ with open(os.path.join(self.path, "font.json"), encoding="utf-8") as metadata_file:
self.metadata = json.load(metadata_file)
except IOError:
pass
def _load_license(self):
try:
- with open(os.path.join(self.path, "LICENSE")) as license_file:
+ with open(os.path.join(self.path, "LICENSE"), encoding="utf-8") as license_file:
self.license = license_file.read()
except IOError:
pass
@@ -101,13 +100,9 @@ class Font(object):
# we'll deal with missing variants when we apply lettering
pass
- def _check_variants(self):
- if self.variants.get(self.default_variant) is None:
- raise FontError("font not found or has no default variant")
-
name = localized_font_metadata('name', '')
description = localized_font_metadata('description', '')
- default_glyph = font_metadata('default_glyph', u"�")
+ default_glyph = font_metadata('defalt_glyph', "�")
leading = font_metadata('leading', 5, multiplier=PIXELS_PER_MM)
kerning_pairs = font_metadata('kerning_pairs', {})
auto_satin = font_metadata('auto_satin', True)
@@ -149,10 +144,13 @@ class Font(object):
return None
def has_variants(self):
+ # returns available variants
font_variants = []
for variant in FontVariant.VARIANT_TYPES:
if os.path.isfile(os.path.join(self.path, "%s.svg" % variant)):
font_variants.append(variant)
+ if not font_variants:
+ raise FontError(_("The font '%s' has no variants.") % self.name)
return font_variants
def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim=False):
@@ -182,12 +180,23 @@ class Font(object):
if self.auto_satin and len(destination_group) > 0:
self._apply_auto_satin(destination_group, trim)
- else:
- # set stroke width because it is almost invisible otherwise (why?)
- for element in destination_group.iterdescendants(SVG_PATH_TAG):
- style = ['stroke-width:1px' if s.startswith('stroke-width') else s for s in element.get('style').split(';')]
- style = ';'.join(style)
- element.set('style', '%s' % style)
+
+ # make sure font stroke styles have always a similar look
+ for element in destination_group.iterdescendants(SVG_PATH_TAG):
+ dash_array = ""
+ stroke_width = ""
+ style = styles.Style(element.get('style'))
+
+ if style.get('fill') == 'none':
+ stroke_width = ";stroke-width:1px"
+ if style.get('stroke-width'):
+ style.pop('stroke-width')
+
+ if style.get('stroke-dasharray') and style.get('stroke-dasharray') != 'none':
+ stroke_width = ";stroke-width:0.5px"
+ dash_array = ";stroke-dasharray:3, 1"
+
+ element.set('style', '%s%s%s' % (style.to_str(), stroke_width, dash_array))
return destination_group
@@ -209,7 +218,8 @@ class Font(object):
Returns:
An svg:g element containing the rendered text.
"""
- group = inkex.etree.Element(SVG_GROUP_TAG, {
+
+ group = etree.Element(SVG_GROUP_TAG, {
INKSCAPE_LABEL: line
})
diff --git a/lib/lettering/font_variant.py b/lib/lettering/font_variant.py
index 7c9fa1c0..2071b2cb 100644
--- a/lib/lettering/font_variant.py
+++ b/lib/lettering/font_variant.py
@@ -1,9 +1,7 @@
-# -*- coding: UTF-8 -*-
-
import os
import inkex
-import simplestyle
+from lxml import etree
from ..svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL
from .glyph import Glyph
@@ -26,10 +24,10 @@ class FontVariant(object):
# We use unicode characters rather than English strings for font file names
# in order to be more approachable for languages other than English.
- LEFT_TO_RIGHT = u"→"
- RIGHT_TO_LEFT = u"←"
- TOP_TO_BOTTOM = u"↓"
- BOTTOM_TO_TOP = u"↑"
+ LEFT_TO_RIGHT = "→"
+ RIGHT_TO_LEFT = "←"
+ TOP_TO_BOTTOM = "↓"
+ BOTTOM_TO_TOP = "↑"
VARIANT_TYPES = (LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP)
@classmethod
@@ -57,9 +55,9 @@ class FontVariant(object):
self._load_glyphs()
def _load_glyphs(self):
- svg_path = os.path.join(self.path, u"%s.svg" % self.variant)
- with open(svg_path) as svg_file:
- svg = inkex.etree.parse(svg_file)
+ svg_path = os.path.join(self.path, "%s.svg" % self.variant)
+ with open(svg_path, encoding="utf-8") as svg_file:
+ svg = etree.parse(svg_file)
glyph_layers = svg.xpath(".//svg:g[starts-with(@inkscape:label, 'GlyphLayer-')]", namespaces=inkex.NSS)
for layer in glyph_layers:
@@ -78,9 +76,9 @@ class FontVariant(object):
if style_text:
# The layer may be marked invisible, so we'll clear the 'display'
# style.
- style = simplestyle.parseStyle(group.get('style'))
+ style = dict(inkex.Style.parse_str(group.get('style')))
style.pop('display')
- group.set('style', simplestyle.formatStyle(style))
+ group.set('style', str(inkex.Style(style)))
def __getitem__(self, character):
if character in self.glyphs:
diff --git a/lib/lettering/glyph.py b/lib/lettering/glyph.py
index 061a930c..84517474 100644
--- a/lib/lettering/glyph.py
+++ b/lib/lettering/glyph.py
@@ -1,9 +1,9 @@
from copy import copy
-import cubicsuperpath
-import simpletransform
+from inkex import paths, transforms
-from ..svg import apply_transforms, get_guides
+from ..svg import get_guides
+from ..svg.path import get_correction_transform
from ..svg.tags import SVG_GROUP_TAG, SVG_PATH_TAG
@@ -37,8 +37,9 @@ class Glyph(object):
def _process_group(self, group):
new_group = copy(group)
- new_group.attrib.pop('transform', None)
- del new_group[:] # delete references to the original group's children
+ # new_group.attrib.pop('transform', None)
+ # delete references to the original group's children
+ del new_group[:]
for node in group:
if node.tag == SVG_GROUP_TAG:
@@ -47,11 +48,9 @@ class Glyph(object):
node_copy = copy(node)
if "d" in node.attrib:
- # Convert the path to absolute coordinates, incorporating all
- # nested transforms.
- path = cubicsuperpath.parsePath(node.get("d"))
- apply_transforms(path, node)
- node_copy.set("d", cubicsuperpath.formatPath(path))
+ transform = -transforms.Transform(get_correction_transform(node, True))
+ path = paths.Path(node.get("d")).transform(transform).to_absolute()
+ node_copy.set("d", str(path))
# Delete transforms from paths and groups, since we applied
# them to the paths already.
@@ -71,16 +70,18 @@ class Glyph(object):
self.baseline = 0
def _process_bbox(self):
- left, right, top, bottom = simpletransform.computeBBox(self.node.iterdescendants())
-
+ bbox = [paths.Path(node.get("d")).bounding_box() for node in self.node.iterdescendants(SVG_PATH_TAG)]
+ left, right = min([box.left for box in bbox]), max([box.right for box in bbox])
self.width = right - left
self.min_x = left
def _move_to_origin(self):
translate_x = -self.min_x
translate_y = -self.baseline
- transform = "translate(%s, %s)" % (translate_x, translate_y)
+ transform = transforms.Transform("translate(%s, %s)" % (translate_x, translate_y))
for node in self.node.iter(SVG_PATH_TAG):
- node.set('transform', transform)
- simpletransform.fuseTransform(node)
+ path = paths.Path(node.get("d"))
+ path = path.transform(transform)
+ node.set('d', str(path))
+ node.attrib.pop('transform', None)
diff --git a/lib/lettering/kerning.py b/lib/lettering/kerning.py
new file mode 100644
index 00000000..920e7d59
--- /dev/null
+++ b/lib/lettering/kerning.py
@@ -0,0 +1,69 @@
+from inkex import NSS
+from lxml import etree
+
+
+class FontKerning(object):
+ """
+ This class reads kerning information from an SVG file
+ """
+ def __init__(self, path):
+ with open(path) as svg:
+ self.svg = etree.parse(svg)
+
+ # horiz_adv_x defines the wdith of specific letters (distance to next letter)
+ def horiz_adv_x(self):
+ # In XPath 2.0 we could use ".//svg:glyph/(@unicode|@horiz-adv-x)"
+ xpath = ".//svg:glyph[@unicode and @horiz-adv-x]/@*[name()='unicode' or name()='horiz-adv-x']"
+ hax = self.svg.xpath(xpath, namespaces=NSS)
+ if len(hax) == 0:
+ return {}
+ return dict(zip(hax[0::2], [int(x) for x in hax[1::2]]))
+
+ # kerning (specific distances of two specified letters)
+ def hkern(self):
+ xpath = ".//svg:hkern[(@u1 or @g1) and (@u1 or @g1) and @k]/@*[contains(name(), '1') or contains(name(), '2') or name()='k']"
+ hkern = self.svg.xpath(xpath, namespaces=NSS)
+ for index, glyph in enumerate(hkern):
+ # fontTools.agl will import fontTools.misc.py23 which will output a deprecation warning
+ # ignore the warning for now - until the library fixed it
+ if index == 0:
+ import warnings
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ from fontTools.agl import toUnicode
+ if len(glyph) > 1 and not (index + 1) % 3 == 0:
+ glyph_names = glyph.split(",")
+ # the glyph name is written in various languages, second is english. Let's look it up.
+ if len(glyph_names) == 1:
+ hkern[index] = toUnicode(glyph)
+ else:
+ hkern[index] = toUnicode(glyph_names[1])
+ k = [int(x) for x in hkern[2::3]]
+ u = [k + v for k, v in zip(hkern[0::3], hkern[1::3])]
+ hkern = dict(zip(u, k))
+ return hkern
+
+ # the space character
+ def word_spacing(self):
+ xpath = "string(.//svg:glyph[@glyph-name='space'][1]/@*[name()='horiz-adv-x'])"
+ word_spacing = self.svg.xpath(xpath, namespaces=NSS) or 26
+ return int(word_spacing)
+
+ # default letter spacing
+ def letter_spacing(self):
+ xpath = "string(.//svg:font[@horiz-adv-x][1]/@*[name()='horiz-adv-x'])"
+ letter_spacing = self.svg.xpath(xpath, namespaces=NSS) or 0
+ return int(letter_spacing)
+
+ # this value will be saved into the json file to preserve it for later font edits
+ # additionally it serves to automatically define the line height (leading)
+ def units_per_em(self, default=100):
+ xpath = "string(.//svg:font-face[@units-per-em][1]/@*[name()='units-per-em'])"
+ units_per_em = self.svg.xpath(xpath, namespaces=NSS) or default
+ return int(units_per_em)
+
+ """
+ def missing_glyph_spacing(self):
+ xpath = "string(.//svg:missing-glyph/@*[name()='horiz-adv-x'])"
+ return float(self.svg.xpath(xpath, namespaces=NSS))
+ """
diff --git a/lib/output.py b/lib/output.py
index fbcdea6c..60579801 100644
--- a/lib/output.py
+++ b/lib/output.py
@@ -1,4 +1,5 @@
import sys
+import inkex
import pyembroidery
@@ -27,7 +28,8 @@ def _string_to_floats(string):
return [float(num) for num in floats]
-def get_origin(svg, (minx, miny, maxx, maxy)):
+def get_origin(svg, xxx_todo_changeme):
+ (minx, miny, maxx, maxy) = xxx_todo_changeme
origin_command = global_command(svg, "origin")
if origin_command:
@@ -91,5 +93,6 @@ def write_embroidery_file(file_path, stitch_plan, svg, settings={}):
except IOError as e:
# L10N low-level file error. %(error)s is (hopefully?) translated by
# the user's system automatically.
- print >> sys.stderr, _("Error writing to %(path)s: %(error)s") % dict(path=file_path, error=e.strerror)
+ msg = _("Error writing to %(path)s: %(error)s") % dict(path=file_path, error=e.strerror)
+ inkex.errormsg(msg)
sys.exit(1)
diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py
index 0b2f75cd..de66cb10 100644
--- a/lib/stitch_plan/stitch_plan.py
+++ b/lib/stitch_plan/stitch_plan.py
@@ -5,7 +5,8 @@ from .stitch import Stitch
from .ties import add_ties
-def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM, disable_ties=False):
+def patches_to_stitch_plan(patches, collapse_len=None, disable_ties=False):
+
"""Convert a collection of inkstitch.element.Patch objects to a StitchPlan.
* applies instructions embedded in the Patch such as trim_after and stop_after
@@ -13,6 +14,7 @@ def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM, disable_ti
* adds jump-stitches between patches if necessary
"""
+ collapse_len = (collapse_len or 3.0) * PIXELS_PER_MM
stitch_plan = StitchPlan()
color_block = stitch_plan.new_color_block(color=patches[0].color)
diff --git a/lib/stitches/__init__.py b/lib/stitches/__init__.py
index e052d144..12c636a6 100644
--- a/lib/stitches/__init__.py
+++ b/lib/stitches/__init__.py
@@ -1,6 +1,6 @@
-from running_stitch import *
-from auto_fill import auto_fill
-from fill import legacy_fill
+from .auto_fill import auto_fill
+from .fill import legacy_fill
+from .running_stitch import *
# Can't put this here because we get a circular import :(
#from auto_satin import auto_satin
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index b5157a5a..485f51e5 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -8,11 +8,12 @@ from shapely import geometry as shgeo
from shapely.ops import snap
from shapely.strtree import STRtree
-from .fill import intersect_region_with_grating, stitch_row
-from .running_stitch import running_stitch
from ..debug import debug
from ..svg import PIXELS_PER_MM
-from ..utils.geometry import Point as InkstitchPoint, line_string_to_point_list
+from ..utils.geometry import Point as InkstitchPoint
+from ..utils.geometry import line_string_to_point_list
+from .fill import intersect_region_with_grating, stitch_row
+from .running_stitch import running_stitch
class PathEdge(object):
@@ -82,7 +83,7 @@ def which_outline(shape, coords):
point = shgeo.Point(*coords)
outlines = list(shape.boundary)
- outline_indices = range(len(outlines))
+ outline_indices = list(range(len(outlines)))
closest = min(outline_indices, key=lambda index: outlines[index].distance(point))
return closest
@@ -175,7 +176,7 @@ def insert_node(graph, shape, point):
if key == "outline":
edges.append(((start, end), data))
- edge, data = min(edges, key=lambda (edge, data): shgeo.LineString(edge).distance(projected_point))
+ edge, data = min(edges, key=lambda edge_data: shgeo.LineString(edge_data[0]).distance(projected_point))
graph.remove_edge(*edge, key="outline")
graph.add_edge(edge[0], node, key="outline", **data)
@@ -204,7 +205,7 @@ def add_boundary_travel_nodes(graph, shape):
if length > 1:
# Just plot a point every pixel, that should be plenty of
# resolution. A pixel is around a quarter of a millimeter.
- for i in xrange(1, int(length)):
+ for i in range(1, int(length)):
subpoint = segment.interpolate(i)
graph.add_node((subpoint.x, subpoint.y), projection=outline.project(subpoint), outline=outline_index)
@@ -480,7 +481,7 @@ def find_stitch_path(graph, travel_graph, starting_point=None, ending_point=None
graph = graph.copy()
if not starting_point:
- starting_point = graph.nodes.keys()[0]
+ starting_point = list(graph.nodes.keys())[0]
starting_node = nearest_node(graph, starting_point)
diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py
index 112c714b..5ce91a49 100644
--- a/lib/stitches/auto_satin.py
+++ b/lib/stitches/auto_satin.py
@@ -1,14 +1,12 @@
import math
-from itertools import chain, izip
+from itertools import chain
+import inkex
import networkx as nx
+from lxml import etree
from shapely import geometry as shgeo
from shapely.geometry import Point as ShapelyPoint
-import cubicsuperpath
-import inkex
-import simplestyle
-
from ..commands import add_commands
from ..elements import SatinColumn, Stroke
from ..i18n import _
@@ -87,7 +85,7 @@ class SatinSegment(object):
num_segments = int(math.ceil(self.center_line.length / segment_size))
segments = []
- for i in xrange(num_segments):
+ for i in range(num_segments):
segments.append(SatinSegment(self.satin,
float(i) / num_segments,
float(i + 1) / num_segments,
@@ -216,13 +214,13 @@ class RunningStitch(object):
original_element.node.get(INKSTITCH_ATTRIBS['contour_underlay_stitch_length_mm'], '')
def to_element(self):
- node = inkex.etree.Element(SVG_PATH_TAG)
- node.set("d", cubicsuperpath.formatPath(
- line_strings_to_csp([self.path])))
+ node = etree.Element(SVG_PATH_TAG)
+ d = str(inkex.paths.CubicSuperPath(line_strings_to_csp([self.path])))
+ node.set("d", d)
style = self.original_element.parse_style()
style['stroke-dasharray'] = "0.5,0.5"
- style = simplestyle.formatStyle(style)
+ style = str(inkex.Style(style))
node.set("style", style)
node.set(INKSTITCH_ATTRIBS['running_stitch_length_mm'], self.running_stitch_length)
@@ -445,8 +443,9 @@ def add_jumps(graph, elements, preserve_order):
point2 = graph.nodes[node2]['point']
potential_edges.append((point1, point2))
- edge = min(potential_edges, key=lambda (p1, p2): p1.distance(p2))
- graph.add_edge(str(edge[0]), str(edge[1]), jump=True)
+ if potential_edges:
+ edge = min(potential_edges, key=lambda p1_p2: p1_p2[0].distance(p1_p2[1]))
+ graph.add_edge(str(edge[0]), str(edge[1]), jump=True)
else:
# networkx makes this super-easy! k_edge_agumentation tells us what edges
# we need to add to ensure that the graph is fully connected. We give it a
@@ -497,7 +496,7 @@ def find_path(graph, starting_node, ending_node):
# forth on each satin twice due to the satin edges being in the graph
# twice (forward and reverse).
graph = nx.Graph(graph)
- graph.remove_edges_from(zip(path[:-1], path[1:]))
+ graph.remove_edges_from(list(zip(path[:-1], path[1:])))
final_path = []
prev = None
@@ -643,14 +642,14 @@ def preserve_original_groups(elements, original_parent_nodes):
to the group that contained the original SatinColumn that spawned it.
"""
- for element, parent in izip(elements, original_parent_nodes):
+ for element, parent in zip(elements, original_parent_nodes):
if parent is not None:
parent.append(element.node)
element.node.set('transform', get_correction_transform(parent, child=True))
def create_new_group(parent, insert_index):
- group = inkex.etree.Element(SVG_GROUP_TAG, {
+ group = etree.Element(SVG_GROUP_TAG, {
INKSCAPE_LABEL: _("Auto-Satin"),
"transform": get_correction_transform(parent, child=True)
})
diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py
index 924f64f6..a19e080b 100644
--- a/lib/stitches/fill.py
+++ b/lib/stitches/fill.py
@@ -3,7 +3,8 @@ import math
import shapely
from ..svg import PIXELS_PER_MM
-from ..utils import cache, Point as InkstitchPoint
+from ..utils import Point as InkstitchPoint
+from ..utils import cache
def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last):
@@ -223,7 +224,7 @@ def pull_runs(rows, shape, row_spacing):
run = []
prev = None
- for row_num in xrange(len(rows)):
+ for row_num in range(len(rows)):
row = rows[row_num]
first, rest = row[0], row[1:]
diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py
index 0bb8fc7d..57a03865 100644
--- a/lib/stitches/running_stitch.py
+++ b/lib/stitches/running_stitch.py
@@ -92,7 +92,7 @@ def bean_stitch(stitches, repeats):
for stitch in stitches:
new_stitches.append(stitch)
- for i in xrange(repeats):
+ for i in range(repeats):
new_stitches.extend(copy(new_stitches[-2:]))
return new_stitches
diff --git a/lib/svg/guides.py b/lib/svg/guides.py
index 3e26a90d..255b3e6a 100644
--- a/lib/svg/guides.py
+++ b/lib/svg/guides.py
@@ -1,7 +1,7 @@
-import simpletransform
+from inkex import transforms
-from ..utils import string_to_floats, Point, cache
-from .tags import SODIPODI_NAMEDVIEW, SODIPODI_GUIDE, INKSCAPE_LABEL
+from ..utils import Point, cache, string_to_floats
+from .tags import INKSCAPE_LABEL, SODIPODI_GUIDE, SODIPODI_NAMEDVIEW
from .units import get_doc_size, get_viewbox_transform
@@ -19,7 +19,7 @@ class InkscapeGuide(object):
# convert the size from viewbox-relative to real-world pixels
viewbox_transform = get_viewbox_transform(self.svg)
- simpletransform.applyTransformToPoint(simpletransform.invertTransform(viewbox_transform), doc_size)
+ viewbox_transform = transforms.Transform(-transforms.Transform(viewbox_transform)).apply_to_point(doc_size)
self.position = Point(*string_to_floats(self.node.get('position')))
diff --git a/lib/svg/path.py b/lib/svg/path.py
index cc4b8cbb..baa93443 100644
--- a/lib/svg/path.py
+++ b/lib/svg/path.py
@@ -1,8 +1,7 @@
-import cubicsuperpath
import inkex
-import simpletransform
-from tags import SVG_GROUP_TAG, SVG_LINK_TAG
+from lxml import etree
+from .tags import SVG_GROUP_TAG, SVG_LINK_TAG
from .units import get_viewbox_transform
@@ -10,7 +9,7 @@ def apply_transforms(path, node):
transform = get_node_transform(node)
# apply the combined transform to this node's path
- simpletransform.applyTransformToPath(transform, path)
+ path = path.transform(transform)
return path
@@ -21,7 +20,7 @@ def compose_parent_transforms(node, mat):
trans = node.get('transform')
if trans:
- mat = simpletransform.composeTransform(simpletransform.parseTransform(trans), mat)
+ mat = inkex.transforms.Transform(trans) * mat
if node.getparent() is not None:
if node.getparent().tag in [SVG_GROUP_TAG, SVG_LINK_TAG]:
mat = compose_parent_transforms(node.getparent(), mat)
@@ -29,20 +28,22 @@ def compose_parent_transforms(node, mat):
def get_node_transform(node):
+ """
+ if getattr(node, "composed_transform", None):
+ return node.composed_transform()
+ """
+
# start with the identity transform
- transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+ transform = inkex.transforms.Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
# this if is because sometimes inkscape likes to create paths outside of a layer?!
if node.getparent() is not None:
# combine this node's transform with all parent groups' transforms
transform = compose_parent_transforms(node, transform)
- if node.get('id', '').startswith('clone_'):
- transform = simpletransform.parseTransform(node.get('transform', ''))
-
# add in the transform implied by the viewBox
viewbox_transform = get_viewbox_transform(node.getroottree().getroot())
- transform = simpletransform.composeTransform(viewbox_transform, transform)
+ transform = viewbox_transform * transform
return transform
@@ -63,9 +64,9 @@ def get_correction_transform(node, child=False):
# now invert it, so that we can position our objects in absolute
# coordinates
- transform = simpletransform.invertTransform(transform)
+ transform = -transform
- return simpletransform.formatTransform(transform)
+ return str(transform)
def line_strings_to_csp(line_strings):
@@ -90,6 +91,6 @@ def point_lists_to_csp(point_lists):
def line_strings_to_path(line_strings):
csp = line_strings_to_csp(line_strings)
- return inkex.etree.Element("path", {
- "d": cubicsuperpath.formatPath(csp)
+ return etree.Element("path", {
+ "d": str(inkex.paths.CubicSuperPath(csp))
})
diff --git a/lib/svg/rendering.py b/lib/svg/rendering.py
index 5860ceef..ced7d4f1 100644
--- a/lib/svg/rendering.py
+++ b/lib/svg/rendering.py
@@ -1,9 +1,8 @@
import math
import inkex
-import simplepath
-import simplestyle
-import simpletransform
+from lxml import etree
+from math import pi
from ..i18n import _
from ..utils import Point, cache
@@ -116,24 +115,25 @@ def realistic_stitch(start, end):
start = Point(*start)
stitch_length = (end - start).length()
- stitch_center = (end + start) / 2.0
+ stitch_center = Point((end.x+start.x)/2.0, (end[1]+start[1])/2.0)
stitch_direction = (end - start)
- stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x)
+ stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x) * (180 / pi)
stitch_length = max(0, stitch_length - 0.2 * PIXELS_PER_MM)
# create the path by filling in the length in the template
- path = simplepath.parsePath(stitch_path % stitch_length)
+ path = inkex.Path(stitch_path % stitch_length).to_arrays()
# rotate the path to match the stitch
rotation_center_x = -stitch_length / 2.0
rotation_center_y = stitch_height / 2.0
- simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y)
+
+ path = inkex.Path(path).rotate(stitch_angle, (rotation_center_x, rotation_center_y))
# move the path to the location of the stitch
- simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y)
+ path = inkex.Path(path).translate(stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y)
- return simplepath.formatPath(path)
+ return str(path)
def color_block_to_point_lists(color_block):
@@ -159,10 +159,9 @@ def get_correction_transform(svg):
transform = get_viewbox_transform(svg)
# we need to correct for the viewbox
- transform = simpletransform.invertTransform(transform)
- transform = simpletransform.formatTransform(transform)
+ transform = -inkex.transforms.Transform(transform)
- return transform
+ return str(transform)
def color_block_to_realistic_stitches(color_block, svg, destination):
@@ -170,12 +169,8 @@ def color_block_to_realistic_stitches(color_block, svg, destination):
color = color_block.color.visible_on_white.darker.to_hex_str()
start = point_list[0]
for point in point_list[1:]:
- destination.append(inkex.etree.Element(SVG_PATH_TAG, {
- 'style': simplestyle.formatStyle({
- 'fill': color,
- 'stroke': 'none',
- 'filter': 'url(#realistic-stitch-filter)'
- }),
+ destination.append(etree.Element(SVG_PATH_TAG, {
+ 'style': "fill: %s; stroke: none; filter: url(#realistic-stitch-filter);" % color,
'd': realistic_stitch(start, point),
'transform': get_correction_transform(svg)
}))
@@ -200,12 +195,8 @@ def color_block_to_paths(color_block, svg, destination, visual_commands):
color = color_block.color.visible_on_white.to_hex_str()
- path = inkex.etree.Element(SVG_PATH_TAG, {
- 'style': simplestyle.formatStyle({
- 'stroke': color,
- 'stroke-width': "0.4",
- 'fill': 'none'
- }),
+ path = etree.Element(SVG_PATH_TAG, {
+ 'style': "stroke: %s; stroke-width: 0.4; fill: none;" % color,
'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list),
'transform': get_correction_transform(svg),
INKSTITCH_ATTRIBS['manual_stitch']: 'true'
@@ -223,10 +214,10 @@ def color_block_to_paths(color_block, svg, destination, visual_commands):
def render_stitch_plan(svg, stitch_plan, realistic=False, visual_commands=True):
layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']")
if layer is None:
- layer = inkex.etree.Element(SVG_GROUP_TAG,
- {'id': '__inkstitch_stitch_plan__',
- INKSCAPE_LABEL: _('Stitch Plan'),
- INKSCAPE_GROUPMODE: 'layer'})
+ layer = etree.Element(SVG_GROUP_TAG,
+ {'id': '__inkstitch_stitch_plan__',
+ INKSCAPE_LABEL: _('Stitch Plan'),
+ INKSCAPE_GROUPMODE: 'layer'})
else:
# delete old stitch plan
del layer[:]
@@ -237,10 +228,10 @@ def render_stitch_plan(svg, stitch_plan, realistic=False, visual_commands=True):
svg.append(layer)
for i, color_block in enumerate(stitch_plan):
- group = inkex.etree.SubElement(layer,
- SVG_GROUP_TAG,
- {'id': '__color_block_%d__' % i,
- INKSCAPE_LABEL: "color block %d" % (i + 1)})
+ group = etree.SubElement(layer,
+ SVG_GROUP_TAG,
+ {'id': '__color_block_%d__' % i,
+ INKSCAPE_LABEL: "color block %d" % (i + 1)})
if realistic:
color_block_to_realistic_stitches(color_block, svg, group)
else:
@@ -250,6 +241,6 @@ def render_stitch_plan(svg, stitch_plan, realistic=False, visual_commands=True):
defs = svg.find(SVG_DEFS_TAG)
if defs is None:
- defs = inkex.etree.SubElement(svg, SVG_DEFS_TAG)
+ defs = etree.SubElement(svg, SVG_DEFS_TAG)
- defs.append(inkex.etree.fromstring(realistic_filter))
+ defs.append(etree.fromstring(realistic_filter))
diff --git a/lib/svg/svg.py b/lib/svg/svg.py
index 3cf7f017..1a6b10e8 100644
--- a/lib/svg/svg.py
+++ b/lib/svg/svg.py
@@ -1,4 +1,5 @@
-from inkex import NSS, etree
+from inkex import NSS
+from lxml import etree
from ..utils import cache
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index 810924a6..6fa47aa0 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -1,10 +1,9 @@
import inkex
+from lxml import etree
-
-# This is used below and added to the document in ../extensions/base.py.
+etree.register_namespace("inkstitch", "http://inkstitch.org/namespace")
inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace'
-
SVG_PATH_TAG = inkex.addNS('path', 'svg')
SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg')
SVG_RECT_TAG = inkex.addNS('rect', 'svg')
diff --git a/lib/svg/units.py b/lib/svg/units.py
index 319e018b..6f16d7fb 100644
--- a/lib/svg/units.py
+++ b/lib/svg/units.py
@@ -1,4 +1,4 @@
-import simpletransform
+import inkex
from ..i18n import _
from ..utils import cache
@@ -6,10 +6,14 @@ from ..utils import cache
# modern versions of Inkscape use 96 pixels per inch as per the CSS standard
PIXELS_PER_MM = 96 / 25.4
-# cribbed from inkscape-silhouette
-
def parse_length_with_units(str):
+ value, unit = inkex.units.parse_unit(str)
+ if not unit:
+ raise ValueError(_("parseLengthWithUnits: unknown unit %s") % str)
+ return value, unit
+
+ """
'''
Parse an SVG value which may or may not have units attached
This version is greatly simplified in that it only allows: no units,
@@ -18,6 +22,8 @@ def parse_length_with_units(str):
generality is ever needed.
'''
+ # cribbed from inkscape-silhouette
+
u = 'px'
s = str.strip()
if s[-2:] == 'px':
@@ -46,11 +52,15 @@ def parse_length_with_units(str):
raise ValueError(_("parseLengthWithUnits: unknown unit %s") % s)
return v, u
+ """
def convert_length(length):
value, units = parse_length_with_units(length)
+ return inkex.units.convert_unit(str(value) + units, 'px')
+
+ """
if not units or units == "px":
return value
@@ -67,7 +77,7 @@ def convert_length(length):
units = 'mm'
if units == 'mm':
- value = value / 25.4
+ value /= 25.4
units = 'in'
if units == 'in':
@@ -76,6 +86,7 @@ def convert_length(length):
return value * 96
raise ValueError(_("Unknown unit: %s") % units)
+ """
@cache
@@ -121,7 +132,7 @@ def get_viewbox_transform(node):
dx = -float(viewbox[0])
dy = -float(viewbox[1])
- transform = simpletransform.parseTransform("translate(%f, %f)" % (dx, dy))
+ transform = inkex.transforms.Transform("translate(%f, %f)" % (dx, dy))
try:
sx = doc_width / float(viewbox[2])
@@ -132,8 +143,8 @@ def get_viewbox_transform(node):
if aspect_ratio != 'none':
sx = sy = max(sx, sy) if 'slice' in aspect_ratio else min(sx, sy)
- scale_transform = simpletransform.parseTransform("scale(%f, %f)" % (sx, sy))
- transform = simpletransform.composeTransform(transform, scale_transform)
+ scale_transform = inkex.transforms.Transform("scale(%f, %f)" % (sx, sy))
+ transform = transform * scale_transform
except ZeroDivisionError:
pass
diff --git a/lib/threads/__init__.py b/lib/threads/__init__.py
index 03cd777b..1081ea14 100644
--- a/lib/threads/__init__.py
+++ b/lib/threads/__init__.py
@@ -1,3 +1,3 @@
-from color import ThreadColor
-from palette import ThreadPalette
-from catalog import ThreadCatalog
+from .catalog import ThreadCatalog
+from .color import ThreadColor
+from .palette import ThreadPalette
diff --git a/lib/threads/catalog.py b/lib/threads/catalog.py
index aba2696d..c91ce227 100644
--- a/lib/threads/catalog.py
+++ b/lib/threads/catalog.py
@@ -1,6 +1,6 @@
import os
import sys
-from collections import Sequence
+from collections.abc import Sequence
from glob import glob
from os.path import dirname, realpath
diff --git a/lib/threads/color.py b/lib/threads/color.py
index e24856a9..32016112 100644
--- a/lib/threads/color.py
+++ b/lib/threads/color.py
@@ -2,8 +2,7 @@ import colorsys
import re
import tinycss2.color3
-
-import simplestyle
+from inkex import Color
from pyembroidery.EmbThread import EmbThread
@@ -19,14 +18,14 @@ class ThreadColor(object):
self.manufacturer = color.brand
self.rgb = (color.get_red(), color.get_green(), color.get_blue())
return
- elif isinstance(color, unicode):
+ elif isinstance(color, str):
self.rgb = tinycss2.color3.parse_color(color)
# remove alpha channel and multiply with 255
self.rgb = tuple(channel * 255.0 for channel in list(self.rgb)[:-1])
elif isinstance(color, (list, tuple)):
self.rgb = tuple(color)
elif self.hex_str_re.match(color):
- self.rgb = simplestyle.parseColor(color)
+ self.rgb = Color.parse_str(color)[1]
else:
raise ValueError("Invalid color: " + repr(color))
@@ -77,7 +76,7 @@ class ThreadColor(object):
@property
def hex_digits(self):
- return "%02X%02X%02X" % self.rgb
+ return "%02X%02X%02X" % tuple([int(x) for x in self.rgb])
@property
def rgb_normalized(self):
diff --git a/lib/threads/palette.py b/lib/threads/palette.py
index d685e5bb..c5e3002c 100644
--- a/lib/threads/palette.py
+++ b/lib/threads/palette.py
@@ -1,7 +1,8 @@
-from collections import Set
-from colormath.color_objects import sRGBColor, LabColor
+from collections.abc import Set
+
from colormath.color_conversions import convert_color
from colormath.color_diff import delta_e_cie1994
+from colormath.color_objects import LabColor, sRGBColor
from .color import ThreadColor
diff --git a/lib/utils/dotdict.py b/lib/utils/dotdict.py
index 76f23697..e946ecd4 100644
--- a/lib/utils/dotdict.py
+++ b/lib/utils/dotdict.py
@@ -13,7 +13,7 @@ class DotDict(dict):
self.dotdictify()
def _dotdictify(self):
- for k, v in self.iteritems():
+ for k, v in self.items():
if isinstance(v, dict):
self[k] = DotDict(v)
diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py
index bae32c32..f7b49407 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -1,6 +1,7 @@
import math
-from shapely.geometry import LineString, Point as ShapelyPoint
+from shapely.geometry import LineString
+from shapely.geometry import Point as ShapelyPoint
def cut(line, distance, normalized=False):
@@ -123,9 +124,6 @@ class Point:
def as_tuple(self):
return (self.x, self.y)
- def __cmp__(self, other):
- return cmp(self.as_tuple(), other.as_tuple())
-
def __getitem__(self, item):
return self.as_tuple()[item]
diff --git a/lib/utils/inkscape.py b/lib/utils/inkscape.py
index a650da69..f89ea447 100644
--- a/lib/utils/inkscape.py
+++ b/lib/utils/inkscape.py
@@ -1,10 +1,10 @@
-from os.path import realpath, expanduser, join as path_join
import sys
+from os.path import expanduser, realpath
def guess_inkscape_config_path():
if getattr(sys, 'frozen', None):
- path = realpath(path_join(sys._MEIPASS, "..", "..", ".."))
+ path = realpath(sys._MEIPASS.split('extensions', 1)[0])
if sys.platform == "win32":
import win32api
diff --git a/lib/utils/io.py b/lib/utils/io.py
index f51f629c..239585f4 100644
--- a/lib/utils/io.py
+++ b/lib/utils/io.py
@@ -1,15 +1,15 @@
import os
import sys
-from cStringIO import StringIO
+from io import StringIO
def save_stderr():
# GTK likes to spam stderr, which inkscape will show in a dialog.
- null = open(os.devnull, 'w')
- sys.stderr_dup = os.dup(sys.stderr.fileno())
- sys.real_stderr = os.fdopen(sys.stderr_dup, 'w')
- os.dup2(null.fileno(), 2)
- sys.stderr = StringIO()
+ with open(os.devnull, 'w') as null:
+ sys.stderr_dup = os.dup(sys.stderr.fileno())
+ sys.real_stderr = os.fdopen(sys.stderr_dup, 'w', encoding='utf-8')
+ os.dup2(null.fileno(), 2)
+ sys.stderr = StringIO()
def restore_stderr():
@@ -22,11 +22,11 @@ def restore_stderr():
def save_stdout():
- null = open(os.devnull, 'w')
- sys.stdout_dup = os.dup(sys.stdout.fileno())
- sys.real_stdout = os.fdopen(sys.stdout_dup, 'w')
- os.dup2(null.fileno(), 1)
- sys.stdout = StringIO()
+ with open(os.devnull, 'w') as null:
+ sys.stdout_dup = os.dup(sys.stdout.fileno())
+ sys.real_stdout = os.fdopen(sys.stdout_dup, 'w')
+ os.dup2(null.fileno(), 1)
+ sys.stdout = StringIO()
def restore_stdout():
diff --git a/lib/utils/version.py b/lib/utils/version.py
new file mode 100644
index 00000000..02eb388b
--- /dev/null
+++ b/lib/utils/version.py
@@ -0,0 +1,17 @@
+import sys
+from os.path import isfile, join, realpath
+
+from ..i18n import _
+
+
+def get_inkstitch_version():
+ if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
+ version = realpath(join(sys._MEIPASS, "..", "VERSION"))
+ else:
+ version = realpath(join(realpath(__file__), "..", "..", "..", 'VERSION'))
+ if isfile(version):
+ with open(version, 'r') as v:
+ inkstitch_version = _("Ink/Stitch Version: %s") % v.readline()
+ else:
+ inkstitch_version = _("Ink/Stitch Version: unkown")
+ return inkstitch_version