Skip to content

Commit

Permalink
#1441 fallback decoding of images packed in RGBA_1010102 config
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed Feb 24, 2025
1 parent a3f6cd7 commit f023635
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ 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
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
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>()
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion lib/model/entry/extensions/props.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit f023635

Please sign in to comment.