summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/icons/decoders
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/icons/decoders')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java197
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java396
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java212
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java133
4 files changed, 938 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java
new file mode 100644
index 000000000..43f5d0ac6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.icons.decoders;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.Base64;
+import android.util.Log;
+
+import org.mozilla.gecko.gfx.BitmapUtils;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Class providing static utility methods for decoding favicons.
+ */
+public class FaviconDecoder {
+ private static final String LOG_TAG = "GeckoFaviconDecoder";
+
+ static enum ImageMagicNumbers {
+ // It is irritating that Java bytes are signed...
+ PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}),
+ GIF(new byte[] {0x47, 0x49, 0x46, 0x38}),
+ JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}),
+ BMP(new byte[] {0x42, 0x4d}),
+ WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a});
+
+ public byte[] value;
+
+ private ImageMagicNumbers(byte[] value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Check for image format magic numbers of formats supported by Android.
+ * @param buffer Byte buffer to check for magic numbers
+ * @param offset Offset at which to look for magic numbers.
+ * @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence
+ * starting with the magic numbers thereof). false otherwise.
+ */
+ private static boolean isDecodableByAndroid(byte[] buffer, int offset) {
+ for (ImageMagicNumbers m : ImageMagicNumbers.values()) {
+ if (bufferStartsWith(buffer, m.value, offset)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Utility function to check for the existence of a test byte sequence at a given offset in a
+ * buffer.
+ *
+ * @param buffer Byte buffer to search.
+ * @param test Byte sequence to search for.
+ * @param bufferOffset Index in input buffer to expect test sequence.
+ * @return true if buffer contains the byte sequence given in test at offset bufferOffset, false
+ * otherwise.
+ */
+ static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) {
+ if (buffer.length < test.length) {
+ return false;
+ }
+
+ for (int i = 0; i < test.length; ++i) {
+ if (buffer[bufferOffset + i] != test[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Decode the favicon present in the region of the provided byte[] starting at offset and
+ * proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the
+ * given range does not contain a bitmap we know how to decode.
+ *
+ * @param buffer Byte array containing the favicon to decode.
+ * @param offset The index of the first byte in the array of the region of interest.
+ * @param length The length of the region in the array to decode.
+ * @return The decoded version of the bitmap in the described region, or null if none can be
+ * decoded.
+ */
+ public static LoadFaviconResult decodeFavicon(Context context, byte[] buffer, int offset, int length) {
+ LoadFaviconResult result;
+ if (isDecodableByAndroid(buffer, offset)) {
+ result = new LoadFaviconResult();
+ result.offset = offset;
+ result.length = length;
+ result.isICO = false;
+
+ Bitmap decodedImage = BitmapUtils.decodeByteArray(buffer, offset, length);
+ if (decodedImage == null) {
+ // What we got wasn't decodable after all. Probably corrupted image, or we got a muffled OOM.
+ return null;
+ }
+
+ // We assume here that decodeByteArray doesn't hold on to the entire supplied
+ // buffer -- worst case, each of our buffers will be twice the necessary size.
+ result.bitmapsDecoded = new SingleBitmapIterator(decodedImage);
+ result.faviconBytes = buffer;
+
+ return result;
+ }
+
+ // If it's not decodable by Android, it might be an ICO. Let's try.
+ ICODecoder decoder = new ICODecoder(context, buffer, offset, length);
+
+ result = decoder.decode();
+
+ if (result == null) {
+ return null;
+ }
+
+ return result;
+ }
+
+ public static LoadFaviconResult decodeDataURI(Context context, String uri) {
+ if (uri == null) {
+ Log.w(LOG_TAG, "Can't decode null data: URI.");
+ return null;
+ }
+
+ if (!uri.startsWith("data:image/")) {
+ // Can't decode non-image data: URI.
+ return null;
+ }
+
+ // Otherwise, let's attack this blindly. Strictly we should be parsing.
+ int offset = uri.indexOf(',') + 1;
+ if (offset == 0) {
+ Log.w(LOG_TAG, "No ',' in data: URI; malformed?");
+ return null;
+ }
+
+ try {
+ String base64 = uri.substring(offset);
+ byte[] raw = Base64.decode(base64, Base64.DEFAULT);
+ return decodeFavicon(context, raw);
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "Couldn't decode data: URI.", e);
+ return null;
+ }
+ }
+
+ public static LoadFaviconResult decodeFavicon(Context context, byte[] buffer) {
+ return decodeFavicon(context, buffer, 0, buffer.length);
+ }
+
+ /**
+ * Iterator to hold a single bitmap.
+ */
+ static class SingleBitmapIterator implements Iterator<Bitmap> {
+ private Bitmap bitmap;
+
+ public SingleBitmapIterator(Bitmap b) {
+ bitmap = b;
+ }
+
+ /**
+ * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure
+ * places where the runtime type of the Iterator under consideration is known and
+ * destruction of it is discouraged.
+ *
+ * @return The bitmap carried by this SingleBitmapIterator.
+ */
+ public Bitmap peek() {
+ return bitmap;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return bitmap != null;
+ }
+
+ @Override
+ public Bitmap next() {
+ if (bitmap == null) {
+ throw new NoSuchElementException("Element already returned from SingleBitmapIterator.");
+ }
+
+ Bitmap ret = bitmap;
+ bitmap = null;
+ return ret;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator.");
+ }
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java
new file mode 100644
index 000000000..44e3f1252
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java
@@ -0,0 +1,396 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.icons.decoders;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.support.annotation.VisibleForTesting;
+import android.util.SparseArray;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.R;
+
+/**
+ * Utility class for determining the region of a provided array which contains the largest bitmap,
+ * assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning
+ * unwanted entries from ICO files, if desired.
+ *
+ * An ICO file is a container format that may hold up to 255 images in either BMP or PNG format.
+ * A mixture of image types may not exist.
+ *
+ * The format consists of a header specifying the number, n, of images, followed by the Icon Directory.
+ *
+ * The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for
+ * the corresponding image, the dimensions, colour information, payload size, and location in the file.
+ *
+ * All numerical fields follow a little-endian byte ordering.
+ *
+ * Header format:
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Reserved field. Must be zero | Type (1 for ICO, 2 for CUR) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Image count (n) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * The type field is expected to always be 1. CUR format images should not be used for Favicons.
+ *
+ *
+ * Icon Directory Entry format:
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Image width | Image height | Palette size | Reserved (0) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Colour plane count | Bits per pixel |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Size of image data, in bytes |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Start of image data, as an offset from start of file |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Image dimensions of zero are to be interpreted as image dimensions of 256.
+ *
+ * The palette size field records the number of colours in the stored BMP, if a palette is used. Zero
+ * if the payload is a PNG or no palette is in use.
+ *
+ * The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be
+ * interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field.
+ * (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.)
+ *
+ *
+ * The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps.
+ *
+ * This class is not thread safe.
+ */
+public class ICODecoder implements Iterable<Bitmap> {
+ // The number of bytes that compacting will save for us to bother doing it.
+ public static final int COMPACT_THRESHOLD = 4000;
+
+ // Some geometry of an ICO file.
+ public static final int ICO_HEADER_LENGTH_BYTES = 6;
+ public static final int ICO_ICONDIRENTRY_LENGTH_BYTES = 16;
+
+ // The buffer containing bytes to attempt to decode.
+ private byte[] decodand;
+
+ // The region of the decodand to decode.
+ private int offset;
+ private int len;
+
+ IconDirectoryEntry[] iconDirectory;
+ private boolean isValid;
+ private boolean hasDecoded;
+ private int largestFaviconSize;
+
+ @RobocopTarget
+ public ICODecoder(Context context, byte[] decodand, int offset, int len) {
+ this.decodand = decodand;
+ this.offset = offset;
+ this.len = len;
+ this.largestFaviconSize = context.getResources()
+ .getDimensionPixelSize(R.dimen.favicon_largest_interesting_size);
+ }
+
+ /**
+ * Decode the Icon Directory for this ICO and store the result in iconDirectory.
+ *
+ * @return true if ICO decoding was considered to probably be a success, false if it certainly
+ * was a failure.
+ */
+ private boolean decodeIconDirectoryAndPossiblyPrune() {
+ hasDecoded = true;
+
+ // Fail if the end of the described range is out of bounds.
+ if (offset + len > decodand.length) {
+ return false;
+ }
+
+ // Fail if we don't have enough space for the header.
+ if (len < ICO_HEADER_LENGTH_BYTES) {
+ return false;
+ }
+
+ // Check that the reserved fields in the header are indeed zero, and that the type field
+ // specifies ICO. If not, we've probably been given something that isn't really an ICO.
+ if (decodand[offset] != 0 ||
+ decodand[offset + 1] != 0 ||
+ decodand[offset + 2] != 1 ||
+ decodand[offset + 3] != 0) {
+ return false;
+ }
+
+ // Here, and in many other places, byte values are ANDed with 0xFF. This is because Java
+ // bytes are signed - to obtain a numerical value of a longer type which holds the unsigned
+ // interpretation of the byte of interest, we do this.
+ int numEncodedImages = (decodand[offset + 4] & 0xFF) |
+ (decodand[offset + 5] & 0xFF) << 8;
+
+
+ // Fail if there are no images or the field is corrupt.
+ if (numEncodedImages <= 0) {
+ return false;
+ }
+
+ final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+ // Fail if there is not enough space in the buffer for the stated number of icondir entries,
+ // let alone the data.
+ if (len < headerAndDirectorySize) {
+ return false;
+ }
+
+ // Put the pointer on the first byte of the first Icon Directory Entry.
+ int bufferIndex = offset + ICO_HEADER_LENGTH_BYTES;
+
+ // We now iterate over the Icon Directory, decoding each entry as we go. We also need to
+ // discard all entries except one >= the maximum interesting size.
+
+ // Size of the smallest image larger than the limit encountered.
+ int minimumMaximum = Integer.MAX_VALUE;
+
+ // Used to track the best entry for each size. The entries we want to keep.
+ SparseArray<IconDirectoryEntry> preferenceArray = new SparseArray<IconDirectoryEntry>();
+
+ for (int i = 0; i < numEncodedImages; i++, bufferIndex += ICO_ICONDIRENTRY_LENGTH_BYTES) {
+ // Decode the Icon Directory Entry at this offset.
+ IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(decodand, offset, len, bufferIndex);
+ newEntry.index = i;
+
+ if (newEntry.isErroneous) {
+ continue;
+ }
+
+ if (newEntry.width > largestFaviconSize) {
+ // If we already have a smaller image larger than the maximum size of interest, we
+ // don't care about the new one which is larger than the smallest image larger than
+ // the maximum size.
+ if (newEntry.width >= minimumMaximum) {
+ continue;
+ }
+
+ // Remove the previous minimum-maximum.
+ preferenceArray.delete(minimumMaximum);
+
+ minimumMaximum = newEntry.width;
+ }
+
+ IconDirectoryEntry oldEntry = preferenceArray.get(newEntry.width);
+ if (oldEntry == null) {
+ preferenceArray.put(newEntry.width, newEntry);
+ continue;
+ }
+
+ if (oldEntry.compareTo(newEntry) < 0) {
+ preferenceArray.put(newEntry.width, newEntry);
+ }
+ }
+
+ final int count = preferenceArray.size();
+
+ // Abort if no entries are desired (Perhaps all are corrupt?)
+ if (count == 0) {
+ return false;
+ }
+
+ // Allocate space for the icon directory entries in the decoded directory.
+ iconDirectory = new IconDirectoryEntry[count];
+
+ // The size of the data in the buffer that we find useful.
+ int retainedSpace = ICO_HEADER_LENGTH_BYTES;
+
+ for (int i = 0; i < count; i++) {
+ IconDirectoryEntry e = preferenceArray.valueAt(i);
+ retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.payloadSize;
+ iconDirectory[i] = e;
+ }
+
+ isValid = true;
+
+ // Set the number of images field in the buffer to reflect the number of retained entries.
+ decodand[offset + 4] = (byte) iconDirectory.length;
+ decodand[offset + 5] = (byte) (iconDirectory.length >>> 8);
+
+ if ((len - retainedSpace) > COMPACT_THRESHOLD) {
+ compactingCopy(retainedSpace);
+ }
+
+ return true;
+ }
+
+ /**
+ * Copy the buffer into a new array of exactly the required size, omitting any unwanted data.
+ */
+ private void compactingCopy(int spaceRetained) {
+ byte[] buf = new byte[spaceRetained];
+
+ // Copy the header.
+ System.arraycopy(decodand, offset, buf, 0, ICO_HEADER_LENGTH_BYTES);
+
+ int headerPtr = ICO_HEADER_LENGTH_BYTES;
+
+ int payloadPtr = ICO_HEADER_LENGTH_BYTES + (iconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+ int ind = 0;
+ for (IconDirectoryEntry entry : iconDirectory) {
+ // Copy this entry.
+ System.arraycopy(decodand, offset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+ // Copy its payload.
+ System.arraycopy(decodand, offset + entry.payloadOffset, buf, payloadPtr, entry.payloadSize);
+
+ // Update the offset field.
+ buf[headerPtr + 12] = (byte) payloadPtr;
+ buf[headerPtr + 13] = (byte) (payloadPtr >>> 8);
+ buf[headerPtr + 14] = (byte) (payloadPtr >>> 16);
+ buf[headerPtr + 15] = (byte) (payloadPtr >>> 24);
+
+ entry.payloadOffset = payloadPtr;
+ entry.index = ind;
+
+ payloadPtr += entry.payloadSize;
+ headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES;
+ ind++;
+ }
+
+ decodand = buf;
+ offset = 0;
+ len = spaceRetained;
+ }
+
+ /**
+ * Decode and return the bitmap represented by the given index in the Icon Directory, if valid.
+ *
+ * @param index The index into the Icon Directory of the image of interest.
+ * @return The decoded Bitmap object for this image, or null if the entry is invalid or decoding
+ * fails.
+ */
+ public Bitmap decodeBitmapAtIndex(int index) {
+ final IconDirectoryEntry iconDirEntry = iconDirectory[index];
+
+ if (iconDirEntry.payloadIsPNG) {
+ // PNG payload. Simply extract it and decode it.
+ return BitmapUtils.decodeByteArray(decodand, offset + iconDirEntry.payloadOffset, iconDirEntry.payloadSize);
+ }
+
+ // The payload is a BMP, so we need to do some magic to get the decoder to do what we want.
+ // We construct an ICO containing just the image we want, and let Android do the rest.
+ byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.payloadSize];
+
+ // Set the type field in the ICO header.
+ decodeTarget[2] = 1;
+
+ // Set the num-images field in the header to 1.
+ decodeTarget[4] = 1;
+
+ // Copy the ICONDIRENTRY we need into the new buffer.
+ System.arraycopy(decodand, offset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+ // Copy the payload into the new buffer.
+ final int singlePayloadOffset = ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES;
+ System.arraycopy(decodand, offset + iconDirEntry.payloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.payloadSize);
+
+ // Update the offset field of the ICONDIRENTRY to make the new ICO valid.
+ decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = singlePayloadOffset;
+ decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (singlePayloadOffset >>> 8);
+ decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (singlePayloadOffset >>> 16);
+ decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (singlePayloadOffset >>> 24);
+
+ // Decode the newly-constructed singleton-ICO.
+ return BitmapUtils.decodeByteArray(decodeTarget);
+ }
+
+ /**
+ * Fetch an iterator over the images in this ICO, or null if this ICO seems to be invalid.
+ *
+ * @return An iterator over the Bitmaps stored in this ICO, or null if decoding fails.
+ */
+ @Override
+ public ICOIterator iterator() {
+ // If a previous call to decode concluded this ICO is invalid, abort.
+ if (hasDecoded && !isValid) {
+ return null;
+ }
+
+ // If we've not been decoded before, but now fail to make any sense of the ICO, abort.
+ if (!hasDecoded) {
+ if (!decodeIconDirectoryAndPossiblyPrune()) {
+ return null;
+ }
+ }
+
+ // If decoding was a success, return an iterator over the images in this ICO.
+ return new ICOIterator();
+ }
+
+ /**
+ * Decode this ICO and return the result as a LoadFaviconResult.
+ * @return A LoadFaviconResult representing the decoded ICO.
+ */
+ public LoadFaviconResult decode() {
+ // The call to iterator returns null if decoding fails.
+ Iterator<Bitmap> bitmaps = iterator();
+ if (bitmaps == null) {
+ return null;
+ }
+
+ LoadFaviconResult result = new LoadFaviconResult();
+
+ result.bitmapsDecoded = bitmaps;
+ result.faviconBytes = decodand;
+ result.offset = offset;
+ result.length = len;
+ result.isICO = true;
+
+ return result;
+ }
+
+ @VisibleForTesting
+ @RobocopTarget
+ public IconDirectoryEntry[] getIconDirectory() {
+ return iconDirectory;
+ }
+
+ @VisibleForTesting
+ @RobocopTarget
+ public int getLargestFaviconSize() {
+ return largestFaviconSize;
+ }
+
+ /**
+ * Inner class to iterate over the elements in the ICO represented by the enclosing instance.
+ */
+ private class ICOIterator implements Iterator<Bitmap> {
+ private int mIndex;
+
+ @Override
+ public boolean hasNext() {
+ return mIndex < iconDirectory.length;
+ }
+
+ @Override
+ public Bitmap next() {
+ if (mIndex > iconDirectory.length) {
+ throw new NoSuchElementException("No more elements in this ICO.");
+ }
+ return decodeBitmapAtIndex(mIndex++);
+ }
+
+ @Override
+ public void remove() {
+ if (iconDirectory[mIndex] == null) {
+ throw new IllegalStateException("Remove already called for element " + mIndex);
+ }
+ iconDirectory[mIndex] = null;
+ }
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java
new file mode 100644
index 000000000..82ff91a55
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java
@@ -0,0 +1,212 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.icons.decoders;
+
+import android.support.annotation.VisibleForTesting;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+/**
+ * Representation of an ICO file ICONDIRENTRY structure.
+ */
+public class IconDirectoryEntry implements Comparable<IconDirectoryEntry> {
+
+ public static int maxBPP;
+
+ int width;
+ int height;
+ int paletteSize;
+ int bitsPerPixel;
+ int payloadSize;
+ int payloadOffset;
+ boolean payloadIsPNG;
+
+ // Tracks the index in the Icon Directory of this entry. Useful only for pruning.
+ int index;
+ boolean isErroneous;
+
+ @RobocopTarget
+ public IconDirectoryEntry(int width, int height, int paletteSize, int bitsPerPixel, int payloadSize, int payloadOffset, boolean payloadIsPNG) {
+ this.width = width;
+ this.height = height;
+ this.paletteSize = paletteSize;
+ this.bitsPerPixel = bitsPerPixel;
+ this.payloadSize = payloadSize;
+ this.payloadOffset = payloadOffset;
+ this.payloadIsPNG = payloadIsPNG;
+ }
+
+ /**
+ * Method to get a dummy Icon Directory Entry with the Erroneous bit set.
+ *
+ * @return An erroneous placeholder Icon Directory Entry.
+ */
+ public static IconDirectoryEntry getErroneousEntry() {
+ IconDirectoryEntry ret = new IconDirectoryEntry(-1, -1, -1, -1, -1, -1, false);
+ ret.isErroneous = true;
+
+ return ret;
+ }
+
+ /**
+ * Create an IconDirectoryEntry object from a byte[]. Interprets the buffer starting at the given
+ * offset as an IconDirectoryEntry and returns the result.
+ *
+ * @param buffer Byte array containing the icon directory entry to decode.
+ * @param regionOffset Offset into the byte array of the valid region of the buffer.
+ * @param regionLength Length of the valid region in the buffer.
+ * @param entryOffset Offset of the icon directory entry to decode within the buffer.
+ * @return An IconDirectoryEntry object representing the entry specified, or null if the entry
+ * is obviously invalid.
+ */
+ public static IconDirectoryEntry createFromBuffer(byte[] buffer, int regionOffset, int regionLength, int entryOffset) {
+ // Verify that the reserved field is really zero.
+ if (buffer[entryOffset + 3] != 0) {
+ return getErroneousEntry();
+ }
+
+ // Verify that the entry points to a region that actually exists in the buffer, else bin it.
+ int fieldPtr = entryOffset + 8;
+ int entryLength = (buffer[fieldPtr] & 0xFF) |
+ (buffer[fieldPtr + 1] & 0xFF) << 8 |
+ (buffer[fieldPtr + 2] & 0xFF) << 16 |
+ (buffer[fieldPtr + 3] & 0xFF) << 24;
+
+ // Advance to the offset field.
+ fieldPtr += 4;
+
+ int payloadOffset = (buffer[fieldPtr] & 0xFF) |
+ (buffer[fieldPtr + 1] & 0xFF) << 8 |
+ (buffer[fieldPtr + 2] & 0xFF) << 16 |
+ (buffer[fieldPtr + 3] & 0xFF) << 24;
+
+ // Fail if the entry describes a region outside the buffer.
+ if (payloadOffset < 0 || entryLength < 0 || payloadOffset + entryLength > regionOffset + regionLength) {
+ return getErroneousEntry();
+ }
+
+ // Extract the image dimensions.
+ int imageWidth = buffer[entryOffset] & 0xFF;
+ int imageHeight = buffer[entryOffset + 1] & 0xFF;
+
+ // Because Microsoft, a size value of zero represents an image size of 256.
+ if (imageWidth == 0) {
+ imageWidth = 256;
+ }
+
+ if (imageHeight == 0) {
+ imageHeight = 256;
+ }
+
+ // If the image uses a colour palette, this is the number of colours, otherwise this is zero.
+ int paletteSize = buffer[entryOffset + 2] & 0xFF;
+
+ // The plane count - usually 0 or 1. When > 1, taken as multiplier on bitsPerPixel.
+ int colorPlanes = buffer[entryOffset + 4] & 0xFF;
+
+ int bitsPerPixel = (buffer[entryOffset + 6] & 0xFF) |
+ (buffer[entryOffset + 7] & 0xFF) << 8;
+
+ if (colorPlanes > 1) {
+ bitsPerPixel *= colorPlanes;
+ }
+
+ // Look for PNG magic numbers at the start of the payload.
+ boolean payloadIsPNG = FaviconDecoder.bufferStartsWith(buffer, FaviconDecoder.ImageMagicNumbers.PNG.value, regionOffset + payloadOffset);
+
+ return new IconDirectoryEntry(imageWidth, imageHeight, paletteSize, bitsPerPixel, entryLength, payloadOffset, payloadIsPNG);
+ }
+
+ /**
+ * Get the number of bytes from the start of the ICO file to the beginning of this entry.
+ */
+ public int getOffset() {
+ return ICODecoder.ICO_HEADER_LENGTH_BYTES + (index * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
+ }
+
+ @Override
+ public int compareTo(IconDirectoryEntry another) {
+ if (width > another.width) {
+ return 1;
+ }
+
+ if (width < another.width) {
+ return -1;
+ }
+
+ // Where both images exceed the max BPP, take the smaller of the two BPP values.
+ if (bitsPerPixel >= maxBPP && another.bitsPerPixel >= maxBPP) {
+ if (bitsPerPixel < another.bitsPerPixel) {
+ return 1;
+ }
+
+ if (bitsPerPixel > another.bitsPerPixel) {
+ return -1;
+ }
+ }
+
+ // Otherwise, take the larger of the BPP values.
+ if (bitsPerPixel > another.bitsPerPixel) {
+ return 1;
+ }
+
+ if (bitsPerPixel < another.bitsPerPixel) {
+ return -1;
+ }
+
+ // Prefer large palettes.
+ if (paletteSize > another.paletteSize) {
+ return 1;
+ }
+
+ if (paletteSize < another.paletteSize) {
+ return -1;
+ }
+
+ // Prefer smaller payloads.
+ if (payloadSize < another.payloadSize) {
+ return 1;
+ }
+
+ if (payloadSize > another.payloadSize) {
+ return -1;
+ }
+
+ // If all else fails, prefer PNGs over BMPs. They tend to be smaller.
+ if (payloadIsPNG && !another.payloadIsPNG) {
+ return 1;
+ }
+
+ if (!payloadIsPNG && another.payloadIsPNG) {
+ return -1;
+ }
+
+ return 0;
+ }
+
+ public static void setMaxBPP(int maxBPP) {
+ IconDirectoryEntry.maxBPP = maxBPP;
+ }
+
+ @VisibleForTesting
+ @RobocopTarget
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public String toString() {
+ return "IconDirectoryEntry{" +
+ "\nwidth=" + width +
+ ", \nheight=" + height +
+ ", \npaletteSize=" + paletteSize +
+ ", \nbitsPerPixel=" + bitsPerPixel +
+ ", \npayloadSize=" + payloadSize +
+ ", \npayloadOffset=" + payloadOffset +
+ ", \npayloadIsPNG=" + payloadIsPNG +
+ ", \nindex=" + index +
+ '}';
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java
new file mode 100644
index 000000000..cc196b91e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.icons.decoders;
+
+import android.graphics.Bitmap;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Class representing the result of loading a favicon.
+ * This operation will produce either a collection of favicons, a single favicon, or no favicon.
+ * It is necessary to model single favicons differently to a collection of one favicon (An entity
+ * that may not exist with this scheme) since the in-database representation of these things differ.
+ * (In particular, collections of favicons are stored in encoded ICO format, whereas single icons are
+ * stored as decoded bitmap blobs.)
+ */
+public class LoadFaviconResult {
+ private static final String LOGTAG = "LoadFaviconResult";
+
+ byte[] faviconBytes;
+ int offset;
+ int length;
+
+ boolean isICO;
+ Iterator<Bitmap> bitmapsDecoded;
+
+ public Iterator<Bitmap> getBitmaps() {
+ return bitmapsDecoded;
+ }
+
+ /**
+ * Return a representation of this result suitable for storing in the database.
+ *
+ * @return A byte array containing the bytes from which this result was decoded,
+ * or null if re-encoding failed.
+ */
+ public byte[] getBytesForDatabaseStorage() {
+ // Begin by normalising the buffer.
+ if (offset != 0 || length != faviconBytes.length) {
+ final byte[] normalised = new byte[length];
+ System.arraycopy(faviconBytes, offset, normalised, 0, length);
+ offset = 0;
+ faviconBytes = normalised;
+ }
+
+ // For results containing multiple images, we store the result verbatim. (But cutting the
+ // buffer to size first).
+ // We may instead want to consider re-encoding the entire ICO as a collection of efficiently
+ // encoded PNGs. This may not be worth the CPU time (Indeed, the encoding of single-image
+ // favicons may also not be worth the time/space tradeoff.).
+ if (isICO) {
+ return faviconBytes;
+ }
+
+ // For results containing a single image, we re-encode the
+ // result as a PNG in an effort to save space.
+ final Bitmap favicon = ((FaviconDecoder.SingleBitmapIterator) bitmapsDecoded).peek();
+ final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+ try {
+ if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
+ return stream.toByteArray();
+ }
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Out of memory re-compressing favicon.");
+ }
+
+ Log.w(LOGTAG, "Favicon re-compression failed.");
+ return null;
+ }
+
+ @Nullable
+ public Bitmap getBestBitmap(int targetWidthAndHeight) {
+ final SparseArray<Bitmap> iconMap = new SparseArray<>();
+ final List<Integer> sizes = new ArrayList<>();
+
+ while (bitmapsDecoded.hasNext()) {
+ final Bitmap b = bitmapsDecoded.next();
+
+ // It's possible to receive null, most likely due to OOM or a zero-sized image,
+ // from BitmapUtils.decodeByteArray(byte[], int, int, BitmapFactory.Options)
+ if (b != null) {
+ iconMap.put(b.getWidth(), b);
+ sizes.add(b.getWidth());
+ }
+ }
+
+ int bestSize = selectBestSizeFromList(sizes, targetWidthAndHeight);
+
+ if (bestSize == -1) {
+ // No icons found: this could occur if we weren't able to process any of the
+ // supplied icons.
+ return null;
+ }
+
+ return iconMap.get(bestSize);
+ }
+
+ /**
+ * Select the closest icon size from a list of icon sizes.
+ * We just find the first icon that is larger than the preferred size if available, or otherwise select the
+ * largest icon (if all icons are smaller than the preferred size).
+ *
+ * @return The closest icon size, or -1 if no sizes are supplied.
+ */
+ public static int selectBestSizeFromList(final List<Integer> sizes, final int preferredSize) {
+ if (sizes.isEmpty()) {
+ // This isn't ideal, however current code assumes this as an error value for now.
+ return -1;
+ }
+
+ Collections.sort(sizes);
+
+ for (int size : sizes) {
+ if (size >= preferredSize) {
+ return size;
+ }
+ }
+
+ // If all icons are smaller than the preferred size then we don't have an icon
+ // selected yet, therefore just take the largest (last) icon.
+ return sizes.get(sizes.size() - 1);
+ }
+}