WarpPI/desktop/src/main/java/ar/com/hjg/pngj/PngReader.java

649 lines
20 KiB
Java

package ar.com.hjg.pngj;
import java.io.File;
import java.io.InputStream;
import java.util.zip.Adler32;
import java.util.zip.CRC32;
import ar.com.hjg.pngj.chunks.ChunkLoadBehaviour;
import ar.com.hjg.pngj.chunks.ChunksList;
import ar.com.hjg.pngj.chunks.PngChunkFCTL;
import ar.com.hjg.pngj.chunks.PngChunkFDAT;
import ar.com.hjg.pngj.chunks.PngChunkIDAT;
import ar.com.hjg.pngj.chunks.PngMetadata;
/**
* Reads a PNG image (pixels and/or metadata) from a file or stream.
* <p>
* Each row is read as an {@link ImageLineInt} object (one int per sample), but
* this can be changed by setting a
* different ImageLineFactory
* <p>
* Internally, this wraps a {@link ChunkSeqReaderPng} with a
* {@link BufferedStreamFeeder}
* <p>
* The reading sequence is as follows: <br>
* 1. At construction time, the header and IHDR chunk are read (basic image
* info) <br>
* 2. Afterwards you can set some additional global options. Eg.
* {@link #setCrcCheckDisabled()}.<br>
* 3. Optional: If you call getMetadata() or getChunksLisk() before start
* reading the rows, all the chunks before IDAT
* are then loaded and available <br>
* 4a. The rows are read in order by calling {@link #readRow()}. You can also
* call {@link #readRow(int)} to skip rows
* -but you can't go backwards, at least not with this implementation. This
* method returns a {@link IImageLine} object
* which can be casted to the concrete class. This class returns by default a
* {@link ImageLineInt}, but this can be
* changed.<br>
* 4b. Alternatively, you can read all rows, or a subset, in a single call:
* {@link #readRows()},
* {@link #readRows(int, int, int)} ,etc. In general this consumes more memory,
* but for interlaced images this is
* equally efficient, and more so if reading a small subset of rows.<br>
* 5. Reading of the last row automatically loads the trailing chunks, and ends
* the reader.<br>
* 6. end() also loads the trailing chunks, if not done, and finishes cleanly
* the reading and closes the stream.
* <p>
* See also {@link PngReaderInt} (esentially the same as this, and slightly
* preferred) and {@link PngReaderByte} (uses
* byte instead of int to store the samples).
*/
public class PngReader {
// some performance/defensive limits
/**
* Defensive limit: refuse to read more than 900MB, can be changed with
* {@link #setMaxTotalBytesRead(long)}
*/
public static final long MAX_TOTAL_BYTES_READ_DEFAULT = 901001001L; // ~ 900MB
/**
* Defensive limit: refuse to load more than 5MB of ancillary metadata, see
* {@link #setMaxBytesMetadata(long)} and
* also {@link #addChunkToSkip(String)}
*/
public static final long MAX_BYTES_METADATA_DEFAULT = 5024024; // for ancillary chunks
/**
* Skip ancillary chunks greater than 2MB, see
* {@link #setSkipChunkMaxSize(long)}
*/
public static final long MAX_CHUNK_SIZE_SKIP = 2024024; // chunks exceeding this size will be skipped (nor even CRC
// checked)
/**
* Basic image info - final and inmutable.
*/
public final ImageInfo imgInfo; // People always told me: be careful what you do, and don't go around declaring public
// fields...
/**
* flag: image was in interlaced format
*/
public final boolean interlaced;
/**
* This object has most of the intelligence to parse the chunks and
* decompress the IDAT stream
*/
protected final ChunkSeqReaderPng chunkseq;
/**
* Takes bytes from the InputStream and passes it to the ChunkSeqReaderPng.
* Never null.
*/
protected final BufferedStreamFeeder streamFeeder;
/**
* @see #getMetadata()
*/
protected final PngMetadata metadata; // this a wrapper over chunks
/**
* Current row number (reading or read), numbered from 0
*/
protected int rowNum = -1;
/**
* Represents the set of lines (rows) being read. Normally this works as a
* cursor, storing only one (the current) row.
* This stores several (perhaps all) rows only if calling
* {@link #readRows()} or for interlaced images (this later is
* transparent to the user)
*/
protected IImageLineSet<? extends IImageLine> imlinesSet;
/**
* This factory decides the concrete type of the ImageLine that will be
* used. See {@link ImageLineSetDefault} for
* examples
*/
private IImageLineSetFactory<? extends IImageLine> imageLineSetFactory;
CRC32 idatCrca;// for internal testing
Adler32 idatCrcb;// for internal testing
/**
* Constructs a PngReader object from a stream, with default options. This
* reads the signature and the first IHDR
* chunk only.
* <p>
* Warning: In case of exception the stream is NOT closed.
* <p>
* Warning: By default the stream will be closed when this object is
* {@link #close()}d. See
* {@link #PngReader(InputStream,boolean)} or
* {@link #setShouldCloseStream(boolean)}
* <p>
*
* @param inputStream
* PNG stream
*/
public PngReader(InputStream inputStream) {
this(inputStream, true);
}
/**
* Same as {@link #PngReader(InputStream)} but allows to specify early if
* the stream must be closed
*
* @param inputStream
* @param shouldCloseStream
* The stream will be closed in case of exception (constructor
* included) or normal
* termination.
*/
public PngReader(InputStream inputStream, boolean shouldCloseStream) {
streamFeeder = new BufferedStreamFeeder(inputStream);
streamFeeder.setCloseStream(shouldCloseStream);
chunkseq = createChunkSeqReader();
try {
streamFeeder.setFailIfNoFeed(true);
if (!streamFeeder.feedFixed(chunkseq, 36)) // 8+13+12=36 PNG signature+IHDR chunk
throw new PngjInputException("error reading first 21 bytes");
imgInfo = chunkseq.getImageInfo();
interlaced = chunkseq.getDeinterlacer() != null;
setMaxBytesMetadata(MAX_BYTES_METADATA_DEFAULT);
setMaxTotalBytesRead(MAX_TOTAL_BYTES_READ_DEFAULT);
setSkipChunkMaxSize(MAX_CHUNK_SIZE_SKIP);
chunkseq.addChunkToSkip(PngChunkFDAT.ID);// default: skip fdAT chunks!
chunkseq.addChunkToSkip(PngChunkFCTL.ID);// default: skip fctl chunks!
this.metadata = new PngMetadata(chunkseq.chunksList);
// sets a default factory (with ImageLineInt),
// this can be overwriten by a extended constructor, or by a setter
setLineSetFactory(ImageLineSetDefault.getFactoryInt());
rowNum = -1;
} catch (RuntimeException e) {
streamFeeder.close();
chunkseq.close();
throw e;
}
}
/**
* Constructs a PngReader opening a file. Sets
* <tt>shouldCloseStream=true</tt>, so that the stream will be closed with
* this object.
*
* @param file
* PNG image file
*/
public PngReader(File file) {
this(PngHelperInternal.istreamFromFile(file), true);
}
/**
* Reads chunks before first IDAT. Normally this is called automatically
* <p>
* Position before: after IDHR (crc included) Position after: just after the
* first IDAT chunk id
* <P>
* This can be called several times (tentatively), it does nothing if
* already run
* <p>
* (Note: when should this be called? in the constructor? hardly, because we
* loose the opportunity to call
* setChunkLoadBehaviour() and perhaps other settings before reading the
* first row? but sometimes we want to access
* some metadata (plte, phys) before. Because of this, this method can be
* called explicitly but is also called
* implicititly in some methods (getMetatada(), getChunksList())
*/
protected void readFirstChunks() {
while (chunkseq.currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT)
if (streamFeeder.feed(chunkseq) <= 0)
throw new PngjInputException("premature ending reading first chunks");
}
/**
* Determines which ancillary chunks (metadata) are to be loaded and which
* skipped.
* <p>
* Additional restrictions may apply. See also
* {@link #setChunksToSkip(String...)}, {@link #addChunkToSkip(String)},
* {@link #setMaxBytesMetadata(long)}, {@link #setSkipChunkMaxSize(long)}
*
* @param chunkLoadBehaviour
* {@link ChunkLoadBehaviour}
*/
public void setChunkLoadBehaviour(ChunkLoadBehaviour chunkLoadBehaviour) {
this.chunkseq.setChunkLoadBehaviour(chunkLoadBehaviour);
}
/**
* All loaded chunks (metada). If we have not yet end reading the image,
* this will include only the chunks before the
* pixels data (IDAT)
* <p>
* Critical chunks are included, except that all IDAT chunks appearance are
* replaced by a single dummy-marker IDAT
* chunk. These might be copied to the PngWriter
* <p>
*
* @see #getMetadata()
*/
public ChunksList getChunksList() {
return getChunksList(true);
}
public ChunksList getChunksList(boolean forceLoadingOfFirstChunks) {
if (forceLoadingOfFirstChunks && chunkseq.firstChunksNotYetRead())
readFirstChunks();
return chunkseq.chunksList;
}
int getCurrentChunkGroup() {
return chunkseq.currentChunkGroup;
}
/**
* High level wrapper over chunksList
*
* @see #getChunksList()
*/
public PngMetadata getMetadata() {
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
return metadata;
}
/**
* Reads next row.
*
* The caller must know that there are more rows to read.
*
* @return Never null. Throws PngInputException if no more
*/
public IImageLine readRow() {
return readRow(rowNum + 1);
}
/**
* True if last row has not yet been read
*/
public boolean hasMoreRows() {
return rowNum < getCurImgInfo().rows - 1;
}
/**
* The row number is mostly meant as a check, the rows must be called in
* ascending order (not necessarily consecutive)
*/
public IImageLine readRow(int nrow) {
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
if (!interlaced) {
if (imlinesSet == null)
imlinesSet = createLineSet(true, -1, 0, 1);
IImageLine line = imlinesSet.getImageLine(nrow);
if (nrow == rowNum)
return line; // already read??
else if (nrow < rowNum)
throw new PngjInputException("rows must be read in increasing order: " + nrow);
while (rowNum < nrow) {
while (!chunkseq.getIdatSet().isRowReady())
if (streamFeeder.feed(chunkseq) < 1)
throw new PngjInputException("premature ending");
rowNum++;
chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb);
if (rowNum == nrow) {
line.readFromPngRaw(chunkseq.getIdatSet().getUnfilteredRow(), getCurImgInfo().bytesPerRow + 1, 0, 1);
line.endReadFromPngRaw();
}
chunkseq.getIdatSet().advanceToNextRow();
}
return line;
} else { // and now, for something completely different (interlaced!)
if (imlinesSet == null) {
imlinesSet = createLineSet(false, getCurImgInfo().rows, 0, 1);
loadAllInterlaced(getCurImgInfo().rows, 0, 1);
}
rowNum = nrow;
return imlinesSet.getImageLine(nrow);
}
}
/**
* Reads all rows in a ImageLineSet This is handy, but less memory-efficient
* (except for interlaced)
*/
public IImageLineSet<? extends IImageLine> readRows() {
return readRows(getCurImgInfo().rows, 0, 1);
}
/**
* Reads a subset of rows.
* <p>
* This method should called once, and not be mixed with {@link #readRow()}
*
* @param nRows
* how many rows to read (default: imageInfo.rows; negative:
* autocompute)
* @param rowOffset
* rows to skip (default:0)
* @param rowStep
* step between rows to load( default:1)
*/
public IImageLineSet<? extends IImageLine> readRows(int nRows, int rowOffset, int rowStep) {
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
if (nRows < 0)
nRows = (getCurImgInfo().rows - rowOffset) / rowStep;
if (rowStep < 1 || rowOffset < 0 || nRows == 0 || nRows * rowStep + rowOffset > getCurImgInfo().rows)
throw new PngjInputException("bad args");
if (rowNum >= rowOffset)
throw new PngjInputException("readRows cannot be mixed with readRow");
imlinesSet = createLineSet(false, nRows, rowOffset, rowStep);
if (!interlaced) {
int m = -1; // last row already read in
while (m < nRows - 1) {
while (!chunkseq.getIdatSet().isRowReady())
if (streamFeeder.feed(chunkseq) < 1)
throw new PngjInputException("Premature ending");
rowNum++;
chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb);
m = (rowNum - rowOffset) / rowStep;
if (rowNum >= rowOffset && rowStep * m + rowOffset == rowNum) {
IImageLine line = imlinesSet.getImageLine(rowNum);
line.readFromPngRaw(chunkseq.getIdatSet().getUnfilteredRow(), getCurImgInfo().bytesPerRow + 1, 0, 1);
line.endReadFromPngRaw();
}
chunkseq.getIdatSet().advanceToNextRow();
}
} else { // and now, for something completely different (interlaced)
loadAllInterlaced(nRows, rowOffset, rowStep);
}
chunkseq.getIdatSet().done();
return imlinesSet;
}
/**
* Sets the factory that creates the ImageLine. By default, this
* implementation uses ImageLineInt but this can be
* changed (at construction time or later) by calling this method.
* <p>
* See also {@link #createLineSet(boolean, int, int, int)}
*
* @param factory
*/
public void setLineSetFactory(IImageLineSetFactory<? extends IImageLine> factory) {
imageLineSetFactory = factory;
}
/**
* By default this uses the factory (which, by default creates
* ImageLineInt). You should rarely override this.
* <p>
* See doc in
* {@link IImageLineSetFactory#create(ImageInfo, boolean, int, int, int)}
*/
protected IImageLineSet<? extends IImageLine> createLineSet(boolean singleCursor, int nlines, int noffset,
int step) {
return imageLineSetFactory.create(getCurImgInfo(), singleCursor, nlines, noffset, step);
}
protected void loadAllInterlaced(int nRows, int rowOffset, int rowStep) {
IdatSet idat = chunkseq.getIdatSet();
int nread = 0;
do {
while (!chunkseq.getIdatSet().isRowReady())
if (streamFeeder.feed(chunkseq) <= 0)
break;
if (!chunkseq.getIdatSet().isRowReady())
throw new PngjInputException("Premature ending?");
chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb);
int rowNumreal = idat.rowinfo.rowNreal;
boolean inset = imlinesSet.hasImageLine(rowNumreal);
if (inset) {
imlinesSet.getImageLine(rowNumreal).readFromPngRaw(idat.getUnfilteredRow(), idat.rowinfo.buflen, idat.rowinfo.oX, idat.rowinfo.dX);
nread++;
}
idat.advanceToNextRow();
} while (nread < nRows || !idat.isDone());
idat.done();
for (int i = 0, j = rowOffset; i < nRows; i++, j += rowStep) {
imlinesSet.getImageLine(j).endReadFromPngRaw();
}
}
/**
* Reads all the (remaining) file, skipping the pixels data. This is much
* more efficient that calling
* {@link #readRow()}, specially for big files (about 10 times faster!),
* because it doesn't even decompress the IDAT
* stream and disables CRC check Use this if you are not interested in
* reading pixels,only metadata.
*/
public void readSkippingAllRows() {
chunkseq.addChunkToSkip(PngChunkIDAT.ID);
chunkseq.addChunkToSkip(PngChunkFDAT.ID);
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
end();
}
/**
* Set total maximum bytes to read (0: unlimited; default: 200MB). <br>
* These are the bytes read (not loaded) in the input stream. If exceeded,
* an exception will be thrown.
*/
public void setMaxTotalBytesRead(long maxTotalBytesToRead) {
chunkseq.setMaxTotalBytesRead(maxTotalBytesToRead);
}
/**
* Set total maximum bytes to load from ancillary chunks (0: unlimited;
* default: 5Mb).<br>
* If exceeded, some chunks will be skipped
*/
public void setMaxBytesMetadata(long maxBytesMetadata) {
chunkseq.setMaxBytesMetadata(maxBytesMetadata);
}
/**
* Set maximum size in bytes for individual ancillary chunks (0: unlimited;
* default: 2MB). <br>
* Chunks exceeding this length will be skipped (the CRC will not be
* checked) and the chunk will be saved as a
* PngChunkSkipped object. See also setSkipChunkIds
*/
public void setSkipChunkMaxSize(long skipChunkMaxSize) {
chunkseq.setSkipChunkMaxSize(skipChunkMaxSize);
}
/**
* Chunks ids to be skipped. <br>
* These chunks will be skipped (the CRC will not be checked) and the chunk
* will be saved as a PngChunkSkipped object.
* See also setSkipChunkMaxSize
*/
public void setChunksToSkip(String... chunksToSkip) {
chunkseq.setChunksToSkip(chunksToSkip);
}
public void addChunkToSkip(String chunkToSkip) {
chunkseq.addChunkToSkip(chunkToSkip);
}
public void dontSkipChunk(String chunkToSkip) {
chunkseq.dontSkipChunk(chunkToSkip);
}
/**
* if true, input stream will be closed after ending read
* <p>
* default=true
*/
public void setShouldCloseStream(boolean shouldCloseStream) {
streamFeeder.setCloseStream(shouldCloseStream);
}
/**
* Reads till end of PNG stream and call <tt>close()</tt>
*
* This should normally be called after reading the pixel data, to read the
* trailing chunks and close the stream. But
* it can be called at anytime. This will also read the first chunks if not
* still read, and skip pixels (IDAT) if
* still pending.
*
* If you want to read all metadata skipping pixels, readSkippingAllRows()
* is a little more efficient.
*
* If you want to abort immediately, call instead <tt>close()</tt>
*/
public void end() {
try {
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
if (chunkseq.getIdatSet() != null && !chunkseq.getIdatSet().isDone())
chunkseq.getIdatSet().done();
while (!chunkseq.isDone())
if (streamFeeder.feed(chunkseq) <= 0)
break;
} finally {
close();
}
}
/**
* Releases resources, and closes stream if corresponds. Idempotent, secure,
* no exceptions.
*
* This can be also called for abort. It is recommended to call this in case
* of exceptions
*/
public void close() {
try {
if (chunkseq != null)
chunkseq.close();
} catch (Exception e) {
PngHelperInternal.LOGGER.warning("error closing chunk sequence:" + e.getMessage());
}
if (streamFeeder != null)
streamFeeder.close();
}
/**
* Interlaced PNG is accepted -though not welcomed- now...
*/
public boolean isInterlaced() {
return interlaced;
}
/**
* Disables the CRC integrity check in IDAT chunks and ancillary chunks,
* this gives a slight increase in reading speed
* for big files
*/
public void setCrcCheckDisabled() {
chunkseq.setCheckCrc(false);
}
/**
* Gets wrapped {@link ChunkSeqReaderPng} object
*/
public ChunkSeqReaderPng getChunkseq() {
return chunkseq;
}
/**
* called on construction time. Override if you want an alternative class
*/
protected ChunkSeqReaderPng createChunkSeqReader() {
return new ChunkSeqReaderPng(false);
}
/**
* Enables and prepare the simple digest computation. Must be called before
* reading the pixels. See
* {@link #getSimpleDigestHex()}
*/
public void prepareSimpleDigestComputation() {
if (idatCrca == null)
idatCrca = new CRC32();
else
idatCrca.reset();
if (idatCrcb == null)
idatCrcb = new Adler32();
else
idatCrcb.reset();
imgInfo.updateCrc(idatCrca);
idatCrcb.update((byte) imgInfo.rows); // not important
}
long getSimpleDigest() {
if (idatCrca == null)
return 0;
else
return (idatCrca.getValue() ^ (idatCrcb.getValue() << 31));
}
/**
* Pseudo 64-bits digest computed over the basic image properties and the
* raw pixels data: it should coincide for
* equivalent images encoded with different filters and compressors; but
* will not coincide for
* interlaced/non-interlaced; also, this does not take into account the
* palette info. This will be valid only if
* {@link #prepareSimpleDigestComputation()} has been called, and all rows
* have been read. Not fool-proof, not
* cryptografically secure, only for informal testing and duplicates
* detection.
*
* @return A 64-digest in hexadecimal
*/
public String getSimpleDigestHex() {
return String.format("%016X", getSimpleDigest());
}
/**
* Basic info, for debugging.
*/
public String toString() { // basic info
return imgInfo.toString() + " interlaced=" + interlaced;
}
/**
* Basic info, in a compact format, apt for scripting
* COLSxROWS[dBITDEPTH][a][p][g][i] ( the default dBITDEPTH='d8' is
* ommited)
*
*/
public String toStringCompact() {
return imgInfo.toStringBrief() + (interlaced ? "i" : "");
}
public ImageInfo getImgInfo() {
return imgInfo;
}
public ImageInfo getCurImgInfo() {
return chunkseq.getCurImgInfo();
}
}