kopia lustrzana https://github.com/inkstitch/inkstitch
317 wiersze
12 KiB
Python
317 wiersze
12 KiB
Python
# Authors: see git history
|
|
#
|
|
# 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 Group, errormsg
|
|
|
|
from ..commands import ensure_command_symbols
|
|
from ..i18n import _
|
|
from ..lettering import get_font_list
|
|
from ..marker import ensure_marker_symbols
|
|
from ..utils.settings import global_settings
|
|
|
|
|
|
class FontSampleFrame(wx.Frame):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.layer = kwargs.pop("layer")
|
|
wx.Frame.__init__(self, None, wx.ID_ANY, _("Font Sampling"), *args, **kwargs)
|
|
|
|
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)
|
|
|
|
notebook_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
self.notebook = wx.Notebook(self.main_panel, wx.ID_ANY)
|
|
notebook_sizer.Add(self.notebook, 1, wx.EXPAND, 0)
|
|
|
|
self.settings = wx.Panel(self.notebook, wx.ID_ANY)
|
|
self.notebook.AddPage(self.settings, _("Settings"))
|
|
|
|
# settings
|
|
settings_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
self.font_chooser = wx.adv.BitmapComboBox(self.settings, wx.ID_ANY, style=wx.CB_READONLY | wx.CB_SORT, size=((800, 20)))
|
|
self.font_chooser.Bind(wx.EVT_COMBOBOX, self.on_font_changed)
|
|
|
|
grid_settings_sizer = wx.FlexGridSizer(7, 2, 5, 5)
|
|
grid_settings_sizer.AddGrowableCol(1)
|
|
|
|
direction_label = wx.StaticText(self.settings, label=_("Stitch direction"))
|
|
self.direction = wx.ComboBox(self.settings, choices=[], style=wx.CB_READONLY)
|
|
scale_spinner_label = wx.StaticText(self.settings, label=_("Scale (%)"))
|
|
self.scale_spinner = wx.SpinCtrl(self.settings, wx.ID_ANY, min=0, max=1000, initial=100)
|
|
max_line_width_label = wx.StaticText(self.settings, label=_("Max. line width"))
|
|
self.max_line_width = wx.SpinCtrl(self.settings, wx.ID_ANY, min=0, max=5000, initial=180)
|
|
self.color_sort_label = wx.StaticText(self.settings, label=_("Color sort"))
|
|
self.color_sort_checkbox = wx.CheckBox(self.settings)
|
|
|
|
grid_settings_sizer.Add(direction_label, 0, wx.ALIGN_LEFT, 0)
|
|
grid_settings_sizer.Add(self.direction, 0, wx.EXPAND, 0)
|
|
grid_settings_sizer.Add(scale_spinner_label, 0, wx.ALIGN_LEFT, 0)
|
|
grid_settings_sizer.Add(self.scale_spinner, 0, wx.EXPAND, 0)
|
|
grid_settings_sizer.Add(max_line_width_label, 0, wx.ALIGN_LEFT, 0)
|
|
grid_settings_sizer.Add(self.max_line_width, 0, wx.EXPAND, 0)
|
|
grid_settings_sizer.Add(self.color_sort_label, 0, wx.ALIGN_LEFT, 0)
|
|
grid_settings_sizer.Add(self.color_sort_checkbox, 0, wx.EXPAND, 0)
|
|
|
|
apply_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
self.cancel_button = wx.Button(self.settings, label=_("Cancel"))
|
|
self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel)
|
|
self.apply_button = wx.Button(self.settings, label=_("Apply"))
|
|
self.apply_button.Bind(wx.EVT_BUTTON, self.apply)
|
|
apply_sizer.Add(self.cancel_button, 0, wx.RIGHT | wx.BOTTOM, 5)
|
|
apply_sizer.Add(self.apply_button, 0, wx.RIGHT | wx.BOTTOM, 10)
|
|
|
|
settings_sizer.Add(self.font_chooser, 1, wx.ALL | wx.EXPAND, 10)
|
|
settings_sizer.Add(grid_settings_sizer, 1, wx.ALL | wx.EXPAND, 10)
|
|
settings_sizer.Add(apply_sizer, 1, wx.ALIGN_RIGHT | wx.ALL, 10)
|
|
|
|
# help
|
|
self.help = wx.Panel(self.notebook, wx.ID_ANY)
|
|
self.notebook.AddPage(self.help, _("Help"))
|
|
|
|
help_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
help_text = wx.StaticText(
|
|
self.help,
|
|
wx.ID_ANY,
|
|
_(" This extension helps font creators to generate an output of every glyph from a selected font."),
|
|
style=wx.ALIGN_LEFT
|
|
)
|
|
help_text.Wrap(500)
|
|
help_sizer.Add(help_text, 0, wx.ALL, 8)
|
|
|
|
help_sizer.Add((20, 20), 0, 0, 0)
|
|
|
|
website_info = wx.StaticText(self.help, wx.ID_ANY, _("More information on our website:"))
|
|
help_sizer.Add(website_info, 0, wx.ALL, 8)
|
|
|
|
self.website_link = wx.adv.HyperlinkCtrl(
|
|
self.help,
|
|
wx.ID_ANY,
|
|
_("https://inkstitch.org/docs/font-tools/#font-sampling"),
|
|
_("https://inkstitch.org/docs/font-tools/#font-sampling")
|
|
)
|
|
help_sizer.Add(self.website_link, 0, wx.ALL, 8)
|
|
|
|
self.help.SetSizer(help_sizer)
|
|
self.settings.SetSizer(settings_sizer)
|
|
self.main_panel.SetSizer(notebook_sizer)
|
|
|
|
max_line_width = global_settings['font_sampling_max_line_width']
|
|
self.max_line_width.SetValue(max_line_width)
|
|
|
|
self.set_font_list()
|
|
select_font = global_settings['last_font']
|
|
self.font_chooser.SetValue(select_font)
|
|
self.on_font_changed()
|
|
|
|
self.SetSizeHints(notebook_sizer.CalcMin())
|
|
|
|
self.Layout()
|
|
|
|
def set_font_list(self):
|
|
self.fonts = {}
|
|
font_list = get_font_list()
|
|
for font in font_list:
|
|
self.fonts[font.marked_custom_font_name] = font
|
|
image = font.preview_image
|
|
if image is not None:
|
|
image = wx.Image(image)
|
|
# Windows requires all images to have the exact same size
|
|
image.Rescale(300, 20, quality=wx.IMAGE_QUALITY_HIGH)
|
|
self.font_chooser.Append(font.marked_custom_font_name, wx.Bitmap(image))
|
|
else:
|
|
self.font_chooser.Append(font.marked_custom_font_name)
|
|
|
|
def on_font_changed(self, event=None):
|
|
selected_font = self.font_chooser.GetValue()
|
|
if selected_font:
|
|
self.font = self.fonts[selected_font]
|
|
else:
|
|
first = list(self.fonts.values())[0].marked_custom_font_name
|
|
self.font = self.fonts[first]
|
|
self.font_chooser.SetValue(first)
|
|
global_settings['last_font'] = self.font.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 self.font.has_variants():
|
|
self.direction.Append(variant)
|
|
self.direction.SetSelection(0)
|
|
if self.font.sortable:
|
|
self.color_sort_label.Enable()
|
|
self.color_sort_checkbox.Enable()
|
|
else:
|
|
self.color_sort_label.Disable()
|
|
self.color_sort_checkbox.Disable()
|
|
|
|
def apply(self, event):
|
|
# apply scale to layer and extract for later use
|
|
self.layer.transform.add_scale(self.scale_spinner.GetValue() / 100)
|
|
scale = self.layer.transform.a
|
|
|
|
if self.font is None:
|
|
self.GetTopLevelParent().Close()
|
|
return
|
|
|
|
# parameters
|
|
line_width = self.max_line_width.GetValue()
|
|
direction = self.direction.GetValue()
|
|
|
|
global_settings['font_sampling_max_line_width'] = line_width
|
|
|
|
self.font._load_variants()
|
|
self.font_variant = self.font.variants[direction]
|
|
|
|
# setup lines of text
|
|
text = ''
|
|
width = 0
|
|
last_glyph = None
|
|
printed_warning = False
|
|
update_glyphlist_warning = _(
|
|
"The glyphlist for this font seems to be outdated.\n\n"
|
|
"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 variant glyph list length falls short if a single quote sign is available
|
|
# let's add it in the length comparison
|
|
if len(set(self.font.available_glyphs)) != len(self.font_variant.glyphs):
|
|
errormsg(update_glyphlist_warning)
|
|
printed_warning = True
|
|
|
|
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 - self.font.kerning_pairs.get(f'{last_glyph} {glyph}', 0)) * scale
|
|
width += width_to_add
|
|
|
|
try:
|
|
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
|
|
|
|
if width + width_to_add > line_width:
|
|
text += '\n'
|
|
width = 0
|
|
last_glyph = None
|
|
else:
|
|
last_glyph = glyph
|
|
text += glyph
|
|
width += width_to_add
|
|
|
|
self._render_text(text)
|
|
|
|
self.GetTopLevelParent().Close()
|
|
|
|
def sortable(self):
|
|
color_sort = self.color_sort_checkbox.GetValue()
|
|
if color_sort and not self.font.sortable:
|
|
color_sort = False
|
|
return color_sort
|
|
|
|
def duplicate_warning(self):
|
|
# warn about duplicated glyphs
|
|
if len(set(self.font.available_glyphs)) != len(self.font.available_glyphs):
|
|
duplicated_glyphs = " ".join(
|
|
[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)
|
|
|
|
ensure_command_symbols(group)
|
|
ensure_marker_symbols(group)
|
|
|
|
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()
|
|
|
|
|
|
class LetteringFontSampleApp(wx.App):
|
|
def __init__(self, layer):
|
|
self.layer = layer
|
|
super().__init__()
|
|
|
|
def OnInit(self):
|
|
self.frame = FontSampleFrame(layer=self.layer)
|
|
self.SetTopWindow(self.frame)
|
|
self.frame.Show()
|
|
return True
|