gecko/mobile/android/base/favicons/decoders/ICODecoder.java

378 lines
14 KiB
Java

/* 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.favicons.decoders;
import android.graphics.Bitmap;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.gfx.BitmapUtils;
import android.util.SparseArray;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 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;
private IconDirectoryEntry[] iconDirectory;
private boolean isValid;
private boolean hasDecoded;
public ICODecoder(byte[] decodand, int offset, int len) {
this.decodand = decodand;
this.offset = offset;
this.len = len;
}
/**
* 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 > Favicons.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] = (byte) singlePayloadOffset;
decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (byte) (singlePayloadOffset >>> 8);
decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (byte) (singlePayloadOffset >>> 16);
decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (byte) (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;
}
/**
* 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;
}
}
}