[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
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
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)
|
||||
|
||||
|
||||
class Word:
|
||||
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):
|
||||
def makebuffer(words):
|
||||
buf = hb.buffer_create()
|
||||
|
||||
# Strip and remove double spaces.
|
||||
text = " ".join(text.split())
|
||||
|
||||
text = " ".join(words)
|
||||
hb.buffer_add_codepoints(buf, [ord(c) for c in text], 0, len(text))
|
||||
|
||||
hb.buffer_guess_segment_properties(buf)
|
||||
|
||||
return buf, text
|
||||
return buf
|
||||
|
||||
|
||||
def makewords(buf, font, text):
|
||||
if hb.buffer_get_direction(buf) == hb.direction_t.RTL:
|
||||
hb.buffer_reverse(buf)
|
||||
words = [Word(font, text)]
|
||||
infos = hb.buffer_get_glyph_infos(buf)
|
||||
def justify(font, words, advance, target_advance):
|
||||
buf = makebuffer(words)
|
||||
|
||||
wiggle = 5
|
||||
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)
|
||||
for info, pos in zip(infos, positions):
|
||||
words[-1].append(info, pos)
|
||||
if text[info.cluster] == " ":
|
||||
words.append(Word(font, text))
|
||||
return words
|
||||
advance = sum(p.x_advance for p in positions)
|
||||
return buf, advance
|
||||
|
||||
|
||||
def typeset(conf):
|
||||
blob = hb.blob_create_from_file(conf.fontpath)
|
||||
face = hb.face_create(blob, 0)
|
||||
def typeset(font, text, target_advance):
|
||||
lines = []
|
||||
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)
|
||||
|
||||
with open(conf.textpath) as f:
|
||||
text = f.read()
|
||||
margin = fontsize * 2
|
||||
scale = fontsize / hb.face_get_upem(face)
|
||||
target_advance = (width - (margin * 2)) / scale
|
||||
|
||||
margin = conf.fontsize * 2
|
||||
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))
|
||||
lines = typeset(font, text, target_advance)
|
||||
|
||||
_, extents = hb.font_get_h_extents(font)
|
||||
lineheight = extents.ascender - extents.descender + extents.line_gap
|
||||
|
@ -326,29 +204,64 @@ def render(context, conf):
|
|||
|
||||
context.save()
|
||||
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.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.scale(scale, scale)
|
||||
line.draw(context, direction)
|
||||
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)
|
||||
|
||||
context.restore()
|
||||
context.restore()
|
||||
|
||||
|
||||
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):
|
||||
alloc = da.get_allocation()
|
||||
conf = Configuration(
|
||||
width=alloc.width,
|
||||
height=alloc.height,
|
||||
fontsize=70,
|
||||
fontpath=fontpath,
|
||||
textpath=textpath,
|
||||
)
|
||||
POOL[id(context)] = context
|
||||
render(context, conf)
|
||||
render(face, text, context, alloc.width, alloc.height, fontsize)
|
||||
del POOL[id(context)]
|
||||
|
||||
drawingarea = Gtk.DrawingArea()
|
||||
|
|
Loading…
Reference in New Issue