Task 031: .ANI File Format
Task 031: .ANI File Format
The .ANI file format is a graphics file format used for animated mouse cursors in Microsoft Windows, based on the Resource Interchange File Format (RIFF). Below, I address each part of your task by first listing the intrinsic properties of the .ANI file format and then providing classes in Python, Java, JavaScript, and C to handle these files.
1. Properties of the .ANI File Format Intrinsic to Its File System
The .ANI file format is a RIFF-based container that stores animated cursor data. Based on available specifications, the intrinsic properties of the .ANI file format include:
- File Signature: Starts with "RIFF" (4 bytes, ASCII: 0x52, 0x49, 0x46, 0x46).
- RIFF Type: "ACON" (4 bytes, ASCII: 0x41, 0x43, 0x4F, 0x4E), indicating an animated cursor.
- File Size: A 4-byte unsigned integer (little-endian) specifying the size of the RIFF chunk data (total file size minus 8 bytes for the RIFF header).
- ANI Header (anih chunk):
- cbSizeof: Size of the ANIHEADER structure (typically 36 bytes).
- cFrames: Number of frames (ICO images) in the animation.
- cSteps: Number of steps in the animation sequence.
- cx, cy: Width and height of the cursor (often unused, set to 0).
- cBitCount, cPlanes: Bit count and color planes (often unused, set to 0).
- jifRate: Default frame display rate in jiffies (1/60th of a second, or 16.666 ms).
- fl: Flags indicating properties (e.g., AF_ICON = 0x1 for ICO format frames, AF_SEQUENCE = 0x2 for sequenced animation).
- INFO List (optional):
- INAM: Animation title (zero-terminated string, optional).
- IART: Artist name (zero-terminated string, optional).
- Rate Table (rate chunk, optional): Array of DWORDs specifying per-step frame rates in jiffies (count equals cSteps if AF_SEQUENCE is set, else cFrames).
- Sequence Table (seq chunk, optional): Array of DWORDs specifying the frame indices for each step (count equals cSteps if AF_SEQUENCE is set).
- Frame List (fram chunk): A LIST chunk with type "fram" containing subchunks labeled "icon", each holding ICO format frame data (count equals cFrames).
- Frame Data: Individual frames stored as ICO files within the "icon" subchunks.
- Hotspot (part of ICO frames): X and Y coordinates for the cursor’s active point (embedded in each ICO frame’s header).
These properties are derived from the RIFF structure and specific ANI format details.
2. Python Class for .ANI File Handling
Below is a Python class that opens, reads, writes, and prints the properties of an .ANI file.
import struct
import os
class AniFile:
def __init__(self, filename):
self.filename = filename
self.signature = b""
self.riff_type = b""
self.file_size = 0
self.ani_header = {}
self.info = {"INAM": "", "IART": ""}
self.rate_table = []
self.seq_table = []
self.frames = []
def read(self):
with open(self.filename, "rb") as f:
# Read RIFF header
self.signature = f.read(4)
if self.signature != b"RIFF":
raise ValueError("Not a valid RIFF file")
self.file_size = struct.unpack("<I", f.read(4))[0]
self.riff_type = f.read(4)
if self.riff_type != b"ACON":
raise ValueError("Not an ANI file")
while f.tell() < self.file_size + 8:
chunk_id = f.read(4)
chunk_size = struct.unpack("<I", f.read(4))[0]
chunk_data = f.read(chunk_size)
if chunk_id == b"anih":
self.ani_header = struct.unpack("<IIIIIIIII", chunk_data[:36])
self.ani_header = {
"cbSizeof": self.ani_header[0],
"cFrames": self.ani_header[1],
"cSteps": self.ani_header[2],
"cx": self.ani_header[3],
"cy": self.ani_header[4],
"cBitCount": self.ani_header[5],
"cPlanes": self.ani_header[6],
"jifRate": self.ani_header[7],
"fl": self.ani_header[8]
}
elif chunk_id == b"LIST":
if chunk_data[:4] == b"INFO":
pos = 4
while pos < len(chunk_data):
sub_id = chunk_data[pos:pos+4]
sub_size = struct.unpack("<I", chunk_data[pos+4:pos+8])[0]
sub_data = chunk_data[pos+8:pos+8+sub_size].decode('ascii').rstrip('\x00')
if sub_id in [b"INAM", b"IART"]:
self.info[sub_id.decode('ascii')] = sub_data
pos += 8 + sub_size
if pos % 2: pos += 1 # Align to even boundary
elif chunk_data[:4] == b"fram":
pos = 4
while pos < len(chunk_data):
if chunk_data[pos:pos+4] == b"icon":
icon_size = struct.unpack("<I", chunk_data[pos+4:pos+8])[0]
icon_data = chunk_data[pos+8:pos+8+icon_size]
self.frames.append(icon_data)
pos += 8 + icon_size
if pos % 2: pos += 1
elif chunk_id == b"rate":
self.rate_table = [struct.unpack("<I", chunk_data[i:i+4])[0]
for i in range(0, chunk_size, 4)]
elif chunk_id == b"seq ":
self.seq_table = [struct.unpack("<I", chunk_data[i:i+4])[0]
for i in range(0, chunk_size, 4)]
if f.tell() % 2: f.read(1) # Align to even boundary
def write(self, output_filename):
with open(output_filename, "wb") as f:
# Write RIFF header
f.write(b"RIFF")
f.write(struct.pack("<I", self.file_size))
f.write(b"ACON")
# Write anih chunk
f.write(b"anih")
f.write(struct.pack("<I", 36))
f.write(struct.pack("<IIIIIIIII",
self.ani_header["cbSizeof"], self.ani_header["cFrames"],
self.ani_header["cSteps"], self.ani_header["cx"], self.ani_header["cy"],
self.ani_header["cBitCount"], self.ani_header["cPlanes"],
self.ani_header["jifRate"], self.ani_header["fl"]))
# Write INFO list
if self.info["INAM"] or self.info["IART"]:
info_data = b"LIST"
info_content = b"INFO"
if self.info["INAM"]:
info_content += b"INAM" + struct.pack("<I", len(self.info["INAM"]) + 1)
info_content += self.info["INAM"].encode('ascii') + b"\x00"
if self.info["IART"]:
info_content += b"IART" + struct.pack("<I", len(self.info["IART"]) + 1)
info_content += self.info["IART"].encode('ascii') + b"\x00"
f.write(struct.pack("<I", len(info_content)))
f.write(info_content)
if len(info_content) % 2: f.write(b"\x00")
# Write rate chunk
if self.rate_table:
f.write(b"rate")
f.write(struct.pack("<I", len(self.rate_table) * 4))
for rate in self.rate_table:
f.write(struct.pack("<I", rate))
# Write seq chunk
if self.seq_table:
f.write(b"seq ")
f.write(struct.pack("<I", len(self.seq_table) * 4))
for seq in self.seq_table:
f.write(struct.pack("<I", seq))
# Write fram list
f.write(b"LIST")
fram_content = b"fram"
for frame in self.frames:
fram_content += b"icon" + struct.pack("<I", len(frame)) + frame
f.write(struct.pack("<I", len(fram_content)))
f.write(fram_content)
if len(fram_content) % 2: f.write(b"\x00")
def print_properties(self):
print(f"Signature: {self.signature.decode('ascii')}")
print(f"RIFF Type: {self.riff_type.decode('ascii')}")
print(f"File Size: {self.file_size} bytes")
print("ANI Header:")
for key, value in self.ani_header.items():
print(f" {key}: {value}")
print("INFO:")
for key, value in self.info.items():
if value:
print(f" {key}: {value}")
print(f"Rate Table: {self.rate_table}")
print(f"Sequence Table: {self.seq_table}")
print(f"Number of Frames: {len(self.frames)}")
# Example usage
if __name__ == "__main__":
ani = AniFile("sample.ani")
ani.read()
ani.print_properties()
ani.write("output.ani")
This class reads an .ANI file, decodes its RIFF structure, stores the properties, prints them, and can write a new .ANI file. It assumes ICO frames are valid but does not parse their internal structure (e.g., hotspot coordinates) for simplicity.
3. Java Class for .ANI File Handling
Below is a Java class that performs the same operations.
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class AniFile {
private String filename;
private String signature;
private String riffType;
private int fileSize;
private Map<String, Integer> aniHeader;
private Map<String, String> info;
private List<Integer> rateTable;
private List<Integer> seqTable;
private List<byte[]> frames;
public AniFile(String filename) {
this.filename = filename;
this.aniHeader = new HashMap<>();
this.info = new HashMap<>();
this.rateTable = new ArrayList<>();
this.seqTable = new ArrayList<>();
this.frames = new ArrayList<>();
}
public void read() throws IOException {
try (DataInputStream dis = new DataInputStream(new FileInputStream(filename))) {
byte[] buffer = new byte[4];
dis.readFully(buffer);
signature = new String(buffer);
if (!signature.equals("RIFF")) throw new IOException("Not a valid RIFF file");
fileSize = Integer.reverseBytes(dis.readInt());
dis.readFully(buffer);
riffType = new String(buffer);
if (!riffType.equals("ACON")) throw new IOException("Not an ANI file");
while (dis.available() > 0) {
dis.readFully(buffer);
String chunkId = new String(buffer);
int chunkSize = Integer.reverseBytes(dis.readInt());
byte[] chunkData = new byte[chunkSize];
dis.readFully(chunkData);
if (chunkId.equals("anih")) {
ByteBuffer bb = ByteBuffer.wrap(chunkData).order(ByteOrder.LITTLE_ENDIAN);
aniHeader.put("cbSizeof", bb.getInt());
aniHeader.put("cFrames", bb.getInt());
aniHeader.put("cSteps", bb.getInt());
aniHeader.put("cx", bb.getInt());
aniHeader.put("cy", bb.getInt());
aniHeader.put("cBitCount", bb.getInt());
aniHeader.put("cPlanes", bb.getInt());
aniHeader.put("jifRate", bb.getInt());
aniHeader.put("fl", bb.getInt());
} else if (chunkId.equals("LIST")) {
ByteBuffer bb = ByteBuffer.wrap(chunkData);
String listType = new String(chunkData, 0, 4);
if (listType.equals("INFO")) {
int pos = 4;
while (pos < chunkSize) {
String subId = new String(chunkData, pos, 4);
int subSize = ByteBuffer.wrap(chunkData, pos + 4, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
String subData = new String(chunkData, pos + 8, subSize - 1);
if (subId.equals("INAM") || subId.equals("IART")) {
info.put(subId, subData);
}
pos += 8 + subSize;
if (pos % 2 == 1) pos++;
}
} else if (listType.equals("fram")) {
int pos = 4;
while (pos < chunkSize) {
if (new String(chunkData, pos, 4).equals("icon")) {
int iconSize = ByteBuffer.wrap(chunkData, pos + 4, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
byte[] iconData = new byte[iconSize];
System.arraycopy(chunkData, pos + 8, iconData, 0, iconSize);
frames.add(iconData);
pos += 8 + iconSize;
if (pos % 2 == 1) pos++;
}
}
}
} else if (chunkId.equals("rate")) {
ByteBuffer bb = ByteBuffer.wrap(chunkData).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < chunkSize / 4; i++) {
rateTable.add(bb.getInt());
}
} else if (chunkId.equals("seq ")) {
ByteBuffer bb = ByteBuffer.wrap(chunkData).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < chunkSize / 4; i++) {
seqTable.add(bb.getInt());
}
}
if (dis.available() % 2 == 1) dis.readByte();
}
}
}
public void write(String outputFilename) throws IOException {
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(outputFilename))) {
dos.writeBytes("RIFF");
dos.writeInt(Integer.reverseBytes(fileSize));
dos.writeBytes("ACON");
dos.writeBytes("anih");
dos.writeInt(Integer.reverseBytes(36));
dos.writeInt(Integer.reverseBytes(aniHeader.get("cbSizeof")));
dos.writeInt(Integer.reverseBytes(aniHeader.get("cFrames")));
dos.writeInt(Integer.reverseBytes(aniHeader.get("cSteps")));
dos.writeInt(Integer.reverseBytes(aniHeader.get("cx")));
dos.writeInt(Integer.reverseBytes(aniHeader.get("cy")));
dos.writeInt(Integer.reverseBytes(aniHeader.get("cBitCount")));
dos.writeInt(Integer.reverseBytes(aniHeader.get("cPlanes")));
dos.writeInt(Integer.reverseBytes(aniHeader.get("jifRate")));
dos.writeInt(Integer.reverseBytes(aniHeader.get("fl")));
if (!info.isEmpty()) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.writeBytes("INFO");
if (info.containsKey("INAM")) {
baos.writeBytes("INAM");
byte[] inam = info.get("INAM").getBytes();
baos.writeInt(Integer.reverseBytes(inam.length + 1));
baos.write(inam);
baos.writeByte(0);
}
if (info.containsKey("IART")) {
baos.writeBytes("IART");
byte[] iart = info.get("IART").getBytes();
baos.writeInt(Integer.reverseBytes(iart.length + 1));
baos.write(iart);
baos.writeByte(0);
}
dos.writeBytes("LIST");
dos.writeInt(Integer.reverseBytes(baos.size()));
dos.write(baos.toByteArray());
if (baos.size() % 2 == 1) dos.writeByte(0);
}
if (!rateTable.isEmpty()) {
dos.writeBytes("rate");
dos.writeInt(Integer.reverseBytes(rateTable.size() * 4));
for (int rate : rateTable) {
dos.writeInt(Integer.reverseBytes(rate));
}
}
if (!seqTable.isEmpty()) {
dos.writeBytes("seq ");
dos.writeInt(Integer.reverseBytes(seqTable.size() * 4));
for (int seq : seqTable) {
dos.writeInt(Integer.reverseBytes(seq));
}
}
dos.writeBytes("LIST");
ByteArrayOutputStream framBaos = new ByteArrayOutputStream();
framBaos.writeBytes("fram");
for (byte[] frame : frames) {
framBaos.writeBytes("icon");
framBaos.writeInt(Integer.reverseBytes(frame.length));
framBaos.write(frame);
}
dos.writeInt(Integer.reverseBytes(framBaos.size()));
dos.write(framBaos.toByteArray());
if (framBaos.size() % 2 == 1) dos.writeByte(0);
}
}
public void printProperties() {
System.out.println("Signature: " + signature);
System.out.println("RIFF Type: " + riffType);
System.out.println("File Size: " + fileSize + " bytes");
System.out.println("ANI Header:");
aniHeader.forEach((k, v) -> System.out.println(" " + k + ": " + v));
System.out.println("INFO:");
info.forEach((k, v) -> System.out.println(" " + k + ": " + v));
System.out.println("Rate Table: " + rateTable);
System.out.println("Sequence Table: " + seqTable);
System.out.println("Number of Frames: " + frames.size());
}
public static void main(String[] args) {
try {
AniFile ani = new AniFile("sample.ani");
ani.read();
ani.printProperties();
ani.write("output.ani");
} catch (IOException e) {
e.printStackTrace();
}
}
}
This Java class mirrors the Python implementation, handling RIFF chunks and subchunks with proper endianness.
4. JavaScript Class for .ANI File Handling
Below is a JavaScript class using Node.js for file operations.
const fs = require('fs');
class AniFile {
constructor(filename) {
this.filename = filename;
this.signature = '';
this.riffType = '';
this.fileSize = 0;
this.aniHeader = {};
this.info = { INAM: '', IART: '' };
this.rateTable = [];
this.seqTable = [];
this.frames = [];
}
read() {
const buffer = fs.readFileSync(this.filename);
let pos = 0;
this.signature = buffer.toString('ascii', pos, pos + 4);
if (this.signature !== 'RIFF') throw new Error('Not a valid RIFF file');
pos += 4;
this.fileSize = buffer.readUInt32LE(pos);
pos += 4;
this.riffType = buffer.toString('ascii', pos, pos + 4);
if (this.riffType !== 'ACON') throw new Error('Not an ANI file');
pos += 4;
while (pos < buffer.length) {
const chunkId = buffer.toString('ascii', pos, pos + 4);
pos += 4;
const chunkSize = buffer.readUInt32LE(pos);
pos += 4;
const chunkData = buffer.slice(pos, pos + chunkSize);
pos += chunkSize;
if (chunkId === 'anih') {
this.aniHeader = {
cbSizeof: chunkData.readUInt32LE(0),
cFrames: chunkData.readUInt32LE(4),
cSteps: chunkData.readUInt32LE(8),
cx: chunkData.readUInt32LE(12),
cy: chunkData.readUInt32LE(16),
cBitCount: chunkData.readUInt32LE(20),
cPlanes: chunkData.readUInt32LE(24),
jifRate: chunkData.readUInt32LE(28),
fl: chunkData.readUInt32LE(32)
};
} else if (chunkId === 'LIST') {
const listType = chunkData.toString('ascii', 0, 4);
if (listType === 'INFO') {
let infoPos = 4;
while (infoPos < chunkSize) {
const subId = chunkData.toString('ascii', infoPos, infoPos + 4);
const subSize = chunkData.readUInt32LE(infoPos + 4);
const subData = chunkData.toString('ascii', infoPos + 8, infoPos + 8 + subSize - 1);
if (subId === 'INAM' || subId === 'IART') {
this.info[subId] = subData;
}
infoPos += 8 + subSize;
if (infoPos % 2) infoPos++;
}
} else if (listType === 'fram') {
let framPos = 4;
while (framPos < chunkSize) {
if (chunkData.toString('ascii', framPos, framPos + 4) === 'icon') {
const iconSize = chunkData.readUInt32LE(framPos + 4);
const iconData = chunkData.slice(framPos + 8, framPos + 8 + iconSize);
this.frames.push(iconData);
framPos += 8 + iconSize;
if (framPos % 2) framPos++;
}
}
}
} else if (chunkId === 'rate') {
for (let i = 0; i < chunkSize; i += 4) {
this.rateTable.push(chunkData.readUInt32LE(i));
}
} else if (chunkId === 'seq ') {
for (let i = 0; i < chunkSize; i += 4) {
this.seqTable.push(chunkData.readUInt32LE(i));
}
}
if (pos % 2) pos++;
}
}
write(outputFilename) {
let buffer = Buffer.alloc(12);
buffer.write('RIFF', 0);
buffer.writeUInt32LE(this.fileSize, 4);
buffer.write('ACON', 8);
buffer = Buffer.concat([buffer, Buffer.alloc(44)]);
buffer.write('anih', 12);
buffer.writeUInt32LE(36, 16);
buffer.writeUInt32LE(this.aniHeader.cbSizeof, 20);
buffer.writeUInt32LE(this.aniHeader.cFrames, 24);
buffer.writeUInt32LE(this.aniHeader.cSteps, 28);
buffer.writeUInt32LE(this.aniHeader.cx, 32);
buffer.writeUInt32LE(this.aniHeader.cy, 36);
buffer.writeUInt32LE(this.aniHeader.cBitCount, 40);
buffer.writeUInt32LE(this.aniHeader.cPlanes, 44);
buffer.writeUInt32LE(this.aniHeader.jifRate, 48);
buffer.writeUInt32LE(this.aniHeader.fl, 52);
if (this.info.INAM || this.info.IART) {
let infoBuffer = Buffer.from('LISTINFO');
if (this.info.INAM) {
const inam = Buffer.from(this.info.INAM + '\x00');
const inamHeader = Buffer.alloc(8);
inamHeader.write('INAM', 0);
inamHeader.writeUInt32LE(inam.length, 4);
infoBuffer = Buffer.concat([infoBuffer, inamHeader, inam]);
}
if (this.info.IART) {
const iart = Buffer.from(this.info.IART + '\x00');
const iartHeader = Buffer.alloc(8);
iartHeader.write('IART', 0);
iartHeader.writeUInt32LE(iart.length, 4);
infoBuffer = Buffer.concat([infoBuffer, iartHeader, iart]);
}
buffer = Buffer.concat([buffer, Buffer.alloc(8)]);
buffer.write('LIST', buffer.length - 8);
buffer.writeUInt32LE(infoBuffer.length - 4, buffer.length - 4);
buffer = Buffer.concat([buffer, infoBuffer.slice(4)]);
if (infoBuffer.length % 2) buffer = Buffer.concat([buffer, Buffer.alloc(1)]);
}
if (this.rateTable.length) {
const rateBuffer = Buffer.alloc(8 + this.rateTable.length * 4);
rateBuffer.write('rate', 0);
rateBuffer.writeUInt32LE(this.rateTable.length * 4, 4);
this.rateTable.forEach((rate, i) => rateBuffer.writeUInt32LE(rate, 8 + i * 4));
buffer = Buffer.concat([buffer, rateBuffer]);
}
if (this.seqTable.length) {
const seqBuffer = Buffer.alloc(8 + this.seqTable.length * 4);
seqBuffer.write('seq ', 0);
seqBuffer.writeUInt32LE(this.seqTable.length * 4, 4);
this.seqTable.forEach((seq, i) => seqBuffer.writeUInt32LE(seq, 8 + i * 4));
buffer = Buffer.concat([buffer, seqBuffer]);
}
let framBuffer = Buffer.from('LISTfram');
this.frames.forEach(frame => {
const iconHeader = Buffer.alloc(8);
iconHeader.write('icon', 0);
iconHeader.writeUInt32LE(frame.length, 4);
framBuffer = Buffer.concat([framBuffer, iconHeader, frame]);
});
buffer = Buffer.concat([buffer, Buffer.alloc(8)]);
buffer.write('LIST', buffer.length - 8);
buffer.writeUInt32LE(framBuffer.length - 4, buffer.length - 4);
buffer = Buffer.concat([buffer, framBuffer.slice(4)]);
if (framBuffer.length % 2) buffer = Buffer.concat([buffer, Buffer.alloc(1)]);
fs.writeFileSync(outputFilename, buffer);
}
printProperties() {
console.log(`Signature: ${this.signature}`);
console.log(`RIFF Type: ${this.riffType}`);
console.log(`File Size: ${this.fileSize} bytes`);
console.log('ANI Header:');
for (let [key, value] of Object.entries(this.aniHeader)) {
console.log(` ${key}: ${value}`);
}
console.log('INFO:');
for (let [key, value] of Object.entries(this.info)) {
if (value) console.log(` ${key}: ${value}`);
}
console.log(`Rate Table: ${this.rateTable}`);
console.log(`Sequence Table: ${this.seqTable}`);
console.log(`Number of Frames: ${this.frames.length}`);
}
}
// Example usage
const ani = new AniFile('sample.ani');
ani.read();
ani.printProperties();
ani.write('output.ani');
This class uses Node.js for file I/O and handles .ANI files similarly to the Python and Java versions.
5. C Class for .ANI File Handling
C does not have classes, but we can use a struct with functions to emulate class-like behavior.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char signature[4];
char riff_type[4];
unsigned int file_size;
struct {
unsigned int cbSizeof;
unsigned int cFrames;
unsigned int cSteps;
unsigned int cx;
unsigned int cy;
unsigned int cBitCount;
unsigned int cPlanes;
unsigned int jifRate;
unsigned int fl;
} ani_header;
char info_inam[256];
char info_iart[256];
unsigned int* rate_table;
unsigned int rate_count;
unsigned int* seq_table;
unsigned int seq_count;
unsigned char** frames;
unsigned int* frame_sizes;
unsigned int frame_count;
} AniFile;
void ani_file_init(AniFile* ani, const char* filename) {
strcpy(ani->signature, "");
strcpy(ani->riff_type, "");
ani->file_size = 0;
memset(&ani->ani_header, 0, sizeof(ani->ani_header));
strcpy(ani->info_inam, "");
strcpy(ani->info_iart, "");
ani->rate_table = NULL;
ani->rate_count = 0;
ani->seq_table = NULL;
ani->seq_count = 0;
ani->frames = NULL;
ani->frame_sizes = NULL;
ani->frame_count = 0;
}
void ani_file_read(AniFile* ani, const char* filename) {
FILE* f = fopen(filename, "rb");
if (!f) {
printf("Cannot open file\n");
return;
}
fread(ani->signature, 1, 4, f);
if (strncmp(ani->signature, "RIFF", 4) != 0) {
printf("Not a valid RIFF file\n");
fclose(f);
return;
}
fread(&ani->file_size, 4, 1, f);
fread(ani->riff_type, 1, 4, f);
if (strncmp(ani->riff_type, "ACON", 4) != 0) {
printf("Not an ANI file\n");
fclose(f);
return;
}
long file_end = ani->file_size + 8;
while (ftell(f) < file_end) {
char chunk_id[4];
unsigned int chunk_size;
fread(chunk_id, 1, 4, f);
fread(&chunk_size, 4, 1, f);
unsigned char* chunk_data = malloc(chunk_size);
fread(chunk_data, 1, chunk_size, f);
if (strncmp(chunk_id, "anih", 4) == 0) {
memcpy(&ani->ani_header, chunk_data, 36);
} else if (strncmp(chunk_id, "LIST", 4) == 0) {
if (strncmp((char*)chunk_data, "INFO", 4) == 0) {
unsigned int pos = 4;
while (pos < chunk_size) {
char sub_id[4];
unsigned int sub_size;
memcpy(sub_id, chunk_data + pos, 4);
memcpy(&sub_size, chunk_data + pos + 4, 4);
if (strncmp(sub_id, "INAM", 4) == 0) {
strncpy(ani->info_inam, (char*)(chunk_data + pos + 8), sub_size - 1);
} else if (strncmp(sub_id, "IART", 4) == 0) {
strncpy(ani->info_iart, (char*)(chunk_data + pos + 8), sub_size - 1);
}
pos += 8 + sub_size;
if (pos % 2) pos++;
}
} else if (strncmp((char*)chunk_data, "fram", 4) == 0) {
unsigned int pos = 4;
ani->frame_count = 0;
while (pos < chunk_size) {
if (strncmp((char*)(chunk_data + pos), "icon", 4) == 0) {
unsigned int icon_size;
memcpy(&icon_size, chunk_data + pos + 4, 4);
ani->frame_count++;
ani->frames = realloc(ani->frames, ani->frame_count * sizeof(unsigned char*));
ani->frame_sizes = realloc(ani->frame_sizes, ani->frame_count * sizeof(unsigned int));
ani->frames[ani->frame_count - 1] = malloc(icon_size);
memcpy(ani->frames[ani->frame_count - 1], chunk_data + pos + 8, icon_size);
ani->frame_sizes[ani->frame_count - 1] = icon_size;
pos += 8 + icon_size;
if (pos % 2) pos++;
}
}
}
} else if (strncmp(chunk_id, "rate", 4) == 0) {
ani->rate_count = chunk_size / 4;
ani->rate_table = malloc(chunk_size);
memcpy(ani->rate_table, chunk_data, chunk_size);
} else if (strncmp(chunk_id, "seq ", 4) == 0) {
ani->seq_count = chunk_size / 4;
ani->seq_table = malloc(chunk_size);
memcpy(ani->seq_table, chunk_data, chunk_size);
}
free(chunk_data);
if (ftell(f) % 2) fseek(f, 1, SEEK_CUR);
}
fclose(f);
}
void ani_file_write(AniFile* ani, const char* output_filename) {
FILE* f = fopen(output_filename, "wb");
if (!f) {
printf("Cannot open output file\n");
return;
}
fwrite("RIFF", 1, 4, f);
fwrite(&ani->file_size, 4, 1, f);
fwrite("ACON", 1, 4, f);
fwrite("anih", 1, 4, f);
unsigned int anih_size = 36;
fwrite(&anih_size, 4, 1, f);
fwrite(&ani->ani_header, 36, 1, f);
if (strlen(ani->info_inam) || strlen(ani->info_iart)) {
unsigned char* info_data = malloc(8);
memcpy(info_data, "INFO", 4);
unsigned int info_size = 4;
if (strlen(ani->info_inam)) {
info_size += 8 + strlen(ani->info_inam) + 1;
info_data = realloc(info_data, info_size);
memcpy(info_data + info_size - strlen(ani->info_inam) - 1 - 8, "INAM", 4);
unsigned int inam_size = strlen(ani->info_inam) + 1;
memcpy(info_data + info_size - strlen(ani->info_inam) - 1 - 4, &inam_size, 4);
memcpy(info_data + info_size - strlen(ani->info_inam) - 1, ani->info_inam, strlen(ani->info_inam) + 1);
}
if (strlen(ani->info_iart)) {
info_size += 8 + strlen(ani->info_iart) + 1;
info_data = realloc(info_data, info_size);
memcpy(info_data + info_size - strlen(ani->info_iart) - 1 - 8, "IART", 4);
unsigned int iart_size = strlen(ani->info_iart) + 1;
memcpy(info_data + info_size - strlen(ani->info_iart) - 1 - 4, &iart_size, 4);
memcpy(info_data + info_size - strlen(ani->info_iart) - 1, ani->info_iart, strlen(ani->info_iart) + 1);
}
fwrite("LIST", 1, 4, f);
fwrite(&info_size, 4, 1, f);
fwrite(info_data, 1, info_size, f);
if (info_size % 2) fwrite("\x00", 1, 1, f);
free(info_data);
}
if (ani->rate_count) {
fwrite("rate", 1, 4, f);
unsigned int rate_size = ani->rate_count * 4;
fwrite(&rate_size, 4, 1, f);
fwrite(ani->rate_table, 1, rate_size, f);
}
if (ani->seq_count) {
fwrite("seq ", 1, 4, f);
unsigned int seq_size = ani->seq_count * 4;
fwrite(&seq_size, 4, 1, f);
fwrite(ani->seq_table, 1, seq_size, f);
}
fwrite("LIST", 1, 4, f);
unsigned int fram_size = 4;
for (unsigned int i = 0; i < ani->frame_count; i++) {
fram_size += 8 + ani->frame_sizes[i];
}
fwrite(&fram_size, 4, 1, f);
fwrite("fram", 1, 4, f);
for (unsigned int i = 0; i < ani->frame_count; i++) {
fwrite("icon", 1, 4, f);
fwrite(&ani->frame_sizes[i], 4, 1, f);
fwrite(ani->frames[i], 1, ani->frame_sizes[i], f);
}
if (fram_size % 2) fwrite("\x00", 1, 1, f);
fclose(f);
}
void ani_file_print_properties(AniFile* ani) {
printf("Signature: %.4s\n", ani->signature);
printf("RIFF Type: %.4s\n", ani->riff_type);
printf("File Size: %u bytes\n", ani->file_size);
printf("ANI Header:\n");
printf(" cbSizeof: %u\n", ani->ani_header.cbSizeof);
printf(" cFrames: %u\n", ani->ani_header.cFrames);
printf(" cSteps: %u\n", ani->ani_header.cSteps);
printf(" cx: %u\n", ani->ani_header.cx);
printf(" cy: %u\n", ani->ani_header.cy);
printf(" cBitCount: %u\n", ani->ani_header.cBitCount);
printf(" cPlanes: %u\n", ani->ani_header.cPlanes);
printf(" jifRate: %u\n", ani->ani_header.jifRate);
printf(" fl: %u\n", ani->ani_header.fl);
printf("INFO:\n");
if (strlen(ani->info_inam)) printf(" INAM: %s\n", ani->info_inam);
if (strlen(ani->info_iart)) printf(" IART: %s\n", ani->info_iart);
printf("Rate Table: [");
for (unsigned int i = 0; i < ani->rate_count; i++) {
printf("%u", ani->rate_table[i]);
if (i < ani->rate_count - 1) printf(", ");
}
printf("]\n");
printf("Sequence Table: [");
for (unsigned int i = 0; i < ani->seq_count; i++) {
printf("%u", ani->seq_table[i]);
if (i < ani->seq_count - 1) printf(", ");
}
printf("]\n");
printf("Number of Frames: %u\n", ani->frame_count);
}
void ani_file_free(AniFile* ani) {
if (ani->rate_table) free(ani->rate_table);
if (ani->seq_table) free(ani->seq_table);
for (unsigned int i = 0; i < ani->frame_count; i++) {
if (ani->frames[i]) free(ani->frames[i]);
}
if (ani->frames) free(ani->frames);
if (ani->frame_sizes) free(ani->frame_sizes);
}
int main() {
AniFile ani;
ani_file_init(&ani, "sample.ani");
ani_file_read(&ani, "sample.ani");
ani_file_print_properties(&ani);
ani_file_write(&ani, "output.ani");
ani_file_free(&ani);
return 0;
}
This C implementation uses a struct and functions to handle .ANI files, ensuring proper memory management.
Notes and Limitations
- Hotspot Handling: The hotspot coordinates are embedded in the ICO frame data. Parsing ICO headers to extract hotspots requires additional logic, which is omitted for brevity but can be added if needed.
- File Size Calculation: The
file_size
field in the write methods assumes the original file size is retained. For new files, you may need to recalculate based on chunk sizes. - Error Handling: Basic error checking is included, but robust validation (e.g., ICO frame integrity) is not implemented.
- Testing: The code assumes a valid .ANI file. Test with real .ANI files to ensure compatibility.
- Dependencies: The JavaScript code requires Node.js for file operations. The Python and Java code use standard libraries. The C code requires standard C libraries.
If you need further enhancements (e.g., ICO parsing for hotspots, validation, or specific error handling), please let me know!
1. List of Properties for the .ANI File Format
The .ANI file format is a RIFF-based container for Windows animated cursors. The properties intrinsic to the format (structural fields, chunks, and metadata) are as follows, based on the specifications:
- Title: Optional string (from INAM chunk in INFO list), typically ASCII, null-terminated or sized.
- Author: Optional string (from IART chunk in INFO list), typically ASCII, null-terminated or sized.
- Header Size: Unsigned 32-bit integer (cbSizeof in anih chunk, always 36 bytes).
- Number of Unique Frames: Unsigned 32-bit integer (cFrames in anih chunk, count of icon chunks).
- Number of Animation Steps: Unsigned 32-bit integer (cSteps in anih chunk, steps in the animation cycle).
- Frame Width: Unsigned 32-bit integer (cx in anih chunk, unused/reserved, typically 0).
- Frame Height: Unsigned 32-bit integer (cy in anih chunk, unused/reserved, typically 0).
- Bits per Pixel: Unsigned 32-bit integer (cBitCount in anih chunk, unused/reserved, typically 0).
- Color Planes: Unsigned 32-bit integer (cPlanes in anih chunk, unused/reserved, typically 0).
- Default Display Rate: Unsigned 32-bit integer (jifRate in anih chunk, default delay in jiffies, where 1 jiffy ≈ 16.67 ms).
- Flags: Unsigned 32-bit integer (fl in anih chunk; bit 0: frames are in icon/cursor format (AF_ICON=1); bit 1: sequence chunk present (AF_SEQUENCE=2)).
- Rate Table: Optional array of unsigned 32-bit integers (from rate chunk; length equals cSteps if AF_SEQUENCE set, else cFrames; per-step/frame delays in jiffies).
- Sequence Table: Optional array of unsigned 32-bit integers (from seq chunk; present if AF_SEQUENCE set; length equals cSteps; frame indices for each step).
- Frame Data: Array of binary data blobs (from icon sub-chunks in fram list; length equals cFrames; each blob is a complete .CUR format binary for a cursor frame, including its own header, hotspot, and image data).
These properties define the format's structure, with all integers in little-endian byte order. The file must start with RIFF 'ACON', and chunks may be padded to even sizes.
2. Python Class
import struct
import os
class AniFile:
def __init__(self):
self.title = None
self.author = None
self.header_size = 36
self.num_frames = 0
self.num_steps = 0
self.frame_width = 0
self.frame_height = 0
self.bits_per_pixel = 0
self.color_planes = 0
self.default_rate = 0
self.flags = 0
self.rate_table = []
self.sequence_table = []
self.frame_data = []
def _read_chunk(self, f):
chunk_id = f.read(4).decode('ascii')
chunk_size = struct.unpack('<I', f.read(4))[0]
chunk_data = f.read(chunk_size)
if chunk_size % 2 == 1:
f.read(1) # Padding byte
return chunk_id, chunk_data
def load(self, filename):
with open(filename, 'rb') as f:
riff_id = f.read(4).decode('ascii')
if riff_id != 'RIFF':
raise ValueError("Not a RIFF file")
riff_size = struct.unpack('<I', f.read(4))[0]
acon_id = f.read(4).decode('ascii')
if acon_id != 'ACON':
raise ValueError("Not an ACON RIFF file")
while f.tell() < os.path.getsize(filename):
chunk_id, chunk_data = self._read_chunk(f)
if chunk_id == 'LIST':
list_type = chunk_data[:4].decode('ascii')
if list_type == 'INFO':
info_pos = 4
while info_pos < len(chunk_data):
info_id = chunk_data[info_pos:info_pos+4].decode('ascii')
info_size = struct.unpack('<I', chunk_data[info_pos+4:info_pos+8])[0]
info_data = chunk_data[info_pos+8:info_pos+8+info_size]
if info_id == 'INAM':
self.title = info_data.decode('ascii').rstrip('\x00')
elif info_id == 'IART':
self.author = info_data.decode('ascii').rstrip('\x00')
info_pos += 8 + info_size + (info_size % 2)
elif chunk_id == 'anih':
(self.header_size, self.num_frames, self.num_steps, self.frame_width,
self.frame_height, self.bits_per_pixel, self.color_planes,
self.default_rate, self.flags) = struct.unpack('<9I', chunk_data[:36])
elif chunk_id == 'rate':
num_rates = len(chunk_data) // 4
self.rate_table = list(struct.unpack(f'<{num_rates}I', chunk_data))
elif chunk_id == 'seq ':
num_seq = len(chunk_data) // 4
self.sequence_table = list(struct.unpack(f'<{num_seq}I', chunk_data))
elif chunk_id == 'fram':
fram_pos = 4 # Skip 'fram' type if present, but it's LIST 'fram'
while fram_pos < len(chunk_data):
fram_id = chunk_data[fram_pos:fram_pos+4].decode('ascii')
fram_size = struct.unpack('<I', chunk_data[fram_pos+4:fram_pos+8])[0]
fram_data = chunk_data[fram_pos+8:fram_pos+8+fram_size]
if fram_id == 'icon':
self.frame_data.append(fram_data)
fram_pos += 8 + fram_size + (fram_size % 2)
def save(self, filename):
with open(filename, 'wb') as f:
# Calculate total size later
f.write(b'RIFF')
f.write(struct.pack('<I', 0)) # Placeholder for size
f.write(b'ACON')
# INFO list if title or author
if self.title or self.author:
info_data = b''
if self.title:
title_bytes = self.title.encode('ascii') + b'\x00'
pad = len(title_bytes) % 2
info_data += b'INAM' + struct.pack('<I', len(title_bytes)) + title_bytes + (b'\x00' if pad else b'')
if self.author:
author_bytes = self.author.encode('ascii') + b'\x00'
pad = len(author_bytes) % 2
info_data += b'IART' + struct.pack('<I', len(author_bytes)) + author_bytes + (b'\x00' if pad else b'')
list_size = len(info_data) + 4 # 'INFO'
f.write(b'LIST')
f.write(struct.pack('<I', list_size))
f.write(b'INFO')
f.write(info_data)
# anih
anih_data = struct.pack('<9I', self.header_size, self.num_frames, self.num_steps, self.frame_width,
self.frame_height, self.bits_per_pixel, self.color_planes,
self.default_rate, self.flags)
f.write(b'anih')
f.write(struct.pack('<I', 36))
f.write(anih_data)
# rate if present
if self.rate_table:
rate_data = struct.pack(f'<{len(self.rate_table)}I', *self.rate_table)
pad = len(rate_data) % 2
f.write(b'rate')
f.write(struct.pack('<I', len(rate_data)))
f.write(rate_data)
if pad:
f.write(b'\x00')
# seq if present
if self.sequence_table:
seq_data = struct.pack(f'<{len(self.sequence_table)}I', *self.sequence_table)
pad = len(seq_data) % 2
f.write(b'seq ')
f.write(struct.pack('<I', len(seq_data)))
f.write(seq_data)
if pad:
f.write(b'\x00')
# fram list
fram_data = b''
for frame in self.frame_data:
pad = len(frame) % 2
fram_data += b'icon' + struct.pack('<I', len(frame)) + frame + (b'\x00' if pad else b'')
list_size = len(fram_data) + 4 # 'fram'
f.write(b'LIST')
f.write(struct.pack('<I', list_size))
f.write(b'fram')
f.write(fram_data)
# Update RIFF size
total_size = os.path.getsize(filename) - 8
f.seek(4)
f.write(struct.pack('<I', total_size))
3. Java Class
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.util.*;
public class AniFile {
private String title = null;
private String author = null;
private int headerSize = 36;
private int numFrames = 0;
private int numSteps = 0;
private int frameWidth = 0;
private int frameHeight = 0;
private int bitsPerPixel = 0;
private int colorPlanes = 0;
private int defaultRate = 0;
private int flags = 0;
private List<Integer> rateTable = new ArrayList<>();
private List<Integer> sequenceTable = new ArrayList<>();
private List<byte[]> frameData = new ArrayList<>();
private ByteBuffer readChunk(RandomAccessFile raf) throws IOException {
byte[] idBytes = new byte[4];
raf.read(idBytes);
String chunkId = new String(idBytes);
int chunkSize = Integer.reverseBytes(raf.readInt()); // LE to BE
byte[] data = new byte[chunkSize];
raf.read(data);
if (chunkSize % 2 == 1) {
raf.skipBytes(1);
}
return ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
}
public void load(String filename) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(filename, "r")) {
byte[] riffId = new byte[4];
raf.read(riffId);
if (!new String(riffId).equals("RIFF")) {
throw new IOException("Not a RIFF file");
}
int riffSize = Integer.reverseBytes(raf.readInt());
byte[] aconId = new byte[4];
raf.read(aconId);
if (!new String(aconId).equals("ACON")) {
throw new IOException("Not an ACON RIFF file");
}
while (raf.getFilePointer() < raf.length()) {
byte[] idBytes = new byte[4];
raf.read(idBytes);
String chunkId = new String(idBytes);
int chunkSize = Integer.reverseBytes(raf.readInt());
byte[] chunkData = new byte[chunkSize];
raf.read(chunkData);
ByteBuffer bb = ByteBuffer.wrap(chunkData).order(ByteOrder.LITTLE_ENDIAN);
if (chunkSize % 2 == 1) {
raf.skipBytes(1);
}
if (chunkId.equals("LIST")) {
byte[] listType = new byte[4];
bb.get(listType);
if (new String(listType).equals("INFO")) {
int pos = 4;
while (pos < chunkSize) {
byte[] infoIdBytes = new byte[4];
System.arraycopy(chunkData, pos, infoIdBytes, 0, 4);
String infoId = new String(infoIdBytes);
ByteBuffer infoBb = ByteBuffer.wrap(chunkData, pos + 4, 4).order(ByteOrder.LITTLE_ENDIAN);
int infoSize = infoBb.getInt();
byte[] infoData = new byte[infoSize];
System.arraycopy(chunkData, pos + 8, infoData, 0, infoSize);
if (infoId.equals("INAM")) {
title = new String(infoData).trim().replace("\0", "");
} else if (infoId.equals("IART")) {
author = new String(infoData).trim().replace("\0", "");
}
pos += 8 + infoSize + (infoSize % 2);
}
}
} else if (chunkId.equals("anih")) {
headerSize = bb.getInt();
numFrames = bb.getInt();
numSteps = bb.getInt();
frameWidth = bb.getInt();
frameHeight = bb.getInt();
bitsPerPixel = bb.getInt();
colorPlanes = bb.getInt();
defaultRate = bb.getInt();
flags = bb.getInt();
} else if (chunkId.equals("rate")) {
for (int i = 0; i < chunkSize / 4; i++) {
rateTable.add(bb.getInt());
}
} else if (chunkId.equals("seq ")) {
for (int i = 0; i < chunkSize / 4; i++) {
sequenceTable.add(bb.getInt());
}
} else if (chunkId.equals("LIST")) {
byte[] listType = new byte[4];
bb.get(listType);
if (new String(listType).equals("fram")) {
int pos = 4;
while (pos < chunkSize) {
byte[] framIdBytes = new byte[4];
System.arraycopy(chunkData, pos, framIdBytes, 0, 4);
String framId = new String(framIdBytes);
ByteBuffer framBb = ByteBuffer.wrap(chunkData, pos + 4, 4).order(ByteOrder.LITTLE_ENDIAN);
int framSize = framBb.getInt();
byte[] framBytes = new byte[framSize];
System.arraycopy(chunkData, pos + 8, framBytes, 0, framSize);
if (framId.equals("icon")) {
frameData.add(framBytes);
}
pos += 8 + framSize + (framSize % 2);
}
}
}
}
}
}
public void save(String filename) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(filename, "rw");
FileChannel channel = raf.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.LITTLE_ENDIAN); // Temp buffer
buffer.put("RIFF".getBytes());
buffer.putInt(0); // Placeholder for size
buffer.put("ACON".getBytes());
// INFO list
if (title != null || author != null) {
ByteBuffer infoBuffer = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN);
if (title != null) {
byte[] titleBytes = (title + "\0").getBytes();
int pad = titleBytes.length % 2;
infoBuffer.put("INAM".getBytes());
infoBuffer.putInt(titleBytes.length);
infoBuffer.put(titleBytes);
if (pad == 1) infoBuffer.put((byte) 0);
}
if (author != null) {
byte[] authorBytes = (author + "\0").getBytes();
int pad = authorBytes.length % 2;
infoBuffer.put("IART".getBytes());
infoBuffer.putInt(authorBytes.length);
infoBuffer.put(authorBytes);
if (pad == 1) infoBuffer.put((byte) 0);
}
int infoSize = infoBuffer.position();
buffer.put("LIST".getBytes());
buffer.putInt(infoSize + 4);
buffer.put("INFO".getBytes());
infoBuffer.flip();
buffer.put(infoBuffer);
}
// anih
buffer.put("anih".getBytes());
buffer.putInt(36);
buffer.putInt(headerSize);
buffer.putInt(numFrames);
buffer.putInt(numSteps);
buffer.putInt(frameWidth);
buffer.putInt(frameHeight);
buffer.putInt(bitsPerPixel);
buffer.putInt(colorPlanes);
buffer.putInt(defaultRate);
buffer.putInt(flags);
// rate
if (!rateTable.isEmpty()) {
int rateSize = rateTable.size() * 4;
int pad = rateSize % 2;
buffer.put("rate".getBytes());
buffer.putInt(rateSize);
for (int rate : rateTable) {
buffer.putInt(rate);
}
if (pad == 1) buffer.put((byte) 0);
}
// seq
if (!sequenceTable.isEmpty()) {
int seqSize = sequenceTable.size() * 4;
int pad = seqSize % 2;
buffer.put("seq ".getBytes());
buffer.putInt(seqSize);
for (int seq : sequenceTable) {
buffer.putInt(seq);
}
if (pad == 1) buffer.put((byte) 0);
}
// fram list
ByteBuffer framBuffer = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.LITTLE_ENDIAN);
for (byte[] frame : frameData) {
int framSize = frame.length;
int pad = framSize % 2;
framBuffer.put("icon".getBytes());
framBuffer.putInt(framSize);
framBuffer.put(frame);
if (pad == 1) framBuffer.put((byte) 0);
}
int framPos = framBuffer.position();
buffer.put("LIST".getBytes());
buffer.putInt(framPos + 4);
buffer.put("fram".getBytes());
framBuffer.flip();
buffer.put(framBuffer);
// Update RIFF size
buffer.flip();
int totalSize = buffer.limit() - 8;
buffer.position(4);
buffer.putInt(totalSize);
buffer.position(0);
channel.write(buffer);
channel.truncate(buffer.limit());
}
}
}
4. JavaScript Class
const fs = require('fs');
class AniFile {
constructor() {
this.title = null;
this.author = null;
this.headerSize = 36;
this.numFrames = 0;
this.numSteps = 0;
this.frameWidth = 0;
this.frameHeight = 0;
this.bitsPerPixel = 0;
this.colorPlanes = 0;
this.defaultRate = 0;
this.flags = 0;
this.rateTable = [];
this.sequenceTable = [];
this.frameData = [];
}
load(filename) {
const data = fs.readFileSync(filename);
let buffer = Buffer.from(data);
let pos = 0;
const riffId = buffer.toString('ascii', pos, pos + 4);
pos += 4;
if (riffId !== 'RIFF') throw new Error('Not a RIFF file');
const riffSize = buffer.readUInt32LE(pos);
pos += 4;
const aconId = buffer.toString('ascii', pos, pos + 4);
pos += 4;
if (aconId !== 'ACON') throw new Error('Not an ACON RIFF file');
while (pos < buffer.length) {
const chunkId = buffer.toString('ascii', pos, pos + 4);
pos += 4;
const chunkSize = buffer.readUInt32LE(pos);
pos += 4;
const chunkData = buffer.slice(pos, pos + chunkSize);
pos += chunkSize;
if (chunkSize % 2 === 1) pos += 1;
if (chunkId === 'LIST') {
const listType = chunkData.toString('ascii', 0, 4);
if (listType === 'INFO') {
let infoPos = 4;
while (infoPos < chunkData.length) {
const infoId = chunkData.toString('ascii', infoPos, infoPos + 4);
const infoSize = chunkData.readUInt32LE(infoPos + 4);
const infoData = chunkData.slice(infoPos + 8, infoPos + 8 + infoSize);
if (infoId === 'INAM') {
this.title = infoData.toString('ascii').replace(/\0/g, '').trim();
} else if (infoId === 'IART') {
this.author = infoData.toString('ascii').replace(/\0/g, '').trim();
}
infoPos += 8 + infoSize + (infoSize % 2);
}
} else if (listType === 'fram') {
let framPos = 4;
while (framPos < chunkData.length) {
const framId = chunkData.toString('ascii', framPos, framPos + 4);
const framSize = chunkData.readUInt32LE(framPos + 4);
const framBytes = chunkData.slice(framPos + 8, framPos + 8 + framSize);
if (framId === 'icon') {
this.frameData.push(framBytes);
}
framPos += 8 + framSize + (framSize % 2);
}
}
} else if (chunkId === 'anih') {
this.headerSize = chunkData.readUInt32LE(0);
this.numFrames = chunkData.readUInt32LE(4);
this.numSteps = chunkData.readUInt32LE(8);
this.frameWidth = chunkData.readUInt32LE(12);
this.frameHeight = chunkData.readUInt32LE(16);
this.bitsPerPixel = chunkData.readUInt32LE(20);
this.colorPlanes = chunkData.readUInt32LE(24);
this.defaultRate = chunkData.readUInt32LE(28);
this.flags = chunkData.readUInt32LE(32);
} else if (chunkId === 'rate') {
for (let i = 0; i < chunkSize / 4; i++) {
this.rateTable.push(chunkData.readUInt32LE(i * 4));
}
} else if (chunkId === 'seq ') {
for (let i = 0; i < chunkSize / 4; i++) {
this.sequenceTable.push(chunkData.readUInt32LE(i * 4));
}
}
}
}
save(filename) {
let buffers = [];
buffers.push(Buffer.from('RIFF'));
const sizePlaceholder = Buffer.alloc(4);
buffers.push(sizePlaceholder);
buffers.push(Buffer.from('ACON'));
// INFO list
if (this.title || this.author) {
let infoBuffers = [];
if (this.title) {
const titleBytes = Buffer.from(this.title + '\0');
const pad = titleBytes.length % 2 === 1 ? Buffer.from('\0') : Buffer.alloc(0);
infoBuffers.push(Buffer.from('INAM'));
infoBuffers.push(Buffer.alloc(4));
infoBuffers[infoBuffers.length - 1].writeUInt32LE(titleBytes.length);
infoBuffers.push(titleBytes);
infoBuffers.push(pad);
}
if (this.author) {
const authorBytes = Buffer.from(this.author + '\0');
const pad = authorBytes.length % 2 === 1 ? Buffer.from('\0') : Buffer.alloc(0);
infoBuffers.push(Buffer.from('IART'));
infoBuffers.push(Buffer.alloc(4));
infoBuffers[infoBuffers.length - 1].writeUInt32LE(authorBytes.length);
infoBuffers.push(authorBytes);
infoBuffers.push(pad);
}
const infoData = Buffer.concat(infoBuffers);
buffers.push(Buffer.from('LIST'));
buffers.push(Buffer.alloc(4));
buffers[buffers.length - 1].writeUInt32LE(infoData.length + 4);
buffers.push(Buffer.from('INFO'));
buffers.push(infoData);
}
// anih
const anihBuffer = Buffer.alloc(44);
anihBuffer.write('anih', 0, 4);
anihBuffer.writeUInt32LE(36, 4);
anihBuffer.writeUInt32LE(this.headerSize, 8);
anihBuffer.writeUInt32LE(this.numFrames, 12);
anihBuffer.writeUInt32LE(this.numSteps, 16);
anihBuffer.writeUInt32LE(this.frameWidth, 20);
anihBuffer.writeUInt32LE(this.frameHeight, 24);
anihBuffer.writeUInt32LE(this.bitsPerPixel, 28);
anihBuffer.writeUInt32LE(this.colorPlanes, 32);
anihBuffer.writeUInt32LE(this.defaultRate, 36);
anihBuffer.writeUInt32LE(this.flags, 40);
buffers.push(anihBuffer.slice(0, 44));
// rate
if (this.rateTable.length > 0) {
const rateBuffer = Buffer.alloc(8 + this.rateTable.length * 4);
rateBuffer.write('rate', 0, 4);
rateBuffer.writeUInt32LE(this.rateTable.length * 4, 4);
this.rateTable.forEach((rate, i) => rateBuffer.writeUInt32LE(rate, 8 + i * 4));
const pad = rateBuffer.length % 2 === 1 ? Buffer.from('\0') : Buffer.alloc(0);
buffers.push(rateBuffer);
buffers.push(pad);
}
// seq
if (this.sequenceTable.length > 0) {
const seqBuffer = Buffer.alloc(8 + this.sequenceTable.length * 4);
seqBuffer.write('seq ', 0, 4);
seqBuffer.writeUInt32LE(this.sequenceTable.length * 4, 4);
this.sequenceTable.forEach((seq, i) => seqBuffer.writeUInt32LE(seq, 8 + i * 4));
const pad = seqBuffer.length % 2 === 1 ? Buffer.from('\0') : Buffer.alloc(0);
buffers.push(seqBuffer);
buffers.push(pad);
}
// fram list
let framBuffers = [];
this.frameData.forEach(frame => {
const framBuffer = Buffer.alloc(8 + frame.length);
framBuffer.write('icon', 0, 4);
framBuffer.writeUInt32LE(frame.length, 4);
frame.copy(framBuffer, 8);
const pad = framBuffer.length % 2 === 1 ? Buffer.from('\0') : Buffer.alloc(0);
framBuffers.push(framBuffer);
framBuffers.push(pad);
});
const framData = Buffer.concat(framBuffers);
buffers.push(Buffer.from('LIST'));
buffers.push(Buffer.alloc(4));
buffers[buffers.length - 1].writeUInt32LE(framData.length + 4);
buffers.push(Buffer.from('fram'));
buffers.push(framData);
const fullBuffer = Buffer.concat(buffers);
sizePlaceholder.writeUInt32LE(fullBuffer.length - 8, 0);
fs.writeFileSync(filename, fullBuffer);
}
}
5. C Class (Implemented as C++ Class)
#include <fstream>
#include <vector>
#include <string>
#include <cstdint>
#include <cstring>
#include <stdexcept>
class AniFile {
public:
std::string title;
std::string author;
uint32_t header_size = 36;
uint32_t num_frames = 0;
uint32_t num_steps = 0;
uint32_t frame_width = 0;
uint32_t frame_height = 0;
uint32_t bits_per_pixel = 0;
uint32_t color_planes = 0;
uint32_t default_rate = 0;
uint32_t flags = 0;
std::vector<uint32_t> rate_table;
std::vector<uint32_t> sequence_table;
std::vector<std::vector<uint8_t>> frame_data;
void load(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
if (!file) throw std::runtime_error("Cannot open file");
char riff_id[5] = {0};
file.read(riff_id, 4);
if (std::strcmp(riff_id, "RIFF") != 0) throw std::runtime_error("Not a RIFF file");
uint32_t riff_size;
file.read(reinterpret_cast<char*>(&riff_size), sizeof(riff_size));
char acon_id[5] = {0};
file.read(acon_id, 4);
if (std::strcmp(acon_id, "ACON") != 0) throw std::runtime_error("Not an ACON RIFF file");
while (file) {
char chunk_id[5] = {0};
file.read(chunk_id, 4);
if (file.eof()) break;
uint32_t chunk_size;
file.read(reinterpret_cast<char*>(&chunk_size), sizeof(chunk_size));
std::vector<char> chunk_data(chunk_size);
file.read(chunk_data.data(), chunk_size);
if (chunk_size % 2 == 1) {
char pad;
file.read(&pad, 1);
}
if (std::strcmp(chunk_id, "LIST") == 0) {
char list_type[5] = {0};
std::memcpy(list_type, chunk_data.data(), 4);
if (std::strcmp(list_type, "INFO") == 0) {
size_t info_pos = 4;
while (info_pos < chunk_size) {
char info_id[5] = {0};
std::memcpy(info_id, chunk_data.data() + info_pos, 4);
uint32_t info_size;
std::memcpy(&info_size, chunk_data.data() + info_pos + 4, sizeof(info_size));
std::string info_str(chunk_data.data() + info_pos + 8, info_size);
size_t null_pos = info_str.find('\0');
if (null_pos != std::string::npos) info_str = info_str.substr(0, null_pos);
if (std::strcmp(info_id, "INAM") == 0) {
title = info_str;
} else if (std::strcmp(info_id, "IART") == 0) {
author = info_str;
}
info_pos += 8 + info_size + (info_size % 2);
}
} else if (std::strcmp(list_type, "fram") == 0) {
size_t fram_pos = 4;
while (fram_pos < chunk_size) {
char fram_id[5] = {0};
std::memcpy(fram_id, chunk_data.data() + fram_pos, 4);
uint32_t fram_size;
std::memcpy(&fram_size, chunk_data.data() + fram_pos + 4, sizeof(fram_size));
std::vector<uint8_t> fram_bytes(fram_size);
std::memcpy(fram_bytes.data(), chunk_data.data() + fram_pos + 8, fram_size);
if (std::strcmp(fram_id, "icon") == 0) {
frame_data.push_back(fram_bytes);
}
fram_pos += 8 + fram_size + (fram_size % 2);
}
}
} else if (std::strcmp(chunk_id, "anih") == 0) {
std::memcpy(&header_size, chunk_data.data(), 4);
std::memcpy(&num_frames, chunk_data.data() + 4, 4);
std::memcpy(&num_steps, chunk_data.data() + 8, 4);
std::memcpy(&frame_width, chunk_data.data() + 12, 4);
std::memcpy(&frame_height, chunk_data.data() + 16, 4);
std::memcpy(&bits_per_pixel, chunk_data.data() + 20, 4);
std::memcpy(&color_planes, chunk_data.data() + 24, 4);
std::memcpy(&default_rate, chunk_data.data() + 28, 4);
std::memcpy(&flags, chunk_data.data() + 32, 4);
} else if (std::strcmp(chunk_id, "rate") == 0) {
rate_table.resize(chunk_size / 4);
std::memcpy(rate_table.data(), chunk_data.data(), chunk_size);
} else if (std::strcmp(chunk_id, "seq ") == 0) {
sequence_table.resize(chunk_size / 4);
std::memcpy(sequence_table.data(), chunk_data.data(), chunk_size);
}
}
}
void save(const std::string& filename) {
std::ofstream file(filename, std::ios::binary);
if (!file) throw std::runtime_error("Cannot open file for writing");
file.write("RIFF", 4);
uint32_t placeholder_size = 0;
file.write(reinterpret_cast<const char*>(&placeholder_size), 4);
file.write("ACON", 4);
// INFO list
if (!title.empty() || !author.empty()) {
std::vector<char> info_data;
if (!title.empty()) {
std::string title_str = title + '\0';
uint32_t str_size = title_str.size();
size_t pad = str_size % 2;
info_data.insert(info_data.end(), {'I', 'N', 'A', 'M'});
info_data.insert(info_data.end(), reinterpret_cast<char*>(&str_size), reinterpret_cast<char*>(&str_size) + 4);
info_data.insert(info_data.end(), title_str.begin(), title_str.end());
if (pad) info_data.push_back('\0');
}
if (!author.empty()) {
std::string author_str = author + '\0';
uint32_t str_size = author_str.size();
size_t pad = str_size % 2;
info_data.insert(info_data.end(), {'I', 'A', 'R', 'T'});
info_data.insert(info_data.end(), reinterpret_cast<char*>(&str_size), reinterpret_cast<char*>(&str_size) + 4);
info_data.insert(info_data.end(), author_str.begin(), author_str.end());
if (pad) info_data.push_back('\0');
}
uint32_t list_size = info_data.size() + 4;
file.write("LIST", 4);
file.write(reinterpret_cast<const char*>(&list_size), 4);
file.write("INFO", 4);
file.write(info_data.data(), info_data.size());
}
// anih
uint32_t anih_size = 36;
file.write("anih", 4);
file.write(reinterpret_cast<const char*>(&anih_size), 4);
file.write(reinterpret_cast<const char*>(&header_size), 4);
file.write(reinterpret_cast<const char*>(&num_frames), 4);
file.write(reinterpret_cast<const char*>(&num_steps), 4);
file.write(reinterpret_cast<const char*>(&frame_width), 4);
file.write(reinterpret_cast<const char*>(&frame_height), 4);
file.write(reinterpret_cast<const char*>(&bits_per_pixel), 4);
file.write(reinterpret_cast<const char*>(&color_planes), 4);
file.write(reinterpret_cast<const char*>(&default_rate), 4);
file.write(reinterpret_cast<const char*>(&flags), 4);
// rate
if (!rate_table.empty()) {
uint32_t rate_size = rate_table.size() * 4;
size_t pad = rate_size % 2;
file.write("rate", 4);
file.write(reinterpret_cast<const char*>(&rate_size), 4);
file.write(reinterpret_cast<const char*>(rate_table.data()), rate_size);
if (pad) file.write("\0", 1);
}
// seq
if (!sequence_table.empty()) {
uint32_t seq_size = sequence_table.size() * 4;
size_t pad = seq_size % 2;
file.write("seq ", 4);
file.write(reinterpret_cast<const char*>(&seq_size), 4);
file.write(reinterpret_cast<const char*>(sequence_table.data()), seq_size);
if (pad) file.write("\0", 1);
}
// fram list
std::vector<char> fram_data;
for (const auto& frame : frame_data) {
uint32_t fram_size = frame.size();
size_t pad = fram_size % 2;
fram_data.insert(fram_data.end(), {'i', 'c', 'o', 'n'});
fram_data.insert(fram_data.end(), reinterpret_cast<const char*>(&fram_size), reinterpret_cast<const char*>(&fram_size) + 4);
fram_data.insert(fram_data.end(), frame.begin(), frame.end());
if (pad) fram_data.push_back('\0');
}
uint32_t list_size = fram_data.size() + 4;
file.write("LIST", 4);
file.write(reinterpret_cast<const char*>(&list_size), 4);
file.write("fram", 4);
file.write(fram_data.data(), fram_data.size());
// Update RIFF size
file.seekp(0, std::ios::end);
uint32_t total_size = file.tellp() - 8;
file.seekp(4);
file.write(reinterpret_cast<const char*>(&total_size), 4);
}
};