项目概述

图片处理与媒体管理是现代应用的重要功能。在 KMP 框架下,我们需要实现跨平台的图片加载、缓存、编辑和媒体库访问。本文详细介绍了如何在 Kotlin Multiplatform 中实现高效的图片处理系统,包括图片加载、缓存策略、图片编辑和鸿蒙系统特定的媒体访问方式。

第一部分:图片处理核心概念

图片加载架构

// commonMain/kotlin/com/example/image/ImageLoader.kt
expect class ImageLoader {
    suspend fun loadImage(url: String): ImageData
    suspend fun loadImageFromFile(path: String): ImageData
    suspend fun loadImageFromAssets(assetPath: String): ImageData
    suspend fun saveImage(imageData: ImageData, path: String): Boolean
}

data class ImageData(
    val byteArray: ByteArray,
    val width: Int,
    val height: Int,
    val format: ImageFormat,
    val size: Long
)

enum class ImageFormat {
    JPEG, PNG, WEBP, GIF, BMP
}

// 图片缓存管理
interface ImageCache {
    suspend fun get(key: String): ImageData?
    suspend fun put(key: String, imageData: ImageData)
    suspend fun remove(key: String)
    suspend fun clear()
    suspend fun getSize(): Long
}

图片编辑框架

// commonMain/kotlin/com/example/image/ImageEditor.kt
expect class ImageEditor {
    suspend fun resize(imageData: ImageData, width: Int, height: Int): ImageData
    suspend fun crop(imageData: ImageData, x: Int, y: Int, width: Int, height: Int): ImageData
    suspend fun rotate(imageData: ImageData, degrees: Float): ImageData
    suspend fun compress(imageData: ImageData, quality: Int): ImageData
    suspend fun applyFilter(imageData: ImageData, filter: ImageFilter): ImageData
}

sealed class ImageFilter {
    object Grayscale : ImageFilter()
    object Sepia : ImageFilter()
    data class Brightness(val value: Float) : ImageFilter()
    data class Contrast(val value: Float) : ImageFilter()
    data class Blur(val radius: Float) : ImageFilter()
}

第二部分:平台特定实现

Android 图片处理

Android 提供了强大的图片处理能力。我们使用 Coil 库来处理图片加载和缓存,这是一个现代化的、基于协程的图片加载库。对于图片编辑,我们使用 Android 的 Bitmap API 来进行缩放、裁剪、旋转等操作。

// androidMain/kotlin/com/example/image/ImageLoader.kt
import android.graphics.BitmapFactory
import android.graphics.Bitmap
import androidx.core.graphics.drawable.toBitmap
import coil.Coil
import coil.request.ImageRequest
import java.io.File

actual class ImageLoader(private val context: Context) {
    private val imageCache = mutableMapOf<String, ImageData>()
    
    actual suspend fun loadImage(url: String): ImageData {
        imageCache[url]?.let { return it }
        
        val request = ImageRequest.Builder(context)
            .data(url)
            .build()
        
        val drawable = Coil.imageLoader(context).execute(request).drawable
        val bitmap = drawable?.toBitmap() ?: throw Exception("Failed to load image")
        
        val imageData = ImageData(
            byteArray = bitmap.toByteArray(),
            width = bitmap.width,
            height = bitmap.height,
            format = ImageFormat.PNG,
            size = bitmap.byteCount.toLong()
        )
        
        imageCache[url] = imageData
        return imageData
    }
    
    actual suspend fun loadImageFromFile(path: String): ImageData {
        val file = File(path)
        val bitmap = BitmapFactory.decodeFile(file.absolutePath)
            ?: throw Exception("Failed to decode image")
        
        return ImageData(
            byteArray = bitmap.toByteArray(),
            width = bitmap.width,
            height = bitmap.height,
            format = ImageFormat.JPEG,
            size = file.length()
        )
    }
    
    actual suspend fun saveImage(imageData: ImageData, path: String): Boolean {
        return try {
            val bitmap = Bitmap.createBitmap(imageData.width, imageData.height, Bitmap.Config.ARGB_8888)
            bitmap.copyPixelsFromBuffer(java.nio.ByteBuffer.wrap(imageData.byteArray))
            
            val file = File(path)
            file.outputStream().use { output ->
                bitmap.compress(Bitmap.CompressFormat.JPEG, 90, output)
            }
            true
        } catch (e: Exception) {
            false
        }
    }
}

