package ar.com.hjg.pngj; import java.util.zip.DataFormatException; import java.util.zip.Inflater; /** * A set of IDAT-like chunks which, concatenated, form a zlib stream. *
* The inflated stream is intented to be read as a sequence of "rows", of which * the caller knows the lengths (not * necessary equal) and number. *
* Eg: For IDAT non-interlaced images, a row has bytesPerRow + 1 filter byte
* For interlaced images, the lengths are variable.
*
* This class can work in sync (polled) mode or async (callback) mode. But for * callback mode the method * processRowCallback() must be overriden *
* See {@link IdatSet}, which is mostly used and has a slightly simpler use.
* See DeflatedChunkSetTest
for example of use.
*/
public class DeflatedChunksSet {
protected byte[] row; // a "row" here means a raw (uncopressed filtered) part of the IDAT stream,
// normally a image row (or subimage row for interlaced) plus a filter byte
private int rowfilled; // effective/valid length of row
private int rowlen; // what amount of bytes is to be interpreted as a complete "row". can change
// (for interlaced)
private int rown; // only coincide with image row if non-interlaced - incremented by
// setNextRowSize()
/*
* States WAITING_FOR_INPUT ROW_READY WORK_DONE TERMINATED
*
* processBytes() is externally called, prohibited in READY (in DONE it's ignored)
*
* WARNING: inflater.finished() != DONE (not enough, not neccesary) DONE means that we have already uncompressed all
* the data of interest.
*
* In non-callback mode, prepareForNextRow() is also externally called, in
*
* Flow: - processBytes() calls inflateData() - inflateData() : if buffer is filled goes to READY else if !
* inf.finished goes to WAITING else if any data goes to READY (incomplete data to be read) else goes to DONE - in
* Callback mode, after going to READY, n=processCallback() is called and then prepareForNextRow(n) is called. - in
* Polled mode, prepareForNextRow(n) must be called from outside (after checking state=READY) - prepareForNextRow(n)
* goes to DONE if n==0 calls inflateData() again - end() goes to DONE
*/
private enum State {
WAITING_FOR_INPUT, // waiting for more input
ROW_READY, // ready for consumption (might be less than fully filled), ephemeral for CALLBACK
// mode
WORK_DONE, // all data of interest has been read, but we might accept still more trailing chunks
// (we'll ignore them)
TERMINATED; // we are done, and also won't accept more IDAT chunks
public boolean isDone() {
return this == WORK_DONE || this == TERMINATED;
} // the caller has already uncompressed all the data of interest or EOF
public boolean isTerminated() {
return this == TERMINATED;
} // we dont accept more chunks
}
State state = State.WAITING_FOR_INPUT; // never null
private Inflater inf;
private final boolean infOwn; // true if we own the inflater (we created it)
private DeflatedChunkReader curChunk;
private boolean callbackMode = true;
private long nBytesIn = 0; // count the total compressed bytes that have been fed
private long nBytesOut = 0; // count the total uncompressed bytes
int chunkNum = -1; // incremented at each new chunk start
int firstChunqSeqNum = -1; // expected seq num for first chunk. used only for fDAT (APNG)
/**
* All IDAT-like chunks that form a same DeflatedChunksSet should have the
* same id
*/
public final String chunkid;
/**
* @param initialRowLen
* Length in bytes of first "row" (see description)
* @param maxRowLen
* Max length in bytes of "rows"
* @param inflater
* Can be null. If not null, must be already reset (and it must
* be closed/released by caller!)
*/
public DeflatedChunksSet(final String chunkid, final int initialRowLen, final int maxRowLen, final Inflater inflater, final byte[] buffer) {
this.chunkid = chunkid;
rowlen = initialRowLen;
if (initialRowLen < 1 || maxRowLen < initialRowLen)
throw new PngjException("bad inital row len " + initialRowLen);
if (inflater != null) {
inf = inflater;
infOwn = false;
} else {
inf = new Inflater();
infOwn = true; // inflater is own, we will release on close()
}
row = buffer != null && buffer.length >= initialRowLen ? buffer : new byte[maxRowLen];
rown = -1;
state = State.WAITING_FOR_INPUT;
try {
prepareForNextRow(initialRowLen);
} catch (final RuntimeException e) {
close();
throw e;
}
}
public DeflatedChunksSet(final String chunkid, final int initialRowLen, final int maxRowLen) {
this(chunkid, initialRowLen, maxRowLen, null, null);
}
protected void appendNewChunk(final DeflatedChunkReader cr) {
// all chunks must have same id
if (!chunkid.equals(cr.getChunkRaw().id))
throw new PngjInputException("Bad chunk inside IdatSet, id:" + cr.getChunkRaw().id + ", expected:" + chunkid);
curChunk = cr;
chunkNum++;
if (firstChunqSeqNum >= 0)
cr.setSeqNumExpected(chunkNum + firstChunqSeqNum);
}
/**
* Feeds the inflater with the compressed bytes
*
* In poll mode, the caller should not call repeatedly this, without
* consuming first, checking
* isDataReadyForConsumer()
*
* @param buf
* @param off
* @param len
*/
protected void processBytes(final byte[] buf, final int off, final int len) {
nBytesIn += len;
// PngHelperInternal.LOGGER.info("processing compressed bytes in chunkreader : " + len);
if (len < 1 || state.isDone())
return;
if (state == State.ROW_READY)
throw new PngjInputException("this should only be called if waitingForMoreInput");
if (inf.needsDictionary() || !inf.needsInput())
throw new RuntimeException("should not happen");
inf.setInput(buf, off, len);
// PngHelperInternal.debug("entering processs bytes, state=" + state +
// " callback="+callbackMode);
if (isCallbackMode())
while (inflateData()) {
final int nextRowLen = processRowCallback();
prepareForNextRow(nextRowLen);
if (isDone())
processDoneCallback();
}
else
inflateData();
}
/*
* This never inflates more than one row This returns true if this has resulted in a row being ready and preprocessed
* with preProcessRow (in callback mode, we should call immediately processRowCallback() and
* prepareForNextRow(nextRowLen)
*/
private boolean inflateData() {
try {
// PngHelperInternal.debug("entering inflateData bytes, state=" + state +
// " callback="+callbackMode);
if (state == State.ROW_READY)
throw new PngjException("invalid state");// assert
if (state.isDone())
return false;
int ninflated = 0;
if (row == null || row.length < rowlen)
row = new byte[rowlen]; // should not happen
if (rowfilled < rowlen && !inf.finished()) {
try {
ninflated = inf.inflate(row, rowfilled, rowlen - rowfilled);
} catch (final DataFormatException e) {
throw new PngjInputException("error decompressing zlib stream ", e);
}
rowfilled += ninflated;
nBytesOut += ninflated;
}
State nextstate = null;
if (rowfilled == rowlen)
nextstate = State.ROW_READY; // complete row, process it
else if (!inf.finished())
nextstate = State.WAITING_FOR_INPUT;
else if (rowfilled > 0)
nextstate = State.ROW_READY; // complete row, process it
else
nextstate = State.WORK_DONE; // eof, no more data
state = nextstate;
if (state == State.ROW_READY) {
preProcessRow();
return true;
}
} catch (final RuntimeException e) {
close();
throw e;
}
return false;
}
/**
* Called automatically in all modes when a full row has been inflated.
*/
protected void preProcessRow() {
}
/**
* Callback, must be implemented in callbackMode
*
* This should use {@link #getRowFilled()} and {@link #getInflatedRow()} to * access the row. *
* Must return byes of next row, for next callback. */ protected int processRowCallback() { throw new PngjInputException("not implemented"); } /** * Callback, to be implemented in callbackMode *
* This will be called once to notify state done */ protected void processDoneCallback() {} /** * Inflated buffer. * * The effective length is given by {@link #getRowFilled()} */ public byte[] getInflatedRow() { return row; } /** * Should be called after the previous row was processed *
* Pass 0 or negative to signal that we are done (not expecting more bytes) *
* This resets {@link #rowfilled} *
* The */ public void prepareForNextRow(final int len) { rowfilled = 0; rown++; if (len < 1) { rowlen = 0; done(); } else if (inf.finished()) { rowlen = 0; done(); } else { state = State.WAITING_FOR_INPUT; rowlen = len; if (!callbackMode) inflateData(); } } /** * In this state, the object is waiting for more input to deflate. *
* Only in this state it's legal to feed this */ public boolean isWaitingForMoreInput() { return state == State.WAITING_FOR_INPUT; } /** * In this state, the object is waiting the caller to retrieve inflated data *
* Effective length: see {@link #getRowFilled()} */ public boolean isRowReady() { return state == State.ROW_READY; } /** * In this state, all relevant data has been uncompressed and retrieved * (exceptionally, the reading has ended * prematurely). *
* We can still feed this object, but the bytes will be swallowed/ignored.
*/
public boolean isDone() {
return state.isDone();
}
public boolean isTerminated() {
return state.isTerminated();
}
/**
* This will be called by the owner to report us the next chunk to come. We
* can make our own internal changes and
* checks. This returns true if we acknowledge the next chunk as part of
* this set
*/
public boolean ackNextChunkId(final String id) {
if (state.isTerminated())
return false;
else if (id.equals(chunkid))
return true;
else if (!allowOtherChunksInBetween(id)) {
if (state.isDone()) {
if (!isTerminated())
terminate();
return false;
} else
throw new PngjInputException("Unexpected chunk " + id + " while " + chunkid + " set is not done");
} else
return true;
}
protected void terminate() {
close();
}
/**
* This should be called when discarding this object, or for aborting.
* Secure, idempotent Don't use this just to
* notify this object that it has no more work to do, see {@link #done()}
*/
public void close() {
try {
if (!state.isTerminated())
state = State.TERMINATED;
if (infOwn && inf != null) {
inf.end();// we end the Inflater only if we created it
inf = null;
}
} catch (final Exception e) {}
}
/**
* Forces the DONE state, this object won't uncompress more data. It's still
* not terminated, it will accept more IDAT
* chunks, but will ignore them.
*/
public void done() {
if (!isDone())
state = State.WORK_DONE;
}
/**
* Target size of the current row, including filter byte.
* should coincide (or be less than) with row.length
*/
public int getRowLen() {
return rowlen;
}
/** This the amount of valid bytes in the buffer */
public int getRowFilled() {
return rowfilled;
}
/**
* Get current (last) row number.
*
* This corresponds to the raw numeration of rows as seen by the deflater. * Not the same as the real image row, if * interlaced. * */ public int getRown() { return rown; } /** * Some IDAT-like set can allow other chunks in between (APGN?). *
* Normally false. * * @param id * Id of the other chunk that appeared in middel of this set. * @return true if allowed */ public boolean allowOtherChunksInBetween(final String id) { return false; } /** * Callback mode = async processing */ public boolean isCallbackMode() { return callbackMode; } public void setCallbackMode(final boolean callbackMode) { this.callbackMode = callbackMode; } /** total number of bytes that have been fed to this object */ public long getBytesIn() { return nBytesIn; } /** total number of bytes that have been uncompressed */ public long getBytesOut() { return nBytesOut; } @Override public String toString() { final StringBuilder sb = new StringBuilder("idatSet : " + curChunk.getChunkRaw().id + " state=" + state + " rows=" + rown + " bytes=" + nBytesIn + "/" + nBytesOut); return sb.toString(); } }