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 imlinesSet; /** * This factory decides the concrete type of the ImageLine that will be * used. See {@link ImageLineSetDefault} for * examples */ private IImageLineSetFactory 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(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 * shouldCloseStream=true, 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 *

* 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(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) *

* 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(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 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 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. *

* See also {@link #createLineSet(boolean, int, int, int)} * * @param factory */ public void setLineSetFactory(IImageLineSetFactory 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 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).
* 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).
* 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).
* 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.
* 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 *

* default=true */ public void setShouldCloseStream(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 (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(); } }