More work on CSD title rendering

This commit is contained in:
Kovid Goyal 2021-03-30 21:48:56 +05:30
parent d7e1e53450
commit da0009a46b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 188 additions and 27 deletions

View File

@ -72,6 +72,9 @@ set_freetype_error(const char* prefix, int err_code) {
static FT_Library library; static FT_Library library;
FT_Library
freetype_library(void) { return library; }
static inline int static inline int
font_units_to_pixels_y(Face *self, int x) { font_units_to_pixels_y(Face *self, int x) {
return (int)ceil((double)FT_MulFix(x, self->face->size->metrics.y_scale) / 64.0); return (int)ceil((double)FT_MulFix(x, self->face->size->metrics.y_scale) / 64.0);
@ -402,6 +405,22 @@ populate_processed_bitmap(FT_GlyphSlotRec *slot, FT_Bitmap *bitmap, ProcessedBit
ans->bitmap_top = slot->bitmap_top; ans->bitmap_left = slot->bitmap_left; ans->bitmap_top = slot->bitmap_top; ans->bitmap_left = slot->bitmap_left;
} }
bool
freetype_convert_mono_bitmap(FT_Bitmap *src, FT_Bitmap *dest) {
FT_Bitmap_Init(dest);
// This also sets pixel_mode to FT_PIXEL_MODE_GRAY so we don't have to
int error = FT_Bitmap_Convert(library, src, dest, 1);
if (error) { set_freetype_error("Failed to convert bitmap, with error:", error); return false; }
// Normalize gray levels to the range [0..255]
dest->num_grays = 256;
unsigned int stride = dest->pitch < 0 ? -dest->pitch : dest->pitch;
for (unsigned i = 0; i < (unsigned)dest->rows; ++i) {
// We only have 2 levels
for (unsigned j = 0; j < (unsigned)dest->width; ++j) dest->buffer[i * stride + j] *= 255;
}
return true;
}
static inline bool static inline bool
render_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, bool bold, bool italic, bool rescale, FONTS_DATA_HANDLE fg) { render_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, bool bold, bool italic, bool rescale, FONTS_DATA_HANDLE fg) {
if (!load_glyph(self, glyph_id, FT_LOAD_RENDER)) return false; if (!load_glyph(self, glyph_id, FT_LOAD_RENDER)) return false;
@ -410,19 +429,7 @@ render_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_
// Embedded bitmap glyph? // Embedded bitmap glyph?
if (self->face->glyph->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) { if (self->face->glyph->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
FT_Bitmap bitmap; FT_Bitmap bitmap;
FT_Bitmap_Init(&bitmap); freetype_convert_mono_bitmap(&self->face->glyph->bitmap, &bitmap);
// This also sets pixel_mode to FT_PIXEL_MODE_GRAY so we don't have to
int error = FT_Bitmap_Convert(library, &self->face->glyph->bitmap, &bitmap, 1);
if (error) { set_freetype_error("Failed to convert bitmap, with error:", error); return false; }
// Normalize gray levels to the range [0..255]
bitmap.num_grays = 256;
unsigned int stride = bitmap.pitch < 0 ? -bitmap.pitch : bitmap.pitch;
for (unsigned i = 0; i < (unsigned)bitmap.rows; ++i) {
// We only have 2 levels
for (unsigned j = 0; j < (unsigned)bitmap.width; ++j) bitmap.buffer[i * stride + j] *= 255;
}
populate_processed_bitmap(self->face->glyph, &bitmap, ans, true); populate_processed_bitmap(self->face->glyph, &bitmap, ans, true);
FT_Bitmap_Done(library, &bitmap); FT_Bitmap_Done(library, &bitmap);
} else { } else {

View File

@ -9,6 +9,7 @@
#include <hb.h> #include <hb.h>
#include <hb-ft.h> #include <hb-ft.h>
#include "charsets.h" #include "charsets.h"
#include FT_BITMAP_H
typedef struct FamilyInformation { typedef struct FamilyInformation {
char *name; char *name;
@ -19,10 +20,21 @@ typedef struct Face {
FT_Face freetype; FT_Face freetype;
hb_font_t *hb; hb_font_t *hb;
FT_UInt pixel_size; FT_UInt pixel_size;
int hinting, hintstyle;
struct Face *fallbacks; struct Face *fallbacks;
size_t count, capacity; size_t count, capacity;
} Face; } Face;
typedef struct {
unsigned char* buf;
size_t start_x, width, stride;
size_t rows;
FT_Pixel_Mode pixel_mode;
unsigned int factor, right_edge;
int bitmap_left, bitmap_top;
} ProcessedBitmap;
Face main_face = {0}; Face main_face = {0};
FontConfigFace main_face_information = {0}; FontConfigFace main_face_information = {0};
FamilyInformation main_face_family = {0}; FamilyInformation main_face_family = {0};
@ -59,12 +71,25 @@ set_main_face_family(const char *family, bool bold, bool italic) {
main_face_family.bold = bold; main_face_family.italic = italic; main_face_family.bold = bold; main_face_family.italic = italic;
} }
static inline int
get_load_flags(int hinting, int hintstyle, int base) {
int flags = base;
if (hinting) {
if (hintstyle >= 3) flags |= FT_LOAD_TARGET_NORMAL;
else if (0 < hintstyle && hintstyle < 3) flags |= FT_LOAD_TARGET_LIGHT;
} else flags |= FT_LOAD_NO_HINTING;
return flags;
}
static bool static bool
load_font(FontConfigFace *info, Face *ans) { load_font(FontConfigFace *info, Face *ans) {
ans->freetype = native_face_from_path(info->path, info->index); ans->freetype = native_face_from_path(info->path, info->index);
if (!ans->freetype) return false; if (!ans->freetype) return false;
ans->hb = hb_ft_font_create(ans->freetype, NULL); ans->hb = hb_ft_font_create(ans->freetype, NULL);
if (!ans->hb) { PyErr_NoMemory(); return false; } if (!ans->hb) { PyErr_NoMemory(); return false; }
ans->hinting = info->hinting; ans->hintstyle = info->hintstyle;
hb_ft_font_set_load_flags(ans->hb, get_load_flags(ans->hinting, ans->hintstyle, FT_LOAD_DEFAULT));
return true; return true;
} }
@ -83,34 +108,125 @@ set_pixel_size(Face *face, FT_UInt sz) {
if (sz != face->pixel_size) { if (sz != face->pixel_size) {
FT_Set_Pixel_Sizes(face->freetype, sz, sz); // TODO: check for and handle failures FT_Set_Pixel_Sizes(face->freetype, sz, sz); // TODO: check for and handle failures
hb_ft_font_changed(face->hb); hb_ft_font_changed(face->hb);
hb_ft_font_set_load_flags(face->hb, get_load_flags(face->hinting, face->hintstyle, FT_LOAD_DEFAULT));
face->pixel_size = sz; face->pixel_size = sz;
} }
} }
typedef struct RenderState { typedef struct RenderState {
uint32_t pending_in_buffer, fg, bg; uint32_t pending_in_buffer, fg, bg;
uint8_t *output; pixel *output;
bool alpha_first;
size_t output_width, output_height; size_t output_width, output_height;
Face *current_face; Face *current_face;
float x, y;
Region src, dest;
} RenderState; } RenderState;
static inline int
font_units_to_pixels_y(FT_Face face, int x) {
return (int)ceil((double)FT_MulFix(x, face->size->metrics.y_scale) / 64.0);
}
bool static void
setup_regions(ProcessedBitmap *bm, RenderState *rs, int baseline) {
rs->src = (Region){ .left = bm->start_x, .bottom = bm->rows, .right = bm->width + bm->start_x };
rs->dest = (Region){ .bottom = rs->output_height, .right = (uint32_t) (rs->output_width - rs->x) };
int xoff = (int)(rs->x + bm->bitmap_left);
if (xoff < 0) rs->src.left += -xoff;
else rs->dest.left = xoff;
int yoff = (int)(rs->y + bm->bitmap_top);
if ((yoff > 0 && yoff > baseline)) {
rs->dest.top = 0;
} else {
rs->dest.top = baseline - yoff;
}
}
static pixel
premult_pixel(pixel p, float alpha) {
#define s(by) ((pixel)(((p >> by) & 0xff) * alpha) << by)
return (((pixel)(255 * alpha)) << 24) | s(16) | s(8) | s(0);
#undef s
}
static pixel
alpha_blend_premult(pixel over, pixel under, pixel final_alpha) {
const uint16_t over_r = (over >> 16) & 0xff, over_g = (over >> 8) & 0xff, over_b = over & 0xff;
const uint16_t under_r = (under >> 16) & 0xff, under_g = (under >> 8) & 0xff, under_b = under & 0xff;
const uint16_t factor = 1 - (over >> 24);
#define ans(x) (over_##x + (factor * under_##x) / 255)
return final_alpha | (ans(r) << 16) | ans(g) << 8 | ans(b);
#undef ans
}
static void
render_gray_bitmap(ProcessedBitmap *src, RenderState *rs) {
for (size_t sr = rs->src.top, dr = rs->dest.top; sr < rs->src.bottom && dr < rs->dest.bottom; sr++, dr++) {
pixel *dest_row = rs->output + rs->output_width * dr;
uint8_t *src_row = src->buf + src->stride * sr;
for(size_t sc = rs->src.left, dc = rs->dest.left; sc < rs->src.right && dc < rs->dest.right; sc++, dc++) {
pixel fg = premult_pixel(rs->fg, src_row[sc] / 255.f);
dest_row[dc] = alpha_blend_premult(fg, dest_row[dc], dest_row[dc] & 0xff000000);
}
}
}
static inline void
populate_processed_bitmap(FT_GlyphSlotRec *slot, FT_Bitmap *bitmap, ProcessedBitmap *ans) {
ans->stride = bitmap->pitch < 0 ? -bitmap->pitch : bitmap->pitch;
ans->rows = bitmap->rows;
ans->start_x = 0; ans->width = bitmap->width;
ans->pixel_mode = bitmap->pixel_mode;
ans->bitmap_top = slot->bitmap_top; ans->bitmap_left = slot->bitmap_left;
}
static bool
render_run(RenderState *rs) { render_run(RenderState *rs) {
hb_buffer_guess_segment_properties(hb_buffer); hb_buffer_guess_segment_properties(hb_buffer);
if (!HB_DIRECTION_IS_HORIZONTAL (hb_buffer_get_direction(hb_buffer))) { if (!HB_DIRECTION_IS_HORIZONTAL (hb_buffer_get_direction(hb_buffer))) {
PyErr_SetString(PyExc_ValueError, "Vertical text is not supported"); PyErr_SetString(PyExc_ValueError, "Vertical text is not supported");
return false; return false;
} }
FT_UInt pixel_size = 2 * rs->output_height / 3; FT_UInt pixel_size = 3 * rs->output_height / 4;
set_pixel_size(rs->current_face, pixel_size); set_pixel_size(rs->current_face, pixel_size);
hb_shape(rs->current_face->hb, hb_buffer, NULL, 0); hb_shape(rs->current_face->hb, hb_buffer, NULL, 0);
unsigned int len = hb_buffer_get_length(hb_buffer); unsigned int len = hb_buffer_get_length(hb_buffer);
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL); hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL);
hb_glyph_position_t *pos = hb_buffer_get_glyph_positions(hb_buffer, NULL); hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL);
FT_Face face = rs->current_face->freetype;
int baseline = font_units_to_pixels_y(face, face->ascender);
bool has_color = FT_HAS_COLOR(face);
int load_flags = get_load_flags(rs->current_face->hinting, rs->current_face->hintstyle, has_color ? FT_LOAD_COLOR : FT_LOAD_RENDER);
(void)len; (void)info; (void)pos; for (unsigned int i = 0; i < len; i++) {
rs->x += (float)positions[i].x_offset / 64.0f;
rs->y += (float)positions[i].y_offset / 64.0f;
int error = FT_Load_Glyph(face, info->codepoint, load_flags);
if (error) continue;
FT_Bitmap *bitmap = &face->glyph->bitmap;
ProcessedBitmap pbm;
switch(bitmap->pixel_mode) {
case FT_PIXEL_MODE_BGRA:
// TODO: Implement this
break;
case FT_PIXEL_MODE_MONO: {
FT_Bitmap bitmap;
freetype_convert_mono_bitmap(&face->glyph->bitmap, &bitmap);
populate_processed_bitmap(face->glyph, &bitmap, &pbm);
setup_regions(&pbm, rs, baseline);
render_gray_bitmap(&pbm, rs);
FT_Bitmap_Done(freetype_library(), &bitmap);
}
break;
case FT_PIXEL_MODE_GRAY:
populate_processed_bitmap(face->glyph, &face->glyph->bitmap, &pbm);
setup_regions(&pbm, rs, baseline);
render_gray_bitmap(&pbm, rs);
break;
}
rs->x += (float)positions[i].x_advance / 64.0f;
}
return true; return true;
} }
@ -148,16 +264,17 @@ find_fallback_font_for(RenderState *rs, char_type codep) {
bool bool
render_single_line(const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, bool alpha_first) { render_single_line(const char *text, pixel fg, pixel bg, uint8_t *output_buf, size_t width, size_t height) {
if (!ensure_state()) return false; if (!ensure_state()) return false;
for (uint32_t *px = (uint32_t*)output_buf, *end = ((uint32_t*)output_buf) + width * height; px < end; px++) *px = bg; bool has_text = text && text[0];
if (!text || !text[0]) return true; pixel pbg = premult_pixel(bg, ((bg >> 24) & 0xff) / 255.f);
(void)fg; (void)alpha_first; for (pixel *px = (pixel*)output_buf, *end = ((pixel*)output_buf) + width * height; px < end; px++) *px = pbg;
if (!has_text) return true;
hb_buffer_clear_contents(hb_buffer); hb_buffer_clear_contents(hb_buffer);
if (!hb_buffer_pre_allocate(hb_buffer, 512)) { PyErr_NoMemory(); return false; } if (!hb_buffer_pre_allocate(hb_buffer, 512)) { PyErr_NoMemory(); return false; }
RenderState rs = { RenderState rs = {
.current_face = &main_face, .fg = fg, .bg = bg, .output_width = width, .output_height = height, .current_face = &main_face, .fg = fg, .bg = bg, .output_width = width, .output_height = height,
.output = output_buf, .alpha_first = alpha_first .output = (pixel*)output_buf,
}; };
for (uint32_t i = 0, codep = 0, state = 0, prev = UTF8_ACCEPT; text[i] > 0; i++) { for (uint32_t i = 0, codep = 0, state = 0, prev = UTF8_ACCEPT; text[i] > 0; i++) {
@ -193,6 +310,40 @@ render_single_line(const char *text, uint32_t fg, uint32_t bg, uint8_t *output_b
return true; return true;
} }
static PyObject*
render_line(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
// use for testing as below
// kitty +runpy "from kitty.fast_data_types import *; open('/tmp/test.rgba', 'wb').write(freetype_render_line('H'))" && convert -size 800x120 -depth 8 /tmp/test.rgba /tmp/test.png && icat /tmp/test.png
const char *text = "Test rendering", *family = NULL;
unsigned int width=800, height=120;
int bold = 0, italic = 0;
unsigned long fg = 0, bg = 0xfffefefe;
static const char* kwlist[] = {"text", "width", "height", "font_family", "bold", "italic", "fg", "bg", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "|sIIzppkk", (char**)kwlist, &text, &width, &height, &family, &bold, &italic, &fg, &bg)) return NULL;
if (family) set_main_face_family(family, bold, italic);
PyObject *ans = PyBytes_FromStringAndSize(NULL, width * height * 4);
if (!ans) return NULL;
uint8_t *buffer = (u_int8_t*) PyBytes_AS_STRING(ans);
if (!render_single_line(text, 0, 0xffffffff, buffer, width, height)) {
Py_CLEAR(ans);
if (!PyErr_Occurred()) PyErr_SetString(PyExc_RuntimeError, "Unknown error while rendering text");
ans = NULL;
} else {
// remove pre-multiplication and convert to ABGR which is what the ImageMagick .rgba filetype wants
for (pixel *p = (pixel*)buffer, *end = (pixel*)(buffer + PyBytes_GET_SIZE(ans)); p < end; p++) {
const uint16_t a = (*p >> 24) & 0xff;
if (!a) continue;
uint16_t r = (*p >> 16) & 0xff, g = (*p >> 8) & 0xff, b = *p & 0xff;
#define c(x) (((x * 255) / a) & 0xff)
r = c(r); g = c(g); b = c(b);
#undef c
*p = (a << 24) | (b << 16) | (g << 8) | r;
}
}
return ans;
}
static PyObject* static PyObject*
path_for_font(PyObject *self UNUSED, PyObject *args) { path_for_font(PyObject *self UNUSED, PyObject *args) {
const char *family = NULL; int bold = 0, italic = 0; const char *family = NULL; int bold = 0, italic = 0;
@ -217,8 +368,9 @@ fallback_for_char(PyObject *self UNUSED, PyObject *args) {
} }
static PyMethodDef module_methods[] = { static PyMethodDef module_methods[] = {
METHODB(path_for_font, METH_VARARGS), {"fontconfig_path_for_font", (PyCFunction)(void (*) (void))(path_for_font), METH_VARARGS, NULL},
METHODB(fallback_for_char, METH_VARARGS), {"fontconfig_fallback_for_char", (PyCFunction)(void (*) (void))(fallback_for_char), METH_VARARGS, NULL},
{"freetype_render_line", (PyCFunction)(void (*) (void))(render_line), METH_VARARGS | METH_KEYWORDS, NULL},
{NULL, NULL, 0, NULL} /* Sentinel */ {NULL, NULL, 0, NULL} /* Sentinel */
}; };

View File

@ -9,7 +9,7 @@
#include "data-types.h" #include "data-types.h"
#include <hb-ft.h> #include <hb-ft.h>
bool render_single_line(const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, bool alpha_first); bool render_single_line(const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height);
typedef struct FontConfigFace { typedef struct FontConfigFace {
char *path; char *path;
@ -21,5 +21,7 @@ typedef struct FontConfigFace {
bool information_for_font_family(const char *family, bool bold, bool italic, FontConfigFace *ans); bool information_for_font_family(const char *family, bool bold, bool italic, FontConfigFace *ans);
FT_Face native_face_from_path(const char *path, int index); FT_Face native_face_from_path(const char *path, int index);
bool fallback_font(char_type ch, const char *family, bool bold, bool italic, FontConfigFace *ans); bool fallback_font(char_type ch, const char *family, bool bold, bool italic, FontConfigFace *ans);
bool freetype_convert_mono_bitmap(FT_Bitmap *src, FT_Bitmap *dest);
FT_Library freetype_library(void);
void set_main_face_family(const char *family, bool bold, bool italic); void set_main_face_family(const char *family, bool bold, bool italic);