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.
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 }