coil-kt / coil

Image loading for Android and Compose Multiplatform.
https://coil-kt.github.io/coil/
Apache License 2.0
10.69k stars 648 forks source link

Add support for HEIC images in Compose Multiplatform #2318

Closed antailyaqwer closed 3 months ago

antailyaqwer commented 3 months ago

Is your feature request related to a problem? Please describe. In skia (as well as in skiko) HEIC format doesn't supported. This leads to incorrect flow on modern ios devices for image rendering (for example, custom media picker).

My idea is to add support of it inside a library.

Describe the solution you'd like I have a simple workaround of this problem: convert heic image to jpeg. My solution isn't elegant enough as I don't have access to coil internal methods. I think there is a better way to support it

var builder = ImageRequest.Builder(LocalPlatformContext.current)
    .data(uri) // uri is usually a path. For example, /var/storage/image.HEIC

if (uri.substringAfterLast('.').equals("HEIC", ignoreCase = true)) {
    builder = builder.decoderFactory { result, options, imageLoader ->
        HEICImageDecoderFactory().create(result, options, imageLoader)
    }
}

AsyncImage(
    model = builder.build(),
    contentDescription = null,
)

expect class HEICImageDecoder(
    source: ImageSource,
    options: Options,
) : Decoder

// Decoder logic
class HEICImageDecoderFactory : Decoder.Factory {

    override fun create(
        result: SourceFetchResult,
        options: Options,
        imageLoader: ImageLoader,
    ): Decoder {
        return HEICImageDecoder(result.source, options)
    }
}

// ios implementation of expect class
private const val COMPRESSION = 0.3

actual class HEICImageDecoder actual constructor(
    private val source: ImageSource,
    private val options: Options
) : Decoder {

    @OptIn(ExperimentalCoilApi::class)
    override suspend fun decode(): DecodeResult {
        val originalBytes = source.source().use { it.readByteArray() }
        val jpegBytes = originalBytes.toJpegBytes()
        val image = Image.makeFromEncoded(jpegBytes)

        val isSampled: Boolean
        val bitmap: Bitmap
        try {
            bitmap = Bitmap.makeFromImage(image, options)
            bitmap.setImmutable()
            isSampled = bitmap.width < image.width || bitmap.height < image.height
        } finally {
            image.close()
        }

        return DecodeResult(
            image = bitmap.asCoilImage(),
            isSampled = isSampled,
        )
    }
}

@OptIn(ExperimentalForeignApi::class)
private fun ByteArray.toJpegBytes(): ByteArray = memScoped {
    val image = UIImage(data = this@toJpegBytes.toNSData())

    return UIImageJPEGRepresentation(image, compressionQuality = COMPRESSION)!!.toByteArray()
}
colinrtwhite commented 3 months ago

I think this is best implemented as an external library to Coil since it could be implemented with a custom Decoder.Factory and doesn't require access to Coil's internals. HEIC is a complex format and adding support for it on all of Coil's platforms would be a large task.