blob: 1a91882af03923a89228cf3f50d9a602b11e2484 [file] [log] [blame]
/*
* 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.xmlgraphics.ps;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DirectColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.xmlgraphics.image.GraphicsUtil;
/**
* Helper class for encoding bitmap images.
*/
public class ImageEncodingHelper {
private static final ColorModel DEFAULT_RGB_COLOR_MODEL = new ComponentColorModel(
ColorSpace.getInstance(ColorSpace.CS_sRGB),
false, false, ColorModel.OPAQUE, DataBuffer.TYPE_BYTE);
private final RenderedImage image;
private ColorModel encodedColorModel;
private boolean firstTileDump;
private boolean enableCMYK;
private boolean isBGR;
private boolean isKMYC;
private boolean outputbw;
private boolean bwinvert;
/**
* Main constructor
* @param image the image
*/
public ImageEncodingHelper(RenderedImage image) {
this(image, true);
outputbw = true;
}
/**
* Main constructor
* @param image the image
* @param enableCMYK true to enable CMYK, false to disable
*/
public ImageEncodingHelper(RenderedImage image, boolean enableCMYK) {
this.image = image;
this.enableCMYK = enableCMYK;
determineEncodedColorModel();
}
/**
* Returns the associated image.
* @return the image
*/
public RenderedImage getImage() {
return this.image;
}
/**
* Returns the native {@link ColorModel} used by the image.
* @return the native color model
*/
public ColorModel getNativeColorModel() {
return getImage().getColorModel();
}
/**
* Returns the effective {@link ColorModel} used to encode the image. If this is different
* from the value returned by {@link #getNativeColorModel()} this means that the image
* is converted in order to encode it because no native encoding is currently possible.
* @return the effective color model
*/
public ColorModel getEncodedColorModel() {
return this.encodedColorModel;
}
/**
* Indicates whether the image has an alpha channel.
* @return true if the image has an alpha channel
*/
public boolean hasAlpha() {
return image.getColorModel().hasAlpha();
}
/**
* Indicates whether the image is converted during encodation.
* @return true if the image cannot be encoded in its native format
*/
public boolean isConverted() {
return getNativeColorModel() != getEncodedColorModel();
}
private void writeRGBTo(OutputStream out) throws IOException {
boolean encoded = encodeRenderedImageWithDirectColorModelAsRGB(image, out);
if (encoded) {
return;
}
encodeRenderedImageAsRGB(image, out, outputbw, bwinvert);
}
public static void encodeRenderedImageAsRGB(RenderedImage image, OutputStream out)
throws IOException {
encodeRenderedImageAsRGB(image, out, false, false);
}
/**
* Writes a RenderedImage to an OutputStream by converting it to RGB.
* @param image the image
* @param out the OutputStream to write the pixels to
* @throws IOException if an I/O error occurs
*/
public static void encodeRenderedImageAsRGB(RenderedImage image, OutputStream out,
boolean outputbw, boolean bwinvert) throws IOException {
Raster raster = getRaster(image);
Object data;
int nbands = raster.getNumBands();
int dataType = raster.getDataBuffer().getDataType();
switch (dataType) {
case DataBuffer.TYPE_BYTE:
data = new byte[nbands];
break;
case DataBuffer.TYPE_USHORT:
data = null;
break;
case DataBuffer.TYPE_INT:
data = new int[nbands];
break;
case DataBuffer.TYPE_FLOAT:
data = new float[nbands];
break;
case DataBuffer.TYPE_DOUBLE:
data = new double[nbands];
break;
default:
throw new IllegalArgumentException("Unknown data buffer type: " + dataType);
}
ColorModel colorModel = image.getColorModel();
int w = image.getWidth();
int h = image.getHeight();
int numDataElements = 3;
if (colorModel.getPixelSize() == 1 && outputbw) {
numDataElements = 1;
}
byte[] buf = new byte[w * numDataElements];
for (int y = 0; y < h; y++) {
int idx = -1;
for (int x = 0; x < w; x++) {
int rgb = colorModel.getRGB(raster.getDataElements(x, y, data));
if (numDataElements > 1) {
buf[++idx] = (byte)(rgb >> 16);
buf[++idx] = (byte)(rgb >> 8);
} else if (bwinvert && rgb == -1) {
rgb = 1;
}
buf[++idx] = (byte)(rgb);
}
out.write(buf);
}
}
/**
* Writes a RenderedImage to an OutputStream. This method optimizes the encoding
* of the {@link DirectColorModel} as it is returned by {@link ColorModel#getRGBdefault}.
* @param image the image
* @param out the OutputStream to write the pixels to
* @return true if this method encoded this image, false if the image is incompatible
* @throws IOException if an I/O error occurs
*/
public static boolean encodeRenderedImageWithDirectColorModelAsRGB(
RenderedImage image, OutputStream out) throws IOException {
ColorModel cm = image.getColorModel();
if (cm.getColorSpace() != ColorSpace.getInstance(ColorSpace.CS_sRGB)) {
return false; //Need to go through color management
}
if (!(cm instanceof DirectColorModel)) {
return false; //Only DirectColorModel is supported here
}
DirectColorModel dcm = (DirectColorModel)cm;
final int[] templateMasks = new int[]
{0x00ff0000 /*R*/, 0x0000ff00 /*G*/, 0x000000ff /*B*/, 0xff000000 /*A*/};
int[] masks = dcm.getMasks();
if (!Arrays.equals(templateMasks, masks)) {
return false; //no flexibility here right now, might never be used anyway
}
Raster raster = getRaster(image);
int dataType = raster.getDataBuffer().getDataType();
if (dataType != DataBuffer.TYPE_INT) {
return false; //not supported
}
int w = image.getWidth();
int h = image.getHeight();
int[] data = new int[w];
byte[] buf = new byte[w * 3];
for (int y = 0; y < h; y++) {
int idx = -1;
raster.getDataElements(0, y, w, 1, data);
for (int x = 0; x < w; x++) {
int rgb = data[x];
buf[++idx] = (byte)(rgb >> 16);
buf[++idx] = (byte)(rgb >> 8);
buf[++idx] = (byte)(rgb);
}
out.write(buf);
}
return true;
}
private static Raster getRaster(RenderedImage image) {
if (image instanceof BufferedImage) {
return ((BufferedImage)image).getRaster();
} else {
//Note: this copies the image data (double memory consumption)
//TODO Investigate encoding in stripes: RenderedImage.copyData(WritableRaster)
return image.getData();
}
}
/**
* Converts a byte array containing 24 bit RGB image data to a grayscale
* image.
*
* @param raw
* the buffer containing the RGB image data
* @param width
* the width of the image in pixels
* @param height
* the height of the image in pixels
* @param bitsPerPixel
* the number of bits to use per pixel
* @param out the OutputStream to write the pixels to
*
* @throws IOException if an I/O error occurs
*/
public static void encodeRGBAsGrayScale(
byte[] raw, int width, int height, int bitsPerPixel, OutputStream out)
throws IOException {
int pixelsPerByte = 8 / bitsPerPixel;
int bytewidth = (width / pixelsPerByte);
if ((width % pixelsPerByte) != 0) {
bytewidth++;
}
//TODO Rewrite to encode directly from a RenderedImage to avoid buffering the whole RGB
//image in memory
byte[] linedata = new byte[bytewidth];
byte ib;
for (int y = 0; y < height; y++) {
ib = 0;
int i = 3 * y * width;
for (int x = 0; x < width; x++, i += 3) {
// see http://www.jguru.com/faq/view.jsp?EID=221919
double greyVal = 0.212671d * (raw[i] & 0xff) + 0.715160d
* (raw[i + 1] & 0xff) + 0.072169d
* (raw[i + 2] & 0xff);
switch (bitsPerPixel) {
case 1:
if (greyVal < 128) {
ib |= (byte) (1 << (7 - (x % 8)));
}
break;
case 4:
greyVal /= 16;
ib |= (byte) ((byte) greyVal << ((1 - (x % 2)) * 4));
break;
case 8:
ib = (byte) greyVal;
break;
default:
throw new UnsupportedOperationException(
"Unsupported bits per pixel: " + bitsPerPixel);
}
if ((x % pixelsPerByte) == (pixelsPerByte - 1)
|| ((x + 1) == width)) {
linedata[(x / pixelsPerByte)] = ib;
ib = 0;
}
}
out.write(linedata);
}
}
private boolean optimizedWriteTo(OutputStream out)
throws IOException {
if (this.firstTileDump) {
Raster raster = image.getTile(0, 0);
DataBuffer buffer = raster.getDataBuffer();
if (buffer instanceof DataBufferByte) {
byte[] bytes = ((DataBufferByte) buffer).getData();
// see determineEncodingColorModel() to see why we permute B and R here
if (isBGR) {
byte[] bytesPermutated = new byte[bytes.length];
for (int i = 0; i < bytes.length; i += 3) {
bytesPermutated[i] = bytes[i + 2];
bytesPermutated[i + 1] = bytes[i + 1];
bytesPermutated[i + 2] = bytes[i];
}
out.write(bytesPermutated);
} else if (isKMYC) {
byte[] bytesPermutated = new byte[bytes.length];
for (int i = 0; i < bytes.length; i += 4) {
bytesPermutated[i] = bytes[i + 3];
bytesPermutated[i + 1] = bytes[i + 2];
bytesPermutated[i + 2] = bytes[i + 1];
bytesPermutated[i + 3] = bytes[i];
}
out.write(bytesPermutated);
} else {
out.write(bytes);
}
return true;
}
}
return false;
}
/**
* Indicates whether the image consists of multiple tiles.
* @return true if there are multiple tiles
*/
protected boolean isMultiTile() {
int tilesX = image.getNumXTiles();
int tilesY = image.getNumYTiles();
return (tilesX != 1 || tilesY != 1);
}
/**
* Determines the color model used for encoding the image.
*/
protected void determineEncodedColorModel() {
this.firstTileDump = false;
this.encodedColorModel = DEFAULT_RGB_COLOR_MODEL;
ColorModel cm = image.getColorModel();
ColorSpace cs = cm.getColorSpace();
int numComponents = cm.getNumComponents();
if (!isMultiTile()) {
if (numComponents == 1 && cs.getType() == ColorSpace.TYPE_GRAY) {
if (cm.getTransferType() == DataBuffer.TYPE_BYTE) {
this.firstTileDump = true;
this.encodedColorModel = cm;
}
} else if (cm instanceof IndexColorModel) {
if (cm.getTransferType() == DataBuffer.TYPE_BYTE) {
this.firstTileDump = true;
this.encodedColorModel = cm;
}
} else if (cm instanceof ComponentColorModel
&& (numComponents == 3 || (enableCMYK && numComponents == 4))
&& !cm.hasAlpha()) {
Raster raster = image.getTile(0, 0);
DataBuffer buffer = raster.getDataBuffer();
SampleModel sampleModel = raster.getSampleModel();
if (sampleModel instanceof PixelInterleavedSampleModel) {
PixelInterleavedSampleModel piSampleModel;
piSampleModel = (PixelInterleavedSampleModel)sampleModel;
int[] offsets = piSampleModel.getBandOffsets();
for (int i = 0; i < offsets.length; i++) {
if (offsets[i] != i && offsets[i] != offsets.length - 1 - i) {
//Don't encode directly as samples are not next to each other
//i.e. offsets are not 012 (RGB) or 0123 (CMYK)
// let also pass 210 BGR and 3210 (KYMC); 3210 will be skipped below
// if 210 (BGR) the B and R bytes will be permuted later in optimizeWriteTo()
return;
}
}
// check if we are in a BGR case; this is added here as a workaround for bug fix
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6549882 that causes some PNG
// images to be loaded as BGR with the consequence that performance was being impacted
this.isBGR = false;
if (offsets.length == 3 && offsets[0] == 2 && offsets[1] == 1 && offsets[2] == 0) {
this.isBGR = true;
}
// make sure we did not get here due to a KMYC image
if (offsets.length == 4 && offsets[0] == 3 && offsets[1] == 2 && offsets[2] == 1
&& offsets[3] == 0) {
isKMYC = true;
}
}
if (cm.getTransferType() == DataBuffer.TYPE_BYTE
&& buffer.getOffset() == 0
&& buffer.getNumBanks() == 1) {
this.firstTileDump = true;
this.encodedColorModel = cm;
}
}
}
}
/**
* Encodes the image and writes everything to the given OutputStream.
* @param out the OutputStream
* @throws IOException if an I/O error occurs
*/
public void encode(OutputStream out) throws IOException {
if (!isConverted()) {
if (optimizedWriteTo(out)) {
return;
}
}
writeRGBTo(out);
}
/**
* Encodes the image's alpha channel. If it doesn't have an alpha channel, an
* {@link IllegalStateException} is thrown.
* @param out the OutputStream
* @throws IOException if an I/O error occurs
*/
public void encodeAlpha(OutputStream out) throws IOException {
if (!hasAlpha()) {
throw new IllegalStateException("Image doesn't have an alpha channel");
}
Raster alpha = GraphicsUtil.getAlphaRaster(image);
DataBuffer buffer = alpha.getDataBuffer();
if (buffer instanceof DataBufferByte) {
out.write(((DataBufferByte)buffer).getData());
} else {
throw new UnsupportedOperationException(
"Alpha raster not supported: " + buffer.getClass().getName());
}
}
/**
* Writes all pixels (color components only) of a RenderedImage to an OutputStream.
* @param image the image to be encoded
* @param out the OutputStream to write to
* @throws IOException if an I/O error occurs
*/
public static void encodePackedColorComponents(RenderedImage image, OutputStream out)
throws IOException {
ImageEncodingHelper helper = new ImageEncodingHelper(image);
helper.encode(out);
}
/**
* Create an ImageEncoder for the given RenderImage instance.
* @param img the image
* @return the requested ImageEncoder
*/
public static ImageEncoder createRenderedImageEncoder(RenderedImage img) {
return new RenderedImageEncoder(img);
}
/**
* ImageEncoder implementation for RenderedImage instances.
*/
private static class RenderedImageEncoder implements ImageEncoder {
private final RenderedImage img;
public RenderedImageEncoder(RenderedImage ri) {
if (ri instanceof BufferedImage && ((BufferedImage) ri).getType() == BufferedImage.TYPE_4BYTE_ABGR) {
BufferedImage convertedImg =
new BufferedImage(ri.getWidth(), ri.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) convertedImg.getGraphics();
g.setBackground(Color.WHITE);
g.clearRect(0, 0, ri.getWidth(), ri.getHeight());
g.drawImage((BufferedImage)ri, 0, 0, null);
g.dispose();
ri = convertedImg;
}
img = ri;
}
public void writeTo(OutputStream out) throws IOException {
ImageEncodingHelper.encodePackedColorComponents(img, out);
}
public String getImplicitFilter() {
return null; //No implicit filters with RenderedImage instances
}
}
public void setBWInvert(boolean v) {
bwinvert = v;
}
}