coil-kt / coil

Image loading for Android and Compose Multiplatform.
https://coil-kt.github.io/coil/
Apache License 2.0
10.67k stars 648 forks source link

[Coil 3] Local image files don't show on PC #2014

Closed alekseyHunter closed 1 month ago

alekseyHunter commented 8 months ago

Describe the bug Hello! Local image files don't show on PC. Error type is not in the function error()

To Reproduce

val platformContext = LocalPlatformContext.current

val uri = "file:///H:/1.png".toUri()

val request = ImageRequest.Builder(platformContext)
    .data(uri)
    .error {
        println("error")
        null
    }.build()

AsyncImage(
    model = request,
    contentDescription = "",
    contentScale = ContentScale.FillHeight,
    modifier = Modifier.fillMaxSize()
)

Logs/Screenshots image

image

image

Version Coil 3.0.0-alpha01

kotlin.version=1.9.21 agp.version=8.1.4 compose.version=1.5.11

colinrtwhite commented 8 months ago

What's the exception stacktrace from AsyncImage's onError?

alekseyHunter commented 8 months ago

@colinrtwhite The exception stacktrace is the following:

java.nio.file.InvalidPathException: Illegal char <:> at index 2: /H:/1.png
    at java.base/sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182)
    at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153)
    at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77)
    at java.base/sun.nio.fs.WindowsPath.parse(WindowsPath.java:92)
    at java.base/sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:232)
    at java.base/java.nio.file.Path.of(Path.java:147)
    at java.base/java.nio.file.Paths.get(Paths.java:69)
    at okio.Path.toNioPath(Path.kt:102)
    at okio.NioSystemFileSystem.metadataOrNull(NioSystemFileSystem.kt:35)
    at okio.internal.-FileSystem.commonMetadata(FileSystem.kt:36)
    at okio.FileSystem.metadata(FileSystem.kt:33)
    at coil3.key.FileUriKeyer.key(FileUriKeyer.kt:16)
    at coil3.key.FileUriKeyer.key(FileUriKeyer.kt:8)
    at coil3.ComponentRegistry.key(ComponentRegistry.kt:56)
    at coil3.memory.MemoryCacheService.newCacheKey(MemoryCacheService.kt:45)
    at coil3.intercept.EngineInterceptor.intercept(EngineInterceptor.kt:52)
    at coil3.intercept.RealInterceptorChain.proceed(RealInterceptorChain.kt:31)
    at coil3.RealImageLoader$executeMain$result$1.invokeSuspend(RealImageLoader.common.kt:136)
    at coil3.RealImageLoader$executeMain$result$1.invoke(RealImageLoader.common.kt)
    at coil3.RealImageLoader$executeMain$result$1.invoke(RealImageLoader.common.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:65)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:167)
    at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
    at coil3.RealImageLoader.executeMain(RealImageLoader.common.kt:127)
    at coil3.RealImageLoader.access$executeMain(RealImageLoader.common.kt:42)
    at coil3.RealImageLoader$executeMain$1.invokeSuspend(RealImageLoader.common.kt)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:235)
    at kotlinx.coroutines.DispatchedTaskKt.resumeUnconfined(DispatchedTask.kt:191)
    at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:163)
    at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:368)
    at kotlinx.coroutines.flow.StateFlowSlot.makePending(StateFlow.kt:284)
    at kotlinx.coroutines.flow.StateFlowImpl.updateState(StateFlow.kt:349)
    at kotlinx.coroutines.flow.StateFlowImpl.setValue(StateFlow.kt:316)
    at coil3.compose.ConstraintsSizeResolver.measure-3p2s80s(ConstraintsSizeResolver.kt:32)
    at androidx.compose.ui.node.BackwardsCompatNode.measure-3p2s80s(BackwardsCompatNode.kt:311)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
    at androidx.compose.foundation.layout.FillNode.measure-3p2s80s(Size.kt:698)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1499)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1495)
    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2300)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:471)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:234)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:133)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:113)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1495)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:35)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:560)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.measure-BRTryo0(LayoutNodeLayoutDelegate.kt:539)
    at androidx.compose.foundation.layout.BoxKt$boxMeasurePolicy$1.measure-3p2s80s(Box.kt:114)
    at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:126)
    at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:646)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
    at androidx.compose.foundation.layout.SizeNode.measure-3p2s80s(Size.kt:837)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
    at androidx.compose.foundation.layout.FillNode.measure-3p2s80s(Size.kt:698)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
    at androidx.compose.foundation.layout.PaddingNode.measure-3p2s80s(Padding.kt:397)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1499)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1495)
    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2300)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:471)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:234)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:133)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:113)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1495)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:35)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:560)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.measure-BRTryo0(LayoutNodeLayoutDelegate.kt:539)
    at androidx.compose.foundation.layout.RowColumnMeasurementHelper.measureWithoutPlacing-_EkL_-Y(RowColumnMeasurementHelper.kt:112)
    at androidx.compose.foundation.layout.RowColumnImplKt$rowColumnMeasurePolicy$1.measure-3p2s80s(RowColumnImpl.kt:72)
    at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:126)
    at androidx.compose.foundation.layout.PaddingNode.measure-3p2s80s(Padding.kt:397)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1499)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1495)
    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2300)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:471)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:234)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:133)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:113)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1495)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:35)
    at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:560)
    at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui(LayoutNode.kt:1140)
    at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui$default(LayoutNode.kt:1131)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA(MeasureAndLayoutDelegate.kt:323)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:458)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.access$remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:39)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout(MeasureAndLayoutDelegate.kt:344)
    at androidx.compose.ui.platform.SkiaBasedOwner.measureAndLayout(SkiaBasedOwner.skiko.kt:246)
    at androidx.compose.ui.node.Owner.measureAndLayout$default(Owner.kt:223)
    at androidx.compose.ui.ComposeScene.render(ComposeScene.skiko.kt:546)
    at androidx.compose.ui.awt.ComposeBridge$skikoView$1$onRender$1.invoke(ComposeBridge.desktop.kt:178)
    at androidx.compose.ui.awt.ComposeBridge$skikoView$1$onRender$1.invoke(ComposeBridge.desktop.kt:177)
    at androidx.compose.ui.awt.ComposeBridge.catchExceptions(ComposeBridge.desktop.kt:150)
    at androidx.compose.ui.awt.ComposeBridge.access$catchExceptions(ComposeBridge.desktop.kt:64)
    at androidx.compose.ui.awt.ComposeBridge$skikoView$1.onRender(ComposeBridge.desktop.kt:177)
    at org.jetbrains.skiko.SkiaLayer.update$skiko(SkiaLayer.awt.kt:548)
    at org.jetbrains.skiko.redrawer.AWTRedrawer.update(AWTRedrawer.kt:54)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invokeSuspend(Direct3DRedrawer.kt:49)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invoke(Direct3DRedrawer.kt)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invoke(Direct3DRedrawer.kt)
    at org.jetbrains.skiko.FrameDispatcher$job$1.invokeSuspend(FrameDispatcher.kt:33)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
    at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
