🤖

Android Glideを拡張する

2022/03/29に公開1

Android用画像読み込みライブラリGlideでapngを扱えるようにしようとしたところ
うまくいかなくて解決するのに時間がかかってしまったので、解決方法を書きます。

svgを扱えるようにしてみる

com.caverock:androidsvg-aar:1.4というライブラリをGlideで使えるようにします。

まずGlideModuleを作成します。
ドキュメント通りにAppGlideModuleを継承します。
ちなみにですがModuleという名前からわかるように、
ここからGlideのインスタンスの設定をおこなっているようです。

@GlideModule(glideName = "GlideApp")
class MiGlideModule : AppGlideModule(){

    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
    
    }


    override fun isManifestParsingEnabled(): Boolean {
        return false
    }
}

InputStreamをSVGに変換する処理を追加する

svgの処理を追加する
Glideはappend, prependで任意の画像処理を追加することができます。
appendとprependの違いとしては、Glideは画像のDecoder処理を
Chain of Responsibility状の処理になっていて、
前のDecoder判定処理に失敗したら次のDecoder処理に回されていくような仕組みになっています。
そのため真っ先に処理させたい場合はprepend,後から処理させたい場合はappendにします。
prependは雑に処理してしまうとGlideでデフォルトで入ってる画像のDecoderの処理を邪魔してしまうため、
それら処理を上書きする意図がなければappendにします。
SVGはGlideの標準処理では全て処理できない扱いになるのでappendにします。
処理の内容としてはInputStreamを受け取って, SvgDecoderでSVGクラスに変換しています。

@GlideModule(glideName = "GlideApp")
class MiGlideModule : AppGlideModule(){

    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        registry
            .append(InputStream::class.java, SVG::class.java, SvgDecoder())

    }


    override fun isManifestParsingEnabled(): Boolean {
        return false
    }
}

次にSvgDecoderクラスを任意のパッケージに作成します。
handlersはこの画像がこのDecoderで処理できる画像なのかを判定しています。
trueを返した場合は判定可能。falseを返した場合は判定不能という扱いになります。
先ほどのappend, prependの話になりますが、
問答無用でtrueを返すようにするとこの後にappendをしてもそのappendしたDecoderは処理されないと言った現象が発生するようになってしまいます。

decodeは第一引数の入力(InputStream)から得られた入力値から、
第二引数の出力(SVG)を出力する処理を記述しています。


private const val SVG_HEADER: Int = 0x3C737667
private const val SVG_HEADER_STARTS_WITH_XML = 0x3c3f786d
class SvgDecoder : ResourceDecoder<InputStream, SVG>{


    override fun handles(source: InputStream, options: Options): Boolean {
        val buffer = ByteArray(8)
        val cnt = source.read(buffer)
        if (cnt < 8) {
            Log.d("SvgDecoder", "svgではない")
            return false
        }

        Log.d("SvgDecoder", "svgだった")
        val header = ByteBuffer.wrap(buffer).int
        return header == SVG_HEADER || header == SVG_HEADER_STARTS_WITH_XML
    }

    override fun decode(
        source: InputStream,
        width: Int,
        height: Int,
        options: Options
    ): Resource<SVG>? {
        return try {
            val svg = SVG.getFromInputStream(source)
            SimpleResource(svg)
        }catch(e: SVGParseException){
            Log.e("SvgDecoder", "error", e)
            null
        }

    }
}

SVGをDrawableにする

SvgDecoderをappendすることによって、
InputStreamからSVGを得られるようになりましたが、
Androidでは直接SVGを扱うことができないので、
Drawableに変換する処理を追加します。

公式ではPictureDrawableを使用していますがPictureDrawableはリサイズや複雑なことをすることができないので、一度BitmapDrawableに変換しています。

package あなたのパッケージ名
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Picture
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.PictureDrawable
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.resource.SimpleResource
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
import com.caverock.androidsvg.SVG

class SvgBitmapTransCoder(val context: Context): ResourceTranscoder<SVG, BitmapDrawable>{
    override fun transcode(toTranscode: Resource<SVG>, options: Options): Resource<BitmapDrawable> {
        val svg = toTranscode.get()
        val picture: Picture = svg.renderToPicture()
        val drawable = PictureDrawable(picture)

        val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        canvas.drawPicture(drawable.picture)
        return SimpleResource(BitmapDrawable(context.resources, bitmap))
    }
}

GlideModuleに追加してください。
※省略しています。

registry.register(SVG::class.java, BitmapDrawable::class.java, SvgBitmapTransCoder(context))

apngを扱えるようにする

