tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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)