Make font rendering tests use a text font rather than system font

This commit is contained in:
Kovid Goyal 2024-12-08 12:51:35 +05:30
parent 850dcec4d7
commit 3dca2687d8
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
5 changed files with 149 additions and 126 deletions

View file

@ -1137,7 +1137,7 @@ def set_font_data(
prerender_func: Callable[
[int, int, int, int, int, int, int, float, float, float, float],
Tuple[Tuple[int, ...], Tuple[Array[c_ubyte], ...]]],
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
descriptor_for_idx: Callable[[int], Tuple[Union[FontObject|str], bool, bool]],
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
narrow_symbols: Tuple[Tuple[int, int, int], ...],

View file

@ -979,7 +979,7 @@ render_group(
#define sendtogpu { FontCellMetrics scaled = fg->fcm; current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sp[i]->x, sp[i]->y, sp[i]->z, b); fg->fcm = scaled; }
if (scale == 1.f) {
if (scale == 1.f && rf.scale == 1 && !rf.subscale_n) {
for (unsigned i = 0; i < num_cells; i++) {
if (!sp[i]->rendered) {
sp[i]->rendered = true;
@ -1757,7 +1757,8 @@ initialize_font(FontGroup *fg, unsigned int desc_idx, const char *ftype) {
if (d == NULL) { PyErr_Print(); fatal("Failed for %s font", ftype); }
bool bold = PyObject_IsTrue(PyTuple_GET_ITEM(d, 1));
bool italic = PyObject_IsTrue(PyTuple_GET_ITEM(d, 2));
PyObject *face = desc_to_face(PyTuple_GET_ITEM(d, 0), (FONTS_DATA_HANDLE)fg);
PyObject *x = PyTuple_GET_ITEM(d, 0);
PyObject *face = PyUnicode_Check(x) ? face_from_path(PyUnicode_AsUTF8(x), 0, (FONTS_DATA_HANDLE)fg) : desc_to_face(x, (FONTS_DATA_HANDLE)fg);
Py_CLEAR(d);
if (face == NULL) { PyErr_Print(); fatal("Failed to convert descriptor to face for %s font", ftype); }
size_t idx = fg->fonts_count++;

View file

@ -28,11 +28,11 @@ def thickness(level: int = 1, horizontal: bool = True) -> int:
return int(math.ceil(pts * (_dpi / 72.0)))
def draw_hline(buf: BufType, width: int, x1: int, x2: int, y: int, level: int, supersample_factor: int = 1) -> None:
def draw_hline(buf: BufType, width: int, height: int, x1: int, x2: int, y: int, level: int, supersample_factor: int = 1) -> None:
' Draw a horizontal line between [x1, x2) centered at y with the thickness given by level and supersample factor '
sz = int(supersample_factor * thickness(level=level, horizontal=False))
start = y - sz // 2
for y in range(start, start + sz):
start = max(0, y - sz // 2)
for y in range(start, min(start + sz, height)):
offset = y * width
for x in range(x1, x2):
buf[offset + x] = 255
@ -41,15 +41,15 @@ def draw_hline(buf: BufType, width: int, x1: int, x2: int, y: int, level: int, s
def draw_vline(buf: BufType, width: int, y1: int, y2: int, x: int, level: int, supersample_factor: float = 1.0) -> None:
' Draw a vertical line between [y1, y2) centered at x with the thickness given by level and supersample factor '
sz = int(supersample_factor * thickness(level=level, horizontal=True))
start = x - sz // 2
for x in range(start, start + sz):
start = max(0, x - sz // 2)
for x in range(start, min(start + sz, width)):
for y in range(y1, y2):
buf[x + y * width] = 255
def half_hline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'left', extend_by: int = 0) -> None:
x1, x2 = (0, extend_by + width // 2) if which == 'left' else (width // 2 - extend_by, width)
draw_hline(buf, width, x1, x2, height // 2, level)
draw_hline(buf, width, height, x1, x2, height // 2, level)
def half_vline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'top', extend_by: int = 0) -> None:
@ -59,7 +59,7 @@ def half_vline(buf: BufType, width: int, height: int, level: int = 1, which: str
def get_holes(sz: int, hole_sz: int, num: int) -> list[tuple[int, ...]]:
all_holes_use = (num + 1) * hole_sz
individual_block_size = (sz - all_holes_use) // (num + 1)
individual_block_size = max(1, (sz - all_holes_use) // (num + 1))
half_hole_sz = hole_sz // 2
pos = - half_hole_sz
holes = []
@ -354,7 +354,7 @@ def fading_hline(buf: SSByteArray, width: int, height: int, level: int = 1, num:
factor = buf.supersample_factor
y = (height // 2 // factor) * factor
for x1, x2 in get_fading_lines(width, num, fade):
draw_hline(buf, width, x1, x2, y, level, supersample_factor = factor)
draw_hline(buf, width, height, x1, x2, y, level, supersample_factor = factor)
@supersampled()
@ -622,9 +622,9 @@ def commit(buf: SSByteArray, width: int, height: int, level: int = 1, scale: flo
for line in lines:
if line == 'horizontal' or line == 'right':
draw_hline(buf, width, hwidth, width, hheight, level, supersample_factor=factor)
draw_hline(buf, width, height, hwidth, width, hheight, level, supersample_factor=factor)
if line == 'horizontal' or line == 'left':
draw_hline(buf, width, 0, hwidth, hheight, level, supersample_factor=factor)
draw_hline(buf, width, height, 0, hwidth, hheight, level, supersample_factor=factor)
if line == 'vertical' or line == 'down':
draw_vline(buf, width, hheight, height, hwidth, level, supersample_factor=factor)
if line == 'vertical' or line == 'up':
@ -639,9 +639,9 @@ def half_dhline(buf: BufType, width: int, height: int, level: int = 1, which: st
x1, x2 = (0, width // 2) if which == 'left' else (width // 2, width)
gap = thickness(level + 1, horizontal=False)
if only != 'bottom':
draw_hline(buf, width, x1, x2, height // 2 - gap, level)
draw_hline(buf, width, height, x1, x2, height // 2 - gap, level)
if only != 'top':
draw_hline(buf, width, x1, x2, height // 2 + gap, level)
draw_hline(buf, width, height, x1, x2, height // 2 + gap, level)
return height // 2 - gap, height // 2 + gap
@ -692,12 +692,12 @@ def dcorner(buf: BufType, width: int, height: int, level: int = 1, which: str =
x2 += vgap
else:
x1 -= vgap
draw_hline(buf, width, x1, x2, height // 2 + ydelta, level)
draw_hline(buf, width, height, x1, x2, height // 2 + ydelta, level)
if hw == 'left':
x2 -= 2 * vgap
else:
x1 += 2 * vgap
draw_hline(buf, width, x1, x2, height // 2 - ydelta, level)
draw_hline(buf, width, height, x1, x2, height // 2 - ydelta, level)
y1, y2 = (0, height // 2) if vw == 'top' else (height // 2, height)
xdelta = vgap if hw == 'right' else -vgap
yd = thickness(level, horizontal=True) // 2
@ -717,7 +717,7 @@ def dpip(buf: BufType, width: int, height: int, level: int = 1, which: str = '
if which in '╟╢':
left, right = dvline(buf, width, height)
x1, x2 = (0, left) if which == '' else (right, width)
draw_hline(buf, width, x1, x2, height // 2, level)
draw_hline(buf, width, height, x1, x2, height // 2, level)
else:
top, bottom = dhline(buf, width, height)
y1, y2 = (0, top) if which == '' else (bottom, height)
@ -730,7 +730,7 @@ def inner_corner(buf: BufType, width: int, height: int, which: str = 'tl', level
vthick = thickness(level, horizontal=True) // 2
x1, x2 = (0, width // 2 - hgap + vthick + 1) if 'l' in which else (width // 2 + hgap - vthick, width)
yd = -1 if 't' in which else 1
draw_hline(buf, width, x1, x2, height // 2 + (yd * vgap), level)
draw_hline(buf, width, height, x1, x2, height // 2 + (yd * vgap), level)
y1, y2 = (0, height // 2 - vgap) if 't' in which else (height // 2 + vgap, height)
xd = -1 if 'l' in which else 1
draw_vline(buf, width, y1, y2, width // 2 + (xd * hgap), level)

View file

@ -163,8 +163,14 @@ def create_narrow_symbols(opts: Options) -> tuple[tuple[int, int, int], ...]:
return tuple((a, b, v) for (a, b), v in coalesce_symbol_maps(opts.narrow_symbols).items())
def descriptor_for_idx(idx: int) -> tuple[FontObject, bool, bool]:
return current_faces[idx]
descriptor_overrides: dict[int, tuple[str, bool, bool]] = {}
def descriptor_for_idx(idx: int) -> tuple[Union[FontObject | str], bool, bool]:
ans = descriptor_overrides.get(idx)
if ans is None:
return current_faces[idx]
return ans
def dump_font_debug() -> None:
@ -431,10 +437,12 @@ def render_box_drawing(codepoint: int, cell_width: int, cell_height: int, dpi: f
class setup_for_testing:
def __init__(self, family: str = 'monospace', size: float = 11.0, dpi: float = 96.0):
def __init__(self, family: str = 'monospace', size: float = 11.0, dpi: float = 96.0, main_face_path: str = ''):
self.family, self.size, self.dpi = family, size, dpi
self.main_face_path = main_face_path
def __enter__(self) -> tuple[dict[tuple[int, int, int], bytes], int, int]:
global descriptor_overrides
opts = defaults._replace(font_family=parse_font_spec(self.family), font_size=self.size)
set_options(opts)
sprites = {}
@ -444,6 +452,10 @@ def send_to_gpu(x: int, y: int, z: int, data: bytes) -> None:
sprite_map_set_limits(100000, 100)
set_send_sprite_to_gpu(send_to_gpu)
self.orig_desc_overrides = descriptor_overrides
descriptor_overrides = {}
if self.main_face_path:
descriptor_overrides[0] = self.main_face_path, False, False
try:
set_font_family(opts)
cell_width, cell_height = create_test_font_group(self.size, self.dpi, self.dpi)
@ -453,6 +465,8 @@ def send_to_gpu(x: int, y: int, z: int, data: bytes) -> None:
raise
def __exit__(self, *args: Any) -> None:
global descriptor_overrides
descriptor_overrides = self.orig_desc_overrides
set_send_sprite_to_gpu(None)

View file

@ -5,7 +5,7 @@
import os
import tempfile
import unittest
from functools import partial
from functools import lru_cache, partial
from math import ceil
from kitty.constants import is_macos, read_kitty_resource
@ -32,6 +32,11 @@ def parse_font_spec(spec):
return FontSpec.from_setting(spec)
@lru_cache(maxsize=64)
def testing_font_data(name):
return read_kitty_resource(name, __name__.rpartition('.')[0])
class Selection(BaseTest):
def test_font_selection(self):
@ -165,28 +170,110 @@ def t(x, **kw):
self.ae(face_from_descriptor(ff['medium']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
self.ae(face_from_descriptor(ff['bold']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
def block_helpers(s, sprites, cell_width, cell_height):
block_size = cell_width * cell_height * 4
class Rendering(BaseTest):
def full_block():
return b'\xff' * block_size
def empty_block():
return b'\0' * block_size
def half_block(first=b'\xff', second=b'\0', swap=False):
frac = 0.5
height = ceil(frac * cell_height)
rest = cell_height - height
if swap:
height, rest = rest, height
first, second = second, first
return (first * (height * cell_width * 4)) + (second * rest * cell_width * 4)
def quarter_block():
frac = 0.5
height = ceil(frac * cell_height)
width = ceil(frac * cell_width)
ans = array.array('I', b'\0' * block_size)
for y in range(height):
pos = cell_width * y
for x in range(width):
ans[pos + x] = 0xffffffff
return ans.tobytes()
def upper_half_block():
return half_block()
def lower_half_block():
return half_block(swap=True)
def block_as_str(a):
pixels = array.array('I', a)
def row(y):
pos = y * cell_width
return ' '.join(f'{int(pixels[pos + x] != 0)}' for x in range(cell_width))
return '\n'.join(row(y) for y in range(cell_height))
def assert_blocks(a, b, msg=''):
if a != b:
msg = msg or 'block not equal'
if len(a) != len(b):
assert_blocks.__msg = msg + f'block lengths not equal: {len(a)/4} != {len(b)/4}'
else:
assert_blocks.__msg = msg + '\n' + block_as_str(a) + '\n\n' + block_as_str(b)
del a, b
raise AssertionError(assert_blocks.__msg)
def multiline_render(text, scale=1, width=1, **kw):
s.reset()
draw_multicell(s, text, scale=scale, width=width, **kw)
ans = []
for y in range(scale):
line = s.line(y)
test_render_line(line)
for x in range(width * scale):
ans.append(sprites[line.sprite_at(x)])
return ans
def block_test(*expected, **kw):
for i, (expected, actual) in enumerate(zip(expected, multiline_render(kw.pop('text', ''), **kw), strict=True)):
assert_blocks(expected(), actual, f'Block {i} is not equal')
return full_block, empty_block, upper_half_block, lower_half_block, quarter_block, block_as_str, block_test
class FontBaseTest(BaseTest):
font_size = 5.0
dpi = 72.
font_name = 'FiraCode-Medium.otf'
def path_for_font(self, name):
if name not in self.font_path_cache:
with open(os.path.join(self.tdir, name), 'wb') as f:
self.font_path_cache[name] = f.name
f.write(testing_font_data(name))
return self.font_path_cache[name]
def setUp(self):
super().setUp()
self.test_ctx = setup_for_testing()
self.test_ctx.__enter__()
self.sprites, self.cell_width, self.cell_height = self.test_ctx.__enter__()
try:
self.assertEqual([k[0] for k in self.sprites], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
except Exception:
self.test_ctx.__exit__()
del self.test_ctx
raise
self.font_path_cache = {}
self.tdir = tempfile.mkdtemp()
self.addCleanup(self.rmtree_ignoring_errors, self.tdir)
path = self.path_for_font(self.font_name) if self.font_name else ''
tc = setup_for_testing(size=self.font_size, dpi=self.dpi, main_face_path=path)
self.sprites, self.cell_width, self.cell_height = tc.__enter__()
self.addCleanup(tc.__exit__)
self.assertEqual([k[0] for k in self.sprites], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
def tearDown(self):
self.test_ctx.__exit__()
del self.sprites, self.cell_width, self.cell_height, self.test_ctx
self.rmtree_ignoring_errors(self.tdir)
del self.sprites, self.cell_width, self.cell_height
self.font_path_cache = {}
super().tearDown()
class Rendering(FontBaseTest):
def test_sprite_map(self):
sprite_map_set_limits(10, 2)
sprite_map_set_layout(5, 5)
@ -210,86 +297,17 @@ def test_box_drawing(self):
self.assertEqual(len(self.sprites) - prerendered, len(box_chars))
def test_scaled_box_drawing(self):
block_size = self.cell_width * self.cell_height * 4
def full_block():
return b'\xff' * block_size
def empty_block():
return b'\0' * block_size
def half_block(first=b'\xff', second=b'\0'):
frac = 0.5
height = ceil(frac * self.cell_height)
rest = self.cell_height - height
return (first * (rest * self.cell_width * 4)) + (second * height * self.cell_width * 4)
def quarter_block():
frac = 0.5
height = ceil(frac * self.cell_height)
width = ceil(frac * self.cell_width)
ans = array.array('I', b'\0' * block_size)
self.ae(len(ans), self.cell_width * self.cell_height)
for y in range(height):
pos = self.cell_width * y
for x in range(width):
ans[pos + x] = 0xffffffff
return ans.tobytes()
def upper_half_block():
return half_block()
def lower_half_block():
return half_block(b'\0', b'\xff')
def block_as_str(a):
pixels = array.array('I', a)
def row(y):
pos = y * self.cell_width
return ' '.join(f'{int(pixels[pos + x] != 0)}' for x in range(self.cell_width))
return '\n'.join(row(y) for y in range(self.cell_height))
def assert_blocks(a, b, msg=''):
if a != b:
assert_blocks.__msg = (msg or 'block not equal') + '\n' + block_as_str(a) + '\n\n' + block_as_str(b)
del a, b
raise AssertionError(assert_blocks.__msg)
s = self.create_screen(cols=8, lines=8, scrollback=0)
s.reset()
before = len(self.sprites)
draw_multicell(s, '██', scale=1, subscale_n=1, subscale_d=2, vertical_align=0, width=1)
test_render_line(s.line(0))
self.ae(len(self.sprites), before + 1)
assert_blocks(upper_half_block(), self.sprites[tuple(self.sprites)[before]])
s.reset()
before = len(self.sprites)
draw_multicell(s, '', scale=1, subscale_n=1, subscale_d=2, vertical_align=0)
test_render_line(s.line(0))
self.ae(len(self.sprites), before + 1)
assert_blocks(quarter_block(), self.sprites[tuple(self.sprites)[before]])
def block_test(a=empty_block, b=empty_block, c=empty_block, d=empty_block, scale=2, half_block=True, vertical_align=0):
s.reset()
before = len(self.sprites)
subscale_n = subscale_d = 0
if half_block:
subscale_n, subscale_d = 1, 2
draw_multicell(s, '', scale=scale, subscale_n=subscale_n, subscale_d=subscale_d, vertical_align=vertical_align)
test_render_line(s.line(0))
self.ae(len(self.sprites), before + 2)
test_render_line(s.line(1))
self.ae(len(self.sprites), before + 4)
blocks = tuple(self.sprites)[before:]
for i, (expected, actual) in enumerate(zip((a(), b(), c(), d()), blocks)):
assert_blocks(expected, self.sprites[actual], f'The {i} block differs')
block_test(full_block, full_block, full_block, full_block, half_block=False)
block_test(a=full_block)
block_test(c=full_block, vertical_align=1)
block_test(a=lower_half_block, c=upper_half_block, vertical_align=2)
full_block, empty_block, upper_half_block, lower_half_block, quarter_block, block_as_str, block_test = block_helpers(
s, self.sprites, self.cell_width, self.cell_height)
block_test(full_block)
block_test(full_block, full_block, full_block, full_block, scale=2)
block_test(full_block, empty_block, empty_block, empty_block, scale=2, subscale_n=1, subscale_d=2)
block_test(full_block, full_block, empty_block, empty_block, scale=2, subscale_n=1, subscale_d=2, text='██')
block_test(empty_block, empty_block, full_block, empty_block, scale=2, subscale_n=1, subscale_d=2, vertical_align=1)
block_test(quarter_block, scale=1, subscale_n=1, subscale_d=2)
block_test(upper_half_block, scale=1, subscale_n=1, subscale_d=2, text='██')
block_test(lower_half_block, scale=1, subscale_n=1, subscale_d=2, text='██', vertical_align=1)
def test_font_rendering(self):
render_string('ab\u0347\u0305你好|\U0001F601|\U0001F64f|\U0001F63a|')
@ -306,18 +324,8 @@ def test_font_rendering(self):
def test_shaping(self):
font_path_cache = {}
def path_for_font(name):
if name not in font_path_cache:
with open(os.path.join(self.tdir, name), 'wb') as f:
font_path_cache[name] = f.name
data = read_kitty_resource(name, __name__.rpartition('.')[0])
f.write(data)
return font_path_cache[name]
def ss(text, font=None):
path = path_for_font(font) if font else None
path = self.path_for_font(font) if font else None
return shape_string(text, path=path)
def groups(text, font=None):