iOS status bar cleaning one year later

At the end of this post, I said I was done blogging about my scripts for cleaning up the statusbars in iOS screenshots. In the intervening 14 months, I’ve done a fair amount of improving, refactoring, and extending of the scripts, and I’ve kept my mouth shut about it. But since a year is a lifetime on the internet, I feel no guilt about breaking my vow of silence.

As was the case with my original series of posts on the topic, the impetus for this post is Federico Viticci, who tweeted this out last week:

After years of scripting, I may give in and just crop status bars on my screenshots instead of cleaning them. Far less trouble now.
Federico Viticci (@viticci) Feb 8 2015 9:43 AM

I blame Workflow, the femme fatale of iOS automation. Workflow apparently can’t clean screenshots, and poor Federico, unable to resist its seductive charms, is planning to throw his statusbars away just to please it. Meanwhile, his first love, the wholesome Pythonista, sits forlornly on his iPad, waiting for his return. Shame!

To remind you of what screenshot statusbar cleaning is all about, here’s a typical iPhone screenshot:

Uncleaned weather screenshot

Neither the cellular signal strength nor the battery strength are full. This one happens to have full WiFi strength, but it’s common for that to be missing an arc or two. A cleaned-up statusbar looks like this one, with everything at full strength:

Cleaned weather screenshot

My various screenshot cleaning scripts turn the former into the latter.

The scripts are written in Pythonista, and we’ll get to them in a bit. I generally run them from Launch Center Pro, where they’re organized in a group according to connectivity (WiFi, LTE, or 4G) and number of screenshots (single or paired).

LCP screenshot cleaners

Each of these LCP actions launches a script through Pythonista’s URL scheme. The six URLs are

pythonista://Cleanbar?action=run
pythonista://Cleanbar%20Pair?action=run
pythonista://Cleanbar%20LTE?action=run
pythonista://Cleanbar%20LTE%20Pair?action=run
pythonista://Cleanbar%204G?action=run
pythonista://Cleanbar%204G%20Pair?action=run

and the corresponding scripts are

Cleanbar
Cleanbar Pair
Cleanbar LTE
Cleanbar LTE Pair
Cleanbar 4G
Cleanbar 4G Pair

If there’s no “Pair” in the name, the script cleans a single screenshot, and if there’s no connectivity type, it assumes WiFi. Here’s a result from running the LTE Pair script:

LTE screenshot pair

The purpose of each script is to paste full-strength signal and battery images on top of the original screenshot. To avoid overwriting anything else, like the background clouds in the images above, the pasted-on images have alpha channels (masks) to allow the background to show through. Here are the masks for the battery strength,

Battery mask

the signal strength for WiFi,

WiFi Mask

the signal strength for LTE,

LTE mask

and the signal strength for 4G,

4G mask

The black parts of the mask are the transparent areas where the background shows through, the white parts are where the original image is overwritten, and the gray parts are the anti-aliased border between the two.

These masks are combined with rectangles of the same size to create the overlays pasted onto the original screenshot. The color of the rectangles depends on the color of the statusbar text and icons in the original screenshot. That color is either white, as in the images above, or black.

To get the masks, I emailed myself a PNG file that consisted of a single black pixel, saved that to my Camera Roll, and set it to be my home page wallpaper. Then I took a series of screenshots of my home page and cropped them to get the images above. The statusbar is 40 pixels high1 and as wide as necessary to go from the edge of the screen to the last changeable icon. I needed a full battery and a strong WiFi signal to get those images; as long as the cellular signal had at least one filled dot, I could copy it into the other positions.

By the way, if you’re worried that I’m confusing images with masks, don’t be. An image with white lettering and icons on a black background happens to be exactly what we need for the mask, regardless of the color of the statusbar.

I want to embed these mask images into my scripts so I don’t have to worry about the scripts and the images getting separated. To do this, I’m going to mimic what email programs do with attached images: use base-64 encoding to turn the image data into a string. That string representation for each of the mask images will be part of the source code of my scripts.

The program that does the encoding is this little script, 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 """PackedImage('L',
16:              (%d, %d),
17:              [%s])""" % (w, h, s)

I call it from the command line on my Mac like this,

pack-image battery.png

and it returns something like this:

PackedImage('L',
            (62, 40),
            ['eJxjYBgFo2CIgH3/iQUHMDXv/B9DpDWR/3diiP33J9pu//+Yup',
             'mI1s30n2no6v7/35oi3f8HUjfTUHW54xBNLRi6KcpjlOVvysqW',
             'UTAK6AMAzJb0gw=='])

