Skip to content

Commit

Permalink
clean up blurring implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
aboedo committed Oct 18, 2023
1 parent ed4e957 commit 19ef51e
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 282 deletions.
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
package com.revenuecat.purchases.ui.revenuecatui.composables

import BlurTransformation
import android.os.Build
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.revenuecat.purchases.paywalls.PaywallData
import com.revenuecat.purchases.ui.revenuecatui.R
import com.revenuecat.purchases.ui.revenuecatui.data.processed.TemplateConfiguration
import com.revenuecat.purchases.ui.revenuecatui.extensions.conditional
import com.revenuecat.purchases.ui.revenuecatui.extensions.defaultBackgroundPlaceholder
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

import com.revenuecat.purchases.ui.revenuecatui.data.processed.TemplateConfiguration
import kotlin.math.roundToInt

@Composable
internal fun BoxScope.PaywallBackground(templateConfiguration: TemplateConfiguration) {
val transformation = if (templateConfiguration.configuration.blurredBackgroundImage)
BlurTransformation(context = LocalContext.current, radius = BackgroundUIConstants.blurSize.toFloatPx(),
scale = BackgroundUIConstants.blurScale)
// current implementation uses a transformation on API level 30-, modifier on 31+.
val transformation = if (templateConfiguration.configuration.blurredBackgroundImage && Build.VERSION.SDK_INT < 31)
BlurTransformation(
context = LocalContext.current, radius = BackgroundUIConstants.blurSize.toFloatPx(),
scale = BackgroundUIConstants.blurScale
)
else null

val modifier = Modifier
.matchParentSize()
.conditional(templateConfiguration.configuration.blurredBackgroundImage) {
// TODO-PAYWALLS: backwards compatibility for blurring
// TODO: try to unify both methods into either a transformation or a modifier
// one notable difference is that the transformation works at the image level so it'd run only once
.conditional(
templateConfiguration.configuration.blurredBackgroundImage
&& Build.VERSION.SDK_INT >= 31
) {
blur(BackgroundUIConstants.blurSize, edgeTreatment = BlurredEdgeTreatment.Unbounded)
.alpha(BackgroundUIConstants.blurAlpha)
}

if (templateConfiguration.configuration.images.background == PaywallData.defaultBackgroundPlaceholder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import kotlin.math.min
import kotlin.math.roundToInt

/**
* BlurTransformation class applies a Box Blur algorithm on a given Bitmap image.
* The blurring is performed by averaging the color values of a square group of
* adjacent pixels for each pixel in the image.
* BlurTransformation class applies a blur on a given Bitmap image.
* The blurring is performed with RenderScript, and should only be used in API level < 31, since newer versions
* of Android have their own native blur implementation.
*
* @property radius - The radius of the square used for blurring. Higher values
* produce a more pronounced blur effect.
Expand Down Expand Up @@ -46,24 +46,9 @@ internal class BlurTransformation(
override fun hashCode(): Int = radius.hashCode()
}

internal suspend fun Bitmap.blur(context: Context, radius: Float = 25f): Bitmap {
return if (Build.VERSION.SDK_INT >= 31) {
this.blur(context = context, radius = radius)
} else {
// useful for testing
val useRenderScript = true
if (useRenderScript) {
return blurUsingRenderScript(context, radius)
} else {
return blurByAveraging(scale = 1f, radius = radius.roundToInt())
}
}
}

internal fun Bitmap.blurUsingRenderScript(context: Context, radius: Float): Bitmap {

internal fun Bitmap.blur(context: Context, radius: Float = 25f): Bitmap {
if (radius < 1f) {
return this@blurUsingRenderScript
return this@blur
}
// max radius supported by RenderScript is 25
val updatedRadius = min(radius.toDouble(), 25.toDouble())
Expand All @@ -87,252 +72,3 @@ internal fun Bitmap.blurUsingRenderScript(context: Context, radius: Float): Bitm

return blurredBitmap
}

/**
* Applies blur effect to a Bitmap.
*
* @param scale - The scale factor for resizing the image before blurring.
* @param radius - The radius of the square used for blurring.
* @return A new Bitmap with the blur effect applied.
*
* The algorithm is a blur transformation that applies a Gaussian blur effect to an input bitmap image.
* It works by computing the average color value of each pixel in a radius around the pixel.
* The radius of the blur effect is specified by the radius parameter.
*
* The algorithm uses a stack to keep track of the color values of the pixels in the blur radius.
* It iterates over each pixel in the image and computes the weighted sum of the red, green, and blue color values
* of the pixels in the blur radius.
*
* This sum is then divided by the sum of the divisor values to obtain the average color value for the pixel.
*/
internal suspend fun Bitmap.blurByAveraging(
scale: Float,
radius: Int,
): Bitmap = withContext(Dispatchers.IO) {
if (radius < 1) {
return@withContext this@blurByAveraging
}

// Scale the bitmap image.
var scaledBitmap = this@blurByAveraging
val scaledWidth = (scaledBitmap.width * scale).roundToInt()
val scaledHeight = (scaledBitmap.height * scale).roundToInt()
scaledBitmap = Bitmap.createScaledBitmap(scaledBitmap, scaledWidth, scaledHeight, false)

val bitmap = scaledBitmap.copy(scaledBitmap.config, true)

// Initialize variables for the blur algorithm.
val width = bitmap.width
val height = bitmap.height
val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
val widthMinusOne = width - 1
val heightMinusOne = height - 1
val imagePixelCount = width * height
val totalPixelsToAverage = radius + radius + 1
val reds = IntArray(imagePixelCount)
val greens = IntArray(imagePixelCount)
val blues = IntArray(imagePixelCount)
var redSum: Int
var greenSum: Int
var blueSum: Int
var x: Int
var y: Int
var i: Int
var pixelIndex: Int
var yOffset: Int
var rowStartIndex: Int
val minimumDimension = IntArray(width.coerceAtLeast(height))
var divisorSum = totalPixelsToAverage + 1 shr 1
divisorSum *= divisorSum
val weightedColorSum = IntArray(256 * divisorSum)
i = 0
while (i < 256 * divisorSum) {
weightedColorSum[i] = i / divisorSum
i++
}
rowStartIndex = 0
var yw = 0
val stack = Array(totalPixelsToAverage) {
IntArray(3)
}
var stackPointer: Int
var stackStart: Int
var currentPixel: IntArray
var radiusMinusAbsolute: Int
val radiusPlusOne = radius + 1
var redOutSum: Int
var greenOutSum: Int
var blueOutSum: Int
var redInSum: Int
var greenInSum: Int
var blueInSum: Int

// Apply the blur algorithm to each row of pixels in the bitmap image.
y = 0
while (y < height) {
blueSum = 0
greenSum = 0
redSum = 0
blueOutSum = 0
greenOutSum = 0
redOutSum = 0
blueInSum = 0
greenInSum = 0
redInSum = 0
i = -radius
while (i <= radius) {
pixelIndex = pixels[rowStartIndex + (i.coerceIn(0, widthMinusOne))]
currentPixel = stack[i + radius]
currentPixel[0] = pixelIndex and 0xff0000 shr 16
currentPixel[1] = pixelIndex and 0x00ff00 shr 8
currentPixel[2] = pixelIndex and 0x0000ff
radiusMinusAbsolute = radiusPlusOne - abs(i)
redSum += currentPixel[0] * radiusMinusAbsolute
greenSum += currentPixel[1] * radiusMinusAbsolute
blueSum += currentPixel[2] * radiusMinusAbsolute
if (i > 0) {
redInSum += currentPixel[0]
greenInSum += currentPixel[1]
blueInSum += currentPixel[2]
} else {
redOutSum += currentPixel[0]
greenOutSum += currentPixel[1]
blueOutSum += currentPixel[2]
}
i++
}
stackPointer = radius
x = 0
while (x < width) {
reds[rowStartIndex] = weightedColorSum[redSum]
greens[rowStartIndex] = weightedColorSum[greenSum]
blues[rowStartIndex] = weightedColorSum[blueSum]
redSum -= redOutSum
greenSum -= greenOutSum
blueSum -= blueOutSum
stackStart = stackPointer - radius + totalPixelsToAverage
currentPixel = stack[stackStart % totalPixelsToAverage]
redOutSum -= currentPixel[0]
greenOutSum -= currentPixel[1]
blueOutSum -= currentPixel[2]
if (y == 0) {
minimumDimension[x] = (x + radius + 1).coerceAtMost(widthMinusOne)
}
pixelIndex = pixels[yw + minimumDimension[x]]
currentPixel[0] = pixelIndex and 0xff0000 shr 16
currentPixel[1] = pixelIndex and 0x00ff00 shr 8
currentPixel[2] = pixelIndex and 0x0000ff
redInSum += currentPixel[0]
greenInSum += currentPixel[1]
blueInSum += currentPixel[2]
redSum += redInSum
greenSum += greenInSum
blueSum += blueInSum
stackPointer = (stackPointer + 1) % totalPixelsToAverage
currentPixel = stack[stackPointer % totalPixelsToAverage]
redOutSum += currentPixel[0]
greenOutSum += currentPixel[1]
blueOutSum += currentPixel[2]
redInSum -= currentPixel[0]
greenInSum -= currentPixel[1]
blueInSum -= currentPixel[2]
rowStartIndex++
x++
}
yw += width
y++
}

// Apply the blur algorithm to each column of pixels in the bitmap image.
x = 0
while (x < width) {
blueSum = 0
greenSum = blueSum
redSum = greenSum
blueOutSum = redSum
greenOutSum = blueOutSum
redOutSum = greenOutSum
blueInSum = redOutSum
greenInSum = blueInSum
redInSum = greenInSum
yOffset = -radius * width
i = -radius
while (i <= radius) {
rowStartIndex = 0.coerceAtLeast(yOffset) + x
currentPixel = stack[i + radius]
currentPixel[0] = reds[rowStartIndex]
currentPixel[1] = greens[rowStartIndex]
currentPixel[2] = blues[rowStartIndex]
radiusMinusAbsolute = radiusPlusOne - abs(i)
redSum += reds[rowStartIndex] * radiusMinusAbsolute
greenSum += greens[rowStartIndex] * radiusMinusAbsolute
blueSum += blues[rowStartIndex] * radiusMinusAbsolute
if (i > 0) {
redInSum += currentPixel[0]
greenInSum += currentPixel[1]
blueInSum += currentPixel[2]
} else {
redOutSum += currentPixel[0]
greenOutSum += currentPixel[1]
blueOutSum += currentPixel[2]
}
if (i < heightMinusOne) {
yOffset += width
}
i++
}
rowStartIndex = x
stackPointer = radius
y = 0
while (y < height) {
// Set the blurred pixel color in the bitmap image.
pixels[rowStartIndex] = -0x1000000 and pixels[rowStartIndex] or
(weightedColorSum[redSum] shl 16) or
(weightedColorSum[greenSum] shl 8) or
weightedColorSum[blueSum]

redSum -= redOutSum
greenSum -= greenOutSum
blueSum -= blueOutSum
stackStart = stackPointer - radius + totalPixelsToAverage
currentPixel = stack[stackStart % totalPixelsToAverage]
redOutSum -= currentPixel[0]
greenOutSum -= currentPixel[1]
blueOutSum -= currentPixel[2]
if (x == 0) {
minimumDimension[y] = (y + radiusPlusOne).coerceAtMost(heightMinusOne) * width
}
pixelIndex = x + minimumDimension[y]
currentPixel[0] = reds[pixelIndex]
currentPixel[1] = greens[pixelIndex]
currentPixel[2] = blues[pixelIndex]
redInSum += currentPixel[0]
greenInSum += currentPixel[1]
blueInSum += currentPixel[2]
redSum += redInSum
greenSum += greenInSum
blueSum += blueInSum
stackPointer = (stackPointer + 1) % totalPixelsToAverage
currentPixel = stack[stackPointer]
redOutSum += currentPixel[0]
greenOutSum += currentPixel[1]
blueOutSum += currentPixel[2]
redInSum -= currentPixel[0]
greenInSum -= currentPixel[1]
blueInSum -= currentPixel[2]
rowStartIndex += width
y++
}
x++
}

// Set the blurred pixels in the bitmap image.
bitmap.setPixels(pixels, 0, width, 0, 0, width, height)

// Recycle the scaled bitmap image.
scaledBitmap.recycle()

// Return the blurred bitmap image.
return@withContext bitmap
}

0 comments on commit 19ef51e

Please sign in to comment.