Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix scaling for Bitmap resources with original size #1072

Merged
merged 5 commits into from Jan 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion coil-base/api/coil-base.api
Expand Up @@ -226,7 +226,8 @@ public final class coil/decode/ImageSources {
}

public final class coil/decode/ResourceMetadata : coil/decode/ImageSource$Metadata {
public fun <init> (Ljava/lang/String;I)V
public fun <init> (Ljava/lang/String;II)V
public final fun getDensity ()I
public final fun getPackageName ()Ljava/lang/String;
public final fun getResId ()I
}
Expand Down
224 changes: 78 additions & 146 deletions coil-base/src/main/java/coil/decode/BitmapFactoryDecoder.kt
Expand Up @@ -2,16 +2,14 @@ package coil.decode

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.RectF
import android.os.Build.VERSION.SDK_INT
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap
import androidx.exifinterface.media.ExifInterface
import coil.ImageLoader
import coil.decode.Exif.Data.Companion.toExifData
import coil.decode.Exif.applyExifTransformations
import coil.fetch.SourceResult
import coil.request.Options
import coil.size.isOriginal
import coil.size.pxOrElse
import coil.util.toDrawable
import coil.util.toSoftware
Expand All @@ -22,7 +20,6 @@ import okio.Buffer
import okio.ForwardingSource
import okio.Source
import okio.buffer
import java.io.InputStream
import kotlin.math.roundToInt

/** The base [Decoder] that uses [BitmapFactory] to decode a given [ImageSource]. */
Expand All @@ -32,8 +29,6 @@ class BitmapFactoryDecoder @JvmOverloads constructor(
private val parallelismLock: Semaphore = Semaphore(Int.MAX_VALUE)
) : Decoder {

private val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)

override suspend fun decode() = parallelismLock.withPermit {
runInterruptible { BitmapFactory.Options().decode() }
}
Expand All @@ -49,26 +44,21 @@ class BitmapFactoryDecoder @JvmOverloads constructor(
inJustDecodeBounds = false

// Read the image's EXIF data.
val isFlipped: Boolean
val rotationDegrees: Int
if (shouldReadExifData(outMimeType)) {
val exifData = if (Exif.shouldReadExifData(outMimeType)) {
val inputStream = safeBufferedSource.peek().inputStream()
val exifInterface = ExifInterface(ExifInterfaceInputStream(inputStream))
safeSource.exception?.let { throw it }
isFlipped = exifInterface.isFlipped
rotationDegrees = exifInterface.rotationDegrees
exifInterface.toExifData()
} else {
isFlipped = false
rotationDegrees = 0
Exif.Data.DEFAULT
}

// srcWidth and srcHeight are the dimensions of the image after
// EXIF transformations (but before sampling).
val isSwapped = rotationDegrees == 90 || rotationDegrees == 270
val srcWidth = if (isSwapped) outHeight else outWidth
val srcHeight = if (isSwapped) outWidth else outHeight
val srcWidth = if (exifData.isSwapped) outHeight else outWidth
val srcHeight = if (exifData.isSwapped) outWidth else outHeight

inPreferredConfig = computeConfig(options, isFlipped, rotationDegrees)
inPreferredConfig = computeConfig(options, exifData)
inPremultiplied = options.premultipliedAlpha

if (SDK_INT >= 26 && options.colorSpace != null) {
Expand All @@ -78,49 +68,7 @@ class BitmapFactoryDecoder @JvmOverloads constructor(
// Always create immutable bitmaps as they have performance benefits.
inMutable = false

if (outWidth > 0 && outHeight > 0) {
val (width, height) = options.size
val dstWidth = width.pxOrElse { srcWidth }
val dstHeight = height.pxOrElse { srcHeight }
inSampleSize = DecodeUtils.calculateInSampleSize(
srcWidth = srcWidth,
srcHeight = srcHeight,
dstWidth = dstWidth,
dstHeight = dstHeight,
scale = options.scale
)

// Calculate the image's density scaling multiple.
var scale = DecodeUtils.computeSizeMultiplier(
srcWidth = srcWidth / inSampleSize.toDouble(),
srcHeight = srcHeight / inSampleSize.toDouble(),
dstWidth = dstWidth.toDouble(),
dstHeight = dstHeight.toDouble(),
scale = options.scale
)

// Only upscale the image if the options require an exact size.
if (options.allowInexactSize) {
scale = scale.coerceAtMost(1.0)
}

inScaled = scale != 1.0
if (inScaled) {
if (scale > 1) {
// Upscale
inDensity = (Int.MAX_VALUE / scale).roundToInt()
inTargetDensity = Int.MAX_VALUE
} else {
// Downscale
inDensity = Int.MAX_VALUE
inTargetDensity = (Int.MAX_VALUE * scale).roundToInt()
}
}
} else {
// This occurs if there was an error decoding the image's size.
inSampleSize = 1
inScaled = false
}
configureScale(srcWidth, srcHeight)

// Decode the bitmap.
val outBitmap: Bitmap? = safeBufferedSource.use {
Expand All @@ -137,29 +85,23 @@ class BitmapFactoryDecoder @JvmOverloads constructor(
outBitmap.density = options.context.resources.displayMetrics.densityDpi

// Apply any EXIF transformations.
val bitmap = applyExifTransformations(outBitmap, inPreferredConfig, isFlipped, rotationDegrees)
val bitmap = applyExifTransformations(outBitmap, inPreferredConfig, exifData)

return DecodeResult(
drawable = bitmap.toDrawable(options.context),
isSampled = inSampleSize > 1 || inScaled
)
}

/** Return 'true' if we should read the image's EXIF data. */
private fun shouldReadExifData(mimeType: String?): Boolean {
return mimeType != null && mimeType in SUPPORTED_EXIF_MIME_TYPES
}

/** Compute and return [BitmapFactory.Options.inPreferredConfig]. */
private fun BitmapFactory.Options.computeConfig(
options: Options,
isFlipped: Boolean,
rotationDegrees: Int
exifData: Exif.Data
): Bitmap.Config {
var config = options.config

// Disable hardware bitmaps if we need to perform EXIF transformations.
if (isFlipped || rotationDegrees > 0) {
if (exifData.isFlipped || exifData.rotationDegrees > 0) {
config = config.toSoftware()
}

Expand All @@ -176,46 +118,69 @@ class BitmapFactoryDecoder @JvmOverloads constructor(
return config
}

/** This method assumes [config] is not [Bitmap.Config.HARDWARE]. */
private fun applyExifTransformations(
inBitmap: Bitmap,
config: Bitmap.Config,
isFlipped: Boolean,
rotationDegrees: Int
): Bitmap {
// Short circuit if there are no transformations to apply.
val isRotated = rotationDegrees > 0
if (!isFlipped && !isRotated) {
return inBitmap
}

val matrix = Matrix()
val centerX = inBitmap.width / 2f
val centerY = inBitmap.height / 2f
if (isFlipped) {
matrix.postScale(-1f, 1f, centerX, centerY)
}
if (isRotated) {
matrix.postRotate(rotationDegrees.toFloat(), centerX, centerY)
}

val rect = RectF(0f, 0f, inBitmap.width.toFloat(), inBitmap.height.toFloat())
matrix.mapRect(rect)
if (rect.left != 0f || rect.top != 0f) {
matrix.postTranslate(-rect.left, -rect.top)
}

val outBitmap = if (rotationDegrees == 90 || rotationDegrees == 270) {
createBitmap(inBitmap.height, inBitmap.width, config)
} else {
createBitmap(inBitmap.width, inBitmap.height, config)
}
/**
* Configure scaling of the output bitmap by considering density, sample size and the given
* scaling algorithm.
*/
private fun BitmapFactory.Options.configureScale(
srcWidth: Int,
srcHeight: Int
) {
when {
options.size.isOriginal && source.metadata is ResourceMetadata -> {
inScaled = true
// Read the resource density if available
inDensity = (source.metadata as ResourceMetadata).density
inTargetDensity = options.context.resources.displayMetrics.densityDpi
// Clear outWidth and outHeight so that BitmapFactory will handle scaling itself.
outWidth = 0
outHeight = 0
}
outWidth > 0 && outHeight > 0 -> {
val (width, height) = options.size
val dstWidth = width.pxOrElse { srcWidth }
val dstHeight = height.pxOrElse { srcHeight }
inSampleSize = DecodeUtils.calculateInSampleSize(
srcWidth = srcWidth,
srcHeight = srcHeight,
dstWidth = dstWidth,
dstHeight = dstHeight,
scale = options.scale
)

// Calculate the image's density scaling multiple.
var scale = DecodeUtils.computeSizeMultiplier(
srcWidth = srcWidth / inSampleSize.toDouble(),
srcHeight = srcHeight / inSampleSize.toDouble(),
dstWidth = dstWidth.toDouble(),
dstHeight = dstHeight.toDouble(),
scale = options.scale
)

// Only upscale the image if the options require an exact size.
if (options.allowInexactSize) {
scale = scale.coerceAtMost(1.0)
}

outBitmap.applyCanvas {
drawBitmap(inBitmap, matrix, paint)
inScaled = scale != 1.0
if (inScaled) {
if (scale > 1) {
// Upscale
inDensity = (Int.MAX_VALUE / scale).roundToInt()
inTargetDensity = Int.MAX_VALUE
} else {
// Downscale
inDensity = Int.MAX_VALUE
inTargetDensity = (Int.MAX_VALUE * scale).roundToInt()
}
}
}
else -> {
// This occurs if there was an error decoding the image's size.
inSampleSize = 1
inScaled = false
}
}
inBitmap.recycle()
return outBitmap
}

class Factory @JvmOverloads constructor(
Expand Down Expand Up @@ -249,44 +214,11 @@ class BitmapFactoryDecoder @JvmOverloads constructor(
}
}

/** Wrap [delegate] so that it works with [ExifInterface]. */
private class ExifInterfaceInputStream(private val delegate: InputStream) : InputStream() {

// Ensure that this value is always larger than the size of the image
// so ExifInterface won't stop reading the stream prematurely.
private var availableBytes = GIGABYTE_IN_BYTES

override fun read() = interceptBytesRead(delegate.read())

override fun read(b: ByteArray) = interceptBytesRead(delegate.read(b))

override fun read(b: ByteArray, off: Int, len: Int) =
interceptBytesRead(delegate.read(b, off, len))

override fun skip(n: Long) = delegate.skip(n)

override fun available() = availableBytes

override fun close() = delegate.close()

private fun interceptBytesRead(bytesRead: Int): Int {
if (bytesRead == -1) availableBytes = 0
return bytesRead
}
}

internal companion object {
private const val MIME_TYPE_JPEG = "image/jpeg"
private const val MIME_TYPE_WEBP = "image/webp"
private const val MIME_TYPE_HEIC = "image/heic"
private const val MIME_TYPE_HEIF = "image/heif"
private const val GIGABYTE_IN_BYTES = 1024 * 1024 * 1024
internal const val MIME_TYPE_JPEG = "image/jpeg"
internal const val MIME_TYPE_WEBP = "image/webp"
internal const val MIME_TYPE_HEIC = "image/heic"
internal const val MIME_TYPE_HEIF = "image/heif"
internal const val DEFAULT_MAX_PARALLELISM = 4

// NOTE: We don't support PNG EXIF data as it's very rarely used and requires buffering
// the entire file into memory. All of the supported formats short circuit when the EXIF
// chunk is found (often near the top of the file).
private val SUPPORTED_EXIF_MIME_TYPES =
arrayOf(MIME_TYPE_JPEG, MIME_TYPE_WEBP, MIME_TYPE_HEIC, MIME_TYPE_HEIF)
}
}