API to render a single codepoint using a Face

This commit is contained in:
Kovid Goyal 2025-01-28 14:42:43 +05:30
parent 689bdde224
commit c599b7e02f
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
5 changed files with 118 additions and 14 deletions

View file

@ -38,6 +38,19 @@ typedef struct {
PyTypeObject CTFace_Type;
static CTFontRef window_title_font = nil;
static BOOL
CTFontSupportsColorGlyphs(CTFontRef font) {
CFTypeRef symbolicTraits = CTFontCopyAttribute(font, kCTFontSymbolicTrait);
if (symbolicTraits) {
if ([(NSNumber *)symbolicTraits unsignedLongValue] & kCTFontColorGlyphsTrait) {
CFRelease(symbolicTraits);
return YES;
}
CFRelease(symbolicTraits);
}
return NO;
}
static PyObject*
convert_cfstring(CFStringRef src, int free_src) {
RAII_CoreFoundation(CFStringRef, releaseme, free_src ? src : nil);
@ -818,6 +831,41 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); }
static PyObject*
render_codepoint(CTFace *self, PyObject *args) {
unsigned long cp, fg = 0xffffff;
if (!PyArg_ParseTuple(args, "k|k", &cp, &fg)) return NULL;
const int num_chars = 1;
ensure_render_space(0, 0, num_chars);
buffers.glyphs[0] = glyph_id_for_codepoint_ctfont(self->ct_font, cp);
CGSize local_advances[num_chars];
CTFontGetAdvancesForGlyphs(self->ct_font, kCTFontOrientationDefault, buffers.glyphs, local_advances, num_chars);
CGRect bounding_box = CTFontGetBoundingRectsForGlyphs(self->ct_font, kCTFontOrientationDefault, buffers.glyphs, buffers.boxes, num_chars);
StringCanvas ans = { .width = (size_t)(bounding_box.size.width + 1), .height = (size_t)(1 + bounding_box.size.height) };
size_t baseline = ans.height;
ensure_render_space(ans.width, ans.height, num_chars);
PyObject *pbuf = PyBytes_FromStringAndSize(NULL, ans.width * ans.height * sizeof(pixel));
if (!pbuf) return NULL;
memset(PyBytes_AS_STRING(pbuf), 0, PyBytes_GET_SIZE(pbuf));
const unsigned long canvas_width = ans.width, canvas_height = ans.height;
if (CTFontSupportsColorGlyphs(self->ct_font)) {
render_color_glyph(self->ct_font, (uint8_t*)PyBytes_AS_STRING(pbuf), buffers.glyphs[0], ans.width, ans.height, baseline);
} else {
render_glyphs(self->ct_font, ans.width, ans.height, baseline, num_chars);
uint8_t r = (fg >> 16) & 0xff, g = (fg >> 8) & 0xff, b = fg & 0xff;
const uint8_t *last_pixel = (uint8_t*)PyBytes_AS_STRING(pbuf) + PyBytes_GET_SIZE(pbuf) - sizeof(pixel);
const uint8_t *s_limit = buffers.render_buf + canvas_width * canvas_height;
for (
uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf), *s = buffers.render_buf;
p <= last_pixel && s < s_limit;
p += sizeof(pixel), s++
) {
p[0] = r; p[1] = g; p[2] = b; p[3] = s[0];
}
}
return Py_BuildValue("Nkk", pbuf, canvas_width, canvas_height);
}
static PyObject*
render_sample_text(CTFace *self, PyObject *args) {
unsigned long canvas_width, canvas_height;
@ -1122,6 +1170,7 @@ static PyMethodDef methods[] = {
METHODB(identify_for_debug, METH_NOARGS),
METHODB(set_size, METH_VARARGS),
METHODB(render_sample_text, METH_VARARGS),
METHODB(render_codepoint, METH_VARARGS),
METHODB(get_best_name, METH_O),
{NULL} /* Sentinel */
};

View file

@ -448,7 +448,10 @@ class Face:
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 render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> Tuple[bytes, int, int]: ...
def render_sample_text(
self, text: str, width: int, height: int, fg_color: int = 0xffffff
) -> tuple[bytes, int, int]: ...
def render_codepoint(self, cp: int, fg_color: int = 0xffffff) -> tuple[bytes, int, int]: ...
def get_variation(self) -> Optional[Dict[str, float]]: ...
def get_features(self) -> Dict[str, Optional[FeatureData]]: ...
def applied_features(self) -> Dict[str, str]: ...
@ -485,7 +488,10 @@ class CTFace:
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 render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> Tuple[bytes, int, int]: ...
def render_sample_text(
self, text: str, width: int, height: int, fg_color: int = 0xffffff,
) -> tuple[bytes, int, int]: ...
def render_codepoint(self, cp: int, fg_color: int = 0xffffff) -> tuple[bytes, int, int]: ...
def get_variation(self) -> Optional[Dict[str, float]]: ...
def get_features(self) -> Dict[str, Optional[FeatureData]]: ...
def applied_features(self) -> Dict[str, str]: ...
@ -1142,9 +1148,7 @@ def get_fallback_font(text: str, bold: bool, italic: bool) -> Any:
pass
def create_test_font_group(sz: float, dpix: float,
dpiy: float) -> Tuple[int, int]:
pass
def create_test_font_group(sz: float, dpix: float, dpiy: float) -> tuple[int, int, int]: ...
class HistoryBuf:

View file

@ -2199,7 +2199,7 @@ create_test_font_group(PyObject *self UNUSED, PyObject *args) {
if (!PyArg_ParseTuple(args, "ddd", &sz, &dpix, &dpiy)) return NULL;
FontGroup *fg = font_group_for(sz, dpix, dpiy);
if (!fg->sprite_map) send_prerendered_sprites(fg);
return Py_BuildValue("II", fg->fcm.cell_width, fg->fcm.cell_height);
return Py_BuildValue("III", fg->fcm.cell_width, fg->fcm.cell_height, fg->fcm.baseline);
}
static PyObject*

View file

@ -221,6 +221,7 @@ class setup_for_testing:
xnum = 100000
ynum = 100
baseline = 0
def __init__(self, family: str = 'monospace', size: float = 11.0, dpi: float = 96.0, main_face_path: str = ''):
self.family, self.size, self.dpi = family, size, dpi
@ -243,7 +244,7 @@ class setup_for_testing:
descriptor_overrides[0] = self.main_face_path, False, False
try:
set_font_family(opts)
cell_width, cell_height = create_test_font_group(self.size, self.dpi, self.dpi)
cell_width, cell_height, self.baseline = create_test_font_group(self.size, self.dpi, self.dpi)
return sprites, cell_width, cell_height
except Exception:
set_send_sprite_to_gpu(None)
@ -351,3 +352,19 @@ def showcase() -> None:
test_render_string('你好,世界', family=f)
test_render_string('│😁│🙏│😺│', family=f)
test_render_string('A=>>B!=C', family='Fira Code')
def test_render_codepoint(char: str = '😺', path: str = '/t/Noto-COLRv1.ttf', font_size: float = 160.0) -> None:
if TYPE_CHECKING:
from kitty.fast_data_types import CTFace, Face
def create_face(path: str) -> 'Union[CTFace, Face]':
if is_macos:
from kitty.fast_data_types import CTFace
return CTFace(path=path)
from kitty.fast_data_types import Face
return Face(path=path)
f = create_face(path=path)
f.set_size(font_size, 96, 96)
bitmap, w, h = f.render_codepoint(ord(char))
display_bitmap(bitmap, w, h)
print('\n')

View file

@ -617,14 +617,18 @@ detect_right_edge(ProcessedBitmap *ans) {
static bool
render_color_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline UNUSED) {
unsigned short best = 0, diff = USHRT_MAX;
const unsigned int width_to_render_in = num_cells * cell_width;
if (self->face->num_fixed_sizes > 0) {
const short limit = self->face->num_fixed_sizes;
for (short i = 0; i < limit; i++) {
for (short i = 0; i < self->face->num_fixed_sizes; i++) {
unsigned short w = self->face->available_sizes[i].width;
unsigned short d = w > (unsigned short)cell_width ? w - (unsigned short)cell_width : (unsigned short)cell_width - w;
if (d < diff) {
diff = d;
best = i;
if (width_to_render_in) {
unsigned short d = w > (unsigned short)width_to_render_in ? w - (unsigned short)width_to_render_in : (unsigned short)width_to_render_in - w;
if (d < diff) {
diff = d;
best = i;
}
} else {
if (w > self->face->available_sizes[best].width) best = i;
}
}
FT_Error error = FT_Select_Size(self->face, best);
@ -638,7 +642,7 @@ render_color_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int
ans->stride = bitmap->pitch < 0 ? -bitmap->pitch : bitmap->pitch;
ans->rows = bitmap->rows;
ans->pixel_mode = bitmap->pixel_mode;
if (ans->width > num_cells * cell_width + 2) downsample_bitmap(ans, num_cells * cell_width, cell_height);
if (width_to_render_in && ans->width > width_to_render_in + 2) downsample_bitmap(ans, width_to_render_in, cell_height);
ans->bitmap_top = (int)((float)self->face->glyph->bitmap_top / ans->factor);
ans->bitmap_left = (int)((float)self->face->glyph->bitmap_left / ans->factor);
detect_right_edge(ans);
@ -992,6 +996,35 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); }
static PyObject*
render_codepoint(Face *self, PyObject *args) {
unsigned long cp, fg = 0xffffff;
if (!PyArg_ParseTuple(args, "k|k", &cp, &fg)) return NULL;
FT_UInt glyph_index = FT_Get_Char_Index(self->face, cp);
ProcessedBitmap pbm = EMPTY_PBM;
if (self->has_color) {
render_color_bitmap(self, glyph_index, &pbm, 0, 0, 0, 0);
} else {
int load_flags = get_load_flags(self->hinting, self->hintstyle, FT_LOAD_RENDER);
FT_Load_Glyph(self->face, glyph_index, load_flags);
FT_Render_Glyph(self->face->glyph, FT_RENDER_MODE_NORMAL);
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
populate_processed_bitmap(self->face->glyph, bitmap, &pbm, false);
}
const unsigned long canvas_width = pbm.width, canvas_height = pbm.rows;
RAII_PyObject(ans, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_height * canvas_width));
if (!ans) return NULL;
pixel *canvas = (pixel*)PyBytes_AS_STRING(ans);
memset(canvas, 0, PyBytes_GET_SIZE(ans));
place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, 0, 0, 0, 99999, fg, 0, 0);
for (pixel *c = canvas; c < canvas + canvas_width * canvas_height; c++) {
uint8_t *p = (uint8_t*)c;
uint8_t a = p[0], b = p[1], g = p[2], r = p[3];
p[0] = r; p[1] = g; p[2] = b; p[3] = a;
}
return Py_BuildValue("Okk", ans, canvas_width, canvas_height);
}
static PyObject*
render_sample_text(Face *self, PyObject *args) {
unsigned long canvas_width, canvas_height;
@ -1090,6 +1123,7 @@ static PyMethodDef methods[] = {
METHODB(get_best_name, METH_O),
METHODB(set_size, METH_VARARGS),
METHODB(render_sample_text, METH_VARARGS),
METHODB(render_codepoint, METH_VARARGS),
{NULL} /* Sentinel */
};