/*
* ICOEncoder.java
*
* Created on 12 May 2006, 04:08
*
* To change this template, choose Tools | Template Manager
* and open the template in the editor.
*/
package com.inet.gradle.setup.image.image4j.codec.ico;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.util.List;
import javax.imageio.ImageWriter;
import com.inet.gradle.setup.image.image4j.codec.bmp.BMPEncoder;
import com.inet.gradle.setup.image.image4j.codec.bmp.InfoHeader;
import com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream;
import com.inet.gradle.setup.image.image4j.util.ConvertUtil;
/**
* Encodes images in ICO format.
* @author Ian McDonagh
*/
public class ICOEncoder {
/** Creates a new instance of ICOEncoder */
private ICOEncoder() {
}
/**
* Encodes and writes a single image to file without colour depth conversion.
* @param image the source image to encode
* @param file the output file to which the encoded image will be written
* @throws java.io.IOException if an exception occurs
*/
public static void write(BufferedImage image, java.io.File file) throws IOException {
write(image, -1, file);
}
/**
* Encodes and writes a single image without colour depth conversion.
* @param image the source image to encode
* @param os the output to which the encoded image will be written
* @throws java.io.IOException if an exception occurs
*/
public static void write(BufferedImage image, java.io.OutputStream os) throws IOException {
write(image, -1, os);
}
/**
* Encodes and writes multiple images without colour depth conversion.
* @param images the list of source images to be encoded
* @param os the output to which the encoded image will be written
* @throws java.io.IOException if an error occurs
*/
public static void write(List<BufferedImage> images, java.io.OutputStream os) throws IOException {
write(images, null, null, os);
}
/**
* Encodes and writes multiple images to file without colour depth conversion.
* @param images the list of source images to encode
* @param file the file to which the encoded images will be written
* @throws java.io.IOException if an exception occurs
*/
public static void write(List<BufferedImage> images, java.io.File file) throws IOException {
write(images, null, file);
}
/**
* Encodes and writes multiple images to file with the colour depth conversion using the specified values.
* @param images the list of source images to encode
* @param bpp array containing desired colour depths for colour depth conversion
* @param file the output file to which the encoded images will be written
* @throws java.io.IOException if an error occurs
*/
public static void write(List<BufferedImage> images, int[] bpp, java.io.File file) throws IOException {
write(images, bpp, new java.io.FileOutputStream(file));
}
/**
* Encodes and outputs a list of images in ICO format. The first image in the list will be at index #0 in the ICO file, the second at index #1, and so on.
* @param images List of images to encode, which will be output in the order supplied in the list.
* @param bpp Array containing the color depth (bits per pixel) for encoding the corresponding image at each index in the <tt>images</tt> list. If the array is <tt>null</tt>, no colour depth conversion will be performed. A colour depth value of <tt>-1</tt> at a particular index indicates that no colour depth conversion should be performed for that image.
* @param compress Array containing the compression flag for the corresponding image at each index in the <tt>images</tt> list. If the array is <tt>null</tt>, no compression will be peformed. A value of <tt>true</tt> specifies that compression should be performed, while a value of <tt>false</tt> specifies that no compression should be performed.
* @param file the file to which the encoded images will be written.
* @throws java.io.IOException if an error occurred.
* @since 0.6
*/
public static void write(List<BufferedImage> images, int[] bpp, boolean[] compress, java.io.File file) throws IOException {
write(images, bpp, compress, new java.io.FileOutputStream(file));
}
/**
* Encodes and writes a single image to file with colour depth conversion using the specified value.
* @param image the source image to encode
* @param bpp the colour depth (bits per pixel) for the colour depth conversion, or <tt>-1</tt> if no colour depth conversion should be performed
* @param file the output file to which the encoded image will be written
* @throws java.io.IOException if an error occurs
*/
public static void write(BufferedImage image, int bpp, java.io.File file) throws IOException {
write(image, bpp, new java.io.FileOutputStream(file));
}
/**
* Encodes and outputs a single image in ICO format.
* Convenience method, which calls {@link #write(java.util.List,int[],java.io.OutputStream) write(java.util.List,int[],java.io.OutputStream)}.
* @param image The image to encode.
* @param bpp Colour depth (in bits per pixel) for the colour depth conversion, or <tt>-1</tt> if no colour depth conversion should be performed.
* @param os The output to which the encoded image will be written.
* @throws java.io.IOException if an error occurs when trying to write the output.
*/
public static void write(BufferedImage image, int bpp, java.io.OutputStream os) throws IOException {
List<BufferedImage> list = new java.util.ArrayList<BufferedImage>(1);
list.add(image);
write(list, new int[] { bpp }, new boolean[] { false }, os);
}
/**
* Encodes and outputs a list of images in ICO format. The first image in the list will be at index #0 in the ICO file, the second at index #1, and so on.
* @param images List of images to encode, which will be output in the order supplied in the list.
* @param bpp Array containing the color depth (bits per pixel) for encoding the corresponding image at each index in the <tt>images</tt> list. If the array is <tt>null</tt>, no colour depth conversion will be performed. A colour depth value of <tt>-1</tt> at a particular index indicates that no colour depth conversion should be performed for that image.
* @param os The output to which the encoded images will be written.
* @throws java.io.IOException if an error occurred.
*/
public static void write(List<BufferedImage> images, int[] bpp, java.io.OutputStream os) throws IOException {
write(images, bpp, null, os);
}
/**
* Encodes and outputs a list of images in ICO format. The first image in the list will be at index #0 in the ICO file, the second at index #1, and so on.
* @param images List of images to encode, which will be output in the order supplied in the list.
* @param bpp Array containing the color depth (bits per pixel) for encoding the corresponding image at each index in the <tt>images</tt> list. If the array is <tt>null</tt>, no colour depth conversion will be performed. A colour depth value of <tt>-1</tt> at a particular index indicates that no colour depth conversion should be performed for that image.
* @param compress Array containing the compression flag for the corresponding image at each index in the <tt>images</tt> list. If the array is <tt>null</tt>, no compression will be peformed. A value of <tt>true</tt> specifies that compression should be performed, while a value of <tt>false</tt> specifies that no compression should be performed.
* @param os The output to which the encoded images will be written.
* @throws java.io.IOException if an error occurred.
* @since 0.6
*/
public static void write(List<BufferedImage> images, int[] bpp, boolean[] compress, java.io.OutputStream os) throws IOException {
LittleEndianOutputStream out = new LittleEndianOutputStream(os);
int count = images.size();
//file header 6
writeFileHeader(count, ICOConstants.TYPE_ICON, out);
//file offset where images start
int fileOffset = 6 + count * 16;
List<InfoHeader> infoHeaders = new java.util.ArrayList<InfoHeader>(count);
List<BufferedImage> converted = new java.util.ArrayList<BufferedImage>(count);
List<byte[]> compressedImages = null;
if (compress != null) {
compressedImages = new java.util.ArrayList<byte[]>(count);
}
javax.imageio.ImageWriter pngWriter = null;
//icon entries 16 * count
for (int i = 0; i < count; i++) {
BufferedImage img = images.get(i);
int b = bpp == null ? -1 : bpp[i];
//convert image
BufferedImage imgc = b == -1 ? img : convert(img, b);
converted.add(imgc);
//create info header
InfoHeader ih = BMPEncoder.createInfoHeader(imgc);
//create icon entry
IconEntry e = createIconEntry(ih);
if (compress != null) {
if (compress[i]) {
if (pngWriter == null) {
pngWriter = getPNGImageWriter();
}
byte[] compressedImage = encodePNG(pngWriter, imgc);
compressedImages.add(compressedImage);
e.iSizeInBytes = compressedImage.length;
} else {
compressedImages.add(null);
}
}
ih.iHeight *= 2;
e.iFileOffset = fileOffset;
fileOffset += e.iSizeInBytes;
e.write(out);
infoHeaders.add(ih);
}
//images
for (int i = 0; i < count; i++) {
BufferedImage img = images.get(i);
BufferedImage imgc = converted.get(i);
if (compress == null || !compress[i]) {
//info header
InfoHeader ih = infoHeaders.get(i);
ih.write(out);
//color map
if (ih.sBitCount <= 8) {
IndexColorModel icm = (IndexColorModel) imgc.getColorModel();
BMPEncoder.writeColorMap(icm, out);
}
//xor bitmap
writeXorBitmap(imgc, ih, out);
//and bitmap
writeAndBitmap(img, out);
}
else {
byte[] compressedImage = compressedImages.get(i);
out.write(compressedImage);
}
//javax.imageio.ImageIO.write(imgc, "png", new java.io.File("test_"+i+".png"));
}
}
/**
* Writes the ICO file header for an ICO containing the given number of images.
* @param count the number of images in the ICO
* @param type one of {@link com.inet.gradle.setup.image.image4j.codec.ico.ICOConstants#TYPE_ICON TYPE_ICON} or
* {@link com.inet.gradle.setup.image.image4j.codec.ico.ICOConstants#TYPE_CURSOR TYPE_CURSOR}
* @param out the output to which the file header will be written
* @throws java.io.IOException if an error occurs
*/
public static void writeFileHeader(int count, int type, LittleEndianOutputStream out) throws IOException {
//reserved 2
out.writeShortLE((short) 0);
//type 2
out.writeShortLE((short) type);
//count 2
out.writeShortLE((short) count);
}
/**
* Constructs an <tt>IconEntry</tt> from the given <tt>InfoHeader</tt>
* structure.
* @param ih the <tt>InfoHeader</tt> structure from which to construct the <tt>IconEntry</tt> structure.
* @return the <tt>IconEntry</tt> structure constructed from the <tt>IconEntry</tt> structure.
*/
public static IconEntry createIconEntry(InfoHeader ih) {
IconEntry ret = new IconEntry();
//width 1
ret.bWidth = ih.iWidth == 256 ? 0 : ih.iWidth;
//height 1
ret.bHeight = ih.iHeight == 256 ? 0 : ih.iHeight;
//color count 1
ret.bColorCount = ih.iNumColors >= 256 ? 0 : ih.iNumColors;
//reserved 1
ret.bReserved = 0;
//planes 2 = 1
ret.sPlanes = 1;
//bit count 2
ret.sBitCount = ih.sBitCount;
//sizeInBytes 4 - size of infoHeader + xor bitmap + and bitbmap
int cmapSize = BMPEncoder.getColorMapSize(ih.sBitCount);
int xorSize = BMPEncoder.getBitmapSize(ih.iWidth, ih.iHeight, ih.sBitCount);
int andSize = BMPEncoder.getBitmapSize(ih.iWidth, ih.iHeight, 1);
int size = ih.iSize + cmapSize + xorSize + andSize;
ret.iSizeInBytes = size;
//fileOffset 4
ret.iFileOffset = 0;
return ret;
}
/**
* Encodes the <em>AND</em> bitmap for the given image according the its alpha channel (transparency) and writes it to the given output.
* @param img the image to encode as the <em>AND</em> bitmap.
* @param out the output to which the <em>AND</em> bitmap will be written
* @throws java.io.IOException if an error occurs.
*/
public static void writeAndBitmap(BufferedImage img, com.inet.gradle.setup.image.image4j.io.LittleEndianOutputStream out) throws IOException {
WritableRaster alpha = img.getAlphaRaster();
//indexed transparency (eg. GIF files)
if (img.getColorModel() instanceof IndexColorModel && img.getColorModel().hasAlpha()) {
int w = img.getWidth();
int h = img.getHeight();
int bytesPerLine = BMPEncoder.getBytesPerLine1(w);
byte[] line = new byte[bytesPerLine];
IndexColorModel icm = (IndexColorModel) img.getColorModel();
Raster raster = img.getRaster();
for (int y = h - 1; y >= 0; y--) {
for (int x = 0; x < w; x++) {
int bi = x / 8;
int i = x % 8;
//int a = alpha.getSample(x, y, 0);
int p = raster.getSample(x, y, 0);
int a = icm.getAlpha(p);
//invert bit since and mask is applied to xor mask
int b = ~a & 1;
line[bi] = setBit(line[bi], i, b);
}
out.write(line);
}
}
//no transparency
else if (alpha == null) {
int h = img.getHeight();
int w = img.getWidth();
//calculate number of bytes per line, including 32-bit padding
int bytesPerLine = BMPEncoder.getBytesPerLine1(w);
byte[] line = new byte[bytesPerLine];
for (int i = 0; i < bytesPerLine; i++) {
line[i] = (byte) 0;
}
for (int y = h - 1; y >= 0; y--) {
out.write(line);
}
}
//transparency (ARGB, etc. eg. PNG)
else {
//BMPEncoder.write1(alpha, cmap, out);
int w = img.getWidth();
int h = img.getHeight();
int bytesPerLine = BMPEncoder.getBytesPerLine1(w);
byte[] line = new byte[bytesPerLine];
for (int y = h - 1; y >= 0; y--) {
for (int x = 0; x < w; x++) {
int bi = x / 8;
int i = x % 8;
int a = alpha.getSample(x, y, 0);
//invert bit since and mask is applied to xor mask
int b = a == 0 ? 1 : 0;
line[bi] = setBit(line[bi], i, b);
}
out.write(line);
}
}
}
private static byte setBit(byte bits, int index, int bit) {
int mask = 1 << (7 - index);
bits &= ~mask;
bits |= bit << (7 - index);
return bits;
}
private static void writeXorBitmap(BufferedImage img, InfoHeader ih, LittleEndianOutputStream out) throws IOException {
Raster raster = img.getRaster();
switch (ih.sBitCount) {
case 1:
BMPEncoder.write1(raster, out);
break;
case 4:
BMPEncoder.write4(raster, out);
break;
case 8:
BMPEncoder.write8(raster, out);
break;
case 24:
BMPEncoder.write24(raster, out);
break;
case 32:
Raster alpha = img.getAlphaRaster();
BMPEncoder.write32(raster, alpha, out);
break;
}
}
/**
* Utility method, which converts the given image to the specified colour depth.
* @param img the image to convert.
* @param bpp the target colour depth (bits per pixel) for the conversion.
* @return the given image converted to the specified colour depth.
*/
public static BufferedImage convert(BufferedImage img, int bpp) {
BufferedImage ret = null;
switch (bpp) {
case 1:
ret = ConvertUtil.convert1(img);
break;
case 4:
ret = ConvertUtil.convert4(img);
break;
case 8:
ret = ConvertUtil.convert8(img);
break;
case 24:
int b = img.getColorModel().getPixelSize();
if (b == 24 || b == 32) {
ret = img;
} else {
ret = ConvertUtil.convert24(img);
}
break;
case 32:
int b2 = img.getColorModel().getPixelSize();
if (b2 == 24 || b2 == 32) {
ret = img;
} else {
ret = ConvertUtil.convert32(img);
}
break;
}
return ret;
}
/**
* @since 0.6
*/
private static javax.imageio.ImageWriter getPNGImageWriter() {
javax.imageio.ImageWriter ret = null;
java.util.Iterator<javax.imageio.ImageWriter> itr = javax.imageio.ImageIO.getImageWritersByFormatName("png");
if (itr.hasNext()) {
ret = itr.next();
}
return ret;
}
/**
* @since 0.6
*/
private static byte[] encodePNG(ImageWriter pngWriter, BufferedImage img) throws IOException {
java.io.ByteArrayOutputStream bout = new java.io.ByteArrayOutputStream();
javax.imageio.stream.ImageOutputStream output = javax.imageio.ImageIO.createImageOutputStream(bout);
pngWriter.setOutput(output);
pngWriter.write(img);
bout.flush();
byte[] ret = bout.toByteArray();
return ret;
}
}