Jetpack ComposeでCoil からGif画像取得してPalette APIで色抽出してみる 

タイトル通りです。 普通にPNGとかなら楽勝ですが、Gifだとちょっと工夫が必要だったのでとうか

## Coilについて
### 概要
サーバURLから画像を取ってきて表示する　ということが簡単にできるライブラリです。

### コード
静的な画像は単純なので、ここではgifで取ってみる
```gradle
// implementation("io.coil-kt:coil-compose:2.3.0") // 通常はこっち
implementation("io.coil-kt:coil-gif:2.3.0")
```
```kotlin
@Composable
fun CoilOnlySample() {
    val url = "https://play.pokemonshowdown.com/sprites/ani/pikachu.gif"
    val context = LocalContext.current
    val imageLoader = ImageLoader.Builder(context)
        .components {
            if (SDK_INT >= 28) {
                add(ImageDecoderDecoder.Factory())
            } else {
                add(GifDecoder.Factory())
            }
        }
        .build()
    AsyncImage(
        modifier = Modifier,
        model = ImageRequest.Builder(LocalContext.current)
            .data(url)
            .crossfade(true)
            .build(),
        imageLoader = imageLoader,
        placeholder = painterResource(R.drawable.ic_placeholder),
        contentDescription = "",
        contentScale = ContentScale.Fit
    )
}
```

下の画像はPngなので動いてないですが、アプリでは動いている
<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/cfc40079-b8e6-7a20-58b1-90b8dc4e1b03.png">

### keyPoint
- ImageRequest
　
画像をリクエストする上で必要な情報を詰め込むと、指定したURLで画像をダウンロードしてくれる。

>ImageRequests are value objects that provide all the necessary information for an ImageLoader to load an image.

- ImageLoader
ざっくり、リクエストした画像をどんな画像として処理するかを定義できるオブジェクト

>ImageLoaders are service objects that execute ImageRequests. They handle caching, data fetching, image decoding, request management, bitmap pooling, memory management, and more. New instances can be created and configured using a builder:






## Paletteについて
### 概要
画像から色を抽出することができる。 デフォルトだと以下種類の色を取ってくる様子
- Light Vibrant
- Vibrant
- Dark Vibrant
- Light Muted
- Muted
- Dark Muted

イメージ
![Screenshot 2023-04-01 at 18.08.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/beaca367-f78a-0b4e-8cea-1c0be0f11b7d.png)

https://developer.android.com/training/material/palette-colors?hl=ja#extract-color-profiles


### コード
```gradle
android {
    // 28 or higher
    compileSdkVersion 28
    // ...
}

dependencies {
    implementation('androidx.palette:palette:1.0.0')
    // ...
}
```


長くなるので一部端折りますが、
```kotlin
@Preview
@Composable
fun PaletteOnlySample() {
    val context = LocalContext.current

    /* Convert our Image Resource into a Bitmap */
    val bitmap = remember {
        BitmapFactory.decodeResource(context.resources, com.example.core.R.drawable.sample)
    }

    /* Create the Palette, pass the bitmap to it */
    val palette = remember { Palette.from(bitmap).generate() }

    LazyColumn(
        modifier = Modifier
~
~
    ) {
        item { Text("rgb", fontSize = 24.sp) }
        item {
            LazyRow(
                contentPadding = PaddingValues(vertical = 12.dp),
                horizontalArrangement = Arrangement.spacedBy(4.dp)
            ) {
                item { Circle(palette.lightVibrantSwatch?.let { Color(it.rgb) } ?: Color.White, hint = "lightVibrantSwatch") }
~
~
        }


        item { Text("Text" , fontSize = 24.sp) }
        item {
            Text(
                text = "Sample Title Text", fontSize = 20.sp, fontWeight = FontWeight.Bold,
                color = palette.lightVibrantSwatch?.let { Color(it.titleTextColor) } ?: Color.White)
        }
        item {
            Text(
                text = "Sample Body Text", fontSize = 20.sp, fontWeight = FontWeight.Bold ,
                color = palette.lightVibrantSwatch?.let { Color(it.bodyTextColor) } ?: Color.White)
        }

        item { Text("Population", fontSize = 24.sp) }
        item { Text(text = "the number of pixels represented by this swatch") }
~
~

        item { Text("Sample Image", fontSize = 24.sp) }
        item { Image(painter = painterResource(id = com.example.core.R.drawable.sample_), contentDescription = "") }
    }
}

@Composable
fun Circle(
    color : Color,
    hint: String = "hint"
) {
    Box(
        modifier = Modifier
            .size(60.dp)
            .clip(CircleShape)
            .background(color = color)
    ) {
        Text(text = hint, modifier = Modifier.align(Alignment.Center))
    }
}
```
<img width="60%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/8e736e60-c897-c747-2526-b1435470fa36.png">

