457 lines
13 KiB
Java
457 lines
13 KiB
Java
package ar.com.hjg.pngj;
|
|
|
|
import java.io.File;
|
|
import java.io.OutputStream;
|
|
import java.util.List;
|
|
|
|
import ar.com.hjg.pngj.chunks.ChunkCopyBehaviour;
|
|
import ar.com.hjg.pngj.chunks.ChunkPredicate;
|
|
import ar.com.hjg.pngj.chunks.ChunksList;
|
|
import ar.com.hjg.pngj.chunks.ChunksListForWrite;
|
|
import ar.com.hjg.pngj.chunks.PngChunk;
|
|
import ar.com.hjg.pngj.chunks.PngChunkIEND;
|
|
import ar.com.hjg.pngj.chunks.PngChunkIHDR;
|
|
import ar.com.hjg.pngj.chunks.PngChunkPLTE;
|
|
import ar.com.hjg.pngj.chunks.PngMetadata;
|
|
import ar.com.hjg.pngj.pixels.PixelsWriter;
|
|
import ar.com.hjg.pngj.pixels.PixelsWriterDefault;
|
|
|
|
/**
|
|
* Writes a PNG image, line by line.
|
|
*/
|
|
public class PngWriter {
|
|
|
|
public final ImageInfo imgInfo;
|
|
|
|
/**
|
|
* last writen row number, starting from 0
|
|
*/
|
|
protected int rowNum = -1;
|
|
|
|
private final ChunksListForWrite chunksList;
|
|
|
|
private final PngMetadata metadata;
|
|
|
|
/**
|
|
* Current chunk grounp, (0-6) already written or currently writing (this is
|
|
* advanced when just starting to write the
|
|
* new group, not when finalizing the previous)
|
|
* <p>
|
|
* see {@link ChunksList}
|
|
*/
|
|
protected int currentChunkGroup = -1;
|
|
|
|
private int passes = 1; // Some writes might require two passes (NOT USED STILL)
|
|
private int currentpass = 0; // numbered from 1
|
|
|
|
private boolean shouldCloseStream = true;
|
|
|
|
private int idatMaxSize = 0; // 0=use default (PngIDatChunkOutputStream 64k)
|
|
// private PngIDatChunkOutputStream datStream;
|
|
|
|
protected PixelsWriter pixelsWriter;
|
|
|
|
private final OutputStream os;
|
|
|
|
private ChunkPredicate copyFromPredicate = null;
|
|
private ChunksList copyFromList = null;
|
|
|
|
protected StringBuilder debuginfo = new StringBuilder();
|
|
|
|
/**
|
|
* Opens a file for writing.
|
|
* <p>
|
|
* Sets shouldCloseStream=true. For more info see
|
|
* {@link #PngWriter(OutputStream, ImageInfo)}
|
|
*
|
|
* @param file
|
|
* @param imgInfo
|
|
* @param allowoverwrite
|
|
* If false and file exists, an {@link PngjOutputException} is
|
|
* thrown
|
|
*/
|
|
public PngWriter(File file, ImageInfo imgInfo, boolean allowoverwrite) {
|
|
this(PngHelperInternal.ostreamFromFile(file, allowoverwrite), imgInfo);
|
|
setShouldCloseStream(true);
|
|
}
|
|
|
|
/**
|
|
* @see #PngWriter(File, ImageInfo, boolean) (overwrite=true)
|
|
*/
|
|
public PngWriter(File file, ImageInfo imgInfo) {
|
|
this(file, imgInfo, true);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* <p>
|
|
*
|
|
* @param outputStream
|
|
* Open stream for binary writing
|
|
* @param imgInfo
|
|
* Basic image parameters
|
|
*/
|
|
public PngWriter(OutputStream outputStream, ImageInfo imgInfo) {
|
|
this.os = outputStream;
|
|
this.imgInfo = imgInfo;
|
|
// prealloc
|
|
chunksList = new ChunksListForWrite(imgInfo);
|
|
metadata = new PngMetadata(chunksList);
|
|
pixelsWriter = createPixelsWriter(imgInfo);
|
|
setCompLevel(9);
|
|
}
|
|
|
|
private void initIdat() { // this triggers the writing of first chunks
|
|
pixelsWriter.setOs(this.os);
|
|
pixelsWriter.setIdatMaxSize(idatMaxSize);
|
|
writeSignatureAndIHDR();
|
|
writeFirstChunks();
|
|
}
|
|
|
|
private void writeEndChunk() {
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_6_END;
|
|
PngChunkIEND c = new PngChunkIEND(imgInfo);
|
|
c.createRawChunk().writeChunk(os);
|
|
chunksList.getChunks().add(c);
|
|
}
|
|
|
|
private void writeFirstChunks() {
|
|
if (currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT)
|
|
return;
|
|
int nw = 0;
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR;
|
|
queueChunksFromOther();
|
|
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);
|
|
}
|
|
|
|
private void writeLastChunks() { // not including end
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT;
|
|
queueChunksFromOther();
|
|
chunksList.writeChunks(os, currentChunkGroup);
|
|
// should not be unwriten chunks
|
|
List<PngChunk> pending = chunksList.getQueuedChunks();
|
|
if (!pending.isEmpty())
|
|
throw new PngjOutputException(pending.size() + " chunks were not written! Eg: " + pending.get(0).toString());
|
|
}
|
|
|
|
/**
|
|
* Write id signature and also "IHDR" chunk
|
|
*/
|
|
private void writeSignatureAndIHDR() {
|
|
PngHelperInternal.writeBytes(os, PngHelperInternal.getPngIdSignature()); // signature
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR;
|
|
PngChunkIHDR ihdr = new PngChunkIHDR(imgInfo);
|
|
// http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
|
|
ihdr.createRawChunk().writeChunk(os);
|
|
chunksList.getChunks().add(ihdr);
|
|
}
|
|
|
|
private void queueChunksFromOther() {
|
|
if (copyFromList == null || copyFromPredicate == null)
|
|
return;
|
|
boolean idatDone = currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT; // we assume this method is not either before
|
|
// or after the IDAT writing, not in the
|
|
// middle!
|
|
for (PngChunk chunk : copyFromList.getChunks()) {
|
|
if (chunk.getRaw().data == null)
|
|
continue; // we cannot copy skipped chunks?
|
|
int groupOri = chunk.getChunkGroup();
|
|
if (groupOri <= ChunksList.CHUNK_GROUP_4_IDAT && idatDone)
|
|
continue;
|
|
if (groupOri >= ChunksList.CHUNK_GROUP_4_IDAT && !idatDone)
|
|
continue;
|
|
if (chunk.crit && !chunk.id.equals(PngChunkPLTE.ID))
|
|
continue; // critical chunks (except perhaps PLTE) are never
|
|
// copied
|
|
boolean copy = copyFromPredicate.match(chunk);
|
|
if (copy) {
|
|
// but if the chunk is already queued or writen, it's ommited!
|
|
if (chunksList.getEquivalent(chunk).isEmpty() && chunksList.getQueuedEquivalent(chunk).isEmpty()) {
|
|
chunksList.queue(chunk);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queues an ancillary chunk for writing.
|
|
* <p>
|
|
* If a "equivalent" chunk is already queued (see
|
|
* {@link ChunkHelper#equivalent(PngChunk, PngChunk)), this overwrites it.
|
|
* <p>
|
|
* The chunk will be written as late as possible, unless the priority is
|
|
* set.
|
|
*
|
|
* @param chunk
|
|
*/
|
|
public void queueChunk(PngChunk chunk) {
|
|
for (PngChunk other : chunksList.getQueuedEquivalent(chunk)) {
|
|
getChunksList().removeChunk(other);
|
|
}
|
|
chunksList.queue(chunk);
|
|
}
|
|
|
|
/**
|
|
* Sets an origin (typically from a {@link PngReader}) of Chunks to be
|
|
* copied. This should be called only once, before
|
|
* starting writing the rows. It doesn't matter the current state of the
|
|
* PngReader reading, this is a live object and
|
|
* what matters is that when the writer writes the pixels (IDAT) the reader
|
|
* has already read them, and that when the
|
|
* writer ends, the reader is already ended (all this is very natural).
|
|
* <p>
|
|
* Apart from the copyMask, there is some addional heuristics:
|
|
* <p>
|
|
* - The chunks will be queued, but will be written as late as possible
|
|
* (unless you explicitly set priority=true)
|
|
* <p>
|
|
* - The chunk will not be queued if an "equivalent" chunk was already
|
|
* queued explicitly. And it will be overwriten
|
|
* another is queued explicitly.
|
|
*
|
|
* @param chunks
|
|
* @param copyMask
|
|
* Some bitmask from {@link ChunkCopyBehaviour}
|
|
*
|
|
* @see #copyChunksFrom(ChunksList, ChunkPredicate)
|
|
*/
|
|
public void copyChunksFrom(ChunksList chunks, int copyMask) {
|
|
copyChunksFrom(chunks, ChunkCopyBehaviour.createPredicate(copyMask, imgInfo));
|
|
}
|
|
|
|
/**
|
|
* Copy all chunks from origin. See {@link #copyChunksFrom(ChunksList, int)}
|
|
* for more info
|
|
*/
|
|
public void copyChunksFrom(ChunksList chunks) {
|
|
copyChunksFrom(chunks, ChunkCopyBehaviour.COPY_ALL);
|
|
}
|
|
|
|
/**
|
|
* Copy chunks from origin depending on some {@link ChunkPredicate}
|
|
*
|
|
* @param chunks
|
|
* @param predicate
|
|
* The chunks (ancillary or PLTE) will be copied if and only if
|
|
* predicate matches
|
|
*
|
|
* @see #copyChunksFrom(ChunksList, int) for more info
|
|
*/
|
|
public void copyChunksFrom(ChunksList chunks, ChunkPredicate predicate) {
|
|
if (copyFromList != null && chunks != null)
|
|
PngHelperInternal.LOGGER.warning("copyChunksFrom should only be called once");
|
|
if (predicate == null)
|
|
throw new PngjOutputException("copyChunksFrom requires a predicate");
|
|
this.copyFromList = chunks;
|
|
this.copyFromPredicate = predicate;
|
|
}
|
|
|
|
/**
|
|
* Computes compressed size/raw size, approximate.
|
|
* <p>
|
|
* 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_5_AFTERIDAT)
|
|
throw new PngjOutputException("must be called after end()");
|
|
return pixelsWriter.getCompression();
|
|
}
|
|
|
|
/**
|
|
* Finalizes all the steps and closes the stream. This must be called after
|
|
* writing the lines. Idempotent
|
|
*/
|
|
public void end() {
|
|
if (rowNum != imgInfo.rows - 1 || !pixelsWriter.isDone())
|
|
throw new PngjOutputException("all rows have not been written");
|
|
try {
|
|
if (pixelsWriter != null)
|
|
pixelsWriter.close();
|
|
if (currentChunkGroup < ChunksList.CHUNK_GROUP_5_AFTERIDAT)
|
|
writeLastChunks();
|
|
if (currentChunkGroup < ChunksList.CHUNK_GROUP_6_END)
|
|
writeEndChunk();
|
|
} finally {
|
|
close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes and releases resources
|
|
* <p>
|
|
* This is normally called internally from {@link #end()}, you should only
|
|
* call this for aborting the writing and
|
|
* release resources (close the stream).
|
|
* <p>
|
|
* Idempotent and secure - never throws exceptions
|
|
*/
|
|
public void close() {
|
|
if (pixelsWriter != null)
|
|
pixelsWriter.close();
|
|
if (shouldCloseStream && os != null)
|
|
try {
|
|
os.close();
|
|
} catch (Exception e) {
|
|
PngHelperInternal.LOGGER.warning("Error closing writer " + e.toString());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* returns the chunks list (queued and writen chunks)
|
|
*/
|
|
public ChunksListForWrite getChunksList() {
|
|
return chunksList;
|
|
}
|
|
|
|
/**
|
|
* Retruns a high level wrapper over for metadata handling
|
|
*/
|
|
public PngMetadata getMetadata() {
|
|
return metadata;
|
|
}
|
|
|
|
/**
|
|
* Sets internal prediction filter type, or strategy to choose it.
|
|
* <p>
|
|
* This must be called just after constructor, before starting writing.
|
|
* <p>
|
|
*/
|
|
public void setFilterType(FilterType filterType) {
|
|
pixelsWriter.setFilterType(filterType);
|
|
}
|
|
|
|
/**
|
|
* This is kept for backwards compatibility, now the PixelsWriter object
|
|
* should be used for setting
|
|
* compression/filtering options
|
|
*
|
|
* @see PixelsWriter#setCompressionFactor(double)
|
|
* @param compLevel
|
|
* between 0 (no compression, max speed) and 9 (max compression)
|
|
*/
|
|
public void setCompLevel(int complevel) {
|
|
pixelsWriter.setDeflaterCompLevel(complevel);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public void setFilterPreserve(boolean filterPreserve) {
|
|
if (filterPreserve)
|
|
pixelsWriter.setFilterType(FilterType.FILTER_PRESERVE);
|
|
else if (pixelsWriter.getFilterType() == null)
|
|
pixelsWriter.setFilterType(FilterType.FILTER_DEFAULT);
|
|
}
|
|
|
|
/**
|
|
* Sets maximum size of IDAT fragments. Incrementing this from the default
|
|
* has very little effect on compression and
|
|
* increments memory usage. You should rarely change this.
|
|
* <p>
|
|
*
|
|
* @param idatMaxSize
|
|
* default=0 : use defaultSize (32K)
|
|
*/
|
|
public void setIdatMaxSize(int idatMaxSize) {
|
|
this.idatMaxSize = idatMaxSize;
|
|
}
|
|
|
|
/**
|
|
* If true, output stream will be closed after ending write
|
|
* <p>
|
|
* default=true
|
|
*/
|
|
public void setShouldCloseStream(boolean shouldCloseStream) {
|
|
this.shouldCloseStream = shouldCloseStream;
|
|
}
|
|
|
|
/**
|
|
* Writes next row, does not check row number.
|
|
*
|
|
* @param imgline
|
|
*/
|
|
public void writeRow(IImageLine imgline) {
|
|
writeRow(imgline, rowNum + 1);
|
|
}
|
|
|
|
/**
|
|
* Writes the full set of row. The ImageLineSet should contain (allow to
|
|
* acces) imgInfo.rows
|
|
*/
|
|
public void writeRows(IImageLineSet<? extends IImageLine> imglines) {
|
|
for (int i = 0; i < imgInfo.rows; i++)
|
|
writeRow(imglines.getImageLineRawNum(i));
|
|
}
|
|
|
|
public void writeRow(IImageLine imgline, int rownumber) {
|
|
rowNum++;
|
|
if (rowNum == imgInfo.rows)
|
|
rowNum = 0;
|
|
if (rownumber == imgInfo.rows)
|
|
rownumber = 0;
|
|
if (rownumber >= 0 && rowNum != rownumber)
|
|
throw new PngjOutputException("rows must be written in order: expected:" + rowNum + " passed:" + rownumber);
|
|
if (rowNum == 0)
|
|
currentpass++;
|
|
if (rownumber == 0 && currentpass == passes) {
|
|
initIdat();
|
|
currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT; // we just begin writing IDAT
|
|
}
|
|
byte[] rowb = pixelsWriter.getRowb();
|
|
imgline.writeToPngRaw(rowb);
|
|
pixelsWriter.processRow(rowb);
|
|
|
|
}
|
|
|
|
/**
|
|
* Utility method, uses internaly a ImageLineInt
|
|
*/
|
|
public void writeRowInt(int[] buf) {
|
|
writeRow(new ImageLineInt(imgInfo, buf));
|
|
}
|
|
|
|
/**
|
|
* Factory method for pixels writer. This will be called once at the moment
|
|
* at start writing a set of IDAT chunks
|
|
* (typically once in a normal PNG)
|
|
*
|
|
* This should be overriden if custom filtering strategies are desired.
|
|
* Remember to release this with close()
|
|
*
|
|
* @param imginfo
|
|
* Might be different than that of this object (eg: APNG with
|
|
* subimages)
|
|
* @param os
|
|
* Output stream
|
|
* @return new PixelsWriter. Don't forget to call close() when discarding it
|
|
*/
|
|
protected PixelsWriter createPixelsWriter(ImageInfo imginfo) {
|
|
PixelsWriterDefault pw = new PixelsWriterDefault(imginfo);
|
|
return pw;
|
|
}
|
|
|
|
public final PixelsWriter getPixelsWriter() {
|
|
return pixelsWriter;
|
|
}
|
|
|
|
public String getDebuginfo() {
|
|
return debuginfo.toString();
|
|
}
|
|
|
|
}
|