H5游戏图集拆解实战:plist/atlas解析与Python还原脚本 简介面向Cocos、白鹭Egret与Laya等2D游戏引擎开发者的图集/合图分解实用工具用于将精灵表按plist元数据还原为单张图片便于资源编辑、版本更新与多设备适配。压缩包共6个文件、约227KB以可执行程序为主含splitImage主程序及调试宿主程序辅以Newtonsoft.Json动态库与XML文档用于解析图集配置数据另有清单文件与调试符号文件支撑运行与排错。已有1286人学习下载说明其在资源拆分场景中具备一定实用参考价值。工具围绕导入合图与plist、解析子图坐标尺寸、裁剪分割、输出单图并生成新plist的流程设计读者可借此理解精灵表结构与元数据组织方式掌握图集拆分思路并迁移到字体图集等资源的整理与优化工作中提升游戏资源管理效率。1. 图集拆解这件事为什么值得单独拎出来讲接手一个二手 H5 小游戏项目美术资源全被打进一张 2048×2048 的大图里配一个 plist 描述文件代码里按帧名取图。现在要换皮、要改某个按钮、要把其中几张图挪到新项目里复用——你手里只有这张大图和一份 XML没有原始 PSD也没有分层的散图。这时候能救命的就是一个能把图集还原成单张 PNG 的分解工具。cocos、白鹭Egret、laya 这几个国内 H5 引擎生态里图集打包是标配流程TexturePacker、引擎自带的打包插件都会产出「大图 plist/atlas/json」的组合而分解工具就是这条流水线的逆向出口。它适合三类人接手遗留项目要改资源的、做资源迁移要拆包复用的、以及想搞清楚 plist 里那些 frame、offset、rotated 字段到底在说什么的。下面按「先看懂格式、再动手拆、最后避坑」的顺序走一遍。2. 先读懂 plist 与 atlas图集描述文件到底存了什么2.1 plist 的两种形态与关键字段cocos 系图集最常见的描述文件是 plist本质是一个 XML也有二进制 plist工具一般会先转成 XML 再解析。它的结构分两层frames字典描述每一张小图metadata描述整张图集。真正决定「怎么切」的字段就那么几个看懂了就不会被工具的黑匣子行为搞懵。字段含义分解时的作用frame小图在大图中的矩形x/y/w/h决定裁剪区域注意 y 轴方向rotated是否旋转 90 度存储为 true 时必须反向旋转回来sourceSize小图原始尺寸还原画布大小处理透明边sourceColorRect原始有效像素在 sourceSize 中的位置决定贴回画布时的偏移offset相对中心点的偏移部分引擎用它做锚点对齐frame里的 y 坐标是最容易翻车的地方。plist 沿用 Cocos2d 的坐标系原点在左上角、y 向下而很多图像库比如 Python 的 Pillow裁剪时原点在左上、y 也向下看起来一致但一旦你手动换算成「左下原点」就会整体上下颠倒。我的习惯是解析出来先不做任何坐标变换直接按frame的 x/y/w/h 裁裁完再对照sourceSize检查尺寸对不对对不上再怀疑坐标系。2.2 rotated 与 offset两个最容易被忽略的还原参数rotatedtrue表示这张小图在打包时被顺时针旋转了 90 度塞进大图目的是提高 packing 利用率。分解时必须逆时针转回来否则你拿到的图是躺着的。判断方向不能靠猜标准做法是裁出w×h的块后如果 rotated 为真先把块逆时针旋转 90 度得到的尺寸应该等于sourceSize在无 trim 的情况下。如果转完尺寸反而对不上说明旋转方向搞反了换顺时针再试一次——这是我在处理白鹭导出的图集时踩过的坑不同打包器对 rotated 的方向约定并不完全统一。offset和sourceColorRect处理的是「透明边裁剪」。打包器为了省空间会把小图四周全透明的像素裁掉只存有效区域原始尺寸记在sourceSize里。分解时如果只按frame裁你会得到一张被裁瘦的图贴回 UI 就会错位。正确做法是新建一张sourceSize大小的透明画布把裁出的有效像素按sourceColorRect的偏移贴回去。很多一键分解工具默认不做这步导出的图看着没问题一进引擎就偏血泪经验就是先确认工具有没有「还原原始尺寸」的选项。2.3 atlas 与 json 格式的差异laya 和部分白鹭项目用的是.atlas文本格式结构比 plist 扁平一行图片名、一行x,y,w,h、一行offsetX,offsetY、一行origW,origH没有 XML 的嵌套。cocos creator 新版本则可能导出.json字段名是frames数组加spriteSourceSize、sourceSize、rotated。三种格式语义相通字段名不同写解析脚本时建议先做一个「统一中间结构」把三种格式都归一成{name, x, y, w, h, rotated, sourceW, sourceH, offsetX, offsetY}后面的裁剪逻辑就只写一份。这个归一化层是整套工具能不能同时吃三种引擎资源的关键也是我建议自己写而不是纯依赖现成 GUI 的原因——GUI 工具往往只认一种格式。3. 动手拆一张图集从解析到落盘的完整脚本3.1 环境与依赖用 Python 写分解脚本是最省事的Pillow 负责图像裁剪和旋转plistlib是标准库自带解析 XML plist 不用装额外包。装依赖就一行pip install pillow如果你的 plist 是二进制格式plistlib.load也能直接读不用先转 XML。atlas 和 json 则用普通文本解析即可。建议单独建一个工作目录把大图和描述文件放进去输出目录也建好避免脚本跑完散落一地。3.2 解析 plist 并归一化import plistlib import os def load_plist(path): with open(path, rb) as f: data plistlib.load(f) frames data.get(frames, {}) result [] for name, info in frames.items(): fr info[frame] # 字符串 {x,y},{w,h} # plist 里 frame 是字符串需要手动拆 xy, wh fr.split(},{) x, y [int(float(v)) for v in xy.strip({).split(,)] w, h [int(float(v)) for v in wh.strip(}).split(,)] ss info.get(sourceSize, f{{{w},{h}}}) sw, sh [int(float(v)) for v in ss.strip({}).split(,)] scr info.get(sourceColorRect, f{{{{0,0}},{{{w},{h}}}}}) # sourceColorRect 形如 {{x,y},{w,h}} inner scr.strip({}) pos, size inner.split(},{) ox, oy [int(float(v)) for v in pos.strip({).split(,)] result.append({ name: name, x: x, y: y, w: w, h: h, rotated: bool(info.get(rotated, False)), sourceW: sw, sourceH: sh, offsetX: ox, offsetY: oy, }) return result这段的关键在frame和sourceColorRect都是字符串而非结构化数据必须手动拆。int(float(v))是为了兼容12.0这种带小数点的写法直接int(12.0)会抛异常。归一化后的列表就是后面裁剪的唯一输入格式无关的解析到此结束。3.3 裁剪、反旋转、还原透明边from PIL import Image def split_atlas(img_path, frames, out_dir): os.makedirs(out_dir, exist_okTrue) atlas Image.open(img_path).convert(RGBA) for f in frames: # 1. 按 frame 裁出存储块 block atlas.crop((f[x], f[y], f[x] f[w], f[y] f[h])) # 2. rotated 为真则逆时针转回 if f[rotated]: block block.rotate(90, expandTrue) # 3. 还原到 sourceSize 画布按 offset 贴回 canvas Image.new(RGBA, (f[sourceW], f[sourceH]), (0, 0, 0, 0)) canvas.paste(block, (f[offsetX], f[offsetY])) # 4. 文件名去掉扩展名避免路径问题 safe f[name].replace(/, _) canvas.save(os.path.join(out_dir, safe))逻辑说明第 1 步裁剪用的是左上原点、y 向下的坐标和 plist 的frame一致不做任何翻转。第 2 步rotate(90, expandTrue)是逆时针expandTrue保证旋转后画布跟着变否则会被裁掉一角。第 3 步是还原透明边的核心——sourceW/sourceH是原始尺寸offsetX/offsetY是有效像素在原始画布里的左上角位置贴回去就得到和打包前一模一样的图。第 4 步把名字里的斜杠替换掉因为有些图集用ui/button/ok.png这种带路径的帧名直接当文件名会创建失败。参数上要留意offsetX/offsetY在部分 plist 里是相对中心点的偏移而非左上角如果你发现贴出来的图整体偏了半个身位就把paste的位置改成(sourceW - w) // 2 offsetX试一下。这个差异没有统一标准只能靠对照原图验证。3.4 批量处理与结果校验单张跑通后把目录下所有图集批量过一遍import glob for plist_path in glob.glob(input/*.plist): base os.path.splitext(plist_path)[0] png_path base .png if not os.path.exists(png_path): print(缺少大图:, png_path) continue frames load_plist(plist_path) split_atlas(png_path, frames, os.path.join(output, os.path.basename(base))) print(f{base}: 拆出 {len(frames)} 张)校验方法很朴素但有效拆完后统计输出图片数量是否等于frames数量再随机抽几张用图片查看器打开看有没有躺着的、有没有被裁掉边的。更严格一点可以写个断言比对每张输出图的尺寸是否等于sourceSize对不上就打印帧名通常能立刻定位到 rotated 方向或 offset 语义的问题。4. 避坑与排查拆图集时最容易翻车的五件事4.1 拆出来的图上下颠倒现象所有小图内容正确但整体上下翻转。原因解析时把 plist 的 y 坐标当成了左下原点做了height - y - h的换算。解决plist 的frame就是左上原点、y 向下和 Pillow 的crop一致不要做任何 y 翻转直接裁。4.2 rotated 图转完尺寸对不上现象rotatedtrue的图旋转后宽高和sourceSize差一位或完全不符。原因旋转方向搞反或者旋转前裁的矩形宽高没交换。解决先确认裁出块的尺寸逆时针转 90 度后宽高应互换若仍不对改顺时针再试。个别打包器在 rotated 时frame的 w/h 已经是旋转后的值需要先交换再裁。4.3 图片边缘出现半透明杂色现象拆出的图边缘有一圈不属于原图的颜色。原因大图是带预乘 alpha 的 PNG直接裁剪再保存导致边缘混色。解决读取时用convert(RGBA)并在保存前不做额外缩放若源图确实是预乘 alpha需要先反预乘再输出否则贴到引擎里边缘会发黑。4.4 帧名带路径导致保存失败现象脚本报No such file or directory。原因帧名形如ui/icon/close.png被当成相对路径。解决保存前把/和\替换成_或者按路径建子目录。前者更省事后者保留目录结构看你的复用需求。4.5 二进制 plist 读出来是乱码现象用文本编辑器打开 plist 全是不可读字符脚本解析报错。原因这是二进制 plist不是 XML。解决用plistlib.load(f)以二进制模式打开即可它会自动识别格式不要用xml.etree去解析。5. 进阶把分解结果直接喂回引擎与自动化校验拆完只是第一步真正省时间的是让输出直接能被引擎认。cocos 和 laya 都支持「散图 自动打图集」的工作流所以分解出的 PNG 只要命名和原帧名一致丢进项目的 assets 目录引擎重新打包时会按原逻辑取图代码一行不用改。这里有个细节如果原图集用了 trim裁透明边分解时已经还原成sourceSize重新打包时引擎会再裁一次最终渲染位置不变但如果你的代码里写死了某张图的像素尺寸做碰撞检测就要注意还原后的尺寸才是逻辑尺寸。自动化校验我一般会加一段尺寸断言跑完批量分解后统一报告异常帧from PIL import Image def verify(out_dir, frames): bad [] for f in frames: p os.path.join(out_dir, f[name].replace(/, _)) if not os.path.exists(p): bad.append((f[name], missing)); continue w, h Image.open(p).size if (w, h) ! (f[sourceW], f[sourceH]): bad.append((f[name], fsize {w}x{h} ! {f[sourceW]}x{f[sourceH]})) return bad这段断言能一次性把 rotated 方向错、offset 语义错、trim 没还原这三类问题全暴露出来比肉眼抽查靠谱得多。跑通一次之后我习惯把它固化成流水线的第一步任何新接手的图集先分解、再断言、全绿了才进改图环节。从那以后我每次拆图集都强制先跑一遍尺寸校验再动美术资源省下的返工时间远比写这十几行脚本多。希望帮到你。本文还有配套的精品资源点击获取