DEV Community

Cover image for Android NDK: Native Bitmap Compression
Rhony
Rhony

Posted on • Edited on

Android NDK: Native Bitmap Compression

Introduction

Previously we are working on implementing native android logging. In this series we will play with native bitmap compression. We already know that by using native code it will reduce the CPU overhead and make the performance faster.

If you are a skipper person 🤣 that just want to jumping into the code you can find the source code at the bottom.

Continue...

Continuing from our previous article, we now create:

  • bitmap folder inside our nativelib library.
  • create a new C++ file bitmap.cpp and check the header option to automatically create our bitmap.h file Bitmap files

Open the bitmap.h file then we need to add our new function:
jbyteArray compressBitmap(JNIEnv *env, jobject bitmap, jint format, jint quality);

Which remains our header file become like this:

#ifndef HI_NDK_BITMAP_H
#define HI_NDK_BITMAP_H

#include <jni.h>

jbyteArray compressBitmap(JNIEnv *env, jobject bitmap, jint format, jint quality);

#endif //HI_NDK_BITMAP_H
Enter fullscreen mode Exit fullscreen mode

Now switching the into our bitmap.cpp file, we will do our actual implementation here. Looking at the official documentation there is a AndroidBitmap_compress function which requires a few parameters:

int AndroidBitmap_compress(
  const AndroidBitmapInfo *info,
  int32_t dataspace,
  const void *pixels,
  int32_t format,
  int32_t quality,
  void *userContext,
  AndroidBitmap_CompressWriteFunc fn
)
Enter fullscreen mode Exit fullscreen mode

So how do we use supply all of the required parameters ? It seems weird that the official documentation doesn't provide the sample, that would be different if we are reading another part of the android development for ex: jetpack compose, we will find bunch of implementation example there.

Here for simplicity, this is the compressBitmap function:

// Struct to store compression data
struct CompressionContext {
    JNIEnv *env;
    std::vector<uint8_t> buffer;
};

// Write callback function for compressed data
bool writeCompressedData(void *userContext, const void *data, size_t size) {
    if (userContext == nullptr || data == nullptr || size == 0) {
        LogPrint(ANDROID_LOG_ERROR, LOG_TAG, "Invalid write callback parameters");
        return false;
    }
    auto *ctx = static_cast<CompressionContext *>(userContext);
    ctx->buffer.insert(ctx->buffer.end(), (uint8_t *) data, (uint8_t *) data + size);

    LogPrint(ANDROID_LOG_DEBUG, LOG_TAG, "Compressed Data Written");
    return true;
}

jbyteArray compressBitmap(JNIEnv *env, jobject bitmap, jint format, jint quality) {
    ..

    if (format != ANDROID_BITMAP_COMPRESS_FORMAT_JPEG &&
        format != ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSY) {
        LogPrint(ANDROID_LOG_ERROR, LOG_TAG,
                 "Invalid format, use JPEG or WebP LOSSY for compression");
        return nullptr;
    }

    ...

    // Compress the Bitmap
    int result = AndroidBitmap_compress(&info, dataspace, pixels, format, quality, &ctx,
                                        writeCompressedData);

    ...

    return compressedData;
}
Enter fullscreen mode Exit fullscreen mode

If we take a look on the format, the image compression should done only on jpeg and webp lossy, for the PNG version it wouldn't be supported. You can refer here for more details about the format.

Head to our CMakeList.txt file, we need to register our bitmap class and folder so our nativelib.cpp will be able to call the compressBitmap function similiarly like we are doing before on native android logging.

  • Register jnigraphics library
target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
          android
        log
        jnigraphics
)
Enter fullscreen mode Exit fullscreen mode
  • Set the target sources
target_sources(
        ${CMAKE_PROJECT_NAME}
        PRIVATE
        ${NATIVE_SRC_DIR}/nativelib.cpp
        ${NATIVE_SRC_DIR}/logging/logging.cpp
        ${NATIVE_SRC_DIR}/bitmap/bitmap.cpp
)
Enter fullscreen mode Exit fullscreen mode
  • Then register the folder
target_include_directories(
        ${CMAKE_PROJECT_NAME}
        PRIVATE
        ${NATIVE_SRC_DIR}/logging
        ${NATIVE_SRC_DIR}/bitmap
)
Enter fullscreen mode Exit fullscreen mode

Moving to our NativeLib class then we register the function:
external fun compressBitmap(bitmap: Bitmap, format: Int, quality: Int): ByteArray?

Here we can make some optimization by encapsulating the NativeLib this is to avoid that our single library exposing all of our native functions. In this case we can create a new interface BitmapUtils

interface BitmapUtils {
    fun compressBitmap(bitmap: Bitmap, format: Int, quality: Int): ByteArray?

    companion object {
        val instance: BitmapUtils by lazy { NativeLib }
    }
}
Enter fullscreen mode Exit fullscreen mode

Then implement the interface into our NativeLib class:

internal object NativeLib : BitmapUtils {

    init {
        System.loadLibrary("nativelib")
    }

    ...

    external override fun compressBitmap(bitmap: Bitmap, format: Int, quality: Int): ByteArray?
}
Enter fullscreen mode Exit fullscreen mode

See that we are now locking the access into internal scope then calling our bitmap function as simply just by calling BitmapUtils.instance. You can found the project repository below, the project is already migrated to using Dependency Injection and there is a new ImageProcessor class to separate the our kotlin image compression logic

For the quality parameter, the maximum value is 100 the highest value is set, the image compression will almost didn't do anything. Meaning, if we want to see a contrast effect between the original image and the compressed we can use lowest quality

On the video above, i already made a TextField so than we can set the quality as we want.

Let's play with the quality parameter.

Quality Comparison Table

Quality 0 Quality 40 Original Size

 

Looking at the quality 40 below

From the above screenshoot data, the image still in good condition altought we put the quality below < 50. So what is the quality here ? Does it represented as percent ?
The answer is NO

The quality parameter in AndroidBitmap_compress does not directly correspond to file size percentage reduction. Instead, it represents the compression level for lossy formats (such as JPEG and WebP Lossy)

What Does the quality Parameter Mean?

  • The value ranges from 0 to 100:

    • 0 → Maximum compression (smallest file size, lowest quality)
    • 100 → No compression (largest file size, highest quality)
  • It does NOT guarantee exact file size reduction.

    • Example: If an image is 100KB, setting quality = 50 does not mean it will become 50KB.
    • The actual file size depends on the image content, resolution, and format.

How Quality Affects Compression ?
We can say that using some of the number between the below range will produce certain compression.

Quality (%) Effect on Image Effect on File Size
100 No compression, max quality Largest file size (almost original)
90 High quality, minor loss Slightly reduced file size
75 Medium compression, minor artifacts Noticeable reduction (~30-50%)
50 High compression, visible quality loss Strong reduction (~50-70%)
25 Very compressed, blocky artifacts Significant size reduction (~70-90%)
0 Maximum compression, poor quality Smallest file size, heavy artifacts

Final Thoughts

For most cases, a quality setting of 70-80% offers a great balance between file size and image clarity, but the optimal value will always depend on your specific use case. If performance is critical, leveraging NDK-based compression can provide significant speed and memory advantages.

If you’re working with large images or performance-intensive applications, it’s always a good idea to experiment with different quality values and measure their impact on file size and visual fidelity.

Optimize wisely, and happy coding!

Source code: ndk-playground

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more