Browse Source

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
tags/fop-1_1rc1^2
Glenn Adams 12 years ago
parent
commit
4bba95be2e

BIN
lib/xmlgraphics-commons-1.5svn.jar View File


+ 2
- 0
src/java/META-INF/services/org.apache.fop.render.ImageHandler View File

@@ -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

+ 5
- 1
src/java/org/apache/fop/pdf/BitmapImage.java View File

@@ -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;
}
}



+ 68
- 1
src/java/org/apache/fop/render/pdf/AbstractImageAdapter.java View File

@@ -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

+ 258
- 0
src/java/org/apache/fop/render/pdf/ImageRawPNGAdapter.java View File

@@ -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);
}
}
}

+ 2
- 71
src/java/org/apache/fop/render/pdf/ImageRenderedAdapter.java View File

@@ -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);
}
}


+ 65
- 0
src/java/org/apache/fop/render/pdf/PDFImageHandlerRawPNG.java View File

@@ -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<ImageRawPNG> 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;
}

}

+ 113
- 0
src/java/org/apache/fop/render/ps/ImageEncoderPNG.java View File

@@ -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;
}
}

+ 111
- 0
src/java/org/apache/fop/render/ps/PSImageHandlerRawPNG.java View File

@@ -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<ImageRawPNG> 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;
}

}

+ 19
- 7
src/java/org/apache/fop/render/ps/PSImageHandlerRenderedImage.java View File

@@ -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} */

+ 3
- 0
status.xml View File

@@ -63,6 +63,9 @@
documents. Example: the fix of marks layering will be such a case when it's done.
-->
<release version="FOP Trunk" date="TBD">
<action context="Images" dev="GA" type="fix" fixes-bug="40676" due-to="Luis Bernardo, Matthias Reischenbacher">
Support use of ImageLoaderRawPNG decoder in order to prevent re-encoding of PNG images (and unnecssary output file bloat).
</action>
<action context="Code" dev="GA" type="fix" fixes-bug="53412" due-to="Alexios Giotis">
Eliminate incorrect use of object identity which produces excessive property cache collisions.
</action>

+ 92
- 0
test/java/org/apache/fop/render/RawPNGTestUtil.java View File

@@ -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);
}
}

+ 142
- 0
test/java/org/apache/fop/render/pdf/ImageRawPNGAdapterTestCase.java View File

@@ -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());
}
}

}

+ 133
- 0
test/java/org/apache/fop/render/ps/ImageEncoderPNGTestCase.java View File

@@ -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());
}

}

Loading…
Cancel
Save