/* * BMPEncoder.java * * Created on 11 May 2006, 04:19 * * To change this template, choose Tools | Template Manager * and open the template in the editor. */ package com.inet.gradle.setup.image.image4j.codec.bmp; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; import java.awt.image.Raster; import java.io.IOException; import com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream; /** * Encodes images in BMP format. * @author Ian McDonagh */ public class BMPEncoder { /** Creates a new instance of BMPEncoder */ private BMPEncoder() { } /** * Encodes and writes BMP data the output file * @param img the image to encode * @param file the file to which encoded data will be written * @throws java.io.IOException if an error occurs */ public static void write(BufferedImage img, java.io.File file) throws IOException { write(img, new java.io.FileOutputStream(file)); } /** * Encodes and writes BMP data to the output * @param img the image to encode * @param os the output to which encoded data will be written * @throws java.io.IOException if an error occurs */ public static void write(BufferedImage img, java.io.OutputStream os) throws IOException { // create info header InfoHeader ih = createInfoHeader(img); // Create colour map if the image uses an indexed colour model. // Images with colour depth of 8 bits or less use an indexed colour model. int mapSize = 0; IndexColorModel icm = null; if (ih.sBitCount <= 8) { icm = (IndexColorModel) img.getColorModel(); mapSize = icm.getMapSize(); } // Calculate header size int headerSize = 14 //file header + ih.iSize //info header ; // Calculate map size int mapBytes = 4 * mapSize; // Calculate data offset int dataOffset = headerSize + mapBytes; // Calculate bytes per line int bytesPerLine = 0; switch (ih.sBitCount) { case 1: bytesPerLine = getBytesPerLine1(ih.iWidth); break; case 4: bytesPerLine = getBytesPerLine4(ih.iWidth); break; case 8: bytesPerLine = getBytesPerLine8(ih.iWidth); break; case 24: bytesPerLine = getBytesPerLine24(ih.iWidth); break; case 32: bytesPerLine = ih.iWidth * 4; break; } // calculate file size int fileSize = dataOffset + bytesPerLine * ih.iHeight; // output little endian byte order LittleEndianOutputStream out = new LittleEndianOutputStream(os); //write file header writeFileHeader(fileSize, dataOffset, out); //write info header ih.write(out); //write color map (bit count <= 8) if (ih.sBitCount <= 8) { writeColorMap(icm, out); } //write raster data switch (ih.sBitCount) { case 1: write1(img.getRaster(), out); break; case 4: write4(img.getRaster(), out); break; case 8: write8(img.getRaster(), out); break; case 24: write24(img.getRaster(), out); break; case 32: write32(img.getRaster(), img.getAlphaRaster(), out); break; } } /** * Creates an <tt>InfoHeader</tt> from the source image. * @param img the source image * @return the resultant <tt>InfoHeader</tt> structure */ public static InfoHeader createInfoHeader(BufferedImage img) { InfoHeader ret = new InfoHeader(); ret.iColorsImportant = 0; ret.iColorsUsed = 0; ret.iCompression = 0; ret.iHeight = img.getHeight(); ret.iWidth = img.getWidth(); ret.sBitCount = (short) img.getColorModel().getPixelSize(); ret.iNumColors = 1 << (ret.sBitCount == 32 ? 24 : ret.sBitCount); ret.iImageSize = 0; return ret; } /** * Writes the file header. * @param fileSize the calculated file size for the BMP data being written * @param dataOffset the calculated offset within the BMP data where the actual bitmap begins * @param out the output to which the file header will be written * @throws java.io.IOException if an error occurs */ public static void writeFileHeader(int fileSize, int dataOffset, com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream out) throws IOException { //signature byte[] signature = BMPConstants.FILE_HEADER.getBytes("UTF-8"); out.write(signature); //file size out.writeIntLE(fileSize); //reserved out.writeIntLE(0); //data offset out.writeIntLE(dataOffset); } /** * Writes the colour map resulting from the source <tt>IndexColorModel</tt>. * @param icm the source <tt>IndexColorModel</tt> * @param out the output to which the colour map will be written * @throws java.io.IOException if an error occurs */ public static void writeColorMap(IndexColorModel icm, com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream out) throws IOException { int mapSize = icm.getMapSize(); for (int i = 0; i < mapSize; i++) { int rgb = icm.getRGB(i); int r = (rgb >> 16) & 0xFF; int g = (rgb >> 8) & 0xFF; int b = (rgb) &0xFF; out.writeByte(b); out.writeByte(g); out.writeByte(r); out.writeByte(0); } } /** * Calculates the number of bytes per line required for the given width in pixels, * for a 1-bit bitmap. Lines are always padded to the next 4-byte boundary. * @param width the width in pixels * @return the number of bytes per line */ public static int getBytesPerLine1(int width) { int ret = (int) width / 8; if (ret % 4 != 0) { ret = (ret / 4 + 1) * 4; } return ret; } /** * Calculates the number of bytes per line required for the given with in pixels, * for a 4-bit bitmap. Lines are always padded to the next 4-byte boundary. * @param width the width in pixels * @return the number of bytes per line */ public static int getBytesPerLine4(int width) { int ret = (int) width / 2; if (ret % 4 != 0) { ret = (ret / 4 + 1) * 4; } return ret; } /** * Calculates the number of bytes per line required for the given with in pixels, * for a 8-bit bitmap. Lines are always padded to the next 4-byte boundary. * @param width the width in pixels * @return the number of bytes per line */ public static int getBytesPerLine8(int width) { int ret = width; if (ret % 4 != 0) { ret = (ret / 4 + 1) * 4; } return ret; } /** * Calculates the number of bytes per line required for the given with in pixels, * for a 24-bit bitmap. Lines are always padded to the next 4-byte boundary. * @param width the width in pixels * @return the number of bytes per line */ public static int getBytesPerLine24(int width) { int ret = width * 3; if (ret % 4 != 0) { ret = (ret / 4 + 1) * 4; } return ret; } /** * Calculates the size in bytes of a bitmap with the specified size and colour depth. * @param w the width in pixels * @param h the height in pixels * @param bpp the colour depth (bits per pixel) * @return the size of the bitmap in bytes */ public static int getBitmapSize(int w, int h, int bpp) { int bytesPerLine = 0; switch (bpp) { case 1: bytesPerLine = getBytesPerLine1(w); break; case 4: bytesPerLine = getBytesPerLine4(w); break; case 8: bytesPerLine = getBytesPerLine8(w); break; case 24: bytesPerLine = getBytesPerLine24(w); break; case 32: bytesPerLine = w * 4; break; } int ret = bytesPerLine * h; return ret; } /** * Encodes and writes raster data as a 1-bit bitmap. * @param raster the source raster data * @param out the output to which the bitmap will be written * @throws java.io.IOException if an error occurs */ public static void write1(Raster raster, com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream out) throws IOException { int bytesPerLine = getBytesPerLine1(raster.getWidth()); byte[] line = new byte[bytesPerLine]; for (int y = raster.getHeight() - 1; y >= 0; y--) { for (int i = 0; i < bytesPerLine; i++) { line[i] = 0; } for (int x = 0; x < raster.getWidth(); x++) { int bi = x / 8; int i = x % 8; int index = raster.getSample(x, y, 0); line[bi] = setBit(line[bi], i, index); } out.write(line); } } /** * Encodes and writes raster data as a 4-bit bitmap. * @param raster the source raster data * @param out the output to which the bitmap will be written * @throws java.io.IOException if an error occurs */ public static void write4(Raster raster, com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream out) throws IOException { // The approach taken here is to use a buffer to hold encoded raster data // one line at a time. // Perhaps we could just write directly to output instead // and avoid using a buffer altogether. Hypothetically speaking, // a very wide image would require a large line buffer here, but then again, // large 4 bit bitmaps are pretty uncommon, so using the line buffer approach // should be okay. int width = raster.getWidth(); int height = raster.getHeight(); // calculate bytes per line int bytesPerLine = getBytesPerLine4(width); // line buffer byte[] line = new byte[bytesPerLine]; // encode and write lines for (int y = height - 1; y >= 0; y--) { // clear line buffer for (int i = 0; i < bytesPerLine; i++) { line[i] = 0; } // encode raster data for line for (int x = 0; x < width; x++) { // calculate buffer index int bi = x / 2; // calculate nibble index (high order or low order) int i = x % 2; // get color index int index = raster.getSample(x, y, 0); // set color index in buffer line[bi] = setNibble(line[bi], i, index); } // write line data (padding bytes included) out.write(line); } } /** * Encodes and writes raster data as an 8-bit bitmap. * @param raster the source raster data * @param out the output to which the bitmap will be written * @throws java.io.IOException if an error occurs */ public static void write8(Raster raster, com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream out) throws IOException { int width = raster.getWidth(); int height = raster.getHeight(); // calculate bytes per line int bytesPerLine = getBytesPerLine8(width); // write lines for (int y = height - 1; y >= 0; y--) { // write raster data for each line for (int x = 0; x < width; x++) { // get color index for pixel int index = raster.getSample(x, y, 0); // write color index out.writeByte(index); } // write padding bytes at end of line for (int i = width; i < bytesPerLine; i++) { out.writeByte(0); } } } /** * Encodes and writes raster data as a 24-bit bitmap. * @param raster the source raster data * @param out the output to which the bitmap will be written * @throws java.io.IOException if an error occurs */ public static void write24(Raster raster, com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream out) throws IOException { int width = raster.getWidth(); int height = raster.getHeight(); // calculate bytes per line int bytesPerLine = getBytesPerLine24(width); // write lines for (int y = height - 1; y >= 0; y--) { // write pixel data for each line for (int x = 0; x < width; x++) { // get RGB values for pixel int r = raster.getSample(x, y, 0); int g = raster.getSample(x, y, 1); int b = raster.getSample(x, y, 2); // write RGB values out.writeByte(b); out.writeByte(g); out.writeByte(r); } // write padding bytes at end of line for (int i = width * 3; i < bytesPerLine; i++) { out.writeByte(0); } } } /** * Encodes and writes raster data, together with alpha (transparency) data, as a 32-bit bitmap. * @param raster the source raster data * @param alpha the source alpha data * @param out the output to which the bitmap will be written * @throws java.io.IOException if an error occurs */ public static void write32(Raster raster, Raster alpha, com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream out) throws IOException { int width = raster.getWidth(); int height = raster.getHeight(); // write lines for (int y = height - 1; y >= 0; y--) { // write pixel data for each line for (int x = 0; x < width; x++) { // get RGBA values int r = raster.getSample(x, y, 0); int g = raster.getSample(x, y, 1); int b = raster.getSample(x, y, 2); int a = alpha.getSample(x, y, 0); // write RGBA values out.writeByte(b); out.writeByte(g); out.writeByte(r); out.writeByte(a); } } } /** * Sets a particular bit in a byte. * @param bits the source byte * @param index the index of the bit to set * @param bit the value for the bit, which should be either <tt>0</tt> or <tt>1</tt>. * @param the resultant byte */ private static byte setBit(byte bits, int index, int bit) { if (bit == 0) { bits &= ~(1 << (7 - index)); } else { bits |= 1 << (7 - index); } return bits; } /** * Sets a particular nibble (4 bits) in a byte. * @param nibbles the source byte * @param index the index of the nibble to set * @param the value for the nibble, which should be in the range <tt>0x0..0xF</tt>. */ private static byte setNibble(byte nibbles, int index, int nibble) { nibbles |= (nibble << ((1 - index) * 4)); return nibbles; } /** * Calculates the size in bytes for a colour map with the specified bit count. * @param sBitCount the bit count, which represents the colour depth * @return the size of the colour map, in bytes if <tt>sBitCount</tt> is less than or equal to 8, * otherwise <tt>0</tt> as colour maps are only used for bitmaps with a colour depth of 8 bits or less. */ public static int getColorMapSize(short sBitCount) { int ret = 0; if (sBitCount <= 8) { ret = (1 << sBitCount) * 4; } return ret; } }