kitty/kitty/freetype.c
Kovid Goyal 55319cd6d6
FreeType: Round glyph advances
The old code would simply add advances and store as a float with
individual glyph positioning using floor() based on the advance +
offset. This breaks rendering of infinite length ligatures at some font
sizes as they either under or overflow. Rounding the advances seems to
fix it. Dont know if it will have other ill-effects for non infinite
length ligatures. If it does we can test total ligature length and
use rounding only for long ligatures.

Fixes #3896
2021-08-04 08:40:49 +05:30

773 lines
31 KiB
C

/*
* freetype.c
* Copyright (C) 2016 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "fonts.h"
#include "cleanup.h"
#include "state.h"
#include <math.h>
#include <structmember.h>
#include <ft2build.h>
#include <hb-ft.h>
#if FREETYPE_MAJOR == 2 && FREETYPE_MINOR < 7
#define FT_Bitmap_Init FT_Bitmap_New
#endif
#include FT_BITMAP_H
#include FT_TRUETYPE_TABLES_H
typedef struct {
PyObject_HEAD
FT_Face face;
unsigned int units_per_EM;
int ascender, descender, height, max_advance_width, max_advance_height, underline_position, underline_thickness, strikethrough_position, strikethrough_thickness;
int hinting, hintstyle, index;
bool is_scalable, has_color;
float size_in_pts;
FT_F26Dot6 char_width, char_height;
FT_UInt xdpi, ydpi;
PyObject *path;
hb_font_t *harfbuzz_font;
hb_codepoint_t space_glyph_id;
void *extra_data;
free_extra_data_func free_extra_data;
float apple_leading;
} Face;
PyTypeObject Face_Type;
static PyObject* FreeType_Exception = NULL;
void
set_freetype_error(const char* prefix, int err_code) {
int i = 0;
#undef FTERRORS_H_
#undef __FTERRORS_H__
#define FT_ERRORDEF( e, v, s ) { e, s },
#define FT_ERROR_START_LIST {
#define FT_ERROR_END_LIST { 0, NULL } };
static const struct {
int err_code;
const char* err_msg;
} ft_errors[] =
#ifdef FT_ERRORS_H
#include FT_ERRORS_H
#else
FT_ERROR_START_LIST FT_ERROR_END_LIST
#endif
while(ft_errors[i].err_msg != NULL) {
if (ft_errors[i].err_code == err_code) {
PyErr_Format(FreeType_Exception, "%s %s", prefix, ft_errors[i].err_msg);
return;
}
i++;
}
PyErr_Format(FreeType_Exception, "%s (error code: %d)", prefix, err_code);
}
static FT_Library library;
FT_Library
freetype_library(void) { return library; }
static int
font_units_to_pixels_y(Face *self, int x) {
return (int)ceil((double)FT_MulFix(x, self->face->size->metrics.y_scale) / 64.0);
}
static int
font_units_to_pixels_x(Face *self, int x) {
return (int)ceil((double)FT_MulFix(x, self->face->size->metrics.x_scale) / 64.0);
}
static 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
load_glyph(Face *self, int glyph_index, int load_type) {
int flags = get_load_flags(self->hinting, self->hintstyle, load_type);
int error = FT_Load_Glyph(self->face, glyph_index, flags);
if (error) {
char buf[256];
snprintf(buf, sizeof(buf) - 1, "Failed to load glyph_index=%d load_type=%d, with error:", glyph_index, load_type);
set_freetype_error(buf, error); return false;
}
return true;
}
static unsigned int
get_height_for_char(Face *self, char ch) {
unsigned int ans = 0;
int glyph_index = FT_Get_Char_Index(self->face, ch);
if (load_glyph(self, glyph_index, FT_LOAD_DEFAULT)) {
unsigned int baseline = font_units_to_pixels_y(self, self->ascender);
FT_GlyphSlotRec *glyph = self->face->glyph;
FT_Bitmap *bm = &glyph->bitmap;
if (glyph->bitmap_top <= 0 || (glyph->bitmap_top > 0 && (unsigned int)glyph->bitmap_top < baseline)) {
ans = baseline - glyph->bitmap_top + bm->rows;
}
}
return ans;
}
static unsigned int
calc_cell_height(Face *self, bool for_metrics) {
unsigned int ans = font_units_to_pixels_y(self, self->height);
if (for_metrics) {
unsigned int underscore_height = get_height_for_char(self, '_');
if (underscore_height > ans) {
if (global_state.debug_font_fallback) printf(
"Increasing cell height by %u pixels to work around buggy font that renders underscore outside the bounding box\n", underscore_height - ans);
return underscore_height;
}
}
return ans;
}
static bool
set_font_size(Face *self, FT_F26Dot6 char_width, FT_F26Dot6 char_height, FT_UInt xdpi, FT_UInt ydpi, unsigned int desired_height, unsigned int cell_height) {
int error = FT_Set_Char_Size(self->face, 0, char_height, xdpi, ydpi);
if (!error) {
unsigned int ch = calc_cell_height(self, false);
if (desired_height && ch != desired_height) {
FT_F26Dot6 h = (FT_F26Dot6)floor((double)char_height * (double)desired_height / (double) ch);
return set_font_size(self, 0, h, xdpi, ydpi, 0, cell_height);
}
self->char_width = char_width; self->char_height = char_height; self->xdpi = xdpi; self->ydpi = ydpi;
if (self->harfbuzz_font != NULL) hb_ft_font_changed(self->harfbuzz_font);
} else {
if (!self->is_scalable && self->face->num_fixed_sizes > 0) {
int32_t min_diff = INT32_MAX;
if (desired_height == 0) desired_height = cell_height;
if (desired_height == 0) {
desired_height = (unsigned int)ceil(((double)char_height / 64.) * (double)ydpi / 72.);
desired_height += (unsigned int)ceil(0.2 * desired_height);
}
FT_Int strike_index = -1;
for (FT_Int i = 0; i < self->face->num_fixed_sizes; i++) {
int h = self->face->available_sizes[i].height;
int32_t diff = h < (int32_t)desired_height ? (int32_t)desired_height - h : h - (int32_t)desired_height;
if (diff < min_diff) {
min_diff = diff;
strike_index = i;
}
}
if (strike_index > -1) {
error = FT_Select_Size(self->face, strike_index);
if (error) { set_freetype_error("Failed to set char size for non-scalable font, with error:", error); return false; }
return true;
}
}
set_freetype_error("Failed to set char size, with error:", error);
return false;
}
return !error;
}
bool
set_size_for_face(PyObject *s, unsigned int desired_height, bool force, FONTS_DATA_HANDLE fg) {
Face *self = (Face*)s;
FT_F26Dot6 w = (FT_F26Dot6)(ceil(fg->font_sz_in_pts * 64.0));
FT_UInt xdpi = (FT_UInt)fg->logical_dpi_x, ydpi = (FT_UInt)fg->logical_dpi_y;
if (!force && (self->char_width == w && self->char_height == w && self->xdpi == xdpi && self->ydpi == ydpi)) return true;
((Face*)self)->size_in_pts = (float)fg->font_sz_in_pts;
return set_font_size(self, w, w, xdpi, ydpi, desired_height, fg->cell_height);
}
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;
CPY(units_per_EM); CPY(ascender); CPY(descender); CPY(height); CPY(max_advance_width); CPY(max_advance_height); CPY(underline_position); CPY(underline_thickness);
#undef CPY
self->is_scalable = FT_IS_SCALABLE(self->face);
self->has_color = FT_HAS_COLOR(self->face);
self->hinting = hinting; self->hintstyle = hintstyle;
if (!set_size_for_face((PyObject*)self, 0, false, fg)) return false;
self->harfbuzz_font = hb_ft_font_create(self->face, NULL);
if (self->harfbuzz_font == NULL) { PyErr_NoMemory(); return false; }
hb_ft_font_set_load_flags(self->harfbuzz_font, get_load_flags(self->hinting, self->hintstyle, FT_LOAD_DEFAULT));
TT_OS2 *os2 = (TT_OS2*)FT_Get_Sfnt_Table(self->face, FT_SFNT_OS2);
if (os2 != NULL) {
self->strikethrough_position = os2->yStrikeoutPosition;
self->strikethrough_thickness = os2->yStrikeoutSize;
}
self->path = path;
Py_INCREF(self->path);
self->index = self->face->face_index & 0xFFFF;
self->space_glyph_id = glyph_id_for_codepoint((PyObject*)self, ' ');
return true;
}
PyObject*
face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
#define D(key, conv, missing_ok) { \
PyObject *t = PyDict_GetItemString(descriptor, #key); \
if (t == NULL) { \
if (!missing_ok) { PyErr_SetString(PyExc_KeyError, "font descriptor is missing the key: " #key); return NULL; } \
} else key = conv(t); \
}
const char *path = NULL;
long index = 0;
bool hinting = false;
long hint_style = 0;
D(path, PyUnicode_AsUTF8, false);
D(index, PyLong_AsLong, true);
D(hinting, PyObject_IsTrue, true);
D(hint_style, PyLong_AsLong, true);
#undef D
Face *self = (Face *)Face_Type.tp_alloc(&Face_Type, 0);
if (self != NULL) {
int error = FT_New_Face(library, path, index, &(self->face));
if(error) { set_freetype_error("Failed to load face, with error:", error); Py_CLEAR(self); return NULL; }
if (!init_ft_face(self, PyDict_GetItemString(descriptor, "path"), hinting, hint_style, fg)) { Py_CLEAR(self); return NULL; }
}
return (PyObject*)self;
}
FT_Face
native_face_from_path(const char *path, int index) {
int error;
FT_Face ans;
error = FT_New_Face(library, path, index, &ans);
if (error) { set_freetype_error("Failed to load face, with error:", error); return NULL; }
return ans;
}
PyObject*
face_from_path(const char *path, int index, FONTS_DATA_HANDLE fg) {
Face *ans = (Face*)Face_Type.tp_alloc(&Face_Type, 0);
if (ans == NULL) return NULL;
int error;
error = FT_New_Face(library, path, index, &ans->face);
if (error) { set_freetype_error("Failed to load face, with error:", error); ans->face = NULL; return NULL; }
if (!init_ft_face(ans, Py_None, true, 3, fg)) { Py_CLEAR(ans); return NULL; }
return (PyObject*)ans;
}
static void
dealloc(Face* self) {
if (self->harfbuzz_font) hb_font_destroy(self->harfbuzz_font);
if (self->face) FT_Done_Face(self->face);
if (self->extra_data && self->free_extra_data) self->free_extra_data(self->extra_data);
Py_CLEAR(self->path);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject *
repr(Face *self) {
const char *ps_name = FT_Get_Postscript_Name(self->face);
return PyUnicode_FromFormat(
"Face(family=%s, style=%s, ps_name=%s, path=%S, index=%d, is_scalable=%S, has_color=%S, ascender=%i, descender=%i, height=%i, underline_position=%i, underline_thickness=%i, strikethrough_position=%i, strikethrough_thickness=%i)",
self->face->family_name ? self->face->family_name : "", self->face->style_name ? self->face->style_name : "",
ps_name ? ps_name: "",
self->path, self->index, self->is_scalable ? Py_True : Py_False, self->has_color ? Py_True : Py_False,
self->ascender, self->descender, self->height, self->underline_position, self->underline_thickness, self->strikethrough_position, self->strikethrough_thickness
);
}
const char*
postscript_name_for_face(const PyObject *face_) {
const Face *self = (const Face*)face_;
const char *ps_name = FT_Get_Postscript_Name(self->face);
return ps_name ? ps_name : "";
}
static unsigned int
calc_cell_width(Face *self) {
unsigned int ans = 0;
for (char_type i = 32; i < 128; i++) {
int glyph_index = FT_Get_Char_Index(self->face, i);
if (load_glyph(self, glyph_index, FT_LOAD_DEFAULT)) {
ans = MAX(ans, (unsigned int)ceilf((float)self->face->glyph->metrics.horiAdvance / 64.f));
}
}
return ans;
}
static unsigned int
adjust_ypos(unsigned int pos, unsigned int cell_height, int adjustment) {
if (adjustment >= 0) adjustment = MIN(adjustment, (int)pos - 1);
else adjustment = MAX(adjustment, (int)pos - (int)cell_height + 1);
return pos - adjustment;
}
void
cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, unsigned int* baseline, unsigned int* underline_position, unsigned int* underline_thickness, unsigned int* strikethrough_position, unsigned int* strikethrough_thickness) {
Face *self = (Face*)s;
*cell_width = calc_cell_width(self);
*cell_height = calc_cell_height(self, true);
int baseline_offset = 0;
if (OPT(adjust_baseline_px) != 0) baseline_offset = OPT(adjust_baseline_px);
else if (OPT(adjust_baseline_frac) != 0) baseline_offset = (int)(*cell_height * OPT(adjust_baseline_frac));
*baseline = font_units_to_pixels_y(self, self->ascender);
*underline_position = MIN(*cell_height - 1, (unsigned int)font_units_to_pixels_y(self, MAX(0, self->ascender - self->underline_position)));
*underline_thickness = MAX(1, font_units_to_pixels_y(self, self->underline_thickness));
if (self->strikethrough_position != 0) {
*strikethrough_position = MIN(*cell_height - 1, (unsigned int)font_units_to_pixels_y(self, MAX(0, self->ascender - self->strikethrough_position)));
} else {
*strikethrough_position = (unsigned int)floor(*baseline * 0.65);
}
if (self->strikethrough_thickness > 0) {
*strikethrough_thickness = MAX(1, font_units_to_pixels_y(self, self->strikethrough_thickness));
} else {
*strikethrough_thickness = *underline_thickness;
}
if (baseline_offset) {
*baseline = adjust_ypos(*baseline, *cell_height, baseline_offset);
*underline_position = adjust_ypos(*underline_position, *cell_height, baseline_offset);
*strikethrough_position = adjust_ypos(*strikethrough_position, *cell_height, baseline_offset);
}
}
unsigned int
glyph_id_for_codepoint(PyObject *s, char_type cp) {
return FT_Get_Char_Index(((Face*)s)->face, cp);
}
bool
is_glyph_empty(PyObject *s, glyph_index g) {
Face *self = (Face*)s;
if (!load_glyph(self, g, FT_LOAD_DEFAULT)) { PyErr_Print(); return false; }
#define M self->face->glyph->metrics
/* printf("glyph: %u horiBearingX: %ld horiBearingY: %ld width: %ld height: %ld\n", g, M.horiBearingX, M.horiBearingY, M.width, M.height); */
return M.width == 0;
#undef M
}
int
get_glyph_width(PyObject *s, glyph_index g) {
Face *self = (Face*)s;
if (!load_glyph(self, g, FT_LOAD_DEFAULT)) { PyErr_Print(); return 0; }
#define M self->face->glyph->metrics
#define B self->face->glyph->bitmap
/* printf("glyph: %u bitmap.width: %d bitmap.rows: %d horiAdvance: %ld horiBearingX: %ld horiBearingY: %ld vertBearingX: %ld vertBearingY: %ld vertAdvance: %ld width: %ld height: %ld\n", */
/* g, B.width, B.rows, M.horiAdvance, M.horiBearingX, M.horiBearingY, M.vertBearingX, M.vertBearingY, M.vertAdvance, M.width, M.height); */
return B.width ? (int)B.width : (int)(M.width / 64);
#undef M
#undef B
}
hb_font_t*
harfbuzz_font_for_face(PyObject *self) { return ((Face*)self)->harfbuzz_font; }
typedef struct {
unsigned char* buf;
size_t start_x, width, stride;
size_t rows;
FT_Pixel_Mode pixel_mode;
bool needs_free;
unsigned int factor, right_edge;
int bitmap_left, bitmap_top;
} ProcessedBitmap;
static void
free_processed_bitmap(ProcessedBitmap *bm) {
if (bm->needs_free) {
bm->needs_free = false;
free(bm->buf); bm->buf = NULL;
}
}
static void
trim_borders(ProcessedBitmap *ans, size_t extra) {
bool column_has_text = false;
// Trim empty columns from the right side of the bitmap
for (ssize_t x = ans->width - 1; !column_has_text && x > -1 && extra > 0; x--) {
for (size_t y = 0; y < ans->rows && !column_has_text; y++) {
if (ans->buf[x + y * ans->stride] > 200) column_has_text = true;
}
if (!column_has_text) { ans->width--; extra--; }
}
// Remove any remaining extra columns from the left edge of the bitmap
ans->start_x = extra;
ans->width -= extra;
}
static void
populate_processed_bitmap(FT_GlyphSlotRec *slot, FT_Bitmap *bitmap, ProcessedBitmap *ans, bool copy_buf) {
ans->stride = bitmap->pitch < 0 ? -bitmap->pitch : bitmap->pitch;
ans->rows = bitmap->rows;
if (copy_buf) {
ans->buf = calloc(ans->rows, ans->stride);
if (!ans->buf) fatal("Out of memory");
ans->needs_free = true;
memcpy(ans->buf, bitmap->buffer, ans->rows * ans->stride);
} else ans->buf = bitmap->buffer;
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;
}
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 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) {
if (!load_glyph(self, glyph_id, FT_LOAD_RENDER)) return false;
unsigned int max_width = cell_width * num_cells;
// Embedded bitmap glyph?
if (self->face->glyph->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
FT_Bitmap bitmap;
freetype_convert_mono_bitmap(&self->face->glyph->bitmap, &bitmap);
populate_processed_bitmap(self->face->glyph, &bitmap, ans, true);
FT_Bitmap_Done(library, &bitmap);
} else {
populate_processed_bitmap(self->face->glyph, &self->face->glyph->bitmap, ans, false);
}
if (ans->width > max_width) {
size_t extra = ans->width - max_width;
if (italic && extra < cell_width / 2) {
trim_borders(ans, extra);
} else if (extra == 2 && num_cells == 1) {
// there exist fonts that have bitmaps just a couple of pixels
// wider than their advances, rather than rescale, which looks
// bad, we just crop the bitmap on the right. See https://github.com/kovidgoyal/kitty/issues/352
} else if (rescale && self->is_scalable && extra > 1) {
FT_F26Dot6 char_width = self->char_width, char_height = self->char_height;
float ar = (float)max_width / (float)ans->width;
if (set_font_size(self, (FT_F26Dot6)((float)self->char_width * ar), (FT_F26Dot6)((float)self->char_height * ar), self->xdpi, self->ydpi, 0, fg->cell_height)) {
free_processed_bitmap(ans);
if (!render_bitmap(self, glyph_id, ans, cell_width, cell_height, num_cells, bold, italic, false, fg)) return false;
if (!set_font_size(self, char_width, char_height, self->xdpi, self->ydpi, 0, fg->cell_height)) return false;
} else return false;
}
}
return true;
}
int
downsample_32bit_image(uint8_t *src, unsigned src_width, unsigned src_height, unsigned src_stride, uint8_t *dest, unsigned dest_width, unsigned dest_height) {
// Downsample using a simple area averaging algorithm. Could probably do
// better with bi-cubic or lanczos, but at these small sizes I don't think
// it matters
float ratio = MAX((float)src_width / dest_width, (float)src_height / dest_height);
int factor = (int)ceilf(ratio);
uint8_t *d = dest;
for (unsigned int i = 0, sr = 0; i < dest_height; i++, sr += factor) {
for (unsigned int j = 0, sc = 0; j < dest_width; j++, sc += factor, d += 4) {
// calculate area average
unsigned int r=0, g=0, b=0, a=0, count=0;
for (unsigned int y=sr; y < MIN(sr + factor, src_height); y++) {
uint8_t *p = src + (y * src_stride) + sc * 4;
for (unsigned int x=sc; x < MIN(sc + factor, src_width); x++, count++) {
b += *(p++); g += *(p++); r += *(p++); a += *(p++);
}
}
if (count) {
d[0] = b / count; d[1] = g / count; d[2] = r / count; d[3] = a / count;
}
}
}
return factor;
}
static void
downsample_bitmap(ProcessedBitmap *bm, unsigned int width, unsigned int cell_height) {
uint8_t *dest = calloc(4, (size_t)width * cell_height);
if (dest == NULL) fatal("Out of memory");
bm->factor = downsample_32bit_image(bm->buf, bm->width, bm->rows, bm->stride, dest, width, cell_height);
bm->buf = dest; bm->needs_free = true; bm->stride = 4 * width; bm->width = width; bm->rows = cell_height;
}
static void
detect_right_edge(ProcessedBitmap *ans) {
ans->right_edge = 0;
for (ssize_t x = ans->width - 1; !ans->right_edge && x > -1; x--) {
for (size_t y = 0; y < ans->rows && !ans->right_edge; y++) {
uint8_t *p = ans->buf + x * 4 + y * ans->stride;
if (p[3] > 20) ans->right_edge = x;
}
}
}
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 short limit = self->face->num_fixed_sizes;
for (short i = 0; i < limit; 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;
}
}
FT_Error error = FT_Select_Size(self->face, best);
if (error) { set_freetype_error("Failed to set char size for non-scalable font, with error:", error); return false; }
if (!load_glyph(self, glyph_id, FT_LOAD_COLOR)) return false;
FT_Set_Char_Size(self->face, 0, self->char_height, self->xdpi, self->ydpi);
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
if (bitmap->pixel_mode != FT_PIXEL_MODE_BGRA) return false;
ans->buf = bitmap->buffer;
ans->start_x = 0; ans->width = bitmap->width;
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);
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);
return true;
}
static void
copy_color_bitmap(uint8_t *src, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride) {
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;
uint8_t *s = src + 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 *bgra = s + 4 * sc;
if (bgra[3]) {
#define C(idx, shift) ( (uint8_t)(((float)bgra[idx] / (float)bgra[3]) * 255) << shift)
d[dc] = C(2, 24) | C(1, 16) | C(0, 8) | bgra[3];
#undef C
} else d[dc] = 0;
}
}
}
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) {
// 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.
Region src = { .left = bm->start_x, .bottom = bm->rows, .right = bm->width + bm->start_x }, dest = { .bottom = cell_height, .right = cell_width };
// Calculate column bounds
int32_t xoff = (int32_t)(x_offset + bm->bitmap_left);
if (debug_placement) printf(" bitmap_left: %d xoff: %d", bm->bitmap_left, xoff);
if (xoff < 0) src.left += -xoff;
else dest.left = xoff;
// Move the dest start column back if the width overflows because of it, but only if we are not in a very long/infinite ligature
if (glyph_num < 4 && dest.left > 0 && dest.left + bm->width > cell_width) {
uint32_t extra = dest.left + bm->width - cell_width;
dest.left = extra > dest.left ? 0 : dest.left - extra;
}
// Calculate row bounds
int32_t yoff = (ssize_t)(y_offset + bm->bitmap_top);
if ((yoff > 0 && (size_t)yoff > baseline)) {
dest.top = 0;
} else {
dest.top = baseline - yoff;
}
/* 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);
} else render_alpha_mask(bm->buf, cell, &src, &dest, bm->stride, cell_width);
}
static const ProcessedBitmap EMPTY_PBM = {.factor = 1};
bool
render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *info, hb_glyph_position_t *positions, unsigned int num_glyphs, pixel *canvas, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline, bool *was_colored, FONTS_DATA_HANDLE fg, bool center_glyph) {
Face *self = (Face*)f;
bool is_emoji = *was_colored; *was_colored = is_emoji && self->has_color;
float x = 0.f, y = 0.f, x_offset = 0.f;
ProcessedBitmap bm;
unsigned int canvas_width = cell_width * num_cells;
for (unsigned int i = 0; i < num_glyphs; i++) {
bm = EMPTY_PBM;
// dont load the space glyph since loading it fails for some fonts/sizes and it is anyway to be rendered as a blank
if (info[i].codepoint != self->space_glyph_id) {
if (*was_colored) {
if (!render_color_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, baseline)) {
if (PyErr_Occurred()) PyErr_Print();
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) {
free_processed_bitmap(&bm);
return false;
}
*was_colored = false;
}
} else {
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) {
free_processed_bitmap(&bm);
return false;
}
}
}
x_offset = x + (float)positions[i].x_offset / 64.0f;
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);
}
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
// use: kitty --config None -o 'font_family Fira Code' -o 'font_size 4.5' sh -c
// "echo '|---|--------|-------|-------------|-------------|HH'; read"
// if this causes issues with non-infinite ligatures, we could choose this behavior
// based on num_glyphs and/or num_cells
x += roundf((float)positions[i].x_advance / 64.0f);
free_processed_bitmap(&bm);
}
if (center_glyph && num_glyphs) {
unsigned int right_edge = (unsigned int)x, delta;
// x_advance is wrong for colored bitmaps that have been downsampled
if (*was_colored) right_edge = num_glyphs == 1 ? bm.right_edge : canvas_width;
if (num_cells > 1 && right_edge < canvas_width && (delta = (canvas_width - right_edge) / 2) && delta > 1) {
right_shift_canvas(canvas, canvas_width, cell_height, delta);
}
}
return true;
}
static PyObject*
display_name(PyObject *s, PyObject *a UNUSED) {
Face *self = (Face*)s;
const char *psname = FT_Get_Postscript_Name(self->face);
if (psname) return Py_BuildValue("s", psname);
Py_INCREF(self->path);
return self->path;
}
static PyObject*
extra_data(PyObject *self, PyObject *a UNUSED) {
return PyLong_FromVoidPtr(((Face*)self)->extra_data);
}
StringCanvas
render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
Face *self = (Face*)s;
StringCanvas ans = {0};
size_t num_chars = strnlen(text, 32);
int max_char_width = font_units_to_pixels_x(self, self->face->max_advance_width);
size_t canvas_width = max_char_width * (num_chars*2);
size_t canvas_height = font_units_to_pixels_y(self, self->face->height) + 8;
pixel *canvas = calloc(canvas_width * canvas_height, sizeof(pixel));
if (!canvas) return ans;
size_t pen_x = 0;
ProcessedBitmap pbm;
for (size_t n = 0; n < num_chars; n++) {
FT_UInt glyph_index = FT_Get_Char_Index(self->face, text[n]);
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;
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);
pen_x += self->face->glyph->advance.x >> 6;
}
ans.width = pen_x; ans.height = canvas_height;
ans.canvas = malloc(ans.width * ans.height);
if (ans.canvas) {
for (size_t row = 0; row < ans.height; row++) {
unsigned char *destp = ans.canvas + (ans.width * row);
pixel *srcp = canvas + (canvas_width * row);
for (size_t i = 0; i < ans.width; i++) destp[i] = srcp[i] & 0xff;
}
}
free(canvas);
return ans;
}
// Boilerplate {{{
static PyMemberDef members[] = {
#define MEM(name, type) {#name, type, offsetof(Face, name), READONLY, #name}
MEM(units_per_EM, T_UINT),
MEM(ascender, T_INT),
MEM(descender, T_INT),
MEM(height, T_INT),
MEM(max_advance_width, T_INT),
MEM(max_advance_height, T_INT),
MEM(underline_position, T_INT),
MEM(underline_thickness, T_INT),
MEM(strikethrough_position, T_INT),
MEM(strikethrough_thickness, T_INT),
MEM(is_scalable, T_BOOL),
MEM(path, T_OBJECT_EX),
{NULL} /* Sentinel */
};
static PyMethodDef methods[] = {
METHODB(display_name, METH_NOARGS),
METHODB(extra_data, METH_NOARGS),
{NULL} /* Sentinel */
};
PyTypeObject Face_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.Face",
.tp_basicsize = sizeof(Face),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "FreeType Font face",
.tp_methods = methods,
.tp_members = members,
.tp_repr = (reprfunc)repr,
};
static void
free_freetype(void) {
FT_Done_FreeType(library);
}
bool
init_freetype_library(PyObject *m) {
if (PyType_Ready(&Face_Type) < 0) return 0;
if (PyModule_AddObject(m, "Face", (PyObject *)&Face_Type) != 0) return 0;
Py_INCREF(&Face_Type);
FreeType_Exception = PyErr_NewException("fast_data_types.FreeTypeError", NULL, NULL);
if (FreeType_Exception == NULL) return false;
if (PyModule_AddObject(m, "FreeTypeError", FreeType_Exception) != 0) return false;
int error = FT_Init_FreeType(&library);
if (error) {
set_freetype_error("Failed to initialize FreeType library, with error:", error);
return false;
}
register_at_exit_cleanup_func(FREETYPE_CLEANUP_FUNC, free_freetype);
return true;
}
// }}}