ネットワーク上から保存した JPG 画像の画質を比較 & Android の DownloadManager を使用して罠にハマってみた件


第一部 〜ネットワーク上から保存した JPG 画像の画質を比較〜

概要

Android アプリ開発中に、ネットワーク上から保存した JPG 画像の画質に微妙な差があることに気付いたので、同一の JPG 画像をダウンロードしてサイズの比較を行ってみます。

手順

Google+ に検証用の JPG 画像を添付して投稿
投稿した JPG 画像を自作の Android アプリから URL を指定してダウンロード
端末内に保存された JPG 画像のサイズを確認します

Google+ に投稿する写真(元画像)

サイズ 172,109 バイト(176KB)
大きさ 458 × 458

検証では大きさの 458 x 458 が変わることはありませんでしたが、サイズの値は変化しました。

画像の取得方法

java.net.URL を用いて画像を取得します(「Android Studio : インターネット上の画像を取得して、Bitmap か Drawable で ImageView に表示する」を参考にしました)。

検証パターン

取得した画像を InputStreamBitmap の状態で保存してみます。

AndroidManifest.xml<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> を記述するのをお忘れなく

InputStream の状態で保存する

画像の保存→表示」を参考にしました。

サイズ 172,207 バイト(176KB)
大きさ 458 × 458

見た感じはオリジナル画像と変わらない気がします。しかしながらサイズが微増しています

Bitmap の状態から保存する

Androidで非同期で画像をサーバからダウンロードしてSDカードに保存する」と「Bitmap画像をsdに保存」を参考にしました。両方とも同じ結果になりました。

サイズ 186,378 バイト(188KB)
大きさ 458 × 458

JPG 画像の白色部分にガサガサが見えるようになりました。そして 176 KB → 188 KB とオリジナル画像よりサイズが大きくなっています
InputStream が生データなのに対して Bitmap は一度加工しているため画質に差が生じています。

Android の Google+ アプリでダウンロードする

サイズ 172,109 バイト(176KB)
大きさ 458 × 458

オリジナル画像と同じサイズの画像を保存することができました。一体どのようにして保存しているのでしょうか(?)

※ 現在の Google+ アプリからは画像を直接保存する機能が無くなっているようでした。画像詳細画面 >「シェア」ボタンをタップ >「フォトにアップロード」を選択すれば Google フォト に画像を保存することができました

第二部 〜DownloadManager との邂逅〜

この記事を公開した後

@KanaSakaguchi さんから「DownloadManager を使用すればオリジナルと同様のサイズで画像を保存できるのではないか」という旨のコメントを頂きました。

DownloadManager とは?

ネットワーク上にある .jpg .png などの画像ファイルや .mp4 などの動画ファイル、音声ファイルなどを保存することができます。

DownloadManager を使用してみる

JavaのDownloadManagerを使ってファイルをダウンロード」を参考にしました。

サイズ 172,109 バイト(176KB)
大きさ 458 × 458

Android 端末に保存された画像を Mac に転送してサイズを確認してみたところ、オリジナルと同じサイズで保存できていることが分かりました

DownloadManager に書き換えてアプリをリリースしてみた結果

バグが3件見つかりました

1件目の不具合

Crashlytics にクラッシュレポートが送られていました。

Fatal Exception: java.lang.IllegalArgumentException
Unknown URL content://downloads/my_downloads

エラー文で検索したところ StackOverflow の「How to Enable Android Download Manager」に辿り着きました。

設定 > [アプリケーション] > すべて(システムのアプリを表示) > ダウンロードマネージャー が無効になっている状態で DownloadManager.enqueue() するとアプリが落ちるので対策が必要になります。

2件目の不具合

Crashlytics にレポートが届いていました。

Fatal Exception: java.lang.IllegalStateException
Unable to create directory:

N-05D (Android 4.0.4) などのプライマリストレージが SD カードになっているスマホからプライマリストレージの SD カードを取り除いた状態にして setDestinationInExternalPublicDir (String dirType, String subPath) を実行するとアプリが落ちます。
Nexus 5Nexus 7 (2013) などの新機種はプライマリストレージが内蔵ストレージになっており SD カードのように取り外しができないため上記エラーは発生しないと思われます(Crashlytics のレポートが送信されてきたのは Android 4.4.4 以下の古い端末でしたので)。

※ 参考にしたサイト Android におけるストレージまとめ

3件目の不具合

Google Play ストアのレビューやカスタマーサポートに「画像が保存できない」というクレームが届くようになりました。端末は N-04E (Android 4.1.2) で発生するとのこと。
原因を調べていると 設定 > [アプリ] > すべて(システムのアプリを表示) > ダウンロードマネージャー のデータ使用量が増えている状態では Response Headers に ETag の付いていない画像を保存できなくなるということが分かりました(※ ネットワーク上の画像の Response Headers に ETag が付いているかは Google Chrome Developer Tools を使用して調べました)。
N-04E 端末内の標準ブラウザも DownloadManager を使用しているため同様の現象が再現します。
対策としては How to Enable Android Download Manager を参考に DownloadManager での保存に失敗した場合にダウンロードマネージャーの設定に誘導して「データを消去」ボタンを押してもらうようにするか、自社サーバーのファイルのみダウンロードするアプリの場合は Response Headers に ETag を設定するというのが考えられます。
この不具合は Android 4.1 系周辺バージョンのみで発生している気がするので、下位バージョンをサポートしていないアプリでは特に対策を考える必要は無いかも知れません。

それでも ETag 無しの画像を保存したい場合は

Jake Wharton 様の Picasso 2 OkHttp 3 DownloaderOkHttp を使用すれば画質劣化ゼロでネットワーク上の画像を保存することができました。

ImageLoadHelper.java
public class ImageLoadHelper {

    private static OkHttp3Downloader downloader;

    private synchronized static OkHttp3Downloader getOkHttp3DownloaderInstance() {
        if (downloader == null) {
           downloader = new OkHttp3Downloader(new OkHttpClient());
        }
        return downloader;
    }

    /**
      * 画像データの InputStream を返す
      *
      * @param url
      * @return
      * @throws IOException
      */
    public static InputStream getInputStream(String url) throws IOException {
        Downloader.Response response = getOkHttp3DownloaderInstance().load(Uri.parse(url), 1);
        return response.getInputStream();
    }
}

手順

RxJava の非同期処理中に上記の getInputStream()を実施
取得した画像の InputStream を端末内に保存する

番外編 〜Android の Google Chrome アプリで画像保存してみる〜

サイズ 172,109 バイト(176KB)
大きさ 458 × 458

Android 端末に保存された画像を Mac に転送してサイズを確認したところ、オリジナルと同じサイズの画像を保存できていることが分かりました。
設定からダウンロードマネージャーを無効にした後 Google Chrome から画像を保存すると「失敗しました」と Snackbar 表示されるのでダウンロードマネージャーが使用されていることが分かります。
他にも Google Play や Google Drive, Slack アプリもダウンロードマネージャーを使用していることが確認できました。