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

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/favicons/decoders/FaviconDecoder.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,240 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +package org.mozilla.gecko.favicons.decoders;
     1.9 +
    1.10 +import android.graphics.Bitmap;
    1.11 +import android.util.Base64;
    1.12 +import android.util.Log;
    1.13 +
    1.14 +import org.mozilla.gecko.gfx.BitmapUtils;
    1.15 +
    1.16 +import java.util.Iterator;
    1.17 +import java.util.NoSuchElementException;
    1.18 +
    1.19 +/**
    1.20 + * Class providing static utility methods for decoding favicons.
    1.21 + */
    1.22 +public class FaviconDecoder {
    1.23 +    private static final String LOG_TAG = "GeckoFaviconDecoder";
    1.24 +
    1.25 +    static enum ImageMagicNumbers {
    1.26 +        // It is irritating that Java bytes are signed...
    1.27 +        PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}),
    1.28 +        GIF(new byte[] {0x47, 0x49, 0x46, 0x38}),
    1.29 +        JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}),
    1.30 +        BMP(new byte[] {0x42, 0x4d}),
    1.31 +        WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a});
    1.32 +
    1.33 +        public byte[] value;
    1.34 +
    1.35 +        private ImageMagicNumbers(byte[] value) {
    1.36 +            this.value = value;
    1.37 +        }
    1.38 +    }
    1.39 +
    1.40 +    /**
    1.41 +     * Check for image format magic numbers of formats supported by Android.
    1.42 +     * @param buffer Byte buffer to check for magic numbers
    1.43 +     * @param offset Offset at which to look for magic numbers.
    1.44 +     * @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence
    1.45 +     *         starting with the magic numbers thereof). false otherwise.
    1.46 +     */
    1.47 +    private static boolean isDecodableByAndroid(byte[] buffer, int offset) {
    1.48 +        for (ImageMagicNumbers m : ImageMagicNumbers.values()) {
    1.49 +            if (bufferStartsWith(buffer, m.value, offset)) {
    1.50 +                return true;
    1.51 +            }
    1.52 +        }
    1.53 +
    1.54 +        return false;
    1.55 +    }
    1.56 +
    1.57 +    /**
    1.58 +     * Utility function to check for the existence of a test byte sequence at a given offset in a
    1.59 +     * buffer.
    1.60 +     *
    1.61 +     * @param buffer Byte buffer to search.
    1.62 +     * @param test Byte sequence to search for.
    1.63 +     * @param bufferOffset Index in input buffer to expect test sequence.
    1.64 +     * @return true if buffer contains the byte sequence given in test at offset bufferOffset, false
    1.65 +     *         otherwise.
    1.66 +     */
    1.67 +    static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) {
    1.68 +        if (buffer.length < test.length) {
    1.69 +            return false;
    1.70 +        }
    1.71 +
    1.72 +        for (int i = 0; i < test.length; ++i) {
    1.73 +            if (buffer[bufferOffset + i] != test[i]) {
    1.74 +                return false;
    1.75 +            }
    1.76 +        }
    1.77 +        return true;
    1.78 +    }
    1.79 +
    1.80 +    /**
    1.81 +     * Decode the favicon present in the region of the provided byte[] starting at offset and
    1.82 +     * proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the
    1.83 +     * given range does not contain a bitmap we know how to decode.
    1.84 +     *
    1.85 +     * @param buffer Byte array containing the favicon to decode.
    1.86 +     * @param offset The index of the first byte in the array of the region of interest.
    1.87 +     * @param length The length of the region in the array to decode.
    1.88 +     * @return The decoded version of the bitmap in the described region, or null if none can be
    1.89 +     *         decoded.
    1.90 +     */
    1.91 +    public static LoadFaviconResult decodeFavicon(byte[] buffer, int offset, int length) {
    1.92 +        LoadFaviconResult result;
    1.93 +        if (isDecodableByAndroid(buffer, offset)) {
    1.94 +            result = new LoadFaviconResult();
    1.95 +            result.offset = offset;
    1.96 +            result.length = length;
    1.97 +            result.isICO = false;
    1.98 +
    1.99 +            Bitmap decodedImage = BitmapUtils.decodeByteArray(buffer, offset, length);
   1.100 +            if (decodedImage == null) {
   1.101 +                // What we got wasn't decodable after all. Probably corrupted image, or we got a muffled OOM.
   1.102 +                return null;
   1.103 +            }
   1.104 +
   1.105 +            // We assume here that decodeByteArray doesn't hold on to the entire supplied
   1.106 +            // buffer -- worst case, each of our buffers will be twice the necessary size.
   1.107 +            result.bitmapsDecoded = new SingleBitmapIterator(decodedImage);
   1.108 +            result.faviconBytes = buffer;
   1.109 +
   1.110 +            return result;
   1.111 +        }
   1.112 +
   1.113 +        // If it's not decodable by Android, it might be an ICO. Let's try.
   1.114 +        ICODecoder decoder = new ICODecoder(buffer, offset, length);
   1.115 +
   1.116 +        result = decoder.decode();
   1.117 +
   1.118 +        if (result == null) {
   1.119 +            return null;
   1.120 +        }
   1.121 +
   1.122 +        return result;
   1.123 +    }
   1.124 +
   1.125 +    public static LoadFaviconResult decodeDataURI(String uri) {
   1.126 +        if (uri == null) {
   1.127 +            Log.w(LOG_TAG, "Can't decode null data: URI.");
   1.128 +            return null;
   1.129 +        }
   1.130 +
   1.131 +        if (!uri.startsWith("data:image/")) {
   1.132 +            Log.w(LOG_TAG, "Can't decode non-image data: URI.");
   1.133 +            return null;
   1.134 +        }
   1.135 +
   1.136 +        // Otherwise, let's attack this blindly. Strictly we should be parsing.
   1.137 +        int offset = uri.indexOf(',') + 1;
   1.138 +        if (offset == 0) {
   1.139 +            Log.w(LOG_TAG, "No ',' in data: URI; malformed?");
   1.140 +            return null;
   1.141 +        }
   1.142 +
   1.143 +        try {
   1.144 +            String base64 = uri.substring(offset);
   1.145 +            byte[] raw = Base64.decode(base64, Base64.DEFAULT);
   1.146 +            return decodeFavicon(raw);
   1.147 +        } catch (Exception e) {
   1.148 +            Log.w(LOG_TAG, "Couldn't decode data: URI.", e);
   1.149 +            return null;
   1.150 +        }
   1.151 +    }
   1.152 +
   1.153 +    public static LoadFaviconResult decodeFavicon(byte[] buffer) {
   1.154 +        return decodeFavicon(buffer, 0, buffer.length);
   1.155 +    }
   1.156 +
   1.157 +    /**
   1.158 +     * Returns the smallest bitmap in the icon represented by the provided
   1.159 +     * data: URI that's larger than the desired width, or the largest if
   1.160 +     * there is no larger icon.
   1.161 +     *
   1.162 +     * Returns null if no bitmap could be extracted.
   1.163 +     *
   1.164 +     * Bug 961600: we shouldn't be doing all of this work. The favicon cache
   1.165 +     * should be used, and will give us the right size icon.
   1.166 +     */
   1.167 +    public static Bitmap getMostSuitableBitmapFromDataURI(String iconURI, int desiredWidth) {
   1.168 +        LoadFaviconResult result = FaviconDecoder.decodeDataURI(iconURI);
   1.169 +        if (result == null) {
   1.170 +            // Nothing we can do.
   1.171 +            Log.w(LOG_TAG, "Unable to decode icon URI.");
   1.172 +            return null;
   1.173 +        }
   1.174 +
   1.175 +        final Iterator<Bitmap> bitmaps = result.getBitmaps();
   1.176 +        if (!bitmaps.hasNext()) {
   1.177 +            Log.w(LOG_TAG, "No bitmaps in decoded icon.");
   1.178 +            return null;
   1.179 +        }
   1.180 +
   1.181 +        Bitmap bitmap = bitmaps.next();
   1.182 +        if (!bitmaps.hasNext()) {
   1.183 +            // We're done! There was only one, so this is as big as it gets.
   1.184 +            return bitmap;
   1.185 +        }
   1.186 +
   1.187 +        // Find a bitmap of the most suitable size.
   1.188 +        int currentWidth = bitmap.getWidth();
   1.189 +        while ((currentWidth < desiredWidth) &&
   1.190 +               bitmaps.hasNext()) {
   1.191 +            final Bitmap b = bitmaps.next();
   1.192 +            if (b.getWidth() > currentWidth) {
   1.193 +                currentWidth = b.getWidth();
   1.194 +                bitmap = b;
   1.195 +            }
   1.196 +        }
   1.197 +
   1.198 +        return bitmap;
   1.199 +    }
   1.200 +
   1.201 +    /**
   1.202 +     * Iterator to hold a single bitmap.
   1.203 +     */
   1.204 +    static class SingleBitmapIterator implements Iterator<Bitmap> {
   1.205 +        private Bitmap bitmap;
   1.206 +
   1.207 +        public SingleBitmapIterator(Bitmap b) {
   1.208 +            bitmap = b;
   1.209 +        }
   1.210 +
   1.211 +        /**
   1.212 +         * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure
   1.213 +         * places where the runtime type of the Iterator under consideration is known and
   1.214 +         * destruction of it is discouraged.
   1.215 +         *
   1.216 +         * @return The bitmap carried by this SingleBitmapIterator.
   1.217 +         */
   1.218 +        public Bitmap peek() {
   1.219 +            return bitmap;
   1.220 +        }
   1.221 +
   1.222 +        @Override
   1.223 +        public boolean hasNext() {
   1.224 +            return bitmap != null;
   1.225 +        }
   1.226 +
   1.227 +        @Override
   1.228 +        public Bitmap next() {
   1.229 +            if (bitmap == null) {
   1.230 +                throw new NoSuchElementException("Element already returned from SingleBitmapIterator.");
   1.231 +            }
   1.232 +
   1.233 +            Bitmap ret = bitmap;
   1.234 +            bitmap = null;
   1.235 +            return ret;
   1.236 +        }
   1.237 +
   1.238 +        @Override
   1.239 +        public void remove() {
   1.240 +            throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator.");
   1.241 +        }
   1.242 +    }
   1.243 +}

mercurial