Files
kitty/kitty/fonts/freetype.py

290 lines
9.8 KiB
Python

#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import ctypes
import sys
import unicodedata
from collections import namedtuple
from functools import lru_cache
from itertools import chain
from threading import Lock
from kitty.fast_data_types import FT_PIXEL_MODE_GRAY, Face
from kitty.fonts.box_drawing import render_missing_glyph
from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth
from .fontconfig import (
FontNotFound, find_font_for_character, font_for_family, get_font_files
)
current_font_family = current_font_family_name = cff_size = cell_width = cell_height = baseline = None
CharTexture = underline_position = underline_thickness = None
alt_face_cache = {}
symbol_map = {}
def set_char_size(face, width=0, height=0, hres=0, vres=0):
face.set_char_size(width, height, hres, vres)
def load_char(font, face, text):
face.load_char(text, font.hinting, font.hintstyle)
def calc_cell_width(font, face):
ans = 0
for i in range(32, 128):
ch = chr(i)
load_char(font, face, ch)
m = face.glyph_metrics()
ans = max(ans, ceil_int(m.horiAdvance / 64))
return ans
@lru_cache(maxsize=2**10)
def font_for_char(char, bold=False, italic=False, allow_bitmaped_fonts=False):
if allow_bitmaped_fonts:
return find_font_for_character(
current_font_family_name,
char,
bold,
italic,
allow_bitmaped_fonts=True,
size_in_pts=cff_size['width'] / 64,
dpi=(cff_size['hres'] + cff_size['vres']) / 2
)
return find_font_for_character(
current_font_family_name, char, bold, italic
)
def install_symbol_map(val):
global symbol_map
symbol_map = {}
family_map = {f: font_for_family(f) for f in set(val.values())}
for ch, family in val.items():
symbol_map[ch] = family_map[family]
def font_units_to_pixels(x, units_per_em, size_in_pts, dpi):
return ceil_int(x * ((size_in_pts * dpi) / (72 * units_per_em)))
def set_font_family(opts, override_font_size=None):
global current_font_family, current_font_family_name, cff_size, cell_width, cell_height, CharTexture, baseline
global underline_position, underline_thickness
size_in_pts = override_font_size or opts.font_size
current_font_family = get_font_files(opts)
current_font_family_name = opts.font_family
dpi = get_logical_dpi()
cff_size = ceil_int(64 * size_in_pts)
cff_size = {
'width': cff_size,
'height': cff_size,
'hres': int(dpi[0]),
'vres': int(dpi[1])
}
install_symbol_map(opts.symbol_map)
for fobj in chain(current_font_family.values(), symbol_map.values()):
set_char_size(fobj.face, **cff_size)
face = current_font_family['regular'].face
cell_width = calc_cell_width(current_font_family['regular'], face)
cell_height = font_units_to_pixels(
face.height, face.units_per_EM, size_in_pts, dpi[1]
)
baseline = font_units_to_pixels(
face.ascender, face.units_per_EM, size_in_pts, dpi[1]
)
underline_position = min(
baseline - font_units_to_pixels(
face.underline_position, face.units_per_EM, size_in_pts, dpi[1]
), cell_height - 1
)
underline_thickness = font_units_to_pixels(
face.underline_thickness, face.units_per_EM, size_in_pts, dpi[1]
)
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
font_for_char.cache_clear()
alt_face_cache.clear()
return cell_width, cell_height
CharBitmap = namedtuple(
'CharBitmap', 'data bearingX bearingY advance rows columns'
)
freetype_lock = Lock()
def render_to_bitmap(font, face, text):
load_char(font, face, text)
bitmap = face.bitmap()
if bitmap.pixel_mode != FT_PIXEL_MODE_GRAY:
raise ValueError(
'FreeType rendered the glyph for {!r} with an unsupported pixel mode: {}'.
format(text, bitmap.pixel_mode)
)
return bitmap
def render_using_face(font, face, text, width, italic, bold):
bitmap = render_to_bitmap(font, face, text)
if width == 1 and bitmap.width > cell_width:
extra = bitmap.width - cell_width
if italic and extra < cell_width // 2:
bitmap = face.trim_to_width(bitmap, cell_width)
elif extra > max(2, 0.3 * cell_width) and face.is_scalable:
# rescale the font size so that the glyph is visible in a single
# cell and hope somebody updates libc's wcwidth
sz = cff_size.copy()
sz['width'] = int(sz['width'] * cell_width / bitmap.width)
# Preserve aspect ratio
sz['height'] = int(sz['height'] * cell_width / bitmap.width)
try:
set_char_size(face, **sz)
bitmap = render_to_bitmap(font, face, text)
finally:
set_char_size(face, **cff_size)
m = face.glyph_metrics()
return CharBitmap(
bitmap.buffer,
ceil_int(abs(m.horiBearingX) / 64),
ceil_int(abs(m.horiBearingY) / 64),
ceil_int(m.horiAdvance / 64), bitmap.rows, bitmap.width
)
def render_char(text, bold=False, italic=False, width=1):
key = 'regular'
if bold:
key = 'bi' if italic else 'bold'
elif italic:
key = 'italic'
ch = text[0]
with freetype_lock:
font = symbol_map.get(ch)
if font is None or not font.face.get_char_index(ch):
font = current_font_family.get(key) or current_font_family['regular']
face = font.face
if not face.get_char_index(ch):
try:
font = font_for_char(ch, bold, italic)
except FontNotFound:
font = font_for_char(
ch, bold, italic, allow_bitmaped_fonts=True
)
face = alt_face_cache.get(font)
if face is None:
face = alt_face_cache[font] = Face(font.face, font.index)
if face.is_scalable:
set_char_size(face, **cff_size)
else:
face = font.face
return render_using_face(font, face, text, width, italic, bold)
def place_char_in_cell(bitmap_char):
# We want the glyph to be positioned inside the cell based on the bearingX
# and bearingY values, making sure that it does not overflow the cell.
# Calculate column bounds
if bitmap_char.columns > cell_width:
src_start_column, dest_start_column = 0, 0
else:
src_start_column, dest_start_column = 0, bitmap_char.bearingX
extra = dest_start_column + bitmap_char.columns - cell_width
if extra > 0:
dest_start_column -= extra
column_count = min(
bitmap_char.columns - src_start_column, cell_width - dest_start_column
)
# Calculate row bounds, making sure the baseline is aligned with the cell
# baseline
if bitmap_char.bearingY > baseline:
src_start_row, dest_start_row = bitmap_char.bearingY - baseline, 0
else:
src_start_row, dest_start_row = 0, baseline - bitmap_char.bearingY
row_count = min(
bitmap_char.rows - src_start_row, cell_height - dest_start_row
)
return create_cell_buffer(
bitmap_char, src_start_row, dest_start_row, row_count,
src_start_column, dest_start_column, column_count
)
def split_char_bitmap(bitmap_char):
stride = bitmap_char.columns
extra = stride - cell_width
rows = bitmap_char.rows
first_buf = (ctypes.c_ubyte * (cell_width * rows))()
second_buf = (ctypes.c_ubyte * (extra * rows))()
src = bitmap_char.data
for r in range(rows):
soff, off = r * stride, r * cell_width
first_buf[off:off + cell_width] = src[soff:soff + cell_width]
off = r * extra
soff += cell_width
second_buf[off:off + extra] = src[soff:soff + extra]
first = bitmap_char._replace(data=first_buf, columns=cell_width)
second = bitmap_char._replace(data=second_buf, bearingX=0, columns=extra)
return first, second
def current_cell():
return CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position
@lru_cache(maxsize=8)
def missing_glyph(width):
w = cell_width * width
ans = bytearray(w * cell_height)
render_missing_glyph(ans, w, cell_height)
first, second = CharBitmap(ans, 0, 0, 0, cell_height, w), None
if width == 2:
first, second = split_char_bitmap(first)
second = create_cell_buffer(
second, 0, 0, second.rows, 0, 0, second.columns
)
first = create_cell_buffer(first, 0, 0, first.rows, 0, 0, first.columns)
return first, second
def render_cell(text=' ', bold=False, italic=False):
# TODO: Handle non-normalizable combining chars. Probably need to use
# harfbuzz for that
text = unicodedata.normalize('NFC', text)[0]
width = wcwidth(text)
try:
bitmap_char = render_char(text, bold, italic, width)
except FontNotFound as err:
safe_print('ERROR:', err, file=sys.stderr)
return missing_glyph(width)
second = None
if width == 2:
if bitmap_char.columns > cell_width:
bitmap_char, second = split_char_bitmap(bitmap_char)
second = place_char_in_cell(second)
else:
second = render_cell()[0]
first = place_char_in_cell(bitmap_char)
return first, second
def create_cell_buffer(
bitmap_char, src_start_row, dest_start_row, row_count, src_start_column,
dest_start_column, column_count
):
src = bitmap_char.data
src_stride = bitmap_char.columns
dest = CharTexture()
for r in range(row_count):
sr, dr = src_start_column + (
src_start_row + r
) * src_stride, dest_start_column + (dest_start_row + r) * cell_width
dest[dr:dr + column_count] = src[sr:sr + column_count]
return dest