Question

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?

Was it helpful?

Solution

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!

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top