diff --git a/kittens/choose_fonts/backend.py b/kittens/choose_fonts/backend.py index 13508cf56..cd1a8fbab 100644 --- a/kittens/choose_fonts/backend.py +++ b/kittens/choose_fonts/backend.py @@ -4,14 +4,18 @@ import json import string import sys +import tempfile from typing import Any, Dict, Tuple, TypedDict 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_font_files, get_variable_data_for_descriptor 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.utils import screen_size_function def send_to_kitten(x: Any) -> None: @@ -55,13 +59,13 @@ def opts_from_cmd(cmd: Dict[str, Any]) -> Tuple[Options, FamilyKey, float, float BaseKey = Tuple[FamilyKey, int, int] FaceKey = Tuple[str, BaseKey] -SAMPLE_TEXT = string.ascii_lowercase + string.digits + string.ascii_uppercase + ' ' + string.punctuation +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: +def render_face_sample(font: Descriptor, opts: Options, dpi_x: float, dpi_y: float, width: int, height: int) -> bytes: 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) + return face.render_sample_text(SAMPLE_TEXT, width, height, opts.foreground.rgb) def render_family_sample( @@ -85,7 +89,9 @@ def render_family_sample( 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) + with tempfile.NamedTemporaryFile(delete=False, suffix='.rgba', dir=output_dir) as tf: + tf.write(render_face_sample(desc, opts, dpi_x, dpi_y, width, height)) + cache[key] = ans[x] = tf.name return ans @@ -106,3 +112,29 @@ def main() -> None: 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) -> 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 = render_face_sample(desc, opts, float(q['dpi_x']), float(q['dpi_y']), width, height) + display_bitmap(bitmap, width, height) diff --git a/kitty/core_text.m b/kitty/core_text.m index ec18647eb..27d68c5f3 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -678,6 +678,54 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) { return ans; } +static PyObject* +render_sample_text(CTFace *self, PyObject *args) { + unsigned long canvas_width, canvas_height; + unsigned long fg = 0xffffff; + CTFontRef font = self->ct_font; + 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); + size_t num_chars = PyUnicode_GET_LENGTH(ptext); + RAII_ALLOC(unichar, chars, calloc(sizeof(unichar), num_chars)); + if (!chars) return PyErr_NoMemory(); + for (size_t i = 0; i < num_chars; i++) chars[i] = PyUnicode_READ_CHAR(ptext, i); + RAII_ALLOC(CGSize, local_advances, calloc(sizeof(CGSize), num_chars)); + if (!local_advances) return PyErr_NoMemory(); + ensure_render_space(0, 0, num_chars); + CTFontGetGlyphsForCharacters(font, chars, buffers.glyphs, num_chars); + CTFontGetAdvancesForGlyphs(font, kCTFontOrientationDefault, buffers.glyphs, local_advances, num_chars); + CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationDefault, buffers.glyphs, buffers.boxes, num_chars); + CGFloat x = 0, y = 0; + if (cell_width > canvas_width) goto end; + for (size_t i = 0; i < num_chars; i++) { + if (local_advances[i].width + x > canvas_width) { + x = 0; + y += cell_height; + } + if (y + cell_height > canvas_height) { + num_chars = i - 1; + break; + } + buffers.positions[i] = CGPointMake(x, -y); + x += cell_width; + } + unsigned long height = MIN((int)ceil(y) + cell_height, canvas_height); + ensure_render_space(canvas_width, height, num_chars); + render_glyphs(font, canvas_width, height, baseline, num_chars); + uint8_t r = (fg >> 16) & 0xff, g = (fg >> 8) & 0xff, b = fg & 0xff; + for (uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf), *s = buffers.render_buf; p < (uint8_t*)PyBytes_AS_STRING(pbuf) + sizeof(pixel) * canvas_width * height; p += 4, s++) { + p[0] = r; p[1] = g; p[2] = b; p[3] = s[0]; + } +end: + Py_INCREF(pbuf); + return pbuf; + +} + static bool ensure_ui_font(size_t in_height) { static size_t for_height = 0; @@ -879,6 +927,7 @@ static PyMethodDef methods[] = { METHODB(get_variable_data, METH_NOARGS), METHODB(identify_for_debug, METH_NOARGS), METHODB(set_size, METH_VARARGS), + METHODB(render_sample_text, 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 68b53a406..eca98d420 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -427,6 +427,7 @@ 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) -> bytes: ... class CoreTextFont(TypedDict): @@ -459,6 +460,7 @@ 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) -> bytes: ... def coretext_all_fonts(monospaced_only: bool) -> Tuple[CoreTextFont, ...]: diff --git a/kitty/fonts.c b/kitty/fonts.c index 1916c4cb3..263903878 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -632,7 +632,7 @@ static PyObject* box_drawing_function = NULL, *prerender_function = NULL, *descr 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) { - const pixel col = color_rgb << 8; + 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; diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index 974745b6e..c2cdf82f4 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -461,29 +461,30 @@ def shape_string( return test_shape(line, path) -def show(outfile: str, width: int, height: int, fmt: int) -> None: - import os +def show(rgba_data: bytes, width: int, height: int, fmt: int = 32) -> None: from base64 import standard_b64encode from kittens.tui.images import GraphicsCommand + + data = memoryview(standard_b64encode(rgba_data)) cmd = GraphicsCommand() cmd.a = 'T' cmd.f = fmt cmd.s = width cmd.v = height - cmd.t = 't' + + while data: + chunk, data = data[:4096], data[4096:] + cmd.m = 1 if data else 0 + sys.stdout.buffer.write(cmd.serialize(chunk)) + cmd.clear() sys.stdout.flush() - sys.stdout.buffer.write(cmd.serialize(standard_b64encode(os.path.abspath(outfile).encode()))) sys.stdout.buffer.flush() def display_bitmap(rgb_data: bytes, width: int, height: int) -> None: - from tempfile import NamedTemporaryFile - setattr(display_bitmap, 'detected', True) - with NamedTemporaryFile(suffix='.rgba', delete=False) as f: - f.write(rgb_data) assert len(rgb_data) == 4 * width * height - show(f.name, width, height, 32) + show(rgb_data, width, height) def test_render_string( diff --git a/kitty/freetype.c b/kitty/freetype.c index 157a7a26c..2e3aa887d 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -633,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, pixel fg_rgb) { +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, size_t x_in_canvas, size_t y_in_canvas) { // 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. @@ -649,6 +649,7 @@ place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size uint32_t extra = dest.left + bm->width - cell_width; dest.left = extra > dest.left ? 0 : dest.left - extra; } + dest.left += x_in_canvas; // Calculate row bounds int32_t yoff = (ssize_t)(y_offset + bm->bitmap_top); @@ -657,8 +658,9 @@ place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size } else { dest.top = baseline - yoff; } + dest.top += y_in_canvas; - /* printf("x_offset: %d y_offset: %d src_start_row: %u src_start_column: %u dest_start_row: %u dest_start_column: %u bm_width: %lu bitmap_rows: %lu\n", xoff, yoff, src.top, src.left, dest.top, dest.left, bm->width, bm->rows); */ + // printf("x_offset: %d y_offset: %d src_start_row: %u src_start_column: %u dest_start_row: %u dest_start_column: %u bm_width: %lu bitmap_rows: %lu\n", xoff, yoff, src.top, src.left, dest.top, dest.left, bm->width, bm->rows); if (bm->pixel_mode == FT_PIXEL_MODE_BGRA) { copy_color_bitmap(bm->buf, cell, &src, &dest, bm->stride, cell_width); @@ -698,7 +700,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, 0xffffff); + place_bitmap_in_canvas(canvas, &bm, canvas_width, cell_height, x_offset, y, baseline, i, 0xffffff, 0, 0); } 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 @@ -878,7 +880,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, 0xffffff); + place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, 0, 0, baseline, n, 0xffffff, pen_x, 0); pen_x += self->face->glyph->advance.x >> 6; } ans.width = pen_x; ans.height = canvas_height; @@ -923,9 +925,13 @@ render_sample_text(Face *self, PyObject *args) { 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); + place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, 0, 0, baseline, 99999, fg, pen_x, pen_y); pen_x += self->face->glyph->advance.x >> 6; } + for (uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf); p < (uint8_t*)PyBytes_AS_STRING(pbuf) + sizeof(pixel) * canvas_width * (MIN(canvas_height, pen_y + cell_height)); p += 4) { + 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; + } end: Py_INCREF(pbuf); return pbuf;