[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.
diff --git a/src/justify.py b/src/justify.py
index deb11d1..f36da86 100644
--- a/src/justify.py
+++ b/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_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 @@
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()