From f58ed6f88b67ff7445d270c46e7e05e9a7c18479 Mon Sep 17 00:00:00 2001 From: awxkee Date: Sat, 9 Mar 2024 14:47:01 +0000 Subject: [PATCH] Fixed colorspace bug --- .../java/com/awxkee/jxlcoder/MainActivity.kt | 80 +++--- jxlcoder/src/main/cpp/JniDecoding.cpp | 3 +- .../cpp/JxlAnimatedDecoderCoordinator.cpp | 3 +- jxlcoder/src/main/cpp/JxlEncoder.cpp | 2 +- .../main/cpp/colorspaces/ColorSpaceProfile.h | 6 +- .../jxlcoder/animation/AnimatedDrawable.kt | 235 ++++++++++++++++++ .../jxlcoder/animation/AnimatedFrameStore.kt | 39 +++ .../jxlcoder/animation/JxlAnimatedStore.kt | 54 ++++ 8 files changed, 379 insertions(+), 43 deletions(-) create mode 100644 jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/AnimatedDrawable.kt create mode 100644 jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/AnimatedFrameStore.kt create mode 100644 jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/JxlAnimatedStore.kt diff --git a/app/src/main/java/com/awxkee/jxlcoder/MainActivity.kt b/app/src/main/java/com/awxkee/jxlcoder/MainActivity.kt index 07bcbfb..2e896fd 100644 --- a/app/src/main/java/com/awxkee/jxlcoder/MainActivity.kt +++ b/app/src/main/java/com/awxkee/jxlcoder/MainActivity.kt @@ -1,13 +1,10 @@ package com.awxkee.jxlcoder import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.ImageDecoder import android.graphics.Matrix import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.Image @@ -26,6 +23,8 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.lifecycleScope +import com.awxkee.jxlcoder.animation.AnimatedDrawable +import com.awxkee.jxlcoder.animation.JxlAnimatedStore import com.awxkee.jxlcoder.ui.theme.JXLCoderTheme import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.google.accompanist.drawablepainter.rememberDrawablePainter @@ -33,7 +32,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okio.buffer import okio.source -import java.io.FileNotFoundException import java.util.UUID class MainActivity : ComponentActivity() { @@ -114,49 +112,61 @@ class MainActivity : ComponentActivity() { } LaunchedEffect(key1 = Unit, block = { lifecycleScope.launch(Dispatchers.IO) { + +// val bufferPng = assets.open("lin.png").source().buffer().readByteArray() +// val bitmap = BitmapFactory.decodeByteArray(bufferPng, 0, bufferPng.size) +// lifecycleScope.launch { +// drawables.add(BitmapDrawable(resources, bitmap)) +// } +// val jxlBuffer = JxlCoder.encode(bitmap) +// val animated = JxlCoder.decode(jxlBuffer) +// lifecycleScope.launch { +// drawables.add(BitmapDrawable(resources, animated)) +// } + // val buffer4 = assets.open("its_totally_safe.gif").source().buffer().readByteArray() // val jxlBuffer = JxlCoder.Convenience.gif2JXL(buffer4, quality = 55) // val animated = JxlAnimatedImage(jxlBuffer) -// val drawable = animated.animatedDrawable +// val drawable = AnimatedDrawable(JxlAnimatedStore(animated)) // lifecycleScope.launch { // drawables.add(drawable) // } -// +//// // val buffer5 = assets.open("elephant.png").source().buffer().readByteArray() // val jxlBufferPNG = JxlCoder.Convenience.apng2JXL(buffer5, quality = 55) // val animated1 = JxlAnimatedImage(jxlBufferPNG) -// val drawable1 = animated1.animatedDrawable +// val drawable1 = AnimatedDrawable(JxlAnimatedStore(animated1)) // lifecycleScope.launch { // drawables.add(drawable1) // } - var assets = (this@MainActivity.assets.list("") ?: return@launch).toList() - for (asset in assets) { - try { - val buffer4 = - this@MainActivity.assets.open(asset).source().buffer() - .readByteArray() - - val largeImageSize = JxlCoder.getSize(buffer4) - if (largeImageSize != null) { - var srcImage = JxlCoder.decodeSampled( - buffer4, - largeImageSize.width / 3, - largeImageSize.height / 3, - preferredColorConfig = PreferredColorConfig.RGBA_8888, - com.awxkee.jxlcoder.ScaleMode.FIT, - JxlResizeFilter.BICUBIC, - toneMapper = JxlToneMapper.LOGARITHMIC, - ) - lifecycleScope.launch { - imagesArray.add(srcImage) - } - } - } catch (e: Exception) { - if (e !is FileNotFoundException) { - throw e - } - } - } +// var assets = (this@MainActivity.assets.list("") ?: return@launch).toList() +// for (asset in assets) { +// try { +// val buffer4 = +// this@MainActivity.assets.open(asset).source().buffer() +// .readByteArray() +// +// val largeImageSize = JxlCoder.getSize(buffer4) +// if (largeImageSize != null) { +// var srcImage = JxlCoder.decodeSampled( +// buffer4, +// largeImageSize.width / 3, +// largeImageSize.height / 3, +// preferredColorConfig = PreferredColorConfig.RGBA_8888, +// com.awxkee.jxlcoder.ScaleMode.FIT, +// JxlResizeFilter.BICUBIC, +// toneMapper = JxlToneMapper.LOGARITHMIC, +// ) +// lifecycleScope.launch { +// imagesArray.add(srcImage) +// } +// } +// } catch (e: Exception) { +// if (e !is FileNotFoundException) { +// throw e +// } +// } +// } } }) // A surface container using the 'background' color from the theme diff --git a/jxlcoder/src/main/cpp/JniDecoding.cpp b/jxlcoder/src/main/cpp/JniDecoding.cpp index e43aa45..ec6f998 100644 --- a/jxlcoder/src/main/cpp/JniDecoding.cpp +++ b/jxlcoder/src/main/cpp/JniDecoding.cpp @@ -153,8 +153,7 @@ jobject decodeSampledImageImpl(JNIEnv *env, std::vector &imageData, jin if (colorEncoding.primaries == JXL_PRIMARIES_2100) { sourceProfile = GamutRgbToXYZ(getRec2020Primaries(), getIlluminantD65()); } else if (colorEncoding.primaries == JXL_PRIMARIES_P3) { - sourceProfile = GamutRgbToXYZ(getDisplayP3Primaries(), getIlluminantDCI()); - useChromaticAdaptation = true; + sourceProfile = GamutRgbToXYZ(getDisplayP3Primaries(), getIlluminantD65()); } else if (colorEncoding.primaries == JXL_PRIMARIES_SRGB) { sourceProfile = GamutRgbToXYZ(getSRGBPrimaries(), getIlluminantD65()); } else { diff --git a/jxlcoder/src/main/cpp/JxlAnimatedDecoderCoordinator.cpp b/jxlcoder/src/main/cpp/JxlAnimatedDecoderCoordinator.cpp index 06dc543..a225f97 100644 --- a/jxlcoder/src/main/cpp/JxlAnimatedDecoderCoordinator.cpp +++ b/jxlcoder/src/main/cpp/JxlAnimatedDecoderCoordinator.cpp @@ -212,8 +212,7 @@ Java_com_awxkee_jxlcoder_JxlAnimatedImage_getFrameImpl(JNIEnv *env, jobject thiz if (colorEncoding.primaries == JXL_PRIMARIES_2100) { sourceProfile = GamutRgbToXYZ(getRec2020Primaries(), getIlluminantD65()); } else if (colorEncoding.primaries == JXL_PRIMARIES_P3) { - sourceProfile = GamutRgbToXYZ(getDisplayP3Primaries(), getIlluminantDCI()); - useChromaticAdaptation = true; + sourceProfile = GamutRgbToXYZ(getDisplayP3Primaries(), getIlluminantD65()); } else if (colorEncoding.primaries == JXL_PRIMARIES_SRGB) { sourceProfile = GamutRgbToXYZ(getSRGBPrimaries(), getIlluminantD65()); } else { diff --git a/jxlcoder/src/main/cpp/JxlEncoder.cpp b/jxlcoder/src/main/cpp/JxlEncoder.cpp index b7aadfe..0660861 100644 --- a/jxlcoder/src/main/cpp/JxlEncoder.cpp +++ b/jxlcoder/src/main/cpp/JxlEncoder.cpp @@ -233,7 +233,7 @@ Java_com_awxkee_jxlcoder_JxlCoder_encodeImpl(JNIEnv *env, jobject thiz, jobject .primaries_green_xy = {matrix(1, 0), matrix(1, 1)}, .primaries_blue_xy = {matrix(2, 0), matrix(2, 1)}, .transfer_function = JXL_TRANSFER_FUNCTION_SRGB, - .gamma = 1 / 2.6 + .gamma = 1 / 2.2 }; } else if (stdString == "sRGB IEC61966-2.1 (Linear)" || dataSpace == ADataSpace::ADATASPACE_SCRGB_LINEAR) { diff --git a/jxlcoder/src/main/cpp/colorspaces/ColorSpaceProfile.h b/jxlcoder/src/main/cpp/colorspaces/ColorSpaceProfile.h index 26b2f65..76e2f07 100644 --- a/jxlcoder/src/main/cpp/colorspaces/ColorSpaceProfile.h +++ b/jxlcoder/src/main/cpp/colorspaces/ColorSpaceProfile.h @@ -33,9 +33,9 @@ static Eigen::Matrix getRec2020Primaries() { static const Eigen::Matrix3f getBradfordAdaptation() { Eigen::Matrix3f M; - M << 0.8951, 0.2664, -0.1614, - -0.7502, 1.7135, 0.0367, - 0.0389, -0.0685, 1.0296; + M << 0.8446965f, -0.1179225f, 0.3948108f, + -0.1366303f, 1.1041226f, 0.1291718f, + 0.0798489f, -0.1348999f, 3.1924009f; return M; } diff --git a/jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/AnimatedDrawable.kt b/jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/AnimatedDrawable.kt new file mode 100644 index 0000000..88af401 --- /dev/null +++ b/jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/AnimatedDrawable.kt @@ -0,0 +1,235 @@ +/* + * MIT License + * + * Copyright (c) 2024 Radzivon Bartoshyk + * jxl-coder [https://github.com/awxkee/jxl-coder] + * + * Created by Radzivon Bartoshyk on 9/3/2024 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.awxkee.jxlcoder.animation + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable +import android.os.Handler +import android.os.HandlerThread +import android.os.Process +import android.util.Log +import androidx.annotation.Keep +import java.util.LinkedList +import java.util.UUID +import kotlin.math.min + +@Keep +public class AnimatedDrawable(private val frameStore: AnimatedFrameStore) : Drawable(), Animatable, + Runnable { + + data class SyncedFrame(val frame: Bitmap, val frameDuration: Int, val frameIndex: Int) + + private val lock = Any() + + private val handlerThread = + HandlerThread( + "AnimationDecoderThread with id: ${UUID.randomUUID()}", + Process.THREAD_PRIORITY_DISPLAY + ) + private val mHandlerThread: Handler + private val queue = LinkedList() + + private var currentBitmap: Bitmap? = null + + private var mCurrentFrameDuration = 0 + private var lastDecodedFrameIndex: Int = 0 + + private val deleteQueue = LinkedList() + + private var isRunning = false + private var lastSavedState = false + + private val decodingRunnable = Runnable { + var nextFrameIndex = lastDecodedFrameIndex + 1 + if (nextFrameIndex >= frameStore.framesCount) { + nextFrameIndex = 0 + } + + var toDeleteItem: Bitmap? = deleteQueue.pollFirst() + while (toDeleteItem != null) { + toDeleteItem.recycle() + toDeleteItem = deleteQueue.pollFirst() + } + + var haveFrameSent = false + + synchronized(lock) { + if (queue.size > 0) { + val frame = queue.pop() + mCurrentFrameDuration = frame.frameDuration + lastDecodedFrameIndex = frame.frameIndex + currentBitmap = frame.frame + scheduleSelf(this, 0) + haveFrameSent = true + } + } + + if (!haveFrameSent) { + val nextFrame = frameStore.getFrame(nextFrameIndex) + val nextFrameDuration = frameStore.getFrameDuration(nextFrameIndex) + synchronized(lock) { + mCurrentFrameDuration = nextFrameDuration + lastDecodedFrameIndex = nextFrameIndex + currentBitmap = nextFrame + scheduleSelf(this, 0) + haveFrameSent = true + } + + nextFrameIndex += 1 + } + + val maybeCachedFrames = min(14, frameStore.framesCount) + val vr = queue.lastOrNull()?.frameIndex + if (vr != null) { + nextFrameIndex = vr + 1 + } + if (nextFrameIndex >= frameStore.framesCount) { + nextFrameIndex = 0 + } + while (queue.size < maybeCachedFrames) { + val nextFrame = frameStore.getFrame(nextFrameIndex) + val nextFrameDuration = frameStore.getFrameDuration(nextFrameIndex) + queue.add(SyncedFrame(nextFrame, nextFrameDuration, nextFrameIndex)) + + nextFrameIndex += 1 + if (nextFrameIndex >= frameStore.framesCount) { + nextFrameIndex = 0 + } + } + + } + + init { + handlerThread.start() + mHandlerThread = Handler(handlerThread.looper) + handlerThread.setUncaughtExceptionHandler { t, e -> + Log.e("AnimatedDrawable", e.message ?: "Failed to decode next frame") + } + } + + private val matrix = Matrix() + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + + private val rect = Rect() + + override fun draw(canvas: Canvas) { + val bmp = currentBitmap + if (bmp != null) { + matrix.reset() + rect.set(0, 0, bmp.width, bmp.height) + canvas.drawBitmap(bmp, rect, bounds, paint) + } + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun getIntrinsicHeight(): Int { + return frameStore.height + } + + override fun getIntrinsicWidth(): Int { + return frameStore.width + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + } + + @Deprecated("Deprecated in Java") + override fun getOpacity(): Int { + return paint.alpha + } + + override fun start() { + if (isRunning) { + return + } + isRunning = true + lastSavedState = true + mHandlerThread.removeCallbacks(decodingRunnable) + mHandlerThread.post(decodingRunnable) + } + + override fun stop() { + if (!isRunning) { + return + } + isRunning = false + lastSavedState = false + unscheduleSelf(this) + } + + override fun isRunning(): Boolean { + return isRunning + } + + override fun unscheduleSelf(what: Runnable) { + mHandlerThread.removeCallbacks(decodingRunnable) + super.unscheduleSelf(what) + } + + override fun setVisible(visible: Boolean, restart: Boolean): Boolean { + if (!visible) { + stop() + } else { + if (lastSavedState) { + start() + } + } + return super.setVisible(visible, restart) + } + + override fun run() { + if (!isRunning) { + return + } + invalidateSelf() + synchronized(lock) { + val duration = mCurrentFrameDuration + if (duration == 0) { + mHandlerThread.post(decodingRunnable) + } else { + mHandlerThread.postDelayed(decodingRunnable, duration.toLong()) + } + } + } + + protected fun finalize() { + stop() + } + +} \ No newline at end of file diff --git a/jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/AnimatedFrameStore.kt b/jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/AnimatedFrameStore.kt new file mode 100644 index 0000000..1ef81a0 --- /dev/null +++ b/jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/AnimatedFrameStore.kt @@ -0,0 +1,39 @@ +/* + * MIT License + * + * Copyright (c) 2024 Radzivon Bartoshyk + * jxl-coder [https://github.com/awxkee/jxl-coder] + * + * Created by Radzivon Bartoshyk on 9/3/2024 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.awxkee.jxlcoder.animation + +import android.graphics.Bitmap + +public interface AnimatedFrameStore { + val width: Int + val height: Int + fun getFrame(frame: Int): Bitmap + fun getFrameDuration(frame: Int): Int + val framesCount: Int +} \ No newline at end of file diff --git a/jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/JxlAnimatedStore.kt b/jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/JxlAnimatedStore.kt new file mode 100644 index 0000000..80f79eb --- /dev/null +++ b/jxlcoder/src/main/java/com/awxkee/jxlcoder/animation/JxlAnimatedStore.kt @@ -0,0 +1,54 @@ +/* + * MIT License + * + * Copyright (c) 2024 Radzivon Bartoshyk + * jxl-coder [https://github.com/awxkee/jxl-coder] + * + * Created by Radzivon Bartoshyk on 9/3/2024 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.awxkee.jxlcoder.animation + +import android.graphics.Bitmap +import com.awxkee.jxlcoder.JxlAnimatedImage + +public class JxlAnimatedStore( + private val jxlAnimatedImage: JxlAnimatedImage, + val targetWidth: Int = 0, + val targetHeight: Int = 0, +) : AnimatedFrameStore { + override val width: Int + get() = if (targetWidth > 0 && targetHeight > 0) targetWidth else jxlAnimatedImage.getWidth() + override val height: Int + get() = if (targetWidth > 0 && targetHeight > 0) targetHeight else jxlAnimatedImage.getHeight() + + override fun getFrame(frame: Int): Bitmap { + return jxlAnimatedImage.getFrame(frame) + } + + override fun getFrameDuration(frame: Int): Int { + return jxlAnimatedImage.getFrameDuration(frame) + } + + override val framesCount: Int + get() = jxlAnimatedImage.numberOfFrames +} \ No newline at end of file