UNPKG

rn-app-icon-generator

Version:

A CLI tool to generate iOS and Android app icons for React Native from a single 1024x1024 PNG image.

118 lines (100 loc) 3.9 kB
import fs from "fs-extra"; import path from "path"; import sharp from "sharp"; import ora from "ora"; import { adaptiveIconSizes, androidSizes } from "./specs.js"; const PADDING_RATIO = 0.55; // Determines how much padding is added for adaptive icons // Main function to generate Android icons export async function generateAndroid( iconPath, // Path to the base icon background, // Background color for the icon hasValidMask, // Whether a custom mask is provided and valid maskBuffer, // Mask image buffer (PNG or SVG) outputDir // Optional custom output directory ) { const spinner = ora("⚙️ Generating Android icons...").start(); const androidResPath = outputDir || path.resolve("android/app/src/main/res"); // Generate standard and round icons for each Android density for (const [folder, size] of Object.entries(androidSizes)) { const dest = path.join(androidResPath, folder); await fs.ensureDir(dest); // Generate standard square icon await sharp(iconPath) .resize(size, size) .flatten({ background }) .toFile(path.join(dest, "ic_launcher.png")); // Generate round icon using an SVG circle mask const circleSvg = `<svg width="${size}" height="${size}"><circle cx="${ size / 2 }" cy="${size / 2}" r="${size / 2}" fill="white" /></svg>`; await sharp(iconPath) .resize(size, size) .composite([{ input: Buffer.from(circleSvg), blend: "dest-in" }]) .flatten({ background }) .toFile(path.join(dest, "ic_launcher_round.png")); } // Generate adaptive foreground icons (used with adaptive backgrounds) for (const [folder, size] of Object.entries(adaptiveIconSizes)) { const dest = path.join(androidResPath, folder); await fs.ensureDir(dest); // Resize icon with padding const paddedSize = Math.floor(size * PADDING_RATIO); const resized = await sharp(iconPath) .resize(paddedSize, paddedSize) .png() .toBuffer(); const offset = Math.floor((size - paddedSize) / 2); // Prepare layers for composite const compositeLayers = [ { input: resized, top: offset, left: offset, }, ]; // Apply custom mask if provided if (hasValidMask && maskBuffer) { const resizedMaskBuffer = await sharp(maskBuffer) .resize(size, size) .toBuffer(); compositeLayers.push({ input: resizedMaskBuffer, blend: "dest-in", }); } // Create adaptive icon foreground file const final = await sharp({ create: { width: size, height: size, channels: 4, background, }, }) .composite(compositeLayers) .png() .toBuffer(); await sharp(final).toFile(path.join(dest, "ic_launcher_foreground.png")); } // Generate XML for adaptive icon configuration const anydpi = path.join(androidResPath, "mipmap-anydpi-v26"); await fs.ensureDir(anydpi); await fs.writeFile( path.join(anydpi, "ic_launcher.xml"), `<?xml version="1.0" encoding="utf-8"?>\n<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">\n <background android:drawable="@color/ic_launcher_background" />\n <foreground android:drawable="@mipmap/ic_launcher_foreground" />\n</adaptive-icon>` ); // Inject background color into colors.xml if not yet defined const colorsPath = path.resolve("android/app/src/main/res/values/colors.xml"); if (fs.existsSync(colorsPath)) { let content = await fs.readFile(colorsPath, "utf-8"); if (!content.includes("ic_launcher_background")) { content = content.replace( /<\/resources>/, ` <color name="ic_launcher_background">${background}</color>\n</resources>` ); await fs.writeFile(colorsPath, content); } } // Finish with success message spinner.succeed(`Android icons generated at: ${androidResPath}`); }