311 lines
9.1 KiB
Java
311 lines
9.1 KiB
Java
package ar.com.hjg.pngj;
|
|
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode;
|
|
import ar.com.hjg.pngj.chunks.ChunkFactory;
|
|
import ar.com.hjg.pngj.chunks.ChunkHelper;
|
|
import ar.com.hjg.pngj.chunks.ChunkLoadBehaviour;
|
|
import ar.com.hjg.pngj.chunks.ChunksList;
|
|
import ar.com.hjg.pngj.chunks.PngChunk;
|
|
import ar.com.hjg.pngj.chunks.PngChunkIDAT;
|
|
import ar.com.hjg.pngj.chunks.PngChunkIEND;
|
|
import ar.com.hjg.pngj.chunks.PngChunkIHDR;
|
|
import ar.com.hjg.pngj.chunks.PngChunkPLTE;
|
|
|
|
/**
|
|
* Adds to ChunkSeqReader the storing of PngChunk, with a PngFactory, and
|
|
* imageInfo + deinterlacer.
|
|
* <p>
|
|
* Most usual PNG reading should use this class, or a {@link PngReader}, which
|
|
* is a thin wrapper over this.
|
|
*/
|
|
public class ChunkSeqReaderPng extends ChunkSeqReader {
|
|
|
|
protected ImageInfo imageInfo; // initialized at parsing the IHDR
|
|
protected ImageInfo curImageInfo; // can vary, for apng
|
|
protected Deinterlacer deinterlacer;
|
|
protected int currentChunkGroup = -1;
|
|
|
|
/**
|
|
* All chunks, but some of them can have the buffer empty (IDAT and skipped)
|
|
*/
|
|
protected ChunksList chunksList = null;
|
|
protected final boolean callbackMode;
|
|
private long bytesAncChunksLoaded = 0; // bytes loaded from buffered chunks non-critical chunks (data only)
|
|
|
|
private boolean checkCrc = true;
|
|
|
|
// --- parameters to be set prior to reading ---
|
|
private boolean includeNonBufferedChunks = false;
|
|
|
|
private final Set<String> chunksToSkip = new HashSet<>();
|
|
private long maxTotalBytesRead = 0;
|
|
private long skipChunkMaxSize = 0;
|
|
private long maxBytesMetadata = 0;
|
|
private IChunkFactory chunkFactory;
|
|
private ChunkLoadBehaviour chunkLoadBehaviour = ChunkLoadBehaviour.LOAD_CHUNK_ALWAYS;
|
|
|
|
public ChunkSeqReaderPng(final boolean callbackMode) {
|
|
super();
|
|
this.callbackMode = callbackMode;
|
|
chunkFactory = new ChunkFactory(); // default factory
|
|
}
|
|
|
|
private void updateAndCheckChunkGroup(final String id) {
|
|
if (id.equals(PngChunkIHDR.ID)) { // IDHR
|
|
if (currentChunkGroup < 0)
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR;
|
|
else
|
|
throw new PngjInputException("unexpected chunk " + id);
|
|
} else if (id.equals(PngChunkPLTE.ID)) { // PLTE
|
|
if (currentChunkGroup == ChunksList.CHUNK_GROUP_0_IDHR || currentChunkGroup == ChunksList.CHUNK_GROUP_1_AFTERIDHR)
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE;
|
|
else
|
|
throw new PngjInputException("unexpected chunk " + id);
|
|
} else if (id.equals(PngChunkIDAT.ID)) { // IDAT (no necessarily the first)
|
|
if (currentChunkGroup >= ChunksList.CHUNK_GROUP_0_IDHR && currentChunkGroup <= ChunksList.CHUNK_GROUP_4_IDAT)
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT;
|
|
else
|
|
throw new PngjInputException("unexpected chunk " + id);
|
|
} else if (id.equals(PngChunkIEND.ID)) { // END
|
|
if (currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT)
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_6_END;
|
|
else
|
|
throw new PngjInputException("unexpected chunk " + id);
|
|
} else if (currentChunkGroup <= ChunksList.CHUNK_GROUP_1_AFTERIDHR)
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR;
|
|
else if (currentChunkGroup <= ChunksList.CHUNK_GROUP_3_AFTERPLTE)
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE;
|
|
else
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT;
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldSkipContent(final int len, final String id) {
|
|
if (super.shouldSkipContent(len, id))
|
|
return true;
|
|
if (ChunkHelper.isCritical(id))
|
|
return false;// critical chunks are never skipped
|
|
if (maxTotalBytesRead > 0 && len + getBytesCount() > maxTotalBytesRead)
|
|
throw new PngjInputException("Maximum total bytes to read exceeeded: " + maxTotalBytesRead + " offset:" + getBytesCount() + " len=" + len);
|
|
if (chunksToSkip.contains(id))
|
|
return true; // specific skip
|
|
if (skipChunkMaxSize > 0 && len > skipChunkMaxSize)
|
|
return true; // too big chunk
|
|
if (maxBytesMetadata > 0 && len > maxBytesMetadata - bytesAncChunksLoaded)
|
|
return true; // too much ancillary chunks loaded
|
|
switch (chunkLoadBehaviour) {
|
|
case LOAD_CHUNK_IF_SAFE:
|
|
if (!ChunkHelper.isSafeToCopy(id))
|
|
return true;
|
|
break;
|
|
case LOAD_CHUNK_NEVER:
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public long getBytesChunksLoaded() {
|
|
return bytesAncChunksLoaded;
|
|
}
|
|
|
|
public int getCurrentChunkGroup() {
|
|
return currentChunkGroup;
|
|
}
|
|
|
|
public void setChunksToSkip(final String... chunksToSkip) {
|
|
this.chunksToSkip.clear();
|
|
for (final String c : chunksToSkip)
|
|
this.chunksToSkip.add(c);
|
|
}
|
|
|
|
public void addChunkToSkip(final String chunkToSkip) {
|
|
chunksToSkip.add(chunkToSkip);
|
|
}
|
|
|
|
public void dontSkipChunk(final String chunkToSkip) {
|
|
chunksToSkip.remove(chunkToSkip);
|
|
}
|
|
|
|
public boolean firstChunksNotYetRead() {
|
|
return getCurrentChunkGroup() < ChunksList.CHUNK_GROUP_4_IDAT;
|
|
}
|
|
|
|
@Override
|
|
protected void postProcessChunk(final ChunkReader chunkR) {
|
|
super.postProcessChunk(chunkR);
|
|
if (chunkR.getChunkRaw().id.equals(PngChunkIHDR.ID)) {
|
|
final PngChunkIHDR ch = new PngChunkIHDR(null);
|
|
ch.parseFromRaw(chunkR.getChunkRaw());
|
|
imageInfo = ch.createImageInfo();
|
|
curImageInfo = imageInfo;
|
|
if (ch.isInterlaced())
|
|
deinterlacer = new Deinterlacer(curImageInfo);
|
|
chunksList = new ChunksList(imageInfo);
|
|
}
|
|
if (chunkR.mode == ChunkReaderMode.BUFFER && countChunkTypeAsAncillary(chunkR.getChunkRaw().id))
|
|
bytesAncChunksLoaded += chunkR.getChunkRaw().len;
|
|
if (chunkR.mode == ChunkReaderMode.BUFFER || includeNonBufferedChunks) {
|
|
final PngChunk chunk = chunkFactory.createChunk(chunkR.getChunkRaw(), getImageInfo());
|
|
chunksList.appendReadChunk(chunk, currentChunkGroup);
|
|
}
|
|
if (isDone())
|
|
processEndPng();
|
|
}
|
|
|
|
protected boolean countChunkTypeAsAncillary(final String id) {
|
|
return !ChunkHelper.isCritical(id);
|
|
}
|
|
|
|
@Override
|
|
protected DeflatedChunksSet createIdatSet(final String id) {
|
|
final IdatSet ids = new IdatSet(id, getCurImgInfo(), deinterlacer);
|
|
ids.setCallbackMode(callbackMode);
|
|
return ids;
|
|
}
|
|
|
|
public IdatSet getIdatSet() {
|
|
final DeflatedChunksSet c = getCurReaderDeflatedSet();
|
|
return c instanceof IdatSet ? (IdatSet) c : null;
|
|
}
|
|
|
|
@Override
|
|
protected boolean isIdatKind(final String id) {
|
|
return id.equals(PngChunkIDAT.ID);
|
|
}
|
|
|
|
@Override
|
|
public int consume(final byte[] buf, final int off, final int len) {
|
|
return super.consume(buf, off, len);
|
|
}
|
|
|
|
/**
|
|
* sets a custom chunk factory. This is typically called with a custom class
|
|
* extends ChunkFactory, to adds custom
|
|
* chunks to the default well-know ones
|
|
*
|
|
* @param chunkFactory
|
|
*/
|
|
public void setChunkFactory(final IChunkFactory chunkFactory) {
|
|
this.chunkFactory = chunkFactory;
|
|
}
|
|
|
|
/**
|
|
* Things to be done after IEND processing. This is not called if
|
|
* prematurely closed.
|
|
*/
|
|
protected void processEndPng() {
|
|
// nothing to do
|
|
}
|
|
|
|
public ImageInfo getImageInfo() {
|
|
return imageInfo;
|
|
}
|
|
|
|
public boolean isInterlaced() {
|
|
return deinterlacer != null;
|
|
}
|
|
|
|
public Deinterlacer getDeinterlacer() {
|
|
return deinterlacer;
|
|
}
|
|
|
|
@Override
|
|
protected void startNewChunk(final int len, final String id, final long offset) {
|
|
updateAndCheckChunkGroup(id);
|
|
super.startNewChunk(len, id, offset);
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
if (currentChunkGroup != ChunksList.CHUNK_GROUP_6_END)// this could only happen if forced close
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_6_END;
|
|
super.close();
|
|
}
|
|
|
|
public List<PngChunk> getChunks() {
|
|
return chunksList.getChunks();
|
|
}
|
|
|
|
public void setMaxTotalBytesRead(final long maxTotalBytesRead) {
|
|
this.maxTotalBytesRead = maxTotalBytesRead;
|
|
}
|
|
|
|
public long getSkipChunkMaxSize() {
|
|
return skipChunkMaxSize;
|
|
}
|
|
|
|
public void setSkipChunkMaxSize(final long skipChunkMaxSize) {
|
|
this.skipChunkMaxSize = skipChunkMaxSize;
|
|
}
|
|
|
|
public long getMaxBytesMetadata() {
|
|
return maxBytesMetadata;
|
|
}
|
|
|
|
public void setMaxBytesMetadata(final long maxBytesMetadata) {
|
|
this.maxBytesMetadata = maxBytesMetadata;
|
|
}
|
|
|
|
public long getMaxTotalBytesRead() {
|
|
return maxTotalBytesRead;
|
|
}
|
|
|
|
@Override
|
|
protected boolean shouldCheckCrc(final int len, final String id) {
|
|
return checkCrc;
|
|
}
|
|
|
|
public boolean isCheckCrc() {
|
|
return checkCrc;
|
|
}
|
|
|
|
public void setCheckCrc(final boolean checkCrc) {
|
|
this.checkCrc = checkCrc;
|
|
}
|
|
|
|
public boolean isCallbackMode() {
|
|
return callbackMode;
|
|
}
|
|
|
|
public Set<String> getChunksToSkip() {
|
|
return chunksToSkip;
|
|
}
|
|
|
|
public void setChunkLoadBehaviour(final ChunkLoadBehaviour chunkLoadBehaviour) {
|
|
this.chunkLoadBehaviour = chunkLoadBehaviour;
|
|
}
|
|
|
|
public ImageInfo getCurImgInfo() {
|
|
return curImageInfo;
|
|
}
|
|
|
|
public void updateCurImgInfo(final ImageInfo iminfo) {
|
|
if (!iminfo.equals(curImageInfo))
|
|
curImageInfo = iminfo;
|
|
if (deinterlacer != null)
|
|
deinterlacer = new Deinterlacer(curImageInfo); // we could reset it, but...
|
|
}
|
|
|
|
/**
|
|
* If true, the chunks with no data (because skipped or because processed
|
|
* like IDAT-type) are still stored in the
|
|
* PngChunks list, which might be more informative.
|
|
*
|
|
* Setting this to false saves a few bytes
|
|
*
|
|
* Default: false
|
|
*
|
|
* @param includeNonBufferedChunks
|
|
*/
|
|
public void setIncludeNonBufferedChunks(final boolean includeNonBufferedChunks) {
|
|
this.includeNonBufferedChunks = includeNonBufferedChunks;
|
|
}
|
|
|
|
}
|