summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java
blob: 44e3f1252b489c49e29794362a5e6384d23421df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
/* 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.icons.decoders;

import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.VisibleForTesting;
import android.util.SparseArray;

import java.util.Iterator;
import java.util.NoSuchElementException;

import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.R;

/**
 * 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;

    IconDirectoryEntry[] iconDirectory;
    private boolean isValid;
    private boolean hasDecoded;
    private int largestFaviconSize;

    @RobocopTarget
    public ICODecoder(Context context, byte[] decodand, int offset, int len) {
        this.decodand = decodand;
        this.offset = offset;
        this.len = len;
        this.largestFaviconSize = context.getResources()
                .getDimensionPixelSize(R.dimen.favicon_largest_interesting_size);
    }

    /**
     * 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 > 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] = singlePayloadOffset;
        decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (singlePayloadOffset >>> 8);
        decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (singlePayloadOffset >>> 16);
        decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (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;
    }

    @VisibleForTesting
    @RobocopTarget
    public IconDirectoryEntry[] getIconDirectory() {
        return iconDirectory;
    }

    @VisibleForTesting
    @RobocopTarget
    public int getLargestFaviconSize() {
        return largestFaviconSize;
    }

    /**
     * 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;
        }
    }
}