summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/extensions/base.py8
-rw-r--r--lib/extensions/print_pdf.py85
-rw-r--r--lib/svg/realistic_rendering.py129
-rw-r--r--lib/svg/svg.py41
-rw-r--r--lib/threads/color.py15
-rw-r--r--lib/utils/geometry.py6
6 files changed, 245 insertions, 39 deletions
diff --git a/lib/extensions/base.py b/lib/extensions/base.py
index ff587ca5..52321cfc 100644
--- a/lib/extensions/base.py
+++ b/lib/extensions/base.py
@@ -63,10 +63,10 @@ class InkStitchMetadata(MutableMapping):
else:
item.getparent().remove(item)
- def _find_item(self, name):
+ def _find_item(self, name, create=True):
tag = inkex.addNS(name, "inkstitch")
item = self.metadata.find(tag)
- if item is None:
+ if item is None and create:
item = inkex.etree.SubElement(self.metadata, tag)
return item
@@ -80,9 +80,9 @@ class InkStitchMetadata(MutableMapping):
return None
def __delitem__(self, name):
- item = self._find_item(name)
+ item = self._find_item(name, create=False)
- if item:
+ if item is not None:
self.metadata.remove(item)
def __iter__(self):
diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py
index baeb7eba..6e2eff58 100644
--- a/lib/extensions/print_pdf.py
+++ b/lib/extensions/print_pdf.py
@@ -21,7 +21,7 @@ import requests
from .base import InkstitchExtension
from ..i18n import _, translation as inkstitch_translation
from ..svg import PIXELS_PER_MM, render_stitch_plan
-from ..svg.tags import SVG_GROUP_TAG
+from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE
from ..stitch_plan import patches_to_stitch_plan
from ..threads import ThreadCatalog
@@ -94,6 +94,8 @@ class PrintPreviewServer(Thread):
self.html = kwargs.pop('html')
self.metadata = kwargs.pop('metadata')
self.stitch_plan = kwargs.pop('stitch_plan')
+ self.realistic_overview_svg = kwargs.pop('realistic_overview_svg')
+ self.realistic_color_block_svgs = kwargs.pop('realistic_color_block_svgs')
Thread.__init__(self, *args, **kwargs)
self.daemon = True
self.last_request_time = None
@@ -202,6 +204,14 @@ class PrintPreviewServer(Thread):
return jsonify(threads)
+ @self.app.route('/realistic/block<int:index>', methods=['GET'])
+ def get_realistic_block(index):
+ return Response(self.realistic_color_block_svgs[index], mimetype='image/svg+xml')
+
+ @self.app.route('/realistic/overview', methods=['GET'])
+ def get_realistic_overview():
+ return Response(self.realistic_overview_svg, mimetype='image/svg+xml')
+
def stop(self):
# for whatever reason, shutting down only seems possible in
# the context of a flask request, so we'll just make one
@@ -295,38 +305,24 @@ class Print(InkstitchExtension):
return env
- def strip_namespaces(self):
+ def strip_namespaces(self, svg):
# namespace prefixes seem to trip up HTML, so get rid of them
- for element in self.document.iter():
+ for element in svg.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 = {}
+ def render_svgs(self, stitch_plan, realistic=False):
+ svg = deepcopy(self.document).getroot()
+ render_stitch_plan(svg, stitch_plan, realistic)
- 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()
+ self.strip_namespaces(svg)
# 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']")
+ layers = svg.findall("./g[@%s='layer']" % INKSCAPE_GROUPMODE)
stitch_plan_layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']")
# First, delete all of the other layers. We don't need them and they'll
@@ -335,9 +331,9 @@ class Print(InkstitchExtension):
if layer is not stitch_plan_layer:
svg.remove(layer)
- overview_svg = inkex.etree.tostring(self.document)
-
+ overview_svg = inkex.etree.tostring(svg)
color_block_groups = stitch_plan_layer.getchildren()
+ color_block_svgs = []
for i, group in enumerate(color_block_groups):
# clear the stitch plan layer
@@ -347,12 +343,15 @@ class Print(InkstitchExtension):
stitch_plan_layer.append(group)
# save an SVG preview
- stitch_plan.color_blocks[i].svg_preview = inkex.etree.tostring(self.document)
+ color_block_svgs.append(inkex.etree.tostring(svg))
+ return overview_svg, color_block_svgs
+
+ def render_html(self, stitch_plan, overview_svg, selected_palette):
env = self.build_environment()
template = env.get_template('index.html')
- html = template.render(
+ return template.render(
view = {'client_overview': False, 'client_detailedview': False, 'operator_overview': True, 'operator_detailedview': True},
logo = {'src' : '', 'title' : 'LOGO'},
date = date.today(),
@@ -371,14 +370,38 @@ class Print(InkstitchExtension):
svg_overview = overview_svg,
color_blocks = stitch_plan.color_blocks,
palettes = ThreadCatalog().palette_names(),
- selected_palette = palette,
+ selected_palette = selected_palette,
)
- # We've totally mucked with the SVG. Restore it so that we can save
- # metadata into it.
- self.document = deepcopy(self.original_document)
+ 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
+
+ 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'])
+
+ overview_svg, color_block_svgs = self.render_svgs(stitch_plan, realistic=False)
+ realistic_overview_svg, realistic_color_block_svgs = self.render_svgs(stitch_plan, realistic=True)
+
+ for i, svg in enumerate(color_block_svgs):
+ stitch_plan.color_blocks[i].svg_preview = svg
+
+ html = self.render_html(stitch_plan, overview_svg, palette)
- print_server = PrintPreviewServer(html=html, metadata=self.get_inkstitch_metadata(), stitch_plan=stitch_plan)
+ print_server = PrintPreviewServer(
+ html=html,
+ metadata=self.get_inkstitch_metadata(),
+ stitch_plan=stitch_plan,
+ realistic_overview_svg=realistic_overview_svg,
+ realistic_color_block_svgs=realistic_color_block_svgs
+ )
print_server.start()
time.sleep(1)
diff --git a/lib/svg/realistic_rendering.py b/lib/svg/realistic_rendering.py
new file mode 100644
index 00000000..e31534da
--- /dev/null
+++ b/lib/svg/realistic_rendering.py
@@ -0,0 +1,129 @@
+import simplepath
+import math
+
+from .units import PIXELS_PER_MM
+from ..utils import cache, Point
+
+# The stitch vector path looks like this:
+# _______
+# (_______)
+#
+# It's 0.32mm high, which is the approximate thickness of common machine
+# embroidery threads.
+
+# 1.216 pixels = 0.32mm
+stitch_height = 1.216
+
+# This vector path starts at the upper right corner of the stitch shape and
+# proceeds counter-clockwise.and contains a placeholder (%s) for the stitch
+# length.
+#
+# It contains two invisible "whiskers" of zero width that go above and below
+# to ensure that the SVG renderer allocates a large enough canvas area when
+# computing the gaussian blur steps. Otherwise, we'd have to expand the
+# width and height attributes of the <filter> tag to add more buffer space.
+# The width and height are specified in multiples of the bounding box
+# size, It's the bounding box aligned with the global SVG canvas's axes, not
+# the axes of the stitch itself. That means that having a big enough value
+# to add enough padding on the long sides of the stitch would waste a ton
+# of space on the short sides and significantly slow down rendering.
+stitch_path = "M0,0c0.4,0,0.4,0.3,0.4,0.6c0,0.3,-0.1,0.6,-0.4,0.6v0.2,-0.2h-%sc-0.4,0,-0.4,-0.3,-0.4,-0.6c0,-0.3,0.1,-0.6,0.4,-0.6v-0.2,0.2z"
+
+# This filter makes the above stitch path look like a real stitch with lighting.
+realistic_filter = """
+ <filter
+ style="color-interpolation-filters:sRGB"
+ id="realistic-stitch-filter"
+ x="-0.1"
+ width="1.2"
+ y="-0.1"
+ height="1.2">
+ <feGaussianBlur
+ stdDeviation="1.5"
+ id="feGaussianBlur1542-6"
+ in="SourceAlpha" />
+ <feComponentTransfer
+ id="feComponentTransfer1544-7"
+ result="result1">
+ <feFuncR
+ id="feFuncR1546-5"
+ type="identity" />
+ <feFuncG
+ id="feFuncG1548-3"
+ type="identity" />
+ <feFuncB
+ id="feFuncB1550-5"
+ type="identity"
+ slope="4.5300000000000002" />
+ <feFuncA
+ id="feFuncA1552-6"
+ type="gamma"
+ slope="0.14999999999999999"
+ intercept="0"
+ amplitude="3.1299999999999999"
+ offset="-0.33000000000000002" />
+ </feComponentTransfer>
+ <feComposite
+ in2="SourceAlpha"
+ id="feComposite1558-2"
+ operator="in" />
+ <feGaussianBlur
+ stdDeviation="0.089999999999999997"
+ id="feGaussianBlur1969" />
+ <feMorphology
+ id="feMorphology1971"
+ operator="dilate"
+ radius="0.10000000000000001" />
+ <feSpecularLighting
+ id="feSpecularLighting1973"
+ result="result2"
+ specularConstant="0.70899999"
+ surfaceScale="30">
+ <fePointLight
+ id="fePointLight1975"
+ z="10" />
+ </feSpecularLighting>
+ <feGaussianBlur
+ stdDeviation="0.040000000000000001"
+ id="feGaussianBlur1979" />
+ <feComposite
+ in2="SourceGraphic"
+ id="feComposite1977"
+ operator="arithmetic"
+ k2="1"
+ k3="1"
+ result="result3"
+ k1="0"
+ k4="0" />
+ <feComposite
+ in2="SourceAlpha"
+ id="feComposite1981"
+ operator="in" />
+ </filter>
+"""
+
+def realistic_stitch(start, end):
+ """Generate a stitch vector path given a start and end point."""
+
+ end = Point(*end)
+ start = Point(*start)
+
+ stitch_length = (end - start).length()
+ stitch_center = (end + start) / 2.0
+ stitch_direction = (end - start)
+ stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x)
+
+ stitch_length = max(0, stitch_length - 0.2 * PIXELS_PER_MM)
+
+ # create the path by filling in the length in the template
+ path = simplepath.parsePath(stitch_path % stitch_length)
+
+ # rotate the path to match the stitch
+ rotation_center_x = -stitch_length / 2.0
+ rotation_center_y = stitch_height / 2.0
+ simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y)
+
+ # move the path to the location of the stitch
+ simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y)
+
+ return simplepath.formatPath(path)
diff --git a/lib/svg/svg.py b/lib/svg/svg.py
index 852215f2..5552abd8 100644
--- a/lib/svg/svg.py
+++ b/lib/svg/svg.py
@@ -1,7 +1,8 @@
import simpletransform, simplestyle, inkex
from .units import get_viewbox_transform
-from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG
+from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG
+from .realistic_rendering import realistic_stitch, realistic_filter
from ..i18n import _
from ..utils import cache
@@ -32,6 +33,28 @@ def get_correction_transform(svg):
return transform
+def color_block_to_realistic_stitches(color_block, svg):
+ paths = []
+
+ for point_list in color_block_to_point_lists(color_block):
+ color = color_block.color.visible_on_white.darker.to_hex_str()
+ start = point_list[0]
+ for point in point_list[1:]:
+ paths.append(inkex.etree.Element(
+ SVG_PATH_TAG,
+ {'style': simplestyle.formatStyle(
+ {
+ 'fill': color,
+ 'stroke': 'none',
+ 'filter': 'url(#realistic-stitch-filter)'
+ }),
+ 'd': realistic_stitch(start, point),
+ 'transform': get_correction_transform(svg)
+ }))
+ start = point
+
+ return paths
+
def color_block_to_paths(color_block, svg):
paths = []
# We could emit just a single path with one subpath per point list, but
@@ -56,8 +79,7 @@ def color_block_to_paths(color_block, svg):
return paths
-
-def render_stitch_plan(svg, stitch_plan):
+def render_stitch_plan(svg, stitch_plan, realistic=False):
layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']")
if layer is None:
layer = inkex.etree.Element(SVG_GROUP_TAG,
@@ -76,6 +98,17 @@ def render_stitch_plan(svg, stitch_plan):
SVG_GROUP_TAG,
{'id': '__color_block_%d__' % i,
INKSCAPE_LABEL: "color block %d" % (i + 1)})
- group.extend(color_block_to_paths(color_block, svg))
+ if realistic:
+ group.extend(color_block_to_realistic_stitches(color_block, svg))
+ else:
+ group.extend(color_block_to_paths(color_block, svg))
svg.append(layer)
+
+ if realistic:
+ defs = svg.find(SVG_DEFS_TAG)
+
+ if defs is None:
+ defs = inkex.etree.SubElement(svg, SVG_DEFS_TAG)
+
+ defs.append(inkex.etree.fromstring(realistic_filter))
diff --git a/lib/threads/color.py b/lib/threads/color.py
index af474127..fede2ecc 100644
--- a/lib/threads/color.py
+++ b/lib/threads/color.py
@@ -80,3 +80,18 @@ class ThreadColor(object):
color = tuple(value * 255 for value in color)
return ThreadColor(color, name=self.name, number=self.number, manufacturer=self.manufacturer)
+
+ @property
+ def darker(self):
+ hls = list(colorsys.rgb_to_hls(*self.rgb_normalized))
+
+ # Capping lightness should make the color visible without changing it
+ # too much.
+ hls[1] *= 0.75
+
+ color = colorsys.hls_to_rgb(*hls)
+
+ # convert back to values in the range of 0-255
+ color = tuple(value * 255 for value in color)
+
+ return ThreadColor(color, name=self.name, number=self.number, manufacturer=self.manufacturer)
diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py
index 61b98bcb..7ff9b1cd 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -71,6 +71,12 @@ class Point:
else:
raise ValueError("cannot multiply Point by %s" % type(other))
+ def __div__(self, other):
+ if isinstance(other, (int, float)):
+ return self * (1.0 / other)
+ else:
+ raise ValueErorr("cannot divide Point by %s" % type(other))
+
def __repr__(self):
return "Point(%s,%s)" % (self.x, self.y)