From c4242f0f940c86766c0c27f65b5a09641b0af4bd Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 05:16:53 -0700 Subject: [PATCH 01/16] 1.4.0 update Slightly reworked the get_supported_formats() to use a more standard "version" syntax. Rather than hiding different versions in various options that differ throughout. This could be a breaking change for some users. Added extensions to supported formats. Updated read and write for jef and dst to better handle trim commands. --- pyembroidery/CsvWriter.py | 13 +++-- pyembroidery/DstReader.py | 36 +++++++++++-- pyembroidery/DstWriter.py | 6 ++- pyembroidery/EmbPattern.py | 97 ++++++++++++++++++++++++++++-------- pyembroidery/EmbThread.py | 49 ++++++++++++++++-- pyembroidery/EmbThreadJef.py | 2 +- pyembroidery/EmbThreadPec.py | 2 +- pyembroidery/ExyReader.py | 2 +- pyembroidery/JefReader.py | 60 +++++++++++++++++++--- pyembroidery/JefWriter.py | 21 +++++--- pyembroidery/PecWriter.py | 19 ++----- pyembroidery/PesWriter.py | 17 ++++--- pyembroidery/PyEmbroidery.py | 80 ++++++++++++++++++++++------- pyembroidery/TapReader.py | 2 +- pyembroidery/TxtWriter.py | 14 ++++-- pyembroidery/XxxReader.py | 7 +-- 16 files changed, 328 insertions(+), 99 deletions(-) diff --git a/pyembroidery/CsvWriter.py b/pyembroidery/CsvWriter.py index af41fa83..d80be636 100644 --- a/pyembroidery/CsvWriter.py +++ b/pyembroidery/CsvWriter.py @@ -221,16 +221,21 @@ def write_stitches(pattern, f): def write(pattern, f, settings=None): - deltas = settings is not None and "deltas" in settings - displacement = settings is not None and "displacement" in settings + version = "default" + if settings is not None: + if "deltas" in settings: + version = "delta" + elif "displacement" in settings: + version = "full" + version = settings.get("version", version) write_data(pattern, f) write_metadata(pattern, f) write_threads(pattern, f) if len(pattern.stitches) > 0: - if displacement: + if version == "full": write_stitches_displacement(pattern, f) - elif deltas: + elif version == "delta": write_stitches_deltas(pattern, f) else: write_stitches(pattern, f) diff --git a/pyembroidery/DstReader.py b/pyembroidery/DstReader.py index 5c24981b..b951a1d2 100644 --- a/pyembroidery/DstReader.py +++ b/pyembroidery/DstReader.py @@ -58,8 +58,17 @@ def dst_read_header(f, out): process_header_info(out, line[0:2].strip(), line[3:].strip()) -def dst_read_stitches(f, out): +def dst_read_stitches(f, out, settings=None): + jump_count_max = 3 + if settings is not None: + jump_count_max = settings.get('trim_at', jump_count_max) sequin_mode = False + jump_count = 0 + jump_start = 0 + jump_dx = 0 + jump_dy = 0 + jumping = False + trimmed = True while True: byte = bytearray(f.read(3)) if len(byte) != 3: @@ -67,21 +76,42 @@ def dst_read_stitches(f, out): dx = decode_dx(byte[0], byte[1], byte[2]) dy = decode_dy(byte[0], byte[1], byte[2]) if byte[2] & 0b11110011 == 0b11110011: - out.end(dx, dy) + break elif byte[2] & 0b11000011 == 0b11000011: out.color_change(dx, dy) + trimmed = True + jumping = False elif byte[2] & 0b01000011 == 0b01000011: out.sequin_mode(dx, dy) sequin_mode = not sequin_mode + jumping = False elif byte[2] & 0b10000011 == 0b10000011: if sequin_mode: out.sequin_eject(dx, dy) else: out.move(dx, dy) + if not jumping: + jump_dx = 0 + jump_dy = 0 + jump_count = 0 + jump_start = len(out.stitches) - 1 + jumping = True + jump_count += 1 + jump_dx += dx + jump_dy += dy + if not trimmed and jump_count == jump_count_max: + out.trim(position=jump_start) + jump_start += 1 # We inserted a position, start jump has moved. + trimmed = True + if jump_dx == 0 and jump_dy == 0: # If our jump displacement is 0, they were part of a trim command. + out.stitches = out.stitches[:jump_start] else: out.stitch(dx, dy) + trimmed = False + jumping = False + out.end() def read(f, out, settings=None): dst_read_header(f, out) - dst_read_stitches(f, out) + dst_read_stitches(f, out, settings) diff --git a/pyembroidery/DstWriter.py b/pyembroidery/DstWriter.py index 2330063f..837908f9 100644 --- a/pyembroidery/DstWriter.py +++ b/pyembroidery/DstWriter.py @@ -103,8 +103,10 @@ def encode_record(x, y, flags): def write(pattern, f, settings=None): extended_header = False if settings is not None: - extended_header = settings.get("extended header", extended_header) - + extended_header = settings.get("extended header", extended_header) # deprecated, use version="extended" + version = settings.get("version", "default") + if version == "extended": + extended_header = True bounds = pattern.bounds() name = pattern.get_metadata("name", "Untitled") diff --git a/pyembroidery/EmbPattern.py b/pyembroidery/EmbPattern.py index 50d8d899..dbc9c53a 100644 --- a/pyembroidery/EmbPattern.py +++ b/pyembroidery/EmbPattern.py @@ -120,50 +120,83 @@ def clear(self): self._previousX = 0 self._previousY = 0 - def move(self, dx=0, dy=0): + def move(self, dx=0, dy=0, position=None): """Move dx, dy""" - self.add_stitch_relative(JUMP, dx, dy) + if position is None: + self.add_stitch_relative(JUMP, dx, dy) + else: + self.insert_stitch_relative(position, JUMP, dx, dy) - def move_abs(self, x, y): + def move_abs(self, x, y, position=None): """Move absolute x, y""" - self.add_stitch_absolute(JUMP, x, y) + if position is None: + self.add_stitch_absolute(JUMP, x, y) + else: + self.insert(position, JUMP, x, y) - def stitch(self, dx=0, dy=0): + def stitch(self, dx=0, dy=0, position=None): """Stitch dx, dy""" - self.add_stitch_relative(STITCH, dx, dy) + if position is None: + self.add_stitch_relative(STITCH, dx, dy) + else: + self.insert_stitch_relative(position, STITCH, dx, dy) - def stitch_abs(self, x, y): + def stitch_abs(self, x, y, position=None): """Stitch absolute x, y""" - self.add_stitch_absolute(STITCH, x, y) + if position is None: + self.add_stitch_absolute(STITCH, x, y) + else: + self.insert(position, STITCH, x, y) - def stop(self, dx=0, dy=0): + def stop(self, dx=0, dy=0, position=None): """Stop dx, dy""" - self.add_stitch_relative(STOP, dx, dy) + if position is None: + self.add_stitch_relative(STOP, dx, dy) + else: + self.insert_stitch_relative(position, STOP, dx, dy) - def trim(self, dx=0, dy=0): + def trim(self, dx=0, dy=0, position=None): """Trim dx, dy""" - self.add_stitch_relative(TRIM, dx, dy) + if position is None: + self.add_stitch_relative(TRIM, dx, dy) + else: + self.insert_stitch_relative(position, TRIM, dx, dy) - def color_change(self, dx=0, dy=0): + def color_change(self, dx=0, dy=0, position=None): """Color Change dx, dy""" - self.add_stitch_relative(COLOR_CHANGE, dx, dy) + if position is None: + self.add_stitch_relative(COLOR_CHANGE, dx, dy) + else: + self.insert_stitch_relative(position, COLOR_CHANGE, dx, dy) - def needle_change(self, needle=0, dx=0, dy=0): + def needle_change(self, needle=0, dx=0, dy=0, position=None): """Needle change, needle, dx, dy""" cmd = encode_thread_change(NEEDLE_SET, None, needle) - self.add_stitch_relative(cmd, dx, dy) + if position is None: + self.add_stitch_relative(cmd, dx, dy) + else: + self.insert_stitch_relative(position, cmd, dx, dy) - def sequin_eject(self, dx=0, dy=0): + def sequin_eject(self, dx=0, dy=0, position=None): """Eject Sequin dx, dy""" - self.add_stitch_relative(SEQUIN_EJECT, dx, dy) + if position is None: + self.add_stitch_relative(SEQUIN_EJECT, dx, dy) + else: + self.insert_stitch_relative(position, SEQUIN_EJECT, dx, dy) - def sequin_mode(self, dx=0, dy=0): + def sequin_mode(self, dx=0, dy=0, position=None): """Eject Sequin dx, dy""" - self.add_stitch_relative(SEQUIN_MODE, dx, dy) + if position is None: + self.add_stitch_relative(SEQUIN_MODE, dx, dy) + else: + self.insert_stitch_relative(position, SEQUIN_MODE, dx, dy) - def end(self, dx=0, dy=0): + def end(self, dx=0, dy=0, position=None): """End Design dx, dy""" - self.add_stitch_relative(END, dx, dy) + if position is None: + self.add_stitch_relative(END, dx, dy) + else: + self.insert_stitch_relative(position, END, dx, dy) def add_thread(self, thread): """Adds thread to design. @@ -356,6 +389,26 @@ def add_stitch_relative(self, cmd, dx=0, dy=0): y = self._previousY + dy self.add_stitch_absolute(cmd, x, y) + def insert_stitch_relative(self, position, cmd, dx=0, dy=0): + """Insert a relative stitch into the pattern. The stitch is relative to the stitch before it. + If inserting at position 0, it's relative to 0,0. If appending, add is called, updating the positioning. + """ + if position < 0: + position += len(self.stitches) # I need positive positions. + if position == 0: + self.stitches.insert(0, [dx, dy, TRIM]) # started (0,0) + elif position == len(self.stitches) or position is None: # This is properly just an add. + self.add_stitch_relative(cmd, dx, dy) + elif 0 < position < len(self.stitches): + p = self.stitches[position - 1] + x = p[0] + dx + y = p[1] + dy + self.stitches.insert(position, [x, y, TRIM]) + + def insert(self, position, cmd, x=0, y=0): + """Insert a stitch or command""" + self.stitches.insert(position, [x, y, cmd]) + def prepend_command(self, cmd, x=0, y=0): """Prepend a command, without treating parameters as locations""" self.stitches.insert(0, [x, y, cmd]) diff --git a/pyembroidery/EmbThread.py b/pyembroidery/EmbThread.py index 4d900be7..f0ba27ca 100644 --- a/pyembroidery/EmbThread.py +++ b/pyembroidery/EmbThread.py @@ -1,14 +1,55 @@ +def build_unique_palette(thread_palette, threadlist): + """Turns a threadlist into a unique index list with the thread palette""" + chart = [None] * len(thread_palette) # Create a lookup chart. + for thread in set(threadlist): # for each unique color, move closest remaining thread to lookup chart. + index = thread.find_nearest_color_index(thread_palette) + if index is None: + break # No more threads remain in palette + thread_palette[index] = None # entries may not be reused. + chart[index] = thread # assign the given index to the lookup. + + palette = [] + for thread in threadlist: # for each thread, return the index. + palette.append(thread.find_nearest_color_index(chart)) + return palette + + +def build_palette(thread_palette, threadlist): + palette = [] + for thread in threadlist: # for each thread, return the index. + palette.append(thread.find_nearest_color_index(thread_palette)) + return palette + + +def build_nonrepeat_palette(thread_palette, threadlist): + last_index = None + last_thread = None + palette = [] + for thread in threadlist: # for each thread, return the index. + index = thread.find_nearest_color_index(thread_palette) + if last_index == index and last_thread != thread: + repeated_thread = thread_palette[index] + repeated_index = index + thread_palette[index] = None + index = thread.find_nearest_color_index(thread_palette) + # index will no longer be repeated. + thread_palette[repeated_index] = repeated_thread + palette.append(index) + last_index = index + last_thread = thread + + return palette + + def find_nearest_color_index(find_color, values): if isinstance(find_color, EmbThread): find_color = find_color.color red = (find_color >> 16) & 0xff green = (find_color >> 8) & 0xff blue = find_color & 0xff - closest_index = -1 - current_index = -1 + closest_index = None current_closest_value = float("inf") - for t in values: - current_index += 1 + for current_index, t in enumerate(values): if t is None: continue dist = color_distance_red_mean( diff --git a/pyembroidery/EmbThreadJef.py b/pyembroidery/EmbThreadJef.py index 47dbcef8..cc82de7d 100644 --- a/pyembroidery/EmbThreadJef.py +++ b/pyembroidery/EmbThreadJef.py @@ -3,7 +3,7 @@ def get_thread_set(): return [ - EmbThreadJef(0x000000, "Placeholder", "000"), + None, #EmbThreadJef(0x000000, "Placeholder", "000"), EmbThreadJef(0x000000, "Black", "002"), EmbThreadJef(0xffffff, "White", "001"), EmbThreadJef(0xffff17, "Yellow", "204"), diff --git a/pyembroidery/EmbThreadPec.py b/pyembroidery/EmbThreadPec.py index abb51e8e..d74cb172 100644 --- a/pyembroidery/EmbThreadPec.py +++ b/pyembroidery/EmbThreadPec.py @@ -3,7 +3,7 @@ def get_thread_set(): return [ - EmbThreadPec(0, 0, 0, "Unknown", "0"), + None, # EmbThreadPec(0, 0, 0, "Unknown", "0"), EmbThreadPec(14, 31, 124, "Prussian Blue", "1"), EmbThreadPec(10, 85, 163, "Blue", "2"), EmbThreadPec(0, 135, 119, "Teal Green", "3"), diff --git a/pyembroidery/ExyReader.py b/pyembroidery/ExyReader.py index 9a4645dd..d567926a 100644 --- a/pyembroidery/ExyReader.py +++ b/pyembroidery/ExyReader.py @@ -3,4 +3,4 @@ def read(f, out, settings=None): f.seek(0x100) - dst_read_stitches(f, out) + dst_read_stitches(f, out, settings) diff --git a/pyembroidery/JefReader.py b/pyembroidery/JefReader.py index b47e6d49..8b3d30e0 100644 --- a/pyembroidery/JefReader.py +++ b/pyembroidery/JefReader.py @@ -2,10 +2,23 @@ from .ReadHelper import read_int_32le, signed8 -def read_jef_stitches(f, out): - count = 0 +def read_jef_stitches(f, out, settings=None): + trims = False + command_count_max = 3 + trim_distance = 3.0 + if settings is not None: + command_count_max = settings.get('trim_at', command_count_max) + trims = settings.get("trims", trims) + trim_distance = settings.get("trim_distance", trim_distance) + if trim_distance is not None: + trim_distance *= 10 # Pixels per mm. Native units are 1/10 mm. + jump_count = 0 + jump_start = 0 + jump_dx = 0 + jump_dy = 0 + jumping = False + trimmed = True while True: - count += 1 b = bytearray(f.read(2)) if len(b) != 2: break @@ -13,6 +26,8 @@ def read_jef_stitches(f, out): x = signed8(b[0]) y = -signed8(b[1]) out.stitch(x, y) + trimmed = False + jumping = False continue ctrl = b[1] b = bytearray(f.read(2)) @@ -21,13 +36,42 @@ def read_jef_stitches(f, out): x = signed8(b[0]) y = -signed8(b[1]) if ctrl == 0x02: - if x == 0 and y == 0: - out.trim() - else: - out.move(x, y) + out.move(x, y) + if not jumping: + jump_dx = 0 + jump_dy = 0 + jump_count = 0 + jump_start = len(out.stitches) - 1 + jumping = True + jump_count += 1 + jump_dx += x + jump_dy += y + if not trimmed and\ + ( + ( + trims + and + jump_count == command_count_max + ) + or + ( + trim_distance is not None + and + ( + abs(jump_dy) > trim_distance or abs(jump_dx) > trim_distance + ) + ) + ): + out.trim(position=jump_start) + jump_start += 1 # We inserted a position, start jump has moved. + trimmed = True + if jump_dx == 0 and jump_dy == 0: # If our jump displacement is 0, they were part of a trim command. + out.stitches = out.stitches[:jump_start] continue if ctrl == 0x01: out.color_change(0, 0) + trimmed = True + jumping = False continue if ctrl == 0x10: break @@ -47,4 +91,4 @@ def read(f, out, settings=None): out.add_thread(jef_threads[index % len(jef_threads)]) f.seek(stitch_offset, 0) - read_jef_stitches(f, out) + read_jef_stitches(f, out, settings) diff --git a/pyembroidery/JefWriter.py b/pyembroidery/JefWriter.py index 102549b3..ae78ea3e 100644 --- a/pyembroidery/JefWriter.py +++ b/pyembroidery/JefWriter.py @@ -1,5 +1,6 @@ import datetime +import EmbThread from .EmbConstant import * from .EmbThreadJef import get_thread_set from .WriteHelper import write_string_utf8, write_int_32le, write_int_8 @@ -19,11 +20,15 @@ def write(pattern, f, settings=None): - trims = True + trims = False + command_count_max = 3 + date_string = datetime.datetime.today().strftime('%Y%m%d%H%M%S') if settings is not None: trims = settings.get("trims", trims) + command_count_max = settings.get('trim_at', command_count_max) date_string = settings.get("date", date_string) + pattern.fix_color_count() color_count = pattern.count_threads() offsets = 0x74 + (color_count * 8) @@ -42,7 +47,7 @@ def write(pattern, f, settings=None): point_count += 2 elif data == TRIM: if trims: - point_count += 2 + point_count += (2 * command_count_max) elif data == COLOR_CHANGE: point_count += 2 elif data == END: @@ -82,9 +87,11 @@ def write(pattern, f, settings=None): write_hoop_edge_distance(f, x_hoop_edge, y_hoop_edge) jef_threads = get_thread_set() - for thread in pattern.threadlist: - thread_index = thread.find_nearest_color_index(jef_threads) - write_int_32le(f, thread_index) + + palette = EmbThread.build_nonrepeat_palette(jef_threads, pattern.threadlist) + for t in palette: + write_int_32le(f, t) + for i in range(0, color_count): write_int_32le(f, 0x0D) @@ -108,8 +115,8 @@ def write(pattern, f, settings=None): write_int_8(f, -dy) continue elif data == TRIM: - if trims: - f.write(b'\x80\x02\x00\x00') + if trims: # command trim. + f.write(b'\x80\x02\x00\x00' * command_count_max) continue elif data == JUMP: f.write(b'\x80\x02') diff --git a/pyembroidery/PecWriter.py b/pyembroidery/PecWriter.py index 7228b8dc..817bae45 100644 --- a/pyembroidery/PecWriter.py +++ b/pyembroidery/PecWriter.py @@ -1,3 +1,4 @@ +import EmbThread from .EmbConstant import * from .EmbThreadPec import get_thread_set from .PecGraphics import get_blank, draw_scaled @@ -45,21 +46,9 @@ def write_pec_header(pattern, f, threadlist): thread_set = get_thread_set() - if len(thread_set) <= len(threadlist): - threadlist = thread_set[:] - # Data is corrupt. Cheat so it won't crash. - - chart = [None] * len(thread_set) - for thread in set(threadlist): - index = thread.find_nearest_color_index(thread_set) - thread_set[index] = None - chart[index] = thread - - color_index_list = [] - rgb_list = [] - for thread in threadlist: - color_index_list.append(thread.find_nearest_color_index(chart)) - rgb_list.append(thread.color) + color_index_list = EmbThread.build_unique_palette(thread_set, pattern.threadlist) + + rgb_list = [thread.color for thread in threadlist] current_thread_count = len(color_index_list) if current_thread_count is not 0: diff --git a/pyembroidery/PesWriter.py b/pyembroidery/PesWriter.py index c979486b..330fd7da 100644 --- a/pyembroidery/PesWriter.py +++ b/pyembroidery/PesWriter.py @@ -10,8 +10,8 @@ MAX_JUMP_DISTANCE = 2047 MAX_STITCH_DISTANCE = 2047 -VERSION_1 = 1 -VERSION_6 = 6 +VERSION_1 = 1.0 +VERSION_6 = 6.0 PES_VERSION_1_SIGNATURE = "#PES0001" PES_VERSION_6_SIGNATURE = "#PES0060" @@ -21,12 +21,17 @@ def write(pattern, f, settings=None): + version = VERSION_1 + truncated = False if settings is not None: - version = float(settings.get("pes version", VERSION_1)) + version = settings.get("pes version", VERSION_1) # deprecated, use "version". + version = settings.get("version", version) truncated = settings.get("truncated", False) - else: - version = VERSION_1 - truncated = False + if isinstance(version, str): + if version.endswith('t'): + truncated = True + version = float(version[:-1]) + version = float(version) if truncated: if version == VERSION_1: write_truncated_version_1(pattern, f) diff --git a/pyembroidery/PyEmbroidery.py b/pyembroidery/PyEmbroidery.py index 014834f0..32c0da93 100644 --- a/pyembroidery/PyEmbroidery.py +++ b/pyembroidery/PyEmbroidery.py @@ -55,7 +55,6 @@ import pyembroidery.XxxWriter as XxxWriter # import pyembroidery.ZhsReader as ZhsReader import pyembroidery.ZxyReader as ZxyReader - from .EmbPattern import EmbPattern @@ -72,6 +71,7 @@ def supported_formats(): yield ({ "description": "Brother Embroidery Format", "extension": "pec", + "extensions": ("pec"), "mimetype": "application/x-pec", "category": "embroidery", "reader": PecReader, @@ -81,19 +81,18 @@ def supported_formats(): yield ({ "description": "Brother Embroidery Format", "extension": "pes", + "extensions": ("pes"), "mimetype": "application/x-pes", "category": "embroidery", "reader": PesReader, "writer": PesWriter, - "options": { - "pes version": (1, 6), - "truncated": (True, False) - }, + "versions": ("1", "6", "1t", "6t"), "metadata": ("name", "author", "category", "keywords", "comments") }) yield ({ "description": "Melco Embroidery Format", "extension": "exp", + "extensions": ("exp"), "mimetype": "application/x-exp", "category": "embroidery", "reader": ExpReader, @@ -102,26 +101,42 @@ def supported_formats(): yield ({ "description": "Tajima Embroidery Format", "extension": "dst", + "extensions": ("dst"), "mimetype": "application/x-dst", "category": "embroidery", "reader": DstReader, "writer": DstWriter, - "options": { - "extended headers": (True, False) + "read_options": { + "trim_at": (2, 3, 4, 5, 6, 7, 8) }, - "metadata": ("name") + "write_options": { + "trim_at": (2, 3, 4, 5, 6, 7, 8) + }, + "versions": ("default", "extended"), + "metadata": ("name", "author", "copyright") }) yield ({ "description": "Janome Embroidery Format", "extension": "jef", + "extensions": ("jef"), "mimetype": "application/x-jef", "category": "embroidery", "reader": JefReader, "writer": JefWriter, + "read_options": { + "trim_distance": (None, 3.0, 50.0), + "trims": (True, False), + "trim_at": (2, 3, 4, 5, 6, 7, 8), + }, + "write_options": { + "trims": (True, False), + "trim_at": (2, 3, 4, 5, 6, 7, 8), + }, }) yield ({ "description": "Pfaff Embroidery Format", "extension": "vp3", + "extensions": ("vp3"), "mimetype": "application/x-vp3", "category": "embroidery", "reader": Vp3Reader, @@ -137,18 +152,17 @@ def supported_formats(): yield ({ "description": "Comma-separated values", "extension": "csv", + "extensions": ("csv"), "mimetype": "text/csv", "category": "debug", "reader": CsvReader, "writer": CsvWriter, - "options": { - "deltas": (True, False), - "displacement": (True, False) - }, + "versions": ("default", "delta", "full") }) yield ({ "description": "Singer Embroidery Format", "extension": "xxx", + "extensions": ("xxx"), "mimetype": "application/x-xxx", "category": "embroidery", "reader": XxxReader, @@ -157,6 +171,7 @@ def supported_formats(): yield ({ "description": "Janome Embroidery Format", "extension": "sew", + "extensions": ("sew"), "mimetype": "application/x-sew", "category": "embroidery", "reader": SewReader @@ -164,6 +179,7 @@ def supported_formats(): yield ({ "description": "Barudan Embroidery Format", "extension": "u01", + "extensions": ("u00", "u01", "u02"), "mimetype": "application/x-u01", "category": "embroidery", "reader": U01Reader, @@ -172,6 +188,7 @@ def supported_formats(): yield ({ "description": "Husqvarna Viking Embroidery Format", "extension": "shv", + "extensions": ("shv"), "mimetype": "application/x-shv", "category": "embroidery", "reader": ShvReader @@ -179,6 +196,7 @@ def supported_formats(): yield ({ "description": "Toyota Embroidery Format", "extension": "10o", + "extensions": ("10o"), "mimetype": "application/x-10o", "category": "embroidery", "reader": A10oReader @@ -186,6 +204,7 @@ def supported_formats(): yield ({ "description": "Toyota Embroidery Format", "extension": "100", + "extensions": ("100"), "mimetype": "application/x-100", "category": "embroidery", "reader": A100Reader @@ -193,6 +212,7 @@ def supported_formats(): yield ({ "description": "Bits & Volts Embroidery Format", "extension": "bro", + "extensions": ("bro"), "mimetype": "application/x-Bro", "category": "embroidery", "reader": BroReader @@ -200,6 +220,7 @@ def supported_formats(): yield ({ "description": "Sunstar or Barudan Embroidery Format", "extension": "dat", + "extensions": ("dat"), "mimetype": "application/x-dat", "category": "embroidery", "reader": DatReader @@ -207,6 +228,7 @@ def supported_formats(): yield ({ "description": "Tajima(Barudan) Embroidery Format", "extension": "dsb", + "extensions": ("dsb"), "mimetype": "application/x-dsb", "category": "embroidery", "reader": DsbReader @@ -214,6 +236,7 @@ def supported_formats(): yield ({ "description": "ZSK USA Embroidery Format", "extension": "dsz", + "extensions": ("dsz"), "mimetype": "application/x-dsz", "category": "embroidery", "reader": DszReader @@ -221,6 +244,7 @@ def supported_formats(): yield ({ "description": "Elna Embroidery Format", "extension": "emd", + "extensions": ("emd"), "mimetype": "application/x-emd", "category": "embroidery", "reader": EmdReader @@ -228,6 +252,7 @@ def supported_formats(): yield ({ "description": "Eltac Embroidery Format", "extension": "exy", # e??, e01 + "extensions": ("e00", "e01", "e02"), "mimetype": "application/x-exy", "category": "embroidery", "reader": ExyReader @@ -235,6 +260,7 @@ def supported_formats(): yield ({ "description": "Fortron Embroidery Format", "extension": "fxy", # f??, f01 + "extensions": ("f00", "f01", "f02"), "mimetype": "application/x-fxy", "category": "embroidery", "reader": FxyReader @@ -242,6 +268,7 @@ def supported_formats(): yield ({ "description": "Gold Thread Embroidery Format", "extension": "gt", + "extensions": ("gt"), "mimetype": "application/x-exy", "category": "embroidery", "reader": GtReader @@ -249,6 +276,7 @@ def supported_formats(): yield ({ "description": "Inbro Embroidery Format", "extension": "inb", + "extensions": ("inb"), "mimetype": "application/x-inb", "category": "embroidery", "reader": InbReader @@ -256,6 +284,7 @@ def supported_formats(): yield ({ "description": "Tajima Embroidery Format", "extension": "tbf", + "extensions": ("tbf"), "mimetype": "application/x-tbf", "category": "embroidery", "reader": TbfReader @@ -263,6 +292,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "ksm", + "extensions": ("ksm"), "mimetype": "application/x-ksm", "category": "embroidery", "reader": KsmReader @@ -270,6 +300,7 @@ def supported_formats(): yield ({ "description": "Happy Embroidery Format", "extension": "tap", + "extensions": ("tap"), "mimetype": "application/x-tap", "category": "embroidery", "reader": TapReader @@ -277,6 +308,7 @@ def supported_formats(): yield ({ "description": "Data Stitch Embroidery Format", "extension": "stx", + "extensions": ("stx"), "mimetype": "application/x-stx", "category": "embroidery", "reader": StxReader @@ -284,6 +316,7 @@ def supported_formats(): yield ({ "description": "Brother Embroidery Format", "extension": "phb", + "extensions": ("phb"), "mimetype": "application/x-phb", "category": "embroidery", "reader": PhbReader @@ -291,6 +324,7 @@ def supported_formats(): yield ({ "description": "Brother Embroidery Format", "extension": "phc", + "extensions": ("phc"), "mimetype": "application/x-phc", "category": "embroidery", "reader": PhcReader @@ -298,6 +332,7 @@ def supported_formats(): yield ({ "description": "Ameco Embroidery Format", "extension": "new", + "extensions": ("new"), "mimetype": "application/x-new", "category": "embroidery", "reader": NewReader @@ -305,6 +340,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "max", + "extensions": ("max"), "mimetype": "application/x-max", "category": "embroidery", "reader": MaxReader @@ -312,6 +348,7 @@ def supported_formats(): yield ({ "description": "Mitsubishi Embroidery Format", "extension": "mit", + "extensions": ("mit"), "mimetype": "application/x-mit", "category": "embroidery", "reader": MitReader @@ -319,6 +356,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "pcd", + "extensions": ("pcd"), "mimetype": "application/x-pcd", "category": "embroidery", "reader": PcdReader @@ -326,6 +364,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "pcq", + "extensions": ("pcq"), "mimetype": "application/x-pcq", "category": "embroidery", "reader": PcqReader @@ -333,6 +372,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "pcm", + "extensions": ("pcm"), "mimetype": "application/x-pcm", "category": "embroidery", "reader": PcmReader @@ -340,6 +380,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "pcs", + "extensions": ("pcs"), "mimetype": "application/x-pcs", "category": "embroidery", "reader": PcsReader @@ -347,6 +388,7 @@ def supported_formats(): yield ({ "description": "Janome Embroidery Format", "extension": "jpx", + "extensions": ("jpx"), "mimetype": "application/x-jpx", "category": "embroidery", "reader": JpxReader @@ -354,6 +396,7 @@ def supported_formats(): yield ({ "description": "Gunold Embroidery Format", "extension": "stc", + "extensions": ("stc"), "mimetype": "application/x-stc", "category": "embroidery", "reader": StcReader @@ -368,6 +411,7 @@ def supported_formats(): yield ({ "description": "ZSK TC Embroidery Format", "extension": "zxy", + "extensions": ("z00", "z01", "z02"), "mimetype": "application/x-zxy", "category": "embroidery", "reader": ZxyReader @@ -375,6 +419,7 @@ def supported_formats(): yield ({ "description": "Brother Stitch Format", "extension": "pmv", + "extensions": ("pmv"), "mimetype": "application/x-pmv", "category": "stitch", "reader": PmvReader, @@ -383,10 +428,11 @@ def supported_formats(): yield ({ "description": "PNG Format, Portable Network Graphics", "extension": "png", + "extensions": ("png"), "mimetype": "image/png", "category": "image", "writer": PngWriter, - "options": { + "write_options": { "background": (0x000000, 0xFFFFFF), "linewidth": (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) }, @@ -394,21 +440,21 @@ def supported_formats(): yield ({ "description": "txt Format, Text File", "extension": "txt", + "extensions": ("txt"), "mimetype": "text/plain", "category": "debug", "writer": TxtWriter, - "options": { - "mimic": (True, False), - }, + "versions": ("default", "embroidermodder") }) yield ({ "description": "gcode Format, Text File", "extension": "gcode", + "extensions": ("gcode", "g-code", "ngc", "nc", ".g"), "mimetype": "text/plain", "category": "embroidery", "reader": GcodeReader, "writer": GcodeWriter, - "options": { + "write_options": { "stitch_z_travel": (5.0, 10.0), }, }) diff --git a/pyembroidery/TapReader.py b/pyembroidery/TapReader.py index f4d1d811..5c63e09f 100644 --- a/pyembroidery/TapReader.py +++ b/pyembroidery/TapReader.py @@ -2,4 +2,4 @@ def read(f, out, settings=None): - dst_read_stitches(f, out) + dst_read_stitches(f, out, settings) diff --git a/pyembroidery/TxtWriter.py b/pyembroidery/TxtWriter.py index 06b2bc58..c43cc80e 100644 --- a/pyembroidery/TxtWriter.py +++ b/pyembroidery/TxtWriter.py @@ -32,7 +32,7 @@ def write_mimic(pattern, f): write_string_utf8(f, txt_line) -def write_basic(pattern, f): +def write_normal(pattern, f): names = get_common_name_dictionary() color_index = 0 color = pattern.get_thread_or_filler(color_index).color @@ -48,8 +48,14 @@ def write_basic(pattern, f): def write(pattern, f, settings=None): - mimic = settings is not None and "mimic" in settings + mimic = False + if settings is not None: + if "mimic" in settings: + mimic = True + version = settings.get("version", "default") + if version != "default": + mimic = True if mimic: write_mimic(pattern, f) - return - write_basic(pattern, f) + else: + write_normal(pattern, f) diff --git a/pyembroidery/XxxReader.py b/pyembroidery/XxxReader.py index 6f670715..66f24696 100644 --- a/pyembroidery/XxxReader.py +++ b/pyembroidery/XxxReader.py @@ -33,12 +33,13 @@ def read(f, out, settings=None): if x != 0 or y != 0: out.move(x, y) continue - elif b2 == 0x08: + elif b2 == 0x08 or 0x0A <= b2 <= 0x17: out.color_change() continue elif b2 == 0x7F: - out.end(0) - break + break # End + elif b2 == 0x18: + break # End out.end() f.seek(2, 1) for i in range(0, num_of_colors): From bdff66bfbf0e2bdd41c071c8ccc27833d0ea96c8 Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 05:27:52 -0700 Subject: [PATCH 02/16] Update Version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2761536c..f38e2019 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="pyembroidery", - version="1.3.10", + version="1.4.0", author="Tatarize", author_email="tatarize@gmail.com", description="Embroidery IO library", From 465b454b920a81d4e0f761db0dc5ae2b0bf94fa9 Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 14:27:01 -0700 Subject: [PATCH 03/16] Adding blank requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ + From 3194f0fa8b9b32665f32197d8340fef1081d65b0 Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 14:38:18 -0700 Subject: [PATCH 04/16] init with name --- __init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..d8449e84 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +name = "pyembroidery" From eaa47d841786cde0b53c4794c206575b754f6cc3 Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 14:38:56 -0700 Subject: [PATCH 05/16] Import Correction --- pyembroidery/JefWriter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyembroidery/JefWriter.py b/pyembroidery/JefWriter.py index ae78ea3e..a671e627 100644 --- a/pyembroidery/JefWriter.py +++ b/pyembroidery/JefWriter.py @@ -1,6 +1,6 @@ import datetime -import EmbThread +from .EmbThread import build_nonrepeat_palette from .EmbConstant import * from .EmbThreadJef import get_thread_set from .WriteHelper import write_string_utf8, write_int_32le, write_int_8 @@ -88,7 +88,7 @@ def write(pattern, f, settings=None): jef_threads = get_thread_set() - palette = EmbThread.build_nonrepeat_palette(jef_threads, pattern.threadlist) + palette = build_nonrepeat_palette(jef_threads, pattern.threadlist) for t in palette: write_int_32le(f, t) From 608febfbdf08e1d0055d6a0b210554eee0ed39f0 Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 14:44:01 -0700 Subject: [PATCH 06/16] Import Correction --- pyembroidery/PecWriter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyembroidery/PecWriter.py b/pyembroidery/PecWriter.py index 817bae45..7cc8b88f 100644 --- a/pyembroidery/PecWriter.py +++ b/pyembroidery/PecWriter.py @@ -1,4 +1,4 @@ -import EmbThread +from .EmbThread import build_unique_palette from .EmbConstant import * from .EmbThreadPec import get_thread_set from .PecGraphics import get_blank, draw_scaled @@ -46,7 +46,7 @@ def write_pec_header(pattern, f, threadlist): thread_set = get_thread_set() - color_index_list = EmbThread.build_unique_palette(thread_set, pattern.threadlist) + color_index_list = build_unique_palette(thread_set, pattern.threadlist) rgb_list = [thread.color for thread in threadlist] From 81839a361b3a4f8a20700ff05a1a1a6f391c47ca Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 15:12:10 -0700 Subject: [PATCH 07/16] correction for lowercase named color --- pyembroidery/EmbThread.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pyembroidery/EmbThread.py b/pyembroidery/EmbThread.py index f0ba27ca..54ab19fa 100644 --- a/pyembroidery/EmbThread.py +++ b/pyembroidery/EmbThread.py @@ -66,8 +66,7 @@ def find_nearest_color_index(find_color, values): def color_rgb(r, g, b): - return int(0xFF000000 | - ((r & 255) << 16) | + return int(((r & 255) << 16) | ((g & 255) << 8) | (b & 255)) @@ -97,7 +96,7 @@ def color_distance_red_mean( class EmbThread: def __init__(self, thread=None): - self.color = 0xFF000000 + self.color = 0x000000 self.description = None # type: str self.catalog_number = None # type: str self.details = None # type: str @@ -115,12 +114,12 @@ def __eq__(self, other): if other is None: return False if isinstance(other, int): - return (0xFF000000 | self.color) == (0xFF000000 | other) + return self.color & 0xFFFFFF == other & 0xFFFFFF if isinstance(other, str): - return (0xFF000000 | self.color) == (0xFF000000 | EmbThread.parse_string_color(other)) + return self.color & 0xFFFFFF == EmbThread.parse_string_color(other) & 0xFFFFFF if not isinstance(other, EmbThread): return False - if (0xFF000000 | self.color) != (0xFF000000 | other.color): + if self.color & 0xFFFFFF != other.color & 0xFFFFFF: return False if self.description != other.description: return False @@ -222,7 +221,7 @@ def set(self, thread): def parse_string_color(color): if color == "random": import random - return 0xFF000000 | random.randint(0, 0xFFFFFF) + return random.randint(0, 0xFFFFFF) if color[0:1] == "#": return color_hex(color[1:]) color_dict = { @@ -374,6 +373,5 @@ def parse_string_color(color): "yellow": color_rgb(255, 255, 0), "yellowgreen": color_rgb(154, 205, 50) } - if color in color_dict: - return color_dict[color] - return 0xFF000000 # Failed, so black. + return color_dict.get(color.lower(), 0x000000) + # return color or black. From c795f298d93aeb12c8f4cc20d35ab4408385c77e Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 15:45:42 -0700 Subject: [PATCH 08/16] Delete reorder.pes --- test/reorder.pes | Bin 8723 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/reorder.pes diff --git a/test/reorder.pes b/test/reorder.pes deleted file mode 100644 index 3fb13a752f185fc8d00ebff5b78dc1a045c8eb06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8723 zcmeHNO=ufO6n?XoY&o`MJHaVx2vso1(34wA+Uk-`?KnLIQgC~dAha^YPMbpr^i(q_ zSA$v#>BR?uedr+|A98VVASd@Hw_Jh+!9Cf~f-yF^q(sy2&CY6ftzBDDgVlm|SZ3Do5YQF%Cj@!Pbq>m`9pMi} zr+C1oaD>TzACjJrl+yEm>gko*Yv}j37aQ`+=i3b#p$%C*dfbpD0Nfg$Jn&+7OS=ud zQeMq3sbN8_!1s3)nH4o-6BN@mECBOI?HfZ4EZ^0~-_Y~HHUW#J zZ5znL^~SNYJWmU1ifXEY+>T99FML^EHom>^WjRj;bykmm8Jj@gEp02A8v5YNa-MRS zl^ZHc50zsRlz=bGbHL;p67Xetj^0#peXz_nLH+P$nK9Y7AHFP?NFZmePV~O92`mIl z+ZdC5PifyAYCNdt>3)4wk8Be(pnY?|;dvySa}8;=0iO!pt54Fyx?>YGsB1_AleF(3 zd|BSs$5OpSe6USW622@i(|(=vCgIDnLhh_sum6VJj!jSszASGT-xPdVPKpn)7I?gG zY=Va1%eEbjLpbL-CY2OdpGkhezY_(GSLRr!So!Id^s}@%DRuYghiib%1DH&^v1Hrno8o z5Ub*Q@!efKyGRzXV=OO~mn79!txBqLf!9oVQ|4tsW@Wa#g1uH>6&0$CY15j#ihWv6 zmw(2-hN$YkiCW???>?o`r9hwOedVyx;`MXyuNU#@YK90%SqM?I@B6qVN) zgRvOX!P=@GXj84cTwdV_2Tx;FarwGzTs#Nspt{9ThR-JW6_}sqReh#S(;989?C`tF z-AJ0K0xI0}@Hi^1v5G)3-D5nBRk={z*9n==IHHyvejGJcVGHRFf{%kJry1))MPIF& zkAsKK%PhY^VB5nJS`8)WVC=kI}Wc^<%VM z8SBlHEuZYyvz+lf-Fd{k|2H^FueMNL9v2HW+sF9mY%WJ+?gU3&gTKxa3OTBBthg{< zyb#0mI@^hI6krosiJxgeZZFE$tJlVn6NT0v(Rp1%$gIhzJnidWnPV$8Sat;8raD{6 zhM@57wW@E8qaFS(mx)A)xZ}08RsnJn9D#!ORes6@u9TM0+j*kBI?7RfXl)(|ZwQX3 zhaP;F@buhsJF4^cznhUD%2B-ho-J|=tko9YCoLOHTdSdkY*8I+QQr>7sJR#p=N0)) z!Q^TBmhrk-M{(@*->Y~LbhmB?x*h0tpxc3N2f7{TcA(pV&UHZlXo3&^?*+mSGg8u1 gg+InGZtrbBe~I(Qj}+9451!zkAn?P=ZTW)eAHXGylmGw# From 836dd3cd5930f866846fed4d1b38fbaa84775591 Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 15:46:13 -0700 Subject: [PATCH 09/16] Adding test code for palette functions. --- test/test_palette.py | 105 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 test/test_palette.py diff --git a/test/test_palette.py b/test/test_palette.py new file mode 100644 index 00000000..38a9470f --- /dev/null +++ b/test/test_palette.py @@ -0,0 +1,105 @@ +from __future__ import print_function + +import unittest + +from pyembroidery import * +from pyembroidery.EmbThreadPec import * +from EmbThread import build_unique_palette, build_nonrepeat_palette, build_palette + + +class TestPalettes(unittest.TestCase): + + def test_unique_palette(self): + """Similar elements should not plot to the same palette index""" + pattern = EmbPattern() + pattern += "#FF0001" + pattern += "Blue" + pattern += "Blue" + pattern += "Red" + threadset = get_thread_set() + palette = build_unique_palette(threadset,pattern.threadlist) + self.assertNotEqual(palette[0],palette[3]) # Red and altered Red + self.assertEqual(palette[1], palette[2]) # Blue and Blue + + def test_unique_palette_large(self): + """Excessive palette entries that all map, should be mapped""" + pattern = EmbPattern() + for x in range(0, 100): + pattern += "black" + threadset = get_thread_set() + palette = build_unique_palette(threadset, pattern.threadlist) + self.assertEqual(palette[0], palette[1]) + + def test_unique_palette_unmap(self): + """Excessive palette entries can't all map, should map what it can and repeat""" + pattern = EmbPattern() + for i in range(0, 100): + thread = EmbThread() + thread.set_color(i, i, i) + pattern += thread + threadset = get_thread_set() + palette = build_unique_palette(threadset, pattern.threadlist) + palette.sort() + + def test_unique_palette_max(self): + """If the entries equal the list they should all map.""" + pattern = EmbPattern() + threadset = get_thread_set() + for i in range(0, len(threadset)-2): + thread = EmbThread() + thread.set_color(i, i, i) + pattern += thread + palette = build_unique_palette(threadset, pattern.threadlist) + palette.sort() + for i in range(1,len(palette)): + self.assertNotEqual(palette[i-1], palette[i]) + + def test_nonrepeat_palette_moving(self): + """The almost same color should not get plotted to the same palette index""" + pattern = EmbPattern() + pattern += "Red" + pattern += "Blue" + pattern += "#0100FF" + pattern += "Red" + threadset = get_thread_set() + palette = build_nonrepeat_palette(threadset,pattern.threadlist) + self.assertEqual(palette[0],palette[3]) # Red and Red + self.assertNotEqual(palette[1], palette[2]) # Blue and altered Blue + + def test_nonrepeat_palette_stay_moved(self): + """An almost same moved, only temporary""" + pattern = EmbPattern() + pattern += "Red" + pattern += "Blue" + pattern += "#0100FF" + pattern += "Red" + pattern += "#0100FF" + threadset = get_thread_set() + palette = build_nonrepeat_palette(threadset,pattern.threadlist) + self.assertEqual(palette[0],palette[3]) # Red and Red + self.assertNotEqual(palette[1], palette[2]) # Blue and altered Blue + self.assertNotEqual(palette[2], palette[4]) # same color, but color was moved + + def test_nonrepeat_palette_same(self): + """The same exact same color if repeated should remain""" + pattern = EmbPattern() + pattern += "Red" + pattern += "Blue" + pattern += "#0000FF" # actual blue + pattern += "Red" + threadset = get_thread_set() + palette = build_nonrepeat_palette(threadset,pattern.threadlist) + self.assertEqual(palette[0],palette[3]) # Red and Red + self.assertEqual(palette[1], palette[2]) # Blue and Blue + + def test_palette(self): + """Similar colors map to the same index""" + pattern = EmbPattern() + pattern += "#FF0001" + pattern += "Blue" + pattern += "Blue" + pattern += "Red" + threadset = get_thread_set() + palette = build_palette(threadset,pattern.threadlist) + self.assertEqual(palette[0],palette[3]) # Red and altered Red + self.assertEqual(palette[1], palette[2]) # Blue and Blue From 681e778b55abdc41cd1c9b3935ac28a9141bbaa9 Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 16:45:25 -0700 Subject: [PATCH 10/16] option for "clipping", implemented trims_at --- pyembroidery/DstReader.py | 4 +++- pyembroidery/DstWriter.py | 11 ++++++++--- pyembroidery/JefReader.py | 4 +++- pyembroidery/PyEmbroidery.py | 4 +++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/pyembroidery/DstReader.py b/pyembroidery/DstReader.py index b951a1d2..d62b9241 100644 --- a/pyembroidery/DstReader.py +++ b/pyembroidery/DstReader.py @@ -60,8 +60,10 @@ def dst_read_header(f, out): def dst_read_stitches(f, out, settings=None): jump_count_max = 3 + clipping = True if settings is not None: jump_count_max = settings.get('trim_at', jump_count_max) + clipping = settings.get('clipping', clipping) sequin_mode = False jump_count = 0 jump_start = 0 @@ -103,7 +105,7 @@ def dst_read_stitches(f, out, settings=None): out.trim(position=jump_start) jump_start += 1 # We inserted a position, start jump has moved. trimmed = True - if jump_dx == 0 and jump_dy == 0: # If our jump displacement is 0, they were part of a trim command. + if clipping and jump_dx == 0 and jump_dy == 0: # jump displacement is 0, clip trim command. out.stitches = out.stitches[:jump_start] else: out.stitch(dx, dy) diff --git a/pyembroidery/DstWriter.py b/pyembroidery/DstWriter.py index 837908f9..1e2b1a15 100644 --- a/pyembroidery/DstWriter.py +++ b/pyembroidery/DstWriter.py @@ -102,11 +102,13 @@ def encode_record(x, y, flags): def write(pattern, f, settings=None): extended_header = False + trim_at = 3 if settings is not None: extended_header = settings.get("extended header", extended_header) # deprecated, use version="extended" version = settings.get("version", "default") if version == "extended": extended_header = True + trim_at = settings.get("trim_at", trim_at) bounds = pattern.bounds() name = pattern.get_metadata("name", "Untitled") @@ -166,8 +168,11 @@ def write(pattern, f, settings=None): xx += dx yy += dy if data == TRIM: - f.write(encode_record(2, 2, JUMP)) - f.write(encode_record(-4, -4, JUMP)) - f.write(encode_record(2, 2, JUMP)) + delta = -4 + f.write(encode_record(-delta/2, -delta/2, JUMP)) + for p in range(1,trim_at-1): + f.write(encode_record(delta, delta, JUMP)) + delta = -delta + f.write(encode_record(delta/2, delta/2, JUMP)) else: f.write(encode_record(dx, dy, data)) diff --git a/pyembroidery/JefReader.py b/pyembroidery/JefReader.py index 8b3d30e0..74b663db 100644 --- a/pyembroidery/JefReader.py +++ b/pyembroidery/JefReader.py @@ -3,6 +3,7 @@ def read_jef_stitches(f, out, settings=None): + clipping = True trims = False command_count_max = 3 trim_distance = 3.0 @@ -10,6 +11,7 @@ def read_jef_stitches(f, out, settings=None): command_count_max = settings.get('trim_at', command_count_max) trims = settings.get("trims", trims) trim_distance = settings.get("trim_distance", trim_distance) + clipping = settings.get('clipping', clipping) if trim_distance is not None: trim_distance *= 10 # Pixels per mm. Native units are 1/10 mm. jump_count = 0 @@ -65,7 +67,7 @@ def read_jef_stitches(f, out, settings=None): out.trim(position=jump_start) jump_start += 1 # We inserted a position, start jump has moved. trimmed = True - if jump_dx == 0 and jump_dy == 0: # If our jump displacement is 0, they were part of a trim command. + if clipping and jump_dx == 0 and jump_dy == 0: # jump displacement is 0, clip trim command. out.stitches = out.stitches[:jump_start] continue if ctrl == 0x01: diff --git a/pyembroidery/PyEmbroidery.py b/pyembroidery/PyEmbroidery.py index 32c0da93..5c996e8c 100644 --- a/pyembroidery/PyEmbroidery.py +++ b/pyembroidery/PyEmbroidery.py @@ -107,7 +107,8 @@ def supported_formats(): "reader": DstReader, "writer": DstWriter, "read_options": { - "trim_at": (2, 3, 4, 5, 6, 7, 8) + "trim_at": (2, 3, 4, 5, 6, 7, 8), + "clipping": (True, False) }, "write_options": { "trim_at": (2, 3, 4, 5, 6, 7, 8) @@ -127,6 +128,7 @@ def supported_formats(): "trim_distance": (None, 3.0, 50.0), "trims": (True, False), "trim_at": (2, 3, 4, 5, 6, 7, 8), + "clipping": (True, False) }, "write_options": { "trims": (True, False), From 2a4912f881de102a454c04664cb630842f631d9b Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 16:46:18 -0700 Subject: [PATCH 11/16] trims test routines --- test/test_trims_dst_jef.py | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/test_trims_dst_jef.py diff --git a/test/test_trims_dst_jef.py b/test/test_trims_dst_jef.py new file mode 100644 index 00000000..156a2b54 --- /dev/null +++ b/test/test_trims_dst_jef.py @@ -0,0 +1,79 @@ +from __future__ import print_function + +import unittest + +from pattern_for_tests import * +from pattern_fractal_for_test import * + + +class TestTrims(unittest.TestCase): + + def test_dst_trims(self): + file0 = "trim.dst" + pattern = EmbPattern() + generate(pattern) + write_dst(pattern, file0) + loaded_pattern = read_dst(file0) + self.assertIsNotNone(loaded_pattern) + self.assertNotEqual(loaded_pattern.count_stitch_commands(TRIM), 0) + self.addCleanup(os.remove, file0) + + def test_dst_trims_fail(self): + file0 = "trim.dst" + pattern = EmbPattern() + generate(pattern) + write_dst(pattern, file0) + loaded_pattern = read_dst(file0, {"trim_at": 50}) # Lines beyond 50 jumps get a trim. + self.assertIsNotNone(loaded_pattern) + self.assertEqual(loaded_pattern.count_stitch_commands(TRIM), 0) + self.addCleanup(os.remove, file0) + + def test_dst_trims_success(self): + file0 = "trim.dst" + pattern = EmbPattern() + generate(pattern) + write_dst(pattern, file0, {"trim_at": 50}) # We make trim jumps big enough. + loaded_pattern = read_dst(file0, {"trim_at": 50, "clipping": False}) # Lines beyond 50 jumps get a trim. + self.assertIsNotNone(loaded_pattern) + self.assertNotEqual(loaded_pattern.count_stitch_commands(TRIM), 0) + self.addCleanup(os.remove, file0) + + def test_jef_trims(self): + file0 = "trim.jef" + pattern = EmbPattern() + generate(pattern) + write_jef(pattern, file0) + loaded_pattern = read_jef(file0) + self.assertIsNotNone(loaded_pattern) + self.assertNotEqual(loaded_pattern.count_stitch_commands(TRIM), 0) + self.addCleanup(os.remove, file0) + + def test_jef_trims_off(self): + file0 = "trim.jef" + pattern = EmbPattern() + generate(pattern) + write_jef(pattern, file0) + loaded_pattern = read_jef(file0, {"trim_distance": None}) + self.assertEqual(loaded_pattern.count_stitch_commands(JUMP), 15) + self.assertIsNotNone(loaded_pattern) + self.assertEqual(loaded_pattern.count_stitch_commands(TRIM), 0) + self.addCleanup(os.remove, file0) + + def test_jef_trims_commands(self): + file0 = "trim.jef" + pattern = EmbPattern() + generate(pattern) + write_jef(pattern, file0, {"trims": True}) + loaded_pattern = read_jef(file0, {"trim_distance": None}) + self.assertEqual(loaded_pattern.count_stitch_commands(JUMP), 15) + self.assertEqual(loaded_pattern.count_stitch_commands(TRIM), 0) + loaded_pattern = read_jef(file0, {"trim_distance": None, "clipping": False}) + self.assertEqual(loaded_pattern.count_stitch_commands(JUMP), 21) + self.assertEqual(loaded_pattern.count_stitch_commands(TRIM), 0) + loaded_pattern = read_jef(file0, {"trim_distance": None, "clipping": False, "trims": True, }) + self.assertEqual(loaded_pattern.count_stitch_commands(JUMP), 21) + self.assertEqual(loaded_pattern.count_stitch_commands(TRIM), 2) + loaded_pattern = read_jef(file0, {"trim_distance": None, "trims": True}) + self.assertEqual(loaded_pattern.count_stitch_commands(JUMP), 15) + self.assertEqual(loaded_pattern.count_stitch_commands(TRIM), 2) + self.addCleanup(os.remove, file0) From 2ce3e20fe27dff95d0341d7b85cd2a02a720bd3e Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 16:49:43 -0700 Subject: [PATCH 12/16] Import Correction For Test --- test/test_palette.py | 2 +- test/test_trims_dst_jef.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_palette.py b/test/test_palette.py index 38a9470f..bf8c1af7 100644 --- a/test/test_palette.py +++ b/test/test_palette.py @@ -4,7 +4,7 @@ from pyembroidery import * from pyembroidery.EmbThreadPec import * -from EmbThread import build_unique_palette, build_nonrepeat_palette, build_palette +from pyembroidery.EmbThread import build_unique_palette, build_nonrepeat_palette, build_palette class TestPalettes(unittest.TestCase): diff --git a/test/test_trims_dst_jef.py b/test/test_trims_dst_jef.py index 156a2b54..a26f6890 100644 --- a/test/test_trims_dst_jef.py +++ b/test/test_trims_dst_jef.py @@ -2,6 +2,7 @@ import unittest +from pyembroidery import * from pattern_for_tests import * from pattern_fractal_for_test import * From f81813575131e58e0fdb98fc3d68e9539da073dc Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 16:53:54 -0700 Subject: [PATCH 13/16] implemented a dunder str --- pyembroidery/EmbThread.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyembroidery/EmbThread.py b/pyembroidery/EmbThread.py index 54ab19fa..aac4318c 100644 --- a/pyembroidery/EmbThread.py +++ b/pyembroidery/EmbThread.py @@ -1,3 +1,4 @@ + def build_unique_palette(thread_palette, threadlist): """Turns a threadlist into a unique index list with the thread palette""" chart = [None] * len(thread_palette) # Create a lookup chart. @@ -138,6 +139,12 @@ def __eq__(self, other): def __hash__(self): return self.color & 0xFFFFFF + def __str__(self): + if self.description is None: + return "EmbThread %s" % self.hex_color() + else: + return "EmbThread %s %s" % self.description, self.hex_color() + def set_color(self, r, g, b): self.color = color_rgb(r, g, b) From f135d66f4483ff27ec8a20f9182b53ccfd7005db Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 17:00:26 -0700 Subject: [PATCH 14/16] moved fractal pattern to test patterns --- test/pattern_for_tests.py | 85 +++++++++++++++++++++++++++++++++++++- test/test_convert_pes.py | 1 + test/test_overloads.py | 2 + test/test_trims_dst_jef.py | 19 +++------ 4 files changed, 92 insertions(+), 15 deletions(-) diff --git a/test/pattern_for_tests.py b/test/pattern_for_tests.py index 790bc6df..ba6ce95e 100644 --- a/test/pattern_for_tests.py +++ b/test/pattern_for_tests.py @@ -1,5 +1,63 @@ from pyembroidery import * +import math + + +def evaluate_lsystem(symbol, rules, depth): + if depth <= 0 or symbol not in rules: + symbol() + else: + for produced_symbol in rules[symbol]: + evaluate_lsystem(produced_symbol, rules, depth - 1) + + +class Turtle: + def __init__(self, pattern): + self.pattern = pattern + self.angle = 0 + self.x = 0 + self.y = 0 + import math + self.turn_amount = math.pi / 3 + + def forward(self, distance): + self.x += distance * math.cos(self.angle) + self.y += distance * math.sin(self.angle) + self.pattern.add_stitch_absolute(STITCH, self.x, self.y) + # self.pattern.add_stitch_absolute(SEQUIN_EJECT, self.x, self.y) + + def turn(self, angle): + self.angle += angle + + def move(self, distance): + self.x += distance * math.cos(self.angle) + self.y += distance * math.sin(self.angle) + + def add_gosper(self): + a = lambda: self.forward(20) + b = lambda: self.forward(20) + l = lambda: self.turn(self.turn_amount) + r = lambda: self.turn(-self.turn_amount) + initial = lambda: None + rules = { + initial: [a], + a: [a, l, b, l, l, b, r, a, r, r, a, a, r, b, l], + b: [r, a, l, b, b, l, l, b, l, a, r, r, a, r, b] + } + evaluate_lsystem(initial, rules, 3) # 4 + + def add_serp(self): + a = lambda: self.forward(20) + b = lambda: self.forward(20) + l = lambda: self.turn(self.turn_amount) + r = lambda: self.turn(-self.turn_amount) + initial = lambda: None + rules = { + initial: [a], + a: [b, l, a, l, b], + b: [a, r, b, r, a] + } + evaluate_lsystem(initial, rules, 3) # 6 def get_big_pattern(): pattern = EmbPattern() @@ -81,11 +139,11 @@ def get_simple_pattern(): return pattern -def get_random_pattern_large(): +def get_random_pattern_large(count=1000): pattern = EmbPattern() import random - for i in range(0, 1000): + for i in range(0, count): pattern.add_block( [(random.uniform(-500, 500), random.uniform(-500, 500)), (random.uniform(-500, 500), random.uniform(-500, 500)), @@ -115,3 +173,26 @@ def get_random_pattern_small_halfs(): (random.randint(-500, 500) / 2.0, random.randint(-500, 500) / 2.0)], random.randint(0x000000, 0xFFFFFF)) return pattern + + +def get_fractal_pattern(): + pattern = EmbPattern() + turtle = Turtle(pattern) + turtle.add_gosper() + pattern.add_command(COLOR_BREAK) + turtle.move(500) + turtle.add_serp() + pattern.add_command(SEQUENCE_BREAK) + pattern.add_command(STOP) + turtle.move(50) + turtle.add_serp() + pattern.add_command(SEQUENCE_BREAK) + turtle.turn(-math.pi / 3) + turtle.move(500) + turtle.add_serp() + pattern.add_command(COLOR_BREAK) + turtle.turn(-math.pi / 3) + turtle.move(500) # 260, -450 + turtle.add_gosper() + pattern.add_command(END) + return pattern diff --git a/test/test_convert_pes.py b/test/test_convert_pes.py index ff9dcddf..937a00d1 100644 --- a/test/test_convert_pes.py +++ b/test/test_convert_pes.py @@ -4,6 +4,7 @@ from pyembroidery import * from pattern_for_tests import * + class TestConverts(unittest.TestCase): def position_equals(self, stitches, j, k): diff --git a/test/test_overloads.py b/test/test_overloads.py index 91bae62d..3e7ef244 100644 --- a/test/test_overloads.py +++ b/test/test_overloads.py @@ -88,3 +88,5 @@ def test_matrix(self): m2.post_scale(2) m2.post_rotate(30) self.assertEqual(catted, m2) + + diff --git a/test/test_trims_dst_jef.py b/test/test_trims_dst_jef.py index a26f6890..b5a6f124 100644 --- a/test/test_trims_dst_jef.py +++ b/test/test_trims_dst_jef.py @@ -4,15 +4,13 @@ from pyembroidery import * from pattern_for_tests import * -from pattern_fractal_for_test import * class TestTrims(unittest.TestCase): def test_dst_trims(self): file0 = "trim.dst" - pattern = EmbPattern() - generate(pattern) + pattern = get_fractal_pattern() write_dst(pattern, file0) loaded_pattern = read_dst(file0) self.assertIsNotNone(loaded_pattern) @@ -21,8 +19,7 @@ def test_dst_trims(self): def test_dst_trims_fail(self): file0 = "trim.dst" - pattern = EmbPattern() - generate(pattern) + pattern = get_fractal_pattern() write_dst(pattern, file0) loaded_pattern = read_dst(file0, {"trim_at": 50}) # Lines beyond 50 jumps get a trim. self.assertIsNotNone(loaded_pattern) @@ -31,8 +28,7 @@ def test_dst_trims_fail(self): def test_dst_trims_success(self): file0 = "trim.dst" - pattern = EmbPattern() - generate(pattern) + pattern = get_fractal_pattern() write_dst(pattern, file0, {"trim_at": 50}) # We make trim jumps big enough. loaded_pattern = read_dst(file0, {"trim_at": 50, "clipping": False}) # Lines beyond 50 jumps get a trim. self.assertIsNotNone(loaded_pattern) @@ -41,8 +37,7 @@ def test_dst_trims_success(self): def test_jef_trims(self): file0 = "trim.jef" - pattern = EmbPattern() - generate(pattern) + pattern = get_fractal_pattern() write_jef(pattern, file0) loaded_pattern = read_jef(file0) self.assertIsNotNone(loaded_pattern) @@ -51,8 +46,7 @@ def test_jef_trims(self): def test_jef_trims_off(self): file0 = "trim.jef" - pattern = EmbPattern() - generate(pattern) + pattern = get_fractal_pattern() write_jef(pattern, file0) loaded_pattern = read_jef(file0, {"trim_distance": None}) self.assertEqual(loaded_pattern.count_stitch_commands(JUMP), 15) @@ -62,8 +56,7 @@ def test_jef_trims_off(self): def test_jef_trims_commands(self): file0 = "trim.jef" - pattern = EmbPattern() - generate(pattern) + pattern = get_fractal_pattern() write_jef(pattern, file0, {"trims": True}) loaded_pattern = read_jef(file0, {"trim_distance": None}) self.assertEqual(loaded_pattern.count_stitch_commands(JUMP), 15) From 42c52e730b50e9495e976f06b55eeef75dab6ff0 Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 23:05:39 -0700 Subject: [PATCH 15/16] Update README.md --- README.md | 130 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index eba0fd99..49607f71 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,12 @@ pip install pyembroidery Any suggestions or comments please raise an issue on the github. -pyembroidery was coded from the ground up with all projects in mind. It includes a lot of higher level and middle level pattern composition abilities, and should accounts for any knowable error. It should be highly robust with a simple api so as to be reasonable for *any* python embroidery project. +pyembroidery was coded from the ground up with all projects in mind. It includes a lot of higher level and middle level pattern composition abilities, and should accounts for any knowable error. If you know an error it does not account for, raise an issue. It should be highly robust with a simple api so as to be reasonable for *any* python embroidery project. It should be complex enough to go very easily from points to stitches, fine grained enough to let you control everything, and good enough that you shouldn't want to. -Mandate ---- +## Mandate pyembroidery must to be small enough to be finished in short order and big enough to pack a punch. * pyembroidery must read and write: PES, DST, EXP, JEF, VP3. @@ -30,12 +29,12 @@ Pyembroidery fully meets and exceeds all of these requirements. * SEQUINS work in all supported formats (.dst) that are known to support sequins. Further it supports SEQUIN to JUMP operations on the other formats. * It is currently fully compatable with Python 2.7 and Python 3.6 -Philosophy ---- + +## Philosophy Pyembroidery will always attempt to minimize information loss. Embroidery reading and writing, the exporting and importing of embroidery files, is always lossy. If there is information in a file, it is within the purview of the project (but not the mandate) to read that information and provide it to the user. If information can be written to a file, it is within the purview of the project to write that information to the file or provide means by which that can be done. * Low level commands: Those commands actually found in binary encoded embroidery files. - * Low level commands will be transcribed and preserved in their exact order, unless doing so will cause an error. + * Low level commands will be transcribed and preserved in their exact order, unless doing so will cause an error or lose information. * Middle level commands: Useful ways of thinking about blocks of low level commands. Commands which describe the way the low level commands are encoded, but are not themselves commands executed by embroidery machines. * Middle level commands will be helpful and converted to low-level commands during writing events. * These will often be context sensitive converting to slightly different low level commands depending on intended writer, or encoder settings. @@ -43,21 +42,21 @@ Pyembroidery will always attempt to minimize information loss. Embroidery readin * High level commands will not exist within this project. Other reasonable elements: -* Higher level objects like .PES or .THR containing shapes are currently ignored in favor of reading raw stitches. However, loading such things would be less lossy and thus within the scope of the project. +* Higher level objects like .PES or .THR containing shapes are currently ignored in favor of reading raw stitches. However, loading those elements would be less lossy and thus within the scope of the project. * Conversions from raw low level commands to some middle level interpretations or iterable generators are provided in the EmbPattern class. Additional methods are entirely reasonable feature requests. +* Complex functionality that requires assistence, especially in cases with significant edge conditions, for example merging patterns. -How it works: ---- -Readers are sent a fileobject and an EmbPattern and parses the file, filling in the metadata, threads, stitches, with as much valid information as the file contains. +## Overview +Readers are sent a fileobject/stream, an EmbPattern and sometimes a settings dict. The reader parses the file, adding in metadata, threads, and stitches with as much valid information as the file contains. -EmbPattern objects contain all the relevant data. You can iterate stitch blocks .get_as_stitchblocks() or access the raw-stitches, threads, or metadata. +EmbPattern objects contain all relevant data. You can iterate stitch blocks .get_as_stitchblocks() or access the raw-stitches, threads, or metadata. -Writers are called to save a pattern to disk. They save raw-stitch data to disk. This data may, however, not be formatted in a way the writer can utilize effectively. For this reason, writers (except csv) utilize the encoder to ensure whatever the input is will be presented to the writer in a manner coherent to the writer. +Writers are called to save a pattern to disk. The writers save raw-stitch data to disk. This data may, however, not be formatted in a way the writer can utilize effectively. For this reason, writers (except lossless csv) utilize the encoder to ensure whatever the data is in the pattern will be presented to the writer in a manner coherent to the writer. -The encoder encode a low level version of the commands in the pattern, not just from low level but also middle-level commands implemented with the encoder. The writer contain format specific information with which to call to the encoder with some format specific values. Each export will reencode the data for the format, without modifying or altering the original data, as doing so would be lossy. +The encoder encode a low level version of the commands in the pattern, not just from low level but also middle-level commands implemented with the encoder. The writer contain format specific information which is given to the encoder. Each export will reencode the data for the format, without modifying or altering the original data, as doing so would be lossy. -The encoder call can be made directly on the EmbPattern with .get_normalized_pattern() on the pattern, this returns a new pattern. Neither, encoding nor saving will modify a pattern, rather they either return a new pattern or utilize one. Most operations performed on the data will have some degree of loss. So there is a level of isolation between lossy operation converting the pattern. +The encoder call can be made directly on the EmbPattern with .get_normalized_pattern() on the pattern, this returns a new pattern. Neither encoding, nor saving, will modify a pattern. The either return a new pattern or utilize one provided. Most operations performed on the data will have some degree of loss, so there is a level of isolation between lossy operation converting the pattern. * Read * File -> Reader -> Pattern @@ -66,30 +65,26 @@ The encoder call can be made directly on the EmbPattern with .get_normalized_pat * Convert * File -> Reader -> Pattern -> Encoder -> Pattern -> Writer -> File -EmbPattern ---- +## EmbPattern EmbPattern objects contain three primary elements: * `stitches`: This is a `list` of lists of three elements [x, y, command] * `threadlist`: This is a `list` of EmbThread class objects. * `extras`: This is a `dict` of various types of things that were found or referenced within the reading or desired for the writing of the file such as metadata info about the label declared within a file or some internal 1 bit graphics found within the embroidery file. -EmbPattern Stitches ---- +### EmbPattern Stitches The stitches contain absolute locations x, y and command. Commands are found defined within the EmbConstant.py file and should be referenced by name rather than value. The commands are the lower 8 bits of the command value. The upper bits of the command values are reserved for additional information (See Thread Changes). For best practices these should be masked off `stitch[stitch_index][2] & COMMAND_MASK`. -EmbPattern Threadlist ---- +### EmbPattern Threadlist The threadlist is a reference table of threads and the information about those threads. By default, if not explitly specified, the threadlist is utilized in the order given. Prior to `pyembroidery` version 1.3, this was the only method to use these. Usually is it sufficient to provide a thread for each color change in the sequence. However, if a color is not provided one, one will be invented when writing to a format that requires one. In some cases like .dst files, no colors exists so this will simply be ignored (except if extended headers are requested as those give a color sequence). The colors are checked and validated during the encoding process, so specifying these elements with greater detail is explitly possible. See Thread Changes for more details. -EmbPattern Extras ---- +### EmbPattern Extras This can largely be ignored except in cases when the metadata within the file matters. If for example you wish to read files and find the label that exists inside many different embroidery file times, the resulting value will be put into extras. This is to store the metadata and sometimes transfer the metadata from one format type to another. So an internal label might be able to be transferred between a .dst file and .pes file without regard to the external file name. Or the 1-bit images within a PEC file could be viewed. -Thread Changes ---- -Some formats do not explicitly use a `COLOR_CHANGE` command, some of them use `NEEDLE_SET` in order to change the thread. The difference here is notable. A color change goes to the next needle in a list usually set within the machine or within the file for the next color of thread. However, some machines like Barudan use what is most properly a needle change. They do not specify the color, but explicitly specifies the needle to be used. This often includes beginning writing the file by explictly setting the current needle, if omitted most machines use the current needle. Setting the same needle again will produce no effect. So often color changes occur between different thread usages, but needle sets occur at the start of each needle usage. Calling for a color change, requires that something be changed, and the machine often stopped. Calling for a needle_set may set the value to the current needle. -When data is loaded from a source with needle set commands. These `NEEDLE_SET` commands are explicitly used rather than color changes as they more accurately represent the original intent of the file. During a write, the encoder will transcribe these in the way requested by the writer settings (as determined by the format itself), using correct thread change command indicated and accounting for the implicit differences. +## Thread Changes +Some formats do not explicitly use a `COLOR_CHANGE` command, some of them use `NEEDLE_SET` in order to change the thread. The difference here is notable. A color change goes to the next needle in a list usually set within the machine or within the file for the next color of thread. However, some machines like Barudan use what is most properly a needle change. They do not specify the color, but explicitly specifies the needle to be used. This often means the current needle is set at the start. But, if omitted most machines use the current needle set in the machine. Setting the same needle again will produce no effect. Color changes occur between different thread usages, but needle sets occur at the start of each needle usage. Calling for a color change, requires that something be changed, and the machine often stopped. Calling for a needle_set may set the value to the current needle. + +When data is loaded from a source with needle set commands. These `NEEDLE_SET` commands are explicitly used rather than color changes as they more accurately represent the original intent of the file. During a write, the encoder will transcribe these in the way requested by the writer settings (as determined by the format itself), using the correct thread change command indicated and accounting for the implicit differences. There are some cases where one software suite will encode U01 (Barudan formating with needle sets) commands such that, rather than using needle set, it simply uses `STOP` commands (techincally in this case C00 or needle #0), while other software will cycle through a list of a few needles, indicating more explicitly these are changes. @@ -105,8 +100,9 @@ There are some other use cases when writing this data out, you could, for exampl In most cases this information isn't going to matter, but it is provided because it is information sometimes contained within the embroidery file. For writing this information, there are quite often other ways to specify it, but `pyembroidery` tends to be overbuilt by design to capture most known and unknown usecases. -Formats: ---- +## File I/O + +### Formats: Pyembroidery will write: * .pes (mandated) @@ -116,7 +112,7 @@ Pyembroidery will write: * .vp3 (mandated) * .u01 * .pec -* .xxx (experimental) +* .xxx * .csv * .svg * .png @@ -164,22 +160,26 @@ Pyembroidery will read: * .csv * .gcode -Writing to CSV: +#### Versions +They .get_supported_formats() in pyembroidery provides an element called 'versions' this will contain a tuple of values which can be passed to the settings object as `{'version': }`. This provides much of the version controls for the types of outputs provided within each writer. For example there is an extended header version of dst called extended, there's "6" and "6t" in the PesWriter which export other and different versions of the file. + +This is also intended as a good method to create new versions. For example, gcode can control a great many thing and varies greatly from one purpose to another and would be ideal for different versions. There's also different machines which may require different tweaks and these would be extended as versions. + +#### Writing to CSV: Prints out a workable CSV file with the given data. Starting in 1.3 the csv patterns are written without being encoded. The CSV format is, in this form, lossless. If you wish to encode before you write the file you can set the encoder to True and override the default. `write_csv(pattern, "file.csv", {"encode": True})` -Writing to PNG: +#### Writing to PNG: Writes to a image/png file. -Writing to TXT: +#### Writing to TXT: Writes to a text file. Generally lossy, it does not write threads or metadata, but certainly more easily parsed for a number of homebrew applications. The "mimic" option should mimic the embroidermodder functionality for exporting to txt files. By default it exports a bit less lossy giving the proper command indexes and their explicit names. -Writing to Gcode: +#### Writing to Gcode: The Gcode is intended for a number of hobbiest projects that use a gcode controller to operate a sewing machine, usually X,Y for plotter and Z to turn the handwheel. However, if you have a hobbiest project and need a different command structure feel free to ask or discuss it by raising an issue. -Reading: ---- +### Reading ```python import pyembroidery @@ -225,8 +225,7 @@ If you intend to write the merged pattern as a single unended pattern, convert t stitch[2] = pyembroidery.NO_COMMAND ``` -Writing: ---- +## Writing To write to a pattern do disk: @@ -292,8 +291,22 @@ Explicitly calling TIE_ON or TIE_OFF within the command sequence performs the se `explicit_trim` sets whether the encoder should overtly include a trim before color change event or not. Default is False. Setting this to True will include a trim if we are going to perform a thread-change action. -Conversion: ---- +## Manipulation + +There are many fully qualified methods of manipulating patterns. For example if you want to add a pattern to another pattern, +```python +pattern1 += pattern2 +``` + +You can also do pattern3 = pattern1 + pattern2 but that requires making an new pattern. With the `__iadd__()` dunder you can also perform actions like adding a colorchange. + +`pattern1 += "red"` will add a color change (if correct to do so), and a red thread to the threadlist. + +Other elements like `pattern += ((0,0), (20,20), (0,0))` will also work to append stitches. + +You can get a particular stitch of the pattern using `pattern[0]`. You can set string metadata elements `pattern['name'] = "My Design"` + +## Conversion As pyembroidery is a fully fleshed out reader/writer within the mandate, it also does conversion. @@ -307,8 +320,7 @@ This will read the embroidery.jef file in JEF format and will export it as conve You can load the file and call some of the helper functions to process the data like, get_pattern_interpolate_trim(), or get_stablized_pattern(). If there's a completely reasonable way to post-process loaded data that isn't accounted for raise an issue. This is still an open question. Since 1.3 the improved conversion testing means most conversions should overtly work. -Composing a pattern: ---- +## Composing a pattern * Use core commands to compose a pattern * Use shorthand commands to compose a pattern @@ -337,6 +349,11 @@ pattern.add_stitch_absolute(JUMP, x, y) pattern.add_command(command) ``` +You can insert a relative stitch inside a pattern. Note that it is relative to the stitch it's being inserted at. This will cause problems if that is a command without x, y operands. +```python +pattern.trim(position=48) +``` + The relative and absolute markers determine whether the numbers given are relative to the last position or an absolute location. Calling add_command does not update the internal record of position. These are taken as positionless and the x and y are taken as parameters. Adding a command that is explicitly positioned with add_command will have undefined behavior. NOTE: the order here is `command, x, y`, not `x, y command`. Python is good with letting you omit values at the end. And the command is *always* needed but the dx, dy can be dropped quite reasonably. While internally these are stored as [x, y, command] mostly to facilitate using them directly as positions. @@ -373,8 +390,8 @@ pattern.trim() pattern.color_change() ``` -StitchBlocks: ---- +## StitchBlocks + Conceptually a lot of embroidery can be thought of as unbroken blocks of stitches. Given the ubiquity of this, pyembroidery allows several methods for manipulating stitchblocks for reading and writing. The stitches within pyembroidery are a list of lists, with each 3 values. x, y, command. The stitchblocks given by commands like .get_as_stitchblock() are subsections of this. For adding stitches like with .add_stitchblock(), iterable set of objects with stitch.command, stitch.x, stitch.y will also works for adding a stitch block to a pattern. @@ -417,8 +434,7 @@ pattern.add_block([0, 0, 0, 100, 100, 100, 100, 0, 0, 0], "#f00") When a call is made to add_stitchblock(), the thread object is required to know whether the current thread is different than the previous one. If a different thread is detected pyembroidery will append a COLOR_BREAK rather than SEQUENCE_BREAK after adding the stitches into the pattern. Depending on your use case, you could implement this yourself using singular calls to add_stitch_relative() or add_stitch_absolute() and then determine the type of break with COLOR_BREAK or SEQUENCE_BREAK afterwards. -Middle-Level Commands: ----- +### Middle-Level Commands: The middle-level commands, as they currently stand: * SET_CHANGE_SEQUENCE - Sets the thread change sequence according to the encoded values. Setting the needle, thread-color, and order of where this occurs. See Thread Changes for more info. @@ -477,8 +493,8 @@ STITCH_BREAK Stitch break is only needed for reallocating jumps. It requires that the long stitch contingency is needle_to for the next stitch and any existing jumps directly afterwards are ignored. This causes the jump sequences to reallocate. If an existing jump sequence exists because it was loaded from a file and fed into a write routine. The write routine may only seek a contingency for the long jumps by providing extra subdivisions, because low level commands are only tweaked if a literal transcription would cause errors. However, calling pattern.get_pattern_merge_jumps() returns a pattern with all sequences of JUMP replaced with a single STITCH_BREAK command which is middle level and converted by the encoder into a series of jumps produced by the encoder rather than directly transcribed from their current sequence. -Stitch Contingency ---- +### Stitch Contingency + The encoder needs to decide what to do when a stitch is too long. The current modes here are: * CONTINGENCY_NEEDLE_JUMP (default) * CONTINGENCY_SEW_TO @@ -486,8 +502,8 @@ The encoder needs to decide what to do when a stitch is too long. The current mo When a stitch is beyond max_stitch (whether set by the format or by the user) it must deal with this event, however opinions differ as to how what a stitch beyond the maximum should do. If it is your intent that STITCH means SEW_TO this location then setting the stitch contingency to SEW_TO will create a series of stitches until we get to the end location. If you use the command SEW_TO this overtly works like a stitch with CONTINGENCY_SEW_TO. Likewise NEEDLE_AT is the STITCH flavor that jumps to to the end location and then stitches. If you set CONTINGENCY_NONE then no contingency method is used, long stitches are simply fed to the writer as they appear which may throw an error or crash. -Sequin Contingency ---- +### Sequin Contingency + The encoder needs to decide what to do when there are sequins in a pattern. The current modes here are: * CONTINGENCY_SEQUIN_UTILIZE - sets the equin contingency to use the sequin information. * CONTINGENCY_SEQUIN_JUMP - Sets the sequin contingency to call the sequins jumps. @@ -496,18 +512,18 @@ The encoder needs to decide what to do when there are sequins in a pattern. The Sequins being written into files that do not support sequins can go several ways, the two typical methods are JUMP and STITCH, this means to replace the SEQUIN_EJECTs with JUMP. This will allow some machines to manually enable sequins for a particular section and interpret the JUMPs as stitches. It is known that some Barudan machines have this ability. The other typical mode is STITCH which will preserve viewable structure of the underlying pattern while destroying the information of where the JUMPs were. With the JUMPs some data will appear to be corrupted, with STITCHes the data will look correct except without the sequins but the information is lost and not recoverable. REMOVE is given for completeness, but it calls all SEQUIN_EJECT commands NO OPERATIONS as if they don't appear in the pattern at all. -Tie On / Tie Off Contingency ---- +### Tie On / Tie Off Contingency + While there's only NONE, and THREE_SMALL for contingencies here, both the tie-on and tie-off contingencies are setup to be forward compatabile with other future potential tie-on and tie-off methods. -Units ---- +### Units + * The core units are 1/10th mm. This is what 1 refers to within most formats, and internally within pyembroidery itself. You are entirely permitted to use floating point numbers. When writing to a format, fractional values will be lost, but this shall happen in such a way to avoid the propagation of error. Relative stitches from position ( 0.0, 0.31 ) of (+5.4, +5.4), (+5.4, +5,4), (+5.4, +5,4) should encode as changes of 5,6 6,5 5,6. Taking the relative distance in the format as the integer change from the last integer position to the new one, maintaining a position as close to the absolute position as possible. All fractional values are considered significant. In some read formats the formats themselves have a slightly different unit systems such as .PCD or .MIT these alternative units will be presented seemlessly as 1/10th mm units. -Core Command Ordering ---- +### Core Command Ordering + Stitch is taken to mean move_position(x,y), needle_strike. Jump is taken to mean move_position(x,y), block_needle_bar. In those orders. If a format takes stitch to mean needle_strike, move_position(x,y) in that order. The encoder will may insert an extra jump in to avoid stitching an unwanted element. These differences matter, and are accounted for by things like FULL_JUMP in places, and within the formats. However, within the pattern the understanding should be consistently be taken as displace then operation. @@ -515,8 +531,8 @@ Note: This is true for sequin_eject too. DST files are the only currently suppor So if write your own pattern and you intend to stitch at the origin and then go somewhere you must `stitch, 0, 0` then `stitch, x, y` if you start by stitching somewhere at x, y. It may insert jump stitches to get you to that location, then stitch at that location. -Coordinate System ---- +### Coordinate System + Fundamentally pyembroidery stores the positions such that the +y direction is down and -y is up (when viewed horizontally) with +x right and -x left. This is consistent with most modern graphics coordinate systems, but this is different from how these values are stored within embroidery formats. pyembroidery reads by flipping the y-axis, and writes by flipping the y-axis (except for SVG which uses the same coordinate system). This allows for seemless reading, writing, and interfacing. The flips occur at the level of the format readers and writers and is not subject to encoding. However encoding with scale of (1, -1) would invert this during the encoding. All patterns are stored such that `top` is in the -y direction and `bottom` is in the +y direction. All patterns start at the origin point (0,0). In keeping with the philosophy the absolute positioning of the data is maintained sometimes this means it an offcenter pattern will move from the origin to an absolute position some distance from the origin. While this preserves information, it might also not be entirely expected at times. This `pattern.move_center_to_origin()` will lose that information and center the pattern at the origin. From 8752c81c1d58af0d85a0d1f19f445b15c48b77b7 Mon Sep 17 00:00:00 2001 From: tatarize Date: Mon, 15 Jul 2019 23:07:16 -0700 Subject: [PATCH 16/16] Moved InterpolateTrim into EmbPattern --- pyembroidery/DstReader.py | 42 ++++++----------------- pyembroidery/EmbPattern.py | 54 +++++++++++++++++++++++++++++ pyembroidery/JefReader.py | 66 ++++++++---------------------------- pyembroidery/PyEmbroidery.py | 1 + 4 files changed, 81 insertions(+), 82 deletions(-) diff --git a/pyembroidery/DstReader.py b/pyembroidery/DstReader.py index d62b9241..8e02f790 100644 --- a/pyembroidery/DstReader.py +++ b/pyembroidery/DstReader.py @@ -59,18 +59,7 @@ def dst_read_header(f, out): def dst_read_stitches(f, out, settings=None): - jump_count_max = 3 - clipping = True - if settings is not None: - jump_count_max = settings.get('trim_at', jump_count_max) - clipping = settings.get('clipping', clipping) sequin_mode = False - jump_count = 0 - jump_start = 0 - jump_dx = 0 - jump_dy = 0 - jumping = False - trimmed = True while True: byte = bytearray(f.read(3)) if len(byte) != 3: @@ -81,38 +70,29 @@ def dst_read_stitches(f, out, settings=None): break elif byte[2] & 0b11000011 == 0b11000011: out.color_change(dx, dy) - trimmed = True - jumping = False elif byte[2] & 0b01000011 == 0b01000011: out.sequin_mode(dx, dy) sequin_mode = not sequin_mode - jumping = False elif byte[2] & 0b10000011 == 0b10000011: if sequin_mode: out.sequin_eject(dx, dy) else: out.move(dx, dy) - if not jumping: - jump_dx = 0 - jump_dy = 0 - jump_count = 0 - jump_start = len(out.stitches) - 1 - jumping = True - jump_count += 1 - jump_dx += dx - jump_dy += dy - if not trimmed and jump_count == jump_count_max: - out.trim(position=jump_start) - jump_start += 1 # We inserted a position, start jump has moved. - trimmed = True - if clipping and jump_dx == 0 and jump_dy == 0: # jump displacement is 0, clip trim command. - out.stitches = out.stitches[:jump_start] else: out.stitch(dx, dy) - trimmed = False - jumping = False out.end() + count_max = 3 + clipping = True + trim_distance = None + if settings is not None: + count_max = settings.get('trim_at', count_max) + trim_distance = settings.get("trim_distance", trim_distance) + clipping = settings.get('clipping', clipping) + if trim_distance is not None: + trim_distance *= 10 # Pixels per mm. Native units are 1/10 mm. + out.interpolate_trims(count_max, trim_distance, clipping) + def read(f, out, settings=None): dst_read_header(f, out) diff --git a/pyembroidery/EmbPattern.py b/pyembroidery/EmbPattern.py index dbc9c53a..1b8f13e7 100644 --- a/pyembroidery/EmbPattern.py +++ b/pyembroidery/EmbPattern.py @@ -501,6 +501,60 @@ def add_pattern(self, pattern): self.stitches[i][2] = NO_COMMAND self.extras.update(pattern.extras) + def interpolate_trims(self, jumps_to_require_trim=None, distance_to_require_trim=None, clipping=True): + """Processes a pattern adding trims according to the given criteria.""" + i = -1 + ie = len(self.stitches) - 1 + + x = 0 + y = 0 + jump_count = 0 + jump_start = 0 + jump_dx = 0 + jump_dy = 0 + jumping = False + trimmed = True + while i < ie: + i += 1 + stitch = self.stitches[i] + dx = stitch[0] - x + dy = stitch[1] - y + x = stitch[0] + y = stitch[1] + command = stitch[2] & COMMAND_MASK + if command == STITCH or command == SEQUIN_EJECT: + trimmed = False + jumping = False + elif command == COLOR_CHANGE or command == NEEDLE_SET or command == TRIM: + trimmed = True + jumping = False + if command == JUMP: + if not jumping: + jump_dx = 0 + jump_dy = 0 + jump_count = 0 + jump_start = i + jumping = True + jump_count += 1 + jump_dx += dx + jump_dy += dy + if not trimmed: + if jump_count == jumps_to_require_trim or\ + distance_to_require_trim is not None and\ + ( + abs(jump_dy) > distance_to_require_trim or\ + abs(jump_dx) > distance_to_require_trim + ): + self.trim(position=jump_start) + jump_start += 1 # We inserted a position, start jump has moved. + i += 1 + ie += 1 + trimmed = True + if clipping and jump_dx == 0 and jump_dy == 0: # jump displacement is 0, clip trim command. + del self.stitches[jump_start:i+1] + i = jump_start - 1 + ie = len(self.stitches) - 1 + def get_pattern_interpolate_trim(self, jumps_to_require_trim): """Gets a processed pattern with untrimmed jumps merged and trims added if merged jumps are beyond the given value. diff --git a/pyembroidery/JefReader.py b/pyembroidery/JefReader.py index 74b663db..96b07901 100644 --- a/pyembroidery/JefReader.py +++ b/pyembroidery/JefReader.py @@ -3,23 +3,6 @@ def read_jef_stitches(f, out, settings=None): - clipping = True - trims = False - command_count_max = 3 - trim_distance = 3.0 - if settings is not None: - command_count_max = settings.get('trim_at', command_count_max) - trims = settings.get("trims", trims) - trim_distance = settings.get("trim_distance", trim_distance) - clipping = settings.get('clipping', clipping) - if trim_distance is not None: - trim_distance *= 10 # Pixels per mm. Native units are 1/10 mm. - jump_count = 0 - jump_start = 0 - jump_dx = 0 - jump_dy = 0 - jumping = False - trimmed = True while True: b = bytearray(f.read(2)) if len(b) != 2: @@ -28,8 +11,6 @@ def read_jef_stitches(f, out, settings=None): x = signed8(b[0]) y = -signed8(b[1]) out.stitch(x, y) - trimmed = False - jumping = False continue ctrl = b[1] b = bytearray(f.read(2)) @@ -39,47 +20,30 @@ def read_jef_stitches(f, out, settings=None): y = -signed8(b[1]) if ctrl == 0x02: out.move(x, y) - if not jumping: - jump_dx = 0 - jump_dy = 0 - jump_count = 0 - jump_start = len(out.stitches) - 1 - jumping = True - jump_count += 1 - jump_dx += x - jump_dy += y - if not trimmed and\ - ( - ( - trims - and - jump_count == command_count_max - ) - or - ( - trim_distance is not None - and - ( - abs(jump_dy) > trim_distance or abs(jump_dx) > trim_distance - ) - ) - ): - out.trim(position=jump_start) - jump_start += 1 # We inserted a position, start jump has moved. - trimmed = True - if clipping and jump_dx == 0 and jump_dy == 0: # jump displacement is 0, clip trim command. - out.stitches = out.stitches[:jump_start] continue if ctrl == 0x01: out.color_change(0, 0) - trimmed = True - jumping = False continue if ctrl == 0x10: break break # Uncaught Control out.end(0, 0) + clipping = True + trims = False + count_max = None + trim_distance = 3.0 + if settings is not None: + count_max = settings.get('trim_at', count_max) + trims = settings.get("trims", trims) + trim_distance = settings.get("trim_distance", trim_distance) + clipping = settings.get('clipping', clipping) + if trims and count_max is None: + count_max = 3 + if trim_distance is not None: + trim_distance *= 10 # Pixels per mm. Native units are 1/10 mm. + out.interpolate_trims(count_max, trim_distance, clipping) + def read(f, out, settings=None): jef_threads = get_thread_set() diff --git a/pyembroidery/PyEmbroidery.py b/pyembroidery/PyEmbroidery.py index 5c996e8c..e27b0772 100644 --- a/pyembroidery/PyEmbroidery.py +++ b/pyembroidery/PyEmbroidery.py @@ -107,6 +107,7 @@ def supported_formats(): "reader": DstReader, "writer": DstWriter, "read_options": { + "trim_distance": (None, 3.0, 50.0), "trim_at": (2, 3, 4, 5, 6, 7, 8), "clipping": (True, False) },