/* * Copyright (c) 2009-2012 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions 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 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.jme3.texture.plugins; import com.jme3.asset.AssetInfo; import com.jme3.asset.AssetLoader; import com.jme3.asset.TextureKey; import com.jme3.texture.Image; import com.jme3.texture.Image.Format; import com.jme3.texture.Texture; import com.jme3.texture.image.ColorSpace; import com.jme3.util.BufferUtils; import com.jme3.util.LittleEndien; import java.io.DataInput; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; /** * * <code>DDSLoader</code> is an image loader that reads in a DirectX DDS file. * Supports DXT1, DXT3, DXT5, RGB, RGBA, Grayscale, Alpha pixel formats. * 2D images, mipmapped 2D images, and cubemaps. * * @author Gareth Jenkins-Jones * @author Kirill Vainer * @version $Id: DDSLoader.java,v 2.0 2008/8/15 */ public class DDSLoader implements AssetLoader { private static final Logger logger = Logger.getLogger(DDSLoader.class.getName()); private static final boolean forceRGBA = false; private static final int DDSD_MANDATORY = 0x1007; private static final int DDSD_MANDATORY_DX10 = 0x6; private static final int DDSD_MIPMAPCOUNT = 0x20000; private static final int DDSD_LINEARSIZE = 0x80000; private static final int DDSD_DEPTH = 0x800000; private static final int DDPF_ALPHAPIXELS = 0x1; private static final int DDPF_FOURCC = 0x4; private static final int DDPF_RGB = 0x40; // used by compressonator to mark grayscale images, red channel mask is used for data and bitcount is 8 private static final int DDPF_GRAYSCALE = 0x20000; // used by compressonator to mark alpha images, alpha channel mask is used for data and bitcount is 8 private static final int DDPF_ALPHA = 0x2; // used by NVTextureTools to mark normal images. private static final int DDPF_NORMAL = 0x80000000; private static final int SWIZZLE_xGxR = 0x78477852; private static final int DDSCAPS_COMPLEX = 0x8; private static final int DDSCAPS_TEXTURE = 0x1000; private static final int DDSCAPS_MIPMAP = 0x400000; private static final int DDSCAPS2_CUBEMAP = 0x200; private static final int DDSCAPS2_VOLUME = 0x200000; private static final int PF_DXT1 = 0x31545844; private static final int PF_DXT3 = 0x33545844; private static final int PF_DXT5 = 0x35545844; private static final int PF_ATI1 = 0x31495441; private static final int PF_ATI2 = 0x32495441; // 0x41544932; private static final int PF_DX10 = 0x30315844; // a DX10 format private static final int DX10DIM_BUFFER = 0x1, DX10DIM_TEXTURE1D = 0x2, DX10DIM_TEXTURE2D = 0x3, DX10DIM_TEXTURE3D = 0x4; private static final int DX10MISC_GENERATE_MIPS = 0x1, DX10MISC_TEXTURECUBE = 0x4; private static final double LOG2 = Math.log(2); private int width; private int height; private int depth; private int flags; private int pitchOrSize; private int mipMapCount; private int caps1; private int caps2; private boolean directx10; private boolean compressed; private boolean texture3D; private boolean grayscaleOrAlpha; private boolean normal; private Format pixelFormat; private int bpp; private int[] sizes; private int redMask, greenMask, blueMask, alphaMask; private DataInput in; public DDSLoader() { } public Object load(AssetInfo info) throws IOException { if (!(info.getKey() instanceof TextureKey)) { throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey"); } InputStream stream = null; try { stream = info.openStream(); in = new LittleEndien(stream); loadHeader(); if (texture3D) { ((TextureKey) info.getKey()).setTextureTypeHint(Texture.Type.ThreeDimensional); } else if (depth > 1) { ((TextureKey) info.getKey()).setTextureTypeHint(Texture.Type.CubeMap); } ArrayList<ByteBuffer> data = readData(((TextureKey) info.getKey()).isFlipY()); return new Image(pixelFormat, width, height, depth, data, sizes, ColorSpace.sRGB); } finally { if (stream != null){ stream.close(); } } } public Image load(InputStream stream) throws IOException { in = new LittleEndien(stream); loadHeader(); ArrayList<ByteBuffer> data = readData(false); return new Image(pixelFormat, width, height, depth, data, sizes, ColorSpace.sRGB); } private void loadDX10Header() throws IOException { int dxgiFormat = in.readInt(); if (dxgiFormat == 0) { pixelFormat = Format.ETC1; bpp = 4; } else { throw new IOException("Unsupported DX10 format: " + dxgiFormat); } compressed = true; int resDim = in.readInt(); if (resDim == DX10DIM_TEXTURE3D) { texture3D = true; } int miscFlag = in.readInt(); int arraySize = in.readInt(); if (is(miscFlag, DX10MISC_TEXTURECUBE)) { // mark texture as cube if (arraySize != 6) { throw new IOException("Cubemaps should consist of 6 images!"); } } in.skipBytes(4); // skip reserved value } /** * Reads the header (first 128 bytes) of a DDS File */ private void loadHeader() throws IOException { if (in.readInt() != 0x20534444 || in.readInt() != 124) { throw new IOException("Not a DDS file"); } flags = in.readInt(); if (!is(flags, DDSD_MANDATORY) && !is(flags, DDSD_MANDATORY_DX10)) { throw new IOException("Mandatory flags missing"); } height = in.readInt(); width = in.readInt(); pitchOrSize = in.readInt(); depth = in.readInt(); mipMapCount = in.readInt(); in.skipBytes(44); pixelFormat = null; directx10 = false; readPixelFormat(); caps1 = in.readInt(); caps2 = in.readInt(); in.skipBytes(12); texture3D = false; if (!directx10) { if (!is(caps1, DDSCAPS_TEXTURE)) { logger.warning("Texture is missing the DDSCAPS_TEXTURE-flag"); } if (depth <= 0) { depth = 1; } if (is(caps2, DDSCAPS2_CUBEMAP)) { depth = 6; // somewhat of a hack, force loading 6 textures if a cubemap } if (is(caps2, DDSCAPS2_VOLUME)) { texture3D = true; } } int expectedMipmaps = 1 + (int) Math.ceil(Math.log(Math.max(height, width)) / LOG2); if (is(caps1, DDSCAPS_MIPMAP)) { if (!is(flags, DDSD_MIPMAPCOUNT)) { mipMapCount = expectedMipmaps; } else if (mipMapCount != expectedMipmaps) { // changed to warning- images often do not have the required amount, // or specify that they have mipmaps but include only the top level.. logger.log(Level.WARNING, "Got {0} mipmaps, expected {1}", new Object[]{mipMapCount, expectedMipmaps}); } } else { mipMapCount = 1; } if (directx10) { loadDX10Header(); } loadSizes(); } /** * Reads the PixelFormat structure in a DDS file */ private void readPixelFormat() throws IOException { int pfSize = in.readInt(); if (pfSize != 32) { throw new IOException("Pixel format size is " + pfSize + ", not 32"); } int pfFlags = in.readInt(); normal = is(pfFlags, DDPF_NORMAL); if (is(pfFlags, DDPF_FOURCC)) { compressed = true; int fourcc = in.readInt(); int swizzle = in.readInt(); in.skipBytes(16); switch (fourcc) { case PF_DXT1: bpp = 4; if (is(pfFlags, DDPF_ALPHAPIXELS)) { pixelFormat = Image.Format.DXT1A; } else { pixelFormat = Image.Format.DXT1; } break; case PF_DXT3: bpp = 8; pixelFormat = Image.Format.DXT3; break; case PF_DXT5: bpp = 8; pixelFormat = Image.Format.DXT5; if (swizzle == SWIZZLE_xGxR) { normal = true; } break; /* case PF_ATI1: bpp = 4; pixelFormat = Image.Format.LTC; break; case PF_ATI2: bpp = 8; pixelFormat = Image.Format.LATC; break; */ case PF_DX10: compressed = false; directx10 = true; // exit here, the rest of the structure is not valid // the real format will be available in the DX10 header return; case 113: compressed = false; bpp = 64; pixelFormat = Image.Format.RGBA16F; break; default: throw new IOException("Unknown fourcc: " + string(fourcc) + ", " + Integer.toHexString(fourcc)); } int size = ((width + 3) / 4) * ((height + 3) / 4) * bpp * 2; if (is(flags, DDSD_LINEARSIZE)) { if (pitchOrSize == 0) { logger.warning("Must use linear size with fourcc"); pitchOrSize = size; } else if (pitchOrSize != size) { logger.log(Level.WARNING, "Expected size = {0}, real = {1}", new Object[]{size, pitchOrSize}); } } else { pitchOrSize = size; } } else { compressed = false; // skip fourCC in.readInt(); bpp = in.readInt(); redMask = in.readInt(); greenMask = in.readInt(); blueMask = in.readInt(); alphaMask = in.readInt(); if (is(pfFlags, DDPF_RGB)) { if (is(pfFlags, DDPF_ALPHAPIXELS)) { if (bpp == 16) { pixelFormat = Format.RGB5A1; } else { pixelFormat = Format.RGBA8; } } else { if (bpp == 16) { pixelFormat = Format.RGB565; } else { pixelFormat = Format.RGB8; } } } else if (is(pfFlags, DDPF_GRAYSCALE) && is(pfFlags, DDPF_ALPHAPIXELS)) { switch (bpp) { case 16: pixelFormat = Format.Luminance8Alpha8; break; default: throw new IOException("Unsupported GrayscaleAlpha BPP: " + bpp); } grayscaleOrAlpha = true; } else if (is(pfFlags, DDPF_GRAYSCALE)) { switch (bpp) { case 8: pixelFormat = Format.Luminance8; break; default: throw new IOException("Unsupported Grayscale BPP: " + bpp); } grayscaleOrAlpha = true; } else if (is(pfFlags, DDPF_ALPHA)) { switch (bpp) { case 8: pixelFormat = Format.Alpha8; break; default: throw new IOException("Unsupported Alpha BPP: " + bpp); } grayscaleOrAlpha = true; } else { throw new IOException("Unknown PixelFormat in DDS file"); } int size = (bpp / 8 * width); if (is(flags, DDSD_LINEARSIZE)) { if (pitchOrSize == 0) { logger.warning("Linear size said to contain valid value but does not"); pitchOrSize = size; } else if (pitchOrSize != size) { logger.log(Level.WARNING, "Expected size = {0}, real = {1}", new Object[]{size, pitchOrSize}); } } else { pitchOrSize = size; } } } /** * Computes the sizes of each mipmap level in bytes, and stores it in sizes_[]. */ private void loadSizes() { int mipWidth = width; int mipHeight = height; sizes = new int[mipMapCount]; int outBpp = pixelFormat.getBitsPerPixel(); for (int i = 0; i < mipMapCount; i++) { int size; if (compressed) { size = ((mipWidth + 3) / 4) * ((mipHeight + 3) / 4) * outBpp * 2; } else { size = mipWidth * mipHeight * outBpp / 8; } sizes[i] = ((size + 3) / 4) * 4; mipWidth = Math.max(mipWidth / 2, 1); mipHeight = Math.max(mipHeight / 2, 1); } } /** * Flips the given image data on the Y axis. * @param data Data array containing image data (without mipmaps) * @param scanlineSize Size of a single scanline = width * bytesPerPixel * @param height Height of the image in pixels * @return The new data flipped by the Y axis */ public byte[] flipData(byte[] data, int scanlineSize, int height) { byte[] newData = new byte[data.length]; for (int y = 0; y < height; y++) { System.arraycopy(data, y * scanlineSize, newData, (height - y - 1) * scanlineSize, scanlineSize); } return newData; } /** * Reads a grayscale image with mipmaps from the InputStream * @param flip Flip the loaded image by Y axis * @param totalSize Total size of the image in bytes including the mipmaps * @return A ByteBuffer containing the grayscale image data with mips. * @throws java.io.IOException If an error occured while reading from InputStream */ public ByteBuffer readGrayscale2D(boolean flip, int totalSize) throws IOException { ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize); if (bpp == 8) { logger.finest("Source image format: R8"); } assert bpp == pixelFormat.getBitsPerPixel(); int mipWidth = width; int mipHeight = height; for (int mip = 0; mip < mipMapCount; mip++) { byte[] data = new byte[sizes[mip]]; in.readFully(data); if (flip) { data = flipData(data, mipWidth * bpp / 8, mipHeight); } buffer.put(data); mipWidth = Math.max(mipWidth / 2, 1); mipHeight = Math.max(mipHeight / 2, 1); } return buffer; } /** * Reads an uncompressed RGB or RGBA image. * * @param flip Flip the image on the Y axis * @param totalSize Size of the image in bytes including mipmaps * @return ByteBuffer containing image data with mipmaps in the format specified by pixelFormat_ * @throws java.io.IOException If an error occured while reading from InputStream */ public ByteBuffer readRGB2D(boolean flip, int totalSize) throws IOException { int redCount = count(redMask), blueCount = count(blueMask), greenCount = count(greenMask), alphaCount = count(alphaMask); if (redMask == 0x00FF0000 && greenMask == 0x0000FF00 && blueMask == 0x000000FF) { if (alphaMask == 0xFF000000 && bpp == 32) { logger.finest("Data source format: BGRA8"); } else if (bpp == 24) { logger.finest("Data source format: BGR8"); } } int sourcebytesPP = bpp / 8; int targetBytesPP = pixelFormat.getBitsPerPixel() / 8; ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize); int mipWidth = width; int mipHeight = height; int offset = 0; byte[] b = new byte[sourcebytesPP]; for (int mip = 0; mip < mipMapCount; mip++) { for (int y = 0; y < mipHeight; y++) { for (int x = 0; x < mipWidth; x++) { in.readFully(b); int i = byte2int(b); byte red = (byte) (((i & redMask) >> redCount)); byte green = (byte) (((i & greenMask) >> greenCount)); byte blue = (byte) (((i & blueMask) >> blueCount)); byte alpha = (byte) (((i & alphaMask) >> alphaCount)); if (flip) { dataBuffer.position(offset + ((mipHeight - y - 1) * mipWidth + x) * targetBytesPP); } //else // dataBuffer.position(offset + (y * width + x) * targetBytesPP); if (alphaMask == 0) { dataBuffer.put(red).put(green).put(blue); } else { dataBuffer.put(red).put(green).put(blue).put(alpha); } } } offset += mipWidth * mipHeight * targetBytesPP; mipWidth = Math.max(mipWidth / 2, 1); mipHeight = Math.max(mipHeight / 2, 1); } return dataBuffer; } /** * Reads a DXT compressed image from the InputStream * * @param totalSize Total size of the image in bytes, including mipmaps * @return ByteBuffer containing compressed DXT image in the format specified by pixelFormat_ * @throws java.io.IOException If an error occured while reading from InputStream */ public ByteBuffer readDXT2D(boolean flip, int totalSize) throws IOException { logger.finest("Source image format: DXT"); ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize); int mipWidth = width; int mipHeight = height; for (int mip = 0; mip < mipMapCount; mip++) { if (flip) { byte[] data = new byte[sizes[mip]]; in.readFully(data); ByteBuffer wrapped = ByteBuffer.wrap(data); wrapped.rewind(); ByteBuffer flipped = DXTFlipper.flipDXT(wrapped, mipWidth, mipHeight, pixelFormat); buffer.put(flipped); } else { byte[] data = new byte[sizes[mip]]; in.readFully(data); buffer.put(data); } mipWidth = Math.max(mipWidth / 2, 1); mipHeight = Math.max(mipHeight / 2, 1); } buffer.rewind(); return buffer; } /** * Reads a grayscale image with mipmaps from the InputStream * @param flip Flip the loaded image by Y axis * @param totalSize Total size of the image in bytes including the mipmaps * @return A ByteBuffer containing the grayscale image data with mips. * @throws java.io.IOException If an error occured while reading from InputStream */ public ByteBuffer readGrayscale3D(boolean flip, int totalSize) throws IOException { ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize * depth); if (bpp == 8) { logger.finest("Source image format: R8"); } assert bpp == pixelFormat.getBitsPerPixel(); for (int i = 0; i < depth; i++) { int mipWidth = width; int mipHeight = height; for (int mip = 0; mip < mipMapCount; mip++) { byte[] data = new byte[sizes[mip]]; in.readFully(data); if (flip) { data = flipData(data, mipWidth * bpp / 8, mipHeight); } buffer.put(data); mipWidth = Math.max(mipWidth / 2, 1); mipHeight = Math.max(mipHeight / 2, 1); } } buffer.rewind(); return buffer; } /** * Reads an uncompressed RGB or RGBA image. * * @param flip Flip the image on the Y axis * @param totalSize Size of the image in bytes including mipmaps * @return ByteBuffer containing image data with mipmaps in the format specified by pixelFormat_ * @throws java.io.IOException If an error occured while reading from InputStream */ public ByteBuffer readRGB3D(boolean flip, int totalSize) throws IOException { int redCount = count(redMask), blueCount = count(blueMask), greenCount = count(greenMask), alphaCount = count(alphaMask); if (redMask == 0x00FF0000 && greenMask == 0x0000FF00 && blueMask == 0x000000FF) { if (alphaMask == 0xFF000000 && bpp == 32) { logger.finest("Data source format: BGRA8"); } else if (bpp == 24) { logger.finest("Data source format: BGR8"); } } int sourcebytesPP = bpp / 8; int targetBytesPP = pixelFormat.getBitsPerPixel() / 8; ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize * depth); for (int k = 0; k < depth; k++) { // ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize); int mipWidth = width; int mipHeight = height; int offset = k * totalSize; byte[] b = new byte[sourcebytesPP]; for (int mip = 0; mip < mipMapCount; mip++) { for (int y = 0; y < mipHeight; y++) { for (int x = 0; x < mipWidth; x++) { in.readFully(b); int i = byte2int(b); byte red = (byte) (((i & redMask) >> redCount)); byte green = (byte) (((i & greenMask) >> greenCount)); byte blue = (byte) (((i & blueMask) >> blueCount)); byte alpha = (byte) (((i & alphaMask) >> alphaCount)); if (flip) { dataBuffer.position(offset + ((mipHeight - y - 1) * mipWidth + x) * targetBytesPP); } //else // dataBuffer.position(offset + (y * width + x) * targetBytesPP); if (alphaMask == 0) { dataBuffer.put(red).put(green).put(blue); } else { dataBuffer.put(red).put(green).put(blue).put(alpha); } } } offset += (mipWidth * mipHeight * targetBytesPP); mipWidth = Math.max(mipWidth / 2, 1); mipHeight = Math.max(mipHeight / 2, 1); } } dataBuffer.rewind(); return dataBuffer; } /** * Reads a DXT compressed image from the InputStream * * @param totalSize Total size of the image in bytes, including mipmaps * @return ByteBuffer containing compressed DXT image in the format specified by pixelFormat_ * @throws java.io.IOException If an error occured while reading from InputStream */ public ByteBuffer readDXT3D(boolean flip, int totalSize) throws IOException { logger.finest("Source image format: DXT"); ByteBuffer bufferAll = BufferUtils.createByteBuffer(totalSize * depth); for (int i = 0; i < depth; i++) { ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize); int mipWidth = width; int mipHeight = height; for (int mip = 0; mip < mipMapCount; mip++) { if (flip) { byte[] data = new byte[sizes[mip]]; in.readFully(data); ByteBuffer wrapped = ByteBuffer.wrap(data); wrapped.rewind(); ByteBuffer flipped = DXTFlipper.flipDXT(wrapped, mipWidth, mipHeight, pixelFormat); flipped.rewind(); buffer.put(flipped); } else { byte[] data = new byte[sizes[mip]]; in.readFully(data); buffer.put(data); } mipWidth = Math.max(mipWidth / 2, 1); mipHeight = Math.max(mipHeight / 2, 1); } buffer.rewind(); bufferAll.put(buffer); } return bufferAll; } /** * Reads the image data from the InputStream in the required format. * If the file contains a cubemap image, it is loaded as 6 ByteBuffers * (potentially containing mipmaps if they were specified), otherwise * a single ByteBuffer is returned for a 2D image. * * @param flip Flip the image data or not. * For cubemaps, each of the cubemap faces is flipped individually. * If the image is DXT compressed, no flipping is done. * @return An ArrayList containing a single ByteBuffer for a 2D image, or 6 ByteBuffers for a cubemap. * The cubemap ByteBuffer order is PositiveX, NegativeX, PositiveY, NegativeY, PositiveZ, NegativeZ. * * @throws java.io.IOException If an error occured while reading from the stream. */ public ArrayList<ByteBuffer> readData(boolean flip) throws IOException { int totalSize = 0; for (int i = 0; i < sizes.length; i++) { totalSize += sizes[i]; } ArrayList<ByteBuffer> allMaps = new ArrayList<ByteBuffer>(); if (depth > 1 && !texture3D) { for (int i = 0; i < depth; i++) { if (compressed) { allMaps.add(readDXT2D(flip, totalSize)); } else if (grayscaleOrAlpha) { allMaps.add(readGrayscale2D(flip, totalSize)); } else { allMaps.add(readRGB2D(flip, totalSize)); } } } else if (texture3D) { if (compressed) { allMaps.add(readDXT3D(flip, totalSize)); } else if (grayscaleOrAlpha) { allMaps.add(readGrayscale3D(flip, totalSize)); } else { allMaps.add(readRGB3D(flip, totalSize)); } } else { if (compressed) { allMaps.add(readDXT2D(flip, totalSize)); } else if (grayscaleOrAlpha) { allMaps.add(readGrayscale2D(flip, totalSize)); } else { allMaps.add(readRGB2D(flip, totalSize)); } } return allMaps; } /** * Checks if flags contains the specified mask */ private static boolean is(int flags, int mask) { return (flags & mask) == mask; } /** * Counts the amount of bits needed to shift till bitmask n is at zero * @param n Bitmask to test */ private static int count(int n) { if (n == 0) { return 0; } int i = 0; while ((n & 0x1) == 0) { n = n >> 1; i++; if (i > 32) { throw new RuntimeException(Integer.toHexString(n)); } } return i; } /** * Converts a 1 to 4 sized byte array to an integer */ private static int byte2int(byte[] b) { if (b.length == 1) { return b[0] & 0xFF; } else if (b.length == 2) { return (b[0] & 0xFF) | ((b[1] & 0xFF) << 8); } else if (b.length == 3) { return (b[0] & 0xFF) | ((b[1] & 0xFF) << 8) | ((b[2] & 0xFF) << 16); } else if (b.length == 4) { return (b[0] & 0xFF) | ((b[1] & 0xFF) << 8) | ((b[2] & 0xFF) << 16) | ((b[3] & 0xFF) << 24); } else { return 0; } } /** * Converts a int representing a FourCC into a String */ private static String string(int value) { StringBuilder buf = new StringBuilder(); buf.append((char) (value & 0xFF)); buf.append((char) ((value & 0xFF00) >> 8)); buf.append((char) ((value & 0xFF0000) >> 16)); buf.append((char) ((value & 0xFF00000) >> 24)); return buf.toString(); } }