【Android】mozjpeg を使って JPEG 画像を軽量化する
こんにちは!アルダグラムでエンジニアをしている渡邊です!
以前画像を軽量化するための調査を行った際に、mozjpeg というライブラリがあることを知りました。 今回はこの mozjpeg を使って Android アプリで JPEG 画像を軽量化させてみたいと思います。 ちなみにこのライブラリは C 言語で書かれているため、C 言語のコードを動かせる環境であれば Android アプリ以外でも使うことは可能です。
mozjpeg とは
mozjpeg は Improved JPEG encoder.
と GitHub では記載されています。 このライブラリを使うことで通常よりさらに軽量化された JPEG 画像を出力させることができるようです。
そもそもこのライブラリを知ったきっかけは、Instagram の Android アプリのオープンソースライセンスにこのライブラリがあったので、どのようなライブラリか気になって調べてみたところからでした。
mozjpeg のビルド手順
前述の通り mozjpeg は C 言語で書かれたライブラリとなっています。 そのため Android アプリに組み込むために共有ライブラリ(.so
拡張子のファイル)にビルドし、そのファイルを Android プロジェクトに組み込む形になります。
Android のビルド手順はこちらに記載されていますが、そのままではビルドできなかったため、以下にビルド手順を紹介します。
1. mozjpeg のプロジェクトをチェックアウトする
まずは mozjpeg のプロジェクトをローカルに落とします。
2. 環境変数の設定
環境変数を設定する必要があります。 私は direnv を使って以下のように環境変数を設定しました。
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
#!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です。 世界中のノンデスクワーク業界における現場の生産性アップを実現する現場DXサービス「KANNA」を開発しています。 採用情報はこちら: herp.careers/v1/aldagram0508/
Discussion