🏞️

【Android】mozjpeg を使って JPEG 画像を軽量化する

2024/02/08に公開

こんにちは!アルダグラムでエンジニアをしている渡邊です!

以前画像を軽量化するための調査を行った際に、mozjpeg というライブラリがあることを知りました。 今回はこの mozjpeg を使って Android アプリで JPEG 画像を軽量化させてみたいと思います。 ちなみにこのライブラリは C 言語で書かれているため、C 言語のコードを動かせる環境であれば Android アプリ以外でも使うことは可能です。

mozjpeg とは

mozjpegImproved JPEG encoder. と GitHub では記載されています。 このライブラリを使うことで通常よりさらに軽量化された JPEG 画像を出力させることができるようです。

そもそもこのライブラリを知ったきっかけは、Instagram の Android アプリのオープンソースライセンスにこのライブラリがあったので、どのようなライブラリか気になって調べてみたところからでした。

mozjpeg のビルド手順

前述の通り mozjpeg は C 言語で書かれたライブラリとなっています。 そのため Android アプリに組み込むために共有ライブラリ(.so 拡張子のファイル)にビルドし、そのファイルを Android プロジェクトに組み込む形になります。

Android のビルド手順はこちらに記載されていますが、そのままではビルドできなかったため、以下にビルド手順を紹介します。

1. mozjpeg のプロジェクトをチェックアウトする

まずは mozjpeg のプロジェクトをローカルに落とします。

2. 環境変数の設定

環境変数を設定する必要があります。 私は direnv を使って以下のように環境変数を設定しました。

.envrc
export PATH="$PATH:$ANDROID_HOME/cmake/3.22.1/bin" # Android SDK の中にある cmake を使うためパスを通す
export NDK_PATH="$ANDROID_HOME/ndk/21.4.7075529" # 使用したい NDK のバージョンのパスを指定
export TOOLCHAIN="clang"
export ANDROID_VERSION="21" # サポートしたい最小の SDK バージョンを指定

3. CMakeLists.txt の修正

java/CMakeLists.txt を開き、先頭に以下の行を加えます。 Android では AWT などのライブラリは不要なためです。

+set(JAVA_AWT_LIBRARY NotNeeded)
+set(JAVA_JVM_LIBRARY NotNeeded)
+set(JAVA_INCLUDE_PATH2 NotNeeded)
+set(JAVA_AWT_INCLUDE_PATH NotNeeded)
find_package(Java REQUIRED)
find_package(JNI REQUIRED)

4. build ディレクトリの作成

ビルド用にディレクトリを作成しておきます。

$ mkdir build
$ cd build

5. ビルドスクリプトの作成と実行

以下の内容で build.sh の名前でビルドスクリプトを作成します。 作成したら sh build.sh でビルドを実行します。

build.sh
build.sh
#!bin/bash

create_directory_if_not_exist() {
  dir="$1"

  if [ ! -d "$dir" ]; then
    mkdir -p "$dir"
  fi
}

# armv7
create_directory_if_not_exist armeabi-v7a
pushd armeabi-v7a

cmake -G"Unix Makefiles" \
  -DPNG_SUPPORTED=0 \
  -DWITH_JAVA=1 \
  -DANDROID_ABI=armeabi-v7a \
  -DANDROID_ARM_MODE=arm \
  -DANDROID_PLATFORM=android-${ANDROID_VERSION} \
  -DANDROID_TOOLCHAIN=${TOOLCHAIN} \
  -DCMAKE_ASM_FLAGS="--target=arm-linux-androideabi${ANDROID_VERSION}" \
  -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
  ../../
make

popd

# armv8
create_directory_if_not_exist arm64-v8a
pushd arm64-v8a

cmake -G"Unix Makefiles" \
  -DPNG_SUPPORTED=0 \
  -DWITH_JAVA=1 \
  -DANDROID_ABI=arm64-v8a \
  -DANDROID_ARM_MODE=arm \
  -DANDROID_PLATFORM=android-${ANDROID_VERSION} \
  -DANDROID_TOOLCHAIN=${TOOLCHAIN} \
  -DCMAKE_ASM_FLAGS="--target=aarch64-linux-android${ANDROID_VERSION}" \
  -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
  ../../
make

popd

# x86
create_directory_if_not_exist x86
pushd x86

cmake -G"Unix Makefiles" \
  -DPNG_SUPPORTED=0 \
  -DWITH_JAVA=1 \
  -DANDROID_ABI=x86 \
  -DANDROID_PLATFORM=android-${ANDROID_VERSION} \
  -DANDROID_TOOLCHAIN=${TOOLCHAIN} \
  -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
  ../../
