All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ar.com.hjg.pngj.PngWriter Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version
package ar.com.hjg.pngj;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

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;

/**
 * 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 read or reading
	 * 

* see {@link ChunksList} */ protected int currentChunkGroup = -1; /** * PNG filter strategy */ protected IFilterWriteStrategy filterStrat; /** * If the ImageLine has a valid filterType (and origin image is not * interlaced) that file type will be used. *

* Default: false */ protected boolean filterPreserve = false; /** * zip compression level 0 - 9 */ private int compLevel = 6; private boolean shouldCloseStream = true; private PngIDatChunkOutputStream datStream; private DeflaterOutputStream datStreamDeflated; /** * Deflate algortithm compression strategy */ private int deflaterStrategy = Deflater.FILTERED; private int idatMaxSize = 0; // 0=use default (PngIDatChunkOutputStream 64k) private final OutputStream os; private byte[] rowb = null; // element 0 is filter type! private byte[] rowbfilter = null; // current line with filter private byte[] rowbprev = null; // rowb prev private ChunkPredicate copyFromPredicate = null; private ChunksList copyFromList = null; /** * Opens a file for writing. *

* 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. *

* * @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); filterStrat = new FilterWriteStrategy(imgInfo, FilterType.FILTER_DEFAULT); // can be changed } private void initIdat() { // this triggers the writing of first chunks if (rowb == null || rowb.length < imgInfo.bytesPerRow + 1) { rowb = new byte[imgInfo.bytesPerRow + 1]; rowbprev = new byte[rowb.length]; rowbfilter = new byte[rowb.length]; } datStream = new PngIDatChunkOutputStream(this.os, idatMaxSize); Deflater def = new Deflater(compLevel); def.setStrategy(deflaterStrategy); datStreamDeflated = new DeflaterOutputStream(datStream, def); writeSignatureAndIHDR(); writeFirstChunks(); } private void writeEndChunk() { PngChunkIEND c = new PngChunkIEND(imgInfo); c.createRawChunk().writeChunk(os); chunksList.getChunks().add(c); } private void writeFirstChunks() { 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); currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT; } private void writeLastChunks() { // not including end queueChunksFromOther(); currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; chunksList.writeChunks(os, currentChunkGroup); // should not be unwriten chunks List pending = chunksList.getQueuedChunks(); if (!pending.isEmpty()) throw new PngjOutputException(pending.size() + " chunks were not written! Eg: " + pending.get(0).toString()); currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; } /** * Write id signature and also "IHDR" chunk */ private void writeSignatureAndIHDR() { currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR; PngHelperInternal.writeBytes(os, PngHelperInternal.getPngIdSignature()); // 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.createRawChunk().writeChunk(os); chunksList.getChunks().add(ihdr); } private void filterRow() { FilterType filterType = FilterType.FILTER_UNKNOWN; if (filterPreserve && FilterType.isValidStandard(rowb[0])) { filterType = FilterType.getByVal(rowb[0]); // preserve original filter } else { for (FilterType ftype : filterStrat.shouldTest(rowNum)) { filterRowWithFilterType(ftype); filterStrat.reportResultsForFilter(rowNum, ftype, rowbfilter, true); } filterType = filterStrat.preferedType(rowNum); } filterRowWithFilterType(filterType); filterStrat.reportResultsForFilter(rowNum, filterType, rowbfilter, false); } private void filterRowWithFilterType(FilterType filterType) { // warning: filters operation rely on: "previous row" (rowbprev) is // initialized to 0 the first time rowbfilter[0] = (byte) filterType.val; switch (filterType) { case FILTER_NONE: filterRowNone(); break; case FILTER_SUB: filterRowSub(); break; case FILTER_UP: filterRowUp(); break; case FILTER_AVERAGE: filterRowAverage(); break; case FILTER_PAETH: filterRowPaeth(); break; default: throw new PngjUnsupportedException("Filter type " + filterType + " not recognized"); } } private void filterAndSend() { filterRow(); try { datStreamDeflated.write(rowbfilter, 0, imgInfo.bytesPerRow + 1); } catch (IOException e) { throw new PngjOutputException(e); } } private void filterRowAverage() { int i, j, imax; imax = imgInfo.bytesPerRow; for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imax; i++, j++) { rowbfilter[i] = (byte) (rowb[i] - ((rowbprev[i] & 0xFF) + (j > 0 ? (rowb[j] & 0xFF) : 0)) / 2); } } private void filterRowNone() { System.arraycopy(rowb, 1, rowbfilter, 1, imgInfo.bytesPerRow); } private void filterRowPaeth() { int i, j, imax; imax = imgInfo.bytesPerRow; for (j = 1 - imgInfo.bytesPixel, i = 1; i <= imax; i++, j++) { rowbfilter[i] = (byte) PngHelperInternal.filterRowPaeth(rowb[i], j > 0 ? (rowb[j] & 0xFF) : 0, rowbprev[i] & 0xFF, j > 0 ? (rowbprev[j] & 0xFF) : 0); } } private void filterRowSub() { int i, j; for (i = 1; i <= imgInfo.bytesPixel; i++) rowbfilter[i] = (byte) rowb[i]; for (j = 1, i = imgInfo.bytesPixel + 1; i <= imgInfo.bytesPerRow; i++, j++) { rowbfilter[i] = (byte) (rowb[i] - rowb[j]); } } private void filterRowUp() { for (int i = 1; i <= imgInfo.bytesPerRow; i++) { rowbfilter[i] = (byte) (rowb[i] - rowbprev[i]); } } private void queueChunksFromOther() { if (copyFromList == null || copyFromPredicate == null) return; boolean idatDone = currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT; for (PngChunk chunk : copyFromList.getChunks()) { if (chunk.getRaw().data == null) continue; // we cannot copy skipped chunks? int group = chunk.getChunkGroup(); if (group <= ChunksList.CHUNK_GROUP_4_IDAT && idatDone) continue; if (group >= 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); //PngChunk newchunk = ChunkHelper.cloneForWrite(chunk, imgInfo); //chunksList.queue(newchunk); } } } } 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). *

