Canvas microjustification

WikiDbImage mjtext.jpg


Richard Suchenwirth 2002-07-28: Microjustification is a typesetting technique where small gaps are inserted between words to make lines fully justified (flush left and right). Donald E. Knuth has done extensive research on the topic (which I didn't have handy over weekend ;-), so here's only my Tk experiments.

The text widget does not support full justification, so I use a canvas instead - it can display text at pixel-precise positions, and also produce Postscript. I could not use its text editing features, because then "a space is a space" of font-dependent constant width.

In this design, a sort of "mega-item" mjtext is created on the canvas, which first just is a rectangle. The returned handle however accepts text input with $mj insert end.. like a text widget. The words are rendered as separate text items and wrap around, except at newlines. For the resulting "raw lines", the number of missing pixels to flush-right is determined, and the rightmost word is shifted right by that amount, then the others by decreasing amounts to make them approximately evenly distributed.

That's all - still a far cry from real DTP (rather like 1970's IBM Composer); but it let me cross another horizon of what's all possible with Tcl/Tk - hyphenation might be the next target..

set docu(mjtext) {
    insert a good-size chunk of text, here
proc mjtext {c x0 y0 x1 y1 args} {
    array set opt {-bg white  -font {Times 11}}
    array set opt $args
    set _self mj[$c create rect $x0 $y0 $x1 $y1 \
        -fill $opt(-bg) -outline $opt(-bg)]
    upvar #0 $_self self
    array set self [list x $x0 x0 $x0 y $y0 x1 $x1 y1 $y1 c $c]
    set self(-font) $opt(-font)
    set self(dy) [font metrics $opt(-font) -linespace]
    interp alias {} $_self {} mjtext'do $_self

proc mjtext'do {_self cmd cmd2 args} {
    upvar #0 $_self self
    if {$cmd=="insert" && $cmd2=="end"} {
        foreach {text tag} $args {
            foreach line [split $text \n] {
                set ids {}
                foreach word [split $line] {
                    if {$word==""} continue
                    set id [$self(c) create text $self(x) $self(y) \
                      -anchor nw -text $word -font $self(-font)]
                    foreach {x0 y0 x1 y1} [$self(c) bbox $id] break
                    if {$x1 > $self(x1)} {
                        set dx [expr {$self(x0) - $x0}]
                        $self(c) move $id $dx $self(dy)
                        foreach {x0 y0 x1 y1} [$self(c) bbox $id] break
                        mjtext'justify $self(c) $ids $self(x1)
                        set ids {}
                    } else {lappend ids $id}
                    set self(x) [expr {$x1 + 1}]
                    set self(y) $y0
                set self(x) $self(x0)
                set self(y) [expr {$self(y) + 2 * $self(dy)}]
    } else {error "usage: $self insert end text"}

proc mjtext'justify {c ids x1} {
    set last [lindex $ids end]
    set diff [expr {$x1 - [lindex [$c bbox $last] 2]}]
    set step [expr {double($diff)/([llength $ids]-1)}]
    for {set i [llength $ids]} {$i>1} {} {
        $c move [lindex $ids [incr i -1]] $diff 0
        set diff [expr {$diff-$step}]

#----------- Test:

proc nl2flowtext s {
    # turn multiline text into flowtext, \n only on empty lines
    regsub -all {\n *\n} $s \x81 s ;# dummy char
    string map {\n " " \x81 \n} $s
pack [canvas .c -width 400 -height 410] -expand 1
set mj [mjtext .c 5 5 390 400]
$mj insert end [nl2flowtext $docu(mjtext)]

Disclaimer: Due to differing font metrics, Postscript output is produced, but not exactly flush right - mildly ragged instead. Hm - to reinvent TeX in a few hours is not that simple ;-)

Donal Fellows has an implementation for text widgets which uses blank images as spacers: