Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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 | } |