iOS statusbar cleaning on any background

The last time I wrote about this topic, I said

This is, I hope the last update to my statusbar cleaning script.

My hopes are about to be dashed.

If you haven’t been following the many posts I’ve written on this topic as I’ve fumbled my way through iteration after iteration after iteration after iteration, the goal is to create Pythonista scripts that can be run on an iPhone or an iPad to clean up screenshots before adding them to a blog post. It was inspired by Federico Viticci’s rant about ugly screenshots in an episode of The Prompt back in October. His main beef is with screenshots that show rundown batteries and poor signal strength in the statusbar. I thought it would be fun to use the Python Imaging Library, which comes bundled with Pythonista, to overlay a cleaned-up statusbar on a screenshot. This initially simple little project has turned into an unhealthy obsession, as I’ve continually rejiggered the script to make it more portable and allow it to handle a greater variety of screenshots.

Apart from a few tweaks that I may do to improve the overlay images, I think I’m finally done. I now have scripts that can clean up the statusbars in any screenshot, including those with variegated backgrounds like you see in the Weather app

Weather

and the Messages app

Messages

As you can see, the scripts can handle screenshots taken on both WiFi and LTE and with either black or white symbols. They work in landscape mode, too.

Cleaned landscape PCalc screenshot

The scripts do four things:

  1. They fill in the battery icon.
  2. They fill in the signal strength dots.
  3. They show Bluetooth as active.
  4. They show Locations Services as active.

These are accomplished by overlaying transparent images onto the screenshot, filling in or thickening up the symbols to get uniformity in the output. Because this approach doesn’t replace the statusbar with an entirely new image, it works on screenshots with any background. There are, however, limitations:

  1. You don’t get to choose the time. The time showing in the original screenshot will still be there in cleaned version. If you’re a big fan of the 9:41 AM thing, this will bother you, but I’ve never cared about slavishly mimicking Apple’s screenshot style. In fact, when I’m telling a story with screenshots, it’s usually better to have the actual time in the screenshot.
  2. You can’t have the battery percentage showing. I used to have the percentage showing, but I’ve learned that I gain nothing from knowing that my battery’s at 71% instead of 78%.
  3. The location arrow isn’t real. You’ve noticed that there are two locations arrows: a solid one when the device is actively determining its location, and a outlined one when it isn’t. I used to think that the solid one was simply a filled-in version of the outline. It isn’t. The solid arrow is actually slightly smaller and is offset to the right. To get an overlay that would cover whichever arrow happened to be in the screenshot, I had to draw one that was large enough to cover both. It’s OK but could use some work.

The alpha channel masks I use to generate the overlays are:

For the left side on WiFi:

Left statusbar image WiFi

For the left side on LTE:

Left statusbar image LTE

For the right side:

Right statusbar image

If you use a different carrier, or if you never use Bluetooth, you’ll need to use slightly different images if you want to use these scripts. My suggestion is that you start with a screenshot of an app that has a true black statusbar. I use the Yahoo Sports app, but there are plenty of other. Don’t be fooled, though, by apps like Reminders that look like they have a true black statusbar but which actually use a very dark gray.

As I explained in my last post on this topic, I embed these images in my scripts by encoding them as ASCII strings. The script that does that for me is called pack-image:

python:
 1:  #!/usr/bin/python
 2:  
 3:  import Image
 4:  import zlib, base64
 5:  import sys
 6:  import textwrap
 7:  
 8:  a = Image.open(sys.argv[1])
 9:  a = a.convert('L')
10:  w, h = a.size
11:  s = a.tostring()
12:  sc = zlib.compress(s)
13:  s64 = base64.b64encode(sc)
14:  s = "'" + "',\n             '".join(textwrap.wrap(s64, 50)) + "'"
15:  print """
16:  PackedImage('L',
17:              (%d, %d),
18:              [%s])""" % (w, h, s)

It’s run from the command line like this,

pack-image 'Right statusbar image.png'

and it prints out something like this

