├── MANIFEST.in ├── appframer ├── __init__.py ├── devices │ ├── iphone5.png │ └── iphone6plus.png ├── fonts │ ├── ufonts.com_arial-ce.ttf │ └── Champagne & Limousines Bold.ttf └── app.py ├── example ├── Banner.jpg ├── Profile.jpeg ├── Search.jpeg ├── Timeline.jpeg ├── Live Video.jpeg ├── Status Update.jpeg ├── stdout.txt └── screens.json ├── setup.py └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /appframer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/Banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/example/Banner.jpg -------------------------------------------------------------------------------- /example/Profile.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/example/Profile.jpeg -------------------------------------------------------------------------------- /example/Search.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/example/Search.jpeg -------------------------------------------------------------------------------- /example/Timeline.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/example/Timeline.jpeg -------------------------------------------------------------------------------- /example/Live Video.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/example/Live Video.jpeg -------------------------------------------------------------------------------- /example/Status Update.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/example/Status Update.jpeg -------------------------------------------------------------------------------- /appframer/devices/iphone5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/appframer/devices/iphone5.png -------------------------------------------------------------------------------- /appframer/devices/iphone6plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/appframer/devices/iphone6plus.png -------------------------------------------------------------------------------- /appframer/fonts/ufonts.com_arial-ce.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/appframer/fonts/ufonts.com_arial-ce.ttf -------------------------------------------------------------------------------- /appframer/fonts/Champagne & Limousines Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olucurious/AppFramer/HEAD/appframer/fonts/Champagne & Limousines Bold.ttf -------------------------------------------------------------------------------- /example/stdout.txt: -------------------------------------------------------------------------------- 1 | Now processing 'Live Video' screen for 3.5 inches 2 | Now processing 'Live Video' screen for 5.5 inches 3 | Now processing 'Live Video' screen for 4.7 inches 4 | Now processing 'Live Video' screen for 4 inches 5 | Done with 'Live Video' screen 6 | ------------------------------------------ 7 | Now processing 'Profile' screen for 3.5 inches 8 | Now processing 'Profile' screen for 5.5 inches 9 | Now processing 'Profile' screen for 4.7 inches 10 | Now processing 'Profile' screen for 4 inches 11 | Done with 'Profile' screen 12 | ------------------------------------------ 13 | Now processing 'Search' screen for 3.5 inches 14 | Now processing 'Search' screen for 5.5 inches 15 | Now processing 'Search' screen for 4.7 inches 16 | Now processing 'Search' screen for 4 inches 17 | Done with 'Search' screen 18 | ------------------------------------------ 19 | Now processing 'Status Update' screen for 3.5 inches 20 | Now processing 'Status Update' screen for 5.5 inches 21 | Now processing 'Status Update' screen for 4.7 inches 22 | Now processing 'Status Update' screen for 4 inches 23 | Done with 'Status Update' screen 24 | ------------------------------------------ 25 | Now processing 'Timeline' screen for 3.5 inches 26 | Now processing 'Timeline' screen for 5.5 inches 27 | Now processing 'Timeline' screen for 4.7 inches 28 | Now processing 'Timeline' screen for 4 inches 29 | Done with 'Timeline' screen 30 | ------------------------------------------ 31 | ------------------------------------------ 32 | Screenshots device frame processing complete... 33 | Get the output at /Users/olucurious/Documents/Fbscr/FramedAppScreens - 04-08-2016 AT 08.21/ -------------------------------------------------------------------------------- /example/screens.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_type": "iOS", 3 | "screens": [ 4 | { 5 | "file_path": "Live Video.jpeg", 6 | "title": "Live Video", 7 | "description": "Rain in Spain falls mainly in the plain", 8 | "title_color": "white", 9 | "description_color": "white", 10 | "background_color": "(3, 169, 244)" 11 | }, 12 | { 13 | "file_path": "Profile.jpeg", 14 | "title": "Profile", 15 | "description": "Rain in Spain falls mainly in the plain", 16 | "title_color": "white", 17 | "description_color": "white", 18 | "background_color": "(3, 169, 244)" 19 | }, 20 | { 21 | "file_path": "Search.jpeg", 22 | "title": "Search", 23 | "description": "Rain in Spain falls mainly in the plain", 24 | "title_color": "white", 25 | "description_color": "white", 26 | "background_color": "(3, 169, 244)" 27 | }, 28 | { 29 | "file_path": "Status Update.jpeg", 30 | "title": "Status Update", 31 | "description": "Rain in Spain falls mainly in the plain", 32 | "title_color": "white", 33 | "description_color": "white", 34 | "background_color": "(3, 169, 244)" 35 | }, 36 | { 37 | "file_path": "Timeline.jpeg", 38 | "title": "Timeline", 39 | "description": "Rain in Spain falls mainly in the plain", 40 | "title_color": "white", 41 | "description_color": "white", 42 | "background_color": "(3, 169, 244)" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import sys, os 3 | 4 | __url__ = 'https://github.com/olucurious/AppFramer' 5 | 6 | __version__ = '1.0.2' 7 | 8 | if sys.argv[-1] == 'publish': 9 | os.system("git tag -a %s -m 'v%s'" % (__version__, __version__)) 10 | os.system("python setup.py sdist bdist_wheel upload -r pypi") 11 | os.system("git push --tags") 12 | sys.exit() 13 | 14 | setup( 15 | name="appframer", 16 | version=__version__, 17 | description="AppFramer helps to put your app screenshots in beautiful device frames with annotations by running a simple command.", 18 | url=__url__, 19 | author="Emmanuel Adegbite", 20 | author_email="olucurious@gmail.com", 21 | license='MIT License', 22 | packages=["appframer"], 23 | entry_points=""" 24 | [console_scripts] 25 | appframer = appframer.app:main 26 | """, 27 | install_requires=[ 28 | 'Pillow', 29 | 'six>=1.10.0', 30 | ], 31 | package_data={'appframer': ['fonts/*', 'devices/*']}, 32 | include_package_data=True, 33 | zip_safe=False, 34 | keywords='ios, android, screenshot, app frame, screenshot framer, itunes assets, googleplay assets', 35 | classifiers=[ 36 | 'Development Status :: 5 - Production/Stable', 37 | 'Intended Audience :: Developers', 38 | 'Operating System :: OS Independent', 39 | 'Programming Language :: Python', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Topic :: Communications', 42 | 'Topic :: Internet', 43 | 'Topic :: Software Development :: Libraries :: Python Modules', 44 | 'Topic :: Utilities', 45 | 'Programming Language :: Python', 46 | 'Programming Language :: Python :: 2', 47 | 'Programming Language :: Python :: 2.6', 48 | 'Programming Language :: Python :: 2.7', 49 | 'Programming Language :: Python :: 3', 50 | 'Programming Language :: Python :: 3.2', 51 | 'Programming Language :: Python :: 3.3', 52 | 'Programming Language :: Python :: 3.4', 53 | 'Programming Language :: Python :: 3.5', 54 | ] 55 | ) 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppFramer 2 | AppFramer helps to put your app screenshots in beautiful device frames with annotations by running a simple command. 3 | Quickly put your screenshots into the various device frames needed for iTunesConnect submission. 4 | 5 | ![Banner of Sample Generated Device Frames](https://raw.githubusercontent.com/olucurious/appframer/develop/example/Banner.jpg) 6 | 7 | ## Installation 8 | ```sh 9 | $ pip install appframer 10 | 11 | OR 12 | 13 | pip install git+https://github.com/olucurious/AppFramer.git 14 | ``` 15 | #### Compatibility 16 | * Linux / OSX 17 | 18 | 19 | ## Usage 20 | * Go to the directory where you want the generate screen frames to be. 21 | * Run the following command in your terminal. 22 | ```sh 23 | $ appframer -i /path/to/screens.json 24 | ``` 25 | 26 | ##Options 27 | `appframer [-h] -i INPUT` 28 | 29 | -h --help show help message and exit 30 | -i --input path to data.json file that describes the screenshots to put in device frame 31 | 32 | ###Example 33 | ```sh 34 | $ appframer -i /path/to/screens.json 35 | ``` 36 | 37 | ###After 38 | ``` 39 | FramedAppScreens - 03-08-2016 AT 18.40 40 | Using splash.jpg, timeline.jpg and profile.jpg as examples of screenshots listed in screens.json 41 | The generated output folder will look like this: 42 | 43 | │   ├── 3.5 44 | │   │   └── splash.jpg 45 | │   │   ├── timeline.jpg 46 | | | ├── profile.jpg 47 | | | 48 | │   ├── 4 49 | │   │   └── splash.jpg 50 | │   │   ├── timeline.jpg 51 | | | ├── profile.jpg 52 | | | 53 | │   ├── 4.7 54 | │   │   └── splash.jpg 55 | │   │   ├── timeline.jpg 56 | | | ├── profile.jpg 57 | | | 58 | │   ├── 5.5 59 | │   │   └── splash.jpg 60 | │   │   ├── timeline.jpg 61 | | | ├── profile.jpg 62 | 63 | ``` 64 | 65 | ###screens.json example 66 | ``` 67 | Screens.json is obviously a json file format 68 | For iOS, take the screenshots you want to use on iPhone6s preferably because of the high resolution 69 | and list them in your screens.json and appframer will take those screenshots and generate 70 | the screenshots framed in different devices in the dimensions needed for iTunesConnect submission 71 | 72 | NOTE: In the screens.json example below, I'm assuming the screenshot images are in the same directory 73 | as the screens.json file itself and I'll be running the command from the same directory 74 | 75 | { 76 | "device_type": "iOS", 77 | "screens": [ 78 | { 79 | "file_path": "Live Video.jpeg", 80 | "title": "Live Video", 81 | "description": "Rain in Spain falls mainly in the plain", 82 | "title_color": "black", 83 | "description_color": "red" 84 | }, 85 | { 86 | "file_path": "Profile.jpeg", 87 | "title": "Profile", 88 | "description": "Rain in Spain falls mainly in the plain", 89 | "title_color": "white", 90 | "description_color": "black" 91 | }, 92 | { 93 | "file_path": "Search.jpeg", 94 | "title": "Search", 95 | "description": "Rain in Spain falls mainly in the plain", 96 | "title_color": "white", 97 | "description_color": "black" 98 | }, 99 | { 100 | "file_path": "Status Update.jpeg", 101 | "title": "Status Update", 102 | "description": "Rain in Spain falls mainly in the plain", 103 | "title_color": "black", 104 | "description_color": "black" 105 | }, 106 | { 107 | "file_path": "Timeline.jpeg", 108 | "title": "Timeline", 109 | "description": "Rain in Spain falls mainly in the plain", 110 | "title_color": "white", 111 | "description_color": "black" 112 | } 113 | ] 114 | } 115 | 116 | ``` 117 | ====== 118 | 119 | ## The MIT License 120 | > Copyright (c) 2016 Emmanuel Adegbite http://github.com/olucurious 121 | 122 | > Permission is hereby granted, free of charge, to any person obtaining a copy 123 | of this software and associated documentation files (the "Software"), to deal 124 | in the Software without restriction, including without limitation the rights 125 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 126 | copies of the Software, and to permit persons to whom the Software is 127 | furnished to do so, subject to the following conditions: 128 | 129 | > The above copyright notice and this permission notice shall be included in 130 | all copies or substantial portions of the Software. 131 | 132 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 133 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 134 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 135 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 136 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 137 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 138 | THE SOFTWARE. 139 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 140 | -------------------------------------------------------------------------------- /appframer/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'olucurious' 4 | 5 | import PIL 6 | from PIL import ImageFont 7 | from PIL import Image, ImageFilter 8 | from PIL import ImageDraw 9 | import textwrap 10 | import os 11 | import json 12 | import string 13 | import sys 14 | import argparse 15 | from time import strftime 16 | import inspect 17 | from six.moves import getcwd 18 | 19 | """ 20 | The following resolutions are acceptable to iTunes connect: 21 | 22 | iPhone 3+4 (3.5 Inch) 23 | 640 x 960 24 | 25 | iPhone 5 (4 Inch) 26 | 640 x 1136 27 | 28 | iPhone 6 (4.7 Inch) 29 | 750 x 1334 30 | 31 | iPhone 6 Plus (5.5 Inch) 32 | 1242 x 2208 33 | 34 | You need the screenshot in this resolution, the phone scales them down to 1080 x 1920 35 | iPad (Air and Mini Retina) 36 | 1536 x 2048 37 | 38 | Apple Watch 39 | 312 x 390 pixels 40 | 41 | (only one orientation) 42 | iPad Pro 43 | 2048 x 2732 44 | """ 45 | 46 | OUTPUT_DIR = getcwd() + '/FramedAppScreens - %s/' % strftime("%d-%m-%Y AT %H.%M") 47 | 48 | 49 | def get_script_dir(follow_symlinks=True): 50 | if getattr(sys, 'frozen', False): # py2exe, PyInstaller, cx_Freeze 51 | path = os.path.abspath(sys.executable) 52 | else: 53 | path = inspect.getabsfile(get_script_dir) 54 | if follow_symlinks: 55 | path = os.path.realpath(path) 56 | return os.path.dirname(path) 57 | 58 | 59 | class FrameScreenshots: 60 | def __init__(self, screenshot_location, title_text, desc_text, title_color=None, desc_color=None, bg_color=None): 61 | self.screenshot_location = screenshot_location 62 | self.title_text = title_text 63 | self.desc_text = desc_text 64 | # Opening the screen shot image file 65 | self.screenshot_image = Image.open(self.screenshot_location) 66 | self.title_color = (self.parse_color(title_color) if title_color else (255, 255, 255)) 67 | self.desc_color = (self.parse_color(desc_color) if desc_color else (255, 255, 255)) 68 | self.bg_color = (self.parse_color(bg_color) if bg_color else (3, 169, 244)) 69 | # Loading Fonts... 70 | self.title_font = ImageFont.truetype(get_script_dir() + "/fonts/Champagne & Limousines Bold.ttf", 120) 71 | self.desc_font = ImageFont.truetype(get_script_dir() + '/fonts/ufonts.com_arial-ce.ttf', 70) 72 | 73 | def parse_color(self, color_string): 74 | if '(' and ')' and ',' in color_string: 75 | color_list = color_string.strip('()').replace(' ', '').split(',') 76 | return int(color_list[0]), int(color_list[1]), int(color_list[2]) 77 | else: 78 | print(color_string) 79 | return color_string 80 | 81 | def generate(self): 82 | screen_sizes = dict() 83 | screen_sizes[3.5] = (640, 960) 84 | screen_sizes[4] = (640, 1136) 85 | screen_sizes[4.7] = (750, 1334) 86 | screen_sizes[5.5] = (1242, 2208) 87 | for iphone in screen_sizes: 88 | print("Now processing '%s' screen for %s inches" % (self.title_text, iphone)) 89 | self.process_iphone(iphone, screen_sizes[iphone][0], screen_sizes[iphone][1]) 90 | print("Done with '%s' screen" % self.title_text) 91 | print("------------------------------------------") 92 | 93 | def process_iphone(self, dim, width, height): 94 | background = Image.new('RGBA', size=(1536, 2726), color=self.bg_color) 95 | # Maybe we should consider a blur background some other time, a plain background will do for now 96 | # scrshot2 = self.screenshot_image.resize((1536, 2726), PIL.Image.ANTIALIAS) 97 | # background = scrshot2.filter(ImageFilter.GaussianBlur(radius=12)) 98 | self.set_text(background, self.desc_text, self.desc_font, 'desc') 99 | self.set_text(background, self.title_text, self.title_font, 'title') 100 | # ------------------ 101 | iphone_type = ('iphone6plus' if dim > 4 else 'iphone5') 102 | iphone_device = Image.open("%s/devices/%s.png" % (get_script_dir(), iphone_type)) 103 | background.paste(iphone_device, (0, 0), iphone_device) 104 | if dim <= 4: 105 | img2 = self.screenshot_image.resize((1135, 1800), PIL.Image.ANTIALIAS) 106 | background.paste(img2, (200, 920)) 107 | else: 108 | img2 = self.screenshot_image.resize((1147, 1906), PIL.Image.ANTIALIAS) 109 | background.paste(img2, (190, 820)) 110 | if not os.path.isdir(OUTPUT_DIR): 111 | os.mkdir(OUTPUT_DIR) 112 | destination_dir = OUTPUT_DIR + '/%s' % dim 113 | if not os.path.isdir(destination_dir): 114 | os.mkdir(destination_dir) 115 | background = background.resize((width, height), PIL.Image.ANTIALIAS) 116 | valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits) 117 | file_name = ''.join(c for c in self.title_text if c in valid_chars) 118 | background.convert('RGB').save("%s/%s.jpg" % (destination_dir, file_name)) 119 | 120 | def set_text(self, blurred_image, text, font, text_type): 121 | MAX_W, MAX_H = blurred_image.size 122 | para = textwrap.wrap(text, width=40) 123 | draw = ImageDraw.Draw(blurred_image) 124 | current_h, pad = (300 if text_type == 'desc' else 100), 10 125 | text_color = (self.desc_color if text_type == 'desc' else self.title_color) 126 | for line in para: 127 | w, h = draw.textsize(line, font=font) 128 | draw.text(((MAX_W - w) / 2, current_h), line, font=font, fill=text_color) 129 | current_h += h + pad 130 | 131 | 132 | def main(): 133 | desc = 'AppFramer helps to put your app screenshots in beautiful device frames with annotations by running a simple command.' 134 | parser = argparse.ArgumentParser(prog='appframer', description=desc) 135 | parser.add_argument('-i', '--input', type=str, help='Pass the input screens.json file location', required=True) 136 | # TODO - allow the user to specify output directory 137 | # parser.add_argument('-o', '--output', help='Directory to write the generated files', required=True) 138 | args = vars(parser.parse_args()) 139 | data = json.load(open(args['input'])) 140 | print("------------------------------------------") 141 | if 'screens' in data and isinstance(data['screens'], list): 142 | for screen in data['screens']: 143 | print(screen['background_color']) 144 | framedshot = FrameScreenshots(screen['file_path'], screen['title'], screen['description'], 145 | screen['title_color'], screen['description_color'], 146 | screen['background_color']) 147 | framedshot.generate() 148 | print("------------------------------------------") 149 | print("Screenshots device frame processing complete...") 150 | print("Get the output at %s" % OUTPUT_DIR) 151 | else: 152 | print("Invalid input format in %s json file" % args['input']) 153 | sys.exit() 154 | --------------------------------------------------------------------------------