3

I have byte arrays stored in the database, previously converted from BufferedImage with:

ImageIO.write(bufferedImage, extension, bao);
return bao.toByteArray();

With those images I want to create a texture atlas (i.e. a sprite sheet). The most naive solution would be to convert every single byte[] back to a BufferedImage (about 500k times), write them to the texture atlas, and when done, convert the texture atlas' BufferedImage to byte[] back.

I guess the most elegant way would be to just concatenate all byte arrays, yet I doubt that would work (with headers etc.), or would it?

2 Answers 2

3

This would only be possible if you have only uncompressed image data (like BMP, TIFF, TGA or PBM file formats).

Assuming you have a whole bunch of BMP files (which is in 3D programming very common), you have to change some bytes in the header.

  • bytes 3 to 6: image file size in bytes (because the file size will increase)
  • bytes 19 to 26: image width and height (because this will change the height and/or width in your new image)

Some more information you need:

  • bytes 11 to 14: reflects the start position of the data content

Furthermore, you have to set your byte order of the content correctly. If you want to concatenate your images one below another, than you only have to concatenate the bytes of the second image to the first (except the bytes of the header; content starts usually with byte 54, look at the bytes 11 to 14 for position).

For visualization purpose:

#########
#########
######### Image 1
#########
#########
=========
=========
========= Image 2
=========
=========

If you want to concatenate them to the right, you have to read each first line of each image and concatenate them. Then the second line and so on...:

 Image 1  Image 2
#########=========
#########=========
#########=========
#########=========
#########=========

For your purpose i recommend putting them below each other.

Then you have to be aware, that the image byte orders of the images are reversed (beginning with the first pixel from the bottom, then the second pixel from the bottom, and so on...).
Also there is a possible padding byte column, see http://en.wikipedia.org/wiki/BMP_file_format#Pixel_storage.

There are probably some more bytes, you have to set in the resulting BMP format file header. For further reading of the byte header structure see: http://en.wikipedia.org/wiki/BMP_file_format#File_structure

The bytes of the first image could be something like this (only red color pixels):