alekseyHunter commented 8 months ago

@colinrtwhite After updating to version 3.0.0-alpha02 and changing the file path from "file:///H:/1.png" to "H://1.png" the exception stacktrace looks like this:

java.lang.IllegalStateException: Unable to create a fetcher that supports: H:\1.png
    at coil3.intercept.EngineInterceptor.fetch(EngineInterceptor.kt:145)
    at coil3.intercept.EngineInterceptor.execute(EngineInterceptor.kt:109)
    at coil3.intercept.EngineInterceptor.access$execute(EngineInterceptor.kt:29)
    at coil3.intercept.EngineInterceptor$intercept$2.invokeSuspend(EngineInterceptor.kt:63)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
    at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
    at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:589)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:806)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:710)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:697)
devmike01 commented 8 months ago

Looks like this has nothing to do with the coil library. /H: is not a valid directory path. Remove the colon : after the H path.

alekseyHunter commented 8 months ago

@devmike01 This doesn't work, I got another exception - FileNotFoundException ;)

java.io.FileNotFoundException: no such file: /H/1.png
    at okio.internal.-FileSystem.commonMetadata(FileSystem.kt:36)
    at okio.FileSystem.metadata(FileSystem.kt:33)
    at coil3.key.FileUriKeyer.key(FileUriKeyer.kt:16)
    at coil3.key.FileUriKeyer.key(FileUriKeyer.kt:8)
    at coil3.ComponentRegistry.key(ComponentRegistry.kt:66)
colinrtwhite commented 8 months ago

@alekseyHunter I agree with @devmike01 I think there might be something wrong with your path, though I'm not sure exactly how to represent a Windows drive name in an Okio path. I'd try: file://\H:\\1.png.

There might also be issues with Coil relying on / as Windows uses \ for file paths.

alekseyHunter commented 8 months ago

@colinrtwhite This doesn't work either (file://\H:\\1.png). I caught the same exception:

java.lang.IllegalStateException: Unable to create a fetcher that supports: file://\H:\\1.png
    at coil3.intercept.EngineInterceptor.fetch(EngineInterceptor.kt:145)
    at coil3.intercept.EngineInterceptor.execute(EngineInterceptor.kt:109)
    at coil3.intercept.EngineInterceptor.access$execute(EngineInterceptor.kt:29)
    at coil3.intercept.EngineInterceptor$intercept$2.invokeSuspend(EngineInterceptor.kt:63)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
    at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
    at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:589)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:806)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:710)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:697)
devmike01 commented 8 months ago

@alekseyHunter Fix your error or post the issue on StackOverflow. Try accessing this image(file://\H:\\1.png) file from your browser and see if you are able to do so. I doubt it. Again, this has nothing to do with Coil.

alekseyHunter commented 8 months ago

Fix your error or post the issue on StackOverflow.

@devmike01 What's the logic? How to fix the error, if there is an error in the library.

I doubt it. Again, this has nothing to do with Coil.

@devmike01 Can you load a local picture on your computer using the library? Will you show the result on video?

Try accessing this image(file://\H:\1.png) file from your browser and see if you are able to do so. I doubt it.

@devmike01 Everything loads fine. image image

devmike01 commented 8 months ago

I meant web browser, not your image browser.

alekseyHunter commented 8 months ago

@devmike01 Yes, the second screenshot shows the result in the web browser (pixlr.com). Everything works in it ;)

devmike01 commented 8 months ago

The second images is hosted on a server. I meant load the photo from your folder path in your web browser.

ejektaflex commented 7 months ago

I just had the same issue, it's nice to know that it wasn't me. I'm using 3.0.0-Alpha 04.

When passing in a File, Coil fails to load the image on Jetbrains Compose (JVM) on Windows.

I had to do this to get it to work:

val loc = "file://" + imgFiles[1].absolutePath.replace("\\", "/")

AsyncImage(
  model = loc, contentDescription = "Test Photo"
)

I would assume that we should expect to just pass a File object to Coil, and it would work correctly, right?

It definitely does not seem to like Windows file paths.

ejektaflex commented 7 months ago

While the above temporary fix does work on my C drive, it also does not work when referencing a file on another drive.

green-pico commented 7 months ago

I run into the exact same problem. I am on 3.0.0-alpha04 implementing a KMP project targeting Windows and Android.

The file I want to show on Windows is located at "C:\Users\<user>\AppData\Roaming\<project>\Thumbnails\tool2019fearinoculum.jpg" (c+p from file explorer)

When I run

File("C:/Users/<user>/AppData/Roaming/<project>/Thumbnails/tool2019fearinoculum.jpg").exists()

or

File("C:\\Users\\<user>\\AppData\\Roaming\\<project>\\Thumbnails\\tool2019fearinoculum.jpg").exists()

it returns true. However, when I run one of the above like this

AsyncImage(
    model = File("C:/Users/<user>/AppData/Roaming/<project>/Thumbnails/tool2019fearinoculum.jpg"),
    contentDescription = "null",
    onError = { println(it) }
)

it gives me throwable=java.lang.IllegalStateException: Unable to create a fetcher that supports: file://C:\Users\<user>\AppData\Roaming\<project>\Thumbnails\tool2019fearinoculum.jpg.

I agree with @ejektaflex. If I am not doing anything completely wrong, I would also assume this code to work.

Edit: Putting file://C:\Users\<user>\AppData\Roaming\<project>\Thumbnails\tool2019fearinoculum.jpg into the browser shows the image. Passing it as the model parameter, however, does not work.

serandel commented 7 months ago

Same here. Compose for Desktop in a Windows 11 machine.

AsyncImage(
   model = imageFile,
   contentDescription = "test",
   contentScale = ContentScale.Fit,
   modifier = Modifier.fillMaxSize(),
   onError = { println(it.result.throwable)}
)

This shows nothing in my UI, but outputs the following error:

java.lang.IllegalStateException: Unable to create a fetcher that supports: file://.myapp\190f6e6c-e527-456a-84bc-301a62bf5486.png

I'm positive the file is OK, because I'm scanning a folder, and I'm migrating from another image loader that displayed it with no problem. Also, if I pass an URL of a remote image, it loads and displays perfectly fine.

serandel commented 7 months ago

This workaround is fine in my Windows, but I suppose it will break anywhere else.

val loc = "file://" + imageFile.absolutePath.replace("\\", "/")
green-pico commented 7 months ago

This workaround is fine in my Windows, but I suppose it will break anywhere else.

val loc = "file://" + imageFile.absolutePath.replace("\\", "/")

Would you mind posting the exact contents of the String that you passed to AsyncImage? My images are located in C:...\AppData\ and I tried all possibilities with \ and / and nothing would work for me...

serandel commented 7 months ago

This workaround is fine in my Windows, but I suppose it will break anywhere else.

val loc = "file://" + imageFile.absolutePath.replace("\\", "/")

Would you mind posting the exact contents of the String that you passed to AsyncImage? My images are located in C:...\AppData\ and I tried all possibilities with \ and / and nothing would work for me...

Of course, this is the original file: .myapp\190f6e6c-e527-456a-84bc-301a62bf5486.png (I've tried passing both the File and its path as a String) and the workaround is using the String file://.myapp/190f6e6c-e527-456a-84bc-301a62bf5486.png instead.

green-pico commented 7 months ago

This workaround is fine in my Windows, but I suppose it will break anywhere else.

val loc = "file://" + imageFile.absolutePath.replace("\\", "/")

Would you mind posting the exact contents of the String that you passed to AsyncImage? My images are located in C:...\AppData\ and I tried all possibilities with \ and / and nothing would work for me...

Of course, this is the original file: .myapp\190f6e6c-e527-456a-84bc-301a62bf5486.png (I've tried passing both the File and its path as a String) and the workaround is using the String file://.myapp/190f6e6c-e527-456a-84bc-301a62bf5486.png instead.

Thank you. What exactly is .myapp? Is it the root directory of your app? I was looking for a workaround where I can load images from the "AppData" folder on the C: drive. Any ideas? Because using the absolute path with the drive letter seems to create the confusion within Coil.

green-pico commented 7 months ago

I also tried it with Okio itself:

FileSystem.SYSTEM.exists("C:/Users/<user>/AppData/Roaming/<project>/Thumbnails/tool2019fearinoculum.jpg".toPath())

returns true. If I put this Okio path into the AsyncImage like this

AsyncImage(
    model = "C:/Users/<user>/AppData/Roaming/<project>/Thumbnails/tool2019fearinoculum.jpg".toPath(),
    contentDescription = "null",
    onError = { println(it) }
)

it throws java.io.FileNotFoundException: no such file: /Users/<user>/AppData/Roaming/<project>/Thumbnails/tool2019fearinoculum.jpg. It seems to swallow the drive letter thus making the path invalid.

serandel commented 6 months ago

This workaround is fine in my Windows, but I suppose it will break anywhere else.

val loc = "file://" + imageFile.absolutePath.replace("\\", "/")

Would you mind posting the exact contents of the String that you passed to AsyncImage? My images are located in C:...\AppData\ and I tried all possibilities with \ and / and nothing would work for me...

Of course, this is the original file: .myapp\190f6e6c-e527-456a-84bc-301a62bf5486.png (I've tried passing both the File and its path as a String) and the workaround is using the String file://.myapp/190f6e6c-e527-456a-84bc-301a62bf5486.png instead.

Thank you. What exactly is .myapp? Is it the root directory of your app? I was looking for a workaround where I can load images from the "AppData" folder on the C: drive. Any ideas? Because using the absolute path with the drive letter seems to create the confusion within Coil.

It's just a subfolder of the working directory of my app, nothing special.

acarlsen commented 6 months ago

I have the same issue on Windows. Using workaround below works on C drive only, not on other drive letters.

val loc = "file://" + imageFile.absolutePath.replace("\\", "/")

I think the issue is in FileUriFetcher.kt in Coil.

It has this code:

    override suspend fun fetch(): FetchResult {
        val path = checkNotNull(uri.path) { "path == null" }.toPath()
        return SourceFetchResult(
            source = ImageSource(path, options.fileSystem),
            mimeType = MimeTypeMap.getMimeTypeFromExtension(path.extension),
            dataSource = DataSource.DISK,
        )
    }

For example input file on drive D "file://D:/861746.jpg" the resulting path in this code becomes "/861768.jpg", thus the files location cannot be resolved.

It works for C drive, because that is the default drive.

The solution is the above code should take drive letter into consideration.

EDIT: I think it might even work with normal windows style path like D:\861768.jp looking and OKIO toPath code, but then the issue is the Coil ComponentRegistry never gets to use the FileUriFetcher..

acarlsen commented 6 months ago

Possible workaround to use normal windows path.

Create a custom fetcher:

internal class WindowsFileUriFetcher(
        private val uri: Uri,
        private val options: Options,
) : Fetcher {

    @OptIn(InternalCoilApi::class)
    override suspend fun fetch(): FetchResult {
        val path = uri.toString().toPath()
        return SourceFetchResult(
                source = ImageSource(path, options.fileSystem),
                mimeType = MimeTypeMap.getMimeTypeFromExtension(path.name.substringAfterLast('.', "")),
                dataSource = DataSource.DISK,
        )
    }

    class Factory : Fetcher.Factory<Uri> {

        private val regex = "^[a-zA-Z]:\\\\.*".toRegex()

        override fun create(
                data: Uri,
                options: Options,
                imageLoader: ImageLoader,
        ): Fetcher? {
            if (hostOs != OS.Windows || !regex.matches(data.toString())) return null
            return WindowsFileUriFetcher(data, options)
        }
    }
}

Register it:

ImageLoader.Builder(context)
            .components {
                add(OkHttpNetworkFetcherFactory())
                add(WindowsFileUriFetcher.Factory())
            }
            .build()
serandel commented 4 months ago

Is this still happening? Do you need any help creating a patch here?