/*
* 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: ImageEncodingHelper.java 1713619 2015-11-10 11:55:53Z ssteiner $ */
package org.apache.xmlgraphics.ps;
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.
/**
* 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
Params: - image – the image
/**
* Main constructor
* @param image the image
*/
public ImageEncodingHelper(RenderedImage image) {
this(image, true);
outputbw = true;
}
Main constructor
Params: - image – the image
- enableCMYK – true to enable CMYK, false to disable
/**
* 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.
Returns: the image
/**
* Returns the associated image.
* @return the image
*/
public RenderedImage getImage() {
return this.image;
}
Returns the native ColorModel
used by the image. Returns: the native color model
/**
* Returns the native {@link ColorModel} used by the image.
* @return the native color model
*/
public ColorModel getNativeColorModel() {
return getImage().getColorModel();
}
Returns the effective ColorModel
used to encode the image. If this is different from the value returned by getNativeColorModel()
this means that the image is converted in order to encode it because no native encoding is currently possible. Returns: the effective color model
/**
* 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.
Returns: true if the image has an alpha channel
/**
* 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.
Returns: true if the image cannot be encoded in its native format
/**
* 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.
Params: - image – the image
- out – the OutputStream to write the pixels to
Throws: - IOException – if an I/O error occurs
/**
* 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 DirectColorModel
as it is returned by ColorModel.getRGBdefault
. Params: - image – the image
- out – the OutputStream to write the pixels to
Throws: - IOException – if an I/O error occurs
Returns: true if this method encoded this image, false if the image is incompatible
/**
* 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.
Params: - raw –
the buffer containing the RGB image data
- width –
the width of the image in pixels
- height –
the height of the image in pixels
- bitsPerPixel –
the number of bits to use per pixel
- out – the OutputStream to write the pixels to
Throws: - IOException – if an I/O error occurs
/**
* 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.
Returns: true if there are multiple tiles
/**
* 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.
/**
* 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.
Params: - out – the OutputStream
Throws: - IOException – if an I/O error occurs
/**
* 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 IllegalStateException
is thrown. Params: - out – the OutputStream
Throws: - IOException – if an I/O error occurs
/**
* 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.
Params: - image – the image to be encoded
- out – the OutputStream to write to
Throws: - IOException – if an I/O error occurs
/**
* 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.
Params: - img – the image
Returns: the requested ImageEncoder
/**
* 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.
/**
* ImageEncoder implementation for RenderedImage instances.
*/
private static class RenderedImageEncoder implements ImageEncoder {
private final RenderedImage img;
public RenderedImageEncoder(RenderedImage img) {
this.img = img;
}
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;
}
}