LZ77 Compression for XML files

From GMpedia.org Wiki

Jump to: navigation, search

Flash can handle most of the common media formats which are already compressed like MP3 or JPG and since version 8 also PNG and GIF can be loaded dynamically into a Flash file. An exception to this is the poplar XML format which is uncompressed by nature.
This is ok for small XML files but when using large XML files (which in our case can contain game related data like map layouts and properties) loading time can suffer for online published games. Either you want to have your XML files compressed for smaller size or just for the purpose that nobody can read the contents immediately, a XML compression routine is a useful option.
LZ77 to the rescue! The LZ77 compression algorithm is simple and fast enough so that ActionScript can handle it easily.

There is a LZ77 compressor and decompression routine in AS1.0 written by Strille at www.strille.net/tutorials/FlashXMLCompressor/ but the compressor executable only works if you have the Microsoft .NET Framework v1.1 installed (it does not work with the new version 2.0). There is also a Java class that can be compiled with the JDK compiler and it works flawless.

[edit] The Java part

I rewrote the Java version a bit to fit my needs. Here is the source code of FlashXMLCompressor.java ...

/*
 * Flash LZ77 XML Compressor - Java version by Strille, 2004
 * Home Page: http://www.strille.net/tutorials/FlashXMLCompressor/
 * Slight modifications by S. Balkau.
 */

import java.io.*;


public class FlashXMLCompressor
{
	/**
	 * main()
	 */
	public static void main(String args[])
	{
		System.out.println("Flash XML Compressor v1.0, Java version by Strille, 2004");
		
		// Check if arguments were given:
		if (args.length > 0)
		{
			String fileName = args[0];
			String outFile;

			// Define output filename:
			if (args.length == 1)
			{
				if (fileName.lastIndexOf(".") != -1)
				{
					outFile = fileName.substring(0, fileName.lastIndexOf(".")) + ".cpr";
				}
				else
				{
					outFile = fileName + ".cpr";
				}
			}
			else
			{
				outFile = args[1];
			}

			String text = loadFile(fileName);

			if (text.length() > 0)
			{
				System.out.println("Encoding " + fileName + " ...");
				String encoded = encode(text);
				saveFile(encoded, outFile);

				double percentage = 100 * encoded.length() / text.length();
				System.out.println("Compressed file: " + outFile
					+ " (" + percentage + "% of the original)");
			}
		}
		else
		{
			System.out.println("No filename specified!");
			System.out.println("Usage: java FlashXMLCompressor <source filename>"
				+ " <optional: new filename>");
		}
	}
	
	
	
	/**
	 * encode()
	 */
	private static String encode(String m)
	{
		String outComp = m.substring(0, 1);
		String encodeChar = null;

		// Find a character not in the file to use in the encoding formatting:
		for (int n = 161; n < 191; n++)
		{
			String c = new Character((char) n).toString();

			if (m.indexOf(c) == -1)
			{
				encodeChar = c;
				break;
			}
		}

		if (encodeChar == null)
		{
			System.out.println("Error: This file can't be compressed (No free character found)");
			return "";
		}

		int radix = 114;
		int bufferSize = radix * radix - 1;
		int currPos = 1;
		int codingBufferSize = radix - 1;
		String codingA = m.substring(currPos, Math.min(m.length(), currPos + codingBufferSize));

		while (codingA.length() > 0)
		{
			int res = encodeFindBest(m, Math.max(currPos - bufferSize, 0), currPos - 1, codingA);
			if (res == -1)
			{
				outComp += m.charAt(currPos);
				currPos++;
			}
			else
			{
				int p = res / 1000;
				int l = res - p * 1000;
				p = currPos - p;
				int ms = (int) (p / radix);
				int ls = p - ms * radix;
				outComp += encodeChar + new Character((char) (14 + ms)).toString() +
					new Character((char) (14 + ls)).toString()
						+ new Character((char) (14 + l)).toString();
				currPos += l;
			}

			codingA = m.substring(currPos, Math.min(m.length(), currPos + codingBufferSize));
		}

		return "XCPR" + encodeChar + outComp;
	}
	
	
	
	/**
	 * encodeFindBest()
	 */
	private static int encodeFindBest(String buf, int winS, int winE, String cA)
	{
		if (winE > buf.length())
		{
			winE = buf.length();
		}

		int lookAheadLength = cA.length();
		String winStr = buf.substring(winS, winE);
		int sequenceLength = 5;
		int matchLength = 0;
		int matchPos = 0;
		int n;

		while (sequenceLength <= lookAheadLength)
		{
			n = winStr.indexOf(cA.substring(0, sequenceLength));
			if (n == -1)
			{
				break;
			}
			else
			{
				matchLength = sequenceLength;
				matchPos = n;
			}
			sequenceLength++;
		}

		if (matchLength > 0)
		{
			return 1000 * (winS + matchPos) + matchLength;
		}
		else
		{
			return -1;
		}
	}
	
	
	