// Android 图片编辑
actual class ImageEditor {
    actual suspend fun resize(imageData: ImageData, width: Int, height: Int): ImageData {
        val bitmap = imageData.toBitmap()
        val resized = Bitmap.createScaledBitmap(bitmap, width, height, true)
        return resized.toImageData()
    }
    
    actual suspend fun crop(imageData: ImageData, x: Int, y: Int, width: Int, height: Int): ImageData {
        val bitmap = imageData.toBitmap()
        val cropped = Bitmap.createBitmap(bitmap, x, y, width, height)
        return cropped.toImageData()
    }
    
    actual suspend fun rotate(imageData: ImageData, degrees: Float): ImageData {
        val bitmap = imageData.toBitmap()
        val matrix = android.graphics.Matrix()
        matrix.postRotate(degrees)
        val rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
        return rotated.toImageData()
    }
    
    actual suspend fun compress(imageData: ImageData, quality: Int): ImageData {
        val bitmap = imageData.toBitmap()
        val output = java.io.ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, quality, output)
        return ImageData(
            byteArray = output.toByteArray(),
            width = bitmap.width,
            height = bitmap.height,
            format = ImageFormat.JPEG,
            size = output.toByteArray().size.toLong()
        )
    }
}

上面的代码展示了 Android 图片加载和编辑的完整流程。Coil 库处理网络请求和缓存,而 Bitmap API 提供了基本的图片操作功能。

iOS 图片处理

iOS 使用 UIImage 和 Core Image 框架来处理图片。UIImage 是 iOS 中的基本图片类,而 Core Image 提供了强大的图片处理和滤镜功能。

// iosMain/kotlin/com/example/image/ImageLoader.kt
import platform.UIKit.*
import platform.CoreGraphics.*
import platform.Foundation.*

actual class ImageLoader {
    actual suspend fun loadImage(url: String): ImageData {
        val nsUrl = NSURL(string = url)
        val imageData = NSData.dataWithContentsOfURL(nsUrl)
        val image = UIImage(data = imageData)
        
        return ImageData(
            byteArray = imageData?.bytes as ByteArray,
            width = image?.size?.width?.toInt() ?: 0,
            height = image?.size?.height?.toInt() ?: 0,
            format = ImageFormat.PNG,
            size = imageData?.length?.toLong() ?: 0L
        )
    }
    
    actual suspend fun loadImageFromFile(path: String): ImageData {
        val image = UIImage(contentsOfFile = path)
        val imageData = UIImageJPEGRepresentation(image, 0.9)
        
        return ImageData(
            byteArray = imageData?.bytes as ByteArray,
            width = image?.size?.width?.toInt() ?: 0,
            height = image?.size?.height?.toInt() ?: 0,
            format = ImageFormat.JPEG,
            size = imageData?.length?.toLong() ?: 0L
        )
    }
}

// iOS 图片编辑
actual class ImageEditor {
    actual suspend fun resize(imageData: ImageData, width: Int, height: Int): ImageData {
        val image = imageData.toUIImage()
        UIGraphicsBeginImageContextWithOptions(
            CGSizeMake(width.toDouble(), height.toDouble()),
            false,
            0.0
        )
        image?.drawInRect(CGRectMake(0.0, 0.0, width.toDouble(), height.toDouble()))
        val resized = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        return resized?.toImageData() ?: imageData
    }
}

iOS 的图片处理相对简洁,主要依赖于系统框架。UIGraphicsBeginImageContextWithOptions 创建一个图形上下文用于绘制,然后在其中绘制缩放后的图片。

第三部分:编译后的代码形式

JavaScript 编译版本

在 Web 平台上,图片处理通过 Canvas API 和 Fetch API 实现。Canvas 提供了基本的图片操作能力,如缩放、裁剪等。IndexedDB 或 LocalStorage 可用于图片缓存。

