// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
namespace IconEditor
{
///
/// Describes the type of an icon entry.
///
public enum IconEntryType
{
///
/// Classic icons are drawn by AND-ing the background with the mask image,
/// then XOR-ing the real image. Classic icons can have the color depths
/// 1bit (2 colors), 4bit (16 colors) and 8bit (256 color).
/// Additionally, the mask image provides the 2 "colors" transparent and
/// invert background. Inverting only some colors of the background is
/// theoretically possible, but not used in practice.
/// Background color inversion is mainly used by cursors.
/// There are also 16bit or 24bit true-color classic icons, though
/// they don't make sense because they seem to be supported on XP,
/// in which case you should use real alpha-transparent 32bit icons.
///
Classic = 0,
///
/// True color icons were introduced by Windows XP and are not supported
/// by previous Windows versions. The AND mask is still present but unused;
/// instead the (former) XOR-part of the image has an alpha channel
/// allowing partial transparency, e.g. for smooth shadows.
/// These icons always have a color depth of 32bit.
///
TrueColor = 1,
///
/// Compressed icons were introduced by Windows Vista and are not supported
/// by previous Windows versions. These icons simply contain a complete
/// .png file in the entries' data.
/// The .png files can use palette images with a single transparency color
/// or true color with alpha channel; though usually the compressed format
/// is only used for the high-resolution 256x256x32 icons in Vista.
///
Compressed = 2
}
///
/// A single image in an icon file.
///
public sealed class IconEntry
{
// official icon sizes: 16, 24, 32, 48, 256
// common hi-quality icon sizes: 64, 98, 128
// hi-quality for Smartphones: 22, 44
// static readonly int[] supportedSizes = {16, 22, 24, 44, 32, 48, 64, 96, 128, 256};
int width, height, colorDepth;
bool isCompressed;
int offsetInFile, sizeInBytes;
byte[] entryData;
public int Width {
get {
return width;
}
}
public int Height {
get {
return height;
}
}
public Size Size {
get {
return new Size(width, height);
}
}
public int ColorDepth {
get {
return colorDepth;
}
}
public IconEntryType Type {
get {
if (isCompressed)
return IconEntryType.Compressed;
else if (colorDepth == 32)
return IconEntryType.TrueColor;
else
return IconEntryType.Classic;
}
}
///
/// Gets the raw data of this image.
/// For uncompressed entries, this is a ICONIMAGE structure.
/// For compressed entries, this is a .PNG file.
///
public Stream GetEntryData()
{
return new MemoryStream(entryData, false);
}
///
/// Gets the data of this image.
/// For uncompressed entries, this is a .BMP file.
/// For compressed entries, this is a .PNG file.
///
public Stream GetImageData()
{
Stream stream = GetEntryData();
if (isCompressed)
return stream;
using (BinaryReader b = new BinaryReader(stream)) {
int biBitCount;
int headerSize = CheckBitmapHeader(b, out biBitCount);
MemoryStream output = new MemoryStream();
BinaryWriter w = new BinaryWriter(output);
w.Write((ushort)19778); // "BM" mark
w.Write(0); // file size, we'll fill it in later
w.Write(0); // 4 reserved bytes
w.Write(0); // data start offset, we'll fill it in later
w.Write(entryData, 0, headerSize); // write header
output.Position = 14 + 8; // position of biHeight in header
w.Write(height); // write correct height into header
output.Position = output.Length;
if (biBitCount <= 8) {
// copy color table:
int colorTableSize = 4 * (1 << biBitCount);
w.Write(b.ReadBytes(colorTableSize));
}
output.Position = 10; // fill in data start offset
w.Write((int)output.Length);
output.Position = output.Length;
// copy bitmap data:
w.Write(b.ReadBytes(GetBitmapSize(width, height, biBitCount)));
output.Position = 2; // fill in file size
w.Write((int)output.Length);
output.Position = 0;
return output;
}
}
///
/// Gets the data of the image mask.
/// The result is a monochrome .BMP file where the transparent
/// image regions are marked as white and the opaque regions
/// are black. This is used as AND-mask before drawing the main image
/// with XOR.
///
/// Image masks are only used in uncompressed icons,
/// an InvalidOperationException is thrown if you call GetImageMaskData on a compressed icon.
public Stream GetMaskImageData()
{
if (isCompressed)
throw new InvalidOperationException("Image masks are only used in uncompressed icons.");
Stream readStream = GetEntryData();
using (BinaryReader b = new BinaryReader(readStream)) {
int biBitCount;
int headerSize = CheckBitmapHeader(b, out biBitCount);
MemoryStream output = new MemoryStream();
BinaryWriter w = new BinaryWriter(output);
w.Write((ushort)19778); // "BM" mark
w.Write(0); // file size, we'll fill it in later
w.Write(0); // 4 reserved bytes
w.Write(0); // data start offset, we'll fill it in later
w.Write(40); // header size
w.Write((int)width);
w.Write((int)height);
w.Write((short)1); // 1 plane
w.Write((short)1); // monochrome
w.Write(0); // no compression
w.Write(0); // biSizeImage, should be zero
w.Write(0); // biXPelsPerMeter, should be zero
w.Write(0); // biYPelsPerMeter, should be zero
w.Write(0); // biClrUsed - calculate color count using bitCount
w.Write(0); // no special "important" colors
// write color table:
w.Write(0); // write black into color table
// write white into color table:
w.Write((byte)255);
w.Write((byte)255);
w.Write((byte)255);
w.Write((byte)0);
output.Position = 10; // fill in data start offset
w.Write((int)output.Length);
output.Position = output.Length;
// skip real color table:
if (biBitCount <= 8) {
readStream.Position += 4 * (1 << biBitCount);
}
// skip real bitmap data:
readStream.Position += GetBitmapSize(width, height, biBitCount);
// copy mask bitmap data:
w.Write(b.ReadBytes(GetBitmapSize(width, height, 1)));
output.Position = 2; // fill in file size
w.Write((int)output.Length);
output.Position = 0;
return output;
}
}
///
/// Gets the size of the data section of a DIB bitmap with the
/// specified parameters
///
static int GetBitmapSize(int width, int height, int bitsPerPixel)
{
const int bitPack = 4*8; // 4 byte packing
int lineBits = width * bitsPerPixel;
// expand size to multiple of 4 bytes
int rem = lineBits % bitPack;
if (rem != 0) {
lineBits += (bitPack - rem);
}
return lineBits / 8 * height;
}
///
/// Gets the image.
/// For uncompressed palette images, this returns the XOR part of the entry.
/// For 32bit images and compressed images, this returns a bitmap with
/// alpha transparency.
///
public Bitmap GetImage()
{
Stream data = GetImageData();
if (IsCompressed || ColorDepth != 32) {
return new Bitmap(data);
} else {
// new Bitmap() does not work with alpha-transparent .bmp's
// Therefore, we have to use our own little bitmap loader
return AlphaTransparentBitmap.LoadAlphaTransparentBitmap(data);
}
}
///
/// Gets the the image mask.
/// The result is a monochrome bitmap where the transparent
/// image regions are marked as white and the opaque regions
/// are black. This is used as AND-mask before drawing the main image
/// with XOR.
///
/// Image masks are only used in uncompressed icons,
/// an InvalidOperationException is thrown if you call GetImageMask on a compressed icon.
public Bitmap GetMaskImage()
{
return new Bitmap(GetMaskImageData());
}
///
/// Sets the data to be used by the icon.
///
public void SetEntryData(byte[] entryData)
{
if (entryData == null)
throw new ArgumentNullException("imageData");
this.entryData = entryData;
isCompressed = false;
if (sizeInBytes > 8) {
// PNG Specification, section 5.2:
// The first eight bytes of a PNG datastream always contain the following (decimal) values:
// 137 80 78 71 13 10 26 10
if (entryData[0] == 137 &&
entryData[1] == 80 &&
entryData[2] == 78 &&
entryData[3] == 71 &&
entryData[4] == 13 &&
entryData[5] == 10 &&
entryData[6] == 26 &&
entryData[7] == 10)
{
isCompressed = true;
}
}
}
int CheckBitmapHeader(BinaryReader b, out int biBitCount)
{
const int knownHeaderSize = 4*3 + 2*2 + 4;
const int BI_RGB = 0;
int biSize = b.ReadInt32();
if (biSize <= knownHeaderSize)
throw new InvalidIconException("biSize invalid: " + biSize);
if (b.ReadInt32() != width)
throw new InvalidIconException("biWidth invalid");
int biHeight = b.ReadInt32();
if (biHeight != 2*height) // double of normal height for AND bitmap
throw new InvalidIconException("biHeight invalid: " + biHeight);
if (b.ReadInt16() != 1)
throw new InvalidIconException("biPlanes invalid");
biBitCount = b.ReadInt16();
// Do not test biBitCount: there are icons where the colorDepth is saved
// incorrectly; biBitCount is the real value to use in those cases
//if (biBitCount != colorDepth)
// throw new InvalidIconException("biBitCount invalid: " + biBitCount);
int compression = b.ReadInt32();
if (compression != BI_RGB)
throw new InvalidIconException("biCompression invalid");
// skip rest of header:
b.ReadBytes(biSize - knownHeaderSize);
return biSize;
}
///
/// Gets if the entry is compressed.
/// Compressed entries are PNG files, uncompressed entries
/// are a special DIB-like format.
///
public bool IsCompressed {
get {
return isCompressed;
}
}
internal IconEntry()
{
}
public IconEntry(int width, int height, int colorDepth, byte[] imageData)
{
this.width = width;
this.height = height;
this.colorDepth = colorDepth;
CheckSize();
CheckColorDepth();
SetEntryData(imageData);
}
void CheckSize()
{
if (width <= 0 || height <= 0 || width > 256 || height > 256) {
throw new InvalidIconException("Invalid icon size: " + width + "x" + width);
}
}
void CheckColorDepth()
{
switch (colorDepth) {
case 1: // monochrome icon
case 4: // 16 palette colors
case 8: // 256 palette colors
case 32: // XP icon with alpha channel
case 16: // allowed by the spec, but very uncommon
case 24: // non-standard, but common
break;
default:
throw new InvalidIconException("Unknown color depth: " + colorDepth);
}
}
internal void ReadHeader(BinaryReader r, ref bool wellFormed)
{
width = r.ReadByte();
height = r.ReadByte();
// For Vista 256x256 icons:
if (width == 0) width = 256;
if (height == 0) height = 256;
CheckSize();
byte colorCount = r.ReadByte();
if (colorCount != 0 && colorCount != 2 && colorCount != 16) {
throw new InvalidIconException("Invalid color count: " + colorCount);
}
if (r.ReadByte() != 0) {
throw new InvalidIconException("Invalid value for reserved");
}
uint planeCount = r.ReadUInt16();
// placeCount should always be 1, but there are some icons with planeCount = 0
if (planeCount == 0) {
wellFormed = false;
}
if (planeCount > 1) {
throw new InvalidIconException("Invalid number of planes: " + planeCount);
}
colorDepth = r.ReadUInt16();
if (colorDepth == 0) {
if (colorCount == 2)
colorDepth = 1;
else if (colorCount == 16)
colorDepth = 4;
else if (colorCount == 0)
colorDepth = 8;
}
CheckColorDepth();
sizeInBytes = r.ReadInt32();
if (sizeInBytes <= 0) {
throw new InvalidIconException("Invalid entry size: " + sizeInBytes);
}
if (sizeInBytes > 10*1024*1024) {
throw new InvalidIconException("Entry too large: " + sizeInBytes);
}
offsetInFile = r.ReadInt32();
if (offsetInFile <= 0) {
throw new InvalidIconException("Invalid offset in file: " + offsetInFile);
}
}
uint saveOffsetToHeaderPosition;
internal void WriteHeader(Stream stream, BinaryWriter w)
{
w.Write((byte)(width == 256 ? 0 : width));
w.Write((byte)(height == 256 ? 0 : height));
w.Write((byte)(colorDepth == 4 ? 16 : 0));
w.Write((byte)0);
w.Write((ushort)1);
w.Write((ushort)colorDepth);
w.Write((int)entryData.Length);
saveOffsetToHeaderPosition = (uint)stream.Position;
w.Write((uint)0);
}
internal void ReadData(Stream stream, ref bool wellFormed)
{
stream.Position = offsetInFile;
byte[] imageData = new byte[sizeInBytes];
int pos = 0;
while (pos < imageData.Length) {
int c = stream.Read(imageData, pos, imageData.Length - pos);
if (c == 0)
throw new InvalidIconException("Unexpected end of stream");
pos += c;
}
SetEntryData(imageData);
if (isCompressed == false) {
using (BinaryReader r = new BinaryReader(new MemoryStream(imageData, false))) {
int biBitCount;
CheckBitmapHeader(r, out biBitCount);
if (biBitCount != colorDepth) {
// inconsistency in header information, fix icon header
wellFormed = false;
colorDepth = biBitCount;
CheckColorDepth();
}
}
}
}
internal void WriteData(Stream stream)
{
uint pos = (uint)stream.Position;
stream.Position = saveOffsetToHeaderPosition;
stream.Write(BitConverter.GetBytes(pos), 0, 4);
stream.Position = pos;
stream.Write(entryData, 0, entryData.Length);
}
///
/// Stores the specified bitmap. The bitmap will be resized and
/// changed to the correct pixel format.
///
public void SetImage(Bitmap bitmap, bool storeCompressed)
{
if (bitmap.Width != width || bitmap.Height != height) {
bitmap = new Bitmap(bitmap, width, height);
}
PixelFormat expected;
switch (colorDepth) {
case 1:
expected = PixelFormat.Format1bppIndexed;
break;
case 4:
expected = PixelFormat.Format4bppIndexed;
break;
case 8:
expected = PixelFormat.Format8bppIndexed;
break;
case 24:
expected = PixelFormat.Format24bppRgb;
break;
case 32:
expected = PixelFormat.Format32bppArgb;
break;
default:
throw new NotSupportedException();
}
if (bitmap.PixelFormat != expected) {
if (expected == PixelFormat.Format32bppArgb) {
bitmap = new Bitmap(bitmap, width, height);
} else {
throw new NotImplementedException();
}
}
if (storeCompressed) {
using (MemoryStream ms = new MemoryStream()) {
bitmap.Save(ms, ImageFormat.Png);
SetEntryData(ms.ToArray());
}
} else {
throw new NotImplementedException();
}
}
public override string ToString()
{
return string.Format("[IconEntry {0}x{1}x{2}]", this.width, this.height, this.colorDepth);
}
}
}