File size: 13,033 Bytes
453b505
 
 
 
 
 
 
 
0b04ef0
 
453b505
 
 
 
 
a145779
 
afc9cc9
 
 
fb4d4aa
453b505
 
 
 
 
c666a95
453b505
 
 
 
c666a95
 
453b505
 
 
 
 
89eb383
 
c666a95
89eb383
453b505
 
 
 
c666a95
89eb383
 
453b505
 
 
 
 
 
 
a145779
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb4d4aa
 
 
 
 
 
 
a145779
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453b505
 
 
 
 
 
 
a145779
 
 
 
 
 
 
453b505
 
a145779
 
453b505
a145779
 
453b505
 
a145779
 
 
453b505
a145779
453b505
a145779
 
 
 
453b505
a145779
 
 
 
 
 
453b505
 
 
 
 
 
 
a145779
 
453b505
a145779
 
 
 
 
 
 
 
 
 
 
 
453b505
a145779
 
 
 
 
453b505
a145779
 
453b505
 
 
c666a95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453b505
 
 
 
 
 
 
 
 
c666a95
453b505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c666a95
a145779
 
 
453b505
 
 
c666a95
 
 
a145779
 
 
453b505
 
c666a95
453b505
 
 
 
 
 
c666a95
 
 
453b505
 
 
 
 
 
 
 
 
 
 
 
 
c666a95
 
a145779
453b505
 
 
 
 
 
 
 
 
afc9cc9
a145779
453b505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
const sharp = require('sharp')

/**
 * 图片处理工具类
 * 专门处理长图检测、智能切割和重叠处理
 */
class ImageProcessor {
  constructor() {
    // 长图阈值:高度超过宽度*2.5倍(更合理的阈值,确保1080x4000等图片能被识别)
    this.LONG_IMAGE_RATIO = 2.5
    // 重叠像素范围
    this.OVERLAP_MIN = 50
    this.OVERLAP_MAX = 100
    // 默认重叠像素
    this.DEFAULT_OVERLAP = 75

    // 智能切割参数 - 针对模型识别效果优化
    this.OPTIMAL_SEGMENT_HEIGHT = 2500  // 理想片段高度
    this.MAX_SEGMENT_HEIGHT = 3000      // 最大片段高度
    this.MIN_SEGMENT_HEIGHT = 2000      // 最小片段高度
    this.MAX_SEGMENTS = 4               // 最大片段数量限制,避免API处理问题
  }

  /**
   * 检测是否为长图
   * @param {Buffer} imageBuffer - 图片缓冲区
   * @returns {Promise<{isLongImage: boolean, width: number, height: number, format: string}>}
   */
  async detectLongImage(imageBuffer) {
    try {
      const metadata = await sharp(imageBuffer).metadata()
      const { width, height, format } = metadata

      if (!width || !height) {
        throw new Error('无法获取图片尺寸信息')
      }

      const isLongImage = height > width * this.LONG_IMAGE_RATIO
      const threshold = width * this.LONG_IMAGE_RATIO

      console.log(`[长图检测] 图片尺寸: ${width}x${height} (${format}), 阈值: ${threshold}, 判断: ${isLongImage ? '是长图' : '非长图'}`)

      return {
        isLongImage,
        width,
        height,
        format,
        ratio: height / width,
        threshold
      }
    } catch (error) {
      throw new Error(`图片尺寸检测失败: ${error.message}`)
    }
  }

  /**
   * 计算最优片段高度
   * @param {number} width - 图片宽度
   * @param {number} height - 图片高度
   * @param {number} overlap - 重叠像素数
   * @returns {Object} 包含最优高度和片段数的对象
   */
  calculateOptimalSegmentHeight(width, height, overlap = this.DEFAULT_OVERLAP) {
    // 如果图片高度小于最大片段高度,直接返回原高度
    if (height <= this.MAX_SEGMENT_HEIGHT) {
      return {
        segmentHeight: height,
        segmentCount: 1,
        strategy: 'single_segment'
      }
    }

    // 计算理想的片段数量(基于理想高度)
    let idealSegmentCount = Math.ceil(height / this.OPTIMAL_SEGMENT_HEIGHT)

    // 如果片段数量超过最大限制,强制减少片段数量
    if (idealSegmentCount > this.MAX_SEGMENTS) {
      idealSegmentCount = this.MAX_SEGMENTS
      console.log(`[智能切割] 片段数量超限,强制限制为${this.MAX_SEGMENTS}个片段`)
    }

    // 计算每个片段的理想高度(不考虑重叠)
    const baseSegmentHeight = Math.floor(height / idealSegmentCount)

    // 检查是否在合理范围内
    if (baseSegmentHeight >= this.MIN_SEGMENT_HEIGHT && baseSegmentHeight <= this.MAX_SEGMENT_HEIGHT) {
      return {
        segmentHeight: baseSegmentHeight,
        segmentCount: idealSegmentCount,
        strategy: 'optimal_division'
      }
    }

    // 如果理想高度太小,减少片段数量
    if (baseSegmentHeight < this.MIN_SEGMENT_HEIGHT) {
      const adjustedSegmentCount = Math.max(1, Math.floor(height / this.MIN_SEGMENT_HEIGHT))
      const adjustedSegmentHeight = Math.floor(height / adjustedSegmentCount)

      return {
        segmentHeight: Math.min(adjustedSegmentHeight, this.MAX_SEGMENT_HEIGHT),
        segmentCount: adjustedSegmentCount,
        strategy: 'min_height_constraint'
      }
    }

    // 如果理想高度太大,增加片段数量
    if (baseSegmentHeight > this.MAX_SEGMENT_HEIGHT) {
      const adjustedSegmentCount = Math.ceil(height / this.MAX_SEGMENT_HEIGHT)
      const adjustedSegmentHeight = Math.floor(height / adjustedSegmentCount)

      return {
        segmentHeight: adjustedSegmentHeight,
        segmentCount: adjustedSegmentCount,
        strategy: 'max_height_constraint'
      }
    }

    // 兜底策略:使用最大高度
    return {
      segmentHeight: this.MAX_SEGMENT_HEIGHT,
      segmentCount: Math.ceil(height / this.MAX_SEGMENT_HEIGHT),
      strategy: 'fallback'
    }
  }

