summaryrefslogtreecommitdiff
path: root/inkstitch.py
diff options
context:
space:
mode:
Diffstat (limited to 'inkstitch.py')
-rw-r--r--inkstitch.py597
1 files changed, 0 insertions, 597 deletions
diff --git a/inkstitch.py b/inkstitch.py
deleted file mode 100644
index ba120be0..00000000
--- a/inkstitch.py
+++ /dev/null
@@ -1,597 +0,0 @@
-#!/usr/bin/env python
-# http://www.achatina.de/sewing/main/TECHNICL.HTM
-
-import os
-import sys
-import gettext
-from copy import deepcopy
-import math
-import libembroidery
-import inkex
-import simplepath
-import simplestyle
-import simpletransform
-from bezmisc import bezierlength, beziertatlength, bezierpointatt
-from cspsubdiv import cspsubdiv
-import cubicsuperpath
-from shapely import geometry as shgeo
-
-
-try:
- from functools import lru_cache
-except ImportError:
- from backports.functools_lru_cache import lru_cache
-
-# modern versions of Inkscape use 96 pixels per inch as per the CSS standard
-PIXELS_PER_MM = 96 / 25.4
-
-SVG_PATH_TAG = inkex.addNS('path', 'svg')
-SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg')
-SVG_DEFS_TAG = inkex.addNS('defs', 'svg')
-SVG_GROUP_TAG = inkex.addNS('g', 'svg')
-
-EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
-
-dbg = open(os.devnull, "w")
-
-_ = lambda message: message
-
-# simplify use of lru_cache decorator
-def cache(*args, **kwargs):
- return lru_cache(maxsize=None)(*args, **kwargs)
-
-def localize():
- if getattr(sys, 'frozen', False):
- # we are in a pyinstaller installation
- locale_dir = sys._MEIPASS
- else:
- locale_dir = os.path.dirname(__file__)
-
- locale_dir = os.path.join(locale_dir, 'locales')
-
- translation = gettext.translation("inkstitch", locale_dir, fallback=True)
-
- global _
- _ = translation.gettext
-
-localize()
-
-# cribbed from inkscape-silhouette
-def parse_length_with_units( str ):
-
- '''
- Parse an SVG value which may or may not have units attached
- This version is greatly simplified in that it only allows: no units,
- units of px, mm, and %. Everything else, it returns None for.
- There is a more general routine to consider in scour.py if more
- generality is ever needed.
- '''
-
- u = 'px'
- s = str.strip()
- if s[-2:] == 'px':
- s = s[:-2]
- elif s[-2:] == 'mm':
- u = 'mm'
- s = s[:-2]
- elif s[-2:] == 'pt':
- u = 'pt'
- s = s[:-2]
- elif s[-2:] == 'pc':
- u = 'pc'
- s = s[:-2]
- elif s[-2:] == 'cm':
- u = 'cm'
- s = s[:-2]
- elif s[-2:] == 'in':
- u = 'in'
- s = s[:-2]
- elif s[-1:] == '%':
- u = '%'
- s = s[:-1]
- try:
- v = float( s )
- except:
- raise ValueError(_("parseLengthWithUnits: unknown unit %s") % s)
-
- return v, u
-
-
-def convert_length(length):
- value, units = parse_length_with_units(length)
-
- if not units or units == "px":
- return value
-
- if units == 'pt':
- value /= 72
- units = 'in'
-
- if units == 'pc':
- value /= 6
- units = 'in'
-
- if units == 'cm':
- value *= 10
- units = 'mm'
-
- if units == 'mm':
- value = value / 25.4
- units = 'in'
-
- if units == 'in':
- # modern versions of Inkscape use CSS's 96 pixels per inch. When you
- # open an old document, inkscape will add a viewbox for you.
- return value * 96
-
- raise ValueError(_("Unknown unit: %s") % units)
-
-
-@cache
-def get_doc_size(svg):
- doc_width = convert_length(svg.get('width'))
- doc_height = convert_length(svg.get('height'))
-
- return doc_width, doc_height
-
-@cache
-def get_viewbox_transform(node):
- # somewhat cribbed from inkscape-silhouette
- doc_width, doc_height = get_doc_size(node)
-
- viewbox = node.get('viewBox').strip().replace(',', ' ').split()
-
- dx = -float(viewbox[0])
- dy = -float(viewbox[1])
- transform = simpletransform.parseTransform("translate(%f, %f)" % (dx, dy))
-
- try:
- sx = doc_width / float(viewbox[2])
- sy = doc_height / float(viewbox[3])
- scale_transform = simpletransform.parseTransform("scale(%f, %f)" % (sx, sy))
- transform = simpletransform.composeTransform(transform, scale_transform)
- except ZeroDivisionError:
- pass
-
- return transform
-
-class Param(object):
- def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0):
- self.name = name
- self.description = description
- self.unit = unit
- self.values = values or [""]
- self.type = type
- self.group = group
- self.inverse = inverse
- self.default = default
- self.tooltip = tooltip
- self.sort_index = sort_index
-
- def __repr__(self):
- return "Param(%s)" % vars(self)
-
-
-# Decorate a member function or property with information about
-# the embroidery parameter it corresponds to
-def param(*args, **kwargs):
- p = Param(*args, **kwargs)
-
- def decorator(func):
- func.param = p
- return func
-
- return decorator
-
-
-class EmbroideryElement(object):
- def __init__(self, node):
- self.node = node
-
- @property
- def id(self):
- return self.node.get('id')
-
- @classmethod
- def get_params(cls):
- params = []
- for attr in dir(cls):
- prop = getattr(cls, attr)
- if isinstance(prop, property):
- # The 'param' attribute is set by the 'param' decorator defined above.
- if hasattr(prop.fget, 'param'):
- params.append(prop.fget.param)
-
- return params
-
- @cache
- def get_param(self, param, default):
- value = self.node.get("embroider_" + param, "").strip()
-
- return value or default
-
- @cache
- def get_boolean_param(self, param, default=None):
- value = self.get_param(param, default)
-
- if isinstance(value, bool):
- return value
- else:
- return value and (value.lower() in ('yes', 'y', 'true', 't', '1'))
-
- @cache
- def get_float_param(self, param, default=None):
- try:
- value = float(self.get_param(param, default))
- except (TypeError, ValueError):
- return default
-
- if param.endswith('_mm'):
- value = value * PIXELS_PER_MM
-
- return value
-
- @cache
- def get_int_param(self, param, default=None):
- try:
- value = int(self.get_param(param, default))
- except (TypeError, ValueError):
- return default
-
- if param.endswith('_mm'):
- value = int(value * PIXELS_PER_MM)
-
- return value
-
- def set_param(self, name, value):
- self.node.set("embroider_%s" % name, str(value))
-
- @cache
- def get_style(self, style_name):
- style = simplestyle.parseStyle(self.node.get("style"))
- if (style_name not in style):
- return None
- value = style[style_name]
- if value == 'none':
- return None
- return value
-
- @cache
- def has_style(self, style_name):
- style = simplestyle.parseStyle(self.node.get("style"))
- return style_name in style
-
- @property
- def path(self):
- return cubicsuperpath.parsePath(self.node.get("d"))
-
-
- @cache
- def parse_path(self):
- # A CSP is a "cubic superpath".
- #
- # A "path" is a sequence of strung-together bezier curves.
- #
- # A "superpath" is a collection of paths that are all in one object.
- #
- # The "cubic" bit in "cubic superpath" is because the bezier curves
- # inkscape uses involve cubic polynomials.
- #
- # Each path is a collection of tuples, each of the form:
- #
- # (control_before, point, control_after)
- #
- # A bezier curve segment is defined by an endpoint, a control point,
- # a second control point, and a final endpoint. A path is a bunch of
- # bezier curves strung together. One could represent a path as a set
- # of four-tuples, but there would be redundancy because the ending
- # point of one bezier is the starting point of the next. Instead, a
- # path is a set of 3-tuples as shown above, and one must construct
- # each bezier curve by taking the appropriate endpoints and control
- # points. Bleh. It should be noted that a straight segment is
- # represented by having the control point on each end equal to that
- # end's point.
- #
- # 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?
-
- path = self.path
-
- # start with the identity transform
- transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
-
- # combine this node's transform with all parent groups' transforms
- transform = simpletransform.composeParents(self.node, transform)
-
- # add in the transform implied by the viewBox
- viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot())
- transform = simpletransform.composeTransform(viewbox_transform, transform)
-
- # apply the combined transform to this node's path
- simpletransform.applyTransformToPath(transform, path)
-
-
- return path
-
- def flatten(self, path):
- """approximate a path containing beziers with a series of points"""
-
- path = deepcopy(path)
-
- cspsubdiv(path, 0.1)
-
- flattened = []
-
- for comp in path:
- vertices = []
- for ctl in comp:
- vertices.append((ctl[1][0], ctl[1][1]))
- flattened.append(vertices)
-
- return flattened
-
- @property
- @param('trim_after',
- _('TRIM after'),
- tooltip=_('Trim thread after this object (for supported machines and file formats)'),
- type='boolean',
- default=False,
- sort_index=1000)
- def trim_after(self):
- return self.get_boolean_param('trim_after', False)
-
- @property
- @param('stop_after',
- _('STOP after'),
- tooltip=_('Add STOP instruction after this object (for supported machines and file formats)'),
- type='boolean',
- default=False,
- sort_index=1000)
- def stop_after(self):
- return self.get_boolean_param('stop_after', False)
-
- def to_patches(self, last_patch):
- raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
-
- def embroider(self, last_patch):
- patches = self.to_patches(last_patch)
-
- if patches:
- patches[-1].trim_after = self.trim_after
- patches[-1].stop_after = self.stop_after
-
- return patches
-
- def fatal(self, message):
- print >> sys.stderr, "error:", message
- sys.exit(1)
-
-
-class Point:
- def __init__(self, x, y):
- self.x = x
- self.y = y
-
- def __add__(self, other):
- return Point(self.x + other.x, self.y + other.y)
-
- def __sub__(self, other):
- return Point(self.x - other.x, self.y - other.y)
-
- def mul(self, scalar):
- return Point(self.x * scalar, self.y * scalar)
-
- def __mul__(self, other):
- if isinstance(other, Point):
- # dot product
- return self.x * other.x + self.y * other.y
- elif isinstance(other, (int, float)):
- return Point(self.x * other, self.y * other)
- else:
- raise ValueError("cannot multiply Point by %s" % type(other))
-
- def __rmul__(self, other):
- if isinstance(other, (int, float)):
- return self.__mul__(other)
- else:
- raise ValueError("cannot multiply Point by %s" % type(other))
-
- def __repr__(self):
- return "Point(%s,%s)" % (self.x, self.y)
-
- def length(self):
- return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0))
-
- def unit(self):
- return self.mul(1.0 / self.length())
-
- def rotate_left(self):
- return Point(-self.y, self.x)
-
- def rotate(self, angle):
- return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
-
- def as_int(self):
- return Point(int(round(self.x)), int(round(self.y)))
-
- 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]
-
- def __len__(self):
- return 2
-
-
-class Stitch(Point):
- def __init__(self, x, y, color=None, jump=False, stop=False, trim=False):
- self.x = x
- self.y = y
- self.color = color
- self.jump = jump
- self.trim = trim
- self.stop = stop
-
- def __repr__(self):
- return "Stitch(%s, %s, %s, %s, %s, %s)" % (self.x, self.y, self.color, "JUMP" if self.jump else "", "TRIM" if self.trim else "", "STOP" if self.stop else "")
-
-
-def descendants(node):
- nodes = []
- element = EmbroideryElement(node)
-
- if element.has_style('display') and element.get_style('display') is None:
- return []
-
- if node.tag == SVG_DEFS_TAG:
- return []
-
- for child in node:
- nodes.extend(descendants(child))
-
- if node.tag in EMBROIDERABLE_TAGS:
- nodes.append(node)
-
- return nodes
-
-
-def get_nodes(effect):
- """Get all XML nodes, or just those selected
-
- effect is an instance of a subclass of inkex.Effect.
- """
-
- if effect.selected:
- nodes = []
- for node in effect.document.getroot().iter():
- if node.get("id") in effect.selected:
- nodes.extend(descendants(node))
- else:
- nodes = descendants(effect.document.getroot())
-
- return nodes
-
-
-def make_thread(color):
- # strip off the leading "#"
- if color.startswith("#"):
- color = color[1:]
-
- thread = libembroidery.EmbThread()
- thread.color = libembroidery.embColor_fromHexStr(color)
-
- thread.description = color
- thread.catalogNumber = ""
-
- return thread
-
-def add_thread(pattern, thread):
- """Add a thread to a pattern and return the thread's index"""
-
- libembroidery.embPattern_addThread(pattern, thread)
-
- return libembroidery.embThreadList_count(pattern.threadList) - 1
-
-def get_flags(stitch):
- flags = 0
-
- if stitch.jump:
- flags |= libembroidery.JUMP
-
- if stitch.trim:
- flags |= libembroidery.TRIM
-
- if stitch.stop:
- flags |= libembroidery.STOP
-
- return flags
-
-
-def _string_to_floats(string):
- floats = string.split(',')
- return [float(num) for num in floats]
-
-
-def get_origin(svg):
- # The user can specify the embroidery origin by defining two guides
- # named "embroidery origin" that intersect.
-
- namedview = svg.find(inkex.addNS('namedview', 'sodipodi'))
- all_guides = namedview.findall(inkex.addNS('guide', 'sodipodi'))
- label_attribute = inkex.addNS('label', 'inkscape')
- guides = [guide for guide in all_guides
- if guide.get(label_attribute, "").startswith("embroidery origin")]
-
- # document size used below
- doc_size = list(get_doc_size(svg))
-
- # convert the size from viewbox-relative to real-world pixels
- viewbox_transform = get_viewbox_transform(svg)
- simpletransform.applyTransformToPoint(simpletransform.invertTransform(viewbox_transform), doc_size)
-
- default = [doc_size[0] / 2.0, doc_size[1] / 2.0]
- simpletransform.applyTransformToPoint(viewbox_transform, default)
- default = Point(*default)
-
- if len(guides) < 2:
- return default
-
- # Find out where the guides intersect. Only pay attention to the first two.
- guides = guides[:2]
-
- lines = []
- for guide in guides:
- # inkscape's Y axis is reversed from SVG's, and the guide is in inkscape coordinates
- position = Point(*_string_to_floats(guide.get('position')))
- position.y = doc_size[1] - position.y
-
-
- # This one baffles me. I think inkscape might have gotten the order of
- # their vector wrong?
- parts = _string_to_floats(guide.get('orientation'))
- direction = Point(parts[1], parts[0])
-
- # We have a theoretically infinite line defined by a point on the line
- # and a vector direction. Shapely can only deal in concrete line
- # segments, so we'll pick points really far in either direction on the
- # line and call it good enough.
- lines.append(shgeo.LineString((position + 100000 * direction, position - 100000 * direction)))
-
- intersection = lines[0].intersection(lines[1])
-
- if isinstance(intersection, shgeo.Point):
- origin = [intersection.x, intersection.y]
- simpletransform.applyTransformToPoint(viewbox_transform, origin)
- return Point(*origin)
- else:
- # Either the two guides are the same line, or they're parallel.
- return default
-
-
-def write_embroidery_file(file_path, stitches, svg):
- origin = get_origin(svg)
-
- pattern = libembroidery.embPattern_create()
- last_color = None
-
- for stitch in stitches:
- if stitch.color != last_color:
- add_thread(pattern, make_thread(stitch.color))
- last_color = stitch.color
-
- flags = get_flags(stitch)
- libembroidery.embPattern_addStitchAbs(pattern, stitch.x - origin.x, stitch.y - origin.y, flags, 1)
-
- libembroidery.embPattern_addStitchAbs(pattern, stitch.x - origin.x, stitch.y - origin.y, libembroidery.END, 1)
-
- # convert from pixels to millimeters
- libembroidery.embPattern_scale(pattern, 1/PIXELS_PER_MM)
-
- # SVG and embroidery disagree on the direction of the Y axis
- libembroidery.embPattern_flipVertical(pattern)
-
- libembroidery.embPattern_write(pattern, file_path)