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

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial