basic lettering (#344)

Can handle multiple lines of text and routes the stitching in alternating directions on each line.
pull/356/head
Lex Neva 2018-11-14 20:23:06 -05:00 zatwierdzone przez GitHub
rodzic 238ad843dd
commit f5c85183d9
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
29 zmienionych plików z 12654 dodań i 94 usunięć

Wyświetl plik

@ -11,6 +11,7 @@ dist: distclean locales inx
cp -a images/examples dist/inkstitch
cp -a palettes dist/inkstitch
cp -a symbols dist/inkstitch
cp -a fonts dist/inkstitch
cp -a icons dist/inkstitch/bin
cp -a locales dist/inkstitch/bin
cp -a print dist/inkstitch/bin

Wyświetl plik

@ -0,0 +1,94 @@
Copyright (c) 2017, Lex Neva.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Wyświetl plik

@ -0,0 +1,24 @@
Small Font
==========
This is a small font for use in machine embroidery. It includes capital and lowercase letters, numbers, and the following punctuation: `< > = - + , : ( ) @ . _ ' " “ ”`
Currently one size is available, 0.2 inches, as measured by the width of the capital letter "M".
You may use this font for free in any embroidery project, including in designs that you intend to sell or sew out and sell. There is no limitation on the number of designs or items you may sell. For full details, check the LICENSE file in the top directory.
I've taken pains to make this font sew out nicely even though it's tiny. Every character is properly underlayed using a center-walk stitch. Even still, your choice of fabric, needle, and stabilizer will definitely affect your end results! Make sure you test it out before sewing onto a garment or anything else that you really care about.
If you can see areas for improvement, I'd like to know. Just open a github issue in this repo and we can talk through your ideas.
File Structure
--------------
This font is designed for Ink/Stitch and uses its standard font folder layout. The folder contains four files:
* `→.svg` contains glyphs designed for words laid out horizontally and stitched left-to-right
* `←.svg` contains glyphs designed for words laid out horizontally and stitched right-to-left
* `↓.svg` contains glyphs designed for words laid out vertically and stitched top-to-bottom
* `↑.svg` contains glyphs designed for words laid out vertically and stitched bottom-to-top
The files are named as they are so as to (hopefully) be recognizable to native speakers of multiple languages.

Wyświetl plik

@ -0,0 +1,12 @@
{
"name": "Ink/Stitch Small Font",
"description": "A font suited for small characters. The capital em is 0.2 inches wide.",
"leading": 8,
"letter_spacing": 1.5,
"word_spacing": 4.5,
"auto_satin": true,
"default_glyph": "<22>",
"kerning_pairs": {
"wo": -0.3
}
}

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 317 KiB

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 311 KiB

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 314 KiB

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 311 KiB

Wyświetl plik

@ -19,6 +19,16 @@ ch.setFormatter(formatter)
logger.addHandler(ch)
logger = logging.getLogger('shapely.geos')
logger.setLevel(logging.DEBUG)
shapely_errors = StringIO()
ch = logging.StreamHandler(shapely_errors)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
parser = ArgumentParser()
parser.add_argument("--extension")
my_args, remaining_args = parser.parse_known_args()

Wyświetl plik

@ -1,6 +1,7 @@
from auto_fill import AutoFill
from element import EmbroideryElement
from fill import Fill
from polyline import Polyline
from satin_column import SatinColumn
from stroke import Stroke
from polyline import Polyline
from fill import Fill
from auto_fill import AutoFill
from utils import node_to_elements, nodes_to_elements

Wyświetl plik

@ -0,0 +1,47 @@
from ..commands import is_command
from ..svg.tags import SVG_POLYLINE_TAG, SVG_PATH_TAG
from .auto_fill import AutoFill
from .element import EmbroideryElement
from .fill import Fill
from .polyline import Polyline
from .satin_column import SatinColumn
from .stroke import Stroke
def node_to_elements(node):
if node.tag == SVG_POLYLINE_TAG:
return [Polyline(node)]
elif node.tag == SVG_PATH_TAG:
element = EmbroideryElement(node)
if element.get_boolean_param("satin_column"):
return [SatinColumn(node)]
else:
elements = []
if element.get_style("fill", "black"):
if element.get_boolean_param("auto_fill", True):
elements.append(AutoFill(node))
else:
elements.append(Fill(node))
if element.get_style("stroke"):
if not is_command(element.node):
elements.append(Stroke(node))
if element.get_boolean_param("stroke_first", False):
elements.reverse()
return elements
else:
return []
def nodes_to_elements(nodes):
elements = []
for node in nodes:
elements.extend(node_to_elements(node))
return elements

Wyświetl plik

@ -1,18 +1,20 @@
from auto_satin import AutoSatin
from convert_to_satin import ConvertToSatin
from cut_satin import CutSatin
from embroider import Embroider
from flip import Flip
from global_commands import GlobalCommands
from input import Input
from install import Install
from layer_commands import LayerCommands
from lettering import Lettering
from object_commands import ObjectCommands
from output import Output
from params import Params
from print_pdf import Print
from simulate import Simulate
from input import Input
from output import Output
from zip import Zip
from flip import Flip
from object_commands import ObjectCommands
from layer_commands import LayerCommands
from global_commands import GlobalCommands
from convert_to_satin import ConvertToSatin
from cut_satin import CutSatin
from auto_satin import AutoSatin
__all__ = extensions = [Embroider,
Install,
@ -28,4 +30,5 @@ __all__ = extensions = [Embroider,
GlobalCommands,
ConvertToSatin,
CutSatin,
AutoSatin]
AutoSatin,
Lettering]

Wyświetl plik

@ -41,14 +41,15 @@ class AutoSatin(CommandsExtension):
if not self.check_selection():
return
group = self.create_group()
new_elements, trim_indices = self.auto_satin()
# The ordering is careful here. Some of the original satins may have
# been used unmodified. That's why we remove all of the original
# satins _first_ before adding new_elements back into the SVG.
self.remove_original_satins()
self.add_elements(group, new_elements)
if self.options.preserve_order:
# when preservering order, auto_satin() takes care of putting the
# newly-created elements into the existing group nodes in the SVG
# DOM
new_elements, trim_indices = self.auto_satin()
else:
group = self.create_group()
new_elements, trim_indices = self.auto_satin()
self.add_elements(group, new_elements)
self.add_trims(new_elements, trim_indices)
@ -79,13 +80,6 @@ class AutoSatin(CommandsExtension):
ending_point = self.get_ending_point()
return auto_satin(self.elements, self.options.preserve_order, starting_point, ending_point)
def remove_original_satins(self):
for element in self.elements:
for command in element.commands:
command.connector.getparent().remove(command.connector)
command.use.getparent().remove(command.use)
element.node.getparent().remove(element.node)
def add_elements(self, group, new_elements):
for i, element in enumerate(new_elements):
if isinstance(element, SatinColumn):

Wyświetl plik

@ -6,10 +6,10 @@ import re
import inkex
from stringcase import snakecase
from ..commands import is_command, layer_commands
from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement
from ..commands import layer_commands
from ..elements import EmbroideryElement, nodes_to_elements
from ..i18n import _
from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS, SVG_POLYLINE_TAG
from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS
SVG_METADATA_TAG = inkex.addNS("metadata", "svg")
@ -109,6 +109,16 @@ class InkstitchExtension(inkex.Effect):
if g.get(INKSCAPE_GROUPMODE) == "layer":
g.set("style", "display:none")
def ensure_current_layer(self):
# if no layer is selected, inkex defaults to the root, which isn't
# particularly useful
if self.current_layer is self.document.getroot():
try:
self.current_layer = self.document.xpath(".//svg:g[@inkscape:groupmode='layer']", namespaces=inkex.NSS)[0]
except IndexError:
# No layers at all?? Fine, we'll stick with the default.
pass
def no_elements_error(self):
if self.selected:
inkex.errormsg(_("No embroiderable paths selected."))
@ -151,38 +161,8 @@ class InkstitchExtension(inkex.Effect):
def get_nodes(self):
return self.descendants(self.document.getroot())
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", "black"):
if element.get_boolean_param("auto_fill", True):
classes.append(AutoFill)
else:
classes.append(Fill)
if element.get_style("stroke"):
if not is_command(element.node):
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)
self.elements = nodes_to_elements(self.get_nodes())
if self.elements:
return True
else:

