This commit refactors the icon and logo generation process by replacing multiple dependencies with ImageMagick. This simplifies the build process and enhances maintainability. Key changes: - Remove unnecessary icon files for macOS (.icns) and Linux (size-specific PNGs). Electron-builder can now auto-generate these from a single `logo.png` starting from version 19.54.0, see: - electron-userland/electron-builder#1682 - electron-userland/electron-builder#2533 - Retain `ico` generation with multiple sizes to fix pixelated/bad looking icons on Windows, see: - electron-userland/electron-builder#7328 - electron-userland/electron-builder#3867 - Replaced `svgexport`, `icon-gen`, and `electron-icon-builder` dependencies with ImageMagick, addressing issues with outdated dependencies and unreliable CI/CD builds. - Move electron-builder build resources to `src/presentation/electron/build` for better project structure. - Improve `electron-builder` configuration file by making it importable/reusable without prebuilding the Electron application.
235 lines
7.4 KiB
JavaScript
235 lines
7.4 KiB
JavaScript
/**
|
|
* Description:
|
|
* This script updates the logo images across the project based on the primary
|
|
* logo file ('img/logo.svg' file).
|
|
*
|
|
* It handles the creation and update of various icon sizes for different purposes,
|
|
* including desktop launcher icons, tray icons, and web favicons from a single source
|
|
* SVG logo file.
|
|
*
|
|
* Usage:
|
|
* node ./scripts/logo-update.js
|
|
*
|
|
* Notes:
|
|
* ImageMagick must be installed and accessible in the system's PATH
|
|
*/
|
|
|
|
import { resolve, join, dirname } from 'node:path';
|
|
import { stat } from 'node:fs/promises';
|
|
import { spawn } from 'node:child_process';
|
|
import { URL, fileURLToPath } from 'node:url';
|
|
import electronBuilderConfig from '../electron-builder.cjs';
|
|
|
|
class ImageAssetPaths {
|
|
constructor(currentScriptDirectory) {
|
|
const projectRoot = resolve(currentScriptDirectory, '../');
|
|
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
|
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
|
this.electronBuildResourcesDirectory = electronBuilderConfig.directories.buildResources;
|
|
}
|
|
|
|
get electronTrayIconFile() {
|
|
return join(this.publicDirectory, 'icon.png');
|
|
}
|
|
|
|
get webFaviconFile() {
|
|
return join(this.publicDirectory, 'favicon.ico');
|
|
}
|
|
|
|
toString() {
|
|
return `Source image: ${this.sourceImage}`
|
|
+ `\nPublic directory: ${this.publicDirectory}`
|
|
+ `\n\t Electron tray icon file: ${this.electronTrayIconFile}`
|
|
+ `\n\t Web favicon file: ${this.webFaviconFile}`
|
|
+ `\nElectron build directory: ${this.electronBuildResourcesDirectory}`;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const paths = new ImageAssetPaths(getCurrentScriptDirectory());
|
|
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
|
const convertCommand = await findAvailableImageMagickCommand();
|
|
await generateDesktopAndTrayIcons(
|
|
paths.sourceImage,
|
|
paths.electronTrayIconFile,
|
|
convertCommand,
|
|
);
|
|
await generateWebFavicon(
|
|
paths.sourceImage,
|
|
paths.webFaviconFile,
|
|
convertCommand,
|
|
);
|
|
await generateDesktopIcons(
|
|
paths.sourceImage,
|
|
paths.electronBuildResourcesDirectory,
|
|
convertCommand,
|
|
);
|
|
console.log('🎉 (Re)created icons successfully.');
|
|
}
|
|
|
|
async function generateDesktopAndTrayIcons(sourceImage, targetFile, convertCommand) {
|
|
// Reference: https://web.archive.org/web/20240502124306/https://www.electronjs.org/docs/latest/api/tray
|
|
console.log(`Updating desktop launcher and tray icon at ${targetFile}.`);
|
|
await ensureFileExists(sourceImage);
|
|
await ensureParentFolderExists(targetFile);
|
|
await convertFromSvgToPng(
|
|
convertCommand,
|
|
sourceImage,
|
|
targetFile,
|
|
'512x512',
|
|
);
|
|
}
|
|
|
|
async function generateWebFavicon(sourceImage, faviconFilePath, convertCommand) {
|
|
console.log(`Updating favicon at ${faviconFilePath}.`);
|
|
await ensureFileExists(sourceImage);
|
|
await ensureParentFolderExists(faviconFilePath);
|
|
await convertFromSvgToIco(
|
|
convertCommand,
|
|
sourceImage,
|
|
faviconFilePath,
|
|
[16, 24, 32, 48, 64, 128, 256],
|
|
);
|
|
}
|
|
|
|
async function generateDesktopIcons(sourceImage, electronBuildResourcesDirectory, convertCommand) {
|
|
console.log(`Creating Electron icon files to ${electronBuildResourcesDirectory}.`);
|
|
// Reference: https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html
|
|
await ensureFolderExists(electronBuildResourcesDirectory);
|
|
await ensureFileExists(sourceImage);
|
|
const electronMainIconFile = join(electronBuildResourcesDirectory, 'icon.png');
|
|
await convertFromSvgToPng(
|
|
convertCommand,
|
|
sourceImage,
|
|
electronMainIconFile,
|
|
'1024x1024', // Should be at least 512x512
|
|
);
|
|
// Relying on `electron-builder`s conversion from png to ico results in pixelated look on Windows
|
|
// 10 and 11 according to tests, see:
|
|
// - https://web.archive.org/web/20240502114650/https://github.com/electron-userland/electron-builder/issues/7328
|
|
// - https://web.archive.org/web/20240502115448/https://github.com/electron-userland/electron-builder/issues/3867
|
|
const electronWindowsIconFile = join(electronBuildResourcesDirectory, 'icon.ico');
|
|
await convertFromSvgToIco(
|
|
convertCommand,
|
|
sourceImage,
|
|
electronWindowsIconFile,
|
|
[16, 24, 32, 48, 64, 128, 256],
|
|
);
|
|
}
|
|
|
|
async function ensureFileExists(filePath) {
|
|
const path = await stat(filePath);
|
|
if (!path.isFile()) {
|
|
throw new Error(`Not a file: ${filePath}`);
|
|
}
|
|
}
|
|
|
|
async function ensureFolderExists(folderPath) {
|
|
if (!folderPath) {
|
|
throw new Error('Path is missing');
|
|
}
|
|
const path = await stat(folderPath);
|
|
if (!path.isDirectory()) {
|
|
throw new Error(`Not a directory: ${folderPath}`);
|
|
}
|
|
}
|
|
|
|
function ensureParentFolderExists(filePath) {
|
|
return ensureFolderExists(dirname(filePath));
|
|
}
|
|
|
|
const BaseImageMagickConvertArguments = Object.freeze([
|
|
'-background none', // Transparent, so they do not get filled with white.
|
|
'-strip', // Strip metadata.
|
|
'-gravity Center', // Center the image when there's empty space
|
|
]);
|
|
|
|
async function convertFromSvgToIco(
|
|
convertCommand,
|
|
inputFile,
|
|
outputFile,
|
|
sizes,
|
|
) {
|
|
await runCommand(
|
|
convertCommand,
|
|
...BaseImageMagickConvertArguments,
|
|
`-density ${Math.max(...sizes).toString()}`, // High enough for sharpness
|
|
`-define icon:auto-resize=${sizes.map((s) => s.toString()).join(',')}`, // Automatically store multiple sizes in an ico image
|
|
'-compress None',
|
|
inputFile,
|
|
outputFile,
|
|
);
|
|
}
|
|
|
|
async function convertFromSvgToPng(
|
|
convertCommand,
|
|
inputFile,
|
|
outputFile,
|
|
size = undefined,
|
|
) {
|
|
await runCommand(
|
|
convertCommand,
|
|
...BaseImageMagickConvertArguments,
|
|
...(size === undefined ? [] : [
|
|
`-resize ${size}`,
|
|
`-density ${size}`, // High enough for sharpness
|
|
]),
|
|
inputFile,
|
|
outputFile,
|
|
);
|
|
}
|
|
|
|
async function runCommand(...args) {
|
|
const command = args.join(' ');
|
|
console.log(`Running command: ${command}`);
|
|
await new Promise((resolve, reject) => {
|
|
const process = spawn(command, { shell: true });
|
|
process.stdout.on('data', (stdout) => {
|
|
console.log(stdout.toString());
|
|
});
|
|
process.stderr.on('data', (stderr) => {
|
|
console.error(stderr.toString());
|
|
});
|
|
process.on('error', (err) => {
|
|
reject(err);
|
|
});
|
|
process.on('close', (exitCode) => {
|
|
if (exitCode !== 0) {
|
|
reject(new Error(`Process exited with non-zero exit code: ${exitCode}`));
|
|
} else {
|
|
resolve();
|
|
}
|
|
process.stdin.end();
|
|
});
|
|
});
|
|
}
|
|
|
|
function getCurrentScriptDirectory() {
|
|
return fileURLToPath(new URL('.', import.meta.url));
|
|
}
|
|
|
|
async function findAvailableImageMagickCommand() {
|
|
// Reference: https://web.archive.org/web/20240502120041/https://imagemagick.org/script/convert.php
|
|
const potentialBaseCommands = [
|
|
'convert', // Legacy command, usually available on Linux/macOS installations
|
|
'magick convert', // Newer command, available on Windows installations
|
|
];
|
|
for (const baseCommand of potentialBaseCommands) {
|
|
const testCommand = `${baseCommand} -version`;
|
|
try {
|
|
await runCommand(testCommand); // eslint-disable-line no-await-in-loop
|
|
console.log(`Confirmed: ImageMagick command '${baseCommand}' is available and operational.`);
|
|
return baseCommand;
|
|
} catch (err) {
|
|
console.log(`Error: The command '${baseCommand}' is not found or failed to execute. Detailed error: ${err.message}"`);
|
|
}
|
|
}
|
|
throw new Error([
|
|
'Unable to locate any operational ImageMagick command.',
|
|
`Attempted commands were: ${potentialBaseCommands.join(', ')}.`,
|
|
'Please ensure ImageMagick is correctly installed and accessible.',
|
|
].join('\n'));
|
|
}
|
|
|
|
await main();
|