mobile/android/base/gfx/BitmapUtils.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko.gfx;
     8 import java.io.IOException;
     9 import java.io.InputStream;
    10 import java.lang.reflect.Field;
    11 import java.net.MalformedURLException;
    12 import java.net.URL;
    14 import org.mozilla.gecko.R;
    15 import org.mozilla.gecko.util.GeckoJarReader;
    16 import org.mozilla.gecko.util.ThreadUtils;
    17 import org.mozilla.gecko.util.UiAsyncTask;
    18 import org.mozilla.gecko.Tab;
    19 import org.mozilla.gecko.Tabs;
    20 import org.mozilla.gecko.ThumbnailHelper;
    22 import android.content.Context;
    23 import android.content.res.Resources;
    24 import android.graphics.Bitmap;
    25 import android.graphics.BitmapFactory;
    26 import android.graphics.Canvas;
    27 import android.graphics.Color;
    28 import android.graphics.drawable.BitmapDrawable;
    29 import android.graphics.drawable.Drawable;
    30 import android.net.Uri;
    31 import android.text.TextUtils;
    32 import android.util.Base64;
    33 import android.util.Log;
    35 public final class BitmapUtils {
    36     private static final String LOGTAG = "GeckoBitmapUtils";
    38     private BitmapUtils() {}
    40     public interface BitmapLoader {
    41         public void onBitmapFound(Drawable d);
    42     }
    44     private static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) {
    45         if (ThreadUtils.isOnUiThread()) {
    46             loader.onBitmapFound(d);
    47             return;
    48         }
    50         ThreadUtils.postToUiThread(new Runnable() {
    51             @Override
    52             public void run() {
    53                 loader.onBitmapFound(d);
    54             }
    55         });
    56     }
    58     /**
    59      * Attempts to find a drawable associated with a given string, using its URI scheme to determine
    60      * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and
    61      * will be called with `null` if no drawable is found.
    62      *
    63      * The BitmapLoader `onBitmapFound` method always runs on the UI thread.
    64      */
    65     public static void getDrawable(final Context context, final String data, final BitmapLoader loader) {
    66         if (TextUtils.isEmpty(data)) {
    67             runOnBitmapFoundOnUiThread(loader, null);
    68             return;
    69         }
    71         if (data.startsWith("data")) {
    72             final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data));
    73             runOnBitmapFoundOnUiThread(loader, d);
    74             return;
    75         }
    77         if (data.startsWith("thumbnail:")) {
    78             getThumbnailDrawable(context, data, loader);
    79             return;
    80         }
    82         if (data.startsWith("jar:") || data.startsWith("file://")) {
    83             (new UiAsyncTask<Void, Void, Drawable>(ThreadUtils.getBackgroundHandler()) {
    84                 @Override
    85                 public Drawable doInBackground(Void... params) {
    86                     try {
    87                         if (data.startsWith("jar:jar")) {
    88                             return GeckoJarReader.getBitmapDrawable(context.getResources(), data);
    89                         }
    91                         // Don't attempt to validate the JAR signature when loading an add-on icon
    92                         if (data.startsWith("jar:file")) {
    93                             return GeckoJarReader.getBitmapDrawable(context.getResources(), Uri.decode(data));
    94                         }
    96                         final URL url = new URL(data);
    97                         final InputStream is = (InputStream) url.getContent();
    98                         try {
    99                             return Drawable.createFromStream(is, "src");
   100                         } finally {
   101                             is.close();
   102                         }
   103                     } catch (Exception e) {
   104                         Log.w(LOGTAG, "Unable to set icon", e);
   105                     }
   106                     return null;
   107                 }
   109                 @Override
   110                 public void onPostExecute(Drawable drawable) {
   111                     loader.onBitmapFound(drawable);
   112                 }
   113             }).execute();
   114             return;
   115         }
   117         if (data.startsWith("-moz-icon://")) {
   118             final Uri imageUri = Uri.parse(data);
   119             final String ssp = imageUri.getSchemeSpecificPart();
   120             final String resource = ssp.substring(ssp.lastIndexOf('/') + 1);
   122             try {
   123                 final Drawable d = context.getPackageManager().getApplicationIcon(resource);
   124                 runOnBitmapFoundOnUiThread(loader, d);
   125             } catch(Exception ex) { }
   127             return;
   128         }
   130         if (data.startsWith("drawable://")) {
   131             final Uri imageUri = Uri.parse(data);
   132             final int id = getResource(imageUri, R.drawable.ic_status_logo);
   133             final Drawable d = context.getResources().getDrawable(id);
   135             runOnBitmapFoundOnUiThread(loader, d);
   136             return;
   137         }
   139         runOnBitmapFoundOnUiThread(loader, null);
   140     }
   142     public static void getThumbnailDrawable(final Context context, final String data, final BitmapLoader loader) {
   143          int id = Integer.parseInt(data.substring(10), 10);
   144          final Tab tab = Tabs.getInstance().getTab(id);
   145          runOnBitmapFoundOnUiThread(loader, tab.getThumbnail());
   146          Tabs.registerOnTabsChangedListener(new Tabs.OnTabsChangedListener() {
   147                  public void onTabChanged(Tab t, Tabs.TabEvents msg, Object data) {
   148                      if (tab == t && msg == Tabs.TabEvents.THUMBNAIL) {
   149                          Tabs.unregisterOnTabsChangedListener(this);
   150                          runOnBitmapFoundOnUiThread(loader, t.getThumbnail());
   151                      }
   152                  }
   153              });
   154          ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab);
   155     }
   157     public static Bitmap decodeByteArray(byte[] bytes) {
   158         return decodeByteArray(bytes, null);
   159     }
   161     public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
   162         return decodeByteArray(bytes, 0, bytes.length, options);
   163     }
   165     public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
   166         return decodeByteArray(bytes, offset, length, null);
   167     }
   169     public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) {
   170         if (bytes.length <= 0) {
   171             throw new IllegalArgumentException("bytes.length " + bytes.length
   172                                                + " must be a positive number");
   173         }
   175         Bitmap bitmap = null;
   176         try {
   177             bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options);
   178         } catch (OutOfMemoryError e) {
   179             Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length
   180                           + ", options= " + options + ") OOM!", e);
   181             return null;
   182         }
   184         if (bitmap == null) {
   185             Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
   186             return null;
   187         }
   189         if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
   190             Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned "
   191                           + "a bitmap with dimensions " + bitmap.getWidth()
   192                           + "x" + bitmap.getHeight());
   193             return null;
   194         }
   196         return bitmap;
   197     }
   199     public static Bitmap decodeStream(InputStream inputStream) {
   200         try {
   201             return BitmapFactory.decodeStream(inputStream);
   202         } catch (OutOfMemoryError e) {
   203             Log.e(LOGTAG, "decodeStream() OOM!", e);
   204             return null;
   205         }
   206     }
   208     public static Bitmap decodeUrl(Uri uri) {
   209         return decodeUrl(uri.toString());
   210     }
   212     public static Bitmap decodeUrl(String urlString) {
   213         URL url;
   215         try {
   216             url = new URL(urlString);
   217         } catch(MalformedURLException e) {
   218             Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
   219             return null;
   220         }
   222         return decodeUrl(url);
   223     }
   225     public static Bitmap decodeUrl(URL url) {
   226         InputStream stream = null;
   228         try {
   229             stream = url.openStream();
   230         } catch(IOException e) {
   231             Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
   232             return null;
   233         }
   235         if (stream == null) {
   236             Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
   237             return null;
   238         }
   240         Bitmap bitmap = decodeStream(stream);
   242         try {
   243             stream.close();
   244         } catch(IOException e) {
   245             Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
   246         }
   248         return bitmap;
   249     }
   251     public static Bitmap decodeResource(Context context, int id) {
   252         return decodeResource(context, id, null);
   253     }
   255     public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) {
   256         Resources resources = context.getResources();
   257         try {
   258             return BitmapFactory.decodeResource(resources, id, options);
   259         } catch (OutOfMemoryError e) {
   260             Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e);
   261             return null;
   262         }
   263     }
   265     public static int getDominantColor(Bitmap source) {
   266         return getDominantColor(source, true);
   267     }
   269     public static int getDominantColor(Bitmap source, boolean applyThreshold) {
   270       if (source == null)
   271         return Color.argb(255,255,255,255);
   273       // Keep track of how many times a hue in a given bin appears in the image.
   274       // Hue values range [0 .. 360), so dividing by 10, we get 36 bins.
   275       int[] colorBins = new int[36];
   277       // The bin with the most colors. Initialize to -1 to prevent accidentally
   278       // thinking the first bin holds the dominant color.
   279       int maxBin = -1;
   281       // Keep track of sum hue/saturation/value per hue bin, which we'll use to
   282       // compute an average to for the dominant color.
   283       float[] sumHue = new float[36];
   284       float[] sumSat = new float[36];
   285       float[] sumVal = new float[36];
   286       float[] hsv = new float[3];
   288       int height = source.getHeight();
   289       int width = source.getWidth();
   290       int[] pixels = new int[width * height];
   291       source.getPixels(pixels, 0, width, 0, 0, width, height);
   292       for (int row = 0; row < height; row++) {
   293         for (int col = 0; col < width; col++) {
   294           int c = pixels[col + row * width];
   295           // Ignore pixels with a certain transparency.
   296           if (Color.alpha(c) < 128)
   297             continue;
   299           Color.colorToHSV(c, hsv);
   301           // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black".
   302           if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f))
   303             continue;
   305           // We compute the dominant color by putting colors in bins based on their hue.
   306           int bin = (int) Math.floor(hsv[0] / 10.0f);
   308           // Update the sum hue/saturation/value for this bin.
   309           sumHue[bin] = sumHue[bin] + hsv[0];
   310           sumSat[bin] = sumSat[bin] + hsv[1];
   311           sumVal[bin] = sumVal[bin] + hsv[2];
   313           // Increment the number of colors in this bin.
   314           colorBins[bin]++;
   316           // Keep track of the bin that holds the most colors.
   317           if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
   318             maxBin = bin;
   319         }
   320       }
   322       // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
   323       if (maxBin < 0)
   324         return Color.argb(255,255,255,255);
   326       // Return a color with the average hue/saturation/value of the bin with the most colors.
   327       hsv[0] = sumHue[maxBin]/colorBins[maxBin];
   328       hsv[1] = sumSat[maxBin]/colorBins[maxBin];
   329       hsv[2] = sumVal[maxBin]/colorBins[maxBin];
   330       return Color.HSVToColor(hsv);
   331     }
   333     /**
   334      * Decodes a bitmap from a Base64 data URI.
   335      *
   336      * @param dataURI a Base64-encoded data URI string
   337      * @return        the decoded bitmap, or null if the data URI is invalid
   338      */
   339     public static Bitmap getBitmapFromDataURI(String dataURI) {
   340         String base64 = dataURI.substring(dataURI.indexOf(',') + 1);
   341         try {
   342             byte[] raw = Base64.decode(base64, Base64.DEFAULT);
   343             return BitmapUtils.decodeByteArray(raw);
   344         } catch (Exception e) {
   345             Log.e(LOGTAG, "exception decoding bitmap from data URI: " + dataURI, e);
   346         }
   347         return null;
   348     }
   350     public static Bitmap getBitmapFromDrawable(Drawable drawable) {
   351         if (drawable instanceof BitmapDrawable) {
   352             return ((BitmapDrawable) drawable).getBitmap();
   353         }
   355         int width = drawable.getIntrinsicWidth();
   356         width = width > 0 ? width : 1;
   357         int height = drawable.getIntrinsicHeight();
   358         height = height > 0 ? height : 1;
   360         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
   361         Canvas canvas = new Canvas(bitmap);
   362         drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
   363         drawable.draw(canvas);
   365         return bitmap;
   366     }
   368     public static int getResource(Uri resourceUrl, int defaultIcon) {
   369         int icon = defaultIcon;
   371         final String scheme = resourceUrl.getScheme();
   372         if ("drawable".equals(scheme)) {
   373             String resource = resourceUrl.getSchemeSpecificPart();
   374             resource = resource.substring(resource.lastIndexOf('/') + 1);
   376             try {
   377                 return Integer.parseInt(resource);
   378             } catch(NumberFormatException ex) {
   379                 // This isn't a resource id, try looking for a string
   380             }
   382             try {
   383                 final Class<R.drawable> drawableClass = R.drawable.class;
   384                 final Field f = drawableClass.getField(resource);
   385                 icon = f.getInt(null);
   386             } catch (final NoSuchFieldException e1) {
   388                 // just means the resource doesn't exist for fennec. Check in Android resources
   389                 try {
   390                     final Class<android.R.drawable> drawableClass = android.R.drawable.class;
   391                     final Field f = drawableClass.getField(resource);
   392                     icon = f.getInt(null);
   393                 } catch (final NoSuchFieldException e2) {
   394                     // This drawable doesn't seem to exist...
   395                 } catch(Exception e3) {
   396                     Log.i(LOGTAG, "Exception getting drawable", e3);
   397                 }
   399             } catch (Exception e4) {
   400               Log.i(LOGTAG, "Exception getting drawable", e4);
   401             }
   403             resourceUrl = null;
   404         }
   405         return icon;
   406     }
   407 }

mercurial