Files
kitty/kitty/fonts/render.py
Kovid Goyal 75e1b30fc1 DRYer
2017-11-09 16:55:44 +05:30

235 lines
8.0 KiB
Python

#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import ctypes
from collections import namedtuple
from math import ceil, floor, pi, sin, sqrt
from kitty.config import defaults
from kitty.constants import isosx
from kitty.fast_data_types import (
Screen, send_prerendered_sprites, set_font, set_font_size,
set_send_sprite_to_gpu, sprite_map_set_limits, test_render_line
)
from kitty.fonts.box_drawing import render_box_char, render_missing_glyph
from kitty.utils import get_logical_dpi
if isosx:
from .core_text import get_font_files, font_for_text, face_from_font, font_for_family, save_medium_face
else:
from .fontconfig import get_font_files, font_for_text, face_from_font, font_for_family
def save_medium_face(f):
pass
def create_face(font):
s = set_font_family.state
return face_from_font(font, s.pt_sz, s.xdpi, s.ydpi)
def create_symbol_map(opts):
val = opts.symbol_map
family_map = {}
faces = []
for family in val.values():
if family not in family_map:
font = font_for_family(family)
o = create_face(font)
family_map[family] = len(faces)
faces.append((o, font.bold, font.italic))
sm = tuple((a, b, family_map[f]) for (a, b), f in val.items())
return sm, tuple(faces)
FontState = namedtuple(
'FontState',
'family pt_sz xdpi ydpi cell_width cell_height baseline underline_position underline_thickness'
)
def get_fallback_font(text, bold, italic):
state = set_font_family.state
font = font_for_text(
text, state.family, state.pt_sz, state.xdpi, state.ydpi, bold,
italic
)
return None if font is None else create_face(font)
def set_font_family(opts=None, override_font_size=None, override_dpi=None):
opts = opts or defaults
sz = override_font_size or opts.font_size
xdpi, ydpi = get_logical_dpi(override_dpi)
set_font_family.state = FontState('', sz, xdpi, ydpi, 0, 0, 0, 0, 0)
font_map = get_font_files(opts)
faces = [create_face(font_map['medium'])]
save_medium_face(faces[0])
for k in 'bold italic bi'.split():
if k in font_map:
faces.append(create_face(font_map[k]))
sm, sfaces = create_symbol_map(opts)
cell_width, cell_height, baseline, underline_position, underline_thickness = set_font(
get_fallback_font, render_box_drawing, sm, sfaces, sz, xdpi, ydpi, *faces
)
set_font_family.state = FontState(
opts.font_family, sz, xdpi, ydpi, cell_width, cell_height, baseline,
underline_position, underline_thickness
)
prerender()
return cell_width, cell_height
def resize_fonts(new_sz, xdpi=None, ydpi=None):
s = set_font_family.state
xdpi = xdpi or s.xdpi
ydpi = ydpi or s.ydpi
cell_width, cell_height, baseline, underline_position, underline_thickness = set_font_size(
new_sz, xdpi, ydpi
)
set_font_family.state = FontState(
s.family, new_sz, xdpi, ydpi, cell_width, cell_height, baseline,
underline_position, underline_thickness
)
prerender()
def add_line(buf, cell_width, position, thickness, cell_height):
y = position - thickness // 2
while thickness:
thickness -= 1
offset = cell_width * y
for x in range(cell_width):
buf[offset + x] = 255
y += 1
def add_curl(buf, cell_width, position, thickness, cell_height):
xfactor = 2.0 * pi / cell_width
yfactor = thickness
def clamp_y(y):
return max(0, min(int(y), cell_height - 1))
def clamp_x(x):
return max(0, min(int(x), cell_width - 1))
def add_intensity(x, y, distance):
buf[cell_width * y + x] = min(
255, buf[cell_width * y + x] + int(255 * (1 - distance))
)
for x_exact in range(cell_width):
y_exact = yfactor * sin(x_exact * xfactor) + position
y_below = clamp_y(floor(y_exact))
y_above = clamp_y(ceil(y_exact))
x_before, x_after = map(clamp_x, (x_exact - 1, x_exact + 1))
for x in {x_before, x_exact, x_after}:
for y in {y_below, y_above}:
dist = sqrt((x - x_exact)**2 + (y - y_exact)**2) / 2
add_intensity(x, y, dist)
def render_special(underline=0, strikethrough=False, missing=False):
s = set_font_family.state
cell_width, cell_height, baseline = s.cell_width, s.cell_height, s.baseline
underline_position, underline_thickness = s.underline_position, s.underline_thickness
underline_position = min(underline_position, cell_height - underline_thickness)
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
ans = CharTexture if missing else CharTexture()
def dl(f, *a):
f(ans, cell_width, *a)
if underline:
t = underline_thickness
if underline == 2:
t = max(1, min(cell_height - underline_position - 1, t))
dl(
add_curl
if underline == 2 else add_line, underline_position, t, cell_height
)
if strikethrough:
pos = int(0.65 * baseline)
dl(add_line, pos, underline_thickness, cell_height)
if missing:
buf = bytearray(cell_width * cell_height)
render_missing_glyph(buf, cell_width, cell_height)
ans = CharTexture.from_buffer(buf)
return ans
def prerender():
# Pre-render the special blank, underline and strikethrough cells
cells = render_special(1), render_special(2), render_special(0, True), render_special(missing=True)
if send_prerendered_sprites(*map(ctypes.addressof, cells)) != len(cells):
raise RuntimeError('Your GPU has too small a max texture size')
def render_box_drawing(codepoint):
s = set_font_family.state
cell_width, cell_height = s.cell_width, s.cell_height
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
buf = render_box_char(
chr(codepoint), CharTexture(), cell_width, cell_height
)
return ctypes.addressof(buf), buf
def setup_for_testing(family='monospace', size=11.0, dpi=96.0):
opts = defaults._replace(font_family=family)
sprites = {}
def send_to_gpu(x, y, z, data):
sprites[(x, y, z)] = data
sprite_map_set_limits(100000, 100)
set_send_sprite_to_gpu(send_to_gpu)
cell_width, cell_height = set_font_family(opts, override_dpi=(dpi, dpi), override_font_size=size)
return sprites, cell_width, cell_height
def render_string(text, family='monospace', size=11.0, dpi=96.0):
try:
sprites, cell_width, cell_height = setup_for_testing(family, size, dpi)
s = Screen(None, 1, len(text)*2)
line = s.line(0)
s.draw(text)
test_render_line(line)
finally:
set_send_sprite_to_gpu(None)
cells = []
for i in range(s.columns):
sp = line.sprite_at(i)
if sp != (0, 0, 0):
cells.append(sprites[sp])
return cell_width, cell_height, cells
def test_render_string(text='Hello, world!', family='monospace', size=144.0, dpi=96.0):
from tempfile import NamedTemporaryFile
from kitty.fast_data_types import concat_cells, current_fonts
from kitty.icat import detect_support, show
if not detect_support():
raise SystemExit('Your terminal does not support the graphics protocol')
cell_width, cell_height, cells = render_string(text, family, size, dpi)
rgb_data = concat_cells(cell_width, cell_height, tuple(cells))
with NamedTemporaryFile(delete=False) as f:
f.write(rgb_data)
cf = current_fonts()
fonts = [cf['medium'].display_name()]
fonts.extend(f.display_name() for f in cf['fallback'])
print('Rendered string {} below, with fonts: {}'.format(text, ', '.join(fonts)))
show(f.name, cell_width * len(cells), cell_height, 24)
print('\n')
def showcase():
test_render_string('He\u0347\u0305llo\u0341, w\u0302or\u0306l\u0354d!', family='Noto Sans Mono')
test_render_string('你好,世界', family='Noto Sans Mono')
test_render_string('|\U0001F929|\U0001F921|\U0001F91f|', family='Noto Sans Mono')
test_render_string('A=>>B!=C', family='Fira Code Medium')