PackedImage('L',
            (130, 40),
            ['eJxjYBgFo2AUDFHwX3XAXfBfm462Ga+9j8UF//XpZv+KV//LsL',
             'ngvzld7Pc6ALTqFaY4yAX/bdFF9/0nFhwgznq25Ktg5cVIYoy7',
             'OSAusAFKuKCq3/k/hkiPRf7fSYQqvtJX38AOuGCMJLr9/38OsA',
             'sY5YFSfig6/vsTHQb+/wnaLzvh80OoapT4/vP//z8OsAsYeIHs',
             'cBQXMBHtAqb/TPjtN176B674AYoMx6//oFAAuwA9EKjnAte9n4',
             'GK/kEVW6FKsnwCOQHoAsn/6GmRSi5gS7j0/zuS2tfoCpieg8WV',
             '/2MUCaS54P9/a6wOMH6CpjYdQwnjDagUeslMqguwpsZL79GU/s',
             'Gm6gRYShpdmCouQAbghFCPVQokY4wpSqoLCOQHiAsEsUiwvgKb',
             'glE90iQM1mARF4QZo0cPF2BpBqgAhQ9D8oIpZS5wJMIFFzBFHY',
             'H6J4FKJGEgw44CFxCyHuICSwzBTKD2NEipzA0sHr1o7AKM0oiB',
             'AVgiOjEgXOBDYxdglkYMvJ/ASQPoAvH/6NUzdetGkIG/8Mj9V/',
             '2P0UShdvuA4V8dPhf8x9JMo3Yb6SEeOZAptG+qFuF3gRbNHYAX',
             'AEukgXUAw3+5AXbAKBgFwxUAAElqKPY='])

which can be pasted directly into the script that does the cleaning.

Speaking of which, here, finally, is the script that cleans the statusbar of a screenshot taken on WiFi:

