coil icon indicating copy to clipboard operation
coil copied to clipboard

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

Open alekseyHunter opened this issue 1 year ago • 25 comments

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

alekseyHunter avatar Jan 06 '24 01:01 alekseyHunter

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

colinrtwhite avatar Jan 08 '24 09:01 colinrtwhite

@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 avatar Jan 08 '24 10:01 alekseyHunter

@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)

alekseyHunter avatar Jan 11 '24 04:01 alekseyHunter

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.

devmike01 avatar Jan 12 '24 16:01 devmike01

@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)

alekseyHunter avatar Jan 12 '24 16:01 alekseyHunter

@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.

colinrtwhite avatar Jan 13 '24 21:01 colinrtwhite

@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)

alekseyHunter avatar Jan 15 '24 10:01 alekseyHunter

@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.

devmike01 avatar Jan 19 '24 14:01 devmike01

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

alekseyHunter avatar Jan 19 '24 16:01 alekseyHunter

I meant web browser, not your image browser.

devmike01 avatar Jan 24 '24 09:01 devmike01

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

alekseyHunter avatar Jan 24 '24 16:01 alekseyHunter

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

devmike01 avatar Jan 24 '24 16:01 devmike01

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 avatar Feb 15 '24 02:02 ejektaflex

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

ejektaflex avatar Feb 17 '24 23:02 ejektaflex

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.

green-pico avatar Feb 20 '24 17:02 green-pico

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 avatar Feb 20 '24 21:02 serandel

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

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

serandel avatar Feb 20 '24 21:02 serandel

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...

green-pico avatar Feb 21 '24 07:02 green-pico

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.

serandel avatar Feb 21 '24 07:02 serandel

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 avatar Feb 22 '24 18:02 green-pico

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.

green-pico avatar Feb 22 '24 19:02 green-pico

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.

serandel avatar Feb 24 '24 09:02 serandel

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 avatar Feb 27 '24 14:02 acarlsen

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()

acarlsen avatar Feb 27 '24 15:02 acarlsen

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

serandel avatar May 18 '24 13:05 serandel