mobile/android/base/favicons/decoders/FaviconDecoder.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial