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.

Wednesday, March 23, 2011

So you want to be a VP, part III

link to part II
Just read this terrific post summarizing a talk by Patty Azzarello on How to be Really Successful at Work AND Like Your Life. Two sentences from amazon.com's "About the author":
Patty Azzarello became the youngest general manager at Hewlett-Packard at age 33, ran a $1B software business at 35 and became a CEO at 38 (without turning into a self-centered, miserable, jerk). Whether leading massive business transformations or advising CEO’s 1-to-1, her insights, integrity, and generosity have fused with her work to create life-changing impact on the careers of thousands of people in large and small companies across the world.
All that's impressive, but I really liked her advice to DO BETTER, LOOK BETTER, CONNECT BETTER (details on the above post on compscigail's blog).

Patty has a short video on her home page introducing her (November 2010) book; amazon.com hosts an author page with links to her book, blog posts, etc.

This looks like great advice, particularly if you want to become a VP without ruining your life.

Limits of Divine Power

Links: my other postings on Lent ; our church's reading plan
Today's reading includes Mark 5:21-6:6, which put me in mind of a message I heard over 30 years ago, at my first Christian conference, where LeRoy Eims talked us through this area of Scripture; it shows the power of Jesus over the wind and the waves (Mark 4:35-41), his power over demons from hell (Mark 5:1-20), over sickness (Mark 5:25-34), over death itself (Mark 5:35-43).

What an exciting passage! Now the people in the land of the Gadarenes demonstrate one limitation on the Lord -- well, it was self-imposed, actually; they asked him to leave (they wouldn't listen to him!) and he did.

Here's another limitation on the power of Jesus. Wait, don't pick up stones to throw at me quite yet -- tell me if this doesn't mean Jesus couldn't...:

He could not do any miracles there, except lay his hands on a few sick people and heal them.
Mark 6:5
Right? Verse 6 says he was amazed at their lack of faith.

I think this an astounding truth. If I have no faith, I can actually limit the power of God to do things in my life. I can grieve his Holy Spirit (as Ephesians 4:30 says).

And on the positive side, I can bring him joy and delight when I take him at his word. Imagine that -- you and I can bring joy to the creator of the universe. We can please him by the way we live (Hebrews 11:6; Colossians 1:9-12).

A pastoral word

Does that mean if I pray and nothing seems to happen, it's because I don't have enough faith? No! And I can prove it.
  1. Look at 2 Corinthians 12:7-9; why didn't God grant Paul's request to be freed from his "thorn in the flesh"?
  2. Consider the girl who was raised from the dead in Mark 5:35-43: how much faith did she have? Zero!
  3. Finally, let me get mathematical. We observe that if I drop a rock on my foot, it hurts. Therefore if my foot hurts, I must have dropped a rock? Uh... no. Maybe I stepped on something sharp. Maybe an insect or some other animal bit it. Maybe something hot was spilled onto my foot, or maybe a golf ball just flew into my yard and hit my foot. That is, [P⇒Q] does not imply [Q⇒P]. Lack of faith may inhibit God's activity, but if God doesn't do something, it might not have anything to do with how much faith I have or haven't got.

Monday, March 21, 2011

Resisters Are Futile. I think.

No, this isn't about electronics, and that isn't a typo. "Resister" comes from Flory and Miller's Finding Faith, chapter 4. I'll simplify their characterization of this group, of which I'm sometimes a member (when I forget...) by calling them moderns fighting the transition to the post-modern era. We moderns have the fantasy that someday facts and logic will once again win the day. (In other words, “Never mind this ‘experience’ stuff; let's discuss the facts.”) Here's a sentence from Flory and Miller's Summary and Conclusions of their Resisters chapter in Finding Faith:
...Resisters are intent on resisting a changing social and cultural order where their conception of reason and rationality is under attack, or more accurately (at least according to their analysis) completely disregarded, and are trying mightily to regain a voice for a commitment to reason and rationalism that legitimates everything they believe in and, they argue, legitimates everything anybody should believe in. (117)
Yes, I sometimes resemble this remark. But when I do, I think I (and they) miss the point (to borrow a phrase from Campolo and McLaren) in a few ways.
  1. First, facts and logic have never really carried the day. As I've often said when reading, say, John 11:45-46 -- “When the chief priests and Pharisees heard that Jesus had raised Lazarus from the dead, they said, ‘Whoa, boys! We've been all wrong about this guy! He really is from God!’”

    No they didn't; facts and logic meant nothing to them in this area. Or when the Roman guards told the priests about the empty tomb in Matthew 28:4,11 -- same deal; God was really working but the priests refused to understand.

  2. Second, society really isn't heading in that direction; it really is heading toward story as the way of understanding truth, as Fallows points out in this great article on the new media (more in this previous posting). This quote from that article bears repeating:
    Jon Stewart and Stephen Colbert ... don't fact-check Fox News, or try to rebut it directly, or fight on its own terms. They change the story ... by presenting the facts in a way that makes them register in a way they hadn't before.
  3. Third, much as people don't apprehend truth presented in the style and structure I'm familiar with (and like), logic isn't 100% ineffective among young people today.

    A few summers back, we had a terrific time discussing parts of Keller's The Reason for God with some college students. Granted that these folks weren't exactly a random sampling, yet in areas where they weren't overly invested in their position, Keller's trenchant critique did gain traction. Some people really do want to know if their thinking is a little too fuzzy.

    But these folks would never pick the book up on their own; they came partly because of the material but also partly because it was at our house, they're friends of our daughters, etc.