  /**
   * 计算切割参数(智能自适应版本)
   * @param {number} width - 图片宽度
   * @param {number} height - 图片高度
   * @param {number} overlap - 重叠像素数
   * @returns {Array<{top: number, left: number, width: number, height: number}>}
   */
  calculateCropRegions(width, height, overlap = this.DEFAULT_OVERLAP) {
    const regions = []

    // 计算最优片段高度
    const optimalConfig = this.calculateOptimalSegmentHeight(width, height, overlap)
    const { segmentHeight: baseSegmentHeight, segmentCount, strategy } = optimalConfig

    console.log(`[智能切割] 图片尺寸: ${width}x${height}, 策略: ${strategy}, 预计片段数: ${segmentCount}, 基础高度: ${baseSegmentHeight}`)

    let currentTop = 0
    let segmentIndex = 0

    while (currentTop < height && segmentIndex < segmentCount * 2) { // 防止无限循环
      const remainingHeight = height - currentTop
      let segmentHeight = Math.min(baseSegmentHeight, remainingHeight)

      // 如果不是第一个片段,需要向上扩展重叠区域
      if (segmentIndex > 0) {
        const overlapTop = Math.min(overlap, currentTop)
        currentTop = currentTop - overlapTop
        segmentHeight = Math.min(baseSegmentHeight + overlapTop, height - currentTop)
      }

      // 如果不是最后一个片段,需要向下扩展重叠区域
      const isLastSegment = (currentTop + segmentHeight >= height) || (segmentIndex >= segmentCount - 1)
      if (!isLastSegment) {
        const overlapBottom = Math.min(overlap, remainingHeight - segmentHeight)
        segmentHeight = Math.min(segmentHeight + overlapBottom, height - currentTop)
      }

      // 确保片段高度在合理范围内
      segmentHeight = Math.max(segmentHeight, this.MIN_SEGMENT_HEIGHT)
      segmentHeight = Math.min(segmentHeight, this.MAX_SEGMENT_HEIGHT)
      segmentHeight = Math.min(segmentHeight, height - currentTop) // 不能超出图片边界

      regions.push({
        top: currentTop,
        left: 0,
        width: width,
        height: segmentHeight,
        segmentIndex: segmentIndex,
        isFirst: segmentIndex === 0,
        isLast: currentTop + segmentHeight >= height,
        strategy: strategy
      })

      console.log(`[切割片段] 片段${segmentIndex + 1}: top=${currentTop}, height=${segmentHeight}, 范围=[${currentTop}, ${currentTop + segmentHeight}]`)

      // 移动到下一个片段的起始位置
      if (segmentIndex === 0) {
        // 第一个片段:直接移动基础高度
        currentTop += baseSegmentHeight
      } else {
        // 后续片段:移动基础高度减去重叠
        currentTop += Math.max(baseSegmentHeight - overlap, 1)
      }

      segmentIndex++

      // 如果已经覆盖了整个图片,退出循环
      if (currentTop >= height) {
        break
      }
    }

    console.log(`[切割完成] 实际生成${regions.length}个片段`)
    return regions
  }

  /**
   * 根据原图格式选择最优输出格式
   * @param {string} originalFormat - 原图格式
   * @returns {string} 最优输出格式
   */
  getOptimalOutputFormat(originalFormat) {
    // 格式映射表:原格式 -> 最优输出格式
    const formatMap = {
      'jpeg': 'jpeg',
      'jpg': 'jpeg',
      'png': 'png',
      'webp': 'webp',
      'gif': 'png', // GIF转PNG保持透明度
      'bmp': 'png', // BMP转PNG减小文件大小
      'tiff': 'png', // TIFF转PNG兼容性更好
      'svg': 'png'  // SVG转PNG用于显示
    }

    return formatMap[originalFormat?.toLowerCase()] || 'png'
  }

