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>} */ 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()