480 lines
23 KiB
JavaScript
480 lines
23 KiB
JavaScript
/* eslint camelcase: 0, no-shadow: 0 */
|
|
|
|
const path = require('path'),
|
|
fs = require('fs'),
|
|
_ = require('underscore'),
|
|
color = require('tinycolor2'),
|
|
cheerio = require('cheerio'),
|
|
colors = require('colors'),
|
|
jsonxml = require('jsontoxml'),
|
|
sizeOf = require('image-size'),
|
|
async = require('async'),
|
|
mkdirp = require('mkdirp'),
|
|
Jimp = require('jimp'),
|
|
svg2png = require('svg2png'),
|
|
File = require('vinyl'),
|
|
Reflect = require('harmony-reflect'),
|
|
NRC = require('node-rest-client').Client,
|
|
PLATFORM_OPTIONS = require('./config/platform-options.json'),
|
|
ANDROID_BASE_SIZE = 36,
|
|
IOS_BASE_SIZE = 57,
|
|
IOS_STARTUP_BASE_SIZE = 320,
|
|
COAST_BASE_SIZE = 228,
|
|
FIREFOX_BASE_SIZE = 60;
|
|
|
|
(() => {
|
|
|
|
'use strict';
|
|
|
|
const xmlconfig = { prettyPrint: true, xmlHeader: true, indent: ' ' },
|
|
client = new NRC(),
|
|
HEX_MAX = 255,
|
|
NON_EXISTANT = -1,
|
|
ROTATE_DEGREES = 90,
|
|
HTTP_SUCCESS = 200;
|
|
|
|
client.setMaxListeners(0);
|
|
|
|
function helpers (options) {
|
|
|
|
function contains (array, element) {
|
|
return array.indexOf(element.toLowerCase()) > NON_EXISTANT;
|
|
}
|
|
|
|
function relative (directory) {
|
|
return path.join(options.path, directory).replace(/\\/g, '/');
|
|
}
|
|
|
|
function print (context, message) {
|
|
let newMessage = '';
|
|
|
|
if (options.logging && message) {
|
|
_.each(message.split(' '), (item) => {
|
|
newMessage += ` ${ ((/^\d+x\d+$/gm).test(item) ? colors.magenta(item) : item) }`;
|
|
});
|
|
console.log(`${ colors.green('[Favicons]') } ${ context.yellow }:${ newMessage }...`);
|
|
}
|
|
}
|
|
|
|
function readFile (filepath, callback) {
|
|
fs.readFile(filepath, callback);
|
|
}
|
|
|
|
function updateDocument (document, code, tags, next) {
|
|
const $ = cheerio.load(document, { decodeEntities: false }),
|
|
target = $('head').length > 0 ? $('head') : $.root(),
|
|
newCode = cheerio.load(code.join('\n'), { decodeEntities: false });
|
|
|
|
async.each(tags, (platform, callback) => {
|
|
async.forEachOf(platform, (tag, selector, cb) => {
|
|
if (options.replace) {
|
|
$(selector).remove();
|
|
} else if ($(selector).length) {
|
|
newCode(selector).remove();
|
|
}
|
|
return cb(null);
|
|
}, callback);
|
|
}, (error) => {
|
|
target.append(newCode.html());
|
|
return next(error, $.html().replace(/^\s*$[\n\r]{1,}/gm, ''));
|
|
});
|
|
}
|
|
|
|
function preparePlatformOptions (platform, options, baseOptions) {
|
|
if (typeof options !== 'object') {
|
|
options = {};
|
|
}
|
|
|
|
_.each(options, (value, key) => {
|
|
const platformOptionsRef = PLATFORM_OPTIONS[key];
|
|
|
|
if (typeof platformOptionsRef === 'undefined' || platformOptionsRef.platforms.indexOf(platform) === -1) {
|
|
return Reflect.deleteProperty(options, key);
|
|
}
|
|
});
|
|
|
|
_.each(PLATFORM_OPTIONS, ({ platforms, defaultTo }, key) => {
|
|
if (typeof options[key] === 'undefined' && platforms.indexOf(platform) !== -1) {
|
|
options[key] = defaultTo;
|
|
}
|
|
});
|
|
|
|
if (typeof options.background === 'boolean') {
|
|
|
|
if (platform === 'android' && !options.background) {
|
|
options.background = 'transparent';
|
|
} else {
|
|
options.background = baseOptions.background;
|
|
}
|
|
}
|
|
|
|
if (platform === 'android' && options.background !== 'transparent') {
|
|
options.disableTransparency = true;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
return {
|
|
|
|
General: {
|
|
preparePlatformOptions,
|
|
background: (hex) => {
|
|
print('General:background', `Parsing colour ${ hex }`);
|
|
const rgba = color(hex).toRgb();
|
|
|
|
return Jimp.rgbaToInt(rgba.r, rgba.g, rgba.b, rgba.a * HEX_MAX);
|
|
},
|
|
source: (source, callback) => {
|
|
let sourceset = [];
|
|
|
|
print('General:source', `Source type is ${ typeof source }`);
|
|
if (!source || !source.length) {
|
|
return callback('No source provided');
|
|
} else if (Buffer.isBuffer(source)) {
|
|
sourceset = [{ size: sizeOf(source), file: source }];
|
|
return callback(null, sourceset);
|
|
} else if (Array.isArray(source)) {
|
|
async.each(source, (file, cb) =>
|
|
readFile(file, (error, buffer) => {
|
|
if (error) {
|
|
return cb(error);
|
|
}
|
|
|
|
sourceset.push({
|
|
size: sizeOf(buffer),
|
|
file: buffer
|
|
});
|
|
cb(null);
|
|
}),
|
|
(error) =>
|
|
callback(error || sourceset.length ? null : 'Favicons source is invalid', sourceset)
|
|
);
|
|
} else if (typeof source === 'string') {
|
|
readFile(source, (error, buffer) => {
|
|
if (error) {
|
|
return callback(error);
|
|
}
|
|
|
|
sourceset = [{ size: sizeOf(buffer), file: buffer }];
|
|
return callback(null, sourceset);
|
|
});
|
|
} else {
|
|
return callback('Invalid source type provided');
|
|
}
|
|
},
|
|
/* eslint no-underscore-dangle: 0 */
|
|
vinyl: (object, input) => {
|
|
const output = new File({
|
|
path: object.name,
|
|
contents: Buffer.isBuffer(object.contents) ? object.contents : new Buffer(object.contents)
|
|
});
|
|
|
|
// gulp-cache support
|
|
if (typeof input._cachedKey !== 'undefined') {
|
|
output._cachedKey = input._cachedKey;
|
|
}
|
|
|
|
return output;
|
|
}
|
|
},
|
|
|
|
HTML: {
|
|
parse: (html, callback) => {
|
|
print('HTML:parse', 'HTML found, parsing and modifying source');
|
|
const $ = cheerio.load(html),
|
|
link = $('*').is('link'),
|
|
attribute = link ? 'href' : 'content',
|
|
value = $('*').first().attr(attribute);
|
|
|
|
if (path.extname(value)) {
|
|
$('*').first().attr(attribute, relative(value));
|
|
} else if (value.slice(0, 1) === '#') {
|
|
$('*').first().attr(attribute, options.background);
|
|
} else if (html.indexOf('application-name') !== NON_EXISTANT || html.indexOf('apple-mobile-web-app-title') !== NON_EXISTANT) {
|
|
$('*').first().attr(attribute, options.appName);
|
|
}
|
|
return callback(null, $.html());
|
|
},
|
|
update: (document, code, tags, callback) => {
|
|
const encoding = { encoding: 'utf8' };
|
|
|
|
async.waterfall([
|
|
(cb) =>
|
|
mkdirp(path.dirname(document), cb),
|
|
(made, cb) =>
|
|
fs.readFile(document, encoding, (error, data) => cb(null, error ? null : data)),
|
|
(data, cb) =>
|
|
(data ? updateDocument(data, code, tags, cb) : cb(null, code.join('\n'))),
|
|
(html, cb) =>
|
|
fs.writeFile(document, html, options, cb)
|
|
], callback);
|
|
}
|
|
},
|
|
|
|
Files: {
|
|
create: (properties, name, platformOptions, callback) => {
|
|
print('Files:create', `Creating file: ${ name }`);
|
|
if (name === 'manifest.json') {
|
|
properties.name = options.appName;
|
|
properties.short_name = options.appName;
|
|
properties.description = options.appDescription;
|
|
properties.dir = options.dir;
|
|
properties.lang = options.lang;
|
|
properties.display = options.display;
|
|
properties.orientation = options.orientation;
|
|
properties.start_url = options.start_url;
|
|
properties.background_color = options.background;
|
|
properties.theme_color = options.theme_color;
|
|
_.map(properties.icons, (icon) => (icon.src = relative(icon.src)));
|
|
properties = JSON.stringify(properties, null, 2);
|
|
} else if (name === 'manifest.webapp') {
|
|
properties.version = options.version;
|
|
properties.name = options.appName;
|
|
properties.description = options.appDescription;
|
|
properties.developer.name = options.developerName;
|
|
properties.developer.url = options.developerURL;
|
|
properties.icons = _.mapObject(properties.icons, (property) => relative(property));
|
|
properties = JSON.stringify(properties, null, 2);
|
|
} else if (name === 'browserconfig.xml') {
|
|
_.map(properties[0].children[0].children[0].children, (property) => {
|
|
if (property.name === 'TileColor') {
|
|
property.text = platformOptions.background;
|
|
} else {
|
|
property.attrs.src = relative(property.attrs.src);
|
|
}
|
|
});
|
|
properties = jsonxml(properties, xmlconfig);
|
|
} else if (name === 'yandex-browser-manifest.json') {
|
|
properties.version = options.version;
|
|
properties.api_version = 1;
|
|
properties.layout.logo = relative(properties.layout.logo);
|
|
properties.layout.color = platformOptions.background;
|
|
properties = JSON.stringify(properties, null, 2);
|
|
} else if (/\.html$/.test(name)) {
|
|
properties = properties.join('\n');
|
|
}
|
|
return callback(null, { name, contents: properties });
|
|
}
|
|
},
|
|
|
|
Images: {
|
|
create: (properties, background, callback) => {
|
|
let jimp = null;
|
|
|
|
print('Image:create', `Creating empty ${ properties.width }x${ properties.height } canvas with ${ (properties.transparent ? 'transparent' : background) } background`);
|
|
jimp = new Jimp(properties.width, properties.height, properties.transparent ? 0x00000000 : background, (error, canvas) =>
|
|
callback(error, canvas, jimp));
|
|
},
|
|
read: (file, callback) => {
|
|
print('Image:read', `Reading file: ${ file.buffer }`);
|
|
return Jimp.read(file, callback);
|
|
},
|
|
nearest: (sourceset, properties, offset, callback) => {
|
|
print('Image:nearest', `Find nearest icon to ${ properties.width }x${ properties.height } with offset ${ offset }`);
|
|
|
|
const offsetSize = offset * 2,
|
|
width = properties.width - offsetSize,
|
|
height = properties.height - offsetSize,
|
|
sideSize = Math.max(width, height),
|
|
svgSource = _.find(sourceset, (source) => source.size.type === 'svg');
|
|
|
|
let nearestIcon = sourceset[0],
|
|
nearestSideSize = Math.max(nearestIcon.size.width, nearestIcon.size.height);
|
|
|
|
if (svgSource) {
|
|
print('Image:nearest', `SVG source will be saved as ${ width }x${ height }`);
|
|
svg2png(svgSource.file, { height, width })
|
|
.then((resizedBuffer) => callback(null, {
|
|
size: sizeOf(resizedBuffer),
|
|
file: resizedBuffer
|
|
}))
|
|
.catch(callback);
|
|
} else {
|
|
_.each(sourceset, (icon) => {
|
|
const max = Math.max(icon.size.width, icon.size.height);
|
|
|
|
if ((nearestSideSize > max || nearestSideSize < sideSize) && max >= sideSize) {
|
|
nearestIcon = icon;
|
|
nearestSideSize = max;
|
|
}
|
|
});
|
|
|
|
return callback(null, nearestIcon);
|
|
}
|
|
},
|
|
resize: (image, properties, offset, callback) => {
|
|
print('Images:resize', `Resizing image to contain in ${ properties.width }x${ properties.height } with offset ${ offset }`);
|
|
const offsetSize = offset * 2;
|
|
|
|
if (properties.rotate) {
|
|
print('Images:resize', `Rotating image by ${ROTATE_DEGREES}`);
|
|
image.rotate(ROTATE_DEGREES, false);
|
|
}
|
|
|
|
image.contain(properties.width - offsetSize, properties.height - offsetSize, Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE);
|
|
return callback(null, image);
|
|
},
|
|
composite: (canvas, image, properties, offset, maximum, callback) => {
|
|
const circle = path.join(__dirname, 'mask.png'),
|
|
overlay = path.join(__dirname, 'overlay.png');
|
|
|
|
function compositeIcon () {
|
|
print('Images:composite', `Compositing favicon on ${ properties.width }x${ properties.height } canvas with offset ${ offset }`);
|
|
canvas.composite(image, offset, offset);
|
|
}
|
|
|
|
if (properties.mask) {
|
|
print('Images:composite', 'Masking composite image on circle');
|
|
async.parallel([
|
|
(cb) => Jimp.read(circle, cb),
|
|
(cb) => Jimp.read(overlay, cb)
|
|
], (error, images) => {
|
|
images[0].resize(maximum, Jimp.AUTO);
|
|
images[1].resize(maximum, Jimp.AUTO);
|
|
canvas.mask(images[0], 0, 0);
|
|
canvas.composite(images[1], 0, 0);
|
|
compositeIcon();
|
|
return callback(error, canvas);
|
|
});
|
|
} else {
|
|
compositeIcon();
|
|
return callback(null, canvas);
|
|
}
|
|
},
|
|
getBuffer: (canvas, callback) => {
|
|
print('Images:getBuffer', 'Collecting image buffer from canvas');
|
|
canvas.getBuffer(Jimp.MIME_PNG, callback);
|
|
}
|
|
},
|
|
|
|
RFG: {
|
|
configure: (sourceset, request, callback) => {
|
|
print('RFG:configure', 'Configuring RFG API request');
|
|
const svgSource = _.find(sourceset, (source) => source.size.type === 'svg');
|
|
|
|
options.background = `#${ color(options.background).toHex() }`;
|
|
request.master_picture.content = (svgSource || _.max(sourceset, ({ size: { width, height } }) => Math.max(width, height))).file.toString('base64');
|
|
request.files_location.path = options.path;
|
|
|
|
if (options.icons.android) {
|
|
const androidOptions = preparePlatformOptions('android', options.icons.android, options);
|
|
|
|
request.favicon_design.android_chrome.theme_color = options.background;
|
|
request.favicon_design.android_chrome.manifest.name = options.appName;
|
|
request.favicon_design.android_chrome.manifest.display = options.display;
|
|
request.favicon_design.android_chrome.manifest.orientation = options.orientation;
|
|
|
|
if (androidOptions.shadow) {
|
|
request.favicon_design.android_chrome.picture_aspect = 'shadow';
|
|
} else if (androidOptions.offset > 0 && androidOptions.background) {
|
|
request.favicon_design.android_chrome.picture_aspect = 'background_and_margin';
|
|
request.favicon_design.android_chrome.background_color = androidOptions.background;
|
|
request.favicon_design.android_chrome.margin = Math.round(ANDROID_BASE_SIZE / 100 * androidOptions.offset);
|
|
}
|
|
|
|
} else {
|
|
Reflect.deleteProperty(request.favicon_design, 'android_chrome');
|
|
}
|
|
|
|
if (options.icons.appleIcon) {
|
|
const appleIconOptions = preparePlatformOptions('appleIcon', options.icons.appleIcon, options);
|
|
|
|
request.favicon_design.ios.background_color = appleIconOptions.background;
|
|
request.favicon_design.ios.margin = Math.round(IOS_BASE_SIZE / 100 * appleIconOptions.offset);
|
|
} else {
|
|
Reflect.deleteProperty(request.favicon_design, 'ios');
|
|
}
|
|
|
|
if (options.icons.appleIcon && options.icons.appleStartup) {
|
|
const appleStartupOptions = preparePlatformOptions('appleStartup', options.icons.appleStartup, options);
|
|
|
|
request.favicon_design.ios.startup_image.background_color = appleStartupOptions.background;
|
|
request.favicon_design.ios.startup_image.margin = Math.round(IOS_STARTUP_BASE_SIZE / 100 * appleStartupOptions.offset);
|
|
} else if (request.favicon_design.ios) {
|
|
Reflect.deleteProperty(request.favicon_design.ios, 'startup_image');
|
|
}
|
|
|
|
if (options.icons.coast) {
|
|
const coastOptions = preparePlatformOptions('coast', options.icons.coast, options);
|
|
|
|
request.favicon_design.coast.background_color = coastOptions.background;
|
|
request.favicon_design.coast.margin = Math.round(COAST_BASE_SIZE / 100 * coastOptions.offset);
|
|
} else {
|
|
Reflect.deleteProperty(request.favicon_design, 'coast');
|
|
}
|
|
|
|
if (!options.icons.favicons) {
|
|
Reflect.deleteProperty(request.favicon_design, 'desktop_browser');
|
|
}
|
|
|
|
if (options.icons.firefox) {
|
|
const firefoxOptions = preparePlatformOptions('firefox', options.icons.firefox, options);
|
|
|
|
request.favicon_design.firefox_app.background_color = firefoxOptions.background;
|
|
request.favicon_design.firefox_app.margin = Math.round(FIREFOX_BASE_SIZE / 100 * firefoxOptions.offset);
|
|
request.favicon_design.firefox_app.manifest.app_name = options.appName;
|
|
request.favicon_design.firefox_app.manifest.app_description = options.appDescription;
|
|
request.favicon_design.firefox_app.manifest.developer_name = options.developerName;
|
|
request.favicon_design.firefox_app.manifest.developer_url = options.developerURL;
|
|
} else {
|
|
Reflect.deleteProperty(request.favicon_design, 'firefox_app');
|
|
}
|
|
|
|
if (options.icons.windows) {
|
|
const windowsOptions = preparePlatformOptions('windows', options.icons.windows, options);
|
|
|
|
request.favicon_design.windows.background_color = windowsOptions.background;
|
|
} else {
|
|
Reflect.deleteProperty(request.favicon_design, 'windows');
|
|
}
|
|
|
|
if (options.icons.yandex) {
|
|
const yandexOptions = preparePlatformOptions('yandex', options.icons.yandex, options);
|
|
|
|
request.favicon_design.yandex_browser.background_color = yandexOptions.background;
|
|
request.favicon_design.yandex_browser.manifest.version = options.version;
|
|
} else {
|
|
Reflect.deleteProperty(request.favicon_design, 'yandex_browser');
|
|
}
|
|
|
|
return callback(null, request);
|
|
},
|
|
request: (request, callback) => {
|
|
print('RFG:request', 'Posting a request to the RFG API');
|
|
client.post('http://realfavicongenerator.net/api/favicon', {
|
|
data: { favicon_generation: request },
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, (data, response) => {
|
|
const result = data.favicon_generation_result;
|
|
|
|
return result && response.statusCode === HTTP_SUCCESS ? callback(null, {
|
|
files: result.favicon.files_urls,
|
|
html: result.favicon.html_code
|
|
}) : callback(result.result.error_message);
|
|
});
|
|
},
|
|
fetch: (address, callback) => {
|
|
const name = path.basename(address),
|
|
image = contains(['.png', '.jpg', '.bmp', '.ico', '.svg'], path.extname(name));
|
|
|
|
print('RFG:fetch', `Fetching ${ image ? 'image' : 'file' } from RFG: ${ address }`);
|
|
client.get(address, (buffer, response) => {
|
|
const success = buffer && response.statusCode === HTTP_SUCCESS;
|
|
|
|
return success ? callback(null, {
|
|
file: image ? null : { name, contents: buffer },
|
|
image: image ? { name, contents: buffer } : null
|
|
}) : callback(`Could not fetch URL: ${ address }`);
|
|
});
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
module.exports = helpers;
|
|
|
|
})();
|