#!/usr/bin/env python #------------------------------------------------------------------------------- # # iPhone 2.0.0 artwork extractor # (c)2008 Dave Peck All Rights Reserved # http://davepeck.org/software/iphoneart/ # # This software is free for you to use and modify, for any purpose. # If you do so, please be sure to mention my name and email address. # If you're adding the ability to handle newer artwork files, or different ones, # send me a patch and I'll update this file! # # To use, you must have python and the python imaging libraries (PIL) installed. # # Then run it as # ./iphone20-artwork.py -export ./exportDirectory # [or] # ./iphone20-artwork -import ./importDirectory # # This software is only capable of exporting images from [and importing images to] # 2.0.0 artwork files named Keyboard-Latin.artwork, Keyboard-Common.artwork, # Other.artwork, and MobilePhonePackedImages.artwork. Everything else will fail. # I may eventually support the remaining TextInput_* .artwork files. # #------------------------------------------------------------------------------- # Version History # # v0.6 7/28/2008 - support other image formats besides RGBA. Fix a filename-related bug (used relative name instead of absolute.) # v0.5 7/25/2008 - add support for MobilePhone images, and clean up usage messages. # v0.4 7/24/2008 - add feature support for -import so that you can make artwork files. # v0.3 7/19/2008 - use os.path to manipulate paths so that things work nicely on windows with standard-style paths # v0.2 7/18/2008 - change command line structure to use -export (preparing for other things like -import) # v0.1 7/13/2008 - released initial version, with export support for all 2.0.0 UIKit artwork import sys import os.path as path import struct import PIL.Image KeyboardLatinArtwork = 'Keyboard-Latin.artwork' KeyboardCommonArtwork = 'Keyboard-Common.artwork' OtherArtwork = 'Other.artwork' MobilePhonePackedImagesArtwork = 'MobilePhonePackedImages.artwork' KeyboardLatin_2_0_0_Size = 27957248 KeyboardCommon_2_0_0_Size = 1914880 Other_2_0_0_Size = 4868544 MobilePhonePackedImages_2_0_0_Size = 1422592 # # These image sizes were acquired by trial and error. # # If you'd like to update this software to support newer (or older) artwork files, # read on and I'll explain how I did it. # # I started by guessing the sizes and offsets of the first two images in each artwork file, # and kept exporting the first two images until they looked exactly right. Then I figured # that the widths and heights should be stored in another file. As it turns out, they appear # in the compiled UIKit binary. Each width and height is stored as a uint16_t in the binary, # and after each width/height pair are four unknown bytes. So, I determined the offset into # UIKit that contained the start of the image sizes and wrote a little code to keep pulling # out values from the binary until the number of bytes I _would_ read from the .artwork file # is exactly the number of bytes actually found in the file! # # Offsets into UIKit, as compiled for the x86 simulator, for 2.0.0: # # KeyboardLatin_2_0_0_Offset = 0x6022C4 # KeyboardCommon_2_0_0_Offset = 0x602864 # Other_2_0_0_Offset = 0x601324 # # The image sizes for MobilePhonePackedImages were determined by a third party and I just # added them back to the app. So I'm not sure what the offset is. Should be easy to determine, # however. # # One last trick was figuring out memory alignments. These appear to be the same across all # .artwork files, so there should be no need to guess on this anymore. (Again, it was simply # guesswork until the images exported well.) # KeyboardLatin_2_0_0_ImageSizes = [(30, 42), (30, 42), (85, 130), (95, 130), (114, 130), (114, 130), (85, 130), (95, 130), (140, 130), (27, 81), (82, 130), (1, 81), (91, 130), (94, 130), (27, 81), (82, 130), (104, 130), (320, 216), (83, 120), (91, 120), (88, 120), (79, 120), (114, 125), (79, 126), (78, 126), (220, 500), (132, 750), (132, 750), (176, 500), (132, 500), (176, 500), (132, 500), (480, 162), (480, 162), (480, 162), (480, 162), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (42, 43), (42, 43), (320, 216), (320, 216), (320, 216), (320, 216), (240, 43), (43, 43), (43, 43), (37, 43), (37, 43), (1, 41), (1, 37), (1, 19), (480, 180), (103, 120), (115, 120), (126, 120), (99, 120), (110, 126), (85, 126), (85, 126), (480, 162), (66, 38), (66, 38), (66, 38), (480, 162), (480, 162), (480, 38), (382, 38), (52, 38), (52, 38), (47, 38), (47, 38), (480, 162), (480, 162), (480, 162), (480, 162), (480, 162), (480, 162), (480, 162), (480, 162), (66, 38), (66, 38), (76, 45), (66, 38), (76, 45), (99, 38), (480, 162), (480, 162), (66, 38), (99, 38), (99, 38), (480, 162), (99, 38), (480, 162), (480, 162), (52, 38), (52, 38), (47, 38), (47, 38), (480, 162), (480, 162), (480, 162), (480, 162), (480, 162), (480, 162), (480, 162), (99, 38), (480, 162), (480, 162), (66, 38), (381, 8), (99, 38), (480, 162), (480, 162), (480, 162), (480, 162), (480, 162), (480, 38), (382, 38), (480, 162), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (107, 43), (80, 43), (160, 43), (320, 216), (320, 216), (320, 216), (320, 216), (42, 43), (42, 43), (42, 43), (40, 41), (40, 41), (42, 43), (40, 41), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (4, 36), (4, 36), (80, 43), (80, 44), (320, 216), (80, 43), (43, 43), (43, 43), (37, 43), (37, 43), (83, 9), (80, 43), (246, 9), (80, 43), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (320, 216), (480, 180), (320, 216), (320, 216)] KeyboardCommon_2_0_0_ImageSizes = [(148, 148), (148, 148), (11, 9), (11, 9), (11, 9), (8, 8), (16, 31), (16, 31), (320, 48), (1, 56), (2, 56), (2, 56), (1, 56), (1, 56), (320, 200), (21, 21), (80, 44), (80, 44), (80, 44), (80, 44), (80, 44), (80, 44), (11, 30), (11, 30), (98, 38), (98, 38), (98, 38), (98, 38), (96, 34), (96, 34), (96, 34), (98, 38), (98, 38), (283, 38), (283, 38), (283, 38), (95, 38), (95, 38), (95, 38), (95, 38), (283, 38), (283, 38), (80, 44), (80, 44), (80, 43), (80, 43), (80, 44), (80, 28), (80, 28), (80, 28), (80, 44), (80, 44), (160, 44), (160, 44), (160, 44), (53, 44), (53, 44), (53, 44), (53, 44), (160, 44), (160, 44), (127, 127), (127, 127), (127, 127), (20, 20), (320, 216)] Other_2_0_0_ImageSizes = [(59, 60), (59, 60), (59, 60), (16, 20), (16, 20), (320, 480), (320, 480), (1, 222), (6, 12), (6, 12), (6, 12), (6, 12), (11, 11), (11, 9), (9, 18), (9, 18), (9, 18), (9, 18), (11, 11), (11, 9), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (14, 15), (20, 20), (14, 15), (20, 20), (37, 37), (14, 15), (29, 29), (29, 29), (29, 29), (1, 22), (27, 46), (27, 46), (27, 46), (25, 46), (25, 46), (25, 46), (25, 46), (1, 44), (27, 46), (27, 46), (25, 46), (25, 46), (25, 46), (280, 2), (3, 31), (25, 25), (25, 25), (22, 17), (21, 18), (17, 14), (16, 19), (13, 13), (16, 19), (13, 13), (21, 18), (17, 14), (22, 23), (27, 28), (27, 28), (1, 44), (1, 49), (11, 24), (24, 16), (20, 16), (24, 18), (20, 15), (22, 18), (29, 29), (29, 29), (1, 44), (1, 32), (20, 20), (21, 19), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (31, 36), (18, 19), (18, 19), (19, 19), (17, 17), (11, 24), (11, 24), (29, 16), (33, 16), (18, 18), (15, 15), (13, 13), (23, 18), (19, 19), (34, 30), (28, 24), (34, 30), (28, 24), (34, 30), (28, 24), (34, 30), (34, 30), (28, 24), (28, 24), (14, 16), (15, 18), (16, 17), (100, 100), (23, 18), (18, 22), (25, 20), (21, 19), (20, 20), (7, 7), (17, 17), (16, 21), (18, 19), (18, 19), (1, 44), (1, 57), (41, 70), (17, 57), (17, 57), (41, 70), (3, 19), (1, 44), (1, 96), (13, 16), (23, 19), (21, 30), (11, 30), (21, 30), (1, 44), (11, 30), (1, 74), (21, 30), (1, 44), (11, 30), (1, 74), (21, 30), (21, 30), (1, 44), (11, 30), (11, 30), (1, 74), (11, 30), (11, 30), (15, 24), (15, 24), (1, 32), (9, 24), (9, 24), (15, 24), (15, 24), (1, 32), (9, 24), (9, 24), (15, 24), (15, 24), (1, 32), (9, 24), (9, 24), (9, 24), (11, 30), (11, 30), (6, 6), (6, 6), (62, 52), (11, 53), (15, 39), (15, 39), (18, 18), (1, 180), (1, 216), (15, 180), (15, 216), (15, 216), (1, 180), (1, 216), (15, 180), (15, 216), (15, 216), (9, 44), (1, 48), (1, 62), (1, 180), (1, 216), (9, 180), (9, 216), (7, 1), (19, 46), (19, 46), (11, 14), (284, 62), (11, 43), (11, 43), (11, 43), (14, 14), (13, 44), (13, 44), (14, 14), (15, 11), (14, 14), (29, 29), (13, 3), (29, 29), (17, 33), (17, 33), (8, 31), (8, 31), (8, 31), (1, 31), (1, 31), (1, 31), (8, 31), (8, 31), (8, 31), (5, 5), (7, 7), (5, 5), (54, 54), (1, 30), (5, 9), (5, 9), (1, 9), (1, 8), (4, 8), (4, 8), (17, 18), (22, 24), (5, 9), (5, 9), (1, 9), (40, 29), (40, 29), (35, 31), (35, 31), (15, 15), (17, 31), (17, 31), (17, 31), (1, 31), (1, 31), (1, 31), (17, 31), (17, 31), (17, 31), (1, 23), (1, 23), (8, 10), (13, 30), (1, 30), (6, 30), (6, 30), (1, 30), (1, 30), (21, 44), (1, 44), (10, 44), (10, 44), (1, 44), (6, 44), (6, 44), (21, 44), (1, 44), (10, 44), (10, 44), (1, 44), (6, 44), (6, 44), (46, 46), (9, 46), (9, 46), (46, 46), (46, 46), (9, 46), (9, 46), (9, 46), (9, 46), (1, 46), (1, 46), (1, 46), (1, 46), (11, 9), (5, 9), (1, 9), (23, 23), (23, 23), (11, 9), (5, 9), (1, 9), (320, 128), (320, 460), (17, 27), (17, 27), (17, 27), (17, 27), (14, 16), (2, 16), (17, 27), (17, 27), (5, 27), (11, 27), (11, 27), (11, 27), (48, 32), (36, 39), (36, 39), (28, 20), (45, 36), (45, 36), (32, 29), (33, 39), (33, 39), (29, 29), (35, 39), (35, 39), (29, 28), (40, 39), (40, 39), (38, 25), (45, 36), (45, 36), (25, 25), (29, 16), (33, 16), (29, 7), (31, 39), (31, 39), (23, 23), (37, 39), (37, 39), (35, 21), (30, 39), (30, 39), (24, 24), (3, 19), (3, 19), (19, 17), (10, 13), (29, 31), (29, 31), (10, 13), (1, 43), (1, 43), (1, 43), (1, 43), (1, 43), (6, 6), (10, 10), (19, 19), (19, 19), (302, 46), (1, 17), (1, 14), (302, 46), (1, 216), (15, 216), (1, 216), (15, 216), (9, 44), (1, 48), (1, 62), (1, 216), (9, 216), (19, 30), (19, 30), (19, 30), (1, 44), (11, 30), (11, 30), (11, 30), (21, 51), (21, 51), (21, 51), (21, 51), (15, 24), (15, 24), (15, 24), (1, 32), (9, 24), (9, 24), (9, 24), (1, 32), (1, 30), (1, 30), (11, 30), (11, 30), (11, 30), (11, 30), (11, 30), (11, 30), (1, 44), (1, 30), (13, 13), (24, 26)] MobilePhonePackedImages_2_0_0_ImageSizes = [(70, 68), (40, 47), (70, 68), (40, 47), (34, 45), (70, 67), (96, 107), (91, 107), (97, 107), (96, 115), (91, 115), (97, 115), (96, 107), (91, 107), (97, 107), (96, 115), (91, 115), (97, 115), (45, 36), (45, 36), (45, 35), (45, 35), (45, 35), (45, 35), (45, 36), (45, 36), (45, 36), (45, 36), (8, 74), (320, 273), (320, 273)] # # If you're supporting a new file, add a key/value pair for it. # If you're supported a new file version, add a new tuple in the appropriate value's list. # SupportedFiles = { KeyboardLatinArtwork:[(KeyboardLatin_2_0_0_Size, KeyboardLatin_2_0_0_ImageSizes, 'keyboard-latin-')], KeyboardCommonArtwork:[(KeyboardCommon_2_0_0_Size, KeyboardCommon_2_0_0_ImageSizes, 'keyboard-common-')], OtherArtwork:[(Other_2_0_0_Size, Other_2_0_0_ImageSizes, 'other-')], MobilePhonePackedImagesArtwork:[(MobilePhonePackedImages_2_0_0_Size, MobilePhonePackedImages_2_0_0_ImageSizes, 'mobile-phone-packed-images-')] } def usage_basic(): print "\nTo convert an artwork file into a bunch of PNG files, run like this:" print " %s -export file.artwork ./exportDirectoryName" % sys.argv[0] print "\nTo convert a bunch of PNG files into an artwork file, run like this:" print " %s -import original.artwork\n ./importDirectoryName created.artwork" % sys.argv[0] print "\n NOTE: Even though you are 'creating' a new artwork file with -import, " print " you must still specify the original artwork file on the command line." print " This is because the 2.0 artwork files have a bunch of extra stuff that" print " isn't images at the end -- and you've got to keep those bits around." print sys.exit(-1) def usage_filename(): print "\nThis application can only export and import the following files:" for k, vs in zip(SupportedFiles.keys(), SupportedFiles.values()): for v in vs: expectedSize, imageSizes, exportPrefix = v print "\t%s of size %u" % (k, expectedSize) print sys.exit(-1) def usage_imagecountmismatch(imageFileName, expectedCount): print "\nIMPORT FAILED." print "\nWhen importing, you must have the same number of images as the original artwork" print "file. But we couldn't find the image named %s.\nThere should be %d total images." % (imageFileName, expectedCount) print sys.exit(-1) def usage_imagesizemismatch(imageFileName, expectedSize, actualSize): print "\nIMPORT FAILED." print "\nWhen importing, your images must have the same size as the original artwork" print "file. Otherwise, things will break! The file %s should have\nbeen %dx%d but was actually %dx%d." % (imageFileName, expectedSize[0], expectedSize[1], actualSize[0], actualSize[1]) print sys.exit(-1) def usage_imageformat(imageFileName): print "\nIMPORT FAILED." print "\nThe artwork tool does not support the format of the image file\nnamed %s. Please try creating the image with a\ndifferent piece of software." % imageFileName print sys.exit(-1) WidthByteAlignment = 8 ImageByteAlignment = 4096 def align_bytes(byteLocation, alignmentAmount): alignedByteLocation = byteLocation remainder = alignedByteLocation % alignmentAmount if remainder != 0: alignedByteLocation += (alignmentAmount - remainder) return alignedByteLocation def export_images(data, imageSizes, exportDirectory, exportPrefix): imageBase = 0 imageIndex = 0 for width, height in imageSizes: exportedImage = PIL.Image.new("RGBA", (width, height)) exportedPixels = exportedImage.load() memoryWidth = align_bytes(width, WidthByteAlignment) for y in range(height): for x in range(width): pixelPosition = imageBase + (4 * ((y * memoryWidth) + x)) b, g, r, a = struct.unpack_from('." usage_basic() print "\twriting joined images to file..." outputFile = open(outputArtworkFileFullName, 'wb') outputFile.write(finalData) outputFile.close() def action_export(): artworkFileFullName = sys.argv[2].strip() exportDirectory = sys.argv[3].strip() artworkFileName = path.basename(artworkFileFullName) if not artworkFileName in SupportedFiles: usage_filename() exportInfos = SupportedFiles[artworkFileName] # For now, just let this throw an exception if they entered a bad file name file = open(artworkFileFullName, 'rb') data = file.read() file.close() expectedFileSize = 0 imageSizes = None exportPrefix = None found = False for exportInfo in exportInfos: expectedFileSize, imageSizes, exportPrefix = exportInfo if len(data) == expectedFileSize: found = True break if not found: usage_filename() print "\nExporting images from %s to directory %s..." % (artworkFileName, exportDirectory) export_images(data, imageSizes, exportDirectory, exportPrefix) print "Finished exporting from %s" % artworkFileName print def action_create(): originalArtworkFileFullName = sys.argv[2].strip() importDirectory = sys.argv[3].strip() outputArtworkFileFullName = sys.argv[4].strip() originalArtworkFileName = path.basename(originalArtworkFileFullName) outputArtworkFileName = path.basename(outputArtworkFileFullName) if not originalArtworkFileName in SupportedFiles: usage_filename() importInfos = SupportedFiles[originalArtworkFileName] # For now, just let this throw an exception if they entered a bad file name originalFile = open(originalArtworkFileFullName, 'rb') originalData = originalFile.read() originalFile.close() expectedFileSize = 0 imageSizes = None importPrefix = None found = False for importInfo in importInfos: expectedFileSize, imageSizes, importPrefix = importInfo if len(originalData) == expectedFileSize: found = True break if not found: usage_filename() print "\nImporting images to %s from directory %s..." % (outputArtworkFileName, importDirectory) import_images(originalData, imageSizes, importDirectory, importPrefix, outputArtworkFileFullName) print "Finished importing to %s" % outputArtworkFileName print pass def action_guess(): print "\nSorry, this is not yet implemented -- I wrote it in another script and haven't" print "yet moved it over. I will move it over and use it to crack the remaining iPhone" print "artwork files sometime in the future (if a lot of people want it.)" print pass if __name__ == "__main__": if len(sys.argv) < 4 or len(sys.argv) > 5: usage_basic() action = sys.argv[1].strip() if action == "-export": if len(sys.argv) != 4: usage_basic() action_export() elif action == "-import": if len(sys.argv) != 5: usage_basic() action_create() elif action == "-guess": # UNDOCUMENTED feature to help you guess image sizes in new files -- useful # if you're trying to crack a new .artwork file or one that isn't currently # supported by this application. action_guess() else: usage_basic()