/* * Copyright (c) 2005 (Mike) Maurice Kienenberger (mkienenb@gmail.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package org.gamenet.application.mm8leveleditor.converter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.gamenet.application.mm8leveleditor.lod.LodResource; import org.gamenet.application.mm8leveleditor.lod.SpriteTGADataConsumer; import org.gamenet.application.mm8leveleditor.lod.TGADataConsumer; import org.gamenet.application.mm8leveleditor.lod.TGADataProducer; import org.gamenet.util.ByteConversions; import com.mmbreakfast.unlod.lod.NoSuchEntryException; /** * @author mlk */ public class TGAToBMPFormatConverter extends FormatConverter { public TGAToBMPFormatConverter() { super(); } public class TGAToBMPConversionOutputStream extends OutputStream { private OutputStream out = null; private int width = -1; private int height = -1; private ByteArrayOutputStream tgaData = null; private byte bitmapFileHeader[] = {'B', 'M', // id '?', '?', '?', '?', // file size 0, 0, 0, 0, // always zero 0x36, 0x04, 0x00, 0x00}; // offset to bitmap data private byte bitmapInfoHeader[] = { 40, 0, 0, 0, // size of bitmap info header '?', '?', '?', '?', // width of image in pixels '?', '?', '?', '?', // height of image in pixels 1, 0, // number of planes of the target device, must be set to zero. 8, 0, // biBitCount, number of bits per pixel 0, 0, 0, 0, // compression type 0, 0, 0, 0, // size of image data, may be zero with no compression 0, 0, 0, 0, // horizontal pixels per meter on the designated targer device, usually set to zero. 0, 0, 0, 0, // vertical pixels per meter on the designated targer device, usually set to zero. 0, 0, 0, 0, // number of colors used in the bitmap, if set to zero the number of colors is calculated using the biBitCount member. 0, 0, 0, 0 // number of color that are 'important' for the bitmap, if set to zero, all colors are important. }; private byte rgbquadColors[] = null; // 256 * 4 bytes in 8-bit mode // {B, G, R, 0} private byte bitmapBits[] = null; public TGAToBMPConversionOutputStream(OutputStream out, LodResource bitmapLodEntry) throws IOException { super(); this.out = out; tgaData = new ByteArrayOutputStream(); TGADataProducer tgaDataProducer = (TGADataProducer)bitmapLodEntry; width = tgaDataProducer.getByteWidth(); height = tgaDataProducer.getByteHeight(); int palette[]; try { palette = tgaDataProducer.getPalette(); } catch (IOException ioException) { if (ioException.getCause() instanceof NoSuchEntryException) { ioException.printStackTrace(); // Meaningless image, but we can continue past it -- generate unique value for every palette point. palette = new int[768]; for (int i = 0; i < palette.length; ++i) { palette[i] = i; } } else { throw ioException; } } rgbquadColors = new byte[256 * 4]; for (int index = 0; index < 256; ++index) { rgbquadColors[(index*4) + 2] = (byte)palette[(index * 3) + 0]; rgbquadColors[(index*4) + 1] = (byte)palette[(index * 3) + 1]; rgbquadColors[(index*4) + 0] = (byte)palette[(index * 3) + 2]; rgbquadColors[(index*4) + 3] = 0; } bitmapInfoHeader[4] = (byte)(width % 256); bitmapInfoHeader[5] = (byte)((width / 256) % 256); bitmapInfoHeader[6] = (byte)(((width / 256) / 256) % 256); bitmapInfoHeader[7] = (byte)((((width / 256) / 256) / 256) % 256); bitmapInfoHeader[8] = (byte)(height % 256); bitmapInfoHeader[9] = (byte)((height / 256) % 256); bitmapInfoHeader[10] = (byte)(((height / 256) / 256) % 256); bitmapInfoHeader[11] = (byte)((((height / 256) / 256) / 256) % 256); } public void close() throws IOException { int paddedWidth = width; // TODO: replace with real computation while (0 != (paddedWidth % 4)) ++paddedWidth; byte rawData[] = tgaData.toByteArray(); int rawDataIndex = 0; bitmapBits = new byte[paddedWidth * height]; int bitmapBitsIndex = 0; for (int h = 0; h < height; ++h) { for (int w = 0; w < paddedWidth; ++w) { if (w >= width) { // System.out.println("calc=" + ((h * width) + w) + ", inc=" + bitmapBitsIndex++); bitmapBits[(((height - 1) - h) * paddedWidth) + w] = 0; } else { // System.out.println("calc=" + ((h * width) + w) + ", inc=" + bitmapBitsIndex++); bitmapBits[(((height - 1) - h) * paddedWidth) + w] = rawData[rawDataIndex++]; } } } int fileSize = bitmapFileHeader.length + bitmapInfoHeader.length + rgbquadColors.length + bitmapBits.length; bitmapFileHeader[2] = (byte)(fileSize % 256); bitmapFileHeader[3] = (byte)((fileSize / 256) % 256); bitmapFileHeader[4] = (byte)(((fileSize / 256) / 256) % 256); bitmapFileHeader[5] = (byte)((((fileSize / 256) / 256) / 256) % 256); out.write(bitmapFileHeader); out.write(bitmapInfoHeader); out.write(rgbquadColors); out.write(bitmapBits); out.flush(); out.close(); } public void write(int b) throws IOException { tgaData.write(b); } } /** * Set output Stream where the new BMP format will be written * * @param stream where the new converted format will be written */ public void setDestinationOutputStreamForNewFormat(OutputStream stream, LodResource bitmapLodEntry) throws IOException { sourceOutputStream = new TGAToBMPConversionOutputStream(stream, bitmapLodEntry); } public class BMPToTGAConversionInputStream extends InputStream { private InputStream in = null; private int width = -1; private int height = -1; private int[] palette = null; private byte[] tgaData = null; private int attributeMask; private int pixelNumber; private int resolution; private int ySkip; private boolean didReadTGAData = true; private int readingTGAIndex = 0; private int FILE_HEADER__ID_OFFSET = 0; private byte[] FILE_HEADER__ID_VALUE = {'B', 'M'}; private int FILE_HEADER__FILE_SIZE_OFFSET = 2; private int FILE_HEADER__UNUSED1_OFFSET = 6; private int FILE_HEADER__UNUSED1_VALUE = 0; private int FILE_HEADER__BITMAP_BITS_OFFSET = 10; private byte bitmapFileHeader[] = {'B', 'M', // id '?', '?', '?', '?', // file size 0, 0, 0, 0, // always zero 0x36, 0x04, 0x00, 0x00}; // offset to bitmap data private int INFO_HEADER__SIZE_OF_BITMAP_INFO_HEADER_OFFSET = 0; private int INFO_HEADER__SIZE_OF_BITMAP_INFO_HEADER_VALUE = 40; private int INFO_HEADER__IMAGE_WIDTH_OFFSET = 4; private int INFO_HEADER__IMAGE_HEIGHT_OFFSET = 8; private int INFO_HEADER__PLANES_OFFSET = 12; private short INFO_HEADER__PLANES_VALUE = 1; private int INFO_HEADER__BITS_PER_PIXEL_OFFSET = 14; private short INFO_HEADER__BITS_PER_PIXEL_VALUE = 8; private int INFO_HEADER__COMPRESSION_OFFSET = 16; private int INFO_HEADER__COMPRESSION_VALUE = 0; private int INFO_HEADER__SIZE_OF_IMAGE_DATA_OFFSET = 20; private int INFO_HEADER__SIZE_OF_IMAGE_DATA_VALUE = 0; private int INFO_HEADER__HORIZONTAL_PIXELS_PER_METER_OFFSET = 24; private int INFO_HEADER__HORIZONTAL_PIXELS_PER_METER_VALUE = 0; private int INFO_HEADER__VERTICAL_PIXELS_PER_METER_OFFSET = 28; private int INFO_HEADER__VERTICAL_PIXELS_PER_METER_VALUE = 0; private int INFO_HEADER__NUMBER_OF_COLORS_OFFSET = 32; private int INFO_HEADER__NUMBER_OF_COLORS_VALUE = 0; private int INFO_HEADER__NUMBER_OF_IMPORTANT_COLORS_OFFSET = 36; private int INFO_HEADER__NUMBER_OF_IMPORTANT_COLORS_VALUE = 0; private byte bitmapInfoHeader[] = { 40, 0, 0, 0, // size of bitmap info header '?', '?', '?', '?', // width of image in pixels '?', '?', '?', '?', // height of image in pixels 1, 0, // number of planes of the target device, must be set to zero. 8, 0, // biBitCount, number of bits per pixel 0, 0, 0, 0, // compression type 0, 0, 0, 0, // size of image data, may be zero with no compression 0, 0, 0, 0, // horizontal pixels per meter on the designated targer device, usually set to zero. 0, 0, 0, 0, // vertical pixels per meter on the designated targer device, usually set to zero. 0, 0, 0, 0, // number of colors used in the bitmap, if set to zero the number of colors is calculated using the biBitCount member. 0, 0, 0, 0 // number of color that are 'important' for the bitmap, if set to zero, all colors are important. }; private byte rgbquadColors[] = null; // 256 * 4 bytes in 8-bit mode // {B, G, R, 0} private byte bitmapBits[] = null; public BMPToTGAConversionInputStream(InputStream in, TGADataConsumer tgaAcceptingLodEntry) throws IOException { super(); SpriteTGADataConsumer spriteTGADataConsumer = null; if (tgaAcceptingLodEntry instanceof SpriteTGADataConsumer) { spriteTGADataConsumer = (SpriteTGADataConsumer)tgaAcceptingLodEntry; } this.in = in; int totalBitmapFileHeader = 0; while (totalBitmapFileHeader < bitmapFileHeader.length) { int count = in.read(bitmapFileHeader, totalBitmapFileHeader, (bitmapFileHeader.length - totalBitmapFileHeader)); if (-1 == count) throw new RuntimeException("unexpected end of file: " + String.valueOf(bitmapFileHeader.length - totalBitmapFileHeader) + " bytes left in bitmapFileHeader."); totalBitmapFileHeader += count; } for (int index = FILE_HEADER__ID_OFFSET; index < FILE_HEADER__ID_VALUE.length; ++index) if (FILE_HEADER__ID_VALUE[index] != bitmapFileHeader[index]) throw new UnsupportedFileFormatException("File header id value <" + bitmapFileHeader[index] + "> does not match expected value <" + FILE_HEADER__ID_VALUE[index] + ">."); int fileSize = ByteConversions.getIntegerInByteArrayAtPosition(bitmapFileHeader, FILE_HEADER__FILE_SIZE_OFFSET); int unused1 = ByteConversions.getIntegerInByteArrayAtPosition(bitmapFileHeader, FILE_HEADER__UNUSED1_OFFSET); if (unused1 != FILE_HEADER__UNUSED1_VALUE) throw new UnsupportedFileFormatException("File header reserved bytes value <" + unused1 + "> does not match expected value <" + FILE_HEADER__UNUSED1_VALUE + ">."); int bitmapBitsOffset = ByteConversions.getIntegerInByteArrayAtPosition(bitmapFileHeader, FILE_HEADER__BITMAP_BITS_OFFSET); int totalBitmapInfoHeader = 0; while (totalBitmapInfoHeader < 4) { int count = in.read(bitmapInfoHeader, totalBitmapInfoHeader, (4 - totalBitmapInfoHeader)); if (-1 == count) throw new RuntimeException("unexpected end of file: " + String.valueOf(4 - totalBitmapInfoHeader) + " bytes left in bitmapInfoHeader pre-header."); totalBitmapInfoHeader += count; } int bitmapInfoHeaderSize = ByteConversions.getIntegerInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__SIZE_OF_BITMAP_INFO_HEADER_OFFSET); if (bitmapInfoHeaderSize != INFO_HEADER__SIZE_OF_BITMAP_INFO_HEADER_VALUE) throw new UnsupportedFileFormatException("File header bitmap info header size value <" + bitmapInfoHeaderSize + "> does not match expected value <" + INFO_HEADER__SIZE_OF_BITMAP_INFO_HEADER_VALUE + ">."); while (totalBitmapInfoHeader < bitmapInfoHeader.length) { int count = in.read(bitmapInfoHeader, totalBitmapInfoHeader, (bitmapInfoHeader.length - totalBitmapInfoHeader)); if (-1 == count) throw new RuntimeException("unexpected end of file: " + String.valueOf(bitmapInfoHeader.length - totalBitmapInfoHeader) + " bytes left in bitmapInfoHeader."); totalBitmapInfoHeader += count; } width = ByteConversions.getIntegerInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__IMAGE_WIDTH_OFFSET); height = ByteConversions.getIntegerInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__IMAGE_HEIGHT_OFFSET); short planes = ByteConversions.getShortInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__PLANES_OFFSET); if (planes != INFO_HEADER__PLANES_VALUE) throw new UnsupportedFileFormatException("Info header planes value <" + planes + "> does not match expected value <" + INFO_HEADER__PLANES_VALUE + ">."); short bitsPerPixel = ByteConversions.getShortInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__BITS_PER_PIXEL_OFFSET); if (bitsPerPixel != INFO_HEADER__BITS_PER_PIXEL_VALUE) throw new UnsupportedFileFormatException("Info header bitsPerPixel value <" + bitsPerPixel + "> does not match expected value <" + INFO_HEADER__BITS_PER_PIXEL_VALUE + ">."); int compression = ByteConversions.getIntegerInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__COMPRESSION_OFFSET); if (compression != INFO_HEADER__COMPRESSION_VALUE) throw new UnsupportedFileFormatException("Info header compression value <" + compression + "> does not match expected value <" + INFO_HEADER__COMPRESSION_VALUE + ">."); int sizeOfImageData = ByteConversions.getIntegerInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__SIZE_OF_IMAGE_DATA_OFFSET); // if (sizeOfImageData != INFO_HEADER__SIZE_OF_IMAGE_DATA_VALUE) // throw new UnsupportedFileFormatException("Info header size of image data value <" + sizeOfImageData + "> does not match expected value <" + INFO_HEADER__SIZE_OF_IMAGE_DATA_VALUE + ">."); sizeOfImageData = 0; // IMPLEMENT: This should still be compared against expected value if not zero int horizontalPixelsPerMeter = ByteConversions.getIntegerInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__HORIZONTAL_PIXELS_PER_METER_OFFSET); // if (horizontalPixelsPerMeter != INFO_HEADER__HORIZONTAL_PIXELS_PER_METER_VALUE) // throw new UnsupportedFileFormatException("Info header horizontal Pixels Per Meter value <" + horizontalPixelsPerMeter + "> does not match expected value <" + INFO_HEADER__HORIZONTAL_PIXELS_PER_METER_VALUE + ">."); horizontalPixelsPerMeter = 0; int verticalPixelsPerMeter = ByteConversions.getIntegerInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__VERTICAL_PIXELS_PER_METER_OFFSET); // if (verticalPixelsPerMeter != INFO_HEADER__VERTICAL_PIXELS_PER_METER_VALUE) // throw new UnsupportedFileFormatException("Info header vertical Pixels Per Meter value <" + verticalPixelsPerMeter + "> does not match expected value <" + INFO_HEADER__VERTICAL_PIXELS_PER_METER_VALUE + ">."); verticalPixelsPerMeter = 0; int numberOfColors = ByteConversions.getIntegerInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__NUMBER_OF_COLORS_OFFSET); // if (numberOfColors != INFO_HEADER__NUMBER_OF_COLORS_VALUE) // throw new UnsupportedFileFormatException("Info header number of colors value <" + numberOfColors + "> does not match expected value <" + INFO_HEADER__NUMBER_OF_IMPORTANT_COLORS_OFFSET + ">."); numberOfColors = 0; int numberOfImportantColors = ByteConversions.getIntegerInByteArrayAtPosition(bitmapInfoHeader, INFO_HEADER__NUMBER_OF_COLORS_OFFSET); // if (numberOfImportantColors != INFO_HEADER__NUMBER_OF_IMPORTANT_COLORS_VALUE) // throw new UnsupportedFileFormatException("Info header number of important colors value <" + numberOfImportantColors + "> does not match expected value <" + INFO_HEADER__NUMBER_OF_IMPORTANT_COLORS_VALUE + ">."); numberOfImportantColors = 0; switch (bitsPerPixel) { case 8: rgbquadColors = new byte[256 * 4]; // 256 * 4 bytes in 8-bit mode // {B, G, R, 0} break; default: throw new UnsupportedFileFormatException("Unsupported bitsPerPixel value <" + bitsPerPixel + ">."); } int totalRgbquadColors = 0; while (totalRgbquadColors < rgbquadColors.length) { int count = in.read(rgbquadColors, totalRgbquadColors, (rgbquadColors.length - totalRgbquadColors)); if (-1 == count) throw new RuntimeException("unexpected end of file: " + String.valueOf(rgbquadColors.length - totalRgbquadColors) + " bytes left in rgbquadColors."); totalRgbquadColors += count; } if (bitmapBitsOffset != (bitmapFileHeader.length + bitmapInfoHeader.length + rgbquadColors.length)) throw new UnsupportedFileFormatException("File header bitmap bits offset <" + bitmapBitsOffset + "> does not match expected value <" + (bitmapFileHeader.length + bitmapInfoHeader.length + rgbquadColors.length) + ">."); int bitmapBitsSize = fileSize - (bitmapFileHeader.length + bitmapInfoHeader.length + rgbquadColors.length); int paddedWidth = width; // TODO: replace with real computation while (0 != (paddedWidth % 4)) ++paddedWidth; if (bitmapBitsSize != (height * paddedWidth)) throw new UnsupportedFileFormatException("Bitmap bits size <" + bitmapBitsSize + "> does not match expected value <" + (height * paddedWidth) + ">."); bitmapBits = new byte[bitmapBitsSize]; int totalBitmapBits = 0; while (totalBitmapBits < bitmapBits.length) { int count = in.read(bitmapBits, totalBitmapBits, (bitmapBits.length - totalBitmapBits)); if (-1 == count) throw new RuntimeException("unexpected end of file: " + String.valueOf(bitmapBits.length - totalBitmapBits) + " bytes left in bitmapBits."); totalBitmapBits += count; } palette = new int[256 * 3]; for (int index = 0; index < 256; ++index) { palette[(index * 3) + 0] = ByteConversions.convertByteToInt(rgbquadColors[(index*4) + 2]); palette[(index * 3) + 1] = ByteConversions.convertByteToInt(rgbquadColors[(index*4) + 1]); palette[(index * 3) + 2] = ByteConversions.convertByteToInt(rgbquadColors[(index*4) + 0]); } int tgaDataIndex = 0; int blankLineCount = 0; tgaData = new byte[width * height]; for (int h = 0; h < height; ++h) { boolean isBlankLine = true; for (int w = 0; w < paddedWidth; ++w) { if (w < width) { // System.out.println("tgaData[" + String.valueOf(tgaDataIndex) + "] = bitmapBits[(((" + String.valueOf(height) + " - 1) - " + String.valueOf(h) + ") * " + String.valueOf(paddedWidth) + ") + " + String.valueOf(w) + "]"); byte tgaDataValue = bitmapBits[(((height - 1) - h) * paddedWidth) + w]; if (tgaDataValue != 0) { isBlankLine = false; } tgaData[tgaDataIndex++] = tgaDataValue; } } if (isBlankLine) { blankLineCount++; } else { blankLineCount = 0; } } tgaAcceptingLodEntry.setImageWidth(width); tgaAcceptingLodEntry.setImageHeight(height); tgaAcceptingLodEntry.setImagePalette(palette); // I don't know if these values are actually correct or acceptable attributeMask = 0; // told that this value is set to zero on load anyway pixelNumber = width * height; resolution = 0; // told that this isn't used ySkip = blankLineCount; if (null != spriteTGADataConsumer) { spriteTGADataConsumer.setAttributeMask(attributeMask); spriteTGADataConsumer.setPixelNumber(pixelNumber); spriteTGADataConsumer.setResolution(resolution); spriteTGADataConsumer.setYSkip(ySkip); } didReadTGAData = false; } public void close() throws IOException { super.close(); in.close(); } public int read() throws IOException { if (false == didReadTGAData) { int value = ByteConversions.convertByteToInt(tgaData[readingTGAIndex++]); if (readingTGAIndex >= tgaData.length) { didReadTGAData = true; } return value; } else { this.close(); return -1; } } } /** * Set input Stream where the new BMP format will be written * * @param stream where the new converted format will be written */ public void setSourceInputStreamForNewFormat(InputStream stream, LodResource tgaAcceptingLodEntry) throws IOException { sourceInputStream = new BMPToTGAConversionInputStream(stream, (TGADataConsumer)tgaAcceptingLodEntry); } }