Saturday, March 26, 2011

Creating a mirror-image truetype font: the wrong way and...

There are two ways to do anything:
  • the wrong way, and
  • your mother’s way.
(redacted)
There are actually a lot more than two ways to code anything. But first here's the story: my elder daughter was talking about writing poetry "as the ox turns" (more here) and wondered how hard it would be to visually flip every other line left-to-right. Just reversing the letters is easy; something like this would do it:
#!/usr/bin/python -utt
# vim:et:sw=4
'''Reverse every other line.  Like a filter.'''
import sys
flipIt = False
for aline in sys.stdin:
    aline = aline.strip()
    if flipIt:
        alist = list(aline)
        alist.reverse()
        print ''.join(alist)
    else:
        print aline
    flipIt = not flipIt
It works like this:
INPUT:
The skies they were ashen and sober;
the leaves they were crisped and sere -
The leaves they were withering and sere;
It was night in the lonesome October
Of my most immemorial year:

OUTPUT:
The skies they were ashen and sober;
- eres dna depsirc erew yeht sevael eht
The leaves they were withering and sere;
rebotcO emosenol eht ni thgin saw tI
Of my most immemorial year:
But what if you wanted to flip the pixels as well? Something like this?
Then you'd need a mirror image font. Right. A web search yielded BackBod, and I think I found a reversed Dabbington font, too. But she wanted something that looked more like Times New Roman.

The Wrong Way to Code This

Naturally I went looking on the web for "truetype file format" and proceeded to pick a font file apart, using information from Apple and Microsoft. I began like this:
def main(filename):
    global font_data, flip_data
    font_data = [ord(X) for X in list(file(filename, 'rb').read())]
    # offset subtable
    scaler_type = u32(0)
    assert(scaler_type == 0x74727565 or scaler_type == 0x10000)
    numTables = u16(4)
    print 'numTables', numTables
    startTabDir = 12
    print 'table list'
    glyfStart = None
    tables = dict()
    toffset2name = dict()
    for tabEntryOffset in range(0,numTables*16,16):
        mystart = startTabDir+tabEntryOffset
        tname = l2s(font_data[mystart:mystart+4])
        tstart, tlen = u32(mystart+8), u32(mystart+0xc)
        print '\t%s offset=%#08x len=%#x' % (tname, tstart, tlen)
        assert(tname not in tables)                     # duplicate => evil
        toffset2name[tstart] = tname                    # to sort by offset
        if tname == 'glyf':                     # flip glyphs left-to-right
Basically, I read the entire file in as a bytestream, then created an array (a "list" in Python-ese) of the bytes. That u32(0) call means "give me the 32-bit integer formed by reading 4 bytes starting at offset 0" (that's way later in the file). This program knows where tables start, and looks for certain tables by name (e.g, "glyf"), and...

So what's wrong with coding like this? The problem is that it's re-inventing the wheel. What I should have done, had I known of it at the time, was consult stackoverflow.com; I would have found questions and answers like this one, which gave me the clue that maybe there was a module out there that already handles truetype (though that question was about PERL) or (aha!) this one which led me to TTX, a fabulous package that turns ".ttf" files into XML and back.

So I was doing a bunch of work (and a lot of it was empirical, based just on what I found in this one file) rather than following the possible versions (etc) that the specs allow. Before I stopped working on the wrong way to code this, I had 476 lines in "flip.py" -- of which 398 were nonblank, noncomment lines. Besides, the fonts it produced weren't quite right.

Not so Wrong

So here's the new plan. Rather than writing all that code to parse the (mostly binary) TTF file, ttx would turn (e.g.) times.ttf⇒times.ttx; I'd modify the XML inside times.ttx, creating, say, semit.ttx ("times" backwards) and ttx would turn that into "semit.ttf".

Besides flipping each glyph left-to-right, I'd also flip the kerning table. Why? Consider the character pair ‘P.’ -- we want the ‘.’ closer to the ‘P’ than it would be without kerning, right? Now imagine if the ‘P’ is flipped left-to-right so it "sticks out to the left" like ‘¶’ -- in this case we want the characters moved closer when the ‘.’ comes before the (flipped) ‘P’. Thus the pair we want to look for is not {‘P’,‘.’} but rather {‘.’,‘P’}.

It's now about 121 lines (nonblank noncomment lines), after trying it out on a few more fonts (one of which didn't have a kerning table). The fonts look good, too. Here's the result of "pydoc -w flipttx":

 
 
flipttx
index
/mnt/home/collin/fonts/flipttx.py

Flip a true type font (ttx) horizontally.
 
