From 809a833dc5e9fd1abb850ecb6e8d476da728fd52 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 24 Oct 2016 13:50:24 +0530 Subject: [PATCH] Code to render character cells --- freetype/__init__.py | 2 +- kitty/char_grid.py | 5 ++-- kitty/fonts.py | 70 ++++++++++++++++++++++++++++++++++++++++---- kitty/utils.py | 13 +++++--- 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/freetype/__init__.py b/freetype/__init__.py index aa29bf20d..e85ba9738 100644 --- a/freetype/__init__.py +++ b/freetype/__init__.py @@ -992,7 +992,7 @@ class Face( object ): FT_Done_Face( self._FT_Face ) def __repr__(self): - return 'Face({})'.format(self.family_name) + return 'Face({}, {})'.format(self.family_name, self.style_name) def attach_file( self, filename ): ''' diff --git a/kitty/char_grid.py b/kitty/char_grid.py index 6179c50bc..1c4bed566 100644 --- a/kitty/char_grid.py +++ b/kitty/char_grid.py @@ -5,7 +5,7 @@ from threading import Lock from .config import build_ansi_color_tables, to_color -from .fonts import load_font_family +from .fonts import set_font_family from OpenGL.arrays import ArrayDatatype from OpenGL.GL import ( @@ -51,8 +51,7 @@ class CharGrid: self.opts = opts self.default_bg = self.original_bg = opts.background self.default_fg = self.original_fg = opts.foreground - self.base_font_family = load_font_family(opts.font_family) - self.font_size = int(opts.font_size * 64) + self.base_font_family = set_font_family(opts.font_family) self.apply_clear_color() def on_resize(self, window, w, h): diff --git a/kitty/fonts.py b/kitty/fonts.py index dc9a81baa..2f69b5bda 100644 --- a/kitty/fonts.py +++ b/kitty/fonts.py @@ -3,11 +3,17 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal import subprocess +import unicodedata import re from collections import namedtuple from functools import lru_cache -from freetype import Face +from freetype import ( + Face, FT_LOAD_RENDER, FT_LOAD_TARGET_NORMAL, FT_LOAD_TARGET_LIGHT, + FT_LOAD_NO_HINTING, FT_PIXEL_MODE_GRAY +) + +from .utils import get_logical_dpi def escape_family_name(name): @@ -23,8 +29,8 @@ def get_font_information(q, bold=False, italic=False): q += ':bold=200' if italic: q += ':slant=100' - raw = subprocess.check_output(['fc-match', q, '-f', '%{file}\x31%{hinting}\x31%{hintstyle}']).decode('utf-8') - parts = raw.split('\x31') + raw = subprocess.check_output(['fc-match', q, '-f', '%{file}\x1e%{hinting}\x1e%{hintstyle}']).decode('utf-8') + parts = raw.split('\x1e') hintstyle, hinting = 1, 'True' if len(parts) == 3: path, hinting, hintstyle = parts @@ -48,6 +54,58 @@ def get_font_files(family): return ans -@lru_cache() -def load_font_family(r): - return get_font_files(r) +current_font_family = current_font_family_name = None + + +def set_font_family(family): + global current_font_family, current_font_family_name + if current_font_family_name != family: + current_font_family = get_font_files(family) + current_font_family_name = family + return current_font_family['regular'].face + +CharData = namedtuple('CharData', 'left width ascender descender') + + +def render_char(text, size_in_pts, bold=False, italic=False): + # TODO: Handle non-normalizable combining chars. Probably need to use + # harfbuzz for that + text = unicodedata.normalize('NFC', text)[0] + key = 'regular' + if bold: + key = 'bi' if italic else 'bold' + elif italic: + key = 'italic' + font = current_font_family.get(key) or current_font_family['regular'] + dpi = get_logical_dpi() + sz = int(64 * size_in_pts) + face = font.face + face.set_char_size(width=sz, height=sz, hres=dpi[0], vres=dpi[1]) + flags = FT_LOAD_RENDER + if font.hinting: + if font.hintstyle >= 3: + flags |= FT_LOAD_TARGET_NORMAL + elif 0 < font.hintstyle < 3: + flags |= FT_LOAD_TARGET_LIGHT + else: + flags |= FT_LOAD_NO_HINTING + face.load_char(text, flags) + bitmap = face.glyph.bitmap + if bitmap.pixel_mode != FT_PIXEL_MODE_GRAY: + raise ValueError('FreeType rendered the glyph with an unsupported pixel mode: {}'.format(bitmap.pixel_mode)) + width = bitmap.width + ascender = face.glyph.bitmap_top + descender = bitmap.rows - ascender + left = face.glyph.bitmap_left + + return bitmap.buffer, CharData(left, width, ascender, descender) + + +def test_rendering(text='M', sz=144, family='monospace'): + set_font_family(family) + buf, char_data = render_char(text, sz) + print(char_data) + from PIL import Image + img = Image.new('L', (char_data.width, char_data.ascender + char_data.descender)) + img.putdata(buf) + img.show() diff --git a/kitty/utils.py b/kitty/utils.py index da7e12f1a..83dc351ec 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -108,6 +108,14 @@ def sanitize_title(x): return re.sub(r'\s+', ' ', re.sub(r'[\0-\x19]', '', x)) +def get_logical_dpi(): + if not hasattr(get_logical_dpi, 'ans'): + raw = subprocess.check_output(['xdpyinfo']).decode('utf-8') + m = re.search(r'^\s*resolution:\s*(\d+)+x(\d+)', raw, flags=re.MULTILINE) + get_logical_dpi.ans = int(m.group(1)), int(m.group(2)) + return get_logical_dpi.ans + + def get_dpi(): if not hasattr(get_dpi, 'ans'): m = glfw.glfwGetPrimaryMonitor() @@ -115,8 +123,5 @@ def get_dpi(): vmode = glfw.glfwGetVideoMode(m) dpix = vmode.width / (width / 25.4) dpiy = vmode.height / (height / 25.4) - get_dpi.ans = {'physical': (dpix, dpiy)} - raw = subprocess.check_output(['xdpyinfo']).decode('utf-8') - m = re.search(r'^\s*resolution:\s*(\d+)+x(\d+)', raw, flags=re.MULTILINE) - get_dpi.ans['logical'] = (int(m.group(1)), int(m.group(2))) + get_dpi.ans = {'physical': (dpix, dpiy), 'logical': get_logical_dpi()} return get_dpi.ans