Friday, February 20, 2009

J2ME: Browsing File System on Mobile Device using FileConnection APIs

There can be application where we need to browse through different files on our mobile phone, File Viewer, File Uploader are few to name. Here I’m going to provide a complete program that helps you browse through your file on mobile device and therefore helps you develop more sophisticated applications using it. Its based on J2ME FileConnection APIs (JSR 75) which is a optional package within MIDP. Though the midlet code provided is quite simple and straightforward and needn’t much explanation, however, please consult JavaDoc for FileConnection API for details which is available here. The midlet allows the user to browse through file system on the Java Enabled devices.
There are some other ways to open file connection but they open connection to specific folders e.g. System.getProperty("fileconn.dir.photos") which retruns the path to ‘photos’ folder to which connection can be opened. Moreover this method might not be supported by the some of the devices. In my case, I tried this approach for SE K800i and Nokia 6630 where it worked but not for Motorola V3. In that case, the approach outlined by the code snippet here is more suitable.

Note: FileConnection APIs (JSR 75) being an optional package of MIDP might not be available on the all the devices. So you’ll have to check if the particular device has support for it using system property System.getProperty("microedition.io.file.FileConnection.version").


/**
*
* @author Atifs
*/
import javax.microedition.lcdui.*;
import javax.microedition.midlet.MIDlet;

public class BrowserLauncher extends MIDlet implements CommandListener {

private Display display;
private final Command CMD_EXIT = new Command("Exit", Command.EXIT, 1);
FileSysBrowser fsb;

public BrowserLauncher() {
display = Display.getDisplay(this);
}

protected void startApp() {
fsb = new FileSysBrowser(this);
fsb.showFiles();
}

public Display getDisplay() {
return display;
}

protected void destroyApp(boolean unconditional) {
}

protected void exitApp() {
destroyApp(false);
notifyDestroyed();
}

protected void pauseApp() {
}

public void commandAction(Command c, Displayable d) {
if (c == CMD_EXIT) {
exitApp();
}
}
}


/**
*
* @author Atifs
*/
import java.io.IOException;
import javax.microedition.lcdui.*;
import javax.microedition.io.*;
import javax.microedition.io.file.*;
import java.util.*;