Wyświetl plik

@ -1,15 +1,15 @@
import os
from .base import InkstitchExtension
from ..i18n import _
from ..output import write_embroidery_file
from ..stitch_plan import patches_to_stitch_plan
from ..svg import render_stitch_plan, PIXELS_PER_MM
from .base import InkstitchExtension
class Embroider(InkstitchExtension):
def __init__(self, *args, **kwargs):
InkstitchExtension.__init__(self)
InkstitchExtension.__init__(self, *args, **kwargs)
self.OptionParser.add_option("-c", "--collapse_len_mm",
action="store", type="float",
dest="collapse_length_mm", default=3.0,

Wyświetl plik

@ -1,25 +1,15 @@
import inkex
from .commands import CommandsExtension
from ..commands import LAYER_COMMANDS, get_command_description
from ..i18n import _
from ..svg.tags import SVG_USE_TAG, INKSCAPE_LABEL, XLINK_HREF
from ..svg import get_correction_transform
from ..svg.tags import SVG_USE_TAG, INKSCAPE_LABEL, XLINK_HREF
from .commands import CommandsExtension
class LayerCommands(CommandsExtension):
COMMANDS = LAYER_COMMANDS
def ensure_current_layer(self):
# if no layer is selected, inkex defaults to the root, which isn't
# particularly useful
if self.current_layer is self.document.getroot():
try:
self.current_layer = self.document.xpath(".//svg:g[@inkscape:groupmode='layer']", namespaces=inkex.NSS)[0]
except IndexError:
# No layers at all?? Fine, we'll stick with the default.
pass
def effect(self):
commands = [command for command in self.COMMANDS if getattr(self.options, command)]

Wyświetl plik

@ -0,0 +1,39 @@
import os
from ..i18n import _
from ..lettering import Font
from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL
from ..utils import get_bundled_dir
from .commands import CommandsExtension
class Lettering(CommandsExtension):
COMMANDS = ["trim"]
def __init__(self, *args, **kwargs):
CommandsExtension.__init__(self, *args, **kwargs)
self.OptionParser.add_option("-t", "--text")
def effect(self):
font_path = os.path.join(get_bundled_dir("fonts"), "small_font")
font = Font(font_path)
self.ensure_current_layer()
lines = font.render_text(self.options.text.decode('utf-8'))
self.set_labels(lines)
self.current_layer.append(lines)
def set_labels(self, lines):
path = 1
for node in lines.iterdescendants():
if node.tag == SVG_PATH_TAG:
node.set("id", self.uniqueId("lettering"))
# L10N Label for an object created by the Lettering extension
node.set(INKSCAPE_LABEL, _("Lettering %d") % path)
path += 1
elif node.tag == SVG_GROUP_TAG:
node.set("id", self.uniqueId("letteringline"))
# lettering extension already set the label

Wyświetl plik

@ -0,0 +1 @@
from font import Font

Wyświetl plik

@ -0,0 +1,185 @@
# -*- coding: UTF-8 -*-
from copy import deepcopy
import json
import os
import inkex
from ..elements import nodes_to_elements
from ..i18n import _
from ..stitches.auto_satin import auto_satin
from ..svg import PIXELS_PER_MM
from ..svg.tags import SVG_GROUP_TAG, SVG_PATH_TAG, INKSCAPE_LABEL
from ..utils import Point
from .font_variant import FontVariant
def font_metadata(name, default=None, multiplier=None):
def getter(self):
value = self.metadata.get(name, default)
if multiplier is not None:
value *= multiplier
return value
return property(getter)
class Font(object):
"""Represents a font with multiple variants.
Each font may have multiple FontVariants for left-to-right, right-to-left,
etc. Each variant has a set of Glyphs, one per character.
Properties:
path -- the path to the directory containing this font
metadata -- A dict of information about the font.
name -- Shortcut property for metadata["name"]
license -- contents of the font's LICENSE file, or None if no LICENSE file exists.
variants -- A dict of FontVariants, with keys in FontVariant.VARIANT_TYPES.
"""
def __init__(self, font_path):
self.path = font_path
self._load_metadata()
self._load_license()
self._load_variants()
def _load_metadata(self):
try:
with open(os.path.join(self.path, "font.json")) as metadata_file:
self.metadata = json.load(metadata_file)
except IOError:
self.metadata = {}
def _load_license(self):
try:
with open(os.path.join(self.path, "LICENSE")) as license_file:
self.license = license_file.read()
except IOError:
self.license = None
def _load_variants(self):
self.variants = {}
for variant in FontVariant.VARIANT_TYPES:
try:
self.variants[variant] = FontVariant(self.path, variant, self.default_glyph)
except IOError:
# we'll deal with missing variants when we apply lettering
pass
name = font_metadata('name', '')
description = font_metadata('description', '')
default_variant = font_metadata('default_variant', FontVariant.LEFT_TO_RIGHT)
default_glyph = font_metadata('defalt_glyph', u"<EFBFBD>")
letter_spacing = font_metadata('letter_spacing', 1.5, multiplier=PIXELS_PER_MM)
leading = font_metadata('leading', 5, multiplier=PIXELS_PER_MM)
word_spacing = font_metadata('word_spacing', 3, multiplier=PIXELS_PER_MM)
kerning_pairs = font_metadata('kerning_pairs', {})
auto_satin = font_metadata('auto_satin', True)
def render_text(self, text, variant=None, back_and_forth=True):
if variant is None:
variant = self.default_variant
if back_and_forth:
glyph_sets = [self.get_variant(variant), self.get_variant(FontVariant.reversed_variant(variant))]
else:
glyph_sets = [self.get_variant(variant)] * 2
line_group = inkex.etree.Element(SVG_GROUP_TAG, {
INKSCAPE_LABEL: _("Ink/Stitch Text")
})
position = Point(0, 0)
for i, line in enumerate(text.splitlines()):
glyph_set = glyph_sets[i % 2]
line = line.strip()
letter_group = self._render_line(line, position, glyph_set)
if glyph_set.variant == FontVariant.RIGHT_TO_LEFT:
letter_group[:] = reversed(letter_group)
line_group.append(letter_group)
position.x = 0
position.y += self.leading
if self.auto_satin:
self._apply_auto_satin(line_group)
return line_group
def get_variant(self, variant):
return self.variants.get(variant, self.variants[self.default_variant])
def _render_line(self, line, position, glyph_set):
"""Render a line of text.
An SVG XML node tree will be returned, with an svg:g at its root. If
the font metadata requests it, Auto-Satin will be applied.
Parameters:
line -- the line of text to render.
position -- Current position. Will be updated to point to the spot
immediately after the last character.
glyph_set -- a FontVariant instance.
Returns:
An svg:g element containing the rendered text.
"""
group = inkex.etree.Element(SVG_GROUP_TAG, {
INKSCAPE_LABEL: line
})
last_character = None
for character in line:
if character == " ":
position.x += self.word_spacing
last_character = None
else:
glyph = glyph_set[character] or glyph_set[self.default_glyph]
if glyph is not None:
node = self._render_glyph(glyph, position, character, last_character)
group.append(node)
last_character = character
return group
def _render_glyph(self, glyph, position, character, last_character):
"""Render a single glyph.
An SVG XML node tree will be returned, with an svg:g at its root.
Parameters:
glyph -- a Glyph instance
position -- Current position. Will be updated based on the width
of this character and the letter spacing.
character -- the current Unicode character.
last_character -- the previous character in the line, or None if
we're at the start of the line or a word.
"""
node = deepcopy(glyph.node)
if last_character is not None:
position.x += self.letter_spacing + self.kerning_pairs.get(last_character + character, 0) * PIXELS_PER_MM
transform = "translate(%s, %s)" % position.as_tuple()
node.set('transform', transform)
position.x += glyph.width
return node
def _apply_auto_satin(self, group):
"""Apply Auto-Satin to an SVG XML node tree with an svg:g at its root.
The group's contents will be replaced with the results of the auto-
satin operation. Any nested svg:g elements will be removed.
"""
elements = nodes_to_elements(group.iterdescendants(SVG_PATH_TAG))
auto_satin(elements, preserve_order=True)

Wyświetl plik

@ -0,0 +1,86 @@
# -*- coding: UTF-8 -*-
import os
import inkex
import simplestyle
from ..svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL
from .glyph import Glyph
class FontVariant(object):
"""Represents a single variant of a font.
Each font may have multiple variants for left-to-right, right-to-left,
etc. Each variant has a set of Glyphs, one per character.
A FontVariant instance can be accessed as a dict by using a unicode
character as a key.
Properties:
path -- the path to the directory containing this font
variant -- the font variant, specified using one of the constants below
glyphs -- a dict of Glyphs, with the glyphs' unicode characters as keys.
"""
# We use unicode characters rather than English strings for font file names
# in order to be more approachable for languages other than English.
LEFT_TO_RIGHT = u""
RIGHT_TO_LEFT = u""
TOP_TO_BOTTOM = u""
BOTTOM_TO_TOP = u""
VARIANT_TYPES = (LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP)
@classmethod
def reversed_variant(cls, variant):
if variant == cls.LEFT_TO_RIGHT:
return cls.RIGHT_TO_LEFT
elif variant == cls.RIGHT_TO_LEFT:
return cls.LEFT_TO_RIGHT
elif variant == cls.TOP_TO_BOTTOM:
return cls.BOTTOM_TO_TOP
elif variant == cls.BOTTOM_TO_TOP:
return cls.TOP_TO_BOTTOM
else:
return None
def __init__(self, font_path, variant, default_glyph=None):
self.path = font_path
self.variant = variant
self.default_glyph = default_glyph
self.glyphs = {}
self._load_glyphs()
def _load_glyphs(self):
svg_path = os.path.join(self.path, u"%s.svg" % self.variant)
svg = inkex.etree.parse(svg_path)
glyph_layers = svg.xpath(".//svg:g[starts-with(@inkscape:label, 'GlyphLayer-')]", namespaces=inkex.NSS)
for layer in glyph_layers:
self._clean_group(layer)
layer.attrib[INKSCAPE_LABEL] = layer.attrib[INKSCAPE_LABEL].replace("GlyphLayer-", "", 1)
glyph_name = layer.attrib[INKSCAPE_LABEL]
self.glyphs[glyph_name] = Glyph(layer)
def _clean_group(self, group):
# We'll repurpose the layer as a container group labelled with the
# glyph.
del group.attrib[INKSCAPE_GROUPMODE]
style_text = group.get('style')
if style_text:
# The layer may be marked invisible, so we'll clear the 'display'
# style.
style = simplestyle.parseStyle(group.get('style'))
style.pop('display')
group.set('style', simplestyle.formatStyle(style))
def __getitem__(self, character):
if character in self.glyphs:
return self.glyphs[character]
else:
return self.glyphs.get(self.default_glyph, None)
def __contains__(self, character):
return character in self.glyphs

Wyświetl plik

@ -0,0 +1,86 @@
from copy import copy
import cubicsuperpath
import simpletransform
from ..svg import apply_transforms, get_guides
from ..svg.tags import SVG_GROUP_TAG, SVG_PATH_TAG
class Glyph(object):
"""Represents a single character in a single font variant.
For example, the font inkstitch_small may have variants for left-to-right,
right-to-left, etc. Each variant would have a set of Glyphs, one for each
character in that variant.
Properties:
width -- total width of this glyph including all component satins
node -- svg:g XML node containing the component satins in this character
"""
def __init__(self, group):
"""Create a Glyph.
The nodes will be copied out of their parent SVG document (with nested
transforms applied). The original nodes will be unmodified.
Arguments:
group -- an svg:g XML node containing all the paths that make up
this Glyph. Nested groups are allowed.
"""
self._process_baseline(group.getroottree().getroot())
self.node = self._process_group(group)
self._process_bbox()
self._move_to_origin()
def _process_group(self, group):
new_group = copy(group)
new_group.attrib.pop('transform', None)
del new_group[:] # delete references to the original group's children
for node in group:
if node.tag == SVG_GROUP_TAG:
new_group.append(self._process_group(node))
else:
node_copy = copy(node)
if "d" in node.attrib:
# Convert the path to absolute coordinates, incorporating all
# nested transforms.
path = cubicsuperpath.parsePath(node.get("d"))
apply_transforms(path, node)
node_copy.set("d", cubicsuperpath.formatPath(path))
# Delete transforms from paths and groups, since we applied
# them to the paths already.
node_copy.attrib.pop('transform', None)
new_group.append(node_copy)
return new_group
def _process_baseline(self, svg):
for guide in get_guides(svg):
if guide.label == "baseline":
self._baseline = guide.position.y
break
else:
# no baseline guide found, assume 0 for lack of anything better to use...
self._baseline = 0
def _process_bbox(self):
left, right, top, bottom = simpletransform.computeBBox(self.node.iterdescendants())
self.width = right - left
self._min_x = left
def _move_to_origin(self):
translate_x = -self._min_x
translate_y = -self._baseline
transform = "translate(%s, %s)" % (translate_x, translate_y)
for node in self.node.iter(SVG_PATH_TAG):
node.set('transform', transform)
simpletransform.fuseTransform(node)

Wyświetl plik

@ -1,4 +1,4 @@
from itertools import chain
from itertools import chain, izip
import math
import cubicsuperpath
@ -9,8 +9,8 @@ import simplestyle
import networkx as nx
from ..elements import Stroke
from ..svg import PIXELS_PER_MM, line_strings_to_csp
from ..elements import Stroke, SatinColumn
from ..svg import PIXELS_PER_MM, line_strings_to_csp, get_correction_transform
from ..svg.tags import SVG_PATH_TAG
from ..utils import Point as InkstitchPoint, cut, cache
@ -25,7 +25,7 @@ class SatinSegment(object):
reverse -- if True, reverse the direction of the satin
"""
def __init__(self, satin, start=0.0, end=1.0, reverse=False):
def __init__(self, satin, start=0.0, end=1.0, reverse=False, original_satin=None):
"""Initialize a SatinEdge.
Arguments:
@ -38,6 +38,7 @@ class SatinSegment(object):
"""
self.satin = satin
self.original_satin = original_satin or self.satin
self.reverse = reverse
# start and end are stored as normalized projections
@ -74,17 +75,19 @@ class SatinSegment(object):
to_element = to_satin
def to_running_stitch(self):
return RunningStitch(self.center_line, self.satin)
return RunningStitch(self.center_line, self.original_satin)
def break_up(self, segment_size):
"""Break this SatinSegment up into SatinSegments of the specified size."""
num_segments = int(math.ceil(self.center_line.length / segment_size))
segments = []
satin = self.to_satin()
for i in xrange(num_segments):
segments.append(SatinSegment(satin, float(
i) / num_segments, float(i + 1) / num_segments, self.reverse))
segments.append(SatinSegment(self.satin,
float(i) / num_segments,
float(i + 1) / num_segments,
self.reverse,
self.original_satin))
if self.reverse:
segments.reverse()
@ -121,6 +124,10 @@ class SatinSegment(object):
def end_point(self):
return self.satin.center_line.interpolate(self.end, normalized=True)
@property
def original_node(self):
return self.original_satin.node
def is_sequential(self, other):
"""Check if a satin segment immediately follows this one on the same satin."""
@ -213,10 +220,11 @@ class RunningStitch(object):
style['stroke-dasharray'] = "0.5,0.5"
style = simplestyle.formatStyle(style)
node.set("style", style)
node.set("embroider_running_stitch_length_mm", self.running_stitch_length)
return Stroke(node)
stroke = Stroke(node)
return stroke
@property
@cache
@ -228,6 +236,10 @@ class RunningStitch(object):
def end_point(self):
return self.path.interpolate(1.0, normalized=True)
@property
def original_node(self):
return self.original_element.node
@cache
def reversed(self):
return RunningStitch(shgeo.LineString(reversed(self.path.coords)), self.style)
@ -236,6 +248,9 @@ class RunningStitch(object):
if not isinstance(other, RunningStitch):
return False
if self.original_element is not other.original_element:
return False
return self.path.distance(other.path) < 0.5
def __add__(self, other):
@ -277,6 +292,11 @@ def auto_satin(elements, preserve_order=False, starting_point=None, ending_point
instances (that are running stitch) can be included to indicate how to travel
between two SatinColumns. This works best when preserve_order is True.
If preserve_order is True, then the elements and any newly-created elements
will be in the same position in the SVG DOM. If preserve_order is False, then
the elements will be removed from the SVG DOM and it's up to the caller to
decide where to put the returned SVG path nodes.
Returns: a list of SVG path nodes making up the selected stitch order.
Jumps between objects are implied if they are not right next to each
other.
@ -289,7 +309,13 @@ def auto_satin(elements, preserve_order=False, starting_point=None, ending_point
path = find_path(graph, starting_node, ending_node)
operations = path_to_operations(graph, path)
operations = collapse_sequential_segments(operations)
return operations_to_elements_and_trims(operations)
new_elements, trims, original_parents = operations_to_elements_and_trims(operations, preserve_order)
remove_original_elements(elements)
if preserve_order:
preserve_original_groups(new_elements, original_parents)
return new_elements, trims
def build_graph(elements, preserve_order=False):
@ -306,7 +332,7 @@ def build_graph(elements, preserve_order=False):
segments = []
if isinstance(element, Stroke):
segments.append(RunningStitch(element))
else:
elif isinstance(element, SatinColumn):
whole_satin = SatinSegment(element)
segments = whole_satin.break_up(PIXELS_PER_MM)
@ -548,26 +574,57 @@ def collapse_sequential_segments(old_operations):
return new_operations
def operations_to_elements_and_trims(operations):
def operations_to_elements_and_trims(operations, preserve_order):
"""Convert a list of operations to Elements and locations of trims.
Returns:
(nodes, trims)
(elements, trims, original_parents)
element -- a list of Element instances
elements -- a list of Element instances
trims -- indices of nodes after which the thread should be trimmed
original_parents -- a parallel list of the original SVG parent nodes that spawned each element
"""
elements = []
trims = []
original_parent_nodes = []
for operation in operations:
# Ignore JumpStitch opertions. Jump stitches in Ink/Stitch are
# Ignore JumpStitch operations. Jump stitches in Ink/Stitch are
# implied and added by Embroider if needed.
if isinstance(operation, (SatinSegment, RunningStitch)):
elements.append(operation.to_element())
original_parent_nodes.append(operation.original_node.getparent())
elif isinstance(operation, (JumpStitch)):
if elements and operation.length > PIXELS_PER_MM:
trims.append(len(elements) - 1)
return elements, list(set(trims))
return elements, list(set(trims)), original_parent_nodes
def remove_original_elements(elements):
for element in elements:
for command in element.commands:
remove_from_parent(command.connector)
remove_from_parent(command.use)
remove_from_parent(element.node)
def remove_from_parent(node):
if node.getparent() is not None:
node.getparent().remove(node)
def preserve_original_groups(elements, original_parent_nodes):
"""Ensure that all elements are contained in the original SVG group elements.
When preserve_order is True, no SatinColumn or Stroke elements will be
reordered in the XML tree. This makes it possible to preserve original SVG
group membership. We'll ensure that each newly-created Element is added
to the group that contained the original SatinColumn that spawned it.
"""
for element, parent in izip(elements, original_parent_nodes):
if parent is not None:
parent.append(element.node)
element.node.set('transform', get_correction_transform(parent, child=True))

Wyświetl plik

@ -1,3 +1,4 @@
from .svg import color_block_to_point_lists, render_stitch_plan
from .units import *
from .path import apply_transforms, get_node_transform, get_correction_transform, line_strings_to_csp, point_lists_to_csp
from .guides import get_guides

43
lib/svg/guides.py 100644
Wyświetl plik

@ -0,0 +1,43 @@
import simpletransform
from ..utils import string_to_floats, Point, cache
from .tags import SODIPODI_NAMEDVIEW, SODIPODI_GUIDE, INKSCAPE_LABEL
from .units import get_doc_size, get_viewbox_transform
class InkscapeGuide(object):
def __init__(self, node):
self.node = node
self.svg = node.getroottree().getroot()
self._parse()
def _parse(self):
self.label = self.node.get(INKSCAPE_LABEL, "")
doc_size = list(get_doc_size(self.svg))
# convert the size from viewbox-relative to real-world pixels
viewbox_transform = get_viewbox_transform(self.svg)
simpletransform.applyTransformToPoint(simpletransform.invertTransform(viewbox_transform), doc_size)
self.position = Point(*string_to_floats(self.node.get('position')))
# inkscape's Y axis is reversed from SVG's, and the guide is in inkscape coordinates
self.position.y = doc_size[1] - self.position.y
# This one baffles me. I think inkscape might have gotten the order of
# their vector wrong?
parts = string_to_floats(self.node.get('orientation'))
self.direction = Point(parts[1], parts[0])
@cache
def get_guides(svg):
"""Find all Inkscape guides and return as InkscapeGuide instances."""
namedview = svg.find(SODIPODI_NAMEDVIEW)
if namedview is None:
return []
return [InkscapeGuide(node) for node in namedview.findall(SODIPODI_GUIDE)]

Wyświetl plik

@ -1,3 +1,4 @@
import inkex
import simpletransform
from .units import get_viewbox_transform
@ -12,6 +13,19 @@ def apply_transforms(path, node):
return path
def compose_parent_transforms(node, mat):
# This is adapted from Inkscape's simpletransform.py's composeParents()
# function. That one can't handle nodes that are detached from a DOM.
trans = node.get('transform')
if trans:
mat = simpletransform.composeTransform(simpletransform.parseTransform(trans), mat)
if node.getparent() is not None:
if node.getparent().tag == inkex.addNS('g', 'svg'):
mat = compose_parent_transforms(node.getparent(), mat)
return mat
def get_node_transform(node):
# start with the identity transform
transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
@ -19,7 +33,7 @@ def get_node_transform(node):
# this if is because sometimes inkscape likes to create paths outside of a layer?!
if node.getparent() is not None:
# combine this node's transform with all parent groups' transforms
transform = simpletransform.composeParents(node, transform)
transform = compose_parent_transforms(node, transform)
# add in the transform implied by the viewBox
viewbox_transform = get_viewbox_transform(node.getroottree().getroot())

Wyświetl plik

@ -14,5 +14,7 @@ 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')
SODIPODI_NAMEDVIEW = inkex.addNS('namedview', 'sodipodi')
SODIPODI_GUIDE = inkex.addNS('guide', 'sodipodi')
EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)

Wyświetl plik

@ -3,3 +3,4 @@ from cache import cache
from io import *
from inkscape import *
from paths import *
from string import *

Wyświetl plik

@ -0,0 +1,5 @@
def string_to_floats(string, delimiter=","):
"""Convert a string of delimiter-separated floats into a list of floats."""
floats = string.split(delimiter)
return [float(num) for num in floats]

Wyświetl plik

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>{% trans %}Lettering{% endtrans %}</name>
<id>org.inkstitch.lettering.{{ locale }}</id>
<dependency type="executable" location="extensions">inkstitch.py</dependency>
<dependency type="executable" location="extensions">inkex.py</dependency>
<param name="text" type="string" _gui-text="{% trans %}Text{% endtrans %}"></param>
<param name="extension" type="string" gui-hidden="true">lettering</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch">
{# L10N This is used for the submenu under Extensions -> Ink/Stitch. Translate this to your language's word for its language, e.g. "Español" for the spanish translation. #}
<submenu name="{% trans %}English{% endtrans %}" />
</submenu>
</effects-menu>
</effect>
<script>
<command reldir="extensions" interpreter="python">inkstitch.py</command>
</script>
</inkscape-extension>