|
@@ -1,56 +1,75 @@
|
|
-from config import capeConfig
|
|
|
|
-from plistlib import dumps,loads,dump,FMT_XML,load
|
|
|
|
-from PIL import Image
|
|
|
|
-import io,os,sys,base64
|
|
|
|
|
|
+import io
|
|
import logging
|
|
import logging
|
|
import time
|
|
import time
|
|
import uuid
|
|
import uuid
|
|
|
|
+from PIL import Image
|
|
|
|
|
|
-logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',level=logging.INFO)
|
|
|
|
|
|
+logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s', level=logging.INFO)
|
|
|
|
|
|
-def analyzeANIFile(filePath):
|
|
|
|
- with open(filePath,'rb') as 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}
|
|
|
|
|
|
|
|
-if __name__ == '__main__':
|
|
|
|
|
|
+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 = {
|
|
capeData = {
|
|
'Author': capeConfig['Author'],
|
|
'Author': capeConfig['Author'],
|
|
'CapeName': capeConfig['CapeName'],
|
|
'CapeName': capeConfig['CapeName'],
|
|
@@ -58,35 +77,48 @@ if __name__ == '__main__':
|
|
'Cloud': False,
|
|
'Cloud': False,
|
|
'Cursors': {},
|
|
'Cursors': {},
|
|
'HiDPI': capeConfig['HiDPI'],
|
|
'HiDPI': capeConfig['HiDPI'],
|
|
- 'Identifier': f"local.{capeConfig['Author']}.{capeConfig['CapeName']}.{time.time()}.{str(uuid.uuid4()).upper()}.{time.time()}",
|
|
|
|
|
|
+ 'Identifier': capeConfig['Identifier'] or uniqueId,
|
|
'MinimumVersion': 2.0,
|
|
'MinimumVersion': 2.0,
|
|
'Version': 2.0
|
|
'Version': 2.0
|
|
}
|
|
}
|
|
- for cursorType in capeConfig['Cursors'].keys():
|
|
|
|
|
|
+ for cursorType, cursorConfig in capeConfig['Cursors'].items():
|
|
cursorSetting = {
|
|
cursorSetting = {
|
|
'FrameCount': 1,
|
|
'FrameCount': 1,
|
|
- 'FrameDuration': capeConfig['Cursors'][cursorType]['FrameDuration'],
|
|
|
|
- 'HotSpotX': capeConfig['Cursors'][cursorType]['HotSpot'][0],
|
|
|
|
- 'HotSpotY': capeConfig['Cursors'][cursorType]['HotSpot'][1],
|
|
|
|
- 'PointsHigh': capeConfig['Cursors'][cursorType]['Size'][0],
|
|
|
|
- 'PointsWide': capeConfig['Cursors'][cursorType]['Size'][1],
|
|
|
|
|
|
+ 'FrameDuration': cursorConfig['FrameDuration'],
|
|
|
|
+ 'HotSpotX': cursorConfig['HotSpot'][0],
|
|
|
|
+ 'HotSpotY': cursorConfig['HotSpot'][1],
|
|
'Representations': []
|
|
'Representations': []
|
|
}
|
|
}
|
|
- res = analyzeANIFile(capeConfig['Cursors'][cursorType]['ANIPath'])
|
|
|
|
- if res["code"] == 0:
|
|
|
|
- logging.info('ANI文件分析完成,帧提取完成!')
|
|
|
|
- cursorSetting['FrameCount'] = len(res["msg"])
|
|
|
|
- spriteSheet = Image.new("RGBA", (int(cursorSetting['PointsHigh']), int(cursorSetting['PointsWide'] * len(res["msg"]))))
|
|
|
|
- for frameIndex in range(len(res["msg"])):
|
|
|
|
- frameImage = Image.open(io.BytesIO(res["msg"][frameIndex]),formats=['cur']).convert('RGBA')
|
|
|
|
- extracted_frame = frameImage.resize((int(cursorSetting['PointsHigh']), int(cursorSetting['PointsWide'])))
|
|
|
|
- position = (0, int(cursorSetting['PointsHigh'] * frameIndex))
|
|
|
|
- spriteSheet.paste(extracted_frame, position)
|
|
|
|
-
|
|
|
|
- byteBuffer = io.BytesIO()
|
|
|
|
- spriteSheet.save(byteBuffer,format="TIFF")
|
|
|
|
- cursorSetting['Representations'].append(byteBuffer.getvalue())
|
|
|
|
- capeData['Cursors'][cursorType] = cursorSetting
|
|
|
|
-
|
|
|
|
- with open(f"{capeData['Identifier']}.cape",'wb') as f:
|
|
|
|
|
|
+ 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 = (0, int(width * frameIndex))
|
|
|
|
+ if frameIndex == 0:
|
|
|
|
+ spriteSheet = Image.new('RGBA', (int(width), int(height) * len(res['msg'])))
|
|
|
|
+ spriteSheet.paste(frame, position)
|
|
|
|
+ else:
|
|
|
|
+ logging.info('尝试作为CUR读入')
|
|
|
|
+ spriteSheet, (width, height) = readCUR(f, width, height)
|
|
|
|
+ logging.info(f'目标尺寸:{width}x{height}@{hidpiRatio}x')
|
|
|
|
+ cursorSetting['PointsHigh'], cursorSetting['PointsWide'] = width, height
|
|
|
|
+ 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)
|
|
dump(capeData, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+if __name__ == '__main__':
|
|
|
|
+ main()
|