##// END OF EJS Templates
fix png/jpeg b64-encoding check
MinRK -
Show More
@@ -1,222 +1,224 b''
1 1 """Utilities to manipulate JSON objects.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2010-2011 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING.txt, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13 # stdlib
14 14 import math
15 15 import re
16 16 import types
17 17 from datetime import datetime
18 18
19 19 try:
20 20 # base64.encodestring is deprecated in Python 3.x
21 21 from base64 import encodebytes
22 22 except ImportError:
23 23 # Python 2.x
24 24 from base64 import encodestring as encodebytes
25 25
26 26 from IPython.utils import py3compat
27 27 from IPython.utils.encoding import DEFAULT_ENCODING
28 28 next_attr_name = '__next__' if py3compat.PY3 else 'next'
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Globals and constants
32 32 #-----------------------------------------------------------------------------
33 33
34 34 # timestamp formats
35 35 ISO8601="%Y-%m-%dT%H:%M:%S.%f"
36 36 ISO8601_PAT=re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+$")
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Classes and functions
40 40 #-----------------------------------------------------------------------------
41 41
42 42 def rekey(dikt):
43 43 """Rekey a dict that has been forced to use str keys where there should be
44 44 ints by json."""
45 45 for k in dikt.iterkeys():
46 46 if isinstance(k, basestring):
47 47 ik=fk=None
48 48 try:
49 49 ik = int(k)
50 50 except ValueError:
51 51 try:
52 52 fk = float(k)
53 53 except ValueError:
54 54 continue
55 55 if ik is not None:
56 56 nk = ik
57 57 else:
58 58 nk = fk
59 59 if nk in dikt:
60 60 raise KeyError("already have key %r"%nk)
61 61 dikt[nk] = dikt.pop(k)
62 62 return dikt
63 63
64 64
65 65 def extract_dates(obj):
66 66 """extract ISO8601 dates from unpacked JSON"""
67 67 if isinstance(obj, dict):
68 68 obj = dict(obj) # don't clobber
69 69 for k,v in obj.iteritems():
70 70 obj[k] = extract_dates(v)
71 71 elif isinstance(obj, (list, tuple)):
72 72 obj = [ extract_dates(o) for o in obj ]
73 73 elif isinstance(obj, basestring):
74 74 if ISO8601_PAT.match(obj):
75 75 obj = datetime.strptime(obj, ISO8601)
76 76 return obj
77 77
78 78 def squash_dates(obj):
79 79 """squash datetime objects into ISO8601 strings"""
80 80 if isinstance(obj, dict):
81 81 obj = dict(obj) # don't clobber
82 82 for k,v in obj.iteritems():
83 83 obj[k] = squash_dates(v)
84 84 elif isinstance(obj, (list, tuple)):
85 85 obj = [ squash_dates(o) for o in obj ]
86 86 elif isinstance(obj, datetime):
87 87 obj = obj.strftime(ISO8601)
88 88 return obj
89 89
90 90 def date_default(obj):
91 91 """default function for packing datetime objects in JSON."""
92 92 if isinstance(obj, datetime):
93 93 return obj.strftime(ISO8601)
94 94 else:
95 95 raise TypeError("%r is not JSON serializable"%obj)
96 96
97 97
98 98 # constants for identifying png/jpeg data
99 99 PNG = b'\x89PNG\r\n\x1a\n'
100 PNG64 = encodebytes(PNG)
100 # front of PNG base64-encoded
101 PNG64 = b'iVBORw0KG'
101 102 JPEG = b'\xff\xd8'
102 JPEG64 = encodebytes(JPEG)
103 # front of JPEG base64-encoded
104 JPEG64 = b'/9'
103 105
104 106 def encode_images(format_dict):
105 107 """b64-encodes images in a displaypub format dict
106 108
107 109 Perhaps this should be handled in json_clean itself?
108 110
109 111 Parameters
110 112 ----------
111 113
112 114 format_dict : dict
113 115 A dictionary of display data keyed by mime-type
114 116
115 117 Returns
116 118 -------
117 119
118 120 format_dict : dict
119 121 A copy of the same dictionary,
120 122 but binary image data ('image/png' or 'image/jpeg')
121 123 is base64-encoded.
122 124
123 125 """
124 126 encoded = format_dict.copy()
125 127
126 128 pngdata = format_dict.get('image/png')
127 129 if isinstance(pngdata, bytes):
128 130 # make sure we don't double-encode
129 if pngdata[:13] != PNG64:
131 if not pngdata.startswith(PNG64):
130 132 pngdata = encodebytes(pngdata)
131 133 encoded['image/png'] = pngdata.decode('ascii')
132 134
133 135 jpegdata = format_dict.get('image/jpeg')
134 136 if isinstance(jpegdata, bytes):
135 137 # make sure we don't double-encode
136 if jpegdata[:5] != JPEG64:
138 if not jpegdata.startswith(JPEG64):
137 139 jpegdata = encodebytes(jpegdata)
138 140 encoded['image/jpeg'] = jpegdata.decode('ascii')
139 141
140 142 return encoded
141 143
142 144
143 145 def json_clean(obj):
144 146 """Clean an object to ensure it's safe to encode in JSON.
145 147
146 148 Atomic, immutable objects are returned unmodified. Sets and tuples are
147 149 converted to lists, lists are copied and dicts are also copied.
148 150
149 151 Note: dicts whose keys could cause collisions upon encoding (such as a dict
150 152 with both the number 1 and the string '1' as keys) will cause a ValueError
151 153 to be raised.
152 154
153 155 Parameters
154 156 ----------
155 157 obj : any python object
156 158
157 159 Returns
158 160 -------
159 161 out : object
160 162
161 163 A version of the input which will not cause an encoding error when
162 164 encoded as JSON. Note that this function does not *encode* its inputs,
163 165 it simply sanitizes it so that there will be no encoding errors later.
164 166
165 167 Examples
166 168 --------
167 169 >>> json_clean(4)
168 170 4
169 171 >>> json_clean(range(10))
170 172 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
171 173 >>> sorted(json_clean(dict(x=1, y=2)).items())
172 174 [('x', 1), ('y', 2)]
173 175 >>> sorted(json_clean(dict(x=1, y=2, z=[1,2,3])).items())
174 176 [('x', 1), ('y', 2), ('z', [1, 2, 3])]
175 177 >>> json_clean(True)
176 178 True
177 179 """
178 180 # types that are 'atomic' and ok in json as-is. bool doesn't need to be
179 181 # listed explicitly because bools pass as int instances
180 182 atomic_ok = (unicode, int, types.NoneType)
181 183
182 184 # containers that we need to convert into lists
183 185 container_to_list = (tuple, set, types.GeneratorType)
184 186
185 187 if isinstance(obj, float):
186 188 # cast out-of-range floats to their reprs
187 189 if math.isnan(obj) or math.isinf(obj):
188 190 return repr(obj)
189 191 return obj
190 192
191 193 if isinstance(obj, atomic_ok):
192 194 return obj
193 195
194 196 if isinstance(obj, bytes):
195 197 return obj.decode(DEFAULT_ENCODING, 'replace')
196 198
197 199 if isinstance(obj, container_to_list) or (
198 200 hasattr(obj, '__iter__') and hasattr(obj, next_attr_name)):
199 201 obj = list(obj)
200 202
201 203 if isinstance(obj, list):
202 204 return [json_clean(x) for x in obj]
203 205
204 206 if isinstance(obj, dict):
205 207 # First, validate that the dict won't lose data in conversion due to
206 208 # key collisions after stringification. This can happen with keys like
207 209 # True and 'true' or 1 and '1', which collide in JSON.
208 210 nkeys = len(obj)
209 211 nkeys_collapsed = len(set(map(str, obj)))
210 212 if nkeys != nkeys_collapsed:
211 213 raise ValueError('dict can not be safely converted to JSON: '
212 214 'key collision would lead to dropped values')
213 215 # If all OK, proceed by making the new dict that will be json-safe
214 216 out = {}
215 217 for k,v in obj.iteritems():
216 218 out[str(k)] = json_clean(v)
217 219 return out
218 220
219 221 # If we get here, we don't know how to handle the object, so we just get
220 222 # its repr and return that. This will catch lambdas, open sockets, class
221 223 # objects, and any other complicated contraption that json can't encode
222 224 return repr(obj)
General Comments 0
You need to be logged in to leave comments. Login now