react-native-svg-app-icon
Version:
App icon generator for React Native projects
284 lines (249 loc) • 10.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.generate = generate;
var path = _interopRequireWildcard(require("path"));
var _svg2vectordrawable = _interopRequireDefault(require("svg2vectordrawable"));
var input = _interopRequireWildcard(require("./input"));
var output = _interopRequireWildcard(require("./output"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
const adaptiveIconMinSdk = 26;
const densities = [{
name: "mdpi",
scale: 1
}, {
name: "hdpi",
scale: 1.5
}, {
name: "xhdpi",
scale: 2
}, {
name: "xxhdpi",
scale: 3
}, {
name: "xxxhdpi",
scale: 4
}];
const launcherName = "ic_launcher";
const roundIconName = "ic_launcher_round";
const launcherBackgroundName = "ic_launcher_background";
const launcherForegroundName = "ic_launcher_foreground";
/** Adaptive Icon **/
const adaptiveIconBaseSize = 108;
const adaptiveIconContent = (launcherBackgroundType, launcherForegroundType) => `xml version="1.0" encoding="utf-8"
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@${launcherBackgroundType}/${launcherBackgroundName}" />
<foreground android:drawable="@${launcherForegroundType}/${launcherForegroundName}" />
</adaptive-icon>`;
/** Legacy Icon **/
const legacyIconBaseSize = 48;
const inputIconContentRatio = input.inputContentSize / input.inputImageSize; // Based on images from image asset studio at
// https://android.googlesource.com/platform/tools/adt/idea/+/refs/heads/mirror-goog-studio-master-dev/android/resources/images/launcher_stencil/
// https://android.googlesource.com/platform/tools/adt/idea/+/refs/heads/mirror-goog-studio-master-dev/android/src/com/android/tools/idea/npw/assetstudio/LauncherLegacyIconGenerator.java
const legacyLightningFilter = `
<filter id="legacyLightningFilter">
<!-- Drop shadow -->
<feGaussianBlur in="SourceAlpha" stdDeviation="0.4" />
<feOffset dx="0" dy="1.125" />
<feComponentTransfer>
<feFuncA type="linear" slope="0.2"/>
</feComponentTransfer>
<feComposite in2="SourceAlpha" operator="out"
result="shadow"
/>
<!-- Edge shade -->
<feComponentTransfer in="SourceAlpha" result="opaque-alpha">
<feFuncA type="linear" slope="0.2"/>
</feComponentTransfer>
<feOffset dx="-0.2" dy="-0.2" in="SourceAlpha" result="offset-alpha" />
<feComposite in="opaque-alpha" in2="offset-alpha" operator="out"
result="edge"
/>
<feMerge>
<feMergeNode in="shadow" />
<feMergeNode in="edge" />
</feMerge>
</filter>`;
/** Legacy Square Icon **/
const legacySquareIconContentSize = 38;
const legacySquareIconBorderRadius = 3;
const legacySquareIconMargin = (legacyIconBaseSize - legacySquareIconContentSize) / 2;
const legacySquareIconContentRatio = legacySquareIconContentSize / legacyIconBaseSize;
const legacySquareIconMask = Buffer.from(`<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
viewBox="${getViewBox(legacySquareIconContentSize)}"
width="${input.inputImageSize}" height="${input.inputImageSize}">
<rect
x="${legacySquareIconMargin}" y="${legacySquareIconMargin}"
width="${legacySquareIconContentSize}" height="${legacySquareIconContentSize}"
rx="${legacySquareIconBorderRadius}" ry="${legacySquareIconBorderRadius}"
/>
</svg>`, "utf-8");
const legacySquareIconOverlay = Buffer.from(`<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
viewBox="${getViewBox(legacySquareIconContentSize)}"
width="${input.inputImageSize}" height="${input.inputImageSize}">
${legacyLightningFilter}
<rect
x="${legacySquareIconMargin}" y="${legacySquareIconMargin}"
width="${legacySquareIconContentSize}" height="${legacySquareIconContentSize}"
rx="${legacySquareIconBorderRadius}" ry="${legacySquareIconBorderRadius}"
filter="url(#legacyLightningFilter)"
/>
</svg>`, "utf-8");
/** Legacy Round Icon **/
const legacyRoundIconContentSize = 44;
const legacyRoundIconContentRatio = legacyRoundIconContentSize / legacyIconBaseSize;
const roundIconMask = Buffer.from(`<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
viewBox="${getViewBox(legacyRoundIconContentSize)}"
width="${input.inputImageSize}" height="${input.inputImageSize}">
<circle
cx="${legacyIconBaseSize / 2}" cy="${legacyIconBaseSize / 2}"
r="${legacyRoundIconContentSize / 2}"
/>
</svg>`, "utf-8");
const roundIconOverlay = Buffer.from(`<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
viewBox="${getViewBox(legacyRoundIconContentSize)}"
width="${input.inputImageSize}" height="${input.inputImageSize}">
${legacyLightningFilter}
<circle
cx="${legacyIconBaseSize / 2}" cy="${legacyIconBaseSize / 2}"
r="${legacyRoundIconContentSize / 2}"
filter="url(#legacyLightningFilter)"
/>
</svg>`, "utf-8");
function getViewBox(input) {
const size = input / inputIconContentRatio;
const margin = (size - legacyIconBaseSize) / 2;
const viewBox = [-margin, -margin, size, size];
return viewBox.join(" ");
}
async function* generate(config, fileInput) {
const fullConfig = getConfig(config);
yield* generateLegacyIcons(fileInput, fullConfig);
yield* generateRoundIcons(fileInput, fullConfig);
yield* generateAdaptiveIcon(fileInput, fullConfig);
}
function getConfig(config) {
return {
androidOutputPath: config.androidOutputPath || "./android/app/src/main/res",
force: config.force || false
};
}
async function* generateLegacyIcons(fileInput, config) {
yield* output.genaratePngs({ ...input.mapInput(fileInput, inputData => ({
baseImage: inputData.backgroundImageData,
operations: [{
type: "composite",
file: inputData.foregroundImageData.data
}, {
type: "composite",
blend: "mask",
file: legacySquareIconMask
}, {
type: "composite",
file: legacySquareIconOverlay
}]
})),
cropSize: input.inputContentSize / legacySquareIconContentRatio
}, densities.map(density => ({
filePath: getIconPath(config, "mipmap", {
density: density.name
}, `${launcherName}.png`),
outputSize: legacyIconBaseSize * density.scale,
force: config.force
})));
}
async function* generateRoundIcons(fileInput, config) {
yield* output.genaratePngs({ ...input.mapInput(fileInput, inputData => ({
baseImage: inputData.backgroundImageData,
operations: [{
type: "composite",
file: inputData.foregroundImageData.data
}, {
type: "composite",
blend: "mask",
file: roundIconMask
}, {
type: "composite",
file: roundIconOverlay
}]
})),
cropSize: input.inputContentSize / legacyRoundIconContentRatio
}, densities.map(density => ({
filePath: getIconPath(config, "mipmap", {
density: density.name
}, `${roundIconName}.png`),
outputSize: legacyIconBaseSize * density.scale,
force: config.force
})));
}
async function* generateAdaptiveIcon(fileInput, config) {
const backgroundImageInput = input.mapInput(fileInput, inputData => ({
image: inputData.backgroundImageData
}));
let backgroundResourceType;
try {
yield* generateAdaptiveIconLayerVd(backgroundImageInput, launcherBackgroundName, config);
backgroundResourceType = "drawable";
} catch {
yield* generateAdaptiveIconLayerPng(backgroundImageInput, launcherBackgroundName, config);
backgroundResourceType = "mipmap";
}
const foregroundImageInput = input.mapInput(fileInput, inputData => ({
image: inputData.foregroundImageData
}));
let foregroundResourceType;
try {
yield* generateAdaptiveIconLayerVd(foregroundImageInput, launcherForegroundName, config);
foregroundResourceType = "drawable";
} catch {
yield* generateAdaptiveIconLayerPng(foregroundImageInput, launcherForegroundName, config);
foregroundResourceType = "mipmap";
} // Adaptive icon
const adaptiveIconXml = adaptiveIconContent(backgroundResourceType, foregroundResourceType);
yield* output.ensureFileContents(getIconPath(config, "mipmap", {
density: "anydpi",
minApiLevel: 26
}, `${launcherName}.xml`), adaptiveIconXml, config);
yield* output.ensureFileContents(getIconPath(config, "mipmap", {
density: "anydpi",
minApiLevel: 26
}, `${roundIconName}.xml`), adaptiveIconXml, config);
}
async function* generateAdaptiveIconLayerVd(imageInput, fileName, config) {
const imageData = await imageInput.read();
const vdData = await (0, _svg2vectordrawable.default)(imageData.image.data.toString("utf-8"), {
// Fail on unsupported elements, so that we fall back to PNG rendering
strict: true,
// Use same default fill behaviour as in SVG spec
fillBlack: true
});
yield* output.ensureFileContents(getIconPath(config, "drawable", {
density: "anydpi",
minApiLevel: 26
}, `${fileName}.xml`), vdData, config);
}
async function* generateAdaptiveIconLayerPng(imageInput, fileName, config) {
yield* output.genaratePngs(input.mapInput(imageInput, imageData => ({
baseImage: imageData.image
})), densities.map(density => ({
filePath: getIconPath(config, "mipmap", {
density: density.name,
minApiLevel: adaptiveIconMinSdk
}, `${fileName}.png`),
outputSize: adaptiveIconBaseSize * density.scale,
force: config.force
})));
}
function getIconPath(config, resourceType, qualifier, fileName) {
let directoryName = [resourceType];
if (qualifier.density) {
directoryName = [...directoryName, qualifier.density];
}
if (qualifier.minApiLevel) {
directoryName = [...directoryName, `v${qualifier.minApiLevel}`];
}
return path.join(config.androidOutputPath, directoryName.join("-"), fileName);
}