  /**
   * 应用最优格式转换
   * @param {Object} sharpInstance - Sharp实例
   * @param {string} format - 目标格式
   * @param {Object} options - 格式选项
   * @returns {Object} 配置后的Sharp实例
   */
  applyOptimalFormat(sharpInstance, format, options = {}) {
    switch (format) {
      case 'jpeg':
        return sharpInstance.jpeg({
          quality: options.quality || 85,
          progressive: true
        })
      case 'png':
        return sharpInstance.png({
          compressionLevel: options.compressionLevel || 6,
          progressive: true
        })
      case 'webp':
        return sharpInstance.webp({
          quality: options.quality || 80,
          effort: 4
        })
      default:
        return sharpInstance.png({ compressionLevel: 6 })
    }
  }

  /**
   * 切割长图为多个片段
   * @param {Buffer} imageBuffer - 原始图片缓冲区
   * @param {number} overlap - 重叠像素数
   * @returns {Promise<Array<{buffer: Buffer, metadata: Object}>>}
   */
  async cropLongImage(imageBuffer, overlap = this.DEFAULT_OVERLAP) {
    try {
      const detection = await this.detectLongImage(imageBuffer)

      if (!detection.isLongImage) {
        // 不是长图,返回原图
        return [{
          buffer: imageBuffer,
          metadata: {
            segmentIndex: 0,
            totalSegments: 1,
            isFirst: true,
            isLast: true,
            originalWidth: detection.width,
            originalHeight: detection.height,
            segmentWidth: detection.width,
            segmentHeight: detection.height
          }
        }]
      }

      const { width, height, format } = detection

      // 计算最优切割配置
      const optimalConfig = this.calculateOptimalSegmentHeight(width, height, overlap)
      const regions = this.calculateCropRegions(width, height, overlap)
      const segments = []

      // 选择最优输出格式
      const outputFormat = this.getOptimalOutputFormat(format)

      console.log(`检测到长图 ${width}x${height} (${format}),智能切割策略: ${optimalConfig.strategy}`)
      console.log(`切割配置: 目标高度=${optimalConfig.segmentHeight}px, 预计片段=${optimalConfig.segmentCount}个, 实际片段=${regions.length}个`)
      console.log(`输出格式: ${outputFormat}, 重叠像素: ${overlap}px`)

      for (const region of regions) {
        const sharpInstance = sharp(imageBuffer)
          .extract({
            left: region.left,
            top: region.top,
            width: region.width,
            height: region.height
          })

        // 应用最优格式
        const segmentBuffer = await this.applyOptimalFormat(sharpInstance, outputFormat)
          .toBuffer()

        segments.push({
          buffer: segmentBuffer,
          metadata: {
            segmentIndex: region.segmentIndex,
            totalSegments: regions.length,
            isFirst: region.isFirst,
            isLast: region.isLast,
            originalWidth: width,
            originalHeight: height,
            segmentWidth: region.width,
            segmentHeight: region.height,
            originalFormat: format,
            outputFormat: outputFormat,
            strategy: region.strategy,
            cropRegion: {
              top: region.top,
              left: region.left,
              width: region.width,
              height: region.height
            }
          }
        })

        const heightStatus = region.height <= 2500 ? '✓理想' : region.height <= 3000 ? '✓良好' : '⚠超限'
        console.log(`生成片段 ${region.segmentIndex + 1}/${regions.length}: ${region.width}x${region.height} (${heightStatus}) [${region.top}-${region.top + region.height}]`)
      }

      return segments
    } catch (error) {
      throw new Error(`长图切割失败: ${error.message}`)
    }
  }

  /**
   * 生成片段文件名
   * @param {string} originalName - 原始文件名
   * @param {number} segmentIndex - 片段索引
   * @param {number} totalSegments - 总片段数
   * @returns {string}
   */
  generateSegmentName(originalName, segmentIndex, totalSegments) {
    const nameWithoutExt = originalName.replace(/\.[^/.]+$/, '')
    const ext = originalName.includes('.') ? originalName.split('.').pop() : 'png'
    
    return `${nameWithoutExt}_part${segmentIndex + 1}of${totalSegments}.${ext}`
  }

  /**
   * 获取处理统计信息
   * @param {Array} segments - 切割后的片段数组
   * @returns {Object}
   */
  getProcessingStats(segments) {
    if (!segments || segments.length === 0) {
      return { totalSegments: 0, isLongImage: false }
    }

    const firstSegment = segments[0]
    const metadata = firstSegment.metadata

    return {
      totalSegments: segments.length,
      isLongImage: segments.length > 1,
      originalDimensions: {
        width: metadata.originalWidth,
        height: metadata.originalHeight
      },
      segmentDimensions: segments.map(segment => ({
        width: segment.metadata.segmentWidth,
        height: segment.metadata.segmentHeight,
        index: segment.metadata.segmentIndex
      }))
    }
  }
}

module.exports = new ImageProcessor()