Browse Source

[resources] Downscale drawable resources if it came from upper dpi. (#5101)

Now drawables from upper DPIs will be downscalled to the expected size.
(the same behavior as on Android)
**BEFORE**
<img
src="https://github.com/user-attachments/assets/20502b72-079b-404b-b5b4-43ed7a8d3446"
width="200" />
**AFTER**
<img
src="https://github.com/user-attachments/assets/cd0b5a69-adb6-4552-bf4f-127d467bba85"
width="200" />


Fixes https://youtrack.jetbrains.com/issue/CMP-5657

## Release Notes
### Fixes - Resources
- Now drawables from upper DPIs will be downscalled to the expected
size. (the same behavior as on Android)
pull/5104/head
Konstantin 4 months ago committed by GitHub
parent
commit
5d22f7ca20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 13
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ImageResources.android.kt
  2. 24
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt
  3. 31
      components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt

13
components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ImageResources.android.kt

@ -6,8 +6,17 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
internal actual fun ByteArray.toImageBitmap(): ImageBitmap = internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap {
BitmapFactory.decodeByteArray(this, 0, size).asImageBitmap() val options = BitmapFactory.Options().apply {
//https://youtrack.jetbrains.com/issue/CMP-5657
//android only downscales drawables. If there is only low dpi resource then use it as is (not upscale)
if (resourceDensity > targetDensity) {
inDensity = resourceDensity
inTargetDensity = targetDensity
}
}
return BitmapFactory.decodeByteArray(this, 0, size, options).asImageBitmap()
}
internal actual class SvgElement internal actual class SvgElement

24
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt

@ -56,10 +56,17 @@ private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) }
@Composable @Composable
fun imageResource(resource: DrawableResource): ImageBitmap { fun imageResource(resource: DrawableResource): ImageBitmap {
val resourceReader = LocalResourceReader.currentOrPreview val resourceReader = LocalResourceReader.currentOrPreview
val imageBitmap by rememberResourceState(resource, resourceReader, { emptyImageBitmap }) { env -> val resourceEnvironment = rememberResourceEnvironment()
val path = resource.getResourceItemByEnvironment(env).path val imageBitmap by rememberResourceState(
val cached = loadImage(path, resourceReader) { resource, resourceReader, resourceEnvironment, { emptyImageBitmap }
ImageCache.Bitmap(it.toImageBitmap()) ) { env ->
val item = resource.getResourceItemByEnvironment(env)
val resourceDensityQualifier = item.qualifiers.firstOrNull { it is DensityQualifier } as? DensityQualifier
val resourceDensity = resourceDensityQualifier?.dpi ?: DensityQualifier.MDPI.dpi
val screenDensity = resourceEnvironment.density.dpi
val path = item.path
val cached = loadImage(path, "$path-${screenDensity}dpi", resourceReader) {
ImageCache.Bitmap(it.toImageBitmap(resourceDensity, screenDensity))
} as ImageCache.Bitmap } as ImageCache.Bitmap
cached.bitmap cached.bitmap
} }
@ -82,7 +89,7 @@ fun vectorResource(resource: DrawableResource): ImageVector {
val density = LocalDensity.current val density = LocalDensity.current
val imageVector by rememberResourceState(resource, resourceReader, density, { emptyImageVector }) { env -> val imageVector by rememberResourceState(resource, resourceReader, density, { emptyImageVector }) { env ->
val path = resource.getResourceItemByEnvironment(env).path val path = resource.getResourceItemByEnvironment(env).path
val cached = loadImage(path, resourceReader) { val cached = loadImage(path, path, resourceReader) {
ImageCache.Vector(it.toXmlElement().toImageVector(density)) ImageCache.Vector(it.toXmlElement().toImageVector(density))
} as ImageCache.Vector } as ImageCache.Vector
cached.vector cached.vector
@ -102,7 +109,7 @@ private fun svgPainter(resource: DrawableResource): Painter {
val density = LocalDensity.current val density = LocalDensity.current
val svgPainter by rememberResourceState(resource, resourceReader, density, { emptySvgPainter }) { env -> val svgPainter by rememberResourceState(resource, resourceReader, density, { emptySvgPainter }) { env ->
val path = resource.getResourceItemByEnvironment(env).path val path = resource.getResourceItemByEnvironment(env).path
val cached = loadImage(path, resourceReader) { val cached = loadImage(path, path, resourceReader) {
ImageCache.Svg(it.toSvgElement().toSvgPainter(density)) ImageCache.Svg(it.toSvgElement().toSvgPainter(density))
} as ImageCache.Svg } as ImageCache.Svg
cached.painter cached.painter
@ -126,7 +133,7 @@ suspend fun getDrawableResourceBytes(
return DefaultResourceReader.read(resourceItem.path) return DefaultResourceReader.read(resourceItem.path)
} }
internal expect fun ByteArray.toImageBitmap(): ImageBitmap internal expect fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap
internal expect fun ByteArray.toXmlElement(): Element internal expect fun ByteArray.toXmlElement(): Element
internal expect fun ByteArray.toSvgElement(): SvgElement internal expect fun ByteArray.toSvgElement(): SvgElement
@ -145,6 +152,7 @@ internal fun dropImageCache() {
private suspend fun loadImage( private suspend fun loadImage(
path: String, path: String,
cacheKey: String,
resourceReader: ResourceReader, resourceReader: ResourceReader,
decode: (ByteArray) -> ImageCache decode: (ByteArray) -> ImageCache
): ImageCache = imageCache.getOrLoad(path) { decode(resourceReader.read(path)) } ): ImageCache = imageCache.getOrLoad(cacheKey) { decode(resourceReader.read(path)) }

31
components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt

@ -6,10 +6,37 @@ import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import org.jetbrains.skia.Data import org.jetbrains.skia.Data
import org.jetbrains.skia.Image import org.jetbrains.skia.Image
import org.jetbrains.skia.Paint
import org.jetbrains.skia.Rect
import org.jetbrains.skia.SamplingMode
import org.jetbrains.skia.Surface
import org.jetbrains.skia.svg.SVGDOM import org.jetbrains.skia.svg.SVGDOM
internal actual fun ByteArray.toImageBitmap(): ImageBitmap = internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap {
Image.makeFromEncoded(this).toComposeImageBitmap() val image = Image.makeFromEncoded(this)
val targetImage: Image
//https://youtrack.jetbrains.com/issue/CMP-5657
//android only downscales drawables. If there is only low dpi resource then use it as is (not upscale)
//we need a consistent behavior on all platforms
if (resourceDensity > targetDensity) {
val scale = targetDensity.toFloat() / resourceDensity.toFloat()
val targetH = image.height * scale
val targetW = image.width * scale
val srcRect = Rect.Companion.makeWH(image.width.toFloat(), image.height.toFloat())
val dstRect = Rect.Companion.makeWH(targetW, targetH)
targetImage = Surface.makeRasterN32Premul(targetW.toInt(), targetH.toInt()).run {
val paint = Paint().apply { isAntiAlias = true }
canvas.drawImageRect(image, srcRect, dstRect, SamplingMode.LINEAR, paint, true)
makeImageSnapshot()
}
} else {
targetImage = image
}
return targetImage.toComposeImageBitmap()
}
internal actual class SvgElement(val svgdom: SVGDOM) internal actual class SvgElement(val svgdom: SVGDOM)

Loading…
Cancel
Save