ANI2Cape/ani2cape.py
2025-05-04 18:38:41 +02:00

140 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import io
import logging
import time
import uuid
from PIL import Image
logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s', level=logging.INFO)
def scaleImage(img, scale):
if img is None:
return
return img.resize((img.width * scale, img.height * scale))
def readCUR(f, width=-1.0, height=-1.0):
frameImage = Image.open(f, formats=['cur', 'ico'])
if (frameImage.mode == 'P'):
palette = list(frameImage.palette.getdata()[1])
for i in range(4, len(palette), 4):
if sum(palette[i:i + 3]) == 0:
continue
palette[i + 3] = 255
frameImage.putpalette(palette, 'BGRA')
frameImage = frameImage.convert('RGBA')
if (width, height) == (-1.0, -1.0):
return frameImage, (float(frameImage.width), float(frameImage.height))
if -1 in (width, height):
width = height / frameImage.height * frameImage.width if width == -1 else width
height = width / frameImage.width * frameImage.height if height == -1 else height
return frameImage.resize((int(width), int(height))), (width, height)
def analyzeANI(f):
if f.read(4) != b'RIFF':
return {'code': -1, 'msg': 'File is not a ANI File!'}
logging.debug('文件头检查完成!')
fileSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
# if os.path.getsize(filePath) != fileSize:
# return {'code':-2,'msg':'File is damaged!'}
logging.debug('文件长度检查完成!')
if f.read(4) != b'ACON':
return {'code': -1, 'msg': 'File is not a ANI File!'}
logging.debug('魔数检查完成!')
frameRate = (1/60)*1000
readANIH = False
while (True):
chunkName = f.read(4)
if chunkName == b'LIST' and readANIH:
break
if chunkName == b'anih':
readANIH = True
chunkSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
if chunkName.lower() == b'rate':
logging.debug('发现自定义速率!')
frameRate = frameRate * int.from_bytes(f.read(4), byteorder='little', signed=False)
logging.warning('发现自定义速率由于GIF限制将取第一帧与第二帧的速率作为整体速率')
f.read(chunkSize - 4)
else:
logging.debug('发现自定义Chunk')
f.read(chunkSize)
listChunkSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
if f.read(4) != b'fram':
return {'code': -3, 'msg': 'File not a ANI File!(No Frames)'}
logging.debug('frame头检查完成')
frameList = []
nowSize = 4
while (nowSize < listChunkSize):
if f.read(4) != b'icon':
return {'code': -4, 'msg': 'File not a ANI File!(Other Kind Frames)'}
nowSize += 4
subChunkSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
nowSize += 4
frameList.append(f.read(subChunkSize))
nowSize += subChunkSize
return {'code': 0, 'msg': frameList, 'frameRate': frameRate}
def main():
from config import capeConfig
uniqueId = (f'local.{capeConfig['Author'] or 'unknown'}'
f'.{capeConfig['CapeName'] or 'untitled'}'
f'.{time.time()}.{str(uuid.uuid4()).upper()}')
capeData = {
'Author': capeConfig['Author'],
'CapeName': capeConfig['CapeName'],
'CapeVersion': capeConfig['CapeVersion'],
'Cloud': False,
'Cursors': {},
'HiDPI': capeConfig['HiDPI'],
'Identifier': capeConfig['Identifier'] or uniqueId,
'MinimumVersion': 2.0,
'Version': 2.0
}
for cursorType, cursorConfig in capeConfig['Cursors'].items():
cursorSetting = {
'FrameCount': 1,
'FrameDuration': cursorConfig['FrameDuration'],
'HotSpotX': cursorConfig['HotSpot'][0] + 2.0,
'HotSpotY': cursorConfig['HotSpot'][1] + 2.0,
'Representations': []
}
hidpiRatio = 2 if capeConfig['HiDPI'] else 1
width, height = cursorConfig.get('Size', (-1.0, -1.0))
rotation = cursorConfig.pop('Rotation', 0)
with open(cursorConfig['Path'], 'rb') as f:
spriteSheet = None
if (res := analyzeANI(f))['code'] == 0:
logging.info('ANI文件分析完成帧提取完成')
cursorSetting['FrameCount'] = len(res['msg'])
for frameIndex in range(len(res['msg'])):
b = io.BytesIO(res['msg'][frameIndex])
frame, (width, height) = readCUR(b, width, height)
frame = frame.rotate(rotation)
position = (2, 2 + int((height + 4) * frameIndex))
if frameIndex == 0:
spriteSheet = Image.new('RGBA', (int(width + 4), int(height + 4) * len(res['msg'])))
spriteSheet.paste(frame, position)
else:
logging.info('尝试作为CUR读入')
frame, (width, height) = readCUR(f, width, height)
frame = frame.rotate(rotation)
spriteSheet = Image.new('RGBA', (int(width + 4), int(height + 4)))
spriteSheet.paste(frame, (2, 2))
logging.info(f'目标尺寸:{width}x{height}@{hidpiRatio}x')
cursorSetting['PointsHigh'], cursorSetting['PointsWide'] = width + 4, height + 4
for scale in (1, 2) if capeConfig['HiDPI'] else (1,):
byteBuffer = io.BytesIO()
scaleImage(spriteSheet, scale).save(byteBuffer, format='tiff', compression='tiff_lzw')
cursorSetting['Representations'].append(byteBuffer.getvalue())
capeData['Cursors'][cursorType] = cursorSetting
from plistlib import dump, FMT_XML
with open(f'{capeData['Identifier']}.cape', 'wb') as f:
dump(capeData, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)
if __name__ == '__main__':
main()