// 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;