Skip to content

Instantly share code, notes, and snippets.

@yin8086
Last active November 18, 2023 03:49
Show Gist options
  • Save yin8086/3882151162fce3869f5a to your computer and use it in GitHub Desktop.
Save yin8086/3882151162fce3869f5a to your computer and use it in GitHub Desktop.
SeitokaiNoichizonLv2_Crack
# -*- coding: utf-8 -*-
"""
Created on Thu Oct 16 10:07:25 2014
@author: Stardrad
"""
import time, struct, os, fnmatch
from cStringIO import StringIO
import gzip
from PIL import Image
def walk(adr):
#root,dirs,files = os.walk(adr).next()
for root,dirs, files in os.walk(adr):
for name in files:
if fnmatch.fnmatch(name, '*.bin') :
yield os.path.join(root, name)
# tarW, tarH 目标图像大小,如512,272
# toW, toH 是最终保存的图像大小,切割30*272后的480, 272
# tiles 16*8的tile数组
def fromTile(pixBuf, tarW, tarH, \
toW, toH, \
tiles, pal = 0):
tileW = 16
tileH = 8
tileInW = tarW / tileW
for picY in xrange(toH):
for picX in xrange(toW):
tileOut = picY / tileH * tileInW + picX / tileW
tileIn = picY % tileH * tileW + picX % tileW
if pal == 0:
pixBuf[picY*toW + picX] = tiles[tileOut][tileIn]
else:
getInd = ord(tiles[tileOut][tileIn])
colBuf = pal[4 * getInd: 4 * getInd + 4]
pixBuf[picY*toW + picX] = tuple([ord(colCh) for colCh in colBuf])
def mapTile(pixBuf, tarW, tarH, tile, pal):
tileW = 32
tileH = 16
for picY in xrange(tarH, tarH + tileH):
for picX in xrange(tarW, tarW + tileW):
tileIn = (picY - tarH) * tileW + (picX - tarW)
getInd = ord(tile[tileIn])
colBuf = pal[4 * getInd: 4 * getInd + 4]
pixBuf[picX, picY] = tuple([ord(colCh) for colCh in colBuf])
def findAddr(fPtr, tarStr, rlen = -1):
if rlen == -1:
content = fPtr.read()
else:
content = fPtr.read(rlen)
begAdd = 0
fIndex = content.find(tarStr, begAdd)
return fIndex
def getBigTile(mLine, big_tile_list, NUM_BT_W, line_len):
curPos = 0
BASE_TILE_W = 16
BASE_TILE_H = 8
for i in xrange(NUM_BT_W):
blk = []
blk.append(mLine[curPos: curPos + BASE_TILE_H * BASE_TILE_W])
blk.append(mLine[curPos + BASE_TILE_H * BASE_TILE_W : curPos + BASE_TILE_H * BASE_TILE_W * 2])
blk.append(mLine[line_len + curPos: line_len + curPos + BASE_TILE_H * BASE_TILE_W])
blk.append(mLine[line_len + curPos + BASE_TILE_H * BASE_TILE_W : line_len + curPos + BASE_TILE_H * BASE_TILE_W * 2])
pixBuf = [0] * (32 * 16)
fromTile(pixBuf, 32, 16, 32, 16, blk)
big_tile_list.append(pixBuf)
curPos = curPos + BASE_TILE_H * BASE_TILE_W * 2
class ImageParser:
def __init__(self, f_ptr, folder, f_name, tw = 16, th = 8):
self.curFile = f_ptr
self.curFolder = folder
self.curName = f_name
self.TILE_W = tw
self.TILE_H = th
def load_file(self):
self.curFile.seek(0, 2)
f_size = self.curFile.tell()
if f_size <= 0x400:
print 'not an image'
return False
self.curFile.seek(0)
baseAddr = -1
for i in xrange(0, f_size, 0x400):
fIndex = findAddr(self.curFile, '\x1f\x8b\x08', 0x400)
if fIndex != -1 :
baseAddr = i
break
self.baseAddr = baseAddr
if baseAddr == -1:
print 'not an image, pass'
return False
elif baseAddr == 0x1800:
self.curFile.seek(0x400)
head = self.curFile.read(4)
if head != 'MAP\x00':
self.type = 'REG'
return True
else:
self.curFile.seek(0x400)
map_header = self.curFile.read(0x30)
self.test_w, = struct.unpack('<H', map_header[0x0a:0x0a+2])
self.test_h, = struct.unpack('<H', map_header[0x0e:0x0e+2])
if self.test_h != 0x10 or self.test_w != 0x10:
print 'unknown format'
return False
else:
if map_header[0x22:0x24] != '\x00\x00':
self.type = 'FACE'
else:
self.type = 'MAP'
return True
else:
self.type = 'REG_M'
return True
def read_pal(self):
self.curFile.seek(0x0)
self.pal = self.curFile.read(0x400)
def decompress_gzip(self, addr = 0x1800):
self.curFile.seek(addr)
try:
chunk_num, = struct.unpack('<I', self.curFile.read(4))
first_addr, = struct.unpack('<I', self.curFile.read(4))
self.curFile.seek(addr + first_addr + 0x10)
test_head = self.curFile.read(3)
if test_head != '\x1f\x8b\x08':
print 'not an Image, pass'
return False
self.curFile.seek(addr + 4)
desc_buf = self.curFile.read( (chunk_num+1) * 4 )
desc_list = []
for i in xrange(chunk_num):
s_addr, = struct.unpack('<I', desc_buf[i*4: i*4 + 4])
e_addr, = struct.unpack('<I', desc_buf[i*4 + 4: i*4 + 8])
desc_list.append([s_addr, e_addr])
self.decomp_buf = ''
self.curFile.seek(addr)
tmp_buf = self.curFile.read(desc_list[-1][1])
for i in xrange(chunk_num):
strBuf = StringIO(tmp_buf[ desc_list[i][0] + 0x10 :desc_list[i][1] ])
g = gzip.GzipFile(mode='rb', fileobj=strBuf)
decBytes = g.read()
self.decomp_buf += decBytes
except:
print '%s decompress failed, pass' % self.curName
return False
return True
def parse_to_png(self):
if not self.load_file():
return
if self.type == 'REG':
self.read_pal()
if not self.decompress_gzip():
return
max_len = len(self.decomp_buf)
tile_size = self.TILE_W * self.TILE_H
tiles = [self.decomp_buf[off: off+tile_size] for off in xrange(0, max_len, tile_size)]
self.max_x = 512
self.max_y = len(self.decomp_buf) / self.max_x
if self.max_y == 272:
pixBuf = [0] * (480 * 272)
fromTile(pixBuf, self.max_x, self.max_y, 480, 272, tiles, self.pal)
print 'Create Regular Image(%d, %d) |' % (480, 272),
img = Image.new('RGBA', (480, 272))
else:
pixBuf = [0] * (self.max_x * self.max_y)
fromTile(pixBuf, self.max_x, self.max_y, self.max_x, self.max_y, tiles, self.pal)
print 'Create Regular Image(%d, %d) |' % (self.max_x, self.max_y),
img = Image.new('RGBA', (self.max_x, self.max_y))
img.putdata(tuple(pixBuf))
elif self.type == 'MAP' or self.type == 'FACE':
self.read_pal()
if not self.decompress_gzip():
return
self.curFile.seek(0x400)
head_len, = struct.unpack('H', self.curFile.read(2))
self.curFile.seek(0x400)
head_buf = self.curFile.read(head_len)
num_pos, = struct.unpack('<H', head_buf[0x06:0x08])
num_tile_w, tile_w, num_tile_h, tile_h, = struct.unpack('<4H', head_buf[0x08:0x10])
data_len, = struct.unpack('<I', head_buf[0x10:0x14])
mapTable = []
max_x = 0
min_x = 2048
max_y = 0
min_y = 2048
for i in xrange(num_pos):
curX, curY, = struct.unpack('<2H', head_buf[0x30+ i*4:0x30+ i*4 + 4])
if curX > max_x :
max_x = curX
if curX < min_x:
min_x = curX
if curY > max_y:
max_y = curY
if curY < min_y:
min_y = curY
mapTable.append([curX, curY])
for i in xrange(num_pos):
mapTable[i][0] -= min_x
mapTable[i][1] -= min_y
self.max_x = max_x - min_x + 32
self.max_y = max_y - min_y + 16
BASE_TILE_W = 16
BASE_TILE_H = 8
BT_W = 32
NUM_BT_W = BASE_TILE_W * num_tile_w / BT_W
lineLen = BASE_TILE_H * BASE_TILE_W * num_tile_w * (tile_h / BASE_TILE_H)
multiLines = []
if self.type == 'FACE':
face_size, = struct.unpack('<H', head_buf[0x22:0x24])
face_len = face_size / 0x10 * lineLen
face_tiles = [self.decomp_buf[face_pos:face_pos+16*8] for face_pos in xrange(0, face_len, 16*8)]
read_pos = face_size / 0x10 * lineLen
else:
read_pos = 0
for i in xrange(num_tile_h):
multiLines.append(self.decomp_buf[read_pos:read_pos+lineLen])
read_pos += lineLen
big_tile_list = []
for mLine in multiLines:
getBigTile(mLine, big_tile_list, NUM_BT_W, lineLen/2)
print 'Create Map Image(%d, %d) | ' % (self.max_x, self.max_y),
img = Image.new('RGBA', (self.max_x, self.max_y), 'rgba(0,0,0,0)')
pixImage = img.load()
for i, tile in enumerate(big_tile_list):
if i == len(mapTable):
break
mapTile(pixImage, mapTable[i][0], mapTable[i][1], tile, self.pal)
if self.type == 'FACE':
pixBufFace = [0] * (32*16 * face_size)
fromTile(pixBufFace, 32*16, face_size, 32*16, face_size, face_tiles, self.pal)
print 'Create External Face(%d, %d) |' % (32*16, face_size),
img2 = Image.new('RGBA', (32*16, face_size), 'rgba(255,255,255,255)')
img2.putdata(tuple(pixBufFace))
elif self.type == 'REG_M':
self.curFile.seek(0)
pals = []
for i in xrange(0, self.baseAddr, 0x400):
pals.append(self.curFile.read(0x400))
cur_addr = self.baseAddr
imgs = []
for pal in pals:
if not self.decompress_gzip(cur_addr):
print 'something error'
return False
max_len = len(self.decomp_buf)
tile_size = self.TILE_W * self.TILE_H
tiles = [self.decomp_buf[off: off+tile_size] for off in xrange(0, max_len, tile_size)]
self.max_x = 512
self.max_y = len(self.decomp_buf) / self.max_x
if self.max_y == 272:
pixBuf = [0] * (480 * 272)
fromTile(pixBuf, self.max_x, self.max_y, 480, 272, tiles, pal)
print 'Create Regular Image(%d, %d) |' % (480, 272),
img = Image.new('RGBA', (480, 272))
else:
pixBuf = [0] * (self.max_x * self.max_y)
fromTile(pixBuf, self.max_x, self.max_y, self.max_x, self.max_y, tiles, pal)
print 'Create Regular Image(%d, %d) |' % (self.max_x, self.max_y),
img = Image.new('RGBA', (self.max_x, self.max_y))
img.putdata(tuple(pixBuf))
imgs.append(img)
else:
print 'not an Image, pass'
return
if self.type == 'FACE' or self.type == 'MAP' or self.type == 'REG':
f_name = self.curName+('.%s.png' % self.type)
print 'Save to %s' % f_name
img.save(os.path.join(self.curFolder, f_name))
elif self.type == 'REG_M':
for i, img in enumerate(imgs):
f_name = self.curName+('.%s.%02d.png' % (self.type, i) )
img.save(os.path.join(self.curFolder, f_name))
if self.type == 'FACE':
if not os.path.isdir(os.path.join(self.curFolder, 'face_data')):
os.mkdir(os.path.join(self.curFolder, 'face_data'))
img2.save(os.path.join(self.curFolder, 'face_data', self.curName+('.%s.ex.png' % self.type)))
def parse_from_png(self):
pass
if __name__ == '__main__':
ntime = time.time()
for curName in walk(ur'F:\学生会研究\testBin'):
with open(curName, 'rb') as fPtr:
ind = 0
fName = curName[curName.rindex('\\') + 1:]
folder = curName[:curName.rindex('\\')]
print 'Process %s | ' % fName,
img_parser = ImageParser(fPtr, folder, fName)
img_parser.parse_to_png()
print 'Total time: %lf' % (time.time() - ntime)

