diff options
Diffstat (limited to 'src/jake2/imageio')
-rw-r--r-- | src/jake2/imageio/ImageFrame.java | 63 | ||||
-rw-r--r-- | src/jake2/imageio/PCX.java | 229 | ||||
-rw-r--r-- | src/jake2/imageio/PCXImageReader.java | 322 | ||||
-rw-r--r-- | src/jake2/imageio/PCXImageReaderSpi.java | 99 | ||||
-rw-r--r-- | src/jake2/imageio/Q2ColorMap.java | 91 | ||||
-rw-r--r-- | src/jake2/imageio/WAL.java | 169 | ||||
-rw-r--r-- | src/jake2/imageio/WALImageReader.java | 273 | ||||
-rw-r--r-- | src/jake2/imageio/WALImageReaderSpi.java | 127 |
8 files changed, 1373 insertions, 0 deletions
diff --git a/src/jake2/imageio/ImageFrame.java b/src/jake2/imageio/ImageFrame.java new file mode 100644 index 0000000..52cf9e4 --- /dev/null +++ b/src/jake2/imageio/ImageFrame.java @@ -0,0 +1,63 @@ +/* + * Created on Apr 26, 2003 + * + */ +package jake2.imageio; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import javax.swing.JFrame; + +/** + * @author cwei + * + */ +public class ImageFrame extends JFrame { + + BufferedImage image; + Component pane; + + public ImageFrame(BufferedImage image) { + super(); + this.image = image; + + pane = getContentPane(); + setIconImage(image); + setSize(640, 480); + + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + System.exit(0); + } + }); + } + + public void paint(Graphics g) { + super.paint(g); + Graphics2D g2 = (Graphics2D) pane.getGraphics(); + if (this.image != null) { + g2.drawImage( + image, + Math.max(0, (getWidth() - image.getWidth()) / 2), + Math.max(0, (getHeight() - image.getHeight()) / 2), + Color.LIGHT_GRAY, + pane); + } else { + g2.drawString( + "EMPTY IMAGE", + this.getWidth() / 4, + this.getHeight() / 2); + } + } + + public void showImage(BufferedImage image) { + this.image = image; + this.repaint(); + } + +} diff --git a/src/jake2/imageio/PCX.java b/src/jake2/imageio/PCX.java new file mode 100644 index 0000000..eb71465 --- /dev/null +++ b/src/jake2/imageio/PCX.java @@ -0,0 +1,229 @@ +/* + * Created on Nov 18, 2003 + * + */ +package jake2.imageio; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * @author cwei + * + */ +public class PCX { + + public static final int HEADER_SIZE = 128; + + /** + * <code> + typedef struct + { + char manufacturer; + char version; + char encoding; + char bits_per_pixel; + unsigned short xmin,ymin,xmax,ymax; + unsigned short horzRes,vertRes; + unsigned char palette[48]; + char reserved; + char color_planes; + unsigned short bytes_per_line; + unsigned short palette_type; + char filler[58]; + unsigned char data; // unbounded + } pcx_t; + + </code> + */ + public static class Header { + + // size of byte arrays + static final int PALETTE_SIZE = 48; + static final int FILLER_SIZE = 58; + + byte manufacturer; + byte version; + byte encoding; + byte bitsPerPixel; + int xmin, ymin, xmax, ymax; // unsigned short + int horzRes, vertRes; // unsigned short + byte[] palette; //unsigned byte + byte reserved; + byte colorPlanes; + int bytesPerLine; // unsigned short + int paletteType; // unsigned short + byte[] filler; + + + + public Header(byte[] headerBytes) { + if (headerBytes == null || headerBytes.length != 128) { + throw new IllegalArgumentException("invalid quake2 pcx header"); + } + + ByteBuffer b = ByteBuffer.wrap(headerBytes); + // is stored as little endian + b.order(ByteOrder.LITTLE_ENDIAN); + + // fill header + manufacturer = b.get(); + version = b.get(); + encoding = b.get(); + bitsPerPixel = b.get(); + xmin = b.getShort() & 0xffff; + ymin = b.getShort() & 0xffff; + xmax = b.getShort() & 0xffff; + ymax = b.getShort() & 0xffff; + horzRes = b.getShort() & 0xffff; + vertRes = b.getShort() & 0xffff; + b.get(palette = new byte[PALETTE_SIZE]); + reserved = b.get(); + colorPlanes = b.get(); + bytesPerLine = b.getShort() & 0xffff; + paletteType = b.getShort() & 0xffff; + b.get(filler = new byte[FILLER_SIZE]); + + // check some attributes + checkHeader(); + } + + private void checkHeader() { + + if (this.getManufacturer() != 0x0a + || this.getVersion() != 5 + || this.getEncoding() != 1 + || this.getBitsPerPixel() != 8 + || this.getXmax() >= 640 + || this.getYmax() >= 480) { + throw new IllegalArgumentException("invalid quake2 pcx header"); + } + } + + /** + * @return + */ + public byte getBitsPerPixel() { + return bitsPerPixel; + } + + /** + * @return + */ + public int getBytesPerLine() { + return bytesPerLine; + } + + /** + * @return + */ + public byte getColorPlanes() { + return colorPlanes; + } + + /** + * @return + */ + public byte getEncoding() { + return encoding; + } + + /** + * @return + */ + public byte[] getFiller() { + return filler; + } + + /** + * @return + */ + public int getHeight() { + return ymax - ymin + 1; + } + + /** + * @return + */ + public byte getManufacturer() { + return manufacturer; + } + + /** + * @return + */ + public byte[] getPalette() { + return palette; + } + + /** + * @return + */ + public int getPaletteType() { + return paletteType; + } + + /** + * @return + */ + public byte getReserved() { + return reserved; + } + + /** + * @return + */ + public byte getVersion() { + return version; + } + + /** + * @return + */ + public int getWidth() { + return xmax - xmin + 1; + } + + /** + * @return + */ + public int getXmax() { + return xmax; + } + + /** + * @return + */ + public int getXmin() { + return xmin; + } + + /** + * @return + */ + public int getYmax() { + return ymax; + } + + /** + * @return + */ + public int getYmin() { + return ymin; + } + /** + * @return + */ + public int getHorzRes() { + return horzRes; + } + + /** + * @return + */ + public int getVertRes() { + return vertRes; + } + + } +}
\ No newline at end of file diff --git a/src/jake2/imageio/PCXImageReader.java b/src/jake2/imageio/PCXImageReader.java new file mode 100644 index 0000000..c81e623 --- /dev/null +++ b/src/jake2/imageio/PCXImageReader.java @@ -0,0 +1,322 @@ +/* + * Created on Nov 17, 2003 + * + */ +package jake2.imageio; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.IIOException; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + +/** + * @author cwei + * + */ +public class PCXImageReader extends ImageReader { + + private static Logger logger = + Logger.getLogger(PCXImageReader.class.getName()); + + ImageInputStream stream = null; + PCX.Header header = null; + + public PCXImageReader(ImageReaderSpi originatingProvider) { + super(originatingProvider); + } + + public void setInput(Object input, boolean seekForwardOnly) { + super.setInput(input, seekForwardOnly); + if (input == null) { + this.stream = null; + return; + } + if (input instanceof ImageInputStream) { + this.stream = (ImageInputStream) input; + } else { + throw new IllegalArgumentException("bad input"); + } + } + + public int getHeight(int imageIndex) throws IOException { + checkIndex(imageIndex); + readHeader(); + return header.getHeight(); + } + + public int getWidth(int imageIndex) throws IOException { + checkIndex(imageIndex); + readHeader(); + return header.getWidth(); + } + + public int getNumImages(boolean allowSearch) throws IOException { + // only 1 image + return 1; + } + + public Iterator getImageTypes(int imageIndex) throws IOException { + checkIndex(imageIndex); + readHeader(); + + ImageTypeSpecifier imageType = null; + java.util.List l = new ArrayList(1); + + imageType = + ImageTypeSpecifier.createIndexed( + Q2ColorMap.RED, + Q2ColorMap.GREEN, + Q2ColorMap.BLUE, + Q2ColorMap.ALPHA, + 8, + DataBuffer.TYPE_BYTE); + + l.add(imageType); + return l.iterator(); + } + + public IIOMetadata getStreamMetadata() throws IOException { + return null; + } + + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + return null; + } + + public BufferedImage read(int imageIndex, ImageReadParam param) + throws IOException { + + checkIndex(imageIndex); + readHeader(); + + int width = header.getWidth(); + int height = header.getHeight(); + + // Compute initial source region, clip against destination later + Rectangle sourceRegion = getSourceRegion(param, width, height); + + // Set everything to default values + int sourceXSubsampling = 1; + int sourceYSubsampling = 1; + int[] sourceBands = null; + int[] destinationBands = null; + Point destinationOffset = new Point(0, 0); + + // Get values from the ImageReadParam, if any + if (param != null) { + sourceXSubsampling = param.getSourceXSubsampling(); + sourceYSubsampling = param.getSourceYSubsampling(); + sourceBands = param.getSourceBands(); + destinationBands = param.getDestinationBands(); + destinationOffset = param.getDestinationOffset(); + } + + // Get the specified detination image or create a new one + BufferedImage dst = + getDestination(param, getImageTypes(0), width, height); + + // Enure band settings from param are compatible with images + int inputBands = 1; + checkReadParamBandSettings( + param, + inputBands, + dst.getSampleModel().getNumBands()); + + int[] bandOffsets = new int[inputBands]; + for (int i = 0; i < inputBands; i++) { + bandOffsets[i] = i; + } + int bytesPerRow = width * inputBands; + DataBufferByte rowDB = new DataBufferByte(bytesPerRow); + WritableRaster rowRas = + Raster.createInterleavedRaster( + rowDB, + width, + 1, + bytesPerRow, + inputBands, + bandOffsets, + new Point(0, 0)); + byte[] rowBuf = rowDB.getData(); + + // Create an int[] that can a single pixel + int[] pixel = rowRas.getPixel(0, 0, (int[]) null); + + WritableRaster imRas = dst.getWritableTile(0, 0); + int dstMinX = imRas.getMinX(); + int dstMaxX = dstMinX + imRas.getWidth() - 1; + int dstMinY = imRas.getMinY(); + int dstMaxY = dstMinY + imRas.getHeight() - 1; + + // Create a child raster exposing only the desired source bands + if (sourceBands != null) { + rowRas = + rowRas.createWritableChild(0, 0, width, 1, 0, 0, sourceBands); + } + + // Create a child raster exposing only the desired dest bands + if (destinationBands != null) { + imRas = + imRas.createWritableChild( + 0, + 0, + imRas.getWidth(), + imRas.getHeight(), + 0, + 0, + destinationBands); + + } + + int dataByte = 0; + int runLength = 0; + + for (int srcY = 0; srcY < height; srcY++) { + // Read the row + try { + /* + * run length decoding for PCX images + */ + int index = 0; + + while (index < rowBuf.length) { + while (runLength-- > 0 && index < rowBuf.length) { + rowBuf[index++] = (byte) (dataByte & 0xff); + } + dataByte = stream.readUnsignedByte(); + if ((dataByte & 0xc0) == 0xc0) { + runLength = dataByte & 0x3f; + dataByte = stream.readUnsignedByte(); + } else { + runLength = 1; + } + } + } catch (IOException e) { + throw new IIOException("Error reading line " + srcY, e); + } + + // Reject rows that lie outside the source region, + // or which aren't part of the subsampling + if ((srcY < sourceRegion.y) + || (srcY >= sourceRegion.y + sourceRegion.height) + || (((srcY - sourceRegion.y) % sourceYSubsampling) != 0)) { + continue; + } + + // Determine where the row will go in the destination + int dstY = + destinationOffset.y + + (srcY - sourceRegion.y) / sourceYSubsampling; + if (dstY < dstMinY) { + continue; // The row is above imRas + } + if (dstY > dstMaxY) { + break; // We're done with the image + } + + // Copy each (subsampled) source pixel into imRas + for (int srcX = sourceRegion.x; + srcX < sourceRegion.x + sourceRegion.width; + srcX++) { + if (((srcX - sourceRegion.x) % sourceXSubsampling) != 0) { + continue; + } + int dstX = + destinationOffset.x + + (srcX - sourceRegion.x) / sourceXSubsampling; + if (dstX < dstMinX) { + continue; // The pixel is to the left of imRas + } + if (dstX > dstMaxX) { + break; // We're done with the row + } + + // Copy the pixel, sub-banding is done automatically + rowRas.getPixel(srcX, 0, pixel); + imRas.setPixel(dstX, dstY, pixel); + } + } + if ((stream.readUnsignedByte()) == 0x0c) { + logger.log( + Level.FINE, + "PCX has a color palette with " + + (stream.length() - stream.getStreamPosition()) + + " Bytes, but use the default palette (quake2)"); + } + return dst; + } + + private void checkIndex(int imageIndex) { + if (imageIndex != 0) { + throw new IndexOutOfBoundsException("bad image index"); + } + } + +// buggy version +/* private void decodeRow(byte[] buffer) throws IOException { + int dataByte = 0; + int runLength = 0; + int index = 0; + + while (index < buffer.length) { + dataByte = stream.readUnsignedByte(); + if ((dataByte & 0xc0) == 0xc0) { + runLength = dataByte & 0x3f; + dataByte = stream.readUnsignedByte(); + } else { + runLength = 1; + } + + while (runLength-- > 0 && index < buffer.length) { + buffer[index++] = (byte) (dataByte & 0xff); + } + assert(runLength == -1) : "runLength decoding bug: " + runLength; + } + } +*/ + private void readHeader() throws IIOException { + + if (header != null) + return; + + logger.log(Level.FINE, "PCX read header"); + + if (stream == null) { + if (this.input == null) { + throw new IllegalStateException("No input stream"); + } + stream = (ImageInputStream) input; + } + + byte[] buffer = new byte[PCX.HEADER_SIZE]; + + try { + stream.readFully(buffer); + this.header = new PCX.Header(buffer); + logger.log( + Level.FINE, + "PCX horzRes: " + + header.getWidth() + + " height: " + + header.getHeight()); + } catch (IOException e) { + throw new IIOException("Error reading quake2 PCX header", e); + } + } +} diff --git a/src/jake2/imageio/PCXImageReaderSpi.java b/src/jake2/imageio/PCXImageReaderSpi.java new file mode 100644 index 0000000..f4089e3 --- /dev/null +++ b/src/jake2/imageio/PCXImageReaderSpi.java @@ -0,0 +1,99 @@ +/* + * Created on Nov 17, 2003 + * + */ +package jake2.imageio; + +import java.io.IOException; +import java.util.Locale; + +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + +/** + * @author cwei + * + * put the own ImageReaderSpi class name in the file (relative to classpath): + * <code> + * META-INF/services/javax.imageio.spi.ImageReaderSpi + * </code> + */ +public class PCXImageReaderSpi extends ImageReaderSpi { + + static final String vendorName = "[email protected]"; + static final String version = "1.0_beta"; + static final String[] names = { "quake2 pcx" }; + static final String[] suffixes = { "pcx" }; + static final String[] MIMETypes = { "image/x-quake2-pcx" }; + static final String readerClassName = "jake2.imageio.PCXImageReader"; + static final String[] writerSpiNames = null; // { "jake2.imageio.PCXImageWriterSpi" }; + + // Metadata formats, more information below + static final boolean supportsStandardStreamMetadataFormat = false; + static final String nativeStreamMetadataFormatName = null; + + static final String nativeStreamMetadataFormatClassName = null; + static final String[] extraStreamMetadataFormatNames = null; + static final String[] extraStreamMetadataFormatClassNames = null; + static final boolean supportsStandardImageMetadataFormat = false; + static final String nativeImageMetadataFormatName = + "jake2.imageio.PCXMetaData_1.0"; + static final String nativeImageMetadataFormatClassName = null; // "jake2.imageio.PCXMetadata"; + static final String[] extraImageMetadataFormatNames = null; + static final String[] extraImageMetadataFormatClassNames = null; + + public PCXImageReaderSpi() { + + super( + vendorName, + version, + names, + suffixes, + MIMETypes, + readerClassName, + ImageReaderSpi.STANDARD_INPUT_TYPE, + // Accept ImageInputStreams + writerSpiNames, + supportsStandardStreamMetadataFormat, + nativeStreamMetadataFormatName, + nativeStreamMetadataFormatClassName, + extraStreamMetadataFormatNames, + extraStreamMetadataFormatClassNames, + supportsStandardImageMetadataFormat, + nativeImageMetadataFormatName, + nativeImageMetadataFormatClassName, + extraImageMetadataFormatNames, + extraImageMetadataFormatClassNames); + } + + public boolean canDecodeInput(Object source) throws IOException { + if (!(source instanceof ImageInputStream)) { + return false; + } + ImageInputStream stream = (ImageInputStream)source; + byte[] buffer = new byte[PCX.HEADER_SIZE]; + try { + stream.mark(); + stream.readFully(buffer); + stream.reset(); + // buffer will be converted to members and header checked + PCX.Header pcx = new PCX.Header(buffer); + } catch (IllegalArgumentException e) { + return false; + } + return true; + } + + public ImageReader createReaderInstance(Object extension) + throws IOException { + return new PCXImageReader(this); + } + + /** + * @see javax.imageio.spi.IIOServiceProvider#getDescription(java.util.Locale) + */ + public String getDescription(Locale locale) { + return "id-software's Quake2 pcx format"; + } +} diff --git a/src/jake2/imageio/Q2ColorMap.java b/src/jake2/imageio/Q2ColorMap.java new file mode 100644 index 0000000..6476e61 --- /dev/null +++ b/src/jake2/imageio/Q2ColorMap.java @@ -0,0 +1,91 @@ +/* + * Created on Nov 18, 2003 + * + */ +package jake2.imageio; + +/** + * @author cwei + * + */ +public interface Q2ColorMap { + // Red channel + static final byte[] RED = { + 0, 15, 31, 47, 63, 75, 91, 107, 123, -117, -101, -85, -69, -53, -37, -21, + 99, 91, 83, 79, 71, 63, 59, 51, 47, 43, 39, 35, 27, 23, 19, 15, + 95, 91, 91, 87, 83, 79, 71, 63, 59, 51, 47, 39, 35, 27, 23, 19, + -113, 123, 115, 103, -49, -89, -117, 111, -21, -53, -81, -109, 119, 91, 63, 35, + -89, -97, -105, -117, 127, 115, 103, 87, 75, 67, 59, 51, 43, 35, 27, 19, + 123, 115, 107, 103, 95, 87, 83, 75, 67, 63, 55, 47, 39, 31, 23, 15, + 111, 95, 83, 67, 55, 39, 27, 15, -77, -65, -53, -41, -53, -77, -97, -121, + 115, 91, 71, 47, 23, 19, 15, 11, 7, 7, 7, 0, 0, 0, 0, 0, + -117, -125, 123, 115, 107, 99, 91, 87, 75, 63, 51, 43, 31, 19, 11, 0, + -105, -113, -121, 127, 119, 115, 107, 99, 91, 79, 67, 55, 47, 35, 23, 15, + -97, -109, -117, 127, 119, 107, 99, 87, 79, 67, 55, 43, 31, 23, 11, 0, + 119, 111, 103, 99, 91, 83, 75, 71, 63, 55, 47, 39, 35, 27, 19, 11, + -101, -113, -121, 123, 115, 103, 95, 87, 75, 63, 55, 47, 35, 27, 19, 11, + 0, 35, 63, 83, 95, 95, 95, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -17, -29, -45, -57, -73, -85, -101, -113, 127, 115, 95, 71, 47, 27, + -17, 55, -1, 0, 43, 27, 19, -21, -61, -97, 123, -21, -57, -89, -121, -97 + }; + + // Green channel + static final byte[] GREEN = { + 0, 15, 31, 47, 63, 75, 91, 107, 123, -117, -101, -85, -69, -53, -37, -21, + 75, 67, 63, 59, 55, 47, 43, 39, 35, 31, 27, 23, 19, 15, 15, 11, + 95, 91, 83, 79, 75, 71, 63, 59, 55, 47, 43, 39, 35, 27, 23, 19, + 119, 99, 91, 79, -105, 123, 103, 83, -97, -117, 119, 99, 79, 59, 39, 23, + 59, 47, 43, 39, 31, 23, 23, 19, 15, 15, 15, 11, 11, 11, 7, 7, + 95, 87, 83, 79, 71, 67, 63, 55, 51, 47, 39, 35, 27, 23, 15, 11, + 59, 55, 47, 43, 35, 27, 19, 11, 91, 123, -101, -69, -41, -57, -73, -89, + -105, -121, 119, 103, 83, 75, 67, 63, 55, 47, 39, 31, 23, 15, 7, 0, + 87, 79, 71, 67, 59, 51, 47, 43, 35, 31, 27, 19, 15, 11, 7, 0, + -97, -105, -117, -125, 123, 115, 107, 99, 91, 79, 67, 55, 47, 35, 23, 15, + 75, 67, 59, 55, 47, 43, 35, 31, 27, 23, 19, 15, 11, 7, 0, 0, + 123, 115, 107, 99, 91, 87, 79, 71, 63, 55, 47, 39, 31, 23, 15, 7, + -85, -97, -105, -117, -125, 119, 111, 103, 91, 79, 67, 59, 47, 35, 23, 15, + -1, -25, -45, -69, -89, -113, 123, -1, -1, -1, -1, -1, -1, -21, -41, -65, + -85, -109, 127, 107, 87, 71, 59, 43, 31, 23, 15, 7, 0, 0, 0, 0, + 0, 55, 0, 0, 43, 27, 19, -105, 115, 87, 63, -45, -85, -117, 107, 91 + }; + + // Blue channel + static final byte[] BLUE = { + 0, 15, 31, 47, 63, 75, 91, 107, 123, -117, -101, -85, -69, -53, -37, -21, + 35, 31, 31, 27, 27, 23, 23, 19, 19, 19, 15, 15, 11, 11, 7, 7, + 111, 103, 95, 91, 83, 75, 67, 59, 55, 47, 43, 39, 35, 27, 23, 19, + 83, 67, 59, 47, 75, 59, 47, 39, 39, 35, 31, 27, 23, 15, 11, 7, + 43, 35, 27, 19, 15, 11, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 75, 67, 63, 59, 55, 51, 47, 43, 39, 35, 27, 23, 19, 15, 11, 7, + 23, 23, 23, 23, 19, 15, 11, 7, 79, 111, -109, -73, -33, -45, -61, -73, + -89, -101, -117, 127, 111, 103, 91, 83, 75, 63, 51, 43, 31, 19, 11, 0, + 87, 79, 71, 67, 59, 51, 47, 43, 35, 31, 27, 19, 15, 11, 7, 0, + 123, 115, 107, 99, 95, 87, 79, 71, 67, 59, 51, 43, 35, 27, 19, 11, + 63, 55, 47, 39, 35, 27, 23, 19, 15, 11, 11, 7, 7, 0, 0, 0, + -49, -61, -73, -89, -101, -113, 127, 115, 103, 87, 75, 63, 47, 35, 23, 7, + 123, 111, 99, 87, 75, 67, 59, 51, 39, 27, 19, 11, 7, 0, 0, 0, + 0, 15, 27, 39, 47, 51, 51, -1, -45, -89, 127, 83, 39, 31, 23, 15, + 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -1, 0, -1, 35, 23, 15, 127, 83, 51, 27, -57, -101, 119, 87, 83 + }; + + // Alpha channel + static final byte[] ALPHA = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0 + }; +} diff --git a/src/jake2/imageio/WAL.java b/src/jake2/imageio/WAL.java new file mode 100644 index 0000000..1c499ef --- /dev/null +++ b/src/jake2/imageio/WAL.java @@ -0,0 +1,169 @@ +/* + * Created on Nov 18, 2003 + * + */ +package jake2.imageio; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * @author cwei + * + */ +public class WAL { + + public static final int HEADER_SIZE = 100; + + /* struct wal_header + { + char name[32]; // name of the texture + + uint32 width; // width (in pixels) of the largest mipmap level + uint32 height; // height (in pixels) of the largest mipmap level + + int32 offset[4]; // byte offset of the start of each of the 4 mipmap levels + + char next_name[32]; // name of the next texture in the animation + + uint32 flags; // ? + uint32 contents; // ? + uint32 value; // ? + }; + */ + public static class Header { + + // size of byte arrays + static final int NAME_SIZE = 32; + static final int OFFSET_SIZE = 4; + + String name; + int width; + int height; + int[] offset; // file offsets for the 4 mipmap images + String nextName; + int flags; // unused + int contents; // unused + int value; // unused + + public Header(byte[] headerBytes) { + if (headerBytes == null || headerBytes.length != HEADER_SIZE) { + throw new IllegalArgumentException("invalid quake2 wal header"); + } + + ByteBuffer b = ByteBuffer.wrap(headerBytes); + // is stored as little endian + b.order(ByteOrder.LITTLE_ENDIAN); + + byte[] tmp = new byte[NAME_SIZE]; + // fill header + + // name + b.get(tmp); + try { + name = new String(tmp, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + name = new String(tmp); + } + // width + width = b.getInt(); + assert(width >= 0) : "unsigned int bug"; // true means ok. + // height + height = b.getInt(); + assert(height >= 0) : "unsigned int bug"; // true means ok. + // 4 offsets + offset = + new int[] { b.getInt(), b.getInt(), b.getInt(), b.getInt()}; + // nextName + b.get(tmp); + try { + nextName = new String(tmp, "ISO-8859-1"); + } catch (UnsupportedEncodingException e1) { + name = new String(tmp); + } + // unused entries + flags = b.getInt(); + contents = b.getInt(); + value = b.getInt(); + + // check some attributes + checkHeader(); + } + + private void checkHeader() { + // start of mipmaps + int mipmap0 = HEADER_SIZE; + int mipmap1 = mipmap0 + getWidth() * getHeight(); + int mipmap2 = mipmap1 + getWidth() / 2 * getHeight() / 2; + int mipmap3 = mipmap2 + getWidth() / 4 * getHeight() / 4; + + if (offset[0] != mipmap0 + || offset[1] != mipmap1 + || offset[2] != mipmap2 + || offset[3] != mipmap3) { + throw new IllegalArgumentException("invalid quake2 wal header"); + } + } + + /** + * @return + */ + public int getContents() { + return contents; + } + + /** + * @return + */ + public int getFlags() { + return flags; + } + + /** + * @return + */ + public int getHeight() { + return height; + } + + /** + * @return + */ + public String getName() { + return name; + } + + /** + * @return + */ + public String getNextName() { + return nextName; + } + + /** + * @return + */ + public int getOffset(int index) { + if (index < 0 || index > 3) { + throw new ArrayIndexOutOfBoundsException("mipmap offset range is 0 to 3"); + } + return offset[index]; + } + + /** + * @return + */ + public int getValue() { + return value; + } + + /** + * @return + */ + public int getWidth() { + return width; + } + + } +}
\ No newline at end of file diff --git a/src/jake2/imageio/WALImageReader.java b/src/jake2/imageio/WALImageReader.java new file mode 100644 index 0000000..949b4f1 --- /dev/null +++ b/src/jake2/imageio/WALImageReader.java @@ -0,0 +1,273 @@ +/* + * Created on Nov 18, 2003 + * + */ +package jake2.imageio; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.IIOException; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + +/** + * @author cwei + * + */ +public class WALImageReader extends ImageReader { + + private static Logger logger = + Logger.getLogger(WALImageReader.class.getName()); + + ImageInputStream stream = null; + WAL.Header header = null; + + public WALImageReader(ImageReaderSpi originatingProvider) { + super(originatingProvider); + } + + public void setInput(Object input, boolean seekForwardOnly) { + super.setInput(input, seekForwardOnly); + if (input == null) { + this.stream = null; + return; + } + if (input instanceof ImageInputStream) { + this.stream = (ImageInputStream) input; + } else { + throw new IllegalArgumentException("bad input"); + } + } + + public int getHeight(int imageIndex) throws IOException { + checkIndex(imageIndex); + readHeader(); + return header.getHeight(); + } + + public int getWidth(int imageIndex) throws IOException { + checkIndex(imageIndex); + readHeader(); + return header.getWidth(); + } + + public int getNumImages(boolean allowSearch) throws IOException { + // only 1 image + return 1; + } + + public Iterator getImageTypes(int imageIndex) throws IOException { + checkIndex(imageIndex); + readHeader(); + + ImageTypeSpecifier imageType = null; + java.util.List l = new ArrayList(1); + + imageType = + ImageTypeSpecifier.createIndexed( + Q2ColorMap.RED, + Q2ColorMap.GREEN, + Q2ColorMap.BLUE, + Q2ColorMap.ALPHA, + 8, + DataBuffer.TYPE_BYTE); + + l.add(imageType); + return l.iterator(); + } + + public IIOMetadata getStreamMetadata() throws IOException { + return null; + } + + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + return null; + } + + public BufferedImage read(int imageIndex, ImageReadParam param) + throws IOException { + + checkIndex(imageIndex); + readHeader(); + + int width = header.getWidth(); + int height = header.getHeight(); + + // Compute initial source region, clip against destination later + Rectangle sourceRegion = getSourceRegion(param, width, height); + + // Set everything to default values + int sourceXSubsampling = 1; + int sourceYSubsampling = 1; + int[] sourceBands = null; + int[] destinationBands = null; + Point destinationOffset = new Point(0, 0); + + // Get values from the ImageReadParam, if any + if (param != null) { + sourceXSubsampling = param.getSourceXSubsampling(); + sourceYSubsampling = param.getSourceYSubsampling(); + sourceBands = param.getSourceBands(); + destinationBands = param.getDestinationBands(); + destinationOffset = param.getDestinationOffset(); + } + + // Get the specified detination image or create a new one + BufferedImage dst = + getDestination(param, getImageTypes(0), width, height); + + // Enure band settings from param are compatible with images + int inputBands = 1; + checkReadParamBandSettings( + param, + inputBands, + dst.getSampleModel().getNumBands()); + + int[] bandOffsets = new int[inputBands]; + for (int i = 0; i < inputBands; i++) { + bandOffsets[i] = i; + } + int bytesPerRow = width * inputBands; + DataBufferByte rowDB = new DataBufferByte(bytesPerRow); + WritableRaster rowRas = + Raster.createInterleavedRaster( + rowDB, + width, + 1, + bytesPerRow, + inputBands, + bandOffsets, + new Point(0, 0)); + byte[] rowBuf = rowDB.getData(); + + // Create an int[] that can a single pixel + int[] pixel = rowRas.getPixel(0, 0, (int[]) null); + + WritableRaster imRas = dst.getWritableTile(0, 0); + int dstMinX = imRas.getMinX(); + int dstMaxX = dstMinX + imRas.getWidth() - 1; + int dstMinY = imRas.getMinY(); + int dstMaxY = dstMinY + imRas.getHeight() - 1; + + // Create a child raster exposing only the desired source bands + if (sourceBands != null) { + rowRas = + rowRas.createWritableChild(0, 0, width, 1, 0, 0, sourceBands); + } + + // Create a child raster exposing only the desired dest bands + if (destinationBands != null) { + imRas = + imRas.createWritableChild( + 0, + 0, + imRas.getWidth(), + imRas.getHeight(), + 0, + 0, + destinationBands); + + } + + for (int srcY = 0; srcY < height; srcY++) { + // Read the row + try { + stream.readFully(rowBuf); + } catch (IOException e) { + throw new IIOException("Error reading line " + srcY, e); + } + + // Reject rows that lie outside the source region, + // or which aren't part of the subsampling + if ((srcY < sourceRegion.y) + || (srcY >= sourceRegion.y + sourceRegion.height) + || (((srcY - sourceRegion.y) % sourceYSubsampling) != 0)) { + continue; + } + + // Determine where the row will go in the destination + int dstY = + destinationOffset.y + + (srcY - sourceRegion.y) / sourceYSubsampling; + if (dstY < dstMinY) { + continue; // The row is above imRas + } + if (dstY > dstMaxY) { + break; // We're done with the image + } + + // Copy each (subsampled) source pixel into imRas + for (int srcX = sourceRegion.x; + srcX < sourceRegion.x + sourceRegion.width; + srcX++) { + if (((srcX - sourceRegion.x) % sourceXSubsampling) != 0) { + continue; + } + int dstX = + destinationOffset.x + + (srcX - sourceRegion.x) / sourceXSubsampling; + if (dstX < dstMinX) { + continue; // The pixel is to the left of imRas + } + if (dstX > dstMaxX) { + break; // We're done with the row + } + + // Copy the pixel, sub-banding is done automatically + rowRas.getPixel(srcX, 0, pixel); + imRas.setPixel(dstX, dstY, pixel); + } + } + return dst; + } + + private void checkIndex(int imageIndex) { + if (imageIndex != 0) { + throw new IndexOutOfBoundsException("bad image index"); + } + } + + private void readHeader() throws IIOException { + + if (header != null) return; + + logger.log(Level.FINE, "WAL read header"); + + if (stream == null) { + if (this.input == null) { + throw new IllegalStateException("No input stream"); + } + stream = (ImageInputStream) input; + } + + byte[] buffer = new byte[WAL.HEADER_SIZE]; + + try { + stream.readFully(buffer); + this.header = new WAL.Header(buffer); + logger.log( + Level.FINE, + "WAL width: " + + header.getWidth() + + " height: " + + header.getHeight()); + } catch (IOException e) { + throw new IIOException("Error reading quake2 WAL header", e); + } + } +}
\ No newline at end of file diff --git a/src/jake2/imageio/WALImageReaderSpi.java b/src/jake2/imageio/WALImageReaderSpi.java new file mode 100644 index 0000000..f731165 --- /dev/null +++ b/src/jake2/imageio/WALImageReaderSpi.java @@ -0,0 +1,127 @@ +/* + * Created on Nov 18, 2003 + * + */ +package jake2.imageio; + +import java.io.IOException; +import java.util.Locale; +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + +/** + * @author cwei + * + * Quake 2 stores textures in a proprietary 2D image format called WAL. + * While this isn't actually part of the BSP file format, + * it's essential information for loading Quake 2 maps. + * WAL textures are stored in a 8-bit indexed color format with a specific palette being used by all textures. + * (this palette is stored in the PAK data file that comes with Quake 2 e.g. pics/colormap.pcx). + * Four mip-map levels are stored for each texture at sizes decreasing by a factor of two. + * This is mostly for software rendering since most 3D APIs will automatically generate + * the mip-map levels when you create a texture. + * Each frame of an animated texture is stored as an individual WAL file, + * and the animation sequence is encoded by storing the name of the next texture in the sequence for each frame; + * texture names are stored with paths and without any extension. + * The format for the WAL file header is the wal_header structure: + * <code> + struct wal_header + { + char name[32]; // name of the texture + + uint32 width; // width (in pixels) of the largest mipmap level + uint32 height; // height (in pixels) of the largest mipmap level + + int32 offset[4]; // byte offset of the start of each of the 4 mipmap levels + + char next_name[32]; // name of the next texture in the animation + + uint32 flags; // ? + uint32 contents; // ? + uint32 value; // ? + }; + </code> + * The actual texture data is stored in an 8-bits-per-pixel raw format in a left-right, top-down order. + * <br> + * put the own ImageReaderSpi class name in the file (relative to classpath): + * <code>META-INF/services/javax.imageio.spi.ImageReaderSpi</code> + * + * */ +public class WALImageReaderSpi extends ImageReaderSpi { + + static final String vendorName = "[email protected]"; + static final String version = "1.0_beta"; + static final String[] names = { "quake2 wal" }; + static final String[] suffixes = { "wal" }; + static final String[] MIMETypes = { "image/x-quake2-wal" }; + static final String readerClassName = "jake2.imageio.WALImageReader"; + static final String[] writerSpiNames = null; // { "jake2.imageio.WALImageWriterSpi" }; + + // Metadata formats, more information below + static final boolean supportsStandardStreamMetadataFormat = false; + static final String nativeStreamMetadataFormatName = null; + + static final String nativeStreamMetadataFormatClassName = null; + static final String[] extraStreamMetadataFormatNames = null; + static final String[] extraStreamMetadataFormatClassNames = null; + static final boolean supportsStandardImageMetadataFormat = false; + static final String nativeImageMetadataFormatName = + "jake2.imageio.WALMetaData_1.0"; + static final String nativeImageMetadataFormatClassName = null; // "jake2.imageio.WALMetadata"; + static final String[] extraImageMetadataFormatNames = null; + static final String[] extraImageMetadataFormatClassNames = null; + + public WALImageReaderSpi() { + + super( + vendorName, + version, + names, + suffixes, + MIMETypes, + readerClassName, + ImageReaderSpi.STANDARD_INPUT_TYPE, // Accept ImageInputStreams + writerSpiNames, + supportsStandardStreamMetadataFormat, + nativeStreamMetadataFormatName, + nativeStreamMetadataFormatClassName, + extraStreamMetadataFormatNames, + extraStreamMetadataFormatClassNames, + supportsStandardImageMetadataFormat, + nativeImageMetadataFormatName, + nativeImageMetadataFormatClassName, + extraImageMetadataFormatNames, + extraImageMetadataFormatClassNames); + } + + public boolean canDecodeInput(Object source) throws IOException { + if (!(source instanceof ImageInputStream)) { + return false; + } + ImageInputStream stream = (ImageInputStream)source; + byte[] buffer = new byte[WAL.HEADER_SIZE]; + try { + stream.mark(); + stream.readFully(buffer); + stream.reset(); + // buffer will be converted to members and header checked + WAL.Header wal = new WAL.Header(buffer); + } catch (IllegalArgumentException e) { + return false; + } + return true; + } + + /** + * returns a WALImageReader + */ + public ImageReader createReaderInstance(Object extension) + throws IOException { + return new WALImageReader(this); + } + + public String getDescription(Locale locale) { + return "id-software's Quake2 wal format for textures"; + } +} |