For convenience, pack-image does more than just encode the image. It also splits the encoded string into list of lines of reasonable length, gets the width and height of the image, and prints everything out in a format that can be pasted directly into the statusbar cleaning script (which we’re getting to, I swear).

The pack-image script will be helpful if you want to clean your screenshots but use a different cellular carrier. You’ll have to generate your own mask images like those above and run pack-image on them so you can make the screenshot cleaning script work for you. You’ll need to install either the Python Imaging Library (PIL) or its fork, Pillow, on your Mac before running pack-image.2

OK, let’s move on to the cleaning scripts themselves. The key script is Cleanbar, which not only cleans a single screenshot with WiFi connectivity, it also acts as a library for the other scripts.

python:
  1:  #!/usr/bin/python
  2:  
  3:  import Image
  4:  import base64, zlib
  5:  
  6:  # The image data class. Used as the mask for the status bar overlays.
  7:  # The mode and size are as defined in the Python Imaging Library.
  8:  # The data is input as a list of strings and saved as one long string.
  9:  class PackedImage(object):
 10:    def __init__(self, mode, size, data):
 11:      self.mode = mode
 12:      self.size = size
 13:      self.data = ''.join(data)
 14:    
 15:    def unpack(self):
 16:      return Image.fromstring(self.mode,
 17:                              self.size,
 18:                              zlib.decompress(base64.b64decode(self.data)))
 19:  
 20:  def cleanbar(screenshot, connection='wifi'):
 21:    '''Clean up the statusbar in an iOS screenshot.
 22:  
 23:    Cover the signal strength and battery graphics
 24:    with full strength symbols.'''
 25:    
 26:    # This is for retina displays.
 27:    height = 40
 28:  
 29:    # Statusbar image data for a retina screenshot.
 30:    # The data string is compressed and base64-encoded.
 31:    if connection == 'lte':
 32:      limg = PackedImage('L',
 33:                         (200, 40),
 34:                         ['eJztl0toE0EYx6fGtvHVYlEiKGlMe/CiaEXTg4KotD56EqTY3k',
 35:                          'RapBVEweBJBK0U0UPAJBVRKj4qiI+bQXxWsFaheGjTeig2hFCQ',
 36:                          'trYlMTGyf/cxs9lkQynZXQw4v8N83zfzLfv/s5kZQgiHw+FwOB',
 37:                          'zO/8Aqt3uJEjQsk5dKHSWZJt1qkXEfOCyGdmhpFGfcL34j7q+S',
 38:                          'm/SrOUzdlINnWksDmWLpaattrP4Vx1Mx7n8kMYGwHLcQsnwcA7',
 39:                          'd/oN9G8q3mIjyQw3qvxB30ybGGCENehXqrfXTiYjTtYNU9XGZp',
 40:                          'C54QUvYdOzO9mtVcqA+FQ2jJN52DMzCRigSqDVaMIcF9Fd48Ss',
 41:                          '/hjDj60GyRj8Y5+Tc6f8BQxajDe7IVY3mU7kFIHD9ikzU+XLN0',
 42:                          's827DFQqfpwgZBi79UrLx3GEXEKfptlMH0H11AgaqBj2n4kKQs',
 43:                          '7jbh6le4WZEN6ttMhHRNUTMVAxWvFQHF1ColKvdO0ocL1M++4C',
 44:                          'fNDz6rjugaSqJ1VSeMV4i4NS6MdJndKjM8IInou9S20GfKSU6+',
 45:                          'OD7gFTv0ctJmWR7fiSq7RNiNXbB9BF1uClAR+L2B89BipKF4S0',
 46:                          'xB9gW7bSjYl4DSHroji2Dzes8OGao3KUU6jASsEWw+dBmSgVqy',
 47:                          'o9qxwH2xOJkHhsWeDDzPujCYM0a8C0PUvpLZySYzMwqe51k+/z',
 48:                          'YCQVCVYbrCSeoYNm4pdpzVLaDb+SvMa3Smt8mIYjnapi+TW8kY',
 49:                          'KqdAeSHil2I4lXpbSpSH148VjN6yDUEq3SHqR7O31hhJ1f0Uvn',
 50:                          'FvLxqU3BSbJ8sOldVhigjKIpUwzjSrbSjjEBmPVVkA0xXFCmFv',
 51:                          'LBTkPpv4zGB5sOWKB/sazwbC7/h6/ncDgcDodThPwFVX6ICw=='])
 52:    elif connection == '4g':
 53:      limg = PackedImage('L',
 54:                         (200, 40),
 55:                         ['eJztll9sS1Ecx0+3tmzYtClGzP5JNNkS4k/izwMe0LAQ28NYDA',
 56:                          '+TyIz4EyFC8IBkEcJEJxHJWLBhEX+CZBNEsiWL8WDZrMa6YQSz',
 57:                          'Vmmra79uzz29vd3u2k5bkbifh3vO709zv9/bc869hMjIyMjIyM',
 58:                          'jI/A9oMjPjuCE5U8xIWlJNULAmqaokcUr/XLd8/468jJgJH8B9',
 59:                          'IJcbdkHMIi6jr/8F2xktbRpclSbZjCQ2XfGa9rqvz4ixAZ4pbg',
 60:                          '9ucmNhoxcLPtBxJiHaHvQ9duEJfcCDqkNwCcyH4oAH3Ve276uy',
 61:                          'wzb3b/g4iPKfrhRfVI8y33QrOkeQyT8hUiGqSlMAN/NxDNhDJ+',
 62:                          'Pq8DVVujvNaHaajWkRRjxxZuirsVdC6Qkc5a63sD58H5N7r3Xw',
 63:                          'PibZUcySiW3YL9ltsNKFZzVEFDGWoYmsRLtvP4uUlqCeEHUnZo',
 64:                          'XtQ1H3Scd8nEWzkN6NFqnudAvbbdb0CCIfNdhGVF+EnStSqunF',
 65:                          'WmUl6kTNIXxsxyrC+1A6sE5IJ0ydqpDoPiccG8YIIobO6RpHiB',
 66:                          'FVEkqLYHuGlyn+5hA+chyVhPnIAnTBHHvpEvSYI4gYO3GHuy6A',
 67:                          'fexgpRmfgMtJRERQH+oX3ck+HwbYaE6h5pH6P5yCHmcEEaMFBd',
 68:                          '7bvUXpIKWldvTgkYrEj9eE5aMMS4nPxxa8ojkDu+N8if5o/h/z',
 69:                          '8CNVx3EazwcqPYhvuZp2VJAleBCOj0Vuulx5HwXoo8nF7724JH',
 70:                          '341/m5CCKe80IOswOVTu93cQfVtG8o2YfyMHwkmztG+X3kAOP9',
 71:                          'tfeSPtKt7M78KfSHEWX0d7S3Uvq4Bx+g9DCqvcPSflcbVofhIx',
 72:                          '/v6KvegaZGA1H9whqhNEp6XUXx/VEME5tthiUxQGkNDtFxK9Cj',
 73:                          'DsuHH+7IrURrvK+0YQgfJK2iy9lVkRZhxNEgvGm1TmwMUHoWtX',
 74:                          'RUmWAWlkgQHwolTwe0Su50mmjFAVbRfBzKR7TIhkcwVYunAUoX',
 75:                          'wlPEDQm34UJjAmsK+X3l2x+E7AAu0J8tfIP+GPs4iYfCPA/QE7',
 76:                          'HSKqDh1D0bbmd14kYcnxuGD7LeBkdD+dVWj2PTxRj7+Cz6BlT3',
 77:                          '4jgJULrL5AFMR+JJtoWWyPB8EP3dr9xecTfPIoUx9hGKMXNCfl',
 78:                          'wEJyt/XmJ0pMjIyMjIyMj8I/wGYmW4NA=='])
 79:    else:
 80:      limg = PackedImage('L',
 81:                       (200, 40),
 82:                       ['eJztlltIFFEYx896v6SSKCaJ2uZDD22UUVtBJBWZaS9BSgnWQy',
 83:                        'ShvSgk9hLdTMl8ENIVozCSCsTqKUW6qFCiYQaZ2oOrq4iUtKm4',
 84:                        'OW7MvzOzM7Ozl3bE8RI0v4fzne+cb2f+/z1nzgwhGhoaGhoaGh',
 85:                        'r/AxF6vZ8jyAjlpwLjdM4ij1kZgenlbSPTmBlpK08PXEXtchqB',
 86:                        'TBryISedjuhbFjBXE80Xec7KSKyakk1OVSWtugfK+l9zeEbj4a',
 87:                        'ccoxjg4zZCwobRdf87Ov2Jt1knEZUMXGEqo1bfRyGujtvjxOwR',
 88:                        'bord02gmJGgEu521slmJvWZOetfltHg/4hd/oPQdl5r3LeLOib',
 89:                        'WjjKU2SWUm8pHV30aJF6WXUETbauT49HF2gcpuNshGDE3ckuQp',
 90:                        '2kif4Rdv9qiqTCQVHWQ7hrwoTUMrbd9ji08fGQymjrmPTYFxH/',
 91:                        'MgeVrYhbPJKjKJGpwjpB/7PZUGD+MEuYEnsmJv+yq7V0/bgMz6',
 92:                        'PiusffVZATTT92Yr2SAm6XEyqchEQn7aIgkpxUMvSg+y1la0r1',
 93:                        'PwQeihrcszS1c3n9HxY0pYpF9YVGQiuXhM22TWFuWpNHYQqAqS',
 94:                        '39urD3rkvYSclmhlF4TMS/WMbumZyFtkcKETFzyUnrSyX/CC1g',
 95:                        'b4K/goohe1XjeGkhDjtW+0X7wYH8u6HimY5EXm44O70vPsxJ6Q',
 96:                        'LpSRGLQp+KAP2YMYoRtuQqPOa5Ebzn1epyITKANr5/gN7HBVus',
 97:                        'k2t5mQDeM4dQh3lXz4H5clmUFea9xJnhHkOE6hJWbC/SfQ080z',
 98:                        'LoiVlBY7joOdNlsrPbYUfHBsbRhjxhoMf533YBnfH1noFnpH8C',
 99:                        'PERek9XORjDjAp/cE+fGTZ+avbsxZvJNFkYSymJJUZx3MUCD26',
100:                        'MrkuSitQ4+i8xlfpa8mHj9B23kdH2OJ9LBtxdkY6Iu/gDRckpb',
101:                        'swb+RiBebxSvwU97WvInuojZ41+EAkpARNUj8VbAqRK62DvaGw',
102:                        'egADiZ/QIIz58kFiP6M/dqWk+mQQst3cj1vERWnBEAtMV0eShA',
103:                        'lccQz59EE2tiasjE61hBsNwWutQUNDQ0NDQ+Pf4g+RSY1o'])
104:  
105:    rimg = PackedImage('L',
106:                       (62, 40),
107:                       ['eJxjYBgFo2CIgH3/iQUHMDXv/B9DpDWR/3diiP33J9pu//+Yup',
108:                        'mI1s30n2no6v7/35oi3f8HUjfTUHW54xBNLRi6KcpjlOVvysqW',
109:                        'UTAK6AMAzJb0gw=='])
110:  
111:    # Calculate various dimensions based on the size of the screenshot.
112:    width = screenshot.size[0]
113:    lbox = (0, 0, limg.size[0], limg.size[1])
114:    rbox = (width - rimg.size[0], 0, width, rimg.size[1])
115:    
116:    # Decide whether the overlay text and graphics should be black or white.
117:    # The pixel at (width-13, 21) is in the button of the battery.
118:    p = screenshot.getpixel((width-13, 21))[:3]
119:    if sum(p) > 3*250:
120:      symbolcolor = 'white'
121:    else:
122:      symbolcolor = 'black'
123:  
124:    # Create the masks.
125:    lmask = limg.unpack()
126:    rmask = rimg.unpack()
127:  
128:    # Make the overlays.
129:    left = Image.new('RGBA', limg.size, symbolcolor)
130:    left.putalpha(lmask)
131:    right = Image.new('RGBA', rimg.size, symbolcolor)
132:    right.putalpha(rmask)
133:  
134:    # Paste the overlays and return.
135:    screenshot.paste(left, lbox, left)
136:    screenshot.paste(right, rbox, right)
137:    return screenshot
138:  
139:  # And here we go.
140:  if __name__ == '__main__':
141:    import photos, console
142:    screenshot = photos.pick_image()
143:    console.clear()
144:    cleanbar(screenshot).show()