* Apart from the copyMask, there is some addional heuristics: *

* - The chunks will be queued, but will be written as late as possible * (unless you explicitly set priority=true) *

* - 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. *

* 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 all the steps 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(); // this should release deflater internal native resources datStream.flush(); writeLastChunks(); writeEndChunk(); } catch (IOException e) { throw new PngjOutputException(e); } finally { close(); } } /** * Closes and releases resources *

* This is normally called internally from {@link #end()}, you should only * call this for aborting the writing and release resources (close the * stream). *

* Idempotent and secure - never throws exceptions */ public void close() { try { datStreamDeflated.close(); } catch (Exception e1) { } try { datStream.close(); } catch (Exception e2) { } if (shouldCloseStream) try { os.close(); } catch (Exception e) { PngHelperInternal.LOGGER.warning("Error closing writer " + e.toString()); } datStreamDeflated = null; //datStream = null; } /** * 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 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 setFilterStrategy setCompLevel() * * @param filterType * One of the five prediction types or strategy to choose it (see * PngFilterType) Recommended values: DEFAULT * (default) or AGGRESIVE */ public void setFilterType(FilterType filterType) { ((FilterWriteStrategy) filterStrat).setConfiguredType(filterType); if (filterType == FilterType.FILTER_NONE) { setDeflaterStrategy(Deflater.DEFAULT_STRATEGY); // TODO this should be also done for FILTER_DEFAULT? } } /** * Set an alternative strategy for selecting the prediction filter * * @param filterS */ public void setFilterStrategy(IFilterWriteStrategy filterS) { filterStrat = filterS; } /** * @see #filterPreserve */ public boolean isFilterPreserve() { return filterPreserve; } /** * @see #filterPreserve */ public void setFilterPreserve(boolean filterPreserve) { this.filterPreserve = filterPreserve; } /** * Sets maximum size of IDAT fragments. This has little effect on * performance you should rarely call this *

* * @param idatMaxSize * default=0 : use defaultSize (64K) */ public void setIdatMaxSize(int idatMaxSize) { this.idatMaxSize = idatMaxSize; } /** * If true, output 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 (for filter NONE usually DEFAULT_STRATEGY is a * little better */ public void setDeflaterStrategy(int deflaterStrategy) { this.deflaterStrategy = deflaterStrategy; } /** * 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 imglines) { for (int i = 0; i < imgInfo.rows; i++) writeRow(imglines.getImageLine(i)); } public void writeRow(IImageLine imgline, int rownumber) { rowNum++; if (rownumber >= 0 && rowNum != rownumber) throw new PngjOutputException("rows must be written in order: expected:" + rowNum + " passed:" + rownumber); if (datStream == null) initIdat(); // swap byte[] tmp = rowb; rowb = rowbprev; rowbprev = tmp; rowb[0] = (byte) FilterType.FILTER_UNKNOWN.val; // writeToPngRaw can overwrite this imgline.writeToPngRaw(rowb); filterAndSend(); } /** * Utility method, uses internaly a ImageLineInt */ public void writeRowInt(int[] buf) { writeRow(new ImageLineInt(imgInfo, buf)); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy