J2ME is bare-bones and I recently realised that it doesn’t have any image encoders. Sure, you can create Image objects… but there is no way to persist them to disc or to send them over the network in a recognised format.

With the goal of creating a low-memory PNG encoder based on TinyLine GZIPInputStream, I wrote an uncompressed BMP encoder as a gentle introduction. I am posting it here (under the LGPL) incase it helps somebody out… I got tripped up by some file format details (including Endianness) so you don’t need to go through it.

Unfortunately, you won’t be able to load BMPs and display them without writing your own decoder! (J2ME only supports PNG)… but at least it allows you to communicate images with the outside world.

One of the biggest problems with J2ME is that there aren’t any great open source libraries out there. That’s one of Java’s greatest strengths. I intend to post a few of my J2ME utility classes here as I write them. I’m currently working on a hobby project which I intend to release here, and if it goes according to plan, I intend to reveal some bluetooth convenience classes.

/*
 * Copyright ThinkTank Mathematics Limited 2007
 *
 * This file is free software: you can redistribute it and/or modify it under the terms of
 * the GNU Lesser General Public License as published by the Free Software Foundation, either
 * version 3 of the License, or (at your option) any later version.
 *
 * This file is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this file.
 * If not, see .
 */
package thinktank.j2me;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import javax.microedition.lcdui.Image;

/**
 * @author Samuel Halliday, ThinkTank Mathematics Limited
 */
public final class BMPGenerator {

    /**
     * @param image
     * @return
     * @throws IOException
     * @see {@link #encodeBMP(int[], int, int)}
     */
    public static byte[] encodeBMP(Image image) throws IOException {
        int width = image.getWidth();
        int height = image.getHeight();
        int[] rgb = new int[height * width];
        image.getRGB(rgb, 0, width, 0, 0, width, height);
        return encodeBMP(rgb, width, height);
    }

    /**
     * A self-contained BMP generator, which takes a byte array (without any unusual
     * offsets) extracted from an {@link Image}. The target platform is J2ME. You may
     * wish to use the convenience method {@link #encodeBMP(Image)} instead of this.
     * 

* A BMP file consists of 4 parts:- *

    *
  • header
  • *
  • information header
  • *
  • optional palette
  • *
  • image data
  • *
* At this time only 24 bit uncompressed BMPs with Windows V3 headers can be created. * Future releases may become much more space-efficient, but will most likely be * ditched in favour of a PNG generator. * * @param rgb * @param width * @param height * @return * @throws IOException * @see http://en.wikipedia.org/wiki/Windows_bitmap */ public static byte[] encodeBMP(int[] rgb, int width, int height) throws IOException { int pad = (4 - (width % 4)) % 4; // the size of the BMP file in bytes int size = 14 + 40 + height * (pad + width * 3); ByteArrayOutputStream bytes = new ByteArrayOutputStream(size); DataOutputStream stream = new DataOutputStream(bytes); // HEADER // the magic number used to identify the BMP file: 0x42 0x4D stream.writeByte(0x42); stream.writeByte(0x4D); stream.writeInt(swapEndian(size)); // reserved stream.writeInt(0); // the offset, i.e. starting address of the bitmap data stream.writeInt(swapEndian(14 + 40)); // INFORMATION HEADER (Windows V3 header) // the size of this header (40 bytes) stream.writeInt(swapEndian(40)); // the bitmap width in pixels (signed integer). stream.writeInt(swapEndian(width)); // the bitmap height in pixels (signed integer). stream.writeInt(swapEndian(height)); // the number of colour planes being used. Must be set to 1. stream.writeShort(swapEndian((short) 1)); // the number of bits per pixel, which is the colour depth of the image. stream.writeShort(swapEndian((short) 24)); // the compression method being used. stream.writeInt(0); // image size. The size of the raw bitmap data. 0 is valid for uncompressed. stream.writeInt(0); // the horizontal resolution of the image. (pixel per meter, signed integer) stream.writeInt(0); // the vertical resolution of the image. (pixel per meter, signed integer) stream.writeInt(0); // the number of colours in the colour palette, or 0 to default to 2n. stream.writeInt(0); // the number of important colours used, or 0 when every colour is important; // generally ignored. stream.writeInt(0); // PALETTE // none for 24 bit depth // IMAGE DATA // starting in the bottom left, working right and then up // a series of 3 bytes per pixel in the order B G R. for (int j = height - 1; j >= 0; j--) { for (int i = 0; i < width; i++) { int val = rgb[i + width * j]; stream.writeByte(val & 0x000000FF); stream.writeByte((val >>> 8) & 0x000000FF); stream.writeByte((val >>> 16) & 0x000000FF); } // number of bytes in each row must be padded to multiple of 4 for (int i = 0; i < pad; i++) { stream.writeByte(0); } } byte[] out = bytes.toByteArray(); bytes.close(); // quick consistency check if (out.length != size) { throw new RuntimeException("bad math"); } return out; } /** * Swap the Endian-ness of a 32 bit integer. * * @param value * @return */ private static int swapEndian(int value) { int b1 = value & 0xff; int b2 = (value >> 8) & 0xff; int b3 = (value >> 16) & 0xff; int b4 = (value >> 24) & 0xff; return b1 << 24 | b2 << 16 | b3 << 8 | b4 << 0; } /** * Swap the Endian-ness of a 16 bit integer. * * @param value * @return */ private static short swapEndian(short value) { int b1 = value & 0xff; int b2 = (value >> 8) & 0xff; return (short) (b1 << 8 | b2 << 0); } }