inkstitch/lib/lettering/font.py

317 wiersze
11 KiB
Python
Czysty Wina Historia

# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import json
import os
from copy import deepcopy
import inkex
from ..elements import nodes_to_elements
from ..exceptions import InkstitchException
from ..extensions.lettering_custom_font_dir import get_custom_font_dir
from ..i18n import _, get_languages
from ..stitches.auto_satin import auto_satin
from ..svg.tags import INKSCAPE_LABEL, SVG_PATH_TAG
from ..utils import Point
from .font_variant import FontVariant
class FontError(InkstitchException):
pass
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)
def localized_font_metadata(name, default=None):
def getter(self):
# If the font contains a localized version of the attribute, use it.
for language in get_languages():
attr = "%s_%s" % (name, language)
if attr in self.metadata:
return self.metadata.get(attr)
if name in self.metadata:
# This may be a font packaged with Ink/Stitch, in which case the
# text will have been sent to CrowdIn for community translation.
# Try to fetch the translated version.
original_metadata = self.metadata.get(name)
localized_metadata = ""
if original_metadata != "":
localized_metadata = _(original_metadata)
return localized_metadata
else:
return default
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.metadata = {}
self.license = None
self.variants = {}
self._load_metadata()
self._load_license()
def _load_metadata(self):
try:
with open(os.path.join(self.path, "font.json"), encoding="utf-8") as metadata_file:
self.metadata = json.load(metadata_file)
except IOError:
pass
def _load_license(self):
try:
with open(os.path.join(self.path, "LICENSE"), encoding="utf-8") as license_file:
self.license = license_file.read()
except IOError:
pass
def _load_variants(self):
if not 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 = localized_font_metadata('name', '')
description = localized_font_metadata('description', '')
letter_case = font_metadata('letter_case', '')
default_glyph = font_metadata('default_glyph', "<EFBFBD>")
leading = font_metadata('leading', 100)
kerning_pairs = font_metadata('kerning_pairs', {})
auto_satin = font_metadata('auto_satin', True)
min_scale = font_metadata('min_scale', 1.0)
max_scale = font_metadata('max_scale', 1.0)
# use values from SVG Font, example:
# <font horiz-adv-x="45" ... <glyph .... horiz-adv-x="49" glyph-name="A" /> ... <hkern ... k="3"g1="A" g2="B" /> .... />
# Example font.json : "horiz_adv_x": {"A":49},
horiz_adv_x = font_metadata('horiz_adv_x', {})
# Example font.json : "horiz_adv_x_default" : 45,
horiz_adv_x_default = font_metadata('horiz_adv_x_default')
# Define by <glyph glyph-name="space" unicode=" " horiz-adv-x="22" />, Example font.json : "horiz_adv_x_space":22,
word_spacing = font_metadata('horiz_adv_x_space', 20)
reversible = font_metadata('reversible', True)
@property
def id(self):
return os.path.basename(self.path)
@property
def default_variant(self):
# Set default variant to any existing variant if default font file is missing
default_variant = font_metadata('default_variant', FontVariant.LEFT_TO_RIGHT)
font_variants = self.has_variants()
if default_variant not in font_variants and len(font_variants) > 0:
default_variant = font_variants[0]
return default_variant
@property
def preview_image(self):
preview_image_path = os.path.join(self.path, "preview.png")
if os.path.isfile(preview_image_path):
return preview_image_path
return None
def has_variants(self):
# returns available variants
font_variants = []
for variant in FontVariant.VARIANT_TYPES:
if os.path.isfile(os.path.join(self.path, "%s.svg" % variant)):
font_variants.append(variant)
if not font_variants:
raise FontError(_("The font '%s' has no variants.") % self.name)
return font_variants
@property
def marked_custom_font_id(self):
if not self.is_custom_font():
return self.id
else:
return self.id + '*'
@property
def marked_custom_font_name(self):
if not self.is_custom_font():
return self.name
else:
return self.name + '*'
def is_custom_font(self):
return get_custom_font_dir() in self.path
def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim=False):
"""Render text into an SVG group element."""
self._load_variants()
if variant is None:
variant = self.default_variant
if back_and_forth and self.reversible:
glyph_sets = [self.get_variant(variant), self.get_variant(FontVariant.reversed_variant(variant))]
else:
glyph_sets = [self.get_variant(variant)] * 2
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 back_and_forth and self.reversible and i % 2 == 1:
letter_group[:] = reversed(letter_group)
destination_group.append(letter_group)
position.x = 0
position.y += self.leading
if self.auto_satin and len(destination_group) > 0:
self._apply_auto_satin(destination_group, trim)
# make sure font stroke styles have always a similar look
for element in destination_group.iterdescendants(SVG_PATH_TAG):
dash_array = ""
stroke_width = ""
style = inkex.styles.Style(element.get('style'))
if style.get('fill') == 'none':
stroke_width = ";stroke-width:1px"
if style.get('stroke-width'):
style.pop('stroke-width')
if style.get('stroke-dasharray') and style.get('stroke-dasharray') != 'none':
stroke_width = ";stroke-width:0.5px"
dash_array = ";stroke-dasharray:3, 1"
element.set('style', '%s%s%s' % (style.to_str(), stroke_width, dash_array))
return destination_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.Group(attrib={
INKSCAPE_LABEL: line
})
last_character = None
for character in line:
if self.letter_case == "upper":
character = character.upper()
elif self.letter_case == "lower":
character = character.lower()
glyph = glyph_set[character]
if character == " " or (glyph is None and self.default_glyph == " "):
position.x += self.word_spacing
last_character = None
else:
if glyph is None:
glyph = 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.
"""
# Concerning min_x: I add it before moving the letter because it is to
# take into account the margin in the drawing of the letter. With respect
# to point 0 the letter can start at 5 or -5. The letters have a defined
# place in the drawing that's important.
# Then to calculate the position of x for the next letter I have to remove
# the min_x margin because the horizontal adv is calculated from point 0 of the drawing.
node = deepcopy(glyph.node)
if last_character is not None:
position.x += glyph.min_x - self.kerning_pairs.get(last_character + character, 0)
transform = "translate(%s, %s)" % position.as_tuple()
node.set('transform', transform)
horiz_adv_x_default = self.horiz_adv_x_default
if horiz_adv_x_default is None:
horiz_adv_x_default = glyph.width + glyph.min_x
position.x += self.horiz_adv_x.get(character, horiz_adv_x_default) - glyph.min_x
return node
def _apply_auto_satin(self, group, trim):
"""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, trim=trim)