Skip to content

Commit

Permalink
feat: added the 'init' command (#77)
Browse files Browse the repository at this point in the history
- added a native copyFile method... needed as this is not in Node 6
- note that node 6 is needed
- doc cleanup
- fix globbing pattern for specs
- move nyc config into package.json, and exclude test helpers
  • Loading branch information
dwmkerr authored Apr 19, 2019
1 parent 33b2b22 commit 3736f84
Show file tree
Hide file tree
Showing 22 changed files with 244 additions and 42 deletions.
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
- run: npm run cov
- run: bash <(curl -s https://codecov.io/bash)
# Store test artifacts, useful for diagnosing failed tests.
- store_artifacts:
path: "src/init/test-images"
- store_artifacts:
path: "src/label/test-images"
- store_artifacts:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ test/CordovaApp/platforms/android/res
test/NativeApp/ios/native_app/Assets.xcassets/AppIcon.appiconset
test/NativeApp/android/native_app/src/main/res
npm-debug.log
src/init/test-images/*-output.png
src/label/test-images/*-output.png
src/resize/test-images/*output.png
.nyc_output/

# Windows junk.
Thumbs.db
41 changes: 28 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# app-icon [![npm version](https://badge.fury.io/js/app-icon.svg)](https://badge.fury.io/js/app-icon) [![CircleCI](https://circleci.com/gh/dwmkerr/app-icon.svg?style=shield)](https://circleci.com/gh/dwmkerr/app-icon) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/3e334rknhjbpx555?svg=true)](https://ci.appveyor.com/project/dwmkerr/app-icon) [![codecov](https://codecov.io/gh/dwmkerr/app-icon/branch/master/graph/badge.svg)](https://codecov.io/gh/dwmkerr/app-icon) [![dependencies Status](https://david-dm.org/dwmkerr/app-icon/status.svg)](https://david-dm.org/dwmkerr/app-icon) [![devDependencies Status](https://david-dm.org/dwmkerr/app-icon/dev-status.svg)](https://david-dm.org/dwmkerr/app-icon?type=dev) [![GuardRails badge](https://badges.production.guardrails.io/dwmkerr/app-icon.svg)](https://www.guardrails.io) [![Greenkeeper badge](https://badges.greenkeeper.io/dwmkerr/app-icon.svg)](https://greenkeeper.io/) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)

Automatic icon resizing for Mobile Apps. Supports Native, Cordova and React Native. Also supports labelling of app icons. Inspired by [cordova-icon](https://github.com/AlexDisler/cordova-icon).
Icon management for Mobile Apps. Create icons, generate all required sizes, label and annotate. Supports Native, Cordova, React Native, Xamarin and more. Inspired by [cordova-icon](https://github.com/AlexDisler/cordova-icon). Node 6 and onwards supported.

<img src="./assets/banner.png" width="614" alt="Banner">


<!-- vim-markdown-toc GFM -->

* [Introduction](#introduction)
* [Installation](#installation)
* [Usage](#usage)
* [Initialising](#initialising)
* [Generating Icons](#generating-icons)
* [Labelling Icons](#labelling-icons)
* [Developer Guide](#developer-guide)
Expand All @@ -32,7 +32,7 @@ Automatic icon resizing for Mobile Apps. Supports Native, Cordova and React Nati

This simple tool allows you to create a single icon in your app project, then create icons of all required sizes from it. It currently works for iOS and Android. You can also add labels to your app icons.

Create a single large `icon.png`, at least 192 pixels square, then run:
Create a single large `icon.png` at least 192 pixels square, or run `app-icon init` to create this icon, then run:

```bash
# If you are using npm 5.2 onwards...
Expand All @@ -44,6 +44,7 @@ app-icon generate
```

You can also use the module directly in node:

```js
/**
* appIcon = {
Expand Down Expand Up @@ -87,11 +88,25 @@ sudo yum install imagemagick # CentOS/etc

## Usage

The commandline tool can be used to generate icons or label icons.
The `app-icon` tool can be used to create a simple template icon, generate icons of all sizes from a template icon, or label icons.

### Initialising

If you do not already have a single icon to use as the main icon for your project, you can create one with the `init` command:

```bash
app-icon init # generates an icon named icon.png
```

You can also add a simple label to the icon.

```bash
app-icon init --caption "App" # creates an icon with the text 'App'
```

### Generating Icons

Add an icon (ideally at least 192x192 pixels) named `icon.png` to your project root. To automatically generate icons of all sizes for all app projects in the same folder, run:
Add an icon (ideally at least 192x192 pixels) named `icon.png` to your project root (or run `app-icon init`). To automatically generate icons of all sizes for all app projects in the same folder, run:

```bash
app-icon generate
Expand Down Expand Up @@ -146,11 +161,11 @@ The only dependencies are Node 6 (or above) and Yarn.

Useful commands for development are:

| Command | Usage |
|---------|-------|
| `npm test` | Runs the unit tests. |
| Command | Usage |
|----------------------|------------------------------------------------------------------------------------------|
| `npm test` | Runs the unit tests. |
| `npm run test:debug` | Runs the tests in a debugger. Combine with `.only` and `debugger` for ease of debugging. |
| `npm run cov` | Runs the tests, writing coverage reports to `./artifacts/coverage`. |
| `npm run cov` | Runs the tests, writing coverage reports to `./artifacts/coverage`. |

Currently the linting style is based on [airbnb](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb). Run `npm run lint` to lint the code.

Expand Down Expand Up @@ -238,10 +253,10 @@ To run the native apps, open the `./test/NativeApp` directory, then open the iOS

The table below shows the current confirmed compatibility:

| Platform | `app-icon` | ImageMagick | Status |
|----------|------------|-------------|--------|
| OSX | `0.6.x` | 6, 7 ||
| Ubuntu 14 | `0.6.x` | 6 ||
| Platform | `app-icon` | ImageMagick | Status |
|-----------|------------|-------------|--------|
| OSX | `0.6.x` | 6, 7 | |
| Ubuntu 14 | `0.6.x` | 6 | |

## Troubleshooting

Expand Down
32 changes: 32 additions & 0 deletions bin/app-icon.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
const chalk = require('chalk');
const program = require('commander');
const imagemagickCli = require('imagemagick-cli');
const path = require('path');
const pack = require('../package.json');
const generate = require('../src/generate');
const init = require('../src/init/init');
const labelImage = require('../src/label/label-image');
const fileExists = require('../src/utils/file-exists');

Expand Down Expand Up @@ -91,13 +93,43 @@ program
});
});

// Define the 'init' command.
program
.command('init')
.description('Initialises app icons')
.option('-c, --caption [caption]', "An optional caption for the icon, e.g 'App'.")
.action((params) => {
const { caption } = params;
imagemagickCli.getVersion()
.then((version) => {
if (!version) {
console.error(' Error: ImageMagick must be installed. Try:');
console.error(' brew install imagemagick');
return process.exit(1);
}

// Create the icon from the template, captioned if needed.
const input = path.resolve(__dirname, '../src/init/icon.template.png');
return init(input, './icon.png', { caption });
})
.then(() => {
console.log(`Created icon '${chalk.green('icon.png')}'`);
})
.catch((createError) => {
console.error('An error occurred creating the icon...');
console.log(createError);
return process.exit(1);
});
});

// Extend the help with some examples.
program.on('--help', () => {
console.log(' Examples:');
console.log('');
console.log(' $ app-icon generate');
console.log(' $ app-icon generate -i myicon.png -s ./app/cordova-app');
console.log(' $ app-icon label -i myicon.png -o myicon.out.png -t qa -b 1.2.3');
console.log(' $ app-icon init --caption "App"');
console.log('');
});

Expand Down
1 change: 1 addition & 0 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ test: build
npm run test

# Run the CircleCI build locally.
# Install the CLI with: curl -fLSs https://circle.ci/cli | bash
circleci:
circleci config validate -c .circleci/config.yml
circleci build --job test
Expand Down
22 changes: 17 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 21 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
},
"scripts": {
"start": "./bin/app-icon.js",
"test": "mocha -t 10000 src/**/*.specs.js",
"test:debug": "mocha -d --inspect-brk -w --inspect ./src/**/*.specs.js",
"cov": "nyc --reporter text --reporter html --reporter lcov --report-dir './artifacts/coverage' -x './src/**/*.specs.js' mocha --timeout 10000 ./src/**/*.specs.js",
"test": "mocha -t 10000 ./src/{,**/}*.specs.js",
"test:debug": "mocha -d --inspect-brk -w --inspect ./src/{,**/}*.specs.js",
"cov": "nyc mocha --timeout 10000 ./src/{,**/}*.specs.js",
"lint": "eslint .",
"release": "standard-version"
},
Expand Down Expand Up @@ -43,7 +43,24 @@
"dependencies": {
"chalk": "^2.3.0",
"commander": "^2.19.0",
"imagemagick-cli": "^0.1.3",
"imagemagick-cli": "^0.5.0",
"mkdirp": "^0.5.1"
},
"nyc": {
"all": true,
"include": [
"src/**/*.js"
],
"exclude": [
"src/testing/*",
"src/**/*.specs.js"
],
"reporter": [
"text",
"html",
"lcov"
],
"cache": true,
"report-dir": "./artifacts/coverage"
}
}
File renamed without changes.
File renamed without changes.
Binary file added src/init/icon.template.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions src/init/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const imagemagickCli = require('imagemagick-cli');
const getImageWidth = require('../imagemagick/get-image-width');
const copyFile = require('../utils/copy-file');

