/* * Decodes a BMP image from an <tt>InputStream</tt> to a <tt>BufferedImage</tt> * * @author Ian McDonagh */ package com.inet.gradle.setup.image.image4j.codec.bmp; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; import java.awt.image.WritableRaster; import java.io.IOException; /** * Decodes images in BMP format. * @author Ian McDonagh */ public class BMPDecoder { private BufferedImage img; private InfoHeader infoHeader; /** Creates a new instance of BMPDecoder and reads the BMP data from the source. * @param in the source <tt>InputStream</tt> from which to read the BMP data * @throws java.io.IOException if an error occurs */ public BMPDecoder(java.io.InputStream in) throws IOException { com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis = new com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream(in); /* header [14] */ //signature "BM" [2] byte[] bsignature = new byte[2]; lis.read(bsignature); String signature = new String(bsignature, "UTF-8"); if (!signature.equals("BM")) { throw new IOException("Invalid signature '"+signature+"' for BMP format"); } //file size [4] int fileSize = lis.readIntLE(); //reserved = 0 [4] int reserved = lis.readIntLE(); //DataOffset [4] file offset to raster data int dataOffset = lis.readIntLE(); /* info header [40] */ infoHeader = readInfoHeader(lis); /* Color table and Raster data */ img = read(infoHeader, lis); } /** * Retrieves a bit from the lowest order byte of the given integer. * @param bits the source integer, treated as an unsigned byte * @param index the index of the bit to retrieve, which must be in the range <tt>0..7</tt>. * @return the bit at the specified index, which will be either <tt>0</tt> or <tt>1</tt>. */ private static int getBit(int bits, int index) { return (bits >> (7 - index)) & 1; } /** * Retrieves a nibble (4 bits) from the lowest order byte of the given integer. * @param nibbles the source integer, treated as an unsigned byte * @param index the index of the nibble to retrieve, which must be in the range <tt>0..1</tt>. * @return the nibble at the specified index, as an unsigned byte. */ private static int getNibble(int nibbles, int index) { return (nibbles >> (4 * (1 - index))) & 0xF; } /** * The <tt>InfoHeader</tt> structure, which provides information about the BMP data. * @return the <tt>InfoHeader</tt> structure that was read from the source data when this <tt>BMPDecoder</tt> * was created. */ public InfoHeader getInfoHeader() { return infoHeader; } /** * The decoded image read from the source input. * @return the <tt>BufferedImage</tt> representing the BMP image. */ public BufferedImage getBufferedImage() { return img; } private static void getColorTable(ColorEntry[] colorTable, byte[] ar, byte[] ag, byte[] ab) { for (int i = 0; i < colorTable.length; i++) { ar[i] = (byte) colorTable[i].bRed; ag[i] = (byte) colorTable[i].bGreen; ab[i] = (byte) colorTable[i].bBlue; } } /** * Reads the BMP info header structure from the given <tt>InputStream</tt>. * @param lis the <tt>InputStream</tt> to read * @return the <tt>InfoHeader</tt> structure * @throws java.io.IOException if an error occurred */ public static InfoHeader readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis) throws IOException { InfoHeader infoHeader = new InfoHeader(lis); return infoHeader; } /** * @since 0.6 */ public static InfoHeader readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis, int infoSize) throws IOException { InfoHeader infoHeader = new InfoHeader(lis, infoSize); return infoHeader; } /** * Reads the BMP data from the given <tt>InputStream</tt> using the information * contained in the <tt>InfoHeader</tt>. * @param lis the source input * @param infoHeader an <tt>InfoHeader</tt> that was read by a call to * {@link #readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream) readInfoHeader()}. * @return the decoded image read from the source input * @throws java.io.IOException if an error occurs */ public static BufferedImage read(InfoHeader infoHeader, com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis) throws IOException { BufferedImage img = null; /* Color table (palette) */ ColorEntry[] colorTable = null; //color table is only present for 1, 4 or 8 bit (indexed) images if (infoHeader.sBitCount <= 8) { colorTable = readColorTable(infoHeader, lis); } img = read(infoHeader, lis, colorTable); return img; } /** * Reads the BMP data from the given <tt>InputStream</tt> using the information * contained in the <tt>InfoHeader</tt>. * @param colorTable <tt>ColorEntry</tt> array containing palette * @param infoHeader an <tt>InfoHeader</tt> that was read by a call to * {@link #readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream) readInfoHeader()}. * @param lis the source input * @return the decoded image read from the source input * @throws java.io.IOException if any error occurs */ public static BufferedImage read(InfoHeader infoHeader, com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis, ColorEntry[] colorTable) throws IOException { BufferedImage img = null; //1-bit (monochrome) uncompressed if (infoHeader.sBitCount == 1 && infoHeader.iCompression == BMPConstants.BI_RGB) { img = read1(infoHeader, lis, colorTable); } //4-bit uncompressed else if (infoHeader.sBitCount == 4 && infoHeader.iCompression == BMPConstants.BI_RGB) { img = read4(infoHeader, lis, colorTable); } //8-bit uncompressed else if (infoHeader.sBitCount == 8 && infoHeader.iCompression == BMPConstants.BI_RGB) { img = read8(infoHeader, lis, colorTable); } //24-bit uncompressed else if (infoHeader.sBitCount == 24 && infoHeader.iCompression == BMPConstants.BI_RGB) { img = read24(infoHeader, lis); } //32bit uncompressed else if (infoHeader.sBitCount == 32 && infoHeader.iCompression == BMPConstants.BI_RGB) { img = read32(infoHeader, lis); } else { throw new IOException("Unrecognized bitmap format: bit count="+infoHeader.sBitCount+", compression="+ infoHeader.iCompression); } return img; } /** * Reads the <tt>ColorEntry</tt> table from the given <tt>InputStream</tt> using * the information contained in the given <tt>infoHeader</tt>. * @param infoHeader the <tt>InfoHeader</tt> structure, which was read using * {@link #readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream) readInfoHeader()} * @param lis the <tt>InputStream</tt> to read * @throws java.io.IOException if an error occurs * @return the decoded image read from the source input */ public static ColorEntry[] readColorTable(InfoHeader infoHeader, com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis) throws IOException { ColorEntry[] colorTable = new ColorEntry[infoHeader.iNumColors]; for (int i = 0; i < infoHeader.iNumColors; i++) { ColorEntry ce = new ColorEntry(lis); colorTable[i] = ce; } return colorTable; } /** * Reads 1-bit uncompressed bitmap raster data, which may be monochrome depending on the * palette entries in <tt>colorTable</tt>. * @param infoHeader the <tt>InfoHeader</tt> structure, which was read using * {@link #readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream) readInfoHeader()} * @param lis the source input * @param colorTable <tt>ColorEntry</tt> array specifying the palette, which * must not be <tt>null</tt>. * @throws java.io.IOException if an error occurs * @return the decoded image read from the source input */ public static BufferedImage read1(InfoHeader infoHeader, com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis, ColorEntry[] colorTable) throws IOException { //1 bit per pixel or 8 pixels per byte //each pixel specifies the palette index byte[] ar = new byte[colorTable.length]; byte[] ag = new byte[colorTable.length]; byte[] ab = new byte[colorTable.length]; getColorTable(colorTable, ar, ag, ab); IndexColorModel icm = new IndexColorModel( 1, 2, ar, ag, ab ); // Create indexed image BufferedImage img = new BufferedImage( infoHeader.iWidth, infoHeader.iHeight, BufferedImage.TYPE_BYTE_BINARY, icm ); // We'll use the raster to set samples instead of RGB values. // The SampleModel of an indexed image interprets samples as // the index of the colour for a pixel, which is perfect for use here. WritableRaster raster = img.getRaster(); //padding int dataBitsPerLine = infoHeader.iWidth; int bitsPerLine = dataBitsPerLine; if (bitsPerLine % 32 != 0) { bitsPerLine = (bitsPerLine / 32 + 1) * 32; } int padBits = bitsPerLine - dataBitsPerLine; int padBytes = padBits / 8; int bytesPerLine = (int) (bitsPerLine / 8); int[] line = new int[bytesPerLine]; for (int y = infoHeader.iHeight - 1; y >= 0; y--) { for (int i = 0; i < bytesPerLine; i++) { line[i] = lis.readUnsignedByte(); } for (int x = 0; x < infoHeader.iWidth; x++) { int i = x / 8; int v = line[i]; int b = x % 8; int index = getBit(v, b); //int rgb = c[index]; //img.setRGB(x, y, rgb); //set the sample (colour index) for the pixel raster.setSample(x, y, 0, index); } } return img; } /** * Reads 4-bit uncompressed bitmap raster data, which is interpreted based on the colours * specified in the palette. * @param infoHeader the <tt>InfoHeader</tt> structure, which was read using * {@link #readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream) readInfoHeader()} * @param lis the source input * @param colorTable <tt>ColorEntry</tt> array specifying the palette, which * must not be <tt>null</tt>. * @throws java.io.IOException if an error occurs * @return the decoded image read from the source input */ public static BufferedImage read4(InfoHeader infoHeader, com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis, ColorEntry[] colorTable) throws IOException { // 2 pixels per byte or 4 bits per pixel. // Colour for each pixel specified by the color index in the pallette. byte[] ar = new byte[colorTable.length]; byte[] ag = new byte[colorTable.length]; byte[] ab = new byte[colorTable.length]; getColorTable(colorTable, ar, ag, ab); IndexColorModel icm = new IndexColorModel( 4, infoHeader.iNumColors, ar, ag, ab ); BufferedImage img = new BufferedImage( infoHeader.iWidth, infoHeader.iHeight, BufferedImage.TYPE_BYTE_BINARY, icm ); WritableRaster raster = img.getRaster(); //padding int bitsPerLine = infoHeader.iWidth * 4; if (bitsPerLine % 32 != 0) { bitsPerLine = (bitsPerLine / 32 + 1) * 32; } int bytesPerLine = (int) (bitsPerLine / 8); int[] line = new int[bytesPerLine]; for (int y = infoHeader.iHeight - 1; y >= 0; y--) { //scan line for (int i = 0; i < bytesPerLine; i++) { int b = lis.readUnsignedByte(); line[i] = b; } //get pixels for (int x = 0; x < infoHeader.iWidth; x++) { //get byte index for line int b = x / 2; // 2 pixels per byte int i = x % 2; int n = line[b]; int index = getNibble(n, i); raster.setSample(x, y, 0, index); } } return img; } /** * Reads 8-bit uncompressed bitmap raster data, which is interpreted based on the colours * specified in the palette. * @param infoHeader the <tt>InfoHeader</tt> structure, which was read using * {@link #readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream) readInfoHeader()} * @param lis the source input * @param colorTable <tt>ColorEntry</tt> array specifying the palette, which * must not be <tt>null</tt>. * @throws java.io.IOException if an error occurs * @return the decoded image read from the source input */ public static BufferedImage read8(InfoHeader infoHeader, com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis, ColorEntry[] colorTable) throws IOException { //1 byte per pixel // color index 1 (index of color in palette) //lines padded to nearest 32bits //no alpha byte[] ar = new byte[colorTable.length]; byte[] ag = new byte[colorTable.length]; byte[] ab = new byte[colorTable.length]; getColorTable(colorTable, ar, ag, ab); IndexColorModel icm = new IndexColorModel( 8, infoHeader.iNumColors, ar, ag, ab ); BufferedImage img = new BufferedImage( infoHeader.iWidth, infoHeader.iHeight, BufferedImage.TYPE_BYTE_INDEXED, icm ); WritableRaster raster = img.getRaster(); /* //create color pallette int[] c = new int[infoHeader.iNumColors]; for (int i = 0; i < c.length; i++) { int r = colorTable[i].bRed; int g = colorTable[i].bGreen; int b = colorTable[i].bBlue; c[i] = (r << 16) | (g << 8) | (b); } */ //padding int dataPerLine = infoHeader.iWidth; int bytesPerLine = dataPerLine; if (bytesPerLine % 4 != 0) { bytesPerLine = (bytesPerLine / 4 + 1) * 4; } int padBytesPerLine = bytesPerLine - dataPerLine; for (int y = infoHeader.iHeight - 1; y >= 0; y--) { for (int x = 0; x < infoHeader.iWidth; x++) { int b = lis.readUnsignedByte(); //int clr = c[b]; //img.setRGB(x, y, clr); //set sample (colour index) for pixel raster.setSample(x, y , 0, b); } lis.skipBytes(padBytesPerLine); } return img; } /** * Reads 24-bit uncompressed bitmap raster data. * @param lis the source input * @param infoHeader the <tt>InfoHeader</tt> structure, which was read using * {@link #readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream) readInfoHeader()} * @throws java.io.IOException if an error occurs * @return the decoded image read from the source input */ public static BufferedImage read24(InfoHeader infoHeader, com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis) throws IOException { //3 bytes per pixel // blue 1 // green 1 // red 1 // lines padded to nearest 32 bits // no alpha BufferedImage img = new BufferedImage( infoHeader.iWidth, infoHeader.iHeight, BufferedImage.TYPE_INT_RGB ); WritableRaster raster = img.getRaster(); //padding to nearest 32 bits int dataPerLine = infoHeader.iWidth * 3; int bytesPerLine = dataPerLine; if (bytesPerLine % 4 != 0) { bytesPerLine = (bytesPerLine / 4 + 1) * 4; } int padBytesPerLine = bytesPerLine - dataPerLine; for (int y = infoHeader.iHeight - 1; y >= 0; y--) { for (int x = 0; x < infoHeader.iWidth; x++) { int b = lis.readUnsignedByte(); int g = lis.readUnsignedByte(); int r = lis.readUnsignedByte(); //int c = 0x00000000 | (r << 16) | (g << 8) | (b); //System.out.println(x + ","+y+"="+Integer.toHexString(c)); //img.setRGB(x, y, c); raster.setSample(x, y, 0, r); raster.setSample(x, y, 1, g); raster.setSample(x, y, 2, b); } lis.skipBytes(padBytesPerLine); } return img; } /** * Reads 32-bit uncompressed bitmap raster data, with transparency. * @param lis the source input * @param infoHeader the <tt>InfoHeader</tt> structure, which was read using * {@link #readInfoHeader(com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream) readInfoHeader()} * @throws java.io.IOException if an error occurs * @return the decoded image read from the source input */ public static BufferedImage read32(InfoHeader infoHeader, com.inet.gradle.setup.image.image4j.io.LittleEndianInputStream lis) throws IOException { //4 bytes per pixel // blue 1 // green 1 // red 1 // alpha 1 //No padding since each pixel = 32 bits BufferedImage img = new BufferedImage( infoHeader.iWidth, infoHeader.iHeight, BufferedImage.TYPE_INT_ARGB ); WritableRaster rgb = img.getRaster(); WritableRaster alpha = img.getAlphaRaster(); for (int y = infoHeader.iHeight - 1; y >= 0; y--) { for (int x = 0; x < infoHeader.iWidth; x++) { int b = lis.readUnsignedByte(); int g = lis.readUnsignedByte(); int r = lis.readUnsignedByte(); int a = lis.readUnsignedByte(); rgb.setSample(x, y, 0, r); rgb.setSample(x, y, 1, g); rgb.setSample(x, y, 2, b); alpha.setSample(x, y, 0, a); } } return img; } /** * Reads and decodes BMP data from the source file. * @param file the source file * @throws java.io.IOException if an error occurs * @return the decoded image read from the source file */ public static BufferedImage read(java.io.File file) throws IOException { return read(new java.io.FileInputStream(file)); } /** * Reads and decodes BMP data from the source input. * @param in the source input * @throws java.io.IOException if an error occurs * @return the decoded image read from the source file */ public static BufferedImage read(java.io.InputStream in) throws IOException { BMPDecoder d = new BMPDecoder(in); return d.getBufferedImage(); } /** * Reads and decodes BMP data from the source file, together with metadata. * @param file the source file * @throws java.io.IOException if an error occurs * @return the decoded image read from the source file * @since 0.7 */ public static BMPImage readExt(java.io.File file) throws IOException { return readExt(new java.io.FileInputStream(file)); } /** * Reads and decodes BMP data from the source input, together with metadata. * @param in the source input * @throws java.io.IOException if an error occurs * @return the decoded image read from the source file * @since 0.7 */ public static BMPImage readExt(java.io.InputStream in) throws IOException { BMPDecoder d = new BMPDecoder(in); BMPImage ret = new BMPImage(d.getBufferedImage(), d.getInfoHeader()); return ret; } }