Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ligature using harfbuzz #1659

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
19 changes: 18 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,24 @@ if not get_option('source-only')
default_options: default_fallback_options + sdl_options
)

lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl]
harfbuzz_options = ['default_library=static']

harfbuzz_options += 'glib=disabled'
harfbuzz_options += 'gobject=disabled'
harfbuzz_options += 'cairo=disabled'
harfbuzz_options += 'icu=disabled'
harfbuzz_options += 'freetype=enabled'
harfbuzz_options += 'tests=disabled'
harfbuzz_options += 'introspection=disabled'
harfbuzz_options += 'docs=disabled'
harfbuzz_options += 'doc_tests=false'
harfbuzz_options += 'utilities=disabled'

harfbuzz_dep = dependency('harfbuzz', fallback: ['harfbuzz'],
default_options: default_fallback_options + harfbuzz_options
)

lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl, harfbuzz_dep]
endif
#===============================================================================
# Install Configuration
Expand Down
175 changes: 110 additions & 65 deletions src/renderer.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
#include "renderer.h"
#include "renwindow.h"

#include <hb.h>
#include <hb-ft.h>

#define MAX_UNICODE 0x100000
#define GLYPHSET_SIZE 256
#define GLYPHSET_SIZE 8
#define MAX_LOADABLE_GLYPHSETS (MAX_UNICODE / GLYPHSET_SIZE)
#define SUBPIXEL_BITMAPS_CACHED 3

