/* Copyright (C) 2012 Volker Berlin (i-net software) This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Jeroen Frijters jeroen@frijters.net */ namespace ikvm.awt { using java.awt.image; using System.IO; using System.Drawing; /// /// Encodes images in ICO format. /// internal class IconFactory { /// /// Indicates that ICO data represents an icon (.ICO). /// private const int TYPE_ICON = 1; private BinaryWriter writer; /// /// Creates a new instance of IconFactory internal IconFactory() { } /// /// Create a Icon from the given list of images /// /// list of images /// internal Icon CreateIcon(java.util.List images, Size size) { MemoryStream stream = new MemoryStream(); Write(images, stream); stream.Position = 0; return new Icon(stream, size); } /// /// Encodes and writes multiple images without colour depth conversion. /// /// /// the list of source images to be encoded /// /// the output to which the encoded image will be written internal void Write(java.util.List images, System.IO.Stream stream) { writer = new BinaryWriter(stream); int count = images.size(); // file header 6 WriteFileHeader(count, TYPE_ICON); // file offset where images start int fileOffset = 6 + count * 16; // icon entries 16 * count for (int i = 0; i < count; i++) { BufferedImage imgc = (BufferedImage)images.get(i); fileOffset += WriteIconEntry(imgc, fileOffset); } // images for (int i = 0; i < count; i++) { BufferedImage imgc = (BufferedImage)images.get(i); // info header WriteInfoHeader(imgc); // color map if (imgc.getColorModel().getPixelSize() <= 8) { IndexColorModel icm = (IndexColorModel)imgc.getColorModel(); WriteColorMap(icm); } // xor bitmap WriteXorBitmap(imgc); // and bitmap WriteAndBitmap(imgc); } } /// /// Writes the ICO file header for an ICO containing the given number of /// images. /// /// /// the number of images in the ICO /// /// TYPE_ICON private void WriteFileHeader(int count, int type) { // reserved 2 writer.Write((short)0); // type 2 writer.Write((short)type); // count 2 writer.Write((short)count); } /// /// Encodes the AND bitmap for the given image according the its /// alpha channel (transparency) and writes it to the given output. /// /// /// the image to encode as the AND bitmap. private void WriteAndBitmap(BufferedImage img) { WritableRaster alpha = img.getAlphaRaster(); // indexed transparency (eg. GIF files) if (img.getColorModel() is IndexColorModel && img.getColorModel().hasAlpha()) { int w = img.getWidth(); int h = img.getHeight(); int bytesPerLine = 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); } writer.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 = 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--) { writer.Write(line); } } // transparency (ARGB, etc. eg. PNG) else { int w = img.getWidth(); int h = img.getHeight(); int bytesPerLine = 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 & 1; line[bi] = SetBit(line[bi], i, b); } writer.Write(line); } } } private void WriteXorBitmap(BufferedImage img) { Raster raster = img.getRaster(); switch (img.getColorModel().getPixelSize()) { case 1: Write1(raster); break; case 4: Write4(raster); break; case 8: Write8(raster); break; case 24: Write24(raster); break; case 32: Raster alpha = img.getAlphaRaster(); Write32(raster, alpha); break; } } /// /// Writes the InfoHeader structure to output /// /// private void WriteInfoHeader(BufferedImage img) { // Size of InfoHeader structure = 40 writer.Write(40); // Width writer.Write(img.getWidth()); // Height writer.Write(img.getHeight() * 2); // Planes (=1) writer.Write((short)1); // Bit count writer.Write((short)img.getColorModel().getPixelSize()); // Compression writer.Write(0); // Image size - compressed size of image or 0 if Compression = 0 writer.Write(0); // horizontal resolution pixels/meter writer.Write(0); // vertical resolution pixels/meter writer.Write(0); // Colors used - number of colors actually used writer.Write(0); // Colors important - number of important colors 0 = all writer.Write(0); } /// /// Writes the IconEntry structure to output /// private int WriteIconEntry(BufferedImage img, int fileOffset) { // Width 1 byte Cursor Width (16, 32 or 64) int width = img.getWidth(); writer.Write((byte)(width == 256 ? 0 : width)); // Height 1 byte Cursor Height (16, 32 or 64 , most commonly = Width) int height = img.getHeight(); writer.Write((byte)(height == 256 ? 0 : height)); // ColorCount 1 byte Number of Colors (2,16, 0=256) short BitCount = (short)img.getColorModel().getPixelSize(); int NumColors = 1 << (BitCount == 32 ? 24 : (int)BitCount); byte ColorCount = (byte)(NumColors >= 256 ? 0 : NumColors); writer.Write((byte)ColorCount); // Reserved 1 byte =0 writer.Write((byte)0); // Planes 2 byte =1 writer.Write((short)1); // BitCount 2 byte bits per pixel (1, 4, 8) writer.Write((short)BitCount); // SizeInBytes 4 byte Size of (InfoHeader + ANDbitmap + XORbitmap) int cmapSize = GetColorMapSize(BitCount); int xorSize = GetBitmapSize(width, height, BitCount); int andSize = GetBitmapSize(width, height, 1); int size = 40 + cmapSize + xorSize + andSize; writer.Write(size); // FileOffset 4 byte FilePos, where InfoHeader starts writer.Write(fileOffset); return size; } /// /// Writes the colour map resulting from the source IndexColorModel. /// /// /// the source IndexColorModel private void WriteColorMap(IndexColorModel icm) { int mapSize = icm.getMapSize(); for (int i = 0; i < mapSize; i++) { int rgb = icm.getRGB(i); byte r = (byte)(rgb >> 16); byte g = (byte)(rgb >> 8); byte b = (byte)(rgb); writer.Write(b); writer.Write(g); writer.Write(r); writer.Write((byte)0); } } /// /// Calculates the number of bytes per line required for the given width in /// pixels, for a 1-bit bitmap. Lines are always padded to the next 4-byte /// boundary. /// /// /// the width in pixels /// the number of bytes per line private int GetBytesPerLine1(int width) { int ret = (int)width / 8; if (ret % 4 != 0) { ret = (ret / 4 + 1) * 4; } return ret; } /// /// Calculates the number of bytes per line required for the given with in /// pixels, for a 4-bit bitmap. Lines are always padded to the next 4-byte /// boundary. /// /// /// the width in pixels /// the number of bytes per line private int GetBytesPerLine4(int width) { int ret = (int)width / 2; if (ret % 4 != 0) { ret = (ret / 4 + 1) * 4; } return ret; } /// /// Calculates the number of bytes per line required for the given with in /// pixels, for a 8-bit bitmap. Lines are always padded to the next 4-byte /// boundary. /// /// /// the width in pixels /// the number of bytes per line private int GetBytesPerLine8(int width) { int ret = width; if (ret % 4 != 0) { ret = (ret / 4 + 1) * 4; } return ret; } /// /// Calculates the number of bytes per line required for the given with in /// pixels, for a 24-bit bitmap. Lines are always padded to the next 4-byte /// boundary. /// /// /// the width in pixels /// the number of bytes per line private int GetBytesPerLine24(int width) { int ret = width * 3; if (ret % 4 != 0) { ret = (ret / 4 + 1) * 4; } return ret; } /// /// Calculates the size in bytes of a bitmap with the specified size and /// colour depth. /// /// /// the width in pixels /// /// the height in pixels /// /// the colour depth (bits per pixel) /// the size of the bitmap in bytes private int GetBitmapSize(int w, int h, int bpp) { int bytesPerLine = 0; switch (bpp) { case 1: bytesPerLine = GetBytesPerLine1(w); break; case 4: bytesPerLine = GetBytesPerLine4(w); break; case 8: bytesPerLine = GetBytesPerLine8(w); break; case 24: bytesPerLine = GetBytesPerLine24(w); break; case 32: bytesPerLine = w * 4; break; } int ret = bytesPerLine * h; return ret; } /// /// Encodes and writes raster data as a 1-bit bitmap. /// /// /// the source raster data private void Write1(Raster raster) { int bytesPerLine = GetBytesPerLine1(raster.getWidth()); byte[] line = new byte[bytesPerLine]; for (int y = raster.getHeight() - 1; y >= 0; y--) { for (int i = 0; i < bytesPerLine; i++) { line[i] = 0; } for (int x = 0; x < raster.getWidth(); x++) { int bi = x / 8; int i = x % 8; int index = raster.getSample(x, y, 0); line[bi] = SetBit(line[bi], i, index); } writer.Write(line); } } /// /// Encodes and writes raster data as a 4-bit bitmap. /// /// /// the source raster data private void Write4(Raster raster) { int width = raster.getWidth(); int height = raster.getHeight(); // calculate bytes per line int bytesPerLine = GetBytesPerLine4(width); // line buffer byte[] line = new byte[bytesPerLine]; // encode and write lines for (int y = height - 1; y >= 0; y--) { // clear line buffer for (int i = 0; i < bytesPerLine; i++) { line[i] = 0; } // encode raster data for line for (int x = 0; x < width; x++) { // calculate buffer index int bi = x / 2; // calculate nibble index (high order or low order) int i = x % 2; // get color index int index = raster.getSample(x, y, 0); // set color index in buffer line[bi] = SetNibble(line[bi], i, index); } // write line data (padding bytes included) writer.Write(line); } } /// /// Encodes and writes raster data as an 8-bit bitmap. /// /// /// the source raster data private void Write8(Raster raster) { int width = raster.getWidth(); int height = raster.getHeight(); // calculate bytes per line int bytesPerLine = GetBytesPerLine8(width); // write lines for (int y = height - 1; y >= 0; y--) { // write raster data for each line for (int x = 0; x < width; x++) { // get color index for pixel byte index = (byte)raster.getSample(x, y, 0); // write color index writer.Write(index); } // write padding bytes at end of line for (int i = width; i < bytesPerLine; i++) { writer.Write((byte)0); } } } /// /// Encodes and writes raster data as a 24-bit bitmap. /// /// /// the source raster data private void Write24(Raster raster) { int width = raster.getWidth(); int height = raster.getHeight(); // calculate bytes per line int bytesPerLine = GetBytesPerLine24(width); // write lines for (int y = height - 1; y >= 0; y--) { // write pixel data for each line for (int x = 0; x < width; x++) { // get RGB values for pixel byte r = (byte)raster.getSample(x, y, 0); byte g = (byte)raster.getSample(x, y, 1); byte b = (byte)raster.getSample(x, y, 2); // write RGB values writer.Write(b); writer.Write(g); writer.Write(r); } // write padding bytes at end of line for (int i = width * 3; i < bytesPerLine; i++) { writer.Write((byte)0); } } } /// /// Encodes and writes raster data, together with alpha (transparency) data, /// as a 32-bit bitmap. /// /// /// the source raster data /// /// the source alpha data private void Write32(Raster raster, Raster alpha) { int width = raster.getWidth(); int height = raster.getHeight(); // write lines for (int y = height - 1; y >= 0; y--) { // write pixel data for each line for (int x = 0; x < width; x++) { // get RGBA values byte r = (byte)raster.getSample(x, y, 0); byte g = (byte)raster.getSample(x, y, 1); byte b = (byte)raster.getSample(x, y, 2); byte a = (byte)alpha.getSample(x, y, 0); // write RGBA values writer.Write(b); writer.Write(g); writer.Write(r); writer.Write(a); } } } /// /// Sets a particular bit in a byte. /// /// /// the source byte /// /// the index of the bit to set /// /// the value for the bit, which should be either 0 or /// 1. /// /// resultant byte private byte SetBit(byte bits, int index, int bit) { if (bit == 0) { bits &= (byte)~(1 << (7 - index)); } else { bits |= (byte)(1 << (7 - index)); } return bits; } /// /// Sets a particular nibble (4 bits) in a byte. /// /// /// the source byte /// /// the index of the nibble to set /// /// value for the nibble, which should be in the range /// 0x0..0xF. private byte SetNibble(byte nibbles, int index, int nibble) { nibbles |= (byte)(nibble << ((1 - index) * 4)); return nibbles; } /// /// Calculates the size in bytes for a colour map with the specified bit /// count. /// /// /// the bit count, which represents the colour depth /// the size of the colour map, in bytes if sBitCount is /// less than or equal to 8, otherwise 0 as colour maps are /// only used for bitmaps with a colour depth of 8 bits or less. private int GetColorMapSize(short sBitCount) { int ret = 0; if (sBitCount <= 8) { ret = (1 << sBitCount) * 4; } return ret; } } }