news 2026/9/7 7:15:43

图集拆图工具解析:从plist/json到碎图的完整实现指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
图集拆图工具解析:从plist/json到碎图的完整实现指南

简介:这款用 Go 语言编写的拆图工具,面向游戏客户端开发、资源优化以及需要处理 TexturePacker 合图的开发者。它能读取 plist、json、fnt 位图字体及 Spine 的 atlas 配置文件,将打包后的子图精准拆分并还原原始尺寸,从而满足发布包中贴图资源的调试、恢复和二次编辑需求。资源包共包含 11 个文件,核心是 5 个 Go 源码文件,对应 plist、json、fnt、atlas 各格式解析器及命令行主程序,另有 README、开源许可证和预览图等说明,整体仅 59KB,非常适合快速阅读和移植。工具通过简单的参数指定文件或目录即可批量导出,还支持 --ext 按后缀筛选,且具备跨平台特性,可运行在 Windows、Mac 与 Linux 系统。目前已有 750 人学习下载,这份小巧的源码完整展示了从合图配置读取到图片还原的实现思路,对希望理解拆图原理或构建类似 Go 工具链的开发者很有参考价值。

1. 为什么需要拆图工具:从图集到碎图的最后一公里

做游戏客户端或者搞 UI 素材的同行应该都有体会:美术从 TexturePacker 里打包好的图集,最后交付到程序手里只是半成品。真正落到代码里用的,是一张张小碎图——按钮的背景、角色的某帧动作、位图字体的某个字符。图集文件固然省内存、省 DrawCall,但如果你要单独改某一个小图标,或者要按图集坐标把原图还原出来供给其他环节使用,没有好用的拆图工具,就只能在 PS 里对着 plist 里的坐标手动抠,一张两张还行,遇到成百上千帧的序列帧直接崩溃。

PlistDumper 就是干这事的:它能把 TexturePacker 导出的 plist、json、fnt、atlas 这几种常见图集描述文件解析出来,再把大图里对应的子图按原始尺寸和位置切出来,自动命名导出。简单说,就是图集的“逆向工程”。

我最早接触它是因为接到一个老项目,美术资源全在 atlas 里,但需要把其中某个角色的技能特效单独拆出来做换皮。手动画了一个下午,眼睛都快瞎了,后来发现这类工具其实原理并不复杂,核心就是读懂图集描述文件的数据结构,再做好像素级的坐标还原。这篇文章就结合我自己的折腾经验,把 PlistDumper 这类工具的拆解思路、实现细节和实战中最容易踩的坑一次说清楚。

适合谁看:游戏客户端开发、UI 特效美术、TA(技术美术),或者说所有需要频繁处理图集资源的同行。哪怕你之前完全没写过解析代码,照着下面的思路也能用现成工具把拆图流程跑通。

2. 先搞懂图集描述文件:plist、json、fnt、atlas 到底存了啥

想拆图,先得知道要拆的文件里面是什么结构。这几种格式用大白话说,就是“一张大图 + 一份说明书”。说明书里记录了每个小图在大图中的哪块位置、原始尺寸是多少、是否旋转过、有没有裁掉透明边。下面一个一个看。

2.1 plist 格式:TexturePacker 默认输出的老牌格式

plist 本质是苹果的 XML 属性列表文件,大致长这样:

<dict> <key>frames</key> <dict> <key>icon_001.png</key> <dict> <key>frame</key> <string>{{0,0},{64,64}}</string> <key>offset</key> <string>{0,0}</string> <key>rotated</key> <false/> <key>sourceColorRect</key> <string>{{0,0},{64,64}}</string> <key>sourceSize</key> <string>{64,64}</string> </dict> </dict> </dict>

