From 41869d88c22d227b3b5621c0fee796fa5c43676e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 8 May 2024 12:11:58 +0530 Subject: [PATCH] Work on rendering sample text for a font --- kittens/choose_fonts/backend.py | 82 ++++++++++++++++++++++++++++++++- kittens/choose_fonts/ui.go | 23 +++++---- kitty/core_text.m | 33 ++++++++++--- kitty/fast_data_types.pyi | 2 + kitty/fonts.c | 9 ++-- kitty/fonts.h | 2 +- kitty/fonts/common.py | 24 ++++++---- kitty/fonts/render.py | 4 +- kitty/freetype.c | 60 ++++++++++++++++++++++-- 9 files changed, 202 insertions(+), 37 deletions(-) diff --git a/kittens/choose_fonts/backend.py b/kittens/choose_fonts/backend.py index e9b57c074..13508cf56 100644 --- a/kittens/choose_fonts/backend.py +++ b/kittens/choose_fonts/backend.py @@ -2,11 +2,16 @@ # License: GPLv3 Copyright: 2024, Kovid Goyal 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}') diff --git a/kittens/choose_fonts/ui.go b/kittens/choose_fonts/ui.go index d313eb558..930bdeed2 100644 --- a/kittens/choose_fonts/ui.go +++ b/kittens/choose_fonts/ui.go @@ -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 } diff --git a/kitty/core_text.m b/kitty/core_text.m index 6bc907394..ec18647eb 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -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 */ }; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index aaae6b153..68b53a406 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -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, ...]: diff --git a/kitty/fonts.c b/kitty/fonts.c index e1e57208a..1916c4cb3 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -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); diff --git a/kitty/fonts.h b/kitty/fonts.h index fec2c84c9..d0789c593 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -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*); diff --git a/kitty/fonts/common.py b/kitty/fonts/common.py index 538f286ef..af1c2657f 100644 --- a/kitty/fonts/common.py +++ b/kitty/fonts/common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2024, Kovid Goyal -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']} diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index b30e916ea..974745b6e 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -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: diff --git a/kitty/freetype.c b/kitty/freetype.c index 7bc14c530..157a7a26c 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -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 */ };