diff --git a/src/com/webcodepro/shrinkit/io/BitInputStream.java b/src/com/webcodepro/shrinkit/io/BitInputStream.java index c976432..07b6641 100644 --- a/src/com/webcodepro/shrinkit/io/BitInputStream.java +++ b/src/com/webcodepro/shrinkit/io/BitInputStream.java @@ -84,6 +84,15 @@ public class BitInputStream extends InputStream { data >>= requestedNumberOfBits; bitsOfData-= requestedNumberOfBits; return b; - } + } + + /** + * When shifting from buffer to buffer, the input stream also should be reset. + * This allows the "left over" bits to be cleared. + */ + public void clearRemainingBitsOfData() { + this.bitsOfData = 0; + this.data = 0; + } } diff --git a/src/com/webcodepro/shrinkit/io/LzwInputStream.java b/src/com/webcodepro/shrinkit/io/LzwInputStream.java index 03cc484..50a25f1 100644 --- a/src/com/webcodepro/shrinkit/io/LzwInputStream.java +++ b/src/com/webcodepro/shrinkit/io/LzwInputStream.java @@ -20,6 +20,7 @@ public class LzwInputStream extends InputStream { private BitInputStream is; private List dictionary; private Queue outputBuffer = new ConcurrentLinkedQueue(); + private boolean newBuffer = true; // See Wikipedia entry on LZW for variable naming private int k; private int[] w; @@ -57,11 +58,14 @@ public class LzwInputStream extends InputStream { dictionary = new ArrayList(); for (short i=0; i<256; i++) dictionary.add(new int[] { i }); dictionary.add(new int[] { 0x100 }); // 0x100 not used by NuFX + } + if (newBuffer) { // Setup for decompression; k = is.read(); outputBuffer.add(k); if (k == -1) return; w = new int[] { k }; + newBuffer = false; } // LZW decompression k = is.read(); @@ -102,5 +106,36 @@ public class LzwInputStream extends InputStream { */ public void clearDictionary() { dictionary = null; + is.setRequestedNumberOfBits(9); + is.clearRemainingBitsOfData(); + outputBuffer.clear(); + k = 0; + w = null; + entry = null; + newBuffer = true; + } + +// /** +// * Provide necessary housekeeping to reset LZW stream between NuFX buffer changes. +// * The dictionary is the only item that is not cleared -- that needs to be done +// * explicitly since behavior between LZW/1 and LZW/2 differ. +// */ +// public void resetState() { +// is.clearRemainingBitsOfData(); +// outputBuffer.clear(); +// k = 0; +// w = null; +// entry = null; +// newBuffer = true; +// } + + /** + * Provide necessary housekeeping to reset LZW stream between NuFX buffer changes. + * The dictionary is the only item that is not cleared -- that needs to be done + * explicitly since behavior between LZW/1 and LZW/2 differ. + */ + public void clearData() { + is.clearRemainingBitsOfData(); + outputBuffer.clear(); } } diff --git a/src/com/webcodepro/shrinkit/io/Lzw1InputStream.java b/src/com/webcodepro/shrinkit/io/NufxLzw1InputStream.java similarity index 91% rename from src/com/webcodepro/shrinkit/io/Lzw1InputStream.java rename to src/com/webcodepro/shrinkit/io/NufxLzw1InputStream.java index 7168dab..dda0693 100644 --- a/src/com/webcodepro/shrinkit/io/Lzw1InputStream.java +++ b/src/com/webcodepro/shrinkit/io/NufxLzw1InputStream.java @@ -6,7 +6,7 @@ import java.io.InputStream; import com.webcodepro.shrinkit.CRC16; /** - * The Lzw1InputStream reads a data fork or + * The NufxLzw1InputStream reads a data fork or * resource fork written in the NuFX LZW/1 format. *

* The layout of the LZW/1 data is as follows: @@ -45,7 +45,7 @@ import com.webcodepro.shrinkit.CRC16; * * @author robgreene@users.sourceforge.net */ -public class Lzw1InputStream extends InputStream { +public class NufxLzw1InputStream extends InputStream { /** This is the raw data stream with all markers and compressed data. */ private LittleEndianByteInputStream dataStream; /** Used for an LZW-only InputStream. */ @@ -70,7 +70,7 @@ public class Lzw1InputStream extends InputStream { /** * Create the LZW/1 input stream. */ - public Lzw1InputStream(LittleEndianByteInputStream dataStream) { + public NufxLzw1InputStream(LittleEndianByteInputStream dataStream) { this.dataStream = dataStream; } @@ -83,12 +83,13 @@ public class Lzw1InputStream extends InputStream { volumeNumber = dataStream.readByte(); rleCharacter = dataStream.readByte(); lzwStream = new LzwInputStream(new BitInputStream(dataStream, 9)); - rleStream = new RleInputStream(dataStream); + rleStream = new RleInputStream(dataStream, rleCharacter); lzwRleStream = new RleInputStream(lzwStream); } if (bytesLeftInChunk == 0) { // read the chunk header bytesLeftInChunk = 4096; // NuFX always reads 4096 bytes lzwStream.clearDictionary(); // Always clear dictionary +// lzwStream.newBuffer(); int length = dataStream.readWord(); int lzwFlag = dataStream.readByte(); int flag = lzwFlag + (length == 4096 ? 0 : 2); @@ -106,6 +107,7 @@ public class Lzw1InputStream extends InputStream { } // Now we can read a data byte int b = decompressionStream.read(); + bytesLeftInChunk--; dataCrc.update(b); return b; } @@ -137,10 +139,7 @@ public class Lzw1InputStream extends InputStream { public void setRleCharacter(int rleCharacter) { this.rleCharacter = rleCharacter; } - public CRC16 getDataCrc() { - return dataCrc; - } - public void setDataCrc(CRC16 dataCrc) { - this.dataCrc = dataCrc; + public long getDataCrc() { + return dataCrc.getValue(); } } diff --git a/src/com/webcodepro/shrinkit/io/NufxLzw2InputStream.java b/src/com/webcodepro/shrinkit/io/NufxLzw2InputStream.java new file mode 100644 index 0000000..a96d649 --- /dev/null +++ b/src/com/webcodepro/shrinkit/io/NufxLzw2InputStream.java @@ -0,0 +1,132 @@ +package com.webcodepro.shrinkit.io; + +import java.io.IOException; +import java.io.InputStream; + +import com.webcodepro.shrinkit.CRC16; + +/** + * The NufxLzw2InputStream reads a data fork or + * resource fork written in the NuFX LZW/2 format. + *

+ * The layout of the LZW/2 data is as follows: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
"Fork" Header
+0ByteLow-level volume number used to format 5.25" disks
+1ByteRLE character used to decode this thread
Each subsequent 4K chunk of data
+0WordBits 0-12: Length after RLE compression
+ * Bit 15: LZW flag (set to 1 if LZW used)
+2WordIf LZW flag = 1, total bytes in chunk
+ * Else (flag = 0) start of data
+ *

+ * The LZW/2 dictionary is only cleared when the table becomes full and is indicated + * in the input stream by 0x100. It is also cleared whenever a chunk that is not + * LZW encoded is encountered. + * + * @author robgreene@users.sourceforge.net + */ +public class NufxLzw2InputStream extends InputStream { + /** This is the raw data stream with all markers and compressed data. */ + private LittleEndianByteInputStream dataStream; + /** Used for an LZW-only InputStream. */ + private LzwInputStream lzwStream; + /** Used for an RLE-only InputStream. */ + private RleInputStream rleStream; + /** Used for an LZW+RLE InputStream. */ + private InputStream lzwRleStream; + /** This is the generic decompression stream from which we read. */ + private InputStream decompressionStream; + /** Counts the number of bytes in the 4096 byte chunk. */ + private int bytesLeftInChunk; + /** This is the volume number for 5.25" disks. */ + private int volumeNumber = -1; + /** This is the RLE character to use. */ + private int rleCharacter; + /** Used to track the CRC of data we've extracted */ + private CRC16 dataCrc = new CRC16(); + + /** + * Create the LZW/2 input stream. + */ + public NufxLzw2InputStream(LittleEndianByteInputStream dataStream) { + this.dataStream = dataStream; + } + + /** + * Read the next byte in the decompressed data stream. + */ + public int read() throws IOException { + if (volumeNumber == -1) { // read the data or resource fork header + volumeNumber = dataStream.readByte(); + rleCharacter = dataStream.readByte(); + lzwStream = new LzwInputStream(new BitInputStream(dataStream, 9)); + rleStream = new RleInputStream(dataStream, rleCharacter); + lzwRleStream = new RleInputStream(lzwStream); + } + if (bytesLeftInChunk == 0) { // read the chunk header + bytesLeftInChunk = 4096; // NuFX always reads 4096 bytes +// lzwStream.newBuffer(); // Allow the LZW stream to do a little housekeeping + lzwStream.clearData(); // Allow the LZW stream to do a little housekeeping + int word = dataStream.readWord(); + int length = word & 0x7fff; + int lzwFlag = word & 0x8000; + if (lzwFlag == 0) { // We clear dictionary whenever a non-LZW chunk is encountered + lzwStream.clearDictionary(); + } else { + dataStream.readWord(); // At this time, I just throw away the total bytes in this chunk... + } + int flag = (lzwFlag == 0 ? 0 : 1) + (length == 4096 ? 0 : 2); + switch (flag) { + case 0: decompressionStream = dataStream; + break; + case 1: decompressionStream = lzwStream; + break; + case 2: decompressionStream = rleStream; + break; + case 3: decompressionStream = lzwRleStream; + break; + default: throw new IOException("Unknown type of decompression, flag = " + flag); + } + } + // Now we can read a data byte + int b = decompressionStream.read(); + bytesLeftInChunk--; + dataCrc.update(b); + return b; + } + + // GENERATED CODE + + public int getVolumeNumber() { + return volumeNumber; + } + public void setVolumeNumber(int volumeNumber) { + this.volumeNumber = volumeNumber; + } + public int getRleCharacter() { + return rleCharacter; + } + public void setRleCharacter(int rleCharacter) { + this.rleCharacter = rleCharacter; + } + public long getDataCrc() { + return dataCrc.getValue(); + } +} diff --git a/test_src/com/webcodepro/shrinkit/io/APPLE.II-LZW1.SHK b/test_src/com/webcodepro/shrinkit/io/APPLE.II-LZW1.SHK new file mode 100644 index 0000000..203fea0 Binary files /dev/null and b/test_src/com/webcodepro/shrinkit/io/APPLE.II-LZW1.SHK differ diff --git a/test_src/com/webcodepro/shrinkit/io/APPLE.II-LZW2.SHK b/test_src/com/webcodepro/shrinkit/io/APPLE.II-LZW2.SHK new file mode 100644 index 0000000..fa49c96 Binary files /dev/null and b/test_src/com/webcodepro/shrinkit/io/APPLE.II-LZW2.SHK differ diff --git a/test_src/com/webcodepro/shrinkit/io/APPLE.II.txt b/test_src/com/webcodepro/shrinkit/io/APPLE.II.txt new file mode 100644 index 0000000..feac5b8 --- /dev/null +++ b/test_src/com/webcodepro/shrinkit/io/APPLE.II.txt @@ -0,0 +1 @@ +The first Apple II computers went on sale on June 6, 1977 with a MOS Technology 6502 microprocessor running at 1 MHz, 4 kB of RAM, an audio cassette interface for loading programs and storing data, and the Integer BASIC programming language built into the ROMs. The video controller displayed 24 lines by 40 columns of monochrome, upper-case-only text on the screen, with NTSC composite video output suitable for display on a monitor, or on a TV set by way of an RF modulator. The original retail price of the computer was US$1298 (with 4 KB of RAM) and US$2638 (with the maximum 48 KB of RAM). To reflect the computer's color graphics capability, the Apple logo on the casing was represented using rainbow stripes,[1] which remained a part of Apple's corporate logo until early 1998. The earliest Apple IIs were assembled in Silicon Valley, and later in Texas;[2] printed circuit boards were manufactured in Ireland and Singapore. In 1978, an external 5 1/4-inch floppy disk drive, the Disk II, attached via a controller card that plugged into one of the computer's expansion slots (usually slot 6), was used for data storage and retrieval to replace cassettes. The Disk II interface, created by Steve Wozniak, was regarded as an engineering masterpiece at the time for its economy of components.[3][4] While other controllers had dozens of chips for synchronizing data I/O with disk rotation, seeking the head to the appropriate track, and encoding the data into magnetic pulses, Wozniak's controller card had few chips; instead, the Apple DOS used software to perform these functions. The Group Code Recording used by the controller was simpler and easier to implement in software than the more common MFM. In the end, the low chip count of the controller contributed to making Apple's Disk II the first affordable floppy drive system for personal computers. As a side effect, Wozniak's scheme made it easy for proprietary software developers to copy-protect the media on which their software shipped by changing the low-level sector format or stepping the drive's head between the tracks; inevitably, other companies eventually sold software to foil this protection. Another Wozniak optimization allowed him to omit Shugart's Track-0 sensor. When the Operating System wants to go to track 0, the controller simply moves forty times toward the next-lower-numbered track, relying on the mechanical stop to prevent it going any further down than track 0. This process, called "recalibration", made a loud buzzing (rapid mechanical chattering) sound that often frightened Apple novices. The approach taken in the Disk II controller was typical of Wozniak's design sensibility. The Apple II was full of clever engineering tricks to save hardware and reduce costs. For example, taking advantage of the way that 6502 instructions only access memory every other clock cycle, the video generation circuitry's memory access on the otherwise unused cycles avoided memory contention issues and also eliminated the need for a separate refresh circuit for the DRAM chips. Rather than using a complex analog-to-digital circuit to read the outputs of the game controller, Wozniak used a simple timer circuit whose period was proportional to the resistance of the game controller, and used a software loop to measure the timer. The text and graphics screens had a somewhat outdated arrangement (the scanlines were not stored in sequential areas of memory) which was reputedly due to Wozniak's realization that doing it that way would save a chip; it was less expensive to have software calculate or look up the address of the required scanline than to include the extra hardware. Similarly, in the high-resolution graphics mode, color was determined by pixel position and could thus be implemented in software, saving Wozniak the chips needed to convert bit patterns to colors. This also allowed for sub-pixel font rendering since orange and blue pixels appeared half a pixel-width further to the right on the screen than green and purple pixels.[5] Color on the Apple II series took advantage of a quirk of the NTSC television signal standard, which made color display really easy (and cheap) to implement. The original NTSC television signal specification was black-and-white. Color was tacked on later by adding a 3.58 MHz subcarrier signal that was ignored by B&W TV sets. Color is encoded based on the phase of this signal in relation to a reference color burst signal. The result is that the position, size, and intensity of a series of pulses define color information. These pulses can translate into pixels on the computer screen. The Apple II display provided two pixels per subcarrier cycle. When the color burst reference signal was turned on and the computer attached to a color display, it could display green by showing one alternating pattern of pixels, magenta with an opposite pattern of alternating pixels, and white by placing two pixels next to each other. Later, blue and orange became available by tweaking the offset of the pixels by half a pixel-width in relation to the colorburst signal. The high-resolution enhanced display offered more colors simply by compressing more, narrower pixels into each subcarrier cycle. The coarse, low-resolution graphics display mode worked differently, as it could output a short burst of high-frequency signal per pixel to offer more color options. The epitome of the Apple II design philosophy was the Apple II sound circuitry. Rather than having a dedicated sound-synthesis chip, the Apple II had a toggle circuit that could only emit a click through a built-in speaker or a line out jack; all other sounds (including two, three and, eventually, four-voice music and playback of audio samples and speech synthesis) were generated entirely by clever software that clicked the speaker at just the right times. Not for nearly a decade would an Apple II be released with a dedicated sound chip. Similar techniques were used for cassette storage: the cassette output worked the same as the speaker, and the input was a simple zero-crossing detector that served as a relatively crude (1-bit) audio digitizer. Routines in the ROM were used to encode and decode data in frequency shift keying for the cassette. Wozniak's open design and the Apple II's multiple expansion slots permitted a wide variety of third-party devices to expand the capabilities of the machine. Apple II peripheral cards such as Serial controllers, improved display controllers, memory boards, hard disks, and networking components were available for this system in its day. There were emulator cards, such as the Z80 card that permitted the Apple to switch to the Z80 processor and run a multitude of programs developed under the CP/M operating system, including the dBase II database and the WordStar word processor. (At one point in the mid-1980s, more than half the machines running CP/M were Apple II's with Z80 cards.)There was also a third-party 6809 card that would allow OS-9 Level One to be run. The Mockingboard sound card greatly improved the audio capabilities of the Apple, with simple music synthesis and text-to-speech functions. Eventually, Apple II accelerator cards were created to double or quadruple the computer's speed. SOURCE: WIKIPEDIA 6/2008 \ No newline at end of file diff --git a/test_src/com/webcodepro/shrinkit/io/Lzw1Test.java b/test_src/com/webcodepro/shrinkit/io/NufxLzw1Test.java similarity index 60% rename from test_src/com/webcodepro/shrinkit/io/Lzw1Test.java rename to test_src/com/webcodepro/shrinkit/io/NufxLzw1Test.java index c6dd857..bcd698a 100644 --- a/test_src/com/webcodepro/shrinkit/io/Lzw1Test.java +++ b/test_src/com/webcodepro/shrinkit/io/NufxLzw1Test.java @@ -1,15 +1,21 @@ package com.webcodepro.shrinkit.io; import java.io.IOException; +import java.util.List; + +import com.webcodepro.shrinkit.HeaderBlock; +import com.webcodepro.shrinkit.NuFileArchive; +import com.webcodepro.shrinkit.ThreadKind; +import com.webcodepro.shrinkit.ThreadRecord; /** * Test some LZW/1 format streams. * * @author robgreene@users.sourceforge.net */ -public class Lzw1Test extends TestCaseHelper { +public class NufxLzw1Test extends TestCaseHelper { public void testTextFile() throws IOException { - Lzw1InputStream is = new Lzw1InputStream(new LittleEndianByteInputStream(getTextFileLzw1StreamData())); + NufxLzw1InputStream is = new NufxLzw1InputStream(new LittleEndianByteInputStream(getTextFileLzw1StreamData())); byte[] expected = getTextFileData(); byte[] actual = new byte[expected.length]; is.read(actual); @@ -17,6 +23,27 @@ public class Lzw1Test extends TestCaseHelper { assertTrue(is.isCrcValid()); } + public void testAppleIIShk() throws IOException { + NuFileArchive archive = new NuFileArchive(getClass().getResourceAsStream("APPLE.II-LZW1.SHK")); + List blocks = archive.getHeaderBlocks(); + HeaderBlock block = blocks.get(0); // only one file + if (block.getFilename() != null) System.out.printf("\n\n%s\n\n", block.getFilename()); + List records = block.getThreadRecords(); + for (ThreadRecord record : records) { + if (record.getThreadKind() == ThreadKind.FILENAME) { + System.out.printf("\n\n%s\n\n", record.getText()); + } + long bytes = record.getThreadEof(); + if (record.getThreadKind() == ThreadKind.DATA_FORK) { + NufxLzw1InputStream is = new NufxLzw1InputStream(new LittleEndianByteInputStream(record.getRawInputStream())); + while ( bytes-- > 0 ) { + System.out.print((char)is.read()); + } + } + } + } + + private byte[] getTextFileLzw1StreamData() { return new byte[] { (byte)0xCA, 0x42, 0x00, (byte)0xDB, (byte)0xB7, 0x00, 0x01, 0x54, diff --git a/test_src/com/webcodepro/shrinkit/io/NufxLzw2Test.java b/test_src/com/webcodepro/shrinkit/io/NufxLzw2Test.java new file mode 100644 index 0000000..e7dfb56 --- /dev/null +++ b/test_src/com/webcodepro/shrinkit/io/NufxLzw2Test.java @@ -0,0 +1,33 @@ +package com.webcodepro.shrinkit.io; + +import java.io.IOException; +import java.util.List; + +import junit.framework.TestCase; + +import com.webcodepro.shrinkit.HeaderBlock; +import com.webcodepro.shrinkit.NuFileArchive; +import com.webcodepro.shrinkit.ThreadKind; +import com.webcodepro.shrinkit.ThreadRecord; + +public class NufxLzw2Test extends TestCase { + public void testPascalFile() throws IOException { + NuFileArchive archive = new NuFileArchive(getClass().getResourceAsStream("APPLE.II-LZW2.SHK")); + List blocks = archive.getHeaderBlocks(); + HeaderBlock block = blocks.get(0); + if (block.getFilename() != null) System.out.printf("\n\n%s\n\n", block.getFilename()); + List records = block.getThreadRecords(); + for (ThreadRecord record : records) { + if (record.getThreadKind() == ThreadKind.FILENAME) { + System.out.printf("\n\n%s\n\n", record.getText()); + } + long bytes = record.getThreadEof(); + if (record.getThreadKind() == ThreadKind.DATA_FORK) { + NufxLzw2InputStream is = new NufxLzw2InputStream(new LittleEndianByteInputStream(record.getRawInputStream())); + while ( bytes-- > 0 ) { + System.out.print((char)is.read()); + } + } + } + } +}