我不推荐直接人肉去读这个文件去算坐标,但你需要理解每个字段的含义,不然程序写出来,遇到特殊参数配置就抓瞎:

  • frame:小图在大图中的实际裁切区域,格式是{{x,y},{w,h}},这里的 x、y 是大图坐标系中的左上角起点。
  • offset:裁切后小图中心点相对于原图中心点的偏移。如果美术导出时没有做透明的 trim(裁掉透明边),offset 就是{0,0};一旦裁过透明边,offset 的值就需要用来做位置补偿。
  • rotated:是否旋转 90 度。TexturePacker 为了尽量提高大图的打包率,会把某些小图旋转 90 度放进图集,取出来的时候必须逆旋转回来。
  • sourceColorRect:小图在原始图片中实际有内容的区域(也就是没被裁掉的可见像素范围)。
  • sourceSize:小图原始文件的完整尺寸,包含透明部分。

提示:mac 上常见的 “plist parsing error” 这类报错,多数并不是文件内容坏了,而是文件带 BOM、编码不是 UTF-8,或者plutil -lint校验时因为证书、签名问题导致读取异常。拆图工具如果读不了别人给的 plist,先拿plutil -lint检查语法,比盲目改代码高效得多。

2.2 json 格式:Cocos、Egret、Laya 项目的常客

json 图集描述文件现在用得越来越多,因为 Cocos Creator 导出的合图就是 JSON 格式。它的结构大体是:

