FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
pythonnative/src/pythonnative/images.py at main · andevsr/pythonnative · GitHub
andevsr
pythonnative
Repository navigation
Code
Pull requests
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
pythonnative
/
src
/
pythonnative
/
images.py
Copy path
More file actions
More file actions
Latest commit
History
History
History
196 lines (159 loc) · 6.31 KB
Breadcrumbs
pythonnative
/
src
/
pythonnative
/
images.py
Copy path
File metadata and controls
196 lines (159 loc) · 6.31 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
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
"""Shared image pipeline: async fetch with memory and disk caching.
Every platform's ``Image`` handler routes remote sources through this
module. The pipeline downloads on a daemon thread (never blocking the
UI), stores the raw bytes in a platform-appropriate disk cache keyed
by URL hash, deduplicates concurrent requests for the same URL, and
keeps a small in-memory LRU of recently fetched byte payloads so
scrolling back to an image doesn't touch the filesystem again.
Decoding stays platform-native (``BitmapFactory`` / ``UIImage`` /
``PhotoImage``): callbacks receive a *local file path*, which each
handler decodes with its platform's downsampling facilities.
Callbacks are delivered on the platform main thread via
[`call_on_main_thread`][pythonnative.runtime.call_on_main_thread], so
handlers can touch native views directly.
"""
from
__future__
import
annotations
import
hashlib
import
os
import
tempfile
import
threading
import
urllib
.
request
from
collections
import
OrderedDict
from
typing
import
Callable
,
Dict
,
List
,
Optional
,
Tuple
_DOWNLOAD_TIMEOUT_S
=
30.0
_MEMORY_CACHE_MAX_BYTES
=
16
*
1024
*
1024
def
_default_cache_dir
()
->
str
:
"""Resolve the on-disk cache directory for the current platform.
Android: the app's ``Context.getCacheDir()`` (purged by the OS
under storage pressure). iOS: ``~/Library/Caches`` (excluded from
backups). Desktop: a per-user directory under the system temp dir.
"""
try
:
from
.
utils
import
IS_ANDROID
if
IS_ANDROID
:
from
.
utils
import
get_android_context
base
=
str
(
get_android_context
().
getCacheDir
().
getAbsolutePath
())
return
os
.
path
.
join
(
base
,
"pn_images"
)
except
Exception
:
pass
home
=
os
.
path
.
expanduser
(
"~"
)
caches
=
os
.
path
.
join
(
home
,
"Library"
,
"Caches"
)
if
os
.
path
.
isdir
(
caches
):
# iOS / macOS
return
os
.
path
.
join
(
caches
,
"pn_images"
)
return
os
.
path
.
join
(
tempfile
.
gettempdir
(),
"pn_images"
)
class
_ByteLru
:
"""Tiny thread-safe LRU for raw image bytes."""
def
__init__
(
self
,
max_bytes
:
int
)
->
None
:
self
.
_max
=
max_bytes
self
.
_size
=
0
self
.
_items
:
"OrderedDict[str, bytes]"
=
OrderedDict
()
self
.
_lock
=
threading
.
Lock
()
def
get
(
self
,
key
:
str
)
->
Optional
[
bytes
]:
with
self
.
_lock
:
data
=
self
.
_items
.
get
(
key
)
if
data
is
not
None
:
self
.
_items
.
move_to_end
(
key
)
return
data
def
put
(
self
,
key
:
str
,
data
:
bytes
)
->
None
:
if
len
(
data
)
>
self
.
_max
:
return
with
self
.
_lock
:
old
=
self
.
_items
.
pop
(
key
,
None
)
if
old
is
not
None
:
self
.
_size
-=
len
(
old
)
self
.
_items
[
key
]
=
data
self
.
_size
+=
len
(
data
)
while
self
.
_size
>
self
.
_max
and
self
.
_items
:
_
,
evicted
=
self
.
_items
.
popitem
(
last
=
False
)
self
.
_size
-=
len
(
evicted
)
def
clear
(
self
)
->
None
:
with
self
.
_lock
:
self
.
_items
.
clear
()
self
.
_size
=
0
_memory_cache
=
_ByteLru
(
_MEMORY_CACHE_MAX_BYTES
)
_cache_dir
:
Optional
[
str
]
=
None
# URL -> callbacks waiting on an in-flight download.
_in_flight
:
Dict
[
str
,
List
[
Tuple
[
Callable
[[
str
],
None
],
Callable
[[
str
],
None
]]]]
=
{}
_in_flight_lock
=
threading
.
Lock
()
def
_get_cache_dir
()
->
str
:
global
_cache_dir
if
_cache_dir
is
None
:
_cache_dir
=
_default_cache_dir
()
os
.
makedirs
(
_cache_dir
,
exist_ok
=
True
)
return
_cache_dir
def
_cache_path
(
url
:
str
)
->
str
:
digest
=
hashlib
.
sha256
(
url
.
encode
(
"utf-8"
)).
hexdigest
()
ext
=
os
.
path
.
splitext
(
url
.
split
(
"?"
,
1
)[
0
])[
1
]
if
len
(
ext
)
>
8
or
"/"
in
ext
:
ext
=
""
return
os
.
path
.
join
(
_get_cache_dir
(),
digest
+
ext
)
def
_dispatch_main
(
fn
:
Callable
[[],
None
])
->
None
:
try
:
from
.
runtime
import
call_on_main_thread
call_on_main_thread
(
fn
)
except
Exception
:
try
:
fn
()
except
Exception
:
pass
def
fetch
(
url
:
str
,
on_ready
:
Callable
[[
str
],
None
],
on_error
:
Optional
[
Callable
[[
str
],
None
]]
=
None
,
)
->
None
:
"""Fetch ``url`` into the cache and deliver a local file path.
Cache hits (memory or disk) still deliver asynchronously-consistent
behavior but resolve without a network round trip. Concurrent
requests for the same URL share one download. Callbacks run on the
platform main thread.
Args:
url: The ``http(s)`` image URL.
on_ready: Called with the local file path once available.
on_error: Called with an error message if the download fails.
"""
path
=
_cache_path
(
url
)
if
_memory_cache
.
get
(
url
)
is
not
None
or
os
.
path
.
isfile
(
path
):
_dispatch_main
(
lambda
:
on_ready
(
path
))
return
err
=
on_error
or
(
lambda
_msg
:
None
)
with
_in_flight_lock
:
waiters
=
_in_flight
.
get
(
url
)
if
waiters
is
not
None
:
waiters
.
append
((
on_ready
,
err
))
return
_in_flight
[
url
]
=
[(
on_ready
,
err
)]
def
_worker
()
->
None
:
error_msg
:
Optional
[
str
]
=
None
try
:
req
=
urllib
.
request
.
Request
(
url
,
headers
=
{
"User-Agent"
:
"PythonNative"
})
with
urllib
.
request
.
urlopen
(
req
,
timeout
=
_DOWNLOAD_TIMEOUT_S
)
as
resp
:
data
=
resp
.
read
()
_memory_cache
.
put
(
url
,
data
)
tmp
=
path
+
".part"
with
open
(
tmp
,
"wb"
)
as
fh
:
fh
.
write
(
data
)
os
.
replace
(
tmp
,
path
)
except
Exception
as
exc
:
error_msg
=
str
(
exc
)
with
_in_flight_lock
:
callbacks
=
_in_flight
.
pop
(
url
, [])
def
_notify
(
ready_cb
:
Callable
[[
str
],
None
],
error_cb
:
Callable
[[
str
],
None
],
msg
:
Optional
[
str
])
->
None
:
if
msg
is
None
:
_dispatch_main
(
lambda
:
ready_cb
(
path
))
else
:
_dispatch_main
(
lambda
:
error_cb
(
msg
))
for
ready_cb
,
error_cb
in
callbacks
:
_notify
(
ready_cb
,
error_cb
,
error_msg
)
threading
.
Thread
(
target
=
_worker
,
name
=
f"pn-image-
{
path
[
-
12
:]
}
"
,
daemon
=
True
).
start
()
def
clear_cache
()
->
None
:
"""Empty the memory cache and delete all cached image files."""
_memory_cache
.
clear
()
try
:
directory
=
_get_cache_dir
()
for
name
in
os
.
listdir
(
directory
):
try
:
os
.
remove
(
os
.
path
.
join
(
directory
,
name
))
except
OSError
:
pass
except
OSError
:
pass
Back
|
FazBrowse Home
|
New Git URL