WZX与WZL是盛大传奇后期客户端采用的图形资源封装格式,WZX承担索引职责,记录每一张图片在WZL中的起始偏移;WZL承担数据存储职责,按顺序存放文件头、图片元信息与像素流。二者同名成对出现,例如 hum.wzx + hum.wzl、Prguse.wzl + Prguse.wzx。解析逻辑本质是“先读WZX拿偏移数组 → 按偏移seek到WZL → 读图片头 → 读像素流 → 结合调色板还原RGB”。
一、WZX索引文件结构
偏移布局如下:
0x00 – 0x2B:44字节标题头(Title数组,内容无解析价值,直接跳过)
0x2C – 0x2F:4字节小端int,图片总数 ImageCount
0x30 起:ImageCount × 4字节,每张图在对应WZL中的绝对偏移量(小端uint32)
空图处理:ImageCount只是数组容量,实际有效图可能少于该值,偏移为0或指向空数据块时需跳过。
Python读取WZX偏移表:
import struct
def parse_wzx(path):
with open(path, 'rb') as f:
f.seek(44)
image_count = struct.unpack('<I', f.read(4))[0]
offsets = []
for i in range(image_count):
f.seek(48 + i * 4)
off = struct.unpack('<I', f.read(4))[0]
offsets.append(off)
return image_count, offsets
cnt, offs = parse_wzx('hum.wzx')
print('image_count=', cnt)
print('first 5 offsets=', offs[:5])
二、WZL数据文件结构
WZL文件头:
0x00 – 0x2B:44字节标题
0x2C – 0x2F:4字节 int,ImageCount
0x30 起:5 × 4字节未知保留字段(Unknow[0..4]),共20字节
之后紧跟按偏移排列的图片块。
单张图片块结构(ImageInfo头,约16字节,实际按字段累加):
偏移 类型 含义
0 byte PixelFormat 像素位深(8=256色索引,16=RGB565等)
1 byte Compressed 是否压缩(1=zlib/gzip,0=裸数据)
2 byte Reserve 保留
3 byte CompressLevel 压缩等级
4–5 int16 Width 宽
6–7 int16 Height 高
8–9 int16 OffsetX 绘制偏移X
10–11 int16 OffsetY 绘制偏移Y
12–15 int32 Length 像素数据长度
ImageInfo头后紧跟 Length 字节像素流;若 Compressed=1,先 zlib.decompress 再处理。
三、像素还原与调色板
传奇历史资源以8bit索引图为主,16bit RGB565次之。
8bit流程:像素流每个字节是调色板下标 → 查256色 MirPalette(TRGBQuad数组,BGRA顺序)→ 写出RGB。
调色板来源:客户端自带 palette 数据,或首个WIL文件头内嵌1024字节(256×4)调色板;新版本UI资源也可能直接存RGB565/RGBA4444,此时不走调色板。
16bit RGB565转RGB公式:
r = ((v >> 11) & 0x1F) << 3
g = ((v >> 5) & 0x3F) << 2
b = (v & 0x1F) << 3
四、完整Python解码单张图示例(8bit索引 + zlib)
import struct, zlib
from PIL import Image
# 256色传奇调色板占位(实际应载入客户端MirPalette.bin)
MIR_PALETTE = [(i, i, i) for i in range(256)]
def read_wzl_image(wzl_path, offset):
with open(wzl_path, 'rb') as f:
f.seek(offset)
pixel_fmt, comp, res, clevel = f.read(4)
w, h = struct.unpack('<hh', f.read(4))
ox, oy = struct.unpack('<hh', f.read(4))
data_len = struct.unpack('<I', f.read(4))[0]
raw = f.read(data_len)
if comp == 1:
raw = zlib.decompress(raw)
if pixel_fmt == 8: # 8bit索引
img = Image.new('RGB', (w, h))
px = img.load()
idx = 0
for y in range(h):
for x in range(w):
pal = raw[idx]; idx += 1
px[x, y] = MIR_PALETTE[pal]
return img, (ox, oy)
elif pixel_fmt == 16: # RGB565
img = Image.new('RGB', (w, h))
px = img.load()
idx = 0
for y in range(h):
for x in range(w):
v = struct.unpack('<H', raw[idx:idx+2])[0]; idx += 2
r = ((v >> 11) & 0x1F) << 3
g = ((v >> 5) & 0x3F) << 2
b = (v & 0x1F) << 3
px[x, y] = (r, g, b)
return img, (ox, oy)
else:
raise ValueError(f'unsupported pixel_fmt={pixel_fmt}')
# 调用示例:取第0张图
cnt, offs = parse_wzx('hum.wzx')
img, off = read_wzl_image('hum.wzl', offs[0])
print('size=', img.size, 'offset=', off)
img.save('out_0.png')
五、批量抽取整个WZL资源包
def dump_wzl_all(wzx_path, wzl_path, out_dir):
import os
os.makedirs(out_dir, exist_ok=True)
cnt, offs = parse_wzx(wzx_path)
for i, off in enumerate(offs):
if off == 0:
continue
try:
img, _ = read_wzl_image(wzl_path, off)
img.save(f'{out_dir}/{i}.png')
except Exception as e:
print('skip', i, e)
dump_wzl_all('Prguse.wzx', 'Prguse.wzl', 'prguse_out')
六、与老版WIL/WIX格式差异
老客户端用 WIL(数据)+ WIX(索引),WIX头44字节标题+int计数+偏移数组,WIL头后紧跟256色调色板1024字节,图片头含宽高/偏移/阴影/长度;WZL/WZX把调色板外置或改为高位深直存,索引与数据分离更干净,支持zlib压缩,文件体积更小,解析时不再强制依赖WIL内嵌调色板。
七、常见解析异常定位
• 全图黑色:调色板未载入或PixelFormat判断错
• 尺寸错位花屏:ImageInfo头字节数算错,常见漏掉CompressLevel或Reserve
• 偏移越界:WZX与WZL不是同一版本补丁包,文件名同但内容不匹配
- decompress报错:Compressed标志误判,裸数据当zlib解
• 透明区域丢失:传奇索引图0号调色板槽通常为透明色,写PNG时应置alpha=0而非黑色
按上述字段顺序与字节对齐方式处理,WZL/WZX全部图片块均可逐张还原为PNG位图。
热血传奇WZL与WZX资源包格式拆解及Python解析完整实现
来源:
作者:
点击:

