428 lines
12 KiB
Java
428 lines
12 KiB
Java
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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* Eg: For IDAT non-interlaced images, a row has bytesPerRow + 1 filter byte<br>
|
|
* For interlaced images, the lengths are variable.
|
|
* <p>
|
|
* This class can work in sync (polled) mode or async (callback) mode. But for
|
|
* callback mode the method
|
|
* processRowCallback() must be overriden
|
|
* <p>
|
|
* See {@link IdatSet}, which is mostly used and has a slightly simpler use.<br>
|
|
* See <code>DeflatedChunkSetTest</code> 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(String chunkid, int initialRowLen, int maxRowLen, Inflater inflater, byte[] buffer) {
|
|
this.chunkid = chunkid;
|
|
this.rowlen = initialRowLen;
|
|
if (initialRowLen < 1 || maxRowLen < initialRowLen)
|
|
throw new PngjException("bad inital row len " + initialRowLen);
|
|
if (inflater != null) {
|
|
this.inf = inflater;
|
|
infOwn = false;
|
|
} else {
|
|
this.inf = new Inflater();
|
|
infOwn = true; // inflater is own, we will release on close()
|
|
}
|
|
this.row = buffer != null && buffer.length >= initialRowLen ? buffer : new byte[maxRowLen];
|
|
rown = -1;
|
|
this.state = State.WAITING_FOR_INPUT;
|
|
try {
|
|
prepareForNextRow(initialRowLen);
|
|
} catch (RuntimeException e) {
|
|
close();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public DeflatedChunksSet(String chunkid, int initialRowLen, int maxRowLen) {
|
|
this(chunkid, initialRowLen, maxRowLen, null, null);
|
|
}
|
|
|
|
protected void appendNewChunk(DeflatedChunkReader cr) {
|
|
// all chunks must have same id
|
|
if (!this.chunkid.equals(cr.getChunkRaw().id))
|
|
throw new PngjInputException("Bad chunk inside IdatSet, id:" + cr.getChunkRaw().id + ", expected:" + this.chunkid);
|
|
this.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(byte[] buf, int off, 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()) {
|
|
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 (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 (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
|
|
* <p>
|
|
* This should use {@link #getRowFilled()} and {@link #getInflatedRow()} to
|
|
* access the row.
|
|
* <p>
|
|
* Must return byes of next row, for next callback.
|
|
*/
|
|
protected int processRowCallback() {
|
|
throw new PngjInputException("not implemented");
|
|
}
|
|
|
|
/**
|
|
* Callback, to be implemented in callbackMode
|
|
* <p>
|
|
* 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
|
|
* <p>
|
|
* Pass 0 or negative to signal that we are done (not expecting more bytes)
|
|
* <p>
|
|
* This resets {@link #rowfilled}
|
|
* <p>
|
|
* The
|
|
*/
|
|
public void prepareForNextRow(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.
|
|
* <p>
|
|
* 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
|
|
* <p>
|
|
* 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).
|
|
* <p>
|
|
* 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(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 (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. <br>
|
|
* 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.
|
|
* <p>
|
|
* 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?).
|
|
* <p>
|
|
* Normally false.
|
|
*
|
|
* @param id
|
|
* Id of the other chunk that appeared in middel of this set.
|
|
* @return true if allowed
|
|
*/
|
|
public boolean allowOtherChunksInBetween(String id) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Callback mode = async processing
|
|
*/
|
|
public boolean isCallbackMode() {
|
|
return callbackMode;
|
|
}
|
|
|
|
public void setCallbackMode(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() {
|
|
StringBuilder sb = new StringBuilder("idatSet : " + curChunk.getChunkRaw().id + " state=" + state + " rows=" + rown + " bytes=" + nBytesIn + "/" + nBytesOut);
|
|
return sb.toString();
|
|
}
|
|
|
|
}
|