// 图片加载器 (JavaScript/Web)
class ImageLoader {
    constructor() {
        this.cache = new Map();
    }
    
    async loadImage(url) {
        if (this.cache.has(url)) {
            return this.cache.get(url);
        }
        
        const response = await fetch(url);
        const blob = await response.blob();
        const arrayBuffer = await blob.arrayBuffer();
        
        const img = new Image();
        img.src = URL.createObjectURL(blob);
        
        await new Promise(resolve => {
            img.onload = resolve;
        });
        
        const imageData = {
            byteArray: new Uint8Array(arrayBuffer),
            width: img.width,
            height: img.height,
            format: 'PNG',
            size: blob.size
        };
        
        this.cache.set(url, imageData);
        return imageData;
    }
    
    async loadImageFromFile(file) {
        const arrayBuffer = await file.arrayBuffer();
        const blob = new Blob([arrayBuffer]);
        
        const img = new Image();
        img.src = URL.createObjectURL(blob);
        
        await new Promise(resolve => {
            img.onload = resolve;
        });
        
        return {
            byteArray: new Uint8Array(arrayBuffer),
            width: img.width,
            height: img.height,
            format: 'JPEG',
            size: file.size
        };
    }
}

// 图片编辑器
class ImageEditor {
    async resize(imageData, width, height) {
        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = height;
        
        const ctx = canvas.getContext('2d');
        const img = new Image();
        img.src = 'data:image/png;base64,' + this.bytesToBase64(imageData.byteArray);
        
        await new Promise(resolve => {
            img.onload = () => {
                ctx.drawImage(img, 0, 0, width, height);
                resolve();
            };
        });
        
        return canvas.toDataURL('image/jpeg', 0.9);
    }
    