学生会立绘类图片拼图数据分析与导出

一、文件的主要结构

从cpk中解包出来的大部分图片类文件中只有一个文件。但是其中也有部分文件中含有多个图片。这点必须注意。

无论是哪一种图片,都有一个特点,调色板,head,数据并未压缩,但是具体的图像数据是经过gzip压缩的。gzip大数据块由多个小数据块组成,大数据块的起始位置是0x400对齐的。

在学生会游戏中捷报出来的图片,按照gzip数据块的位置分成两种。

  • 只有一个调色板的 [情况1]
start         len
0x0           0x400        256色RGBA调色板
0x400         0x1400       头信息,可为全0
0x1800        some Len     gzip 大数据块
  • 一个或者多个调色板的 [情况2]
start           len
0x0           0x400 * n     若干个256色RGBA调色板
0x400 * n     some Len      gzip大数据块1(一个大数据块对应一个调色板)
next addr     some Len      gzip大数据块2

二、gzip大数据块

gzip大数据块的主要构成如下

start                 len
0x0                    4          gzip小块个数
0x4 + 4 * i            4          第i个gzip小数据块的起始偏移
0x4 + 4 * i + 4        4          第i个gzip小数据块的终止偏移,也是i+1个小数据块起始偏移
addr[align=0x10]                  在最后一个偏移之后,下一个0x10的对齐的地址处是第一个小数据块位置

