kopia lustrzana https://github.com/inkstitch/inkstitch
241 wiersze
9.8 KiB
Python
241 wiersze
9.8 KiB
Python
# 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
|