|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.favicons.decoders; |
|
6 |
|
7 import android.graphics.Bitmap; |
|
8 import android.util.Base64; |
|
9 import android.util.Log; |
|
10 |
|
11 import org.mozilla.gecko.gfx.BitmapUtils; |
|
12 |
|
13 import java.util.Iterator; |
|
14 import java.util.NoSuchElementException; |
|
15 |
|
16 /** |
|
17 * Class providing static utility methods for decoding favicons. |
|
18 */ |
|
19 public class FaviconDecoder { |
|
20 private static final String LOG_TAG = "GeckoFaviconDecoder"; |
|
21 |
|
22 static enum ImageMagicNumbers { |
|
23 // It is irritating that Java bytes are signed... |
|
24 PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}), |
|
25 GIF(new byte[] {0x47, 0x49, 0x46, 0x38}), |
|
26 JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}), |
|
27 BMP(new byte[] {0x42, 0x4d}), |
|
28 WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a}); |
|
29 |
|
30 public byte[] value; |
|
31 |
|
32 private ImageMagicNumbers(byte[] value) { |
|
33 this.value = value; |
|
34 } |
|
35 } |
|
36 |
|
37 /** |
|
38 * Check for image format magic numbers of formats supported by Android. |
|
39 * @param buffer Byte buffer to check for magic numbers |
|
40 * @param offset Offset at which to look for magic numbers. |
|
41 * @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence |
|
42 * starting with the magic numbers thereof). false otherwise. |
|
43 */ |
|
44 private static boolean isDecodableByAndroid(byte[] buffer, int offset) { |
|
45 for (ImageMagicNumbers m : ImageMagicNumbers.values()) { |
|
46 if (bufferStartsWith(buffer, m.value, offset)) { |
|
47 return true; |
|
48 } |
|
49 } |
|
50 |
|
51 return false; |
|
52 } |
|
53 |
|
54 /** |
|
55 * Utility function to check for the existence of a test byte sequence at a given offset in a |
|
56 * buffer. |
|
57 * |
|
58 * @param buffer Byte buffer to search. |
|
59 * @param test Byte sequence to search for. |
|
60 * @param bufferOffset Index in input buffer to expect test sequence. |
|
61 * @return true if buffer contains the byte sequence given in test at offset bufferOffset, false |
|
62 * otherwise. |
|
63 */ |
|
64 static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) { |
|
65 if (buffer.length < test.length) { |
|
66 return false; |
|
67 } |
|
68 |
|
69 for (int i = 0; i < test.length; ++i) { |
|
70 if (buffer[bufferOffset + i] != test[i]) { |
|
71 return false; |
|
72 } |
|
73 } |
|
74 return true; |
|
75 } |
|
76 |
|
77 /** |
|
78 * Decode the favicon present in the region of the provided byte[] starting at offset and |
|
79 * proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the |
|
80 * given range does not contain a bitmap we know how to decode. |
|
81 * |
|
82 * @param buffer Byte array containing the favicon to decode. |
|
83 * @param offset The index of the first byte in the array of the region of interest. |
|
84 * @param length The length of the region in the array to decode. |
|
85 * @return The decoded version of the bitmap in the described region, or null if none can be |
|
86 * decoded. |
|
87 */ |
|
88 public static LoadFaviconResult decodeFavicon(byte[] buffer, int offset, int length) { |
|
89 LoadFaviconResult result; |
|
90 if (isDecodableByAndroid(buffer, offset)) { |
|
91 result = new LoadFaviconResult(); |
|
92 result.offset = offset; |
|
93 result.length = length; |
|
94 result.isICO = false; |
|
95 |
|
96 Bitmap decodedImage = BitmapUtils.decodeByteArray(buffer, offset, length); |
|
97 if (decodedImage == null) { |
|
98 // What we got wasn't decodable after all. Probably corrupted image, or we got a muffled OOM. |
|
99 return null; |
|
100 } |
|
101 |
|
102 // We assume here that decodeByteArray doesn't hold on to the entire supplied |
|
103 // buffer -- worst case, each of our buffers will be twice the necessary size. |
|
104 result.bitmapsDecoded = new SingleBitmapIterator(decodedImage); |
|
105 result.faviconBytes = buffer; |
|
106 |
|
107 return result; |
|
108 } |
|
109 |
|
110 // If it's not decodable by Android, it might be an ICO. Let's try. |
|
111 ICODecoder decoder = new ICODecoder(buffer, offset, length); |
|
112 |
|
113 result = decoder.decode(); |
|
114 |
|
115 if (result == null) { |
|
116 return null; |
|
117 } |
|
118 |
|
119 return result; |
|
120 } |
|
121 |
|
122 public static LoadFaviconResult decodeDataURI(String uri) { |
|
123 if (uri == null) { |
|
124 Log.w(LOG_TAG, "Can't decode null data: URI."); |
|
125 return null; |
|
126 } |
|
127 |
|
128 if (!uri.startsWith("data:image/")) { |
|
129 Log.w(LOG_TAG, "Can't decode non-image data: URI."); |
|
130 return null; |
|
131 } |
|
132 |
|
133 // Otherwise, let's attack this blindly. Strictly we should be parsing. |
|
134 int offset = uri.indexOf(',') + 1; |
|
135 if (offset == 0) { |
|
136 Log.w(LOG_TAG, "No ',' in data: URI; malformed?"); |
|
137 return null; |
|
138 } |
|
139 |
|
140 try { |
|
141 String base64 = uri.substring(offset); |
|
142 byte[] raw = Base64.decode(base64, Base64.DEFAULT); |
|
143 return decodeFavicon(raw); |
|
144 } catch (Exception e) { |
|
145 Log.w(LOG_TAG, "Couldn't decode data: URI.", e); |
|
146 return null; |
|
147 } |
|
148 } |
|
149 |
|
150 public static LoadFaviconResult decodeFavicon(byte[] buffer) { |
|
151 return decodeFavicon(buffer, 0, buffer.length); |
|
152 } |
|
153 |
|
154 /** |
|
155 * Returns the smallest bitmap in the icon represented by the provided |
|
156 * data: URI that's larger than the desired width, or the largest if |
|
157 * there is no larger icon. |
|
158 * |
|
159 * Returns null if no bitmap could be extracted. |
|
160 * |
|
161 * Bug 961600: we shouldn't be doing all of this work. The favicon cache |
|
162 * should be used, and will give us the right size icon. |
|
163 */ |
|
164 public static Bitmap getMostSuitableBitmapFromDataURI(String iconURI, int desiredWidth) { |
|
165 LoadFaviconResult result = FaviconDecoder.decodeDataURI(iconURI); |
|
166 if (result == null) { |
|
167 // Nothing we can do. |
|
168 Log.w(LOG_TAG, "Unable to decode icon URI."); |
|
169 return null; |
|
170 } |
|
171 |
|
172 final Iterator<Bitmap> bitmaps = result.getBitmaps(); |
|
173 if (!bitmaps.hasNext()) { |
|
174 Log.w(LOG_TAG, "No bitmaps in decoded icon."); |
|
175 return null; |
|
176 } |
|
177 |
|
178 Bitmap bitmap = bitmaps.next(); |
|
179 if (!bitmaps.hasNext()) { |
|
180 // We're done! There was only one, so this is as big as it gets. |
|
181 return bitmap; |
|
182 } |
|
183 |
|
184 // Find a bitmap of the most suitable size. |
|
185 int currentWidth = bitmap.getWidth(); |
|
186 while ((currentWidth < desiredWidth) && |
|
187 bitmaps.hasNext()) { |
|
188 final Bitmap b = bitmaps.next(); |
|
189 if (b.getWidth() > currentWidth) { |
|
190 currentWidth = b.getWidth(); |
|
191 bitmap = b; |
|
192 } |
|
193 } |
|
194 |
|
195 return bitmap; |
|
196 } |
|
197 |
|
198 /** |
|
199 * Iterator to hold a single bitmap. |
|
200 */ |
|
201 static class SingleBitmapIterator implements Iterator<Bitmap> { |
|
202 private Bitmap bitmap; |
|
203 |
|
204 public SingleBitmapIterator(Bitmap b) { |
|
205 bitmap = b; |
|
206 } |
|
207 |
|
208 /** |
|
209 * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure |
|
210 * places where the runtime type of the Iterator under consideration is known and |
|
211 * destruction of it is discouraged. |
|
212 * |
|
213 * @return The bitmap carried by this SingleBitmapIterator. |
|
214 */ |
|
215 public Bitmap peek() { |
|
216 return bitmap; |
|
217 } |
|
218 |
|
219 @Override |
|
220 public boolean hasNext() { |
|
221 return bitmap != null; |
|
222 } |
|
223 |
|
224 @Override |
|
225 public Bitmap next() { |
|
226 if (bitmap == null) { |
|
227 throw new NoSuchElementException("Element already returned from SingleBitmapIterator."); |
|
228 } |
|
229 |
|
230 Bitmap ret = bitmap; |
|
231 bitmap = null; |
|
232 return ret; |
|
233 } |
|
234 |
|
235 @Override |
|
236 public void remove() { |
|
237 throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator."); |
|
238 } |
|
239 } |
|
240 } |