diff --git a/README.md b/README.md index 7231ada..6917fcf 100644 --- a/README.md +++ b/README.md @@ -20,5 +20,5 @@ repositories { ``` ```groovy -implementation 'com.github.awxkee:jxlcoder:1.0.4' // or any version above picker from release tags +implementation 'com.github.awxkee:jxlcoder:1.0.5' // or any version above picker from release tags ``` \ No newline at end of file diff --git a/app/src/main/assets/alpha_jxl.jxl b/app/src/main/assets/alpha_jxl.jxl new file mode 100644 index 0000000..278edf2 Binary files /dev/null and b/app/src/main/assets/alpha_jxl.jxl differ diff --git a/app/src/main/java/com/awxkee/jxlcoder/MainActivity.kt b/app/src/main/java/com/awxkee/jxlcoder/MainActivity.kt index bc1744d..16fa75e 100644 --- a/app/src/main/java/com/awxkee/jxlcoder/MainActivity.kt +++ b/app/src/main/java/com/awxkee/jxlcoder/MainActivity.kt @@ -22,8 +22,14 @@ class MainActivity : ComponentActivity() { val buffer1 = this.assets.open("first_jxl.jxl").source().buffer().readByteArray() val buffer2 = this.assets.open("second_jxl.jxl").source().buffer().readByteArray() - val image = JxlCoder().decode(buffer2) - val compressedBuffer = JxlCoder().encode(image) + val buffer3 = this.assets.open("alpha_jxl.jxl").source().buffer().readByteArray() + val image = JxlCoder().decode(buffer3) + val compressedBuffer = JxlCoder().encode( + image, + colorSpace = JxlColorSpace.RGBA, + compressionOption = JxlCompressionOption.LOOSY, + loosyLevel = 5.0f + ) val decompressedImage = JxlCoder().decode(compressedBuffer) setContent { diff --git a/jxlcoder/src/main/cpp/jxlcoder.cpp b/jxlcoder/src/main/cpp/jxlcoder.cpp index 6ab3961..53e517f 100644 --- a/jxlcoder/src/main/cpp/jxlcoder.cpp +++ b/jxlcoder/src/main/cpp/jxlcoder.cpp @@ -13,6 +13,16 @@ #include #include "android/bitmap.h" +enum jxl_colorspace { + rgb = 1, + rgba = 2 +}; + +enum jxl_compression_option { + loseless = 1, + loosy = 2 +}; + /** * Compresses the provided pixels. * @@ -22,7 +32,9 @@ * @param compressed will be populated with the compressed bytes */ bool EncodeJxlOneshot(const std::vector &pixels, const uint32_t xsize, - const uint32_t ysize, std::vector *compressed) { + const uint32_t ysize, std::vector *compressed, + jxl_colorspace colorspace, jxl_compression_option compression_option, + float compression_distance) { auto enc = JxlEncoderMake(/*memory_manager=*/nullptr); auto runner = JxlThreadParallelRunnerMake( /*memory_manager=*/nullptr, @@ -34,7 +46,15 @@ bool EncodeJxlOneshot(const std::vector &pixels, const uint32_t xsize, return false; } - JxlPixelFormat pixel_format = {3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + JxlPixelFormat pixel_format; + switch (colorspace) { + case rgb: + pixel_format = {3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + break; + case rgba: + pixel_format = {4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + break; + } JxlBasicInfo basic_info; JxlEncoderInitBasicInfo(&basic_info); @@ -42,17 +62,37 @@ bool EncodeJxlOneshot(const std::vector &pixels, const uint32_t xsize, basic_info.ysize = ysize; basic_info.bits_per_sample = 32; basic_info.exponent_bits_per_sample = 8; - basic_info.uses_original_profile = JXL_FALSE; + basic_info.uses_original_profile = compression_option == loosy ? JXL_FALSE : JXL_TRUE; basic_info.num_color_channels = 3; - basic_info.alpha_bits = 0; + + if (colorspace == rgba) { + basic_info.num_extra_channels = 1; + basic_info.alpha_bits = 8; + } + if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc.get(), &basic_info)) { fprintf(stderr, "JxlEncoderSetBasicInfo failed\n"); return false; } + switch (colorspace) { + case rgb: + basic_info.num_color_channels = 3; + break; + case rgba: + basic_info.num_color_channels = 4; + JxlExtraChannelInfo channelInfo; + JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &channelInfo); + channelInfo.bits_per_sample = 8; + channelInfo.alpha_premultiplied = false; + if (JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelInfo(enc.get(), 0, &channelInfo)) { + return false; + } + break; + } + JxlColorEncoding color_encoding = {}; - JxlColorEncodingSetToSRGB(&color_encoding, - /*is_gray=*/pixel_format.num_channels < 3); + JxlColorEncodingSetToSRGB(&color_encoding, pixel_format.num_channels < 3); if (JXL_ENC_SUCCESS != JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) { fprintf(stderr, "JxlEncoderSetColorEncoding failed\n"); @@ -69,6 +109,16 @@ bool EncodeJxlOneshot(const std::vector &pixels, const uint32_t xsize, fprintf(stderr, "JxlEncoderAddImageFrame failed\n"); return false; } + + if (compression_option == loseless && + JXL_ENC_SUCCESS != JxlEncoderSetFrameDistance(frame_settings, JXL_TRUE)) { + return false; + } else if (compression_option == loosy && + JXL_ENC_SUCCESS != + JxlEncoderSetFrameDistance(frame_settings, compression_distance)) { + return false; + } + JxlEncoderCloseInput(enc.get()); compressed->resize(64); @@ -223,6 +273,18 @@ jint throwCantCompressImage(JNIEnv *env) { return env->ThrowNew(exClass, ""); } +jint throwInvalidColorSpaceException(JNIEnv *env) { + jclass exClass; + exClass = env->FindClass("com/awxkee/jxlcoder/InvalidColorSpaceException"); + return env->ThrowNew(exClass, ""); +} + +jint throwInvalidCompressionOptionException(JNIEnv *env) { + jclass exClass; + exClass = env->FindClass("com/awxkee/jxlcoder/InvalidCompressionOptionException"); + return env->ThrowNew(exClass, ""); +} + extern "C" JNIEXPORT jobject JNICALL Java_com_awxkee_jxlcoder_JxlCoder_decodeImpl(JNIEnv *env, jobject thiz, jbyteArray byte_array) { @@ -273,7 +335,19 @@ Java_com_awxkee_jxlcoder_JxlCoder_decodeImpl(JNIEnv *env, jobject thiz, jbyteArr } extern "C" JNIEXPORT jbyteArray JNICALL -Java_com_awxkee_jxlcoder_JxlCoder_encodeImpl(JNIEnv *env, jobject thiz, jobject bitmap) { +Java_com_awxkee_jxlcoder_JxlCoder_encodeImpl(JNIEnv *env, jobject thiz, jobject bitmap, + jint javaColorSpace, jint javaCompressionOption, + jfloat compression_level) { + auto colorspace = static_cast(javaColorSpace); + if (!colorspace) { + throwInvalidColorSpaceException(env); + return static_cast(nullptr); + } + auto compression_option = static_cast(javaCompressionOption); + if (!compression_option) { + throwInvalidCompressionOptionException(env); + return static_cast(nullptr); + } AndroidBitmapInfo info; if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) { throwPixelsException(env); @@ -302,23 +376,31 @@ Java_com_awxkee_jxlcoder_JxlCoder_encodeImpl(JNIEnv *env, jobject thiz, jobject AndroidBitmap_unlockPixels(env, bitmap); std::vector rgbPixels; - rgbPixels.resize(info.width * info.height * 3); - libyuv::ARGBToRGB24(rgbaPixels.data(), static_cast(info.stride), rgbPixels.data(), - static_cast(info.width * 3), static_cast(info.width), - static_cast(info.height)); + switch (colorspace) { + case rgb: + rgbPixels.resize(info.width * info.height * 3); + libyuv::ARGBToRGB24(rgbaPixels.data(), static_cast(info.stride), rgbPixels.data(), + static_cast(info.width * 3), static_cast(info.width), + static_cast(info.height)); + break; + case rgba: + rgbPixels.resize(info.stride * info.height); + libyuv::ARGBToABGR(rgbaPixels.data(), static_cast(info.stride), rgbPixels.data(), + static_cast(info.stride), static_cast(info.width), + static_cast(info.height)); + break; + } rgbaPixels.clear(); rgbaPixels.resize(1); std::vector compressedVector; - if (!EncodeJxlOneshot(rgbPixels, info.width, info.height, &compressedVector)) { + if (!EncodeJxlOneshot(rgbPixels, info.width, info.height, &compressedVector, colorspace, + compression_option, compression_level)) { throwCantCompressImage(env); return static_cast(nullptr); } - rgbaPixels.clear(); - rgbaPixels.resize(1); - jbyteArray byteArray = env->NewByteArray((jsize) compressedVector.size()); char *memBuf = (char *) ((void *) compressedVector.data()); env->SetByteArrayRegion(byteArray, 0, (jint) compressedVector.size(), diff --git a/jxlcoder/src/main/java/com/awxkee/jxlcoder/InvalidColorSpaceException.kt b/jxlcoder/src/main/java/com/awxkee/jxlcoder/InvalidColorSpaceException.kt new file mode 100644 index 0000000..660c493 --- /dev/null +++ b/jxlcoder/src/main/java/com/awxkee/jxlcoder/InvalidColorSpaceException.kt @@ -0,0 +1,6 @@ +package com.awxkee.jxlcoder + +import androidx.annotation.Keep + +@Keep +class InvalidColorSpaceException: Exception("Invalid color space was provided") \ No newline at end of file diff --git a/jxlcoder/src/main/java/com/awxkee/jxlcoder/InvalidCompressionOptionException.kt b/jxlcoder/src/main/java/com/awxkee/jxlcoder/InvalidCompressionOptionException.kt new file mode 100644 index 0000000..d8c5889 --- /dev/null +++ b/jxlcoder/src/main/java/com/awxkee/jxlcoder/InvalidCompressionOptionException.kt @@ -0,0 +1,7 @@ +package com.awxkee.jxlcoder + +import androidx.annotation.Keep + +@Keep +class InvalidCompressionOptionException: Exception("Invalid compression option was provided") { +} \ No newline at end of file diff --git a/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlCoder.kt b/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlCoder.kt index 77e512e..144810d 100644 --- a/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlCoder.kt +++ b/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlCoder.kt @@ -1,6 +1,8 @@ package com.awxkee.jxlcoder import android.graphics.Bitmap +import androidx.annotation.FloatRange +import androidx.annotation.IntRange import androidx.annotation.Keep @Keep @@ -10,15 +12,33 @@ class JxlCoder { return decodeImpl(byteArray) } - fun encode(bitmap: Bitmap): ByteArray { - return encodeImpl(bitmap) + /** + * @param loosyLevel Sets the distance level for lossy compression: target max butteraugli + * * distance, lower = higher quality. Range: 0 .. 15. + * * 0.0 = mathematically lossless (however, use JxlEncoderSetFrameLossless + * * instead to use true lossless, as setting distance to 0 alone is not the only + * * requirement). 1.0 = visually lossless. Recommended range: 0.5 .. 3.0. Default + * * value: 1.0. + */ + fun encode( + bitmap: Bitmap, + colorSpace: JxlColorSpace = JxlColorSpace.RGB, + compressionOption: JxlCompressionOption = JxlCompressionOption.LOOSY, + @FloatRange(from = 0.0, to = 15.0) loosyLevel: Float = 1.0f, + ): ByteArray { + return encodeImpl(bitmap, colorSpace.cValue, compressionOption.cValue, loosyLevel) } private external fun decodeImpl( - byteArray: ByteArray, + byteArray: ByteArray ): Bitmap - private external fun encodeImpl(bitmap: Bitmap): ByteArray + private external fun encodeImpl( + bitmap: Bitmap, + colorSpace: Int, + compressionOption: Int, + loosyLevel: Float, + ): ByteArray companion object { // Used to load the 'jxlcoder' library on application startup. diff --git a/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlColorSpace.kt b/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlColorSpace.kt new file mode 100644 index 0000000..33e55ca --- /dev/null +++ b/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlColorSpace.kt @@ -0,0 +1,5 @@ +package com.awxkee.jxlcoder + +enum class JxlColorSpace(internal val cValue: Int) { + RGB(1), RGBA(2) +} \ No newline at end of file diff --git a/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlCompressionOption.kt b/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlCompressionOption.kt new file mode 100644 index 0000000..dc4c209 --- /dev/null +++ b/jxlcoder/src/main/java/com/awxkee/jxlcoder/JxlCompressionOption.kt @@ -0,0 +1,5 @@ +package com.awxkee.jxlcoder + +enum class JxlCompressionOption(internal val cValue: Int) { + LOSELESS(1), LOOSY(2) +} \ No newline at end of file