其中如果有n个小数据块,则会有n+1个偏移,最后一个偏移,是最后一个小数据块的终止位置的偏移。

对于每一个小数据块,其结构如下

start          len
0x0             4          gzip压缩块解压缩后块大小
0x10            4          1f 8b 08开始的gzip块

三、头信息(head)

对于第一部分中提及的[情况2],或者[情况1]中头信息全为0的情况,我们认为这种图片是不需要拼图的,认为其为[REG](Regular)图片。而有头信息的,我们则认为其为[MAP]图片。

1. REG图片

对于此类图片,就是简单的16*8的tile类型图片。但是由于未知宽高,只能靠数据推测。根据学生会游戏中所有导出的图片判断,所有的图片的宽度都为512,而高度可以通过数据区大小除以512得到。

reg image 对于高度为272大小的图片,一般为游戏的背景事件图,最右边的30*272应该被切割掉。

tile转化的主要代码,可以参见我写的fromTile

# tarW, tarH 目标图像大小,如512,272
# toW,  toH  是最终保存的图像大小,切割30*272后的480, 272
# tiles      16*8的tile数组
def fromTile(pixBuf, tarW, tarH, \
            toW, toH, \
             tiles, pal = 0):
        
        tileW = 16
        tileH = 8
        
        tileInW = tarW / tileW
        for picY in xrange(toH):
            for picX in xrange(toW):
                tileOut = picY / tileH * tileInW + picX / tileW
                tileIn  = picY % tileH * tileW + picX % tileW 
                if pal == 0:
                    pixBuf[picY*toW + picX] = tiles[tileOut][tileIn]
                else:
                    getInd = ord(tiles[tileOut][tileIn])
                    colBuf = pal[4 * getInd: 4 * getInd + 4]
                    pixBuf[picY*toW + picX] = tuple([ord(colCh) for colCh in colBuf])

