backend.py 9.0 KB

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