以下のようなライブラリを使うことで簡単にapngの対応を行うことができますが、
このライブラリの処理系を見てみると明らかGlideの標準処理より非効率な処理を行なっている上に
壊れたGifを読み込むとアプリがクラッシュするといったことがあるため、
なるべくライブラリへの依存は少なくしたいところです。
そこでこのライブラリのapngの部分だけをGlideにうまく組み込んでみたいと思います。
https://github.com/penfeizhou/APNG4Android

apng対応の厄介なところ

svg対応はHeaderで判定することができるのですが、
apngはヘッダーがpngになってしまうので、png == apng判定をしてしまうと、
pngを処理できなくなってしまうので注意が必要です。(ライブラリ次第)

build.gradleにライブラリを追加する

バージョンはライブラリを参照してください。

dependencies {
	implementation 'com.github.penfeizhou.android.animation:apng:${VERSION}'
}

apngのDecoderを追加する。

ソースコードの内容は参考にしたライブラリの丸パクリです
判定処理でまずヘッダーがPNGであるかを判定しています。
この判定を入れることで、余分なデータを読み込む必要がないので、
オーバーヘッドを少なくすることができます。
最終的にAPNGParserのisAPNG判定をすることで、
pngの処理を潰してしまわないようにしています。
ちなみにByteBufferにしている理由としては、
Glideのソースコードを見たところpngなどの処理はまずByteBufferを通して処理をしているためだったと思います(うろ覚え)

const val PNG: Long = 0x89504E47


class ByteBufferApngDecoder : ResourceDecoder<ByteBuffer, FrameSeqDecoder<*, *>> {


    override fun decode(
        source: ByteBuffer,
        width: Int,
        height: Int,
        options: Options
    ): Resource<FrameSeqDecoder<*, *>> {
        val loader: Loader = object : ByteBufferLoader() {
            override fun getByteBuffer(): ByteBuffer {
                source.position(0)
                return source
            }
        }
        val decoder = APNGDecoder(loader, null)
        return FrameSeqDecoderResource(decoder, source.limit())
    }

    override fun handles(source: ByteBuffer, options: Options): Boolean {
        Log.d("ByteBufferApngDecoder", "apng decoder on decode")
        val byteBufferArray = ByteArray(8)
        source.get(byteBufferArray, 0, 4)
        val header = ByteBuffer.wrap(byteBufferArray).long ushr 32
        if (header != PNG) {
            Log.d("ByteBufferApngDecoder", "is not png:${header.toHexString()}")
            return false
        }

        val result = APNGParser.isAPNG(ByteBufferReader(source))
        Log.d("ByteBufferApngDecoder", "handlers isApng:$result")
        return result

    }

    private class FrameSeqDecoderResource(
        private val decoder: FrameSeqDecoder<*, *>,
        private val size: Int
    ) : Resource<FrameSeqDecoder<*, *>> {
        override fun getResourceClass(): Class<FrameSeqDecoder<*, *>> {
            return FrameSeqDecoder::class.java
        }

        override fun get(): FrameSeqDecoder<*, *> {
            return decoder
        }

        override fun getSize(): Int {
            return size
        }

        override fun recycle() {
            decoder.stop()
        }
    }

}

Glide Moduleに追加する

最終的なGlideModuleです。
本当はClassに切り出すべきですが、ベタ書きをしてしまいました。
SVGの時の違いとしては、

  • ソースがさらに汚い
  • transcode処理でリソースの解放処理を行なっている
  • appendではなくprepandを使用している

特にappendではなくprependを使用している理由としては、
GlideのpngのDecoderより先に判定をする必要があったからです。


@GlideModule(glideName = "GlideApp")
class MiGlideModule : AppGlideModule(){

    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        registry
            .prepend(ByteBuffer::class.java, FrameSeqDecoder::class.java, ByteBufferApngDecoder())
            .register(FrameSeqDecoder::class.java, Drawable::class.java, object : ResourceTranscoder<FrameSeqDecoder<*, *>, Drawable> {
                override fun transcode(
                    toTranscode: Resource<FrameSeqDecoder<*, *>>,
                    options: Options
                ): Resource<Drawable> {
                    val apngDrawable = APNGDrawable(toTranscode.get() as APNGDecoder)
                    apngDrawable.setAutoPlay(false)
                    return object : DrawableResource<Drawable>(apngDrawable) {
                        override fun getResourceClass(): Class<Drawable> {
                            return Drawable::class.java
                        }

                        override fun getSize(): Int {
                            return apngDrawable.memorySize
                        }

                        override fun recycle() {
                            apngDrawable.stop()
                        }

                    }
                }
            })
            .register(SVG::class.java, BitmapDrawable::class.java, SvgBitmapTransCoder(context))
            .append(InputStream::class.java, SVG::class.java, SvgDecoder())

    }


    override fun isManifestParsingEnabled(): Boolean {
        return false
    }
}

以上です。

Discussion