coil icon indicating copy to clipboard operation
coil copied to clipboard

Scaled down images look excessively pixelated on desktop

Open hypergonial opened this issue 10 months ago • 5 comments

Describe the bug

Large AsyncImages scaled down (compared to their original size) look excessively pixelated (on at least) desktop. This doesn't seem to be an issue on Android, so I'm guessing this is not intended behaviour? I can only test these two platforms right now, so it is possible other platforms are also affected.

The effect is most noticeable with screenshots that contain lots of text, as the text becomes completely distorted and unreadable.

To Reproduce

Try to load a high-resolution image with medium-to-small text on it, as the issue is most apparent on images of this type, (e.g. a screenshot of an IDE window) and try to scale it down.

Version v3.1.0

Samples

Original image:

Image

Android scaled down a lot (this is still just about readable!):

Image

Desktop scaled down slightly (already falling apart):

Image

Desktop scaled down a lot (unreadable):

Image

I considered that this is simply due to the higher pixel density of a phone's screen, but the issue is also not reproducible in the emulator so I'm not sure anymore. 😅

hypergonial avatar Mar 09 '25 19:03 hypergonial

Can you share the exact code you are using to scale the images down, it might be helpful for figuring out why this is happening.

ojasvib avatar Mar 10 '25 10:03 ojasvib

Can you share the exact code you are using to scale the images down, it might be helpful for figuring out why this is happening.

Hi!

As far as I can tell, the only thing coil-related is ContentScale.Fit.

AsyncImage(
    model = ImageRequest.Builder(LocalPlatformContext.current).data(url).crossfade(true).build(),
    contentDescription = attachment.filename,
    modifier =
        Modifier.padding(top = 8.dp, end = 20.dp)
            .widthIn(Dp.Unspecified, 500.dp)
            .heightIn(Dp.Unspecified, 500.dp)
            .clip(RoundedCornerShape(8.dp))
            .clickable(null, indication = null) { component.onAttachmentClicked(attachment.id) }
            .pointerHoverIcon(PointerIcon.Hand),
    contentScale = ContentScale.Fit,
)

The ImageLoader is defined as follows:

setSingletonImageLoaderFactory { context ->
    ImageLoader.Builder(context).crossfade(true).build()
}

The project I'm working on is open source, with this snippet originating from here, if that's of any use.

hypergonial avatar Mar 10 '25 12:03 hypergonial

Android uses a scaler that does not perform anti-aliasing and does not follow frequency folding rules, due to performance reasons ( not only android, any typical 2D rendering engine as Skia ). This issue is not solvable without an external, correct resampler. You’ll need to build and integrate something like the following to make it work properly: https://github.com/Cykooz/fast_image_resize https://github.com/awxkee/pic-scale https://gist.github.com/awxkee/0b39f832b6a81e17f91c9ae6a7dd61c6

Btw OpenCV does the same incorrect scaling, there is only Box filter that works more correct, but not fully correct, because it is correct only for downscaling with factor 2.

awxkee avatar May 21 '25 10:05 awxkee

Can you share the exact code you are using to scale the images down, it might be helpful for figuring out why this is happening.

https://apache-cordova73.github.io/ty/

Overlay-progressive avatar Oct 17 '25 22:10 Overlay-progressive

Temporary solution generated by Gemini. @hypergonial

import coil3.compose.AsyncImage
import coil3.compose.LocalPlatformContext
import coil3.request.ImageRequest
import coil3.size.Precision
import coil3.size.Size
import androidx.compose.ui.graphics.FilterQuality

// Get the context for the current platform (e.g., JVM)
val context = LocalPlatformContext.current 

val imageRequest = ImageRequest.Builder(context)
    .data("https://your.image.url/image.jpg")
    // Option A: Force the original size
    .size(Size.ORIGINAL) 
    // Option B: Tell Coil not to use an 'inexact' (optimized) size
    .precision(Precision.EXACT) 
    .build()

AsyncImage(
    model = imageRequest,
    contentDescription = "Description",
    // Still use High quality for the draw-time scaling
    filterQuality = FilterQuality.High, 
    modifier = Modifier.size(100.dp)
)

hmy65 avatar Nov 11 '25 02:11 hmy65