1import gi 2 3gi.require_version("Gtk", "3.0") 4from gi.repository import Gtk, HarfBuzz as hb 5 6 7POOL = {} 8 9 10def move_to_f(funcs, draw_data, st, to_x, to_y, user_data): 11 context = POOL[draw_data] 12 context.move_to(to_x, to_y) 13 14 15def line_to_f(funcs, draw_data, st, to_x, to_y, user_data): 16 context = POOL[draw_data] 17 context.line_to(to_x, to_y) 18 19 20def cubic_to_f( 21 funcs, 22 draw_data, 23 st, 24 control1_x, 25 control1_y, 26 control2_x, 27 control2_y, 28 to_x, 29 to_y, 30 user_data, 31): 32 context = POOL[draw_data] 33 context.curve_to(control1_x, control1_y, control2_x, control2_y, to_x, to_y) 34 35 36def close_path_f(funcs, draw_data, st, user_data): 37 context = POOL[draw_data] 38 context.close_path() 39 40 41DFUNCS = hb.draw_funcs_create() 42hb.draw_funcs_set_move_to_func(DFUNCS, move_to_f, None) 43hb.draw_funcs_set_line_to_func(DFUNCS, line_to_f, None) 44hb.draw_funcs_set_cubic_to_func(DFUNCS, cubic_to_f, None) 45hb.draw_funcs_set_close_path_func(DFUNCS, close_path_f, None) 46 47 48def push_transform_f(funcs, paint_data, xx, yx, xy, yy, dx, dy, user_data): 49 raise NotImplementedError 50 51 52def pop_transform_f(funcs, paint_data, user_data): 53 raise NotImplementedError 54 55 56def color_f(funcs, paint_data, is_foreground, color, user_data): 57 context = POOL[paint_data] 58 r = hb.color_get_red(color) / 255 59 g = hb.color_get_green(color) / 255 60 b = hb.color_get_blue(color) / 255 61 a = hb.color_get_alpha(color) / 255 62 context.set_source_rgba(r, g, b, a) 63 context.paint() 64 65 66def push_clip_rectangle_f(funcs, paint_data, xmin, ymin, xmax, ymax, user_data): 67 context = POOL[paint_data] 68 context.save() 69 context.rectangle(xmin, ymin, xmax, ymax) 70 context.clip() 71 72 73def push_clip_glyph_f(funcs, paint_data, glyph, font, user_data): 74 context = POOL[paint_data] 75 context.save() 76 context.new_path() 77 hb.font_draw_glyph(font, glyph, DFUNCS, paint_data) 78 context.close_path() 79 context.clip() 80 81 82def pop_clip_f(funcs, paint_data, user_data): 83 context = POOL[paint_data] 84 context.restore() 85 86 87def push_group_f(funcs, paint_data, user_data): 88 raise NotImplementedError 89 90 91def pop_group_f(funcs, paint_data, mode, user_data): 92 raise NotImplementedError 93 94 95PFUNCS = hb.paint_funcs_create() 96hb.paint_funcs_set_push_transform_func(PFUNCS, push_transform_f, None) 97hb.paint_funcs_set_pop_transform_func(PFUNCS, pop_transform_f, None) 98hb.paint_funcs_set_color_func(PFUNCS, color_f, None) 99hb.paint_funcs_set_push_clip_glyph_func(PFUNCS, push_clip_glyph_f, None) 100hb.paint_funcs_set_push_clip_rectangle_func(PFUNCS, push_clip_rectangle_f, None) 101hb.paint_funcs_set_pop_clip_func(PFUNCS, pop_clip_f, None) 102hb.paint_funcs_set_push_group_func(PFUNCS, push_group_f, None) 103hb.paint_funcs_set_pop_group_func(PFUNCS, pop_group_f, None) 104 105 106def makebuffer(words): 107 buf = hb.buffer_create() 108 109 text = " ".join(words) 110 hb.buffer_add_codepoints(buf, [ord(c) for c in text], 0, len(text)) 111 112 hb.buffer_guess_segment_properties(buf) 113 114 return buf 115 116 117def justify(face, words, advance, target_advance): 118 font = hb.font_create(face) 119 buf = makebuffer(words) 120 121 wiggle = 5 122 shrink = target_advance - wiggle < advance 123 expand = target_advance + wiggle > advance 124 125 ret, advance, tag, value = hb.shape_justify( 126 font, 127 buf, 128 None, 129 None, 130 target_advance, 131 target_advance, 132 advance, 133 ) 134 135 if not ret: 136 return False, buf, None 137 138 if tag: 139 variation = hb.variation_t() 140 variation.tag = tag 141 variation.value = value 142 else: 143 variation = None 144 145 if shrink and advance > target_advance + wiggle: 146 return False, buf, variation 147 if expand and advance < target_advance - wiggle: 148 return False, buf, variation 149 150 return True, buf, variation 151 152 153def shape(face, words): 154 font = hb.font_create(face) 155 buf = makebuffer(words) 156 hb.shape(font, buf) 157 positions = hb.buffer_get_glyph_positions(buf) 158 advance = sum(p.x_advance for p in positions) 159 return buf, advance 160 161 162def typeset(face, text, target_advance): 163 lines = [] 164 words = [] 165 for word in text.split(): 166 words.append(word) 167 buf, advance = shape(face, words) 168 if advance > target_advance: 169 # Shrink 170 ret, buf, variation = justify(face, words, advance, target_advance) 171 if ret: 172 lines.append((buf, variation)) 173 words = [] 174 # If if fails, pop the last word and shrink, and hope for the best. 175 # A too short line is better than too long. 176 elif len(words) > 1: 177 words.pop() 178 _, buf, variation = justify(face, words, advance, target_advance) 179 lines.append((buf, variation)) 180 words = [word] 181 # But if it is one word, meh. 182 else: 183 lines.append((buf, variation)) 184 words = [] 185 186 # Justify last line 187 if words: 188 _, buf, variation = justify(face, words, advance, target_advance) 189 lines.append((buf, variation)) 190 191 return lines 192 193 194def render(face, text, context, width, height, fontsize): 195 font = hb.font_create(face) 196 197 margin = fontsize * 2 198 scale = fontsize / hb.face_get_upem(face) 199 target_advance = (width - (margin * 2)) / scale 200 201 lines = typeset(face, text, target_advance) 202 203 _, extents = hb.font_get_h_extents(font) 204 lineheight = extents.ascender - extents.descender + extents.line_gap 205 lineheight *= scale 206 207 context.save() 208 context.translate(0, margin) 209 context.set_font_size(12) 210 context.set_source_rgb(1, 0, 0) 211 for buf, variation in lines: 212 rtl = hb.buffer_get_direction(buf) == hb.direction_t.RTL 213 if rtl: 214 hb.buffer_reverse(buf) 215 infos = hb.buffer_get_glyph_infos(buf) 216 positions = hb.buffer_get_glyph_positions(buf) 217 advance = sum(p.x_advance for p in positions) 218 219 context.translate(0, lineheight) 220 context.save() 221 222 context.save() 223 context.move_to(0, -20) 224 if variation: 225 tag = hb.tag_to_string(variation.tag).decode("ascii") 226 context.show_text(f" {tag}={variation.value:g}") 227 context.move_to(0, 0) 228 context.show_text(f" {advance:g}/{target_advance:g}") 229 context.restore() 230 231 if variation: 232 hb.font_set_variations(font, [variation]) 233 234 context.translate(margin, 0) 235 context.scale(scale, -scale) 236 237 if rtl: 238 context.translate(target_advance, 0) 239 240 for info, pos in zip(infos, positions): 241 if rtl: 242 context.translate(-pos.x_advance, pos.y_advance) 243 context.save() 244 context.translate(pos.x_offset, pos.y_offset) 245 hb.font_paint_glyph(font, info.codepoint, PFUNCS, id(context), 0, 0x0000FF) 246 context.restore() 247 if not rtl: 248 context.translate(+pos.x_advance, pos.y_advance) 249 250 context.restore() 251 context.restore() 252 253 254def main(fontpath, textpath): 255 fontsize = 70 256 257 blob = hb.blob_create_from_file(fontpath) 258 face = hb.face_create(blob, 0) 259 260 with open(textpath) as f: 261 text = f.read() 262 263 def on_draw(da, context): 264 alloc = da.get_allocation() 265 POOL[id(context)] = context 266 render(face, text, context, alloc.width, alloc.height, fontsize) 267 del POOL[id(context)] 268 269 drawingarea = Gtk.DrawingArea() 270 drawingarea.connect("draw", on_draw) 271 272 win = Gtk.Window() 273 win.connect("destroy", Gtk.main_quit) 274 win.set_default_size(1000, 700) 275 win.add(drawingarea) 276 277 win.show_all() 278 Gtk.main() 279 280 281if __name__ == "__main__": 282 import argparse 283 284 parser = argparse.ArgumentParser(description="HarfBuzz justification demo.") 285 parser.add_argument("fontfile", help="font file") 286 parser.add_argument("textfile", help="text") 287 args = parser.parse_args() 288 main(args.fontfile, args.textfile) 289