利用Python还原TexturePacker或者cocos studio创建好的pList图集,考虑旋转和裁切造成的偏移

目录

1、博客介绍

2、内容

运行环境:python2.7.X.X

分析原理:

解析过程:

1、分析pList

2、python解析XML

3、读取大图

4、散图数据字典内的数据转换

5、处理转化后的数据

6、裁剪图片

7、旋转图片

8、创建原始大小的图片容器

9、计算小图在大图中的位置

10、将小图复制到图片容器内并保存

完整代码:

3、推送

4、结语


 

1、博客介绍

最近运营那里有一些需求,需要把外包给的一些pList图集分割成原本的小图,百度了一下网上有很多相关的写法,看了看发现都没有考虑到图集创建时可能出现的裁切情况,所以没办法就只能用python来写一个小脚本去裁剪图集了,之前写过一个用python去裁切json图集的博文,本篇和上一篇原理一模一样,这次详细介绍一下,具体效果如下gif所示。


2、内容

 

运行环境:python2.7.X.X


分析原理:

情景:一套动画散图,所有图片整体的宽高都相同,每张图片在打成图集的时候裁剪掉了周遭的透明像素,为了节省空间,有的图片进行了旋转

需求:将整图中所有的散图切下来,并恢复成打成图集前的散图样式,宽高和旋转都恢复

 

步骤思路:

1、获取图中散图相对于大图的位置

2、在相应的位置裁剪出对应小图宽高的图片数据

3、若有旋转则恢复

4、根据初始图片的宽高新建一个等大的图片容器

5、将裁剪好的图片数据复制在容器内

6、根据偏移量设置图片在容器内的位置

7、根据图片名保存在输出路径


解析过程:

1、分析pList

我们首先分析一下pList文件,我们可以从pList文件当中获取到以下的有效信息:

 

bonus_1.png : 散图的命名

frame:散图对应大图的位置{{324,235},{65,60}},表中数据解析意为在大图纹理坐标x=324,y=235的位置截取宽65,高60的图片数据

rotated:是否旋转

sourceSize:原始图片的宽高

offset:裁剪后的小图在容器内距离中心点的偏移

 

以上我们已经获取到足够的数据了,可以开始裁剪图片了

2、python解析XML

# 导入XML解析库
from xml.etree import ElementTree
# 将XML转换为一个字典
def tree_to_dict(tree):
    d = {}
    for index, item in enumerate(tree):
        if item.tag == 'key':
            if tree[index+1].tag == 'string':
                d[item.text] = tree[index + 1].text
            elif tree[index + 1].tag == 'true':
                d[item.text] = True
            elif tree[index + 1].tag == 'false':
                d[item.text] = False
            elif tree[index+1].tag == 'dict':
                d[item.text] = tree_to_dict(tree[index+1])
    return d

# 读取pList文件
plist_filename = os.path.join(CURRENT_PATH, json_name+".plist")
root = ElementTree.fromstring(open(plist_filename, 'r').read())
# 转化后的字典
plist_dict = tree_to_dict(root[0])

如上述代码所示:

1、我们首先导入XML的解析库

2、创建一个根据XML字段来填充字典的方法

3、读取pList文件

4、利用方法来获取一个包含pList数据的字典

5、字典中每一条数据就是每一张散图的数据,字典数据如下图所示

3、读取大图

image = Image.open(os.path.join(CURRENT_PATH, json_name+".png"))

我们根据预先设置好的全路径和图片名字来读取大纹理

 

4、散图数据字典内的数据转换

to_list = lambda x: x.replace('{','').replace('}','').split(',') 

for k,v in plist_dict['frames'].items():

        spriteFrameStr  = to_list(v['frame'])
        spriteOffsetStr = to_list(v['offset'])
        spriteSizeStr   = to_list(v['sourceSize'])
        spriteRotStr    = v['rotated']

      

1、创建一个替换括号的lambda表达式,目的是将字典中数据转化为数组

2、遍历散图数据的字典

3、将散图的关键数据转换为数组,方便取用

 

5、处理转化后的数据

 for k,v in plist_dict['frames'].items():
        spriteFrameStr  = to_list(v['frame'])
        spriteOffsetStr = to_list(v['offset'])
        spriteSizeStr   = to_list(v['sourceSize'])
        spriteRotStr    = v['rotated']

        framePos = {"x":(int)(spriteFrameStr[0]),"y":(int)(spriteFrameStr[1]),"w":(int)(spriteFrameStr[2]),"h":(int)(spriteFrameStr[3])}
        offsetSize = {"x":(int)(spriteOffsetStr[0]),"y":(int)(spriteOffsetStr[1])}
        sourceSize = {"w":(int)(spriteSizeStr[0]),"h":(int)(spriteSizeStr[1])}

1、获取小图在大图中的位置 ——framePos

