Skip to content

Instantly share code, notes, and snippets.

@grassmunk
Created April 16, 2020 00:11
Show Gist options
  • Save grassmunk/93d46d6b665d210215cde9331e875d17 to your computer and use it in GitHub Desktop.
Save grassmunk/93d46d6b665d210215cde9331e875d17 to your computer and use it in GitHub Desktop.
import sys
import os
import collections
import PIL.Image
import svgwrite
import pathlib
import shutil
import subprocess
import argparse
import logging
import logging.handlers
import configparser
import xml.etree.ElementTree as ET
from pathlib import Path
from PIL import Image
from configparser import ConfigParser
from fontTools import ttLib
#import binascii
import struct
from pprint import pprint
import json
def extract_ani( file_name):
# convert an ani a dict with Icon information
# input: .ani file location/name
# output: dict with cursor information
# from http://www.toolsandtips.de/Tutorial/Aufbau-Animierte-Cursor.htm
#1. 0000. First RIFF : then the size of the file as DWORD, a total of 8 bytes. Note the length of the file can be different!
# A: the actual length of the file.
# B: the length of the file minus the 8 bytes for RIFF and the DWORD for the length specification.
#2. 0008. ACON : This part may contain the following. Optional!
# LIST : Length as DWORD to "anih" without the 4 bytes of the length specification from LIST . The data can be: Optional!
# INAM: Size of the title as DWORD without the 4 bytes of the length specification, then data. Note there can be "INFO" in front of it! Optional!
# IART: Length from the author as DWORD without the 4 bytes of the length specification, then data.
# anih : size of the Ani header structure as DWORD maximum 36 bytes, then the structure with 36 bytes. The first value of the DWORD is with the size of the structure = 36 bytes. (See Ani header structure).
# rate : size of the rate as DWORD. Data in DWORDs. The specification of "rate" can also be optional !
# With which the speed of the image change can be adjusted more finely, does not have to be.
# Since a standard value for the speed is already entered in the Ani header structure (ANIHEADER.iDispRate) !
# According to the length specification. So with Hex 10 = 16 bytes, 4 DWORDs = 16 bytes follow.
# Example: The Ani has 4 pictures, then a " DWORD " followed by a DWORD with a hex number 10 = 16 bytes, the length as DWORD!
# This DWORD is followed by a DWORD for each picture, which indicates the speed of the picture change. That can be different.
# Eg. For 4 pictures 0000 0011, 0000 0011, 0000 0011, 0000 0011.or: for 4 pictures 0000 0011, 0000 0030, 0000 0050, 0000 0018.
# seq : Size of the sequence block as DWORD, data in DWORDs. The specification of "seq" can also be optional !
# What the order of the images is in the animation before the animation is repeated.
# Eg. 5 pictures are actually in the file (ANIHEADER.nFrames = 5) in ANIHEADER.nSteps = 8 there are 8 pictures.
# The length of "seq" would then be Hex 20 = 32 bytes = 8 DWORD.
# The arrangement of the pictures could be picture 1, picture 1, picture 2, picture 3, picture 4, picture 1, picture 2, picture 5 then the series is repeated.
# If the order of the pictures were 1,2,3,4,5, then you don't actually need a "seq" block.
# If there are more pictures in the ANIHEADER.nSteps than in the ANIHEADER.nFrames then the "seq" block is mandatory!
# Which of course must then correspond to the size of the number of images, e.g. 8 images * 4 bytes (= 1DWORD) = 32 bytes = hex 20 = 8 DWORDs.
# Furthermore, if the "rate" block is to be used, the "rate" block must have the same size as the "seq" block, ie also 32 bytes = 8 DWORDs! One DWORD for each image to be displayed.
#
# 3. LIST : Length of the rest of the file, as DWORD, from this length specification, that is after this DWORD.
# fram:
# icon : Size of the image data after this DWORD, then data. (First picture)
# Note! In the size specification, the sizes of the two cursor structures, the BITMAPINFOHEADER structure, the color table and the XOR and the AND image are added together.
# So from here to the next "icon" frame, or the end of the file if it is the last picture. The icon block contains according to the size specification.
# A: The structure CURHEADER (or iconheader) with the size is 6 bytes. (see CURHEADER structure).
# B: The structure CURSORDIEENTRY (or icondir) with the size is 16 bytes. (see CURSORDIEENTRYR structure)
# C: the structure BITMAPINFOHEADER with the size is 40 bytes. (see BITMAPINFOHEADER structure)
# D: color table (number of colors * 4 bytes)
# E: image data of the cursor first the XOR image then the AND image ........ ........ (etc.) . .......
#
# icon : size of the image data after this DWORD, then data. (Last picture)
# Note! In the size specification, the sizes of the two cursor structures, the BITMAPINFOHEADER structure, the color table and the XOR and the AND image are added together.
# So from here to the end of the file. The icon block contains according to the size specification.
# A: The structure CURHEADER with the size is 6 bytes. (see CURHEADER structure)
# B: the structure CURSORDIEENTRY with the size is 16 bytes. (see CURSORDIEENTRYR structure)
# C: the structure BITMAPINFOHEADER with the size is 40 bytes. (see BITMAPINFOHEADER structure)
# D: color table (number of colors * 4 bytes)
# E: image data of the cursor first the XOR image then the AND image
#
f = open(file_name,'rb')
ani_file = f.read()
f.close()
ani_bytes = bytearray(ani_file)
print(len(ani_bytes))
rate = False
seq = False
rtIconDir = False
rtIconDirEntry = False
INFO = False
icon = []
icon_count = 0
ckID = ani_bytes[0:4].decode()
ckSize = struct.unpack('<L',ani_bytes[4:8])[0]
ckForm = ani_bytes[8:12].decode()
total_size = 0
if ckID == 'RIFF':
print("RIFF detected! File: {}, ckSize :{}".format(file_name, ckSize))
if ckForm == 'ACON': #ACON is optional
print("ACON detected (optional)")
total_size = 12 # RIFF Header with ACON
else:
total_size = 8 # RIFF Header without ACON
if ckSize == len(ani_bytes) - 8:
ckSize = ckSize + 8 # Sometimes, but not always, the header isn't included in ckSize
print("Adjusting ckSize to actual file size: {}".format(ckSize))
while total_size < ckSize:
section = ani_bytes[total_size:total_size+4].decode()
total_size = total_size + 4
chunk_size = struct.unpack('<L',ani_bytes[total_size:total_size+4])[0]
total_size = total_size + 4
#print("Chunk {}, Size: {}".format(section, chunk_size))
#print(ani_bytes[total_size:total_size+36])
if section == 'anih': #ANI Header
print("RIFF section: anih")
anih = {
'cbSize': struct.unpack('<L',ani_bytes[total_size:total_size+4])[0],
'nFrames': struct.unpack('<L',ani_bytes[total_size+4:total_size+8])[0],
'nSteps' : struct.unpack('<L',ani_bytes[total_size+8:total_size+12])[0],
'iWidth' : struct.unpack('<L',ani_bytes[total_size+12:total_size+16])[0],
'iHeight' : struct.unpack('<L',ani_bytes[total_size+16:total_size+20])[0],
'iBitCount' : struct.unpack('<L',ani_bytes[total_size+20:total_size+24])[0],
'nPlanes' : struct.unpack('<L',ani_bytes[total_size+24:total_size+28])[0],
'iDispRate' : struct.unpack('<L',ani_bytes[total_size+28:total_size+32])[0], # The value is expressed in 1/60th-of-a-second units, which are known as jiffie, ignored if seq exists
'bfAttributes' : struct.unpack('<L',ani_bytes[total_size+32:total_size+36])[0]
}
elif section == 'rate':
print("RIFF section: rate")
rate = []
for jiffie in range(0,chunk_size,4):
rate.append(struct.unpack('<L',ani_bytes[total_size+jiffie:total_size+jiffie+4])[0])
elif section == 'seq ':
print("RIFF section: seq")
seq = []
for sequence in range(0,chunk_size,4):
seq.append(struct.unpack('<L',ani_bytes[total_size+sequence:total_size+sequence+4])[0])
# bfAttributes: 1 == CUR or ICO, 0 == BMP, 3 == 'seq' block is present
elif section == 'LIST':
chunk_type = ani_bytes[total_size:total_size+4].decode()
LIST_item_size = total_size + 4
print("RIFF section: {}, size: {} Total chunk size: {}".format(chunk_type, LIST_item_size, chunk_size))
if chunk_type == 'INFO':
INFO = {}
while LIST_item_size < chunk_size:
info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode()
list_chunk_size = struct.unpack('<L',ani_bytes[LIST_item_size+4:LIST_item_size+8])[0]
INFO[info_section] = ani_bytes[LIST_item_size+8:LIST_item_size+8+list_chunk_size].decode()
if (list_chunk_size % 2) != 0: # Yay DWORD boundaries
list_chunk_size = list_chunk_size + 1
LIST_item_size = LIST_item_size + list_chunk_size + 8
elif chunk_type == 'fram':
info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode()
while LIST_item_size < chunk_size:
info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode()
list_chunk_size = struct.unpack('<L',ani_bytes[LIST_item_size+4:LIST_item_size+8])[0]
if info_section == 'icon':
icon.append({
'index' : icon_count,
#ICONDIR
'rtIconDir' : {
'res' : struct.unpack('<H',ani_bytes[LIST_item_size+8:LIST_item_size+10])[0],
'ico_type' : struct.unpack('<H',ani_bytes[LIST_item_size+10:LIST_item_size+12])[0],
'ico_num_images' : struct.unpack('<H',ani_bytes[LIST_item_size+12:LIST_item_size+14])[0]
},
#ICONDIRENTRY
'rtIconDirEntry' : {
'bWidth' : ani_bytes[LIST_item_size+14], # Width, in pixels, of the image
'bHeight' : ani_bytes[LIST_item_size+15], # Height, in pixels, of the image
'bColorCount' : ani_bytes[LIST_item_size+16], # Number of colors in image (0 if >=8bpp)
'bReserved' : ani_bytes[LIST_item_size+17], # Reserved
'wPlanes' : struct.unpack('<H',ani_bytes[LIST_item_size+18:LIST_item_size+20])[0], # Color Planes (or hotspot X coords for cur)
'wBitCount' : struct.unpack('<H',ani_bytes[LIST_item_size+20:LIST_item_size+22])[0], # Bits per pixel
'dwBytesInRes' : struct.unpack('<L',ani_bytes[LIST_item_size+22:LIST_item_size+26])[0], # how many bytes in this resource?
'dwDIBOffset' : struct.unpack('<H',ani_bytes[LIST_item_size+26:LIST_item_size+28])[0] # RT_ICON rnID
},
'ico_file' : ani_bytes[LIST_item_size+8:LIST_item_size + list_chunk_size + 8]
})
icon_count += 1
#print(info_section, hex(LIST_item_size), list_chunk_size)
if (list_chunk_size % 2) != 0: # Yay DWORD boundaries
list_chunk_size = list_chunk_size + 1
LIST_item_size = LIST_item_size + list_chunk_size + 8
total_size = total_size + chunk_size # The 8 accounts for the chunk id and size which is not included in the size
else:
print("No RIFF ID or Form is not ACON, is {} an ANI file?".format(file_name))
print("RIFF ID: {}, Form: {}".format(ckID, ckForm))
cursor = {
'INFO' : INFO,
'anih' : anih,
'seq' : seq,
'rate' : rate,
'icon' : icon
}
return cursor
print("ANI File: {}".format(sys.argv[1]))
pprint(extract_ani(sys.argv[1]))
icons = extract_ani(sys.argv[1])['icon']
for i in icons:
f = open(sys.argv[1]+"_"+str(i['index']),"wb")
f.write(i['ico_file'])
f.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment