diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/icons/decoders | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/icons/decoders')
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); + } +} |