工具 - 分解TexturePacker导出的大图

 这是一个用于分解TexturePacker导出的大图的工具。

需要包含png图片和.plist文件

使用Python编写的,界面使用的是wxPython库写的。

以下是实现代码:

'''
功能:
分解TexturePacker制作的图集,要求包含plist文件和图片

1.制作一个简单的界面来指定要分解的图集(plist文件和图片)
    1.提供选择plist文件
    2.提供选择图片文件
    3.提供保存路径选择
2.根据选择的文件做分解操作
    1.读取plist文件,并解析
    2.根据解析出来的数据对图集进行裁剪,并生成新的图片
    3.将图片保存到指定文件夹中

参考:
1.wxpython教程1:https://blog.csdn.net/xufive/article/details/82665460?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166532024216782388061584%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166532024216782388061584&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~top_positive~default-1-82665460-null-null.nonecase&utm_term=wxpython&spm=1018.2226.3001.4450
2.wxpython教程2:https://edu.csdn.net/skill/python/python-3-176?category=9&typeId=17490
3.wxpython教程 - 事件:https://blog.csdn.net/weixin_42161954/article/details/109302843?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-109302843-blog-113688944.pc_relevant_3mothn_strategy_recovery&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-109302843-blog-113688944.pc_relevant_3mothn_strategy_recovery&utm_relevant_index=5
4.Python中的Partial函数(偏函数):https://edu.csdn.net/skill/python/python-3-176?category=9&typeId=17490#41__429
5.wxpython教程Wiki:https://wiki.wxpython.org/Passing%20Arguments%20to%20Callbacks
6.with open() 结构:https://blog.csdn.net/m0_48936146/article/details/124360734
7.plist文件解析plistlib:https://www.osgeo.cn/cpython/library/plistlib.html
8.python打包exe:https://blog.csdn.net/m0_64355682/article/details/125043126
8.python打包exe:https://www.cnblogs.com/strides/p/16422602.html
'''


from email.mime import image
from functools import partial
from PIL import Image
import wx
import os
import plistlib

APP_TITLE = "TexturePacker分解工具"
APP_ICON = ""
SPLITY_TYPE = "COCOS"

