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.ImageLine.SampleType; import jogamp.opengl.util.pngj.chunks.ChunkCopyBehaviour; import jogamp.opengl.util.pngj.chunks.ChunkHelper; import jogamp.opengl.util.pngj.chunks.ChunksList; import jogamp.opengl.util.pngj.chunks.ChunksListForWrite; 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.PngChunkSkipped; import jogamp.opengl.util.pngj.chunks.PngChunkTextVar; import jogamp.opengl.util.pngj.chunks.PngMetadata; /** * Writes a PNG image */ public class PngWriter { public final ImageInfo imgInfo; private final String filename; // optional, can be a description /** * last read row number, starting from 0 */ protected int rowNum = -1; private final ChunksListForWrite chunksList; private final PngMetadata metadata; // high level wrapper over chunkList /** * Current chunk grounp, (0-6) already read or reading *
* see {@link ChunksList} */ protected int currentChunkGroup = -1; /** * PNG filter strategy */ protected FilterWriteStrategy filterStrat; /** * zip compression level 0 - 9 */ private int compLevel = 6; private boolean shouldCloseStream = true; // true: closes stream after ending write private PngIDatChunkOutputStream datStream; private DeflaterOutputStream datStreamDeflated; /** * Deflate algortithm compression strategy */ private int deflaterStrategy = Deflater.FILTERED; private int[] histox = new int[256]; // auxiliar buffer, only used by reportResultsForFilter private int idatMaxSize = 0; // 0=use default (PngIDatChunkOutputStream 32768) private final OutputStream os; protected byte[] rowb = null; // element 0 is filter type! protected byte[] rowbfilter = null; // current line with filter protected byte[] rowbprev = null; // rowb prev // this only influences the 1-2-4 bitdepth format - and if we pass a ImageLine to writeRow, this is ignored private boolean unpackedMode = false; public PngWriter(OutputStream outputStream, ImageInfo imgInfo) { this(outputStream, imgInfo, "[NO FILENAME AVAILABLE]"); } /** * Constructs a new PngWriter from a output stream. After construction nothing is writen yet. You still can set some * parameters (compression, filters) and queue chunks before start writing the pixels. *
* See also
* 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 >= ChunksList.CHUNK_GROUP_4_IDAT;
if (onlyAfterIdat && reader.getCurrentChunkGroup() < ChunksList.CHUNK_GROUP_6_END)
throw new PngjExceptionInternal("tried to copy last chunks but reader has not ended");
for (PngChunk chunk : reader.getChunksList().getChunks()) {
int group = chunk.getChunkGroup();
if (group < ChunksList.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 (chunk instanceof PngChunkSkipped)
copy = false;
}
if (copy) {
chunksList.queue(PngChunk.cloneChunk(chunk, imgInfo));
}
}
}
/**
* 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
* Actually: compressed size = total size of IDAT data , raw size = uncompressed pixel bytes = rows * (bytesPerRow +
* 1).
*
* This must be called after pngw.end()
*/
public double computeCompressionRatio() {
if (currentChunkGroup < ChunksList.CHUNK_GROUP_6_END)
throw new PngjOutputException("must be called after end()");
double compressed = (double) datStream.getCountFlushed();
double raw = (imgInfo.bytesPerRow + 1) * imgInfo.rows;
return compressed / raw;
}
/**
* Finalizes the image creation and closes the stream. This MUST be called after writing the lines.
*/
public void end() {
if (rowNum != imgInfo.rows - 1)
throw new PngjOutputException("all rows have not been written");
try {
datStreamDeflated.finish();
datStream.flush();
writeLastChunks();
writeEndChunk();
if (shouldCloseStream)
os.close();
} catch (IOException e) {
throw new PngjOutputException(e);
}
}
/**
* returns the chunks list (queued and writen chunks)
*/
public ChunksListForWrite getChunksList() {
return chunksList;
}
/**
* Filename or description, from the optional constructor argument.
*/
public String getFilename() {
return filename;
}
/**
* High level wrapper over chunksList for metadata handling
*/
public PngMetadata getMetadata() {
return metadata;
}
/**
* Sets compression level of ZIP algorithm.
*
* 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 PngjOutputException("Compression level invalid (" + compLevel + ") Must be 0..9");
this.compLevel = compLevel;
}
/**
* Sets internal prediction filter type, or strategy to choose it.
*
* 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
*
* @param idatMaxSize
* default=0 : use defaultSize (32K)
*/
public void setIdatMaxSize(int idatMaxSize) {
this.idatMaxSize = idatMaxSize;
}
/**
* if true, input stream will be closed after ending write
*
* default=true
*/
public void setShouldCloseStream(boolean shouldCloseStream) {
this.shouldCloseStream = shouldCloseStream;
}
/**
* Deflater strategy: one of Deflater.FILTERED Deflater.HUFFMAN_ONLY Deflater.DEFAULT_STRATEGY
*
* Default: Deflater.FILTERED . This should be changed very rarely.
*/
public void setDeflaterStrategy(int deflaterStrategy) {
this.deflaterStrategy = deflaterStrategy;
}
/**
* Writes line, checks that the row number is consistent with that of the ImageLine See writeRow(int[] newrow, int
* rown)
*
* @deprecated Better use writeRow(ImageLine imgline, int rownumber)
*/
public void writeRow(ImageLine imgline) {
writeRow(imgline.scanline, imgline.getRown());
}
/**
* Writes line. See writeRow(int[] newrow, int rown)
*
* The packed flag of the imageline is honoured!
*
* @see #writeRowInt(int[], int)
*/
public void writeRow(ImageLine imgline, int rownumber) {
unpackedMode = imgline.samplesUnpacked;
if (imgline.sampleType == SampleType.INT)
writeRowInt(imgline.scanline, rownumber);
else
writeRowByte(imgline.scanlineb, rownumber);
}
/**
* Same as writeRow(int[] newrow, int rown), but does not check row number
*
* @param newrow
*/
public void writeRow(int[] newrow) {
writeRow(newrow, -1);
}
/**
* Alias to writeRowInt
*
* @see #writeRowInt(int[], int)
*/
public void writeRow(int[] newrow, int rown) {
writeRowInt(newrow, rown);
}
/**
* Writes a full image row.
*
* This must be called sequentially from n=0 to n=rows-1 One integer per sample , in the natural order: R G B R G B
* ... (or R G B A R G B A... if has alpha) The values should be between 0 and 255 for 8 bitspc images, and between
* 0- 65535 form 16 bitspc images (this applies also to the alpha channel if present) The array can be reused.
*
* Warning: the array might be modified in some cases (unpacked row with low bitdepth)
*
*
* @param newrow
* Array of pixel values. Warning: the array size should be exact (samplesPerRowP)
* @param rown
* Row number, from 0 (top) to rows-1 (bottom). This is just used as a check. Pass -1 if you want to
* autocompute it
*/
public void writeRowInt(int[] newrow, int rown) {
prepareEncodeRow(rown);
encodeRowFromInt(newrow);
filterAndSend(rown);
}
/**
* Same semantics as writeRowInt but using bytes. Each byte is still a sample. If 16bitdepth, we are passing only
* the most significant byte (and hence losing some info)
*
* @see PngWriter#writeRowInt(int[], int)
*/
public void writeRowByte(byte[] newrow, int rown) {
prepareEncodeRow(rown);
encodeRowFromByte(newrow);
filterAndSend(rown);
}
/**
* Writes all the pixels, calling writeRowInt() for each image row
*/
public void writeRowsInt(int[][] image) {
for (int i = 0; i < imgInfo.rows; i++)
writeRowInt(image[i], i);
}
/**
* Writes all the pixels, calling writeRowByte() for each image row
*/
public void writeRowsByte(byte[][] image) {
for (int i = 0; i < imgInfo.rows; i++)
writeRowByte(image[i], i);
}
public boolean isUnpackedMode() {
return unpackedMode;
}
/**
* If false (default), and image has bitdepth 1-2-4, the scanlines passed are assumed to be already packed.
*
* If true, each element is a sample, the writer will perform the packing if necessary.
*
* Warning: when using {@link #writeRow(ImageLine, int)} (recommended) the packed flag of the ImageLine
* object overrides (and overwrites!) this field.
*/
public void setUseUnPackedMode(boolean useUnpackedMode) {
this.unpackedMode = useUnpackedMode;
}
}
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
rowb = new byte[imgInfo.bytesPerRow + 1];
rowbprev = new byte[rowb.length];
rowbfilter = new byte[rowb.length];
chunksList = new ChunksListForWrite(imgInfo);
metadata = new PngMetadata(chunksList);
filterStrat = new FilterWriteStrategy(imgInfo, FilterType.FILTER_DEFAULT); // can be changed
}
private void init() {
datStream = new PngIDatChunkOutputStream(this.os, idatMaxSize);
Deflater def = new Deflater(compLevel);
def.setStrategy(deflaterStrategy);
datStreamDeflated = new DeflaterOutputStream(datStream, def);
writeSignatureAndIHDR();
writeFirstChunks();
}
private void reportResultsForFilter(int rown, FilterType type, boolean tentative) {
Arrays.fill(histox, 0);
int s = 0, v;
for (int i = 1; i <= imgInfo.bytesPerRow; i++) {
v = rowbfilter[i];
if (v < 0)
s -= (int) v;
else
s += (int) v;
histox[v & 0xFF]++;
}
filterStrat.fillResultsForFilter(rown, type, s, histox, tentative);
}
private void writeEndChunk() {
PngChunkIEND c = new PngChunkIEND(imgInfo);
c.createRawChunk().writeChunk(os);
}
private void writeFirstChunks() {
int nw = 0;
currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR;
nw = chunksList.writeChunks(os, currentChunkGroup);
currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE;
nw = chunksList.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 = ChunksList.CHUNK_GROUP_3_AFTERPLTE;
nw = chunksList.writeChunks(os, currentChunkGroup);
currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT;
}
private void writeLastChunks() { // not including end
currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT;
chunksList.writeChunks(os, currentChunkGroup);
// should not be unwriten chunks
ListChunksToWrite.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);
}
/**
* Computes compressed size/raw size, approximate.
* PngFilterType
) Recommended
* values: DEFAULT (default) or AGGRESIVE
*/
public void setFilterType(FilterType filterType) {
filterStrat = new FilterWriteStrategy(imgInfo, filterType);
}
/**
* Sets maximum size of IDAT fragments. This has little effect on performance you should rarely call this
*