From 9aa1d74f83c56cbfd334c554485e9adf67ac4eba Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 8 Feb 2017 09:32:15 +0530 Subject: [PATCH] Linux: Don't crash when fontconfig is unable to find a font for a character. Instead render it as a missing glyph --- kitty/fonts/box_drawing.py | 11 +++++++++- kitty/fonts/fontconfig.py | 41 +++++++++++++++++++++++++++++++------- kitty/fonts/freetype.py | 31 +++++++++++++++++++++------- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/kitty/fonts/box_drawing.py b/kitty/fonts/box_drawing.py index fea1ff8e1..4d83859c8 100644 --- a/kitty/fonts/box_drawing.py +++ b/kitty/fonts/box_drawing.py @@ -26,7 +26,7 @@ def draw_hline(buf, width, x1, x2, y, level): def draw_vline(buf, width, y1, y2, x, level): - ' Draw a horizontal line between [y1, y2) centered at x with the thickness given by level ' + ' Draw a vertical line between [y1, y2) centered at x with the thickness given by level ' sz = thickness(level=level, horizontal=True) start = x - sz // 2 for x in range(start, start + sz): @@ -345,6 +345,15 @@ def render_box_char(ch, buf, width, height): return buf +def render_missing_glyph(buf, width, height): + hgap = thickness(level=0, horizontal=True) + 1 + vgap = thickness(level=0, horizontal=False) + 1 + draw_hline(buf, width, hgap, width - hgap + 1, vgap, 0) + draw_hline(buf, width, hgap, width - hgap + 1, height - vgap, 0) + draw_vline(buf, width, vgap, height - vgap + 1, hgap, 0) + draw_vline(buf, width, vgap, height - vgap + 1, width - hgap, 0) + + def join_rows(width, height, rows): import ctypes ans = (ctypes.c_ubyte * (width * height * len(rows)))() diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index 69dc2b75a..1019df231 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -2,10 +2,12 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal +import os import re import subprocess from collections import namedtuple from functools import lru_cache + from kitty.fast_data_types import Face @@ -16,9 +18,15 @@ def escape_family_name(name): Font = namedtuple('Font', 'face hinting hintstyle bold italic') +class FontNotFound(ValueError): + pass + + def get_font(query, bold, italic): query += ':scalable=true:outline=true' - raw = subprocess.check_output(['fc-match', query, '-f', '%{file}\x1e%{hinting}\x1e%{hintstyle}']).decode('utf-8') + raw = subprocess.check_output( + ['fc-match', query, '-f', '%{file}\x1e%{hinting}\x1e%{hintstyle}' + ]).decode('utf-8') parts = raw.split('\x1e') hintstyle, hinting = 1, 'True' if len(parts) == 3: @@ -32,12 +40,21 @@ def get_font(query, bold, italic): @lru_cache(maxsize=4096) def find_font_for_character(family, char, bold=False, italic=False): - q = escape_family_name(family) + ':charset={}'.format(hex(ord(char[0]))[2:]) + q = escape_family_name(family) + ':charset={}'.format( + hex(ord(char[0]))[2:]) if bold: q += ':weight=200' if italic: q += ':slant=100' - return get_font(q, bold, italic) + try: + ans = get_font(q, bold, italic) + except subprocess.CalledProcessError as err: + raise FontNotFound( + 'Failed to find font for character U+{:X}, error from fontconfig: {}'. + format(ord(char[0]), err)) + if not ans.face or not os.path.exists(ans.face): + raise FontNotFound('Failed to find font for character U+{:X}'.format(ord(char[0]))) + return ans @lru_cache(maxsize=64) @@ -52,7 +69,11 @@ def get_font_information(q, bold=False, italic=False): def get_font_files(opts): ans = {} - attr_map = {'bold': 'bold_font', 'italic': 'italic_font', 'bi': 'bold_italic_font'} + attr_map = { + 'bold': 'bold_font', + 'italic': 'italic_font', + 'bi': 'bold_italic_font' + } def get_family(key=None): ans = getattr(opts, attr_map.get(key, 'font_family')) @@ -61,11 +82,17 @@ def get_font_files(opts): return ans n = get_font_information(get_family()) - ans['regular'] = Font(Face(n.face), n.hinting, n.hintstyle, n.bold, n.italic) + ans['regular'] = Font( + Face(n.face), n.hinting, n.hintstyle, n.bold, n.italic) def do(key): - b = get_font_information(get_family(key), bold=key in ('bold', 'bi'), italic=key in ('italic', 'bi')) + b = get_font_information( + get_family(key), + bold=key in ('bold', 'bi'), + italic=key in ('italic', 'bi')) if b.face != n.face: - ans[key] = Font(Face(b.face), b.hinting, b.hintstyle, b.bold, b.italic) + ans[key] = Font( + Face(b.face), b.hinting, b.hintstyle, b.bold, b.italic) + do('bold'), do('italic'), do('bi') return ans diff --git a/kitty/fonts/freetype.py b/kitty/fonts/freetype.py index 51e1e8c39..8f2d22e2f 100644 --- a/kitty/fonts/freetype.py +++ b/kitty/fonts/freetype.py @@ -2,18 +2,18 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal -import unicodedata import ctypes +import sys +import unicodedata from collections import namedtuple from functools import lru_cache from threading import Lock -from kitty.fast_data_types import Face, FT_PIXEL_MODE_GRAY -from kitty.utils import ceil_int -from .fontconfig import find_font_for_character, get_font_files - -from kitty.utils import get_logical_dpi, wcwidth +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 find_font_for_character, get_font_files, FontNotFound current_font_family = current_font_family_name = cff_size = cell_width = cell_height = baseline = None CharTexture = underline_position = underline_thickness = None @@ -168,12 +168,29 @@ 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) - bitmap_char = render_char(text, bold, italic, width) + 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: