kopia lustrzana https://github.com/inkstitch/inkstitch
Lettering typographic features (#3466)
* add svg font to layers extension which saves glyph annotations into the glyph name --------- Co-authored-by: Claudinepull/3491/head
rodzic
8f1f68a1db
commit
af6cdc442b
|
@ -40,6 +40,7 @@ from .lettering_force_lock_stitches import LetteringForceLockStitches
|
|||
from .lettering_generate_json import LetteringGenerateJson
|
||||
from .lettering_remove_kerning import LetteringRemoveKerning
|
||||
from .lettering_set_color_sort_index import LetteringSetColorSortIndex
|
||||
from .lettering_svg_font_to_layers import LetteringSvgFontToLayers
|
||||
from .letters_to_font import LettersToFont
|
||||
from .object_commands import ObjectCommands
|
||||
from .object_commands_toggle_visibility import ObjectCommandsToggleVisibility
|
||||
|
@ -112,6 +113,7 @@ __all__ = extensions = [About,
|
|||
LetteringGenerateJson,
|
||||
LetteringRemoveKerning,
|
||||
LetteringSetColorSortIndex,
|
||||
LetteringSvgFontToLayers,
|
||||
LettersToFont,
|
||||
ObjectCommands,
|
||||
ObjectCommandsToggleVisibility,
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright (C) 2011 Felipe Correa da Silva Sanches <juca@members.fsf.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
#
|
||||
# Adapted for the inkstitch lettering module to allow glyph annotations for characters
|
||||
# in specific positions or settings. Changes: see git history
|
||||
"""Extension for converting svg fonts to layers"""
|
||||
|
||||
from inkex import Layer, PathElement, errormsg
|
||||
|
||||
from .base import InkstitchExtension
|
||||
|
||||
|
||||
class LetteringSvgFontToLayers(InkstitchExtension):
|
||||
"""Convert an svg font to layers"""
|
||||
|
||||
def add_arguments(self, pars):
|
||||
pars.add_argument(
|
||||
"--count",
|
||||
type=int,
|
||||
default=30,
|
||||
help="Stop making layers after this number of glyphs.",
|
||||
)
|
||||
|
||||
def flip_cordinate_system(self, elem, emsize, baseline):
|
||||
"""Scale and translate the element's path, returns the path object"""
|
||||
path = elem.path
|
||||
path.scale(1, -1, inplace=True)
|
||||
path.translate(0, int(emsize) - int(baseline), inplace=True)
|
||||
return path
|
||||
|
||||
def effect(self):
|
||||
# Current code only reads the first svgfont instance
|
||||
font = self.svg.defs.findone("svg:font")
|
||||
if font is None:
|
||||
return errormsg("There are no svg fonts")
|
||||
# setwidth = font.get("horiz-adv-x")
|
||||
baseline = font.get("horiz-origin-y")
|
||||
if baseline is None:
|
||||
baseline = 0
|
||||
|
||||
guidebase = self.svg.viewbox_height - baseline
|
||||
|
||||
fontface = font.findone("svg:font-face")
|
||||
|
||||
emsize = fontface.get("units-per-em")
|
||||
|
||||
# TODO: should we guarantee that <svg:font horiz-adv-x> equals <svg:font-face units-per-em> ?
|
||||
caps = int(fontface.get("cap-height", 0))
|
||||
xheight = int(fontface.get("x-height", 0))
|
||||
ascender = int(fontface.get("ascent", 0))
|
||||
descender = int(fontface.get("descent", 0))
|
||||
|
||||
self.svg.set("width", emsize)
|
||||
self.svg.namedview.add_guide(guidebase, True, "baseline")
|
||||
self.svg.namedview.add_guide(guidebase - ascender, True, "ascender")
|
||||
self.svg.namedview.add_guide(guidebase - caps, True, "caps")
|
||||
self.svg.namedview.add_guide(guidebase - xheight, True, "xheight")
|
||||
self.svg.namedview.add_guide(guidebase + descender, True, "decender")
|
||||
|
||||
count = 0
|
||||
for glyph in font.findall("svg:glyph"):
|
||||
hide_layer = count != 0
|
||||
self.convert_glyph_to_layer(glyph, emsize, baseline, hide_layer=hide_layer)
|
||||
count += 1
|
||||
if count >= self.options.count:
|
||||
break
|
||||
|
||||
def convert_glyph_to_layer(self, glyph, emsize, baseline, hide_layer):
|
||||
unicode_char = glyph.get("unicode")
|
||||
|
||||
glyph_name = glyph.get("glyph-name").split('.')
|
||||
if unicode_char is None:
|
||||
if len(glyph_name) == 2:
|
||||
unicode_char = glyph_name[0]
|
||||
else:
|
||||
return
|
||||
|
||||
typographic_feature = ''
|
||||
if len(glyph_name) == 2:
|
||||
typographic_feature = glyph_name[1]
|
||||
else:
|
||||
arabic_form = glyph.get('arabic-form', None)
|
||||
if arabic_form is not None and len(arabic_form) > 4:
|
||||
typographic_feature = arabic_form[:4]
|
||||
if typographic_feature:
|
||||
typographic_feature = f".{typographic_feature}"
|
||||
|
||||
layer = self.svg.add(Layer.new(f"GlyphLayer-{unicode_char}{typographic_feature}"))
|
||||
|
||||
# glyph layers (except the first one) are innitially hidden
|
||||
if hide_layer:
|
||||
layer.style["display"] = "none"
|
||||
|
||||
# Using curve description in d attribute of svg:glyph
|
||||
path = layer.add(PathElement())
|
||||
path.path = self.flip_cordinate_system(glyph, emsize, baseline)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
LetteringSvgFontToLayers().run()
|
|
@ -237,16 +237,29 @@ class LetteringEditJsonPanel(wx.Panel):
|
|||
self.horiz_adv_x = self.font.horiz_adv_x
|
||||
|
||||
kerning_combinations = combinations_with_replacement(self.glyphs, 2)
|
||||
self.kerning_combinations = [''.join(combination) for combination in kerning_combinations]
|
||||
self.kerning_combinations.extend([combination[1] + combination[0] for combination in self.kerning_combinations])
|
||||
self.kerning_combinations = [' '.join(combination) for combination in kerning_combinations]
|
||||
self.kerning_combinations.extend([f'{combination[1]} {combination[0]}' for combination in kerning_combinations])
|
||||
self.kerning_combinations = list(set(self.kerning_combinations))
|
||||
self.kerning_combinations.sort()
|
||||
|
||||
self.update_legacy_kerning_pairs()
|
||||
self.update_settings()
|
||||
self.update_kerning_list()
|
||||
self.update_glyph_list()
|
||||
self.update_preview()
|
||||
|
||||
def update_legacy_kerning_pairs(self):
|
||||
new_list = defaultdict(list)
|
||||
for kerning_pair, value in self.kerning_pairs.items():
|
||||
if " " in kerning_pair:
|
||||
# legacy kerning pairs do not use a space
|
||||
return
|
||||
if len(kerning_pair) < 2:
|
||||
continue
|
||||
pair = f'{kerning_pair[0]} {kerning_pair[1]}'
|
||||
new_list[pair] = value
|
||||
self.kerning_pairs = new_list
|
||||
|
||||
def update_settings(self):
|
||||
# reset font_meta
|
||||
self.font_meta = defaultdict(list)
|
||||
|
@ -305,7 +318,8 @@ class LetteringEditJsonPanel(wx.Panel):
|
|||
kerning_list.AppendColumn("New kerning", width=wx.LIST_AUTOSIZE_USEHEADER)
|
||||
for kerning_pair in self.kerning_combinations:
|
||||
if self.font_meta['text_direction'] == 'rtl':
|
||||
kerning_pair = kerning_pair[::-1]
|
||||
pair = kerning_pair.split()
|
||||
kerning_pair = ' '.join(pair[::-1])
|
||||
index = kerning_list.InsertItem(kerning_list.GetItemCount(), kerning_pair)
|
||||
# kerning_list.SetItem(index, 0, kerning_pair)
|
||||
kerning_list.SetItem(index, 1, str(self.kerning_pairs.get(kerning_pair, 0.0)))
|
||||
|
@ -373,43 +387,59 @@ class LetteringEditJsonPanel(wx.Panel):
|
|||
if self.last_notebook_selection == 3:
|
||||
text = self.get_active_glyph()
|
||||
else:
|
||||
text = self.get_active_kerning_pair()
|
||||
kerning = self.get_active_kerning_pair()
|
||||
kerning = kerning.split()
|
||||
text = ''.join(kerning)
|
||||
if self.font_meta['text_direction'] == 'rtl':
|
||||
text = ''.join(kerning[::-1])
|
||||
if not text:
|
||||
return
|
||||
|
||||
text = self.text_before + text + self.text_after
|
||||
if self.font_meta['text_direction'] == 'rtl':
|
||||
text = text[::-1]
|
||||
|
||||
self._render_text(text)
|
||||
position_x = self._render_text(self.text_before, 0, True)
|
||||
position_x = self._render_text(text, position_x, False)
|
||||
self._render_text(self.text_after, position_x, True)
|
||||
|
||||
if self.default_variant.variant == FontVariant.RIGHT_TO_LEFT:
|
||||
self.layer[:] = reversed(self.layer)
|
||||
for group in self.layer:
|
||||
group[:] = reversed(group)
|
||||
|
||||
def _render_text(self, text):
|
||||
last_character = None
|
||||
position_x = 0
|
||||
for character in text:
|
||||
glyph = self.default_variant[character]
|
||||
if character == " " or (glyph is None and self.font_meta['default_glyph'] == " "):
|
||||
position_x += self.font_meta['horiz_adv_x_space']
|
||||
last_character = None
|
||||
else:
|
||||
if glyph is None:
|
||||
glyph = self.default_variant[self.font_meta['default_glyph']]
|
||||
def _render_text(self, text, position_x, use_character_position):
|
||||
words = text.split()
|
||||
for i, word in enumerate(words):
|
||||
glyphs = []
|
||||
skip = []
|
||||
previous_is_binding = False
|
||||
for i, character in enumerate(word):
|
||||
if i in skip:
|
||||
continue
|
||||
if use_character_position:
|
||||
glyph, glyph_len, previous_is_binding = self.default_variant.get_next_glyph(word, i, previous_is_binding)
|
||||
else:
|
||||
glyph, glyph_len = self.default_variant.get_glyph(character, word[i:])
|
||||
glyphs.append(glyph)
|
||||
skip = list(range(i, i+glyph_len))
|
||||
|
||||
if glyph is not None:
|
||||
position_x, last_character = self._render_glyph(glyph, position_x, character, last_character)
|
||||
last_character = None
|
||||
for glyph in glyphs:
|
||||
if glyph is None:
|
||||
position_x += self.font_meta['horiz_adv_x_space']
|
||||
last_character = None
|
||||
continue
|
||||
|
||||
position_x = self._render_glyph(glyph, position_x, glyph.name, last_character)
|
||||
last_character = glyph.name
|
||||
position_x += self.font_meta['horiz_adv_x_space']
|
||||
position_x -= self.font_meta['horiz_adv_x_space']
|
||||
return position_x
|
||||
|
||||
def _render_glyph(self, glyph, position_x, character, last_character):
|
||||
node = deepcopy(glyph.node)
|
||||
if last_character is not None:
|
||||
if self.font_meta['text_direction'] != 'rtl':
|
||||
position_x += glyph.min_x - self.kerning_pairs.get(last_character + character, 0)
|
||||
position_x += glyph.min_x - self.kerning_pairs.get(f'{last_character} {character}', 0)
|
||||
else:
|
||||
position_x += glyph.min_x - self.kerning_pairs.get(character + last_character, 0)
|
||||
position_x += glyph.min_x - self.kerning_pairs.get(f'{character} {last_character}', 0)
|
||||
|
||||
transform = f"translate({position_x}, 0)"
|
||||
node.set('transform', transform)
|
||||
|
@ -427,7 +457,7 @@ class LetteringEditJsonPanel(wx.Panel):
|
|||
# because this is not unique it will be overwritten by inkscape when inserted into the document
|
||||
node.set("id", "glyph")
|
||||
self.layer.add(node)
|
||||
return position_x, character
|
||||
return position_x
|
||||
|
||||
def render_stitch_plan(self):
|
||||
stitch_groups = []
|
||||
|
|
|
@ -97,7 +97,7 @@ class FontInfo(wx.Panel):
|
|||
)
|
||||
|
||||
default_variant_label = wx.StaticText(self, label=_("Default Variant"))
|
||||
self.default_variant = wx.Choice(self, choices=[_("→"), _("←"), _("↓"), ("↑")])
|
||||
self.default_variant = wx.Choice(self, choices=["→", "←", "↓", "↑"])
|
||||
self.default_variant.Bind(wx.EVT_CHOICE, self.parent.on_default_variant_change)
|
||||
|
||||
text_direction_label = wx.StaticText(self, label=_("Text direction"))
|
||||
|
|
|
@ -15,7 +15,7 @@ from ...i18n import _
|
|||
from ...lettering import FontError, get_font_list
|
||||
from ...lettering.categories import FONT_CATEGORIES
|
||||
from ...stitch_plan import stitch_groups_to_stitch_plan
|
||||
from ...svg.tags import INKSCAPE_LABEL, INKSTITCH_LETTERING
|
||||
from ...svg.tags import INKSTITCH_LETTERING
|
||||
from ...utils import DotDict, cache
|
||||
from ...utils.threading import ExitThread, check_stop_flag
|
||||
from .. import PresetsPanel, PreviewRenderer, info_dialog
|
||||
|
@ -292,15 +292,11 @@ class LetteringPanel(wx.Panel):
|
|||
|
||||
del self.group[:]
|
||||
|
||||
destination_group = inkex.Group(attrib={
|
||||
# L10N The user has chosen to scale the text by some percentage
|
||||
# (50%, 200%, etc). If you need to use the percentage symbol,
|
||||
# make sure to double it (%%).
|
||||
INKSCAPE_LABEL: _("Text scale") + f' {self.settings.scale}%'
|
||||
})
|
||||
destination_group = inkex.Group()
|
||||
self.group.append(destination_group)
|
||||
|
||||
font = self.fonts.get(self.options_panel.font_chooser.GetValue(), self.default_font)
|
||||
destination_group.label = f"{font.name} {_('scale')} {self.settings.scale}%"
|
||||
try:
|
||||
font.render_text(
|
||||
self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth,
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
# Copyright (c) 2023 Authors
|
||||
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import wx
|
||||
import wx.adv
|
||||
from inkex import errormsg
|
||||
from inkex import Group, errormsg
|
||||
|
||||
from ..i18n import _
|
||||
from ..lettering import get_font_list
|
||||
|
@ -20,6 +22,8 @@ class FontSampleFrame(wx.Frame):
|
|||
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
|
||||
|
||||
self.fonts = None
|
||||
self.font = None
|
||||
self.font_variant = None
|
||||
|
||||
self.main_panel = wx.Panel(self, wx.ID_ANY)
|
||||
|
||||
|
@ -124,14 +128,14 @@ class FontSampleFrame(wx.Frame):
|
|||
self.font_chooser.Append(font.marked_custom_font_name)
|
||||
|
||||
def on_font_changed(self, event=None):
|
||||
font = self.fonts.get(self.font_chooser.GetValue(), list(self.fonts.values())[0].marked_custom_font_name)
|
||||
self.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100))
|
||||
self.font = self.fonts.get(self.font_chooser.GetValue(), list(self.fonts.values())[0].marked_custom_font_name)
|
||||
self.scale_spinner.SetRange(int(self.font.min_scale * 100), int(self.font.max_scale * 100))
|
||||
# font._load_variants()
|
||||
self.direction.Clear()
|
||||
for variant in font.has_variants():
|
||||
for variant in self.font.has_variants():
|
||||
self.direction.Append(variant)
|
||||
self.direction.SetSelection(0)
|
||||
if font.sortable:
|
||||
if self.font.sortable:
|
||||
self.color_sort_label.Enable()
|
||||
self.color_sort_checkbox.Enable()
|
||||
else:
|
||||
|
@ -143,19 +147,16 @@ class FontSampleFrame(wx.Frame):
|
|||
self.layer.transform.add_scale(self.scale_spinner.GetValue() / 100)
|
||||
scale = self.layer.transform.a
|
||||
|
||||
# set font
|
||||
font = self.fonts.get(self.font_chooser.GetValue())
|
||||
if font is None:
|
||||
if self.font is None:
|
||||
self.GetTopLevelParent().Close()
|
||||
return
|
||||
|
||||
# parameters
|
||||
line_width = self.max_line_width.GetValue()
|
||||
direction = self.direction.GetValue()
|
||||
color_sort = self.sortable(font)
|
||||
|
||||
font._load_variants()
|
||||
font_variant = font.variants[direction]
|
||||
self.font._load_variants()
|
||||
self.font_variant = self.font.variants[direction]
|
||||
|
||||
# setup lines of text
|
||||
text = ''
|
||||
|
@ -164,33 +165,32 @@ class FontSampleFrame(wx.Frame):
|
|||
printed_warning = False
|
||||
update_glyphlist_warning = _(
|
||||
"The glyphlist for this font seems to be outdated.\n\n"
|
||||
"Please update the glyph list for %s:\n"
|
||||
"open Extensions > Ink/Stitch > Font Management > Edit JSON "
|
||||
"select this font and apply. No other changes necessary."
|
||||
% font.marked_custom_font_name
|
||||
)
|
||||
"Please update the glyph list for {font_name}:\n"
|
||||
"* Open Extensions > Ink/Stitch > Font Management > Edit JSON\n"
|
||||
"* Select this font and apply."
|
||||
).format(font_name=self.font.marked_custom_font_name)
|
||||
|
||||
self.duplicate_warning(font)
|
||||
self.duplicate_warning()
|
||||
|
||||
# font variant glyph list length falls short if a single quote sign is available
|
||||
# let's add it in the length comparison
|
||||
if len(set(font.available_glyphs)) != len(font_variant.glyphs):
|
||||
if len(set(self.font.available_glyphs)) != len(self.font_variant.glyphs):
|
||||
errormsg(update_glyphlist_warning)
|
||||
printed_warning = True
|
||||
|
||||
for glyph in font.available_glyphs:
|
||||
glyph_obj = font_variant[glyph]
|
||||
for glyph in self.font.available_glyphs:
|
||||
glyph_obj = self.font_variant[glyph]
|
||||
if glyph_obj is None:
|
||||
if not printed_warning:
|
||||
errormsg(update_glyphlist_warning)
|
||||
printed_warning = True
|
||||
continue
|
||||
if last_glyph is not None:
|
||||
width_to_add = (glyph_obj.min_x - font.kerning_pairs.get(last_glyph + glyph, 0)) * scale
|
||||
width_to_add = (glyph_obj.min_x - self.font.kerning_pairs.get(f'{last_glyph} {glyph}', 0)) * scale
|
||||
width += width_to_add
|
||||
|
||||
try:
|
||||
width_to_add = (font.horiz_adv_x.get(glyph, font.horiz_adv_x_default) - glyph_obj.min_x) * scale
|
||||
width_to_add = (self.font.horiz_adv_x.get(glyph, self.font.horiz_adv_x_default) - glyph_obj.min_x) * scale
|
||||
except TypeError:
|
||||
width_to_add = glyph_obj.width
|
||||
|
||||
|
@ -203,24 +203,84 @@ class FontSampleFrame(wx.Frame):
|
|||
text += glyph
|
||||
width += width_to_add
|
||||
|
||||
# render text and close
|
||||
font.render_text(text, self.layer, variant=direction, back_and_forth=False, color_sort=color_sort)
|
||||
self._render_text(text)
|
||||
|
||||
self.GetTopLevelParent().Close()
|
||||
|
||||
def sortable(self, font):
|
||||
def sortable(self):
|
||||
color_sort = self.color_sort_checkbox.GetValue()
|
||||
if color_sort and not font.sortable:
|
||||
if color_sort and not self.font.sortable:
|
||||
color_sort = False
|
||||
return color_sort
|
||||
|
||||
def duplicate_warning(self, font):
|
||||
def duplicate_warning(self):
|
||||
# warn about duplicated glyphs
|
||||
if len(set(font.available_glyphs)) != len(font.available_glyphs):
|
||||
if len(set(self.font.available_glyphs)) != len(self.font.available_glyphs):
|
||||
duplicated_glyphs = " ".join(
|
||||
[glyph for glyph in set(font.available_glyphs) if font.available_glyphs.count(glyph) > 1]
|
||||
[glyph for glyph in set(self.font.available_glyphs) if self.font.available_glyphs.count(glyph) > 1]
|
||||
)
|
||||
errormsg(_("Found duplicated glyphs in font file: {duplicated_glyphs}").format(duplicated_glyphs=duplicated_glyphs))
|
||||
|
||||
def _render_text(self, text):
|
||||
lines = text.splitlines()
|
||||
position = {'x': 0, 'y': 0}
|
||||
for line in lines:
|
||||
group = Group()
|
||||
group.label = line
|
||||
group.set("inkstitch:letter-group", "line")
|
||||
glyphs = []
|
||||
skip = []
|
||||
for i, character in enumerate(line):
|
||||
if i in skip:
|
||||
continue
|
||||
default_variant = self.font.variants[self.font.json_default_variant]
|
||||
glyph, glyph_len = default_variant.get_glyph(character, line[i:])
|
||||
glyphs.append(glyph)
|
||||
skip = list(range(i, i+glyph_len))
|
||||
|
||||
last_character = None
|
||||
for glyph in glyphs:
|
||||
if glyph is None:
|
||||
position['x'] += self.font.horiz_adv_x_space
|
||||
last_character = None
|
||||
continue
|
||||
|
||||
position = self._render_glyph(group, glyph, position, glyph.name, last_character)
|
||||
last_character = glyph.name
|
||||
self.layer.add(group)
|
||||
position['x'] = 0
|
||||
position['y'] += self.font.leading
|
||||
|
||||
if self.sortable():
|
||||
self.font.do_color_sort(self.layer, 1)
|
||||
|
||||
def _render_glyph(self, group, glyph, position, character, last_character):
|
||||
node = deepcopy(glyph.node)
|
||||
if last_character is not None:
|
||||
if self.font.text_direction != 'rtl':
|
||||
position['x'] += glyph.min_x - self.font.kerning_pairs.get(f'{last_character} {character}', 0)
|
||||
else:
|
||||
position['x'] += glyph.min_x - self.font.kerning_pairs.get(f'{character} {last_character}', 0)
|
||||
|
||||
transform = f"translate({position['x']}, {position['y']})"
|
||||
node.set('transform', transform)
|
||||
|
||||
horiz_adv_x_default = self.font.horiz_adv_x_default
|
||||
if horiz_adv_x_default is None:
|
||||
horiz_adv_x_default = glyph.width + glyph.min_x
|
||||
|
||||
position['x'] += self.font.horiz_adv_x.get(character, horiz_adv_x_default) - glyph.min_x
|
||||
|
||||
self.font._update_commands(node, glyph)
|
||||
self.font._update_clips(group, node, glyph)
|
||||
|
||||
# this is used to recognize a glyph layer later in the process
|
||||
# because this is not unique it will be overwritten by inkscape when inserted into the document
|
||||
node.set("id", "glyph")
|
||||
node.set("inkstitch:letter-group", "glyph")
|
||||
group.add(node)
|
||||
return position
|
||||
|
||||
def cancel(self, event):
|
||||
self.GetTopLevelParent().Close()
|
||||
|
||||
|
|
|
@ -324,41 +324,70 @@ class Font(object):
|
|||
|
||||
group = inkex.Group()
|
||||
group.label = line
|
||||
if self.text_direction == 'rtl':
|
||||
group.label = line[::-1]
|
||||
group.set("inkstitch:letter-group", "line")
|
||||
last_character = None
|
||||
|
||||
words = line.split(" ")
|
||||
for word in words:
|
||||
|
||||
word_group = inkex.Group()
|
||||
word_group.label = word
|
||||
label = word
|
||||
if self.text_direction == 'rtl':
|
||||
label = word[::-1]
|
||||
word_group.label = label
|
||||
word_group.set("inkstitch:letter-group", "word")
|
||||
|
||||
for character in word:
|
||||
if self.letter_case == "upper":
|
||||
character = character.upper()
|
||||
elif self.letter_case == "lower":
|
||||
character = character.lower()
|
||||
if self.text_direction == 'rtl':
|
||||
glyphs = self._get_word_glyphs(glyph_set, word[::-1])
|
||||
glyphs = glyphs[::-1]
|
||||
else:
|
||||
glyphs = self._get_word_glyphs(glyph_set, word)
|
||||
|
||||
glyph = glyph_set[character]
|
||||
|
||||
if glyph is None and self.default_glyph == " ":
|
||||
last_character = None
|
||||
for glyph in glyphs:
|
||||
if glyph is None:
|
||||
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(destination_group, glyph, position, character, last_character)
|
||||
word_group.append(node)
|
||||
|
||||
last_character = character
|
||||
position.x += self.word_spacing
|
||||
last_character = None
|
||||
continue
|
||||
node = self._render_glyph(destination_group, glyph, position, glyph.name, last_character)
|
||||
word_group.append(node)
|
||||
last_character = glyph.name
|
||||
group.append(word_group)
|
||||
|
||||
position.x += self.word_spacing
|
||||
return group
|
||||
|
||||
def _get_word_glyphs(self, glyph_set, word):
|
||||
glyphs = []
|
||||
skip = []
|
||||
previous_is_binding = True
|
||||
|
||||
for i, character in enumerate(word):
|
||||
if i in skip:
|
||||
continue
|
||||
|
||||
# forced letter case
|
||||
if self.letter_case == "upper":
|
||||
character = character.upper()
|
||||
elif self.letter_case == "lower":
|
||||
character = character.lower()
|
||||
|
||||
glyph, glyph_len, binding = glyph_set.get_next_glyph(word, i, previous_is_binding)
|
||||
previous_is_binding = binding
|
||||
|
||||
skip = list(range(i, i+glyph_len))
|
||||
|
||||
if glyph is None and self.default_glyph == " ":
|
||||
glyphs.append(None)
|
||||
else:
|
||||
if glyph is None:
|
||||
glyphs.append(glyph_set[self.default_glyph])
|
||||
if glyph is not None:
|
||||
glyphs.append(glyph)
|
||||
|
||||
return glyphs
|
||||
|
||||
def _render_glyph(self, destination_group, glyph, position, character, last_character):
|
||||
"""Render a single glyph.
|
||||
|
||||
|
@ -383,9 +412,17 @@ class Font(object):
|
|||
node = deepcopy(glyph.node)
|
||||
if last_character is not None:
|
||||
if self.text_direction != "rtl":
|
||||
position.x += glyph.min_x - self.kerning_pairs.get(last_character + character, 0)
|
||||
kerning = self.kerning_pairs.get(f'{last_character} {character}', None)
|
||||
if kerning is None:
|
||||
# legacy kerning without space
|
||||
kerning = self.kerning_pairs.get(last_character + character, 0)
|
||||
position.x += glyph.min_x - kerning
|
||||
else:
|
||||
position.x += glyph.min_x - self.kerning_pairs.get(character + last_character, 0)
|
||||
kerning = self.kerning_pairs.get(f'{character} {last_character}', None)
|
||||
if kerning is None:
|
||||
# legacy kerning without space
|
||||
kerning = self.kerning_pairs.get(character + last_character, 0)
|
||||
position.x += glyph.min_x - kerning
|
||||
|
||||
transform = "translate(%s, %s)" % position.as_tuple()
|
||||
node.set('transform', transform)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
# Copyright (c) 2010 Authors
|
||||
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from fontTools.agl import toUnicode
|
||||
from inkex import NSS
|
||||
from lxml import etree
|
||||
|
@ -21,11 +23,35 @@ class FontFileInfo(object):
|
|||
# horiz_adv_x defines the width of specific letters (distance to next letter)
|
||||
def horiz_adv_x(self):
|
||||
# In XPath 2.0 we could use ".//svg:glyph/(@unicode|@horiz-adv-x)"
|
||||
xpath = ".//svg:glyph[@unicode and @horiz-adv-x]/@*[name()='unicode' or name()='horiz-adv-x']"
|
||||
hax = self.svg.xpath(xpath, namespaces=NSS)
|
||||
if len(hax) == 0:
|
||||
xpath = ".//svg:glyph" # [@unicode and @horiz-adv-x and @glyph-name]/@*[name()='unicode' or name()='horiz-adv-x' or name()='glyph-name']"
|
||||
glyph_definitions = self.svg.xpath(xpath, namespaces=NSS)
|
||||
if len(glyph_definitions) == 0:
|
||||
return {}
|
||||
return dict(zip(hax[0::2], [float(x) for x in hax[1::2]]))
|
||||
|
||||
horiz_adv_x_dict = defaultdict(list)
|
||||
for glyph in glyph_definitions:
|
||||
unicode_char = glyph.get('unicode', None)
|
||||
if unicode_char is None:
|
||||
continue
|
||||
hax = glyph.get('horiz-adv-x', None)
|
||||
if hax is None:
|
||||
continue
|
||||
else:
|
||||
hax = float(hax)
|
||||
|
||||
glyph_name = glyph.get('glyph-name', None)
|
||||
if glyph_name is not None:
|
||||
glyph_name = glyph_name.split('.')
|
||||
if len(glyph_name) == 2:
|
||||
typographic_feature = glyph_name[1]
|
||||
unicode_char += f'.{typographic_feature}'
|
||||
else:
|
||||
arabic_form = glyph.get('arabic-form', None)
|
||||
if arabic_form is not None and len(arabic_form) > 4:
|
||||
typographic_feature = arabic_form[:4]
|
||||
unicode_char += f'.{typographic_feature}'
|
||||
horiz_adv_x_dict[unicode_char] = hax
|
||||
return horiz_adv_x_dict
|
||||
|
||||
# kerning (specific distances of two specified letters)
|
||||
def hkern(self):
|
||||
|
@ -54,7 +80,7 @@ class FontFileInfo(object):
|
|||
for first, second, key in kern_list:
|
||||
for f in first:
|
||||
for s in second:
|
||||
hkern[f+s] = key
|
||||
hkern[f'{f} {s}'] = key
|
||||
return hkern
|
||||
|
||||
def split_glyph_list(self, glyph):
|
||||
|
@ -62,7 +88,7 @@ class FontFileInfo(object):
|
|||
if len(glyph) > 1:
|
||||
# glyph names need to be converted to unicode
|
||||
# we need to take into account, that there can be more than one first/second letter in the very same hkern element
|
||||
# in this case they will be commas separated and each first letter needs to be combined with each next letter
|
||||
# in this case they will be comma separated and each first letter needs to be combined with each next letter
|
||||
# e.g. <hkern g1="A,Agrave,Aacute,Acircumflex,Atilde,Adieresis,Amacron,Abreve,Aogonek" g2="T,Tcaron" k="5" />
|
||||
glyph_names = glyph.split(",")
|
||||
for glyph_name in glyph_names:
|
||||
|
@ -73,11 +99,12 @@ class FontFileInfo(object):
|
|||
separators = [".", "_"]
|
||||
used_separator = False
|
||||
for separator in separators:
|
||||
if used_separator:
|
||||
continue
|
||||
glyph_with_separator = glyph_name.split(separator)
|
||||
if len(glyph_with_separator) == 2:
|
||||
glyphs.append("%s%s%s" % (toUnicode(glyph_with_separator[0]), separator, glyph_with_separator[1]))
|
||||
glyphs.append(f"{toUnicode(glyph_with_separator[0])}{separator}{glyph_with_separator[1]}")
|
||||
used_separator = True
|
||||
continue
|
||||
# there is no extra separator
|
||||
if not used_separator:
|
||||
glyphs.append(toUnicode(glyph_name))
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from unicodedata import normalize
|
||||
|
||||
import inkex
|
||||
|
||||
|
@ -73,7 +74,7 @@ class FontVariant(object):
|
|||
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]
|
||||
glyph_name = normalize('NFKC', layer.attrib[INKSCAPE_LABEL])
|
||||
try:
|
||||
self.glyphs[glyph_name] = Glyph(layer)
|
||||
except (AttributeError, ValueError):
|
||||
|
@ -134,6 +135,93 @@ class FontVariant(object):
|
|||
|
||||
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 isbinding(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 ispunctuation(self, character):
|
||||
# punctuation sign are not considered as part of the word. They onnly have one shape
|
||||
punctuation_signs = ['؟', '،', '.', ',', ';', '.', '!', ':', '؛']
|
||||
normalized_punctuation_signs = [normalize('NFKC', letter) for letter in punctuation_signs]
|
||||
return (character in normalized_punctuation_signs)
|
||||
|
||||
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 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 self.ispunctuation(word[last_char_index]):
|
||||
last_char_index = last_char_index - 1
|
||||
while self.ispunctuation(word[first_char_index]):
|
||||
first_char_index = first_char_index + 1
|
||||
|
||||
# first glyph is eithher isol or init depending wether it is also the last glyph of the actual word
|
||||
if starting == first_char_index:
|
||||
if not self.isbinding(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.isbinding(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.isbinding(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):
|
||||
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]
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
from unicodedata import normalize
|
||||
|
||||
from inkex import paths, transforms, units
|
||||
|
||||
|
@ -36,6 +37,9 @@ class Glyph(object):
|
|||
this Glyph. Nested groups are allowed.
|
||||
"""
|
||||
|
||||
self.name = group.label
|
||||
if len(self.name) > 11:
|
||||
self.name = normalize('NFKC', self.name[11:])
|
||||
self._process_baseline(group.getroottree().getroot())
|
||||
self.clips = self._process_clips(group)
|
||||
self.node = self._process_group(group)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Convert SVG Font to Glyph Layers</name>
|
||||
<id>org.{{ id_inkstitch }}.lettering_svg_font_to_layers</id>
|
||||
<param name="extension" type="string" gui-hidden="true">lettering_svg_font_to_layers</param>
|
||||
<param name="count" type="int" min="1" max="65535" gui-text="Stop after">100</param>
|
||||
|
||||
<effect needs-live-preview="false">
|
||||
<object-type>all</object-type>
|
||||
<icon>{{ icon_path }}inx/font_management.svg</icon>
|
||||
<menu-tip>Converts a svg font to glyph layers</menu-tip>
|
||||
<effects-menu>
|
||||
<submenu name="{{ menu_inkstitch }}" translatable="no">
|
||||
<submenu name="Font Management" />
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
{{ command_tag | safe }}
|
||||
</script>
|
||||
</inkscape-extension>
|
Ładowanie…
Reference in New Issue