in src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java [335:632]
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);
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);
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 PngText> outputTexts = params.getTextChunks();
if (outputTexts != null) {
for (final PngText text : outputTexts) {
if (text instanceof PngText.Text) {
writeChunktEXt(os, (PngText.Text) text);
} else if (text instanceof PngText.Ztxt) {
writeChunkzTXt(os, (PngText.Ztxt) text);
} else if (text instanceof PngText.Itxt) {
writeChunkiTXt(os, (PngText.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;
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