Files
fourdst/electron/generate-icons.js

330 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Icon Generation Script for 4DSTAR Bundle Manager
*
* Generates all required macOS icon formats from the SVG source
* and creates the proper .icns file for the app bundle.
*/
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
class IconGenerator {
constructor() {
this.projectRoot = path.resolve(__dirname, '..');
this.iconOutputDir = path.join(__dirname, 'icons');
this.tempDir = path.join(__dirname, 'temp-icons');
// Icon source paths
this.appIconSvg = path.join(this.projectRoot, 'assets', 'toolkit', 'appicon', 'toolkitIcon.svg');
this.bundleIconSvg = path.join(this.projectRoot, 'assets', 'bundle', 'fourdst_bundle_icon.svg');
this.opatIconSvg = path.join(this.projectRoot, 'assets', 'opat', 'fourdst_opat_icon.svg');
// macOS app icon sizes (for .icns)
this.appIconSizes = [
{ size: 16, scale: 1 },
{ size: 16, scale: 2 },
{ size: 32, scale: 1 },
{ size: 32, scale: 2 },
{ size: 128, scale: 1 },
{ size: 128, scale: 2 },
{ size: 256, scale: 1 },
{ size: 256, scale: 2 },
{ size: 512, scale: 1 },
{ size: 512, scale: 2 }
];
}
log(message, type = 'info') {
const prefix = {
'info': '📋',
'success': '✅',
'warning': '⚠️',
'error': '❌'
}[type] || '';
console.log(`${prefix} ${message}`);
}
async runCommand(command, args, options = {}) {
return new Promise((resolve, reject) => {
const process = spawn(command, args, {
stdio: 'pipe',
...options
});
let stdout = '';
let stderr = '';
process.stdout.on('data', (data) => {
stdout += data.toString();
});
process.stderr.on('data', (data) => {
stderr += data.toString();
});
process.on('close', (code) => {
if (code === 0) {
resolve({ stdout, stderr });
} else {
reject(new Error(`Command failed with exit code ${code}: ${stderr}`));
}
});
process.on('error', (err) => {
reject(err);
});
});
}
checkDependencies() {
this.log('Checking dependencies...', 'info');
// Check if all SVG sources exist
const svgSources = [
{ name: 'App icon', path: this.appIconSvg },
{ name: 'Bundle icon', path: this.bundleIconSvg },
{ name: 'OPAT icon', path: this.opatIconSvg }
];
for (const source of svgSources) {
if (!fs.existsSync(source.path)) {
throw new Error(`${source.name} SVG not found: ${source.path}`);
}
this.log(`${source.name} SVG found: ${source.path}`, 'success');
}
// Check for required tools
const requiredTools = ['rsvg-convert', 'iconutil'];
const missingTools = [];
for (const tool of requiredTools) {
try {
this.runCommand('which', [tool]);
this.log(`${tool} available`, 'success');
} catch (e) {
missingTools.push(tool);
}
}
if (missingTools.length > 0) {
this.log('Missing required tools. Installing...', 'warning');
this.installDependencies(missingTools);
}
}
async installDependencies(missingTools) {
if (missingTools.includes('rsvg-convert')) {
this.log('Installing librsvg (for rsvg-convert)...', 'info');
try {
await this.runCommand('brew', ['install', 'librsvg']);
this.log('✓ librsvg installed', 'success');
} catch (e) {
throw new Error('Failed to install librsvg. Please install manually: brew install librsvg');
}
}
// iconutil is part of Xcode command line tools
if (missingTools.includes('iconutil')) {
this.log('iconutil not found. Installing Xcode command line tools...', 'info');
try {
await this.runCommand('xcode-select', ['--install']);
this.log('✓ Xcode command line tools installation started', 'success');
this.log('Please complete the installation and run this script again', 'warning');
process.exit(0);
} catch (e) {
throw new Error('Failed to install Xcode command line tools. Please install manually.');
}
}
}
async generatePNGIcons() {
this.log('Generating PNG icons from SVG...', 'info');
// Create temp directory
if (fs.existsSync(this.tempDir)) {
fs.rmSync(this.tempDir, { recursive: true });
}
fs.mkdirSync(this.tempDir, { recursive: true });
// Generate app icons
this.log(' Generating app icons...', 'info');
for (const iconSpec of this.appIconSizes) {
const fileName = iconSpec.scale === 1
? `app_icon_${iconSpec.size}x${iconSpec.size}.png`
: `app_icon_${iconSpec.size}x${iconSpec.size}@${iconSpec.scale}x.png`;
const outputPath = path.join(this.tempDir, fileName);
const actualSize = iconSpec.size * iconSpec.scale;
this.log(` Generating ${fileName} (${actualSize}x${actualSize})...`, 'info');
try {
await this.runCommand('rsvg-convert', [
'--width', actualSize.toString(),
'--height', actualSize.toString(),
'--format', 'png',
'--output', outputPath,
this.appIconSvg
]);
this.log(`${fileName}`, 'success');
} catch (error) {
throw new Error(`Failed to generate ${fileName}: ${error.message}`);
}
}
}
async createAppIconsFile() {
this.log('Creating .icns file for macOS app bundle...', 'info');
const iconsetDir = path.join(this.tempDir, 'app-icon.iconset');
if (!fs.existsSync(iconsetDir)) {
fs.mkdirSync(iconsetDir, { recursive: true });
}
// Copy PNG files to iconset with proper naming
for (const iconSpec of this.appIconSizes) {
const sourceFileName = iconSpec.scale === 1
? `app_icon_${iconSpec.size}x${iconSpec.size}.png`
: `app_icon_${iconSpec.size}x${iconSpec.size}@${iconSpec.scale}x.png`;
const targetFileName = iconSpec.scale === 1
? `icon_${iconSpec.size}x${iconSpec.size}.png`
: `icon_${iconSpec.size}x${iconSpec.size}@${iconSpec.scale}x.png`;
const sourcePath = path.join(this.tempDir, sourceFileName);
const targetPath = path.join(iconsetDir, targetFileName);
fs.copyFileSync(sourcePath, targetPath);
}
// Create .icns file
const icnsPath = path.join(this.iconOutputDir, 'app-icon.icns');
await this.runCommand('iconutil', [
'--convert', 'icns',
'--output', icnsPath,
iconsetDir
]);
this.log(`✓ Created app-icon.icns: ${icnsPath}`, 'success');
// Clean up iconset directory
fs.rmSync(iconsetDir, { recursive: true, force: true });
}
async createDocumentIcons() {
this.log('Generating document type icons...', 'info');
const documentTypes = [
{ name: 'fbundle', filename: 'fbundle-icon.icns', svgSource: this.bundleIconSvg },
{ name: 'opat', filename: 'opat-icon.icns', svgSource: this.opatIconSvg }
];
for (const docType of documentTypes) {
this.log(` Creating ${docType.name} document icon...`, 'info');
const iconsetDir = path.join(this.tempDir, `${docType.name}-icon.iconset`);
if (!fs.existsSync(iconsetDir)) {
fs.mkdirSync(iconsetDir, { recursive: true });
}
// Generate document icons using the specific SVG for each type
for (const iconSpec of this.appIconSizes) {
const fileName = iconSpec.scale === 1
? `icon_${iconSpec.size}x${iconSpec.size}.png`
: `icon_${iconSpec.size}x${iconSpec.size}@${iconSpec.scale}x.png`;
const outputPath = path.join(iconsetDir, fileName);
const actualSize = iconSpec.size * iconSpec.scale;
try {
await this.runCommand('rsvg-convert', [
'--width', actualSize.toString(),
'--height', actualSize.toString(),
'--format', 'png',
'--output', outputPath,
docType.svgSource
]);
} catch (error) {
throw new Error(`Failed to generate ${docType.name} icon ${fileName}: ${error.message}`);
}
}
// Create .icns file
const icnsPath = path.join(this.iconOutputDir, docType.filename);
await this.runCommand('iconutil', [
'--convert', 'icns',
'--output', icnsPath,
iconsetDir
]);
this.log(` ✓ Created ${docType.filename}`, 'success');
// Clean up iconset directory
fs.rmSync(iconsetDir, { recursive: true, force: true });
}
}
cleanup() {
this.log('Cleaning up temporary files...', 'info');
if (fs.existsSync(this.tempDir)) {
fs.rmSync(this.tempDir, { recursive: true });
}
}
async generate() {
try {
this.log('Starting icon generation for 4DSTAR Bundle Manager...', 'info');
// Create output directory
if (!fs.existsSync(this.iconOutputDir)) {
fs.mkdirSync(this.iconOutputDir, { recursive: true });
}
// Check dependencies
this.checkDependencies();
// Generate PNG icons
await this.generatePNGIcons();
// Generate .icns files
await this.createAppIconsFile();
await this.createDocumentIcons();
// Cleanup
this.cleanup();
this.log('\n🎉 Icon generation completed successfully!', 'success');
this.log(`Generated icons in: ${this.iconOutputDir}`, 'info');
this.log('Files created:', 'info');
this.log(' - app-icon.icns (main app icon)', 'info');
this.log(' - fbundle-icon.icns (for .fbundle files)', 'info');
this.log(' - opat-icon.icns (for .opat files)', 'info');
return true;
} catch (error) {
this.log(`❌ Icon generation failed: ${error.message}`, 'error');
this.cleanup();
return false;
}
}
}
// Run icon generation if called directly
if (require.main === module) {
const generator = new IconGenerator();
generator.generate().then(success => {
process.exit(success ? 0 : 1);
}).catch(error => {
console.error('Icon generation failed:', error);
process.exit(1);
});
}
module.exports = { IconGenerator };