michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.favicons.decoders; michael@0: michael@0: import android.graphics.Bitmap; michael@0: import android.util.Base64; michael@0: import android.util.Log; michael@0: michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: michael@0: import java.util.Iterator; michael@0: import java.util.NoSuchElementException; michael@0: michael@0: /** michael@0: * Class providing static utility methods for decoding favicons. michael@0: */ michael@0: public class FaviconDecoder { michael@0: private static final String LOG_TAG = "GeckoFaviconDecoder"; michael@0: michael@0: static enum ImageMagicNumbers { michael@0: // It is irritating that Java bytes are signed... michael@0: PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}), michael@0: GIF(new byte[] {0x47, 0x49, 0x46, 0x38}), michael@0: JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}), michael@0: BMP(new byte[] {0x42, 0x4d}), michael@0: WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a}); michael@0: michael@0: public byte[] value; michael@0: michael@0: private ImageMagicNumbers(byte[] value) { michael@0: this.value = value; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Check for image format magic numbers of formats supported by Android. michael@0: * @param buffer Byte buffer to check for magic numbers michael@0: * @param offset Offset at which to look for magic numbers. michael@0: * @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence michael@0: * starting with the magic numbers thereof). false otherwise. michael@0: */ michael@0: private static boolean isDecodableByAndroid(byte[] buffer, int offset) { michael@0: for (ImageMagicNumbers m : ImageMagicNumbers.values()) { michael@0: if (bufferStartsWith(buffer, m.value, offset)) { michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Utility function to check for the existence of a test byte sequence at a given offset in a michael@0: * buffer. michael@0: * michael@0: * @param buffer Byte buffer to search. michael@0: * @param test Byte sequence to search for. michael@0: * @param bufferOffset Index in input buffer to expect test sequence. michael@0: * @return true if buffer contains the byte sequence given in test at offset bufferOffset, false michael@0: * otherwise. michael@0: */ michael@0: static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) { michael@0: if (buffer.length < test.length) { michael@0: return false; michael@0: } michael@0: michael@0: for (int i = 0; i < test.length; ++i) { michael@0: if (buffer[bufferOffset + i] != test[i]) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: /** michael@0: * Decode the favicon present in the region of the provided byte[] starting at offset and michael@0: * proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the michael@0: * given range does not contain a bitmap we know how to decode. michael@0: * michael@0: * @param buffer Byte array containing the favicon to decode. michael@0: * @param offset The index of the first byte in the array of the region of interest. michael@0: * @param length The length of the region in the array to decode. michael@0: * @return The decoded version of the bitmap in the described region, or null if none can be michael@0: * decoded. michael@0: */ michael@0: public static LoadFaviconResult decodeFavicon(byte[] buffer, int offset, int length) { michael@0: LoadFaviconResult result; michael@0: if (isDecodableByAndroid(buffer, offset)) { michael@0: result = new LoadFaviconResult(); michael@0: result.offset = offset; michael@0: result.length = length; michael@0: result.isICO = false; michael@0: michael@0: Bitmap decodedImage = BitmapUtils.decodeByteArray(buffer, offset, length); michael@0: if (decodedImage == null) { michael@0: // What we got wasn't decodable after all. Probably corrupted image, or we got a muffled OOM. michael@0: return null; michael@0: } michael@0: michael@0: // We assume here that decodeByteArray doesn't hold on to the entire supplied michael@0: // buffer -- worst case, each of our buffers will be twice the necessary size. michael@0: result.bitmapsDecoded = new SingleBitmapIterator(decodedImage); michael@0: result.faviconBytes = buffer; michael@0: michael@0: return result; michael@0: } michael@0: michael@0: // If it's not decodable by Android, it might be an ICO. Let's try. michael@0: ICODecoder decoder = new ICODecoder(buffer, offset, length); michael@0: michael@0: result = decoder.decode(); michael@0: michael@0: if (result == null) { michael@0: return null; michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: michael@0: public static LoadFaviconResult decodeDataURI(String uri) { michael@0: if (uri == null) { michael@0: Log.w(LOG_TAG, "Can't decode null data: URI."); michael@0: return null; michael@0: } michael@0: michael@0: if (!uri.startsWith("data:image/")) { michael@0: Log.w(LOG_TAG, "Can't decode non-image data: URI."); michael@0: return null; michael@0: } michael@0: michael@0: // Otherwise, let's attack this blindly. Strictly we should be parsing. michael@0: int offset = uri.indexOf(',') + 1; michael@0: if (offset == 0) { michael@0: Log.w(LOG_TAG, "No ',' in data: URI; malformed?"); michael@0: return null; michael@0: } michael@0: michael@0: try { michael@0: String base64 = uri.substring(offset); michael@0: byte[] raw = Base64.decode(base64, Base64.DEFAULT); michael@0: return decodeFavicon(raw); michael@0: } catch (Exception e) { michael@0: Log.w(LOG_TAG, "Couldn't decode data: URI.", e); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public static LoadFaviconResult decodeFavicon(byte[] buffer) { michael@0: return decodeFavicon(buffer, 0, buffer.length); michael@0: } michael@0: michael@0: /** michael@0: * Returns the smallest bitmap in the icon represented by the provided michael@0: * data: URI that's larger than the desired width, or the largest if michael@0: * there is no larger icon. michael@0: * michael@0: * Returns null if no bitmap could be extracted. michael@0: * michael@0: * Bug 961600: we shouldn't be doing all of this work. The favicon cache michael@0: * should be used, and will give us the right size icon. michael@0: */ michael@0: public static Bitmap getMostSuitableBitmapFromDataURI(String iconURI, int desiredWidth) { michael@0: LoadFaviconResult result = FaviconDecoder.decodeDataURI(iconURI); michael@0: if (result == null) { michael@0: // Nothing we can do. michael@0: Log.w(LOG_TAG, "Unable to decode icon URI."); michael@0: return null; michael@0: } michael@0: michael@0: final Iterator bitmaps = result.getBitmaps(); michael@0: if (!bitmaps.hasNext()) { michael@0: Log.w(LOG_TAG, "No bitmaps in decoded icon."); michael@0: return null; michael@0: } michael@0: michael@0: Bitmap bitmap = bitmaps.next(); michael@0: if (!bitmaps.hasNext()) { michael@0: // We're done! There was only one, so this is as big as it gets. michael@0: return bitmap; michael@0: } michael@0: michael@0: // Find a bitmap of the most suitable size. michael@0: int currentWidth = bitmap.getWidth(); michael@0: while ((currentWidth < desiredWidth) && michael@0: bitmaps.hasNext()) { michael@0: final Bitmap b = bitmaps.next(); michael@0: if (b.getWidth() > currentWidth) { michael@0: currentWidth = b.getWidth(); michael@0: bitmap = b; michael@0: } michael@0: } michael@0: michael@0: return bitmap; michael@0: } michael@0: michael@0: /** michael@0: * Iterator to hold a single bitmap. michael@0: */ michael@0: static class SingleBitmapIterator implements Iterator { michael@0: private Bitmap bitmap; michael@0: michael@0: public SingleBitmapIterator(Bitmap b) { michael@0: bitmap = b; michael@0: } michael@0: michael@0: /** michael@0: * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure michael@0: * places where the runtime type of the Iterator under consideration is known and michael@0: * destruction of it is discouraged. michael@0: * michael@0: * @return The bitmap carried by this SingleBitmapIterator. michael@0: */ michael@0: public Bitmap peek() { michael@0: return bitmap; michael@0: } michael@0: michael@0: @Override michael@0: public boolean hasNext() { michael@0: return bitmap != null; michael@0: } michael@0: michael@0: @Override michael@0: public Bitmap next() { michael@0: if (bitmap == null) { michael@0: throw new NoSuchElementException("Element already returned from SingleBitmapIterator."); michael@0: } michael@0: michael@0: Bitmap ret = bitmap; michael@0: bitmap = null; michael@0: return ret; michael@0: } michael@0: michael@0: @Override michael@0: public void remove() { michael@0: throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator."); michael@0: } michael@0: } michael@0: }