2、获取小图在原始图片容器中心点的偏移 ——offserSize

3、获取原始图片容器的宽高 ——sourceSize

4、将所有数据转换为int整数值

 

6、裁剪图片

# 裁切的位置
src_rect_l = [framePos["x"], framePos["y"], framePos["x"] + framePos["w"], framePos["y"] + framePos["h"]]

# 如果有旋转 则裁剪的宽高互换
if spriteRotStr:
    src_rect_l = [framePos["x"], framePos["y"], framePos["x"] + framePos["h"], framePos["y"] + framePos["w"]]

# 裁切小图
src_crop = image.crop(src_rect_l)

 

1、我们从framePos中整理出裁切信息

2、从大图纹理中裁切出小图的数据,这里用的是PIL库中Image模块的crop方法

 

7、旋转图片

if spriteRotStr:
    src_crop = src_crop.rotate(90, expand = 1)

1、如果有旋转则恢复到原始角度

2、expand参数设置为true,使得旋转后的图片尺寸自适应

 

8、创建原始大小的图片容器

dst_img = Image.new("RGBA", [sourceSize ["w"],sourceSize ["h"]])

1、根据原始图片的宽高数据sourceSize创建一个图片容器

 

9、计算小图在大图中的位置

adjust_w = (sourceSize["w"] - framePos["w"])/2
adjust_h = (sourceSize["h"] - framePos["h"])/2

dst_x = adjust_w + offsetSize["x"]
dst_y = adjust_h - offsetSize["y"] 

dst_w = dst_x + src_rect["w"]
dst_h = dst_y + src_rect["h"]
# 从裁切位置复制的位置
dst_rect_l = [dst_x,dst_y,dst_w,dst_h]

注:这里比较复杂难懂,博主在这里以举例子的方式来介绍,多看几遍最好

 

1、假设我们原始的图片宽高为(98,89),以下是我们用python创建的一个图片容器,用来承接散图

2、假设我们切割下来的散图宽高为(65,60)(因为合图时剔除了透明像素)。

3、假设将小图复制到容器的坐标数据为[0,0,65,60],则输出结果如下所示:

 

注:python合图的坐标原点在左上角,即(0,0)

我们在这里解析一下坐标数据的含义

第一个参数:0        距离坐标原点x的距离

第二个参数:0        距离坐标原点y的距离

第三个参数:65      第三个参数 - 第一个参数 必须等于小图的宽      (如果第一个参数=10  则第三个参数=75)

第四个参数:60      第四个参数 - 第二个参数 必须等于小图的高      (如果第二个参数=20  则第四个参数=80)

如果坐标数据为[50,50,115,110],则输出的图片显示如下

 

4、所以我们首先要算出小图放到中心点的一个坐标数据

即    (大图的宽 - 小图的宽)/  2   =  x应该移动的距离

       (大图的高 - 小图的高)/  2   =  y应该移动的距离

adjust_w = (sourceSize["w"] - framePos["w"])/2
adjust_h = (sourceSize["h"] - framePos["h"])/2

5、然后我们需要补上偏移才可以获取最终的,xy距离

dst_x = adjust_w + offsetSize["x"]
dst_y = adjust_h - offsetSize["y"] 

注:这里x轴的偏移需要相加,y轴需要减去,博主不太清楚这里符号不同的原因,这里是试出来了,试了好久,就感觉挺奇怪的

 

6、坐标数据的后两个参数

adjust_w = (sourceSize["w"] - framePos["w"])/2
adjust_h = (sourceSize["h"] - framePos["h"])/2

dst_x = adjust_w + offsetSize["x"]
dst_y = adjust_h - offsetSize["y"] 

dst_w = dst_x + framePos["w"]
dst_h = dst_y + framePos["h"]

dst_rect_l = [dst_x,dst_y,dst_w,dst_h]

坐标数据的后两个参数只需要加上小图的对应宽高就可以了 ,坐标数据就出来了

 

10、将小图复制到图片容器内并保存

dst_img.paste(src_crop, dst_rect_l)
dst_img.save(dst_path)

 最后我们只需要将裁切到的小图数据src_crop根据坐标数据dst_rect_l复制到容器dst_img内,保存即可


完整代码:

 

以下为完整代码

注:

1、所有整图裁切的原理都是相同的

2、不同的pList内的参数定义可能不一样

3、你只需要尝试找到以下三个数据即可

       .获取小图在大图中的位置 ——framePos

      .获取小图在原始图片容器中心点的偏移 ——offserSize

      .获取原始图片容器的宽高 ——sourceSize

# -*- coding: utf-8 -*-
import json
import os
from PIL import Image
import shutil
import os,sys
from xml.etree import ElementTree

CURRENT_PATH = os.getcwd()
SPLIT_OUT_PATH = os.path.join(CURRENT_PATH, "cut1")


