/*
* Copyright (C) 2007, 2009, 2010 IsmAvatar <IsmAvatar@gmail.com>
* Copyright (C) 2006, 2007 Clam <clamisgood@gmail.com>
*
* This file is part of LateralGM.
* LateralGM is free software and comes with ABSOLUTELY NO WARRANTY.
* See LICENSE for details.
*/
package org.lateralgm.file;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import javax.imageio.ImageIO;
import org.lateralgm.messages.Messages;
import org.lateralgm.util.PropertyMap;
public class GmStreamDecoder extends StreamDecoder
{
protected int originalPos = -1;
protected InputStream originalStream;
private int[] table = null;
public GmStreamDecoder(InputStream in)
{
super(in);
}
public GmStreamDecoder(String path) throws FileNotFoundException
{
super(path);
}
public GmStreamDecoder(File f) throws FileNotFoundException
{
super(f);
}
public int read(byte b[]) throws IOException
{
return read(b,0,b.length);
}
public int read(byte b[], int off, int len) throws IOException
{
int total = 0;
while (true)
{
int n = in.read(b,off + total,len - total);
if (n <= 0)
{
if (total == 0) total = n;
break;
}
total += n;
if (total == len) break;
}
if (total != len)
{
String error = Messages.format("StreamDecoder.UNEXPECTED_EOF",getPosString()); //$NON-NLS-1$
throw new IOException(error);
}
if (table != null)
{
for (int i = 0; i < len; i++)
{
int t = b[off + i] & 0xFF;
int x = (table[t] - pos - i) & 0xFF;
b[off + i] = (byte) x;
}
}
pos += len;
return total;
}
public int read() throws IOException
{
int t = in.read();
if (t == -1)
{
String error = Messages.format("StreamDecoder.UNEXPECTED_EOF",getPosString()); //$NON-NLS-1$
throw new IOException(error);
}
if (table != null)
{
t = (table[t] - pos) & 0xFF;
}
pos++;
return t;
}
/**
* ISO-8859-1 was the fixed charset in earlier LGM versions, so those parts of the code which
* have not been updated to set the charset explicitly should continue to use it to avoid
* regressions.
*/
private Charset charset = Charset.forName("ISO-8859-1");
public Charset getCharset()
{
return charset;
}
public void setCharset(Charset charset)
{
this.charset = charset;
}
public String readStr() throws IOException
{
byte data[] = new byte[read4()];
read(data);
return new String(data,charset);
}
public String readStr1() throws IOException
{
byte data[] = new byte[read()];
read(data);
return new String(data,charset);
}
public boolean readBool() throws IOException
{
int val = read4();
if (val != 0 && val != 1)
{
String error = Messages.format("GmStreamDecoder.INVALID_BOOLEAN",val,getPosString()); //$NON-NLS-1$
throw new IOException(error);
}
return val == 0 ? false : true;
}
public <P extends Enum<P>>void read4(PropertyMap<P> map, P...keys) throws IOException
{
for (P key : keys)
map.put(key,read4());
}
public <P extends Enum<P>>void readStr(PropertyMap<P> map, P...keys) throws IOException
{
for (P key : keys)
map.put(key,readStr());
}
public <P extends Enum<P>>void readBool(PropertyMap<P> map, P...keys) throws IOException
{
for (P key : keys)
map.put(key,readBool());
}
public <P extends Enum<P>>void readD(PropertyMap<P> map, P...keys) throws IOException
{
for (P key : keys)
map.put(key,readD());
}
public byte[] decompress(int length) throws IOException,DataFormatException
{
return decompress(length,length);
}
public byte[] decompress(int length, int initialCapacity) throws IOException,DataFormatException
{
Inflater decompresser = new Inflater();
byte[] compressedData = new byte[length];
read(compressedData,0,length);
decompresser.setInput(compressedData);
byte[] result = new byte[131072];
ByteArrayOutputStream baos = new ByteArrayOutputStream(initialCapacity);
while (!decompresser.finished())
{
int len = decompresser.inflate(result);
baos.write(result,0,len);
}
decompresser.end();
return baos.toByteArray();
}
public void beginInflate() throws IOException
{
int limit = read4();
originalStream = in;
in = new LimitedInflaterInputStream(originalStream,limit);
originalPos = pos;
pos = 0;
}
/**
* Safely finishes this stream if it's an inflater, otherwise this call does nothing.
* This places the file reader after the end of the compressed data in the underlying stream.
*/
public void endInflate() throws IOException
{
if (originalStream != null)
{
LimitedInflaterInputStream inf = (LimitedInflaterInputStream) in;
inf.finish();
pos = originalPos + (int) inf.getLimit();
originalPos = -1;
in = originalStream;
originalStream = null;
}
}
public BufferedImage readZlibImage(int width, int height) throws IOException,DataFormatException
{
int length = read4();
int estimate = height * width * 4 + 100; //100 for generous header
return ImageIO.read(new ByteArrayInputStream(decompress(length,estimate)));
}
public BufferedImage readZlibImage() throws IOException,DataFormatException
{
return readZlibImage(0,0);
}
public BufferedImage readBGRAImage(int w, int h) throws IOException
{
//meta and setup
final int datatype = DataBuffer.TYPE_BYTE, trans = Transparency.TRANSLUCENT;
final int[] bitSizes = { 8,8,8,8 }, bitOrder = { 2,1,0,3 }; //2103 = RGBA ordering
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ColorModel cm = new ComponentColorModel(cs,bitSizes,true,false,trans,datatype);
WritableRaster raster = Raster.createInterleavedRaster(datatype,w,h,w * 4,4,bitOrder,null);
//populate raster
byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
int s = read4();
if (s != data.length)
throw new IOException(Messages.format(
"GmStreamDecoder.IMAGE_SIZE_MISMATCH",s,data.length,getPosString())); //$NON-NLS-1$
read(data);
//combine and return
return new BufferedImage(cm,raster,false,null);
}
/**
* Convenience method to retrieve whether the given bit is masked in bits,
* That is, if given flag is set.
* E.g.: to find out if the 3rd flag from right is set in 00011*0*10, use mask(26,4);
* @param bits - A cluster of flags/bits
* @param bit - The desired (and already shifted) bit or bits to mask
* @return Whether bit is masked in bits
*/
public static boolean mask(int bits, int bit)
{
return (bits & bit) == bit;
}
/**
* GM7 Notice: since the first useful byte after the seed isn't encrypted,
* you may wish to delay setting the seed until that byte is retrieved,
* as implementing such functionality into these lower-level routines would add overhead
*/
public void setSeed(int s)
{
if (s >= 0)
table = makeDecodeTable(s);
else
table = null;
}
protected static int[] makeDecodeTable(int seed)
{
int[] encTable = GmStreamEncoder.makeEncodeTable(seed);
return makeDecodeTable(encTable);
}
protected static int[] makeDecodeTable(int[] encTable)
{
int[] table = new int[256];
for (int i = 1; i < 256; i++)
table[encTable[i]] = i;
return table;
}
/**
* If the stream is currently reading zlib data,
* this returns a string in the format:
* <code><file offset>[<decompressed data offset>]</code><br/>
* Otherwise just the file offset is returned.
*/
protected String getPosString()
{
if (originalPos != -1) return originalPos + "[" + pos + "]";
return Integer.toString(pos);
}
}