xref: /aosp_15_r20/external/harfbuzz_ng/src/justify.py (revision 2d1272b857b1f7575e6e246373e1cb218663db8a)
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