textRegionCropper.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. // server/utils/textRegionCropper.js
  2. import sharp from 'sharp';
  3. import fse from 'fs-extra';
  4. import * as path from 'path';
  5. class TextRegionCropper {
  6. constructor() {
  7. this.logger = {
  8. info: (msg, ...args) => console.log(`✂️ [裁剪] ${msg}`, ...args),
  9. debug: (msg, ...args) => console.log(`🐛 [裁剪] ${msg}`, ...args),
  10. error: (msg, ...args) => console.error(`❌ [裁剪] ${msg}`, ...args)
  11. };
  12. // 确保裁剪调试目录存在
  13. this.cropDebugDir = path.join(process.cwd(), 'temp', 'crop_debug');
  14. fse.ensureDirSync(this.cropDebugDir);
  15. }
  16. async cropTextRegion(imageBuffer, box, regionIndex) {
  17. const timestamp = Date.now();
  18. try {
  19. const metadata = await sharp(imageBuffer).metadata();
  20. const imgWidth = metadata.width;
  21. const imgHeight = metadata.height;
  22. // 计算文本框的边界
  23. const left = Math.min(box.x1, box.x2, box.x3, box.x4);
  24. const top = Math.min(box.y1, box.y2, box.y3, box.y4);
  25. const right = Math.max(box.x1, box.x2, box.x3, box.x4);
  26. const bottom = Math.max(box.y1, box.y2, box.y3, box.y4);
  27. const originalWidth = right - left;
  28. const originalHeight = bottom - top;
  29. // 四边各扩大5像素
  30. const expandPixels = 5;
  31. const expandedLeft = Math.max(0, left - expandPixels);
  32. const expandedTop = Math.max(0, top - expandPixels);
  33. const expandedRight = Math.min(imgWidth - 1, right + expandPixels);
  34. const expandedBottom = Math.min(imgHeight - 1, bottom + expandPixels);
  35. const expandedWidth = expandedRight - expandedLeft;
  36. const expandedHeight = expandedBottom - expandedTop;
  37. if (expandedWidth <= 0 || expandedHeight <= 0) {
  38. this.logger.debug(`区域 ${regionIndex}: 无效的裁剪区域`);
  39. return null;
  40. }
  41. const croppedBuffer = await sharp(imageBuffer)
  42. .extract({
  43. left: Math.floor(expandedLeft),
  44. top: Math.floor(expandedTop),
  45. width: Math.floor(expandedWidth),
  46. height: Math.floor(expandedHeight)
  47. })
  48. .png()
  49. .toBuffer();
  50. // 保存裁剪后的图像用于调试
  51. const cropPath = path.join(this.cropDebugDir, `crop-${regionIndex}-${timestamp}.png`);
  52. await fse.writeFile(cropPath, croppedBuffer);
  53. this.logger.debug(`区域 ${regionIndex}: 裁剪 ${Math.floor(expandedWidth)}x${Math.floor(expandedHeight)} -> ${cropPath}`);
  54. return {
  55. buffer: croppedBuffer,
  56. boxInfo: {
  57. original: { left, top, right, bottom, width: originalWidth, height: originalHeight },
  58. expanded: {
  59. left: expandedLeft,
  60. top: expandedTop,
  61. right: expandedRight,
  62. bottom: expandedBottom,
  63. width: expandedWidth,
  64. height: expandedHeight
  65. }
  66. }
  67. };
  68. } catch (error) {
  69. this.logger.error(`区域 ${regionIndex}: 裁剪失败`, error);
  70. return null;
  71. }
  72. }
  73. async rotateImage(imageBuffer, degrees) {
  74. return await sharp(imageBuffer)
  75. .rotate(degrees)
  76. .png()
  77. .toBuffer();
  78. }
  79. calculateExpansion(originalWidth, originalHeight, expansionFactor = 1.2) {
  80. return {
  81. width: originalWidth * expansionFactor,
  82. height: originalHeight * expansionFactor
  83. };
  84. }
  85. validateCropRegion(left, top, width, height, imgWidth, imgHeight) {
  86. if (width <= 0 || height <= 0) {
  87. return false;
  88. }
  89. if (left < 0 || top < 0) {
  90. return false;
  91. }
  92. if (left + width > imgWidth || top + height > imgHeight) {
  93. return false;
  94. }
  95. return true;
  96. }
  97. }
  98. export default TextRegionCropper;