Electron-vue3-ts-offline/server/utils/textRegionCropper.js
2025-11-13 18:09:31 +08:00

114 行
4.1 KiB
JavaScript

// server/utils/textRegionCropper.js
import sharp from 'sharp';
import fse from 'fs-extra';
import * as path from 'path';
class TextRegionCropper {
constructor() {
this.logger = {
info: (msg, ...args) => console.log(`✂️ [裁剪] ${msg}`, ...args),
debug: (msg, ...args) => console.log(`🐛 [裁剪] ${msg}`, ...args),
error: (msg, ...args) => console.error(`❌ [裁剪] ${msg}`, ...args)
};
// 确保裁剪调试目录存在
this.cropDebugDir = path.join(process.cwd(), 'temp', 'crop_debug');
fse.ensureDirSync(this.cropDebugDir);
}
async cropTextRegion(imageBuffer, box, regionIndex) {
const timestamp = Date.now();
try {
const metadata = await sharp(imageBuffer).metadata();
const imgWidth = metadata.width;
const imgHeight = metadata.height;
// 计算文本框的边界
const left = Math.min(box.x1, box.x2, box.x3, box.x4);
const top = Math.min(box.y1, box.y2, box.y3, box.y4);
const right = Math.max(box.x1, box.x2, box.x3, box.x4);
const bottom = Math.max(box.y1, box.y2, box.y3, box.y4);
const originalWidth = right - left;
const originalHeight = bottom - top;
// 四边各扩大5像素
const expandPixels = 5;
const expandedLeft = Math.max(0, left - expandPixels);
const expandedTop = Math.max(0, top - expandPixels);
const expandedRight = Math.min(imgWidth - 1, right + expandPixels);
const expandedBottom = Math.min(imgHeight - 1, bottom + expandPixels);
const expandedWidth = expandedRight - expandedLeft;
const expandedHeight = expandedBottom - expandedTop;
if (expandedWidth <= 0 || expandedHeight <= 0) {
this.logger.debug(`区域 ${regionIndex}: 无效的裁剪区域`);
return null;
}
const croppedBuffer = await sharp(imageBuffer)
.extract({
left: Math.floor(expandedLeft),
top: Math.floor(expandedTop),
width: Math.floor(expandedWidth),
height: Math.floor(expandedHeight)
})
.png()
.toBuffer();
// 保存裁剪后的图像用于调试
const cropPath = path.join(this.cropDebugDir, `crop-${regionIndex}-${timestamp}.png`);
await fse.writeFile(cropPath, croppedBuffer);
this.logger.debug(`区域 ${regionIndex}: 裁剪 ${Math.floor(expandedWidth)}x${Math.floor(expandedHeight)} -> ${cropPath}`);
return {
buffer: croppedBuffer,
boxInfo: {
original: { left, top, right, bottom, width: originalWidth, height: originalHeight },
expanded: {
left: expandedLeft,
top: expandedTop,
right: expandedRight,
bottom: expandedBottom,
width: expandedWidth,
height: expandedHeight
}
}
};
} catch (error) {
this.logger.error(`区域 ${regionIndex}: 裁剪失败`, error);
return null;
}
}
async rotateImage(imageBuffer, degrees) {
return await sharp(imageBuffer)
.rotate(degrees)
.png()
.toBuffer();
}
calculateExpansion(originalWidth, originalHeight, expansionFactor = 1.2) {
return {
width: originalWidth * expansionFactor,
height: originalHeight * expansionFactor
};
}
validateCropRegion(left, top, width, height, imgWidth, imgHeight) {
if (width <= 0 || height <= 0) {
return false;
}
if (left < 0 || top < 0) {
return false;
}
if (left + width > imgWidth || top + height > imgHeight) {
return false;
}
return true;
}
}
export default TextRegionCropper;