Flory and Miller say "the relative success or failure Resisters remains to be seen" (122) and I suppose they are right. Though I don't ever see our society returning to a condition where people at least pretended to be logical more of the time, some people really are interested in truth, and facts and logic, if presented in a palatable way, can change people's minds, and sometimes, eventually, their hearts.

Yet as someone said, nobody ever decided to follow Christ or feed the hungry because they lost an argument.

Sort of Python - part deux

Continuing from part 1...
Now suppose you wanted to do something a little more useful, like a case-ignore search. Well, you'll need, of course, a case-ignore comparator function. Fortunately Python strings have a method called "lower()", which returns a string that's been shifted to lower-case. So rather than comparing the original strings using cmp(), we could pass in stringX.tolower() and stringY.tolower() to cmp() and use that return value, as shown below in compIgnoreCase:
#!/usr/bin/python -utt
# vim:et
'''Program to sort a list of words using various criteria.'''

def show(listA):
    print "\tlistA:", listA
    print

def compLen(x,y): '''Compare words for length''' return cmp(len(x), len(y))
def compIgnoreCase(x,y): '''ignore case in sort''' return cmp(x.lower(), y.lower())
def main(): '''Create a list of words, then sort using various criteria. Display the contents of the list after each sort.''' # Easier to type than ['In','the','beginning' (etc) listA = 'In the beginning God created'.split() print "Before sorting:" show(listA) listA.sort() print "after default sort:" show(listA)
# Sort for length listA.sort(compLen) print "after sort by length:" show(listA)
# Sort ignoring case listA.sort(compIgnoreCase) print "after case-ignore sort" show(listA)
return 0 if __name__ == '__main__': main()
Note the code added in this color; we compare the lower()'d version of the strings; the new results are shown in this color below:
% ./sort1c.py 
Before sorting:
        listA: ['In', 'the', 'beginning', 'God', 'created']

after default sort:
        listA: ['God', 'In', 'beginning', 'created', 'the']

after sort by length: listA: ['In', 'God', 'the', 'created', 'beginning']
after case-ignore sort listA: ['beginning', 'created', 'God', 'In', 'the']
Now what if we wanted to put the words with the highest proportion of consonants at the end, and the more vowel-heavy words at the beginning? I'd write a routine to calculate consonant density, and, like the other comparators, include it in a call to sort(). Added code and results are in this color:
#!/usr/bin/python -utt
# vim:et
'''Program to sort a list of words using various criteria.'''

def show(listA):
    print "\tlistA:", listA
    print

def compLen(x,y): '''Compare words for length''' return cmp(len(x), len(y))
def compIgnoreCase(x,y): '''ignore case in sort''' return cmp(x.lower(), y.lower())
def compConsonants(x,y): '''compare words for consonant density''' return cmp(consonantDensity(x), consonantDensity(y)) def consonantDensity(astring): '''how many consonants in astring?''' ret = 0.0 for abyte in astring.lower(): if abyte in ('a','e','i','o','u'): continue ret += 1.0 return (ret / len(astring))
def main(): '''Create a list of words, then sort using various criteria. Display the contents of the list after each sort.''' # Easier to type than ['In','the','beginning' (etc) listA = 'In the beginning God created'.split() print "Before sorting:" show(listA) listA.sort() print "after default sort:" show(listA)
# Sort for length listA.sort(compLen) print "after sort by length:" show(listA)
# Sort ignoring case listA.sort(compIgnoreCase) print "after case-ignore sort" show(listA)
# Sort by increasing consonant density listA.sort(compConsonants) print "in order of increasing consonant density" show(listA)
return 0 if __name__ == '__main__': main()
So consonantDensity calculates what proportion of each word's letters are consonants, and compConsonants(x,y) compares the consonant proportions of the two strings passed. The results look like this:
% ./sort1d.py 
Before sorting:
        listA: ['In', 'the', 'beginning', 'God', 'created']

after default sort:
        listA: ['God', 'In', 'beginning', 'created', 'the']

