2018-11-15 01:23:06 +00:00
|
|
|
|
# -*- 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
|
|
|
|
|
|
2018-12-16 01:21:41 +00:00
|
|
|
|
if self.auto_satin and len(line_group) > 0:
|
2018-11-15 01:23:06 +00:00
|
|
|
|
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)
|