From: Glenn Adams Date: Fri, 15 Jun 2012 02:56:21 +0000 (+0000) Subject: Bugzilla #40676: Support use of ImageLoaderRawPNG decoder in order to prevent re... X-Git-Tag: fop-1_1rc1^2~1^2~10 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=4bba95be2e5bd480bd5adec9e9b1083d5fb9a561;p=xmlgraphics-fop.git Bugzilla #40676: Support use of ImageLoaderRawPNG decoder in order to prevent re-encoding of PNG images (and unnecssary output file bloat). Submitted by Luis Bernardo, Matthias Reischenbacher. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1350455 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/lib/xmlgraphics-commons-1.5svn.jar b/lib/xmlgraphics-commons-1.5svn.jar index 867ecb6e0..c11f18608 100644 Binary files a/lib/xmlgraphics-commons-1.5svn.jar and b/lib/xmlgraphics-commons-1.5svn.jar differ diff --git a/src/java/META-INF/services/org.apache.fop.render.ImageHandler b/src/java/META-INF/services/org.apache.fop.render.ImageHandler index f9b890c8e..9066913ad 100644 --- a/src/java/META-INF/services/org.apache.fop.render.ImageHandler +++ b/src/java/META-INF/services/org.apache.fop.render.ImageHandler @@ -1,6 +1,7 @@ org.apache.fop.render.pdf.PDFImageHandlerGraphics2D org.apache.fop.render.pdf.PDFImageHandlerRenderedImage org.apache.fop.render.pdf.PDFImageHandlerRawJPEG +org.apache.fop.render.pdf.PDFImageHandlerRawPNG org.apache.fop.render.pdf.PDFImageHandlerRawCCITTFax org.apache.fop.render.pdf.PDFImageHandlerSVG org.apache.fop.render.java2d.Java2DImageHandlerRenderedImage @@ -11,6 +12,7 @@ org.apache.fop.render.ps.PSImageHandlerRenderedImage org.apache.fop.render.ps.PSImageHandlerEPS org.apache.fop.render.ps.PSImageHandlerRawCCITTFax org.apache.fop.render.ps.PSImageHandlerRawJPEG +org.apache.fop.render.ps.PSImageHandlerRawPNG org.apache.fop.render.ps.PSImageHandlerGraphics2D org.apache.fop.render.ps.PSImageHandlerSVG org.apache.fop.render.afp.AFPImageHandlerRenderedImage diff --git a/src/java/org/apache/fop/pdf/BitmapImage.java b/src/java/org/apache/fop/pdf/BitmapImage.java index 186c00986..0979ddf9a 100644 --- a/src/java/org/apache/fop/pdf/BitmapImage.java +++ b/src/java/org/apache/fop/pdf/BitmapImage.java @@ -37,6 +37,7 @@ public class BitmapImage implements PDFImage { private PDFColor transparent = null; private String key; private PDFDocument pdfDoc; + private PDFFilter pdfFilter; /** * Create a bitmap image. @@ -208,9 +209,12 @@ public class BitmapImage implements PDFImage { * {@inheritDoc} */ public PDFFilter getPDFFilter() { - return null; + return pdfFilter; } + public void setPDFFilter(PDFFilter pdfFilter) { + this.pdfFilter = pdfFilter; + } } diff --git a/src/java/org/apache/fop/render/pdf/AbstractImageAdapter.java b/src/java/org/apache/fop/render/pdf/AbstractImageAdapter.java index 59d3930f7..ce99b9fef 100644 --- a/src/java/org/apache/fop/render/pdf/AbstractImageAdapter.java +++ b/src/java/org/apache/fop/render/pdf/AbstractImageAdapter.java @@ -20,13 +20,16 @@ package org.apache.fop.render.pdf; import java.awt.color.ColorSpace; import java.awt.color.ICC_Profile; +import java.awt.image.IndexColorModel; +import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.xmlgraphics.image.loader.Image; import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil; +import org.apache.fop.pdf.PDFArray; import org.apache.fop.pdf.PDFColor; import org.apache.fop.pdf.PDFConformanceException; import org.apache.fop.pdf.PDFDeviceColorSpace; @@ -50,7 +53,9 @@ public abstract class AbstractImageAdapter implements PDFImage { /** the image */ protected Image image; - private PDFICCStream pdfICCStream = null; + private PDFICCStream pdfICCStream; + + private static final int MAX_HIVAL = 255; /** * Creates a new PDFImage from an Image instance. @@ -202,6 +207,68 @@ public abstract class AbstractImageAdapter implements PDFImage { //nop } + /** + * This is to be used by populateXObjectDictionary() when the image is palette based. + * @param dict the dictionary to fill in + * @param icm the image color model + */ + protected void populateXObjectDictionaryForIndexColorModel(PDFDictionary dict, IndexColorModel icm) { + PDFArray indexed = new PDFArray(dict); + indexed.add(new PDFName("Indexed")); + if (icm.getColorSpace().getType() != ColorSpace.TYPE_RGB) { + log.warn("Indexed color space is not using RGB as base color space." + + " The image may not be handled correctly." + " Base color space: " + + icm.getColorSpace() + " Image: " + image.getInfo()); + } + indexed.add(new PDFName(toPDFColorSpace(icm.getColorSpace()).getName())); + int c = icm.getMapSize(); + int hival = c - 1; + if (hival > MAX_HIVAL) { + throw new UnsupportedOperationException("hival must not go beyond " + MAX_HIVAL); + } + indexed.add(new Integer(hival)); + int[] palette = new int[c]; + icm.getRGBs(palette); + ByteArrayOutputStream baout = new ByteArrayOutputStream(); + for (int i = 0; i < c; i++) { + // TODO Probably doesn't work for non RGB based color spaces + // See log warning above + int entry = palette[i]; + baout.write((entry & 0xFF0000) >> 16); + baout.write((entry & 0xFF00) >> 8); + baout.write(entry & 0xFF); + } + indexed.add(baout.toByteArray()); + + dict.put("ColorSpace", indexed); + dict.put("BitsPerComponent", icm.getPixelSize()); + + Integer index = getIndexOfFirstTransparentColorInPalette(icm); + if (index != null) { + PDFArray mask = new PDFArray(dict); + mask.add(index); + mask.add(index); + dict.put("Mask", mask); + } + } + + private static Integer getIndexOfFirstTransparentColorInPalette(IndexColorModel icm) { + byte[] alphas = new byte[icm.getMapSize()]; + byte[] reds = new byte[icm.getMapSize()]; + byte[] greens = new byte[icm.getMapSize()]; + byte[] blues = new byte[icm.getMapSize()]; + icm.getAlphas(alphas); + icm.getReds(reds); + icm.getGreens(greens); + icm.getBlues(blues); + for (int i = 0; i < icm.getMapSize(); i++) { + if ((alphas[i] & 0xFF) == 0) { + return new Integer(i); + } + } + return null; + } + /** * Converts a ColorSpace object to a PDFColorSpace object. * @param cs ColorSpace instance diff --git a/src/java/org/apache/fop/render/pdf/ImageRawPNGAdapter.java b/src/java/org/apache/fop/render/pdf/ImageRawPNGAdapter.java new file mode 100644 index 000000000..55264e2fe --- /dev/null +++ b/src/java/org/apache/fop/render/pdf/ImageRawPNGAdapter.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +// Original author: Matthias Reichenbacher + +package org.apache.fop.render.pdf; + +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.ByteArrayOutputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.impl.ImageRawPNG; +import org.apache.xmlgraphics.image.loader.impl.ImageRawStream; + +import org.apache.fop.pdf.BitmapImage; +import org.apache.fop.pdf.FlateFilter; +import org.apache.fop.pdf.PDFColor; +import org.apache.fop.pdf.PDFDeviceColorSpace; +import org.apache.fop.pdf.PDFDictionary; +import org.apache.fop.pdf.PDFDocument; +import org.apache.fop.pdf.PDFFilter; +import org.apache.fop.pdf.PDFFilterException; +import org.apache.fop.pdf.PDFFilterList; +import org.apache.fop.pdf.PDFICCStream; +import org.apache.fop.pdf.PDFReference; + +public class ImageRawPNGAdapter extends AbstractImageAdapter { + + /** logging instance */ + private static Log log = LogFactory.getLog(ImageRawPNGAdapter.class); + + private PDFICCStream pdfICCStream; + private PDFFilter pdfFilter; + private String maskRef; + private PDFReference softMask; + private int numberOfInterleavedComponents; + + /** + * Creates a new PDFImage from an Image instance. + * @param image the image + * @param key XObject key + */ + public ImageRawPNGAdapter(ImageRawPNG image, String key) { + super(image, key); + } + + /** {@inheritDoc} */ + public void setup(PDFDocument doc) { + super.setup(doc); + ColorModel cm = ((ImageRawPNG) this.image).getColorModel(); + if (cm instanceof IndexColorModel) { + numberOfInterleavedComponents = 1; + } else { + // this can be 1 (gray), 2 (gray + alpha), 3 (rgb) or 4 (rgb + alpha) + // numberOfInterleavedComponents = (cm.hasAlpha() ? 1 : 0) + cm.getNumColorComponents(); + numberOfInterleavedComponents = cm.getNumComponents(); + } + + // set up image compression for non-alpha channel + FlateFilter flate; + try { + flate = new FlateFilter(); + flate.setApplied(true); + flate.setPredictor(FlateFilter.PREDICTION_PNG_OPT); + if (numberOfInterleavedComponents < 3) { + // means palette (1) or gray (1) or gray + alpha (2) + flate.setColors(1); + } else { + // means rgb (3) or rgb + alpha (4) + flate.setColors(3); + } + flate.setColumns(image.getSize().getWidthPx()); + flate.setBitsPerComponent(this.getBitsPerComponent()); + } catch (PDFFilterException e) { + throw new RuntimeException("FlateFilter configuration error", e); + } + this.pdfFilter = flate; + + // Handle transparency channel if applicable; note that for palette images the transparency is + // not TRANSLUCENT + if (cm.hasAlpha() && cm.getTransparency() == ColorModel.TRANSLUCENT) { + doc.getProfile().verifyTransparencyAllowed(image.getInfo().getOriginalURI()); + // TODO: Implement code to combine image with background color if transparency is not allowed + // here we need to inflate the PNG pixel data, which includes alpha, separate the alpha channel + // and then deflate it back again + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DeflaterOutputStream dos = new DeflaterOutputStream(baos, new Deflater()); + InputStream in = ((ImageRawStream) image).createInputStream(); + try { + InflaterInputStream infStream = new InflaterInputStream(in, new Inflater()); + DataInputStream dataStream = new DataInputStream(infStream); + // offset is the byte offset of the alpha component + int offset = numberOfInterleavedComponents - 1; // 1 for GA, 3 for RGBA + int numColumns = image.getSize().getWidthPx(); + int bytesPerRow = numberOfInterleavedComponents * numColumns; + int filter; + // read line by line; the first byte holds the filter + while ((filter = dataStream.read()) != -1) { + byte[] bytes = new byte[bytesPerRow]; + dataStream.readFully(bytes, 0, bytesPerRow); + dos.write((byte) filter); + for (int j = 0; j < numColumns; j++) { + dos.write(bytes, offset, 1); + offset += numberOfInterleavedComponents; + } + offset = numberOfInterleavedComponents - 1; + } + dos.close(); + } catch (IOException e) { + throw new RuntimeException("Error processing transparency channel:", e); + } finally { + IOUtils.closeQuietly(in); + } + // set up alpha channel compression + FlateFilter transFlate; + try { + transFlate = new FlateFilter(); + transFlate.setApplied(true); + transFlate.setPredictor(FlateFilter.PREDICTION_PNG_OPT); + transFlate.setColors(1); + transFlate.setColumns(image.getSize().getWidthPx()); + transFlate.setBitsPerComponent(this.getBitsPerComponent()); + } catch (PDFFilterException e) { + throw new RuntimeException("FlateFilter configuration error", e); + } + BitmapImage alphaMask = new BitmapImage("Mask:" + this.getKey(), image.getSize().getWidthPx(), + image.getSize().getHeightPx(), baos.toByteArray(), null); + alphaMask.setPDFFilter(transFlate); + alphaMask.setColorSpace(new PDFDeviceColorSpace(PDFDeviceColorSpace.DEVICE_GRAY)); + softMask = doc.addImage(null, alphaMask).makeReference(); + } + } + + /** {@inheritDoc} */ + public PDFDeviceColorSpace getColorSpace() { + // DeviceGray, DeviceRGB, or DeviceCMYK + return toPDFColorSpace(image.getColorSpace()); + } + + /** {@inheritDoc} */ + public int getBitsPerComponent() { + return ((ImageRawPNG) this.image).getBitDepth(); + } + + /** {@inheritDoc} */ + public boolean isTransparent() { + return ((ImageRawPNG) this.image).isTransparent(); + } + + /** {@inheritDoc} */ + public PDFColor getTransparentColor() { + return new PDFColor(((ImageRawPNG) this.image).getTransparentColor()); + } + + /** {@inheritDoc} */ + public String getMask() { + return maskRef; + } + + /** {@inheritDoc} */ + public String getSoftMask() { + return softMask.toString(); + } + + /** {@inheritDoc} */ + public PDFReference getSoftMaskReference() { + return softMask; + } + + /** {@inheritDoc} */ + public PDFFilter getPDFFilter() { + return pdfFilter; + } + + /** {@inheritDoc} */ + public void outputContents(OutputStream out) throws IOException { + InputStream in = ((ImageRawStream) image).createInputStream(); + + try { + if (numberOfInterleavedComponents == 1 || numberOfInterleavedComponents == 3) { + // means we have Gray, RGB, or Palette + IOUtils.copy(in, out); + } else { + // means we have Gray + alpha or RGB + alpha + // TODO: since we have alpha here do this when the alpha channel is extracted + int numBytes = numberOfInterleavedComponents - 1; // 1 for Gray, 3 for RGB + int numColumns = image.getSize().getWidthPx(); + InflaterInputStream infStream = new InflaterInputStream(in, new Inflater()); + DataInputStream dataStream = new DataInputStream(infStream); + int offset = 0; + int bytesPerRow = numberOfInterleavedComponents * numColumns; + int filter; + // here we need to inflate the PNG pixel data, which includes alpha, separate the alpha + // channel and then deflate the RGB channels back again + DeflaterOutputStream dos = new DeflaterOutputStream(out, new Deflater()); + while ((filter = dataStream.read()) != -1) { + byte[] bytes = new byte[bytesPerRow]; + dataStream.readFully(bytes, 0, bytesPerRow); + dos.write((byte) filter); + for (int j = 0; j < numColumns; j++) { + dos.write(bytes, offset, numBytes); + offset += numberOfInterleavedComponents; + } + offset = 0; + } + dos.close(); + } + } finally { + IOUtils.closeQuietly(in); + } + } + + /** {@inheritDoc} */ + public PDFICCStream getICCStream() { + return pdfICCStream; + } + + /** {@inheritDoc} */ + public String getFilterHint() { + return PDFFilterList.PRECOMPRESSED_FILTER; + } + + public void populateXObjectDictionary(PDFDictionary dict) { + ColorModel cm = ((ImageRawPNG) image).getColorModel(); + if (cm instanceof IndexColorModel) { + IndexColorModel icm = (IndexColorModel) cm; + super.populateXObjectDictionaryForIndexColorModel(dict, icm); + } + } +} diff --git a/src/java/org/apache/fop/render/pdf/ImageRenderedAdapter.java b/src/java/org/apache/fop/render/pdf/ImageRenderedAdapter.java index c38a2edaf..b0b7b79d1 100644 --- a/src/java/org/apache/fop/render/pdf/ImageRenderedAdapter.java +++ b/src/java/org/apache/fop/render/pdf/ImageRenderedAdapter.java @@ -27,8 +27,6 @@ import java.awt.image.RenderedImage; import java.io.IOException; import java.io.OutputStream; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -36,14 +34,12 @@ import org.apache.xmlgraphics.image.loader.impl.ImageRendered; import org.apache.xmlgraphics.ps.ImageEncodingHelper; import org.apache.fop.pdf.AlphaRasterImage; -import org.apache.fop.pdf.PDFArray; import org.apache.fop.pdf.PDFColor; import org.apache.fop.pdf.PDFDeviceColorSpace; import org.apache.fop.pdf.PDFDictionary; import org.apache.fop.pdf.PDFDocument; import org.apache.fop.pdf.PDFFilter; import org.apache.fop.pdf.PDFFilterList; -import org.apache.fop.pdf.PDFName; import org.apache.fop.pdf.PDFReference; /** @@ -162,30 +158,6 @@ public class ImageRenderedAdapter extends AbstractImageAdapter { return (getImage().getTransparentColor() != null); } - private static Integer getIndexOfFirstTransparentColorInPalette(RenderedImage image) { - ColorModel cm = image.getColorModel(); - if (cm instanceof IndexColorModel) { - IndexColorModel icm = (IndexColorModel)cm; - //Identify the transparent color in the palette - byte[] alphas = new byte[icm.getMapSize()]; - byte[] reds = new byte[icm.getMapSize()]; - byte[] greens = new byte[icm.getMapSize()]; - byte[] blues = new byte[icm.getMapSize()]; - icm.getAlphas(alphas); - icm.getReds(reds); - icm.getGreens(greens); - icm.getBlues(blues); - for (int i = 0; - i < ((IndexColorModel) cm).getMapSize(); - i++) { - if ((alphas[i] & 0xFF) == 0) { - return Integer.valueOf(i); - } - } - } - return null; - } - /** {@inheritDoc} */ @Override public PDFColor getTransparentColor() { @@ -230,54 +202,13 @@ public class ImageRenderedAdapter extends AbstractImageAdapter { } } - private static final int MAX_HIVAL = 255; - /** {@inheritDoc} */ @Override public void populateXObjectDictionary(PDFDictionary dict) { ColorModel cm = getEffectiveColorModel(); if (cm instanceof IndexColorModel) { - IndexColorModel icm = (IndexColorModel)cm; - PDFArray indexed = new PDFArray(dict); - indexed.add(new PDFName("Indexed")); - - if (icm.getColorSpace().getType() != ColorSpace.TYPE_RGB) { - log.warn("Indexed color space is not using RGB as base color space." - + " The image may not be handled correctly." - + " Base color space: " + icm.getColorSpace() - + " Image: " + image.getInfo()); - } - indexed.add(new PDFName(toPDFColorSpace(icm.getColorSpace()).getName())); - int c = icm.getMapSize(); - int hival = c - 1; - if (hival > MAX_HIVAL) { - throw new UnsupportedOperationException("hival must not go beyond " + MAX_HIVAL); - } - indexed.add(Integer.valueOf(hival)); - int[] palette = new int[c]; - icm.getRGBs(palette); - ByteArrayOutputStream baout = new ByteArrayOutputStream(); - for (int i = 0; i < c; i++) { - //TODO Probably doesn't work for non RGB based color spaces - //See log warning above - int entry = palette[i]; - baout.write((entry & 0xFF0000) >> 16); - baout.write((entry & 0xFF00) >> 8); - baout.write(entry & 0xFF); - } - indexed.add(baout.toByteArray()); - IOUtils.closeQuietly(baout); - - dict.put("ColorSpace", indexed); - dict.put("BitsPerComponent", icm.getPixelSize()); - - Integer index = getIndexOfFirstTransparentColorInPalette(getImage().getRenderedImage()); - if (index != null) { - PDFArray mask = new PDFArray(dict); - mask.add(index); - mask.add(index); - dict.put("Mask", mask); - } + IndexColorModel icm = (IndexColorModel) cm; + super.populateXObjectDictionaryForIndexColorModel(dict, icm); } } diff --git a/src/java/org/apache/fop/render/pdf/PDFImageHandlerRawPNG.java b/src/java/org/apache/fop/render/pdf/PDFImageHandlerRawPNG.java new file mode 100644 index 000000000..3475954a8 --- /dev/null +++ b/src/java/org/apache/fop/render/pdf/PDFImageHandlerRawPNG.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +// Original author: Matthias Reichenbacher + +package org.apache.fop.render.pdf; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.impl.ImageRawPNG; + +import org.apache.fop.pdf.PDFImage; +import org.apache.fop.render.RenderingContext; + +/** + * Image handler implementation which handles CCITT encoded images (CCITT fax group 3/4) + * for PDF output. + */ +public class PDFImageHandlerRawPNG extends AbstractPDFImageHandler { + + private static final ImageFlavor[] FLAVORS = new ImageFlavor[] {ImageFlavor.RAW_PNG}; + + @Override + PDFImage createPDFImage(Image image, String xobjectKey) { + return new ImageRawPNGAdapter((ImageRawPNG) image, xobjectKey); + } + + /** {@inheritDoc} */ + public int getPriority() { + return 100; + } + + /** {@inheritDoc} */ + public Class getSupportedImageClass() { + return ImageRawPNG.class; + } + + /** {@inheritDoc} */ + public ImageFlavor[] getSupportedImageFlavors() { + return FLAVORS; + } + + /** {@inheritDoc} */ + public boolean isCompatible(RenderingContext targetContext, Image image) { + return (image == null || image instanceof ImageRawPNG) + && targetContext instanceof PDFRenderingContext; + } + +} diff --git a/src/java/org/apache/fop/render/ps/ImageEncoderPNG.java b/src/java/org/apache/fop/render/ps/ImageEncoderPNG.java new file mode 100644 index 000000000..bcda90bda --- /dev/null +++ b/src/java/org/apache/fop/render/ps/ImageEncoderPNG.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.render.ps; + +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.image.loader.impl.ImageRawPNG; +import org.apache.xmlgraphics.image.loader.impl.ImageRawStream; +import org.apache.xmlgraphics.ps.ImageEncoder; + +/** + * ImageEncoder implementation for PNG images. + */ +public class ImageEncoderPNG implements ImageEncoder { + private final ImageRawPNG image; + private int numberOfInterleavedComponents; + + /** + * Main constructor + * @param image the PNG image + */ + public ImageEncoderPNG(ImageRawPNG image) { + this.image = image; + ColorModel cm = ((ImageRawPNG) this.image).getColorModel(); + if (cm instanceof IndexColorModel) { + numberOfInterleavedComponents = 1; + } else { + // this can be 1 (gray), 2 (gray + alpha), 3 (rgb) or 4 (rgb + alpha) + // numberOfInterleavedComponents = (cm.hasAlpha() ? 1 : 0) + cm.getNumColorComponents(); + numberOfInterleavedComponents = cm.getNumComponents(); + } + } + + /** {@inheritDoc} */ + public void writeTo(OutputStream out) throws IOException { + // TODO: refactor this code with equivalent PDF code + InputStream in = ((ImageRawStream) image).createInputStream(); + try { + if (numberOfInterleavedComponents == 1 || numberOfInterleavedComponents == 3) { + // means we have Gray, RGB, or Palette + IOUtils.copy(in, out); + } else { + // means we have Gray + alpha or RGB + alpha + int numBytes = numberOfInterleavedComponents - 1; // 1 for Gray, 3 for RGB + int numColumns = image.getSize().getWidthPx(); + InflaterInputStream infStream = new InflaterInputStream(in, new Inflater()); + DataInputStream dataStream = new DataInputStream(infStream); + int offset = 0; + int bytesPerRow = numberOfInterleavedComponents * numColumns; + int filter; + // here we need to inflate the PNG pixel data, which includes alpha, separate the alpha + // channel and then deflate the RGB channels back again + // TODO: not using the baos below and using the original out instead (as happens in PDF) + // would be preferable but that does not work with the rest of the postscript code; this + // needs to be revisited + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DeflaterOutputStream dos = new DeflaterOutputStream(/* out */baos, new Deflater()); + while ((filter = dataStream.read()) != -1) { + byte[] bytes = new byte[bytesPerRow]; + dataStream.readFully(bytes, 0, bytesPerRow); + dos.write((byte) filter); + for (int j = 0; j < numColumns; j++) { + dos.write(bytes, offset, numBytes); + offset += numberOfInterleavedComponents; + } + offset = 0; + } + dos.close(); + IOUtils.copy(new ByteArrayInputStream(baos.toByteArray()), out); + } + } finally { + IOUtils.closeQuietly(in); + } + } + + /** {@inheritDoc} */ + public String getImplicitFilter() { + String filter = "<< /Predictor 15 /Columns " + image.getSize().getWidthPx(); + filter += " /Colors " + (numberOfInterleavedComponents > 2 ? 3 : 1); + filter += " /BitsPerComponent " + image.getBitDepth() + " >> /FlateDecode"; + return filter; + } +} diff --git a/src/java/org/apache/fop/render/ps/PSImageHandlerRawPNG.java b/src/java/org/apache/fop/render/ps/PSImageHandlerRawPNG.java new file mode 100644 index 000000000..2a283e913 --- /dev/null +++ b/src/java/org/apache/fop/render/ps/PSImageHandlerRawPNG.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.render.ps; + +import java.awt.Dimension; +import java.awt.Rectangle; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; +import java.io.IOException; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.impl.ImageRawPNG; +import org.apache.xmlgraphics.ps.FormGenerator; +import org.apache.xmlgraphics.ps.ImageEncoder; +import org.apache.xmlgraphics.ps.ImageFormGenerator; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSImageUtils; + +import org.apache.fop.render.RenderingContext; + +/** + * Image handler implementation which handles raw (not decoded) PNG images for PostScript output. + */ +public class PSImageHandlerRawPNG implements PSImageHandler { + + private static final ImageFlavor[] FLAVORS = new ImageFlavor[] {ImageFlavor.RAW_PNG}; + + /** {@inheritDoc} */ + public void handleImage(RenderingContext context, Image image, Rectangle pos) throws IOException { + PSRenderingContext psContext = (PSRenderingContext) context; + PSGenerator gen = psContext.getGenerator(); + ImageRawPNG png = (ImageRawPNG) image; + + float x = (float) pos.getX() / 1000f; + float y = (float) pos.getY() / 1000f; + float w = (float) pos.getWidth() / 1000f; + float h = (float) pos.getHeight() / 1000f; + Rectangle2D targetRect = new Rectangle2D.Float(x, y, w, h); + + ImageEncoder encoder = new ImageEncoderPNG(png); + ImageInfo info = image.getInfo(); + Dimension imgDim = info.getSize().getDimensionPx(); + String imgDescription = image.getClass().getName(); + ColorModel cm = png.getColorModel(); + + PSImageUtils.writeImage(encoder, imgDim, imgDescription, targetRect, cm, gen); + } + + /** {@inheritDoc} */ + public void generateForm(RenderingContext context, Image image, PSImageFormResource form) + throws IOException { + PSRenderingContext psContext = (PSRenderingContext) context; + PSGenerator gen = psContext.getGenerator(); + ImageRawPNG png = (ImageRawPNG) image; + ImageInfo info = image.getInfo(); + String imageDescription = info.getMimeType() + " " + info.getOriginalURI(); + + ImageEncoder encoder = new ImageEncoderPNG(png); + FormGenerator formGen = new ImageFormGenerator(form.getName(), imageDescription, info.getSize() + .getDimensionPt(), info.getSize().getDimensionPx(), encoder, png.getColorSpace(), + false); + formGen.generate(gen); + } + + /** {@inheritDoc} */ + public int getPriority() { + return 200; + } + + /** {@inheritDoc} */ + public Class getSupportedImageClass() { + return ImageRawPNG.class; + } + + /** {@inheritDoc} */ + public ImageFlavor[] getSupportedImageFlavors() { + return FLAVORS; + } + + /** {@inheritDoc} */ + public boolean isCompatible(RenderingContext targetContext, Image image) { + if (targetContext instanceof PSRenderingContext) { + PSRenderingContext psContext = (PSRenderingContext) targetContext; + // The filters required for this implementation need PS level 2 or higher + if (psContext.getGenerator().getPSLevel() >= 2) { + return (image == null || image instanceof ImageRawPNG); + } + } + return false; + } + +} diff --git a/src/java/org/apache/fop/render/ps/PSImageHandlerRenderedImage.java b/src/java/org/apache/fop/render/ps/PSImageHandlerRenderedImage.java index 5a13c1c8e..46597e46d 100644 --- a/src/java/org/apache/fop/render/ps/PSImageHandlerRenderedImage.java +++ b/src/java/org/apache/fop/render/ps/PSImageHandlerRenderedImage.java @@ -19,7 +19,10 @@ package org.apache.fop.render.ps; +import java.awt.Dimension; import java.awt.Rectangle; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; import java.awt.image.RenderedImage; import java.io.IOException; @@ -28,6 +31,8 @@ import org.apache.xmlgraphics.image.loader.ImageFlavor; import org.apache.xmlgraphics.image.loader.ImageInfo; import org.apache.xmlgraphics.image.loader.impl.ImageRendered; import org.apache.xmlgraphics.ps.FormGenerator; +import org.apache.xmlgraphics.ps.ImageEncoder; +import org.apache.xmlgraphics.ps.ImageEncodingHelper; import org.apache.xmlgraphics.ps.ImageFormGenerator; import org.apache.xmlgraphics.ps.PSGenerator; import org.apache.xmlgraphics.ps.PSImageUtils; @@ -47,17 +52,24 @@ public class PSImageHandlerRenderedImage implements PSImageHandler { /** {@inheritDoc} */ public void handleImage(RenderingContext context, Image image, Rectangle pos) throws IOException { - PSRenderingContext psContext = (PSRenderingContext)context; + PSRenderingContext psContext = (PSRenderingContext) context; PSGenerator gen = psContext.getGenerator(); - ImageRendered imageRend = (ImageRendered)image; + ImageRendered imageRend = (ImageRendered) image; - float x = (float)pos.getX() / 1000f; - float y = (float)pos.getY() / 1000f; - float w = (float)pos.getWidth() / 1000f; - float h = (float)pos.getHeight() / 1000f; + float x = (float) pos.getX() / 1000f; + float y = (float) pos.getY() / 1000f; + float w = (float) pos.getWidth() / 1000f; + float h = (float) pos.getHeight() / 1000f; + Rectangle2D targetRect = new Rectangle2D.Double(x, y, w, h); RenderedImage ri = imageRend.getRenderedImage(); - PSImageUtils.renderBitmapImage(ri, x, y, w, h, gen); + ImageEncoder encoder = ImageEncodingHelper.createRenderedImageEncoder(ri); + Dimension imgDim = new Dimension(ri.getWidth(), ri.getHeight()); + String imgDescription = ri.getClass().getName(); + ImageEncodingHelper helper = new ImageEncodingHelper(ri); + ColorModel cm = helper.getEncodedColorModel(); + + PSImageUtils.writeImage(encoder, imgDim, imgDescription, targetRect, cm, gen); } /** {@inheritDoc} */ diff --git a/status.xml b/status.xml index ba895efab..f96a9487b 100644 --- a/status.xml +++ b/status.xml @@ -63,6 +63,9 @@ documents. Example: the fix of marks layering will be such a case when it's done. --> + + Support use of ImageLoaderRawPNG decoder in order to prevent re-encoding of PNG images (and unnecssary output file bloat). + Eliminate incorrect use of object identity which produces excessive property cache collisions. diff --git a/test/java/org/apache/fop/render/RawPNGTestUtil.java b/test/java/org/apache/fop/render/RawPNGTestUtil.java new file mode 100644 index 000000000..e6660bb42 --- /dev/null +++ b/test/java/org/apache/fop/render/RawPNGTestUtil.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.render; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import org.apache.xmlgraphics.image.loader.ImageSize; + +public final class RawPNGTestUtil { + + private static int NUM_ROWS = 32; + private static int NUM_COLUMNS = 32; + private static int DPI = 72; + + private RawPNGTestUtil() { + + } + + /** + * Builds a PNG IDAT section for a square of a given color and alpha; the filter is fixed. + * @param gray the gray color; set to -1 if using RGB + * @param red the red color; ignored if gray > -1 + * @param green the green color; ignored if gray > -1 + * @param blue the blue color; ignored if gray > -1 + * @param alpha the alpha color; set to -1 if not present + * @return the PNG IDAT byte array + * @throws IOException + */ + public static byte[] buildGRGBAData(int gray, int red, int green, int blue, int alpha) throws IOException { + // build an image, 32x32, Gray or RGB, with or without alpha, and with filter + int filter = 0; + int numRows = NUM_ROWS; + int numColumns = NUM_COLUMNS; + int numComponents = (gray > -1 ? 1 : 3) + (alpha > -1 ? 1 : 0); + int numBytesPerRow = numColumns * numComponents + 1; // 1 for filter + int numBytes = numRows * numBytesPerRow; + byte[] data = new byte[numBytes]; + for (int r = 0; r < numRows; r++) { + data[r * numBytesPerRow] = (byte) filter; + for (int c = 0; c < numColumns; c++) { + if (numComponents == 1) { + data[r * numBytesPerRow + numComponents * c + 1] = (byte) gray; + } else if (numComponents == 2) { + data[r * numBytesPerRow + numComponents * c + 1] = (byte) gray; + data[r * numBytesPerRow + numComponents * c + 2] = (byte) alpha; + } else if (numComponents == 3) { + data[r * numBytesPerRow + numComponents * c + 1] = (byte) red; + data[r * numBytesPerRow + numComponents * c + 2] = (byte) green; + data[r * numBytesPerRow + numComponents * c + 3] = (byte) blue; + } else if (numComponents == 4) { + data[r * numBytesPerRow + numComponents * c + 1] = (byte) red; + data[r * numBytesPerRow + numComponents * c + 2] = (byte) green; + data[r * numBytesPerRow + numComponents * c + 3] = (byte) blue; + data[r * numBytesPerRow + numComponents * c + 4] = (byte) alpha; + } + } + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DeflaterOutputStream dos = new DeflaterOutputStream(baos, new Deflater()); + dos.write(data); + dos.close(); + return baos.toByteArray(); + } + + /** + * + * @return a default ImageSize + */ + public static ImageSize getImageSize() { + return new ImageSize(NUM_ROWS, NUM_COLUMNS, DPI); + } +} diff --git a/test/java/org/apache/fop/render/pdf/ImageRawPNGAdapterTestCase.java b/test/java/org/apache/fop/render/pdf/ImageRawPNGAdapterTestCase.java new file mode 100644 index 000000000..885821f66 --- /dev/null +++ b/test/java/org/apache/fop/render/pdf/ImageRawPNGAdapterTestCase.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.render.pdf; + +import java.awt.image.ComponentColorModel; +import java.awt.image.IndexColorModel; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import org.junit.Test; + +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.impl.ImageRawPNG; + +import org.apache.fop.pdf.FlateFilter; +import org.apache.fop.pdf.PDFAMode; +import org.apache.fop.pdf.PDFDocument; +import org.apache.fop.pdf.PDFProfile; +import org.apache.fop.render.RawPNGTestUtil; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ImageRawPNGAdapterTestCase { + + @Test + public void testSetupWithIndexColorModel() { + IndexColorModel cm = mock(IndexColorModel.class); + ImageRawPNG irpng = mock(ImageRawPNG.class); + PDFDocument doc = mock(PDFDocument.class); + PDFProfile profile = mock(PDFProfile.class); + ImageRawPNGAdapter irpnga = new ImageRawPNGAdapter(irpng, "mock"); + ImageSize is = RawPNGTestUtil.getImageSize(); + + when(irpng.getColorModel()).thenReturn(cm); + // when(cm.hasAlpha()).thenReturn(false); + when(doc.getProfile()).thenReturn(profile); + when(profile.getPDFAMode()).thenReturn(PDFAMode.PDFA_1A); + when(irpng.getSize()).thenReturn(is); + irpnga.setup(doc); + FlateFilter filter = (FlateFilter) irpnga.getPDFFilter(); + assertEquals(1, filter.getColors()); + } + + @Test + public void testSetupWithComponentColorModel() throws IOException { + ComponentColorModel cm = mock(ComponentColorModel.class); + ImageRawPNG irpng = mock(ImageRawPNG.class); + PDFDocument doc = mock(PDFDocument.class); + PDFProfile profile = mock(PDFProfile.class); + ImageRawPNGAdapter irpnga = new ImageRawPNGAdapter(irpng, "mock"); + ImageSize is = RawPNGTestUtil.getImageSize(); + + when(irpng.getColorModel()).thenReturn(cm); + when(cm.getNumComponents()).thenReturn(3); + // when(cm.hasAlpha()).thenReturn(false); + when(doc.getProfile()).thenReturn(profile); + when(profile.getPDFAMode()).thenReturn(PDFAMode.PDFA_1A); + when(irpng.getSize()).thenReturn(is); + irpnga.setup(doc); + FlateFilter filter = (FlateFilter) irpnga.getPDFFilter(); + assertEquals(3, filter.getColors()); + } + + @Test + public void testOutputContentsWithRGBPNG() throws IOException { + testOutputContentsWithGRGBAPNG(-1, 128, 128, 128, -1); + } + + @Test + public void testOutputContentsWithRGBAPNG() throws IOException { + testOutputContentsWithGRGBAPNG(-1, 128, 128, 128, 128); + } + + @Test + public void testOutputContentsWithGPNG() throws IOException { + testOutputContentsWithGRGBAPNG(128, -1, -1, -1, -1); + } + + @Test + public void testOutputContentsWithGAPNG() throws IOException { + testOutputContentsWithGRGBAPNG(128, -1, -1, -1, 128); + } + + private void testOutputContentsWithGRGBAPNG(int gray, int red, int green, int blue, int alpha) + throws IOException { + int numColorComponents = gray > -1 ? 1 : 3; + int numComponents = numColorComponents + (alpha > -1 ? 1 : 0); + ComponentColorModel cm = mock(ComponentColorModel.class); + ImageRawPNG irpng = mock(ImageRawPNG.class); + PDFDocument doc = mock(PDFDocument.class); + PDFProfile profile = mock(PDFProfile.class); + ImageRawPNGAdapter irpnga = new ImageRawPNGAdapter(irpng, "mock"); + ImageSize is = RawPNGTestUtil.getImageSize(); + + when(irpng.getColorModel()).thenReturn(cm); + when(cm.getNumComponents()).thenReturn(numComponents); + // when(cm.hasAlpha()).thenReturn(false); + when(doc.getProfile()).thenReturn(profile); + when(profile.getPDFAMode()).thenReturn(PDFAMode.PDFA_1A); + when(irpng.getSize()).thenReturn(is); + irpnga.setup(doc); + FlateFilter filter = (FlateFilter) irpnga.getPDFFilter(); + assertEquals(numColorComponents, filter.getColors()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] data = RawPNGTestUtil.buildGRGBAData(gray, red, green, blue, alpha); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + when(irpng.createInputStream()).thenReturn(bais); + irpnga.outputContents(baos); + if (alpha > -1) { + byte[] expected = RawPNGTestUtil.buildGRGBAData(gray, red, green, blue, -1); + assertArrayEquals(expected, baos.toByteArray()); + } else { + assertArrayEquals(data, baos.toByteArray()); + } + } + +} diff --git a/test/java/org/apache/fop/render/ps/ImageEncoderPNGTestCase.java b/test/java/org/apache/fop/render/ps/ImageEncoderPNGTestCase.java new file mode 100644 index 000000000..458033dad --- /dev/null +++ b/test/java/org/apache/fop/render/ps/ImageEncoderPNGTestCase.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.render.ps; + +import java.awt.image.ComponentColorModel; +import java.awt.image.IndexColorModel; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.Test; + +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.impl.ImageRawPNG; + +import org.apache.fop.render.RawPNGTestUtil; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ImageEncoderPNGTestCase { + + @Test + public void testWriteToWithRGBPNG() throws IOException { + testWriteToWithGRGBAPNG(-1, 128, 128, 128, -1); + } + + @Test + public void testWriteToWithGPNG() throws IOException { + testWriteToWithGRGBAPNG(128, -1, -1, -1, -1); + } + + @Test + public void testWriteToWithRGBAPNG() throws IOException { + testWriteToWithGRGBAPNG(-1, 128, 128, 128, 128); + } + + @Test + public void testWriteToWithGAPNG() throws IOException { + testWriteToWithGRGBAPNG(128, -1, -1, -1, 128); + } + + private void testWriteToWithGRGBAPNG(int gray, int red, int green, int blue, int alpha) + throws IOException { + int numComponents = (gray > -1 ? 1 : 3) + (alpha > -1 ? 1 : 0); + ImageSize is = RawPNGTestUtil.getImageSize(); + ComponentColorModel cm = mock(ComponentColorModel.class); + when(cm.getNumComponents()).thenReturn(numComponents); + ImageRawPNG irpng = mock(ImageRawPNG.class); + when(irpng.getColorModel()).thenReturn(cm); + when(irpng.getSize()).thenReturn(is); + ImageEncoderPNG iepng = new ImageEncoderPNG(irpng); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] data = RawPNGTestUtil.buildGRGBAData(gray, red, green, blue, alpha); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + when(irpng.createInputStream()).thenReturn(bais); + iepng.writeTo(baos); + if (alpha > -1) { + byte[] expected = RawPNGTestUtil.buildGRGBAData(gray, red, green, blue, -1); + assertArrayEquals(expected, baos.toByteArray()); + } else { + assertArrayEquals(data, baos.toByteArray()); + } + } + + @Test + public void testWriteToWithPalettePNG() throws IOException { + ImageSize is = RawPNGTestUtil.getImageSize(); + IndexColorModel cm = mock(IndexColorModel.class); + ImageRawPNG irpng = mock(ImageRawPNG.class); + when(irpng.getColorModel()).thenReturn(cm); + when(irpng.getSize()).thenReturn(is); + ImageEncoderPNG iepng = new ImageEncoderPNG(irpng); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] data = RawPNGTestUtil.buildGRGBAData(128, -1, -1, -1, -1); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + when(irpng.createInputStream()).thenReturn(bais); + iepng.writeTo(baos); + assertArrayEquals(data, baos.toByteArray()); + } + + @Test + public void testGetImplicitFilterWithIndexColorModel() { + ImageSize is = RawPNGTestUtil.getImageSize(); + IndexColorModel cm = mock(IndexColorModel.class); + ImageRawPNG irpng = mock(ImageRawPNG.class); + when(irpng.getColorModel()).thenReturn(cm); + when(irpng.getBitDepth()).thenReturn(8); + when(irpng.getSize()).thenReturn(is); + ImageEncoderPNG iepng = new ImageEncoderPNG(irpng); + + String expectedFilter = "<< /Predictor 15 /Columns 32 /Colors 1 /BitsPerComponent 8 >> /FlateDecode"; + assertEquals(expectedFilter, iepng.getImplicitFilter()); + } + + @Test + public void testGetImplicitFilterWithComponentColorModel() { + ImageSize is = RawPNGTestUtil.getImageSize(); + ComponentColorModel cm = mock(ComponentColorModel.class); + when(cm.getNumComponents()).thenReturn(3); + ImageRawPNG irpng = mock(ImageRawPNG.class); + when(irpng.getColorModel()).thenReturn(cm); + when(irpng.getBitDepth()).thenReturn(8); + when(irpng.getSize()).thenReturn(is); + ImageEncoderPNG iepng = new ImageEncoderPNG(irpng); + + String expectedFilter = "<< /Predictor 15 /Columns 32 /Colors 3 /BitsPerComponent 8 >> /FlateDecode"; + assertEquals(expectedFilter, iepng.getImplicitFilter()); + } + +}