Added limited support for more efficient image encoding in AFP. Where possible the raw image data from the RenderedImage is used. Limitations include: - missing support for color lookup tables (IndexColorModel) - monochrome images: additive/subtractive flag is not supported by the AFP library, yet. Images with fewer bits per pixel than specified in the configuration are no longer blown up to 4, 8 or even 24 bits. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@734737 13f79535-47bb-0310-9956-ffa450edef68tags/fop-1_0
@@ -19,24 +19,33 @@ | |||
package org.apache.fop.render.afp; | |||
import java.awt.image.ColorModel; | |||
import java.awt.image.RenderedImage; | |||
import java.io.IOException; | |||
import org.apache.commons.io.output.ByteArrayOutputStream; | |||
import org.apache.fop.afp.AFPDataObjectInfo; | |||
import org.apache.fop.afp.AFPImageObjectInfo; | |||
import org.apache.fop.afp.AFPObjectAreaInfo; | |||
import org.apache.fop.afp.AFPPaintingState; | |||
import org.apache.commons.logging.Log; | |||
import org.apache.commons.logging.LogFactory; | |||
import org.apache.xmlgraphics.image.loader.ImageFlavor; | |||
import org.apache.xmlgraphics.image.loader.impl.ImageRendered; | |||
import org.apache.xmlgraphics.ps.ImageEncodingHelper; | |||
import org.apache.xmlgraphics.util.MimeConstants; | |||
import org.apache.fop.afp.AFPDataObjectInfo; | |||
import org.apache.fop.afp.AFPImageObjectInfo; | |||
import org.apache.fop.afp.AFPObjectAreaInfo; | |||
import org.apache.fop.afp.AFPPaintingState; | |||
import org.apache.fop.util.BitmapImageUtil; | |||
/** | |||
* PDFImageHandler implementation which handles RenderedImage instances. | |||
*/ | |||
public class AFPImageHandlerRenderedImage extends AFPImageHandler { | |||
/** logging instance */ | |||
private static Log log = LogFactory.getLog(AFPImageHandlerRenderedImage.class); | |||
private static final ImageFlavor[] FLAVORS = new ImageFlavor[] { | |||
ImageFlavor.BUFFERED_IMAGE, | |||
ImageFlavor.RENDERED_IMAGE | |||
@@ -67,22 +76,69 @@ public class AFPImageHandlerRenderedImage extends AFPImageHandler { | |||
int dataWidth = renderedImage.getWidth(); | |||
imageObjectInfo.setDataWidth(dataWidth); | |||
int maxPixelSize = paintingState.getBitsPerPixel(); | |||
if (paintingState.isColorImages()) { | |||
maxPixelSize *= 3; //RGB only at the moment | |||
} | |||
ColorModel cm = renderedImage.getColorModel(); | |||
if (log.isTraceEnabled()) { | |||
log.trace("ColorModel: " + cm); | |||
} | |||
int pixelSize = cm.getPixelSize(); | |||
if (cm.hasAlpha()) { | |||
pixelSize -= 8; | |||
} | |||
//TODO Add support for CMYK images | |||
byte[] imageData = null; | |||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |||
ImageEncodingHelper.encodeRenderedImageAsRGB(renderedImage, baos); | |||
byte[] imageData = baos.toByteArray(); | |||
boolean colorImages = paintingState.isColorImages(); | |||
imageObjectInfo.setColor(colorImages); | |||
// convert to grayscale | |||
if (!colorImages) { | |||
baos.reset(); | |||
int bitsPerPixel = paintingState.getBitsPerPixel(); | |||
imageObjectInfo.setBitsPerPixel(bitsPerPixel); | |||
ImageEncodingHelper.encodeRGBAsGrayScale( | |||
imageData, dataWidth, dataHeight, bitsPerPixel, baos); | |||
boolean allowDirectEncoding = true; | |||
if (allowDirectEncoding && pixelSize <= maxPixelSize) { | |||
//Attempt to encode without resampling the image | |||
ImageEncodingHelper helper = new ImageEncodingHelper(renderedImage); | |||
ColorModel encodedColorModel = helper.getEncodedColorModel(); | |||
boolean directEncode = true; | |||
if (helper.getEncodedColorModel().getPixelSize() > maxPixelSize) { | |||
directEncode = false; //pixel size needs to be reduced | |||
} | |||
if (BitmapImageUtil.getColorIndexSize(renderedImage) > 2) { | |||
directEncode = false; //Lookup tables are not implemented, yet | |||
} | |||
if (BitmapImageUtil.isMonochromeImage(renderedImage) | |||
&& BitmapImageUtil.isZeroBlack(renderedImage)) { | |||
directEncode = false; //Passing additive/subtractive info not implemented, yet | |||
} | |||
if (directEncode) { | |||
log.debug("Encoding image directly..."); | |||
imageObjectInfo.setBitsPerPixel(encodedColorModel.getPixelSize()); | |||
helper.encode(baos); | |||
imageData = baos.toByteArray(); | |||
} | |||
} | |||
if (imageData == null) { | |||
log.debug("Encoding image via RGB..."); | |||
//Convert image to 24bit RGB | |||
ImageEncodingHelper.encodeRenderedImageAsRGB(renderedImage, baos); | |||
imageData = baos.toByteArray(); | |||
boolean colorImages = paintingState.isColorImages(); | |||
imageObjectInfo.setColor(colorImages); | |||
// convert to grayscale | |||
if (!colorImages) { | |||
log.debug("Converting RGB image to grayscale..."); | |||
baos.reset(); | |||
int bitsPerPixel = paintingState.getBitsPerPixel(); | |||
imageObjectInfo.setBitsPerPixel(bitsPerPixel); | |||
//TODO this should be done off the RenderedImage to avoid buffering the | |||
//intermediate 24bit image | |||
ImageEncodingHelper.encodeRGBAsGrayScale( | |||
imageData, dataWidth, dataHeight, bitsPerPixel, baos); | |||
imageData = baos.toByteArray(); | |||
} | |||
} | |||
imageObjectInfo.setData(imageData); | |||
// set object area info |
@@ -47,6 +47,8 @@ import org.apache.commons.io.output.ByteArrayOutputStream; | |||
import org.apache.xmlgraphics.image.GraphicsUtil; | |||
import org.apache.xmlgraphics.util.UnitConv; | |||
import org.apache.fop.util.BitmapImageUtil; | |||
/** | |||
* This class provides methods for generating PCL print files. | |||
*/ | |||
@@ -522,7 +524,7 @@ public class PCLGenerator { | |||
* @return the gray value | |||
*/ | |||
public final int convertToGray(int r, int g, int b) { | |||
return (r * 30 + g * 59 + b * 11) / 100; | |||
return BitmapImageUtil.convertToGray(r, g, b); | |||
} | |||
/** | |||
@@ -574,13 +576,7 @@ public class PCLGenerator { | |||
* @return true if it's a monochrome image | |||
*/ | |||
public static boolean isMonochromeImage(RenderedImage img) { | |||
ColorModel cm = img.getColorModel(); | |||
if (cm instanceof IndexColorModel) { | |||
IndexColorModel icm = (IndexColorModel)cm; | |||
return icm.getMapSize() == 2; | |||
} else { | |||
return false; | |||
} | |||
return BitmapImageUtil.isMonochromeImage(img); | |||
} | |||
/** | |||
@@ -589,7 +585,7 @@ public class PCLGenerator { | |||
* @return true if it's a grayscale image | |||
*/ | |||
public static boolean isGrayscaleImage(RenderedImage img) { | |||
return (img.getColorModel().getColorSpace().getNumComponents() == 1); | |||
return BitmapImageUtil.isGrayscaleImage(img); | |||
} | |||
private MonochromeBitmapConverter createMonochromeBitmapConverter() { | |||
@@ -766,22 +762,7 @@ public class PCLGenerator { | |||
} | |||
} | |||
if (src == null) { | |||
src = new BufferedImage(effDim.width, effDim.height, | |||
BufferedImage.TYPE_BYTE_GRAY); | |||
Graphics2D g2d = src.createGraphics(); | |||
try { | |||
g2d.setBackground(Color.white); | |||
g2d.setColor(Color.black); | |||
g2d.clearRect(0, 0, effDim.width, effDim.height); | |||
AffineTransform at = new AffineTransform(); | |||
double sx = effDim.getWidth() / orgDim.getWidth(); | |||
double sy = effDim.getHeight() / orgDim.getHeight(); | |||
at.scale(sx, sy); | |||
g2d.drawRenderedImage(img, at); | |||
} finally { | |||
g2d.dispose(); | |||
} | |||
src = BitmapImageUtil.convertToGrayscale(img, effDim); | |||
} | |||
MonochromeBitmapConverter converter = createMonochromeBitmapConverter(); | |||
converter.setHint("quality", "false"); | |||
@@ -793,22 +774,9 @@ public class PCLGenerator { | |||
setTransparencyMode(sourceTransparency || mask != null, true); | |||
paintMonochromeBitmap(red, effResolution); | |||
} else { | |||
//TODO untested! | |||
RenderedImage effImg = img; | |||
if (scaled) { | |||
BufferedImage buf = new BufferedImage(effDim.width, effDim.height, | |||
BufferedImage.TYPE_BYTE_BINARY); | |||
Graphics2D g2d = buf.createGraphics(); | |||
try { | |||
AffineTransform at = new AffineTransform(); | |||
double sx = effDim.getWidth() / orgDim.getWidth(); | |||
double sy = effDim.getHeight() / orgDim.getHeight(); | |||
at.scale(sx, sy); | |||
g2d.drawRenderedImage(img, at); | |||
} finally { | |||
g2d.dispose(); | |||
} | |||
effImg = buf; | |||
effImg = BitmapImageUtil.convertToMonochrome(img, effDim); | |||
} | |||
setSourceTransparencyMode(sourceTransparency); | |||
selectCurrentPattern(0, 0); //Solid black |
@@ -0,0 +1,173 @@ | |||
/* | |||
* 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.util; | |||
import java.awt.Color; | |||
import java.awt.Dimension; | |||
import java.awt.Graphics2D; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.image.BufferedImage; | |||
import java.awt.image.ColorModel; | |||
import java.awt.image.IndexColorModel; | |||
import java.awt.image.RenderedImage; | |||
/** | |||
* Utility method for dealing with bitmap images. | |||
*/ | |||
public class BitmapImageUtil { | |||
/** | |||
* Indicates whether an image is a monochrome (1 bit black and white) image. | |||
* @param img the image | |||
* @return true if it's a monochrome image | |||
*/ | |||
public static final boolean isMonochromeImage(RenderedImage img) { | |||
return (getColorIndexSize(img) == 2); | |||
} | |||
/** | |||
* Indicates whether a zero bit indicates a black/dark pixel for a monochrome image. | |||
* @param img the image (must be 1 bit monochrome) | |||
* @return true if a zero bit indicates a black/dark pixel, false for a white/bright pixel | |||
*/ | |||
public static final boolean isZeroBlack(RenderedImage img) { | |||
if (!isMonochromeImage(img)) { | |||
throw new IllegalArgumentException("Image is not a monochrome image!"); | |||
} | |||
IndexColorModel icm = (IndexColorModel)img.getColorModel(); | |||
int gray0 = convertToGray(icm.getRGB(0)); | |||
int gray1 = convertToGray(icm.getRGB(1)); | |||
return gray0 < gray1; | |||
} | |||
/** | |||
* Convert an RGB color value to a grayscale from 0 to 100. | |||
* @param r the red component | |||
* @param g the green component | |||
* @param b the blue component | |||
* @return the gray value | |||
*/ | |||
public static final int convertToGray(int r, int g, int b) { | |||
return (r * 30 + g * 59 + b * 11) / 100; | |||
} | |||
/** | |||
* Convert an RGB color value to a grayscale from 0 to 100. | |||
* @param rgb the RGB value | |||
* @return the gray value | |||
*/ | |||
public static final int convertToGray(int rgb) { | |||
int r = (rgb & 0xFF0000) >> 16; | |||
int g = (rgb & 0xFF00) >> 8; | |||
int b = rgb & 0xFF; | |||
return convertToGray(r, g, b); | |||
} | |||
/** | |||
* Returns the size of the color index if the given image has one. | |||
* @param img the image | |||
* @return the size of the color index or 0 if there's no color index | |||
*/ | |||
public static final int getColorIndexSize(RenderedImage img) { | |||
ColorModel cm = img.getColorModel(); | |||
if (cm instanceof IndexColorModel) { | |||
IndexColorModel icm = (IndexColorModel)cm; | |||
return icm.getMapSize(); | |||
} else { | |||
return 0; | |||
} | |||
} | |||
/** | |||
* Indicates whether an image is a grayscale image. | |||
* @param img the image | |||
* @return true if it's a grayscale image | |||
*/ | |||
public static final boolean isGrayscaleImage(RenderedImage img) { | |||
return (img.getColorModel().getColorSpace().getNumComponents() == 1); | |||
} | |||
/** | |||
* Converts an image to sRGB. Optionally, the image can be scaled. | |||
* @param img the image to be converted | |||
* @param targetDimension the new target dimensions or null if no scaling is necessary | |||
* @return the sRGB image | |||
*/ | |||
public static final BufferedImage convertTosRGB(RenderedImage img, | |||
Dimension targetDimension) { | |||
return convertAndScaleImage(img, targetDimension, BufferedImage.TYPE_INT_RGB); | |||
} | |||
/** | |||
* Converts an image to a grayscale (8 bits) image. Optionally, the image can be scaled. | |||
* @param img the image to be converted | |||
* @param targetDimension the new target dimensions or null if no scaling is necessary | |||
* @return the grayscale image | |||
*/ | |||
public static final BufferedImage convertToGrayscale(RenderedImage img, | |||
Dimension targetDimension) { | |||
return convertAndScaleImage(img, targetDimension, BufferedImage.TYPE_BYTE_GRAY); | |||
} | |||
/** | |||
* Converts an image to a monochrome 1-bit image. Optionally, the image can be scaled. | |||
* @param img the image to be converted | |||
* @param targetDimension the new target dimensions or null if no scaling is necessary | |||
* @return the monochrome image | |||
*/ | |||
public static final BufferedImage convertToMonochrome(RenderedImage img, | |||
Dimension targetDimension) { | |||
return convertAndScaleImage(img, targetDimension, BufferedImage.TYPE_BYTE_BINARY); | |||
} | |||
private static BufferedImage convertAndScaleImage(RenderedImage img, | |||
Dimension targetDimension, int imageType) { | |||
Dimension bmpDimension = targetDimension; | |||
if (bmpDimension == null) { | |||
bmpDimension = new Dimension(img.getWidth(), img.getHeight()); | |||
} | |||
BufferedImage target = new BufferedImage(bmpDimension.width, bmpDimension.height, | |||
imageType); | |||
transferImage(img, target); | |||
return target; | |||
} | |||
private static void transferImage(RenderedImage source, BufferedImage target) { | |||
Graphics2D g2d = target.createGraphics(); | |||
try { | |||
g2d.setBackground(Color.white); | |||
g2d.setColor(Color.black); | |||
g2d.clearRect(0, 0, target.getWidth(), target.getHeight()); | |||
AffineTransform at = new AffineTransform(); | |||
if (source.getWidth() != target.getWidth() | |||
|| source.getHeight() != target.getHeight()) { | |||
double sx = target.getWidth() / (double)source.getWidth(); | |||
double sy = target.getHeight() / (double)source.getHeight(); | |||
at.scale(sx, sy); | |||
} | |||
g2d.drawRenderedImage(source, at); | |||
} finally { | |||
g2d.dispose(); | |||
} | |||
} | |||
} |