Improve desktop icon quality and generation

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.
This commit is contained in:
undergroundwires
2024-05-04 12:09:42 +02:00
parent 813d820b85
commit ab25e0a066
22 changed files with 191 additions and 3977 deletions

View File

@@ -15,6 +15,10 @@ jobs:
-
name: Checkout
uses: actions/checkout@v4
-
name: Install ImageMagick on macOS
if: matrix.os == 'macos'
run: brew install imagemagick
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -1,5 +0,0 @@
# build
This folder contains files that are used by Electron to serve the desktop version.
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

View File

@@ -23,8 +23,10 @@ The presentation layer uses an event-driven architecture for bidirectional react
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
- [`/main/` **`index.ts`**](./../src/presentation/electron/main/index.ts): Main entry for Electron, managing application windows and lifecycle events.
- [`/preload/` **`index.ts`**](./../src/presentation/electron/preload/index.ts): Script executed before the renderer, securing Node.js features for renderer use.
- [**`/shared/`**](./../src/presentation/electron/shared/): Shared logic between different Electron processes.
- [**`/build/`**](./../src/presentation/electron/build/): `electron-builder` build resources directory, [README.md](./../src/presentation/electron/build/README.md).
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-template-curly-in-string */
const { join } = require('node:path');
const { readdirSync } = require('fs');
const { join, resolve } = require('node:path');
const { readdirSync, existsSync } = require('node:fs');
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
/**
@@ -17,6 +17,7 @@ module.exports = {
},
directories: {
output: electronBundled,
buildResources: resolvePathFromProjectRoot('src/presentation/electron/build'),
},
extraMetadata: {
main: findMainEntryFile(
@@ -53,10 +54,18 @@ module.exports = {
* Finds by accommodating different JS file extensions and module formats.
*/
function findMainEntryFile(parentDirectory) {
const files = readdirSync(parentDirectory);
const absoluteParentDirectory = resolvePathFromProjectRoot(parentDirectory);
if (!existsSync(absoluteParentDirectory)) {
return null; // Avoid disrupting other processes such `npm install`.
}
const files = readdirSync(absoluteParentDirectory);
const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
if (!entryFile) {
throw new Error(`Main entry file not found in ${parentDirectory}.`);
throw new Error(`Main entry file not found in ${absoluteParentDirectory}.`);
}
return join(parentDirectory, entryFile);
}
function resolvePathFromProjectRoot(pathSegment) {
return resolve(__dirname, pathSegment);
}

3920
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -62,13 +62,11 @@
"electron": "^29.3.0",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-vite": "^2.1.0",
"eslint": "8.57.0",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-vue": "^9.25.0",
"eslint-plugin-vuejs-accessibility": "^2.2.1",
"icon-gen": "^4.0.0",
"jsdom": "^24.0.0",
"markdownlint-cli": "^0.39.0",
"postcss": "^8.4.38",
@@ -78,7 +76,6 @@
"remark-validate-links": "^13.0.1",
"sass": "^1.75.0",
"start-server-and-test": "^2.0.3",
"svgexport": "^0.4.2",
"terser": "^5.30.3",
"tslib": "^2.6.2",
"typescript": "^5.4.5",

View File

@@ -2,94 +2,119 @@
* 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
* node ./scripts/logo-update.js
*
* Notes:
* ImageMagick must be installed and accessible in the system's PATH
*/
import { resolve, join } from 'node:path';
import { rm, mkdtemp, stat } from 'node:fs/promises';
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 Paths {
constructor(selfDirectory) {
const projectRoot = resolve(selfDirectory, '../');
class ImageAssetPaths {
constructor(currentScriptDirectory) {
const projectRoot = resolve(currentScriptDirectory, '../');
this.sourceImage = join(projectRoot, 'img/logo.svg');
this.publicDirectory = join(projectRoot, 'src/presentation/public');
this.electronBuildDirectory = join(projectRoot, 'build');
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}\n`
+ `Public directory: ${this.publicDirectory}\n`
+ `Electron build directory: ${this.electronBuildDirectory}`;
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 Paths(getCurrentScriptDirectory());
const paths = new ImageAssetPaths(getCurrentScriptDirectory());
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
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 updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
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 ensureFolderExists(publicFolder);
const electronTrayIconFile = join(publicFolder, 'icon.png');
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
await runCommand(
'npx',
'svgexport',
await ensureParentFolderExists(targetFile);
await convertFromSvgToPng(
convertCommand,
sourceImage,
electronTrayIconFile,
targetFile,
'512x512',
);
}
async function updateWebFavicon(sourceImage, faviconFolder) {
console.log('Updating favicon');
async function generateWebFavicon(sourceImage, faviconFilePath, convertCommand) {
console.log(`Updating favicon at ${faviconFilePath}.`);
await ensureFileExists(sourceImage);
await ensureFolderExists(faviconFolder);
await runCommand(
'npx',
'icon-gen',
`--input ${sourceImage}`,
`--output ${faviconFolder}`,
'--ico',
'--ico-name \'favicon\'',
'--report',
await ensureParentFolderExists(faviconFilePath);
await convertFromSvgToIco(
convertCommand,
sourceImage,
faviconFilePath,
[16, 24, 32, 48, 64, 128, 256],
);
}
async function updateDesktopIcons(sourceImage, electronIconsDir) {
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);
await ensureFolderExists(electronIconsDir);
const temporaryDir = await mkdtemp('icon-');
const temporaryPngFile = join(temporaryDir, 'icon.png');
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
await runCommand(
'npx',
'svgexport',
const electronMainIconFile = join(electronBuildResourcesDirectory, 'icon.png');
await convertFromSvgToPng(
convertCommand,
sourceImage,
temporaryPngFile,
'1024:1024',
electronMainIconFile,
'1024x1024', // Should be at least 512x512
);
console.log(`Creating electron icons to ${electronIconsDir}.`);
await runCommand(
'npx',
'electron-icon-builder',
`--input="${temporaryPngFile}"`,
`--output="${electronIconsDir}"`,
'--flatten',
// 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],
);
console.log('Cleaning up temporary directory.');
await rm(temporaryDir, { recursive: true, force: true });
}
async function ensureFileExists(filePath) {
@@ -100,12 +125,60 @@ async function ensureFileExists(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}`);
@@ -135,4 +208,27 @@ 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();

View File

@@ -0,0 +1,13 @@
# build
This folder is the *build resources directory* [1] and contains files used by electron-build to
create the desktop version of the application.
Generate icons from the main logo file.
Do not modify these icons manually.
For more details, see the [related documentation](./../../../../img/README.md).
The electron-builder [1] uses these files, as specified in the [configuration file](./../../../../electron-builder.cjs).
For more information on icons in electron-builder,
visit [Icons - electron-builder | www.electron.build](https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 24 KiB