制作Pak文件的方法
制作Pak文件的方法
序言:由于前些时间,一些matrixer常问关于j2me中使用Pak文件的问题。本人虽学艺不深,但满怀热心的做了一番探索,现将制作Pak文件的看法和方法公布出来,大家多多提意见。
一、什么是Pak文件:
Pak文件就是将多个文件打包为一个单独文件,在这个文件中保存着多个文件的数据,当然还有一些描述文件结构的数据。所以将“Pak”作为文件的后缀是一种常规的用法,大家可以自定义其它的文件后缀。
二、为什么使用Pak文件:
由于MIDP对发布安装的j2me程序大小进行了限制,所以缩小发布程序就意味着能够提供更多的程序或者内容(如图片、音乐)给用户。而通过研究发现zip/jar算法对大文件的压缩率高于对等量的多个小文件的压缩率。
当然还有其它方法,这里简单做一下讨论比如使用混淆器ProGuard的“-overloadaggressively”选项使jar文件缩小,但也会导致一些错误,因为这种方法生成jar中的class符合java byte code标准,但是与java语法相悖,严重的可能造成一些jre对Object的序列化错误。
所以使用Pak方法将程序中要用到的资源(图片、音乐、文本)组合为单一文件是一个安全有效的方法。而且对于一些商用程序,完全可以在pak文件中对文件数据进行加密,很好的保护了作者和公司的权益。本人的sample中使用了简单的“加减法”加密,对于手机这类设备来讲是一个效率较高的选择。
三、Pak文件的结构:
大家可以自己设计Pak文件结构,本人这里只是抛砖引玉的作个sample。下面就是本人设计的Pak文件结构:
PAK File Header:Pak文件的头部
* 签名:6字节char数组 * 版本号:32位float * 文件table数量:32位整数 * 密码行为:8位字节 * 密码:8位字节 * 文件唯一ID:10字节char数组 * 保留位:32位整数(4字节)
File Table:Pak文件中包含文件的列表,在一个Pak文件中一个被包含的文件对应一个File Table。
* 文件名:30字节char数组 * 文件大小:32位整型 * 文件在pak文件中的位移:32位整数
Concatenated File Data:按File Table的顺序连接在一起的文件数据。
* 文件数据
四、程序框架:
说明:由于Pak文件的制作和使用分别要使用两个java应用领域:j2se和j2me,所以本人将PakUtil类制作了2个版本(j2se和j2me)。
程序框架如下:
1。PakHeader类,定义了Pak文件头。
2。PakFileTable类,定义Pak文件table。
3。PakUtil类(j2se版),具备两个功能:将多个png图片合成一个Pak文件,并使用简单的加减加密法对其进行加密;从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)或者写为文件。
PakUtil类(j2me版),具备的功能:从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)。
五、PakHeader和PakFileTable类:
PakHeader.java:
package cn.org.matrix.gmatrix.gameLab.util.pak; /** * Pak文件头: * 结构: * 签名:6字节char数组 * 版本号:32位float * 文件table数量:32位整数 * 密码行为:8位字节 * 密码:8位字节 * 文件唯一ID:10字节char数组 * 保留位:32位整数(4字节) * @author cleverpig * */ class PakHeader { //定义文件唯一ID长度 public static final int UNIQUEID_LENGTH=10; //定义文件签名长度 public static final int SIGNATURE_LENGTH=6; //定义加法运算 public static final int ADDITION_CIPHERACTION=0; //定义减法运算 public static final int SUBTRACT_CIHOERACTION=1; //文件签名 private char[] signature=new char[SIGNATURE_LENGTH]; //版本号 private float version=0f; //文件table数量 private long numFileTableEntries=0; //密码使用方法:在原数据上进行加法还是减法 private byte cipherAction=ADDITION_CIPHERACTION; //密码值 private byte cipherValue=0x00; //唯一ID private char[] uniqueID=new char[UNIQUEID_LENGTH]; //保留的4字节 private long reserved=0; public PakHeader(){ } /** * 构造方法 * @param signature 签名 * @param version 版本 * @param numFileTableEntries 文件table数量 * @param cipherAction 密码使用方法 * @param cipherValue 密码值 * @param uniqueID 唯一ID * @param reserved 保留的2字节 */ public PakHeader(char[] signature,float version, long numFileTableEntries,byte cipherAction, byte cipherValue,char[] uniqueID,long reserved){ for(int i=0;i<SIGNATURE_LENGTH;this.signature[i]=signature[i],i++) ; this.version=version; this.cipherAction=cipherAction; this.numFileTableEntries=numFileTableEntries; this.cipherValue=cipherValue; for(int i=0;i<UNIQUEID_LENGTH;this.uniqueID[i]=uniqueID[i],i++) ; this.reserved=reserved; } public byte getCipherValue() { return cipherValue; } public void setCipherValue(byte cipherValue) { this.cipherValue = cipherValue; } public long getNumFileTableEntries() { return numFileTableEntries; } public void setNumFileTableEntries(long numFileTableEntries) { this.numFileTableEntries = numFileTableEntries; } public long getReserved() { return reserved; } public void setReserved(long reserved) { this.reserved = reserved; } public char[] getUniqueID() { return uniqueID; } public void setUniqueID(char[] uniqueID) { for(int i=0;i<UNIQUEID_LENGTH;this.uniqueID[i]=uniqueID[i],i++) ; } public float getVersion() { return version; } public void setVersion(float version) { this.version = version; } public byte getCipherAction() { return cipherAction; } public void setCipherAction(byte cipherAction) { this.cipherAction = cipherAction; } public char[] getSignature() { return signature; } public void setSignature(char[] signature) { for(int i=0;i<SIGNATURE_LENGTH;this.signature[i] = signature[i],i++) ; } /** * 返回PakHeader的大小 * @return 返回PakHeader的大小 */ public static int size(){ return SIGNATURE_LENGTH+4+4+1+1+UNIQUEID_LENGTH+4; } public String toString(){ String result=""; result+="/t签名:"+new String(this.signature).trim() +"/t版本号:"+this.version +"/t文件table数量:"+this.numFileTableEntries +"/t密码行为:" +this.cipherAction +"/t密码:"+this.cipherValue +"/t文件唯一ID:"+new String(this.uniqueID).trim() +"/t保留位:"+this.reserved; return result; } }
PakFileTable.java
package cn.org.matrix.gmatrix.gameLab.util.pak; /** * Pak文件table类 * 文件table结构: * 文件名:30字节char数组 * 文件大小:32位整型 * 文件在pak文件中的位移:32位整数 * @author cleverpig * */ class PakFileTable { public static final int FILENAME_LENGTH=30; //文件名 private char[] fileName=new char[FILENAME_LENGTH]; //文件大小 private long fileSize=0L; //文件在pak文件中的位移 private long offSet=0L; public PakFileTable(){ } /** * 构造方法 * @param fileName 文件名 * @param fileSize 文件大小 * @param offSet 文件在Pak文件中的位移 */ public PakFileTable(char[] fileName, long fileSize,long offSet){ for(int i=0;i<FILENAME_LENGTH;this.fileName[i]=fileName[i],i++) ; this.fileSize=fileSize; this.offSet=offSet; } public char[] getFileName() { return fileName; } public void setFileName(char[] fileName) { for(int i=0;i<fileName.length;this.fileName[i]=fileName[i],i++) ; } public long getFileSize() { return fileSize; } public void setFileSize(long fileSize) { this.fileSize = fileSize; } public long getOffSet() { return offSet; } public void setOffSet(long offSet) { this.offSet = offSet; } /** * 返回文件Table的大小 * @return 返回文件Table的大小 */ public static int size(){ return FILENAME_LENGTH+4+4; } public String toString(){ return "/t文件名:"+new String(this.fileName).trim() +"/t文件大小:"+this.fileSize +"/t文件位移:"+this.offSet; } }
六、PakUtil类(j2se版):
PakUtil.java
package cn.org.matrix.gmatrix.gameLab.util.pak; import java.io.*; import java.util.Vector; /** * Pak工具类 * 功能: * 1.将多个png图片合成一个Pak文件,并使用简单的加减加密法对其进行加密; * 2.从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)或者写为文件 * @author cleverpig * */ public class PakUtil { public PakUtil(){ } /** * 返回文件长度 * @param filePath 文件路径 * @return 文件长度 */ private long getFileSize(String filePath){ File file=new File(filePath); return file.length(); } /** * 返回文件名 * @param filePath 文件路径 * @return 文件名 */ private String getFileName(String filePath){ File file=new File(filePath); return file.getName(); } /** * 计算文件位移的起始点 * @return 文件位移的起始点 */ private long workOutOffsetStart(PakHeader header){ //计算出文件头+文件table的长度 return PakHeader.size()+header.getNumFileTableEntries()*PakFileTable.size(); } /** * 计算文件位移 * @param fileIndex 文件序号 * @param lastFileOffset 上一个文件位移 * @return文件在pak文件中的位移 */ private long workOutNextOffset(long sourceFileSize,long lastFileOffset){ return lastFileOffset+sourceFileSize; } /** * 生成文件table * @param sourceFileName 源文件名 * @param sourceFileSize 源文件长度 * @param currentFileOffset 当前文件位移 * @return 生成的PakFileTable对象 */ private PakFileTable generateFileTable(String sourceFileName, long sourceFileSize,long currentFileOffset){ PakFileTable ft=new PakFileTable(); ft.setFileName(sourceFileName.toCharArray()); ft.setFileSize(sourceFileSize); ft.setOffSet(currentFileOffset); return ft; } /** * 将char字符数组写入到DataOutputStream中 * @param toWriteCharArray 被写入的char数组 * @param dos DataOutputStream * @throws Exception */ private void writeCharArray(char[] toWriteCharArray,DataOutputStream dos) throws Exception{ for(int i=0;i<toWriteCharArray.length;dos.writeChar(toWriteCharArray[i]),i++); } /** * 使用文件头中的密码对数据进行加密 * @param buff 被加密的数据 * @param buffLength 数据的长度 * @param header 文件头 */ private void encryptBuff(byte[] buff,int buffLength,PakHeader header){ for(int i=0;i<buffLength;i++){ switch(header.getCipherAction()){ case PakHeader.ADDITION_CIPHERACTION: buff[i]+=header.getCipherValue(); break; case PakHeader.SUBTRACT_CIHOERACTION: buff[i]-=header.getCipherValue(); break; } } } /** * 使用文件头中的密码对数据进行解密 * @param buff 被解密的数据 * @param buffLength 数据的长度 * @param header 文件头 */ private void decryptBuff(byte[] buff,int buffLength,PakHeader header){ for(int i=0;i<buffLength;i++){ switch(header.getCipherAction()){ case PakHeader.ADDITION_CIPHERACTION: buff[i]-=header.getCipherValue(); break; case PakHeader.SUBTRACT_CIHOERACTION: buff[i]+=header.getCipherValue(); break; } } } /** * 制作Pak文件 * @param sourceFilePath 源文件路径数组 * @param destinateFilePath 目的文件路径(Pak文件) * @param cipherAction 密码行为 * @param cipherValue 密码 * @throws Exception */ public void makePakFile(String[] sourceFilePath, String destinateFilePath,PakHeader header) throws Exception{ PakFileTable[] fileTable=new PakFileTable[sourceFilePath.length]; //计算文件位移起始点 long fileOffset=workOutOffsetStart(header); //逐个建立文件table for(int i=0;i<sourceFilePath.length;i++){ String sourceFileName=getFileName(sourceFilePath[i]); long sourceFileSize=getFileSize(sourceFilePath[i]); PakFileTable ft=generateFileTable(sourceFileName,sourceFileSize,fileOffset); //计算下一个文件位移 fileOffset=workOutNextOffset(sourceFileSize,fileOffset); fileTable[i]=ft; } //写入文件头 File wFile=new File(destinateFilePath); FileOutputStream fos=new FileOutputStream(wFile); DataOutputStream dos=new DataOutputStream(fos); writeCharArray(header.getSignature(),dos); dos.writeFloat(header.getVersion()); dos.writeLong(header.getNumFileTableEntries()); dos.writeByte(header.getCipherAction()); dos.writeByte(header.getCipherValue()); writeCharArray(header.getUniqueID(),dos); dos.writeLong(header.getReserved()); //写入文件table for(int i=0;i<fileTable.length;i++){ writeCharArray(fileTable[i].getFileName(),dos); dos.writeLong(fileTable[i].getFileSize()); dos.writeLong(fileTable[i].getOffSet()); } //写入文件数据 for(int i=0;i<fileTable.length;i++){ File ftFile=new File(sourceFilePath[i]); FileInputStream ftFis=new FileInputStream(ftFile); DataInputStream ftDis=new DataInputStream(ftFis); byte[] buff=new byte[256]; int readLength=0; while((readLength=ftDis.read(buff))!=-1){ encryptBuff(buff,readLength,header); dos.write(buff,0,readLength); } ftDis.close(); ftFis.close(); } dos.close(); } /** * 从DataInputStream读取char数组 * @param dis DataInputStream * @param readLength 读取长度 * @return char数组 * @throws Exception */ private char[] readCharArray(DataInputStream dis,int readLength) throws Exception{ char[] readCharArray=new char[readLength]; for(int i=0;i<readLength;i++){ readCharArray[i]=dis.readChar(); } return readCharArray; } /** * 从PAK文件中读取文件头 * @param dis DataInputStream * @return PakHeader * @throws Exception */ private PakHeader readHeader(DataInputStream dis) throws Exception{ PakHeader header=new PakHeader(); char[] signature=readCharArray(dis,PakHeader.SIGNATURE_LENGTH); header.setSignature(signature); header.setVersion(dis.readFloat()); header.setNumFileTableEntries(dis.readLong()); header.setCipherAction(dis.readByte()); header.setCipherValue(dis.readByte()); char[] uniqueID=readCharArray(dis,PakHeader.UNIQUEID_LENGTH); header.setUniqueID(uniqueID); header.setReserved(dis.readLong()); return header; } /** * 读取所有的文件table * @param dis DataInputStream * @param fileTableNumber 文件表总数 * @return 文件table数组 * @throws Exception */ private PakFileTable[] readFileTable(DataInputStream dis,int fileTableNumber) throws Exception{ PakFileTable[] fileTable=new PakFileTable[fileTableNumber]; for(int i=0;i<fileTableNumber;i++){ PakFileTable ft=new PakFileTable(); ft.setFileName(readCharArray(dis,PakFileTable.FILENAME_LENGTH)); ft.setFileSize(dis.readLong()); ft.setOffSet(dis.readLong()); fileTable[i]=ft; } return fileTable; } /** * 从pak文件读取文件到byte数组 * @param dis DataInputStream * @param fileTable PakFileTable * @return byte数组 * @throws Exception */ private byte[] readFileFromPak(DataInputStream dis,PakHeader header,PakFileTable fileTable) throws Exception{ dis.skip(fileTable.getOffSet()-workOutOffsetStart(header)); // int fileLength=(int)fileTable.getFileSize(); byte[] fileBuff=new byte[fileLength]; int readLength=dis.read(fileBuff,0,fileLength); if (readLength<fileLength){ System.out.println("读取数据长度不正确"); return null; } else{ decryptBuff(fileBuff,readLength,header); return fileBuff; } } /** * 将buffer中的内容写入到文件 * @param fileBuff 保存文件内容的buffer * @param fileName 文件名 * @param extractDir 文件导出目录 * @throws Exception */ private void writeFileFromByteBuffer(byte[] fileBuff,String fileName,String extractDir) throws Exception{ String extractFilePath=extractDir+fileName; File wFile=new File(extractFilePath); FileOutputStream fos=new FileOutputStream(wFile); DataOutputStream dos=new DataOutputStream(fos); dos.write(fileBuff); dos.close(); fos.close(); } /** * 从pak文件中取出指定的文件到byte数组,如果需要的话可以将byte数组写为文件 * @param pakFilePathpak文件路径 * @param extractFileName pak文件中将要被取出的文件名 * @param writeFile 是否需要将byte数组写为文件 * @param extractDir 如果需要的话可以将byte数组写为文件,extractDir为取出数据被写的目录文件 * @return byte数组 * @throws Exception */ public byte[] extractFileFromPak(String pakFilePath, String extractFileName,boolean writeFile,String extractDir) throws Exception{ File rFile=new File(pakFilePath); FileInputStream fis=new FileInputStream(rFile); DataInputStream dis=new DataInputStream(fis); PakHeader header=readHeader(dis); PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries()); boolean find=false; int fileIndex=0; for(int i=0;i<fileTable.length;i++){ String fileName=new String(fileTable[i].getFileName()).trim(); if (fileName.equals(extractFileName)){ find=true; fileIndex=i; break; } } if (find==false){ System.out.println("没有找到指定的文件"); return null; } else{ byte[] buff=readFileFromPak(dis,header,fileTable[fileIndex]); if (writeFile){ writeFileFromByteBuffer(buff,extractFileName,extractDir); } else{ dis.close(); fis.close(); } return buff; } } /** * 从pak文件中取出指定的Pak文件的信息 * @param pakFilePathpak文件路径 * @return 装载文件头和文件table数组的Vector * @throws Exception */ public Vector showPakFileInfo(String pakFilePath) throws Exception{ File rFile=new File(pakFilePath); FileInputStream fis=new FileInputStream(rFile); DataInputStream dis=new DataInputStream(fis); PakHeader header=readHeader(dis); PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries()); Vector result=new Vector(); result.add(header); result.add(fileTable); return result; } public static void main(String[] argv) throws Exception{ PakUtil pu=new PakUtil(); //构造文件头 char[] signature=new char[PakHeader.SIGNATURE_LENGTH]; signature=new String("012345").toCharArray(); char[] uniqueID=new char[PakHeader.UNIQUEID_LENGTH]; uniqueID=new String("0123456789").toCharArray(); PakHeader header=new PakHeader(); header.setSignature(signature); header.setNumFileTableEntries(3); header.setCipherAction((byte)PakHeader.ADDITION_CIPHERACTION); header.setCipherValue((byte)0x0f); header.setUniqueID(uniqueID); header.setVersion(1.0f); header.setReserved(0L); String[] filePathArray={"F://eclipse3.1RC3//workspace//gmatrixProject_j2se//testFiles//apple.png", "F://eclipse3.1RC3//workspace//gmatrixProject_j2se//testFiles//cushaw.png", "F://eclipse3.1RC3//workspace//gmatrixProject_j2se//testFiles//flash.png"}; String extractFilePath="F://eclipse3.1RC3//workspace//gmatrixProject_j2se//testFiles//test.pak"; //制作Pak文件 System.out.println("制作Pak文件..."); pu.makePakFile(filePathArray,extractFilePath,header); System.out.println("制作Pak文件完成"); //从Pak文件中取出所有的图片文件 Vector pakInfo=pu.showPakFileInfo(extractFilePath); header=(PakHeader)pakInfo.elementAt(0);