backend.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. #!/usr/bin/env python
  2. # License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
  3. import json
  4. import os
  5. import string
  6. import sys
  7. import tempfile
  8. from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple, TypedDict
  9. from kitty.cli import create_default_opts
  10. from kitty.conf.utils import to_color
  11. from kitty.constants import kitten_exe
  12. from kitty.fonts import Descriptor
  13. from kitty.fonts.common import (
  14. face_from_descriptor,
  15. get_axis_map,
  16. get_font_files,
  17. get_named_style,
  18. get_variable_data_for_descriptor,
  19. get_variable_data_for_face,
  20. is_variable,
  21. spec_for_face,
  22. )
  23. from kitty.fonts.features import Type, known_features
  24. from kitty.fonts.list import create_family_groups
  25. from kitty.fonts.render import display_bitmap
  26. from kitty.options.types import Options
  27. from kitty.options.utils import parse_font_spec
  28. from kitty.typing import NotRequired
  29. from kitty.utils import screen_size_function
  30. if TYPE_CHECKING:
  31. from kitty.fast_data_types import FeatureData
  32. def setup_debug_print() -> bool:
  33. if 'KITTY_STDIO_FORWARDED' in os.environ:
  34. try:
  35. fd = int(os.environ['KITTY_STDIO_FORWARDED'])
  36. except Exception:
  37. return False
  38. try:
  39. sys.stdout = open(fd, 'w', closefd=False)
  40. return True
  41. except OSError:
  42. return False
  43. return False
  44. def send_to_kitten(x: Any) -> None:
  45. try:
  46. sys.__stdout__.buffer.write(json.dumps(x).encode())
  47. sys.__stdout__.buffer.write(b'\n')
  48. sys.__stdout__.buffer.flush()
  49. except BrokenPipeError:
  50. raise SystemExit('Pipe to kitten was broken while sending data to it')
  51. class TextStyle(TypedDict):
  52. font_size: float
  53. dpi_x: float
  54. dpi_y: float
  55. foreground: str
  56. background: str
  57. OptNames = Literal['font_family', 'bold_font', 'italic_font', 'bold_italic_font']
  58. FamilyKey = Tuple[OptNames, ...]
  59. def opts_from_cmd(cmd: Dict[str, Any]) -> Tuple[Options, FamilyKey, float, float]:
  60. opts = Options()
  61. ts: TextStyle = cmd['text_style']
  62. opts.font_size = ts['font_size']
  63. opts.foreground = to_color(ts['foreground'])
  64. opts.background = to_color(ts['background'])
  65. family_key = []
  66. def d(k: OptNames) -> None:
  67. if k in cmd:
  68. setattr(opts, k, parse_font_spec(cmd[k]))
  69. family_key.append(k)
  70. d('font_family')
  71. d('bold_font')
  72. d('italic_font')
  73. d('bold_italic_font')
  74. return opts, tuple(family_key), ts['dpi_x'], ts['dpi_y']
  75. BaseKey = Tuple[str, int, int]
  76. FaceKey = Tuple[str, BaseKey]
  77. RenderedSample = Tuple[bytes, Dict[str, Any]]
  78. RenderedSampleTransmit = Dict[str, Any]
  79. SAMPLE_TEXT = string.ascii_lowercase + ' ' + string.digits + ' ' + string.ascii_uppercase + ' ' + string.punctuation
  80. class FD(TypedDict):
  81. is_index: bool
  82. name: NotRequired[str]
  83. tooltip: NotRequired[str]
  84. sample: NotRequired[str]
  85. params: NotRequired[Tuple[str, ...]]
  86. def get_features(features: Dict[str, Optional['FeatureData']]) -> Dict[str, FD]:
  87. ans = {}
  88. for tag, data in features.items():
  89. kf = known_features.get(tag)
  90. if kf is None or kf.type is Type.hidden:
  91. continue
  92. fd: FD = {'is_index': kf.type is Type.index}
  93. ans[tag] = fd
  94. if data is not None:
  95. if n := data.get('name'):
  96. fd['name'] = n
  97. if n := data.get('tooltip'):
  98. fd['tooltip'] = n
  99. if n := data.get('sample'):
  100. fd['sample'] = n
  101. if p := data.get('params'):
  102. fd['params'] = p
  103. return ans
  104. def render_face_sample(font: Descriptor, opts: Options, dpi_x: float, dpi_y: float, width: int, height: int) -> RenderedSample:
  105. face = face_from_descriptor(font, opts.font_size, dpi_x, dpi_y)
  106. face.set_size(opts.font_size, dpi_x, dpi_y)
  107. metadata = {
  108. 'variable_data': get_variable_data_for_face(face),
  109. 'style': font['style'],
  110. 'psname': face.postscript_name(),
  111. 'features': get_features(face.get_features()),
  112. 'applied_features': face.applied_features(),
  113. 'spec': spec_for_face(font['family'], face).as_setting,
  114. 'cell_width': 0, 'cell_height': 0, 'canvas_height': 0, 'canvas_width': width,
  115. }
  116. if is_variable(font):
  117. ns = get_named_style(face)
  118. if ns:
  119. metadata['variable_named_style'] = ns
  120. metadata['variable_axis_map'] = get_axis_map(face)
  121. bitmap, cell_width, cell_height = face.render_sample_text(SAMPLE_TEXT, width, height, opts.foreground.rgb)
  122. metadata['cell_width'] = cell_width
  123. metadata['cell_height'] = cell_height
  124. metadata['canvas_height'] = len(bitmap) // (4 *width)
  125. return bitmap, metadata
  126. def render_family_sample(
  127. opts: Options, family_key: FamilyKey, dpi_x: float, dpi_y: float, width: int, height: int, output_dir: str,
  128. cache: Dict[FaceKey, RenderedSampleTransmit]
  129. ) -> Dict[str, RenderedSampleTransmit]:
  130. base_key: BaseKey = opts.font_family.created_from_string, width, height
  131. ans: Dict[str, RenderedSampleTransmit] = {}
  132. font_files = get_font_files(opts)
  133. for x in family_key:
  134. key: FaceKey = x + ': ' + str(getattr(opts, x)), base_key
  135. if x == 'font_family':
  136. desc = font_files['medium']
  137. elif x == 'bold_font':
  138. desc = font_files['bold']
  139. elif x == 'italic_font':
  140. desc = font_files['italic']
  141. elif x == 'bold_italic_font':
  142. desc = font_files['bi']
  143. cached = cache.get(key)
  144. if cached is not None:
  145. ans[x] = cached
  146. else:
  147. with tempfile.NamedTemporaryFile(delete=False, suffix='.rgba', dir=output_dir) as tf:
  148. bitmap, metadata = render_face_sample(desc, opts, dpi_x, dpi_y, width, height)
  149. tf.write(bitmap)
  150. metadata['path'] = tf.name
  151. cache[key] = ans[x] = metadata
  152. return ans
  153. ResolvedFace = Dict[Literal['family', 'spec', 'setting'], str]
  154. def spec_for_descriptor(d: Descriptor, font_size: float) -> str:
  155. face = face_from_descriptor(d, font_size, 288, 288)
  156. return spec_for_face(d['family'], face).as_setting
  157. def resolved_faces(opts: Options) -> Dict[OptNames, ResolvedFace]:
  158. font_files = get_font_files(opts)
  159. ans: Dict[OptNames, ResolvedFace] = {}
  160. def d(key: Literal['medium', 'bold', 'italic', 'bi'], opt_name: OptNames) -> None:
  161. descriptor = font_files[key]
  162. ans[opt_name] = {
  163. 'family': descriptor['family'], 'spec': spec_for_descriptor(descriptor, opts.font_size),
  164. 'setting': getattr(opts, opt_name).created_from_string
  165. }
  166. d('medium', 'font_family')
  167. d('bold', 'bold_font')
  168. d('italic', 'italic_font')
  169. d('bi', 'bold_italic_font')
  170. return ans
  171. def main() -> None:
  172. setup_debug_print()
  173. cache: Dict[FaceKey, RenderedSampleTransmit] = {}
  174. for line in sys.stdin.buffer:
  175. cmd = json.loads(line)
  176. action = cmd.get('action', '')
  177. if action == 'list_monospaced_fonts':
  178. opts = create_default_opts()
  179. send_to_kitten({'fonts': create_family_groups(), 'resolved_faces': resolved_faces(opts)})
  180. elif action == 'read_variable_data':
  181. ans = []
  182. for descriptor in cmd['descriptors']:
  183. ans.append(get_variable_data_for_descriptor(descriptor))
  184. send_to_kitten(ans)
  185. elif action == 'render_family_samples':
  186. opts, family_key, dpi_x, dpi_y = opts_from_cmd(cmd)
  187. send_to_kitten(render_family_sample(opts, family_key, dpi_x, dpi_y, cmd['width'], cmd['height'], cmd['output_dir'], cache))
  188. else:
  189. raise SystemExit(f'Unknown action: {action}')
  190. def query_kitty() -> Dict[str, str]:
  191. import subprocess
  192. ans = {}
  193. for line in subprocess.check_output([kitten_exe(), 'query-terminal']).decode().splitlines():
  194. k, sep, v = line.partition(':')
  195. if sep == ':':
  196. ans[k] = v.strip()
  197. return ans
  198. def showcase(family: str = 'family="Fira Code"') -> None:
  199. q = query_kitty()
  200. opts = Options()
  201. opts.foreground = to_color(q['foreground'])
  202. opts.background = to_color(q['background'])
  203. opts.font_size = float(q['font_size'])
  204. opts.font_family = parse_font_spec(family)
  205. font_files = get_font_files(opts)
  206. desc = font_files['medium']
  207. ss = screen_size_function()()
  208. width = ss.cell_width * ss.cols
  209. height = 5 * ss.cell_height
  210. bitmap, m = render_face_sample(desc, opts, float(q['dpi_x']), float(q['dpi_y']), width, height)
  211. display_bitmap(bitmap, m['canvas_width'], m['canvas_height'])
  212. def test_render(spec: str = 'family="Fira Code"', width: int = 1560, height: int = 116, font_size: float = 12, dpi: float = 288) -> None:
  213. opts = Options()
  214. opts.font_family = parse_font_spec(spec)
  215. opts.font_size = font_size
  216. opts.foreground = to_color('white')
  217. desc = get_font_files(opts)['medium']
  218. bitmap, m = render_face_sample(desc, opts, float(dpi), float(dpi), width, height)
  219. display_bitmap(bitmap, m['canvas_width'], m['canvas_height'])