| 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) |