/** * 文件:image.ts * 功能:小红书图片处理工具集合。 * * 提供: * - 图片格式统一(目标:PNG) * - EXIF 方向纠正(避免旋转错误) * - 尺寸/压缩策略(可扩展为自适应裁剪) * - Base64 / Blob 转换辅助 * * 说明:当前为前端侧工具,未接入后端压缩/去重; * 若后续需要高质量/批量处理,可接入本地原生库或后端服务。 */ import { XiaohongshuImageProcessor, ProcessedImage, XIAOHONGSHU_CONSTANTS } from './types'; /** * XiaohongshuImageHandler * * 说明(中文注释): * 小红书图片处理器,负责将各种格式的图片转换为小红书平台支持的格式。 * * 主要功能: * - 统一转换为PNG格式(根据用户需求) * - 处理图片尺寸优化 * - EXIF方向信息处理(复用现有逻辑) * - 图片质量压缩 * - 批量图片处理 * * 设计原则: * - 复用项目现有的图片处理能力 * - 保持图片质量的前提下优化文件大小 * - 支持所有常见图片格式 * - 提供灵活的配置选项 */ export class XiaohongshuImageHandler implements XiaohongshuImageProcessor { /** * 转换图片为PNG格式 * 使用Canvas API进行格式转换 */ async convertToPNG(imageBlob: Blob): Promise { return new Promise((resolve, reject) => { const img = new Image(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('无法获取Canvas上下文')); return; } img.onload = () => { try { // 设置canvas尺寸 canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; // 清除canvas并绘制图片 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); // 转换为PNG格式的Blob canvas.toBlob((pngBlob) => { if (pngBlob) { resolve(pngBlob); } else { reject(new Error('PNG转换失败')); } }, 'image/png', 1.0); } catch (error) { reject(new Error(`图片转换失败: ${error.message}`)); } }; img.onerror = () => { reject(new Error('图片加载失败')); }; // 加载图片 const imageUrl = URL.createObjectURL(imageBlob); const originalOnLoad = img.onload; img.onload = (event) => { URL.revokeObjectURL(imageUrl); if (originalOnLoad) { originalOnLoad.call(img, event); } }; img.src = imageUrl; }); } /** * 优化图片质量和尺寸 * 根据小红书平台要求调整图片 */ async optimizeImage( imageBlob: Blob, quality: number = 85, maxWidth?: number, maxHeight?: number ): Promise { const { RECOMMENDED_SIZE } = XIAOHONGSHU_CONSTANTS.IMAGE_LIMITS; const targetWidth = maxWidth || RECOMMENDED_SIZE.width; const targetHeight = maxHeight || RECOMMENDED_SIZE.height; return new Promise((resolve, reject) => { const img = new Image(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('无法获取Canvas上下文')); return; } img.onload = () => { try { let { naturalWidth: width, naturalHeight: height } = img; // 计算缩放比例 const scaleX = targetWidth / width; const scaleY = targetHeight / height; const scale = Math.min(scaleX, scaleY, 1); // 不放大图片 // 计算新尺寸 const newWidth = Math.floor(width * scale); const newHeight = Math.floor(height * scale); // 设置canvas尺寸 canvas.width = newWidth; canvas.height = newHeight; // 使用高质量缩放 ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; // 绘制缩放后的图片 ctx.drawImage(img, 0, 0, newWidth, newHeight); // 转换为指定质量的PNG canvas.toBlob((optimizedBlob) => { if (optimizedBlob) { resolve(optimizedBlob); } else { reject(new Error('图片优化失败')); } }, 'image/png', quality / 100); } catch (error) { reject(new Error(`图片优化失败: ${error.message}`)); } }; img.onerror = () => { reject(new Error('图片加载失败')); }; const imageUrl = URL.createObjectURL(imageBlob); img.src = imageUrl; }); } /** * 处理EXIF方向信息 * 复用现有的EXIF处理逻辑 */ private async handleEXIFOrientation(imageBlob: Blob): Promise { // 检查是否为JPEG格式 if (!imageBlob.type.includes('jpeg') && !imageBlob.type.includes('jpg')) { return imageBlob; } try { // 读取EXIF信息 const arrayBuffer = await imageBlob.arrayBuffer(); const uint8Array = new Uint8Array(arrayBuffer); // 查找EXIF orientation标记 let orientation = 1; // 简单的EXIF解析(查找orientation标记) if (uint8Array[0] === 0xFF && uint8Array[1] === 0xD8) { // JPEG标记 let offset = 2; while (offset < uint8Array.length) { if (uint8Array[offset] === 0xFF && uint8Array[offset + 1] === 0xE1) { // 找到EXIF段 const exifLength = (uint8Array[offset + 2] << 8) | uint8Array[offset + 3]; const exifData = uint8Array.slice(offset + 4, offset + 4 + exifLength); // 查找orientation标记(0x0112) for (let i = 0; i < exifData.length - 8; i++) { if (exifData[i] === 0x01 && exifData[i + 1] === 0x12) { orientation = exifData[i + 8] || 1; break; } } break; } offset += 2; if (uint8Array[offset - 2] === 0xFF) { const segmentLength = (uint8Array[offset] << 8) | uint8Array[offset + 1]; offset += segmentLength; } } } // 如果需要旋转 if (orientation > 1) { return await this.rotateImage(imageBlob, orientation); } return imageBlob; } catch (error) { console.warn('EXIF处理失败,使用原图:', error); return imageBlob; } } /** * 根据EXIF方向信息旋转图片 */ private async rotateImage(imageBlob: Blob, orientation: number): Promise { return new Promise((resolve, reject) => { const img = new Image(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('无法获取Canvas上下文')); return; } img.onload = () => { const { naturalWidth: width, naturalHeight: height } = img; // 根据orientation设置变换 switch (orientation) { case 3: // 180度 canvas.width = width; canvas.height = height; ctx.rotate(Math.PI); ctx.translate(-width, -height); break; case 6: // 顺时针90度 canvas.width = height; canvas.height = width; ctx.rotate(Math.PI / 2); ctx.translate(0, -height); break; case 8: // 逆时针90度 canvas.width = height; canvas.height = width; ctx.rotate(-Math.PI / 2); ctx.translate(-width, 0); break; default: canvas.width = width; canvas.height = height; break; } ctx.drawImage(img, 0, 0); canvas.toBlob((rotatedBlob) => { if (rotatedBlob) { resolve(rotatedBlob); } else { reject(new Error('图片旋转失败')); } }, 'image/png', 1.0); }; img.onerror = () => { reject(new Error('图片加载失败')); }; const imageUrl = URL.createObjectURL(imageBlob); img.src = imageUrl; }); } /** * 批量处理图片 * 对多张图片进行统一处理 */ async processImages(images: { name: string; blob: Blob }[]): Promise { const results: ProcessedImage[] = []; for (const { name, blob } of images) { try { console.log(`[XiaohongshuImageHandler] 处理图片: ${name}`); // 检查文件大小 if (blob.size > XIAOHONGSHU_CONSTANTS.IMAGE_LIMITS.MAX_SIZE) { console.warn(`图片 ${name} 过大 (${Math.round(blob.size / 1024)}KB),将进行压缩`); } // 处理EXIF方向 let processedBlob = await this.handleEXIFOrientation(blob); // 优化图片(转换为PNG并调整尺寸) processedBlob = await this.optimizeImage(processedBlob, 85); // 转换为PNG格式 const pngBlob = await this.convertToPNG(processedBlob); // 获取处理后的图片尺寸 const dimensions = await this.getImageDimensions(pngBlob); results.push({ originalName: name, blob: pngBlob, dimensions, size: pngBlob.size }); console.log(`[XiaohongshuImageHandler] 图片 ${name} 处理完成: ${dimensions.width}x${dimensions.height}, ${Math.round(pngBlob.size / 1024)}KB`); } catch (error) { console.error(`处理图片 ${name} 失败:`, error); // 继续处理其他图片,不抛出异常 } } return results; } /** * 获取图片尺寸信息 */ private async getImageDimensions(imageBlob: Blob): Promise<{ width: number; height: number }> { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { resolve({ width: img.naturalWidth, height: img.naturalHeight }); URL.revokeObjectURL(img.src); }; img.onerror = () => { reject(new Error('无法获取图片尺寸')); }; img.src = URL.createObjectURL(imageBlob); }); } /** * 验证图片格式是否支持 */ static isSupportedFormat(filename: string): boolean { const ext = filename.toLowerCase().split('.').pop() || ''; return XIAOHONGSHU_CONSTANTS.IMAGE_LIMITS.SUPPORTED_FORMATS.includes(ext as any); } /** * 创建图片预览URL * 用于界面预览 */ static createPreviewUrl(imageBlob: Blob): string { return URL.createObjectURL(imageBlob); } /** * 清理预览URL */ static revokePreviewUrl(url: string): void { URL.revokeObjectURL(url); } /** * 获取图片处理统计信息 */ static getProcessingStats(original: { name: string; blob: Blob }[], processed: ProcessedImage[]): { totalOriginalSize: number; totalProcessedSize: number; compressionRatio: number; processedCount: number; failedCount: number; } { const totalOriginalSize = original.reduce((sum, img) => sum + img.blob.size, 0); const totalProcessedSize = processed.reduce((sum, img) => sum + img.size, 0); return { totalOriginalSize, totalProcessedSize, compressionRatio: totalOriginalSize > 0 ? totalProcessedSize / totalOriginalSize : 0, processedCount: processed.length, failedCount: original.length - processed.length }; } } /** * 小红书图片处理器管理类 * 提供单例模式的图片处理器 */ export class XiaohongshuImageManager { private static instance: XiaohongshuImageHandler | null = null; /** * 获取图片处理器实例 */ static getInstance(): XiaohongshuImageHandler { if (!this.instance) { this.instance = new XiaohongshuImageHandler(); } return this.instance; } /** * 销毁实例 */ static destroyInstance(): void { this.instance = null; } }