├── .gitignore ├── bin └── cordova-imaging.js ├── LICENSE ├── package.json ├── config.js ├── README.md └── imaging.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /bin/cordova-imaging.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../imaging').cmd(); 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mike MacMillan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-imaging", 3 | "description": "Configuration driven imaging module used by Cordova apps to generate app icons, splash screens, app store previews, etc.", 4 | "version": "0.0.6", 5 | "homepage": "https://github.com/mmacmillan/cordova-imaging", 6 | "author": { 7 | "name": "Mike MacMillan", 8 | "email": "mikejmacmillan@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/mmacmillan/cordova-imaging.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/mmacmillan/cordova-imaging/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/mmacmillan/cordova-imaging/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "engines": { 24 | "node": ">= 0.8.0" 25 | }, 26 | "dependencies": { 27 | "lodash": "2.4.x", 28 | "q": "1.1.x", 29 | "xml2js": "0.4.x", 30 | "imagemagick": "0.1.x", 31 | "gm": "1.17.x", 32 | "mkdirp": "0.5.x", 33 | "phantom": "0.7.x" 34 | }, 35 | "keywords": [ 36 | "cordova", 37 | "imaging", 38 | "app icons", 39 | "splash screen", 40 | "splashscreen", 41 | "generate splash screen", 42 | "generate app icon", 43 | "graphicsmagick" 44 | ], 45 | "directories": { 46 | "bin": "./bin" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * provides application configuration and definitions for the image assets generated, for use 3 | * in the mobile application and app store. 4 | * 5 | */ 6 | 7 | module.exports = { 8 | //** the name of the cordova project config xml file 9 | configXml: 'config.xml', 10 | 11 | //** assets like previews, etc, are output here, and source assets are read from here 12 | assetPath: 'assets/', 13 | 14 | //** appicon and splashscreen source paths. by default they are in an ./assets subfolder in the root of your 15 | //** cordova project. these can be located anywhere on disk; override in the config local to your project. 16 | sources: { 17 | appicon: 'assets/appicon.png', 18 | splashscreen: 'assets/splashscreen.png' 19 | }, 20 | 21 | //** default platforms we target imaging for are ios and android 22 | platforms: ['ios', 'android'], 23 | 24 | //** default config for imagemagick 25 | imagemagick: { 26 | resize: { format: 'png', quality: 1.0 }, 27 | crop: { format: 'png', quality: 1.0, gravity: 'Center' } 28 | }, 29 | 30 | 31 | 32 | //** platform specific configurations 33 | //** ---- 34 | 35 | ios: { 36 | name: 'iOS', 37 | 38 | //** path to cordova iOS project 39 | path: 'platforms/ios', 40 | 41 | //** path where assets are output for mobile app 42 | destinationPath: 'platforms/ios/$name$/Resources/', 43 | 44 | //** by default, generate icons, splashscreens, and previews 45 | generateIcons: true, 46 | generateSplashscreens: true, 47 | generatePreviews: true, 48 | 49 | //** supported icons, source: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html 50 | icons: [ 51 | //** non-retina 52 | { size: 40, output: 'icons/icon-40.png' }, 53 | { size: 50, output: 'icons/icon-50.png' }, 54 | { size: 60, output: 'icons/icon-60.png' }, 55 | { size: 72, output: 'icons/icon-72.png' }, 56 | { size: 76, output: 'icons/icon-76.png' }, 57 | { size: 29, output: 'icons/icon-small.png' }, 58 | { size: 57, output: 'icons/icon.png' }, 59 | 60 | //** retina 61 | { size: 58, output: 'icons/icon-small@2x.png' }, 62 | { size: 80, output: 'icons/icon-40@2x.png' }, 63 | { size: 100, output: 'icons/icon-50@2x.png' }, 64 | { size: 120, output: 'icons/icon-60@2x.png' }, 65 | { size: 180, output: 'icons/icon-60@3x.png' }, //** iphone 6+ 66 | { size: 144, output: 'icons/icon-72@2x.png' }, 67 | { size: 152, output: 'icons/icon-76@2x.png' }, 68 | { size: 114, output: 'icons/icon@2x.png' } 69 | ], 70 | 71 | //** define the app store app icon; this will be generated along with the normal app icons; no alphas or transparencies, hence the jpg 72 | appstoreIcon: { size: 1024, output: 'appstore-icon.jpg' }, 73 | 74 | //** supported splashscreens, source: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html#//apple_ref/doc/uid/TP40006556-CH27-SW2 75 | splashscreens: [ 76 | //** portrait 77 | { width : 640, height : 1136, output: 'splash/Default-568h@2x~iphone.png' }, 78 | { width : 750, height : 1334, output: 'splash/Default-667h.png' }, 79 | { width : 1242, height : 2208, output: 'splash/Default-736h.png' }, 80 | { width : 1536, height : 2208, output: 'splash/Default-Portrait@2x~ipad.png' }, 81 | { width : 768, height : 2048, output: 'splash/Default-Portrait~ipad.png' }, 82 | { width : 640, height : 960, output: 'splash/Default@2x~iphone.png' }, 83 | { width : 320, height : 480, output: 'splash/Default~iphone.png' }, 84 | 85 | //** landscape 86 | { width : 2208, height : 1242, output: 'splash/Default-Landscape-736h.png' }, 87 | { width : 2048, height : 1536, output: 'splash/Default-Landscape@2x~ipad.png' }, 88 | { width : 1024, height : 768, output: 'splash/Default-Landscape~ipad.png' } 89 | ], 90 | 91 | //** https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnect_Guide/Appendices/Properties.html#//apple_ref/doc/uid/TP40011225-CH26-SW2 92 | //** note: we render jpgs because the app store wont accept images with transparencies or alpha channels 93 | previews: [ 94 | //** 3.5 inch retina displays 95 | { width : 640, height : 920, type: '3-5inch', output: '$file$-port.jpg' }, 96 | { width : 640, height : 960, type: '3-5inch', output: '$file$-port-full.jpg' }, 97 | { width : 960, height : 600, type: '3-5inch', output: '$file$-land.jpg' }, 98 | { width : 960, height : 640, type: '3-5inch', output: '$file$-land-full.jpg' }, 99 | 100 | //** 4 inch retina displays 101 | { width : 640, height : 1096, type: '4inch', output: '$file$-port.jpg' }, 102 | { width : 640, height : 1136, type: '4inch', output: '$file$-port-full.jpg' }, 103 | { width : 1136, height : 600, type: '4inch', output: '$file$-land.jpg' }, 104 | { width : 1136, height : 640, type: '4inch', output: '$file$-land-full.jpg' }, 105 | 106 | //** 4.7 inch retina displays (iphone6) 107 | { width : 750, height : 1334, type: '4-7inch', output: '$file$-port.jpg' }, 108 | { width : 1334, height : 750, type: '4-7inch', output: '$file$-land.jpg' }, 109 | 110 | //** 5.5 inch retina displays (iphone6 plus) 111 | { width : 1242, height : 2208, type: '5-5inch', output: '$file$-port.jpg' }, 112 | { width : 2208, height : 1242, type: '5-5inch', output: '$file$-land.jpg' }, 113 | 114 | //** ipad 115 | { width : 768, height : 1004, type: 'ipad', output: '$file$-port.jpg' }, 116 | { width : 768, height : 1024, type: 'ipad', output: '$file$-port-full.jpg' }, 117 | { width : 1024, height : 748, type: 'ipad', output: '$file$-land.jpg' }, 118 | { width : 1024, height : 768, type: 'ipad', output: '$file$-land-full.jpg' }, 119 | 120 | //** ipad retina 121 | { width : 1536, height : 2008, type: 'ipad-retina', output: '$file$-port.jpg' }, 122 | { width : 1536, height : 2048, type: 'ipad-retina', output: '$file$-port-full.jpg' }, 123 | { width : 2048, height : 1496, type: 'ipad-retina', output: '$file$-land.jpg' }, 124 | { width : 2048, height : 1536, type: 'ipad-retina', output: '$file$-land-full.jpg' } 125 | 126 | ] 127 | }, 128 | 129 | android: { 130 | name: 'Android', 131 | 132 | //** path to cordova android project 133 | path: 'platforms/android/', 134 | 135 | //** path where assets are output for mobile app 136 | destinationPath: 'platforms/android/res/', 137 | 138 | //** by default, generate icons, splashscreens, and previews 139 | generateIcons: true, 140 | generateSplashscreens: true, 141 | generatePreviews: true, 142 | 143 | //** supported icons, source: http://developer.android.com/design/style/iconography.html 144 | //** note: ldpi support is automatically provided by android 145 | icons: [ 146 | { size: 96, output: 'drawable/icon.png' }, 147 | { size: 48, output: 'drawable-mdpi/icon.png' }, 148 | { size: 72, output: 'drawable-hdpi/icon.png' }, 149 | { size: 96, output: 'drawable-xhdpi/icon.png' } 150 | 151 | //** cordova doesn't create these folders (yet), and imagemagick wont create folders as it writes paths...commenting out for now 152 | //{ size: 144, output: 'drawable-xxhdpi/icon.png' }, 153 | //{ size: 192, output: 'drawable-xxxhdpi/icon.png' } 154 | ], 155 | 156 | //** supported splashscreens, source: http://developer.android.com/guide/practices/screens_support.html 157 | splashscreens: [ 158 | //** landscape 159 | { width: 320, height: 200, output: 'drawable-land-ldpi/screen.png' }, 160 | { width: 480, height: 320, output: 'drawable-land-mdpi/screen.png' }, 161 | { width: 800, height: 480, output: 'drawable-land-hdpi/screen.png' }, 162 | { width: 1280, height: 720, output: 'drawable-land-xhdpi/screen.png' }, 163 | 164 | //** portrait 165 | { width: 200, height: 320, output: 'drawable-port-ldpi/screen.png' }, 166 | { width: 320, height: 480, output: 'drawable-port-mdpi/screen.png' }, 167 | { width: 480, height: 800, output: 'drawable-port-hdpi/screen.png' }, 168 | { width: 720, height: 1280, output: 'drawable-port-xhdpi/screen.png' } 169 | ] 170 | } 171 | 172 | }; 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Cordova-Imaging 2 | 3 | >A configuration driven command line imaging utility for Cordova Mobile apps to generate app icons, splash screens, and app store previews for iOS and Android 4 | 5 | [Apache Cordova](https://cordova.apache.org/) is an amazing framework for building mobile apps that target many platforms and form factors, but that support comes with the need to provide a version of your app icon and splash screen for each device form factor you choose to support. Additionally, when you submit your app to the app store, you will need to upload a version of each screen shot for each device form factor. 6 | 7 | **Cordova-Imaging** is a Node.js command line utility, driven by an extensible configuration file, that generates these images with [GraphicsMagick](http://www.graphicsmagick.org/), using the [gm](http://aheckmann.github.io/gm/) npm module, based on a single source app icon, splash screen, and preview image(s). 8 | 9 | Currently, only iOS and Android are supported. 10 | 11 | 12 | ###Installing 13 | 14 | If you have not already done so, install [GraphicsMagick](http://www.graphicsmagick.org/). Technically, [ImageMagick](http://www.imagemagick.org/script/index.php) may also be used, as the gm module [supports both](https://github.com/aheckmann/gm#use-imagemagick-instead-of-gm), but I haven't tested it with this library extensively. 15 | 16 | If you are on OS X: 17 | 18 | brew install graphicsmagick 19 | 20 | Once installed, install the **Cordova-Imaging** module (globally): 21 | 22 | npm install cordova-imaging -g 23 | 24 | 25 | 26 | 27 | ###Running 28 | 29 | To use the default configuration, from within your project's root directory, ensure you have an *./assets* directory containing your app icon and splash screen images. By default, their paths are assumed to be: 30 | 31 | - ./assets/appicon.png 32 | - ./assets/splashscreen.png 33 | 34 | To generate the images, from your project's directory, run the following command: 35 | ```shell 36 | cordova-imaging 37 | ``` 38 | 39 | The app icons and splashscreens generated will be automatically added to the mobile projects of the platforms you support; iOS or Android. This means you will need to have already added support for that platform to your project, such as: 40 | ```shell 41 | cordova platform add android 42 | ``` 43 | 44 | To view the new app icon and splash screen, just rebuild and relaunch your app using the command line/IDE. Preview images are output by default to their respective *./assets/previews/\/\
* directory. 45 | 46 | 47 | 48 | ###Custom Configuration 49 | 50 | You may provide a custom configuration file, called **./imaging.json**, in your projects root folder, to override default behavior, provide paths to preview images for generation, configure which platforms are supported, configure icon and splash screen form factors supported, etc. Here are a few example configurations: 51 | 52 | ####Configure a new location for the source app icon and splash screen 53 | 54 | ```javascript 55 | { 56 | "sources": { 57 | "appicon": "assets/otherappicon.png", 58 | "splashscreen": "assets/othersplashscreen.png" 59 | } 60 | } 61 | ``` 62 | 63 | ####Add preview images to generate for each form factor 64 | 65 | 66 | ```javascript 67 | { 68 | "sources": { 69 | "previews": [ 70 | "assets/source/preview1.png", 71 | "assets/source/preview2.png", 72 | "assets/source/preview3.png" 73 | ] 74 | } 75 | } 76 | ``` 77 | 78 | ####Support only generating assets for iOS 79 | ```javascript 80 | { 81 | "platforms": ['iOS'] 82 | } 83 | ``` 84 | 85 | ####Only generate preview images, no app icons or splash screens 86 | ```javascript 87 | { 88 | "sources": { 89 | "previews": [ 90 | "assets/source/preview1.png", 91 | "assets/source/preview2.png", 92 | "assets/source/preview3.png" 93 | ] 94 | }, 95 | 96 | "ios": { 97 | "generateIcons": false, 98 | "generateSplashscreens": false 99 | }, 100 | 101 | "android": { 102 | "generateIcons": false, 103 | "generateSplashscreens": false 104 | } 105 | 106 | } 107 | ``` 108 | 109 | ####Generate all images except previews for android 110 | ```javascript 111 | { 112 | "sources": { 113 | "previews": [ 114 | "assets/source/preview1.png", 115 | "assets/source/preview2.png", 116 | "assets/source/preview3.png" 117 | ] 118 | }, 119 | 120 | "android": { 121 | "generatePreviews": false 122 | } 123 | 124 | } 125 | ``` 126 | 127 | ####Override the background color for the generated splash screen 128 | 129 | ```javascript 130 | { 131 | "sources": { 132 | "splashscreen": { 133 | "path": "assets/splashscreen.png", 134 | "background": "#fff" 135 | } 136 | } 137 | } 138 | ``` 139 | 140 | The above is useful for when you need to alter the background color used when generating a splash screen, if the image contains any transparencies, without modifying the source image itself. Since transparencies and alpha channels aren't allowed, if not specified, a background color of black will be used by default. 141 | 142 | ###Full Configuration 143 | 144 | The default configuration file is located within your npm_modules folder where **Cordova-Imaging** is installed, located at *./config.js*. The *imaging.json* configuration you provide will be extended over this default config, allowing you to fully alter the base configuration used when generating images. This can be useful, for example, if your app only supports a subset of form factors, and you dont want to generate images you dont need. Here is the full JSON configuration, with comments inline: 145 | 146 | ```javascript 147 | { 148 | //** the name of the cordova project config xml file 149 | configXml: 'config.xml', 150 | 151 | //** assets like previews, etc, are output here, and source assets are read from here 152 | assetPath: 'assets/', 153 | 154 | //** appicon and splashscreen source paths. by default they are in an ./assets subfolder in the root of your 155 | //** cordova project. these can be located anywhere on disk; override in the config local to your project. 156 | sources: { 157 | appicon: 'assets/appicon.png', 158 | splashscreen: 'assets/splashscreen.png' 159 | }, 160 | 161 | //** default platforms we target imaging for are ios and android 162 | platforms: ['ios', 'android'], 163 | 164 | //** default config for imagemagick 165 | imagemagick: { 166 | resize: { format: 'png', quality: 1.0 }, 167 | crop: { format: 'png', quality: 1.0, gravity: 'Center' } 168 | }, 169 | 170 | 171 | 172 | //** platform specific configurations 173 | //** ---- 174 | 175 | ios: { 176 | name: 'iOS', 177 | 178 | //** path to cordova iOS project 179 | path: 'platforms/ios', 180 | 181 | //** path where assets are output for mobile app 182 | destinationPath: 'platforms/ios/$name$/Resources/', 183 | 184 | //** by default, generate icons, splashscreens, and previews 185 | generateIcons: true, 186 | generateSplashscreens: true, 187 | generatePreviews: true, 188 | 189 | //** supported icons, source: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html 190 | icons: [ 191 | //** non-retina 192 | { size: 40, output: 'icons/icon-40.png' }, 193 | { size: 50, output: 'icons/icon-50.png' }, 194 | { size: 60, output: 'icons/icon-60.png' }, 195 | { size: 72, output: 'icons/icon-72.png' }, 196 | { size: 76, output: 'icons/icon-76.png' }, 197 | { size: 29, output: 'icons/icon-small.png' }, 198 | { size: 57, output: 'icons/icon.png' }, 199 | 200 | //** retina 201 | { size: 58, output: 'icons/icon-small@2x.png' }, 202 | { size: 80, output: 'icons/icon-40@2x.png' }, 203 | { size: 100, output: 'icons/icon-50@2x.png' }, 204 | { size: 120, output: 'icons/icon-60@2x.png' }, 205 | { size: 180, output: 'icons/icon-60@3x.png' }, //** iphone 6+ 206 | { size: 144, output: 'icons/icon-72@2x.png' }, 207 | { size: 152, output: 'icons/icon-76@2x.png' }, 208 | { size: 114, output: 'icons/icon@2x.png' } 209 | ], 210 | 211 | //** define the app store app icon; this will be generated along with the normal app icons; no alphas or transparencies, hence the jpg 212 | appstoreIcon: { size: 1024, output: 'appstore-icon.jpg' }, 213 | 214 | //** supported splashscreens, source: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html#//apple_ref/doc/uid/TP40006556-CH27-SW2 215 | splashscreens: [ 216 | //** portrait 217 | { width : 640, height : 1136, output: 'splash/Default-568h@2x~iphone.png' }, 218 | { width : 750, height : 1334, output: 'splash/Default-667h.png' }, 219 | { width : 1242, height : 2208, output: 'splash/Default-736h.png' }, 220 | { width : 1536, height : 2208, output: 'splash/Default-Portrait@2x~ipad.png' }, 221 | { width : 768, height : 2048, output: 'splash/Default-Portrait~ipad.png' }, 222 | { width : 640, height : 960, output: 'splash/Default@2x~iphone.png' }, 223 | { width : 320, height : 480, output: 'splash/Default~iphone.png' }, 224 | 225 | //** landscape 226 | { width : 2208, height : 1242, output: 'splash/Default-Landscape-736h.png' }, 227 | { width : 2048, height : 1536, output: 'splash/Default-Landscape@2x~ipad.png' }, 228 | { width : 1024, height : 768, output: 'splash/Default-Landscape~ipad.png' } 229 | ], 230 | 231 | //** https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnect_Guide/Appendices/Properties.html#//apple_ref/doc/uid/TP40011225-CH26-SW2 232 | //** note: we render jpgs because the app store wont accept images with transparencies or alpha channels 233 | previews: [ 234 | //** 3.5 inch retina displays 235 | { width : 640, height : 920, type: '3-5inch', output: '$file$-port.jpg' }, 236 | { width : 640, height : 960, type: '3-5inch', output: '$file$-port-full.jpg' }, 237 | { width : 960, height : 600, type: '3-5inch', output: '$file$-land.jpg' }, 238 | { width : 960, height : 640, type: '3-5inch', output: '$file$-land-full.jpg' }, 239 | 240 | //** 4 inch retina displays 241 | { width : 640, height : 1096, type: '4inch', output: '$file$-port.jpg' }, 242 | { width : 640, height : 1136, type: '4inch', output: '$file$-port-full.jpg' }, 243 | { width : 1136, height : 600, type: '4inch', output: '$file$-land.jpg' }, 244 | { width : 1136, height : 640, type: '4inch', output: '$file$-land-full.jpg' }, 245 | 246 | //** 4.7 inch retina displays (iphone6) 247 | { width : 750, height : 1334, type: '4-7inch', output: '$file$-port.jpg' }, 248 | { width : 1334, height : 750, type: '4-7inch', output: '$file$-land.jpg' }, 249 | 250 | //** 5.5 inch retina displays (iphone6 plus) 251 | { width : 1242, height : 2208, type: '5-5inch', output: '$file$-port.jpg' }, 252 | { width : 2208, height : 1242, type: '5-5inch', output: '$file$-land.jpg' }, 253 | 254 | //** ipad 255 | { width : 768, height : 1004, type: 'ipad', output: '$file$-port.jpg' }, 256 | { width : 768, height : 1024, type: 'ipad', output: '$file$-port-full.jpg' }, 257 | { width : 1024, height : 748, type: 'ipad', output: '$file$-land.jpg' }, 258 | { width : 1024, height : 768, type: 'ipad', output: '$file$-land-full.jpg' }, 259 | 260 | //** ipad retina 261 | { width : 1536, height : 2008, type: 'ipad-retina', output: '$file$-port.jpg' }, 262 | { width : 1536, height : 2048, type: 'ipad-retina', output: '$file$-port-full.jpg' }, 263 | { width : 2048, height : 1496, type: 'ipad-retina', output: '$file$-land.jpg' }, 264 | { width : 2048, height : 1536, type: 'ipad-retina', output: '$file$-land-full.jpg' } 265 | 266 | ] 267 | }, 268 | 269 | android: { 270 | name: 'Android', 271 | 272 | //** path to cordova android project 273 | path: 'platforms/android/', 274 | 275 | //** path where assets are output for mobile app 276 | destinationPath: 'platforms/android/res/', 277 | 278 | //** by default, generate icons, splashscreens, and previews 279 | generateIcons: true, 280 | generateSplashscreens: true, 281 | generatePreviews: true, 282 | 283 | //** supported icons, source: http://developer.android.com/design/style/iconography.html 284 | //** note: ldpi support is automatically provided by android 285 | icons: [ 286 | { size: 96, output: 'drawable/icon.png' }, 287 | { size: 48, output: 'drawable-mdpi/icon.png' }, 288 | { size: 72, output: 'drawable-hdpi/icon.png' }, 289 | { size: 96, output: 'drawable-xhdpi/icon.png' } 290 | 291 | //** cordova doesn't create these folders (yet), and imagemagick wont create folders as it writes paths...commenting out for now 292 | //{ size: 144, output: 'drawable-xxhdpi/icon.png' }, 293 | //{ size: 192, output: 'drawable-xxxhdpi/icon.png' } 294 | ], 295 | 296 | //** supported splashscreens, source: http://developer.android.com/guide/practices/screens_support.html 297 | splashscreens: [ 298 | //** landscape 299 | { width: 320, height: 200, output: 'drawable-land-ldpi/screen.png' }, 300 | { width: 480, height: 320, output: 'drawable-land-mdpi/screen.png' }, 301 | { width: 800, height: 480, output: 'drawable-land-hdpi/screen.png' }, 302 | { width: 1280, height: 720, output: 'drawable-land-xhdpi/screen.png' }, 303 | 304 | //** portrait 305 | { width: 200, height: 320, output: 'drawable-port-ldpi/screen.png' }, 306 | { width: 320, height: 480, output: 'drawable-port-mdpi/screen.png' }, 307 | { width: 480, height: 800, output: 'drawable-port-hdpi/screen.png' }, 308 | { width: 720, height: 1280, output: 'drawable-port-xhdpi/screen.png' } 309 | ] 310 | } 311 | } 312 | ``` 313 | 314 | -------------------------------------------------------------------------------- /imaging.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | pth = require('path'), 3 | Q = require('q'), 4 | _ = require('lodash'), 5 | xmlParser = require('xml2js').parseString, 6 | magick = require('imagemagick'), 7 | gm = require('gm'), 8 | mkdirp = require('mkdirp'), 9 | phantomjs = require('phantomjs'), 10 | defaultConfig = require('./config'); 11 | 12 | //** load the optional app specific imaging config 13 | var config = _.extend({}, defaultConfig); 14 | try { 15 | var cfg = require(process.cwd() + '/imaging.json'); 16 | _.merge(config, cfg); 17 | } 18 | catch(e) {} 19 | 20 | var handleError = function(err) { console.log('\nan unexpected error occurred:', err, '\n') }; 21 | 22 | var imaging = { 23 | project: null, 24 | targetPlatforms: [], 25 | 26 | //** runs and generates all-the-things, based on the configuration 27 | run: function() { 28 | var def = Q.defer(); 29 | 30 | imaging.verifyEnvironment() 31 | .then(imaging.verifySources) 32 | .then(imaging.loadProject) 33 | .then(imaging.generateIcons) 34 | .then(imaging.generateSplashscreens) 35 | .then(imaging.generatePreviews) 36 | .then(function() { 37 | console.log('imaging complete!'); 38 | }) 39 | .catch(handleError); 40 | }, 41 | 42 | //** this is used by the binary for the cli 43 | cmd: function() { 44 | imaging.run(); 45 | }, 46 | 47 | util: { 48 | checkPlatform: function(platform) { 49 | var def = Q.defer(), 50 | cfg = config[platform]; 51 | 52 | if(!cfg) def.resolve(); 53 | 54 | cfg && fs.exists(cfg.path, function(exists) { 55 | console.log(' ', platform, '...', exists?'found':'not found'); 56 | if(exists) imaging.targetPlatforms.push(cfg); 57 | def.resolve(); 58 | }); 59 | 60 | return def.promise; 61 | }, 62 | 63 | checkPath: function(path) { 64 | var def = Q.defer(), 65 | //** allows for the path to be an object { path: '' } or string 66 | path = typeof(path) === 'string' ? path : path.path; 67 | 68 | fs.exists(path, function(exists) { 69 | !!exists 70 | ? def.resolve() 71 | : def.reject('could not find the path: ', path); 72 | }); 73 | 74 | return def.promise; 75 | }, 76 | 77 | ensurePath: function(path) { 78 | var def = Q.defer(); 79 | 80 | //** checks a path if its exists, creating it if it doesn't (at any depth ala mkdirp) 81 | imaging.util.checkPath(path).then(def.resolve, mkdirp.bind(this, path, function(err) { 82 | !!err && def.reject() || def.resolve(); 83 | })); 84 | 85 | return def.promise; 86 | }, 87 | 88 | ensurePathSync: function(path) { 89 | if(!fs.existsSync(path)) mkdirp.sync(path) 90 | } 91 | }, 92 | 93 | verifyEnvironment: function() { 94 | var def = Q.defer(), 95 | sources = config.sources, 96 | platforms = []; 97 | 98 | //** determine which platforms are supported based on whats defined in the config, and what is on disk 99 | console.log('verifying platforms'); 100 | return Q.all(_.map(config.platforms||[], imaging.util.checkPlatform)); 101 | }, 102 | 103 | verifySources: function() { 104 | var def = Q.defer(), 105 | p = def.promise, 106 | util = imaging.util; 107 | 108 | console.log('\nverifying sources'); 109 | 110 | //** make sure we've defined a few platforms 111 | if(imaging.targetPlatforms.length == 0) 112 | def.reject('none of the target platforms were found; have you added platforms to Cordova yet?'); 113 | else { 114 | //** see what we need to generate 115 | var needsIcon = _.find(imaging.targetPlatforms, function(cfg) { return cfg.generateIcons; }), 116 | needsSplash = _.find(imaging.targetPlatforms, function(cfg) { return cfg.generateSplashscreens; }), 117 | actions = []; 118 | 119 | //** verify the asset path exists 120 | util.ensurePath(config.assetPath); 121 | 122 | //** if we need to generate app icons, verify the source has been defined and is valid 123 | if(needsIcon) { 124 | console.log(' appicon source ...', !!config.sources.appicon?'found':'not found'); 125 | !config.sources.appicon 126 | ? def.reject('at least one platform is generating icons, and an appicon source hasn\'t been defined') 127 | : actions.push(util.checkPath(config.sources.appicon)); 128 | } 129 | 130 | //** same thing for splashscreens 131 | if(needsSplash) { 132 | console.log(' splashscreen source ...', !!config.sources.splashscreen?'found':'not found'); 133 | !config.sources.splashscreen 134 | ? def.reject('at least one platform is generating splashscreens, and a splashscreen source hasn\'t been defined') 135 | : actions.push(util.checkPath(config.sources.splashscreen)); 136 | } 137 | 138 | Q.all(actions).then(def.resolve, def.reject.bind(def, 'the sources could not be verified; check the paths to your sources.')); 139 | } 140 | 141 | return p; 142 | }, 143 | 144 | loadProject: function() { 145 | var def = Q.defer(); 146 | 147 | console.log('\nloading project'); 148 | 149 | //** load the project's config.xml and extract the project's xml 150 | fs.readFile(config.configXml, function(err, xml) { 151 | if(err) return def.reject('the config.xml file could not be located; are you sure this is a Cordova project?'); 152 | 153 | xmlParser(xml, function(err, data) { 154 | if(err) return def.reject('there was a problem reading', config.configXml); 155 | 156 | //** grab a few of the project details 157 | imaging.project = { 158 | id: data.widget['$'].id, 159 | version: data.widget['$'].version, 160 | name: data.widget.name[0] 161 | }; 162 | 163 | console.log(' ', imaging.project.name, imaging.project.version); 164 | def.resolve(); 165 | }); 166 | }); 167 | 168 | return def.promise; 169 | }, 170 | 171 | 172 | 173 | //** appicon generation methods 174 | //** ---- 175 | 176 | generateIcons: function() { 177 | var queue = []; 178 | 179 | //** initiate icon generation for each platform that has it enabled 180 | _.each(imaging.targetPlatforms, function(cfg) { 181 | if(!cfg.generateIcons || !cfg.icons) 182 | return Q.resolve(); 183 | 184 | console.log('\ngenerating icons for', cfg.name); 185 | 186 | var dest = cfg.destinationPath.replace('$name$', imaging.project.name), 187 | fn = imaging.generateIcon.bind(this, dest), 188 | actions = _.map(cfg.icons, fn); 189 | 190 | //** add one more task to generate the app store app icon at the asset path 191 | !!cfg.appstoreIcon && actions.push(imaging.generateIcon(config.assetPath, cfg.appstoreIcon)); 192 | 193 | queue.push(Q.all(actions)); 194 | }); 195 | 196 | return Q.all(queue); 197 | }, 198 | 199 | generateIcon: function(path, icon) { 200 | 201 | //** generate the options for this icon, based on the root config 202 | var def = Q.defer(), 203 | iconPath = config.sources.appicon, 204 | opt = _.extend({}, config.imagemagick.resize, { 205 | srcPath: iconPath[0]=='/' ? iconPath : pth.join(process.cwd(), iconPath), 206 | dstPath: pth.join(process.cwd(), path, icon.output), 207 | height: icon.size, 208 | width: icon.size 209 | }); 210 | 211 | 212 | console.log(' ', icon.size, 'x', icon.size, icon.output); 213 | 214 | //** its possible for some directories to not be created; ex latest version of cordova android 4.* doesn't create res/drawable/ 215 | var dstDir = pth.dirname(opt.dstPath); 216 | fs.exists(dstDir, function(exists) { 217 | !!exists 218 | ? doResize() 219 | : fs.mkdir(dstDir, function(err) { !!err ? def.reject(err) : doResize() }); 220 | }); 221 | 222 | function doResize() { 223 | magick.resize(opt, function(err) { 224 | //** imagemagick wont create directories (yet), so a failure in creating images usually means directories are missing... 225 | !!err ? def.reject(err) : def.resolve(); 226 | }); 227 | } 228 | 229 | return def.promise; 230 | }, 231 | 232 | 233 | 234 | //** splashscreen generation methods 235 | //** ---- 236 | 237 | generateSplashscreens: function() { 238 | var queue = []; 239 | 240 | //** initiate splashscreen generation for each platform that has it enabled 241 | _.each(imaging.targetPlatforms, function(cfg) { 242 | if(!cfg.generateSplashscreens || !cfg.splashscreens) 243 | return Q.resolve(); 244 | 245 | console.log('\ngenerating splashscreens for', cfg.name); 246 | 247 | var dest = cfg.destinationPath.replace('$name$', imaging.project.name), 248 | fn = imaging.generateSplashscreen.bind(this, dest); 249 | 250 | queue.push(Q.all(_.map(cfg.splashscreens, fn))); 251 | }); 252 | 253 | return Q.all(queue); 254 | }, 255 | 256 | 257 | generateSplashscreen: function(path, splash) { 258 | //** generate the options for this splashscreen, based on the root config 259 | var def = Q.defer(), 260 | splashCfg = config.sources.splashscreen; 261 | 262 | if(typeof(splashCfg) === 'string') 263 | splashCfg = { path: splashCfg }; 264 | 265 | var srcPath = splashCfg.path[0]=='/' ? splashCfg.path : pth.join(process.cwd(), splashCfg.path), 266 | dstPath = pth.join(process.cwd(), path, splash.output); 267 | 268 | console.log(' ', splash.width, 'x', splash.height, splash.output); 269 | 270 | gm(srcPath) 271 | .gravity('Center') 272 | .resize(splash.width) //** resize/scale the image to the desired output width 273 | .extent(splash.width, splash.height) //** sets the destination image size; overflow will be cropped 274 | .background(splashCfg.background || 'black') 275 | .quality(100) 276 | .noProfile() //** no exif, smaller image 277 | .write(dstPath, function(err) { 278 | !err && def.resolve() || def.reject(); 279 | }); 280 | 281 | return def.promise; 282 | }, 283 | 284 | 285 | //** preview generation methods 286 | //** ---- 287 | 288 | generatePreviews: function() { 289 | var queue = [], 290 | sources = config.sources.previews; 291 | 292 | if(!sources || !Array.isArray(sources)) 293 | return Q.resolve(); 294 | 295 | //** initiate preview generation for each platform that has it enabled 296 | _.each(imaging.targetPlatforms, function(cfg) { 297 | if(!cfg.generatePreviews || !cfg.previews) 298 | return Q.resolve(); 299 | 300 | console.log('\ngenerating previews for', cfg.name); 301 | var path = pth.join(config.assetPath, 'previews', cfg.name), 302 | fn = imaging.generatePreview.bind(this, path, cfg); 303 | 304 | queue.push(Q.all(_.map(sources, fn))); 305 | }); 306 | 307 | return Q.all(queue); 308 | }, 309 | 310 | 311 | generatePreview: function(path, cfg, source) { 312 | console.log(' ', source); 313 | 314 | //** generate the options for this splashscreen, based on the root config 315 | var queue = [], 316 | srcPath = source[0]=='/' ? source : pth.join(process.cwd(), source); 317 | 318 | _.each(cfg.previews, function(preview) { 319 | 320 | /* 321 | * GraphicsMagick implementation 322 | * - gm provides a bit easier interface to guarentee the destination image size, so i prefer it over imagemagick 323 | */ 324 | 325 | //** determine the path to the previews of the specific type/platform 326 | var previewRoot = pth.join(process.cwd(), path, preview.type), 327 | filename = preview.output.replace('$file$', pth.basename(source, pth.extname(source))), 328 | previewPath = pth.join(previewRoot, filename), 329 | opt = _.extend({}, config.imagemagick.crop, { 330 | srcPath: srcPath, 331 | dstPath: previewPath 332 | }); 333 | 334 | var def = Q.defer(); 335 | queue.push(def.promise); 336 | 337 | //** ensure the destination path, then generate the preview images based on the configuration 338 | imaging.util.ensurePath(previewRoot).then(function() { 339 | console.log(' ', preview.width, 'x', preview.height, preview.type, filename); 340 | 341 | gm(srcPath) 342 | .resize(preview.width) //** resize/scale the image to the desired output width 343 | .extent(preview.width, preview.height) //** sets the destination image size; overflow will be cropped 344 | .quality(100) 345 | .noProfile() //** no exif, smaller image 346 | .write(previewPath, function(err) { 347 | !err && def.resolve() || def.reject(); 348 | }); 349 | }, handleError); 350 | 351 | 352 | /* 353 | * ImageMagick implementation 354 | * 355 | //** determine the path to the previews of the specific type/platform 356 | var previewRoot = pth.join(process.cwd(), path, preview.type), 357 | filename = preview.output.replace('$file$', pth.basename(source, pth.extname(source))), 358 | previewPath = pth.join(previewRoot, filename), 359 | opt = _.extend({}, config.imagemagick.crop, { 360 | srcPath: srcPath, 361 | dstPath: previewPath 362 | }); 363 | 364 | console.log(' ', preview.width, 'x', preview.height, preview.type, filename); 365 | imaging.util.ensurePathSync(previewRoot); 366 | 367 | //** if we provide both height and width, we get a square and its fubars the crop; give it the largest dimension only. this will 368 | //** scale the image, however, which will effect the final dimensions of the image. 369 | preview.width > preview.height 370 | ? (opt.width = preview.width) 371 | : (opt.height = preview.height); 372 | 373 | //** specify the gravity and crop dimensions for our target preview 374 | opt.customArgs = [ 375 | '-gravity', 376 | 'Center', 377 | '-crop', 378 | preview.width +'x'+ preview.height +'+0+0', 379 | '+repage' 380 | ]; 381 | 382 | var def = Q.defer(); 383 | queue.push(def.promise); 384 | magick.resize(opt, function(err) { 385 | !!err ? def.reject(err) : def.resolve(); 386 | }); 387 | */ 388 | }); 389 | 390 | return Q.all(queue); 391 | } 392 | 393 | 394 | } 395 | 396 | module.exports = imaging; 397 | --------------------------------------------------------------------------------