diff options
author | Kenneth Russel <[email protected]> | 2006-01-09 02:37:43 +0000 |
---|---|---|
committer | Kenneth Russel <[email protected]> | 2006-01-09 02:37:43 +0000 |
commit | b464faaae70ba12e5842b901a6c6d1601af0e1c7 (patch) | |
tree | 6411322c882c48dfe04870adf981c33609fabefa /src/classes/com/sun/opengl | |
parent | c1e4d8a8937ecce5bbc88c6de604ce35e793b9cc (diff) |
Added output support to TGAImage, SGIImage and newly-renamed DDSImage
classes. Added support to TextureIO for writing textures back to disk
via new TextureWriter plug-in interface. Added TextureConvert demo
which shows how an application might convert between arbitrary file
formats using these APIs, including automatic compression to DXT3
format when available.
git-svn-id: file:///usr/local/projects/SUN/JOGL/git-svn/svn-server-sync/jogl/trunk@525 232f8b59-042b-4e1e-8c03-345bb8c30851
Diffstat (limited to 'src/classes/com/sun/opengl')
-rwxr-xr-x | src/classes/com/sun/opengl/utils/DDSImage.java (renamed from src/classes/com/sun/opengl/utils/DDSReader.java) | 264 | ||||
-rwxr-xr-x | src/classes/com/sun/opengl/utils/SGIImage.java | 283 | ||||
-rwxr-xr-x | src/classes/com/sun/opengl/utils/TGAImage.java | 112 | ||||
-rwxr-xr-x | src/classes/com/sun/opengl/utils/Texture.java | 41 | ||||
-rwxr-xr-x | src/classes/com/sun/opengl/utils/TextureData.java | 11 | ||||
-rwxr-xr-x | src/classes/com/sun/opengl/utils/TextureIO.java | 454 | ||||
-rwxr-xr-x | src/classes/com/sun/opengl/utils/TextureWriter.java | 55 |
7 files changed, 1106 insertions, 114 deletions
diff --git a/src/classes/com/sun/opengl/utils/DDSReader.java b/src/classes/com/sun/opengl/utils/DDSImage.java index dbbf8e65b..305e77348 100755 --- a/src/classes/com/sun/opengl/utils/DDSReader.java +++ b/src/classes/com/sun/opengl/utils/DDSImage.java @@ -43,12 +43,12 @@ import java.io.*; import java.nio.*; import java.nio.channels.*; -/** A reader for DirectDraw Surface (.dds) files, which are used to - describe textures. These files can contain multiple mipmap levels - in one file. This reader is currently minimal and does not support - all of the possible file formats. */ +/** A reader and writer for DirectDraw Surface (.dds) files, which are + used to describe textures. These files can contain multiple mipmap + levels in one file. This class is currently minimal and does not + support all of the possible file formats. */ -public class DDSReader { +public class DDSImage { /** Simple class describing images and data; does not encapsulate image format information. User is responsible for transmitting @@ -134,30 +134,90 @@ public class DDSReader { public static final int D3DFMT_DXT4 = 0x34545844; public static final int D3DFMT_DXT5 = 0x35545844; - public void loadFile(String filename) throws IOException { - loadFile(new File(filename)); + /** Reads a DirectDraw surface from the specified file name, + returning the resulting DDSImage. */ + public static DDSImage read(String filename) throws IOException { + return read(new File(filename)); } - - public void loadFile(File file) throws IOException { - fis = new FileInputStream(file); - chan = fis.getChannel(); - buf = chan.map(FileChannel.MapMode.READ_ONLY, - 0, (int) file.length()); - buf.order(ByteOrder.LITTLE_ENDIAN); - header = new Header(); - header.read(buf); - fixupHeader(); + + /** Reads a DirectDraw surface from the specified file, returning + the resulting DDSImage. */ + public static DDSImage read(File file) throws IOException { + DDSImage image = new DDSImage(); + image.readFromFile(file); + return image; } + /** Closes open files and resources associated with the open + DDSImage. No other methods may be called on this object once + this is called. */ public void close() { try { - chan.close(); - fis.close(); + if (chan != null) { + chan.close(); + chan = null; + } + if (fis != null) { + fis.close(); + fis = null; + } + buf = null; } catch (IOException e) { e.printStackTrace(); } } + /** + * Creates a new DDSImage from data supplied by the user. The + * resulting DDSImage can be written to disk using the write() + * method. + * + * @param d3dFormat the D3DFMT_ constant describing the data; it is + * assumed that it is packed tightly + * @param width the width in pixels of the topmost mipmap image + * @param height the height in pixels of the topmost mipmap image + * @param mipmapData the data for each mipmap level of the resulting + * DDSImage; either only one mipmap level should + * be specified, or they all must be + * @throws IllegalArgumentException if the data does not match the + * specified arguments + */ + public static DDSImage createFromData(int d3dFormat, + int width, + int height, + ByteBuffer[] mipmapData) throws IllegalArgumentException { + DDSImage image = new DDSImage(); + image.initFromData(d3dFormat, width, height, mipmapData); + return image; + } + + /** + * Writes this DDSImage to the specified file name. + */ + public void write(String filename) throws IOException { + write(new File(filename)); + } + + /** + * Writes this DDSImage to the specified file name. + */ + public void write(File file) throws IOException { + FileOutputStream stream = new FileOutputStream(file); + FileChannel chan = stream.getChannel(); + // Create ByteBuffer for header in case the start of our + // ByteBuffer isn't actually memory-mapped + ByteBuffer hdr = ByteBuffer.allocate(Header.writtenSize()); + hdr.order(ByteOrder.LITTLE_ENDIAN); + header.write(hdr); + hdr.rewind(); + chan.write(hdr); + buf.position(Header.writtenSize()); + chan.write(buf); + chan.force(true); + chan.close(); + stream.close(); + } + /** Test for presence/absence of surface description flags (DDSD_*) */ public boolean isSurfaceDescFlagSet(int flag) { return ((header.flags & flag) != 0); @@ -246,7 +306,7 @@ public class DDSReader { } // Figure out how far to seek - int seek = 4 + header.size; + int seek = Header.writtenSize(); for (int i = 0; i < map; i++) { seek += mipMapSizeInBytes(i); } @@ -439,6 +499,152 @@ public class DDSReader { ddsCapsReserved2 = buf.getInt(); textureStage = buf.getInt(); } + + // buf must be in little-endian byte order + void write(ByteBuffer buf) { + buf.putInt(MAGIC); + buf.putInt(size); + buf.putInt(flags); + buf.putInt(height); + buf.putInt(width); + buf.putInt(pitchOrLinearSize); + buf.putInt(backBufferCountOrDepth); + buf.putInt(mipMapCountOrAux); + buf.putInt(alphaBitDepth); + buf.putInt(reserved1); + buf.putInt(surface); + buf.putInt(colorSpaceLowValue); + buf.putInt(colorSpaceHighValue); + buf.putInt(destBltColorSpaceLowValue); + buf.putInt(destBltColorSpaceHighValue); + buf.putInt(srcOverlayColorSpaceLowValue); + buf.putInt(srcOverlayColorSpaceHighValue); + buf.putInt(srcBltColorSpaceLowValue); + buf.putInt(srcBltColorSpaceHighValue); + buf.putInt(pfSize); + buf.putInt(pfFlags); + buf.putInt(pfFourCC); + buf.putInt(pfRGBBitCount); + buf.putInt(pfRBitMask); + buf.putInt(pfGBitMask); + buf.putInt(pfBBitMask); + buf.putInt(pfABitMask); + buf.putInt(ddsCaps1); + buf.putInt(ddsCaps2); + buf.putInt(ddsCapsReserved1); + buf.putInt(ddsCapsReserved2); + buf.putInt(textureStage); + } + + private static final int size() { + return 124; + } + + private static final int pfSize() { + return 32; + } + + private static final int writtenSize() { + return 128; + } + } + + private DDSImage() { + } + + private void readFromFile(File file) throws IOException { + fis = new FileInputStream(file); + chan = fis.getChannel(); + buf = chan.map(FileChannel.MapMode.READ_ONLY, + 0, (int) file.length()); + buf.order(ByteOrder.LITTLE_ENDIAN); + header = new Header(); + header.read(buf); + fixupHeader(); + } + + private void initFromData(int d3dFormat, + int width, + int height, + ByteBuffer[] mipmapData) throws IllegalArgumentException { + // Check size of mipmap data compared against format, width and + // height + int topmostMipmapSize = width * height; + int pitchOrLinearSize = width; + boolean isCompressed = false; + switch (d3dFormat) { + case D3DFMT_R8G8B8: topmostMipmapSize *= 3; pitchOrLinearSize *= 3; break; + case D3DFMT_A8R8G8B8: topmostMipmapSize *= 4; pitchOrLinearSize *= 4; break; + case D3DFMT_X8R8G8B8: topmostMipmapSize *= 4; pitchOrLinearSize *= 4; break; + case D3DFMT_DXT1: + case D3DFMT_DXT2: + case D3DFMT_DXT3: + case D3DFMT_DXT4: + case D3DFMT_DXT5: + topmostMipmapSize = computeCompressedBlockSize(width, height, 1, d3dFormat); + pitchOrLinearSize = topmostMipmapSize; + isCompressed = true; + break; + default: + throw new IllegalArgumentException("d3dFormat must be one of the known formats"); + } + + // Now check the mipmaps against this size + int curSize = topmostMipmapSize; + int totalSize = 0; + for (int i = 0; i < mipmapData.length; i++) { + if (mipmapData[i].remaining() != curSize) { + throw new IllegalArgumentException("Mipmap level " + i + + " didn't match expected data size (expected " + curSize + ", got " + + mipmapData[i].remaining() + ")"); + } + curSize /= 4; + totalSize += mipmapData[i].remaining(); + } + + // OK, create one large ByteBuffer to hold all of the mipmap data + totalSize += Header.writtenSize(); + ByteBuffer buf = ByteBuffer.allocate(totalSize); + buf.position(Header.writtenSize()); + for (int i = 0; i < mipmapData.length; i++) { + buf.put(mipmapData[i]); + } + this.buf = buf; + + // Allocate and initialize a Header + header = new Header(); + header.size = Header.size(); + header.flags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT; + if (mipmapData.length > 1) { + header.flags |= DDSD_MIPMAPCOUNT; + header.mipMapCountOrAux = mipmapData.length; + } + header.width = width; + header.height = height; + if (isCompressed) { + header.flags |= DDSD_LINEARSIZE; + header.pfFlags |= DDPF_FOURCC; + header.pfFourCC = d3dFormat; + } else { + header.flags |= DDSD_PITCH; + // Figure out the various settings from the pixel format + header.pfFlags |= DDPF_RGB; + switch (d3dFormat) { + case D3DFMT_R8G8B8: header.pfRGBBitCount = 24; break; + case D3DFMT_A8R8G8B8: header.pfRGBBitCount = 32; header.pfFlags |= DDPF_ALPHAPIXELS; break; + case D3DFMT_X8R8G8B8: header.pfRGBBitCount = 32; break; + } + header.pfRBitMask = 0x00FF0000; + header.pfGBitMask = 0x0000FF00; + header.pfBBitMask = 0x000000FF; + if (d3dFormat == D3DFMT_A8R8G8B8) { + header.pfABitMask = 0xFF000000; + } + } + header.pitchOrLinearSize = pitchOrLinearSize; + header.pfSize = Header.pfSize(); + // Not sure whether we can get away with leaving the rest of the + // header blank } // Microsoft doesn't follow their own specifications and the @@ -454,17 +660,25 @@ public class DDSReader { depth = 1; } - int blockSize = ((getWidth() + 3)/4) * ((getHeight() + 3)/4) * ((depth + 3)/4); - switch (getCompressionFormat()) { - case D3DFMT_DXT1: blockSize *= 8; break; - default: blockSize *= 16; break; - } + int blockSize = computeCompressedBlockSize(getWidth(), getHeight(), depth, getCompressionFormat()); header.pitchOrLinearSize = blockSize; header.flags |= DDSD_LINEARSIZE; } } + private static int computeCompressedBlockSize(int width, + int height, + int depth, + int compressionFormat) { + int blockSize = ((width + 3)/4) * ((height + 3)/4) * ((depth + 3)/4); + switch (compressionFormat) { + case D3DFMT_DXT1: blockSize *= 8; break; + default: blockSize *= 16; break; + } + return blockSize; + } + private int mipMapWidth(int map) { int width = getWidth(); for (int i = 0; i < map; i++) { diff --git a/src/classes/com/sun/opengl/utils/SGIImage.java b/src/classes/com/sun/opengl/utils/SGIImage.java index d115604b1..f06c10b56 100755 --- a/src/classes/com/sun/opengl/utils/SGIImage.java +++ b/src/classes/com/sun/opengl/utils/SGIImage.java @@ -47,7 +47,7 @@ import com.sun.opengl.utils.*; import java.awt.image.*; import javax.swing.*; -/** <p> Reads SGI RGB/RGBA images. </p> +/** <p> Reads and writes SGI RGB/RGBA images. </p> <p> Written from <a href = "http://astronomy.swin.edu.au/~pbourke/dataformats/sgirgb/">Paul @@ -104,6 +104,10 @@ public class SGIImage { // 404 bytes char DUMMY Ignored // Should be set to 0, makes the header 512 bytes. + Header() { + magic = MAGIC; + } + Header(DataInputStream in) throws IOException { magic = in.readShort(); storage = in.readByte(); @@ -159,6 +163,35 @@ public class SGIImage { return res; } + /** Writes this SGIImage to the specified file name. If + flipVertically is set, outputs the scanlines from top to bottom + rather than the default bottom to top order. */ + public void write(String filename, boolean flipVertically) throws IOException { + write(new File(filename), flipVertically); + } + + /** Writes this SGIImage to the specified file. If flipVertically is + set, outputs the scanlines from top to bottom rather than the + default bottom to top order. */ + public void write(File file, boolean flipVertically) throws IOException { + writeImage(file, data, header.xsize, header.ysize, header.zsize, flipVertically); + } + + /** Creates an SGIImage from the specified data in either RGB or + RGBA format. */ + public static SGIImage createFromData(int width, + int height, + boolean hasAlpha, + byte[] data) { + Header header = new Header(); + header.xsize = (short) width; + header.ysize = (short) height; + header.zsize = (short) (hasAlpha ? 4 : 3); + SGIImage image = new SGIImage(header); + image.data = data; + return image; + } + /** Determines from the magic number whether the given InputStream points to an SGI RGB image. The given InputStream must return true from markSupported() and support a minimum of two bytes @@ -258,6 +291,7 @@ public class SGIImage { tmpData = null; tmpRead = null; format = GL.GL_RGBA; + header.zsize = 4; } private void getRow(byte[] buf, int y, int z) { @@ -325,6 +359,250 @@ public class SGIImage { } } + private static byte imgref(byte[] i, + int x, + int y, + int z, + int xs, + int ys, + int zs) { + return i[(xs*ys*z)+(xs*y)+x]; + } + + + private void writeHeader(DataOutputStream stream, + int xsize, int ysize, int zsize, boolean rle) throws IOException { + // effects: outputs the 512-byte IRIS RGB header to STREAM, using xsize, + // ysize, and depth as the dimensions of the image. NOTE that + // the following defaults are used: + // STORAGE = 1 (storage format = RLE) + // BPC = 1 (# bytes/channel) + // DIMENSION = 3 + // PIXMIN = 0 + // PIXMAX = 255 + // IMAGENAME = <80 nulls> + // COLORMAP = 0 + // See ftp://ftp.sgi.com/pub/sgi/SGIIMAGESPEC for more details. + + // write out MAGIC, STORAGE, BPC + stream.writeShort(474); + stream.write((rle ? 1 : 0)); + stream.write(1); + + // write out DIMENSION + stream.writeShort(3); + + // write XSIZE, YSIZE, ZSIZE + stream.writeShort(xsize); + stream.writeShort(ysize); + stream.writeShort(zsize); + + // write PIXMIN, PIXMAX + stream.writeInt(0); + stream.writeInt(255); + + // write DUMMY + stream.writeInt(0); + + // write IMAGENAME + for (int i = 0; i < 80; i++) + stream.write(0); + + // write COLORMAP + stream.writeInt(0); + + // write DUMMY (404 bytes) + for (int i = 0; i < 404; i++) + stream.write(0); + } + + private void writeImage(File file, + byte[] data, + int xsize, + int ysize, + int zsize, + boolean yflip) throws IOException { + // Input data is in RGBRGBRGB or RGBARGBARGBA format; first unswizzle it + byte[] tmpData = new byte[xsize * ysize * zsize]; + int dest = 0; + for (int i = 0; i < zsize; i++) { + for (int j = i; j < (xsize * ysize * zsize); j += zsize) { + tmpData[dest++] = data[j]; + } + } + data = tmpData; + + // requires: DATA must be an array of size XSIZE * YSIZE * ZSIZE, + // indexed in the following manner: + // data[0] ...data[xsize-1] == first row of first channel + // data[xsize]...data[2*xsize-1] == second row of first channel + // ... data[(ysize - 1) * xsize]...data[(ysize * xsize) - 1] == + // last row of first channel + // Later channels follow the same format. + // *** NOTE that "first row" is defined by the BOTTOM ROW of + // the image. That is, the origin is in the lower left corner. + // effects: writes out an SGI image to FILE, RLE-compressed, INCLUDING + // header, of dimensions (xsize, ysize, zsize), and containing + // the data in DATA. If YFLIP is set, outputs the data in DATA + // in reverse order vertically (equivalent to a flip about the + // x axis). + + // Build the offset tables + int[] starttab = new int[ysize * zsize]; + int[] lengthtab = new int[ysize * zsize]; + + // Temporary buffer for holding RLE data. + // Note that this makes the assumption that RLE-compressed data will + // never exceed twice the size of the input data. + // There are surely formal proofs about how big the RLE buffer should + // be, as well as what the optimal look-ahead size is (i.e. don't switch + // copy/repeat modes for less than N repeats). However, I'm going from + // empirical evidence here; the break-even point seems to be a look- + // ahead of 3. (That is, if the three values following this one are all + // the same as the current value, switch to repeat mode.) + int lookahead = 3; + byte[] rlebuf = new byte[2 * xsize * ysize * zsize]; + + int cur_loc = 0; // current offset location. + int ptr = 0; + int total_size = 0; + int ystart = 0; + int yincr = 1; + int yend = ysize; + + if (yflip) { + ystart = ysize - 1; + yend = -1; + yincr = -1; + } + + boolean DEBUG = false; + + for (int z = 0; z < zsize; z++) { + for (int y = ystart; y != yend; y += yincr) { + // RLE-compress each row. + + int x = 0; + byte count = 0; + boolean repeat_mode = false; + boolean should_switch = false; + int start_ptr = ptr; + int num_ptr = ptr++; + byte repeat_val = 0; + + while (x < xsize) { + // see if we should switch modes + should_switch = false; + if (repeat_mode) { + if (imgref(data, x, y, z, xsize, ysize, zsize) != repeat_val) { + should_switch = true; + } + } else { + // look ahead to see if we should switch to repeat mode. + // stay within the scanline for the lookahead + if ((x + lookahead) < xsize) { + should_switch = true; + for (int i = 1; i <= lookahead; i++) { + if (DEBUG) + System.err.println("left side was " + ((int) imgref(data, x, y, z, xsize, ysize, zsize)) + + ", right side was " + (int)imgref(data, x+i, y, z, xsize, ysize, zsize)); + + if (imgref(data, x, y, z, xsize, ysize, zsize) != + imgref(data, x+i, y, z, xsize, ysize, zsize)) + should_switch = false; + } + } + } + + if (should_switch || (count == 127)) { + // update the number of elements we repeated/copied + if (x > 0) { + if (repeat_mode) + rlebuf[num_ptr] = count; + else + rlebuf[num_ptr] = (byte) (count | 0x80); + } + // perform mode switch if necessary; output repeat_val if + // switching FROM repeat mode, and set it if switching + // TO repeat mode. + if (repeat_mode) { + if (should_switch) + repeat_mode = false; + rlebuf[ptr++] = repeat_val; + } else { + if (should_switch) + repeat_mode = true; + repeat_val = imgref(data, x, y, z, xsize, ysize, zsize); + } + + if (x > 0) { + // reset the number pointer + num_ptr = ptr++; + // reset number of bytes copied + count = 0; + } + } + + // if not in repeat mode, copy element to ptr + if (!repeat_mode) { + rlebuf[ptr++] = imgref(data, x, y, z, xsize, ysize, zsize); + } + count++; + + if (x == xsize - 1) { + // Need to store the number of pixels we copied/repeated. + if (repeat_mode) { + rlebuf[num_ptr] = count; + // If we ended the row in repeat mode, store the + // repeated value + rlebuf[ptr++] = repeat_val; + } + else + rlebuf[num_ptr] = (byte) (count | 0x80); + + // output zero counter for the last value in the row + rlebuf[ptr++] = 0; + } + + x++; + } + // output this row's length into the length table + int rowlen = ptr - start_ptr; + if (yflip) + lengthtab[ysize*z+(ysize-y-1)] = rowlen; + else + lengthtab[ysize*z+y] = rowlen; + // add to the start table, and update the current offset + if (yflip) + starttab[ysize*z+(ysize-y-1)] = cur_loc; + else + starttab[ysize*z+y] = cur_loc; + cur_loc += rowlen; + } + } + + // Now we have the offset tables computed, as well as the RLE data. + // Output this information to the file. + total_size = ptr; + + if (DEBUG) + System.err.println("total_size was " + total_size); + + DataOutputStream stream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file))); + + writeHeader(stream, xsize, ysize, zsize, true); + + int SIZEOF_INT = 4; + for (int i = 0; i < (ysize * zsize); i++) + stream.writeInt(starttab[i] + 512 + (2 * ysize * zsize * SIZEOF_INT)); + for (int i = 0; i < (ysize * zsize); i++) + stream.writeInt(lengthtab[i]); + for (int i = 0; i < total_size; i++) + stream.write(rlebuf[i]); + + stream.close(); + } + private byte[] readAll(DataInputStream in) throws IOException { byte[] dest = new byte[16384]; int pos = 0; @@ -357,6 +635,8 @@ public class SGIImage { return dest; } + // Test case + /* public static void main(String[] args) { for (int i = 0; i < args.length; i++) { try { @@ -387,4 +667,5 @@ public class SGIImage { } } } + */ } diff --git a/src/classes/com/sun/opengl/utils/TGAImage.java b/src/classes/com/sun/opengl/utils/TGAImage.java index d5505afd5..c676d1a6a 100755 --- a/src/classes/com/sun/opengl/utils/TGAImage.java +++ b/src/classes/com/sun/opengl/utils/TGAImage.java @@ -40,11 +40,13 @@ package com.sun.opengl.utils; import java.io.*; +import java.nio.*; +import java.nio.channels.*; import javax.media.opengl.*; import com.sun.opengl.utils.*; /** - * Targa image reader adapted from sources of the <a href = + * Targa image reader and writer adapted from sources of the <a href = * "http://java.sun.com/products/jimi/">Jimi</a> image I/O class library. * * <P> @@ -70,7 +72,7 @@ import com.sun.opengl.utils.*; public class TGAImage { private Header header; private int format; - private byte[] data; + private ByteBuffer data; private TGAImage(Header header) { this.header = header; @@ -100,7 +102,7 @@ public class TGAImage { /** Field image descriptor bitfield values definitions */ public final static int ID_ATTRIBPERPIXEL = 0xF; - public final static int ID_LEFTTORIGHT = 0x10; + public final static int ID_RIGHTTOLEFT = 0x10; public final static int ID_TOPTOBOTTOM = 0x20; public final static int ID_INTERLEAVE = 0xC0; @@ -130,15 +132,14 @@ public class TGAImage { private byte pixelDepth; private byte imageDescriptor; - /** bitfields in imageDescriptor */ - private byte attribPerPixel; // how many attribute bits per pixel - private boolean leftToRight; // order of data on scan line - private boolean topToBottom; // order scan lines stored - private byte interleave; // how rows are stored in image data - private byte[] imageIDbuf; private String imageID; + // For construction from user data + Header() { + tgaType = TYPE_OLD; // dont try and get footer. + } + Header(LEDataInputStream in) throws IOException { int ret; @@ -146,7 +147,7 @@ public class TGAImage { // initial header fields idLength = in.readUnsignedByte(); - colorMapType = in.readUnsignedByte(); + colorMapType = in.readUnsignedByte(); imageType = in.readUnsignedByte(); // color map header fields @@ -162,11 +163,6 @@ public class TGAImage { pixelDepth = in.readByte(); imageDescriptor = in.readByte(); - attribPerPixel = (byte)(imageDescriptor & ID_ATTRIBPERPIXEL); - leftToRight = (imageDescriptor & ID_LEFTTORIGHT) != 0; // not used ? - topToBottom = (imageDescriptor & ID_TOPTOBOTTOM) != 0; - interleave = (byte)((imageDescriptor & ID_INTERLEAVE) >> 6); - if (idLength > 0) { imageIDbuf = new byte[idLength]; in.read(imageIDbuf, 0, idLength); @@ -195,10 +191,10 @@ public class TGAImage { public byte imageDescriptor() { return imageDescriptor; } /** bitfields in imageDescriptor */ - public byte attribPerPixel() { return attribPerPixel; } - public boolean leftToRight() { return leftToRight; } - public boolean topToBottom() { return topToBottom; } - public byte interleave() { return interleave; } + public byte attribPerPixel() { return (byte)(imageDescriptor & ID_ATTRIBPERPIXEL); } + public boolean rightToLeft() { return ((imageDescriptor & ID_RIGHTTOLEFT) != 0); } + public boolean topToBottom() { return ((imageDescriptor & ID_TOPTOBOTTOM) != 0); } + public byte interleave() { return (byte)((imageDescriptor & ID_INTERLEAVE) >> 6); } public byte[] imageIDbuf() { return imageIDbuf; } public String imageID() { return imageID; } @@ -219,6 +215,32 @@ public class TGAImage { " image descriptor: "+ imageDescriptor + (imageIDbuf == null ? "" : (" ID String: " + imageID)); } + + public int size() { return 18 + idLength; } + + // buf must be in little-endian byte order + private void write(ByteBuffer buf) { + buf.put((byte) idLength); + buf.put((byte) colorMapType); + buf.put((byte) imageType); + buf.putShort((short) firstEntryIndex); + buf.putShort((short) colorMapLength); + buf.put((byte) colorMapEntrySize); + buf.putShort((short) xOrigin); + buf.putShort((short) yOrigin); + buf.putShort((short) width); + buf.putShort((short) height); + buf.put((byte) pixelDepth); + buf.put((byte) imageDescriptor); + if (idLength > 0) { + try { + byte[] chars = imageID.getBytes("US-ASCII"); + buf.put(chars); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + } } @@ -269,7 +291,7 @@ public class TGAImage { int raw; // index through the raw input buffer int rawWidth = header.width() * (header.pixelDepth() / 8); byte[] rawBuf = new byte[rawWidth]; - data = new byte[rawWidth * header.height()]; + byte[] tmpData = new byte[rawWidth * header.height()]; if (header.pixelDepth() == 24) { format = GL.GL_BGR; @@ -281,13 +303,15 @@ public class TGAImage { for (i = 0; i < header.height(); ++i) { dIn.readFully(rawBuf, 0, rawWidth); - if (header.topToBottom) + if (header.topToBottom()) y = header.height - i - 1; // range 0 to (header.height - 1) else y = i; - System.arraycopy(rawBuf, 0, data, y * rawWidth, rawBuf.length); + System.arraycopy(rawBuf, 0, tmpData, y * rawWidth, rawBuf.length); } + + data = ByteBuffer.wrap(tmpData); } /** Returns the width of the image. */ @@ -301,7 +325,7 @@ public class TGAImage { /** Returns the raw data for this texture in the correct (bottom-to-top) order for calls to glTexImage2D. */ - public byte[] getData() { return data; } + public ByteBuffer getData() { return data; } /** Reads a Targa image from the specified file. */ public static TGAImage read(String filename) throws IOException { @@ -317,4 +341,46 @@ public class TGAImage { res.decodeImage(dIn); return res; } + + /** Writes the image in Targa format to the specified file name. */ + public void write(String filename) throws IOException { + write(new File(filename)); + } + + /** Writes the image in Targa format to the specified file. */ + public void write(File file) throws IOException { + FileOutputStream stream = new FileOutputStream(file); + FileChannel chan = stream.getChannel(); + ByteBuffer buf = ByteBuffer.allocate(header.size()); + buf.order(ByteOrder.LITTLE_ENDIAN); + header.write(buf); + buf.rewind(); + chan.write(buf); + chan.write(data); + data.rewind(); + chan.force(true); + chan.close(); + stream.close(); + } + + /** Creates a TGAImage from data supplied by the end user. Shares + data with the passed ByteBuffer. Assumes the data is already in + the correct byte order for writing to disk, i.e., BGR or + BGRA. */ + public static TGAImage createFromData(int width, + int height, + boolean hasAlpha, + boolean topToBottom, + ByteBuffer data) { + Header header = new Header(); + header.imageType = Header.UTRUECOLOR; + header.width = width; + header.height = height; + header.pixelDepth = (byte) (hasAlpha ? 32 : 24); + header.imageDescriptor = (byte) (topToBottom ? Header.ID_TOPTOBOTTOM : 0); + // Note ID not supported + TGAImage ret = new TGAImage(header); + ret.data = data; + return ret; + } } diff --git a/src/classes/com/sun/opengl/utils/Texture.java b/src/classes/com/sun/opengl/utils/Texture.java index efecdffea..924161d69 100755 --- a/src/classes/com/sun/opengl/utils/Texture.java +++ b/src/classes/com/sun/opengl/utils/Texture.java @@ -79,7 +79,7 @@ public class Texture { // For now make Texture constructor package-private to limit the // number of public APIs we commit to Texture(TextureData data) throws GLException { - GL gl = getCurrentGL(); + GL gl = GLU.getCurrentGL(); texID = createTextureID(gl); updateImage(data); @@ -88,7 +88,7 @@ public class Texture { // Constructor for use when creating e.g. cube maps, where there is // no initial texture data Texture(int target) throws GLException { - GL gl = getCurrentGL(); + GL gl = GLU.getCurrentGL(); texID = createTextureID(gl); this.target = target; } @@ -101,7 +101,7 @@ public class Texture { * OpenGL-related errors occurred */ public void enable() throws GLException { - getCurrentGL().glEnable(target); + GLU.getCurrentGL().glEnable(target); } /** @@ -112,7 +112,7 @@ public class Texture { * OpenGL-related errors occurred */ public void disable() throws GLException { - getCurrentGL().glDisable(target); + GLU.getCurrentGL().glDisable(target); } /** @@ -122,7 +122,7 @@ public class Texture { * OpenGL-related errors occurred */ public void bind() throws GLException { - getCurrentGL().glBindTexture(target, texID); + GLU.getCurrentGL().glBindTexture(target, texID); } /** @@ -132,7 +132,7 @@ public class Texture { * OpenGL-related errors occurred */ public void dispose() throws GLException { - getCurrentGL().glDeleteTextures(1, new int[] {texID}, 0); + GLU.getCurrentGL().glDeleteTextures(1, new int[] {texID}, 0); texID = 0; } @@ -242,6 +242,17 @@ public class Texture { } /** + * Indicates whether this texture's texture coordinates must be + * flipped vertically in order to properly display the texture. This + * is handled automatically by {@link #getImageTexCoords} and {@link + * #getSubImageTexCoords}, but applications may generate or + * otherwise produce texture coordinates which must be corrected. + */ + public boolean getMustFlipVertically() { + return mustFlipVertically; + } + + /** * Updates the content area of the specified target of this texture * using the data in the given image. In general this is intended * for construction of cube maps. @@ -250,7 +261,7 @@ public class Texture { * OpenGL-related errors occurred */ public void updateImage(TextureData data, int target) throws GLException { - GL gl = getCurrentGL(); + GL gl = GLU.getCurrentGL(); imgWidth = data.getWidth(); imgHeight = data.getHeight(); @@ -395,7 +406,7 @@ public class Texture { public void setTexParameteri(int parameterName, int value) { bind(); - GL gl = getCurrentGL(); + GL gl = GLU.getCurrentGL(); gl.glTexParameteri(target, parameterName, value); } @@ -413,18 +424,6 @@ public class Texture { // /** - * Returns the current GL object. Throws GLException if no OpenGL - * context was current. - */ - private static GL getCurrentGL() throws GLException { - GLContext context = GLContext.getCurrent(); - if (context == null) { - throw new GLException("No OpenGL context current on current thread"); - } - return context.getGL(); - } - - /** * Returns true if the given value is a power of two. * * @return true if the given value is a power of two, false otherwise @@ -475,7 +474,7 @@ public class Texture { } private void updateSubImageImpl(TextureData data, int newTarget, int mipmapLevel, int x, int y) throws GLException { - GL gl = getCurrentGL(); + GL gl = GLU.getCurrentGL(); gl.glBindTexture(newTarget, texID); int width = data.getWidth(); int height = data.getHeight(); diff --git a/src/classes/com/sun/opengl/utils/TextureData.java b/src/classes/com/sun/opengl/utils/TextureData.java index a6fcd9073..6bb459432 100755 --- a/src/classes/com/sun/opengl/utils/TextureData.java +++ b/src/classes/com/sun/opengl/utils/TextureData.java @@ -316,16 +316,7 @@ public class TextureData { private void createNIOBufferFromImage(BufferedImage image, boolean flipVertically) { if (flipVertically) { - WritableRaster raster = image.getRaster(); - Object scanline1 = null; - Object scanline2 = null; - - for (int i = 0; i < image.getHeight() / 2; i++) { - scanline1 = raster.getDataElements(0, i, image.getWidth(), 1, scanline1); - scanline2 = raster.getDataElements(0, image.getHeight() - i - 1, image.getWidth(), 1, scanline2); - raster.setDataElements(0, i, image.getWidth(), 1, scanline2); - raster.setDataElements(0, image.getHeight() - i - 1, image.getWidth(), 1, scanline1); - } + TextureIO.flipImageVertically(image); } // diff --git a/src/classes/com/sun/opengl/utils/TextureIO.java b/src/classes/com/sun/opengl/utils/TextureIO.java index 07bb67ce4..d1f62cd5c 100755 --- a/src/classes/com/sun/opengl/utils/TextureIO.java +++ b/src/classes/com/sun/opengl/utils/TextureIO.java @@ -39,6 +39,7 @@ package com.sun.opengl.utils; +import java.awt.Graphics; import java.awt.image.*; import java.io.*; import java.net.*; @@ -47,25 +48,28 @@ import java.util.*; import javax.imageio.*; import javax.media.opengl.*; +import javax.media.opengl.glu.*; /** <P> Provides input and output facilities for both loading OpenGL textures from disk and streams as well as writing textures already in memory back to disk. </P> <P> The TextureIO class supports an arbitrary number of plug-in - TextureProviders which know how to produce TextureData objects - from files, InputStreams and URLs. The TextureData class - represents the raw data of the texture before it has been - converted to an OpenGL texture object. The Texture class + readers and writers via TextureProviders and TextureWriters. + TextureProviders know how to produce TextureData objects from + files, InputStreams and URLs. TextureWriters know how to write + TextureData objects to disk in various file formats. The + TextureData class represents the raw data of the texture before it + has been converted to an OpenGL texture object. The Texture class represents the OpenGL texture object and provides easy facilities for using the texture. </P> - <P> There are several built-in TextureProviders supplied with the - TextureIO implementation. The most basic provider uses the - platform's Image I/O facilities to read in a BufferedImage and - convert it to a texture. This is the baseline provider and is - registered so that it is the last one consulted. All others are - asked first to open a given file. </P> + <P> There are several built-in TextureProviders and TextureWriters + supplied with the TextureIO implementation. The most basic + provider uses the platform's Image I/O facilities to read in a + BufferedImage and convert it to a texture. This is the baseline + provider and is registered so that it is the last one consulted. + All others are asked first to open a given file. </P> <P> There are three other providers registered by default as of the time of this writing. One handles SGI RGB (".sgi", ".rgb") @@ -84,6 +88,18 @@ import javax.media.opengl.*; when probing for e.g. magic numbers at the head of the file to make sure not to disturb the state of the InputStream for downstream TextureProviders. </P> + + <P> There are analogous TextureWriters provided for writing + textures back to disk if desired. As of this writing, there are + four TextureWriters registered by default: one for Targa files, + one for SGI RGB files, one for DirectDraw surface (.dds) files, + and one for ImageIO-supplied formats such as .jpg and .png. Some + of these writers have certain limitations such as only being able + to write out textures stored in GL_RGB or GL_RGBA format. The DDS + writer supports fetching and writing to disk of texture data in + DXTn compressed format. Whether this will occur is dependent on + whether the texture's internal format is one of the DXTn + compressed formats and whether the target file is .dds format. */ public class TextureIO { @@ -396,10 +412,11 @@ public class TextureIO { * @param data the texture data to turn into an OpenGL texture * @throws GLException if no OpenGL context is current or if an * OpenGL error occurred + * @throws IllegalArgumentException if the passed TextureData was null */ - public static Texture newTexture(TextureData data) throws GLException { + public static Texture newTexture(TextureData data) throws GLException, IllegalArgumentException { if (data == null) { - return null; + throw new IllegalArgumentException("Null TextureData"); } return new Texture(data); } @@ -513,6 +530,120 @@ public class TextureIO { return new Texture(target); } + /** + * Writes the given texture to a file. The type of the file is + * inferred from its suffix. An OpenGL context must be current in + * order to fetch the texture data back from the OpenGL pipeline. + * This method causes the specified Texture to be bound to the + * GL_TEXTURE_2D state. If no suitable writer for the requested file + * format was found, throws an IOException. <P> + * + * Reasonable attempts are made to produce good results in the + * resulting images. The Targa, SGI and ImageIO writers produce + * results in the correct vertical orientation for those file + * formats. The DDS writer performs no vertical flip of the data, + * even in uncompressed mode. (It is impossible to perform such a + * vertical flip with compressed data.) Applications should keep + * this in mind when using this routine to save textures to disk for + * later re-loading. <P> + * + * Any mipmaps for the specified texture are currently discarded + * when it is written to disk, regardless of whether the underlying + * file format supports multiple mipmaps in a given file. + * + * @throws IOException if an error occurred during writing or no + * suitable writer was found + * @throws GLException if no OpenGL context was current or an + * OpenGL-related error occurred + */ + public static void write(Texture texture, File file) throws IOException, GLException { + if (texture.getTarget() != GL.GL_TEXTURE_2D) { + throw new GLException("Only GL_TEXTURE_2D textures are supported"); + } + + // First fetch the texture data + GL gl = GLU.getCurrentGL(); + + texture.bind(); + int internalFormat = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_INTERNAL_FORMAT); + int width = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_WIDTH); + int height = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_HEIGHT); + int border = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_BORDER); + TextureData data = null; + if (internalFormat == GL.GL_COMPRESSED_RGB_S3TC_DXT1_EXT || + internalFormat == GL.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT || + internalFormat == GL.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT || + internalFormat == GL.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT) { + // Fetch using glGetCompressedTexImage + int size = glGetTexLevelParameteri(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_COMPRESSED_IMAGE_SIZE); + ByteBuffer res = ByteBuffer.allocate(size); + gl.glGetCompressedTexImage(GL.GL_TEXTURE_2D, 0, res); + data = new TextureData(internalFormat, width, height, border, internalFormat, GL.GL_UNSIGNED_BYTE, + false, true, true, res, null); + } else { + int bytesPerPixel = 0; + int fetchedFormat = 0; + switch (internalFormat) { + case GL.GL_RGB: + case GL.GL_BGR: + case GL.GL_RGB8: + bytesPerPixel = 3; + fetchedFormat = GL.GL_RGB; + break; + case GL.GL_RGBA: + case GL.GL_BGRA: + case GL.GL_ABGR_EXT: + case GL.GL_RGBA8: + bytesPerPixel = 4; + fetchedFormat = GL.GL_RGBA; + break; + default: + throw new IOException("Unsupported texture internal format 0x" + Integer.toHexString(internalFormat)); + } + + // Fetch using glGetTexImage + int packAlignment = glGetInteger(GL.GL_PACK_ALIGNMENT); + int packRowLength = glGetInteger(GL.GL_PACK_ROW_LENGTH); + int packSkipRows = glGetInteger(GL.GL_PACK_SKIP_ROWS); + int packSkipPixels = glGetInteger(GL.GL_PACK_SKIP_PIXELS); + int packSwapBytes = glGetInteger(GL.GL_PACK_SWAP_BYTES); + + gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, 1); + gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH, 0); + gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS, 0); + gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS, 0); + gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES, 0); + + ByteBuffer res = ByteBuffer.allocate((width + (2 * border)) * + (height + (2 * border)) * + bytesPerPixel); + System.err.println("Allocated buffer of size " + res.remaining() + " for fetched image (" + + ((fetchedFormat == GL.GL_RGB) ? "GL_RGB" : "GL_RGBA") + ")"); + gl.glGetTexImage(GL.GL_TEXTURE_2D, 0, fetchedFormat, GL.GL_UNSIGNED_BYTE, res); + + gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, packAlignment); + gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH, packRowLength); + gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS, packSkipRows); + gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS, packSkipPixels); + gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES, packSwapBytes); + + data = new TextureData(internalFormat, width, height, border, fetchedFormat, GL.GL_UNSIGNED_BYTE, + false, false, false, res, null); + + System.out.println("data.getPixelFormat() = " + + ((data.getPixelFormat() == GL.GL_RGB) ? "GL_RGB" : "GL_RGBA")); + } + + for (Iterator iter = textureWriters.iterator(); iter.hasNext(); ) { + TextureWriter writer = (TextureWriter) iter.next(); + if (writer.write(file, data)) { + return; + } + } + + throw new IOException("No suitable texture writer found"); + } + //---------------------------------------------------------------------- // Helper function for above TextureProviders /** @@ -540,8 +671,25 @@ public class TextureIO { return toLowerCase(filename.substring(lastDot + 1)); } - // FIXME: add texture writing capabilities - // public void writeTextureToFile(Texture texture, File file, boolean saveUncompressed) throws IOException, GLException; + //---------------------------------------------------------------------- + // Helper function which may be more generally useful + // + + /** Flips the supplied BufferedImage vertically. This is often a + necessary conversion step to display a Java2D image correctly + with OpenGL and vice versa. */ + public static void flipImageVertically(BufferedImage image) { + WritableRaster raster = image.getRaster(); + Object scanline1 = null; + Object scanline2 = null; + + for (int i = 0; i < image.getHeight() / 2; i++) { + scanline1 = raster.getDataElements(0, i, image.getWidth(), 1, scanline1); + scanline2 = raster.getDataElements(0, image.getHeight() - i - 1, image.getWidth(), 1, scanline2); + raster.setDataElements(0, i, image.getWidth(), 1, scanline2); + raster.setDataElements(0, image.getHeight() - i - 1, image.getWidth(), 1, scanline1); + } + } //---------------------------------------------------------------------- // SPI support @@ -556,11 +704,21 @@ public class TextureIO { textureProviders.add(0, provider); } + /** Adds a TextureWriter to support writing of a new file + format. */ + public static void addTextureWriter(TextureWriter writer) { + // Must always add at the front so the ImageIO writer is last, + // so we don't accidentally use it instead of a user's possibly + // more optimal writer + textureWriters.add(0, writer); + } + //---------------------------------------------------------------------- // Internals only below this point // private static List/*<TextureProvider>*/ textureProviders = new ArrayList/*<TextureProvider>*/(); + private static List/*<TextureWriter>*/ textureWriters = new ArrayList/*<TextureWriter>*/(); static { // ImageIO provider, the fall-back, must be the first one added @@ -570,6 +728,14 @@ public class TextureIO { addTextureProvider(new DDSTextureProvider()); addTextureProvider(new SGITextureProvider()); addTextureProvider(new TGATextureProvider()); + + // ImageIO writer, the fall-back, must be the first one added + textureWriters.add(new IIOTextureWriter()); + + // Other special-case writers + addTextureWriter(new DDSTextureWriter()); + addTextureWriter(new SGITextureWriter()); + addTextureWriter(new TGATextureWriter()); } // Implementation methods @@ -595,7 +761,8 @@ public class TextureIO { return data; } } - return null; + + throw new IOException("No suitable reader for given file"); } private static TextureData newTextureDataImpl(InputStream stream, @@ -626,7 +793,7 @@ public class TextureIO { } } - return null; + throw new IOException("No suitable reader for given stream"); } private static TextureData newTextureDataImpl(URL url, @@ -652,7 +819,7 @@ public class TextureIO { } } - return null; + throw new IOException("No suitable reader for given URL"); } private static TextureData newTextureDataImpl(BufferedImage image, @@ -713,12 +880,11 @@ public class TextureIO { String fileSuffix) throws IOException { if (DDS.equals(fileSuffix) || DDS.equals(getFileSuffix(file))) { - final DDSReader reader = new DDSReader(); - reader.loadFile(file); - DDSReader.ImageInfo info = reader.getMipMap(0); + final DDSImage image = DDSImage.read(file); + DDSImage.ImageInfo info = image.getMipMap(0); if (pixelFormat == 0) { - switch (reader.getPixelFormat()) { - case DDSReader.D3DFMT_R8G8B8: + switch (image.getPixelFormat()) { + case DDSImage.D3DFMT_R8G8B8: pixelFormat = GL.GL_RGB; break; default: @@ -728,23 +894,23 @@ public class TextureIO { } if (info.isCompressed()) { switch (info.getCompressionFormat()) { - case DDSReader.D3DFMT_DXT1: + case DDSImage.D3DFMT_DXT1: internalFormat = GL.GL_COMPRESSED_RGB_S3TC_DXT1_EXT; break; - case DDSReader.D3DFMT_DXT3: + case DDSImage.D3DFMT_DXT3: internalFormat = GL.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; break; - case DDSReader.D3DFMT_DXT5: + case DDSImage.D3DFMT_DXT5: internalFormat = GL.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break; default: throw new RuntimeException("Unsupported DDS compression format \"" + - DDSReader.getCompressionFormatName(info.getCompressionFormat()) + "\""); + DDSImage.getCompressionFormatName(info.getCompressionFormat()) + "\""); } } if (internalFormat == 0) { - switch (reader.getPixelFormat()) { - case DDSReader.D3DFMT_R8G8B8: + switch (image.getPixelFormat()) { + case DDSImage.D3DFMT_R8G8B8: pixelFormat = GL.GL_RGB; break; default: @@ -754,14 +920,14 @@ public class TextureIO { } TextureData.Flusher flusher = new TextureData.Flusher() { public void flush() { - reader.close(); + image.close(); } }; TextureData data; - if (mipmap && reader.getNumMipMaps() > 0) { - Buffer[] mipmapData = new Buffer[reader.getNumMipMaps()]; - for (int i = 0; i < reader.getNumMipMaps(); i++) { - mipmapData[i] = reader.getMipMap(i).getData(); + if (mipmap && image.getNumMipMaps() > 0) { + Buffer[] mipmapData = new Buffer[image.getNumMipMaps()]; + for (int i = 0; i < image.getNumMipMaps(); i++) { + mipmapData[i] = image.getMipMap(i).getData(); } data = new TextureData(internalFormat, info.getWidth(), @@ -909,7 +1075,7 @@ public class TextureIO { mipmap, false, false, - ByteBuffer.wrap(image.getData()), + image.getData(), null); } @@ -918,9 +1084,229 @@ public class TextureIO { } //---------------------------------------------------------------------- + // ImageIO texture writer + // + static class IIOTextureWriter implements TextureWriter { + public boolean write(File file, + TextureData data) throws IOException { + int pixelFormat = data.getPixelFormat(); + int pixelType = data.getPixelType(); + if ((pixelFormat == GL.GL_RGB || + pixelFormat == GL.GL_RGBA) && + (pixelType == GL.GL_BYTE || + pixelType == GL.GL_UNSIGNED_BYTE)) { + // Convert TextureData to appropriate BufferedImage + // FIXME: almost certainly not obeying correct pixel order + BufferedImage image = new BufferedImage(data.getWidth(), data.getHeight(), + (pixelFormat == GL.GL_RGB) ? + BufferedImage.TYPE_3BYTE_BGR : + BufferedImage.TYPE_4BYTE_ABGR); + byte[] imageData = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + ByteBuffer buf = (ByteBuffer) data.getBuffer(); + if (buf == null) { + buf = (ByteBuffer) data.getMipmapData()[0]; + } + buf.rewind(); + buf.get(imageData); + buf.rewind(); + + // Swizzle image components to be correct + if (pixelFormat == GL.GL_RGB) { + for (int i = 0; i < imageData.length; i += 3) { + byte red = imageData[i + 0]; + byte blue = imageData[i + 2]; + imageData[i + 0] = blue; + imageData[i + 2] = red; + } + } else { + for (int i = 0; i < imageData.length; i += 4) { + byte red = imageData[i + 0]; + byte green = imageData[i + 1]; + byte blue = imageData[i + 2]; + byte alpha = imageData[i + 3]; + imageData[i + 0] = alpha; + imageData[i + 1] = blue; + imageData[i + 2] = green; + imageData[i + 3] = red; + } + } + + // Flip image vertically for the user's convenience + flipImageVertically(image); + + // Happened to notice that writing RGBA images to JPEGS is broken + if (JPG.equals(getFileSuffix(file)) && + image.getType() == BufferedImage.TYPE_4BYTE_ABGR) { + BufferedImage tmpImage = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage.TYPE_3BYTE_BGR); + Graphics g = tmpImage.getGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + image = tmpImage; + } + + return ImageIO.write(image, getFileSuffix(file), file); + } + + throw new IOException("ImageIO writer doesn't support this pixel format / type (only GL_RGB/A + bytes)"); + } + } + + //---------------------------------------------------------------------- + // DDS texture writer + // + static class DDSTextureWriter implements TextureWriter { + public boolean write(File file, + TextureData data) throws IOException { + if (DDS.equals(getFileSuffix(file))) { + // See whether the DDS writer can handle this TextureData + int pixelFormat = data.getPixelFormat(); + int pixelType = data.getPixelType(); + if (pixelType != GL.GL_BYTE && + pixelType != GL.GL_UNSIGNED_BYTE) { + throw new IOException("DDS writer only supports byte / unsigned byte textures"); + } + + int d3dFormat = 0; + // FIXME: some of these are probably not completely correct and would require swizzling + switch (pixelFormat) { + case GL.GL_RGB: d3dFormat = DDSImage.D3DFMT_R8G8B8; break; + case GL.GL_RGBA: d3dFormat = DDSImage.D3DFMT_A8R8G8B8; break; + case GL.GL_COMPRESSED_RGB_S3TC_DXT1_EXT: d3dFormat = DDSImage.D3DFMT_DXT1; break; + case GL.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: throw new IOException("RGBA DXT1 not yet supported"); + case GL.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: d3dFormat = DDSImage.D3DFMT_DXT3; break; + case GL.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: d3dFormat = DDSImage.D3DFMT_DXT5; break; + default: throw new IOException("Unsupported pixel format 0x" + Integer.toHexString(pixelFormat) + " by DDS writer"); + } + + ByteBuffer[] mipmaps = null; + if (data.getMipmapData() != null) { + mipmaps = new ByteBuffer[data.getMipmapData().length]; + for (int i = 0; i < mipmaps.length; i++) { + mipmaps[i] = (ByteBuffer) data.getMipmapData()[i]; + } + } else { + mipmaps = new ByteBuffer[] { (ByteBuffer) data.getBuffer() }; + } + + DDSImage image = DDSImage.createFromData(d3dFormat, + data.getWidth(), + data.getHeight(), + mipmaps); + image.write(file); + return true; + } + + return false; + } + } + + //---------------------------------------------------------------------- + // SGI (rgb) texture writer + // + static class SGITextureWriter implements TextureWriter { + public boolean write(File file, + TextureData data) throws IOException { + String fileSuffix = getFileSuffix(file); + if (SGI.equals(fileSuffix) || + SGI_RGB.equals(fileSuffix)) { + // See whether the SGI writer can handle this TextureData + int pixelFormat = data.getPixelFormat(); + int pixelType = data.getPixelType(); + if ((pixelFormat == GL.GL_RGB || + pixelFormat == GL.GL_RGBA) && + (pixelType == GL.GL_BYTE || + pixelType == GL.GL_UNSIGNED_BYTE)) { + ByteBuffer buf = ((data.getBuffer() != null) ? + (ByteBuffer) data.getBuffer() : + (ByteBuffer) data.getMipmapData()[0]); + byte[] bytes; + if (buf.hasArray()) { + bytes = buf.array(); + } else { + buf.rewind(); + bytes = new byte[buf.remaining()]; + buf.get(bytes); + buf.rewind(); + } + + SGIImage image = SGIImage.createFromData(data.getWidth(), + data.getHeight(), + (pixelFormat == GL.GL_RGBA), + bytes); + image.write(file, false); + return true; + } + + throw new IOException("SGI writer doesn't support this pixel format / type (only GL_RGB/A + bytes)"); + } + + return false; + } + } + + //---------------------------------------------------------------------- + // TGA (Targa) texture writer + + static class TGATextureWriter implements TextureWriter { + public boolean write(File file, + TextureData data) throws IOException { + if (TGA.equals(getFileSuffix(file))) { + // See whether the TGA writer can handle this TextureData + int pixelFormat = data.getPixelFormat(); + int pixelType = data.getPixelType(); + if ((pixelFormat == GL.GL_RGB || + pixelFormat == GL.GL_RGBA) && + (pixelType == GL.GL_BYTE || + pixelType == GL.GL_UNSIGNED_BYTE)) { + ByteBuffer buf = ((data.getBuffer() != null) ? + (ByteBuffer) data.getBuffer() : + (ByteBuffer) data.getMipmapData()[0]); + // Must reverse order of red and blue channels to get correct results + int skip = ((pixelFormat == GL.GL_RGB) ? 3 : 4); + for (int i = 0; i < buf.remaining(); i += skip) { + byte red = buf.get(i + 0); + byte blue = buf.get(i + 2); + buf.put(i + 0, blue); + buf.put(i + 2, red); + } + + TGAImage image = TGAImage.createFromData(data.getWidth(), + data.getHeight(), + (pixelFormat == GL.GL_RGBA), + false, + ((data.getBuffer() != null) ? + (ByteBuffer) data.getBuffer() : + (ByteBuffer) data.getMipmapData()[0])); + image.write(file); + return true; + } + + throw new IOException("TGA writer doesn't support this pixel format / type (only GL_RGB/A + bytes)"); + } + + return false; + } + } + + //---------------------------------------------------------------------- // Helper routines // + private static int glGetInteger(int pname) { + int[] tmp = new int[1]; + GL gl = GLU.getCurrentGL(); + gl.glGetIntegerv(pname, tmp, 0); + return tmp[0]; + } + + private static int glGetTexLevelParameteri(int target, int level, int pname) { + int[] tmp = new int[1]; + GL gl = GLU.getCurrentGL(); + gl.glGetTexLevelParameteriv(target, 0, pname, tmp, 0); + return tmp[0]; + } + private static String toLowerCase(String arg) { if (arg == null) { return null; diff --git a/src/classes/com/sun/opengl/utils/TextureWriter.java b/src/classes/com/sun/opengl/utils/TextureWriter.java new file mode 100755 index 000000000..38b976b9d --- /dev/null +++ b/src/classes/com/sun/opengl/utils/TextureWriter.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + * + * Sun gratefully acknowledges that this software was originally authored + * and developed by Kenneth Bradley Russell and Christopher John Kline. + */ + +package com.sun.opengl.utils; + +import java.io.*; + +/** Plug-in interface to TextureIO to support writing OpenGL textures + to new file formats. */ + +public interface TextureWriter { + /** Writes the given TextureData to the passed file. Returns true if + this TextureWriter successfully handled the writing of the file, + otherwise false. May throw IOException if either this writer did + not support certain parameters of the TextureData or if an I/O + error occurred. */ + public boolean write(File file, + TextureData data) throws IOException; +} |