1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/favicons/decoders/FaviconDecoder.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,240 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.favicons.decoders; 1.9 + 1.10 +import android.graphics.Bitmap; 1.11 +import android.util.Base64; 1.12 +import android.util.Log; 1.13 + 1.14 +import org.mozilla.gecko.gfx.BitmapUtils; 1.15 + 1.16 +import java.util.Iterator; 1.17 +import java.util.NoSuchElementException; 1.18 + 1.19 +/** 1.20 + * Class providing static utility methods for decoding favicons. 1.21 + */ 1.22 +public class FaviconDecoder { 1.23 + private static final String LOG_TAG = "GeckoFaviconDecoder"; 1.24 + 1.25 + static enum ImageMagicNumbers { 1.26 + // It is irritating that Java bytes are signed... 1.27 + PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}), 1.28 + GIF(new byte[] {0x47, 0x49, 0x46, 0x38}), 1.29 + JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}), 1.30 + BMP(new byte[] {0x42, 0x4d}), 1.31 + WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a}); 1.32 + 1.33 + public byte[] value; 1.34 + 1.35 + private ImageMagicNumbers(byte[] value) { 1.36 + this.value = value; 1.37 + } 1.38 + } 1.39 + 1.40 + /** 1.41 + * Check for image format magic numbers of formats supported by Android. 1.42 + * @param buffer Byte buffer to check for magic numbers 1.43 + * @param offset Offset at which to look for magic numbers. 1.44 + * @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence 1.45 + * starting with the magic numbers thereof). false otherwise. 1.46 + */ 1.47 + private static boolean isDecodableByAndroid(byte[] buffer, int offset) { 1.48 + for (ImageMagicNumbers m : ImageMagicNumbers.values()) { 1.49 + if (bufferStartsWith(buffer, m.value, offset)) { 1.50 + return true; 1.51 + } 1.52 + } 1.53 + 1.54 + return false; 1.55 + } 1.56 + 1.57 + /** 1.58 + * Utility function to check for the existence of a test byte sequence at a given offset in a 1.59 + * buffer. 1.60 + * 1.61 + * @param buffer Byte buffer to search. 1.62 + * @param test Byte sequence to search for. 1.63 + * @param bufferOffset Index in input buffer to expect test sequence. 1.64 + * @return true if buffer contains the byte sequence given in test at offset bufferOffset, false 1.65 + * otherwise. 1.66 + */ 1.67 + static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) { 1.68 + if (buffer.length < test.length) { 1.69 + return false; 1.70 + } 1.71 + 1.72 + for (int i = 0; i < test.length; ++i) { 1.73 + if (buffer[bufferOffset + i] != test[i]) { 1.74 + return false; 1.75 + } 1.76 + } 1.77 + return true; 1.78 + } 1.79 + 1.80 + /** 1.81 + * Decode the favicon present in the region of the provided byte[] starting at offset and 1.82 + * proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the 1.83 + * given range does not contain a bitmap we know how to decode. 1.84 + * 1.85 + * @param buffer Byte array containing the favicon to decode. 1.86 + * @param offset The index of the first byte in the array of the region of interest. 1.87 + * @param length The length of the region in the array to decode. 1.88 + * @return The decoded version of the bitmap in the described region, or null if none can be 1.89 + * decoded. 1.90 + */ 1.91 + public static LoadFaviconResult decodeFavicon(byte[] buffer, int offset, int length) { 1.92 + LoadFaviconResult result; 1.93 + if (isDecodableByAndroid(buffer, offset)) { 1.94 + result = new LoadFaviconResult(); 1.95 + result.offset = offset; 1.96 + result.length = length; 1.97 + result.isICO = false; 1.98 + 1.99 + Bitmap decodedImage = BitmapUtils.decodeByteArray(buffer, offset, length); 1.100 + if (decodedImage == null) { 1.101 + // What we got wasn't decodable after all. Probably corrupted image, or we got a muffled OOM. 1.102 + return null; 1.103 + } 1.104 + 1.105 + // We assume here that decodeByteArray doesn't hold on to the entire supplied 1.106 + // buffer -- worst case, each of our buffers will be twice the necessary size. 1.107 + result.bitmapsDecoded = new SingleBitmapIterator(decodedImage); 1.108 + result.faviconBytes = buffer; 1.109 + 1.110 + return result; 1.111 + } 1.112 + 1.113 + // If it's not decodable by Android, it might be an ICO. Let's try. 1.114 + ICODecoder decoder = new ICODecoder(buffer, offset, length); 1.115 + 1.116 + result = decoder.decode(); 1.117 + 1.118 + if (result == null) { 1.119 + return null; 1.120 + } 1.121 + 1.122 + return result; 1.123 + } 1.124 + 1.125 + public static LoadFaviconResult decodeDataURI(String uri) { 1.126 + if (uri == null) { 1.127 + Log.w(LOG_TAG, "Can't decode null data: URI."); 1.128 + return null; 1.129 + } 1.130 + 1.131 + if (!uri.startsWith("data:image/")) { 1.132 + Log.w(LOG_TAG, "Can't decode non-image data: URI."); 1.133 + return null; 1.134 + } 1.135 + 1.136 + // Otherwise, let's attack this blindly. Strictly we should be parsing. 1.137 + int offset = uri.indexOf(',') + 1; 1.138 + if (offset == 0) { 1.139 + Log.w(LOG_TAG, "No ',' in data: URI; malformed?"); 1.140 + return null; 1.141 + } 1.142 + 1.143 + try { 1.144 + String base64 = uri.substring(offset); 1.145 + byte[] raw = Base64.decode(base64, Base64.DEFAULT); 1.146 + return decodeFavicon(raw); 1.147 + } catch (Exception e) { 1.148 + Log.w(LOG_TAG, "Couldn't decode data: URI.", e); 1.149 + return null; 1.150 + } 1.151 + } 1.152 + 1.153 + public static LoadFaviconResult decodeFavicon(byte[] buffer) { 1.154 + return decodeFavicon(buffer, 0, buffer.length); 1.155 + } 1.156 + 1.157 + /** 1.158 + * Returns the smallest bitmap in the icon represented by the provided 1.159 + * data: URI that's larger than the desired width, or the largest if 1.160 + * there is no larger icon. 1.161 + * 1.162 + * Returns null if no bitmap could be extracted. 1.163 + * 1.164 + * Bug 961600: we shouldn't be doing all of this work. The favicon cache 1.165 + * should be used, and will give us the right size icon. 1.166 + */ 1.167 + public static Bitmap getMostSuitableBitmapFromDataURI(String iconURI, int desiredWidth) { 1.168 + LoadFaviconResult result = FaviconDecoder.decodeDataURI(iconURI); 1.169 + if (result == null) { 1.170 + // Nothing we can do. 1.171 + Log.w(LOG_TAG, "Unable to decode icon URI."); 1.172 + return null; 1.173 + } 1.174 + 1.175 + final Iterator<Bitmap> bitmaps = result.getBitmaps(); 1.176 + if (!bitmaps.hasNext()) { 1.177 + Log.w(LOG_TAG, "No bitmaps in decoded icon."); 1.178 + return null; 1.179 + } 1.180 + 1.181 + Bitmap bitmap = bitmaps.next(); 1.182 + if (!bitmaps.hasNext()) { 1.183 + // We're done! There was only one, so this is as big as it gets. 1.184 + return bitmap; 1.185 + } 1.186 + 1.187 + // Find a bitmap of the most suitable size. 1.188 + int currentWidth = bitmap.getWidth(); 1.189 + while ((currentWidth < desiredWidth) && 1.190 + bitmaps.hasNext()) { 1.191 + final Bitmap b = bitmaps.next(); 1.192 + if (b.getWidth() > currentWidth) { 1.193 + currentWidth = b.getWidth(); 1.194 + bitmap = b; 1.195 + } 1.196 + } 1.197 + 1.198 + return bitmap; 1.199 + } 1.200 + 1.201 + /** 1.202 + * Iterator to hold a single bitmap. 1.203 + */ 1.204 + static class SingleBitmapIterator implements Iterator<Bitmap> { 1.205 + private Bitmap bitmap; 1.206 + 1.207 + public SingleBitmapIterator(Bitmap b) { 1.208 + bitmap = b; 1.209 + } 1.210 + 1.211 + /** 1.212 + * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure 1.213 + * places where the runtime type of the Iterator under consideration is known and 1.214 + * destruction of it is discouraged. 1.215 + * 1.216 + * @return The bitmap carried by this SingleBitmapIterator. 1.217 + */ 1.218 + public Bitmap peek() { 1.219 + return bitmap; 1.220 + } 1.221 + 1.222 + @Override 1.223 + public boolean hasNext() { 1.224 + return bitmap != null; 1.225 + } 1.226 + 1.227 + @Override 1.228 + public Bitmap next() { 1.229 + if (bitmap == null) { 1.230 + throw new NoSuchElementException("Element already returned from SingleBitmapIterator."); 1.231 + } 1.232 + 1.233 + Bitmap ret = bitmap; 1.234 + bitmap = null; 1.235 + return ret; 1.236 + } 1.237 + 1.238 + @Override 1.239 + public void remove() { 1.240 + throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator."); 1.241 + } 1.242 + } 1.243 +}