diff options
Diffstat (limited to 'lib/extensions')
| -rw-r--r-- | lib/extensions/__init__.py | 6 | ||||
| -rw-r--r-- | lib/extensions/base.py | 222 | ||||
| -rw-r--r-- | lib/extensions/embroider.py | 86 | ||||
| -rw-r--r-- | lib/extensions/input.py | 66 | ||||
| -rw-r--r-- | lib/extensions/palettes.py | 111 | ||||
| -rw-r--r-- | lib/extensions/params.py | 756 | ||||
| -rw-r--r-- | lib/extensions/print_pdf.py | 391 | ||||
| -rw-r--r-- | lib/extensions/simulate.py | 27 |
8 files changed, 1665 insertions, 0 deletions
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py new file mode 100644 index 00000000..ebdd2fc9 --- /dev/null +++ b/lib/extensions/__init__.py @@ -0,0 +1,6 @@ +from embroider import Embroider +from palettes import Palettes +from params import Params +from print_pdf import Print +from simulate import Simulate +from input import Input diff --git a/lib/extensions/base.py b/lib/extensions/base.py new file mode 100644 index 00000000..91e050eb --- /dev/null +++ b/lib/extensions/base.py @@ -0,0 +1,222 @@ +import inkex +import re +import json +from copy import deepcopy +from collections import MutableMapping +from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement +from .. import SVG_POLYLINE_TAG, SVG_GROUP_TAG, SVG_DEFS_TAG, INKSCAPE_GROUPMODE, EMBROIDERABLE_TAGS, PIXELS_PER_MM +from ..utils import cache + + +SVG_METADATA_TAG = inkex.addNS("metadata", "svg") + + +def strip_namespace(tag): + """Remove xml namespace from a tag name. + + >>> {http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}namedview + <<< namedview + """ + + match = re.match('^\{[^}]+\}(.+)$', tag) + + if match: + return match.group(1) + else: + return tag + + +class InkStitchMetadata(MutableMapping): + """Helper class to get and set inkstitch-specific metadata attributes. + + Operates on a document and acts like a dict. Setting an item adds or + updates a metadata element in the document. Getting an item retrieves + a metadata element's text contents or None if an element by that name + doesn't exist. + """ + + def __init__(self, document): + self.document = document + self.metadata = self._get_or_create_metadata() + + def _get_or_create_metadata(self): + metadata = self.document.find(SVG_METADATA_TAG) + + if metadata is None: + metadata = inkex.etree.SubElement(self.document.getroot(), SVG_METADATA_TAG) + + # move it so that it goes right after the first element, sodipodi:namedview + self.document.getroot().remove(metadata) + self.document.getroot().insert(1, metadata) + + return metadata + + # Because this class inherints from MutableMapping, all we have to do is + # implement these five methods and we get a full dict-like interface. + + def __setitem__(self, name, value): + item = self._find_item(name) + + if value: + item.text = json.dumps(value) + else: + item.getparent().remove(item) + + def _find_item(self, name): + tag = inkex.addNS(name, "inkstitch") + item = self.metadata.find(tag) + if item is None: + item = inkex.etree.SubElement(self.metadata, tag) + + return item + + def __getitem__(self, name): + item = self._find_item(name) + + try: + return json.loads(item.text) + except (ValueError, TypeError): + return None + + def __delitem__(self, name): + item = self._find_item(name) + + if item: + self.metadata.remove(item) + + def __iter__(self): + for child in self.metadata: + if child.prefix == "inkstitch": + yield strip_namespace(child.tag) + + def __len__(self): + i = 0 + for i, item in enumerate(self): + pass + + return i + 1 + + +class InkstitchExtension(inkex.Effect): + """Base class for Inkstitch extensions. Not intended for direct use.""" + + def hide_all_layers(self): + for g in self.document.getroot().findall(SVG_GROUP_TAG): + if g.get(INKSCAPE_GROUPMODE) == "layer": + g.set("style", "display:none") + + def no_elements_error(self): + if self.selected: + inkex.errormsg(_("No embroiderable paths selected.")) + else: + inkex.errormsg(_("No embroiderable paths found in document.")) + inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths before embroidering.")) + + def descendants(self, node): + nodes = [] + element = EmbroideryElement(node) + + if element.has_style('display') and element.get_style('display') is None: + return [] + + if node.tag == SVG_DEFS_TAG: + return [] + + for child in node: + nodes.extend(self.descendants(child)) + + if node.tag in EMBROIDERABLE_TAGS: + nodes.append(node) + + return nodes + + def get_nodes(self): + """Get all XML nodes, or just those selected + + effect is an instance of a subclass of inkex.Effect. + """ + + if self.selected: + nodes = [] + for node in self.document.getroot().iter(): + if node.get("id") in self.selected: + nodes.extend(self.descendants(node)) + else: + nodes = self.descendants(self.document.getroot()) + + return nodes + + def detect_classes(self, node): + if node.tag == SVG_POLYLINE_TAG: + return [Polyline] + else: + element = EmbroideryElement(node) + + if element.get_boolean_param("satin_column"): + return [SatinColumn] + else: + classes = [] + + if element.get_style("fill"): + if element.get_boolean_param("auto_fill", True): + classes.append(AutoFill) + else: + classes.append(Fill) + + if element.get_style("stroke"): + classes.append(Stroke) + + if element.get_boolean_param("stroke_first", False): + classes.reverse() + + return classes + + + def get_elements(self): + self.elements = [] + for node in self.get_nodes(): + classes = self.detect_classes(node) + self.elements.extend(cls(node) for cls in classes) + + if self.elements: + return True + else: + self.no_elements_error() + return False + + def elements_to_patches(self, elements): + patches = [] + for element in elements: + if patches: + last_patch = patches[-1] + else: + last_patch = None + + patches.extend(element.embroider(last_patch)) + + return patches + + def get_inkstitch_metadata(self): + return InkStitchMetadata(self.document) + + def parse(self): + """Override inkex.Effect to add Ink/Stitch xml namespace""" + + # SVG parsers don't actually look for anything at this URL. They just + # care that it's unique. That defines a "namespace" of element and + # attribute names to disambiguate conflicts with element and + # attribute names other XML namespaces. + # + # Updating inkex.NSS here allows us to pass 'inkstitch' into + # inkex.addNS(). + inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace' + + # call the superclass's method first + inkex.Effect.parse(self) + + # This is the only way I could find to add a namespace to an existing + # element tree at the top without getting ugly prefixes like "ns0". + inkex.etree.cleanup_namespaces(self.document, + top_nsmap=inkex.NSS, + keep_ns_prefixes=inkex.NSS.keys()) + self.original_document = deepcopy(self.document) diff --git a/lib/extensions/embroider.py b/lib/extensions/embroider.py new file mode 100644 index 00000000..564e96ca --- /dev/null +++ b/lib/extensions/embroider.py @@ -0,0 +1,86 @@ +import sys +import traceback +import os + +import inkex +from .. import _, PIXELS_PER_MM, write_embroidery_file +from .base import InkstitchExtension +from ..stitch_plan import patches_to_stitch_plan +from ..svg import render_stitch_plan + + +class Embroider(InkstitchExtension): + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self) + self.OptionParser.add_option("-c", "--collapse_len_mm", + action="store", type="float", + dest="collapse_length_mm", default=3.0, + help="max collapse length (mm)") + self.OptionParser.add_option("--hide_layers", + action="store", type="choice", + choices=["true", "false"], + dest="hide_layers", default="true", + help="Hide all other layers when the embroidery layer is generated") + self.OptionParser.add_option("-O", "--output_format", + action="store", type="string", + dest="output_format", default="csv", + help="Output file extenstion (default: csv)") + self.OptionParser.add_option("-P", "--path", + action="store", type="string", + dest="path", default=".", + help="Directory in which to store output file") + self.OptionParser.add_option("-F", "--output-file", + action="store", type="string", + dest="output_file", + help="Output filename.") + self.OptionParser.add_option("-b", "--max-backups", + action="store", type="int", + dest="max_backups", default=5, + help="Max number of backups of output files to keep.") + self.OptionParser.usage += _("\n\nSeeing a 'no such option' message? Please restart Inkscape to fix.") + + def get_output_path(self): + if self.options.output_file: + output_path = os.path.join(self.options.path, self.options.output_file) + else: + svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'), "embroidery.svg") + csv_filename = svg_filename.replace('.svg', '.%s' % self.options.output_format) + output_path = os.path.join(self.options.path, csv_filename) + + def add_suffix(path, suffix): + if suffix > 0: + path = "%s.%s" % (path, suffix) + + return path + + def move_if_exists(path, suffix=0): + source = add_suffix(path, suffix) + + if suffix >= self.options.max_backups: + return + + dest = add_suffix(path, suffix + 1) + + if os.path.exists(source): + move_if_exists(path, suffix + 1) + + if os.path.exists(dest): + os.remove(dest) + + os.rename(source, dest) + + move_if_exists(output_path) + + return output_path + + def effect(self): + if not self.get_elements(): + return + + if self.options.hide_layers: + self.hide_all_layers() + + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) + write_embroidery_file(self.get_output_path(), stitch_plan, self.document.getroot()) + render_stitch_plan(self.document.getroot(), stitch_plan) diff --git a/lib/extensions/input.py b/lib/extensions/input.py new file mode 100644 index 00000000..bd3db0ed --- /dev/null +++ b/lib/extensions/input.py @@ -0,0 +1,66 @@ +import os +from os.path import realpath, dirname, join as path_join +import sys + +# help python find libembroidery when running in a local repo clone +if getattr(sys, 'frozen', None) is None: + sys.path.append(realpath(path_join(dirname(__file__), '..', '..'))) + +from libembroidery import * +from inkex import etree +import inkex +from .. import PIXELS_PER_MM, INKSCAPE_LABEL, _ +from ..stitch_plan import StitchPlan +from ..svg import render_stitch_plan + + +class Input(object): + def pattern_stitches(self, pattern): + stitch_pointer = pattern.stitchList + while stitch_pointer: + yield stitch_pointer.stitch + stitch_pointer = stitch_pointer.next + + + def affect(self, args): + embroidery_file = args[0] + pattern = embPattern_create() + embPattern_read(pattern, embroidery_file) + embPattern_flipVertical(pattern) + + stitch_plan = StitchPlan() + color_block = None + current_color = None + + for stitch in self.pattern_stitches(pattern): + if stitch.color != current_color: + thread = embThreadList_getAt(pattern.threadList, stitch.color) + color = thread.color + color_block = stitch_plan.new_color_block((color.r, color.g, color.b)) + current_color = stitch.color + + if not stitch.flags & END: + color_block.add_stitch(stitch.xx * PIXELS_PER_MM, stitch.yy * PIXELS_PER_MM, + jump=stitch.flags & JUMP, + stop=stitch.flags & STOP, + trim=stitch.flags & TRIM) + + extents = stitch_plan.extents + svg = etree.Element("svg", nsmap=inkex.NSS, attrib= + { + "width": str(extents[0] * 2), + "height": str(extents[1] * 2), + "viewBox": "0 0 %s %s" % (extents[0] * 2, extents[1] * 2), + }) + render_stitch_plan(svg, stitch_plan) + + # rename the Stitch Plan layer so that it doesn't get overwritten by Embroider + layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + layer.set(INKSCAPE_LABEL, os.path.basename(embroidery_file)) + layer.attrib.pop('id') + + # Shift the design so that its origin is at the center of the canvas + # Note: this is NOT the same as centering the design in the canvas! + layer.set('transform', 'translate(%s,%s)' % (extents[0], extents[1])) + + print etree.tostring(svg) diff --git a/lib/extensions/palettes.py b/lib/extensions/palettes.py new file mode 100644 index 00000000..269dc6dc --- /dev/null +++ b/lib/extensions/palettes.py @@ -0,0 +1,111 @@ +import sys +import traceback +import os +from os.path import realpath, dirname +from glob import glob +from threading import Thread +import socket +import errno +import time +import logging +import wx +import inkex +from ..utils import guess_inkscape_config_path + + +class InstallPalettesFrame(wx.Frame): + def __init__(self, *args, **kwargs): + wx.Frame.__init__(self, *args, **kwargs) + + default_path = os.path.join(guess_inkscape_config_path(), "palettes") + + panel = wx.Panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + + text = wx.StaticText(panel, label=_("Directory in which to install palettes:")) + font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL) + text.SetFont(font) + sizer.Add(text, proportion=0, flag=wx.ALL|wx.EXPAND, border=10) + + path_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.path_input = wx.TextCtrl(panel, wx.ID_ANY, value=default_path) + path_sizer.Add(self.path_input, proportion=3, flag=wx.RIGHT|wx.EXPAND, border=20) + chooser_button = wx.Button(panel, wx.ID_OPEN, _('Choose another directory...')) + path_sizer.Add(chooser_button, proportion=1, flag=wx.EXPAND) + sizer.Add(path_sizer, proportion=0, flag=wx.ALL|wx.EXPAND, border=10) + + buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) + install_button = wx.Button(panel, wx.ID_ANY, _("Install")) + install_button.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK)) + buttons_sizer.Add(install_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5) + cancel_button = wx.Button(panel, wx.ID_CANCEL, _("Cancel")) + buttons_sizer.Add(cancel_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5) + sizer.Add(buttons_sizer, proportion=0, flag=wx.ALIGN_RIGHT) + + outer_sizer = wx.BoxSizer(wx.HORIZONTAL) + outer_sizer.Add(sizer, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) + + panel.SetSizer(outer_sizer) + panel.Layout() + + chooser_button.Bind(wx.EVT_BUTTON, self.chooser_button_clicked) + cancel_button.Bind(wx.EVT_BUTTON, self.cancel_button_clicked) + install_button.Bind(wx.EVT_BUTTON, self.install_button_clicked) + + def cancel_button_clicked(self, event): + self.Destroy() + + def chooser_button_clicked(self, event): + dialog = wx.DirDialog(self, _("Choose Inkscape palettes directory")) + if dialog.ShowModal() != wx.ID_CANCEL: + self.path_input.SetValue(dialog.GetPath()) + + def install_button_clicked(self, event): + try: + self.install_palettes() + except Exception, e: + wx.MessageDialog(self, + _('Thread palette installation failed') + ': \n' + traceback.format_exc(), + _('Installation Failed'), + wx.OK).ShowModal() + else: + wx.MessageDialog(self, + _('Thread palette files have been installed. Please restart Inkscape to load the new palettes.'), + _('Installation Completed'), + wx.OK).ShowModal() + + self.Destroy() + + def install_palettes(self): + path = self.path_input.GetValue() + palettes_dir = self.get_bundled_palettes_dir() + self.copy_files(glob(os.path.join(palettes_dir, "*")), path) + + def get_bundled_palettes_dir(self): + if getattr(sys, 'frozen', None) is not None: + return realpath(os.path.join(sys._MEIPASS, '..', 'palettes')) + else: + return os.path.join(dirname(realpath(__file__)), 'palettes') + + if (sys.platform == "win32"): + # If we try to just use shutil.copy it says the operation requires elevation. + def copy_files(self, files, dest): + import winutils + + winutils.copy(files, dest) + else: + def copy_files(self, files, dest): + import shutil + + if not os.path.exists(dest): + os.makedirs(dest) + + for palette_file in files: + shutil.copy(palette_file, dest) + +class Palettes(inkex.Effect): + def effect(self): + app = wx.App() + installer_frame = InstallPalettesFrame(None, title=_("Ink/Stitch Thread Palette Installer"), size=(450, 200)) + installer_frame.Show() + app.MainLoop() diff --git a/lib/extensions/params.py b/lib/extensions/params.py new file mode 100644 index 00000000..881dab49 --- /dev/null +++ b/lib/extensions/params.py @@ -0,0 +1,756 @@ +# -*- coding: UTF-8 -*- + +import os +import sys +import json +import traceback +import time +from threading import Thread, Event +from copy import copy +import wx +from wx.lib.scrolledpanel import ScrolledPanel +from collections import defaultdict +from functools import partial +from itertools import groupby + +from .. import _ +from .base import InkstitchExtension +from ..stitch_plan import patches_to_stitch_plan +from ..elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn +from ..utils import save_stderr, restore_stderr +from ..simulator import EmbroiderySimulator + + +def presets_path(): + try: + import appdirs + config_path = appdirs.user_config_dir('inkstitch') + except ImportError: + config_path = os.path.expanduser('~/.inkstitch') + + if not os.path.exists(config_path): + os.makedirs(config_path) + return os.path.join(config_path, 'presets.json') + + +def load_presets(): + try: + with open(presets_path(), 'r') as presets: + presets = json.load(presets) + return presets + except: + return {} + + +def save_presets(presets): + with open(presets_path(), 'w') as presets_file: + json.dump(presets, presets_file) + + +def load_preset(name): + return load_presets().get(name) + + +def save_preset(name, data): + presets = load_presets() + presets[name] = data + save_presets(presets) + + +def delete_preset(name): + presets = load_presets() + presets.pop(name, None) + save_presets(presets) + + +def confirm_dialog(parent, question, caption = 'ink/stitch'): + dlg = wx.MessageDialog(parent, question, caption, wx.YES_NO | wx.ICON_QUESTION) + result = dlg.ShowModal() == wx.ID_YES + dlg.Destroy() + return result + + +def info_dialog(parent, message, caption = 'ink/stitch'): + dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION) + dlg.ShowModal() + dlg.Destroy() + + +class ParamsTab(ScrolledPanel): + def __init__(self, *args, **kwargs): + self.params = kwargs.pop('params', []) + self.name = kwargs.pop('name', None) + self.nodes = kwargs.pop('nodes') + kwargs["style"] = wx.TAB_TRAVERSAL + ScrolledPanel.__init__(self, *args, **kwargs) + self.SetupScrolling() + + self.changed_inputs = set() + self.dependent_tabs = [] + self.parent_tab = None + self.param_inputs = {} + self.paired_tab = None + self.disable_notify_pair = False + + toggles = [param for param in self.params if param.type == 'toggle'] + + if toggles: + self.toggle = toggles[0] + self.params.remove(self.toggle) + self.toggle_checkbox = wx.CheckBox(self, label=self.toggle.description) + + value = any(self.toggle.values) + if self.toggle.inverse: + value = not value + self.toggle_checkbox.SetValue(value) + + self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.update_toggle_state) + self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.changed) + + self.param_inputs[self.toggle.name] = self.toggle_checkbox + else: + self.toggle = None + + self.settings_grid = wx.FlexGridSizer(rows=0, cols=3, hgap=10, vgap=10) + self.settings_grid.AddGrowableCol(0, 1) + self.settings_grid.SetFlexibleDirection(wx.HORIZONTAL) + + self.__set_properties() + self.__do_layout() + + if self.toggle: + self.update_toggle_state() + # end wxGlade + + def pair(self, tab): + # print self.name, "paired with", tab.name + self.paired_tab = tab + self.update_description() + + def add_dependent_tab(self, tab): + self.dependent_tabs.append(tab) + self.update_description() + + def set_parent_tab(self, tab): + self.parent_tab = tab + + def is_dependent_tab(self): + return self.parent_tab is not None + + def enabled(self): + if self.toggle_checkbox: + return self.toggle_checkbox.IsChecked() + else: + return True + + def update_toggle_state(self, event=None, notify_pair=True): + enable = self.enabled() + # print self.name, "update_toggle_state", enable + for child in self.settings_grid.GetChildren(): + widget = child.GetWindow() + if widget: + child.GetWindow().Enable(enable) + + if notify_pair and self.paired_tab: + self.paired_tab.pair_changed(enable) + + for tab in self.dependent_tabs: + tab.dependent_enable(enable) + + if event: + event.Skip() + + def pair_changed(self, value): + # print self.name, "pair_changed", value + new_value = not value + + if self.enabled() != new_value: + self.set_toggle_state(not value) + self.update_toggle_state(notify_pair=False) + + if self.on_change_hook: + self.on_change_hook(self) + + def dependent_enable(self, enable): + if enable: + self.toggle_checkbox.Enable() + else: + self.set_toggle_state(False) + self.toggle_checkbox.Disable() + self.update_toggle_state() + + if self.on_change_hook: + self.on_change_hook(self) + + def set_toggle_state(self, value): + if self.toggle_checkbox: + self.toggle_checkbox.SetValue(value) + self.changed_inputs.add(self.toggle_checkbox) + + def get_values(self): + values = {} + + if self.toggle: + checked = self.enabled() + if self.toggle_checkbox in self.changed_inputs and not self.toggle.inverse: + values[self.toggle.name] = checked + + if not checked: + # Ignore params on this tab if the toggle is unchecked, + # because they're grayed out anyway. + return values + + for name, input in self.param_inputs.iteritems(): + if input in self.changed_inputs and input != self.toggle_checkbox: + values[name] = input.GetValue() + + return values + + def apply(self): + values = self.get_values() + for node in self.nodes: + # print >> sys.stderr, "apply: ", self.name, node.id, values + for name, value in values.iteritems(): + node.set_param(name, value) + + def on_change(self, callable): + self.on_change_hook = callable + + def changed(self, event): + self.changed_inputs.add(event.GetEventObject()) + event.Skip() + + if self.on_change_hook: + self.on_change_hook(self) + + def load_preset(self, preset): + preset_data = preset.get(self.name, {}) + + for name, value in preset_data.iteritems(): + if name in self.param_inputs: + self.param_inputs[name].SetValue(value) + self.changed_inputs.add(self.param_inputs[name]) + + self.update_toggle_state() + + def save_preset(self, storage): + preset = storage[self.name] = {} + for name, input in self.param_inputs.iteritems(): + preset[name] = input.GetValue() + + def update_description(self): + if len(self.nodes) == 1: + description = _("These settings will be applied to 1 object.") + else: + description = _("These settings will be applied to %d objects.") % len(self.nodes) + + if any(len(param.values) > 1 for param in self.params): + description += "\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") + + if self.dependent_tabs: + if len(self.dependent_tabs) == 1: + description += "\n • " + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs) + else: + description += "\n • " + _("Disabling this tab will disable the following tab.") + + if self.paired_tab: + description += "\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name + + self.description_text = description + + def resized(self, event): + if not hasattr(self, 'rewrap_timer'): + self.rewrap_timer = wx.Timer() + self.rewrap_timer.Bind(wx.EVT_TIMER, self.rewrap) + + # If we try to rewrap every time we get EVT_SIZE then a resize is + # extremely slow. + self.rewrap_timer.Start(50, oneShot=True) + event.Skip() + + def rewrap(self, event=None): + self.description.SetLabel(self.description_text) + self.description.Wrap(self.GetSize().x - 20) + self.description_container.Layout() + if event: + event.Skip() + + def __set_properties(self): + # begin wxGlade: SatinPane.__set_properties + # end wxGlade + pass + + def __do_layout(self): + # just to add space around the settings + box = wx.BoxSizer(wx.VERTICAL) + + summary_box = wx.StaticBox(self, wx.ID_ANY, label=_("Inkscape objects")) + sizer = wx.StaticBoxSizer(summary_box, wx.HORIZONTAL) +# sizer = wx.BoxSizer(wx.HORIZONTAL) + self.description = wx.StaticText(self) + self.update_description() + self.description.SetLabel(self.description_text) + self.description_container = box + self.Bind(wx.EVT_SIZE, self.resized) + sizer.Add(self.description, proportion=0, flag=wx.EXPAND|wx.ALL, border=5) + box.Add(sizer, proportion=0, flag=wx.ALL, border=5) + + if self.toggle: + box.Add(self.toggle_checkbox, proportion=0, flag=wx.BOTTOM, border=10) + + for param in self.params: + description = wx.StaticText(self, label=param.description) + description.SetToolTip(param.tooltip) + + self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND|wx.RIGHT, border=40) + + if param.type == 'boolean': + + if len(param.values) > 1: + input = wx.CheckBox(self, style=wx.CHK_3STATE) + input.Set3StateValue(wx.CHK_UNDETERMINED) + else: + input = wx.CheckBox(self) + if param.values: + input.SetValue(param.values[0]) + + input.Bind(wx.EVT_CHECKBOX, self.changed) + elif len(param.values) > 1: + input = wx.ComboBox(self, wx.ID_ANY, choices=sorted(param.values), style=wx.CB_DROPDOWN) + input.Bind(wx.EVT_COMBOBOX, self.changed) + input.Bind(wx.EVT_TEXT, self.changed) + else: + value = param.values[0] if param.values else "" + input = wx.TextCtrl(self, wx.ID_ANY, value=str(value)) + input.Bind(wx.EVT_TEXT, self.changed) + + self.param_inputs[param.name] = input + + self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) + self.settings_grid.Add(wx.StaticText(self, label=param.unit or ""), proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) + + box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) + self.SetSizer(box) + + self.Layout() + +# end of class SatinPane + +class SettingsFrame(wx.Frame): + def __init__(self, *args, **kwargs): + # begin wxGlade: MyFrame.__init__ + self.tabs_factory = kwargs.pop('tabs_factory', []) + self.cancel_hook = kwargs.pop('on_cancel', None) + wx.Frame.__init__(self, None, wx.ID_ANY, + _("Embroidery Params") + ) + self.notebook = wx.Notebook(self, wx.ID_ANY) + self.tabs = self.tabs_factory(self.notebook) + + for tab in self.tabs: + tab.on_change(self.update_simulator) + + self.simulate_window = None + self.simulate_thread = None + self.simulate_refresh_needed = Event() + + wx.CallLater(1000, self.update_simulator) + + self.presets_box = wx.StaticBox(self, wx.ID_ANY, label=_("Presets")) + + self.preset_chooser = wx.ComboBox(self, wx.ID_ANY) + self.update_preset_list() + + self.load_preset_button = wx.Button(self, wx.ID_ANY, _("Load")) + self.load_preset_button.Bind(wx.EVT_BUTTON, self.load_preset) + + self.add_preset_button = wx.Button(self, wx.ID_ANY, _("Add")) + self.add_preset_button.Bind(wx.EVT_BUTTON, self.add_preset) + + self.overwrite_preset_button = wx.Button(self, wx.ID_ANY, _("Overwrite")) + self.overwrite_preset_button.Bind(wx.EVT_BUTTON, self.overwrite_preset) + + self.delete_preset_button = wx.Button(self, wx.ID_ANY, _("Delete")) + self.delete_preset_button.Bind(wx.EVT_BUTTON, self.delete_preset) + + self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel")) + self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) + self.Bind(wx.EVT_CLOSE, self.cancel) + + self.use_last_button = wx.Button(self, wx.ID_ANY, _("Use Last Settings")) + self.use_last_button.Bind(wx.EVT_BUTTON, self.use_last) + + self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) + self.apply_button.Bind(wx.EVT_BUTTON, self.apply) + + self.__set_properties() + self.__do_layout() + # end wxGlade + + def update_simulator(self, tab=None): + if self.simulate_window: + self.simulate_window.stop() + self.simulate_window.clear() + + if not self.simulate_thread or not self.simulate_thread.is_alive(): + self.simulate_thread = Thread(target=self.simulate_worker) + self.simulate_thread.daemon = True + self.simulate_thread.start() + + self.simulate_refresh_needed.set() + + def simulate_worker(self): + while True: + self.simulate_refresh_needed.wait() + self.simulate_refresh_needed.clear() + self.update_patches() + + def update_patches(self): + patches = self.generate_patches() + + if patches and not self.simulate_refresh_needed.is_set(): + wx.CallAfter(self.refresh_simulator, patches) + + def refresh_simulator(self, patches): + stitch_plan = patches_to_stitch_plan(patches) + if self.simulate_window: + self.simulate_window.stop() + self.simulate_window.load(stitch_plan=stitch_plan) + else: + my_rect = self.GetRect() + simulator_pos = my_rect.GetTopRight() + simulator_pos.x += 5 + + screen_rect = wx.Display(0).ClientArea + max_width = screen_rect.GetWidth() - my_rect.GetWidth() + max_height = screen_rect.GetHeight() + + try: + self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"), + simulator_pos, + size=(300, 300), + stitch_plan=stitch_plan, + on_close=self.simulate_window_closed, + target_duration=5, + max_width=max_width, + max_height=max_height) + except: + error = traceback.format_exc() + + try: + # a window may have been created, so we need to destroy it + # or the app will never exit + wx.Window.FindWindowByName("Preview").Destroy() + except: + pass + + info_dialog(self, error, _("Internal Error")) + + self.simulate_window.Show() + wx.CallLater(10, self.Raise) + + wx.CallAfter(self.simulate_window.go) + + def simulate_window_closed(self): + self.simulate_window = None + + def generate_patches(self): + patches = [] + nodes = [] + + for tab in self.tabs: + tab.apply() + + if tab.enabled() and not tab.is_dependent_tab(): + nodes.extend(tab.nodes) + + # sort nodes into the proper stacking order + nodes.sort(key=lambda node: node.order) + + try: + for node in nodes: + if self.simulate_refresh_needed.is_set(): + # cancel; params were updated and we need to start over + return [] + + # Making a copy of the embroidery element is an easy + # way to drop the cache in the @cache decorators used + # for many params in embroider.py. + + patches.extend(copy(node).embroider(None)) + except SystemExit: + raise + except: + # Ignore errors. This can be things like incorrect paths for + # satins or division by zero caused by incorrect param values. + pass + + return patches + + def update_preset_list(self): + preset_names = load_presets().keys() + preset_names = [preset for preset in preset_names if preset != "__LAST__"] + self.preset_chooser.SetItems(sorted(preset_names)) + + def get_preset_name(self): + preset_name = self.preset_chooser.GetValue().strip() + if preset_name: + return preset_name + else: + info_dialog(self, _("Please enter or select a preset name first."), caption=_('Preset')) + return + + def check_and_load_preset(self, preset_name): + preset = load_preset(preset_name) + if not preset: + info_dialog(self, _('Preset "%s" not found.') % preset_name, caption=_('Preset')) + + return preset + + def get_preset_data(self): + preset = {} + + current_tab = self.tabs[self.notebook.GetSelection()] + while current_tab.parent_tab: + current_tab = current_tab.parent_tab + + tabs = [current_tab] + if current_tab.paired_tab: + tabs.append(current_tab.paired_tab) + tabs.extend(current_tab.paired_tab.dependent_tabs) + tabs.extend(current_tab.dependent_tabs) + + for tab in tabs: + tab.save_preset(preset) + + return preset + + def add_preset(self, event, overwrite=False): + preset_name = self.get_preset_name() + if not preset_name: + return + + if not overwrite and load_preset(preset_name): + info_dialog(self, _('Preset "%s" already exists. Please use another name or press "Overwrite"') % preset_name, caption=_('Preset')) + + save_preset(preset_name, self.get_preset_data()) + self.update_preset_list() + + event.Skip() + + def overwrite_preset(self, event): + self.add_preset(event, overwrite=True) + + + def _load_preset(self, preset_name): + preset = self.check_and_load_preset(preset_name) + if not preset: + return + + for tab in self.tabs: + tab.load_preset(preset) + + + def load_preset(self, event): + preset_name = self.get_preset_name() + if not preset_name: + return + + self._load_preset(preset_name) + + event.Skip() + + + def delete_preset(self, event): + preset_name = self.get_preset_name() + if not preset_name: + return + + preset = self.check_and_load_preset(preset_name) + if not preset: + return + + delete_preset(preset_name) + self.update_preset_list() + self.preset_chooser.SetValue("") + + event.Skip() + + def _apply(self): + for tab in self.tabs: + tab.apply() + + def apply(self, event): + self._apply() + save_preset("__LAST__", self.get_preset_data()) + self.close() + + def use_last(self, event): + self._load_preset("__LAST__") + self.apply(event) + + def close(self): + if self.simulate_window: + self.simulate_window.stop() + self.simulate_window.Close() + + self.Destroy() + + def cancel(self, event): + if self.cancel_hook: + self.cancel_hook() + + self.close() + + def __set_properties(self): + # begin wxGlade: MyFrame.__set_properties + self.notebook.SetMinSize((800, 600)) + self.preset_chooser.SetSelection(-1) + # end wxGlade + + def __do_layout(self): + # begin wxGlade: MyFrame.__do_layout + sizer_1 = wx.BoxSizer(wx.VERTICAL) + #self.sizer_3_staticbox.Lower() + sizer_2 = wx.StaticBoxSizer(self.presets_box, wx.HORIZONTAL) + sizer_3 = wx.BoxSizer(wx.HORIZONTAL) + for tab in self.tabs: + self.notebook.AddPage(tab, tab.name) + sizer_1.Add(self.notebook, 1, wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, 10) + sizer_2.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) + sizer_2.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) + sizer_2.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) + sizer_2.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) + sizer_2.Add(self.delete_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) + sizer_1.Add(sizer_2, 0, flag=wx.EXPAND|wx.ALL, border=10) + sizer_3.Add(self.cancel_button, 0, wx.ALIGN_RIGHT|wx.RIGHT, 5) + sizer_3.Add(self.use_last_button, 0, wx.ALIGN_RIGHT|wx.RIGHT|wx.BOTTOM, 5) + sizer_3.Add(self.apply_button, 0, wx.ALIGN_RIGHT|wx.RIGHT|wx.BOTTOM, 5) + sizer_1.Add(sizer_3, 0, wx.ALIGN_RIGHT, 0) + self.SetSizer(sizer_1) + sizer_1.Fit(self) + self.Layout() + # end wxGlade + +class Params(InkstitchExtension): + def __init__(self, *args, **kwargs): + self.cancelled = False + InkstitchExtension.__init__(self, *args, **kwargs) + + def embroidery_classes(self, node): + element = EmbroideryElement(node) + classes = [] + + if element.get_style("fill"): + classes.append(AutoFill) + classes.append(Fill) + + if element.get_style("stroke"): + classes.append(Stroke) + + if element.get_style("stroke-dasharray") is None: + classes.append(SatinColumn) + + return classes + + def get_nodes_by_class(self): + nodes = self.get_nodes() + nodes_by_class = defaultdict(list) + + for z, node in enumerate(nodes): + for cls in self.embroidery_classes(node): + element = cls(node) + element.order = z + nodes_by_class[cls].append(element) + + return sorted(nodes_by_class.items(), key=lambda (cls, nodes): cls.__name__) + + def get_values(self, param, nodes): + getter = 'get_param' + + if param.type in ('toggle', 'boolean'): + getter = 'get_boolean_param' + else: + getter = 'get_param' + + values = filter(lambda item: item is not None, + (getattr(node, getter)(param.name, str(param.default)) for node in nodes)) + + return values + + def group_params(self, params): + def by_group_and_sort_index(param): + return param.group, param.sort_index + + def by_group(param): + return param.group + + return groupby(sorted(params, key=by_group_and_sort_index), by_group) + + def create_tabs(self, parent): + tabs = [] + for cls, nodes in self.get_nodes_by_class(): + params = cls.get_params() + + for param in params: + param.values = list(set(self.get_values(param, nodes))) + + parent_tab = None + new_tabs = [] + for group, params in self.group_params(params): + tab = ParamsTab(parent, id=wx.ID_ANY, name=group or cls.element_name, params=list(params), nodes=nodes) + new_tabs.append(tab) + + if group is None: + parent_tab = tab + + for tab in new_tabs: + if tab != parent_tab: + parent_tab.add_dependent_tab(tab) + tab.set_parent_tab(parent_tab) + + tabs.extend(new_tabs) + + for tab in tabs: + if tab.toggle and tab.toggle.inverse: + for other_tab in tabs: + if other_tab != tab and other_tab.toggle.name == tab.toggle.name: + tab.pair(other_tab) + other_tab.pair(tab) + + def tab_sort_key(tab): + parent = tab.parent_tab or tab + + sort_key = ( + # For Stroke and SatinColumn, place the one that's + # enabled first. Place dependent tabs first too. + parent.toggle and parent.toggle_checkbox.IsChecked(), + + # If multiple tabs are enabled, make sure dependent + # tabs are grouped with the parent. + parent, + + # Within parent/dependents, put the parent first. + tab == parent + ) + + return sort_key + + tabs.sort(key=tab_sort_key, reverse=True) + + return tabs + + + def cancel(self): + self.cancelled = True + + def effect(self): + app = wx.App() + frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel) + frame.Show() + app.MainLoop() + + if self.cancelled: + # This prevents the superclass from outputting the SVG, because we + # may have modified the DOM. + sys.exit(0) diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py new file mode 100644 index 00000000..5d462c0f --- /dev/null +++ b/lib/extensions/print_pdf.py @@ -0,0 +1,391 @@ +import sys +import traceback +import os +from threading import Thread +import socket +import errno +import time +import logging +from copy import deepcopy +import wx +import appdirs +import json + +import inkex +from .. import _, PIXELS_PER_MM, SVG_GROUP_TAG, translation as inkstitch_translation +from .base import InkstitchExtension +from ..stitch_plan import patches_to_stitch_plan +from ..svg import render_stitch_plan +from ..threads import ThreadCatalog + +from jinja2 import Environment, FileSystemLoader, select_autoescape +from datetime import date +import base64 + +from flask import Flask, request, Response, send_from_directory, jsonify +import webbrowser +import requests + + +def datetimeformat(value, format='%Y/%m/%d'): + return value.strftime(format) + + +def defaults_path(): + defaults_dir = appdirs.user_config_dir('inkstitch') + + if not os.path.exists(defaults_dir): + os.makedirs(defaults_dir) + + return os.path.join(defaults_dir, 'print_settings.json') + + +def load_defaults(): + try: + with open(defaults_path(), 'r') as defaults_file: + defaults = json.load(defaults_file) + return defaults + except: + return {} + + +def save_defaults(defaults): + with open(defaults_path(), 'w') as defaults_file: + json.dump(defaults, defaults_file) + + +def open_url(url): + # Avoid spurious output from xdg-open. Any output on stdout will crash + # inkscape. + null = open(os.devnull, 'w') + old_stdout = os.dup(sys.stdout.fileno()) + os.dup2(null.fileno(), sys.stdout.fileno()) + + if getattr(sys, 'frozen', False): + + # PyInstaller sets LD_LIBRARY_PATH. We need to temporarily clear it + # to avoid confusing xdg-open, which webbrowser will run. + + # The following code is adapted from PyInstaller's documentation + # http://pyinstaller.readthedocs.io/en/stable/runtime-information.html + + old_environ = dict(os.environ) # make a copy of the environment + lp_key = 'LD_LIBRARY_PATH' # for Linux and *BSD. + lp_orig = os.environ.get(lp_key + '_ORIG') # pyinstaller >= 20160820 has this + if lp_orig is not None: + os.environ[lp_key] = lp_orig # restore the original, unmodified value + else: + os.environ.pop(lp_key, None) # last resort: remove the env var + + webbrowser.open(url) + + # restore the old environ + os.environ.clear() + os.environ.update(old_environ) + else: + webbrowser.open(url) + + # restore file descriptors + os.dup2(old_stdout, sys.stdout.fileno()) + os.close(old_stdout) + + +class PrintPreviewServer(Thread): + def __init__(self, *args, **kwargs): + self.html = kwargs.pop('html') + self.metadata = kwargs.pop('metadata') + self.stitch_plan = kwargs.pop('stitch_plan') + Thread.__init__(self, *args, **kwargs) + self.daemon = True + self.last_request_time = None + self.shutting_down = False + + self.__setup_app() + + def __set_resources_path(self): + if getattr(sys, 'frozen', False): + self.resources_path = os.path.join(sys._MEIPASS, 'print', 'resources') + else: + self.resources_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', 'print', 'resources')) + + def __setup_app(self): + self.__set_resources_path() + self.app = Flask(__name__) + + @self.app.before_request + def request_started(): + self.last_request_time = time.time() + + @self.app.before_first_request + def start_watcher(): + self.watcher_thread = Thread(target=self.watch) + self.watcher_thread.daemon = True + self.watcher_thread.start() + + @self.app.route('/') + def index(): + return self.html + + @self.app.route('/shutdown', methods=['POST']) + def shutdown(): + self.shutting_down = True + request.environ.get('werkzeug.server.shutdown')() + return _('Closing...') + '<br/><br/>' + _('It is safe to close this window now.') + + @self.app.route('/resources/<path:resource>', methods=['GET']) + def resources(resource): + return send_from_directory(self.resources_path, resource, cache_timeout=1) + + @self.app.route('/ping') + def ping(): + # Javascript is letting us know it's still there. This resets self.last_request_time. + return "pong" + + @self.app.route('/printing/start') + def printing_start(): + # temporarily turn off the watcher while the print dialog is up, + # because javascript will be frozen + self.last_request_time = None + return "OK" + + @self.app.route('/printing/end') + def printing_end(): + # nothing to do here -- request_started() will restart the watcher + return "OK" + + @self.app.route('/settings/<field_name>', methods=['POST']) + def set_field(field_name): + self.metadata[field_name] = request.json['value'] + return "OK" + + @self.app.route('/settings/<field_mame>', methods=['GET']) + def get_field(field_name): + return jsonify(self.metadata[field_name]) + + @self.app.route('/settings', methods=['GET']) + def get_settings(): + settings = {} + settings.update(load_defaults()) + settings.update(self.metadata) + return jsonify(settings) + + @self.app.route('/defaults', methods=['POST']) + def set_defaults(): + save_defaults(request.json['value']) + return "OK" + + @self.app.route('/palette', methods=['POST']) + def set_palette(): + name = request.json['name'] + catalog = ThreadCatalog() + palette = catalog.get_palette_by_name(name) + catalog.apply_palette(self.stitch_plan, palette) + + # clear any saved color or thread names + for field in self.metadata: + if field.startswith('color-') or field.startswith('thread-'): + del self.metadata[field] + + self.metadata['thread-palette'] = name + + return "OK" + + @self.app.route('/threads', methods=['GET']) + def get_threads(): + threads = [] + for color_block in self.stitch_plan: + threads.append({ + 'hex': color_block.color.hex_digits, + 'name': color_block.color.name, + 'manufacturer': color_block.color.manufacturer, + 'number': color_block.color.number, + }) + + return jsonify(threads) + + def stop(self): + # for whatever reason, shutting down only seems possible in + # the context of a flask request, so we'll just make one + requests.post("http://%s:%s/shutdown" % (self.host, self.port)) + + def watch(self): + try: + while True: + time.sleep(1) + if self.shutting_down: + break + + if self.last_request_time is not None and \ + (time.time() - self.last_request_time) > 3: + self.stop() + break + except: + # seems like sometimes this thread blows up during shutdown + pass + + def disable_logging(self): + logging.getLogger('werkzeug').setLevel(logging.ERROR) + + def run(self): + self.disable_logging() + + self.host = "127.0.0.1" + self.port = 5000 + + while True: + try: + self.app.run(self.host, self.port, threaded=True) + except socket.error, e: + if e.errno == errno.EADDRINUSE: + self.port += 1 + continue + else: + raise + else: + break + + +class PrintInfoFrame(wx.Frame): + def __init__(self, *args, **kwargs): + self.print_server = kwargs.pop("print_server") + wx.Frame.__init__(self, *args, **kwargs) + + panel = wx.Panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + + text = wx.StaticText(panel, label=_("A print preview has been opened in your web browser. This window will stay open in order to communicate with the JavaScript code running in your browser.\n\nThis window will close after you close the print preview in your browser, or you can close it manually if necessary.")) + font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.NORMAL) + text.SetFont(font) + sizer.Add(text, proportion=1, flag=wx.ALL|wx.EXPAND, border=20) + + stop_button = wx.Button(panel, id=wx.ID_CLOSE) + stop_button.Bind(wx.EVT_BUTTON, self.close_button_clicked) + sizer.Add(stop_button, proportion=0, flag=wx.ALIGN_CENTER|wx.ALL, border=10) + + panel.SetSizer(sizer) + panel.Layout() + + self.timer = wx.PyTimer(self.__watcher) + self.timer.Start(250) + + def close_button_clicked(self, event): + self.print_server.stop() + + def __watcher(self): + if not self.print_server.is_alive(): + self.timer.Stop() + self.timer = None + self.Destroy() + + +class Print(InkstitchExtension): + def build_environment(self): + if getattr( sys, 'frozen', False ) : + template_dir = os.path.join(sys._MEIPASS, "print", "templates") + else: + template_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..", "print", "templates")) + + env = Environment( + loader = FileSystemLoader(template_dir), + autoescape=select_autoescape(['html', 'xml']), + extensions=['jinja2.ext.i18n'] + ) + + env.filters['datetimeformat'] = datetimeformat + env.install_gettext_translations(inkstitch_translation) + + return env + + def strip_namespaces(self): + # namespace prefixes seem to trip up HTML, so get rid of them + for element in self.document.iter(): + if element.tag[0]=='{': + element.tag = element.tag[element.tag.index('}',1) + 1:] + + def effect(self): + # It doesn't really make sense to print just a couple of selected + # objects. It's almost certain they meant to print the whole design. + # If they really wanted to print just a few objects, they could set + # the rest invisible temporarily. + self.selected = {} + + if not self.get_elements(): + return + + self.hide_all_layers() + + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches) + palette = ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) + render_stitch_plan(self.document.getroot(), stitch_plan) + + self.strip_namespaces() + + # Now the stitch plan layer will contain a set of groups, each + # corresponding to a color block. We'll create a set of SVG files + # corresponding to each individual color block and a final one + # for all color blocks together. + + svg = self.document.getroot() + layers = svg.findall("./g[@{http://www.inkscape.org/namespaces/inkscape}groupmode='layer']") + stitch_plan_layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + + # First, delete all of the other layers. We don't need them and they'll + # just bulk up the SVG. + for layer in layers: + if layer is not stitch_plan_layer: + svg.remove(layer) + + overview_svg = inkex.etree.tostring(self.document) + + color_block_groups = stitch_plan_layer.getchildren() + + for i, group in enumerate(color_block_groups): + # clear the stitch plan layer + del stitch_plan_layer[:] + + # add in just this group + stitch_plan_layer.append(group) + + # save an SVG preview + stitch_plan.color_blocks[i].svg_preview = inkex.etree.tostring(self.document) + + env = self.build_environment() + template = env.get_template('index.html') + + html = template.render( + view = {'client_overview': False, 'client_detailedview': False, 'operator_overview': True, 'operator_detailedview': True}, + logo = {'src' : '', 'title' : 'LOGO'}, + date = date.today(), + client = "", + job = { + 'title': '', + 'num_colors': stitch_plan.num_colors, + 'num_color_blocks': len(stitch_plan), + 'num_stops': stitch_plan.num_stops, + 'num_trims': stitch_plan.num_trims, + 'dimensions': stitch_plan.dimensions_mm, + 'num_stitches': stitch_plan.num_stitches, + 'estimated_time': '', # TODO + 'estimated_thread': '', # TODO + }, + svg_overview = overview_svg, + color_blocks = stitch_plan.color_blocks, + palettes = ThreadCatalog().palette_names(), + selected_palette = palette.name, + ) + + # We've totally mucked with the SVG. Restore it so that we can save + # metadata into it. + self.document = deepcopy(self.original_document) + + print_server = PrintPreviewServer(html=html, metadata=self.get_inkstitch_metadata(), stitch_plan=stitch_plan) + print_server.start() + + time.sleep(1) + open_url("http://%s:%s/" % (print_server.host, print_server.port)) + + app = wx.App() + info_frame = PrintInfoFrame(None, title=_("Ink/Stitch Print"), size=(450, 350), print_server=print_server) + info_frame.Show() + app.MainLoop() diff --git a/lib/extensions/simulate.py b/lib/extensions/simulate.py new file mode 100644 index 00000000..75bc62c7 --- /dev/null +++ b/lib/extensions/simulate.py @@ -0,0 +1,27 @@ +import wx + +from .base import InkstitchExtension +from ..simulator import EmbroiderySimulator +from ..stitch_plan import patches_to_stitch_plan + + +class Simulate(InkstitchExtension): + def __init__(self): + InkstitchExtension.__init__(self) + self.OptionParser.add_option("-P", "--path", + action="store", type="string", + dest="path", default=".", + help="Directory in which to store output file") + + def effect(self): + if not self.get_elements(): + return + + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches) + app = wx.App() + frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), stitch_plan=stitch_plan) + app.SetTopWindow(frame) + frame.Show() + wx.CallAfter(frame.go) + app.MainLoop() |
