/**
|
* 基于 Web API 的图片处理器
|
* 完全不依赖外部库,使用浏览器原生 API
|
* 适用于 Electron 渲染进程
|
*/
|
|
class WebImageProcessor {
|
/**
|
* 使用 Canvas API 压缩图片
|
* @param {File|Blob|string} input - 输入图片
|
* @param {Object} options - 压缩选项
|
* @returns {Promise<Blob>} - 压缩后的图片
|
*/
|
static async compressImage(input, options = {}) {
|
const {
|
maxWidth = 1920,
|
maxHeight = 1080,
|
quality = 0.8,
|
format = 'image/jpeg'
|
} = options;
|
|
// 1. 加载图片
|
const img = await this.loadImage(input);
|
|
// 2. 计算新尺寸
|
const { width, height } = this.calculateDimensions(
|
img.width,
|
img.height,
|
maxWidth,
|
maxHeight
|
);
|
|
// 3. 创建 Canvas 并绘制
|
const canvas = document.createElement('canvas');
|
canvas.width = width;
|
canvas.height = height;
|
|
const ctx = canvas.getContext('2d');
|
|
// 设置高质量缩放
|
ctx.imageSmoothingEnabled = true;
|
ctx.imageSmoothingQuality = 'high';
|
|
// 绘制图片
|
ctx.drawImage(img, 0, 0, width, height);
|
|
// 4. 导出压缩后的图片
|
return new Promise((resolve) => {
|
canvas.toBlob(
|
(blob) => resolve(blob),
|
format,
|
quality
|
);
|
});
|
}
|
|
/**
|
* 生成缩略图
|
*/
|
static async generateThumbnail(input, options = {}) {
|
const {
|
width = 200,
|
height = 200,
|
quality = 0.7,
|
fit = 'cover' // 'cover' | 'contain' | 'fill'
|
} = options;
|
|
const img = await this.loadImage(input);
|
|
const canvas = document.createElement('canvas');
|
canvas.width = width;
|
canvas.height = height;
|
|
const ctx = canvas.getContext('2d');
|
|
if (fit === 'cover') {
|
// 裁剪以填充
|
const scale = Math.max(width / img.width, height / img.height);
|
const scaledWidth = img.width * scale;
|
const scaledHeight = img.height * scale;
|
const x = (width - scaledWidth) / 2;
|
const y = (height - scaledHeight) / 2;
|
|
ctx.drawImage(img, x, y, scaledWidth, scaledHeight);
|
} else if (fit === 'contain') {
|
// 保持比例,可能有空白
|
const scale = Math.min(width / img.width, height / img.height);
|
const scaledWidth = img.width * scale;
|
const scaledHeight = img.height * scale;
|
const x = (width - scaledWidth) / 2;
|
const y = (height - scaledHeight) / 2;
|
|
// 填充背景
|
ctx.fillStyle = '#f0f0f0';
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.drawImage(img, x, y, scaledWidth, scaledHeight);
|
} else {
|
// 拉伸填充
|
ctx.drawImage(img, 0, 0, width, height);
|
}
|
|
return new Promise((resolve) => {
|
canvas.toBlob(
|
(blob) => resolve(blob),
|
'image/jpeg',
|
quality
|
);
|
});
|
}
|
|
/**
|
* 加载图片
|
*/
|
static async loadImage(input) {
|
return new Promise((resolve, reject) => {
|
const img = new Image();
|
|
img.onload = () => resolve(img);
|
img.onerror = reject;
|
|
if (input instanceof File || input instanceof Blob) {
|
img.src = URL.createObjectURL(input);
|
} else if (typeof input === 'string') {
|
img.src = input;
|
} else {
|
reject(new Error('不支持的输入类型'));
|
}
|
});
|
}
|
|
/**
|
* 计算新尺寸
|
*/
|
static calculateDimensions(origWidth, origHeight, maxWidth, maxHeight) {
|
if (origWidth <= maxWidth && origHeight <= maxHeight) {
|
return { width: origWidth, height: origHeight };
|
}
|
|
const ratio = Math.min(maxWidth / origWidth, maxHeight / origHeight);
|
|
return {
|
width: Math.round(origWidth * ratio),
|
height: Math.round(origHeight * ratio)
|
};
|
}
|
|
/**
|
* 批量处理图片
|
*/
|
static async batchCompress(files, options = {}) {
|
const results = [];
|
|
for (const file of files) {
|
try {
|
const compressed = await this.compressImage(file, options);
|
results.push({
|
success: true,
|
original: file,
|
compressed,
|
reduction: ((file.size - compressed.size) / file.size * 100).toFixed(2)
|
});
|
} catch (error) {
|
results.push({
|
success: false,
|
original: file,
|
error: error.message
|
});
|
}
|
}
|
|
return results;
|
}
|
|
/**
|
* 转换图片格式
|
*/
|
static async convertFormat(input, targetFormat = 'image/webp', quality = 0.9) {
|
const img = await this.loadImage(input);
|
|
const canvas = document.createElement('canvas');
|
canvas.width = img.width;
|
canvas.height = img.height;
|
|
const ctx = canvas.getContext('2d');
|
ctx.drawImage(img, 0, 0);
|
|
return new Promise((resolve) => {
|
canvas.toBlob(
|
(blob) => resolve(blob),
|
targetFormat,
|
quality
|
);
|
});
|
}
|
|
/**
|
* 获取图片信息
|
*/
|
static async getImageInfo(input) {
|
const img = await this.loadImage(input);
|
|
return {
|
width: img.width,
|
height: img.height,
|
aspectRatio: (img.width / img.height).toFixed(2),
|
size: input.size || 0,
|
type: input.type || 'unknown'
|
};
|
}
|
|
/**
|
* 智能压缩(根据图片大小自动调整)
|
*/
|
static async smartCompress(input, targetSizeKB = 500) {
|
const originalSize = input.size || 0;
|
const targetSize = targetSizeKB * 1024;
|
|
// 如果已经小于目标大小,直接返回
|
if (originalSize <= targetSize) {
|
return input;
|
}
|
|
// 估算压缩质量
|
let quality = Math.min(targetSize / originalSize, 0.9);
|
let compressed = await this.compressImage(input, { quality });
|
|
// 二分查找最佳质量
|
let minQuality = 0.1;
|
let maxQuality = quality;
|
let attempts = 0;
|
const maxAttempts = 5;
|
|
while (attempts < maxAttempts) {
|
if (compressed.size <= targetSize) {
|
// 如果小于目标,尝试提高质量
|
minQuality = quality;
|
} else {
|
// 如果大于目标,降低质量
|
maxQuality = quality;
|
}
|
|
quality = (minQuality + maxQuality) / 2;
|
compressed = await this.compressImage(input, { quality });
|
|
// 如果接近目标大小(±10%),停止
|
if (Math.abs(compressed.size - targetSize) / targetSize < 0.1) {
|
break;
|
}
|
|
attempts++;
|
}
|
|
return compressed;
|
}
|
}
|
|
// 用于主进程的包装器
|
class MainProcessImageHandler {
|
/**
|
* 在主进程中处理图片(使用 Canvas 库)
|
*/
|
static async processInMainProcess(imagePath, outputPath, options = {}) {
|
const fs = require('fs').promises;
|
const { nativeImage } = require('electron');
|
|
try {
|
// 读取图片
|
const imageBuffer = await fs.readFile(imagePath);
|
const image = nativeImage.createFromBuffer(imageBuffer);
|
|
// 获取尺寸
|
const { width: origWidth, height: origHeight } = image.getSize();
|
|
// 计算新尺寸
|
const maxWidth = options.maxWidth || 1920;
|
const maxHeight = options.maxHeight || 1080;
|
const scale = Math.min(1, maxWidth / origWidth, maxHeight / origHeight);
|
|
// 调整大小
|
const resized = image.resize({
|
width: Math.round(origWidth * scale),
|
height: Math.round(origHeight * scale),
|
quality: 'best'
|
});
|
|
// 保存
|
const quality = Math.round((options.quality || 0.8) * 100);
|
const outputBuffer = resized.toJPEG(quality);
|
await fs.writeFile(outputPath, outputBuffer);
|
|
return {
|
success: true,
|
path: outputPath,
|
size: outputBuffer.length
|
};
|
|
} catch (error) {
|
return {
|
success: false,
|
error: error.message
|
};
|
}
|
}
|
}
|
|
// 导出
|
if (typeof module !== 'undefined' && module.exports) {
|
module.exports = {
|
WebImageProcessor,
|
MainProcessImageHandler
|
};
|
}
|