On Heathcliff and hackish image manipulation

This should probably just be two posts, but it’s been months since I posted anything and I’m just going to go for it. But if you just want to see me talk about a terrible bodge-job of a shell script, scroll down a bit.

For a while I’ve had this idea to start a Twitter bot that posts a strip made up of a random Heathcliff panel paired with a random Heathcliff caption. There are a few reasons for this, the first of which is that under Peter Gallagher’s tenure, Heathcliff has gotten… weird. Recurring themes include friendly but inexplicable robots, helmets that communicate what their wearer is thinking (maybe?), the Garbage Ape, the magical levitating properties of bubblegum1, the meat tank… the strip has gotten to be a real experience for every possible state of the human mind. But more importantly, the strip tends to follow a handful of rules. The vast majority of daily strips are a single panel, have Heathcliff in them, and have a caption that is spoken by some non-Heathcliff entity. Generally, there are at least two entities grouped together in the scene, one of whom is speaking the captioned text to the other. Often this is Grandpa and Grandma Nutmeg, Grandma Nutmeg and Mrs. Jablonski, Iggy and Willy, two random birds, two random mice, two random fish in a fishbowl… it’s rarely terribly significant who is speaking, just that someone is commenting on the scene in front of them.

All of these rules mean that, just in a sort of technical, editorial sense, most captions should basically work with most panels. My theory was that since they largely are technically compatible, and since the strip largely exists in this curious space, that this experiment would likely work in more ways than one. I ended up not going full bot with this experiment; ensuring that good, comprehensive alt text was present was a high priority, so every post on Heathcliff Stew is created manually and scheduled ahead of time. This is also important because my script is quite imperfect, with about 10% of attempts resulting in failure modes including entire strips being inserted instead of mere captions, cut off captions, and double captions. Since I have to intervene anyway, I have simple rule of my own: I don’t curate. As long as a strip doesn’t meet one of the failure modes, I post it. I have made a couple of exceptions, where I thought the combination of panel and caption could have potentially been offensive or insensitive; if I have even the slightest iffy feeling about it, I just… don’t have to post it. But otherwise, everything gets posted.

This was important to me, because I don’t think curating out only the good ones is… fair, exactly? I think that the idea that the Heathcliff world is strange enough that this often works is very funny, and in fact, I often have to double-check that I’m not actually working with a failure. When the strips are good, they’re very good. But the world of Heathcliff still has rules and norms and plenty of the randomized strips simply aren’t good. I think it’s only fair to the land of Westfinster to post these Ls alongside the bangers. In a recent interview, Peter Gallagher said,

There are a couple of other [Twitter accounts] – and I appreciate anybody who’s doing stuff – but there are guys who are just taking random Heathcliff comics and putting different captions on them. So they make absolutely no sense. I’m like, “Hey, come on.”

I kind of laughed when I first read this, but it gets to the heart of why I don’t want to post just the good ones. When the comics work, it’s a testament to the consistency of Gallagher’s bizarre vision of Westfinster, but if it worked all of the time, it would kind of just… dismantle that and reinforce the belief that Gallagher-era Heathcliffs are inherently random in design. And that’s not the reality of the situation. I don’t know, if you read this blog, you probably realize that I’m not a huge fan of artists having inalienable control over their work. Gallagher certainly didn’t seem to be trying to assert that, just expressing disappointment. I’d like to think that I’m exploring this world in a way that doesn’t disappoint, setting parameters that are at best clever détournement and at worst… what you’d expect out of a random comics machine.

About that machine…

So, the script itself. It’s an incredible hack that I don’t really want to post, but I learned a couple of things while trying to figure out how to do this in an incredibly hackish way. The key to hacking this together was Hough Lines detection, specifically the tool for this built in to ImageMagick. First we use a basic raster-based edge detection algorithm to reduce our image to a 1-bit representation of edges. The Hough Lines detector takes this and attempts to find lines, which it crucially saves out as vectors. The result is something like this (source):

Hough line transform: 130x2+150 218 184 169 197 216 230 190 234

What’s important here is that we have a plaintext vector format describing a bunch of lines. More on this format (MVG) later, but it’s essentially a simplified version of SVG. This means with some hackish parsing, we can find the line that is (hopefully) the bottom of the panel frame, and split a strip into its panel and its caption. Originally I was just trying to detect the lowest horizontal line, but this caused a fair few failures where the caption was dense enough to cause erroneous line detections. I improved my success rate by just discarding lines that weren’t at least ten pixels or so high. Here’s the current version of this embarrassing script:

#!/bin/zsh
a=$RANDOM
b=$RANDOM
c=$RANDOM
d=$RANDOM
[[ -z $2 ]] && fullDate[1]=$(date -d "August 30, 2004 +"$(echo $a $(date "+%s") "1093838400-604800/%p" |dc)" weeks +"$(echo $b "5%p" |dc)" days") || fullDate[1]=$2
[[ -z $3 ]] && fullDate[2]=$(date -d "August 30, 2004 +"$(echo $c $(date "+%s") "1093838400-604800/%p" |dc)" weeks +"$(echo $d "5%p" |dc)" days") || fullDate[2]=$3
for i in 1 2; do
	temp[$i]=$(mktemp)
	shortDate[$i]=$(date -I -d $fullDate[$i])
	curl -o $temp[$i] $(date -d $fullDate[$i] "+http://picayune.uclick.com/comics/crhea/%Y/crhea%y%m%d.gif")
	mogrify -resize 300 $temp[$i]