2. MAP图片

此类图片的关键即是在0x400多了拼图数据。但是其中有部分数据是属于脸部表情,其具体的拼图坐标在文件中并不存在,只有后半部分的拼图信息,因此为了能正确处理,必须根据头信息讲前半部分的头部数据单独提取出来。

reg image

具体的头信息如下所示

0x400 
-----0x00    4d 41 50 00(MAP\0)
-----0x04    e4 00 (map区长度,从0x400开始)
-----0x06    28 00 (0x28(numPos) 坐标的个数,每个坐标数据4个字节)
-----0x08    20 00(0x20=32) 宽上32个拼图块
-----0x0a    10 00(0x10=16) 拼图块宽为16 (宽=32*16 = 512)
-----0x0c    03 00(0x03=3)  高上3个拼图块
-----0x0e    10 00(0x10=16) 拼图块高16 (高=3*16=48)(不含脸和嘴)
-----0x10    00 60 00 00 (0x6000 数据区总长度,包括前半部分脸和嘴,和后半部分)
-----0x14    00 02 00 00 (未知)
-----0x18    01 01 (未知)
-----0x1a    00 00 (有脸和嘴的非0,意义未知)
-----0x1c    00 02 (0x200=512,实际图片宽)
-----0x1e    80 00 (0x80=96, 实际图片高,包括前半部分脸和嘴,和后半部分) = val[0x0c] * val[0x0e] + val[0x22]
-----0x20    00 00 (未知)
-----0x22    50 00 (0x50=80,脸部分高度,无脸为0)
-----0x24    00 00 (未知)
-----0x26    10 00 (未知)
-----0x28    20 00 (未知)
-----0x2a    00 00 (未知)
-----0x2c    00 00 ff ff

