From 85e05a447d98195d9d0a3e0c51eee078703bb670 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 8 Feb 2017 11:29:24 +0530 Subject: [PATCH] Linux: Fallback to using bitmapped fonts for characters that are not present in any scalable fonts on the system Fixes #46 --- kitty/fonts/fontconfig.py | 33 ++++++++++--- kitty/fonts/freetype.py | 97 ++++++++++++++++++++++++++++++--------- kitty/freetype.c | 3 ++ 3 files changed, 105 insertions(+), 28 deletions(-) diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index e853ce3ca..3c94cf706 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -6,7 +6,6 @@ import os import re import subprocess from collections import namedtuple -from functools import lru_cache from kitty.fast_data_types import Face @@ -29,7 +28,13 @@ def to_bool(x): def get_font( - family, bold, italic, allow_bitmaped_fonts=False, size_in_pts=None, character=None + family, + bold, + italic, + allow_bitmaped_fonts=False, + size_in_pts=None, + character=None, + dpi=None ): query = escape_family_name(family) if character is not None: @@ -38,6 +43,8 @@ def get_font( query += ':scalable=true:outline=true' if size_in_pts is not None: query += ':size={:.1f}'.format(size_in_pts) + if dpi is not None: + query += ':dpi={:.1f}'.format(dpi) if bold: query += ':weight=200' if italic: @@ -60,10 +67,25 @@ def get_font( ) -@lru_cache(maxsize=4096) -def find_font_for_character(family, char, bold=False, italic=False): +def find_font_for_character( + family, + char, + bold=False, + italic=False, + allow_bitmaped_fonts=False, + size_in_pts=None, + dpi=None +): try: - ans = get_font(family, bold, italic, character=char) + ans = get_font( + family, + bold, + italic, + character=char, + allow_bitmaped_fonts=allow_bitmaped_fonts, + size_in_pts=size_in_pts, + dpi=dpi + ) except subprocess.CalledProcessError as err: raise FontNotFound( 'Failed to find font for character U+{:X}, error from fontconfig: {}'. @@ -76,7 +98,6 @@ def find_font_for_character(family, char, bold=False, italic=False): return ans -@lru_cache(maxsize=64) def get_font_information(family, bold=False, italic=False): return get_font(family, bold, italic) diff --git a/kitty/fonts/freetype.py b/kitty/fonts/freetype.py index 8f2d22e2f..5f34eb338 100644 --- a/kitty/fonts/freetype.py +++ b/kitty/fonts/freetype.py @@ -39,8 +39,20 @@ def calc_cell_width(font, face): @lru_cache(maxsize=2**10) -def font_for_char(char, bold=False, italic=False): - return find_font_for_character(current_font_family_name, char, bold, italic) +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 font_units_to_pixels(x, units_per_em, size_in_pts, dpi): @@ -51,27 +63,43 @@ def set_font_family(opts): global current_font_family, current_font_family_name, cff_size, cell_width, cell_height, CharTexture, baseline global underline_position, underline_thickness size_in_pts = opts.font_size - find_font_for_character.cache_clear() 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])} + cff_size = { + 'width': cff_size, + 'height': cff_size, + 'hres': int(dpi[0]), + 'vres': int(dpi[1]) + } for fobj in current_font_family.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]) + 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') +CharBitmap = namedtuple( + 'CharBitmap', 'data bearingX bearingY advance rows columns' +) freetype_lock = Lock() @@ -80,7 +108,9 @@ def render_to_bitmap(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)) + 'FreeType rendered the glyph for {!r} with an unsupported pixel mode: {}'. + format(text, bitmap.pixel_mode) + ) return bitmap @@ -94,17 +124,23 @@ def render_char(text, bold=False, italic=False, width=1): font = current_font_family.get(key) or current_font_family['regular'] face = font.face if not face.get_char_index(text[0]): - font = font_for_char(text[0], bold, italic) + try: + font = font_for_char(text[0], bold, italic) + except FontNotFound: + font = font_for_char( + text[0], bold, italic, allow_bitmaped_fonts=True + ) face = alt_face_cache.get(font) if face is None: face = alt_face_cache[font] = Face(font.face) - set_char_size(face, **cff_size) + if face.is_scalable: + set_char_size(face, **cff_size) 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.1 * cell_width): + elif extra > max(2, 0.1 * 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() @@ -117,8 +153,12 @@ def render_char(text, bold=False, italic=False, width=1): 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) + 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 place_char_in_cell(bitmap_char): @@ -133,7 +173,9 @@ def place_char_in_cell(bitmap_char): 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) + 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 @@ -141,9 +183,13 @@ def place_char_in_cell(bitmap_char): 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) + 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): @@ -176,7 +222,9 @@ def missing_glyph(width): 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) + 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 @@ -204,11 +252,16 @@ def render_cell(text=' ', bold=False, italic=False): 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): +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 + 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 diff --git a/kitty/freetype.c b/kitty/freetype.c index 157ae768c..97f41c805 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -15,6 +15,7 @@ typedef struct { FT_Face face; unsigned int units_per_EM; int ascender, descender, height, max_advance_width, max_advance_height, underline_position, underline_thickness; + bool is_scalable; } Face; @@ -68,6 +69,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { #define CPY(n) self->n = self->face->n; CPY(units_per_EM); CPY(ascender); CPY(descender); CPY(height); CPY(max_advance_width); CPY(max_advance_height); CPY(underline_position); CPY(underline_thickness); #undef CPY + self->is_scalable = FT_IS_SCALABLE(self->face); } return (PyObject*)self; } @@ -236,6 +238,7 @@ static PyMemberDef members[] = { MEM(max_advance_height, T_INT), MEM(underline_position, T_INT), MEM(underline_thickness, T_INT), + MEM(is_scalable, T_BOOL), {NULL} /* Sentinel */ };