/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2000-2011 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine is distributed in the hope that it will *
* be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.android.fontgen;
import totalcross.*;
import android.os.*;
import java.io.*;
import java.util.*;
import java.util.zip.*;
/**
A tcz (TotalCross Zip) file has the following format:
<ul>
<li> version (2 bytes)
<li> attributes (2 bytes)
<li> base offset (header size + 4)
<li> header
<li> compressed data chunks
</ul>
The header is:
<ul>
<li> length (4 bytes)
<li> offsets array (length+1) - offset[i+1]-offset[i] gives the compressed size
<li> uncompressed sizes array (length)
<li> names array (length)
</ul>
The header is compressed to save space.
The first record is the class that implements totalcross.MainClass or extends totalcross.ui.MainWindow.
*/
public class TCZ
{
/** Set this to be the main class name. It is stored in record #0. */
public static String mainClassName; // remove dependency on DeploySettings
/** An entry of the TCZ file. */
public static class Entry
{
/** The compressed byte block. */
public byte[] bytes;
/** The name of the entry. */
public String name;
/** The size of the block when it is uncompressed. */
public int uncompressedSize;
/** Anything you want to hold here. */
public Object extra; // JavaClass for the converter
private String name2write; // guich@tc115_23
public Entry(byte []bytes, String name, int uncompressedSize) throws Exception
{
this(bytes, name, uncompressedSize, null);
}
public Entry(byte []bytes, String name, int uncompressedSize, Object extra) throws Exception
{
this.uncompressedSize = uncompressedSize;
this.bytes = bytes;
this.name = name.replace('\\','/');
this.extra = extra;
if (name.endsWith(".class"))
name = name.substring(0,name.length()-6);
name2write = name;
if (name2write.indexOf('.') < 0) // no dots on string? note that an image will have a dot, so its correctly kept with / on the name
name2write = name2write.replace('/','.'); // assuming not ending with .class
}
/** Returns a String representing this Entry. Used in the Vector.qsort method */
public String toString()
{
if (mainClassName != null && name.equals(mainClassName))
return '\1'+name2write; // make sure this name will go first when sorting
return name2write;
}
}
public static final short TCZ_VERSION = 106; // must sync with tcz.h
/** Defines that the tcz file has a MainClass at the first record. If false, it is a library-only module */
public static final short ATTR_HAS_MAINCLASS = 1;
/** Defines that the tcz file has a MainWindow at the first record. */
public static final short ATTR_HAS_MAINWINDOW = 2;
/** Defines that the tcz file is a library-only module. */
public static final short ATTR_LIBRARY = 4;
/** The names of the files. */
public String[] names;
/** The attributes.
* @see #ATTR_HAS_MAINCLASS
* @see #ATTR_HAS_MAINWINDOW
* @see #ATTR_LIBRARY
*/
public int attr;
/** Version of the tcz file.
* @see #TCZ_VERSION
*/
public int version;
/** Offsets to the compressed data chunks */
public int[] offsets;
/** Sizes of the data chunks when they are uncompressed. */
public int[] uncompressedSizes;
/** The number of chunks. */
public int numberOfChunks;
/** Stores the total size when a TCZ file is created. */
public int size;
/** Bag that can be used to store anything that the user wants. */
public Object bag;
/** Create a TCZ file with the given vector of Entry(s).
* @throws IOException
* @throws ZipException */
public TCZ(Vector<Entry> vout, String outName, short attr) throws IOException
{
if (!outName.toLowerCase().endsWith(".tcz"))
outName = outName.concat(".tcz");
// creates an empty file for output
String ss = Environment.getExternalStorageDirectory()+"/"+"_"+outName;
FileOutputStream fout = new FileOutputStream(ss);
Object[] out = vout.toArray();
sort(out,0,out.length-1);
// now we process the files.
int n = out.length;
// first pass, we setup the names and offset arrays
offsets = new int[n+1]; // first offset is 0
names = new String[n];
uncompressedSizes = new int[n];
int ofs = 0;
for (int i =0; i < n; i++)
{
Entry of = (Entry)out[i];
names[i] = of.name2write;
ofs += of.bytes.length;
offsets[i+1] = ofs;
uncompressedSizes[i] = of.uncompressedSize;
}
// prepare the header
ByteArrayOutputStream header = new ByteArrayOutputStream(4096);
writeInt(header,n);
for (int i = 0; i <= n; i++)
writeInt(header,offsets[i]); // the offsets are stored first to ensure alignment.
for (int i = 0; i < n; i++)
writeInt(header,uncompressedSizes[i]); // idem
for (int i = 0; i < n; i++) // strings are stored as UTF8
{
String s = names[i];
int l = s.length();
if (l > 255) throw new ZipException("Error: Name too long: "+s+". ("+l+" chars). Maximum allowed: 255 chars");
writeSmallString(header,s);
}
byte[] hh = header.toByteArray();
byte[] compressedHeaderBytes = compress(hh);
AndroidUtils.debug("header: "+hh.length+" -> "+compressedHeaderBytes.length);
// write the first part: the version, the attribute and the compressed header
size += writeShort(fout, TCZ_VERSION);
size += writeShort(fout, attr);
size += writeInt(fout, compressedHeaderBytes.length+8); // base offset = compressed header size + 4
fout.write(compressedHeaderBytes, 0, compressedHeaderBytes.length);
size += compressedHeaderBytes.length;
// now write the compressed chunks
for (int i = 0; i < n; i++)
{
byte[] bytes = ((Entry)out[i]).bytes;
fout.write(bytes);
size += bytes.length;
}
fout.close();
AndroidUtils.debug("OUTPUT: "+ss);
}
public static byte[] compress(byte[] in) throws IOException
{
ByteArrayOutputStream bc = new ByteArrayOutputStream(in.length/2);
Deflater dd = new Deflater(9, false);
DeflaterOutputStream def = new DeflaterOutputStream(bc, dd);
def.write(in);
def.finish();
dd.end();
return bc.toByteArray();
}
private static void sort(Object []items, int first, int last)
{
if (first >= last)
return;
int low = first;
int high = last;
String mid = items[(first+last) >> 1].toString();
while (true)
{
while (high >= low && mid.compareTo(items[low].toString()) > 0) // guich@566_25: added "high > low" here and below - guich@568_5: changed to >=
low++;
while (high >= low && mid.compareTo(items[high].toString()) < 0)
high--;
if (low <= high)
{
Object temp = items[low];
items[low++] = items[high];
items[high--] = temp;
}
else break;
}
if (first < high)
sort(items,first,high);
if (low < last)
sort(items,low,last);
}
public static int writeShort(OutputStream stream, int i) throws IOException
{
byte[] b = buffer;
b[0] = (byte)i;
i >>= 8;
b[1] = (byte)i;
stream.write(b, 0, 2);
return 2;
}
static private byte[] buffer = new byte[256]; // starts with 256 bytes since readSmallString uses it
public static int writeSmallString(OutputStream stream, String s) throws IOException
{
int len = s == null ? 0 : s.length();
if (s.length() > 255) throw new IOException("String size "+s.length()+" is too big to use with writeSmallString!");
int pos = 0;
buffer[pos++] = (byte)len;
int ret = len+1;
if (len > 0)
for (int i = 0; len-- > 0; i++)
buffer[pos++] = (byte)s.charAt(i);
stream.write(buffer, 0, pos);
return ret;
}
public static int writeInt(OutputStream stream, int i) throws IOException
{
byte[] b = buffer;
b[0] = (byte)i;
i >>= 8; // guich@300_40
b[1] = (byte)i;
i >>= 8;
b[2] = (byte)i;
i >>= 8;
b[3] = (byte)i;
stream.write(b, 0, 4);
return 4;
}
}