2025-11-13 16:34:41 +08:00
|
|
|
// server/utils/textRegionCropper.js
|
|
|
|
|
import sharp from 'sharp';
|
2025-11-13 18:09:31 +08:00
|
|
|
import fse from 'fs-extra';
|
|
|
|
|
import * as path from 'path';
|
2025-11-13 16:34:41 +08:00
|
|
|
|
|
|
|
|
class TextRegionCropper {
|
|
|
|
|
constructor() {
|
2025-11-13 18:09:31 +08:00
|
|
|
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);
|
2025-11-13 16:34:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async cropTextRegion(imageBuffer, box, regionIndex) {
|
2025-11-13 18:09:31 +08:00
|
|
|
const timestamp = Date.now();
|
2025-11-13 16:34:41 +08:00
|
|
|
try {
|
|
|
|
|
const metadata = await sharp(imageBuffer).metadata();
|
|
|
|
|
const imgWidth = metadata.width;
|
|
|
|
|
const imgHeight = metadata.height;
|
|
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
// 计算文本框的边界
|
2025-11-13 16:34:41 +08:00
|
|
|
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;
|
|
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
// 四边各扩大5像素
|
|
|
|
|
const expandPixels = 5;
|
2025-11-13 16:34:41 +08:00
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
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);
|
2025-11-13 16:34:41 +08:00
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
const expandedWidth = expandedRight - expandedLeft;
|
|
|
|
|
const expandedHeight = expandedBottom - expandedTop;
|
2025-11-13 16:34:41 +08:00
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
if (expandedWidth <= 0 || expandedHeight <= 0) {
|
|
|
|
|
this.logger.debug(`区域 ${regionIndex}: 无效的裁剪区域`);
|
2025-11-13 16:34:41 +08:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const croppedBuffer = await sharp(imageBuffer)
|
|
|
|
|
.extract({
|
2025-11-13 18:09:31 +08:00
|
|
|
left: Math.floor(expandedLeft),
|
|
|
|
|
top: Math.floor(expandedTop),
|
|
|
|
|
width: Math.floor(expandedWidth),
|
|
|
|
|
height: Math.floor(expandedHeight)
|
2025-11-13 16:34:41 +08:00
|
|
|
})
|
|
|
|
|
.png()
|
|
|
|
|
.toBuffer();
|
|
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
// 保存裁剪后的图像用于调试
|
|
|
|
|
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}`);
|
2025-11-13 16:34:41 +08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
buffer: croppedBuffer,
|
|
|
|
|
boxInfo: {
|
|
|
|
|
original: { left, top, right, bottom, width: originalWidth, height: originalHeight },
|
|
|
|
|
expanded: {
|
2025-11-13 18:09:31 +08:00
|
|
|
left: expandedLeft,
|
|
|
|
|
top: expandedTop,
|
|
|
|
|
right: expandedRight,
|
|
|
|
|
bottom: expandedBottom,
|
|
|
|
|
width: expandedWidth,
|
|
|
|
|
height: expandedHeight
|
2025-11-13 16:34:41 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-11-13 18:09:31 +08:00
|
|
|
this.logger.error(`区域 ${regionIndex}: 裁剪失败`, error);
|
2025-11-13 16:34:41 +08:00
|
|
|
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;
|