Work on rendering sample text for a font

This commit is contained in:
Kovid Goyal
2024-05-08 12:11:58 +05:30
parent 65b790df40
commit 41869d88c2
9 changed files with 202 additions and 37 deletions

View File

@@ -2,11 +2,16 @@
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
import json
import string
import sys
from typing import Any
from typing import Any, Dict, Tuple, TypedDict
from kitty.fonts.common import get_variable_data_for_descriptor
from kitty.conf.utils import to_color
from kitty.fonts import Descriptor
from kitty.fonts.common import face_from_descriptor, get_font_files, get_variable_data_for_descriptor
from kitty.fonts.list import create_family_groups
from kitty.options.types import Options
from kitty.options.utils import parse_font_spec
def send_to_kitten(x: Any) -> None:
@@ -15,7 +20,77 @@ def send_to_kitten(x: Any) -> None:
sys.stdout.buffer.flush()
class TextStyle(TypedDict):
font_size: float
dpi_x: float
dpi_y: float
foreground: str
background: str
FamilyKey = Tuple[str, ...]
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 = []
if 'font_family' in cmd:
opts.font_family = parse_font_spec(cmd['font_family'])
family_key.append(cmd['font_family'])
if 'bold_font' in cmd:
opts.bold_font = parse_font_spec(cmd['bold_font'])
family_key.append(cmd['bold_font'])
if 'italic_font' in cmd:
opts.italic_font = parse_font_spec(cmd['italic_font'])
family_key.append(cmd['italic_font'])
if 'bold_italic_font' in cmd:
opts.bold_italic_font = parse_font_spec(cmd['bold_italic_font'])
family_key.append(cmd['bold_italic_font'])
return opts, tuple(family_key), ts['dpi_x'], ts['dpi_y']
BaseKey = Tuple[FamilyKey, int, int]
FaceKey = Tuple[str, BaseKey]
SAMPLE_TEXT = string.ascii_lowercase + string.digits + string.ascii_uppercase + ' ' + string.punctuation
def render_face_sample(font: Descriptor, opts: Options, dpi_x: float, dpi_y: float, width: int, height: int, output_dir: str) -> str:
face = face_from_descriptor(font)
face.set_size(opts.font_size, dpi_x, dpi_y)
face.render_sample_text(SAMPLE_TEXT, width, height, opts.foreground.rgb)
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, str]
) -> Dict[str, str]:
base_key: BaseKey = family_key, width, height
ans: Dict[str, str] = {}
font_files = get_font_files(opts)
for x in family_key:
key: FaceKey = 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:
cache[key] = ans[x] = render_face_sample(desc, opts, dpi_x, dpi_y, width, height, output_dir)
return ans
def main() -> None:
cache: Dict[FaceKey, str] = {}
for line in sys.stdin.buffer:
cmd = json.loads(line)
action = cmd.get('action', '')
@@ -26,5 +101,8 @@ def main() -> None:
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}')

View File