Everything above Line 139 is the library. It starts by defining the PackedImage class, which is how we store the mask images. The four mask images themselves are defined in

The first three of these is where you’d substitute your own data if you use a different cellular carrier.

The statusbar color in the original screenshot image is determined by looking at the button of the battery icon. A nice spot within the button is 13 pixels in from the right edge and 21 pixels down from the top. This is what Lines 118–122 do.

The overlay images are constructed in Lines 129–132, using the color chosen in Lines 118–122 and the masks extracted in Lines 125–126. These are then pasted onto the top of the original screenshot in Lines 135–136.

All of this is inside the definition of the cleanbar function, which comprises Lines 20–137. It takes the original screenshot as its first argument and the connectivity as its second, optional, argument. If only one argument is given, the connectivity is assumed to be WiFi.

When Cleanbar is run as a script, Lines 140–144 are executed. They

At this point, the user can copy the cleaned image, save it to the Camera Roll, or ignore it.

All of the other screenshot cleaning scripts import the cleanbar function. They are, as a result, much shorter. Here’s Cleanbar LTE:

python:
1:  from Cleanbar import cleanbar
2:  import photos, console
3:  
4:  screenshot = photos.pick_image()
5:  console.clear()
6:  cleanbar(screenshot, connection='lte').show()

And here’s the nearly identical Cleanbar 4G:

python:
1:  from Cleanbar import cleanbar
2:  import photos, console
3:  
4:  screenshot = photos.pick_image()
5:  console.clear()
6:  cleanbar(screenshot, connection='4g').show()

The last three lines of these scripts differ from the last three lines in Cleanbar only in the connection argument.

The Cleanbar Pair script is slightly more complicated, but only because it has to present the image picker twice and has to combine the two cleaned screenshots into a single image:

python:
 1:  import Image
 2:  from Cleanbar import cleanbar
 3:  import photos, speech, console
 4:  
 5:  speech.say('left image?', '', .18)
 6:  s1 = cleanbar(photos.pick_image())
 7:  speech.say('right image?', '', .18)
 8:  s2 = cleanbar(photos.pick_image())
 9:  
10:  w = s1.size[0] + s2.size[0] + 60
11:  h = max(s1.size[1], s2.size[1]) + 40
12:  ss = Image.new('RGB', (w,h), '#3C659C')
13:  ss.paste(s1, (20, (h - s1.size[1])/2))
14:  ss.paste(s2, (s1.size[0] + 40, (h - s2.size[1])/2))
15:  console.clear()
16:  ss.show()

This script literally asks the user, with the Siri voice, which images it wants for the left and right sides. I like that because it reminds me of where I am in the process. If you think it’s too cute, you can comment out Lines 5 and 7.

