画像にモザイク処理を行う

# 作った理由
`AGSL`を使えば`Shader`を使ってエフェクトを書けることができ、下記の記事のように`AGSL`を使ってモザイクをかける処理を実装されている方もいます。
[AGSLで遊んでみる](https://qiita.com/keseran/items/a1ce402c312c6f3f7e0e)

しかし、`AGSL`はAndroid13以降でないと対応しておらず、プロジェクト作成時に確認できるデバイスの割合も33%程度は留まっており、別の方法で実装可能か確認がてら作ってみました。

# 実装例
今回はこちらのページを参考に、カスタムペインタとして実装しています。特筆すべきことも無いので、処理の内容はコメントを見てください。
[カスタム ペインタ](https://developer.android.com/develop/ui/compose/graphics/images/custompainter?hl=ja)

```kotlin:MosaicPainter.kt
class MosaicPainter(
    private val image: ImageBitmap,
    private val blockSize: Int
) : Painter() {

    override val intrinsicSize: Size get() = Size(image.width.toFloat(), image.height.toFloat())

    override fun DrawScope.onDraw() {

        // 画像と画面のサイズ比を取得
        val scale = image.width / size.width

        // ある程度ブロックサイズが大きい場合にモザイク処理を行う
        if (blockSize > 5) {
            for (x in 0 until size.width.roundToInt() step blockSize) {
                for (y in 0 until size.height.roundToInt() step blockSize) {

                    // 画像内での値に変換
                    val imageX = (x * scale).roundToInt()
                    val imageY = (y * scale).roundToInt()
                    val imageBlockSize = (blockSize * scale).roundToInt()

                    // 読み込む範囲を画像サイズ内に収める
                    val blockWidth = if (imageX + imageBlockSize < image.width) {
                        imageBlockSize
                    } else {
                        image.width - imageX
                    }
                    val blockHeight = if (imageY + imageBlockSize < image.height) {
                        imageBlockSize
                    } else {
                        image.height - imageY
                    }

                    // BitmapImageから指定範囲を読み込み
                    val buffer = IntArray(blockWidth * blockHeight)
                    image.readPixels(
                        buffer = buffer,
                        startX = imageX,
                        startY = imageY,
                        width = blockWidth,
                        height = blockHeight
                    )

                    // 範囲内の色を平均化しブロックを描画
                    drawRect(
                        averagingColor(buffer),
                        Offset(x.toFloat(), y.toFloat()),
                        Size(blockSize.toFloat(), blockSize.toFloat())
                    )
                }
            }
        } else {
            // ブロックサイズが小さい場合は画像をそのまま描画
            drawImage(
                image = image,
                dstSize = IntSize(
                    size.width.roundToInt(),
                    size.height.roundToInt()
                )
            )
        }
    }
}

/**
 * 平均化したColorを返す
 */
private fun averagingColor(
    buffer: IntArray
): Color {
    var r = 0L
    var g = 0L
    var b = 0L
    buffer.forEach {
        r += (it shr 16) and 0xff
        g += (it shr 8) and 0xff
        b += it and 0xff
    }
    return Color(
        (r / buffer.size).toInt(),
        (g / buffer.size).toInt(),
        (b / buffer.size).toInt())
}
```

# 呼び出し方
```kotlin:MainContent.kt
@Composable
fun MainContent() {

    val bitmap = ImageBitmap.imageResource(R.drawable.gundam)
    var blockSize by remember { mutableIntStateOf(0) }
    val mosaicPainter = remember(blockSize) {
        MosaicPainter(bitmap, blockSize)
    }

    Box() {
        Image(
            modifier = Modifier.align(Alignment.Center),
            painter = mosaicPainter,
            contentDescription = null
        )
        Box(
            modifier = Modifier
                .wrapContentSize()
                .align(Alignment.BottomCenter)
                .padding(all = 32.dp)
                .clip(shape = RoundedCornerShape(50))
                .background(color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f))
        ) {
            Slider(
                modifier = Modifier
                    .align(Alignment.Center)
                    .padding(horizontal = 16.dp),
                value = blockSize.toFloat(),
                valueRange = 0f..100f,
                steps = 9,
                onValueChange = { blockSize = it.roundToInt() }
            )
        }
    }
}
```
# 実際の動作
画像はららぽーとで撮った𝛎ガンダムです。

![Screen_recording_20241226_145104-ezgif.com-video-to-gif-converter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115846/7a6198a2-90ce-3fa0-90bc-5bcfaa166067.gif)

# 作ってみて
ブロックが小さくなると若干もたつく感じはするものの、思ってたよりはスムーズに動作しました。ただ、カスタム ペインタのページに下記の注意書きがあるため、DrawModifierを使った実装も試してみたいと思います。

>測定またはレイアウトに影響を与える必要がある場合は、Painter を使用する必要があります。指定の境界内でしかレンダリングを行わない場合は、DrawModifier を使用してください。