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']).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 while (True): chunkName = f.read(4) if chunkName == b'LIST': break 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)) 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) 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) 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()