|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.favicons.decoders; |
|
6 |
|
7 import android.graphics.Bitmap; |
|
8 import org.mozilla.gecko.favicons.Favicons; |
|
9 import org.mozilla.gecko.gfx.BitmapUtils; |
|
10 |
|
11 import android.util.SparseArray; |
|
12 |
|
13 import java.util.Iterator; |
|
14 import java.util.NoSuchElementException; |
|
15 |
|
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; |
|
75 |
|
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; |
|
79 |
|
80 // The buffer containing bytes to attempt to decode. |
|
81 private byte[] decodand; |
|
82 |
|
83 // The region of the decodand to decode. |
|
84 private int offset; |
|
85 private int len; |
|
86 |
|
87 private IconDirectoryEntry[] iconDirectory; |
|
88 private boolean isValid; |
|
89 private boolean hasDecoded; |
|
90 |
|
91 public ICODecoder(byte[] decodand, int offset, int len) { |
|
92 this.decodand = decodand; |
|
93 this.offset = offset; |
|
94 this.len = len; |
|
95 } |
|
96 |
|
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; |
|
105 |
|
106 // Fail if the end of the described range is out of bounds. |
|
107 if (offset + len > decodand.length) { |
|
108 return false; |
|
109 } |
|
110 |
|
111 // Fail if we don't have enough space for the header. |
|
112 if (len < ICO_HEADER_LENGTH_BYTES) { |
|
113 return false; |
|
114 } |
|
115 |
|
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 } |
|
124 |
|
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; |
|
130 |
|
131 |
|
132 // Fail if there are no images or the field is corrupt. |
|
133 if (numEncodedImages <= 0) { |
|
134 return false; |
|
135 } |
|
136 |
|
137 final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES); |
|
138 |
|
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 } |
|
144 |
|
145 // Put the pointer on the first byte of the first Icon Directory Entry. |
|
146 int bufferIndex = offset + ICO_HEADER_LENGTH_BYTES; |
|
147 |
|
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. |
|
150 |
|
151 // Size of the smallest image larger than the limit encountered. |
|
152 int minimumMaximum = Integer.MAX_VALUE; |
|
153 |
|
154 // Used to track the best entry for each size. The entries we want to keep. |
|
155 SparseArray<IconDirectoryEntry> preferenceArray = new SparseArray<IconDirectoryEntry>(); |
|
156 |
|
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; |
|
161 |
|
162 if (newEntry.isErroneous) { |
|
163 continue; |
|
164 } |
|
165 |
|
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 } |
|
173 |
|
174 // Remove the previous minimum-maximum. |
|
175 preferenceArray.delete(minimumMaximum); |
|
176 |
|
177 minimumMaximum = newEntry.width; |
|
178 } |
|
179 |
|
180 IconDirectoryEntry oldEntry = preferenceArray.get(newEntry.width); |
|
181 if (oldEntry == null) { |
|
182 preferenceArray.put(newEntry.width, newEntry); |
|
183 continue; |
|
184 } |
|
185 |
|
186 if (oldEntry.compareTo(newEntry) < 0) { |
|
187 preferenceArray.put(newEntry.width, newEntry); |
|
188 } |
|
189 } |
|
190 |
|
191 final int count = preferenceArray.size(); |
|
192 |
|
193 // Abort if no entries are desired (Perhaps all are corrupt?) |
|
194 if (count == 0) { |
|
195 return false; |
|
196 } |
|
197 |
|
198 // Allocate space for the icon directory entries in the decoded directory. |
|
199 iconDirectory = new IconDirectoryEntry[count]; |
|
200 |
|
201 // The size of the data in the buffer that we find useful. |
|
202 int retainedSpace = ICO_HEADER_LENGTH_BYTES; |
|
203 |
|
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 } |
|
209 |
|
210 isValid = true; |
|
211 |
|
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); |
|
215 |
|
216 if ((len - retainedSpace) > COMPACT_THRESHOLD) { |
|
217 compactingCopy(retainedSpace); |
|
218 } |
|
219 |
|
220 return true; |
|
221 } |
|
222 |
|
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]; |
|
228 |
|
229 // Copy the header. |
|
230 System.arraycopy(decodand, offset, buf, 0, ICO_HEADER_LENGTH_BYTES); |
|
231 |
|
232 int headerPtr = ICO_HEADER_LENGTH_BYTES; |
|
233 |
|
234 int payloadPtr = ICO_HEADER_LENGTH_BYTES + (iconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES); |
|
235 |
|
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); |
|
240 |
|
241 // Copy its payload. |
|
242 System.arraycopy(decodand, offset + entry.payloadOffset, buf, payloadPtr, entry.payloadSize); |
|
243 |
|
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); |
|
249 |
|
250 entry.payloadOffset = payloadPtr; |
|
251 entry.index = ind; |
|
252 |
|
253 payloadPtr += entry.payloadSize; |
|
254 headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES; |
|
255 ind++; |
|
256 } |
|
257 |
|
258 decodand = buf; |
|
259 offset = 0; |
|
260 len = spaceRetained; |
|
261 } |
|
262 |
|
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]; |
|
272 |
|
273 if (iconDirEntry.payloadIsPNG) { |
|
274 // PNG payload. Simply extract it and decode it. |
|
275 return BitmapUtils.decodeByteArray(decodand, offset + iconDirEntry.payloadOffset, iconDirEntry.payloadSize); |
|
276 } |
|
277 |
|
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]; |
|
281 |
|
282 // Set the type field in the ICO header. |
|
283 decodeTarget[2] = 1; |
|
284 |
|
285 // Set the num-images field in the header to 1. |
|
286 decodeTarget[4] = 1; |
|
287 |
|
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); |
|
290 |
|
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); |
|
294 |
|
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); |
|
300 |
|
301 // Decode the newly-constructed singleton-ICO. |
|
302 return BitmapUtils.decodeByteArray(decodeTarget); |
|
303 } |
|
304 |
|
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 } |
|
316 |
|
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 } |
|
323 |
|
324 // If decoding was a success, return an iterator over the images in this ICO. |
|
325 return new ICOIterator(); |
|
326 } |
|
327 |
|
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 } |
|
338 |
|
339 LoadFaviconResult result = new LoadFaviconResult(); |
|
340 |
|
341 result.bitmapsDecoded = bitmaps; |
|
342 result.faviconBytes = decodand; |
|
343 result.offset = offset; |
|
344 result.length = len; |
|
345 result.isICO = true; |
|
346 |
|
347 return result; |
|
348 } |
|
349 |
|
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; |
|
355 |
|
356 @Override |
|
357 public boolean hasNext() { |
|
358 return mIndex < iconDirectory.length; |
|
359 } |
|
360 |
|
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 } |
|
368 |
|
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 } |