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

Pre-render SVGs while decoding. #124

Merged
merged 2 commits into from
Sep 27, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions coil-base/src/main/java/coil/decode/DecodeUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,42 @@ object DecodeUtils {
}

/**
* Calculate the [BitmapFactory.Options.inSampleSize] given the source dimensions of the image ([inWidth] and [inHeight]),
* the output dimensions ([outWidth], [outHeight]), and the [scale].
* Calculate the [BitmapFactory.Options.inSampleSize] given the source dimensions of the image
* ([srcWidth] and [srcHeight]), the output dimensions ([destWidth], [destHeight]), and the [scale].
*/
@JvmStatic
fun calculateInSampleSize(
@Px inWidth: Int,
@Px inHeight: Int,
@Px outWidth: Int,
@Px outHeight: Int,
@Px srcWidth: Int,
@Px srcHeight: Int,
@Px destWidth: Int,
@Px destHeight: Int,
scale: Scale
): Int {
val widthInSampleSize = max(1, Integer.highestOneBit(inWidth / outWidth))
val heightInSampleSize = max(1, Integer.highestOneBit(inHeight / outHeight))
val widthInSampleSize = max(1, Integer.highestOneBit(srcWidth / destWidth))
val heightInSampleSize = max(1, Integer.highestOneBit(srcHeight / destHeight))
return when (scale) {
Scale.FILL -> min(widthInSampleSize, heightInSampleSize)
Scale.FIT -> max(widthInSampleSize, heightInSampleSize)
}
}

/**
* Calculate the percentage to multiply the source dimensions by to fit/fill the
* destination dimensions while preserving aspect ratio.
*/
@JvmStatic
fun computeSizeMultiplier(
@Px srcWidth: Float,
@Px srcHeight: Float,
@Px destWidth: Float,
@Px destHeight: Float,
scale: Scale
): Float {
val widthPercent = destWidth / srcWidth
val heightPercent = destHeight / srcHeight
return when (scale) {
Scale.FILL -> max(widthPercent, heightPercent)
Scale.FIT -> min(widthPercent, heightPercent)
}
}
}
19 changes: 10 additions & 9 deletions coil-base/src/main/java/coil/drawable/CrossfadeDrawable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.os.SystemClock
import androidx.annotation.VisibleForTesting
import coil.decode.DecodeUtils
import coil.size.Scale
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt

/**
Expand Down Expand Up @@ -150,14 +150,15 @@ class CrossfadeDrawable(

val targetWidth = targetBounds.width()
val targetHeight = targetBounds.height()
val widthPercent = targetWidth / width.toFloat()
val heightPercent = targetHeight / height.toFloat()
val scale = when (scale) {
Scale.FIT -> min(widthPercent, heightPercent)
Scale.FILL -> max(widthPercent, heightPercent)
}
val dx = ((targetWidth - scale * width) / 2).roundToInt()
val dy = ((targetHeight - scale * height) / 2).roundToInt()
val multiplier = DecodeUtils.computeSizeMultiplier(
srcWidth = width.toFloat(),
srcHeight = height.toFloat(),
destWidth = targetWidth.toFloat(),
destHeight = targetHeight.toFloat(),
scale = scale
)
val dx = ((targetWidth - multiplier * width) / 2).roundToInt()
val dy = ((targetHeight - multiplier * height) / 2).roundToInt()

val left = targetBounds.left + dx
val top = targetBounds.top + dy
Expand Down
2 changes: 1 addition & 1 deletion coil-sample/src/main/java/coil/sample/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Application : MultiDexApplication() {
} else {
add(GifDecoder())
}
add(SvgDecoder())
add(SvgDecoder(applicationContext))
}
okHttpClient {
// Create a disk cache with "unlimited" size. Don't do this in production.
Expand Down
68 changes: 56 additions & 12 deletions coil-svg/src/main/java/coil/decode/SvgDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@

package coil.decode

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
import androidx.core.graphics.drawable.toDrawable
import coil.bitmappool.BitmapPool
import coil.drawable.SvgDrawable
import coil.size.OriginalSize
import coil.size.PixelSize
import coil.size.Size
import com.caverock.androidsvg.SVG
import okio.BufferedSource
import kotlin.math.ceil

/**
* A [Decoder] that uses [AndroidSVG](https://bigbadaboom.github.io/androidsvg/) to decode SVG files.
*/
class SvgDecoder : Decoder {
class SvgDecoder(private val context: Context) : Decoder {

companion object {
private const val MIME_TYPE_SVG = "image/svg+xml"
private const val DEFAULT_SIZE = 512
}

override fun handles(source: BufferedSource, mimeType: String?) = mimeType == MIME_TYPE_SVG
Expand All @@ -28,17 +34,55 @@ class SvgDecoder : Decoder {
size: Size,
options: Options
): DecodeResult {
val svg = source.use { SVG.getFromInputStream(it.inputStream()) }

val svgWidth = svg.documentWidth
val svgHeight = svg.documentHeight

val bitmapWidth: Int
val bitmapHeight: Int
when (size) {
is PixelSize -> {
if (svgWidth > 0 && svgHeight > 0) {
val multiplier = DecodeUtils.computeSizeMultiplier(
srcWidth = svgWidth,
srcHeight = svgHeight,
destWidth = size.width.toFloat(),
destHeight = size.height.toFloat(),
scale = options.scale
)
bitmapWidth = ceil(multiplier * svgWidth).toInt()
bitmapHeight = ceil(multiplier * svgHeight).toInt()
} else {
bitmapWidth = size.width
bitmapHeight = size.height
}
}
is OriginalSize -> {
if (svgWidth > 0 && svgHeight > 0) {
bitmapWidth = ceil(svgWidth).toInt()
bitmapHeight = ceil(svgHeight).toInt()
} else {
bitmapWidth = DEFAULT_SIZE
bitmapHeight = DEFAULT_SIZE
}
}
}

val config = when {
options.allowRgb565 -> Bitmap.Config.RGB_565
SDK_INT >= O && options.config == Bitmap.Config.HARDWARE -> Bitmap.Config.ARGB_8888
else -> options.config
}
val bitmap = pool.get(bitmapWidth, bitmapHeight, config)

svg.setDocumentWidth("100%")
svg.setDocumentHeight("100%")
svg.renderToCanvas(Canvas(bitmap))

return DecodeResult(
drawable = SvgDrawable(
svg = source.use { SVG.getFromInputStream(it.inputStream()) },
config = when {
options.allowRgb565 -> Bitmap.Config.RGB_565
SDK_INT >= O && options.config == Bitmap.Config.HARDWARE -> Bitmap.Config.ARGB_8888
else -> options.config
},
pool = pool
),
isSampled = false
drawable = bitmap.toDrawable(context.resources),
isSampled = true // SVGs can always be re-decoded at a higher resolution.
)
}
}
89 changes: 0 additions & 89 deletions coil-svg/src/main/java/coil/drawable/SvgDrawable.kt

This file was deleted.