123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- #!/usr/bin/env python
- # License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
- import json
- import os
- import string
- import sys
- import tempfile
- from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple, TypedDict
- from kitty.cli import create_default_opts
- from kitty.conf.utils import to_color
- from kitty.constants import kitten_exe
- from kitty.fonts import Descriptor
- from kitty.fonts.common import (
- face_from_descriptor,
- get_axis_map,
- get_font_files,
- get_named_style,
- get_variable_data_for_descriptor,
- get_variable_data_for_face,
- is_variable,
- spec_for_face,
- )
- from kitty.fonts.features import Type, known_features
- from kitty.fonts.list import create_family_groups
- from kitty.fonts.render import display_bitmap
- from kitty.options.types import Options
- from kitty.options.utils import parse_font_spec
- from kitty.typing import NotRequired
- from kitty.utils import screen_size_function
- if TYPE_CHECKING:
- from kitty.fast_data_types import FeatureData
- def setup_debug_print() -> bool:
- if 'KITTY_STDIO_FORWARDED' in os.environ:
- try:
- fd = int(os.environ['KITTY_STDIO_FORWARDED'])
- except Exception:
- return False
- try:
- sys.stdout = open(fd, 'w', closefd=False)
- return True
- except OSError:
- return False
- return False
- def send_to_kitten(x: Any) -> None:
- f = sys.__stdout__
- assert f is not None
- try:
- f.buffer.write(json.dumps(x).encode())
- f.buffer.write(b'\n')
- f.buffer.flush()
- except BrokenPipeError:
- raise SystemExit('Pipe to kitten was broken while sending data to it')
- class TextStyle(TypedDict):
- font_size: float
- dpi_x: float
- dpi_y: float
- foreground: str
- background: str
- OptNames = Literal['font_family', 'bold_font', 'italic_font', 'bold_italic_font']
- FamilyKey = Tuple[OptNames, ...]
- def opts_from_cmd(cmd: Dict[str, Any]) -> Tuple[Options, FamilyKey, float, float]:
- opts = Options()
- ts: TextStyle = cmd['text_style']
- opts.font_size = ts['font_size']
- opts.foreground = to_color(ts['foreground'])
- opts.background = to_color(ts['background'])
- family_key = []
- def d(k: OptNames) -> None:
- if k in cmd:
- setattr(opts, k, parse_font_spec(cmd[k]))
- family_key.append(k)
- d('font_family')
- d('bold_font')
- d('italic_font')
- d('bold_italic_font')
- return opts, tuple(family_key), ts['dpi_x'], ts['dpi_y']
- BaseKey = Tuple[str, int, int]
- FaceKey = Tuple[str, BaseKey]
- RenderedSample = Tuple[bytes, Dict[str, Any]]
- RenderedSampleTransmit = Dict[str, Any]
- SAMPLE_TEXT = string.ascii_lowercase + ' ' + string.digits + ' ' + string.ascii_uppercase + ' ' + string.punctuation
- class FD(TypedDict):
- is_index: bool
- name: NotRequired[str]
- tooltip: NotRequired[str]
- sample: NotRequired[str]
- params: NotRequired[Tuple[str, ...]]
- def get_features(features: Dict[str, Optional['FeatureData']]) -> Dict[str, FD]:
- ans = {}
- for tag, data in features.items():
- kf = known_features.get(tag)
- if kf is None or kf.type is Type.hidden:
- continue
- fd: FD = {'is_index': kf.type is Type.index}
- ans[tag] = fd
- if data is not None:
- if n := data.get('name'):
- fd['name'] = n
- if n := data.get('tooltip'):
- fd['tooltip'] = n
- if n := data.get('sample'):
- fd['sample'] = n
- if p := data.get('params'):
- fd['params'] = p
- return ans
- def render_face_sample(font: Descriptor, opts: Options, dpi_x: float, dpi_y: float, width: int, height: int, sample_text: str = '') -> RenderedSample:
- face = face_from_descriptor(font, opts.font_size, dpi_x, dpi_y)
- face.set_size(opts.font_size, dpi_x, dpi_y)
- metadata = {
- 'variable_data': get_variable_data_for_face(face),
- 'style': font['style'],
- 'psname': face.postscript_name(),
- 'features': get_features(face.get_features()),
- 'applied_features': face.applied_features(),
- 'spec': spec_for_face(font['family'], face).as_setting,
- 'cell_width': 0, 'cell_height': 0, 'canvas_height': 0, 'canvas_width': width,
- }
- if is_variable(font):
- ns = get_named_style(face)
- if ns:
- metadata['variable_named_style'] = ns
- metadata['variable_axis_map'] = get_axis_map(face)
- bitmap, cell_width, cell_height = face.render_sample_text(sample_text or SAMPLE_TEXT, width, height, opts.foreground.rgb)
- metadata['cell_width'] = cell_width
- metadata['cell_height'] = cell_height
- metadata['canvas_height'] = len(bitmap) // (4 *width)
- return bitmap, metadata
- def render_family_sample(
- opts: Options, family_key: FamilyKey, dpi_x: float, dpi_y: float, width: int, height: int, output_dir: str,
- cache: Dict[FaceKey, RenderedSampleTransmit]
- ) -> Dict[str, RenderedSampleTransmit]:
- base_key: BaseKey = opts.font_family.created_from_string, width, height
- ans: Dict[str, RenderedSampleTransmit] = {}
- font_files = get_font_files(opts)
- for x in family_key:
- key: FaceKey = x + ': ' + str(getattr(opts, x)), base_key
- if x == 'font_family':
- desc = font_files['medium']
- elif x == 'bold_font':
- desc = font_files['bold']
- elif x == 'italic_font':
- desc = font_files['italic']
- elif x == 'bold_italic_font':
- desc = font_files['bi']
- cached = cache.get(key)
- if cached is not None:
- ans[x] = cached
- else:
- with tempfile.NamedTemporaryFile(delete=False, suffix='.rgba', dir=output_dir) as tf:
- bitmap, metadata = render_face_sample(desc, opts, dpi_x, dpi_y, width, height)
- tf.write(bitmap)
- metadata['path'] = tf.name
- cache[key] = ans[x] = metadata
- return ans
- ResolvedFace = Dict[Literal['family', 'spec', 'setting'], str]
- def spec_for_descriptor(d: Descriptor, font_size: float) -> str:
- face = face_from_descriptor(d, font_size, 288, 288)
- return spec_for_face(d['family'], face).as_setting
- def resolved_faces(opts: Options) -> Dict[OptNames, ResolvedFace]:
- font_files = get_font_files(opts)
- ans: Dict[OptNames, ResolvedFace] = {}
- def d(key: Literal['medium', 'bold', 'italic', 'bi'], opt_name: OptNames) -> None:
- descriptor = font_files[key]
- ans[opt_name] = {
- 'family': descriptor['family'], 'spec': spec_for_descriptor(descriptor, opts.font_size),
- 'setting': getattr(opts, opt_name).created_from_string
- }
- d('medium', 'font_family')
- d('bold', 'bold_font')
- d('italic', 'italic_font')
- d('bi', 'bold_italic_font')
- return ans
- def main() -> None:
- setup_debug_print()
- cache: Dict[FaceKey, RenderedSampleTransmit] = {}
- for line in sys.stdin.buffer:
- cmd = json.loads(line)
- action = cmd.get('action', '')
- if action == 'list_monospaced_fonts':
- opts = create_default_opts()
- send_to_kitten({'fonts': create_family_groups(), 'resolved_faces': resolved_faces(opts)})
- elif action == 'read_variable_data':
- ans = []
- for descriptor in cmd['descriptors']:
- ans.append(get_variable_data_for_descriptor(descriptor))
- send_to_kitten(ans)
- elif action == 'render_family_samples':
- opts, family_key, dpi_x, dpi_y = opts_from_cmd(cmd)
- send_to_kitten(render_family_sample(opts, family_key, dpi_x, dpi_y, cmd['width'], cmd['height'], cmd['output_dir'], cache))
- else:
- raise SystemExit(f'Unknown action: {action}')
- def query_kitty() -> Dict[str, str]:
- import subprocess
- ans = {}
- for line in subprocess.check_output([kitten_exe(), 'query-terminal']).decode().splitlines():
- k, sep, v = line.partition(':')
- if sep == ':':
- ans[k] = v.strip()
- return ans
- def showcase(family: str = 'family="Fira Code"', sample_text: str = '') -> None:
- q = query_kitty()
- opts = Options()
- opts.foreground = to_color(q['foreground'])
- opts.background = to_color(q['background'])
- opts.font_size = float(q['font_size'])
- opts.font_family = parse_font_spec(family)
- font_files = get_font_files(opts)
- desc = font_files['medium']
- ss = screen_size_function()()
- width = ss.cell_width * ss.cols
- height = 5 * ss.cell_height
- bitmap, m = render_face_sample(desc, opts, float(q['dpi_x']), float(q['dpi_y']), width, height, sample_text=sample_text)
- display_bitmap(bitmap, m['canvas_width'], m['canvas_height'])
- def test_render(spec: str = 'family="Fira Code"', width: int = 1560, height: int = 116, font_size: float = 12, dpi: float = 288) -> None:
- opts = Options()
- opts.font_family = parse_font_spec(spec)
- opts.font_size = font_size
- opts.foreground = to_color('white')
- desc = get_font_files(opts)['medium']
- bitmap, m = render_face_sample(desc, opts, float(dpi), float(dpi), width, height)
- display_bitmap(bitmap, m['canvas_width'], m['canvas_height'])
|