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()
|