2
0

ani2cape.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import io
  2. import logging
  3. import time
  4. import uuid
  5. from PIL import Image
  6. logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s', level=logging.INFO)
  7. def scaleImage(img, scale):
  8. if img is None:
  9. return
  10. return img.resize((img.width * scale, img.height * scale))
  11. def readCUR(f, width=-1.0, height=-1.0):
  12. frameImage = Image.open(f, formats=['cur', 'ico'])
  13. if (frameImage.mode == 'P'):
  14. palette = list(frameImage.palette.getdata()[1])
  15. for i in range(4, len(palette), 4):
  16. if sum(palette[i:i + 3]) == 0:
  17. continue
  18. palette[i + 3] = 255
  19. frameImage.putpalette(palette, 'BGRA')
  20. frameImage = frameImage.convert('RGBA')
  21. if (width, height) == (-1.0, -1.0):
  22. return frameImage, (float(frameImage.width), float(frameImage.height))
  23. if -1 in (width, height):
  24. width = height / frameImage.height * frameImage.width if width == -1 else width
  25. height = width / frameImage.width * frameImage.height if height == -1 else height
  26. return frameImage.resize((int(width), int(height))), (width, height)
  27. def analyzeANI(f):
  28. if f.read(4) != b'RIFF':
  29. return {'code': -1, 'msg': 'File is not a ANI File!'}
  30. logging.debug('文件头检查完成!')
  31. fileSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
  32. # if os.path.getsize(filePath) != fileSize:
  33. # return {'code':-2,'msg':'File is damaged!'}
  34. logging.debug('文件长度检查完成!')
  35. if f.read(4) != b'ACON':
  36. return {'code': -1, 'msg': 'File is not a ANI File!'}
  37. logging.debug('魔数检查完成!')
  38. frameRate = (1/60)*1000
  39. readANIH = False
  40. while (True):
  41. chunkName = f.read(4)
  42. if chunkName == b'LIST' and readANIH:
  43. break
  44. if chunkName == b'anih':
  45. readANIH = True
  46. chunkSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
  47. if chunkName.lower() == b'rate':
  48. logging.debug('发现自定义速率!')
  49. frameRate = frameRate * int.from_bytes(f.read(4), byteorder='little', signed=False)
  50. logging.warning('发现自定义速率!由于GIF限制,将取第一帧与第二帧的速率作为整体速率!')
  51. f.read(chunkSize - 4)
  52. else:
  53. logging.debug('发现自定义Chunk!')
  54. f.read(chunkSize)
  55. listChunkSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
  56. if f.read(4) != b'fram':
  57. return {'code': -3, 'msg': 'File not a ANI File!(No Frames)'}
  58. logging.debug('frame头检查完成!')
  59. frameList = []
  60. nowSize = 4
  61. while (nowSize < listChunkSize):
  62. if f.read(4) != b'icon':
  63. return {'code': -4, 'msg': 'File not a ANI File!(Other Kind Frames)'}
  64. nowSize += 4
  65. subChunkSize = int.from_bytes(f.read(4), byteorder='little', signed=False)
  66. nowSize += 4
  67. frameList.append(f.read(subChunkSize))
  68. nowSize += subChunkSize
  69. return {'code': 0, 'msg': frameList, 'frameRate': frameRate}
  70. def main():
  71. from config import capeConfig
  72. uniqueId = (f'local.{capeConfig['Author'] or 'unknown'}'
  73. f'.{capeConfig['CapeName'] or 'untitled'}'
  74. f'.{time.time()}.{str(uuid.uuid4()).upper()}')
  75. capeData = {
  76. 'Author': capeConfig['Author'],
  77. 'CapeName': capeConfig['CapeName'],
  78. 'CapeVersion': capeConfig['CapeVersion'],
  79. 'Cloud': False,
  80. 'Cursors': {},
  81. 'HiDPI': capeConfig['HiDPI'],
  82. 'Identifier': capeConfig['Identifier'] or uniqueId,
  83. 'MinimumVersion': 2.0,
  84. 'Version': 2.0
  85. }
  86. for cursorType, cursorConfig in capeConfig['Cursors'].items():
  87. cursorSetting = {
  88. 'FrameCount': 1,
  89. 'FrameDuration': cursorConfig['FrameDuration'],
  90. 'HotSpotX': cursorConfig['HotSpot'][0] + 2.0,
  91. 'HotSpotY': cursorConfig['HotSpot'][1] + 2.0,
  92. 'Representations': []
  93. }
  94. hidpiRatio = 2 if capeConfig['HiDPI'] else 1
  95. width, height = cursorConfig.get('Size', (-1.0, -1.0))
  96. rotation = cursorConfig.pop('Rotation', 0)
  97. with open(cursorConfig['Path'], 'rb') as f:
  98. spriteSheet = None
  99. if (res := analyzeANI(f))['code'] == 0:
  100. logging.info('ANI文件分析完成,帧提取完成!')
  101. cursorSetting['FrameCount'] = len(res['msg'])
  102. for frameIndex in range(len(res['msg'])):
  103. b = io.BytesIO(res['msg'][frameIndex])
  104. frame, (width, height) = readCUR(b, width, height)
  105. frame = frame.rotate(rotation)
  106. position = (2, 2 + int((height + 4) * frameIndex))
  107. if frameIndex == 0:
  108. spriteSheet = Image.new('RGBA', (int(width + 4), int(height + 4) * len(res['msg'])))
  109. spriteSheet.paste(frame, position)
  110. else:
  111. logging.info('尝试作为CUR读入')
  112. frame, (width, height) = readCUR(f, width, height)
  113. frame = frame.rotate(rotation)
  114. spriteSheet = Image.new('RGBA', (int(width + 4), int(height + 4)))
  115. spriteSheet.paste(frame, (2, 2))
  116. logging.info(f'目标尺寸:{width}x{height}@{hidpiRatio}x')
  117. cursorSetting['PointsHigh'], cursorSetting['PointsWide'] = width + 4, height + 4
  118. for scale in (1, 2) if capeConfig['HiDPI'] else (1,):
  119. byteBuffer = io.BytesIO()
  120. scaleImage(spriteSheet, scale).save(byteBuffer, format='tiff', compression='tiff_lzw')
  121. cursorSetting['Representations'].append(byteBuffer.getvalue())
  122. capeData['Cursors'][cursorType] = cursorSetting
  123. from plistlib import dump, FMT_XML
  124. with open(f'{capeData['Identifier']}.cape', 'wb') as f:
  125. dump(capeData, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)
  126. if __name__ == '__main__':
  127. main()