summaryrefslogtreecommitdiff
path: root/lib/lettering
diff options
context:
space:
mode:
authorLex Neva <lexelby@users.noreply.github.com>2018-11-14 20:23:06 -0500
committerGitHub <noreply@github.com>2018-11-14 20:23:06 -0500
commitf5c85183d9c874fca806917e50992daea4101496 (patch)
treea2450e2e37a7d94625a917240e78eadc939fd65b /lib/lettering
parent238ad843dd658de6c7afd5b8697c0e080b1cf965 (diff)
basic lettering (#344)
Can handle multiple lines of text and routes the stitching in alternating directions on each line.
Diffstat (limited to 'lib/lettering')
-rw-r--r--lib/lettering/__init__.py1
-rw-r--r--lib/lettering/font.py185
-rw-r--r--lib/lettering/font_variant.py86
-rw-r--r--lib/lettering/glyph.py86
4 files changed, 358 insertions, 0 deletions
diff --git a/lib/lettering/__init__.py b/lib/lettering/__init__.py
new file mode 100644
index 00000000..c6201223
--- /dev/null
+++ b/lib/lettering/__init__.py
@@ -0,0 +1 @@
+from font import Font \ No newline at end of file
diff --git a/lib/lettering/font.py b/lib/lettering/font.py
new file mode 100644
index 00000000..4a89df47
--- /dev/null
+++ b/lib/lettering/font.py
@@ -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"�")
+ 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)
diff --git a/lib/lettering/font_variant.py b/lib/lettering/font_variant.py
new file mode 100644
index 00000000..445946e2
--- /dev/null
+++ b/lib/lettering/font_variant.py
@@ -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
diff --git a/lib/lettering/glyph.py b/lib/lettering/glyph.py
new file mode 100644
index 00000000..bb1a971c
--- /dev/null
+++ b/lib/lettering/glyph.py
@@ -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)