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 org.mozilla.gecko.favicons.Favicons; michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: michael@0: import android.util.SparseArray; michael@0: michael@0: import java.util.Iterator; michael@0: import java.util.NoSuchElementException; michael@0: michael@0: /** michael@0: * Utility class for determining the region of a provided array which contains the largest bitmap, michael@0: * assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning michael@0: * unwanted entries from ICO files, if desired. michael@0: * michael@0: * An ICO file is a container format that may hold up to 255 images in either BMP or PNG format. michael@0: * A mixture of image types may not exist. michael@0: * michael@0: * The format consists of a header specifying the number, n, of images, followed by the Icon Directory. michael@0: * michael@0: * The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for michael@0: * the corresponding image, the dimensions, colour information, payload size, and location in the file. michael@0: * michael@0: * All numerical fields follow a little-endian byte ordering. michael@0: * michael@0: * Header format: michael@0: * michael@0: * 0 1 2 3 michael@0: * 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 michael@0: * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: * | Reserved field. Must be zero | Type (1 for ICO, 2 for CUR) | michael@0: * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: * | Image count (n) | michael@0: * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: * michael@0: * The type field is expected to always be 1. CUR format images should not be used for Favicons. michael@0: * michael@0: * michael@0: * Icon Directory Entry format: michael@0: * michael@0: * 0 1 2 3 michael@0: * 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 michael@0: * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: * | Image width | Image height | Palette size | Reserved (0) | michael@0: * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: * | Colour plane count | Bits per pixel | michael@0: * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: * | Size of image data, in bytes | michael@0: * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: * | Start of image data, as an offset from start of file | michael@0: * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: * michael@0: * Image dimensions of zero are to be interpreted as image dimensions of 256. michael@0: * michael@0: * The palette size field records the number of colours in the stored BMP, if a palette is used. Zero michael@0: * if the payload is a PNG or no palette is in use. michael@0: * michael@0: * The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be michael@0: * interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field. michael@0: * (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.) michael@0: * michael@0: * michael@0: * The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps. michael@0: * michael@0: * This class is not thread safe. michael@0: */ michael@0: public class ICODecoder implements Iterable { michael@0: // The number of bytes that compacting will save for us to bother doing it. michael@0: public static final int COMPACT_THRESHOLD = 4000; michael@0: michael@0: // Some geometry of an ICO file. michael@0: public static final int ICO_HEADER_LENGTH_BYTES = 6; michael@0: public static final int ICO_ICONDIRENTRY_LENGTH_BYTES = 16; michael@0: michael@0: // The buffer containing bytes to attempt to decode. michael@0: private byte[] decodand; michael@0: michael@0: // The region of the decodand to decode. michael@0: private int offset; michael@0: private int len; michael@0: michael@0: private IconDirectoryEntry[] iconDirectory; michael@0: private boolean isValid; michael@0: private boolean hasDecoded; michael@0: michael@0: public ICODecoder(byte[] decodand, int offset, int len) { michael@0: this.decodand = decodand; michael@0: this.offset = offset; michael@0: this.len = len; michael@0: } michael@0: michael@0: /** michael@0: * Decode the Icon Directory for this ICO and store the result in iconDirectory. michael@0: * michael@0: * @return true if ICO decoding was considered to probably be a success, false if it certainly michael@0: * was a failure. michael@0: */ michael@0: private boolean decodeIconDirectoryAndPossiblyPrune() { michael@0: hasDecoded = true; michael@0: michael@0: // Fail if the end of the described range is out of bounds. michael@0: if (offset + len > decodand.length) { michael@0: return false; michael@0: } michael@0: michael@0: // Fail if we don't have enough space for the header. michael@0: if (len < ICO_HEADER_LENGTH_BYTES) { michael@0: return false; michael@0: } michael@0: michael@0: // Check that the reserved fields in the header are indeed zero, and that the type field michael@0: // specifies ICO. If not, we've probably been given something that isn't really an ICO. michael@0: if (decodand[offset] != 0 || michael@0: decodand[offset + 1] != 0 || michael@0: decodand[offset + 2] != 1 || michael@0: decodand[offset + 3] != 0) { michael@0: return false; michael@0: } michael@0: michael@0: // Here, and in many other places, byte values are ANDed with 0xFF. This is because Java michael@0: // bytes are signed - to obtain a numerical value of a longer type which holds the unsigned michael@0: // interpretation of the byte of interest, we do this. michael@0: int numEncodedImages = (decodand[offset + 4] & 0xFF) | michael@0: (decodand[offset + 5] & 0xFF) << 8; michael@0: michael@0: michael@0: // Fail if there are no images or the field is corrupt. michael@0: if (numEncodedImages <= 0) { michael@0: return false; michael@0: } michael@0: michael@0: final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES); michael@0: michael@0: // Fail if there is not enough space in the buffer for the stated number of icondir entries, michael@0: // let alone the data. michael@0: if (len < headerAndDirectorySize) { michael@0: return false; michael@0: } michael@0: michael@0: // Put the pointer on the first byte of the first Icon Directory Entry. michael@0: int bufferIndex = offset + ICO_HEADER_LENGTH_BYTES; michael@0: michael@0: // We now iterate over the Icon Directory, decoding each entry as we go. We also need to michael@0: // discard all entries except one >= the maximum interesting size. michael@0: michael@0: // Size of the smallest image larger than the limit encountered. michael@0: int minimumMaximum = Integer.MAX_VALUE; michael@0: michael@0: // Used to track the best entry for each size. The entries we want to keep. michael@0: SparseArray preferenceArray = new SparseArray(); michael@0: michael@0: for (int i = 0; i < numEncodedImages; i++, bufferIndex += ICO_ICONDIRENTRY_LENGTH_BYTES) { michael@0: // Decode the Icon Directory Entry at this offset. michael@0: IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(decodand, offset, len, bufferIndex); michael@0: newEntry.index = i; michael@0: michael@0: if (newEntry.isErroneous) { michael@0: continue; michael@0: } michael@0: michael@0: if (newEntry.width > Favicons.largestFaviconSize) { michael@0: // If we already have a smaller image larger than the maximum size of interest, we michael@0: // don't care about the new one which is larger than the smallest image larger than michael@0: // the maximum size. michael@0: if (newEntry.width >= minimumMaximum) { michael@0: continue; michael@0: } michael@0: michael@0: // Remove the previous minimum-maximum. michael@0: preferenceArray.delete(minimumMaximum); michael@0: michael@0: minimumMaximum = newEntry.width; michael@0: } michael@0: michael@0: IconDirectoryEntry oldEntry = preferenceArray.get(newEntry.width); michael@0: if (oldEntry == null) { michael@0: preferenceArray.put(newEntry.width, newEntry); michael@0: continue; michael@0: } michael@0: michael@0: if (oldEntry.compareTo(newEntry) < 0) { michael@0: preferenceArray.put(newEntry.width, newEntry); michael@0: } michael@0: } michael@0: michael@0: final int count = preferenceArray.size(); michael@0: michael@0: // Abort if no entries are desired (Perhaps all are corrupt?) michael@0: if (count == 0) { michael@0: return false; michael@0: } michael@0: michael@0: // Allocate space for the icon directory entries in the decoded directory. michael@0: iconDirectory = new IconDirectoryEntry[count]; michael@0: michael@0: // The size of the data in the buffer that we find useful. michael@0: int retainedSpace = ICO_HEADER_LENGTH_BYTES; michael@0: michael@0: for (int i = 0; i < count; i++) { michael@0: IconDirectoryEntry e = preferenceArray.valueAt(i); michael@0: retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.payloadSize; michael@0: iconDirectory[i] = e; michael@0: } michael@0: michael@0: isValid = true; michael@0: michael@0: // Set the number of images field in the buffer to reflect the number of retained entries. michael@0: decodand[offset + 4] = (byte) iconDirectory.length; michael@0: decodand[offset + 5] = (byte) (iconDirectory.length >>> 8); michael@0: michael@0: if ((len - retainedSpace) > COMPACT_THRESHOLD) { michael@0: compactingCopy(retainedSpace); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: /** michael@0: * Copy the buffer into a new array of exactly the required size, omitting any unwanted data. michael@0: */ michael@0: private void compactingCopy(int spaceRetained) { michael@0: byte[] buf = new byte[spaceRetained]; michael@0: michael@0: // Copy the header. michael@0: System.arraycopy(decodand, offset, buf, 0, ICO_HEADER_LENGTH_BYTES); michael@0: michael@0: int headerPtr = ICO_HEADER_LENGTH_BYTES; michael@0: michael@0: int payloadPtr = ICO_HEADER_LENGTH_BYTES + (iconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES); michael@0: michael@0: int ind = 0; michael@0: for (IconDirectoryEntry entry : iconDirectory) { michael@0: // Copy this entry. michael@0: System.arraycopy(decodand, offset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES); michael@0: michael@0: // Copy its payload. michael@0: System.arraycopy(decodand, offset + entry.payloadOffset, buf, payloadPtr, entry.payloadSize); michael@0: michael@0: // Update the offset field. michael@0: buf[headerPtr + 12] = (byte) payloadPtr; michael@0: buf[headerPtr + 13] = (byte) (payloadPtr >>> 8); michael@0: buf[headerPtr + 14] = (byte) (payloadPtr >>> 16); michael@0: buf[headerPtr + 15] = (byte) (payloadPtr >>> 24); michael@0: michael@0: entry.payloadOffset = payloadPtr; michael@0: entry.index = ind; michael@0: michael@0: payloadPtr += entry.payloadSize; michael@0: headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES; michael@0: ind++; michael@0: } michael@0: michael@0: decodand = buf; michael@0: offset = 0; michael@0: len = spaceRetained; michael@0: } michael@0: michael@0: /** michael@0: * Decode and return the bitmap represented by the given index in the Icon Directory, if valid. michael@0: * michael@0: * @param index The index into the Icon Directory of the image of interest. michael@0: * @return The decoded Bitmap object for this image, or null if the entry is invalid or decoding michael@0: * fails. michael@0: */ michael@0: public Bitmap decodeBitmapAtIndex(int index) { michael@0: final IconDirectoryEntry iconDirEntry = iconDirectory[index]; michael@0: michael@0: if (iconDirEntry.payloadIsPNG) { michael@0: // PNG payload. Simply extract it and decode it. michael@0: return BitmapUtils.decodeByteArray(decodand, offset + iconDirEntry.payloadOffset, iconDirEntry.payloadSize); michael@0: } michael@0: michael@0: // The payload is a BMP, so we need to do some magic to get the decoder to do what we want. michael@0: // We construct an ICO containing just the image we want, and let Android do the rest. michael@0: byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.payloadSize]; michael@0: michael@0: // Set the type field in the ICO header. michael@0: decodeTarget[2] = 1; michael@0: michael@0: // Set the num-images field in the header to 1. michael@0: decodeTarget[4] = 1; michael@0: michael@0: // Copy the ICONDIRENTRY we need into the new buffer. michael@0: System.arraycopy(decodand, offset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES); michael@0: michael@0: // Copy the payload into the new buffer. michael@0: final int singlePayloadOffset = ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES; michael@0: System.arraycopy(decodand, offset + iconDirEntry.payloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.payloadSize); michael@0: michael@0: // Update the offset field of the ICONDIRENTRY to make the new ICO valid. michael@0: decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = (byte) singlePayloadOffset; michael@0: decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (byte) (singlePayloadOffset >>> 8); michael@0: decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (byte) (singlePayloadOffset >>> 16); michael@0: decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (byte) (singlePayloadOffset >>> 24); michael@0: michael@0: // Decode the newly-constructed singleton-ICO. michael@0: return BitmapUtils.decodeByteArray(decodeTarget); michael@0: } michael@0: michael@0: /** michael@0: * Fetch an iterator over the images in this ICO, or null if this ICO seems to be invalid. michael@0: * michael@0: * @return An iterator over the Bitmaps stored in this ICO, or null if decoding fails. michael@0: */ michael@0: @Override michael@0: public ICOIterator iterator() { michael@0: // If a previous call to decode concluded this ICO is invalid, abort. michael@0: if (hasDecoded && !isValid) { michael@0: return null; michael@0: } michael@0: michael@0: // If we've not been decoded before, but now fail to make any sense of the ICO, abort. michael@0: if (!hasDecoded) { michael@0: if (!decodeIconDirectoryAndPossiblyPrune()) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: // If decoding was a success, return an iterator over the images in this ICO. michael@0: return new ICOIterator(); michael@0: } michael@0: michael@0: /** michael@0: * Decode this ICO and return the result as a LoadFaviconResult. michael@0: * @return A LoadFaviconResult representing the decoded ICO. michael@0: */ michael@0: public LoadFaviconResult decode() { michael@0: // The call to iterator returns null if decoding fails. michael@0: Iterator bitmaps = iterator(); michael@0: if (bitmaps == null) { michael@0: return null; michael@0: } michael@0: michael@0: LoadFaviconResult result = new LoadFaviconResult(); michael@0: michael@0: result.bitmapsDecoded = bitmaps; michael@0: result.faviconBytes = decodand; michael@0: result.offset = offset; michael@0: result.length = len; michael@0: result.isICO = true; michael@0: michael@0: return result; michael@0: } michael@0: michael@0: /** michael@0: * Inner class to iterate over the elements in the ICO represented by the enclosing instance. michael@0: */ michael@0: private class ICOIterator implements Iterator { michael@0: private int mIndex = 0; michael@0: michael@0: @Override michael@0: public boolean hasNext() { michael@0: return mIndex < iconDirectory.length; michael@0: } michael@0: michael@0: @Override michael@0: public Bitmap next() { michael@0: if (mIndex > iconDirectory.length) { michael@0: throw new NoSuchElementException("No more elements in this ICO."); michael@0: } michael@0: return decodeBitmapAtIndex(mIndex++); michael@0: } michael@0: michael@0: @Override michael@0: public void remove() { michael@0: if (iconDirectory[mIndex] == null) { michael@0: throw new IllegalStateException("Remove already called for element " + mIndex); michael@0: } michael@0: iconDirectory[mIndex] = null; michael@0: } michael@0: } michael@0: }