1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/gfx/BitmapUtils.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,408 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.gfx; 1.10 + 1.11 +import java.io.IOException; 1.12 +import java.io.InputStream; 1.13 +import java.lang.reflect.Field; 1.14 +import java.net.MalformedURLException; 1.15 +import java.net.URL; 1.16 + 1.17 +import org.mozilla.gecko.R; 1.18 +import org.mozilla.gecko.util.GeckoJarReader; 1.19 +import org.mozilla.gecko.util.ThreadUtils; 1.20 +import org.mozilla.gecko.util.UiAsyncTask; 1.21 +import org.mozilla.gecko.Tab; 1.22 +import org.mozilla.gecko.Tabs; 1.23 +import org.mozilla.gecko.ThumbnailHelper; 1.24 + 1.25 +import android.content.Context; 1.26 +import android.content.res.Resources; 1.27 +import android.graphics.Bitmap; 1.28 +import android.graphics.BitmapFactory; 1.29 +import android.graphics.Canvas; 1.30 +import android.graphics.Color; 1.31 +import android.graphics.drawable.BitmapDrawable; 1.32 +import android.graphics.drawable.Drawable; 1.33 +import android.net.Uri; 1.34 +import android.text.TextUtils; 1.35 +import android.util.Base64; 1.36 +import android.util.Log; 1.37 + 1.38 +public final class BitmapUtils { 1.39 + private static final String LOGTAG = "GeckoBitmapUtils"; 1.40 + 1.41 + private BitmapUtils() {} 1.42 + 1.43 + public interface BitmapLoader { 1.44 + public void onBitmapFound(Drawable d); 1.45 + } 1.46 + 1.47 + private static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) { 1.48 + if (ThreadUtils.isOnUiThread()) { 1.49 + loader.onBitmapFound(d); 1.50 + return; 1.51 + } 1.52 + 1.53 + ThreadUtils.postToUiThread(new Runnable() { 1.54 + @Override 1.55 + public void run() { 1.56 + loader.onBitmapFound(d); 1.57 + } 1.58 + }); 1.59 + } 1.60 + 1.61 + /** 1.62 + * Attempts to find a drawable associated with a given string, using its URI scheme to determine 1.63 + * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and 1.64 + * will be called with `null` if no drawable is found. 1.65 + * 1.66 + * The BitmapLoader `onBitmapFound` method always runs on the UI thread. 1.67 + */ 1.68 + public static void getDrawable(final Context context, final String data, final BitmapLoader loader) { 1.69 + if (TextUtils.isEmpty(data)) { 1.70 + runOnBitmapFoundOnUiThread(loader, null); 1.71 + return; 1.72 + } 1.73 + 1.74 + if (data.startsWith("data")) { 1.75 + final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data)); 1.76 + runOnBitmapFoundOnUiThread(loader, d); 1.77 + return; 1.78 + } 1.79 + 1.80 + if (data.startsWith("thumbnail:")) { 1.81 + getThumbnailDrawable(context, data, loader); 1.82 + return; 1.83 + } 1.84 + 1.85 + if (data.startsWith("jar:") || data.startsWith("file://")) { 1.86 + (new UiAsyncTask<Void, Void, Drawable>(ThreadUtils.getBackgroundHandler()) { 1.87 + @Override 1.88 + public Drawable doInBackground(Void... params) { 1.89 + try { 1.90 + if (data.startsWith("jar:jar")) { 1.91 + return GeckoJarReader.getBitmapDrawable(context.getResources(), data); 1.92 + } 1.93 + 1.94 + // Don't attempt to validate the JAR signature when loading an add-on icon 1.95 + if (data.startsWith("jar:file")) { 1.96 + return GeckoJarReader.getBitmapDrawable(context.getResources(), Uri.decode(data)); 1.97 + } 1.98 + 1.99 + final URL url = new URL(data); 1.100 + final InputStream is = (InputStream) url.getContent(); 1.101 + try { 1.102 + return Drawable.createFromStream(is, "src"); 1.103 + } finally { 1.104 + is.close(); 1.105 + } 1.106 + } catch (Exception e) { 1.107 + Log.w(LOGTAG, "Unable to set icon", e); 1.108 + } 1.109 + return null; 1.110 + } 1.111 + 1.112 + @Override 1.113 + public void onPostExecute(Drawable drawable) { 1.114 + loader.onBitmapFound(drawable); 1.115 + } 1.116 + }).execute(); 1.117 + return; 1.118 + } 1.119 + 1.120 + if (data.startsWith("-moz-icon://")) { 1.121 + final Uri imageUri = Uri.parse(data); 1.122 + final String ssp = imageUri.getSchemeSpecificPart(); 1.123 + final String resource = ssp.substring(ssp.lastIndexOf('/') + 1); 1.124 + 1.125 + try { 1.126 + final Drawable d = context.getPackageManager().getApplicationIcon(resource); 1.127 + runOnBitmapFoundOnUiThread(loader, d); 1.128 + } catch(Exception ex) { } 1.129 + 1.130 + return; 1.131 + } 1.132 + 1.133 + if (data.startsWith("drawable://")) { 1.134 + final Uri imageUri = Uri.parse(data); 1.135 + final int id = getResource(imageUri, R.drawable.ic_status_logo); 1.136 + final Drawable d = context.getResources().getDrawable(id); 1.137 + 1.138 + runOnBitmapFoundOnUiThread(loader, d); 1.139 + return; 1.140 + } 1.141 + 1.142 + runOnBitmapFoundOnUiThread(loader, null); 1.143 + } 1.144 + 1.145 + public static void getThumbnailDrawable(final Context context, final String data, final BitmapLoader loader) { 1.146 + int id = Integer.parseInt(data.substring(10), 10); 1.147 + final Tab tab = Tabs.getInstance().getTab(id); 1.148 + runOnBitmapFoundOnUiThread(loader, tab.getThumbnail()); 1.149 + Tabs.registerOnTabsChangedListener(new Tabs.OnTabsChangedListener() { 1.150 + public void onTabChanged(Tab t, Tabs.TabEvents msg, Object data) { 1.151 + if (tab == t && msg == Tabs.TabEvents.THUMBNAIL) { 1.152 + Tabs.unregisterOnTabsChangedListener(this); 1.153 + runOnBitmapFoundOnUiThread(loader, t.getThumbnail()); 1.154 + } 1.155 + } 1.156 + }); 1.157 + ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab); 1.158 + } 1.159 + 1.160 + public static Bitmap decodeByteArray(byte[] bytes) { 1.161 + return decodeByteArray(bytes, null); 1.162 + } 1.163 + 1.164 + public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) { 1.165 + return decodeByteArray(bytes, 0, bytes.length, options); 1.166 + } 1.167 + 1.168 + public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) { 1.169 + return decodeByteArray(bytes, offset, length, null); 1.170 + } 1.171 + 1.172 + public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) { 1.173 + if (bytes.length <= 0) { 1.174 + throw new IllegalArgumentException("bytes.length " + bytes.length 1.175 + + " must be a positive number"); 1.176 + } 1.177 + 1.178 + Bitmap bitmap = null; 1.179 + try { 1.180 + bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options); 1.181 + } catch (OutOfMemoryError e) { 1.182 + Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length 1.183 + + ", options= " + options + ") OOM!", e); 1.184 + return null; 1.185 + } 1.186 + 1.187 + if (bitmap == null) { 1.188 + Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null"); 1.189 + return null; 1.190 + } 1.191 + 1.192 + if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { 1.193 + Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned " 1.194 + + "a bitmap with dimensions " + bitmap.getWidth() 1.195 + + "x" + bitmap.getHeight()); 1.196 + return null; 1.197 + } 1.198 + 1.199 + return bitmap; 1.200 + } 1.201 + 1.202 + public static Bitmap decodeStream(InputStream inputStream) { 1.203 + try { 1.204 + return BitmapFactory.decodeStream(inputStream); 1.205 + } catch (OutOfMemoryError e) { 1.206 + Log.e(LOGTAG, "decodeStream() OOM!", e); 1.207 + return null; 1.208 + } 1.209 + } 1.210 + 1.211 + public static Bitmap decodeUrl(Uri uri) { 1.212 + return decodeUrl(uri.toString()); 1.213 + } 1.214 + 1.215 + public static Bitmap decodeUrl(String urlString) { 1.216 + URL url; 1.217 + 1.218 + try { 1.219 + url = new URL(urlString); 1.220 + } catch(MalformedURLException e) { 1.221 + Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString); 1.222 + return null; 1.223 + } 1.224 + 1.225 + return decodeUrl(url); 1.226 + } 1.227 + 1.228 + public static Bitmap decodeUrl(URL url) { 1.229 + InputStream stream = null; 1.230 + 1.231 + try { 1.232 + stream = url.openStream(); 1.233 + } catch(IOException e) { 1.234 + Log.w(LOGTAG, "decodeUrl: IOException downloading " + url); 1.235 + return null; 1.236 + } 1.237 + 1.238 + if (stream == null) { 1.239 + Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url); 1.240 + return null; 1.241 + } 1.242 + 1.243 + Bitmap bitmap = decodeStream(stream); 1.244 + 1.245 + try { 1.246 + stream.close(); 1.247 + } catch(IOException e) { 1.248 + Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e); 1.249 + } 1.250 + 1.251 + return bitmap; 1.252 + } 1.253 + 1.254 + public static Bitmap decodeResource(Context context, int id) { 1.255 + return decodeResource(context, id, null); 1.256 + } 1.257 + 1.258 + public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) { 1.259 + Resources resources = context.getResources(); 1.260 + try { 1.261 + return BitmapFactory.decodeResource(resources, id, options); 1.262 + } catch (OutOfMemoryError e) { 1.263 + Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e); 1.264 + return null; 1.265 + } 1.266 + } 1.267 + 1.268 + public static int getDominantColor(Bitmap source) { 1.269 + return getDominantColor(source, true); 1.270 + } 1.271 + 1.272 + public static int getDominantColor(Bitmap source, boolean applyThreshold) { 1.273 + if (source == null) 1.274 + return Color.argb(255,255,255,255); 1.275 + 1.276 + // Keep track of how many times a hue in a given bin appears in the image. 1.277 + // Hue values range [0 .. 360), so dividing by 10, we get 36 bins. 1.278 + int[] colorBins = new int[36]; 1.279 + 1.280 + // The bin with the most colors. Initialize to -1 to prevent accidentally 1.281 + // thinking the first bin holds the dominant color. 1.282 + int maxBin = -1; 1.283 + 1.284 + // Keep track of sum hue/saturation/value per hue bin, which we'll use to 1.285 + // compute an average to for the dominant color. 1.286 + float[] sumHue = new float[36]; 1.287 + float[] sumSat = new float[36]; 1.288 + float[] sumVal = new float[36]; 1.289 + float[] hsv = new float[3]; 1.290 + 1.291 + int height = source.getHeight(); 1.292 + int width = source.getWidth(); 1.293 + int[] pixels = new int[width * height]; 1.294 + source.getPixels(pixels, 0, width, 0, 0, width, height); 1.295 + for (int row = 0; row < height; row++) { 1.296 + for (int col = 0; col < width; col++) { 1.297 + int c = pixels[col + row * width]; 1.298 + // Ignore pixels with a certain transparency. 1.299 + if (Color.alpha(c) < 128) 1.300 + continue; 1.301 + 1.302 + Color.colorToHSV(c, hsv); 1.303 + 1.304 + // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black". 1.305 + if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f)) 1.306 + continue; 1.307 + 1.308 + // We compute the dominant color by putting colors in bins based on their hue. 1.309 + int bin = (int) Math.floor(hsv[0] / 10.0f); 1.310 + 1.311 + // Update the sum hue/saturation/value for this bin. 1.312 + sumHue[bin] = sumHue[bin] + hsv[0]; 1.313 + sumSat[bin] = sumSat[bin] + hsv[1]; 1.314 + sumVal[bin] = sumVal[bin] + hsv[2]; 1.315 + 1.316 + // Increment the number of colors in this bin. 1.317 + colorBins[bin]++; 1.318 + 1.319 + // Keep track of the bin that holds the most colors. 1.320 + if (maxBin < 0 || colorBins[bin] > colorBins[maxBin]) 1.321 + maxBin = bin; 1.322 + } 1.323 + } 1.324 + 1.325 + // maxBin may never get updated if the image holds only transparent and/or black/white pixels. 1.326 + if (maxBin < 0) 1.327 + return Color.argb(255,255,255,255); 1.328 + 1.329 + // Return a color with the average hue/saturation/value of the bin with the most colors. 1.330 + hsv[0] = sumHue[maxBin]/colorBins[maxBin]; 1.331 + hsv[1] = sumSat[maxBin]/colorBins[maxBin]; 1.332 + hsv[2] = sumVal[maxBin]/colorBins[maxBin]; 1.333 + return Color.HSVToColor(hsv); 1.334 + } 1.335 + 1.336 + /** 1.337 + * Decodes a bitmap from a Base64 data URI. 1.338 + * 1.339 + * @param dataURI a Base64-encoded data URI string 1.340 + * @return the decoded bitmap, or null if the data URI is invalid 1.341 + */ 1.342 + public static Bitmap getBitmapFromDataURI(String dataURI) { 1.343 + String base64 = dataURI.substring(dataURI.indexOf(',') + 1); 1.344 + try { 1.345 + byte[] raw = Base64.decode(base64, Base64.DEFAULT); 1.346 + return BitmapUtils.decodeByteArray(raw); 1.347 + } catch (Exception e) { 1.348 + Log.e(LOGTAG, "exception decoding bitmap from data URI: " + dataURI, e); 1.349 + } 1.350 + return null; 1.351 + } 1.352 + 1.353 + public static Bitmap getBitmapFromDrawable(Drawable drawable) { 1.354 + if (drawable instanceof BitmapDrawable) { 1.355 + return ((BitmapDrawable) drawable).getBitmap(); 1.356 + } 1.357 + 1.358 + int width = drawable.getIntrinsicWidth(); 1.359 + width = width > 0 ? width : 1; 1.360 + int height = drawable.getIntrinsicHeight(); 1.361 + height = height > 0 ? height : 1; 1.362 + 1.363 + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 1.364 + Canvas canvas = new Canvas(bitmap); 1.365 + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 1.366 + drawable.draw(canvas); 1.367 + 1.368 + return bitmap; 1.369 + } 1.370 + 1.371 + public static int getResource(Uri resourceUrl, int defaultIcon) { 1.372 + int icon = defaultIcon; 1.373 + 1.374 + final String scheme = resourceUrl.getScheme(); 1.375 + if ("drawable".equals(scheme)) { 1.376 + String resource = resourceUrl.getSchemeSpecificPart(); 1.377 + resource = resource.substring(resource.lastIndexOf('/') + 1); 1.378 + 1.379 + try { 1.380 + return Integer.parseInt(resource); 1.381 + } catch(NumberFormatException ex) { 1.382 + // This isn't a resource id, try looking for a string 1.383 + } 1.384 + 1.385 + try { 1.386 + final Class<R.drawable> drawableClass = R.drawable.class; 1.387 + final Field f = drawableClass.getField(resource); 1.388 + icon = f.getInt(null); 1.389 + } catch (final NoSuchFieldException e1) { 1.390 + 1.391 + // just means the resource doesn't exist for fennec. Check in Android resources 1.392 + try { 1.393 + final Class<android.R.drawable> drawableClass = android.R.drawable.class; 1.394 + final Field f = drawableClass.getField(resource); 1.395 + icon = f.getInt(null); 1.396 + } catch (final NoSuchFieldException e2) { 1.397 + // This drawable doesn't seem to exist... 1.398 + } catch(Exception e3) { 1.399 + Log.i(LOGTAG, "Exception getting drawable", e3); 1.400 + } 1.401 + 1.402 + } catch (Exception e4) { 1.403 + Log.i(LOGTAG, "Exception getting drawable", e4); 1.404 + } 1.405 + 1.406 + resourceUrl = null; 1.407 + } 1.408 + return icon; 1.409 + } 1.410 +} 1.411 +