Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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/. */
5 package org.mozilla.gecko.favicons.decoders;
7 import android.graphics.Bitmap;
8 import android.util.Base64;
9 import android.util.Log;
11 import org.mozilla.gecko.gfx.BitmapUtils;
13 import java.util.Iterator;
14 import java.util.NoSuchElementException;
16 /**
17 * Class providing static utility methods for decoding favicons.
18 */
19 public class FaviconDecoder {
20 private static final String LOG_TAG = "GeckoFaviconDecoder";
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});
30 public byte[] value;
32 private ImageMagicNumbers(byte[] value) {
33 this.value = value;
34 }
35 }
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 }
51 return false;
52 }
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 }
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 }
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;
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 }
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;
107 return result;
108 }
110 // If it's not decodable by Android, it might be an ICO. Let's try.
111 ICODecoder decoder = new ICODecoder(buffer, offset, length);
113 result = decoder.decode();
115 if (result == null) {
116 return null;
117 }
119 return result;
120 }
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 }
128 if (!uri.startsWith("data:image/")) {
129 Log.w(LOG_TAG, "Can't decode non-image data: URI.");
130 return null;
131 }
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 }
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 }
150 public static LoadFaviconResult decodeFavicon(byte[] buffer) {
151 return decodeFavicon(buffer, 0, buffer.length);
152 }
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 }
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 }
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 }
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 }
195 return bitmap;
196 }
198 /**
199 * Iterator to hold a single bitmap.
200 */
201 static class SingleBitmapIterator implements Iterator<Bitmap> {
202 private Bitmap bitmap;
204 public SingleBitmapIterator(Bitmap b) {
205 bitmap = b;
206 }
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 }
219 @Override
220 public boolean hasNext() {
221 return bitmap != null;
222 }
224 @Override
225 public Bitmap next() {
226 if (bitmap == null) {
227 throw new NoSuchElementException("Element already returned from SingleBitmapIterator.");
228 }
230 Bitmap ret = bitmap;
231 bitmap = null;
232 return ret;
233 }
235 @Override
236 public void remove() {
237 throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator.");
238 }
239 }
240 }