fonts.py 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213
  1. #!/usr/bin/env python
  2. # License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
  3. import array
  4. import os
  5. import tempfile
  6. import unittest
  7. from collections.abc import Iterable
  8. from functools import lru_cache, partial
  9. from itertools import repeat
  10. from math import ceil
  11. from kitty.constants import is_macos, read_kitty_resource
  12. from kitty.fast_data_types import (
  13. DECAWM,
  14. ParsedFontFeature,
  15. get_fallback_font,
  16. set_allow_use_of_box_fonts,
  17. sprite_idx_to_pos,
  18. sprite_map_set_layout,
  19. sprite_map_set_limits,
  20. test_render_line,
  21. test_sprite_position_increment,
  22. wcwidth,
  23. )
  24. from kitty.fonts import family_name_to_key
  25. from kitty.fonts.common import FontSpec, all_fonts_map, face_from_descriptor, get_font_files, get_named_style, spec_for_face
  26. from kitty.fonts.render import coalesce_symbol_maps, create_face, render_string, setup_for_testing, shape_string
  27. from kitty.options.types import Options
  28. from . import BaseTest, draw_multicell
  29. def parse_font_spec(spec):
  30. return FontSpec.from_setting(spec)
  31. @lru_cache(maxsize=64)
  32. def testing_font_data(name):
  33. return read_kitty_resource(name, __name__.rpartition('.')[0])
  34. class Selection(BaseTest):
  35. def test_font_selection(self):
  36. self.set_options({'font_features': {'LiberationMono': (ParsedFontFeature('-dlig'),)}})
  37. opts = Options()
  38. fonts_map = all_fonts_map(True)
  39. names = set(fonts_map['family_map']) | set(fonts_map['variable_map'])
  40. del fonts_map
  41. def s(family: str, *expected: str, alternate=None) -> None:
  42. opts.font_family = parse_font_spec(family)
  43. ff = get_font_files(opts)
  44. actual = tuple(face_from_descriptor(ff[x]).postscript_name() for x in ('medium', 'bold', 'italic', 'bi')) # type: ignore
  45. del ff
  46. for x in actual:
  47. if '/' in x: # Old FreeType failed to generate postscript name for a variable font probably
  48. return
  49. with self.subTest(spec=family):
  50. try:
  51. self.ae(expected, actual)
  52. except AssertionError:
  53. if alternate:
  54. self.ae(alternate, actual)
  55. else:
  56. raise
  57. def both(family: str, *expected: str, alternate=None) -> None:
  58. for family in (family, f'family="{family}"'):
  59. s(family, *expected, alternate=alternate)
  60. def has(family, allow_missing_in_ci=False):
  61. ans = family_name_to_key(family) in names
  62. if self.is_ci and not allow_missing_in_ci and not ans:
  63. raise AssertionError(f'The family: {family} is not available')
  64. return ans
  65. def t(family, psprefix, bold='Bold', italic='Italic', bi='', reg='Regular', allow_missing_in_ci=False, alternate=None):
  66. if has(family, allow_missing_in_ci=allow_missing_in_ci):
  67. bi = bi or bold + italic
  68. if reg:
  69. reg = '-' + reg
  70. both(family, f'{psprefix}{reg}', f'{psprefix}-{bold}', f'{psprefix}-{italic}', f'{psprefix}-{bi}', alternate=alternate)
  71. t('Source Code Pro', 'SourceCodePro', 'Semibold', 'It')
  72. t('sourcecodeVf', 'SourceCodeVF', 'Semibold')
  73. # The Arch ttf-fira-code package excludes the variable fonts for some reason
  74. t('fira code', 'FiraCodeRoman', 'SemiBold', 'Regular', 'SemiBold', alternate=(
  75. 'FiraCode-Regular', 'FiraCode-SemiBold', 'FiraCode-Retina', 'FiraCode-SemiBold'))
  76. t('hack', 'Hack')
  77. # some ubuntu systems (such as the build VM) have only the regular and
  78. # bold faces of DejaVu Sans Mono installed.
  79. # t('DejaVu Sans Mono', 'DejaVuSansMono', reg='', italic='Oblique')
  80. t('ubuntu mono', 'UbuntuMono')
  81. t('liberation mono', 'LiberationMono', reg='')
  82. t('ibm plex mono', 'IBMPlexMono', 'SmBld', reg='')
  83. t('iosevka fixed', 'Iosevka-Fixed', 'Semibold', reg='', bi='Semibold-Italic', allow_missing_in_ci=True)
  84. t('iosevka term', 'Iosevka-Term', 'Semibold', reg='', bi='Semibold-Italic', allow_missing_in_ci=True)
  85. t('fantasque sans mono', 'FantasqueSansMono')
  86. t('jetbrains mono', 'JetBrainsMono', 'SemiBold')
  87. t('consolas', 'Consolas', reg='', allow_missing_in_ci=True)
  88. if has('cascadia code'):
  89. if is_macos:
  90. both('cascadia code', 'CascadiaCode-Regular', 'CascadiaCode-Regular_SemiBold', 'CascadiaCode-Italic', 'CascadiaCode-Italic_SemiBold-Italic')
  91. else:
  92. both('cascadia code', 'CascadiaCodeRoman-Regular', 'CascadiaCodeRoman-SemiBold', 'CascadiaCode-Italic', 'CascadiaCode-SemiBoldItalic')
  93. if has('cascadia mono'):
  94. if is_macos:
  95. both('cascadia mono', 'CascadiaMono-Regular', 'CascadiaMono-Regular_SemiBold', 'CascadiaMono-Italic', 'CascadiaMono-Italic_SemiBold-Italic')
  96. else:
  97. both('cascadia mono', 'CascadiaMonoRoman-Regular', 'CascadiaMonoRoman-SemiBold', 'CascadiaMono-Italic', 'CascadiaMono-SemiBoldItalic')
  98. if has('operator mono', allow_missing_in_ci=True):
  99. both('operator mono', 'OperatorMono-Medium', 'OperatorMono-Bold', 'OperatorMono-MediumItalic', 'OperatorMono-BoldItalic')
  100. # Test variable font selection
  101. if has('SourceCodeVF'):
  102. opts = Options()
  103. opts.font_family = parse_font_spec('family="SourceCodeVF" variable_name="SourceCodeUpright" style="Bold"')
  104. ff = get_font_files(opts)
  105. face = face_from_descriptor(ff['medium'])
  106. self.ae(get_named_style(face)['name'], 'Bold')
  107. face = face_from_descriptor(ff['italic'])
  108. self.ae(get_named_style(face)['name'], 'Bold Italic')
  109. face = face_from_descriptor(ff['bold'])
  110. self.ae(get_named_style(face)['name'], 'Black')
  111. face = face_from_descriptor(ff['bi'])
  112. self.ae(get_named_style(face)['name'], 'Black Italic')
  113. opts.font_family = parse_font_spec('family=SourceCodeVF variable_name=SourceCodeUpright wght=470')
  114. opts.italic_font = parse_font_spec('family=SourceCodeVF variable_name=SourceCodeItalic style=Black')
  115. ff = get_font_files(opts)
  116. self.assertFalse(get_named_style(ff['medium']))
  117. self.ae(get_named_style(ff['italic'])['name'], 'Black Italic')
  118. if has('cascadia code'):
  119. opts = Options()
  120. opts.font_family = parse_font_spec('family="cascadia code"')
  121. opts.italic_font = parse_font_spec('family="cascadia code" variable_name= style="Light Italic"')
  122. ff = get_font_files(opts)
  123. def t(x, **kw):
  124. if 'spec' in kw:
  125. fs = FontSpec.from_setting('family="Cascadia Code" ' + kw['spec'])._replace(created_from_string='')
  126. else:
  127. kw['family'] = 'Cascadia Code'
  128. fs = FontSpec(**kw)
  129. face = face_from_descriptor(ff[x])
  130. self.ae(fs.as_setting, spec_for_face('Cascadia Code', face).as_setting)
  131. t('medium', variable_name='CascadiaCodeRoman', style='Regular')
  132. t('italic', variable_name='', style='Light Italic')
  133. opts = Options()
  134. opts.font_family = parse_font_spec('family="cascadia code" variable_name=CascadiaCodeRoman wght=455')
  135. opts.italic_font = parse_font_spec('family="cascadia code" variable_name= wght=405')
  136. opts.bold_font = parse_font_spec('family="cascadia code" variable_name=CascadiaCodeRoman wght=603')
  137. ff = get_font_files(opts)
  138. t('medium', spec='variable_name=CascadiaCodeRoman wght=455')
  139. t('italic', spec='variable_name= wght=405')
  140. t('bold', spec='variable_name=CascadiaCodeRoman wght=603')
  141. t('bi', spec='variable_name= wght=603')
  142. # Test font features
  143. if has('liberation mono'):
  144. opts = Options()
  145. opts.font_family = parse_font_spec('family="liberation mono"')
  146. ff = get_font_files(opts)
  147. self.ae(face_from_descriptor(ff['medium']).applied_features(), {'dlig': '-dlig'})
  148. self.ae(face_from_descriptor(ff['bold']).applied_features(), {})
  149. opts.font_family = parse_font_spec('family="liberation mono" features="dlig test=3"')
  150. ff = get_font_files(opts)
  151. self.ae(face_from_descriptor(ff['medium']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
  152. self.ae(face_from_descriptor(ff['bold']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
  153. def block_helpers(s, sprites, cell_width, cell_height):
  154. block_size = cell_width * cell_height * 4
  155. def full_block():
  156. return b'\xff' * block_size
  157. def empty_block():
  158. return b'\0' * block_size
  159. def half_block(first=b'\xff', second=b'\0', swap=False):
  160. frac = 0.5
  161. height = ceil(frac * cell_height)
  162. rest = cell_height - height
  163. if swap:
  164. height, rest = rest, height
  165. first, second = second, first
  166. return (first * (height * cell_width * 4)) + (second * rest * cell_width * 4)
  167. def quarter_block():
  168. frac = 0.5
  169. height = ceil(frac * cell_height)
  170. width = ceil(frac * cell_width)
  171. ans = array.array('I', b'\0' * block_size)
  172. for y in range(height):
  173. pos = cell_width * y
  174. for x in range(width):
  175. ans[pos + x] = 0xffffffff
  176. return ans.tobytes()
  177. def upper_half_block():
  178. return half_block()
  179. def lower_half_block():
  180. return half_block(swap=True)
  181. def block_as_str(a):
  182. pixels = array.array('I', a)
  183. def row(y):
  184. pos = y * cell_width
  185. return ' '.join(f'{int(pixels[pos + x] != 0)}' for x in range(cell_width))
  186. return '\n'.join(row(y) for y in range(cell_height))
  187. def assert_blocks(a, b, msg=''):
  188. if a != b:
  189. msg = msg or 'block not equal'
  190. if len(a) != len(b):
  191. assert_blocks.__msg = msg + f' block lengths not equal: {len(a)/4} != {len(b)/4}'
  192. else:
  193. assert_blocks.__msg = msg + '\n' + block_as_str(a) + '\n\n' + block_as_str(b)
  194. del a, b
  195. raise AssertionError(assert_blocks.__msg)
  196. def multiline_render(text, scale=1, width=1, **kw):
  197. s.reset()
  198. draw_multicell(s, text, scale=scale, width=width, **kw)
  199. ans = []
  200. for y in range(scale):
  201. line = s.line(y)
  202. test_render_line(line)
  203. for x in range(width * scale):
  204. ans.append(sprites[sprite_idx_to_pos(line.sprite_at(x), setup_for_testing.xnum, setup_for_testing.ynum)])
  205. return ans
  206. def block_test(*expected, **kw):
  207. mr = multiline_render(kw.pop('text', '█'), **kw)
  208. try:
  209. z = zip(expected, mr, strict=True)
  210. except TypeError:
  211. z = zip(expected, mr)
  212. for i, (expected, actual) in enumerate(z):
  213. assert_blocks(expected(), actual, f'Block {i} is not equal')
  214. return full_block, empty_block, upper_half_block, lower_half_block, quarter_block, block_as_str, block_test
  215. class FontBaseTest(BaseTest):
  216. font_size = 5.0
  217. dpi = 72.
  218. font_name = 'FiraCode-Medium.otf'
  219. def path_for_font(self, name):
  220. if name not in self.font_path_cache:
  221. with open(os.path.join(self.tdir, name), 'wb') as f:
  222. self.font_path_cache[name] = f.name
  223. f.write(testing_font_data(name))
  224. return self.font_path_cache[name]
  225. def setUp(self):
  226. super().setUp()
  227. self.font_path_cache = {}
  228. self.tdir = tempfile.mkdtemp()
  229. self.addCleanup(self.rmtree_ignoring_errors, self.tdir)
  230. path = self.path_for_font(self.font_name) if self.font_name else ''
  231. tc = setup_for_testing(size=self.font_size, dpi=self.dpi, main_face_path=path)
  232. self.sprites, self.cell_width, self.cell_height = tc.__enter__()
  233. self.addCleanup(tc.__exit__)
  234. self.assertEqual([k[0] for k in self.sprites], list(range(11)))
  235. def tearDown(self):
  236. del self.sprites, self.cell_width, self.cell_height
  237. self.font_path_cache = {}
  238. super().tearDown()
  239. class Rendering(FontBaseTest):
  240. def test_sprite_map(self):
  241. sprite_map_set_limits(10, 3)
  242. sprite_map_set_layout(5, 4) # 4 because of underline_exclusion row
  243. self.ae(test_sprite_position_increment(), (0, 0, 0))
  244. self.ae(test_sprite_position_increment(), (1, 0, 0))
  245. self.ae(test_sprite_position_increment(), (0, 1, 0))
  246. self.ae(test_sprite_position_increment(), (1, 1, 0))
  247. self.ae(test_sprite_position_increment(), (0, 0, 1))
  248. self.ae(test_sprite_position_increment(), (1, 0, 1))
  249. self.ae(test_sprite_position_increment(), (0, 1, 1))
  250. self.ae(test_sprite_position_increment(), (1, 1, 1))
  251. self.ae(test_sprite_position_increment(), (0, 0, 2))
  252. self.ae(test_sprite_position_increment(), (1, 0, 2))
  253. def test_box_drawing(self):
  254. s = self.create_screen(cols=len(box_chars) + 1, lines=1, scrollback=0)
  255. prerendered = len(self.sprites)
  256. s.draw(''.join(box_chars))
  257. line = s.line(0)
  258. test_render_line(line)
  259. self.assertEqual(len(self.sprites) - prerendered, len(box_chars))
  260. def test_scaled_box_drawing(self):
  261. self.scaled_drawing_test()
  262. def test_scaled_font_drawing(self):
  263. set_allow_use_of_box_fonts(False)
  264. try:
  265. self.scaled_drawing_test()
  266. finally:
  267. set_allow_use_of_box_fonts(True)
  268. def scaled_drawing_test(self):
  269. s = self.create_screen(cols=8, lines=8, scrollback=0)
  270. full_block, empty_block, upper_half_block, lower_half_block, quarter_block, block_as_str, block_test = block_helpers(
  271. s, self.sprites, self.cell_width, self.cell_height)
  272. block_test(full_block)
  273. block_test(full_block, full_block, full_block, full_block, scale=2)
  274. block_test(full_block, empty_block, empty_block, empty_block, scale=2, subscale_n=1, subscale_d=2)
  275. block_test(full_block, full_block, empty_block, empty_block, scale=2, subscale_n=1, subscale_d=2, text='██')
  276. block_test(empty_block, empty_block, full_block, empty_block, scale=2, subscale_n=1, subscale_d=2, vertical_align=1)
  277. block_test(quarter_block, scale=1, subscale_n=1, subscale_d=2)
  278. block_test(upper_half_block, scale=1, subscale_n=1, subscale_d=2, text='██')
  279. block_test(lower_half_block, scale=1, subscale_n=1, subscale_d=2, text='██', vertical_align=1)
  280. def test_font_rendering(self):
  281. render_string('ab\u0347\u0305你好|\U0001F601|\U0001F64f|\U0001F63a|')
  282. text = 'He\u0347\u0305llo\u0341, w\u0302or\u0306l\u0354d!'
  283. # macOS has no fonts capable of rendering combining chars
  284. if is_macos:
  285. text = text.encode('ascii', 'ignore').decode('ascii')
  286. cells = render_string(text)[-1]
  287. self.ae(len(cells), len(text.encode('ascii', 'ignore')))
  288. text = '你好,世界'
  289. sz = sum(map(lambda x: wcwidth(ord(x)), text))
  290. cells = render_string(text)[-1]
  291. self.ae(len(cells), sz)
  292. @unittest.skipIf(is_macos, 'COLRv1 is only supported on Linux')
  293. def test_rendering_colrv1(self):
  294. f = create_face(self.path_for_font('twemoji_smiley-cff2_colr_1.otf'))
  295. f.set_size(64, 96, 96)
  296. for char in '😁😇😈':
  297. _, w, h = f.render_codepoint(ord(char))
  298. self.assertGreater(w, 64)
  299. self.assertGreater(h, 64)
  300. def test_shaping(self):
  301. def ss(text, font=None):
  302. path = self.path_for_font(font) if font else None
  303. return shape_string(text, path=path)
  304. def groups(text, font=None):
  305. return [x[:2] for x in ss(text, font)]
  306. for font in ('FiraCode-Medium.otf', 'CascadiaCode-Regular.otf', 'iosevka-regular.ttf'):
  307. g = partial(groups, font=font)
  308. self.ae(g('abcd'), [(1, 1) for i in range(4)])
  309. self.ae(g('A===B!=C'), [(1, 1), (3, 3), (1, 1), (2, 2), (1, 1)])
  310. self.ae(g('A=>>B!=C'), [(1, 1), (3, 3), (1, 1), (2, 2), (1, 1)])
  311. if 'iosevka' in font:
  312. self.ae(g('--->'), [(4, 4)])
  313. self.ae(g('-' * 12 + '>'), [(13, 13)])
  314. self.ae(g('<~~~'), [(4, 4)])
  315. self.ae(g('a<~~~b'), [(1, 1), (4, 4), (1, 1)])
  316. else:
  317. self.ae(g('----'), [(4, 4)])
  318. self.ae(g('F--a--'), [(1, 1), (2, 2), (1, 1), (2, 2)])
  319. self.ae(g('===--<>=='), [(3, 3), (2, 2), (2, 2), (2, 2)])
  320. self.ae(g('==!=<>==<><><>'), [(4, 4), (2, 2), (2, 2), (2, 2), (2, 2), (2, 2)])
  321. self.ae(g('-' * 18), [(18, 18)])
  322. self.ae(g('a>\u2060<b'), [(1, 1), (1, 2), (1, 1), (1, 1)])
  323. colon_glyph = ss('9:30', font='FiraCode-Medium.otf')[1][2]
  324. self.assertNotEqual(colon_glyph, ss(':', font='FiraCode-Medium.otf')[0][2])
  325. self.ae(colon_glyph, 1031)
  326. self.ae(groups('9:30', font='FiraCode-Medium.otf'), [(1, 1), (1, 1), (1, 1), (1, 1)])
  327. self.ae(groups('|\U0001F601|\U0001F64f|\U0001F63a|'), [(1, 1), (2, 1), (1, 1), (2, 1), (1, 1), (2, 1), (1, 1)])
  328. self.ae(groups('He\u0347\u0305llo\u0337,', font='LiberationMono-Regular.ttf'),
  329. [(1, 1), (1, 3), (1, 1), (1, 1), (1, 2), (1, 1)])
  330. self.ae(groups('i\u0332\u0308', font='LiberationMono-Regular.ttf'), [(1, 2)])
  331. self.ae(groups('u\u0332 u\u0332\u0301', font='LiberationMono-Regular.ttf'), [(1, 2), (1, 1), (1, 2)])
  332. def test_emoji_presentation(self):
  333. s = self.create_screen()
  334. s.draw('\u2716\u2716\ufe0f')
  335. self.ae((s.cursor.x, s.cursor.y), (3, 0))
  336. s.draw('\u2716\u2716')
  337. self.ae((s.cursor.x, s.cursor.y), (5, 0))
  338. s.draw('\ufe0f')
  339. self.ae((s.cursor.x, s.cursor.y), (2, 1))
  340. self.ae(str(s.line(0)), '\u2716\u2716\ufe0f\u2716')
  341. self.ae(str(s.line(1)), '\u2716\ufe0f')
  342. s.draw('\u2716' * 3)
  343. self.ae((s.cursor.x, s.cursor.y), (5, 1))
  344. self.ae(str(s.line(1)), '\u2716\ufe0f\u2716\u2716\u2716')
  345. self.ae((s.cursor.x, s.cursor.y), (5, 1))
  346. s.reset_mode(DECAWM)
  347. s.draw('\ufe0f')
  348. s.set_mode(DECAWM)
  349. self.ae((s.cursor.x, s.cursor.y), (5, 1))
  350. self.ae(str(s.line(1)), '\u2716\ufe0f\u2716\u2716\ufe0f')
  351. s.cursor.y = s.lines - 1
  352. s.draw('\u2716' * s.columns)
  353. self.ae((s.cursor.x, s.cursor.y), (5, 4))
  354. s.draw('\ufe0f')
  355. self.ae((s.cursor.x, s.cursor.y), (2, 4))
  356. self.ae(str(s.line(s.cursor.y)), '\u2716\ufe0f')
  357. @unittest.skipUnless(is_macos, 'Only macOS has a Last Resort font')
  358. def test_fallback_font_not_last_resort(self):
  359. # Ensure that the LastResort font is not reported as a fallback font on
  360. # macOS. See https://github.com/kovidgoyal/kitty/issues/799
  361. with self.assertRaises(ValueError, msg='No fallback font found'):
  362. get_fallback_font('\U0010FFFF', False, False)
  363. def test_coalesce_symbol_maps(self):
  364. q = {(2, 3): 'a', (4, 6): 'b', (5, 5): 'b', (7, 7): 'b', (9, 9): 'b', (1, 1): 'a'}
  365. self.ae(coalesce_symbol_maps(q), {(1, 3): 'a', (4, 7): 'b', (9, 9): 'b'})
  366. q = {(1, 4): 'a', (2, 3): 'b'}
  367. self.ae(coalesce_symbol_maps(q), {(1, 1): 'a', (2, 3): 'b', (4, 4): 'a'})
  368. q = {(2, 3): 'b', (1, 4): 'a'}
  369. self.ae(coalesce_symbol_maps(q), {(1, 4): 'a'})
  370. q = {(1, 4): 'a', (2, 5): 'b'}
  371. self.ae(coalesce_symbol_maps(q), {(1, 1): 'a', (2, 5): 'b'})
  372. q = {(2, 5): 'b', (1, 4): 'a'}
  373. self.ae(coalesce_symbol_maps(q), {(1, 4): 'a', (5, 5): 'b'})
  374. q = {(1, 4): 'a', (2, 5): 'a'}
  375. self.ae(coalesce_symbol_maps(q), {(1, 5): 'a'})
  376. q = {(1, 4): 'a', (4, 5): 'b'}
  377. self.ae(coalesce_symbol_maps(q), {(1, 3): 'a', (4, 5): 'b'})
  378. q = {(4, 5): 'b', (1, 4): 'a'}
  379. self.ae(coalesce_symbol_maps(q), {(1, 4): 'a', (5, 5): 'b'})
  380. q = {(0, 30): 'a', (10, 10): 'b', (11, 11): 'b', (2, 2): 'c', (1, 1): 'c'}
  381. self.ae(coalesce_symbol_maps(q), {
  382. (0, 0): 'a', (1, 2): 'c', (3, 9): 'a', (10, 11): 'b', (12, 30): 'a'})
  383. def test_chars(chars: str = '╌', sz: int = 128) -> None:
  384. # kitty +runpy "from kitty.fonts.box_drawing import test_chars; test_chars('XXX')"
  385. from kitty.fast_data_types import concat_cells, render_box_char, set_send_sprite_to_gpu
  386. from kitty.fonts.render import display_bitmap, setup_for_testing
  387. if not chars:
  388. import sys
  389. chars = sys.argv[-1]
  390. def as_ord(x: str) -> int:
  391. if x.lower().startswith('u+'):
  392. return int(x[2:], 16)
  393. return ord(x)
  394. if '...' in chars:
  395. start, end = chars.partition('...')[::2]
  396. chars = ''.join(map(chr, range(as_ord(start), as_ord(end)+1)))
  397. with setup_for_testing('monospace', sz) as (_, width, height):
  398. try:
  399. for ch in chars:
  400. nb = render_box_char(as_ord(ch), width, height)
  401. rgb_data = concat_cells(width, height, False, (nb,))
  402. display_bitmap(rgb_data, width, height)
  403. print()
  404. finally:
  405. set_send_sprite_to_gpu(None)
  406. def test_drawing(sz: int = 48, family: str = 'monospace', start: int = 0x2500, num_rows: int = 10, num_cols: int = 16) -> None:
  407. from kitty.fast_data_types import concat_cells, render_box_char, set_send_sprite_to_gpu
  408. from .render import display_bitmap, setup_for_testing
  409. with setup_for_testing(family, sz) as (_, width, height):
  410. space = bytearray(width * height)
  411. def join_cells(cells: Iterable[bytes]) -> bytes:
  412. cells = tuple(bytes(x) for x in cells)
  413. return concat_cells(width, height, False, cells)
  414. def render_chr(ch: str) -> bytearray:
  415. if ch in box_chars:
  416. return bytearray(render_box_char(ord(ch), width, height))
  417. return space
  418. pos = start
  419. rows = []
  420. space_row = join_cells(repeat(space, 32))
  421. try:
  422. for r in range(num_rows):
  423. row = []
  424. for i in range(num_cols):
  425. row.append(render_chr(chr(pos)))
  426. row.append(space)
  427. pos += 1
  428. rows.append(join_cells(row))
  429. rows.append(space_row)
  430. rgb_data = b''.join(rows)
  431. width *= 32
  432. height *= len(rows)
  433. assert len(rgb_data) == width * height * 4, f'{len(rgb_data)} != {width * height * 4}'
  434. display_bitmap(rgb_data, width, height)
  435. finally:
  436. set_send_sprite_to_gpu(None)
  437. box_chars = { # {{{
  438. '─',
  439. '━',
  440. '│',
  441. '┃',
  442. '┄',
  443. '┅',
  444. '┆',
  445. '┇',
  446. '┈',
  447. '┉',
  448. '┊',
  449. '┋',
  450. '┌',
  451. '┍',
  452. '┎',
  453. '┏',
  454. '┐',
  455. '┑',
  456. '┒',
  457. '┓',
  458. '└',
  459. '┕',
  460. '┖',
  461. '┗',
  462. '┘',
  463. '┙',
  464. '┚',
  465. '┛',
  466. '├',
  467. '┝',
  468. '┞',
  469. '┟',
  470. '┠',
  471. '┡',
  472. '┢',
  473. '┣',
  474. '┤',
  475. '┥',
  476. '┦',
  477. '┧',
  478. '┨',
  479. '┩',
  480. '┪',
  481. '┫',
  482. '┬',
  483. '┭',
  484. '┮',
  485. '┯',
  486. '┰',
  487. '┱',
  488. '┲',
  489. '┳',
  490. '┴',
  491. '┵',
  492. '┶',
  493. '┷',
  494. '┸',
  495. '┹',
  496. '┺',
  497. '┻',
  498. '┼',
  499. '┽',
  500. '┾',
  501. '┿',
  502. '╀',
  503. '╁',
  504. '╂',
  505. '╃',
  506. '╄',
  507. '╅',
  508. '╆',
  509. '╇',
  510. '╈',
  511. '╉',
  512. '╊',
  513. '╋',
  514. '╌',
  515. '╍',
  516. '╎',
  517. '╏',
  518. '═',
  519. '║',
  520. '╒',
  521. '╓',
  522. '╔',
  523. '╕',
  524. '╖',
  525. '╗',
  526. '╘',
  527. '╙',
  528. '╚',
  529. '╛',
  530. '╜',
  531. '╝',
  532. '╞',
  533. '╟',
  534. '╠',
  535. '╡',
  536. '╢',
  537. '╣',
  538. '╤',
  539. '╥',
  540. '╦',
  541. '╧',
  542. '╨',
  543. '╩',
  544. '╪',
  545. '╫',
  546. '╬',
  547. '╭',
  548. '╮',
  549. '╯',
  550. '╰',
  551. '╱',
  552. '╲',
  553. '╳',
  554. '╴',
  555. '╵',
  556. '╶',
  557. '╷',
  558. '╸',
  559. '╹',
  560. '╺',
  561. '╻',
  562. '╼',
  563. '╽',
  564. '╾',
  565. '╿',
  566. '▀',
  567. '▁',
  568. '▂',
  569. '▃',
  570. '▄',
  571. '▅',
  572. '▆',
  573. '▇',
  574. '█',
  575. '▉',
  576. '▊',
  577. '▋',
  578. '▌',
  579. '▍',
  580. '▎',
  581. '▏',
  582. '▐',
  583. '░',
  584. '▒',
  585. '▓',
  586. '▔',
  587. '▕',
  588. '▖',
  589. '▗',
  590. '▘',
  591. '▙',
  592. '▚',
  593. '▛',
  594. '▜',
  595. '▝',
  596. '▞',
  597. '▟',
  598. '◉',
  599. '○',
  600. '●',
  601. '◖',
  602. '◗',
  603. '◜',
  604. '◝',
  605. '◞',
  606. '◟',
  607. '◠',
  608. '◡',
  609. '◢',
  610. '◣',
  611. '◤',
  612. '◥',
  613. '⠀',
  614. '⠁',
  615. '⠂',
  616. '⠃',
  617. '⠄',
  618. '⠅',
  619. '⠆',
  620. '⠇',
  621. '⠈',
  622. '⠉',
  623. '⠊',
  624. '⠋',
  625. '⠌',
  626. '⠍',
  627. '⠎',
  628. '⠏',
  629. '⠐',
  630. '⠑',
  631. '⠒',
  632. '⠓',
  633. '⠔',
  634. '⠕',
  635. '⠖',
  636. '⠗',
  637. '⠘',
  638. '⠙',
  639. '⠚',
  640. '⠛',
  641. '⠜',
  642. '⠝',
  643. '⠞',
  644. '⠟',
  645. '⠠',
  646. '⠡',
  647. '⠢',
  648. '⠣',
  649. '⠤',
  650. '⠥',
  651. '⠦',
  652. '⠧',
  653. '⠨',
  654. '⠩',
  655. '⠪',
  656. '⠫',
  657. '⠬',
  658. '⠭',
  659. '⠮',
  660. '⠯',
  661. '⠰',
  662. '⠱',
  663. '⠲',
  664. '⠳',
  665. '⠴',
  666. '⠵',
  667. '⠶',
  668. '⠷',
  669. '⠸',
  670. '⠹',
  671. '⠺',
  672. '⠻',
  673. '⠼',
  674. '⠽',
  675. '⠾',
  676. '⠿',
  677. '⡀',
  678. '⡁',
  679. '⡂',
  680. '⡃',
  681. '⡄',
  682. '⡅',
  683. '⡆',
  684. '⡇',
  685. '⡈',
  686. '⡉',
  687. '⡊',
  688. '⡋',
  689. '⡌',
  690. '⡍',
  691. '⡎',
  692. '⡏',
  693. '⡐',
  694. '⡑',
  695. '⡒',
  696. '⡓',
  697. '⡔',
  698. '⡕',
  699. '⡖',
  700. '⡗',
  701. '⡘',
  702. '⡙',
  703. '⡚',
  704. '⡛',
  705. '⡜',
  706. '⡝',
  707. '⡞',
  708. '⡟',
  709. '⡠',
  710. '⡡',
  711. '⡢',
  712. '⡣',
  713. '⡤',
  714. '⡥',
  715. '⡦',
  716. '⡧',
  717. '⡨',
  718. '⡩',
  719. '⡪',
  720. '⡫',
  721. '⡬',
  722. '⡭',
  723. '⡮',
  724. '⡯',
  725. '⡰',
  726. '⡱',
  727. '⡲',
  728. '⡳',
  729. '⡴',
  730. '⡵',
  731. '⡶',
  732. '⡷',
  733. '⡸',
  734. '⡹',
  735. '⡺',
  736. '⡻',
  737. '⡼',
  738. '⡽',
  739. '⡾',
  740. '⡿',
  741. '⢀',
  742. '⢁',
  743. '⢂',
  744. '⢃',
  745. '⢄',
  746. '⢅',
  747. '⢆',
  748. '⢇',
  749. '⢈',
  750. '⢉',
  751. '⢊',
  752. '⢋',
  753. '⢌',
  754. '⢍',
  755. '⢎',
  756. '⢏',
  757. '⢐',
  758. '⢑',
  759. '⢒',
  760. '⢓',
  761. '⢔',
  762. '⢕',
  763. '⢖',
  764. '⢗',
  765. '⢘',
  766. '⢙',
  767. '⢚',
  768. '⢛',
  769. '⢜',
  770. '⢝',
  771. '⢞',
  772. '⢟',
  773. '⢠',
  774. '⢡',
  775. '⢢',
  776. '⢣',
  777. '⢤',
  778. '⢥',
  779. '⢦',
  780. '⢧',
  781. '⢨',
  782. '⢩',
  783. '⢪',
  784. '⢫',
  785. '⢬',
  786. '⢭',
  787. '⢮',
  788. '⢯',
  789. '⢰',
  790. '⢱',
  791. '⢲',
  792. '⢳',
  793. '⢴',
  794. '⢵',
  795. '⢶',
  796. '⢷',
  797. '⢸',
  798. '⢹',
  799. '⢺',
  800. '⢻',
  801. '⢼',
  802. '⢽',
  803. '⢾',
  804. '⢿',
  805. '⣀',
  806. '⣁',
  807. '⣂',
  808. '⣃',
  809. '⣄',
  810. '⣅',
  811. '⣆',
  812. '⣇',
  813. '⣈',
  814. '⣉',
  815. '⣊',
  816. '⣋',
  817. '⣌',
  818. '⣍',
  819. '⣎',
  820. '⣏',
  821. '⣐',
  822. '⣑',
  823. '⣒',
  824. '⣓',
  825. '⣔',
  826. '⣕',
  827. '⣖',
  828. '⣗',
  829. '⣘',
  830. '⣙',
  831. '⣚',
  832. '⣛',
  833. '⣜',
  834. '⣝',
  835. '⣞',
  836. '⣟',
  837. '⣠',
  838. '⣡',
  839. '⣢',
  840. '⣣',
  841. '⣤',
  842. '⣥',
  843. '⣦',
  844. '⣧',
  845. '⣨',
  846. '⣩',
  847. '⣪',
  848. '⣫',
  849. '⣬',
  850. '⣭',
  851. '⣮',
  852. '⣯',
  853. '⣰',
  854. '⣱',
  855. '⣲',
  856. '⣳',
  857. '⣴',
  858. '⣵',
  859. '⣶',
  860. '⣷',
  861. '⣸',
  862. '⣹',
  863. '⣺',
  864. '⣻',
  865. '⣼',
  866. '⣽',
  867. '⣾',
  868. '⣿',
  869. '\ue0b0',
  870. '\ue0b1',
  871. '\ue0b2',
  872. '\ue0b3',
  873. '\ue0b4',
  874. '\ue0b5',
  875. '\ue0b6',
  876. '\ue0b7',
  877. '\ue0b8',
  878. '\ue0b9',
  879. '\ue0ba',
  880. '\ue0bb',
  881. '\ue0bc',
  882. '\ue0bd',
  883. '\ue0be',
  884. '\ue0bf',
  885. '\ue0d6',
  886. '\ue0d7',
  887. '\uee00',
  888. '\uee01',
  889. '\uee02',
  890. '\uee03',
  891. '\uee04',
  892. '\uee05',
  893. '\uee06',
  894. '\uee07',
  895. '\uee08',
  896. '\uee09',
  897. '\uee0a',
  898. '\uee0b',
  899. '\uf5d0',
  900. '\uf5d1',
  901. '\uf5d2',
  902. '\uf5d3',
  903. '\uf5d4',
  904. '\uf5d5',
  905. '\uf5d6',
  906. '\uf5d7',
  907. '\uf5d8',
  908. '\uf5d9',
  909. '\uf5da',
  910. '\uf5db',
  911. '\uf5dc',
  912. '\uf5dd',
  913. '\uf5de',
  914. '\uf5df',
  915. '\uf5e0',
  916. '\uf5e1',
  917. '\uf5e2',
  918. '\uf5e3',
  919. '\uf5e4',
  920. '\uf5e5',
  921. '\uf5e6',
  922. '\uf5e7',
  923. '\uf5e8',
  924. '\uf5e9',
  925. '\uf5ea',
  926. '\uf5eb',
  927. '\uf5ec',
  928. '\uf5ed',
  929. '\uf5ee',
  930. '\uf5ef',
  931. '\uf5f0',
  932. '\uf5f1',
  933. '\uf5f2',
  934. '\uf5f3',
  935. '\uf5f4',
  936. '\uf5f5',
  937. '\uf5f6',
  938. '\uf5f7',
  939. '\uf5f8',
  940. '\uf5f9',
  941. '\uf5fa',
  942. '\uf5fb',
  943. '\uf5fc',
  944. '\uf5fd',
  945. '\uf5fe',
  946. '\uf5ff',
  947. '\uf600',
  948. '\uf601',
  949. '\uf602',
  950. '\uf603',
  951. '\uf604',
  952. '\uf605',
  953. '\uf606',
  954. '\uf607',
  955. '\uf608',
  956. '\uf609',
  957. '\uf60a',
  958. '\uf60b',
  959. '\uf60c',
  960. '\uf60d',
  961. '🬀',
  962. '🬁',
  963. '🬂',
  964. '🬃',
  965. '🬄',
  966. '🬅',
  967. '🬆',
  968. '🬇',
  969. '🬈',
  970. '🬉',
  971. '🬊',
  972. '🬋',
  973. '🬌',
  974. '🬍',
  975. '🬎',
  976. '🬏',
  977. '🬐',
  978. '🬑',
  979. '🬒',
  980. '🬓',
  981. '🬔',
  982. '🬕',
  983. '🬖',
  984. '🬗',
  985. '🬘',
  986. '🬙',
  987. '🬚',
  988. '🬛',
  989. '🬜',
  990. '🬝',
  991. '🬞',
  992. '🬟',
  993. '🬠',
  994. '🬡',
  995. '🬢',
  996. '🬣',
  997. '🬤',
  998. '🬥',
  999. '🬦',
  1000. '🬧',
  1001. '🬨',
  1002. '🬩',
  1003. '🬪',
  1004. '🬫',
  1005. '🬬',
  1006. '🬭',
  1007. '🬮',
  1008. '🬯',
  1009. '🬰',
  1010. '🬱',
  1011. '🬲',
  1012. '🬳',
  1013. '🬴',
  1014. '🬵',
  1015. '🬶',
  1016. '🬷',
  1017. '🬸',
  1018. '🬹',
  1019. '🬺',
  1020. '🬻',
  1021. '🬼',
  1022. '🬽',
  1023. '🬾',
  1024. '🬿',
  1025. '🭀',
  1026. '🭁',
  1027. '🭂',
  1028. '🭃',
  1029. '🭄',
  1030. '🭅',
  1031. '🭆',
  1032. '🭇',
  1033. '🭈',
  1034. '🭉',
  1035. '🭊',
  1036. '🭋',
  1037. '🭌',
  1038. '🭍',
  1039. '🭎',
  1040. '🭏',
  1041. '🭐',
  1042. '🭑',
  1043. '🭒',
  1044. '🭓',
  1045. '🭔',
  1046. '🭕',
  1047. '🭖',
  1048. '🭗',
  1049. '🭘',
  1050. '🭙',
  1051. '🭚',
  1052. '🭛',
  1053. '🭜',
  1054. '🭝',
  1055. '🭞',
  1056. '🭟',
  1057. '🭠',
  1058. '🭡',
  1059. '🭢',
  1060. '🭣',
  1061. '🭤',
  1062. '🭥',
  1063. '🭦',
  1064. '🭧',
  1065. '🭨',
  1066. '🭩',
  1067. '🭪',
  1068. '🭫',
  1069. '🭬',
  1070. '🭭',
  1071. '🭮',
  1072. '🭯',
  1073. '🭰',
  1074. '🭱',
  1075. '🭲',
  1076. '🭳',
  1077. '🭴',
  1078. '🭵',
  1079. '🭶',
  1080. '🭷',
  1081. '🭸',
  1082. '🭹',
  1083. '🭺',
  1084. '🭻',
  1085. '🭼',
  1086. '🭽',
  1087. '🭾',
  1088. '🭿',
  1089. '🮀',
  1090. '🮁',
  1091. '🮂',
  1092. '🮃',
  1093. '🮄',
  1094. '🮅',
  1095. '🮆',
  1096. '🮇',
  1097. '🮈',
  1098. '🮉',
  1099. '🮊',
  1100. '🮋',
  1101. '🮌',
  1102. '🮍',
  1103. '🮎',
  1104. '🮏',
  1105. '🮐',
  1106. '🮑',
  1107. '🮒',
  1108. '\U0001fb93',
  1109. '🮔',
  1110. '🮕',
  1111. '🮖',
  1112. '🮗',
  1113. '🮘',
  1114. '🮙',
  1115. '🮚',
  1116. '🮛',
  1117. '🮜',
  1118. '🮝',
  1119. '🮞',
  1120. '🮟',
  1121. '🮠',
  1122. '🮡',
  1123. '🮢',
  1124. '🮣',
  1125. '🮤',
  1126. '🮥',
  1127. '🮦',
  1128. '🮧',
  1129. '🮨',
  1130. '🮩',
  1131. '🮪',
  1132. '🮫',
  1133. '🮬',
  1134. '🮭',
  1135. '🮮',
  1136. '\U0001fbe6', '\U0001fbe7',
  1137. } # }}}
  1138. for ch in range(0x1cd00, 0x1cde5+1): # octants
  1139. box_chars.add(chr(ch))