一个命令行的 TexturePacker 拆解工具 (一)

  1. 1. 一. 起源
  2. 2. 二. 分析
    1. 2.1. 1.plist 文件的解析和分析.
    2. 2.2. 2. 图像的切取和保存.
  • 三. 实现
    1. 0.1. 1.plist 读取
    2. 0.2. 2.图片的 trim+rotate
  • 四.用法
  • 五.后记
  • 一. 起源

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

    事情是这样的, 在游戏后期的优化过程中, 发现特效在资源的总量中占比很大, 所以打算压缩下特效的尺寸. 我们特效是由策划用 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–)