-
Notifications
You must be signed in to change notification settings - Fork 0
/
AsyncImage.py
379 lines (323 loc) · 11.8 KB
/
AsyncImage.py
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
from FileReader import TempFileReader
from Tkinter import *
import grailutil
import os
import string
TkPhotoImage = PhotoImage
class ImageTempFileReader(TempFileReader):
def __init__(self, context, api, image):
self.image = image
self.url = self.image.url
TempFileReader.__init__(self, context, api)
def handle_meta(self, errcode, errmsg, headers):
TempFileReader.handle_meta(self, errcode, errmsg, headers)
if errcode == 200:
try:
ctype = headers['content-type']
except KeyError:
return # Hope for the best
if self.image_filters.has_key(ctype) and not isPILAllowed():
self.set_pipeline(self.image_filters[ctype])
# List of image type filters
image_filters = {
'image/gif': '',
'image/jpeg': 'djpeg -gif',
'image/x-xbitmap':
'xbmtopbm | ppmtogif -transparent "#FFFFFF" 2>/dev/null',
'image/tiff':
"""(T=${TMPDIR-/usr/tmp}/@$$.tiff; cat >$T;
tifftopnm $T 2>/dev/null; rm -f $T)""",
'image/png':
# This requires pngtopnm which isn't standard netpbm yet
'pngtopnm | ppmtogif -transparent "#FFFFFF" 2>/dev/null',
}
def handle_done(self):
self.image.set_file(self.getfilename())
self.cleanup()
def handle_error(self, errcode, errmsg, headers):
if errcode == 401:
if headers.has_key('www-authenticate'):
cred_headers = {}
for k in headers.keys():
cred_headers[string.lower(k)] = headers[k]
cred_headers['request-uri'] = self.image.url
self.stop()
credentials = self.image.context.app.auth.request_credentials(
cred_headers)
if credentials.has_key('Authorization'):
for k,v in credentials.items():
self.image.headers[k] = v
# self.image.restart(self.image.url)
self.image.start_loading(self.image.context)
self.image.set_error(errcode, errmsg, headers)
self.cleanup()
def stop(self):
TempFileReader.stop(self)
if self.image:
self.image.reader = None
def cleanup(self):
self.image = None
import os
try:
os.unlink(self.getfilename())
except os.error:
pass
class BaseAsyncImage:
def setup(self, context, url, reload):
self.context = context
self.url = url
self.reader = None
self.loaded = 0
self.headers = {}
if reload:
self.reload = 1
else:
self.reload = 0
def load_synchronously(self, context=None):
if not self.loaded:
self.start_loading(context)
if self.reader:
self.reader.geteverything()
return self.loaded
def start_loading(self, context=None, reload=0):
# seems that the reload=1 when you click on an image that
# you had stopped loading
if context: self.context = context
if self.reader:
return
try:
api = self.context.app.open_url(self.url, 'GET', self.headers,
self.reload or reload)
except IOError, msg:
self.show_bad()
return
cached_file, content_type = api.tk_img_access()
if cached_file \
and ImageTempFileReader.image_filters.has_key(content_type) \
and ImageTempFileReader.image_filters[content_type] == '':
api.close()
self.set_file(cached_file)
else:
self.show_busy()
# even if the item is in the cache, use the ImageTempFile
# to handle the proper type coercion
self.reader = ImageTempFileReader(self.context, api, self)
def stop_loading(self):
if not self.reader:
return
self.reader.kill()
self.show_bad()
def set_file(self, filename):
self.blank()
self.do_color_magic()
try:
self['file'] = filename
except TclError:
self.show_bad()
else:
self.loaded = 1
def do_color_magic(self):
self.context.root.tk.setvar("TRANSPARENT_GIF_COLOR",
self.context.viewer.text["background"])
def set_error(self, errcode, errmsg, headers):
self.loaded = 0
if errcode in (301, 302) and headers.has_key('location'):
self.url = headers['location']
self.start_loading()
def is_reloading(self):
return self.reload and not self.loaded
def get_load_status(self):
if self.reader:
return 'loading'
else:
return 'idle'
def show_bad(self):
self.blank()
try:
self['file'] = grailutil.which(
os.path.join("icons", "sadsmiley.gif")) or ""
except TclError:
pass
def show_busy(self):
self.blank()
try:
self['file'] = grailutil.which(
os.path.join("icons", "image.gif")) or ""
except TclError:
pass
class TkAsyncImage(BaseAsyncImage, TkPhotoImage):
def __init__(self, context, url, reload=0, **kw):
apply(TkPhotoImage.__init__, (self,), kw)
self.setup(context, url, reload)
def get_cache_key(self):
return self.url, 0, 0
class PILAsyncImageSupport(BaseAsyncImage):
#
# We can't actually inherit from the PIL PhotoImage, so we'll be a mixin
# that really takes over. A new class will be created from this & the
# PIL PhotoImage which forms the actual implementation class iff PIL is
# both available and enabled.
#
__width = 0
__height = 0
def __init__(self, context, url, reload=0, width=None, height=None, **kw):
import ImageTk
self.setup(context, url, reload)
master = kw.get("master")
if master is None:
ImageTk.PhotoImage.__init__(self, "RGB", (width or 1, height or 1))
else:
ImageTk.PhotoImage.__init__(self, "RGB", (width or 1, height or 1),
master=kw.get("master"))
if not hasattr(self, 'image'):
# Steal a private variable from ImageTk
self.image = self._PhotoImage__photo
# Make sure these are integers
self.__width = width or 0
self.__height = height or 0
def blank(self):
self.image.blank()
def get_cache_key(self):
#
# Note that two different cache keys may be generated for an image
# depending on how they are specified. In particular, the keys
# (URL, 0, 0) and (URL, WIDTH, HEIGHT) may be generated for the same
# real image (not Image object) if WIDTH and HEIGHT are the default
# dimensions of the image and the image is specified both with and
# without size hints. This still generates no more than two distinct
# keys for otherwise identical image objects.
#
return self.url, self.__width, self.__height
def set_file(self, filename):
import Image
try:
im = Image.open(filename)
im.load() # force loading to catch IOError
except (IOError, ValueError):
# either of these may occur during decoding...
return self.show_bad()
if im.format == "XBM":
im = xbm_to_rgba(im)
real_size = im.size
# determine desired size:
if self.__width and not self.__height and self.__width != im.size[0]:
# scale horizontally
self.__height = int(1.0 * im.size[1] * self.__width / im.size[0])
elif self.__height and not self.__width \
and self.__height != im.size[1]:
# scale vertically
self.__width = int(1.0 * im.size[0] * self.__height / im.size[1])
else:
self.__width = self.__width or im.size[0]
self.__height = self.__height or im.size[1]
# transparency stuff
if im.mode == "RGBA" \
or (im.mode == "P" and im.info.has_key("transparency")):
r, g, b = self.context.viewer.text.winfo_rgb(
self.context.viewer.text["background"])
r = r / 256 # convert these to 8-bit versions
g = g / 256
b = b / 256
if im.mode == "P":
im = p_to_rgb(im, (r, g, b))
else:
im = rgba_to_rgb(im, (r, g, b))
#
if real_size != (self.__width, self.__height):
w, h = real_size
if w != self.__width or h != self.__height:
im = im.resize((self.__width, self.__height))
# This appears to be absolutely necessary, but I'm not sure why....
self._PhotoImage__size = im.size
self.blank()
self.paste(im)
w, h = im.size
self.image['width'] = w
self.image['height'] = h
def width(self):
return self.__width
def height(self):
return self.__height
def __setitem__(self, key, value):
if key == "file":
self.do_color_magic()
self.image[key] = value
def p_to_rgb(im, rgb):
"""Translate a P-mode image with transparency to an RGB image.
im
The transparent image.
rgb
The RGB-value to use for the transparent areas. This should be
a 3-tuple of integers, 8 bits for each band.
"""
import Image
new_im = Image.new("RGB", im.size, rgb)
point_mask = [0xff] * 256
point_mask[im.info['transparency']] = 0
new_im.paste(im, None, im.point(point_mask, '1'))
return new_im
def rgba_to_rgb(im, rgb):
"""Translate an RGBA-mode image to an RGB image.
im
The transparent image.
rgb
The RGB-value to use for the transparent areas. This should be
a 3-tuple of integers, 8 bits for each band.
"""
import Image
new_im = Image.new("RGB", im.size, rgb)
new_im.paste(im, None, im)
return new_im
def xbm_to_rgba(im):
"""Translate a XBM image to an RGBA image.
im
The XBM image.
"""
import Image
# invert & mask so we get transparency
mapping = [255] * 256
mapping[255] = 0
mask = im.point(mapping)
return Image.merge("RGBA", (mask, mask, mask, im))
def pil_installed():
# Determine if the Python Imaging Library is available.
#
# Note that "import Image" is not sufficient to test the availability of
# the image loading capability. Image can be imported without _imaging
# and still supports identification of file types. Grail requires _imaging
# to support image loading.
#
try:
import _imaging
import Image
import ImageTk
except ImportError:
return 0
# Now check the integration with Tk:
try:
ImageTk.PhotoImage(Image.new("L", (1, 1)))
except TclError:
return 0
return 1
_pil_allowed = None
def isPILAllowed():
"""Return true iff PIL should be used by the caller."""
global _pil_allowed
if _pil_allowed is None:
app = grailutil.get_grailapp()
_pil_allowed = (app.prefs.GetBoolean("browser", "enable-pil")
and pil_installed())
return _pil_allowed
def AsyncImage(context, url, reload=0, **kw):
# Check the enable-pil preference and replace this function
# with the appropriate implementation in the module namespace:
#
global AsyncImage
if isPILAllowed():
import ImageTk
class PILAsyncImage(PILAsyncImageSupport, ImageTk.PhotoImage):
pass
AsyncImage = PILAsyncImage
else:
AsyncImage = TkAsyncImage
return apply(AsyncImage, (context, url, reload), kw)