# iOS statusbar cleaning on any background

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

and the Messages app

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.

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:

For the left side on LTE:

For the right side:

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.

(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:

• Instead of having two scripts, Wifi Statusbar.py and LTE Statusbar.py, that do essentially the same thing, I could have combined them into one script that asks the user to choose which overlay to use on the left side. I decided to make two scripts because I’m always going to know which version I want to use before I run it.
• A better solution might be a module with two functions: one for WiFi and one for LTE. That module could then be imported into any number of scripts, including ones that create the side-by-side screenshots that Federico often uses.
• The mask images could probably be improved. If I make any such improvements, I’ll update the Gists, but I won’t write a post about it.
• Although I’ve shown them working on iPhone screenshots only, the scripts should work on the iPad, too.
• I honestly think I’m done with this topic now. If I write any more statusbar cleaning scripts, I’ll post them as Gists but I won’t blog about them.