Skip to content

Instantly share code, notes, and snippets.

@smourier
Created August 12, 2023 13:22
Show Gist options
  • Save smourier/87545e8ad322b250407e022216a45b6e to your computer and use it in GitHub Desktop.
Save smourier/87545e8ad322b250407e022216a45b6e to your computer and use it in GitHub Desktop.
save .ANI (doesn't work, not sure why)
public static void SaveAsANI(Image inputImage, string outputFilePath)
{
ArgumentNullException.ThrowIfNull(inputImage);
ArgumentNullException.ThrowIfNull(outputFilePath);
using var stream = new FileStream(outputFilePath, FileMode.Create);
SaveAsANI(inputImage, stream);
}
public static void SaveAsANI(Image inputImage, Stream outputStream)
{
ArgumentNullException.ThrowIfNull(inputImage);
ArgumentNullException.ThrowIfNull(outputStream);
var dimension = new FrameDimension(inputImage.FrameDimensionsList[0]);
var frameCount = inputImage.GetFrameCount(dimension);
// see https://en.wikipedia.org/wiki/Resource_Interchange_File_Format on RIFF format
// see https://www.gdgsoft.com/anituner/help/aniformat.htm for example on .ANI format
using var writer = new BinaryWriter(outputStream, Encoding.ASCII);
// riff format
writeString("RIFF");
// get pos & write dummy value, we'll rewrite that later
var riffSizePos = outputStream.Position;
writeInt32(0); // 'RIFF' chunk size
writeString("ACON"); // .ani format
// header
writeString("anih");
writeInt32(36); // 'anih' chunk size
writeInt32(36); // cbSizeof
writeInt32(frameCount); // cFrames
writeInt32(frameCount); // cSteps
// unused in .ani case
writeInt32(0); // cx
writeInt32(0); // cy
writeInt32(32); // cBitCount
writeInt32(1); // cPlanes
writeInt32(3); // jifRate
writeInt32(1); // flags (AF_ICON = 1)
// some other RIFF chunks
writeString("LIST");
// get pos & write dummy value, we'll rewrite that later
var listSizePos = outputStream.Position;
writeInt32(0); // 'LIST' chunk size
writeString("fram");
// note: we use the same buffer for all frames
using var ms = new MemoryStream();
for (var i = 0; i < frameCount; i++)
{
// get image frame #i
inputImage.SelectActiveFrame(dimension, i);
writeString("icon");
// get pos & write dummy value, we'll rewrite that later
var iconSizePos = outputStream.Position;
writeInt32(0); // 'icon' chunk size
// get png bytes
ms.Position = 0;
inputImage.Save(ms, ImageFormat.Png);
var beforeIconPos = outputStream.Position;
WritePNGCursor(outputStream, (uint)inputImage.Width, (uint)inputImage.Height, ms.GetBuffer(), 0, (int)ms.Length);
var afterIconPos = outputStream.Position;
// now we can get back to write final icon size
outputStream.Position = iconSizePos;
writeInt32((int)(afterIconPos - beforeIconPos));
// restore pos
outputStream.Position = afterIconPos;
}
// now we can get back to write final sizes
outputStream.Position = riffSizePos;
writeInt32((int)(outputStream.Length - riffSizePos - 4));
outputStream.Position = listSizePos;
writeInt32((int)(outputStream.Length - listSizePos - 4));
void writeString(string str) => outputStream.Write(Encoding.ASCII.GetBytes(str));
void writeInt32(int i) => outputStream.Write(BitConverter.GetBytes(i));
}
// utility methods to write .cur files with PNG format (the easiest)
public static void WritePNGCursor(Stream iconFileStream, uint width, uint height, byte[] pngBytes, int pngBytesIndex = 0, int pngBytesCount = int.MaxValue)
{
ArgumentNullException.ThrowIfNull(iconFileStream);
ArgumentNullException.ThrowIfNull(pngBytes);
var startPos = iconFileStream.Position;
if (pngBytesCount == int.MaxValue || pngBytesCount < 0)
{
pngBytesCount = pngBytes.Length;
}
var writer = new BinaryWriter(iconFileStream);
writer.Write((short)0); // reserved
writer.Write((short)2); // image type (2 for .cur)
writer.Write((short)1); // count
const int maxIconSize = 256;
writer.Write(width == maxIconSize ? (byte)0 : (byte)width);
writer.Write(height == maxIconSize ? (byte)0 : (byte)height);
writer.Write((byte)0); // number of colors in the color palette
writer.Write((byte)0); // reserved
writer.Write((short)0); // h hotspot
writer.Write((short)0); // v hotspot
var offsets = iconFileStream.Position;
writer.Write(0); // size of image data, we'll rewrite that later
writer.Write(0); // offset of data from beginning;
var pngOffset = iconFileStream.Position - startPos;
writer.Write(pngBytes, pngBytesIndex, pngBytesCount);
// save end position
var endPos = iconFileStream.Position;
iconFileStream.Seek(offsets, SeekOrigin.Begin);
writer.Write(pngBytesCount);
writer.Write((int)pngOffset);
iconFileStream.Position = endPos; // restore end position
}
public static void WritePNGCursor(string iconFilePath, uint width, uint height, byte[] pngBytes, int pngBytesIndex = 0, int pngBytesCount = int.MaxValue)
{
ArgumentNullException.ThrowIfNull(iconFilePath);
ArgumentNullException.ThrowIfNull(pngBytes);
using var stream = File.OpenWrite(iconFilePath);
WritePNGCursor(stream, width, height, pngBytes, pngBytesIndex, pngBytesCount);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment