#1441 fallback decoding of images packed in RGBA_1010102 config
This commit is contained in:
parent
a3f6cd7a32
commit
f02363592f
8 changed files with 106 additions and 11 deletions
|
@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file.
|
|||
- opening motion photo embedded video when video track is not the first one
|
||||
- some SVG rendering issues
|
||||
- decoding of SVG containing references to namespaces in !ATTLIST
|
||||
- fallback decoding of images packed in RGBA_1010102 config
|
||||
|
||||
## <a id="v1.12.3"></a>[v1.12.3] - 2025-02-06
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.bumptech.glide.Glide
|
|||
import deckers.thibault.aves.decoder.AvesAppGlideModule
|
||||
import deckers.thibault.aves.decoder.MultiPageImage
|
||||
import deckers.thibault.aves.utils.BitmapRegionDecoderCompat
|
||||
import deckers.thibault.aves.utils.BitmapUtils
|
||||
import deckers.thibault.aves.utils.BitmapUtils.ARGB_8888_BYTE_SIZE
|
||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
|
@ -111,7 +112,14 @@ class RegionFetcher internal constructor(
|
|||
}
|
||||
val bitmap = decoder.decodeRegion(effectiveRect, options)
|
||||
if (bitmap != null) {
|
||||
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true))
|
||||
val canHaveAlpha = MimeTypes.canHaveAlpha(mimeType)
|
||||
val recycle = false
|
||||
var bytes = bitmap.getBytes(canHaveAlpha, recycle = recycle)
|
||||
if (bytes != null && bytes.isEmpty()) {
|
||||
bytes = BitmapUtils.tryPixelFormatConversion(bitmap)?.getBytes(canHaveAlpha, recycle = recycle)
|
||||
}
|
||||
bitmap.recycle()
|
||||
result.success(bytes)
|
||||
} else {
|
||||
result.error("fetch-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.os.Build
|
|||
import android.provider.MediaStore
|
||||
import android.util.Size
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toUri
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
|
@ -14,6 +15,7 @@ import com.bumptech.glide.request.RequestOptions
|
|||
import com.bumptech.glide.signature.ObjectKey
|
||||
import deckers.thibault.aves.decoder.AvesAppGlideModule
|
||||
import deckers.thibault.aves.decoder.MultiPageImage
|
||||
import deckers.thibault.aves.utils.BitmapUtils
|
||||
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
|
@ -24,7 +26,6 @@ import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
|||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class ThumbnailFetcher internal constructor(
|
||||
private val context: Context,
|
||||
|
@ -78,7 +79,13 @@ class ThumbnailFetcher internal constructor(
|
|||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false, quality = quality))
|
||||
val canHaveAlpha = MimeTypes.canHaveAlpha(mimeType)
|
||||
val recycle = false
|
||||
var bytes = bitmap.getBytes(canHaveAlpha, quality, recycle)
|
||||
if (bytes != null && bytes.isEmpty()) {
|
||||
bytes = BitmapUtils.tryPixelFormatConversion(bitmap)?.getBytes(canHaveAlpha, quality, recycle)
|
||||
}
|
||||
result.success(bytes)
|
||||
} else {
|
||||
var errorDetails: String? = exception?.message
|
||||
if (errorDetails?.isNotEmpty() == true) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.util.Log
|
|||
import androidx.core.net.toUri
|
||||
import com.bumptech.glide.Glide
|
||||
import deckers.thibault.aves.decoder.AvesAppGlideModule
|
||||
import deckers.thibault.aves.utils.BitmapUtils
|
||||
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
|
@ -137,7 +138,12 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
|
|||
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
|
||||
}
|
||||
if (bitmap != null) {
|
||||
val bytes = bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false)
|
||||
val recycle = false
|
||||
val canHaveAlpha = MimeTypes.canHaveAlpha(mimeType)
|
||||
var bytes = bitmap.getBytes(canHaveAlpha, recycle = recycle)
|
||||
if (bytes != null && bytes.isEmpty()) {
|
||||
bytes = BitmapUtils.tryPixelFormatConversion(bitmap)?.getBytes(canHaveAlpha, recycle = recycle)
|
||||
}
|
||||
if (MemoryUtils.canAllocate(sizeBytes)) {
|
||||
success(bytes)
|
||||
} else {
|
||||
|
|
|
@ -2,13 +2,18 @@ package deckers.thibault.aves.utils
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ColorSpace
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.graphics.createBitmap
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
||||
import deckers.thibault.aves.metadata.Metadata.getExifCode
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object BitmapUtils {
|
||||
private val LOG_TAG = LogUtils.createTag<BitmapUtils>()
|
||||
|
@ -21,6 +26,7 @@ object BitmapUtils {
|
|||
private val mutex = Mutex()
|
||||
|
||||
const val ARGB_8888_BYTE_SIZE = 4
|
||||
private const val RGBA_1010102_BYTE_SIZE = 4
|
||||
|
||||
suspend fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean): ByteArray? {
|
||||
val stream: ByteArrayOutputStream
|
||||
|
@ -62,6 +68,76 @@ object BitmapUtils {
|
|||
return null
|
||||
}
|
||||
|
||||
// On some devices, RGBA_1010102 config can be displayed directly from the hardware buffer,
|
||||
// but the native image decoder cannot convert RGBA_1010102 to another config like ARGB_8888,
|
||||
// so we manually check the config and convert the pixels as a fallback mechanism.
|
||||
fun tryPixelFormatConversion(bitmap: Bitmap): Bitmap? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && bitmap.config == Bitmap.Config.RGBA_1010102) {
|
||||
val byteCount = bitmap.byteCount
|
||||
if (MemoryUtils.canAllocate(byteCount)) {
|
||||
val bytes = ByteBuffer.allocate(byteCount).apply {
|
||||
bitmap.copyPixelsToBuffer(this)
|
||||
rewind()
|
||||
}.array()
|
||||
val srcColorSpace = bitmap.colorSpace
|
||||
if (srcColorSpace != null) {
|
||||
val dstColorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
|
||||
val connector = ColorSpace.connect(srcColorSpace, dstColorSpace)
|
||||
rgba1010102toArgb8888(bytes, connector)
|
||||
|
||||
val hasAlpha = false
|
||||
return createBitmap(
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
Bitmap.Config.ARGB_8888,
|
||||
hasAlpha = hasAlpha,
|
||||
colorSpace = dstColorSpace,
|
||||
).apply {
|
||||
copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// convert bytes, without reallocation:
|
||||
// - from config RGBA_1010102 to ARGB_8888,
|
||||
// - from original color space to sRGB.
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun rgba1010102toArgb8888(bytes: ByteArray, connector: ColorSpace.Connector) {
|
||||
val max10Bits = 0x3ff.toFloat()
|
||||
val dstAlpha = 0xff.toByte()
|
||||
|
||||
val byteCount = bytes.size
|
||||
for (i in 0..<byteCount step RGBA_1010102_BYTE_SIZE) {
|
||||
val i3 = bytes[i + 3].toInt()
|
||||
val i2 = bytes[i + 2].toInt()
|
||||
val i1 = bytes[i + 1].toInt()
|
||||
val i0 = bytes[i].toInt()
|
||||
|
||||
// unpacking from RGBA_1010102
|
||||
// stored as [3,2,1,0] -> [AABBBBBB BBBBGGGG GGGGGGRR RRRRRRRR]
|
||||
// val iA = ((i3 and 0xc0) shr 6)
|
||||
val iB = ((i3 and 0x3f) shl 4) or ((i2 and 0xf0) shr 4)
|
||||
val iG = ((i2 and 0x0f) shl 6) or ((i1 and 0xfc) shr 2)
|
||||
val iR = ((i1 and 0x03) shl 8) or ((i0 and 0xff) shr 0)
|
||||
|
||||
// components as floats in sRGB
|
||||
val srgbFloats = connector.transform(iR / max10Bits, iG / max10Bits, iB / max10Bits)
|
||||
val srgbR = (srgbFloats[0] * 255.0f + 0.5f).toInt()
|
||||
val srgbG = (srgbFloats[1] * 255.0f + 0.5f).toInt()
|
||||
val srgbB = (srgbFloats[2] * 255.0f + 0.5f).toInt()
|
||||
|
||||
// packing to ARGB_8888
|
||||
// stored as [3,2,1,0] -> [AAAAAAAA BBBBBBBB GGGGGGGG RRRRRRRR]
|
||||
bytes[i + 3] = dstAlpha
|
||||
bytes[i + 2] = srgbB.toByte()
|
||||
bytes[i + 1] = srgbG.toByte()
|
||||
bytes[i] = srgbR.toByte()
|
||||
}
|
||||
}
|
||||
|
||||
fun applyExifOrientation(context: Context, bitmap: Bitmap?, rotationDegrees: Int?, isFlipped: Boolean?): Bitmap? {
|
||||
if (bitmap == null || rotationDegrees == null || isFlipped == null) return bitmap
|
||||
if (rotationDegrees == 0 && !isFlipped) return bitmap
|
||||
|
|
|
@ -90,12 +90,7 @@ object BmpWriter {
|
|||
|
||||
var column = 0
|
||||
while (column < biWidth) {
|
||||
/*
|
||||
alpha: (value shr 24 and 0xFF).toByte()
|
||||
red: (value shr 16 and 0xFF).toByte()
|
||||
green: (value shr 8 and 0xFF).toByte()
|
||||
blue: (value and 0xFF).toByte()
|
||||
*/
|
||||
// non-premultiplied ARGB values in the sRGB color space
|
||||
value = pixels[column]
|
||||
// blue: [0], green: [1], red: [2]
|
||||
rgb[0] = (value and 0xFF).toByte()
|
||||
|
|
|
@ -8,6 +8,8 @@ fun ByteBuffer.toByteArray(): ByteArray {
|
|||
return bytes
|
||||
}
|
||||
|
||||
fun Int.toHex(): String = "0x${byteArrayOf(shr(8).toByte(), toByte()).toHex()}"
|
||||
|
||||
fun ByteArray.toHex(): String = joinToString(separator = "") { it.toHex() }
|
||||
|
||||
fun Byte.toHex(): String = "%02x".format(this)
|
|
@ -34,7 +34,7 @@ extension ExtraAvesEntryProps on AvesEntry {
|
|||
|
||||
// size
|
||||
|
||||
bool get useTiles => (width > 4096 || height > 4096) && !isAnimated;
|
||||
bool get useTiles => !isAnimated;
|
||||
|
||||
bool get isSized => width > 0 && height > 0;
|
||||
|
||||
|
|
Loading…
Reference in a new issue