Electron开发-创建Electron项目及安装Sharp模块处理图片

前言

Sharp模块作为Node.js生态中重要的图像处理库,这里我们使用Electron+Sharp开发图片处理工具。

Sharp v0.33.x要求Node.js版本满足^18.17.0 || ^20.3.0 || >=21.0.0

可通过以下命令检查版本:

1
2
node -v
npm view sharp dist-tags.latest

创建项目

切换NodeJS版本

1
nvm use 18.20.4

创建项目

1
npm create electron-vite@latest z-img-tools

编码设置

日志乱码

package.json执行的命令前添加chcp 65001 >nul &&

1
2
3
4
5
{
"scripts": {
"dev": "chcp 65001 >nul && vite"
},
}

设置全局样式

src/style.css

1
2
3
4
5
#app {
width: 100vw;
height: 100vh;
overflow: hidden;
}

安装Less

1
npm install -D less

Electron设置

不显示菜单栏

1
2
3
import {Menu} from 'electron'
/*隐藏electron创听的菜单栏*/
Menu.setApplicationMenu(null)

显示调试窗口

1
win.webContents.openDevTools({mode: 'detach'});

图片处理库

添加依赖

1
2
# 确保使用最新版Sharp
npm install sharp@latest

图片处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');

// 获取指定目录下的所有 PNG 文件
function getImgFiles(dir: string): string[] {
const filterImg = [".png", ".jpg", ".jpeg", ".bmp"]
const files = fs.readdirSync(dir);
return files
.filter((file: string) => filterImg.indexOf(path.extname(file).toLowerCase()) > -1)
.map((file: string) => path.resolve(dir, file)); // 使用 resolve 得到绝对路径
}

// 获取图片尺寸
async function getImageSize(filePath: String) {
try {
const metadata = await sharp(filePath).metadata();
return {
width: metadata.width,
height: metadata.height,
format: metadata.format, // 'jpeg', 'png', etc.
};
} catch (err) {
return null;
}
}

// 判断文件是否存在
function fileExists(filePath: string) {
try {
fs.accessSync(filePath);
return true;
} catch (err) {
return false;
}
}

// 合并图片
async function mergeImagesInDir(dir: string) {
let files = getImgFiles(dir);
if (files.length == 0) {
return ""
}
let sizeArr = []
let maxWidth = 0;
let maxHeight = 0;
for (let i = 0; i < files.length; i++) {
let filePath = files[i];
let imgSize = await getImageSize(filePath);
if (imgSize != null) {
if (imgSize.width > maxWidth) {
maxWidth = imgSize.width;
}
if (imgSize.height > maxHeight) {
maxHeight = imgSize.height;
}

sizeArr.push(imgSize)
}
}
maxWidth = parseInt(maxWidth + "")
maxHeight = parseInt(maxHeight + "")

if (sizeArr.length != files.length) {
return "";
}

let cols = Math.ceil(Math.sqrt(files.length))
// 数量少于8张就放一行显示
if (files.length <= 8) {
cols = files.length
}
let rows = parseInt(Math.ceil(files.length / cols) + "")
let totalWidth = maxWidth * cols;
let totalHeight = maxHeight * rows;
let outputDir = path.join(dir, '_z_merge');
if (!fileExists(outputDir)) {
fs.mkdirSync(outputDir);
}
let outputPath = path.join(outputDir, 'z_merge_output.png');
try {
if (fileExists(outputPath)) {
fs.unlinkSync(outputPath);
}
} catch (e) {
return ""
}

// 创建一个透明背景的画布
const canvas = sharp({
create: {
width: totalWidth,
height: totalHeight,
channels: 4,
background: {r: 0, g: 0, b: 0, alpha: 0}
}
});

const overlays = [];
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
let currIndex = i * cols + j;
if (currIndex < files.length) {
let filePath = files[currIndex];
let imgSize = sizeArr[currIndex];
let x = j * maxWidth;
let y = i * maxHeight;
let left = parseInt((x + (maxWidth - imgSize.width) / 2) + "")
overlays.push({
input: filePath,
left: left,
top: y + (maxHeight - imgSize.height),
});
}
}
}

await canvas
.composite(overlays)
.png()
.toFile(outputPath);
return outputPath;
}

export {getImgFiles, getImageSize, mergeImagesInDir}

监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {ipcMain} from "electron";
import {mergeImagesInDir} from "./utils/z_img_utils"

ipcMain.handle('merge_img', async (_, arg) => {
return await mergeImagesInDir(arg)
});

import {shell} from 'electron'

ipcMain.on('open_folder', async (_, arg) => {
console.info("arg", arg)
// 打开指定文件夹
await shell.openPath(arg);
});

显示本地图片

主进程

窗口设置

1
2
3
4
5
6
7
8
9
10
11
12
13
win = new BrowserWindow({
width: 1000,
height: 600,
icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
title: "图片工具",
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
webSecurity: false,
contextIsolation: true,
webviewTag: true
},
})

主要是webSecurity设置为false

开发模式不显示警告

1
2
3
4
// 仅开发环境关闭安全警告
if (process.env.NODE_ENV === 'development') {
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
}

渲染进程

1
2
3
4
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self';img-src 'self' file: data:;script-src 'self';style-src 'self' 'unsafe-inline';"
/>

加载的图片file:///

1
<img :src="resultPath" alt="">

常见错误

npm error RequestError: Hostname/IP does not match certificate’s altnames

设置NPM镜像

1
npm config edit

添加

1
2
3
4
5
registry=https://registry.npmmirror.com/
disturl=http://npmmirror.com/mirrors/node/
electron_mirror=http://npmmirror.com/mirrors/electron/
python_mirror=http://npmmirror.com/mirrors/python/
sass_binary_site=http://npmmirror.com/mirrors/node-sass/