in src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java [299:571]
public void writeImage(final BufferedImage src, final OutputStream os, PngImagingParameters params, PaletteFactory paletteFactory)
throws ImagingException, IOException {
if (params == null) {
params = new PngImagingParameters();
}
if (paletteFactory == null) {
paletteFactory = new PaletteFactory();
}
final int compressionLevel = Deflater.DEFAULT_COMPRESSION;
final int width = src.getWidth();
final int height = src.getHeight();
final boolean hasAlpha = paletteFactory.hasTransparency(src);
Debug.debug("hasAlpha: " + hasAlpha);
// int transparency = paletteFactory.getTransparency(src);
boolean isGrayscale = paletteFactory.isGrayscale(src);
Debug.debug("isGrayscale: " + isGrayscale);
final PngColorType pngColorType;
{
final boolean forceIndexedColor = params.isForceIndexedColor();
final boolean forceTrueColor = params.isForceTrueColor();
if (forceIndexedColor && forceTrueColor) {
throw new ImagingException("Params: Cannot force both indexed and true color modes");
}
if (forceIndexedColor) {
pngColorType = PngColorType.INDEXED_COLOR;
} else if (forceTrueColor) {
pngColorType = hasAlpha ? PngColorType.TRUE_COLOR_WITH_ALPHA : PngColorType.TRUE_COLOR;
isGrayscale = false;
} else {
pngColorType = PngColorType.getColorType(hasAlpha, isGrayscale);
}
Debug.debug("colorType: " + pngColorType);
}
final byte bitDepth = getBitDepth(pngColorType, params);
Debug.debug("bitDepth: " + bitDepth);
final int sampleDepth;
if (pngColorType == PngColorType.INDEXED_COLOR) {
sampleDepth = 8;
} else {
sampleDepth = bitDepth;
}
Debug.debug("sampleDepth: " + sampleDepth);
{
PngConstants.PNG_SIGNATURE.writeTo(os);
}
{
// IHDR must be first
final byte compressionMethod = PngConstants.COMPRESSION_TYPE_INFLATE_DEFLATE;
final byte filterMethod = PngConstants.FILTER_METHOD_ADAPTIVE;
final InterlaceMethod interlaceMethod = InterlaceMethod.NONE;
final ImageHeader imageHeader = new ImageHeader(width, height, bitDepth, pngColorType, compressionMethod, filterMethod, interlaceMethod);
writeChunkIHDR(os, imageHeader);
}
// {
// sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the
// iCCP chunk should not be present.
// charles
// }
Palette palette = null;
if (pngColorType == PngColorType.INDEXED_COLOR) {
// PLTE No Before first IDAT
final int maxColors = 256;
if (hasAlpha) {
palette = paletteFactory.makeQuantizedRgbaPalette(src, hasAlpha, maxColors);
writeChunkPLTE(os, palette);
writeChunkTRNS(os, palette);
} else {
palette = paletteFactory.makeQuantizedRgbPalette(src, maxColors);
writeChunkPLTE(os, palette);
}
}
final Object pixelDensityObj = params.getPixelDensity();
if (pixelDensityObj != null) {
final PixelDensity pixelDensity = (PixelDensity) pixelDensityObj;
if (pixelDensity.isUnitless()) {
writeChunkPHYS(os, (int) Math.round(pixelDensity.getRawHorizontalDensity()), (int) Math.round(pixelDensity.getRawVerticalDensity()), (byte) 0);
} else {
writeChunkPHYS(os, (int) Math.round(pixelDensity.horizontalDensityMetres()), (int) Math.round(pixelDensity.verticalDensityMetres()), (byte) 1);
}
}
final PhysicalScale physicalScale = params.getPhysicalScale();
if (physicalScale != null) {
writeChunkSCAL(os, physicalScale.getHorizontalUnitsPerPixel(), physicalScale.getVerticalUnitsPerPixel(),
physicalScale.isInMeters() ? (byte) 1 : (byte) 2);
}
final String xmpXml = params.getXmpXml();
if (xmpXml != null) {
writeChunkXmpiTXt(os, xmpXml);
}
final List<? extends AbstractPngText> outputTexts = params.getTextChunks();
if (outputTexts != null) {
for (final AbstractPngText text : outputTexts) {
if (text instanceof AbstractPngText.Text) {
writeChunktEXt(os, (AbstractPngText.Text) text);
} else if (text instanceof AbstractPngText.Ztxt) {
writeChunkzTXt(os, (AbstractPngText.Ztxt) text);
} else if (text instanceof AbstractPngText.Itxt) {
writeChunkiTXt(os, (AbstractPngText.Itxt) text);
} else {
throw new ImagingException("Unknown text to embed in PNG: " + text);
}
}
}
{
// Debug.debug("writing IDAT");
// IDAT Yes Multiple IDAT chunks shall be consecutive
// 28 March 2022. At this time, we only apply the predictor
// for non-grayscale, true-color images. This choice is made
// out of caution and is not necessarily required by the PNG
// spec. We may broaden the use of predictors in future versions.
final boolean usePredictor = params.isPredictorEnabled() && !isGrayscale && palette == null;
final byte[] uncompressed;
if (!usePredictor) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA;
final int[] row = Allocator.intArray(width);
for (int y = 0; y < height; y++) {
// Debug.debug("y", y + "/" + height);
src.getRGB(0, y, width, 1, row, 0, width);
baos.write(FilterType.NONE.ordinal());
for (int x = 0; x < width; x++) {
final int argb = row[x];
if (palette != null) {
final int index = palette.getPaletteIndex(argb);
baos.write(0xff & index);
} else {
final int alpha = 0xff & argb >> 24;
final int red = 0xff & argb >> 16;
final int green = 0xff & argb >> 8;
final int blue = 0xff & argb >> 0;
if (isGrayscale) {
final int gray = (red + green + blue) / 3;
// if (y == 0)
// {
// Debug.debug("gray: " + x + ", " + y +
// " argb: 0x"
// + Integer.toHexString(argb) + " gray: 0x"
// + Integer.toHexString(gray));
// // Debug.debug(x + ", " + y + " gray", gray);
// // Debug.debug(x + ", " + y + " gray", gray);
// Debug.debug(x + ", " + y + " gray", gray +
// " " + Integer.toHexString(gray));
// Debug.debug();
// }
baos.write(gray);
} else {
baos.write(red);
baos.write(green);
baos.write(blue);
}
if (useAlpha) {
baos.write(alpha);
}
}
}
}
uncompressed = baos.toByteArray();
} else {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA;
final int[] row = Allocator.intArray(width);
for (int y = 0; y < height; y++) {
// Debug.debug("y", y + "/" + height);
src.getRGB(0, y, width, 1, row, 0, width);
int priorA = 0;
int priorR = 0;
int priorG = 0;
int priorB = 0;
baos.write(FilterType.SUB.ordinal());
for (int x = 0; x < width; x++) {
final int argb = row[x];
final int alpha = 0xff & argb >> 24;
final int red = 0xff & argb >> 16;
final int green = 0xff & argb >> 8;
final int blue = 0xff & argb;
baos.write(red - priorR);
baos.write(green - priorG);
baos.write(blue - priorB);
priorR = red;
priorG = green;
priorB = blue;
if (useAlpha) {
baos.write(alpha - priorA);
priorA = alpha;
}
}
}
uncompressed = baos.toByteArray();
}
// Debug.debug("uncompressed", uncompressed.length);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final int chunkSize = 256 * 1024;
final Deflater deflater = new Deflater(compressionLevel);
final DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, chunkSize);
for (int index = 0; index < uncompressed.length; index += chunkSize) {
final int end = Math.min(uncompressed.length, index + chunkSize);
final int length = end - index;
dos.write(uncompressed, index, length);
dos.flush();
baos.flush();
final byte[] compressed = baos.toByteArray();
baos.reset();
if (compressed.length > 0) {
// Debug.debug("compressed", compressed.length);
writeChunkIDAT(os, compressed);
}
}
{
dos.finish();
final byte[] compressed = baos.toByteArray();
if (compressed.length > 0) {
// Debug.debug("compressed final", compressed.length);
writeChunkIDAT(os, compressed);
}
}
}
{
// IEND No Shall be last
writeChunkIEND(os);
}
/*
* Ancillary chunks (need not appear in this order) Chunk name Multiple allowed Ordering constraints cHRM No Before PLTE and IDAT gAMA No Before PLTE
* and IDAT iCCP No Before PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be present. sBIT No Before PLTE and IDAT sRGB No
* Before PLTE and IDAT. If the sRGB chunk is present, the iCCP chunk should not be present. bKGD No After PLTE; before IDAT hIST No After PLTE; before
* IDAT tRNS No After PLTE; before IDAT pHYs No Before IDAT sCAL No Before IDAT sPLT Yes Before IDAT tIME No None iTXt Yes None tEXt Yes None zTXt Yes
* None
*/
os.close();
} // todo: filter types