inkstitch/lib/lettering/font_variant.py

241 wiersze
9.8 KiB
Python
Czysty Wina Historia

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 os
from collections import defaultdict
from unicodedata import normalize, category
import inkex
from ..svg.tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SVG_GROUP_TAG,
SVG_PATH_TAG, SVG_USE_TAG)
from ..update import update_inkstitch_document
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 = ""
RIGHT_TO_LEFT = ""
TOP_TO_BOTTOM = ""
BOTTOM_TO_TOP = ""
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):
# If the font variant file does not exist, this constructor will
# raise an exception. The caller should catch it and decide
# what to do.
self.path = font_path
self.variant = variant
self.default_glyph = default_glyph
self.glyphs = {}
self._load_glyphs()
def _load_glyphs(self):
variant_file_paths = self._get_variant_file_paths()
for svg_path in variant_file_paths:
document = inkex.load_svg(svg_path)
update_inkstitch_document(document, warn_unversioned=False)
svg = document.getroot()
svg = self._apply_transforms(svg)
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 = normalize('NFKC', layer.attrib[INKSCAPE_LABEL])
try:
self.glyphs[glyph_name] = Glyph(layer)
except (AttributeError, ValueError):
pass
def _get_variant_file_paths(self):
file_paths = []
direct_path = os.path.join(self.path, "%s.svg" % self.variant)
if os.path.isfile(direct_path):
file_paths.append(direct_path)
elif os.path.isdir(os.path.join(self.path, "%s" % self.variant)):
path = os.path.join(self.path, "%s" % self.variant)
file_paths.extend([os.path.join(path, svg) for svg in os.listdir(path) if svg.endswith('.svg')])
return file_paths
def _clean_group(self, group):
# We'll repurpose the layer as a container group labelled with the
# glyph.
del group.attrib[INKSCAPE_GROUPMODE]
# The layer may be marked invisible, so we'll clear the 'display'
# style and presentation attribute.
group.style.pop('display', None)
group.attrib.pop('display', None)
def _apply_transforms(self, svg):
self.clip_transforms = defaultdict(list)
# apply transforms to paths and use tags
for element in svg.iterdescendants((SVG_PATH_TAG, SVG_USE_TAG, SVG_GROUP_TAG)):
transform = element.composed_transform()
if element.clip is not None:
self.clip_transforms[element.clip] = element.composed_transform()
if element.tag == SVG_GROUP_TAG:
continue
if element.tag == SVG_PATH_TAG:
path = element.path.transform(transform)
element.set_path(path)
element.attrib.pop("transform", None)
elif element.tag == SVG_USE_TAG:
oldx = element.get('x', 0)
oldy = element.get('y', 0)
newx, newy = transform.apply_to_point((oldx, oldy))
element.set('x', newx)
element.set('y', newy)
element.attrib.pop("transform", None)
for clip, transform in self.clip_transforms.items():
for element in clip.iterdescendants():
if element.tag == SVG_PATH_TAG:
path = element.path.transform(transform)
element.set_path(path)
element.attrib.pop("transform", None)
# remove transforms after they have been applied
for group in svg.iterdescendants(SVG_GROUP_TAG):
group.attrib.pop('transform', None)
return svg
def glyphs_start_with(self, character):
glyph_selection = [glyph_name for glyph_name, glyph_layer in self.glyphs.items() if glyph_name.startswith(character)]
return sorted(glyph_selection, key=lambda glyph: (len(glyph.split('.')[0]), len(glyph)), reverse=True)
def is_binding(self, character):
# after a non binding letter a letter can only be in isol or fina shape.
# binding glyph only have two shapes, isol and fina
non_binding_char = ['ا', 'أ', '', 'آ', 'ٱ', 'د', 'ذ', 'ر', 'ز', 'و', 'ؤ']
normalized_non_binding_char = [normalize('NFKC', letter) for letter in non_binding_char]
return not (character in normalized_non_binding_char)
def is_mark(self, character):
# this category includes all the combining diacritics.
return (category(character)[0] == 'M')
def is_letter(self, character):
return (category(character)[0] == 'L')
def get_glyph(self, character, word):
"""
Returns the glyph for the given character, searching for combined glyphs first
This expects glyph annotations to be within the given word, for example: a.init
Returns glyph node and length of the glyph name
"""
glyph_selection = self.glyphs_start_with(character)
for glyph in glyph_selection:
if word.startswith(glyph):
return self.glyphs[glyph], len(glyph)
return self.glyphs.get(self.default_glyph, None), 1
def get_next_glyph_shape(self, word, starting, ending, previous_is_binding):
# in arabic each letter (or ligature) may have up to 4 different shapes, hence 4 glyphs
# this computes the shape of the glyph that represents word[starting:ending+1]
# punctuation or a combining accent is not really part of the word
# they may appear at begining or end of words
# computes where the actual word begins and ends up
last_char_index = len(word)-1
first_char_index = 0
while not self.is_letter(word[last_char_index]):
last_char_index = last_char_index - 1
while not self.is_letter(word[first_char_index]):
first_char_index = first_char_index + 1
# first glyph is either isol or init depending if it is also the last glyph of the actual word
if starting == first_char_index:
if not self.is_binding(word[ending]) or len(word) == 1:
shape = 'isol'
else:
shape = 'init'
# last glyph is final if previous is binding, isol otherwise
# a non binding glyph behaves like the last glyph
elif ending == last_char_index or not self.is_binding(word[ending]):
if previous_is_binding:
shape = 'fina'
else:
shape = 'isol'
# in the middle of the actual word, the shape of a glyph is medi if previous glyph is bendinng, init otherwise
elif previous_is_binding:
shape = 'medi'
else:
shape = 'init'
return shape
def get_next_glyph(self, word, i, previous_is_binding):
# search for the glyph of word that starts at i,taking into acount the previous glyph binding status
# find all the glyphs in tthe font that start with first letter of the glyph
glyph_selection = self.glyphs_start_with(word[i])
# find the longest glyph that match
for glyph in glyph_selection:
glyph_name = glyph.split('.')
if len(glyph_name) == 2 and glyph_name[1] in ['isol', 'init', 'medi', 'fina']:
is_binding = self.is_binding(glyph_name[0][-1])
if len(word) < i + len(glyph_name[0]):
continue
shape = self.get_next_glyph_shape(word, i, i + len(glyph_name[0]) - 1, previous_is_binding)
if glyph_name[1] == shape and word[i:].startswith(glyph_name[0]):
return self.glyphs[glyph], len(glyph_name[0]), is_binding
elif word[i:].startswith(glyph):
if self.is_mark(word[i]):
return self.glyphs[glyph], len(glyph), previous_is_binding
else:
return self.glyphs[glyph], len(glyph), True
# nothing was found
return self.glyphs.get(self.default_glyph, None), 1, True
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