Glideで独自形式の画像読み込みに対応する

はじめまして、アプリ開発グループの大場です。 2021年10月に中途入社し、ニコニコ漫画と読書メーターのAndroidアプリを主に担当しています。

ニコニコ漫画では以前より画像ライブラリにPicasso、Glideの二種を並行利用していましたが、先日Glideに完全移行しました。今回はGlide移行時において理解に時間のかかった部分である、独自形式の画像読み込みについて記載していきます。

これからGlideについてキャッチアップする方の参考になれば幸いです。

なお、記事中のライブラリ、言語のバージョンは以下の通りです。

ライブラリ名 バージョン
Kotlin 1.6.10
Picasso 2.71828
Glide 4.12.0

目次:

Picassoでの実装例

ニコニコ漫画ではセキュリティ上の観点から、漫画の画像(いわゆる原稿)にはDRMが付与されています。DRM画像は暗号化されているため、画像ライブラリの標準読み込み関数でそのまま表示することは出来ません。その対処としてアプリではPicassoのDownloaderインターフェースを実装することで、DRM画像の復号化を行っていました。

Picassoを利用した実装例は以下です。

class DecryptDownloader(context: Context) : Downloader {  
  
    private val downloader: OkHttp3Downloader = OkHttp3Downloader(context)  
  
    override fun load(request: Request): Response {  
        val response = downloader.load(request)  
        return decryptDrm(response)  
    }  
  
    fun decryptDrm(response: Response): Response {  
        /* 画像の復号化 */
    }  
  
    override fun shutdown() {  
        downloader.shutdown()  
    }
}

// このように使う
Picasso.Builder(context)  
    .downloader(DecryptDownloader(context))  
    .build()  
    .load(/* Image Url */)
    .into(/* ImageView */))

前述の通りPicassoではDownloaderインターフェースの実装を行うことによって、標準の読み込み処理の実装を差し替えることができます。

Glideではこの仕組みがModel , ModelLoader , DataFetcherインターフェースによってより細かく設定できるようになっています。

Picassoでの実装をGlideに置き換えた例を見ていきましょう。

Glideでの実装例

1. Modelの定義

サーバー上の画像ファイルを特定のアルゴリズムで復号するためのリクエストとしてDecryptRequestを定義します。 ここで定義するModelの型はなんでもよいのですが、Stringだと味気ないので今回は独自にDecryptRequestという名前で定義しています。

data class DecryptRequest(val url: GlideUrl)

2.DataFetcherの実装

GlideではDataFetcherというインターフェースを実装することで、どのようにデータを取得して、どの型で返すかを決めることが出来ます。Glideの標準画像デコーダーはInputStreamまたはByteBufferに対応しているため、今回はInputStreamを最終的なデータ型として返すようにしました。

class DrmImageFetcher(private val decryptRequest: DecryptRequest) : DataFetcher<InputStream> {  
  
    private val urlFetcher: HttpUrlFetcher = HttpUrlFetcher(decryptRequest.url, 5000)  
  
    override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {  
        urlFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {  
            override fun onDataReady(data: InputStream?) {  
                callback.onDataReady(decrypt(data))  
            }  
  
            override fun onLoadFailed(e: Exception) {  
                callback.onLoadFailed(e)  
            }  
        })  
    }  
  
    fun decrypt(rawData: InputStream?): InputStream? {  
        // 実際の復号化処理を記述します。今回はそのまま返します。
        return rawData  
    }  
  
    override fun cleanup() {  
        urlFetcher.cleanup()  
    }  
  
    override fun cancel() {  
        urlFetcher.cancel()  
    }  
  
    override fun getDataClass(): Class<InputStream> {  
        return InputStream::class.java  
    }  
  
    override fun getDataSource(): DataSource {  
        return DataSource.REMOTE  
    }  
  
}

3.ModelLoaderの実装

作成したModelDataFetcherを紐付けるためのModelLoaderインターフェースを実装します。コード中のhandles関数は今回DecryptRequestDrmImageFetcherが必ず1:1で紐付いているので 強制的にtrueを返しています。もし1:nの関係になる場合はhandles関数で条件分岐を行いましょう。

class DrmImageLoader : ModelLoader<DecryptRequest, InputStream> {  
  
    override fun buildLoadData(  
        model: DecryptRequest,  
        width: Int,  
        height: Int,  
        options: Options  
    ): LoadData<InputStream> {  
        return LoadData(  
            model.url,  
            DrmImageFetcher(model)  
        )  
    }  
  
    override fun handles(model: DecryptRequest): Boolean { 
        // 今回は必ずDecryptRequestとDrmImageFetcherが紐づくので強制的にtrueを返す
        return true  
    }  
  
    class Factory : ModelLoaderFactory<DecryptRequest, InputStream> {  
        override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<DecryptRequest, InputStream> =  
            DrmImageLoader()  
        override fun teardown() = Unit  
    }  
  
}

4.Glide Moduleへの登録

最後にGlide ModuleへModelLoaderを登録します。

@GlideModule  
public final class AppGlideModule: AppGlideModule() {  
  
    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {  
        registry.prepend(DecryptRequest::class.java, InputStream::class.java, DrmImageLoader.Factory())  
    }  
  
}

使用例

GlideModuleを利用してDRM画像を読み込む実装例です。Picassoでの実装例と同等のコードになったことがわかるかと思います。

GlideApp  
    .with(this)  
    .load(DecryptRequest(GlideUrl(/* 画像URL */)))  
    .into(/* Target ImageView */)

まとめ

Glideを使って独自形式の画像読み込みに対応する実装例を紹介しました。
その他にもGlideにはいろいろな機能がありますので、興味のある方はドキュメントを調べてみてください。

これからもよりよいユーザー体験を目指して開発面でも様々な改善を行っていきますので、どうぞよろしくお願いいたします。