package ar.com.hjg.pngj; import java.io.File; import java.io.InputStream; import java.util.List; import ar.com.hjg.pngj.chunks.PngChunk; import ar.com.hjg.pngj.chunks.PngChunkACTL; import ar.com.hjg.pngj.chunks.PngChunkFCTL; import ar.com.hjg.pngj.chunks.PngChunkFDAT; import ar.com.hjg.pngj.chunks.PngChunkIDAT; /** */ public class PngReaderApng extends PngReaderByte { public PngReaderApng(final File file) { super(file); dontSkipChunk(PngChunkFCTL.ID); } public PngReaderApng(final InputStream inputStream) { super(inputStream); dontSkipChunk(PngChunkFCTL.ID); } private Boolean apngKind = null; private boolean firsIdatApngFrame = false; protected PngChunkACTL actlChunk; // null if not APNG private PngChunkFCTL fctlChunk; // current (null for the pseudo still frame) /** * Current frame number (reading or read). First animated frame is 0. Frame * -1 represents the IDAT (default image) * when it's not part of the animation */ protected int frameNum = -1; // incremented after each fctl finding public boolean isApng() { if (apngKind == null) { // this triggers the loading of first chunks; actlChunk = (PngChunkACTL) getChunksList().getById1(PngChunkACTL.ID); // null if not apng apngKind = actlChunk != null; firsIdatApngFrame = fctlChunk != null; } return apngKind.booleanValue(); } public void advanceToFrame(final int frame) { if (frame < frameNum) throw new PngjInputException("Cannot go backwards"); if (frame >= getApngNumFrames()) throw new PngjInputException("Frame out of range " + frame); if (frame > frameNum) { addChunkToSkip(PngChunkIDAT.ID); addChunkToSkip(PngChunkFDAT.ID); if (chunkseq.getIdatSet() != null && !chunkseq.getIdatSet().isDone()) chunkseq.getIdatSet().done(); // seems to be necessary sometimes (we should check this) while (frameNum < frame & !chunkseq.isDone()) if (streamFeeder.feed(chunkseq) <= 0) break; } if (frame == frameNum) { // prepare to read rows. at this point we have a new dontSkipChunk(PngChunkIDAT.ID); dontSkipChunk(PngChunkFDAT.ID); rowNum = -1; imlinesSet = null;// force recreation (this is slightly dirty) // seek the next IDAT/fDAT - TODO: set the expected sequence number while (!chunkseq.isDone() && !chunkseq.getCurChunkReader().isFromDeflatedSet()) if (streamFeeder.feed(chunkseq) <= 0) break; } else throw new PngjInputException("unexpected error seeking from frame " + frame); } /** * True if it has a default image (IDAT) that is not part of the animation. * In that case, we consider it as a * pseudo-frame (number -1) */ public boolean hasExtraStillImage() { return isApng() && !firsIdatApngFrame; } /** * Only counts true animation frames. */ public int getApngNumFrames() { if (isApng()) return actlChunk.getNumFrames(); else return 0; } /** * 0 if it's to been played infinitely. -1 if not APNG */ public int getApngNumPlays() { if (isApng()) return actlChunk.getNumPlays(); else return -1; } @Override public IImageLine readRow() { // TODO Auto-generated method stub return super.readRow(); } @Override public boolean hasMoreRows() { // TODO Auto-generated method stub return super.hasMoreRows(); } @Override public IImageLine readRow(final int nrow) { // TODO Auto-generated method stub return super.readRow(nrow); } @Override public IImageLineSet readRows() { // TODO Auto-generated method stub return super.readRows(); } @Override public IImageLineSet readRows(final int nRows, final int rowOffset, final int rowStep) { // TODO Auto-generated method stub return super.readRows(nRows, rowOffset, rowStep); } @Override public void readSkippingAllRows() { // TODO Auto-generated method stub super.readSkippingAllRows(); } @Override protected ChunkSeqReaderPng createChunkSeqReader() { final ChunkSeqReaderPng cr = new ChunkSeqReaderPng(false) { @Override public boolean shouldSkipContent(final int len, final String id) { return super.shouldSkipContent(len, id); } @Override protected boolean isIdatKind(final String id) { return id.equals(PngChunkIDAT.ID) || id.equals(PngChunkFDAT.ID); } @Override protected DeflatedChunksSet createIdatSet(final String id) { final IdatSet ids = new IdatSet(id, getCurImgInfo(), deinterlacer); ids.setCallbackMode(callbackMode); return ids; } @Override protected void startNewChunk(final int len, final String id, final long offset) { super.startNewChunk(len, id, offset); } @Override protected void postProcessChunk(final ChunkReader chunkR) { super.postProcessChunk(chunkR); if (chunkR.getChunkRaw().id.equals(PngChunkFCTL.ID)) { frameNum++; final List chunkslist = chunkseq.getChunks(); fctlChunk = (PngChunkFCTL) chunkslist.get(chunkslist.size() - 1); // as this is slightly dirty, we check if (chunkR.getChunkRaw().getOffset() != fctlChunk.getRaw().getOffset()) throw new PngjInputException("something went wrong"); final ImageInfo frameInfo = fctlChunk.getEquivImageInfo(); getChunkseq().updateCurImgInfo(frameInfo); } } @SuppressWarnings("unlikely-arg-type") @Override protected boolean countChunkTypeAsAncillary(final String id) { // we don't count fdat as ancillary data return super.countChunkTypeAsAncillary(id) && !id.equals(id.equals(PngChunkFDAT.ID)); } }; return cr; } /** * @see #frameNum */ public int getFrameNum() { return frameNum; } @Override public void end() { // TODO Auto-generated method stub super.end(); } public PngChunkFCTL getFctl() { return fctlChunk; } }