/* * 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.scene.plugins.blender.file; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.logging.Logger; import java.util.zip.GZIPInputStream; /** * An input stream with random access to data. * @author Marcin Roguski */ public class BlenderInputStream extends InputStream { private static final Logger LOGGER = Logger.getLogger(BlenderInputStream.class.getName()); /** The default size of the blender buffer. */ private static final int DEFAULT_BUFFER_SIZE = 1048576; // 1MB /** * Size of a pointer; all pointers in the file are stored in this format. '_' means 4 bytes and '-' means 8 bytes. */ private int pointerSize; /** * Type of byte ordering used; 'v' means little endian and 'V' means big endian. */ private char endianess; /** Version of Blender the file was created in; '248' means version 2.48. */ private String versionNumber; /** The buffer we store the read data to. */ protected byte[] cachedBuffer; /** The total size of the stored data. */ protected int size; /** The current position of the read cursor. */ protected int position; /** * Constructor. The input stream is stored and used to read data. * @param inputStream * the stream we read data from * @throws BlenderFileException * this exception is thrown if the file header has some invalid data */ public BlenderInputStream(InputStream inputStream) throws BlenderFileException { // the size value will canche while reading the file; the available() method cannot be counted on try { size = inputStream.available(); } catch (IOException e) { size = 0; } if (size <= 0) { size = BlenderInputStream.DEFAULT_BUFFER_SIZE; } // buffered input stream is used here for much faster file reading BufferedInputStream bufferedInputStream; if (inputStream instanceof BufferedInputStream) { bufferedInputStream = (BufferedInputStream) inputStream; } else { bufferedInputStream = new BufferedInputStream(inputStream); } try { this.readStreamToCache(bufferedInputStream); } catch (IOException e) { throw new BlenderFileException("Problems occured while caching the file!", e); } finally { try { inputStream.close(); } catch (IOException e) { LOGGER.warning("Unable to close stream with blender file."); } } try { this.readFileHeader(); } catch (BlenderFileException e) {// the file might be packed, don't panic, try one more time ;) this.decompressFile(); position = 0; this.readFileHeader(); } } /** * This method reads the whole stream into a buffer. * @param inputStream * the stream to read the file data from * @throws IOException * an exception is thrown when data read from the stream is invalid or there are problems with i/o * operations */ private void readStreamToCache(InputStream inputStream) throws IOException { int data = inputStream.read(); cachedBuffer = new byte[size]; size = 0;// this will count the actual size while (data != -1) { if (size >= cachedBuffer.length) {// widen the cached array byte[] newBuffer = new byte[cachedBuffer.length + (cachedBuffer.length >> 1)]; System.arraycopy(cachedBuffer, 0, newBuffer, 0, cachedBuffer.length); cachedBuffer = newBuffer; } cachedBuffer[size++] = (byte) data; data = inputStream.read(); } } /** * This method is used when the blender file is gzipped. It decompresses the data and stores it back into the * cachedBuffer field. */ private void decompressFile() { GZIPInputStream gis = null; try { gis = new GZIPInputStream(new ByteArrayInputStream(cachedBuffer)); this.readStreamToCache(gis); } catch (IOException e) { throw new IllegalStateException("IO errors occured where they should NOT! " + "The data is already buffered at this point!", e); } finally { try { if (gis != null) { gis.close(); } } catch (IOException e) { LOGGER.warning(e.getMessage()); } } } /** * This method loads the header from the given stream during instance creation. * @param inputStream * the stream we read the header from * @throws BlenderFileException * this exception is thrown if the file header has some invalid data */ private void readFileHeader() throws BlenderFileException { byte[] identifier = new byte[7]; int bytesRead = this.readBytes(identifier); if (bytesRead != 7) { throw new BlenderFileException("Error reading header identifier. Only " + bytesRead + " bytes read and there should be 7!"); } String strIdentifier = new String(identifier); if (!"BLENDER".equals(strIdentifier)) { throw new BlenderFileException("Wrong file identifier: " + strIdentifier + "! Should be 'BLENDER'!"); } char pointerSizeSign = (char) this.readByte(); if (pointerSizeSign == '-') { pointerSize = 8; } else if (pointerSizeSign == '_') { pointerSize = 4; } else { throw new BlenderFileException("Invalid pointer size character! Should be '_' or '-' and there is: " + pointerSizeSign); } endianess = (char) this.readByte(); if (endianess != 'v' && endianess != 'V') { throw new BlenderFileException("Unknown endianess value! 'v' or 'V' expected and found: " + endianess); } byte[] versionNumber = new byte[3]; bytesRead = this.readBytes(versionNumber); if (bytesRead != 3) { throw new BlenderFileException("Error reading version numberr. Only " + bytesRead + " bytes read and there should be 3!"); } this.versionNumber = new String(versionNumber); } @Override public int read() throws IOException { return this.readByte(); } /** * This method reads 1 byte from the stream. * It works just in the way the read method does. * It just not throw an exception because at this moment the whole file * is loaded into buffer, so no need for IOException to be thrown. * @return a byte from the stream (1 bytes read) */ public int readByte() { return cachedBuffer[position++] & 0xFF; } /** * This method reads a bytes number big enough to fill the table. * It does not throw exceptions so it is for internal use only. * @param bytes * an array to be filled with data * @return number of read bytes (a length of array actually) */ private int readBytes(byte[] bytes) { for (int i = 0; i < bytes.length; ++i) { bytes[i] = (byte) this.readByte(); } return bytes.length; } /** * This method reads 2-byte number from the stream. * @return a number from the stream (2 bytes read) */ public int readShort() { int part1 = this.readByte(); int part2 = this.readByte(); if (endianess == 'v') { return (part2 << 8) + part1; } else { return (part1 << 8) + part2; } } /** * This method reads 4-byte number from the stream. * @return a number from the stream (4 bytes read) */ public int readInt() { int part1 = this.readByte(); int part2 = this.readByte(); int part3 = this.readByte(); int part4 = this.readByte(); if (endianess == 'v') { return (part4 << 24) + (part3 << 16) + (part2 << 8) + part1; } else { return (part1 << 24) + (part2 << 16) + (part3 << 8) + part4; } } /** * This method reads 4-byte floating point number (float) from the stream. * @return a number from the stream (4 bytes read) */ public float readFloat() { int intValue = this.readInt(); return Float.intBitsToFloat(intValue); } /** * This method reads 8-byte number from the stream. * @return a number from the stream (8 bytes read) */ public long readLong() { long part1 = this.readInt(); long part2 = this.readInt(); long result = -1; if (endianess == 'v') { result = part2 << 32 | part1; } else { result = part1 << 32 | part2; } return result; } /** * This method reads 8-byte floating point number (double) from the stream. * @return a number from the stream (8 bytes read) */ public double readDouble() { long longValue = this.readLong(); return Double.longBitsToDouble(longValue); } /** * This method reads the pointer value. Depending on the pointer size defined in the header, the stream reads either * 4 or 8 bytes of data. * @return the pointer value */ public long readPointer() { if (pointerSize == 4) { return this.readInt(); } return this.readLong(); } /** * This method reads the string. It assumes the string is terminated with zero in the stream. * @return the string read from the stream */ public String readString() { StringBuilder stringBuilder = new StringBuilder(); int data = this.readByte(); while (data != 0) { stringBuilder.append((char) data); data = this.readByte(); } return stringBuilder.toString(); } /** * This method sets the current position of the read cursor. * @param position * the position of the read cursor */ public void setPosition(int position) { this.position = position; } /** * This method returns the position of the read cursor. * @return the position of the read cursor */ public int getPosition() { return position; } /** * This method returns the blender version number where the file was created. * @return blender version number */ public String getVersionNumber() { return versionNumber; } /** * This method returns the size of the pointer. * @return the size of the pointer */ public int getPointerSize() { return pointerSize; } /** * This method aligns cursor position forward to a given amount of bytes. * @param bytesAmount * the byte amount to which we aligh the cursor */ public void alignPosition(int bytesAmount) { if (bytesAmount <= 0) { throw new IllegalArgumentException("Alignment byte number shoulf be positivbe!"); } long move = position % bytesAmount; if (move > 0) { position += bytesAmount - move; } } @Override public void close() throws IOException { // this method is unimplemented because some loaders (ie. TGALoader) tend close the stream given from the outside // because the images can be stored directly in the blender file then this stream is properly positioned and given to the loader // to read the image file, that is why we do not want it to be closed before the reading is done // and anyway this stream is only a cached buffer, so it does not hold any open connection to anything } }