package jogamp.opengl.util.pngj; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.zip.InflaterInputStream; import jogamp.opengl.util.pngj.PngIDatChunkInputStream.IdatChunkInfo; import jogamp.opengl.util.pngj.chunks.ChunkHelper; import jogamp.opengl.util.pngj.chunks.ChunkList; import jogamp.opengl.util.pngj.chunks.ChunkLoadBehaviour; import jogamp.opengl.util.pngj.chunks.ChunkRaw; import jogamp.opengl.util.pngj.chunks.PngChunk; import jogamp.opengl.util.pngj.chunks.PngChunkIHDR; import jogamp.opengl.util.pngj.chunks.PngMetadata; /** * Reads a PNG image, line by line */ public class PngReader { /** * Basic image info - final and inmutable. */ public final ImageInfo imgInfo; protected final String filename; // not necesarily a file, can be a description - merely informative private static int MAX_BYTES_CHUNKS_TO_LOAD = 640000; private ChunkLoadBehaviour chunkLoadBehaviour = ChunkLoadBehaviour.LOAD_CHUNK_ALWAYS; private final InputStream is; private InflaterInputStream idatIstream; private PngIDatChunkInputStream iIdatCstream; protected int currentChunkGroup = -1; protected int rowNum = -1; // current row number private int offset = 0; private int bytesChunksLoaded; // bytes loaded from anciallary chunks protected ImageLine imgLine; // line as bytes, counting from 1 (index 0 is reserved for filter type) protected byte[] rowb = null; protected byte[] rowbprev = null; // rowb previous protected byte[] rowbfilter = null; // current line 'filtered': exactly as in uncompressed stream /** * All chunks loaded. Criticals are included, except that all IDAT chunks appearance are replaced by a single * dummy-marker IDAT chunk. These might be copied to the PngWriter */ private final ChunkList chunksList; private final PngMetadata metadata; // this a wrapper over chunks /** * Constructs a PngReader from an InputStream. *

* See also FileHelper.createPngReader(File f) if available. * * Reads only the signature and first chunk (IDHR) * * @param filenameOrDescription * : Optional, can be a filename or a description. Just for error/debug messages * */ public PngReader(InputStream inputStream, String filenameOrDescription) { this.filename = filenameOrDescription == null ? "" : filenameOrDescription; this.is = inputStream; this.chunksList = new ChunkList(null); this.metadata = new PngMetadata(chunksList, true); // reads header (magic bytes) byte[] pngid = new byte[PngHelper.pngIdBytes.length]; PngHelper.readBytes(is, pngid, 0, pngid.length); offset += pngid.length; if (!Arrays.equals(pngid, PngHelper.pngIdBytes)) throw new PngjInputException("Bad PNG signature"); // reads first chunk currentChunkGroup = ChunkList.CHUNK_GROUP_0_IDHR; int clen = PngHelper.readInt4(is); offset += 4; if (clen != 13) throw new RuntimeException("IDHR chunk len != 13 ?? " + clen); byte[] chunkid = new byte[4]; PngHelper.readBytes(is, chunkid, 0, 4); if (!Arrays.equals(chunkid, ChunkHelper.b_IHDR)) throw new PngjInputException("IHDR not found as first chunk??? [" + ChunkHelper.toString(chunkid) + "]"); offset += 4; ChunkRaw chunk = new ChunkRaw(clen, chunkid, true); String chunkids = ChunkHelper.toString(chunkid); offset += chunk.readChunkData(is); PngChunkIHDR ihdr = (PngChunkIHDR) addChunkToList(chunk); boolean alpha = (ihdr.getColormodel() & 0x04) != 0; boolean palette = (ihdr.getColormodel() & 0x01) != 0; boolean grayscale = (ihdr.getColormodel() == 0 || ihdr.getColormodel() == 4); imgInfo = new ImageInfo(ihdr.getCols(), ihdr.getRows(), ihdr.getBitspc(), alpha, grayscale, palette); imgLine = new ImageLine(imgInfo); if (ihdr.getInterlaced() != 0) throw new PngjUnsupportedException("PNG interlaced not supported by this library"); if (ihdr.getFilmeth() != 0 || ihdr.getCompmeth() != 0) throw new PngjInputException("compmethod o filtermethod unrecognized"); if (ihdr.getColormodel() < 0 || ihdr.getColormodel() > 6 || ihdr.getColormodel() == 1 || ihdr.getColormodel() == 5) throw new PngjInputException("Invalid colormodel " + ihdr.getColormodel()); if (ihdr.getBitspc() != 1 && ihdr.getBitspc() != 2 && ihdr.getBitspc() != 4 && ihdr.getBitspc() != 8 && ihdr.getBitspc() != 16) throw new PngjInputException("Invalid bit depth " + ihdr.getBitspc()); // allocation: one extra byte for filter type one pixel rowbfilter = new byte[imgInfo.bytesPerRow + 1]; rowb = new byte[imgInfo.bytesPerRow + 1]; rowbprev = new byte[rowb.length]; } private static class FoundChunkInfo { public final String id; public final int len; public final int offset; public final boolean loaded; private FoundChunkInfo(String id, int len, int offset, boolean loaded) { this.id = id; this.len = len; this.offset = offset; this.loaded = loaded; } public String toString() { return "chunk " + id + " len=" + len + " offset=" + offset + (this.loaded ? " " : " X "); } } private PngChunk addChunkToList(ChunkRaw chunk) { // this requires that the currentChunkGroup is ok PngChunk chunkType = PngChunk.factory(chunk, imgInfo); if (!chunkType.crit) { bytesChunksLoaded += chunk.len; } if (bytesChunksLoaded > MAX_BYTES_CHUNKS_TO_LOAD) { throw new PngjInputException("Chunk exceeded available space (" + MAX_BYTES_CHUNKS_TO_LOAD + ") chunk: " + chunk + " See PngReader.MAX_BYTES_CHUNKS_TO_LOAD\n"); } chunksList.appendReadChunk(chunkType, currentChunkGroup); return chunkType; } /** * Reads chunks before first IDAT. 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(), getChunks()) * **/ public void readFirstChunks() { if (!firstChunksNotYetRead()) return; int clen = 0; boolean found = false; byte[] chunkid = new byte[4]; // it's important to reallocate in each iteration currentChunkGroup = ChunkList.CHUNK_GROUP_1_AFTERIDHR; while (!found) { clen = PngHelper.readInt4(is); offset += 4; if (clen < 0) break; PngHelper.readBytes(is, chunkid, 0, 4); offset += 4; if (Arrays.equals(chunkid, ChunkHelper.b_IDAT)) { found = true; currentChunkGroup = ChunkList.CHUNK_GROUP_4_IDAT; // add dummy idat chunk to list ChunkRaw chunk = new ChunkRaw(0, chunkid, false); addChunkToList(chunk); break; } else if (Arrays.equals(chunkid, ChunkHelper.b_IEND)) { throw new PngjInputException("END chunk found before image data (IDAT) at offset=" + offset); } ChunkRaw chunk = new ChunkRaw(clen, chunkid, true); String chunkids = ChunkHelper.toString(chunkid); boolean loadchunk = ChunkHelper.shouldLoad(chunkids, chunkLoadBehaviour); offset += chunk.readChunkData(is); if (chunkids.equals(ChunkHelper.PLTE)) currentChunkGroup = ChunkList.CHUNK_GROUP_2_PLTE; if (loadchunk) addChunkToList(chunk); if (chunkids.equals(ChunkHelper.PLTE)) currentChunkGroup = ChunkList.CHUNK_GROUP_3_AFTERPLTE; } int idatLen = found ? clen : -1; if (idatLen < 0) throw new PngjInputException("first idat chunk not found!"); iIdatCstream = new PngIDatChunkInputStream(is, idatLen, offset); idatIstream = new InflaterInputStream(iIdatCstream); } /** * Reads (and processes) chunks after last IDAT. **/ private void readLastChunks() { // PngHelper.logdebug("idat ended? " + iIdatCstream.isEnded()); currentChunkGroup = ChunkList.CHUNK_GROUP_5_AFTERIDAT; if (!iIdatCstream.isEnded()) iIdatCstream.forceChunkEnd(); int clen = iIdatCstream.getLenLastChunk(); byte[] chunkid = iIdatCstream.getIdLastChunk(); boolean endfound = false; boolean first = true; boolean ignore = false; while (!endfound) { ignore = false; if (!first) { clen = PngHelper.readInt4(is); offset += 4; if (clen < 0) throw new PngjInputException("bad len " + clen); PngHelper.readBytes(is, chunkid, 0, 4); offset += 4; } first = false; if (Arrays.equals(chunkid, ChunkHelper.b_IDAT)) { // PngHelper.logdebug("extra IDAT chunk len - ignoring : "); ignore = true; } else if (Arrays.equals(chunkid, ChunkHelper.b_IEND)) { currentChunkGroup = ChunkList.CHUNK_GROUP_6_END; endfound = true; } ChunkRaw chunk = new ChunkRaw(clen, chunkid, true); String chunkids = ChunkHelper.toString(chunkid); boolean loadchunk = ChunkHelper.shouldLoad(chunkids, chunkLoadBehaviour); offset += chunk.readChunkData(is); if (loadchunk && !ignore) { addChunkToList(chunk); } } if (!endfound) throw new PngjInputException("end chunk not found - offset=" + offset); // PngHelper.logdebug("end chunk found ok offset=" + offset); } /** * Calls readRow(int[] buffer, int nrow) using internal ImageLine as buffer. This doesn't allocate or * copy anything. * * @return The ImageLine that also is available inside this object. */ public ImageLine readRow(int nrow) { readRow(imgLine.scanline, nrow); imgLine.filterUsed = FilterType.getByVal(rowbfilter[0]); imgLine.setRown(nrow); return imgLine; } /** * Reads a line and returns it as a int[] array. * * You can pass (optionally) a prealocatted buffer. * * @param buffer * Prealocated buffer, or null. * @param nrow * Row number (0 is top). This is mostly for checking, because this library reads rows in sequence. * * @return The scanline in the same passwd buffer if it was allocated, a newly allocated one otherwise */ public int[] readRow(int[] buffer, int nrow) { if (nrow < 0 || nrow >= imgInfo.rows) throw new PngjInputException("invalid line"); if (nrow != rowNum + 1) throw new PngjInputException("invalid line (expected: " + (rowNum + 1)); if (nrow == 0 && firstChunksNotYetRead()) readFirstChunks(); rowNum++; if (buffer == null || buffer.length < imgInfo.samplesPerRowP) buffer = new int[imgInfo.samplesPerRowP]; // swap byte[] tmp = rowb; rowb = rowbprev; rowbprev = tmp; // loads in rowbfilter "raw" bytes, with filter PngHelper.readBytes(idatIstream, rowbfilter, 0, rowbfilter.length); rowb[0] = 0; unfilterRow(); rowb[0] = rowbfilter[0]; convertRowFromBytes(buffer); return buffer; } /** * This should be called after having read the last line. It reads extra chunks after IDAT, if present. */ public void end() { offset = (int) iIdatCstream.getOffset(); try { idatIstream.close(); } catch (Exception e) { } readLastChunks(); try { is.close(); } catch (Exception e) { throw new PngjInputException("error closing input stream!", e); } } private void convertRowFromBytes(int[] buffer) { // http://www.libpng.org/pub/png/spec/1.2/PNG-DataRep.html int i, j; if (imgInfo.bitDepth <= 8) { for (i = 0, j = 1; i < imgInfo.samplesPerRowP; i++) { buffer[i] = (rowb[j++] & 0xFF); } } else { // 16 bitspc for (i = 0, j = 1; i < imgInfo.samplesPerRowP; i++) { buffer[i] = ((rowb[j++] & 0xFF) << 8) + (rowb[j++] & 0xFF); } } } private void unfilterRow() { int ftn = rowbfilter[0]; FilterType ft = FilterType.getByVal(ftn); if (ft == null) throw new PngjInputException("Filter type " + ftn + " invalid"); switch (ft) { case FILTER_NONE: unfilterRowNone(); break; case FILTER_SUB: unfilterRowSub(); break; case FILTER_UP: unfilterRowUp(); break; case FILTER_AVERAGE: unfilterRowAverage(); break; case FILTER_PAETH: unfilterRowPaeth(); break; default: throw new PngjInputException("Filter type " + ftn + " not implemented"); } } private void unfilterRowNone() { for (int i = 1; i <= imgInfo.bytesPerRow; i++) { rowb[i] = (byte) (rowbfilter[i]); } } private void unfilterRowSub() { int i, j; for (i = 1; i <= imgInfo.bytesPixel; i++) { rowb[i] = (byte) (rowbfilter[i]); } for (j = 1, i = imgInfo.bytesPixel + 1; i <= imgInfo.bytesPerRow; i++, j++) { rowb[i] = (byte) (rowbfilter[i] + rowb[j]); } } private void unfilterRowUp() { for (int i = 1; i <= imgInfo.bytesPerRow; i++) { rowb[i] = (byte) (rowbfilter[i] + rowbprev[i]); } } private void unfilterRowAverage() { int i, j, x; for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imgInfo.bytesPerRow; i++, j++) { x = j > 0 ? (rowb[j] & 0xff) : 0; rowb[i] = (byte) (rowbfilter[i] + (x + (rowbprev[i] & 0xFF)) / 2); } } private void unfilterRowPaeth() { int i, j, x, y; for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imgInfo.bytesPerRow; i++, j++) { x = j > 0 ? (rowb[j] & 0xFF) : 0; y = j > 0 ? (rowbprev[j] & 0xFF) : 0; rowb[i] = (byte) (rowbfilter[i] + FilterType.filterPaethPredictor(x, rowbprev[i] & 0xFF, y)); } } public ChunkLoadBehaviour getChunkLoadBehaviour() { return chunkLoadBehaviour; } public void setChunkLoadBehaviour(ChunkLoadBehaviour chunkLoadBehaviour) { this.chunkLoadBehaviour = chunkLoadBehaviour; } private boolean firstChunksNotYetRead() { return currentChunkGroup < ChunkList.CHUNK_GROUP_1_AFTERIDHR; } public ChunkList getChunksList() { if (firstChunksNotYetRead()) readFirstChunks(); return chunksList; } public PngMetadata getMetadata() { if (firstChunksNotYetRead()) readFirstChunks(); return metadata; } public String toString() { // basic info return "filename=" + filename + " " + imgInfo.toString(); } }