それぞれ、rgbだったり、テキストだったり、Pixel数が取れます。（なぜかLight Mutedがnullになってますが、、）



## CoilからGif取得してPalettで色抽出

### Coilから取得するGifをBitmapに変換する

ざっと見た感じ、PalettがBitmap以外の形式を受け入れてくれないので、Coilから取得するGif画像からBitmapを取得する方法があれば良い。
で、ちょいはまりどころがあったがGifからだと以下しかない様子

前準備 前述のPaletteOnlySampleを書き換え。　単純に外からBitmapを入れられるようにしたのみ（UI表示のため）
```kotlin
fun PaletteOnlySample(_bitmap: Bitmap? = null) {
    val context = LocalContext.current

    /* Convert our Image Resource into a Bitmap */
    val bitmap = remember {
        _bitmap ?: BitmapFactory.decodeResource(context.resources, R.drawable.sample)
    }
```

以下にて取得できる
はまりどころはコメント↓
```kotlin
@Composable
fun CoilAndPaletteSample(
    url: String = "https://play.pokemonshowdown.com/sprites/ani/pikachu.gif"
) {
    val context = LocalContext.current
    var bitmap: Bitmap? by remember { mutableStateOf(null) }

    val imageLoader = ImageLoader.Builder(context)
        .components {
            if (SDK_INT >= 28) {
                add(ImageDecoderDecoder.Factory())
            } else {
                add(GifDecoder.Factory())
            }
        }
        .build()

    /*
    Gif からの取得は現状、別で画像をリクエストして取得 -> bitmapとして変換
    という手順が必要
     */
    LaunchedEffect(Unit) {
        val req = ImageRequest.Builder(context)
            .data(url)
            .allowHardware(false)
            .build()

        val result = req.context.imageLoader.execute(req)

        if (result is SuccessResult) {
            bitmap = result.drawable.toBitmap() // Bitmap保管
        }
    }

    // gif でないならこれだけでもいける
//    val painter = rememberAsyncImagePainter(
//        model = url,
//        onState = { state ->
//            when(state) {
//                is AsyncImagePainter.State.Success -> {
//                    Log.d(TAG, "CoilAndPaletteSample: ${state.result.drawable.toBitmap()}")
//                    bitmap = state.result.drawable.toBitmap()
//                }
//                else -> Unit
//            }
//        }
//    )
//    Image(painter = painter, contentDescription = "")

    Column {
        AsyncImage(
            modifier = Modifier,
            model = ImageRequest.Builder(LocalContext.current)
                .data(url)
                .crossfade(true)
//            .target(  2.0.0 以降はrequest.target must be null.　というエラー
//                onSuccess = { result ->
//
//                }
//            )
                .build(),
            imageLoader = imageLoader,
            placeholder = painterResource(R.drawable.ic_placeholder),
            contentDescription = "",
            contentScale = ContentScale.Fit
        )
        bitmap?.let {
            PaletteOnlySample(it)
        }
    }
}
```

コメントにも書いてますが、Gif画像のみをロードしてBitmap変換が必要な様子。

理由は上述の`target(onSuccess(result))`が使えないため。（これが使えればresult.toBitmap()でとれた）

gifじゃなければ`rememberAsyncImagePainter`のみでいける


rgb　みる限り取れてますね。

<img width="60%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/a45f97d8-8c17-f77b-9a50-0fe1cdf667a0.gif">!


全体はこちら
https://github.com/kkhouse/Playground/tree/main/AndroidProject/Other/CoilAndPaletteSample

### 余談
ChatGPTに聞こうと思ったが良いプロンプトが浮かばなかったので、手作業ったはまった。。