-- Header ----------------------------------------------------------------------
42  4D *86  00  00  00* 00  00  00  00 *36  00  00  00* 28  00  BM........6...(.
00  00  05  00  00  00 *05  00  00  00* 01  00  18  00  00  00  ................
00  00  50  00  00  00  00  00  00  00  00  00  00  00  00  00  ..P.............
00  00  00  00  00  00                                          ......
-- Body / Content --------------------------------------------------------------
                         v---v---v---< red pixel RGB FF,00,00 in reverse >
      < Padding >---v   00  00  FF  00  00  FF  00  00  FF  00        ..........
00  FF  00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  ................
00  FF  00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  ................
00  FF  00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  ................
00  FF  00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  ................
00  FF  00  00  FF  00                                          ......

And this could be a concatenated byte order of both (assuming the second image only consists of blue pixels):

-- Header ----------------------------------------------------------------------
42  4D  *D6 00  00  00* 00  00  00  00 *36  00  00  00* 28  00  BM........6...(.
00  00  05  00  00  00 *0A  00  00  00* 01  00  18  00  00  00  ................
00  00  A0  00  00  00  00  00  00  00  00  00  00  00  00  00  ................
00  00  00  00  00  00                                          ......
-- Body / Content --------------------------------------------------------------
                         v---v---v---< blue pixel RGB 00,00,FF in reverse >
      < Padding >---v   FF  00  00  FF  00  00  FF  00  00  FF        ..........
00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  00  FF  ................
00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  00  FF  ................
00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  00  FF  ................
00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  00  FF  ................
00  00  FF  00  00  00  00  00  FF  00  00  FF  00  00  FF  00  ................
00  FF  00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  ................
00  FF  00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  ................
00  FF  00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  ................
00  FF  00  00  FF  00  00  00  FF  00  00  FF  00  00  FF  00  ................
00  FF  00  00  FF  00                                          ......

(marked important with stars *...*) As you can see, in the header of the first image you can find the size of 05 00 00 00 which means 5 pixels of height. In the second header it is set to 0A 00 00 00, which is hexadecimal and means 10 pixels. The width is represented by the 4 bytes in front of them (which are in this case not modified, since the width will be the same). If you compare these two byte orders with the description of the BMP file header and content, you could imagine, how you have to set your bytes correctly.

Because of I was very interested by myself, how this can be done, I've written an example programm to do the task:

import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.imageio.ImageIO;

public class ConcatImages {
    private static final int POS_FILE_SIZE = 2;
    private static final int POS_START_CONTENT = 10;
    private static final int POS_WIDTH = 18;
    private static final int POS_HEIGHT = 22;

    public static void main(final String[] args) throws IOException, IllegalAccessException {
        final String[] files = {
            "image1.bmp", "image2.bmp", "image3.bmp"
        };

        concatBMPFiles(files, "result_image.bmp");
    }

    private static void concatBMPFiles(final String[] filenames, final String resultFilename) throws IOException, IllegalAccessException {
        final byte[][] fileContents = new byte[filenames.length][];

        int i = 0;

        for (final String file : filenames) {
            fileContents[i++] = readImageBytes(file);
        }

        final byte[] result = concatBMPImageData(fileContents);

        final OutputStream out = new BufferedOutputStream(new FileOutputStream(resultFilename));
        out.write(result);
        out.close();
    }

    private static byte[] concatBMPImageData(final byte[] ... imageDatas) throws IllegalAccessException {
        int newFileSize = 0;
        int newHeight = 0;
        int compWidth = -1;

        for (final byte[] imageData : imageDatas) {
            if (compWidth > -1) {
                // remove header length for all images, except the first
                newFileSize -=  getInt(imageData, POS_START_CONTENT);

                if (compWidth != getInt(imageDatas[0], POS_WIDTH)) {
                    throw new IllegalAccessException("All images must have the same width!");
                }
            } else {
                compWidth = getInt(imageDatas[0], POS_WIDTH);
            }

            newHeight += getInt(imageData, POS_HEIGHT);
            newFileSize += imageData.length;
        }

        newFileSize += getInt(imageDatas[0], POS_START_CONTENT);

        final byte[] result = new byte[newFileSize];
        int idx = 0;

        // read header from first image
        for (int i = 0; i < getInt(imageDatas[0], POS_START_CONTENT); i++) {
            result[idx++] = imageDatas[0][i];
        }

        // read content from all images
        for (int fIdx = imageDatas.length - 1; fIdx >= 0; fIdx--) {
            final int startContentDest = getInt(imageDatas[fIdx], POS_START_CONTENT);

            for (int i = startContentDest; i < imageDatas[fIdx].length; i++) {
                result[idx++] = imageDatas[fIdx][i];
            }
        }

        // set new file size to header
        setInt(result, POS_FILE_SIZE, newFileSize);

        // set new height to header
        setInt(result, POS_HEIGHT, newHeight);

        return result;
    }

    private static byte[] readImageBytes(final String filename) throws IOException {
        final BufferedImage image = ImageIO.read(new File(filename));
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();

        ImageIO.write(image, "bmp", baos);

        return baos.toByteArray();
    }

    private static int getInt(byte[] src, int start) {
        return ((0xFF & src[start + 3]) << 24) |
            ((0xFF & src[start + 2]) << 16) |
            ((0xFF & src[start + 1]) << 8) |
            (0xFF & src[start]);
    }

    private static void setInt(byte[] src, int start, int newValue) {
        byte[] value = intToByteArr(newValue);

        src[start] = value[3];
        src[start + 1] = value[2];
        src[start + 2] = value[1];
        src[start + 3] = value[0];
    }

    private static byte[] intToByteArr(int value) {
        byte[] result = new byte[4];

        for (int i = 0; i < 4; i++) {
            int shift = i << 3;
            result[3 - i] = (byte) ((value & (0xff << shift)) >>> shift);
        }

        return result;
    }
}

This is just a first version and it works for simple BMP files. For your purpose, you probably have to use the method concatBMPImageData directly instead of concatBMPFiles. Let me know, if this works for you, too!

Sign up to request clarification or add additional context in comments.

5 Comments

Note that the term "bitmaps" is commonly used to refer to raster images, not only the BMP file format. You may want to clarify your answer.
Thanks for the information! I changed the answer! Can you specify some other uncompressed raster image formats next to the BMP file format?
Some formats such as TIFF and TGA can store image data uncompressed (although they also support compression optionally). Also the PBM family of file formats (not very widely used nowadays).
Thank you again for your examples! Additionally I've now extended my answer with a possible solution in Java. This works as expected for three simple bitmaps.
How do it for PNG image formats?
2

The "naive" way is also the only way, and a good way. I don't know why you don't want to do that.

I guess the most elegant way would be to just concatenate all byte arrays, yet I doubt that would work (with headers etc.) - or would it?

It would not really be more elegant, and it would only work if the images were originally saved in some extremely primitive raw or headerless bitmap format.

10 Comments

I see. Problem is that I need to do this 500k times and I'm unsure about the overhead. Since I need to do it that often I want to squeeze out every inch of performance, but if you say that's the wrong place then so it be. I'll give chance for others to answer and accept in about 2-3 hours :) Thanks
Why do you need to do it 500k times? Is that really how many images you have?
Yeah. I have a database of images and for a 3D client I need textureAtlases of 16x16px icons, which are those images (they are already in the right scale, and as byte arrays). On one texture atlas I can put 65536 icons. Spoken in code: pastebin.com/dLnvWMwQ
I don't really see a better way to do it, although the whole process can't take more than a minute or so, can it? The time taken to decompress your sprites will depend on the image format they're saved in though: JPEG and PNG are probably slower than GIF, and all are slower than a bitmap. Once they're as BufferedImages in memory, the rest of the work is just shoveling bytes around in RAM, so it won't make much difference whether you use Graphics.drawImage or System.arrayCopy.
In theory, definitely yes. In practice, you just don't know. GPU acceleration technology has been around for decades and now even mobile phones have it. Whether a particular implementation of BufferedImage takes advantage of this is another story. I didn't even say that this was the case, only that it might.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.