mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
378 lines
14 KiB
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;
|
|
}
|
|
}
|
|
}
|