2023-03-04 00:41:34 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
def makebuffer(words):
|
2023-03-04 00:41:34 +01:00
|
|
|
buf = hb.buffer_create()
|
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
text = " ".join(words)
|
2023-03-04 00:41:34 +01:00
|
|
|
hb.buffer_add_codepoints(buf, [ord(c) for c in text], 0, len(text))
|
|
|
|
|
2023-03-04 01:01:12 +01:00
|
|
|
hb.buffer_guess_segment_properties(buf)
|
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
return buf
|
2023-03-04 00:41:34 +01:00
|
|
|
|
|
|
|
|
2023-03-05 05:09:26 +01:00
|
|
|
def justify(face, words, advance, target_advance):
|
|
|
|
font = hb.font_create(face)
|
2023-03-04 06:19:20 +01:00
|
|
|
buf = makebuffer(words)
|
2023-03-04 00:41:34 +01:00
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
wiggle = 5
|
|
|
|
shrink = target_advance - wiggle < advance
|
|
|
|
expand = target_advance + wiggle > advance
|
2023-03-04 00:41:34 +01:00
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
ret, advance, tag, value = hb.shape_justify(
|
|
|
|
font,
|
|
|
|
buf,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
target_advance,
|
|
|
|
target_advance,
|
|
|
|
advance,
|
|
|
|
)
|
2023-03-04 00:41:34 +01:00
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
if not ret:
|
|
|
|
return False, buf, None
|
2023-03-04 00:41:34 +01:00
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
if tag:
|
|
|
|
variation = hb.variation_t()
|
|
|
|
variation.tag = tag
|
|
|
|
variation.value = value
|
|
|
|
else:
|
|
|
|
variation = None
|
2023-03-04 00:41:34 +01:00
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
if shrink and advance > target_advance + wiggle:
|
|
|
|
return False, buf, variation
|
|
|
|
if expand and advance < target_advance - wiggle:
|
|
|
|
return False, buf, variation
|
2023-03-04 00:41:34 +01:00
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
return True, buf, variation
|
2023-03-04 00:41:34 +01:00
|
|
|
|
|
|
|
|
2023-03-05 05:09:26 +01:00
|
|
|
def shape(face, words):
|
|
|
|
font = hb.font_create(face)
|
2023-03-04 06:19:20 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2023-03-05 05:09:26 +01:00
|
|
|
def typeset(face, text, target_advance):
|
2023-03-04 06:19:20 +01:00
|
|
|
lines = []
|
|
|
|
words = []
|
|
|
|
for word in text.split():
|
|
|
|
words.append(word)
|
2023-03-05 05:09:26 +01:00
|
|
|
buf, advance = shape(face, words)
|
2023-03-04 06:19:20 +01:00
|
|
|
if advance > target_advance:
|
|
|
|
# Shrink
|
2023-03-05 05:09:26 +01:00
|
|
|
ret, buf, variation = justify(face, words, advance, target_advance)
|
2023-03-04 06:19:20 +01:00
|
|
|
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()
|
2023-03-05 05:09:26 +01:00
|
|
|
_, buf, variation = justify(face, words, advance, target_advance)
|
2023-03-04 06:19:20 +01:00
|
|
|
lines.append((buf, variation))
|
|
|
|
words = [word]
|
|
|
|
# But if it is one word, meh.
|
2023-03-04 00:41:34 +01:00
|
|
|
else:
|
2023-03-04 06:19:20 +01:00
|
|
|
lines.append((buf, variation))
|
|
|
|
words = []
|
|
|
|
|
|
|
|
# Justify last line
|
|
|
|
if words:
|
2023-03-05 05:09:26 +01:00
|
|
|
_, buf, variation = justify(face, words, advance, target_advance)
|
2023-03-04 06:19:20 +01:00
|
|
|
lines.append((buf, variation))
|
2023-03-04 00:41:34 +01:00
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
return lines
|
2023-03-04 00:41:34 +01:00
|
|
|
|
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
def render(face, text, context, width, height, fontsize):
|
|
|
|
font = hb.font_create(face)
|
2023-03-04 00:41:34 +01:00
|
|
|
|
2023-03-04 06:19:20 +01:00
|
|
|
margin = fontsize * 2
|
|
|
|
scale = fontsize / hb.face_get_upem(face)
|
|
|
|
target_advance = (width - (margin * 2)) / scale
|
2023-03-04 00:41:34 +01:00
|
|
|
|
2023-03-05 05:09:26 +01:00
|
|
|
lines = typeset(face, text, target_advance)
|
2023-03-04 00:41:34 +01:00
|
|
|
|
|
|
|
_, extents = hb.font_get_h_extents(font)
|
|
|
|
lineheight = extents.ascender - extents.descender + extents.line_gap
|
|
|
|
lineheight *= scale
|
|
|
|
|
|
|
|
context.save()
|
|
|
|
context.translate(0, margin)
|
2023-03-04 06:19:20 +01:00
|
|
|
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)
|
|
|
|
|
2023-03-04 00:41:34 +01:00
|
|
|
context.translate(0, lineheight)
|
2023-03-04 06:19:20 +01:00
|
|
|
context.save()
|
2023-03-04 00:41:34 +01:00
|
|
|
|
|
|
|
context.save()
|
2023-03-04 06:19:20 +01:00
|
|
|
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])
|
|
|
|
|
2023-03-04 00:41:34 +01:00
|
|
|
context.translate(margin, 0)
|
2023-03-04 06:19:20 +01:00
|
|
|
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)
|
|
|
|
|
2023-03-04 00:41:34 +01:00
|
|
|
context.restore()
|
|
|
|
context.restore()
|
|
|
|
|
|
|
|
|
|
|
|
def main(fontpath, textpath):
|
2023-03-04 06:19:20 +01:00
|
|
|
fontsize = 70
|
|
|
|
|
|
|
|
blob = hb.blob_create_from_file(fontpath)
|
|
|
|
face = hb.face_create(blob, 0)
|
|
|
|
|
|
|
|
with open(textpath) as f:
|
|
|
|
text = f.read()
|
|
|
|
|
2023-03-04 00:41:34 +01:00
|
|
|
def on_draw(da, context):
|
|
|
|
alloc = da.get_allocation()
|
|
|
|
POOL[id(context)] = context
|
2023-03-04 06:19:20 +01:00
|
|
|
render(face, text, context, alloc.width, alloc.height, fontsize)
|
2023-03-04 00:41:34 +01:00
|
|
|
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.")
|
2023-03-10 20:40:43 +01:00
|
|
|
parser.add_argument("fontfile", help="font file")
|
|
|
|
parser.add_argument("textfile", help="text")
|
2023-03-04 00:41:34 +01:00
|
|
|
args = parser.parse_args()
|
|
|
|
main(args.fontfile, args.textfile)
|