[justify-demo] Rewrite in a simpler way
No need to overthink it, append text words to the line and reshape, no need to shape the whole text first and do complicated glyph/input mapping. Much simpler code and as fast.
This commit is contained in:
parent
e9d6f23b5d
commit
690145fa00
339
src/justify.py
339
src/justify.py
|
@ -1,7 +1,5 @@
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, HarfBuzz as hb
|
from gi.repository import Gtk, HarfBuzz as hb
|
||||||
|
|
||||||
|
@ -105,220 +103,100 @@ hb.paint_funcs_set_push_group_func(PFUNCS, push_group_f, None)
|
||||||
hb.paint_funcs_set_pop_group_func(PFUNCS, pop_group_f, None)
|
hb.paint_funcs_set_pop_group_func(PFUNCS, pop_group_f, None)
|
||||||
|
|
||||||
|
|
||||||
class Word:
|
def makebuffer(words):
|
||||||
def __init__(self, font, text):
|
|
||||||
self._text = text
|
|
||||||
self._font = font
|
|
||||||
self._glyphs = []
|
|
||||||
self._positions = []
|
|
||||||
|
|
||||||
def append(self, info, pos):
|
|
||||||
self._glyphs.append(info)
|
|
||||||
self._positions.append(pos)
|
|
||||||
|
|
||||||
def draw(self, context, font, direction):
|
|
||||||
for info, pos in zip(self._glyphs, self._positions):
|
|
||||||
if direction == hb.direction_t.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 direction != hb.direction_t.RTL:
|
|
||||||
context.translate(+pos.x_advance, pos.y_advance)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def advance(self):
|
|
||||||
return sum(pos.x_advance for pos in self._positions)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def strippedadvance(self):
|
|
||||||
w = self.advance
|
|
||||||
if len(self) and self._text[self._glyphs[-1].cluster] == " ":
|
|
||||||
w -= self._positions[-1].x_advance
|
|
||||||
return w
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if not self._glyphs:
|
|
||||||
return ""
|
|
||||||
first = min(g.cluster for g in self._glyphs)
|
|
||||||
last = max(g.cluster for g in self._glyphs)
|
|
||||||
return self._text[first : last + 1]
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._glyphs)
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return len(self) != 0
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Word advance={self.advance} text='{str(self)}'>"
|
|
||||||
|
|
||||||
|
|
||||||
class Line:
|
|
||||||
def __init__(self, font, target_advance):
|
|
||||||
self._font = font
|
|
||||||
self._target_advance = target_advance
|
|
||||||
self._words = []
|
|
||||||
self._variation = None
|
|
||||||
|
|
||||||
def append(self, word):
|
|
||||||
self._words.append(word)
|
|
||||||
|
|
||||||
def pop(self):
|
|
||||||
return self._words.pop()
|
|
||||||
|
|
||||||
def justify(self):
|
|
||||||
buf, text = makebuffer(str(self))
|
|
||||||
|
|
||||||
wiggle = 5
|
|
||||||
advance = self.advance
|
|
||||||
shrink = self._target_advance - wiggle < advance
|
|
||||||
expand = self._target_advance + wiggle > advance
|
|
||||||
|
|
||||||
ret, advance, tag, value = hb.shape_justify(
|
|
||||||
self._font,
|
|
||||||
buf,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
self._target_advance,
|
|
||||||
self._target_advance,
|
|
||||||
advance,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not ret:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if tag:
|
|
||||||
self._variation = hb.variation_t()
|
|
||||||
self._variation.tag = tag
|
|
||||||
self._variation.value = value
|
|
||||||
self._words = makewords(buf, self._font, text)
|
|
||||||
|
|
||||||
if shrink and advance > self._target_advance + wiggle:
|
|
||||||
return False
|
|
||||||
if expand and advance < self._target_advance - wiggle:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def draw(self, context, direction):
|
|
||||||
context.save()
|
|
||||||
context.move_to(-1600, -200)
|
|
||||||
context.set_font_size(130)
|
|
||||||
context.set_source_rgb(1, 0, 0)
|
|
||||||
if self._variation:
|
|
||||||
tag = hb.tag_to_string(self._variation.tag).decode("ascii")
|
|
||||||
context.show_text(f" {tag}={self._variation.value:g}")
|
|
||||||
context.move_to(-1600, 0)
|
|
||||||
context.show_text(f" {self.advance:g}/{self._target_advance:g}")
|
|
||||||
context.restore()
|
|
||||||
|
|
||||||
if self._variation:
|
|
||||||
hb.font_set_variations(self._font, [self._variation])
|
|
||||||
|
|
||||||
context.scale(1, -1)
|
|
||||||
if direction == hb.direction_t.RTL:
|
|
||||||
context.translate(self._target_advance, 0)
|
|
||||||
for word in self._words:
|
|
||||||
word.draw(context, self._font, direction)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def advance(self):
|
|
||||||
w = sum(word.advance for word in self._words[:-1])
|
|
||||||
if len(self):
|
|
||||||
w += self._words[-1].strippedadvance
|
|
||||||
return w
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "".join(str(w) for w in self._words)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._words)
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return len(self) != 0
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Line advance={self.advance} text='{str(self)}'>"
|
|
||||||
|
|
||||||
|
|
||||||
Configuration = namedtuple(
|
|
||||||
"Configuration", ["width", "height", "fontsize", "fontpath", "textpath"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def makebuffer(text):
|
|
||||||
buf = hb.buffer_create()
|
buf = hb.buffer_create()
|
||||||
|
|
||||||
# Strip and remove double spaces.
|
text = " ".join(words)
|
||||||
text = " ".join(text.split())
|
|
||||||
|
|
||||||
hb.buffer_add_codepoints(buf, [ord(c) for c in text], 0, len(text))
|
hb.buffer_add_codepoints(buf, [ord(c) for c in text], 0, len(text))
|
||||||
|
|
||||||
hb.buffer_guess_segment_properties(buf)
|
hb.buffer_guess_segment_properties(buf)
|
||||||
|
|
||||||
return buf, text
|
return buf
|
||||||
|
|
||||||
|
|
||||||
def makewords(buf, font, text):
|
def justify(font, words, advance, target_advance):
|
||||||
if hb.buffer_get_direction(buf) == hb.direction_t.RTL:
|
buf = makebuffer(words)
|
||||||
hb.buffer_reverse(buf)
|
|
||||||
words = [Word(font, text)]
|
wiggle = 5
|
||||||
infos = hb.buffer_get_glyph_infos(buf)
|
shrink = target_advance - wiggle < advance
|
||||||
|
expand = target_advance + wiggle > advance
|
||||||
|
|
||||||
|
ret, advance, tag, value = hb.shape_justify(
|
||||||
|
font,
|
||||||
|
buf,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
target_advance,
|
||||||
|
target_advance,
|
||||||
|
advance,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not ret:
|
||||||
|
return False, buf, None
|
||||||
|
|
||||||
|
if tag:
|
||||||
|
variation = hb.variation_t()
|
||||||
|
variation.tag = tag
|
||||||
|
variation.value = value
|
||||||
|
else:
|
||||||
|
variation = None
|
||||||
|
|
||||||
|
if shrink and advance > target_advance + wiggle:
|
||||||
|
return False, buf, variation
|
||||||
|
if expand and advance < target_advance - wiggle:
|
||||||
|
return False, buf, variation
|
||||||
|
|
||||||
|
return True, buf, variation
|
||||||
|
|
||||||
|
|
||||||
|
def shape(font, words):
|
||||||
|
buf = makebuffer(words)
|
||||||
|
hb.shape(font, buf)
|
||||||
positions = hb.buffer_get_glyph_positions(buf)
|
positions = hb.buffer_get_glyph_positions(buf)
|
||||||
for info, pos in zip(infos, positions):
|
advance = sum(p.x_advance for p in positions)
|
||||||
words[-1].append(info, pos)
|
return buf, advance
|
||||||
if text[info.cluster] == " ":
|
|
||||||
words.append(Word(font, text))
|
|
||||||
return words
|
|
||||||
|
|
||||||
|
|
||||||
def typeset(conf):
|
def typeset(font, text, target_advance):
|
||||||
blob = hb.blob_create_from_file(conf.fontpath)
|
lines = []
|
||||||
face = hb.face_create(blob, 0)
|
words = []
|
||||||
|
for word in text.split():
|
||||||
|
words.append(word)
|
||||||
|
buf, advance = shape(font, words)
|
||||||
|
if advance > target_advance:
|
||||||
|
# Shrink
|
||||||
|
ret, buf, variation = justify(font, words, advance, target_advance)
|
||||||
|
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()
|
||||||
|
_, buf, variation = justify(font, words, advance, target_advance)
|
||||||
|
lines.append((buf, variation))
|
||||||
|
words = [word]
|
||||||
|
# But if it is one word, meh.
|
||||||
|
else:
|
||||||
|
lines.append((buf, variation))
|
||||||
|
words = []
|
||||||
|
|
||||||
|
# Justify last line
|
||||||
|
if words:
|
||||||
|
_, buf, variation = justify(font, words, advance, target_advance)
|
||||||
|
lines.append((buf, variation))
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def render(face, text, context, width, height, fontsize):
|
||||||
font = hb.font_create(face)
|
font = hb.font_create(face)
|
||||||
|
|
||||||
with open(conf.textpath) as f:
|
margin = fontsize * 2
|
||||||
text = f.read()
|
scale = fontsize / hb.face_get_upem(face)
|
||||||
|
target_advance = (width - (margin * 2)) / scale
|
||||||
|
|
||||||
margin = conf.fontsize * 2
|
lines = typeset(font, text, target_advance)
|
||||||
scale = conf.fontsize / hb.face_get_upem(face)
|
|
||||||
target_advance = (conf.width - (margin * 2)) / scale
|
|
||||||
|
|
||||||
buf, text = makebuffer(text)
|
|
||||||
direction = hb.buffer_get_direction(buf)
|
|
||||||
|
|
||||||
hb.shape(font, buf)
|
|
||||||
|
|
||||||
words = makewords(buf, font, text)
|
|
||||||
|
|
||||||
lines = [Line(font, target_advance)]
|
|
||||||
|
|
||||||
for word in words:
|
|
||||||
lines[-1].append(word)
|
|
||||||
if lines[-1].advance > target_advance:
|
|
||||||
if lines[-1].justify():
|
|
||||||
# Shrink
|
|
||||||
lines.append(Line(font, target_advance))
|
|
||||||
else:
|
|
||||||
# Remove last word and expand
|
|
||||||
lines[-1].pop()
|
|
||||||
lines[-1].justify()
|
|
||||||
lines.append(Line(font, target_advance))
|
|
||||||
lines[-1].append(word)
|
|
||||||
|
|
||||||
if lines[-1].advance != target_advance:
|
|
||||||
lines[-1].justify()
|
|
||||||
|
|
||||||
return lines, font, direction
|
|
||||||
|
|
||||||
|
|
||||||
def render(context, conf):
|
|
||||||
lines, font, direction = typeset(conf)
|
|
||||||
|
|
||||||
margin = conf.fontsize * 2
|
|
||||||
scale = conf.fontsize / hb.face_get_upem(hb.font_get_face(font))
|
|
||||||
|
|
||||||
_, extents = hb.font_get_h_extents(font)
|
_, extents = hb.font_get_h_extents(font)
|
||||||
lineheight = extents.ascender - extents.descender + extents.line_gap
|
lineheight = extents.ascender - extents.descender + extents.line_gap
|
||||||
|
@ -326,29 +204,64 @@ def render(context, conf):
|
||||||
|
|
||||||
context.save()
|
context.save()
|
||||||
context.translate(0, margin)
|
context.translate(0, margin)
|
||||||
for line in lines:
|
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)
|
||||||
|
|
||||||
context.translate(0, lineheight)
|
context.translate(0, lineheight)
|
||||||
|
context.save()
|
||||||
|
|
||||||
context.save()
|
context.save()
|
||||||
|
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])
|
||||||
|
|
||||||
context.translate(margin, 0)
|
context.translate(margin, 0)
|
||||||
context.scale(scale, scale)
|
context.scale(scale, -scale)
|
||||||
line.draw(context, direction)
|
|
||||||
|
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)
|
||||||
|
|
||||||
context.restore()
|
context.restore()
|
||||||
context.restore()
|
context.restore()
|
||||||
|
|
||||||
|
|
||||||
def main(fontpath, textpath):
|
def main(fontpath, textpath):
|
||||||
|
fontsize = 70
|
||||||
|
|
||||||
|
blob = hb.blob_create_from_file(fontpath)
|
||||||
|
face = hb.face_create(blob, 0)
|
||||||
|
|
||||||
|
with open(textpath) as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
def on_draw(da, context):
|
def on_draw(da, context):
|
||||||
alloc = da.get_allocation()
|
alloc = da.get_allocation()
|
||||||
conf = Configuration(
|
|
||||||
width=alloc.width,
|
|
||||||
height=alloc.height,
|
|
||||||
fontsize=70,
|
|
||||||
fontpath=fontpath,
|
|
||||||
textpath=textpath,
|
|
||||||
)
|
|
||||||
POOL[id(context)] = context
|
POOL[id(context)] = context
|
||||||
render(context, conf)
|
render(face, text, context, alloc.width, alloc.height, fontsize)
|
||||||
del POOL[id(context)]
|
del POOL[id(context)]
|
||||||
|
|
||||||
drawingarea = Gtk.DrawingArea()
|
drawingarea = Gtk.DrawingArea()
|
||||||
|
|
Loading…
Reference in New Issue