简单描述一下总的过程:在某个后台上(版本发布平台)上传原始的ipa文件,解析ipa(主要是解析info.plist,从中获取软件名、版本、icons等;解析embedded.mobileprovision,获取证书过期时间),生成一个新的plist文件,最终将ipa、新的plist、图标上传至发布服务器。这个plist文件里面会指向这个ipa的地址,最终在Safari上访问 itms-services://?action=download-manifest&url=plist文件 就可以下载IOS应用了。
下面先来看看最重要的plist文件.
items
assets
kind
software-package
url
https://uc-download.xxx.com/api/v1/file/download/5194c4cfcb3d4c128c51b9e3138ca8e8
kind
full-size-image
needs-shine
url
https://uc-download.xxx.com/api/v1/file/download/d11184a8a9184c00836f86e14ed11d10
kind
display-image
needs-shine
url
https://uc-download.xxx.com/api/v1/file/download/d11184a8a9184c00836f86e14ed11d10
metadata
bundle-identifier
enterprise.xxx.Odin-UC
bundle-version
2.0.0.66
kind
software
title
UME
一、服务器
首先,我们需要一台服务器,并且有SSL证书。这是因为最终在Safari上访问plist文件需要。
二、解析IPA
FileService这里主要用到就是在本地构造临时文件,ClientVersionRecord是数据库存储的版本实体对象,比较简单,不再赘述。
controller入口可以见:https://blog.csdn.net/Mr_EvanChen/article/details/106937688
/**
* 解析ios安装包,后缀名为ipa
*/
@Service
public class IOSPacketParser extends InstallPacketParser {
private static final Logger logger = LoggerFactory.getLogger(IOSPacketParser.class);
@Autowired
private FileService fileService;
@Autowired
private RestTemplate restTemplate;
@Autowired
private IosIntranetService iosIntranetService;
@Override
public ClientType getClientType() {
return ClientType.IOS;
}
@Override
public ClientVersionRecord parse(File ipaFile, ClientVersionRecord version) throws Exception {
String intranetIpaUrl = version.getIpaDownloadUrl();
String intranetUrl = intranetIpaUrl.substring(0, intranetIpaUrl.lastIndexOf("/") + 1);
IPA ipa = iosIntranetService.transFile2IPA(ipaFile);
String ipaFileName = ipaFile.getName();
ipaFileName = ipaFileName.substring(0, ipaFileName.lastIndexOf("."));
//生成icon
String iconUrl = buildIcon(ipaFile, ipa, intranetUrl + ipaFileName);
//生成plist
File plistFile = iosIntranetService.createPlistFile(ipa, intranetIpaUrl, iconUrl, ipaFileName);
String plistUrl = iosIntranetService.uploadPlistAsFile(plistFile, intranetUrl + plistFile.getName());
version.setMd5(FileUtil.md5(ipaFile))
.setFileSize(ipaFile.length())
.setClientVersion(ipa.getCFBundleVersion())
.setIconUrl(iconUrl)
.setIntranetDownloadUrl("itms-services://?action=download-manifest&url=" + plistUrl);
logger.info("parse [ipa] file, version: {}", JSONUtil.serialize(version));
return version;
}
//----------------------------------private-----------------------------------------
private String buildIcon(File ipaFile, IPA ipa, String intranetIconUrl) {
String iconName = getMaxIcon(ipa);
File oriIcon = null;
File tmpDecodedIcon = null;
try {
oriIcon = readIcons(ipaFile, iconName);
tmpDecodedIcon = new File(fileService.getTempDir() + "decoded-" + oriIcon.getName());
new IPngConverter(oriIcon, tmpDecodedIcon).convert();
String oriIconName = oriIcon.getName();
intranetIconUrl = intranetIconUrl + StringPool.DOT + oriIconName.substring(oriIconName.lastIndexOf(StringPool.DOT) + 1);
restTemplate.put(intranetIconUrl, new FileSystemResource(tmpDecodedIcon));
} catch (Exception e) {
logger.info("上传icon至内网失败,结果:{}", intranetIconUrl);
} finally {
if (oriIcon != null) {
logger.info("删除临时文件,结果:{}", oriIcon.delete());
logger.info("删除临时文件,结果:{}", oriIcon.getParentFile().delete());
}
if (tmpDecodedIcon != null) {
logger.info("删除临时文件,结果:{}",tmpDecodedIcon.delete());
logger.info("删除临时文件,结果:{}",tmpDecodedIcon.getParentFile().delete());
}
}
return intranetIconUrl;
}
private File readIcons(File ipaFile, String icon) {
if (StringUtil.isNotBlank(icon)) {
icon = "AppIcon";
}
long maxSize = Long.MIN_VALUE;
ZipEntry maxEntry = null;
try (ZipFile zipFile = new ZipFile(ipaFile)) {
Enumeration e = zipFile.entries();
while (e.hasMoreElements()) {
ZipEntry ze = (ZipEntry) e.nextElement();
if (ze.isDirectory()) {
continue;
}
String name = ze.getName();
long size = ze.getSize();
if (name != null && name.contains(icon) && size > maxSize) {
maxSize = size;
maxEntry = ze;
}
}
if (maxEntry != null) {
return fileService.buildTmpFile(zipFile.getInputStream(maxEntry), FileNameUtil.getName(maxEntry.getName()));
}
} catch (IOException e) {
throw new InternalServerException("parse error");
}
throw new ResourceNotFoundException("icon not found");
}
private static final Pattern pattern = Pattern.compile("[+-]?\\d+(\\.\\d+)?x[+-]?\\d+(\\.\\d+)?");
/**
* 获取最大的图标,根据图标名称解析
*
* @param ipa ipa信息
* @return 最大图标名称
*/
private static String getMaxIcon(IPA ipa) {
if (isEmpty(ipa.getIcons())) {
return null;
}
double maxSize = Double.MIN_VALUE;
String iconName = null;
for (String icon : ipa.getIcons()) {
Matcher matcher = pattern.matcher(icon);
if (matcher.find()) {
try {
String[] sizes = matcher.group(0).split("x");
Double d = Double.valueOf(sizes[0]);
if (d > maxSize) {
maxSize = d;
iconName = icon;
}
} catch (Exception e) {
logger.error("parse size error", e);
}
}
}
return iconName;
}
public static void main(String[] args) {
logger.info(getMaxIcon(new IPA().setIcons(Arrays.asList("AppIcon20x20", "AppIcon29x29", "AppIcon40x40", "AppIcon60x60"))));
}
}
IosIntranetService接口及其实现类如下。
public interface IosIntranetService {
String handleIPAFile(File ipaFile, ClientVersionProtocol.Download.Input input, String uploadUrl) throws Exception;
/**
* ipa文件解析成对象
* @param ipaFile
* @return
*/
IPA transFile2IPA(File ipaFile) throws Exception;
/**
* 从ipa对象和两个地址创建出Plist对象
* @param ipa
* @param ipaUrl
* @param iconUrl
* @return
*/
File createPlistFile(IPA ipa, String ipaUrl, String iconUrl,String plistName);
/**
* 上传Plist文件,返回uri
* @param plistFile
* @param uploadUrl
* @return
*/
String uploadPlistAsFile(File plistFile,String uploadUrl);
}
@Service
public class IosIntranetServiceImp implements IosIntranetService {
Logger logger = LoggerFactory.getLogger(IosIntranetService.class);
private Configuration configuration;
@Autowired
private RestTemplate restTemplate;
@Value("${com.xxx.uc.manager.tmp.dir}")
private String tmpDir;
@PostConstruct
private void init() {
configuration = new Configuration(Configuration.VERSION_2_3_23);
configuration.setTemplateLoader(new ClassTemplateLoader(this.getClass(), "/template/"));
}
@Override
public String handleIPAFile(File ipaFile, ClientVersionProtocol.Download.Input input, String uploadUrl) throws Exception {
String localIconUrl = uploadImageFile(input.getIconUrl(), uploadUrl);
IPA ipa = transFile2IPA(ipaFile);
String ipaFileName = ipaFile.getName();
ipaFileName = ipaFileName.substring(0, ipaFileName.lastIndexOf("."));
File plistFile = createPlistFile(ipa, uploadUrl + ipaFile.getName(), localIconUrl, ipaFileName);
return uploadPlistAsFile(plistFile, uploadUrl + plistFile.getName());
}
@Override
public IPA transFile2IPA(File ipaFile) throws Exception {
return IPAUtil.readIPA(ipaFile);
}
@Override
public File createPlistFile(IPA ipa, String ipaUrl, String iconUrl, String plistName) {
Plist plist = new Plist(ipa)
.setFullSizeImageUrl(iconUrl)
.setDisplayImageUrl(iconUrl)
.setIpaUrl(ipaUrl);
File plistFile = new File(tmpDir + File.separator + plistName + ".plist");
try {
getTemplate().process(plist, new FileWriter(plistFile));
} catch (TemplateException e) {
logger.error("freemarker生成pList文件出现异常:{}", e);
} catch (IOException e) {
logger.error("生成pList临时文件出现异常:{}", e);
}
return plistFile;
}
public String uploadPlistAsFile(File plistFile, String uploadUrl) {
try {
restTemplate.put(uploadUrl, new FileSystemResource(plistFile));
} catch (Exception e) {
logger.error("上传plist{}至内网服务器{}失败!{}", plistFile.getName(), uploadUrl, Throwables.getStackTraceAsString(e));
// 上传失败
return null;
} finally {
FileUtils.deleteQuietly(plistFile);
}
return uploadUrl;
}
/**
* 这个上传url没添加文件名
*
* @param originalUrl
* @param uploadUrl
* @return
*/
public String uploadImageFile(String originalUrl, String uploadUrl) {
String filename = originalUrl.substring(originalUrl.lastIndexOf("/"));
File rcvFile = new File(tmpDir + File.separator + filename);
try {
//创建目录
// 从公网下载至本地
try {
restTemplate.execute(originalUrl, HttpMethod.GET, null, response -> {
if (response.getStatusCode() == HttpStatus.OK) {
FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));
return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(rcvFile);
} else {
return null;
}
});
} catch (Exception e) {
logger.error("从公网下载图标文件{}失败!{}", rcvFile.getName(), Throwables.getStackTraceAsString(e));
}
restTemplate.put(uploadUrl + filename, new FileSystemResource(rcvFile));
} catch (Exception e) {
logger.error("上传图标文件{}至内网服务器{}失败!{}", filename, uploadUrl, Throwables.getStackTraceAsString(e));
// 上传失败
return null;
} finally {
FileUtils.deleteQuietly(rcvFile);
}
return uploadUrl + filename;
}
protected Template getTemplate() throws IOException {
return configuration.getTemplate("plist.xml");
}
}
Plist对象如下。
public class Plist {
private String ipaUrl;
private String fullSizeImageUrl;
private String displayImageUrl;
private String bundleIdentifier;
private String bundleVersion;
private String title;
public Plist() {
}
public Plist(IPA ipa) {
this.bundleIdentifier = ipa.getCFBundleIdentifier();
this.bundleVersion = ipa.getCFBundleVersion();
this.title = ipa.getCFBundleDisplayName();
}
}
Plist模板可放在resource下,模板如下。
items assets kind software-package url ${ipaUrl} kind full-size-image needs-shine url ${fullSizeImageUrl} kind display-image needs-shine url ${displayImageUrl} metadata bundle-identifier ${bundleIdentifier} bundle-version ${bundleVersion} kind software title ${title}
IPngConverter、PNGTrunk、PNGIHDRTrunk如下。
import com.jcraft.jzlib.Deflater;
import com.jcraft.jzlib.GZIPException;
import com.jcraft.jzlib.Inflater;
import com.jcraft.jzlib.JZlib;
import java.io.*;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.zip.CRC32;
/**
* @author Rex
*/
public class IPngConverter {
private static final Logger log = Logger.getLogger(IPngConverter.class.getName());
private final File source;
private final File target;
private ArrayList trunks = null;
public static void main(String[] args) throws Exception {
log.info("===");
File file = new File("E:\\needFixPng");
for (File file1 : file.listFiles()) {
try {
new IPngConverter(file1, new File("E:\\fixed\\" + file1.getName()))
.convert();
} catch (Exception e) {
log.info(e +"");
}
}
}
public IPngConverter(File source, File target) {
if (source == null) throw new NullPointerException("'source' cannot be null");
if (target == null) throw new NullPointerException("'target' cannot be null");
this.source = source;
this.target = target;
}
private File getTargetFile(File convertedFile) throws IOException {
if (source.isFile()) {
if (target.isDirectory()) {
return new File(target, source.getName());
} else {
return target;
}
} else { // source is a directory
if (target.isFile()) { // single existing target
return target;
} else { // otherwise reconstruct a similar directory structure
if (!target.isDirectory() && !target.mkdirs()) {
throw new IOException("failed to create folder " + target.getAbsolutePath());
}
Path relativeConvertedPath = source.toPath().relativize(convertedFile.toPath());
File targetFile = new File(target, relativeConvertedPath.toString());
File targetFileDir = targetFile.getParentFile();
if (targetFileDir != null && !targetFileDir.exists() && !targetFileDir.mkdirs()) {
throw new IOException("unable to create folder " + targetFileDir.getAbsolutePath());
}
return targetFile;
}
}
}
public void convert() throws IOException {
convert(source);
}
private boolean isPngFileName(File file) {
return file.getName().toLowerCase().endsWith(".png");
}
private PNGTrunk getTrunk(String szName) {
if (trunks == null) {
return null;
}
PNGTrunk trunk;
for (int n = 0; n < trunks.size(); n++) {
trunk = trunks.get(n);
if (trunk.getName().equalsIgnoreCase(szName)) {
return trunk;
}
}
return null;
}
private void convertPngFile(File pngFile, File targetFile) throws IOException {
readTrunks(pngFile);
if (getTrunk("CgBI") != null) {
// Convert data
PNGIHDRTrunk ihdrTrunk = (PNGIHDRTrunk) getTrunk("IHDR");
log.fine("Width:" + ihdrTrunk.m_nWidth + " Height:" + ihdrTrunk.m_nHeight);
int nMaxInflateBuffer = 4 * (ihdrTrunk.m_nWidth + 1) * ihdrTrunk.m_nHeight;
byte[] outputBuffer = new byte[nMaxInflateBuffer];
convertDataTrunk(ihdrTrunk, outputBuffer, nMaxInflateBuffer);
writePng(targetFile);
} else {
// Likely a standard PNG: just copy
byte[] buffer = new byte[1024];
int bytesRead;
InputStream inputStream = new FileInputStream(pngFile);
try {
OutputStream outputStream = new FileOutputStream(targetFile);
try {
while ((bytesRead = inputStream.read(buffer)) >= 0) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
} finally {
outputStream.close();
}
} finally {
inputStream.close();
}
}
}
private long inflate(byte[] conversionBuffer, int nMaxInflateBuffer) throws GZIPException {
Inflater inflater = new Inflater(-15);
for (PNGTrunk dataTrunk : trunks) {
if (!"IDAT".equalsIgnoreCase(dataTrunk.getName())) continue;
inflater.setInput(dataTrunk.getData(), true);
}
inflater.setOutput(conversionBuffer);
int nResult;
try {
nResult = inflater.inflate(JZlib.Z_NO_FLUSH);
checkResultStatus(nResult);
} finally {
inflater.inflateEnd();
}
if (inflater.getTotalOut() > nMaxInflateBuffer) {
log.fine("PNGCONV_ERR_INFLATED_OVER");
}
return inflater.getTotalOut();
}
private Deflater deflate(byte[] buffer, int length, int nMaxInflateBuffer) throws GZIPException {
Deflater deflater = new Deflater();
deflater.setInput(buffer, 0, length, false);
int nMaxDeflateBuffer = nMaxInflateBuffer + 1024;
byte[] deBuffer = new byte[nMaxDeflateBuffer];
deflater.setOutput(deBuffer);
deflater.deflateInit(JZlib.Z_BEST_COMPRESSION);
int nResult = deflater.deflate(JZlib.Z_FINISH);
checkResultStatus(nResult);
if (deflater.getTotalOut() > nMaxDeflateBuffer) {
throw new GZIPException("deflater output buffer was too small");
}
return deflater;
}
private void checkResultStatus(int nResult) throws GZIPException {
switch (nResult) {
case JZlib.Z_OK:
case JZlib.Z_STREAM_END:
break;
case JZlib.Z_NEED_DICT:
throw new GZIPException("Z_NEED_DICT - " + nResult);
case JZlib.Z_DATA_ERROR:
throw new GZIPException("Z_DATA_ERROR - " + nResult);
case JZlib.Z_MEM_ERROR:
throw new GZIPException("Z_MEM_ERROR - " + nResult);
case JZlib.Z_STREAM_ERROR:
throw new GZIPException("Z_STREAM_ERROR - " + nResult);
case JZlib.Z_BUF_ERROR:
throw new GZIPException("Z_BUF_ERROR - " + nResult);
default:
throw new GZIPException("inflater error: " + nResult);
}
}
private boolean convertDataTrunk(
PNGIHDRTrunk ihdrTrunk, byte[] conversionBuffer, int nMaxInflateBuffer)
throws IOException {
log.fine("converting colors");
long inflatedSize = inflate(conversionBuffer, nMaxInflateBuffer);
// Switch the color
int nIndex = 0;
byte nTemp;
for (int y = 0; y < ihdrTrunk.m_nHeight; y++) {
nIndex++;
for (int x = 0; x < ihdrTrunk.m_nWidth; x++) {
nTemp = conversionBuffer[nIndex];
conversionBuffer[nIndex] = conversionBuffer[nIndex + 2];
conversionBuffer[nIndex + 2] = nTemp;
nIndex += 4;
}
}
Deflater deflater = deflate(conversionBuffer, (int) inflatedSize, nMaxInflateBuffer);
// Put the result in the first IDAT chunk (the only one to be written out)
PNGTrunk firstDataTrunk = getTrunk("IDAT");
CRC32 crc32 = new CRC32();
crc32.update(firstDataTrunk.getName().getBytes());
crc32.update(deflater.getNextOut(), 0, (int) deflater.getTotalOut());
long lCRCValue = crc32.getValue();
firstDataTrunk.m_nData = deflater.getNextOut();
firstDataTrunk.m_nCRC[0] = (byte) ((lCRCValue & 0xFF000000) >> 24);
firstDataTrunk.m_nCRC[1] = (byte) ((lCRCValue & 0xFF0000) >> 16);
firstDataTrunk.m_nCRC[2] = (byte) ((lCRCValue & 0xFF00) >> 8);
firstDataTrunk.m_nCRC[3] = (byte) (lCRCValue & 0xFF);
firstDataTrunk.m_nSize = (int) deflater.getTotalOut();
return false;
}
private void writePng(File newFileName) throws IOException {
FileOutputStream outStream = new FileOutputStream(newFileName);
try {
byte[] pngHeader = {-119, 80, 78, 71, 13, 10, 26, 10};
outStream.write(pngHeader);
boolean dataWritten = false;
for (PNGTrunk trunk : trunks) {
// Skip Apple specific and misplaced CgBI chunk
if (trunk.getName().equalsIgnoreCase("CgBI")) {
continue;
}
// Only write the first IDAT chunk as they have all been put together now
if ("IDAT".equalsIgnoreCase(trunk.getName())) {
if (dataWritten) {
continue;
} else {
dataWritten = true;
}
}
trunk.writeToStream(outStream);
}
outStream.flush();
} finally {
outStream.close();
}
}
private void readTrunks(File pngFile) throws IOException {
DataInputStream input = new DataInputStream(new FileInputStream(pngFile));
try {
byte[] nPNGHeader = new byte[8];
input.readFully(nPNGHeader);
boolean bWithCgBI = false;
trunks = new ArrayList();
if ((nPNGHeader[0] == -119) && (nPNGHeader[1] == 0x50) && (nPNGHeader[2] == 0x4e) && (nPNGHeader[3] == 0x47)
&& (nPNGHeader[4] == 0x0d) && (nPNGHeader[5] == 0x0a) && (nPNGHeader[6] == 0x1a) && (nPNGHeader[7] == 0x0a)) {
PNGTrunk trunk;
do {
trunk = PNGTrunk.generateTrunk(input);
trunks.add(trunk);
if (trunk.getName().equalsIgnoreCase("CgBI")) {
bWithCgBI = true;
}
}
while (!trunk.getName().equalsIgnoreCase("IEND"));
}
} finally {
input.close();
}
}
private void convertDirectory(File dir) throws IOException {
for (File file : dir.listFiles()) {
convert(file);
}
}
private void convert(File sourceFile) throws IOException {
if (sourceFile.isDirectory()) {
convertDirectory(sourceFile);
} else if (isPngFileName(sourceFile)) {
File targetFile = getTargetFile(sourceFile);
log.fine("converting " + sourceFile.getPath() + " --> " + targetFile.getPath());
convertPngFile(sourceFile, targetFile);
}
}
}
import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author Rex
*/
public class PNGTrunk {
protected int m_nSize;
protected String m_szName;
protected byte[] m_nData;
protected byte[] m_nCRC;
public static PNGTrunk generateTrunk(DataInputStream input) throws IOException {
int nSize = readPngInt(input);
byte[] nData = new byte[4];
input.readFully(nData);
String szName = new String(nData, "ASCII");
byte[] nDataBuffer = new byte[nSize];
input.readFully(nDataBuffer);
byte[] nCRC = new byte[4];
input.readFully(nCRC);
if (szName.equalsIgnoreCase("IHDR")) {
return new PNGIHDRTrunk(nSize, szName, nDataBuffer, nCRC);
}
return new PNGTrunk(nSize, szName, nDataBuffer, nCRC);
}
protected PNGTrunk(int nSize, String szName, byte[] nCRC) {
m_nSize = nSize;
m_szName = szName;
m_nCRC = nCRC;
}
protected PNGTrunk(int nSize, String szName, byte[] nData, byte[] nCRC) {
this(nSize, szName, nCRC);
m_nData = nData;
}
public int getSize() {
return m_nSize;
}
public String getName() {
return m_szName;
}
public byte[] getData() {
return m_nData;
}
public byte[] getCRC() {
return m_nCRC;
}
public void writeToStream(FileOutputStream outStream) throws IOException {
byte nSize[] = new byte[4];
nSize[0] = (byte) ((m_nSize & 0xFF000000) >> 24);
nSize[1] = (byte) ((m_nSize & 0xFF0000) >> 16);
nSize[2] = (byte) ((m_nSize & 0xFF00) >> 8);
nSize[3] = (byte) (m_nSize & 0xFF);
outStream.write(nSize);
outStream.write(m_szName.getBytes("ASCII"));
outStream.write(m_nData, 0, m_nSize);
outStream.write(m_nCRC);
}
public static void writeInt(byte[] nDes, int nPos, int nVal) {
nDes[nPos] = (byte) ((nVal & 0xff000000) >> 24);
nDes[nPos + 1] = (byte) ((nVal & 0xff0000) >> 16);
nDes[nPos + 2] = (byte) ((nVal & 0xff00) >> 8);
nDes[nPos + 3] = (byte) (nVal & 0xff);
}
public static int readPngInt(DataInputStream input) throws IOException {
final byte[] buffer = new byte[4];
input.readFully(buffer);
return readInt(buffer, 0);
}
public static int readInt(byte[] nDest, int nPos) { //读一个int
return ((nDest[nPos++] & 0xFF) << 24)
| ((nDest[nPos++] & 0xFF) << 16)
| ((nDest[nPos++] & 0xFF) << 8)
| (nDest[nPos] & 0xFF);
}
public static void writeCRC(byte[] nData, int nPos) {
int chunklen = readInt(nData, nPos);
int sum = CRCChecksum(nData, nPos + 4, 4 + chunklen) ^ 0xffffffff;
writeInt(nData, nPos + 8 + chunklen, sum);
}
public static int[] crc_table = null;
public static int CRCChecksum(byte[] nBuffer, int nOffset, int nLength) {
int c = 0xffffffff;
int n;
if (crc_table == null) {
int mkc;
int mkn, mkk;
crc_table = new int[256];
for (mkn = 0; mkn < 256; mkn++) {
mkc = mkn;
for (mkk = 0; mkk < 8; mkk++) {
if ((mkc & 1) == 1) {
mkc = 0xedb88320 ^ (mkc >>> 1);
} else {
mkc = mkc >>> 1;
}
}
crc_table[mkn] = mkc;
}
}
for (n = nOffset; n < nLength + nOffset; n++) {
c = crc_table[(c ^ nBuffer[n]) & 0xff] ^ (c >>> 8);
}
return c;
}
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
/**
* @author Rex
*/
public class PNGIHDRTrunk extends PNGTrunk {
public int m_nWidth;
public int m_nHeight;
public PNGIHDRTrunk(int nSize, String szName, byte[] nData, byte[] nCRC) {
super(nSize, szName, nData, nCRC);
m_nWidth = readInt(nData, 0);
m_nHeight = readInt(nData, 4);
}
}
pom依赖。
org.freemarker
freemarker
2.3.23
com.jcraft
jzlib
1.1.3
IPAUtil、IPA实体,见:https://blog.csdn.net/Mr_EvanChen/article/details/100565769