Displaying GIF Images on J2ME Mobile Phones

Displaying GIF Images on J2ME Mobile Phones

With some phones, you have to roll your own GIF decoder

Tom Thompson
Surprisingly, many Java-based mobile phones couldn't display GIF image files--until now.

Tom is a technical writer providing support for J2ME wireless technologies at KPI Consulting Inc. He can be contacted at [email protected] .

 

 Today's mobile phones are versatile devices. Besides placing calls, they offer features such as Bluetooth connectivity, Internet support, built-in cameras that take pictures or capture video, and 3D graphics rendering capability, to name a few. Surprisingly, they sometimes lack certain important features. For instance, despite their ability to connect to and surf the Web, many mobile phones, from within their own Java environment, are unable to display a Graphics Interchange Format (GIF) file.

 I discovered this odd omission in graphics capability while writing a program that made use of a mobile phone's GPS APIs. The idea was to get a GPS fix of the phone's location (presumably its owner would be nearby) and present a map of the area. I thought that the hard part of the project would be to generate the location map, while the easy part would be to get the latitude and longitude position fix from the GPS APIs. I didn't anticipate any problems displaying the map, given the mobile phone's rich and varied graphics support.

 Typical of software-project scheduling, I got it all backwards: Obtaining the map turned out to be the easy part. I discovered the U.S. Census Department's Tiger server that, if provided latitude and longitude and the desired image size in pixels, renders a map and returns it as a GIF file to you. As you can see in Figure 1 , the map is exquisitely detailed, using standard map colors for parks, cities, state boundaries, and highways labeled with their route numbers.

 The hard part turned out to be displaying that GIF file. It took only several crashes in the debugging emulator to trace the problem to the Java 2 Mobility Edition (J2ME) platform, which is the phone's Java runtime environment. Simply put, J2ME couldn't decode and display GIF files. As the census server provides its maps only as GIF files, completing the project required that I roll my own GIF decoder for J2ME. In this article, I show how this was done. The complete source code that implements this technique is available electronically; see "Resource Center," page 6.

