diff --git a/src/com/webcodepro/shrinkit/io/RleOutputStream.java b/src/com/webcodepro/shrinkit/io/RleOutputStream.java new file mode 100644 index 0000000..b365245 --- /dev/null +++ b/src/com/webcodepro/shrinkit/io/RleOutputStream.java @@ -0,0 +1,82 @@ +package com.webcodepro.shrinkit.io; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * The RleOutputStream handles the NuFX RLE data stream. + * This data stream is byte oriented. If a repeat occurs, + * the data stream will contain the marker byte, byte to + * repeat, and the number of repeats (zero based; ie, $00=1, + * $01=2, ... $ff=256). The default marker is $DB. + * + * @author robgreene@users.sourceforge.net + */ +public class RleOutputStream extends OutputStream { + private OutputStream os; + private int escapeChar; + private int repeatedByte; + private int numBytes = -1; + + /** + * Create an RLE output stream with the default marker byte. + */ + public RleOutputStream(OutputStream bs) { + this(bs, 0xdb); + } + /** + * Create an RLE output stream with the specified marker byte. + */ + public RleOutputStream(OutputStream os, int escapeChar) { + this.os = os; + this.escapeChar = escapeChar; + } + + /** + * Write the next byte to the output stream. + */ + public void write(int b) throws IOException { + if (numBytes == -1) { + repeatedByte = b; + numBytes++; + } else if (repeatedByte == b) { + numBytes++; + if (numBytes > 255) { + flush(); + } + } else { + flush(); + repeatedByte = b; + numBytes++; + } + } + + /** + * Flush out any remaining data. + * If we only have 1 byte and it is not the repeated + * byte, we can just dump that byte. Otherwise, we need to + * write out the escape character, the repeated byte, and + * the number of bytes. + */ + public void flush() throws IOException { + if (numBytes != -1) { + if (numBytes == 0 && escapeChar != repeatedByte) { + os.write(repeatedByte); + } else { + os.write(escapeChar); + os.write(repeatedByte); + os.write(numBytes); + } + numBytes = -1; + } + } + + /** + * Close out the data stream. Makes sure the repeate buffer + * is flushed. + */ + public void close() throws IOException { + flush(); + os.close(); + } +} diff --git a/test_src/com/webcodepro/shrinkit/io/RleTest.java b/test_src/com/webcodepro/shrinkit/io/RleTest.java index 7b99e6f..be4022c 100644 --- a/test_src/com/webcodepro/shrinkit/io/RleTest.java +++ b/test_src/com/webcodepro/shrinkit/io/RleTest.java @@ -1,5 +1,6 @@ package com.webcodepro.shrinkit.io; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -11,23 +12,59 @@ import java.io.OutputStream; * @author robgreene@users.sourceforge.net */ public class RleTest extends TestCaseHelper { + /** + * Test the RleInputStream to verify decoding. + */ public void testInputStream() throws IOException { - InputStream is = new RleInputStream(new LittleEndianByteInputStream(getPatternFileRle())); + InputStream is = new RleInputStream(new ByteArrayInputStream(getPatternFileRle())); ByteArrayOutputStream os = new ByteArrayOutputStream(); copy(is,os); byte[] expected = getPatternFileUncompressed(); byte[] actual = os.toByteArray(); assertEquals(expected, actual); } + /** + * Test the RleOutputStream to verify compression. + */ + public void testOutputStream() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + RleOutputStream os = new RleOutputStream(baos); + ByteArrayInputStream is = new ByteArrayInputStream(getPatternFileUncompressed()); + copy(is,os); + byte[] expected = getPatternFileRle(); + byte[] actual = baos.toByteArray(); + assertEquals(expected, actual); + } + /** + * Test the RleOutputStream with the escape character to ensure it always is encoded. + * Note that a file with lots of 0xdb codes will grow. + */ + public void testOutputStreamEscapeCharacter() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + RleOutputStream os = new RleOutputStream(baos); + ByteArrayInputStream is = new ByteArrayInputStream(new byte[] { (byte)0xdb, (byte)0xdb, 0x00, (byte)0xdb }); + copy(is,os); + byte[] expected = new byte[] { (byte)0xdb, (byte)0xdb, 0x01, 0x00, (byte)0xdb, (byte)0xdb, 0x00 }; + byte[] actual = baos.toByteArray(); + assertEquals(expected, actual); + } + /** + * Copy the input stream to the output stream. + */ private void copy(InputStream is, OutputStream os) throws IOException { int b = is.read(); while (b != -1) { os.write(b); b = is.read(); } + is.close(); + os.close(); } - + + /** + * This the RLE compressed pattern file that happens to not compress via LZW. + */ private byte[] getPatternFileRle() { return new byte[] { (byte)0xdb, 0x01, (byte)0xfd, @@ -49,6 +86,9 @@ public class RleTest extends TestCaseHelper { (byte)0xdb, 0x11, (byte)0x97 }; } + /** + * This is the uncompressed pattern file that happens to not compress via LZW. + */ private byte[] getPatternFileUncompressed() { byte[] data = new byte[4096]; int value = 0x01;