inkstitch/inkstitch.py

598 wiersze
17 KiB
Python

#!/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)