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

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 package org.mozilla.gecko.favicons.decoders;
michael@0 6
michael@0 7 import android.graphics.Bitmap;
michael@0 8 import org.mozilla.gecko.favicons.Favicons;
michael@0 9 import org.mozilla.gecko.gfx.BitmapUtils;
michael@0 10
michael@0 11 import android.util.SparseArray;
michael@0 12
michael@0 13 import java.util.Iterator;
michael@0 14 import java.util.NoSuchElementException;
michael@0 15
michael@0 16 /**
michael@0 17 * Utility class for determining the region of a provided array which contains the largest bitmap,
michael@0 18 * assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning
michael@0 19 * unwanted entries from ICO files, if desired.
michael@0 20 *
michael@0 21 * An ICO file is a container format that may hold up to 255 images in either BMP or PNG format.
michael@0 22 * A mixture of image types may not exist.
michael@0 23 *
michael@0 24 * The format consists of a header specifying the number, n, of images, followed by the Icon Directory.
michael@0 25 *
michael@0 26 * The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for
michael@0 27 * the corresponding image, the dimensions, colour information, payload size, and location in the file.
michael@0 28 *
michael@0 29 * All numerical fields follow a little-endian byte ordering.
michael@0 30 *
michael@0 31 * Header format:
michael@0 32 *
michael@0 33 * 0 1 2 3
michael@0 34 * 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 35 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
michael@0 36 * | Reserved field. Must be zero | Type (1 for ICO, 2 for CUR) |
michael@0 37 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
michael@0 38 * | Image count (n) |
michael@0 39 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
michael@0 40 *
michael@0 41 * The type field is expected to always be 1. CUR format images should not be used for Favicons.
michael@0 42 *
michael@0 43 *
michael@0 44 * Icon Directory Entry format:
michael@0 45 *
michael@0 46 * 0 1 2 3
michael@0 47 * 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 48 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
michael@0 49 * | Image width | Image height | Palette size | Reserved (0) |
michael@0 50 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
michael@0 51 * | Colour plane count | Bits per pixel |
michael@0 52 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
michael@0 53 * | Size of image data, in bytes |
michael@0 54 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
michael@0 55 * | Start of image data, as an offset from start of file |
michael@0 56 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
michael@0 57 *
michael@0 58 * Image dimensions of zero are to be interpreted as image dimensions of 256.
michael@0 59 *
michael@0 60 * The palette size field records the number of colours in the stored BMP, if a palette is used. Zero
michael@0 61 * if the payload is a PNG or no palette is in use.
michael@0 62 *
michael@0 63 * The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be
michael@0 64 * interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field.
michael@0 65 * (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.)
michael@0 66 *
michael@0 67 *
michael@0 68 * The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps.
michael@0 69 *
michael@0 70 * This class is not thread safe.
michael@0 71 */
michael@0 72 public class ICODecoder implements Iterable<Bitmap> {
michael@0 73 // The number of bytes that compacting will save for us to bother doing it.
michael@0 74 public static final int COMPACT_THRESHOLD = 4000;
michael@0 75
michael@0 76 // Some geometry of an ICO file.
michael@0 77 public static final int ICO_HEADER_LENGTH_BYTES = 6;
michael@0 78 public static final int ICO_ICONDIRENTRY_LENGTH_BYTES = 16;
michael@0 79
michael@0 80 // The buffer containing bytes to attempt to decode.
michael@0 81 private byte[] decodand;
michael@0 82
michael@0 83 // The region of the decodand to decode.
michael@0 84 private int offset;
michael@0 85 private int len;
michael@0 86
michael@0 87 private IconDirectoryEntry[] iconDirectory;
michael@0 88 private boolean isValid;
michael@0 89 private boolean hasDecoded;
michael@0 90
michael@0 91 public ICODecoder(byte[] decodand, int offset, int len) {
michael@0 92 this.decodand = decodand;
michael@0 93 this.offset = offset;
michael@0 94 this.len = len;
michael@0 95 }
michael@0 96
michael@0 97 /**
michael@0 98 * Decode the Icon Directory for this ICO and store the result in iconDirectory.
michael@0 99 *
michael@0 100 * @return true if ICO decoding was considered to probably be a success, false if it certainly
michael@0 101 * was a failure.
michael@0 102 */
michael@0 103 private boolean decodeIconDirectoryAndPossiblyPrune() {
michael@0 104 hasDecoded = true;
michael@0 105
michael@0 106 // Fail if the end of the described range is out of bounds.
michael@0 107 if (offset + len > decodand.length) {
michael@0 108 return false;
michael@0 109 }
michael@0 110
michael@0 111 // Fail if we don't have enough space for the header.
michael@0 112 if (len < ICO_HEADER_LENGTH_BYTES) {
michael@0 113 return false;
michael@0 114 }
michael@0 115
michael@0 116 // Check that the reserved fields in the header are indeed zero, and that the type field
michael@0 117 // specifies ICO. If not, we've probably been given something that isn't really an ICO.
michael@0 118 if (decodand[offset] != 0 ||
michael@0 119 decodand[offset + 1] != 0 ||
michael@0 120 decodand[offset + 2] != 1 ||
michael@0 121 decodand[offset + 3] != 0) {
michael@0 122 return false;
michael@0 123 }
michael@0 124
michael@0 125 // Here, and in many other places, byte values are ANDed with 0xFF. This is because Java
michael@0 126 // bytes are signed - to obtain a numerical value of a longer type which holds the unsigned
michael@0 127 // interpretation of the byte of interest, we do this.
michael@0 128 int numEncodedImages = (decodand[offset + 4] & 0xFF) |
michael@0 129 (decodand[offset + 5] & 0xFF) << 8;
michael@0 130
michael@0 131
michael@0 132 // Fail if there are no images or the field is corrupt.
michael@0 133 if (numEncodedImages <= 0) {
michael@0 134 return false;
michael@0 135 }
michael@0 136
michael@0 137 final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES);
michael@0 138
michael@0 139 // Fail if there is not enough space in the buffer for the stated number of icondir entries,
michael@0 140 // let alone the data.
michael@0 141 if (len < headerAndDirectorySize) {
michael@0 142 return false;
michael@0 143 }
michael@0 144
michael@0 145 // Put the pointer on the first byte of the first Icon Directory Entry.
michael@0 146 int bufferIndex = offset + ICO_HEADER_LENGTH_BYTES;
michael@0 147
michael@0 148 // We now iterate over the Icon Directory, decoding each entry as we go. We also need to
michael@0 149 // discard all entries except one >= the maximum interesting size.
michael@0 150
michael@0 151 // Size of the smallest image larger than the limit encountered.
michael@0 152 int minimumMaximum = Integer.MAX_VALUE;
michael@0 153
michael@0 154 // Used to track the best entry for each size. The entries we want to keep.
michael@0 155 SparseArray<IconDirectoryEntry> preferenceArray = new SparseArray<IconDirectoryEntry>();
michael@0 156
michael@0 157 for (int i = 0; i < numEncodedImages; i++, bufferIndex += ICO_ICONDIRENTRY_LENGTH_BYTES) {
michael@0 158 // Decode the Icon Directory Entry at this offset.
michael@0 159 IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(decodand, offset, len, bufferIndex);
michael@0 160 newEntry.index = i;
michael@0 161
michael@0 162 if (newEntry.isErroneous) {
michael@0 163 continue;
michael@0 164 }
michael@0 165
michael@0 166 if (newEntry.width > Favicons.largestFaviconSize) {
michael@0 167 // If we already have a smaller image larger than the maximum size of interest, we
michael@0 168 // don't care about the new one which is larger than the smallest image larger than
michael@0 169 // the maximum size.
michael@0 170 if (newEntry.width >= minimumMaximum) {
michael@0 171 continue;
michael@0 172 }
michael@0 173
michael@0 174 // Remove the previous minimum-maximum.
michael@0 175 preferenceArray.delete(minimumMaximum);
michael@0 176
michael@0 177 minimumMaximum = newEntry.width;
michael@0 178 }
michael@0 179
michael@0 180 IconDirectoryEntry oldEntry = preferenceArray.get(newEntry.width);
michael@0 181 if (oldEntry == null) {
michael@0 182 preferenceArray.put(newEntry.width, newEntry);
michael@0 183 continue;
michael@0 184 }
michael@0 185
michael@0 186 if (oldEntry.compareTo(newEntry) < 0) {
michael@0 187 preferenceArray.put(newEntry.width, newEntry);
michael@0 188 }
michael@0 189 }
michael@0 190
michael@0 191 final int count = preferenceArray.size();
michael@0 192
michael@0 193 // Abort if no entries are desired (Perhaps all are corrupt?)
michael@0 194 if (count == 0) {
michael@0 195 return false;
michael@0 196 }
michael@0 197
michael@0 198 // Allocate space for the icon directory entries in the decoded directory.
michael@0 199 iconDirectory = new IconDirectoryEntry[count];
michael@0 200
michael@0 201 // The size of the data in the buffer that we find useful.
michael@0 202 int retainedSpace = ICO_HEADER_LENGTH_BYTES;
michael@0 203
michael@0 204 for (int i = 0; i < count; i++) {
michael@0 205 IconDirectoryEntry e = preferenceArray.valueAt(i);
michael@0 206 retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.payloadSize;
michael@0 207 iconDirectory[i] = e;
michael@0 208 }
michael@0 209
michael@0 210 isValid = true;
michael@0 211
michael@0 212 // Set the number of images field in the buffer to reflect the number of retained entries.
michael@0 213 decodand[offset + 4] = (byte) iconDirectory.length;
michael@0 214 decodand[offset + 5] = (byte) (iconDirectory.length >>> 8);
michael@0 215
michael@0 216 if ((len - retainedSpace) > COMPACT_THRESHOLD) {
michael@0 217 compactingCopy(retainedSpace);
michael@0 218 }
michael@0 219
michael@0 220 return true;
michael@0 221 }
michael@0 222
michael@0 223 /**
michael@0 224 * Copy the buffer into a new array of exactly the required size, omitting any unwanted data.
michael@0 225 */
michael@0 226 private void compactingCopy(int spaceRetained) {
michael@0 227 byte[] buf = new byte[spaceRetained];
michael@0 228
michael@0 229 // Copy the header.
michael@0 230 System.arraycopy(decodand, offset, buf, 0, ICO_HEADER_LENGTH_BYTES);
michael@0 231
michael@0 232 int headerPtr = ICO_HEADER_LENGTH_BYTES;
michael@0 233
michael@0 234 int payloadPtr = ICO_HEADER_LENGTH_BYTES + (iconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES);
michael@0 235
michael@0 236 int ind = 0;
michael@0 237 for (IconDirectoryEntry entry : iconDirectory) {
michael@0 238 // Copy this entry.
michael@0 239 System.arraycopy(decodand, offset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES);
michael@0 240
michael@0 241 // Copy its payload.
michael@0 242 System.arraycopy(decodand, offset + entry.payloadOffset, buf, payloadPtr, entry.payloadSize);
michael@0 243
michael@0 244 // Update the offset field.
michael@0 245 buf[headerPtr + 12] = (byte) payloadPtr;
michael@0 246 buf[headerPtr + 13] = (byte) (payloadPtr >>> 8);
michael@0 247 buf[headerPtr + 14] = (byte) (payloadPtr >>> 16);
michael@0 248 buf[headerPtr + 15] = (byte) (payloadPtr >>> 24);
michael@0 249
michael@0 250 entry.payloadOffset = payloadPtr;
michael@0 251 entry.index = ind;
michael@0 252
michael@0 253 payloadPtr += entry.payloadSize;
michael@0 254 headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES;
michael@0 255 ind++;
michael@0 256 }
michael@0 257
michael@0 258 decodand = buf;
michael@0 259 offset = 0;
michael@0 260 len = spaceRetained;
michael@0 261 }
michael@0 262
michael@0 263 /**
michael@0 264 * Decode and return the bitmap represented by the given index in the Icon Directory, if valid.
michael@0 265 *
michael@0 266 * @param index The index into the Icon Directory of the image of interest.
michael@0 267 * @return The decoded Bitmap object for this image, or null if the entry is invalid or decoding
michael@0 268 * fails.
michael@0 269 */
michael@0 270 public Bitmap decodeBitmapAtIndex(int index) {
michael@0 271 final IconDirectoryEntry iconDirEntry = iconDirectory[index];
michael@0 272
michael@0 273 if (iconDirEntry.payloadIsPNG) {
michael@0 274 // PNG payload. Simply extract it and decode it.
michael@0 275 return BitmapUtils.decodeByteArray(decodand, offset + iconDirEntry.payloadOffset, iconDirEntry.payloadSize);
michael@0 276 }
michael@0 277
michael@0 278 // The payload is a BMP, so we need to do some magic to get the decoder to do what we want.
michael@0 279 // We construct an ICO containing just the image we want, and let Android do the rest.
michael@0 280 byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.payloadSize];
michael@0 281
michael@0 282 // Set the type field in the ICO header.
michael@0 283 decodeTarget[2] = 1;
michael@0 284
michael@0 285 // Set the num-images field in the header to 1.
michael@0 286 decodeTarget[4] = 1;
michael@0 287
michael@0 288 // Copy the ICONDIRENTRY we need into the new buffer.
michael@0 289 System.arraycopy(decodand, offset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES);
michael@0 290
michael@0 291 // Copy the payload into the new buffer.
michael@0 292 final int singlePayloadOffset = ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES;
michael@0 293 System.arraycopy(decodand, offset + iconDirEntry.payloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.payloadSize);
michael@0 294
michael@0 295 // Update the offset field of the ICONDIRENTRY to make the new ICO valid.
michael@0 296 decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = (byte) singlePayloadOffset;
michael@0 297 decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (byte) (singlePayloadOffset >>> 8);
michael@0 298 decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (byte) (singlePayloadOffset >>> 16);
michael@0 299 decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (byte) (singlePayloadOffset >>> 24);
michael@0 300
michael@0 301 // Decode the newly-constructed singleton-ICO.
michael@0 302 return BitmapUtils.decodeByteArray(decodeTarget);
michael@0 303 }
michael@0 304
michael@0 305 /**
michael@0 306 * Fetch an iterator over the images in this ICO, or null if this ICO seems to be invalid.
michael@0 307 *
michael@0 308 * @return An iterator over the Bitmaps stored in this ICO, or null if decoding fails.
michael@0 309 */
michael@0 310 @Override
michael@0 311 public ICOIterator iterator() {
michael@0 312 // If a previous call to decode concluded this ICO is invalid, abort.
michael@0 313 if (hasDecoded && !isValid) {
michael@0 314 return null;
michael@0 315 }
michael@0 316
michael@0 317 // If we've not been decoded before, but now fail to make any sense of the ICO, abort.
michael@0 318 if (!hasDecoded) {
michael@0 319 if (!decodeIconDirectoryAndPossiblyPrune()) {
michael@0 320 return null;
michael@0 321 }
michael@0 322 }
michael@0 323
michael@0 324 // If decoding was a success, return an iterator over the images in this ICO.
michael@0 325 return new ICOIterator();
michael@0 326 }
michael@0 327
michael@0 328 /**
michael@0 329 * Decode this ICO and return the result as a LoadFaviconResult.
michael@0 330 * @return A LoadFaviconResult representing the decoded ICO.
michael@0 331 */
michael@0 332 public LoadFaviconResult decode() {
michael@0 333 // The call to iterator returns null if decoding fails.
michael@0 334 Iterator<Bitmap> bitmaps = iterator();
michael@0 335 if (bitmaps == null) {
michael@0 336 return null;
michael@0 337 }
michael@0 338
michael@0 339 LoadFaviconResult result = new LoadFaviconResult();
michael@0 340
michael@0 341 result.bitmapsDecoded = bitmaps;
michael@0 342 result.faviconBytes = decodand;
michael@0 343 result.offset = offset;
michael@0 344 result.length = len;
michael@0 345 result.isICO = true;
michael@0 346
michael@0 347 return result;
michael@0 348 }
michael@0 349
michael@0 350 /**
michael@0 351 * Inner class to iterate over the elements in the ICO represented by the enclosing instance.
michael@0 352 */
michael@0 353 private class ICOIterator implements Iterator<Bitmap> {
michael@0 354 private int mIndex = 0;
michael@0 355
michael@0 356 @Override
michael@0 357 public boolean hasNext() {
michael@0 358 return mIndex < iconDirectory.length;
michael@0 359 }
michael@0 360
michael@0 361 @Override
michael@0 362 public Bitmap next() {
michael@0 363 if (mIndex > iconDirectory.length) {
michael@0 364 throw new NoSuchElementException("No more elements in this ICO.");
michael@0 365 }
michael@0 366 return decodeBitmapAtIndex(mIndex++);
michael@0 367 }
michael@0 368
michael@0 369 @Override
michael@0 370 public void remove() {
michael@0 371 if (iconDirectory[mIndex] == null) {
michael@0 372 throw new IllegalStateException("Remove already called for element " + mIndex);
michael@0 373 }
michael@0 374 iconDirectory[mIndex] = null;
michael@0 375 }
michael@0 376 }
michael@0 377 }

mercurial