diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/extensions/input.py | 44 | ||||
| -rw-r--r-- | lib/extensions/output.py | 5 | ||||
| -rw-r--r-- | lib/extensions/zip.py | 24 | ||||
| -rw-r--r-- | lib/output.py | 76 | ||||
| -rw-r--r-- | lib/stitch_plan/stitch.py | 14 | ||||
| -rw-r--r-- | lib/stitch_plan/stitch_plan.py | 123 | ||||
| -rw-r--r-- | lib/stitch_plan/stop.py | 43 | ||||
| -rw-r--r-- | lib/stitch_plan/ties.py | 18 | ||||
| -rw-r--r-- | lib/stitch_plan/trim.py | 23 | ||||
| -rw-r--r-- | lib/svg/svg.py | 3 | ||||
| -rw-r--r-- | lib/threads/color.py | 17 | ||||
| -rw-r--r-- | lib/utils/geometry.py | 3 |
12 files changed, 161 insertions, 232 deletions
diff --git a/lib/extensions/input.py b/lib/extensions/input.py index 21248dd9..cb5ac452 100644 --- a/lib/extensions/input.py +++ b/lib/extensions/input.py @@ -3,53 +3,29 @@ from os.path import realpath, dirname, join as path_join import sys from inkex import etree import inkex - -# help python find libembroidery when running in a local repo clone -if getattr(sys, 'frozen', None) is None: - sys.path.append(realpath(path_join(dirname(__file__), '..', '..'))) - -from libembroidery import * +import pyembroidery from ..svg import PIXELS_PER_MM, render_stitch_plan from ..svg.tags import INKSCAPE_LABEL from ..i18n import _ -from ..stitch_plan import StitchPlan +from ..stitch_plan import StitchPlan, ColorBlock from ..utils.io import save_stdout class Input(object): - def pattern_stitches(self, pattern): - stitch_pointer = pattern.stitchList - while stitch_pointer: - yield stitch_pointer.stitch - stitch_pointer = stitch_pointer.next - - def affect(self, args): - # libembroidery likes to dump a bunch of debugging stuff to stdout - save_stdout() - embroidery_file = args[0] - pattern = embPattern_create() - embPattern_read(pattern, embroidery_file) - embPattern_flipVertical(pattern) + pattern = pyembroidery.read(embroidery_file) stitch_plan = StitchPlan() color_block = None - current_color = None - - for stitch in self.pattern_stitches(pattern): - if stitch.color != current_color: - thread = embThreadList_getAt(pattern.threadList, stitch.color) - color = thread.color - color_block = stitch_plan.new_color_block((color.r, color.g, color.b)) - current_color = stitch.color - if not stitch.flags & END: - color_block.add_stitch(stitch.xx * PIXELS_PER_MM, stitch.yy * PIXELS_PER_MM, - jump=stitch.flags & JUMP, - color_change=stitch.flags & STOP, - trim=stitch.flags & TRIM) + for raw_stitches, thread in pattern.get_as_colorblocks(): + color_block = stitch_plan.new_color_block(thread) + for x, y, command in raw_stitches: + color_block.add_stitch(x * PIXELS_PER_MM / 10.0, y * PIXELS_PER_MM / 10.0, + jump=(command == pyembroidery.JUMP), + trim=(command == pyembroidery.TRIM)) extents = stitch_plan.extents svg = etree.Element("svg", nsmap=inkex.NSS, attrib= @@ -69,4 +45,4 @@ class Input(object): # Note: this is NOT the same as centering the design in the canvas! layer.set('transform', 'translate(%s,%s)' % (extents[0], extents[1])) - print >> sys.real_stdout, etree.tostring(svg) + print etree.tostring(svg) diff --git a/lib/extensions/output.py b/lib/extensions/output.py index f4b153e6..1dc8d19d 100644 --- a/lib/extensions/output.py +++ b/lib/extensions/output.py @@ -29,20 +29,17 @@ class Output(InkstitchExtension): patches = self.elements_to_patches(self.elements) stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) - # libembroidery wants to write to an actual file rather than stdout temp_file = tempfile.NamedTemporaryFile(suffix=".%s" % self.options.file_extension, delete=False) # in windows, failure to close here will keep the file locked temp_file.close() - # libembroidery likes to debug log things to stdout. No way to disable it. - save_stdout() write_embroidery_file(temp_file.name, stitch_plan, self.document.getroot()) # 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.real_stdout.write(output_file.read()) + sys.stdout.write(output_file.read()) # clean up the temp file os.remove(temp_file.name) diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py index ca12efdd..02f29e8a 100644 --- a/lib/extensions/zip.py +++ b/lib/extensions/zip.py @@ -4,7 +4,7 @@ import os import inkex import tempfile from zipfile import ZipFile -from libembroidery import * +import pyembroidery from .base import InkstitchExtension from ..i18n import _ @@ -24,18 +24,11 @@ class Zip(InkstitchExtension): # it's kind of obnoxious that I have to do this... self.formats = [] - formatList = embFormatList_create() - curFormat = formatList - while(curFormat): - # extension includes the dot, so we'll remove it - extension = embFormat_extension(curFormat)[1:] - description = embFormat_description(curFormat) - writer_state = embFormat_writerState(curFormat) - - if writer_state.strip() and embFormat_type(curFormat) != EMBFORMAT_OBJECTONLY: + 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.formats.append(extension) - curFormat = curFormat.next def effect(self): if not self.get_elements(): @@ -49,19 +42,12 @@ class Zip(InkstitchExtension): files = [] - # libembroidery likes to debug log things to stdout. No way to disable it. - save_stdout() for format in self.formats: if getattr(self.options, format): output_file = os.path.join(path, "%s.%s" % (base_file_name, format)) write_embroidery_file(output_file, stitch_plan, self.document.getroot()) files.append(output_file) - # I'd love to do restore_stderr() here, but if I do, libembroidery's - # stuff still prints out and corrupts the zip! That's because it uses - # C's buffered stdout, so it hasn't actually written anything to the - # real standard output yet. - if not files: self.errormsg(_("No embroidery file formats selected.")) @@ -77,7 +63,7 @@ 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.real_stdout.write(output_file.read()) + sys.stdout.write(output_file.read()) os.remove(temp_file.name) for file in files: diff --git a/lib/output.py b/lib/output.py index 84128a25..0d7f9918 100644 --- a/lib/output.py +++ b/lib/output.py @@ -1,4 +1,4 @@ -import libembroidery +import pyembroidery import inkex import simpletransform import shapely.geometry as shgeo @@ -7,36 +7,17 @@ from .utils import Point from .svg import PIXELS_PER_MM, get_doc_size, get_viewbox_transform -def make_thread(color): - thread = libembroidery.EmbThread() - thread.color = libembroidery.embColor_make(*color.rgb) - - thread.description = color.name - 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 - +def get_command(stitch): if stitch.jump: - flags |= libembroidery.JUMP - - if stitch.trim: - flags |= libembroidery.TRIM - - if stitch.color_change: - flags |= libembroidery.STOP - - return flags - + return pyembroidery.JUMP + elif stitch.trim: + return pyembroidery.TRIM + elif stitch.color_change: + return pyembroidery.COLOR_CHANGE + elif stitch.stop: + return pyembroidery.STOP + else: + return pyembroidery.NEEDLE_AT def _string_to_floats(string): floats = string.split(',') @@ -102,27 +83,32 @@ def get_origin(svg): def write_embroidery_file(file_path, stitch_plan, svg): origin = get_origin(svg) - pattern = libembroidery.embPattern_create() + pattern = pyembroidery.EmbPattern() for color_block in stitch_plan: - add_thread(pattern, make_thread(color_block.color)) + pattern.add_thread(color_block.color.pyembroidery_thread) for stitch in color_block: - if stitch.stop: - # This is the start of the extra color block added by the - # "STOP after" handler (see stitch_plan/stop.py). Assign it - # the same color. - add_thread(pattern, make_thread(color_block.color)) + command = get_command(stitch) + pattern.add_stitch_absolute(command, stitch.x, stitch.y) - 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) + pattern.add_stitch_absolute(pyembroidery.END, stitch.x, stitch.y) # convert from pixels to millimeters - libembroidery.embPattern_scale(pattern, 1/PIXELS_PER_MM) + # also multiply by 10 to get tenths of a millimeter as required by pyembroidery + scale = 10 / PIXELS_PER_MM + + settings = { + # correct for the origin + "translate": -origin, + + # convert from pixels to millimeters + # also multiply by 10 to get tenths of a millimeter as required by pyembroidery + "scale": (scale, scale), - # SVG and embroidery disagree on the direction of the Y axis - libembroidery.embPattern_flipVertical(pattern) + # This forces a jump at the start of the design and after each trim, + # even if we're close enough not to need one. + "full_jump": True, + } - libembroidery.embPattern_write(pattern, file_path) + pyembroidery.write(pattern, file_path, settings) diff --git a/lib/stitch_plan/stitch.py b/lib/stitch_plan/stitch.py index 12642a60..5230efec 100644 --- a/lib/stitch_plan/stitch.py +++ b/lib/stitch_plan/stitch.py @@ -2,7 +2,7 @@ from ..utils.geometry import Point class Stitch(Point): - def __init__(self, x, y, color=None, jump=False, stop=False, trim=False, color_change=False, no_ties=False): + def __init__(self, x, y, color=None, jump=False, stop=False, trim=False, color_change=False, fake_color_change=False, no_ties=False): self.x = x self.y = y self.color = color @@ -10,10 +10,20 @@ class Stitch(Point): self.trim = trim self.stop = stop self.color_change = color_change + self.fake_color_change = fake_color_change self.no_ties = no_ties def __repr__(self): - return "Stitch(%s, %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 " ", "NO TIES" if self.no_ties else " ") + return "Stitch(%s, %s, %s, %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 " ", + "NO TIES" if self.no_ties else " ", + "FAKE " if self.fake_color_change else "", + "COLOR CHANGE" if self.color_change else " " + ) def copy(self): return Stitch(self.x, self.y, self.color, self.jump, self.stop, self.trim, self.color_change, self.no_ties) diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index 742916f0..682ea09f 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -1,6 +1,4 @@ from .stitch import Stitch -from .stop import process_stop -from .trim import process_trim from .ties import add_ties from ..svg import PIXELS_PER_MM from ..utils.geometry import Point @@ -16,62 +14,45 @@ def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM): """ stitch_plan = StitchPlan() - color_block = stitch_plan.new_color_block() - need_trim = False + if not patches: + return stitch_plan + + color_block = stitch_plan.new_color_block(color=patches[0].color) + for patch in patches: if not patch.stitches: continue - if not color_block.has_color(): - # set the color for the first color block - color_block.color = patch.color - - if color_block.color == patch.color: - if need_trim: - process_trim(color_block, patch.stitches[0]) - need_trim = False - - # add a jump stitch between patches if the distance is more - # than the collapse length - if color_block.last_stitch: - if (patch.stitches[0] - color_block.last_stitch).length() > collapse_len: - color_block.add_stitch(patch.stitches[0].x, patch.stitches[0].y, jump=True) - - else: - # add a color change (only if we didn't just do a "STOP after") - if not color_block.last_stitch.color_change: - stitch = color_block.last_stitch.copy() - stitch.color_change = True - color_block.add_stitch(stitch) + if color_block.color != patch.color: + if len(color_block) == 0: + # We just processed a stop, which created a new color block. + # We'll just claim this new block as ours: + color_block.color = patch.color + else: + # end the previous block with a color change + color_block.add_stitch(color_change=True) - color_block = stitch_plan.new_color_block() - color_block.color = patch.color + # make a new block of our color + color_block = stitch_plan.new_color_block(color=patch.color) - color_block.filter_duplicate_stitches() color_block.add_stitches(patch.stitches, no_ties=patch.stitch_as_is) if patch.trim_after: - # a trim needs to be followed by a jump to the next stitch, so - # we'll process it when we start the next patch - need_trim = True + color_block.add_stitch(trim=True) if patch.stop_after: - process_stop(color_block) - - add_jumps(stitch_plan) - add_ties(stitch_plan) - - return stitch_plan + color_block.add_stitch(stop=True) + color_block = stitch_plan.new_color_block(color_block.color) + if len(color_block) == 0: + # last block ended in a stop, so now we have an empty block + del stitch_plan.color_blocks[-1] -def add_jumps(stitch_plan): - """Add a JUMP stitch at the start of each color block.""" + stitch_plan.filter_duplicate_stitches() + stitch_plan.add_ties() - for color_block in stitch_plan: - stitch = color_block.stitches[0].copy() - stitch.jump = True - color_block.stitches.insert(0, stitch) + return stitch_plan class StitchPlan(object): @@ -85,6 +66,17 @@ class StitchPlan(object): self.color_blocks.append(color_block) return color_block + def add_color_block(self, color_block): + self.color_blocks.append(color_block) + + def filter_duplicate_stitches(self): + for color_block in self: + color_block.filter_duplicate_stitches() + + def add_ties(self): + # see ties.py + add_ties(self) + def __iter__(self): return iter(self.color_blocks) @@ -100,8 +92,12 @@ class StitchPlan(object): return len({block.color for block in self}) @property + def num_color_blocks(self): + return len(self.color_blocks) + + @property def num_stops(self): - return sum(block.num_stops for block in self) + return sum(1 for block in self if block.stop_after) @property def num_trims(self): @@ -137,6 +133,13 @@ class StitchPlan(object): dimensions = self.dimensions return (dimensions[0] / PIXELS_PER_MM, dimensions[1] / PIXELS_PER_MM) + @property + def last_color_block(self): + if self.color_blocks: + return self.color_blocks[-1] + else: + return None + class ColorBlock(object): """Holds a set of stitches, all with the same thread color.""" @@ -148,6 +151,9 @@ class ColorBlock(object): def __iter__(self): return iter(self.stitches) + def __len__(self): + return len(self.stitches) + def __repr__(self): return "ColorBlock(%s, %s)" % (self.color, self.stitches) @@ -180,17 +186,18 @@ class ColorBlock(object): return len(self.stitches) @property - def num_stops(self): - """Number of pauses in this color block.""" - - return sum(1 for stitch in self if stitch.stop) - - @property def num_trims(self): """Number of trims in this color block.""" return sum(1 for stitch in self if stitch.trim) + @property + def stop_after(self): + if self.last_stitch is not None: + return self.last_stitch.stop + else: + return False + def filter_duplicate_stitches(self): if not self.stitches: return @@ -198,12 +205,12 @@ class ColorBlock(object): stitches = [self.stitches[0]] for stitch in self.stitches[1:]: - if stitches[-1].jump or stitch.stop or stitch.trim: - # Don't consider jumps, stops, or trims as candidates for filtering + if stitches[-1].jump or stitch.stop or stitch.trim or stitch.color_change: + # Don't consider jumps, stops, color changes, or trims as candidates for filtering pass else: l = (stitch - stitches[-1]).length() - if l <= 0.1: + if l <= 0.1 * PIXELS_PER_MM: # duplicate stitch, skip this one continue @@ -212,11 +219,21 @@ class ColorBlock(object): self.stitches = stitches def add_stitch(self, *args, **kwargs): + if not args: + # They're adding a command, e.g. `color_block.add_stitch(stop=True)``. + # Use the position from the last stitch. + if self.last_stitch: + args = (self.last_stitch.x, self.last_stitch.y) + else: + raise ValueError("internal error: can't add a command to an empty stitch block") + if isinstance(args[0], Stitch): self.stitches.append(args[0]) elif isinstance(args[0], Point): self.stitches.append(Stitch(args[0].x, args[0].y, *args[1:], **kwargs)) else: + if not args and self.last_stitch: + args = (self.last_stitch.x, self.last_stitch.y) self.stitches.append(Stitch(*args, **kwargs)) def add_stitches(self, stitches, *args, **kwargs): diff --git a/lib/stitch_plan/stop.py b/lib/stitch_plan/stop.py deleted file mode 100644 index 81dec1da..00000000 --- a/lib/stitch_plan/stop.py +++ /dev/null @@ -1,43 +0,0 @@ -def process_stop(color_block): - """Handle the "stop after" checkbox. - - The user wants the machine to pause after this patch. This can - be useful for applique and similar on multi-needle machines that - normally would not stop between colors. - - In machine embroidery files, there's no such thing as an actual - "STOP" instruction. All that exists is a "color change" command - (which libembroidery calls STOP just to be confusing). - - On multi-needle machines, the user assigns needles to the colors in - the design before starting stitching. C01, C02, etc are normal - needles, but C00 is special. For a block of stitches assigned - to C00, the machine will continue sewing with the last color it - had and pause after it completes the C00 block. - - That means we need to add an artificial color change instruction - shortly before the current stitch so that the user can set that color - block to C00. We'll go back 3 stitches and mark the start of the C00 - block: - """ - - if len(color_block.stitches) >= 3: - # make a copy of the stitch and set it as a color change - stitch = color_block.stitches[-3].copy() - stitch.color_change = True - - # mark this stitch as a "stop" so that we can avoid - # adding tie stitches in ties.py - stitch.stop = True - - # insert it after the stitch - color_block.stitches.insert(-2, stitch) - - # and also add a color change on this stitch, completing the C00 - # block: - - stitch = color_block.stitches[-1].copy() - stitch.color_change = True - color_block.add_stitch(stitch) - - # reference for the above: https://github.com/lexelby/inkstitch/pull/29#issuecomment-359175447 diff --git a/lib/stitch_plan/ties.py b/lib/stitch_plan/ties.py index 6d07ac71..573469f5 100644 --- a/lib/stitch_plan/ties.py +++ b/lib/stitch_plan/ties.py @@ -30,15 +30,16 @@ def add_tie_in(stitches, upcoming_stitches): def add_ties(stitch_plan): """Add tie-off before and after trims, jumps, and color changes.""" + need_tie_in = True for color_block in stitch_plan: - need_tie_in = True new_stitches = [] for i, stitch in enumerate(color_block.stitches): - # Tie before and after TRIMs, JUMPs, and color changes, but ignore - # the fake color change introduced by a "STOP after" (see stop.py). - is_special = stitch.trim or stitch.jump or (stitch.color_change and not stitch.stop) + is_special = stitch.trim or stitch.jump or stitch.color_change or stitch.stop - if is_special and not need_tie_in: + # see stop.py for an explanation of the fake color change + is_fake = stitch.fake_color_change + + if is_special and not is_fake and not need_tie_in: add_tie_off(new_stitches) new_stitches.append(stitch) need_tie_in = True @@ -49,7 +50,8 @@ def add_ties(stitch_plan): else: new_stitches.append(stitch) - if not need_tie_in: - add_tie_off(new_stitches) - color_block.replace_stitches(new_stitches) + + if not need_tie_in: + # tie off at the end if we haven't already + add_tie_off(color_block.stitches) diff --git a/lib/stitch_plan/trim.py b/lib/stitch_plan/trim.py deleted file mode 100644 index f692a179..00000000 --- a/lib/stitch_plan/trim.py +++ /dev/null @@ -1,23 +0,0 @@ -def process_trim(color_block, next_stitch): - """Handle the "trim after" checkbox. - - DST (and maybe other formats?) has no actual TRIM instruction. - Instead, 3 sequential JUMPs cause the machine to trim the thread. - - To support both DST and other formats, we'll add a TRIM and two - JUMPs. The TRIM will be converted to a JUMP by libembroidery - if saving to DST, resulting in the 3-jump sequence. - """ - - delta = next_stitch - color_block.last_stitch - delta = delta * (1/4.0) - - pos = color_block.last_stitch - - for i in xrange(3): - pos += delta - color_block.add_stitch(pos.x, pos.y, jump=True) - - # first one should be TRIM instead of JUMP - color_block.stitches[-3].jump = False - color_block.stitches[-3].trim = True diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 5552abd8..48b1343a 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -37,6 +37,9 @@ def color_block_to_realistic_stitches(color_block, svg): paths = [] for point_list in color_block_to_point_lists(color_block): + if not point_list: + continue + color = color_block.color.visible_on_white.darker.to_hex_str() start = point_list[0] for point in point_list[1:]: diff --git a/lib/threads/color.py b/lib/threads/color.py index fede2ecc..cc6c0c48 100644 --- a/lib/threads/color.py +++ b/lib/threads/color.py @@ -1,7 +1,7 @@ import simplestyle import re import colorsys - +from pyembroidery.EmbThread import EmbThread class ThreadColor(object): hex_str_re = re.compile('#([0-9a-z]{3}|[0-9a-z]{6})', re.I) @@ -9,6 +9,12 @@ class ThreadColor(object): def __init__(self, color, name=None, number=None, manufacturer=None): if color is None: self.rgb = (0, 0, 0) + elif isinstance(color, EmbThread): + self.name = color.description + self.number = color.catalog_number + self.manufacturer = color.brand + self.rgb = (color.get_red(), color.get_green(), color.get_blue()) + return elif isinstance(color, (list, tuple)): self.rgb = tuple(color) elif self.hex_str_re.match(color): @@ -39,6 +45,15 @@ class ThreadColor(object): return "#%s" % self.hex_digits @property + def pyembroidery_thread(self): + return { + "name": self.name, + "id": self.number, + "manufacturer": self.manufacturer, + "rgb": int(self.hex_digits, 16), + } + + @property def hex_digits(self): return "%02X%02X%02X" % self.rgb diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py index 7ff9b1cd..d0cb96cf 100644 --- a/lib/utils/geometry.py +++ b/lib/utils/geometry.py @@ -65,6 +65,9 @@ class Point: else: raise ValueError("cannot multiply Point by %s" % type(other)) + def __neg__(self): + return self * -1 + def __rmul__(self, other): if isinstance(other, (int, float)): return self.__mul__(other) |
