Kreslíme font
Náš počítač dokáže vzít text, a vykreslit jej pomocí zvoleného fontu na obrazovku.
Jak to ale dělá? Jaká jsou vlastně ve fontu data, která umožní převést tu informaci,
že máme znak, např. A, a že ho máme vykreslit jako písmeno skupinu ploch?
Vezmeme Python, najdeme si font ve formátu ttf
a podíváme se na to.
Začněte tím, že si někde najdete font. Buď vezměte rozumný font z Vašeho počítače, nebo si třeba na stáhněte Roboto. Připomínám, že chceme ttf.
Dále si v Pythonu3 udělejte virtuální prostředí, a nainstaujte si do něj
fonttools
a wxPython
.
python3 -m venv FONTPLAY
cd FONTPLAY
. ./bin/activate
pip install fonttools wxPython
Čteme font
Projekt fonttools
, původně od Google, umí tak nějak všechno, co bychom si
mohli přát. Pro začátek jej použijeme k načtení fontu a získání informací
o glyphech, tedy znacích, které font obsahuje.
from fontTools.ttLib import TTFont
font = TTFont('Roboto-Regular.ttf')
print(font.getGlyphNames())
print('a' in font.getGlyphNames())
print('A' in font.getGlyphNames())
print('=' in font.getGlyphNames())
print('1' in font.getGlyphNames())
Ve výpisu jmen glyphů vidíte, že jména písmen tam nalezneme, ale číslice, nebo
speciální znaky (např =
) ne. Na druhou stranu, zkuste najít třeba glyph
se jménem one
. No nic, pro teď si vystačíme s písmeny.
glyph_set = font.getGlyphSet()
a = glyph_set["A"]
print(a.name, a.width, a.height, a.lsb, a.tsb)
Vlastnosti name
, width
, a height
jsou asi poměrně intuitivní.
LSB
a TSB
neboli Left/Top Side Bearing jsou hodnoty, které říkají,
kde se začíná kresba znaku. Neboli o kolik je znak posunot zleva
a shora. Zajímavé je, že u fontů často vídám, že Height a TSB
je None
.
A co tvar? Zajímavé je, že objekt a
má metodu draw
. Ta nás bude zajímat.
Kreslíme font
Metoda draw
očekává jako parametr pen
. To jako by naznačovalo, že
když jí předhodíme správný pen
, tak nám pomocí něj vykreslí znak. pen
je typu fontTools.pens.basePen
. Tak se , který je v modulu fontTools.pens.ttGlyphPen
.
A jakápak zajímavá pera tady máme:
WxPen
QtPen
SVGPathPen
ReportLabPen
CocoaPen
CairoPen
A spoustu dalších, u kterých ani není na první pohled vidět, k čemu jsou. My ale
chceme kreslit, pro začátek na obrazovku, tak začneme s WxPen
.
import wx
from fontTools.pens.wxPen import WxPen
from fontTools.ttLib import TTFont
class MyFrame(wx.Frame):
def __init__(self, ttfont: TTFont):
wx.Frame.__init__(self, None, -1, "Font painter")
self.font = ttfont
self.Bind(wx.EVT_PAINT, self.on_paint)
def on_paint(self, evt):
evt.Skip()
dc = wx.PaintDC(self)
gc = wx.GraphicsContext.Create(dc)
glyph = self.font.getGlyphSet()['A']
pen = WxPen(self.font.getGlyphSet())
glyph.draw(pen)
gc.SetBrush(wx.Brush('BLACK', wx.BRUSHSTYLE_SOLID))
gc.FillPath(pen.path)
def show_ui(font):
app = wx.App()
frame = MyFrame(font)
frame.Show()
frame.Maximize(True)
app.MainLoop()
if "__main__" == __name__:
show_ui(TTFont("Roboto-Regular.ttf"))
Pro nás zajímavá část je ta, kde vytváříme pen
a kde jej používáme.
# Vytvoříme pen pro sadu glyphů
pen = WxPen(self.font.getGlyphSet())
# Vykreslíme pomocí toho pera námi vybraný glyph
glyph.draw(pen)
# Vykreslíme pomocí GraphicsContextu
gc.SetBrush(wx.Brush('BLACK', wx.BRUSHSTYLE_SOLID))
gc.FillPath(pen.path)
Jestli se ptáte: Proč dostává pen sadu glyphů? Tak to proto, že některé fonty mají tzv. composite glyphs, tedy takové, které jsou složené z více glyphů. Například znak
Č
je složený zC
aháčku
. A při vykreslování se potřebuje dostat na ty podglyphy.
Ale teď: Jestliže jste tohle úspěšně spustili, vidíte asi velké A, tak velké, že se nevejde na obrazovku, a navíc je vzhůru nohama. Vzhůru nohama je snadné opravit. Co s velikostí? Musíme si zjistit nějakou základní velikost ve fontu, ke které všechno vztáhneme.
Font má v sobě uloženou tzv. units per em, což je počet jednotek fontu (tedy to je jednička v rámci souřadnic jednotlivých křivek), na jedno EM, což je zase typografická jednotka. (Jedno EM se dá naivně popsat jako šířka písmene velké M. Není to přesně takhle, ale nám to bude stačit.)
Takže kresbu posuneme, převrátíme a zmenšíme, třeba tak, aby baseline byla uprostřed obrazovky.
height = self.GetSize()[1]
units_per_em = self.font['head'].unitsPerEm
scale = height / 4 / units_per_em
m = gc.CreateMatrix()
# Posuneme se na střed obrazovky
m.Translate(0, height // 2)
# Převrátíme a zmenšíme
m.Scale(scale, -scale)
pen.path.Transform(m)
Což nám vygeneruje tohle:
Více písmenek vedle sebe
No dobře. Vykreslili jsme si písmeno A. Ale co když vedle něj budeme chtít ještě písmeno I? No, zkusíme to.
def draw_glyph(self, gc, glyph_name, m, brush):
glyph = self.font.getGlyphSet()[glyph_name]
pen = WxPen(self.font.getGlyphSet())
glyph.draw(pen)
pen.path.Transform(m)
gc.SetBrush(brush)
gc.FillPath(pen.path)
return glyph
def on_paint(self, evt):
evt.Skip()
dc = wx.PaintDC(self)
gc = wx.GraphicsContext.Create(dc)
height = self.GetSize()[1]
units_per_em = self.font['head'].unitsPerEm
scale = height / 4 / units_per_em
m = gc.CreateMatrix()
m.Translate(0, height // 2)
m.Scale(scale, -scale)
self.draw_glyph(gc, 'A', m, wx.BLACK_BRUSH)
self.draw_glyph(gc, 'I', m, wx.RED_BRUSH)
Tímto jsme je ovšem nakreslili přes sebe. Takže se musíme posunout, vždy o šířku glyphu. Nějak takhle:
glyph = self.draw_glyph(gc, 'A', m, wx.BLACK_BRUSH)
m.Translate(glyph.width, 0)
glyph = self.draw_glyph(gc, 'I', m, wx.RED_BRUSH)
m.Translate(glyph.width, 0)
Nebo ještě lépe:
for name in "AIai":
glyph = self.draw_glyph(gc, name, m, wx.BLACK_BRUSH)
m.Translate(glyph.width, 0)
Hledáme unicode
Mapování unicode kódu na jméno glyphu najdeme v cmap
tabulce.
Asi takhle:
cmap = self.font['cmap'].getBestCmap()
cmap.get(ord('A')) == 'A'
cmap.get(ord('1')) == 'one'
Takže když budeme chtít tisknout i něco jiného, něž písmena, můžeme třeba takto:
cmap = self.font['cmap'].getBestCmap()
for unicode in "AIai&32\u06CF87Š":
glyph_name = cmap.get(ord(unicode))
if not glyph_name:
glyph_name = ".notdef"
glyph = self.draw_glyph(gc, glyph_name, m, wx.BLACK_BRUSH)
m.Translate(glyph.width, 0)
Všimněte si toho .notdef
, které se použije, pokud ten unicode ve fontu
nebyl nalezen. S fontem Roboto dostavu něco takového:
Co dál
Určitě jste slyšeli o kerningu. Na něj se zkusíme podívat příště.
Komentáře byly zrušeny
V EU teď máme složitou situaci s Cookies. Na komentáře jsem používal jistou službu třetí strany. Ta však používá Cookies poměrně, ehm, benevolentně. Tak jsem se rozhodl komentáře zrušit. Pokud chcete, můžete mi napsat přímo