目录
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
4、结语
若是觉得博主的文章写的不错,不妨关注一下博主,点赞一下博文,另博主能力有限,若文中有出现什么错误的地方,欢迎各位评论指摘。
QQ交流群:806091680(Chinar)
该群为CSDN博主Chinar所创,推荐一下!我也在群里!
本文属于原创文章,转载请著名作者出处并置顶!!!!