/**
* init - creates an icon from a template.
*
* @param template
* @param output
* @param options
* @returns a promise which resolves when the icon has been created.
*/
function init(template, output, options) {
// If there is no caption, then we can just copy the image.
const caption = (options && options.caption) || '';
if (!caption) return copyFile(template, output);

// We have a caption, so we'll need to get the image width to work out how
// to arrange it on the icon.
return getImageWidth(template)
.then((width) => {
// The height will be 80% of the width, i.e. the text almost fills the icon.
// TODO: getImageWidth should be getImageDimensions
const w = Math.floor(width * 0.8);
const h = Math.floor(width * 0.8);

// Create the command to generate the image.
const command = `convert \
-background "rgba(0,0,0,0)" -fill white \
-gravity center -size ${w}x${h} \
caption:"${caption}" \
${template} +swap -composite ${output}`;
return imagemagickCli.exec(command);
});
}

module.exports = init;
27 changes: 27 additions & 0 deletions src/init/init.specs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { expect } = require('chai');
const init = require('./init');
const compareImages = require('../testing/compare-images');

describe('init', () => {
it('should be able to init an icon', () => {
const template = './src/init/icon.template.png';
const output = './src/init/test-images/init-output.png';
const reference = './src/init/test-images/init-reference.png';
return init(template, output)
.then(() => compareImages(output, reference))
.then((difference) => {
expect(difference).to.be.below(2.5, 'Generated image is below accepted similarly threshold');
});
});

it('should be able to init an icon with a caption', () => {
const template = './src/init/icon.template.png';
const output = './src/init/test-images/init-with-caption-output.png';
const reference = './src/init/test-images/init-with-caption-reference.png';
return init(template, output, { caption: 'Test' })
.then(() => compareImages(output, reference))
.then((difference) => {
expect(difference).to.be.below(2.5, 'Generated image is below accepted similarly threshold');
});
});
});
Binary file added src/init/test-images/init-reference.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 29 additions & 17 deletions src/label/label-image.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
const imagemagickCli = require('imagemagick-cli');
const getImageWidth = require('./get-image-width');
const getImageWidth = require('../imagemagick/get-image-width');
const copyFile = require('../utils/copy-file');

