diff --git a/README.md b/README.md index eba0fd9..49607f7 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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d8449e8 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +name = "pyembroidery" diff --git a/pyembroidery/CsvWriter.py b/pyembroidery/CsvWriter.py index af41fa8..d80be63 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 5c24981..8e02f79 100644 --- a/pyembroidery/DstReader.py +++ b/pyembroidery/DstReader.py @@ -58,7 +58,7 @@ 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): sequin_mode = False while True: byte = bytearray(f.read(3)) @@ -67,7 +67,7 @@ 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) elif byte[2] & 0b01000011 == 0b01000011: @@ -80,8 +80,20 @@ def dst_read_stitches(f, out): out.move(dx, dy) else: out.stitch(dx, dy) + 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) - dst_read_stitches(f, out) + dst_read_stitches(f, out, settings) diff --git a/pyembroidery/DstWriter.py b/pyembroidery/DstWriter.py index 2330063..1e2b1a1 100644 --- a/pyembroidery/DstWriter.py +++ b/pyembroidery/DstWriter.py @@ -102,9 +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) - + 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") @@ -164,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/EmbPattern.py b/pyembroidery/EmbPattern.py index 50d8d89..1b8f13e 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]) @@ -448,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/EmbThread.py b/pyembroidery/EmbThread.py index 4d900be..aac4318 100644 --- a/pyembroidery/EmbThread.py +++ b/pyembroidery/EmbThread.py @@ -1,14 +1,56 @@ + +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( @@ -25,8 +67,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)) @@ -56,7 +97,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 @@ -74,12 +115,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 @@ -98,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) @@ -181,7 +228,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 = { @@ -333,6 +380,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. diff --git a/pyembroidery/EmbThreadJef.py b/pyembroidery/EmbThreadJef.py index 47dbcef..cc82de7 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 abb51e8..d74cb17 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 9a4645d..d567926 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 b47e6d4..96b0790 100644 --- a/pyembroidery/JefReader.py +++ b/pyembroidery/JefReader.py @@ -2,10 +2,8 @@ from .ReadHelper import read_int_32le, signed8 -def read_jef_stitches(f, out): - count = 0 +def read_jef_stitches(f, out, settings=None): while True: - count += 1 b = bytearray(f.read(2)) if len(b) != 2: break @@ -21,10 +19,7 @@ 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) continue if ctrl == 0x01: out.color_change(0, 0) @@ -34,6 +29,21 @@ def read_jef_stitches(f, out): 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() @@ -47,4 +57,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 102549b..a671e62 100644 --- a/pyembroidery/JefWriter.py +++ b/pyembroidery/JefWriter.py @@ -1,5 +1,6 @@ import datetime +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 @@ -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 = 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 7228b8d..7cc8b88 100644 --- a/pyembroidery/PecWriter.py +++ b/pyembroidery/PecWriter.py @@ -1,3 +1,4 @@ +from .EmbThread import build_unique_palette 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 = 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 c979486..330fd7d 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 014834f..e27b077 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,45 @@ 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_distance": (None, 3.0, 50.0), + "trim_at": (2, 3, 4, 5, 6, 7, 8), + "clipping": (True, False) }, - "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), + "clipping": (True, False) + }, + "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 +155,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 +174,7 @@ def supported_formats(): yield ({ "description": "Janome Embroidery Format", "extension": "sew", + "extensions": ("sew"), "mimetype": "application/x-sew", "category": "embroidery", "reader": SewReader @@ -164,6 +182,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 +191,7 @@ def supported_formats(): yield ({ "description": "Husqvarna Viking Embroidery Format", "extension": "shv", + "extensions": ("shv"), "mimetype": "application/x-shv", "category": "embroidery", "reader": ShvReader @@ -179,6 +199,7 @@ def supported_formats(): yield ({ "description": "Toyota Embroidery Format", "extension": "10o", + "extensions": ("10o"), "mimetype": "application/x-10o", "category": "embroidery", "reader": A10oReader @@ -186,6 +207,7 @@ def supported_formats(): yield ({ "description": "Toyota Embroidery Format", "extension": "100", + "extensions": ("100"), "mimetype": "application/x-100", "category": "embroidery", "reader": A100Reader @@ -193,6 +215,7 @@ def supported_formats(): yield ({ "description": "Bits & Volts Embroidery Format", "extension": "bro", + "extensions": ("bro"), "mimetype": "application/x-Bro", "category": "embroidery", "reader": BroReader @@ -200,6 +223,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 +231,7 @@ def supported_formats(): yield ({ "description": "Tajima(Barudan) Embroidery Format", "extension": "dsb", + "extensions": ("dsb"), "mimetype": "application/x-dsb", "category": "embroidery", "reader": DsbReader @@ -214,6 +239,7 @@ def supported_formats(): yield ({ "description": "ZSK USA Embroidery Format", "extension": "dsz", + "extensions": ("dsz"), "mimetype": "application/x-dsz", "category": "embroidery", "reader": DszReader @@ -221,6 +247,7 @@ def supported_formats(): yield ({ "description": "Elna Embroidery Format", "extension": "emd", + "extensions": ("emd"), "mimetype": "application/x-emd", "category": "embroidery", "reader": EmdReader @@ -228,6 +255,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 +263,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 +271,7 @@ def supported_formats(): yield ({ "description": "Gold Thread Embroidery Format", "extension": "gt", + "extensions": ("gt"), "mimetype": "application/x-exy", "category": "embroidery", "reader": GtReader @@ -249,6 +279,7 @@ def supported_formats(): yield ({ "description": "Inbro Embroidery Format", "extension": "inb", + "extensions": ("inb"), "mimetype": "application/x-inb", "category": "embroidery", "reader": InbReader @@ -256,6 +287,7 @@ def supported_formats(): yield ({ "description": "Tajima Embroidery Format", "extension": "tbf", + "extensions": ("tbf"), "mimetype": "application/x-tbf", "category": "embroidery", "reader": TbfReader @@ -263,6 +295,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "ksm", + "extensions": ("ksm"), "mimetype": "application/x-ksm", "category": "embroidery", "reader": KsmReader @@ -270,6 +303,7 @@ def supported_formats(): yield ({ "description": "Happy Embroidery Format", "extension": "tap", + "extensions": ("tap"), "mimetype": "application/x-tap", "category": "embroidery", "reader": TapReader @@ -277,6 +311,7 @@ def supported_formats(): yield ({ "description": "Data Stitch Embroidery Format", "extension": "stx", + "extensions": ("stx"), "mimetype": "application/x-stx", "category": "embroidery", "reader": StxReader @@ -284,6 +319,7 @@ def supported_formats(): yield ({ "description": "Brother Embroidery Format", "extension": "phb", + "extensions": ("phb"), "mimetype": "application/x-phb", "category": "embroidery", "reader": PhbReader @@ -291,6 +327,7 @@ def supported_formats(): yield ({ "description": "Brother Embroidery Format", "extension": "phc", + "extensions": ("phc"), "mimetype": "application/x-phc", "category": "embroidery", "reader": PhcReader @@ -298,6 +335,7 @@ def supported_formats(): yield ({ "description": "Ameco Embroidery Format", "extension": "new", + "extensions": ("new"), "mimetype": "application/x-new", "category": "embroidery", "reader": NewReader @@ -305,6 +343,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "max", + "extensions": ("max"), "mimetype": "application/x-max", "category": "embroidery", "reader": MaxReader @@ -312,6 +351,7 @@ def supported_formats(): yield ({ "description": "Mitsubishi Embroidery Format", "extension": "mit", + "extensions": ("mit"), "mimetype": "application/x-mit", "category": "embroidery", "reader": MitReader @@ -319,6 +359,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "pcd", + "extensions": ("pcd"), "mimetype": "application/x-pcd", "category": "embroidery", "reader": PcdReader @@ -326,6 +367,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "pcq", + "extensions": ("pcq"), "mimetype": "application/x-pcq", "category": "embroidery", "reader": PcqReader @@ -333,6 +375,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "pcm", + "extensions": ("pcm"), "mimetype": "application/x-pcm", "category": "embroidery", "reader": PcmReader @@ -340,6 +383,7 @@ def supported_formats(): yield ({ "description": "Pfaff Embroidery Format", "extension": "pcs", + "extensions": ("pcs"), "mimetype": "application/x-pcs", "category": "embroidery", "reader": PcsReader @@ -347,6 +391,7 @@ def supported_formats(): yield ({ "description": "Janome Embroidery Format", "extension": "jpx", + "extensions": ("jpx"), "mimetype": "application/x-jpx", "category": "embroidery", "reader": JpxReader @@ -354,6 +399,7 @@ def supported_formats(): yield ({ "description": "Gunold Embroidery Format", "extension": "stc", + "extensions": ("stc"), "mimetype": "application/x-stc", "category": "embroidery", "reader": StcReader @@ -368,6 +414,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 +422,7 @@ def supported_formats(): yield ({ "description": "Brother Stitch Format", "extension": "pmv", + "extensions": ("pmv"), "mimetype": "application/x-pmv", "category": "stitch", "reader": PmvReader, @@ -383,10 +431,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 +443,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 f4d1d81..5c63e09 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 06b2bc5..c43cc80 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 6f67071..66f2469 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): diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ + diff --git a/setup.py b/setup.py index 2761536..f38e201 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", diff --git a/test/pattern_for_tests.py b/test/pattern_for_tests.py index 790bc6d..ba6ce95 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/reorder.pes b/test/reorder.pes deleted file mode 100644 index 3fb13a7..0000000 Binary files a/test/reorder.pes and /dev/null differ diff --git a/test/test_convert_pes.py b/test/test_convert_pes.py index ff9dcdd..937a00d 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 91bae62..3e7ef24 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_palette.py b/test/test_palette.py new file mode 100644 index 0000000..bf8c1af --- /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 pyembroidery.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 diff --git a/test/test_trims_dst_jef.py b/test/test_trims_dst_jef.py new file mode 100644 index 0000000..b5a6f12 --- /dev/null +++ b/test/test_trims_dst_jef.py @@ -0,0 +1,73 @@ +from __future__ import print_function + +import unittest + +from pyembroidery import * +from pattern_for_tests import * + + +class TestTrims(unittest.TestCase): + + def test_dst_trims(self): + file0 = "trim.dst" + pattern = get_fractal_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 = 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) + self.assertEqual(loaded_pattern.count_stitch_commands(TRIM), 0) + self.addCleanup(os.remove, file0) + + def test_dst_trims_success(self): + file0 = "trim.dst" + 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) + self.assertNotEqual(loaded_pattern.count_stitch_commands(TRIM), 0) + self.addCleanup(os.remove, file0) + + def test_jef_trims(self): + file0 = "trim.jef" + pattern = get_fractal_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 = get_fractal_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 = 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) + 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)