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. *
* Each row is read as an {@link ImageLineInt} object (one int per sample), but * this can be changed by setting a * different ImageLineFactory *
* Internally, this wraps a {@link ChunkSeqReaderPng} with a * {@link BufferedStreamFeeder} *
* The reading sequence is as follows:
* 1. At construction time, the header and IHDR chunk are read (basic image
* info)
* 2. Afterwards you can set some additional global options. Eg.
* {@link #setCrcCheckDisabled()}.
* 3. Optional: If you call getMetadata() or getChunksLisk() before start
* reading the rows, all the chunks before IDAT
* are then loaded and available
* 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.
* 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.
* 5. Reading of the last row automatically loads the trailing chunks, and ends
* the reader.
* 6. end() also loads the trailing chunks, if not done, and finishes cleanly
* the reading and closes the stream.
*
* 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. *
* Warning: In case of exception the stream is NOT closed. *
* Warning: By default the stream will be closed when this object is * {@link #close()}d. See * {@link #PngReader(InputStream,boolean)} or * {@link #setShouldCloseStream(boolean)} *
* * @param inputStream * PNG stream */ public PngReader(final 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(final InputStream inputStream, final 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(PngReader.MAX_BYTES_METADATA_DEFAULT); setMaxTotalBytesRead(PngReader.MAX_TOTAL_BYTES_READ_DEFAULT); setSkipChunkMaxSize(PngReader.MAX_CHUNK_SIZE_SKIP); chunkseq.addChunkToSkip(PngChunkFDAT.ID);// default: skip fdAT chunks! chunkseq.addChunkToSkip(PngChunkFCTL.ID);// default: skip fctl chunks! 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 (final RuntimeException e) { streamFeeder.close(); chunkseq.close(); throw e; } } /** * Constructs a PngReader opening a file. Sets * shouldCloseStream=true, so that the stream will be closed with * this object. * * @param file * PNG image file */ public PngReader(final File file) { this(PngHelperInternal.istreamFromFile(file), true); } /** * Reads chunks before first IDAT. Normally this is called automatically *
* Position before: after IDHR (crc included) Position after: just after the * first IDAT chunk id *
* This can be called several times (tentatively), it does nothing if * already run *
* (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. *
* 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(final ChunkLoadBehaviour chunkLoadBehaviour) { 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) *
* 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 *
* * @see #getMetadata() */ public ChunksList getChunksList() { return getChunksList(true); } public ChunksList getChunksList(final 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(final int nrow) { if (chunkseq.firstChunksNotYetRead()) readFirstChunks(); if (!interlaced) { if (imlinesSet == null) imlinesSet = createLineSet(true, -1, 0, 1); final 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. *
* 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, final int rowOffset, final 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) { final IImageLine line = imlinesSet.getImageLine(rowNum); line.readFromPngRaw(chunkseq.getIdatSet().getUnfilteredRow(), getCurImgInfo().bytesPerRow + 1, 0, 1); line.endReadFromPngRaw(); } chunkseq.getIdatSet().advanceToNextRow(); } } else 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. *
* See also {@link #createLineSet(boolean, int, int, int)} * * @param factory */ public void setLineSetFactory(final IImageLineSetFactory extends IImageLine> factory) { imageLineSetFactory = factory; } /** * By default this uses the factory (which, by default creates * ImageLineInt). You should rarely override this. *
* See doc in
* {@link IImageLineSetFactory#create(ImageInfo, boolean, int, int, int)}
*/
protected IImageLineSet extends IImageLine> createLineSet(final boolean singleCursor, final int nlines,
final int noffset, final int step) {
return imageLineSetFactory.create(getCurImgInfo(), singleCursor, nlines, noffset, step);
}
protected void loadAllInterlaced(final int nRows, final int rowOffset, final int rowStep) {
final 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);
final int rowNumreal = idat.rowinfo.rowNreal;
final 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).
* These are the bytes read (not loaded) in the input stream. If exceeded,
* an exception will be thrown.
*/
public void setMaxTotalBytesRead(final long maxTotalBytesToRead) {
chunkseq.setMaxTotalBytesRead(maxTotalBytesToRead);
}
/**
* Set total maximum bytes to load from ancillary chunks (0: unlimited;
* default: 5Mb).
* If exceeded, some chunks will be skipped
*/
public void setMaxBytesMetadata(final long maxBytesMetadata) {
chunkseq.setMaxBytesMetadata(maxBytesMetadata);
}
/**
* Set maximum size in bytes for individual ancillary chunks (0: unlimited;
* default: 2MB).
* 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(final long skipChunkMaxSize) {
chunkseq.setSkipChunkMaxSize(skipChunkMaxSize);
}
/**
* Chunks ids to be skipped.
* 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(final String... chunksToSkip) {
chunkseq.setChunksToSkip(chunksToSkip);
}
public void addChunkToSkip(final String chunkToSkip) {
chunkseq.addChunkToSkip(chunkToSkip);
}
public void dontSkipChunk(final String chunkToSkip) {
chunkseq.dontSkipChunk(chunkToSkip);
}
/**
* if true, input stream will be closed after ending read
*
* default=true */ public void setShouldCloseStream(final boolean shouldCloseStream) { streamFeeder.setCloseStream(shouldCloseStream); } /** * Reads till end of PNG stream and call close() * * 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 close() */ 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 (final 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. */ @Override 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(); } }