Building on the perfect minimal hash I’ve developed for Java String objects, I’ve developed an memory efficient implementation of Java’s Map interface.
View the map source code in SVN
The map implementation is not general purpose, it’s specifically targeted to situations where:
Even given these constraints, I frequently encounter situations where all of these apply:
Map for substitution into a query.Proxy implementations of setter/getter interfaces that are backed by a Map of values.The implementation stores the set of valid keys in an object that can be shared between ‘like’ maps. The keys are hashed perfectly so that the map values can be stored directly into a single object array; no entry objects are required. This means that the memory required to store large numbers of similar maps can . Furthermore, none of the basic map operations (put/get/contains etc.) performs any memory allocation. This means that for many applications both memory overhead and object churn is minimal.
From the javadocs:
Unlike regular
Mapimplementations, there is no default, or copy, constructor. Instead the map must be constructed around a set of possibleStringkeys. Only the keys supplied to a constructor may be used in the maps it constructs. Re-using constructors (by repeatedly using them to createParameterMapinstances) minimizes the memory required to store the map. Furthermore, the only map operations that allocate new objects are those methods that required to do so by theMapinterface; in-short, object creation strictly minimized. This makes the class ideal for environments where reducing GC overhead is important.
The source code is under the Apache 2.0 licence, built with Maven, and is available from my website’s google code project:
A minimal perfect hash is simply a bijection between a set of N objects and the first N integers. See the wikipedia page about perfect hashing for more information.
I’m looking to produce a fast and memory efficient map implementation over String keys and I want to use a perfect hash to pack values without risking collisions.
Casting around online for a Java implementation led me to Sux4J (this rather unpromising sounding library looks excellent and I’m going to take a closer look at it soon) but the implementations are too generic for my purposes.
So I’ve fashioned my own minimal perfect hashing algorithm for Java strings.I don’t know anything about the theory of such algorithms, but my approach was to build a data-structure as follows:
The gist of this approach is outlined visually in the following diagram. Here, the hashes for the strings “cat” and “dog” collide, so a small decision tree needs to be consulted in the case that either of these two are hashed to confirm which value should be returned.