// Use imagemagick to label an image. Gravity should be 'north' or 'south'.
function caption(input, output, label, gravity) {
/**
* caption - add a caption to an image.
*
* @param input - the path to the input image.
* @param output - the path to the output image.
* @param label - the label or caption to add.
* @param gravity - the position - any imagemagick 'gravity' value. Normally
* 'north', 'south' or 'center'.
* @param proportionalSize - the size of the total image which should be taken
* up by the caption. e.g. 0.2 means 20% of the icon will have the caption.
* @returns a promise which resolves with the result of the CLI command.
*/
function caption(input, output, label, gravity, proportionalSize) {
// Get the image width.
return getImageWidth(input)
.then((width) => {
// The height is a fifth of the width.
// The height is a proportional amount of the of the width. This means
// with a square image, a proportionalSize of 0.2 and a gravity of 'bottom',
// the bottom 20% of the icon will contain the caption.
const height = width * proportionalSize;

// Important: the quotes around the colour must be double quoutes for the
// command to work on Windows!
const height = width / 5;
const command = `convert \
-background "rgba(0,0,0,0.5)" -fill white \
-gravity center -size ${width}x${height} \
Expand All @@ -20,17 +35,14 @@ function caption(input, output, label, gravity) {
}

// Single function to label an image (optionally top and bottom).
module.exports = function labelImage(input, output, top, bottom) {
if (top && bottom) {
return caption(input, output, top, 'north')
.then(() => caption(output, output, bottom, 'south'));
}
if (top) {
return caption(input, output, top, 'north');
}
if (bottom) {
return caption(input, output, bottom, 'south');
}
module.exports = function labelImage(input, output, top, bottom, middle) {
// First, create the output image. This will overwrite any existing file.
let promise = copyFile(input, output);

// We'll have a set of promises which we will run, which will update the image.
if (top) promise = promise.then(() => caption(output, output, top, 'north', 0.2));
if (bottom) promise = promise.then(() => caption(output, output, bottom, 'south', 0.2));
if (middle) promise = promise.then(() => caption(output, output, middle, 'center', 0.6));

return Promise.reject(Error("When labeling an image, a 'top' or 'bottom' must be specified."));
return promise;
};
Loading

0 comments on commit 3736f84

Please sign in to comment.