after sort by length: listA: ['In', 'God', 'the', 'created', 'beginning']
after case-ignore sort listA: ['beginning', 'created', 'God', 'In', 'the']
in order of increasing consonant density listA: ['In', 'created', 'beginning', 'God', 'the']
That looks about right: "in" is 50% consonants; "created" is 4/7 or about 57% consonants; "beginning", "God", and "the" are 2/3 (about 67%) consonants.

Finally, I wanted to mention "pydoc", a terrific documentation aid. When run on the above code, it produces this:

% pydoc sort1d
Help on module sort1d:

NAME
    sort1d - Program to sort a list of words using various criteria.

FILE
    /Users/collin/tmp/sorting/sort1d.py

FUNCTIONS
    compConsonants(x, y)
        compare words for consonant density
    
    compIgnoreCase(x, y)
        ignore case in sort
    
    compLen(x, y)
        Compare words for length
    
    consonantDensity(astring)
        how many consonants in astring?
    
    main()
        Create a list of words, then sort using various criteria.
        Display the contents of the list after each sort.
    
    show(listA)
Pretty cool, huh? Just put the "documentation strings" in the function declarations, and voila -- instant manpage!

Saturday, March 19, 2011

Sort of Python

Sorry for the title; I couldn't resist. My nephew and I were talking about sorting in Java, a language which I can't even spell. But I remembered, when talking about the idea of passing a function to another function, that this concept takes some getting used to. I decided to write a little example this morning, using a language I actually write code in, viz., Python. Here's the first part:
#!/usr/bin/python -utt
# vim:et
'''Program to sort a list of words using various criteria.'''

def show(listA):
    print "\tlistA:", listA
    print

def main():
    '''Create a list of words, then sort using various criteria.
    Display the contents of the list after each sort.'''

    # Easier to type than ['In','the','beginning' (etc)
    listA = 'In the beginning God created'.split()

    print "Before sorting:"
    show(listA)

    listA.sort()
    print "after default sort:"
    show(listA)

    return 0

if __name__ == '__main__':
    main()
Let me explain briefly. "show" is just a helper that displays the list in a certain way -- i.e., with a tab in front and an extra newline after the word list. I wrote "show" so that, in case I wanted to do something different (e.g., put the extra newline before, rather than after, the list; or use some spaces rather than a tab character) I would just change the layout once, in show(), rather than changing a bunch of print statements throughout.

