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
use ahash::HashMap;
use egui::{
load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError},
mutex::Mutex,
};
use std::{path::PathBuf, sync::Arc, task::Poll, thread};
#[derive(Clone)]
struct File {
bytes: Arc<[u8]>,
mime: Option<String>,
}
type Entry = Poll<Result<File, String>>;
#[derive(Default)]
pub struct FileLoader {
/// Cache for loaded files
cache: Arc<Mutex<HashMap<String, Entry>>>,
}
impl FileLoader {
pub const ID: &'static str = egui::generate_loader_id!(FileLoader);
}
const PROTOCOL: &str = "file://";
/// Converts a hopefully uri encoded string into a `PathBuf`
///
/// Note that there is only minimal translation of the uri string into a path to support windows
/// file and unc paths. Other translations like percent un-encoding are not handled.
fn convert_uri_to_path(s: &str) -> Result<PathBuf, egui::load::LoadError> {
// File loader only supports the `file` protocol.
let s = s
.strip_prefix(PROTOCOL)
.ok_or(egui::load::LoadError::NotSupported)?;
if cfg!(target_os = "windows") {
// Standard windows file uris should have the form
//
// file:///c:/path/to/the%20file.txt
//
// in which the hostname field is left out. Check for this by looking at the next character
// after the schema, if it's a slash then we likely have a standard file path.
if let Some(stripped) = s.strip_prefix("/") {
let path = PathBuf::from(stripped);
return Ok(path);
}
// If it's not a standard file uri, it might be a UNC network path of the form
//
// file://hostname/path/to/the%20file.txt
//
// These file uris need to be converted into UNC correct and so need to have the leading
// two backslashes prepended.
let path = PathBuf::from(format!("\\\\{s}"));
return Ok(path);
}
Ok(PathBuf::from(s))
}
impl BytesLoader for FileLoader {
fn id(&self) -> &str {
Self::ID
}
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
let path = convert_uri_to_path(uri)?;
let mut cache = self.cache.lock();
if let Some(entry) = cache.get(uri).cloned() {
// `path` has either begun loading, is loaded, or has failed to load.
match entry {
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
size: None,
bytes: Bytes::Shared(file.bytes),
mime: file.mime,
}),
Poll::Ready(Err(err)) => Err(LoadError::Loading(err)),
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
}
} else {
log::trace!("started loading {uri:?}");
// We need to load the file at `path`.
// Set the file to `pending` until we finish loading it.
cache.insert(uri.to_owned(), Poll::Pending);
drop(cache);
// Spawn a thread to read the file, so that we don't block the render for too long.
thread::Builder::new()
.name(format!("egui_extras::FileLoader::load({uri:?})"))
.spawn({
let ctx = ctx.clone();
let cache = Arc::clone(&self.cache);
let uri = uri.to_owned();
move || {
let result = match std::fs::read(&path) {
Ok(bytes) => {
#[cfg(feature = "file")]
let mime = mime_guess2::from_path(&path)
.first_raw()
.map(|v| v.to_owned());
#[cfg(not(feature = "file"))]
let mime = None;
Ok(File {
bytes: bytes.into(),
mime,
})
}
Err(err) => Err(err.to_string()),
};
let repaint = {
let mut cache = cache.lock();
if let std::collections::hash_map::Entry::Occupied(mut entry) = cache.entry(uri.clone()) {
let entry = entry.get_mut();
*entry = Poll::Ready(result);
log::trace!("Finished loading {uri:?}");
true
} else {
log::trace!("Canceled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading.");
false
}
};
// We may not lock Context while the cache lock is held (see ImageLoader::load
// for details).
if repaint {
ctx.request_repaint();
}
}
})
.expect("failed to spawn thread");
Ok(BytesPoll::Pending { size: None })
}
}
fn forget(&self, uri: &str) {
let _ = self.cache.lock().remove(uri);
}
fn forget_all(&self) {
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|entry| match entry {
Poll::Ready(Ok(file)) => {
file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len())
}
Poll::Ready(Err(err)) => err.len(),
_ => 0,
})
.sum()
}
fn has_pending(&self) -> bool {
self.cache.lock().values().any(|entry| entry.is_pending())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_convert_uri_to_path() {
let mut checks: Vec<(&str, Result<PathBuf, egui::load::LoadError>, &str)> = vec![
(
"http://host/path/to/image.jpg",
Err(egui::load::LoadError::NotSupported),
"Schemas other than file are rejected.",
),
(
"https://host/path/to/image.jpg",
Err(egui::load::LoadError::NotSupported),
"Schemas other than file are rejected.",
),
(
"ftp://host/path/to/image.jpg",
Err(egui::load::LoadError::NotSupported),
"Schemas other than file are rejected.",
),
];
if cfg!(target_os = "windows") {
let mut windows_checks = vec![
(
"file:///path/to/image.jpg",
Ok(PathBuf::from("path\\to\\image.jpg")),
"file uris with no hosts and no drive letter are turned into bare paths on windows.",
),
(
"file:///c:/path/to/image.jpg",
Ok(PathBuf::from("c:\\path\\to\\image.jpg")),
"file uris with no hosts and drive letters are turned into absolute paths on windows.",
),
(
"file://host/share/path/to/image.jpg",
Ok(PathBuf::from("\\\\host\\share\\path\\to\\image.jpg")),
"file uris with a host are turned into UNC paths with leading backslashes on windows.",
),
];
checks.append(&mut windows_checks);
} else {
let mut more_checks = vec![(
"file://path/to/image.jpg",
Ok(PathBuf::from("path/to/image.jpg")),
"file uris are turned into bare paths.",
)];
checks.append(&mut more_checks);
}
for (uri_s, path, reason) in checks {
assert_eq!(convert_uri_to_path(uri_s), path, "{reason}");
}
}
}