mobile/android/base/gfx/BitmapUtils.java

branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
equal deleted inserted replaced
-1:000000000000 0:413f7d3b406e
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/. */
5
6 package org.mozilla.gecko.gfx;
7
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;
13
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;
21
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;
34
35 public final class BitmapUtils {
36 private static final String LOGTAG = "GeckoBitmapUtils";
37
38 private BitmapUtils() {}
39
40 public interface BitmapLoader {
41 public void onBitmapFound(Drawable d);
42 }
43
44 private static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) {
45 if (ThreadUtils.isOnUiThread()) {
46 loader.onBitmapFound(d);
47 return;
48 }
49
50 ThreadUtils.postToUiThread(new Runnable() {
51 @Override
52 public void run() {
53 loader.onBitmapFound(d);
54 }
55 });
56 }
57
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 }
70
71 if (data.startsWith("data")) {
72 final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data));
73 runOnBitmapFoundOnUiThread(loader, d);
74 return;
75 }
76
77 if (data.startsWith("thumbnail:")) {
78 getThumbnailDrawable(context, data, loader);
79 return;
80 }
81
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 }
90
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 }
95
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 }
108
109 @Override
110 public void onPostExecute(Drawable drawable) {
111 loader.onBitmapFound(drawable);
112 }
113 }).execute();
114 return;
115 }
116
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);
121
122 try {
123 final Drawable d = context.getPackageManager().getApplicationIcon(resource);
124 runOnBitmapFoundOnUiThread(loader, d);
125 } catch(Exception ex) { }
126
127 return;
128 }
129
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);
134
135 runOnBitmapFoundOnUiThread(loader, d);
136 return;
137 }
138
139 runOnBitmapFoundOnUiThread(loader, null);
140 }
141
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 }
156
157 public static Bitmap decodeByteArray(byte[] bytes) {
158 return decodeByteArray(bytes, null);
159 }
160
161 public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
162 return decodeByteArray(bytes, 0, bytes.length, options);
163 }
164
165 public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
166 return decodeByteArray(bytes, offset, length, null);
167 }
168
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 }
174
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 }
183
184 if (bitmap == null) {
185 Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
186 return null;
187 }
188
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 }
195
196 return bitmap;
197 }
198
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 }
207
208 public static Bitmap decodeUrl(Uri uri) {
209 return decodeUrl(uri.toString());
210 }
211
212 public static Bitmap decodeUrl(String urlString) {
213 URL url;
214
215 try {
216 url = new URL(urlString);
217 } catch(MalformedURLException e) {
218 Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
219 return null;
220 }
221
222 return decodeUrl(url);
223 }
224
225 public static Bitmap decodeUrl(URL url) {
226 InputStream stream = null;
227
228 try {
229 stream = url.openStream();
230 } catch(IOException e) {
231 Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
232 return null;
233 }
234
235 if (stream == null) {
236 Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
237 return null;
238 }
239
240 Bitmap bitmap = decodeStream(stream);
241
242 try {
243 stream.close();
244 } catch(IOException e) {
245 Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
246 }
247
248 return bitmap;
249 }
250
251 public static Bitmap decodeResource(Context context, int id) {
252 return decodeResource(context, id, null);
253 }
254
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 }
264
265 public static int getDominantColor(Bitmap source) {
266 return getDominantColor(source, true);
267 }
268
269 public static int getDominantColor(Bitmap source, boolean applyThreshold) {
270 if (source == null)
271 return Color.argb(255,255,255,255);
272
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];
276
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;
280
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];
287
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;
298
299 Color.colorToHSV(c, hsv);
300
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;
304
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);
307
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];
312
313 // Increment the number of colors in this bin.
314 colorBins[bin]++;
315
316 // Keep track of the bin that holds the most colors.
317 if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
318 maxBin = bin;
319 }
320 }
321
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);
325
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 }
332
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 }
349
350 public static Bitmap getBitmapFromDrawable(Drawable drawable) {
351 if (drawable instanceof BitmapDrawable) {
352 return ((BitmapDrawable) drawable).getBitmap();
353 }
354
355 int width = drawable.getIntrinsicWidth();
356 width = width > 0 ? width : 1;
357 int height = drawable.getIntrinsicHeight();
358 height = height > 0 ? height : 1;
359
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);
364
365 return bitmap;
366 }
367
368 public static int getResource(Uri resourceUrl, int defaultIcon) {
369 int icon = defaultIcon;
370
371 final String scheme = resourceUrl.getScheme();
372 if ("drawable".equals(scheme)) {
373 String resource = resourceUrl.getSchemeSpecificPart();
374 resource = resource.substring(resource.lastIndexOf('/') + 1);
375
376 try {
377 return Integer.parseInt(resource);
378 } catch(NumberFormatException ex) {
379 // This isn't a resource id, try looking for a string
380 }
381
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) {
387
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 }
398
399 } catch (Exception e4) {
400 Log.i(LOGTAG, "Exception getting drawable", e4);
401 }
402
403 resourceUrl = null;
404 }
405 return icon;
406 }
407 }
408

mercurial