@@ -24,6 +24,14 @@ const (
CHOOSING_FACES
)
type TextStyle struct {
Font_sz float64 `json:"font_size"`
Dpi_x float64 `json:"dpi_x"`
Dpi_y float64 `json:"dpi_y"`
Foreground string `json:"foreground"`
Background string `json:"background"`
}
type handler struct {
lp *loop.Loop
fonts map[string][]ListedFont
@@ -33,10 +41,7 @@ type handler struct {
mouse_state tui.MouseState
render_count uint
render_lines tui.RenderLines
text_style struct {
font_sz, dpi_x, dpi_y float64
foreground, background string
}
text_style TextStyle
// Listing
rl *readline.Readline
@@ -295,21 +300,21 @@ func (h *handler) on_query_response(key, val string, valid bool) error {
}
switch key {
case "font_size":
if err := set_float(key, val, &h.text_style.font_sz); err != nil {
if err := set_float(key, val, &h.text_style.Font_sz); err != nil {
return err
}
case "dpi_x":
if err := set_float(key, val, &h.text_style.dpi_x); err != nil {
if err := set_float(key, val, &h.text_style.Dpi_x); err != nil {
return err
}
case "dpi_y":
if err := set_float(key, val, &h.text_style.dpi_y); err != nil {
if err := set_float(key, val, &h.text_style.Dpi_y); err != nil {
return err
}
case "foreground":
h.text_style.foreground = val
h.text_style.Foreground = val
case "background":
h.text_style.background = val
h.text_style.Background = val
}
return nil
}

View File

@@ -416,14 +416,18 @@ get_glyph_width(PyObject *s, glyph_index g) {
}
static float
scaled_point_sz(FONTS_DATA_HANDLE fg) {
return ((fg->logical_dpi_x + fg->logical_dpi_y) / 144.0) * fg->font_sz_in_pts;
_scaled_point_sz(double font_sz_in_pts, double dpi_x, double dpi_y) {
return ((dpi_x + dpi_y) / 144.0) * font_sz_in_pts;
}
bool
set_size_for_face(PyObject *s, unsigned int UNUSED desired_height, bool force, FONTS_DATA_HANDLE fg) {
CTFace *self = (CTFace*)s;
float sz = scaled_point_sz(fg);
static float
scaled_point_sz(FONTS_DATA_HANDLE fg) {
return _scaled_point_sz(fg->font_sz_in_pts, fg->logical_dpi_x, fg->logical_dpi_y);
}
static bool
_set_size_for_face(CTFace *self, bool force, double font_sz_in_pts, double dpi_x, double dpi_y) {
float sz = _scaled_point_sz(font_sz_in_pts, dpi_x, dpi_y);
if (!force && self->scaled_point_sz == sz) return true;
RAII_CoreFoundation(CTFontRef, new_font, CTFontCreateCopyWithAttributes(self->ct_font, sz, NULL, NULL));
if (new_font == NULL) fatal("Out of memory");
@@ -431,6 +435,20 @@ set_size_for_face(PyObject *s, unsigned int UNUSED desired_height, bool force, F
return true;
}
bool
set_size_for_face(PyObject *s, unsigned int UNUSED desired_height, bool force, FONTS_DATA_HANDLE fg) {
CTFace *self = (CTFace*)s;
return _set_size_for_face(self, force, fg->font_sz_in_pts, fg->logical_dpi_x, fg->logical_dpi_y);
}
static PyObject*
set_size(CTFace *self, PyObject *args) {
double font_sz_in_pts, dpi_x, dpi_y;
if (!PyArg_ParseTuple(args, "ddd", &font_sz_in_pts, &dpi_x, &dpi_y)) return NULL;
if (!_set_size_for_face(self, false, font_sz_in_pts, dpi_x, dpi_y)) return NULL;
Py_RETURN_NONE;
}
hb_font_t*
harfbuzz_font_for_face(PyObject* s) {
CTFace *self = (CTFace*)s;
@@ -777,7 +795,7 @@ do_render(CTFontRef ct_font, unsigned int units_per_em, bool bold, bool italic,
} else {
render_glyphs(ct_font, canvas_width, cell_height, baseline, num_glyphs);
Region src = {.bottom=cell_height, .right=canvas_width}, dest = {.bottom=cell_height, .right=canvas_width};
render_alpha_mask(buffers.render_buf, canvas, &src, &dest, canvas_width, canvas_width);
render_alpha_mask(buffers.render_buf, canvas, &src, &dest, canvas_width, canvas_width, 0xffffff);
}
if (num_cells && (center_glyph || (num_cells == 2 && *was_colored))) {
if (debug_rendering) printf("centering glyphs: center_glyph: %d\n", center_glyph);
@@ -860,6 +878,7 @@ static PyMethodDef methods[] = {
METHODB(postscript_name, METH_NOARGS),
METHODB(get_variable_data, METH_NOARGS),
METHODB(identify_for_debug, METH_NOARGS),
METHODB(set_size, METH_VARARGS),
METHODB(get_best_name, METH_O),
{NULL} /* Sentinel */
};

View File

@@ -426,6 +426,7 @@ class Face:
def get_variable_data(self) -> VariableData: ...
def identify_for_debug(self) -> str: ...
def postscript_name(self) -> str: ...
def set_size(self, sz_in_pts: float, dpi_x: float, dpi_y: float) -> None: ...
class CoreTextFont(TypedDict):
@@ -457,6 +458,7 @@ class CTFace:
def get_variable_data(self) -> VariableData: ...
def identify_for_debug(self) -> str: ...
def postscript_name(self) -> str: ...
def set_size(self, sz_in_pts: float, dpi_x: float, dpi_y: float) -> None: ...
def coretext_all_fonts(monospaced_only: bool) -> Tuple[CoreTextFont, ...]:

View File

@@ -631,14 +631,15 @@ END_ALLOW_CASE_RANGE
static PyObject* box_drawing_function = NULL, *prerender_function = NULL, *descriptor_for_idx = NULL;
void
render_alpha_mask(const uint8_t *alpha_mask, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride) {
render_alpha_mask(const uint8_t *alpha_mask, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride, pixel color_rgb) {
const pixel col = color_rgb << 8;
for (size_t sr = src_rect->top, dr = dest_rect->top; sr < src_rect->bottom && dr < dest_rect->bottom; sr++, dr++) {
pixel *d = dest + dest_stride * dr;
const uint8_t *s = alpha_mask + src_stride * sr;
for(size_t sc = src_rect->left, dc = dest_rect->left; sc < src_rect->right && dc < dest_rect->right; sc++, dc++) {
uint8_t src_alpha = d[dc] & 0xff;
uint8_t alpha = s[sc];
d[dc] = 0xffffff00 | MAX(alpha, src_alpha);
d[dc] = col | MAX(alpha, src_alpha);
}
}
}
@@ -662,7 +663,7 @@ render_box_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell) {
uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(ret, 0));
ensure_canvas_can_fit(fg, 1);
Region r = { .right = fg->cell_width, .bottom = fg->cell_height };
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width);
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width, 0xffffff);
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sp->x, sp->y, sp->z, fg->canvas.buf);
Py_DECREF(ret);
}
@@ -1465,7 +1466,7 @@ send_prerendered_sprites(FontGroup *fg) {
uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(cell_addresses, i));
ensure_canvas_can_fit(fg, 1); // clear canvas
Region r = { .right = fg->cell_width, .bottom = fg->cell_height };
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width);
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width, 0xffffff);
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, x, y, z, fg->canvas.buf);
}
Py_CLEAR(args);

View File

@@ -35,7 +35,7 @@ bool face_equals_descriptor(PyObject *face_, PyObject *descriptor);
const char* postscript_name_for_face(const PyObject*);
void sprite_tracker_current_layout(FONTS_DATA_HANDLE data, unsigned int *x, unsigned int *y, unsigned int *z);
void render_alpha_mask(const uint8_t *alpha_mask, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride);
void render_alpha_mask(const uint8_t *alpha_mask, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride, pixel color_rgb);
void render_line(FONTS_DATA_HANDLE, Line *line, index_type lnum, Cursor *cursor, DisableLigature);
void sprite_tracker_set_limits(size_t max_texture_size, size_t max_array_len);
typedef void (*free_extra_data_func)(void*);

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Sequence, Union
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Sequence, TypedDict, Union
from kitty.constants import is_macos
from kitty.options.types import Options
@@ -14,14 +14,14 @@ if TYPE_CHECKING:
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map']
FontMap = Dict[FontCollectionMapType, Dict[str, List[Descriptor]]]
def Face(descriptor: Descriptor) -> Union[FT_Face, CTFace]:
pass
Face = Union[FT_Face, CTFace]
def all_fonts_map(monospaced: bool) -> FontMap: ...
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer: ...
def find_best_match(
family: str, bold: bool = False, italic: bool = False, monospaced: bool = True, ignore_face: Optional[Descriptor] = None
) -> Descriptor: ...
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> Descriptor: ...
def face_from_descriptor(descriptor: Descriptor) -> Face: ...
else:
FontCollectionMapType = FontMap = None
if is_macos:
@@ -32,6 +32,7 @@ else:
from kitty.fast_data_types import Face
from .fontconfig import all_fonts_map, create_scorer, find_best_match, find_last_resort_text_font
def face_from_descriptor(descriptor: Descriptor) -> Face: return Face(descriptor=descriptor)
cache_for_variable_data_by_path: Dict[str, VariableData] = {}
@@ -40,10 +41,10 @@ attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, T
def get_variable_data_for_descriptor(d: Descriptor) -> VariableData:
if not d['path']:
return Face(descriptor=d).get_variable_data()
return face_from_descriptor(d).get_variable_data()
ans = cache_for_variable_data_by_path.get(d['path'])
if ans is None:
ans = cache_for_variable_data_by_path[d['path']] = Face(descriptor=d).get_variable_data()
ans = cache_for_variable_data_by_path[d['path']] = face_from_descriptor(d).get_variable_data()
return ans
@@ -92,7 +93,7 @@ def apply_variation_to_pattern(pat: Descriptor, spec: FontSpec) -> Descriptor:
if not pat['variable']:
return pat
vd = Face(descriptor=pat).get_variable_data()
vd = face_from_descriptor(pat).get_variable_data()
if spec.style:
q = spec.style.lower()
for i, ns in enumerate(vd['named_styles']):
@@ -148,7 +149,14 @@ def get_font_from_spec(
return find_best_match(family, bold, italic, ignore_face=resolved_medium_font)
def get_font_files(opts: Options) -> Dict[str, Descriptor]:
class FontFiles(TypedDict):
medium: Descriptor
bold: Descriptor
italic: Descriptor
bi: Descriptor
def get_font_files(opts: Options) -> FontFiles:
ans: Dict[str, Descriptor] = {}
medium_font = get_font_from_spec(opts.font_family)
kd = {(False, False): 'medium', (True, False): 'bold', (False, True): 'italic', (True, True): 'bi'}
@@ -159,4 +167,4 @@ def get_font_files(opts: Options) -> Dict[str, Descriptor]:
font = medium_font
key = kd[(bold, italic)]
ans[key] = font
return ans
return {'medium': ans['medium'], 'bold': ans['bold'], 'italic': ans['italic'], 'bi': ans['bi']}

View File

@@ -5,7 +5,7 @@ import ctypes
import sys
from functools import partial
from math import ceil, cos, floor, pi
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Tuple, Union, cast
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Literal, Optional, Tuple, Union, cast
from kitty.constants import is_macos
from kitty.fast_data_types import (
@@ -176,7 +176,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
sz = override_font_size or opts.font_size
font_map = get_font_files(opts)
current_faces = [(font_map['medium'], False, False)]
ftypes = 'bold italic bi'.split()
ftypes: List[Literal['bold', 'italic', 'bi']] = ['bold', 'italic', 'bi']
indices = {k: 0 for k in ftypes}
for k in ftypes:
if k in font_map:

View File

@@ -197,6 +197,18 @@ set_size_for_face(PyObject *s, unsigned int desired_height, bool force, FONTS_DA
return set_font_size(self, w, w, xdpi, ydpi, desired_height, fg->cell_height);
}
static PyObject*
set_size(Face *self, PyObject *args) {
double font_sz_in_pts, dpi_x, dpi_y;
if (!PyArg_ParseTuple(args, "ddd", &font_sz_in_pts, &dpi_x, &dpi_y)) return NULL;
FT_F26Dot6 w = (FT_F26Dot6)(ceil(font_sz_in_pts * 64.0));
FT_UInt xdpi = (FT_UInt)dpi_x, ydpi = (FT_UInt)dpi_y;
if (self->char_width == w && self->char_height == w && self->xdpi == xdpi && self->ydpi == ydpi) { Py_RETURN_NONE; }
self->size_in_pts = (float)font_sz_in_pts;
if (!set_font_size(self, w, w, xdpi, ydpi, 0, 0)) return NULL;
Py_RETURN_NONE;
}
static bool
init_ft_face(Face *self, PyObject *path, int hinting, int hintstyle, FONTS_DATA_HANDLE fg) {
#define CPY(n) self->n = self->face->n;
@@ -621,7 +633,7 @@ copy_color_bitmap(uint8_t *src, pixel* dest, Region *src_rect, Region *dest_rect
static const bool debug_placement = false;
static void
place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size_t cell_height, float x_offset, float y_offset, size_t baseline, unsigned int glyph_num) {
place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size_t cell_height, float x_offset, float y_offset, size_t baseline, unsigned int glyph_num, pixel fg_rgb) {
// We want the glyph to be positioned inside the cell based on the bearingX
// and bearingY values, making sure that it does not overflow the cell.
@@ -650,7 +662,7 @@ place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size
if (bm->pixel_mode == FT_PIXEL_MODE_BGRA) {
copy_color_bitmap(bm->buf, cell, &src, &dest, bm->stride, cell_width);
} else render_alpha_mask(bm->buf, cell, &src, &dest, bm->stride, cell_width);
} else render_alpha_mask(bm->buf, cell, &src, &dest, bm->stride, cell_width, fg_rgb);
}
static const ProcessedBitmap EMPTY_PBM = {.factor = 1};
@@ -686,7 +698,7 @@ render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *inf
y = (float)positions[i].y_offset / 64.0f;
if (debug_placement) printf("%d: x=%f canvas: %u", i, x_offset, canvas_width);
if ((*was_colored || self->face->glyph->metrics.width > 0) && bm.width > 0) {
place_bitmap_in_canvas(canvas, &bm, canvas_width, cell_height, x_offset, y, baseline, i);
place_bitmap_in_canvas(canvas, &bm, canvas_width, cell_height, x_offset, y, baseline, i, 0xffffff);
}
if (debug_placement) printf(" adv: %f\n", (float)positions[i].x_advance / 64.0f);
// the roundf() below is needed for infinite length ligatures, for a test case
@@ -866,7 +878,7 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
pbm = EMPTY_PBM;
populate_processed_bitmap(self->face->glyph, bitmap, &pbm, false);
place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, pen_x, 0, baseline, n);
place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, pen_x, 0, baseline, n, 0xffffff);
pen_x += self->face->glyph->advance.x >> 6;
}
ans.width = pen_x; ans.height = canvas_height;
@@ -882,6 +894,44 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
return ans;
}
static PyObject*
render_sample_text(Face *self, PyObject *args) {
unsigned long canvas_width, canvas_height, pen_x = 0, pen_y = 0;
unsigned long fg = 0xffffff;
PyObject *ptext;
if (!PyArg_ParseTuple(args, "Ukk|k", &ptext, &canvas_width, &canvas_height, &fg)) return NULL;
RAII_PyObject(pbuf, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_width * canvas_height));
if (!pbuf) return NULL;
unsigned int cell_width, cell_height, baseline, underline_position, underline_thickness, strikethrough_position, strikethrough_thickness;
cell_metrics((PyObject*)self, &cell_width, &cell_height, &baseline, &underline_position, &underline_thickness, &strikethrough_position, &strikethrough_thickness);
pixel *canvas = (pixel*)PyBytes_AS_STRING(pbuf);
if (cell_width > canvas_width) goto end;
for (ssize_t n = 0; n < PyUnicode_GET_LENGTH(ptext); n++) {
if (pen_x + cell_width > canvas_width) {
pen_y += cell_height;
pen_x = 0;
}
if (pen_y + cell_height > canvas_height) break;
Py_UCS4 ch = PyUnicode_READ_CHAR(ptext, n);
FT_UInt glyph_index = FT_Get_Char_Index(self->face, ch);
if (!glyph_index) continue;
int error = FT_Load_Glyph(self->face, glyph_index, FT_LOAD_DEFAULT);
if (error) continue;
error = FT_Render_Glyph(self->face->glyph, FT_RENDER_MODE_NORMAL);
if (error) continue;
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
ProcessedBitmap pbm = EMPTY_PBM;
populate_processed_bitmap(self->face->glyph, bitmap, &pbm, false);
place_bitmap_in_canvas(canvas, &pbm, cell_width, cell_height, pen_x, pen_y, baseline, 0, fg);
pen_x += self->face->glyph->advance.x >> 6;
}
end:
Py_INCREF(pbuf);
return pbuf;
}
// Boilerplate {{{
static PyMemberDef members[] = {
@@ -910,6 +960,8 @@ static PyMethodDef methods[] = {
METHODB(extra_data, METH_NOARGS),
METHODB(get_variable_data, METH_NOARGS),
METHODB(get_best_name, METH_O),
METHODB(set_size, METH_VARARGS),
METHODB(render_sample_text, METH_VARARGS),
{NULL} /* Sentinel */
};