diff options
Diffstat (limited to 'lib/svg')
| -rw-r--r-- | lib/svg/__init__.py | 1 | ||||
| -rw-r--r-- | lib/svg/path.py | 25 | ||||
| -rw-r--r-- | lib/svg/realistic_rendering.py | 129 | ||||
| -rw-r--r-- | lib/svg/svg.py | 44 | ||||
| -rw-r--r-- | lib/svg/tags.py | 6 | ||||
| -rw-r--r-- | lib/svg/units.py | 19 |
6 files changed, 217 insertions, 7 deletions
diff --git a/lib/svg/__init__.py b/lib/svg/__init__.py index 1895bba4..8e846555 100644 --- a/lib/svg/__init__.py +++ b/lib/svg/__init__.py @@ -1,2 +1,3 @@ from .svg import color_block_to_point_lists, render_stitch_plan from .units import * +from .path import apply_transforms, get_node_transform diff --git a/lib/svg/path.py b/lib/svg/path.py new file mode 100644 index 00000000..2d9c0ff3 --- /dev/null +++ b/lib/svg/path.py @@ -0,0 +1,25 @@ +import simpletransform +import cubicsuperpath + +from .units import get_viewbox_transform + +def apply_transforms(path, node): + transform = get_node_transform(node) + + # apply the combined transform to this node's path + simpletransform.applyTransformToPath(transform, path) + + return path + +def get_node_transform(node): + # start with the identity transform + transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + + # combine this node's transform with all parent groups' transforms + transform = simpletransform.composeParents(node, transform) + + # add in the transform implied by the viewBox + viewbox_transform = get_viewbox_transform(node.getroottree().getroot()) + transform = simpletransform.composeTransform(viewbox_transform, transform) + + return transform 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..48b1343a 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,31 @@ 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): + if not point_list: + continue + + color = color_block.color.visible_on_white.darker.to_hex_str() + start = point_list[0] + for point in point_list[1:]: + 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 +82,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 +101,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/svg/tags.py b/lib/svg/tags.py index fee59957..7eb87540 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -5,8 +5,14 @@ SVG_PATH_TAG = inkex.addNS('path', 'svg') SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg') SVG_DEFS_TAG = inkex.addNS('defs', 'svg') SVG_GROUP_TAG = inkex.addNS('g', 'svg') +SVG_SYMBOL_TAG = inkex.addNS('symbol', 'svg') +SVG_USE_TAG = inkex.addNS('use', 'svg') INKSCAPE_LABEL = inkex.addNS('label', 'inkscape') INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape') +CONNECTION_START = inkex.addNS('connection-start', 'inkscape') +CONNECTION_END = inkex.addNS('connection-end', 'inkscape') +CONNECTOR_TYPE = inkex.addNS('connector-type', 'inkscape') +XLINK_HREF = inkex.addNS('href', 'xlink') EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) diff --git a/lib/svg/units.py b/lib/svg/units.py index 015da60e..126027bc 100644 --- a/lib/svg/units.py +++ b/lib/svg/units.py @@ -75,11 +75,24 @@ def convert_length(length): raise ValueError(_("Unknown unit: %s") % units) +@cache +def get_viewbox(svg): + return svg.get('viewBox').strip().replace(',', ' ').split() + @cache def get_doc_size(svg): - doc_width = convert_length(svg.get('width')) - doc_height = convert_length(svg.get('height')) + width = svg.get('width') + height = svg.get('height') + + if width is None or height is None: + # fall back to the dimensions from the viewBox + viewbox = get_viewbox(svg) + width = viewbox[2] + height = viewbox[3] + + doc_width = convert_length(width) + doc_height = convert_length(height) return doc_width, doc_height @@ -88,7 +101,7 @@ def get_viewbox_transform(node): # somewhat cribbed from inkscape-silhouette doc_width, doc_height = get_doc_size(node) - viewbox = node.get('viewBox').strip().replace(',', ' ').split() + viewbox = get_viewbox(node) dx = -float(viewbox[0]) dy = -float(viewbox[1]) |