"main" creates the list, which I creatively call listA, by taking a string (a sentence or phrase will be fine) and splitting it based on whitespace (this is like PERL's "qw"). Then it calls "show" to display the list's original contents.

Finally, it calls the list's sort function, passing no parameters. This sorts according to the default ordering, which may depend on your $LANG or $LC_ALL environment variable. Here's what happens when you run it:

% ./sort1a.py 
Before sorting:
        listA: ['In', 'the', 'beginning', 'God', 'created']

after default sort:
        listA: ['God', 'In', 'beginning', 'created', 'the']

% 
Now what if you want to do something different with the sort -- rather than sorting based on the natural ordering of these words, what if you wanted to put longest words last?

Well, sort() can still help you, but you need a comparator function -- one that, given two words, tells whether the first is shorter (or "less than", or "comes before") the second word in the desired outcome. Let's add that in like this:

#!/usr/bin/python -utt
# vim:et
'''Program to sort a list of words using various criteria.'''

def show(listA):
    print "\tlistA:", listA
    print

def compLen(x,y): '''Compare words for length''' return cmp(len(x), len(y))
def main(): '''Create a list of words, then sort using various criteria. Display the contents of the list after each sort.''' # Easier to type than ['In','the','beginning' (etc) listA = 'In the beginning God created'.split() print "Before sorting:" show(listA) listA.sort() print "after default sort:" show(listA)
# Sort for length listA.sort(compLen) print "after sort by length:" show(listA)
return 0 if __name__ == '__main__': main()
See how that works? I created a routine compLen, which returns -1, not when the first word would precede the second in natural ordering, but when the first word is shorter than the second word. By the way, Python's cmp does this comparison/return thing on whatever we pass it -- hence by just typing cmp(len(x), len(y)) I've created a routine that does what we need to sort by length.

The second change, within main(), calls sort(), passing in this comparator routine. When sort() does its thing, it will put them in ascending order, as defined by compLen().

Does that make sense? When sort() wants to decide if, what's currently say, element#2 should go after what's currently element#3 (i.e., if 2 and 3 are in the correct order), it calls a function. If you just say someList.sort(), then sort() will call a function that returns -1 if element#2 is "less than" element#3, whatever that means. But if you pass in a function like compLen, then sort() will call compLen, which will say whether element#2 is (in this case) shorter than element#3, and so you'll end up with a list sorted in order of increasing length. Here's how it looks when run:

% ./sort1b.py 
Before sorting:
        listA: ['In', 'the', 'beginning', 'God', 'created']

after default sort:
        listA: ['God', 'In', 'beginning', 'created', 'the']

after sort by length: listA: ['In', 'God', 'the', 'created', 'beginning']
%
Did that all make sense? More examples in part 2

Wednesday, March 16, 2011

Cinema: the new church?

Two statements:
  1. Jon Stewart and Stephen Colbert ... don't fact-check Fox News, or try to rebut it directly, or fight on its own terms. They change the story ... by presenting the facts in a way that makes them register in a way they hadn't before.
    Learning to Love the (Shallow, Divisive, Unreliable) New Media
    by James Fallows, The Atlantic April 2011

  2. More theology is discussed at Starbucks on a Saturday night than at most churches on Sunday morning.
    Ralph Winter (or maybe Chap Clark)
    pre-Windrider Festival film event 2011-03-14
It's not quite fair to say that people don't care about truth any more, but the way we take truth in, the way we decide what's true -- those have changed. Here's another snippet from the same article:
“There is actually a lot of energy released by opposing ‘settled facts,’” I was told by Jay Rosen, of the journalism school at NYU. “The more ‘settled’ it is, the more furious the energy. When someone points out an error in what Sarah Palin has said, that becomes another example of the liberal media, and it becomes another tool for organizing.”
Fallows, op. cit.
In other words, people don't decide what to accept or not based upon facts and logic. Instead, it's about something else entirely, which is where we have a lot to learn from the world of film.

We evangelicals have a hard time with this concept. (Which one? Yes.) First, we are (at least where I'm coming from) all about facts and logic to understand issues of importance. What happened about 33 AD after they crucified this man called "Jesus of Nazareth, King of the Jews"? The body was never found by highly motivated, powerful people. They were well-connected and focused enough to get him killed, and producing his corpse would have wiped out that troublesome sect—which instead has lasted nearly 2000 years.

Jesus's empty tomb stands as a serious challenge to to the likes of Dawkins and Pinker, men who say in their hearts, "There is no God." But facts and logic on their own are impotent to change the heart of man.

Yes, it has ever been thus. Those highly motivated powerful people from the 1st century were also unswayed by facts; Semmelweis had facts to prove hand-washing saves lives, but died unemployed and destitute. And so on.

But it does seem worse in our current century than it was, say, in the middle of the previous one.

We evangelicals (some more than others) think of weekly gatherings as being about The Content in The Sermon—surrounded by some music and stuff on either side. We don't give enough thought to the entire experience, which echoes something I heard before about sensory experience. People like me need to think of the entire experience of coming to a meeting -- the sights, the sounds, the smells, and the story all these tell -- rather than focusing only on the propositions contained in the sermon.

Second, the idea of listening to those outside the church, to learn how to communicate truth... well, I can't say we're always as open-minded as we should be. I think we humans -- not just the church by the way -- tend to think too much in terms of "us vs them" in echoes of what Judy Harris calls "group socialization theory." We are very concerned about getting facts straight about our faith, but we're not always so sure about their theology for example. Or on the other side, we put a lot of energy into welcoming the alien and stranger as Jesus and Peter and other Biblical writers, but we're not so sure how a newcomer would feel at their church.

Either of those preceding statements may be at least partly true, but what's worse (besides the smug attitude) is that we add, in our hearts, ...therefore, they can't teach us anything about ________.

Which comes under the heading of "biting off your nose to spite your face" or something like this. We all need to be about following Jesus, and that means taking off our blinders and prejudices, setting aside our pride, and looking for anything we can learn about how to communicate, serve, love, worship.

... that we may live a life worthy of the Lord and please him in every way, bearing fruit in every good work, growing in our knowledge of God, being strengthened with all power according to his glorious might in order that we may have great endurance and patience, and joyfully giving thanks to the Father, who has qualified us to share in the inheritance of the saints in the kingdom of light -- that in all things he may have the glory.

Right. So what do we learn from film? The film world, as Ralph Winter told us the other night, are very interested in the structure, the style. Whatever the substance is or isn't, if the style and structure are good, you've got a hit.

So what do you think of Avatar? Is it too much about tree-worshiping, or is it about hope and redemption, resolving one's inner conflicts, rebirth? How about the first Star Wars film from the 1970s? Was that one about a false impersonal deity (The Force) and occult religions? Or was it about self-discovery, self-sacrifice (e.g., Obi-Wan sacrificing himself so Luke could get away)? These are great stories, brilliantly told.

So are The Little Gorilla and Kavi by the way. If you can see these -- or better yet, see them with friends -- they can provoke more discussion of theology than one typically gets on Sunday morning.