package jogamp.opengl.util.pngj; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.List; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import jogamp.opengl.util.pngj.chunks.ChunkCopyBehaviour; import jogamp.opengl.util.pngj.chunks.ChunkHelper; import jogamp.opengl.util.pngj.chunks.ChunkList; import jogamp.opengl.util.pngj.chunks.PngChunk; import jogamp.opengl.util.pngj.chunks.PngChunkIEND; import jogamp.opengl.util.pngj.chunks.PngChunkIHDR; import jogamp.opengl.util.pngj.chunks.PngChunkTextVar; import jogamp.opengl.util.pngj.chunks.PngMetadata; /** * Writes a PNG image, line by line. */ public class PngWriter { public final ImageInfo imgInfo; protected int compLevel = 6; // zip compression level 0 - 9 private int deflaterStrategy = Deflater.FILTERED; protected FilterWriteStrategy filterStrat; protected int currentChunkGroup = -1; protected int rowNum = -1; // current line number // current line, one (packed) sample per element (layout differnt from rowb!) protected int[] scanline = null; protected byte[] rowb = null; // element 0 is filter type! protected byte[] rowbprev = null; // rowb prev protected byte[] rowbfilter = null; // current line with filter protected final OutputStream os; protected final String filename; // optional, can be a description private PngIDatChunkOutputStream datStream; private DeflaterOutputStream datStreamDeflated; private final ChunkList chunkList; private final PngMetadata metadata; // high level wrapper over chunkList public PngWriter(OutputStream outputStream, ImageInfo imgInfo) { this(outputStream, imgInfo, "[NO FILENAME AVAILABLE]"); } /** * Constructs a new PngWriter from a output stream. *
* See also
* This must be called just after constructor, before starting writing.
*
* See also setCompLevel()
*
* @param filterType
* One of the five prediction types or strategy to choose it (see
* This must be called just after constructor, before starting writing.
*
* See also setFilterType()
*
* @param compLevel
* between 0 and 9 (default:6 , recommended: 6 or more)
*/
public void setCompLevel(int compLevel) {
if (compLevel < 0 || compLevel > 9)
throw new PngjException("Compression level invalid (" + compLevel + ") Must be 0..9");
this.compLevel = compLevel;
}
/**
* copy chunks from reader - copy_mask : see ChunksToWrite.COPY_XXX
*
* If we are after idat, only considers those chunks after IDAT in PngReader TODO: this should be more customizable
*/
private void copyChunks(PngReader reader, int copy_mask, boolean onlyAfterIdat) {
boolean idatDone = currentChunkGroup >= ChunkList.CHUNK_GROUP_4_IDAT;
for (PngChunk chunk : reader.getChunksList().getChunks()) {
int group = chunk.getChunkGroup();
if (group < ChunkList.CHUNK_GROUP_4_IDAT && idatDone)
continue;
boolean copy = false;
if (chunk.crit) {
if (chunk.id.equals(ChunkHelper.PLTE)) {
if (imgInfo.indexed && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_PALETTE))
copy = true;
if (!imgInfo.greyscale && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_ALL))
copy = true;
}
} else { // ancillary
boolean text = (chunk instanceof PngChunkTextVar);
boolean safe = chunk.safe;
// notice that these if are not exclusive
if (ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_ALL))
copy = true;
if (safe && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_ALL_SAFE))
copy = true;
if (chunk.id.equals(ChunkHelper.tRNS)
&& ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_TRANSPARENCY))
copy = true;
if (chunk.id.equals(ChunkHelper.pHYs) && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_PHYS))
copy = true;
if (text && ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_TEXTUAL))
copy = true;
if (ChunkHelper.maskMatch(copy_mask, ChunkCopyBehaviour.COPY_ALMOSTALL)
&& !(ChunkHelper.isUnknown(chunk) || text || chunk.id.equals(ChunkHelper.hIST) || chunk.id
.equals(ChunkHelper.tIME)))
copy = true;
}
if (copy) {
chunkList.queueChunk(PngChunk.cloneChunk(chunk, imgInfo), !chunk.allowsMultiple(), false);
}
}
}
/**
* Copies first (pre IDAT) ancillary chunks from a PngReader.
*
* Should be called when creating an image from another, before starting writing lines, to copy relevant chunks.
*
*
* @param reader
* : PngReader object, already opened.
* @param copy_mask
* : Mask bit (OR), see
* Should be called when creating an image from another, after writing all lines, before closing the writer, to copy
* additional chunks.
*
*
* @param reader
* : PngReader object, already opened and fully read.
* @param copy_mask
* : Mask bit (OR), see FileHelper.createPngWriter()
if available.
*
* @param outputStream
* Opened stream for binary writing
* @param imgInfo
* Basic image parameters
* @param filenameOrDescription
* Optional, just for error/debug messages
*/
public PngWriter(OutputStream outputStream, ImageInfo imgInfo, String filenameOrDescription) {
this.filename = filenameOrDescription == null ? "" : filenameOrDescription;
this.os = outputStream;
this.imgInfo = imgInfo;
// prealloc
scanline = new int[imgInfo.samplesPerRowP];
rowb = new byte[imgInfo.bytesPerRow + 1];
rowbprev = new byte[rowb.length];
rowbfilter = new byte[rowb.length];
datStream = new PngIDatChunkOutputStream(this.os);
chunkList = new ChunkList(imgInfo);
metadata = new PngMetadata(chunkList, false);
filterStrat = new FilterWriteStrategy(imgInfo, FilterType.FILTER_DEFAULT);
}
/**
* Write id signature and also "IHDR" chunk
*/
private void writeSignatureAndIHDR() {
currentChunkGroup = ChunkList.CHUNK_GROUP_0_IDHR;
if (datStreamDeflated == null) {
Deflater def = new Deflater(compLevel);
def.setStrategy(deflaterStrategy);
datStreamDeflated = new DeflaterOutputStream(datStream, def, 8192);
}
PngHelper.writeBytes(os, PngHelper.pngIdBytes); // signature
PngChunkIHDR ihdr = new PngChunkIHDR(imgInfo);
// http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
ihdr.setCols(imgInfo.cols);
ihdr.setRows(imgInfo.rows);
ihdr.setBitspc(imgInfo.bitDepth);
int colormodel = 0;
if (imgInfo.alpha)
colormodel += 0x04;
if (imgInfo.indexed)
colormodel += 0x01;
if (!imgInfo.greyscale)
colormodel += 0x02;
ihdr.setColormodel(colormodel);
ihdr.setCompmeth(0); // compression method 0=deflate
ihdr.setFilmeth(0); // filter method (0)
ihdr.setInterlaced(0); // we never interlace
ihdr.createChunk().writeChunk(os);
}
private void writeFirstChunks() {
int nw = 0;
currentChunkGroup = ChunkList.CHUNK_GROUP_1_AFTERIDHR;
nw = chunkList.writeChunks(os, currentChunkGroup);
currentChunkGroup = ChunkList.CHUNK_GROUP_2_PLTE;
nw = chunkList.writeChunks(os, currentChunkGroup);
if (nw > 0 && imgInfo.greyscale)
throw new PngjOutputException("cannot write palette for this format");
if (nw == 0 && imgInfo.indexed)
throw new PngjOutputException("missing palette");
currentChunkGroup = ChunkList.CHUNK_GROUP_3_AFTERPLTE;
nw = chunkList.writeChunks(os, currentChunkGroup);
currentChunkGroup = ChunkList.CHUNK_GROUP_4_IDAT;
}
private void writeLastChunks() { // not including end
currentChunkGroup = ChunkList.CHUNK_GROUP_5_AFTERIDAT;
chunkList.writeChunks(os, currentChunkGroup);
// should not be unwriten chunks
ListPngFilterType
) Recommended
* values: DEFAULT (default) or AGGRESIVE
*/
public void setFilterType(FilterType filterType) {
filterStrat = new FilterWriteStrategy(imgInfo, filterType);
}
/**
* Sets compression level of ZIP algorithm.
* ChunksToWrite.COPY_XXX
constants
*/
public void copyChunksFirst(PngReader reader, int copy_mask) {
copyChunks(reader, copy_mask, false);
}
/**
* Copies last (post IDAT) ancillary chunks from a PngReader.
* ChunksToWrite.COPY_XXX
constants
*/
public void copyChunksLast(PngReader reader, int copy_mask) {
copyChunks(reader, copy_mask, true);
}
public ChunkList getChunkList() {
return chunkList;
}
public PngMetadata getMetadata() {
return metadata;
}
}