There are some nuances in the actual implementation. For example, Strings are actually ordered by length and character sequence too; this assists the building of fast decision trees. Also each duplicated string stores its offset within the span to avoid scanning back to find the start of the span (I think there are better space/time trade-offs around this that I may investigate in the future.).
However, these complexities only come into play when there are collisions of the String hash function. The common case, even for fairly large numbers of strings, is that there are no collisions. This reduces the time complexity to a binary search over the integer array of hashcodes — not as fast as only taking the hashcode, but still fast. And even the exceptional case is still fairly fast since it typically needs to test candidate string’s length to determine which minimal hash value it should be assigned.
Note that the algorithm is designed to examine as little about the string as is necessary to determine the perfect hash value (this helps to make it fast). Furthermore, the algorithm does not keep a reference to the list of potential candidates for hashing (this helps to reduce memory overhead). As a consequence, the algorithm cannot necessarily distinguish valid candidates; though they are frequently identified as such, invalid string arguments may generate valid hashes.
The source code for this implementation is fully documented and is under the Apache 2.0 licence. It is available through my website’s Google code project
Reading posts on the android developer group prompted me to reread the Java Language Specification and confirmed that I had forgotten something very important about Java, specifically:
Thread contention for Java synchronized blocks is not guaranteed to be fair.
This matters, because if you have a situation where a ‘producer’ is delivering data to a ‘consumer’ via a synchronized data structure, the producer may be permanently blocked while the consumer polls for data even though the consumer is repeatedly taking and releasing the lock.
In the context of an Android application: if the producer is the event thread delivering events to an application created thread for processing, then it’s a potential (and surprising!) source of ANRs. This is exactly what was happening to me when I filed an issue I titled: “ANR arising from touch event dispatch”. If only I’d remembered that Java threads aren’t guaranteed to be fair, I wouldn’t have wasted hours producing a (mostly) reproducible test case.
You can avoid the problem by using a fair lock, but this is relatively expensive if you’re aiming to create a high performance application, like a game say, which I was (and still am). So instead I wrote a simple event queue for delivering motion events to a thread other than the main application thread based on a synchronization-free fixed-size circular buffer that uses volatiles to manage concurrency.
I’ve been using the class below, for a while now and haven’t encountered any issues with it. Then again, I could once have said that of the code it replaced…
import android.view.MotionEvent;
/**
* <p>
* A simple event queue for delivering motion events to a thread other
* than the main application thread. The implementation is based on a
* synchronization-free fixed-size circular buffer that uses volatiles
* to manage concurrency. Adding an event to the queue is allocation
* free. Removing an event from the queue is only allocation free if
* the event object has been recycled.
* </p>
*
* <p>
* The implementation assumes that calls to the add (resp. next) method
* are single threaded (ie. made on just one thread, or externally
* synchronized).
* </p>
*
* <p>
* NOTE: Mapping of the MotionEvent fields is incomplete and does not
* include historical data; this is easily addressed by modifying the
* implementation.
* </p>
*
* @author Tom Gibara
*/
// NOTE: The safe concurrency reliability of this implementation is based on:
// 1. Calls to the add and next methods are single threaded.
// 2. The mHead and mTail fields are volatile.
// 3. The mHead (resp. mTail) field is only modified in the next (resp. add) method.
// 4. Updates to the mHead and mTail methods are the last operations in any method.
public class MotionEventQueue {
private final int mSize;
private final long[] mDownTimes;
private final long[] mEventTimes;
private final int[] mActions;
private final float[] mXs;
private final float[] mYs;
private volatile int mHead;
private volatile int mTail;
public MotionEventQueue(int size) {
mSize = size;
mDownTimes = new long[size];
mEventTimes = new long[size];
mActions = new int[size];
mXs = new float[size];
mYs = new float[size];
mHead = 0;
mTail = 0;
}
public boolean addEvent(MotionEvent event) {
final int head = mHead;
final int tail = mTail;
int nextTail = tail + 1;
if (nextTail == mSize) nextTail = 0;
if (nextTail == head) return false;
mDownTimes[tail] = event.getDownTime();
mEventTimes[tail] = event.getEventTime();
mActions[tail] = event.getAction();
mXs[tail] = event.getX();
mYs[tail] = event.getY();
mTail = nextTail;
return true;
}
public MotionEvent nextEvent() {
int head = mHead;
if (head == mTail) return null;
MotionEvent event = MotionEvent.obtain(mDownTimes[head], mEventTimes[head], mActions[head], mXs[head], mYs[head], 0);
mHead = head == mSize - 1 ? 0 : head + 1;
return event;
}
}
Okay, this has to be the ugliest class in my private sprawling Java computer vision library (and that really is an achievement). Everything I want to forget about Java image processing is in this class. One day I might come back here and document it.
In practice, it’s incredibly useful and will repay any kindness you show it; it’s just very raw.
package com.tomgibara.vision.image;
import java.awt.Container;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
public class ImageUtil {
public enum IntensityModel {
AVERAGE, RED, GREEN, BLUE;
byte combine(int r, int g, int b) {
switch (this) {
case AVERAGE: return (byte) ((r + b + g) / 3);
case RED: return (byte) r;
case GREEN: return (byte) g;
case BLUE: return (byte) b;
default: throw new IllegalStateException();
}
}
byte combine(byte r, byte g, byte b) {
return combine( r & 0xff, g & 0xff, b & 0xff);
}
}
public static byte[] toByteIntensity(BufferedImage image, IntensityModel model, byte[] bytes) {
if (model == null) model = IntensityModel.AVERAGE;
int width = image.getWidth();
int height = image.getHeight();
switch(image.getType()) {
case BufferedImage.TYPE_BYTE_GRAY :
{
image.getData().getDataElements(0, 0, width, height, bytes);
return bytes;
}
case BufferedImage.TYPE_3BYTE_BGR :
{
byte[] data = (byte[]) image.getData().getDataElements(0, 0, width, height, null);
int i = 0;
for (int j = 0; j < data.length; j += 3) {
//TODO WTF!
//work around for bug 4886732
bytes[i++] = model.combine(data[j], data[j+1], data[j+2]);
//bytes[i++] = model.combine(data[j+2], data[j+1], data[j]);
}
return bytes;
}
case BufferedImage.TYPE_4BYTE_ABGR:
case BufferedImage.TYPE_4BYTE_ABGR_PRE:
{
byte[] data = (byte[]) image.getData().getDataElements(0, 0, width, height, null);
int i = 0;
for (int j = 0; j < data.length; j += 4) {
bytes[i++] = model.combine(data[j+3], data[j+2], data[j+1]);
}
return bytes;
}
case BufferedImage.TYPE_INT_ARGB:
case BufferedImage.TYPE_INT_ARGB_PRE:
case BufferedImage.TYPE_INT_RGB:
{
int[] data = (int[]) image.getData().getDataElements(0, 0, width, height, null);
for (int i = 0; i < data.length; i++) {
int d = data[i];
bytes[i] = model.combine((d >> 16) & 0xff, (d >> 8) & 0xff, d & 0xff);
}
return bytes;
}
case BufferedImage.TYPE_USHORT_GRAY:
{
short[] data = (short[]) image.getData().getDataElements(0, 0, width, height, null);
for (int i = 0; i < data.length; i++) {
bytes[i] = (byte) (data[i] >> 8);
}
return bytes;
}
default: throw new IllegalArgumentException("Unsupported image type: " + image.getType());
}
}
public static byte[] toByteIntensity(BufferedImage image, IntensityModel model) {
return toByteIntensity(image, model, new byte[image.getWidth() * image.getHeight()]);
}
public static BufferedImage fromByteIntensity(BufferedImage image, byte[] bytes) {
int width = image.getWidth();
int height = image.getHeight();
switch(image.getType()) {
case BufferedImage.TYPE_BYTE_BINARY:
case BufferedImage.TYPE_BYTE_GRAY :
{
image.getWritableTile(0,0).setDataElements(0, 0, width, height, bytes);
return image;
}
case BufferedImage.TYPE_INT_ARGB:
case BufferedImage.TYPE_INT_ARGB_PRE:
case BufferedImage.TYPE_INT_RGB:
{
int[] data = new int[width * height];
for (int i = 0; i < bytes.length; i++) {
int b = bytes[i] & 0xff;
data[i] = (((((0xff << 8) | b) << 8) | b) << 8) | b;
}
image.getWritableTile(0,0).setDataElements(0, 0, width, height, data);
return image;
}
case BufferedImage.TYPE_3BYTE_BGR:
{
byte[] data = new byte[width * height * 3];
int offset = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
data[offset++] = b;
data[offset++] = b;
data[offset++] = b;
}
image.getWritableTile(0,0).setDataElements(0, 0, width, height, data);
return image;
}
default: throw new IllegalArgumentException("Unsupported image type: " + image.getType());
}
}
public static BufferedImage fromByteIntensity(int width, int height, int imageType, byte[] bytes) {
BufferedImage image = new BufferedImage(width, height, imageType);
return fromByteIntensity(image, bytes);
}
public static BufferedImage fromByteRGB(BufferedImage image, byte[] reds, byte[] greens, byte[] blues) {
int width = image.getWidth();
int height = image.getHeight();
int size = width * height;
if (image.getType() != BufferedImage.TYPE_3BYTE_BGR) throw new IllegalArgumentException();
byte[] combined = new byte[3 * size];
int offset = 0;
for (int i = 0; i < size; i++) {
//Work around for bug 4886732
combined[offset++] = reds[i];
combined[offset++] = greens[i];
combined[offset++] = blues[i];
}
// System.arraycopy(reds, 0, combined, 0, size);
// System.arraycopy(greens, 0, combined, size, size);
// System.arraycopy(blues, 0, combined, 2*size, size);
image.getWritableTile(0,0).setDataElements(0,0,width,height,combined);
return image;
}
public static BufferedImage fromByteRGB(int width, int height, byte[] reds, byte[] greens, byte[] blues) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
return fromByteRGB(image, reds, greens, blues);
}
public static int[] intFromByteRGB(byte[] reds, byte[] greens, byte[] blues) {
int[] pixels = new int[reds.length];
for (int i = 0; i < pixels.length; i++) {
pixels[i] = ((reds[i] & 0xff) << 16) | ((greens[i] & 0xff) << 8) | (blues[i] & 0xff);
}
return pixels;
}
public static int[] toIntRGB(BufferedImage image, int[] ints) {
int width = image.getWidth();
int height = image.getHeight();
switch(image.getType()) {
case BufferedImage.TYPE_BYTE_GRAY :
{
byte[] data = (byte[]) image.getData().getDataElements(0, 0, width, height, null);
for (int i = 0; i < data.length; i++) {
int v = data[i] & 0xff;
ints[i] = (v << 16) | (v << 8) | v;
}
return ints;
}
case BufferedImage.TYPE_3BYTE_BGR :
{
byte[] data = (byte[]) image.getData().getDataElements(0, 0, width, height, null);
int i = 0;
for (int j = 0; j < data.length; j += 3) {
//TODO WTF!
//work around for bug 4886732
ints[i++] = ((data[j] & 0xff) << 16) | ((data[j+1] & 0xff) << 8) | (data[j+2] & 0xff);
}
return ints;
}
case BufferedImage.TYPE_4BYTE_ABGR:
case BufferedImage.TYPE_4BYTE_ABGR_PRE:
{
byte[] data = (byte[]) image.getData().getDataElements(0, 0, width, height, null);
int i = 0;
for (int j = 0; j < data.length; j += 3) {
//TODO WTF!
//work around for bug 4886732
ints[i++] = ((data[j+1] & 0xff) << 16) | ((data[j+2] & 0xff) << 8) | (data[j+3] & 0xff);
}
return ints;
}
case BufferedImage.TYPE_INT_ARGB:
case BufferedImage.TYPE_INT_ARGB_PRE:
case BufferedImage.TYPE_INT_RGB:
{
image.getData().getDataElements(0, 0, width, height, ints);
return ints;
}
case BufferedImage.TYPE_USHORT_GRAY:
{
short[] data = (short[]) image.getData().getDataElements(0, 0, width, height, null);
for (int i = 0; i < data.length; i++) {
int v = (data[i] & 0xffff) >> 8;
ints[i] = (v << 16) | (v << 8) | v;
}
return ints;
}
default: throw new IllegalArgumentException("Unsupported image type: " + image.getType());
}
}
public static int[] toIntRGB(BufferedImage image) {
return toIntRGB(image, new int[image.getWidth() * image.getHeight()]);
}
public static BufferedImage fromIntRGB(BufferedImage image, int[] rgb) {
//TODO must support more types
if (image.getType() != BufferedImage.TYPE_INT_RGB) throw new IllegalArgumentException();
image.getWritableTile(0, 0).setDataElements(0, 0, image.getWidth(), image.getHeight(), rgb);
return image;
}
public static BufferedImage fromIntRGB(int width, int height, int[] rgb) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
return fromIntRGB(image, rgb);
}
public static byte[][] splitIntRGB(int[] rgb) {
byte[] rs = new byte[ rgb.length ];
byte[] gs = new byte[ rgb.length ];
byte[] bs = new byte[ rgb.length ];
for (int i = 0; i < rgb.length; i++) {
int p = rgb[i];
rs[i] = (byte) (p >> 16);
gs[i] = (byte) (p >> 8);
bs[i] = (byte) (p );
}
return new byte[][] { rs, gs, bs };
}
public static BufferedImage convertImage(BufferedImage source, int type) {
BufferedImage target = new BufferedImage(source.getWidth(), source.getHeight(), type);
Graphics2D g = target.createGraphics();
g.drawImage(source, null, null);
g.dispose();
return target;
}
public static JFrame showImage(String title, BufferedImage image) {
final JFrame f = new JFrame(title);
Container pane = f.getContentPane();
JButton display = new JButton(new ImageIcon(image));
display.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
f.dispose();
}
});
display.setMargin(new Insets(50, 50, 50, 50));
pane.add(display);
f.pack();
f.setLocationRelativeTo(null);
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.setVisible(true);
return f;
}
public static BufferedImage duplicateImage(BufferedImage image) {
BufferedImage dup = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
image.copyData(dup.getWritableTile(0, 0));
dup.releaseWritableTile(0, 0);
return dup;
}
public static BufferedImage scaleImage(BufferedImage image, int width, int height) {
return scaleImage(image, image.getType(), width, height);
}
public static BufferedImage scaleImage(BufferedImage image, int type, int width, int height) {
BufferedImage scaled = new BufferedImage(width, height, type);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(image, 0, 0, width, height, null);
g.dispose();
return scaled;
}
public static BufferedImage fromIntIntensity(int width, int height, int[] data, int shift) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
byte[] bytes = new byte[data.length];
for (int i = 0; i < bytes.length; i++) {
int v = Math.abs(data[i]);
v = v >> shift;
if (v > 255) v = 255;
bytes[i] = (byte) v;
}
image.getWritableTile(0, 0).setDataElements(0, 0, width, height, bytes);
image.releaseWritableTile(0, 0);
return image;
}
public static BufferedImage fromIntIntensityScaled(int width, int height, int[] data) {
int max = 0;
for (int d : data)
if (d > max) max = d;
else if (-d > max) max = -d;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
byte[] bytes = new byte[data.length];
if (max > 0) {
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (Math.abs(data[i]) * 255 / max);
}
}
image.getWritableTile(0, 0).setDataElements(0, 0, width, height, bytes);
image.releaseWritableTile(0, 0);
return image;
}
public static BufferedImage fromBooleans(int width, int height, boolean[] bools) {
byte[] data = new byte[width * height];
for (int i = 0; i < data.length; i++) {
data[i] = bools[i] ? (byte)255 : 0;
}
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
image.getWritableTile(0, 0).setDataElements(0, 0, width, height, data);
image.releaseWritableTile(0, 0);
return image;
}
public static int[][] bytesToInts(byte[][] bytes) {
int[][] ints = new int[bytes.length][];
for (int a = 0; a < bytes.length; a++) {
byte[] bs = bytes[a];
int[] is = new int[bs.length];
ints[a] = is;
for (int i = 0; i < bs.length; i++) {
is[i] = bs[i] & 0xff;
}
}
return ints;
}
public static byte[][] intsToBytes(int[][] ints) {
byte[][] bytes = new byte[ints.length][];
for (int a = 0; a < ints.length; a++) {
int[] is = ints[a];
byte[] bs = new byte[is.length];
bytes[a] = bs;
for (int i = 0; i < is.length; i++) {
int v = is[i];
if (v < 0) v = 0;
else if (v > 255) v = 255;
bs[i] = (byte) v;
}
}
return bytes;
}
public static void unscaledYUVToRGB(int[][] yuv) {
int length = yuv[0].length;
final int[] ys = yuv[0];
final int[] us = yuv[1];
final int[] vs = yuv[2];
for (int i = 0; i < length; i++) {
int y = ys[i];
int u = us[i];
int v = vs[i];
int g = (y-u-v) >> 2;
us[i] = g;
ys[i] = v + g;
vs[i] = u + g;
}
}
public static void rgbToUnscaledYUV(int[][] rgb) {
int length = rgb[0].length;
final int[] rs = rgb[0];
final int[] gs = rgb[1];
final int[] bs = rgb[2];
for (int i = 0; i < length; i++) {
int r = rs[i];
int g = gs[i];
int b = bs[i];
rs[i] = r + 2*g + b;
gs[i] = b-g;
bs[i] = r-g;
}
}
public static void rgbToScaledYUV(byte[][] rgb) {
int length = rgb[0].length;
final byte[] rs = rgb[0];
final byte[] gs = rgb[1];
final byte[] bs = rgb[2];
for (int i = 0; i < length; i++) {
int r = rs[i] & 0xff;
int g = gs[i] & 0xff;
int b = bs[i] & 0xff;
rs[i] = (byte) ((r + (g << 1) + b) >> 2);
gs[i] = (byte) ((b-g)/2);
bs[i] = (byte) ((r-g)/2);
}
}
public static void scaledYUVToRGB(byte[][] yuv) {
int length = yuv[0].length;
final byte[] ys = yuv[0];
final byte[] us = yuv[1];
final byte[] vs = yuv[2];
for (int i = 0; i < length; i++) {
int y = ys[i] & 0xff;
int u = us[i];
int v = vs[i];
int g = y - ((u+v)/2);
int r = g + (v*2);
int b = g + (u*2);
//TODO verify why these overflows occur
if (b < 0) b = 0;
if (r < 0) r = 0;
us[i] = (byte) g;
ys[i] = (byte) r;
vs[i] = (byte) b;
}
}
}
I’ve been investigating some rotational codes. Along the way, I thought I’d put together this image of the first few binary de Bruijn sequences. Each ring forms one sequence.
All applications carry some global state and Android applications are no different, but their componentized structure and the constraints of the hardware they are intended to run on throw up some new code design considerations.
Complex Android applications will have many possible entry points, for instance a user might activate the application from a launcher, or from a notification, or to fulfil a request by another application altogether, so you can’t make many assumptions about ‘where’ your application is going to launch from.
At the same time, the limited processing capacity available means that its important to avoid repeating work, so there are generally resources that you want to cache. But you will probably find that most of these resources will depend on a Context, so its not simply a case of hiding their acquisition behind static accessors as one might in a typical desktop Java application.
In addition, application management in Android puts a premium on rapid application start-up — users have every right to expect responsive applications — so in non-trivial applications it’s usually not the case that you can simply initialize all your global state because much of it may be wasted depending on how the application was launched.
My preferred approach is to sensibly modularize my global state and attach it to an Application instance that initializes it lazily behind static accessors. I’m not saying this is the only approach, or even the best, but it’s the one I use.
Since the Application component is always the first thing to be initialized in your application, you don’t need to worry about whether it’s ‘ready’ when you call it from other components (important caveat ContentProviders are started before the Application). Furthermore, if there is state you know you will always need when your application is launched, its onCreate() is an ideal place to do that work. Finally, and this is its most important benefit in my opinion, helper classes that need to hold-on to a Context to do their work, can do so permanently without inadvertently pinning an Activity or Service in memory. Of course, nothing stops you from being careful with the Context objects you choose to initialize your context dependent classes with, but this approach means you don’t need to be overly mindful of it.
import android.app.Application;
import android.content.Context;
public class MyApp extends Application {
//sensible place to declare a log tag for the application
public static final String LOG_TAG = "myapp";
//instance
private static MyApp instance = null;
//keep references to our global resources
private static SomeResource someResource = null;
private static AnotherResource anotherResource = null;
/**
* Convenient accessor, saves having to call and cast getApplicationContext()
*/
public static MyApp getInstance() {
checkInstance();
return instance;
}
/**
* Accessor for some resource that depends on a context
*/
public static SomeResource getSomeResource1() {
if (someResource == null) {
checkInstance();
someResource = new SomeResource(instance);
}
return someResource;
}
/**
* Accessor for another resource that depends on a context
*/
public static AnotherResource getSomeResource2() {
if (anotherResource == null) {
checkInstance();
anotherResource = new AnotherResource(instance);
}
return anotherResource;
}
private static void checkInstance() {
if (instance == null)
throw new IllegalStateException("Application not created yet!");
}
@Override
public void onCreate() {
//provide an instance for our static accessors
instance = this;
}
}
I’m producing a UI for Android which has a hand-drawn feel to it. To preserve this effect on disabled components, I don’t simply want to globally modify their transparency — it’s not very realistic. Instead, a better approach is to simulate the way that the dark parts of drawings usually are more opaque by generating a mask based on intensity. See my previous post for an example of the effect.
Android makes this really easy by providing a ColorMatrixColorFilter.
/**
* Mask a (typically opaque) source bitmap according to the intensity of its pixels.
*
* @param source the image to be
* @param color a fixed color used to render the source image,
* or zero use the source colors
* @param invert true if lighter pixels are more transparent,
* false if the converse is true
* @param target optional bitmap in which the result will be stored,
* passing in null will generate an ARGB bitmap of the same dimensions
* @return a bitmap containing the masked source
*/
private Bitmap toTransparency(final Bitmap source, final int color, boolean invert, Bitmap target) {
//cache the values we know we will need
final int srcW = source.getWidth();
final int srcH = source.getHeight();
//generate the color matrix coefficients
final float[] coefficients;
if (color == 0) {
final float m = invert ? -0.333333f : 0.333333f;
final float ac = invert ? 256f : 0f;
coefficients = new float[] {
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
m, m, m, 0, ac,
};
} else {
//(these rgb assignments could be tidied away)
final int a = (color >> 24) & 0xff;
final int r = (color >> 16) & 0xff;
final int g = (color >> 8) & 0xff;
final int b = (color ) & 0xff;
final float m = a/255f * (invert ? -0.333333f : 0.333333f);
final float ac = a * (invert ? 1f : 0f);
coefficients = new float[] {
0, 0, 0, 0, r,
0, 0, 0, 0, g,
0, 0, 0, 0, b,
m, m, m, 0, ac,
};
}
//create a suitable target if none specified
final boolean sameSize;
if (target == null) {
target = Bitmap.createBitmap(srcW, srcH, Bitmap.Config.ARGB_8888);
sameSize = true;
} else {
sameSize = target.getWidth() == srcW && target.getHeight() == srcH;
}
//prepare our canvas and paint
//(everything here could be cached in some way)
final Canvas canvas = new Canvas(target);
final Paint paint = new Paint();
paint.setFilterBitmap(true);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
paint.setColorFilter(new ColorMatrixColorFilter(coefficients));
//do conversion of the source bitmap
if (sameSize) {
canvas.drawBitmap(source, 0, 0, paint);
} else {
Rect src = new Rect(0, 0, srcW, srcH);
Rect dst = new Rect(0, 0, target.getWidth(), target.getHeight());
canvas.drawBitmap(source, src, dst, paint);
}
//return the result
return target;
}
Last night I decided to knock out a quick app to test how straightforwardly one can upload twitter images. With the exception of one hiccup:
http://code.google.com/p/twitter-api/issues/detail?id=697
it went very smoothly.
Essentially you need to introduce two libraries into your project that are absent from Android:
I used versions 4.0-beta2 and 0.5 respectively.
Here’s my very basic implementation:
//sample method for uploading a user profile image to twitter
int uploadImage(InputStream in, String type, String fileName, Credentials credentials) throws IOException {
try {
//convert the input stream into a byte array - this is just a convenient way
InputStreamEntity ise = new InputStreamEntity(in, -1L);
byte[] bytes = EntityUtils.toByteArray(ise);
//establish the credentials
DefaultHttpClient client = new DefaultHttpClient();
client.getCredentialsProvider().setCredentials(new AuthScope("twitter.com", 80), credentials);
//create the multipart entity
MultipartEntity entity = new MultipartEntity();
ByteArrayBody body = new ByteArrayBody(bytes, type, fileName);
entity.addPart("image", body);
//assign the entity to the request and disable the Expect header
//see http://code.google.com/p/twitter-api/issues/detail?id=697
HttpPost request = new HttpPost("http://twitter.com/account/update_profile_image.xml");
request.setEntity(entity);
request.getParams().setBooleanParameter( "http.protocol.expect-continue", false );
//execute the request and return the response
HttpResponse response = null;
try {
response = client.execute(request);
} finally {
//exhaust the response
if (response != null) {
HttpEntity re = response.getEntity();
if (re != null) re.consumeContent();
}
}
return response.getStatusLine().getStatusCode();
} finally {
try {
in.close();
} catch (IOException e) {
/* ignored */
}
}
}
I haven’t investigated properly, but disabling the “expect-continue” feature appears to cause the request entity to be read twice. So the method above depends on a helper class that provides a repeatable entity.
private class ByteArrayBody extends AbstractContentBody {
private final byte[] bytes;
private final String fileName;
public ByteArrayBody(byte[] bytes, String mimeType, String fileName) {
super(mimeType);
this.bytes = bytes;
this.fileName = fileName;
}
@Override
public String getFilename() {
return fileName;
}
@Override
public void writeTo(OutputStream out, int mode) throws IOException, MimeException {
out.write(bytes);
}
@Override
public String getCharset() {
return null;
}
@Override
public long getContentLength() {
return bytes.length;
}
@Override
public String getTransferEncoding() {
return MIME.ENC_BINARY;
}
}
The Java code in this post is in the public domain.