-----0x30 起
-----====numPos个4字节数据
数据基本上市后面不变,前面变,下面是例子
X= 192 Y= 0
X= 224 Y= 0
X= 160 Y= 16
X= 192 Y= 16
X= 224 Y= 16
X= 160 Y= 32
X= 192 Y= 32        

前半部分就不细说了,基本上就跟上面描述的一样,可以参考下我的代码理解。最重要的是后半部分拼图数据的分析理解。

001 [x] = 0x0000, 0, 0[/16]	[y] = 0x0000, 0, 0[/16]
002 [x] = 0x0020, 32, 2[/16]	[y] = 0x0000, 0, 0[/16]
003 [x] = 0x0040, 64, 4[/16]	[y] = 0x0000, 0, 0[/16]
004 [x] = 0x0060, 96, 6[/16]	[y] = 0x0000, 0, 0[/16]
005 [x] = 0x0000, 0, 0[/16]	[y] = 0x0010, 16, 1[/16]
006 [x] = 0x0020, 32, 2[/16]	[y] = 0x0010, 16, 1[/16]
007 [x] = 0x0040, 64, 4[/16]	[y] = 0x0010, 16, 1[/16]
008 [x] = 0x0060, 96, 6[/16]	[y] = 0x0010, 16, 1[/16]
009 [x] = 0x0000, 0, 0[/16]	[y] = 0x0020, 32, 2[/16]
010 [x] = 0x0020, 32, 2[/16]	[y] = 0x0020, 32, 2[/16]
011 [x] = 0x0040, 64, 4[/16]	[y] = 0x0020, 32, 2[/16]
012 [x] = 0x0060, 96, 6[/16]	[y] = 0x0020, 32, 2[/16]
013 [x] = 0x0000, 0, 0[/16]	[y] = 0x0030, 48, 3[/16]
014 [x] = 0x0020, 32, 2[/16]	[y] = 0x0030, 48, 3[/16]
015 [x] = 0x0040, 64, 4[/16]	[y] = 0x0030, 48, 3[/16]
016 [x] = 0x0060, 96, 6[/16]	[y] = 0x0030, 48, 3[/16]
017 [x] = 0x0000, 0, 0[/16]	[y] = 0x0040, 64, 4[/16]
018 [x] = 0x0020, 32, 2[/16]	[y] = 0x0040, 64, 4[/16]
019 [x] = 0x0040, 64, 4[/16]	[y] = 0x0040, 64, 4[/16]
020 [x] = 0x0060, 96, 6[/16]	[y] = 0x0040, 64, 4[/16]