python:
  1:  #!/usr/bin/python
  2:  
  3:  import Image
  4:  import base64, zlib
  5:  
  6:  # Jay Parlar convinced me to turn this data structure
  7:  # from a dictionary into an object. 
  8:  class PackedImage(object):
  9:    def __init__(self, mode, size, data):
 10:      self.mode = mode
 11:      self.size = size
 12:      self.data = ''.join(data)
 13:    
 14:    def unpack(self):
 15:      return Image.fromstring(self.mode,
 16:                              self.size,
 17:                              zlib.decompress(base64.b64decode(self.data)))
 18:  
 19:  def cleanbar(screenshot):
 20:    '''Clean up the statusbar in an iOS screenshot.
 21:  
 22:    Cover the signal strength, battery, location, and bluetooth
 23:    graphics with full strength symbols.'''
 24:    
 25:    # This is for retina displays.
 26:    height = 40
 27:  
 28:    # Statusbar image data for an iPhone 4, 4s, 5, 5c, or 5s portrait screenshot.
 29:    # The data string is compressed and base64-encoded.
 30:    limg = PackedImage('L',
 31:                       (200, 40),
 32:                       ['eJztlltIFFEYx896v6SSKCaJ2uZDD22UUVtBJBWZaS9BSgnWQy',
 33:                        'ShvSgk9hLdTMl8ENIVozCSCsTqKUW6qFCiYQaZ2oOrq4iUtKm4',
 34:                        'OW7MvzOzM7Ozl3bE8RI0v4fzne+cb2f+/z1nzgwhGhoaGhoaGh',
 35:                        'r/AxF6vZ8jyAjlpwLjdM4ij1kZgenlbSPTmBlpK08PXEXtchqB',
 36:                        'TBryISedjuhbFjBXE80Xec7KSKyakk1OVSWtugfK+l9zeEbj4a',
 37:                        'ccoxjg4zZCwobRdf87Ov2Jt1knEZUMXGEqo1bfRyGujtvjxOwR',
 38:                        'bord02gmJGgEu521slmJvWZOetfltHg/4hd/oPQdl5r3LeLOib',
 39:                        'WjjKU2SWUm8pHV30aJF6WXUETbauT49HF2gcpuNshGDE3ckuQp',
 40:                        '2kif4Rdv9qiqTCQVHWQ7hrwoTUMrbd9ji08fGQymjrmPTYFxH/',
 41:                        'MgeVrYhbPJKjKJGpwjpB/7PZUGD+MEuYEnsmJv+yq7V0/bgMz6',
 42:                        'PiusffVZATTT92Yr2SAm6XEyqchEQn7aIgkpxUMvSg+y1la0r1',
 43:                        'PwQeihrcszS1c3n9HxY0pYpF9YVGQiuXhM22TWFuWpNHYQqAqS',
 44:                        '39urD3rkvYSclmhlF4TMS/WMbumZyFtkcKETFzyUnrSyX/CC1g',
 45:                        'b4K/goohe1XjeGkhDjtW+0X7wYH8u6HimY5EXm44O70vPsxJ6Q',
 46:                        'LpSRGLQp+KAP2YMYoRtuQqPOa5Ebzn1epyITKANr5/gN7HBVus',
 47:                        'k2t5mQDeM4dQh3lXz4H5clmUFea9xJnhHkOE6hJWbC/SfQ080z',
 48:                        'LoiVlBY7joOdNlsrPbYUfHBsbRhjxhoMf533YBnfH1noFnpH8C',
 49:                        'PERek9XORjDjAp/cE+fGTZ+avbsxZvJNFkYSymJJUZx3MUCD26',
 50:                        'MrkuSitQ4+i8xlfpa8mHj9B23kdH2OJ9LBtxdkY6Iu/gDRckpb',
 51:                        'swb+RiBebxSvwU97WvInuojZ41+EAkpARNUj8VbAqRK62DvaGw',
 52:                        'egADiZ/QIIz58kFiP6M/dqWk+mQQst3cj1vERWnBEAtMV0eShA',
 53:                        'lccQz59EE2tiasjE61hBsNwWutQUNDQ0NDQ+Pf4g+RSY1o'])
 54:    rimg = PackedImage('L',
 55:                       (130, 40),
 56:                       ['eJxjYBgFo2AUDFHwX3XAXfBfm462Ga+9j8UF//XpZv+KV//LsL',
 57:                        'ngvzld7Pc6ALTqFaY4yAX/bdFF9/0nFhwgznq25Ktg5cVIYoy7',
 58:                        'OSAusAFKuKCq3/k/hkiPRf7fSYQqvtJX38AOuGCMJLr9/38OsA',
 59:                        'sY5YFSfig6/vsTHQb+/wnaLzvh80OoapT4/vP//z8OsAsYeIHs',
 60:                        'cBQXMBHtAqb/TPjtN176B674AYoMx6//oFAAuwA9EKjnAte9n4',
 61:                        'GK/kEVW6FKsnwCOQHoAsn/6GmRSi5gS7j0/zuS2tfoCpieg8WV',
 62:                        '/2MUCaS54P9/a6wOMH6CpjYdQwnjDagUeslMqguwpsZL79GU/s',
 63:                        'Gm6gRYShpdmCouQAbghFCPVQokY4wpSqoLCOQHiAsEsUiwvgKb',
 64:                        'glE90iQM1mARF4QZo0cPF2BpBqgAhQ9D8oIpZS5wJMIFFzBFHY',
 65:                        'H6J4FKJGEgw44CFxCyHuICSwzBTKD2NEipzA0sHr1o7AKM0oiB',
 66:                        'AVgiOjEgXOBDYxdglkYMvJ/ASQPoAvH/6NUzdetGkIG/8Mj9V/',
 67:                        '2P0UShdvuA4V8dPhf8x9JMo3Yb6SEeOZAptG+qFuF3gRbNHYAX',
 68:                        'AEukgXUAw3+5AXbAKBgFwxUAAElqKPY='])
 69:  
 70:    # Calculate various dimensions based on the size of the screenshot.
 71:    width = screenshot.size[0]
 72:    lbox = (0, 0, limg.size[0], limg.size[1])
 73:    rbox = (width - rimg.size[0], 0, width, rimg.size[1])
 74:    
 75:    # Decide whether the overlay text and graphics should be black or white.
 76:    # The pixel at (width-13, 21) is in the button of the battery.
 77:    p = screenshot.getpixel((width-13, 21))[:3]
 78:    if sum(p) > 3*250:
 79:      symbolcolor = 'white'
 80:    else:
 81:      symbolcolor = 'black'
 82:  
 83:    # Create the masks.
 84:    lmask = limg.unpack()
 85:    rmask = rimg.unpack()
 86:  
 87:    # Make the overlays.
 88:    left = Image.new('RGBA', limg.size, symbolcolor)
 89:    left.putalpha(lmask)
 90:    right = Image.new('RGBA', rimg.size, symbolcolor)
 91:    right.putalpha(rmask)
 92:  
 93:    # Paste the overlays and return.
 94:    screenshot.paste(left, lbox, left)
 95:    screenshot.paste(right, rbox, right)
 96:    return screenshot
 97:  
 98:  # And here we go.
 99:  if __name__ == '__main__':
100:    import photos, console
101:    screenshot = photos.pick_image()
102:    console.clear()
103:    cleanbar(screenshot).show()

I have it saved in as Wifi Statusbar.py in this Gist. If you have Ole Zorn’s New from Gist.py script, you can install mine directly from within Pythonista. There’s also a Gist for the similar script that cleans screenshots taken on LTE. The only difference between the two is the image used on the left side.

Using the scripts is straightforward. When you have a screenshot that needs to be cleaned, launch Pythonista and run the WiFi or LTE script, as appropriate. You’ll be taken to the image picker, which looks very much like the Camera Roll view in the Photos app. Tap on the screenshot image you want to clean, and you’ll soon see the cleaned version in the Pythonista console. Tap and hold on the image to bring up the choices to copy the image to the clipboard or save it to the Camera Roll.

Pythonista console

(The most recent version of Pythonista allows you to save images directly to the Camera Roll, without going through this step, but I’ve found that images saved that way are always put in JPEG format. Saving the image through the console keeps it in PNG format, which I prefer for screenshots.)

A few final points: