-
Notifications
You must be signed in to change notification settings - Fork 28
/
SubSamplingImageSource.kt
executable file
·226 lines (199 loc) · 6.72 KB
/
SubSamplingImageSource.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package me.saket.telephoto.subsamplingimage
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Context
import android.content.res.AssetManager
import android.graphics.BitmapRegionDecoder
import android.net.Uri
import android.os.ParcelFileDescriptor
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.ImageBitmap
import okio.BufferedSource
import okio.Closeable
import okio.Path
import okio.Source
import okio.buffer
import java.io.InputStream
/**
* Image to display with [SubSamplingImage]. Can be one of:
*
* * [SubSamplingImageSource.file]
* * [SubSamplingImageSource.asset]
* * [SubSamplingImageSource.resource]
* * [SubSamplingImageSource.contentUri]
* * [SubSamplingImageSource.rawSource]
*/
sealed interface SubSamplingImageSource : Closeable {
/**
* A preview that can be displayed immediately while the bitmap tiles
* are loaded, which can be slightly slow depending on the file size.
*/
val preview: ImageBitmap?
companion object {
/**
* An image stored on the device file system. This can be used with
* image loading libraries that store cached images on disk.
*
* @param preview See [SubSamplingImageSource.preview].
* @param onClose Called when the image is no longer visible. This is useful for files
* stored in, say, an LRU cache that is capable of locking open files to
* prevent them from getting discarded.
*
* @return The returned value is stable and does not need to be remembered.
*/
@Stable
fun file(
path: Path,
preview: ImageBitmap? = null,
onClose: Closeable? = null
): SubSamplingImageSource = FileImageSource(path, preview, onClose)
/**
* An image stored in `src/main/assets`.
*
* @param preview See [SubSamplingImageSource.preview].
*
* @return The returned value is stable and does not need to be remembered.
*/
@Stable
fun asset(
name: String,
preview: ImageBitmap? = null
): SubSamplingImageSource = AssetImageSource(AssetPath(name), preview)
/**
* An image stored in `src/main/res/drawable*` directories.
*
* @param preview See [SubSamplingImageSource.preview].
*
* @return The returned value is stable and does not need to be remembered.
* */
@Stable
fun resource(
@DrawableRes id: Int,
preview: ImageBitmap? = null
): SubSamplingImageSource = ResourceImageSource(id, preview)
/**
* An image exposed by a content provider. A common use-case for this
* would be to display images shared by other apps.
*
* @param preview See [SubSamplingImageSource.preview].
*
* @return The returned value is stable and does not need to be remembered.
*/
@Stable
fun contentUri(
uri: Uri,
preview: ImageBitmap? = null
): SubSamplingImageSource {
val assetPath = uri.asAssetPathOrNull()
return if (assetPath != null) AssetImageSource(assetPath, preview) else UriImageSource(uri, preview)
}
/**
* An arbitrary stream that should only be used for images that can't be read directly
* from the disk. For all other purposes, prefer using [SubSamplingImageSource.file]
* instead as it is significantly faster.
*
* @param preview See [SubSamplingImageSource.preview].
* @param onClose Called when the image is no longer visible.
*/
@Stable
fun rawSource(
source: () -> Source, // todo: should this be a BufferedSource?
preview: ImageBitmap? = null,
onClose: Closeable? = null,
): SubSamplingImageSource = RawImageSource(source, preview, onClose)
}
suspend fun decoder(context: Context): BitmapRegionDecoder
/** Called when the image is no longer visible. */
override fun close() = Unit
}
@Immutable
internal data class FileImageSource(
val path: Path,
override val preview: ImageBitmap?,
val onClose: Closeable?
) : SubSamplingImageSource {
init {
check(path.isAbsolute)
}
override suspend fun decoder(context: Context): BitmapRegionDecoder {
return ParcelFileDescriptor.open(path.toFile(), ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
BitmapRegionDecoder.newInstance(fd.fileDescriptor, /* ignored */ false)
}
}
override fun close() {
onClose?.close()
}
}
@Immutable
internal data class AssetImageSource(
private val asset: AssetPath,
override val preview: ImageBitmap?
) : SubSamplingImageSource {
fun peek(context: Context): InputStream {
return context.assets.open(asset.path, AssetManager.ACCESS_RANDOM)
}
override suspend fun decoder(context: Context): BitmapRegionDecoder {
return peek(context).use { stream ->
check (stream is AssetManager.AssetInputStream) {
error("BitmapRegionDecoder won't be able to optimize reading of this asset")
}
BitmapRegionDecoder.newInstance(stream, /* ignored */ false)!!
}
}
}
@Immutable
internal data class ResourceImageSource(
@DrawableRes val id: Int,
override val preview: ImageBitmap?,
) : SubSamplingImageSource {
@SuppressLint("ResourceType")
fun peek(context: Context): InputStream {
return context.resources.openRawResource(id)
}
override suspend fun decoder(context: Context): BitmapRegionDecoder {
return peek(context).use { stream ->
BitmapRegionDecoder.newInstance(stream, /* ignored */ false)!!
}
}
}
@Immutable
internal data class UriImageSource(
private val uri: Uri,
override val preview: ImageBitmap?
) : SubSamplingImageSource {
fun peek(context: Context): InputStream {
return context.contentResolver.openInputStream(uri) ?: error("Failed to read uri: $uri")
}
override suspend fun decoder(context: Context): BitmapRegionDecoder {
return peek(context).use {
stream -> BitmapRegionDecoder.newInstance(stream, /* ignored */ false)!!
}
}
}
@Immutable
internal data class RawImageSource(
val source: () -> Source,
override val preview: ImageBitmap? = null,
private val onClose: Closeable?
) : SubSamplingImageSource {
fun peek(): BufferedSource {
return source().buffer().peek()
}
override suspend fun decoder(context: Context): BitmapRegionDecoder {
return source().buffer().inputStream().use { stream ->
BitmapRegionDecoder.newInstance(stream, /* ignored */ false)!!
}
}
override fun close() {
onClose?.close()
}
}
@Immutable
@JvmInline
internal value class AssetPath(val path: String)
internal fun Uri.asAssetPathOrNull(): AssetPath? {
val isAssetUri = scheme == ContentResolver.SCHEME_FILE && pathSegments.firstOrNull() == "android_asset"
return if (isAssetUri) AssetPath(pathSegments.drop(1).joinToString("/")) else null
}