图片压缩与格式转换:优化应用资源加载
引言
在 HarmonyOS 应用开发中,图片资源的管理对应用性能至关重要。不合理的图片处理会导致应用体积膨胀、加载速度变慢,甚至引发内存溢出问题。本文将深入讲解如何在 HarmonyOS Next(API 10+)中进行高效的图片压缩和格式转换,帮助开发者优化应用资源加载体验。
官方参考资料:
- HarmonyOS API 参考
- 媒体服务开发指南
图片处理基础概念
图片格式选择策略
在 HarmonyOS 应用开发中,选择合适的图片格式直接影响应用性能:
- JPEG 格式:适用于照片类图片,支持高压缩比
- PNG 格式:适用于需要透明度的图标和图形
- WebP 格式:现代格式,在相同质量下体积更小
- HEIC 格式:高效图像格式,iOS 生态常用
图片压缩级别
// 压缩质量级别定义示例
const CompressionLevel = {LOW: 0.3, // 低质量,高压缩MEDIUM: 0.6, // 中等质量HIGH: 0.8, // 高质量,低压缩LOSSLESS: 1.0, // 无损压缩
} as const;
HarmonyOS 图片处理 API 概览
核心图像处理类
HarmonyOS Next 提供了丰富的图像处理 API:
- ImageSource:图像数据源管理
- ImagePacker:图像打包和压缩
- PixelMap:像素级图像操作
- ImageReceiver:图像接收和处理
支持的图像格式
| 格式类型 | 编码支持 | 解码支持 | 特性说明 |
|---|---|---|---|
| JPEG | ✅ | ✅ | 有损压缩,适合照片 |
| PNG | ✅ | ✅ | 无损压缩,支持透明 |
| WebP | ✅ | ✅ | 现代格式,压缩率高 |
| HEIC | ✅ | ✅ | 高效图像格式 |
| GIF | ❌ | ✅ | 仅支持解码 |
| BMP | ❌ | ✅ | 仅支持解码 |
图片压缩实战
基础压缩方法
import image from '@ohos.multimedia.image';
import fileIo from '@ohos.file.fs';// 基础图片压缩函数
async function compressImage(sourceUri: string, targetUri: string, quality: number): Promise<boolean> {try {// 1. 创建ImageSource实例const sourceFile = await fileIo.open(sourceUri, fileIo.OpenMode.READ_ONLY);const imageSource = image.createImageSource(sourceFile.fd);// 2. 创建解码选项const decodingOptions: image.DecodingOptions = {desiredSize: { width: 1024, height: 1024 }, // 限制最大尺寸desiredRegion: { size: { width: 1024, height: 1024 }, x: 0, y: 0 },desiredPixelFormat: image.PixelMapFormat.RGBA_8888} catch (error) {console.error('内存优化处理失败:', error);return false;} finally {// 确保资源释放if (originalPixelMap) {originalPixelMap.release();}if (processedPixelMap && processedPixelMap !== originalPixelMap) {processedPixelMap.release();}if (sourceFile) {fileIo.close(sourceFile).catch(e => console.error('关闭文件失败:', e));}}}
}## 图片缓存策略### 内存缓存实现```typescript
// 内存缓存管理器
class ImageMemoryCache {private cache: Map<string, { pixelMap: image.PixelMap, timestamp: number }>;private maxSize: number;private memoryUsage: number;constructor(maxSizeBytes: number = 20 * 1024 * 1024) { // 默认20MBthis.cache = new Map();this.maxSize = maxSizeBytes;this.memoryUsage = 0;}async put(key: string, pixelMap: image.PixelMap): Promise<void> {// 估算内存使用const imageInfo = await pixelMap.getImageInfo();const pixelFormat = await pixelMap.getPixelFormat();const bytesPerPixel = this.getBytesPerPixel(pixelFormat);const estimatedSize = imageInfo.size.width * imageInfo.size.height * bytesPerPixel;// 检查是否需要清理缓存while (this.memoryUsage + estimatedSize > this.maxSize && this.cache.size > 0) {this.evictOldest();}// 存储到缓存this.cache.set(key, { pixelMap, timestamp: Date.now() });this.memoryUsage += estimatedSize;}get(key: string): image.PixelMap | null {const cached = this.cache.get(key);if (cached) {// 更新访问时间cached.timestamp = Date.now();return cached.pixelMap;}return null;}private evictOldest(): void {let oldestKey = '';let oldestTime = Infinity;this.cache.forEach((value, key) => {if (value.timestamp < oldestTime) {oldestTime = value.timestamp;oldestKey = key;}});if (oldestKey) {const removed = this.cache.get(oldestKey);if (removed) {removed.pixelMap.release();}this.cache.delete(oldestKey);// 注意:这里简化了内存计算,实际应该记录每个缓存项的大小}}private getBytesPerPixel(format: number): number {// 根据不同像素格式返回每像素字节数switch (format) {case image.PixelMapFormat.ARGB_8888:case image.PixelMapFormat.RGBA_8888:return 4;case image.PixelMapFormat.RGB_565:return 2;default:return 4; // 默认保守估计}}
}// 使用示例
const imageCache = new ImageMemoryCache();async function loadImageWithCache(imageUri: string): Promise<image.PixelMap | null> {// 尝试从缓存获取const cachedImage = imageCache.get(imageUri);if (cachedImage) {console.log('从缓存加载图片:', imageUri);return cachedImage;}// 缓存未命中,加载并缓存try {const file = await fileIo.open(imageUri, fileIo.OpenMode.READ_ONLY);const imageSource = image.createImageSource(file.fd);const pixelMap = await imageSource.createPixelMap();await fileIo.close(file);// 存入缓存await imageCache.put(imageUri, pixelMap);console.log('加载并缓存图片:', imageUri);return pixelMap;} catch (error) {console.error('加载图片失败:', error);return null;}
}
磁盘缓存实现
// 磁盘缓存管理器
class ImageDiskCache {private cacheDir: string;private maxSize: number;constructor(cacheDirectory: string,maxSizeBytes: number = 100 * 1024 * 1024) {// 默认100MBthis.cacheDir = cacheDirectory;this.maxSize = maxSizeBytes;// 确保缓存目录存在this.ensureCacheDirExists();}private async ensureCacheDirExists(): Promise<void> {try {const fileStat = await fileIo.stat(this.cacheDir);if (!fileStat.isDirectory) {await fileIo.mkdir(this.cacheDir, { recursive: true });}} catch (error) {// 目录可能不存在,创建它await fileIo.mkdir(this.cacheDir, { recursive: true });}}async put(key: string, imageData: Uint8Array): Promise<void> {const cachePath = this.getKeyPath(key);try {// 写入文件const file = await fileIo.open(cachePath,fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);await fileIo.write(file.fd, imageData);await fileIo.close(file);// 更新最后修改时间await fileIo.utimes(cachePath, Date.now() / 1000, Date.now() / 1000);// 检查缓存大小并清理await this.cleanupIfNeeded();} catch (error) {console.error("写入缓存失败:", error);}}async get(key: string): Promise<Uint8Array | null> {const cachePath = this.getKeyPath(key);try {// 检查文件是否存在await fileIo.access(cachePath);// 读取文件内容const file = await fileIo.open(cachePath, fileIo.OpenMode.READ_ONLY);const fileStat = await fileIo.stat(cachePath);const buffer = new ArrayBuffer(fileStat.size);await fileIo.read(file.fd, buffer);await fileIo.close(file);// 更新最后访问时间await fileIo.utimes(cachePath, Date.now() / 1000, Date.now() / 1000);return new Uint8Array(buffer);} catch (error) {// 文件不存在或读取失败return null;}}private getKeyPath(key: string): string {// 使用简单的哈希方式生成文件名let hash = 0;for (let i = 0; i < key.length; i++) {hash = (hash << 5) - hash + key.charCodeAt(i);hash |= 0; // 转换为32位整数}return `${this.cacheDir}/img_cache_${hash}.bin`;}private async cleanupIfNeeded(): Promise<void> {try {// 获取缓存目录中的所有文件const files = await fileIo.readdir(this.cacheDir);// 计算总大小let totalSize = 0;const fileStats: Array<{ path: string; size: number; mtime: number }> =[];for (const file of files) {const filePath = `${this.cacheDir}/${file}`;const stat = await fileIo.stat(filePath);if (!stat.isDirectory) {totalSize += stat.size;fileStats.push({path: filePath,size: stat.size,mtime: stat.mtime * 1000, // 转换为毫秒});}}// 如果超过最大大小,删除最旧的文件if (totalSize > this.maxSize) {// 按修改时间排序fileStats.sort((a, b) => a.mtime - b.mtime);while (totalSize > this.maxSize && fileStats.length > 0) {const oldest = fileStats.shift();if (oldest) {await fileIo.unlink(oldest.path);totalSize -= oldest.size;}}}} catch (error) {console.error("清理缓存失败:", error);}}
}
实际应用案例
列表图片优化加载
// 图片列表优化管理器
class OptimizedImageListManager {private diskCache: ImageDiskCache;private memoryCache: ImageMemoryCache;private pendingOperations: Map<string, Promise<image.PixelMap | null>>;constructor() {this.diskCache = new ImageDiskCache("internal://app/image_cache");this.memoryCache = new ImageMemoryCache();this.pendingOperations = new Map();}async loadImageForList(imageUri: string,targetSize: { width: number; height: number }): Promise<image.PixelMap | null> {// 生成缓存键,包含目标尺寸const cacheKey = `${imageUri}_${targetSize.width}x${targetSize.height}`;// 检查是否有相同的请求正在进行if (this.pendingOperations.has(cacheKey)) {return this.pendingOperations.get(cacheKey);}// 创建加载操作const loadOperation = this.doLoadImage(cacheKey, imageUri, targetSize);this.pendingOperations.set(cacheKey, loadOperation);try {return await loadOperation;} finally {// 移除已完成的操作this.pendingOperations.delete(cacheKey);}}private async doLoadImage(cacheKey: string,originalUri: string,targetSize: { width: number; height: number }): Promise<image.PixelMap | null> {// 1. 尝试从内存缓存获取const memoryCached = this.memoryCache.get(cacheKey);if (memoryCached) {return memoryCached;}// 2. 尝试从磁盘缓存获取const diskCached = await this.diskCache.get(cacheKey);if (diskCached) {// 从缓存数据创建PixelMapconst pixelMap = await this.createPixelMapFromData(diskCached);if (pixelMap) {await this.memoryCache.put(cacheKey, pixelMap);}return pixelMap;}// 3. 加载原图并处理try {const file = await fileIo.open(originalUri, fileIo.OpenMode.READ_ONLY);const imageSource = image.createImageSource(file.fd);// 解码选项,按目标尺寸缩放const decodingOptions: image.DecodingOptions = {desiredSize: targetSize,desiredPixelFormat: image.PixelMapFormat.RGBA_8888,};// 创建缩放后的PixelMapconst pixelMap = await imageSource.createPixelMap(decodingOptions);await fileIo.close(file);// 编码处理后的图片用于缓存const imagePacker = image.createImagePacker();const packingOptions: image.PackingOption = {format: "image/webp",quality: 80,};const packedData = await imagePacker.packing(pixelMap, packingOptions);// 保存到缓存await this.diskCache.put(cacheKey, packedData);await this.memoryCache.put(cacheKey, pixelMap);return pixelMap;} catch (error) {console.error("加载并处理图片失败:", error);return null;}}private async createPixelMapFromData(data: Uint8Array): Promise<image.PixelMap | null> {try {// 创建内存缓冲区const buffer = new ArrayBuffer(data.length);const view = new Uint8Array(buffer);for (let i = 0; i < data.length; i++) {view[i] = data[i];}// 从缓冲区创建ImageSourceconst imageSource = image.createImageSource(buffer);return await imageSource.createPixelMap();} catch (error) {console.error("从缓存数据创建PixelMap失败:", error);return null;}}
}
性能优化最佳实践
图片资源优化建议
-
选择合适的图片格式
- 照片类图片使用 JPEG 格式
- 图标和需要透明度的图片使用 PNG 或 WebP
- 追求最佳压缩率时使用 WebP
-
合理设置图片质量
- 列表图片: 60-70%
- 详情页图片: 75-85%
- 高清展示图片: 85-95%
-
预加载和懒加载结合
- 预加载可见区域附近的图片
- 懒加载远离可视区域的图片
-
避免重复解码
- 使用缓存机制减少重复解码
- 相同图片只解码一次
-
资源释放时机
- 页面离开时清理缓存
- 组件销毁时释放相关图片资源
代码优化建议
// 图片资源管理器 - 全局单例
class ImageResourceManager {private static instance: ImageResourceManager;private memoryCache: ImageMemoryCache;private diskCache: ImageDiskCache;private activeResources: Set<string>;private constructor() {this.memoryCache = new ImageMemoryCache();this.diskCache = new ImageDiskCache("internal://app/image_cache");this.activeResources = new Set();}public static getInstance(): ImageResourceManager {if (!ImageResourceManager.instance) {ImageResourceManager.instance = new ImageResourceManager();}return ImageResourceManager.instance;}// 注册使用中的资源public registerResource(resourceId: string): void {this.activeResources.add(resourceId);}// 注销不再使用的资源public unregisterResource(resourceId: string): void {this.activeResources.delete(resourceId);// 可以在这里添加资源清理逻辑}// 清理未使用的资源public async cleanupUnused(): Promise<void> {// 实现资源清理逻辑console.log("清理未使用的图片资源");}// 应用退出时释放所有资源public async releaseAll(): Promise<void> {// 释放所有缓存资源console.log("释放所有图片资源");}
}
结语
本文详细介绍了在 HarmonyOS 应用开发中进行图片压缩与格式转换的技术要点。通过合理运用这些技术,可以显著提升应用的性能表现,减少资源消耗,改善用户体验。
在实际开发过程中,建议结合应用的具体场景和需求,选择合适的图片处理策略。同时,也要注意在追求性能优化的同时,保证图片的显示质量,找到性能与质量之间的平衡点。
随着 HarmonyOS 的不断发展,相信未来会有更多高效的图片处理 API 和技术出现,让开发者能够更轻松地优化应用资源加载。;
// 3. 解码图片
const pixelMap = await imageSource.createPixelMap(decodingOptions);// 4. 创建打包选项
const packingOptions: image.PackingOption = {format: "image/jpeg",quality: quality // 压缩质量 0-100
};// 5. 创建ImagePacker并打包
const imagePacker = image.createImagePacker();
const packResult = await imagePacker.packing(pixelMap, packingOptions);// 6. 保存压缩后的图片
const targetFile = await fileIo.open(targetUri, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);
await fileIo.write(targetFile.fd, packResult);// 7. 释放资源
await fileIo.close(sourceFile);
await fileIo.close(targetFile);
pixelMap.release();return true;
} catch (error) {
console.error('图片压缩失败:', error);
return false;
}
}
### 智能尺寸压缩```typescript
// 根据目标尺寸智能压缩
async function smartCompressBySize(sourceUri: string,targetUri: string,maxWidth: number,maxHeight: number
): Promise<boolean> {try {const sourceFile = await fileIo.open(sourceUri, fileIo.OpenMode.READ_ONLY);const imageSource = image.createImageSource(sourceFile.fd);// 获取图片原始尺寸const imageInfo = await imageSource.getImageInfo();const originalWidth = imageInfo.size.width;const originalHeight = imageInfo.size.height;// 计算缩放比例const scale = Math.min(maxWidth / originalWidth, maxHeight / originalHeight, 1);const targetWidth = Math.round(originalWidth * scale);const targetHeight = Math.round(originalHeight * scale);const decodingOptions: image.DecodingOptions = {desiredSize: { width: targetWidth, height: targetHeight },desiredPixelFormat: image.PixelMapFormat.RGBA_8888};const pixelMap = await imageSource.createPixelMap(decodingOptions);const imagePacker = image.createImagePacker();const packingOptions: image.PackingOption = {format: "image/jpeg",quality: 85 // 保持较好质量的压缩};const packResult = await imagePacker.packing(pixelMap, packingOptions);const targetFile = await fileIo.open(targetUri, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);await fileIo.write(targetFile.fd, packResult);await fileIo.close(sourceFile);await fileIo.close(targetFile);pixelMap.release();console.log(`图片从 ${originalWidth}x${originalHeight} 压缩到 ${targetWidth}x${targetHeight}`);return true;} catch (error) {console.error('智能压缩失败:', error);return false;}
}
批量图片压缩
// 批量处理多张图片
class BatchImageCompressor {private maxConcurrent: number;constructor(maxConcurrent: number = 3) {this.maxConcurrent = maxConcurrent;}async compressImages(imageList: Array<{ source: string; target: string }>,quality: number): Promise<Array<{ source: string; target: string; success: boolean }>> {const results: Array<{ source: string; target: string; success: boolean }> =[];// 控制并发数量for (let i = 0; i < imageList.length; i += this.maxConcurrent) {const batch = imageList.slice(i, i + this.maxConcurrent);const batchPromises = batch.map(async (item) => {const success = await compressImage(item.source, item.target, quality);return { ...item, success };});const batchResults = await Promise.all(batchPromises);results.push(...batchResults);}return results;}
}// 使用示例
const compressor = new BatchImageCompressor(2);
const imagesToCompress = [{source: "internal://app/images/photo1.jpg",target: "internal://app/compressed/photo1.jpg",},{source: "internal://app/images/photo2.png",target: "internal://app/compressed/photo2.jpg",},{source: "internal://app/images/photo3.webp",target: "internal://app/compressed/photo3.jpg",},
];compressor.compressImages(imagesToCompress, 75).then((results) => {results.forEach((result) => {console.log(`图片 ${result.source} 压缩${result.success ? "成功" : "失败"}`);});
});
图片格式转换
基础格式转换
// 通用格式转换函数
async function convertImageFormat(sourceUri: string,targetUri: string,targetFormat: string,quality: number = 80
): Promise<boolean> {try {const sourceFile = await fileIo.open(sourceUri, fileIo.OpenMode.READ_ONLY);const imageSource = image.createImageSource(sourceFile.fd);// 解码原图const pixelMap = await imageSource.createPixelMap();// 格式转换打包选项const packingOptions: image.PackingOption = {format: targetFormat,quality: quality,};const imagePacker = image.createImagePacker();const packResult = await imagePacker.packing(pixelMap, packingOptions);// 保存转换后的图片const targetFile = await fileIo.open(targetUri,fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);await fileIo.write(targetFile.fd, packResult);await fileIo.close(sourceFile);await fileIo.close(targetFile);pixelMap.release();console.log(`格式转换完成: ${sourceUri} -> ${targetUri} (${targetFormat})`);return true;} catch (error) {console.error("格式转换失败:", error);return false;}
}
PNG 转 WebP 优化
// PNG转WebP专项优化
async function pngToWebPOptimized(sourceUri: string,targetUri: string,quality: number = 75
): Promise<{ success: boolean; originalSize: number; compressedSize: number }> {try {// 获取原文件大小const fileStats = await fileIo.stat(sourceUri);const originalSize = fileStats.size;const sourceFile = await fileIo.open(sourceUri, fileIo.OpenMode.READ_ONLY);const imageSource = image.createImageSource(sourceFile.fd);// 对于PNG转WebP,可以优化解码选项const decodingOptions: image.DecodingOptions = {desiredPixelFormat: image.PixelMapFormat.RGBA_8888,};const pixelMap = await imageSource.createPixelMap(decodingOptions);// WebP特定打包选项const packingOptions: image.PackingOption = {format: "image/webp",quality: quality,};const imagePacker = image.createImagePacker();const packResult = await imagePacker.packing(pixelMap, packingOptions);const targetFile = await fileIo.open(targetUri,fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);await fileIo.write(targetFile.fd, packResult);// 获取压缩后文件大小const compressedStats = await fileIo.stat(targetUri);const compressedSize = compressedStats.size;const compressionRatio = (((originalSize - compressedSize) / originalSize) *100).toFixed(2);console.log(`压缩率: ${compressionRatio}% (${originalSize} -> ${compressedSize} bytes)`);await fileIo.close(sourceFile);await fileIo.close(targetFile);pixelMap.release();return {success: true,originalSize,compressedSize,};} catch (error) {console.error("PNG转WebP失败:", error);return { success: false, originalSize: 0, compressedSize: 0 };}
}
支持的目标格式配置
// 支持的转换格式配置
const SupportedConversions = {JPEG: {format: "image/jpeg",extensions: [".jpg", ".jpeg"],maxQuality: 100,supportsLossless: false,},PNG: {format: "image/png",extensions: [".png"],maxQuality: 100,supportsLossless: true,},WEBP: {format: "image/webp",extensions: [".webp"],maxQuality: 100,supportsLossless: true,},
} as const;// 格式转换管理器
class FormatConversionManager {async convertWithOptions(sourceUri: string,targetUri: string,options: {targetFormat: keyof typeof SupportedConversions;quality?: number;maxWidth?: number;maxHeight?: number;}): Promise<boolean> {const formatConfig = SupportedConversions[options.targetFormat];const effectiveQuality = Math.min(options.quality || 80,formatConfig.maxQuality);try {const sourceFile = await fileIo.open(sourceUri,fileIo.OpenMode.READ_ONLY);const imageSource = image.createImageSource(sourceFile.fd);// 动态设置解码选项const decodingOptions: image.DecodingOptions = {desiredPixelFormat: image.PixelMapFormat.RGBA_8888,};if (options.maxWidth && options.maxHeight) {decodingOptions.desiredSize = {width: options.maxWidth,height: options.maxHeight,};}const pixelMap = await imageSource.createPixelMap(decodingOptions);const imagePacker = image.createImagePacker();const packingOptions: image.PackingOption = {format: formatConfig.format,quality: effectiveQuality,};const packResult = await imagePacker.packing(pixelMap, packingOptions);const targetFile = await fileIo.open(targetUri,fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);await fileIo.write(targetFile.fd, packResult);await fileIo.close(sourceFile);await fileIo.close(targetFile);pixelMap.release();return true;} catch (error) {console.error(`格式转换失败 [${options.targetFormat}]:`, error);return false;}}
}
高级优化技巧
渐进式加载优化
// 渐进式JPEG生成
async function createProgressiveJPEG(sourceUri: string,targetUri: string,quality: number = 75
): Promise<boolean> {try {const sourceFile = await fileIo.open(sourceUri, fileIo.OpenMode.READ_ONLY);const imageSource = image.createImageSource(sourceFile.fd);const pixelMap = await imageSource.createPixelMap();const imagePacker = image.createImagePacker();// 渐进式JPEG配置const packingOptions: image.PackingOption = {format: "image/jpeg",quality: quality,// 注意:HarmonyOS API 10中渐进式JPEG支持需要检查具体实现};const packResult = await imagePacker.packing(pixelMap, packingOptions);const targetFile = await fileIo.open(targetUri,fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);await fileIo.write(targetFile.fd, packResult);await fileIo.close(sourceFile);await fileIo.close(targetFile);pixelMap.release();return true;} catch (error) {console.error("生成渐进式JPEG失败:", error);return false;}
}
内存优化处理
// 内存优化的图片处理
class MemoryOptimizedImageProcessor {private readonly MAX_MEMORY_USAGE = 50 * 1024 * 1024; // 50MBasync processWithMemoryLimit(sourceUri: string,processingCallback: (pixelMap: image.PixelMap) => Promise<image.PixelMap>): Promise<boolean> {let sourceFile: fileIo.File | null = null;let originalPixelMap: image.PixelMap | null = null;let processedPixelMap: image.PixelMap | null = null;try {sourceFile = await fileIo.open(sourceUri, fileIo.OpenMode.READ_ONLY);const imageSource = image.createImageSource(sourceFile.fd);// 获取图片信息评估内存使用const imageInfo = await imageSource.getImageInfo();const estimatedMemory = imageInfo.size.width * imageInfo.size.height * 4; // RGBAif (estimatedMemory > this.MAX_MEMORY_USAGE) {// 大图片需要先缩放const scale = Math.sqrt(this.MAX_MEMORY_USAGE / estimatedMemory);const decodingOptions: image.DecodingOptions = {desiredSize: {width: Math.round(imageInfo.size.width * scale),height: Math.round(imageInfo.size.height * scale)}};originalPixelMap = await imageSource.createPixelMap(decodingOptions);} else {originalPixelMap = await imageSource.createPixelMap();}// 执行处理回调processedPixelMap = await processingCallback(originalPixelMap);return true;}