let "targetHeight[$i] = $(identify -format '%h' $temp[$i])-10"
convert \( $temp[$i] -canny 0x1+10%+30% \) -background black -stroke red -hough-lines 130x2+150 mvg:- | grep -oP '(?<=line 0,)[^. ]*' | sort -nr | while read -r j; do
	[[ $j -lt $targetHeight[$i] ]] && let "crop[$i] = $j + 1" && break
	done
done
outFile=$(echo $1"."$shortDate[1]"+"$shortDate[2]".gif")
let "caption = $(identify -format '%h' $temp[2]) - $crop[2]"
convert $temp[1] -crop 300x$crop[1]+0+0 top/$outFile
convert $temp[2] -crop 300x$caption+0+$crop[2] bottom/$outFile
convert \( $temp[1] -crop 300x$crop[1]+0+0 \) \( $temp[2] -crop 300x$caption+0+$crop[2] \) -append $outFile

[[ -e captions.txt ]] && printf "ID $1\nPanel: $shortDate[1]\nCaption: $shortDate[2]\n($1)\n\n" >> captions.txt
sleep 2

Nobody should try to learn anything from this (well, maybe the Hough Lines trick). I start out setting a bunch of variables to $RANDOM, because zsh handles this environmental variable in an odd way; only certain things trigger a reseed, so just manually making the four random numbers I need is a surefire way to ensure I actually get… four random numbers. From here, I generate the random dates I need, assuming I haven’t passed them as arguments. Since this is just for me, the arguments are not parsed with any intelligence; $1 is always a prefix for the filename, $2 is always a panel to force, and $3 is always a caption to force. I test for $2 and $3 with -z, which checks for a zero-length string. A problem I was worried about here is how to do a random panel with a forced caption – if $2 is blank, then what we want as $3 will simply become $2. This is trivially hacked around, however, test.sh '' test will pass a zero-length string to $1 and “test” to $2.

Parsing the dates was, as always, a wild ride. GNU date tries to interpret the dates passed to it in a sort of magical way2. This isn’t documented in its manpage, and is only sort-of-kind-of documented in info. Fortunately, it’s easy enough to say August 30, 2004 (the date of the first Gallagher strip) + 420 weeks + 3 days. I generate two random numbers for each date so that I can do it in weeks/days format; this allows me to easily ensure I’m only getting strips from Monday through Saturday. Typically when I use date, I’m either fetching the current date or tomorrow. Every time I have to do something more complex with the utility, I run into this matter of trying to figure out what does and doesn’t work with its input schema, and as per usual, I don’t feel like I learned anything of material value this time. For forced dates, I just pass the unaltered values of $2 and/or $3. Fortunately date accepts ISO 8601 style YYYY-MM-DD dates, so I’ve been opting for this.

Unlike its input, date’s output formatting is well-documented. Helpfully, it allows any arbitrary text to be output along with the date variables, presumably so you can do something like %Y-%m-%d. I exploit this convenience, however, to format the entire URL of the strip image to download with curl. Once the two images are downloaded, I ensure they’re the same width (most of the Heathcliff strips seem to be 300px wide, but I’ve run into a few that aren’t, which obviously causes a problem when the two images don’t match) and then do the Hough Lines detection. ImageMagick’s MVG format is pretty easy to parse:

# Hough line transform: 130x2+150
viewbox 0 0 300 360
line 0,0.5 300,0.5  # 218
line 0,3.5 300,3.5  # 184
line 1.5,0 1.5,360  # 169
line 293.38,0 299.664,360  # 197
line 295.5,0 295.5,360  # 216
line 298.5,0 298.5,360  # 230
line 0,328.5 300,328.5  # 190
line 0,331.5 300,331.5  # 234

The lines are essentially ordered top-to-bottom, and horizontal lines are always going to start with line 0. My original naïve approach just grepped all such lines and took the last one, but my slightly-more-optimized approach iterates from the bottom up until one is at least 10 pixels from the bottom. I can probably optimize this better, as well as some of the values in the Hough Lines detection3. From here it’s a simple matter of mashing the two halves together.

All in all, for this sort of lightweight image detection task, ImageMagick’s tools are useful enough to hack together a good-enough shell script. Would I have been better served learning some actual image detection libraries and scripting something in Lua or Python? Probably, but I also likely would’ve gotten bored and given up before coming up with something useful. And ultimately I learned a few things about ImageMagick, I learned that when lazily parsing arguments '' counts as a zero-length yet extant string, and I learned that for hacks and code golfing purposes, date’s output formatting lets you go wild with arbitrary characters. Also, I’ve realized that the simplicity of ImageMagick’s MVG format could make it useful for going in the other direction in the future; generating MVG code in a script (or by hand) seems trivial compared to SVG. I still insist that anyone looking at my script above only use it as an example of what not to do, but it does the job I need it to with a success factor that I’m happy with, and I learned a few new tricks. Plus I can get a random Heathcliff any time I want.


  1. This might have started as a George Gately thing, tbh. I know a lot of Heathcliff history, but I haven’t dived into the deep end on this one. I just feel like I remember this as an older gag. ↩︎
  2. I haven’t used a BSD system in a while. I know BSD date is different, but I don’t remember off-hand if its input schema is as vague and reliant on heuristics as GNU’s. ↩︎
  3. Notably, this script has a massive failure rate if I try it on Marmaduke strips. These parameters don’t work generally, and need to be tweaked for the task at hand. ↩︎