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