433 lines
12 KiB
Java
433 lines
12 KiB
Java
package ar.com.hjg.pngj;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.InputStream;
|
|
import java.util.Arrays;
|
|
|
|
import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode;
|
|
import ar.com.hjg.pngj.chunks.ChunkHelper;
|
|
|
|
/**
|
|
* Consumes a stream of bytes that consist of a series of PNG-like chunks.
|
|
* <p>
|
|
* This has little intelligence, it's quite low-level and general (it could even
|
|
* be used for a MNG stream, for example).
|
|
* It supports signature recognition and idat deflate
|
|
*/
|
|
public class ChunkSeqReader implements IBytesConsumer {
|
|
|
|
protected static final int SIGNATURE_LEN = 8;
|
|
protected final boolean withSignature;
|
|
|
|
private final byte[] buf0 = new byte[8]; // for signature or chunk starts
|
|
private int buf0len = 0;
|
|
|
|
private boolean signatureDone = false;
|
|
private boolean done = false; // ended, normally or not
|
|
|
|
private int chunkCount = 0;
|
|
|
|
private long bytesCount = 0;
|
|
|
|
private DeflatedChunksSet curReaderDeflatedSet; // one instance is created for each
|
|
// "idat-like set". Normally one.
|
|
|
|
private ChunkReader curChunkReader;
|
|
|
|
private long idatBytes; // this is only for the IDAT (not mrerely "idat-like")
|
|
|
|
/**
|
|
* Creates a ChunkSeqReader (with signature)
|
|
*/
|
|
public ChunkSeqReader() {
|
|
this(true);
|
|
}
|
|
|
|
/**
|
|
* @param withSignature
|
|
* If true, the stream is assumed be prepended by 8 bit signature
|
|
*/
|
|
public ChunkSeqReader(final boolean withSignature) {
|
|
this.withSignature = withSignature;
|
|
signatureDone = !withSignature;
|
|
}
|
|
|
|
/**
|
|
* Consumes (in general, partially) a number of bytes. A single call never
|
|
* involves more than one chunk.
|
|
*
|
|
* When the signature is read, it calls checkSignature()
|
|
*
|
|
* When the start of a chunk is detected, it calls
|
|
* {@link #startNewChunk(int, String, long)}
|
|
*
|
|
* When data from a chunk is being read, it delegates to
|
|
* {@link ChunkReader#feedBytes(byte[], int, int)}
|
|
*
|
|
* The caller might want to call this method more than once in succesion
|
|
*
|
|
* This should rarely be overriden
|
|
*
|
|
* @param buffer
|
|
* @param offset
|
|
* Offset in buffer
|
|
* @param len
|
|
* Valid bytes that can be consumed
|
|
* @return processed bytes, in the 1-len range. -1 if done. Only returns 0
|
|
* if len=0.
|
|
**/
|
|
@Override
|
|
public int consume(final byte[] buffer, final int offset, final int len) {
|
|
if (done)
|
|
return -1;
|
|
if (len == 0)
|
|
return 0; // nothing to do
|
|
if (len < 0)
|
|
throw new PngjInputException("Bad len: " + len);
|
|
int processed = 0;
|
|
if (signatureDone) {
|
|
if (curChunkReader == null || curChunkReader.isDone()) { // new chunk: read first 8 bytes
|
|
int read0 = 8 - buf0len;
|
|
if (read0 > len)
|
|
read0 = len;
|
|
System.arraycopy(buffer, offset, buf0, buf0len, read0);
|
|
buf0len += read0;
|
|
processed += read0;
|
|
bytesCount += read0;
|
|
// len -= read0;
|
|
// offset += read0;
|
|
if (buf0len == 8) { // end reading chunk length and id
|
|
chunkCount++;
|
|
final int clen = PngHelperInternal.readInt4fromBytes(buf0, 0);
|
|
final String cid = ChunkHelper.toString(buf0, 4, 4);
|
|
startNewChunk(clen, cid, bytesCount - 8);
|
|
buf0len = 0;
|
|
}
|
|
} else { // reading chunk, delegates to curChunkReader
|
|
final int read1 = curChunkReader.feedBytes(buffer, offset, len);
|
|
processed += read1;
|
|
bytesCount += read1;
|
|
}
|
|
} else { // reading signature
|
|
int read = ChunkSeqReader.SIGNATURE_LEN - buf0len;
|
|
if (read > len)
|
|
read = len;
|
|
System.arraycopy(buffer, offset, buf0, buf0len, read);
|
|
buf0len += read;
|
|
if (buf0len == ChunkSeqReader.SIGNATURE_LEN) {
|
|
checkSignature(buf0);
|
|
buf0len = 0;
|
|
signatureDone = true;
|
|
}
|
|
processed += read;
|
|
bytesCount += read;
|
|
}
|
|
return processed;
|
|
}
|
|
|
|
/**
|
|
* Trys to feeds exactly <tt>len</tt> bytes, calling
|
|
* {@link #consume(byte[], int, int)} retrying if necessary.
|
|
*
|
|
* This should only be used in callback mode
|
|
*
|
|
* @return true if succceded
|
|
*/
|
|
public boolean feedAll(final byte[] buf, int off, int len) {
|
|
while (len > 0) {
|
|
final int n = consume(buf, off, len);
|
|
if (n < 1)
|
|
return false;
|
|
len -= n;
|
|
off += n;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Called for all chunks when a chunk start has been read (id and length),
|
|
* before the chunk data itself is read. It
|
|
* creates a new ChunkReader (field accesible via
|
|
* {@link #getCurChunkReader()}) in the corresponding mode, and
|
|
* eventually a curReaderDeflatedSet.(field accesible via
|
|
* {@link #getCurReaderDeflatedSet()})
|
|
*
|
|
* To decide the mode and options, it calls
|
|
* {@link #shouldCheckCrc(int, String)},
|
|
* {@link #shouldSkipContent(int, String)}, {@link #isIdatKind(String)}.
|
|
* Those methods should be overriden in
|
|
* preference to this; if overriden, this should be called first.
|
|
*
|
|
* The respective {@link ChunkReader#chunkDone()} method is directed to this
|
|
* {@link #postProcessChunk(ChunkReader)}.
|
|
*
|
|
* Instead of overriding this, see also
|
|
* {@link #createChunkReaderForNewChunk(String, int, long, boolean)}
|
|
*/
|
|
protected void startNewChunk(final int len, final String id, final long offset) {
|
|
if (id.equals(ChunkHelper.IDAT))
|
|
idatBytes += len;
|
|
final boolean checkCrc = shouldCheckCrc(len, id);
|
|
final boolean skip = shouldSkipContent(len, id);
|
|
final boolean isIdatType = isIdatKind(id);
|
|
// PngHelperInternal.debug("start new chunk id=" + id + " off=" + offset + " skip=" + skip + " idat=" +
|
|
// isIdatType);
|
|
// first see if we should terminate an active curReaderDeflatedSet
|
|
boolean forCurrentIdatSet = false;
|
|
if (curReaderDeflatedSet != null)
|
|
forCurrentIdatSet = curReaderDeflatedSet.ackNextChunkId(id);
|
|
if (isIdatType && !skip) { // IDAT non skipped: create a DeflatedChunkReader owned by a idatSet
|
|
if (!forCurrentIdatSet) {
|
|
if (curReaderDeflatedSet != null && !curReaderDeflatedSet.isDone())
|
|
throw new PngjInputException("new IDAT-like chunk when previous was not done");
|
|
curReaderDeflatedSet = createIdatSet(id);
|
|
}
|
|
curChunkReader = new DeflatedChunkReader(len, id, checkCrc, offset, curReaderDeflatedSet) {
|
|
@Override
|
|
protected void chunkDone() {
|
|
super.chunkDone();
|
|
postProcessChunk(this);
|
|
}
|
|
};
|
|
|
|
} else { // for non-idat chunks (or skipped idat like)
|
|
curChunkReader = createChunkReaderForNewChunk(id, len, offset, skip);
|
|
if (!checkCrc)
|
|
curChunkReader.setCrcCheck(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This will be called for all chunks (even skipped), except for IDAT-like
|
|
* non-skiped chunks
|
|
*
|
|
* The default behaviour is to create a ChunkReader in BUFFER mode (or SKIP
|
|
* if skip==true) that calls
|
|
* {@link #postProcessChunk(ChunkReader)} (always) when done.
|
|
*
|
|
* @param id
|
|
* Chunk id
|
|
* @param len
|
|
* Chunk length
|
|
* @param offset
|
|
* offset inside PNG stream , merely informative
|
|
* @param skip
|
|
* flag: is true, the content will not be buffered (nor
|
|
* processed)
|
|
* @return a newly created ChunkReader that will create the ChunkRaw and
|
|
* then discarded
|
|
*/
|
|
protected ChunkReader createChunkReaderForNewChunk(final String id, final int len, final long offset,
|
|
final boolean skip) {
|
|
return new ChunkReader(len, id, offset, skip ? ChunkReaderMode.SKIP : ChunkReaderMode.BUFFER) {
|
|
@Override
|
|
protected void chunkDone() {
|
|
postProcessChunk(this);
|
|
}
|
|
|
|
@Override
|
|
protected void processData(final int offsetinChhunk, final byte[] buf, final int off, final int len) {
|
|
throw new PngjExceptionInternal("should never happen");
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This is called after a chunk is read, in all modes
|
|
*
|
|
* This implementation only chenks the id of the first chunk, and process
|
|
* the IEND chunk (sets done=true)
|
|
**
|
|
* Further processing should be overriden (call this first!)
|
|
**/
|
|
protected void postProcessChunk(final ChunkReader chunkR) { // called after chunk is read
|
|
if (chunkCount == 1) {
|
|
final String cid = firstChunkId();
|
|
if (cid != null && !cid.equals(chunkR.getChunkRaw().id))
|
|
throw new PngjInputException("Bad first chunk: " + chunkR.getChunkRaw().id + " expected: " + firstChunkId());
|
|
}
|
|
if (chunkR.getChunkRaw().id.equals(endChunkId()))
|
|
done = true;
|
|
}
|
|
|
|
/**
|
|
* DeflatedChunksSet factory. This implementation is quite dummy, it usually
|
|
* should be overriden.
|
|
*/
|
|
protected DeflatedChunksSet createIdatSet(final String id) {
|
|
return new DeflatedChunksSet(id, 1024, 1024); // sizes: arbitrary This should normally be
|
|
// overriden
|
|
}
|
|
|
|
/**
|
|
* Decides if this Chunk is of "IDAT" kind (in concrete: if it is, and if
|
|
* it's not to be skiped, a DeflatedChunksSet
|
|
* will be created to deflate it and process+ the deflated data)
|
|
*
|
|
* This implementation always returns always false
|
|
*
|
|
* @param id
|
|
*/
|
|
protected boolean isIdatKind(final String id) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Chunks can be skipped depending on id and/or length. Skipped chunks are
|
|
* still processed, but their data will be
|
|
* null, and CRC will never checked
|
|
*
|
|
* @param len
|
|
* @param id
|
|
*/
|
|
protected boolean shouldSkipContent(final int len, final String id) {
|
|
return false;
|
|
}
|
|
|
|
protected boolean shouldCheckCrc(final int len, final String id) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Throws PngjInputException if bad signature
|
|
*
|
|
* @param buf
|
|
* Signature. Should be of length 8
|
|
*/
|
|
protected void checkSignature(final byte[] buf) {
|
|
if (!Arrays.equals(buf, PngHelperInternal.getPngIdSignature()))
|
|
throw new PngjInputException("Bad PNG signature");
|
|
}
|
|
|
|
/**
|
|
* If false, we are still reading the signature
|
|
*
|
|
* @return true if signature has been read (or if we don't have signature)
|
|
*/
|
|
public boolean isSignatureDone() {
|
|
return signatureDone;
|
|
}
|
|
|
|
/**
|
|
* If true, we either have processe the IEND chunk, or close() has been
|
|
* called, or a fatal error has happened
|
|
*/
|
|
public boolean isDone() {
|
|
return done;
|
|
}
|
|
|
|
/**
|
|
* total of bytes read (buffered or not)
|
|
*/
|
|
public long getBytesCount() {
|
|
return bytesCount;
|
|
}
|
|
|
|
/**
|
|
* @return Chunks already read, including partial reading (currently
|
|
* reading)
|
|
*/
|
|
public int getChunkCount() {
|
|
return chunkCount;
|
|
}
|
|
|
|
/**
|
|
* Currently reading chunk, or just ended reading
|
|
*
|
|
* @return null only if still reading signature
|
|
*/
|
|
public ChunkReader getCurChunkReader() {
|
|
return curChunkReader;
|
|
}
|
|
|
|
/**
|
|
* The latest deflated set (typically IDAT chunks) reader. Notice that there
|
|
* could be several idat sets (eg for APNG)
|
|
*/
|
|
public DeflatedChunksSet getCurReaderDeflatedSet() {
|
|
return curReaderDeflatedSet;
|
|
}
|
|
|
|
/**
|
|
* Closes this object and release resources. For normal termination or
|
|
* abort. Secure and idempotent.
|
|
*/
|
|
public void close() { // forced closing
|
|
if (curReaderDeflatedSet != null)
|
|
curReaderDeflatedSet.close();
|
|
done = true;
|
|
}
|
|
|
|
/**
|
|
* Returns true if we are not in middle of a chunk: we have just ended
|
|
* reading past chunk , or we are at the start, or
|
|
* end of signature, or we are done
|
|
*/
|
|
public boolean isAtChunkBoundary() {
|
|
return bytesCount == 0 || bytesCount == 8 || done || curChunkReader == null || curChunkReader.isDone();
|
|
}
|
|
|
|
/**
|
|
* Which should be the id of the first chunk
|
|
*
|
|
* @return null if you don't want to check it
|
|
*/
|
|
protected String firstChunkId() {
|
|
return "IHDR";
|
|
}
|
|
|
|
/**
|
|
* Helper method, reports amount of bytes inside IDAT chunks.
|
|
*
|
|
* @return Bytes in IDAT chunks
|
|
*/
|
|
public long getIdatBytes() {
|
|
return idatBytes;
|
|
}
|
|
|
|
/**
|
|
* Which should be the id of the last chunk
|
|
*
|
|
* @return "IEND"
|
|
*/
|
|
protected String endChunkId() {
|
|
return "IEND";
|
|
}
|
|
|
|
/**
|
|
* Reads all content from a file. Helper method, only for callback mode
|
|
*/
|
|
public void feedFromFile(final File f) {
|
|
try {
|
|
feedFromInputStream(new FileInputStream(f), true);
|
|
} catch (final FileNotFoundException e) {
|
|
throw new PngjInputException(e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads all content from an input stream. Helper method, only for callback
|
|
* mode
|
|
*
|
|
* @param is
|
|
* @param closeStream
|
|
* Closes the input stream when done (or if error)
|
|
*/
|
|
public void feedFromInputStream(final InputStream is, final boolean closeStream) {
|
|
final BufferedStreamFeeder sf = new BufferedStreamFeeder(is);
|
|
sf.setCloseStream(closeStream);
|
|
try {
|
|
sf.feedAll(this);
|
|
} finally {
|
|
close();
|
|
sf.close();
|
|
}
|
|
}
|
|
|
|
public void feedFromInputStream(final InputStream is) {
|
|
feedFromInputStream(is, true);
|
|
}
|
|
}
|