diff options
Diffstat (limited to 'gfx/harfbuzz/src/justify.py')
-rw-r--r-- | gfx/harfbuzz/src/justify.py | 288 |
1 files changed, 288 insertions, 0 deletions
diff --git a/gfx/harfbuzz/src/justify.py b/gfx/harfbuzz/src/justify.py new file mode 100644 index 0000000000..db2fb70104 --- /dev/null +++ b/gfx/harfbuzz/src/justify.py @@ -0,0 +1,288 @@ +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, HarfBuzz as hb + + +POOL = {} + + +def move_to_f(funcs, draw_data, st, to_x, to_y, user_data): + context = POOL[draw_data] + context.move_to(to_x, to_y) + + +def line_to_f(funcs, draw_data, st, to_x, to_y, user_data): + context = POOL[draw_data] + context.line_to(to_x, to_y) + + +def cubic_to_f( + funcs, + draw_data, + st, + control1_x, + control1_y, + control2_x, + control2_y, + to_x, + to_y, + user_data, +): + context = POOL[draw_data] + context.curve_to(control1_x, control1_y, control2_x, control2_y, to_x, to_y) + + +def close_path_f(funcs, draw_data, st, user_data): + context = POOL[draw_data] + context.close_path() + + +DFUNCS = hb.draw_funcs_create() +hb.draw_funcs_set_move_to_func(DFUNCS, move_to_f, None) +hb.draw_funcs_set_line_to_func(DFUNCS, line_to_f, None) +hb.draw_funcs_set_cubic_to_func(DFUNCS, cubic_to_f, None) +hb.draw_funcs_set_close_path_func(DFUNCS, close_path_f, None) + + +def push_transform_f(funcs, paint_data, xx, yx, xy, yy, dx, dy, user_data): + raise NotImplementedError + + +def pop_transform_f(funcs, paint_data, user_data): + raise NotImplementedError + + +def color_f(funcs, paint_data, is_foreground, color, user_data): + context = POOL[paint_data] + r = hb.color_get_red(color) / 255 + g = hb.color_get_green(color) / 255 + b = hb.color_get_blue(color) / 255 + a = hb.color_get_alpha(color) / 255 + context.set_source_rgba(r, g, b, a) + context.paint() + + +def push_clip_rectangle_f(funcs, paint_data, xmin, ymin, xmax, ymax, user_data): + context = POOL[paint_data] + context.save() + context.rectangle(xmin, ymin, xmax, ymax) + context.clip() + + +def push_clip_glyph_f(funcs, paint_data, glyph, font, user_data): + context = POOL[paint_data] + context.save() + context.new_path() + hb.font_draw_glyph(font, glyph, DFUNCS, paint_data) + context.close_path() + context.clip() + + +def pop_clip_f(funcs, paint_data, user_data): + context = POOL[paint_data] + context.restore() + + +def push_group_f(funcs, paint_data, user_data): + raise NotImplementedError + + +def pop_group_f(funcs, paint_data, mode, user_data): + raise NotImplementedError + + +PFUNCS = hb.paint_funcs_create() +hb.paint_funcs_set_push_transform_func(PFUNCS, push_transform_f, None) +hb.paint_funcs_set_pop_transform_func(PFUNCS, pop_transform_f, None) +hb.paint_funcs_set_color_func(PFUNCS, color_f, None) +hb.paint_funcs_set_push_clip_glyph_func(PFUNCS, push_clip_glyph_f, None) +hb.paint_funcs_set_push_clip_rectangle_func(PFUNCS, push_clip_rectangle_f, None) +hb.paint_funcs_set_pop_clip_func(PFUNCS, pop_clip_f, None) +hb.paint_funcs_set_push_group_func(PFUNCS, push_group_f, None) +hb.paint_funcs_set_pop_group_func(PFUNCS, pop_group_f, None) + + +def makebuffer(words): + buf = hb.buffer_create() + + text = " ".join(words) + hb.buffer_add_codepoints(buf, [ord(c) for c in text], 0, len(text)) + + hb.buffer_guess_segment_properties(buf) + + return buf + + +def justify(face, words, advance, target_advance): + font = hb.font_create(face) + buf = makebuffer(words) + + wiggle = 5 + shrink = target_advance - wiggle < advance + expand = target_advance + wiggle > advance + + ret, advance, tag, value = hb.shape_justify( + font, + buf, + None, + None, + target_advance, + target_advance, + advance, + ) + + if not ret: + return False, buf, None + + if tag: + variation = hb.variation_t() + variation.tag = tag + variation.value = value + else: + variation = None + + if shrink and advance > target_advance + wiggle: + return False, buf, variation + if expand and advance < target_advance - wiggle: + return False, buf, variation + + return True, buf, variation + + +def shape(face, words): + font = hb.font_create(face) + buf = makebuffer(words) + hb.shape(font, buf) + positions = hb.buffer_get_glyph_positions(buf) + advance = sum(p.x_advance for p in positions) + return buf, advance + + +def typeset(face, text, target_advance): + lines = [] + words = [] + for word in text.split(): + words.append(word) + buf, advance = shape(face, words) + if advance > target_advance: + # Shrink + ret, buf, variation = justify(face, words, advance, target_advance) + if ret: + lines.append((buf, variation)) + words = [] + # If if fails, pop the last word and shrink, and hope for the best. + # A too short line is better than too long. + elif len(words) > 1: + words.pop() + _, buf, variation = justify(face, words, advance, target_advance) + lines.append((buf, variation)) + words = [word] + # But if it is one word, meh. + else: + lines.append((buf, variation)) + words = [] + + # Justify last line + if words: + _, buf, variation = justify(face, words, advance, target_advance) + lines.append((buf, variation)) + + return lines + + +def render(face, text, context, width, height, fontsize): + font = hb.font_create(face) + + margin = fontsize * 2 + scale = fontsize / hb.face_get_upem(face) + target_advance = (width - (margin * 2)) / scale + + lines = typeset(face, text, target_advance) + + _, extents = hb.font_get_h_extents(font) + lineheight = extents.ascender - extents.descender + extents.line_gap + lineheight *= scale + + context.save() + context.translate(0, margin) + context.set_font_size(12) + context.set_source_rgb(1, 0, 0) + for buf, variation in lines: + rtl = hb.buffer_get_direction(buf) == hb.direction_t.RTL + if rtl: + hb.buffer_reverse(buf) + infos = hb.buffer_get_glyph_infos(buf) + positions = hb.buffer_get_glyph_positions(buf) + advance = sum(p.x_advance for p in positions) + + context.translate(0, lineheight) + context.save() + + context.save() + context.move_to(0, -20) + if variation: + tag = hb.tag_to_string(variation.tag).decode("ascii") + context.show_text(f" {tag}={variation.value:g}") + context.move_to(0, 0) + context.show_text(f" {advance:g}/{target_advance:g}") + context.restore() + + if variation: + hb.font_set_variations(font, [variation]) + + context.translate(margin, 0) + context.scale(scale, -scale) + + if rtl: + context.translate(target_advance, 0) + + for info, pos in zip(infos, positions): + if rtl: + context.translate(-pos.x_advance, pos.y_advance) + context.save() + context.translate(pos.x_offset, pos.y_offset) + hb.font_paint_glyph(font, info.codepoint, PFUNCS, id(context), 0, 0x0000FF) + context.restore() + if not rtl: + context.translate(+pos.x_advance, pos.y_advance) + + context.restore() + context.restore() + + +def main(fontpath, textpath): + fontsize = 70 + + blob = hb.blob_create_from_file(fontpath) + face = hb.face_create(blob, 0) + + with open(textpath) as f: + text = f.read() + + def on_draw(da, context): + alloc = da.get_allocation() + POOL[id(context)] = context + render(face, text, context, alloc.width, alloc.height, fontsize) + del POOL[id(context)] + + drawingarea = Gtk.DrawingArea() + drawingarea.connect("draw", on_draw) + + win = Gtk.Window() + win.connect("destroy", Gtk.main_quit) + win.set_default_size(1000, 700) + win.add(drawingarea) + + win.show_all() + Gtk.main() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="HarfBuzz justification demo.") + parser.add_argument("fontfile", help="font file") + parser.add_argument("textfile", help="text") + args = parser.parse_args() + main(args.fontfile, args.textfile) |