make

popd

# x86-64
create_directory_if_not_exist x86-64
pushd x86-64

cmake -G"Unix Makefiles" \
  -DPNG_SUPPORTED=0 \
  -DWITH_JAVA=1 \
  -DANDROID_ABI=x86_64 \
  -DANDROID_PLATFORM=android-${ANDROID_VERSION} \
  -DANDROID_TOOLCHAIN=${TOOLCHAIN} \
  -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
  ../../
make

popd

6. ビルドの成果物ができているかの確認

ビルドが完了すると、以下のようにそれぞれの CPU アーキテクチャごとに libturbojpeg.so というファイルが作成されます。これらのファイルが作成されていればビルドは成功になります。

build
├── armeabi-v7a
│   └── libturbojpeg.so
├── arm64-v8a
│   └── libturbojpeg.so
├── x86
│   └── libturbojpeg.so
└── x86_64
    └── libturbojpeg.so

Android プロジェクトに組み込む

上記のライブラリをそのまま Android プロジェクトの以下のフォルダに格納します。

app/src/main/jniLibs
├── armeabi-v7a
│   └── libturbojpeg.so
├── arm64-v8a
│   └── libturbojpeg.so
├── x86
│   └── libturbojpeg.so
└── x86_64
    └── libturbojpeg.so

これらのライブラリには Java から JNI でアクセスします。 そのため mozjpeg の java フォルダ内にあるいくつかの Java ファイルも Android プロジェクトに追加する必要があります。 以下は追加したファイルの一覧です。

app/src/main/java/org/libjpegturbo/turbojpeg
├── TJ.java
├── TJCompressor.java
├── TJDecompressor.java
├── TJException.java
├── TJScalingFactor.java
└── YUVImage.java

続いて以下のように Android の Bitmap から mozjpeg で画像を軽量化してファイル出力するユーティリティメソッドを追加します。 このメソッドで Bitmap を指定した quality で JPEG 画像に変換します。

fun compressBitmap(bitmap: Bitmap, destFile: File, @IntRange(from = 50, to = 100) quality: Int) {
    val baos = ByteArrayOutputStream()
    baos.buffered().use { bos ->
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos)
        val byteArray = baos.toByteArray()

        val decompressedImageArray = TJDecompressor(byteArray).use { decompressor ->
            decompressor.decompress(bitmap.width, 0, bitmap.height, TJ.PF_RGB, 0)
        }

        TJCompressor(decompressedImageArray, 0, 0, bitmap.width, 0, bitmap.height, TJ.PF_RGB)
            .use { compressor ->
                compressor.setSubsamp(TJ.SAMP_420)
                compressor.setJPEGQuality(quality)
                val buffer = compressor.compress(0)
                BufferedOutputStream(destFile.outputStream()).use { bufferedOutputStream ->
                    bufferedOutputStream.write(buffer, 0, compressor.compressedSize)
                }
            }
    }
}

実行結果

こちらの画像を使ってどれくらい軽量になるのか試してみたいと思います。 画像サイズは 2304 x 1728 (593.4KB) となっており、この画像を一度 Android の Bitmap に変換し、画像サイズは変えず変換の際の quality をいくつか変更して画像ファイルとして出力させてみました。

また比較として以下の3パターンで検証します。

  • ① mozjpeg を使って JPEG 変換
  • ② Android SDK での JPEG 変換
  • ③ Webp に変換

結果としては以下のようになりました。

変換時の quality
80 353.8KB 444.7KB 293.7KB
85 417.8KB 512.7KB 373.7KB
90 522.3KB 685KB 508.3KB
95 710KB 862.2KB 742.7KB

上記から Webp の方がファイルサイズは小さくなっていますが、Android SDK で JPEG 画像に出力するよりは mozjpeg を使った方がファイルサイズは小さくなっています。

また画像の品質についてですが、quality 85 の結果の画像を以下に載せておきます。

画像にもよると思いますが、オリジナルの画像と比較してもどれも大して劣化を感じませんでした。

まとめ

mozjpeg を使って画像を軽量化させてみました。 Webp の方がサイズは小さくなりますが、Webp が使えず JPEG 画像のみを使わざるを得ない状況があれば役に立つかもしれません。
この記事がどなたかの参考になれば幸いです!

もっとアルダグラムエンジニア組織を知りたい人、ぜひ下記の情報をチェックしてみてください!

アルダグラム Tech Blog

Discussion