Usage: flipttx.py [-d] {-o oldname} {-n newname} [infile [outfile]]
        -d (OPTIONAL): add debugging output
        oldname (REQUIRED) is original font name, e.g., "Times New Roman"
        newname (REQUIRED) is new (flipped) font name
        infile (OPTIONAL) is name of old font's ttx file 
        newfile (OPTIONAL) is name of new font's ttx file
 
OPERATION
Given a truetype font, named "Foo", in a file "fontFile.ttf",
create a flipped font (call it "Oof") in "oof-flip.ttf" as follows:
1. ttx fontFile.ttf
   => will create fontFile.ttx
2. flipttx.py -o Foo -n Oof fontFile.ttx oof-flip.ttx
   => will create oof-flip.ttx
   * and "Foo" in fontFile.ttx's NAME table becomes "Oof" in oof-flip.ttx
3. ttx oof-flip.ttx
   => will create oof-flip.ttf
 
Use spadmin (for OpenOffice.org) or FontBook (on Mac OS X), etc.
to get oof-flip into your system.  In your application (OpenOffice.org,
NeoOffice, Micro$oft Office, etc.) specify font name "Oof"
 
Why are oldname/newname required?  Because you need a way to specify
the flipped font name (e.g., "Oof").
 
GUIDES FOR THE PERPLEXED
    * "ttx" -- see http://www.letterror.com/code/ttx/
    * what's in a truetype font file?
      http://developer.apple.com/fonts/ttrefman/rm06/Chap6.html (2002)
      http://www.microsoft.com/typography/otspec/otff.htm (2008)
 
HOW TO AVOID UGLY ON-SCREEN DISPLAY
    1. On a system like Mac OS X 10.6 you can just do what comes 
       naturally: add fonts using the "Font Book" application. 
       Then NeoOffice, Aqua (maybe even X11) will be able to 
       display the new font just fine.
 
    2. On a system like OpenSUSE 11.3 where OpenOffice.org gets 
       fonts from "spadmin" but you need to "xset fp+" or similar
       for display fonts, be sure to run mkfontdir(1) then add
       the directory to your X server's font path, lest your
       screen look truly ugly.  But at least on my system, "print"
       and "export to PDF" from OpenOffice.org both produced nice
       enough output.
 
BUGS
    1. Doesn't handle a too-short hmtx table, such as might be
       the case with some monospaced font.  (But Courier New
       was OK -- maybe ttx creates a full hmtx table?)
 
    2. Doesn't do anything with composite glyphs.  Maybe they'll 
       "Just Work" -- but probably not.
 
    3. Doesn't do anything with GSUB so ligatures, if your font has
       any, will probably look goofy.
 
VERSION
    $Id: flipttx.py,v 0.5 2011/03/26 20:05:07 collin Exp $

 
Modules
       
getopt
sys
_xmlplus

 
Functions
       
DPRINT(what)
Print onto sys.stderr if DEBUG is on.
flipGlyphs(glyfNode, hmtxDict)
Use hmtxDict to flip glyphs left-to-right (modify in place).
flipKern(kernNode)
reverse the order of the pair, i.e., L<-->R, in each entry.
main()
Parse the input XML, flip glyphs, flip kern table, give a new name
to the new font, write the XML tree to output file.  How did I know
what to do here?  Lots of it came from web info on truetype fonts,
especially http://developer.apple.com/fonts/ttrefman/rm06/Chap6.html
makeHmtxDict(hmtxNode)
Return a mapping of name to node in horiz. metrics table
tweakNames(nameNode)
Change all instances of OLDNAME to NEWNAME, in nameNode.
If font's original name="Times New Roman", OLDNAME="Roman", 
NEWNAME="Namor" then new name would be "Times New Namor" -- i.e., 
OLDNAME/NEWNAME can be substrings of the font's name.
usage()
Print help message to stderr and exit.

 
Data
        DEBUG = False
INFILE = <open file '<stdin>', mode 'r'>
NEWNAME = None
OLDNAME = None
OUTFILE = <open file '<stdout>', mode 'w'>
progname = 'flipttx.py'
Leave a comment if you want the source, which is currently 264 lines (wc -l) and about 121 noncomment nonblank lines.

And maybe an even less wrong way...

A search on "poetry oxturn" (no quotes) led me here which in turn led to this program which does the whole thing for you -- flips every other line and creates a postscript or PDF of the result. Apparently you can just try it online without having to download it and run under Tcl/Tk.

But... currently it gives you a typewriter-like font, not so pretty. This may change soon, as I'll send my program to the site's webmaster.

No comments: