February 17, 2015 at 1:33 AM by Dr. Drang
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:
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:
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).
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:
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,
the signal strength for WiFi,
the signal strength for LTE,
and the signal strength for 4G,
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
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) 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,
and it returns something like this:
PackedImage('L', (62, 40), ['eJxjYBgFo2CIgH3/iQUHMDXv/B9DpDWR/3diiP33J9pu//+Yup', 'mI1s30n2no6v7/35oi3f8HUjfTUHW54xBNLRi6KcpjlOVvysqW', 'UTAK6AMAzJb0gw=='])
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).
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
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 113: lbox = (0, 0, limg.size, limg.size) 114: rbox = (width - rimg.size, 0, width, rimg.size) 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
- Lines 32–51 for the LTE signal mask.
- Lines 53–78 for the 4G signal mask.
- Lines 80–103 for the WiFi signal mask.
- Lines 105–109 for the battery mask.
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.
Cleanbar is run as a script, Lines 140–144 are executed. They
- bring up the photo picker, which allows the user to choose the image to be cleaned;
- clean the (WiFi) screenshot by calling
cleanbarwith one argument; and
- present the result to the user in the Pythonista console.
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
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
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
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 + s2.size + 60 11: h = max(s1.size, s2.size) + 40 12: ss = Image.new('RGB', (w,h), '#3C659C') 13: ss.paste(s1, (20, (h - s1.size)/2)) 14: ss.paste(s2, (s1.size + 40, (h - s2.size)/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.
Cleanbar LTE Pair script is the same except for the
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 + s2.size + 60 11: h = max(s1.size, s2.size) + 40 12: ss = Image.new('RGB', (w,h), '#3C659C') 13: ss.paste(s1, (20, (h - s1.size)/2)) 14: ss.paste(s2, (s1.size + 40, (h - s2.size)/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 + s2.size + 60 11: h = max(s1.size, s2.size) + 40 12: ss = Image.new('RGB', (w,h), '#3C659C') 13: ss.paste(s1, (20, (h - s1.size)/2)) 14: ss.paste(s2, (s1.size + 40, (h - s2.size)/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.
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. ↩︎
pack-imagecould 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