The Cleanbar LTE Pair script is the same except for the connection argument:

python:
 1:  import Image
 2:  from Cleanbar import cleanbar
 3:  import photos, speech, console
 4:  
 5:  speech.say('left image?', '', .18)
 6:  s1 = cleanbar(photos.pick_image(), connection='lte')
 7:  speech.say('right image?', '', .18)
 8:  s2 = cleanbar(photos.pick_image(), connection='lte')
 9:  
10:  w = s1.size[0] + s2.size[0] + 60
11:  h = max(s1.size[1], s2.size[1]) + 40
12:  ss = Image.new('RGB', (w,h), '#3C659C')
13:  ss.paste(s1, (20, (h - s1.size[1])/2))
14:  ss.paste(s2, (s1.size[0] + 40, (h - s2.size[1])/2))
15:  console.clear()
16:  ss.show()

And so is Cleanbar 4G Pair:

python:
 1:  import Image
 2:  from Cleanbar import cleanbar
 3:  import photos, speech, console
 4:  
 5:  speech.say('left image?', '', .18)
 6:  s1 = cleanbar(photos.pick_image(), connection='4g')
 7:  speech.say('right image?', '', .18)
 8:  s2 = cleanbar(photos.pick_image(), connection='4g')
 9:  
10:  w = s1.size[0] + s2.size[0] + 60
11:  h = max(s1.size[1], s2.size[1]) + 40
12:  ss = Image.new('RGB', (w,h), '#3C659C')
13:  ss.paste(s1, (20, (h - s1.size[1])/2))
14:  ss.paste(s2, (s1.size[0] + 40, (h - s2.size[1])/2))
15:  console.clear()
16:  ss.show()

You might think the cleaning and the pairing should be separated, and I wouldn’t necessarily disagree. But I did it this way because I can’t imagine needing to combine two images with different connectivity, and I didn’t want my Camera Roll cluttered with intermediate images—screenshots that have been cleaned but not yet paired together.

Without the Launch Center Pro actions, these scripts would be a pain in the ass to run, because I’d have to scroll through my list of Pythonista scripts to find the right one. With LCP, though, the process of generating clean screenshots is very smooth.


  1. This is true, at least, for all Retina iOS devices other than the 6 Plus. I have a feeling the statusbar on the 6 Plus is taller because of that funny downscaling it does. But since I don’t have a 6 Plus, I’m not sure. The scripts are written to accomodate at taller statusbar, but they won’t work as-is. ↩︎

  2. I’m sure pack-image could be rewritten to work directly on iOS, but I just couldn’t be arsed to do that. I brought the images over to my Mac to crop them, so it seemed most natural to run pack-image there. ↩︎