Expand Down Expand Up @@ -52,8 +55,10 @@ typedef struct {
typedef struct RenFont {
FT_Face face;
FT_StreamRec stream;
hb_font_t *font;
GlyphSet* sets[SUBPIXEL_BITMAPS_CACHED][MAX_LOADABLE_GLYPHSETS];
float size, space_advance, tab_advance;
float size, space_advance;
int tab_size;
unsigned short max_height, baseline, height;
ERenFontAntialiasing antialiasing;
ERenFontHinting hinting;
Expand All @@ -62,23 +67,6 @@ typedef struct RenFont {
char path[];
} RenFont;

static const char* utf8_to_codepoint(const char *p, unsigned *dst) {
const unsigned char *up = (unsigned char*)p;
unsigned res, n;
switch (*p & 0xf0) {
case 0xf0 : res = *up & 0x07; n = 3; break;
case 0xe0 : res = *up & 0x0f; n = 2; break;
case 0xd0 :
case 0xc0 : res = *up & 0x1f; n = 1; break;
default : res = *up; n = 0; break;
}
while (n--) {
res = (res << 6) | (*(++up) & 0x3f);
}
*dst = res;
return (const char*)up + 1;
}

static int font_set_load_options(RenFont* font) {
int load_target = font->antialiasing == FONT_ANTIALIASING_NONE ? FT_LOAD_TARGET_MONO
: (font->hinting == FONT_HINTING_SLIGHT ? FT_LOAD_TARGET_LIGHT : FT_LOAD_TARGET_NORMAL);
Expand Down Expand Up @@ -120,15 +108,15 @@ static int font_set_style(FT_Outline* outline, int x_translation, unsigned char
return 0;
}

static void font_load_glyphset(RenFont* font, int idx) {
static void font_load_glyphset(RenFont* font, unsigned int idx) {
unsigned int render_option = font_set_render_options(font), load_option = font_set_load_options(font);
int bitmaps_cached = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1;
unsigned int byte_width = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1;
for (int j = 0, pen_x = 0; j < bitmaps_cached; ++j) {
GlyphSet* set = check_alloc(calloc(1, sizeof(GlyphSet)));
font->sets[j][idx] = set;
for (int i = 0; i < GLYPHSET_SIZE; ++i) {
int glyph_index = FT_Get_Char_Index(font->face, i + idx * GLYPHSET_SIZE);
int glyph_index = i + idx * GLYPHSET_SIZE;
if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, load_option | FT_LOAD_BITMAP_METRICS_ONLY)
|| font_set_style(&font->face->glyph->outline, j * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) || FT_Render_Glyph(font->face->glyph, render_option)) {
continue;
Expand All @@ -141,7 +129,7 @@ static void font_load_glyphset(RenFont* font, int idx) {
pen_x += glyph_width;
font->max_height = slot->bitmap.rows > font->max_height ? slot->bitmap.rows : font->max_height;
// In order to fix issues with monospacing; we need the unhinted xadvance; as FreeType doesn't correctly report the hinted advance for spaces on monospace fonts (like RobotoMono). See #843.
if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, (load_option | FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_HINTING) & ~FT_LOAD_FORCE_AUTOHINT)
if (FT_Load_Glyph(font->face, glyph_index, (load_option | FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_HINTING) & ~FT_LOAD_FORCE_AUTOHINT)
|| font_set_style(&font->face->glyph->outline, j * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) || FT_Render_Glyph(font->face->glyph, render_option)) {
continue;
}
Expand All @@ -153,7 +141,7 @@ static void font_load_glyphset(RenFont* font, int idx) {
set->surface = check_alloc(SDL_CreateRGBSurface(0, pen_x, font->max_height, font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 24 : 8, 0, 0, 0, 0));
uint8_t* pixels = set->surface->pixels;
for (int i = 0; i < GLYPHSET_SIZE; ++i) {
int glyph_index = FT_Get_Char_Index(font->face, i + idx * GLYPHSET_SIZE);
int glyph_index = i + idx * GLYPHSET_SIZE;
if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, load_option))
continue;
FT_GlyphSlot slot = font->face->glyph;
Expand All @@ -174,7 +162,7 @@ static void font_load_glyphset(RenFont* font, int idx) {
}
}
}
}
}

static GlyphSet* font_get_glyphset(RenFont* font, unsigned int codepoint, int subpixel_idx) {
int idx = (codepoint / GLYPHSET_SIZE) % MAX_LOADABLE_GLYPHSETS;
Expand All @@ -183,20 +171,21 @@ static GlyphSet* font_get_glyphset(RenFont* font, unsigned int codepoint, int su
return font->sets[font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? subpixel_idx : 0][idx];
}

static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFont** fonts, unsigned int codepoint, int bitmap_index) {
static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFont** fonts, unsigned int codepoint, unsigned fb_codepoint, int bitmap_index) {
if (!metric) {
return NULL;
}
if (bitmap_index < 0)
bitmap_index += SUBPIXEL_BITMAPS_CACHED;
for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) {
*set = font_get_glyphset(fonts[i], codepoint, bitmap_index);
*metric = &(*set)->metrics[codepoint % GLYPHSET_SIZE];
if ((*metric)->loaded || codepoint < 0xFF)
unsigned cp = i == 0 ? codepoint : FT_Get_Char_Index(fonts[i]->face, fb_codepoint);
*set = font_get_glyphset(fonts[i], cp, bitmap_index);
*metric = &(*set)->metrics[cp % GLYPHSET_SIZE];
if ((*metric)->loaded || fb_codepoint == 0)
return fonts[i];
}
if (*metric && !(*metric)->loaded && codepoint > 0xFF && codepoint != 0x25A1)
return font_group_get_glyph(set, metric, fonts, 0x25A1, bitmap_index);
if (*metric && !(*metric)->loaded && fb_codepoint > 0xFF && fb_codepoint != 0x25A1)
return font_group_get_glyph(set, metric, fonts, 0x25A1, 0x25A1, bitmap_index);
return fonts[0];
}

Expand Down Expand Up @@ -235,8 +224,7 @@ static void font_file_close(FT_Stream stream) {

RenFont* ren_font_load(RenWindow *window_renderer, const char* path, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style) {
RenFont *font = NULL;
FT_Face face = NULL;

FT_Face face = NULL;
SDL_RWops *file = SDL_RWFromFile(path, "rb");
if (!file)
goto rwops_failure;
Expand All @@ -251,7 +239,6 @@ RenFont* ren_font_load(RenWindow *window_renderer, const char* path, float size,

if (FT_Open_Face(library, &(FT_Open_Args){ .flags = FT_OPEN_STREAM, .stream = &font->stream }, 0, &face))
goto failure;

const int surface_scale = renwin_get_surface(window_renderer).scale;
if (FT_Set_Pixel_Sizes(face, 0, (int)(size*surface_scale)))
goto failure;
Expand All @@ -273,13 +260,18 @@ RenFont* ren_font_load(RenWindow *window_renderer, const char* path, float size,
if (FT_Load_Char(face, ' ', font_set_load_options(font)))
goto failure;

font->font = hb_ft_font_create_referenced(face);
if (font->font == 0)
goto failure;
font->space_advance = face->glyph->advance.x / 64.0f;
font->tab_advance = font->space_advance * 2;
font->tab_size = 0;
return font;

failure:
if (face)
FT_Done_Face(face);
if (font && font->font)
hb_font_destroy(font->font);
if (font)
free(font);
return NULL;
Expand All @@ -305,24 +297,39 @@ const char* ren_font_get_path(RenFont *font) {
void ren_font_free(RenFont* font) {
font_clear_glyph_cache(font);
FT_Done_Face(font->face);
hb_font_destroy(font->font);
free(font);
}

void ren_font_group_set_tab_size(RenFont **fonts, int n) {
unsigned int tab_index = '\t' % GLYPHSET_SIZE;
for (int j = 0; j < FONT_FALLBACK_MAX && fonts[j]; ++j) {
for (int i = 0; i < (fonts[j]->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1); ++i)
font_get_glyphset(fonts[j], '\t', i)->metrics[tab_index].xadvance = fonts[j]->space_advance * n;
if (fonts[j]->tab_size == n) continue;
unsigned codepoint = 0;
if (j == 0) {
hb_buffer_t *buff = hb_buffer_create();
hb_buffer_set_direction(buff, HB_DIRECTION_LTR);
hb_buffer_set_script(buff, HB_SCRIPT_LATIN);
hb_buffer_add_utf8(buff, "\t", 1, 0, -1);
hb_shape(fonts[j]->font, buff, NULL, 0);
unsigned counter =0;
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(buff, &counter);
codepoint = info[0].codepoint;
hb_buffer_destroy(buff);
} else {
codepoint = FT_Get_Char_Index(fonts[j]->face, '\t');
}
fonts[j]->tab_size = n;
float f = (n-1) * fonts[j]->space_advance;
unsigned index = codepoint % GLYPHSET_SIZE;
for (int i = 0; i < (fonts[j]->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1); ++i) {
GlyphSet * set = font_get_glyphset(fonts[j], codepoint, i);
if (set != 0 && index < GLYPHSET_SIZE) set->metrics[index].xadvance = f;
}
}
}

int ren_font_group_get_tab_size(RenFont **fonts) {
unsigned int tab_index = '\t' % GLYPHSET_SIZE;
float advance = font_get_glyphset(fonts[0], '\t', 0)->metrics[tab_index].xadvance;
if (fonts[0]->space_advance) {
advance /= fonts[0]->space_advance;
}
return advance;
return fonts[0]->tab_size;
}

float ren_font_group_get_size(RenFont **fonts) {
Expand All @@ -340,26 +347,50 @@ void ren_font_group_set_size(RenWindow *window_renderer, RenFont **fonts, float
fonts[i]->baseline = (short)((face->ascender / (float)face->units_per_EM) * size);
FT_Load_Char(face, ' ', font_set_load_options(fonts[i]));
fonts[i]->space_advance = face->glyph->advance.x / 64.0f;
fonts[i]->tab_advance = fonts[i]->space_advance * 2;
}
}

int ren_font_group_get_height(RenFont **fonts) {
return fonts[0]->height;
}

static const unsigned utf8_to_codepoint(const char *p) {
const unsigned char *up = (unsigned char*)p;
unsigned res, n;
switch (*p & 0xf0) {
case 0xf0 : res = *up & 0x07; n = 3; break;
case 0xe0 : res = *up & 0x0f; n = 2; break;
case 0xd0 :
case 0xc0 : res = *up & 0x1f; n = 1; break;
default : res = *up; n = 0; break;
}
while (n--) {
res = (res << 6) | (*(++up) & 0x3f);
}
return res;
}

double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len) {
double width = 0;
const char* end = text + len;
GlyphMetric* metric = NULL; GlyphSet* set = NULL;
while (text < end) {
unsigned int codepoint;
text = utf8_to_codepoint(text, &codepoint);
RenFont* font = font_group_get_glyph(&set, &metric, fonts, codepoint, 0);
hb_buffer_t *buf;
buf = hb_buffer_create();
hb_buffer_set_direction(buf, HB_DIRECTION_LTR);
hb_buffer_set_script(buf, HB_SCRIPT_LATIN);
hb_buffer_add_utf8(buf, text, -1, 0, -1);
RenFont * font = fonts[0];
hb_shape(font->font, buf, NULL, 0);
unsigned int glyph_count;
hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count);
for (unsigned int i = 0; i < glyph_count; i++) {
unsigned int codepoint = glyph_info[i].codepoint;
unsigned fb_codepoint = utf8_to_codepoint(&text[glyph_info[i].cluster]);
RenFont* font = font_group_get_glyph(&set, &metric, fonts, codepoint, fb_codepoint, 0);
if (!metric)
break;
width += (!font || metric->xadvance) ? metric->xadvance : fonts[0]->space_advance;
}
hb_buffer_destroy(buf);
const int surface_scale = renwin_get_surface(window_renderer).scale;
return width / surface_scale;
}
Expand All @@ -373,27 +404,39 @@ double ren_draw_text(RenSurface *rs, RenFont **fonts, const char *text, size_t l
double pen_x = x * surface_scale;
y *= surface_scale;
int bytes_per_pixel = surface->format->BytesPerPixel;
const char* end = text + len;
// const char* end = text + len;
uint8_t* destination_pixels = surface->pixels;
int clip_end_x = clip.x + clip.w, clip_end_y = clip.y + clip.h;

RenFont* last = NULL;
double last_pen_x = x;
bool underline = fonts[0]->style & FONT_STYLE_UNDERLINE;
bool strikethrough = fonts[0]->style & FONT_STYLE_STRIKETHROUGH;

while (text < end) {
unsigned int codepoint, r, g, b;
text = utf8_to_codepoint(text, &codepoint);
// convert text in glyphs
hb_buffer_t *buf;
buf = hb_buffer_create();
hb_buffer_set_direction(buf, HB_DIRECTION_LTR);
hb_buffer_set_script(buf, HB_SCRIPT_LATIN);
hb_buffer_add_utf8(buf, text, len, 0, -1);

RenFont * font = fonts[0];
hb_shape(font->font, buf, NULL, 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question, in case that a fallback font is used for some codepoints not available on main font, shouldn't that portion get shaped also with the fallback font instead of using the main font?

Copy link
Author

@Mandarancio Mandarancio Nov 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, as it is implemented now, ligatures are supported only for the main font and so the shaping is only applied to the main one, for the fallaback case I am using the fb_codepoint that is computed in src/renderer.c:413 that is then converted if needed to the correct font index using FT_Get_Char_Index at src/renderer.c:181.
I hope the explanation is clear.
It would be possible to shape for all the available fonts but I suspect will be more computational intensive and you could end up with half a ligature of a fallback font and half a normal character form another.
What do you think?

edit: also I am not sure that the mapping between text and glyph would be the same and could be possible that some kind of mapping would be needed between different fonts and the string.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function shared on pragtical/pragtical@ed28c3a iterates a string giving you the range of a string (thru the char** next out param) and returns the font that has all of its codepoints for rendering:

static RenFont* utf8_iterator(
  RenFont** fonts, const char* start, const char* end, char** next
) {
  if (start == end) return NULL;

  unsigned int codepoint;
  RenFont* current_font = fonts[0];

  bool iterated = false;
  *next = (char*) start;
  char* prev_next = *next;
  while(*next < end) {
    *next = (char*) utf8_to_codepoint(*next, &codepoint);
    if (!FT_Get_Char_Index(current_font->face, codepoint)) {
      if (!iterated) {
        bool codepoint_found = false;
        for (short i = 1; i < FONT_FALLBACK_MAX && fonts[i]; ++i) {
          if (FT_Get_Char_Index(fonts[i]->face, codepoint)) {
            current_font = fonts[i];
            codepoint_found = true;
            break;
          }
        }
        if (!codepoint_found)
          return fonts[0];
      } else {
        // set next to the end of previously iterated text range
        *next = prev_next;
        return current_font;
      }
    }
    // Allow emojis to be rendered with different fonts, this is needed because
    // sometimes a single glyph can be queried, which could be on the first
    // font and also on previously used fallbacks when rendering.
    if (!iterated && (*next - start) > 3) {
      return current_font;
    }
    prev_next = *next;
    iterated = true;
  }

  return current_font;
}

This way you can shape string segments using the assigned font (which is the strategy used on that patch). When tested I didn't notice a performance drop but I didn't benchmark the thing :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I start to have a look at your commit but I did not finish yet as I am quite busy now. However I am still not 100% sure that is a good idea to support ligatures in fallback fonts, maybe can @Guldoman gives us his opinion?

unsigned int glyph_count;
hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count);
for (unsigned int i = 0; i < glyph_count; i++) {
unsigned int r, g, b;
unsigned fb_codepoint = utf8_to_codepoint(&text[glyph_info[i].cluster]);
hb_codepoint_t codepoint = glyph_info[i].codepoint;
GlyphSet* set = NULL; GlyphMetric* metric = NULL;
RenFont* font = font_group_get_glyph(&set, &metric, fonts, codepoint, (int)(fmod(pen_x, 1.0) * SUBPIXEL_BITMAPS_CACHED));
RenFont* font = font_group_get_glyph(&set, &metric, fonts, codepoint, fb_codepoint, (int)(fmod(pen_x, 1.0) * SUBPIXEL_BITMAPS_CACHED));
if (!metric)
break;
int start_x = floor(pen_x) + metric->bitmap_left;
int end_x = (metric->x1 - metric->x0) + start_x;
int glyph_end = metric->x1, glyph_start = metric->x0;
if (!metric->loaded && codepoint > 0xFF)
if (!metric->loaded && fb_codepoint > 0xFF)
ren_draw_rect(rs, (RenRect){ start_x + 1, y, font->space_advance - 1, ren_font_group_get_height(fonts) }, color);

if (set->surface && color.a > 0 && end_x >= clip.x && start_x < clip_end_x) {
uint8_t* source_pixels = set->surface->pixels;
for (int line = metric->y0; line < metric->y1; ++line) {
Expand Down Expand Up @@ -441,8 +484,8 @@ double ren_draw_text(RenSurface *rs, RenFont **fonts, const char *text, size_t l
float adv = metric->xadvance ? metric->xadvance : font->space_advance;

if(!last) last = font;
else if(font != last || text == end) {
double local_pen_x = text == end ? pen_x + adv : pen_x;
else if(font != last || i == glyph_count - 1) {
double local_pen_x = i == glyph_count - 1 ? pen_x + adv : pen_x;
if (underline)
ren_draw_rect(rs, (RenRect){last_pen_x, y / surface_scale + last->height - 1, (local_pen_x - last_pen_x) / surface_scale, last->underline_thickness * surface_scale}, color);
if (strikethrough)
Expand All @@ -453,17 +496,18 @@ double ren_draw_text(RenSurface *rs, RenFont **fonts, const char *text, size_t l

pen_x += adv;
}
hb_buffer_destroy(buf);
return pen_x / surface_scale;
}

/******************* Rectangles **********************/
static inline RenColor blend_pixel(RenColor dst, RenColor src) {
int ia = 0xff - src.a;
dst.r = ((src.r * src.a) + (dst.r * ia)) >> 8;
dst.g = ((src.g * src.a) + (dst.g * ia)) >> 8;
dst.b = ((src.b * src.a) + (dst.b * ia)) >> 8;
return dst;
}
// static inline RenColor blend_pixel(RenColor dst, RenColor src) {
// int ia = 0xff - src.a;
// dst.r = ((src.r * src.a) + (dst.r * ia)) >> 8;
// dst.g = ((src.g * src.a) + (dst.g * ia)) >> 8;
// dst.b = ((src.b * src.a) + (dst.b * ia)) >> 8;
// return dst;
// }

void ren_draw_rect(RenSurface *rs, RenRect rect, RenColor color) {
if (color.a == 0) { return; }
Expand Down Expand Up @@ -543,3 +587,4 @@ void ren_get_size(RenWindow *window_renderer, int *x, int *y) {
*x = rs.surface->w / rs.scale;
*y = rs.surface->h / rs.scale;
}

10 changes: 10 additions & 0 deletions subprojects/harfbuzz.wrap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[wrap-file]
directory = harfbuzz-8.2.2
source_url = https://github.com/harfbuzz/harfbuzz/releases/download/8.2.2/harfbuzz-8.2.2.tar.xz
source_filename = harfbuzz-8.2.2.tar.xz
source_hash = e433ad85fbdf57f680be29479b3f964577379aaf319f557eb76569f0ecbc90f3
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/harfbuzz_8.2.2-1/harfbuzz-8.2.2.tar.xz
wrapdb_version = 8.2.2-1

[provide]
dependency_names = harfbuzz
Loading