易语言提取石器时代SPR图片:调色板与像素索引转换实践 简介这是一份面向易语言学习者的图形图像处理源码专注解决从石器时代客户端中提取图片数据并动态显示的问题。项目核心为一个.e源码文件通过调用易语言常用支持库实现图片解码与显示并借助动画框控件呈现提取结果适合希望掌握游戏资源解析、动画框应用及支持库调用技巧的中级易语言爱好者。压缩包共4个文件除核心源码外还包含两个htm格式的参考资料系统整理魔力宝贝Bin文件格式与图像压缩格式的上、下篇可作为理解图像数据结构的延伸阅读另有txt说明文件提供使用前指引降低上手门槛。整包仅33KB轻量易读。目前已有344人学习对想要入门游戏素材提取或研究易语言图像处理的开发者具有不错的参考价值尤其适合作为解析游戏图片数据的起步案例。1. 易语言提取石器时代图片先绕过“图片”这两个字去年某位开发者丢给我一个老客户端的资源目录里面全是没后缀的二进制文件需求很简单把里面的物品图标、地图图块全抠出来。我随手用十六进制编辑器打开一个文件看到前100字节是整齐的颜色表后面跟着一大段单字节索引值立刻就明白这根本不是什么加密图片而是一块调色板加一串像素索引。所谓“提取图片”本质上就是把这段索引值按调色板映射成RGB再按照位图的存储规则重新组装。易语言在这件事上反而有优势它处理字节集比操作指针直观得多读文件、切片、拼接都不需要管内存地址写起来像在操作一个大数组。这篇文章就按我实际做过的方式来讲先拆SPR格式的结构再给出一套可直接复用的易语言转换代码最后讲那些不跑一遍就发现不了的坑。适合正在做游戏资源分析、贴图归档、MOD素材提取的易语言开发者也适合想搞懂索引位图原理的初学者。2. SPR资源格式拆解先看懂石器时代的像素存储逻辑再谈提取动手之前建议先把手上的文件用十六进制编辑器打开看一遍因为不同版本的石器时代客户端SPR文件头部字段的位置会有差异。下面说的结构是我处理过的一种常见变体字段顺序以你实际拿到的文件为准。理解了这种结构再遇到其他变体只是调整偏移量的问题整体思路完全通用。2.1 文件头里藏着什么帧数、宽度、压缩标志的读取位置SPR文件最前面的若干字节通常包含像素宽度、像素高度、帧数量、压缩标志这类信息。常见的排布规律大致是偏移从0开始长度含义0x002字节像素宽度小端0x022字节像素高度小端0x041字节压缩标志0为不压缩1为RLE压缩0x053字节保留位或附加标识0x082字节调色板偏移相对文件头0x0A2字节像素区偏移相对文件头0x0C之后不定各帧的偏移表或帧数量描述用易语言读取这些字段时核心工具就是两个读入文件把整个SPR变成字节集取字节集中间按偏移切出需要的部分。注意易语言字节集的下标从1开始但文件偏移从0开始切的时候要把偏移量加1这一步写错会带来后面所有数据的错位。读取头部的典型代码如下.版本 2 .支持库 spec .子程序 读取SPR头部, 整数型, , 返回帧数量 .参数 SPR字节集, 字节集 .参数 图宽, 整数型, 传址 .参数 图高, 整数型, 传址 .参数 调色板偏移, 整数型, 传址 .参数 像素偏移, 整数型, 传址 .局部变量 宽度字节, 字节集 .局部变量 高度字节, 字节集 宽度字节 取字节集中间 (SPR字节集, 1, 2) 高度字节 取字节集中间 (SPR字节集, 3, 2) 图宽 字节集到短整数 (宽度字节) 图高 字节集到短整数 (高度字节) 调色板偏移 字节集到短整数 (取字节集中间 (SPR字节集, 9, 2)) 1 像素偏移 字节集到短整数 (取字节集中间 (SPR字节集, 11, 2)) 1 从偏移表推算帧数这里假设偏移表从0x0C开始每2字节记录一帧的像素区起始位置 返回 (取字节集长度 (SPR字节集) 2)这里字节集到短整数是易语言自带的数据转换功能会把两个字节按小端序解析成整数正好对应SPR头里以小端存储的宽度和高度。传址参数是关键多个调用方需要共享这些头部结果用传址方式可以让子程序直接修改外部变量。加1是因为易语言字节集索引从1开始而文件偏移从0开始。帧数用文件总长度除以2推断只是临时估算实际应该以偏移表中的有效条目数为准后面讲批量导出时会提到更严谨的处理。2.2 调色板与像素索引区为什么256色文件比24位BMP小一倍调色板是SPR文件最值钱的部分它决定了整个文件里所有帧共用的颜色。常见存储方式是每个颜色占4字节前3字节分别是红、绿、蓝第4字节是占位符一共256种颜色正好1024字节。也就是说SPR只存调色板一次后面每一帧都只存像素索引每个像素1个字节值为0到255对应调色板里的某个颜色。这和24位BMP直接存RGB三通道的做法相比同样一张图体积差了三倍。例如一个64×64的图标24位位图光像素数据就要12288字节而SPR加上调色板也只有4096多字节。代价就是读取时必须先解析调色板再做一次索引到颜色的映射这一步正是提取程序的核心。像素区本身没有附加信息。帧的宽度和高度在头部像素区只是按“从左到右、从上到下”顺序排列的索引值。这里有一个大多数版本都存在的坑SPR像素区是紧凑排列的每一行多少字节就存多少字节不会像BMP那样在行尾补零对齐到4字节。这个差异直接导致后面转换BMP时会出现斜条纹花屏第五章会专门展开。3. 用易语言把SPR转换为BMP调色板映射与像素对齐的完整代码理解了结构之后提取的算法其实只有三步读取调色板、逐字节读取像素索引、按BMP格式重组数据。易语言里这三步都能用字节集操作完成不需要任何第三方支持库。这一章直接给出可复用的函数并解释每个参数为什么需要这样设置。3.1 易语言字节集操作读二进制文件不要用指针用取字节集中间很多人在易语言里读二进制文件第一反应是取字节集指针、读写内存地址其实完全没有必要。字节集本身就是一段连续的内存取字节集中间相当于安全的切片操作取字节集长度得到总大小两个字节集用直接拼接写起来比C语言里反复计算指针偏移要省心得多。底层的原理要说清楚字节集变量在易语言内部保存着“数据起始地址长度”取字节集中间返回的是一个新的字节集两个字节集相加返回的是拼接后的新字节集。这意味着如果在一个大循环里反复拼接字节集性能会随着数据量增长而下降。像SPR这种单帧只有几万字节的场景完全够用。如果以后遇到几十MB的超大贴图考虑改用写到内存配合内存分配接口但石器时代的素材规模远达不到那个量级。字节集下标从1开始这个设计是写转换代码时最容易出错的点。文件偏移量是0起字节集下标是1起两者相差1。我在所有子程序参数命名时都会加上“起始位置从1计算”的注释这是实操中总结出来的习惯。3.2 核心转换子程序SPR像素区变成带调色板的8位BMP下面这个函数接受一帧的原始数据和相关偏移返回完整的BMP文件字节集可以直接用写到文件保存为.bmp。它做了三件事把SPR调色板转成BMP调色板格式、把紧凑像素行补零对齐、组装BMP文件头。.版本 2 .支持库 spec .子程序 SPR帧转8位BMP, 字节集, , 返回完整BMP文件字节集 .参数 帧数据, 字节集, , 从SPR文件中切出来的完整帧数据 .参数 调色板起始, 整数型, , 调色板首字节在帧数据中的下标从1开始 .参数 像素区起始, 整数型, , 像素区首字节在帧数据中的下标从1开始 .参数 图宽, 整数型 .参数 图高, 整数型 .局部变量 BMP调色板, 字节集 .局部变量 BMP像素区, 字节集 .局部变量 原始调色板, 字节集 .局部变量 调色板条目, 字节集 .局部变量 当前行号, 整数型 .局部变量 颜色索引, 整数型 .局部变量 每行原始字节数, 整数型 .局部变量 每行对齐字节数, 整数型 .局部变量 像素行, 字节集 .局部变量 当前行偏移, 整数型 .局部变量 文件头, 字节集 .局部变量 信息头, 字节集 .局部变量 总文件大小, 整数型 .局部变量 零填充, 字节集 原始调色板 取字节集中间 (帧数据, 调色板起始, 1024) 第一步把SPR的RGB顺序调色板转换成BMP的BGR顺序 .计次循环首 (256, 颜色索引) 调色板条目 取空白字节集 (4) 调色板条目[1] 原始调色板[颜色索引 × 4 1] 原R放入B通道 调色板条目[2] 原始调色板[颜色索引 × 4 2] 原G放入G通道 调色板条目[3] 原始调色板[颜色索引 × 4 3] 原B放入R通道 调色板条目[4] 0 BMP调色板 BMP调色板 调色板条目 .计次循环尾 () 第二步逐行复制像素索引行尾补零到4字节对齐 每行原始字节数 图宽 每行对齐字节数 到整数 (((图宽 3) ÷ 4) × 4) 零填充 取重复字节集 (每行对齐字节数 每行原始字节数, { 0 }) .计次循环首 (图高, 当前行号) 当前行偏移 像素区起始 (当前行号 1) × 每行原始字节数 像素行 取字节集中间 (帧数据, 当前行偏移, 每行原始字节数) 如果真 (每行对齐字节数 每行原始字节数) 像素行 像素行 零填充 如果真结束 BMP像素区 BMP像素区 像素行 .计次循环尾 () 第三步组装BMP文件头、信息头和调色板 总文件大小 14 40 1024 取字节集长度 (BMP像素区) 文件头 { 66, 77 } 到字节集 (总文件大小) { 0, 0, 0, 0, 0, 0 } 到字节集 (1078) 信息头 到字节集 (40) 到字节集 (图宽) 到字节集 (图高) 到字节集 (1) 到字节集 (8) 到字节集 (取字节集长度 (BMP像素区)) 到字节集 (2835) 到字节集 (2835) 到字节集 (256) 到字节集 (0) 返回 (文件头 信息头 BMP调色板 BMP像素区)代码逻辑分三步走。第一步把调色板顺序从RGB交换到BGR这是BMP格式的硬性要求。Windows位图调色板中每个颜色条目的存储顺序是蓝色、绿色、红色、占位而SPR文件里通常是红、绿、蓝、占位不交换就会出现第五章那个颜色整体发蓝的经典问题。第二步是像素行的处理SPR像素区紧凑存储BMP要求每行按4字节对齐行尾补零不会影响显示因为BMP规范规定这些字节被忽略。第三步组装文件头时到字节集把整数直接转换成了小端字节序正好符合BMP格式需求这点要比手动拼字节省去不少麻烦。几个参数需要说明调色板起始和像素区起始传的都是帧数据里的字节集下标第一个字节的下标是1而不是0。图宽图高来自头部解析如果读出来是反的图像会旋转90度而不是简单的拉伸。图高这一行很多人看不懂它表示BMP采用自顶向下的行序这样像素区从上到下扫描正好和SPR的存储顺序一致不需要翻转行。如果看图工具对负高度兼容性不好可以改成正高度并把像素行逆序写入但绝大多数现代看图工具都支持负高度没有必要增加复杂度。4. 完整提取器落地文件选择、帧列表、画板预览与导出有了核心转换函数剩下的就是把它做成一个能用的窗口程序。这一章给出一个标准提取器的界面结构和主流程代码照着搭就能跑起来。界面不追求花哨稳定和直观优先。4.1 界面控件选型通用对话框、超级列表框、画板的分工窗口上放四个控件就能覆盖完整流程一个“打开文件”按钮配合通用对话框选择SPR文件一个超级列表框展示帧清单一个图片框用来预览当前选中帧一个“导出当前帧”按钮。不需要画板图片框的图片属性直接接受字节集数据省去手动处理句柄这一步。这里有一个设计取舍预览控件用图片框而不是画板。画板显示图片需要创建设备相关位图并管理画刷容易出现“导出正常但预览黑屏”的问题图片框直接赋值字节集则没有这些麻烦。如果你一定要用画板点击列表项后需要调用画板.置背景图形并刷新第五章最后一个坑会讲。超级列表框的列建议这样设置第一列“编号”、第二列“像素区偏移”、第三列“帧尺寸”。第二列看起来不美观但它是调试和信息核对的重要依据。同一个SPR里各帧尺寸可能不同比如地图图块是32×32物品图标是48×48在列表里把尺寸显示出来能提前发现解析错误。4.2 一条龙提取代码从打开SPR到导出单帧BMP打开文件时先读入字节集解析头部再扫描像素区偏移表填充超级列表框.版本 2 .支持库 iext .程序集变量 SPR字节集, 字节集 .程序集变量 全局调色板偏移, 整数型 .程序集变量 全局图宽, 整数型 .程序集变量 全局图高, 整数型 .子程序 _按钮打开文件_被单击 .局部变量 文件路径, 文本型 .局部变量 帧数量, 整数型 .局部变量 当前帧, 整数型 .局部变量 每帧偏移, 整数型 通用对话框1.类型 0 通用对话框1.过滤器 “资源文件 (*.spr)|*.spr|所有文件 (*.*)|*.*” 如果真 (通用对话框1.打开 ()) 文件路径 通用对话框1.文件名 SPR字节集 读入文件 (文件路径) 如果真 (取字节集长度 (SPR字节集) 128) 信息框 (“文件大小异常可能不是有效SPR”, 0, “提示”, ) 返回 () 如果真结束 帧数量 读取SPR头部 (SPR字节集, 全局图宽, 全局图高, 全局调色板偏移, 像素区偏移变量) 超级列表框1.全部删除 () .计次循环首 (帧数量, 当前帧) 每帧偏移 字节集到短整数 (取字节集中间 (SPR字节集, 13 (当前帧 1) × 2, 2)) 1 超级列表框1.插入表项 (, 到文本 (当前帧), , , , ) .计次循环尾 () 信息框 (“共加载” 到文本 (帧数量) “帧”, 0, “提示”, ) 如果真结束这里用了一个程序集变量SPR字节集它保证打开文件后点击任何列表项都能访问到同一份数据。把字节集存在程序集变量而不是局部变量是这种工具类程序的常见做法避免跨子程序传递大字节集带来的复制开销。帧数量从头部读出来后偏移表从文件偏移12开始字节集下标13每2字节记录一帧的像素区起始位置。注意这里我做了转换后加1和前面讲的1-based下标保持一致。有些SPR变体的偏移表不是固定12字节开始而是由头部字段给出偏移表位置如果你的文件解析出来帧数量明显不对优先检查这个起始位置。列表项点击后预览当前帧导出按钮保存当前帧.子程序 _超级列表框1_当前选中项被改变 .局部变量 当前帧号, 整数型 .局部变量 帧像素偏移, 整数型 .局部变量 BMP数据, 字节集 当前帧号 超级列表框1.取当前选中项 () 如果真 (当前帧号 0) 返回 () 如果真结束 帧像素偏移 字节集到短整数 (取字节集中间 (SPR字节集, 13 当前帧号 × 2, 2)) 1 BMP数据 SPR帧转8位BMP (SPR字节集, 全局调色板偏移, 帧像素偏移, 全局图宽, 全局图高) 图片框预览.图片 BMP数据导出保存.子程序 _按钮导出_被单击 .局部变量 保存路径, 文本型 .局部变量 当前帧号, 整数型 .局部变量 帧像素偏移, 整数型 .局部变量 BMP数据, 字节集 当前帧号 超级列表框1.取当前选中项 () 如果真 (当前帧号 0) 信息框 (“请先选择帧”, 0, “提示”, ) 返回 () 如果真结束 通用对话框保存.类型 1 通用对话框保存.过滤器 “位图文件 (*.bmp)|*.bmp” 如果真 (通用对话框保存.打开 ()) 保存路径 通用对话框保存.文件名 如果真 (取文本右边 (保存路径, 4) ≠ “.bmp”) 保存路径 保存路径 “.bmp” 如果真结束 帧像素偏移 字节集到短整数 (取字节集中间 (SPR字节集, 13 当前帧号 × 2, 2)) 1 BMP数据 SPR帧转8位BMP (SPR字节集, 全局调色板偏移, 帧像素偏移, 全局图宽, 全局图高) 写到文件 (保存路径, BMP数据) 信息框 (“导出完成”, 0, “提示”, ) 如果真结束预览和导出共用同一个转换函数保证看到什么就导出什么。注意两次都重新取了一遍帧像素偏移这样即使列表被刷新过只要索引正确就能拿到对应的数据。如果帧尺寸不统一这里还需要从偏移表或者帧各自的头部去读取宽高但很多SPR是同一尺寸的多帧合图先用全局宽高就能跑通大多数场景。5. 图片提取的五个高频坑与排查记录从颜色发蓝到内存报错这一章是实际操作下来最值得记录的部分。每个坑我都按“现象 → 原因 → 解决”的顺序写方便你对照排查。这些坑单独看都不难但连在一起出现的概率非常高知道原因以后基本上都能一次定位。5.1 颜色整体发蓝调色板RGB顺序的坑现象导出的BMP颜色整体偏蓝尤其是红色物体变成了蓝色绿色正常。像树叶变蓝、火焰变蓝非常诡异。原因SPR调色板条目按红、绿、蓝存储而Windows BMP调色板要求蓝、绿、红。直接用原始调色板写BMP等于把红色通道和蓝色通道对调了。我的第一次提取就翻在这个上面对着十六进制数据看了半天没发现后来把前16个颜色单独导出成色块才意识到是通道顺序问题。解决转换调色板时交换第1和第3字节。第三章代码里调色板条目[1] 原始调色板[颜色索引 × 4 1]这一组赋值就是干这个的。排查时写一个调试子程序把调色板前16个颜色输出成16个横向色块一眼就能看出顺序对不对。5.2 图片出现斜条纹BMP每行4字节对齐的坑现象图片整体是完好的但每隔一段距离出现一条斜向的杂色条纹像画面被斜着剪了一刀。图像宽度不是4的倍数时更容易出现。原因SPR像素区一行就是图宽个字节紧凑排列。BMP每行必须对齐到4字节不对齐的话读取器会把上一行尾部多出来的数据当成下一行的开头整个画面就斜了。宽度为33、34、35像素时最容易触发64这种能被4整除的宽度反而不会出问题。解决写像素区时每行末尾补零到4字节对齐。第三章代码里每行对齐字节数 到整数 (((图宽 3) ÷ 4) × 4)和像素行 像素行 零填充就是标准做法。注意补零字节数不等于固定的某个值而是对齐字节数 原始字节数宽度不同补的零也不同。5.3 大文件读着读着数据错乱整数溢出与长整数型现象小SPR文件一切正常换成几MB的合图后前面几十帧正常后面的帧导出的图片变成随机噪点。重新打开文件再试出错的帧位置固定不变。原因易语言的整数型是32位有符号数最大约21亿。合图尺寸大时像素区起始 (行号 1) × 每行原始字节数这个计算结果在循环过程中超过了整数上限溢出成负数或者截断值取字节集中间就取到了错误的位置。这种错误不是随机发生的所以每次错的位置都一样。解决所有涉及“起始位置 行号 × 行字节数”的变量统一定义成长整数型。易语言里直接定义当前行偏移, 长整数型就行。另外取字节集中间的第三个参数长度比较小不会溢出重点检查第一个参数相关的偏移计算。我一般会在子程序参数里也把偏移类参数设成整数型但内部局部变量必须用长整数型。5.4 后半部分变成随机色块每帧的压缩标志字节现象帧列表里能看到所有帧前几帧正常后面的帧内容完全错乱像是把之前的某块数据重复读了一遍。并且用十六进制编辑器对比后发现错乱的帧像素区和前面某一帧高度相似。原因SPR有两种存储模式不压缩和RLE压缩。头部有压缩标志字段但有些版本在每一帧前面单独标记这个帧是否压缩。解析偏移表时如果没跳过这个标志字节读到的“偏移量”实际上是标志位和真正的偏移量拼接出来的错误值相当于每一帧都错开了几个字节越到后面的帧错得越厉害。解决逐帧读取时先判断当前帧前1个字节是否为压缩标志。如果是1需要先按RLE规则把压缩数据解压成原始索引序列再送进转换函数。如果确认文件没有压缩帧则把跳过标志字节的偏移量写进偏移表。排查方法是导出第一帧和最后一帧对比错乱帧在十六进制里的实际偏移通常能看出规律性位移。5.5 画板预览全黑但导出正常设备相关位图的句柄问题现象用画板显示BMP数据画板全黑或显示成一块纯色但保存出来的BMP文件用外部看图软件打开完全正常。图片框控件没有这个问题。原因画板是设备相关位图DDB和具体显示设备的颜色格式绑定。直接把一个字节集赋给画板的图片属性画板没有拿到有效的位图句柄或者句柄仍然是空的导致显示失败。图片框控件内部处理了DIB转换所以直接赋值就能显示。解决要么换成图片框控件预览把图片框预览.图片 BMP数据作为标准做法要么用画板时先调用画板.创建自内存位图 (BMP数据)之类的接口拿到句柄再赋值。我个人的习惯是工具类程序一律用图片框只有需要频繁重绘的游戏界面类程序才用画板省掉一批句柄相关的玄学问题。6. 批量导出全部帧与透明背景提取器从“能跑”到“好用”单帧提取跑通以后很快就会发现真正的需求是批量。几百帧的合图如果一帧一帧手点导出手指会先崩溃。批量导出的核心就是一个计次循环加上文件名生成但有几个细节能让结果更规整。文件名规则建议用三位数字补齐格式比如001.bmp002.bmp避免导出100帧以后排序变成1、10、100。实现方式是在循环序号小于100时拼接前导零。导出目录用固定子目录先创建目录再写文件避免写到错误的位置.计次循环首 (帧数量, 当前帧) 帧像素偏移 字节集到短整数 (取字节集中间 (SPR字节集, 13 (当前帧 1) × 2, 2)) 1 帧号文本 到文本 (当前帧) 如果真 (当前帧 100) 帧号文本 “0” 帧号文本 如果真结束 如果真 (当前帧 10) 帧号文本 “0” 帧号文本 如果真结束 写到文件 (创建目录 (取运行目录 () “\导出”) 帧号文本 “.bmp”, SPR帧转8位BMP (SPR字节集, 全局调色板偏移, 帧像素偏移, 全局图宽, 全局图高)) .计次循环尾 ()透明背景的处理要按实际用途来。很多素材图里索引0是固定的背景色导出BMP后背景是纯色而不是透明。如果需要透明通道两个方案可以选简单方案是把背景色统一替换成一种明确的特殊色如品红色在后续图像处理里做色键抠图完整方案是写一个TGA导出子程序TGA支持Alpha通道把索引0对应的像素Alpha值写成0其余写成255。TGA的头部结构比BMP简单很多调色板格式也相近值得一试。最后说一个验证方法批量导出后不要只看预览用外部取色工具对几个关键像素做逐点对比。具体做法是取原SPR帧中某个像素的索引值查调色板得到原始RGB再在导出的BMP对应坐标位置取色两个颜色应该完全一致。这个验证过程可以写成一个自动化的对比子程序循环检查100个随机坐标点全部一致才算通过。我现在每次拿到不认识的资源格式第一件事就是写个小函数把调色板导出成256个色块的横向长条保存成PNG看一遍颜色排布再决定怎么解析像素区。这个习惯帮我省了好几个小时的无头绪排查。做资源提取这件事最大的成本从来不是写代码而是猜格式。把格式猜透易语言写转换逻辑是真的顺手。希望帮到你。本文还有配套的精品资源点击获取