# 这里是窗口类
class MainFrame(wx.Frame):
    
    __plistPath = None
    __imgPath = None
    __savePath = None
    __saveDir = None

    
    # 分解类型,根据TexturePacker导出的引擎定义对于的函数, 注意这里的函数名不能加上前面下划线,否则无法调用
    __splitFuncDict = {
        'COCOS':'SplitImage_Cocos',
    }

    # 初始化窗口显示界面
    def __init__(self):
        '''
        初始化,这里使用默认的样式
        默认style是下列项的组合:wx.MINIMIZE_BOX | wx.MAXIMIZE_BOX | wx.RESIZE_BORDER | wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX | wx.CLIP_CHILDREN 
        但是不需要改变窗口的大小,所以加上 ^ wx.RESIZE_BORDER
        '''
        wx.Frame.__init__(self, None, style = wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)

        # 设置标题,背景色,大小,居中显示
        self.SetTitle(APP_TITLE)
        self.SetBackgroundColour(wx.Colour(224, 224, 224))
        self.SetSize((450, 220))
        self.Center()

        # 创建一个Panel,存放控件
        panel = wx.Panel(self)

        # plist文件获取
        # 固定文本显示:label:文本内容,pos:在panel内的位置(x, y)
        wx.StaticText(panel, label = "Plist文件:", pos = (20, 20))
        # 文本框,size:控件大小(w, h),-1表示默认大小
        self.plistTxtBox = wx.TextCtrl(panel, pos = (80, 20), size = (250, -1))
        # 按钮
        plistBtn = wx.Button(panel, label = "获取", pos = (340, 20))
        # 绑定点击事件,使用控件的Bind接口(事件类型,回调函数,对象),partial接口用于给回调事件中传入额外的参数,可以用于判断是哪个按钮的回调
        plistBtn.Bind(wx.EVT_BUTTON, partial(self.OnSelectBtnClick, bPlist = True))

        # 图片获取
        wx.StaticText(panel, label = "图片文件:", pos = (20, 60))
        self.imgTxtBox = wx.TextCtrl(panel, pos = (80, 60), size = (250, -1))
        imgBtn = wx.Button(panel, label = "获取", pos = (340, 60))
        imgBtn.Bind(wx.EVT_BUTTON, partial(self.OnSelectBtnClick, bPlist = False))

        # 分解保存路径
        wx.StaticText(panel, label = "保存地址:", pos = (20, 100))
        self.saveTxtBox = wx.TextCtrl(panel, pos = (80, 100), size = (250, -1))
        saveBtn = wx.Button(panel, label = "保存", pos = (340, 100))
        saveBtn.Bind(wx.EVT_BUTTON, self.OnSaveBtnClick, saveBtn)

        # 分解图片
        splitBtn = wx.Button(panel, label = "分解", pos = (50, 140))
        splitBtn.Bind(wx.EVT_BUTTON, self.OnSplitBtnClick, splitBtn)
        
    # 选择文件路径按钮点击事件,用于获取Plist、png文件路径
    def OnSelectBtnClick(self, evt, bPlist):
        fileWildCard = ""           # 筛选文件类型:描述|*.后缀|下一组...
        titleTips = ""              # 文件浏览器的标题

        if bPlist :
            fileWildCard = "Plist files(*.plist)|*.plist|All files(*.*)|*.*" 
            titleTips = "请选择plist文件"
        else:
            fileWildCard = "PNG files(*.png)|*.png|All files(*.*)|*.*" 
            titleTips = "请选择图片文件"

        # 通过 wx.FileDialog 打开文件浏览器,并选择对应的文件, defaultDir:起始路径
        fd = wx.FileDialog(self, titleTips, defaultDir = os.getcwd(), wildcard = fileWildCard) 
        
        # 用户确认后的操作,这一步必须写上,不然弹窗无法正常打开,如果用户取消则该值 = wx.ID_CANCEL
        if fd.ShowModal() == wx.ID_OK :
            filePath = fd.GetPath()
            os.chdir(os.path.dirname(filePath))
            if bPlist :
                self.__plistPath = filePath
                self.plistTxtBox.write(self.__plistPath)
            else:
                self.__imgPath = filePath
                self.imgTxtBox.write(self.__imgPath)            
            
        # 用完就关了
        fd.Destroy()
        
    # 保存路径按钮点击事件
    def OnSaveBtnClick(self, evt):
        fd = wx.DirDialog(self, "请选择保存文件夹", defaultPath = os.getcwd())
        if fd.ShowModal() == wx.ID_OK :
            self.__savePath = fd.GetPath()
            self.saveTxtBox.write(self.__savePath)
        fd.Destroy()

    # 分解按钮点击事件
    def OnSplitBtnClick(self, evt):
        if self.__plistPath == None or not os.path.exists(self.__plistPath) :
            wx.MessageBox("请指定有效的Plist文件!", "警告", style = wx.OK)
            return
        if self.__imgPath == None or not os.path.exists(self.__imgPath)  :
            wx.MessageBox("请指定有效的图片文件!", "警告", style = wx.OK)
            return
        if self.__savePath == None or not os.path.exists(self.__savePath)  :
            wx.MessageBox("请指定有效的保存路径!", "警告", style = wx.OK)
            return
        
        '''
            1.读取plist文件内容,并转换为python数据结构
            2.遍历碎图数据,对大图进行裁剪
            3.将小图生成到指定的路径
        '''
        self.__InitPlistData()

    # 初始化Plist文件数据
    def __InitPlistData(self):
        funcStr = self.__splitFuncDict.get(SPLITY_TYPE, 'Defuault')
        if funcStr == 'Defuault' :
            wx.MessageBox(f"没有{SPLITY_TYPE}对应的分解处理接口", "警告", style = wx.OK)
            return

        '''
        with open(文件路径,打开方式:r只读/w只写/a追加) as 文件对象
        r:	以只读方式打开文件。文件的指针将会放在文件的开头。这是**默认模式**。
        rb: 以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。
        r+: 打开一个文件用于读写。文件指针将会放在文件的开头。
        rb+:以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。
        w:	打开一个文件只用于写入。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。
        wb:	以二进制格式打开一个文件只用于写入。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。
        w+:	打开一个文件用于读写。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。
        wb+:以二进制格式打开一个文件用于读写。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。
        a:	打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
        ab:	以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
        a+:	打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。
        ab+:以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。
        '''
        with open(self.__plistPath, "rb") as fp:
            
            # 解析Plist文件
            plistDict = plistlib.load(fp)
            if plistDict == None :
                wx.MessageBox("Plist解析失败", "错误", style = wx.OK)
                return

            # 获取图片名称,并根据图片名称创建文件夹存储小图
            imgName = plistDict['metadata']['realTextureFileName']
            self.__saveDir = os.path.join(self.__savePath, imgName.replace(".png", "")) # 把后缀去掉,然后组合一下碎图的保存路径
            if not os.path.isdir(self.__saveDir) : # 判断有没有该目录,没有就创建
                os.mkdir(self.__saveDir) # 创建目录

            # 打开图片对象
            srcImage = Image.open(self.__imgPath)

            # 开始分解,getattr(self, xxx):相当于 self.xxx
            func = getattr(self, funcStr)
            func(plistDict['frames'], srcImage)

    # Cocos版本的分解接口
    def SplitImage_Cocos(self, imgsDic, srcImage):
        '''
            大图中的原点是在左上角,右为x正轴,下为y正轴
            frame:小图在大图中的位置大小(这里是经过Trim去掉空白之后的):{x,y},{w,h}
            offset:小图经过Trim之后的中心点与原小图的中心点的偏移值,注意这里的坐标系是:右为x正轴,上为y正轴
            rotated:是否旋转了90°
            sourceColorRect:原小图经过Trim之后在原图中的位置大小:{x,y},{w,h}
            sourceSize:原图大小
        '''
        # 将json格式的数据转换成数组
        json2List = lambda x : x.replace('{', '').replace('}', '').split(',')

        for key, value in imgsDic.items() :
            
            # 获取裁剪的位置大小:[x,y,w,h]
            cutRect = []
            valueFrame = json2List(value['frame'])
            frameList = [int(valueFrame[0]), int(valueFrame[1]), int(valueFrame[2]), int(valueFrame[3])]
            if value['rotated'] == True :
                cutRect = [frameList[0], frameList[1], frameList[0] + frameList[3], frameList[1] + frameList[2],]
            else:
                cutRect = [frameList[0], frameList[1], frameList[0] + frameList[2], frameList[1] + frameList[3],]

            # 原始的图片大小[w,h]
            valueSourceSize = json2List(value['sourceSize'])
            sourceSizeList = [int(valueSourceSize[0]), int(valueSourceSize[1])]
            
            # 获取偏移值: [x,y]
            '''
                首先大图里的小图是有可能对原图裁剪后的,所以如果要放回原图中的话,
                需要用(原图的大小 - 裁剪后的图的大小)/ 2,得到一个偏移值
                小图在原图的位置应该从原点(0,0),即左上角做一个偏移

                再加上中心点的偏移值就是最终的位置,注意这里因为坐标系问题,y轴应该是减法
                宽高 = 位置 + 大小
            '''
            valueOffset = json2List(value['offset'])
            offsetList = [int(valueOffset[0]), int(valueOffset[1])]
            adjust_x = int((sourceSizeList[0] - frameList[2]) / 2)
            adjust_y = int((sourceSizeList[1] - frameList[3]) / 2)
            order_x = adjust_x + offsetList[0]
            order_y = adjust_y - offsetList[1]
            orderRect = [order_x, order_y, order_x + frameList[2], order_y + frameList[3],]
            
            # 存储位置
            subImgPath = ""
            if key.endswith('.png') :
                subImgPath = os.path.join(self.__saveDir, key)
            else:
                subImgPath = os.path.join(self.__saveDir, key + ".png")

            self.GenerateImage(srcImage, cutRect, orderRect, subImgPath)

    # 创建小图并保存
    def GenerateImage(self, srcImage, cutRect, orderRect, orderPath):
        subImage = srcImage.crop(cutRect)
        orderImage = Image.new("RGBA", [orderRect[2], orderRect[3]])
        orderImage.paste(subImage, orderRect)
        orderImage.save(orderPath)

# 这里是程序类
class MainApp(wx.App):
    def OnInit(self):
        self.SetAppName(APP_TITLE)
        self.frame = MainFrame()
        self.frame.Show()
        return True

# 启动应用程序
if __name__ == "__main__":
    app = MainApp()
    app.MainLoop()

本人新学Python,有什么不对的地方,欢迎各位大佬指正~~ 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值