根据上面的例子,发现规律,y不动,x每次加32。y每次加16,x再从头开始变化,从这个结合下图ct2中的截图可以发现,每个坐标信息,代表由4个16*8的tile组成的32*16的tile,其x,y值即代表赐个tile在目标图片中的坐标值。

reg image

如何处理这种组合tile问题呢。我才用了这样一个思路

1.每次读取 两行 16*8 的tile, 共 64 * (16*8)

curFile.seek(0x1800)
lineLen = BASE_TILE_H * BASE_TILE_W * num_tile_w * (tile_h / BASE_TILE_H)
lineLen2 = BASE_TILE_H * BASE_TILE_W * num_tile_w
multiLines = []
for i in xrange(num_tile_h):
    multiLines.append(curFile.read(lineLen))

2.每两行tile,从左往右一次读入4个,合成为1个32*16,生成若干个32*16的tile

def getBigTile(mLine, big_tile_list, NUM_BT_W, line_len):
    curPos = 0
    for i in xrange(NUM_BT_W):
        blk = []
        blk.append(mLine[curPos: curPos + BASE_TILE_H * BASE_TILE_W])
        blk.append(mLine[curPos + BASE_TILE_H * BASE_TILE_W : curPos + BASE_TILE_H * BASE_TILE_W * 2])
        blk.append(mLine[line_len + curPos: line_len + curPos + BASE_TILE_H * BASE_TILE_W])
        blk.append(mLine[line_len + curPos + BASE_TILE_H * BASE_TILE_W : line_len + curPos + BASE_TILE_H * BASE_TILE_W * 2])
        pixBuf = [0] * (32 * 16)
        fromTile(pixBuf, 32, 16, blk)
        big_tile_list.append(pixBuf)
        curPos = curPos + BASE_TILE_H * BASE_TILE_W * 2

big_tile_list = []
for mLine in multiLines:
    getBigTile(mLine, big_tile_list, NUM_BT_W, lineLen/2)
  1. 将32*16的tile根据坐标信息,贴入目标图片
def mapTile(pixBuf, tarW, tarH, tile, pal):
        
        tileW = 32
        tileH = 16
        
        tileInW = tarW / tileW
        for picY in xrange(tarH, tarH + tileH):
            for picX in xrange(tarW, tarW + tileW):
                tileIn  = (picY - tarH) * tileW + (picX - tarW)
                getInd = ord(tile[tileIn])
                colBuf = pal[4 * getInd: 4 * getInd + 4]
                pixBuf[picX, picY] = tuple([ord(colCh) for colCh in colBuf])
for i, tile in enumerate(big_tile_list):
    if i == len(mapTable):
        break
    mapTile(pixImage, mapTable[i][0], mapTable[i][1], tile, pal)

依照此思路便可将图片拼起来,如果有脸部数据,则需要单独提取出来,按照[REG]图片的思路单独导出来即可。

reg image

下面是我自己讲脸部数据,和后半部分分开的结果。最后尝试人工合成了下,效果还不错

reg image

四、完整代码

ImageExport.py

五、导出资源分享

下载地址:http://pan.baidu.com/s/1o6ODNnG

pw: seitokai_stardrad

主要就是以前有人传过的CG,和新的立绘,表情类图片。缩略图如下:

reg image reg image reg image

####表情是单独导出的 reg image

拼图实验

reg image

看腿猜妹子

1

reg image

2

reg image

3

reg image

4

reg image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment