#!/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 };