352 lines
11 KiB
JavaScript
352 lines
11 KiB
JavaScript
const { contextBridge, ipcRenderer } = require("electron");
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const { changeDpiBlob } = require("changedpi");
|
||
const { Canvas, loadImage } = require("skia-canvas");
|
||
const sharp = require("sharp");
|
||
|
||
//在主进程或渲染进程(需启用 nodeIntegration)中
|
||
// console.log("当前架构:", process.arch);
|
||
// 增加 sharp 的像素限制(根据需要调整)
|
||
// 默认限制是 268402689 像素 (~16384x16384)
|
||
// sharp.concurrency(1); // 降低并发以减少内存使用
|
||
// console.log(sharp)
|
||
// sharp.setMaxBuffer(1024 * 1024 * 500); // 增加缓冲区大小(500MB)
|
||
|
||
const commonApi = {
|
||
/**
|
||
* 将 Blob 转换为 Buffer
|
||
* @param {Blob} blob - 图片 Blob 数据
|
||
* @returns {Promise<Buffer>} 转换后的 Buffer
|
||
*/
|
||
blobToBuffer: async (blob) => {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onloadend = () => {
|
||
// FileReader 结果为 ArrayBuffer,需转换为 Buffer
|
||
const buffer = Buffer.from(reader.result);
|
||
resolve(buffer);
|
||
};
|
||
reader.onerror = reject;
|
||
reader.readAsArrayBuffer(blob);
|
||
});
|
||
},
|
||
/**
|
||
* 将 Blob 对象写入本地文件
|
||
* @param {Blob} blob - 要写入的 Blob 对象
|
||
* @param {string} filePath - 目标文件路径
|
||
* @returns {Promise<void>}
|
||
*/
|
||
writeBlobToFile: async (blob, filePath) => {
|
||
try {
|
||
// 将 Blob 转换为 ArrayBuffer
|
||
const arrayBuffer = await blob.arrayBuffer();
|
||
|
||
// 将 ArrayBuffer 转换为 Buffer
|
||
const buffer = Buffer.from(arrayBuffer);
|
||
|
||
// 写入文件
|
||
fs.writeFileSync(filePath, buffer);
|
||
|
||
// console.log(`Blob 已成功写入文件: ${filePath}`);
|
||
} catch (error) {
|
||
console.error("写入文件时发生错误:", error);
|
||
throw error; // 允许调用者捕获错误
|
||
}
|
||
},
|
||
// loadImage: async (path) => {
|
||
// const image = await new Promise((resolve, reject) => {
|
||
// const img = new Image();
|
||
// img.onload = () => resolve(img);
|
||
// img.onerror = reject;
|
||
// img.src = path;
|
||
// });
|
||
// return image;
|
||
// },
|
||
|
||
// setDpiMetadata: async (outputPath, dpi) => {
|
||
// // // 1. 用 Jimp 处理图像(像素尺寸不变)
|
||
// // const image = await Jimp.read(inputPath);
|
||
// // // (可选)进行缩放、绘制等操作
|
||
// // await image.write(outputPath);
|
||
|
||
// // 2. 用 exiftool 写入 DPI 元数据
|
||
// // console.log(exiftool, dpi);
|
||
// await exiftool.write(outputPath, {
|
||
// XResolution: dpi, // 水平 DPI
|
||
// YResolution: dpi, // 垂直 DPI
|
||
// ResolutionUnit: "inches", // 单位为英寸
|
||
// });
|
||
|
||
// // console.log(`已为 ${outputPath} 设置 DPI: ${dpi}`);
|
||
// await exiftool.end();
|
||
// },
|
||
};
|
||
const electronAPIInstance = {
|
||
// 读取文件功能
|
||
readFile: (filePath) => {
|
||
try {
|
||
return fs.readFileSync(filePath, "utf8");
|
||
} catch (err) {
|
||
console.error("读取文件失败:", err);
|
||
return null;
|
||
}
|
||
},
|
||
|
||
// 写入文件功能
|
||
writeFile: (filePath, content) => {
|
||
try {
|
||
// 确保目录存在
|
||
const dir = path.dirname(filePath);
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
fs.writeFileSync(filePath, content, "utf8");
|
||
return true;
|
||
} catch (err) {
|
||
console.error("写入文件失败:", err);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// mergeImages(blobs, filePath) {
|
||
// console.log("合并图片", blobs, filePath);
|
||
// },
|
||
|
||
// 通过 IPC 与主进程通信
|
||
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
|
||
|
||
/**
|
||
* 使用 Jimp 合并多张图片
|
||
* @param {Object} options 合并选项
|
||
* @param {string[]} options.blobs 图片Blob数组
|
||
* @param {string} options.outputPath 输出图片路径
|
||
* @param {string} [options.layout='horizontal'] 布局方式: horizontal/vertical/grid
|
||
* @param {number} [options.spacing=10] 图片间距(像素)
|
||
* @param {number} [options.gridColumns=2] 网格布局列数
|
||
* @param {number} [options.quality=90] 输出质量(0-100)
|
||
*/
|
||
combineImages: async (options) => {
|
||
// try {
|
||
// 默认配置
|
||
const config = {
|
||
layout: "horizontal",
|
||
spacing: 10,
|
||
gridColumns: 2,
|
||
quality: 90,
|
||
imageWidth: 100,
|
||
imageHeight: 100,
|
||
images: [],
|
||
quality: 1,
|
||
dpi: 72,
|
||
contentType: "image/png",
|
||
...options,
|
||
};
|
||
|
||
if (!config.images || config.images.length === 0) {
|
||
throw new Error("请提供至少一张图片的路径");
|
||
}
|
||
|
||
// // 加载所有图片
|
||
// const images = [];
|
||
// for (const blob of config.blobs) {
|
||
// try {
|
||
// // console.log(Jimp);
|
||
// const buf = await commonApi.blobToBuffer(blob);
|
||
// const image = await Jimp.read(buf);
|
||
// images.push({
|
||
// image,
|
||
// width: image.bitmap.width,
|
||
// height: image.bitmap.height,
|
||
// buf: buf,
|
||
// });
|
||
// // console.log(
|
||
// // `已加载图片: ${imgPath} (${image.bitmap.width}x${image.bitmap.height})`
|
||
// // );
|
||
// } catch (err) {
|
||
// console.error(`加载图片失败:`, err.message);
|
||
// }
|
||
// }
|
||
|
||
// if (images.length === 0) {
|
||
// throw new Error("没有成功加载任何图片");
|
||
// }
|
||
|
||
// 计算合成后图片的尺寸
|
||
var totalWidth, totalHeight;
|
||
const spacing = config.spacing;
|
||
const imageWidth = config.imageWidth;
|
||
const imageHeight = config.imageHeight;
|
||
// if (config.layout === "horizontal") {
|
||
// 水平排列
|
||
(totalHeight = imageHeight), //Math.max(...images.map((img) => img.height)) + spacing * 2;
|
||
(totalWidth = (imageWidth + spacing) * config.images.length);
|
||
// images.reduce((sum, img) => sum + img.width, 0) +
|
||
// spacing * (images.length + 1);
|
||
// }
|
||
// else if (config.layout === "vertical") {
|
||
// // 垂直排列
|
||
// totalWidth = Math.max(...images.map((img) => img.width)) + spacing * 2;
|
||
// totalHeight =
|
||
// images.reduce((sum, img) => sum + img.height, 0) +
|
||
// spacing * (images.length + 1);
|
||
// } else {
|
||
// // 网格排列
|
||
// const gridColumns = Math.max(1, config.gridColumns);
|
||
// const rows = Math.ceil(images.length / gridColumns);
|
||
|
||
// // 计算每列最大宽度
|
||
// const columnWidths = [];
|
||
// for (let col = 0; col < gridColumns; col++) {
|
||
// let maxWidth = 0;
|
||
// for (let i = col; i < images.length; i += gridColumns) {
|
||
// maxWidth = Math.max(maxWidth, images[i].width);
|
||
// }
|
||
// columnWidths.push(maxWidth);
|
||
// }
|
||
|
||
// // 计算每行最大高度
|
||
// const rowHeights = [];
|
||
// for (let row = 0; row < rows; row++) {
|
||
// let maxHeight = 0;
|
||
// for (let col = 0; col < gridColumns; col++) {
|
||
// const index = row * gridColumns + col;
|
||
// if (index < images.length) {
|
||
// maxHeight = Math.max(maxHeight, images[index].height);
|
||
// }
|
||
// }
|
||
// rowHeights.push(maxHeight);
|
||
// }
|
||
|
||
// // 总尺寸
|
||
// totalWidth =
|
||
// columnWidths.reduce((sum, width) => sum + width, 0) +
|
||
// spacing * (gridColumns + 1);
|
||
// totalHeight =
|
||
// rowHeights.reduce((sum, height) => sum + height, 0) +
|
||
// spacing * (rows + 1);
|
||
// }
|
||
|
||
// 创建空白画布(透明背景)
|
||
const canvas = new Canvas(totalWidth, totalHeight);
|
||
const ctx = canvas.getContext("2d");
|
||
|
||
// 根据布局绘制图片
|
||
// if (config.layout === "horizontal") {
|
||
var currentX = 0;
|
||
|
||
for (var i = 0; i < config.images.length; i++) {
|
||
const img = await loadImage(config.images[i]);
|
||
// console.log(img.height, imageHeight);
|
||
ctx.drawImage(
|
||
img,
|
||
0,
|
||
0,
|
||
img.width,
|
||
img.height,
|
||
currentX,
|
||
0,
|
||
imageWidth,
|
||
imageHeight
|
||
);
|
||
currentX += imageWidth + spacing;
|
||
}
|
||
// for (const img of images) {
|
||
// await combined.composite(img.image, currentX, currentY);
|
||
// currentX += img.width + spacing;
|
||
// }
|
||
// } else if (config.layout === "vertical") {
|
||
// let currentY = spacing;
|
||
// const currentX = spacing;
|
||
|
||
// for (const img of images) {
|
||
// await combined.composite(img.image, currentX, currentY);
|
||
// currentY += img.height + spacing;
|
||
// }
|
||
// } else {
|
||
// // 网格布局
|
||
// let currentX = spacing;
|
||
// let currentY = spacing;
|
||
// let currentCol = 0;
|
||
// const gridColumns = Math.max(1, config.gridColumns);
|
||
|
||
// for (let i = 0; i < images.length; i++) {
|
||
// const img = images[i];
|
||
// await combined.composite(img.image, currentX, currentY);
|
||
|
||
// currentCol++;
|
||
// if (currentCol >= gridColumns) {
|
||
// currentCol = 0;
|
||
// currentX = spacing;
|
||
|
||
// // 计算当前行高度
|
||
// let rowHeight = 0;
|
||
// const rowStart = i - (gridColumns - 1);
|
||
// const rowEnd = Math.min(i, images.length - 1);
|
||
// for (let j = rowStart; j <= rowEnd; j++) {
|
||
// rowHeight = Math.max(rowHeight, images[j].height);
|
||
// }
|
||
|
||
// currentY += rowHeight + spacing;
|
||
// } else {
|
||
// // 计算当前列宽度
|
||
// let colWidth = 0;
|
||
// for (let j = currentCol; j < images.length; j += gridColumns) {
|
||
// colWidth = Math.max(colWidth, images[j].width);
|
||
// }
|
||
// currentX += colWidth + spacing;
|
||
// }
|
||
// }
|
||
// }
|
||
|
||
// 确保输出目录存在
|
||
const outputDir = path.dirname(config.outputPath);
|
||
if (!fs.existsSync(outputDir)) {
|
||
fs.mkdirSync(outputDir, { recursive: true });
|
||
}
|
||
|
||
// 保存合并后的图片
|
||
// 根据文件扩展名自动选择格式
|
||
// const extension = path.extname(config.outputPath).toLowerCase();
|
||
|
||
// if (extension === ".png") {
|
||
// await combined.write(config.outputPath);
|
||
// } else {
|
||
// // JPG 或其他格式
|
||
// await combined.write(config.outputPath, { quality: config.quality });
|
||
// }
|
||
|
||
return new Promise(async (resolve, reject) => {
|
||
const buffer = await canvas.toBuffer(config.contentType, {
|
||
quality: config.quality,
|
||
});
|
||
// 3. 使用 sharp 处理图像并设置 DPI
|
||
await sharp(buffer, { limitInputPixels: 1024 * 1024 * 1024 * 5 }) //5G
|
||
.withMetadata({
|
||
density: config.dpi, // 设置 DPI
|
||
xres: config.dpi, // 水平分辨率
|
||
yres: config.dpi, // 垂直分辨率
|
||
})
|
||
.toFile(config.outputPath);
|
||
// // 写入文件
|
||
// fs.writeFileSync(config.outputPath, buffer);
|
||
|
||
// // 将 Buffer 转换为 Blob
|
||
// var blob = new Blob([buffer], { type: config.contentType });
|
||
// console.log("config.dpi:", config.dpi, "blob:", blob);
|
||
// blob = await changeDpiBlob(blob, config.dpi);
|
||
// console.log("blob:", blob);
|
||
// await commonApi.writeBlobToFile(blob, config.outputPath);
|
||
// console.log(`图片合并完成,已保存至: ${config.outputPath}`);
|
||
resolve(true);
|
||
});
|
||
// } catch (err) {
|
||
// console.error("图片合并失败:", err.message);
|
||
// return false;
|
||
// }
|
||
},
|
||
};
|
||
|
||
// 通过 contextBridge 安全地暴露 API 给渲染进程
|
||
contextBridge.exposeInMainWorld("electronAPI", electronAPIInstance);
|