一. 起源

为什么想着做这样的一个工具呢?

事情是这样的, 在游戏后期的优化过程中, 发现特效在资源的总量中占比很大, 所以打算压缩下特效的尺寸. 我们特效是由策划用 TexturePacker 打成 plist + png的形式, 因为项目经手了好多人, 所以好多特效的源文件都已丢失. 因此想到做一个这样的脚本来实现这个功能.

首先想到的是 @偶尔e网事 同学的Anti_TexturePacker, 非常好用, 我经常推荐给有需要的朋友. 不过目前只能运行在 Windows 上, 而且不支持命令行, 我们的特效有好几百个, 一个一个来的话会死人的哦~

下来找到的是这个, texture_unpacker_scirpt, 一看还不错, 简单测试了下集成到了开发环境中, 不过后来发现部分 trim+rotate 过的 frame 解析是不正确的.

一种造轮子的冲动涌上心头.

二. 分析

试着分析下这个项目, 主要分为这几点:

1.plist 文件的解析和分析.

plist 文件其实就是 xml 文件, 可以用 xml.etree.ElementTree 来解析, 恰好之前做过一个 ccb2lua 的项目, 搞起来也是轻车熟路.

plsit 中关键的一个结构是:

1
2
3
4
5
6
7
8
9
10
11
12
13
<key>oooo_001.png</key>
<dict>
<key>frame</key>
<string>{{978,582},{38,40}}</string>
<key>offset</key>
<string>{15,42}</string>
<key>rotated</key>
<true/>
<key>sourceColorRect</key>
<string>{{76,18},{38,40}}</string>
<key>sourceSize</key>
<string>{160,160}</string>
</dict>
字段 含义
frame 在大图上的坐标和尺寸信息
offset trim 过的图片和原图之间的偏移
rotated 是否进行了旋转
sourceColorRect 原始图上的坐标和尺寸信息
sourceSize 原始图片尺寸

我做了一张图来解释这些参数:

这里要注意:

  1. 一张图片是否旋转, 都会保存相同的frame.w,frame.h 这点比较坑.
  2. 如果没有裁剪, sourceColorRectx,y就是0, w,hsourceSize 一致.

2. 图像的切取和保存.

图像 python 的还是选用了 PIL 来搞, 用到的接口也不多:

Image.open(file) ⇒ image

打开图像

Image.new(mode, size) ⇒ image

创建一张图像

im.crop(box) ⇒ image

截图图像一个区域

im.paste(image, box)

将一张图像粘贴到一个区域

三. 实现

这里我大概讲一下实现, 具体大家可以去看源码.

1.plist 读取

cocos2d-x 的这个 plist 的格式是比较奇怪的 xml, 每一个 key 的 value 都是在下一行:

1
2
3
4
5
6
<key>format</key>
<integer>2</integer>
<key>realTextureFileName</key>
<string>oooo.png</string>
<key>size</key>
<string>{1024,1024}</string>

所以比较方便的实现是一次获取两行, 如下:

1
2
3
4
5
6
7
8
9
data = {}
iterator = iter(_element)
while True:
try:
key = iterator.next()
value = iterator.next()
data[key.text] = parseElement(value.tag, value)
except StopIteration:
break

2.图片的 trim+rotate

这个着实令人蛋疼, 因为旋转之后, frame 标签记录的宽高值并没有改变, 所以还得自己去判断下有没有旋转. trim+rotate 之后更是令人发指, 因为这样 sourceColorRect 的宽高,xy偏移都会互换. 这里若是思路不清晰, 极有可能被绕晕.

因为开始参考了texture_unpacker_scirpt的实现, 所以思路一直按照它的路线走, 代码中多次判断了是否旋转. 后来发现代码过于晦涩, 于是完全抛弃了它的实现.

最终核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src_image = Image.open(_imagefile)

for (name,config) in data["frames"].items():
# parse config
frame = parse.parse("{{{{{x:d},{y:d}}},{{{w:d},{h:d}}}}}",config["frame"])
sourceColorRect = parse.parse("{{{{{x:d},{y:d}}},{{{w:d},{h:d}}}}}",config["sourceColorRect"])
sourceSize = parse.parse("{{{w:d},{h:d}}",config["sourceSize"])
rotated = config["rotated"]

# create temp image
src_rect = (frame["x"],frame["y"],frame["x"]+(frame["h"] if rotated else frame["w"]),frame["y"]+(frame["w"] if rotated else frame["h"]))
temp_image = src_image.crop(src_rect)
if rotated:
temp_image = temp_image.rotate(90)

# create dst image
dst_image = Image.new('RGBA', (sourceSize["w"], sourceSize["h"]), (0,0,0,0))
dst_image.paste(temp_image, (sourceColorRect["x"],sourceColorRect["y"]), mask=0)
dst_image.save(outpath + "/" + name)

可以看到只有两处判断了旋转. 大家点这里可以看下texture_unpacker_scirpt的实现, 就会发现它的逻辑确实略显晦涩.

四.用法

用法已经更新, 大家到这里查看.

五.后记

项目现已开源至github, 地址https://github.com/justbilt/untp, 并提供了 windows 和 mac 的可执行文件, 位于 Release Page, 有问题欢迎评论或 issue 告知!

(–EOF–)