The Law of Unintended Consequences

 J2ME's inability to display GIFs is particularly baffling considering that some mobile phones can download JPEG, BMP, and animated GIF files as wallpaper. How could this gap in graphics capability occur? The answer lies in the unintended consequences of how J2ME implements security and graphics.

 The J2ME platform uses a "sandbox" model to carry out its security mechanism. MIDlets—mobile applications that are similar in design to a web browser's Java applet—execute within a separate memory space that's walled off from the phone's native operating system and resources. MIDlets are free to play in the sandbox, but can't access any hardware or resources outside of the box by themselves. Access to the phone's screen, audio system, and other hardware features is mediated through J2ME API classes. Simply put, if an API isn't available for a particular hardware feature (such as Bluetooth), a MIDlet can't use it. This prevents a malicious MIDlet from crashing the phone just before a critical call is placed, or tampering with the phone's address book, or doing funny things with Bluetooth.

 Per the specifications of J2ME's support APIs, the execution environment only has to implement the display of Portable Network Graphics (PNG) images. Ironically, support for this one particular format has to do with the legal problems surrounding GIF files.

 CompuServe invented the GIF format in the 1980s to handle the transfer and display of online graphics. Given the limited capabilities of the graphics hardware of the time, GIF files contained a 24-bit color table, with pixel color information represented as 8-bit values. The 8-bit value limits GIF files to displaying only 256 colors. Because of the low bandwidth connections, the file contents were reduced in size through the use of a Lempel-Ziv-Welch (LZW) compression scheme. For a while, the fact that the LZW algorithm was patented wasn't an issue. Then came the Web, and with it, the demand for graphic images soared. At the start of 1995, Unisys—the owner of the LZW patent—and CompuServe began aggressively pursuing fees for the use of GIF images.

 This event precipitated the development of the PNG format as an alternative to the GIF format, and by late 1996, the PNG file format was adopted as a standard by the W3C. PNG is a lossless image format that supports images with pixel depths ranging from 1 bit (black-and-white), up to 48 bits (what's termed "truecolor"), an alpha channel, and data compression. Crucially, PNG was carefully designed so that its data structures and compression algorithms didn't infringe on any patents. For more information on the PNG file format, see "PNG: The Portable Network Graphic Format," by Lee Daniel Crocker (DDJ , July 1995).

 In 1999, when Sun Microsystems developed the J2ME platform specification and its supporting APIs, PNG was chosen as the default image format because of its graphics capabilities, small file size, and that it was unencumbered with patent issues. GIF was, to a large extent, sidelined as a graphics format.

 The situation has improved for GIF since 2004, when the world-wide Unisys patents expired. While GIF has a limited color palette, this range is adequate for most graphic images, and its small size is still valuable for the mobile phone's limited bandwidth wireless connections. Its animation feature is widely supported by all web browsers.

 Because of GIF's checkered history, the J2ME platform often doesn't support the format. For the same reason, the native operating systems of many mobile phones don't handle GIF files, either. Even if the phone's native OS happens to support GIFs, the sandbox security mechanism blocks its use unless the vendor goes through the trouble to expose the GIF decoder routine to J2ME.

Checking for Native GIF Support

 Once I knew what the problem was, how could I go about fixing it? It might seem that when your MIDlet receives a GIF file, the best course of action is to simply invoke the GIF-decoder routine. Practically, to conserve the phone's limited working memory, you want to check for the presence of any GIF-decoding capability. J2ME has a collection of classes known as the Mobile Information Device Profile (MIDP) that implement the MIDlet's runtime environment and thus constitute its operating system. The MIDP 2.0 implementation provides utility methods that can query the host phone as to its capabilities and features.

 To determine if J2ME handles a specific media format, you invoke the getSupportedContentTypes() method. This method returns a string array of supported media types, and includes the audio and video formats along with the image formats. The strings present this information in MIME-type format. To check for GIF support, you scan this array, looking for the GIF MIME-type string. If there is a match, then the phone's J2ME implementation supports native display of GIF files. Listing One shows how to use getSupportedContentTypes() to obtain the media types array, along with loop code that does the array scan. The code sets the Boolean flag, gifSupported , if a match is found. Because this check only has to execute once, a good place for this code is in the class's constructor.

 If the MIDlet executes on one of those rare phones with GIF support, then displaying it becomes a matter of writing a few lines of code. J2ME stores its graphic data in an Image object. A method in this class, createImage(), takes as an argument an array whose data is organized into a self-identifying image format such as PNG, GIF, and others. All that's required is to route the data stream returned from the server into createImage(), and it generates the Image object for you.

 For the more common case, the mobile phone lacks GIF decoder support, so you must invoke the homemade GIF decoder class (call it GifDecoder ) to handle the chore. You'd therefore make an instance of the GIF decoder object, supply it with the data stream, and invoke one of GifDecode 's methods (call it read() ) to read and decode the data. Another method (getImage() ) then extracts the converted image and returns it in an Image object. Listing Two shows the code that tests the state of the gifSupported flag and calls the appropriate method.

Undertaking the Port

 Now all that was left to do was to close that gap in Listing Two by writing the GIF decoder class. Seasoned programmers don't want to reinvent the wheel if they can help it, particularly because the odds were good that a Java-based GIF decoder already existed. Therefore, I immediately Googled to see what code was available on the Internet. Unfortunately, Sturgeon's Law applies equally to Internet content: 90 percent of what the search turned up was junk. Still, the remaining 10 percent appeared promising and could be sifted through quickly.

 I soon happened upon a lightweight Java-based GIF decoder written by Kevin Weiner, from FM Software. His decoder class, GifDecoder , has a small resource footprint, provides LZW decompression, and offers methods that can read an image either from a data stream or from a file (http://www.fmsware.com/stuff/gif.html). The decoder could even parse animated GIF files and methods were provided to help animate such images. In addition, Kevin offered the source code to the community at large, with no stringent copyright conditions. GifDecoder was written for the J2SE platform, but porting this class to J2ME appeared manageable.

 The port of GifDecoder was relatively painless, thanks in large part to the fact that J2ME's capabilities are identical to J2SE in many areas. The largest variations between the two Java platforms lie in their GUI classes. Because GifDecoder processes data behind the scenes for other objects, this averted dealing with the GUI differences.

 I started by commenting out those GifDecoder methods I didn't need for my project. I eliminated GifDecoder's file read() method because my application receives its images over the air through an HTTP connection. I would have handled this differently if the target phones used either the Symbian OS or the JSR 75 FileConnection API, both of which provide file access. However, I did modify the file read() method to retrieve data from MIDlet resources. Resources are part of the MIDlet's archive file, and often store the graphic images the MIDlet uses to draw its GUI.

 Next, I changed all instances of BufferedInputStream to DataInputStream . The BufferedInputStream class isn't available on J2ME, and DataInputStream 's capabilities were adequate for the job. I did spend some time struggling with getting the code to read the GIF file's color tables. I tracked this down to a bug where, when reading an HTTP stream, DataInputStream 's read() method can throw an exception if the amount of bytes for the requested read is larger than the stream buffer's size (255 bytes). The workaround was to write a loop that used readByte() to fetch the color table, byte by byte.

 Another modification was to replace Image 's getRaster() J2SE method with a pair of MIDP 2.0 methods, getRGB() and createRGBImage(). J2SE's getRaster() , in one swoop, converts the decoded array of RGB pixels that comprise the picture into the Image object's native format. Because J2ME lacks getRaster() , the data translation became a two-step process. The getRGB() call converts the decoded GIF pixels into integers, and the subsequent call to createRGBImage() translates the integer array into the Image format.

 GifDecoder is also able to read GIF files that contain multiple images for animation, plus any control blocks that contain a delay interval associated with each image. See Figure 2 for the structure of an animated GIF file. On J2SE, GifDecoder stores the images and delay values using the ArrayList class. ArrayList isn't implemented on J2ME, but its superclass, Vector , is. So I modified all appearances of ArrayList to use Vector , and rewrote the code to use Vector' s methods.

 The entire process didn't take long, and I had an example GIF appearing on the phone's screen within a few days. The bulk of that time was spent tracking down and identifying the read() bug.

Using the GIF Decoder

 Adding the GIF decoder in a mobile application is easy: Just include the GifDecoder.java file along with the MIDlet's source files. In your application code, make an instance of GifDecoder, then use the appropriate methods to read and display the GIF images. Table 1 documents the methods available. Listing Two shows how it's done. Once you've made an instance of GifDecoder, you invoke the appropriate read() method, depending upon whether you obtained the GIF file from a data stream or are reading a MIDlet resource. If the read completes without errors, then you call getImage(), which returns the GIF picture in an Image object. If getImage() is applied to an animated GIF file, this method returns the first image in the file.

 Presenting an animated GIF is relatively straightforward, thanks to the utility methods GifDecoder provides. You first use getFrameCount() to fetch the value that specifies how many images (or frames) make up the animation. This becomes the termination value for a loop counter. Within the loop body, you pass the loop's index as an argument to both getDelay() and getFrame() to obtain each frame's delay time and its corresponding image, respectively.

 The delay values represent time intervals in hundredths of a second. Because most computers and cellphones have timers with millisecond resolution, getDelay() multiplies the value it obtains by 10 to convert the interval value into milliseconds. The result from getDelay() can therefore be jammed straight into J2ME's Thread.sleep() method to implement the delay. GifDecoder supports the drawing options specified in the GIF file's graphic-control extension blocks. It also supports a transparent color, although the phone's J2ME implementation can affect this feature's behavior.

 You'll execute the GIF animation code in a separate thread, either as part of a Thread class or as part of another class's run() method. This is done so that the MIDlet's UI—which runs in another thread—is free to respond to any events that the user generates. Listing Three shows how to use a run() method to read and display the images in an animated GIF file. Figure 3 shows how the successive images in a GIF can present the animation of a UFO. The serial serviceRepaints() method acts as a simple synchronization mechanism in that it ensures that the image is drawn completely before the next delay interval and image are fetched and displayed. An example MIDlet, GIF_Display , is available for download, and it displays several animated GIF images. Note that a phone that natively supports the GIF format may only display static images, not animated ones.

 When compiled, GifDecoder is only 11 KB in size, suitable for use in any MIDlet that has special graphics needs. Note that this size is before an obfusticator is applied to GifDecoder to further reduce its code footprint. The downside to using GifDecoder is that large and lengthy image animations can consume a lot of memory. An animation that consists of 24 144×52-pixel images can easily consume around 60 KB of memory. On a phone with 256 KB of working memory, that consumes over 20 percent of available memory just to support a graphic. You can reduce the animation's memory footprint by using smaller images and fewer of them.

Conclusion

 While a GIF image has a limited color palette, it's often adequate for most graphics purposes, including the display of some photos. In exchange for the limited color range, GIF images require less storage than JPEG images. In addition, there are plenty of GIF editing animation tools that allow you to easily add your own graphics and animations for addition to a MIDlet's UI. The ability to read, display, and animate GIF images that appear on many Internet sites is an important asset to have for any MIDlet performing network tasks. GifDecoder is thus a valuable tool to have in your J2ME programming toolbox.

 DDJ

Listing One

private  String   mediaTypes[];
private   final     String GIF_MIME_TYPE  =   " image/gif " ;
private   boolean   gifSupported;    

//  Get the media types to check for support of GIF file display
        mediaTypes  =  Manager.getSupportedContentTypes( null );
        
int  count  =  mediaTypes.length;

//  Check list for GIF MIME type; set support flag if present
        gifSupported  =   false ;
        
for  ( int  i  =   0 ; i  <  count; i ++ ) {
            
if  (mediaTypes[i]  ==  GIF_MIME_TYPE)
                gifSupported 
=   true ;
        } 
//  end for

Listing Two

private   static   final  String READ_OK  =   0
String          url 
=   " http://www.nosuchsite.net/StarTrek/enterprise.gif " ;

HttpConnection  hC 
=   null ;
DataInputStream dS 
=   null ;

Image           mapImage 
=   null ;


//  Open the connection as an HTTPConnection; send request
     try  {         
        hC 
=  (HttpConnection) Connector.open(url);
        hC.setRequestMethod(HttpConnection.GET);
        hC.setRequestProperty(
" IF-Modified-Since "
                               
" 10 Nov 2000 17:29:12 GMT " );
        hC.setRequestProperty(
" User-Agent " , " Profile/MIDP-2.0 
                               Configuration / CLDC - 1.1 " );
        hC.setRequestProperty( " Content-Language " " en-CA " );
    } 
catch  (IOException e) { }  //  running without safety net!

//  Read the data stream for the returned GIF image
     int  iByteCount;
    iByteCount 
=  ( int )hC.getLength();
    dS 
=  hC.openDataInputStream();
 
//  Does J2ME implementation support native GIF format decode?
     if  (gifSupported) {
        mapImage 
=  Image.createImage(dS);    //  Yes, translate data
                                            
//     into an Image
    }  else  {
//  No, do it ourselves: get instance of GIF decoder and decode stream
        GifDecoder d  =   new  GifDecoder();
        
if  (d  !=   null ) {
            
int  err  ==  d.read(dS);
            
if  (err  ==  READ_OK) {
                mapImage 
=  d.getImage();
            } 
// end if
        } end  if
    } 
//  end else

Listing Three

//  The run method for the class.
     public   void  run() {
    
int  t;
        
if  (gifImage  !=   null ) {
            
while  (action) { 
                
int  n  =  d.getFrameCount();      //  Get # of frames
                 for  ( int  i  =   0 ; i  <  n; i ++ ) {   //  Loop through all
                    gifImage  =  d.getFrame(i);   //  Get frame i
//  Delay duration for frame i in milliseconds
                    t  =  d.getDelay(i);          //  Get frame's delay
                    repaint();
                    serviceRepaints();
                    
try  {
                        Thread.sleep(t);       
//  Delay as directed
                    }  catch  (Exception ex){}
                } 
//  end for
            }  //  end while
        }  //  end if
    }  //  end run


Figure 1: GIF map output by the Tiger server.

 

 

Displaying GIF Images on J2ME Mobile Phones_第1张图片

Figure 2: Format of a GIF89a animated image file.




Displaying GIF Images on J2ME Mobile Phones_第2张图片

Figure 3: How an animated GIF image is displayed.

Displaying GIF Images on J2ME Mobile Phones

 

Result Constructor/Method Description
Constructor    
GifDecoder GifDecoder(void) Constructor that generates an instance of the GIF decoder object.
Methods
int getDelay(int frameNumber) Returns the delay time, in milliseconds, associated with the image specified by frameNumber.
Image getFrame(int frameNumber) Returns the image specified by frameNumber.
int getFrameCount(void) Returns the number of images (frames) in an animated GIF file.
Image getImage(void) Returns the first image in an animated GIF file.
int getLoopCount(void) Returns a value that represents the times the animation should loop.
int read(DataInputStream is) Reads the referenced DataInputStream object. Returns a status code of zero if no errors occurred during the read.
int read(InputStream is) Reads the referenced InputStream object. Returns a status code of zero if no errors occurred during the read.
int read(String name) Reads the named resource in the MIDlet's JAR file. Returns a status code of zero if no errors occurred during the read.

Table 1: GifDecoder class and methods.

你可能感兴趣的:(image,File,mobile,animation,j2me,methods)