	/**
	 * loadFile()
	 */
	private static String loadFile(String inputFileName)
	{
		String inputText = "";

		try
		{
			FileReader inputFileReader = new FileReader(inputFileName);
			BufferedReader inputStream = new BufferedReader(inputFileReader);

			String inLine = null;
			while ((inLine = inputStream.readLine()) != null)
			{
				inputText += inLine;
			}

			// Strip tabs
			int tabPos = -1;
			while ((tabPos = inputText.indexOf("\t")) != -1)
			{
				inputText = inputText.substring(0, tabPos)
					+ inputText.substring(tabPos + 1, inputText.length());
			}

			inputStream.close();
		}
		catch (IOException e)
		{
			System.out.println("Error: " + e.getMessage());
		}

		return inputText;
	}
	
	
	
	/**
	 * saveFile()
	 */
	private static void saveFile(String txt, String fileName)
	{
		try
		{
			FileWriter outputFileReader = new FileWriter(fileName);
			PrintWriter outputStream = new PrintWriter(outputFileReader);
			outputStream.print(txt);
			outputStream.close();
		}
		catch (IOException e)
		{
			System.out.println("Error: " + e.getMessage());
		}
	}
}

This source can be compiled with the Java compiler if you have the JDK installed. To compile, open a DOS prompt, navigate to the folder with the class and type:

javac FlashXMLCompressor.java

After the class compiled successfully you will have another file called FlashXMLCompressor.class which is the compiled bytecode of the XML compressor. It is helpful to write a small batchfile to compress your XML files, for example like:

java FlashXMLCompressor test.xml

Depending on the size of the XML file, it will be compressed quite well. Note that the compressed file will get the .cpr suffix (Flash doesn't mind a different suffix). If you want another suffix you can easily change it in the Java source file. Another thing that I changed is that the compressed file's first four bits act as some sort of identification header and are named 'XCPR' This also can be changed in the Java file.

Why not using ActionScript?
I've tried to write a compressor in AS2.0 based on the Java source shown above but since AS is relatively slow and takes very long to compress even a small XML file it doesn't make sense to use Actionscript for it. If it takes longer than 10 seconds, Flash Player will fire up it's own SandBox security limitation and displays a popup requester noting that the player eventually hung up. Even if it is theoretically possible to trick this limitation by distribute the compression process onto an onEnterFrame loop, it would still take too long to compress to be productive.
I'm not aware on how it would look if written in AS 3.0 but it might be worth a try since AS 3.0 is alot faster and optimized.


[edit] The ActionScript part

The following is an AS 2.0 class as an example on how to load the compressed XML files into Flash. I've wrote it as part of the tile scrolling engine that I'm working on. All it does is overriding the onData method of the XML object. The onData method is called as soon as XML data is finished loading.

MapXML.as

/**
 * MapXML Class
 * Loads and processes compressed and uncompressed map XML files.
 * @version		1.0.0 - 07.12.2005
 * @author			S. Balkau
 */
class MapXML extends XML
{
	
	/**
	 * onData()
	 * Overridden onData that can handle LZ77 compressed XML.
	 * @param	src	The file that contains the map XML data.
	 */
	private function onData(src:String):Void
	{
		// If the data start with 'XCPR' it is compressed, so execute decompression routine:
		if (src.substr(0, 4) == "XCPR")
		{
			var ecPos:Number = src.indexOf("R") + 1;
			var eChar:String = src.charAt(ecPos);
			src = src.substr(ecPos + 1);
			var srcLen:Number = src.length;
			var out:String = "";
			
			for (var i:Number = 0; i < srcLen; i++)
			{
				if (src.charAt(i) == eChar)
				{
					var p:Number = src.charCodeAt(i + 1) * 114 + src.charCodeAt(i + 2) - 1610;
					var l:Number = src.charCodeAt(i + 3) - 14;
					out += out.substr(-p, l);
					i += 3;
				}
				else
				{
					out += src.charAt(i);
				}
			}
			this.parseXML(out);
		}
		// Uncompressed XML can be parsed immediately:
		else
		{
			this.parseXML(src);
		}
		
		this.loaded = (src != undefined);
		this.onLoad(src != undefined);
	}
}

If you study the code you see that only files which start with the header 'XCPR' are processed. Other files are untouched by the decompression so it is possible to still load uncompressed XML.

Here is a small example on how to use the MapXML class:

class Test
{
	private var mapDataXML:MapXML;

	public function Test()
	{
		mapDataXML = new MapXML();
		mapDataXML.ignoreWhite = true;
		mapDataXML.onLoad = processMapData;
		mapDataXML.load("test.cpr");
	}
	
	private function processMapData(success:Boolean):Void
	{
		if (success)
		{
			// Proceed with parsing the XML data ...
		}
		else
		{
			trace("Load Error - Map file not found!");
		}
	}
}

You can see that it doesn't take much effort in your projects to load compressed XML from now on. All to do is using the MapXML class instead of Flash's own XML class. With these example codes you are equipped to write your own decompression class and use compressed XML files.

--Sascha


[edit] External Links

Personal tools