mirror of
https://mikeslab.dix.asia/gogs/My-Mods/ANI2Cape.git
synced 2025-10-01 23:14:57 +00:00
140 lines
5.9 KiB
Python
140 lines
5.9 KiB
Python
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()
|