| const sharp = require('sharp') |
|
|
| |
| |
| |
| |
| class ImageProcessor { |
| constructor() { |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
| 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}`) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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' |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
| getOptimalOutputFormat(originalFormat) { |
| |
| const formatMap = { |
| 'jpeg': 'jpeg', |
| 'jpg': 'jpeg', |
| 'png': 'png', |
| 'webp': 'webp', |
| 'gif': 'png', |
| 'bmp': 'png', |
| 'tiff': 'png', |
| 'svg': 'png' |
| } |
|
|
| return formatMap[originalFormat?.toLowerCase()] || 'png' |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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 }) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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}`) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| generateSegmentName(originalName, segmentIndex, totalSegments) { |
| const nameWithoutExt = originalName.replace(/\.[^/.]+$/, '') |
| const ext = originalName.includes('.') ? originalName.split('.').pop() : 'png' |
| |
| return `${nameWithoutExt}_part${segmentIndex + 1}of${totalSegments}.${ext}` |
| } |
|
|
| |
| |
| |
| |
| |
| 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() |
|
|