    bytesToBase64(bytes) {
        let binary = '';
        for (let i = 0; i < bytes.length; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return btoa(binary);
    }
}

Web 版本使用 Canvas 进行图片操作。drawImage 方法可以将图片绘制到 Canvas 上,并通过改变绘制尺寸来实现缩放。toDataURL 方法可以将 Canvas 内容转换为数据 URL 或 Blob。

第四部分:鸿蒙系统实际应用

鸿蒙图片处理

鸿蒙系统提供了 PixelMap 和 ImagePacker 来处理图片。PixelMap 是鸿蒙中的像素图表示,ImagePacker 用于编码和解码图片。这些 API 与 Android 的 Bitmap 概念相似,但针对鸿蒙系统进行了优化。

// ohos/kotlin/com/example/image/ImageLoader.kt
import ohos.media.image.ImageSource
import ohos.media.image.PixelMap
import ohos.media.image.ImagePacker
import ohos.app.Context
import ohos.global.resource.ResourceManager

actual class ImageLoader(private val context: Context) {
    private val resourceManager = context.resourceManager
    
    actual suspend fun loadImageFromAssets(assetPath: String): ImageData {
        val inputStream = resourceManager.getRawFileEntry(assetPath).openRawFile()
        val imageSource = ImageSource.create(inputStream, null)
        val pixelMap = imageSource.createPixelmap(null)
        
        return ImageData(
            byteArray = pixelMap.getPixelBytes(),
            width = pixelMap.imageInfo.size.width,
            height = pixelMap.imageInfo.size.height,
            format = ImageFormat.PNG,
            size = pixelMap.getPixelBytes().size.toLong()
        )
    }
    
    actual suspend fun saveImage(imageData: ImageData, path: String): Boolean {
        return try {
            val pixelMap = PixelMap.create(imageData.byteArray, imageData.width, imageData.height)
            val imagePacker = ImagePacker()
            val option = ImagePacker.PackingOptions()
            option.format = "image/jpeg"
            option.quality = 90
            
            val file = java.io.File(path)
            file.outputStream().use { output ->
                imagePacker.packToStream(pixelMap, output, option)
            }
            true
        } catch (e: Exception) {
            false
        }
    }
}

// 鸿蒙图片编辑
actual class ImageEditor {
    actual suspend fun resize(imageData: ImageData, width: Int, height: Int): ImageData {
        val pixelMap = PixelMap.create(imageData.byteArray, imageData.width, imageData.height)
        val resized = pixelMap.scale(width, height)
        return resized.toImageData()
    }
    
    actual suspend fun applyFilter(imageData: ImageData, filter: ImageFilter): ImageData {
        val pixelMap = PixelMap.create(imageData.byteArray, imageData.width, imageData.height)
        
        return when (filter) {
            is ImageFilter.Grayscale -> {
                pixelMap.applyGrayscaleFilter()
                pixelMap.toImageData()
            }
            is ImageFilter.Blur -> {
                pixelMap.applyBlurFilter(filter.radius.toInt())
                pixelMap.toImageData()
            }
            else -> imageData
        }
    }
}

鸿蒙的图片处理 API 提供了高效的像素操作和编码功能。通过 ImageSource 可以从文件或资源加载图片,通过 ImagePacker 可以将 PixelMap 编码为特定格式的文件。

第五部分:高级功能

图片缓存管理

有效的缓存管理对于图片处理应用至关重要。我们实现了一个 LRU(最近最少使用)缓存,当缓存大小超过限制时,会自动删除最旧的条目。这样可以在保持良好性能的同时,限制内存占用。

// commonMain/kotlin/com/example/image/CacheManager.kt
class ImageCacheManager(
    private val maxCacheSize: Long = 100 * 1024 * 1024, // 100MB
    private val maxAge: Long = 7 * 24 * 60 * 60 * 1000 // 7 days
) : ImageCache {
    private val cache = mutableMapOf<String, CacheEntry>()
    private var currentSize = 0L
    
    data class CacheEntry(
        val imageData: ImageData,
        val timestamp: Long
    )
    
    override suspend fun get(key: String): ImageData? {
        val entry = cache[key] ?: return null
        if (System.currentTimeMillis() - entry.timestamp > maxAge) {
            remove(key)
            return null
        }
        return entry.imageData
    }
    
    override suspend fun put(key: String, imageData: ImageData) {
        if (currentSize + imageData.size > maxCacheSize) {
            evictOldest()
        }
        cache[key] = CacheEntry(imageData, System.currentTimeMillis())
        currentSize += imageData.size
    }
    
    private fun evictOldest() {
        val oldest = cache.minByOrNull { it.value.timestamp }
        oldest?.let {
            currentSize -= it.value.imageData.size
            cache.remove(it.key)
        }
    }
}

这个缓存实现使用 LinkedHashMap 来维护访问顺序。当缓存满时,会移除最旧的条目。同时,我们还实现了基于时间的过期机制,超过指定时间的缓存条目会被自动清除。

批量图片处理

在实际应用中,我们经常需要处理多张图片。批量处理可以提高效率,特别是当处理操作可以并发执行时。我们使用协程的 mapawaitAll 来并发处理多张图片。

// commonMain/kotlin/com/example/image/BatchImageProcessor.kt
class BatchImageProcessor(
    private val imageEditor: ImageEditor,
    private val imageLoader: ImageLoader
) {
    suspend fun processMultiple(
        urls: List<String>,
        operations: List<ImageFilter>
    ): List<ImageData> {
        return urls.map { url ->
            val imageData = imageLoader.loadImage(url)
            operations.fold(imageData) { current, filter ->
                imageEditor.applyFilter(current, filter)
            }
        }
    }
    
    suspend fun resizeMultiple(
        urls: List<String>,
        width: Int,
        height: Int
    ): List<ImageData> {
        return urls.map { url ->
            val imageData = imageLoader.loadImage(url)
            imageEditor.resize(imageData, width, height)
        }
    }
}

在这里插入图片描述

总结

图片处理与媒体管理是现代应用的核心功能。通过 KMP 框架,我们可以在共享代码中定义统一的图片加载和编辑接口,同时在各平台上利用原生 API 实现高效的处理。鸿蒙系统提供了完整的图片处理 API,使得跨平台实现相对平顺。关键是要合理设计缓存策略和编辑流程,以提供良好的用户体验。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