public class FileSysBrowser extends List implements CommandListener {
// 'DATHHEAD' holds the root, please see javadoc for details
// This may vary for different devices.
String DATHHEAD = "file:///";
private Command CMD_SUBMIT = new Command("Open", Command.ITEM, 0);
private Command CMD_EXIT = new Command("Exit", Command.EXIT, 1);
private Command CMD_BACK = new Command("Back", Command.BACK, 1);
private BrowserLauncher fl;
private String path = null;
private String selectedItem = null;
FileConnection srcconn = null;

public FileSysBrowser(BrowserLauncher fl) {
super("File List", List.IMPLICIT);
addCommand(CMD_SUBMIT);
addCommand(CMD_EXIT);
addCommand(CMD_BACK);
setCommandListener(this);
this.fl = fl;
}

protected void showFiles() {
if (path == null) {
Enumeration e = FileSystemRegistry.listRoots();
path = DATHHEAD;
setTitle(path);
while (e.hasMoreElements()) {
String root = (String) e.nextElement();
// you can pass an 'image' object instead of 'null' to
// append method
append(root, null);
}
fl.getDisplay().setCurrent(this);
} else {
if (selectedItem != null) {
setTitle(path + selectedItem);
} else {
setTitle(path);
}
try {
// works when users opens a directory
if (selectedItem != null) {
srcconn = (FileConnection) Connector.open(path + selectedItem, Connector.READ);
} else // works when presses 'Back' to go one level above/up
{
srcconn = (FileConnection) Connector.open(path, Connector.READ);
}
// Check if the selected item is a directory
if (srcconn.isDirectory()) {
if (selectedItem != null) {
path = path + selectedItem;
selectedItem = null;
}
Enumeration files = srcconn.list();
while (files.hasMoreElements()) {
String file = (String) files.nextElement();
append(file, null);
}
//
fl.getDisplay().setCurrent(this);
try {
if (srcconn != null) {
srcconn.close();
srcconn = null;
}
} catch (IOException ex) {
ex.printStackTrace();
}
}//if (srcconn.isDirectory())
else {
// 'else' is unnecessary here, i just out here to signify the
// fact that you handle the selected item if it is a 'file'
// instead of a directory. For example you can display a file
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

protected String getDirectoryName() {
int select = getSelectedIndex();
return getString(select);
}

public void commandAction(Command c, Displayable d) {
if (c == CMD_SUBMIT) {
selectedItem = getDirectoryName();
deleteAll();
showFiles();
}
if (c == CMD_EXIT) {
fl.exitApp();
}
if (c == CMD_BACK) {
if (!path.equals(DATHHEAD)) {
int slashIndex = path.lastIndexOf('/', path.length() - 2);
path = path.substring(0, slashIndex + 1);
if (path.equals(DATHHEAD)) {
path = null;
}
deleteAll();
showFiles();
}
}
}
}


Disclaimer: The code provided has been written for exploratory purposes, not following the official guideline of Software Engineering and therefore has not been tested thoroughly. However, you are FREE to use the code for any purpose and be aware to test it before using for production uses!!!

Wednesday, February 4, 2009

J2ME: Thumbnails Extraction of JPEG (Exif) Images made with Mobile Phone Camera

....continuation
Following are the code snippets for extracting the thumbnail from JPEG image/picture created from mobile phone camera. I have put comments inside the code where necessary, I guess they should be enough to explain the code, however, I would strongly recommend to study this link for understanding the Exif format. This will really help you understand the code. Please the read the comments carefully!!

Disclaimer: The code provided has been written for exploratory purposes (that’s why there are so many console output and debug statements), not following the official guideline of Software Engineering and therefore has not been tested thoroughly but it does provide a good foundation and good starting point if you intend to work on the topic. However, you are FREE to use the code for any purpose and be aware to test it before using for production uses!!!

To start with you need an object of ‘InputStream’ opened to the JPEG file. (We will discuss at some other time how to open a connection to a file). Pass this object to the extractThumbnail(). Rest of stuff is pretty much clear from the comments so I leave you here with it!!!

/*
This method is the starting point, it takes an 'InputStream' object as an
argument to the JPEG file
*/
public int extractThumbnail(InputStream in) throws Exception {
int current;
if (in == null) {
return 0;
}
current = read16Bytes(in);
if (current != 0xFFD8) { //SOI
error("Not a JPEG file");
return 0;
}
current = read16Bytes(in);

// Reading until 'APP' marker is reached
while (!(current == 0xFFE0 || current == 0xFFE1 || current == 0xFFE2 ||
current == 0xFFE3 || current == 0xFFE4 || current == 0xFFE5 ||
current == 0xFFE6 || current == 0xFFE7 || current == 0xFFE8 ||
current == 0xFFE9 || current == 0xFFEA || current == 0xFFEB ||
current == 0xFFEC || current == 0xFFED || current == 0xFFEE ||
current == 0xFFEF)) {
current = read16Bytes(in);
if (current == 0xFFD9) { // End of Image
error("ERROR: File Malformed");
return 0;
}
}
return readAppMarker(in);
}

// Utility Method to throw exceptions
private void error(String message) throws Exception {
throw new Exception(message);
}

// Utility Method to read 2 bytes from the file*/
private final int read16Bytes(InputStream in) throws Exception {
int temp;
try {
temp = in.read();
temp <<= 8;
return temp | in.read();
} catch (IOException e) {
error("read16Bytes() read error: " + e.toString());
return -1;
}
}

// Utility Method to read 1 bytes from the file
private final int read8Bytes(InputStream in) throws Exception {
try {
return in.read();
} catch (IOException e) {
error("read8Bytes() read error: " + e.toString());
return -1;
}
}

// Method that reads the App Marker from the file to the point where
// thumbnail exists...
private int readAppMarker(InputStream in) throws Exception {
boolean debug = false;
// 'Lp' holds the lenght of the marker
int Lp;
// 'content' contains the bytes read from the file
int content = -1;
//'count' for the bytes read from the file
int count = 0;
int offset = 0;
int entries = 0;
// Readin the lenght for marker
Lp = read16Bytes(in);
count += 2;
System.out.println("Length of App1 marker: " + Lp);
if (debug)
System.out.println("Reading App1 identifier\nIdentifier: ");

// Reading the identifier i.e.6 bytes
int iden = 0;
String id = null;
content = read8Bytes(in); //1st byte read of the identifier
count++;
id = Integer.toHexString(content); // This byte should be '45' for Exif or '4a' for JFIF
if (!id.equals("45")) {
while (iden < 5) { // Reading the remaining 5 bytes which are 78, 69, 66, 00, 00
content = read8Bytes(in);
count++; // Incrementing each time a byte is read
iden++;
if (debug)
System.out.print(Integer.toHexString(content));
}
} else {
// Not an exif image
return 0;
}

/* If the image is found to be ''Exif', remaining
* bytes will be following 'TIFF' image format
*/

if (debug) {
System.out.println("\nBytes read for identifier: " + (count - 2));
System.out.println("\n\nReading TIFF Header");
}
// 'tiffCounter' contains the bytes read from the file
// Though i could have used 'count'.....
int tiffCounter = 0;
// 'intelAlign' used as a flag indiacte byte alignment
// either intel (little endian) or
// non-intel(motorola-big endian)
boolean intelAlign = false;

// Reading first 8 bytes for TIFF header
while (tiffCounter < 8) {
if (tiffCounter == 0) {
content = read16Bytes(in);
// count+=2;
tiffCounter += 2;
if (Integer.toHexString(content).equals("4949")) {
intelAlign = true;
}
if (debug) {
System.out.println("Byte Align: " + Integer.toHexString(content));
}
} else {
if (tiffCounter == 2) {
content = read16Bytes(in);
// count += 2;
tiffCounter += 2;
if (debug) {
System.out.println("Tag mark: " + Integer.toHexString(content));
}
} else if (tiffCounter == 4) {
// Reading offset for IFD0 which conatins
// offset for IFD1 which in turn conatins the
// thumbnail
content = read16Bytes(in);
content <<= 16;
content |= read16Bytes(in);
if (intelAlign) {
content = convertIntegerToBigEndian(content);
}
offset = content;
// count += 4;
tiffCounter += 4;
if (debug) {
System.out.println("IFD0 Offset: " + Integer.toHexString(offset));
}
}
}
}
if (debug) {
System.out.println("Bytes read for header: " + tiffCounter);
System.out.println("\n\nBytes to be read for IFD0: " + (offset - tiffCounter));
}

// Reading bytes until reached IFD0
while (offset - tiffCounter != 0) {
content = read8Bytes(in);
tiffCounter++;
if (debug) {
System.out.println("Bytes to be read for IFD0: " + (offset - tiffCounter));
}

}

// Reading IFD0 .......to get the offset for FD01 which
// contains Thumbnail
content = 0;
// reading the number of entries in IFD0
content = read16Bytes(in);
if (intelAlign) {
content = convertShortToBigEndian(content);
}

tiffCounter += 2;
entries = content;
int bytesReadSoFar = tiffCounter;
if (debug) {
System.out.println("\n\nNo. of entries in IFD0: " + entries);
}

// Reading all the entries in IFD0
// 12 bytes for each entry
// The loop is bit elaborated although i could have just skim
// all those byte for all the entries
// I was just exploring it:)
for (int i = 0; i < entries; i++) {

int entryCounter = tiffCounter;
if (debug) {
System.out.println("\n\nScaning entry in IFD0 no. : " + (i + 1));
}
content = read16Bytes(in);
tiffCounter += 2;
if (debug) {
System.out.println("Tag no. : " + content);
}

content = read16Bytes(in);
tiffCounter += 2;
if (debug) {
System.out.println("Data Format : " + content);
}

content = read16Bytes(in);
content |= read16Bytes(in);
tiffCounter += 4;
if (debug) {
System.out.println("no. of Components : " + content);
}

content = read16Bytes(in);
content |= read16Bytes(in);
tiffCounter += 4;
if (debug) {
System.out.println("Either data value or offset to data value : " + content);
System.out.println("Bytes read for entry : " + (tiffCounter - entryCounter));
}
}

if (debug) {
System.out.println("Bytes read for IFD0 : " + (tiffCounter - bytesReadSoFar));
}

// Reading offset for IFD1
content = 0;
content = read16Bytes(in);
content <<= 16;
content |= read16Bytes(in);
if (intelAlign) {
content = convertIntegerToBigEndian(content);
}
tiffCounter += 4;
offset = content;
if (debug) {
System.out.println("\n\nBytes to be read for IFD1 " + (offset - tiffCounter));
}

// Reading bytes until reached IFD1
while (offset - tiffCounter != 0) {
content = read8Bytes(in);
tiffCounter++;
if (debug) {
System.out.println("Bytes to be read for IFD1: " + (offset - tiffCounter));
}
}

// Reading number of entries for IFD1
content = read16Bytes(in);
if (intelAlign) {
content = convertShortToBigEndian(content);
}
tiffCounter += 2;
entries = content;
bytesReadSoFar = tiffCounter;
if (debug) {
System.out.println("\n\nNo. of entries in IFD1: " + entries);
}

// 'data' array contains the info about the thumbnail
int data[] = new int[3];
// Reading the entried for IFD1
// 12 bytes for each entry
for (int i = 0; i < entries; i++) {
int desiredTag = -1;
int entryCounter = tiffCounter;
if (debug) {
System.out.println("\n\nScaning entry in IFD1 no. : " + (i + 1));
}
content = read16Bytes(in);
content = convertShortToBigEndian(content);
tiffCounter += 2;
// tag '103' containd about the format of thumbnail i.e. Jpeg compression
// or TIFF format howeve, we are interested in JPEg compression
// JPEG compression has been standardised by DCS so we expect it to
// JPEG compression for mobile phone cameras
if (Integer.toHexString(content).equals("103")) {
desiredTag = 0;
}
// if JPEG compression tag '201' contains the offset to the thumbnail image
if (Integer.toHexString(content).equals("201")) {
desiredTag = 1;
}
// if JPEG compression tag '202' contains the size of the thumbnail
if (Integer.toHexString(content).equals("202")) {
desiredTag = 2;
}

if (debug) {
System.out.println("Tag no. : " + Integer.toHexString(content));
}

content = read16Bytes(in);
content = convertShortToBigEndian(content);
tiffCounter += 2;
if (debug) {
System.out.println("Data Format : " + content);
System.out.println("Data Format Hex : " + Integer.toHexString(content));
}

content = read16Bytes(in);
content <<= 16;
content |= read16Bytes(in);
content = convertIntegerToBigEndian(content);
tiffCounter += 4;
if (debug) {
System.out.println("no. of Components : " + content);
System.out.println("no. of Components : " + Integer.toHexString(content));
}

content = read16Bytes(in);
content <<= 16;
content |= read16Bytes(in);
content = convertIntegerToBigEndian(content);
tiffCounter += 4;
if (desiredTag != -1) {
data[desiredTag] = content;
}
if (debug) {
System.out.println("Either data value or offset to data value : " + content);
System.out.println("Either data value or offset to data value : " + Integer.toHexString(content));
System.out.println("Bytes read for entry : " + (tiffCounter - entryCounter));
}
}
if (debug) {
System.out.println("Bytes read for IFD1 : " + (tiffCounter - bytesReadSoFar));
}

/*
'data[0]' conatins the thumbnail format
'data[1]' contains the offset to the thumbnail
'data[2]' contains the size of the thumbnail in bytes (which we might not need as we'll pass
to a JPEG Decoder which will take care of it...in fact the length is already there in JPEG
thumbnail image!!!)
*/

// we only proceed only if the thumbnail is JPED compressed
// for which data[0] = 6
if (data[0] != 6) {
error("Not a JPEG file");
return 0;
} else {
// reading bytes until reached the thumbnail
while (data[1] - tiffCounter != 0) {
content = read8Bytes(in);
tiffCounter++;
if (debug) {
System.out.println("Bytes to be read for IFD1: " + (data[1] - tiffCounter));
}
}
}
if (debug) {
System.out.println("Total Length for the marker: " + Lp);
System.out.println("Total bytes read so far: " + (tiffCounter + count));
System.out.println("Bytes left for this marker");
}

/*
At this point, we need a JPEG decoder which starts reading the file
* from this point in the file, that decodes the thumbnail. Remember
* thumbnail is reduced version of actual image but still a JPEG image
* if JPEG Compressed!!
*/
return 1;

}

// Utility Method to convert Short (2bytes) from Little Endian to
// Big Endian
int convertShortToBigEndian(int i) {
return ((i >> 8) & 0xff) + ((i << 8) & 0xff00);
}

// Utility Method to convert Integer (4bytes) from Little Endian to
// Big Endian
int convertIntegerToBigEndian(int i) {
return ((i & 0xff) << 24) + ((i & 0xff00) << 8) + ((i & 0xff0000) >> 8) + ((i >> 24) & 0xff);
}



After the execution of extractThumbnail() method you are at the point where thumbnail exists in the file, it should be noted that thumbnail is scaled down version of the actual image, but some of the markers are missing here. You need a JPEG decoder that decodes the thumbnail for display; there is one free JPEG Decoder available here. It also instructs how to incorporate this decoder in your code.
Well, you might be wondering why we are extracting thumbnail if we still need a JPEG decoder!!! Why don’t we just decode the actual image and produced a reduced version of the image using above mentioned decoder?? The answer is, as I experienced it, it takes very long to decode the full image and producing a thumbnail, the situation is even worst while dealing with image of very high dimension e.g. 2048x1680, which is not good from your application point of view. So it’s a good idea to use the already embedded thumbnail which is far quicker to decode for the decoder, moreover, we should use this if already there because that’s the whole purpose of it!!!!!