Use only cluster numbers and glyph sizes for grouping glyphs into cells

See https://github.com/behdad/harfbuzz/issues/615 for discussion
This commit is contained in:
Kovid Goyal 2017-11-12 20:32:36 +05:30
parent 0bd093ec2e
commit 088087df73
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 130 additions and 82 deletions

View File

@ -405,29 +405,8 @@ extract_cell_from_canvas(unsigned int i, unsigned int num_cells) {
} }
static inline void static inline void
render_group(unsigned int num_cells, unsigned int num_glyphs, Cell *cells, hb_glyph_info_t *info, hb_glyph_position_t *positions, Font *font) { render_group(unsigned int num_cells, unsigned int num_glyphs, Cell *cells, hb_glyph_info_t *info, hb_glyph_position_t *positions, Font *font, glyph_index glyph, uint64_t extra_glyphs) {
uint64_t extra_glyphs; static SpritePosition* sprite_position[16];
#define G(n) ((uint64_t)(info[n].codepoint & 0xffff))
glyph_index glyph = G(0);
SpritePosition* sprite_position[5];
switch(num_glyphs) {
case 1:
extra_glyphs = 0;
break;
case 2:
extra_glyphs = G(1);
break;
case 3:
extra_glyphs = G(1) | (G(2) << 16);
break;
case 4:
extra_glyphs = G(1) | (G(2) << 16) | (G(3) << 32);
break;
default: // we only support a maximum of four extra glyphs per cell
extra_glyphs = G(1) | (G(2) << 16) | (G(3) << 32) | (G(4) << 48);
break;
}
#undef G
int error = 0; int error = 0;
for (unsigned int i = 0; i < num_cells; i++) { for (unsigned int i = 0; i < num_cells; i++) {
sprite_position[i] = sprite_position_for(font, glyph, extra_glyphs, (uint8_t)i, &error); sprite_position[i] = sprite_position_for(font, glyph, extra_glyphs, (uint8_t)i, &error);
@ -450,40 +429,110 @@ render_group(unsigned int num_cells, unsigned int num_glyphs, Cell *cells, hb_gl
} }
static inline void static inline bool
next_group(unsigned int *num_group_cells, unsigned int *num_group_glyphs, Cell *cells, hb_glyph_info_t *info, hb_glyph_position_t *positions, unsigned int num_glyphs, unsigned int num_cells) { is_dummy_glyph(glyph_index glyph_id, hb_font_t *font) {
num_glyphs = MIN(num_glyphs, 5); // we only support groupes of upto 5 glyphs // we assume glyphs with no width are dummy glyphs used for a contextual ligature, so skip it
*num_group_cells = 0, *num_group_glyphs = 0; static hb_glyph_extents_t extents;
bool unsafe_to_break = false; hb_font_get_glyph_extents(font, glyph_id, &extents);
do { return extents.width == 0;
// If the glyph has no advance, then it is a combining char }
if (positions[*num_group_glyphs].x_advance != 0) *num_group_cells += ((cells[*num_group_cells].attrs & WIDTH_MASK) == 2) ? 2 : 1;
uint32_t cluster = info[*num_group_glyphs].cluster; static inline int
num_codepoints_in_cell(Cell *cell) {
// check if the next glyph can be broken at unsigned int ans = 1;
#define IS_COMBINING_GLYPH ((unsafe_to_break = info[*num_group_glyphs].mask & HB_GLYPH_FLAG_UNSAFE_TO_BREAK || info[*num_group_glyphs].cluster == cluster)) if (cell->cc) ans += ((cell->cc >> CC_SHIFT) & CC_MASK) ? 2 : 1;
// Soak up all combining char glyphs return ans;
do { }
*num_group_glyphs += 1;
} while (*num_group_glyphs < num_glyphs && IS_COMBINING_GLYPH && positions[*num_group_glyphs].x_advance == 0); typedef struct {
Cell *cell;
} while (unsafe_to_break && *num_group_cells < num_cells && *num_group_glyphs < MIN(num_glyphs, 6)); int num_codepoints;
#undef IS_COMBINING_GLYPH } CellData;
*num_group_cells = MAX(1, MIN(*num_group_cells, num_cells));
*num_group_glyphs = MAX(1, MIN(*num_group_glyphs, num_glyphs)); static inline unsigned int
check_cell_consumed(CellData *cell_data, Cell *last_cell) {
cell_data->num_codepoints--;
if (cell_data->num_codepoints <= 0) {
attrs_type width = cell_data->cell->attrs & WIDTH_MASK;
cell_data->cell += MAX(1, width);
if (cell_data->cell <= last_cell) cell_data->num_codepoints = num_codepoints_in_cell(cell_data->cell);
return width;
}
return 0;
}
static inline glyph_index
next_group(hb_font_t *font, unsigned int *num_group_cells, unsigned int *num_group_glyphs, Cell *cells, hb_glyph_info_t *info, unsigned int max_num_glyphs, unsigned int max_num_cells, uint64_t *extra_glyphs) {
// See https://github.com/behdad/harfbuzz/issues/615 for a discussion of
// how to break text into cells. In addition, we have to deal with
// monospace ligature fonts that use dummy glyphs of zero size to implement
// their ligatures.
CellData cell_data;
cell_data.cell = cells; cell_data.num_codepoints = num_codepoints_in_cell(cells);
glyph_index significant_glyphs[5];
significant_glyphs[0] = 0;
unsigned int num_significant_glyphs = 0;
unsigned int ncells = 0, nglyphs = 0, n;
uint32_t previous_cluster = UINT32_MAX, cluster;
Cell *last_cell = cells + max_num_cells;
while(num_significant_glyphs < sizeof(significant_glyphs)/sizeof(significant_glyphs[0]) && ncells < max_num_cells && nglyphs < max_num_glyphs) {
glyph_index glyph_id = info[nglyphs].codepoint;
cluster = info[nglyphs].cluster;
nglyphs += 1;
bool is_dummy = is_dummy_glyph(glyph_id, font);
if (!is_dummy) significant_glyphs[num_significant_glyphs++] = glyph_id;
if (cluster > previous_cluster || nglyphs == 1) {
n = nglyphs == 1 ? 1 : cluster - previous_cluster;
unsigned int before = ncells;
while(n-- && ncells < max_num_cells) ncells += check_cell_consumed(&cell_data, last_cell);
if (ncells > before && !is_dummy) break;
}
previous_cluster = cluster;
}
*num_group_cells = MAX(1, MIN(ncells, max_num_cells));
*num_group_glyphs = MAX(1, MIN(nglyphs, max_num_glyphs));
#define G(n) ((uint64_t)(significant_glyphs[n] & 0xffff))
switch(num_significant_glyphs) {
case 0:
case 1:
*extra_glyphs = 0;
break;
case 2:
*extra_glyphs = G(1);
break;
case 3:
*extra_glyphs = G(1) | (G(2) << 16);
break;
case 4:
*extra_glyphs = G(1) | (G(2) << 16) | (G(3) << 32);
break;
default: // we only support a maximum of four extra glyphs per cell
*extra_glyphs = G(1) | (G(2) << 16) | (G(3) << 32) | (G(4) << 48);
break;
}
#undef G
return significant_glyphs[0];
}
static inline unsigned int
shape(Cell *first_cell, index_type num_cells, hb_font_t *font, hb_glyph_info_t **info, hb_glyph_position_t **positions) {
load_hb_buffer(first_cell, num_cells);
hb_shape(font, harfbuzz_buffer, NULL, 0);
unsigned int info_length, positions_length;
*info = hb_buffer_get_glyph_infos(harfbuzz_buffer, &info_length);
*positions = hb_buffer_get_glyph_positions(harfbuzz_buffer, &positions_length);
if (!info || !positions) return 0;
return MIN(info_length, positions_length);
} }
static const char* SHAPERS[] = {"ot", NULL};
static inline void static inline void
shape_run(Cell *first_cell, index_type num_cells, Font *font) { shape_run(Cell *first_cell, index_type num_cells, Font *font) {
load_hb_buffer(first_cell, num_cells); hb_glyph_info_t *info;
hb_shape_full(font->hb_font, harfbuzz_buffer, NULL, 0, SHAPERS); hb_glyph_position_t *positions;
unsigned int info_length, positions_length, num_glyphs; unsigned int num_glyphs = shape(first_cell, num_cells, font->hb_font, &info, &positions);
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(harfbuzz_buffer, &info_length);
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(harfbuzz_buffer, &positions_length);
if (!info || !positions) return;
num_glyphs = MIN(info_length, positions_length);
#if 0 #if 0
// You can also generate this easily using hb-shape --show-flags --show-extents --cluster-level=1 --shapers=ot /path/to/font/file text // You can also generate this easily using hb-shape --show-flags --show-extents --cluster-level=1 --shapers=ot /path/to/font/file text
hb_buffer_serialize_glyphs(harfbuzz_buffer, 0, num_glyphs, (char*)canvas, CELLS_IN_CANVAS * cell_width * cell_height, NULL, font->hb_font, HB_BUFFER_SERIALIZE_FORMAT_TEXT, HB_BUFFER_SERIALIZE_FLAG_DEFAULT | HB_BUFFER_SERIALIZE_FLAG_GLYPH_EXTENTS | HB_BUFFER_SERIALIZE_FLAG_GLYPH_FLAGS); hb_buffer_serialize_glyphs(harfbuzz_buffer, 0, num_glyphs, (char*)canvas, CELLS_IN_CANVAS * cell_width * cell_height, NULL, font->hb_font, HB_BUFFER_SERIALIZE_FORMAT_TEXT, HB_BUFFER_SERIALIZE_FLAG_DEFAULT | HB_BUFFER_SERIALIZE_FLAG_GLYPH_EXTENTS | HB_BUFFER_SERIALIZE_FLAG_GLYPH_FLAGS);
@ -491,10 +540,11 @@ shape_run(Cell *first_cell, index_type num_cells, Font *font) {
clear_canvas(); clear_canvas();
#endif #endif
unsigned int run_pos = 0, cell_pos = 0, num_group_glyphs, num_group_cells; unsigned int run_pos = 0, cell_pos = 0, num_group_glyphs, num_group_cells;
uint64_t extra_glyphs; glyph_index first_glyph;
while(run_pos < num_glyphs && cell_pos < num_cells) { while(run_pos < num_glyphs && cell_pos < num_cells) {
next_group(&num_group_cells, &num_group_glyphs, first_cell + cell_pos, info + run_pos, positions + run_pos, num_glyphs - run_pos, num_cells - cell_pos); first_glyph = next_group(font->hb_font, &num_group_cells, &num_group_glyphs, first_cell + cell_pos, info + run_pos, num_glyphs - run_pos, num_cells - cell_pos, &extra_glyphs);
/* printf("Group: num_group_cells: %u num_group_glyphs: %u\n", num_group_cells, num_group_glyphs); */ /* printf("Group: num_group_cells: %u num_group_glyphs: %u\n", num_group_cells, num_group_glyphs); */
render_group(num_group_cells, num_group_glyphs, first_cell + cell_pos, info + run_pos, positions + run_pos, font); render_group(num_group_cells, num_group_glyphs, first_cell + cell_pos, info + run_pos, positions + run_pos, font, first_glyph, extra_glyphs);
run_pos += num_group_glyphs; cell_pos += num_group_cells; run_pos += num_group_glyphs; cell_pos += num_group_cells;
} }
} }
@ -506,7 +556,7 @@ test_shape(PyObject UNUSED *self, PyObject *args) {
int index = 0; int index = 0;
if(!PyArg_ParseTuple(args, "O!|zi", &Line_Type, &line, &path, &index)) return NULL; if(!PyArg_ParseTuple(args, "O!|zi", &Line_Type, &line, &path, &index)) return NULL;
index_type num = 0; index_type num = 0;
while(num < line->xnum && line->cells[num].ch) num++; while(num < line->xnum && line->cells[num].ch) num += line->cells[num].attrs & WIDTH_MASK;
load_hb_buffer(line->cells, num); load_hb_buffer(line->cells, num);
PyObject *face = NULL; PyObject *face = NULL;
hb_font_t *hb_font; hb_font_t *hb_font;
@ -516,12 +566,20 @@ test_shape(PyObject UNUSED *self, PyObject *args) {
if (face == NULL) return NULL; if (face == NULL) return NULL;
hb_font = harfbuzz_font_for_face(face); hb_font = harfbuzz_font_for_face(face);
} else hb_font = font->hb_font; } else hb_font = font->hb_font;
hb_shape_full(hb_font, harfbuzz_buffer, NULL, 0, SHAPERS); hb_shape(hb_font, harfbuzz_buffer, NULL, 0);
unsigned int num_glyphs; hb_glyph_info_t *info;
hb_buffer_get_glyph_infos(harfbuzz_buffer, &num_glyphs); hb_glyph_position_t *positions;
hb_buffer_serialize_glyphs(harfbuzz_buffer, 0, num_glyphs, (char*)canvas, CELLS_IN_CANVAS * cell_width * cell_height, NULL, hb_font, HB_BUFFER_SERIALIZE_FORMAT_JSON, HB_BUFFER_SERIALIZE_FLAG_DEFAULT | HB_BUFFER_SERIALIZE_FLAG_GLYPH_EXTENTS | HB_BUFFER_SERIALIZE_FLAG_GLYPH_FLAGS); unsigned int num_glyphs = shape(line->cells, num, hb_font, &info, &positions);
Py_CLEAR(face);
return Py_BuildValue("s", canvas); PyObject *ans = PyList_New(0);
unsigned int run_pos = 0, cell_pos = 0, num_group_glyphs, num_group_cells;
uint64_t extra_glyphs; glyph_index first_glyph;
while(run_pos < num_glyphs && cell_pos < num) {
first_glyph = next_group(hb_font, &num_group_cells, &num_group_glyphs, line->cells + cell_pos, info + run_pos, num_glyphs - run_pos, num - cell_pos, &extra_glyphs);
PyList_Append(ans, Py_BuildValue("IIIK", num_group_cells, num_group_glyphs, first_glyph, extra_glyphs));
run_pos += num_group_glyphs; cell_pos += num_group_cells;
}
return ans;
} }
static inline void static inline void
@ -767,19 +825,9 @@ init_fonts(PyObject *module) {
} }
harfbuzz_buffer = hb_buffer_create(); harfbuzz_buffer = hb_buffer_create();
if (harfbuzz_buffer == NULL || !hb_buffer_allocation_successful(harfbuzz_buffer) || !hb_buffer_pre_allocate(harfbuzz_buffer, 2048)) { PyErr_NoMemory(); return false; } if (harfbuzz_buffer == NULL || !hb_buffer_allocation_successful(harfbuzz_buffer) || !hb_buffer_pre_allocate(harfbuzz_buffer, 2048)) { PyErr_NoMemory(); return false; }
// A cluster level of HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS is needed for the unsafe to break API
hb_buffer_set_cluster_level(harfbuzz_buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); hb_buffer_set_cluster_level(harfbuzz_buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS);
if (PyModule_AddFunctions(module, module_methods) != 0) return false; if (PyModule_AddFunctions(module, module_methods) != 0) return false;
current_send_sprite_to_gpu = send_sprite_to_gpu; current_send_sprite_to_gpu = send_sprite_to_gpu;
sprite_tracker_set_limits(2000, 2000); sprite_tracker_set_limits(2000, 2000);
const char** shapers = hb_shape_list_shapers();
bool found = false;
for(int i = 0; shapers[i] != NULL && !found; i++) {
if (strcmp(shapers[i], "ot") == 0) found = true;
}
if (!found) {
PyErr_SetString(PyExc_RuntimeError, "The harfbuzz library on your system does not support the OpenType shaper");
return false;
}
return true; return true;
} }

View File

@ -188,17 +188,14 @@ def render_string(text, family='monospace', size=11.0, dpi=96.0):
def shape_string(text="abcd", family='monospace', size=11.0, dpi=96.0, path=None): def shape_string(text="abcd", family='monospace', size=11.0, dpi=96.0, path=None):
import json
try: try:
sprites, cell_width, cell_height = setup_for_testing(family, size, dpi) sprites, cell_width, cell_height = setup_for_testing(family, size, dpi)
s = Screen(None, 1, len(text)*2) s = Screen(None, 1, len(text)*2)
line = s.line(0) line = s.line(0)
s.draw(text) s.draw(text)
data = test_shape(line, path) return test_shape(line, path)
return json.loads('[' + data + ']')
finally: finally:
set_send_sprite_to_gpu(None) set_send_sprite_to_gpu(None)
return data
def test_render_string(text='Hello, world!', family='monospace', size=144.0, dpi=96.0): def test_render_string(text='Hello, world!', family='monospace', size=144.0, dpi=96.0):
@ -240,7 +237,7 @@ def test_fallback_font(qtext=None, bold=False, italic=False):
def showcase(): def showcase():
change_wcwidth(True) change_wcwidth(True)
f = 'monospace' if isosx else 'Liberation Mono' f = 'monospace' if isosx else 'Liberation Mono'
test_render_string('He\u0347\u0305llo\u0341, w\u0302or\u0306l\u0354d!', family=f) test_render_string('He\u0347\u0305llo\u0337, w\u0302or\u0306l\u0354d!', family=f)
test_render_string('你好,世界', family=f) test_render_string('你好,世界', family=f)
test_render_string('|\U0001F601|\U0001F64f|\U0001F63a|', family=f) test_render_string('|\U0001F601|\U0001F64f|\U0001F63a|', family=f)
test_render_string('A=>>B!=C', family='Fira Code') test_render_string('A=>>B!=C', family='Fira Code')

Binary file not shown.

View File

@ -70,9 +70,12 @@ class Rendering(BaseTest):
self.ae(len(cells), sz) self.ae(len(cells), sz)
def test_shaping(self): def test_shaping(self):
flags = [x.get('fl', 0) for x in shape_string('abcd')]
self.ae(flags, [0, 0, 0, 0]) def groups(text, path=None):
flags = [x.get('fl', 0) for x in shape_string('e\u0347\u0305')] return [x[:2] for x in shape_string(text, path=path)]
self.assertEqual(flags, [0, 1, 1])
flags = [x.get('fl', 0) for x in shape_string('===', path='kitty_tests/FiraCode-Medium.otf')] self.ae(groups('abcd'), [(1, 1) for i in range(4)])
self.assertEqual(flags, [0, 1, 1]) self.ae(groups('A=>>B!=C', path='kitty_tests/FiraCode-Medium.otf'), [(1, 1), (3, 3), (1, 1), (2, 2), (1, 1)])
self.ae(groups('|\U0001F601|\U0001F64f|\U0001F63a|'), [(1, 1), (2, 1), (1, 1), (2, 1), (1, 1), (2, 1), (1, 1)])
self.ae(groups('He\u0347\u0305llo\u0337,', path='kitty_tests/LiberationMono-Regular.ttf'),
[(1, 1), (1, 3), (1, 1), (1, 1), (1, 2), (1, 1)])