{ "frames": { "icon_001.png": { "frame": {"x": 0, "y": 0, "w": 64, "h": 64}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x": 3, "y": 5, "w": 60, "h": 58}, "sourceSize": {"w": 64, "h": 64} } } }

注意 json 格式里,frame的坐标是用四个独立字段表示的,没有 plist 里那层花括号包装。另外 Cocos 的 json 里还有一个trimmed字段,表示是否裁剪过透明区域。如果trimmed为 false,那spriteSourceSize基本可以忽略;如果为 true,要还原原始图片,就比 plist 要稍微绕一点——后面讲还原计算时我会细说。

json 格式拆图的实际坑主要在编码和非法字符上。很多项目从美术那边拿到的 json 是 Windows 下 Excel 改过的,或者写入的时候带了 BOM。Python 的json.load直接读这种文件会报invalid JSON,你先用文本编辑器把文件另存为 UTF-8 无 BOM,往往就能解决一大半问题。

2.3 fnt 格式:位图字体的字符映射表

fnt 主要用于位图字体。TexturePacker 导出的位图字体通常包含一个.fnt文件加一张或多张 PNG 图。fnt 有文本格式和二进制格式,TextPacker 默认导出的是文本格式,结构类似:

info face="Arial" size=32 bold=0 common lineHeight=38 base=26 scaleW=512 scaleH=512 pages=1 page id=0 file="font_0.png" chars count=95 char id=32 x=0 y=0 width=0 height=0 xoffset=0 yoffset=19 xadvance=18 page=0 chnl=0

拆 fnt 的关键不在切图,因为char已经告诉你每个字符在字体大图中的坐标和宽高,你按坐标把字符裁出来就行。难点在于位图字体由多个文件组成(.fnt + 若干页 PNG),而且page字段会指示字符属于第几张贴图。如果你的工具只处理单页字体,遇到pages=2以上就会漏字符。

经验提醒:如果你只是为了给聊天系统做动态表情,或者把位图字体转回 TTF 风格的小图,切记要保留xadvance(字符间距)信息,否则重新排列时文字会挤成一团。

2.4 atlas 格式:LibGDX 系的紧凑文本格式

atlas 是文本格式,结构上和前面几种完全不同,没有层级包裹,直接平铺键值对:

char.png size: 512, 512 format: RGBA8888 filter: Linear, Linear repeat: none player_walk_01 rotate: false xy: 10, 12 size: 64, 64 orig: 64, 64 offset: 0, 0 index: -1

这里的xy是小图左上角坐标,size是裁切后的尺寸,orig是原始含透明区域的尺寸,offset是中心偏移,index一般用于序列帧命名(比如同一名字的player_walk_01.png用 index 区分),值为 -1 表示无序列帧索引。

atlas 和 plist 一样,核心坑点是origoffset的组合。很多刚上手的同学只读xysize,导出的图片永远多一圈透明边或者少一段像素,原因就是没有处理origoffset的还原算法。

3. 工具设计思路:解析、还原、导出三板斧

理解了格式,PlistDumper 这类工具的整体设计就清晰了。不管用什么语言实现,核心流程都逃不开“解析 → 还原像素 → 按规则导出”这三步。下面把这几个环节逐个拆开讲。

3.1 不同格式的差异如何屏蔽掉

实际开发里,老项目用 plist,新项目用 json,特殊需求还会碰 atlas 和 fnt。所以工具的第一步必须做一个“格式识别 + 统一数据结构”。

我最开始写的版本是四个格式各写一套解析逻辑,结果代码重复得厉害,后面重构才发现:与其解析完直接切图,不如先转换成统一的中间模型:

class SpriteItem: name: str # 子图名称 page: int # 所属图集页(fnt/atlas 有多页) region: tuple # 在大图中的区域 (x, y, w, h) rotated: bool # 是否需要旋转还原 source_size: tuple # 原始完整尺寸 (w, h) offset: tuple # 中心点偏移 trimmed: bool # 是否被裁掉过透明边

解析 plist 时遇到frame={{x,y},{w,h}},就把它拆成region;解析 json 时直接读frame.x等字段;解析 atlas 时依次按缩进读取块。最终都塞进同一个SpriteItem。这样切图函数只需要写一遍,后续加新格式也只是加个解析器的事。

这个屏蔽层带来的收益是巨大的:排错的时候不用四份代码分别调试,出图的逻辑只在同一个函数里。建议所有做这类工具的同学都先花半小时把中间数据结构定义好,别上来就直接写“plist 切图函数”“json 切图函数”。

3.2 透明裁切还原:offset 与 sourceColorRect 的计算逻辑

这是拆图工具里最容易算错的一环。先说 plist。sourceColorRect记录的是原图中可见像素的区域,offset记录的是裁切后区域中心相对于原图中心的偏移。还原原图的算法是:

  1. sourceColorRect的 x、y 反向计算贴片位置:在新建的sourceSize大小的透明画布上,把sourceColorRect中的可见区域放到对应坐标。
  2. 从大图中切出frame区域对应的像素。
  3. 如果rotated为 true,先把像素逆时针旋转 90 度(也可按工具导出的旋转方向调整)。
  4. 将切出的像素贴到可见区域位置。

简单地说,从大图截出来的像素块,面积会比原始图片小(因为透明边被裁掉了),所以不能直接保存为 PNG。必须创建一个原始尺寸的透明画布,然后把像素块平移到偏移补偿后的位置。plist 的offset是根据 center 算的,而sourceColorRect给出的是可见内容相对原图左上角的精确坐标,所以按sourceColorRect定位即可。

json 的情况更直白:spriteSourceSize中的xy,就是可见区域相对于原图左上角的偏移量。直接用它作为贴片坐标就行。所以 json 格式还原时,不用去算中心偏移,直接按spriteSourceSize贴。

3.3 命令行的交互设计:参数越少越好

做工具最忌讳参数轰炸。PlistDumper 我后来把默认行为固定成“智能模式”:用户只需提供图集描述文件路径,工具自动在同目录查找同名的 png/jpg/webp,如果描述文件里指定了不同的图片路径则优先用描述文件里的。其余参数只留几个常见的:

  • --format:强制指定输入格式(默认自动识别)。
  • --output:输出目录,默认是描述文件同目录下的dump/文件夹。
  • --scale:导出缩放比例,比如 0.5 用于生成缩略图,一般不常用。
  • --rename-prefix:给导出的文件加统一前缀,方便区分图集来源。

这样设计的好处是,美术同学拿到工具后,只需要记住一条命令:

python plist_dumper.py assets/role.atlas

所有碎图自动导出到assets/dump/role/下,命名保持图集里的原始名字。我认为这类资源处理工具的重点不是功能多,而是“顺手”,如果每次都要查文档回忆参数,那离吃灰就不远了。

3.4 命名冲突和自定义前缀的坑

图集里的子图名字有时会包含路径,比如images/icon/btn_start.png。如果直接按这个全路径导出,输出目录会自动创建多层文件夹,通常没问题。但遇到同一个图集里有两张不同来源的同名图(比如 A 帧和 B 帧都叫frame_01),直接落盘会互相覆盖。

我的做法是在输出时做一次冲突检测,如果发现重名,自动追加_1_2后缀。另外批量处理多个图集时,强烈建议在导出时加上图集名前缀,比如输出成player_icon_01.png,否则几十个图集的碎图混在一起,后续维护根本分不清哪张来自哪里。

4. 实操记录:拿 Python + Pillow 写一个完整拆图脚本

理论讲完,直接上实操。这个脚本我目前还在用,逻辑不算复杂,但足够应付绝大多数拆图场景。用 Python 是因为 Pillow 处理图像最方便,而且跨平台,美术电脑上装个 Python 环境也不费劲。

4.1 环境准备和依赖

先安装 Python 3.9+ 和 Pillow:

pip install pillow

如果要读取 webp 或某些特殊格式的图集,Pillow 需要额外编译支持,不过日常用 PNG 和 JPG 基本没问题。顺便说一句,图集如果是pvr.ccz或者ktx这类 GPU 压缩格式,Pillow 读不了,这种情况建议先用 TexturePacker 自带的命令行工具解压成 PNG,再做拆图。

4.2 核心代码实现

下面给出完整的拆图脚本核心部分,我精简了异常处理,保留主干方便阅读:

import json import os import sys import xml.etree.ElementTree as ET from PIL import Image def parse_plist(path): tree = ET.parse(path) root = tree.getroot() frames = {} for e in root.iter('dict'): pass # 这里简单起见用 plistlib 更稳 import plistlib with open(path, 'rb') as f: data = plistlib.load(f) meta = data.get('metadata', {}) frames = data.get('frames', {}) items = [] for name, info in frames.items(): frame = parse_region(info['frame']) offset = parse_offset(info.get('offset', '{0,0}')) source_size = parse_size(info.get('sourceSize', '{0,0}')) source_color_rect = parse_region(info.get('sourceColorRect', '{{0,0},{0,0}}')) items.append(SpriteItem( name, 0, frame, info.get('rotated', False), source_size, offset, True, source_color_rect )) return items def parse_json(path): with open(path, 'r', encoding='utf-8-sig') as f: data = json.load(f) frames = data['frames'] items = [] for name in frames: if isinstance(frames[name], dict): info = frames[name] frame = (info['frame']['x'], info['frame']['y'], info['frame']['w'], info['frame']['h']) rotated = info.get('rotated', False) trimmed = info.get('trimmed', False) ss = info.get('spriteSourceSize', {}) ss_size = info.get('sourceSize', {}) items.append(SpriteItem( name, 0, frame, rotated, (ss_size['w'], ss_size['h']), (0, 0), trimmed, (ss['x'], ss['y'], ss['w'], ss['h']) )) return items def parse_atlas(path): with open(path, 'r', encoding='utf-8') as f: lines = f.readlines() items = [] i = 0 page = 0 image_path = None while i < len(lines): line = lines[i].strip() if not line: i += 1 continue if line.endswith(('.png', '.jpg', '.jpeg', '.webp')) and ',' not in line: image_path = line[:-1] if line.startswith('"') else line i += 1 # 下一行 size 信息属于 page if i < len(lines): size_line = lines[i].strip() i += 1 page += 1 continue if ':' not in line: # 这是子图名称行 name = line.strip() i += 1 region = None rotated = False orig = (0, 0) offset = (0, 0) while i < len(lines) and ':' in lines[i] and not lines[i].strip().endswith(('.png', '.jpg')): key_val = lines[i].strip().split(':') key = key_val[0].strip() val = key_val[1].strip() if key == 'xy': x, y = map(int, val.split(',')) elif key == 'size': w, h = map(int, val.split(',')) region = (x, y, w, h) elif key == 'orig': ow, oh = map(int, val.split(',')) orig = (ow, oh) elif key == 'offset': ox, oy = map(int, val.split(',')) offset = (ox, oy) elif key == 'rotate': rotated = (val == 'true') i += 1 items.append(SpriteItem(name, page - 1, region, rotated, orig, offset, True, region)) else: i += 1 return items

然后是切图和导出的核心:

def dump_sprite(atlas_img, item, out_path): x, y, w, h = item.region if w == 0 or h == 0: return sprite = atlas_img.crop((x, y, x + w, y + h)) if item.rotated: sprite = sprite.transpose(Image.ROTATE_90) # 按需改方向 canvas = Image.new('RGBA', item.source_size, (0, 0, 0, 0)) if item.trimmed: # 将裁切后的像素贴回原图位置 sx = item.source_color_rect[0] sy = item.source_color_rect[1] else: sx = item.source_size[0] // 2 - sprite.width // 2 sy = item.source_size[1] // 2 - sprite.height // 2 canvas.paste(sprite, (int(sx), int(sy)), sprite) canvas.save(out_path)

注意:atlas_img.crop()出来的图不带 alpha 信息时,粘贴到透明画布上必须用sprite本身作为第三个参数(mask),否则透明区域会变成黑色,这是新手最容易犯的错。

4.3 实际运行的输出示例

我用一个 Cocos 项目导出的ui_atlas.json跑了一遍,命令和结果如下:

python plist_dumper.py assets/ui/ui_atlas.json --output export

运行完成后控制台会打出:

解析到 128 个子图 图集图片: assets/ui/ui_atlas.png 导出目录: export/ui_atlas/ 已导出: btn_close.png 已导出: btn_ok.png ... 完成,共导出 128 个文件,耗时 1.2s

导出目录里的碎图,我随机抽了几张放到 PS 里对比原图,像素完全一致,包括带透明边的按钮和旋转过的小图标。整个过程不到两秒,比手抠快了一个数量级。

5. 常见问题与排查技巧:从报错到结果不对

工具写好了不代表不会出问题,尤其是在不同项目、不同 TexturePacker 版本之间流转的时候。下面是我在实操中反复遇到的几个典型问题,按出现频率排序,整理成一张速查表。

5.1 图集描述文件解析失败

现象原因排查/解决
plist 报parsing error文件带 BOM、编码不对用 plutil 或文本编辑器转成 UTF-8 无 BOM
json 报invalid JSON文件里有注释、尾逗号、非法字符先检查文件末尾是否多了逗号;去掉注释
json 报Expecting property name enclosed in double quotes键名用了单引号统一替换成双引号
json 报missing field(反序列化类报错)不是标准图集 json,而是接口返回的数据确认文件是不是 TexturePacker/Cocos 导出的
atlas 报index out of range子图块的键值对没解析完就跳出循环检查行尾是否有空行或大小写不一致

5.2 导出图片和原图对不上

这是拆图工具最常见的“隐性 bug”,表面不报错,但结果错误。我遇到过几个典型场景:

场景一:透明边没有被还原

导出的图片是一个小方块,直接铺在原图上位置不对。这通常是trimmed处理绕过了spriteSourceSizesourceColorRect的计算。记住一个原则:只要trimmed为 true,必须用sourceColorRect(plist)或spriteSourceSize(json)来定位。

场景二:图片旋转后发现方向不对

TexturePacker 的rotated字段只告诉你“旋转过”,但不同版本可能旋转方向不同(顺时针还是逆时针)。遇到导出的图横竖颠倒时,把Image.ROTATE_90换成Image.ROTATE_270,通常一两张测试图就能试出来。

场景三:有 1~2 像素的偏差

如果所有图都往右上角偏了 1 像素,多半是图集导出时设置了Extrude(边缘外扩防锯齿)或者Padding。TexturePacker 默认会给子图之间留 2 像素 padding,并且有时会做 1 像素的描边外扩。这种情况最稳妥的方法,是在 TexturePacker 里重新导出一次,关闭 extrude,或者让工具支持读 metadata 里的 padding 参数,根据它再偏移坐标。

注意:新版 TexturePacker 的 plist 会在metadata里写入realTextureFileNamesize等信息,但 padding/extrude 不一定有明确字段。如果你频繁遇到 1 像素偏差,建议导出图集时统一取消 Extrude。

5.3 fnt 位图字体的独有坑

fnt 格式还要额外注意:

  • char id是对应字符的 Unicode/ASCII 编码,导出文件名不要直接用 id,最好转成可读字符,否则一堆char_32.png根本看不懂。
  • kerning(字距调整)信息常常单独成块,如果只是拆图可以忽略;但如果要做字体生成器,一定要解析kerning pairs
  • 多页字体(pages=2以上)需要根据page字段选择对应的图片,否则坐标全部错位。

5.4 内存和性能问题

大图集动辄 4096×4096,Pillow 直接加载没太大问题,但如果你批量拆十几个图集,内存可能一路飙升。我的建议是:逐张处理,用完立刻close();如果还嫌慢,可以先用缩略图测试坐标对不对,再跑全量导出。另外 Pillow 对超大 PNG 的解压速度不算快,追求效率可以考虑用 pyvips,但日常工具没必要过度优化。

6. 扩展思路:从拆图到整套资源管线

PlistDumper 只是个开始,把它接入资源管线之后你会发现,拆图的需求往往伴随着一系列后续操作。这里给你几个实际可以做的扩展方向,都是我已经在用的。

  • 批量替换图集里的某个子图:先拆图 → 修改碎图 → 用 TexturePacker 的 CLI 重新打包。整套流程自动化之后,改 UI 素材的效率能提升不少。
  • 把 fnt 位图字体的每个字符导出成单独图片,再配合 OCR 生成书源、词库等数据,虽然用途偏门,但关键时刻很省事。
  • 检查图集内是否有重复子图,做个 md5 碰撞检测,帮美术瘦身图集尺寸。
  • 拆出来的碎图按目录结构重新组织,可以用于生成雪碧图预览页,方便策划验收资源。

我个人的体会是,拆图这件事本身不难,难的是把不同格式的差异、旋转和透明裁切的还原这些细节处理好。这套工具写好之后,后来不管是切序列帧、导位图字体,还是给外包交付单图资源,都是几十秒的事。如果你也有类似需求,建议直接照着上面的思路写一个自己用的版本,跑通后你会回来感谢我的。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 7:15:24

嵌入式开发工具选型指南:好用与专业如何平衡

你随便在哪个嵌入式技术社区搜“嵌入式开发工具”&#xff0c;大概率会看到两拨人互相看不太顺眼&#xff1a;轻量派说 VS Code 加 PlatformIO 好用得飞起&#xff0c;传统派说 Keil、IAR 才是真正吃饭的家伙。新人夹在中间容易犯晕&#xff0c;今天听张三说这个好&#xff0c;…

作者头像 李华
网站建设 2026/9/7 7:15:15

用vim宏编程实现康威生命游戏:寄存器、vimscript与逐代演化

如果你经常在 Linux 服务器上做事&#xff0c;一定遇到过这种场景&#xff1a;某个重复性文本操作要点几十次甚至上百次&#xff0c;手都酸了&#xff0c;还是要老老实实一批批改。之前我折腾 vim 宏编程时&#xff0c;一直觉得“录制按键再回放”这个能力非常神奇&#xff0c;…

作者头像 李华
网站建设 2026/9/7 7:12:50

FunASR说话人分离完全指南:三步自动标记“谁说了什么“

FunASR说话人分离完全指南&#xff1a;三步自动标记"谁说了什么" 【免费下载链接】FunASR Open-source speech recognition toolkit for training, inference, streaming ASR, VAD, punctuation, speaker diarization pipelines, and OpenAI-compatible/MCP serving.…

作者头像 李华
网站建设 2026/9/7 7:12:39

Emblem工具实测:长文档自动生成PPT与溯源功能全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 7:10:33

猫抓插件教程:浏览器视频下载与网页资源嗅探完整指南

猫抓插件教程&#xff1a;浏览器视频下载与网页资源嗅探完整指南 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓&#xff08;Cat-Catch&#…

作者头像 李华