# 输出文件夹
if os.path.exists(SPLIT_OUT_PATH):
    shutil.rmtree(SPLIT_OUT_PATH)
os.mkdir(SPLIT_OUT_PATH)

# 获取所有.json文件
cur_all_files = os.listdir(CURRENT_PATH)
json_names = []
for cmp_name in cur_all_files:
    if os.path.isdir(os.path.join(CURRENT_PATH, cmp_name)):
        continue
    name, ext = os.path.splitext(cmp_name)
    if ext.lower() != ".plist":
        continue
    json_names += [name]


def generate_texture(src_img, src_rect, offset_size, dst_size, dst_path, rotated):
    # 裁切的位置
    src_rect_l = [src_rect["x"], src_rect["y"], src_rect["x"] + src_rect["w"], src_rect["y"] + src_rect["h"]]

    adjust_w = (dst_size["w"] - src_rect["w"])/2
    adjust_h = (dst_size["h"] - src_rect["h"])/2

    dst_x = adjust_w + offset_size["x"]
    dst_y = adjust_h - offset_size["y"] 

    dst_w = dst_x + src_rect["w"]
    dst_h = dst_y + src_rect["h"]
    # 从裁切位置复制的位置
    dst_rect_l = [dst_x,dst_y,dst_w,dst_h]

    if rotated:
        src_rect_l = [src_rect["x"], src_rect["y"], src_rect["x"] + src_rect["h"], src_rect["y"] + src_rect["w"]]

    src_crop = src_img.crop(src_rect_l)
    if rotated:
        src_crop = src_crop.rotate(90, expand = 1)

    dst_img = Image.new("RGBA", [dst_size["w"],dst_size["h"]])
   
    dst_img.paste(src_crop, dst_rect_l)
    dst_img.save(dst_path)


def tree_to_dict(tree):
    d = {}
    for index, item in enumerate(tree):
        if item.tag == 'key':
            if tree[index+1].tag == 'string':
                d[item.text] = tree[index + 1].text
            elif tree[index + 1].tag == 'true':
                d[item.text] = True
            elif tree[index + 1].tag == 'false':
                d[item.text] = False
            elif tree[index+1].tag == 'dict':
                d[item.text] = tree_to_dict(tree[index+1])
    return d

def split_json(json_name):
    plist_filename = os.path.join(CURRENT_PATH, json_name+".plist")
    root = ElementTree.fromstring(open(plist_filename, 'r').read())
    plist_dict = tree_to_dict(root[0])

    print(plist_dict)

    to_list = lambda x: x.replace('{','').replace('}','').split(',')
   

    image = Image.open(os.path.join(CURRENT_PATH, json_name+".png"))
    image = image.resize((image.width, image.height),Image.ANTIALIAS)

    if not image:
        return
    if os.path.exists(os.path.join(SPLIT_OUT_PATH, json_name)):
        shutil.rmtree(os.path.join(SPLIT_OUT_PATH, json_name))
    os.mkdir(os.path.join(SPLIT_OUT_PATH, json_name))


    for k,v in plist_dict['frames'].items():
        dst_path = os.path.join(SPLIT_OUT_PATH, json_name + "\\" + k)

        spriteFrameStr  = to_list(v['frame'])
        spriteOffsetStr = to_list(v['offset'])
        spriteSizeStr   = to_list(v['sourceSize'])
        spriteRotStr    = v['rotated']

        framePos = {"x":(int)(spriteFrameStr[0]),"y":(int)(spriteFrameStr[1]),"w":(int)(spriteFrameStr[2]),"h":(int)(spriteFrameStr[3])}
        offsetSize = {"x":(int)(spriteOffsetStr[0]),"y":(int)(spriteOffsetStr[1])}
        sourceSize = {"w":(int)(spriteSizeStr[0]),"h":(int)(spriteSizeStr[1])}

        generate_texture(image, framePos, offsetSize, sourceSize, dst_path, spriteRotStr)
    image.close()

        
for json_name in json_names:
    print("##############:cut the "+ pList_name+":##############")
    with open(os.path.join(CURRENT_PATH, json_name+".plist"), "r") as json_file:
        if not os.path.exists(json_name+".png"):
            continue
        split_json(json_name)

 


3、推送

Github:https://github.com/KingSun5

本篇源码和资源例子:https://github.com/KingSun5/CutPList-byPython

 


4、结语

若是觉得博主的文章写的不错,不妨关注一下博主,点赞一下博文,另博主能力有限,若文中有出现什么错误的地方,欢迎各位评论指摘。

       QQ交流群:806091680(Chinar)

       该群为CSDN博主Chinar所创,推荐一下!我也在群里!

       本文属于原创文章,转载请著名作者出处并置顶!!!!

 

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值